Files
Yogstation/code/modules/spells/spell.dm
2023-07-01 21:08:14 -05:00

451 lines
18 KiB
Plaintext

/**
* # 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
/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
// 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()
// 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)
/// 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 ""