Module: Lich::Gemstone::QStrike

Defined in:
documented/gemstone/psms/qstrike.rb

Overview

Module for handling QStrike combat mechanics.

This module includes methods for calculating attack costs, managing combat settings, and executing attacks.

Constant Summary collapse

SPEED_MULTIPLIERS =

Speed multipliers by weapon category Applied per-weapon to calculate Equipment Speed

{
  two_handed: 1.5,
  polearm: 1.5,
  ranged: 2.5,
  # All others default to 1.0
}.freeze
DEFAULT_MULTIPLIER =

Default speed multiplier for weapons not categorized.

Examples:

DEFAULT_MULTIPLIER # => 1.0
1.0
STRIKING_ASP_MULTIPLIERS =

Striking Asp stance cost multipliers by rank Rank 1 = 2/3 cost, Rank 2 = 1/2 cost, Rank 3 = 1/3 cost

{
  1 => 2.0 / 3.0,  # 0.667
  2 => 1.0 / 2.0,  # 0.500
  3 => 1.0 / 3.0,  # 0.333
}.freeze
BASE_COST =

Base cost constant from formula

10
MAX_REDUCTION =

Maximum seconds of RT reduction (reasonable upper bound)

8
VALID_ACTIONS =

Valid combat actions that can use QSTRIKE

%w[
  ascension ambush attack cheapshot cman cock disarm feat fire
  grapple hurl jab kill kick mstrike punch shield smite
  stunman subdue sweep tackle trip weapon wtricks
].freeze
DEFAULT_SETTINGS =

Default settings Default settings for QStrike module.

Examples:

DEFAULT_SETTINGS # => { reserve: 1, adaptive: false }
{
  reserve: 1,
  adaptive: false
}.freeze
TECHNIQUE_MODULES =

Module lookup configuration: [Module, class_var, type_symbol]

[
  [:CMan, :@@combat_mans, :cman],
  [:Weapon, :@@weapon_techniques, :weapon],
  [:Shield, :@@shield_techniques, :shield],
].freeze

Class Method Summary collapse

Class Method Details

.affordable?(reserve: nil, attack_cost: 0, attack_name: nil, attack_rt: nil) ⇒ Boolean

Checks if the QStrike reduction can be afforded with current stamina.

Parameters:

  • reserve (Integer, nil) (defaults to: nil)

    the stamina reserve to consider.

  • attack_cost (Integer) (defaults to: 0)

    the cost of the attack.

  • attack_name (String, nil) (defaults to: nil)

    the name of the attack to look up.

  • attack_rt (Integer, nil) (defaults to: nil)

    the current RT of the attack.

Returns:

  • (Boolean)

    true if the reduction can be afforded, false otherwise.



221
222
223
224
# File 'documented/gemstone/psms/qstrike.rb', line 221

def self.affordable?(reserve: nil, attack_cost: 0, attack_name: nil, attack_rt: nil)
  result = calculate(reserve: reserve, attack_cost: attack_cost, attack_name: attack_name, attack_rt: attack_rt)
  result[:seconds].positive?
end

.base_rtInteger

Retrieves the base RT for the currently equipped weapon.

Returns:

  • (Integer)

    the base RT of the weapon.



318
319
320
321
# File 'documented/gemstone/psms/qstrike.rb', line 318

def self.base_rt
  hand = ranged_weapon? ? GameObj.left_hand : GameObj.right_hand
  weapon_speed_for(hand)[:base_rt]
end

.build_attack_command(attack, target = nil) ⇒ Object



747
748
749
750
751
752
753
# File 'documented/gemstone/psms/qstrike.rb', line 747

def self.build_attack_command(attack, target = nil)
  if target && !target.empty?
    "#{attack} #{target}"
  else
    attack
  end
end

.cache_cost(cost) ⇒ Object



567
568
569
570
571
572
573
# File 'documented/gemstone/psms/qstrike.rb', line 567

def self.cache_cost(cost)
  @cached_cost = cost
  @cached_right_hand = hand_cache_key(GameObj.right_hand)
  @cached_left_hand = hand_cache_key(GameObj.left_hand)
rescue StandardError
  # Ignore caching errors
end

.calculate(reserve: nil, attack_cost: 0, attack_name: nil, attack_rt: nil) ⇒ Hash

Calculates the QStrike reduction based on provided parameters.

Parameters:

  • reserve (Integer, nil) (defaults to: nil)

    the stamina reserve to consider.

  • attack_cost (Integer) (defaults to: 0)

    the cost of the attack.

  • attack_name (String, nil) (defaults to: nil)

    the name of the attack to look up.

  • attack_rt (Integer, nil) (defaults to: nil)

    the current RT of the attack.

Returns:

  • (Hash)

    a hash containing the calculated values.



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
# File 'documented/gemstone/psms/qstrike.rb', line 141

def self.calculate(reserve: nil, attack_cost: 0, attack_name: nil, attack_rt: nil)
  reserve ||= default(:reserve)
  # Look up attack cost if name provided
  if attack_name && attack_cost.zero?
    attack_cost = lookup_attack_cost(attack_name)
  end

  current_stamina = Char.stamina
  available = current_stamina - reserve - attack_cost

  if available <= 0
    return {
      seconds: 0,
      stamina_cost: 0,
      qstrike_cmd: nil,
      reason: :insufficient_stamina,
      current_stamina: current_stamina,
      available_stamina: available,
      attack_cost: attack_cost,
      reserve: reserve
    }
  end

  cost_per_second = cost_per_second_reduction
  max_seconds = find_max_seconds(available, cost_per_second)

  # Cap reduction based on attack's RT (can't reduce below 1 second)
  if attack_rt && attack_rt > 1
    max_useful = attack_rt - 1
    max_seconds = [max_seconds, max_useful].min
  end

  if max_seconds.positive?
    total_cost = cost_per_second * max_seconds
    {
      seconds: max_seconds,
      stamina_cost: total_cost,
      qstrike_cmd: "qstrike -#{max_seconds}",
      current_stamina: current_stamina,
      available_stamina: available,
      attack_cost: attack_cost,
      attack_rt: attack_rt,
      reserve: reserve,
      cost_per_second: cost_per_second,
      striking_asp_active: striking_asp_active?
    }
  else
    {
      seconds: 0,
      stamina_cost: 0,
      qstrike_cmd: nil,
      reason: :too_expensive,
      current_stamina: current_stamina,
      available_stamina: available,
      attack_cost: attack_cost,
      attack_rt: attack_rt,
      reserve: reserve,
      cost_per_second: cost_per_second
    }
  end
end

.clear_cacheObject



575
576
577
578
579
# File 'documented/gemstone/psms/qstrike.rb', line 575

def self.clear_cache
  @cached_cost = nil
  @cached_right_hand = nil
  @cached_left_hand = nil
end

.command(reserve: nil, attack_cost: 0, attack_name: nil, attack_rt: nil) ⇒ String?

Generates the QStrike command based on the current settings.

Parameters:

  • reserve (Integer, nil) (defaults to: nil)

    the stamina reserve to consider.

  • attack_cost (Integer) (defaults to: 0)

    the cost of the attack.

  • attack_name (String, nil) (defaults to: nil)

    the name of the attack to look up.

  • attack_rt (Integer, nil) (defaults to: nil)

    the current RT of the attack.

Returns:

  • (String, nil)

    the generated QStrike command.



210
211
212
# File 'documented/gemstone/psms/qstrike.rb', line 210

def self.command(reserve: nil, attack_cost: 0, attack_name: nil, attack_rt: nil)
  calculate(reserve: reserve, attack_cost: attack_cost, attack_name: attack_name, attack_rt: attack_rt)[:qstrike_cmd]
end

.cost_for_reduction(seconds) ⇒ Integer

Calculates the cost for a given reduction in seconds.

Parameters:

  • seconds (Integer)

    the number of seconds to reduce.

Returns:

  • (Integer)

    the cost associated with the reduction.



309
310
311
312
313
# File 'documented/gemstone/psms/qstrike.rb', line 309

def self.cost_for_reduction(seconds)
  return 0 if seconds.nil? || seconds <= 0

  cost_per_second_reduction * seconds
end

.cost_per_second_reductionInteger

Calculates the cost per second for QStrike reduction.

Returns:

  • (Integer)

    the cost per second for reduction.



430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# File 'documented/gemstone/psms/qstrike.rb', line 430

def self.cost_per_second_reduction
  # Check memoization cache
  return @cached_cost if valid_cache?

  primary = primary_equipment_speed
  secondary = secondary_equipment_speed

  # Base formula: 10 + primary + (secondary / 2)
  base_cost = BASE_COST + primary + (secondary / 2)

  # Apply Striking Asp discount if active
  final_cost = (base_cost * striking_asp_multiplier).to_i

  # Cache the result
  cache_cost(final_cost)

  final_cost
end

.debug_calculationObject



449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
# File 'documented/gemstone/psms/qstrike.rb', line 449

def self.debug_calculation
  respond "=== QStrike Debug Calculation ==="

  # Right hand
  right = GameObj.right_hand
  respond "Right hand: #{right&.name || 'Empty'} (noun: #{right&.noun || 'nil'}, id: #{right&.id || 'nil'})"
  if right && right.name != "Empty"
    stats = find_weapon_stats(right)
    if stats
      respond "  Weapon found: #{stats[:base_name]} (category: #{stats[:category]})"
      respond "  base_rt: #{stats[:base_rt]}"
      multiplier = SPEED_MULTIPLIERS[stats[:category]] || DEFAULT_MULTIPLIER
      respond "  multiplier: #{multiplier}"
      equip_speed = (stats[:base_rt].to_i * multiplier).to_i
      respond "  equipment_speed: #{equip_speed}"
    else
      respond "  WARNING: No weapon stats found!"
    end
  end

  # Left hand
  left = GameObj.left_hand
  respond "Left hand: #{left&.name || 'Empty'} (noun: #{left&.noun || 'nil'}, id: #{left&.id || 'nil'})"
  if left && left.name != "Empty"
    stats = find_weapon_stats(left)
    if stats
      respond "  Weapon found: #{stats[:base_name]} (category: #{stats[:category]})"
      respond "  base_rt: #{stats[:base_rt]}"
      multiplier = SPEED_MULTIPLIERS[stats[:category]] || DEFAULT_MULTIPLIER
      respond "  multiplier: #{multiplier}"
      equip_speed = (stats[:base_rt].to_i * multiplier).to_i
      respond "  equipment_speed: #{equip_speed}"
    else
      respond "  WARNING: No weapon stats found!"
    end
  end

  # Primary/Secondary determination
  is_ranged = ranged_weapon?
  respond "Ranged mode: #{is_ranged}"
  respond "Primary hand: #{is_ranged ? 'LEFT' : 'RIGHT'}"
  respond "Secondary hand: #{is_ranged ? 'RIGHT' : 'LEFT'}"

  # Calculated values
  primary = primary_equipment_speed
  secondary = secondary_equipment_speed
  respond "Primary equipment_speed: #{primary}"
  respond "Secondary equipment_speed: #{secondary}"
  respond "Secondary / 2 (integer division): #{secondary / 2}"

  # Formula
  base_cost = BASE_COST + primary + (secondary / 2)
  respond "Formula: BASE_COST(#{BASE_COST}) + primary(#{primary}) + secondary/2(#{secondary / 2}) = #{base_cost}"

  # Striking Asp
  asp_mult = striking_asp_multiplier
  if (asp_mult - 1.0).abs > Float::EPSILON
    respond "Striking Asp multiplier: #{asp_mult}"
    final_cost = (base_cost * asp_mult).to_i
    respond "Final cost (with Asp): #{base_cost} * #{asp_mult} = #{final_cost}"
  else
    respond "Striking Asp: not active"
    respond "Final cost per second: #{base_cost}"
  end

  respond "=== End Debug ==="
end

.default(key) ⇒ Object

Retrieves the value of a specific default setting.

Parameters:

  • key (Symbol)

    the setting key to retrieve.

Returns:

  • (Object)

    the value of the specified setting.



94
95
96
97
# File 'documented/gemstone/psms/qstrike.rb', line 94

def self.default(key)
  load_settings
  @settings.fetch(key.to_sym, DEFAULT_SETTINGS[key.to_sym])
end

.defaultsHash

Loads and returns the default settings for QStrike.

Returns:

  • (Hash)

    the current default settings.



61
62
63
64
65
66
67
# File 'documented/gemstone/psms/qstrike.rb', line 61

def self.defaults
  load_settings
  {
    reserve: @settings[:reserve],
    adaptive: @settings[:adaptive]
  }
end

.defaults=(new_defaults) ⇒ void

This method returns an undefined value.

Updates the default settings with new values.

Parameters:

  • new_defaults (Hash)

    new settings to apply.



73
74
75
76
77
# File 'documented/gemstone/psms/qstrike.rb', line 73

def self.defaults=(new_defaults)
  load_settings
  @settings.merge!(new_defaults)
  save_settings
end

.defined_module?(mod_name) ⇒ Boolean

Returns:

  • (Boolean)


629
630
631
632
633
# File 'documented/gemstone/psms/qstrike.rb', line 629

def self.defined_module?(mod_name)
  Object.const_defined?(mod_name)
rescue StandardError
  false
end

.detect_attack_type(name) ⇒ Object



731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
# File 'documented/gemstone/psms/qstrike.rb', line 731

def self.detect_attack_type(name)
  TECHNIQUE_MODULES.each do |mod_name, class_var, type|
    next unless defined_module?(mod_name)

    begin
      mod = Object.const_get(mod_name)
      data = mod.class_variable_get(class_var)
      return type if data.key?(name) || data.values.any? { |v| v[:short_name] == name }
    rescue StandardError
      next
    end
  end

  :basic
end

.execute_attack(attack, target = nil) ⇒ void

This method returns an undefined value.

Executes the specified attack against a target.

Parameters:

  • attack (String)

    the name of the attack to execute.

  • target (String, nil) (defaults to: nil)

    the target of the attack.



699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
# File 'documented/gemstone/psms/qstrike.rb', line 699

def self.execute_attack(attack, target = nil)
  attack_str = attack.to_s
  normalized = normalize_attack_name(attack)

  # Detect and execute based on attack type
  attack_type = detect_attack_type(normalized)

  case attack_type
  when :cman
    if defined?(CMan) && CMan.respond_to?(:use)
      CMan.use(normalized, target)
    else
      fput build_attack_command(attack_str, target)
    end
  when :weapon
    if defined?(Weapon) && Weapon.respond_to?(:use)
      Weapon.use(normalized, target)
    else
      fput build_attack_command(attack_str, target)
    end
  when :shield
    if defined?(Shield) && Shield.respond_to?(:use)
      Shield.use(normalized, target)
    else
      fput build_attack_command(attack_str, target)
    end
  else
    # Basic command - just send it
    fput build_attack_command(attack_str, target)
  end
end

.execute_qstrike(reduction) ⇒ void

This method returns an undefined value.

Executes the QStrike command with the specified reduction.

Parameters:

  • reduction (Integer)

    the amount of reduction to apply.



688
689
690
691
692
# File 'documented/gemstone/psms/qstrike.rb', line 688

def self.execute_qstrike(reduction)
  return if reduction.nil? || reduction <= 0

  fput "qstrike -#{reduction}"
end

.find_max_seconds(available_stamina, cost_per_second) ⇒ Integer

Determines the maximum number of seconds that can be afforded based on stamina.

Parameters:

  • available_stamina (Integer)

    the available stamina to use.

  • cost_per_second (Integer)

    the cost per second of reduction.

Returns:

  • (Integer)

    the maximum seconds that can be afforded.



640
641
642
643
644
645
646
647
648
# File 'documented/gemstone/psms/qstrike.rb', line 640

def self.find_max_seconds(available_stamina, cost_per_second)
  return 0 if cost_per_second <= 0

  # Simple division - how many full seconds can we afford?
  max = available_stamina / cost_per_second

  # Cap at reasonable maximum
  [max, MAX_REDUCTION].min
end

.find_weapon_stats(hand) ⇒ Hash?

Finds the weapon stats for a given hand.

Parameters:

  • hand (Object)

    the weapon object to analyze.

Returns:

  • (Hash, nil)

    the weapon stats or nil if not found.



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'documented/gemstone/psms/qstrike.rb', line 339

def self.find_weapon_stats(hand)
  return nil if hand.nil?

  # Strategy 1: Try the noun directly (works for "dagger", "broadsword", etc.)
  stats = Armaments::WeaponStats.find(hand.noun)
  return stats if stats

  # Strategy 2: Try the full name (works for "slim short sword" -> finds "short sword")
  stats = Armaments::WeaponStats.find(hand.name)
  return stats if stats

  # Strategy 3: Extract weapon type from name by removing common adjectives
  # e.g., "slim short sword" -> try "short sword"
  name = hand.name.to_s.downcase
  adjectives = %w[slim gleaming steel iron silver gold mithril vultite golvern
                  ora krodera drakar rhimar gornar zorchar eonake faenor invar
                  kelyn laje razern rolaren vaalorn veil imflass alexandrite
                  black white red blue green small large heavy light ornate
                  polished rusted ancient old new fine]

  words = name.split
  # Remove leading adjectives
  while words.length > 1 && adjectives.include?(words.first)
    words.shift
  end

  # Try progressively shorter suffixes: "short sword", then "sword"
  while words.length > 0
    attempt = words.join(' ')
    stats = Armaments::WeaponStats.find(attempt)
    return stats if stats
    words.shift
  end

  nil
end

.hand_cache_key(hand) ⇒ Object



548
549
550
551
552
553
554
# File 'documented/gemstone/psms/qstrike.rb', line 548

def self.hand_cache_key(hand)
  return "empty" if hand.nil?
  return "empty" if hand.name.nil? || hand.name.empty? || hand.name == "Empty"
  return "empty" if hand.id.nil?

  "#{hand.id}:#{hand.noun}"
end

.load_settingsObject



107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'documented/gemstone/psms/qstrike.rb', line 107

def self.load_settings
  return if @settings_loaded

  if defined?(Lich::Common::DB_Store) && defined?(XMLData) && !XMLData.game.to_s.empty? && !XMLData.name.to_s.empty?
    scope = "#{XMLData.game}:#{XMLData.name}"
    stored = Lich::Common::DB_Store.read(scope, 'lich_qstrike')
    @settings = DEFAULT_SETTINGS.merge(stored || {})
    @settings_loaded = true
  else
    # Fallback to in-memory defaults
    @settings ||= DEFAULT_SETTINGS.dup
  end
end

.lookup_attack_cost(name) ⇒ Integer

Looks up the cost of an attack by its name.

Parameters:

  • name (String)

    the name of the attack to look up.

Returns:

  • (Integer)

    the stamina cost of the attack.



594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
# File 'documented/gemstone/psms/qstrike.rb', line 594

def self.lookup_attack_cost(name)
  name = name.to_s.downcase.gsub(/[\s-]+/, '_')

  # Handle explicit type prefixes for disambiguation
  TECHNIQUE_MODULES.each do |_, _, type|
    prefix = "#{type}_"
    return lookup_technique_cost(name.sub(prefix, ''), type) if name.start_with?(prefix)
  end

  # Try each module in order until we find a cost
  TECHNIQUE_MODULES.each do |_, _, type|
    cost = lookup_technique_cost(name, type)
    return cost if cost.positive?
  end

  0
end

.lookup_technique_cost(name, type) ⇒ Integer

Looks up the technique cost for a given name and type.

Parameters:

  • name (String)

    the name of the technique to look up.

  • type (Symbol)

    the type of technique (e.g., :cman, :weapon).

Returns:

  • (Integer)

    the stamina cost of the technique.



617
618
619
620
621
622
623
624
625
626
627
# File 'documented/gemstone/psms/qstrike.rb', line 617

def self.lookup_technique_cost(name, type)
  mod_name, class_var, = TECHNIQUE_MODULES.find { |_, _, t| t == type }
  return 0 unless mod_name && defined_module?(mod_name)

  mod = Object.const_get(mod_name)
  data = mod.class_variable_get(class_var)
  entry = data[name] || data.values.find { |v| v[:short_name] == name }
  entry&.dig(:cost, :stamina).to_i
rescue StandardError
  0
end

.normalize_attack_name(attack) ⇒ String

Normalizes the attack name for consistent lookup.

Parameters:

  • attack (String)

    the attack name to normalize.

Returns:

  • (String)

    the normalized attack name.



680
681
682
# File 'documented/gemstone/psms/qstrike.rb', line 680

def self.normalize_attack_name(attack)
  attack.to_s.downcase.gsub(/[\s-]+/, '_').gsub(/[^a-z0-9_]/, '')
end

.primary_equipment_speedInteger

Retrieves the equipment speed for the primary weapon.

Returns:

  • (Integer)

    the equipment speed of the primary weapon.



414
415
416
417
# File 'documented/gemstone/psms/qstrike.rb', line 414

def self.primary_equipment_speed
  hand = ranged_weapon? ? GameObj.left_hand : GameObj.right_hand
  weapon_speed_for(hand)[:equipment_speed]
end

.ranged_weapon?Boolean

Checks if the currently equipped weapon is a ranged weapon.

Returns:

  • (Boolean)

    true if the weapon is ranged, false otherwise.



379
380
381
382
383
384
385
# File 'documented/gemstone/psms/qstrike.rb', line 379

def self.ranged_weapon?
  left = GameObj.left_hand
  return false if left.nil? || left.name == "Empty"

  stats = find_weapon_stats(left)
  stats&.dig(:category) == :ranged
end

.reduction_for_target_rt(target_rt) ⇒ Integer

Calculates the reduction needed to reach a target RT.

Parameters:

  • target_rt (Integer)

    the target RT to achieve.

Returns:

  • (Integer)

    the calculated reduction.



327
328
329
330
331
332
# File 'documented/gemstone/psms/qstrike.rb', line 327

def self.reduction_for_target_rt(target_rt)
  current_base = base_rt
  return 0 if target_rt >= current_base

  [current_base - target_rt, MAX_REDUCTION].min
end

.reset_defaultsvoid

This method returns an undefined value.

Resets all default settings to their initial values.



102
103
104
105
# File 'documented/gemstone/psms/qstrike.rb', line 102

def self.reset_defaults
  @settings = DEFAULT_SETTINGS.dup
  save_settings
end

.reset_settings_cacheObject



128
129
130
131
# File 'documented/gemstone/psms/qstrike.rb', line 128

def self.reset_settings_cache
  @settings_loaded = false
  @settings = nil
end

.resolve_reduction(reduction, reserve, attack_cost) ⇒ Integer?

Resolves the reduction value based on the input parameters.

Parameters:

  • reduction (Integer, Symbol)

    the requested reduction value.

  • reserve (Integer)

    the stamina reserve to consider.

  • attack_cost (Integer)

    the cost of the attack.

Returns:

  • (Integer, nil)

    the resolved reduction value.



657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
# File 'documented/gemstone/psms/qstrike.rb', line 657

def self.resolve_reduction(reduction, reserve, attack_cost)
  case reduction
  when :max, :optimal
    calculate(reserve: reserve, attack_cost: attack_cost)[:seconds]
  when Integer
    if reduction.negative?
      # Negative = reduce by that many seconds
      reduction.abs
    elsif reduction.positive?
      # Positive = target absolute RT
      reduction_for_target_rt(reduction)
    else
      0
    end
  else
    nil
  end
end

.save_settingsObject



121
122
123
124
125
126
# File 'documented/gemstone/psms/qstrike.rb', line 121

def self.save_settings
  if defined?(Lich::Common::DB_Store) && defined?(XMLData) && !XMLData.game.to_s.empty? && !XMLData.name.to_s.empty?
    scope = "#{XMLData.game}:#{XMLData.name}"
    Lich::Common::DB_Store.save(scope, 'lich_qstrike', @settings)
  end
end

.secondary_equipment_speedInteger

Retrieves the equipment speed for the secondary weapon.

Returns:

  • (Integer)

    the equipment speed of the secondary weapon.



422
423
424
425
# File 'documented/gemstone/psms/qstrike.rb', line 422

def self.secondary_equipment_speed
  hand = ranged_weapon? ? GameObj.right_hand : GameObj.left_hand
  weapon_speed_for(hand)[:equipment_speed]
end

.set_default(key, value) ⇒ void

This method returns an undefined value.

Sets a specific default setting to a new value.

Parameters:

  • key (Symbol)

    the setting key to update.

  • value (Object)

    the new value for the setting.



84
85
86
87
88
# File 'documented/gemstone/psms/qstrike.rb', line 84

def self.set_default(key, value)
  load_settings
  @settings[key.to_sym] = value
  save_settings
end

.striking_asp_active?Boolean

Checks if the Striking Asp buff is currently active.

Returns:

  • (Boolean)

    true if active, false otherwise.



521
522
523
524
525
526
527
# File 'documented/gemstone/psms/qstrike.rb', line 521

def self.striking_asp_active?
  return false unless defined?(Effects::Buffs)

  Effects::Buffs.active?('Striking Asp')
rescue StandardError
  false
end

.striking_asp_multiplierFloat

Retrieves the multiplier for the Striking Asp buff based on its rank.

Returns:

  • (Float)

    the multiplier for the Striking Asp buff.



540
541
542
543
544
545
# File 'documented/gemstone/psms/qstrike.rb', line 540

def self.striking_asp_multiplier
  return 1.0 unless striking_asp_active?

  rank = striking_asp_rank
  STRIKING_ASP_MULTIPLIERS[rank] || 1.0
end

.striking_asp_rankObject



529
530
531
532
533
534
535
# File 'documented/gemstone/psms/qstrike.rb', line 529

def self.striking_asp_rank
  return 0 unless defined?(CMan)

  CMan['striking_asp'].to_i
rescue StandardError
  0
end

.use(reduction:, attack:, target: nil, reserve: nil, adaptive: nil) ⇒ Hash

Executes a QStrike reduction and attack.

Parameters:

  • reduction (Integer)

    the amount of reduction to apply.

  • attack (String)

    the name of the attack to execute.

  • target (String, nil) (defaults to: nil)

    the target of the attack.

  • reserve (Integer, nil) (defaults to: nil)

    the stamina reserve to consider.

  • adaptive (Boolean, nil) (defaults to: nil)

    whether to adaptively reduce the attack.

Returns:

  • (Hash)

    a hash containing the result of the operation.



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'documented/gemstone/psms/qstrike.rb', line 235

def self.use(reduction:, attack:, target: nil, reserve: nil, adaptive: nil)
  reserve ||= default(:reserve)
  adaptive = default(:adaptive) if adaptive.nil?

  attack_name = normalize_attack_name(attack)
  attack_cost = lookup_attack_cost(attack_name)

  # Determine the actual reduction to attempt
  actual_reduction = resolve_reduction(reduction, reserve, attack_cost)

  if actual_reduction.nil? || actual_reduction.zero?
    max_affordable = calculate(reserve: reserve, attack_cost: attack_cost)[:seconds]
    respond "[QStrike] Cannot afford any reduction. Stamina: #{Char.stamina}, Reserve: #{reserve}, Attack cost: #{attack_cost}"
    return {
      success: false,
      reason: :cannot_afford,
      requested_reduction: reduction,
      max_affordable: max_affordable
    }
  end

  # Check if we can afford the requested reduction
  qstrike_cost = cost_for_reduction(actual_reduction)
  available = Char.stamina - reserve - attack_cost

  if qstrike_cost > available
    if adaptive
      # Calculate what we can actually afford
      max_affordable = calculate(reserve: reserve, attack_cost: attack_cost)[:seconds]
      if max_affordable.positive?
        actual_reduction = max_affordable
        qstrike_cost = cost_for_reduction(actual_reduction)
      else
        respond "[QStrike] Insufficient stamina. Need: #{qstrike_cost}, Available: #{available} (after #{reserve} reserve + #{attack_cost} attack)"
        return {
          success: false,
          reason: :insufficient_stamina,
          requested_reduction: reduction,
          available_stamina: available,
          qstrike_cost: qstrike_cost
        }
      end
    else
      max_affordable = calculate(reserve: reserve, attack_cost: attack_cost)[:seconds]
      respond "[QStrike] Insufficient stamina for #{actual_reduction}s reduction. Need: #{qstrike_cost}, Available: #{available}. Max affordable: #{max_affordable}s"
      return {
        success: false,
        reason: :insufficient_stamina,
        requested_reduction: reduction,
        available_stamina: available,
        qstrike_cost: qstrike_cost,
        max_affordable: max_affordable
      }
    end
  end

  # Execute the qstrike and attack
  execute_qstrike(actual_reduction)
  execute_attack(attack, target)

  {
    success: true,
    reduction_used: actual_reduction,
    qstrike_cost: qstrike_cost,
    attack_cost: attack_cost,
    total_cost: qstrike_cost + attack_cost,
    stamina_after: Char.stamina - qstrike_cost - attack_cost
  }
end

.valid_cache?Boolean

Returns:

  • (Boolean)


556
557
558
559
560
561
562
563
564
565
# File 'documented/gemstone/psms/qstrike.rb', line 556

def self.valid_cache?
  return false unless @cached_cost

  current_right = hand_cache_key(GameObj.right_hand)
  current_left = hand_cache_key(GameObj.left_hand)

  @cached_right_hand == current_right && @cached_left_hand == current_left
rescue StandardError
  false
end

.weapon_speed_for(hand) ⇒ Hash

Calculates the speed for a given weapon based on its stats.

Parameters:

  • hand (Object)

    the weapon object to analyze.

Returns:

  • (Hash)

    a hash containing the weapon's speed stats.



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'documented/gemstone/psms/qstrike.rb', line 391

def self.weapon_speed_for(hand)
  empty_result = { base_rt: 0, category: nil, equipment_speed: 0 }
  return empty_result if hand.nil? || hand.name == "Empty"

  stats = find_weapon_stats(hand)
  return empty_result unless stats

  base_rt = stats[:base_rt]
  base_rt = base_rt.first if base_rt.is_a?(Array)
  base_rt = base_rt.to_i

  category = stats[:category]
  multiplier = SPEED_MULTIPLIERS[category] || DEFAULT_MULTIPLIER

  # Equipment Speed = Weapon Base RT * Speed Modifier
  equipment_speed = (base_rt * multiplier).to_i

  { base_rt: base_rt, category: category, equipment_speed: equipment_speed }
end