Class: Lich::Common::Spell

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

Overview

Represents a spell from the game's spell database, loaded from effect-list.xml.

Provides spell metadata (name, circle, type, duration, costs), active tracking, casting methods, and convenience accessors for calculated values like duration formulas and spell bonuses. Spells are globally indexed by number and name for fast lookup.

See Also:

  • Spell.load
  • Spell.[]

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

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Initializes a Spell from an XML element (from effect-list.xml).

Parses spell metadata including number, name, type, duration formulas, cost formulas, bonus formulas, and caster requirements from the XML node. Sets default duration to 250 seconds per self-cast unless specified. Registers this spell in the global list unless a spell with the same number already exists.

Parameters:

  • xml_spell (Ox::Element)

    an XML element with attributes and child elements describing the spell (e.g., number, name, type, channel, stance) and optional children (bonus, message, cost, duration, cast-proc)



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'documented/common/spell.rb', line 86

def initialize(xml_spell)
  @num = xml_spell['number'].to_i
  @name = xml_spell['name']
  @type = xml_spell['type']
  @no_incant = ((xml_spell['incant'] == 'no') ? true : false)
  if xml_spell['availability'] == 'all'
    @availability = 'all'
  elsif xml_spell['availability'] == 'group'
    @availability = 'group'
  else
    @availability = 'self-cast'
  end
  @bonus = Hash.new
  xml_spell.locate('bonus').each { |e|
    bonus_type = e['type']
    next unless bonus_type # skip malformed bonus elements

    @bonus[bonus_type] = e.text
  }
  @msgup = xml_spell.locate('message').select { |e| e['type'].downcase == 'start' }.collect { |e| e.text }.join('$|^')
  @msgup = nil if @msgup.empty?
  @msgdn = xml_spell.locate('message').select { |e| e['type'].downcase == 'end' }.collect { |e| e.text }.join('$|^')
  @msgdn = nil if @msgdn.empty?
  @stance = ((xml_spell['stance'] =~ /^(yes|true)$/i) ? true : false)
  @channel = ((xml_spell['channel'] =~ /^(yes|true)$/i) ? true : false)
  @cost = Hash.new
  xml_spell.locate('cost').each { |xml_cost|
    cost_type = xml_cost['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['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.locate('duration').each { |xml_duration|
    # cast-type defaults to 'self' if not specified
    if xml_duration['cast-type']&.downcase == 'target'
      cast_type = 'target'
    else
      cast_type = 'self'
      if xml_duration['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['span']&.downcase
    @duration[cast_type][:stackable] = (span == 'stackable')
    @duration[cast_type][:refreshable] = (span == 'refreshable')
    if xml_duration['multicastable'] =~ /^(yes|true)$/i
      @duration[cast_type][:multicastable] = true
    else
      @duration[cast_type][:multicastable] = false
    end
    if xml_duration['persist-on-death'] =~ /^(yes|true)$/i
      @persist_on_death = true
    else
      @persist_on_death = false
    end
    if xml_duration['max']
      @duration[cast_type][:max_duration] = xml_duration['max'].to_f
    else
      @duration[cast_type][:max_duration] = 250.0
    end
  }
  @cast_proc = xml_spell.locate('cast-proc').first&.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, ...

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Provides dynamic accessors for spell bonuses and costs based on XML data.

Supports three categories of dynamic methods:

  1. Bonus accessors (e.g., bolt_as, physical_ds):

    • Method name keys map to hyphens (e.g., bolt_asbolt-as).
    • Without _formula suffix: evaluates the formula and returns integer result.
    • With _formula suffix: returns the formula string as-is.
    • Returns 0 if the bonus key is not found.
  2. Cost accessors (e.g., mana_cost, spirit_cost):

    • Method name keys map to cost type with hyphens removed (e.g., mana_costmana).
    • Without _formula suffix: evaluates the formula with options and returns integer.
    • With _formula suffix: returns the formula string.
    • Handles :caster, :target, and :multicast options to select and rewrite formulas.
    • For mana costs, adds 5 if spell 597 (Rapid Fire Penalty) is active.
    • Returns 0 if the cost key is not found or formula is nil.
  3. Unknown methods raise NoMethodError with the method name.

Formula evaluation substitutes skill references (Spells.minorelemental, Skills.magicitemuse) with runtime values, including SpellRanks lookups for non-self casters.

Examples:

spell.bolt_as           #=> 10 (evaluated)
spell.bolt_as_formula   #=> "Spells.wizard + 5"
spell.mana_cost(caster: "Bob")  #=> 50 (evaluated for Bob's skills)

Parameters:

  • args (Array)

    method name and arguments; args is the method name, args is options hash (for costs)

Returns:

  • (Integer, String, nil)

    evaluated result (integer or formula string) or 0 if key not found



1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
# File 'documented/common/spell.rb', line 1291

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.



28
29
30
# File 'documented/common/spell.rb', line 28

def active
  @active
end

#availabilityObject (readonly)

Returns the value of attribute availability.



28
29
30
# File 'documented/common/spell.rb', line 28

def availability
  @availability
end

#cast_procObject (readonly)

Returns the value of attribute cast_proc.



28
29
30
# File 'documented/common/spell.rb', line 28

def cast_proc
  @cast_proc
end

#channelObject

Returns the value of attribute channel.



29
30
31
# File 'documented/common/spell.rb', line 29

def channel
  @channel
end

#circleObject (readonly)

Returns the value of attribute circle.



28
29
30
# File 'documented/common/spell.rb', line 28

def circle
  @circle
end

#last_castObject (readonly)

Returns the value of attribute last_cast.



28
29
30
# File 'documented/common/spell.rb', line 28

def last_cast
  @last_cast
end

#msgdnObject (readonly)

Returns the value of attribute msgdn.



28
29
30
# File 'documented/common/spell.rb', line 28

def msgdn
  @msgdn
end

#msgupObject (readonly)

Returns the value of attribute msgup.



28
29
30
# File 'documented/common/spell.rb', line 28

def msgup
  @msgup
end

#nameObject (readonly)

Returns the value of attribute name.



28
29
30
# File 'documented/common/spell.rb', line 28

def name
  @name
end

#no_incantObject (readonly)

Returns the value of attribute no_incant.



28
29
30
# File 'documented/common/spell.rb', line 28

def no_incant
  @no_incant
end

#numObject (readonly)

Returns the value of attribute num.



28
29
30
# File 'documented/common/spell.rb', line 28

def num
  @num
end

#persist_on_deathObject (readonly)

Returns the value of attribute persist_on_death.



28
29
30
# File 'documented/common/spell.rb', line 28

def persist_on_death
  @persist_on_death
end

#real_timeObject (readonly)

Returns the value of attribute real_time.



28
29
30
# File 'documented/common/spell.rb', line 28

def real_time
  @real_time
end

#stanceObject

Returns the value of attribute stance.



29
30
31
# File 'documented/common/spell.rb', line 29

def stance
  @stance
end

#timestampObject (readonly)

Returns the value of attribute timestamp.



28
29
30
# File 'documented/common/spell.rb', line 28

def timestamp
  @timestamp
end

#typeObject (readonly)

Returns the value of attribute type.



28
29
30
# File 'documented/common/spell.rb', line 28

def type
  @type
end

Instance Method Details

#_bonusHash{String => String}

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a copy of the spell's bonus formulas hash.

Bonus keys are spell attribute types (e.g., 'bolt-as', 'physical-ds', 'elemental-cs'). Values are formula strings evaluated at runtime. Used by method_missing for dynamic bonus accessors like #bolt_as and #physical_ds.

Returns:



1243
1244
1245
# File 'documented/common/spell.rb', line 1243

def _bonus
  @bonus.dup
end

#_costHash{String => Hash{String => String}}

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a copy of the spell's cost formulas hash.

Cost keys are resource types (e.g., 'mana', 'spirit', 'stamina'). Values are hashes mapping cast-type ('self', 'target') to formula strings. Used by method_missing for dynamic cost accessors like #mana_cost and #mana_cost_formula.

Returns:



1255
1256
1257
# File 'documented/common/spell.rb', line 1255

def _cost
  @cost.dup
end

#active?Boolean

Tests whether this spell is currently active.

A spell is active when both the remaining duration is greater than 0 AND the internal active flag is set to true (via #putup or #active=).

Returns:

  • (Boolean)

    true if duration > 0 and the spell is marked active

See Also:



535
536
537
# File 'documented/common/spell.rb', line 535

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

#affordable?(options = {}) ⇒ Boolean

Tests whether the player can currently cast this spell given resource constraints.

Checks if the player has sufficient mana, spirit, and stamina to cast the spell. For monks with the mental_acuity feat (circle 12 spells), uses stamina instead of mana for cost calculations. Accounts for active debuffs like Overexerted. For mana-costing spells with the mental_acuity feat, doubles the mana cost and checks stamina instead.

Examples:

Spell[505].affordable?  #=> true (if mana/spirit/stamina sufficient)

Parameters:

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

    optional parameters (reserved for future use)

Returns:

  • (Boolean)

    true if all required resources are available, false otherwise

See Also:



867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
# File 'documented/common/spell.rb', line 867

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

Tests whether this spell can be cast by a given caster on a given target.

A spell is available if it is #known? by the relevant caster AND the spell's availability allows casting on the target. Availability is either 'all' (group/other), 'group', or 'self-cast' (self only). When a :caster other than self is provided, availability is checked as 'all' for other targets. When a :target other than self is provided, availability is checked as 'all'.

Examples:

Spell[505].available?                      #=> true (if known and can self-cast)
Spell[505].available?(target: "Zeke")      #=> true (if known and 'all' availability)

Parameters:

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

    optional parameters for determining availability

Options Hash (options):

  • :caster (String)

    the caster's name

  • :target (String)

    the target's name

Returns:

  • (Boolean)

    true if #known? and availability requirements are met

See Also:



741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
# File 'documented/common/spell.rb', line 741

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

#boltASString

Deprecated.

Use #bolt_as_formula instead

Returns the spell's bolt attack strength formula (backward compatibility alias).

Returns:

  • (String)

    the formula, or nil if not defined



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

def boltAS;        self.bolt_as_formula;             end

#boltDSString

Deprecated.

Use #bolt_ds_formula instead

Returns the spell's bolt defense strength formula (backward compatibility alias).

Returns:

  • (String)

    the formula, or nil if not defined



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

def boltDS;        self.bolt_ds_formula;             end

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

Casts this spell, handling preparation, casting, and stance management.

A comprehensive casting method that handles spell preparation (if not incant), resource checks, target validation, the actual cast/incant/channel/evoke command, and post-cast stance restoration. Acquires the global cast lock to serialize casting across scripts.

Returns the final cast result string (e.g., "Cast Roundtime 5 Seconds.") or a status message if preparation or casting fails (e.g., "You don't have a spell prepared!").

Supports custom cast commands (incant, cast, channel, evoke) via arg_options. Handles stance enforcement: if spell.stance is true and force_stance is not false, moves to offensive stance before casting and returns to Spell.after_stance (if set) or guarded/defensive after casting.

For spells with a cast_proc (custom cast logic), evaluates that proc instead of generating a standard command.

Checks spell affordability (mana, spirit, stamina) before and during preparation. Automatically releases a prepared spell if a different spell is needed.

Examples:

result = Spell[505].cast("Lich")  # Cast on target
result = Spell[505].cast         # Self-cast

Parameters:

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

    the target, as object ID, object, or name

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

    a custom regex of additional successful cast outcomes

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

    space-separated cast command and arguments (e.g., "cast at ground")

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

    true to enforce stance, false to skip, nil for default

Returns:

  • (String)

    the cast result message from the game

See Also:



954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
# File 'documented/common/spell.rb', line 954

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

#castProcString?

Deprecated.

Use the #cast_proc reader instead

Returns this spell's custom cast procedure (backward compatibility alias).

Returns:

  • (String, nil)

    the cast-proc XML text, or nil if not defined



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

def castProc;      @cast_proc;                       end

#circle_nameString

Returns the human-readable name of this spell's circle.

Examples:

Spell[505].circle_name  #=> "Minor Spiritual"

Returns:

  • (String)

    the circle name (e.g., "Minor Spiritual", "Major Elemental", "Wizard")

See Also:

  • Spells.get_circle_name


1344
1345
1346
# File 'documented/common/spell.rb', line 1344

def circle_name
  Spells.get_circle_name(@circle)
end

#circlenameString

Deprecated.

Use #circle_name instead

Returns the human-readable circle name (backward compatibility alias).

Returns:

  • (String)

    the circle name



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

def circlename;    self.circle_name;                 end

#clear_on_deathBoolean

Tests whether this spell is cleared (removed) when the character dies.

The inverse of #persist_on_death; returns true if the spell does NOT persist across death.

Returns:

  • (Boolean)

    true if the spell is cleared on death, false if it persists

See Also:



1354
1355
1356
# File 'documented/common/spell.rb', line 1354

def clear_on_death
  !@persist_on_death
end

#commandnil

Deprecated.

Legacy API; use #cast directly instead

Returns the command string to cast this spell (backward compatibility alias).

Always returns nil; cast commands are generated dynamically by #cast.

Returns:

  • (nil)


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

def command;       nil;                              end

#costString

Deprecated.

Use #mana_cost_formula instead

Returns the mana cost formula for this spell (backward compatibility alias).

Returns:

  • (String)

    the mana cost formula, or '0' if not defined



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

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

#durationObject

for backwards compatiblity



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

def duration;      self.time_per_formula;            end

#elementalCSString

Deprecated.

Use #elemental_cs_formula instead

Returns the spell's elemental cast strength formula (backward compatibility alias).

Returns:

  • (String)

    the formula, or nil if not defined



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

def elementalCS;   self.elemental_cs_formula;        end

#elementalTDString

Deprecated.

Use #elemental_td_formula instead

Returns the spell's elemental trivial difficulty formula (backward compatibility alias).

Returns:

  • (String)

    the formula, or nil if not defined



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

def elementalTD;   self.elemental_td_formula;        end

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

Casts this spell using the standard "cast" command, bypassing incant.

Wrapper around #cast that prepends "cast" to arg_options, forcing the spell to use the cast command even if it would normally incant.

Examples:

Spell[505].force_cast("Lich")  #=> "Cast Roundtime 5 Seconds."

Parameters:

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

    the spell target

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

    additional command arguments (appended after "cast")

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

    custom successful cast outcomes

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

    true to enforce stance, false to skip, nil for default

Returns:

  • (String)

    the cast result message

See Also:



1161
1162
1163
1164
1165
1166
1167
1168
# File 'documented/common/spell.rb', line 1161

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

Casts this spell using the "channel" command, for spells that support channeling.

Wrapper around #cast that prepends "channel" to arg_options, forcing the spell to use the channel command. Returns an error string if the spell does not support channeling.

Examples:

Spell[505].force_channel("Lich")  #=> "Cast Roundtime 5 Seconds."

Parameters:

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

    the spell target

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

    additional command arguments (appended after "channel")

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

    custom successful cast outcomes

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

    true to enforce stance, false to skip, nil for default

Returns:

  • (String)

    the cast result message, or error if channeling not supported

See Also:



1183
1184
1185
1186
1187
1188
1189
1190
# File 'documented/common/spell.rb', line 1183

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

Casts this spell using the "evoke" command, for spells that support evoking.

Wrapper around #cast that prepends "evoke" to arg_options, forcing the spell to use the evoke command. Returns an error string if the spell does not support evoking.

Examples:

Spell[505].force_evoke("Lich")  #=> "Cast Roundtime 5 Seconds."

Parameters:

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

    the spell target

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

    additional command arguments (appended after "evoke")

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

    custom successful cast outcomes

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

    true to enforce stance, false to skip, nil for default

Returns:

  • (String)

    the cast result message, or error if evoking not supported

See Also:



1205
1206
1207
1208
1209
1210
1211
1212
# File 'documented/common/spell.rb', line 1205

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

Casts this spell using the "incant" command (verbal incantation).

Wrapper around #cast that prepends "incant" to arg_options, forcing the spell to use verbal incantation. No target parameter; incanting is always self-initiated.

Examples:

Spell[505].force_incant  #=> "Cast Roundtime 5 Seconds."

Parameters:

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

    additional command arguments (appended after "incant")

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

    custom successful cast outcomes

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

    true to enforce stance, false to skip, nil for default

Returns:

  • (String)

    the cast result message

See Also:



1226
1227
1228
1229
1230
1231
1232
1233
# File 'documented/common/spell.rb', line 1226

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

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Sets whether this spell requires an incantation.

Parameters:

  • val (Boolean)

    true to require incant, false to mark as no-incant

Returns:

  • (Boolean)

    the value assigned



773
774
775
# File 'documented/common/spell.rb', line 773

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

#incant?Boolean

Tests whether this spell requires an incantation.

Returns:

  • (Boolean)

    true if the spell requires spoken incantation, false if it is a no-incant spell



764
765
766
# File 'documented/common/spell.rb', line 764

def incant?
  !@no_incant
end

#known?Boolean

Tests whether the player character knows this spell.

Checks if the spell number falls within the player's trained circle ranks for the appropriate spell school (e.g., minorspiritual for circle 1, majorelemental for circle 5). Circle 17 spells (1700) are available only to Wizard, Cleric, Empath, Sorcerer, or Savant. Circles 96-99 (society/custom magic) are handled specially via Society status and rank.

If Lich::Gemstone::SK (spell knowledge system) is available, uses that directly.

Examples:

Spell[505].known?  #=> true (if trained in minorspiritual and within circle)

Returns:

  • (Boolean)

    true if the spell is trained and the character meets circle/level requirements

See Also:



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

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

#manaCostString

Deprecated.

Use #mana_cost_formula instead

Returns the mana cost formula for this spell (backward compatibility alias).

Returns:

  • (String)

    the mana cost formula, or '0' if not defined



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

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

#max_duration(options = {}) ⇒ Float

Returns the maximum allowed duration for this spell in minutes.

Duration maxima depend on the spell's XML metadata and can differ for self-cast vs. target-cast variants. Defaults to 250 minutes per cast type if not specified in XML. When a :caster option is provided, uses the caster's spell data. When a :target is specified that differs from the caster, uses target-cast metadata; otherwise uses self-cast.

Parameters:

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

    optional parameters for determining cast type

Options Hash (options):

  • :caster (String)

    the caster's name

  • :target (String)

    the target's name

Returns:

  • (Float)

    the maximum duration in minutes

See Also:



796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
# File 'documented/common/spell.rb', line 796

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

#mentalCSString

Deprecated.

Use #mental_cs_formula instead

Returns the spell's mental cast strength formula (backward compatibility alias).

Returns:

  • (String)

    the formula, or nil if not defined



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

def mentalCS;      self.mental_cs_formula;           end

#mentalTDString

Deprecated.

Use #mental_td_formula instead

Returns the spell's mental trivial difficulty formula (backward compatibility alias).

Returns:

  • (String)

    the formula, or nil if not defined



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

def mentalTD;      self.mental_td_formula;           end

#minsleftFloat

Returns the remaining duration of this spell in minutes.

Alias for #timeleft.

Returns:

  • (Float)

    remaining duration in minutes



505
506
507
# File 'documented/common/spell.rb', line 505

def minsleft
  self.timeleft
end

#multicastable?(options = {}) ⇒ Boolean

Tests whether this spell can be multicast (cast multiple times in rapid succession).

Duration multicast behavior depends on the spell's duration XML metadata (multicastable="yes") and can differ for self-cast vs. target-cast variants. When a :caster option is provided, uses the caster's spell data if it differs from the player. When a :target is specified that differs from the caster, uses target-cast metadata; otherwise uses self-cast.

Parameters:

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

    optional parameters for determining cast type

Options Hash (options):

  • :caster (String)

    the caster's name

  • :target (String)

    the target's name

Returns:

  • (Boolean)

    true if the spell can be multicast

See Also:



626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
# File 'documented/common/spell.rb', line 626

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

#physicalASString

Deprecated.

Use #physical_as_formula instead

Returns the spell's physical attack strength formula (backward compatibility alias).

Returns:

  • (String)

    the formula, or nil if not defined



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

def physicalAS;    self.physical_as_formula;         end

#physicalDSString

Deprecated.

Use #physical_ds_formula instead

Returns the spell's physical defense strength formula (backward compatibility alias).

Returns:

  • (String)

    the formula, or nil if not defined



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

def physicalDS;    self.physical_ds_formula;         end

#putdownvoid

This method returns an undefined value.

Marks this spell as inactive and clears its remaining duration.

Sets timeleft to 0 and active flag to false.

See Also:



840
841
842
843
# File 'documented/common/spell.rb', line 840

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

#putup(options = {}) ⇒ void

This method returns an undefined value.

Marks this spell as active and sets its duration.

Calculates the spell's duration using #time_per and applies stacking rules: if #stackable? is true, adds the new duration to the current remaining duration; otherwise replaces the duration. Clamps the result to #max_duration. Sets the active flag to true.

Parameters:

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

    optional parameters for duration calculation (see #time_per)

Options Hash (options):

  • :caster (String)

    the caster's name

  • :target (String)

    the target's name

See Also:



825
826
827
828
829
830
831
832
# File 'documented/common/spell.rb', line 825

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

Tests whether recasting this spell before expiration refreshes its duration.

Duration refresh behavior depends on the spell's duration XML metadata (span="refreshable") and can differ for self-cast vs. target-cast variants. When a :caster option is provided, uses the caster's spell data if it differs from the player. When a :target is specified that differs from the caster, uses target-cast metadata; otherwise uses self-cast.

Parameters:

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

    optional parameters for determining cast type

Options Hash (options):

  • :caster (String)

    the caster's name

  • :target (String)

    the target's name

Returns:

  • (Boolean)

    true if the spell refreshes (restarts duration) on recast

See Also:



589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
# File 'documented/common/spell.rb', line 589

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

#remainingString

Returns a human-readable string of the remaining spell duration.

Returns:

  • (String)

    the formatted duration (e.g., "2 hours 30 minutes")

See Also:



850
851
852
# File 'documented/common/spell.rb', line 850

def remaining
  self.timeleft.as_time
end

#secsleftFloat

Returns the remaining duration of this spell in seconds.

Multiplies #timeleft by 60.

Returns:

  • (Float)

    remaining duration in seconds



514
515
516
# File 'documented/common/spell.rb', line 514

def secsleft
  self.timeleft * 60
end

#selfonlyBoolean

Deprecated.

Use #available? instead

Tests whether this spell is self-cast only (backward compatibility alias).

Returns:

  • (Boolean)

    true if spell availability is not 'all'



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

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

#sorcererCSString

Deprecated.

Use #sorcerer_cs_formula instead

Returns the spell's sorcerer cast strength formula (backward compatibility alias).

Returns:

  • (String)

    the formula, or nil if not defined



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

def sorcererCS;    self.sorcerer_cs_formula;         end

#sorcererTDString

Deprecated.

Use #sorcerer_td_formula instead

Returns the spell's sorcerer trivial difficulty formula (backward compatibility alias).

Returns:

  • (String)

    the formula, or nil if not defined



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

def sorcererTD;    self.sorcerer_td_formula;         end

#spiritCostString

Deprecated.

Use #spirit_cost_formula instead

Returns the spirit cost formula for this spell (backward compatibility alias).

Returns:

  • (String)

    the spirit cost formula, or '0' if not defined



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

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

#spiritCSString

Deprecated.

Use #spirit_cs_formula instead

Returns the spell's spirit cast strength formula (backward compatibility alias).

Returns:

  • (String)

    the formula, or nil if not defined



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

def spiritCS;      self.spirit_cs_formula;           end

#spiritTDString

Deprecated.

Use #spirit_td_formula instead

Returns the spell's spirit trivial difficulty formula (backward compatibility alias).

Returns:

  • (String)

    the formula, or nil if not defined



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

def spiritTD;      self.spirit_td_formula;           end

#stackable?(options = {}) ⇒ Boolean

Tests whether this spell's duration stacks when recast before expiration.

Duration stacking behavior depends on the spell's duration XML metadata (span="stackable") and can differ for self-cast vs. target-cast variants. When a :caster option is provided, uses the caster's spell data if it differs from the player. When a :target is specified that differs from the caster, uses target-cast metadata; otherwise uses self-cast.

Parameters:

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

    optional parameters for determining cast type

Options Hash (options):

  • :caster (String)

    the caster's name

  • :target (String)

    the target's name

Returns:

  • (Boolean)

    true if the spell stacks with itself on recast

See Also:



552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
# File 'documented/common/spell.rb', line 552

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

#stacksBoolean

Deprecated.

Use #stackable? instead

Tests whether this spell stacks when recast (backward compatibility alias).

Returns:

  • (Boolean)

    true if the spell stacks



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

def stacks;        self.stackable?                   end

#staminaCostString

Deprecated.

Use #stamina_cost_formula instead

Returns the stamina cost formula for this spell (backward compatibility alias).

Returns:

  • (String)

    the stamina cost formula, or '0' if not defined



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

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

#time_per(options = {}) ⇒ Float

Evaluates the duration formula and returns the calculated spell duration in minutes.

Computes the duration by evaluating #time_per_formula with the game's current skill values and bonuses. For spells with spell knowledge (SK) integration, enforces a 10-minute minimum duration.

Examples:

spell = Spell[505]
spell.time_per  #=> 15.5

Parameters:

Options Hash (options):

  • :caster (String)

    the caster's name

  • :target (String)

    the target's name

  • :activator (String)

    the activation method

  • :line (String)

    reserved for internal use

Returns:

  • (Float)

    the duration in minutes, with a minimum of 10.0 for known spells

See Also:



450
451
452
453
454
455
456
457
458
459
# File 'documented/common/spell.rb', line 450

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

Returns the duration formula for this spell, with skill references substituted.

Retrieves the appropriate duration formula (self-cast or target-cast) and rewrites skill references (e.g., Spells.minorelemental, Skills.magicitemuse) to concrete expressions. The substitution depends on the :caster, :target, and :activator options.

Activators (tap, rub, wave, raise, drink, etc.) get scaled multipliers applied to skill values. Invocation methods (invoke, scroll) use arcanesymbols instead. Caster and target names are case-insensitive; a caster other than self receives lookups via SpellRanks instead of global Skills.

Examples:

spell = Spell[505]
spell.time_per_formula                #=> "(Spells.minorspiritual * 2) + 20"

Parameters:

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

    optional parameters for skill substitution

Options Hash (options):

  • :caster (String)

    the name of the caster, defaults to player character

  • :target (String)

    the name of the spell target

  • :activator (String)

    the activation method (tap, rub, wave, raise, invoke, scroll, etc.)

  • :line (String)

    reserved for internal use

Returns:

  • (String)

    the duration formula with skill references replaced

See Also:



382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# File 'documented/common/spell.rb', line 382

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

Returns the remaining duration of this spell in minutes.

Subtracts elapsed time since the last #timeleft= or #timeleft call from the tracked duration. If the spell's duration formula is a Spellsong reference, queries Spellsong.timeleft directly instead. When remaining time drops to 0 or below, calls #putdown and returns 0.0.

Updates the timestamp on each call.

Returns:

  • (Float)

    remaining duration in minutes, or 0.0 if expired

See Also:



486
487
488
489
490
491
492
493
494
495
496
497
498
# File 'documented/common/spell.rb', line 486

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

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Sets the remaining duration for this spell and updates the timestamp.

Used internally to track spell duration across duration checks. Setting a new value resets the base timestamp to now.

Parameters:

  • val (Numeric)

    the remaining duration in minutes

Returns:



469
470
471
472
# File 'documented/common/spell.rb', line 469

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

#to_sString

Returns the spell's name as a string.

Returns:



780
781
782
# File 'documented/common/spell.rb', line 780

def to_s
  @name.to_s
end