March into Mapness: Meateor (#74070)

This commit is contained in:
Jacquerel
2023-04-01 03:40:29 +01:00
committed by GitHub
parent 17e23fed64
commit 966b8e5fd8
28 changed files with 1785 additions and 96 deletions
@@ -0,0 +1,112 @@
/// An ability which makes spikes come out of the ground towards your target
/datum/action/cooldown/chasing_spikes
name = "impaling tendril"
desc = "Send a spiked subterranean tendril chasing after your target."
button_icon = 'icons/mob/simple/meteor_heart.dmi'
button_icon_state = "spike"
cooldown_time = 10 SECONDS
click_to_activate = TRUE
/// Lazy list of references to spike trails
var/list/active_chasers
/datum/action/cooldown/chasing_spikes/Activate(atom/target)
. = ..()
playsound(owner, 'sound/magic/demon_attack1.ogg', vol = 100, vary = TRUE, pressure_affected = FALSE)
var/obj/effect/temp_visual/spike_chaser/chaser = new(get_turf(owner), target)
LAZYADD(active_chasers, WEAKREF(chaser))
RegisterSignal(chaser, COMSIG_PARENT_QDELETING, PROC_REF(on_chaser_destroyed))
/// Remove a spike trail from our list of active trails
/datum/action/cooldown/chasing_spikes/proc/on_chaser_destroyed(atom/chaser)
SIGNAL_HANDLER
LAZYREMOVE(active_chasers, WEAKREF(chaser))
// Clean up after ourselves
/datum/action/cooldown/chasing_spikes/Remove(mob/removed_from)
QDEL_LIST(active_chasers)
return ..()
/// An invisible effect which chases a target, spawning spikes every so often.
/obj/effect/temp_visual/spike_chaser
name = "spike chaser"
desc = "An invisible effect, how did you examine this?"
icon = 'icons/mob/silicon/cameramob.dmi'
icon_state = "marker"
duration = 15 SECONDS
invisibility = INVISIBILITY_ABSTRACT
/// Speed at which we chase target
var/move_speed = 3
/// What are we chasing?
var/datum/weakref/target
/// Handles chasing the target
var/datum/move_loop/movement
/obj/effect/temp_visual/spike_chaser/Initialize(mapload, atom/target)
. = ..()
if (!target)
return INITIALIZE_HINT_QDEL
AddElement(/datum/element/floor_loving)
AddComponent(/datum/component/spawner, spawn_types = list(/obj/effect/temp_visual/emerging_ground_spike), spawn_time = 0.5 SECONDS)
src.target = WEAKREF(target)
movement = SSmove_manager.move_towards(src, chasing = target, delay = move_speed, home = TRUE, timeout = duration, flags = MOVEMENT_LOOP_START_FAST)
RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(on_target_invalid))
if (isliving(target))
RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_target_invalid))
/// Destroy ourselves if the target is no longer valid
/obj/effect/temp_visual/spike_chaser/proc/on_target_invalid()
SIGNAL_HANDLER
qdel(src)
/obj/effect/temp_visual/spike_chaser/Destroy()
QDEL_NULL(movement)
return ..()
/// A spike comes out of the ground, dealing damage after a short delay
/obj/effect/temp_visual/emerging_ground_spike
name = "bone spike"
desc = "A sharp spur of bone erupting from the ground!"
icon = 'icons/mob/simple/meteor_heart.dmi'
icon_state = "spike"
duration = 1 SECONDS
/// Time until we hurt people stood on us
var/harm_delay = 0.3 SECONDS
/// Amount by which to vary our position on spawn
var/position_variance = 8
/// Damage to deal on impale
var/impale_damage = 15
/// Typecache of types of mobs not to damage
var/list/damage_blacklist_typecache = list(
/mob/living/basic/meteor_heart,
)
/// Weighted list of body zones to target while standing
var/static/list/standing_damage_zones = list(
BODY_ZONE_CHEST = 1,
BODY_ZONE_R_LEG = 3,
BODY_ZONE_L_LEG = 3,
)
/obj/effect/temp_visual/emerging_ground_spike/Initialize(mapload)
. = ..()
damage_blacklist_typecache = typecacheof(damage_blacklist_typecache)
pixel_x += rand(-position_variance, position_variance)
pixel_y += rand(-position_variance, position_variance)
addtimer(CALLBACK(src, PROC_REF(impale)), harm_delay, TIMER_DELETE_ME)
/// Stab people who are stood on us after a delay in the shins
/obj/effect/temp_visual/emerging_ground_spike/proc/impale()
if (!isturf(loc))
return
var/hit_someone = FALSE
for(var/mob/living/victim in loc)
if (is_type_in_typecache(victim, damage_blacklist_typecache))
continue
hit_someone = TRUE
var/target_zone = victim.resting ? BODY_ZONE_CHEST : pick_weight(standing_damage_zones)
victim.apply_damage(impale_damage, damagetype = BRUTE, def_zone = target_zone, sharpness = SHARP_POINTY)
if (hit_someone)
playsound(src, 'sound/weapons/slice.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE)
else
playsound(src, 'sound/misc/splort.ogg', vol = 25, vary = TRUE, pressure_affected = FALSE)
@@ -0,0 +1,36 @@
#define EYEBALL_BLINK_INTERVAL_MIN 10 SECONDS
#define EYEBALL_BLINK_INTERVAL_MAX 30 SECONDS
/// List of all the meteor eyeballs so we can gib them upon meteor death
GLOBAL_LIST_EMPTY(meteor_eyeballs)
/// Basically just an organic floor light
/obj/structure/meateor_fluff/eyeball
name = "beady eye"
desc = "An eyeball growing out of the ground, gross."
icon_state = "eyeball"
max_integrity = 15
layer = LOW_OBJ_LAYER
plane = FLOOR_PLANE
/obj/structure/meateor_fluff/eyeball/Initialize(mapload)
. = ..()
GLOB.meteor_eyeballs += src
set_light(l_range = 4, l_color = COLOR_VERY_SOFT_YELLOW)
blink()
/// Play a blinking animation and queue it again
/obj/structure/meateor_fluff/eyeball/proc/blink()
flick("eyeball_blink", src)
addtimer(CALLBACK(src, PROC_REF(blink)), rand(EYEBALL_BLINK_INTERVAL_MIN, EYEBALL_BLINK_INTERVAL_MAX), TIMER_DELETE_ME)
/obj/structure/meateor_fluff/eyeball/atom_destruction(damage_flag)
new /obj/effect/gibspawner/generic(loc)
return ..()
/obj/structure/meateor_fluff/eyeball/Destroy()
GLOB.meteor_eyeballs -= src
return ..()
#undef EYEBALL_BLINK_INTERVAL_MIN
#undef EYEBALL_BLINK_INTERVAL_MAX
@@ -0,0 +1,131 @@
#define HEARTBEAT_NORMAL (1.2 SECONDS)
#define HEARTBEAT_FAST (0.6 SECONDS)
#define HEARTBEAT_FRANTIC (0.4 SECONDS)
/mob/living/basic/meteor_heart
name = "meteor heart"
desc = "A pulsing lump of flesh and bone growing directly out of the ground."
icon = 'icons/mob/simple/meteor_heart.dmi'
icon_state = "heart"
icon_living = "heart"
mob_biotypes = MOB_ORGANIC
basic_mob_flags = DEL_ON_DEATH
mob_size = MOB_SIZE_HUGE
health = 600 // 15 PKA shots
maxHealth = 600
pressure_resistance = 200
response_help_continuous = "pets"
response_help_simple = "pet"
response_disarm_continuous = "gently pushes"
response_disarm_simple = "gently push"
faction = list()
ai_controller = /datum/ai_controller/basic_controller/meteor_heart
habitable_atmos = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
minimum_survivable_temperature = 0
maximum_survivable_temperature = 1500
combat_mode = TRUE
move_resist = INFINITY // This mob IS the floor
/// Action which sends a line of spikes chasing a player
var/datum/action/cooldown/chasing_spikes/spikes
/// Action which summons areas the player can't stand in
var/datum/action/cooldown/spine_traps/traps
/// Looping heartbeat sound
var/datum/looping_sound/heartbeat/soundloop
/mob/living/basic/meteor_heart/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_IMMOBILIZED, INNATE_TRAIT)
AddElement(/datum/element/death_drops, list(/obj/effect/temp_visual/meteor_heart_death))
AddElement(/datum/element/relay_attackers)
spikes = new(src)
spikes.Grant(src)
ai_controller.blackboard[BB_METEOR_HEART_GROUND_SPIKES] = WEAKREF(spikes)
traps = new(src)
traps.Grant(src)
ai_controller.blackboard[BB_METEOR_HEART_SPINE_TRAPS] = WEAKREF(traps)
ai_controller.set_ai_status(AI_STATUS_OFF)
RegisterSignal(src, COMSIG_MOB_ABILITY_FINISHED, PROC_REF(used_ability))
RegisterSignal(src, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(aggro))
for (var/obj/structure/meateor_fluff/body_part in view(5, src))
RegisterSignal(body_part, COMSIG_ATOM_DESTRUCTION, PROC_REF(aggro))
soundloop = new(src, start_immediately = FALSE)
soundloop.mid_length = HEARTBEAT_NORMAL
soundloop.pressure_affected = FALSE
soundloop.start()
/// Called when we get mad at something, either for attacking us or attacking the nearby area
/mob/living/basic/meteor_heart/proc/aggro()
if (ai_controller.ai_status == AI_STATUS_ON)
return
ai_controller.reset_ai_status()
if (!ai_controller.ai_status == AI_STATUS_ON)
return
icon_state = "heart_aggro"
soundloop.set_mid_length(HEARTBEAT_FAST)
/// Called when we stop being mad
/mob/living/basic/meteor_heart/proc/deaggro()
ai_controller.set_ai_status(AI_STATUS_OFF)
icon_state = "heart"
soundloop.set_mid_length(HEARTBEAT_NORMAL)
/// Animate when using certain abilities
/mob/living/basic/meteor_heart/proc/used_ability(mob/living/owner, datum/action/cooldown/ability)
SIGNAL_HANDLER
if (ability != spikes)
return
Shake(1, 0, 1.5 SECONDS)
/mob/living/basic/meteor_heart/Destroy()
QDEL_NULL(spikes)
QDEL_NULL(traps)
QDEL_NULL(soundloop)
return ..()
/// Dramatic death animation for the meteor heart mob
/obj/effect/temp_visual/meteor_heart_death
name = "meteor heart"
icon = 'icons/mob/simple/meteor_heart.dmi'
icon_state = "heart_dying"
desc = "You've killed this innocent asteroid, I hope you feel happy."
duration = 3 SECONDS
/// Looping heartbeat sound
var/datum/looping_sound/heartbeat/soundloop
/obj/effect/temp_visual/meteor_heart_death/Initialize(mapload)
. = ..()
playsound(src, 'sound/magic/demon_dies.ogg', vol = 100, vary = TRUE, pressure_affected = FALSE)
Shake(2, 0, 3 SECONDS)
addtimer(CALLBACK(src, PROC_REF(gib)), duration - 1, TIMER_DELETE_ME)
soundloop = new(src, start_immediately = FALSE)
soundloop.mid_length = HEARTBEAT_FRANTIC
soundloop.pressure_affected = FALSE
soundloop.start()
/obj/effect/temp_visual/meteor_heart_death/Destroy()
QDEL_NULL(soundloop)
return ..()
/// Make this place a mess
/obj/effect/temp_visual/meteor_heart_death/proc/gib()
playsound(loc, 'sound/effects/attackblob.ogg', vol = 100, vary = TRUE, pressure_affected = FALSE)
var/turf/my_turf = get_turf(src)
new /obj/effect/gibspawner/human(my_turf)
for (var/obj/structure/eyeball as anything in GLOB.meteor_eyeballs)
if (eyeball.z != src.z)
continue
addtimer(CALLBACK(eyeball, TYPE_PROC_REF(/atom/, take_damage), eyeball.max_integrity), rand(0.5 SECONDS, 2 SECONDS)) // pop!
for (var/mob/murderer in range(10, src))
if (!murderer.client || isspaceturf(get_turf(murderer)))
continue
shake_camera(murderer, duration = 2 SECONDS, strength = 2)
#undef HEARTBEAT_NORMAL
#undef HEARTBEAT_FAST
#undef HEARTBEAT_FRANTIC
@@ -0,0 +1,32 @@
/// A spellcasting AI which does not move
/datum/ai_controller/basic_controller/meteor_heart
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(),
BB_TARGETLESS_TIME = 0,
)
planning_subtrees = list(
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/targeted_mob_ability/ground_spikes,
/datum/ai_planning_subtree/use_mob_ability/spine_traps,
/datum/ai_planning_subtree/sleep_with_no_target/meteor_heart,
)
/datum/ai_planning_subtree/targeted_mob_ability/ground_spikes
ability_key = BB_METEOR_HEART_GROUND_SPIKES
finish_planning = FALSE
/datum/ai_planning_subtree/use_mob_ability/spine_traps
ability_key = BB_METEOR_HEART_SPINE_TRAPS
/// After enough time with no target, deaggro and change animation state
/datum/ai_planning_subtree/sleep_with_no_target/meteor_heart
sleep_behaviour = /datum/ai_behavior/sleep_after_targetless_time/meteor_heart
/datum/ai_behavior/sleep_after_targetless_time/meteor_heart
/datum/ai_behavior/sleep_after_targetless_time/meteor_heart/enter_sleep(datum/ai_controller/controller)
var/mob/living/basic/meteor_heart/heart = controller.pawn
if (!istype(heart))
return ..()
heart.deaggro()
@@ -0,0 +1,97 @@
/// Marks several areas with thrusting spines which damage and slow people
/datum/action/cooldown/spine_traps
name = "thrusting spines"
desc = "Mark several nearby areas with thrusting spines, which will spring up when disturbed."
button_icon = 'icons/mob/simple/meteor_heart.dmi'
button_icon_state = "spikes_stabbing"
cooldown_time = 15 SECONDS
/// Create zones at most this far away
var/range = 3
/// Don't create zones within this radius
var/min_range = 2
/// Number of zones to place
var/zones_to_create = 3
/datum/action/cooldown/spine_traps/Activate(atom/target)
. = ..()
playsound(owner, 'sound/magic/demon_consume.ogg', vol = 100, falloff_exponent = 2, vary = TRUE, pressure_affected = FALSE)
var/list/valid_turfs = list()
var/turf/our_turf = get_turf(owner)
for (var/turf/zone_turf in orange(range, our_turf))
if (!is_valid_turf(zone_turf) || get_dist(zone_turf, our_turf) < min_range)
continue
valid_turfs += zone_turf
var/created = 0
while(length(valid_turfs) && created < zones_to_create)
var/turf/place_turf = pick_n_take(valid_turfs)
var/list/covered_turfs = place_zone(place_turf)
valid_turfs -= covered_turfs
created++
/// Returns true if we can place a trap at the specified location
/datum/action/cooldown/spine_traps/proc/is_valid_turf(turf/target_turf)
return !target_turf.is_blocked_turf(exclude_mobs = TRUE) && !isspaceturf(target_turf) && !isopenspaceturf(target_turf)
/// Places a 3x3 area of spike traps around a central provided point, returns the list of now occupied turfs
/datum/action/cooldown/spine_traps/proc/place_zone(turf/target_turf)
var/list/used_turfs = list()
for (var/turf/zone_turf in range(1, target_turf))
if (!is_valid_turf(zone_turf))
continue
new /obj/effect/temp_visual/thrusting_spines(zone_turf)
used_turfs += zone_turf
return used_turfs
/obj/effect/temp_visual/thrusting_spines
icon = 'icons/mob/simple/meteor_heart.dmi'
icon_state = "spikes_idle"
desc = "Sharp spines lying in wait in the ground, you probably don't want to walk on those."
duration = 10 SECONDS
/// If this will trigger a trap when entered
var/active = FALSE
/// Damage to deal on activation
var/impale_damage = 10
/// Time between activations
COOLDOWN_DECLARE(thrust_delay)
/// Weighted list of body zones to target while standing
var/static/list/standing_damage_zones = list(
BODY_ZONE_CHEST = 1,
BODY_ZONE_R_LEG = 3,
BODY_ZONE_L_LEG = 3,
)
/obj/effect/temp_visual/thrusting_spines/Initialize(mapload)
. = ..()
flick("spikes_emerge", src)
addtimer(CALLBACK(src, PROC_REF(ready)), 1 SECONDS, TIMER_DELETE_ME)
addtimer(CALLBACK(src, PROC_REF(retract)), duration - (0.5 SECONDS), TIMER_DELETE_ME)
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
)
AddElement(/datum/element/connect_loc, loc_connections)
/// Called when we're ready to start impaling people
/obj/effect/temp_visual/thrusting_spines/proc/ready()
active = TRUE
/// Called when it is time to stop impaling people
/obj/effect/temp_visual/thrusting_spines/proc/retract()
active = FALSE
icon_state = "spikes_submerge"
/// Called when something enters our turf, if it is a non-flying mob then give it a stab
/obj/effect/temp_visual/thrusting_spines/proc/on_entered(datum/source, atom/movable/arrived)
if (!active || !isliving(arrived) || (arrived.movement_type & (FLYING | FLOATING)))
return
if (!COOLDOWN_FINISHED(src, thrust_delay))
return
COOLDOWN_START(src, thrust_delay, 0.7 SECONDS)
playsound(src, 'sound/weapons/pierce.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE)
var/mob/living/victim = arrived
flick("spikes_stabbing", src)
var/target_zone = victim.resting ? BODY_ZONE_CHEST : pick_weight(standing_damage_zones)
victim.apply_damage(impale_damage, damagetype = BRUTE, def_zone = target_zone, sharpness = SHARP_POINTY)