mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-07-20 20:45:28 +01:00
March into Mapness: Meateor (#74070)
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user