diff --git a/cev_eris.dme b/cev_eris.dme index 4918ced24a3..1cbd417431a 100644 --- a/cev_eris.dme +++ b/cev_eris.dme @@ -1954,6 +1954,8 @@ #include "code\modules\media\mediamanager.dm" #include "code\modules\mining\abandonedcrates.dm" #include "code\modules\mining\alloys.dm" +#include "code\modules\mining\ambush.dm" +#include "code\modules\mining\ambush_structures.dm" #include "code\modules\mining\coins.dm" #include "code\modules\mining\machine_processing.dm" #include "code\modules\mining\machine_stacking.dm" diff --git a/code/__HELPERS/matrices.dm b/code/__HELPERS/matrices.dm index 5e66c27b4ba..8e5554f1e7b 100644 --- a/code/__HELPERS/matrices.dm +++ b/code/__HELPERS/matrices.dm @@ -43,11 +43,11 @@ animate(transform = transforms[3], time = 0.2) animate(transform = transforms[4], time = 0.3) -/atom/proc/shake_animation(intensity = 8) +/atom/proc/shake_animation(intensity = 8, duration = 0.5 SECONDS) var/init_px = pixel_x var/shake_dir = pick(-1, 1) animate(src, transform=turn(matrix(), intensity*shake_dir), pixel_x=init_px + 2*shake_dir, time=1) - animate(transform=null, pixel_x=init_px, time=6, easing=ELASTIC_EASING) + animate(transform=null, pixel_x=init_px, time=duration, easing=ELASTIC_EASING) /// Perform a shake on an atom, resets its position afterwards /atom/proc/Shake(pixelshiftx = 2, pixelshifty = 2, duration = 2.5 SECONDS, shake_interval = 0.02 SECONDS) diff --git a/code/controllers/subsystems/processing/mobs.dm b/code/controllers/subsystems/processing/mobs.dm index d8ca0822258..2c18bb54eba 100644 --- a/code/controllers/subsystems/processing/mobs.dm +++ b/code/controllers/subsystems/processing/mobs.dm @@ -9,9 +9,12 @@ PROCESSING_SUBSYSTEM_DEF(mobs) var/list/mob_list var/list/mob_living_by_zlevel[][] + ///used by ambushcode to keep track of which mobs are currently involved in ambushes + var/list/ambushed_mobs /datum/controller/subsystem/processing/mobs/PreInit() mob_list = processing // Simply setups a more recognizable var name than "processing" + ambushed_mobs = new() MaxZChanged() /datum/controller/subsystem/processing/mobs/proc/MaxZChanged() diff --git a/code/modules/dungeons/procedural/deepmaint.dm b/code/modules/dungeons/procedural/deepmaint.dm index 42420eb16d9..3202bf37438 100644 --- a/code/modules/dungeons/procedural/deepmaint.dm +++ b/code/modules/dungeons/procedural/deepmaint.dm @@ -126,6 +126,8 @@ var/global/list/big_deepmaint_room_templates = list() /obj/procedural/jp_DungeonGenerator/deepmaint/proc/makeNiche(turf/T) var/list/nicheline = list() + //only 1 hard encounter per niche zone please + var/ambush_placed = FALSE for(var/i in list(NORTH,EAST,SOUTH,WEST)) //Checks range of 5 tiles in all 4 directions from the turf tile being passed switch(i) if(NORTH) @@ -146,6 +148,9 @@ var/global/list/big_deepmaint_room_templates = list() for(var/turf/W in nicheline) //Every turf in the path returned by findNicheTurfs has a 30% chance of becoming a random deepmaint machine if(prob(30)) new /obj/spawner/pack/deep_machine(W) + if(!ambush_placed && prob(1)) + new /obj/effect/ambush_snare + ambush_placed = TRUE for(var/turf/W in wall_line) //Every turf in the path returned by checkForWalls is turned into a floor tile, and has a 70% chance of becoming a random deepmaint machine if(locate(/obj/machinery/light/small/autoattach, W)) var/obj/machinery/light/small/autoattach/L = locate(/obj/machinery/light/small/autoattach, W) @@ -176,6 +181,7 @@ var/global/list/big_deepmaint_room_templates = list() var/niche_count = 20 var/try_count = niche_count * 7 //In case it somehow zig-zags all of the corridors and stucks in a loop var/trap_count = 100 + var/ambush_count = rand(4, 8)// Add a few ambushes in corridors to keep players on their toes var/list/path_turfs_copy = path_turfs.Copy() while(niche_count > 0 && try_count > 0) try_count = try_count - 1 @@ -188,6 +194,11 @@ var/global/list/big_deepmaint_room_templates = list() var/turf/N = pick(path_turfs_copy) path_turfs_copy -= N new /obj/spawner/traps(N) + while(ambush_count > 0) + ambush_count = ambush_count - 1 + var/turf/ourturf = pick(path_turfs_copy) + path_turfs_copy -= ourturf + new /obj/effect/ambush_snare(ourturf) for(var/turf/T in path_turfs) if(prob(30)) new /obj/effect/decal/cleanable/dirt(T) //Wanted to put rust on the floors in deep maint, but by god, the overlay looks like ASS diff --git a/code/modules/mining/ambush.dm b/code/modules/mining/ambush.dm new file mode 100644 index 00000000000..081a04da8a2 --- /dev/null +++ b/code/modules/mining/ambush.dm @@ -0,0 +1,311 @@ +#define AMBUSH_CLEANUP_DELAY 3 MINUTES //after the ambush ends, living mobs & any remaining burrows will be deleted this many minutes later. + +#define AMBUSH_SKIRMISH "ambush_skirmish" //burrows will immediately unload 1 wave of enemies, then close up +#define AMBUSH_SIEGE "ambush_siege" //burrows will periodically spawn mobs according to spawn_interval until the ambush ends. + +/datum/ambush_controller + + ///the mob that triggered the ambush + var/mob/living/ambushed_mob + /// The location of the ambush + var/turf/ambush_loc + /// A List of burrows tied to this controller + var/list/obj/structure/ambush_burrow/burrows = list() + /// A List of mobs tied to this controller + var/list/mob/living/carbon/superior_animal/ourmobs = list() + ///Allegedly used to keep processing() from runtiming + var/processing = TRUE + + ///A datum containing the behavior of our ambush + var/datum/ambush_type/our_datum + /// Number of burrows created since the start of the ambush + var/count = 0 + /// A Timestamp of the last created burrow + var/time_burrow = 0 + // A Timestamp of last spawn wave (in siege mode) + var/time_spawn = 0 + /// reference to time ambush began + var/start_time = 0 + ///Do ambush burrows appear around the mob who spawned them, even if they move? + var/following = TRUE + + ///Essentially local list of ambushed mobs, to compare ssmobs.ambushed_mobs to + var/list/our_ambushed_mobs + +/datum/ambush_controller/New(turf/trigger_location, new_ambushed_mob, ambush_datum) + our_datum = new ambush_datum() + ambushed_mob = new_ambushed_mob + + if(!trigger_location || !our_datum)//if they forgot to pass a wave datum or trigger location, explode + log_runtime("[src.type] is missing required new() arguments!") + QDEL_NULL(src) + return + + if(ambushed_mob) //get the starting location for our ambush + ambush_loc = ambush_follow_check() + if(!ambush_loc) //if no ambushed mob, just use the trigger location + ambush_loc = trigger_location + + var/list/our_ambushed_mobs = new() + //give mobs in range a warning they're about to be ambushed + for(var/mob/living/ourmob as anything in hearers(8, ambush_loc)) + //add the mob to ambush reference lists + our_ambushed_mobs += ourmob + SSmobs.ambushed_mobs += ourmob + ourmob.show_message(span_userdanger("You feel the ground tremble beneath you..."),2) + shake_camera(ourmob, 6, 0.5, 0.25) + playsound(ambush_loc, 'sound/effects/impacts/rumble4.ogg', 75, TRUE, extrarange = 4) + + start_time = world.time + START_PROCESSING(SSobj, src) + +/// gets the target's current loc if following is on or it hasn't been gotten before. +/datum/ambush_controller/proc/ambush_follow_check() + if((following || !ambush_loc) && ambushed_mob) + return ambushed_mob.loc + else//if no ambushed mob, just use existing loc + return ambush_loc + + +/datum/ambush_controller/Destroy() + processing = FALSE + //just in case the controller was qdel'd + for(var/mob/living/ambushee as anything in our_ambushed_mobs) + SSmobs.ambushed_mobs.Remove(ambushee) + for(var/obj/structure/ambush_burrow/burrow in burrows) // Unlink burrows and controller + qdel(burrow) + QDEL_NULL(our_datum) + . = ..() + +/datum/ambush_controller/Process() + // I'm too scared to test this -Sun + // Currently, STOP_PROCESSING does NOT instantly remove the object from processing queue + // This is a quick and dirty fix for runtime error spam caused by this + if(!processing) + return + + //our work here is done. End ambush + if(ambushed_mob.stat == DEAD || (world.time - start_time) - our_datum.setup_time >= our_datum.ambush_duration || count >= our_datum.spawn_cap) + stop() + + var/burrow_num = burrows.len + // Check if new burrows can be created + if((burrow_num < our_datum.max_burrows) && (world.time - time_burrow) > our_datum.burrow_interval) + time_burrow = world.time + for(var/mob/ourmob as anything in hearers(8, ambush_loc)) + shake_camera(ourmob, 6, 0.5, 0.25) + for(var/burrow in 1 to our_datum.burrow_number) + count++ + spawn_burrow() + + // if we're in siege mode, ambush controller handles spawns directly + if(our_datum.ambush_type != AMBUSH_SIEGE) + return + + if((world.time - start_time) <= our_datum.setup_time) + return + + // Check if a new spawn wave should occur + if((world.time - time_spawn) <= our_datum.spawn_interval) + return + + time_spawn = world.time + + for(var/obj/structure/ambush_burrow/ourburrow as anything in burrows) + if(!get_turf(ourburrow)) // If the burrow is in nullspace for some reason + burrows -= ourburrow // Remove it from the pool of burrows + continue + ourburrow.spawn_mobs() + + +///locates an appropriate turf to spawn a burrow, then creates it +/datum/ambush_controller/proc/spawn_burrow() + ambush_loc = ambush_follow_check() + // Spawn burrow randomly in a donut around our ambush turf + var/radius = our_datum.burrow_spawn_range + var/turf/burrow_turf + while(radius > 2) + burrow_turf = pick(getcircle(ambush_loc, radius)) + if(!istype(burrow_turf)) // Try again with a smaller circle + radius-- + continue + break + if(!istype(burrow_turf)) // Something wrong is happening + log_and_message_admins("Ambush controller failed to create a new burrow around ([ambush_loc.x], [ambush_loc.y], [ambush_loc.z]).") + return + + //if we are in a closed space or target is not visible, move towards the spawn turf + while(ambush_loc && check_density_no_mobs(burrow_turf) && burrow_turf != ambush_loc || !can_see(burrow_turf, ambush_loc)) + burrow_turf = get_step(burrow_turf, get_dir(burrow_turf, ambush_loc)) + // If we end up on top of the trigger loc, just spawn next to it + if(burrow_turf == ambush_loc) + burrow_turf = get_step(ambush_loc, pick(GLOB.cardinal)) + + burrow_turf.shake_animation(14)//HEY! Pay attention to this spot + burrows += new /obj/structure/ambush_burrow(burrow_turf, src, our_datum) // Spawn burrow at final location + +///ends the ambush. Should preferentially be called before cleanup or a qdel +/datum/ambush_controller/proc/stop() + // Disable processing + processing = FALSE + //give mobs in range an indicate that it's over + for(var/mob/ourmob as anything in hearers(8, ambush_loc)) + ourmob.show_message(span_danger("The shaking in the ground finally subsides."),2) + for(var/obj/structure/ambush_burrow/burrow in burrows) //visibly collapse burrows to show players it's over + burrow.crumble() + //allow mobs in the original ambush range to once again trigger ambushes + for(var/mob/living/ambushee as anything in our_ambushed_mobs) + SSmobs.ambushed_mobs.Remove(ambushee) + // Clean up controller and all remaining objects after given delay + addtimer(CALLBACK(src, PROC_REF(cleanup)), AMBUSH_CLEANUP_DELAY) + +///properly cleans up the controller. Final step of the deletion chain +/datum/ambush_controller/proc/cleanup() + // Delete any remaining burrows + for(var/obj/structure/ambush_burrow/burrow as anything in burrows) + qdel(burrow) + + // Delete mobs + for(var/mob/living/carbon/superior_animal/mob as anything in ourmobs) + if(mob.stat == DEAD) + continue + qdel(mob) + + qdel(src) + +//inherited from old golem controller. Why is it defined here? Good question +///check that determines if a turf is a wall or already holding a mob +/datum/ambush_controller/proc/check_density_no_mobs(turf/F) + if(F.density) + return TRUE + for(var/atom/A in F) + if(A.density && !(A.flags & ON_BORDER) && !ismob(A)) + return TRUE + return FALSE + + +///contains the code that manages an ambush controller's behavior +/datum/ambush_type + //ambush behavior vars + /// Determines the logic burrows follow when spawning mobs + var/ambush_type = AMBUSH_SKIRMISH + /// Once this time passes, ambush ends + var/ambush_duration = 20 SECONDS + /// If set, ambush ends once this many burrows have spawned + var/spawn_cap = 8 + /// Amnt. of prep time given between when burrows first appear & mobs start spawning + var/setup_time = 3 SECONDS + + //burrow vars + /// If set, total number of burrows that can exist at any single time + var/max_burrows = 4 + /// the number of burrows to spawn in a single wave + var/burrow_number = 2 + /// Number of seconds that pass between each new burrow spawn wave + var/burrow_interval = 10 SECONDS + ///the starting range at which the ambush controller will try to place burrows + var/burrow_spawn_range = 6 + + //mob vars + /// Number of mobs spawned by each burrow on spawn event + var/mob_spawn = 3 + /// Probability of a mob being a special one instead of a normal one + var/special_probability = 35 + /// Types of mobs normally spawned by the ambush + var/list/normal_types = list(/mob/living/carbon/superior_animal/roach, + /mob/living/carbon/superior_animal/roach/hunter, + /mob/living/carbon/superior_animal/roach/support) + /// Types of unusual mobs to be passed to the spawn pool according to special_probability + var/list/special_types = list(/mob/living/carbon/superior_animal/roach/fuhrer, + /mob/living/carbon/superior_animal/roach/nanite, + /mob/living/carbon/superior_animal/roach/tank, + /mob/living/carbon/superior_animal/roach/toxic) + + ///Sound played before the ambush triggers + //var/ambush_sound = 'sound/effects/impacts/rumble4.ogg' + + //siege-specific vars + /// Number of seconds that pass between spawn events of burrows - if siege mode is enabled. + var/spawn_interval = 15 SECONDS + + +// AMBUSH TYPE DEFINES + +/datum/ambush_type/golem + ambush_duration = 30 SECONDS + spawn_cap = 12 + + max_burrows = 6 + burrow_number = 3 + + special_probability = 25 + + normal_types = list(/mob/living/carbon/superior_animal/golem/coal, + /mob/living/carbon/superior_animal/golem/iron) + + special_types = list(/mob/living/carbon/superior_animal/golem/silver, + /mob/living/carbon/superior_animal/golem/silver/enhanced, + /mob/living/carbon/superior_animal/golem/gold, + /mob/living/carbon/superior_animal/golem/plasma, + /mob/living/carbon/superior_animal/golem/ansible, + /mob/living/carbon/superior_animal/golem/coal/enhanced, + /mob/living/carbon/superior_animal/golem/diamond, + /mob/living/carbon/superior_animal/golem/uranium) + + +/datum/ambush_type/golem/beginner + special_probability = 0 + + +/datum/ambush_type/golem/novice + special_probability = 25 + + special_types = list(/mob/living/carbon/superior_animal/golem/silver) + + +/datum/ambush_type/golem/adept + normal_types = list(/mob/living/carbon/superior_animal/golem/coal, + /mob/living/carbon/superior_animal/golem/iron, + /mob/living/carbon/superior_animal/golem/silver) + + special_types = list(/mob/living/carbon/superior_animal/golem/platinum, + /mob/living/carbon/superior_animal/golem/coal/enhanced, + /mob/living/carbon/superior_animal/golem/plasma, + /mob/living/carbon/superior_animal/golem/uranium) + + +/datum/ambush_type/golem/experienced + normal_types = list(/mob/living/carbon/superior_animal/golem/coal/enhanced, + /mob/living/carbon/superior_animal/golem/iron, + /mob/living/carbon/superior_animal/golem/silver) + + special_types = list(/mob/living/carbon/superior_animal/golem/platinum, + /mob/living/carbon/superior_animal/golem/plasma, + /mob/living/carbon/superior_animal/golem/uranium) + + +/datum/ambush_type/golem/expert + special_probability = 30 + normal_types = list(/mob/living/carbon/superior_animal/golem/coal/enhanced, + /mob/living/carbon/superior_animal/golem/iron, + /mob/living/carbon/superior_animal/golem/uranium, + /mob/living/carbon/superior_animal/golem/platinum, + /mob/living/carbon/superior_animal/golem/silver/enhanced) + + special_types = list(/mob/living/carbon/superior_animal/golem/plasma, + /mob/living/carbon/superior_animal/golem/gold) + + +/datum/ambush_type/golem/nightmare + special_probability = 35 + normal_types = list(/mob/living/carbon/superior_animal/golem/coal/enhanced, + /mob/living/carbon/superior_animal/golem/iron, + /mob/living/carbon/superior_animal/golem/uranium, + /mob/living/carbon/superior_animal/golem/platinum, + /mob/living/carbon/superior_animal/golem/silver/enhanced) + + special_types = list(/mob/living/carbon/superior_animal/golem/plasma, + /mob/living/carbon/superior_animal/golem/ansible, + /mob/living/carbon/superior_animal/golem/diamond, + /mob/living/carbon/superior_animal/golem/gold) + diff --git a/code/modules/mining/ambush_structures.dm b/code/modules/mining/ambush_structures.dm new file mode 100644 index 00000000000..ee08cead2f2 --- /dev/null +++ b/code/modules/mining/ambush_structures.dm @@ -0,0 +1,135 @@ +/obj/structure/ambush_burrow + name = "burrow" + icon = 'icons/obj/burrows.dmi' + icon_state = "cracks_animated" + desc = "A pile of debris that regularly pulses and shifts. Something is coming..." + density = TRUE + anchored = TRUE + + maxHealth = 50 + health = 50 + explosion_coverage = 0.3 + var/datum/ambush_controller/controller + var/datum/ambush_type/our_ambush + +/obj/structure/ambush_burrow/New(loc, parent, ambush_datum) + ..() + controller = parent // Link burrow with controller + our_ambush = ambush_datum //Give burrow datum + if(!controller || !our_ambush) + log_runtime("[src.type] was spawned without required arguments!") + qdel(src) + + shake_animation(duration = our_ambush.setup_time) + if(our_ambush.ambush_type == AMBUSH_SKIRMISH) + addtimer(CALLBACK(src, PROC_REF(spawn_mobs)), our_ambush.setup_time) + +///now we actually do the heavy lifting. +/obj/structure/ambush_burrow/proc/spawn_mobs() + + shake_animation(intensity = 14)//shake again for good measure + playsound(src, pick(crumble_sound), 40) + icon_state = "maint_hole" + //get potential directions to place a mob + var/list/possible_directions = GLOB.cardinal.Copy() + var/mobs_spawned = 0 + var/probability = our_ambush.special_probability + + while(mobs_spawned < our_ambush.mob_spawn && possible_directions.len) + var/turf/possible_T = get_step(loc, pick_n_take(possible_directions)) + new /obj/effect/decal/cleanable/rubble(possible_T) + var/mobtype + if(prob(probability))//if prob allows, pick a special mob + mobtype = pick(our_ambush.special_types) + //proba = max(0, probability - 5) // Decreasing probability to avoid mass spam of special mobs + else + mobtype = pick(our_ambush.normal_types) // Pick a normal mob + if(!controller.check_density_no_mobs(possible_T)) + new mobtype(possible_T) // Spawn mob at free location + else + new mobtype(loc) + mobs_spawned++ + if(our_ambush.ambush_type == AMBUSH_SKIRMISH) + addtimer(CALLBACK(src, PROC_REF(crumble)), 2 SECONDS) + +///visibly indicates to players that this burrow is out of commission. Also preps for deletion +/obj/structure/ambush_burrow/proc/crumble() + icon_state = "maint_hole_collapsed" + desc = "It's filled with loose debris. As you watch, it begins to crumble away..." + QDEL_IN(src, 3 SECONDS) + +/obj/structure/ambush_burrow/Destroy() + visible_message(span_danger("\The [src] crumbles away!")) + new /obj/effect/decal/cleanable/rubble(src.loc) + if(controller) + controller.burrows -= src + controller = null + ..() + +/obj/structure/ambush_burrow/attack_generic(mob/user, damage) + user.do_attack_animation(src) + visible_message(span_danger("\The [user] smashes \the [src]!")) + take_damage(damage) + user.setClickCooldown(DEFAULT_ATTACK_COOLDOWN * 1.5) + +/obj/structure/ambush_burrow/attackby(obj/item/I, mob/user) + if (user.a_intent == I_HURT && user.Adjacent(src)) + if(!(I.flags & NOBLUDGEON)) + user.do_attack_animation(src) + var/damage = I.force * I.structure_damage_factor + var/volume = min(damage * 3.5, 15) + if (I.hitsound) + playsound(src, I.hitsound, volume, 1, -1) + visible_message(span_danger("[src] has been hit by [user] with [I].")) + take_damage(damage) + user.setClickCooldown(DEFAULT_ATTACK_COOLDOWN * 1.5) + return TRUE + +/obj/structure/ambush_burrow/bullet_act(obj/item/projectile/Proj) + ..() + // Bullet not really efficient against a pile of debris + take_damage(Proj.get_structure_damage() * 0.25) + +/obj/structure/ambush_burrow/take_damage(damage) + . = health - damage < 0 ? damage - (damage - health) : damage + . *= explosion_coverage + health = min(max(health - damage, 0), maxHealth) + if(health == 0) + qdel(src) + return + +///lies in wait until a human mob enters its watched turfs, then creates an ambush event tied to them +/obj/effect/ambush_snare + name = "Ambush Trigger" + icon = 'icons/misc/landmarks.dmi' + icon_state = "trap_red" + alpha = 120 + anchored = TRUE + unacidable = 1 + simulated = FALSE + invisibility = 101 + ///the proximity trigger used to detect mobs in nearby turfs + var/datum/proximity_trigger/square/snare + ///the type of ambush deployed by this snare + var/datum/ambush_type/our_ambush_type = /datum/ambush_type + ///the range of turfs detected by this snare + var/triprange = 7 + +/obj/effect/ambush_snare/New() + ..() + snare = new(src, /obj/effect/ambush_snare/proc/trip_snare, /obj/effect/ambush_snare/proc/trip_snare, triprange, proc_owner = src) + snare.register_turfs() + +///triggers an ambush on the target, if they're human +/obj/effect/ambush_snare/proc/trip_snare(sucker) + if(!isturf(loc) || !ishuman(sucker) || !can_see(src, sucker))//let's keep this on visible players for now + return + if(sucker in SSmobs.ambushed_mobs)//don't spam ambushes if someone walks into multiple snares + return + new /datum/ambush_controller(loc, sucker, our_ambush_type) + qdel(src) + +//gc our prox trigger +/obj/effect/ambush_snare/Destroy() + QDEL_NULL(snare) + . = ..() diff --git a/icons/obj/burrows.dmi b/icons/obj/burrows.dmi index 57cc6207d5b..5d657dcaaa8 100644 Binary files a/icons/obj/burrows.dmi and b/icons/obj/burrows.dmi differ