diff --git a/_maps/map_files/vanderlin/vanderlin_mountain.dmm b/_maps/map_files/vanderlin/vanderlin_mountain.dmm index 6492bbfbd00..c1fb630dbc4 100644 --- a/_maps/map_files/vanderlin/vanderlin_mountain.dmm +++ b/_maps/map_files/vanderlin/vanderlin_mountain.dmm @@ -3368,7 +3368,7 @@ /turf/open/floor/cobble, /area/indoors/mountains/anvil/upperkeep) "aqq" = ( -/obj/item/book/granter/spellbook/horrible, +/obj/item/spellbook/horrible, /turf/open/floor/herringbone, /area/indoors/mountains/anvil/upperkeep) "aqr" = ( @@ -6008,7 +6008,7 @@ /obj/structure/fluff/walldeco/painting/castle{ pixel_y = 32 }, -/obj/item/book/granter/spellbook/adept, +/obj/item/spellbook/adept, /turf/open/floor/herringbone, /area/indoors/mountains/anvil/upperkeep) "aCs" = ( @@ -13652,7 +13652,7 @@ /area/indoors/mountains/anvil/upperkeep) "rjA" = ( /obj/effect/decal/cleanable/ritual_rune/arcyne/summoning/adv, -/obj/effect/decal/cleanable/ritual_rune/arcyne/attunement, +/obj/effect/decal/cleanable/ritual_rune/arcyne/recharge, /obj/item/riddleofsteel, /mob/living/simple_animal/hostile/retaliate/voiddragon/red, /turf/open/floor/plasteel/maniac, diff --git a/code/__DEFINES/ai/_ai.dm b/code/__DEFINES/ai/_ai.dm index 323c8197ba8..035f80d280a 100644 --- a/code/__DEFINES/ai/_ai.dm +++ b/code/__DEFINES/ai/_ai.dm @@ -179,6 +179,9 @@ #define BB_ENERGY_REGEN_RATE "energy_regen" #define BB_BASIC_MOB_STOP_FLEEING "bb_stop_fleeing" +///Blackboard key for a whitelist typecache of "things we can target while trying to move" +#define BB_OBSTACLE_TARGETING_WHITELIST "BB_targeting_whitelist" + #define BB_DRAGGER_HUNTING_COOLDOWN "dragger_hunting_cooldown" #define BB_DRAGGER_TELEPORT_COOLDOWN "dragger_teleport_cooldown" #define BB_DRAGGER_DRAG_COOLDOWN "dragger_drag_cooldown" @@ -207,6 +210,8 @@ #define BB_CAT_GROOM_CHANCE "cat_groom" #define BB_CAT_RACISM "cat_racist" +#define BB_FORCED_ATTACK_ZONE "bb_forced_attack_zone" + /// key that holds the target we will battle over our turf #define BB_TRESSPASSER_TARGET "tresspasser_target" /// key that holds angry meows diff --git a/code/__DEFINES/attunement.dm b/code/__DEFINES/attunement.dm index 2af539a416a..10ac4d7c37d 100644 --- a/code/__DEFINES/attunement.dm +++ b/code/__DEFINES/attunement.dm @@ -7,71 +7,8 @@ to_convert = (-to_convert+1); \ } \ } \ -// Elements - -#define MAGIC_ELEMENT_FIRE /datum/attunement/fire -#define MAGIC_ELEMENT_ICE /datum/attunement/ice -#define MAGIC_ELEMENT_BLOOD /datum/attunement/blood - -#define MAGIC_ELEMENT_EARTH /datum/attunement/earth -#define MAGIC_ELEMENT_WIND /datum/attunement/aeromancy - -#define MAGIC_ELEMENT_LIFE /datum/attunement/life -#define MAGIC_ELEMENT_DEATH /datum/attunement/death -#define MAGIC_ELEMENT_LIGHT /datum/attunement/light - -#define MAGIC_ELEMENT_ELECTRIC /datum/attunement/electric // When other elements are used, add them here // Alignments #define MAGIC_ALIGNMENT_NORMAL "Follower of Noc" #define MAGIC_ALIGNMENT_NECROMANCER "Child of Zizo" - -/// Returns the raw attunement correspondance between args 1 and 3. Its fine if both are negative, since thats a positive correspondance. -/// The direction and intensity of the correspondance should correspond to altered casting cost - intense positive correspondance should lower -/// casting cost by a large amount-but a weak negative correspondance should increase casting cost by a small amount. -#define GET_RAW_ATTUNEMENT_CORRESPONDANCE(attunements, intensity, attunement) (attunements[attunement] * intensity) - -/// Compares the two lists of attunements and combines the raw correspondance. -/proc/get_total_correspondance(list/attunements, list/other_attunements) - . = 0 - - for (var/datum/attunement/iterated_attunement as anything in attunements) - var/intensity = (other_attunements?[iterated_attunement]) || 1 - . += GET_RAW_ATTUNEMENT_CORRESPONDANCE(attunements, intensity, iterated_attunement) - -/// Returns the "mult" value using the correspondance between attunements and intensity. This value should be multiplied against -/// the mana cost of an action to determine how much "effective" mana a certain mana source can provide to it. -/// Should NEVER return anything negative, ever. The minimum is 0. -/proc/get_total_attunement_mult(list/mana_attunements, list/user_attunements, atom/caster) - . = 1 - var/total_correspondance = get_total_correspondance(mana_attunements, user_attunements) - - if (total_correspondance != 0) - . = convert_to_attunement_mult(total_correspondance) - - var/bias_amount = get_total_attunement_bias(caster, mana_attunements) - if (bias_amount != 0) - . *= convert_to_attunement_mult(bias_amount) - -/// attunements should only ever be attunements of a mana source, not mana user. -/proc/get_total_attunement_bias(atom/caster, list/attunements) - . = 0 - var/total_attunement = 0 - for (var/datum/attunement/iterated_attunement as anything in attunements) - total_attunement += abs(attunements[iterated_attunement]) - if (total_attunement == 0) - return - for (var/datum/attunement/iterated_attunement as anything in attunements) - if (attunements[iterated_attunement] == 0) - continue - var/datum/attunement/singleton_instance = GLOB.magic_attunements[iterated_attunement] - . += singleton_instance.get_bias_mult_increment(caster) * ((attunements[iterated_attunement])/total_attunement) //access singleton - -/proc/convert_to_attunement_mult(amount) - if (amount != 0) - if (amount > 0) - amount = (1/(1+amount)) - else - amount = (-amount+1) - return amount diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm index 588f02c454d..aa11feeed7b 100644 --- a/code/__DEFINES/colors.dm +++ b/code/__DEFINES/colors.dm @@ -117,6 +117,7 @@ #define CLOTHING_WET "#afafaf" #define GLOW_COLOR_BARDIC "#E8837C" // Warm rose, for bardic songs and cadences +#define GLOW_COLOR_ICE "#87CEEB" // Cyan #define CM_COLOR_SAT_MIN 0.6 #define CM_COLOR_SAT_MAX 0.7 diff --git a/code/__DEFINES/dcs/signals/signals_atoms/signals_atom_main.dm b/code/__DEFINES/dcs/signals/signals_atoms/signals_atom_main.dm index 70bde28ed96..1a601b14132 100644 --- a/code/__DEFINES/dcs/signals/signals_atoms/signals_atom_main.dm +++ b/code/__DEFINES/dcs/signals/signals_atoms/signals_atom_main.dm @@ -73,7 +73,7 @@ #define COMPONENT_CANCEL_ATTACK_CHAIN (1<<0) ///Skips the specific attack step, continuing for the next one to happen. #define COMPONENT_SKIP_ATTACK (1<<1) - + #define COMPONENT_ITEM_NO_DEFENSE (1<<2) #define COMSIG_ATOM_GET_RESISTANCE "atom_get_resistance" #define COMSIG_ATOM_GET_MAX_RESISTANCE "atom_get_max_resistance" #define COMSIG_ATOM_GET_STATUS_MOD "atom_get_status_mod" diff --git a/code/__DEFINES/dcs/signals/signals_spell.dm b/code/__DEFINES/dcs/signals/signals_spell.dm index 208698356d7..a0035845a98 100644 --- a/code/__DEFINES/dcs/signals/signals_spell.dm +++ b/code/__DEFINES/dcs/signals/signals_spell.dm @@ -100,3 +100,12 @@ #define COMPONENT_ITEM_CHARGED (1 << 0) /// Return if the item had a negative side effect occur while recharging #define COMPONENT_ITEM_BURNT_OUT (1 << 1) + +#define COMSIG_SPELL_REQUEST_MODIFIERS "spell_request_modifiers" + +#define COMSIG_MASTERY_CAST "mastery_cast" + +#define COMSIG_MASTERY_ADD_SPELLS "add_mastery_spells" +#define COMSIG_MASTERY_REMOVE_SPELLS "remove_mastery_spells" +#define COMSIG_MASTERY_CHECK_PARENT "mastery_parent_status" + #define COMPONENT_MASTERY_CANCEL (1<<0) diff --git a/code/__DEFINES/magic.dm b/code/__DEFINES/magic.dm index 0cca77abb76..b1cc506081e 100644 --- a/code/__DEFINES/magic.dm +++ b/code/__DEFINES/magic.dm @@ -1,32 +1,3 @@ -// Magic schools - -/// Unset / default / "not actually magic" school. -#define SCHOOL_UNSET "unset" - -// GOOD SCHOOLS (allowed by honorbound gods, some of these you can get on station) -/// Holy school (chaplain magic) -#define SCHOOL_HOLY "holy" -/// Mime... school? Mime magic. It counts -#define SCHOOL_MIME "mime" -/// Restoration school, which is mostly healing stuff -#define SCHOOL_RESTORATION "restoration" - -// NEUTRAL SPELLS (punished by honorbound gods if you get caught using it) -/// Evocation school, usually involves killing or destroy stuff, usually out of thin air -#define SCHOOL_EVOCATION "evocation" -/// School of transforming stuff into other stuff -#define SCHOOL_TRANSMUTATION "transmutation" -/// School of transolcation, usually movement spells -#define SCHOOL_TRANSLOCATION "translocation" -/// Conjuration spells summon items / mobs / etc somehow -#define SCHOOL_CONJURATION "conjuration" - -// EVIL SPELLS (instant smite + banishment) -/// Necromancy spells, usually involves soul / evil / bad stuff -#define SCHOOL_NECROMANCY "necromancy" -/// Other forbidden magics, such as heretic spells -#define SCHOOL_FORBIDDEN "forbidden" - #define NO_MANA_POOL (1<<0) #define MANA_POOL_FULL (1<<1) @@ -188,6 +159,10 @@ DEFINE_BITFIELD(antimagic_flags, list( #define SPELL_CASTABLE_WITHOUT_INVOCATION (1 << 6) /// If the spell requires the user to not move during casting #define SPELL_REQUIRES_NO_MOVE (1 << 7) +/// Whether the spell requires the target to be on the same Z-level as the caster. +#define SPELL_REQUIRES_SAME_Z (1 << 8) +/// Whether the spell can be cast while buckled to a living mount (on horseback). +#define SPELL_CASTABLE_WHILE_MOUNTED (1 << 9) DEFINE_BITFIELD(spell_requirements, list( "SPELL_CASTABLE_WITHOUT_INVOCATION" = SPELL_CASTABLE_WITHOUT_INVOCATION, @@ -198,6 +173,7 @@ DEFINE_BITFIELD(spell_requirements, list( "SPELL_REQUIRES_NO_MOVE" = SPELL_REQUIRES_NO_MOVE, "SPELL_REQUIRES_STATION" = SPELL_REQUIRES_STATION, "SPELL_REQUIRES_WIZARD_GARB" = SPELL_REQUIRES_WIZARD_GARB, + "SPELL_REQUIRES_SAME_Z" = SPELL_REQUIRES_SAME_Z, )) /** @@ -225,10 +201,104 @@ DEFINE_BITFIELD(spell_requirements, list( /// Diceroll requirement at each arcane skill tier for aimed-fire /// Lower = easier to hit the intended target (roll-under system) -#define SPELLOBJECT_AIM_REQ_NONE 6 -#define SPELLOBJECT_AIM_REQ_NOVICE 9 +#define SPELLOBJECT_AIM_REQ_NONE 6 +#define SPELLOBJECT_AIM_REQ_NOVICE 9 #define SPELLOBJECT_AIM_REQ_APPRENTICE 11 #define SPELLOBJECT_AIM_REQ_JOURNEYMAN 13 -#define SPELLOBJECT_AIM_REQ_EXPERT 15 -#define SPELLOBJECT_AIM_REQ_MASTER 16 -#define SPELLOBJECT_AIM_REQ_LEGENDARY 17 +#define SPELLOBJECT_AIM_REQ_EXPERT 15 +#define SPELLOBJECT_AIM_REQ_MASTER 16 +#define SPELLOBJECT_AIM_REQ_LEGENDARY 17 + +#define TECHNIQUE_DESTRUCTION "Destruction" +#define TECHNIQUE_CREATION "Creation" +#define TECHNIQUE_SUMMONING "Summoning" +#define TECHNIQUE_RESTORATION "Restoration" +#define TECHNIQUE_ALTERATION "Alteration" +#define TECHNIQUE_ILLUSION "Illusion" +#define TECHNIQUE_IMBUE "Imbue" + +#define FORM_FIRE "Fire" +#define FORM_ICE "Ice" +#define FORM_LIGHTNING "Lightning" +#define FORM_EARTH "Earth" +#define FORM_ARCANE "Arcane" +#define FORM_DEATH "Death" +#define FORM_LIFE "Life" +#define FORM_AIR "Aeromancy" +#define FORM_WATER "Hydromancy" + +#define MASTERY_RANK_NOVICE 0 +#define MASTERY_RANK_ADEPT 2 +#define MASTERY_RANK_EXPERT 4 +#define MASTERY_RANK_MASTER 6 + +GLOBAL_LIST_INIT(mastery_rank_names, list( + "[MASTERY_RANK_NOVICE]" = "Novice", + "[MASTERY_RANK_ADEPT]" = "Adept", + "[MASTERY_RANK_EXPERT]" = "Expert", + "[MASTERY_RANK_MASTER]" = "Master", +)) + +GLOBAL_LIST_INIT(form_colors, list( + FORM_FIRE = "#FF4500", + FORM_ICE = "#00BFFF", + FORM_LIGHTNING = "#FFD700", + FORM_WATER = "#00158b", + FORM_LIFE = "#32CD32", + FORM_DEATH = "#800080", + FORM_EARTH = "#8B4513", + FORM_AIR = "#C0C0C0", + FORM_ARCANE = "#9932CC" +)) + +GLOBAL_LIST_INIT(all_techniques, list( + TECHNIQUE_DESTRUCTION, + TECHNIQUE_CREATION, + TECHNIQUE_SUMMONING, + TECHNIQUE_RESTORATION, + TECHNIQUE_ALTERATION, + TECHNIQUE_ILLUSION, + TECHNIQUE_IMBUE, +)) + +GLOBAL_LIST_INIT(all_forms, list( + FORM_FIRE, + FORM_ICE, + FORM_LIGHTNING, + FORM_EARTH, + FORM_ARCANE, + FORM_LIFE, + FORM_DEATH, + FORM_AIR, + FORM_WATER, +)) + +#define CHARGETIME_POKE 0.5 SECONDS // Staple poke spells +#define CHARGETIME_MINOR 1 SECONDS // Minor utility / support spells +#define CHARGETIME_MAJOR 1.5 SECONDS // Major projectiles +#define CHARGETIME_HEAVY 2 SECONDS // Heavy AOE / ultimates +#define CHARGETIME_BARRAGE 3 SECONDS // Barrage / Channeled spells + +#define CHARGING_SLOWDOWN_NONE 0 +#define CHARGING_SLOWDOWN_SMALL 1 +#define CHARGING_SLOWDOWN_MEDIUM 2 +#define CHARGING_SLOWDOWN_HEAVY 3 + +#define SPELL_RANGE_PROJECTILE 10 // Standard projectile travel distance and projectile spell cast range +#define SPELL_RANGE_GROUND 7 // Standard ground-targeted / AOE spell cast range +#define SPELL_RANGE_TWO_SCREENS 14 // Two screens away for very very special spells +#define SPELL_RANGE_AURA 4 // For 'warcry' type miracles or AOE BUFFS originating on the caster +#define SPELL_RANGE_ADJACENT 1 // Self explanatory + +#define SPELL_IMPACT_NONE 0 +#define SPELL_IMPACT_LOW 1 +#define SPELL_IMPACT_MEDIUM 2 +#define SPELL_IMPACT_HIGH 3 + +#define CONJURE_RECOIL_LIGHT 0 +#define CONJURE_RECOIL_PARTIAL 1 +#define CONJURE_RECOIL_FULL 2 + +#define SPELLBOOK_THEME_BASELINE_QUALITY 6 +#define SPELLBOOK_THEME_BASELINE_FORM_POINTS 7 +#define SPELLBOOK_THEME_BASELINE_TECHNIQUE_POINTS 3 diff --git a/code/__DEFINES/mob_timers.dm b/code/__DEFINES/mob_timers.dm index 59e2bbe732e..eaedd27bd97 100644 --- a/code/__DEFINES/mob_timers.dm +++ b/code/__DEFINES/mob_timers.dm @@ -47,3 +47,6 @@ // I've separated then here for clarity's sake. #define MT_PAINSTUN "painstun" + +#define LIGHTNING_ADAPTATION_COOLDOWN 15 SECONDS +#define MT_LIGHTNING_ADAPTATION "lightning_adaptation" diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm index 3bb7ac564d5..a1cc68fe129 100644 --- a/code/__DEFINES/mobs.dm +++ b/code/__DEFINES/mobs.dm @@ -276,7 +276,8 @@ #define SHOCK_ILLUSION (1 << 2) ///The shock doesn't stun. #define SHOCK_NOSTUN (1 << 3) - +///we only show a visual +#define SHOCK_VISUAL_ONLY (1<<4) #define INCORPOREAL_MOVE_BASIC 1 #define INCORPOREAL_MOVE_SHADOW 2 // leaves a trail of shadows #define INCORPOREAL_MOVE_JAUNT 3 // is blocked by holy water/salt diff --git a/code/__DEFINES/movespeed_modification.dm b/code/__DEFINES/movespeed_modification.dm index 664eece2366..6cac24f8598 100644 --- a/code/__DEFINES/movespeed_modification.dm +++ b/code/__DEFINES/movespeed_modification.dm @@ -118,3 +118,5 @@ #define MOVESPEED_ID_CARDIAC_ARREST "cardiac" #define MOVESPEED_ID_SHOCK "shock" + +#define MOVESPEED_ID_LIGHTNINGSTRUCK "LIGHTNINGSTRUCK" diff --git a/code/__DEFINES/traits/definitions.dm b/code/__DEFINES/traits/definitions.dm index 6ff7acd507d..601472fbbf9 100644 --- a/code/__DEFINES/traits/definitions.dm +++ b/code/__DEFINES/traits/definitions.dm @@ -445,6 +445,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_BLOODDRINKER "Blood Drinker" // Can drink blood without vomiting #define TRAIT_FORAGER "Expert Forager" #define TRAIT_TINY "Tiny" +#define TRAIT_BIGGUY "Big Guy" #define TRAIT_DREAM_WATCHER "Noc Blessed" //Unique Trait of the Dream Watcher Town Elder Class, they have a chance to know about antags or gods influences. #define TRAIT_HOLLOWBONES "Hollow Bones" #define TRAIT_AMAZING_BACK "Light Load" @@ -636,3 +637,10 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai // genetic traits #define TRAIT_ANIMAL_NATURAL_ARMOR "natural_armor" #define TRAIT_ANIMAL_PRODUCTIVE "trait_productive" + +#define TRAIT_CONJURED_SUMMON "conjured_summon" +#define TRAIT_CONJURE_BACKLASH "conjure_backlash" + +#define TRAIT_EMPOWERED_UNARMED "empowered_unarmed" +#define TRAIT_SORCERER "Unattuned" + diff --git a/code/_globalvars/special_traits/traits.dm b/code/_globalvars/special_traits/traits.dm index d38612826e4..9f235aa310d 100644 --- a/code/_globalvars/special_traits/traits.dm +++ b/code/_globalvars/special_traits/traits.dm @@ -180,10 +180,10 @@ allowed_jobs = list(/datum/job/magician, /datum/job/mageapprentice) /datum/special_trait/darkmagic/on_apply(mob/living/carbon/human/character, silent) - character.add_spell(/datum/action/cooldown/spell/eyebite, silent = TRUE) - character.add_spell(/datum/action/cooldown/spell/projectile/sickness, silent = TRUE) - character.add_spell(/datum/action/cooldown/spell/conjure/raise_lesser_undead/necromancer, silent = TRUE) - character.add_spell(/datum/action/cooldown/spell/gravemark, silent = TRUE) + character.add_spell(/datum/action/cooldown/spell/eyebite, silent = TRUE, mastery_spell = TRUE) + character.add_spell(/datum/action/cooldown/spell/projectile/sickness, silent = TRUE, mastery_spell = TRUE) + character.add_spell(/datum/action/cooldown/spell/conjure_summon/raise_lesser_undead/necromancer, silent = TRUE, mastery_spell = TRUE) + character.add_spell(/datum/action/cooldown/spell/gravemark, silent = TRUE, mastery_spell = TRUE) /datum/special_trait/too_smart name = "Too smart" @@ -826,12 +826,12 @@ /datum/special_trait/thinker/on_apply(mob/living/carbon/human/character, silent) character.attributes?.add_sheet(/datum/attribute_holder/sheet/job/thinker) - character.adjust_spell_points(14) //Less points than Court Mage, why do Court mage get 17 points? what even? - character.add_spell(/datum/action/cooldown/spell/undirected/touch/prestidigitation, silent = TRUE) - character.generate_random_attunements(rand(4,6)) + character.adjust_technique_mastery_points(3) + character.adjust_form_mastery_points(8) + character.add_spell(/datum/action/cooldown/spell/undirected/touch/prestidigitation, silent = TRUE, mastery_spell = TRUE) character.mana_pool.set_intrinsic_recharge(MANA_ALL_LEYLINES) character.mana_pool.adjust_mana(100) //I don't know, they don't spawn with their full mana bar, so we give them a bit more mana at the start. - new /obj/item/book/granter/spellbook/master(get_turf(character)) + new /obj/item/spellbook/master(get_turf(character)) /datum/special_trait/skeleton name = "Skeleton" diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index 7a629830efd..73218b04d2b 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -99,6 +99,11 @@ if(next_move > world.time) // in the year 2000... return + if(isliving(src)) + var/mob/living/clicker = src + if(clicker.is_swinging()) + return + if(LAZYACCESS(modifiers, RIGHT_CLICK)) if(LAZYACCESS(modifiers, SHIFT_CLICKED)) ShiftRightClickOn(clicked_atom, modifiers) diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm index c64f19a215c..2875b5549de 100644 --- a/code/_onclick/hud/screen_objects.dm +++ b/code/_onclick/hud/screen_objects.dm @@ -1811,25 +1811,6 @@ icon = 'icons/mob/rogueheat.dmi' screen_loc = mana_loc -/atom/movable/screen/mana/extra_info(mob/user) - var/info = "" - for(var/datum/attunement/attunement as anything in user?.mana_pool.attunements) - var/value = user.mana_pool.attunements[attunement] - if(!value) - continue - - switch(value) - if(0.01 to 0.4) - info += "
Minor [initial(attunement.name)] Attunment" - if(0.41 to 0.7) - info += "
Moderate [initial(attunement.name)] Attunment" - if(0.71 to 1.2) - info += "
Major [initial(attunement.name)] Attunment" - if(1.21 to INFINITY) - info += "
Apex [initial(attunement.name)] Attunment" - - return info - /atom/movable/screen/vis_holder icon = "" invisibility = INVISIBILITY_MAXIMUM diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm index 83d78d71714..499c2a04d39 100644 --- a/code/_onclick/item_attack.dm +++ b/code/_onclick/item_attack.dm @@ -267,6 +267,12 @@ var/datum/intent/cached_intent = user.used_intent if(user.used_intent.swingdelay) sleep(user.used_intent.swingdelay) + + // Getting struck w/ /disrupt swingdelay type sets our swing_state to false. + // If we had the effect, but not the bool, we were interrupted. (Or something else went wrong.) + if(user.is_swinging() && !user.swing_state) + return + if(user.a_intent != cached_intent) return if(QDELETED(src) || QDELETED(M)) @@ -284,14 +290,14 @@ user.adjust_stamina(10) var/turf/turf_before = get_turf(M) SEND_SIGNAL(user, COMSIG_MOB_ITEM_ATTACK_POST_SWINGDELAY, M, user, src) - if(M.checkdefense(user.used_intent, user)) + if(!(signal_return & COMPONENT_ITEM_NO_DEFENSE) && M.checkdefense(user.used_intent, user)) if(M.d_intent == INTENT_PARRY) if(!M.get_active_held_item() && !M.get_inactive_held_item()) //we parried with a bracer, redirect damage if(M.active_hand_index == 1) user.tempatarget = BODY_ZONE_L_ARM else user.tempatarget = BODY_ZONE_R_ARM - if(M.attacked_by(src, user)) //we change intents when attacking sometimes so don't play if we do (embedding items) + if(M.attacked_by(src, user, signal_return)) //we change intents when attacking sometimes so don't play if we do (embedding items) if(user.used_intent == cached_intent) var/tempsound = user.used_intent.hitsound if(tempsound) @@ -336,7 +342,7 @@ "I'm disarmed by [user]!") return - if(M.attacked_by(src, user)) + if(M.attacked_by(src, user, signal_return)) if(user.used_intent == cached_intent) var/tempsound = user.used_intent.hitsound if(tempsound) diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm index 999d1e1f141..044bcc05f6f 100644 --- a/code/_onclick/other_mobs.dm +++ b/code/_onclick/other_mobs.dm @@ -51,7 +51,7 @@ if(L.checkmiss(src)) return TRUE - if(!L.checkdefense(used_intent, src)) + if(HAS_TRAIT(src, TRAIT_EMPOWERED_UNARMED) || !L.checkdefense(used_intent, src)) if(LAZYACCESS(modifiers, RIGHT_CLICK)) if(L.attack_hand_secondary(src, modifiers) != SECONDARY_ATTACK_CALL_NORMAL) return TRUE diff --git a/code/datums/ai/behaviours/hostile/melee_attack.dm b/code/datums/ai/behaviours/hostile/melee_attack.dm index 28cd4eb5a27..79b11255c11 100644 --- a/code/datums/ai/behaviours/hostile/melee_attack.dm +++ b/code/datums/ai/behaviours/hostile/melee_attack.dm @@ -37,6 +37,9 @@ controller.set_blackboard_key(hiding_location_key, hiding_target) basic_mob.face_atom(target) + var/forced_zone = controller.blackboard[BB_FORCED_ATTACK_ZONE] + if(forced_zone) + basic_mob.zone_selected = forced_zone var/list/possible_intents = list() for(var/datum/intent/intent as anything in basic_mob.possible_a_intents) if(istype(intent, /datum/intent/unarmed/help) || istype(intent, /datum/intent/unarmed/shove) || istype(intent, /datum/intent/unarmed/grab)) @@ -286,3 +289,24 @@ weapon.attack_self(pawn) if(!weapon && !offweapon) pawn.d_intent = INTENT_DODGE + +/datum/ai_behavior/opportunistic_ranged_attack + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + required_distance = 0 + action_cooldown = 0.4 SECONDS + +/datum/ai_behavior/opportunistic_ranged_attack/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) + var/atom/target = controller.blackboard[target_key] + return !QDELETED(target) + +/datum/ai_behavior/opportunistic_ranged_attack/perform(delta_time, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) + . = ..() + var/mob/living/simple_animal/hostile/basic_mob = controller.pawn + var/atom/target = controller.blackboard[target_key] + var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key] + if(!istype(basic_mob) || QDELETED(target) || target == basic_mob || !targetting_datum?.can_attack(basic_mob, target)) + finish_action(controller, FALSE, target_key) + return + basic_mob.face_atom(target) + basic_mob.ranged_attack(target) + finish_action(controller, TRUE, target_key) diff --git a/code/datums/ai/controllers/primordials.dm b/code/datums/ai/controllers/primordials.dm new file mode 100644 index 00000000000..4bb5954ec0f --- /dev/null +++ b/code/datums/ai/controllers/primordials.dm @@ -0,0 +1,47 @@ +/datum/ai_controller/flame_primordial + movement_delay = 0.15 SECONDS + ai_movement = /datum/ai_movement/hybrid_pathing + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic() + ) + planning_subtrees = list( + /datum/ai_planning_subtree/being_a_minion, + /datum/ai_planning_subtree/aggro_find_target, + /datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic, + /datum/ai_planning_subtree/basic_ranged_attack_subtree/opportunistic, + /datum/ai_planning_subtree/attack_obstacle_in_path, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + /datum/ai_planning_subtree/simple_self_recovery, + ) + +/datum/ai_controller/water_primordial + movement_delay = 0.15 SECONDS + ai_movement = /datum/ai_movement/hybrid_pathing + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic() + ) + planning_subtrees = list( + /datum/ai_planning_subtree/being_a_minion, + /datum/ai_planning_subtree/aggro_find_target, + /datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic, + /datum/ai_planning_subtree/basic_ranged_attack_subtree/opportunistic, + /datum/ai_planning_subtree/attack_obstacle_in_path, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + /datum/ai_planning_subtree/simple_self_recovery, + ) + +/datum/ai_controller/air_primordial + movement_delay = 0.1 SECONDS + ai_movement = /datum/ai_movement/hybrid_pathing + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic() + ) + planning_subtrees = list( + /datum/ai_planning_subtree/being_a_minion, + /datum/ai_planning_subtree/aggro_find_target, + /datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic, + /datum/ai_planning_subtree/basic_ranged_attack_subtree/opportunistic, + /datum/ai_planning_subtree/attack_obstacle_in_path, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + /datum/ai_planning_subtree/simple_self_recovery, + ) diff --git a/code/datums/ai/controllers/vengeful_spirit.dm b/code/datums/ai/controllers/vengeful_spirit.dm new file mode 100644 index 00000000000..a9471cc6ea5 --- /dev/null +++ b/code/datums/ai/controllers/vengeful_spirit.dm @@ -0,0 +1,13 @@ +/datum/ai_controller/spirit_vengeance + movement_delay = 0.5 SECONDS + + ai_movement = /datum/ai_movement/astar + + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic() + ) + + planning_subtrees = list( + /datum/ai_planning_subtree/being_a_minion, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + ) diff --git a/code/datums/ai/subtrees/attack_obstacles.dm b/code/datums/ai/subtrees/attack_obstacles.dm new file mode 100644 index 00000000000..b4820119446 --- /dev/null +++ b/code/datums/ai/subtrees/attack_obstacles.dm @@ -0,0 +1,93 @@ +/// If there's something between us and our target then we need to queue a behaviour to make it not be there +/datum/ai_planning_subtree/attack_obstacle_in_path + /// Blackboard key containing current target + var/target_key = BB_BASIC_MOB_CURRENT_TARGET + /// The action to execute, extend to add a different cooldown or something + var/attack_behaviour = /datum/ai_behavior/attack_obstructions + +/datum/ai_planning_subtree/attack_obstacle_in_path/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return + + var/turf/next_step = get_step_towards(controller.pawn, target) + if (!next_step.is_blocked_turf(exclude_mobs = TRUE, source_atom = controller.pawn)) + return + + controller.queue_behavior(attack_behaviour, target_key) + // Don't cancel future planning, maybe we can move now + +/// Something is in our way, get it outta here +/datum/ai_behavior/attack_obstructions + action_cooldown = 2 SECONDS + /// If we should attack walls, be prepared for complaints about breaches + var/can_attack_turfs = FALSE + /// For if you want your mob to be able to attack dense objects + var/can_attack_dense_objects = FALSE + +/datum/ai_behavior/attack_obstructions/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() + var/mob/living/basic_mob = controller.pawn + // Humans route through melee_attack_chain which only damages when used_intent is + // an attacking intent. Pick one (excluding help/shove/grab) before we click. + if(ishuman(basic_mob)) + var/mob/living/carbon/human/human_pawn = basic_mob + var/list/possible_intents = list() + for(var/datum/intent/intent as anything in human_pawn.possible_a_intents) + if(istype(intent, /datum/intent/unarmed/help) || istype(intent, /datum/intent/unarmed/shove) || istype(intent, /datum/intent/unarmed/grab)) + continue + possible_intents |= intent + if(length(possible_intents)) + human_pawn.a_intent = pick(possible_intents) + human_pawn.used_intent = human_pawn.a_intent + var/atom/target = controller.blackboard[target_key] + + if (QDELETED(target)) + finish_action(controller, succeeded = FALSE) + return + + var/turf/next_step = get_step_towards(basic_mob, target) + var/dir_to_next_step = get_dir(basic_mob, next_step) + // If moving diagonally we need to punch both ways, or more accurately the one we are blocked in + var/list/dirs_to_move = list() + if (ISDIAGONALDIR(dir_to_next_step)) + for(var/direction in GLOB.cardinals) + if(direction & dir_to_next_step) + dirs_to_move += direction + else + dirs_to_move += dir_to_next_step + + for (var/direction in dirs_to_move) + if (attack_in_direction(controller, basic_mob, direction)) + return + finish_action(controller, succeeded = TRUE) + +/datum/ai_behavior/attack_obstructions/proc/attack_in_direction(datum/ai_controller/controller, mob/living/basic_mob, direction) + var/turf/next_step = get_step(basic_mob, direction) + if (!next_step.is_blocked_turf(exclude_mobs = TRUE, source_atom = controller.pawn)) + return FALSE + + for (var/obj/object as anything in next_step.contents) + if (!can_smash_object(basic_mob, object)) + continue + basic_mob.ClickOn(object, list()) + return TRUE + + if (can_attack_turfs) + basic_mob.ClickOn(next_step) + return TRUE + return FALSE + +/datum/ai_behavior/attack_obstructions/proc/can_smash_object(mob/living/basic_mob, obj/object) + if (!object.density && !can_attack_dense_objects) + return FALSE + if (object.IsObscured()) + return FALSE + if (basic_mob.see_invisible < object.invisibility) + return FALSE + var/list/whitelist = basic_mob.ai_controller.blackboard[BB_OBSTACLE_TARGETING_WHITELIST] + if(whitelist && !is_type_in_typecache(object, whitelist)) + return FALSE + + return TRUE // It's in our way, let's get it out of our way diff --git a/code/datums/ai/subtrees/be_a_minion.dm b/code/datums/ai/subtrees/be_a_minion.dm index 9596bc4df67..84aaddd7897 100644 --- a/code/datums/ai/subtrees/be_a_minion.dm +++ b/code/datums/ai/subtrees/be_a_minion.dm @@ -12,6 +12,15 @@ var/mob/following = controller.blackboard[follow_target] var/mob/living/pawn = controller.pawn + if(isliving(pawn) && SHOULD_RESIST(pawn)) + return + + var/mob/living/threat = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!isliving(threat) || QDELETED(threat) || threat.stat == DEAD) + threat = controller.blackboard[BB_HIGHEST_THREAT_MOB] + if(isliving(threat) && !QDELETED(threat) && threat.stat != DEAD) + return + if(travel) controller.queue_behavior(travel_behavior, location_key) return SUBTREE_RETURN_FINISH_PLANNING //end here @@ -24,7 +33,7 @@ return //no travel target and no one to follow. being a minion in other ways /// Follow the target /datum/ai_behavior/follow_friend - behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION /datum/ai_behavior/follow_friend/setup(datum/ai_controller/controller, target_key) . = ..() var/atom/target = controller.blackboard[target_key] diff --git a/code/datums/ai/subtrees/simple_melee_subtree.dm b/code/datums/ai/subtrees/simple_melee_subtree.dm index 8d6de411004..3ceaee6f4c3 100644 --- a/code/datums/ai/subtrees/simple_melee_subtree.dm +++ b/code/datums/ai/subtrees/simple_melee_subtree.dm @@ -116,3 +116,57 @@ /datum/ai_planning_subtree/basic_melee_attack_subtree/species_hostile melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/species_hostile + +/// Attack something which is already adjacent to us, without ending planning +/datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic + melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/opportunistic + end_planning = FALSE + var/travelling + +///If on this, clears the walk travel target upon first contact with enemy +/datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic/event_loc + travelling = BB_TRAVEL_DESTINATION + +/datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(QDELETED(target) || !controller.pawn.Adjacent(target)) + return + if (isliving(controller.pawn)) + var/mob/living/pawn = controller.pawn + if (LAZYLEN(pawn.do_afters)) + return + + if(travelling && controller.blackboard[travelling]) + controller.clear_blackboard_key(travelling) + controller.queue_behavior(melee_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) + +/// Attack something which is already adjacent to us without moving +/datum/ai_behavior/basic_melee_attack/opportunistic + action_cooldown = 0.2 SECONDS // We gotta check unfortunately often because we're in a race condition with nextmove + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/basic_melee_attack/opportunistic/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) + if (!controller.blackboard_key_exists(targeting_strategy_key)) + CRASH("No target datum was supplied in the blackboard for [controller.pawn]") + return controller.blackboard_key_exists(target_key) + +/datum/ai_behavior/basic_melee_attack/opportunistic/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) + var/atom/movable/atom_pawn = controller.pawn + if(!atom_pawn.CanReach(controller.blackboard[target_key])) + finish_action(controller, TRUE, target_key) // Don't clear target + return FALSE + . = ..() + finish_action(controller, TRUE, target_key) // Try doing something else + +/datum/ai_planning_subtree/basic_ranged_attack_subtree/opportunistic + ranged_attack_behavior = /datum/ai_behavior/opportunistic_ranged_attack + +/datum/ai_planning_subtree/basic_ranged_attack_subtree/opportunistic/SelectBehaviors(datum/ai_controller/controller, delta_time) + var/mob/living/simple_animal/hostile/pawn = controller.pawn + if(!istype(pawn) || !pawn.ranged || pawn.ranged_cooldown > world.time) + return + var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(QDELETED(target) || pawn.Adjacent(target)) + return + controller.queue_behavior(ranged_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) diff --git a/code/datums/ai/targetting_datum/simpe_targetting_datum.dm b/code/datums/ai/targetting_datum/simpe_targetting_datum.dm index e2b02a19381..b5cdfa123d3 100644 --- a/code/datums/ai/targetting_datum/simpe_targetting_datum.dm +++ b/code/datums/ai/targetting_datum/simpe_targetting_datum.dm @@ -62,3 +62,21 @@ if(target.mind?.has_antag_datum(/datum/antagonist/zizocultist)) return FALSE . = ..() + + +GLOBAL_DATUM_INIT(conjured_targetting, /datum/targetting_datum/basic/conjured, new) + +/datum/targetting_datum/basic/conjured + +/datum/targetting_datum/basic/conjured/can_attack(mob/living/living_mob, atom/the_target) + . = ..() + if(!.) + return FALSE + var/datum/component/conjured_minion/comp = living_mob.GetComponent(/datum/component/conjured_minion) + if(!comp) + return TRUE + var/mob/living/summoner = comp.summoner_ref?.resolve() + if(!summoner || summoner.z != living_mob.z) + return TRUE + if(get_dist(the_target, summoner) > comp.leash_range + 1) + return FALSE diff --git a/code/datums/attributes/attributes/skills/magic.dm b/code/datums/attributes/attributes/skills/magic.dm index cd04982281c..f2c03b3a24e 100644 --- a/code/datums/attributes/attributes/skills/magic.dm +++ b/code/datums/attributes/attributes/skills/magic.dm @@ -35,14 +35,6 @@ "...you look up to your captors, smiling through broken teeth. the cackling brings a kick to your ribs... you spit a broken tooth out of your mouth, and mutter under your breath... you hear gurgling as a baptism of blue fire spews from his open mouth..." ) -/datum/attribute/skill/magic/arcane/on_level_change(mob/owner, new_level, old_level) - var/old_tier = floor(old_level / 10) - var/new_tier = floor(new_level / 10) - if(new_tier > old_tier) - owner?.adjust_spell_points(new_tier - old_tier) - else if(new_tier < old_tier) - owner?.adjust_spell_points(new_tier - old_tier) // negative delta = remove points - /datum/attribute/skill/magic/druidic name = "Druidic Trickery" desc = "Represents your character's ability to perform druidic magic. The higher your skill in Druidic Trickery, the more powerful your druidic magic will be." diff --git a/code/datums/chimeric_organs/outputs/wild_magic.dm b/code/datums/chimeric_organs/outputs/wild_magic.dm index 90d924a73b7..29f5ce1db76 100644 --- a/code/datums/chimeric_organs/outputs/wild_magic.dm +++ b/code/datums/chimeric_organs/outputs/wild_magic.dm @@ -7,14 +7,12 @@ /datum/chimeric_node/output/wild_magic/trigger_effect(multiplier) . = ..() if(!length(spell_types)) - for(var/datum/spell_node/node as anything in subtypesof(/datum/spell_node)) + for(var/datum/action/cooldown/spell/node as anything in subtypesof(/datum/action/cooldown/spell)) if(IS_ABSTRACT(node)) continue - if(initial(node.is_passive)) + if(!initial(node.required_form)) continue - if(!initial(node.spell_type)) - continue - spell_types += initial(node.spell_type) + spell_types += node var/picked_type = pick(spell_types) var/datum/action/cooldown/spell/picked_spell = new picked_type diff --git a/code/datums/components/arcane_conduit.dm b/code/datums/components/arcane_conduit.dm new file mode 100644 index 00000000000..87379a89054 --- /dev/null +++ b/code/datums/components/arcane_conduit.dm @@ -0,0 +1,62 @@ +#define CONDUIT_FILTER "arcyne_conduit" + +/datum/component/arcyne_conduit + dupe_mode = COMPONENT_DUPE_UNIQUE + var/outline_color = "#4a90d9" + var/gain_cooldown = 2 SECONDS // 2 seconds between melee momentum gains + var/last_melee_gain = 0 + var/datum/weakref/owner_ref + +/datum/component/arcyne_conduit/Initialize(outline_color_override, mob/living/owner) + if(!isitem(parent)) + return COMPONENT_INCOMPATIBLE + if(outline_color_override) + outline_color = outline_color_override + if(owner) + owner_ref = WEAKREF(owner) + RegisterSignal(owner, COMSIG_LIVING_DEATH, PROC_REF(on_owner_death)) + var/obj/item/I = parent + I.add_filter(CONDUIT_FILTER, 2, list("type" = "outline", "color" = outline_color, "alpha" = 200, "size" = 1)) + RegisterSignal(parent, COMSIG_ITEM_ATTACK, PROC_REF(on_attack_success)) + RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) + +/datum/component/arcyne_conduit/UnregisterFromParent() + var/obj/item/I = parent + if(istype(I)) + I.remove_filter(CONDUIT_FILTER) + UnregisterSignal(parent, list(COMSIG_ITEM_ATTACK, COMSIG_ATOM_EXAMINE)) + var/mob/living/owner = owner_ref?.resolve() + if(owner) + UnregisterSignal(owner, COMSIG_LIVING_DEATH) + +/datum/component/arcyne_conduit/proc/on_owner_death() + SIGNAL_HANDLER + var/mob/living/owner = owner_ref?.resolve() + if(owner) + var/datum/status_effect/buff/arcyne_momentum/M = owner.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(M) + M.bound_weapon = null + qdel(src) + +/datum/component/arcyne_conduit/proc/on_attack_success(obj/item/source, mob/living/target, mob/living/user) + SIGNAL_HANDLER + if(!isliving(user) || !isliving(target)) + return + if(target == user) + return + if(target.stat == DEAD) + return + if(world.time < last_melee_gain + gain_cooldown) + return + var/datum/status_effect/buff/arcyne_momentum/M = user.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(!M) + M = user.apply_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(M) + M.add_stacks(1) + last_melee_gain = world.time + +/datum/component/arcyne_conduit/proc/on_examine(datum/source, mob/user, list/examine_list) + SIGNAL_HANDLER + examine_list += span_notice("This weapon pulses with faint arcyne energy. It is bound as an arcyne conduit, to be used by a skilled spellblade.") + +#undef CONDUIT_FILTER diff --git a/code/datums/components/conjured_minion.dm b/code/datums/components/conjured_minion.dm new file mode 100644 index 00000000000..f203fd2d4c7 --- /dev/null +++ b/code/datums/components/conjured_minion.dm @@ -0,0 +1,369 @@ +#define CONJURE_UNTETHER_ID "conjure_untether" +#define CONJURE_DISMISS_FADE_TIME (4 SECONDS) +#define CONJURE_RECOIL_SLOW "conjure_recoil_slow" + +/datum/component/conjured_minion + var/datum/weakref/summoner_ref + var/recoil_energy_floor = 200 + var/recoil_severity = CONJURE_RECOIL_FULL + var/recoil_stamina_only = FALSE + var/dismissing = FALSE + var/leash_range = 12 + var/next_leash_message = 0 + var/base_alpha = 255 + var/untether_strain = 0 + var/untether_max = 10 + var/tether_timer + +/datum/component/conjured_minion/Initialize(mob/living/summoner, energy_floor = 200, severity = CONJURE_RECOIL_FULL, stamina_only = FALSE) + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + summoner_ref = WEAKREF(summoner) + recoil_energy_floor = energy_floor + recoil_severity = severity + recoil_stamina_only = stamina_only + if(isliving(summoner)) + summoner.add_summoned_minion(parent) + ADD_TRAIT(parent, TRAIT_CONJURED_SUMMON, REF(src)) + RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(on_summon_death)) + RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(check_leash)) + if(ishuman(parent)) + apply_phantasmal() + RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) + var/mob/living/M = parent + base_alpha = M.alpha + make_docile() + addtimer(CALLBACK(src, PROC_REF(make_docile)), 1.5 SECONDS) + tether_timer = addtimer(CALLBACK(src, PROC_REF(check_tether)), 4 SECONDS, TIMER_LOOP | TIMER_STOPPABLE) + +/datum/component/conjured_minion/proc/make_docile() + var/mob/living/M = parent + if(QDELETED(M) || !M.ai_controller) + return + var/mob/living/summoner = summoner_ref?.resolve() + if(!summoner) + return + M.ai_controller.set_blackboard_key(BB_FOLLOW_TARGET, summoner) + M.ai_controller.set_blackboard_key(BB_TARGETTING_DATUM, GLOB.conjured_targetting) + M.pet_passive = TRUE + +/datum/component/conjured_minion/Destroy(force, silent) + if(tether_timer) + deltimer(tether_timer) + tether_timer = null + var/mob/living/summoner = summoner_ref?.resolve() + if(isliving(summoner)) + summoner.remove_summoned_minion(parent) + if(!QDELETED(parent)) + REMOVE_TRAIT(parent, TRAIT_CONJURED_SUMMON, REF(src)) + var/mob/living/M = parent + M.remove_movespeed_modifier(CONJURE_UNTETHER_ID) + return ..() + +/datum/component/conjured_minion/proc/on_summon_death(mob/living/source, gibbed) + SIGNAL_HANDLER + if(dismissing) + return + var/mob/living/summoner = summoner_ref?.resolve() + if(!summoner || summoner.stat == DEAD) + return + if(untether_strain > 0 || summoner.z != source.z || get_dist(source, summoner) > leash_range) + to_chat(summoner, span_warning("A dull ache echoes down the leyline - [source] has perished beyond the tether's reach.")) + return + INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(apply_conjure_recoil), summoner, recoil_energy_floor, recoil_severity, 1, TRUE, recoil_stamina_only) + +/datum/component/conjured_minion/proc/check_leash(atom/movable/source, atom/newloc) + SIGNAL_HANDLER + var/mob/living/M = source + var/mob/living/summoner = summoner_ref?.resolve() + if(!summoner || summoner.z != source.z) + return + var/datum/ai_controller/AC = M.ai_controller + if(AC && AC.blackboard[BB_TRAVEL_DESTINATION]) + return + var/newdist = get_dist(newloc, summoner) + if(newdist <= leash_range) + return + if(newdist < get_dist(source, summoner)) + return + if(M.ckey && world.time > next_leash_message) + next_leash_message = world.time + 3 SECONDS + to_chat(M, span_warning("The tether binding you to your body stops you from moving further..")) + return COMPONENT_MOVABLE_BLOCK_PRE_MOVE + +/datum/component/conjured_minion/proc/check_tether() + var/mob/living/M = parent + if(QDELETED(M) || dismissing) + return + var/mob/living/summoner = summoner_ref?.resolve() + validate_combat_target(M, summoner) + if(summoner && !QDELETED(summoner) && summoner.z == M.z && get_dist(M, summoner) <= leash_range) + if(untether_strain > 0) + relax_tether(M) + return + strain_tether(M) + +/datum/component/conjured_minion/proc/validate_combat_target(mob/living/M, mob/living/summoner) + var/datum/ai_controller/AC = M.ai_controller + if(!AC) + return + var/mob/living/current = AC.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(isnull(current)) + return + if(!QDELETED(current) && !current.stat) + if(!summoner || QDELETED(summoner) || summoner.z != M.z) + return + if(get_dist(current, summoner) <= leash_range + 1) + return + AC.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET) + if(AC.blackboard[BB_HIGHEST_THREAT_MOB] == current) + AC.clear_blackboard_key(BB_HIGHEST_THREAT_MOB) + var/list/table = AC.blackboard[BB_MOB_AGGRO_TABLE] + if(islist(table)) + table -= current + +/datum/component/conjured_minion/proc/strain_tether(mob/living/M) + untether_strain++ + var/mob/living/summoner = summoner_ref?.resolve() + if(untether_strain == 1) + M.visible_message(span_warning("[M] flickers, its form straining against the distant leyline.")) + if(summoner) + to_chat(summoner, span_warning("I feel the tether to [M] strain - my servant is beyond my reach.")) + M.alpha = max(50, M.alpha - 12) + M.add_movespeed_modifier(CONJURE_UNTETHER_ID, update = TRUE, override = TRUE, multiplicative_slowdown = min(untether_strain, 2) * 0.6) + if(untether_strain == untether_max - 2 && summoner) + to_chat(summoner, span_userdanger("The tether to [M] is fraying - it will unravel unless I close the distance!")) + if(untether_strain < untether_max) + return + if(M.ckey) + untether_strain = untether_max + return + M.visible_message(span_warning("[M] loses all cohesion, unraveling as the leyline tether snaps.")) + dismiss_conjured_minion(M) + +/datum/component/conjured_minion/proc/relax_tether(mob/living/M) + untether_strain = 0 + M.remove_movespeed_modifier(CONJURE_UNTETHER_ID) + M.alpha = base_alpha + M.visible_message(span_notice("[M] steadies as its master's presence returns.")) + +/datum/component/conjured_minion/proc/apply_phantasmal() + var/mob/living/M = parent + M.alpha = 170 + var/col = get_phantom_color() + M.add_atom_colour(soften_color(col, 0.55), FIXED_COLOUR_PRIORITY) + M.filters += filter(type = "drop_shadow", x = 0, y = 0, size = 2, offset = 0, color = col) + +/datum/component/conjured_minion/proc/soften_color(col, blend = 0.55) + var/list/parts = ReadRGB(col) + if(length(parts) < 3) + return col + return rgb(parts[1] + (255 - parts[1]) * blend, parts[2] + (255 - parts[2]) * blend, parts[3] + (255 - parts[3]) * blend) + +/datum/component/conjured_minion/proc/get_phantom_color() + var/mob/living/summoner = summoner_ref?.resolve() + var/key = summoner ? "[summoner.real_name]" : "arcyne" + var/hash = 0 + for(var/i in 1 to length(key)) + hash += text2ascii(key, i) + var/list/palette = list("#d13b2e", "#e0a020") + return palette[(hash % length(palette)) + 1] + +/datum/component/conjured_minion/proc/on_examine(datum/source, mob/user, list/examine_list) + SIGNAL_HANDLER + var/mob/living/summoner = summoner_ref?.resolve() + examine_list += span_notice("A phantasmal servant, bound to the will of [summoner ? summoner.real_name : "an unknown magus"].") + +#undef CONJURE_UNTETHER_ID + +/mob/living/carbon/human/proc/release_conjured_gear() + for(var/obj/item/gear in (get_equipped_items() + held_items)) + if(HAS_TRAIT(gear, TRAIT_NODROP)) + qdel(gear) + else + dropItemToGround(gear, force = TRUE) + +/mob/living/proc/add_summoned_minion(mob/living/summon) + if(QDELETED(summon)) + return + if(!summoned_minions) + summoned_minions = list() + if(summon in summoned_minions) + return + if(!length(summoned_minions)) + request_attack_relay() + RegisterSignal(src, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(relay_attack_to_summons), override = TRUE) + RegisterSignal(src, COMSIG_MOB_ITEM_ATTACK, PROC_REF(relay_weapon_attack_to_summons), override = TRUE) + RegisterSignal(src, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, PROC_REF(relay_unarmed_attack_to_summons), override = TRUE) + summoned_minions += summon + +/mob/living/proc/remove_summoned_minion(mob/living/summon) + if(!summoned_minions || !(summon in summoned_minions)) + return + summoned_minions -= summon + if(length(summoned_minions)) + return + summoned_minions = null + UnregisterSignal(src, list(COMSIG_ATOM_WAS_ATTACKED, COMSIG_MOB_ITEM_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK)) + release_attack_relay() + +/mob/living/proc/request_attack_relay() + attack_relay_refs++ + if(attack_relay_refs > 1) + return + if(!HAS_TRAIT(src, TRAIT_RELAYING_ATTACKER)) + AddElement(/datum/element/relay_attackers) + attack_relay_self_added = TRUE + +/mob/living/proc/release_attack_relay() + if(attack_relay_refs <= 0) + return + attack_relay_refs-- + if(attack_relay_refs > 0) + return + if(attack_relay_self_added) + RemoveElement(/datum/element/relay_attackers) + attack_relay_self_added = FALSE + +/mob/living/proc/relay_attack_to_summons(mob/living/source, atom/attacker, damage) + SIGNAL_HANDLER + if(!isliving(attacker) || !length(summoned_minions)) + return + for(var/mob/living/summon in summoned_minions) + if(QDELETED(summon) || summon.stat == DEAD || summon == attacker) + continue + if(summon.faction_check_atom(attacker)) + continue + var/datum/component/ai_aggro_system/aggro = summon.GetComponent(/datum/component/ai_aggro_system) + if(!aggro) + continue + aggro.add_threat_to_mob_capped(attacker, 24, 24) + +/mob/living/proc/relay_weapon_attack_to_summons(datum/source, mob/target, mob/user, obj/item/weapon) + SIGNAL_HANDLER + if(weapon && !weapon.force) + return + propagate_focus_aggro(target) + +/mob/living/proc/relay_unarmed_attack_to_summons(datum/source, atom/target, proximity) + SIGNAL_HANDLER + if(!cmode) + return + propagate_focus_aggro(target) + +/mob/living/proc/propagate_focus_aggro(atom/target) + if(!isliving(target) || target == src || !length(summoned_minions)) + return + for(var/mob/living/summon in summoned_minions) + if(QDELETED(summon) || summon.stat == DEAD || summon == target) + continue + if(summon.faction_check_atom(target)) + continue + var/datum/component/ai_aggro_system/aggro = summon.GetComponent(/datum/component/ai_aggro_system) + if(!aggro) + continue + aggro.add_threat_to_mob_capped(target, 18, 18) + + +/proc/dismiss_conjured_minion(mob/living/M) + if(QDELETED(M)) + return + var/datum/component/conjured_minion/minion = M.GetComponent(/datum/component/conjured_minion) + if(minion) + minion.dismissing = TRUE + M.ai_controller?.set_ai_status(AI_STATUS_OFF) + M.visible_message(span_notice("[M] unravels, dissolving back into the leyline.")) + animate(M, alpha = 0, time = 4 SECONDS) + QDEL_IN(M, 4 SECONDS) + + +/mob/living/proc/conjure_damage_fraction() + if(maxHealth <= 0) + return 0 + var/total = getBruteLoss() + getFireLoss() + getToxLoss() + getOxyLoss() + return clamp(total / maxHealth, 0, 1) + +/proc/apply_conjure_recoil(mob/living/summoner, energy_floor = 200, severity = CONJURE_RECOIL_FULL, scale = 1, block = TRUE, stamina_only = FALSE) + if(!istype(summoner)) + return + scale = clamp(scale, 0, 1) + if(scale <= 0) + return + if(stamina_only) + summoner.adjust_stamina(-round(summoner.maximum_stamina * 0.5 * scale)) + to_chat(summoner, span_warning("The leyline snaps taut and tears the wind from me as my primordial unravels.")) + scale *= 0.5 + block = FALSE + if(severity == CONJURE_RECOIL_LIGHT) + to_chat(summoner, span_warning("A jolt of pain stings me as my conjured servant falls.")) + return + + if(summoner.energy > energy_floor) + summoner.energy = max(energy_floor, summoner.energy - (summoner.energy - energy_floor) * scale) + + var/list/base_stats + var/base_duration + if(severity == CONJURE_RECOIL_FULL) + base_stats = list(STAT_STRENGTH = -4, STAT_SPEED = -4, STAT_CONSTITUTION = -4, STAT_ENDURANCE = -4, STATKEY_PER = -3, STAT_INTELLIGENCE = -3) + base_duration = 3 MINUTES + else + base_stats = list(STAT_STRENGTH = -2, STAT_CONSTITUTION = -2, STAT_ENDURANCE = -2) + base_duration = 45 SECONDS + + if(block) + var/slow_time = round(30 * scale) + if(slow_time > 0) + summoner.add_movespeed_modifier(CONJURE_RECOIL_SLOW, update = TRUE, override = TRUE, multiplicative_slowdown = 2 * scale) + addtimer(CALLBACK(summoner, TYPE_PROC_REF(/mob, remove_movespeed_modifier), CONJURE_RECOIL_SLOW), slow_time) + summoner.emote("painscream") + to_chat(summoner, span_userdanger("Agony tears through me as my conjured servant is struck down!")) + else if(!stamina_only) + to_chat(summoner, span_warning("A cold recoil ripples through me as I unbind my servant.")) + + summoner.apply_status_effect(/datum/status_effect/debuff/conjure_backlash, scale, base_stats, base_duration, block) + summoner.apply_status_effect(/datum/status_effect/debuff/exposed, 10 SECONDS) + +/datum/status_effect/debuff/conjure_backlash + id = "conjure_backlash" + alert_type = /atom/movable/screen/alert/status_effect/debuff/conjure_backlash + status_type = STATUS_EFFECT_REPLACE + duration = 3 MINUTES + var/blocks_resummon = FALSE + +/datum/status_effect/debuff/conjure_backlash/on_creation(mob/living/new_owner, scale = 1, list/base_stats, base_duration = 45 SECONDS, block = FALSE) + scale = clamp(scale, 0, 1) + blocks_resummon = block + var/list/scaled = list() + if(islist(base_stats)) + for(var/statkey in base_stats) + var/mag = round(abs(base_stats[statkey]) * scale) + if(mag) + scaled[statkey] = -mag + effectedstats = scaled + duration = max(1 SECONDS, round(base_duration * scale)) + return ..(new_owner) + +/datum/status_effect/debuff/conjure_backlash/on_apply() + . = ..() + if(!.) + return + if(blocks_resummon) + ADD_TRAIT(owner, TRAIT_CONJURE_BACKLASH, "conjure_backlash") + +/datum/status_effect/debuff/conjure_backlash/on_remove() + if(blocks_resummon) + REMOVE_TRAIT(owner, TRAIT_CONJURE_BACKLASH, "conjure_backlash") + return ..() + +/datum/status_effect/debuff/conjure_backlash/be_replaced() + if(blocks_resummon) + REMOVE_TRAIT(owner, TRAIT_CONJURE_BACKLASH, "conjure_backlash") + return ..() + +/atom/movable/screen/alert/status_effect/debuff/conjure_backlash + name = "Conjurer's Backlash" + desc = "The unbinding of my conjured servant recoils upon me - the more grievously it was hurt, the deeper the toll. My body and focus are sapped until it passes." + icon_state = "debuff" + +#undef CONJURE_DISMISS_FADE_TIME +#undef CONJURE_RECOIL_SLOW diff --git a/code/datums/components/orbiter.dm b/code/datums/components/orbiter.dm index 00732b29525..4cf38bf2101 100644 --- a/code/datums/components/orbiter.dm +++ b/code/datums/components/orbiter.dm @@ -9,13 +9,13 @@ //rotation_speed: how fast to rotate (how many ds should it take for a rotation to complete) //rotation_segments: the resolution of the orbit circle, less = a more block circle, this can be used to produce hexagons (6 segments) triangles (3 segments), and so on, 36 is the best default. //pre_rotation: Chooses to rotate src 90 degress towards the orbit dir (clockwise/anticlockwise), useful for things to go "head first" like ghosts -/datum/component/orbiter/Initialize(atom/movable/orbiter, radius, clockwise, rotation_speed, rotation_segments, pre_rotation) +/datum/component/orbiter/Initialize(atom/movable/orbiter, radius, clockwise, rotation_speed, rotation_segments, pre_rotation, starting_rotation) if(!istype(orbiter) || !isatom(parent) || isarea(parent)) return COMPONENT_INCOMPATIBLE orbiter_list = list() - begin_orbit(orbiter, radius, clockwise, rotation_speed, rotation_segments, pre_rotation) + begin_orbit(orbiter, radius, clockwise, rotation_speed, rotation_segments, pre_rotation, starting_rotation) /datum/component/orbiter/RegisterWithParent() var/atom/target = parent @@ -60,7 +60,7 @@ return COMPONENT_INCOMPATIBLE move_react(new_parent) -/datum/component/orbiter/proc/begin_orbit(atom/movable/orbiter, radius, clockwise, rotation_speed, rotation_segments, pre_rotation) +/datum/component/orbiter/proc/begin_orbit(atom/movable/orbiter, radius, clockwise, rotation_speed, rotation_segments, pre_rotation, starting_rotation) if(!istype(orbiter)) stack_trace("begin orbit called on [orbiter] which is a [orbiter.type]!") return @@ -90,6 +90,8 @@ var/matrix/shift = matrix(orbiter.transform) shift.Translate(0, radius) + if(starting_rotation) + shift.Turn(starting_rotation) orbiter.transform = shift orbiter.SpinAnimation(rotation_speed, -1, clockwise, rotation_segments, parallel = FALSE) @@ -164,11 +166,11 @@ ///////////////////// -/atom/movable/proc/orbit(atom/A, radius = 10, clockwise = FALSE, rotation_speed = 20, rotation_segments = 36, pre_rotation = TRUE) +/atom/movable/proc/orbit(atom/A, radius = 10, clockwise = FALSE, rotation_speed = 20, rotation_segments = 36, pre_rotation = TRUE, starting_rotation = 0) if(!istype(A) || !get_turf(A) || A == src) return orbit_target = A - return A.AddComponent(/datum/component/orbiter, src, radius, clockwise, rotation_speed, rotation_segments, pre_rotation) + return A.AddComponent(/datum/component/orbiter, src, radius, clockwise, rotation_speed, rotation_segments, pre_rotation, starting_rotation) /atom/movable/proc/stop_orbit(datum/component/orbiter/orbits) orbit_target = null diff --git a/code/datums/components/skill_bind.dm b/code/datums/components/skill_bind.dm new file mode 100644 index 00000000000..0241d06896c --- /dev/null +++ b/code/datums/components/skill_bind.dm @@ -0,0 +1,68 @@ +#define SKILL_BIND_GLOW_FILTER "skill_bind_glow" + +/datum/component/skill_bind + var/swapped_skill + var/original_skill + var/stored = FALSE + var/datum/weakref/binder_ref + var/glow_applied = FALSE + +/datum/component/skill_bind/Initialize(skill, mob/binder) + if(!isitem(parent)) + return COMPONENT_INCOMPATIBLE + if(!skill) + return COMPONENT_INCOMPATIBLE + swapped_skill = skill + if(binder) + binder_ref = WEAKREF(binder) + var/obj/item/I = parent + original_skill = I.associated_skill + stored = TRUE + I.associated_skill = swapped_skill + apply_glow(I) + RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) + RegisterSignal(parent, COMSIG_ITEM_PICKUP, PROC_REF(on_pickup)) + +/datum/component/skill_bind/proc/apply_glow(obj/item/I) + if(glow_applied || already_glows(I)) + return + I.add_filter(SKILL_BIND_GLOW_FILTER, 2, list("type" = "drop_shadow", "x" = 0, "y" = 0, "size" = 1, "offset" = 2, "color" = GLOB.form_colors[FORM_ARCANE])) + glow_applied = TRUE + +/datum/component/skill_bind/proc/already_glows(obj/item/I) + if(I.GetComponent(/datum/component/conjured_item)) + return TRUE + for(var/filter_name in I.filter_data) + var/list/params = I.filter_data[filter_name] + if(params?["type"] == "outline" || params?["type"] == "drop_shadow") + return TRUE + return FALSE + +/datum/component/skill_bind/proc/on_pickup(datum/source, mob/taker) + SIGNAL_HANDLER + var/mob/binder = binder_ref?.resolve() + if(taker && taker == binder) + return + var/obj/item/I = parent + if(istype(I)) + I.visible_message(span_warning("The arcyne bond on [I] flickers and dies as it changes hands.")) + if(taker) + to_chat(taker, span_notice("[I] shudders in my grip as an arcyne bond sloughs off of it.")) + qdel(src) + +/datum/component/skill_bind/proc/on_examine(datum/source, mob/user, list/examine_list) + SIGNAL_HANDLER + examine_list += "An arcyne bond guides this weapon; it answers to a conjurer's training." + +/datum/component/skill_bind/Destroy() + var/obj/item/I = parent + if(istype(I)) + if(stored && I.associated_skill == swapped_skill) + I.associated_skill = original_skill + if(glow_applied) + I.remove_filter(SKILL_BIND_GLOW_FILTER) + glow_applied = FALSE + UnregisterSignal(I, list(COMSIG_ATOM_EXAMINE, COMSIG_ITEM_PICKUP)) + return ..() + +#undef SKILL_BIND_GLOW_FILTER diff --git a/code/datums/components/spell_modifiers.dm b/code/datums/components/spell_modifiers.dm new file mode 100644 index 00000000000..3b91bca57c3 --- /dev/null +++ b/code/datums/components/spell_modifiers.dm @@ -0,0 +1,66 @@ +/datum/component/spell_modifier + dupe_mode = COMPONENT_DUPE_ALLOWED + + /// list(FORM_FIRE = 1.2, FORM_ICE = 0.8, ...) - missing form = 1 + var/list/form_cost_multipliers + var/list/form_cast_speed_multipliers + var/list/form_magnitude_modifications + + /// list(TECHNIQUE_X = 1.2, ...) - missing technique = 1 + var/list/technique_cost_multipliers + var/list/technique_cast_speed_multipliers + var/list/technique_magnitude_modifications + +/datum/component/spell_modifier/Initialize( + list/form_cost_multipliers, + list/form_cast_speed_multipliers, + list/form_magnitude_modifications, + list/technique_cost_multipliers, + list/technique_cast_speed_multipliers, + list/technique_magnitude_modifications, +) + if(!isitem(parent)) + return COMPONENT_INCOMPATIBLE + src.form_cost_multipliers = form_cost_multipliers || list() + src.form_cast_speed_multipliers = form_cast_speed_multipliers || list() + src.form_magnitude_modifications = form_magnitude_modifications || list() + src.technique_cost_multipliers = technique_cost_multipliers || list() + src.technique_cast_speed_multipliers = technique_cast_speed_multipliers || list() + src.technique_magnitude_modifications = technique_magnitude_modifications || list() + +/datum/component/spell_modifier/RegisterWithParent() + RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equipped)) + RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(on_dropped)) + RegisterSignal(parent, COMSIG_SPELL_REQUEST_MODIFIERS, PROC_REF(on_request_modifiers)) + +/datum/component/spell_modifier/UnregisterFromParent() + UnregisterSignal(parent, list(COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_DROPPED)) + +/datum/component/spell_modifier/proc/on_equipped(datum/source, mob/user) + SIGNAL_HANDLER + RegisterSignal(user, COMSIG_SPELL_REQUEST_MODIFIERS, PROC_REF(on_request_modifiers), override = TRUE) + +/datum/component/spell_modifier/proc/on_dropped(datum/source, mob/user) + SIGNAL_HANDLER + UnregisterSignal(user, COMSIG_SPELL_REQUEST_MODIFIERS) + +/datum/component/spell_modifier/proc/on_request_modifiers(datum/source, list/modifiers) + SIGNAL_HANDLER + add_modifier_entries(modifiers, "form", form_cost_multipliers, form_cast_speed_multipliers, form_magnitude_modifications) + add_modifier_entries(modifiers, "technique", technique_cost_multipliers, technique_cast_speed_multipliers, technique_magnitude_modifications) + +/datum/component/spell_modifier/proc/add_modifier_entries(list/modifiers, key, list/cost_multipliers, list/cast_speed_multipliers, list/magnitude_modifications) + var/list/keys = cost_multipliers | cast_speed_multipliers | magnitude_modifications + for(var/entry_key in keys) + var/cost = cost_multipliers[entry_key] || 1 + var/cast_speed = cast_speed_multipliers[entry_key] || 1 + var/magnitude = magnitude_modifications[entry_key] || 0 + if(cost == 1 && cast_speed == 1 && magnitude == 0) + continue + var/list/entry = list( + "cost" = cost, + "castSpeed" = cast_speed, + "magnitude" = magnitude, + ) + entry[key] = entry_key + modifiers += list(entry) diff --git a/code/datums/components/use_mana.dm b/code/datums/components/use_mana.dm deleted file mode 100644 index 93f1582dd58..00000000000 --- a/code/datums/components/use_mana.dm +++ /dev/null @@ -1,284 +0,0 @@ -/* Design notes: -* This component designates the parent object as something that "uses mana". -* As such, it should modify the object's behavior at some point to require, consume, or check mana. -* Example: A magic crowbar that requires 5 mana to use it with anything. -* A component created for this crowbar should register the various signals for checking things, such as COMSIG_TOOL_START_USE, or -* COMSIG_ITEM_ATTACK. These should be linked to a proc that checks if the user has enough available mana (not sure how to make this info available with the current setuo) -* and if not, return something that cancels the proc. -* However, if the mana IS sufficient, we should listen for the successful item act signal, and react by, say, subtracting 5 mana from the mana pool provided. -* Not all need to do this, though, some could simply check and no nothing else, or others. -*/ - -/// Designates the item it's added to as something that "uses mana". -/datum/component/uses_mana - var/datum/callback/get_mana_callback - var/datum/callback/activate_check_failure_callback - var/datum/callback/get_user_callback - var/datum/callback/get_mana_required_callback - - var/list/datum/attunement/attunements - - var/pre_use_check_with_feedback_comsig - var/pre_use_check_comsig - var/post_use_comsig - - var/mana_required - -/datum/component/uses_mana/Initialize( - datum/callback/activate_check_failure_callback, - datum/callback/get_user_callback, - pre_use_check_with_feedback_comsig, - pre_use_check_comsig, - post_use_comsig, - datum/callback/mana_required, - list/datum/attunement/attunements, -) - . = ..() - - if (isnull(pre_use_check_with_feedback_comsig)) - stack_trace("pre_use with feed back null") - return COMPONENT_INCOMPATIBLE - if (isnull(post_use_comsig)) - stack_trace("post_use comsig null") - return COMPONENT_INCOMPATIBLE - - src.activate_check_failure_callback = activate_check_failure_callback - src.get_user_callback = get_user_callback - - if (istype(mana_required)) - src.get_mana_required_callback = mana_required - else if (isnum(mana_required)) - src.mana_required = mana_required - - src.attunements = attunements - src.pre_use_check_with_feedback_comsig = pre_use_check_with_feedback_comsig - src.post_use_comsig = post_use_comsig - - -/datum/component/uses_mana/RegisterWithParent() - . = ..() - - RegisterSignal(parent, pre_use_check_with_feedback_comsig, PROC_REF(can_activate_with_feedback)) - RegisterSignal(parent, post_use_comsig, PROC_REF(react_to_successful_use)) - -/datum/component/uses_mana/UnregisterFromParent() - . = ..() - - UnregisterSignal(parent, pre_use_check_with_feedback_comsig) - UnregisterSignal(parent, post_use_comsig) - -// TODO: Do I need the vararg? -/// Should return the numerical value of mana needed to use whatever it is we're using. Unaffected by attunements. -/datum/component/uses_mana/proc/get_mana_required(atom/caster, ...) // Get the mana required to cast the spell. - if (!isnull(get_mana_required_callback)) - return get_mana_required_callback?.Invoke(arglist(args)) - - var/required = 0 - - if (!isnull(mana_required)) - required = mana_required - else - return stack_trace("Both the Callback and value for mana required is null!") - return required - -/datum/component/uses_mana/proc/get_mana_to_use() - var/atom/movable/caster = get_parent_user() - var/list/datum/mana_pool/usable_pools = list() - - for (var/atom/movable/thing as anything in caster.get_all_contents()) - if (!isnull(thing.mana_pool) && HAS_TRAIT(thing, TRAIT_POOL_AVAILABLE_FOR_CAST)) - usable_pools += thing.mana_pool - - if (!isnull(caster.mana_pool)) //we want this last so foci run first - usable_pools += caster.mana_pool - - - return usable_pools - - -/// Should return TRUE if the total adjusted mana of all mana pools surpasses get_mana_required(). FALSE otherwise. -/datum/component/uses_mana/proc/is_mana_sufficient(atom/movable/user, ...) - var/total_effective_mana = 0 - var/list/datum/mana_pool/provided_mana = get_mana_to_use() - var/required_mana = get_mana_required(arglist(args)) - var/atom/caster = user - - - for (var/datum/mana_pool/iterated_pool as anything in provided_mana) - total_effective_mana += iterated_pool.get_attuned_amount(attunements, caster) - if (total_effective_mana > required_mana) - return TRUE - else - return FALSE - -/// The primary proc we will use for draining mana to simulate it being consumed to power our actions. -/datum/component/uses_mana/proc/drain_mana(...) - - var/mob/user = get_user_callback?.Invoke() - - var/mana_consumed = -get_mana_required(arglist(args)) - var/total_mana_consumed = -mana_consumed - if (isnull(mana_consumed)) - stack_trace("mana_consumed after get_mana_required is null!") - return - - var/list/datum/mana_pool/available_pools = get_mana_to_use() - var/mob/living/caster = get_parent_user() - var/attunement_total_value = 0 - var/total_damage = 0 - for(var/datum/attunement/attunement as anything in attunements) - attunement_total_value += attunements[attunement] - - while (mana_consumed <= -0.05) - var/mult - var/attuned_cost - for (var/datum/mana_pool/pool as anything in available_pools) - mult = pool.get_overall_attunement_mults(attunements, user) - attuned_cost = (mana_consumed * mult) - if (pool.amount < attuned_cost) - attuned_cost = (pool.amount) - var/mana_adjusted = SAFE_DIVIDE(pool.adjust_mana((attuned_cost)), mult) * (has_world_trait(/datum/world_trait/noc_wisdom) ? (is_ascendant(NOC) ? 0.7 : 0.8) : 1) - mana_consumed -= mana_adjusted - record_featured_stat(FEATURED_STATS_MAGES, user, abs(mana_adjusted)) - record_round_statistic(STATS_MANA_SPENT, abs(mana_adjusted)) - if (available_pools.Find(pool) == available_pools.len && mana_consumed <= -0.05) // if we're at the end of the list and mana_consumed is not 0 or near 0 (floating points grrr) - mana_consumed = 0 // lets terminate the loop to be safe - if(pool.parent == caster) - for(var/datum/attunement/attunement as anything in attunements) - if(pool.negative_attunements[attunement] < 0) - var/composition_gain = attunement_total_value / attunements[attunement] - var/negative_impact_mana = total_mana_consumed * composition_gain - total_damage += round(negative_impact_mana * 0.1,1) - if(total_damage) - caster.mana_pool.mana_backlash(total_damage) - - - -/// Should be the raw conditional we use for determining if the thing that "uses mana" can actually -/// activate the behavior that "uses mana". -/datum/component/uses_mana/proc/can_activate(...) - return is_mana_sufficient(arglist(list(get_parent_user()) + args)) - -/// Wrapper for can_activate(). Should return a bitflag that will be passed down to the signal sender on failure. -/datum/component/uses_mana/proc/can_activate_with_feedback(...) - var/can_activate - var/list/argss = args.Copy(1) - can_activate = can_activate(arglist(argss)) //doesnt return this + can_activate_check_... because returning TRUE/FALSE can gave bitflag implications - - if (!can_activate) - return can_activate_check_failure(arglist(args.Copy())) - return NONE - -/// What can_activate_check returns apon failing to activate. -/datum/component/uses_mana/proc/can_activate_check_failure(...) - return !activate_check_failure_callback?.Invoke(arglist(args)) - -/// Should react to a post-use signal given by the parent, and ideally subtract mana, or something. -/datum/component/uses_mana/proc/react_to_successful_use(...) - - drain_mana(arglist(list(get_parent_user()) + args)) - - return - -/datum/component/uses_mana/proc/get_parent_user() - return get_user_callback?.Invoke() - -// this is basically only used to cut down on boilerplate. -// all that remains is stuff that has to be on the spell itself to work (checking for mana cost, attunements, some of the callbacks) -// i'll provide a sample block so this file can be a "tutorial" IG -/datum/component/uses_mana/spell - can_transfer = FALSE - -/datum/component/uses_mana/spell/Initialize( - datum/callback/activate_check_failure_callback, - datum/callback/get_user_callback, - pre_use_check_with_feedback_comsig = COMSIG_SPELL_BEFORE_CAST, - pre_use_check_comsig, - post_use_comsig = COMSIG_SPELL_AFTER_CAST, - datum/callback/mana_required, - list/datum/attunement/attunements - ) - - . = ..() - -/* sample tutorial block. plus explanations. - -/datum/action/cooldown/spell/ourspell/New(Target, original) - . = ..() - - var/list/datum/attunement/attunements = GLOB.default_attunements.Copy() - attunements[MAGIC_ELEMENT_X] += SPELL_X_ATTUNEMENT // define the numeric value you want for the attunement ideally at the top, it is the value you want the spell's cost to be multiplied (done using decimals since its supposed to be a discount) by when the user is correctly attunned.. - replace the "X" with what element you want, such as "light". Replace "SPELL" with the name of your spell. - - AddComponent(/datum/component/uses_mana/spell, \ // the two main signals are pre-defined in uses_mana/spell so you don't have to copy paste it. everything here needs to be in the init since its on the spell itself. - activate_check_failure_callback = CALLBACK(src, PROC_REF(spell_cannot_activate)), \ // this is defined on datum/action/cooldown/spell and just sends the signal used to cancel casting. must be in this define. - get_user_callback = CALLBACK(src, PROC_REF(get_owner)), \ // "get_owner" is a proc on datum/action/ and must be defined in "the block" - mana_required = mana_cost, \ // mana_cost is defined on the spell datum action, set it to a variable at the top ideally. if you want fancier stuff, see the next line: - mana_required = CALLBACK(src, PROC_REF(get_mana_consumed)), \ // obviously only ever set mana required ONCE, but this is an example of what you want to do if you want to do a variable cost system, define the proc for this on the spell itself. - attunements = attunements, \ // see above. you can just copy paste this to every instance, if you don't want attunements just don't add this block or the two lines outside the add component - ) - - heres a sample callback for get mana required, taken from healing touch. - - /datum/action/cooldown/spell/touch/healing_touch/proc/get_mana_consumed(atom/caster, atom/cast_on, ...) - return (brute_heal + burn_heal + tox_heal + oxy_heal + pain_heal * 3) \ // instead this will calculate your cost based on how much it heals. whenever it wants to check the mana cost value, it'll defer to this proc. - * mana_cost -*/ -// this should be the only thing left lmfao, the rest is going to be held till the end -/** - * 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/component/uses_mana/spell/RegisterWithParent() - . = ..() - - RegisterSignal(parent, COMSIG_SPELL_BEFORE_CAST, PROC_REF(handle_precast)) - RegisterSignal(parent, COMSIG_SPELL_CAST, PROC_REF(handle_cast)) - RegisterSignal(parent, COMSIG_SPELL_AFTER_CAST, PROC_REF(react_to_successful_use)) */ - -/* /datum/component/uses_mana/spell/UnregisterFromParent() - . = ..() - - UnregisterSignal(parent, COMSIG_SPELL_BEFORE_CAST) - UnregisterSignal(parent, COMSIG_SPELL_CAST) - UnrecooldowngisterSignal(parent, COMSIG_SPELL_AFTER_CAST) */ - -/* /datum/component/uses_mana/spell/give_unable_to_activate_feedback(atom/cast_on) - . = ..() - var/datum/action/cooldown/spell/spell_parent = parent - - spell_parent.owner.balloon_alert(spell_parent.owner, "insufficient mana!") */ // should be redundant, holding onto till pr completion - -// SIGNAL HANDLERS - - -/* /datum/component/uses_mana/spell/proc/handle_precast(atom/cast_on) - SIGNAL_HANDLER - return can_activate_with_feedback() */ // todo get this up to date - //can_activate_with_feedback(TRUE, parent_spell.owner, cast_on) - //var/datum/action/cooldown/spell/parent_spell = parent - - - -/** - * 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/component/uses_mana/spell/proc/handle_cast(atom/cast_on) - SIGNAL_HANDLER - return - -/datum/component/uses_mana/spell/proc/get_mana_required_spell(atom/caster, atom/cast_on, ...) - if(ismob(caster)) - var/mob/caster_mob = caster - return caster_mob.get_casting_cost_mult() - return 1 */ diff --git a/code/datums/components/wet_floor.dm b/code/datums/components/wet_floor.dm index 9f29848415b..5ec788c23c3 100644 --- a/code/datums/components/wet_floor.dm +++ b/code/datums/components/wet_floor.dm @@ -5,7 +5,7 @@ var/lube_flags = NONE //why do we have this? var/list/time_left_list //In deciseconds. var/static/mutable_appearance/permafrost_overlay = mutable_appearance('icons/effects/water.dmi', "ice_floor") - var/static/mutable_appearance/ice_overlay = mutable_appearance('icons/turf/overlays.dmi', "snowfloor") + var/static/mutable_appearance/ice_overlay = mutable_appearance('icons/turf/overlays.dmi', "snowfloor", layer = ABOVE_OPEN_TURF_LAYER) var/static/mutable_appearance/water_overlay = mutable_appearance('icons/effects/water.dmi', "wet_floor_static") var/static/mutable_appearance/generic_turf_overlay = mutable_appearance('icons/effects/water.dmi', "wet_static") var/current_overlay diff --git a/code/datums/faith/devotion_subtypes.dm b/code/datums/faith/devotion_subtypes.dm index 822ef3fd9a0..2eca9125e14 100644 --- a/code/datums/faith/devotion_subtypes.dm +++ b/code/datums/faith/devotion_subtypes.dm @@ -187,7 +187,7 @@ miracles = list( CLERIC_T0 = list(/datum/action/cooldown/spell/undirected/touch/orison, /datum/action/cooldown/spell/aoe/snuff), CLERIC_T1 = /datum/action/cooldown/spell/projectile/profane, - CLERIC_T2 = /datum/action/cooldown/spell/conjure/raise_lesser_undead, + CLERIC_T2 = /datum/action/cooldown/spell/conjure_summon/raise_lesser_undead, CLERIC_T3 = /datum/action/cooldown/spell/undirected/rituos, ) traits = list(TRAIT_DEATHSIGHT) diff --git a/code/datums/keybinding/carbon.dm b/code/datums/keybinding/carbon.dm index bfd9d592438..e1e642a3287 100644 --- a/code/datums/keybinding/carbon.dm +++ b/code/datums/keybinding/carbon.dm @@ -92,6 +92,32 @@ C.rog_intent_change(4) return TRUE + +/datum/keybinding/carbon/toggle_arc_mode + hotkey_keys = list("ShiftG") + name = "toggle_arc_mode" + full_name = "Toggle Spell Alt Mode" + description = "Toggle alt mode on the currently active spell - arc mode for projectiles, ward type cycling, etc." + category = CATEGORY_CARBON + +/datum/keybinding/carbon/toggle_arc_mode/down(client/user) + . = ..() + if(!ishuman(user.mob)) + return FALSE + var/mob/living/carbon/human/H = user.mob + + // Check new V2 spell system first + var/datum/action/cooldown/spell/projectile/v2_spell = H.click_intercept + if(istype(v2_spell)) + v2_spell.toggle_arc_mode(H) + return TRUE + + // Check for generic alt mode (ward cycling, etc.) + var/datum/action/cooldown/spell/v2_generic = H.click_intercept + if(istype(v2_generic) && v2_generic.toggle_alt_mode(H)) + return TRUE + + //****** Quad Intents ****** /datum/keybinding/carbon/give_intent diff --git a/code/datums/mana/leylines/_leyline.dm b/code/datums/mana/leylines/_leyline.dm index f690fd85005..abaf6c092c6 100644 --- a/code/datums/mana/leylines/_leyline.dm +++ b/code/datums/mana/leylines/_leyline.dm @@ -30,7 +30,6 @@ GLOBAL_LIST_EMPTY_TYPED(all_leylines, /datum/mana_pool/leyline) /// The lines of latent energy that run under the universe. Available to all people in the game. Should be high capacity, but slow to recharge. /datum/mana_pool/leyline var/datum/leyline_variable/leyline_intensity/intensity - var/list/datum/leyline_variable/attunement_theme/themes maximum_mana_capacity = LEYLINE_BASE_CAPACITY @@ -46,7 +45,6 @@ GLOBAL_LIST_EMPTY_TYPED(all_leylines, /datum/mana_pool/leyline) /datum/mana_pool/leyline/Destroy(force) QDEL_NULL(intensity) - QDEL_LIST(themes) if(current_beam) QDEL_NULL(current_beam) @@ -58,10 +56,6 @@ GLOBAL_LIST_EMPTY_TYPED(all_leylines, /datum/mana_pool/leyline) GLOB.all_leylines += src intensity = generate_initial_intensity() - themes = generate_initial_themes() - - for (var/datum/leyline_variable/attunement_theme/theme as anything in themes) - theme.adjust_attunements(attunements_to_generate) maximum_mana_capacity *= (intensity.overall_mult) softcap = maximum_mana_capacity @@ -75,18 +69,10 @@ GLOBAL_LIST_EMPTY_TYPED(all_leylines, /datum/mana_pool/leyline) return ..() -/datum/mana_pool/leyline/generate_initial_attunements() - return attunements_to_generate.Copy() - /datum/mana_pool/leyline/proc/generate_initial_intensity() var/picked_intensity = pickweight(GLOB.leyline_intensities) return new picked_intensity -/datum/mana_pool/leyline/proc/generate_initial_themes() - var/list/datum/leyline_variable/attunement_theme/themes = get_random_attunement_themes() - - return themes - /// GETTERS / SETTERS /datum/proc/get_accessable_leylines() @@ -157,10 +143,8 @@ GLOBAL_LIST_EMPTY_TYPED(all_leylines, /datum/mana_pool/leyline) var/list/data = generate_start_and_end(z_level) var/turf/starting = data[1] var/turf/ending = data[2] - var/datum/leyline_variable/attunement_theme/theme - if(length(themes)) - theme = themes[1] + var/form = pick(GLOB.all_forms) current_beam = starting.Beam( ending, @@ -168,7 +152,7 @@ GLOBAL_LIST_EMPTY_TYPED(all_leylines, /datum/mana_pool/leyline) time = INFINITY, beam_type = /obj/effect/ebeam/leyline, max_distance = world.maxx, - beam_color = theme?.beam_color, + beam_color = GLOB.form_colors[form], beam_layer = UPPER_LEYLINE_LAYER, beam_plane = LEYLINE_PLANE, invisibility = INVISIBILITY_LEYLINES, diff --git a/code/datums/mana/leylines/leyline_attunement.dm b/code/datums/mana/leylines/leyline_attunement.dm deleted file mode 100644 index aba0151732d..00000000000 --- a/code/datums/mana/leylines/leyline_attunement.dm +++ /dev/null @@ -1,35 +0,0 @@ -// works differently than others - uses prob(), so treat this stuff as percents -GLOBAL_LIST_INIT(leyline_attunement_themes, list( - /datum/leyline_variable/attunement_theme/fire_minor = 0.5, -)) - -/proc/get_random_attunement_themes() - RETURN_TYPE(/list/datum/leyline_variable/attunement_theme) - - var/list/datum/leyline_variable/attunement_theme/themes = list() - - for (var/datum/leyline_variable/attunement_theme/theme as anything in GLOB.leyline_attunement_themes) - if (prob(GLOB.leyline_attunement_themes[theme])) - themes += new theme - - return themes - -/// For each entry in GLOB.[leyline_attunement_themes], prob() is ran, and if it succeeds, it adjusts the leyline attunement by [adjust_attunements] -/datum/leyline_variable/attunement_theme - -/datum/leyline_variable/attunement_theme/proc/adjust_attunements(list/datum/attunement/attunements) - return - -/datum/leyline_variable/attunement_theme/fire_minor - name = "Smoldering" - beam_color = COLOR_ORANGE - -/datum/leyline_variable/attunement_theme/fire_minor/adjust_attunements(list/datum/attunement/attunements) - attunements[MAGIC_ELEMENT_FIRE] += 0.2 - -/datum/leyline_variable/attunement_theme/arcyne_minor - name = "Arcyne" - beam_color = COLOR_PALE_PURPLE_GRAY - -/datum/leyline_variable/attunement_theme/arcyne_minor/adjust_attunements(list/datum/attunement/attunements) - attunements[/datum/attunement/arcyne] += 0.2 diff --git a/code/datums/mana/living_mana.dm b/code/datums/mana/living_mana.dm index b5321e81863..61f6733669d 100644 --- a/code/datums/mana/living_mana.dm +++ b/code/datums/mana/living_mana.dm @@ -74,14 +74,6 @@ /mob/living/carbon/get_initial_mana_pool_type() return /datum/mana_pool/mob -/mob/living/carbon/proc/generate_random_attunements(amount = rand(2, 3)) - var/list/attunements = subtypesof(/datum/attunement) - for(var/i = 1 to amount) - var/datum/attunement/picked = pick(attunements) - mana_pool?.adjust_attunement(picked, rand(1, 3) * 0.1) - -/mob/living/carbon/after_manapool_init() - generate_random_attunements() /mob/living/carbon/human/dummy has_initial_mana_pool = FALSE diff --git a/code/datums/mana/mana_pool.dm b/code/datums/mana/mana_pool.dm index ec19e02bf04..9820500cb49 100644 --- a/code/datums/mana/mana_pool.dm +++ b/code/datums/mana/mana_pool.dm @@ -1,23 +1,7 @@ -#define MANA_POOL_REPLACE_ALL_ATTUNEMENTS (1<<2) - -/* DESIGN NOTES -* This exists because mana will eventually have attunemenents and alignments that will incresae their efficiency in being used -* on spells/by people with corresponding attunements/alignments, vice versa for conflicting. -* -*/ - /// An abstract representation of collections of mana, as it's impossible to represent each individual mana unit /datum/mana_pool var/atom/movable/parent = null - // As attunements on mana is actually a tangible thing, and not just a preference, mana attunements should never go below zero. - /// A abstract representation of the attunements of [amount]. This is just an abstraction of the overall bias of all stored mana - in reality, every unit of mana has its own attunement. - var/list/datum/attunement/attunements - ///this is a list of the intensity of negative attunements, basically since we can't go negative - ///this acts as a way for us to get negative effects without modifying all other attunement procs - ///these are ONLY negative - var/list/datum/attunement/negative_attunements - // In vols /// The absolute maximum [amount] we can hold. Under no circumstances should [amount] ever exceed this value. var/maximum_mana_capacity = BASE_MANA_CAPACITY @@ -51,8 +35,6 @@ /// The natural regen rate, detached from transferrals. Mana generated via this comes from nothing. var/ethereal_recharge_rate = 0 - /// If we have an ethereal recharge rate,i ths is the attunement set that will be given to the generated mana. - var/list/datum/attunement/attunements_to_generate = list() /// The mana pool types we will try to discharge excess mana (from exponential decay) into. Uses defines from magic_charge_bitflags.dm. var/discharge_destinations = MANA_ALL_LEYLINES | MANA_ALL_PYLONS @@ -75,6 +57,10 @@ VAR_PRIVATE/is_processing = FALSE + /// Tracks this mana pool owner's technique/form investment, unspent points, and unlocked spells. + /// Lazily created - use get_mastery() rather than referencing this directly. + var/datum/spell_mastery/mastery + /datum/mana_pool/New(atom/parent = null) . = ..() donation_budget_this_tick = max_donation_rate_per_second @@ -85,10 +71,6 @@ START_PROCESSING(SSmagic, src) /datum/mana_pool/Destroy(force) - attunements = null - attunements_to_generate = null - negative_attunements = null - transfer_rates = null transfer_caps = null transferring_to = null @@ -121,6 +103,14 @@ return TRUE return FALSE +/datum/mana_pool/proc/get_mastery() + RETURN_TYPE(/datum/spell_mastery) + if(!mastery) + mastery = new(src) + if(isliving(parent)) + add_verb(parent, list(/mob/living/proc/open_spellbook)) + return mastery + /datum/mana_pool/proc/update_processing_state() var/should_process = needs_processing() if(should_process && !is_processing) @@ -168,50 +158,6 @@ status_tab += "Mana Count: [general_amount_estimate]" -/datum/mana_pool/proc/generate_initial_attunements() - RETURN_TYPE(/list/datum/attunement) - - return GLOB.default_attunements.Copy() - -/datum/mana_pool/proc/set_attunements(datum/patron/incoming_patron) - if(!length(attunements)) - attunements = generate_initial_attunements() - - for(var/datum/attunement/listed as anything in attunements) - var/datum/attunement/created = new listed - if(incoming_patron.type in created.alignments) - attunements[listed] += created.alignments[incoming_patron.type] - -/datum/mana_pool/proc/remove_attunements(datum/patron/incoming_patron) - if(!length(attunements)) - return - - for(var/datum/attunement/listed as anything in attunements) - var/datum/attunement/created = new listed - if(incoming_patron.type in created.alignments) - attunements[listed] -= created.alignments[incoming_patron.type] - -/datum/mana_pool/proc/adjust_attunement(datum/attunement/attunement_type, amount) - if(!length(attunements)) - attunements = generate_initial_attunements() - if(!length(negative_attunements)) - negative_attunements = generate_initial_attunements() - - if(negative_attunements[attunement_type] < 0 && amount > 0) - var/attunement_left = amount + negative_attunements[attunement_type] - if(!attunement_left) - negative_attunements[attunement_type] += amount - return - amount = attunement_left - - var/actual_value = attunements[attunement_type] + amount - attunements[attunement_type] = max(0, attunements[attunement_type] + amount) - - if(actual_value < 0) - negative_attunements[attunement_type] += amount - - - // order of operations is as follows: // 1. we recharge // 2. we transfer mana @@ -221,7 +167,7 @@ donation_budget_this_tick = (max_donation_rate_per_second) if (ethereal_recharge_rate != 0 && (amount < get_safe_softcap())) - adjust_mana(ethereal_recharge_rate, attunements_to_generate) + adjust_mana(ethereal_recharge_rate) if((intrinsic_recharge_sources & MANA_ALL_LEYLINES) && amount < get_safe_softcap()) var/list/leylines = list() for(var/obj/effect/ebeam/beam in range(3, get_turf(parent))) @@ -353,7 +299,7 @@ donation_budget_this_tick -= amount_to_transfer adjust_mana(-adjusted_amount) - return other_pool.adjust_mana(adjusted_amount, attunements) + return other_pool.adjust_mana(adjusted_amount) /datum/mana_pool/proc/start_transfer(datum/mana_pool/target_pool, force_process = FALSE) @@ -397,14 +343,9 @@ /datum/mana_pool/proc/incoming_transfer_end(datum/mana_pool/donator) transferring_from -= donator -// TODO BIG FUCKING WARNING THIS EQUATION DOSENT WORK AT ALL -// Should be fine as long as nothing actually has any attunements -/// The proc used to modify the mana composition of a mana pool. Should modify attunements in proportion to the ratio -/// between the current amount of mana we have and the mana coming in/being removed, as well as the attunements. -/// Mana pools in general will eventually be refactored to be lists of individual mana pieces with unchanging attunements, -/// so this is not permanent. +/// The proc used to modify the mana composition of a mana pool. /// Returns how much of "amount" was used. -/datum/mana_pool/proc/adjust_mana(amount, list/incoming_attunements) +/datum/mana_pool/proc/adjust_mana(amount) if (amount == 0) return amount @@ -437,18 +378,6 @@ return decay_prevention -= string -/// Returns an adjusted amount of "effective" mana, affected by the attunements. -/// Will always return a minimum of zero and a maximum of the total amount of mana we can give multiplied by the mults. -///TODO The math here sucks and is practically always full mana usage now -/datum/mana_pool/proc/get_attuned_amount(list/datum/attunement/incoming_attunements, atom/caster, amount_to_adjust = src.amount) - var/mult = get_overall_attunement_mults(incoming_attunements, caster) - - return clamp(SAFE_DIVIDE(amount_to_adjust, mult), 0, amount) - -/// Returns the combined attunement mults of all entries in the argument. -/datum/mana_pool/proc/get_overall_attunement_mults(list/attunements, atom/caster) - return get_total_attunement_mult(src.attunements, attunements, caster) - /datum/mana_pool/proc/can_transfer(datum/mana_pool/target_pool) SHOULD_BE_PURE(TRUE) @@ -481,15 +410,8 @@ /datum/mana_pool/proc/set_natural_recharge(new_value) ethereal_recharge_rate = new_value - if ((ethereal_recharge_rate > 0) && isnull(attunements_to_generate)) - attunements_to_generate = get_default_attunements_to_generate() update_processing_state() -/datum/mana_pool/proc/get_default_attunements_to_generate() - RETURN_TYPE(/list/datum/attunement) - - return GLOB.default_attunements.Copy() - /datum/mana_pool/proc/set_max_mana(new_max, change_amount = FALSE, change_softcap = TRUE) var/percent = get_percent_to_max() //originally this was a duplicate redefinition- see change_amount var/softcap_increase = new_max - maximum_mana_capacity @@ -557,5 +479,3 @@ parent.visible_message(span_danger("[parent] collapses as they vomit blood from the recoil."), span_danger("I feel my organs being ripped apart!")) parent:vomit(1, blood = TRUE, stun = FALSE) parent:apply_damage(backlash_intensity, BRUTE, BODY_ZONE_CHEST) - -#undef MANA_POOL_REPLACE_ALL_ATTUNEMENTS diff --git a/code/datums/mana/mastery/_base.dm b/code/datums/mana/mastery/_base.dm new file mode 100644 index 00000000000..d4fd1418531 --- /dev/null +++ b/code/datums/mana/mastery/_base.dm @@ -0,0 +1,538 @@ +/datum/spell_mastery + /// The mana pool that owns us. + var/datum/mana_pool/owner + ///optionally the atom that this is attached to + var/atom/movable/parent + /// assoc list of technique define (string) -> points invested (your mastery level in it) + var/list/technique_levels = list() + /// assoc list of form define (string) -> points invested (your mastery level in it) + var/list/form_levels = list() + + /// Free/unspent level points, spendable ONLY to level up Form tracks + var/unspent_form_points = 0 + /// Free/unspent level points, spendable ONLY to level up Technique tracks + var/unspent_technique_points = 0 + + ///our initial form points + var/initial_form_points = 0 + /// our initial technique points + var/initial_technique_points = 0 + + /// Dynamic pools: track define (string) -> spendable points left to buy actual spells + var/list/spendable_form_points = list() + var/list/spendable_technique_points = list() + + /// Lifetime totals, purely informational (character sheet / debug purposes). + var/total_form_points_earned = 0 + var/total_technique_points_earned = 0 + + /// The last level we synced points against - lets sync_points_to_level() be idempotent. + var/last_synced_level = 0 + + /// List of spell typepaths that have been unlocked/learned innately. + var/list/unlocked_spells = list() + /// assoc list of spell typepath -> the actual granted action instance, so we can + /// cleanly remove it again if the spell is unlearned. + var/list/granted_actions = list() + + /// assoc list of spellbook item -> (assoc list of spell typepath -> granted action instance). + /// Only contains entries for books CURRENTLY held/granted. + var/list/spellbook_granted_actions = list() + /// assoc list of spellbook item -> (assoc list of spell typepath -> charges remaining today). + /// Persists across equip/drop - a book keeps its charge count between holds. + var/list/spellbook_charges = list() + ///who use to hold us + var/atom/movable/holder + +/datum/spell_mastery/New(datum/mana_pool/owner, atom/contained_source) + src.owner = owner + if(contained_source) + register_parent(contained_source) + +/datum/spell_mastery/Destroy(force) + remove_spells() + unregister_parent(parent) + + owner = null + technique_levels = null + form_levels = null + unlocked_spells = null + granted_actions = null + spellbook_granted_actions = null + spellbook_charges = null + return ..() + +/datum/spell_mastery/proc/adjust_technique_points(amount) + initial_technique_points += amount + unspent_technique_points += amount + +/datum/spell_mastery/proc/adjust_form_points(amount) + initial_form_points += amount + unspent_form_points += amount + +/datum/spell_mastery/proc/register_parent(atom/movable/new_parent) + if(parent) + unregister_parent(parent) + parent = new_parent + RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(remove_spells)) + RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(add_spells)) + RegisterSignal(parent, COMSIG_QDELETING, PROC_REF(on_deletion)) + RegisterSignal(parent, COMSIG_ITEM_ATTACK_SELF, PROC_REF(open_menu)) + RegisterSignal(parent, COMSIG_MASTERY_ADD_SPELLS, PROC_REF(add_spells)) + RegisterSignal(parent, COMSIG_MASTERY_REMOVE_SPELLS, PROC_REF(remove_spells)) + +/datum/spell_mastery/proc/unregister_parent(atom/movable/old_parent) + parent = null + UnregisterSignal(old_parent, list(COMSIG_ITEM_ATTACK_SELF, COMSIG_ITEM_DROPPED, COMSIG_ITEM_EQUIPPED, COMSIG_QDELETING,COMSIG_MASTERY_REMOVE_SPELLS, COMSIG_MASTERY_ADD_SPELLS)) + +/datum/spell_mastery/proc/open_menu(datum/source, mob/living/user) + var/datum/spellbook/book = new(parent, src) + book.ui_interact(user) + +/** + * Grants a spell directly, bypassing the normal spendable-point cost check. + * Use this for innate/bonus spells (quest rewards, traits, etc.) rather than + * mutating unlocked_spells/granted_actions directly. + */ +/datum/spell_mastery/proc/grant_bonus_spell(datum/action/cooldown/spell/spell, mob/living/user) + if(!istype(spell) || !user) + return FALSE + + var/spell_path = spell.type + if(spell_path in unlocked_spells) + return FALSE + + unlocked_spells += spell_path + granted_actions[spell_path] = spell + + // Since this wasn't bought via try_learn_spell(), bump the lifetime totals + // so recalculate_unspent_points() doesn't dock the player for a spell + // they never paid points for. + var/req_form = spell.required_form + var/req_technique = spell.required_technique + + if(req_form) + form_levels[req_form] = get_form_level(req_form) + 1 + initial_form_points += 1 + if(req_technique) + technique_levels[req_technique] = get_technique_level(req_technique) + 1 + initial_technique_points += 1 + + recalculate_unspent_points() + return TRUE + +/** + * Instantiates the singleton spell action and grants it to the user. + * Triggered when the parent item is equipped. + */ +/datum/spell_mastery/proc/add_spells(datum/source, mob/living/user) + holder = user + if(HAS_TRAIT(holder, TRAIT_SORCERER)) + return + if(parent && (SEND_SIGNAL(parent, COMSIG_MASTERY_CHECK_PARENT) == COMPONENT_MASTERY_CANCEL)) + return + var/atom/movable/AM = parent + if(!AM || !istype(user)) + return + + // Avoid double-granting if already active + if(spellbook_granted_actions[AM]) + return + + var/list/charges = spellbook_charges[AM] + if(!charges) + charges = list() + spellbook_charges[AM] = charges + + var/list/actions = list() + for(var/spell_path in unlocked_spells) + if(isnull(charges[spell_path])) + charges[spell_path] = initial(spell_path:initial_charges) || 0 + + // Fetch our persistent singleton spell instance + var/datum/action/cooldown/spell/existing_spell = granted_actions[spell_path] + if(!existing_spell) + existing_spell = new spell_path() + RegisterSignal(existing_spell, COMSIG_SPELL_CAST, PROC_REF(pass_spell_cast)) + granted_actions[spell_path] = existing_spell + + existing_spell.mastery_source = src + existing_spell.charge_item = AM + existing_spell.uses_spellbook_charges = TRUE + + // Grant the persistent instance to the current user + existing_spell.Grant(user) + actions[spell_path] = existing_spell + + spellbook_granted_actions[AM] = actions + +/** + * Removes the active spell instance when the parent item is dropped or unequipped. + * Note: We DO NOT destroy (qdel) the spell here anymore, ensuring cooldowns persist. + */ +/datum/spell_mastery/proc/remove_spells(datum/source, mob/living/user) + if(!user) + user = holder + holder = null + var/atom/movable/AM = parent + var/list/actions = spellbook_granted_actions[AM] + if(!actions) + return + + for(var/spell_path in actions) + var/datum/action/cooldown/spell/granted = actions[spell_path] + if(granted && user) + granted.Remove(user) + + spellbook_granted_actions -= AM + +/datum/spell_mastery/proc/on_deletion() + remove_spells() + unregister_parent(parent) + +/datum/spell_mastery/proc/get_technique_level(technique) + if(!technique) + return 0 + return technique_levels[technique] || 0 + +/datum/spell_mastery/proc/get_form_level(form) + if(!form) + return 0 + return form_levels[form] || 0 + +/datum/spell_mastery/proc/get_technique_rank_name(technique) + return get_rank_name_for_level(get_technique_level(technique)) + +/datum/spell_mastery/proc/get_form_rank_name(form) + return get_rank_name_for_level(get_form_level(form)) + +/datum/spell_mastery/proc/get_rank_name_for_level(level) + var/best_name = "Untrained" + var/best_threshold = -1 + for(var/threshold_text in GLOB.mastery_rank_names) + var/threshold = text2num(threshold_text) + if(level >= threshold && threshold > best_threshold) + best_threshold = threshold + best_name = GLOB.mastery_rank_names[threshold_text] + return best_name + +/** + * Dynamically computes unspent pools by taking total lifetime points (initial_*) + * and subtracting the points invested into tracks. + * Additionally recalculates the spendable points for each track based on + * track level minus currently known spells. + */ +/datum/spell_mastery/proc/recalculate_unspent_points() + total_form_points_earned = initial_form_points + total_technique_points_earned = initial_technique_points + + var/spent_on_forms = 0 + var/spent_on_techniques = 0 + + spendable_form_points.Cut() + spendable_technique_points.Cut() + + for(var/form in form_levels) + var/lvl = form_levels[form] + spent_on_forms += lvl + spendable_form_points[form] = lvl + + for(var/technique in technique_levels) + var/lvl = technique_levels[technique] + spent_on_techniques += lvl + spendable_technique_points[technique] = lvl + + for(var/spell_path in unlocked_spells) + var/req_form = initial(spell_path:required_form) + if(req_form) + spendable_form_points[req_form] -= 1 + + var/req_technique = initial(spell_path:required_technique) + if(req_technique) + spendable_technique_points[req_technique] -= 1 + + unspent_form_points = max(0, total_form_points_earned - spent_on_forms) + unspent_technique_points = max(0, total_technique_points_earned - spent_on_techniques) + +/datum/spell_mastery/proc/invest_technique(technique, amount = 1) + if(!technique || amount <= 0) + return FALSE + if(amount > unspent_technique_points) + return FALSE + + unspent_technique_points -= amount + technique_levels[technique] = get_technique_level(technique) + amount + + // Gain 1 point specifically for buying spells under this technique track + spendable_technique_points[technique] = (spendable_technique_points[technique] || 0) + amount + return TRUE + +/datum/spell_mastery/proc/invest_form(form, amount = 1) + if(!form || amount <= 0) + return FALSE + if(amount > unspent_form_points) + return FALSE + + unspent_form_points -= amount + form_levels[form] = get_form_level(form) + amount + + // Gain 1 point specifically for buying spells under this form track + spendable_form_points[form] = (spendable_form_points[form] || 0) + amount + return TRUE + +/// Returns the {form points, technique points} cost of learning spell_path, as a 2-item list. +/datum/spell_mastery/proc/get_spell_cost(spell_path) + var/form_cost = initial(spell_path:required_form) ? 1 : 0 + var/technique_cost = initial(spell_path:required_technique) ? 1 : 0 + return list(form_cost, technique_cost) + +/** + * Checks whether spell_path is currently purchasable (NOT whether it's already known - + * check unlocked_spells / can_unlearn_spell for that). Reads spell vars via initial() + * so we never have to instance the spell just to inspect its requirements. + */ +/datum/spell_mastery/proc/can_learn_spell(spell_path) + if(!ispath(spell_path, /datum/action/cooldown/spell)) + return FALSE + if(spell_path in unlocked_spells) + return FALSE + + var/technique = initial(spell_path:required_technique) + var/form = initial(spell_path:required_form) + var/level_req = initial(spell_path:required_level) + + // Check if you have enough specific spell currency remaining for this element + if(form && (spendable_form_points[form] || 0) < 1) + return FALSE + if(technique && (spendable_technique_points[technique] || 0) < 1) + return FALSE + + // Rank check: Your mastery level in the track must meet the spell's required level + if(level_req > 0) + if(technique && get_technique_level(technique) < level_req) + return FALSE + if(form && get_form_level(form) < level_req) + return FALSE + + var/list/prereqs = initial(spell_path:prerequisite_spells) + if(length(prereqs)) + for(var/prereq_path in prereqs) + if(!(prereq_path in unlocked_spells)) + return FALSE + + return TRUE + +/datum/spell_mastery/proc/try_learn_spell(spell_path) + if(!can_learn_spell(spell_path)) + return FALSE + + var/technique = initial(spell_path:required_technique) + var/form = initial(spell_path:required_form) + + if(form) + spendable_form_points[form] -= 1 + if(technique) + spendable_technique_points[technique] -= 1 + + unlocked_spells += spell_path + + if(!parent || spellbook_granted_actions[parent]) + var/atom/movable/AM = parent + var/list/actions = parent ? spellbook_granted_actions[AM] : granted_actions + var/list/charges = parent ? spellbook_charges[AM] : null + + if(charges && isnull(charges[spell_path])) + charges[spell_path] = initial(spell_path:initial_charges) || 0 + + var/datum/action/cooldown/spell/new_spell = new spell_path() + RegisterSignal(new_spell, COMSIG_SPELL_CAST, PROC_REF(pass_spell_cast)) + if(owner) + new_spell.Grant(owner.parent) + if(parent) + new_spell.mastery_source = src + new_spell.charge_item = parent + new_spell.uses_spellbook_charges = TRUE + actions[spell_path] = new_spell + + if(holder) + var/atom/cached_holder = holder + if(parent && (SEND_SIGNAL(parent, COMSIG_MASTERY_CHECK_PARENT) == COMPONENT_MASTERY_CANCEL)) + remove_spells(src, cached_holder) + return + remove_spells(src, cached_holder) + add_spells(src, cached_holder) + return TRUE + +/datum/spell_mastery/proc/try_unlearn_spell(spell_path) + if(!can_unlearn_spell(spell_path)) + return FALSE + + var/technique = initial(spell_path:required_technique) + var/form = initial(spell_path:required_form) + + if(form) + spendable_form_points[form] += 1 + if(technique) + spendable_technique_points[technique] += 1 + + unlocked_spells -= spell_path + + var/atom/movable/AM = parent + var/list/actions = AM ? spellbook_granted_actions[AM] : granted_actions + + if(actions && actions[spell_path]) + var/datum/action/cooldown/spell/granted = actions[spell_path] + if(granted) + UnregisterSignal(granted, COMSIG_SPELL_CAST) + granted.Remove(owner?.parent) + qdel(granted) + actions -= spell_path + + if(AM && spellbook_charges[AM]) + var/list/charges = spellbook_charges[AM] + charges -= spell_path + + if(holder) + var/atom/cached_holder = holder + if(parent && (SEND_SIGNAL(parent, COMSIG_MASTERY_CHECK_PARENT) == COMPONENT_MASTERY_CANCEL)) + remove_spells(src, cached_holder) + return + remove_spells(src, cached_holder) + add_spells(src, cached_holder) + + return TRUE + +/// Whether spell_path is currently known and can be unlearned/refunded. +/datum/spell_mastery/proc/can_unlearn_spell(spell_path) + return (spell_path in unlocked_spells) + +/// Checks remaining charges for a spell on the currently attached item. +/datum/spell_mastery/proc/has_spellbook_charges(spell_path) + var/list/charges = spellbook_charges[parent] + if(!charges) + return TRUE + return (charges[spell_path] || 0) > 0 + +/// Consumes a charge from the attached item's tracking list. +/datum/spell_mastery/proc/consume_spellbook_charge(spell_path) + var/list/charges = spellbook_charges[parent] + if(!charges) + return TRUE + if((charges[spell_path] || 0) <= 0) + return FALSE + charges[spell_path]-- + return TRUE + +/// Resets an item's charge list back to the spell's initial baseline. +/datum/spell_mastery/proc/reset_spellbook_charges(atom/movable/target_item) + var/atom/movable/AM = target_item || parent + if(!AM) + return + + var/list/charges = spellbook_charges[AM] + if(!charges) + return + + for(var/spell_path in unlocked_spells) + charges[spell_path] = initial(spell_path:initial_charges) || 0 + + var/list/actions = spellbook_granted_actions[AM] + if(actions) + for(var/spell_path in actions) + var/datum/action/cooldown/spell/granted = actions[spell_path] + granted?.build_all_button_icons(UPDATE_BUTTON_STATUS) + +/datum/spell_mastery/proc/reset_all_spellbook_charges() + for(var/atom/movable/AM in spellbook_charges) + reset_spellbook_charges(AM) + +/datum/spell_mastery/proc/sync_points_to_level(new_level) + last_synced_level = new_level + recalculate_unspent_points() + +/** + * adjusts the amount of available form mastery points + * + * Args + * * points - amount of points to grant or reduce + * * used_points - adjust used/spendable points instead of the lifetime pool + * * specific_form - if provided, adjusts this form's level directly instead + * of the general unspent form pool +*/ +/datum/spell_mastery/proc/adjust_form_mastery_points(points, used_points = FALSE, specific_form = null) + if(QDELETED(src)) + return + + if(specific_form) + if(used_points) + // consume from that form's spendable (spell-buying) pool specifically + spendable_form_points[specific_form] = max(0, (spendable_form_points[specific_form] || 0) - points) + else + // actually rank up (or down) that specific form + form_levels[specific_form] = max(0, get_form_level(specific_form) + points) + initial_form_points += points + else + if(used_points) + unspent_form_points = max(0, unspent_form_points - points) + else + adjust_form_points(points) + + recalculate_unspent_points() + +/** + * adjusts the amount of available technique mastery points + * + * Args + * * points - amount of points to grant or reduce + * * used_points - adjust used/spendable points instead of the lifetime pool + * * specific_technique - if provided, adjusts this technique's level directly + * instead of the general unspent technique pool +*/ +/datum/spell_mastery/proc/adjust_technique_mastery_points(points, used_points = FALSE, specific_technique = null) + if(QDELETED(src)) + return + + if(specific_technique) + if(used_points) + spendable_technique_points[specific_technique] = max(0, (spendable_technique_points[specific_technique] || 0) - points) + else + technique_levels[specific_technique] = max(0, get_technique_level(specific_technique) + points) + initial_technique_points += points + else + if(used_points) + unspent_technique_points = max(0, unspent_technique_points - points) + else + adjust_technique_points(points) + + recalculate_unspent_points() + +/// Reset form mastery points and used form points +/datum/spell_mastery/proc/reset_form_mastery_points(silent = TRUE) + if(QDELETED(src)) + return + + initial_form_points = 0 + unspent_form_points = 0 + + if(!silent && holder) + to_chat(holder, span_boldwarning("I lost all my form mastery points!")) + + recalculate_unspent_points() + +/// Reset technique mastery points and used technique points +/datum/spell_mastery/proc/reset_technique_mastery_points(silent = TRUE) + if(QDELETED(src)) + return + + initial_technique_points = 0 + unspent_technique_points = 0 + + if(!silent && holder) + to_chat(holder, span_boldwarning("I lost all my technique mastery points!")) + + recalculate_unspent_points() + +/datum/spell_mastery/proc/pass_spell_cast(datum/action/cooldown/spell/spell, atom/cast_on) + if(!parent) + return + SEND_SIGNAL(parent, COMSIG_MASTERY_CAST, spell, cast_on) diff --git a/code/datums/mana/mastery/_ui.dm b/code/datums/mana/mastery/_ui.dm new file mode 100644 index 00000000000..5467886a010 --- /dev/null +++ b/code/datums/mana/mastery/_ui.dm @@ -0,0 +1,156 @@ +/datum/spellbook + var/mob/living/owner + var/datum/spell_mastery/mastery + var/unlearn_mode = FALSE + +/datum/spellbook/New(mob/living/owner, datum/spell_mastery/_mastery) + src.owner = owner + if(owner.mana_pool) + mastery = owner.mana_pool?.get_mastery() + if(_mastery) + mastery = _mastery + +/datum/spellbook/Destroy(force) + owner = null + mastery = null + return ..() + +/datum/spellbook/ui_state(mob/user) + return GLOB.always_state + +/datum/spellbook/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "SpellBook") + ui.open() + +/datum/spellbook/ui_static_data(mob/user) + var/list/data = list() + data["techniques"] = GLOB.all_techniques + data["forms"] = GLOB.all_forms + return data + +/datum/spellbook/ui_data(mob/user) + var/list/data = list() + + if(!mastery) + return data + + data["unspentFormPoints"] = mastery.unspent_form_points + data["unspentTechniquePoints"] = mastery.unspent_technique_points + data["unlearnMode"] = unlearn_mode + + var/list/form_modifiers = get_modifiers_by_form() + var/list/technique_modifiers = get_modifiers_by_technique() + + var/list/technique_data = list() + for(var/technique in GLOB.all_techniques) + technique_data += list(list( + "id" = technique, + "name" = technique, + "level" = mastery.get_technique_level(technique), + "rank" = mastery.get_technique_rank_name(technique), + "modifiers" = technique_modifiers[technique], + )) + data["techniqueLevels"] = technique_data + + var/list/form_data = list() + for(var/form in GLOB.all_forms) + form_data += list(list( + "id" = form, + "name" = form, + "level" = mastery.get_form_level(form), + "rank" = mastery.get_form_rank_name(form), + "modifiers" = form_modifiers[form], + )) + data["formLevels"] = form_data + + + var/list/spell_data = list() + for(var/datum/action/cooldown/spell/spell_path as anything in subtypesof(/datum/action/cooldown/spell)) + if(IS_ABSTRACT(spell_path)) + continue + if(!initial(spell_path.learnable)) + continue + var/technique = initial(spell_path:required_technique) + var/form = initial(spell_path:required_form) + if(!form) + continue + var/is_unlocked = (spell_path in mastery.unlocked_spells) + + spell_data += list(list( + "path" = "[spell_path]", + "name" = initial(spell_path.name), + "desc" = initial(spell_path.desc), + "technique" = technique, + "form" = form, + "level" = initial(spell_path.required_level), + "formCost" = form ? 1 : 0, + "techniqueCost" = technique ? 1 : 0, + "unlocked" = is_unlocked, + "canLearn" = mastery.can_learn_spell(spell_path), + "canUnlearn" = is_unlocked, // any unlocked spell can be unlearned + "icon" = "[initial(spell_path.button_icon)]", + "iconState" = initial(spell_path.button_icon_state), + )) + data["spells"] = spell_data + return data + +/datum/spellbook/ui_act(action, list/params) + . = ..() + if(.) + return + if(!mastery) + return + + switch(action) + if("invest_technique") + var/technique = params["technique"] + if(!(technique in GLOB.all_techniques)) + return + . = mastery.invest_technique(technique, 1) + + if("invest_form") + var/form = params["form"] + if(!(form in GLOB.all_forms)) + return + . = mastery.invest_form(form, 1) + + if("learn_spell") + var/spell_path = text2path(params["path"]) + . = mastery.try_learn_spell(spell_path) + + if("unlearn_spell") + var/spell_path = text2path(params["path"]) + . = mastery.try_unlearn_spell(spell_path) + +/datum/spellbook/proc/get_modifiers_by_form() + return get_modifiers_by_key("form") + +/datum/spellbook/proc/get_modifiers_by_technique() + return get_modifiers_by_key("technique") + +/datum/spellbook/proc/get_modifiers_by_key(key) + if(!mastery?.parent) + return list() + var/list/modifiers = list() + SEND_SIGNAL(mastery.parent, COMSIG_SPELL_REQUEST_MODIFIERS, modifiers) + + var/list/by_key = list() + for(var/list/entry in modifiers) + var/id = entry[key] + if(!id) + continue + if(!by_key[id]) + by_key[id] = list("cost" = 1, "castSpeed" = 1, "magnitude" = 0) + by_key[id]["cost"] *= entry["cost"] + by_key[id]["castSpeed"] *= entry["castSpeed"] + by_key[id]["magnitude"] += entry["magnitude"] + return by_key + +/mob/living/proc/open_spellbook() + set name = "Open Innate Spells" + set category = "Magic" + + var/datum/spellbook/book = new(src) + book.ui_interact(src) diff --git a/code/datums/migrants/waves/magic_school_expedition.dm b/code/datums/migrants/waves/magic_school_expedition.dm index 457279cd3fc..efd24f2a112 100644 --- a/code/datums/migrants/waves/magic_school_expedition.dm +++ b/code/datums/migrants/waves/magic_school_expedition.dm @@ -33,7 +33,7 @@ attribute_sheet = /datum/attribute_holder/sheet/job/migrant/magic_teacher spells = list(/datum/action/cooldown/spell/undirected/touch/prestidigitation) - spell_points = 10 + form_points = 7 cmode_music = 'sound/music/cmode/adventurer/CombatSorcerer.ogg' voicepack_m = /datum/voicepack/male/wizard @@ -55,7 +55,7 @@ shoes = /obj/item/clothing/shoes/shortboots neck = /obj/item/clothing/neck/mana_star backpack_contents = list( - /obj/item/book/granter/spellbook/expert = 1, + /obj/item/spellbook/expert = 1, /obj/item/storage/belt/pouch/coins/rich = 1, ) @@ -95,7 +95,7 @@ attribute_sheet = /datum/attribute_holder/sheet/job/migrant/magic_student spells = list(/datum/action/cooldown/spell/undirected/touch/prestidigitation) - spell_points = 6 + form_points = 3 /datum/job/migrant/magic_student/after_spawn(mob/living/carbon/human/spawned, client/player_client) . = ..() @@ -103,11 +103,24 @@ if(prob(5)) spawned.cmode_music = 'sound/music/cmode/antag/combat_evilwizard.ogg' + var/list/selectable_books = list( + "Blazing Tome (Fire)" = /obj/item/spellbook/adept/starter/fire, + "Frostbound Tome (Ice)" = /obj/item/spellbook/adept/starter/ice, + "Storm-Charged Tome (Lightning)" = /obj/item/spellbook/adept/starter/lightning, + "Stoneveined Tome (Earth)" = /obj/item/spellbook/adept/starter/earth, + "Thrice-Warded Tome (Arcane)" = /obj/item/spellbook/adept/starter/arcane, + "Grave-Touched Tome (Death)" = /obj/item/spellbook/adept/starter/death, + "Verdant Tome (Life)" = /obj/item/spellbook/adept/starter/life, + "Windswept Tome (Air)" = /obj/item/spellbook/adept/starter/air, + "Tidebound Tome (Water)" = /obj/item/spellbook/adept/starter/water, + ) + + INVOKE_ASYNC(src, PROC_REF(grant_selected_spellbooks), spawned, selectable_books, 1) + /datum/outfit/magic_student name = "Magic School Student (Migrant Wave)" neck = /obj/item/clothing/neck/mana_star belt = /obj/item/storage/belt/leather - beltl = /obj/item/book/granter/spellbook/adept beltr = /obj/item/storage/magebag/apprentice armor = /obj/item/clothing/shirt/robe/newmage/adept backr = /obj/item/storage/backpack/satchel diff --git a/code/datums/mind.dm b/code/datums/mind.dm index 9e281c73368..05d36d91b6d 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -127,6 +127,8 @@ GLOBAL_LIST_EMPTY(personal_objective_minds) var/has_studied = FALSE /// Variable that lets the event picker see if someones getting chosen or not var/picking = FALSE + /// Whether this mind has arcyne momentum (persists through death) + var/has_arcyne_momentum = FALSE /datum/mind/New(key) src.key = key diff --git a/code/datums/rituals/runes/arcane/attunement.dm b/code/datums/rituals/runes/arcane/attunement.dm deleted file mode 100644 index cea57b357a5..00000000000 --- a/code/datums/rituals/runes/arcane/attunement.dm +++ /dev/null @@ -1,46 +0,0 @@ - -/obj/effect/decal/cleanable/ritual_rune/arcyne/attunement - name = "arcyne attunement matrix" - desc = "A large matrix designed to imbue the energies of materials." - icon = 'icons/effects/160x160.dmi' - icon_state = "imbuement" - tier = 2 - req_invokers = 2 - invocation = "Xel'thix un'oral!" - req_keyword = TRUE - runesize = 2 - SET_BASE_PIXEL(-64, -64) - pixel_z = 0 - can_be_scribed = TRUE - associated_ritual = /datum/runerituals/attunement - takes_all_items = TRUE - -/obj/effect/decal/cleanable/ritual_rune/arcyne/attunement/invoke(list/invokers, datum/runerituals/runeritual) - runeritual = associated_ritual - if(!..()) - return - var/mob/living/user = invokers[1] - var/datum/runerituals/attunement/attune = pickritual - for(var/datum/attunement/att as anything in attune.attunement_modifiers) - user.mana_pool?.adjust_attunement(att, attune.attunement_modifiers[att]) - pickritual.cleanup_atoms(selected_atoms) - do_invoke_glow() - -/datum/runerituals/attunement - name = "arcyne attunement" - blacklisted = FALSE - required_atoms = list( - /obj/item/reagent_containers/food/snacks/produce/manabloom = 1, - /obj/item/natural/melded/t1 = 1 - ) - /// Assoc list of [/datum/attunement] = modifier value, accumulated from attuned items - var/list/attunement_modifiers = list() - -/datum/runerituals/attunement/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) - for(var/obj/item/item in selected_atoms) - if(!length(item.attunement_values)) - continue - for(var/datum/attunement/att in item.attunement_values) - attunement_modifiers |= att - attunement_modifiers[att] += item.attunement_values[att] - return TRUE diff --git a/code/datums/rituals/runes/arcane/recharging.dm b/code/datums/rituals/runes/arcane/recharging.dm new file mode 100644 index 00000000000..a0e33a7b8ae --- /dev/null +++ b/code/datums/rituals/runes/arcane/recharging.dm @@ -0,0 +1,149 @@ +/obj/effect/decal/cleanable/ritual_rune/arcyne/recharge + name = "arcyne recharging matrix" + desc = "A tight, coiling sigil that hungers to feed dormant pages." + icon = 'icons/effects/160x160.dmi' + icon_state = "portal" + tier = 1 + runesize = 1 + SET_BASE_PIXEL(-64, -64) + pixel_z = 0 + invocation = "Veth'kael sar'un renai!" + can_be_scribed = TRUE + color = "#2F9BBE" + + /// Mana cost to invok + var/mana_cost = 70 + /// Minimum arcane skill required to invoke this rune at all + var/required_skill = SKILL_LEVEL_NONE + /// The spellbook currently staged on the rune, if any + var/obj/item/staged_book = null + /// TRUE while the animation is playing; blocks all interaction + var/animating = FALSE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/recharge/attack_hand(mob/living/user) + if(animating) + to_chat(user, span_notice("The rune is already working...")) + return + + if(!user.get_active_held_item()) + if(staged_book) + try_invoke(user) + else + . = ..() + return + +/obj/effect/decal/cleanable/ritual_rune/arcyne/recharge/attack_hand_secondary(mob/living/user, list/modifiers) + if(animating) + to_chat(user, span_notice("The rune is already working...")) + return + if(!staged_book) + return ..() + abort_ritual() + to_chat(user, span_cultsmall("The book clatters free from the rune.")) + playsound(src, 'sound/magic/glass.ogg', 40, TRUE) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/recharge/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(user.cmode) + return NONE + + if(tool.item_flags & ABSTRACT || HAS_TRAIT(tool, TRAIT_NODROP)) + return NONE + + if(animating) + to_chat(user, span_notice("The rune is already working...")) + return ITEM_INTERACT_BLOCKING + + if(!istype(tool, /obj/item/spellbook)) + to_chat(user, span_hierophant_warning("Only a spellbook can be placed here.")) + return ITEM_INTERACT_BLOCKING + + if(staged_book) + to_chat(user, span_notice("There's already a book placed on the rune.")) + return ITEM_INTERACT_BLOCKING + + user.temporarilyRemoveItemFromInventory(tool) + tool.forceMove(get_turf(src)) + tool.anchored = TRUE + staged_book = tool + animate(tool, pixel_x = 0, pixel_y = 0, time = 0.7 SECONDS, flags = ANIMATION_END_NOW) + + to_chat(user, span_cultsmall("The book settles onto the rune, spine humming faintly...")) + playsound(src, 'sound/magic/glass.ogg', 60, TRUE) + return ITEM_INTERACT_SUCCESS + +/obj/effect/decal/cleanable/ritual_rune/arcyne/recharge/proc/try_invoke(mob/living/user) + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) <= SKILL_LEVEL_NONE) + to_chat(user, span_warning("You aren't able to invoke these symbols.")) + return + if(rune_in_use) + to_chat(user, span_notice("The rune is already active.")) + return + rune_in_use = TRUE + animating = TRUE + + var/skill_level = GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) + if(skill_level < required_skill) + to_chat(user, span_hierophant_warning("My arcyne is not refined enough to complete this working...")) + abort_ritual() + return + if(user.mana_pool.amount < mana_cost) + to_chat(user, span_hierophant_warning("My mana is lacking...")) + abort_ritual() + return + user.mana_pool.adjust_mana(-mana_cost) + + user.say(invocation, language = /datum/language/common, ignore_spam = TRUE, forced = "cult invocation") + playsound(src, 'sound/magic/cosmic_expansion.ogg', 60, TRUE) + SpinAnimation( + speed = 1.5 SECONDS, + loops = 10, + clockwise = TRUE, + segments = 6, + parallel = TRUE + ) + + INVOKE_ASYNC(src, PROC_REF(run_recharge_animation), user) + +/obj/effect/decal/cleanable/ritual_rune/arcyne/recharge/proc/run_recharge_animation(mob/living/user) + var/obj/item/spellbook/book = staged_book + + // pulse the book brighter as it charges + for(var/i = 1 to 10) + animate(book, transform = matrix() * 1.15, time = 0.8 SECONDS, flags = ANIMATION_END_NOW) + sleep(0.8 SECONDS) + animate(book, transform = matrix(), time = 0.6 SECONDS, flags = ANIMATION_END_NOW) + sleep(0.6 SECONDS) + + playsound(src, 'sound/magic/blink.ogg', 80, TRUE) + + var/datum/spell_mastery/mastery = book.mastery + if(mastery) + mastery.reset_spellbook_charges(book) + to_chat(user, span_hierophant_warning("The book's pages flare, charges restored.")) + else + to_chat(user, span_warning("...nothing happens. The book seems inert.")) + + book.anchored = FALSE + staged_book = null + + if(user.put_in_hands(book)) + to_chat(user, span_cultsmall("The book leaps back into my grip.")) + + animating = FALSE + rune_in_use = FALSE + + user.mind.add_sleep_experience(/datum/attribute/skill/magic/arcane, (GET_MOB_ATTRIBUTE_VALUE(user, STAT_INTELLIGENCE) * 2) + required_skill, FALSE) + + do_invoke_glow() + +/obj/effect/decal/cleanable/ritual_rune/arcyne/recharge/proc/abort_ritual() + if(staged_book && !QDELETED(staged_book)) + staged_book.anchored = FALSE + animate(staged_book, pixel_x = 0, pixel_y = 0, time = 0.5 SECONDS, flags = ANIMATION_END_NOW) + staged_book = null + animating = FALSE + rune_in_use = FALSE + +/obj/effect/decal/cleanable/ritual_rune/arcyne/recharge/Destroy() + abort_ritual() + return ..() diff --git a/code/datums/status_effects/arcane_momentum.dm b/code/datums/status_effects/arcane_momentum.dm new file mode 100644 index 00000000000..87dd1d6a02a --- /dev/null +++ b/code/datums/status_effects/arcane_momentum.dm @@ -0,0 +1,208 @@ +#define MOMENTUM_FILTER "momentum_glow" +#define MOMENTUM_DECAY_DELAY (8 SECONDS) +#define SECOND_PER_MOMENTUM (6 SECONDS) // Time between each stack lost during decay. + +/atom/movable/screen/alert/status_effect/buff/arcyne_momentum + name = "Arcyne Momentum (0)" + desc = "Melee strikes fuel arcyne power. Build momentum to unleash your power. Melee strikes grant 1 stack every 2 seconds. Certain abilities capable of striking multiple targets grant bonus momentum. Take care not to lose control." + icon_state = "buff" + +/datum/status_effect/buff/arcyne_momentum + id = "arcyne_momentum" + alert_type = /atom/movable/screen/alert/status_effect/buff/arcyne_momentum + duration = -1 + tick_interval = 20 + status_type = STATUS_EFFECT_UNIQUE + var/stacks = 0 + var/max_stacks = 10 + var/glow_colour = "#4a90d9" + var/crackle_colour = "#7b5ea7" + var/overcharge_threshold = 7 + var/overcharge_damage = 4 + var/is_overcharged = FALSE + var/last_stack_time = 0 + var/last_decay_time = 0 + var/static/mutable_appearance/electricity_overlay + var/obj/item/bound_weapon + var/chant + var/last_melee_gain = 0 + var/melee_gain_cooldown = 2 SECONDS + +/datum/status_effect/buff/arcyne_momentum/on_apply() + . = ..() + RegisterSignal(owner, COMSIG_LIVING_STATUS_STUN, PROC_REF(on_stunned)) + RegisterSignal(owner, COMSIG_LIVING_STATUS_KNOCKDOWN, PROC_REF(on_knockdown)) + // Flag the mind so it persists through death/revival + if(owner.mind) + owner.mind.has_arcyne_momentum = TRUE + update_alert() + +/datum/status_effect/buff/arcyne_momentum/on_remove() + UnregisterSignal(owner, list(COMSIG_LIVING_STATUS_STUN, COMSIG_LIVING_STATUS_KNOCKDOWN)) + if(chant == "unarmed") + UnregisterSignal(owner, list(COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_MOB_ITEM_ATTACK)) + if(is_overcharged) + owner.cut_overlay(electricity_overlay) + owner.remove_filter(MOMENTUM_FILTER) + . = ..() + +// Call this after setting chant to register the appropriate signals +/datum/status_effect/buff/arcyne_momentum/proc/set_chant(new_chant) + // Unregister old unarmed signals if switching away + if(chant == "unarmed" && new_chant != "unarmed") + UnregisterSignal(owner, list(COMSIG_HUMAN_MELEE_UNARMED_ATTACK, COMSIG_MOB_ITEM_ATTACK)) + chant = new_chant + // Register unarmed signals if needed + if(chant == "unarmed") + RegisterSignal(owner, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, PROC_REF(on_unarmed_attack)) + RegisterSignal(owner, COMSIG_MOB_ITEM_ATTACK, PROC_REF(on_unarmed_item_attack)) + +/datum/status_effect/buff/arcyne_momentum/proc/on_unarmed_attack(mob/living/source, atom/target, proximity) + SIGNAL_HANDLER + if(!isliving(target)) + return + // Only punch intent builds momentum, not touch/shove/grab + if(!istype(owner.used_intent, /datum/intent/unarmed/punch)) + return + var/mob/living/victim = target + if(victim == owner || victim.stat == DEAD) + return + if(world.time < last_melee_gain + melee_gain_cooldown) + return + add_stacks(1) + last_melee_gain = world.time + +// Momentum from knuckle/katar hits — only if the weapon uses unarmed skill +/datum/status_effect/buff/arcyne_momentum/proc/on_unarmed_item_attack(mob/living/source, mob/living/target, mob/living/user) + SIGNAL_HANDLER + if(!isliving(target) || target == owner || target.stat == DEAD) + return + var/obj/item/weapon = user?.get_active_held_item() + if(!weapon || weapon.associated_skill != /datum/attribute/skill/combat/unarmed) + return + if(world.time < last_melee_gain + melee_gain_cooldown) + return + add_stacks(1) + last_melee_gain = world.time + +/datum/status_effect/buff/arcyne_momentum/proc/on_stunned() + SIGNAL_HANDLER + if(stacks <= 0) + return + stacks = 0 + owner.balloon_alert(owner, "M: 0/[max_stacks]") + update_visuals() + update_alert() + update_spell_buttons() + to_chat(owner, span_warning("The shock breaks my concentration, all momentum lost!")) + +/datum/status_effect/buff/arcyne_momentum/proc/on_knockdown() + SIGNAL_HANDLER + if(stacks <= 0) + return + stacks = 0 + owner.balloon_alert(owner, "M: 0/[max_stacks]") + update_visuals() + update_alert() + update_spell_buttons() + to_chat(owner, span_warning("I hit the ground, all momentum lost!")) + +/datum/status_effect/buff/arcyne_momentum/proc/add_stacks(amount) + var/old_stacks = stacks + stacks = min(stacks + amount, max_stacks) + last_stack_time = world.time + if(stacks == old_stacks) + return + owner.balloon_alert(owner, "M: [stacks]/[max_stacks]") + update_visuals() + update_alert() + update_spell_buttons() + if(old_stacks < 3 && stacks >= 3) + to_chat(owner, span_notice("Arcyne force gathers within me!")) + playsound(get_turf(owner), 'sound/magic/charging.ogg', 30, TRUE) + if(old_stacks < 6 && stacks >= 6) + to_chat(owner, span_warning("Release! I must ACT NOW!")) + playsound(get_turf(owner), 'sound/magic/charged.ogg', 50, TRUE) + if(old_stacks < overcharge_threshold && stacks >= overcharge_threshold) + to_chat(owner, span_boldwarning("POWER! POWER! POWER! UNLEASH! UNLEASH! UNLEASH!")) + playsound(get_turf(owner), 'sound/magic/charged.ogg', 70, TRUE) + +/datum/status_effect/buff/arcyne_momentum/proc/consume_stacks(amount) + var/consumed = min(stacks, amount) + stacks = max(stacks - amount, 0) + owner.balloon_alert(owner, "M: [stacks]/[max_stacks]") + update_visuals() + update_alert() + update_spell_buttons() + return consumed + +/datum/status_effect/buff/arcyne_momentum/proc/consume_all_stacks() + var/consumed = stacks + stacks = 0 + owner.balloon_alert(owner, "M: 0/[max_stacks]") + update_visuals() + update_alert() + update_spell_buttons() + return consumed + +/datum/status_effect/buff/arcyne_momentum/proc/update_visuals() + owner.remove_filter(MOMENTUM_FILTER) + if(stacks >= overcharge_threshold) + owner.add_filter(MOMENTUM_FILTER, 2, list("type" = "outline", "color" = crackle_colour, "alpha" = 200, "size" = 2)) + else if(stacks >= 6) + owner.add_filter(MOMENTUM_FILTER, 2, list("type" = "outline", "color" = crackle_colour, "alpha" = 150, "size" = 2)) + else if(stacks >= 3) + owner.add_filter(MOMENTUM_FILTER, 2, list("type" = "outline", "color" = glow_colour, "alpha" = 100, "size" = 1)) + if(stacks >= overcharge_threshold) + if(!is_overcharged) + enter_overcharge() + else if(is_overcharged) + exit_overcharge() + +/datum/status_effect/buff/arcyne_momentum/proc/update_alert() + if(!linked_alert) + return + linked_alert.name = "Arcyne Momentum ([stacks]/[max_stacks])" + +/datum/status_effect/buff/arcyne_momentum/proc/update_spell_buttons() + if(!owner?.mind) + return + for(var/datum/action/cooldown/spell/S in owner.actions) + S.build_all_button_icons(UPDATE_BUTTON_STATUS) + +/datum/status_effect/buff/arcyne_momentum/tick() + if(stacks > 0 && world.time - last_stack_time >= MOMENTUM_DECAY_DELAY) + if(world.time - last_decay_time >= SECOND_PER_MOMENTUM) + last_decay_time = world.time + stacks = max(stacks - 1, 0) + owner.balloon_alert(owner, "M: [stacks]/[max_stacks]") + update_visuals() + update_alert() + update_spell_buttons() + if(stacks >= overcharge_threshold) + owner.apply_damage(overcharge_damage, BRUTE, BODY_ZONE_CHEST) + owner.emote(pick("twitch", "strain"), forced = TRUE) + +/datum/status_effect/buff/arcyne_momentum/proc/enter_overcharge() + is_overcharged = TRUE + if(!electricity_overlay) + electricity_overlay = mutable_appearance('icons/effects/effects.dmi', "electricity", EFFECTS_LAYER) + electricity_overlay.appearance_flags = RESET_COLOR + owner.add_overlay(electricity_overlay) + to_chat(owner, span_boldwarning("Electricity crackles across my body as arcyne energy overloads!")) + +/datum/status_effect/buff/arcyne_momentum/proc/exit_overcharge() + is_overcharged = FALSE + owner.cut_overlay(electricity_overlay) + +/proc/get_arcyne_momentum(mob/living/target) + if(!istype(target)) + return 0 + var/datum/status_effect/buff/arcyne_momentum/M = target.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(!M) + return 0 + return M.stacks + +#undef MOMENTUM_FILTER +#undef MOMENTUM_DECAY_DELAY +#undef SECOND_PER_MOMENTUM diff --git a/code/datums/status_effects/buffs/adrenaline_rush.dm b/code/datums/status_effects/buffs/adrenaline_rush.dm new file mode 100644 index 00000000000..d4c38dd1b25 --- /dev/null +++ b/code/datums/status_effects/buffs/adrenaline_rush.dm @@ -0,0 +1,43 @@ + +/atom/movable/screen/alert/status_effect/buff/adrenaline_rush + name = "Adrenaline Rush" + desc = "The gambit worked! I can do anything! My heart races, the throb of my wounds wavers." + icon_state = "adrrush" + +/datum/status_effect/buff/adrenaline_rush + id = "adrrush" + status_type = STATUS_EFFECT_REPLACE + alert_type = /atom/movable/screen/alert/status_effect/buff/adrenaline_rush + duration = 18 SECONDS + examine_text = "SUBJECTPRONOUN is amped up!" + effectedstats = list(STAT_ENDURANCE = 1) + var/blood_restore = 30 + +/datum/status_effect/buff/adrenaline_rush/on_apply() + . = ..() + var/mob/living/carbon/human/human = owner + if(istype(human)) + human.blood_volume = min((human.blood_volume + blood_restore), BLOOD_VOLUME_NORMAL) + human.physiology.pain_mod *= 0.5 + +/datum/status_effect/buff/adrenaline_rush/on_remove() + . = ..() + clear_adrenaline_rush() + +/datum/status_effect/buff/adrenaline_rush/be_replaced() + clear_adrenaline_rush() + return ..() + +/datum/status_effect/buff/adrenaline_rush/proc/clear_adrenaline_rush() + var/mob/living/carbon/human/human = owner + if(istype(human)) + human.physiology.pain_mod *= 2 + +/datum/status_effect/buff/adrenaline_rush/ranged + effectedstats = list(STAT_SPEED = 2) + +/datum/status_effect/buff/adrenaline_rush/melee + effectedstats = list(STAT_ENDURANCE = 1, STAT_CONSTITUTION = 1) + +/datum/status_effect/buff/adrenaline_rush/graggar + effectedstats = list(STAT_CONSTITUTION = 3) diff --git a/code/datums/status_effects/buffs/empowered_strike.dm b/code/datums/status_effects/buffs/empowered_strike.dm new file mode 100644 index 00000000000..340d09419df --- /dev/null +++ b/code/datums/status_effects/buffs/empowered_strike.dm @@ -0,0 +1,61 @@ +#define EMPOWER_FILTER "empower_glow" + +/atom/movable/screen/alert/status_effect/buff/empowered_strike + name = "Empowered Strike" + desc = "My next melee strike will bypass parry and dodge." + icon_state = "buff" + +/datum/status_effect/buff/empowered_strike + id = "empowered_strike" + alert_type = /atom/movable/screen/alert/status_effect/buff/empowered_strike + duration = 8 SECONDS + status_type = STATUS_EFFECT_UNIQUE + +/datum/status_effect/buff/empowered_strike/on_creation(mob/living/new_owner, new_duration) + if(new_duration) + duration = new_duration + return ..() + +/datum/status_effect/buff/empowered_strike/on_apply() + . = ..() + RegisterSignal(owner, COMSIG_MOB_ITEM_ATTACK, PROC_REF(on_attack)) + RegisterSignal(owner, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, PROC_REF(on_unarmed_attack)) + owner.add_filter(EMPOWER_FILTER, 2, list("type" = "outline", "color" = "#ff2020", "alpha" = 200, "size" = 2)) + owner.balloon_alert_to_viewers("empowered!") + +/datum/status_effect/buff/empowered_strike/on_remove() + UnregisterSignal(owner, list(COMSIG_MOB_ITEM_ATTACK, COMSIG_HUMAN_MELEE_UNARMED_ATTACK)) + owner.remove_filter(EMPOWER_FILTER) + . = ..() + +/datum/status_effect/buff/empowered_strike/proc/on_attack(mob/living/source, mob/living/target, mob/living/user, obj/item/weapon) + SIGNAL_HANDLER + if(target == owner || target.stat == DEAD) + return + // Consume the buff - this swing bypasses defense + consume_empower(target) + return COMPONENT_ITEM_NO_DEFENSE + +/datum/status_effect/buff/empowered_strike/proc/on_unarmed_attack(mob/living/source, atom/target, proximity) + SIGNAL_HANDLER + if(!isliving(target) || target == owner) + return + var/mob/living/L = target + if(L.stat == DEAD) + return + // Flag for the unarmed attack path to skip defense + ADD_TRAIT(owner, TRAIT_EMPOWERED_UNARMED, "empowered_strike") + // Consume on next tick after the attack resolves + addtimer(CALLBACK(src, PROC_REF(consume_empower), L), 0) + +/datum/status_effect/buff/empowered_strike/proc/consume_empower(mob/living/hit_target) + REMOVE_TRAIT(owner, TRAIT_EMPOWERED_UNARMED, "empowered_strike") + playsound(get_turf(owner), 'sound/magic/antimagic.ogg', 40, TRUE) + owner.visible_message( + span_danger("[owner]'s empowered strike blazes through!"), + span_notice("My empowered strike lands true!")) + if(istype(hit_target) && hit_target != owner) + to_chat(hit_target, span_bigbold("There was no way to avoid that empowered attack!")) + owner.remove_status_effect(/datum/status_effect/buff/empowered_strike) + +#undef EMPOWER_FILTER diff --git a/code/datums/status_effects/buffs/iron_skin.dm b/code/datums/status_effects/buffs/iron_skin.dm new file mode 100644 index 00000000000..5cfdbb1ed05 --- /dev/null +++ b/code/datums/status_effects/buffs/iron_skin.dm @@ -0,0 +1,31 @@ +#define IRON_SKIN_FILTER "iron_skin_glow" + +/atom/movable/screen/alert/status_effect/buff/iron_skin + name = "Iron Skin" + desc = "Bits of arcyne iron and steel surround my armor, any attacks against me are blunted." + icon_state = "buff" + +/datum/status_effect/buff/iron_skin + var/outline_colour = "#708090" + id = "iron_skin" + alert_type = /atom/movable/screen/alert/status_effect/buff/iron_skin + duration = 1 MINUTES + +/datum/status_effect/buff/iron_skin/on_creation(mob/living/new_owner, var/new_duration = null) + if(new_duration) + duration = new_duration + . = ..() + +/datum/status_effect/buff/iron_skin/on_apply() + . = ..() + var/filter = owner.get_filter(IRON_SKIN_FILTER) + if(!filter) + owner.add_filter(IRON_SKIN_FILTER, 2, list("type" = "outline", "color" = outline_colour, "alpha" = 40, "size" = 1)) + to_chat(owner, span_notice("Bits of arcyne iron and steel surround my armor, any blows and attacks against me are blunted.")) + +/datum/status_effect/buff/iron_skin/on_remove() + . = ..() + to_chat(owner, span_warning("The iron shell flakes away.")) + owner.remove_filter(IRON_SKIN_FILTER) + +#undef IRON_SKIN_FILTER diff --git a/code/datums/status_effects/buffs/second_chance.dm b/code/datums/status_effects/buffs/second_chance.dm new file mode 100644 index 00000000000..ca1430edcd7 --- /dev/null +++ b/code/datums/status_effects/buffs/second_chance.dm @@ -0,0 +1,19 @@ +/atom/movable/screen/alert/status_effect/buff/second_chance + name = "Second Chance" + desc = "Magickal resilience hardens me - I shrug off critical wounds, and pain no longer staggers me." + icon_state = "buff" + +/datum/status_effect/buff/second_chance + id = "second_chance" + alert_type = /atom/movable/screen/alert/status_effect/buff/second_chance + duration = 30 SECONDS + +/datum/status_effect/buff/second_chance/on_apply() + . = ..() + ADD_TRAIT(owner, TRAIT_CRITICAL_RESISTANCE, MAGIC_TRAIT) + ADD_TRAIT(owner, TRAIT_NOPAINSTUN, MAGIC_TRAIT) + +/datum/status_effect/buff/second_chance/on_remove() + REMOVE_TRAIT(owner, TRAIT_CRITICAL_RESISTANCE, MAGIC_TRAIT) + REMOVE_TRAIT(owner, TRAIT_NOPAINSTUN, MAGIC_TRAIT) + . = ..() diff --git a/code/datums/status_effects/debuffs/lightning_struck.dm b/code/datums/status_effects/debuffs/lightning_struck.dm new file mode 100644 index 00000000000..e9935a7e57a --- /dev/null +++ b/code/datums/status_effects/debuffs/lightning_struck.dm @@ -0,0 +1,43 @@ +/datum/status_effect/buff/lightningstruck + id = "lightningstruck" + alert_type = /atom/movable/screen/alert/status_effect/buff/lightningstruck + duration = 6 SECONDS + effectedstats = list(STATKEY_STR = -2, STATKEY_SPD = -2, STATKEY_PER = -3, STATKEY_INT = -2) + +/atom/movable/screen/alert/status_effect/buff/lightningstruck + name = "Lightning Struck" + desc = "Shocked, slowed, and disoriented. I can't swing my blade or think properly." + icon_state = "debuff" + color = "#ffff00" + +/datum/status_effect/buff/lightningstruck/on_apply() + . = ..() + var/mob/living/target = owner + target.add_movespeed_modifier(MOVESPEED_ID_LIGHTNINGSTRUCK, update=TRUE, priority=100, multiplicative_slowdown=4, movetypes=GROUND) + target.adjust_stamina(-25) + +/datum/status_effect/buff/lightningstruck/on_remove() + . = ..() + var/mob/living/target = owner + target.remove_movespeed_modifier(MOVESPEED_ID_LIGHTNINGSTRUCK, TRUE) + +/datum/status_effect/buff/lightningstruck/minor + duration = 3 SECONDS + effectedstats = list("speed" = -1) + +/datum/status_effect/buff/lightningstruck/minor/on_apply() + . = ..() + var/mob/living/target = owner + target.add_movespeed_modifier(MOVESPEED_ID_LIGHTNINGSTRUCK, update=TRUE, priority=100, multiplicative_slowdown=1, movetypes=GROUND) + +/mob/living/proc/lightning_shock(source) + electrocute_act(1, source, 1, SHOCK_NOSTUN) + if(!mob_timers[MT_LIGHTNING_ADAPTATION] || world.time > mob_timers[MT_LIGHTNING_ADAPTATION] + LIGHTNING_ADAPTATION_COOLDOWN) + Immobilize(0.5 SECONDS) + apply_status_effect(/datum/status_effect/buff/lightningstruck) + balloon_alert_to_viewers("shocked! (6s)") + mob_timers[MT_LIGHTNING_ADAPTATION] = world.time + return TRUE + var/remaining = round((mob_timers[MT_LIGHTNING_ADAPTATION] + LIGHTNING_ADAPTATION_COOLDOWN - world.time) / 10) + balloon_alert_to_viewers("shock adapted ([remaining]s)") + return FALSE diff --git a/code/datums/status_effects/debuffs/withered.dm b/code/datums/status_effects/debuffs/withered.dm new file mode 100644 index 00000000000..b4600cf1cd0 --- /dev/null +++ b/code/datums/status_effects/debuffs/withered.dm @@ -0,0 +1,24 @@ + +/datum/status_effect/buff/witherd + id = "withered" + alert_type = /atom/movable/screen/alert/status_effect/buff/witherd + duration = 30 SECONDS + effectedstats = list(STAT_SPEED = -2, STAT_STRENGTH = -2, STAT_CONSTITUTION = -2, STAT_ENDURANCE = -2) + +/atom/movable/screen/alert/status_effect/buff/witherd + name = "Withering" + desc = "I can feel my physical prowess waning." + icon_state = "debuff" + color = "#b884f8" //talk about a coder sprite x2 + +/datum/status_effect/buff/witherd/on_apply() + . = ..() + to_chat(owner, span_warning("I feel sapped of vitality!")) + var/mob/living/target = owner + var/newcolor = rgb(207, 135, 255) + target.add_atom_colour(newcolor, TEMPORARY_COLOUR_PRIORITY) + addtimer(CALLBACK(target, TYPE_PROC_REF(/atom, remove_atom_colour), TEMPORARY_COLOUR_PRIORITY, newcolor), 30 SECONDS) + +/datum/status_effect/buff/witherd/on_remove() + . = ..() + to_chat(owner, span_warning("I feel my physical prowess returning.")) diff --git a/code/datums/status_effects/swing_delay.dm b/code/datums/status_effects/swing_delay.dm new file mode 100644 index 00000000000..b331a139176 --- /dev/null +++ b/code/datums/status_effects/swing_delay.dm @@ -0,0 +1,67 @@ + +/datum/status_effect/swingdelay + id = "swingdelay" + alert_type = /atom/movable/screen/alert/status_effect/swingdelay + duration = 1 SECONDS + mob_overlay_icon_state = "eff_swingdelay" + mob_overlay_icon = 'icons/mob/mob_effects.dmi' + +/datum/status_effect/swingdelay/on_creation(mob/living/new_owner, newdur) + if(newdur) + duration = newdur + . = ..() + +/datum/status_effect/swingdelay/on_apply() + . = ..() + owner.swing_state = TRUE + +/atom/movable/screen/alert/status_effect/swingdelay + name = "Swinging!" + desc = "I am swinging my weapon! Why did I have the time to read this?!" + icon = 'icons/mob/screen_alert_combat.dmi' + icon_state = "swingdelay" + +/datum/status_effect/swingdelay/penalty + alert_type = /atom/movable/screen/alert/status_effect/swingdelay/penalty + mob_overlay_icon_state = "eff_swingdelay_penalty" + +/atom/movable/screen/alert/status_effect/swingdelay/penalty + name = "Swinging with a penalty!" + desc = "I am swinging my weapon! My guard is weaker! Pay attention to the screen, not here, you loon!" + icon_state = "swingdelay_penalty" + +/datum/status_effect/swingdelay/disrupt + id = "swingdelay_disrupt" + alert_type = /atom/movable/screen/alert/status_effect/swingdelay/disrupt + mob_overlay_icon_state = "eff_swingdelay_cancel" + var/is_disrupted = FALSE + +/datum/status_effect/swingdelay/disrupt/on_creation(mob/living/new_owner, newdur, apply_slow = FALSE) + if(apply_slow) + var/spd_mod = 10 - GET_MOB_ATTRIBUTE_VALUE(new_owner, STAT_SPEED) + effectedstats = list(STAT_SPEED = spd_mod) + . = ..() + +/datum/status_effect/swingdelay/disrupt/proc/attacked() + owner.swing_state = FALSE + is_disrupted = TRUE + playsound(owner, 'sound/combat/swingdelay_disrupted.ogg', 100, TRUE) + if(mob_visual) + mob_visual.icon_state = "eff_swingdelay_disrupted" + +/datum/status_effect/swingdelay/disrupt/proc/is_disrupted() + return is_disrupted + +/atom/movable/screen/alert/status_effect/swingdelay/disrupt + name = "Swinging fiercely!" + desc = "THEY WILL JAB ME AND INTERRUPT THE ATTACK YOU GOBLINBRAINED WRETCH! LOOK AT THE ENEMY!!!" + icon_state = "swingdelay_disrupt" + +/datum/status_effect/swingdelay/penalty/committed + id = "swingdelay_committed" + +/datum/status_effect/swingdelay/penalty/committed/on_creation(mob/living/new_owner, newdur, apply_slow = FALSE) + if(apply_slow) + var/spd_mod = 10 - GET_MOB_ATTRIBUTE_VALUE(new_owner, STAT_SPEED) + effectedstats = list(STAT_SPEED = spd_mod) + . = ..() diff --git a/code/game/objects/effects/temporary_visuals/pillar_warning.dm b/code/game/objects/effects/temporary_visuals/pillar_warning.dm new file mode 100644 index 00000000000..598854fbeef --- /dev/null +++ b/code/game/objects/effects/temporary_visuals/pillar_warning.dm @@ -0,0 +1,34 @@ + +/obj/effect/temp_visual/pillar_warning + icon = 'icons/effects/effects.dmi' + icon_state = "spellwarning" + layer = ABOVE_MOB_LAYER + duration = 2 SECONDS + +/obj/effect/temp_visual/pillar_warning/Initialize(mapload, life) + if(life) + duration = life + . = ..() + +/obj/effect/temp_visual/pillar_warning/fadein + alpha = 0 + +/obj/effect/temp_visual/pillar_warning/fadein/Initialize(mapload, life) + . = ..() + animate(src, alpha = 255, time = duration) + +/obj/effect/temp_visual/fire_pillar + icon = 'icons/effects/32x96.dmi' + icon_state = "sunstrike" + light_outer_range = 2 + light_color = LIGHT_COLOR_FIRE + duration = 1 SECONDS + +/obj/effect/temp_visual/dragonfire + icon = 'icons/effects/fire.dmi' + icon_state = "3" + layer = GASFIRE_LAYER + light_outer_range = LIGHT_RANGE_FIRE + light_color = LIGHT_COLOR_FIRE + blend_mode = BLEND_ADD + duration = 8 diff --git a/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm b/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm index 1c2a95e6b8a..1dc06298bf5 100644 --- a/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm +++ b/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm @@ -35,3 +35,6 @@ /obj/effect/projectile/tracer/bloodsteal name = "blood steal" icon_state = "hcult" + +/obj/effect/projectile/tracer/wormhole + icon_state = "wormhole_g" diff --git a/code/game/objects/effects/temporary_visuals/spell_impact.dm b/code/game/objects/effects/temporary_visuals/spell_impact.dm new file mode 100644 index 00000000000..7e4b351a6b3 --- /dev/null +++ b/code/game/objects/effects/temporary_visuals/spell_impact.dm @@ -0,0 +1,23 @@ + +/obj/effect/temp_visual/spell_impact + icon = 'icons/effects/spell_cast.dmi' + icon_state = "particle_up" + layer = ABOVE_ALL_MOB_LAYER + duration = 1 SECONDS + randomdir = FALSE + light_outer_range = 3 + light_color = "#FFFFFF" + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + +/obj/effect/temp_visual/spell_impact/Initialize(mapload, impact_color = "#FFFFFF", intensity = SPELL_IMPACT_LOW) + . = ..() + color = impact_color + light_color = impact_color + + switch(intensity) + if(SPELL_IMPACT_LOW) + light_outer_range = 3 + if(SPELL_IMPACT_MEDIUM) + light_outer_range = 5 + if(SPELL_IMPACT_HIGH) + light_outer_range = 7 diff --git a/code/game/objects/effects/temporary_visuals/target.dm b/code/game/objects/effects/temporary_visuals/target.dm index ff805ac1bdc..21c16f0d6f2 100644 --- a/code/game/objects/effects/temporary_visuals/target.dm +++ b/code/game/objects/effects/temporary_visuals/target.dm @@ -126,3 +126,17 @@ for(var/mob/living/L in T) L.electrocute_act(50) + +/obj/effect/temp_visual/target/lightning/sundering + light_color = COLOR_PALE_BLUE_GRAY + duration = 12 + +/obj/effect/temp_visual/target/lightning/sundering/fall(list/hit_atoms) + var/turf/T = get_turf(src) + sleep(duration) + playsound(T,'sound/magic/lightning.ogg', 80, TRUE) + new /obj/effect/temp_visual/lightning(T) + + for(var/mob/living/L in T) + L.adjustFireLoss(50) + L.lightning_shock(src) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index b8917832873..34bbc9e798a 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -304,6 +304,8 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e var/wield_block = TRUE /// Needed for grandmaster/martyr weapons, might be shitcode, might be usable for the future, *shrug, it works var/toggle_state + ///if this is set we add the spell modifier component with these stats + var/datum/spellcraft_contribution/item/spell_modifier /obj/item/Initialize(mapload) if (attack_verb) @@ -322,6 +324,8 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e getmoboverlay(i,prop,behind=FALSE,mirrored=TRUE) getmoboverlay(i,prop,behind=TRUE,mirrored=TRUE) + if(spell_modifier) + apply_spell_modifiers() if(experimental_onhip) if(slot_flags & ITEM_SLOT_BELT) var/i = "onbelt" @@ -1350,6 +1354,24 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e return TRUE +/obj/item/proc/apply_spell_modifiers() + var/datum/spellcraft_contribution/contribution = GLOB.spellcraft_items[spell_modifier] + + if(!contribution) + return + + if(contribution.is_empty()) + return + + AddComponent(/datum/component/spell_modifier, \ + contribution.form_cost_multipliers, \ + contribution.form_cast_speed_multipliers, \ + contribution.form_magnitude_modifications, \ + contribution.technique_cost_multipliers, \ + contribution.technique_cast_speed_multipliers, \ + contribution.technique_magnitude_modifications \ + ) + // Called before use_tool if there is a delay, or by use_tool if there isn't. // Only ever used by welding tools and stacks, so it's not added on any other use_tool checks. /obj/item/proc/tool_start_check(mob/living/user, amount=0) diff --git a/code/game/objects/items/arcyne_spellobject.dm b/code/game/objects/items/arcyne_spellobject.dm index ae2e33abcbb..00b5bc4f2ad 100644 --- a/code/game/objects/items/arcyne_spellobject.dm +++ b/code/game/objects/items/arcyne_spellobject.dm @@ -158,7 +158,6 @@ ) var/datum/action/cooldown/spell/instance = new E.spell_type(user) - instance.point_cost = 0 instance.spell_cost = 0 instance.cooldown_time = 0 instance.spell_flags |= SPELL_TEMPORARY @@ -216,7 +215,6 @@ if(E.live_spell || !E.spell_type) return E.live_spell = new E.spell_type(user) - E.live_spell.point_cost = 0 E.live_spell.cooldown_time = 0 E.live_spell.spell_cost = 0 E.live_spell.spell_flags |= SPELL_TEMPORARY diff --git a/code/game/objects/items/gems.dm b/code/game/objects/items/gems.dm index 89d4f388bdc..31c6e5f332b 100644 --- a/code/game/objects/items/gems.dm +++ b/code/game/objects/items/gems.dm @@ -51,15 +51,9 @@ /obj/item/gem/on_consume(mob/living/eater) . = ..() eater.extra_mob_weight += get_carry_weight(eater) - if(attuned) - //eater.adjust_spell_points(0.5) - eater.mana_pool.adjust_attunement(attuned, 0.1) /obj/item/gem/on_anti_consume(mob/living/eater) eater.extra_mob_weight -= get_carry_weight(eater) - if(attuned) - //eater.adjust_spell_points(0.5) - eater.mana_pool.adjust_attunement(attuned, -0.1) ///This is a switch incase anyone would like to add more... /obj/item/gem/update_icon_state() diff --git a/code/game/objects/items/granters.dm b/code/game/objects/items/granters.dm index 41d38578c62..d8762c2d5f3 100644 --- a/code/game/objects/items/granters.dm +++ b/code/game/objects/items/granters.dm @@ -209,7 +209,7 @@ var/arcaneskill = GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) if(arcaneskill >= SKILL_LEVEL_NOVICE) //Required arcane skill of NOVICE or higher to use the granter to_chat(user, span_notice("I absorb the insights on the scroll, and feel more adept at spellcraft!")) - user.adjust_spell_points(1) + user.adjust_form_mastery_points(1) onlearned(user) else to_chat(user, span_notice("I don't know what to make of this.")) diff --git a/code/game/objects/items/mageitems.dm b/code/game/objects/items/mageitems.dm index 6f317373920..6288cf647c3 100644 --- a/code/game/objects/items/mageitems.dm +++ b/code/game/objects/items/mageitems.dm @@ -656,7 +656,7 @@ w_class = WEIGHT_CLASS_SMALL sellprice = 20 item_weight = 40 GRAMS - var/obj/item/book/granter/spellbook/melded_quality = /obj/item/book/granter/spellbook/adept + var/obj/item/spellbook/melded_quality = /obj/item/spellbook/adept var/shock_damage = 20 /obj/item/natural/melded/t1 @@ -673,7 +673,7 @@ item_flags = OBTAINED_DATA obtained_from = list(list("Killing a Sylph", /mob/living/simple_animal/hostile/retaliate/fae/sylph)) item_weight = 50 GRAMS - melded_quality = /obj/item/book/granter/spellbook/expert + melded_quality = /obj/item/spellbook/expert shock_damage = 40 /obj/item/natural/melded/t3 @@ -682,7 +682,7 @@ icon_state = "wessence" desc = "A melding of molten core, heartwood core and elemental fragment." item_weight = 60 GRAMS - melded_quality = /obj/item/book/granter/spellbook/master + melded_quality = /obj/item/spellbook/master shock_damage = 60 /obj/item/natural/melded/t4 @@ -691,7 +691,7 @@ icon_state = "wessence" desc = "A melding of abyssal flame, sylvan essence and elemental relic." item_weight = 70 GRAMS - melded_quality = /obj/item/book/granter/spellbook/legendary + melded_quality = /obj/item/spellbook/legendary shock_damage = 80 /obj/item/natural/melded/t5 @@ -699,7 +699,7 @@ icon_state = "wessence" desc = "A melding of arcyne fusion and voidstone. It pulses erratically, power coiled tightly within and dangerous. Many would be afraid of going near this, let alone holding it." item_weight = 80 GRAMS - melded_quality = /obj/item/book/granter/spellbook/legendary + melded_quality = /obj/item/spellbook/legendary shock_damage = 40 /obj/structure/soul diff --git a/code/game/objects/items/natural/stones.dm b/code/game/objects/items/natural/stones.dm index 172ddb46a6a..fbd9667648d 100644 --- a/code/game/objects/items/natural/stones.dm +++ b/code/game/objects/items/natural/stones.dm @@ -155,11 +155,6 @@ GLOBAL_LIST_INIT(stone_personality_descs, list( /obj/item/natural/stone/on_consume(mob/living/eater) . = ..() eater.extra_mob_weight += get_carry_weight(eater) - if(!magic_power) - return - //eater.adjust_spell_points(magic_power * 0.1) - //eater.mana_pool?.adjust_mana(magic_power * 25) - //to_chat(eater, span_warning("I feel magic flowing from my stomach.")) /obj/item/natural/stone/on_anti_consume(mob/living/eater) eater.extra_mob_weight -= get_carry_weight(eater) diff --git a/code/game/objects/items/spellbook.dm b/code/game/objects/items/spellbook.dm deleted file mode 100644 index be4da58ed62..00000000000 --- a/code/game/objects/items/spellbook.dm +++ /dev/null @@ -1,306 +0,0 @@ -/obj/item/book/granter/spellbook - icon = 'icons/roguetown/items/books.dmi' - icon_state = "spellbookbrown_0" - slot_flags = ITEM_SLOT_HIP - unique = TRUE - firefuel = 2 MINUTES - dropshrink = 0.6 - drop_sound = 'sound/foley/dropsound/book_drop.ogg' - force = 5 - associated_skill = /datum/attribute/skill/misc/reading - name = "tome of the arcyne" - desc = "A crackling, glowing book, filled with runes and symbols that hurt the mind to stare at." - pages_to_mastery = 7 - remarks = list( - "Recall that place of white and black, so cold after its season of heat...", - "Time slips away as I devour each pictograph and sigil...", - "Noc is a shrewd God, and his followers' writings are no different...", - "The smell of wet rain fills the room with every turned page...", - "Helical text spans the page like a winding puzzle...", - "Tracing a finger over one rune renders my hand paralyzed, if only for a moment...", - "This page clearly details the benefits of swampweed on one's capacity to conceptualize the arcyne...", - "Conceptualize. Theorize. Feel. Flow. Manifest...", - "Passion. Strength. Power. Victory. The tenets through which we break the chains of reality...", - "Magick is to be kept close, a guarded secret. Noc changed the rules again. I need to catch up...", - "Didn't I just read this page...?", - "A lone illustration of Noc's visage fills this page, his stony gaze boring into my soul...", - "My eyes begin to lid as I finish this chapter. These symbols cast a heavy fog over my mind...", - "Silver. Blade. Mana. Blood. These are the ingredients I'll need to imbibe the very ground with arcyne abilities...", - "Elysium incants speak to me in an extinct tongue immortalized on parchment...", - "My mind wanders and waves. Z's temptations draw close, but I weather through as I finally finish this chapter...", - "I close my eyes for but a moment, and the competing visages of Noc and Z stare into my very soul. I see them blink, and my eyelids open...", - "I am the Root. The Root is me. I must reach it, and the Tree...", - "I feel the arcyne circuits running through my body, empowered with each word I read...", - "Am I reading? Are these words, symbols or inane scribbles? I cannot be sure, yet with each one my eyes glaze over, I can feel the arcyne pulse within me...", - "A mystery is revealed before my very eyes. I do not read it, yet I am aware. Gems are the Root's natural arcyne energy, manifest. Perhaps I can use them to better my conceptualization..." - ) - oneuse = FALSE - item_weight = 547 GRAMS - /// The mob who owns and originally bound this tome - var/owner = null - /// Up to two additional mobs allowed to read this tome - var/list/allowed_readers = list() - /// Flat quality bonus stored from a crushed gem, consumed on next read - var/stored_gem = FALSE - /// Attunement datum stored from the gem used, applied on next read - var/datum/attunement/stored_attunement - /// Whether the player has chosen a visual style for this book yet - var/picked = FALSE - /// If TRUE, this tome was made from a magic stone rather than a gem and has a reading penalty - var/born_of_rock = FALSE - /// Multiplier for spell points gained per read. Higher = better book. - var/bookquality = 3 - - -// ============================================================ -// VISUAL / OPEN STATE -// ============================================================ - -/// The open and closed states share identical mob prop data, so we return -/// one table regardless of open state. -/obj/item/book/granter/spellbook/getonmobprop(tag) - . = ..() - if(!tag) - return - switch(tag) - if("gen") - return list( - "shrink" = 0.4, - "sx" = -2, "sy" = -3, - "nx" = 10, "ny" = -2, - "wx" = 1, "wy" = -3, - "ex" = 5, "ey" = -3, - "northabove" = 0, "southabove" = 1, - "eastabove" = 1, "westabove" = 0, - "nturn" = 0, "sturn" = 0, "wturn" = 0, "eturn" = 0, - "nflip" = 0, "sflip" = 0, "wflip" = 0, "eflip" = 0 - ) - if("onbelt") - return list( - "shrink" = 0.3, - "sx" = -2, "sy" = -5, - "nx" = 4, "ny" = -5, - "wx" = 0, "wy" = -5, - "ex" = 2, "ey" = -5, - "nturn" = 0, "sturn" = 0, "wturn" = 0, "eturn" = 0, - "nflip" = 0, "sflip" = 0, "wflip" = 0, "eflip" = 0, - "northabove" = 0, "southabove" = 1, - "eastabove" = 1, "westabove" = 0 - ) - -/obj/item/book/granter/spellbook/update_icon_state() - . = ..() - icon_state = "[base_icon_state]_[open]" - -/obj/item/book/granter/spellbook/attack_self(mob/user, list/modifiers) - if(!open) - attack_hand_secondary(user, modifiers) - return - ..() - user.update_inv_hands() - -/obj/item/book/granter/spellbook/attack_self_secondary(mob/user, list/modifiers) - . = ..() - if(. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN) - return - attack_hand_secondary(user, modifiers) - return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN - -/obj/item/book/granter/spellbook/read(mob/user) - return FALSE - -/obj/item/book/granter/spellbook/attack_hand_secondary(mob/user, list/modifiers) - //first pick styles - if(!picked) - var/list/designlist = list("green", "yellow", "brown") - var/mob/living/carbon/human/gamer = user - if(gamer.job == JOB_COURT_MAGE) - designlist = list("steel", "gem", "skin", "mimic") - var/the_time = world.time - var/design = input(user, "Select a design.", "Spellbook Design") as null|anything in designlist - if(!design || world.time > (the_time + 30 SECONDS)) - return - base_icon_state = "spellbook[design]" - update_appearance(UPDATE_ICON_STATE) - picked = TRUE - return - - //now we togge state - if(owner == null) - owner = user - if(!open) - slot_flags &= ~ITEM_SLOT_HIP - open = TRUE - playsound(src, 'sound/items/book_open.ogg', 100, FALSE, -1) - else - slot_flags |= ITEM_SLOT_HIP - open = FALSE - playsound(src, 'sound/items/book_close.ogg', 100, FALSE, -1) - curpage = 1 - update_appearance(UPDATE_ICON_STATE) - user.update_inv_hands() - -/obj/item/book/granter/spellbook/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers) - if(!isliving(interacting_with)) - return NONE - - var/mob/living/target = interacting_with - if(target.stat == DEAD) - target.visible_message(span_danger("[user] smacks [target]'s lifeless corpse with [src].")) - playsound(src, "punch", 25, TRUE, -1) - return - - if(user == target) - to_chat(user, span_warning("I'm already chained to this tome!")) - return - - target.visible_message( - span_danger("[user] beats [target] over the head with [src]!"), - span_danger("[user] beats [target] over the head with [src]!") - ) - - if(length(allowed_readers) > 2 || (target in allowed_readers)) - to_chat(user, span_smallnotice("I can't chain [target.p_them()] to my tome...")) - return - - allowed_readers += target - - playsound(src, "punch", 25, TRUE, -1) - -/obj/item/book/granter/spellbook/on_reading_start(mob/user) - to_chat(user, span_notice("Arcyne mysteries abound in this enigmatic tome, gift of Noc...")) - -/obj/item/book/granter/spellbook/on_reading_finished(mob/user) - var/mob/living/carbon/human/gamer = user - if(gamer != owner && !allowed_readers.Find(gamer)) - to_chat(user, span_notice("What was that gibberish? Even for the arcyne it was completely illegible!")) - recoil(user) - return - - var/mob/living/reader = user - reader.mind?.has_studied = TRUE - - // Base quality is a blend of INT, reading skill, and arcane skill. - var/qualityoflearn = (GET_MOB_ATTRIBUTE_VALUE(reader, STAT_INTELLIGENCE) * 2 + GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/misc/reading) * 5 + GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) * 5) - - // Bonuses from external factors. - if(reader.has_status_effect(/datum/status_effect/buff/weed)) - to_chat(user, span_smallgreen("Swampweed truly does open one's third eye to the secrets of the arcyne...")) - qualityoflearn += 10 - - var/obj/effect/decal/cleanable/ritual_rune/rune = locate(/obj/effect/decal/cleanable/ritual_rune) in range(1, user) - if(rune) - to_chat(user, span_cultsmall("The rune beneath my feet glows...")) - qualityoflearn += rune.spellbonus - rune.do_invoke_glow() - - if(stored_gem) - to_chat(user, span_smallnotice("I can feel the magical energies imbued within the crystalline dust scattered upon my tome resonate with the arcyne...")) - qualityoflearn += stored_gem - stored_gem = FALSE - - // Penalties for untrained readers. - if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) <= SKILL_LEVEL_NONE) - if(gamer != owner) - qualityoflearn = 1 - else - qualityoflearn = min(qualityoflearn * 0.5, 15) - - // Born-of-rock tomes are easier to craft, so they're harder to read. - if(born_of_rock) - qualityoflearn *= 0.8 - - user.visible_message( - span_warning("[user] is filled with arcyne energy! You witness [user.p_their()] body convulse and spark brightly."), - span_notice("Noc blesses me. I have been granted knowledge and wisdom beyond my years, this tome's mysteries unveiled one at a time.") - ) - - var/spellpoints = CEILING(bookquality * (qualityoflearn / 100), 1) - reader.adjust_spell_points(spellpoints) - log_game("SPELLBOOK: [key_name(reader)] read a spellbook and gained [spellpoints] spell points.") - - if(stored_attunement) - user.mana_pool?.adjust_attunement(stored_attunement, 0.1 * (spellpoints / 0.2)) - - user.log_message("successfully studied their spellbook and gained spellpoints", LOG_ATTACK, color = "orange") - onlearned(user) - - if(prob(55)) - to_chat(user, span_notice("Confounded arcyne mysteries, my notes have gone in circles. I must sleep before I can bring myself to open this damned thing again...")) - user.mind?.add_sleep_experience(/datum/attribute/skill/misc/reading, GET_MOB_ATTRIBUTE_VALUE(reader, STAT_INTELLIGENCE) * 10) - - to_chat(user, span_small("My notes include passages I've read before, but don't understand. I must sleep on their meaning...")) - -/obj/item/book/granter/spellbook/onlearned(mob/user) - used = FALSE - -/obj/item/book/granter/spellbook/recoil(mob/user) - user.visible_message(span_warning("[src] shoots out a spark of angry, arcyne energy at [user]!")) - var/mob/living/gamer = user - gamer.electrocute_act(5, src) - -/obj/item/book/granter/spellbook/item_interaction(mob/living/user, obj/item/tool, list/modifiers) - if(!istype(tool, /obj/item/gem)) - return NONE - - if(stored_gem) - to_chat(user, span_notice("This tome is already coursing with arcyne energies...")) - return ITEM_INTERACT_BLOCKING - - if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) <= SKILL_LEVEL_NONE) - to_chat(user, span_notice("Why am I jamming a gem into a book? I must look like a fool!")) - return ITEM_INTERACT_BLOCKING - - var/obj/item/gem/gem = tool - var/crafttime = max(0, 60 - GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) * 5) - if(!do_after(user, crafttime, target = src)) - return ITEM_INTERACT_BLOCKING - - playsound(src, 'sound/magic/glass.ogg', 100, TRUE) - to_chat(user, span_notice("Running my arcyne energy through this crystal, I imbue the tome with my natural essence, attuning it to my state of mind...")) - stored_gem = gem.arcyne_potency - stored_attunement = gem.attuned - qdel(tool) - return ITEM_INTERACT_SUCCESS - -/obj/item/book/granter/spellbook/horrible - name = "poorly made tome of the arcyne" - desc = "A poorly made book, it barely glows with arcyne and has only small notes on arcyne symbols." - bookquality = 1 - sellprice = 15 - -/obj/item/book/granter/spellbook/mid - name = "beginners tome of the arcyne" - desc = "An obviously handcrafted book, it glows occasionally with arcyne and has a meager amount of notes on arcyne symbols." - bookquality = 2 - sellprice = 30 - -/obj/item/book/granter/spellbook/apprentice - name = "apprentice tome of the arcyne" - desc = "A carefully made book, faintly glowing with arcyne and half filled with notes and theory on arcyne symbols." - bookquality = 3 - sellprice = 75 - -/obj/item/book/granter/spellbook/adept - name = "adept tome of the arcyne" - desc = "A well-made book, it shines moderately with arcyne light. It has been filled with notes of varying degrees on the arcyne." - bookquality = 4 - sellprice = 150 - -/obj/item/book/granter/spellbook/expert - name = "expert tome of the arcyne" - desc = "A well cared for book, shining brightly with arcyne. It has many runes and arcyne symbols scribed within, with detailed notes." - bookquality = 6 - sellprice = 200 - -/obj/item/book/granter/spellbook/master - name = "masterful tome of the arcyne" - desc = "A crackling, glowing book, filled with advanced arcyne runes and symbols that hurt the mind to stare at. A true master of the arcyne has left their mark behind." - bookquality = 8 - sellprice = 250 - -/obj/item/book/granter/spellbook/legendary - name = "legendary tome of the arcyne" - desc = "An incredible book that gives off glowing arcyne motes, it is filled with runes and arcyne theories that is hard for even masters of arcyne to understand. The arcyne script glows and practically whispers from the page..." - bookquality = 12 - sellprice = 400 diff --git a/code/game/objects/items/spellbooks/_starter_helpers.dm b/code/game/objects/items/spellbooks/_starter_helpers.dm new file mode 100644 index 00000000000..4f2ab4ba651 --- /dev/null +++ b/code/game/objects/items/spellbooks/_starter_helpers.dm @@ -0,0 +1,21 @@ +/obj/item/spellbook/proc/get_theme_flavor(form) + switch(form) + if(FORM_FIRE) + return list("aflame", "Faint heat radiates from its pages, and its runes are all subtly singed.") + if(FORM_ICE) + return list("frostbound", "Its pages are cool to the touch, rimed with a frost that never quite melts.") + if(FORM_LIGHTNING) + return list("storm-charged", "Tiny sparks crawl between its runes whenever the cover is opened.") + if(FORM_EARTH) + return list("stoneveined", "Its binding is threaded with fine mineral veins, heavy as river stone.") + if(FORM_ARCANE) + return list("thrice-warded", "Its script folds in on itself in ways the eye struggles to follow.") + if(FORM_DEATH) + return list("grave-touched", "A faint chill of the grave lingers about its pages.") + if(FORM_LIFE) + return list("verdant", "Its margins are traced with fine, living green filigree.") + if(FORM_AIR) + return list("windswept", "Its pages riffle gently even in still air.") + if(FORM_WATER) + return list("tidebound", "Condensation beads along its cover no matter how dry the room.") + return list(null, null) diff --git a/code/game/objects/items/spellbooks/adept.dm b/code/game/objects/items/spellbooks/adept.dm new file mode 100644 index 00000000000..030ff3b7efc --- /dev/null +++ b/code/game/objects/items/spellbooks/adept.dm @@ -0,0 +1,54 @@ +/obj/item/spellbook/adept/starter + abstract_type = /obj/item/spellbook/adept/starter + abstract_type = /obj/item/spellbook/adept/starter + themed_cost_multiplier = 0.88 + themed_cast_speed_multiplier = 1.22 + themed_magnitude_bonus = 0.3 + +/obj/item/spellbook/adept/starter/apply_themed_bonuses() + if(!themed_form) + return ..() + var/list/flavor = get_theme_flavor(themed_form) + if(flavor[1]) + name = "[initial(name)], [flavor[1]]" + if(flavor[2]) + desc = "[initial(desc)] [flavor[2]]" + var/scale = bookquality / SPELLBOOK_THEME_BASELINE_QUALITY + base_form_points = CEILING(SPELLBOOK_THEME_BASELINE_FORM_POINTS * scale, 1) + base_technique_points = CEILING(SPELLBOOK_THEME_BASELINE_TECHNIQUE_POINTS * scale, 1) + var/themed_amount = CEILING(base_form_points * 0.5, 1) + mastery.adjust_form_mastery_points(themed_amount, FALSE, themed_form) + if(base_form_points - themed_amount) + mastery.adjust_form_points(base_form_points - themed_amount) + mastery.adjust_technique_points(base_technique_points) + AddComponent(/datum/component/spell_modifier, \ + list("[themed_form]" = themed_cost_multiplier), \ + list("[themed_form]" = themed_cast_speed_multiplier), \ + list("[themed_form]" = themed_magnitude_bonus)) + +/obj/item/spellbook/adept/starter/fire + themed_form = FORM_FIRE + designlist = list("steel") +/obj/item/spellbook/adept/starter/ice + themed_form = FORM_ICE +/obj/item/spellbook/adept/starter/lightning + themed_form = FORM_LIGHTNING + designlist = list("steel") +/obj/item/spellbook/adept/starter/earth + themed_form = FORM_EARTH + designlist = list("steel") +/obj/item/spellbook/adept/starter/arcane + themed_form = FORM_ARCANE + designlist = list("gem") +/obj/item/spellbook/adept/starter/death + themed_form = FORM_DEATH + designlist = list("skin") +/obj/item/spellbook/adept/starter/life + themed_form = FORM_LIFE + designlist = list("mimic") +/obj/item/spellbook/adept/starter/air + themed_form = FORM_AIR + designlist = list("steel") +/obj/item/spellbook/adept/starter/water + themed_form = FORM_WATER + designlist = list("steel") diff --git a/code/game/objects/items/spellbooks/apprentice.dm b/code/game/objects/items/spellbooks/apprentice.dm new file mode 100644 index 00000000000..fae2d06ab29 --- /dev/null +++ b/code/game/objects/items/spellbooks/apprentice.dm @@ -0,0 +1,53 @@ +/obj/item/spellbook/apprentice/starter + abstract_type = /obj/item/spellbook/apprentice/starter + themed_cost_multiplier = 0.91 + themed_cast_speed_multiplier = 1.15 + themed_magnitude_bonus = 0.2 + +/obj/item/spellbook/apprentice/starter/apply_themed_bonuses() + if(!themed_form) + return ..() + var/list/flavor = get_theme_flavor(themed_form) + if(flavor[1]) + name = "[initial(name)], [flavor[1]]" + if(flavor[2]) + desc = "[initial(desc)] [flavor[2]]" + var/scale = bookquality / SPELLBOOK_THEME_BASELINE_QUALITY + base_form_points = CEILING(SPELLBOOK_THEME_BASELINE_FORM_POINTS * scale, 1) + base_technique_points = CEILING(SPELLBOOK_THEME_BASELINE_TECHNIQUE_POINTS * scale, 1) + var/themed_amount = CEILING(base_form_points * 0.5, 1) + mastery.adjust_form_mastery_points(themed_amount, FALSE, themed_form) + if(base_form_points - themed_amount) + mastery.adjust_form_points(base_form_points - themed_amount) + mastery.adjust_technique_points(base_technique_points) + AddComponent(/datum/component/spell_modifier, \ + list("[themed_form]" = themed_cost_multiplier), \ + list("[themed_form]" = themed_cast_speed_multiplier), \ + list("[themed_form]" = themed_magnitude_bonus)) + +/obj/item/spellbook/apprentice/starter/fire + themed_form = FORM_FIRE + designlist = list("steel") +/obj/item/spellbook/apprentice/starter/ice + themed_form = FORM_ICE +/obj/item/spellbook/apprentice/starter/lightning + themed_form = FORM_LIGHTNING + designlist = list("steel") +/obj/item/spellbook/apprentice/starter/earth + themed_form = FORM_EARTH + designlist = list("steel") +/obj/item/spellbook/apprentice/starter/arcane + themed_form = FORM_ARCANE + designlist = list("gem") +/obj/item/spellbook/apprentice/starter/death + themed_form = FORM_DEATH + designlist = list("skin") +/obj/item/spellbook/apprentice/starter/life + themed_form = FORM_LIFE + designlist = list("mimic") +/obj/item/spellbook/apprentice/starter/air + themed_form = FORM_AIR + designlist = list("steel") +/obj/item/spellbook/apprentice/starter/water + themed_form = FORM_WATER + designlist = list("steel") diff --git a/code/game/objects/items/spellbooks/expert.dm b/code/game/objects/items/spellbooks/expert.dm new file mode 100644 index 00000000000..c21a72acba3 --- /dev/null +++ b/code/game/objects/items/spellbooks/expert.dm @@ -0,0 +1,54 @@ + +/obj/item/spellbook/expert/starter + abstract_type = /obj/item/spellbook/expert/starter + themed_cost_multiplier = 0.85 + themed_cast_speed_multiplier = 1.30 + themed_magnitude_bonus = 0.4 + +/obj/item/spellbook/expert/starter/apply_themed_bonuses() + if(!themed_form) + return ..() + var/list/flavor = get_theme_flavor(themed_form) + if(flavor[1]) + name = "[initial(name)], [flavor[1]]" + if(flavor[2]) + desc = "[initial(desc)] [flavor[2]]" + var/scale = bookquality / SPELLBOOK_THEME_BASELINE_QUALITY + base_form_points = CEILING(SPELLBOOK_THEME_BASELINE_FORM_POINTS * scale, 1) + base_technique_points = CEILING(SPELLBOOK_THEME_BASELINE_TECHNIQUE_POINTS * scale, 1) + var/themed_amount = CEILING(base_form_points * 0.5, 1) + mastery.adjust_form_mastery_points(themed_amount, FALSE, themed_form) + if(base_form_points - themed_amount) + mastery.adjust_form_points(base_form_points - themed_amount) + mastery.adjust_technique_points(base_technique_points) + AddComponent(/datum/component/spell_modifier, \ + list("[themed_form]" = themed_cost_multiplier), \ + list("[themed_form]" = themed_cast_speed_multiplier), \ + list("[themed_form]" = themed_magnitude_bonus)) + +/obj/item/spellbook/expert/starter/fire + themed_form = FORM_FIRE + designlist = list("steel") +/obj/item/spellbook/expert/starter/ice + themed_form = FORM_ICE +/obj/item/spellbook/expert/starter/lightning + themed_form = FORM_LIGHTNING + designlist = list("steel") +/obj/item/spellbook/expert/starter/earth + themed_form = FORM_EARTH + designlist = list("steel") +/obj/item/spellbook/expert/starter/arcane + themed_form = FORM_ARCANE + designlist = list("gem") +/obj/item/spellbook/expert/starter/death + themed_form = FORM_DEATH + designlist = list("skin") +/obj/item/spellbook/expert/starter/life + themed_form = FORM_LIFE + designlist = list("mimic") +/obj/item/spellbook/expert/starter/air + themed_form = FORM_AIR + designlist = list("steel") +/obj/item/spellbook/expert/starter/water + themed_form = FORM_WATER + designlist = list("steel") diff --git a/code/game/objects/items/spellbooks/horrible.dm b/code/game/objects/items/spellbooks/horrible.dm new file mode 100644 index 00000000000..c136cf37a32 --- /dev/null +++ b/code/game/objects/items/spellbooks/horrible.dm @@ -0,0 +1,55 @@ + +/obj/item/spellbook/horrible/starter + abstract_type = /obj/item/spellbook/horrible/starter + themed_cost_multiplier = 0.97 + themed_cast_speed_multiplier = 1.05 + themed_magnitude_bonus = 0.1 + + +/obj/item/spellbook/horrible/starter/apply_themed_bonuses() + if(!themed_form) + return ..() + var/list/flavor = get_theme_flavor(themed_form) + if(flavor[1]) + name = "[initial(name)], [flavor[1]]" + if(flavor[2]) + desc = "[initial(desc)] [flavor[2]]" + var/scale = bookquality / SPELLBOOK_THEME_BASELINE_QUALITY + base_form_points = CEILING(SPELLBOOK_THEME_BASELINE_FORM_POINTS * scale, 1) + base_technique_points = CEILING(SPELLBOOK_THEME_BASELINE_TECHNIQUE_POINTS * scale, 1) + var/themed_amount = CEILING(base_form_points * 0.5, 1) + mastery.adjust_form_mastery_points(themed_amount, FALSE, themed_form) + if(base_form_points - themed_amount) + mastery.adjust_form_points(base_form_points - themed_amount) + mastery.adjust_technique_points(base_technique_points) + AddComponent(/datum/component/spell_modifier, \ + list("[themed_form]" = themed_cost_multiplier), \ + list("[themed_form]" = themed_cast_speed_multiplier), \ + list("[themed_form]" = themed_magnitude_bonus)) + +/obj/item/spellbook/horrible/starter/fire + themed_form = FORM_FIRE + designlist = list("steel") +/obj/item/spellbook/horrible/starter/ice + themed_form = FORM_ICE +/obj/item/spellbook/horrible/starter/lightning + themed_form = FORM_LIGHTNING + designlist = list("steel") +/obj/item/spellbook/horrible/starter/earth + themed_form = FORM_EARTH + designlist = list("steel") +/obj/item/spellbook/horrible/starter/arcane + themed_form = FORM_ARCANE + designlist = list("gem") +/obj/item/spellbook/horrible/starter/death + themed_form = FORM_DEATH + designlist = list("skin") +/obj/item/spellbook/horrible/starter/life + themed_form = FORM_LIFE + designlist = list("mimic") +/obj/item/spellbook/horrible/starter/air + themed_form = FORM_AIR + designlist = list("steel") +/obj/item/spellbook/horrible/starter/water + themed_form = FORM_WATER + designlist = list("steel") diff --git a/code/game/objects/items/spellbooks/legendary.dm b/code/game/objects/items/spellbooks/legendary.dm new file mode 100644 index 00000000000..4551d002ccf --- /dev/null +++ b/code/game/objects/items/spellbooks/legendary.dm @@ -0,0 +1,53 @@ +/obj/item/spellbook/legendary/starter + abstract_type = /obj/item/spellbook/legendary/starter + themed_cost_multiplier = 0.72 + themed_cast_speed_multiplier = 1.55 + themed_magnitude_bonus = 0.75 + +/obj/item/spellbook/legendary/starter/apply_themed_bonuses() + if(!themed_form) + return ..() + var/list/flavor = get_theme_flavor(themed_form) + if(flavor[1]) + name = "[initial(name)], [flavor[1]]" + if(flavor[2]) + desc = "[initial(desc)] [flavor[2]]" + var/scale = bookquality / SPELLBOOK_THEME_BASELINE_QUALITY + base_form_points = CEILING(SPELLBOOK_THEME_BASELINE_FORM_POINTS * scale, 1) + base_technique_points = CEILING(SPELLBOOK_THEME_BASELINE_TECHNIQUE_POINTS * scale, 1) + var/themed_amount = CEILING(base_form_points * 0.5, 1) + mastery.adjust_form_mastery_points(themed_amount, FALSE, themed_form) + if(base_form_points - themed_amount) + mastery.adjust_form_points(base_form_points - themed_amount) + mastery.adjust_technique_points(base_technique_points) + AddComponent(/datum/component/spell_modifier, \ + list("[themed_form]" = themed_cost_multiplier), \ + list("[themed_form]" = themed_cast_speed_multiplier), \ + list("[themed_form]" = themed_magnitude_bonus)) + +/obj/item/spellbook/legendary/starter/fire + themed_form = FORM_FIRE + designlist = list("steel") +/obj/item/spellbook/legendary/starter/ice + themed_form = FORM_ICE +/obj/item/spellbook/legendary/starter/lightning + themed_form = FORM_LIGHTNING + designlist = list("steel") +/obj/item/spellbook/legendary/starter/earth + themed_form = FORM_EARTH + designlist = list("steel") +/obj/item/spellbook/legendary/starter/arcane + themed_form = FORM_ARCANE + designlist = list("gem") +/obj/item/spellbook/legendary/starter/death + themed_form = FORM_DEATH + designlist = list("skin") +/obj/item/spellbook/legendary/starter/life + themed_form = FORM_LIFE + designlist = list("mimic") +/obj/item/spellbook/legendary/starter/air + themed_form = FORM_AIR + designlist = list("steel") +/obj/item/spellbook/legendary/starter/water + themed_form = FORM_WATER + designlist = list("steel") diff --git a/code/game/objects/items/spellbooks/master.dm b/code/game/objects/items/spellbooks/master.dm new file mode 100644 index 00000000000..b6c765c6fb5 --- /dev/null +++ b/code/game/objects/items/spellbooks/master.dm @@ -0,0 +1,54 @@ + +/obj/item/spellbook/master/starter + abstract_type = /obj/item/spellbook/master/starter + themed_cost_multiplier = 0.80 + themed_cast_speed_multiplier = 1.40 + themed_magnitude_bonus = 0.5 + +/obj/item/spellbook/master/starter/apply_themed_bonuses() + if(!themed_form) + return ..() + var/list/flavor = get_theme_flavor(themed_form) + if(flavor[1]) + name = "[initial(name)], [flavor[1]]" + if(flavor[2]) + desc = "[initial(desc)] [flavor[2]]" + var/scale = bookquality / SPELLBOOK_THEME_BASELINE_QUALITY + base_form_points = CEILING(SPELLBOOK_THEME_BASELINE_FORM_POINTS * scale, 1) + base_technique_points = CEILING(SPELLBOOK_THEME_BASELINE_TECHNIQUE_POINTS * scale, 1) + var/themed_amount = CEILING(base_form_points * 0.5, 1) + mastery.adjust_form_mastery_points(themed_amount, FALSE, themed_form) + if(base_form_points - themed_amount) + mastery.adjust_form_points(base_form_points - themed_amount) + mastery.adjust_technique_points(base_technique_points) + AddComponent(/datum/component/spell_modifier, \ + list("[themed_form]" = themed_cost_multiplier), \ + list("[themed_form]" = themed_cast_speed_multiplier), \ + list("[themed_form]" = themed_magnitude_bonus)) + +/obj/item/spellbook/master/starter/fire + themed_form = FORM_FIRE + designlist = list("steel") +/obj/item/spellbook/master/starter/ice + themed_form = FORM_ICE +/obj/item/spellbook/master/starter/lightning + themed_form = FORM_LIGHTNING + designlist = list("steel") +/obj/item/spellbook/master/starter/earth + themed_form = FORM_EARTH + designlist = list("steel") +/obj/item/spellbook/master/starter/arcane + themed_form = FORM_ARCANE + designlist = list("gem") +/obj/item/spellbook/master/starter/death + themed_form = FORM_DEATH + designlist = list("skin") +/obj/item/spellbook/master/starter/life + themed_form = FORM_LIFE + designlist = list("mimic") +/obj/item/spellbook/master/starter/air + themed_form = FORM_AIR + designlist = list("steel") +/obj/item/spellbook/master/starter/water + themed_form = FORM_WATER + designlist = list("steel") diff --git a/code/game/objects/items/spellbooks/mid.dm b/code/game/objects/items/spellbooks/mid.dm new file mode 100644 index 00000000000..397742dce74 --- /dev/null +++ b/code/game/objects/items/spellbooks/mid.dm @@ -0,0 +1,53 @@ +/obj/item/spellbook/mid/starter + abstract_type = /obj/item/spellbook/mid/starter + themed_cost_multiplier = 0.94 + themed_cast_speed_multiplier = 1.10 + themed_magnitude_bonus = 0.15 + +/obj/item/spellbook/mid/starter/apply_themed_bonuses() + if(!themed_form) + return ..() + var/list/flavor = get_theme_flavor(themed_form) + if(flavor[1]) + name = "[initial(name)], [flavor[1]]" + if(flavor[2]) + desc = "[initial(desc)] [flavor[2]]" + var/scale = bookquality / SPELLBOOK_THEME_BASELINE_QUALITY + base_form_points = CEILING(SPELLBOOK_THEME_BASELINE_FORM_POINTS * scale, 1) + base_technique_points = CEILING(SPELLBOOK_THEME_BASELINE_TECHNIQUE_POINTS * scale, 1) + var/themed_amount = CEILING(base_form_points * 0.5, 1) + mastery.adjust_form_mastery_points(themed_amount, FALSE, themed_form) + if(base_form_points - themed_amount) + mastery.adjust_form_points(base_form_points - themed_amount) + mastery.adjust_technique_points(base_technique_points) + AddComponent(/datum/component/spell_modifier, \ + list("[themed_form]" = themed_cost_multiplier), \ + list("[themed_form]" = themed_cast_speed_multiplier), \ + list("[themed_form]" = themed_magnitude_bonus)) + +/obj/item/spellbook/mid/starter/fire + themed_form = FORM_FIRE + designlist = list("steel") +/obj/item/spellbook/mid/starter/ice + themed_form = FORM_ICE +/obj/item/spellbook/mid/starter/lightning + themed_form = FORM_LIGHTNING + designlist = list("steel") +/obj/item/spellbook/mid/starter/earth + themed_form = FORM_EARTH + designlist = list("steel") +/obj/item/spellbook/mid/starter/arcane + themed_form = FORM_ARCANE + designlist = list("gem") +/obj/item/spellbook/mid/starter/death + themed_form = FORM_DEATH + designlist = list("skin") +/obj/item/spellbook/mid/starter/life + themed_form = FORM_LIFE + designlist = list("mimic") +/obj/item/spellbook/mid/starter/air + themed_form = FORM_AIR + designlist = list("steel") +/obj/item/spellbook/mid/starter/water + themed_form = FORM_WATER + designlist = list("steel") diff --git a/code/game/objects/items/spellbooks/spellbook.dm b/code/game/objects/items/spellbooks/spellbook.dm new file mode 100644 index 00000000000..c971a6f4054 --- /dev/null +++ b/code/game/objects/items/spellbooks/spellbook.dm @@ -0,0 +1,249 @@ +/obj/item/spellbook + icon = 'icons/roguetown/items/books.dmi' + icon_state = "spellbookbrown_0" + slot_flags = ITEM_SLOT_HIP + firefuel = 2 MINUTES + dropshrink = 0.6 + drop_sound = 'sound/foley/dropsound/book_drop.ogg' + force = 5 + associated_skill = /datum/attribute/skill/misc/reading + name = "tome of the arcyne" + desc = "A crackling, glowing book, filled with runes and symbols that hurt the mind to stare at." + item_weight = 547 GRAMS + /// The mob who owns and originally bound this tome + var/owner = null + /// Up to two additional mobs allowed to read this tome + var/list/allowed_readers = list() + /// Flat quality bonus stored from a crushed gem, consumed on next read + var/stored_gem = FALSE + /// Attunement datum stored from the gem used, applied on next read + var/datum/attunement/stored_attunement + /// Whether the player has chosen a visual style for this book yet + var/picked = FALSE + /// If TRUE, this tome was made from a magic stone rather than a gem and has a reading penalty + var/born_of_rock = FALSE + /// Multiplier for spell points gained per read. Higher = better book. + var/bookquality = 3 + var/datum/spell_mastery/mastery + /// If set, this book is specialized in one form and gets bonus points + spell buffs for it + var/themed_form = null + /// Total innate points this book grants toward its form track on creation + var/base_form_points = 0 + /// Total innate points this book grants toward the (generic) technique pool on creation + var/base_technique_points = 0 + /// Cost multiplier for spells of themed_form (below 1 = cheaper) + var/themed_cost_multiplier = 1 + var/themed_cast_speed_multiplier = 1 + /// Flat magnitude addition for spells of themed_form + var/themed_magnitude_bonus = 0 + + var/list/designlist = list("green", "yellow", "brown") + + var/open = FALSE + +/obj/item/spellbook/Initialize() + . = ..() + mastery = new /datum/spell_mastery(null, src) + apply_themed_bonuses() + RegisterSignal(src, COMSIG_MASTERY_CAST, PROC_REF(check_reader_and_recoil)) + RegisterSignal(src, COMSIG_MASTERY_CHECK_PARENT, PROC_REF(is_open)) + if(length(designlist) == 1) + base_icon_state = "spellbook[designlist[1]]" + update_appearance(UPDATE_ICON_STATE) + picked = TRUE + +/obj/item/spellbook/proc/is_open() + return !open + +/obj/item/spellbook/proc/get_or_make_mastery() + if(!mastery) + mastery = new /datum/spell_mastery(null, src) + return mastery + +/obj/item/spellbook/proc/apply_themed_bonuses() + if(base_form_points > 0) + if(themed_form) + var/themed_amount = CEILING(base_form_points * 0.5, 1) + var/generic_amount = base_form_points - themed_amount + mastery.adjust_form_mastery_points(themed_amount, FALSE, themed_form) + if(generic_amount) + mastery.adjust_form_points(generic_amount) + else + mastery.adjust_form_points(base_form_points) + + if(base_technique_points > 0) + mastery.adjust_technique_points(base_technique_points) + + if(themed_form && (themed_cost_multiplier != 1 || themed_cast_speed_multiplier != 1 || themed_magnitude_bonus != 0)) + AddComponent(/datum/component/spell_modifier, \ + list("[themed_form]" = themed_cost_multiplier), \ + list("[themed_form]" = themed_cast_speed_multiplier), \ + list("[themed_form]" = themed_magnitude_bonus)) + +/// The open and closed states share identical mob prop data, so we return +/// one table regardless of open state. +/obj/item/spellbook/getonmobprop(tag) + . = ..() + if(!tag) + return + switch(tag) + if("gen") + return list( + "shrink" = 0.4, + "sx" = -2, "sy" = -3, + "nx" = 10, "ny" = -2, + "wx" = 1, "wy" = -3, + "ex" = 5, "ey" = -3, + "northabove" = 0, "southabove" = 1, + "eastabove" = 1, "westabove" = 0, + "nturn" = 0, "sturn" = 0, "wturn" = 0, "eturn" = 0, + "nflip" = 0, "sflip" = 0, "wflip" = 0, "eflip" = 0 + ) + if("onbelt") + return list( + "shrink" = 0.3, + "sx" = -2, "sy" = -5, + "nx" = 4, "ny" = -5, + "wx" = 0, "wy" = -5, + "ex" = 2, "ey" = -5, + "nturn" = 0, "sturn" = 0, "wturn" = 0, "eturn" = 0, + "nflip" = 0, "sflip" = 0, "wflip" = 0, "eflip" = 0, + "northabove" = 0, "southabove" = 1, + "eastabove" = 1, "westabove" = 0 + ) + +/obj/item/spellbook/update_icon_state() + . = ..() + icon_state = "[base_icon_state]_[open]" + +/obj/item/spellbook/attack_self(mob/user, list/modifiers) + if(!open) + attack_hand_secondary(user, modifiers) + return + if(SEND_SIGNAL(src, COMSIG_ITEM_ATTACK_SELF, user, modifiers) & COMPONENT_CANCEL_ATTACK_CHAIN) + return TRUE + ..() + user.update_inv_hands() + +/obj/item/spellbook/attack_self_secondary(mob/user, list/modifiers) + . = ..() + if(. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN) + return + attack_hand_secondary(user, modifiers) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +/obj/item/spellbook/attack_hand_secondary(mob/user, list/modifiers) + if(!picked) + var/the_time = world.time + var/design = input(user, "Select a design.", "Spellbook Design") as null|anything in designlist + if(!design || world.time > (the_time + 30 SECONDS)) + return + base_icon_state = "spellbook[design]" + update_appearance(UPDATE_ICON_STATE) + picked = TRUE + return + + if(owner == null) + owner = user + if(!open) + slot_flags &= ~ITEM_SLOT_HIP + open = TRUE + playsound(src, 'sound/items/book_open.ogg', 100, FALSE, -1) + SEND_SIGNAL(src, COMSIG_MASTERY_ADD_SPELLS, user) + else + slot_flags |= ITEM_SLOT_HIP + open = FALSE + playsound(src, 'sound/items/book_close.ogg', 100, FALSE, -1) + SEND_SIGNAL(src, COMSIG_MASTERY_REMOVE_SPELLS, user) + update_appearance(UPDATE_ICON_STATE) + user.update_inv_hands() + +/// Punishes anyone casting a spell sourced from this book who isn't the owner or an +/// allowed reader. Does not prevent the cast - only recoil()s them. +/obj/item/spellbook/proc/check_reader_and_recoil(datum/source, mob/living/cast_on) + SIGNAL_HANDLER + var/mob/living/user = loc + if(!istype(user)) + return + if(!user) + return + if(user == owner) + return + if(user in allowed_readers) + return + addtimer(CALLBACK(src, PROC_REF(recoil), user), 0.1 SECONDS) + +/obj/item/spellbook/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers) + if(!isliving(interacting_with)) + return NONE + + var/mob/living/target = interacting_with + if(target.stat == DEAD) + target.visible_message(span_danger("[user] smacks [target]'s lifeless corpse with [src].")) + playsound(src, "punch", 25, TRUE, -1) + return + + if(user == target) + to_chat(user, span_warning("I'm already chained to this tome!")) + return + + target.visible_message( + span_danger("[user] beats [target] over the head with [src]!"), + span_danger("[user] beats [target] over the head with [src]!") + ) + + if(length(allowed_readers) > 2 || (target in allowed_readers)) + to_chat(user, span_smallnotice("I can't chain [target.p_them()] to my tome...")) + return + + allowed_readers += target + + playsound(src, "punch", 25, TRUE, -1) + + +/obj/item/spellbook/proc/recoil(mob/user) + user.visible_message(span_warning("[src] shoots out a spark of angry, arcyne energy at [user]!")) + var/mob/living/gamer = user + gamer.electrocute_act(5, src) + +/obj/item/spellbook/horrible + name = "poorly made tome of the arcyne" + desc = "A poorly made book, it barely glows with arcyne and has only small notes on arcyne symbols." + bookquality = 1 + sellprice = 15 + +/obj/item/spellbook/mid + name = "beginners tome of the arcyne" + desc = "An obviously handcrafted book, it glows occasionally with arcyne and has a meager amount of notes on arcyne symbols." + bookquality = 2 + sellprice = 30 + +/obj/item/spellbook/apprentice + name = "apprentice tome of the arcyne" + desc = "A carefully made book, faintly glowing with arcyne and half filled with notes and theory on arcyne symbols." + bookquality = 3 + sellprice = 75 + +/obj/item/spellbook/adept + name = "adept tome of the arcyne" + desc = "A well-made book, it shines moderately with arcyne light. It has been filled with notes of varying degrees on the arcyne." + bookquality = 4 + sellprice = 150 + +/obj/item/spellbook/expert + name = "expert tome of the arcyne" + desc = "A well cared for book, shining brightly with arcyne. It has many runes and arcyne symbols scribed within, with detailed notes." + bookquality = 6 + sellprice = 200 + +/obj/item/spellbook/master + name = "masterful tome of the arcyne" + desc = "A crackling, glowing book, filled with advanced arcyne runes and symbols that hurt the mind to stare at. A true master of the arcyne has left their mark behind." + bookquality = 8 + sellprice = 250 + +/obj/item/spellbook/legendary + name = "legendary tome of the arcyne" + desc = "An incredible book that gives off glowing arcyne motes, it is filled with runes and arcyne theories that is hard for even masters of arcyne to understand. The arcyne script glows and practically whispers from the page..." + bookquality = 12 + sellprice = 400 diff --git a/code/game/objects/items/unfiinshed_spellbook.dm b/code/game/objects/items/unfiinshed_spellbook.dm deleted file mode 100644 index 923ec78bada..00000000000 --- a/code/game/objects/items/unfiinshed_spellbook.dm +++ /dev/null @@ -1,172 +0,0 @@ - -/obj/item/spellbook_unfinished - name = "bound scrollpaper" - dropshrink = 0.6 - icon = 'icons/roguetown/items/books.dmi' - icon_state = "basic_book_0" - desc = "Thick scroll paper bound at the spine. It lacks pages." - throw_speed = 1 - throw_range = 5 - w_class = WEIGHT_CLASS_NORMAL - attack_verb = list("bashed", "whacked", "educated") - resistance_flags = FLAMMABLE - drop_sound = 'sound/foley/dropsound/book_drop.ogg' - pickup_sound = 'sound/blank.ogg' - /// Pages still needed before the binding is complete - var/pages_left = 4 - -/obj/item/spellbook_unfinished/pre_arcyne - name = "tome in waiting" - icon_state = "spellbook_unfinished" - desc = "A fully bound tome of scroll paper. It's lacking a certain arcyne energy." - -/obj/item/spellbook_unfinished/item_interaction(mob/living/user, obj/item/tool, list/modifiers) - if(!istype(tool, /obj/item/paper/scroll)) - return NONE - - if(!isturf(loc) || !locate(/obj/structure/table) in loc) - to_chat(user, "You need to put the [src] on a table to work on it.") - return ITEM_INTERACT_BLOCKING - - var/crafttime = max(0, 60 - GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) * 5) - if(!do_after(user, crafttime, target = src)) - return ITEM_INTERACT_BLOCKING - - pages_left-- - if(pages_left > 0) - playsound(src, 'sound/items/book_page.ogg', 100, TRUE) - to_chat(user, span_notice("[pages_left] left...")) - qdel(tool) - return ITEM_INTERACT_SUCCESS - - //promote to pre_arcyne. - playsound(src, 'sound/items/book_open.ogg', 100, TRUE) - if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) > SKILL_LEVEL_NONE) - to_chat(user, span_notice("The book is bound. I must find a catalyst to channel the arcyne into it now.")) - else - to_chat(user, span_notice("I've made an empty book of thick, useless scroll paper. I can't even thumb through it!")) - new /obj/item/spellbook_unfinished/pre_arcyne(loc) - qdel(tool) - qdel(src) - return ITEM_INTERACT_SUCCESS - -/obj/item/spellbook_unfinished/pre_arcyne/item_interaction(mob/living/user, obj/item/tool, list/modifiers) - if(istype(tool, /obj/item/gem/amethyst)) - user.visible_message(span_notice("I run my arcyne energy into the crystal. Its artificial lattices pulse and then fall dormant. It must not be strong enough to make a spellbook with!")) - return ITEM_INTERACT_BLOCKING - - var/found_table = locate(/obj/structure/table) in loc - if(isturf(loc) && !found_table) - to_chat(user, "You need to put the [src] on a table to work on it.") - return ITEM_INTERACT_SUCCESS - - if(istype(tool, /obj/item/gem/violet)) - apply_gem_catalyst(user, tool, /obj/item/book/granter/spellbook/expert, found_table) - return ITEM_INTERACT_SUCCESS - - if(istype(tool, /obj/item/gem)) - apply_gem_catalyst(user, tool, /obj/item/book/granter/spellbook/adept, found_table) - return ITEM_INTERACT_SUCCESS - - if(istype(tool, /obj/item/natural/stone)) - var/obj/item/natural/stone/the_rock = tool - if(!the_rock.magic_power) - to_chat(user, span_notice("This is a mere rock — it has no arcyne potential. Bah!")) - return ITEM_INTERACT_BLOCKING - apply_stone_catalyst(user, the_rock, found_table) - return ITEM_INTERACT_SUCCESS - - if(istype(tool, /obj/item/natural/melded)) - var/obj/item/natural/melded/meld = tool - apply_melded_catalyst(user, tool, meld.melded_quality, meld.shock_damage) - return ITEM_INTERACT_SUCCESS - -/// Spawns a finished book, sets owner, and cleans up catalyst + self. -/obj/item/spellbook_unfinished/pre_arcyne/proc/finish_book(mob/user, obj/item/catalyst, book_type, born_of_rock = FALSE, extra_desc = null) - playsound(src, 'sound/magic/crystal.ogg', 100, TRUE) - var/obj/item/book/granter/spellbook/newbook = new book_type(get_turf(loc)) - var/atom/old_loc = loc - newbook.owner = user - if(born_of_rock) - newbook.born_of_rock = TRUE - if(extra_desc) - newbook.desc += extra_desc - qdel(catalyst) - qdel(src) - if(ismob(old_loc)) - var/mob/living/mob = old_loc - mob.put_in_hands(newbook) - -/// Handles gem catalysts. Unskilled readers just get a cosmetic press. -/obj/item/spellbook_unfinished/pre_arcyne/proc/apply_gem_catalyst(mob/user, obj/item/gem/gem, book_type, found_table) - if(!do_after(user, max(0, 100 - GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) * 5), target = src)) - return - if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) > SKILL_LEVEL_NONE) - user.visible_message( - span_warning("[user] crushes [user.p_their()] [gem]! Its powder seeps into the [src]."), - span_notice("I run my arcyne energy into the crystal. It shatters and seeps into the cover of the tome! Runes and symbols of an unknowable language cover its pages now...") - ) - finish_book(user, gem, book_type) - else - to_chat(user, span_notice("I press the gem into the cover of the book. What a pretty design this would make!")) - return TRUE - -/// Handles magic stone catalysts. Quality tier is determined by magic_power. -/// Unskilled users have a prob() chance; failure destroys the stone and shocks. -/obj/item/spellbook_unfinished/pre_arcyne/proc/apply_stone_catalyst(mob/living/user, obj/item/natural/stone/the_rock, found_table) - var/crafttime = max(0, (130 - the_rock.magic_power) - GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) * 5) - if(!do_after(user, crafttime, target = src)) - return - - var/book_type = stone_quality_to_book(the_rock.magic_power) - - if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) > SKILL_LEVEL_NONE) - user.visible_message( - span_warning("[user] crushes [user.p_their()] [the_rock]! Its powder seeps into the [src]."), - span_notice("I join my arcyne energy with that of the magical stone in my hands, which shudders briefly before dissolving into motes of ash. Runes and symbols of an unknowable language cover its pages now...") - ) - to_chat(user, span_notice("...yet even for an enigma of the arcyne, these characters are unlike anything I've seen before. They're going to be -much- harder to understand...")) - finish_book(user, the_rock, book_type, born_of_rock = TRUE, extra_desc = " Traces of multicolored stone limn its margins.") - else - // Unskilled: prob chance proportional to magic_power (capped at ~15%). - if(prob(the_rock.magic_power * 5)) - user.visible_message( - span_warning("[user] carefully sets down [the_rock] upon [src]. Nothing happens for a moment or three, then suddenly, the glow surrounding the stone becomes as liquid, seeps down and soaks into the tome!"), - span_notice("I knew this stone was special! Its colourful magick has soaked into my tome and given me gift of mystery!") - ) - to_chat(user, span_notice("...what in the world does any of this scribbling possibly mean?")) - finish_book(user, the_rock, book_type, born_of_rock = TRUE, extra_desc = " Traces of multicolored stone limn its margins.") - else - user.visible_message( - span_warning("[user] sets down [the_rock] upon the surface of [src] and watches expectantly. Without warning, the rock violently pops like a squashed gourd!"), - span_notice("No! My precious stone! It mustn't have wanted to share its mysteries with me...") - ) - user.electrocute_act(5, src) - qdel(the_rock) - -/// Maps a stone's magic_power to the appropriate book subtype. -/obj/item/spellbook_unfinished/pre_arcyne/proc/stone_quality_to_book(magic_power) - if(magic_power >= 10) - return /obj/item/book/granter/spellbook/apprentice - if(magic_power > 5) - return /obj/item/book/granter/spellbook/mid - return /obj/item/book/granter/spellbook/horrible - -/// Handles melded catalysts. Unskilled users take escalating shock damage. -/obj/item/spellbook_unfinished/pre_arcyne/proc/apply_melded_catalyst(mob/living/user, obj/item/P, book_type, shock_damage) - if(!do_after(user, max(0, 100 - GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) * 5), target = src)) - return - if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) > SKILL_LEVEL_NONE) - user.visible_message( - span_warning("[user] imbues [user.p_their()] [P]! It fuses into the [src]."), - span_notice("I join my arcyne energy with that of the [P] in my hands, which shudders briefly before dissolving into motes of energy. Runes and symbols of an unknowable language cover its pages now...") - ) - to_chat(user, span_notice("...yet even for an enigma of the arcyne, these characters are unlike anything I've seen before. They're going to be -much- harder to understand...")) - finish_book(user, P, book_type) - else - user.visible_message( - span_warning("[user] sets down [P] upon the surface of [src] and watches expectantly. Without warning, the [P] violently explodes!"), - span_notice("I should have known messing with the arcyne was dangerous!") - ) - user.electrocute_act(shock_damage, src) - qdel(P) diff --git a/code/game/objects/items/unfinished_spellbook/session.dm b/code/game/objects/items/unfinished_spellbook/session.dm new file mode 100644 index 00000000000..8a931ac0251 --- /dev/null +++ b/code/game/objects/items/unfinished_spellbook/session.dm @@ -0,0 +1,256 @@ +/proc/merge_spellcraft_multipliers(list/target, list/source, amplifier = 1) + for(var/key in source) + var/base = target[key] || 1 + var/scaled_source = 1 + (source[key] - 1) * amplifier + target[key] = base * scaled_source + +/proc/merge_spellcraft_additions(list/target, list/source) + for(var/key in source) + target[key] = (target[key] || 0) + source[key] + +/datum/spellcraft_session + var/obj/item/spellbook_unfinished/pre_arcyne/book_base + var/obj/item/meld + var/list/materials = list(null, null, null, null, null, null) + var/mob/living/user + +/datum/spellcraft_session/proc/try_insert_meld(obj/item/item, mob/living/user) + if(meld) + to_chat(user, span_warning("The center is already occupied.")) + return FALSE + if(!item.get_spellcraft_meld_data()) + to_chat(user, span_warning("[item] doesn't feel arcyne enough to be a catalyst.")) + return FALSE + meld = item + user.temporarilyRemoveItemFromInventory(item) + return TRUE + +/datum/spellcraft_session/proc/try_insert_material(index, obj/item/item, mob/living/user) + if(materials[index]) + return FALSE + var/contribution = item.get_spellcraft_contribution() + if(!contribution) + to_chat(user, span_warning("[item] has no arcyne properties.")) + return FALSE + materials[index] = item + user.temporarilyRemoveItemFromInventory(item) + return TRUE + +/datum/spellcraft_session/proc/assemble(mob/living/user) + if(!can_assemble()) + return FALSE + var/list/meld_data = meld.get_spellcraft_meld_data() + if(!meld_data) + return FALSE + + var/born_of_rock = !!meld_data["born_of_rock"] + var/base_crafttime = 100 - (meld_data["crafttime_bonus"] || 0) + var/crafttime = max(0, base_crafttime - GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) * 5) + if(!do_after(user, crafttime, target = book_base)) + return FALSE + + var/list/form_points = list() + var/list/technique_points = list() + var/list/form_cost_multipliers = list() + var/list/form_cast_speed_multipliers = list() + var/list/form_magnitude_modifications = list() + var/list/technique_cost_multipliers = list() + var/list/technique_cast_speed_multipliers = list() + var/list/technique_magnitude_modifications = list() + var/amplifier = meld_data["amplifier"] || 1 + + for(var/obj/item/mat in materials) + if(!mat) + continue + var/datum/spellcraft_contribution/contribution = mat.get_spellcraft_contribution() + if(!contribution) + continue + for(var/form in contribution.form_points) + form_points[form] = (form_points[form] || 0) + contribution.form_points[form] * amplifier + for(var/tech in contribution.technique_points) + technique_points[tech] = (technique_points[tech] || 0) + contribution.technique_points[tech] * amplifier + merge_spellcraft_multipliers(form_cost_multipliers, contribution.form_cost_multipliers, amplifier) + merge_spellcraft_multipliers(form_cast_speed_multipliers, contribution.form_cast_speed_multipliers, amplifier) + merge_spellcraft_additions(form_magnitude_modifications, contribution.form_magnitude_modifications) + merge_spellcraft_multipliers(technique_cost_multipliers, contribution.technique_cost_multipliers, amplifier) + merge_spellcraft_multipliers(technique_cast_speed_multipliers, contribution.technique_cast_speed_multipliers, amplifier) + merge_spellcraft_additions(technique_magnitude_modifications, contribution.technique_magnitude_modifications) + var/book_type = meld_data["book_type"] + var/skilled = GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) > SKILL_LEVEL_NONE + + if(!skilled) + switch(meld_data["unskilled_behavior"]) + if("cosmetic") + to_chat(user, span_notice("I press [meld] into the cover of the book. What a pretty design this would make!")) + return FALSE + + if("fumble_chance") + if(prob(meld_data["unskilled_prob"] || 0)) + user.visible_message( + span_warning("[user] carefully sets down [meld] upon [book_base]. Nothing happens for a moment or three, then the glow surrounding it becomes as liquid, seeping down into the tome!"), + span_notice("I knew this stone was special! Its colourful magick has soaked into my tome and given me gift of mystery!") + ) + to_chat(user, span_notice("...what in the world does any of this scribbling possibly mean?")) + var/obj/item/spellbook/newbook = book_base.finish_book(user, meld, book_type, born_of_rock, meld_data["extra_desc"]) + apply_crafting_bonuses(newbook, meld_data, form_points, technique_points, \ + form_cost_multipliers, form_cast_speed_multipliers, form_magnitude_modifications, \ + technique_cost_multipliers, technique_cast_speed_multipliers, technique_magnitude_modifications) + clear_materials() + return TRUE + user.visible_message( + span_warning("[user] sets down [meld] upon the surface of [book_base] and watches expectantly. Without warning, it violently pops like a squashed gourd!"), + span_notice("No! My precious [meld]! It mustn't have wanted to share its mysteries with me...") + ) + user.electrocute_act(meld_data["shock_damage"] || 5, book_base) + qdel(meld) + meld = null + clear_materials() + return TRUE + + else + user.visible_message( + span_warning("[user] sets down [meld] upon the surface of [book_base] and watches expectantly. Without warning, [meld] violently explodes!"), + span_notice("I should have known messing with the arcyne was dangerous!") + ) + user.electrocute_act(meld_data["shock_damage"] || 5, book_base) + qdel(meld) + meld = null + clear_materials() + return TRUE + + user.visible_message( + span_warning("[user] imbues [user.p_their()] [meld]! It fuses into [book_base]."), + span_notice("I join my arcyne energy with that of the [meld] in my hands, which shudders briefly before dissolving into motes of energy...") + ) + var/obj/item/spellbook/newbook = book_base.finish_book(user, meld, book_type, born_of_rock, meld_data["extra_desc"]) + apply_crafting_bonuses(newbook, meld_data, form_points, technique_points, \ + form_cost_multipliers, form_cast_speed_multipliers, form_magnitude_modifications, \ + technique_cost_multipliers, technique_cast_speed_multipliers, technique_magnitude_modifications) + newbook.owner = user + clear_materials() + return TRUE + +/datum/spellcraft_session/proc/apply_crafting_bonuses(obj/item/spellbook/newbook, list/meld_data, \ + list/form_points, list/technique_points, list/form_cost_multipliers, list/form_cast_speed_multipliers, \ + list/form_magnitude_modifications, list/technique_cost_multipliers, list/technique_cast_speed_multipliers, \ + list/technique_magnitude_modifications) + + if(!newbook) + return + + var/has_modifiers = length(form_cost_multipliers) || length(form_cast_speed_multipliers) || length(form_magnitude_modifications) \ + || length(technique_cost_multipliers) || length(technique_cast_speed_multipliers) || length(technique_magnitude_modifications) + if(has_modifiers) + newbook.AddComponent(/datum/component/spell_modifier, \ + form_cost_multipliers, form_cast_speed_multipliers, form_magnitude_modifications, \ + technique_cost_multipliers, technique_cast_speed_multipliers, technique_magnitude_modifications) + + var/datum/spell_mastery/mastery = newbook.get_or_make_mastery() + if(!mastery) + return + + var/generic_points = meld_data["generic_points"] || 0 + var/generic_points_form = meld_data["generic_points_form"] || 0 + if(generic_points_form) + mastery.adjust_form_points(generic_points_form) + if(generic_points) + mastery.adjust_technique_points(generic_points) + + for(var/form in form_points) + mastery.invest_form(form, form_points[form]) + mastery.unspent_form_points += form_points[form] + for(var/tech in technique_points) + mastery.invest_technique(tech, technique_points[tech]) + mastery.unspent_technique_points += technique_points[tech] + +/datum/spellcraft_session/proc/eject_meld(mob/living/user) + if(!meld) + return FALSE + user.put_in_hands(meld) + meld = null + return TRUE + +/datum/spellcraft_session/proc/eject_material(index, mob/living/user) + if(!materials[index]) + return FALSE + user.put_in_hands(materials[index]) + materials[index] = null + return TRUE + +/datum/visual_ui/spellcraft_star/valid() + var/obj/item/spellbook_unfinished/pre_arcyne/book = get_book() + if(!book) + return FALSE + var/mob/user = get_user() + if(!user || !user.Adjacent(book)) + return FALSE + return TRUE + +/datum/spellcraft_session/proc/clear_materials() + for(var/i in 1 to length(materials)) + var/obj/item/mat = materials[i] + if(mat) + qdel(mat) + materials[i] = null + +/datum/spellcraft_session/proc/can_assemble() + return !!meld + +///behold shitcode +/obj/item/proc/get_spellcraft_meld_data() + return null + +/obj/item/gem/get_spellcraft_meld_data() + return list( + "book_type" = /obj/item/spellbook/adept, + "amplifier" = 1.0, + "generic_points" = 0, + "generic_points_form" = 0, + "unskilled_behavior" = "cosmetic", // unskilled users just admire it, nothing consumed + ) + +/obj/item/gem/violet/get_spellcraft_meld_data() + return list( + "book_type" = /obj/item/spellbook/expert, + "amplifier" = 1.2, + "generic_points" = 0, + "generic_points_form" = 0, + "unskilled_behavior" = "cosmetic", + ) + +/obj/item/natural/stone/get_spellcraft_meld_data() + if(!magic_power) + return null + return list( + "book_type" = stone_quality_to_book(), + "amplifier" = 1.0, + "generic_points" = round(magic_power / 4), + "generic_points_form" = round(magic_power / 2), + "crafttime_bonus" = magic_power, // faster to bind, mirrors the old (130 - magic_power) formula + "shock_damage" = 5, + "born_of_rock" = TRUE, + "extra_desc" = " Traces of multicolored stone limn its margins.", + "unskilled_behavior" = "fumble_chance", + "unskilled_prob" = magic_power * 5, + ) + +/obj/item/natural/melded/get_spellcraft_meld_data() + return list( + "book_type" = melded_quality, + "amplifier" = 1.0, + "generic_points" = round(shock_damage / 20), + "generic_points_form" = round(shock_damage / 10), + "shock_damage" = shock_damage, + ) + +/// Moved off spellbook_unfinished/pre_arcyne since the stone itself is the natural owner of this logic now. +/obj/item/natural/stone/proc/stone_quality_to_book() + if(magic_power >= 10) + return /obj/item/spellbook/apprentice + if(magic_power > 5) + return /obj/item/spellbook/mid + return /obj/item/spellbook/horrible + + +/obj/item/proc/get_spellcraft_contribution() + return GLOB.spellcraft_contributions[type] diff --git a/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/_base.dm b/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/_base.dm new file mode 100644 index 00000000000..f00dbaa4422 --- /dev/null +++ b/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/_base.dm @@ -0,0 +1,93 @@ + +GLOBAL_LIST_INIT(spellcraft_contributions, build_spellcraft_contributions()) + +GLOBAL_LIST_INIT(spellcraft_items, build_spellcraft_items()) + +/proc/build_spellcraft_items() + . = list() + for(var/contribution_type in typesof(/datum/spellcraft_contribution)) + var/datum/spellcraft_contribution/contribution = new contribution_type() + if(!contribution.holder) + continue + .[contribution.type] = contribution + +/proc/build_spellcraft_contributions() + . = list() + for(var/contribution_type in typesof(/datum/spellcraft_contribution)) + var/datum/spellcraft_contribution/contribution = new contribution_type() + if(!contribution.atom_path) + continue + if(contribution.holder) + continue + if(contribution.include_subtypes) + for(var/sub_path in typesof(contribution.atom_path)) + .[sub_path] = contribution + else + .[contribution.atom_path] = contribution + +/datum/spellcraft_contribution + abstract_type = /datum/spellcraft_contribution + ///are we a spellcraft piece or a holder? + var/holder = FALSE + /// The atom typepath this contribution is registered under. + var/atom/atom_path + /// If TRUE, all subtypes of atom_path get mapped to this same singleton too. + var/include_subtypes = FALSE + + /// FORM_X = amount + var/list/form_points = list() + /// TECHNIQUE_X = amount + var/list/technique_points = list() + var/list/form_cost_multipliers = list() + var/list/form_cast_speed_multipliers = list() + var/list/form_magnitude_modifications = list() + var/list/technique_cost_multipliers = list() + var/list/technique_cast_speed_multipliers = list() + var/list/technique_magnitude_modifications = list() + +/datum/spellcraft_contribution/proc/is_empty() + return !length(form_points) && !length(technique_points) \ + && !length(form_cost_multipliers) && !length(form_cast_speed_multipliers) && !length(form_magnitude_modifications) \ + && !length(technique_cost_multipliers) && !length(technique_cast_speed_multipliers) && !length(technique_magnitude_modifications) + +/datum/spellcraft_contribution/return_recipe_data(atom/source_path) + var/list/data = list() + data["type"] = holder ? "spellcraft-item" : "spellcraft" + data["name"] = initial(atom_path.name) + data["category"] = holder ? "Spellcraft Item Stats" : "Spellcraft" + data["output_icon"] = "[initial(atom_path.icon)]" + data["output_state"] = "[initial(atom_path.icon_state)]" + data["_output_path"] = "[source_path]" + data["holder"] = holder + + var/list/forms = list() + for(var/form in GLOB.all_forms) + var/list/entry = list("name" = form, "color" = GLOB.form_colors[form]) + if(form_points[form]) + entry["points"] = form_points[form] + if(form_cost_multipliers[form]) + entry["cost_mult"] = form_cost_multipliers[form] + if(form_cast_speed_multipliers[form]) + entry["speed_mult"] = form_cast_speed_multipliers[form] + if(form_magnitude_modifications[form]) + entry["magnitude_mod"] = form_magnitude_modifications[form] + if(length(entry) > 2) // more than just name+color means it actually has data + forms += list(entry) + data["forms"] = forms + + var/list/techniques = list() + for(var/technique in GLOB.all_techniques) + var/list/entry = list("name" = technique) + if(technique_points[technique]) + entry["points"] = technique_points[technique] + if(technique_cost_multipliers[technique]) + entry["cost_mult"] = technique_cost_multipliers[technique] + if(technique_cast_speed_multipliers[technique]) + entry["speed_mult"] = technique_cast_speed_multipliers[technique] + if(technique_magnitude_modifications[technique]) + entry["magnitude_mod"] = technique_magnitude_modifications[technique] + if(length(entry) > 1) + techniques += list(entry) + data["techniques"] = techniques + + return data diff --git a/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/earth.dm b/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/earth.dm new file mode 100644 index 00000000000..63eeae45cb7 --- /dev/null +++ b/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/earth.dm @@ -0,0 +1,69 @@ +/datum/spellcraft_contribution/fairydust + atom_path = /obj/item/natural/fairydust + form_points = list( + FORM_EARTH = 1, + ) + form_cost_multipliers = list( + FORM_EARTH = 0.9, + FORM_LIGHTNING = 1.2, + ) + +/datum/spellcraft_contribution/earthdust + atom_path = /obj/item/alch/earthdust + form_points = list( + FORM_EARTH = 1, + ) + form_cast_speed_multipliers = list( + FORM_EARTH = 1.1, + FORM_LIGHTNING = 0.8, + ) + +/datum/spellcraft_contribution/iridescentscale + atom_path = /obj/item/natural/iridescentscale + form_points = list( + FORM_EARTH = 1, + ) + form_magnitude_modifications = list( + FORM_EARTH = 0.1, + FORM_LIGHTNING = -0.1, + ) + +/datum/spellcraft_contribution/heartwoodcore + atom_path = /obj/item/natural/heartwoodcore + form_points = list( + FORM_EARTH = 2, + ) + form_cost_multipliers = list( + FORM_EARTH = 1.2, + FORM_LIGHTNING = 1.2, + ) + technique_points = list( + TECHNIQUE_CREATION = 1, + ) + +/datum/spellcraft_contribution/sylvanessence + atom_path = /obj/item/natural/sylvanessence + form_points = list( + FORM_EARTH = 4, + ) + form_cost_multipliers = list( + FORM_EARTH = 1.4, + FORM_LIGHTNING = 2, + ) + technique_points = list( + TECHNIQUE_CREATION = 2, + TECHNIQUE_IMBUE = 1, + ) + + +/datum/spellcraft_contribution/stone + atom_path = /obj/item/natural/stone + form_cost_multipliers = list( + FORM_EARTH = 0.95, + ) + form_cast_speed_multipliers = list( + FORM_EARTH = 1.05, + ) + form_magnitude_modifications = list( + FORM_EARTH = 0.05, + ) diff --git a/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/elemental.dm b/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/elemental.dm new file mode 100644 index 00000000000..28371dfa4cd --- /dev/null +++ b/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/elemental.dm @@ -0,0 +1,78 @@ +/datum/spellcraft_contribution/elementalmote + atom_path = /obj/item/natural/elementalmote + form_cost_multipliers = list( + FORM_LIGHTNING = 0.9, + FORM_WATER = 0.9, + FORM_ICE = 0.9, + FORM_AIR = 0.9, + FORM_EARTH = 1.2, + ) + +/datum/spellcraft_contribution/elementalshard + atom_path = /obj/item/natural/elementalshard + form_cast_speed_multipliers = list( + FORM_LIGHTNING = 1.1, + FORM_WATER = 1.1, + FORM_ICE = 1.1, + FORM_AIR = 1.1, + FORM_EARTH = 0.8, + ) + +/datum/spellcraft_contribution/elementalfragment + atom_path = /obj/item/natural/iridescentscale + form_magnitude_modifications = list( + FORM_LIGHTNING = 0.1, + FORM_WATER = 0.1, + FORM_ICE = 0.1, + FORM_AIR = 0.1, + FORM_EARTH = -0.2, + ) + +/datum/spellcraft_contribution/elementalrelic + atom_path = /obj/item/natural/heartwoodcore + form_cost_multipliers = list( + FORM_EARTH = 1.2, + FORM_LIGHTNING = 1.2, + FORM_WATER = 1.2, + FORM_AIR = 1.2, + FORM_ICE = 1.2, + ) + technique_points = list( + TECHNIQUE_ALTERATION = 1, + ) + +/datum/spellcraft_contribution/airdust + atom_path = /obj/item/alch/airdust + form_points = list( + FORM_AIR = 2, + ) + +/datum/spellcraft_contribution/waterdust + atom_path = /obj/item/alch/waterdust + form_points = list( + FORM_WATER = 2, + ) + +/datum/spellcraft_contribution/seeddust + atom_path = /obj/item/alch/seeddust + form_points = list( + FORM_LIFE = 2, + ) + +/datum/spellcraft_contribution/feaudust + atom_path = /obj/item/alch/feaudust + form_points = list( + FORM_DEATH = 2, + ) + +/datum/spellcraft_contribution/magicdust + atom_path = /obj/item/alch/magicdust + form_points = list( + FORM_ARCANE = 2, + ) + +/datum/spellcraft_contribution/runedust + atom_path = /obj/item/alch/runedust + form_points = list( + FORM_LIGHTNING = 2, + ) diff --git a/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/fire.dm b/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/fire.dm new file mode 100644 index 00000000000..54d4127d142 --- /dev/null +++ b/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/fire.dm @@ -0,0 +1,59 @@ +/datum/spellcraft_contribution/infernalash + atom_path = /obj/item/natural/infernalash + form_points = list( + FORM_FIRE = 1, + ) + form_cost_multipliers = list( + FORM_FIRE = 0.9, + FORM_WATER = 1.2, + ) + +/datum/spellcraft_contribution/hellhoundfang + atom_path = /obj/item/natural/hellhoundfang + form_points = list( + FORM_FIRE = 1, + ) + form_cast_speed_multipliers = list( + FORM_FIRE = 1.1, + FORM_WATER = 0.8, + ) + +/datum/spellcraft_contribution/moltencore + atom_path = /obj/item/natural/moltencore + form_points = list( + FORM_FIRE = 2, + ) + form_cost_multipliers = list( + FORM_FIRE = 1.2, + FORM_WATER = 1.2, + ) + technique_points = list( + TECHNIQUE_DESTRUCTION = 1, + ) + +/datum/spellcraft_contribution/abyssalflame + atom_path = /obj/item/natural/abyssalflame + form_points = list( + FORM_FIRE = 4, + ) + form_cost_multipliers = list( + FORM_FIRE = 1.4, + FORM_WATER = 2, + ) + technique_points = list( + TECHNIQUE_DESTRUCTION = 2, + TECHNIQUE_IMBUE = 1, + ) + + +/datum/spellcraft_contribution/firedust + atom_path = /obj/item/alch/firedust + form_cost_multipliers = list( + FORM_FIRE = 0.95, + ) + form_cast_speed_multipliers = list( + FORM_FIRE = 0.95, + ) + form_magnitude_modifications = list( + FORM_FIRE = 0.05, + ) diff --git a/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/items/_base.dm b/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/items/_base.dm new file mode 100644 index 00000000000..4f28e1cb7f7 --- /dev/null +++ b/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/items/_base.dm @@ -0,0 +1,3 @@ +/datum/spellcraft_contribution/item + abstract_type = /datum/spellcraft_contribution/item + holder = TRUE diff --git a/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/special.dm b/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/special.dm new file mode 100644 index 00000000000..31fe371f21e --- /dev/null +++ b/code/game/objects/items/unfinished_spellbook/spellcraft_contributions/special.dm @@ -0,0 +1,18 @@ +/datum/spellcraft_contribution/voidstone + atom_path = /obj/item/natural/voidstone + + technique_points = list( + TECHNIQUE_DESTRUCTION = 1, + TECHNIQUE_CREATION = 1, + TECHNIQUE_ALTERATION = 1, + TECHNIQUE_SUMMONING = 1, + TECHNIQUE_RESTORATION = 1, + ) + +/datum/spellcraft_contribution/bone + atom_path = /obj/item/alch/bone + + form_cost_multipliers = list( + FORM_DEATH = 0.9, + FORM_LIFE = 1.2, + ) diff --git a/code/game/objects/items/unfinished_spellbook/unfiinshed_spellbook.dm b/code/game/objects/items/unfinished_spellbook/unfiinshed_spellbook.dm new file mode 100644 index 00000000000..22fe9c596fd --- /dev/null +++ b/code/game/objects/items/unfinished_spellbook/unfiinshed_spellbook.dm @@ -0,0 +1,92 @@ + +/obj/item/spellbook_unfinished + name = "bound scrollpaper" + dropshrink = 0.6 + icon = 'icons/roguetown/items/books.dmi' + icon_state = "basic_book_0" + desc = "Thick scroll paper bound at the spine. It lacks pages." + throw_speed = 1 + throw_range = 5 + w_class = WEIGHT_CLASS_NORMAL + attack_verb = list("bashed", "whacked", "educated") + resistance_flags = FLAMMABLE + drop_sound = 'sound/foley/dropsound/book_drop.ogg' + pickup_sound = 'sound/blank.ogg' + /// Pages still needed before the binding is complete + var/pages_left = 4 + ///wahhhh + var/datum/spellcraft_session/session + +/obj/item/spellbook_unfinished/pre_arcyne + name = "tome in waiting" + icon_state = "spellbook_unfinished" + desc = "A fully bound tome of scroll paper. It's lacking a certain arcyne energy." + +/obj/item/spellbook_unfinished/pre_arcyne/attack_self(mob/living/user) + if(!session) + session = new /datum/spellcraft_session() + session.book_base = src + session.user = user + var/datum/visual_ui/spellcraft_star/ui + if("spellcraft_star" in user.mind.active_uis) + ui = user.mind.active_uis["spellcraft_star"] + ui.remove_from_client() + user.mind.active_uis -= "spellcraft_star" + qdel(ui) + return + else + ui = new(user.mind, src) + ui.display() + +/obj/item/spellbook_unfinished/item_interaction(mob/living/user, obj/item/tool, list/modifiers) + if(!istype(tool, /obj/item/paper/scroll)) + return NONE + + if(!isturf(loc) || !locate(/obj/structure/table) in loc) + to_chat(user, "You need to put the [src] on a table to work on it.") + return ITEM_INTERACT_BLOCKING + + var/crafttime = max(0, 60 - GET_MOB_SKILL_VALUE_OLD(user, /datum/attribute/skill/magic/arcane) * 5) + if(!do_after(user, crafttime, target = src)) + return ITEM_INTERACT_BLOCKING + + pages_left-- + if(pages_left > 0) + playsound(src, 'sound/items/book_page.ogg', 100, TRUE) + to_chat(user, span_notice("[pages_left] left...")) + qdel(tool) + return ITEM_INTERACT_SUCCESS + + //promote to pre_arcyne. + playsound(src, 'sound/items/book_open.ogg', 100, TRUE) + if(GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) > SKILL_LEVEL_NONE) + to_chat(user, span_notice("The book is bound. I must find a catalyst to channel the arcyne into it now.")) + else + to_chat(user, span_notice("I've made an empty book of thick, useless scroll paper. I can't even thumb through it!")) + new /obj/item/spellbook_unfinished/pre_arcyne(loc) + qdel(tool) + qdel(src) + return ITEM_INTERACT_SUCCESS + +/// Spawns a finished book, sets owner, and cleans up catalyst + self. +/obj/item/spellbook_unfinished/pre_arcyne/proc/finish_book(mob/user, obj/item/catalyst, book_type, born_of_rock = FALSE, extra_desc = null) + playsound(src, 'sound/magic/crystal.ogg', 100, TRUE) + var/obj/item/spellbook/newbook = new book_type(get_turf(loc)) + var/atom/old_loc = loc + newbook.owner = user + if(born_of_rock) + newbook.born_of_rock = TRUE + if(extra_desc) + newbook.desc += extra_desc + qdel(catalyst) + qdel(src) + if(ismob(old_loc)) + var/mob/living/mob = old_loc + mob.put_in_hands(newbook) + var/datum/visual_ui/spellcraft_star/ui + if("spellcraft_star" in user.mind.active_uis) + ui = user.mind.active_uis["spellcraft_star"] + ui.remove_from_client() + user.mind.active_uis -= "spellcraft_star" + qdel(ui) + return newbook diff --git a/code/game/objects/structures.dm b/code/game/objects/structures.dm index c4e21318491..0cb75d455f9 100644 --- a/code/game/objects/structures.dm +++ b/code/game/objects/structures.dm @@ -38,12 +38,33 @@ if(ishuman(AM)) var/mob/living/carbon/human/H = AM if(H.dir == get_dir(H,src) && H.m_intent == MOVE_INTENT_RUN && H.body_position != LYING_DOWN) - H.Immobilize(10) - H.apply_damage(15, BRUTE, BODY_ZONE_CHEST, H.run_armor_check("chest", "blunt", damage = 15), damage_type = BCLASS_BLUNT) - H.toggle_rogmove_intent(MOVE_INTENT_WALK, TRUE) - playsound(src, "genblunt", 100, TRUE) - H.visible_message("[H] runs into [src]!", "I run into [src]!") - addtimer(CALLBACK(H, TYPE_PROC_REF(/mob/living/carbon/human, Knockdown), 10), 10) + var/is_bigguy = FALSE + if(HAS_TRAIT(H,TRAIT_BIGGUY)) + if(istype(src,/obj/structure/door)) + var/obj/structure/door/S = src + if(S.smashable) + is_bigguy = TRUE + if(is_bigguy && get_integrity() > max_integrity / 3) + if(max_integrity > 1000) //Custom-set HP door, should be respected + take_damage(max_integrity / 6 + 1) + else + if(GET_MOB_ATTRIBUTE_VALUE(H, STAT_STRENGTH) >= 13) //STR adding role w/ Giant or half-orc, seems fair + take_damage((max_integrity / 3) * 2 + 1) + else + take_damage(max_integrity / 3 + 1) + H.Immobilize(20) + //hurts you a little bit but doesn't immediately chestfrac you lmao + H.apply_damage(20, BRUTE, BODY_ZONE_CHEST, H.run_armor_check("chest", "blunt", damage = 20), damage_type = BCLASS_BLUNT) + audible_message(span_warning("\The [src] shakes under the force of a great impact!")) + playsound(src, "meteor", 100, TRUE) + addtimer(CALLBACK(H, TYPE_PROC_REF(/mob/living/carbon/human, Knockdown), 10), 10) + else + H.Immobilize(10) + H.apply_damage(15, BRUTE, BODY_ZONE_CHEST, H.run_armor_check("chest", "blunt", damage = 15), damage_type = BCLASS_BLUNT) + H.toggle_rogmove_intent(MOVE_INTENT_WALK, TRUE) + playsound(src, "genblunt", 100, TRUE) + H.visible_message("[H] runs into [src]!", "I run into [src]!") + addtimer(CALLBACK(H, TYPE_PROC_REF(/mob/living/carbon/human, Knockdown), 10), 10) /obj/structure/Destroy() if(isturf(loc)) diff --git a/code/game/objects/structures/doors.dm b/code/game/objects/structures/doors.dm index acd0a34ce9d..c3956c9e63b 100644 --- a/code/game/objects/structures/doors.dm +++ b/code/game/objects/structures/doors.dm @@ -21,6 +21,8 @@ var/omni_bolt = FALSE + var/smashable = TRUE + /// Can people riding go through without falling off their mount var/ridethrough = FALSE /// If TRUE when bumped open we callback close diff --git a/code/game/turfs/open/_open.dm b/code/game/turfs/open/_open.dm index a5e92328a2f..655761e0537 100644 --- a/code/game/turfs/open/_open.dm +++ b/code/game/turfs/open/_open.dm @@ -116,8 +116,6 @@ C.Knockdown(knockdown_amount) C.Paralyze(paralyze_amount) C.stop_pulling() - else - C.Knockdown(1) if(buckled_obj) buckled_obj.unbuckle_mob(C) diff --git a/code/game/turfs/open/water.dm b/code/game/turfs/open/water.dm index 798dfa8dc72..36977ad77d7 100644 --- a/code/game/turfs/open/water.dm +++ b/code/game/turfs/open/water.dm @@ -886,6 +886,19 @@ fishing_datum = /datum/fish_source/river river_current = TRUE + +/turf/open/water/river/flow + icon_state = "rockwd2" + +/turf/open/water/river/flow/west + dir = 8 + +/turf/open/water/river/flow/east + dir = 4 + +/turf/open/water/river/flow/north + dir = 1 + /turf/open/water/river/get_heuristic_slowdown(mob/traverser, travel_dir) . = ..() if(travel_dir & dir) // downriver diff --git a/code/modules/antagonists/villain/lich/lich.dm b/code/modules/antagonists/villain/lich/lich.dm index da15351a21b..c1a0e9b0225 100644 --- a/code/modules/antagonists/villain/lich/lich.dm +++ b/code/modules/antagonists/villain/lich/lich.dm @@ -155,10 +155,14 @@ beltr = /obj/item/reagent_containers/glass/bottle/manapot beltl = /obj/item/weapon/knife/dagger/steel r_hand = /obj/item/weapon/polearm/woodstaff + backpack_contents = list( + /obj/item/spellbook/expert/starter/death = 1 + ) H.attributes?.add_sheet(/datum/attribute_holder/sheet/job/lich) - H.adjust_spell_points(17) //Same as CM - Until they receive their spellbook. + H.adjust_technique_mastery_points(12) + H.adjust_form_mastery_points(20) H.grant_language(/datum/language/undead) if(H.dna?.species) H.dna.species.native_language = "Zizo Chant" diff --git a/code/modules/antagonists/villain/lich/spells/eye_bite.dm b/code/modules/antagonists/villain/lich/spells/eye_bite.dm index 4b338e83a75..0069c245c73 100644 --- a/code/modules/antagonists/villain/lich/spells/eye_bite.dm +++ b/code/modules/antagonists/villain/lich/spells/eye_bite.dm @@ -4,9 +4,8 @@ button_icon_state = "raiseskele" sound = 'sound/items/beartrap.ogg' - attunements = list( - /datum/attunement/dark = 0.4, - ) + required_form = FORM_DEATH + required_level = 2 charge_time = 2 SECONDS charge_drain = 1 diff --git a/code/modules/antagonists/villain/lich/spells/raise_undead.dm b/code/modules/antagonists/villain/lich/spells/raise_undead.dm index 8e3ab488ac1..1816d43aa0f 100644 --- a/code/modules/antagonists/villain/lich/spells/raise_undead.dm +++ b/code/modules/antagonists/villain/lich/spells/raise_undead.dm @@ -4,10 +4,9 @@ button_icon_state = "raiseskele" sound = 'sound/magic/magnet.ogg' - attunements = list( - /datum/attunement/dark = 0.4, - /datum/attunement/death = 1, - ) + required_form = FORM_DEATH + required_technique = TECHNIQUE_SUMMONING + required_level = 12 charge_time = 6 SECONDS charge_drain = 1 @@ -15,6 +14,34 @@ cooldown_time = 30 SECONDS spell_cost = 40 + var/max_summons = 30 + var/list/conjured_mobs = list() + var/recoil_energy_floor = 200 + var/recoil_severity = CONJURE_RECOIL_LIGHT + var/recoil_stamina_only = FALSE + +/datum/action/cooldown/spell/raise_undead/Destroy() + for(var/mob/living/M in conjured_mobs.Copy()) + if(!QDELETED(M)) + qdel(M) + conjured_mobs.Cut() + return ..() + +/datum/action/cooldown/spell/raise_undead/proc/remove_conjure(mob/living/summoned) + SIGNAL_HANDLER + conjured_mobs -= summoned + +/datum/action/cooldown/spell/raise_undead/proc/register_minion(mob/living/minion, mob/living/user) + if(length(conjured_mobs) >= max_summons) + for(var/mob/living/M in conjured_mobs.Copy()) + if(!QDELETED(M)) + qdel(M) + conjured_mobs.Cut() + + conjured_mobs += minion + RegisterSignal(minion, COMSIG_QDELETING, PROC_REF(remove_conjure)) + minion.AddComponent(/datum/component/conjured_minion, user, recoil_energy_floor, recoil_severity, recoil_stamina_only) + /datum/action/cooldown/spell/raise_undead/is_valid_target(atom/cast_on) . = ..() if(!.) @@ -37,6 +64,8 @@ /datum/action/cooldown/spell/raise_undead/cast(mob/living/carbon/human/cast_on) . = ..() + var/mob/living/user = owner + owner.say("Hgf'ant'kthar!") cast_on.visible_message(span_warning("[cast_on.real_name]'s body is engulfed by dark energy..."), runechat_message = TRUE) @@ -48,6 +77,7 @@ to_chat(cast_on, span_danger("You rise as a minion.")) cast_on.turn_to_minion(owner, cast_on.ckey) cast_on.visible_message(span_warning("[cast_on.real_name]'s eyes light up with an evil glow."), runechat_message = TRUE) + register_minion(cast_on, user) return else to_chat(cast_on, span_danger("Another soul will take over.")) @@ -61,6 +91,8 @@ cast_on.turn_to_minion(owner) cast_on.visible_message(span_warning("[cast_on.real_name]'s eyes light up with a weak glow."), runechat_message = TRUE) + register_minion(cast_on, user) + /mob/living/carbon/human/proc/turn_to_minion(mob/living/carbon/human/master, ckey) if(!master) return FALSE diff --git a/code/modules/antagonists/villain/lich/spells/ray_of_sickness.dm b/code/modules/antagonists/villain/lich/spells/ray_of_sickness.dm index d4d5082a394..394b7b1f1b6 100644 --- a/code/modules/antagonists/villain/lich/spells/ray_of_sickness.dm +++ b/code/modules/antagonists/villain/lich/spells/ray_of_sickness.dm @@ -4,10 +4,7 @@ button_icon_state = "raiseskele" sound = 'sound/misc/portal_enter.ogg' - attunements = list( - /datum/attunement/dark = 0.4, - /datum/attunement/blood = 0.5, - ) + required_form = FORM_WATER charge_time = 2 SECONDS charge_drain = 1 diff --git a/code/modules/antagonists/villain/lich/spells/strengthen_undead.dm b/code/modules/antagonists/villain/lich/spells/strengthen_undead.dm index aec1ae3786e..2c03978cba3 100644 --- a/code/modules/antagonists/villain/lich/spells/strengthen_undead.dm +++ b/code/modules/antagonists/villain/lich/spells/strengthen_undead.dm @@ -4,10 +4,9 @@ button_icon_state = "raiseskele" sound = 'sound/magic/whiteflame.ogg' - attunements = list( - /datum/attunement/dark = 0.4, - /datum/attunement/death = 0.5, - ) + required_form = FORM_DEATH + required_technique = TECHNIQUE_RESTORATION + required_level = 2 charge_required = FALSE cooldown_time = 15 SECONDS diff --git a/code/modules/antagonists/villain/overlord/outfit.dm b/code/modules/antagonists/villain/overlord/outfit.dm index b4ae25efb35..de19462a279 100644 --- a/code/modules/antagonists/villain/overlord/outfit.dm +++ b/code/modules/antagonists/villain/overlord/outfit.dm @@ -19,7 +19,8 @@ H.attributes?.add_sheet(/datum/attribute_holder/sheet/job/lich) - H.adjust_spell_points(17) + H.adjust_technique_mastery_points(12) + H.adjust_form_mastery_points(20) H.grant_language(/datum/language/undead) if(H.dna?.species) H.dna.species.native_language = "Zizo Chant" diff --git a/code/modules/antagonists/villain/overlord/spells.dm b/code/modules/antagonists/villain/overlord/spells.dm index a098d7d8598..984bf971e4b 100644 --- a/code/modules/antagonists/villain/overlord/spells.dm +++ b/code/modules/antagonists/villain/overlord/spells.dm @@ -6,10 +6,6 @@ spell_requirements = NONE charge_time = 2 SECONDS - attunements = list( - /datum/attunement/death = 0.2, - ) - /datum/action/cooldown/spell/remove_enchantment/can_cast_spell(feedback) . = ..() if(!.) @@ -89,10 +85,6 @@ charge_time = 2 SECONDS spell_requirements = NONE - attunements = list( - /datum/attunement/death = 0.2, - ) - /datum/action/cooldown/spell/undirected/enter_overseer_mode/can_cast_spell(feedback) . = ..() if(!.) @@ -147,10 +139,6 @@ charge_time = 2 SECONDS spell_requirements = NONE - attunements = list( - /datum/attunement/death = 0.2, - ) - cast_range = 1 /datum/action/cooldown/spell/remove_enchantment/can_cast_spell(feedback) @@ -195,10 +183,6 @@ spell_requirements = NONE spell_cost = 100 - attunements = list( - /datum/attunement/death = 0.2, - ) - /datum/action/cooldown/spell/undirected/summon_worker/before_cast(atom/cast_on) . = ..() if(. & SPELL_CANCEL_CAST) diff --git a/code/modules/antagonists/villain/zizo_cult/rituals.dm b/code/modules/antagonists/villain/zizo_cult/rituals.dm index 77e37005d33..332422d5ae4 100644 --- a/code/modules/antagonists/villain/zizo_cult/rituals.dm +++ b/code/modules/antagonists/villain/zizo_cult/rituals.dm @@ -464,9 +464,9 @@ GLOBAL_LIST_INIT(ritualslist, build_zizo_rituals()) if(!target) return target.set_faction(list(FACTION_UNDEAD)) - target.add_spell(/datum/action/cooldown/spell/gravemark) - target.add_spell(/datum/action/cooldown/spell/control_undead) - target.add_spell(/datum/action/cooldown/spell/decompose) + target.add_spell(/datum/action/cooldown/spell/gravemark, mastery_spell = TRUE) + target.add_spell(/datum/action/cooldown/spell/control_undead, mastery_spell = TRUE) + target.add_spell(/datum/action/cooldown/spell/decompose, mastery_spell = TRUE) to_chat(target, span_notice("The undead bow down to my will.")) /datum/ritual/fleshcrafting/arcane @@ -485,9 +485,9 @@ GLOBAL_LIST_INIT(ritualslist, build_zizo_rituals()) mage.gib() cultist.adjust_stat_modifier(STATMOD_RITUAL, list(/datum/attribute/skill/magic/arcane = 40)) - cultist.adjust_spell_points(18) + cultist.adjust_form_mastery_points(12) + cultist.adjust_technique_mastery_points(5) cultist.mana_pool.set_intrinsic_recharge(MANA_ALL_LEYLINES) - cultist.generate_random_attunements(rand(6, 8)) to_chat(cultist, span_notice("Stolen Arcane prowess floods my mind, ZIZO empowers me.")) /datum/ritual/fleshcrafting/nopain diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/essence_gauntlet.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/essence_gauntlet.dm index 8a21daf6dcb..3af584852af 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/essence_gauntlet.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/essence_gauntlet.dm @@ -128,38 +128,37 @@ available += vial.essence_amount return available >= amount -/// Consumes essence, splitting cost evenly across matching attunements when multiple are required. +/// Consumes essence, splitting cost evenly across matching essence types when multiple are required. /// Returns TRUE on success. -/obj/item/clothing/gloves/essence_gauntlet/proc/consume_essence(amount, list/attunements = null) - if(!can_consume_essence(amount, attunements)) +/obj/item/clothing/gloves/essence_gauntlet/proc/consume_essence(amount, list/essences = null) + if(!can_consume_essence(amount, essences)) return FALSE - // Build a list of eligible vials grouped by attunement type - var/list/vials_by_attunement = list() + // Build a list of eligible vials grouped by essence type + var/list/vials_by_type = list() for(var/obj/item/essence_vial/vial in stored_vials) if(!vial.contained_essence || vial.essence_amount <= 0) continue - var/att = vial.contained_essence.attunement - if(attunements && !(att in attunements)) + var/ess_type = vial.contained_essence.type + if(essences && !(ess_type in essences)) continue - if(!vials_by_attunement[att]) - vials_by_attunement[att] = list() - vials_by_attunement[att] += vial + if(!vials_by_type[ess_type]) + vials_by_type[ess_type] = list() + vials_by_type[ess_type] += vial - // Split cost as evenly as possible across attunement groups - var/list/att_types = vials_by_attunement - var/num_groups = length(att_types) + // Split cost as evenly as possible across essence type groups + var/num_groups = length(vials_by_type) var/remaining = amount if(num_groups > 1) // First pass: try to take an even share from each group var/share = CEILING(amount / num_groups, 1) - for(var/att in att_types) + for(var/ess_type in vials_by_type) if(remaining <= 0) break var/to_draw = min(share, remaining) var/drawn_from_group = 0 - for(var/obj/item/essence_vial/vial in att_types[att]) + for(var/obj/item/essence_vial/vial in vials_by_type[ess_type]) if(to_draw <= 0) break var/drawn = min(vial.essence_amount, to_draw) @@ -172,10 +171,10 @@ remaining -= drawn_from_group if(remaining > 0) - for(var/att in att_types) + for(var/ess_type in vials_by_type) if(remaining <= 0) break - for(var/obj/item/essence_vial/vial in att_types[att]) + for(var/obj/item/essence_vial/vial in vials_by_type[ess_type]) if(remaining <= 0) break if(!vial.contained_essence || vial.essence_amount <= 0) diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/_base.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/_base.dm index 7463c3a6447..5f6668bcba9 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/_base.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/_base.dm @@ -3,12 +3,10 @@ /datum/action/cooldown/spell/essence name = "Utility Spell" desc = "A minor utility spell." - school = "utility" spell_cost = 5 charge_drain = 0 charge_required = FALSE cooldown_time = 30 SECONDS - point_cost = 2 spell_type = SPELL_ESSENCE experience_modifier = 0 associated_skill = /datum/attribute/skill/craft/alchemy @@ -17,6 +15,8 @@ return charge_time /datum/action/cooldown/spell/essence/get_adjusted_cost(cost_override) + if(spell_type == SPELL_MANA) + return ..() if(cost_override) return cost_override return spell_cost diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/accellerated_growth.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/accellerated_growth.dm index 65937ad1975..0a65495ad3b 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/accellerated_growth.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/accellerated_growth.dm @@ -3,8 +3,6 @@ desc = "Dramatically speeds up natural growth processes." button_icon_state = "great_shelter" cast_range = 2 - point_cost = 7 - attunements = list(/datum/attunement/light, /datum/attunement/life) essences = list(/datum/thaumaturgical_essence/life, /datum/thaumaturgical_essence/light) /datum/action/cooldown/spell/essence/growth_acceleration/cast(atom/cast_on) @@ -17,3 +15,13 @@ for(var/obj/structure/soil/plant in range(1, target_turf)) plant.accellerated_growth = world.time + 600 SECONDS new /obj/effect/temp_visual/bless_swirl(get_turf(plant)) + + +/datum/action/cooldown/spell/essence/growth_acceleration/spell + name = "Accelerated Growth" + charge_required = TRUE + charge_time = 2 SECONDS + spell_cost = 40 + spell_type = SPELL_MANA + + required_form = FORM_LIFE diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/airwalk.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/airwalk.dm index dbf502b03e4..b5f8cebcae8 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/airwalk.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/airwalk.dm @@ -3,8 +3,6 @@ desc = "Allows brief movement over chasms or gaps by creating temporary air platforms." button_icon_state = "air_walk" cast_range = 0 - point_cost = 5 - attunements = list(/datum/attunement/aeromancy) essences = list(/datum/thaumaturgical_essence/air) has_visual_effects = FALSE @@ -36,3 +34,13 @@ REMOVE_TRAIT(owner, TRAIT_HOLLOWBONES, TRAIT_STATUS_EFFECT(id)) REMOVE_TRAIT(owner, TRAIT_MOVE_FLYING, TRAIT_STATUS_EFFECT(id)) to_chat(owner, span_notice("Your feet return to solid ground.")) + +/datum/action/cooldown/spell/essence/air_walk/spell + name = "Lesser Air Walk" + charge_required = TRUE + charge_time = 1 SECONDS + spell_cost = 50 + spell_type = SPELL_MANA + + required_form = FORM_AIR + required_technique = TECHNIQUE_ALTERATION diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/ancestral_smithing.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/ancestral_smithing.dm index 67ac85996ed..ad73fffaa9e 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/ancestral_smithing.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/ancestral_smithing.dm @@ -11,8 +11,6 @@ desc = "Channels the spirits of ancient dwarven smiths to guide crafting." button_icon_state = "ancestral_smithing" cast_range = 0 - point_cost = 7 - attunements = list(/datum/attunement/fire, /datum/attunement/earth) essences = list(/datum/thaumaturgical_essence/earth, /datum/thaumaturgical_essence/fire) /datum/action/cooldown/spell/essence/ancestral_smithing/cast(atom/cast_on) diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/arcane_focus.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/arcane_focus.dm index a4b81e631e5..58955616288 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/arcane_focus.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/arcane_focus.dm @@ -3,8 +3,6 @@ desc = "Creates a crystal focus that enhances magical abilities." button_icon_state = "rune2" cast_range = 0 - point_cost = 8 - attunements = list(/datum/attunement/light, /datum/attunement/earth) essences = list(/datum/thaumaturgical_essence/magic, /datum/thaumaturgical_essence/crystal) /datum/action/cooldown/spell/essence/arcane_focus/cast(atom/cast_on) diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/arcane_mark.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/arcane_mark.dm index 9f6a2977fb4..81879696476 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/arcane_mark.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/arcane_mark.dm @@ -3,7 +3,6 @@ desc = "Places an invisible magical mark on an object for identification." button_icon_state = "arcane_mark" cast_range = 1 - point_cost = 2 essences = list(/datum/thaumaturgical_essence/magic) /datum/action/cooldown/spell/essence/arcane_mark/cast(atom/cast_on) @@ -39,3 +38,10 @@ else new_image.loc = source add_client_image(new_image, enterer.client) + +/datum/action/cooldown/spell/essence/arcane_mark/spell + name = "Arcane Markings" + spell_cost = 20 + spell_type = SPELL_MANA + + required_form = FORM_ARCANE diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/breeze.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/breeze.dm index 0679deee981..2d19a30454c 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/breeze.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/breeze.dm @@ -5,7 +5,6 @@ button_icon_state = "breeze" //sound = 'sound/magic/whiff.ogg' cast_range = 3 - attunements = list(/datum/attunement/aeromancy = 0.2) essences = list(/datum/thaumaturgical_essence/air) /datum/action/cooldown/spell/essence/breeze/cast(atom/cast_on) @@ -23,3 +22,10 @@ for(var/obj/item/I in target_turf) if(I.w_class <= WEIGHT_CLASS_SMALL && prob(50)) SSmove_manager.move_rand(I) + +/datum/action/cooldown/spell/essence/breeze/spell + name = "Breeze" + spell_cost = 15 + spell_type = SPELL_MANA + + required_form = FORM_AIR diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/chill.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/chill.dm index ec8dfedbf3a..007d43e94ed 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/chill.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/chill.dm @@ -4,7 +4,6 @@ button_icon_state = "chill" //sound = 'sound/magic/whiff.ogg' cast_range = 1 - attunements = list(/datum/attunement/ice) essences = list(/datum/thaumaturgical_essence/frost) /datum/action/cooldown/spell/essence/chill/cast(atom/cast_on) @@ -43,3 +42,12 @@ /obj/structure/ice_zone/Destroy() remove_temp_effect() return ..() + +/datum/action/cooldown/spell/essence/chill/spell + name = "Ice Zone" + charge_required = TRUE + charge_time = 0.5 SECONDS + spell_cost = 20 + spell_type = SPELL_MANA + + required_form = FORM_ICE diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/cleanse.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/cleanse.dm index 0f2abbf5412..6f24ac0424a 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/cleanse.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/cleanse.dm @@ -4,7 +4,6 @@ button_icon_state = "deathdoor" //sound = 'sound/magic/splash.ogg' cast_range = 1 - attunements = list(/datum/attunement/blood) essences = list(/datum/thaumaturgical_essence/water) /datum/action/cooldown/spell/essence/cleanse/cast(atom/cast_on) @@ -17,3 +16,12 @@ playsound(target, 'sound/effects/splash.ogg', 50, TRUE) target.wash(CLEAN_WASH) + +/datum/action/cooldown/spell/essence/cleanse/spell + name = "Lesser Cleanse" + charge_required = TRUE + charge_time = 2 SECONDS + spell_cost = 15 + spell_type = SPELL_MANA + + required_form = FORM_WATER diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/create_beer.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/create_beer.dm index 3b712717385..285366b509f 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/create_beer.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/create_beer.dm @@ -3,8 +3,6 @@ desc = "A dwarven secret - transforms water and grain into fine ale." button_icon_state = "bread" cast_range = 1 - point_cost = 4 - attunements = list(/datum/attunement/earth, /datum/attunement/blood) essences = list(/datum/thaumaturgical_essence/earth, /datum/thaumaturgical_essence/water) /datum/action/cooldown/spell/essence/create_beer/cast(atom/cast_on) diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/daylight_orb.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/daylight_orb.dm index 9ba370cc6f4..03b1bf5aa0a 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/daylight_orb.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/daylight_orb.dm @@ -3,8 +3,6 @@ desc = "Creates a bright light that mimics natural sunlight." button_icon_state = "conjure_dragonhide" cast_range = 0 - point_cost = 4 - attunements = list(/datum/attunement/light) essences = list(/datum/thaumaturgical_essence/light) /datum/action/cooldown/spell/essence/daylight/cast(atom/cast_on) @@ -19,3 +17,13 @@ icon = 'icons/effects/effects.dmi' icon_state = "impact_laser" duration = 1 HOURS + +/datum/action/cooldown/spell/essence/daylight/spell + name = "Daylight Orb" + charge_required = TRUE + charge_time = 5 SECONDS + spell_cost = 40 + spell_type = SPELL_MANA + + required_form = FORM_FIRE + required_technique = TECHNIQUE_CREATION diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/elven_grace.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/elven_grace.dm index 8d16acb3b8f..dc296281607 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/elven_grace.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/elven_grace.dm @@ -3,8 +3,6 @@ desc = "Grants the ethereal grace and agility of the ancient elves." button_icon_state = "conjinstrum" cast_range = 0 - point_cost = 6 - attunements = list(/datum/attunement/life, /datum/attunement/light) essences = list(/datum/thaumaturgical_essence/life, /datum/thaumaturgical_essence/light) /datum/action/cooldown/spell/essence/elven_grace/cast(atom/cast_on) diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/energize.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/energize.dm index 6db2ff26ecc..5038b634b68 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/energize.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/energize.dm @@ -3,8 +3,6 @@ desc = "Restores energy to magical devices or provides a burst of vitality." button_icon_state = "primetriangle" cast_range = 1 - point_cost = 4 - attunements = list(/datum/attunement/electric) essences = list(/datum/thaumaturgical_essence/energia) /datum/action/cooldown/spell/essence/energize/cast(atom/cast_on) @@ -22,3 +20,13 @@ if(istype(target, /obj/structure/mana_pylon)) var/obj/structure/mana_pylon/pylon = target pylon.mana_pool.adjust_mana(30) + +/datum/action/cooldown/spell/essence/energize/spell + name = "Re-Energize" + charge_required = TRUE + charge_time = 1 SECONDS + spell_cost = 40 + spell_type = SPELL_MANA + + required_form = FORM_LIGHTNING + required_technique = TECHNIQUE_RESTORATION diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/fan_of_flames.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/fan_of_flames.dm index 615a45876b3..1373092afaa 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/fan_of_flames.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/fan_of_flames.dm @@ -1,12 +1,9 @@ /datum/action/cooldown/spell/essence/fan_of_flames name = "Fan of Flames" desc = "Unleash a spreading fan of slow-moving flame projectiles." - school = "evocation" button_icon_state = "sacredflame" spell_cost = 4 cooldown_time = 45 SECONDS - point_cost = 4 - attunements = list(/datum/attunement/fire, /datum/attunement/aeromancy) essences = list(/datum/thaumaturgical_essence/fire, /datum/thaumaturgical_essence/air) click_to_activate = TRUE /// Projectile type to fire @@ -46,6 +43,16 @@ icon_state = "blastwave" damage = 5 damage_type = BURN - speed = 1 SECONDS + speed = 0.8 SECONDS homing = FALSE - range = 3 + range = 4 + +/datum/action/cooldown/spell/essence/fan_of_flames/spell + name = "Greater Fan of Flames" + charge_required = TRUE + charge_time = 0.3 SECONDS + spell_cost = 35 + spell_type = SPELL_MANA + + required_form = FORM_FIRE + required_technique = TECHNIQUE_DESTRUCTION diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/fertilize_soil.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/fertilize_soil.dm index e36735109a8..179a4c1deac 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/fertilize_soil.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/fertilize_soil.dm @@ -3,8 +3,6 @@ desc = "Enriches soil to promote plant growth." button_icon_state = "blesscrop" cast_range = 2 - point_cost = 4 - attunements = list(/datum/attunement/blood, /datum/attunement/earth) essences = list(/datum/thaumaturgical_essence/water, /datum/thaumaturgical_essence/earth) /datum/action/cooldown/spell/essence/fertile_soil/cast(atom/cast_on) @@ -17,3 +15,10 @@ for(var/obj/structure/soil/plant in range(1, target_turf)) plant.bless_soil() new /obj/effect/temp_visual/bless_swirl(get_turf(plant)) + +/datum/action/cooldown/spell/essence/fertile_soil/spell + name = "Fertilze Land" + spell_cost = 15 + spell_type = SPELL_MANA + + required_form = FORM_EARTH diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/fire_cascade.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/fire_cascade.dm index 3822b630335..e578b8098b3 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/fire_cascade.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/fire_cascade.dm @@ -1,13 +1,10 @@ /datum/action/cooldown/spell/essence/fire_cascade - name = "Fire Cascadec" - desc = "Unleash a spreading fan of slow-moving flame projectiles." - school = "evocation" + name = "Fire Cascade" + desc = "Unleash a nova of spreading flames." button_icon_state = "fireaura" spell_cost = 4 cooldown_time = 45 SECONDS - point_cost = 4 click_to_activate = TRUE - attunements = list(/datum/attunement/fire, /datum/attunement/aeromancy) essences = list(/datum/thaumaturgical_essence/fire, /datum/thaumaturgical_essence/air) var/flame_radius = 2 var/hotspot_lifetime = 3 @@ -26,3 +23,13 @@ new /obj/effect/hotspot(nearby_turf, null, null, hotspot_lifetime) sleep(0.3 SECONDS) + +/datum/action/cooldown/spell/essence/fire_cascade/spell + name = "Cascade of Fire" + charge_required = TRUE + charge_time = 0.2 SECONDS + spell_cost = 40 + spell_type = SPELL_MANA + + required_form = FORM_FIRE + required_technique = TECHNIQUE_CREATION diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/found_poison.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/found_poison.dm index aa3fcca78c6..709de148273 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/found_poison.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/found_poison.dm @@ -3,8 +3,6 @@ desc = "Reveals the presence of toxins or poisons in nearby objects." button_icon_state = "detect_poison" cast_range = 2 - point_cost = 2 - attunements = list(/datum/attunement/life) essences = list(/datum/thaumaturgical_essence/poison) /datum/action/cooldown/spell/essence/detect_poison/cast(atom/cast_on) @@ -35,3 +33,12 @@ icon_state = "solosnake" duration = 2 SECONDS layer = EFFECTS_LAYER + +/datum/action/cooldown/spell/essence/detect_poison/spell + name = "Detect Toxin" + charge_required = TRUE + charge_time = 2 SECONDS + spell_cost = 10 + spell_type = SPELL_MANA + + required_form = FORM_LIFE diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/frozen_storage.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/frozen_storage.dm index 48d27d9292d..b9d8567fac2 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/frozen_storage.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/frozen_storage.dm @@ -3,8 +3,6 @@ desc = "Prevents rot from ever touching." button_icon_state = "fridigitation" cast_range = 1 - point_cost = 6 - attunements = list(/datum/attunement/ice, /datum/attunement/blood) essences = list(/datum/thaumaturgical_essence/frost, /datum/thaumaturgical_essence/water) /datum/action/cooldown/spell/fridigitation/cast(atom/cast_on, mob/user = usr) @@ -24,3 +22,10 @@ else to_chat(user, span_warning("That is not a valid target for Fridigitation.")) return FALSE + +/datum/action/cooldown/spell/essence/frozen_storage/spell + name = "Greater Fridigitation" + spell_cost = 5 + spell_type = SPELL_MANA + + required_form = FORM_ICE diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/gem_detect.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/gem_detect.dm index a19f44209c3..753f2ae9728 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/gem_detect.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/gem_detect.dm @@ -4,8 +4,6 @@ button_icon_state = "aros" button_icon = 'icons/roguetown/items/gems.dmi' cast_range = 3 - point_cost = 4 - attunements = list(/datum/attunement/earth) essences = list(/datum/thaumaturgical_essence/crystal) /datum/action/cooldown/spell/essence/gem_detect/cast(atom/cast_on) diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/haste.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/haste.dm index b10e4f0636f..8092b416298 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/haste.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/haste.dm @@ -4,9 +4,7 @@ button_icon_state = "haste" //sound = 'sound/magic/whiff.ogg' cast_range = 0 - point_cost = 4 has_visual_effects = FALSE - attunements = list(/datum/attunement/aeromancy) essences = list(/datum/thaumaturgical_essence/air) /datum/action/cooldown/spell/essence/haste/cast(atom/cast_on) @@ -17,3 +15,12 @@ var/mob/living/L = owner L.apply_status_effect(/datum/status_effect/buff/haste, 10 SECONDS) new /obj/effect/temp_visual/snake/swarm(null, L) + +/datum/action/cooldown/spell/essence/haste/spell + name = "Lesser Haste" + charge_required = TRUE + charge_time = 0.2 SECONDS + spell_cost = 25 + spell_type = SPELL_MANA + + required_form = FORM_AIR diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/healing_spring.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/healing_spring.dm index 5ca68a39700..9e70ad2323d 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/healing_spring.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/healing_spring.dm @@ -4,8 +4,6 @@ button_icon_state = "healingspring" button_icon = 'icons/effects/effects.dmi' cast_range = 2 - point_cost = 8 - attunements = list(/datum/attunement/life, /datum/attunement/blood) essences = list(/datum/thaumaturgical_essence/life, /datum/thaumaturgical_essence/water) /datum/action/cooldown/spell/essence/healing_spring/cast(atom/cast_on) @@ -41,3 +39,14 @@ organ_healing = TRUE, \ healing_color = "#375637", \ ) + +/datum/action/cooldown/spell/essence/healing_spring/spell + name = "Spring of Healing" + charge_required = TRUE + charge_time = 10 SECONDS + spell_cost = 120 + spell_type = SPELL_MANA + + required_form = FORM_EARTH + required_technique = TECHNIQUE_RESTORATION + required_level = 4 diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/ice_bridge.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/ice_bridge.dm index 30ccf9b278b..e75cbe6d826 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/ice_bridge.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/ice_bridge.dm @@ -3,8 +3,6 @@ desc = "Creates a temporary bridge of solid ice from the cast location to you." button_icon_state = "ice_bridge" cast_range = 3 - point_cost = 7 - attunements = list(/datum/attunement/ice, /datum/attunement/blood) essences = list(/datum/thaumaturgical_essence/frost, /datum/thaumaturgical_essence/water) /datum/action/cooldown/spell/essence/ice_bridge/cast(atom/cast_on) @@ -24,7 +22,7 @@ steps++ var/obj/structure/ice_bridge/bridge = new(target_turf) QDEL_IN(bridge, 300 SECONDS) - target_turf = step_towards(target_turf, caster_turf) + target_turf = get_step_towards(target_turf, caster_turf) /obj/structure/ice_bridge name = "ice bridge" @@ -44,3 +42,12 @@ /obj/structure/ice_bridge/Destroy() remove_temp_effect() return ..() + +/datum/action/cooldown/spell/essence/ice_bridge/spell + name = "Bridge of Ice" + charge_required = TRUE + charge_time = 2 SECONDS + spell_cost = 60 + spell_type = SPELL_MANA + + required_form = FORM_ICE diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/illuminate.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/illuminate.dm index 97dbc87cb97..ce4093d1175 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/illuminate.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/illuminate.dm @@ -4,8 +4,6 @@ button_icon_state = "light" //sound = 'sound/magic/staff_healing.ogg' cast_range = 0 - attunements = list(/datum/attunement/light) - point_cost = 1 essences = list(/datum/thaumaturgical_essence/light) /datum/action/cooldown/spell/essence/illuminate/cast(atom/cast_on) diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/kinetic_burst.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/kinetic_burst.dm index 1f04d4ff682..a7b5c46d5a0 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/kinetic_burst.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/kinetic_burst.dm @@ -3,8 +3,6 @@ desc = "Releases stored energy as a burst of kinetic force." button_icon_state = "kinetic_burst" cast_range = 3 - point_cost = 7 - attunements = list(/datum/attunement/light, /datum/attunement/aeromancy) essences = list(/datum/thaumaturgical_essence/energia, /datum/thaumaturgical_essence/motion) /datum/action/cooldown/spell/essence/kinetic_burst/cast(atom/cast_on) diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/mend.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/mend.dm index 8b579d558a0..a84d7e87d8d 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/mend.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/mend.dm @@ -3,8 +3,6 @@ desc = "Repairs minor damage to simple objects." button_icon_state = "mending" cast_range = 1 - attunements = list(/datum/attunement/earth) - point_cost = 3 essences = list(/datum/thaumaturgical_essence/earth) var/repair_percent = 0.08 @@ -64,3 +62,12 @@ else to_chat(user, span_warning("My concentration breaks! I could not repair [I].")) return FALSE + +/datum/action/cooldown/spell/essence/mend/spell + name = "Lesser Mend" + spell_cost = 40 + spell_type = SPELL_MANA + cooldown_time = 3 MINUTES + + required_form = FORM_EARTH + required_technique = TECHNIQUE_RESTORATION diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/momentum_transfer.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/momentum_transfer.dm index 9398e3061f2..583863c83b5 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/momentum_transfer.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/momentum_transfer.dm @@ -3,10 +3,8 @@ desc = "Transfers kinetic energy between objects or creatures." button_icon_state = "longstrider" cast_range = 2 - point_cost = 6 has_visual_effects = FALSE cooldown_time = 2 MINUTES - attunements = list(/datum/attunement/light, /datum/attunement/aeromancy) essences = list(/datum/thaumaturgical_essence/energia, /datum/thaumaturgical_essence/motion) /datum/action/cooldown/spell/essence/momentum_transfer/cast(atom/cast_on) @@ -98,3 +96,13 @@ to_chat(M, span_danger("You are hurled by a kinetic shockwave!")) else AM.throw_at(throw_target, throw_range, throw_speed) + +/datum/action/cooldown/spell/essence/momentum_transfer/spell + name = "Kinetic Transfer" + charge_required = TRUE + charge_time = 1 SECONDS + spell_cost = 60 + spell_type = SPELL_MANA + + required_form = FORM_ARCANE + required_technique = TECHNIQUE_ALTERATION diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/mud_shape.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/mud_shape.dm index eaefe2adeab..d36fefd8cff 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/mud_shape.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/mud_shape.dm @@ -4,8 +4,6 @@ button_icon_state = "stone1" button_icon = 'icons/roguetown/items/natural.dmi' cast_range = 2 - point_cost = 5 - attunements = list(/datum/attunement/blood, /datum/attunement/earth) essences = list(/datum/thaumaturgical_essence/water, /datum/thaumaturgical_essence/earth) /datum/action/cooldown/spell/essence/mud_shape/cast(atom/cast_on) diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/neutralize.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/neutralize.dm index e0d199143c7..5d23189dfd6 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/neutralize.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/neutralize.dm @@ -3,8 +3,6 @@ desc = "Removes harmful toxins and poisons from objects or creatures." button_icon_state = "borrowtime" cast_range = 1 - point_cost = 4 - attunements = list(/datum/attunement/life) essences = list(/datum/thaumaturgical_essence/poison) /datum/action/cooldown/spell/essence/neutralize/cast(atom/cast_on) @@ -19,3 +17,13 @@ L.reagents?.remove_all_type(/datum/reagent/toxin, 5) L.reagents?.remove_all_type(/datum/reagent/poison, 5) new /obj/effect/temp_visual/snake/twin_up(null, L) + +/datum/action/cooldown/spell/essence/neutralize/spell + name = "Neutralize Toxins" + charge_required = TRUE + charge_time = 0.2 SECONDS + spell_cost = 40 + spell_type = SPELL_MANA + + required_form = FORM_LIFE + required_technique = TECHNIQUE_RESTORATION diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/phase_step.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/phase_step.dm index 15f481676b6..7cb0d8873cb 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/phase_step.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/phase_step.dm @@ -3,8 +3,6 @@ desc = "Allows brief passage through solid objects." button_icon_state = "deathdoor" cast_range = 0 - point_cost = 6 - attunements = list(/datum/attunement/aeromancy) essences = list(/datum/thaumaturgical_essence/motion) has_visual_effects = FALSE @@ -36,3 +34,13 @@ name = "Phase Walking" desc = "You can pass through solid objects." icon_state = "buff" + +/datum/action/cooldown/spell/essence/phase_step/spell + name = "Lesser Phase Step" + charge_required = TRUE + charge_time = 4 SECONDS + spell_cost = 40 + spell_type = SPELL_MANA + + required_form = FORM_ARCANE + required_technique = TECHNIQUE_ILLUSION diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/probability_warp.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/probability_warp.dm index 4d9f1e45024..a63d8dac929 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/probability_warp.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/probability_warp.dm @@ -4,8 +4,6 @@ desc = "Alters the likelihood of minor events occurring." button_icon_state = "guidanceneu" cast_range = 3 - point_cost = 7 - attunements = list(/datum/attunement/polymorph, /datum/attunement/illusion) essences = list(/datum/thaumaturgical_essence/chaos, /datum/thaumaturgical_essence/void) /datum/action/cooldown/spell/essence/probability_warp/cast(atom/cast_on) diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/purify_water.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/purify_water.dm index e04a5722440..8771bd99566 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/purify_water.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/purify_water.dm @@ -3,8 +3,6 @@ desc = "Removes all impurities and toxins from water, making it pure and safe." button_icon_state = "purify_water" cast_range = 1 - point_cost = 5 - attunements = list(/datum/attunement/life, /datum/attunement/blood) essences = list(/datum/thaumaturgical_essence/life, /datum/thaumaturgical_essence/water) /datum/action/cooldown/spell/essence/purify_water/cast(atom/cast_on) diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/read_omen.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/read_omen.dm index 5aff0d3c0f5..d72aaf3c8e3 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/read_omen.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/read_omen.dm @@ -3,8 +3,6 @@ desc = "Draw upon the leylines themselves to reveal secrets of fate itself." button_icon_state = "readomen" cast_range = 3 - point_cost = 7 - attunements = list(/datum/attunement/fire) essences = list(/datum/thaumaturgical_essence/chaos) /datum/action/cooldown/spell/essence/read_omen/cast(mob/living/cast_on) diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/refresh.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/refresh.dm index 6edfb03cf90..e7258bbc334 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/refresh.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/refresh.dm @@ -4,8 +4,6 @@ button_icon_state = "terrors" //sound = 'sound/magic/staff_healing.ogg' cast_range = 1 - point_cost = 3 - attunements = list(/datum/attunement/life) essences = list(/datum/thaumaturgical_essence/life) /datum/action/cooldown/spell/essence/refresh/cast(atom/cast_on) @@ -18,3 +16,12 @@ target.adjust_stamina(20) target.adjust_energy(20) + +/datum/action/cooldown/spell/essence/refresh/spell + name = "Reinvigorate" + charge_required = TRUE + charge_time = 0.2 SECONDS + spell_cost = 30 + spell_type = SPELL_MANA + + required_form = FORM_LIGHTNING diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/regeneration_cycle.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/regeneration_cycle.dm index 183cf7a461d..f1add6130e0 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/regeneration_cycle.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/regeneration_cycle.dm @@ -3,8 +3,6 @@ desc = "Establishes a cycle of continuous healing over time." button_icon_state = "regeneratelimb" cast_range = 1 - point_cost = 8 - attunements = list(/datum/attunement/light, /datum/attunement/life) essences = list(/datum/thaumaturgical_essence/cycle, /datum/thaumaturgical_essence/life) /datum/action/cooldown/spell/essence/regeneration_cycle/cast(atom/cast_on) @@ -34,3 +32,13 @@ carbon.adjustBruteLoss(-0.1, FALSE) carbon.adjustFireLoss(-0.1, TRUE) + +/datum/action/cooldown/spell/essence/regeneration_cycle/spell + name = "Rejuvenation Cycle" + charge_required = TRUE + charge_time = 2 SECONDS + spell_cost = 80 + spell_type = SPELL_MANA + + required_form = FORM_LIFE + required_technique = TECHNIQUE_RESTORATION diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/seasonal_attunement.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/seasonal_attunement.dm index 8414d43084d..20decea4a9a 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/seasonal_attunement.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/seasonal_attunement.dm @@ -3,8 +3,6 @@ desc = "Attunes the caster to natural cycles, providing minor benefits." button_icon_state = "lightning_sunder" cast_range = 0 - point_cost = 3 - attunements = list(/datum/attunement/light) essences = list(/datum/thaumaturgical_essence/cycle) /datum/action/cooldown/spell/essence/seasonal_attune/cast(atom/cast_on) diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/spark.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/spark.dm index 0aa0e44a255..4a76e46e42a 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/spark.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/spark.dm @@ -4,7 +4,6 @@ button_icon_state = "spark" sound = 'sound/magic/fireball.ogg' cast_range = 1 - attunements = list(/datum/attunement/fire) essences = list(/datum/thaumaturgical_essence/fire) /datum/action/cooldown/spell/essence/spark/cast(atom/cast_on) @@ -28,3 +27,10 @@ if(!T.on) T.fuel += 5 MINUTES T.fire_act() + +/datum/action/cooldown/spell/essence/spark/spell + name = "Lesser Spark" + spell_cost = 10 + spell_type = SPELL_MANA + + required_form = FORM_FIRE diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/spell_crystal.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/spell_crystal.dm index 40ab4a6b498..cfa2045b8ee 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/spell_crystal.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/spell_crystal.dm @@ -4,7 +4,6 @@ button_icon_state = "quartz" button_icon = 'icons/roguetown/items/gems.dmi' cast_range = 1 - point_cost = 9 cooldown_time = 3 MINUTES essences = list(/datum/thaumaturgical_essence/magic, /datum/thaumaturgical_essence/crystal) @@ -94,7 +93,6 @@ return granted_spell = new stored_spell_type(user) - granted_spell.point_cost = 0 granted_spell.cooldown_time = 0 granted_spell.background_icon_state = "spelltemp" granted_spell.base_background_icon_state = "spelltemp0" diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/stabilize.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/stabilize.dm index 63ead1431d4..a5bad24e56b 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/stabilize.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/stabilize.dm @@ -3,8 +3,6 @@ desc = "Prevents objects from moving or falling for a short time." button_icon_state = "ravox_tug" cast_range = 2 - point_cost = 4 - attunements = list(/datum/attunement/earth) essences = list(/datum/thaumaturgical_essence/order) /datum/action/cooldown/spell/essence/stabilize/cast(atom/cast_on) diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/stone_shape.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/stone_shape.dm index b3e3aaf24ba..b17f3de69af 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/stone_shape.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/stone_shape.dm @@ -4,8 +4,6 @@ button_icon_state = "magicians_brick" sound = 'sound/magic/whiteflame.ogg' cast_range = 2 - point_cost = 4 - attunements = list(/datum/attunement/time) essences = list(/datum/thaumaturgical_essence/earth) /datum/action/cooldown/spell/essence/stone_shape/cast(atom/cast_on) diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/toxic_cleanse.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/toxic_cleanse.dm index 9b7659fcc70..f46b63053d5 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/toxic_cleanse.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/toxic_cleanse.dm @@ -3,8 +3,6 @@ desc = "Completely purges an area of all toxic substances and poisons." button_icon_state = "detect_poison" cast_range = 3 - point_cost = 7 - attunements = list(/datum/attunement/life, /datum/attunement/blood) essences = list(/datum/thaumaturgical_essence/poison, /datum/thaumaturgical_essence/water) /datum/action/cooldown/spell/essence/toxic_cleanse/cast(atom/cast_on) @@ -38,3 +36,14 @@ /datum/status_effect/buff/toxin_immunity/on_remove() . = ..() REMOVE_TRAIT(owner, TRAIT_TOXINLOVER, TRAIT_STATUS_EFFECT(id)) + +/datum/action/cooldown/spell/essence/toxic_cleanse/spell + name = "Detoxify" + charge_required = TRUE + charge_time = 5 SECONDS + spell_cost = 80 + spell_type = SPELL_MANA + + required_form = FORM_LIFE + required_technique = TECHNIQUE_RESTORATION + required_level = 6 diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/transcribe.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/transcribe.dm index c8b59cbbdca..336f2a72daf 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/transcribe.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/transcribe.dm @@ -19,9 +19,7 @@ desc = "A magic piece of paper transcribes whats happening around." button_icon_state = "transcribe" cast_range = 1 - point_cost = 4 cooldown_time = 3 MINUTES - attunements = list(/datum/attunement/polymorph) essences = list(/datum/thaumaturgical_essence/order, /datum/thaumaturgical_essence/light) var/item_type = /obj/item/paper/magictranscription @@ -204,3 +202,10 @@ . += span_notice("It contains [storedinfo.len] recorded speech\s ([mins]m [secs]s of magic remaining).") else . += span_notice("It's blank and ready to record.") + +/datum/action/cooldown/spell/essence/transcribe/spell + name = "Arcane Transcription" + spell_cost = 20 + spell_type = SPELL_MANA + + required_form = FORM_ARCANE diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/vigor.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/vigor.dm index 0848ad3b864..df20820f7f7 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/vigor.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/vigor.dm @@ -3,8 +3,6 @@ desc = "Increases physical strength and endurance temporarily." button_icon_state = "bat_transform" cast_range = 1 - point_cost = 4 - attunements = list(/datum/attunement/life) essences = list(/datum/thaumaturgical_essence/life) /datum/action/cooldown/spell/essence/vigor/cast(atom/cast_on) @@ -13,7 +11,7 @@ if(!istype(target)) target = owner owner.visible_message(span_notice("[target] appears invigorated.")) - target.apply_status_effect(/datum/status_effect/buff/vigor, 60 SECONDS) + target.apply_status_effect(/datum/status_effect/buff/vigor, 10 MINUTES) new /obj/effect/temp_visual/snake/twin_up(null, target) /atom/movable/screen/alert/status_effect/vigor @@ -24,7 +22,7 @@ /datum/status_effect/buff/vigor id = "vigor" alert_type = /atom/movable/screen/alert/status_effect/vigor - duration = 60 SECONDS + duration = 10 MINUTES effectedstats = list(STAT_STRENGTH = 1, STAT_ENDURANCE = 1) /datum/status_effect/buff/vigor/on_apply() @@ -39,3 +37,13 @@ . = ..() REMOVE_TRAIT(owner, TRAIT_STRONG_GRABBER, TRAIT_STATUS_EFFECT(id)) to_chat(owner, span_notice("The supernatural vigor fades.")) + +/datum/action/cooldown/spell/essence/vigor/spell + name = "Greater Vigor" + charge_required = TRUE + charge_time = 3 SECONDS + spell_cost = 70 + spell_type = SPELL_MANA + + required_form = FORM_LIFE + required_technique = TECHNIQUE_ALTERATION diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/warmth.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/warmth.dm index 37926b66ccc..e4b7f7adaa7 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/warmth.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/warmth.dm @@ -3,8 +3,6 @@ desc = "Provides resistance to cold and warms the body." button_icon_state = "warmth" cast_range = 1 - point_cost = 3 - attunements = list(/datum/attunement/fire) essences = list(/datum/thaumaturgical_essence/fire) /datum/action/cooldown/spell/essence/warmth/cast(atom/cast_on) @@ -35,3 +33,12 @@ . = ..() REMOVE_TRAIT(owner, TRAIT_RESISTCOLD, TRAIT_STATUS_EFFECT(id)) to_chat(owner, span_notice("The magical warmth fades away.")) + +/datum/action/cooldown/spell/essence/warmth/spell + name = "Lesser Warmth" + charge_required = TRUE + charge_time = 1 SECONDS + spell_cost = 40 + spell_type = SPELL_MANA + + required_form = FORM_FIRE diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/water_breathing.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/water_breathing.dm index f7be886f540..5e87aaaabb7 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/water_breathing.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/water_breathing.dm @@ -3,9 +3,8 @@ desc = "Allows breathing underwater for a short duration." button_icon_state = "water_breathing" cast_range = 1 - point_cost = 4 - attunements = list(/datum/attunement/blood) essences = list(/datum/thaumaturgical_essence/water) + var/duration = 60 SECONDS /datum/action/cooldown/spell/essence/water_breathing/cast(atom/cast_on) . = ..() @@ -13,7 +12,7 @@ if(!istype(target)) target = owner owner.visible_message(span_notice("[target] gains the ability to breathe underwater.")) - target.apply_status_effect(/datum/status_effect/buff/water_breathing, 60 SECONDS) + target.apply_status_effect(/datum/status_effect/buff/water_breathing, duration) /atom/movable/screen/alert/status_effect/water_breathing name = "Water Breathing" @@ -34,3 +33,13 @@ . = ..() REMOVE_TRAIT(owner, TRAIT_NODROWN, TRAIT_STATUS_EFFECT(id)) to_chat(owner, span_notice("Your ability to breathe underwater fades.")) + +/datum/action/cooldown/spell/essence/water_breathing/spell + name = "Transmogifiy: Gills" + charge_required = TRUE + charge_time = 3 SECONDS + spell_cost = 50 + spell_type = SPELL_MANA + + required_form = FORM_WATER + duration = 5 MINUTES diff --git a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/wind_step.dm b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/wind_step.dm index e177c953a78..e84dec69aea 100644 --- a/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/wind_step.dm +++ b/code/modules/crafting/alchemy/essence_machines/essence_gauntlet/spells/wind_step.dm @@ -3,9 +3,7 @@ desc = "Allows rapid movement by riding currents of air." button_icon_state = "haste" cast_range = 0 - point_cost = 6 has_visual_effects = FALSE - attunements = list(/datum/attunement/aeromancy, /datum/attunement/aeromancy) essences = list(/datum/thaumaturgical_essence/motion, /datum/thaumaturgical_essence/air) /datum/action/cooldown/spell/essence/wind_step/cast(atom/cast_on) diff --git a/code/modules/crafting/quality_of_crafting/books.dm b/code/modules/crafting/quality_of_crafting/books.dm index 4823ef34505..49e85535612 100644 --- a/code/modules/crafting/quality_of_crafting/books.dm +++ b/code/modules/crafting/quality_of_crafting/books.dm @@ -543,6 +543,7 @@ GLOBAL_LIST_EMPTY(linked_recipe_cache) /datum/book_entry/grimoire, /datum/book_entry/attunement, /datum/book_entry/mana_sources, + /datum/spellcraft_contribution, /datum/arcyne_crafting_recipe, /datum/repeatable_crafting_recipe/arcyne, /datum/blueprint_recipe/arcyne, diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm index d756ddadc57..cc01122ee94 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -138,14 +138,10 @@ /// Innate spells that get removed when the job is removed var/list/spells - /// Spell points to give/take to the mob - var/spell_points - - /// Upper number of attunements to grant - var/attunements_max - - /// Lower number of attunemnets to grant - var/attunements_min + /// Form points to give/take to the mob + var/form_points + /// Technique points to give/take to the mob + var/technique_points var/banned_leprosy = TRUE var/banned_lunatic = TRUE @@ -376,9 +372,11 @@ spawned.set_apprentice_name(apprentice_name) add_spells(spawned) - spawned.adjust_spell_points(spell_points) - spawned.generate_random_attunements(rand(attunements_min, attunements_max)) + if(form_points) + spawned.adjust_form_mastery_points(form_points) + if(technique_points) + spawned.adjust_technique_mastery_points(technique_points) // When we have sourced skill mods (praying, add to this as well) if(clear_job_stats) // Reset for most non-advclasses spawned.remove_stat_modifier(STATMOD_JOB) @@ -539,7 +537,11 @@ else spawned.adjust_skillrank(skill, -amount_or_list, TRUE) - spawned.adjust_spell_points(-spell_points) + if(form_points) + spawned.adjust_form_mastery_points(-form_points) + if(technique_points) + spawned.adjust_technique_mastery_points(-technique_points) + remove_spells(spawned) spawned.remove_stat_modifier(STATMOD_JOB) @@ -792,7 +794,10 @@ /datum/job/proc/add_spells(mob/living/equipped_human) for(var/datum/action/cooldown/spell/spell as anything in spells) - equipped_human.add_spell(spell, source = src) + var/formed = FALSE + if(initial(spell.required_form)) + formed = TRUE + equipped_human.add_spell(spell, source = src, mastery_spell = formed) /datum/job/proc/remove_spells(mob/living/equipped_human) equipped_human.remove_spells(source = src) @@ -1055,3 +1060,43 @@ return FALSE return TRUE + +/datum/job/proc/grant_selected_spellbooks(mob/living/carbon/human/spawned, list/selectable_books, amount = 2) + var/list/remaining = selectable_books.Copy() + var/list/chosen_paths = list() + + for(var/i in 1 to amount) + if(!length(remaining)) + break + + var/choice = input(spawned, "Choose a spellbook ([i] of [amount]):", "Spellbook Selection") as null|anything in remaining + if(!choice) + choice = pick(remaining) + + var/picked_path = remaining[choice] + chosen_paths += picked_path + remaining -= choice + + for(var/path in chosen_paths) + place_spellbook(spawned, path) + +/datum/job/proc/place_spellbook(mob/living/carbon/human/spawned, path) + var/obj/item/new_item = new path(spawned) + + var/obj/item/container = spawned.get_item_by_slot(ITEM_SLOT_BACK_L) + if(!container || !attempt_insert_with_flipping(container, new_item, null, TRUE, TRUE)) + container = spawned.get_item_by_slot(ITEM_SLOT_BACK_R) + if(!container || !attempt_insert_with_flipping(container, new_item, null, TRUE, TRUE)) + if(!spawned.put_in_hands(new_item)) + container = spawned.get_item_by_slot(ITEM_SLOT_BELT) + if(!container || !attempt_insert_with_flipping(container, new_item, null, TRUE, TRUE)) + new_item.forceMove(get_turf(spawned)) + message_admins("[spawned] had a granted spellbook ([path]) with no room to store: [new_item]") + +/datum/job/proc/attempt_insert_with_flipping(obj/item/storage_item, obj/item/object_to_insert, mob/living/carbon/human/H, silent, force) + var/success = FALSE + success = SEND_SIGNAL(storage_item, COMSIG_TRY_STORAGE_INSERT, object_to_insert, H, silent, force) + if(!success) + object_to_insert.inventory_flip() + success = SEND_SIGNAL(storage_item, COMSIG_TRY_STORAGE_INSERT, object_to_insert, H, silent, force) + return success diff --git a/code/modules/jobs/job_types/adventurer/types/antag/roguemage.dm b/code/modules/jobs/job_types/adventurer/types/antag/roguemage.dm index b2c07bf19ad..269996add1f 100644 --- a/code/modules/jobs/job_types/adventurer/types/antag/roguemage.dm +++ b/code/modules/jobs/job_types/adventurer/types/antag/roguemage.dm @@ -58,7 +58,7 @@ cmode_music = 'sound/music/cmode/antag/CombatRogueMage.ogg' exp_types_granted = list(EXP_TYPE_COMBAT, EXP_TYPE_MAGICK) magic_user = TRUE - spell_points = 8 + form_points = 4 attribute_sheet = /datum/attribute_holder/sheet/job/roguemage attribute_sheet_old = /datum/attribute_holder/sheet/job/roguemage/old @@ -70,11 +70,25 @@ /datum/job/advclass/bandit/roguemage/after_spawn(mob/living/carbon/human/spawned, client/player_client) . = ..() if(spawned.age == AGE_OLD) - spawned.adjust_spell_points(1) + spawned.adjust_form_mastery_points(1) if(prob(1)) spawned.cmode_music = 'sound/music/cmode/antag/combat_evilwizard.ogg' + var/list/selectable_books = list( + "Blazing Tome (Fire)" = /obj/item/spellbook/apprentice/starter/fire, + "Frostbound Tome (Ice)" = /obj/item/spellbook/apprentice/starter/ice, + "Storm-Charged Tome (Lightning)" = /obj/item/spellbook/apprentice/starter/lightning, + "Stoneveined Tome (Earth)" = /obj/item/spellbook/apprentice/starter/earth, + "Thrice-Warded Tome (Arcane)" = /obj/item/spellbook/apprentice/starter/arcane, + "Grave-Touched Tome (Death)" = /obj/item/spellbook/apprentice/starter/death, + "Verdant Tome (Life)" = /obj/item/spellbook/apprentice/starter/life, + "Windswept Tome (Air)" = /obj/item/spellbook/apprentice/starter/air, + "Tidebound Tome (Water)" = /obj/item/spellbook/apprentice/starter/water, + ) + + INVOKE_ASYNC(src, PROC_REF(grant_selected_spellbooks), spawned, selectable_books, 1) + /datum/job/advclass/bandit/roguemage/on_roundstart(mob/living/spawned, client/player_client) . = ..() @@ -101,7 +115,7 @@ belt = /obj/item/storage/belt/leather beltr = /obj/item/reagent_containers/glass/bottle/manapot backr = /obj/item/storage/backpack/satchel - backpack_contents = list(/obj/item/needle/thorn = 1, /obj/item/natural/cloth = 1, /obj/item/clothing/face/spectacles/sglasses, /obj/item/chalk = 1, /obj/item/book/granter/spellbook/apprentice = 1, /obj/item/clothing/face/shepherd/rag = 1) + backpack_contents = list(/obj/item/needle/thorn = 1, /obj/item/natural/cloth = 1, /obj/item/clothing/face/spectacles/sglasses, /obj/item/chalk = 1, /obj/item/clothing/face/shepherd/rag = 1) mask = /obj/item/clothing/face/facemask/steel neck = /obj/item/clothing/neck/coif r_hand = /obj/item/weapon/polearm/woodstaff/quarterstaff/iron diff --git a/code/modules/jobs/job_types/adventurer/types/combat/dredge.dm b/code/modules/jobs/job_types/adventurer/types/combat/dredge.dm index 09a990a20cb..1539a963ae5 100644 --- a/code/modules/jobs/job_types/adventurer/types/combat/dredge.dm +++ b/code/modules/jobs/job_types/adventurer/types/combat/dredge.dm @@ -361,11 +361,26 @@ spawned.change_stat(STAT_INTELLIGENCE, 3) spawned.change_stat(STAT_CONSTITUTION, -2) spawned.change_stat(STAT_SPEED, -2) - spawned.adjust_spell_points(6) + spawned.adjust_form_mastery_points(6) + spawned.adjust_technique_mastery_points(2) spawned.cmode_music = 'sound/music/cmode/adventurer/CombatSorcerer.ogg' to_chat(spawned,span_info("\ I've studied the arcyne, those who step to me shall perish.") ) + var/list/selectable_books = list( + "Blazing Tome (Fire)" = /obj/item/spellbook/apprentice/starter/fire, + "Frostbound Tome (Ice)" = /obj/item/spellbook/apprentice/starter/ice, + "Storm-Charged Tome (Lightning)" = /obj/item/spellbook/apprentice/starter/lightning, + "Stoneveined Tome (Earth)" = /obj/item/spellbook/apprentice/starter/earth, + "Thrice-Warded Tome (Arcane)" = /obj/item/spellbook/apprentice/starter/arcane, + "Grave-Touched Tome (Death)" = /obj/item/spellbook/apprentice/starter/death, + "Verdant Tome (Life)" = /obj/item/spellbook/apprentice/starter/life, + "Windswept Tome (Air)" = /obj/item/spellbook/apprentice/starter/air, + "Tidebound Tome (Water)" = /obj/item/spellbook/apprentice/starter/water, + ) + + INVOKE_ASYNC(src, PROC_REF(grant_selected_spellbooks), spawned, selectable_books, 1) + if("Sword2") spawned.change_stat(STAT_ENDURANCE, 1) spawned.adjust_skillrank(/datum/attribute/skill/combat/swords, 3, TRUE) @@ -968,7 +983,7 @@ head = /obj/item/clothing/head/roguehood/colored/mage armor = /obj/item/clothing/shirt/robe/colored/mage beltl = /obj/item/reagent_containers/glass/bottle/manapot - beltr = /obj/item/book/granter/spellbook/apprentice + if("Sword2") beltl = /obj/item/weapon/sword/short/iron beltr = /obj/item/weapon/sword/short/iron diff --git a/code/modules/jobs/job_types/adventurer/types/combat/mage.dm b/code/modules/jobs/job_types/adventurer/types/combat/mage.dm index 3490d069784..1fba4c1849e 100644 --- a/code/modules/jobs/job_types/adventurer/types/combat/mage.dm +++ b/code/modules/jobs/job_types/adventurer/types/combat/mage.dm @@ -39,7 +39,7 @@ blacklisted_species = list(SPEC_ID_HALFLING) exp_types_granted = list(EXP_TYPE_ADVENTURER, EXP_TYPE_COMBAT, EXP_TYPE_MAGICK) magic_user = TRUE - spell_points = 5 + form_points = 4 spells = list( /datum/action/cooldown/spell/undirected/touch/prestidigitation @@ -54,6 +54,20 @@ if(istype(spawned.patron, /datum/patron/inhumen/zizo)) spawned.grant_language(/datum/language/undead) + var/list/selectable_books = list( + "Blazing Tome (Fire)" = /obj/item/spellbook/apprentice/starter/fire, + "Frostbound Tome (Ice)" = /obj/item/spellbook/apprentice/starter/ice, + "Storm-Charged Tome (Lightning)" = /obj/item/spellbook/apprentice/starter/lightning, + "Stoneveined Tome (Earth)" = /obj/item/spellbook/apprentice/starter/earth, + "Thrice-Warded Tome (Arcane)" = /obj/item/spellbook/apprentice/starter/arcane, + "Grave-Touched Tome (Death)" = /obj/item/spellbook/apprentice/starter/death, + "Verdant Tome (Life)" = /obj/item/spellbook/apprentice/starter/life, + "Windswept Tome (Air)" = /obj/item/spellbook/apprentice/starter/air, + "Tidebound Tome (Water)" = /obj/item/spellbook/apprentice/starter/water, + ) + + INVOKE_ASYNC(src, PROC_REF(grant_selected_spellbooks), spawned, selectable_books, 2) + /datum/job/advclass/combat/mage/on_roundstart(mob/living/spawned, client/player_client) . = ..() @@ -84,7 +98,6 @@ beltl = /obj/item/reagent_containers/glass/bottle/manapot r_hand = /obj/item/weapon/polearm/woodstaff backpack_contents = list( - /obj/item/book/granter/spellbook/apprentice = 1, /obj/item/chalk = 1, ) diff --git a/code/modules/jobs/job_types/adventurer/types/wretch/hedgemage.dm b/code/modules/jobs/job_types/adventurer/types/wretch/hedgemage.dm index 6d79cb32eae..b4e52f16b9b 100644 --- a/code/modules/jobs/job_types/adventurer/types/wretch/hedgemage.dm +++ b/code/modules/jobs/job_types/adventurer/types/wretch/hedgemage.dm @@ -42,7 +42,7 @@ attribute_sheet_old = /datum/attribute_holder/sheet/job/hedgemage/old magic_user = TRUE - spell_points = 12 + form_points = 6 exp_types_granted = list(EXP_TYPE_COMBAT, EXP_TYPE_MAGICK) traits = list( @@ -57,6 +57,19 @@ . = ..() if(prob(1)) spawned.cmode_music = 'sound/music/cmode/antag/combat_evilwizard.ogg' + var/list/selectable_books = list( + "Blazing Tome (Fire)" = /obj/item/spellbook/adept/starter/fire, + "Frostbound Tome (Ice)" = /obj/item/spellbook/adept/starter/ice, + "Storm-Charged Tome (Lightning)" = /obj/item/spellbook/adept/starter/lightning, + "Stoneveined Tome (Earth)" = /obj/item/spellbook/adept/starter/earth, + "Thrice-Warded Tome (Arcane)" = /obj/item/spellbook/adept/starter/arcane, + "Grave-Touched Tome (Death)" = /obj/item/spellbook/adept/starter/death, + "Verdant Tome (Life)" = /obj/item/spellbook/adept/starter/life, + "Windswept Tome (Air)" = /obj/item/spellbook/adept/starter/air, + "Tidebound Tome (Water)" = /obj/item/spellbook/adept/starter/water, + ) + + INVOKE_ASYNC(src, PROC_REF(grant_selected_spellbooks), spawned, selectable_books, 2) /datum/job/advclass/wretch/hedgemage/on_roundstart(mob/living/carbon/human/spawned, client/player_client) . = ..() @@ -87,7 +100,6 @@ beltl = /obj/item/reagent_containers/glass/bottle/manapot r_hand = /obj/item/weapon/polearm/woodstaff/quarterstaff/steel backpack_contents = list( - /obj/item/book/granter/spellbook/adept = 1, /obj/item/chalk = 1, /obj/item/rope/chain = 1, /obj/item/reagent_containers/glass/bottle/stronghealthpot = 1, diff --git a/code/modules/jobs/job_types/adventurer/types/wretch/necromancer.dm b/code/modules/jobs/job_types/adventurer/types/wretch/necromancer.dm index 8e9f854c4ca..165ea34b3ca 100644 --- a/code/modules/jobs/job_types/adventurer/types/wretch/necromancer.dm +++ b/code/modules/jobs/job_types/adventurer/types/wretch/necromancer.dm @@ -23,7 +23,8 @@ total_positions = 1 exp_types_granted = list(EXP_TYPE_COMBAT, EXP_TYPE_MAGICK) allowed_patrons = list(/datum/patron/inhumen/zizo) - spell_points = 7 + form_points = 5 + technique_points = 3 languages = list(/datum/language/undead) faction = FACTION_CABAL @@ -33,6 +34,7 @@ TRAIT_MEDIUMARMOR, TRAIT_CABAL, TRAIT_GRAVEROBBER, + TRAIT_SORCERER, TRAIT_DEADNOSE ) @@ -40,7 +42,7 @@ /datum/action/cooldown/spell/undirected/touch/prestidigitation, /datum/action/cooldown/spell/eyebite, /datum/action/cooldown/spell/projectile/sickness, - /datum/action/cooldown/spell/conjure/raise_lesser_undead/necromancer, + /datum/action/cooldown/spell/conjure_summon/raise_lesser_undead/necromancer, /datum/action/cooldown/spell/gravemark, /datum/action/cooldown/spell/control_undead ) @@ -87,7 +89,7 @@ beltr = /obj/item/reagent_containers/glass/bottle/manapot r_hand = /obj/item/weapon/polearm/woodstaff backpack_contents = list( - /obj/item/book/granter/spellbook/adept = 1, + /obj/item/spellbook/expert/starter/death = 1, /obj/item/chalk = 1, /obj/item/rope/chain = 1, /obj/item/reagent_containers/glass/bottle/stronghealthpot = 1, diff --git a/code/modules/jobs/job_types/apprentices/wapprentice.dm b/code/modules/jobs/job_types/apprentices/wapprentice.dm index 2953491cf6e..70b685c6fc4 100644 --- a/code/modules/jobs/job_types/apprentices/wapprentice.dm +++ b/code/modules/jobs/job_types/apprentices/wapprentice.dm @@ -72,8 +72,21 @@ /datum/job/mageapprentice/after_spawn(mob/living/carbon/human/spawned, client/player_client) . = ..() if(spawned.age == AGE_ADULT) - spawned.adjust_spell_points(4) + spawned.adjust_form_mastery_points(3) + var/list/selectable_books = list( + "Blazing Tome (Fire)" = /obj/item/spellbook/apprentice/starter/fire, + "Frostbound Tome (Ice)" = /obj/item/spellbook/apprentice/starter/ice, + "Storm-Charged Tome (Lightning)" = /obj/item/spellbook/apprentice/starter/lightning, + "Stoneveined Tome (Earth)" = /obj/item/spellbook/apprentice/starter/earth, + "Thrice-Warded Tome (Arcane)" = /obj/item/spellbook/apprentice/starter/arcane, + "Grave-Touched Tome (Death)" = /obj/item/spellbook/apprentice/starter/death, + "Verdant Tome (Life)" = /obj/item/spellbook/apprentice/starter/life, + "Windswept Tome (Air)" = /obj/item/spellbook/apprentice/starter/air, + "Tidebound Tome (Water)" = /obj/item/spellbook/apprentice/starter/water, + ) + + INVOKE_ASYNC(src, PROC_REF(grant_selected_spellbooks), spawned, selectable_books, 2) /datum/outfit/mageapprentice name = JOB_MAGIC_APP @@ -86,7 +99,6 @@ shirt = /obj/item/clothing/shirt/dress/silkdress/colored/random head = /obj/item/clothing/head/wizhat/witch backpack_contents = list( - /obj/item/book/granter/spellbook/apprentice = 1, /obj/item/chalk = 1 ) diff --git a/code/modules/jobs/job_types/nobility/archivist.dm b/code/modules/jobs/job_types/nobility/archivist.dm index a333f770252..d18d2a85a7c 100644 --- a/code/modules/jobs/job_types/nobility/archivist.dm +++ b/code/modules/jobs/job_types/nobility/archivist.dm @@ -91,7 +91,6 @@ category_tags = list(CTAG_ARCHIVIST) magic_user = TRUE spells = list( - /datum/action/cooldown/spell/undirected/learn, /datum/action/cooldown/spell/undirected/touch/prestidigitation, /datum/action/cooldown/spell/undirected/conjure_item/summon_parchment, /datum/action/cooldown/spell/undirected/conjure_item/summon_parchment/scroll, @@ -100,12 +99,27 @@ attribute_sheet = /datum/attribute_holder/sheet/job/chronicler attribute_sheet_old = /datum/attribute_holder/sheet/job/chronicler/old +/datum/job/advclass/archivist/chronicler/after_spawn(mob/living/carbon/human/spawned, client/player_client) + . = ..() + var/list/selectable_books = list( + "Blazing Tome (Fire)" = /obj/item/spellbook/expert/starter/fire, + "Frostbound Tome (Ice)" = /obj/item/spellbook/expert/starter/ice, + "Storm-Charged Tome (Lightning)" = /obj/item/spellbook/expert/starter/lightning, + "Stoneveined Tome (Earth)" = /obj/item/spellbook/expert/starter/earth, + "Thrice-Warded Tome (Arcane)" = /obj/item/spellbook/expert/starter/arcane, + "Grave-Touched Tome (Death)" = /obj/item/spellbook/expert/starter/death, + "Verdant Tome (Life)" = /obj/item/spellbook/expert/starter/life, + "Windswept Tome (Air)" = /obj/item/spellbook/expert/starter/air, + "Tidebound Tome (Water)" = /obj/item/spellbook/expert/starter/water, + ) + + INVOKE_ASYNC(src, PROC_REF(grant_selected_spellbooks), spawned, selectable_books, 2) + /datum/outfit/archivist/chronicler name = "Chronicler (Archivist)" shoes = /obj/item/clothing/shoes/boots belt = /obj/item/storage/belt/leather/plaquesilver beltl = /obj/item/storage/keyring/archivist - beltr = /obj/item/book/granter/spellbook/expert backl = /obj/item/storage/backpack/satchel neck = /obj/item/clothing/neck/psycross/silver/divine/noc backpack_contents = list( diff --git a/code/modules/jobs/job_types/nobility/courtagent.dm b/code/modules/jobs/job_types/nobility/courtagent.dm index a498fd9ed17..6e60b9759e6 100644 --- a/code/modules/jobs/job_types/nobility/courtagent.dm +++ b/code/modules/jobs/job_types/nobility/courtagent.dm @@ -243,7 +243,7 @@ /datum/job/advclass/courtagent/mystic/after_spawn(mob/living/carbon/human/spawned, client/player_client) . = ..() - spawned.adjust_spell_points(10) + spawned.adjust_form_mastery_points(5) /datum/outfit/courtagent/mystic name = "Mystic Spy" diff --git a/code/modules/jobs/job_types/nobility/magician.dm b/code/modules/jobs/job_types/nobility/magician.dm index 0657222eadb..01b82366b87 100644 --- a/code/modules/jobs/job_types/nobility/magician.dm +++ b/code/modules/jobs/job_types/nobility/magician.dm @@ -63,9 +63,6 @@ cmode_music = 'sound/music/cmode/nobility/CombatCourtMagician.ogg' allowed_patrons = list(/datum/patron/divine/noc, /datum/patron/inhumen/zizo) magic_user = TRUE - spell_points = 17 - attunements_max = 6 - attunements_min = 4 job_bitflag = BITFLAG_ROYALTY max_apprentices = 2 honorary = "Archmage" @@ -77,6 +74,8 @@ /datum/action/cooldown/spell/undirected/touch/prestidigitation, ) + form_points = 4 + exp_type = list(EXP_TYPE_ADVENTURER, EXP_TYPE_LIVING, EXP_TYPE_MAGICK) exp_types_granted = list(EXP_TYPE_NOBLE, EXP_TYPE_MAGICK, EXP_TYPE_ADVENTURER) exp_requirements = list( @@ -108,6 +107,20 @@ if(spawned.gender == MALE && spawned.dna?.species && spawned.dna.species.id != SPEC_ID_MEDICATOR) spawned.dna.species.soundpack_m = new /datum/voicepack/male/wizard() + var/list/selectable_books = list( + "Blazing Tome (Fire)" = /obj/item/spellbook/expert/starter/fire, + "Frostbound Tome (Ice)" = /obj/item/spellbook/expert/starter/ice, + "Storm-Charged Tome (Lightning)" = /obj/item/spellbook/expert/starter/lightning, + "Stoneveined Tome (Earth)" = /obj/item/spellbook/expert/starter/earth, + "Thrice-Warded Tome (Arcane)" = /obj/item/spellbook/expert/starter/arcane, + "Grave-Touched Tome (Death)" = /obj/item/spellbook/expert/starter/death, + "Verdant Tome (Life)" = /obj/item/spellbook/expert/starter/life, + "Windswept Tome (Air)" = /obj/item/spellbook/expert/starter/air, + "Tidebound Tome (Water)" = /obj/item/spellbook/expert/starter/water, + ) + + INVOKE_ASYNC(src, PROC_REF(grant_selected_spellbooks), spawned, selectable_books, 2) + /datum/job/magician/on_roundstart(mob/living/spawned, client/player_client) . = ..() @@ -142,7 +155,6 @@ /obj/item/scrying = 1, /obj/item/chalk = 1, /obj/item/reagent_containers/glass/bottle/killersice = 1, - /obj/item/book/granter/spellbook/master = 1, /obj/item/weapon/knife/dagger/silver/arcyne = 1, /obj/item/storage/keyring/mage = 1 ) diff --git a/code/modules/jobs/job_types/nobility/minor_noble.dm b/code/modules/jobs/job_types/nobility/minor_noble.dm index 6e6d667a8b2..4cb28ec2274 100644 --- a/code/modules/jobs/job_types/nobility/minor_noble.dm +++ b/code/modules/jobs/job_types/nobility/minor_noble.dm @@ -186,6 +186,22 @@ TRAIT_NOBLE_POWER ) +/datum/job/advclass/minornoble/magickal_graduate/after_spawn(mob/living/carbon/human/spawned, client/player_client) + . = ..() + var/list/selectable_books = list( + "Blazing Tome (Fire)" = /obj/item/spellbook/apprentice/starter/fire, + "Frostbound Tome (Ice)" = /obj/item/spellbook/apprentice/starter/ice, + "Storm-Charged Tome (Lightning)" = /obj/item/spellbook/apprentice/starter/lightning, + "Stoneveined Tome (Earth)" = /obj/item/spellbook/apprentice/starter/earth, + "Thrice-Warded Tome (Arcane)" = /obj/item/spellbook/apprentice/starter/arcane, + "Grave-Touched Tome (Death)" = /obj/item/spellbook/apprentice/starter/death, + "Verdant Tome (Life)" = /obj/item/spellbook/apprentice/starter/life, + "Windswept Tome (Air)" = /obj/item/spellbook/apprentice/starter/air, + "Tidebound Tome (Water)" = /obj/item/spellbook/apprentice/starter/water, + ) + + INVOKE_ASYNC(src, PROC_REF(grant_selected_spellbooks), spawned, selectable_books, 2) + /datum/outfit/minornoble/magickal_graduate name = "Magical Graduate (noble)" head = /obj/item/clothing/head/wizhat/gen @@ -194,7 +210,6 @@ cloak = /obj/item/clothing/cloak/raincloak/furcloak backr = /obj/item/storage/backpack/satchel/black backpack_contents = list( - /obj/item/book/granter/spellbook/apprentice = 1, /obj/item/chalk = 1 ) diff --git a/code/modules/jobs/job_types/other/merc_classes/mercmage.dm b/code/modules/jobs/job_types/other/merc_classes/mercmage.dm index a62064739da..d8b4d72cd8a 100644 --- a/code/modules/jobs/job_types/other/merc_classes/mercmage.dm +++ b/code/modules/jobs/job_types/other/merc_classes/mercmage.dm @@ -64,7 +64,8 @@ allowed_patrons = list(/datum/patron/divine/noc, /datum/patron/inhumen/zizo)//only noc or zizo worshippers can be mages exp_types_granted = list(EXP_TYPE_MERCENARY, EXP_TYPE_COMBAT, EXP_TYPE_MAGICK) magic_user = TRUE - spell_points = 8 //less than courtmagician, more than an adventurer wizard + form_points = 8 + technique_points = 1 attribute_sheet = /datum/attribute_holder/sheet/job/sellmage attribute_sheet_old = /datum/attribute_holder/sheet/job/sellmage/old @@ -81,6 +82,20 @@ if(prob(1)) //extremely rare just like court mage spawned.cmode_music = 'sound/music/cmode/antag/combat_evilwizard.ogg' + var/list/selectable_books = list( + "Blazing Tome (Fire)" = /obj/item/spellbook/adept/starter/fire, + "Frostbound Tome (Ice)" = /obj/item/spellbook/adept/starter/ice, + "Storm-Charged Tome (Lightning)" = /obj/item/spellbook/adept/starter/lightning, + "Stoneveined Tome (Earth)" = /obj/item/spellbook/adept/starter/earth, + "Thrice-Warded Tome (Arcane)" = /obj/item/spellbook/adept/starter/arcane, + "Grave-Touched Tome (Death)" = /obj/item/spellbook/adept/starter/death, + "Verdant Tome (Life)" = /obj/item/spellbook/adept/starter/life, + "Windswept Tome (Air)" = /obj/item/spellbook/adept/starter/air, + "Tidebound Tome (Water)" = /obj/item/spellbook/adept/starter/water, + ) + + INVOKE_ASYNC(src, PROC_REF(grant_selected_spellbooks), spawned, selectable_books, 2) + /datum/job/advclass/mercenary/sellmage/on_roundstart(mob/living/spawned, client/player_client) . = ..() @@ -114,7 +129,6 @@ backr = /obj/item/storage/backpack/satchel backl = /obj/item/weapon/polearm/woodstaff/quarterstaff/iron backpack_contents = list( - /obj/item/book/granter/spellbook/adept = 1, /obj/item/chalk = 1, /obj/item/reagent_containers/glass/bottle/manapot = 1 ) diff --git a/code/modules/jobs/job_types/other/merc_classes/spellsword.dm b/code/modules/jobs/job_types/other/merc_classes/spellsword.dm index 78e90696d9f..9fb126a5d7d 100644 --- a/code/modules/jobs/job_types/other/merc_classes/spellsword.dm +++ b/code/modules/jobs/job_types/other/merc_classes/spellsword.dm @@ -29,17 +29,27 @@ blacklisted_species = list(SPEC_ID_HALFLING) exp_types_granted = list(EXP_TYPE_MERCENARY, EXP_TYPE_COMBAT, EXP_TYPE_MAGICK) magic_user = TRUE - spell_points = 5 + form_points = 7 + technique_points = 2 + + traits = list( + TRAIT_SORCERER, + ) attribute_sheet = /datum/attribute_holder/sheet/job/spellsword spells = list( - /datum/action/cooldown/spell/undirected/touch/prestidigitation + /datum/action/cooldown/spell/undirected/touch/prestidigitation, + /datum/action/cooldown/spell/bind_weapon, + /datum/action/cooldown/spell/recall_weapon, + /datum/action/cooldown/spell/empower_weapon, + /datum/action/cooldown/spell/essence/mend/spell, ) /datum/job/advclass/mercenary/spellsword/after_spawn(mob/living/carbon/human/spawned, client/player_client) . = ..() spawned.merctype = 9 + spawned.adjust_technique_mastery_points(3, FALSE, TECHNIQUE_IMBUE) /datum/outfit/mercenary/spellsword name = "Spellsword (Mercenary)" @@ -59,6 +69,5 @@ /obj/item/storage/belt/pouch/coins/poor = 1, /obj/item/weapon/knife/dagger = 1, /obj/item/reagent_containers/glass/bottle/manapot = 1, - /obj/item/book/granter/spellbook/apprentice = 1, /obj/item/chalk = 1 ) diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index dbec5ff4c4d..9eba073da2d 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -220,6 +220,10 @@ if(!throwable_mob.buckled) var/obj/item/grabbing/other_grab = offhand ? get_active_held_item() : get_inactive_held_item() if(grab_state < GRAB_AGGRESSIVE) + if(HAS_TRAIT(throwable_mob, TRAIT_BIGGUY)) + return + if(!HAS_TRAIT(src,TRAIT_BIGGUY)) + return stop_pulling(pulling_broke_free = TRUE) return stop_pulling(pulling_broke_free = TRUE) diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm index 40336ca2dc7..d946bcd4e3a 100644 --- a/code/modules/mob/living/carbon/carbon_defense.dm +++ b/code/modules/mob/living/carbon/carbon_defense.dm @@ -75,7 +75,12 @@ /mob/living/carbon/check_projectile_dismemberment(obj/projectile/P, def_zone) var/obj/item/bodypart/affecting = get_bodypart(check_zone(def_zone)) - if(affecting && affecting.dismemberable && affecting.get_damage() >= (affecting.max_damage - P.dismemberment)) + + if(istype(P, /obj/projectile/magic) && (P.woundclass == BCLASS_CUT || P.woundclass == BCLASS_CHOP)) + var/dismember_chance = affecting.get_spell_dismemberment_chance(P.damage, P.woundclass, def_zone) + if(affecting && affecting.dismemberable && prob(dismember_chance)) + affecting.dismember(P.damtype, P.woundclass) + else if(affecting && affecting.dismemberable && affecting.get_damage() >= (affecting.max_damage - P.dismemberment)) affecting.dismember(P.damtype, P.woundclass) /mob/living/carbon/proc/can_catch_item(skip_throw_mode_check) diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index b36f3131745..f1a50f4d56a 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -1,6 +1,8 @@ /mob/living/carbon/human/getarmor(def_zone, type, damage, armor_penetration, blade_dulling, simulate=FALSE) var/armorval = 0 var/organnum = 0 + if(has_status_effect(/datum/status_effect/buff/iron_skin)) + damage *= 0.75 if(def_zone) return checkarmor(def_zone, type, damage, armor_penetration, blade_dulling, simulate) @@ -242,7 +244,7 @@ . = ..() -/mob/living/carbon/human/attacked_by(obj/item/I, mob/living/user) +/mob/living/carbon/human/attacked_by(obj/item/I, mob/living/user, signal) if(!I || !user) return 0 @@ -268,7 +270,7 @@ SSblackbox.record_feedback("tally", "zone_targeted", 1, useder) // the attacked_by code varies among species - return dna.species.spec_attacked_by(I, user, affecting, used_intent, src, useder, accurate) + return dna.species.spec_attacked_by(I, user, affecting, used_intent, src, useder, accurate, signal) /mob/living/carbon/human/attack_hand(mob/user) . = ..() diff --git a/code/modules/mob/living/carbon/human/npc/rousman.dm b/code/modules/mob/living/carbon/human/npc/rousman.dm index 59d86b9b4d9..df2339bb96e 100644 --- a/code/modules/mob/living/carbon/human/npc/rousman.dm +++ b/code/modules/mob/living/carbon/human/npc/rousman.dm @@ -643,8 +643,10 @@ GLOBAL_LIST_EMPTY(rousman_ambush_objects) /datum/action/cooldown/spell/sundering_lightning, ) - seer.adjust_spell_points(17) - seer.generate_random_attunements(rand(4,6)) + //! MAGIC BALANCE POINT + ADD_TRAIT(seer, TRAIT_SORCERER, INNATE_TRAIT) + seer.adjust_technique_mastery_points(12) + seer.adjust_form_mastery_points(20) seer.mana_pool.set_intrinsic_recharge(MANA_ALL_LEYLINES) seer.mana_pool.adjust_mana(100) for(var/spell in spells) @@ -675,7 +677,7 @@ GLOBAL_LIST_EMPTY(rousman_ambush_objects) head = /obj/item/clothing/head/roguehood/rousman/rousseer r_hand = /obj/item/weapon/polearm/woodstaff/seer belt = /obj/item/storage/belt/leather/black - l_pocket = /obj/item/book/granter/spellbook/expert + l_pocket = /obj/item/spellbook/expert/starter/earth var/list/spells = list( /datum/action/cooldown/spell/undirected/jaunt/ethereal_jaunt, @@ -689,9 +691,9 @@ GLOBAL_LIST_EMPTY(rousman_ambush_objects) /datum/action/cooldown/spell/sundering_lightning, ) - seer.adjust_spell_points(17) - seer.generate_random_attunements(rand(4,6)) + seer.adjust_technique_mastery_points(14) + seer.adjust_form_mastery_points(20) seer.mana_pool.set_intrinsic_recharge(MANA_ALL_LEYLINES) seer.mana_pool.adjust_mana(100) for(var/spell in spells) - seer.add_spell(spell) + seer.add_spell(spell, mastery_spell = TRUE) diff --git a/code/modules/mob/living/carbon/human/species.dm b/code/modules/mob/living/carbon/human/species.dm index 95d6467d5cf..2f5e0beedc8 100644 --- a/code/modules/mob/living/carbon/human/species.dm +++ b/code/modules/mob/living/carbon/human/species.dm @@ -1820,7 +1820,7 @@ harm(M, H, attacker_style) // We need to remove this -/datum/species/proc/spec_attacked_by(obj/item/I, mob/living/user, obj/item/bodypart/affecting, intent, mob/living/carbon/human/H, selzone, accurate = FALSE) +/datum/species/proc/spec_attacked_by(obj/item/I, mob/living/user, obj/item/bodypart/affecting, intent, mob/living/carbon/human/H, selzone, accurate = FALSE, signal) if(!I || !affecting) return FALSE @@ -1873,6 +1873,8 @@ var/def_zone = affecting.body_zone var/armor_block = H.run_armor_check(selzone, I.damage_type, "", "", pen, damage = item_force, blade_dulling = user.used_intent.blade_class) + if(signal & COMPONENT_ITEM_NO_DEFENSE) + armor_block = 0 var/weakness = H.check_weakness(I, user) var/actual_damage = apply_damage(item_force * weakness, I.damtype, def_zone, armor_block, H, skip_dtype = TRUE) SEND_SIGNAL(I, COMSIG_ITEM_SPEC_ATTACKEDBY, H, user, affecting, actual_damage) diff --git a/code/modules/mob/living/damage_procs.dm b/code/modules/mob/living/damage_procs.dm index 0fd6af533f9..7f78cf9f0c3 100644 --- a/code/modules/mob/living/damage_procs.dm +++ b/code/modules/mob/living/damage_procs.dm @@ -343,6 +343,14 @@ * @return TRUE if defense successful, FALSE otherwise */ /mob/living/proc/checkdefense(datum/intent/intenty, mob/living/user) + // We check for a disruptable swingdelay first. + var/datum/status_effect/swingdelay/disrupt/SW = has_status_effect(/datum/status_effect/swingdelay/disrupt) + if(SW) + if(!SW.is_disrupted()) + SW.attacked() + swing_state = FALSE + return FALSE + if(!cmode || stat || (HAS_TRAIT(src, TRAIT_UNPARRYING) && HAS_TRAIT(src, TRAIT_UNDODGING)) || user == src || HAS_TRAIT(src, TRAIT_IMMOBILIZED)) return FALSE if(client && used_intent && client.charging && used_intent.tranged && !used_intent.tshield) diff --git a/code/modules/mob/living/emote.dm b/code/modules/mob/living/emote.dm index 4f3d2ffeb18..a9a925f4d97 100644 --- a/code/modules/mob/living/emote.dm +++ b/code/modules/mob/living/emote.dm @@ -534,6 +534,15 @@ message_muffled = "makes a muffled groan." emote_type = EMOTE_AUDIBLE +/datum/emote/living/attack + key = "attack" + key_third_person = "attacks" + message = "" + nomsg = TRUE + message_muffled = "makes a muffled groan." + emote_type = EMOTE_AUDIBLE + show_runechat = FALSE + /mob/living/carbon/human/verb/emote_groan() set name = "Groan" set category = "Emotes.Noises" diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 5890c6421a4..9df947a9ae9 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1044,6 +1044,10 @@ updatehealth() get_up(TRUE) + // Reapply arcyne momentum if this mind had it before death + if(mind?.has_arcyne_momentum && !has_status_effect(/datum/status_effect/buff/arcyne_momentum)) + apply_status_effect(/datum/status_effect/buff/arcyne_momentum) + // The signal is called after everything else so components can properly check the updated values SEND_SIGNAL(src, COMSIG_LIVING_REVIVE, full_heal_flags) @@ -3026,11 +3030,11 @@ * defaults to src and mind makes it transfer with the mind to new mobs. * * override - Replace existing spell if present, instead of returning early */ -/mob/living/proc/add_spell(datum/action/cooldown/spell/spell_type, silent = TRUE, source, override = FALSE) +/mob/living/proc/add_spell(datum/action/cooldown/spell/spell_type, silent = TRUE, source, override = FALSE, mastery_spell = FALSE) if(QDELETED(src)) return - var/datum/action/cooldown/spell = get_spell(spell_type, TRUE) + var/datum/action/cooldown/spell/spell = get_spell(spell_type, TRUE) if(spell) if(!override) return @@ -3047,6 +3051,9 @@ to_chat(src, span_nicegreen("I learnt [spell.name]!")) spell.Grant(src) + if(mastery_spell && spell.required_form) + var/datum/spell_mastery/mastery = mana_pool?.get_mastery() + mastery?.grant_bonus_spell(spell, src) /mob/living/proc/remove_spell(datum/action/cooldown/spell/spell, return_skill_points = FALSE, silent = TRUE) if(QDELETED(src)) @@ -3056,14 +3063,13 @@ if(!real_spell) return - if(return_skill_points) - used_spell_points = max(used_spell_points - real_spell.point_cost, 0) - spell_points = max(spell_points + real_spell.point_cost, 0) - check_learnspell() - if(!silent) to_chat(src, span_boldwarning("I forgot [real_spell.name]!")) + var/datum/spell_mastery/mastery = mana_pool?.get_mastery() + if(mastery && (real_spell in mastery.granted_actions)) + mastery.granted_actions -= real_spell + mastery.unlocked_spells -= real_spell.type qdel(real_spell) /** @@ -3090,50 +3096,48 @@ if(!silent && !silent_individual) to_chat(src, span_boldwarning("I forgot all my spells!")) -/** - * adjusts the amount of available spellpoints - * - * Args - * * points - amount of points to grant or reduce - * * used_points - ajust used points -*/ -/mob/proc/adjust_spell_points(points, used_points = FALSE) +/mob/proc/adjust_form_mastery_points(points, used_points = FALSE, specific_form = null) + return -/mob/living/adjust_spell_points(points, used_points = FALSE) +/mob/living/adjust_form_mastery_points(points, used_points = FALSE, specific_form = null) if(QDELETED(src)) return - if(used_points) - used_spell_points += points - else - spell_points += points + mana_pool?.get_mastery().adjust_form_mastery_points(points, used_points, specific_form) - check_learnspell() +/mob/proc/adjust_technique_mastery_points(points, used_points = FALSE, specific_technique = null) + return -/// Reset spell points and used spell points -/mob/living/proc/reset_spell_points(silent = TRUE) +/mob/living/adjust_technique_mastery_points(points, used_points = FALSE, specific_technique = null) if(QDELETED(src)) return - spell_points = 0 - used_spell_points = 0 + mana_pool?.get_mastery().adjust_technique_mastery_points(points, used_points, specific_technique) - if(!silent) - to_chat(src, span_boldwarning("I lost all my spellpoints!")) +/mob/proc/reset_form_mastery_points(silent = TRUE) + return - check_learnspell() +/mob/living/reset_form_mastery_points(silent = TRUE) + if(QDELETED(src)) + return -/// Check if learnspell should be removed or granted -/mob/living/proc/check_learnspell() + mana_pool?.get_mastery().reset_form_mastery_points(silent) + +/mob/proc/reset_technique_mastery_points(silent = TRUE) + return + +/mob/living/reset_technique_mastery_points(silent = TRUE) if(QDELETED(src)) return - if(get_spell(/datum/action/cooldown/spell/undirected/learn)) + mana_pool?.get_mastery().reset_technique_mastery_points(silent) + +/// Check if learnspell should be removed or granted +/mob/living/proc/check_learnspell() + if(QDELETED(src)) return - // Because of kobolds spellpoints can be decimal, but you can't do anything with that if below 1 - if(floor(spell_points - used_spell_points) > 0) - add_spell(/datum/action/cooldown/spell/undirected/learn) + mana_pool?.get_mastery() /** * purges all spells and skills @@ -3143,7 +3147,8 @@ /mob/living/proc/purge_combat_knowledge(silent = TRUE) purge_all_skills(silent) remove_spells(silent = silent) - reset_spell_points(silent) + reset_technique_mastery_points(silent) + reset_form_mastery_points(silent) /mob/living/proc/offer_item(mob/living/offered_to, obj/offered_item) if(isnull(offered_to) || isnull(offered_item)) diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index 5bb5c0827b3..fc669baa48d 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -381,10 +381,12 @@ return FALSE if(HAS_TRAIT(src, TRAIT_SHOCKIMMUNE)) return FALSE - if(shock_damage < 1) + if(shock_damage < 1 && !(flags & SHOCK_VISUAL_ONLY)) return FALSE - if(!(flags & SHOCK_ILLUSION)) - adjustFireLoss(shock_damage) + if(!(flags & SHOCK_VISUAL_ONLY)) + if(!(flags & SHOCK_ILLUSION)) + adjustFireLoss(shock_damage) + visible_message( "[src] was shocked by [source ? "\the [source]" : "something"]!", \ "I feel a powerful shock coursing through my body!", \ diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index 9dd716b0660..925cb666f09 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -256,10 +256,12 @@ var/pegleg = 0 //Handles check & slowdown for peglegs. Fuckin' bootleg, literally, but hey it at least works. var/pet_passive = FALSE - /// amount of spell points this mob currently has - var/spell_points - /// amount of spell points this mob has used - var/used_spell_points + var/list/summoned_minions + var/attack_relay_refs = 0 + var/attack_relay_self_added = FALSE + + /// Whether we are in a swingdelay, used to check for disrupted swingdelays. + var/swing_state = FALSE var/list/affixes = list() var/delve_level = 0 @@ -295,3 +297,5 @@ var/looking_vertically = NONE ///looking holder we use for look_up and look_down. we use this over resetting to the turf because we want to glide var/atom/movable/looking_holder/looking_holder + ///The NAME (not the reference) of the mob's summoner and probable master. + var/summoner = null diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/creacher/broodmother.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/creacher/broodmother.dm index c585bda5831..127c9ad8454 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/creacher/broodmother.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/creacher/broodmother.dm @@ -114,6 +114,8 @@ button_icon_state = "acidsplash" sound = 'sound/magic/whiteflame.ogg' + required_form = null + charge_time = 3 SECONDS charge_slowdown = 0.7 cooldown_time = 30 SECONDS diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/primordial.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/primordial.dm new file mode 100644 index 00000000000..5b11ced28f4 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/primordial.dm @@ -0,0 +1,404 @@ + //PRIMORDIALS// + ////////////////////// + ////////////// + +//The idea for Primordials is that they are conjurable companions for arcyne types. They should cost essentia to conjure, and will follow the command minion order spell. +//Three differant types, air water and fire. Potential for unique effects/attacks for all three. Perhaps delineate between speed health and damage. +//Might also be worth looking into a spell to adjust their 'modes' from melee to ranged, or a command for special abilities. + +/mob/living/simple_animal/hostile/retaliate/primordial/Initialize(mapload, mob/user) + if(user) + if(user.mind && user.mind.current) + summoner = user.mind.current.real_name + else + summoner = user.name + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_TOXIMMUNE, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_BLOODLOSS_IMMUNE, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOFIRE, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_BASHDOORS, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOPAIN, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_CRITICAL_RESISTANCE, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_STRONGBITE, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOFALLDAMAGE1, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + + . = ..() + src.adjust_skill_level(/datum/attribute/skill/combat/unarmed, 30, TRUE) + AddComponent(/datum/component/ai_aggro_system) + if(user && user.mind && user.mind.current) + befriend(user) + + +/datum/intent/simple/claw/primordial + name = "claw" + icon_state = "instrike" + attack_verb = list("claws", "pecks") + animname = "blank22" + blade_class = BCLASS_CUT + hitsound = "smallslash" + chargetime = 0 + penfactor = 0 + candodge = TRUE + canparry = TRUE + miss_text = "slash the air" + item_damage_type = "slash" + clickcd = 12 + +/mob/living/simple_animal/hostile/retaliate/primordial + icon = 'icons/mob/primordial.dmi' + faction = list(FACTION_NEUTRAL) + var/next_ability_use + var/ability_cooldown = 30 SECONDS + var/next_heal_time = 0 + +/mob/living/simple_animal/hostile/retaliate/primordial/death() + ..() + spill_embedded_objects() + qdel(src) + +/mob/living/simple_animal/hostile/retaliate/primordial/proc/ability(turf/target_location, mob/living/user) + return + +/mob/living/simple_animal/hostile/retaliate/primordial/get_pilot_ability() + return /datum/action/cooldown/spell/primordial_special + +/datum/action/cooldown/spell/primordial_special + button_icon = 'icons/mob/actions/spells/mage_conjure.dmi' + button_icon_state = "primordial_mark" + name = "Elemental Surge" + desc = "Unleash your elemental vessel's innate power at a spot within reach - a flame primordial breathes a searing cone, a water primordial churns a whirlpool, an air primordial hurls a gale." + sound = null + + click_to_activate = TRUE + cast_range = 6 + self_cast_possible = FALSE + + charge_required = FALSE + spell_cost = 0 + + cooldown_time = 30 SECONDS + spell_tier = 3 + + spell_impact_intensity = SPELL_IMPACT_NONE + invocation_type = INVOCATION_NONE + spell_requirements = SPELL_REQUIRES_SAME_Z + +/datum/action/cooldown/spell/primordial_special/cast(atom/cast_on) + . = ..() + var/mob/living/simple_animal/hostile/retaliate/primordial/P = owner + if(!istype(P)) + return FALSE + var/turf/T = get_turf(cast_on) + if(!T) + return FALSE + if(world.time < P.next_ability_use) + P.balloon_alert(P, "not ready yet!") + return FALSE + P.ability(T, P) + return TRUE + +/mob/living/simple_animal/hostile/retaliate/primordial/fire + name = "flame primordial" + desc = "Billowing heat strikes your face and threatens to singe your eyebrows! \ + It may be wise not to touch it." + icon_state = "primordial_fire" + icon_living = "primordial_fire" + icon_dead = "" + mob_biotypes = MOB_ORGANIC|MOB_BEAST + emote_hear = null + emote_see = null + see_in_dark = 10 + move_to_delay = 3 + + base_intents = list(/datum/intent/simple/claw/primordial) + health = 525 + maxHealth = 525 + melee_damage_lower = 30 + melee_damage_upper = 40 + vision_range = 10 + aggro_vision_range = 9 + environment_smash = ENVIRONMENT_SMASH_NONE + retreat_distance = 0 + minimum_distance = 0 + ranged = 1 + ranged_cooldown_time = 4 SECONDS + projectiletype = /obj/projectile/magic/spitfire/primordial + projectilesound = 'sound/magic/whiteflame.ogg' + next_ability_use + base_constitution = 10 + base_strength = 10 + base_speed = 13 + footstep_type = FOOTSTEP_MOB_BAREFOOT + defprob = 30 + retreat_health = 0 + next_ability_use + ai_controller = /datum/ai_controller/flame_primordial + +/mob/living/simple_animal/hostile/retaliate/primordial/fire/ability(turf/target_location, mob/living/user) + if(!target_location) + return FALSE + visible_message(span_danger("[src] inhales, heat gathering about its form!")) + addtimer(CALLBACK(src, PROC_REF(do_fire_cone), target_location), 1 SECONDS) + return TRUE + +/mob/living/simple_animal/hostile/retaliate/primordial/fire/proc/do_fire_cone(turf/target_location) + if(QDELETED(src) || stat == DEAD || !target_location) + return + var/range = 3 + var/angle = 60 + + var/dx = target_location.x - src.x + var/dy = target_location.y - src.y + + var/dir_angle = ATAN2(dy, dx) + + visible_message(span_danger("[src] exhales a cone of searing fire!")) + + for(var/turf/T in view(range, src)) + var/tx = T.x - src.x + var/ty = T.y - src.y + var/mag = sqrt(tx*tx + ty*ty) + if(mag == 0) + continue + + tx /= mag + ty /= mag + + var/angle_to_turf = ATAN2(ty, tx) + var/delta = abs(dir_angle - angle_to_turf) + if(delta > 180) + delta = 360 - delta + + if(delta <= angle/2) + new /obj/effect/curtain_fire(T, 5 SECONDS) + +/mob/living/simple_animal/hostile/retaliate/primordial/water + name = "water primordial" + desc = "A torrential flood, magically animated and bound to service. It seems \ + to draw moisture from the ground it traverses." + icon_state = "primordial_water" + icon_living = "primordial_water" + icon_dead = "" + mob_biotypes = MOB_ORGANIC|MOB_BEAST + emote_hear = null + emote_see = null + speak_chance = 1 + see_in_dark = 10 + move_to_delay = 3 + + attack_sound = list('sound/misc/undertow.ogg') + + base_intents = list(/datum/intent/simple/claw/primordial) + + health = 650 + maxHealth = 650 + melee_damage_lower = 30 + melee_damage_upper = 35 + vision_range = 10 + aggro_vision_range = 9 + environment_smash = ENVIRONMENT_SMASH_NONE + retreat_distance = 0 + minimum_distance = 0 + ranged = 1 + ranged_cooldown_time = 4 SECONDS + projectiletype = /obj/projectile/magic/frost_shard/primordial + projectilesound = 'sound/spellbooks/icicle.ogg' + + base_constitution = 10 + base_strength = 10 + base_speed = 8 + footstep_type = FOOTSTEP_MOB_BAREFOOT + defprob = 20 + retreat_health = 0 + + ai_controller = /datum/ai_controller/water_primordial + +/mob/living/simple_animal/hostile/retaliate/primordial/water/ability(turf/target_location, mob/living/user) + if(!target_location) + return FALSE + visible_message(span_danger("[src] gathers the waters into a churning knot!")) + addtimer(CALLBACK(src, PROC_REF(do_whirlpool), target_location), 1 SECONDS) + return TRUE + +/mob/living/simple_animal/hostile/retaliate/primordial/water/proc/do_whirlpool(turf/target_location) + if(QDELETED(src) || stat == DEAD || !target_location) + return + visible_message(span_danger("[src] unleashes a spiralling wave of floodwaters!")) + new /obj/effect/primordial_pool(target_location) + +/obj/effect/primordial_pool + name = "floodwave" + desc = "A swirling wavepool churns violently." + icon_state = "blueshatter2" + anchored = TRUE + density = FALSE + var/list/turf_data = list() + var/duration = 15 SECONDS + +/obj/effect/primordial_pool/Initialize(mapload, turf/center) + . = ..() + if(!center) + center = get_turf(src) + // Build a 3x3 block around the center + var/list/affected = block( + locate(center.x - 1, center.y - 1, center.z), + locate(center.x + 1, center.y + 1, center.z) + ) + // Save original turfs + for(var/turf/T in affected) + turf_data[T] = T.type + // Apply whirlpool layout + for(var/turf/T in affected) + var/dx = T.x - center.x + var/dy = T.y - center.y + if(dx == 0 && dy == 0) + T.ChangeTurf(/turf/open/water/ocean, flags = CHANGETURF_IGNORE_AIR) //deep center + else if(dx == 0) // vertical (north/south) + if(dy > 0) + T.ChangeTurf(/turf/open/water/river/flow, flags = CHANGETURF_IGNORE_AIR) + else + T.ChangeTurf(/turf/open/water/river/flow/north, flags = CHANGETURF_IGNORE_AIR) + else if(dy == 0) // horizontal (east/west) + if(dx > 0) + T.ChangeTurf(/turf/open/water/river/flow/west, flags = CHANGETURF_IGNORE_AIR) + else + T.ChangeTurf(/turf/open/water/river/flow/east, flags = CHANGETURF_IGNORE_AIR) + else + if(dx < 0 && dy < 0) // SW corner + T.ChangeTurf(/turf/open/water/river/flow/east, flags = CHANGETURF_IGNORE_AIR) + else if(dx > 0 && dy < 0) // SE corner + T.ChangeTurf(/turf/open/water/river/flow/north, flags = CHANGETURF_IGNORE_AIR) + else if(dx > 0 && dy > 0) // NE corner + T.ChangeTurf(/turf/open/water/river/flow/west, flags = CHANGETURF_IGNORE_AIR) + else if(dx < 0 && dy > 0) // NW corner + T.ChangeTurf(/turf/open/water/river/flow, flags = CHANGETURF_IGNORE_AIR) + // Auto-remove after duration + spawn(duration) + qdel(src) + +/obj/effect/primordial_pool/Destroy() + // Restore saved turfs + for(var/turf/T in turf_data) + if(T) + T.ChangeTurf(turf_data[T], flags = CHANGETURF_IGNORE_AIR) + turf_data.Cut() + return ..() + +/mob/living/simple_animal/hostile/retaliate/primordial/air + name = "air primordial" + desc = "Storm-winds whip at the air wherever this creature travels! \ + It is scarcely even easy to keep one's footing while close." + icon_state = "primordial_air" + icon_living = "primordial_air" + icon_dead = "" + mob_biotypes = MOB_ORGANIC|MOB_BEAST + emote_hear = null + emote_see = null + speak_chance = 1 + see_in_dark = 10 + move_to_delay = 3 + + attack_sound = list('sound/combat/wooshes/bladed/wooshmed (1).ogg','sound/combat/wooshes/bladed/wooshmed (2).ogg','sound/combat/wooshes/bladed/wooshmed (3).ogg') + + base_intents = list(/datum/intent/simple/claw/primordial) + + health = 450 + maxHealth = 450 + melee_damage_lower = 35 + melee_damage_upper = 45 + vision_range = 10 + aggro_vision_range = 9 + environment_smash = ENVIRONMENT_SMASH_NONE + retreat_distance = 0 + minimum_distance = 0 + ranged = 1 + ranged_cooldown_time = 4 SECONDS + projectiletype = /obj/projectile/magic/greater_arcyne_bolt/primordial + projectilesound = 'sound/magic/vlightning.ogg' + + + base_constitution = 10 + base_strength = 10 + base_speed = 13 + footstep_type = FOOTSTEP_MOB_BAREFOOT + defprob = 40 + retreat_health = 0 + + ai_controller = /datum/ai_controller/air_primordial + +/mob/living/simple_animal/hostile/retaliate/primordial/air/ability(turf/target_location, mob/living/user) + if(!target_location) + return FALSE + visible_message(span_danger("[src] draws a whirl of stormwinds about itself!")) + addtimer(CALLBACK(src, PROC_REF(do_gust), target_location), 1 SECONDS) + return TRUE + +/mob/living/simple_animal/hostile/retaliate/primordial/air/proc/do_gust(turf/target_location) + if(QDELETED(src) || stat == DEAD || !target_location) + return + var/dir_to_target = get_dir(src, target_location) + + // Starting turf = just in front of the primordial + var/turf/current = get_step(get_turf(src), dir_to_target) + + // Collect the 3-length line outward from source + var/list/wave_rows = list() + for(var/i = 1, i <= 3, i++) + if(!current) + break + var/list/row = list() + row += current + row += get_step(current, turn(dir_to_target, 90)) + row += get_step(current, turn(dir_to_target, -90)) + + wave_rows += list(row) // push row as sublist + current = get_step(current, dir_to_target) + + // Now release rows one after another + var/delay = 3 // deciseconds = 0.3s between rows + for(var/row_index = 1, row_index <= wave_rows.len, row_index++) + var/list/row = wave_rows[row_index] + spawn(delay * (row_index - 1)) + for(var/turf/T in row) + if(!T) + continue + for(var/mob/living/L in T) + if(L == src) + continue + knockback(L, dir_to_target, 8) + new /obj/effect/temp_visual/gust(T, dir_to_target) + visible_message(span_danger("[src] exhales a violent gust of wind!")) + playsound(src, 'sound/weather/rain/wind_6.ogg', 100, TRUE) + + +/mob/living/simple_animal/hostile/retaliate/primordial/air/proc/knockback(mob/living/L, dir, distance) + if(!L || !isturf(L.loc)) + return + var/turf/target_turf = get_ranged_target_turf(L, dir, distance) + if(!target_turf) + return + L.throw_at(target_turf, 7, 4) + +/obj/effect/temp_visual/gust + icon = 'icons/effects/effects.dmi' + icon_state = "kick" + layer = ABOVE_MOB_LAYER + anchored = TRUE + duration = 8 + +/obj/projectile/magic/spitfire/primordial + name = "primordial flame" + damage = 20 + arcshot = TRUE + +/obj/projectile/magic/frost_shard/primordial + name = "primordial frost shard" + damage = 18 + arcshot = TRUE + +/obj/projectile/magic/greater_arcyne_bolt/primordial + name = "primordial gale" + damage = 27 + arcshot = TRUE diff --git a/code/modules/mob/living/simple_animal/hostile/vengeful_spirit.dm b/code/modules/mob/living/simple_animal/hostile/vengeful_spirit.dm new file mode 100644 index 00000000000..67089b4cbe4 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/vengeful_spirit.dm @@ -0,0 +1,52 @@ +/mob/living/simple_animal/hostile/spirit_vengeance + name = "rancor spirit" + desc = "Death is no end, but a fascinating new beginning." + icon = 'icons/mob/spirit_vengeance.dmi' + icon_state = "spirit_vengeance" + icon_living = "spirit_vengeance" + gender = MALE + mob_biotypes = MOB_HUMANOID + robust_searching = 1 + move_to_delay = 3 + base_constitution = 9 + base_strength = 9 + base_speed = 14 + maxHealth = 50 + health = 50 + harm_intent_damage = 5 + melee_damage_lower = 5 + melee_damage_upper = 7 + vision_range = 7 + aggro_vision_range = 9 + retreat_distance = 0 + minimum_distance = 0 + limb_destroyer = 1 + base_intents = list(/datum/intent/simple/bite) + attack_verb_continuous = "hacks" + attack_verb_simple = "hack" + attack_sound = 'sound/blank.ogg' + canparry = TRUE + d_intent = INTENT_DODGE + defprob = 20 + speak_emote = list("growls") + footstep_type = null + del_on_death = TRUE + ai_controller = /datum/ai_controller/spirit_vengeance + +/mob/living/simple_animal/hostile/spirit_vengeance/get_sound(input) + switch(input) + if("laugh") + return pick('sound/vo/mobs/ghost/laugh (1).ogg','sound/vo/mobs/ghost/laugh (2).ogg','sound/vo/mobs/ghost/laugh (3).ogg','sound/vo/mobs/ghost/laugh (4).ogg','sound/vo/mobs/ghost/laugh (5).ogg','sound/vo/mobs/ghost/laugh (6).ogg') + if("moan") + return pick('sound/vo/mobs/ghost/moan (1).ogg','sound/vo/mobs/ghost/laugh (2).ogg','sound/vo/mobs/ghost/laugh (3).ogg') + if("death") + return 'sound/vo/mobs/ghost/death.ogg' + if("whisper") + return pick('sound/vo/mobs/ghost/whisper (1).ogg','sound/vo/mobs/ghost/whisper (2).ogg','sound/vo/mobs/ghost/whisper (3).ogg') + if("aggro") + return pick('sound/vo/mobs/ghost/aggro (1).ogg','sound/vo/mobs/ghost/aggro (2).ogg','sound/vo/mobs/ghost/aggro (3).ogg','sound/vo/mobs/ghost/aggro (4).ogg','sound/vo/mobs/ghost/aggro (5).ogg','sound/vo/mobs/ghost/aggro (6).ogg') + +/mob/living/simple_animal/hostile/spirit_vengeance/Initialize(mapload) + . = ..() + AddComponent(/datum/component/ai_aggro_system) + addtimer(CALLBACK(src, TYPE_PROC_REF(/mob/living/simple_animal, death), TRUE), 30 SECONDS) diff --git a/code/modules/mob/living/stats.dm b/code/modules/mob/living/stats.dm index f13fab83c0b..3b63383477f 100644 --- a/code/modules/mob/living/stats.dm +++ b/code/modules/mob/living/stats.dm @@ -45,7 +45,6 @@ return FALSE if(patron && !ispath(patron)) patron.on_remove(src) - mana_pool?.remove_attunements(patron) var/mob/living/carbon/human/devout var/stored_cleric_class @@ -57,7 +56,6 @@ patron = new_patron patron.on_gain(src) - mana_pool?.set_attunements(patron) if(devout && stored_cleric_class) var/holder = devout.patron?.devotion_holder if(holder) @@ -206,7 +204,7 @@ * Adjusts an existing named stat modifier, adding delta values on top. * If no modifier for this source exists yet, creates one. * - * This way of stats adjustment should be somewhat temporary (effects, clothing, etc), + * This way of stats adjustment should be somewhat temporary (effects, clothing, etc), * and it has much stronger impact on skills. * * Arguments: diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index 258b81f111c..31340a58519 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -1003,3 +1003,29 @@ animate(my_client, pixel_x = oldx + mpx, pixel_y = oldy + mpy, time = duration, flags = ANIMATION_RELATIVE) animate(pixel_x = oldx, pixel_y = oldy, time = backtime_duration, easing = BACK_EASING) + +///Return any anti magic atom on this mob that matches the magic type +/mob/proc/anti_magic_check(magic = TRUE, holy = FALSE, tinfoil = FALSE, chargecost = 1, self = FALSE) + if(!magic && !holy && !tinfoil) + return + var/list/protection_sources = list() + if(SEND_SIGNAL(src, COMSIG_MOB_RECEIVE_MAGIC, src, magic, holy, tinfoil, chargecost, self, protection_sources) & COMPONENT_MAGIC_BLOCKED) + if(protection_sources.len) + return pick(protection_sources) + else + return src + if((magic && HAS_TRAIT(src, TRAIT_ANTIMAGIC)) || (holy && HAS_TRAIT(src, TRAIT_HOLY))) + return src + +/mob/living/anti_magic_check(magic = TRUE, holy = FALSE, tinfoil = FALSE, chargecost = 1, self = FALSE) + . = ..() + if(.) + return + if((magic && HAS_TRAIT(src, TRAIT_ANTIMAGIC)) || (holy && HAS_TRAIT(src, TRAIT_HOLY))) + return src + +/mob/living/proc/is_swinging(disrupt_only = FALSE) + if(!disrupt_only) + return (has_status_effect(/datum/status_effect/swingdelay) || has_status_effect(/datum/status_effect/swingdelay/disrupt)) + else + return (has_status_effect(/datum/status_effect/swingdelay/disrupt)) diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index 69ddd3f885e..f003a07f77b 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -151,6 +151,14 @@ ///projectile crit reduce chance since more dmg increases the crit chance it can get absurdly high, 0 for nothing. var/reduce_crit_chance = 0 + /// Min tile distance for full damage/AP. + var/min_range = 0 + /// Max tile distance for full damage/AP. + var/max_range = 0 + /// Falloff factor for damage. Multiplicative. + var/dam_falloff_factor = 1 + var/suppress_effects_past_range = FALSE + /obj/projectile/proc/handle_drop() return @@ -216,6 +224,22 @@ /obj/projectile/proc/on_range() //if we want there to be effects when they reach the end of their range qdel(src) +/obj/projectile/proc/out_of_effective_range() + return suppress_effects_past_range && max_range && check_range(get_turf(src)) + +/obj/projectile/proc/check_range(turf/T) + if(!starting) + return FALSE + if(!istype(T)) + T = get_turf(src) + if(!istype(T)) + return FALSE + if(T.z != starting.z) + return FALSE + var/distance = get_dist(T, starting) + if((min_range && distance < min_range) || (max_range && distance > max_range)) + return TRUE + //to get the correct limb (if any) for the projectile hit message /mob/living/proc/check_limb_hit(hit_zone) if(has_limbs) @@ -384,6 +408,9 @@ // 2. LAZYSET(impacted, target, TRUE) //hash lookup > in for performance in hit-checking // 3. + if(check_range(T)) + if(damage) + damage = round(damage * dam_falloff_factor) var/mode = prehit_pierce(target) if(mode == PROJECTILE_DELETE_WITHOUT_HITTING) qdel(src) diff --git a/code/modules/projectiles/projectile/magic.dm b/code/modules/projectiles/projectile/magic.dm index 3093a89f770..2482bfc7269 100644 --- a/code/modules/projectiles/projectile/magic.dm +++ b/code/modules/projectiles/projectile/magic.dm @@ -7,10 +7,19 @@ armor_penetration = 100 pass_flags = PASSTABLE | PASSGRILLE flag = "magic" + + dam_falloff_factor = 0.5 + suppress_effects_past_range = TRUE + max_range = 7 + /// determines what type of antimagic can block the spell projectile var/antimagic_flags = MAGIC_RESISTANCE /// determines the drain cost on the antimagic item var/antimagic_charge_cost = 1 + /// Impact visual intensity. SPELL_IMPACT_NONE / SPELL_IMPACT_LOW / SPELL_IMPACT_MEDIUM / SPELL_IMPACT_HIGH + var/spell_impact_intensity = SPELL_IMPACT_LOW + /// Override color for the impact effect. If null, uses light_color. + var/spell_impact_color /obj/projectile/magic/prehit_pierce(mob/living/target) . = ..() @@ -18,6 +27,13 @@ visible_message(span_warning("[src] fizzles on contact with [target]!")) return PROJECTILE_DELETE_WITHOUT_HITTING + +/obj/projectile/magic/on_hit(atom/target, blocked = FALSE) + . = ..() + if(spell_impact_intensity > SPELL_IMPACT_NONE) + var/impact_color = spell_impact_color || light_color || "#FFFFFF" + new /obj/effect/temp_visual/spell_impact(get_turf(target), impact_color, spell_impact_intensity) + /obj/projectile/magic/death name = "bolt of death" icon_state = "pulse1_bl" diff --git a/code/modules/questing/quests/courier.dm b/code/modules/questing/quests/courier.dm index 51d81a4ffe9..8a093410bba 100644 --- a/code/modules/questing/quests/courier.dm +++ b/code/modules/questing/quests/courier.dm @@ -67,7 +67,7 @@ /obj/item/perfume/random, ), /area/indoors/town/keep/magician = list( - /obj/item/book/granter/spellbook, + /obj/item/spellbook, /obj/item/gem/yellow, /obj/item/reagent_containers/glass/bottle/manapot, ), diff --git a/code/modules/spells/_casting/casting.dm b/code/modules/spells/_casting/casting.dm index f63ce956589..db7d381ca47 100644 --- a/code/modules/spells/_casting/casting.dm +++ b/code/modules/spells/_casting/casting.dm @@ -17,35 +17,29 @@ return usable_pools /// Check if all pools have the required amount of mana -/mob/living/proc/has_mana_available(list/attunements, total_required_mana) +/mob/living/proc/has_mana_available(total_required_mana) var/list/datum/mana_pool/provided_mana = get_all_pools() var/total_effective_mana = 0 for(var/datum/mana_pool/iterated_pool as anything in provided_mana) - total_effective_mana += iterated_pool.get_attuned_amount(attunements, src) + total_effective_mana += iterated_pool.amount return (total_effective_mana > total_required_mana) /// Consume the given amount of mana -/mob/living/proc/consume_mana(list/attunements, amount_to_use) +/mob/living/proc/consume_mana(amount_to_use) var/mana_consumed = -amount_to_use - var/total_mana_consumed = -mana_consumed var/list/datum/mana_pool/available_pools = get_all_pools() - var/attunement_total_value = 0 var/total_damage = 0 - for(var/datum/attunement/attunement as anything in attunements) - attunement_total_value += attunements[attunement] while(mana_consumed <= -0.05) - var/mult var/attuned_cost for(var/datum/mana_pool/pool as anything in available_pools) - mult = pool.get_overall_attunement_mults(attunements, src) - attuned_cost = mana_consumed * mult + attuned_cost = mana_consumed if(pool.amount < attuned_cost) attuned_cost = pool.amount - var/mana_adjusted = SAFE_DIVIDE(pool.adjust_mana((attuned_cost)), mult) * (has_world_trait(/datum/world_trait/noc_wisdom) ? 0.8 : 1) + var/mana_adjusted = pool.adjust_mana(attuned_cost) * (has_world_trait(/datum/world_trait/noc_wisdom) ? 0.8 : 1) mana_consumed -= mana_adjusted record_featured_stat(FEATURED_STATS_MAGES, src, abs(mana_adjusted)) record_round_statistic(STATS_MANA_SPENT, abs(mana_adjusted)) @@ -53,12 +47,6 @@ if(available_pools.Find(pool) == length(available_pools) && mana_consumed <= -0.05) // if we're at the end of the list and mana_consumed is not 0 or near 0 (floating points grrr) mana_consumed = 0 // lets terminate the loop to be safe - if(pool.parent == src) - for(var/datum/attunement/attunement as anything in attunements) - if(pool.negative_attunements[attunement] < 0) - var/composition_gain = attunement_total_value / attunements[attunement] - var/negative_impact_mana = total_mana_consumed * composition_gain - total_damage += round(negative_impact_mana * 0.1, 1) if(total_damage) mana_pool.mana_backlash(total_damage) @@ -68,12 +56,10 @@ /// Start effects, used for channeled spells, /// Creates and sets the spell_rune. -/mob/living/proc/start_spell_visual_effects(list/attunements) +/mob/living/proc/start_spell_visual_effects(spell_color) if(QDELETED(src)) return - var/spell_color = get_blended_attunement_color(attunements) - if(spell_rune) QDEL_NULL(spell_rune) @@ -99,12 +85,10 @@ /// Finish effect, used by most spells. /// Cleans up the spell_rune if present and creates a cloud visual based on attunement. -/mob/living/proc/finish_spell_visual_effects(list/attunements) +/mob/living/proc/finish_spell_visual_effects(spell_color) if(QDELETED(src)) return - var/spell_color = get_blended_attunement_color(attunements) - // Clean up the rune if(spell_rune) QDEL_NULL(spell_rune) diff --git a/code/modules/spells/_helpers.dm b/code/modules/spells/_helpers.dm new file mode 100644 index 00000000000..ff261d1ede0 --- /dev/null +++ b/code/modules/spells/_helpers.dm @@ -0,0 +1,212 @@ +/* Shared helper procs for Spellblade pseudo-melee abilities. +These mirror the species.dm melee attack flow (armor check -> apply_damage -> bodypart_attacked_by) +without going through the click pipeline, so spells can deliver weapon-style strikes. */ + +/proc/arcyne_strike(mob/living/carbon/human/user, mob/living/target, obj/item/weapon, damage, def_zone, blade_class_override, armor_penetration = 0, spell_name = "Arcyne Strike", skip_animation = FALSE, skip_message = FALSE, allow_shield_check = FALSE, damage_type = BRUTE, npc_simple_damage_mult = 1, intdamage_factor, exact_zone = FALSE, flat_integ = TRUE) + if(!user || !target || QDELETED(user) || QDELETED(target)) + return FALSE + + var/blade_class = BCLASS_CUT + var/attack_flag = "slash" + if(blade_class_override) + blade_class = blade_class_override + else + var/datum/intent/current_intent = user.a_intent + if(current_intent) + blade_class = current_intent.blade_class + + switch(blade_class) + if(BCLASS_BLUNT, BCLASS_SMASH) + blade_class = BCLASS_BLUNT + attack_flag = "blunt" + armor_penetration = 0 // Blunt uses DR, not penetration + if(BCLASS_STAB, BCLASS_PICK) + blade_class = BCLASS_STAB + attack_flag = "stab" + if(BCLASS_PIERCE) + blade_class = BCLASS_PIERCE + attack_flag = "piercing" + if(BCLASS_BURN) + attack_flag = "fire" + else + blade_class = BCLASS_CUT + attack_flag = "slash" + + if(!def_zone) + def_zone = user.zone_selected || BODY_ZONE_CHEST + + // Zone accuracy uses the same system as ranged — precise zones are capped. + // Base accuracy from PER/INT: 60 base + 10 per point of PER above 10 + 10 per point of INT above 10 + // Below 10 penalizes instead. A class-intended spellblade (PER ~12, INT ~12) gets ~100 base accuracy. + // This feeds into bullet_hit_accuracy_check which caps ultra-precise at 50%, precise at 75%, face at 30%. + // exact_zone bypasses the roll entirely, striking precisely where the caster aimed. + if(!exact_zone && def_zone != BODY_ZONE_CHEST && isliving(target)) + if(prob(40)) + def_zone = accuracy_check(def_zone, user, target, weapon?.associated_skill) + + if(iscarbon(target)) + var/mob/living/carbon/C = target + var/obj/item/bodypart/targeting = C.get_bodypart(check_zone(def_zone)) + if(!targeting) + def_zone = BODY_ZONE_CHEST + + var/visual_effect = ATTACK_EFFECT_SLASH + var/anim_type = ATTACK_ANIMATION_SWIPE + switch(blade_class) + if(BCLASS_BLUNT) + visual_effect = ATTACK_EFFECT_SMASH + anim_type = ATTACK_ANIMATION_BONK + if(BCLASS_STAB) + anim_type = ATTACK_ANIMATION_THRUST + if(BCLASS_BURN) + visual_effect = ATTACK_EFFECT_MECHFIRE + if(!skip_animation) + user.do_attack_animation(target, visual_effect, weapon, item_animation_override = anim_type) + + // Optional shield check — blocked like a projectile (shield takes 25% as integrity damage). + if(allow_shield_check && ishuman(target)) + var/mob/living/carbon/human/H = target + if(user != H && H.check_shields(weapon, damage, spell_name, MELEE_ATTACK, armor_penetration)) + // Shield eats the hit but takes integrity damage, matching projectile behavior + for(var/obj/item/I in H.held_items) + if(I.block_chance > 0) + I.take_damage(floor(damage / 4)) + break + return 0 + + // NPC damage multiplier (e.g. fireball's npc_simple_damage_mult) + if(npc_simple_damage_mult != 1 && istype(target, /mob/living/simple_animal)) + damage = round(damage * npc_simple_damage_mult) + + var/armor_block = target.run_armor_check(def_zone, attack_flag, blade_dulling = blade_class, armor_penetration = armor_penetration, damage = damage) + var/damage_dealt = target.apply_damage(damage, damage_type, def_zone, armor_block) + SEND_SIGNAL(target, COMSIG_ATOM_WAS_ATTACKED, user, damage) + + // Match standard melee flow: only apply wounds if damage actually got through armor + if(damage_dealt) + var/wound_damage = max(0, damage - armor_block) + if(wound_damage > 0) + if(iscarbon(target)) + var/mob/living/carbon/C = target + var/obj/item/bodypart/affecting = C.get_bodypart(check_zone(def_zone)) + if(affecting) + affecting.bodypart_attacked_by(blade_class, wound_damage, user, def_zone, crit_message = TRUE) + var/dismember_chance = affecting.get_spell_dismemberment_chance(damage, blade_class, def_zone) + if(dismember_chance && prob(dismember_chance)) + affecting.dismember(damage_type, blade_class, user, def_zone) + else + target.simple_woundcritroll(blade_class, wound_damage, user, def_zone, crit_message = TRUE) + + var/attack_verb = "strikes" + var/hit_sound + switch(blade_class) + if(BCLASS_CUT) + attack_verb = "slashes" + hit_sound = pick('sound/combat/hits/bladed/largeslash (1).ogg', 'sound/combat/hits/bladed/largeslash (2).ogg', 'sound/combat/hits/bladed/largeslash (3).ogg') + if(BCLASS_BLUNT) + attack_verb = "smashes" + hit_sound = pick('sound/combat/hits/blunt/genblunt (1).ogg', 'sound/combat/hits/blunt/genblunt (2).ogg', 'sound/combat/hits/blunt/genblunt (3).ogg') + if(BCLASS_STAB) + attack_verb = "stabs" + hit_sound = pick('sound/combat/hits/bladed/genthrust (1).ogg', 'sound/combat/hits/bladed/genthrust (2).ogg') + if(BCLASS_BURN) + attack_verb = "scorches" + hit_sound = 'sound/items/firelight.ogg' + + playsound(get_turf(target), hit_sound, 100, TRUE) + if(!skip_message) + var/weapon_name = weapon ? weapon.name : lowertext(spell_name) + var/armor_msg = "" + if(!damage_dealt) + armor_msg += span_warning("Armor stops the damage.") + target.visible_message( + span_danger("[user] [attack_verb] \the [target] with [weapon_name] in the [parse_zone(def_zone)]![armor_msg]"), + span_danger("[user] [attack_verb] me in the [span_userdanger(parse_zone(def_zone))]![armor_msg]"), + null, COMBAT_MESSAGE_RANGE) + + log_combat(user, target, "spell-struck ([spell_name])") + return max(0, damage - armor_block) + +/proc/arcyne_get_weapon(mob/living/carbon/human/H) + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(!M?.bound_weapon) + return null + if(H.is_holding(M.bound_weapon)) + return M.bound_weapon + return null + +/* Shared blink/teleport validation used by Blink, Caedo, and any future teleport spell. +Returns null on success, or an error string describing the failure. */ +/proc/arcyne_validate_blink_dest(turf/dest, mob/user) + if(!dest) + return "Invalid target location!" + var/turf/start = get_turf(user) + if(dest.z != start.z) + return "I can only teleport on the same plane!" + if(istransparentturf(dest)) + return "I cannot teleport to the open air!" + if(dest.density) + return "I cannot teleport into a wall!" + for(var/obj/structure/window/W in dest) + if(W.density) + return "I cannot teleport through a window!" + for(var/obj/structure/door/door in dest) + if(door.density) + return "I cannot teleport through a door!" + for(var/obj/structure/bars/B in dest) + if(B.density) + return "I cannot teleport through bars!" + for(var/obj/structure/gate/G in dest) + if(G.density) + return "I cannot teleport through a gate!" + return null + +/* Validates the path between start and dest for obstacles. +Excludes dest turf from wall checks (you're landing there, not passing through). +Returns null on success, or an error string. */ +/proc/arcyne_validate_blink_path(turf/start, turf/dest) + var/list/turf_list = getline(start, dest) + if(length(turf_list) > 0) + turf_list.len-- + for(var/turf/T in turf_list) + if(T == start) + continue + if(T.density) + return "I cannot teleport through walls!" + for(var/obj/structure/door/door in T.contents) + if(door.density) + return "I cannot teleport through doors!" + for(var/obj/structure/window/window in T.contents) + if(window.density && !window.climbable) + return "I cannot teleport through windows!" + for(var/obj/structure/bars/B in T.contents) + if(B.density) + return "I cannot teleport through bars!" + for(var/obj/structure/gate/G in T.contents) + if(G.density) + return "I cannot teleport through gates!" + return null + +/* Walks toward target up to max_range tiles, returning the farthest valid turf. +Used by Caedo to clamp distance instead of failing when out of range. */ +/proc/arcyne_find_max_blink_dest(mob/user, turf/target, max_range) + var/turf/start = get_turf(user) + if(!start || !target) + return null + var/list/full_line = getline(start, target) + var/turf/best = null + var/steps = 0 + for(var/turf/T in full_line) + if(T == start) + continue + steps++ + if(steps > max_range) + break + var/err = arcyne_validate_blink_dest(T, user) + if(err) + break + var/path_err = arcyne_validate_blink_path(start, T) + if(path_err) + break + best = T + return best diff --git a/code/modules/spells/_spell_tree/_base_spellnode.dm b/code/modules/spells/_spell_tree/_base_spellnode.dm deleted file mode 100644 index e125630af56..00000000000 --- a/code/modules/spells/_spell_tree/_base_spellnode.dm +++ /dev/null @@ -1,27 +0,0 @@ -/datum/spell_node - var/name = "Unknown Spell" - var/desc = "A mysterious magical technique." - var/icon = 'icons/mob/actions/roguespells.dmi' - var/icon_state = "rune1" - var/node_x = 0 // Position on the research tree - var/node_y = 0 - /// List of spell node types required - var/list/prerequisites = list() - /// Description of what this unlocks - var/list/unlocks = list() - /// Spell points required - var/cost = 1 - /// Whether this is a passive ability instead of a spell - var/is_passive = FALSE - /// What spell this grants (null for passives) - var/datum/action/cooldown/spell/spell_type = null - -/datum/spell_node/New() - . = ..() - if(spell_type) - cost = spell_type.point_cost - icon = spell_type.button_icon - icon_state = spell_type.button_icon_state - -/datum/spell_node/proc/on_node_buy(mob/user) - return diff --git a/code/modules/spells/_spell_tree/circular_tree.dm b/code/modules/spells/_spell_tree/circular_tree.dm deleted file mode 100644 index e6a73a882d6..00000000000 --- a/code/modules/spells/_spell_tree/circular_tree.dm +++ /dev/null @@ -1,618 +0,0 @@ -/datum/spell_node/arcyne_eye - name = "Arcyne Eye" - desc = "Imbue yourself with the ability to perceive magical phenomena." - node_x = 0 - node_y = 0 - spell_type = /datum/action/cooldown/spell/undirected/arcyne_eye - -/datum/spell_node/illusionist - name = "Illusionist" - desc = "Bend reality with deception and trickery." - cost = 8 - node_x = 50 - node_y = -50 - prerequisites = list(/datum/spell_node/air_affinity, /datum/spell_node/death_affinity) - is_passive = TRUE - -/datum/spell_node/illusionist/on_node_buy(mob/user) - user.mana_pool?.adjust_attunement(/datum/attunement/illusion, 0.3) - user.mana_pool?.adjust_attunement(/datum/attunement/dark, 0.1) - user.mana_pool?.adjust_attunement(/datum/attunement/light, 0.1) - to_chat(user, span_notice("Reality bends to your whims.")) - -/datum/spell_node/nondetection - name = "Nondetection" - desc = "Hide a target from divination magic." - node_x = 90 - node_y = -110 - prerequisites = list(/datum/spell_node/illusionist) - spell_type = /datum/action/cooldown/spell/undirected/touch/non_detection - -/datum/spell_node/forcewall_weak - name = "Weak Force Wall" - desc = "Create a weak barrier of magical force." - node_x = 110 - node_y = -90 - prerequisites = list(/datum/spell_node/illusionist) - spell_type = /datum/action/cooldown/spell/undirected/forcewall/breakable - -/datum/spell_node/mana_well - name = "Mana Well" - desc = "Dig deeper into your magical reserves and recovery." - node_x = -290 - node_y = 290 - cost = 3 - is_passive = TRUE - -/datum/spell_node/mana_well/on_node_buy(mob/user) - var/current_max = user.mana_pool?.maximum_mana_capacity || 100 - user.mana_pool?.set_max_mana(current_max + 25, TRUE, TRUE) - user.mana_overload_threshold += 25 - user.mana_pool?.set_natural_recharge(user.mana_pool.ethereal_recharge_rate + 0.1) - to_chat(user, span_notice("Your magical well deepens and your mind achieves clarity.")) - -/datum/spell_node/meditation - name = "Meditation" - desc = "Improve your natural mana recovery." - cost = 4 - node_x = -290 - node_y = 390 - prerequisites = list(/datum/spell_node/mana_well) - is_passive = TRUE - -/datum/spell_node/meditation/on_node_buy(mob/user) - user.mana_pool?.set_natural_recharge(user.mana_pool.ethereal_recharge_rate + 0.15) - to_chat(user, span_notice("Your mind achieves even greater focus and clarity.")) - -/datum/spell_node/expanded_reserves - name = "Expanded Reserves" - desc = "Increase your magical capacity." - cost = 4 - node_x = -390 - node_y = 290 - prerequisites = list(/datum/spell_node/mana_well) - is_passive = TRUE - -/datum/spell_node/expanded_reserves/on_node_buy(mob/user) - var/current_max = user.mana_pool?.maximum_mana_capacity || 100 - user.mana_pool?.set_max_mana(current_max + 125, TRUE, TRUE) - user.mana_overload_threshold += 125 - to_chat(user, span_notice("Your magical reserves expand deeper.")) - -/datum/spell_node/eternal_wellspring - name = "Eternal Wellspring" - desc = "Immensely empower your mana capacity and your rate of mana recovery." - cost = 6 - node_x = -440 - node_y = 440 - prerequisites = list(/datum/spell_node/expanded_reserves, /datum/spell_node/meditation) - is_passive = TRUE - -/datum/spell_node/eternal_wellspring/on_node_buy(mob/user) - var/current_max = user.mana_pool?.maximum_mana_capacity || 100 - user.mana_pool?.set_max_mana(current_max + 200, TRUE, TRUE) - user.mana_pool?.set_natural_recharge(user.mana_pool.ethereal_recharge_rate + 1.0) - user.mana_overload_threshold += 200 - to_chat(user, span_notice("You become one with the eternal flow of magic.")) - - -/datum/spell_node/find_familiar - name = "Find Familiar" - desc = "Summon an animal to serve as your familiar." - node_x = -110 - node_y = -110 - prerequisites = list(/datum/spell_node/primal_savagery) - spell_type = /datum/action/cooldown/spell/conjure/familiar - -/datum/spell_node/primal_savagery - name = "Primal Savagery" - desc = "Channel primal magic to grow poisonous fangs." - node_x = -50 - node_y = -50 - prerequisites = list(/datum/spell_node/earth_affinity, /datum/spell_node/arcyne_affinity) - spell_type = /datum/action/cooldown/spell/status/primal_savagery - - -/datum/spell_node/blood_pact - name = "Blood Pact" - desc = "Bind your life force to blood magic." - cost = 5 - node_x = -50 - node_y = 50 - prerequisites = list(/datum/spell_node/dark_affinity, /datum/spell_node/electric_affinity) - is_passive = TRUE - -/datum/spell_node/blood_pact/on_node_buy(mob/user) - user.mana_pool?.adjust_attunement(/datum/attunement/dark, 0.12) - user.mana_pool?.adjust_attunement(/datum/attunement/blood, 0.12) - to_chat(user, span_notice("Dark power flows through your lifeblood.")) - -/datum/spell_node/blood_lightning - name = "Blood Lightning" - desc = "Channel crimson lightning through blood." - node_x = -110 - node_y = 110 - prerequisites = list(/datum/spell_node/blood_pact) - spell_type = /datum/action/cooldown/spell/projectile/blood_bolt - -/datum/spell_node/elemental_harmony - name = "Elemental Harmony" - desc = "Balance fire and ice within yourself." - cost = 5 - node_x = 50 - node_y = 50 - prerequisites = list(/datum/spell_node/fire_affinity, /datum/spell_node/frost_affinity) - is_passive = TRUE - -/datum/spell_node/elemental_harmony/on_node_buy(mob/user) - user.mana_pool?.adjust_attunement(/datum/attunement/fire, 0.1) - user.mana_pool?.adjust_attunement(/datum/attunement/ice, 0.1) - to_chat(user, span_notice("Fire and ice flow in harmony within you.")) - -/datum/spell_node/create_bonfire - name = "Create Bonfire" - desc = "Create a bonfire on the ground." - node_x = 100 - node_y = 0 - prerequisites = list(/datum/spell_node/arcyne_eye) - spell_type = /datum/action/cooldown/spell/conjure/bonfire - -/datum/spell_node/fire_affinity - name = "Fire Affinity" - desc = "Deepen your connection to the flames." - cost = 3 - node_x = RIGHT_X_TIER_1 - node_y = RIGHT_Y_RIGHT - prerequisites = list(/datum/spell_node/create_bonfire) - is_passive = TRUE - -/datum/spell_node/fire_affinity/on_node_buy(mob/user) - user.mana_pool?.adjust_attunement(/datum/attunement/fire, 0.15) - to_chat(user, span_notice("You feel the flames dance within your soul.")) - -/datum/spell_node/death_affinity - name = "Death Affinity" - desc = "Touch the void and understand mortality." - cost = 3 - node_x = RIGHT_X_TIER_1 - node_y = RIGHT_Y_LEFT - prerequisites = list(/datum/spell_node/create_bonfire) - is_passive = TRUE - -/datum/spell_node/death_affinity/on_node_buy(mob/user) - user.mana_pool?.adjust_attunement(/datum/attunement/death, 0.15) - to_chat(user, span_notice("The void whispers secrets to you.")) - -/datum/spell_node/acid_splash - name = "Acid Splash" - desc = "Hurl a bubble of acid at your enemies." - node_x = RIGHT_X_TIER_2 - node_y = RIGHT_Y_LEFT - prerequisites = list(/datum/spell_node/death_affinity) - spell_type = /datum/action/cooldown/spell/projectile/acid_splash - -/datum/spell_node/infestation - name = "Infestation" - desc = "Cause a cloud of mites, fleas, and other parasites to swarm an enemy." - node_x = RIGHT_X_TIER_2 - node_y = RIGHT_Y_LEFT - 50 - prerequisites = list(/datum/spell_node/death_affinity) - spell_type = /datum/action/cooldown/spell/status/infestation - -/datum/spell_node/decompose - name = "Decompose" - desc = "Accelerate the decay of organic matter." - node_x = RIGHT_X_TIER_3 - node_y = RIGHT_Y_LEFT - 50 - prerequisites = list(/datum/spell_node/infestation) - spell_type = /datum/action/cooldown/spell/decompose - -/datum/spell_node/poison_spray - name = "Create Cloud" - desc = "Produce smoke from the liquid within an open bottle." - node_x = RIGHT_X_TIER_3 - node_y = RIGHT_Y_LEFT - prerequisites = list(/datum/spell_node/acid_splash) - spell_type = /datum/action/cooldown/spell/undirected/create_cloud - -/datum/spell_node/green_flame_blade - name = "Green-Flame Blade" - desc = "Evoke fiery green flames along your weapon." - node_x = RIGHT_X_TIER_2 - node_y = RIGHT_Y_RIGHT + 50 - prerequisites = list(/datum/spell_node/fire_affinity) - spell_type = /datum/action/cooldown/spell/enchantment/green_flame - -/datum/spell_node/spitfire - name = "Spitfire" - desc = "Spit a small gout of flame at your enemy." - node_x = RIGHT_X_TIER_2 - node_y = RIGHT_Y_RIGHT - prerequisites = list(/datum/spell_node/fire_affinity) - spell_type = /datum/action/cooldown/spell/projectile/fire_flare - -/datum/spell_node/fireball - name = "Fireball" - desc = "Launch an explosive ball of fire." - node_x = RIGHT_X_TIER_3 - node_y = RIGHT_Y_RIGHT - prerequisites = list(/datum/spell_node/spitfire) - spell_type = /datum/action/cooldown/spell/projectile/fireball - -/datum/spell_node/meteor_storm - name = "Meteor Storm" - desc = "Call down meteors from the heavens." - node_x = RIGHT_X_TIER_3 - node_y = RIGHT_Y_RIGHT + 50 - prerequisites = list(/datum/spell_node/fireball, /datum/spell_node/green_flame_blade) - spell_type = /datum/action/cooldown/spell/aoe/on_turf/meteor_storm - -/datum/spell_node/message - name = "Message" - desc = "Send telepathic communications across distances." - node_x = 0 - node_y = -100 - prerequisites = list(/datum/spell_node/arcyne_eye) - spell_type = /datum/action/cooldown/spell/undirected/message - -/datum/spell_node/phantom_ear - name = "Phantom Ear" - desc = "Conjure a spectral ear to perceive distant sounds." - node_x = 0 - node_y = -140 - prerequisites = list(/datum/spell_node/message) - spell_type = /datum/action/cooldown/spell/conjure/phantom_ear - -/datum/spell_node/arcyne_affinity - name = "Arcyne Affinity" - desc = "Devote yourself to Noc." - cost = 3 - node_x = DOWN_X_LEFT - node_y = DOWN_Y_TIER_1 - prerequisites = list(/datum/spell_node/message) - is_passive = TRUE - -/datum/spell_node/arcyne_affinity/on_node_buy(mob/user) - user.mana_pool?.adjust_attunement(/datum/attunement/arcyne, 0.15) - to_chat(user, span_notice("Noc whispers to you.")) - -/datum/spell_node/air_affinity - name = "Air Affinity" - desc = "Free yourself of burden." - cost = 3 - node_x = DOWN_X_RIGHT - node_y = DOWN_Y_TIER_1 - is_passive = TRUE - prerequisites = list(/datum/spell_node/message) - -/datum/spell_node/booming_blade - name = "Booming Blade" - desc = "Evoke thunderous energy around an enemy, which will burst if they move." - node_x = 0 - node_y = DOWN_Y_TIER_1 - 30 - prerequisites = list(/datum/spell_node/air_affinity, /datum/spell_node/arcyne_affinity) - spell_type = /datum/action/cooldown/spell/status/booming_blade - -/datum/spell_node/blade_ward - name = "Blade Ward" - desc = "Trace a sigil of warding, making you more difficult to injure." - node_x = 0 - node_y = DOWN_Y_TIER_1 - 70 - prerequisites = list(/datum/spell_node/air_affinity, /datum/spell_node/arcyne_affinity) - spell_type = /datum/action/cooldown/spell/undirected/blade_ward - -/datum/spell_node/air_affinity/on_node_buy(mob/user) - user.mana_pool?.adjust_attunement(/datum/attunement/aeromancy, 0.15) - to_chat(user, span_notice("You feel lighter.")) - -/datum/spell_node/longstrider - name = "Longstrider" - desc = "Ignore the effects of rough terrain." - node_x = DOWN_X_RIGHT - node_y = DOWN_Y_TIER_2 - prerequisites = list(/datum/spell_node/air_affinity) - spell_type = /datum/action/cooldown/spell/undirected/longstrider - -/datum/spell_node/haste - name = "Haste" - desc = "Make a creature move and act more quickly." - node_x = DOWN_X_RIGHT - node_y = DOWN_Y_TIER_3 - prerequisites = list(/datum/spell_node/longstrider) - spell_type = /datum/action/cooldown/spell/status/haste - -/datum/spell_node/repel - name = "Repel" - desc = "Push creatures away from you with a bolt of force." - node_x = DOWN_X_RIGHT + 100 - node_y = DOWN_Y_TIER_3 - prerequisites = list(/datum/spell_node/haste) - spell_type = /datum/action/cooldown/spell/projectile/repel - -/datum/spell_node/featherfall - name = "Featherfall" - desc = "Slow your descent when falling." - node_x = DOWN_X_RIGHT + 50 - node_y = DOWN_Y_TIER_2 - prerequisites = list(/datum/spell_node/air_affinity) - spell_type = /datum/action/cooldown/spell/undirected/feather_falling - -/datum/spell_node/slowdown_aoe - name = "Ensnare" - desc = "Root creatures to the floor in an area." - node_x = DOWN_X_RIGHT + 50 - node_y = DOWN_Y_TIER_2 - 50 - prerequisites = list(/datum/spell_node/featherfall) - spell_type = /datum/action/cooldown/spell/aoe/on_turf/ensnare - -/datum/spell_node/fetch - name = "Fetch" - desc = "Magically retrieve distant objects with a telekinetic bolt." - node_x = DOWN_X_LEFT - node_y = DOWN_Y_TIER_2 - prerequisites = list(/datum/spell_node/arcyne_affinity) - spell_type = /datum/action/cooldown/spell/projectile/fetch - -/datum/spell_node/arcane_bolt - name = "Arcyne Bolt" - desc = "Launch a bolt of pure magical energy." - node_x = DOWN_X_LEFT -50 - node_y = DOWN_Y_TIER_2 - prerequisites = list(/datum/spell_node/arcyne_affinity) - spell_type = /datum/action/cooldown/spell/projectile/arcyne_bolt - -/datum/spell_node/arcyne_storm - name = "Arcyne Storm" - desc = "Unleash a devastating storm of magical energy." - node_x = DOWN_X_LEFT -25 - node_y = DOWN_Y_TIER_3 - prerequisites = list(/datum/spell_node/arcane_bolt, /datum/spell_node/fetch) - spell_type = /datum/action/cooldown/spell/aoe/on_turf/arcyne_storm - -/datum/spell_node/light - name = "Light" - desc = "Create a magical source of illumination." - node_x = -100 - node_y = 0 - prerequisites = list(/datum/spell_node/arcyne_eye) - spell_type = /datum/action/cooldown/spell/undirected/conjure_item/light - -/datum/spell_node/darkvision - name = "Darkvision" - desc = "Grant the ability to see in darkness." - node_x = LEFT_X_TIER_1 - 30 - node_y = 0 - prerequisites = list(/datum/spell_node/light) - spell_type = /datum/action/cooldown/spell/undirected/touch/darkvision - -/datum/spell_node/electric_affinity - name = "Electric Affinity" - desc = "Attune yourself to the power of lightning." - cost = 3 - node_x = LEFT_X_TIER_1 - node_y = LEFT_Y_RIGHT - prerequisites = list(/datum/spell_node/light) - is_passive = TRUE - -/datum/spell_node/electric_affinity/on_node_buy(mob/user) - user.mana_pool?.adjust_attunement(/datum/attunement/electric, 0.15) - to_chat(user, span_notice("Lightning crackles through your veins.")) - -/datum/spell_node/earth_affinity - name = "Earth Affinity" - desc = "Attune yourself to the earth." - cost = 3 - node_x = LEFT_X_TIER_1 - node_y = LEFT_Y_LEFT - prerequisites = list(/datum/spell_node/light) - is_passive = TRUE - -/datum/spell_node/earth_affinity/on_node_buy(mob/user) - user.mana_pool?.adjust_attunement(/datum/attunement/earth, 0.15) - to_chat(user, span_notice("The earth moves beneath you.")) - -/datum/spell_node/guidance - name = "Guidance" - desc = "Provide arcyne assistance to aid in both study and combat." //this was "divine assistance" before??? its an arcyne spell you wot - node_x = LEFT_X_TIER_2 - node_y = LEFT_Y_LEFT - prerequisites = list(/datum/spell_node/earth_affinity) - spell_type = /datum/action/cooldown/spell/status/guidance - - -/datum/spell_node/magic_stone - name = "Magicians Brick" - desc = "Concentrate your intellect into an arcyne brick." - node_x = LEFT_X_TIER_2 - node_y = LEFT_Y_LEFT - 50 - prerequisites = list(/datum/spell_node/earth_affinity) - spell_type = /datum/action/cooldown/spell/undirected/conjure_item/brick - -/datum/spell_node/flower_field - name = "Flower Field" - desc = "Use a flower as a catalyst to create a magical field of that flower." - node_x = LEFT_X_TIER_3 - node_y = LEFT_Y_LEFT - 50 - prerequisites = list(/datum/spell_node/magic_stone) - spell_type = /datum/action/cooldown/spell/aoe/on_turf/circle/flower_field - -/datum/spell_node/storm_caller - name = "Storm Caller" - desc = "Channel the fury of tempests." - cost = 5 - node_x = LEFT_X_TIER_2 - node_y = LEFT_Y_RIGHT - prerequisites = list(/datum/spell_node/electric_affinity) - is_passive = TRUE - -/datum/spell_node/storm_caller/on_node_buy(mob/user) - user.mana_pool?.adjust_attunement(/datum/attunement/electric, 0.12) - user.mana_pool?.adjust_attunement(/datum/attunement/ice, 0.08) - to_chat(user, span_notice("The fury of storms courses through you.")) - -/datum/spell_node/lightning_bolt - name = "Lightning Bolt" - desc = "Strike your enemies with a bolt of lightning." - node_x = LEFT_X_TIER_2 - node_y = LEFT_Y_RIGHT + 50 - prerequisites = list(/datum/spell_node/electric_affinity) - spell_type = /datum/action/cooldown/spell/projectile/lightning - -/datum/spell_node/mana_conservation - name = "Mana Conservation" - desc = "Learn to passively recharge your mana." - cost = 5 - node_x = LEFT_X_TIER_3 - node_y = LEFT_Y_RIGHT - prerequisites = list(/datum/spell_node/storm_caller) - is_passive = TRUE - -/datum/spell_node/mana_conservation/on_node_buy(mob/user) - user.mana_pool?.set_natural_recharge(user.mana_pool.ethereal_recharge_rate + 0.1) - to_chat(user, span_notice("You learn to channel magic more efficiently.")) - -/datum/spell_node/sundering_lightning - name = "Sundering Lightning" - desc = "Conjure a miniature thunderstorm that bypasses magical defenses." - node_x = LEFT_X_TIER_3 - node_y = LEFT_Y_RIGHT + 50 - prerequisites = list(/datum/spell_node/lightning_bolt, /datum/spell_node/mana_conservation) - spell_type = /datum/action/cooldown/spell/sundering_lightning - -/datum/spell_node/blade_burst - name = "Blade Burst" - desc = "Create a spectral blade to slice a target." - node_x = LEFT_X_TIER_3 - node_y = LEFT_Y_LEFT - prerequisites = list(/datum/spell_node/guidance) - spell_type = /datum/action/cooldown/spell/blade_burst - -/datum/spell_node/prestidigitation - name = "Prestidigitation" - desc = "Perform a variety of simple magical tricks." - node_x = 0 - node_y = 100 - prerequisites = list(/datum/spell_node/arcyne_eye) - spell_type = /datum/action/cooldown/spell/undirected/touch/prestidigitation - -/datum/spell_node/frost_affinity - name = "Frost Affinity" - desc = "Embrace the cold within your soul." - cost = 3 - node_x = UP_X_RIGHT - node_y = UP_Y_TIER_1 - prerequisites = list(/datum/spell_node/prestidigitation) - is_passive = TRUE - -/datum/spell_node/frost_affinity/on_node_buy(mob/user) - user.mana_pool?.adjust_attunement(/datum/attunement/ice, 0.15) - to_chat(user, span_notice("Cold seeps into your very essence.")) - -/datum/spell_node/snap_freeze - name = "Snap Freeze" - desc = "Instantly freeze the moisture in an area." - node_x = UP_X_RIGHT + 50 - node_y = UP_Y_TIER_1 - prerequisites = list(/datum/spell_node/frost_affinity) - spell_type = /datum/action/cooldown/spell/aoe/on_turf/snap_freeze - -/datum/spell_node/dark_affinity - name = "Dark Affinity" - desc = "Feel the light vanish." - cost = 3 - node_x = UP_X_LEFT - node_y = UP_Y_TIER_1 - prerequisites = list(/datum/spell_node/prestidigitation) - is_passive = TRUE - -/datum/spell_node/dark_affinity/on_node_buy(mob/user) - user.mana_pool?.adjust_attunement(/datum/attunement/dark, 0.15) - to_chat(user, span_notice("The void whispers secrets to you.")) - -/datum/spell_node/eldritch_blast - name = "Eldritch Blast" - desc = "Let forth a wave of maddening mind-energy." - node_x = UP_X_LEFT - node_y = UP_Y_TIER_2 - prerequisites = list(/datum/spell_node/dark_affinity) - spell_type = /datum/action/cooldown/spell/cone/staggered/eldritch_blast - -/datum/spell_node/encode_thoughts - name = "Encode Thoughts" - desc = "Incept an intrusive thought into the mind of your target. They may not agree with it." - node_x = UP_X_LEFT - 50 - node_y = UP_Y_TIER_2 - prerequisites = list(/datum/spell_node/dark_affinity) - spell_type = /datum/action/cooldown/spell/undirected/list_target/encode_thoughts - -/datum/spell_node/mind_sliver - name = "Mind Spike" - desc = "Drive a disorienting spike of psychic energy into the brain of your enemy." - node_x = UP_X_LEFT - 50 - node_y = UP_Y_TIER_3 - prerequisites = list(/datum/spell_node/encode_thoughts) - spell_type = /datum/action/cooldown/spell/mind_spike - -/datum/spell_node/gravity - name = "Gravity" - desc = "Manipulate gravitational forces around a target to force them to the ground." - node_x = UP_X_LEFT - node_y = UP_Y_TIER_3 - prerequisites = list(/datum/spell_node/mind_sliver, /datum/spell_node/eldritch_blast) - spell_type = /datum/action/cooldown/spell/gravity - - -/datum/spell_node/beam_of_frost - name = "Ray of Frost" - desc = "Set a frigid beam of hyperborean energy upon a target." - node_x = UP_X_RIGHT - node_y = UP_Y_TIER_2 - prerequisites = list(/datum/spell_node/frost_affinity) - spell_type = /datum/action/cooldown/spell/beam/beam_of_frost - -/datum/spell_node/chill_touch - name = "Chill Touch" - desc = "Create a ghostly, skeletal hand that wrestles with foes." - node_x = UP_X_RIGHT + 50 - node_y = UP_Y_TIER_2 - prerequisites = list(/datum/spell_node/frost_affinity) - spell_type = /datum/action/cooldown/spell/chill_touch - -/datum/spell_node/cone_of_frost - name = "Cone of Frost" - desc = "Let forth a wave of frost to form." - node_x = UP_X_RIGHT + 50 - node_y = UP_Y_TIER_2 + 50 - prerequisites = list(/datum/spell_node/chill_touch) - spell_type = /datum/action/cooldown/spell/cone/staggered/cone_of_cold - -/datum/spell_node/frostbolt - name = "Frostbolt" - desc = "Launch a shard of arcyne ice at your enemy." - node_x = UP_X_RIGHT - node_y = UP_Y_TIER_3 - prerequisites = list(/datum/spell_node/beam_of_frost) - spell_type = /datum/action/cooldown/spell/projectile/frost_bolt - - -/datum/spell_node/gib - name = "Xylix's Cruel Prank" - desc = "Fucked up and evil." - node_x = -500 - node_y = -500 - is_passive = TRUE - -/datum/spell_node/gib/on_node_buy(mob/living/user) - . = ..() - user.gib() - for(var/mob/target in GLOB.player_list) - if(isnewplayer(target)) - continue - bordered_message(target, list( - span_extremelybig(html_tag("center", SPAN_GOD_XYLIX("An Infectious Laugh from the God of Fools!"))), - html_tag("center", span_extremelybig(span_info("[user.real_name] has fallen for a jest most cruel!"))), - )) - target.playsound_local(target, 'sound/magic/mockery.ogg', 100, environment_override = 25) - if(target.add_stress(/datum/stress_event/xylix_laughed)) - addtimer(CALLBACK(target, TYPE_PROC_REF(/mob, emote), pick("laugh", "chuckle")), rand(5, 20)) - record_round_statistic(STATS_CRUEL_PRANKS) diff --git a/code/modules/spells/_spell_tree/learn_spell.dm b/code/modules/spells/_spell_tree/learn_spell.dm deleted file mode 100644 index 5e0be2371d5..00000000000 --- a/code/modules/spells/_spell_tree/learn_spell.dm +++ /dev/null @@ -1,622 +0,0 @@ -/datum/action/cooldown/spell/undirected/learn - name = "Attempt to learn a new spell" - desc = "Weave a new spell" - button_icon_state = "book1" - sound = null - - school = SCHOOL_TRANSMUTATION - spell_flags = SPELL_UNETCHABLE - - charge_required = FALSE - has_visual_effects = FALSE - spell_cost = 0 - - var/list/unlocked_spells = list() - var/datum/spell_node/selected_spell = null - -/datum/action/cooldown/spell/undirected/learn/Destroy() - unlocked_spells = null - selected_spell = null - return ..() - -/datum/action/cooldown/spell/undirected/learn/cast(list/targets, mob/user = usr) - . = ..() - open_spell_interface(owner) - -/datum/action/cooldown/spell/undirected/learn/proc/open_spell_interface(mob/user) - var/datum/spell_interface/interface = new(src, user) - interface.show() - -/datum/action/cooldown/spell/undirected/learn/Topic(href, href_list) - if(href_list["action"] == "learn_spell") - var/spell_type = text2path(href_list["spell"]) - if(!spell_type) - return - - var/datum/spell_node/node = new spell_type - - if(spell_type in unlocked_spells) - to_chat(usr, span_warning("You have already learned this [node.is_passive ? "technique" : "spell"].")) - qdel(node) - return - - for(var/prereq in node.prerequisites) - if(!(prereq in unlocked_spells)) - to_chat(usr, span_warning("Prerequisites not met for this [node.is_passive ? "technique" : "spell"].")) - qdel(node) - return - var/mob/living/user = usr - var/cost = node.cost - if(node.spell_type) - for(var/datum/action/cooldown/spell in user.actions) - if(istype(spell, node.spell_type)) - cost = 0 - break - - if(cost > user.spell_points - user.used_spell_points) - to_chat(user, span_warning("You do not have enough spell points to learn this.")) - qdel(node) - return - - user.adjust_spell_points(cost, TRUE) - unlocked_spells += spell_type - - if(node.is_passive) - node.on_node_buy(user) - to_chat(user, span_notice("You have learned the passive technique: [node.name]")) - else - if(node.spell_type) - user.add_spell(node.spell_type, override = TRUE) - to_chat(usr, span_notice("You have learned the spell: [node.name]")) - - selected_spell = node - - if(owner == usr) - addtimer(CALLBACK(src, PROC_REF(open_spell_interface), usr), 0.1) - -/datum/spell_interface - var/datum/action/cooldown/spell/undirected/learn/matrix - var/datum/browser/window - var/mob/living/user - -/datum/spell_interface/New(datum/action/cooldown/spell/undirected/learn/M, mob/U) - matrix = M - user = U - -/datum/spell_interface/Destroy(force) - matrix = null - user = null - window.close() - window = null - return ..() - -/datum/spell_interface/proc/show() - if(!user || !matrix) - return - - var/content = generate_interface_html() - window = new(user, "spell_matrix", null, 800, 600) - window.set_content(content) - window.open() - -/datum/spell_interface/proc/generate_interface_html() - user << browse_rsc('html/research_hover.png') - user << browse_rsc('html/research_base.png') - user << browse_rsc('html/research_known.png') - user << browse_rsc('html/research_selected.png') - user << browse_rsc('html/KettleParallaxBG.png') - user << browse_rsc('html/KettleParallaxNeb.png') - - var/html = {" - - - - - -
-
-
-
-
-
- -
-
- [generate_connections_html()] - [generate_nodes_html()] -
-
- -
-
Spell Points: [user.spell_points - user.used_spell_points]/[user.spell_points]
-
- - - - - - - "} - - return html - -/datum/spell_interface/proc/generate_nodes_html() - var/html = "" - var/center_x = 0 - var/center_y = 0 - - for(var/datum/spell_node/node_type as anything in subtypesof(/datum/spell_node)) - var/datum/spell_node/node = new node_type - var/is_unlocked = (node_type in matrix.unlocked_spells) - var/is_available = is_unlocked || can_learn_spell(node) - var/is_selected = (matrix.selected_spell && matrix.selected_spell.type == node_type) - - var/class_list = "spell-node" - if(node.is_passive) - class_list += " passive" - if(is_unlocked) - class_list += " unlocked" - else if(is_available) - class_list += " available" - else - class_list += " locked" - - if(is_selected) - class_list += " selected" - - var/node_x = center_x + node.node_x - 16 - var/node_y = center_y + node.node_y - 16 - - var/list/req_text = list() - for(var/prereq_type in node.prerequisites) - var/datum/spell_node/prereq = new prereq_type - var/status = (prereq_type in matrix.unlocked_spells) ? "✓" : "✗" - req_text += "[status] [prereq.name]" - qdel(prereq) - - var/node_data = list( - "name" = node.name, - "desc" = node.desc, - "cost" = node.cost, - "requirements" = req_text, - "unlocks" = node.unlocks, - "is_passive" = node.is_passive - ) - - html += {"
- [node.name] -
"} - - qdel(node) - - return html - -/datum/spell_interface/proc/generate_connections_html() - var/html = "" - var/center_x = 0 - var/center_y = 0 - - var/list/node_positions = list() - for(var/datum/spell_node/node_type as anything in subtypesof(/datum/spell_node)) - var/datum/spell_node/temp_node = new node_type - node_positions[node_type] = list("x" = temp_node.node_x, "y" = temp_node.node_y) - qdel(temp_node) - - for(var/datum/spell_node/node_type as anything in subtypesof(/datum/spell_node)) - var/datum/spell_node/node = new node_type - - for(var/prereq_type in node.prerequisites) - if(!(prereq_type in node_positions)) - continue - - var/list/prereq_pos = node_positions[prereq_type] - var/list/current_pos = node_positions[node_type] - - var/start_center_x = center_x + prereq_pos["x"] - var/start_center_y = center_y + prereq_pos["y"] - var/end_center_x = center_x + current_pos["x"] - var/end_center_y = center_y + current_pos["y"] - - var/dx = end_center_x - start_center_x - var/dy = end_center_y - start_center_y - var/distance = sqrt(dx*dx + dy*dy) - - if(distance == 0) - continue - - var/angle = arctan(dx, dy) - if(angle < 0) - angle += 360 - - var/is_unlocked_connection = (prereq_type in matrix.unlocked_spells) && (node_type in matrix.unlocked_spells) - var/connection_class = "connection-line" - if(is_unlocked_connection) - connection_class += " unlocked" - - html += {"
-
"} - - qdel(node) - return html - -/datum/spell_interface/proc/can_learn_spell(datum/spell_node/node) - for(var/prereq in node.prerequisites) - if(!(prereq in matrix.unlocked_spells)) - return FALSE - if(node.cost > user.spell_points - user.used_spell_points) - return FALSE - - return TRUE - diff --git a/code/modules/spells/_talent_tree/learn_talent.dm b/code/modules/spells/_talent_tree/learn_talent.dm index ba5660b30ff..e55a68fd7df 100644 --- a/code/modules/spells/_talent_tree/learn_talent.dm +++ b/code/modules/spells/_talent_tree/learn_talent.dm @@ -4,7 +4,6 @@ button_icon_state = "book1" sound = null - school = SCHOOL_TRANSMUTATION charge_required = FALSE has_visual_effects = FALSE spell_cost = 0 diff --git a/code/modules/spells/spell.dm b/code/modules/spells/spell.dm index 1b9664c2d9e..5d0a3d1a01f 100644 --- a/code/modules/spells/spell.dm +++ b/code/modules/spells/spell.dm @@ -33,13 +33,16 @@ * 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. * - [update_spell_name][/datum/action/cooldown/spell/update_spell_name] updates the prefix of the spell name based on its level. */ + +/// Dedicated maptext holder for the ARC indicator, separate from cooldown and such +/atom/movable/screen/arc_maptext_holder + maptext_x = 6 + maptext_y = 12 + /datum/action/cooldown/spell + abstract_type = /datum/action/cooldown/spell name = "Spell" desc = "A wizard spell." background_icon = 'icons/mob/actions/roguespells.dmi' @@ -54,25 +57,20 @@ /// Variable for type of spell. var/spell_type = SPELL_MANA - /// School of magic. (Might go unused) - var/school = SCHOOL_UNSET - /// Cost to learn this spell in the tree. - var/point_cost = 0 /// Cost to cast based on [spell_type]. var/spell_cost = 0 ///this is purely for etching var/spell_tier = 1 + ///do we even bother to show in the ui? + var/learnable = TRUE + /// The sound played on cast. var/sound = 'sound/magic/whiteflame.ogg' /// If the spell uses the wizard spell rank system, the cooldown reduction per rank of the spell var/cooldown_reduction_per_rank = 0 SECONDS - /// 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 /// What is uttered when the user casts the spell. var/invocation @@ -108,12 +106,13 @@ /// Stat associated with spell enchancements. var/associated_stat = STAT_INTELLIGENCE - /// Assoc list of [datum/attunement] to value. - var/list/attunements + ///the color we use (overrides attunements for animate color) + var/spell_color + ///list of essences we can use as a sub for cost var/list/essences - /// Value summed from caster and spell attunements to adjust some spell effects. - var/attuned_strength + /// Flat magnitude bonus/penalty pulled from active form/technique modifiers (spell_modifier components), computed once per cast. + var/spell_magnitude_modifier = 0 // Pointed vars // In the TG refactor these weren't a given but almost all our spells are pointed including most spell types. @@ -180,6 +179,33 @@ /// Timer ID for the auto cancel, so we can cancel it var/auto_cancel_timer = null + /// Technique (class) this spell belongs to, e.g. TECHNIQUE_DESTRUCTION. Null = techniqueless. + var/required_technique = null + /// Form (element) this spell belongs to, e.g. FORM_FIRE. Null = formless. + var/required_form = null + /// How many points must be invested in required_technique (and required_form, if set) + /// before this spell can be learned. Ignored if both required_technique and required_form are null. + var/required_level = 0 + /// Optional list of other spell typepaths that must already be unlocked before this one + /// can be learned. Lets you build a simple prerequisite chain within a technique/form. + var/list/prerequisite_spells + ///the amount of charges we have if we are a spellbook varient + var/initial_charges = 10 + /// If TRUE, this spell's uses are gated by a spellbook item's charge pool instead of (or in addition to) spell_cost. + var/uses_spellbook_charges = FALSE + /// The spell_mastery datum that owns our charge pool, set when granted from a spellbook. + var/datum/spell_mastery/mastery_source + /// The specific item instance our charge pool is keyed against. + var/atom/movable/charge_item + /// Cached overlay image showing remaining charges, added/removed from action buttons. + var/image/charge_overlay + /// Impact visual intensity. SPELL_IMPACT_NONE / SPELL_IMPACT_LOW / SPELL_IMPACT_MEDIUM / SPELL_IMPACT_HIGH + var/spell_impact_intensity = SPELL_IMPACT_LOW + /// Override color for the impact effect. If null, uses light_color. + var/spell_impact_color + + + /datum/action/cooldown/spell/New(Target) . = ..() if(!active_msg) @@ -190,6 +216,12 @@ if(click_to_activate && !charge_required) ranged_mousepointer = 'icons/effects/mousemice/charge/spell_charged.dmi' + if(required_form && !spell_color) + spell_color = GLOB.form_colors[required_form] + + if(!spell_impact_color) + spell_impact_color = spell_color + if(!charge_required) return if(charge_time <= 0) @@ -385,23 +417,30 @@ SEND_SIGNAL(owner, COMSIG_MOB_ABILITY_FINISHED, src) return target_val -/// Adjust the base charge time based on the users stats -/datum/action/cooldown/spell/proc/get_adjusted_charge_time() - if(charge_time <= 0) - return - - var/mob/living/living_owner = owner - var/new_time = charge_time +/// Queries the owner for any active form/technique modifiers (from spell_modifier components) +/// that apply to THIS spell, based on its required_form / required_technique. +/// Returns list("cost" = mult, "castSpeed" = mult, "magnitude" = total) - defaults if nothing matches. +/datum/action/cooldown/spell/proc/get_form_technique_modifiers() + var/list/result = list("cost" = 1, "castSpeed" = 1, "magnitude" = 1) + if(!owner) + return result - new_time -= charge_time * GET_MOB_SKILL_VALUE_OLD(living_owner, associated_skill) * 0.05 + var/list/modifiers = list() + SEND_SIGNAL(owner, COMSIG_SPELL_REQUEST_MODIFIERS, modifiers) + if(!length(modifiers)) + return result - var/owner_stat = living_owner.get_stat(associated_stat) - if(owner_stat > 10) - new_time -= charge_time * (owner_stat - 10) * 0.02 - else - new_time += charge_time * (10 - owner_stat) * 0.02 + for(var/list/entry in modifiers) + if(required_form && entry["form"] == required_form) + result["cost"] *= entry["cost"] + result["castSpeed"] *= entry["castSpeed"] + result["magnitude"] += entry["magnitude"] + if(required_technique && entry["technique"] == required_technique) + result["cost"] *= entry["cost"] + result["castSpeed"] *= entry["castSpeed"] + result["magnitude"] += entry["magnitude"] - return max(new_time, 1 DECISECONDS) + return result /// Adjust the base spell cost based on the users stats /datum/action/cooldown/spell/proc/get_adjusted_cost(cost_override) @@ -421,29 +460,29 @@ else new_cost += spell_cost * (10 - owner_stat) * 0.02 + new_cost *= get_form_technique_modifiers()["cost"] + return max(new_cost, 0) -/// Do any attunement handling in here or any time after before_cast -/datum/action/cooldown/spell/proc/handle_attunements() - SHOULD_CALL_PARENT(TRUE) +/// Adjust the base charge time based on the users stats +/datum/action/cooldown/spell/proc/get_adjusted_charge_time() + if(charge_time <= 0) + return - var/mob/living/caster = owner - var/list/datum/mana_pool/usable_pools = caster.get_all_pools() - var/list/total_attunements = GLOB.default_attunements.Copy() - - for(var/datum/mana_pool/pool as anything in usable_pools) - for(var/negative_attunement in pool.negative_attunements) - total_attunements[negative_attunement] += pool.negative_attunements[negative_attunement] - for(var/attunement in pool.attunements) - total_attunements[attunement] += pool.attunements[attunement] - - var/total_value = 1 - for(var/datum/attunement/attunement as anything in attunements) - if(!(attunement in total_attunements)) - continue - total_value += total_attunements[attunement] * attunements[attunement] + var/mob/living/living_owner = owner + var/new_time = charge_time + + new_time -= charge_time * GET_MOB_SKILL_VALUE_OLD(living_owner, associated_skill) * 0.05 + + var/owner_stat = living_owner.get_stat(associated_stat) + if(owner_stat > 10) + new_time -= charge_time * (owner_stat - 10) * 0.02 + else + new_time += charge_time * (10 - owner_stat) * 0.02 - attuned_strength = clamp(total_value, 0.5, 2.5) + new_time /= get_form_technique_modifiers()["castSpeed"] + + return max(new_time, 1 DECISECONDS) /// Checks if the owner of the spell can currently cast it. /// Does not check anything involving potential targets. @@ -472,6 +511,16 @@ if(!check_cost(feedback = feedback)) return FALSE + if(!(spell_requirements & SPELL_CASTABLE_WHILE_MOUNTED) && owner.client && owner.buckled && isliving(owner.buckled)) + if(feedback) + owner.balloon_alert(owner, "Too distracted riding to cast!") + return FALSE + + if(uses_spellbook_charges && mastery_source && !mastery_source.has_spellbook_charges(type)) + if(feedback) + owner.balloon_alert(owner, "No charges remaining!") + return FALSE + // Certain spells are not allowed on the centcom zlevel var/turf/caster_turf = get_turf(owner) if((spell_requirements & SPELL_REQUIRES_STATION) && is_centcom_level(caster_turf.z)) @@ -522,7 +571,10 @@ if(cast_on == owner) owner.balloon_alert(owner, "Can't self cast!") return FALSE - + if(spell_requirements & SPELL_REQUIRES_SAME_Z) + if(owner.z != cast_on.z) + owner.balloon_alert(owner, "They are to far to cast on!") + return FALSE return TRUE // The actual cast chain occurs here, in Activate(). @@ -550,8 +602,7 @@ // That way stuff like teleports or shape-shifts can be invoked before ocurring spell_feedback(owner) - if(length(attunements)) - handle_attunements() + spell_magnitude_modifier = get_form_technique_modifiers()["magnitude"] // Actually cast the spell. Main effects go here cast(target) @@ -642,6 +693,47 @@ return FALSE return TRUE +/datum/action/cooldown/spell/proc/get_remaining_charges() + if(!uses_spellbook_charges || !mastery_source || !charge_item) + return -1 + var/list/charges = mastery_source.spellbook_charges[charge_item] + if(!charges) + return -1 + return charges[type] || 0 + +/datum/action/cooldown/spell/update_button_status(atom/movable/screen/movable/action_button/button, force = FALSE) + . = ..() + if(!uses_spellbook_charges || !button) + return + + var/charges = get_remaining_charges() + if(charges < 0) + if(charge_overlay) + button.cut_overlay(charge_overlay) + charge_overlay = null + return + + if(charge_overlay) + button.cut_overlay(charge_overlay) + else + charge_overlay = image(icon = 'icons/hud/screen_gen.dmi', icon_state = "") + charge_overlay.plane = ABOVE_HUD_PLANE + charge_overlay.pixel_x = 20 + charge_overlay.pixel_y = 20 + charge_overlay.maptext_x = 0 + charge_overlay.maptext_y = 0 + charge_overlay.maptext_width = 16 + charge_overlay.maptext_height = 16 + + var/badge_color = (charges <= 0) ? "#ff4444" : "#ffffff" + charge_overlay.maptext = MAPTEXT_PIXELIFY("[charges]") + + button.add_overlay(charge_overlay) + + // Red-out the whole button when it genuinely cannot fire right now + if(charges <= 0 && !active_background_icon_state && !active_icon_state && !active_overlay_icon_state) + button.color = "#ff4444" + /** * Actions done as the main effect of the spell. * @@ -653,6 +745,9 @@ SEND_SIGNAL(src, COMSIG_SPELL_CAST, cast_on) record_featured_object_stat(FEATURED_STATS_SPELLS, name) + if(uses_spellbook_charges && mastery_source) + mastery_source.consume_spellbook_charge(type) + build_all_button_icons(UPDATE_BUTTON_STATUS) if(owner) SEND_SIGNAL(owner, COMSIG_MOB_CAST_SPELL, src, cast_on) if(owner.ckey) @@ -688,7 +783,7 @@ if(has_visual_effects) var/mob/living/caster = owner - caster.finish_spell_visual_effects(attunements) + caster.finish_spell_visual_effects(spell_color) /// Provides feedback after a spell cast occurs, in the form of a cast sound and/or invocation /datum/action/cooldown/spell/proc/spell_feedback(mob/living/invoker) @@ -736,7 +831,7 @@ if(has_visual_effects) var/mob/living/caster = owner - caster.start_spell_visual_effects(attunements) + caster.start_spell_visual_effects(spell_color) if(charge_message) owner.balloon_alert(owner, charge_message) @@ -818,66 +913,10 @@ next_use_time -= cooldown_time // Basically, ensures that the ability can be used now build_all_button_icons() -// Spell level is unused currently, it could be used for attunement thresholds - -/** - * 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) - 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)]" + name = "[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 "" - /// Check if the spell is castable by cost /datum/action/cooldown/spell/proc/check_cost(cost_override, feedback = TRUE) var/mob/living/caster = owner @@ -898,7 +937,7 @@ switch(spell_type) if(SPELL_MANA) - if(!caster.has_mana_available(attunements, used_cost)) + if(!caster.has_mana_available(used_cost)) if(feedback) owner.balloon_alert(owner, "Not enough mana to cast!") return FALSE @@ -990,7 +1029,7 @@ switch(used_type) if(SPELL_MANA) var/mob/living/caster = owner - caster.consume_mana(attunements, used_cost) + caster.consume_mana(used_cost) if(SPELL_MIRACLE) var/mob/living/carbon/human/H = owner @@ -1013,7 +1052,7 @@ owner.balloon_alert(owner, "not enough essence!") return - gaunt.consume_essence(used_cost, attunements) + gaunt.consume_essence(used_cost, essences) if(SPELL_BLOOD) var/mob/living/caster = owner @@ -1127,6 +1166,11 @@ cancel_casting() +/// Override on spells that have an alt mode (e.g. cycling ward types). Called by the Alt Mode keybind (Shift+G). +/// Return TRUE if handled. +/datum/action/cooldown/spell/proc/toggle_alt_mode(mob/user) + return FALSE + /** *Used to calculate bonuses to Great Hunt miracles/spells. * diff --git a/code/modules/spells/spell_types/augment_buff/_base.dm b/code/modules/spells/spell_types/augment_buff/_base.dm new file mode 100644 index 00000000000..59041f716d6 --- /dev/null +++ b/code/modules/spells/spell_types/augment_buff/_base.dm @@ -0,0 +1,38 @@ +/datum/action/cooldown/spell/augment_buff + button_icon = 'icons/mob/actions/spells/mage_augmentation.dmi' + sound = 'sound/magic/haste.ogg' + + click_to_activate = TRUE + cast_range = SPELL_RANGE_GROUND + self_cast_possible = TRUE + + spell_cost = 25 + required_technique = TECHNIQUE_ALTERATION + + invocation_type = INVOCATION_SHOUT + invocation = "FIX ME!" + + charge_required = TRUE + charge_time = CHARGETIME_MINOR + charge_slowdown = CHARGING_SLOWDOWN_SMALL + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 90 SECONDS + + spell_impact_intensity = SPELL_IMPACT_NONE + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/self_cast_cooldown_multiplier = 1 + var/other_cast_cooldown_reduction = 0.25 + /// TRUE only while resolving a self-targeted cast, so the cooldown hook knows to apply the penalty. + var/tmp/empowering_self = FALSE + +/datum/action/cooldown/spell/augment_buff/before_cast(atom/cast_on) + empowering_self = (cast_on == owner) + return ..() + +/datum/action/cooldown/spell/augment_buff/after_cast(atom/cast_on) + . = ..() + var/used_multi = empowering_self ? self_cast_cooldown_multiplier : 1 - other_cast_cooldown_reduction + StartCooldown(cooldown_time * used_multi) + empowering_self = FALSE diff --git a/code/modules/spells/spell_types/augment_buff/defense.dm b/code/modules/spells/spell_types/augment_buff/defense.dm new file mode 100644 index 00000000000..ec747165298 --- /dev/null +++ b/code/modules/spells/spell_types/augment_buff/defense.dm @@ -0,0 +1,38 @@ +/datum/action/cooldown/spell/augment_buff/augment_defense + name = "Augment Defense" + desc = "Uses arcyne energy to restore a target's defenses, clearing away any vulnerability or exposure they may have suffered." + button_icon = 'icons/mob/actions/spells/mage_augmentation.dmi' + button_icon_state = "stoneskin" + + spell_cost = 5 + + invocation = "Resta!" + required_form = FORM_LIFE + invocation_type = INVOCATION_SHOUT + + charge_required = FALSE + + cooldown_time = 30 SECONDS + +/datum/action/cooldown/spell/augment_buff/augment_defense/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/caster = owner + if(!istype(caster)) + return FALSE + if(!isliving(cast_on)) + to_chat(caster, span_warning("That is not a valid target!")) + return FALSE + var/mob/living/target = cast_on + + if(target.has_status_effect(/datum/status_effect/debuff/exposed)) + target.remove_status_effect(/datum/status_effect/debuff/exposed) + if(target.has_status_effect(/datum/status_effect/debuff/vulnerable)) + target.remove_status_effect(/datum/status_effect/debuff/vulnerable) + if(target.has_status_effect(/datum/status_effect/debuff/feinted)) + target.remove_status_effect(/datum/status_effect/debuff/feinted) + + target.balloon_alert_to_viewers("guarded!") + target.visible_message(span_warning("[target] 's defenses are reinforced!"), span_notice("My defense snaps back into place.")) + if(target != caster) + to_chat(caster, span_notice("I shore up [target]'s defenses, clearing their openings.")) + return TRUE diff --git a/code/modules/spells/spell_types/augment_buff/enlarge.dm b/code/modules/spells/spell_types/augment_buff/enlarge.dm new file mode 100644 index 00000000000..fb1168700cb --- /dev/null +++ b/code/modules/spells/spell_types/augment_buff/enlarge.dm @@ -0,0 +1,46 @@ +/datum/action/cooldown/spell/augment_buff/enlarge + name = "Enlarge Person" + desc = "For a time, enlarges your target to a giant hulking version of themselves capable of bashing into doors. Does not work on folk who are already large." + button_icon_state = "enlarge" + + required_form = FORM_EARTH + + invocation = "Dilatare!" + invocation_type = INVOCATION_SHOUT + cooldown_time = 2 MINUTES + +/datum/action/cooldown/spell/augment_buff/enlarge/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + if(!isliving(cast_on)) + to_chat(H, span_warning("That is not a valid target!")) + return FALSE + + var/mob/living/carbon/target = cast_on + if(target != H) + to_chat(H, span_warning("[target] is not of my fellowship!")) + return FALSE + + if(HAS_TRAIT(target, TRAIT_BIGGUY)) + to_chat(H, span_warning("They're too big to enlarge!")) + return FALSE + + ADD_TRAIT(target, TRAIT_BIGGUY, MAGIC_TRAIT) + target.transform = target.transform.Scale(1.25, 1.25) + target.transform = target.transform.Translate(0, (0.25 * 16)) + target.update_transform() + to_chat(target, span_warning("I feel taller than usual, and like I could run through a door!")) + target.visible_message("[target]'s body grows in size!") + addtimer(CALLBACK(src, PROC_REF(remove_buff), target), wait = 60 SECONDS) + return TRUE + +/datum/action/cooldown/spell/augment_buff/enlarge/proc/remove_buff(mob/living/carbon/target) + REMOVE_TRAIT(target, TRAIT_BIGGUY, MAGIC_TRAIT) + target.transform = target.transform.Translate(0, -(0.25 * 16)) + target.transform = target.transform.Scale(1/1.25, 1/1.25) + target.update_transform() + to_chat(target, span_warning("I feel smaller all of a sudden.")) + target.visible_message("[target]'s body shrinks quickly!") diff --git a/code/modules/spells/spell_types/augment_buff/precognititon.dm b/code/modules/spells/spell_types/augment_buff/precognititon.dm new file mode 100644 index 00000000000..f2a4cd49c08 --- /dev/null +++ b/code/modules/spells/spell_types/augment_buff/precognititon.dm @@ -0,0 +1,63 @@ +/datum/action/cooldown/spell/augment_buff/precognition + name = "Precognition" + desc = "Peer a few moments into the future for yourself or an ally, readying them before the moment arrives. Cuts 15 seconds from the remaining cooldown of the target's Defend and Special." + button_icon_state = "readomen" + + required_form = FORM_ARCANE + + invocation = "Praescientia!" + invocation_type = INVOCATION_SHOUT + + charge_required = FALSE + cooldown_time = 75 SECONDS + other_cast_cooldown_reduction = 0 // Does not benefit from ally-cast cooldown reduction + +/datum/action/cooldown/spell/augment_buff/precognition/cast(atom/cast_on) + . = ..() + if(!isliving(cast_on)) + to_chat(owner, span_warning("That is not a valid target!")) + return FALSE + var/mob/living/target = cast_on + + var/hastened = FALSE + hastened |= reduce_intent_cooldown(target, /datum/status_effect/debuff/clashcd) + hastened |= reduce_intent_cooldown(target, /datum/status_effect/debuff/specialcd) + + var/obj/effect/temp_visual/origin_haste/V = new + target.vis_contents += V + + if(hastened) + target.balloon_alert_to_viewers("cooldowns -15s!") + to_chat(target, span_notice("I glimpse the moments ahead, and ready myself for the next move.")) + else + to_chat(target, span_notice("I glimpse the moments ahead, but there is nothing left to hasten.")) + return TRUE + +/proc/reduce_intent_cooldown(mob/living/target, effect_type, amount = 15 SECONDS) + var/datum/status_effect/S = target.has_status_effect(effect_type) + if(!S) + return FALSE + S.duration -= amount + if(S.duration <= world.time) + target.remove_status_effect(effect_type) + return TRUE + +/obj/effect/temp_visual/origin_haste + icon = 'icons/effects/effects.dmi' + icon_state = "chronofield" + duration = 10 + layer = ABOVE_MOB_LAYER + alpha = 200 + color = "#66ffcc" + +/obj/effect/temp_visual/origin_haste/Initialize(mapload) + . = ..() + transform = matrix()*3 + animate(src, transform = matrix()*0.1, alpha = 0, time = duration, easing = EASE_IN) + return INITIALIZE_HINT_NORMAL + +/obj/effect/temp_visual/origin_haste/Destroy() + if(ismob(loc)) + var/mob/M = loc + M.vis_contents -= src + return ..() diff --git a/code/modules/spells/spell_types/augment_buff/second_chance.dm b/code/modules/spells/spell_types/augment_buff/second_chance.dm new file mode 100644 index 00000000000..9c6d7d95a82 --- /dev/null +++ b/code/modules/spells/spell_types/augment_buff/second_chance.dm @@ -0,0 +1,44 @@ +/datum/action/cooldown/spell/second_chance + name = "Second Chance" + desc = "Invigorate a target with magickal resilience for 30 seconds - they shrug off critical wounds, and no longer stagger from pain." + button_icon = 'icons/mob/actions/spells/mage_augmentation.dmi' + button_icon_state = "stoneskin" + sound = 'sound/magic/charging.ogg' + + click_to_activate = TRUE + cast_range = SPELL_RANGE_GROUND + self_cast_possible = TRUE + + required_form = FORM_LIFE + required_technique = TECHNIQUE_ALTERATION + + spell_cost = 50 + + invocation = "Iterum!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_MINOR + charge_slowdown = CHARGING_SLOWDOWN_MEDIUM + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 2 MINUTES + + spell_impact_intensity = SPELL_IMPACT_HIGH + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + +/datum/action/cooldown/spell/second_chance/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + if(!isliving(cast_on)) + to_chat(H, span_warning("That is not a valid target!")) + return FALSE + var/mob/living/target = cast_on + target.apply_status_effect(/datum/status_effect/buff/second_chance) + if(target != H) + H.Beam(target, icon_state = "b_beam", time = 1.5 SECONDS) + target.visible_message(span_warning("[target] is wreathed in magickal light!"), \ + span_notice("Magickal resilience floods my body - I will not fall so easily!")) + new /obj/effect/temp_visual/spell_impact(get_turf(target), spell_color, spell_impact_intensity) + return TRUE diff --git a/code/modules/spells/spell_types/augment_buff/surge.dm b/code/modules/spells/spell_types/augment_buff/surge.dm new file mode 100644 index 00000000000..27f129362ae --- /dev/null +++ b/code/modules/spells/spell_types/augment_buff/surge.dm @@ -0,0 +1,47 @@ +/datum/action/cooldown/spell/augment_buff/surge + name = "Surge" + desc = "Flood someone's body with vigors, instantly shaking off any stun, bringing them back to their feet. Cannot be cast on yourself" + button_icon_state = "enlarge" + + check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_PHASED + + self_cast_possible = FALSE + + spell_cost = 75 + required_form = FORM_LIGHTNING + + invocation = "Resurge!" + invocation_type = INVOCATION_SHOUT + + charge_required = FALSE + other_cast_cooldown_reduction = 0 // Does not benefit from ally-cast cooldown reduction + + spell_impact_intensity = SPELL_IMPACT_MEDIUM + +/datum/action/cooldown/spell/augment_buff/surge/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/caster = owner + if(!istype(caster)) + return FALSE + if(!isliving(cast_on)) + to_chat(caster, span_warning("That is not a valid target!")) + return FALSE + var/mob/living/target = cast_on + + target.SetUnconscious(0) + target.SetSleeping(0) + target.SetParalyzed(0) + target.SetImmobilized(0) + target.SetStun(0) + target.SetKnockdown(0) + if(target.has_status_effect(/datum/status_effect/incapacitating/off_balanced)) + target.remove_status_effect(/datum/status_effect/incapacitating/off_balanced) + target.set_resting(FALSE) + target.adjust_stamina(10) + + target.balloon_alert_to_viewers("surge!") + target.visible_message(span_warning("[target] surges back up, wreathed in energy!"), span_notice("Arcyne energy floods my body - I rise!")) + if(target != caster) + to_chat(caster, span_notice("I flood [target] with arcyne vigor, hauling them back to their feet!")) + new /obj/effect/temp_visual/spell_impact(get_turf(target), spell_color, spell_impact_intensity) + return TRUE diff --git a/code/modules/spells/spell_types/cone/cone_of_cold.dm b/code/modules/spells/spell_types/cone/cone_of_cold.dm index d6c9bab1efb..6b1d82e73a8 100644 --- a/code/modules/spells/spell_types/cone/cone_of_cold.dm +++ b/code/modules/spells/spell_types/cone/cone_of_cold.dm @@ -2,7 +2,6 @@ name = "Cone of Cold" desc = "Shoots out a freezing cone in front of you." - school = SCHOOL_EVOCATION cooldown_time = 30 SECONDS cooldown_reduction_per_rank = 4 SECONDS @@ -12,10 +11,10 @@ cone_levels = 4 respect_density = TRUE delay_between_level = 0.05 SECONDS - point_cost = 3 - attunements = list( - /datum/attunement/ice = 0.3, - ) + + required_form = FORM_ICE + required_level = 2 + button_icon_state = "frostbolt" charge_time = 2 SECONDS charge_drain = 1 diff --git a/code/modules/spells/spell_types/cone/eldritch_blast.dm b/code/modules/spells/spell_types/cone/eldritch_blast.dm index 13622f5c786..6bcf4003003 100644 --- a/code/modules/spells/spell_types/cone/eldritch_blast.dm +++ b/code/modules/spells/spell_types/cone/eldritch_blast.dm @@ -75,10 +75,7 @@ button_icon_state = "eldritch_blast" sound = 'sound/magic/whiteflame.ogg' - point_cost = 1 - attunements = list( - /datum/attunement/dark = 0.3, - ) + required_form = FORM_DEATH invocation = "Eldritch blast!" invocation_type = INVOCATION_SHOUT diff --git a/code/modules/spells/spell_types/cone/void_cone.dm b/code/modules/spells/spell_types/cone/void_cone.dm index 1ba0f838bb8..1af0886794d 100644 --- a/code/modules/spells/spell_types/cone/void_cone.dm +++ b/code/modules/spells/spell_types/cone/void_cone.dm @@ -132,7 +132,8 @@ overlay_icon_state = "bg_heretic_border" button_icon_state = "icebeam" - school = SCHOOL_FORBIDDEN + required_form = NONE + cooldown_time = 30 SECONDS invocation = "FR'ZE!" diff --git a/code/modules/spells/spell_types/conjure_summon/_base.dm b/code/modules/spells/spell_types/conjure_summon/_base.dm new file mode 100644 index 00000000000..0e243bed81c --- /dev/null +++ b/code/modules/spells/spell_types/conjure_summon/_base.dm @@ -0,0 +1,206 @@ + +/datum/action/cooldown/spell/conjure_summon + button_icon = 'icons/mob/actions/spells/mage_conjure.dmi' + sound = 'sound/magic/magnet.ogg' + + click_to_activate = TRUE + cast_range = 2 + + spell_cost = 40 + + required_technique = TECHNIQUE_SUMMONING + + invocation_type = INVOCATION_SHOUT + invocation = "FIX ME!" + + charge_required = TRUE + charge_time = CHARGETIME_HEAVY + charge_slowdown = CHARGING_SLOWDOWN_HEAVY + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 2 MINUTES + + spell_impact_intensity = SPELL_IMPACT_NONE + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/max_summons = 1 + var/summons_per_cast = 1 + var/list/conjured_mobs = list() + var/current_mode = 1 + var/list/modes = list() + var/summon_noun = "servant" + var/recoil_energy_floor = 200 + var/recoil_severity = CONJURE_RECOIL_FULL + var/recoil_stamina_only = FALSE + var/static/list/command_word_types = list( + /datum/action/cooldown/spell/command_word/fray, + /datum/action/cooldown/spell/command_word/harry, + /datum/action/cooldown/spell/command_word/quicken, + /datum/action/cooldown/spell/command_word/beckon, + ) + +/datum/action/cooldown/spell/conjure_summon/Remove(mob/remove_from) + check_remove_command_words(remove_from) + return ..() + +/datum/action/cooldown/spell/conjure_summon/Grant(mob/grant_to) + . = ..() + apply_mode() + grant_command_words(grant_to) + +/datum/action/cooldown/spell/conjure_summon/proc/grant_command_words(mob/living/user) + if(!istype(user)) + return + for(var/path in command_word_types) + if(has_command_word(user, path)) + continue + var/datum/action/cooldown/spell/command_word/new_word = new path() + new_word.Grant(user) + +/datum/action/cooldown/spell/conjure_summon/proc/has_command_word(mob/living/user, path) + for(var/datum/action/cooldown/spell/command_word/existing in user.actions) + if(existing.type == path) + return TRUE + return FALSE + +/datum/action/cooldown/spell/conjure_summon/proc/check_remove_command_words(mob/living/user) + if(!istype(user)) + return + // bail if another conjure_summon spell is still granted + for(var/datum/action/cooldown/spell/conjure_summon/other in user.actions) + if(other == src) + continue + return + // none left - strip the command words too + for(var/datum/action/cooldown/spell/command_word/cw in user.actions) + if(!(cw.type in command_word_types)) + continue + cw.Remove(user) + +/datum/action/cooldown/spell/conjure_summon/Destroy() + for(var/mob/living/M in conjured_mobs.Copy()) + if(!QDELETED(M)) + qdel(M) + conjured_mobs.Cut() + return ..() + +/datum/action/cooldown/spell/conjure_summon/toggle_alt_mode(mob/user) + if(length(modes) < 2) + return + var/next = current_mode + for(var/i in 1 to length(modes)) + next = (next % length(modes)) + 1 + if(mode_available(next, user)) + break + current_mode = next + apply_mode() + to_chat(user, span_notice("[name]: [modes[current_mode]["name"]].")) + return TRUE + +/datum/action/cooldown/spell/conjure_summon/proc/mode_available(index, mob/user) + var/req = modes[index]["tier_req"] + if(!req) + return TRUE + return get_summon_tier(user) >= req + +/datum/action/cooldown/spell/conjure_summon/proc/apply_mode() + if(!length(modes)) + return + var/list/mode = modes[current_mode] + if(mode["invocation"]) + invocation = mode["invocation"] + update_mode_maptext() + +/datum/action/cooldown/spell/conjure_summon/proc/update_mode_maptext() + if(!length(modes)) + return + var/list/mode = modes[current_mode] + for(var/datum/hud/hud as anything in viewers) + var/atom/movable/screen/movable/action_button/B = viewers[hud] + var/atom/movable/screen/arc_maptext_holder/holder + for(var/atom/movable/screen/arc_maptext_holder/existing in B.vis_contents) + holder = existing + break + if(!holder) + holder = new(B) + B.vis_contents.Add(holder) + holder.maptext = MAPTEXT(mode["tag"]) + holder.maptext_x = 5 + holder.color = mode["color"] + +/datum/action/cooldown/spell/conjure_summon/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return + if(HAS_TRAIT(owner, TRAIT_CONJURE_BACKLASH)) + if(feedback) + owner.balloon_alert(owner, "The backlash still grips me!") + return FALSE + +/datum/action/cooldown/spell/conjure_summon/cast(atom/cast_on) + . = ..() + var/mob/living/user = owner + if(!istype(user)) + return FALSE + + if(!validate_cast_location(cast_on, user)) + return FALSE + + var/at_capacity = (length(conjured_mobs) >= max_summons) + if(at_capacity) + dismiss_summons(conjured_mobs.Copy()) + var/to_spawn = at_capacity ? summons_per_cast : min(summons_per_cast, max_summons - length(conjured_mobs)) + if(to_spawn < 1) + to_spawn = 1 + + var/list/all_summoned = list() + for(var/i in 1 to to_spawn) + var/turf/spawn_turf = get_summon_turf(user, cast_on, i) + var/mob/living/summoned = spawn_summon(spawn_turf, user) + if(summoned) + all_summoned += summoned + if(!length(all_summoned)) + return FALSE + for(var/mob/living/summoned in all_summoned) + conjured_mobs += summoned + RegisterSignal(summoned, COMSIG_QDELETING, PROC_REF(remove_conjure)) + summoned.AddComponent(/datum/component/conjured_minion, user, recoil_energy_floor, recoil_severity, recoil_stamina_only) + var/turf/landing = get_turf(summoned) + landing?.zFall(summoned) + on_summon_complete(user, cast_on, all_summoned) + return TRUE + +/// Checked before spawning starts. Override to skip/replace the default "cast_on must be open" check. +/datum/action/cooldown/spell/conjure_summon/proc/validate_cast_location(atom/cast_on, mob/living/user) + var/turf/T = get_turf(cast_on) + if(!isopenturf(T) || T.is_blocked_turf()) + to_chat(user, span_warning("The targeted location is blocked. My summon fails to come forth.")) + return FALSE + return TRUE + +/datum/action/cooldown/spell/conjure_summon/proc/spawn_summon(turf/T, mob/living/user) + return TRUE + +/// Where the Nth summon of this cast lands. Default: everyone piles on cast_on's turf. +/datum/action/cooldown/spell/conjure_summon/proc/get_summon_turf(mob/living/user, atom/cast_on, index) + return get_turf(cast_on) + +/// Fires once, after all mobs for this cast have been created and registered. Good spot for AI targeting etc. +/datum/action/cooldown/spell/conjure_summon/proc/on_summon_complete(mob/living/user, atom/cast_on, list/summoned_mobs) + return + +/datum/action/cooldown/spell/conjure_summon/proc/get_summon_tier(mob/living/user) + var/lvl = GET_MOB_SKILL_VALUE(user, /datum/attribute/skill/magic/arcane) + if(lvl >= SKILL_LEVEL_MASTER) + return 3 + if(lvl >= SKILL_LEVEL_EXPERT) + return 2 + return 1 + +/datum/action/cooldown/spell/conjure_summon/proc/dismiss_summons(list/mobs) + for(var/mob/living/M in mobs) + dismiss_conjured_minion(M) + +/datum/action/cooldown/spell/conjure_summon/proc/remove_conjure(mob/living/summoned) + SIGNAL_HANDLER + conjured_mobs -= summoned diff --git a/code/modules/spells/spell_types/conjure_summon/find_familiar.dm b/code/modules/spells/spell_types/conjure_summon/find_familiar.dm new file mode 100644 index 00000000000..059a0cc2226 --- /dev/null +++ b/code/modules/spells/spell_types/conjure_summon/find_familiar.dm @@ -0,0 +1,42 @@ +/datum/action/cooldown/spell/conjure_summon/familiar + name = "Find Familiar" + desc = "Summons a temporary spectral wolf to aid you." + button_icon = 'icons/mob/actions/roguespells.dmi' + button_icon_state = "zad" + sound = 'sound/magic/whiteflame.ogg' + + invocation = "B'ST FR'ND!" + invocation_type = INVOCATION_SHOUT + + charge_time = 2 SECONDS + charge_drain = 1 + charge_slowdown = 1.3 + cooldown_time = 6 MINUTES + spell_cost = 30 + spell_flags = SPELL_RITUOS + + required_form = FORM_ARCANE + required_technique = TECHNIQUE_SUMMONING + + self_cast_possible = FALSE + + summon_noun = "wolf" + max_summons = 1 + summons_per_cast = 1 + + var/summon_lifespan = 5 MINUTES + +/datum/action/cooldown/spell/conjure_summon/familiar/spawn_summon(turf/T, mob/living/user) + var/mob/living/simple_animal/hostile/retaliate/wolf/familiar/wolf = new(T) + + wolf.befriend(user) + if(summon_lifespan) + QDEL_IN(wolf, summon_lifespan) + + for(var/mob/living/L in T) + if(L.stat == DEAD) + continue + wolf.ai_controller?.set_blackboard_key(BB_BASIC_MOB_PRIORITY_TARGETS, L) + break + + return wolf diff --git a/code/modules/spells/spell_types/conjure_summon/primordials.dm b/code/modules/spells/spell_types/conjure_summon/primordials.dm new file mode 100644 index 00000000000..67ab88e5971 --- /dev/null +++ b/code/modules/spells/spell_types/conjure_summon/primordials.dm @@ -0,0 +1,498 @@ +/datum/action/cooldown/spell/conjure_summon/primordial + name = "Conjure Flame Primordial" + desc = "Conjure a Primordial to fight at your side. Toggle its element with Shift+G while the spell is selected: Flame, Water, or Air. \ + It grows mightier with your skill at Arcyne Armament - upgrading at Expert, and further at Master.\ + An elemental of moderate health and damage, able to breath out fire in a cone in front of them." + button_icon_state = "primetriangle" + invocation = "Exsurge, ignis!" + summon_noun = "primordial" + recoil_energy_floor = 150 + required_form = FORM_FIRE + var/mob_path = /mob/living/simple_animal/hostile/retaliate/primordial/fire + +/datum/action/cooldown/spell/conjure_summon/primordial/air + name = "Conjure Air Primordial" + desc = "Conjure a Primordial to fight at your side. Toggle its element with Shift+G while the spell is selected: Flame, Water, or Air. \ + It grows mightier with your skill at Arcyne Armament - upgrading at Expert, and further at Master.\ + A fast elemental with the highest melee damage, lowest health able to throw targets back" + required_form = FORM_AIR + mob_path = /mob/living/simple_animal/hostile/retaliate/primordial/air + +/datum/action/cooldown/spell/conjure_summon/primordial/water + name = "Conjure Water Primordial" + desc = "Conjure a Primordial to fight at your side. Toggle its element with Shift+G while the spell is selected: Flame, Water, or Air. \ + It grows mightier with your skill at Arcyne Armament - upgrading at Expert, and further at Master.\ + A slow elemental with high health able to create a whirlpool around themselves" + required_form = FORM_WATER + mob_path = /mob/living/simple_animal/hostile/retaliate/primordial/water + +/datum/action/cooldown/spell/conjure_summon/primordial/spawn_summon(turf/T, mob/living/user) + var/mob/living/simple_animal/hostile/retaliate/primordial/conjured = new mob_path(T, user) + scale_primordial(conjured, user) + return conjured + +/datum/action/cooldown/spell/conjure_summon/primordial/proc/scale_primordial(mob/living/simple_animal/hostile/retaliate/primordial/P, mob/living/user) + var/tier = get_summon_tier(user) + var/mult = 1 + (tier - 1) * 0.2 + P.maxHealth = round(P.maxHealth * mult) + P.health = P.maxHealth + P.melee_damage_lower = round(P.melee_damage_lower * mult) + P.melee_damage_upper = round(P.melee_damage_upper * mult) + +#define CONJURE_TAUNT_TELEGRAPH (1.5 SECONDS) +#define CONJURE_OVERLOAD_WINDUP (3.5 SECONDS) + +/obj/effect/temp_visual/conjure_taunt + icon = 'icons/effects/effects.dmi' + icon_state = "trap" + layer = BELOW_MOB_LAYER + duration = CONJURE_TAUNT_TELEGRAPH + +/datum/action/cooldown/spell/command_word + button_icon = 'icons/mob/actions/spells/mage_conjure.dmi' + sound = null + + click_to_activate = TRUE + cast_range = 12 + self_cast_possible = TRUE + + spell_cost = 0 + + charge_required = FALSE + cooldown_time = 1 SECONDS + + spell_impact_intensity = SPELL_IMPACT_NONE + + spell_requirements = SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/current_mode = 1 + var/list/modes = list() + var/command_range = 12 + var/focusing = FALSE + +/datum/action/cooldown/spell/command_word/Grant(mob/grant_to) + . = ..() + apply_mode(current_mode) + +/datum/action/cooldown/spell/command_word/proc/apply_mode(index) + if(!length(modes)) + return + current_mode = index + var/list/mode = modes[index] + invocation = mode["invocation"] + cooldown_time = mode["cooldown"] + build_all_button_icons() + update_mode_maptext() + +/datum/action/cooldown/spell/command_word/toggle_alt_mode(mob/user) + if(length(modes) < 2) + return FALSE + apply_mode((current_mode % length(modes)) + 1) + if(user) + user.balloon_alert(user, modes[current_mode]["name"]) + return TRUE + +/datum/action/cooldown/spell/command_word/proc/update_mode_maptext() + if(!length(modes)) + return + var/list/mode = modes[current_mode] + for(var/datum/hud/hud as anything in viewers) + var/atom/movable/screen/movable/action_button/B = viewers[hud] + var/atom/movable/screen/arc_maptext_holder/holder + for(var/atom/movable/screen/arc_maptext_holder/existing in B.vis_contents) + holder = existing + break + if(!holder) + holder = new(B) + B.vis_contents.Add(holder) + holder.maptext = MAPTEXT(mode["tag"]) + holder.maptext_x = 5 + holder.color = mode["color"] + +/datum/action/cooldown/spell/command_word/cast(atom/cast_on) + . = ..() + return fire_command(cast_on) + +/datum/action/cooldown/spell/command_word/proc/get_summons_in_range() + var/mob/living/user = owner + var/list/found = list() + if(!istype(user)) + return found + for(var/mob/living/M in user.summoned_minions) + if(QDELETED(M) || M.stat == DEAD || M == user) + continue + if(get_dist(user, M) > command_range) + continue + found += M + return found + +/datum/action/cooldown/spell/command_word/proc/find_nearest_enemy(mob/living/summon) + var/mob/living/nearest + var/nearest_dist = INFINITY + for(var/mob/living/L in oview(9, summon)) + if(QDELETED(L) || L.stat == DEAD || L == summon) + continue + if(summon.faction_check_atom(L)) + continue + var/d = get_dist(summon, L) + if(d < nearest_dist) + nearest_dist = d + nearest = L + return nearest + +/datum/action/cooldown/spell/command_word/proc/fire_command(atom/cast_on) + var/mob/living/user = owner + if(!istype(user)) + return FALSE + var/list/summons = get_summons_in_range() + if(!length(summons)) + to_chat(user, span_warning("I have no conjured servants at hand to command.")) + return FALSE + + var/atom/aim + if(isliving(cast_on) && !(cast_on in summons) && cast_on != user) + aim = cast_on + else if(isturf(cast_on)) + aim = cast_on + + var/key = modes[current_mode]["key"] + if(key == "focus") + return do_focus(summons) + if(key == "taunt") + return do_taunt(summons, get_turf(cast_on)) + if(key == "overload") + var/mob/living/bomb = pick_overload_summon(summons, cast_on) + if(!bomb) + return FALSE + to_chat(user, span_userdanger("[bomb] surges with unstable arcyne power - it will overload!")) + return summon_overload(bomb) + + var/count = 0 + var/balloon = "[lowertext(modes[current_mode]["name"])]!" + for(var/mob/living/summon in summons) + if(command_summon(summon, key, aim)) + summon.balloon_alert_to_viewers(balloon) + count++ + + if(!count) + to_chat(user, span_warning("None of my servants can answer that command right now.")) + return FALSE + return TRUE + +/datum/action/cooldown/spell/command_word/proc/command_summon(mob/living/summon, key, atom/aim) + switch(key) + if("special") + return summon_special(summon, aim) + if("feint") + return summon_feint(summon, aim) + if("kick") + return summon_kick(summon, aim) + if("surge", "bloodrush", "empower") + return empower_summon(summon, key, aim) + return FALSE + +/datum/action/cooldown/spell/command_word/proc/is_primordial(mob/living/summon) + return istype(summon, /mob/living/simple_animal/hostile/retaliate/primordial) + +/datum/action/cooldown/spell/command_word/proc/primordial_ward(mob/living/simple_animal/hostile/retaliate/primordial/P) + P.defprob = min(initial(P.defprob) + 40, 95) + addtimer(CALLBACK(src, PROC_REF(end_primordial_ward), P), 5 SECONDS) + return TRUE + +/datum/action/cooldown/spell/command_word/proc/end_primordial_ward(mob/living/simple_animal/hostile/retaliate/primordial/P) + if(QDELETED(P)) + return + P.defprob = initial(P.defprob) + +/datum/action/cooldown/spell/command_word/proc/primordial_shove(mob/living/simple_animal/hostile/retaliate/primordial/P, mob/living/target) + var/shove_dir = get_dir(P, target) + var/turf/dest = get_ranged_target_turf(target, shove_dir, 2) + if(dest) + target.throw_at(dest, 2, 1, P) + return TRUE + +/datum/action/cooldown/spell/command_word/proc/primordial_heal(mob/living/simple_animal/hostile/retaliate/primordial/P) + if(world.time < P.next_heal_time) + return FALSE + P.next_heal_time = world.time + 15 SECONDS + P.adjustHealth(-round(P.maxHealth * 0.25)) + return TRUE + +/datum/action/cooldown/spell/command_word/proc/primordial_overcharge(mob/living/simple_animal/hostile/retaliate/primordial/P) + P.next_ability_use = 0 + return TRUE + +/datum/action/cooldown/spell/command_word/proc/summon_special(mob/living/summon, atom/aim) + var/atom/target = aim + if(!target && summon.ai_controller) + target = summon.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target) + target = find_nearest_enemy(summon) + if(!target) + return FALSE + + if(istype(summon, /mob/living/simple_animal/hostile/retaliate/primordial)) + var/mob/living/simple_animal/hostile/retaliate/primordial/P = summon + if(world.time < P.next_ability_use) + return FALSE + P.face_atom(target) + P.ability(get_turf(target), P) + P.next_ability_use = world.time + P.ability_cooldown + return TRUE + + if(summon.has_status_effect(/datum/status_effect/debuff/specialcd)) + return FALSE + var/obj/item/weapon/W = summon.get_active_held_item() + if(!istype(W) || !W.weapon_special) + return FALSE + summon.face_atom(target) + if(!W.weapon_special.apply_cost(summon)) + return FALSE + W.weapon_special.deploy(summon, W, target) + return TRUE + +/datum/action/cooldown/spell/command_word/proc/summon_feint(mob/living/summon, atom/aim) + var/atom/target = aim + if(!target && summon.ai_controller) + target = summon.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target) + target = find_nearest_enemy(summon) + if(!isliving(target)) + return FALSE + summon.face_atom(target) + var/datum/rmb_intent/feint/F = new() + F.special_attack(summon, target) + return TRUE + +/datum/action/cooldown/spell/command_word/proc/summon_kick(mob/living/summon, atom/aim) + var/atom/target = aim + if(!target && summon.ai_controller) + target = summon.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target) + target = find_nearest_enemy(summon) + if(!isliving(target) || !summon.Adjacent(target)) + return FALSE + summon.face_atom(target) + if(is_primordial(summon)) + return primordial_shove(summon, target) + INVOKE_ASYNC(src, PROC_REF(do_kick), summon, target) + return TRUE + +/datum/action/cooldown/spell/command_word/proc/do_kick(mob/living/summon, mob/living/target) + if(QDELETED(summon) || QDELETED(target)) + return + var/old_mmb = summon.mmb_intent + summon.mmb_intent = new INTENT_KICK(summon) + target.onkick(summon) + QDEL_NULL(summon.mmb_intent) + summon.mmb_intent = old_mmb + +/datum/action/cooldown/spell/command_word/proc/do_focus(list/summons) + focusing = !focusing + var/mob/living/user = owner + var/zone = user.zone_selected + var/count = 0 + for(var/mob/living/summon in summons) + if(!summon.ai_controller) + continue + if(focusing) + summon.ai_controller.set_blackboard_key(BB_FORCED_ATTACK_ZONE, zone) + summon.balloon_alert_to_viewers("focus!") + else + summon.ai_controller.clear_blackboard_key(BB_FORCED_ATTACK_ZONE) + count++ + if(!count) + return TRUE + if(focusing) + to_chat(user, span_notice("My servants lock onto where I aim - the [parse_zone(zone)].")) + else + to_chat(user, span_notice("My servants return to striking where they see fit.")) + return TRUE + +/datum/action/cooldown/spell/command_word/proc/overload_scale(mob/living/summon) + if(istype(summon, /mob/living/simple_animal/hostile/retaliate/primordial)) + return 0.5 + return 1 + +/datum/action/cooldown/spell/command_word/proc/pick_overload_summon(list/summons, atom/cast_on) + if(isliving(cast_on) && (cast_on in summons)) + return cast_on + var/turf/ref = get_turf(cast_on) + if(!ref) + ref = get_turf(owner) + var/mob/living/best + var/best_dist = INFINITY + for(var/mob/living/S in summons) + var/d = get_dist(S, ref) + if(d < best_dist) + best_dist = d + best = S + return best + +/datum/action/cooldown/spell/command_word/proc/summon_overload(mob/living/summon) + summon.do_jitter_animation(1000) + summon.Slowdown(3) + summon.balloon_alert_to_viewers("detonating! (-4 spd)") + addtimer(CALLBACK(src, PROC_REF(do_overload), summon, overload_scale(summon)), CONJURE_OVERLOAD_WINDUP) + return TRUE + +/datum/action/cooldown/spell/command_word/proc/do_overload(mob/living/summon, scale) + if(QDELETED(summon) || summon.stat == DEAD) + return + var/turf/epicenter = get_turf(summon) + if(!epicenter) + return + var/mob/living/carbon/human/caster = owner + if(!istype(caster)) + caster = null + var/zone = summon.zone_selected || BODY_ZONE_CHEST + var/damage = round(120 * scale) + var/curtain_life = (scale >= 1) ? 8 SECONDS : 3 SECONDS + new /obj/effect/temp_visual/explosion(epicenter) + new /obj/effect/temp_visual/fire_pillar(epicenter) + playsound(epicenter, pick('sound/misc/explode/explosionclose (1).ogg', 'sound/misc/explode/explosionclose (2).ogg', 'sound/misc/explode/explosionclose (3).ogg'), 100, TRUE) + for(var/turf/T in range(1, epicenter)) + new /obj/effect/temp_visual/dragonfire(T) + new /obj/effect/curtain_fire(T, curtain_life, caster) + for(var/mob/living/victim in T) + if(victim == summon || victim == caster || victim.stat == DEAD) + continue + if(summon.faction_check_atom(victim)) + continue + if(caster && !QDELETED(caster)) + arcyne_strike(caster, victim, null, damage, zone, BCLASS_BURN, spell_name = "Overloaded", damage_type = BURN, skip_animation = TRUE) + else + victim.adjustFireLoss(damage) + apply_scorch_stack(victim, 2, zone) + victim.apply_status_effect(/datum/status_effect/debuff/exposed, 4 SECONDS) + summon.death() + +/datum/action/cooldown/spell/command_word/proc/empower_summon(mob/living/summon, key, atom/aim) + if(is_primordial(summon)) + switch(key) + if("surge", "bloodrush") + return primordial_heal(summon) + if("empower") + return primordial_overcharge(summon) + return FALSE + switch(key) + if("surge") + return do_surge(summon) + if("bloodrush") + summon.apply_status_effect(/datum/status_effect/buff/adrenaline_rush) + return TRUE + if("empower") + if(summon.has_status_effect(/datum/status_effect/buff/empowered_strike)) + return FALSE + summon.apply_status_effect(/datum/status_effect/buff/empowered_strike, 10 SECONDS) + return TRUE + return FALSE + +/datum/action/cooldown/spell/command_word/proc/do_surge(mob/living/summon) + summon.SetUnconscious(0) + summon.SetSleeping(0) + summon.SetParalyzed(0) + summon.SetImmobilized(0) + summon.SetStun(0) + summon.SetKnockdown(0) + if(summon.has_status_effect(/datum/status_effect/incapacitating/off_balanced)) + summon.remove_status_effect(/datum/status_effect/incapacitating/off_balanced) + summon.set_resting(FALSE) + summon.adjust_stamina(10) + return TRUE + +/datum/action/cooldown/spell/command_word/proc/do_taunt(list/summons, turf/dest) + if(!isturf(dest) || !length(summons)) + return FALSE + new /obj/effect/temp_visual/conjure_taunt(dest) + for(var/mob/living/summon in summons) + if(QDELETED(summon) || summon.stat == DEAD) + continue + summon.Beam(dest, "purple_lightning", time = CONJURE_TAUNT_TELEGRAPH) + playsound(dest, 'sound/magic/charging.ogg', 60, TRUE) + addtimer(CALLBACK(src, PROC_REF(finish_taunt), summons.Copy(), dest), CONJURE_TAUNT_TELEGRAPH) + return TRUE + +/datum/action/cooldown/spell/command_word/proc/finish_taunt(list/summons, turf/dest) + if(!isturf(dest)) + return + new /obj/effect/temp_visual/blink(dest) + playsound(dest, 'sound/magic/blink.ogg', 60, TRUE) + for(var/mob/living/summon in summons) + if(QDELETED(summon) || summon.stat == DEAD) + continue + if(!do_teleport(summon, dest, precision = 2, channel = TELEPORT_CHANNEL_MAGIC, forced = TRUE)) + var/turf/landing = get_teleport_turf(dest, 2) + if(landing) + summon.forceMove(landing) + summon.balloon_alert_to_viewers("taunt!") + summon.emote("warcry") + if(summon.ai_controller) + var/mob/living/foe = find_nearest_enemy(summon) + if(foe) + summon.ai_controller.set_blackboard_key(BB_CURRENT_PET_TARGET, foe) + summon.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, foe) + if(isanimal(summon)) + var/mob/living/simple_animal/SA = summon + SA.pet_passive = FALSE + for(var/mob/living/enemy in oview(5, summon)) + if(QDELETED(enemy) || enemy.stat == DEAD || enemy == summon) + continue + if(summon.faction_check_atom(enemy)) + continue + var/datum/component/ai_aggro_system/A = enemy.GetComponent(/datum/component/ai_aggro_system) + if(A) + A.add_threat_to_mob(summon, 100) + +/datum/action/cooldown/spell/command_word/fray + name = "Fray" + desc = "Battle order. Order your summon to unleash a Special, or a primordial's special attack." + button_icon_state = "order_servants" + invocation_type = INVOCATION_SHOUT + invocation = "Impetum!" + cooldown_time = 1 SECONDS + modes = list( + list("name" = "Special", "tag" = "SPC", "key" = "special", "color" = LIGHT_COLOR_FIRE, "invocation" = "Impetum!", "cooldown" = 1 SECONDS, "desc" = ""), + ) + +/datum/action/cooldown/spell/command_word/harry + name = "Harry" + desc = "Order your summons to Feint or Kick your enemies. Toggle with Shift+G. " + button_icon_state = "aetherknife" + invocation_type = INVOCATION_SHOUT + invocation = "Fallere!" + cooldown_time = 6 SECONDS + modes = list( + list("name" = "Feint", "tag" = "FNT", "key" = "feint", "color" = "#c9a0ff", "invocation" = "Fallere!", "cooldown" = 6 SECONDS, "desc" = ""), + list("name" = "Kick", "tag" = "KCK", "key" = "kick", "color" = "#e0a020", "invocation" = "Calcitra!", "cooldown" = 6 SECONDS, "desc" = ""), + ) + +/datum/action/cooldown/spell/command_word/quicken + name = "Quicken" + desc = "Powerful abilities to quicken your summons. Empower let their next strike bypass Guard, and reset a Primordial's ability cooldown. Surge removes stun and Blood Rush floods it with vigor and blood. Toggle with Shift+G." + button_icon_state = "conjure_aegis" + invocation_type = INVOCATION_SHOUT + invocation = "Vera Manus!" + cooldown_time = 25 SECONDS + spell_cost = 10 + modes = list( + list("name" = "Empower", "tag" = "EMP", "key" = "empower", "color" = "#d13b2e", "invocation" = "Vera Manus!", "cooldown" = 30 SECONDS, "desc" = ""), + list("name" = "Surge", "tag" = "SRG", "key" = "surge", "color" = "#d13b2e", "invocation" = "Resurge!", "cooldown" = 30 SECONDS, "desc" = ""), + list("name" = "Blood Rush", "tag" = "RSH", "key" = "bloodrush", "color" = "#d13b2e", "invocation" = "Concita!", "cooldown" = 30 SECONDS, "desc" = ""), + ) + +/datum/action/cooldown/spell/command_word/beckon + name = "Beckon" + desc = "Taunt teleport your servants to a marked spot and attract their aggression, Overload makes one explode with arcyne energy, Focus sets them to strike the zone you are aiming at, it does not guarantee they'll hit." + button_icon_state = "primetriangle" + invocation_type = INVOCATION_SHOUT + invocation = "Provoco!" + cooldown_time = 30 SECONDS + modes = list( + list("name" = "Taunt", "tag" = "TNT", "key" = "taunt", "color" = "#e0a020", "invocation" = "Provoco!", "cooldown" = 30 SECONDS, "desc" = ""), + list("name" = "Overloaded", "tag" = "OVL", "key" = "overload", "color" = LIGHT_COLOR_FIRE, "invocation" = "Displode!", "cooldown" = 0, "desc" = ""), + list("name" = "Focus", "tag" = "FCS", "key" = "focus", "color" = "#66ff66", "invocation" = "Coniunge!", "cooldown" = 0, "desc" = ""), + ) + +#undef CONJURE_TAUNT_TELEGRAPH +#undef CONJURE_OVERLOAD_WINDUP diff --git a/code/modules/spells/spell_types/conjure_summon/raise_lesser_undead.dm b/code/modules/spells/spell_types/conjure_summon/raise_lesser_undead.dm new file mode 100644 index 00000000000..f3e2193a240 --- /dev/null +++ b/code/modules/spells/spell_types/conjure_summon/raise_lesser_undead.dm @@ -0,0 +1,75 @@ +/datum/action/cooldown/spell/conjure_summon/raise_lesser_undead + name = "Raise Lesser undead" + desc = "Summons a temporary skeleton to aid you." + button_icon_state = "animate" + button_icon = 'icons/mob/actions/roguespells.dmi' + sound = 'sound/magic/magnet.ogg' + + invocation = "SERVE ME!" + invocation_type = INVOCATION_SHOUT + + charge_time = 2 SECONDS + charge_drain = 1 + charge_slowdown = 1.3 + cooldown_time = 6 MINUTES + spell_cost = 70 + + spell_type = SPELL_MIRACLE + antimagic_flags = MAGIC_RESISTANCE_HOLY + associated_skill = /datum/attribute/skill/magic/holy + + self_cast_possible = FALSE + + summon_noun = "skeleton" + max_summons = 1 + summons_per_cast = 1 + + var/summon_lifespan = 5 MINUTES + var/cabal_affine = FALSE + +/datum/action/cooldown/spell/conjure_summon/raise_lesser_undead/spawn_summon(turf/T, mob/living/user) + var/skeleton_type = pick_skeleton_type() + var/mob/living/simple_animal/hostile/skeleton/skeleton = new skeleton_type(T) + + skeleton.befriend(user) + if(cabal_affine) + skeleton.add_faction(FACTION_CABAL) + if(summon_lifespan) + QDEL_IN(skeleton, summon_lifespan) + + assign_priority_target(skeleton, T) + return skeleton + +/datum/action/cooldown/spell/conjure_summon/raise_lesser_undead/proc/pick_skeleton_type() + var/skeleton_roll = rand(1, 100) + switch(skeleton_roll) + if(1 to 20) + return /mob/living/simple_animal/hostile/skeleton/axe + if(21 to 40) + return /mob/living/simple_animal/hostile/skeleton/spear + if(41 to 60) + return /mob/living/simple_animal/hostile/skeleton/guard + if(61 to 80) + return /mob/living/simple_animal/hostile/skeleton/bow + return /mob/living/simple_animal/hostile/skeleton + +/datum/action/cooldown/spell/conjure_summon/raise_lesser_undead/proc/assign_priority_target(mob/living/skeleton, turf/T) + for(var/mob/living/L in T) + if(L.stat == DEAD) + continue + skeleton.ai_controller?.set_blackboard_key(BB_BASIC_MOB_PRIORITY_TARGETS, L) + break + +/datum/action/cooldown/spell/conjure_summon/raise_lesser_undead/necromancer + name = "Lesser Necromancy" + desc = "Summons a temporary spectral skeleton to aid you." + spell_type = SPELL_MANA + antimagic_flags = MAGIC_RESISTANCE + associated_skill = /datum/attribute/skill/magic/arcane + cabal_affine = TRUE + cooldown_time = 30 SECONDS + summon_lifespan = 1 MINUTES + + required_form = FORM_DEATH + required_technique = TECHNIQUE_SUMMONING + required_level = 2 diff --git a/code/modules/spells/spell_types/conjure_summon/vengeful_spirits.dm b/code/modules/spells/spell_types/conjure_summon/vengeful_spirits.dm new file mode 100644 index 00000000000..778ca63ee49 --- /dev/null +++ b/code/modules/spells/spell_types/conjure_summon/vengeful_spirits.dm @@ -0,0 +1,58 @@ +/datum/action/cooldown/spell/conjure_summon/vengeful_spirits + name = "Avenging Spirits" + desc = "Summon temporary rancorous spirits to tear at an opponent!" + button_icon = 'icons/mob/actions/spells/necramiracles.dmi' + button_icon_state = "vengeful_spirit" + sound = 'sound/magic/magnet.ogg' + + click_to_activate = TRUE + cast_range = 7 + + spell_cost = 50 + required_form = FORM_DEATH + + invocation = "Awaken, rancor!!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_HEAVY + charge_slowdown = CHARGING_SLOWDOWN_HEAVY + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 90 SECONDS + + spell_impact_intensity = SPELL_IMPACT_NONE + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + max_summons = 3 + summons_per_cast = 3 + summon_noun = "spirit" + recoil_energy_floor = 200 + recoil_severity = CONJURE_RECOIL_LIGHT + +/datum/action/cooldown/spell/conjure_summon/vengeful_spirits/cast(atom/cast_on) + if(!isliving(cast_on)) + to_chat(owner, span_warning("There is nothing here worth turning my spirits' rancor upon.")) + return FALSE + return ..() + +// Spirits erupt around the caster, not on the target's tile +/datum/action/cooldown/spell/conjure_summon/vengeful_spirits/validate_cast_location(atom/cast_on, mob/living/user) + return TRUE + +/datum/action/cooldown/spell/conjure_summon/vengeful_spirits/get_summon_turf(mob/living/user, atom/cast_on, index) + var/perpendicular = (user.dir == SOUTH || user.dir == NORTH) ? EAST : NORTH + var/opposite = (user.dir == SOUTH || user.dir == NORTH) ? WEST : SOUTH + switch(index) + if(2) + return get_step(user, perpendicular) + if(3) + return get_step(user, opposite) + return get_turf(user) + +/datum/action/cooldown/spell/conjure_summon/vengeful_spirits/spawn_summon(turf/T, mob/living/user) + return new /mob/living/simple_animal/hostile/spirit_vengeance(T, user) + +/datum/action/cooldown/spell/conjure_summon/vengeful_spirits/on_summon_complete(mob/living/user, atom/cast_on, list/summoned_mobs) + var/mob/living/target = cast_on + for(var/mob/living/simple_animal/hostile/spirit_vengeance/spirit in summoned_mobs) + spirit.ai_controller?.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, target) diff --git a/code/modules/spells/spell_types/pointed/aoe/howl.dm b/code/modules/spells/spell_types/pointed/aoe/howl.dm index b84c54d7428..59cff17df1f 100644 --- a/code/modules/spells/spell_types/pointed/aoe/howl.dm +++ b/code/modules/spells/spell_types/pointed/aoe/howl.dm @@ -8,6 +8,8 @@ spell_type = SPELL_RAGE associated_skill = null + required_form = null + invocation_type = INVOCATION_NONE aoe_radius = 5 diff --git a/code/modules/spells/spell_types/pointed/aoe/knock.dm b/code/modules/spells/spell_types/pointed/aoe/knock.dm index 8a7705701e2..50c6e4e6c1a 100644 --- a/code/modules/spells/spell_types/pointed/aoe/knock.dm +++ b/code/modules/spells/spell_types/pointed/aoe/knock.dm @@ -5,11 +5,8 @@ button_icon_state = "knock" click_to_activate = FALSE - attunements = list( - /datum/attunement/aeromancy = 0.2, - ) + required_form = FORM_AIR - school = SCHOOL_TRANSMUTATION charge_time = 2 SECONDS cooldown_time = 20 SECONDS diff --git a/code/modules/spells/spell_types/pointed/aoe/lightning_lure.dm b/code/modules/spells/spell_types/pointed/aoe/lightning_lure.dm index 9e38a0cf7c6..5c90128b3e9 100644 --- a/code/modules/spells/spell_types/pointed/aoe/lightning_lure.dm +++ b/code/modules/spells/spell_types/pointed/aoe/lightning_lure.dm @@ -6,10 +6,8 @@ charge_sound = 'sound/magic/charging_lightning.ogg' click_to_activate = FALSE - point_cost = 4 - attunements = list( - /datum/attunement/electric = 0.9 - ) + required_form = FORM_LIGHTNING + required_technique = TECHNIQUE_DESTRUCTION invocation = "Lighnting Lure!!!" invocation_type = INVOCATION_SHOUT diff --git a/code/modules/spells/spell_types/pointed/aoe/on_turf/arcyne_storm.dm b/code/modules/spells/spell_types/pointed/aoe/on_turf/arcyne_storm.dm index 84b0aa0315f..7eea3814fe0 100644 --- a/code/modules/spells/spell_types/pointed/aoe/on_turf/arcyne_storm.dm +++ b/code/modules/spells/spell_types/pointed/aoe/on_turf/arcyne_storm.dm @@ -3,8 +3,6 @@ desc = "Conjure ripples of force into existence over a large area, injuring any who enter." button_icon_state = "hierophant" - point_cost = 8 - charge_time = 4 SECONDS charge_drain = 1 cooldown_time = 50 SECONDS @@ -12,9 +10,8 @@ aoe_radius = 4 - attunements = list( - /datum/attunement/arcyne = 1.2 - ) + required_form = FORM_ARCANE + required_technique = TECHNIQUE_DESTRUCTION invocation = "BE TORN APART!!!" invocation_type = INVOCATION_SHOUT @@ -27,7 +24,7 @@ new /obj/effect/temp_visual/arcyne_storm(victim) playsound(victim, "genslash", 40, TRUE) for(var/mob/living/L in victim.contents) - L.adjustBruteLoss(round(7 * attuned_strength), damage_type = BCLASS_CUT) + L.adjustBruteLoss(round(7 * spell_magnitude_modifier), damage_type = BCLASS_CUT) to_chat(L, span_userdanger("I'm cut by an arcyne force!")) /obj/effect/temp_visual/arcyne_storm diff --git a/code/modules/spells/spell_types/pointed/aoe/on_turf/ensnare.dm b/code/modules/spells/spell_types/pointed/aoe/on_turf/ensnare.dm index ca5d500c52e..ea189b0711c 100644 --- a/code/modules/spells/spell_types/pointed/aoe/on_turf/ensnare.dm +++ b/code/modules/spells/spell_types/pointed/aoe/on_turf/ensnare.dm @@ -2,11 +2,9 @@ name = "Ensnare" desc = "Tendrils of arcyne force will hold all in an area in place, for a time." button_icon_state = "ensnare" - point_cost = 3 - attunements = list( - /datum/attunement/time = 0.3, - /datum/attunement/arcyne = 0.4, - ) + + required_form = FORM_ARCANE + required_technique = TECHNIQUE_CREATION invocation = "H'ld D'wn!" invocation_type = INVOCATION_SHOUT diff --git a/code/modules/spells/spell_types/pointed/aoe/on_turf/flower_field.dm b/code/modules/spells/spell_types/pointed/aoe/on_turf/flower_field.dm index 61e24c894ca..d0515a8acca 100644 --- a/code/modules/spells/spell_types/pointed/aoe/on_turf/flower_field.dm +++ b/code/modules/spells/spell_types/pointed/aoe/on_turf/flower_field.dm @@ -3,11 +3,9 @@ desc = "Summons a magical field of flowers using a single flower." button_icon_state = "flower_field" - point_cost = 5 - attunements = list( - /datum/attunement/earth = 0.4, - /datum/attunement/life = 0.3, - ) + required_form = FORM_EARTH + required_technique = TECHNIQUE_CREATION + required_level = 3 invocation = "May the earth bloom!" invocation_type = INVOCATION_WHISPER @@ -55,6 +53,7 @@ if(isliving(owner)) var/mob/living/L = owner L.apply_status_effect(/datum/status_effect/buff/flowerfield_resistance) + qdel(flower_item) /datum/action/cooldown/spell/aoe/on_turf/circle/flower_field/cast_on_thing_in_aoe(turf/victim, atom/caster) if(prob(25)) diff --git a/code/modules/spells/spell_types/pointed/aoe/on_turf/meteor_storm.dm b/code/modules/spells/spell_types/pointed/aoe/on_turf/meteor_storm.dm index b92d0b56d55..be6ce632fe7 100644 --- a/code/modules/spells/spell_types/pointed/aoe/on_turf/meteor_storm.dm +++ b/code/modules/spells/spell_types/pointed/aoe/on_turf/meteor_storm.dm @@ -5,10 +5,9 @@ button_icon_state = "fireball_greater" sound = 'sound/magic/meteorstorm.ogg' - point_cost = 8 - attunements = list( - /datum/attunement/fire = 1.2 - ) + required_form = FORM_FIRE + required_technique = TECHNIQUE_DESTRUCTION + required_level = 4 charge_time = 25 SECONDS charge_drain = 2 @@ -28,5 +27,5 @@ cast_on.visible_message(span_boldwarning("Fire is raining from the sky!")) /datum/action/cooldown/spell/aoe/on_turf/meteor_storm/cast_on_thing_in_aoe(turf/victim, atom/caster) - if(prob(20 * attuned_strength)) + if(prob(20 * spell_magnitude_modifier)) new /obj/effect/temp_visual/target/meteor(get_turf(victim)) diff --git a/code/modules/spells/spell_types/pointed/aoe/on_turf/snap_freeze.dm b/code/modules/spells/spell_types/pointed/aoe/on_turf/snap_freeze.dm index bf391a371ef..b55a80dfd08 100644 --- a/code/modules/spells/spell_types/pointed/aoe/on_turf/snap_freeze.dm +++ b/code/modules/spells/spell_types/pointed/aoe/on_turf/snap_freeze.dm @@ -3,11 +3,8 @@ desc = "Freeze the air in a small area in an instant." button_icon_state = "snapfreeze" - point_cost = 2 - attunements = list( - /datum/attunement/blood = 0.3, - /datum/attunement/ice = 0.4, - ) + required_form = FORM_ICE + required_technique = TECHNIQUE_DESTRUCTION invocation = "Air be still!" invocation_type = INVOCATION_SHOUT @@ -34,7 +31,7 @@ playsound(L, 'sound/magic/magic_nulled.ogg', 100) continue L.adjustFireLoss(35) - L.apply_status_effect(/datum/status_effect/debuff/frostbite, null, attuned_strength) + L.apply_status_effect(/datum/status_effect/debuff/frostbite, null, spell_magnitude_modifier) to_chat(L, span_userdanger("The air chills your bones!")) /obj/effect/temp_visual/trapice diff --git a/code/modules/spells/spell_types/pointed/aoe/repulse.dm b/code/modules/spells/spell_types/pointed/aoe/repulse.dm index 65ee3136833..6751298c073 100644 --- a/code/modules/spells/spell_types/pointed/aoe/repulse.dm +++ b/code/modules/spells/spell_types/pointed/aoe/repulse.dm @@ -3,9 +3,6 @@ desc = "This spell throws everything around the user away." button_icon_state = "repulse" - point_cost = 3 - - school = SCHOOL_EVOCATION invocation = "GITTAH WEIGH" invocation_type = INVOCATION_SHOUT aoe_radius = 2 @@ -18,9 +15,8 @@ charge_drain = 1 charge_slowdown = 0.7 - attunements = list( - /datum/attunement/aeromancy = 0.4, - ) + required_form = FORM_AIR + required_technique = TECHNIQUE_CREATION /// The minimum throw range of the replusion var/min_throw = 3 @@ -76,7 +72,7 @@ desc = "Throw back attackers with a sweep of your tail." button_icon_state = "tailsweep" - point_cost = 0 + required_form = null sound = 'sound/misc/tail_swing.ogg' spell_flags = NONE diff --git a/code/modules/spells/spell_types/pointed/arcane_burst.dm b/code/modules/spells/spell_types/pointed/arcane_burst.dm new file mode 100644 index 00000000000..3e281107827 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/arcane_burst.dm @@ -0,0 +1,80 @@ +/datum/action/cooldown/spell/arcyne_burst + button_icon = 'icons/mob/actions/spells/mage_telomancy.dmi' + name = "Arcyne Burst" + desc = "Mark a nearby area with arcyne force. It swells for a few seconds before bursting, striking everyone across the whole zone with a wave of kinetic force and hurling them outward." + button_icon_state = "arcyne_burst" + sound = 'sound/magic/vlightning.ogg' + + click_to_activate = TRUE + cast_range = SPELL_RANGE_GROUND + + required_form = FORM_ARCANE + required_technique = TECHNIQUE_DESTRUCTION + + invocation = "Erumpe!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_POKE + charge_slowdown = CHARGING_SLOWDOWN_SMALL + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 20 SECONDS + + spell_impact_intensity = SPELL_IMPACT_MEDIUM + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/blast_radius = 1 + var/blast_delay = 2 SECONDS + var/blast_damage = 75 + var/push_dist = 3 + +/datum/action/cooldown/spell/arcyne_burst/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/turf/center = get_turf(cast_on) + if(!center) + return FALSE + + if(!(center in get_hear(cast_range, get_turf(H)))) + to_chat(H, span_warning("I can't cast where I can't see!")) + return FALSE + + for(var/turf/T in range(blast_radius, center)) + if(T.density) + continue + new /obj/effect/temp_visual/pillar_warning/fadein(T, blast_delay) + + addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(arcyne_burst_erupt), center, H, blast_radius, blast_damage, push_dist, src, name), blast_delay) + + return TRUE + +/proc/arcyne_burst_erupt(turf/epicenter, mob/living/carbon/human/caster, radius = 1, damage = 50, push_dist = 3, datum/action/cooldown/spell/guard_source, spell_name = "Arcyne Burst") + if(!epicenter) + return + playsound(epicenter, 'sound/magic/repulse.ogg', 90, TRUE, 4) + for(var/turf/T in range(radius, epicenter)) + if(!(T in get_hear(radius, epicenter))) + continue + new /obj/effect/temp_visual/kinetic_blast(T) + for(var/obj/structure/S in T) + S.take_damage(damage, BRUTE, "blunt", FALSE) + for(var/mob/living/L in T.contents) + if(L.anti_magic_check()) + L.visible_message(span_warning("The arcyne force dissipates around [L]!")) + playsound(T, 'sound/magic/magic_nulled.ogg', 100) + continue + if(istype(caster) && !QDELETED(caster) && ishuman(L)) + arcyne_strike(caster, L, null, damage, caster.zone_selected, \ + BCLASS_BLUNT, spell_name = spell_name, \ + damage_type = BRUTE, npc_simple_damage_mult = 1, \ + skip_animation = TRUE) + else + L.adjustBruteLoss(damage * 1.5) + SEND_SIGNAL(L, COMSIG_ATOM_WAS_ATTACKED, caster, damage) + var/push_dir = get_dir(epicenter, L) || pick(GLOB.cardinals) + L.safe_throw_at(get_ranged_target_turf(L, push_dir, push_dist), push_dist, 1, caster, force = MOVE_FORCE_STRONG) + new /obj/effect/temp_visual/spell_impact(get_turf(L), GLOB.form_colors[FORM_ARCANE], SPELL_IMPACT_MEDIUM) diff --git a/code/modules/spells/spell_types/pointed/battle_ward.dm b/code/modules/spells/spell_types/pointed/battle_ward.dm new file mode 100644 index 00000000000..573588633ad --- /dev/null +++ b/code/modules/spells/spell_types/pointed/battle_ward.dm @@ -0,0 +1,256 @@ +#define BATTLE_WARD_RUNE_DURATION (1 MINUTES) +#define BATTLE_WARD_TELEGRAPH_TIME (3 SECONDS) + +/datum/action/cooldown/spell/battle_ward + button_icon = 'icons/mob/actions/spells/mage_battlewardry.dmi' + name = "Battle Ward" + desc = "Channel arcyne energy to inscribe a pattern of five rune wards in an X formation on a targeted area. The runes are fragile and last for one minute before fading. In addition, Battle Wards are indiscriminate unlike permanent Rune Wards, and will hit the caster and their allies. They can be circumvented by jumping or flying over them, or destroyed with a few solid hits. The type of rune placed depends on your current ward mode.\n\ + Use the Alt Mode keybind to cycle between Stun, Fire, Chill, and Damage." + button_icon_state = "battle_ward" + sound = 'sound/magic/charging.ogg' + + click_to_activate = TRUE + cast_range = SPELL_RANGE_GROUND + self_cast_possible = FALSE + + spell_cost = 40 + required_form = FORM_WATER + required_technique = TECHNIQUE_CREATION + + invocation = "Bellitutela Inscriptum!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_HEAVY + charge_slowdown = CHARGING_SLOWDOWN_SMALL + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 20 SECONDS + + spell_impact_intensity = SPELL_IMPACT_NONE + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/ward_mode = "stun" + var/list/ward_modes = list( + "stun" = /obj/structure/rune_ward/stun, + "fire" = /obj/structure/rune_ward/fire, + "chill" = /obj/structure/rune_ward/chill, + "damage" = /obj/structure/rune_ward/damage + ) + var/static/list/ward_mode_labels = list("stun" = "STUN", "fire" = "FIRE", "chill" = "CHILL", "damage" = "DMG") + +/datum/action/cooldown/spell/battle_ward/Grant(mob/grant_to) + . = ..() + update_ward_maptext() + +/datum/action/cooldown/spell/battle_ward/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/turf/center = get_turf(cast_on) + if(!center) + return FALSE + + var/rune_path = ward_modes[ward_mode] + if(!rune_path) + return FALSE + + // X pattern: center + 4 corners + var/list/target_turfs = list() + target_turfs += center + target_turfs += get_step(center, NORTHWEST) + target_turfs += get_step(center, NORTHEAST) + target_turfs += get_step(center, SOUTHWEST) + target_turfs += get_step(center, SOUTHEAST) + + // Show telegraph visuals before runes appear + for(var/turf/T in target_turfs) + new /obj/effect/temp_visual/trap_wall/battle_ward(T) + + playsound(center, 'sound/magic/whiteflame.ogg', 60, TRUE) + H.visible_message(span_warning("[H] completes a complex inscription - runes begin to materialize!"), span_notice("I finish inscribing the [ward_mode] ward pattern.")) + + addtimer(CALLBACK(src, PROC_REF(spawn_runes), target_turfs, rune_path, H.real_name, H.ckey || "no ckey"), BATTLE_WARD_TELEGRAPH_TIME) + return TRUE + +/datum/action/cooldown/spell/battle_ward/proc/spawn_runes(list/turfs, rune_path, caster_name, caster_ckey) + for(var/turf/T in turfs) + var/obj/structure/rune_ward/rune = new rune_path(T) + rune.owner_name = caster_name + rune.owner_ckey = caster_ckey + rune.modify_max_integrity(50) + QDEL_IN(rune, BATTLE_WARD_RUNE_DURATION) + + +/datum/action/cooldown/spell/battle_ward/toggle_alt_mode(mob/user) + var/current_index = ward_modes.Find(ward_mode) + current_index = (current_index % length(ward_modes)) + 1 + ward_mode = ward_modes[current_index] + var/label = ward_mode_labels[ward_mode] || uppertext(ward_mode) + to_chat(user, span_notice("Ward mode set to: [label].")) + update_ward_maptext() + return TRUE + +/datum/action/cooldown/spell/battle_ward/proc/update_ward_maptext() + var/label = ward_mode_labels[ward_mode] + for(var/datum/hud/hud as anything in viewers) + var/atom/movable/screen/movable/action_button/B = viewers[hud] + var/atom/movable/screen/arc_maptext_holder/holder + for(var/atom/movable/screen/arc_maptext_holder/existing in B.vis_contents) + holder = existing + break + if(!holder) + holder = new(B) + B.vis_contents.Add(holder) + holder.maptext = MAPTEXT(label) + holder.maptext_x = 5 + +/obj/effect/temp_visual/trap_wall/battle_ward + duration = BATTLE_WARD_TELEGRAPH_TIME + +#undef BATTLE_WARD_RUNE_DURATION +#undef BATTLE_WARD_TELEGRAPH_TIME + +#define RUNE_WARD_IMMUNITY_DURATION (3 SECONDS) +#define RUNE_WARD_IMMUNITY_KEY "rune_ward_immunity" + +/obj/structure/rune_ward + name = "arcyne rune" + desc = "A faintly glowing symbol etched into the ground." + icon = 'icons/misc/rune_wards.dmi' + icon_state = "rune" + attacked_sound = 'sound/magic/magic_nulled.ogg' + density = FALSE + anchored = TRUE + alpha = 180 + layer = TURF_LAYER + 0.1 + max_integrity = 300 + + var/datum/weakref/owner_ref + var/datum/weakref/spell_ref + var/owner_name = "unknown" + var/owner_ckey = "unknown" + var/checks_antimagic = TRUE + +/obj/structure/rune_ward/Crossed(atom/movable/AM) + if(ismob(AM)) + var/mob/M = AM + var/mob/owner = owner_ref?.resolve() + if(M == owner) + return + if(checks_antimagic && M.anti_magic_check()) + trigger_visual() + qdel(src) + return + if(AM.throwing) + return + if(isliving(AM)) + var/mob/living/L = AM + if(L.movement_type & (FLYING|FLOATING)) + return + if(L.pulledby) + return + if(L.mob_timers[RUNE_WARD_IMMUNITY_KEY] && world.time < L.mob_timers[RUNE_WARD_IMMUNITY_KEY]) + return + else + return + var/mob/living/L = AM + if(!isliving(L)) + return + L.mob_timers[RUNE_WARD_IMMUNITY_KEY] = world.time + RUNE_WARD_IMMUNITY_DURATION + log_combat(AM, src, "triggered [name] placed by [owner_name] ([owner_ckey]) at [AREACOORD(src)]") + rune_effect(L) + trigger_visual() + qdel(src) + +/obj/structure/rune_ward/proc/trigger_visual() + alpha = 255 + flick(icon_state, src) + +/obj/structure/rune_ward/proc/rune_effect(mob/living/L) + return + +/obj/structure/rune_ward/Destroy() + owner_ref = null + spell_ref = null + return ..() + +/obj/structure/rune_ward/examine(mob/user) + . = ..() + if(max_integrity <= 50) + . += span_info("This rune looks very fragile - a few solid hits would destroy it.") + else + . += span_info("A skilled mage can scrub this effortlessly. Otherwise, it must be destroyed by force.") + . += span_info("Flying, jumping, or being thrown over the rune will not trigger it.") + +/obj/structure/rune_ward/stun + name = "shock rune" + icon_state = "rune_stun" + +/obj/structure/rune_ward/stun/rune_effect(mob/living/L) + to_chat(L, span_danger("The rune erupts with arcyne lightning!")) + playsound(src, 'sound/magic/lightning.ogg', 80, TRUE) + L.lightning_shock(src) + +/obj/structure/rune_ward/fire + name = "flame rune" + icon_state = "rune_fire" + +/obj/structure/rune_ward/fire/rune_effect(mob/living/L) + to_chat(L, span_danger("The rune erupts in flames!")) + playsound(src, pick('sound/misc/explode/incendiary (1).ogg', 'sound/misc/explode/incendiary (2).ogg'), 80, TRUE) + new /obj/effect/hotspot(get_turf(src)) + L.Slowdown(4) + L.adjust_fire_stacks(6) + +/obj/structure/rune_ward/chill + name = "frost rune" + icon_state = "rune_chill" + +/obj/structure/rune_ward/chill/rune_effect(mob/living/L) + to_chat(L, span_danger("Frost erupts from the rune and seizes your limbs!")) + playsound(src, 'sound/spellbooks/crystal.ogg', 80, TRUE) + new /obj/effect/temp_visual/trapice(get_turf(src)) + L.Slowdown(4) + L.adjustFireLoss(10) + apply_frost_stack(L, 4) + +/obj/structure/rune_ward/damage + name = "force rune" + icon_state = "rune_damage" + var/rune_damage = 80 + +/obj/structure/rune_ward/damage/rune_effect(mob/living/L) + to_chat(L, span_danger("Arcyne blades erupt from the rune!")) + playsound(src, 'sound/magic/blade_burst.ogg', 80, TRUE) + playsound(src, pick('sound/combat/hits/bladed/genstab (1).ogg', 'sound/combat/hits/bladed/genstab (2).ogg', 'sound/combat/hits/bladed/genstab (3).ogg'), 80, TRUE) + new /obj/effect/temp_visual/blade_burst(get_turf(src)) + L.Slowdown(4) + var/mob/living/carbon/human/owner = owner_ref?.resolve() + if(ishuman(owner) && ishuman(L)) + arcyne_strike(owner, L, null, rune_damage, BODY_ZONE_CHEST, \ + BCLASS_STAB, armor_penetration = 25, spell_name = "Force Rune", \ + damage_type = BRUTE, skip_animation = TRUE) + else + L.adjustBruteLoss(rune_damage) + +/obj/structure/rune_ward/alarm + name = "alarm rune" + icon_state = "rune_alarm" + alpha = 40 + +/obj/structure/rune_ward/alarm/rune_effect(mob/living/L) + to_chat(L, span_danger("The rune chimes loudly!")) + playsound(src, 'sound/magic/charging.ogg', 80, TRUE) + var/mob/owner = owner_ref?.resolve() + if(owner) + var/area/A = get_area(src) + var/area_name = A ? A.name : "an unknown location" + to_chat(owner, span_warning("One of my alarm runes has been triggered at [area_name]!")) + if(owner.client) + SEND_SOUND(owner, sound('sound/magic/charging.ogg', volume = 40)) + +#undef RUNE_WARD_IMMUNITY_DURATION +#undef RUNE_WARD_IMMUNITY_KEY diff --git a/code/modules/spells/spell_types/pointed/beam/beam_of_frost.dm b/code/modules/spells/spell_types/pointed/beam/beam_of_frost.dm index 7aea2b02249..201005aaa7c 100644 --- a/code/modules/spells/spell_types/pointed/beam/beam_of_frost.dm +++ b/code/modules/spells/spell_types/pointed/beam/beam_of_frost.dm @@ -3,10 +3,8 @@ desc = "Shoots a beam of frost out, slowing anyone hit by it." button_icon_state = "rayoffrost" - point_cost = 3 - attunements = list( - /datum/attunement/ice = 0.6, - ) + required_form = FORM_ICE + required_technique = TECHNIQUE_DESTRUCTION charge_time = 2.5 SECONDS charge_drain = 1 @@ -60,7 +58,7 @@ qdel(active) return - var/strength = clamp(attuned_strength * extra_modifer, 0.5, 2) + var/strength = clamp(spell_magnitude_modifier * extra_modifer, 0.5, 2) victim.adjustFireLoss(round(20 * strength)) victim.apply_status_effect(/datum/status_effect/debuff/frostbite, null, strength) diff --git a/code/modules/spells/spell_types/pointed/beast_tame.dm b/code/modules/spells/spell_types/pointed/beast_tame.dm index 13bc10f247d..1f744b3a7b3 100644 --- a/code/modules/spells/spell_types/pointed/beast_tame.dm +++ b/code/modules/spells/spell_types/pointed/beast_tame.dm @@ -11,9 +11,9 @@ antimagic_flags = MAGIC_RESISTANCE_HOLY associated_skill = /datum/attribute/skill/magic/holy required_items = list(/obj/item/clothing/neck/psycross/silver/divine/dendor) - attunements = list( - /datum/attunement/earth = 1, - ) + + required_form = FORM_EARTH + required_technique = TECHNIQUE_SUMMONING invocation = "Be still and calm, brotherbeast." invocation_type = INVOCATION_WHISPER diff --git a/code/modules/spells/spell_types/pointed/blade_burst.dm b/code/modules/spells/spell_types/pointed/blade_burst.dm index 5f6e946a4d8..6f8142acad4 100644 --- a/code/modules/spells/spell_types/pointed/blade_burst.dm +++ b/code/modules/spells/spell_types/pointed/blade_burst.dm @@ -4,10 +4,8 @@ button_icon_state = "blade_burst" self_cast_possible = FALSE - point_cost = 2 - attunements = list( - /datum/attunement/earth = 0.5, - ) + required_form = FORM_EARTH + required_technique = TECHNIQUE_CREATION invocation = "Earth pierce you!!!" invocation_type = INVOCATION_SHOUT @@ -24,10 +22,10 @@ var/turf/T = get_turf(cast_on) new /obj/effect/temp_visual/trap(T) playsound(T, 'sound/magic/blade_burst.ogg', 80, TRUE, soundping = TRUE) - addtimer(CALLBACK(src, PROC_REF(send_blades), T), round(1 SECONDS / attuned_strength)) + addtimer(CALLBACK(src, PROC_REF(send_blades), T), round(1 SECONDS / spell_magnitude_modifier)) /datum/action/cooldown/spell/blade_burst/proc/send_blades(turf/victim) - var/scaled_damage = round(45 * attuned_strength) + var/scaled_damage = round(45 * spell_magnitude_modifier) new /obj/effect/temp_visual/blade_burst(victim) playsound(victim,'sound/combat/newstuck.ogg', 80, TRUE, soundping = TRUE) for(var/mob/living/L in victim) @@ -35,7 +33,7 @@ L.apply_damage(scaled_damage, BRUTE, def_zone) var/obj/item/bodypart/BP = L.get_bodypart(def_zone) if(BP) - var/probby = (BP.get_damage() / BP.max_damage) * 20 * attuned_strength + var/probby = (BP.get_damage() / BP.max_damage) * 20 * spell_magnitude_modifier if(prob(probby)) BP.add_wound(/datum/wound/fracture) L.adjustBruteLoss(scaled_damage, damage_type = BCLASS_CUT) @@ -53,3 +51,13 @@ icon_state = "grassblade" randomdir = FALSE duration = 1 SECONDS + +/obj/effect/temp_visual/trap/telomancy + color = "#9932CC" + light_color = "#9932CC" + duration = 3 SECONDS + +/obj/effect/temp_visual/trap/geomancy + color = "#8B4513" + light_color = "#8B4513" + duration = 4 SECONDS diff --git a/code/modules/spells/spell_types/pointed/blindness.dm b/code/modules/spells/spell_types/pointed/blindness.dm index 1d856acfd6f..6f29e6fcbe4 100644 --- a/code/modules/spells/spell_types/pointed/blindness.dm +++ b/code/modules/spells/spell_types/pointed/blindness.dm @@ -4,9 +4,8 @@ button_icon_state = "blindness" sound = 'sound/magic/churn.ogg' - attunements = list( - /datum/attunement/arcyne = 0.1 - ) + required_form = FORM_ARCANE + required_technique = TECHNIQUE_ILLUSION invocation = "darkness envelop them." invocation_type = INVOCATION_WHISPER diff --git a/code/modules/spells/spell_types/pointed/chill_touch.dm b/code/modules/spells/spell_types/pointed/chill_touch.dm index a81b87adf1f..6c705d02420 100644 --- a/code/modules/spells/spell_types/pointed/chill_touch.dm +++ b/code/modules/spells/spell_types/pointed/chill_touch.dm @@ -5,11 +5,7 @@ sound = 'sound/magic/whiteflame.ogg' self_cast_possible = FALSE - point_cost = 2 - attunements = list( - /datum/attunement/ice = 0.3, - /datum/attunement/death = 0.2, - ) + required_form = FORM_ICE invocation = "Death grip you!" invocation_type = INVOCATION_SHOUT diff --git a/code/modules/spells/spell_types/pointed/circumdatum.dm b/code/modules/spells/spell_types/pointed/circumdatum.dm new file mode 100644 index 00000000000..4f09ed53139 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/circumdatum.dm @@ -0,0 +1,104 @@ +/datum/action/cooldown/spell/circumdatum + button_icon = 'icons/mob/actions/spells/mage_telomancy.dmi' + name = "Circumdatum" + desc = "Surrounds an ally with warding orbs instantly. Each reduce an incoming blow's integrity damage by 25% before disintegrating." + button_icon_state = "circumdatum" + sound = 'sound/magic/vlightning.ogg' + + click_to_activate = TRUE + self_cast_possible = FALSE + cast_range = 7 + + spell_color = 12 + required_form = FORM_ARCANE + + invocation = "Circumdatum!" + invocation_type = INVOCATION_SHOUT + + charge_required = FALSE + cooldown_time = 15 SECONDS + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN + + var/orb_count = 5 + +/datum/action/cooldown/spell/circumdatum/cast(atom/cast_on) + . = ..() + if(!ishuman(cast_on)) + to_chat(owner, span_warning("I can only ward another person!")) + return FALSE + var/mob/living/carbon/human/target = cast_on + target.apply_status_effect(/datum/status_effect/buff/circumdatum, null, orb_count) + return TRUE + +/atom/movable/screen/alert/status_effect/buff/circumdatum + name = "Circumdatum" + desc = "Arcyne orbs circle me,ready to blunt a blow before it lands." + icon_state = "buff" + +/datum/status_effect/buff/circumdatum + id = "circumdatum" + duration = 30 SECONDS + alert_type = /atom/movable/screen/alert/status_effect/buff/circumdatum + var/orbs = 5 + var/list/orb_visuals + var/last_struck_time = 0 + +/datum/status_effect/buff/circumdatum/on_creation(mob/living/new_owner, duration_override, count = 5) + orbs = count + return ..() + +/datum/status_effect/buff/circumdatum/on_apply() + . = ..() + if(!.) + return + orb_visuals = list() + owner.apply_status_effect(/datum/status_effect/buff/iron_skin, duration) + RegisterSignals(owner, list(COMSIG_MOB_APPLY_DAMAGE, COMSIG_ATOM_BULLET_ACT, COMSIG_ATOM_HITBY, COMSIG_ATOM_WAS_ATTACKED), PROC_REF(on_struck)) + for(var/i in 1 to orbs) + var/obj/effect/circumdatum_orb/orb = new() + orb_visuals += orb + owner.vis_contents += orb + spin_orb(orb, (i - 1) * (360 / orbs)) + owner.balloon_alert_to_viewers("warded!") + +/datum/status_effect/buff/circumdatum/proc/spin_orb(obj/effect/orb, phase) + orb.orbit(owner, 16, TRUE, 40, 36, starting_rotation = phase) + +/datum/status_effect/buff/circumdatum/proc/on_struck(datum/source, mob/living/struck, mob/living/attacker, obj/item/weapon) + SIGNAL_HANDLER + if(world.time == last_struck_time) + return + last_struck_time = world.time + deplete_orb() + +/datum/status_effect/buff/circumdatum/proc/deplete_orb() + orbs = max(0, orbs - 1) + if(length(orb_visuals)) + var/obj/effect/spent = orb_visuals[length(orb_visuals)] + orb_visuals -= spent + if(owner) + owner.vis_contents -= spent + if(!QDELETED(spent)) + qdel(spent) + if(orbs <= 0) + owner.remove_status_effect(/datum/status_effect/buff/circumdatum) + +/datum/status_effect/buff/circumdatum/on_remove() + UnregisterSignal(owner, list(COMSIG_MOB_APPLY_DAMAGE, COMSIG_ATOM_BULLET_ACT, COMSIG_ATOM_HITBY, COMSIG_ATOM_WAS_ATTACKED)) + owner.remove_status_effect(/datum/status_effect/buff/iron_skin) + for(var/obj/effect/orb in orb_visuals) + if(owner) + owner.vis_contents -= orb + if(!QDELETED(orb)) + qdel(orb) + orb_visuals = null + . = ..() + +/obj/effect/circumdatum_orb + name = "arcyne orb" + icon = 'icons/obj/magic_projectiles.dmi' + icon_state = "seeker_orb" + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + layer = ABOVE_MOB_LAYER + vis_flags = VIS_INHERIT_PLANE diff --git a/code/modules/spells/spell_types/pointed/conjure/_conjure.dm b/code/modules/spells/spell_types/pointed/conjure/_conjure.dm index 6b5d38e2cb0..bfef5500f8d 100644 --- a/code/modules/spells/spell_types/pointed/conjure/_conjure.dm +++ b/code/modules/spells/spell_types/pointed/conjure/_conjure.dm @@ -1,6 +1,4 @@ /datum/action/cooldown/spell/conjure - school = SCHOOL_CONJURATION - /// A list of types that will be created on summon. /// The type is picked from this list, not all provided are guaranteed. var/list/summon_type = list() diff --git a/code/modules/spells/spell_types/pointed/conjure/aether_knife.dm b/code/modules/spells/spell_types/pointed/conjure/aether_knife.dm new file mode 100644 index 00000000000..c14a5f12558 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/conjure/aether_knife.dm @@ -0,0 +1,63 @@ +/datum/action/cooldown/spell/aetherknife + button_icon = 'icons/mob/actions/spells/mage_conjure.dmi' + name = "Aetherknife" + desc = "Congeal magickal energies into a blade which gains a bonus to power based on INT.\n\ + The blade lasts until a new one is summoned or the spell is forgotten. Deals physical damage." + button_icon_state = "aetherknife" + sound = 'sound/magic/whiteflame.ogg' + + click_to_activate = FALSE + self_cast_possible = TRUE + + required_form = FORM_LIGHTNING + + invocation = "Desperta ferro!" + invocation_type = INVOCATION_SHOUT + + charge_required = FALSE + cooldown_time = 5 SECONDS + + spell_impact_intensity = SPELL_IMPACT_LOW + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/obj/item/weapon/conjured_knife = null + +/datum/action/cooldown/spell/aetherknife/cast(atom/cast_on) + . = ..() + var/mob/living/user = owner + if(!istype(user)) + return FALSE + + if(src.conjured_knife) + qdel(conjured_knife) + var/obj/item/weapon/R = new /obj/item/weapon/knife/throwingknife/aetherknife(user.drop_location()) + R.AddComponent(/datum/component/conjured_item, null, FALSE, user, src) + + if(GET_MOB_ATTRIBUTE_VALUE(user, STAT_INTELLIGENCE) > 10) + var/int_scaling = GET_MOB_ATTRIBUTE_VALUE(user, STAT_INTELLIGENCE) - 10 + R.force = R.force + int_scaling + R.throwforce = R.throwforce + int_scaling * 2 + R.name = "aetherknife +[int_scaling]" + user.put_in_hands(R) + src.conjured_knife = R + return TRUE + +/datum/action/cooldown/spell/aetherknife/Destroy() + if(src.conjured_knife) + conjured_knife.visible_message(span_warning("[conjured_knife] disintegrates into glittering motes!")) + qdel(conjured_knife) + return ..() + +/obj/item/weapon/knife/throwingknife/aetherknife + name = "aetherknife" + desc = "A knife formed out of congealed magickal energies. Makes for a very deadly melee and throwing weapon." + icon_state = "aetherknife" + icon = 'icons/mob/actions/spells/mage_conjure.dmi' + force = 15 + throwforce = 20 + wdefense = 3 + w_class = WEIGHT_CLASS_TINY + slot_flags = ITEM_SLOT_MOUTH + possible_item_intents = list(/datum/intent/dagger/thrust, /datum/intent/sword/thrust/short) + sellprice = 0 diff --git a/code/modules/spells/spell_types/pointed/conjure/bonfire.dm b/code/modules/spells/spell_types/pointed/conjure/bonfire.dm deleted file mode 100644 index ed31d08221b..00000000000 --- a/code/modules/spells/spell_types/pointed/conjure/bonfire.dm +++ /dev/null @@ -1,20 +0,0 @@ -/datum/action/cooldown/spell/conjure/bonfire - name = "Create Bonfire" - desc = "Creates a magical bonfire to cook on." - button_icon_state = "create_campfire" - self_cast_possible = FALSE - - point_cost = 1 - - sound = 'sound/magic/whiteflame.ogg' - - charge_required = FALSE - cooldown_time = 30 SECONDS - spell_cost = 30 - spell_flags = SPELL_RITUOS - invocation = "Bonfire!" - invocation_type = INVOCATION_SHOUT - - summon_type = list(/obj/machinery/light/fueled/firebowl/church/magic) - summon_radius = 0 - summon_lifespan = 30 SECONDS diff --git a/code/modules/spells/spell_types/pointed/conjure/familiar.dm b/code/modules/spells/spell_types/pointed/conjure/familiar.dm deleted file mode 100644 index 8f71f0e2768..00000000000 --- a/code/modules/spells/spell_types/pointed/conjure/familiar.dm +++ /dev/null @@ -1,41 +0,0 @@ -/datum/action/cooldown/spell/conjure/familiar - name = "Find Familiar" - desc = "Summons a temporary spectral volf to aid you." - button_icon_state = "zad" - self_cast_possible = FALSE - - point_cost = 3 - - sound = 'sound/magic/whiteflame.ogg' - - invocation = "B'ST FR'ND!" - invocation_type = INVOCATION_SHOUT - - charge_time = 2 SECONDS - charge_drain = 1 - charge_slowdown = 1.3 - cooldown_time = 6 MINUTES - spell_cost = 30 - spell_flags = SPELL_RITUOS - summon_type = list(/mob/living/simple_animal/hostile/retaliate/wolf/familiar) - summon_radius = 0 - summon_lifespan = 5 MINUTES - - attunements = list( - /datum/attunement/arcyne = 0.4, - ) - -/datum/action/cooldown/spell/conjure/familiar/post_summon(atom/summoned_object, atom/cast_on) - var/mob/living/wolf = summoned_object - wolf.befriend(owner) - - if(isliving(cast_on)) - var/mob/living/L = cast_on - if(L.stat != DEAD) - wolf.ai_controller?.set_blackboard_key(BB_BASIC_MOB_PRIORITY_TARGETS, L) - return - for(var/mob/living/L in get_turf(cast_on)) - if(L.stat == DEAD) - continue - wolf.ai_controller?.set_blackboard_key(BB_BASIC_MOB_PRIORITY_TARGETS, L) - break diff --git a/code/modules/spells/spell_types/pointed/conjure/garden_fae.dm b/code/modules/spells/spell_types/pointed/conjure/garden_fae.dm index c2cfd23d33c..dd6addd4f50 100644 --- a/code/modules/spells/spell_types/pointed/conjure/garden_fae.dm +++ b/code/modules/spells/spell_types/pointed/conjure/garden_fae.dm @@ -16,10 +16,6 @@ charge_slowdown = 1.2 summon_radius = 0 self_cast_possible = FALSE - attunements = list( - /datum/attunement/arcyne = 0.3, - /datum/attunement/life = 0.7 - ) var/datum/weakref/gardener_ref var/static/list/garden_fae = list( diff --git a/code/modules/spells/spell_types/pointed/conjure/hunters_trick.dm b/code/modules/spells/spell_types/pointed/conjure/hunters_trick.dm index 8fa9a445c52..221ce2b2301 100644 --- a/code/modules/spells/spell_types/pointed/conjure/hunters_trick.dm +++ b/code/modules/spells/spell_types/pointed/conjure/hunters_trick.dm @@ -10,9 +10,6 @@ antimagic_flags = MAGIC_RESISTANCE_HOLY associated_skill = /datum/attribute/skill/magic/holy required_items = list(/obj/item/clothing/neck/psycross/great_hunt) - attunements = list( - /datum/attunement/earth = 0.6, - ) invocation = "Forest bind them..." invocation_type = INVOCATION_WHISPER diff --git a/code/modules/spells/spell_types/pointed/conjure/kneestingers.dm b/code/modules/spells/spell_types/pointed/conjure/kneestingers.dm index f5460247097..ee0f87b3eb3 100644 --- a/code/modules/spells/spell_types/pointed/conjure/kneestingers.dm +++ b/code/modules/spells/spell_types/pointed/conjure/kneestingers.dm @@ -10,9 +10,6 @@ antimagic_flags = MAGIC_RESISTANCE_HOLY associated_skill = /datum/attribute/skill/magic/holy required_items = list(/obj/item/clothing/neck/psycross/silver/divine/dendor) - attunements = list( - /datum/attunement/earth = 0.6, - ) invocation = "Treefather light the way." invocation_type = INVOCATION_WHISPER diff --git a/code/modules/spells/spell_types/pointed/conjure/phantom_ear.dm b/code/modules/spells/spell_types/pointed/conjure/phantom_ear.dm index d000d74cd17..93fd01164a1 100644 --- a/code/modules/spells/spell_types/pointed/conjure/phantom_ear.dm +++ b/code/modules/spells/spell_types/pointed/conjure/phantom_ear.dm @@ -4,14 +4,15 @@ button_icon_state = "phantomear" self_cast_possible = TRUE - point_cost = 2 - has_visual_effects = FALSE charge_required = FALSE cooldown_time = 2 MINUTES spell_cost = 60 spell_flags = SPELL_RITUOS invocation = "Lend me thine ear." + + required_form = FORM_WATER + invocation_type = INVOCATION_WHISPER summon_type = list(/obj/item/phantom_ear) diff --git a/code/modules/spells/spell_types/pointed/conjure/raise_lesser_undead.dm b/code/modules/spells/spell_types/pointed/conjure/raise_lesser_undead.dm deleted file mode 100644 index 5847d167017..00000000000 --- a/code/modules/spells/spell_types/pointed/conjure/raise_lesser_undead.dm +++ /dev/null @@ -1,68 +0,0 @@ -/datum/action/cooldown/spell/conjure/raise_lesser_undead - name = "Raise Lesser undead" - desc = "Summons a temporary skeleton to aid you." - button_icon_state = "animate" - self_cast_possible = FALSE - sound = 'sound/magic/magnet.ogg' - - invocation = "SERVE ME!" - invocation_type = INVOCATION_SHOUT - - charge_time = 2 SECONDS - charge_drain = 1 - charge_slowdown = 1.3 - cooldown_time = 6 MINUTES - spell_cost = 70 - summon_type = list(/mob/living/simple_animal/hostile/skeleton) - summon_radius = 0 - summon_lifespan = 5 MINUTES - spell_type = SPELL_MIRACLE - antimagic_flags = MAGIC_RESISTANCE_HOLY - associated_skill = /datum/attribute/skill/magic/holy - attunements = list( - /datum/attunement/arcyne = 0.4, - ) - var/cabal_affine = FALSE - -/datum/action/cooldown/spell/conjure/raise_lesser_undead/before_cast() - . = ..() - var/skeleton_roll = rand(1,100) - switch(skeleton_roll) - if(1 to 20) - summon_type = list(/mob/living/simple_animal/hostile/skeleton/axe) - if(21 to 40) - summon_type = list(/mob/living/simple_animal/hostile/skeleton/spear) - if(41 to 60) - summon_type = list(/mob/living/simple_animal/hostile/skeleton/guard) - if(61 to 80) - summon_type = list(/mob/living/simple_animal/hostile/skeleton/bow) - if(81 to 100) - summon_type = list(/mob/living/simple_animal/hostile/skeleton) - - -/datum/action/cooldown/spell/conjure/raise_lesser_undead/post_summon(atom/summoned_object, atom/cast_on) - var/mob/living/skeleton = summoned_object - skeleton.befriend(owner) - if(cabal_affine) - skeleton.add_faction(FACTION_CABAL) - - if(isliving(cast_on)) - var/mob/living/L = cast_on - if(L.stat != DEAD) - skeleton.ai_controller?.set_blackboard_key(BB_BASIC_MOB_PRIORITY_TARGETS, L) - return - for(var/mob/living/L in get_turf(cast_on)) - if(L.stat == DEAD) - continue - skeleton.ai_controller?.set_blackboard_key(BB_BASIC_MOB_PRIORITY_TARGETS, L) - break - - -/datum/action/cooldown/spell/conjure/raise_lesser_undead/necromancer - name = "Lesser Necromancy" - spell_type = SPELL_MANA - antimagic_flags = MAGIC_RESISTANCE - associated_skill = /datum/attribute/skill/magic/arcane - cabal_affine = TRUE - cooldown_time = 30 SECONDS - summon_lifespan = 1 MINUTES diff --git a/code/modules/spells/spell_types/pointed/conjure/web.dm b/code/modules/spells/spell_types/pointed/conjure/web.dm index a843df1a74f..63dbb3b8584 100644 --- a/code/modules/spells/spell_types/pointed/conjure/web.dm +++ b/code/modules/spells/spell_types/pointed/conjure/web.dm @@ -16,6 +16,6 @@ summon_radius = 3 summon_amount = 0 spell_flags = SPELL_RITUOS - attunements = list( - /datum/attunement/death = 0.3, - ) + + required_form = FORM_DEATH + required_technique = TECHNIQUE_CREATION diff --git a/code/modules/spells/spell_types/pointed/control_undead.dm b/code/modules/spells/spell_types/pointed/control_undead.dm index fd37393d467..8de2c224c47 100644 --- a/code/modules/spells/spell_types/pointed/control_undead.dm +++ b/code/modules/spells/spell_types/pointed/control_undead.dm @@ -9,9 +9,9 @@ spell_type = SPELL_MANA antimagic_flags = MAGIC_RESISTANCE_UNHOLY associated_skill = /datum/attribute/skill/magic/arcane - attunements = list( - /datum/attunement/death = 1, - ) + + required_form = FORM_DEATH + required_technique = TECHNIQUE_SUMMONING invocation = "Obey me." invocation_type = INVOCATION_WHISPER @@ -20,6 +20,10 @@ cooldown_time = 1 MINUTES spell_cost = 75 + var/recoil_energy_floor = 200 + var/recoil_severity = CONJURE_RECOIL_LIGHT + var/recoil_stamina_only = FALSE + var/static/list/pet_commands = list( /datum/pet_command/idle, /datum/pet_command/free, @@ -52,6 +56,7 @@ cast_on.set_faction(list(FACTION_CABAL, FACTION_UNDEAD)) cast_on.befriend(owner) cast_on.pet_passive = TRUE + cast_on.AddComponent(/datum/component/conjured_minion, owner, recoil_energy_floor, recoil_severity, recoil_stamina_only) owner.visible_message( span_greentext("[owner] soothes \the [cast_on] with zizo's blessing."), diff --git a/code/modules/spells/spell_types/pointed/create_campfire.dm b/code/modules/spells/spell_types/pointed/create_campfire.dm new file mode 100644 index 00000000000..2eb8ed5748e --- /dev/null +++ b/code/modules/spells/spell_types/pointed/create_campfire.dm @@ -0,0 +1,66 @@ +/datum/action/cooldown/spell/create_campfire + button_icon = 'icons/mob/actions/roguespells.dmi' + name = "Create Campfire" + desc = "Creates a magical campfire to cook on. 3 tiles range. Lasts for 10 minutes." + button_icon_state = "create_campfire" + sound = 'sound/magic/whiteflame.ogg' + click_to_activate = TRUE + self_cast_possible = FALSE + cast_range = 3 + + spell_cost = 10 + required_form = FORM_FIRE + + invocation = "Ignis Castrensis Magicae." + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = 3 SECONDS + charge_slowdown = CHARGING_SLOWDOWN_MEDIUM + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 60 SECONDS + + spell_impact_intensity = SPELL_IMPACT_NONE + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_NO_MOVE | SPELL_REQUIRES_SAME_Z + + var/static/list/turf_blacklist = list( + /turf/open/water, + /turf/open/openspace, + ) + +/datum/action/cooldown/spell/create_campfire/cast(atom/cast_on) + . = ..() + var/turf/target = get_turf(cast_on) + + if(!target || !target.Enter(owner) || is_type_in_list(target, turf_blacklist)) + to_chat(owner, span_warning("This turf can't be on fiyaaaah! (It's blocked sire.)")) + return FALSE + + new /obj/machinery/light/fueled/campfire/create_campfire(target) + + return TRUE + +/obj/machinery/light/fueled/campfire/create_campfire + name = "magical campfire" + icon_state = "densefire1" + base_state = "densefire" + density = FALSE + layer = 2.8 + brightness = 7 + fueluse = 10 MINUTES + color = "#6ab2ee" + bulb_colour = "#6ab2ee" + max_integrity = 30 + var/lifespan = 10 MINUTES + +/obj/machinery/light/fueled/campfire/create_campfire/Initialize() + . = ..() + if(lifespan) + QDEL_IN(src, lifespan) //delete after it runs out + +/obj/machinery/light/fueled/campfire/create_campfire/onkick(mob/user) + var/mob/living/L = user + L.visible_message(span_info("[L] kicks \the [src], the arcyne fire dissipating.")) + burn_out() + qdel(src) diff --git a/code/modules/spells/spell_types/pointed/curtain_fire.dm b/code/modules/spells/spell_types/pointed/curtain_fire.dm new file mode 100644 index 00000000000..bc395a823f0 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/curtain_fire.dm @@ -0,0 +1,378 @@ +#define SCORCH_ADAPTATION_DURATION (3 SECONDS) +#define SCORCH_ADAPTATION_KEY "scorch_adaptation" +#define SCORCH_OVERLAY_COLOR rgb(255, 138, 61) +#define SCORCH_BURN_DAMAGE 60 + +/obj/effect/temp_visual/scorch_flash + icon = 'icons/mob/OnFire.dmi' + icon_state = "Generic_mob_burning" + layer = ABOVE_MOB_LAYER + duration = 8 + +/proc/apply_scorch_stack(mob/living/target, stacks = 1, zone_override = null) + if(!isliving(target)) + return + new /obj/effect/temp_visual/scorch_flash(get_turf(target)) + var/final_tier = 0 + for(var/i in 1 to stacks) + if(target.has_status_effect(/datum/status_effect/debuff/scorched4)) + apply_scorch_burn(target, zone_override) + final_tier = 4 + break + if(target.has_status_effect(/datum/status_effect/debuff/scorched3)) + target.remove_status_effect(/datum/status_effect/debuff/scorched3) + target.apply_status_effect(/datum/status_effect/debuff/scorched4) + apply_scorch_burn(target, zone_override) + final_tier = 4 + break + if(target.has_status_effect(/datum/status_effect/debuff/scorched2)) + target.remove_status_effect(/datum/status_effect/debuff/scorched2) + target.apply_status_effect(/datum/status_effect/debuff/scorched3) + final_tier = 3 + continue + if(target.has_status_effect(/datum/status_effect/debuff/scorched1)) + target.remove_status_effect(/datum/status_effect/debuff/scorched1) + target.apply_status_effect(/datum/status_effect/debuff/scorched2) + final_tier = 2 + continue + target.apply_status_effect(/datum/status_effect/debuff/scorched1) + final_tier = max(final_tier, 1) + switch(final_tier) + if(1) + target.balloon_alert_to_viewers("scorched I") + if(2) + target.balloon_alert_to_viewers("scorched II (-1 con)") + if(3) + target.balloon_alert_to_viewers("scorched III (-2 con)") + +/proc/apply_scorch_burn(mob/living/target, zone_override = null) + if(!isliving(target)) + return FALSE + if(target.mob_timers[SCORCH_ADAPTATION_KEY] && world.time < target.mob_timers[SCORCH_ADAPTATION_KEY]) + var/remaining = round((target.mob_timers[SCORCH_ADAPTATION_KEY] - world.time) / 10) + target.balloon_alert_to_viewers("fire adapted ([remaining]s)") + return FALSE + var/target_zone = BODY_ZONE_CHEST + var/mob/living/carbon/carbon_target + if(iscarbon(target)) + carbon_target = target + var/aimed_zone = zone_override ? check_zone(zone_override) : null + if(aimed_zone && carbon_target.get_bodypart(aimed_zone)) + target_zone = aimed_zone + else + var/obj/item/bodypart/most_wounded + for(var/obj/item/bodypart/BP as anything in carbon_target.bodyparts) + if(QDELETED(BP)) + continue + if(!most_wounded || (BP.brute_dam + BP.burn_dam) > (most_wounded.brute_dam + most_wounded.burn_dam)) + most_wounded = BP + if(most_wounded && (most_wounded.brute_dam + most_wounded.burn_dam) > 0) + target_zone = most_wounded.body_zone + target.apply_damage(SCORCH_BURN_DAMAGE, BURN, target_zone, 0) + target.mob_timers[SCORCH_ADAPTATION_KEY] = world.time + SCORCH_ADAPTATION_DURATION + var/hit_zone_name = parse_zone(target_zone) + target.balloon_alert_to_viewers("CHARRED!") + target.visible_message( + span_boldwarning("Flames burn straight through [target]'s armor, searing a wound deep into the [hit_zone_name]!"), + span_userdanger("Flames burn straight through my armor, searing a wound deep into my [hit_zone_name]!")) + playsound(get_turf(target), pick('sound/misc/explode/explosionclose (1).ogg', 'sound/misc/explode/explosionclose (2).ogg', 'sound/misc/explode/explosionclose (3).ogg'), 100, TRUE) + new /obj/effect/temp_visual/fire(get_turf(target)) + new /obj/effect/temp_visual/explosion(get_turf(target)) + return TRUE + +/proc/remove_scorch_stack(mob/living/target) + if(!isliving(target)) + return FALSE + if(target.has_status_effect(/datum/status_effect/debuff/scorched4)) + target.remove_status_effect(/datum/status_effect/debuff/scorched4) + target.apply_status_effect(/datum/status_effect/debuff/scorched3) + return TRUE + if(target.has_status_effect(/datum/status_effect/debuff/scorched3)) + target.remove_status_effect(/datum/status_effect/debuff/scorched3) + target.apply_status_effect(/datum/status_effect/debuff/scorched2) + return TRUE + if(target.has_status_effect(/datum/status_effect/debuff/scorched2)) + target.remove_status_effect(/datum/status_effect/debuff/scorched2) + target.apply_status_effect(/datum/status_effect/debuff/scorched1) + return TRUE + if(target.has_status_effect(/datum/status_effect/debuff/scorched1)) + target.remove_status_effect(/datum/status_effect/debuff/scorched1) + return TRUE + return FALSE + +/proc/has_scorch_stacks(mob/living/target) + if(!isliving(target)) + return FALSE + return get_scorch_stacks(target) > 0 + +/proc/get_scorch_stacks(mob/living/target) + if(!isliving(target)) + return 0 + if(target.has_status_effect(/datum/status_effect/debuff/scorched4)) + return 4 + if(target.has_status_effect(/datum/status_effect/debuff/scorched3)) + return 3 + if(target.has_status_effect(/datum/status_effect/debuff/scorched2)) + return 2 + if(target.has_status_effect(/datum/status_effect/debuff/scorched1)) + return 1 + return 0 + +/proc/remove_all_scorch_stacks(mob/living/target) + if(!isliving(target)) + return + target.remove_status_effect(/datum/status_effect/debuff/scorched4) + target.remove_status_effect(/datum/status_effect/debuff/scorched3) + target.remove_status_effect(/datum/status_effect/debuff/scorched2) + target.remove_status_effect(/datum/status_effect/debuff/scorched1) + +/datum/status_effect/debuff/scorched1 + id = "scorched1" + alert_type = /atom/movable/screen/alert/status_effect/debuff/scorched1 + duration = 25 SECONDS + +/atom/movable/screen/alert/status_effect/debuff/scorched1 + name = "Scorched" + desc = "Flames lick at me, but I can shake this off." + icon_state = "debuff" + +/datum/status_effect/debuff/scorched1/on_apply() + . = ..() + owner.add_atom_colour(SCORCH_OVERLAY_COLOR, TEMPORARY_COLOUR_PRIORITY) + +/datum/status_effect/debuff/scorched1/on_remove() + owner.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, SCORCH_OVERLAY_COLOR) + . = ..() + +/datum/status_effect/debuff/scorched2 + id = "scorched2" + alert_type = /atom/movable/screen/alert/status_effect/debuff/scorched2 + duration = 25 SECONDS + effectedstats = list(STAT_CONSTITUTION = -1) + +/atom/movable/screen/alert/status_effect/debuff/scorched2 + name = "Scorched II" + desc = "The heat saps the vigor from my flesh." + icon_state = "debuff" + +/datum/status_effect/debuff/scorched2/on_apply() + . = ..() + owner.add_atom_colour(SCORCH_OVERLAY_COLOR, TEMPORARY_COLOUR_PRIORITY) + +/datum/status_effect/debuff/scorched2/on_remove() + owner.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, SCORCH_OVERLAY_COLOR) + . = ..() + +/datum/status_effect/debuff/scorched3 + id = "scorched3" + alert_type = /atom/movable/screen/alert/status_effect/debuff/scorched3 + duration = 25 SECONDS + effectedstats = list(STAT_CONSTITUTION = -2) + +/atom/movable/screen/alert/status_effect/debuff/scorched3 + name = "Scorched III" + desc = "The searing burns wrack my body, leaving it frail." + icon_state = "debuff" + +/datum/status_effect/debuff/scorched3/on_apply() + . = ..() + owner.add_atom_colour(SCORCH_OVERLAY_COLOR, TEMPORARY_COLOUR_PRIORITY) + +/datum/status_effect/debuff/scorched3/on_remove() + owner.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, SCORCH_OVERLAY_COLOR) + . = ..() + +/datum/status_effect/debuff/scorched4 + id = "scorched4" + alert_type = /atom/movable/screen/alert/status_effect/debuff/scorched4 + duration = 25 SECONDS + effectedstats = list(STAT_CONSTITUTION = -2) + +/atom/movable/screen/alert/status_effect/debuff/scorched4 + name = "Scorched IV" + desc = "I am utterly consumed by flame - my flesh is searing apart." + icon_state = "debuff" + +/datum/status_effect/debuff/scorched4/on_apply() + . = ..() + owner.add_atom_colour(SCORCH_OVERLAY_COLOR, TEMPORARY_COLOUR_PRIORITY) + +/datum/status_effect/debuff/scorched4/on_remove() + owner.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, SCORCH_OVERLAY_COLOR) + . = ..() + +#undef SCORCH_ADAPTATION_DURATION +#undef SCORCH_ADAPTATION_KEY +#undef SCORCH_OVERLAY_COLOR +#undef SCORCH_BURN_DAMAGE + +#define CURTAIN_TICK_DAMAGE 30 +#define CURTAIN_BURN_KEY "curtain_burn" + +/datum/action/cooldown/spell/fire_curtain + button_icon = 'icons/mob/actions/spells/mage_pyromancy.dmi' + name = "Fire Curtain" + desc = "Conjure a 5x2 curtain of flame at a target location, perpendicular to your facing. \ + After a 2-second telegraph, the fire erupts. Burning for 10 seconds. \ + The fire does not block movement but will burn anything that passes through or stands in it. \ + You are not immune to your own curtain." + button_icon_state = "fire_curtain" + sound = 'sound/magic/fireball.ogg' + + required_technique = TECHNIQUE_CREATION + required_form = FORM_FIRE + + click_to_activate = TRUE + cast_range = SPELL_RANGE_GROUND + self_cast_possible = TRUE + + spell_cost = 50 + + invocation = "Velum Ignis!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = 1 SECONDS + charge_slowdown = CHARGING_SLOWDOWN_MEDIUM + charge_sound = 'sound/magic/charging_fire.ogg' + cooldown_time = 25 SECONDS + + spell_impact_intensity = SPELL_IMPACT_HIGH + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN + + var/curtain_width = 5 + var/curtain_depth = 2 + var/curtain_life = 10 SECONDS + var/telegraph_time = 3 SECONDS + +/datum/action/cooldown/spell/fire_curtain/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/turf/center = get_turf(cast_on) + if(!center) + return FALSE + + var/list/affected_turfs = get_curtain_turfs(center, H.dir) + + for(var/turf/T in affected_turfs) + new /obj/effect/temp_visual/trap_wall/fire(T) + + H.visible_message(span_danger("[H] conjures a wall of flame!")) + playsound(get_turf(H), 'sound/magic/charging_fire.ogg', 60, TRUE) + + addtimer(CALLBACK(src, PROC_REF(spawn_curtain), affected_turfs, H, H.zone_selected), telegraph_time) + return TRUE + +/datum/action/cooldown/spell/fire_curtain/proc/get_curtain_turfs(turf/center, facing) + var/list/row_turfs = list(center) + var/spread_dir1 + var/spread_dir2 + if(facing == NORTH || facing == SOUTH) + spread_dir1 = WEST + spread_dir2 = EAST + else + spread_dir1 = NORTH + spread_dir2 = SOUTH + + var/half = (curtain_width - 1) / 2 + var/turf/current = center + for(var/i in 1 to half) + current = get_step(current, spread_dir1) + if(current) + row_turfs += current + current = center + for(var/i in 1 to half) + current = get_step(current, spread_dir2) + if(current) + row_turfs += current + + // Extend depth along the facing direction + var/list/all_turfs = row_turfs.Copy() + for(var/d in 1 to curtain_depth - 1) + var/list/next_row = list() + for(var/turf/T in row_turfs) + var/turf/deep = get_step(T, facing) + if(deep) + all_turfs |= deep + next_row += deep + row_turfs = next_row + return all_turfs + +/datum/action/cooldown/spell/fire_curtain/proc/spawn_curtain(list/turfs, mob/living/caster, aim_zone) + if(QDELETED(src) || QDELETED(owner)) + return + for(var/turf/T in turfs) + new /obj/effect/curtain_fire(T, curtain_life, caster, aim_zone) + playsound(turfs[1], pick('sound/misc/explode/incendiary (1).ogg', 'sound/misc/explode/incendiary (2).ogg'), 120, TRUE, 6) + +/obj/effect/temp_visual/trap_wall/fire + color = LIGHT_COLOR_FIRE + light_color = LIGHT_COLOR_FIRE + duration = 3 SECONDS + +/obj/effect/curtain_fire + name = "wall of flame" + desc = "A searing curtain of conjured fire." + icon = 'icons/effects/fire.dmi' + icon_state = "3" + anchored = TRUE + density = FALSE + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + light_outer_range = LIGHT_RANGE_FIRE + light_color = LIGHT_COLOR_FIRE + object_slowdown = 15 + var/lifetime = 10 SECONDS + var/tick_damage = CURTAIN_TICK_DAMAGE + var/burn_cooldown = 1 SECONDS + var/datum/weakref/caster_ref + var/aim_zone + +/obj/effect/curtain_fire/Initialize(mapload, life, mob/living/new_caster, aimed_zone) + . = ..() + if(life) + lifetime = life + if(new_caster) + caster_ref = WEAKREF(new_caster) + aim_zone = aimed_zone + START_PROCESSING(SSobj, src) + QDEL_IN(src, lifetime) + +/obj/effect/curtain_fire/Destroy() + STOP_PROCESSING(SSobj, src) + return ..() + +/obj/effect/curtain_fire/Crossed(atom/movable/AM, oldLoc) + . = ..() + if(isliving(AM)) + burn_occupant(AM) + +/obj/effect/curtain_fire/process(seconds_per_tick) + var/turf/T = get_turf(src) + if(!isturf(T)) + return + for(var/mob/living/L in T) + burn_occupant(L) + +/obj/effect/curtain_fire/proc/burn_occupant(mob/living/L) + if(HAS_TRAIT(L, TRAIT_NOFIRE)) + return + if(L.mob_timers[CURTAIN_BURN_KEY] && world.time < L.mob_timers[CURTAIN_BURN_KEY]) + return + L.mob_timers[CURTAIN_BURN_KEY] = world.time + burn_cooldown + var/hit_zone = aim_zone || BODY_ZONE_CHEST + var/mob/living/carbon/human/caster = caster_ref?.resolve() + if(istype(caster) && !QDELETED(caster)) + arcyne_strike(caster, L, null, tick_damage, hit_zone, BCLASS_BURN, spell_name = "Fire Curtain", damage_type = BURN, skip_animation = TRUE, exact_zone = TRUE) + else + var/fallback_zone = check_zone(hit_zone) + var/armor_block = L.run_armor_check(fallback_zone, "fire", blade_dulling = BCLASS_BURN, damage = tick_damage) + L.apply_damage(tick_damage, BURN, fallback_zone, armor_block) + apply_scorch_stack(L, 1, hit_zone) + L.emote("pain", forced = TRUE) + +#undef CURTAIN_TICK_DAMAGE +#undef CURTAIN_BURN_KEY diff --git a/code/modules/spells/spell_types/pointed/decompose.dm b/code/modules/spells/spell_types/pointed/decompose.dm index db10d0c1d68..3ad43e10aac 100644 --- a/code/modules/spells/spell_types/pointed/decompose.dm +++ b/code/modules/spells/spell_types/pointed/decompose.dm @@ -7,12 +7,11 @@ self_cast_possible = FALSE spell_flags = SPELL_RITUOS cast_range = 1 - point_cost = 3 associated_skill = /datum/attribute/skill/magic/blood - attunements = list( - /datum/attunement/death = 0.3, - /datum/attunement/blood = 0.2, - ) + + required_form = FORM_DEATH + required_technique = TECHNIQUE_ALTERATION + invocation = "Return to rot." invocation_type = INVOCATION_WHISPER diff --git a/code/modules/spells/spell_types/pointed/earthshock_fist.dm b/code/modules/spells/spell_types/pointed/earthshock_fist.dm new file mode 100644 index 00000000000..0d77ccfa2bf --- /dev/null +++ b/code/modules/spells/spell_types/pointed/earthshock_fist.dm @@ -0,0 +1,83 @@ +/datum/action/cooldown/spell/earthshock_fist + button_icon = 'icons/mob/actions/spells/spellfist.dmi' + button_icon_state = "fist_of_psydon" + name = "Earthshock Fist" + desc = "Slam your fist downward, sending arcyne force crashing into a 3x3 target area up to 5 paces away. \ + Brief telegraph before the strike lands. Deals blunt damage to the aimed bodypart. \ + At 3+ momentum: consumes 3 to double damage." + cast_range = 5 + + required_form = FORM_EARTH + required_technique = TECHNIQUE_IMBUE + spell_cost = 25 + + invocation = "Idrib!" // https://en.wiktionary.org/wiki/%D8%B6%D8%B1%D8%A8 -- "To strike, to beat, to hit" in Arabic + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_POKE + charge_slowdown = CHARGING_SLOWDOWN_NONE + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 12 SECONDS + + spell_impact_intensity = SPELL_IMPACT_MEDIUM + + sound = list('sound/combat/wooshes/punch/punchwoosh (1).ogg','sound/combat/wooshes/punch/punchwoosh (2).ogg','sound/combat/wooshes/punch/punchwoosh (3).ogg') + spell_tier = 2 + + var/base_damage = 40 + var/empowered_mult = 2 + var/momentum_cost = 3 + var/area_of_effect = 1 // 1-tile radius = 3x3 + var/telegraph_delay = 4 + +/datum/action/cooldown/spell/earthshock_fist/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/turf/T = get_turf(cast_on) + if(!T) + return FALSE + + // Check and consume momentum for empowerment + var/empowered = FALSE + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(M && M.stacks >= momentum_cost) + M.consume_stacks(momentum_cost) + empowered = TRUE + to_chat(H, span_notice("[momentum_cost] momentum released — empowered strike!")) + + var/damage = empowered ? (base_damage * empowered_mult) : base_damage + var/def_zone = H.zone_selected || BODY_ZONE_CHEST + + // Telegraph on 3x3 area + for(var/turf/affected_turf in get_hear(area_of_effect, T)) + new /obj/effect/temp_visual/air_strike_telegraph(affected_turf) + playsound(T, pick('sound/combat/ground_smash1.ogg', 'sound/combat/ground_smash2.ogg', 'sound/combat/ground_smash3.ogg'), 60, TRUE) + H.emote("attackgrunt", forced = TRUE) + + sleep(telegraph_delay) + + if(QDELETED(H) || H.stat == DEAD) + return + var/hit_count = 0 + for(var/turf/affected_turf in get_hear(area_of_effect, T)) + new /obj/effect/temp_visual/kinetic_blast(affected_turf) + for(var/mob/living/victim in affected_turf) + if(victim == H || victim.stat == DEAD) + continue + arcyne_strike(H, victim, null, damage, def_zone, BCLASS_BLUNT, spell_name = "Fist of Psydon") + hit_count++ + + playsound(T, pick('sound/combat/ground_smash1.ogg', 'sound/combat/ground_smash2.ogg', 'sound/combat/ground_smash3.ogg'), 100, TRUE) + H.emote("attack", forced = TRUE) + + if(hit_count) + H.visible_message(span_danger("[H] slams [H.p_their()] fist down, sending a shockwave of arcyne force crashing into the ground!")) + else + H.visible_message(span_notice("[H] slams [H.p_their()] fist down, sending a shockwave into empty ground!")) + + log_combat(H, null, "used Fist of Psydon[empowered ? " (empowered)" : ""]") + return TRUE diff --git a/code/modules/spells/spell_types/pointed/enchantment/_enchantment.dm b/code/modules/spells/spell_types/pointed/enchantment/_enchantment.dm index db3a87a25e1..d32157cba05 100644 --- a/code/modules/spells/spell_types/pointed/enchantment/_enchantment.dm +++ b/code/modules/spells/spell_types/pointed/enchantment/_enchantment.dm @@ -43,6 +43,6 @@ to_chat(owner, span_warning("There is nothing to enchant!")) return if(spell_type != SPELL_MIRACLE) - enchantment_duration *= attuned_strength + enchantment_duration *= spell_magnitude_modifier enchant_item.AddComponent(/datum/component/enchanted_weapon, enchantment_duration, allows_refresh, associated_skill, 1, enchantment, holder) cooldown_time = cooldown_time || enchantment_duration diff --git a/code/modules/spells/spell_types/pointed/enchantment/greenflame_blade.dm b/code/modules/spells/spell_types/pointed/enchantment/greenflame_blade.dm index b0e76a00b2c..1459b8675dc 100644 --- a/code/modules/spells/spell_types/pointed/enchantment/greenflame_blade.dm +++ b/code/modules/spells/spell_types/pointed/enchantment/greenflame_blade.dm @@ -3,12 +3,9 @@ desc = "Enchant a weapon with searing flames." button_icon_state = "enchant_weapon" - point_cost = 1 - attunements = list( - /datum/attunement/fire = 0.3, - ) - charge_required = FALSE spell_cost = 30 spell_flags = SPELL_RITUOS enchantment = SEARING_BLADE_ENCHANT + + required_form = FORM_FIRE diff --git a/code/modules/spells/spell_types/pointed/enchantment/holyflame_blade.dm b/code/modules/spells/spell_types/pointed/enchantment/holyflame_blade.dm index 68798b50ade..6a513b2b2c3 100644 --- a/code/modules/spells/spell_types/pointed/enchantment/holyflame_blade.dm +++ b/code/modules/spells/spell_types/pointed/enchantment/holyflame_blade.dm @@ -4,9 +4,6 @@ button_icon_state = "enchant_weapon" enchantment_duration = 5 MINUTES - attunements = list( - /datum/attunement/fire = 0.3, - ) charge_required = FALSE spell_cost = 30 diff --git a/code/modules/spells/spell_types/pointed/eoran_charm.dm b/code/modules/spells/spell_types/pointed/eoran_charm.dm index fb1f35e52cb..8e3432395bd 100644 --- a/code/modules/spells/spell_types/pointed/eoran_charm.dm +++ b/code/modules/spells/spell_types/pointed/eoran_charm.dm @@ -49,10 +49,6 @@ sound = 'sound/magic/PSY.ogg' charge_sound = 'sound/magic/chargingold.ogg' - attunements = list( - /datum/attunement/blood = 0.5, - ) - charge_time = 3 SECONDS charge_drain = 1 charge_slowdown = 1.3 diff --git a/code/modules/spells/spell_types/pointed/geas.dm b/code/modules/spells/spell_types/pointed/geas.dm new file mode 100644 index 00000000000..8659181d2ca --- /dev/null +++ b/code/modules/spells/spell_types/pointed/geas.dm @@ -0,0 +1,69 @@ +/datum/action/cooldown/spell/geas + button_icon = 'icons/mob/actions/spells/mage_geomancy.dmi' + name = "Geas" + desc = "Lay a geas upon a small patch of ground - roots of stone and arcyne force bind anyone caught there fast for 3 seconds. Can hit yourself." + button_icon_state = "ensnare" + sound = 'sound/magic/webspin.ogg' + click_to_activate = TRUE + cast_range = SPELL_RANGE_GROUND + + spell_cost = 50 + + required_form = FORM_EARTH + + invocation = "Terra Teneat!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = 1 SECONDS + charge_slowdown = CHARGING_SLOWDOWN_MEDIUM + charge_sound = 'sound/magic/charging_fire.ogg' + cooldown_time = 25 SECONDS + + spell_impact_intensity = SPELL_IMPACT_LOW + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN + + var/area_of_effect = 1 + var/hold_duration = 3 SECONDS + var/delay = 0.8 SECONDS + +/datum/action/cooldown/spell/geas/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/turf/T = get_turf(cast_on) + if(!T) + return FALSE + + for(var/turf/affected_turf in get_hear(area_of_effect, T)) + if(affected_turf.density) + continue + new /obj/effect/temp_visual/geas(affected_turf) + + addtimer(CALLBACK(src, PROC_REF(apply_geas), T, H), delay) + playsound(T, 'sound/magic/webspin.ogg', 50, TRUE) + return TRUE + +/datum/action/cooldown/spell/geas/proc/apply_geas(turf/T, mob/living/caster) + for(var/mob/living/simple_animal/hostile/animal in range(area_of_effect, T)) + animal.Paralyze(hold_duration, ignore_canstun = TRUE) + for(var/mob/living/L in range(area_of_effect, T)) + if(L.anti_magic_check()) + L.visible_message(span_warning("The binding can't seem to latch onto [L]!")) + playsound(get_turf(L), 'sound/magic/magic_nulled.ogg', 100) + continue + L.Immobilize(hold_duration) + L.OffBalance(hold_duration) + L.visible_message(span_warning("[L] is bound fast to the earth!")) + new /obj/effect/temp_visual/geas/long(get_turf(L)) + +/obj/effect/temp_visual/geas + icon = 'icons/effects/effects.dmi' + icon_state = "curseblob" + duration = 1 SECONDS + +/obj/effect/temp_visual/geas/long + duration = 3 SECONDS diff --git a/code/modules/spells/spell_types/pointed/gravemark.dm b/code/modules/spells/spell_types/pointed/gravemark.dm index d107a160083..d33c6d84f71 100644 --- a/code/modules/spells/spell_types/pointed/gravemark.dm +++ b/code/modules/spells/spell_types/pointed/gravemark.dm @@ -7,6 +7,8 @@ cast_range = 4 charge_required = FALSE + required_form = FORM_DEATH + /datum/action/cooldown/spell/gravemark/is_valid_target(atom/cast_on) . = ..() if(!.) diff --git a/code/modules/spells/spell_types/pointed/gravity.dm b/code/modules/spells/spell_types/pointed/gravity.dm index 4647845acb0..4c9780b92bc 100644 --- a/code/modules/spells/spell_types/pointed/gravity.dm +++ b/code/modules/spells/spell_types/pointed/gravity.dm @@ -5,10 +5,7 @@ sound = 'sound/magic/gravity.ogg' self_cast_possible = FALSE spell_flags = SPELL_RITUOS - point_cost = 2 - attunements = list( - /datum/attunement/dark = 0.6, - ) + required_form = FORM_AIR //https://www.youtube.com/watch?v=MfK3Ygb7Jzk invocation = "Fall." diff --git a/code/modules/spells/spell_types/pointed/great_shelter.dm b/code/modules/spells/spell_types/pointed/great_shelter.dm new file mode 100644 index 00000000000..aa0459fee35 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/great_shelter.dm @@ -0,0 +1,140 @@ + +#define SHELTER_DURATION 15 MINUTES + +/datum/action/cooldown/spell/great_shelter + name = "Great Shelter" + desc = "Conjure a cramped but functional shelter from arcyne force.\n\ + Contains a bed, a hearth, and an oven. Bring your own cooking tools.\n\ + The shelter lasts for 15 minutes. Door always faces south." + button_icon = 'icons/mob/actions/spells/mage_conjure.dmi' + button_icon_state = "great_shelter" + sound = 'sound/spellbooks/crystal.ogg' + + required_form = FORM_ARCANE + required_technique = TECHNIQUE_CREATION + + click_to_activate = TRUE + cast_range = 5 + self_cast_possible = FALSE + + spell_cost = 150 + + invocation = "Domus Arcana!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = 5 SECONDS + charge_slowdown = CHARGING_SLOWDOWN_HEAVY + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 5 MINUTES + + spell_impact_intensity = SPELL_IMPACT_NONE + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_NO_MOVE | SPELL_REQUIRES_SAME_Z + +/datum/action/cooldown/spell/great_shelter/before_cast(atom/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return . | SPELL_CANCEL_CAST + var/turf/center = get_turf(cast_on) + if(!center) + return . | SPELL_CANCEL_CAST + var/list/offsets = build_shelter_offsets() + for(var/list/offset in offsets) + var/turf/T = locate(center.x + offset[1], center.y + offset[2], center.z) + if(!T || T.density) + to_chat(H, span_warning("There isn't enough space to conjure a shelter here!")) + return . | SPELL_CANCEL_CAST + for(var/obj/structure/S in T) + if(S.density) + to_chat(H, span_warning("There isn't enough space to conjure a shelter here!")) + return . | SPELL_CANCEL_CAST + +/datum/action/cooldown/spell/great_shelter/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/turf/center = get_turf(cast_on) + if(!center) + return FALSE + + var/list/offsets = build_shelter_offsets() + + playsound(center, 'sound/spellbooks/crystal.ogg', 100, TRUE) + H.visible_message(span_warning("[H] conjures a shelter from arcyne force!")) + + // Place structures + for(var/list/offset in offsets) + var/turf/T = locate(center.x + offset[1], center.y + offset[2], center.z) + var/tile_type = offset[3] + switch(tile_type) + if("wall") + new /obj/structure/forcefield_weak/shelter_wall(T, H) + if("bed") + new /obj/structure/bed/conjured(T) + if("hearth") + new /obj/machinery/light/fueled/hearth/conjured(T) + new /obj/machinery/light/fueled/oven/conjured(T) + if("empty") + continue + + return TRUE + +/datum/action/cooldown/spell/great_shelter/proc/build_shelter_offsets() + return list( + list(-1, 2, "wall"), list( 0, 2, "wall"), list( 1, 2, "wall"), list( 2, 2, "wall"), + list(-1, 1, "wall"), list( 0, 1, "bed"), list( 1, 1, "hearth"), list( 2, 1, "wall"), + list(-1, 0, "wall"), list( 0, 0, "empty"), list( 1, 0, "empty"), list( 2, 0, "wall"), + list(-1, -1, "wall"), list( 0, -1, "empty"), list( 1, -1, "wall"), list( 2, -1, "wall"), + ) + +/obj/structure/forcefield_weak/shelter_wall + name = "arcyne wall" + desc = "A shimmering wall of arcyne force. It hums faintly." + max_integrity = 200 + timeleft = 0 + opacity = TRUE + color = "#6495ED" + +/obj/structure/forcefield_weak/shelter_wall/Initialize(mapload, mob/summoner) + . = ..() + QDEL_IN(src, SHELTER_DURATION) + +/obj/structure/forcefield_weak/shelter_wall/CanPass(atom/movable/mover, turf/target) + if(mover == caster) + return TRUE + return ..() + +/obj/structure/bed/conjured + name = "arcyne bed" + desc = "A bed conjured from arcyne force. It looks uncomfortable, but functional." + color = "#6495ED" + +/obj/structure/bed/conjured/Initialize(mapload) + . = ..() + QDEL_IN(src, SHELTER_DURATION) + +/obj/machinery/light/fueled/hearth/conjured + name = "arcyne hearth" + desc = "A hearth of blue arcyne flame. It burns without fuel." + color = "#6495ED" + +/obj/machinery/light/fueled/hearth/conjured/Initialize() + . = ..() + QDEL_IN(src, SHELTER_DURATION) + +/obj/machinery/light/fueled/oven/conjured + name = "arcyne oven" + desc = "An oven conjured from arcyne force. It glows with a faint blue heat." + color = "#6495ED" + +/obj/machinery/light/fueled/oven/conjured/Initialize() + . = ..() + QDEL_IN(src, SHELTER_DURATION) + +#undef SHELTER_DURATION diff --git a/code/modules/spells/spell_types/pointed/ice_wall.dm b/code/modules/spells/spell_types/pointed/ice_wall.dm new file mode 100644 index 00000000000..23579e732cb --- /dev/null +++ b/code/modules/spells/spell_types/pointed/ice_wall.dm @@ -0,0 +1,41 @@ +#define ICEWALL_FROST_PASS_KEY "icewall_frost_pass" + +/datum/action/cooldown/spell/forcewall/ice + button_icon = 'icons/mob/actions/spells/mage_cryomancy.dmi' + name = "Ice Wall" + desc = "Conjure a 5x1 wall of solid ice, blocking anyone and anything from shooting or moving through it. \ + Those caught beside it as it erupts are left frosted. The wall lasts for 20 seconds or until shattered." + button_icon_state = "frost_blast" + sound = 'sound/spellbooks/crystal.ogg' + + required_form = FORM_ICE + required_technique = TECHNIQUE_CREATION + + telegraph_type = /obj/effect/temp_visual/trapice + invocation = "Murus Glaciei!" + +/datum/action/cooldown/spell/forcewall/ice/spawn_wall(turf/target, mob/caster) + new /obj/structure/forcefield_weak/ice(target, caster) + +/obj/structure/forcefield_weak/ice + name = "wall of ice" + desc = "A slab of conjured ice" + icon = 'icons/effects/ice_wall.dmi' + icon_state = "ice_cube" + max_integrity = 120 + break_sound = 'sound/combat/fracture/fracturedry (1).ogg' + +/obj/structure/forcefield_weak/ice/Initialize(mapload, mob/summoner) + . = ..() + frost_adjacent() + +/obj/structure/forcefield_weak/ice/proc/frost_adjacent() + for(var/mob/living/L in range(1, src)) + if(L == caster) + continue + if(L.mob_timers[ICEWALL_FROST_PASS_KEY] == world.time) + continue + L.mob_timers[ICEWALL_FROST_PASS_KEY] = world.time + apply_frost_stack(L) + +#undef ICEWALL_FROST_PASS_KEY diff --git a/code/modules/spells/spell_types/pointed/levinstroke.dm b/code/modules/spells/spell_types/pointed/levinstroke.dm new file mode 100644 index 00000000000..950da04ea0e --- /dev/null +++ b/code/modules/spells/spell_types/pointed/levinstroke.dm @@ -0,0 +1,132 @@ +/datum/action/cooldown/spell/levinstroke + button_icon = 'icons/mob/actions/spells/mage_fulgurmancy.dmi' + name = "Levinstroke" + desc = "Transform yourself briefly into a bolt of lightning toward a target location, inflicting 60 burn damage to any living things in your path." + button_icon_state = "levinstroke" + sound = 'sound/magic/lightning.ogg' + + cast_range = SPELL_RANGE_GROUND + + spell_cost = 40 + + required_form = FORM_LIGHTNING + required_technique = TECHNIQUE_ALTERATION + + invocation = "Fio Fulmen!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_MINOR + charge_slowdown = CHARGING_SLOWDOWN_NONE + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 20 SECONDS + + spell_impact_intensity = SPELL_IMPACT_MEDIUM + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/max_range = 5 + var/strike_damage = 60 + +/datum/action/cooldown/spell/levinstroke/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/turf/start = get_turf(H) + var/turf/dest + + if(isliving(cast_on)) + dest = find_landing_turf(H, cast_on) + else + dest = get_turf(cast_on) + + if(!dest || dest.z != start.z) + to_chat(H, span_warning("Invalid target!")) + return FALSE + + dest = arcyne_find_max_blink_dest(H, dest, max_range) + if(!dest) + to_chat(H, span_warning("I can't surge there!")) + return FALSE + + var/distance = get_dist(start, dest) + if(distance < 1) + to_chat(H, span_warning("I need somewhere to surge to!")) + return FALSE + + var/list/full_path = getline(start, dest) + + var/list/mobs_in_path = list() + for(var/turf/path_turf in full_path) + if(path_turf == start) + continue + for(var/mob/living/M in path_turf) + if(M != H && M.stat != DEAD) + mobs_in_path += M + + create_levin_visual(H, start, dest) + + playsound(start, 'sound/magic/lightning.ogg', 40, TRUE) + H.visible_message( + span_warning("[H] becomes a bolt of thunder!"), + span_notice("I surge forward as living lightning!")) + + if(H.buckled) + H.buckled.unbuckle_mob(H, TRUE) + do_teleport(H, dest, channel = TELEPORT_CHANNEL_MAGIC) + playsound(dest, 'sound/magic/lightning.ogg', 25, TRUE) + + log_combat(H, cast_on, "used Levinstroke on") + + var/locked_zone = H.zone_selected || BODY_ZONE_CHEST + + if(length(mobs_in_path)) + addtimer(CALLBACK(src, PROC_REF(execute_path_strikes), H, mobs_in_path, locked_zone), 5) + + return TRUE + +/datum/action/cooldown/spell/levinstroke/proc/find_landing_turf(mob/living/user, mob/living/target_mob) + var/approach_dir = get_dir(user, target_mob) + var/turf/far_side = get_step(target_mob, approach_dir) + if(far_side && !far_side.density && !istransparentturf(far_side) && isfloorturf(far_side)) + return far_side + return get_turf(target_mob) + +/datum/action/cooldown/spell/levinstroke/proc/execute_path_strikes(mob/living/carbon/human/user, list/victims, def_zone) + if(!user || QDELETED(user)) + return + for(var/mob/living/victim in victims) + if(QDELETED(victim) || victim.stat == DEAD) + continue + if(victim.anti_magic_check()) + victim.visible_message(span_warning("The lightning fades away around [victim]!")) + playsound(get_turf(victim), 'sound/magic/magic_nulled.ogg', 100) + continue + if(ishuman(victim)) + arcyne_strike(user, victim, null, strike_damage, def_zone, BCLASS_BURN, \ + spell_name = "Levinstroke", damage_type = BURN, npc_simple_damage_mult = 1, \ + skip_animation = TRUE) + else + victim.electrocute_act(strike_damage, src, 1, SHOCK_NOSTUN) + victim.electrocute_act(0, src, 1, SHOCK_NOSTUN|SHOCK_VISUAL_ONLY) + new /obj/effect/temp_visual/spell_impact(get_turf(victim), spell_color, spell_impact_intensity) + +/datum/action/cooldown/spell/levinstroke/proc/create_levin_visual(mob/living/carbon/human/user, turf/start, turf/dest) + var/obj/effect/after_image/start_shadow = new(start, 0, 0, 0, 0, 0.5 SECONDS, 2 SECONDS, 0) + start_shadow.name = user.name + start_shadow.appearance = user.appearance + start_shadow.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + start_shadow.dir = user.dir + start_shadow.alpha = 120 + animate(start_shadow, alpha = 0, time = 1.5 SECONDS, easing = LINEAR_EASING) + + var/obj/effect/after_image/end_shadow = new(dest, 0, 0, 0, 0, 0.5 SECONDS, 2 SECONDS, 0) + end_shadow.name = user.name + end_shadow.appearance = user.appearance + end_shadow.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + end_shadow.dir = user.dir + end_shadow.alpha = 120 + animate(end_shadow, alpha = 0, time = 1.5 SECONDS, easing = LINEAR_EASING) + + start_shadow.Beam(end_shadow, icon_state = "lightning[rand(1, 12)]", time = 5) diff --git a/code/modules/spells/spell_types/pointed/magic_stone.dm b/code/modules/spells/spell_types/pointed/magic_stone.dm new file mode 100644 index 00000000000..8abe6e7e56e --- /dev/null +++ b/code/modules/spells/spell_types/pointed/magic_stone.dm @@ -0,0 +1,46 @@ +/datum/action/cooldown/spell/magicians_stone + button_icon = 'icons/mob/actions/spells/mage_geomancy.dmi' + name = "Magician's Stone" + desc = "Tear several stones from the earth itself and materialize them at your feet." + button_icon_state = "magicians_stone" + sound = 'sound/items/stonestone.ogg' + + click_to_activate = FALSE + self_cast_possible = TRUE + + spell_cost = 10 + + invocation = "Emerge, Lapis." + invocation_type = INVOCATION_SHOUT + + required_form = FORM_EARTH + + charge_required = FALSE + cooldown_time = 2 MINUTES + + spell_impact_intensity = SPELL_IMPACT_NONE + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/stone_count_min = 3 + var/stone_count_max = 5 + +/datum/action/cooldown/spell/magicians_stone/cast(atom/cast_on) + . = ..() + var/mob/living/user = owner + if(!istype(user)) + return FALSE + + var/count = rand(stone_count_min, stone_count_max) + var/turf/T = user.drop_location() + var/handed = FALSE + + for(var/i in 1 to count) + var/obj/item/natural/stone/S = new /obj/item/natural/stone(T) + if(!handed && user.put_in_hands(S)) + handed = TRUE + + playsound(user, 'sound/foley/stone_scrape.ogg', 50, TRUE) + owner.visible_message(span_notice("[owner] clenches [owner.p_their()] fist and [count] stones tear themselves from the earth."), span_notice("I tear [count] stones from the earth itself.")) + + return TRUE diff --git a/code/modules/spells/spell_types/pointed/mend_item.dm b/code/modules/spells/spell_types/pointed/mend_item.dm index 7acedee35a5..7af3fd48c74 100644 --- a/code/modules/spells/spell_types/pointed/mend_item.dm +++ b/code/modules/spells/spell_types/pointed/mend_item.dm @@ -1,13 +1,12 @@ /datum/action/cooldown/spell/mend_item name = "Mend Item" desc = "Use arcyne energy to mend an item." - point_cost = 1 sound = 'sound/magic/whiteflame.ogg' self_cast_possible = FALSE spell_flags = SPELL_RITUOS - attunements = list( - /datum/attunement/life = 1.2 - ) + + required_form = FORM_LIFE + required_technique = TECHNIQUE_ALTERATION charge_time = 3 SECONDS cooldown_time = 30 SECONDS diff --git a/code/modules/spells/spell_types/pointed/menhir.dm b/code/modules/spells/spell_types/pointed/menhir.dm new file mode 100644 index 00000000000..315b30018b1 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/menhir.dm @@ -0,0 +1,250 @@ +#define BOULDER_MODE_HEAVE "heave" +#define BOULDER_MODE_DROP "drop" + +/obj/effect/temp_visual/falling_boulder + icon = 'icons/obj/magic_projectiles.dmi' + icon_state = "rock" + name = "falling boulder" + layer = FLY_LAYER + plane = GAME_PLANE_UPPER + randomdir = FALSE + duration = 9 + pixel_z = 270 + var/datum/callback/on_impact + +/obj/effect/temp_visual/falling_boulder/Initialize(mapload, datum/callback/impact_cb) + . = ..() + on_impact = impact_cb + animate(src, pixel_z = 0, time = duration) + addtimer(CALLBACK(src, PROC_REF(do_impact)), duration) + +/obj/effect/temp_visual/falling_boulder/proc/do_impact() + on_impact?.Invoke() + +/obj/effect/temp_visual/trap/meteor + color = "#8B4513" + light_color = "#8B4513" + duration = 3 SECONDS + +/datum/action/cooldown/spell/menhir + button_icon = 'icons/mob/actions/spells/mage_geomancy.dmi' + name = "Menhir" + desc = "Hurl a massive boulder that shatters into stone fragments.\n\ + HEAVE flings it in a flat line at a target - immediate, but stopped by walls and cover.\n\ + DROP calls it down from directly above you.\n\ + A piece of boulder will remain where it lands - you can roll through it with Ramstam to burst it.\n\ + Use Shift-G to switch." + button_icon_state = "boulder_strike" + sound = 'sound/combat/hits/onstone/stonedeath.ogg' + + click_to_activate = TRUE + cast_range = SPELL_RANGE_PROJECTILE + + required_form = FORM_EARTH + required_technique = TECHNIQUE_DESTRUCTION + + spell_cost = 40 + + invocation = "Saxum Ruinae!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_MAJOR + charge_slowdown = CHARGING_SLOWDOWN_MEDIUM + charge_sound = 'sound/magic/charging_fire.ogg' + cooldown_time = 15 SECONDS + + spell_impact_intensity = SPELL_IMPACT_HIGH + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/boulder_mode = BOULDER_MODE_HEAVE + var/heave_damage = 90 + var/drop_damage = 90 + var/drop_splash = 35 + var/drop_telegraph_time = 3 + var/frag_count = 8 + var/frag_damage = 15 + var/pillar_lifetime = 15 SECONDS + var/static/list/mode_labels = list(BOULDER_MODE_HEAVE = "HEAVE", BOULDER_MODE_DROP = "DROP") + +/datum/action/cooldown/spell/menhir/Grant(mob/grant_to) + . = ..() + update_mode_maptext() + +/datum/action/cooldown/spell/menhir/toggle_alt_mode(mob/user) + boulder_mode = (boulder_mode == BOULDER_MODE_HEAVE) ? BOULDER_MODE_DROP : BOULDER_MODE_HEAVE + to_chat(user, span_notice("Menhir set to: [mode_labels[boulder_mode]].")) + update_mode_maptext() + return TRUE + +/datum/action/cooldown/spell/menhir/proc/update_mode_maptext() + var/label = mode_labels[boulder_mode] + for(var/datum/hud/hud as anything in viewers) + var/atom/movable/screen/movable/action_button/B = viewers[hud] + var/atom/movable/screen/arc_maptext_holder/holder + for(var/atom/movable/screen/arc_maptext_holder/existing in B.vis_contents) + holder = existing + break + if(!holder) + holder = new(B) + B.vis_contents.Add(holder) + holder.maptext = MAPTEXT(label) + holder.maptext_x = 5 + holder.color = GLOB.form_colors[FORM_EARTH] + +/datum/action/cooldown/spell/menhir/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + if(boulder_mode == BOULDER_MODE_DROP) + var/turf/target = get_turf(cast_on) + if(!target) + return FALSE + do_drop(H, target) + else + do_heave(H, cast_on) + return TRUE + +/datum/action/cooldown/spell/menhir/proc/do_heave(mob/living/carbon/human/H, atom/aim) + var/turf/origin = get_turf(H) + if(!origin || !aim) + return + playsound(origin, 'sound/combat/hits/onstone/stonedeath.ogg', 80, TRUE) + var/obj/projectile/magic/boulder/B = new(origin) + B.damage = heave_damage + B.frag_count = frag_count + B.frag_damage = frag_damage + B.pillar_lifetime = pillar_lifetime + B.firer = H + B.preparePixelProjectile(aim, H) + B.fire() + +/datum/action/cooldown/spell/menhir/proc/do_drop(mob/living/carbon/human/H, turf/target) + new /obj/effect/temp_visual/trap/geomancy(target) + target.visible_message(span_boldwarning("A shadow spreads over [target] - a boulder plummets from the sky!")) + playsound(target, 'sound/combat/wooshes/blunt/wooshhuge (2).ogg', 60, TRUE) + addtimer(CALLBACK(src, PROC_REF(drop_fall), target, H), drop_telegraph_time) + +/datum/action/cooldown/spell/menhir/proc/drop_fall(turf/target, mob/living/carbon/human/H) + if(QDELETED(target)) + return + new /obj/effect/temp_visual/falling_boulder(target, CALLBACK(src, PROC_REF(do_drop_hit), target, H)) + +/datum/action/cooldown/spell/menhir/proc/do_drop_hit(turf/target, mob/living/carbon/human/H) + if(QDELETED(target)) + return + playsound(target, 'sound/combat/hits/onstone/stonedeath.ogg', 100, TRUE, 5) + new /obj/effect/temp_visual/spell_impact(target, spell_color, SPELL_IMPACT_HIGH) + for(var/obj/structure/S in target) + S.take_damage(drop_damage, BRUTE, "blunt") + for(var/mob/living/L in target) + strike_mob(H, L, drop_damage) + for(var/turf/T in range(1, target)) + if(T == target) + continue + for(var/mob/living/L in T) + strike_mob(H, L, drop_splash) + fragment_burst(target, H) + leave_pillar(target, H) + +/datum/action/cooldown/spell/menhir/proc/leave_pillar(turf/T, mob/living/carbon/human/H) + if(!T || T.density) + return + if(locate(/obj/structure/earthen_pillar) in T) + return + for(var/obj/structure/S in T) + if(S.density) + return + var/obj/structure/earthen_pillar/pillar = new(T) + pillar.caster_ref = WEAKREF(H) + QDEL_IN(pillar, pillar_lifetime) + +/datum/action/cooldown/spell/menhir/proc/strike_mob(mob/living/carbon/human/H, mob/living/L, dmg) + if(L == H || QDELETED(L)) + return + if(L.anti_magic_check()) + return + if(ishuman(L)) + arcyne_strike(H, L, null, dmg, H.zone_selected || BODY_ZONE_CHEST, BCLASS_BLUNT, \ + spell_name = name, damage_type = BRUTE, npc_simple_damage_mult = 2, skip_animation = TRUE) + else + L.adjustBruteLoss(dmg * (L.mind ? 1 : 2)) + new /obj/effect/temp_visual/spell_impact(get_turf(L), spell_color, spell_impact_intensity) + +/datum/action/cooldown/spell/menhir/proc/fragment_burst(turf/T, mob/living/carbon/human/H) + if(!T) + return + var/static/list/burst_dirs = list(NORTH, SOUTH, EAST, WEST, NORTHEAST, NORTHWEST, SOUTHEAST, SOUTHWEST) + for(var/i in 1 to frag_count) + var/fdir = pick(burst_dirs) + var/turf/ftarget = get_ranged_target_turf(T, fdir, 3) + var/obj/projectile/magic/gravel_blast/frag = new(T) + frag.damage = frag_damage + frag.range = 3 + frag.ricochets_max = 0 + frag.firer = H + frag.preparePixelProjectile(ftarget, T) + frag.fire() + +/obj/projectile/magic/boulder + name = "boulder" + icon = 'icons/obj/magic_projectiles.dmi' + icon_state = "boulder" + damage = 100 + nodamage = FALSE + damage_type = BRUTE + woundclass = BCLASS_BLUNT + flag = "blunt" + range = SPELL_RANGE_PROJECTILE + speed = 3.5 + accuracy = 30 + hitsound = 'sound/combat/hits/onstone/stonedeath.ogg' + var/frag_count = 8 + var/frag_damage = 15 + var/pillar_lifetime = 0 + +/obj/projectile/magic/boulder/on_hit(target) + . = ..() + if(out_of_effective_range()) + return + var/turf/impact = get_turf(src) + if(!impact) + return + for(var/i in 1 to frag_count) + var/obj/projectile/magic/gravel_blast/frag = new(impact) + frag.damage = frag_damage + frag.range = 3 + frag.ricochets_max = 0 + frag.ricochet_chance = 0 + frag.firer = firer + frag.name = "gravel fragment" + var/angle = rand(0, 359) + frag.fire(angle) + playsound(impact, 'sound/combat/hits/onstone/stonedeath.ogg', 100, TRUE, 5) + if(pillar_lifetime > 0 && !impact.density && !(locate(/obj/structure/earthen_pillar) in impact)) + var/blocked = FALSE + for(var/obj/structure/S in impact) + if(S.density) + blocked = TRUE + break + if(!blocked) + var/obj/structure/earthen_pillar/pillar = new(impact) + pillar.caster_ref = WEAKREF(firer) + QDEL_IN(pillar, pillar_lifetime) + +/obj/projectile/magic/boulder/Bump(atom/A) + if(ismob(A)) + var/mob/living/M = A + if(M.anti_magic_check()) + visible_message(span_warning("[src] shatters harmlessly against [M]!")) + playsound(get_turf(M), 'sound/magic/magic_nulled.ogg', 100) + qdel(src) + return + if(M == firer) + damage = round(damage / 2) + return ..() + +#undef BOULDER_MODE_HEAVE +#undef BOULDER_MODE_DROP diff --git a/code/modules/spells/spell_types/pointed/mimicry.dm b/code/modules/spells/spell_types/pointed/mimicry.dm index b9b15b36106..6e85fa83642 100644 --- a/code/modules/spells/spell_types/pointed/mimicry.dm +++ b/code/modules/spells/spell_types/pointed/mimicry.dm @@ -4,10 +4,9 @@ button_icon_state = "invisibility" sound = 'sound/misc/stings/generic.ogg' self_cast_possible = FALSE - attunements = list( - /datum/attunement/dark = 0.4, - /datum/attunement/polymorph = 1.2, - ) + + required_technique = TECHNIQUE_ILLUSION + required_form = FORM_ARCANE charge_time = 4 SECONDS charge_drain = 1 diff --git a/code/modules/spells/spell_types/pointed/mind_spike.dm b/code/modules/spells/spell_types/pointed/mind_spike.dm index 6f34876aa68..dcfbd40ebd8 100644 --- a/code/modules/spells/spell_types/pointed/mind_spike.dm +++ b/code/modules/spells/spell_types/pointed/mind_spike.dm @@ -4,10 +4,7 @@ sound = 'sound/magic/whiteflame.ogg' self_cast_possible = FALSE - point_cost = 1 - attunements = list( - /datum/attunement/dark = 0.5, - ) + required_form = FORM_DEATH invocation = "Suffer." invocation_type = INVOCATION_SHOUT diff --git a/code/modules/spells/spell_types/pointed/mindforged_blade.dm b/code/modules/spells/spell_types/pointed/mindforged_blade.dm new file mode 100644 index 00000000000..2767d5a08e0 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/mindforged_blade.dm @@ -0,0 +1,81 @@ +/datum/action/cooldown/spell/mindforged_blade + button_icon = 'icons/mob/actions/spells/spellfist.dmi' + name = "Mindforged Blade" + desc = "The manifestation of the higher concept of a blade itself. each casting a poor facsimile of the perfect weapon They hold." + button_icon_state = "boundkatar" + + click_to_activate = FALSE + self_cast_possible = TRUE + + required_form = FORM_EARTH + required_technique = TECHNIQUE_IMBUE + + spell_cost = 30 + + charge_required = FALSE + cooldown_time = 1 SECONDS + + spell_impact_intensity = SPELL_IMPACT_NONE + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/drawmessage = "I imagine the perfect weapon, forged by arcyne knowledge, it's edge flawless. \ + I feel it in my mind's eye -- but it's just out of reach. I pull away it's shadow, a bad copy, and yet it is one of a great weapon nonetheless... " + var/dropmessage = "Letting go, I watch the blade lose it's form, unable to stay stable without my energy rooting it to this world..." + var/obj/item/melee/touch_attack/weapon/mindforged_katar/summoned_blade + +/datum/action/cooldown/spell/mindforged_blade/Destroy() + if(summoned_blade && !QDELETED(summoned_blade)) + UnregisterSignal(summoned_blade, COMSIG_QDELETING) + QDEL_NULL(summoned_blade) + return ..() + +/datum/action/cooldown/spell/mindforged_blade/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/user = owner + if(!istype(user)) + return FALSE + + if(summoned_blade && !QDELETED(summoned_blade)) + qdel(summoned_blade) + to_chat(user, span_notice("[dropmessage]")) + return FALSE + + summoned_blade = new(user) + summoned_blade.spell_which_made_us = null + RegisterSignal(summoned_blade, COMSIG_QDELETING, PROC_REF(on_blade_destroyed)) + if(!user.put_in_hands(summoned_blade)) + qdel(summoned_blade) + if(user.usable_hands <= 0) + to_chat(user, span_warning("I don't have any usable hands!")) + else + to_chat(user, span_warning("My hands are full!")) + return FALSE + to_chat(user, span_notice("[drawmessage]")) + return TRUE + +/datum/action/cooldown/spell/mindforged_blade/proc/on_blade_destroyed(datum/source) + SIGNAL_HANDLER + summoned_blade = null + +/obj/item/melee/touch_attack/weapon/mindforged_katar + name = "\improper mindforged katar" + desc = "This blade throbs, translucent and iridiscent, blueish arcyne energies running through its translucent surface..." + icon = 'icons/mob/actions/roguespells.dmi' + icon_state = "katar_bound" + force = 24 + possible_item_intents = list(/datum/intent/katar/cut, /datum/intent/katar/thrust) + gripsprite = FALSE + wlength = WLENGTH_SHORT + w_class = WEIGHT_CLASS_HUGE + parrysound = list('sound/combat/parry/bladed/bladedsmall (1).ogg','sound/combat/parry/bladed/bladedsmall (2).ogg','sound/combat/parry/bladed/bladedsmall (3).ogg') + max_blade_int = 999 + max_integrity = 50 + swingsound = list('sound/combat/wooshes/bladed/wooshsmall (1).ogg','sound/combat/wooshes/bladed/wooshsmall (2).ogg','sound/combat/wooshes/bladed/wooshsmall (3).ogg') + associated_skill = /datum/attribute/skill/combat/unarmed + pickup_sound = 'sound/foley/equip/swordsmall2.ogg' + wdefense = 0 + can_parry = TRUE + +/obj/item/melee/touch_attack/weapon/mindforged_katar/attack_self() + qdel(src) diff --git a/code/modules/spells/spell_types/pointed/misty_step.dm b/code/modules/spells/spell_types/pointed/misty_step.dm new file mode 100644 index 00000000000..75bb1b59ab3 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/misty_step.dm @@ -0,0 +1,93 @@ +/datum/action/cooldown/spell/misty_step + name = "Misty Step" + desc = "Teleport to a targeted location within your field of view. Limited to a range of 5 tiles. Only works on the same plane as the caster." + button_icon = 'icons/mob/actions/roguespells.dmi' + button_icon_state = "rune6" + sound = 'sound/magic/blink.ogg' + + click_to_activate = TRUE + self_cast_possible = FALSE + + spell_cost = 40 + + required_form = FORM_WATER + required_technique = TECHNIQUE_ALTERATION + + invocation = "Saltus Arcanus!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_POKE + charge_slowdown = CHARGING_SLOWDOWN_SMALL + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 24 SECONDS + + spell_impact_intensity = SPELL_IMPACT_NONE + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/max_range = 5 + var/phase = /obj/effect/temp_visual/blink + var/phase_sound = 'sound/magic/blink.ogg' + var/phase_beam = "purple_lightning" + +/obj/effect/temp_visual/blink + icon = 'icons/effects/effects.dmi' + icon_state = "hierophant_blast" + name = "teleportation magic" + desc = "Get out of the way!" + randomdir = FALSE + duration = 4 SECONDS + light_outer_range = 2 + light_color = COLOR_PALE_PURPLE_GRAY + +/obj/effect/temp_visual/blink/Initialize(mapload, new_caster) + . = ..() + +/datum/action/cooldown/spell/misty_step/cast(atom/cast_on) + . = ..() + var/turf/T = get_turf(cast_on) + var/turf/start = get_turf(owner) + + var/dest_err = arcyne_validate_blink_dest(T, owner) + if(dest_err) + to_chat(owner, span_warning(dest_err)) + return FALSE + + var/distance = get_dist(start, T) + if(distance > max_range) + to_chat(owner, span_warning("That location is too far away! I can only blink up to [max_range] tiles.")) + return FALSE + + var/path_err = arcyne_validate_blink_path(start, T) + if(path_err) + to_chat(owner, span_warning(path_err)) + return FALSE + + owner.visible_message(span_warning("[owner]'s body begins to shimmer with arcane energy as [owner.p_they()] prepare[owner.p_s()] to blink!"), + span_notice("I focus my arcane energy, preparing to blink across space!")) + + var/obj/spot_one = new phase(start, owner.dir) + var/obj/spot_two = new phase(T, owner.dir) + + if(phase_beam) + spot_one.Beam(spot_two, phase_beam, time = 1.5 SECONDS) + playsound(start, phase_sound, 65, TRUE) + playsound(T, phase_sound, 25, TRUE) + + var/mob/living/L = owner + if(istype(L) && L.buckled) + L.buckled.unbuckle_mob(L, TRUE) + + // Afterimage at departure point + var/obj/effect/after_image/img = new(start, 0, 0, 0, 0, 0.5 SECONDS, 2 SECONDS, 0) + img.name = owner.name + img.appearance = owner.appearance + img.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + img.alpha = 120 + animate(img, alpha = 0, time = 1.5 SECONDS, easing = LINEAR_EASING) + QDEL_IN(img, 1.5 SECONDS) + + do_teleport(owner, T, channel = TELEPORT_CHANNEL_MAGIC) + + owner.visible_message(span_danger("[owner] vanishes in a mysterious purple flash!"), span_notice("I blink through space in an instant!")) + return TRUE diff --git a/code/modules/spells/spell_types/pointed/numbing_pleasure.dm b/code/modules/spells/spell_types/pointed/numbing_pleasure.dm index d4698d885e5..630a3158974 100644 --- a/code/modules/spells/spell_types/pointed/numbing_pleasure.dm +++ b/code/modules/spells/spell_types/pointed/numbing_pleasure.dm @@ -10,10 +10,7 @@ invocation = "May you find bliss through your pain." invocation_type = INVOCATION_WHISPER - attunements = list( - /datum/attunement/electric = 0.3, - /datum/attunement/aeromancy = 0.3, - ) + required_form = FORM_AIR charge_required = FALSE cooldown_time = 2 MINUTES diff --git a/code/modules/spells/spell_types/pointed/ore_vision.dm b/code/modules/spells/spell_types/pointed/ore_vision.dm new file mode 100644 index 00000000000..1b6ab8d5df4 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/ore_vision.dm @@ -0,0 +1,20 @@ +/datum/action/cooldown/spell/gem_detect + name = "Ore Sight" + desc = "Reveals the location of precious stones and crystals nearby." + button_icon_state = "aros" + button_icon = 'icons/roguetown/items/gems.dmi' + spell_cost = 30 + required_form = FORM_EARTH + +/datum/action/cooldown/spell/gem_detect/is_valid_target(atom/cast_on) + . = ..() + if(!.) + return FALSE + return ishuman(cast_on) + +/datum/action/cooldown/spell/gem_detect/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human = cast_on + human.set_oresight(TRUE) + + addtimer(CALLBACK(human, TYPE_PROC_REF(/mob/living, set_oresight), FALSE), 5 MINUTES) diff --git a/code/modules/spells/spell_types/pointed/projectile/_projectile.dm b/code/modules/spells/spell_types/pointed/projectile/_projectile.dm index 1665a354121..3d2d08d54b9 100644 --- a/code/modules/spells/spell_types/pointed/projectile/_projectile.dm +++ b/code/modules/spells/spell_types/pointed/projectile/_projectile.dm @@ -12,11 +12,14 @@ * These REQUIRE click_to_activate to be true */ /datum/action/cooldown/spell/projectile + abstract_type = /datum/action/cooldown/spell/projectile self_cast_possible = FALSE experience_modifier = 0.3 // More earned when hitting a target /// What projectile we create when we shoot our spell. var/obj/projectile/magic/projectile_type = /obj/projectile/magic/teleport + ///if we are in arc mode what projectile to use + var/obj/projectile/magic/projectile_type_arc /// How many projectiles we can fire per cast. Not all at once, per click, kinda like charges var/projectile_amount = 1 /// How many projectiles we have yet to fire, based on projectile_amount @@ -24,6 +27,8 @@ /// How many projectiles we fire every fire_projectile() call. /// Unwise to change without overriding or extending ready_projectile. var/projectiles_per_fire = 1 + /// Whether this spell is currently set to fire in arc mode. + var/arc_mode = FALSE /datum/action/cooldown/spell/projectile/New(Target) . = ..() @@ -66,10 +71,11 @@ // Reset our cooldown and let them fire away reset_spell_cooldown() +/// Fire the projectile(s) at the target. /datum/action/cooldown/spell/projectile/proc/fire_projectile(atom/target) - current_amount-- for(var/i in 1 to projectiles_per_fire) - var/obj/projectile/to_fire = new projectile_type() + var/active_type = (arc_mode && projectile_type_arc) ? projectile_type_arc : projectile_type + var/obj/projectile/to_fire = new active_type(owner.loc) ready_projectile(to_fire, target, owner, i) to_fire.fire() return TRUE @@ -77,7 +83,7 @@ /datum/action/cooldown/spell/projectile/proc/ready_projectile(obj/projectile/to_fire, atom/target, mob/user, iteration) to_fire.firer = owner to_fire.fired_from = get_turf(owner) - to_fire.scale = clamp(attuned_strength, 0.5, 1.5) + to_fire.scale = clamp(spell_magnitude_modifier, 0.5, 1.5) to_fire.preparePixelProjectile(target, owner) RegisterSignal(to_fire, COMSIG_PROJECTILE_SELF_ON_HIT, PROC_REF(on_cast_hit)) @@ -85,6 +91,8 @@ if(istype(to_fire, /obj/projectile/magic)) var/obj/projectile/magic/magic_to_fire = to_fire magic_to_fire.antimagic_flags = antimagic_flags + magic_to_fire.spell_impact_intensity = spell_impact_intensity + magic_to_fire.spell_impact_color = spell_impact_color /// Signal proc for whenever the projectile we fire hits someone. /// Pretty much relays to the spell when the projectile actually hits something. @@ -104,3 +112,32 @@ return handle_exp(get_adjusted_cost() / 4) + +/// Toggle arc mode on this spell. Only works if projectile_type_arc is set. +/datum/action/cooldown/spell/projectile/proc/toggle_arc_mode(mob/user) + if(!projectile_type_arc) + to_chat(user, span_warning("[name] cannot be arced.")) + return + arc_mode = !arc_mode + if(arc_mode) + to_chat(user, span_notice("[name] arc mode enabled - aim above or below to lob the shot across elevations.")) + else + to_chat(user, span_notice("[name] arc mode disabled.")) + update_arc_maptext() + +/// Updates the ARC maptext indicator on the spell's action button. +/datum/action/cooldown/spell/projectile/proc/update_arc_maptext() + for(var/datum/hud/hud as anything in viewers) + var/atom/movable/screen/movable/action_button/B = viewers[hud] + var/atom/movable/screen/arc_maptext_holder/arc_holder + for(var/atom/movable/screen/arc_maptext_holder/existing in B.vis_contents) + arc_holder = existing + break + if(!arc_holder) + arc_holder = new(B) + B.vis_contents.Add(arc_holder) + if(arc_mode) + arc_holder.maptext = MAPTEXT("ARC") + arc_holder.color = "#00ccff" + else + arc_holder.maptext = null diff --git a/code/modules/spells/spell_types/pointed/projectile/acid_splash.dm b/code/modules/spells/spell_types/pointed/projectile/acid_splash.dm index 8904375d4fd..1f29fdbe336 100644 --- a/code/modules/spells/spell_types/pointed/projectile/acid_splash.dm +++ b/code/modules/spells/spell_types/pointed/projectile/acid_splash.dm @@ -4,11 +4,8 @@ button_icon_state = "acidsplash" sound = 'sound/magic/whiteflame.ogg' - point_cost = 1 - attunements = list( - /datum/attunement/blood = 0.3, - /datum/attunement/death = 0.4, - ) + required_form = FORM_WATER + required_technique = TECHNIQUE_DESTRUCTION invocation = "HYDRO MURIATIC!!!!" invocation_type = INVOCATION_SHOUT @@ -23,9 +20,9 @@ /datum/action/cooldown/spell/projectile/acid_splash/ready_projectile(obj/projectile/magic/acidsplash/to_fire, atom/target, mob/user, iteration) . = ..() - to_fire.damage *= attuned_strength - to_fire.aoe_range *= attuned_strength - to_fire.strength_modifier *= attuned_strength + to_fire.damage *= spell_magnitude_modifier + to_fire.aoe_range *= spell_magnitude_modifier + to_fire.strength_modifier *= spell_magnitude_modifier /datum/action/cooldown/spell/projectile/acid_splash/quietus name = "Caustic Splash" @@ -38,6 +35,8 @@ sound = 'sound/vo/vomit.ogg' charge_sound = null + required_form = null + associated_skill = null invocation_type = INVOCATION_EMOTE diff --git a/code/modules/spells/spell_types/pointed/projectile/arc.dm b/code/modules/spells/spell_types/pointed/projectile/arc.dm new file mode 100644 index 00000000000..9c1bfce2e90 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/projectile/arc.dm @@ -0,0 +1,172 @@ +#define ARC_BOLT_JUMPS 3 +#define ARC_BOLT_JUMP_RANGE 4 +#define ARC_BOLT_ARC_MULT 0.5 + +/datum/action/cooldown/spell/projectile/arc_bolt + button_icon = 'icons/mob/actions/spells/mage_fulgurmancy.dmi' + name = "Arc Bolt" + desc = "Fire a precise jolt of lightning that sears a target's struck body part with heavy burn. \ + Toggle firing mode (Shift+G) while active: Chain leaps to up to three foes behind the target, searing the same body part for half as much, \ + Focus only strike one target, and Arc lobs a single weakened bolt over obstacles. \ + The bolt loses power past 5 paces." + button_icon_state = "shock" + sound = 'sound/magic/lightning.ogg' + + projectile_type = /obj/projectile/magic/arc_bolt + cast_range = SPELL_RANGE_PROJECTILE + + spell_cost = 20 + + required_form = FORM_LIGHTNING + required_technique = TECHNIQUE_DESTRUCTION + + invocation = "Fulgur!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_POKE + charge_slowdown = CHARGING_SLOWDOWN_SMALL + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 6.5 SECONDS + + spell_impact_intensity = SPELL_IMPACT_LOW + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN + + var/current_mode = 1 + var/list/modes = list( + list("name" = "Chain", "tag" = "CHAIN", "proj" = /obj/projectile/magic/arc_bolt, "invocation" = "Fulgur!"), + list("name" = "Focus", "tag" = "FOCUS", "proj" = /obj/projectile/magic/arc_bolt/single, "invocation" = "Fulgur Singularis!"), + list("name" = "Arc", "tag" = "ARC", "proj" = /obj/projectile/magic/arc_bolt/single/arc, "invocation" = "Fulgur Arcus!"), + ) + +/datum/action/cooldown/spell/projectile/arc_bolt/Grant(mob/grant_to) + . = ..() + apply_mode(current_mode) + +/datum/action/cooldown/spell/projectile/arc_bolt/proc/apply_mode(index) + var/list/mode = modes[index] + projectile_type = mode["proj"] + invocation = mode["invocation"] + update_mode_maptext(mode["tag"]) + +/datum/action/cooldown/spell/projectile/arc_bolt/toggle_arc_mode(mob/user) + current_mode = (current_mode % length(modes)) + 1 + apply_mode(current_mode) + to_chat(user, span_notice("[name]: [modes[current_mode]["name"]] mode.")) + +/datum/action/cooldown/spell/projectile/arc_bolt/proc/update_mode_maptext(tag) + for(var/datum/hud/hud as anything in viewers) + var/atom/movable/screen/movable/action_button/B = viewers[hud] + var/atom/movable/screen/arc_maptext_holder/holder + for(var/atom/movable/screen/arc_maptext_holder/existing in B.vis_contents) + holder = existing + break + if(!holder) + holder = new(B) + B.vis_contents.Add(holder) + holder.maptext = MAPTEXT(tag) + holder.color = "#00ccff" + +/obj/projectile/magic/arc_bolt + name = "arc bolt" + tracer_type = /obj/effect/projectile/tracer/wormhole + impact_type = null + hitscan = TRUE + damage = 60 + max_range = 12 + damage_type = BURN + woundclass = BCLASS_BURN + nodamage = FALSE + speed = 0.3 + flag = "fire" + light_outer_range = 5 + var/arcs = TRUE + +/obj/projectile/magic/arc_bolt/single + name = "focused bolt" + damage = 60 + arcs = FALSE + +/obj/projectile/magic/arc_bolt/single/arc + name = "arced bolt" + damage = 45 + arcshot = TRUE + + +/obj/projectile/magic/arc_bolt/on_hit(target) + . = ..() + if(ismob(target)) + var/mob/M = target + if(M.anti_magic_check()) + visible_message(span_warning("[src] fizzles on contact with [target]!")) + playsound(get_turf(target), 'sound/magic/magic_nulled.ogg', 100) + qdel(src) + return BULLET_ACT_BLOCK + if(isliving(target)) + var/mob/living/L = target + if(out_of_effective_range()) + qdel(src) + return + L.electrocute_act(1, src, 1, SHOCK_NOSTUN) + if(arcs) + arc_to_targets(L) + else if(isatom(target)) + var/atom/A = target + A.fire_act() + qdel(src) + +/obj/projectile/magic/arc_bolt/proc/arc_to_targets(mob/living/primary) + var/turf/origin = fired_from || (firer ? get_turf(firer) : get_turf(primary)) + var/list/already_hit = list(primary) + if(firer) + already_hit += firer + var/arc_damage = round(damage * ARC_BOLT_ARC_MULT) + var/mob/living/current = primary + for(var/i in 1 to ARC_BOLT_JUMPS) + var/mob/living/next = pick_arc_target(current, origin, already_hit) + if(!next) + break + already_hit += next + current.Beam(next, icon_state = "lightning[rand(1, 12)]", time = 5) + playsound(get_turf(next), 'sound/magic/lightning.ogg', 40) + zap_arc_target(next, arc_damage) + current = next + +/obj/projectile/magic/arc_bolt/proc/pick_arc_target(mob/living/from, turf/origin, list/already_hit) + var/from_dist = origin ? get_dist(origin, from) : 0 + var/mob/living/best + var/best_dist = ARC_BOLT_JUMP_RANGE + 1 + for(var/mob/living/candidate in view(ARC_BOLT_JUMP_RANGE, from)) + if(candidate in already_hit) + continue + if(candidate.stat == DEAD) + continue + if(origin && get_dist(origin, candidate) < from_dist) + continue + var/candidate_dist = get_dist(from, candidate) + if(candidate_dist < best_dist) + best_dist = candidate_dist + best = candidate + return best + +/obj/projectile/magic/arc_bolt/proc/zap_arc_target(mob/living/L, arc_damage) + if(L.anti_magic_check()) + L.visible_message(span_warning("The arc scatters around [L]!")) + return + var/actual_damage = arc_damage + var/mob/living/carbon/human/caster = firer + if(istype(caster) && ishuman(L)) + arcyne_strike(caster, L, null, actual_damage, def_zone, BCLASS_BURN, \ + spell_name = "Arc Bolt", damage_type = BURN,\ + skip_animation = TRUE) + else + L.electrocute_act(actual_damage, src, 1, SHOCK_NOSTUN) + L.electrocute_act(0, src, 1, SHOCK_NOSTUN|SHOCK_VISUAL_ONLY) + if(spell_impact_intensity > SPELL_IMPACT_NONE) + var/impact_color = spell_impact_color || light_color || "#FFFFFF" + new /obj/effect/temp_visual/spell_impact(get_turf(L), impact_color, spell_impact_intensity) + +#undef ARC_BOLT_JUMPS +#undef ARC_BOLT_JUMP_RANGE +#undef ARC_BOLT_ARC_MULT diff --git a/code/modules/spells/spell_types/pointed/projectile/arcyne_barrage.dm b/code/modules/spells/spell_types/pointed/projectile/arcyne_barrage.dm new file mode 100644 index 00000000000..25c6aabb640 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/projectile/arcyne_barrage.dm @@ -0,0 +1,148 @@ +#define ARCYNE_BARRAGE_SLOWDOWN_ID "arcyne_barrage_channel" + +/datum/action/cooldown/spell/projectile/arcyne_barrage + button_icon = 'icons/mob/actions/spells/mage_telomancy.dmi' + name = "Arcyne Barrage" + desc = "Barrage a direction with pulses of piercing, ricocheting arcyne bolts for ten seconds. The bolts can pierce up to five targets. \ + You can walk while channeling but you are greatly slowed down and cannot change your direction. \ + Being stunned, knocked down, or grabbed will break the channeling." + button_icon_state = "arcyne_barrage" + sound = 'sound/magic/vlightning.ogg' + + click_to_activate = TRUE + cast_range = 21 + + required_form = FORM_ARCANE + required_technique = TECHNIQUE_DESTRUCTION + + invocation_type = INVOCATION_NONE + var/barrage_invocation = "Ultima Ratio Telum!" + + charge_required = TRUE + charge_time = CHARGETIME_HEAVY + charge_slowdown = CHARGING_SLOWDOWN_MEDIUM + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 90 SECONDS + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN + + var/channel_duration = 10 SECONDS + var/pulse_interval = 2 SECONDS + var/bolts_per_pulse = 5 + var/pulse_spread = 120 + var/channel_slowdown = 3 + +/datum/action/cooldown/spell/projectile/arcyne_barrage/before_cast(atom/cast_on) + . = ..() + . |= SPELL_NO_IMMEDIATE_COOLDOWN + +/datum/action/cooldown/spell/projectile/arcyne_barrage/fire_projectile(atom/target) + return TRUE + +/datum/action/cooldown/spell/projectile/arcyne_barrage/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + H.say(barrage_invocation, forced = "spell", language = /datum/language/common) + + var/locked_angle = dir2angle(H.dir) + var/end_time = world.time + channel_duration + var/pulse_index = 0 + + H.visible_message(span_danger("[H] unleashes a storm of arcyne bolts!")) + playsound(get_turf(H), 'sound/magic/vlightning.ogg', 100, TRUE) + H.add_movespeed_modifier(ARCYNE_BARRAGE_SLOWDOWN_ID, update = TRUE, priority = 100, multiplicative_slowdown = channel_slowdown, movetypes = GROUND) + + while(world.time < end_time) + if(!channel_valid(H)) + end_channel(H, TRUE) + return TRUE + + pulse_index++ + fire_pulse(H, locked_angle, pulse_index) + + new /obj/effect/temp_visual/spell_impact(get_turf(H), spell_color, spell_impact_intensity) + sleep(pulse_interval) + + end_channel(H, FALSE) + return TRUE + +/datum/action/cooldown/spell/projectile/arcyne_barrage/proc/end_channel(mob/living/carbon/human/H, interrupted) + if(H) + H.remove_movespeed_modifier(ARCYNE_BARRAGE_SLOWDOWN_ID) + if(interrupted) + StartCooldown() + if(H) + to_chat(H, span_warning("My barrage falters!")) + else + StartCooldown() + +/datum/action/cooldown/spell/projectile/arcyne_barrage/proc/channel_valid(mob/living/carbon/human/H) + if(QDELETED(H)) + return FALSE + if(H.stat != CONSCIOUS) + return FALSE + if(H.IsParalyzed() || H.IsStun() || H.IsKnockdown() || H.IsUnconscious()) + return FALSE + if(H.pulledby) + return FALSE + return TRUE + +/datum/action/cooldown/spell/projectile/arcyne_barrage/proc/fire_pulse(mob/living/carbon/human/H, locked_angle, pulse_index) + var/arc_start = locked_angle - (pulse_spread / 2) + var/step_size = bolts_per_pulse > 1 ? (pulse_spread / (bolts_per_pulse - 1)) : 0 + for(var/i in 1 to bolts_per_pulse) + fire_barrage_bolt(H, arc_start + (step_size * (i - 1))) + +/datum/action/cooldown/spell/projectile/arcyne_barrage/proc/fire_barrage_bolt(mob/living/carbon/human/H, angle) + var/obj/projectile/magic/arcyne_barrage_bolt/bolt = new(get_turf(H)) + bolt.firer = H + bolt.fired_from = get_turf(H) + bolt.def_zone = BODY_ZONE_CHEST + bolt.spell_impact_intensity = SPELL_IMPACT_MEDIUM + bolt.accuracy += (GET_MOB_ATTRIBUTE_VALUE(H, STAT_INTELLIGENCE) - 9) * 4 + bolt.bonus_accuracy += (GET_MOB_ATTRIBUTE_VALUE(H, STAT_INTELLIGENCE) - 8) * 3 + if(H.mind) + bolt.bonus_accuracy += (GET_MOB_SKILL_VALUE(H, associated_skill) * 0.5) + bolt.setAngle(angle) + bolt.fire() + +/obj/projectile/magic/arcyne_barrage_bolt + name = "greater arcyne bolt" + icon = 'icons/obj/magic_projectiles.dmi' + icon_state = "arcyne_bolt" + damage = 45 + nodamage = FALSE + damage_type = BRUTE + woundclass = BCLASS_BLUNT + flag = "blunt" + range = 30 + speed = 3 + accuracy = 60 + ricochets_max = 5 + ricochet_chance = 100 + hitsound = 'sound/combat/hits/blunt/shovel_hit2.ogg' + var/hits = 0 + var/max_hits = 5 + +/obj/projectile/magic/arcyne_barrage_bolt/on_hit(target) + hitsound = pick('sound/combat/hits/blunt/shovel_hit.ogg', 'sound/combat/hits/blunt/shovel_hit2.ogg', 'sound/combat/hits/blunt/shovel_hit3.ogg') + if(ismob(target)) + var/mob/living/M = target + if(M.anti_magic_check()) + visible_message(span_warning("[src] dissipates harmlessly against [target]!")) + playsound(get_turf(target), 'sound/magic/magic_nulled.ogg', 100) + qdel(src) + return BULLET_ACT_BLOCK + . = ..() + if(!ismob(target)) + return + hits++ + if(hits >= max_hits) + qdel(src) + return . || BULLET_ACT_HIT + return BULLET_ACT_FORCE_PIERCE + +#undef ARCYNE_BARRAGE_SLOWDOWN_ID diff --git a/code/modules/spells/spell_types/pointed/projectile/arcyne_bolt.dm b/code/modules/spells/spell_types/pointed/projectile/arcyne_bolt.dm index ce2af68e495..10ee4f4051a 100644 --- a/code/modules/spells/spell_types/pointed/projectile/arcyne_bolt.dm +++ b/code/modules/spells/spell_types/pointed/projectile/arcyne_bolt.dm @@ -5,10 +5,8 @@ sound = 'sound/magic/vlightning.ogg' cast_range = 12 - point_cost = 2 - attunements = list( - /datum/attunement/arcyne = 0.7, - ) + + required_form = FORM_ARCANE invocation = "MAGIC MISSILE!!!" invocation_type = INVOCATION_SHOUT @@ -23,8 +21,8 @@ /datum/action/cooldown/spell/projectile/arcyne_bolt/ready_projectile(obj/projectile/to_fire, atom/target, mob/user, iteration) . = ..() - to_fire.damage *= attuned_strength - to_fire.armor_penetration *= attuned_strength + to_fire.damage *= spell_magnitude_modifier + to_fire.armor_penetration *= spell_magnitude_modifier /obj/projectile/magic/energy/rogue3 name = "arcyne bolt" diff --git a/code/modules/spells/spell_types/pointed/projectile/blood_lightning.dm b/code/modules/spells/spell_types/pointed/projectile/blood_lightning.dm index a353b1bdedd..9dfa7fb0e94 100644 --- a/code/modules/spells/spell_types/pointed/projectile/blood_lightning.dm +++ b/code/modules/spells/spell_types/pointed/projectile/blood_lightning.dm @@ -5,12 +5,10 @@ sound = 'sound/magic/vlightning.ogg' cast_range = 8 - point_cost = 6 associated_skill = /datum/attribute/skill/magic/blood - attunements = list( - /datum/attunement/electric = 0.3, - /datum/attunement/blood = 0.7, - ) + + required_form = FORM_WATER + required_technique = TECHNIQUE_DESTRUCTION invocation = "Blood Bolt!!" invocation_type = INVOCATION_SHOUT diff --git a/code/modules/spells/spell_types/pointed/projectile/blood_steal.dm b/code/modules/spells/spell_types/pointed/projectile/blood_steal.dm index e4e7272c89a..a10b5568b52 100644 --- a/code/modules/spells/spell_types/pointed/projectile/blood_steal.dm +++ b/code/modules/spells/spell_types/pointed/projectile/blood_steal.dm @@ -5,9 +5,8 @@ sound = 'sound/magic/vlightning.ogg' associated_skill = /datum/attribute/skill/magic/blood - attunements = list( - /datum/attunement/blood = 0.7, - ) + required_form = FORM_WATER + required_technique = TECHNIQUE_ALTERATION invocation = "DR'N LF'E!" invocation_type = INVOCATION_SHOUT diff --git a/code/modules/spells/spell_types/pointed/projectile/enrapturing Powder.dm b/code/modules/spells/spell_types/pointed/projectile/enrapturing_powder.dm similarity index 92% rename from code/modules/spells/spell_types/pointed/projectile/enrapturing Powder.dm rename to code/modules/spells/spell_types/pointed/projectile/enrapturing_powder.dm index c777eec8c34..0c22cc0673f 100644 --- a/code/modules/spells/spell_types/pointed/projectile/enrapturing Powder.dm +++ b/code/modules/spells/spell_types/pointed/projectile/enrapturing_powder.dm @@ -9,12 +9,6 @@ associated_skill = /datum/attribute/skill/magic/holy invocation = "Have a taste of the maiden's pure-bliss..." invocation_type = INVOCATION_WHISPER - - attunements = list( - /datum/attunement/electric = 0.3, - /datum/attunement/aeromancy = 0.3, - ) - charge_time = 2 SECONDS cooldown_time = 10 SECONDS spell_cost = 30 diff --git a/code/modules/spells/spell_types/pointed/projectile/falcon_disrupt.dm b/code/modules/spells/spell_types/pointed/projectile/falcon_disrupt.dm index 35a667ba810..2bf4f2c4575 100644 --- a/code/modules/spells/spell_types/pointed/projectile/falcon_disrupt.dm +++ b/code/modules/spells/spell_types/pointed/projectile/falcon_disrupt.dm @@ -9,7 +9,6 @@ antimagic_flags = MAGIC_RESISTANCE_HOLY associated_skill = /datum/attribute/skill/magic/holy required_items = list(/obj/item/clothing/neck/psycross/silver/divine/dendor) - attunements = list(/datum/attunement/earth = 0.5) charge_time = 1 SECONDS charge_drain = 1 charge_slowdown = 0.7 diff --git a/code/modules/spells/spell_types/pointed/projectile/fetch.dm b/code/modules/spells/spell_types/pointed/projectile/fetch.dm index 633049620c7..794d3bfd866 100644 --- a/code/modules/spells/spell_types/pointed/projectile/fetch.dm +++ b/code/modules/spells/spell_types/pointed/projectile/fetch.dm @@ -5,10 +5,7 @@ cast_range = 15 sound = 'sound/magic/magnet.ogg' - point_cost = 2 - attunements = list( - /datum/attunement/aeromancy = 0.4, - ) + required_form = FORM_AIR //yoink invocation = "YO'EIN'KAR!" diff --git a/code/modules/spells/spell_types/pointed/projectile/fire_flare.dm b/code/modules/spells/spell_types/pointed/projectile/fire_flare.dm index 541ddaaaec0..c3a65ec3123 100644 --- a/code/modules/spells/spell_types/pointed/projectile/fire_flare.dm +++ b/code/modules/spells/spell_types/pointed/projectile/fire_flare.dm @@ -5,11 +5,6 @@ sound = 'sound/magic/whiteflame.ogg' charge_sound = 'sound/magic/charging_fire.ogg' - point_cost = 3 - attunements = list( - /datum/attunement/fire = 0.3, - ) - charge_time = 2 SECONDS charge_drain = 1 charge_slowdown = 0.7 @@ -18,10 +13,12 @@ spell_flags = SPELL_RITUOS projectile_type = /obj/projectile/magic/aoe/fireball/flare + required_form = FORM_FIRE + /datum/action/cooldown/spell/projectile/fire_flare/ready_projectile(obj/projectile/magic/aoe/fireball/to_fire, atom/target, mob/user, iteration) . = ..() - to_fire.damage *= attuned_strength - to_fire.exp_flash *= attuned_strength + to_fire.damage *= spell_magnitude_modifier + to_fire.exp_flash *= spell_magnitude_modifier /obj/projectile/magic/aoe/fireball/flare name = "spitfire" diff --git a/code/modules/spells/spell_types/pointed/projectile/fireball.dm b/code/modules/spells/spell_types/pointed/projectile/fireball.dm index d4c133845ed..f37721ec503 100644 --- a/code/modules/spells/spell_types/pointed/projectile/fireball.dm +++ b/code/modules/spells/spell_types/pointed/projectile/fireball.dm @@ -6,10 +6,6 @@ sound = 'sound/magic/fireball.ogg' cast_range = 8 - point_cost = 4 - attunements = list( - /datum/attunement/fire = 0.5 - ) invocation = "ONI SOMA!!!" invocation_type = INVOCATION_SHOUT @@ -22,13 +18,16 @@ spell_flags = SPELL_RITUOS projectile_type = /obj/projectile/magic/aoe/fireball/rogue + required_form = FORM_FIRE + required_technique = TECHNIQUE_DESTRUCTION + /datum/action/cooldown/spell/projectile/fireball/ready_projectile(obj/projectile/magic/aoe/fireball/to_fire, atom/target, mob/user, iteration) . = ..() - to_fire.damage *= attuned_strength - to_fire.exp_light *= attuned_strength - to_fire.exp_fire *= attuned_strength + to_fire.damage *= spell_magnitude_modifier + to_fire.exp_light *= spell_magnitude_modifier + to_fire.exp_fire *= spell_magnitude_modifier /datum/action/cooldown/spell/projectile/fireball/baali name = "Infernal Fireball" @@ -46,11 +45,6 @@ desc = "Shoot out an immense ball of fire that explodes on impact." button_icon_state = "fireball_greater" - point_cost = 6 - attunements = list( - /datum/attunement/fire = 1.1, - ) - charge_time = 4 SECONDS charge_drain = 2 charge_slowdown = 1.3 diff --git a/code/modules/spells/spell_types/pointed/projectile/frost_bolt.dm b/code/modules/spells/spell_types/pointed/projectile/frost_bolt.dm index 44fc9ba13d3..c81bd1dee76 100644 --- a/code/modules/spells/spell_types/pointed/projectile/frost_bolt.dm +++ b/code/modules/spells/spell_types/pointed/projectile/frost_bolt.dm @@ -4,10 +4,8 @@ button_icon_state = "frostbite" sound = 'sound/magic/whiteflame.ogg' - point_cost = 1 - attunements = list( - /datum/attunement/ice = 0.7, - ) + required_form = FORM_ICE + required_technique = TECHNIQUE_DESTRUCTION invocation = "Beam of Frost!!" invocation_type = INVOCATION_SHOUT @@ -26,7 +24,7 @@ /datum/action/cooldown/spell/projectile/frost_bolt/ready_projectile(obj/projectile/to_fire, atom/target, mob/user, iteration) . = ..() - to_fire.damage *= attuned_strength + to_fire.damage *= spell_magnitude_modifier /obj/projectile/magic/frostbolt name = "frost bolt" diff --git a/code/modules/spells/spell_types/pointed/projectile/gravel_blast.dm b/code/modules/spells/spell_types/pointed/projectile/gravel_blast.dm new file mode 100644 index 00000000000..f2557219cf6 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/projectile/gravel_blast.dm @@ -0,0 +1,44 @@ +/datum/action/cooldown/spell/projectile/gravel_blast + button_icon = 'icons/mob/actions/spells/mage_geomancy.dmi' + name = "Gravel Blast" + desc = "Spray a volley of stones at a target. Stones ricochet off walls. Subsequent hits on the same target deal reduced damage. \ + Stones are particularly effective at degrading armor. \ + Toggle arc mode (Shift+G) to lob over obstacles at reduced damage." + button_icon_state = "gravel_blast" + sound = 'sound/combat/hits/onstone/wallhit.ogg' + + projectile_type = /obj/projectile/magic/gravel_blast + projectile_type_arc = /obj/projectile/magic/gravel_blast/arc + cast_range = SPELL_RANGE_PROJECTILE + projectiles_per_fire = 5 + + spell_cost = 15 + + required_form = FORM_EARTH + required_technique = TECHNIQUE_DESTRUCTION + + invocation = "Saxum Iaci!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_POKE + charge_slowdown = CHARGING_SLOWDOWN_SMALL + charge_sound = 'sound/magic/charging_fire.ogg' + cooldown_time = 6 SECONDS + var/spread_step = 8 + + spell_impact_intensity = SPELL_IMPACT_LOW + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN + +/datum/action/cooldown/spell/projectile/gravel_blast/ready_projectile(obj/projectile/to_fire, atom/target, mob/user, iteration) + . = ..() + var/base_angle = to_fire.Angle + if(isnull(base_angle)) + base_angle = get_angle(user, target) + var/center_index = (projectiles_per_fire + 1) / 2 + to_fire.Angle = base_angle + ((iteration - center_index) * spread_step) + // Only the center stone can roll for blunt crit/knockout + if(iteration != center_index) + to_fire.woundclass = null + diff --git a/code/modules/spells/spell_types/pointed/projectile/lightning.dm b/code/modules/spells/spell_types/pointed/projectile/lightning.dm index 488a9335937..4eafb2659a0 100644 --- a/code/modules/spells/spell_types/pointed/projectile/lightning.dm +++ b/code/modules/spells/spell_types/pointed/projectile/lightning.dm @@ -10,10 +10,9 @@ invocation_type = INVOCATION_SHOUT cast_range = 8 - point_cost = 3 - attunements = list( - /datum/attunement/electric = 0.7, - ) + + required_form = FORM_LIGHTNING + required_technique = TECHNIQUE_DESTRUCTION charge_time = 3 SECONDS charge_drain = 1 @@ -36,9 +35,21 @@ light_color = "#dbe72c" light_outer_range = 7 -/obj/projectile/magic/lightning/on_hit(atom/target, blocked, pierce_hit) +/obj/projectile/magic/lightning/on_hit(target) . = ..() - if(isliving(target)) - var/mob/living/L = target - L.electrocute_act(1, src) - L.apply_status_effect(/datum/status_effect/debuff/electrified) + if(ismob(target)) + var/mob/M = target + if(M.anti_magic_check()) + visible_message(span_warning("[src] fizzles on contact with [target]!")) + playsound(get_turf(target), 'sound/magic/magic_nulled.ogg', 100) + qdel(src) + return BULLET_ACT_BLOCK + if(isliving(target)) + var/mob/living/L = target + if(out_of_effective_range()) + return + L.lightning_shock(src) + else if(isatom(target)) + var/atom/A = target + A.fire_act() + qdel(src) diff --git a/code/modules/spells/spell_types/pointed/projectile/profane.dm b/code/modules/spells/spell_types/pointed/projectile/profane.dm index e34d4dd59b1..65d2616bec7 100644 --- a/code/modules/spells/spell_types/pointed/projectile/profane.dm +++ b/code/modules/spells/spell_types/pointed/projectile/profane.dm @@ -11,11 +11,6 @@ invocation = "Oblino!" invocation_type = INVOCATION_SHOUT - attunements = list( - /datum/attunement/death = 0.3, - /datum/attunement/blood = 0.3, - ) - charge_drain = 1 charge_time = 2 SECONDS charge_slowdown = 0.3 @@ -23,6 +18,15 @@ spell_cost = 35 projectile_type = /obj/projectile/magic/profane +/datum/action/cooldown/spell/projectile/profane/spell + name = "Profane Rupture" + + spell_type = SPELL_MANA + antimagic_flags = MAGIC_RESISTANCE_MIND + + required_form = FORM_DEATH + required_technique = TECHNIQUE_DESTRUCTION + /datum/action/cooldown/spell/projectile/profane/before_cast(mob/living/cast_on) . = ..() if(. & SPELL_CANCEL_CAST) diff --git a/code/modules/spells/spell_types/pointed/projectile/repel.dm b/code/modules/spells/spell_types/pointed/projectile/repel.dm index b4a59b79187..85a0dbb54b2 100644 --- a/code/modules/spells/spell_types/pointed/projectile/repel.dm +++ b/code/modules/spells/spell_types/pointed/projectile/repel.dm @@ -5,10 +5,7 @@ cast_range = 10 sound = 'sound/magic/unmagnet.ogg' - point_cost = 3 - attunements = list( - /datum/attunement/aeromancy = 0.4, - ) + required_form = FORM_AIR invocation = "Repel!" invocation_type = INVOCATION_SHOUT diff --git a/code/modules/spells/spell_types/pointed/projectile/revel_in_slaughter.dm b/code/modules/spells/spell_types/pointed/projectile/revel_in_slaughter.dm index 80ba2d5a850..626610574ba 100644 --- a/code/modules/spells/spell_types/pointed/projectile/revel_in_slaughter.dm +++ b/code/modules/spells/spell_types/pointed/projectile/revel_in_slaughter.dm @@ -7,9 +7,7 @@ spell_type = SPELL_MIRACLE antimagic_flags = MAGIC_RESISTANCE_HOLY associated_skill = /datum/attribute/skill/magic/holy - attunements = list( - /datum/attunement/blood = 0.5, - ) + projectile_type = /obj/projectile/magic/revel_in_slaughter charge_time = 1 SECONDS charge_drain = 1 diff --git a/code/modules/spells/spell_types/pointed/projectile/smoke_ball.dm b/code/modules/spells/spell_types/pointed/projectile/smoke_ball.dm new file mode 100644 index 00000000000..408f751c075 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/projectile/smoke_ball.dm @@ -0,0 +1,68 @@ +/datum/action/cooldown/spell/projectile/smoke_burst + button_icon = 'icons/mob/actions/spells/mage_pyromancy.dmi' + name = "Smoke Burst" + desc = "Hurl a harmless ball of smouldering cinders that bursts into a thick 3x3 cloud of smoke on impact, blocking sight for around 15 seconds.\ + Deals no damage. \ + Toggle arc mode (Shift+G) while the spell is active to lob it over intervening mobs and obstacles." + button_icon_state = "smoke_burst" + sound = 'sound/items/firesnuff.ogg' + + projectile_type = /obj/projectile/magic/smoke_burst + projectile_type_arc = /obj/projectile/magic/smoke_burst/arc + cast_range = SPELL_RANGE_PROJECTILE + + spell_cost = 20 + + required_form = FORM_FIRE + + invocation = "Evomere Fumum!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = 0.4 SECONDS + + charge_slowdown = CHARGING_SLOWDOWN_SMALL + charge_sound = 'sound/magic/charging_fire.ogg' + cooldown_time = 45 SECONDS + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN + +/obj/projectile/magic/smoke_burst + name = "smoke burst" + icon = 'icons/obj/projectiles.dmi' + icon_state = "spark" + light_color = "#8a8a8a" + light_outer_range = 2 + speed = 2.5 + nodamage = TRUE + damage = 0 + flag = "fire" + hitsound = 'sound/blank.ogg' + reflectable = FALSE + var/smoke_range = 1 + var/has_burst = FALSE + +/obj/projectile/magic/smoke_burst/arc + name = "arced smoke burst" + arcshot = TRUE + +/obj/projectile/magic/smoke_burst/proc/burst_smoke(turf/epicenter) + if(has_burst || !epicenter) + return + has_burst = TRUE + playsound(epicenter, 'sound/items/firesnuff.ogg', 70, TRUE) + for(var/turf/T in range(smoke_range, epicenter)) + if(T.density) + continue + new /obj/effect/particle_effect/smoke/pyro_screen(T) + +/obj/projectile/magic/smoke_burst/on_hit(atom/target, blocked = FALSE) + . = ..() + burst_smoke(get_turf(target)) + +/obj/projectile/magic/smoke_burst/on_range() + burst_smoke(get_turf(src)) + . = ..() + +/obj/effect/particle_effect/smoke/pyro_screen + lifetime = 7 diff --git a/code/modules/spells/spell_types/pointed/projectile/unholy_grasp.dm b/code/modules/spells/spell_types/pointed/projectile/unholy_grasp.dm index f75dfb4cacd..229b4e2b6dc 100644 --- a/code/modules/spells/spell_types/pointed/projectile/unholy_grasp.dm +++ b/code/modules/spells/spell_types/pointed/projectile/unholy_grasp.dm @@ -9,10 +9,6 @@ antimagic_flags = MAGIC_RESISTANCE_HOLY associated_skill = /datum/attribute/skill/magic/holy - attunements = list( - /datum/attunement/blood = 0.5, - ) - invocation_type = INVOCATION_EMOTE invocation = span_userdanger("%CASTER casts %PRONOUN_their hands outward!") invocation_self_message = span_danger("I throw out an unholy snare!") diff --git a/code/modules/spells/spell_types/pointed/rending_grasp.dm b/code/modules/spells/spell_types/pointed/rending_grasp.dm new file mode 100644 index 00000000000..24ce49b3ec1 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/rending_grasp.dm @@ -0,0 +1,105 @@ +/datum/action/cooldown/spell/rending_grasp + button_icon = 'icons/mob/actions/spells/spellfist.dmi' + button_icon_state = "grasp_of_psydon" + name = "Rending Grasp" + desc = "Slam your open palm forward, sending forth tendrils of lightning to a target area up to 4 paces away on the same level. After a brief telegraph, all targets in the area are yanked toward you. \ + At 3+ momentum: consumes 3 to deal 40 blunt damage to the aimed bodypart on each yanked target." + sound = list('sound/combat/wooshes/punch/punchwoosh (1).ogg','sound/combat/wooshes/punch/punchwoosh (2).ogg','sound/combat/wooshes/punch/punchwoosh (3).ogg') + + cast_range = 5 + + required_form = FORM_LIGHTNING + required_technique = TECHNIQUE_IMBUE + spell_cost = 30 + + invocation = "Iqbid!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = 0.5 SECONDS + charge_slowdown = CHARGING_SLOWDOWN_MEDIUM + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 20 SECONDS + + spell_impact_intensity = SPELL_IMPACT_MEDIUM + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/area_of_effect = 1 + var/pull_distance = 7 + var/telegraph_delay = 0.8 SECONDS + var/base_damage = 15 + var/empowered_damage = 40 + var/momentum_cost = 3 + +/datum/action/cooldown/spell/rending_grasp/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/turf/T = get_turf(cast_on) + if(!T) + return FALSE + + var/turf/caster_turf = get_turf(H) + if(T.z != caster_turf.z) + to_chat(H, span_warning("The tendrils can't reach across planes!")) + return FALSE + + var/empowered = FALSE + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(M && M.stacks >= momentum_cost) + M.consume_stacks(momentum_cost) + empowered = TRUE + to_chat(H, span_notice("[momentum_cost] momentum released - empowered grasp!")) + + H.emote("attack", forced = TRUE) + + for(var/turf/affected_turf in get_hear(area_of_effect, T)) + if(affected_turf.density) + continue + new /obj/effect/temp_visual/grasp_telegraph(affected_turf) + + playsound(T, 'sound/magic/webspin.ogg', 50, TRUE) + + addtimer(CALLBACK(src, PROC_REF(resolve_grasp), H, T, empowered), telegraph_delay) + return TRUE + +/datum/action/cooldown/spell/rending_grasp/proc/resolve_grasp(mob/living/carbon/human/H, turf/center, empowered = FALSE) + if(QDELETED(H) || H.stat == DEAD) + return + + var/turf/caster_turf = get_turf(H) + playsound(center, 'sound/combat/grabbreak.ogg', 80, TRUE) + + var/hit_count = 0 + for(var/mob/living/victim in range(area_of_effect, center)) + if(victim == H || victim.stat == DEAD) + continue + if(victim.anti_magic_check()) + victim.visible_message(span_warning("The tendrils of force can't seem to latch onto [victim]!")) + playsound(get_turf(victim), 'sound/magic/magic_nulled.ogg', 100) + continue + var/def_zone = H.zone_selected || BODY_ZONE_CHEST + arcyne_strike(H, victim, null, base_damage, def_zone, BCLASS_BLUNT, spell_name = "Grasp of Psydon") + if(empowered) + arcyne_strike(H, victim, null, empowered_damage, def_zone, BCLASS_BLUNT, spell_name = "Grasp of Psydon (Empowered)") + victim.throw_at(caster_turf, pull_distance, 4) + + victim.visible_message(span_warning("[victim] is yanked toward [H] by tendrils of arcyne force!")) + new /obj/effect/temp_visual/grasp_telegraph/long(get_turf(victim)) + hit_count++ + + if(hit_count) + H.visible_message(span_danger("[H] clenches [H.p_their()] fist, pulling [hit_count > 1 ? "enemies" : "an enemy"] toward [H.p_them()]!")) + + log_combat(H, null, "used Grasp of Psydon[empowered ? " (empowered)" : ""]") + +/obj/effect/temp_visual/grasp_telegraph + icon = 'icons/effects/effects.dmi' + icon_state = "curseblob" + duration = 1 SECONDS + +/obj/effect/temp_visual/grasp_telegraph/long + duration = 2 SECONDS diff --git a/code/modules/spells/spell_types/pointed/spirit_possession.dm b/code/modules/spells/spell_types/pointed/spirit_possession.dm new file mode 100644 index 00000000000..243a1fac7bf --- /dev/null +++ b/code/modules/spells/spell_types/pointed/spirit_possession.dm @@ -0,0 +1,261 @@ +/datum/action/cooldown/spell/conjure_projection + button_icon = 'icons/mob/actions/spells/mage_conjure.dmi' + button_icon_state = "spirit_projection" + name = "Spirit Projection" + desc = "Cast your spirit into one of your conjured servants and control it directly, leaving your true body behind. A line links your body to the vessel. If the vessel strays too far, changes Z level, your abandoned body is struck, or the vessel is slain, your spirit is torn home at once. Use Return to Body to withdraw." + sound = 'sound/magic/soulsteal.ogg' + + click_to_activate = TRUE + cast_range = 12 + self_cast_possible = FALSE + + spell_cost = 20 + + required_form = FORM_DEATH + required_technique = TECHNIQUE_SUMMONING + + invocation_type = INVOCATION_SHOUT + invocation = "In te ligabo, servus - spiritus meus intret!" + + charge_required = FALSE + cooldown_time = 30 SECONDS + + spell_impact_intensity = SPELL_IMPACT_NONE + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z | SPELL_REQUIRES_MIND + + var/projecting = FALSE + var/datum/weakref/body_ref + var/datum/weakref/vessel_ref + var/datum/action/cooldown/spell/spirit_return/return_action + var/datum/action/cooldown/spell/pilot_action + var/body_base_pixel_x = 0 + var/body_base_pixel_y = 0 + var/leash_range = 7 + var/datum/beam/tether_beam + +/datum/action/cooldown/spell/conjure_projection/Destroy() + if(projecting) + return_to_body("caster_gone") + return ..() + +/datum/action/cooldown/spell/conjure_projection/is_valid_target(atom/cast_on) + . = ..() + if(!.) + return + var/mob/living/caster = owner + if(!isliving(caster)) + return FALSE + if(!isliving(cast_on)) + caster.balloon_alert(caster, "not a servant!") + return FALSE + var/mob/living/vessel = cast_on + if(!HAS_TRAIT(vessel, TRAIT_CONJURED_SUMMON) || !(vessel in caster.summoned_minions)) + caster.balloon_alert(caster, "not my servant!") + return FALSE + if(vessel.stat == DEAD) + caster.balloon_alert(caster, "it is slain!") + return FALSE + if(vessel.ckey) + caster.balloon_alert(caster, "already inhabited!") + return FALSE + return TRUE + +/datum/action/cooldown/spell/conjure_projection/before_cast(atom/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return + return . | SPELL_NO_IMMEDIATE_COOLDOWN + +/datum/action/cooldown/spell/conjure_projection/cast(atom/cast_on) + . = ..() + var/mob/living/body = owner + if(!istype(body) || projecting) + return FALSE + if(!body.key) + return FALSE + var/mob/living/vessel = cast_on + if(!istype(vessel)) + return FALSE + begin_projection(body, vessel) + return TRUE + +/datum/action/cooldown/spell/conjure_projection/proc/begin_projection(mob/living/body, mob/living/vessel) + projecting = TRUE + body_ref = WEAKREF(body) + vessel_ref = WEAKREF(vessel) + + ADD_TRAIT(body, TRAIT_SLEEPIMMUNE, "spirit_projection") + ADD_TRAIT(body, TRAIT_NOBREATH, "spirit_projection") + ADD_TRAIT(body, TRAIT_NOHUNGER, "spirit_projection") + ADD_TRAIT(body, TRAIT_NOMOOD, "spirit_projection") + ADD_TRAIT(body, TRAIT_NOSSDINDICATOR, "spirit_projection") + + apply_projection_visuals(body) + + RegisterSignal(vessel, COMSIG_LIVING_DEATH, PROC_REF(on_vessel_lost)) + RegisterSignal(vessel, COMSIG_QDELETING, PROC_REF(on_vessel_qdel)) + RegisterSignal(vessel, COMSIG_MOVABLE_MOVED, PROC_REF(on_tether_moved)) + RegisterSignal(body, COMSIG_MOB_APPLY_DAMAGE, PROC_REF(on_body_harmed)) + RegisterSignal(body, COMSIG_LIVING_DEATH, PROC_REF(on_body_death)) + RegisterSignal(body, COMSIG_MOVABLE_MOVED, PROC_REF(on_tether_moved)) + + tether_beam = body.Beam(vessel, icon_state = "b_beam", time = INFINITY, max_distance = leash_range + 3, beam_type = /obj/effect/ebeam/spirit_tether) + START_PROCESSING(SSprocessing, src) + + return_action = new /datum/action/cooldown/spell/spirit_return() + return_action.origin = src + return_action.Grant(vessel) + + var/pilot_ability_type = vessel.get_pilot_ability() + if(pilot_ability_type) + pilot_action = new pilot_ability_type() + pilot_action.Grant(vessel) + + vessel.ai_controller?.set_ai_status(AI_STATUS_OFF) + vessel.key = body.key + + body.visible_message(span_warning("[body] stiffens, then stands vacant, eyes glazed and empty, a faint blue aura shivering about their trembling frame.")) + to_chat(vessel, span_notice("My spirit surges into [vessel.real_name] - I see through its eyes and move as it moves.")) + playsound(body, 'sound/magic/soulsteal.ogg', 60, TRUE) + +/datum/action/cooldown/spell/conjure_projection/proc/apply_projection_visuals(mob/living/body) + body_base_pixel_x = body.pixel_x + body_base_pixel_y = body.pixel_y + body.add_filter("spirit_projection_aura", 2, list("type" = "outline", "size" = 1, "color" = "#4d90e0")) + animate(body, pixel_x = body_base_pixel_x + 1, time = 2, loop = -1) + animate(pixel_x = body_base_pixel_x - 1, time = 2) + +/datum/action/cooldown/spell/conjure_projection/proc/clear_projection_visuals(mob/living/body) + body.remove_filter("spirit_projection_aura") + animate(body, pixel_x = body_base_pixel_x, pixel_y = body_base_pixel_y, time = 1) + +/datum/action/cooldown/spell/conjure_projection/proc/on_vessel_lost(mob/living/source, gibbed) + SIGNAL_HANDLER + INVOKE_ASYNC(src, PROC_REF(return_to_body), "vessel_slain") + +/datum/action/cooldown/spell/conjure_projection/proc/on_vessel_qdel(datum/source) + SIGNAL_HANDLER + return_to_body("vessel_gone") + +/datum/action/cooldown/spell/conjure_projection/proc/on_body_harmed(datum/source, damage, damagetype, def_zone) + SIGNAL_HANDLER + if(damage <= 0) + return + INVOKE_ASYNC(src, PROC_REF(return_to_body), "body_struck") + +/datum/action/cooldown/spell/conjure_projection/proc/on_body_death(mob/living/source, gibbed) + SIGNAL_HANDLER + INVOKE_ASYNC(src, PROC_REF(return_to_body), "body_slain") + +/datum/action/cooldown/spell/conjure_projection/proc/on_tether_moved(datum/source) + SIGNAL_HANDLER + evaluate_tether() + +/datum/action/cooldown/spell/conjure_projection/process(seconds_per_tick) + evaluate_tether() + +/datum/action/cooldown/spell/conjure_projection/proc/evaluate_tether() + if(!projecting) + return + var/mob/living/body = body_ref?.resolve() + var/mob/living/vessel = vessel_ref?.resolve() + if(!body || !vessel) + return + if(body.z != vessel.z || get_dist(body, vessel) > leash_range) + INVOKE_ASYNC(src, PROC_REF(return_to_body), "out_of_range") + +/datum/action/cooldown/spell/conjure_projection/proc/return_to_body(reason) + if(!projecting) + return + projecting = FALSE + + var/mob/living/body = body_ref?.resolve() + var/mob/living/vessel = vessel_ref?.resolve() + body_ref = null + vessel_ref = null + + STOP_PROCESSING(SSprocessing, src) + if(tether_beam) + if(!QDELETED(tether_beam)) + qdel(tether_beam) + tether_beam = null + + if(return_action) + if(!QDELETED(return_action)) + qdel(return_action) + return_action = null + if(pilot_action) + if(!QDELETED(pilot_action)) + qdel(pilot_action) + pilot_action = null + + if(vessel) + UnregisterSignal(vessel, list(COMSIG_LIVING_DEATH, COMSIG_QDELETING, COMSIG_MOVABLE_MOVED)) + if(body) + UnregisterSignal(body, list(COMSIG_MOB_APPLY_DAMAGE, COMSIG_LIVING_DEATH, COMSIG_MOVABLE_MOVED)) + REMOVE_TRAIT(body, TRAIT_SLEEPIMMUNE, "spirit_projection") + REMOVE_TRAIT(body, TRAIT_NOBREATH, "spirit_projection") + REMOVE_TRAIT(body, TRAIT_NOHUNGER, "spirit_projection") + REMOVE_TRAIT(body, TRAIT_NOMOOD, "spirit_projection") + REMOVE_TRAIT(body, TRAIT_NOSSDINDICATOR, "spirit_projection") + clear_projection_visuals(body) + + if(body && vessel && vessel.key) + body.key = vessel.key + vessel.ai_controller?.reset_ai_status() + + if(body) + switch(reason) + if("vessel_slain") + to_chat(body, span_userdanger("My vessel is cut down - my spirit is flung violently back into my flesh!")) + if("vessel_gone") + to_chat(body, span_warning("The vessel unravels beneath me and my spirit rushes home.")) + if("body_struck") + to_chat(body, span_userdanger("Pain wracks my true body - my spirit is snapped back in an instant!")) + if("body_slain") + to_chat(body, span_userdanger("My true body falls - I am dragged down into it as it dies!")) + if("out_of_range") + to_chat(body, span_userdanger("My vessel strays too far - the tether snaps and my spirit recoils home!")) + else + to_chat(body, span_notice("I draw my spirit back and settle once more into my own flesh.")) + playsound(body, 'sound/magic/soulsteal.ogg', 50, TRUE) + + if(!QDELETED(src)) + StartCooldown() + +/datum/action/cooldown/spell/spirit_return + button_icon = 'icons/mob/actions/spells/mage_conjure.dmi' + button_icon_state = "spirit_projection" + name = "Return to Body" + desc = "Withdraw your projected spirit and awaken once more in your own flesh." + sound = null + + click_to_activate = FALSE + self_cast_possible = TRUE + charge_required = FALSE + spell_type = NONE + + cooldown_time = 0 + spell_tier = 3 + spell_impact_intensity = SPELL_IMPACT_NONE + spell_requirements = NONE + check_flags = NONE + + var/datum/action/cooldown/spell/conjure_projection/origin + +/datum/action/cooldown/spell/spirit_return/can_cast_spell(feedback = TRUE) + return !isnull(owner) && !isnull(origin) + +/datum/action/cooldown/spell/spirit_return/cast(atom/cast_on) + . = ..() + origin?.return_to_body("voluntary") + return TRUE + +/mob/living/proc/get_pilot_ability() + return null + +/obj/effect/ebeam/spirit_tether + color = "#4d90e0" + alpha = 180 + layer = ABOVE_MOB_LAYER diff --git a/code/modules/spells/spell_types/pointed/status/_status.dm b/code/modules/spells/spell_types/pointed/status/_status.dm index 0e9caf783c4..d4870f20c6b 100644 --- a/code/modules/spells/spell_types/pointed/status/_status.dm +++ b/code/modules/spells/spell_types/pointed/status/_status.dm @@ -39,9 +39,9 @@ var/new_duration = duration ? duration : initial(status_effect.duration) if(duration_scaling) if(duration_modification) - new_duration += duration_modification * attuned_strength + new_duration += duration_modification * spell_magnitude_modifier else - new_duration *= attuned_strength + new_duration *= spell_magnitude_modifier var/list/status_args = list( status_effect, new_duration, diff --git a/code/modules/spells/spell_types/pointed/status/booming_blade.dm b/code/modules/spells/spell_types/pointed/status/booming_blade.dm index 6e9c3a08fe9..bc05ccbf4da 100644 --- a/code/modules/spells/spell_types/pointed/status/booming_blade.dm +++ b/code/modules/spells/spell_types/pointed/status/booming_blade.dm @@ -5,10 +5,9 @@ self_cast_possible = FALSE spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC|SPELL_REQUIRES_NO_MOVE // reap what you sow - point_cost = 1 - attunements = list( - /datum/attunement/arcyne = 0.4, - ) + required_form = FORM_ARCANE + required_technique = TECHNIQUE_ALTERATION + spell_flags = SPELL_RITUOS invocation = "Be still!" invocation_type = INVOCATION_SHOUT @@ -21,10 +20,6 @@ status_effect = /datum/status_effect/debuff/booming_blade -/datum/action/cooldown/spell/status/booming_blade/handle_attunements() - . = ..() - extra_args = list(attuned_strength) - /datum/status_effect/debuff/booming_blade id = "booming_blade" alert_type = /atom/movable/screen/alert/status_effect/debuff/booming_blade diff --git a/code/modules/spells/spell_types/pointed/status/frostbite.dm b/code/modules/spells/spell_types/pointed/status/frostbite.dm index 91c481fb04b..c1f570ccc46 100644 --- a/code/modules/spells/spell_types/pointed/status/frostbite.dm +++ b/code/modules/spells/spell_types/pointed/status/frostbite.dm @@ -5,10 +5,9 @@ self_cast_possible = FALSE sound = 'sound/magic/whiteflame.ogg' - point_cost = 1 - attunements = list( - /datum/attunement/ice = 0.9 - ) + + required_form = FORM_ICE + required_technique = TECHNIQUE_ALTERATION invocation = "Bite of Frost!!" invocation_type = INVOCATION_SHOUT @@ -22,7 +21,7 @@ /datum/action/cooldown/spell/status/frostbite/cast(mob/living/cast_on) . = ..() - extra_args = list(attuned_strength) + extra_args = list(spell_magnitude_modifier) if(iscarbon(cast_on)) var/mob/living/carbon/C = cast_on - C.adjustFireLoss(15 * attuned_strength) + C.adjustFireLoss(15 * spell_magnitude_modifier) diff --git a/code/modules/spells/spell_types/pointed/status/guidance.dm b/code/modules/spells/spell_types/pointed/status/guidance.dm index 18859c99dc1..df4e7f32ad9 100644 --- a/code/modules/spells/spell_types/pointed/status/guidance.dm +++ b/code/modules/spells/spell_types/pointed/status/guidance.dm @@ -4,11 +4,7 @@ button_icon_state = "guidance" sound = 'sound/magic/haste.ogg' - point_cost = 2 - attunements = list( - /datum/attunement/earth = 1, - ) - school = SCHOOL_TRANSMUTATION + required_form = FORM_EARTH charge_time = 4 SECONDS charge_drain = 1 diff --git a/code/modules/spells/spell_types/pointed/status/haste.dm b/code/modules/spells/spell_types/pointed/status/haste.dm index 0b627742375..237cdbe565d 100644 --- a/code/modules/spells/spell_types/pointed/status/haste.dm +++ b/code/modules/spells/spell_types/pointed/status/haste.dm @@ -2,13 +2,9 @@ name = "Haste" desc = "Cause a target to be magically hastened." button_icon_state = "haste" - point_cost = 2 sound = 'sound/magic/haste.ogg' - attunements = list( - /datum/attunement/aeromancy = 0.5, - ) - school = SCHOOL_TRANSMUTATION + required_form = FORM_EARTH charge_time = 2 SECONDS charge_slowdown = 0.3 diff --git a/code/modules/spells/spell_types/pointed/status/infestation.dm b/code/modules/spells/spell_types/pointed/status/infestation.dm index f1d9db15e19..d17a9a504bb 100644 --- a/code/modules/spells/spell_types/pointed/status/infestation.dm +++ b/code/modules/spells/spell_types/pointed/status/infestation.dm @@ -5,12 +5,10 @@ self_cast_possible = FALSE sound = 'sound/magic/whiteflame.ogg' - point_cost = 1 associated_skill = /datum/attribute/skill/magic/druidic - attunements = list( - /datum/attunement/dark = 0.3, - /datum/attunement/death = 0.3, - ) + + required_form = FORM_DEATH + required_technique = TECHNIQUE_SUMMONING invocation = "Rot, take them!" invocation_type = INVOCATION_SHOUT diff --git a/code/modules/spells/spell_types/pointed/status/primal_savagery.dm b/code/modules/spells/spell_types/pointed/status/primal_savagery.dm index 3621d6cf728..770b24c34b0 100644 --- a/code/modules/spells/spell_types/pointed/status/primal_savagery.dm +++ b/code/modules/spells/spell_types/pointed/status/primal_savagery.dm @@ -4,14 +4,13 @@ button_icon_state = "wolf_head" sound = 'sound/magic/whiteflame.ogg' - point_cost = 1 associated_skill = /datum/attribute/skill/magic/druidic - attunements = list( - /datum/attunement/earth = 0.3, - ) invocation = "Teeth of a serpent." invocation_type = INVOCATION_WHISPER + required_form = FORM_LIFE + required_technique = TECHNIQUE_ALTERATION + charge_required = FALSE cooldown_time = 60 SECONDS spell_cost = 50 diff --git a/code/modules/spells/spell_types/pointed/sundering_lightning.dm b/code/modules/spells/spell_types/pointed/sundering_lightning.dm index 765ac253fec..971ce0abe10 100644 --- a/code/modules/spells/spell_types/pointed/sundering_lightning.dm +++ b/code/modules/spells/spell_types/pointed/sundering_lightning.dm @@ -4,11 +4,9 @@ button_icon_state = "sundering" sound = 'sound/weather/rain/thunder_1.ogg' - point_cost = 8 cast_range = 4 - attunements = list( - /datum/attunement/electric = 0.9 - ) + required_form = FORM_LIGHTNING + required_technique = TECHNIQUE_DESTRUCTION invocation = "Lightning strikes more than twice!" invocation_type = INVOCATION_SHOUT @@ -35,4 +33,4 @@ if(dist > last_dist) last_dist = dist sleep(2 + min(4 - last_dist, 12) * 0.5) - new /obj/effect/temp_visual/target/lightning(T) + new /obj/effect/temp_visual/target/lightning/sundering(T) diff --git a/code/modules/spells/spell_types/pointed/tempest_rush.dm b/code/modules/spells/spell_types/pointed/tempest_rush.dm new file mode 100644 index 00000000000..783dd736597 --- /dev/null +++ b/code/modules/spells/spell_types/pointed/tempest_rush.dm @@ -0,0 +1,316 @@ +/datum/action/cooldown/spell/tempest_rush + button_icon = 'icons/mob/actions/spells/spellfist.dmi' + button_icon_state = "storm_of_psydon" + name = "Tempest Rush" + desc = "Channel mana into your legs to leap toward a target from a distance, closing the gap rapidly. \ + Then, channel the mana into your fists to unleash a storm of blows. \ + Requires 7 Momentum: 3 punches + 1 kick (20 damage each). \ + Overcharged at 10 Momentum: 9 punches + 1 kick (20 damage each). \ + Cannot be parried or dodged. \ + Consumes all momentum only on a successful hit." + sound = list('sound/combat/wooshes/punch/punchwoosh (1).ogg','sound/combat/wooshes/punch/punchwoosh (2).ogg','sound/combat/wooshes/punch/punchwoosh (3).ogg') + + cast_range = 7 + + spell_cost = 120 + + required_form = FORM_LIGHTNING + required_technique = TECHNIQUE_IMBUE + + invocation = null + invocation_type = INVOCATION_NONE + + charge_required = TRUE + charge_time = 1 SECONDS + charge_slowdown = CHARGING_SLOWDOWN_NONE + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 60 SECONDS + + spell_impact_intensity = SPELL_IMPACT_MEDIUM + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/start_invocation = "'Asifa!" + var/dash_range = 7 + var/step_delay = 1 + var/punch_damage = 20 + var/kick_damage = 20 + var/punch_sets_full = 3 + var/punches_per_set = 3 + var/punch_count_lame = 3 + var/min_momentum = 7 + var/empowered_momentum = 10 + +/datum/action/cooldown/spell/tempest_rush/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return FALSE + if(!ishuman(owner)) + return FALSE + var/mob/living/carbon/human/H = owner + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(!M || M.stacks < min_momentum) + if(feedback) + to_chat(H, span_warning("Not enough momentum! I need at least [min_momentum] stacks!")) + return FALSE + return TRUE + +/datum/action/cooldown/spell/tempest_rush/before_cast(atom/cast_on) + . = ..() + . |= SPELL_NO_IMMEDIATE_COOLDOWN + +/datum/action/cooldown/spell/tempest_rush/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(!M || M.stacks < min_momentum) + return FALSE + + var/stacks = M.stacks + var/is_full = stacks >= empowered_momentum + + var/mob/living/preferred_target + if(isliving(cast_on)) + preferred_target = cast_on + else + var/turf/clicked_turf = get_turf(cast_on) + if(clicked_turf) + for(var/mob/living/L in clicked_turf) + if(L != H && L.stat != DEAD) + preferred_target = L + break + + if(!preferred_target) + to_chat(H, span_warning("I need a target to focus my storm on!")) + return FALSE + + H.say(start_invocation, forced = "spell", language = /datum/language/common) + + var/turf/start = get_turf(H) + var/facing = get_dir(start, get_turf(preferred_target)) || H.dir + H.dir = facing + + if(get_dist(H, preferred_target) <= 1) + if(preferred_target.anti_magic_check()) + preferred_target.visible_message(span_warning("The storm dissipates on contact with [preferred_target]!")) + StartCooldown() + return TRUE + H.visible_message(span_danger("[H] latches onto [preferred_target], unleashing a flurry of blows!")) + release_momentum(H, M, stacks) + if(is_full) + oraora(H, preferred_target) + else + oraora_lame(H, preferred_target) + StartCooldown() + return TRUE + + H.visible_message(span_danger("[H] launches toward [preferred_target] with storming intent!")) + + var/old_pass = H.pass_flags + var/old_throwing = H.throwing + H.pass_flags |= PASSMOB + H.throwing = TRUE + var/prev_pixel_z = H.pixel_z + var/prev_transform = H.transform + + animate(H, pixel_z = prev_pixel_z + 18, time = 1, easing = EASE_OUT) + + var/mob/living/hit_target + for(var/i in 1 to dash_range) + if(H.stat != CONSCIOUS || H.IsParalyzed() || H.IsStun() || QDELETED(H)) + break + + if(get_dist(H, preferred_target) <= 1) + hit_target = preferred_target + break + + facing = get_dir(get_turf(H), get_turf(preferred_target)) + if(!facing) + break + H.dir = facing + + var/turf/next = get_step(get_turf(H), facing) + if(!next || next.density) + break + + var/blocked = FALSE + for(var/obj/structure/S in next.contents) + if(S.density && !S.climbable) + blocked = TRUE + break + if(blocked) + break + + step(H, facing) + + if(i < dash_range) + sleep(step_delay) + + var/land_angle = pick(-20, -15, 15, 20) + animate(H, pixel_z = prev_pixel_z, transform = turn(prev_transform, land_angle), time = 1, easing = EASE_IN) + animate(transform = prev_transform, time = 2) + + H.pass_flags = old_pass + H.throwing = old_throwing + + if(!hit_target && get_dist(H, preferred_target) <= 1) + hit_target = preferred_target + + if(!hit_target) + to_chat(H, span_warning("My storm finds no purchase!")) + StartCooldown() + return TRUE + + if(hit_target.anti_magic_check()) + hit_target.visible_message(span_warning("The storm dissipates on contact with [hit_target]!")) + StartCooldown() + return TRUE + + release_momentum(H, M, stacks) + if(is_full) + oraora(H, hit_target) + else + oraora_lame(H, hit_target) + StartCooldown() + return TRUE + +/datum/action/cooldown/spell/tempest_rush/proc/release_momentum(mob/living/carbon/human/H, datum/status_effect/buff/arcyne_momentum/M, stacks) + if(M) + M.consume_all_stacks() + to_chat(H, span_notice("All [stacks] momentum released into the storm!")) + +/datum/action/cooldown/spell/tempest_rush/proc/combo_valid(mob/living/carbon/human/user, mob/living/target) + if(QDELETED(user) || QDELETED(target)) + return FALSE + if(user.stat != CONSCIOUS) + return FALSE + if(get_dist(user, target) > 1) + return FALSE + return TRUE + +/datum/action/cooldown/spell/tempest_rush/proc/cling(mob/living/carbon/human/user, mob/living/target) + if(get_dist(user, target) <= 1) + return TRUE + var/turf/user_turf = get_turf(user) + var/turf/target_turf = get_turf(target) + if(!user_turf || !target_turf || user_turf.z != target_turf.z) + return FALSE + var/dir_to = get_dir(user, target) + if(!dir_to) + return FALSE + user.dir = dir_to + step(user, dir_to) + return get_dist(user, target) <= 1 + +/datum/action/cooldown/spell/tempest_rush/proc/combo_cleanup(obj/effect/after_image/shadow_left, obj/effect/after_image/shadow_right) + QDEL_NULL(shadow_left) + QDEL_NULL(shadow_right) + +/datum/action/cooldown/spell/tempest_rush/proc/create_shadows(mob/living/carbon/human/user, mob/living/target) + var/turf/user_turf = get_turf(user) + + var/obj/effect/after_image/shadow_left = new(user_turf, 0, 0, 0, 0, 0, 0, 0) + shadow_left.appearance = user.appearance + shadow_left.pixel_x = -10 + shadow_left.pixel_y = 4 + shadow_left.alpha = 120 + shadow_left.color = "#2a0a3a" + shadow_left.dir = user.dir + shadow_left.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + shadow_left.name = "" + + var/obj/effect/after_image/shadow_right = new(user_turf, 0, 0, 0, 0, 0, 0, 0) + shadow_right.appearance = user.appearance + shadow_right.pixel_x = 10 + shadow_right.pixel_y = 4 + shadow_right.alpha = 120 + shadow_right.color = "#2a0a3a" + shadow_right.dir = user.dir + shadow_right.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + shadow_right.name = "" + + return list(shadow_left, shadow_right) + +/datum/action/cooldown/spell/tempest_rush/proc/oraora(mob/living/carbon/human/user, mob/living/target) + user.changeNext_move(CLICK_CD_MELEE * 3) + + var/target_zone = user.zone_selected || BODY_ZONE_CHEST // Every punch lands where the caster aimed. + + var/list/shadows = create_shadows(user, target) + var/obj/effect/after_image/shadow_left = shadows[1] + var/obj/effect/after_image/shadow_right = shadows[2] + + var/combo_broken = FALSE + var/hit_num = 0 + for(var/set_i in 1 to punch_sets_full) + if(combo_broken) + break + if(!cling(user, target)) + combo_broken = TRUE + break + user.emote("attack", forced = TRUE) + var/turf/new_turf = get_turf(user) + shadow_left.forceMove(new_turf) + shadow_right.forceMove(new_turf) + var/punch_dir = get_dir(user, target) + var/lunge_px = 0 + var/lunge_py = 0 + if(punch_dir & NORTH) + lunge_py = 6 + if(punch_dir & SOUTH) + lunge_py = -6 + if(punch_dir & EAST) + lunge_px = 6 + if(punch_dir & WEST) + lunge_px = -6 + for(var/punch_i in 1 to punches_per_set) + if(!combo_valid(user, target)) + combo_broken = TRUE + break + hit_num++ + arcyne_strike(user, target, null, punch_damage, target_zone, BCLASS_BLUNT, spell_name = "Storm of Psydon (Punch [hit_num])", exact_zone = TRUE) + playsound(get_turf(target), pick('sound/combat/hits/punch/punch_hard (1).ogg','sound/combat/hits/punch/punch_hard (2).ogg','sound/combat/hits/punch/punch_hard (3).ogg'), 80, TRUE) + animate(shadow_left, pixel_x = -10 + lunge_px, pixel_y = 4 + lunge_py, time = 0.5, easing = EASE_OUT) + animate(pixel_x = -10, pixel_y = 4, time = 0.5, easing = EASE_IN) + animate(shadow_right, pixel_x = 10 + lunge_px, pixel_y = 4 + lunge_py, time = 0.5, easing = EASE_OUT) + animate(pixel_x = 10, pixel_y = 4, time = 0.5, easing = EASE_IN) + sleep(3) + + sleep(3) + if(!combo_broken && cling(user, target) && combo_valid(user, target)) + user.emote("attack", forced = TRUE) + arcyne_strike(user, target, null, kick_damage, target_zone, BCLASS_BLUNT, spell_name = "Storm of Psydon (Kick)", exact_zone = TRUE) + playsound(get_turf(target), pick('sound/combat/hits/blunt/genblunt (1).ogg','sound/combat/hits/blunt/genblunt (2).ogg','sound/combat/hits/blunt/genblunt (3).ogg'), 100, TRUE) + var/atom/throw_target = get_edge_target_turf(user, get_dir(user, target)) + target.throw_at(throw_target, 3, 4) + + combo_cleanup(shadow_left, shadow_right) + log_combat(user, target, "used Storm of Psydon (full)") + +/datum/action/cooldown/spell/tempest_rush/proc/oraora_lame(mob/living/carbon/human/user, mob/living/target) + user.changeNext_move(CLICK_CD_MELEE * 2) + + var/target_zone = user.zone_selected || BODY_ZONE_CHEST + + var/combo_broken = FALSE + user.emote("attack", forced = TRUE) + + for(var/i in 1 to punch_count_lame) + if(!combo_valid(user, target)) + combo_broken = TRUE + break + arcyne_strike(user, target, null, punch_damage, target_zone, BCLASS_BLUNT, spell_name = "Storm of Psydon (Punch [i])", exact_zone = TRUE) + playsound(get_turf(target), pick('sound/combat/hits/punch/punch_hard (1).ogg','sound/combat/hits/punch/punch_hard (2).ogg','sound/combat/hits/punch/punch_hard (3).ogg'), 80, TRUE) + + sleep(1) + if(!combo_broken && cling(user, target) && combo_valid(user, target)) + user.emote("attack", forced = TRUE) + arcyne_strike(user, target, null, kick_damage, target_zone, BCLASS_BLUNT, spell_name = "Storm of Psydon (Kick)", exact_zone = TRUE) + playsound(get_turf(target), pick('sound/combat/hits/blunt/genblunt (1).ogg','sound/combat/hits/blunt/genblunt (2).ogg','sound/combat/hits/blunt/genblunt (3).ogg'), 100, TRUE) + var/atom/throw_target = get_edge_target_turf(user, get_dir(user, target)) + target.throw_at(throw_target, 3, 4) + + log_combat(user, target, "used Storm of Psydon (lame)") diff --git a/code/modules/spells/spell_types/pointed/tumult.dm b/code/modules/spells/spell_types/pointed/tumult.dm new file mode 100644 index 00000000000..cfa376de84d --- /dev/null +++ b/code/modules/spells/spell_types/pointed/tumult.dm @@ -0,0 +1,454 @@ +#define TUMULT_MODE_ERUPT "erupt" +#define TUMULT_MODE_CHARGE "charge" + +#define MT_ROCKSHOT "rockshot" +#define ROCKSHOT_DR_DURATION 1 SECONDS + +/obj/projectile/magic/gravel_blast + name = "gravel shot" + icon = 'icons/obj/magic_projectiles.dmi' + icon_state = "stone" + damage = 26 + nodamage = FALSE + damage_type = BRUTE + woundclass = BCLASS_BLUNT + flag = "blunt" + range = SPELL_RANGE_PROJECTILE + speed = 4 + accuracy = 50 + hitsound = 'sound/combat/hits/onstone/wallhit.ogg' + ricochets_max = 2 + ricochet_chance = 80 + var/reduced_damage = 11 + +/obj/projectile/magic/gravel_blast/arc + name = "arced gravel shot" + damage = 20 + arcshot = TRUE + +/obj/projectile/magic/gravel_blast/process_hit(atom/target) + if(ismob(target)) + var/mob/living/M = target + if(M == firer) + damage = round(damage / 2) + else if(M.mob_timers[MT_ROCKSHOT] && world.time < M.mob_timers[MT_ROCKSHOT] + ROCKSHOT_DR_DURATION) + damage = reduced_damage + else + M.mob_timers[MT_ROCKSHOT] = world.time + return ..() + +/obj/projectile/magic/gravel_blast/on_hit(target) + if(ismob(target)) + var/mob/living/M = target + if(M.anti_magic_check()) + visible_message(span_warning("[src] shatters harmlessly against [target]!")) + playsound(get_turf(target), 'sound/magic/magic_nulled.ogg', 100) + qdel(src) + return BULLET_ACT_BLOCK + . = ..() + +/obj/structure/earthen_wall + name = "earthen wall" + desc = "A wall of conjured stone. It will crumble in time." + icon = 'icons/obj/flora/rocks.dmi' + icon_state = "basalt1" + break_sound = 'sound/combat/hits/onstone/stonedeath.ogg' + attacked_sound = list('sound/combat/hits/onstone/wallhit.ogg', 'sound/combat/hits/onstone/wallhit2.ogg', 'sound/combat/hits/onstone/wallhit3.ogg') + density = TRUE + opacity = TRUE + max_integrity = 300 + var/timeleft = 10 SECONDS + +/obj/structure/earthen_wall/Initialize() + . = ..() + if(timeleft) + QDEL_IN(src, timeleft) + +/obj/structure/earthen_pillar + name = "stone pillar" + desc = "A pillar of conjured stone. Sturdy, but not indestructible. Shatters into gravel when destroyed." + icon = 'icons/obj/flora/rocks.dmi' + icon_state = "basalt1" + break_sound = 'sound/combat/hits/onstone/stonedeath.ogg' + attacked_sound = list('sound/combat/hits/onstone/wallhit.ogg', 'sound/combat/hits/onstone/wallhit2.ogg', 'sound/combat/hits/onstone/wallhit3.ogg') + density = TRUE + opacity = TRUE + max_integrity = 150 + anchored = TRUE + var/datum/weakref/caster_ref + var/fragment_count = 3 + var/fragment_damage = 15 + +/obj/structure/earthen_pillar/Destroy() + caster_ref = null + return ..() + +/obj/structure/earthen_pillar/atom_break(damage_flag, silent) + shatter_fragments() + return ..() + +/obj/structure/earthen_pillar/proc/shatter_fragments() + var/turf/T = get_turf(src) + if(!T) + return + var/mob/caster = caster_ref?.resolve() + var/list/dirs = list(NORTH, SOUTH, EAST, WEST, NORTHEAST, NORTHWEST, SOUTHEAST, SOUTHWEST) + for(var/i in 1 to fragment_count) + var/dir = pick_n_take(dirs) + var/turf/target = get_ranged_target_turf(T, dir, 3) + var/obj/projectile/magic/gravel_blast/frag = new(T) + frag.damage = fragment_damage + frag.ricochets_max = 0 + if(caster) + frag.firer = caster + frag.preparePixelProjectile(target, T) + frag.fire() + +/datum/action/cooldown/spell/tumult + button_icon = 'icons/mob/actions/spells/mage_geomancy.dmi' + name = "Tumult" + desc = "Erupts stone from the ground, or become one yourself and bowls through your foes! switch mode with Shift-G.\n\ + CAIRN erupts a cairn from a spot you choose, hitting those caught and leaving them Vulnerable, with more damage in the center. You are unharmed by the spell.\n\ + RAMSTAM turns you into a rolling boulders that hurtle to a marked destination, restricted to the cardinal and diagonal direction. You batter asides anyone in the way for a small amount of damage. If you hit a wall, you will burst out gravel around you, deals damage to it, and ricochet to the spot you came from. If you are riposted, it will halt and exposes you. If you rolls through a stone pillar, you will shatter it for gravel bursts that you are immune to. You cannot steer once begun." + button_icon_state = "cairn" + sound = 'sound/combat/hits/onstone/stonedeath.ogg' + + click_to_activate = TRUE + cast_range = SPELL_RANGE_GROUND + self_cast_possible = TRUE + + spell_cost = 40 + + required_form = FORM_EARTH + required_technique = TECHNIQUE_CREATION + + invocation = "Surge, Terra!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_MINOR + charge_slowdown = CHARGING_SLOWDOWN_SMALL + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 10 SECONDS + + spell_impact_intensity = SPELL_IMPACT_MEDIUM + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/geo_mode = TUMULT_MODE_ERUPT + var/static/list/mode_labels = list(TUMULT_MODE_ERUPT = "CAIRN", TUMULT_MODE_CHARGE = "RAMSTAM") + var/erupt_cooldown = 10 SECONDS + var/charge_cooldown = 12 SECONDS + + var/telegraph_delay = 4 + var/erupt_direct = 40 + var/erupt_aoe = 20 + var/erupt_npc_mult = 2 + var/erupt_push = 1 + var/pillar_integrity = 150 + var/vuln_duration = 5 SECONDS + + var/max_tiles = 7 + var/telegraph_time = 3 + var/roll_speed = 1 + var/barrel_damage = 15 + var/knock_dist = 1 + var/charge_frag_count = 8 + var/charge_frag_damage = 12 + var/crash_structure_damage = 100 + var/rolling = FALSE + +/datum/action/cooldown/spell/tumult/Grant(mob/grant_to) + . = ..() + update_mode_maptext() + +/datum/action/cooldown/spell/tumult/toggle_alt_mode(mob/user) + if(rolling) + return FALSE + geo_mode = (geo_mode == TUMULT_MODE_ERUPT) ? TUMULT_MODE_CHARGE : TUMULT_MODE_ERUPT + cooldown_time = (geo_mode == TUMULT_MODE_CHARGE) ? charge_cooldown : erupt_cooldown + invocation = geo_mode == TUMULT_MODE_ERUPT ? "Surge, Terra!" : "Volve!" + to_chat(user, span_notice("Tumult set to: [mode_labels[geo_mode]].")) + update_mode_maptext() + build_all_button_icons() + return TRUE + +/datum/action/cooldown/spell/tumult/proc/update_mode_maptext() + var/label = mode_labels[geo_mode] + for(var/datum/hud/hud as anything in viewers) + var/atom/movable/screen/movable/action_button/B = viewers[hud] + var/atom/movable/screen/arc_maptext_holder/holder + for(var/atom/movable/screen/arc_maptext_holder/existing in B.vis_contents) + holder = existing + break + if(!holder) + holder = new(B) + B.vis_contents.Add(holder) + holder.maptext = MAPTEXT(label) + holder.maptext_x = 5 + holder.color = GLOB.form_colors[FORM_EARTH] + +/datum/action/cooldown/spell/tumult/cast(atom/cast_on) + cooldown_time = (geo_mode == TUMULT_MODE_CHARGE) ? charge_cooldown : erupt_cooldown + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + if(geo_mode == TUMULT_MODE_CHARGE) + return cast_charge(H, cast_on) + return cast_erupt(H, cast_on) + +/datum/action/cooldown/spell/tumult/proc/cast_erupt(mob/living/carbon/human/H, atom/cast_on) + var/turf/T = get_turf(cast_on) + if(!T) + return FALSE + var/turf/source_turf = get_turf(H) + if(T.z > H.z) + source_turf = get_step_multiz(source_turf, UP) + if(T.z < H.z) + source_turf = get_step_multiz(source_turf, DOWN) + if(!(T in get_hear(cast_range, source_turf))) + to_chat(H, span_warning("I can't cast where I can't see!")) + return FALSE + if(T.density) + to_chat(H, span_warning("There's no room to raise stone there!")) + return FALSE + for(var/obj/structure/S in T.contents) + if(S.density) + to_chat(H, span_warning("Something is already there!")) + return FALSE + new /obj/effect/temp_visual/trap/geomancy(T) + playsound(T, 'sound/combat/hits/onstone/wallhit.ogg', 60, TRUE) + addtimer(CALLBACK(src, PROC_REF(erupt_strike), T, H), telegraph_delay) + return TRUE + +/datum/action/cooldown/spell/tumult/proc/erupt_strike(turf/T, mob/living/carbon/human/caster) + if(QDELETED(caster) || caster.stat == DEAD) + return + playsound(T, 'sound/combat/hits/onstone/stonedeath.ogg', 100, TRUE, 4) + var/target_zone = caster.zone_selected || BODY_ZONE_CHEST + + for(var/mob/living/victim in T.contents) + if(victim == caster || victim.stat == DEAD) + continue + if(victim.anti_magic_check()) + victim.visible_message(span_warning("The erupting stone crumbles around [victim]!")) + playsound(get_turf(victim), 'sound/magic/magic_nulled.ogg', 100) + continue + arcyne_strike(caster, victim, null, erupt_direct, target_zone, BCLASS_BLUNT, \ + spell_name = "Cairn", damage_type = BRUTE, npc_simple_damage_mult = erupt_npc_mult, skip_animation = TRUE) + victim.apply_status_effect(/datum/status_effect/debuff/vulnerable, vuln_duration) + to_chat(victim, span_userdanger("Stone erupts beneath me!")) + new /obj/effect/temp_visual/spell_impact(get_turf(victim), spell_color, spell_impact_intensity) + var/push_dir = get_dir(T, victim) || get_dir(caster, victim) || pick(GLOB.cardinals) + victim.safe_throw_at(get_ranged_target_turf(victim, push_dir, erupt_push), erupt_push, 1, caster, force = MOVE_FORCE_STRONG) + + for(var/turf/affected in get_hear(1, T)) + if(affected == T) + continue + new /obj/effect/temp_visual/kinetic_blast(affected) + for(var/mob/living/victim in affected) + if(victim == caster || victim.stat == DEAD) + continue + if(victim.anti_magic_check()) + continue + arcyne_strike(caster, victim, null, erupt_aoe, target_zone, BCLASS_BLUNT, \ + spell_name = "Cairn", damage_type = BRUTE, npc_simple_damage_mult = erupt_npc_mult, skip_animation = TRUE) + victim.apply_status_effect(/datum/status_effect/debuff/vulnerable, vuln_duration) + var/push_dir = get_dir(T, victim) || get_dir(caster, victim) || pick(GLOB.cardinals) + victim.safe_throw_at(get_ranged_target_turf(victim, push_dir, erupt_push), erupt_push, 1, caster, force = MOVE_FORCE_STRONG) + + for(var/turf/struct_turf in get_hear(1, T)) + for(var/obj/structure/S in struct_turf) + S.take_damage(erupt_direct, BRUTE, "blunt") + + new /obj/effect/temp_visual/kinetic_blast(T) + var/obj/structure/earthen_pillar/pillar = new(T) + pillar.max_integrity = pillar_integrity + pillar.update_integrity(pillar_integrity) + pillar.caster_ref = WEAKREF(caster) + QDEL_IN(pillar, erupt_cooldown) + +/datum/action/cooldown/spell/tumult/proc/cast_charge(mob/living/carbon/human/H, atom/cast_on) + if(rolling) + return FALSE + var/turf/target = get_turf(cast_on) + if(!target) + return FALSE + var/dir = get_dir(H, target) + if(!dir) + return FALSE + var/dist = min(get_dist(get_turf(H), target), max_tiles) + if(dist < 1) + return FALSE + INVOKE_ASYNC(src, PROC_REF(do_ramstam), H, dir, dist) + return TRUE + +/datum/action/cooldown/spell/tumult/proc/do_ramstam(mob/living/carbon/human/H, dir, dist) + rolling = TRUE + H.setDir(dir) + var/commit_time = telegraph_time + dist * roll_speed * 2 + 4 + H.changeNext_move(commit_time) + H.apply_status_effect(/datum/status_effect/swingdelay/penalty/committed, commit_time, TRUE) + H.tempfixeye = TRUE + + var/list/indicators = list() + var/turf/cur = get_turf(H) + for(var/i in 1 to dist) + cur = get_step(cur, dir) + if(!cur || cur.density) + break + indicators += new /obj/effect/temp_visual/trap/geomancy(cur) + playsound(H, 'sound/foley/stone_scrape.ogg', 60, TRUE) + sleep(telegraph_time) + for(var/obj/effect/E in indicators) + qdel(E) + + if(QDELETED(H) || H.stat != CONSCIOUS) + end_ramstam(H, null, initial(H.alpha)) + return + + var/saved_alpha = H.alpha + H.alpha = 0 + var/obj/effect/ramstam_boulder/B = new(get_turf(H)) + B.setDir(dir) + + var/list/struck = list() + var/tiles_rolled = 0 + var/hit_wall = FALSE + var/stopped = FALSE + for(var/i in 1 to dist) + if(QDELETED(H) || H.stat != CONSCIOUS) + break + var/turf/next = get_step(H, dir) + if(!next || next.density) + roll_crash(H, next) + hit_wall = TRUE + break + var/obj/structure/earthen_pillar/pillar = (locate(/obj/structure/earthen_pillar) in next) + if(pillar) + pillar.caster_ref = WEAKREF(H) + pillar.shatter_fragments() + qdel(pillar) + else + var/blocked = FALSE + for(var/obj/structure/S in next) + if(S.density && !S.climbable) + blocked = TRUE + break + if(blocked) + roll_crash(H, next) + hit_wall = TRUE + break + var/countered = FALSE + for(var/mob/living/L in next) + if(L == H) + continue + if(countered) + stopped = TRUE + break + H.forceMove(next) + H.setDir(dir) + B.forceMove(next) + tiles_rolled++ + playsound(next, 'sound/foley/stone_scrape.ogg', 45, TRUE) + new /obj/effect/temp_visual/kinetic_blast(next) + for(var/mob/living/L in next) + if(L == H || (L in struck)) + continue + struck += L + roll_hit(H, L, dir) + sleep(roll_speed) + + if(hit_wall && !stopped) + roll_return(H, B, turn(dir, 180), tiles_rolled) + + end_ramstam(H, B, saved_alpha) + +/datum/action/cooldown/spell/tumult/proc/roll_return(mob/living/carbon/human/H, obj/effect/ramstam_boulder/B, return_dir, tiles) + for(var/i in 1 to tiles) + if(QDELETED(H) || H.stat != CONSCIOUS) + return + var/turf/next = get_step(H, return_dir) + if(!next || next.density) + return + var/blocked = FALSE + for(var/obj/structure/S in next) + if(S.density && !S.climbable) + blocked = TRUE + break + if(blocked) + return + H.forceMove(next) + H.setDir(return_dir) + if(!QDELETED(B)) + B.setDir(return_dir) + B.forceMove(next) + playsound(next, 'sound/foley/stone_scrape.ogg', 40, TRUE) + sleep(roll_speed) + +/datum/action/cooldown/spell/tumult/proc/end_ramstam(mob/living/carbon/human/H, obj/effect/ramstam_boulder/B, restore_alpha) + if(!QDELETED(B)) + qdel(B) + if(!QDELETED(H)) + H.alpha = restore_alpha + H.tempfixeye = FALSE + rolling = FALSE + +/datum/action/cooldown/spell/tumult/proc/roll_hit(mob/living/carbon/human/H, mob/living/L, dir) + if(L.anti_magic_check()) + return + if(ishuman(L)) + arcyne_strike(H, L, null, barrel_damage, H.zone_selected || BODY_ZONE_CHEST, BCLASS_BLUNT, \ + spell_name = "Ramstam", damage_type = BRUTE, npc_simple_damage_mult = 1.5, skip_animation = TRUE) + else + L.adjustBruteLoss(barrel_damage * (L.mind ? 1 : 1.5)) + new /obj/effect/temp_visual/spell_impact(get_turf(L), spell_color, spell_impact_intensity) + var/knockdir = pick(turn(dir, 90), turn(dir, -90)) + L.safe_throw_at(get_ranged_target_turf(L, knockdir, knock_dist), knock_dist, 1, H, force = MOVE_FORCE_STRONG) + +/datum/action/cooldown/spell/tumult/proc/roll_crash(mob/living/carbon/human/H, turf/obstacle) + var/turf/from = get_turf(H) + playsound(from, 'sound/combat/hits/onstone/stonedeath.ogg', 80, TRUE) + if(obstacle) + new /obj/effect/temp_visual/kinetic_blast(obstacle) + if(obstacle.density) + obstacle.take_damage(crash_structure_damage, BRUTE, "blunt") + for(var/obj/structure/S in obstacle) + if(S.density) + S.take_damage(crash_structure_damage, BRUTE, "blunt") + if(!from) + return + new /obj/effect/temp_visual/spell_impact(from, spell_color, SPELL_IMPACT_MEDIUM) + var/static/list/burst_dirs = list(NORTH, SOUTH, EAST, WEST, NORTHEAST, NORTHWEST, SOUTHEAST, SOUTHWEST) + for(var/i in 1 to charge_frag_count) + var/fdir = pick(burst_dirs) + var/turf/ftarget = get_ranged_target_turf(from, fdir, 4) + var/obj/projectile/magic/gravel_blast/frag = new(from) + frag.damage = charge_frag_damage + frag.range = 4 + frag.ricochets_max = 0 + frag.firer = H + frag.preparePixelProjectile(ftarget, from) + frag.fire() + +/datum/action/cooldown/spell/tumult/proc/riposte_counter(mob/living/carbon/human/H, mob/living/L) + H.visible_message(span_warning("[L] braces and turns [H]'s charge aside - [H] sprawls, exposed!"), span_userdanger("[L] catches my charge - I am thrown off and left exposed!")) + +/obj/effect/ramstam_boulder + icon = 'icons/obj/magic_projectiles.dmi' + icon_state = "boulder" + anchored = TRUE + density = FALSE + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + layer = MOB_LAYER + +/obj/effect/ramstam_boulder/Initialize(mapload) + . = ..() + SpinAnimation(15, -1) + +#undef TUMULT_MODE_ERUPT +#undef TUMULT_MODE_CHARGE + +#undef MT_ROCKSHOT +#undef ROCKSHOT_DR_DURATION diff --git a/code/modules/spells/spell_types/pointed/verglas.dm b/code/modules/spells/spell_types/pointed/verglas.dm new file mode 100644 index 00000000000..c635f3e4aac --- /dev/null +++ b/code/modules/spells/spell_types/pointed/verglas.dm @@ -0,0 +1,148 @@ +#define VERGLAS_MODE_LAY 1 +#define VERGLAS_MODE_HOLD 2 +#define VERGLAS_DURATION (15 SECONDS) + +/datum/action/cooldown/spell/verglas + button_icon = 'icons/mob/actions/spells/mage_cryomancy.dmi' + name = "Verglas" + desc = "Lay a 7x7 patch of treacherous ice; anyone who steps onto it slides in the direction they were moving until they run off. \ + Use the Alt Mode keybind to switch between Lay (cast and forget) and Hold (maintained by concentration for as long as you like, but shattered the instant you are struck or cast another spell)." + button_icon_state = "snap_freeze" + + click_to_activate = TRUE + cast_range = SPELL_RANGE_GROUND + + required_form = FORM_ICE + required_technique = TECHNIQUE_CREATION + + invocation = "Gelu Fallax!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_MAJOR + charge_slowdown = CHARGING_SLOWDOWN_MEDIUM + cooldown_time = 20 SECONDS + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/verglas_radius = 3 + var/verglas_mode = VERGLAS_MODE_LAY + +/datum/action/cooldown/spell/verglas/Grant(mob/grant_to) + . = ..() + update_mode_maptext() + +/datum/action/cooldown/spell/verglas/toggle_alt_mode(mob/user) + verglas_mode = (verglas_mode == VERGLAS_MODE_LAY) ? VERGLAS_MODE_HOLD : VERGLAS_MODE_LAY + update_mode_maptext() + return TRUE + +/datum/action/cooldown/spell/verglas/proc/update_mode_maptext() + var/label = (verglas_mode == VERGLAS_MODE_LAY) ? "LAY" : "HOLD" + var/label_color = (verglas_mode == VERGLAS_MODE_LAY) ? GLOW_COLOR_ICE : "#ff6a3d" + for(var/datum/hud/hud as anything in viewers) + var/atom/movable/screen/movable/action_button/B = viewers[hud] + var/atom/movable/screen/arc_maptext_holder/holder + for(var/atom/movable/screen/arc_maptext_holder/existing in B.vis_contents) + holder = existing + break + if(!holder) + holder = new(B) + B.vis_contents.Add(holder) + holder.maptext = MAPTEXT(label) + holder.maptext_x = 5 + holder.color = label_color + +/datum/action/cooldown/spell/verglas/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + if(verglas_mode == VERGLAS_MODE_HOLD && H.has_status_effect(/datum/status_effect/verglas_concentration)) + H.remove_status_effect(/datum/status_effect/verglas_concentration) + return TRUE + + var/turf/centerpoint = get_turf(cast_on) + if(!centerpoint) + return FALSE + + var/turf/source_turf = get_turf(H) + if(centerpoint.z > H.z) + source_turf = get_step_multiz(source_turf, UP) + if(centerpoint.z < H.z) + source_turf = get_step_multiz(source_turf, DOWN) + if(!(centerpoint in get_hear(cast_range, source_turf))) + to_chat(H, span_warning("I can't cast where I can't see!")) + return FALSE + + var/hold = (verglas_mode == VERGLAS_MODE_HOLD) + playsound(centerpoint, 'sound/spellbooks/crystal.ogg', 80, TRUE) + var/list/spawned = list() + for(var/turf/open/T in get_hear(verglas_radius, centerpoint)) + if(!(T in get_hear(cast_range, source_turf))) + continue + if(T.density) + continue + T.MakeSlippery(TURF_WET_PERMAFROST, hold ? MAXIMUM_WET_TIME : VERGLAS_DURATION) + spawned |= T + + if(hold && length(spawned)) + H.apply_status_effect(/datum/status_effect/verglas_concentration, spawned, centerpoint) + return TRUE + +/datum/status_effect/verglas_concentration + id = "verglas_concentration" + duration = -1 + alert_type = /atom/movable/screen/alert/status_effect/verglas_concentration + var/list/tiles + var/atom/field_center + var/datum/beam/link_beam + +/datum/status_effect/verglas_concentration/on_creation(mob/living/new_owner, list/field_tiles, atom/center) + tiles = field_tiles + field_center = center + . = ..() + +/datum/status_effect/verglas_concentration/on_apply() + . = ..() + RegisterSignal(owner, COMSIG_MOB_APPLY_DAMAGE, PROC_REF(on_struck)) + RegisterSignal(owner, COMSIG_MOB_CAST_SPELL, PROC_REF(on_cast)) + if(field_center) + link_beam = owner.Beam(field_center, "b_beam", 'icons/effects/beam.dmi', INFINITY, 32, /obj/effect/ebeam/verglas_link) + owner.balloon_alert_to_viewers("concentrating - fragile!") + +/datum/status_effect/verglas_concentration/on_remove() + . = ..() + QDEL_NULL(link_beam) + field_center = null + UnregisterSignal(owner, COMSIG_MOB_APPLY_DAMAGE) + UnregisterSignal(owner, COMSIG_MOB_CAST_SPELL) + for(var/turf/open/T in tiles) + T.MakeDry(TURF_WET_PERMAFROST) + tiles = list() + to_chat(owner, span_warning("My concentration breaks and the verglas melts away!")) + +/obj/effect/ebeam/verglas_link + color = "#ff6a3d" + +/datum/status_effect/verglas_concentration/proc/on_struck(mob/source, damage, damagetype, def_zone) + SIGNAL_HANDLER + if(!damage) + return + owner.remove_status_effect(/datum/status_effect/verglas_concentration) + +/datum/status_effect/verglas_concentration/proc/on_cast(mob/source, datum/action/cooldown/spell/spell, atom/cast_on) + SIGNAL_HANDLER + if(istype(spell, /datum/action/cooldown/spell/verglas)) + return + owner.remove_status_effect(/datum/status_effect/verglas_concentration) + +/atom/movable/screen/alert/status_effect/verglas_concentration + name = "Verglas Concentration" + desc = "I am concentrating to hold the verglas in place. A single blow or casting another spell will shatter it." + icon_state = "debuff" + +#undef VERGLAS_MODE_LAY +#undef VERGLAS_MODE_HOLD +#undef VERGLAS_DURATION diff --git a/code/modules/spells/spell_types/pointed/void_beam.dm b/code/modules/spells/spell_types/pointed/void_beam.dm new file mode 100644 index 00000000000..10790bde64c --- /dev/null +++ b/code/modules/spells/spell_types/pointed/void_beam.dm @@ -0,0 +1,180 @@ +#define VOID_BEAM_SLOWDOWN_ID "void_beam_windup" + +/datum/action/cooldown/spell/void_beam + button_icon = 'icons/mob/actions/spells/mage_telomancy.dmi' + name = "Void Beam" + desc = "Fire a lance of raw arcyne force that exposes your foe. It is well telegraphed and does a decent amount of damaage." + button_icon_state = "void_beam" + sound = 'sound/magic/soulshot.ogg' + + click_to_activate = TRUE + cast_range = 8 + + spell_cost = 40 + cooldown_time = 20 SECONDS + + required_form = FORM_ARCANE + required_technique = TECHNIQUE_DESTRUCTION + + invocation = "Scintilla Magistra!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_POKE + charge_slowdown = CHARGING_SLOWDOWN_SMALL + charge_sound = 'sound/magic/charging.ogg' + + spell_impact_intensity = SPELL_IMPACT_MEDIUM + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/beam_range = 8 + var/beam_delay = 1.5 SECONDS + var/beam_damage = 100 + var/push_dist = 1 + var/expose_duration = 6 SECONDS + var/beam_slowdown = 2 + +/datum/action/cooldown/spell/void_beam/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H) || !get_turf(H)) + return FALSE + + H.add_movespeed_modifier(VOID_BEAM_SLOWDOWN_ID, update = TRUE, priority = 100, multiplicative_slowdown = beam_slowdown, movetypes = GROUND) + playsound(H, 'sound/magic/charging.ogg', 60, TRUE, channel = CHANNEL_CHARGED_SPELL) + INVOKE_ASYNC(src, PROC_REF(windup_and_fire), H, cast_on) + return TRUE + +/datum/action/cooldown/spell/void_beam/proc/windup_and_fire(mob/living/carbon/human/H, atom/target) + H.apply_status_effect(/datum/status_effect/swingdelay/penalty, beam_delay + 2) + var/list/warnings = list() + var/last_dir = null + var/turf/last_origin = null + var/list/turf/line = list() + var/elapsed = 0 + while(elapsed < beam_delay) + if(QDELETED(H) || H.stat != CONSCIOUS) + finish_windup(H, warnings) + return + var/turf/origin = get_turf(H) + var/turf/tt = get_turf(target) + var/beam_dir = cardinal_toward(origin, tt) || nearest_cardinal(H.dir) + if(beam_dir != last_dir || origin != last_origin) + last_dir = beam_dir + last_origin = origin + line = build_beam_line(origin, beam_dir) + redraw_warnings(line, warnings) + sleep(2) + elapsed += 2 + + finish_windup(H, warnings) + if(QDELETED(H) || isnull(last_dir)) + return + var/turf/origin = get_turf(H) + line = build_beam_line(origin, last_dir) + void_beam_detonate(line, H, origin, last_dir, beam_damage, push_dist, expose_duration, src, name) + +/datum/action/cooldown/spell/void_beam/proc/build_beam_line(turf/origin, beam_dir) + var/list/turf/line = list() + var/turf/current = origin + for(var/i in 1 to beam_range) + var/turf/next = get_step(current, beam_dir) + if(!next || next.density) + break + line += next + current = next + return line + +/datum/action/cooldown/spell/void_beam/proc/cardinal_toward(turf/origin, turf/tt) + if(!origin || !tt || tt == origin) + return null + var/dx = tt.x - origin.x + var/dy = tt.y - origin.y + if(abs(dx) >= abs(dy)) + return (dx > 0) ? EAST : WEST + return (dy > 0) ? NORTH : SOUTH + +/datum/action/cooldown/spell/void_beam/proc/nearest_cardinal(dir) + if(dir & NORTH) + return NORTH + if(dir & SOUTH) + return SOUTH + if(dir & EAST) + return EAST + if(dir & WEST) + return WEST + return SOUTH + +/datum/action/cooldown/spell/void_beam/proc/redraw_warnings(list/turf/line, list/warnings) + for(var/obj/effect/old in warnings) + qdel(old) + warnings.Cut() + for(var/turf/T in line) + warnings += new /obj/effect/temp_visual/trap/telomancy(T) + +/datum/action/cooldown/spell/void_beam/proc/finish_windup(mob/living/carbon/human/H, list/warnings) + for(var/obj/effect/old in warnings) + qdel(old) + warnings.Cut() + if(H && !QDELETED(H)) + H.remove_movespeed_modifier(VOID_BEAM_SLOWDOWN_ID) + H.stop_sound_channel(CHANNEL_CHARGED_SPELL) + +/proc/void_beam_detonate(list/turf/line, mob/living/carbon/human/caster, turf/origin, beam_dir, damage = 35, push_dist = 1, expose_dur = 6 SECONDS, datum/action/cooldown/spell/guard_source, spell_name = "Void Beam") + if(caster) + caster.remove_movespeed_modifier(VOID_BEAM_SLOWDOWN_ID) + if(!length(line)) + return + + playsound(origin, 'sound/magic/soulshot.ogg', 90, TRUE, 4) + var/index = 0 + var/last_index = length(line) + var/list/struck = list() + + for(var/turf/T in line) + index++ + var/obj/effect/temp_visual/void_beam/seg = new(T) + seg.setDir(beam_dir) + if(index == 1) + seg.icon_state = "obeliskbeam_start" + else if(index == last_index) + seg.icon_state = "obeliskbeam_end" + + for(var/obj/structure/S in T) + S.take_damage(damage, BRUTE, "blunt", FALSE) + + for(var/mob/living/L in T.contents) + if(L in struck) + continue + struck += L + if(L.anti_magic_check()) + L.visible_message(span_warning("The beam splinters against [L]!")) + playsound(T, 'sound/magic/magic_nulled.ogg', 100) + continue + if(istype(caster) && !QDELETED(caster) && ishuman(L)) + arcyne_strike(caster, L, null, damage, caster.zone_selected, \ + BCLASS_PIERCE, spell_name = spell_name, \ + damage_type = BRUTE, npc_simple_damage_mult = 1, \ + skip_animation = TRUE) + else + L.adjustBruteLoss(damage) + SEND_SIGNAL(L, COMSIG_ATOM_WAS_ATTACKED, caster, damage) + L.apply_status_effect(/datum/status_effect/debuff/exposed, expose_dur) + var/push_dir = get_dir(origin, L) || beam_dir + L.safe_throw_at(get_ranged_target_turf(L, push_dir, push_dist), push_dist, 1, caster, force = MOVE_FORCE_STRONG) + new /obj/effect/temp_visual/spell_impact(get_turf(L), GLOB.form_colors[FORM_ARCANE], SPELL_IMPACT_MEDIUM) + +/obj/effect/temp_visual/void_beam + name = "void beam" + icon = 'icons/effects/effects.dmi' + icon_state = "obeliskbeam_mid" + layer = ABOVE_MOB_LAYER + plane = GAME_PLANE_UPPER + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + light_color = "#9400D3" + light_power = 4 + light_outer_range = 3 + duration = 6 + +#undef VOID_BEAM_SLOWDOWN_ID diff --git a/code/modules/spells/spell_types/pointed/wither.dm b/code/modules/spells/spell_types/pointed/wither.dm new file mode 100644 index 00000000000..6bd78ec488d --- /dev/null +++ b/code/modules/spells/spell_types/pointed/wither.dm @@ -0,0 +1,92 @@ +/datum/action/cooldown/spell/wither + button_icon = 'icons/mob/actions/spells/mage_hex.dmi' + name = "Wither" + desc = "Lash out a delayed line of dark magic, sapping the physical prowess of all in its path.\n\ + The line telegraphs for a moment before striking every tile at once." + button_icon_state = "wither" + sound = 'sound/magic/shadowstep_destination.ogg' + + click_to_activate = TRUE + cast_range = SPELL_RANGE_GROUND + + spell_cost = 60 + + required_form = FORM_DEATH + required_technique = TECHNIQUE_ALTERATION + + invocation = "Arescentem!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_MAJOR + charge_slowdown = CHARGING_SLOWDOWN_MEDIUM + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 20 SECONDS + + spell_impact_intensity = SPELL_IMPACT_MEDIUM + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + /// Ticks of telegraph before the line strikes. + var/strike_delay = 4 + +/datum/action/cooldown/spell/wither/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/turf/target_turf = get_turf(cast_on) + if(!target_turf) + return FALSE + + var/turf/source_turf = get_turf(H) + if(!(target_turf in get_hear(cast_range, source_turf))) + to_chat(H, span_warning("I can't cast where I can't see!")) + return FALSE + + var/list/affected_turfs = list() + for(var/turf/line_turf in getline(source_turf, target_turf)) + if(line_turf == source_turf) + continue + if(!(line_turf in get_hear(cast_range, source_turf))) + continue + affected_turfs += line_turf + new /obj/effect/temp_visual/trap/wither_line(line_turf, strike_delay) + + if(!length(affected_turfs)) + return FALSE + + addtimer(CALLBACK(src, PROC_REF(strike_line), affected_turfs), strike_delay) + return TRUE + +/datum/action/cooldown/spell/wither/proc/strike_line(list/turfs) + for(var/turf/damage_turf as anything in turfs) + new /obj/effect/temp_visual/wither_strike(damage_turf) + playsound(damage_turf, 'sound/magic/shadowstep_destination.ogg', 50) + for(var/mob/living/L in damage_turf.contents) + if(L.anti_magic_check()) + L.visible_message(span_warning("The dark magic fades away around [L]!")) + playsound(damage_turf, 'sound/magic/magic_nulled.ogg', 100) + continue + L.apply_status_effect(/datum/status_effect/buff/witherd) + +/obj/effect/temp_visual/trap/wither_line + icon = 'icons/effects/effects.dmi' + icon_state = "curse" + color = "#800080" + light_outer_range = 0 + duration = 1 SECONDS + alpha = 70 + +/obj/effect/temp_visual/trap/wither_line/Initialize(mapload, duration_override) + if(duration_override) + duration = duration_override + . = ..() + +/obj/effect/temp_visual/wither_strike + icon = 'icons/effects/effects.dmi' + icon_state = "curseblob" + light_outer_range = 2 + light_color = "#800080" + duration = 1 SECONDS diff --git a/code/modules/spells/spell_types/telegraphed_strike/_base.dm b/code/modules/spells/spell_types/telegraphed_strike/_base.dm new file mode 100644 index 00000000000..157fb769ba3 --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/_base.dm @@ -0,0 +1,319 @@ +/datum/action/cooldown/spell/telegraphed_strike + click_to_activate = FALSE + self_cast_possible = TRUE + charge_required = FALSE + invocation_type = INVOCATION_SHOUT + invocation = "FIX ME!" + + var/damage = 50 + var/strike_damage_type = BRUTE + var/npc_simple_damage_mult = 1.5 + var/blade_class = BCLASS_CUT + var/strike_armor_pen = 0 + var/detonate_sound = 'sound/combat/newstuck.ogg' + var/strike_sound = 'sound/magic/blade_burst.ogg' + var/windup_time = 8 + var/charging_slowdown = 0 + var/committed_strike = TRUE + var/interruptible = FALSE + var/lock_direction = FALSE + var/redraw_interval = 2 + var/sweep_step = 1 + var/impact_delay = 0 + var/stop_at_dense = TRUE + var/damage_structures = TRUE + var/structure_damage = 0 + var/swipe_state = null + var/vuln_on_hit = 0 + var/immobilize_on_hit = 0 + var/telegraph_type = /obj/effect/temp_visual/trap + var/requires_weapon = FALSE + var/weapon_missing_message = "I need a weapon to strike with!" + var/sweep_hit_count = 0 + var/list/struck_obstacles + var/list/struck_mobs + +/datum/action/cooldown/spell/telegraphed_strike/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + if(requires_weapon && !get_strike_weapon(H)) + to_chat(H, span_warning(weapon_missing_message)) + return FALSE + var/strike_duration = windup_time + impact_delay + max(0, length(get_pattern_offsets()) - 1) * sweep_step + if(committed_strike) + H.changeNext_move(strike_duration) + if(interruptible) + H.apply_status_effect(/datum/status_effect/swingdelay/disrupt, strike_duration + 2, FALSE) + else + H.apply_status_effect(/datum/status_effect/swingdelay/penalty/committed, strike_duration + 2, TRUE) + INVOKE_ASYNC(src, PROC_REF(windup_and_strike), H) + return TRUE + +/datum/action/cooldown/spell/telegraphed_strike/proc/windup_and_strike(mob/living/carbon/human/H) + var/list/indicator = list() + var/iterations = max(1, round(windup_time / redraw_interval)) + var/turf/last_turf + var/last_facing + var/locked_facing = lock_direction ? get_cardinal(H.dir) : null + if(locked_facing) + H.setDir(locked_facing) + H.tempfixeye = TRUE + if(charging_slowdown) + H.add_movespeed_modifier("telegraphed_strike_windup", TRUE, 100, override = TRUE, multiplicative_slowdown = charging_slowdown) + for(var/i in 1 to iterations) + if(QDELETED(H) || H.stat != CONSCIOUS || strike_disrupted(H)) + break + var/facing = locked_facing || get_cardinal(H.dir) + if(get_turf(H) != last_turf || facing != last_facing) + last_turf = get_turf(H) + last_facing = facing + draw_indicators(H, facing, indicator) + sleep(redraw_interval) + if(charging_slowdown && !QDELETED(H)) + H.remove_movespeed_modifier("telegraphed_strike_windup") + if(locked_facing && !QDELETED(H)) + H.tempfixeye = FALSE + if(QDELETED(H) || H.stat != CONSCIOUS || strike_disrupted(H)) + clear_indicators(indicator) + return + strike(H, locked_facing || get_cardinal(H.dir), indicator) + +/datum/action/cooldown/spell/telegraphed_strike/proc/strike_disrupted(mob/living/carbon/human/H) + if(!interruptible) + return FALSE + var/datum/status_effect/swingdelay/disrupt/SW = H.has_status_effect(/datum/status_effect/swingdelay/disrupt) + return SW && SW.is_disrupted() + +/datum/action/cooldown/spell/telegraphed_strike/proc/draw_indicators(mob/living/carbon/human/H, facing, list/indicator) + draw_offsets(H, facing, indicator, get_pattern_offsets()) + +/datum/action/cooldown/spell/telegraphed_strike/proc/draw_offsets(mob/living/carbon/human/H, facing, list/indicator, list/offs) + var/turf/origin = get_turf(H) + var/list/wanted = list() + if(origin) + for(var/list/off in offs) + var/list/r = rotate_offset(off[1], off[2], facing) + var/turf/T = locate(origin.x + r[1], origin.y + r[2], origin.z) + if(!T || T.density) + continue + if(stop_at_dense && path_blocked(origin, T)) + continue + wanted |= T + for(var/obj/effect/old in indicator.Copy()) + var/turf/ot = get_turf(old) + if(!QDELETED(old) && (ot in wanted)) + wanted -= ot + else + indicator -= old + qdel(old) + for(var/turf/T in wanted) + indicator += new telegraph_type(T) + +/datum/action/cooldown/spell/telegraphed_strike/proc/clear_indicators(list/indicator) + for(var/obj/effect/old in indicator) + if(!QDELETED(old)) + qdel(old) + +/datum/action/cooldown/spell/telegraphed_strike/proc/strike(mob/living/carbon/human/H, facing, list/indicator) + if(!length(get_pattern_offsets())) + clear_indicators(indicator) + return + if(strike_sound) + playsound(get_turf(H), strike_sound, 75, TRUE) + var/atom/movable/visual = do_blade_animation(H, facing) + INVOKE_ASYNC(src, PROC_REF(execute_hits), H, facing, indicator, visual) + +/datum/action/cooldown/spell/telegraphed_strike/proc/execute_hits(mob/living/carbon/human/H, facing, list/indicator, atom/movable/visual) + var/turf/last_turf = get_turf(H) + draw_indicators(H, facing, indicator) + var/elapsed = 0 + while(elapsed < impact_delay) + if(QDELETED(H) || H.stat != CONSCIOUS) + clear_indicators(indicator) + return + if(get_turf(H) != last_turf) + last_turf = get_turf(H) + draw_indicators(H, facing, indicator) + sleep(redraw_interval) + elapsed += redraw_interval + if(!QDELETED(H) && H.stat == CONSCIOUS) + on_impact(H, facing, visual) + var/list/bands = get_sweep_bands() + var/deflected = FALSE + sweep_hit_count = 0 + struck_obstacles = list() + struck_mobs = list() + for(var/b in 1 to length(bands)) + if(QDELETED(H) || H.stat != CONSCIOUS) + break + var/turf/origin = get_turf(H) + for(var/list/off in bands[b]) + var/list/r = rotate_offset(off[1], off[2], facing) + var/turf/T = origin ? locate(origin.x + r[1], origin.y + r[2], origin.z) : null + if(!T) + continue + if(stop_at_dense) + var/turf/blocker = path_blocked(origin, T) + if(blocker) + if(damage_structures) + damage_obstacles(blocker) + continue + if(damage_structures) + damage_obstacles(T) + deflected = hit_turf(H, T, facing, deflected) + if(swipe_state) + var/obj/effect/temp_visual/dir_setting/attack_effect/slash = new(T, facing) + slash.icon_state = swipe_state + var/list/remaining = list() + for(var/j in b + 1 to length(bands)) + remaining += bands[j] + draw_offsets(H, facing, indicator, remaining) + if(sweep_step > 0 && b < length(bands)) + sleep(sweep_step) + clear_indicators(indicator) + on_strike_complete(H, sweep_hit_count, deflected) + +/datum/action/cooldown/spell/telegraphed_strike/proc/on_impact(mob/living/carbon/human/H, facing, atom/movable/visual) + return + +/datum/action/cooldown/spell/telegraphed_strike/proc/hit_turf(mob/living/carbon/human/H, turf/T, facing, deflected = FALSE) + if(QDELETED(H) || QDELETED(T)) + return deflected + var/obj/item/weapon = get_strike_weapon(H) + var/dmg = get_strike_damage() + var/hit_any = FALSE + for(var/mob/living/L in T.contents) + if(L == H) + continue + if(L in struck_mobs) + continue + struck_mobs += L + if(L.anti_magic_check()) + on_antimagic_block(L) + continue + hit_any = TRUE + sweep_hit_count++ + if(ishuman(L)) + var/target_zone = H.zone_selected || BODY_ZONE_CHEST + arcyne_strike(H, L, weapon, dmg, target_zone, blade_class, armor_penetration = strike_armor_pen, spell_name = name, damage_type = strike_damage_type, npc_simple_damage_mult = npc_simple_damage_mult, skip_animation = TRUE) + else + var/actual_damage = dmg + if(!L.mind) + actual_damage *= npc_simple_damage_mult + if(strike_damage_type == BURN) + L.adjustFireLoss(actual_damage) + else + L.adjustBruteLoss(actual_damage) + if(vuln_on_hit) + L.apply_status_effect(/datum/status_effect/debuff/vulnerable, vuln_on_hit) + if(immobilize_on_hit) + L.apply_status_effect(STATUS_EFFECT_IMMOBILIZED, immobilize_on_hit) + new /obj/effect/temp_visual/spell_impact(get_turf(L), spell_color, spell_impact_intensity) + on_hit_target(H, L, facing) + if(hit_any && detonate_sound) + playsound(T, detonate_sound, 65, TRUE) + return deflected + +/datum/action/cooldown/spell/telegraphed_strike/proc/path_blocked(turf/origin, turf/target) + if(!origin || !target || origin == target) + return null + for(var/turf/step in getline(origin, target)) + if(step == origin) + continue + if(step == target) + break + if(step.density) + return step + for(var/obj/structure/S in step) + if(S.density && !S.climbable) + return step + return null + +/datum/action/cooldown/spell/telegraphed_strike/proc/damage_obstacles(turf/T) + if(!T || (T in struck_obstacles)) + return + struck_obstacles += T + var/dmg = structure_damage ? structure_damage : damage + var/hit_any = FALSE + for(var/obj/structure/S in T) + S.take_damage(dmg, BRUTE, "blunt", TRUE) + hit_any = TRUE + if(hit_any) + new /obj/effect/temp_visual/spell_impact(T, spell_color, spell_impact_intensity) + if(detonate_sound) + playsound(T, detonate_sound, 65, TRUE) + +/datum/action/cooldown/spell/telegraphed_strike/proc/forward_reach(mob/living/carbon/human/H, facing, max_steps) + var/reach = 0 + var/turf/current = get_turf(H) + for(var/i in 1 to max_steps) + var/turf/next = get_step(current, facing) + if(!next || next.density) + break + current = next + reach++ + for(var/obj/structure/S in next) + if(S.density && !S.climbable) + return reach + return reach + +/datum/action/cooldown/spell/telegraphed_strike/proc/get_strike_weapon(mob/living/carbon/human/H) + return null + +/datum/action/cooldown/spell/telegraphed_strike/proc/get_strike_damage() + return damage + +/datum/action/cooldown/spell/telegraphed_strike/proc/on_hit_target(mob/living/carbon/human/H, mob/living/L, facing) + return + +/datum/action/cooldown/spell/telegraphed_strike/proc/on_strike_complete(mob/living/carbon/human/H, hit_count, deflected) + return + +/datum/action/cooldown/spell/telegraphed_strike/proc/on_antimagic_block(mob/living/L) + L.visible_message(span_warning("The arcyne blades dispel as they near [L]!")) + playsound(get_turf(L), 'sound/magic/magic_nulled.ogg', 100) + +/datum/action/cooldown/spell/telegraphed_strike/proc/do_blade_animation(mob/living/carbon/human/H, facing) + return + +/datum/action/cooldown/spell/telegraphed_strike/proc/get_pattern_offsets() + return list() + +/datum/action/cooldown/spell/telegraphed_strike/proc/get_sweep_bands() + var/list/bands = list() + for(var/list/off in get_pattern_offsets()) + bands += list(list(off)) + return bands + +/datum/action/cooldown/spell/telegraphed_strike/proc/get_cardinal(dir) + if(dir & NORTH) + return NORTH + if(dir & SOUTH) + return SOUTH + if(dir & EAST) + return EAST + if(dir & WEST) + return WEST + return NORTH + +/datum/action/cooldown/spell/telegraphed_strike/proc/rotate_offset(dx, dy, facing) + switch(facing) + if(SOUTH) + return list(-dx, -dy) + if(EAST) + return list(dy, -dx) + if(WEST) + return list(-dy, dx) + return list(dx, dy) + +/datum/action/cooldown/spell/telegraphed_strike/proc/facing_position_angle(facing) + switch(facing) + if(EAST) + return 90 + if(SOUTH) + return 180 + if(WEST) + return 270 + return 0 diff --git a/code/modules/spells/spell_types/telegraphed_strike/dragon_breath.dm b/code/modules/spells/spell_types/telegraphed_strike/dragon_breath.dm new file mode 100644 index 00000000000..789a637c3c5 --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/dragon_breath.dm @@ -0,0 +1,85 @@ +/datum/action/cooldown/spell/telegraphed_strike/dragons_breath + button_icon = 'icons/mob/actions/spells/mage_pyromancy.dmi' + name = "Dragon's Breath" + desc = "Let loose a wide cone of flame that erupts forward, burning everything in its path and pushing back anyone it hits. \ + The windup leaves you committed and wide open." + button_icon_state = "fire_blast" + sound = 'sound/magic/fireball.ogg' + + required_form = FORM_FIRE + required_technique = TECHNIQUE_CREATION + + invocation_type = INVOCATION_SHOUT + invocation = "Exhala, Draco!" + + spell_cost = 60 + + cooldown_time = 20 SECONDS + + spell_impact_intensity = SPELL_IMPACT_HIGH + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN + + damage = 60 + strike_damage_type = BURN + blade_class = BCLASS_BURN + npc_simple_damage_mult = 2 + committed_strike = TRUE + interruptible = FALSE + charging_slowdown = CHARGING_SLOWDOWN_SMALL + windup_time = 1.6 SECONDS + sweep_step = 0 + strike_sound = 'sound/magic/fireball.ogg' + detonate_sound = 'sound/misc/explode/incendiary (1).ogg' + + var/cone_range = 4 + var/push_dist = 2 + +/datum/action/cooldown/spell/telegraphed_strike/dragons_breath/proc/cone_rings() + var/list/rings = list() + for(var/d in 1 to cone_range) + var/list/ring = list() + var/half = max(1, round(d / 2)) + for(var/lat in -half to half) + ring += list(list(lat, d)) + rings += list(ring) + return rings + +/datum/action/cooldown/spell/telegraphed_strike/dragons_breath/get_sweep_bands() + return list(get_pattern_offsets()) + +/datum/action/cooldown/spell/telegraphed_strike/dragons_breath/get_pattern_offsets() + var/list/flat = list() + for(var/list/ring in cone_rings()) + flat += ring + return flat + +/datum/action/cooldown/spell/telegraphed_strike/dragons_breath/on_hit_target(mob/living/carbon/human/H, mob/living/L, facing) + apply_scorch_stack(L, 1) + var/push_dir = get_dir(H, L) + if(!push_dir) + push_dir = facing + L.safe_throw_at(get_ranged_target_turf(L, push_dir, push_dist), push_dist, 2, H, force = MOVE_FORCE_STRONG) + +/datum/action/cooldown/spell/telegraphed_strike/dragons_breath/on_impact(mob/living/carbon/human/H, facing, atom/movable/visual) + var/turf/origin = get_turf(H) + if(!origin) + return + for(var/list/off in get_pattern_offsets()) + var/list/r = rotate_offset(off[1], off[2], facing) + var/turf/T = locate(origin.x + r[1], origin.y + r[2], origin.z) + if(!T || T.density) + continue + new /obj/effect/temp_visual/dragonfire(T) + for(var/atom/movable/A in T) + if(ismob(A)) + continue + A.fire_act() + +/obj/effect/temp_visual/dragonfire + icon = 'icons/effects/fire.dmi' + icon_state = "3" + layer = GASFIRE_LAYER + light_outer_range = LIGHT_RANGE_FIRE + light_color = LIGHT_COLOR_FIRE + blend_mode = BLEND_ADD + duration = 8 diff --git a/code/modules/spells/spell_types/telegraphed_strike/kinetic_burst.dm b/code/modules/spells/spell_types/telegraphed_strike/kinetic_burst.dm new file mode 100644 index 00000000000..9154c692841 --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/kinetic_burst.dm @@ -0,0 +1,76 @@ +/datum/action/cooldown/spell/telegraphed_strike/kinetic_burst + button_icon = 'icons/mob/actions/spells/mage_kinesis.dmi' + name = "Greater Kinetic Burst" + desc = "Wind up raw electric force in a 5x5 area around yourself, then detonate it - hurling everyone caught back, hurting them and leaving them Vulnerable for a moment. \ + The field follows you as you wind it up. Guard will deflect the strike entirely and you are vulnerable while charging this up." + button_icon_state = "gravity_anchor" + sound = 'sound/magic/repulse.ogg' + + required_form = FORM_LIGHTNING + required_technique = TECHNIQUE_ALTERATION + + invocation_type = INVOCATION_SHOUT + invocation = "Impello!" + + spell_cost = 40 + + cooldown_time = 30 SECONDS + + spell_impact_intensity = SPELL_IMPACT_MEDIUM + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN + + damage = 60 + strike_damage_type = BRUTE + blade_class = BCLASS_BLUNT + npc_simple_damage_mult = 1.5 + committed_strike = TRUE + interruptible = FALSE + lock_direction = FALSE + charging_slowdown = CHARGING_SLOWDOWN_SMALL + windup_time = 2 SECONDS + sweep_step = 0 + vuln_on_hit = 2 SECONDS + telegraph_type = /obj/effect/temp_visual/trap/kinesis + strike_sound = 'sound/magic/repulse.ogg' + detonate_sound = 'sound/magic/gravity.ogg' + + var/push_dist = 3 + +/datum/action/cooldown/spell/telegraphed_strike/kinetic_burst/get_pattern_offsets() + var/list/offsets = list() + for(var/x in -2 to 2) + for(var/y in -2 to 2) + if(!x && !y) + continue + offsets += list(list(x, y)) + return offsets + +/datum/action/cooldown/spell/telegraphed_strike/kinetic_burst/get_sweep_bands() + return list(get_pattern_offsets()) + +/datum/action/cooldown/spell/telegraphed_strike/kinetic_burst/on_impact(mob/living/carbon/human/H, facing, atom/movable/visual) + var/turf/origin = get_turf(H) + if(!origin) + return + for(var/list/off in get_pattern_offsets()) + var/turf/T = locate(origin.x + off[1], origin.y + off[2], origin.z) + if(!T || T.density) + continue + new /obj/effect/temp_visual/kinetic_blast(T) + +/datum/action/cooldown/spell/telegraphed_strike/kinetic_burst/on_hit_target(mob/living/carbon/human/H, mob/living/L, facing) + var/push_dir = get_dir(H, L) + if(!push_dir) + return + L.safe_throw_at(get_ranged_target_turf(L, push_dir, push_dist), push_dist, 2, H, force = MOVE_FORCE_STRONG) + +/obj/effect/temp_visual/trap/kinesis + color = "#FFD700" + light_color = "#FFD700" + duration = 3 SECONDS + +/obj/effect/temp_visual/trap/kinesis/Initialize(mapload) + . = ..() + var/target_alpha = alpha + alpha = 0 + animate(src, alpha = target_alpha, time = 4) diff --git a/code/modules/spells/spell_types/telegraphed_strike/spellblade/_base.dm b/code/modules/spells/spell_types/telegraphed_strike/spellblade/_base.dm new file mode 100644 index 00000000000..d265fdb8025 --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/spellblade/_base.dm @@ -0,0 +1,115 @@ +/datum/action/cooldown/spell/telegraphed_strike/spellblade + abstract_type = /datum/action/cooldown/spell/telegraphed_strike/spellblade + button_icon = 'icons/mob/actions/spells/spellblade.dmi' + sound = 'sound/combat/ground_smash1.ogg' + + spell_cost = 10 + cooldown_time = 12 SECONDS + + spell_impact_intensity = SPELL_IMPACT_MEDIUM + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + requires_weapon = TRUE + weapon_missing_message = "I need my bound weapon in hand!" + telegraph_type = /obj/effect/temp_visual/trap/arcyne + blade_class = BCLASS_BLUNT + strike_sound = null + committed_strike = FALSE + + required_form = FORM_ARCANE + required_technique = TECHNIQUE_IMBUE + + var/momentum_cost = 3 + var/empowered_mult = 2 + var/blunt_penalty = 5 + var/momentum_on_hit = 0 + var/momentum_on_surge = 1 + var/push_dist = 0 + var/empowered = FALSE + +/datum/action/cooldown/spell/telegraphed_strike/spellblade/get_strike_weapon(mob/living/carbon/human/H) + return arcyne_get_weapon(H) + +/datum/action/cooldown/spell/telegraphed_strike/spellblade/get_strike_damage() + var/dmg = damage + if(blade_class == BCLASS_BLUNT) + dmg = max(0, dmg - blunt_penalty) + return empowered ? dmg * empowered_mult : dmg + +/datum/action/cooldown/spell/telegraphed_strike/spellblade/cast(atom/cast_on) + empowered = FALSE + . = ..() + if(!.) + return + var/mob/living/carbon/human/H = owner + var/datum/intent/active = H.a_intent + if(active && (active.blade_class == BCLASS_BLUNT || active.blade_class == BCLASS_SMASH)) + blade_class = BCLASS_BLUNT + else + blade_class = BCLASS_CUT + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(M && M.stacks >= momentum_cost) + M.consume_stacks(momentum_cost) + empowered = TRUE + to_chat(H, span_notice("[momentum_cost] momentum released - empowered strike!")) + +/datum/action/cooldown/spell/telegraphed_strike/spellblade/on_antimagic_block(mob/living/L) + L.visible_message(span_warning("The arcyne force scatters as it nears [L]!")) + playsound(get_turf(L), 'sound/magic/magic_nulled.ogg', 100) + +/datum/action/cooldown/spell/telegraphed_strike/spellblade/on_hit_target(mob/living/carbon/human/H, mob/living/L, facing) + if(!push_dist) + return + var/push_dir = get_dir(H, L) || facing || pick(GLOB.cardinals) + L.safe_throw_at(get_ranged_target_turf(L, push_dir, push_dist), push_dist, 1, H, force = MOVE_FORCE_STRONG) + +/datum/action/cooldown/spell/telegraphed_strike/spellblade/on_strike_complete(mob/living/carbon/human/H, hit_count, deflected) + if(!hit_count) + return + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(!M) + return + if(momentum_on_hit) + M.add_stacks(momentum_on_hit) + if(hit_count >= 2 && momentum_on_surge) + M.add_stacks(momentum_on_surge) + to_chat(H, span_notice("DOUBLE STRIKE! ARCYNE SURGE!")) + +/datum/action/cooldown/spell/telegraphed_strike/spellblade/on_impact(mob/living/carbon/human/H, facing, atom/movable/visual) + var/turf/center = get_turf(H) + if(!center) + return + var/list/tiles = list() + for(var/list/off in get_pattern_offsets()) + var/list/r = rotate_offset(off[1], off[2], facing) + var/turf/T = locate(center.x + r[1], center.y + r[2], center.z) + if(T && !path_blocked(center, T)) + tiles += T + play_impact(center, facing, tiles) + +/datum/action/cooldown/spell/telegraphed_strike/spellblade/proc/play_impact(turf/center, facing, list/tiles) + if(blade_class == BCLASS_BLUNT) + playsound(center, pick('sound/combat/hits/blunt/metalblunt (1).ogg', 'sound/combat/hits/blunt/metalblunt (2).ogg', 'sound/combat/hits/blunt/metalblunt (3).ogg'), 90, TRUE, 3) + playsound(center, pick('sound/combat/ground_smash1.ogg', 'sound/combat/ground_smash2.ogg', 'sound/combat/ground_smash3.ogg'), 60, TRUE) + for(var/mob/shaken in range(4, center)) + shake_camera(shaken, 1, 1) + for(var/turf/T in tiles) + new /obj/effect/temp_visual/kinetic_blast(T) + else + playsound(center, pick('sound/combat/hits/bladed/largeslash (1).ogg', 'sound/combat/hits/bladed/largeslash (2).ogg', 'sound/combat/hits/bladed/largeslash (3).ogg'), 90, TRUE, 3) + for(var/turf/T in tiles) + var/obj/effect/temp_visual/blade_cut/V = new(T) + V.dir = get_dir(center, T) || facing + +/obj/effect/temp_visual/trap/arcyne + color = "#9932CC" + light_color = "#9932CC" + duration = 3 SECONDS + +/obj/effect/temp_visual/blade_cut + icon = 'icons/effects/effects.dmi' + icon_state = "cut" + dir = NORTH + name = "arcyne slash" + randomdir = FALSE + duration = 1 SECONDS diff --git a/code/modules/spells/spell_types/telegraphed_strike/spellblade/advance.dm b/code/modules/spells/spell_types/telegraphed_strike/spellblade/advance.dm new file mode 100644 index 00000000000..c875d96376c --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/spellblade/advance.dm @@ -0,0 +1,151 @@ +/datum/action/cooldown/spell/advance + name = "Advance!" + desc = "Leap forward up to 4 tiles, passing through enemies, then stab ahead on landing. \ + At 3+ momentum: consumes 3 to double damage. \ + Strikes your aimed bodypart." + button_icon = 'icons/mob/actions/spells/spellblade.dmi' + button_icon_state = "advance" + sound = 'sound/combat/wooshes/bladed/wooshsmall (1).ogg' + + cast_range = 15 + + spell_color = 20 + + required_form = FORM_ARCANE + required_technique = TECHNIQUE_IMBUE + + invocation = null + invocation_type = INVOCATION_NONE + + charge_required = TRUE + charge_time = CHARGETIME_POKE + charge_slowdown = CHARGING_SLOWDOWN_NONE + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 15 SECONDS + + spell_impact_intensity = SPELL_IMPACT_LOW + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/leap_range = 4 + var/base_damage = 30 + var/empowered_mult = 2 + var/momentum_cost = 3 + var/step_delay = 1 + +/datum/action/cooldown/spell/advance/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/obj/item/held_weapon = arcyne_get_weapon(H) + if(!held_weapon) + to_chat(H, span_warning("I need my bound weapon in hand!")) + return FALSE + + H.say("Procede!", forced = "spell", language = /datum/language/common) + + var/turf/target_turf = get_turf(cast_on) + var/turf/start = get_turf(H) + var/facing = get_dir(start, target_turf) || H.dir + var/def_zone = H.zone_selected || BODY_ZONE_CHEST + + var/turf/first_step = get_step(start, facing) + if(!first_step || first_step.density) + to_chat(H, span_warning("There's no room to leap!")) + return FALSE + + var/empowered = FALSE + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(M && M.stacks >= momentum_cost) + M.consume_stacks(momentum_cost) + empowered = TRUE + to_chat(H, span_notice("[momentum_cost] momentum released - empowered advance!")) + + var/damage = empowered ? (base_damage * empowered_mult) : base_damage + + if(H.buckled) + H.buckled.unbuckle_mob(H, TRUE) + + H.visible_message( + span_warning("[H] lowers [H.p_their()] [held_weapon.name] and leaps forward!"), + span_notice("I advance!")) + playsound(start, pick('sound/combat/wooshes/bladed/wooshsmall (1).ogg', 'sound/combat/wooshes/bladed/wooshsmall (2).ogg'), 60, TRUE) + + // Leap phase - pass through mobs with jump arc animation + var/old_pass = H.pass_flags + var/old_throwing = H.throwing + H.pass_flags |= PASSMOB + H.throwing = TRUE + var/prev_pixel_z = H.pixel_z + var/prev_transform = H.transform + + animate(H, pixel_z = prev_pixel_z + 18, time = 1, easing = EASE_OUT) + + var/steps_taken = 0 + for(var/i in 1 to leap_range) + if(H.stat != CONSCIOUS || H.IsParalyzed() || H.IsStun() || QDELETED(H)) + break + var/turf/next = get_step(get_turf(H), facing) + if(!next || next.density) + break + + var/blocked = FALSE + for(var/obj/structure/S in next.contents) + if(S.density && !S.climbable) + blocked = TRUE + break + if(blocked) + break + if(!step(H, facing)) + break + steps_taken++ + + if(i < leap_range) + sleep(step_delay) + + // Slam down - fast drop with impact tilt + var/land_angle = pick(-20, -15, 15, 20) + animate(H, pixel_z = prev_pixel_z, transform = turn(prev_transform, land_angle), time = 1, easing = EASE_IN) + animate(transform = prev_transform, time = 2) + + H.pass_flags = old_pass + H.throwing = old_throwing + + var/turf/landing_turf = get_turf(H) + if(landing_turf?.zFall(H)) + return TRUE + + if(steps_taken == 0) + to_chat(H, span_warning("My leap is blocked!")) + return TRUE + + // Strike phase - stab 1 tile ahead of landing + var/turf/jab_turf = get_step(get_turf(H), facing) + if(!jab_turf) + H.visible_message(span_notice("[H] lands with a thrust at the air.")) + return TRUE + + var/hit_count = 0 + for(var/mob/living/victim in jab_turf) + if(victim == H || victim.stat == DEAD) + continue + arcyne_strike(H, victim, held_weapon, damage, def_zone, BCLASS_STAB, spell_name = "Advance!") + hit_count++ + + if(!hit_count) + // Also check landing tile for targets standing on top of caster + var/turf/landing = get_turf(H) + for(var/mob/living/victim in landing) + if(victim == H || victim.stat == DEAD) + continue + arcyne_strike(H, victim, held_weapon, damage, def_zone, BCLASS_STAB, spell_name = "Advance!") + hit_count++ + + if(!hit_count) + H.visible_message(span_notice("[H] lands with a thrust at the air.")) + else + H.visible_message(span_danger("[H] lands and drives [H.p_their()] [held_weapon.name] forward!")) + + log_combat(H, null, "used Advance! ([hit_count] hits)") + return TRUE diff --git a/code/modules/spells/spell_types/telegraphed_strike/spellblade/air_strike.dm b/code/modules/spells/spell_types/telegraphed_strike/spellblade/air_strike.dm new file mode 100644 index 00000000000..a704b88a58a --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/spellblade/air_strike.dm @@ -0,0 +1,229 @@ +/datum/action/cooldown/spell/air_strike + name = "Air Strike" + desc = "Your blade passes into the immaterial and the leyline carries it forth, striking up to 4 tiles away. \ + Brief telegraph before the strike lands - aim where they will be. \ + At 3+ momentum: consumes 3 to double damage. \ + Strikes your aimed bodypart. Adaptable to intent: \ + - Cut: 3x1 perpendicular line, multiple targets. (30/60 damage) \ + - Stab: 3x1 forward line, pierces through enemies. (20/40 damage, LIGHT PENETRATION) \ + - Blunt: All force focused on a single target. (45/90 damage)" + button_icon = 'icons/mob/actions/spells/spellblade.dmi' + button_icon_state = "air_strike" + sound = 'sound/combat/wooshes/bladed/wooshsmall (1).ogg' + cast_range = 4 + + spell_cost = 10 + + required_form = FORM_AIR + required_technique = TECHNIQUE_IMBUE + + invocation = "Ictus Venti!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = 1 + charge_slowdown = CHARGING_SLOWDOWN_NONE + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 12 SECONDS + + spell_impact_intensity = SPELL_IMPACT_LOW + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/cut_damage = 30 + var/stab_damage = 20 + var/stab_ap = 15 + var/blunt_damage = 45 + var/empowered_mult = 2 + var/momentum_cost = 3 + +/datum/action/cooldown/spell/air_strike/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/obj/item/held_weapon = arcyne_get_weapon(H) + if(!held_weapon) + to_chat(H, span_warning("I need my bound weapon in hand!")) + return FALSE + + var/turf/target_turf = get_turf(cast_on) + if(!target_turf) + return FALSE + + var/empowered = FALSE + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(M && M.stacks >= momentum_cost) + M.consume_stacks(momentum_cost) + empowered = TRUE + to_chat(H, span_notice("[momentum_cost] momentum released - empowered strike!")) + + var/facing = get_dir(get_turf(H), target_turf) || H.dir + var/datum/intent/current_intent = H.a_intent + var/blade_class = current_intent?.blade_class || BCLASS_CUT + + switch(blade_class) + if(BCLASS_STAB, BCLASS_PICK) + do_stab_strike(H, held_weapon, empowered, target_turf, facing) + if(BCLASS_BLUNT, BCLASS_SMASH) + do_blunt_strike(H, held_weapon, empowered, target_turf) + else + do_cut_strike(H, held_weapon, empowered, target_turf, facing) + + return TRUE + +/datum/action/cooldown/spell/air_strike/proc/do_cut_strike(mob/living/carbon/human/H, obj/item/weapon, empowered, turf/origin, facing) + var/def_zone = H.zone_selected || BODY_ZONE_CHEST + var/damage = empowered ? (cut_damage * empowered_mult) : cut_damage + + var/list/affected_turfs = get_perpendicular_line(origin, facing) + + for(var/turf/T in affected_turfs) + new /obj/effect/temp_visual/air_strike_telegraph(T) + + addtimer(CALLBACK(src, PROC_REF(resolve_cut_strike), H, weapon, empowered, affected_turfs, damage, def_zone, facing), 2) + +/datum/action/cooldown/spell/air_strike/proc/resolve_cut_strike(mob/living/carbon/human/H, obj/item/weapon, empowered, list/affected_turfs, damage, def_zone, facing) + if(QDELETED(H) || H.stat == DEAD) + return + + for(var/turf/T in affected_turfs) + var/obj/effect/temp_visual/blade_cut/V = new(T) + V.dir = facing + playsound(affected_turfs[1], pick('sound/combat/wooshes/bladed/wooshsmall (1).ogg', 'sound/combat/wooshes/bladed/wooshsmall (2).ogg'), 80, TRUE) + + var/hit_count = 0 + for(var/turf/T in affected_turfs) + for(var/mob/living/victim in T) + if(victim == H || victim.stat == DEAD) + continue + if(empowered) + arcyne_strike(H, victim, weapon, round(damage / 2), def_zone, BCLASS_CUT, spell_name = "Air Strike (Cut)", skip_message = TRUE) + arcyne_strike(H, victim, weapon, round(damage / 2), def_zone, BCLASS_CUT, spell_name = "Air Strike (Cut)") + else + arcyne_strike(H, victim, weapon, damage, def_zone, BCLASS_CUT, spell_name = "Air Strike (Cut)") + hit_count++ + + if(hit_count) + H.visible_message(span_danger("[H] sweeps [weapon.name] in a wide arcyne arc!")) + else + H.visible_message(span_notice("[H] sweeps [weapon.name] through the air!")) + if(hit_count >= 2) + var/datum/status_effect/buff/arcyne_momentum/surge = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(surge) + surge.add_stacks(1) + to_chat(H, span_notice("DOUBLE STRIKE! ARCYNE SURGE!")) + +/datum/action/cooldown/spell/air_strike/proc/do_stab_strike(mob/living/carbon/human/H, obj/item/weapon, empowered, turf/origin, facing) + var/def_zone = H.zone_selected || BODY_ZONE_CHEST + var/damage = empowered ? (stab_damage * empowered_mult) : stab_damage + + var/list/affected_turfs = get_forward_line(origin, facing, 3) + + for(var/turf/T in affected_turfs) + new /obj/effect/temp_visual/air_strike_telegraph(T) + + addtimer(CALLBACK(src, PROC_REF(resolve_stab_strike), H, weapon, empowered, affected_turfs, damage, def_zone, facing), 2) + +/datum/action/cooldown/spell/air_strike/proc/resolve_stab_strike(mob/living/carbon/human/H, obj/item/weapon, empowered, list/affected_turfs, damage, def_zone, facing) + if(QDELETED(H) || H.stat == DEAD) + return + + for(var/turf/T in affected_turfs) + var/obj/effect/temp_visual/blade_burst/V = new(T) + V.dir = facing + playsound(affected_turfs[1], pick('sound/combat/wooshes/bladed/wooshsmall (1).ogg', 'sound/combat/wooshes/bladed/wooshsmall (2).ogg'), 80, TRUE) + + var/hit_count = 0 + for(var/turf/T in affected_turfs) + for(var/mob/living/victim in T) + if(victim == H || victim.stat == DEAD) + continue + if(empowered) + arcyne_strike(H, victim, weapon, round(damage / 2), def_zone, BCLASS_STAB, armor_penetration = stab_ap, spell_name = "Air Strike (Stab)", skip_message = TRUE) + arcyne_strike(H, victim, weapon, round(damage / 2), def_zone, BCLASS_STAB, armor_penetration = stab_ap, spell_name = "Air Strike (Stab)") + else + arcyne_strike(H, victim, weapon, damage, def_zone, BCLASS_STAB, armor_penetration = stab_ap, spell_name = "Air Strike (Stab)") + hit_count++ + + if(hit_count) + H.visible_message(span_danger("[H] thrusts [weapon.name] forward, sending an arcyne lance through the air!")) + else + H.visible_message(span_notice("[H] thrusts [weapon.name] forward, sending an arcyne lance through empty air!")) + if(hit_count >= 2) + var/datum/status_effect/buff/arcyne_momentum/surge = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(surge) + surge.add_stacks(1) + to_chat(H, span_notice("DOUBLE STRIKE! ARCYNE SURGE!")) + +/datum/action/cooldown/spell/air_strike/proc/do_blunt_strike(mob/living/carbon/human/H, obj/item/weapon, empowered, turf/origin) + var/def_zone = H.zone_selected || BODY_ZONE_CHEST + var/damage = empowered ? (blunt_damage * empowered_mult) : blunt_damage + + new /obj/effect/temp_visual/air_strike_telegraph(origin) + + addtimer(CALLBACK(src, PROC_REF(resolve_blunt_strike), H, weapon, empowered, origin, damage, def_zone), 2) + +/datum/action/cooldown/spell/air_strike/proc/resolve_blunt_strike(mob/living/carbon/human/H, obj/item/weapon, empowered, turf/origin, damage, def_zone) + if(QDELETED(H) || H.stat == DEAD) + return + + new /obj/effect/temp_visual/kinetic_blast(origin) + playsound(origin, pick('sound/combat/wooshes/bladed/wooshsmall (1).ogg', 'sound/combat/wooshes/bladed/wooshsmall (2).ogg'), 80, TRUE) + + var/mob/living/victim = null + for(var/mob/living/L in origin) + if(L == H || L.stat == DEAD) + continue + victim = L + break + + if(!victim) + H.visible_message(span_notice("[H] slams [weapon.name] down, sending a shockwave into the ground!")) + return + + if(empowered) + arcyne_strike(H, victim, weapon, round(damage / 2), def_zone, BCLASS_BLUNT, spell_name = "Air Strike (Blunt)", skip_message = TRUE) + arcyne_strike(H, victim, weapon, round(damage / 2), def_zone, BCLASS_BLUNT, spell_name = "Air Strike (Blunt)") + else + arcyne_strike(H, victim, weapon, damage, def_zone, BCLASS_BLUNT, spell_name = "Air Strike (Blunt)") + H.visible_message(span_danger("[H] slams [weapon.name] down, focusing all arcyne force into [victim]!")) + +/datum/action/cooldown/spell/air_strike/proc/get_perpendicular_line(turf/center, facing) + var/list/turfs = list() + turfs += center + var/perp_dir1 + var/perp_dir2 + if(facing == NORTH || facing == SOUTH) + perp_dir1 = WEST + perp_dir2 = EAST + else + perp_dir1 = NORTH + perp_dir2 = SOUTH + var/turf/left = get_step(center, perp_dir1) + var/turf/right = get_step(center, perp_dir2) + if(left) turfs += left + if(right) turfs += right + return turfs + +/datum/action/cooldown/spell/air_strike/proc/get_forward_line(turf/center, facing, length = 3) + var/list/turfs = list() + turfs += center + var/turf/current = center + for(var/i in 1 to (length - 1)) + current = get_step(current, facing) + if(!current) + break + turfs += current + return turfs + +/obj/effect/temp_visual/air_strike_telegraph + icon = 'icons/effects/effects.dmi' + icon_state = "trap" + light_outer_range = 1 + duration = 3 + +/obj/effect/temp_visual/arcyne_strike_fx + icon = 'icons/effects/effects.dmi' + icon_state = "strike" + duration = 5 diff --git a/code/modules/spells/spell_types/telegraphed_strike/spellblade/arcyne_phalanx.dm b/code/modules/spells/spell_types/telegraphed_strike/spellblade/arcyne_phalanx.dm new file mode 100644 index 00000000000..ff9b136aa45 --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/spellblade/arcyne_phalanx.dm @@ -0,0 +1,188 @@ +#define PHALANX_FILTER "phalanx_glow" + +/datum/action/cooldown/spell/arcane_phalanx + name = "Arcyne Phalanx" + desc = "Prime your next melee strike with arcyne force. On hit, the blow pierces through, \ + striking enemies in a line behind the target. Builds 1 momentum on hit. \ + At 3+ momentum: consumes 3 for a deeper, more damaging pierce." + button_icon = 'icons/mob/actions/spells/spellblade.dmi' + button_icon_state = "azurean_phalanx" + sound = 'sound/magic/antimagic.ogg' + + click_to_activate = FALSE + self_cast_possible = TRUE + + spell_cost = 15 + + required_form = FORM_ARCANE + required_technique = TECHNIQUE_IMBUE + + invocation = "Phalanx Arcana!" + invocation_type = INVOCATION_SHOUT + + charge_required = FALSE + cooldown_time = 12 SECONDS + + spell_impact_intensity = SPELL_IMPACT_MEDIUM + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/pierce_length = 3 + var/pierce_damage = 35 + var/empowered_pierce_length = 4 + var/empowered_pierce_damage = 50 + var/momentum_cost = 3 + +/datum/action/cooldown/spell/arcane_phalanx/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + if(!arcyne_get_weapon(H)) + to_chat(H, span_warning("I need my bound weapon in hand!")) + return FALSE + + if(H.has_status_effect(/datum/status_effect/buff/phalanx_ready)) + to_chat(H, span_warning("My thrust is already primed!")) + return FALSE + + var/empowered = FALSE + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(M && M.stacks >= momentum_cost) + M.consume_stacks(momentum_cost) + empowered = TRUE + to_chat(H, span_notice("[momentum_cost] momentum released - empowered pierce primed!")) + + var/datum/status_effect/buff/phalanx_ready/effect = H.apply_status_effect(/datum/status_effect/buff/phalanx_ready) + if(effect) + effect.spell_ref = WEAKREF(src) + effect.empowered = empowered + + H.visible_message( + span_notice("[H] primes [H.p_their()] weapon with a surge of arcyne force."), + span_notice("My thrust is primed - the next strike will pierce through.")) + playsound(get_turf(H), 'sound/magic/antimagic.ogg', 60, TRUE) + return TRUE + +/datum/action/cooldown/spell/arcane_phalanx/proc/do_pierce(mob/living/carbon/human/user, mob/living/struck, empowered) + if(QDELETED(user) || user.stat == DEAD) + return + + var/obj/item/weapon = arcyne_get_weapon(user) + if(!weapon) + return + + var/turf/origin = get_turf(struck) + if(!origin) + return + + var/pierce_dir = get_dir(user, struck) + if(!pierce_dir) + pierce_dir = user.dir + + var/def_zone = user.zone_selected || BODY_ZONE_CHEST + var/damage = empowered ? empowered_pierce_damage : pierce_damage + var/length = empowered ? empowered_pierce_length : pierce_length + + var/list/pierce_turfs = list() + var/turf/current = origin + for(var/i in 1 to length) + current = get_step(current, pierce_dir) + if(!current || current.density) + break + var/struct_blocked = FALSE + for(var/obj/structure/S in current.contents) + if(S.density && !S.climbable) + struct_blocked = TRUE + break + if(struct_blocked) + break + pierce_turfs += current + + // Visuals on origin tile and all pierce tiles + var/obj/effect/temp_visual/origin_burst = new /obj/effect/temp_visual/blade_stab(origin) + origin_burst.dir = pierce_dir + for(var/turf/T in pierce_turfs) + var/obj/effect/temp_visual/V = new /obj/effect/temp_visual/blade_stab(T) + V.dir = pierce_dir + + playsound(origin, pick('sound/combat/wooshes/bladed/wooshsmall (1).ogg', 'sound/combat/wooshes/bladed/wooshsmall (2).ogg'), 70, TRUE) + + var/hit_count = 0 + var/list/already_hit = list() + + // Hit the original struck target with the arcyne strike (they already took the normal weapon hit) + if(!QDELETED(struck) && struck.stat != DEAD) + arcyne_strike(user, struck, weapon, damage, def_zone, BCLASS_STAB, spell_name = "Azurean Phalanx", skip_animation = TRUE) + hit_count++ + already_hit += struck + + for(var/turf/T in pierce_turfs) + for(var/mob/living/victim in T) + if(victim == user || victim == struck || victim.stat == DEAD || (victim in already_hit)) + continue + arcyne_strike(user, victim, weapon, damage, def_zone, BCLASS_STAB, spell_name = "Azurean Phalanx", skip_animation = TRUE) + hit_count++ + already_hit += victim + + var/datum/status_effect/buff/arcyne_momentum/M = user.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(M) + M.add_stacks(1) + + if(hit_count) + user.visible_message(span_danger("[user] drives [user.p_their()] [weapon.name] through [struck], piercing those behind!")) + if(hit_count >= 2 && M) + M.add_stacks(1) + to_chat(user, span_notice("DOUBLE PIERCE! ARCYNE SURGE!")) + else + user.visible_message(span_notice("[user]'s strike pierces through [struck].")) + +// --- Status effect --- + +/atom/movable/screen/alert/status_effect/buff/phalanx_ready + name = "Phalanx Primed" + desc = "My next melee strike will pierce through the target." + icon_state = "buff" + +/datum/status_effect/buff/phalanx_ready + id = "phalanx_ready" + alert_type = /atom/movable/screen/alert/status_effect/buff/phalanx_ready + duration = 10 SECONDS + status_type = STATUS_EFFECT_UNIQUE + var/datum/weakref/spell_ref + var/empowered = FALSE + +/datum/status_effect/buff/phalanx_ready/on_apply() + . = ..() + RegisterSignal(owner, COMSIG_MOB_ITEM_ATTACK, PROC_REF(on_attack)) + owner.add_filter(PHALANX_FILTER, 2, list("type" = "outline", "color" = "#4488ff", "alpha" = 160, "size" = 2)) + +/datum/status_effect/buff/phalanx_ready/on_remove() + UnregisterSignal(owner, COMSIG_MOB_ITEM_ATTACK) + owner.remove_filter(PHALANX_FILTER) + . = ..() + +/datum/status_effect/buff/phalanx_ready/proc/on_attack(mob/living/source, mob/living/target, mob/living/user, obj/item/weapon) + SIGNAL_HANDLER + if(!isliving(target) || target == owner || target.stat == DEAD) + return + var/obj/item/bound = arcyne_get_weapon(owner) + if(!bound || bound != weapon) + return + var/datum/action/cooldown/spell/arcane_phalanx/spell = spell_ref?.resolve() + if(!spell) + owner.remove_status_effect(/datum/status_effect/buff/phalanx_ready) + return + var/was_empowered = empowered + owner.remove_status_effect(/datum/status_effect/buff/phalanx_ready) + INVOKE_ASYNC(spell, TYPE_PROC_REF(/datum/action/cooldown/spell/arcane_phalanx, do_pierce), source, target, was_empowered) + +/obj/effect/temp_visual/blade_stab + icon = 'icons/effects/effects.dmi' + icon_state = "stab" + dir = NORTH + name = "arcyne stab" + randomdir = FALSE + duration = 1 SECONDS + +#undef PHALANX_FILTER diff --git a/code/modules/spells/spell_types/telegraphed_strike/spellblade/bind.dm b/code/modules/spells/spell_types/telegraphed_strike/spellblade/bind.dm new file mode 100644 index 00000000000..69988adf54e --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/spellblade/bind.dm @@ -0,0 +1,91 @@ +/datum/action/cooldown/spell/bind_weapon + name = "Bind Weapon" + desc = "Bind your held weapon as an arcyne conduit. Successful strikes with bound weapons build arcyne momentum, fueling your abilities. \ + It can also be recalled to your hand from anywhere with Recall Weapon. \ + The weapon must match your chant - Blade requires a sword or dagger, Phalangite a polearm, Macebearer a mace or warhammer. \ + You can rebind to restore a lost Arcyne Momentum status, or bind a new weapon if your old one was destroyed. \ + Cast with empty hands to unbind your current weapon." + button_icon = 'icons/mob/actions/spells/spellblade.dmi' + button_icon_state = "bind_weapon" + sound = 'sound/magic/charged.ogg' + + click_to_activate = FALSE + self_cast_possible = TRUE + + required_form = FORM_ARCANE + required_technique = TECHNIQUE_IMBUE + spell_cost = 40 + + invocation = "Vinculum Arcanum." + invocation_type = INVOCATION_SHOUT + + charge_required = FALSE + cooldown_time = 5 SECONDS + + spell_impact_intensity = SPELL_IMPACT_NONE + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + +/datum/action/cooldown/spell/bind_weapon/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/obj/item/weapon = H.get_active_held_item() + if(!weapon) + // Empty hands - unbind current weapon if one exists + var/datum/status_effect/buff/arcyne_momentum/unbind_M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(unbind_M?.bound_weapon && !QDELETED(unbind_M.bound_weapon)) + var/datum/component/arcyne_conduit/old_conduit = unbind_M.bound_weapon.GetComponent(/datum/component/arcyne_conduit) + if(old_conduit) + qdel(old_conduit) + to_chat(H, span_notice("The arcyne bond on [unbind_M.bound_weapon] fades. I am unbound.")) + playsound(get_turf(H), 'sound/magic/charging.ogg', 30, TRUE) + unbind_M.bound_weapon = null + return TRUE + to_chat(H, span_warning("I have no bound weapon to release!")) + return FALSE + + var/datum/component/arcyne_conduit/existing_conduit = weapon.GetComponent(/datum/component/arcyne_conduit) + if(existing_conduit) + var/mob/living/existing_owner = existing_conduit.owner_ref?.resolve() + if(existing_owner && existing_owner != H) + to_chat(H, span_warning("[weapon] is already bound to another spellblade!")) + return FALSE + if(existing_owner == H) + to_chat(H, span_warning("[weapon] is already bound as my conduit!")) + return FALSE + + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(!M) + M = H.apply_status_effect(/datum/status_effect/buff/arcyne_momentum) + + if(M?.chant) + var/valid = FALSE + var/list/valid_skills + switch(M.chant) + if("blade") + valid_skills = list(/datum/attribute/skill/combat/swords, /datum/attribute/skill/combat/knives) + if("phalangite") + valid_skills = list(/datum/attribute/skill/combat/polearms) + if("macebearer") + valid_skills = list(/datum/attribute/skill/combat/axesmaces) + if(valid_skills) + valid = (weapon.associated_skill in valid_skills) + if(!valid) + to_chat(H, span_warning("This weapon does not match my chant!")) + return FALSE + + if(M?.bound_weapon && !QDELETED(M.bound_weapon)) + var/datum/component/arcyne_conduit/old_conduit = M.bound_weapon.GetComponent(/datum/component/arcyne_conduit) + if(old_conduit) + qdel(old_conduit) + to_chat(H, span_notice("The arcyne bond on [M.bound_weapon] fades.")) + + weapon.AddComponent(/datum/component/arcyne_conduit, owner = H) + if(M) + M.bound_weapon = weapon + to_chat(H, span_notice("I bind [weapon] as my arcyne conduit. Its strikes will build momentum.")) + playsound(get_turf(H), 'sound/magic/charged.ogg', 50, TRUE) + H.visible_message(span_notice("[H] passes a hand over [weapon], which begins to glow faintly.")) + return TRUE diff --git a/code/modules/spells/spell_types/telegraphed_strike/spellblade/blade_storm.dm b/code/modules/spells/spell_types/telegraphed_strike/spellblade/blade_storm.dm new file mode 100644 index 00000000000..eeed40d106f --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/spellblade/blade_storm.dm @@ -0,0 +1,248 @@ +/datum/action/cooldown/spell/projectile/blade_storm + name = "Blade Storm" + desc = "Hurls forth a shadow of yourself. On impact, teleport onto the target \ + and unleash a storm of slashes on them and around yourself.\ + Requires 7 Momentum: 3 strikes at 30 damage each. \ + Overcharged at 10 Momentum: 5 strikes at 30 damage each. \ + If reflected onto yourself, the arcyne energy tears into your own chest. \ + The blade has no eyes - it does not distinguish friend from foe. Let not your foe deflect it into your ally." + button_icon = 'icons/mob/actions/spells/spellblade.dmi' + button_icon_state = "blade_storm" + sound = 'sound/magic/blink.ogg' + + projectile_type = /obj/projectile/magic/blade_storm + cast_range = 7 + + required_form = FORM_AIR + required_technique = TECHNIQUE_IMBUE + spell_cost = 90 + + invocation = "Procella Gladiorum!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = 2 SECONDS + charge_slowdown = CHARGING_SLOWDOWN_NONE + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 60 SECONDS + + spell_impact_intensity = SPELL_IMPACT_HIGH + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN + + var/aoe_damage = 30 + var/aoe_base_cuts = 3 + var/aoe_bonus_cuts = 2 + var/personal_damage = 30 + var/personal_base_cuts = 3 + var/personal_bonus_cuts = 2 + var/min_momentum = 7 + var/empowered_momentum = 10 + var/cut_delay = 2 + var/telegraph_delay = 8 + var/storm_deflected = FALSE + + var/cached_aoe_cuts = 3 + var/cached_p_cuts = 3 + var/cached_locked_zone = BODY_ZONE_CHEST + +/datum/action/cooldown/spell/projectile/blade_storm/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return FALSE + if(!ishuman(owner)) + return FALSE + var/mob/living/carbon/human/H = owner + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(!M || M.stacks < min_momentum) + return FALSE + return TRUE + +/datum/action/cooldown/spell/projectile/blade_storm/cast(atom/cast_on) + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/obj/item/held_weapon = arcyne_get_weapon(H) + if(!held_weapon) + to_chat(H, span_warning("I need my bound weapon in hand!")) + return FALSE + + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(!M || M.stacks < min_momentum) + to_chat(H, span_warning("Not enough momentum! I need at least [min_momentum] stacks!")) + return FALSE + + var/stacks = M.stacks + cached_aoe_cuts = aoe_base_cuts + cached_p_cuts = personal_base_cuts + if(stacks >= empowered_momentum) + cached_aoe_cuts += aoe_bonus_cuts + cached_p_cuts += personal_bonus_cuts + M.consume_all_stacks() + to_chat(H, span_notice("All [stacks] momentum released into shadow!")) + + cached_locked_zone = H.zone_selected || BODY_ZONE_CHEST + + H.visible_message(span_boldwarning("[H] hurls a shadow of [H.p_them()]self forward!")) + log_combat(H, cast_on, "used Blade Storm on") + + . = ..() + +/datum/action/cooldown/spell/projectile/blade_storm/ready_projectile(obj/projectile/to_fire, atom/target, mob/user, iteration) + ..() + var/obj/projectile/magic/blade_storm/P = to_fire + if(istype(P)) + P.aoe_cuts = cached_aoe_cuts + P.aoe_dmg = aoe_damage + P.p_cuts = cached_p_cuts + P.p_dmg = personal_damage + P.def_zone = cached_locked_zone + P.parent_spell = src + +/datum/action/cooldown/spell/projectile/blade_storm/proc/execute_storm(mob/living/carbon/human/user, mob/living/victim, aoe_cuts, aoe_dmg, p_cuts, p_dmg, def_zone) + if(QDELETED(user) || user.stat == DEAD) + return + + var/obj/item/held_weapon = arcyne_get_weapon(user) + if(!held_weapon) + return + + var/turf/landing = get_turf(victim) + + // Afterimage at departure + var/turf/start = get_turf(user) + var/obj/effect/after_image/img = new(start, 0, 0, 0, 0, 0.5 SECONDS, 2 SECONDS, 0) + img.name = user.name + img.appearance = user.appearance + img.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + img.alpha = 80 + QDEL_IN(img, 2 SECONDS) + + if(user.buckled) + user.buckled.unbuckle_mob(user, TRUE) + do_teleport(user, landing, channel = TELEPORT_CHANNEL_MAGIC) + playsound(landing, 'sound/magic/blink.ogg', 40, TRUE) + + user.visible_message( + span_warning("[user] materializes from the shadow onto [victim]!"), + span_notice("I emerge from the shadow!")) + + var/turf/center = get_turf(victim) + var/list/ring_turfs = get_hollow_ring(center) + + for(var/turf/T in ring_turfs) + new /obj/effect/temp_visual/blade_storm_telegraph(T) + playsound(center, 'sound/magic/blade_burst.ogg', 80, TRUE) + + user.visible_message(span_warning("[user] raises [held_weapon.name] - arcyne energy surges toward the [span_warning(parse_zone(def_zone))]!")) + + storm_deflected = FALSE + for(var/cut_num in 1 to aoe_cuts) + var/delay = telegraph_delay + (cut_num - 1) * cut_delay + addtimer(CALLBACK(src, PROC_REF(do_storm_cut), user, held_weapon, center, ring_turfs, cut_num, def_zone, aoe_dmg), delay) + + for(var/strike_num in 1 to p_cuts) + var/delay = telegraph_delay + (strike_num - 1) * cut_delay + addtimer(CALLBACK(src, PROC_REF(do_personal_strike), user, held_weapon, victim, strike_num, def_zone, p_dmg), delay) + +/datum/action/cooldown/spell/projectile/blade_storm/proc/do_storm_cut(mob/living/carbon/human/user, obj/item/weapon, turf/center, list/ring_turfs, cut_num, def_zone, aoe_dmg) + if(QDELETED(user) || user.stat == DEAD) + return + + for(var/swing_dir in list(NORTH, SOUTH, EAST, WEST)) + var/obj/effect/melee_swing/S = new(center) + S.dir = swing_dir + flick(pick("left_swing", "right_swing"), S) + QDEL_IN(S, 1 SECONDS) + + playsound(center, pick("genslash"), 80, TRUE) + + for(var/turf/T in ring_turfs) + for(var/mob/living/victim in T) + if(victim == user || victim.stat == DEAD) + continue + arcyne_strike(user, victim, weapon, aoe_dmg, def_zone, BCLASS_CUT, spell_name = "Blade Storm (Cut [cut_num])", skip_animation = TRUE, skip_message = TRUE) + +/datum/action/cooldown/spell/projectile/blade_storm/proc/do_personal_strike(mob/living/carbon/human/user, obj/item/weapon, mob/living/victim, strike_num, def_zone, p_dmg) + if(QDELETED(user) || user.stat == DEAD || QDELETED(victim) || victim.stat == DEAD) + return + + arcyne_strike(user, victim, weapon, p_dmg, def_zone, spell_name = "Blade Storm (Strike [strike_num])", skip_animation = TRUE) + + var/turf/victim_turf = get_turf(victim) + if(victim_turf) + var/obj/effect/temp_visual/blade_cut/V = new(victim_turf) + V.dir = get_dir(user, victim) + +/datum/action/cooldown/spell/projectile/blade_storm/proc/get_hollow_ring(turf/center) + var/list/ring = list() + for(var/dx in -1 to 1) + for(var/dy in -1 to 1) + if(dx == 0 && dy == 0) + continue + var/turf/T = locate(center.x + dx, center.y + dy, center.z) + if(T) + ring += T + return ring + +/datum/action/cooldown/spell/projectile/blade_storm/proc/handle_self_hit(mob/living/carbon/human/caster, p_cuts, p_dmg) + to_chat(caster, span_userdanger("My own shadow crashes back into me!")) + caster.visible_message(span_warning("[caster]'s shadow rebounds violently!")) + var/backlash = p_dmg * p_cuts * 0.5 + caster.apply_damage(backlash, BRUTE, BODY_ZONE_CHEST) + playsound(get_turf(caster), 'sound/combat/hits/bladed/genstab (1).ogg', 80, TRUE) + +/obj/projectile/magic/blade_storm + name = "shadow afterimage" + icon_state = "cursehand0" + damage = 0 + nodamage = TRUE + speed = 1.5 + hitsound = 'sound/magic/blink.ogg' + var/aoe_cuts = 3 + var/aoe_dmg = 30 + var/p_cuts = 3 + var/p_dmg = 30 + var/datum/action/cooldown/spell/projectile/blade_storm/parent_spell + +/obj/projectile/magic/blade_storm/on_hit(atom/target) + if(!isliving(target) || !firer || !ishuman(firer)) + return ..() + var/mob/living/victim = target + var/mob/living/carbon/human/caster = firer + + if(!parent_spell && caster.mind) + parent_spell = locate(/datum/action/cooldown/spell/projectile/blade_storm) in caster.actions + if(!parent_spell) + return ..() + + if(victim == caster) + parent_spell.handle_self_hit(caster, p_cuts, p_dmg) + return BULLET_ACT_BLOCK + + if(victim.anti_magic_check()) + visible_message(span_warning("[victim] shatters the shadow!")) + playsound(get_turf(victim), 'sound/magic/magic_nulled.ogg', 100) + return BULLET_ACT_BLOCK + + if(out_of_effective_range()) + return + + INVOKE_ASYNC(parent_spell, TYPE_PROC_REF(/datum/action/cooldown/spell/projectile/blade_storm, execute_storm), caster, victim, aoe_cuts, aoe_dmg, p_cuts, p_dmg, def_zone) + return BULLET_ACT_BLOCK + +/obj/effect/temp_visual/blade_storm_telegraph + icon = 'icons/effects/effects.dmi' + icon_state = "trap" + light_outer_range = 1 + duration = 8 + +/obj/effect/melee_swing + name = "arcyne slash" + icon = 'icons/effects/effects.dmi' + icon_state = "strike" + pixel_x = -32 + pixel_y = -32 + anchored = TRUE + layer = ABOVE_ALL_MOB_LAYER + mouse_opacity = MOUSE_OPACITY_TRANSPARENT diff --git a/code/modules/spells/spell_types/telegraphed_strike/spellblade/caedo.dm b/code/modules/spells/spell_types/telegraphed_strike/spellblade/caedo.dm new file mode 100644 index 00000000000..8d352501eb6 --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/spellblade/caedo.dm @@ -0,0 +1,210 @@ +/datum/action/cooldown/spell/caedo + name = "Caedo" + desc = "In the old tongue, caedo - to strike or to cut down. Dash forward at blinding speed, \ + leaving afterimages that strike every enemy in your path. \ + Empowered (3 Momentum): Consumes 3 stacks to strike twice. \ + If any of them defend against the strike, you will be left exposed and slowed!" + button_icon = 'icons/mob/actions/spells/spellblade.dmi' + button_icon_state = "caedo" + sound = 'sound/magic/blink.ogg' + + cast_range = 7 + + required_form = FORM_LIGHTNING + required_technique = TECHNIQUE_IMBUE + + spell_cost = 10 + + invocation = "Caedo!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_POKE + charge_slowdown = CHARGING_SLOWDOWN_NONE + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 12 SECONDS + + spell_impact_intensity = SPELL_IMPACT_LOW + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/max_range = 5 + var/strike_damage = 40 + var/empower_cost = 3 + +/datum/action/cooldown/spell/caedo/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/obj/item/held_weapon = arcyne_get_weapon(H) + if(!held_weapon) + to_chat(H, span_warning("I need my bound weapon in hand!")) + return FALSE + + var/turf/start = get_turf(H) + var/turf/dest + + if(isliving(cast_on)) + dest = find_landing_turf(H, cast_on) + else + dest = get_turf(cast_on) + + if(!dest || dest.z != start.z) + to_chat(H, span_warning("Invalid target!")) + return FALSE + + // Soft clamp: if too far or path blocked, dash as far as possible toward target + dest = arcyne_find_max_blink_dest(H, dest, max_range) + if(!dest) + to_chat(H, span_warning("I can't dash there!")) + return FALSE + + var/distance = get_dist(start, dest) + if(distance < 1) + to_chat(H, span_warning("I need somewhere to dash to!")) + return FALSE + + var/list/full_path = getline(start, dest) + + var/list/mobs_in_path = list() + for(var/turf/path_turf in full_path) + if(path_turf == start) + continue + for(var/mob/living/M in path_turf) + if(M != H && M.stat != DEAD) + mobs_in_path += M + + INVOKE_ASYNC(src, PROC_REF(create_afterimage_trail), H, full_path) + + playsound(start, 'sound/magic/blink.ogg', 40, TRUE) + H.visible_message( + span_warning("[H] vanishes in a blur of motion!"), + span_notice("I dash!")) + + if(H.buckled) + H.buckled.unbuckle_mob(H, TRUE) + do_teleport(H, dest, channel = TELEPORT_CHANNEL_MAGIC) + playsound(dest, 'sound/magic/blink.ogg', 25, TRUE) + + log_combat(H, cast_on, "used Caedo on") + + var/empowered = FALSE + var/datum/status_effect/buff/arcyne_momentum/momentum = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(momentum && momentum.stacks >= empower_cost) + momentum.consume_stacks(empower_cost) + empowered = TRUE + to_chat(H, span_notice("Momentum surges - twin strikes!")) + + var/locked_zone = H.zone_selected || BODY_ZONE_CHEST + + if(length(mobs_in_path)) + addtimer(CALLBACK(src, PROC_REF(execute_path_strikes), H, mobs_in_path, held_weapon, locked_zone, empowered), 5) + + return TRUE + +/datum/action/cooldown/spell/caedo/proc/find_landing_turf(mob/living/user, mob/living/target_mob) + var/approach_dir = get_dir(user, target_mob) + var/turf/far_side = get_step(target_mob, approach_dir) + if(far_side && !far_side.density && !istransparentturf(far_side) && isfloorturf(far_side)) + return far_side + return get_turf(target_mob) + +/datum/action/cooldown/spell/caedo/proc/execute_path_strikes(mob/living/carbon/human/user, list/victims, obj/item/weapon, def_zone, empowered = FALSE) + if(!user || QDELETED(user)) + return + var/hit_count = 0 + for(var/mob/living/victim in victims) + if(QDELETED(victim) || victim.stat == DEAD) + continue + var/total_damage = strike_damage + arcyne_strike(user, victim, weapon, total_damage, def_zone, spell_name = "Caedo", skip_animation = TRUE) + hit_count++ + var/turf/victim_turf = get_turf(victim) + if(victim_turf) + var/slash_dir = get_dir(user, victim) + var/obj/effect/temp_visual/blade_cut/V = new(victim_turf) + V.dir = slash_dir + if(empowered) + addtimer(CALLBACK(src, PROC_REF(second_strike), user, victim, weapon, def_zone), 3) + if(hit_count >= 2) + var/datum/status_effect/buff/arcyne_momentum/surge = user.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(surge) + surge.add_stacks(1) + to_chat(user, span_notice("DOUBLE STRIKE! ARCYNE SURGE!")) + +/datum/action/cooldown/spell/caedo/proc/second_strike(mob/living/carbon/human/user, mob/living/victim, obj/item/weapon, def_zone) + if(!user || QDELETED(user) || !victim || QDELETED(victim) || victim.stat == DEAD) + return + var/total_damage = strike_damage + arcyne_strike(user, victim, weapon, total_damage, def_zone, spell_name = "Caedo", skip_animation = TRUE) + var/turf/victim_turf = get_turf(victim) + if(victim_turf) + var/slash_dir = get_dir(user, victim) + var/obj/effect/temp_visual/blade_cut/V = new(victim_turf) + V.dir = slash_dir + +/datum/action/cooldown/spell/caedo/proc/create_afterimage_trail(mob/living/carbon/human/user, list/path_turfs) + set waitfor = FALSE + var/list/images = list() + var/path_len = length(path_turfs) + if(path_len < 2) + return + var/travel_dir = get_dir(path_turfs[1], path_turfs[path_len]) + + var/front_px = 0 + var/front_py = 0 + var/back_px = 0 + var/back_py = 0 + switch(travel_dir) + if(NORTH) + front_py = 10 + back_py = -10 + if(SOUTH) + front_py = -10 + back_py = 10 + if(EAST) + front_px = 10 + back_px = -10 + if(WEST) + front_px = -10 + back_px = 10 + if(NORTHEAST) + front_px = 8 + front_py = 8 + back_px = -8 + back_py = -8 + if(NORTHWEST) + front_px = -8 + front_py = 8 + back_px = 8 + back_py = -8 + if(SOUTHEAST) + front_px = 8 + front_py = -8 + back_px = -8 + back_py = 8 + if(SOUTHWEST) + front_px = -8 + front_py = -8 + back_px = 8 + back_py = 8 + + for(var/i in 1 to path_len) + var/turf/T = path_turfs[i] + var/base_alpha = round(40 + 80 * (i - 1) / max(path_len - 1, 1)) + for(var/side in 1 to 2) + var/obj/effect/after_image/img = new(T, 0, 0, 0, 0, 0.5 SECONDS, 3 SECONDS, 0) + images += img + img.name = user.name + img.appearance = user.appearance + img.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + img.dir = travel_dir + img.alpha = base_alpha + if(side == 1) + img.pixel_x = front_px + img.pixel_y = front_py + else + img.pixel_x = back_px + img.pixel_y = back_py + QDEL_LIST_IN(images, 2 SECONDS) diff --git a/code/modules/spells/spell_types/telegraphed_strike/spellblade/cataclysm.dm b/code/modules/spells/spell_types/telegraphed_strike/spellblade/cataclysm.dm new file mode 100644 index 00000000000..4928cab1a4d --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/spellblade/cataclysm.dm @@ -0,0 +1,152 @@ +/datum/action/cooldown/spell/cataclysm + name = "Cataclysm" + desc = "Let the mountain fall. Conjure a hammer of pure earthen force and hurl it at a target area. \ + On impact it explodes, crushing everyone in a 5x5 area for 75 blunt damage and leaving them Vulnerable. \ + Requires 7 momentum. Overcharged at 10 momentum: 135 damage. \ + Same level only." + button_icon = 'icons/mob/actions/spells/spellblade.dmi' + button_icon_state = "cataclysm" + sound = 'sound/combat/ground_smash1.ogg' + cast_range = 7 + + spell_cost = 60 + required_form = FORM_EARTH + required_technique = TECHNIQUE_IMBUE + + invocation = null + invocation_type = INVOCATION_NONE + + charge_required = TRUE + charge_time = CHARGETIME_MAJOR + charge_slowdown = 1 + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 60 SECONDS + + spell_impact_intensity = SPELL_IMPACT_HIGH + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/delay = 10 + var/damage = 75 + var/bonus_damage = 60 + var/area_of_effect = 2 + var/min_momentum = 7 + var/empowered_momentum = 10 + var/vulnerable_duration = 6 SECONDS + +/datum/action/cooldown/spell/cataclysm/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return FALSE + if(!ishuman(owner)) + return FALSE + var/mob/living/carbon/human/H = owner + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(!M || M.stacks < min_momentum) + return FALSE + return TRUE + +/datum/action/cooldown/spell/cataclysm/cast(atom/cast_on) + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/obj/item/held_weapon = arcyne_get_weapon(H) + if(!held_weapon) + to_chat(H, span_warning("I need my bound weapon in hand!")) + return FALSE + + var/turf/T = get_turf(cast_on) + var/turf/start = get_turf(H) + + if(!T) + to_chat(H, span_warning("Invalid target!")) + return FALSE + + if(T.density) + to_chat(H, span_warning("I cannot hurl a hammer into solid ground!")) + return FALSE + + if(T.z != start.z) + to_chat(H, span_warning("Too far - I need to be on the same level!")) + return FALSE + + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(!M || M.stacks < min_momentum) + to_chat(H, span_warning("Not enough momentum! I need at least [min_momentum] stacks!")) + return FALSE + + var/stacks = M.stacks + var/empowered = stacks >= empowered_momentum + var/final_damage = damage + (empowered ? bonus_damage : 0) + var/def_zone = H.zone_selected || BODY_ZONE_CHEST + M.consume_all_stacks() + to_chat(H, span_notice("All [stacks] momentum released - cataclysm [empowered ? "fully empowered" : "unleashed"]!")) + + H.say("RUINA CAELI!", forced = "spell", language = /datum/language/common) + + playsound(start, pick('sound/combat/ground_smash1.ogg', 'sound/combat/ground_smash2.ogg', 'sound/combat/ground_smash3.ogg'), 100, TRUE) + H.visible_message(span_boldwarning("[H] conjures a massive hammer out of arcyne force!")) + + for(var/turf/affected_turf in get_hear(area_of_effect, T)) + new /obj/effect/temp_visual/blade_storm_telegraph(affected_turf) + + log_combat(H, cast_on, "used Cataclysm on") + + addtimer(CALLBACK(src, PROC_REF(do_hammer_throw), H, held_weapon, T, final_damage, def_zone, empowered), delay) + . = ..() + +/datum/action/cooldown/spell/cataclysm/proc/do_hammer_throw(mob/living/carbon/human/user, obj/item/weapon, turf/dest, final_damage, def_zone, empowered) + if(QDELETED(user) || user.stat == DEAD) + return + + user.visible_message(span_warning("[user] hurls the hammer, aimed at the [span_warning(parse_zone(def_zone))]!")) + playsound(get_turf(user), 'sound/combat/shieldraise.ogg', 100) + var/obj/effect/temp_visual/cataclysm_boulder/boulder = new(get_turf(user)) + var/dx = (dest.x - user.x) * 32 + var/dy = (dest.y - user.y) * 32 + animate(boulder, pixel_x = dx, pixel_y = dy, time = 4) + var/matrix/q1 = matrix() + q1.Turn(90) + animate(boulder, transform = q1, time = 1, flags = ANIMATION_PARALLEL) + var/matrix/q2 = matrix() + q2.Turn(180) + animate(transform = q2, time = 1) + var/matrix/q3 = matrix() + q3.Turn(270) + animate(transform = q3, time = 1) + animate(transform = matrix(), time = 1) + + addtimer(CALLBACK(src, PROC_REF(do_hammer_impact), user, weapon, dest, final_damage, def_zone, empowered, boulder), 4) + +/datum/action/cooldown/spell/cataclysm/proc/do_hammer_impact(mob/living/carbon/human/user, obj/item/weapon, turf/dest, final_damage, def_zone, empowered, obj/effect/temp_visual/cataclysm_boulder/boulder) + if(!QDELETED(boulder)) + qdel(boulder) + + playsound(dest, pick('sound/combat/ground_smash1.ogg', 'sound/combat/ground_smash2.ogg', 'sound/combat/ground_smash3.ogg'), 100, TRUE) + + for(var/turf/affected_turf in get_hear(area_of_effect, dest)) + new /obj/effect/temp_visual/kinetic_blast(affected_turf) + for(var/mob/living/L in affected_turf.contents) + if(L == user || L.stat == DEAD) + continue + if(L.anti_magic_check()) + L.visible_message(span_warning("The hammer shatters around [L]!")) + playsound(get_turf(L), 'sound/magic/magic_nulled.ogg', 100) + continue + if(empowered) + arcyne_strike(user, L, weapon, round(final_damage / 2), def_zone, BCLASS_BLUNT, spell_name = "Cataclysm", skip_message = TRUE) + arcyne_strike(user, L, weapon, round(final_damage / 2), def_zone, BCLASS_BLUNT, spell_name = "Cataclysm") + else + arcyne_strike(user, L, weapon, final_damage, def_zone, BCLASS_BLUNT, spell_name = "Cataclysm") + L.apply_status_effect(/datum/status_effect/debuff/vulnerable, vulnerable_duration) + playsound(affected_turf, pick('sound/combat/ground_smash1.ogg', 'sound/combat/ground_smash2.ogg', 'sound/combat/ground_smash3.ogg'), 60, TRUE) + +/obj/effect/temp_visual/cataclysm_boulder + icon = 'icons/roguetown/weapons/32/clubs.dmi' + icon_state = "iwarhammer" + name = "arcyne hammer" + desc = "A hammer of pure arcyne force." + layer = FLY_LAYER + plane = GAME_PLANE_UPPER + randomdir = FALSE + duration = 8 diff --git a/code/modules/spells/spell_types/telegraphed_strike/spellblade/empower_weapon.dm b/code/modules/spells/spell_types/telegraphed_strike/spellblade/empower_weapon.dm new file mode 100644 index 00000000000..348dddaff32 --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/spellblade/empower_weapon.dm @@ -0,0 +1,62 @@ +/datum/action/cooldown/spell/empower_weapon + name = "Empower Weapon" + desc = "Channel all accumulated momentum into your next strike, empowering it to bypass parry and dodge. Works with both weapons and unarmed attacks. \ + Requires 5+ momentum. Burns ALL momentum." + button_icon = 'icons/mob/actions/spells/spellblade.dmi' + button_icon_state = "empower_weapon" + sound = 'sound/magic/antimagic.ogg' + + required_form = FORM_ARCANE + required_technique = TECHNIQUE_IMBUE + + spell_cost = 0 + + invocation = null + invocation_type = INVOCATION_NONE + + charge_required = FALSE + cooldown_time = 30 SECONDS + + spell_impact_intensity = SPELL_IMPACT_NONE + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/min_momentum = 5 + +/datum/action/cooldown/spell/empower_weapon/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return FALSE + if(!ishuman(owner)) + return FALSE + var/mob/living/carbon/human/H = owner + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(!M || M.stacks < min_momentum) + return FALSE + return TRUE + +/datum/action/cooldown/spell/empower_weapon/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(!M || M.stacks < min_momentum) + to_chat(H, span_warning("I need at least [min_momentum] momentum to empower my weapon!")) + return FALSE + + if(H.has_status_effect(/datum/status_effect/buff/empowered_strike)) + to_chat(H, span_warning("My weapon is already empowered!")) + return FALSE + + var/stacks_burned = M.stacks + M.consume_stacks(stacks_burned) + + H.apply_status_effect(/datum/status_effect/buff/empowered_strike) + playsound(get_turf(H), 'sound/magic/antimagic.ogg', 60, TRUE) + H.visible_message( + span_danger("[H]'s weapon flares with a violent red glow!"), + span_notice("I channel [stacks_burned] momentum into my weapon. The next strike will not be denied.")) + + return TRUE + diff --git a/code/modules/spells/spell_types/telegraphed_strike/spellblade/gate_of_reckoning.dm b/code/modules/spells/spell_types/telegraphed_strike/spellblade/gate_of_reckoning.dm new file mode 100644 index 00000000000..a2667482ccc --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/spellblade/gate_of_reckoning.dm @@ -0,0 +1,311 @@ +#define GATE_PHALANX_FILTER "gate_phalanx_glow" + +/datum/action/cooldown/spell/gate_of_reckoning + name = "Gate of Reckoning" + desc = "Porta Iudicii - the Gate of Judgement. Summon two phantom spears that flank you in formation. \ + For the next 12 seconds, each melee hit you land sends the phantom spears thrusting forward \ + in a 3-wide line in the direction you face, striking all enemies caught in their path. \ + Requires 7 momentum. 3 charges at 40 damage each. \ + Overcharged at 10 momentum: 6 charges at 40 damage each. \ + Phantom strikes can be blocked by shields but cannot be parried." + button_icon = 'icons/mob/actions/spells/spellblade.dmi' + button_icon_state = "gate_of_reckoning" + sound = 'sound/magic/antimagic.ogg' + click_to_activate = FALSE + self_cast_possible = TRUE + + required_form = FORM_LIFE + required_technique = TECHNIQUE_IMBUE + + spell_cost = 100 + + invocation = "Porta Iudicii!" + invocation_type = INVOCATION_SHOUT + + charge_required = FALSE + cooldown_time = 60 SECONDS + + + spell_impact_intensity = SPELL_IMPACT_HIGH + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN + + var/phantom_damage = 40 + var/base_charges = 3 + var/empowered_charges = 6 + var/min_momentum = 7 + var/max_momentum = 10 + +/datum/action/cooldown/spell/gate_of_reckoning/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return FALSE + if(!ishuman(owner)) + return FALSE + var/mob/living/carbon/human/H = owner + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(!M || M.stacks < min_momentum) + return FALSE + return TRUE + +/datum/action/cooldown/spell/gate_of_reckoning/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/obj/item/held_weapon = arcyne_get_weapon(H) + if(!held_weapon) + to_chat(H, span_warning("I need my bound weapon in hand!")) + return FALSE + + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(!M || M.stacks < min_momentum) + to_chat(H, span_warning("Not enough momentum! I need at least [min_momentum] stacks!")) + return FALSE + + var/stacks = M.stacks + var/empowered = stacks >= max_momentum + var/charges = empowered ? empowered_charges : base_charges + M.consume_all_stacks() + to_chat(H, span_notice("All [stacks] momentum released - [charges] phantom spears summoned!")) + + var/datum/status_effect/buff/gate_phalanx/effect = H.apply_status_effect(/datum/status_effect/buff/gate_phalanx) + if(effect) + effect.spell_ref = WEAKREF(src) + effect.charges = charges + effect.max_charges = charges + effect.phantom_damage = phantom_damage + effect.bound_weapon = held_weapon + effect.update_alert() + effect.create_spear_overlays() + + H.visible_message( + span_boldwarning("[H] tears open a leyline rift - three phantom spears materialize around [H.p_them()]!"), + span_notice("Three phantom spears answer my call!")) + playsound(get_turf(H), 'sound/misc/portalactivate.ogg', 70, TRUE) + playsound(get_turf(H), 'sound/magic/antimagic.ogg', 60, TRUE) + + log_combat(H, H, "activated Gate of Reckoning ([charges] charges)") + return TRUE + +/datum/action/cooldown/spell/gate_of_reckoning/proc/do_phantom_thrust(mob/living/carbon/human/user, obj/item/weapon, charges_remaining) + if(QDELETED(user) || user.stat == DEAD) + return + + var/turf/origin = get_turf(user) + if(!origin) + return + + var/facing = user.dir + var/list/thrust_turfs = get_phalanx_line(origin, facing) + + // Visual: spear thrust on each tile + for(var/turf/T in thrust_turfs) + var/obj/effect/temp_visual/gate_phalanx_thrust/V = new(T) + V.dir = facing + + playsound(origin, pick('sound/combat/hits/bladed/genthrust (1).ogg', 'sound/combat/hits/bladed/genthrust (2).ogg'), 80, TRUE) + + // Strike all enemies in the 3x1 line + for(var/turf/T in thrust_turfs) + for(var/mob/living/victim in T) + if(victim == user || victim.stat == DEAD) + continue + arcyne_strike(user, victim, weapon, phantom_damage, BODY_ZONE_CHEST, BCLASS_STAB, \ + spell_name = "Gate of Reckoning", \ + skip_animation = TRUE, \ + allow_shield_check = TRUE) + +/datum/action/cooldown/spell/gate_of_reckoning/proc/get_phalanx_line(turf/origin, facing) + // 3-wide perpendicular line one tile ahead of the user + var/turf/center = get_step(origin, facing) + if(!center) + return list() + var/list/turfs = list(center) + var/perp_dir1 + var/perp_dir2 + switch(facing) + if(NORTH, SOUTH) + perp_dir1 = WEST + perp_dir2 = EAST + if(EAST, WEST) + perp_dir1 = NORTH + perp_dir2 = SOUTH + else + // Diagonal facing: just use center tile + return turfs + var/turf/left = get_step(center, perp_dir1) + var/turf/right = get_step(center, perp_dir2) + if(left) + turfs += left + if(right) + turfs += right + return turfs + +// --- Status effect: tracks charges, manages overlays, listens for hits --- + +/atom/movable/screen/alert/status_effect/buff/gate_phalanx + name = "Gate of Reckoning (0)" + desc = "Phantom spears thrust alongside my melee attacks." + icon_state = "buff" + +/datum/status_effect/buff/gate_phalanx + id = "gate_phalanx" + alert_type = /atom/movable/screen/alert/status_effect/buff/gate_phalanx + duration = 12 SECONDS + status_type = STATUS_EFFECT_UNIQUE + var/datum/weakref/spell_ref + var/charges = 3 + var/max_charges = 3 + var/phantom_damage = 40 + var/obj/item/bound_weapon + var/list/spear_visuals = list() + +/datum/status_effect/buff/gate_phalanx/on_apply() + . = ..() + RegisterSignal(owner, COMSIG_MOB_ITEM_ATTACK_POST_SWINGDELAY, PROC_REF(on_melee_hit)) + RegisterSignal(owner, COMSIG_ATOM_DIR_CHANGE, PROC_REF(on_dir_change)) + owner.add_filter(GATE_PHALANX_FILTER, 2, list("type" = "outline", "color" = "#4488ff", "alpha" = 200, "size" = 2)) + +/datum/status_effect/buff/gate_phalanx/on_remove() + UnregisterSignal(owner, list(COMSIG_MOB_ITEM_ATTACK_POST_SWINGDELAY, COMSIG_ATOM_DIR_CHANGE)) + owner.remove_filter(GATE_PHALANX_FILTER) + remove_spear_visuals() + if(charges > 0) + to_chat(owner, span_warning("The phantom spears dissipate - [charges] thrusts unused.")) + owner.visible_message(span_notice("[owner]'s phantom spears fade away.")) + else + to_chat(owner, span_notice("All phantom spears expended!")) + owner.visible_message(span_notice("[owner]'s final phantom spear shatters.")) + . = ..() + +/datum/status_effect/buff/gate_phalanx/proc/update_alert() + if(!linked_alert) + return + linked_alert.name = "Gate of Reckoning ([charges]/[max_charges])" + +/datum/status_effect/buff/gate_phalanx/proc/create_spear_overlays() + remove_spear_visuals() + // Two spears flanking the user, one tile left and one tile right + for(var/i in 1 to 2) + var/obj/effect/gate_phalanx_spear/spear = new(null) + spear_visuals += spear + owner.vis_contents += spear + update_spear_positions(owner.dir) + +/datum/status_effect/buff/gate_phalanx/proc/remove_spear_visuals() + for(var/obj/effect/gate_phalanx_spear/spear in spear_visuals) + owner.vis_contents -= spear + qdel(spear) + spear_visuals.Cut() + +/datum/status_effect/buff/gate_phalanx/proc/on_dir_change(atom/source, old_dir, new_dir) + SIGNAL_HANDLER + update_spear_positions(new_dir) + +/datum/status_effect/buff/gate_phalanx/proc/update_spear_positions(facing) + var/rotation + switch(facing) + if(NORTH) + rotation = -45 + if(SOUTH) + rotation = 135 + if(EAST) + rotation = 45 + if(WEST) + rotation = 225 + else + rotation = -45 + + // One tile left, one tile right (perpendicular to facing) + var/list/perp_offsets + switch(facing) + if(NORTH, SOUTH) + perp_offsets = list(list(-32, 0), list(32, 0)) + if(EAST, WEST) + perp_offsets = list(list(0, 32), list(0, -32)) + else + perp_offsets = list(list(-32, 0), list(32, 0)) + + for(var/i in 1 to min(length(spear_visuals), length(perp_offsets))) + var/obj/effect/gate_phalanx_spear/spear = spear_visuals[i] + var/list/offset = perp_offsets[i] + var/matrix/M = matrix() + M.Scale(0.5, 0.5) + M.Turn(rotation) + // -16 centers the 64x64 sprite on a 32x32 tile; pixel_x/pixel_y don't work correctly in vis_contents + M.Translate(offset[1] - 16, offset[2] - 16) + spear.transform = M + spear.pixel_x = 0 + spear.pixel_y = 0 + +/datum/status_effect/buff/gate_phalanx/proc/on_melee_hit(mob/living/source, mob/living/target, mob/living/user, obj/item/weapon) + SIGNAL_HANDLER + if(!isliving(target) || target == owner || target.stat == DEAD) + return + if(!bound_weapon || weapon != bound_weapon) + return + if(charges <= 0) + return + + charges-- + update_alert() + owner.balloon_alert(owner, "PHANTOM THRUST! ([charges] left)") + + var/datum/action/cooldown/spell/gate_of_reckoning/spell = spell_ref?.resolve() + if(!spell) + owner.remove_status_effect(/datum/status_effect/buff/gate_phalanx) + return + + INVOKE_ASYNC(spell, TYPE_PROC_REF(/datum/action/cooldown/spell/gate_of_reckoning, do_phantom_thrust), owner, bound_weapon, charges) + + if(charges <= 0) + owner.remove_status_effect(/datum/status_effect/buff/gate_phalanx) + +/obj/effect/temp_visual/gate_phalanx_thrust + icon = 'icons/roguetown/weapons/64/polearms.dmi' + icon_state = "bronzespear_winged" + duration = 4 + layer = ABOVE_ALL_MOB_LAYER + randomdir = FALSE + +/obj/effect/temp_visual/gate_phalanx_thrust/Initialize(mapload) + . = ..() + var/matrix/M = matrix() + M.Scale(0.7, 0.7) + switch(dir) + if(NORTH) + M.Turn(0) + if(SOUTH) + M.Turn(180) + if(EAST) + M.Turn(90) + if(WEST) + M.Turn(270) + transform = M + // Thrust animation: lunge from behind into position then fade + var/target_x = pixel_x + var/target_y = pixel_y + switch(dir) + if(NORTH) + pixel_y = -16 + if(SOUTH) + pixel_y = 16 + if(EAST) + pixel_x = -16 + if(WEST) + pixel_x = 16 + animate(src, pixel_x = target_x, pixel_y = target_y, alpha = 255, time = 1, easing = LINEAR_EASING) + animate(alpha = 0, time = 3, easing = LINEAR_EASING) + +/obj/effect/gate_phalanx_spear + name = "phantom spear" + icon = 'icons/roguetown/weapons/64/polearms.dmi' + icon_state = "bronzespear_winged" + layer = EFFECTS_LAYER + plane = GAME_PLANE + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + anchored = TRUE + alpha = 180 + +#undef GATE_PHALANX_FILTER diff --git a/code/modules/spells/spell_types/telegraphed_strike/spellblade/leyline_anchor.dm b/code/modules/spells/spell_types/telegraphed_strike/spellblade/leyline_anchor.dm new file mode 100644 index 00000000000..bf4d182c00a --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/spellblade/leyline_anchor.dm @@ -0,0 +1,128 @@ +/datum/action/cooldown/spell/leyline_anchor + name = "Leyline Anchor" + desc = "Anchor an arcyne tether to the leyline beneath your feet. Recast to recall. \ + The tether has 75 health and lasts 20 seconds. \ + 7 tile maximum recall range. Same level only. \ + Cannot recall while grabbed, restrained, or buckled. \ + If the tether is destroyed or expires, the spell goes on full cooldown." + button_icon = 'icons/mob/actions/spells/spellblade.dmi' + button_icon_state = "leyline_anchor" + sound = 'sound/misc/portalactivate.ogg' + + click_to_activate = FALSE + self_cast_possible = TRUE + + spell_cost = 40 + + required_form = FORM_ARCANE + required_technique = TECHNIQUE_IMBUE + + invocation = "Ancora Lineam!" + invocation_type = INVOCATION_WHISPER + + charge_required = FALSE + cooldown_time = 30 SECONDS + + spell_impact_intensity = SPELL_IMPACT_NONE + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + + var/obj/structure/leyline_anchor_tether/active_portal + var/max_range = 7 + var/teleport_delay = 5 + +/datum/action/cooldown/spell/leyline_anchor/before_cast(atom/cast_on) + . = ..() + . |= SPELL_NO_IMMEDIATE_COOLDOWN + +/datum/action/cooldown/spell/leyline_anchor/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + // Recast while tether active - recall + if(active_portal && !QDELETED(active_portal)) + var/turf/portal_turf = get_turf(active_portal) + if(!portal_turf) + to_chat(H, span_warning("The leyline tether has been severed!")) + clear_portal() + return FALSE + if(H.pulledby || HAS_TRAIT(H, TRAIT_RESTRAINED) || H.buckled) + to_chat(H, span_warning("I can't recall while restrained!")) + return FALSE + var/turf/caster_turf = get_turf(H) + if(caster_turf.z != portal_turf.z) + to_chat(H, span_warning("The leyline tether is on a different level!")) + return FALSE + if(get_dist(caster_turf, portal_turf) > max_range) + to_chat(H, span_warning("Too far from the leyline tether!")) + return FALSE + H.visible_message(span_warning("Arcyne energy crackles around [H]!")) + playsound(get_turf(H), 'sound/magic/shadowstep_destination.ogg', 50, TRUE) + addtimer(CALLBACK(src, PROC_REF(do_recall), H), teleport_delay) + return TRUE + + // First cast - anchor tether + var/turf/T = get_turf(H) + active_portal = new /obj/structure/leyline_anchor_tether(T, src) + to_chat(H, span_notice("I anchor the leyline beneath my feet.")) + playsound(T, 'sound/misc/portalactivate.ogg', 50, TRUE) + reset_spell_cooldown() + return FALSE + +/datum/action/cooldown/spell/leyline_anchor/proc/do_recall(mob/living/carbon/human/user) + if(QDELETED(user) || user.stat == DEAD) + return + if(!active_portal || QDELETED(active_portal)) + return + var/turf/dest = get_turf(active_portal) + if(!dest) + return + + var/turf/start = get_turf(user) + var/obj/effect/after_image/img = new(start, 0, 0, 0, 0, 0.5 SECONDS, 2 SECONDS, 0) + img.name = user.name + img.appearance = user.appearance + img.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + img.alpha = 80 + QDEL_IN(img, 2 SECONDS) + + do_teleport(user, dest, channel = TELEPORT_CHANNEL_MAGIC) + playsound(dest, 'sound/magic/blink.ogg', 50, TRUE) + user.visible_message( + span_warning("[user] is pulled back through the leyline!"), + span_notice("The leyline pulls me back.")) + active_portal.parent_spell = null + qdel(active_portal) + active_portal = null + StartCooldown() + +/datum/action/cooldown/spell/leyline_anchor/proc/clear_portal() + active_portal = null + StartCooldown() + build_all_button_icons() + +/obj/structure/leyline_anchor_tether + name = "leyline tether" + desc = "An arcyne tether anchored to the leyline. Destroy it to sever the connection." + icon = 'icons/effects/effects.dmi' + icon_state = "leylinerupture" + max_integrity = 75 + anchored = TRUE + density = FALSE + opacity = 0 + light_outer_range = 1 + break_sound = 'sound/magic/magic_nulled.ogg' + attacked_sound = list('sound/combat/hits/onstone/wallhit.ogg', 'sound/combat/hits/onstone/wallhit2.ogg') + var/datum/action/cooldown/spell/leyline_anchor/parent_spell + +/obj/structure/leyline_anchor_tether/Initialize(mapload, datum/action/cooldown/spell/leyline_anchor/spell) + . = ..() + parent_spell = spell + QDEL_IN(src, 20 SECONDS) + +/obj/structure/leyline_anchor_tether/Destroy() + if(parent_spell) + INVOKE_ASYNC(parent_spell, TYPE_PROC_REF(/datum/action/cooldown/spell/leyline_anchor, clear_portal)) + parent_spell = null + return ..() diff --git a/code/modules/spells/spell_types/telegraphed_strike/spellblade/pilum.dm b/code/modules/spells/spell_types/telegraphed_strike/spellblade/pilum.dm new file mode 100644 index 00000000000..31f892baecf --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/spellblade/pilum.dm @@ -0,0 +1,111 @@ +/datum/action/cooldown/spell/projectile/pilum + name = "Pilum" + desc = "A borrowed art - spellblades of tradition learned to imbue their throw with ice essence, \ + flash-chilling the target on impact. Applies 2 frost stacks on hit. \ + At 3+ momentum: consumes 3 for a heavier throw that applies 3 stacks, guaranteeing a freeze on any frosted target. \ + Toggle arc mode (Shift+G) to arc over allies." + button_icon = 'icons/mob/actions/spells/spellblade.dmi' + button_icon_state = "azurean_javelin" + sound = 'sound/combat/wooshes/bladed/wooshsmall (1).ogg' + + projectile_type = /obj/projectile/energy/pilum + projectile_type_arc = /obj/projectile/energy/pilum/arc + cast_range = 15 + + required_form = FORM_ICE + required_technique = TECHNIQUE_IMBUE + + spell_cost = 25 + + invocation = "Pilum Azureum!" + invocation_type = INVOCATION_SHOUT + + charge_required = TRUE + charge_time = CHARGETIME_POKE + charge_slowdown = CHARGING_SLOWDOWN_NONE + charge_sound = 'sound/magic/charging.ogg' + cooldown_time = 10 SECONDS + + spell_impact_intensity = SPELL_IMPACT_MEDIUM + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN + + var/base_damage = 35 + var/empowered_damage = 50 + var/momentum_cost = 3 + var/cached_damage = 0 + var/cached_empowered = FALSE + +/datum/action/cooldown/spell/projectile/pilum/cast(atom/cast_on) + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + if(!arcyne_get_weapon(H)) + to_chat(H, span_warning("I need my bound weapon in hand!")) + return FALSE + + cached_empowered = FALSE + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + if(M && M.stacks >= momentum_cost) + M.consume_stacks(momentum_cost) + cached_empowered = TRUE + to_chat(H, span_notice("[momentum_cost] momentum released - empowered pilum!")) + + cached_damage = cached_empowered ? empowered_damage : base_damage + + if(cached_empowered) + projectile_type = /obj/projectile/energy/pilum/empowered + projectile_type_arc = /obj/projectile/energy/pilum/empowered/arc + else + projectile_type = /obj/projectile/energy/pilum + projectile_type_arc = /obj/projectile/energy/pilum/arc + + . = ..() + +/datum/action/cooldown/spell/projectile/pilum/ready_projectile(obj/projectile/to_fire, atom/target, mob/user, iteration) + ..() + to_fire.damage = cached_damage + +/obj/projectile/energy/pilum + name = "Pilum" + icon = 'icons/obj/magic_projectiles.dmi' + icon_state = "air_blade_stab" + color = "#88BFFF" + damage = 35 + woundclass = BCLASS_STAB + nodamage = FALSE + speed = 1.5 + hitsound = 'sound/combat/hits/bladed/genthrust (1).ogg' + /// How many frost stacks to apply on hit + var/frost_stacks = 1 + +/obj/projectile/energy/pilum/on_hit(target) + . = ..() + if(isliving(target)) + var/mob/living/L = target + if(L.anti_magic_check()) + visible_message(span_warning("[src] disperses on contact with [L]!")) + playsound(get_turf(L), 'sound/magic/magic_nulled.ogg', 100) + return BULLET_ACT_BLOCK + if(out_of_effective_range()) + return + apply_frost_stack(L, frost_stacks) + to_chat(L, span_danger("An icy pilum strikes true - the cold seeps into my bones!")) + if(firer) + log_combat(firer, L, "pilum-struck") + +/obj/projectile/energy/pilum/empowered + name = "Empowered Pilum" + icon_state = "youreyesonly" + color = "#4CADEE" + frost_stacks = 3 + +/obj/projectile/energy/pilum/arc + name = "Arced Pilum" + damage = 26 + arcshot = TRUE + +/obj/projectile/energy/pilum/empowered/arc + name = "Empowered Arced Pilum" + damage = 26 + arcshot = TRUE diff --git a/code/modules/spells/spell_types/telegraphed_strike/spellblade/recall_weapon.dm b/code/modules/spells/spell_types/telegraphed_strike/spellblade/recall_weapon.dm new file mode 100644 index 00000000000..2aa88cd69ea --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/spellblade/recall_weapon.dm @@ -0,0 +1,66 @@ +/datum/action/cooldown/spell/recall_weapon + name = "Recall Weapon" + desc = "Recall your bound weapon to your hand from anywhere." + button_icon = 'icons/mob/actions/spells/spellblade.dmi' + button_icon_state = "recall_weapon" + sound = 'sound/magic/blink.ogg' + + click_to_activate = FALSE + self_cast_possible = TRUE + + required_form = FORM_ARCANE + required_technique = TECHNIQUE_IMBUE + + spell_cost = 15 + + invocation = "Revoca, ferrum!" + invocation_type = INVOCATION_SHOUT + + charge_required = FALSE + cooldown_time = 20 SECONDS + + spell_impact_intensity = SPELL_IMPACT_NONE + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN | SPELL_REQUIRES_SAME_Z + +/datum/action/cooldown/spell/recall_weapon/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/datum/status_effect/buff/arcyne_momentum/M = H.has_status_effect(/datum/status_effect/buff/arcyne_momentum) + var/obj/item/bound_weapon = M?.bound_weapon + + if(!bound_weapon || QDELETED(bound_weapon)) + to_chat(H, span_warning("I have no bound weapon to recall!")) + return FALSE + + if(bound_weapon in H.held_items) + to_chat(H, span_warning("My bound weapon is already in my hand!")) + return FALSE + + // If the weapon is embedded in someone, rip it out first + if(bound_weapon.is_embedded && istype(bound_weapon.loc, /obj/item/bodypart)) + var/obj/item/bodypart/BP = bound_weapon.loc + BP.remove_embedded_object(bound_weapon) + else if(ismob(bound_weapon.loc)) + var/mob/holder = bound_weapon.loc + holder.dropItemToGround(bound_weapon, TRUE) + + var/turf/weapon_turf = get_turf(bound_weapon) + if(!weapon_turf) + to_chat(H, span_warning("Cannot locate my bound weapon!")) + return FALSE + + playsound(weapon_turf, 'sound/magic/blink.ogg', 30, TRUE) + weapon_turf.visible_message(span_notice("[bound_weapon] vanishes in a flash of arcyne light.")) + + if(!H.put_in_hands(bound_weapon)) + bound_weapon.forceMove(get_turf(H)) + to_chat(H, span_notice("My bound weapon returns to my feet - my hands are full.")) + else + to_chat(H, span_notice("My bound weapon flies back to my hand.")) + + playsound(get_turf(H), 'sound/magic/blink.ogg', 40, TRUE) + H.visible_message(span_notice("[bound_weapon] materializes in [H]'s hand.")) + return TRUE diff --git a/code/modules/spells/spell_types/telegraphed_strike/spellblade/shatter.dm b/code/modules/spells/spell_types/telegraphed_strike/spellblade/shatter.dm new file mode 100644 index 00000000000..2debbfeb4ef --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/spellblade/shatter.dm @@ -0,0 +1,25 @@ +/datum/action/cooldown/spell/telegraphed_strike/spellblade/shatter + name = "Shatter" + desc = "Wind up a blow, then strike a line straight ahead of you, hurling those struck back a tile. Cuts for 40 damage \ + The strike will not carry through walls, but it batters any structure in its path. \ + At 3+ momentum: consumes 3 to double damage. Builds momentum on a multi-target hit." + + required_form = FORM_EARTH + button_icon_state = "shatter" + invocation = "Frange!" + damage = 40 + empowered_mult = 2 + windup_time = 6 + sweep_step = 0 + push_dist = 1 + sound = null + detonate_sound = null + momentum_on_hit = 0 + momentum_on_surge = 1 + var/line_length = 3 + +/datum/action/cooldown/spell/telegraphed_strike/spellblade/shatter/get_pattern_offsets() + var/list/offsets = list() + for(var/i in 1 to line_length) + offsets += list(list(0, i)) + return offsets diff --git a/code/modules/spells/spell_types/telegraphed_strike/spellblade/tremor.dm b/code/modules/spells/spell_types/telegraphed_strike/spellblade/tremor.dm new file mode 100644 index 00000000000..2e38db2a516 --- /dev/null +++ b/code/modules/spells/spell_types/telegraphed_strike/spellblade/tremor.dm @@ -0,0 +1,30 @@ +/datum/action/cooldown/spell/telegraphed_strike/spellblade/tremor + name = "Tremor" + desc = "The earth answers. Wind up a strike, then erupt the ground in a ring around you, hurling back everyone adjacent a tile. The ring follows you as you wind up your weapon, and follow your weapon's damage type. \ + Builds 1 momentum on hit. At 3+ momentum: consumes 3 to double damage." + + required_form = FORM_EARTH + button_icon_state = "tremor" + sound = null + invocation = "Tremor!" + damage = 30 + empowered_mult = 2 + push_dist = 1 + detonate_sound = null + momentum_on_hit = 1 + momentum_on_surge = 1 + +/datum/action/cooldown/spell/telegraphed_strike/spellblade/tremor/get_pattern_offsets() + return list( + list(-1, -1), list(0, -1), list(1, -1), + list(-1, 0), list(1, 0), + list(-1, 1), list(0, 1), list(1, 1), + ) + +/datum/action/cooldown/spell/telegraphed_strike/spellblade/tremor/on_impact(mob/living/carbon/human/H, facing, atom/movable/visual) + . = ..() + var/obj/item/weapon = get_strike_weapon(H) + if(blade_class == BCLASS_BLUNT) + H.visible_message(span_danger("[H] slams [weapon ? "\the [weapon.name]" : "down"] into the ground, sending shockwaves outward!")) + else + H.visible_message(span_danger("[H] sweeps [weapon ? "\the [weapon.name]" : "around"] low, scything everything around [H.p_them()]!")) diff --git a/code/modules/spells/spell_types/undirected/_undirected.dm b/code/modules/spells/spell_types/undirected/_undirected.dm index 32425fdc5bb..b2a5ef2a14c 100644 --- a/code/modules/spells/spell_types/undirected/_undirected.dm +++ b/code/modules/spells/spell_types/undirected/_undirected.dm @@ -6,4 +6,5 @@ * If this spell has a cast time it will instead delay casting. */ /datum/action/cooldown/spell/undirected + abstract_type = /datum/action/cooldown/spell/undirected click_to_activate = FALSE diff --git a/code/modules/spells/spell_types/undirected/arcane_bind.dm b/code/modules/spells/spell_types/undirected/arcane_bind.dm new file mode 100644 index 00000000000..b449774c1c3 --- /dev/null +++ b/code/modules/spells/spell_types/undirected/arcane_bind.dm @@ -0,0 +1,72 @@ +/datum/action/cooldown/spell/bind_armament + name = "Arcane Binding" + desc = "Bind a weapon, changing its skills to Arcane instead of its original skill. Once it is unbound, it returns to the original skill. Cast with an empty hand to release the bond. If another takes up the weapon, the bond snaps." + button_icon = 'icons/mob/actions/spells/spellblade.dmi' + button_icon_state = "bind_weapon" + sound = 'sound/magic/charged.ogg' + + click_to_activate = FALSE + self_cast_possible = TRUE + + spell_cost = 10 + + invocation = "Vinculum Ferri." + invocation_type = INVOCATION_WHISPER + + required_form = FORM_ARCANE + + charge_required = TRUE + charge_time = CHARGETIME_MINOR + charge_slowdown = CHARGING_SLOWDOWN_NONE + cooldown_time = 3 SECONDS + + spell_impact_intensity = SPELL_IMPACT_NONE + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC | SPELL_REQUIRES_HUMAN + + var/bind_skill = /datum/attribute/skill/magic/arcane + var/obj/item/bound + +/datum/action/cooldown/spell/bind_armament/Destroy() + release_bind() + return ..() + +/datum/action/cooldown/spell/bind_armament/proc/release_bind() + if(bound && !QDELETED(bound)) + var/datum/component/skill_bind/existing = bound.GetComponent(/datum/component/skill_bind) + if(existing) + qdel(existing) + bound = null + +/datum/action/cooldown/spell/bind_armament/proc/get_bound_weapon() + if(bound && !QDELETED(bound) && bound.GetComponent(/datum/component/skill_bind)) + return bound + bound = null + return null + +/datum/action/cooldown/spell/bind_armament/cast(atom/cast_on) + . = ..() + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + var/obj/item/weapon = H.get_active_held_item() + if(!weapon) + var/obj/item/current_bound = get_bound_weapon() + if(current_bound) + to_chat(H, span_notice("The arcyne bond on [current_bound] fades.")) + release_bind() + return TRUE + to_chat(H, span_warning("I have no bound weapon to release!")) + return FALSE + if(!istype(weapon, /obj/item/weapon) || !ispath(weapon.associated_skill, /datum/attribute/skill/combat)) + to_chat(H, span_warning("[weapon] is not something my arts can guide.")) + return FALSE + if(weapon.GetComponent(/datum/component/skill_bind)) + to_chat(H, span_warning("[weapon] already carries an arcyne bond.")) + return FALSE + release_bind() + weapon.AddComponent(/datum/component/skill_bind, bind_skill, H) + bound = weapon + to_chat(H, span_notice("I lay an arcyne bond on [weapon]; it answers to my conjurer's training now.")) + playsound(get_turf(H), 'sound/magic/charged.ogg', 50, TRUE) + H.visible_message(span_notice("[H] passes a hand over [weapon], which glows faintly.")) + return TRUE diff --git a/code/modules/spells/spell_types/undirected/arcyne_eye.dm b/code/modules/spells/spell_types/undirected/arcyne_eye.dm index d820b912fa0..5f793ce580d 100644 --- a/code/modules/spells/spell_types/undirected/arcyne_eye.dm +++ b/code/modules/spells/spell_types/undirected/arcyne_eye.dm @@ -4,14 +4,11 @@ button_icon_state = "transfix" sound = 'sound/vo/smokedrag.ogg' - point_cost = 0 - charge_required = FALSE cooldown_time = 15 SECONDS spell_cost = 0 - attunements = list( - /datum/attunement/arcyne = 0.1 - ) + + required_form = FORM_ARCANE /datum/action/cooldown/spell/undirected/arcyne_eye/cast(atom/cast_on) . = ..() @@ -21,7 +18,7 @@ if(L.has_status_effect(status)) L.remove_status_effect(status) else - var/duration_increase = attuned_strength * 1.5 MINUTES + var/duration_increase = spell_magnitude_modifier * 1.5 MINUTES L.apply_status_effect(status, initial(status.duration) + duration_increase) /datum/status_effect/buff/arcyne_eye diff --git a/code/modules/spells/spell_types/undirected/beast_sense.dm b/code/modules/spells/spell_types/undirected/beast_sense.dm index 24dbac704d4..edbb6871e94 100644 --- a/code/modules/spells/spell_types/undirected/beast_sense.dm +++ b/code/modules/spells/spell_types/undirected/beast_sense.dm @@ -8,9 +8,6 @@ antimagic_flags = MAGIC_RESISTANCE_HOLY associated_skill = /datum/attribute/skill/magic/holy required_items = list(/obj/item/clothing/neck/psycross/silver/divine/dendor) - attunements = list( - /datum/attunement/earth = 0.5, - ) invocation = "Beast-Lord, lend me the eyes of the zad, the nose of the volf." invocation_type = INVOCATION_WHISPER diff --git a/code/modules/spells/spell_types/undirected/blade_ward.dm b/code/modules/spells/spell_types/undirected/blade_ward.dm index b93fe408f6f..e4b3019baf4 100644 --- a/code/modules/spells/spell_types/undirected/blade_ward.dm +++ b/code/modules/spells/spell_types/undirected/blade_ward.dm @@ -3,8 +3,6 @@ desc = "Improves constitution for a brief duration." button_icon_state = "conjure_armor" - point_cost = 1 - charge_required = FALSE cooldown_time = 1 MINUTES spell_cost = 30 @@ -12,14 +10,14 @@ invocation = "Blades, be dulled!" invocation_type = INVOCATION_SHOUT spell_flags = SPELL_RITUOS - attunements = list( - /datum/attunement/arcyne = 0.3, - ) + + required_form = FORM_ARCANE + /datum/action/cooldown/spell/undirected/blade_ward/cast(atom/cast_on) . = ..() var/datum/status_effect/status = /datum/status_effect/buff/bladeward - var/duration_increase = max(0, attuned_strength * 1.5 MINUTES) + var/duration_increase = max(0, spell_magnitude_modifier * 1.5 MINUTES) if(isliving(owner)) var/mob/living/L = owner L.apply_status_effect(status, initial(status.duration) + duration_increase) @@ -28,10 +26,10 @@ span_notice("I trace a sigil of warding in the air."), ) - if(attuned_strength < 1.5) + if(spell_magnitude_modifier < 1.5) return - for(var/mob/living/extra_target in orange(FLOOR(attuned_strength, 1), owner)) + for(var/mob/living/extra_target in orange(FLOOR(spell_magnitude_modifier, 1), owner)) extra_target.apply_status_effect(status, initial(status.duration) + duration_increase) extra_target.visible_message( span_info("[extra_target] has a sigil of warding appear over them."), diff --git a/code/modules/spells/spell_types/undirected/bless_crops.dm b/code/modules/spells/spell_types/undirected/bless_crops.dm index 87fd2e789d6..c17695d80ee 100644 --- a/code/modules/spells/spell_types/undirected/bless_crops.dm +++ b/code/modules/spells/spell_types/undirected/bless_crops.dm @@ -8,10 +8,6 @@ antimagic_flags = MAGIC_RESISTANCE_HOLY associated_skill = /datum/attribute/skill/magic/holy required_items = list(/obj/item/clothing/neck/psycross/silver/divine/dendor) - attunements = list( - /datum/attunement/earth = 0.5, - /datum/attunement/life = 0.5, - ) invocation = "The Treefather commands thee, be fruitful!" invocation_type = INVOCATION_SHOUT diff --git a/code/modules/spells/spell_types/undirected/conjure_item/_conjure_item.dm b/code/modules/spells/spell_types/undirected/conjure_item/_conjure_item.dm index 9bcb3531614..b98a3461dec 100644 --- a/code/modules/spells/spell_types/undirected/conjure_item/_conjure_item.dm +++ b/code/modules/spells/spell_types/undirected/conjure_item/_conjure_item.dm @@ -4,7 +4,6 @@ * Create a single item, attaching [/datum/component/conjured_item] */ /datum/action/cooldown/spell/undirected/conjure_item - school = SCHOOL_CONJURATION invocation_type = INVOCATION_NONE charge_required = FALSE diff --git a/code/modules/spells/spell_types/undirected/conjure_item/briar_claw.dm b/code/modules/spells/spell_types/undirected/conjure_item/briar_claw.dm index d9c6fef82c9..ed63633d3c0 100644 --- a/code/modules/spells/spell_types/undirected/conjure_item/briar_claw.dm +++ b/code/modules/spells/spell_types/undirected/conjure_item/briar_claw.dm @@ -16,10 +16,6 @@ refresh_count = 0 delete_old = TRUE item_outline = "#1bab68" - attunements = list( - /datum/attunement/blood = 0.3, - /datum/attunement/earth = 0.7 - ) /datum/action/cooldown/spell/undirected/conjure_item/briar_claw/can_cast_spell(feedback) . = ..() diff --git a/code/modules/spells/spell_types/undirected/conjure_item/call_bird.dm b/code/modules/spells/spell_types/undirected/conjure_item/call_bird.dm index 77d3fd611a2..869bc9b2070 100644 --- a/code/modules/spells/spell_types/undirected/conjure_item/call_bird.dm +++ b/code/modules/spells/spell_types/undirected/conjure_item/call_bird.dm @@ -10,10 +10,6 @@ charge_required = FALSE experience_modifier = 0 - attunements = list( - /datum/attunement/light = 0.3, - ) - var/bird_called = FALSE var/obj/item/reagent_containers/food/snacks/messenger_bird/owned_bird = null diff --git a/code/modules/spells/spell_types/undirected/conjure_item/light.dm b/code/modules/spells/spell_types/undirected/conjure_item/light.dm index 12efc8089bb..9a748e79b66 100644 --- a/code/modules/spells/spell_types/undirected/conjure_item/light.dm +++ b/code/modules/spells/spell_types/undirected/conjure_item/light.dm @@ -4,7 +4,6 @@ button_icon_state = "light" sound = 'sound/magic/whiteflame.ogg' - point_cost = 1 cooldown_time = 30 SECONDS spell_cost = 50 @@ -16,9 +15,7 @@ item_type = /obj/item/flashlight/flare/light item_duration = null - attunements = list( - /datum/attunement/light = 0.3, - ) + required_form = FORM_FIRE /datum/action/cooldown/spell/undirected/conjure_item/light/make_item() var/obj/item = ..() diff --git a/code/modules/spells/spell_types/undirected/conjure_item/magicians_brick.dm b/code/modules/spells/spell_types/undirected/conjure_item/magicians_brick.dm index c76538dedaf..c9b48d24497 100644 --- a/code/modules/spells/spell_types/undirected/conjure_item/magicians_brick.dm +++ b/code/modules/spells/spell_types/undirected/conjure_item/magicians_brick.dm @@ -5,8 +5,6 @@ button_icon_state = "magicians_brick" sound = 'sound/magic/whiteflame.ogg' - point_cost = 1 - cooldown_time = 5 SECONDS spell_cost = 30 @@ -17,9 +15,8 @@ item_duration = 3 MINUTES item_outline = "#6495ED" - attunements = list( - /datum/attunement/earth = 0.3, - ) + required_form = FORM_EARTH + required_technique = TECHNIQUE_CREATION /datum/action/cooldown/spell/undirected/conjure_item/brick/make_item() . = ..() @@ -31,8 +28,8 @@ return var/obj/item/brick = . var/int_scaling = INT - 10 - brick.force = (brick.force + int_scaling) * attuned_strength - brick.throwforce = (brick.throwforce + int_scaling * 2) * attuned_strength // 2x scaling for throwing. Let's go. + brick.force = (brick.force + int_scaling) * spell_magnitude_modifier + brick.throwforce = (brick.throwforce + int_scaling * 2) * spell_magnitude_modifier // 2x scaling for throwing. Let's go. brick.name = "magician's brick +[int_scaling]" return brick diff --git a/code/modules/spells/spell_types/undirected/create_cloud.dm b/code/modules/spells/spell_types/undirected/create_cloud.dm index 2fbc83af144..a395e7b2c4a 100644 --- a/code/modules/spells/spell_types/undirected/create_cloud.dm +++ b/code/modules/spells/spell_types/undirected/create_cloud.dm @@ -5,8 +5,6 @@ button_icon_state = "aerosolize" sound = 'sound/magic/whiteflame.ogg' - point_cost = 1 - charge_required = FALSE cooldown_time = 20 SECONDS spell_cost = 30 @@ -14,10 +12,9 @@ invocation = "Create cloud!" invocation_type = INVOCATION_SHOUT - attunements = list( - /datum/attunement/blood = 0.3, - /datum/attunement/death = 0.3, - ) + required_form = FORM_WATER + required_technique = TECHNIQUE_ALTERATION + required_level = 4 /datum/action/cooldown/spell/undirected/create_cloud/is_valid_target(atom/cast_on) . = ..() @@ -51,4 +48,5 @@ var/datum/effect_system/smoke_spread/chem/smoke = new smoke.set_up(reagents, 3, get_turf(owner), FALSE) smoke.start() + held_item.reagents.remove_all(held_item.reagents.total_volume) diff --git a/code/modules/spells/spell_types/undirected/feather_fall.dm b/code/modules/spells/spell_types/undirected/feather_fall.dm index 20020a98d91..954f9c8434b 100644 --- a/code/modules/spells/spell_types/undirected/feather_fall.dm +++ b/code/modules/spells/spell_types/undirected/feather_fall.dm @@ -4,19 +4,18 @@ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC|SPELL_REQUIRES_NO_MOVE button_icon_state = "jump" - point_cost = 1 - charge_time = 4 SECONDS cooldown_time = 3 MINUTES spell_cost = 50 spell_flags = SPELL_RITUOS - attunements = list( - /datum/attunement/aeromancy = 0.5, - ) + + required_form = FORM_AIR + required_technique = TECHNIQUE_ALTERATION + /datum/action/cooldown/spell/undirected/feather_falling/cast(atom/cast_on) . = ..() var/datum/status_effect/status = /datum/status_effect/buff/featherfall - var/duration_increase = max(0, attuned_strength * 90 SECONDS) - for(var/mob/living/L in viewers(max(1, FLOOR(attuned_strength, 1)), owner)) + var/duration_increase = max(0, spell_magnitude_modifier * 90 SECONDS) + for(var/mob/living/L in viewers(max(1, FLOOR(spell_magnitude_modifier, 1)), owner)) L.apply_status_effect(status, initial(status.duration) + duration_increase) diff --git a/code/modules/spells/spell_types/undirected/forcewall.dm b/code/modules/spells/spell_types/undirected/forcewall.dm index 8e437573041..8118c04e944 100644 --- a/code/modules/spells/spell_types/undirected/forcewall.dm +++ b/code/modules/spells/spell_types/undirected/forcewall.dm @@ -1,13 +1,10 @@ -/datum/action/cooldown/spell/undirected/forcewall +/datum/action/cooldown/spell/forcewall name = "Forcewall" desc = "Conjure a wall of arcyne force, preventing anyone and anything other than you from moving through it." button_icon_state = "forcewall" - point_cost = 3 - attunements = list( - /datum/attunement/illusion = 0.3, - ) - school = SCHOOL_TRANSMUTATION + required_form = FORM_ARCANE + required_technique = TECHNIQUE_ILLUSION charge_time = 2 SECONDS charge_drain = 1 @@ -15,19 +12,73 @@ cooldown_time = 35 SECONDS spell_cost = 30 spell_flags = SPELL_RITUOS - var/wall_type = /obj/effect/forcefield/wizard + var/telegraph_type = /obj/effect/temp_visual/trap_wall -/datum/action/cooldown/spell/undirected/forcewall/cast(atom/cast_on) +/datum/action/cooldown/spell/forcewall/cast(atom/cast_on) . = ..() - var/turf/ahead = get_step(owner, owner.dir) - new wall_type(ahead, owner, antimagic_flags) - if(owner.dir == SOUTH || owner.dir == NORTH) - new wall_type(get_step(ahead, EAST), owner, antimagic_flags) - new wall_type(get_step(ahead, WEST), owner, antimagic_flags) + var/mob/living/carbon/human/H = owner + if(!istype(H)) + return FALSE + + var/turf/front = get_turf(cast_on) + if(!front) + return FALSE + + var/list/affected_turfs = list() + affected_turfs += front + + if(H.dir == SOUTH || H.dir == NORTH) + affected_turfs += get_step(front, WEST) + affected_turfs += get_step(get_step(front, WEST), WEST) + affected_turfs += get_step(front, EAST) + affected_turfs += get_step(get_step(front, EAST), EAST) else - new wall_type(get_step(ahead, NORTH), owner, antimagic_flags) - new wall_type(get_step(ahead, SOUTH), owner, antimagic_flags) + affected_turfs += get_step(front, NORTH) + affected_turfs += get_step(get_step(front, NORTH), NORTH) + affected_turfs += get_step(front, SOUTH) + affected_turfs += get_step(get_step(front, SOUTH), SOUTH) + + for(var/turf/affected_turf in affected_turfs) + new telegraph_type(affected_turf) + addtimer(CALLBACK(src, PROC_REF(spawn_wall), affected_turf, H), 1 SECONDS) + + H.visible_message("[H] mutters an incantation and a wall of arcyne force manifests out of thin air!") + return TRUE + +/datum/action/cooldown/spell/forcewall/proc/spawn_wall(turf/target, mob/caster) + new /obj/structure/forcefield_weak(target, caster) + + +/obj/structure/forcefield_weak + desc = "A wall of pure arcyne force." + name = "Arcyne Wall" + icon = 'icons/effects/effects.dmi' + icon_state = "arcynewall" + break_sound = 'sound/combat/hits/onstone/stonedeath.ogg' + attacked_sound = list('sound/combat/hits/onstone/wallhit.ogg', 'sound/combat/hits/onstone/wallhit2.ogg', 'sound/combat/hits/onstone/wallhit3.ogg') + opacity = 0 + density = TRUE + max_integrity = 150 + CanAtmosPass = ATMOS_PASS_DENSITY + var/timeleft = 20 SECONDS + var/mob/caster + +/obj/structure/forcefield_weak/Initialize(mapload, mob/summoner) + . = ..() + caster = summoner + if(timeleft) + QDEL_IN(src, timeleft) + +/obj/structure/forcefield_weak/CanPass(atom/movable/mover, turf/target) + . = ..() + if(ismob(mover)) + var/mob/M = mover + if(M.anti_magic_check(chargecost = 0)) + return TRUE + return FALSE -/datum/action/cooldown/spell/undirected/forcewall/breakable - name = "Lesser Forcewall" - wall_type = /obj/structure/forcefield/casted +/obj/effect/temp_visual/trap_wall + icon = 'icons/effects/effects.dmi' + icon_state = "trap" + light_outer_range = 2 + duration = 1 SECONDS diff --git a/code/modules/spells/spell_types/undirected/hunters_grace.dm b/code/modules/spells/spell_types/undirected/hunters_grace.dm index a4748c70c15..541cb0069e2 100644 --- a/code/modules/spells/spell_types/undirected/hunters_grace.dm +++ b/code/modules/spells/spell_types/undirected/hunters_grace.dm @@ -3,11 +3,6 @@ desc = "Grant yourself and any creatures adjacent to you free movement through rough terrain." button_icon_state = "bush_jaunt" - point_cost = 1 - attunements = list( - /datum/attunement/aeromancy = 0.8, - ) - spell_type = SPELL_MIRACLE antimagic_flags = MAGIC_RESISTANCE_HOLY diff --git a/code/modules/spells/spell_types/undirected/jaunt/_jaunt.dm b/code/modules/spells/spell_types/undirected/jaunt/_jaunt.dm index 147fe88ccbf..6dc6cf22150 100644 --- a/code/modules/spells/spell_types/undirected/jaunt/_jaunt.dm +++ b/code/modules/spells/spell_types/undirected/jaunt/_jaunt.dm @@ -12,8 +12,6 @@ * Use enter_jaunt() and exit_jaunt() as wrappers. */ /datum/action/cooldown/spell/undirected/jaunt - school = SCHOOL_TRANSMUTATION - invocation_type = INVOCATION_NONE spell_flags = SPELL_UNETCHABLE diff --git a/code/modules/spells/spell_types/undirected/jaunt/bush_jaunt.dm b/code/modules/spells/spell_types/undirected/jaunt/bush_jaunt.dm index e4a23e83a3e..2e208972c76 100644 --- a/code/modules/spells/spell_types/undirected/jaunt/bush_jaunt.dm +++ b/code/modules/spells/spell_types/undirected/jaunt/bush_jaunt.dm @@ -13,7 +13,6 @@ cooldown_time = 2 MINUTES spell_cost = 20 has_visual_effects = FALSE - attunements = list(/datum/attunement/life = 0.5) var/obj/effect/dummy/bush_disguise/active_dummy = null var/static/list/allowed_structures = list( diff --git a/code/modules/spells/spell_types/undirected/jaunt/ethereal_jaunt.dm b/code/modules/spells/spell_types/undirected/jaunt/ethereal_jaunt.dm index f50997a02c1..fa7672107c8 100644 --- a/code/modules/spells/spell_types/undirected/jaunt/ethereal_jaunt.dm +++ b/code/modules/spells/spell_types/undirected/jaunt/ethereal_jaunt.dm @@ -9,9 +9,8 @@ invocation = "VANISHIKA" invocation_type = INVOCATION_SHOUT - attunements = list( - /datum/attunement/arcyne = 0.4, - ) + required_form = FORM_ARCANE + required_technique = TECHNIQUE_ALTERATION jaunt_type = /obj/effect/dummy/phased_mob/spell_jaunt diff --git a/code/modules/spells/spell_types/undirected/jaunt/illusory_prop.dm b/code/modules/spells/spell_types/undirected/jaunt/illusory_prop.dm index e0c0d1ad221..27932a53894 100644 --- a/code/modules/spells/spell_types/undirected/jaunt/illusory_prop.dm +++ b/code/modules/spells/spell_types/undirected/jaunt/illusory_prop.dm @@ -13,7 +13,9 @@ cooldown_time = 2 MINUTES spell_cost = 25 has_visual_effects = FALSE - attunements = list(/datum/attunement/illusion = 0.6) + + required_form = FORM_EARTH + required_technique = TECHNIQUE_ILLUSION var/obj/effect/dummy/bush_disguise/active_dummy = null var/static/list/allowed_structures = list( diff --git a/code/modules/spells/spell_types/undirected/list_target/encode_thoughts.dm b/code/modules/spells/spell_types/undirected/list_target/encode_thoughts.dm index f2497259be7..e20c3a12693 100644 --- a/code/modules/spells/spell_types/undirected/list_target/encode_thoughts.dm +++ b/code/modules/spells/spell_types/undirected/list_target/encode_thoughts.dm @@ -4,10 +4,7 @@ button_icon_state = "encode_thought" sound = 'sound/magic/PSY.ogg' - point_cost = 1 - attunements = list( - /datum/attunement/dark = 0.5, - ) + required_form = FORM_DEATH cooldown_time = 25 SECONDS spell_cost = 25 @@ -39,6 +36,6 @@ /datum/action/cooldown/spell/undirected/list_target/encode_thoughts/vampire name = "Vampiric Manipulation" - point_cost = 0 spell_type = SPELL_BLOOD spell_flags = NONE + required_form = null diff --git a/code/modules/spells/spell_types/undirected/longstrider.dm b/code/modules/spells/spell_types/undirected/longstrider.dm index 522dbcb3f76..32c13834f07 100644 --- a/code/modules/spells/spell_types/undirected/longstrider.dm +++ b/code/modules/spells/spell_types/undirected/longstrider.dm @@ -3,11 +3,8 @@ desc = "Grant yourself and any creatures adjacent to you free movement through rough terrain." button_icon_state = "longstride" - point_cost = 1 - attunements = list( - /datum/attunement/aeromancy = 0.8, - ) - school = SCHOOL_TRANSMUTATION + required_form = FORM_AIR + required_technique = TECHNIQUE_ALTERATION invocation = "Walk unopposed." invocation_type = INVOCATION_WHISPER @@ -26,7 +23,7 @@ span_notice("I mutter the incantation and a dim pulse of light radiates out from me."), ) - var/duration_increase = max(0, attuned_strength * 2 MINUTES) + var/duration_increase = max(0, spell_magnitude_modifier * 2 MINUTES) for(var/mob/living/L in viewers(1, owner)) L.apply_status_effect(/datum/status_effect/buff/longstrider, duration_increase) diff --git a/code/modules/spells/spell_types/undirected/message.dm b/code/modules/spells/spell_types/undirected/message.dm index 5df77407fc8..1f782c25bac 100644 --- a/code/modules/spells/spell_types/undirected/message.dm +++ b/code/modules/spells/spell_types/undirected/message.dm @@ -4,7 +4,7 @@ desc = "Latch onto the mind of one who is familiar to you, whispering a message into their head." button_icon_state = "message" - point_cost = 1 + required_form = FORM_AIR charge_required = FALSE spell_cost = 30 diff --git a/code/modules/spells/spell_types/undirected/rituos.dm b/code/modules/spells/spell_types/undirected/rituos.dm index 0a83c512b17..3cdb470c597 100644 --- a/code/modules/spells/spell_types/undirected/rituos.dm +++ b/code/modules/spells/spell_types/undirected/rituos.dm @@ -14,10 +14,6 @@ cooldown_time = 3 HOURS spell_cost = 120 - attunements = list( - /datum/attunement/death = 0.5 - ) - /// Weakref to granted spell, for the dream ending var/datum/weakref/granted_spell @@ -64,7 +60,7 @@ cast_on.mob_biotypes |= MOB_UNDEAD cast_on.mana_pool.intrinsic_recharge_sources &= ~MANA_ALL_LEYLINES cast_on.mana_pool.set_intrinsic_recharge(MANA_SOULS) - cast_on.add_spell(/datum/action/cooldown/spell/undirected/arcyne_eye) + cast_on.add_spell(/datum/action/cooldown/spell/undirected/arcyne_eye, mastery_spell = TRUE) to_chat(cast_on, span_smallred("I have forsaken the living. I am now closer to a deadite than a mortal... but I still yet draw breath and bleed.")) var/obj/item/bodypart/the_part = pick(get_unskeletonized_bodyparts(cast_on)) @@ -79,8 +75,10 @@ if(!length(get_unskeletonized_bodyparts(cast_on))) cast_on.adjust_stat_modifier(STATMOD_RITUAL, list(/datum/attribute/skill/magic/arcane = 30)) - cast_on.add_spell(/datum/action/cooldown/spell/undirected/touch/prestidigitation) - cast_on.adjust_spell_points(18) + cast_on.add_spell(/datum/action/cooldown/spell/undirected/touch/prestidigitation, mastery_spell = TRUE) + //! MAGIC BALANCE POINT + cast_on.adjust_form_mastery_points(20) + cast_on.adjust_technique_mastery_points(12) cast_on.visible_message( span_boldwarning("[cast_on]'s form swells with terrible power as they cast away almost all of \ the remnants of their mortal flesh, arcyne runes glowing upon their exposed bones..."), diff --git a/code/modules/spells/spell_types/undirected/secondsight.dm b/code/modules/spells/spell_types/undirected/secondsight.dm index d122b3e4919..dacc4e96bfa 100644 --- a/code/modules/spells/spell_types/undirected/secondsight.dm +++ b/code/modules/spells/spell_types/undirected/secondsight.dm @@ -17,7 +17,7 @@ if(L.has_status_effect(status)) L.remove_status_effect(status) else - var/duration_increase = attuned_strength * 1.0 MINUTES // god damnit this shit won't compile or some shit without these. Fuck it. + var/duration_increase = spell_magnitude_modifier * 1.0 MINUTES // god damnit this shit won't compile or some shit without these. Fuck it. L.apply_status_effect(status, initial(status.duration) + duration_increase) /datum/status_effect/buff/secondsight diff --git a/code/modules/spells/spell_types/undirected/shadow_step.dm b/code/modules/spells/spell_types/undirected/shadow_step.dm index 1743c7b8f4d..df7d9bbf318 100644 --- a/code/modules/spells/spell_types/undirected/shadow_step.dm +++ b/code/modules/spells/spell_types/undirected/shadow_step.dm @@ -9,9 +9,9 @@ cooldown_time = 5 MINUTES spell_cost = 15 spell_flags = SPELL_RITUOS - attunements = list( - /datum/attunement/dark = 0.4 - ) + + required_form = FORM_DEATH + required_technique = TECHNIQUE_ALTERATION /datum/action/cooldown/spell/undirected/shadow_step/cast(atom/cast_on) . = ..() diff --git a/code/modules/spells/spell_types/undirected/shapeshift/_shapeshift.dm b/code/modules/spells/spell_types/undirected/shapeshift/_shapeshift.dm index 9178d0d6664..c1ce1de839a 100644 --- a/code/modules/spells/spell_types/undirected/shapeshift/_shapeshift.dm +++ b/code/modules/spells/spell_types/undirected/shapeshift/_shapeshift.dm @@ -8,7 +8,6 @@ */ /datum/action/cooldown/spell/undirected/shapeshift button_icon_state = "shapeshift" - school = SCHOOL_TRANSMUTATION cooldown_time = 10 SECONDS charge_required = FALSE spell_flags = SPELL_UNETCHABLE diff --git a/code/modules/spells/spell_types/undirected/shapeshift/bat.dm b/code/modules/spells/spell_types/undirected/shapeshift/bat.dm index dd2b26e2918..82c2db1742c 100644 --- a/code/modules/spells/spell_types/undirected/shapeshift/bat.dm +++ b/code/modules/spells/spell_types/undirected/shapeshift/bat.dm @@ -2,11 +2,6 @@ name = "Bat Form" desc = "Transform into a lesser bat to fly into the nite." - attunements = list( - /datum/attunement/dark = 0.4, - /datum/attunement/polymorph = 0.5, - ) - charge_required = FALSE cooldown_time = 50 SECONDS diff --git a/code/modules/spells/spell_types/undirected/shapeshift/frog.dm b/code/modules/spells/spell_types/undirected/shapeshift/frog.dm index 0ed1bea09b5..942819f2ed2 100644 --- a/code/modules/spells/spell_types/undirected/shapeshift/frog.dm +++ b/code/modules/spells/spell_types/undirected/shapeshift/frog.dm @@ -2,11 +2,6 @@ name = "Frog Form" desc = "Transform into a frog. Damage is not inherited between forms." - attunements = list( - /datum/attunement/dark = 0.4, - /datum/attunement/polymorph = 0.5, - /datum/attunement/aeromancy = 0.3, - ) charge_required = FALSE cooldown_time = 50 SECONDS diff --git a/code/modules/spells/spell_types/undirected/shapeshift/gaseous_form.dm b/code/modules/spells/spell_types/undirected/shapeshift/gaseous_form.dm index 95c4e223f49..7e338a906cd 100644 --- a/code/modules/spells/spell_types/undirected/shapeshift/gaseous_form.dm +++ b/code/modules/spells/spell_types/undirected/shapeshift/gaseous_form.dm @@ -2,12 +2,6 @@ name = "Mist Form" desc = "Transform into a cloud of mist." - attunements = list( - /datum/attunement/dark = 0.4, - /datum/attunement/polymorph = 0.5, - /datum/attunement/aeromancy = 0.3, - ) - charge_required = FALSE cooldown_time = 50 SECONDS diff --git a/code/modules/spells/spell_types/undirected/shapeshift/rat.dm b/code/modules/spells/spell_types/undirected/shapeshift/rat.dm index c2c5c31800d..094f6fefaf5 100644 --- a/code/modules/spells/spell_types/undirected/shapeshift/rat.dm +++ b/code/modules/spells/spell_types/undirected/shapeshift/rat.dm @@ -2,12 +2,6 @@ name = "Rat Form" desc = "Transform into a big rat. Damage is not inherited between forms." - attunements = list( - /datum/attunement/dark = 0.4, - /datum/attunement/polymorph = 0.5, - /datum/attunement/aeromancy = 0.3, - ) - charge_required = FALSE cooldown_time = 50 SECONDS diff --git a/code/modules/spells/spell_types/undirected/shapeshift/rat_vampire.dm b/code/modules/spells/spell_types/undirected/shapeshift/rat_vampire.dm index f3bd12dd970..d267ffd4d99 100644 --- a/code/modules/spells/spell_types/undirected/shapeshift/rat_vampire.dm +++ b/code/modules/spells/spell_types/undirected/shapeshift/rat_vampire.dm @@ -2,12 +2,6 @@ name = "Vampiric Rat Form" desc = "Transform into a small rat. Damage is not inherited between forms." - attunements = list( - /datum/attunement/dark = 0.4, - /datum/attunement/polymorph = 0.5, - /datum/attunement/aeromancy = 0.3, - ) - charge_required = FALSE cooldown_time = 30 SECONDS diff --git a/code/modules/spells/spell_types/undirected/teleport/_teleport.dm b/code/modules/spells/spell_types/undirected/teleport/_teleport.dm index 0b2a0c0935d..c07591e5bec 100644 --- a/code/modules/spells/spell_types/undirected/teleport/_teleport.dm +++ b/code/modules/spells/spell_types/undirected/teleport/_teleport.dm @@ -4,8 +4,6 @@ * Teleports the caster to a turf selected by get_destinations(). */ /datum/action/cooldown/spell/undirected/teleport - school = SCHOOL_TRANSLOCATION - /// What channel the teleport is done under. var/teleport_channel = TELEPORT_CHANNEL_MAGIC /// Whether we force the teleport to happen (ie, it cannot be blocked by noteleport areas or blessings or whatever) diff --git a/code/modules/spells/spell_types/undirected/touch/_touch.dm b/code/modules/spells/spell_types/undirected/touch/_touch.dm index 8ccd0a2716d..dc833994c4e 100644 --- a/code/modules/spells/spell_types/undirected/touch/_touch.dm +++ b/code/modules/spells/spell_types/undirected/touch/_touch.dm @@ -18,6 +18,7 @@ * (generally) inadvisable unless you know what you're doing */ /datum/action/cooldown/spell/undirected/touch + abstract_type = /datum/action/cooldown/spell/undirected/touch check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_HANDS_BLOCKED charge_required = FALSE has_visual_effects = FALSE diff --git a/code/modules/spells/spell_types/undirected/touch/darkvision.dm b/code/modules/spells/spell_types/undirected/touch/darkvision.dm index 5cc69aa8fba..961c0bc37ec 100644 --- a/code/modules/spells/spell_types/undirected/touch/darkvision.dm +++ b/code/modules/spells/spell_types/undirected/touch/darkvision.dm @@ -4,10 +4,8 @@ button_icon_state = "darkvision" can_cast_on_self = TRUE - point_cost = 2 - attunements = list( - /datum/attunement/light = 0.6, - ) + + required_form = FORM_DEATH spell_flags = SPELL_RITUOS cooldown_time = 6 MINUTES @@ -17,7 +15,7 @@ charges = 3 /datum/action/cooldown/spell/undirected/touch/darkvision/adjust_hand_charges() - charges += FLOOR(attuned_strength * 1.5, 1) + charges += FLOOR(spell_magnitude_modifier * 1.5, 1) /datum/action/cooldown/spell/undirected/touch/darkvision/is_valid_target(atom/cast_on) . = ..() @@ -30,7 +28,7 @@ if(!do_after(caster, 5 SECONDS, victim)) return FALSE - var/duration_increase = attuned_strength * 2 MINUTES + var/duration_increase = spell_magnitude_modifier * 2 MINUTES if(victim != caster) caster.visible_message("[caster] draws a glyph in the air and touches [victim] with an arcyne focus.") diff --git a/code/modules/spells/spell_types/undirected/touch/entangler.dm b/code/modules/spells/spell_types/undirected/touch/entangler.dm index 7304cc0d582..1e0ee97b460 100644 --- a/code/modules/spells/spell_types/undirected/touch/entangler.dm +++ b/code/modules/spells/spell_types/undirected/touch/entangler.dm @@ -7,9 +7,6 @@ spell_type = SPELL_MIRACLE associated_skill = /datum/attribute/skill/magic/holy required_items = list(/obj/item/clothing/neck/psycross/silver/divine/dendor) - attunements = list( - /datum/attunement/earth = 0.5, - ) cooldown_time = 5 MINUTES spell_cost = 15 diff --git a/code/modules/spells/spell_types/undirected/touch/non_detection.dm b/code/modules/spells/spell_types/undirected/touch/non_detection.dm index da1cf967d78..420f0efe5f0 100644 --- a/code/modules/spells/spell_types/undirected/touch/non_detection.dm +++ b/code/modules/spells/spell_types/undirected/touch/non_detection.dm @@ -4,10 +4,9 @@ button_icon_state = "prestidigitation" can_cast_on_self = TRUE - point_cost = 1 - attunements = list( - /datum/attunement/illusion = 0.4, - ) + required_form = FORM_EARTH + required_technique = TECHNIQUE_ILLUSION + spell_flags = SPELL_RITUOS cooldown_time = 10 MINUTES @@ -17,7 +16,7 @@ charges = 2 /datum/action/cooldown/spell/undirected/touch/non_detection/adjust_hand_charges() - charges += FLOOR(attuned_strength * 1.5, 1) + charges += FLOOR(spell_magnitude_modifier * 1.5, 1) /datum/action/cooldown/spell/undirected/touch/non_detection/is_valid_target(atom/cast_on) . = ..() diff --git a/code/modules/spells/spell_types/undirected/touch/orison.dm b/code/modules/spells/spell_types/undirected/touch/orison.dm index fe321453e19..30a7798b94a 100644 --- a/code/modules/spells/spell_types/undirected/touch/orison.dm +++ b/code/modules/spells/spell_types/undirected/touch/orison.dm @@ -32,7 +32,6 @@ spell_type = SPELL_MIRACLE associated_skill = /datum/attribute/skill/magic/holy - school = SCHOOL_TRANSMUTATION cooldown_time = 3 MINUTES diff --git a/code/modules/spells/spell_types/undirected/touch/prestidigitation.dm b/code/modules/spells/spell_types/undirected/touch/prestidigitation.dm index cd30e99bf7d..7820b1a856d 100644 --- a/code/modules/spells/spell_types/undirected/touch/prestidigitation.dm +++ b/code/modules/spells/spell_types/undirected/touch/prestidigitation.dm @@ -9,11 +9,7 @@ button_icon_state = "prestidigitation" can_cast_on_self = TRUE - point_cost = 1 - school = SCHOOL_TRANSMUTATION - attunements = list( - /datum/attunement/arcyne = 0.2, - ) + required_form = FORM_ARCANE cooldown_time = 1 MINUTES diff --git a/code/modules/spells/spell_types/undirected/touch/psydon_blade.dm b/code/modules/spells/spell_types/undirected/touch/psydon_blade.dm deleted file mode 100644 index cd88191420a..00000000000 --- a/code/modules/spells/spell_types/undirected/touch/psydon_blade.dm +++ /dev/null @@ -1,45 +0,0 @@ -/datum/action/cooldown/spell/undirected/touch/bladeofpsydon - name = "Blade of Psydon" - desc = "The manifestation of the higher concept of a blade itself. Said to be drawn upon from Noc's treasury of wisdom, each casting a poor facsimile of the perfect weapon They hold." - button_icon_state = "boundkatar" - can_cast_on_self = TRUE - - point_cost = 2 - attunements = list( - /datum/attunement/light = 0.6, - ) - - hand_path = /obj/item/melee/touch_attack/bladeofpsydon - draw_message = "I imagine the perfect weapon, forged by arcyne knowledge, it's edge flawless. \ - I feel it in my mind's eye -- but it's just out of reach. I pull away it's shadow, a bad copy, and yet it is one of a great weapon nonetheless... " - drop_message = "I release my arcyne focus." - charges = 3 - -/datum/action/cooldown/spell/undirected/touch/bladeofpsydon/adjust_hand_charges() - charges += FLOOR(attuned_strength * 1.5, 1) - -/datum/action/cooldown/spell/undirected/touch/bladeofpsydon/is_valid_target(atom/cast_on) - . = ..() - if(!.) - return FALSE - return isliving(cast_on) - - -/obj/item/melee/touch_attack/bladeofpsydon - name = "\improper arcyne push dagger" - desc = "This blade throbs, translucent and iridescent, blueish arcyne energies running through its translucent surface..." - icon = 'icons/mob/actions/roguespells.dmi' - icon_state = "katar_bound" - force = DAMAGE_KATAR + 9 - wdefense = GREAT_PARRY - wlength = WLENGTH_SHORT - possible_item_intents = list(KATAR_CUT, KATAR_THRUST) - max_blade_int = 999 - max_integrity = INTEGRITY_WORST / 2 - - gripsprite = FALSE - w_class = WEIGHT_CLASS_HUGE - parrysound = list('sound/combat/parry/bladed/bladedsmall (1).ogg','sound/combat/parry/bladed/bladedsmall (2).ogg','sound/combat/parry/bladed/bladedsmall (3).ogg') - swingsound = list('sound/combat/wooshes/bladed/wooshsmall (1).ogg','sound/combat/wooshes/bladed/wooshsmall (2).ogg','sound/combat/wooshes/bladed/wooshsmall (3).ogg') - associated_skill = /datum/attribute/skill/combat/unarmed - pickup_sound = 'sound/foley/equip/swordsmall2.ogg' diff --git a/code/modules/surgery/bodyparts/bodypart_spell_dismemberment.dm b/code/modules/surgery/bodyparts/bodypart_spell_dismemberment.dm new file mode 100644 index 00000000000..9f443afe263 --- /dev/null +++ b/code/modules/surgery/bodyparts/bodypart_spell_dismemberment.dm @@ -0,0 +1,33 @@ +/obj/item/bodypart/proc/get_spell_dismemberment_chance(force, bclass, zone_precise = src.body_zone) + if(!can_dismember()) + return 0 + if(bclass != BCLASS_CUT && bclass != BCLASS_CHOP) + return 0 + if(bclass == BCLASS_CHOP) + force *= 1.1 + return dismemberment_chance_from_force(force, zone_precise) + +/obj/item/bodypart/proc/dismemberment_chance_from_force(nuforce, zone_precise = src.body_zone) + if(nuforce < 10) + return 0 + var/total_dam = get_damage() + var/probability = nuforce * (total_dam / max_damage) + var/hard_dismember = HAS_TRAIT(src, TRAIT_HARDDISMEMBER) + var/easy_dismember = HAS_TRAIT(src, TRAIT_ROTTEN) || skeletonized || HAS_TRAIT(src, TRAIT_EASYDISMEMBER) + var/easy_decapitation = HAS_TRAIT(src, TRAIT_EASYDISMEMBER) + if(owner) + if(!hard_dismember) + hard_dismember = HAS_TRAIT(owner, TRAIT_HARDDISMEMBER) + if(!easy_dismember) + easy_dismember = HAS_TRAIT(owner, TRAIT_EASYDISMEMBER) + if(!easy_decapitation) + easy_decapitation = HAS_TRAIT(owner, TRAIT_EASYDISMEMBER) + if((total_dam <= (max_damage * 0.7)) && !easy_dismember) + return 0 + if(easy_decapitation && zone_precise == BODY_ZONE_PRECISE_NECK) + return probability * 1.5 + if(hard_dismember) + return min(probability, 5) + else if(easy_dismember) + return probability * 1.5 + return probability diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm index c8341db88eb..687ddf40d92 100644 --- a/code/modules/unit_tests/unit_test.dm +++ b/code/modules/unit_tests/unit_test.dm @@ -155,6 +155,8 @@ GLOBAL_LIST_EMPTY(required_map_items) /obj/effect/meatvine_controller, //Single use case holder atom requiring a user /atom/movable/looking_holder, + ///same as with the primordials + /obj/effect/primordial_pool, ) /// ??? ignore += typesof(/obj/effect/bombard_zone) @@ -189,6 +191,8 @@ GLOBAL_LIST_EMPTY(required_map_items) ignore += typesof(/obj/effect/spawner) ignore += typesof(/atom/movable/screen) ignore += typesof(/obj/abstract) + ///these have turf changing race conditions when spawned specifically for create and destroy. + ignore += typesof(/mob/living/simple_animal/hostile/retaliate/primordial) return ignore diff --git a/code/modules/visual_ui/merak/_base.dm b/code/modules/visual_ui/merak/_base.dm new file mode 100644 index 00000000000..f1f53ced308 --- /dev/null +++ b/code/modules/visual_ui/merak/_base.dm @@ -0,0 +1,60 @@ +/datum/visual_ui/spellcraft_star + uniqueID = "spellcraft_star" + offset_layer = VISUAL_UI_GROUP_C + y = "BOTTOM-1:16" + x = "CENTER-8:16" + element_types_to_spawn = list( + /obj/abstract/visual_ui_element/spellcraft_star/center, + /obj/abstract/visual_ui_element/spellcraft_star/point/one, + /obj/abstract/visual_ui_element/spellcraft_star/point/two, + /obj/abstract/visual_ui_element/spellcraft_star/point/three, + /obj/abstract/visual_ui_element/spellcraft_star/point/four, + /obj/abstract/visual_ui_element/spellcraft_star/point/five, + /obj/abstract/visual_ui_element/spellcraft_star/point/six, + /obj/abstract/visual_ui_element/spellcraft_star/confirm, + ) + var/datum/weakref/book_ref + +/datum/visual_ui/spellcraft_star/New(datum/mind/M, obj/item/spellbook_unfinished/pre_arcyne/book) + book_ref = WEAKREF(book) + . = ..(M) + +/datum/visual_ui/spellcraft_star/proc/get_book() + var/obj/item/spellbook_unfinished/pre_arcyne/book = book_ref?.resolve() + if(QDELETED(book)) + return null + return book + +/datum/visual_ui/spellcraft_star/proc/get_session() + var/obj/item/spellbook_unfinished/pre_arcyne/book = get_book() + return book?.session + +/datum/visual_ui/spellcraft_star/proc/refresh_all() + for(var/obj/abstract/visual_ui_element/spellcraft_star/element in elements) + element.UpdateIcon() + +/obj/abstract/visual_ui_element/spellcraft_star + icon = 'icons/hud/merak.dmi' + element_flags = MINDUI_FLAG_TOOLTIP + mouse_opacity = MOUSE_OPACITY_ICON + var/icon_offset_x = 0 + var/icon_offset_y = 0 + +/obj/abstract/visual_ui_element/spellcraft_star/proc/get_ui() + return parent + +/obj/abstract/visual_ui_element/spellcraft_star/confirm + icon_state = "star_confirm" + name = "bind the tome" + +/obj/abstract/visual_ui_element/spellcraft_star/confirm/Click(location, control, params) + var/mob/living/user = get_user() + if(!user) + return + var/datum/visual_ui/spellcraft_star/ui = parent + var/datum/spellcraft_session/session = ui.get_session() + if(!session || !session.can_assemble()) + to_chat(user, span_warning("The star isn't ready yet.")) + return + if(session.assemble(user)) + ui.hide() diff --git a/code/modules/visual_ui/merak/center.dm b/code/modules/visual_ui/merak/center.dm new file mode 100644 index 00000000000..6728b9c9173 --- /dev/null +++ b/code/modules/visual_ui/merak/center.dm @@ -0,0 +1,38 @@ + +/obj/abstract/visual_ui_element/spellcraft_star/center + icon_state = "star_center" + name = "catalyst slot" + layer = VISUAL_UI_BUTTON + icon_offset_x = 240 + icon_offset_y = 240 + +/obj/abstract/visual_ui_element/spellcraft_star/center/UpdateIcon(appear = FALSE) + . = ..() + cut_overlays() + var/datum/visual_ui/spellcraft_star/ui = parent + var/datum/spellcraft_session/session = ui?.get_session() + var/obj/item/slotted = session?.meld + if(slotted) + var/image/item_overlay = image(icon = slotted.icon, icon_state = slotted.icon_state) + item_overlay.pixel_x = icon_offset_x + item_overlay.pixel_y = icon_offset_y + item_overlay.transform = matrix() * 3 + add_overlay(item_overlay) + name = "catalyst slot ([slotted.name])" + else + name = "catalyst slot" + +/obj/abstract/visual_ui_element/spellcraft_star/center/Click(location, control, params) + var/mob/living/user = get_user() + if(!user) + return + var/datum/visual_ui/spellcraft_star/ui = parent + var/datum/spellcraft_session/session = ui.get_session() + if(!session) + return + var/obj/item/held = user.get_active_held_item() + if(held) + if(session.try_insert_meld(held, user)) + ui.refresh_all() + else if(session.eject_meld(user)) + ui.refresh_all() diff --git a/code/modules/visual_ui/merak/slots.dm b/code/modules/visual_ui/merak/slots.dm new file mode 100644 index 00000000000..232bf59d028 --- /dev/null +++ b/code/modules/visual_ui/merak/slots.dm @@ -0,0 +1,72 @@ + +/obj/abstract/visual_ui_element/spellcraft_star/point + icon_state = "star_point_1" + name = "material slot" + layer = VISUAL_UI_BUTTON + var/index = 0 + +/obj/abstract/visual_ui_element/spellcraft_star/point/one + index = 1 + icon_offset_x = 60 + icon_offset_y = 350 + +/obj/abstract/visual_ui_element/spellcraft_star/point/two + icon_state = "star_point_2" + index = 2 + icon_offset_x = 60 + icon_offset_y = 160 + +/obj/abstract/visual_ui_element/spellcraft_star/point/three + icon_state = "star_point_3" + index = 3 + icon_offset_x = 240 + icon_offset_y = 60 + +/obj/abstract/visual_ui_element/spellcraft_star/point/four + icon_state = "star_point_4" + index = 4 + icon_offset_x = 420 + icon_offset_y = 160 + +/obj/abstract/visual_ui_element/spellcraft_star/point/five + icon_state = "star_point_5" + index = 5 + icon_offset_x = 420 + icon_offset_y = 350 + +/obj/abstract/visual_ui_element/spellcraft_star/point/six + icon_state = "star_point_6" + index = 6 + icon_offset_x = 240 + icon_offset_y = 460 + +/obj/abstract/visual_ui_element/spellcraft_star/point/UpdateIcon(appear = FALSE) + . = ..() + cut_overlays() + var/datum/visual_ui/spellcraft_star/ui = parent + var/datum/spellcraft_session/session = ui?.get_session() + var/obj/item/slotted = session?.materials[index] + if(slotted) + var/image/item_overlay = image(icon = slotted.icon, icon_state = slotted.icon_state) + item_overlay.pixel_x = icon_offset_x + item_overlay.pixel_y = icon_offset_y + item_overlay.transform = matrix() * 3 + add_overlay(item_overlay) + name = "material slot ([slotted.name])" + else + name = "material slot" + +/obj/abstract/visual_ui_element/spellcraft_star/point/Click(location, control, params) + var/mob/living/user = get_user() + if(!user) + return + var/datum/visual_ui/spellcraft_star/ui = parent + var/datum/spellcraft_session/session = ui.get_session() + if(!session) + return + var/obj/item/held = user.get_active_held_item() + if(held) + if(session.try_insert_material(index, held, user)) + ui.refresh_all() + else if(session.eject_material(index, user)) + ui.refresh_all() diff --git a/code/modules/visual_ui/quake_console/commands/test/testing_situations/test_debug_follower.dm b/code/modules/visual_ui/quake_console/commands/test/testing_situations/test_debug_follower.dm index c0d647ffea1..92cbd0c7c89 100644 --- a/code/modules/visual_ui/quake_console/commands/test/testing_situations/test_debug_follower.dm +++ b/code/modules/visual_ui/quake_console/commands/test/testing_situations/test_debug_follower.dm @@ -7,7 +7,7 @@ output.add_line("ERROR: No host to bind to.") return - var/datum/action/cooldown/spell/conjure/familiar/spell = new + var/datum/action/cooldown/spell/conjure_summon/familiar/spell = new spell.cast(starter) qdel(starter) var/mob/living/simple_animal/hostile/retaliate/wolf/familiar/pet = locate(/mob/living/simple_animal/hostile/retaliate/wolf/familiar) in get_turf(starter) diff --git a/icons/effects/32x96.dmi b/icons/effects/32x96.dmi index 052f5447da5..6862b9397bb 100644 Binary files a/icons/effects/32x96.dmi and b/icons/effects/32x96.dmi differ diff --git a/icons/effects/ice_wall.dmi b/icons/effects/ice_wall.dmi new file mode 100644 index 00000000000..2ef1edc17d0 Binary files /dev/null and b/icons/effects/ice_wall.dmi differ diff --git a/icons/hud/merak.dmi b/icons/hud/merak.dmi new file mode 100644 index 00000000000..9ddd428af64 Binary files /dev/null and b/icons/hud/merak.dmi differ diff --git a/icons/misc/rune_wards.dmi b/icons/misc/rune_wards.dmi new file mode 100644 index 00000000000..d96233de1b5 Binary files /dev/null and b/icons/misc/rune_wards.dmi differ diff --git a/icons/mob/actions/spells/mage_augmentation.dmi b/icons/mob/actions/spells/mage_augmentation.dmi new file mode 100644 index 00000000000..28c338a1609 Binary files /dev/null and b/icons/mob/actions/spells/mage_augmentation.dmi differ diff --git a/icons/mob/actions/spells/mage_battlewardry.dmi b/icons/mob/actions/spells/mage_battlewardry.dmi new file mode 100644 index 00000000000..8843c78c976 Binary files /dev/null and b/icons/mob/actions/spells/mage_battlewardry.dmi differ diff --git a/icons/mob/actions/spells/mage_conjure.dmi b/icons/mob/actions/spells/mage_conjure.dmi new file mode 100644 index 00000000000..f3155e72e61 Binary files /dev/null and b/icons/mob/actions/spells/mage_conjure.dmi differ diff --git a/icons/mob/actions/spells/mage_cryomancy.dmi b/icons/mob/actions/spells/mage_cryomancy.dmi new file mode 100644 index 00000000000..3a424b035ba Binary files /dev/null and b/icons/mob/actions/spells/mage_cryomancy.dmi differ diff --git a/icons/mob/actions/spells/mage_ferramancy.dmi b/icons/mob/actions/spells/mage_ferramancy.dmi new file mode 100644 index 00000000000..6127f7cf55f Binary files /dev/null and b/icons/mob/actions/spells/mage_ferramancy.dmi differ diff --git a/icons/mob/actions/spells/mage_fulgurmancy.dmi b/icons/mob/actions/spells/mage_fulgurmancy.dmi new file mode 100644 index 00000000000..396f4132b9a Binary files /dev/null and b/icons/mob/actions/spells/mage_fulgurmancy.dmi differ diff --git a/icons/mob/actions/spells/mage_geomancy.dmi b/icons/mob/actions/spells/mage_geomancy.dmi new file mode 100644 index 00000000000..c77115d2c6d Binary files /dev/null and b/icons/mob/actions/spells/mage_geomancy.dmi differ diff --git a/icons/mob/actions/spells/mage_hex.dmi b/icons/mob/actions/spells/mage_hex.dmi new file mode 100644 index 00000000000..e3e33e288c1 Binary files /dev/null and b/icons/mob/actions/spells/mage_hex.dmi differ diff --git a/icons/mob/actions/spells/mage_kinesis.dmi b/icons/mob/actions/spells/mage_kinesis.dmi new file mode 100644 index 00000000000..2986ac9b2ea Binary files /dev/null and b/icons/mob/actions/spells/mage_kinesis.dmi differ diff --git a/icons/mob/actions/spells/mage_pyromancy.dmi b/icons/mob/actions/spells/mage_pyromancy.dmi new file mode 100644 index 00000000000..48faf2c3c64 Binary files /dev/null and b/icons/mob/actions/spells/mage_pyromancy.dmi differ diff --git a/icons/mob/actions/spells/mage_shared.dmi b/icons/mob/actions/spells/mage_shared.dmi new file mode 100644 index 00000000000..9cb7c7ef828 Binary files /dev/null and b/icons/mob/actions/spells/mage_shared.dmi differ diff --git a/icons/mob/actions/spells/mage_telomancy.dmi b/icons/mob/actions/spells/mage_telomancy.dmi new file mode 100644 index 00000000000..ad455445437 Binary files /dev/null and b/icons/mob/actions/spells/mage_telomancy.dmi differ diff --git a/icons/mob/actions/spells/mage_utilities.dmi b/icons/mob/actions/spells/mage_utilities.dmi new file mode 100644 index 00000000000..bc04a01c333 Binary files /dev/null and b/icons/mob/actions/spells/mage_utilities.dmi differ diff --git a/icons/mob/actions/spells/necramiracles.dmi b/icons/mob/actions/spells/necramiracles.dmi new file mode 100644 index 00000000000..f0a45108331 Binary files /dev/null and b/icons/mob/actions/spells/necramiracles.dmi differ diff --git a/icons/mob/actions/spells/spellblade.dmi b/icons/mob/actions/spells/spellblade.dmi new file mode 100644 index 00000000000..4bff30dfd0f Binary files /dev/null and b/icons/mob/actions/spells/spellblade.dmi differ diff --git a/icons/mob/actions/spells/spellfist.dmi b/icons/mob/actions/spells/spellfist.dmi new file mode 100644 index 00000000000..31e545aea8c Binary files /dev/null and b/icons/mob/actions/spells/spellfist.dmi differ diff --git a/icons/mob/primordial.dmi b/icons/mob/primordial.dmi new file mode 100644 index 00000000000..be4eb1e9597 Binary files /dev/null and b/icons/mob/primordial.dmi differ diff --git a/icons/mob/screen_alert_combat.dmi b/icons/mob/screen_alert_combat.dmi new file mode 100644 index 00000000000..acf03db3e64 Binary files /dev/null and b/icons/mob/screen_alert_combat.dmi differ diff --git a/icons/mob/spirit_vengeance.dmi b/icons/mob/spirit_vengeance.dmi new file mode 100644 index 00000000000..36cfafdf969 Binary files /dev/null and b/icons/mob/spirit_vengeance.dmi differ diff --git a/icons/obj/magic_projectiles.dmi b/icons/obj/magic_projectiles.dmi index 3fc671353bd..4c6699dcae6 100644 Binary files a/icons/obj/magic_projectiles.dmi and b/icons/obj/magic_projectiles.dmi differ diff --git a/sound/combat/swingdelay_disrupted.ogg b/sound/combat/swingdelay_disrupted.ogg new file mode 100644 index 00000000000..d999e6384d1 Binary files /dev/null and b/sound/combat/swingdelay_disrupted.ogg differ diff --git a/sound/foley/stone_scrape.ogg b/sound/foley/stone_scrape.ogg new file mode 100644 index 00000000000..4dac7f44062 Binary files /dev/null and b/sound/foley/stone_scrape.ogg differ diff --git a/sound/magic/shadowstep_destination.ogg b/sound/magic/shadowstep_destination.ogg new file mode 100644 index 00000000000..f3983c995cb Binary files /dev/null and b/sound/magic/shadowstep_destination.ogg differ diff --git a/sound/magic/soulshot.ogg b/sound/magic/soulshot.ogg new file mode 100644 index 00000000000..83e551f2603 Binary files /dev/null and b/sound/magic/soulshot.ogg differ diff --git a/sound/misc/undertow.ogg b/sound/misc/undertow.ogg new file mode 100644 index 00000000000..eb104ddca51 Binary files /dev/null and b/sound/misc/undertow.ogg differ diff --git a/sound/spellbooks/bladescrape.ogg b/sound/spellbooks/bladescrape.ogg new file mode 100644 index 00000000000..0cc261f6945 Binary files /dev/null and b/sound/spellbooks/bladescrape.ogg differ diff --git a/sound/spellbooks/crystal.ogg b/sound/spellbooks/crystal.ogg new file mode 100644 index 00000000000..3ed5e177a7b Binary files /dev/null and b/sound/spellbooks/crystal.ogg differ diff --git a/sound/spellbooks/glass.ogg b/sound/spellbooks/glass.ogg new file mode 100644 index 00000000000..bc1306a92cc Binary files /dev/null and b/sound/spellbooks/glass.ogg differ diff --git a/sound/spellbooks/icicle.ogg b/sound/spellbooks/icicle.ogg new file mode 100644 index 00000000000..21b6b10ee49 Binary files /dev/null and b/sound/spellbooks/icicle.ogg differ diff --git a/sound/spellbooks/scrapeblade.ogg b/sound/spellbooks/scrapeblade.ogg new file mode 100644 index 00000000000..eddbdd12699 Binary files /dev/null and b/sound/spellbooks/scrapeblade.ogg differ diff --git a/tgui/packages/tgui/interfaces/RecipeBook/RecipeDetails.tsx b/tgui/packages/tgui/interfaces/RecipeBook/RecipeDetails.tsx index 81e1bba7de1..889039ed005 100644 --- a/tgui/packages/tgui/interfaces/RecipeBook/RecipeDetails.tsx +++ b/tgui/packages/tgui/interfaces/RecipeBook/RecipeDetails.tsx @@ -30,6 +30,7 @@ import { DetailOrgan } from './details/DetailOrgan'; import { DetailChemicalReaction } from './details/DetailChemicalReaction'; import { DetailDistillation } from './details/DetailDistillation'; import { DetailArcyneCrafting } from './details/DetailArcyneCrafting'; +import { DetailSpellcraft } from './details/DetailSpellcraft'; type Props = { recipe: Recipe; @@ -77,6 +78,8 @@ export const RecipeDetail = ({ recipe: r, lookup, pickerMap, allRecipes, essence case 'chemical_reaction': return ; case 'distillation': return ; case 'arcyne_crafting': return ; + case 'spellcraft': return ; + case 'spellcraft-item': return ; default: return No details available for type: {r.type}; } diff --git a/tgui/packages/tgui/interfaces/RecipeBook/details/DetailSpellcraft.tsx b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailSpellcraft.tsx new file mode 100644 index 00000000000..051fe2603db --- /dev/null +++ b/tgui/packages/tgui/interfaces/RecipeBook/details/DetailSpellcraft.tsx @@ -0,0 +1,61 @@ +import { Box } from 'tgui-core/components'; +import { SectionHead } from '../Primitives'; +import type { Recipe, FormTechniqueContribution } from '../types'; + +type Props = { r: Recipe }; + +const fmtMult = (v?: number) => (v !== undefined ? `×${Math.round(v * 100)}%` : '—'); +const fmtMagnitude = (v?: number) => (v !== undefined ? `${v > 0 ? '+' : ''}${v}` : '—'); +const fmtPoints = (v?: number) => (v !== undefined ? `+${v}` : '—'); + +const ContributionTable = ({ rows }: { rows: FormTechniqueContribution[] }) => ( + + + + + + + + + + + + {rows.map((c, i) => ( + + + + + + + + ))} + +
NamePointsCostCast SpeedMagnitude
+ {c.name} + {c.holder && ( + + Item Stats + + )} + {fmtPoints(c.points)}{fmtMult(c.cost_mult)}{fmtMult(c.speed_mult)}{fmtMagnitude(c.magnitude_mod)}
+); + +export const DetailSpellcraft = ({ r }: Props) => ( + <> + {!!r.forms?.length && ( + <> + Forms + + + )} + {!!r.techniques?.length && ( + <> + Techniques + + + )} + {!r.forms?.length && !r.techniques?.length && ( + No spellcraft contributions. + )} + +); diff --git a/tgui/packages/tgui/interfaces/RecipeBook/types.ts b/tgui/packages/tgui/interfaces/RecipeBook/types.ts index 74d205dcb35..3c2c37cd447 100644 --- a/tgui/packages/tgui/interfaces/RecipeBook/types.ts +++ b/tgui/packages/tgui/interfaces/RecipeBook/types.ts @@ -284,6 +284,9 @@ export interface Recipe { distill_message?: string; ingredients?: ItemRef[]; required_skill?: number; + // spellcraft + forms?: FormTechniqueContribution[]; + techniques?: FormTechniqueContribution[]; } export interface RecipeBookData { @@ -292,3 +295,13 @@ export interface RecipeBookData { recipes: Recipe[]; linked_recipes: Recipe[]; } + +export interface FormTechniqueContribution { + name: string; + color?: string; + points?: number; + cost_mult?: number; + speed_mult?: number; + magnitude_mod?: number; + holder?: number; +} diff --git a/tgui/packages/tgui/interfaces/SpellBook.tsx b/tgui/packages/tgui/interfaces/SpellBook.tsx new file mode 100644 index 00000000000..572eb2055da --- /dev/null +++ b/tgui/packages/tgui/interfaces/SpellBook.tsx @@ -0,0 +1,491 @@ +import { useState } from 'react'; +import { useBackend } from 'tgui/backend'; +import { + Box, + Button, + DmIcon, + Icon, + Section, + Stack, + Tabs, +} from 'tgui-core/components'; +import { Window } from 'tgui/layouts'; + +type Track = { + id: string; + name: string; + level: number; + rank: string; + modifiers?: { + cost: number; + castSpeed: number; + magnitude: number; + } | null; +}; + +type SpellEntry = { + path: string; + name: string; + desc: string; + technique: string | null; + form: string | null; + level: number; + formCost: number; + techniqueCost: number; + unlocked: boolean; + canLearn: boolean; + canUnlearn: boolean; + icon: string; + iconState: string; +}; + +type SpellBookData = { + unspentFormPoints: number; + unspentTechniquePoints: number; + unlearnMode: boolean; + techniqueLevels: Track[]; + formLevels: Track[]; + spells: SpellEntry[]; +}; + +type SpellBookStatic = { + techniques: string[]; + forms: string[]; +}; + +const formatModifierPart = (label: string, multiplier: number) => { + if (multiplier === 1) return null; + const pct = Math.round((multiplier - 1) * 100); + return `${pct > 0 ? '+' : ''}${pct}% ${label}`; +}; + +const formatModifierWhole = (label: string, multiplier: number) => { + if (multiplier === 0) return null; + const pct = ((multiplier)); + return `${pct > 0 ? '+' : ''}${pct} ${label}`; +}; + +const formatModifiers = (modifiers: Track['modifiers']) => { + if (!modifiers) return null; + const parts = [ + formatModifierPart('mana cost', modifiers.cost), + formatModifierPart('cast speed', modifiers.castSpeed), + formatModifierWhole('magnitude', modifiers.magnitude), + ].filter(Boolean); + return parts.length > 0 ? parts.join(', ') : null; +}; + +const SpellSprite = (props: { icon: string; iconState: string }) => { + const { icon, iconState } = props; + if (!icon || !iconState) { + return null; + } + return ( + } + icon={icon} + icon_state={iconState} + height={2} + width={2} + /> + ); +}; + +const costLabel = (spell: SpellEntry) => { + const parts: string[] = []; + if (spell.formCost > 0) { + parts.push(`${spell.formCost} form pt${spell.formCost === 1 ? '' : 's'}`); + } + if (spell.techniqueCost > 0) { + parts.push( + `${spell.techniqueCost} technique pt${spell.techniqueCost === 1 ? '' : 's'}`, + ); + } + return parts.length > 0 ? parts.join(', ') : 'Free'; +}; + +const SpellCard = (props: { + spell: SpellEntry; + unlearnMode: boolean; + onLearn: (path: string) => void; + onUnlearn: (path: string) => void; +}) => { + const { spell, unlearnMode, onLearn, onUnlearn } = props; + const status = spell.unlocked + ? 'unlocked' + : spell.canLearn + ? 'available' + : 'locked'; + + return ( + + + + + + + {spell.name} + + {spell.desc} + + + Cost: {costLabel(spell)} + {spell.level > 0 && + (spell.form || spell.technique) && + ` · Requires ${spell.form || spell.technique} ${spell.level}`} + + + + {unlearnMode ? ( +