-
-
Notifications
You must be signed in to change notification settings - Fork 452
/
spell.dm
470 lines (401 loc) · 18.4 KB
/
spell.dm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
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
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
/**
* # The spell action
*
* This is the base action for how many of the game's
* spells (and spell adjacent) abilities function.
* These spells function off of a cooldown-based system.
*
* ## Pre-spell checks:
* - [can_cast_spell][/datum/action/cooldown/spell/can_cast_spell] checks if the OWNER
* of the spell is able to cast the spell.
* - [is_valid_target][/datum/action/cooldown/spell/is_valid_target] checks if the TARGET
* THE SPELL IS BEING CAST ON is a valid target for the spell. NOTE: The CAST TARGET is often THE SAME as THE OWNER OF THE SPELL,
* but is not always - depending on how [Pre Activate][/datum/action/cooldown/spell/PreActivate] is resolved.
* - [try_invoke][/datum/action/cooldown/spell/try_invoke] is run in can_cast_spell to check if
* the OWNER of the spell is able to say the current invocation.
*
* ## The spell chain:
* - [before_cast][/datum/action/cooldown/spell/before_cast] is the last chance for being able
* to interrupt a spell cast. This returns a bitflag. if SPELL_CANCEL_CAST is set, the spell will not continue.
* - [spell_feedback][/datum/action/cooldown/spell/spell_feedback] is called right before cast, and handles
* invocation and sound effects. Overridable, if you want a special method of invocation or sound effects,
* or you want your spell to handle invocation / sound via special means.
* - [cast][/datum/action/cooldown/spell/cast] is where the brunt of the spell effects should be done
* and implemented.
* - [after_cast][/datum/action/cooldown/spell/after_cast] is the aftermath - final effects that follow
* the main cast of the spell. By now, the spell cooldown has already started
*
* ## Other procs called / may be called within the chain:
* - [invocation][/datum/action/cooldown/spell/invocation] handles saying any vocal (or emotive) invocations the spell
* may have, and can be overriden or extended. Called by spell_feedback.
* - [reset_spell_cooldown][/datum/action/cooldown/spell/reset_spell_cooldown] is a way to handle reverting a spell's
* cooldown and making it ready again if it fails to go off at any point. Not called anywhere by default. If you
* want to cancel a spell in before_cast and would like the cooldown restart, call this.
*
* ## Other procs of note:
* - [level_spell][/datum/action/cooldown/spell/level_spell] is where the process of adding a spell level is handled.
* this can be extended if you wish to add unique effects on level up for wizards.
* - [delevel_spell][/datum/action/cooldown/spell/delevel_spell] is where the process of removing a spell level is handled.
* this can be extended if you wish to undo unique effects on level up for wizards.
* - [get_spell_title][/datum/action/cooldown/spell/get_spell_title] returns the prefix of the spell name based on its level,
* for use in updating the button name / spell name.
*/
/datum/action/cooldown/spell
name = "Spell"
desc = "A wizard spell."
background_icon_state = "bg_spell"
button_icon = 'icons/mob/actions/actions_spells.dmi'
button_icon_state = "spell_default"
overlay_icon_state = "bg_spell_border"
active_overlay_icon_state = "bg_spell_border_active_red"
check_flags = AB_CHECK_CONSCIOUS
panel = "Spells"
/// The sound played on cast.
var/sound = null
/// The school of magic the spell belongs to.
/// Checked by some holy sects to punish the
/// caster for casting things that do not align
/// with their sect's alignment - see magic.dm in defines to learn more
var/school = SCHOOL_UNSET
/// If the spell uses the wizard spell rank system, the cooldown reduction per rank of the spell
var/cooldown_reduction_per_rank = 0 SECONDS
/// What is uttered when the user casts the spell
var/invocation
/// What is shown in chat when the user casts the spell, only matters for INVOCATION_EMOTE
var/invocation_self_message
/// What type of invocation the spell is.
/// Can be "none", "whisper", "shout", "emote"
var/invocation_type = INVOCATION_NONE
/// Flag for certain states that the spell requires the user be in to cast.
var/spell_requirements = SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_NO_ANTIMAGIC
/// This determines what type of antimagic is needed to block the spell.
/// (MAGIC_RESISTANCE, MAGIC_RESISTANCE_MIND, MAGIC_RESISTANCE_HOLY)
/// If SPELL_REQUIRES_NO_ANTIMAGIC is set in Spell requirements,
/// The spell cannot be cast if the caster has any of the antimagic flags set.
var/antimagic_flags = MAGIC_RESISTANCE
/// The current spell level, if taken multiple times by a wizard
var/spell_level = 1
/// The max possible spell level
var/spell_max_level = 5
/// If set to a positive number, the spell will produce sparks when casted.
var/sparks_amt = 0
/// The typepath of the smoke to create on cast.
var/smoke_type
/// The amount of smoke to create on cast. This is a range, so a value of 5 will create enough smoke to cover everything within 5 steps.
var/smoke_amt = 0
/// An associative list of all resource costs
var/list/resource_costs
/// Boolean, if true, resource costs will be ignored
var/bypass_cost = FALSE
/datum/action/cooldown/spell/Grant(mob/grant_to)
// If our spell is mind-bound, we only wanna grant it to our mind
if(istype(target, /datum/mind))
var/datum/mind/mind_target = target
if(mind_target.current != grant_to)
return
. = ..()
if(!owner)
return
// Register some signals so our button's icon stays up to date
if(spell_requirements & SPELL_REQUIRES_STATION)
RegisterSignal(owner, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(update_status_on_signal))
if(spell_requirements & (SPELL_REQUIRES_NO_ANTIMAGIC|SPELL_REQUIRES_WIZARD_GARB))
RegisterSignal(owner, COMSIG_MOB_EQUIPPED_ITEM, PROC_REF(update_status_on_signal))
RegisterSignals(owner, list(COMSIG_MOB_ENTER_JAUNT, COMSIG_MOB_AFTER_EXIT_JAUNT), PROC_REF(update_status_on_signal))
if(owner.client)
owner.client << output(null, "statbrowser:check_spells")
/datum/action/cooldown/spell/Remove(mob/living/remove_from)
if(remove_from.client)
remove_from.client << output(null, "statbrowser:check_spells")
UnregisterSignal(remove_from, list(
COMSIG_MOB_AFTER_EXIT_JAUNT,
COMSIG_MOB_ENTER_JAUNT,
COMSIG_MOB_EQUIPPED_ITEM,
COMSIG_MOVABLE_Z_CHANGED,
))
return ..()
/datum/action/cooldown/spell/IsAvailable(feedback = FALSE)
return ..() && can_cast_spell(FALSE)
/datum/action/cooldown/spell/Trigger(trigger_flags, atom/target)
// We implement this can_cast_spell check before the parent call of Trigger()
// to allow people to click unavailable abilities to get a feedback chat message
// about why the ability is unavailable.
// It is otherwise redundant, however, as IsAvailable(feedback = FALSE) checks can_cast_spell as well.
if(!can_cast_spell())
return FALSE
return ..()
/datum/action/cooldown/spell/set_click_ability(mob/on_who)
if(SEND_SIGNAL(on_who, COMSIG_MOB_SPELL_ACTIVATED, src) & SPELL_CANCEL_CAST)
return FALSE
return ..()
// Where the cast chain starts
/datum/action/cooldown/spell/PreActivate(atom/target)
if(SEND_SIGNAL(owner, COMSIG_MOB_ABILITY_STARTED, src) & COMPONENT_BLOCK_ABILITY_START)
return FALSE
if(!is_valid_target(target))
return FALSE
return Activate(target)
/// Checks if the owner of the spell can currently cast it.
/// Does not check anything involving potential targets.
/datum/action/cooldown/spell/proc/can_cast_spell(feedback = TRUE)
if(!owner)
CRASH("[type] - can_cast_spell called on a spell without an owner!")
// Certain spells are not allowed on the centcom zlevel
var/turf/caster_turf = get_turf(owner)
// Spells which require being on the station
if((spell_requirements & SPELL_REQUIRES_STATION) && !is_station_level(caster_turf.z))
if(feedback)
to_chat(owner, span_warning("You can't cast [src] here!"))
return FALSE
if((spell_requirements & SPELL_REQUIRES_MIND) && !owner.mind)
// No point in feedback here, as mindless mobs aren't players
return FALSE
if((spell_requirements & SPELL_REQUIRES_MIME_VOW) && !owner.mind?.miming)
// In the future this can be moved out of spell checks exactly
if(feedback)
to_chat(owner, span_warning("You must dedicate yourself to silence first!"))
return FALSE
//used for darkspawn spells
if(owner.mind && !bypass_cost && LAZYLEN(resource_costs))
for(var/i in resource_costs)
var/has_cost = SEND_SIGNAL(owner.mind, COMSIG_MIND_CHECK_ANTAG_RESOURCE, i, resource_costs[i])
if(!has_cost)
if(feedback)
to_chat(owner, span_warning("You don't have enough [i]!"))
return FALSE
// If the spell requires the user has no antimagic equipped, and they're holding antimagic
// that corresponds with the spell's antimagic, then they can't actually cast the spell
if((spell_requirements & SPELL_REQUIRES_NO_ANTIMAGIC) && !owner.can_cast_magic(antimagic_flags))
if(feedback)
to_chat(owner, span_warning("Some form of antimagic is preventing you from casting [src]!"))
return FALSE
if(!(spell_requirements & SPELL_CASTABLE_WHILE_PHASED) && HAS_TRAIT(owner, TRAIT_MAGICALLY_PHASED))
if(feedback)
to_chat(owner, span_warning("[src] cannot be cast unless you are completely manifested in the material plane!"))
return FALSE
if(!try_invoke(feedback = feedback))
return FALSE
var/list/casting_clothes = typecacheof(list( //HELLO. CHANGE THIS LATER....
/obj/item/clothing/suit/wizrobe,
/obj/item/clothing/suit/space/hardsuit/wizard,
/obj/item/clothing/head/wizard,
/obj/item/clothing/head/wizard/armor,
/obj/item/clothing/suit/wizrobe/armor,
/obj/item/clothing/head/helmet/space/hardsuit/wizard,
/obj/item/clothing/head/helmet/space/hardsuit/shielded/wizard
))
if(ishuman(owner))
if(spell_requirements & SPELL_REQUIRES_WIZARD_GARB)
var/mob/living/carbon/human/human_owner = owner
if(!is_type_in_typecache(human_owner.wear_suit, casting_clothes))
if(feedback)
to_chat(owner, span_warning("You don't feel strong enough without your robe!"))
return FALSE
if(!is_type_in_typecache(human_owner.head, casting_clothes))
if(feedback)
to_chat(owner, span_warning("You don't feel strong enough without your hat!"))
return FALSE
else
// If the spell requires wizard equipment and we're not a human (can't wear robes or hats), that's just a given
if(spell_requirements & (SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_HUMAN))
if(feedback)
to_chat(owner, span_warning("[src] can only be cast by humans!"))
return FALSE
if(!(spell_requirements & SPELL_CASTABLE_AS_BRAIN) && isbrain(owner))
if(feedback)
to_chat(owner, span_warning("[src] can't be cast in this state!"))
return FALSE
// Being put into a card form breaks a lot of spells, so we'll just forbid them in these states
if(ispAI(owner) || (isAI(owner) && istype(owner.loc, /obj/item/aicard)))
return FALSE
return TRUE
/**
* Check if the target we're casting on is a valid target.
* For self-casted spells, the target being checked (cast_on) is the caster.
* For click_to_activate spells, the target being checked is the clicked atom.
*
* Return TRUE if cast_on is valid, FALSE otherwise
*/
/datum/action/cooldown/spell/proc/is_valid_target(atom/cast_on)
return TRUE
// The actual cast chain occurs here, in Activate().
// You should generally not be overriding or extending Activate() for spells.
// Defer to any of the cast chain procs instead.
/datum/action/cooldown/spell/Activate(atom/cast_on)
SHOULD_NOT_OVERRIDE(TRUE)
// Pre-casting of the spell
// Pre-cast is the very last chance for a spell to cancel
// Stuff like target input can go here.
var/precast_result = before_cast(cast_on)
if(precast_result & SPELL_CANCEL_CAST)
return FALSE
// Spell is officially being cast
if(!(precast_result & SPELL_NO_FEEDBACK))
// We do invocation and sound effects here, before actual cast
// That way stuff like teleports or shape-shifts can be invoked before ocurring
spell_feedback()
// Actually cast the spell. Main effects go here
cast(cast_on)
if(!(precast_result & SPELL_NO_IMMEDIATE_COOLDOWN))
// The entire spell is done, start the actual cooldown at its set duration
StartCooldown()
consume_resource() //a resource cost is basically the same as a cooldown
// And then proceed with the aftermath of the cast
// Final effects that happen after all the casting is done can go here
after_cast(cast_on)
build_all_button_icons()
return TRUE
/**
* Actions done before the actual cast is called.
* This is the last chance to cancel the spell from being cast.
*
* Can be used for target selection or to validate checks on the caster (cast_on).
*
* Returns a bitflag.
* - SPELL_CANCEL_CAST will stop the spell from being cast.
* - SPELL_NO_FEEDBACK will prevent the spell from calling [proc/spell_feedback] on cast. (invocation), sounds)
* - SPELL_NO_IMMEDIATE_COOLDOWN will prevent the spell from starting its cooldown between cast and before after_cast.
*/
/datum/action/cooldown/spell/proc/before_cast(atom/cast_on)
SHOULD_CALL_PARENT(TRUE)
var/sig_return = SEND_SIGNAL(src, COMSIG_SPELL_BEFORE_CAST, cast_on)
if(owner)
sig_return |= SEND_SIGNAL(owner, COMSIG_MOB_BEFORE_SPELL_CAST, src, cast_on)
return sig_return
/**
* Actions done as the main effect of the spell.
*
* For spells without a click intercept, [cast_on] will be the owner.
* For click spells, [cast_on] is whatever the owner clicked on in casting the spell.
*/
/datum/action/cooldown/spell/proc/cast(atom/cast_on)
SHOULD_CALL_PARENT(TRUE)
SEND_SIGNAL(src, COMSIG_SPELL_CAST, cast_on)
if(owner)
SEND_SIGNAL(owner, COMSIG_MOB_CAST_SPELL, src, cast_on)
if(owner.ckey)
owner.log_message("cast the spell [name][cast_on != owner ? " on / at [cast_on]":""].", LOG_ATTACK)
/**
* Actions done after the main cast is finished.
* This is called after the cooldown's already begun.
*
* It can be used to apply late spell effects where order matters
* (for example, causing smoke *after* a teleport occurs in cast())
* or to clean up variables or references post-cast.
*/
/datum/action/cooldown/spell/proc/after_cast(atom/cast_on)
SHOULD_CALL_PARENT(TRUE)
if(!owner) // Could have been destroyed by the effect of the spell
SEND_SIGNAL(src, COMSIG_SPELL_AFTER_CAST, cast_on)
return
if(sparks_amt)
do_sparks(sparks_amt, FALSE, get_turf(owner))
if(ispath(smoke_type, /datum/effect_system/fluid_spread/smoke))
var/datum/effect_system/fluid_spread/smoke/smoke = new smoke_type()
smoke.set_up(smoke_amt, holder = owner, location = get_turf(owner))
smoke.start()
// Send signals last in case they delete the spell
SEND_SIGNAL(owner, COMSIG_MOB_AFTER_SPELL_CAST, src, cast_on)
SEND_SIGNAL(src, COMSIG_SPELL_AFTER_CAST, cast_on)
/// Called after the effect happens, whether that's after the button press or after hitting someone with a touch ability
/datum/action/cooldown/spell/proc/consume_resource() //to-do: rework vampire blood use into using this proc
if(!bypass_cost && owner.mind && LAZYLEN(resource_costs))
SEND_SIGNAL(owner.mind, COMSIG_MIND_SPEND_ANTAG_RESOURCE, resource_costs)
/// Provides feedback after a spell cast occurs, in the form of a cast sound and/or invocation
/datum/action/cooldown/spell/proc/spell_feedback()
if(!owner)
return
if(invocation_type != INVOCATION_NONE)
invocation()
if(sound)
playsound(get_turf(owner), sound, 50, TRUE)
/// The invocation that accompanies the spell, called from spell_feedback() before cast().
/datum/action/cooldown/spell/proc/invocation()
switch(invocation_type)
if(INVOCATION_SHOUT)
if(prob(50))
owner.say(invocation, ignore_spam = TRUE, forced = "spell ([src])")
else
owner.say(replacetext(invocation," ","`"), ignore_spam = TRUE, forced = "spell ([src])")
if(INVOCATION_WHISPER)
if(prob(50))
owner.whisper(invocation, ignore_spam = TRUE, forced = "spell ([src])")
else
owner.whisper(replacetext(invocation," ","`"), ignore_spam = TRUE, forced = "spell ([src])")
if(INVOCATION_EMOTE)
owner.visible_message(invocation, invocation_self_message)
/// Checks if the current OWNER of the spell is in a valid state to say the spell's invocation
/datum/action/cooldown/spell/proc/try_invoke(feedback = TRUE)
if(spell_requirements & SPELL_CASTABLE_WITHOUT_INVOCATION)
return TRUE
if(invocation_type == INVOCATION_NONE)
return TRUE
// If you want a spell usable by ghosts for some reason, it must be INVOCATION_NONE
if(!isliving(owner))
if(feedback)
to_chat(owner, span_warning("You need to be living to invoke [src]!"))
return FALSE
var/mob/living/living_owner = owner
if(invocation_type == INVOCATION_EMOTE && HAS_TRAIT(living_owner, TRAIT_EMOTEMUTE))
if(feedback)
to_chat(owner, span_warning("You can't position your hands correctly to invoke [src]!"))
return FALSE
if((invocation_type == INVOCATION_WHISPER || invocation_type == INVOCATION_SHOUT) && !living_owner.can_speak())
if(feedback)
to_chat(owner, span_warning("You can't get the words out to invoke [src]!"))
return FALSE
return TRUE
/// Resets the cooldown of the spell, sending COMSIG_SPELL_CAST_RESET
/// and allowing it to be used immediately (+ updating button icon accordingly)
/datum/action/cooldown/spell/proc/reset_spell_cooldown()
SEND_SIGNAL(src, COMSIG_SPELL_CAST_RESET)
next_use_time -= cooldown_time // Basically, ensures that the ability can be used now
build_all_button_icons()
/**
* Levels the spell up a single level, reducing the cooldown.
* If bypass_cap is TRUE, will level the spell up past it's set cap.
*/
/datum/action/cooldown/spell/proc/level_spell(bypass_cap = FALSE)
// Spell cannot be levelled
if(spell_max_level <= 1)
return FALSE
// Spell is at cap, and we will not bypass it
if(!bypass_cap && (spell_level >= spell_max_level))
return FALSE
spell_level++
cooldown_time = max(cooldown_time - cooldown_reduction_per_rank, 0.25 SECONDS) // 0 second CD starts to break things.
build_all_button_icons(UPDATE_BUTTON_NAME)
return TRUE
/**
* Levels the spell down a single level, down to 1.
*/
/datum/action/cooldown/spell/proc/delevel_spell()
// Spell cannot be levelled
if(spell_max_level <= 1)
return FALSE
if(spell_level <= 1)
return FALSE
spell_level--
if(cooldown_reduction_per_rank > 0 SECONDS)
cooldown_time = min(cooldown_time + cooldown_reduction_per_rank, initial(cooldown_time))
else
cooldown_time = max(cooldown_time + cooldown_reduction_per_rank, initial(cooldown_time))
build_all_button_icons(UPDATE_BUTTON_NAME)
return TRUE
/datum/action/cooldown/spell/update_button_name(atom/movable/screen/movable/action_button/button, force)
name = "[get_spell_title()][initial(name)]"
return ..()
/// Gets the title of the spell based on its level.
/datum/action/cooldown/spell/proc/get_spell_title()
switch(spell_level)
if(2)
return "Efficient "
if(3)
return "Quickened "
if(4)
return "Free "
if(5)
return "Instant "
if(6)
return "Ludicrous "
return ""