Class: Lich::Common::Spell

Inherits:
Object
  • Object
show all
Defined in:
documented/common/spell.rb

Overview

Manages spell-related functionality in the Lich project.

This class handles the loading, casting, and management of spells.

Constant Summary collapse

@@load_mutex =
Mutex.new
@@after_stance =
nil
@@prepare_regex =
Regexp.union(
  /^You already have a spell readied!  You must RELEASE it if you wish to prepare another!$/,
  /^Your spell(?:song)? is ready\./,
  /^You can't think clearly enough to prepare a spell!$/,
  /^You are concentrating too intently .*?to prepare a spell\.$/,
  /^You are too injured to make that dextrous of a movement/,
  /^The searing pain in your throat makes that impossible/,
  /^But you don't have any mana!\.$/,
  /^You can't make that dextrous of a move!$/,
  /^As you begin to prepare the spell the wind blows small objects at you thwarting your attempt\.$/,
  /^You do not know that spell!$/,
  /^All you manage to do is cough up some blood\.$/,
  /^The incantations of countless spells swirl through your mind as a golden light flashes before your eyes\./
)
@@results_regex =
Regexp.union(
  /^(?:Cast|Sing) Roundtime [0-9]+ Seconds?\.$/,
  /^Cast at what\?$/,
  /^But you don't have any mana!$/,
  /^You don't have a spell prepared!$/,
  /keeps? the spell from working\./,
  /^Be at peace my child, there is no need for spells of war in here\.$/,
  /Spells of War cannot be cast/,
  /^As you focus on your magic, your vision swims with a swirling haze of crimson\.$/,
  /^Your magic fizzles ineffectually\.$/,
  /^All you manage to do is cough up some blood\.$/,
  /^And give yourself away!  Never!$/,
  /^You are unable to do that right now\.$/,
  /^You feel a sudden rush of power as you absorb [0-9]+ mana!$/,
  /^You are unable to drain it!$/,
  /leaving you casting at nothing but thin air!$/,
  /^You don't seem to be able to move to do that\.$/,
  /^Provoking a GameMaster is not such a good idea\.$/,
  /^You can't think clearly enough to prepare a spell!$/,
  /^You do not currently have a target\.$/,
  /The incantations of countless spells swirl through your mind as a golden light flashes before your eyes\./,
  /You can only evoke certain spells\./,
  /You can only channel certain spells for extra power\./,
  /That is not something you can prepare\./,
  /^\[Spell preparation time: \d seconds?\]$/,
  /^You are too injured to make that dextrous of a movement/,
  /^You can't make that dextrous of a move!$/
)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(xml_spell) ⇒ Spell

Initializes a new spell instance from the provided XML data.

Parameters:

  • xml_spell (OpenStruct)

    the XML data representing the spell



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
# File 'documented/common/spell.rb', line 72

def initialize(xml_spell)
  @num = xml_spell.attributes['number'].to_i
  @name = xml_spell.attributes['name']
  @type = xml_spell.attributes['type']
  @no_incant = ((xml_spell.attributes['incant'] == 'no') ? true : false)
  if xml_spell.attributes['availability'] == 'all'
    @availability = 'all'
  elsif xml_spell.attributes['availability'] == 'group'
    @availability = 'group'
  else
    @availability = 'self-cast'
  end
  @bonus = Hash.new
  xml_spell.elements.find_all { |e| e.name == 'bonus' }.each { |e|
    @bonus[e.attributes['type']] = e.text
  }
  @msgup = xml_spell.elements.find_all { |e| (e.name == 'message') and (e.attributes['type'].downcase == 'start') }.collect { |e| e.text }.join('$|^')
  @msgup = nil if @msgup.empty?
  @msgdn = xml_spell.elements.find_all { |e| (e.name == 'message') and (e.attributes['type'].downcase == 'end') }.collect { |e| e.text }.join('$|^')
  @msgdn = nil if @msgdn.empty?
  @stance = ((xml_spell.attributes['stance'] =~ /^(yes|true)$/i) ? true : false)
  @channel = ((xml_spell.attributes['channel'] =~ /^(yes|true)$/i) ? true : false)
  @cost = Hash.new
  xml_spell.elements.find_all { |e| e.name == 'cost' }.each { |xml_cost|
    cost_type = xml_cost.attributes['type']&.downcase
    next unless cost_type # skip malformed cost elements

    @cost[cost_type] ||= Hash.new
    # cast-type defaults to 'self' if not specified (most cost elements omit it)
    if xml_cost.attributes['cast-type']&.downcase == 'target'
      @cost[cost_type]['target'] = xml_cost.text
    else
      @cost[cost_type]['self'] = xml_cost.text
    end
  }
  @duration = Hash.new
  xml_spell.elements.find_all { |e| e.name == 'duration' }.each { |xml_duration|
    # cast-type defaults to 'self' if not specified
    if xml_duration.attributes['cast-type']&.downcase == 'target'
      cast_type = 'target'
    else
      cast_type = 'self'
      if xml_duration.attributes['real-time'] =~ /^(yes|true)$/i
        @real_time = true
      else
        @real_time = false
      end
    end
    @duration[cast_type] = Hash.new
    @duration[cast_type][:duration] = xml_duration.text
    span = xml_duration.attributes['span']&.downcase
    @duration[cast_type][:stackable] = (span == 'stackable')
    @duration[cast_type][:refreshable] = (span == 'refreshable')
    if xml_duration.attributes['multicastable'] =~ /^(yes|true)$/i
      @duration[cast_type][:multicastable] = true
    else
      @duration[cast_type][:multicastable] = false
    end
    if xml_duration.attributes['persist-on-death'] =~ /^(yes|true)$/i
      @persist_on_death = true
    else
      @persist_on_death = false
    end
    if xml_duration.attributes['max']
      @duration[cast_type][:max_duration] = xml_duration.attributes['max'].to_f
    else
      @duration[cast_type][:max_duration] = 250.0
    end
  }
  @cast_proc = xml_spell.elements['cast-proc']&.text
  @last_cast = Time.at(0)
  @timestamp = Time.now
  @timeleft = 0
  @active = false
  @circle = (num.to_s.length == 3 ? num.to_s[0..0] : num.to_s[0..1])
  @@list.push(self) unless @@list.find { |spell| spell.num == @num }
  # self # rubocop Lint/Void: self used in void context
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(*args) ⇒ Integer?

Handles calls to methods that are not explicitly defined.

Parameters:

  • args (Array)

    the arguments passed to the missing method

Returns:

  • (Integer, nil)

    the result of the method call or nil if not found



929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
# File 'documented/common/spell.rb', line 929

def method_missing(*args)
  if @@bonus_list.include?(args[0].to_s.gsub('_', '-'))
    if @bonus[args[0].to_s.gsub('_', '-')]
      proc { eval(@bonus[args[0].to_s.gsub('_', '-')]) }.call.to_i
    else
      0
    end
  elsif @@bonus_list.include?(args[0].to_s.sub(/_formula$/, '').gsub('_', '-'))
    @bonus[args[0].to_s.sub(/_formula$/, '').gsub('_', '-')].dup
  elsif (args[0].to_s =~ /_cost(?:_formula)?$/) and @@cost_list.include?(args[0].to_s.sub(/_formula$/, '').sub(/_cost$/, ''))
    options = args[1].to_hash
    if options[:caster] and (options[:caster] !~ /^(?:self|#{XMLData.name})$/i)
      if options[:target] and (options[:target].downcase == options[:caster].downcase)
        formula = @cost[args[0].to_s.sub(/_formula$/, '').sub(/_cost$/, '')]['self'].dup
      else
        formula = @cost[args[0].to_s.sub(/_formula$/, '').sub(/_cost$/, '')]['target'].dup || @cost[args[0].to_s.gsub('_', '-')]['self'].dup
      end
      skills = { 'Spells.minorelemental' => "SpellRanks['#{options[:caster]}'].minorelemental.to_i", 'Spells.majorelemental' => "SpellRanks['#{options[:caster]}'].majorelemental.to_i", 'Spells.minorspiritual' => "SpellRanks['#{options[:caster]}'].minorspiritual.to_i", 'Spells.majorspiritual' => "SpellRanks['#{options[:caster]}'].majorspiritual.to_i", 'Spells.wizard' => "SpellRanks['#{options[:caster]}'].wizard.to_i", 'Spells.sorcerer' => "SpellRanks['#{options[:caster]}'].sorcerer.to_i", 'Spells.ranger' => "SpellRanks['#{options[:caster]}'].ranger.to_i", 'Spells.paladin' => "SpellRanks['#{options[:caster]}'].paladin.to_i", 'Spells.empath' => "SpellRanks['#{options[:caster]}'].empath.to_i", 'Spells.cleric' => "SpellRanks['#{options[:caster]}'].cleric.to_i", 'Spells.bard' => "SpellRanks['#{options[:caster]}'].bard.to_i", 'Stats.level' => '100' }
      skills.each_pair { |a, b| formula.gsub!(a, b) }
    else
      if options[:target] and (options[:target] !~ /^(?:self|#{XMLData.name})$/i)
        formula = @cost[args[0].to_s.sub(/_formula$/, '').sub(/_cost$/, '')]['target'].dup || @cost[args[0].to_s.gsub('_', '-')]['self'].dup
      else
        formula = @cost[args[0].to_s.sub(/_formula$/, '').sub(/_cost$/, '')]['self'].dup
      end
    end
    if args[0].to_s =~ /mana/ and Spell[597].active? # Rapid Fire Penalty
      formula = "#{formula}+5"
    end
    if options[:multicast].to_i > 1
      formula = "(#{formula})*#{options[:multicast].to_i}"
    end
    if args[0].to_s =~ /_formula$/
      formula.dup
    else
      if formula
        proc { eval(formula) }.call.to_i
      else
        0
      end
    end
  else
    respond 'missing method: ' + args.inspect.to_s
    raise NoMethodError
  end
end

Instance Attribute Details

#activeObject

Returns the value of attribute active.



23
24
25
# File 'documented/common/spell.rb', line 23

def active
  @active
end

#availabilityObject (readonly)

Returns the value of attribute availability.



23
24
25
# File 'documented/common/spell.rb', line 23

def availability
  @availability
end

#cast_procObject (readonly)

Returns the value of attribute cast_proc.



23
24
25
# File 'documented/common/spell.rb', line 23

def cast_proc
  @cast_proc
end

#channelObject

Returns the value of attribute channel.



24
25
26
# File 'documented/common/spell.rb', line 24

def channel
  @channel
end

#circleObject (readonly)

Returns the value of attribute circle.



23
24
25
# File 'documented/common/spell.rb', line 23

def circle
  @circle
end

#last_castObject (readonly)

Returns the value of attribute last_cast.



23
24
25
# File 'documented/common/spell.rb', line 23

def last_cast
  @last_cast
end

#msgdnObject (readonly)

Returns the value of attribute msgdn.



23
24
25
# File 'documented/common/spell.rb', line 23

def msgdn
  @msgdn
end

#msgupObject (readonly)

Returns the value of attribute msgup.



23
24
25
# File 'documented/common/spell.rb', line 23

def msgup
  @msgup
end

#nameObject (readonly)

Returns the value of attribute name.



23
24
25
# File 'documented/common/spell.rb', line 23

def name
  @name
end

#no_incantObject (readonly)

Returns the value of attribute no_incant.



23
24
25
# File 'documented/common/spell.rb', line 23

def no_incant
  @no_incant
end

#numObject (readonly)

Returns the value of attribute num.



23
24
25
# File 'documented/common/spell.rb', line 23

def num
  @num
end

#persist_on_deathObject (readonly)

Returns the value of attribute persist_on_death.



23
24
25
# File 'documented/common/spell.rb', line 23

def persist_on_death
  @persist_on_death
end

#real_timeObject (readonly)

Returns the value of attribute real_time.



23
24
25
# File 'documented/common/spell.rb', line 23

def real_time
  @real_time
end

#stanceObject

Returns the value of attribute stance.



24
25
26
# File 'documented/common/spell.rb', line 24

def stance
  @stance
end

#timestampObject (readonly)

Returns the value of attribute timestamp.



23
24
25
# File 'documented/common/spell.rb', line 23

def timestamp
  @timestamp
end

#typeObject (readonly)

Returns the value of attribute type.



23
24
25
# File 'documented/common/spell.rb', line 23

def type
  @type
end

Instance Method Details

#_bonusHash

Retrieves the bonus attributes of the spell.

Returns:

  • (Hash)

    a hash of bonus attributes



916
917
918
# File 'documented/common/spell.rb', line 916

def _bonus
  @bonus.dup
end

#_costHash

Retrieves the cost attributes of the spell.

Returns:

  • (Hash)

    a hash of cost attributes



922
923
924
# File 'documented/common/spell.rb', line 922

def _cost
  @cost.dup
end

#active?Boolean

Checks if the spell is currently active.

Returns:

  • (Boolean)

    true if the spell is active, false otherwise



383
384
385
# File 'documented/common/spell.rb', line 383

def active?
  (self.timeleft > 0) and @active
end

#affordable?(options = {}) ⇒ Boolean

Checks if the spell can be cast based on the character's resources.

Parameters:

  • options (Hash) (defaults to: {})

    options for checking affordability

Returns:

  • (Boolean)

    true if the spell can be afforded, false otherwise



615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
# File 'documented/common/spell.rb', line 615

def affordable?(options = {})
  # fixme: deal with them dirty bards!
  release_options = options.dup
  release_options[:multicast] = nil
  if (self.stamina_cost(options) > 0) and (Spell[9699].active? or not Char.stamina >= self.stamina_cost(options) or Effects::Debuffs.active?("Overexerted"))
    false
  elsif (self.spirit_cost(options) > 0) and not (Char.spirit >= (self.spirit_cost(options) + 1 + [9912, 9913, 9914, 9916, 9916, 9916].delete_if { |num| !Spell[num].active? }.length))
    false
  elsif (self.mana_cost(options) > 0)
    ## convert Spell[9699].active? to Effects::Debuffs test (if Debuffs is where it shows)
    if (Feat.known?(:mental_acuity) and self.num.between?(1201, 1220)) and (Spell[9699].active? or not Char.stamina >= (self.mana_cost(options) * 2) or Effects::Debuffs.active?("Overexerted"))
      false
    elsif (!(Feat.known?(:mental_acuity) and self.num.between?(1201, 1220))) and !(Char.mana >= self.mana_cost(options))
      false
    else
      true
    end
  else
    true
  end
end

#available?(options = {}) ⇒ Boolean

Checks if the spell is available for casting based on the provided options.

Parameters:

  • options (Hash) (defaults to: {})

    options for checking availability

Returns:

  • (Boolean)

    true if the spell is available, false otherwise



534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
# File 'documented/common/spell.rb', line 534

def available?(options = {})
  if self.known?
    if options[:caster] and (options[:caster] !~ /^(?:self|#{XMLData.name})$/i)
      if options[:target] and (options[:target].downcase == options[:caster].downcase)
        true
      else
        @availability == 'all'
      end
    else
      if options[:target] and (options[:target] !~ /^(?:self|#{XMLData.name})$/i)
        @availability == 'all'
      else
        true
      end
    end
  else
    false
  end
end

#boltASObject



999
# File 'documented/common/spell.rb', line 999

def boltAS;        self.bolt_as_formula;             end

#boltDSObject



1001
# File 'documented/common/spell.rb', line 1001

def boltDS;        self.bolt_ds_formula;             end

#cast(target = nil, results_of_interest = nil, arg_options = nil, force_stance: nil) ⇒ String?

Casts the spell on the specified target with optional arguments.

Parameters:

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

    the target of the spell (defaults to nil)

  • results_of_interest (Regexp, nil) (defaults to: nil)

    regex for filtering results (defaults to nil)

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

    additional casting options (defaults to nil)

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

    whether to force a specific stance (defaults to nil)

Returns:

  • (String, nil)

    the result of the cast or nil if unsuccessful



661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
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
730
731
732
733
734
735
736
737
738
739
740
741
742
743
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
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
# File 'documented/common/spell.rb', line 661

def cast(target = nil, results_of_interest = nil, arg_options = nil, force_stance: nil)
  # fixme: find multicast in target and check mana for it
  check_energy = proc {
    if Feat.known?(:mental_acuity)
      unless (self.mana_cost <= 0) or Char.stamina >= (self.mana_cost * 2)
        echo 'cast: not enough stamina there, Monk!'
        sleep 0.1
        return false
      end
    else
      unless (self.mana_cost <= 0) or Char.mana >= self.mana_cost
        echo 'cast: not enough mana'
        sleep 0.1
        return false
      end
    end
    unless (self.spirit_cost <= 0) or Char.spirit >= (self.spirit_cost + 1 + [9912, 9913, 9914, 9916, 9916, 9916].delete_if { |num| !Spell[num].active? }.length)
      echo 'cast: not enough spirit'
      sleep 0.1
      return false
    end
    unless (self.stamina_cost <= 0) or Char.stamina >= self.stamina_cost
      echo 'cast: not enough stamina'
      sleep 0.1
      return false
    end
  }
  script = Script.current
  if @type.nil?
    echo "cast: spell missing type (#{@name})"
    sleep 0.1
    return false
  end
  check_energy.call
  begin
    save_want_downstream = script.want_downstream
    save_want_downstream_xml = script.want_downstream_xml
    script.want_downstream = true
    script.want_downstream_xml = false
    @@cast_lock.push(script)
    until (@@cast_lock.first == script) or @@cast_lock.empty?
      sleep 0.1
      Script.current # allows this loop to be paused
      @@cast_lock.delete_if { |s| s.paused or not Script.list.include?(s) }
    end
    check_energy.call
    if @cast_proc
      waitrt?
      waitcastrt?
      check_energy.call
      begin
        proc { eval(@cast_proc) }.call
      rescue
        echo "cast: error: #{$!}"
        respond $!.backtrace[0..2]
        return false
      end
    else
      if @channel
        cast_cmd = 'channel'
      else
        cast_cmd = 'cast'
      end
      unless (arg_options.nil? || arg_options.empty?)
        if arg_options.split(" ")[0] =~ /incant|channel|evoke|cast/
          cast_cmd = arg_options.split(" ")[0]
          arg_options = arg_options.split(" ").drop(1)
          arg_options = arg_options.join(" ") unless arg_options.empty?
        end
      end

      if (((target.nil? || target.to_s.empty?) && !(@no_incant)) && (cast_cmd == "cast" && arg_options.nil?) || cast_cmd == "incant") && cast_cmd !~ /^(?:channel|evoke)/
        cast_cmd = "incant #{@num}"
      elsif (target.nil? or target.to_s.empty?) and (@type =~ /attack/i) and not [410, 435, 525, 912, 909, 609].include?(@num)
        cast_cmd += ' target'
      elsif target.is_a?(GameObj)
        cast_cmd += " ##{target.id}"
      elsif target.is_a?(Integer)
        cast_cmd += " ##{target}"
      elsif cast_cmd !~ /^incant/
        cast_cmd += " #{target}"
      end

      unless (arg_options.nil? || arg_options.empty?)
        cast_cmd += " #{arg_options}"
      end

      cast_result = nil
      loop {
        waitrt?
        if cast_cmd =~ /^incant/
          if (checkprep != @name) and (checkprep != 'None')
            dothistimeout 'release', 5, /^You feel the magic of your spell rush away from you\.$|^You don't have a prepared spell to release!$/
          end
        else
          unless checkprep == @name
            unless checkprep == 'None'
              dothistimeout 'release', 5, /^You feel the magic of your spell rush away from you\.$|^You don't have a prepared spell to release!$/
              unless (self.mana_cost <= 0) or Char.mana >= self.mana_cost
                echo 'cast: not enough mana'
                sleep 0.1
                return false
              end
              unless (self.spirit_cost <= 0) or Char.spirit >= (self.spirit_cost + 1 + (if checkspell(9912) then 1 else 0 end) + (if checkspell(9913) then 1 else 0 end) + (if checkspell(9914) then 1 else 0 end) + (if checkspell(9916) then 5 else 0 end))
                echo 'cast: not enough spirit'
                sleep 0.1
                return false
              end
              unless (self.stamina_cost <= 0) or Char.stamina >= self.stamina_cost
                echo 'cast: not enough stamina'
                sleep 0.1
                return false
              end
            end
            loop {
              waitrt?
              waitcastrt?
              prepare_result = dothistimeout "prepare #{@num}", 8, @@prepare_regex
              if prepare_result =~ /^Your spell(?:song)? is ready\./
                break
              elsif prepare_result == 'You already have a spell readied!  You must RELEASE it if you wish to prepare another!'
                dothistimeout 'release', 5, /^You feel the magic of your spell rush away from you\.$|^You don't have a prepared spell to release!$/
                unless (self.mana_cost <= 0) or Char.mana >= self.mana_cost
                  echo 'cast: not enough mana'
                  sleep 0.1
                  return false
                end
              elsif prepare_result =~ /^You can't think clearly enough to prepare a spell!$|^You are concentrating too intently .*?to prepare a spell\.$|^You are too injured to make that dextrous of a movement|^The searing pain in your throat makes that impossible|^But you don't have any mana!\.$|^You can't make that dextrous of a move!$|^As you begin to prepare the spell the wind blows small objects at you thwarting your attempt\.$|^You do not know that spell!$|^All you manage to do is cough up some blood\.$|The incantations of countless spells swirl through your mind as a golden light flashes before your eyes\./
                sleep 0.1
                return prepare_result
              end
            }
          end
        end
        waitcastrt?
        if ((@stance && force_stance != false) || force_stance == true) && Char.stance != 'offensive'
          put 'stance offensive'
          # dothistimeout 'stance offensive', 5, /^You (?:are now in|move into) an? offensive stance|^You are unable to change your stance\.$/
        end
        if results_of_interest.is_a?(Regexp)
          merged_results_regex = Regexp.union(@@results_regex, results_of_interest)
        else
          merged_results_regex = @@results_regex
        end

        if Effects::Spells.active?("Armored Casting")
          merged_results_regex = Regexp.union(/^Roundtime: \d+ sec.$/, merged_results_regex)
        else
          merged_results_regex = Regexp.union(/^\[Spell Hindrance for/, merged_results_regex)
        end
        cast_result = dothistimeout cast_cmd, 5, merged_results_regex
        if cast_result == "You don't seem to be able to move to do that."
          100.times { break if clear.any? { |line| line =~ /^You regain control of your senses!$/ }; sleep 0.1 }
          cast_result = dothistimeout cast_cmd, 5, merged_results_regex
        end
        if cast_cmd =~ /^incant/i && cast_result =~ /^\[Spell preparation time: (\d) seconds?\]$/
          sleep(Regexp.last_match(1).to_i + 0.5)
          cast_result = dothistimeout cast_cmd, 5, merged_results_regex
        end
        if ((@stance && force_stance != false) || force_stance == true)
          if @@after_stance
            if Char.stance !~ /#{@@after_stance}/
              waitrt?
              dothistimeout "stance #{@@after_stance}", 3, /^You (?:are now in|move into) an? \w+ stance|^You are unable to change your stance\.$/
            end
          elsif Char.stance !~ /^guarded$|^defensive$/
            waitrt?
            if checkcastrt > 0
              dothistimeout 'stance guarded', 3, /^You (?:are now in|move into) an? \w+ stance|^You are unable to change your stance\.$/
            else
              dothistimeout 'stance defensive', 3, /^You (?:are now in|move into) an? \w+ stance|^You are unable to change your stance\.$/
            end
          end
        end
        if cast_result =~ /^Cast at what\?$|^Be at peace my child, there is no need for spells of war in here\.$|^Provoking a GameMaster is not such a good idea\.$/
          dothistimeout 'release', 5, /^You feel the magic of your spell rush away from you\.$|^You don't have a prepared spell to release!$/
        end
        if cast_result =~ /You can only evoke certain spells\.|You can only channel certain spells for extra power\./
          echo "cast: can't evoke/channel #{@num}"
          cast_cmd = cast_cmd.gsub(/^(?:evoke|channel)/, "cast")
          next
        end
        break unless ((@circle.to_i == 10) && (cast_result =~ /^\[Spell Hindrance for/))
      }
      cast_result
    end
  ensure
    @last_cast = Time.now
    script.want_downstream = save_want_downstream
    script.want_downstream_xml = save_want_downstream_xml
    @@cast_lock.delete(script)
  end
end

#castProcObject



1011
# File 'documented/common/spell.rb', line 1011

def castProc;      @cast_proc;                       end

#circle_nameString

Retrieves the name of the spell's circle.

Returns:

  • (String)

    the name of the circle



978
979
980
# File 'documented/common/spell.rb', line 978

def circle_name
  Spells.get_circle_name(@circle)
end

#circlenameObject



1014
# File 'documented/common/spell.rb', line 1014

def circlename;    self.circle_name;                 end

#clear_on_deathBoolean

Checks if the spell should be cleared upon death.

Returns:

  • (Boolean)

    true if the spell should be cleared, false otherwise



984
985
986
# File 'documented/common/spell.rb', line 984

def clear_on_death
  !@persist_on_death
end

#commandObject



1013
# File 'documented/common/spell.rb', line 1013

def command;       nil;                              end

#costString

Retrieves the mana cost of the spell.

Returns:

  • (String)

    the mana cost formula or '0' if none



993
# File 'documented/common/spell.rb', line 993

def cost;          self.mana_cost_formula    || '0'; end

#durationString

Retrieves the duration of the spell.

Returns:

  • (String)

    the duration formula



990
# File 'documented/common/spell.rb', line 990

def duration;      self.time_per_formula;            end

#elementalCSObject



1003
# File 'documented/common/spell.rb', line 1003

def elementalCS;   self.elemental_cs_formula;        end

#elementalTDObject



1007
# File 'documented/common/spell.rb', line 1007

def elementalTD;   self.elemental_td_formula;        end

#force_cast(target = nil, arg_options = nil, results_of_interest = nil, force_stance: nil) ⇒ String?

Forces the casting of the spell on the specified target with optional arguments.

Parameters:

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

    the target of the spell (defaults to nil)

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

    additional casting options (defaults to nil)

  • results_of_interest (Regexp, nil) (defaults to: nil)

    regex for filtering results (defaults to nil)

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

    whether to force a specific stance (defaults to nil)

Returns:

  • (String, nil)

    the result of the cast or nil if unsuccessful



861
862
863
864
865
866
867
868
# File 'documented/common/spell.rb', line 861

def force_cast(target = nil, arg_options = nil, results_of_interest = nil, force_stance: nil)
  unless arg_options.nil? || arg_options.empty?
    arg_options = "cast #{arg_options}"
  else
    arg_options = "cast"
  end
  cast(target, results_of_interest, arg_options, force_stance: force_stance)
end

#force_channel(target = nil, arg_options = nil, results_of_interest = nil, force_stance: nil) ⇒ String?

Forces the channeling of the spell on the specified target with optional arguments.

Parameters:

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

    the target of the spell (defaults to nil)

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

    additional channeling options (defaults to nil)

  • results_of_interest (Regexp, nil) (defaults to: nil)

    regex for filtering results (defaults to nil)

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

    whether to force a specific stance (defaults to nil)

Returns:

  • (String, nil)

    the result of the channeling or nil if unsuccessful



876
877
878
879
880
881
882
883
# File 'documented/common/spell.rb', line 876

def force_channel(target = nil, arg_options = nil, results_of_interest = nil, force_stance: nil)
  unless arg_options.nil? || arg_options.empty?
    arg_options = "channel #{arg_options}"
  else
    arg_options = "channel"
  end
  cast(target, results_of_interest, arg_options, force_stance: force_stance)
end

#force_evoke(target = nil, arg_options = nil, results_of_interest = nil, force_stance: nil) ⇒ String?

Forces the evocation of the spell on the specified target with optional arguments.

Parameters:

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

    the target of the spell (defaults to nil)

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

    additional evocation options (defaults to nil)

  • results_of_interest (Regexp, nil) (defaults to: nil)

    regex for filtering results (defaults to nil)

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

    whether to force a specific stance (defaults to nil)

Returns:

  • (String, nil)

    the result of the evocation or nil if unsuccessful



891
892
893
894
895
896
897
898
# File 'documented/common/spell.rb', line 891

def force_evoke(target = nil, arg_options = nil, results_of_interest = nil, force_stance: nil)
  unless arg_options.nil? || arg_options.empty?
    arg_options = "evoke #{arg_options}"
  else
    arg_options = "evoke"
  end
  cast(target, results_of_interest, arg_options, force_stance: force_stance)
end

#force_incant(arg_options = nil, results_of_interest = nil, force_stance: nil) ⇒ String?

Forces the incantation of the spell with optional arguments.

Parameters:

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

    additional incantation options (defaults to nil)

  • results_of_interest (Regexp, nil) (defaults to: nil)

    regex for filtering results (defaults to nil)

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

    whether to force a specific stance (defaults to nil)

Returns:

  • (String, nil)

    the result of the incantation or nil if unsuccessful



905
906
907
908
909
910
911
912
# File 'documented/common/spell.rb', line 905

def force_incant(arg_options = nil, results_of_interest = nil, force_stance: nil)
  unless arg_options.nil? || arg_options.empty?
    arg_options = "incant #{arg_options}"
  else
    arg_options = "incant"
  end
  cast(nil, results_of_interest, arg_options, force_stance: force_stance)
end

#incant=(val) ⇒ Object



560
561
562
# File 'documented/common/spell.rb', line 560

def incant=(val)
  @no_incant = !val
end

#incant?Boolean

Checks if the spell requires an incantation.

Returns:

  • (Boolean)

    true if the spell requires an incantation, false otherwise



556
557
558
# File 'documented/common/spell.rb', line 556

def incant?
  !@no_incant
end

#known?Boolean

Checks if the spell is known based on the character's ranks and circle.

Returns:

  • (Boolean)

    true if the spell is known, false otherwise



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
516
517
518
519
520
521
522
523
524
525
526
527
528
529
# File 'documented/common/spell.rb', line 470

def known?
  return true if defined?(Lich::Gemstone::SK) && Lich::Gemstone::SK.known?(self)
  if @num.to_s.length == 3
    circle_num = @num.to_s[0..0].to_i
  elsif @num.to_s.length == 4
    circle_num = @num.to_s[0..1].to_i
  else
    return false
  end
  if circle_num == 1
    ranks = [Spells.minorspiritual, XMLData.level].min
  elsif circle_num == 2
    ranks = [Spells.majorspiritual, XMLData.level].min
  elsif circle_num == 3
    ranks = [Spells.cleric, XMLData.level].min
  elsif circle_num == 4
    ranks = [Spells.minorelemental, XMLData.level].min
  elsif circle_num == 5
    ranks = [Spells.majorelemental, XMLData.level].min
  elsif circle_num == 6
    ranks = [Spells.ranger, XMLData.level].min
  elsif circle_num == 7
    ranks = [Spells.sorcerer, XMLData.level].min
  elsif circle_num == 9
    ranks = [Spells.wizard, XMLData.level].min
  elsif circle_num == 10
    ranks = [Spells.bard, XMLData.level].min
  elsif circle_num == 11
    ranks = [Spells.empath, XMLData.level].min
  elsif circle_num == 12
    ranks = [Spells.minormental, XMLData.level].min
  elsif circle_num == 16
    ranks = [Spells.paladin, XMLData.level].min
  elsif circle_num == 17
    if (@num == 1700) and (Stats.prof =~ /^(?:Wizard|Cleric|Empath|Sorcerer|Savant)$/)
      return true
    else
      return false
    end
  elsif (circle_num == 97) and (Society.status == 'Guardians of Sunfist')
    ranks = Society.rank
  elsif (circle_num == 98) and (Society.status == 'Order of Voln')
    ranks = Society.rank
  elsif (circle_num == 99) and (Society.status == 'Council of Light')
    ranks = Society.rank
  elsif (circle_num == 96)
    return false

  #          deprecate CMan from Spell class .known?
  #          See CMan, CMan.known? and CMan.available? methods in CMan class

  else
    return false
  end
  if (@num % 100) <= ranks.to_i
    return true
  else
    return false
  end
end

#manaCostObject



994
# File 'documented/common/spell.rb', line 994

def manaCost;      self.mana_cost_formula    || '0'; end

#max_duration(options = {}) ⇒ Float

Retrieves the maximum duration of the spell based on the provided options.

Parameters:

  • options (Hash) (defaults to: {})

    options for checking maximum duration

Returns:

  • (Float)

    the maximum duration in seconds



573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
# File 'documented/common/spell.rb', line 573

def max_duration(options = {})
  if options[:caster] and (options[:caster] !~ /^(?:self|#{XMLData.name})$/i)
    if options[:target] and (options[:target].downcase == options[:caster].downcase)
      @duration['self'][:max_duration]
    else
      @duration['target'][:max_duration] || @duration['self'][:max_duration]
    end
  else
    if options[:target] and (options[:target] !~ /^(?:self|#{XMLData.name})$/i)
      @duration['target'][:max_duration] || @duration['self'][:max_duration]
    else
      @duration['self'][:max_duration]
    end
  end
end

#mentalCSObject



1004
# File 'documented/common/spell.rb', line 1004

def mentalCS;      self.mental_cs_formula;           end

#mentalTDObject



1008
# File 'documented/common/spell.rb', line 1008

def mentalTD;      self.mental_td_formula;           end

#minsleftObject



366
367
368
# File 'documented/common/spell.rb', line 366

def minsleft
  self.timeleft
end

#multicastable?(options = {}) ⇒ Boolean

Checks if the spell can be multicast based on the provided options.

Parameters:

  • options (Hash) (defaults to: {})

    options for checking multicastability

Returns:

  • (Boolean)

    true if the spell is multicastable, false otherwise



444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# File 'documented/common/spell.rb', line 444

def multicastable?(options = {})
  if options[:caster] and (options[:caster] !~ /^(?:self|#{XMLData.name})$/i)
    if options[:target] and (options[:target].downcase == options[:caster].downcase)
      @duration['self'][:multicastable]
    else
      if @duration['target'][:multicastable].nil?
        @duration['self'][:multicastable]
      else
        @duration['target'][:multicastable]
      end
    end
  else
    if options[:target] and (options[:target] !~ /^(?:self|#{XMLData.name})$/i)
      if @duration['target'][:multicastable].nil?
        @duration['self'][:multicastable]
      else
        @duration['target'][:multicastable]
      end
    else
      @duration['self'][:multicastable]
    end
  end
end

#physicalASObject



1000
# File 'documented/common/spell.rb', line 1000

def physicalAS;    self.physical_as_formula;         end

#physicalDSObject



1002
# File 'documented/common/spell.rb', line 1002

def physicalDS;    self.physical_ds_formula;         end

#putdownvoid

This method returns an undefined value.

Deactivates the spell and resets its duration.



603
604
605
606
# File 'documented/common/spell.rb', line 603

def putdown
  self.timeleft = 0
  @active = false
end

#putup(options = {}) ⇒ void

This method returns an undefined value.

Activates the spell and sets its duration based on the provided options.

Parameters:

  • options (Hash) (defaults to: {})

    options for activating the spell



592
593
594
595
596
597
598
599
# File 'documented/common/spell.rb', line 592

def putup(options = {})
  if stackable?(options)
    self.timeleft = [self.timeleft + self.time_per(options), self.max_duration(options)].min
  else
    self.timeleft = [self.time_per(options), self.max_duration(options)].min
  end
  @active = true
end

#refreshable?(options = {}) ⇒ Boolean

Checks if the spell can be refreshed based on the provided options.

Parameters:

  • options (Hash) (defaults to: {})

    options for checking refreshability

Returns:

  • (Boolean)

    true if the spell is refreshable, false otherwise



417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'documented/common/spell.rb', line 417

def refreshable?(options = {})
  if options[:caster] and (options[:caster] !~ /^(?:self|#{XMLData.name})$/i)
    if options[:target] and (options[:target].downcase == options[:caster].downcase)
      @duration['self'][:refreshable]
    else
      if @duration['target'][:refreshable].nil?
        @duration['self'][:refreshable]
      else
        @duration['target'][:refreshable]
      end
    end
  else
    if options[:target] and (options[:target] !~ /^(?:self|#{XMLData.name})$/i)
      if @duration['target'][:refreshable].nil?
        @duration['self'][:refreshable]
      else
        @duration['target'][:refreshable]
      end
    else
      @duration['self'][:refreshable]
    end
  end
end

#remainingObject



608
609
610
# File 'documented/common/spell.rb', line 608

def remaining
  self.timeleft.as_time
end

#secsleftObject



370
371
372
# File 'documented/common/spell.rb', line 370

def secsleft
  self.timeleft * 60
end

#selfonlyObject



1015
# File 'documented/common/spell.rb', line 1015

def selfonly;      @availability != 'all';           end

#sorcererCSObject



1006
# File 'documented/common/spell.rb', line 1006

def sorcererCS;    self.sorcerer_cs_formula;         end

#sorcererTDObject



1010
# File 'documented/common/spell.rb', line 1010

def sorcererTD;    self.sorcerer_td_formula;         end

#spiritCostObject



995
# File 'documented/common/spell.rb', line 995

def spiritCost;    self.spirit_cost_formula  || '0'; end

#spiritCSObject



1005
# File 'documented/common/spell.rb', line 1005

def spiritCS;      self.spirit_cs_formula;           end

#spiritTDObject



1009
# File 'documented/common/spell.rb', line 1009

def spiritTD;      self.spirit_td_formula;           end

#stackable?(options = {}) ⇒ Boolean

Checks if the spell can be stacked based on the provided options.

Parameters:

  • options (Hash) (defaults to: {})

    options for checking stackability

Returns:

  • (Boolean)

    true if the spell is stackable, false otherwise



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'documented/common/spell.rb', line 390

def stackable?(options = {})
  if options[:caster] and (options[:caster] !~ /^(?:self|#{XMLData.name})$/i)
    if options[:target] and (options[:target].downcase == options[:caster].downcase)
      @duration['self'][:stackable]
    else
      if @duration['target'][:stackable].nil?
        @duration['self'][:stackable]
      else
        @duration['target'][:stackable]
      end
    end
  else
    if options[:target] and (options[:target] !~ /^(?:self|#{XMLData.name})$/i)
      if @duration['target'][:stackable].nil?
        @duration['self'][:stackable]
      else
        @duration['target'][:stackable]
      end
    else
      @duration['self'][:stackable]
    end
  end
end

#stacksObject



1012
# File 'documented/common/spell.rb', line 1012

def stacks;        self.stackable?                   end

#staminaCostString

Retrieves the stamina cost of the spell.

Returns:

  • (String)

    the stamina cost formula or '0' if none



998
# File 'documented/common/spell.rb', line 998

def staminaCost;   self.stamina_cost_formula || '0'; end

#time_per(options = {}) ⇒ Float

Calculates the time required for a spell based on the provided options.

Parameters:

  • options (Hash) (defaults to: {})

    options for calculating time

Returns:

  • (Float)

    the calculated time in seconds



331
332
333
334
335
336
337
338
339
340
# File 'documented/common/spell.rb', line 331

def time_per(options = {})
  formula = self.time_per_formula(options)
  if options[:line]
    # line = options[:line] rubocop useless assignment to line
    options[:line]
  end
  result = proc { eval(formula) }.call.to_f
  return 10.0 if defined?(Lich::Gemstone::SK) && Lich::Gemstone::SK.known?(self) && (result.nil? || result < 10)
  return result
end

#time_per_formula(options = {}) ⇒ String

Calculates the time required for a spell based on the provided options.

Parameters:

  • options (Hash) (defaults to: {})

    options for calculating time

Returns:

  • (String)

    the formula for time calculation



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
325
326
# File 'documented/common/spell.rb', line 276

def time_per_formula(options = {})
  activator_modifier = { 'tap' => 0.5, 'rub' => 1, 'wave' => 1, 'raise' => 1.33, 'drink' => 0, 'bite' => 0, 'eat' => 0, 'gobble' => 0 }
  can_haz_spell_ranks = /Spells\.(?:minorelemental|majorelemental|minorspiritual|majorspiritual|wizard|sorcerer|ranger|paladin|empath|cleric|bard|minormental)/
  skills = ['Spells.minorelemental', 'Spells.majorelemental', 'Spells.minorspiritual', 'Spells.majorspiritual', 'Spells.wizard', 'Spells.sorcerer', 'Spells.ranger', 'Spells.paladin', 'Spells.empath', 'Spells.cleric', 'Spells.bard', 'Spells.minormental', 'Skills.magicitemuse', 'Skills.arcanesymbols']
  if options[:caster] and (options[:caster] !~ /^(?:self|#{XMLData.name})$/i)
    if options[:target] and (options[:target].downcase == options[:caster].downcase)
      formula = @duration['self'][:duration].to_s.dup
    else
      formula = @duration['target'][:duration].dup || @duration['self'][:duration].to_s.dup
    end
    if options[:activator] =~ /^(#{activator_modifier.keys.join('|')})$/i
      if formula =~ can_haz_spell_ranks
        skills.each { |skill_name| formula.gsub!(skill_name, "(SpellRanks['#{options[:caster]}'].magicitemuse * #{activator_modifier[options[:activator]]}).to_i") }
        formula = "(#{formula})/2.0"
      elsif formula =~ /Skills\.(?:magicitemuse|arcanesymbols)/
        skills.each { |skill_name| formula.gsub!(skill_name, "(SpellRanks['#{options[:caster]}'].magicitemuse * #{activator_modifier[options[:activator]]}).to_i") }
      end
    elsif options[:activator] =~ /^(invoke|scroll)$/i
      if formula =~ can_haz_spell_ranks
        skills.each { |skill_name| formula.gsub!(skill_name, "SpellRanks['#{options[:caster]}'].arcanesymbols.to_i") }
        formula = "(#{formula})/2.0"
      elsif formula =~ /Skills\.(?:magicitemuse|arcanesymbols)/
        skills.each { |skill_name| formula.gsub!(skill_name, "SpellRanks['#{options[:caster]}'].arcanesymbols.to_i") }
      end
    else
      skills.each { |skill_name| formula.gsub!(skill_name, "SpellRanks[#{options[:caster].to_s.inspect}].#{skill_name.sub(/^(?:Spells|Skills)\./, '')}.to_i") }
    end
  else
    if options[:target] and (options[:target] !~ /^(?:self|#{XMLData.name})$/i)
      formula = @duration['target'][:duration].dup || @duration['self'][:duration].to_s.dup
    else
      formula = @duration['self'][:duration].to_s.dup
    end
    if options[:activator] =~ /^(#{activator_modifier.keys.join('|')})$/i
      if formula =~ can_haz_spell_ranks
        skills.each { |skill_name| formula.gsub!(skill_name, "(Skills.magicitemuse * #{activator_modifier[options[:activator]]}).to_i") }
        formula = "(#{formula})/2.0"
      elsif formula =~ /Skills\.(?:magicitemuse|arcanesymbols)/
        skills.each { |skill_name| formula.gsub!(skill_name, "(Skills.magicitemuse * #{activator_modifier[options[:activator]]}).to_i") }
      end
    elsif options[:activator] =~ /^(invoke|scroll)$/i
      if formula =~ can_haz_spell_ranks
        skills.each { |skill_name| formula.gsub!(skill_name, "Skills.arcanesymbols.to_i") }
        formula = "(#{formula})/2.0"
      elsif formula =~ /Skills\.(?:magicitemuse|arcanesymbols)/
        skills.each { |skill_name| formula.gsub!(skill_name, "Skills.arcanesymbols.to_i") }
      end
    end
  end
  formula
end

#timeleftFloat

Retrieves the remaining time for the spell.

Returns:

  • (Float)

    the time left in seconds



352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'documented/common/spell.rb', line 352

def timeleft
  if self.time_per_formula.to_s == 'Spellsong.timeleft'
    @timeleft = Spellsong.timeleft
  else
    @timeleft = @timeleft - ((Time.now - @timestamp) / 60.to_f)
    if @timeleft <= 0
      self.putdown
      return 0.to_f
    end
  end
  @timestamp = Time.now
  @timeleft
end

#timeleft=(val) ⇒ void

This method returns an undefined value.

Sets the remaining time for the spell.

Parameters:

  • val (Float)

    the time left in seconds



345
346
347
348
# File 'documented/common/spell.rb', line 345

def timeleft=(val)
  @timeleft = val
  @timestamp = Time.now
end

#to_sString

Returns the string representation of the spell.

Returns:

  • (String)

    the name of the spell



566
567
568
# File 'documented/common/spell.rb', line 566

def to_s
  @name.to_s
end