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

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.message}" 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).

Returns:

  • (Boolean)


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