Module: Lich::Gemstone::Combat::Parser
- Defined in:
- documented/gemstone/combat/parser.rb
Constant Summary collapse
- TARGET_LINK_PATTERN =
Target link pattern - extract creatures/players from XML
/<a exist="(?<id>[^"]+)" noun="(?<noun>[^"]+)">(?<name>[^<]+)<\/a>/i.freeze
- BOLD_WRAPPER_PATTERN =
Bold tag pattern - creatures are wrapped in bold tags Non-greedy match to avoid spanning multiple creatures Allow zero or more characters before tag (e.g., "a creature" or just "creature")
/<pushBold\/>([^<]*<a exist="[^"]+"[^>]+>[^<]+<\/a>)<popBold\/>/i.freeze
- SELF_TARGET_PATTERN =
A target capture that refers to US rather than a creature. Anchored at the start of the capture: real forms observed in session logs are "you", "your
" and tails like "you with its tusk" - but NOT a creature whose name merely contains the word ("a youngling"), hence the \b and the anchor. /\A(?:you|your)\b/i.freeze
- SELF_IN_PATTERN =
Some inbound defs name us in the pattern LITERAL rather than in a target capture ("
springs from the shadows and strikes at you!", "... plants ... near you"). They have an attacker capture and no target capture, so without this they too fall through to the line-scan and self-attribute. A def that captures an attacker and addresses "you"/"your" in its own source is aimed at us by construction. /\b(?:at|towards?|upon|around|near|on)\s+your?\b/i.freeze
- SWING_WEAPON_PATTERN =
The weapon a swing line names, in plain text (swing lines do not link the weapon): "You swing a kelyn-edged slim short sword at
..." - used to claim pre-flares by weapon. /You(?: take aim and)? (?:swing|fire) (?:an? |your |some )?(?<weapon>[^<]+?) at </.freeze
Class Method Summary collapse
-
.extract_attacker_from_match(match) ⇒ Object
Attacker from a third-person def's (?
...) capture. -
.extract_creature_target(line) ⇒ Object
Extract creature target (must be wrapped in bold tags).
-
.extract_target_from_line(line) ⇒ Hash?
Extracts a creature target from a combat line by looking for a bolded link.
-
.extract_target_from_match(match) ⇒ Object
Try to extract target from regex match first, then from line.
-
.inbound_attack?(line) ⇒ Boolean
True when this line is an attack aimed at US.
-
.parse_assault_end(line) ⇒ Hash?
Parses a single-target multi-round assault bracket closing line.
-
.parse_assault_start(line) ⇒ Object
Single-target multi-round assault brackets (flurry, barrage, pummel, guardant thrusts, thrash) - see defs/assaults.rb.
-
.parse_attack(line) ⇒ Object
Parse attack initiation.
-
.parse_damage(line) ⇒ Object
Parse damage amounts using damage definitions.
-
.parse_flare(line) ⇒ Object
Parse a flare announce line (weapon scripts, enchants, GEFs, flourishes).
-
.parse_outcome(line) ⇒ Object
Why a swing produced nothing: :evade, :block, :parry, :warded, :miss, :fumble, :hindrance, :confused.
-
.parse_resolution(line) ⇒ Object
Roll lines: AS/DS, CS/TD, UAF/UDF, SMR - returns the full breakdown { type:, as:, ds:, avd:, roll:, result: ... }.
-
.parse_sequence_end(line) ⇒ Hash?
Parses a multi-target sequence bracket closing line.
-
.parse_sequence_start(line) ⇒ Object
Multi-TARGET action brackets (mstrike, volley, spawned casts).
-
.parse_spell_loss(line) ⇒ Object
Third-person spell wear-off lines pinned to a spell number (defs/spell_losses.rb).
-
.parse_status(line) ⇒ Object
Parse status effects (optional - performance setting).
-
.parse_swing_weapon(line) ⇒ String?
Extracts the weapon name from a swing or ranged attack line.
-
.parse_ucs(line) ⇒ Object
Parse UCS events (position, tierup, smite).
-
.self_target?(match) ⇒ Boolean
Determines whether a regex match names the player as its target.
-
.strip_links(text) ⇒ Object
Drop XML link/bold markup from a captured fragment.
Class Method Details
.extract_attacker_from_match(match) ⇒ Object
Attacker from a third-person def's (?
187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'documented/gemstone/combat/parser.rb', line 187 def extract_attacker_from_match(match) return nil unless match.names.include?('attacker') text = match[:attacker] return nil if text.nil? || text.strip.empty? if (link = TARGET_LINK_PATTERN.match(text)) { id: link[:id].to_i, noun: link[:noun], name: link[:name] } else { name: strip_links(text).strip } end end |
.extract_creature_target(line) ⇒ Object
Extract creature target (must be wrapped in bold tags)
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 |
# File 'documented/gemstone/combat/parser.rb', line 322 def extract_creature_target(line) # Cheap gate: the wrapper pattern can't match without the bold tag, # and the substring check avoids two regexes on most lines return nil unless line.include?('<pushBold/>') # Check if line contains a bolded link bold_match = BOLD_WRAPPER_PATTERN.match(line) return nil unless bold_match # Extract the link from within the bold tags link_text = bold_match[1] link_match = TARGET_LINK_PATTERN.match(link_text) return nil unless link_match id = link_match[:id].to_i return nil if id <= 0 # Skip invalid IDs { id: id, noun: link_match[:noun], name: link_match[:name] } end |
.extract_target_from_line(line) ⇒ Hash?
Extracts a creature target from a combat line by looking for a bolded link.
Only creature links wrapped in
374 375 376 377 378 |
# File 'documented/gemstone/combat/parser.rb', line 374 def extract_target_from_line(line) # ONLY accept bolded creatures as targets # Non-bolded links are equipment, objects, or other non-combatants extract_creature_target(line) end |
.extract_target_from_match(match) ⇒ Object
Try to extract target from regex match first, then from line
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 |
# File 'documented/gemstone/combat/parser.rb', line 347 def extract_target_from_match(match) return nil unless match.names.include?('target') target_text = match[:target] return nil if target_text.nil? || target_text.strip.empty? # Look for creature in target text if (target_match = TARGET_LINK_PATTERN.match(target_text)) id = target_match[:id].to_i return nil if id < 0 return { id: id, noun: target_match[:noun], name: target_match[:name] } end nil end |
.inbound_attack?(line) ⇒ Boolean
True when this line is an attack aimed at US. Used by the processor to keep the attacker's own link out of the target-switcher (see the inbound_line note there).
173 174 175 176 177 178 179 180 181 182 |
# File 'documented/gemstone/combat/parser.rb', line 173 def inbound_attack?(line) return false if Definitions::Attacks.rejects?(line) Definitions::Attacks::ATTACK_LOOKUP.each do |pattern, _name| if (match = pattern.match(line)) return self_target?(match) end end false end |
.parse_assault_end(line) ⇒ Hash?
Parses a single-target multi-round assault bracket closing line.
Identifies the end of assault sequences (flurry, barrage, pummel, guardant thrusts, thrash) that span multiple rounds against one target.
261 262 263 |
# File 'documented/gemstone/combat/parser.rb', line 261 def parse_assault_end(line) Definitions::Assaults.parse_end(line) end |
.parse_assault_start(line) ⇒ Object
Single-target multi-round assault brackets (flurry, barrage, pummel, guardant thrusts, thrash) - see defs/assaults.rb
249 250 251 |
# File 'documented/gemstone/combat/parser.rb', line 249 def parse_assault_start(line) Definitions::Assaults.parse_start(line) end |
.parse_attack(line) ⇒ Object
Parse attack initiation
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 81 82 83 84 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 129 |
# File 'documented/gemstone/combat/parser.rb', line 41 def parse_attack(line) return nil if Definitions::Attacks.rejects?(line) Definitions::Attacks::ATTACK_LOOKUP.each do |pattern, name| if (match = pattern.match(line)) # An inbound attack (creature -> us) names US as its target. # Its only creature link is the ATTACKER, so the line-scan # fallback below would install the attacker as its own # target and apply its damage/crits to itself. Resolve the # target as us and stop - never fall through. # Environmental / self-inflicted defs (SELF_INFLICTED) are # damage to us by construction - no attacker, no "you" # capture - unless this particular pattern named someone # else (a nearby player taking the same tick). self_inflicted = Definitions::Attacks::SELF_INFLICTED.include?(name) && !(match.names.include?('target') && match[:target]) if self_target?(match) || self_inflicted return { name: name, target: {}, inbound: true, attacker: extract_attacker_from_match(match), damaging: true } end # The fallback is only safe when the def has NO target # capture: then the sole creature link on the line really is # the target. When a def DID capture a target but it failed # to resolve to a creature link, scanning the line would find # some other creature (typically the attacker) - so don't. attacker = extract_attacker_from_match(match) target_info = if match.names.include?('target') extract_target_from_match(match) else extract_target_from_line(line) end # Nothing can attack itself. An untargeted def (an AoE like # the mastodon's tremors) has no target capture, so the # line-scan above returns the only link present - the # attacker. Drop it: the event is genuinely untargeted, and # the per-target lines that follow bind the real victims. target_info = nil if target_info && attacker && attacker[:id] && target_info[:id] == attacker[:id] result = { name: name, target: target_info || {}, damaging: true } # The def NAMED a target but it is not a creature link - a # player ("striking Sugiin!"), or a name we cannot resolve. # The event is therefore bound to someone who is not a # creature, and must never later adopt one: the caster's own # bolded props (a summoned ethereal sphere) and room echoes # would otherwise fill the empty slot through the # target-switcher and take that third party's damage with # them (real-feed replay, GSIV-Monstr 2025-09-30). if target_info.nil? && match.names.include?('target') && !match[:target].to_s.strip.empty? result[:foreign_target] = true end # Third-person defs capture the attacker (a player link with # a negative exist id, or a pushBold-wrapped creature link) result[:attacker] = attacker if attacker # A NEARBY PLAYER's attack on a creature we can see. When a # 3p attack names its actor but that actor is not us and not # a creature - a player link (negative id) or a bare name # from mid-sentence prose (the paladin weapon_infusion proc # names "Heavenscent", not a link) - the whole event belongs # to that player. Marked foreign_caster so the processor # emits it for observers but never lands its damage on our # ledger, the attacker-side mirror of foreign_target # (real-feed, GSIV-Nisugi 2026-09-06). Attacks a CREATURE # makes on us are :inbound and handled above; those keep # their creature-id attacker and are not foreign_caster. if attacker && !result[:inbound] && (attacker[:id].to_i < 0 || (attacker[:id].nil? && attacker[:name])) result[:foreign_caster] = true end result[:weapon] = strip_links(match[:weapon]) if match.names.include?('weapon') && match[:weapon] # Aimed shots ("You take aim and fire...", "You make a precise # attempt to jab...") roll differently from unaimed ones. The # defs have captured this all along; it was never surfaced. result[:aimed] = true if match.names.include?('aimed') && match[:aimed] return result end end nil end |
.parse_damage(line) ⇒ Object
Parse damage amounts using damage definitions
206 207 208 209 |
# File 'documented/gemstone/combat/parser.rb', line 206 def parse_damage(line) result = Definitions::Damage.parse(line) result ? result[:damage] : nil end |
.parse_flare(line) ⇒ Object
Parse a flare announce line (weapon scripts, enchants, GEFs, flourishes). Returns name/damaging/aoe/spawns plus the flaring weapon's { id:, name: } when the line links it.
214 215 216 |
# File 'documented/gemstone/combat/parser.rb', line 214 def parse_flare(line) Definitions::Flares.parse(line) end |
.parse_outcome(line) ⇒ Object
Why a swing produced nothing: :evade, :block, :parry, :warded, :miss, :fumble, :hindrance, :confused
220 221 222 |
# File 'documented/gemstone/combat/parser.rb', line 220 def parse_outcome(line) Definitions::Outcomes.parse(line) end |
.parse_resolution(line) ⇒ Object
Roll lines: AS/DS, CS/TD, UAF/UDF, SMR - returns the full breakdown { type:, as:, ds:, avd:, roll:, result: ... }
226 227 228 |
# File 'documented/gemstone/combat/parser.rb', line 226 def parse_resolution(line) Definitions::Resolutions.parse(line) end |
.parse_sequence_end(line) ⇒ Hash?
Parses a multi-target sequence bracket closing line.
Identifies the end of action sequences (mstrike, volley, spawned casts) that span multiple targets.
243 244 245 |
# File 'documented/gemstone/combat/parser.rb', line 243 def parse_sequence_end(line) Definitions::Sequences.parse_end(line) end |
.parse_sequence_start(line) ⇒ Object
Multi-TARGET action brackets (mstrike, volley, spawned casts)
231 232 233 |
# File 'documented/gemstone/combat/parser.rb', line 231 def parse_sequence_start(line) Definitions::Sequences.parse_start(line) end |
.parse_spell_loss(line) ⇒ Object
Third-person spell wear-off lines pinned to a spell number (defs/spell_losses.rb). Unlike statuses these apply to players in view as well as creatures, so the subject resolves through ANY link (negative player ids included), falling back to the stripped capture text for plain logs.
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 |
# File 'documented/gemstone/combat/parser.rb', line 298 def parse_spell_loss(line) return nil unless Tracker.settings[:track_statuses] result = Definitions::SpellLosses.parse(line) return nil unless result if (link = TARGET_LINK_PATTERN.match(result[:target].to_s)) result[:id] = link[:id].to_i result[:name] = link[:name] else result[:id] = nil result[:name] = strip_links(result[:target].to_s).strip end result end |
.parse_status(line) ⇒ Object
Parse status effects (optional - performance setting)
286 287 288 289 290 291 |
# File 'documented/gemstone/combat/parser.rb', line 286 def parse_status(line) return nil unless Tracker.settings[:track_statuses] # Return the full result including action field Definitions::Statuses.parse(line) end |
.parse_swing_weapon(line) ⇒ String?
Extracts the weapon name from a swing or ranged attack line.
Swing lines name the weapon in plain text (not as a link): "You swing a
kelyn-edged slim short sword at
281 282 283 |
# File 'documented/gemstone/combat/parser.rb', line 281 def parse_swing_weapon(line) SWING_WEAPON_PATTERN.match(line)&.[](:weapon) end |
.parse_ucs(line) ⇒ Object
Parse UCS events (position, tierup, smite)
315 316 317 318 319 |
# File 'documented/gemstone/combat/parser.rb', line 315 def parse_ucs(line) return nil unless Tracker.settings[:track_ucs] Definitions::UCS.parse(line) end |
.self_target?(match) ⇒ Boolean
Returns false if the match includes a target capture but it is empty or nil.
Determines whether a regex match names the player as its target.
Handles two patterns: explicit target captures like "you" or "your
156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'documented/gemstone/combat/parser.rb', line 156 def self_target?(match) if match.names.include?('target') text = match[:target] return false if text.nil? return SELF_TARGET_PATTERN.match?(strip_links(text).strip) end # No target capture: inbound only if the def both identifies an # attacker and addresses us literally. match.names.include?('attacker') && SELF_IN_PATTERN.match?(match.regexp.source) end |
.strip_links(text) ⇒ Object
Drop XML link/bold markup from a captured fragment
201 202 203 |
# File 'documented/gemstone/combat/parser.rb', line 201 def strip_links(text) text.gsub(/<[^>]+>/, '') end |