Module: Lich::Gemstone::QStrike

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

Overview

QStrike module calculates optimal QSTRIKE roundtime reduction while avoiding negative stamina. Accounts for weapon speeds, attack costs, and Striking Asp stance discounts.

Examples:

Basic calculation

QStrike.calculate(reserve: 1, attack_name: :cripple)
# => { seconds: 2, stamina_cost: 30, qstrike_cmd: "qstrike -2", ... }

Get just the command string

QStrike.command(attack_name: :tackle, reserve: 5)
# => "qstrike -3"

Execute qstrike + attack

QStrike.use(reduction: -3, attack: "cripple", target: "kobold")
# Executes: put "qstrike -3" then CMan.use("cripple", "kobold")

Absolute RT mode (reduce to 2 second RT)

QStrike.use(reduction: 2, attack: "attack", target: "troll")

Set defaults

QStrike.set_default(:reserve, 5)
QStrike.set_default(:adaptive, true)

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 =

All others default to 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

{
  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

Check if any QSTRIKE reduction is affordable

Parameters:

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

    Minimum stamina to keep (uses default if nil)

  • attack_cost (Integer) (defaults to: 0)

    Stamina cost of attack

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

    Name of attack to look up cost

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

    The attack’s actual roundtime (caps max useful reduction)

Returns:

  • (Boolean)

    true if at least 1 second of reduction is affordable



230
231
232
233
# File 'documented/gemstone/psms/qstrike.rb', line 230

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

Get base RT for current weapon setup Uses the primary weapon’s base RT

Returns:

  • (Integer)

    Base roundtime in seconds



340
341
342
343
# File 'documented/gemstone/psms/qstrike.rb', line 340

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) ⇒ String

Build attack command string

Parameters:

  • attack (String)

    Attack command

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

    Target

Returns:

  • (String)

    Full command string



801
802
803
804
805
806
807
# File 'documented/gemstone/psms/qstrike.rb', line 801

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

.cache_cost(cost) ⇒ Object

Store cost in cache with current hand state

Parameters:

  • cost (Integer)

    The calculated cost to cache



609
610
611
612
613
614
615
# File 'documented/gemstone/psms/qstrike.rb', line 609

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

Calculate optimal QSTRIKE reduction that won’t cause negative stamina

Parameters:

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

    Minimum stamina to keep after QSTRIKE + attack (uses default if nil)

  • attack_cost (Integer) (defaults to: 0)

    Stamina cost of attack being performed (default: 0)

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

    Name of CMan/Weapon technique to look up cost (optional)

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

    The attack’s actual roundtime (caps max useful reduction)

Returns:

  • (Hash)

    Result hash with :seconds, :stamina_cost, :qstrike_cmd, etc.



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

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

Clear the cache (call if you know equipment changed)



618
619
620
621
622
# File 'documented/gemstone/psms/qstrike.rb', line 618

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?

Convenience method: returns just the command string

Parameters:

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

    Minimum stamina to keep (uses default if nil)

  • attack_cost (Integer) (defaults to: 0)

    Stamina cost of attack

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

    Name of attack to look up cost

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

    The attack’s actual roundtime (caps max useful reduction)

Returns:

  • (String, nil)

    “qstrike -N” or nil if can’t afford any reduction



219
220
221
# File 'documented/gemstone/psms/qstrike.rb', line 219

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

Calculate stamina cost for a specific reduction amount

Parameters:

  • seconds (Integer)

    Seconds of RT reduction

Returns:

  • (Integer)

    Stamina cost



330
331
332
333
334
# File 'documented/gemstone/psms/qstrike.rb', line 330

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

  cost_per_second_reduction * seconds
end

.cost_per_second_reductionInteger

Calculate stamina cost per second of RT reduction Formula: (10 + primary_equipment_speed + (secondary_equipment_speed / 2)) * asp_multiplier

Returns:

  • (Integer)

    Cost per second (with Striking Asp discount if active)



459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
# File 'documented/gemstone/psms/qstrike.rb', line 459

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_calculationvoid

This method returns an undefined value.

Display detailed calculation breakdown for debugging Shows all intermediate values used in cost calculation



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
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
# File 'documented/gemstone/psms/qstrike.rb', line 482

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

Get a default value

Parameters:

  • key (Symbol)

    Setting name

Returns:

  • (Object)

    Current value



98
99
100
101
# File 'documented/gemstone/psms/qstrike.rb', line 98

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

.defaultsHash

Get current defaults

Returns:

  • (Hash)

    Current default settings



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

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

.defaults=(new_defaults) ⇒ Object

Set all defaults at once

Parameters:

  • new_defaults (Hash)

    New default values



80
81
82
83
84
# File 'documented/gemstone/psms/qstrike.rb', line 80

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

.defined_module?(mod_name) ⇒ Boolean

Check if a module is defined

Parameters:

  • mod_name (Symbol)

    Module name

Returns:

  • (Boolean)


674
675
676
677
678
# File 'documented/gemstone/psms/qstrike.rb', line 674

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

.detect_attack_type(name) ⇒ Symbol

Detect what type of attack this is

Parameters:

  • name (String)

    Normalized attack name

Returns:

  • (Symbol)

    :cman, :weapon, :shield, or :basic



780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
# File 'documented/gemstone/psms/qstrike.rb', line 780

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) ⇒ Object

Execute the attack command using appropriate method

Parameters:

  • attack (String, Symbol)

    Attack name or command

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

    Target for the attack



744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
# File 'documented/gemstone/psms/qstrike.rb', line 744

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) ⇒ Object

Execute the qstrike command

Parameters:

  • reduction (Integer)

    Seconds of reduction



734
735
736
737
738
# File 'documented/gemstone/psms/qstrike.rb', line 734

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

  fput "qstrike -#{reduction}"
end

.find_max_seconds(available_stamina, cost_per_second) ⇒ Integer

Find maximum seconds of reduction affordable

Parameters:

  • available_stamina (Integer)

    Stamina available for QSTRIKE

  • cost_per_second (Integer)

    Cost per second of reduction

Returns:

  • (Integer)

    Max seconds (0 if can’t afford any)



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

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?

Find weapon stats using multiple lookup strategies Tries: noun, then extracts weapon type from full name

Parameters:

  • hand (GameObj)

    The hand to check

Returns:

  • (Hash, nil)

    WeaponStats data or nil if not found



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'documented/gemstone/psms/qstrike.rb', line 363

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) ⇒ String

Get a cache key for a hand (handles empty hands)

Parameters:

  • hand (GameObj, nil)

    The hand object

Returns:

  • (String)

    Cache key representing this hand state



586
587
588
589
590
591
592
# File 'documented/gemstone/psms/qstrike.rb', line 586

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_settingsvoid

This method returns an undefined value.

Load settings from DB_Store (per-character)



111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'documented/gemstone/psms/qstrike.rb', line 111

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

Look up stamina cost for a CMan or Weapon technique

Parameters:

  • name (String, Symbol)

    Attack name

Returns:

  • (Integer)

    Stamina cost, or 0 if not found



637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
# File 'documented/gemstone/psms/qstrike.rb', line 637

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

Generic technique cost lookup

Parameters:

  • name (String)

    Normalized attack name

  • type (Symbol)

    Technique type (:cman, :weapon, :shield)

Returns:

  • (Integer)

    Stamina cost, or 0 if not found



659
660
661
662
663
664
665
666
667
668
669
# File 'documented/gemstone/psms/qstrike.rb', line 659

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

Normalize attack name for lookup

Parameters:

  • attack (String, Symbol)

    Attack name or command

Returns:

  • (String)

    Normalized name



727
728
729
# File 'documented/gemstone/psms/qstrike.rb', line 727

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

.primary_equipment_speedInteger

Get primary hand equipment speed For ranged: LEFT hand is primary For melee: RIGHT hand is primary

Returns:

  • (Integer)

    Equipment speed (base_rt * multiplier)



439
440
441
442
# File 'documented/gemstone/psms/qstrike.rb', line 439

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

.ranged_weapon?Boolean

Determine if using ranged weapon (bow/crossbow in LEFT hand)

Returns:

  • (Boolean)

    true if ranged weapon detected in left hand



402
403
404
405
406
407
408
# File 'documented/gemstone/psms/qstrike.rb', line 402

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

Calculate reduction needed to achieve target absolute RT

Parameters:

  • target_rt (Integer)

    Desired final roundtime

Returns:

  • (Integer)

    Seconds of reduction needed (0 if target >= base)



349
350
351
352
353
354
# File 'documented/gemstone/psms/qstrike.rb', line 349

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_defaultsObject

Reset defaults to factory values



104
105
106
107
# File 'documented/gemstone/psms/qstrike.rb', line 104

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

.reset_settings_cachevoid

This method returns an undefined value.

Reset settings loaded flag (for testing)



136
137
138
139
# File 'documented/gemstone/psms/qstrike.rb', line 136

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

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

Resolve reduction value to actual seconds Handles :max, negative (reduce by N), and positive (target RT)

Parameters:

  • reduction (Integer, Symbol)

    Reduction specification

  • reserve (Integer)

    Stamina reserve

  • attack_cost (Integer)

    Attack stamina cost

Returns:

  • (Integer, nil)

    Actual seconds of reduction, or nil if invalid



704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
# File 'documented/gemstone/psms/qstrike.rb', line 704

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_settingsvoid

This method returns an undefined value.

Save settings to DB_Store (per-character)



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

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

Get secondary hand equipment speed For ranged: RIGHT hand is secondary For melee: LEFT hand is secondary Only weapons count - shields and non-weapons = 0

Returns:

  • (Integer)

    Equipment speed (base_rt * multiplier), or 0 if not a weapon



450
451
452
453
# File 'documented/gemstone/psms/qstrike.rb', line 450

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) ⇒ Object

Set a single default value (persists to DB_Store for per-character storage)

Parameters:

  • key (Symbol)

    Setting name (:reserve, :adaptive)

  • value (Object)

    New value



89
90
91
92
93
# File 'documented/gemstone/psms/qstrike.rb', line 89

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

.striking_asp_active?Boolean

Check if Striking Asp stance is active

Returns:

  • (Boolean)


554
555
556
557
558
559
560
# File 'documented/gemstone/psms/qstrike.rb', line 554

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

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

.striking_asp_multiplierFloat

Get the cost multiplier based on Striking Asp status

Returns:

  • (Float)

    1.0 if not active, or discounted multiplier if active



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

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_rankInteger

Get Striking Asp rank (1, 2, or 3)

Returns:

  • (Integer)

    Rank, or 0 if not known



564
565
566
567
568
569
570
# File 'documented/gemstone/psms/qstrike.rb', line 564

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

Execute QSTRIKE with an attack command

Examples:

Reduce RT by 3 seconds

QStrike.use(reduction: -3, attack: "cripple", target: "kobold")

Target absolute 2 second RT

QStrike.use(reduction: 2, attack: "attack", target: "troll")

Use maximum affordable reduction

QStrike.use(reduction: :max, attack: "mstrike")

Parameters:

  • reduction (Integer)

    RT reduction: negative = reduce by N seconds, positive = target absolute RT

  • attack (String, Symbol)

    Attack to perform (e.g., “cripple”, “attack”, “mstrike”)

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

    Target for the attack (optional)

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

    Stamina to reserve (uses default if nil)

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

    If true, use max affordable when can’t afford requested (uses default if nil)

Returns:

  • (Hash)

    Result with :success, :reduction_used, :reason, etc.



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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'documented/gemstone/psms/qstrike.rb', line 256

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

Check if cached value is still valid (hands haven’t changed)

Returns:

  • (Boolean)


596
597
598
599
600
601
602
603
604
605
# File 'documented/gemstone/psms/qstrike.rb', line 596

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

Get weapon stats and equipment speed for a hand

Parameters:

  • hand (GameObj)

    The hand to check

Returns:

  • (Hash)

    { base_rt: N, category: :sym, equipment_speed: N }



414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# File 'documented/gemstone/psms/qstrike.rb', line 414

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