Module: Lich::Gemstone::Combat::Processor Private
- Defined in:
- documented/gemstone/combat/processor.rb
Overview
This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.
Parses combat events from game output and applies them to tracked creatures.
Implements a state machine that transitions through attack, damage, and critical hit states to extract structured combat data from raw game text. Events are accumulated per target and persisted once an attack completes or a target switch occurs.
The processor:
- Recognizes attack declarations and transitions to damage-seeking state
- Accumulates all damage lines for an attack
- Looks ahead 2-3 lines for critical hits associated with each damage
- Detects target switches in multi-target attacks and saves events accordingly
- Tracks status effects and UCS events on all lines regardless of state
- Applies all extracted data (damage, wounds, statuses) to creature instances
Class Method Summary collapse
-
.apply_status_to_target(status, target_name_or_id, target_id = nil, action = :add) ⇒ Object
private
Apply status effect directly to a creature (outside combat events).
-
.apply_ucs_to_target(ucs_result, current_target = nil) ⇒ Object
private
Apply UCS event to a creature.
-
.event_worth_saving?(event) ⇒ Boolean
private
An event is worth persisting only if it has a target to apply data to and any data to apply.
-
.map_critranks_to_body_part(location) ⇒ Object
private
Map CritRanks location strings to creature body part constants.
-
.parse_events(lines) ⇒ Object
private
State machine parser.
-
.persist_event(event) ⇒ Object
private
Apply combat event to creature instance (same as before).
-
.process(chunk) ⇒ Object
private
Process a chunk of game lines for combat events.
Class Method Details
.apply_status_to_target(status, target_name_or_id, target_id = nil, action = :add) ⇒ Object
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.
Apply status effect directly to a creature (outside combat events)
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'documented/gemstone/combat/processor.rb', line 292 def apply_status_to_target(status, target_name_or_id, target_id = nil, action = :add) # Handle both name lookup and direct ID if target_id creature = Creature[target_id.to_i] else # Try to find creature by name - this is less reliable # but might work for some cases return unless defined?(Creature) creatures = Creature.all.select { |c| c.name&.downcase&.include?(target_name_or_id.downcase) } creature = creatures.first if creatures.size == 1 end if creature if action == :remove creature.remove_status(status) respond "[Combat] Removed status #{status} from #{creature.name} (#{creature.id})" if Tracker.debug? else creature.add_status(status) respond "[Combat] Applied status #{status} to #{creature.name} (#{creature.id})" if Tracker.debug? end else respond "[Combat] Could not find creature for status: #{status} -> #{target_name_or_id}" if Tracker.debug? end end |
.apply_ucs_to_target(ucs_result, current_target = nil) ⇒ Object
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.
Apply UCS event to a creature
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'documented/gemstone/combat/processor.rb', line 259 def apply_ucs_to_target(ucs_result, current_target = nil) target_id = ucs_result[:target_id] # For tierup events, use current combat target if no ID in the event target_id ||= current_target[:id] if current_target && ucs_result[:type] == :tierup return unless target_id creature = Creature[target_id.to_i] return unless creature case ucs_result[:type] when :position creature.set_ucs_position(ucs_result[:value]) respond "[Combat] Set UCS position #{ucs_result[:value]} on #{creature.name} (#{creature.id})" if Tracker.debug? when :tierup creature.set_ucs_tierup(ucs_result[:value]) respond "[Combat] Set UCS tierup #{ucs_result[:value]} on #{creature.name} (#{creature.id})" if Tracker.debug? when :smite_on creature.smite! respond "[Combat] Applied smite to #{creature.name} (#{creature.id})" if Tracker.debug? when :smite_off creature.clear_smote respond "[Combat] Cleared smite from #{creature.name} (#{creature.id})" if Tracker.debug? end rescue => e respond "[Combat] Error applying UCS: #{e.}" if Tracker.debug? end |
.event_worth_saving?(event) ⇒ 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.
An event is worth persisting only if it has a target to apply data to and any data to apply. Single predicate so every save site agrees (previously three sites used three different criteria).
59 60 61 62 63 |
# File 'documented/gemstone/combat/processor.rb', line 59 def event_worth_saving?(event) return false unless event && event[:target][:id] !event[:damages].empty? || !event[:crits].empty? || !event[:statuses].empty? end |
.map_critranks_to_body_part(location) ⇒ Object
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.
Map CritRanks location strings to creature body part constants
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 |
# File 'documented/gemstone/combat/processor.rb', line 318 def map_critranks_to_body_part(location) return nil unless location case location.to_s.downcase.gsub(/[^a-z]/, '') when 'leftarm', 'larm' then 'leftArm' when 'rightarm', 'rarm' then 'rightArm' when 'leftleg', 'lleg' then 'leftLeg' when 'rightleg', 'rleg' then 'rightLeg' when 'lefthand', 'lhand' then 'leftHand' when 'righthand', 'rhand' then 'rightHand' when 'leftfoot', 'lfoot' then 'leftFoot' when 'rightfoot', 'rfoot' then 'rightFoot' when 'lefteye', 'leye' then 'leftEye' when 'righteye', 'reye' then 'rightEye' when 'head' then 'head' when 'neck' then 'neck' when 'chest' then 'chest' when 'abdomen', 'abs' then 'abdomen' when 'back' then 'back' when 'nerves' then 'nerves' else # Try the location as-is in case it's already correct location.to_s if CreatureInstance::BODY_PARTS.include?(location.to_s) end end |
.parse_events(lines) ⇒ Object
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.
State machine parser
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 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 |
# File 'documented/gemstone/combat/processor.rb', line 66 def parse_events(lines) events = [] current_event = nil parse_state = :seeking_attack current_target = nil lines.each_with_index do |line, index| next if line.strip.empty? # Extract creature target once per line; reused by the status # handler and the target-switch logic below line_target = Parser.extract_target_from_line(line) # Always check for status effects on every line (even outside combat) if Tracker.settings[:track_statuses] if (status_result = Parser.parse_status(line)) if line_target && line_target[:id] # Use ID-based lookup - this is most reliable if status_result.is_a?(Hash) apply_status_to_target(status_result[:status], line_target[:name], line_target[:id], status_result[:action]) else # Legacy format - status_result is just the status symbol apply_status_to_target(status_result, line_target[:name], line_target[:id], :add) end elsif status_result.is_a?(Hash) && status_result[:target] # Fallback to name-based lookup only if no ID available apply_status_to_target(status_result[:status], status_result[:target], nil, status_result[:action]) end respond "[Combat] Found status effect: #{status_result}" if Tracker.debug? end end # Always check for UCS events on every line if Tracker.settings[:track_ucs] if (ucs_result = Parser.parse_ucs(line)) apply_ucs_to_target(ucs_result, current_target) respond "[Combat] Found UCS event: #{ucs_result}" if Tracker.debug? end end # Handle target switching (for multi-target attacks like volley) if line_target && parse_state != :seeking_attack # Check if this is a real target switch (different creature) if current_target && current_target[:id] != line_target[:id] # Save previous event if it has data if event_worth_saving?(current_event) events << current_event respond "[Combat] Saved event for #{current_event[:target][:name]}: #{current_event[:damages].size} damages, #{current_event[:crits].size} crits, #{current_event[:statuses].size} statuses" if Tracker.debug? end # Create new event for this target (inherit attack name from previous) current_event = { name: current_event ? current_event[:name] : :unknown, target: line_target, damages: [], crits: [], statuses: [] } current_target = line_target respond "[Combat] Switched to target: #{line_target[:name]} (#{line_target[:id]})" if Tracker.debug? elsif current_target.nil? # First target for current event - just set it, don't discard data current_event[:target] = line_target current_target = line_target respond "[Combat] Found target: #{line_target[:name]} (#{line_target[:id]})" if Tracker.debug? end # If current_target[:id] == line_target[:id], do nothing (same target) end # Attack check is needed in both states (a new attack while seeking # damage closes the previous event), so run it once per line. This # replaces the old `redo`, which re-ran the status/UCS handlers # above on the same line and double-applied their effects. attack = Parser.parse_attack(line) if attack # Save previous event before starting a new one if event_worth_saving?(current_event) events << current_event respond "[Combat] Completed event for #{current_event[:target][:name]}: #{current_event[:damages].size} damages, #{current_event[:crits].size} crits" if Tracker.debug? end current_event = { name: attack[:name], target: attack[:target] || {}, damages: [], crits: [], statuses: [] } current_target = current_event[:target][:id] ? current_event[:target] : nil respond "[Combat] Found attack: #{attack[:name]}" if Tracker.debug? parse_state = :seeking_damage elsif parse_state == :seeking_damage # Accumulate all damage lines for the current attack if (damage = Parser.parse_damage(line)) current_event[:damages] << damage respond "[Combat] Found damage: #{damage}" if Tracker.debug? # When we find damage, look ahead 2-3 lines for related crit if Tracker.settings[:track_wounds] (1..3).each do |offset| next_line_index = index + offset break if next_line_index >= lines.size next_line = lines[next_line_index] # Stop looking if we hit another damage line (belongs to next damage) if Parser.parse_damage(next_line) respond "[Combat] Stopped crit search - found next damage line" if Tracker.debug? break end # Look for crit on this line if (c = CritRanks.parse(next_line.gsub(/<.+?>/, '')).values.first) current_event[:crits] << { type: c[:type], location: c[:location], rank: c[:rank], wound_rank: c[:wound_rank], fatal: c[:fatal] } respond "[Combat] Found critical hit: #{c[:location]} rank #{c[:wound_rank]}" if Tracker.debug? break # Only take first crit found after this damage end end end end end end # Don't forget the last event events << current_event if event_worth_saving?(current_event) events end |
.persist_event(event) ⇒ Object
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.
Apply combat event to creature instance (same as before)
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 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'documented/gemstone/combat/processor.rb', line 205 def persist_event(event) target = event[:target] return unless target[:id] creature = Creature[target[:id].to_i] unless creature respond "[Combat] No creature found for ID #{target[:id]}" if Tracker.debug? return end respond "[Combat] Applying to #{creature.name} (#{target[:id]})" if Tracker.debug? # Apply direct damage total_damage = 0 event[:damages].each do |damage| creature.add_damage(damage) total_damage += damage respond " +#{damage} damage" if Tracker.debug? end # Apply critical wounds if Tracker.settings[:track_wounds] event[:crits].each do |crit| if crit[:wound_rank] && crit[:wound_rank] > 0 # Map CritRanks location to creature body part format body_part = map_critranks_to_body_part(crit[:location]) if body_part creature.add_injury(body_part, crit[:wound_rank]) respond " +wound: #{body_part} rank #{crit[:wound_rank]}" if Tracker.debug? else respond " !unknown body part: #{crit[:location]}" if Tracker.debug? end end # Check for fatal critical hit if crit[:fatal] creature.mark_fatal_crit! respond " +FATAL CRIT: #{crit[:location]} - creature died from crit, not HP loss" if Tracker.debug? end end end # Apply status effects if Tracker.settings[:track_statuses] event[:statuses].each do |status| creature.add_status(status) respond " +status: #{status}" if Tracker.debug? end end respond " Total damage applied: #{total_damage}" if total_damage > 0 && Tracker.debug? end |
.process(chunk) ⇒ Object
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.
Process a chunk of game lines for combat events
47 48 49 50 51 52 53 54 |
# File 'documented/gemstone/combat/processor.rb', line 47 def process(chunk) events = parse_events(chunk) return if events.empty? events.each { |event| persist_event(event) } respond "[Combat] Processed #{events.size} events" if Tracker.debug? end |