From e4f04afb398eada14149d99d84ac25ab7baf955e Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Tue, 10 Nov 2026 20:00:55 -0800 Subject: [PATCH 01/20] Stop time on a lone Reflex Surge --- tgstation.dme | 1 + voidcrew/modules/cyberware/cyberware_anim.dm | 17 +- voidcrew/modules/cyberware/ware_legend.dm | 8 + voidcrew/modules/cyberware/ware_line_sandy.dm | 2 + .../modules/cyberware/ware_sandy_timestop.dm | 277 ++++++++++++++++++ voidcrew/modules/unit_tests/_unit_tests.dm | 1 + .../unit_tests/voidcrew_cyberware_timestop.dm | 82 ++++++ 7 files changed, 385 insertions(+), 3 deletions(-) create mode 100644 voidcrew/modules/cyberware/ware_sandy_timestop.dm create mode 100644 voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm diff --git a/tgstation.dme b/tgstation.dme index 51a7c2bd84a..2bf19a31c90 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7225,6 +7225,7 @@ #include "voidcrew\modules\cyberware\ware_pro_combat.dm" #include "voidcrew\modules\cyberware\ware_pro_utility.dm" #include "voidcrew\modules\cyberware\ware_registry.dm" +#include "voidcrew\modules\cyberware\ware_sandy_timestop.dm" #include "voidcrew\modules\cyberware\ware_second_wind.dm" #include "voidcrew\modules\cyberware\ware_street.dm" #include "voidcrew\modules\cyberware\ware_street_passives.dm" diff --git a/voidcrew/modules/cyberware/cyberware_anim.dm b/voidcrew/modules/cyberware/cyberware_anim.dm index 0284e0a0c88..4bb4691941b 100644 --- a/voidcrew/modules/cyberware/cyberware_anim.dm +++ b/voidcrew/modules/cyberware/cyberware_anim.dm @@ -641,16 +641,20 @@ var/ring_alpha = 235 /// How long the ring takes to roll out and fade. var/ring_time = 0.65 SECONDS + /// How far the ring rolls out when it stops time (ware_sandy_timestop.dm). + var/stop_ring_scale = 7.5 /obj/item/organ/cyberimp/cyberware/cascade/milspec ring_scale = 2.7 ring_alpha = 190 ring_time = 0.55 SECONDS + stop_ring_scale = 6.5 /obj/item/organ/cyberimp/cyberware/cascade/street ring_scale = 2.2 ring_alpha = 140 ring_time = 0.45 SECONDS + stop_ring_scale = 5.5 /obj/item/organ/cyberimp/cyberware/cascade/cyberware_worn_key() return "[..()]-[node_look]" @@ -735,8 +739,13 @@ /obj/item/organ/cyberimp/cyberware/cascade/proc/roll_ring() ring_timer = null var/turf/here = get_turf(owner) - if(here) - new /obj/effect/temp_visual/cyberware_time_ring(here, ring_scale, ring_alpha, ring_time) + if(!here) + return + var/datum/status_effect/cyberware_cascade_window/window = owner?.has_status_effect(/datum/status_effect/cyberware_cascade_window) + if(window?.time_stop) // a green ring that rolls out past the edge of the screen + new /obj/effect/temp_visual/cyberware_time_ring(here, stop_ring_scale, ring_alpha, ring_time, TRUE) + return + new /obj/effect/temp_visual/cyberware_time_ring(here, ring_scale, ring_alpha, ring_time) /// The crash: nodes die hips to neck and stay dark. /obj/item/organ/cyberimp/cyberware/cascade/proc/play_crash() @@ -784,9 +793,11 @@ randomdir = FALSE duration = 0.65 SECONDS -/obj/effect/temp_visual/cyberware_time_ring/Initialize(mapload, max_scale = 3.3, peak_alpha = 235, ring_time = 0.65 SECONDS) +/obj/effect/temp_visual/cyberware_time_ring/Initialize(mapload, max_scale = 3.3, peak_alpha = 235, ring_time = 0.65 SECONDS, green = FALSE) duration = ring_time . = ..() + if(green) + color = cyberware_time_stop_green() add_overlay(emissive_appearance(icon, icon_state, src)) transform = matrix().Scale(0.2) alpha = peak_alpha diff --git a/voidcrew/modules/cyberware/ware_legend.dm b/voidcrew/modules/cyberware/ware_legend.dm index 58e909208b7..b5bca636523 100644 --- a/voidcrew/modules/cyberware/ware_legend.dm +++ b/voidcrew/modules/cyberware/ware_legend.dm @@ -90,6 +90,8 @@ var/window_alert = /atom/movable/screen/alert/status_effect/cyberware_cascade /// What the bearer reads on activation. var/online_message = "CASCADE ONLINE." + /// How long the world stops when no other player is near (ware_sandy_timestop.dm). + var/stop_duration = 4 SECONDS /obj/item/organ/cyberimp/cyberware/cascade/on_mob_remove(mob/living/carbon/organ_owner, special = FALSE, movement_flags) . = ..() @@ -188,6 +190,7 @@ online_message = lattice.online_message crash_duration = lattice.crash_duration crash_stamina = lattice.crash_stamina + stop_duration = lattice.stop_duration return ..() /datum/status_effect/cyberware_cascade_window/on_apply() @@ -205,9 +208,11 @@ ) owner.balloon_alert(owner, "cascade online") cyberware_cascade_window_fx(owner) + try_time_stop() // ware_sandy_timestop.dm return TRUE /datum/status_effect/cyberware_cascade_window/on_remove() + end_time_stop() owner.remove_movespeed_modifier(speed_type) owner.remove_movespeed_mod_immunities(id, /datum/movespeed_modifier/reagent) cyberware_unregister_dodge_source(owner, id) @@ -240,6 +245,9 @@ steps_taken++ if(steps_taken % afterimage_interval) // cheaper chrome leaves a sparser trail return + if(time_stop) + stopped_afterimage(old_loc) + return new /obj/effect/temp_visual/decoy/fading/cascade(old_loc, owner, trail_hue, afterimage_alpha) trail_hue = (trail_hue + CYBERWARE_CASCADE_HUE_STEP) % 360 diff --git a/voidcrew/modules/cyberware/ware_line_sandy.dm b/voidcrew/modules/cyberware/ware_line_sandy.dm index 303ff54f346..8cb63930cbe 100644 --- a/voidcrew/modules/cyberware/ware_line_sandy.dm +++ b/voidcrew/modules/cyberware/ware_line_sandy.dm @@ -52,6 +52,7 @@ afterimage_alpha = 120 window_alert = /atom/movable/screen/alert/status_effect/cyberware_cascade/aftermarket online_message = "REFLEX RIG ONLINE." + stop_duration = 2 SECONDS /datum/action/cooldown/cyberware/cascade_surge/street name = "Reflex Surge" @@ -89,6 +90,7 @@ afterimage_alpha = 160 window_alert = /atom/movable/screen/alert/status_effect/cyberware_cascade/aftermarket online_message = "LATTICE ONLINE." + stop_duration = 3 SECONDS /datum/action/cooldown/cyberware/cascade_surge/milspec name = "Reflex Surge" diff --git a/voidcrew/modules/cyberware/ware_sandy_timestop.dm b/voidcrew/modules/cyberware/ware_sandy_timestop.dm new file mode 100644 index 00000000000..fb264d05af5 --- /dev/null +++ b/voidcrew/modules/cyberware/ware_sandy_timestop.dm @@ -0,0 +1,277 @@ +/** + * # The Cascade line's time stop + * + * When a Cascade-line surge (ware_legend.dm, ware_line_sandy.dm) opens and no other player is within + * SANDY_STOP_WITNESS_RANGE tiles, the world stops for the lattice's stop_duration (street 2 s, service 3 s, + * Cascade 4 s). The rest of the window then runs as normal, then the crash. With another player near, the + * surge is the plain one. Only a player's surge stops time: an NPC criminal's lattice never does. + * + * Built on tg's own time stop (code/datums/proximity_monitor/fields/timestop.dm). The field is hosted on + * the user, so it follows them; anything it leaves behind stays frozen until it ends. + * + * No shortcuts on people: frozen mobs are held by tg's Stun, re-applied every tick by the tg field, and + * anything with an AI controller also gets TRAIT_AI_PAUSED (Voidcrew's human NPCs run a basic controller on a + * carbon body, which the tg field's basic-mob branch misses). Frozen bodies still take damage, wounds and + * bleeding through tg as normal. Bosses (a finisher user, or a boss-rank human NPC) are held for + * SANDY_STOP_BOSS_HOLD only, then let go while the field carries on. + * + * The field re-checks for players every tick and ends at once if one turns up. + * + * Look: frozen things get a grey-green tint instead of tg's inversion. Floor items, blood and the user are + * never frozen, so they keep their colour. The user's own shots stop about a tile out, keep their colour, + * and fly on when time restarts. + */ + +/// Another player within this many tiles of the user means no time stop. +#define SANDY_STOP_WITNESS_RANGE 10 +/// Radius of the frozen field around the user (a widescreen view is 9 tiles to each side). +#define SANDY_STOP_FIELD_RANGE 9 +/// How long a boss stays frozen before it shrugs the field off. +#define SANDY_STOP_BOSS_HOLD (1 SECONDS) +/// How far, in pixels, the user's own shots fly before they hang. +#define SANDY_STOP_SHOT_HANG 32 +/// Trait source for everything the time stop applies. +#define SANDY_STOP_TRAIT "sandevistan_time_stop" +/// Filter name of the user's edge glow. +#define SANDY_STOP_OUTLINE "sandy_time_stop_outline" + +/// Whether a player other than `user` is within `range` tiles: anyone with a client, or a living body with a ckey whose player has disconnected. +/proc/cyberware_time_stop_witnessed(mob/living/user, range = SANDY_STOP_WITNESS_RANGE) + var/turf/here = get_turf(user) + if(!here) + return TRUE + for(var/atom/movable/found as anything in SSspatial_grid.orthogonal_range_search(here, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS, range)) + if(found == user) + continue + if(isliving(found)) + return TRUE + if(ismob(found)) // ghosts and camera eyes don't count + continue + // a locker or a mech with a player in it + for(var/mob/living/inside in found.get_all_contents()) + if(inside != user && inside.client) + return TRUE + for(var/mob/living/body in SSspatial_grid.orthogonal_range_search(here, SPATIAL_GRID_CONTENTS_TYPE_HEARING, range)) + if(body != user && body.ckey) + return TRUE + return FALSE + +/// Whether `victim` is a boss: anything with a finisher, or a boss-rank human NPC. +/proc/cyberware_time_stop_boss(mob/living/victim) + if(victim.GetComponent(/datum/component/hv_finisher_user)) + return TRUE + if(istype(victim, /mob/living/carbon/human/npc)) + var/mob/living/carbon/human/npc/person = victim + return person.npc_rank >= HUMAN_NPC_RANK_BOSS + return FALSE + +/// The grey-green wash on frozen things. +/proc/cyberware_time_stop_tint() + var/static/list/tint = list(0.26,0.28,0.27, 0.52,0.56,0.54, 0.10,0.11,0.105, 0.02,0.05,0.04) + return tint + +/// Flattens a sprite to luminance and paints it the time stop's green (#5AFF96), lifted so dark parts still glow. +/proc/cyberware_time_stop_green() + var/static/list/green + if(!green) + var/red = 90 / 255 + var/grn = 1 + var/blue = 150 / 255 + var/gain = 0.8 + var/lift = 0.5 + green = list( + 0.3 * gain * red, 0.3 * gain * grn, 0.3 * gain * blue, + 0.59 * gain * red, 0.59 * gain * grn, 0.59 * gain * blue, + 0.11 * gain * red, 0.11 * gain * grn, 0.11 * gain * blue, + lift * red, lift * grn, lift * blue, + ) + return green + +// ========================================================================= +// THE STOP +// ========================================================================= + +/// Lives as long as the stop, holds the field on the user, and ends the stop when it's deleted. +/obj/effect/timestop/sandevistan + name = "stopped moment" + desc = null + hidden = TRUE + alpha = 0 + freezerange = SANDY_STOP_FIELD_RANGE + /// Whose stop this is. The field is hosted on them. + var/mob/living/user + +/// `immune_atoms` must be list(user). +/obj/effect/timestop/sandevistan/Initialize(mapload, radius, time, list/immune_atoms, start = TRUE, silent = TRUE) + if(length(immune_atoms)) + user = immune_atoms[1] + return ..(mapload, radius, time, immune_atoms, start, TRUE) + +/obj/effect/timestop/sandevistan/Destroy() + user = null + return ..() + +/obj/effect/timestop/sandevistan/timestop() + target = get_turf(src) + QDEL_IN(src, duration) + if(QDELETED(user)) + return + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = new(user, freezerange, TRUE, immune, antimagic_flags, FALSE) + field.stopper = src + chronofield = field + +/datum/proximity_monitor/advanced/timestop/sandevistan + /// The effect that owns this field. Deleting it ends the stop. + var/obj/effect/timestop/sandevistan/stopper + /// Frozen bosses, to the world.time they shrug the field off. + var/list/boss_holds = list() + /// Bosses that already shrugged it off. The field leaves them be. + var/list/released = list() + /// The user's own shots, hanging in the air. + var/list/hung_shots = list() + +/datum/proximity_monitor/advanced/timestop/sandevistan/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, list/immune, antimagic_flags, channelled) + . = ..() + RegisterSignal(host, COMSIG_PROJECTILE_FIRER_BEFORE_FIRE, PROC_REF(on_user_fire)) + +/datum/proximity_monitor/advanced/timestop/sandevistan/Destroy() + if(host) + UnregisterSignal(host, COMSIG_PROJECTILE_FIRER_BEFORE_FIRE) + for(var/obj/projectile/shot as anything in hung_shots) + UnregisterSignal(shot, COMSIG_QDELETING) + shot.paused = FALSE + hung_shots.Cut() + stopper = null + . = ..() + released.Cut() + boss_holds.Cut() + +/datum/proximity_monitor/advanced/timestop/sandevistan/freeze_atom(atom/movable/A) + if(released[A]) + return FALSE + if(isprojectile(A)) + var/obj/projectile/shot = A + if(shot.firer && immune[shot.firer]) // the user's own shots hang on their own clock + return FALSE + return ..() + +/datum/proximity_monitor/advanced/timestop/sandevistan/freeze_mob(mob/living/victim) + . = ..() + if(victim.ai_controller) + ADD_TRAIT(victim, TRAIT_AI_PAUSED, SANDY_STOP_TRAIT) + if(cyberware_time_stop_boss(victim)) + boss_holds[victim] = world.time + SANDY_STOP_BOSS_HOLD + +/datum/proximity_monitor/advanced/timestop/sandevistan/unfreeze_mob(mob/living/victim) + REMOVE_TRAIT(victim, TRAIT_AI_PAUSED, SANDY_STOP_TRAIT) + boss_holds -= victim + return ..() + +/datum/proximity_monitor/advanced/timestop/sandevistan/process() + if(QDELETED(host) || cyberware_time_stop_witnessed(host)) + if(!QDELETED(stopper)) + qdel(stopper) // takes this field with it + return + for(var/mob/living/boss as anything in boss_holds.Copy()) + if(world.time < boss_holds[boss]) + continue + unfreeze_atom(boss) + released[boss] = TRUE + return ..() + +/datum/proximity_monitor/advanced/timestop/sandevistan/into_the_negative_zone(atom/A) + A.add_atom_colour(cyberware_time_stop_tint(), TEMPORARY_COLOUR_PRIORITY) + +/// Signal proc for [COMSIG_PROJECTILE_FIRER_BEFORE_FIRE] on the user: watch the shot leave the barrel. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/on_user_fire(datum/source, obj/projectile/shot, atom/fired_from, atom/original) + SIGNAL_HANDLER + RegisterSignal(shot, COMSIG_PROJECTILE_FIRE, PROC_REF(hang_shot)) + +/// Signal proc for [COMSIG_PROJECTILE_FIRE]: the shot flies a tile, then hangs until time restarts. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/hang_shot(obj/projectile/shot) + SIGNAL_HANDLER + UnregisterSignal(shot, COMSIG_PROJECTILE_FIRE) + if(QDELETED(shot) || shot.deletion_queued) + return + // A hitscan beam takes its first step itself once paused (process_hitscan) and stops there. + if(!shot.hitscan) + shot.process_movement(SANDY_STOP_SHOT_HANG) + if(QDELETED(shot) || shot.deletion_queued) + return + shot.paused = TRUE + hung_shots += shot + RegisterSignal(shot, COMSIG_QDELETING, PROC_REF(shot_gone)) + +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/shot_gone(obj/projectile/shot) + SIGNAL_HANDLER + hung_shots -= shot + +// ========================================================================= +// THE WINDOW'S SIDE +// ========================================================================= + +/datum/status_effect/cyberware_cascade_window + /// How long the world stops if nobody else is near when the window opens. Copied from the lattice. + var/stop_duration = 4 SECONDS + /// The running stop, if time is stopped. + var/obj/effect/timestop/sandevistan/time_stop + +/// Stops time if the bearer is a player and nobody else is near. Called as the window opens. +/datum/status_effect/cyberware_cascade_window/proc/try_time_stop() + if(stop_duration <= 0 || !owner.ckey || time_stop) + return FALSE + if(cyberware_time_stop_witnessed(owner)) + return FALSE + var/turf/here = get_turf(owner) + if(!here) + return FALSE + time_stop = new(here, SANDY_STOP_FIELD_RANGE, stop_duration, list(owner)) + if(QDELETED(time_stop)) + time_stop = null + return FALSE + RegisterSignal(time_stop, COMSIG_QDELETING, PROC_REF(time_stop_over)) + owner.remove_client_colour(id) // the user sees the frozen world as it is + owner.add_filter(SANDY_STOP_OUTLINE, 2, outline_filter(1, "#5AFF96D2")) + return TRUE + +/// Signal proc for [COMSIG_QDELETING] on the stop: time restarts and the rest of the window runs as normal. +/datum/status_effect/cyberware_cascade_window/proc/time_stop_over(datum/source) + SIGNAL_HANDLER + time_stop = null + if(QDELETED(owner)) + return + owner.remove_filter(SANDY_STOP_OUTLINE) + owner.add_client_colour(colour_type, id) + +/// Ends the stop early (the window is closing). +/datum/status_effect/cyberware_cascade_window/proc/end_time_stop() + if(!time_stop) + return + UnregisterSignal(time_stop, COMSIG_QDELETING) + QDEL_NULL(time_stop) + if(!QDELETED(owner)) + owner.remove_filter(SANDY_STOP_OUTLINE) + +/// One afterimage while time is stopped: green, gone in 0.4 s. It sits on the user's own tile, shifted back to +/// where they were, so it sorts on their row and draws behind them in every facing. +/datum/status_effect/cyberware_cascade_window/proc/stopped_afterimage(turf/old_loc) + var/turf/here = get_turf(owner) + if(!here) + return + var/obj/effect/temp_visual/decoy/fading/cascade/stopped/image = new(here, owner, 0, afterimage_alpha) + image.color = cyberware_time_stop_green() + image.filters = null + image.pixel_w = owner.pixel_w + (old_loc.x - here.x) * ICON_SIZE_X + image.pixel_z = owner.pixel_z + (old_loc.y - here.y) * ICON_SIZE_Y + image.layer = owner.layer - 0.01 + +/// The afterimage left while time is stopped. +/obj/effect/temp_visual/decoy/fading/cascade/stopped + duration = 4 + +#undef SANDY_STOP_WITNESS_RANGE +#undef SANDY_STOP_FIELD_RANGE +#undef SANDY_STOP_BOSS_HOLD +#undef SANDY_STOP_SHOT_HANG +#undef SANDY_STOP_TRAIT +#undef SANDY_STOP_OUTLINE diff --git a/voidcrew/modules/unit_tests/_unit_tests.dm b/voidcrew/modules/unit_tests/_unit_tests.dm index e27485cc825..e3b63e8408e 100644 --- a/voidcrew/modules/unit_tests/_unit_tests.dm +++ b/voidcrew/modules/unit_tests/_unit_tests.dm @@ -102,6 +102,7 @@ #include "voidcrew_cyberware_passives.dm" #include "voidcrew_cyberware_soak.dm" #include "voidcrew_cyberware_text.dm" +#include "voidcrew_cyberware_timestop.dm" #include "voidcrew_cyberware_widowline.dm" #include "voidcrew_cyberware_zoom.dm" #include "voidcrew_derelict_helpers.dm" diff --git a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm new file mode 100644 index 00000000000..a6c376d4f55 --- /dev/null +++ b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm @@ -0,0 +1,82 @@ +/** + * # The Cascade line's time stop + * + * voidcrew/modules/cyberware/ware_sandy_timestop.dm. A player's surge with no other player near stops time: + * the field spawns, a human NPC is stunned with its AI paused, the user can still act, and the NPC is let go + * when the stop runs out. Another player near means no stop at all. Uses the street rig (2 s stop) to keep + * the waits short. + */ +/datum/unit_test/voidcrew_cyberware_timestop + abstract_type = /datum/unit_test/voidcrew_cyberware_timestop + +/// A player body (a key, no client) at `where` with the street rig in it +/datum/unit_test/voidcrew_cyberware_timestop/proc/make_user(turf/where, key) + var/mob/living/carbon/human/user = allocate(/mob/living/carbon/human/consistent, where) + user.key = key + var/obj/item/organ/cyberimp/cyberware/cascade/street/rig = allocate(/obj/item/organ/cyberimp/cyberware/cascade/street) + TEST_ASSERT(rig.Insert(user, special = TRUE), "Test fixture: the reflex rig wouldn't go in") + return user + +/// Opens the rig's window the way its surge button does +/datum/unit_test/voidcrew_cyberware_timestop/proc/surge(mob/living/carbon/human/user) + var/obj/item/organ/cyberimp/cyberware/cascade/rig = locate() in user.organs + return user.apply_status_effect(/datum/status_effect/cyberware_cascade_window, rig) + +/// A turf `dx`, `dy` from the bottom left of the test room +/datum/unit_test/voidcrew_cyberware_timestop/proc/spot(dx, dy) + return locate(run_loc_floor_bottom_left.x + dx, run_loc_floor_bottom_left.y + dy, run_loc_floor_bottom_left.z) + +/datum/unit_test/voidcrew_cyberware_timestop/Destroy() + for(var/mob/living/body in allocated) + body.key = null + return ..() + +/// Nobody else near: time stops, the NPC freezes with its AI paused, the user can still act, and it all ends on time +/datum/unit_test/voidcrew_cyberware_timestop/alone + +/datum/unit_test/voidcrew_cyberware_timestop/alone/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/mob/living/carbon/human/npc/raider = allocate(/mob/living/carbon/human/npc, spot(2, 0)) + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + TEST_ASSERT_NOTNULL(window, "The rig's window didn't open") + sleep(0.1 SECONDS) // the field starts async + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't stop with nobody else near") + TEST_ASSERT(istype(window.time_stop.chronofield, /datum/proximity_monitor/advanced/timestop/sandevistan), "The stop has no field") + + TEST_ASSERT(raider.IsStun(), "The NPC wasn't frozen") + TEST_ASSERT(HAS_TRAIT(raider, TRAIT_AI_PAUSED), "The NPC's AI kept running in stopped time") + TEST_ASSERT(!user.IsStun(), "The user froze in their own time stop") + TEST_ASSERT(!HAS_TRAIT(user, TRAIT_AI_PAUSED), "The user's AI was paused") + var/turf/start = get_turf(user) + user.Move(spot(0, 1), NORTH) + TEST_ASSERT(get_turf(user) != start, "The user couldn't move in stopped time") + + // Frozen bodies still get hurt through tg. + var/brute_before = raider.getBruteLoss() + raider.apply_damage(10, BRUTE, BODY_ZONE_CHEST) + TEST_ASSERT(raider.getBruteLoss() > brute_before, "A frozen NPC didn't take damage") + + sleep(2.3 SECONDS) // the street rig stops time for 2 s + TEST_ASSERT_NULL(window.time_stop, "The time stop outlived its 2 s") + TEST_ASSERT(!raider.IsStun(), "The NPC stayed frozen after time restarted") + TEST_ASSERT(!HAS_TRAIT(raider, TRAIT_AI_PAUSED), "The NPC's AI stayed paused after time restarted") + TEST_ASSERT(user.has_status_effect(/datum/status_effect/cyberware_cascade_window), "The window didn't carry on after the stop") + user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) + user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) + +/// Another player near: the plain surge, no stop +/datum/unit_test/voidcrew_cyberware_timestop/witnessed + +/datum/unit_test/voidcrew_cyberware_timestop/witnessed/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/mob/living/carbon/human/other = allocate(/mob/living/carbon/human/consistent, spot(3, 3)) + other.key = "sandytimestopother" // a player body, client or not + var/mob/living/carbon/human/npc/raider = allocate(/mob/living/carbon/human/npc, spot(2, 0)) + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + TEST_ASSERT_NOTNULL(window, "The rig's window didn't open") + sleep(0.1 SECONDS) + TEST_ASSERT_NULL(window.time_stop, "Time stopped with another player in sight") + TEST_ASSERT(!raider.IsStun(), "The NPC froze without a time stop") + TEST_ASSERT(!(locate(/obj/effect/timestop/sandevistan) in world), "A time stop field spawned with another player in sight") + user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) + user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) From 34d48420868c09bc0c69df4dd358fcf7ae33b35e Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Tue, 10 Nov 2026 20:16:39 -0800 Subject: [PATCH 02/20] Paint the stopped-time afterimages the ring's green --- voidcrew/modules/cyberware/ware_sandy_timestop.dm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/voidcrew/modules/cyberware/ware_sandy_timestop.dm b/voidcrew/modules/cyberware/ware_sandy_timestop.dm index fb264d05af5..cc56041d8a0 100644 --- a/voidcrew/modules/cyberware/ware_sandy_timestop.dm +++ b/voidcrew/modules/cyberware/ware_sandy_timestop.dm @@ -77,8 +77,9 @@ var/red = 90 / 255 var/grn = 1 var/blue = 150 / 255 - var/gain = 0.8 - var/lift = 0.5 + // gain + lift = 1, so white maps to the green itself and black to a dim green + var/gain = 0.65 + var/lift = 0.35 green = list( 0.3 * gain * red, 0.3 * gain * grn, 0.3 * gain * blue, 0.59 * gain * red, 0.59 * gain * grn, 0.59 * gain * blue, From 86cd34433c2bb22afd7902fedf7d4a059b1f2eb0 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Tue, 10 Nov 2026 20:29:30 -0800 Subject: [PATCH 03/20] Make the lone Reflex Surge slow the world instead of stopping it --- code/modules/projectiles/gun.dm | 11 + voidcrew/modules/cyberware/ware_legend.dm | 13 +- voidcrew/modules/cyberware/ware_line_sandy.dm | 8 +- .../modules/cyberware/ware_sandy_timestop.dm | 417 ++++++++++++++---- .../unit_tests/voidcrew_cyberware_timestop.dm | 85 ++-- 5 files changed, 409 insertions(+), 125 deletions(-) diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index 78941bbe241..371efe8e779 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -448,6 +448,11 @@ if(!user || !firing_burst) firing_burst = FALSE return FALSE + // VOIDCREW EDIT ADDITION START - cyberware time dilation (voidcrew/modules/cyberware/ware_sandy_timestop.dm): a burst queued before the world froze waits for it to move again + if(!cyberware_time_scale(user)) + addtimer(CALLBACK(src, PROC_REF(process_burst), user, target, message, params, zone_override, random_spread, burst_spread_mult, iteration), 1) + return TRUE + // VOIDCREW EDIT ADDITION END if(!issilicon(user)) if(iteration > 1 && !(user.is_holding(src))) //for burst firing firing_burst = FALSE @@ -517,6 +522,12 @@ if(user && HAS_TRAIT(user, TRAIT_DOUBLE_TAP)) modified_burst_delay = ROUND_UP(burst_delay * 0.5) modified_fire_delay = ROUND_UP(fire_delay * 0.5) + // VOIDCREW EDIT ADDITION START - cyberware time dilation (voidcrew/modules/cyberware/ware_sandy_timestop.dm): a slowed shooter's gun cycles slowly too + var/dilation = cyberware_time_scale(user) + if(dilation < 1) + modified_burst_delay /= max(dilation, 0.1) + modified_fire_delay /= max(dilation, 0.1) + // VOIDCREW EDIT ADDITION END if(burst_size > 1) firing_burst = TRUE diff --git a/voidcrew/modules/cyberware/ware_legend.dm b/voidcrew/modules/cyberware/ware_legend.dm index b5bca636523..ae0a29e8df8 100644 --- a/voidcrew/modules/cyberware/ware_legend.dm +++ b/voidcrew/modules/cyberware/ware_legend.dm @@ -90,8 +90,13 @@ var/window_alert = /atom/movable/screen/alert/status_effect/cyberware_cascade /// What the bearer reads on activation. var/online_message = "CASCADE ONLINE." - /// How long the world stops when no other player is near (ware_sandy_timestop.dm). - var/stop_duration = 4 SECONDS + // When no other player is near, the world slows (ware_sandy_timestop.dm): + /// how long it freezes first, + var/dilation_freeze = 1 SECONDS + /// how long slow-mo runs after that, + var/dilation_time = 3 SECONDS + /// and how fast the world runs in slow-mo. + var/dilation_scale = 0.2 /obj/item/organ/cyberimp/cyberware/cascade/on_mob_remove(mob/living/carbon/organ_owner, special = FALSE, movement_flags) . = ..() @@ -190,7 +195,9 @@ online_message = lattice.online_message crash_duration = lattice.crash_duration crash_stamina = lattice.crash_stamina - stop_duration = lattice.stop_duration + dilation_freeze = lattice.dilation_freeze + dilation_time = lattice.dilation_time + dilation_scale = lattice.dilation_scale return ..() /datum/status_effect/cyberware_cascade_window/on_apply() diff --git a/voidcrew/modules/cyberware/ware_line_sandy.dm b/voidcrew/modules/cyberware/ware_line_sandy.dm index 8cb63930cbe..57850ce3801 100644 --- a/voidcrew/modules/cyberware/ware_line_sandy.dm +++ b/voidcrew/modules/cyberware/ware_line_sandy.dm @@ -52,7 +52,9 @@ afterimage_alpha = 120 window_alert = /atom/movable/screen/alert/status_effect/cyberware_cascade/aftermarket online_message = "REFLEX RIG ONLINE." - stop_duration = 2 SECONDS + dilation_freeze = 0 + dilation_time = 2 SECONDS + dilation_scale = 0.5 /datum/action/cooldown/cyberware/cascade_surge/street name = "Reflex Surge" @@ -90,7 +92,9 @@ afterimage_alpha = 160 window_alert = /atom/movable/screen/alert/status_effect/cyberware_cascade/aftermarket online_message = "LATTICE ONLINE." - stop_duration = 3 SECONDS + dilation_freeze = 0 + dilation_time = 3 SECONDS + dilation_scale = 0.35 /datum/action/cooldown/cyberware/cascade_surge/milspec name = "Reflex Surge" diff --git a/voidcrew/modules/cyberware/ware_sandy_timestop.dm b/voidcrew/modules/cyberware/ware_sandy_timestop.dm index cc56041d8a0..ae71484ebaf 100644 --- a/voidcrew/modules/cyberware/ware_sandy_timestop.dm +++ b/voidcrew/modules/cyberware/ware_sandy_timestop.dm @@ -1,59 +1,78 @@ /** - * # The Cascade line's time stop + * # The Cascade line's time dilation * * When a Cascade-line surge (ware_legend.dm, ware_line_sandy.dm) opens and no other player is within - * SANDY_STOP_WITNESS_RANGE tiles, the world stops for the lattice's stop_duration (street 2 s, service 3 s, - * Cascade 4 s). The rest of the window then runs as normal, then the crash. With another player near, the - * surge is the plain one. Only a player's surge stops time: an NPC criminal's lattice never does. + * SANDY_STOP_WITNESS_RANGE tiles, the world around the user slows down: * - * Built on tg's own time stop (code/datums/proximity_monitor/fields/timestop.dm). The field is hosted on - * the user, so it follows them; anything it leaves behind stays frozen until it ends. + * model freeze then slow-mo + * street - 50% for 2 s + * service - 35% for 3 s + * Cascade T4 1 s 20% for 3 s * - * No shortcuts on people: frozen mobs are held by tg's Stun, re-applied every tick by the tg field, and - * anything with an AI controller also gets TRAIT_AI_PAUSED (Voidcrew's human NPCs run a basic controller on a - * carbon body, which the tg field's basic-mob branch misses). Frozen bodies still take damage, wounds and - * bleeding through tg as normal. Bosses (a finisher user, or a boss-rank human NPC) are held for - * SANDY_STOP_BOSS_HOLD only, then let go while the field carries on. + * The rest of the window then runs as normal, then the crash. With another player near, the surge is the + * plain one. Only a player's surge does this (the user needs a ckey): an NPC criminal's lattice never does. * - * The field re-checks for players every tick and ends at once if one turns up. + * Built on tg's time stop field (code/datums/proximity_monitor/fields/timestop.dm), hosted on the user so + * it follows them. Anything it touches stays slowed (or frozen) until the effect ends, even once the user + * walks off. The field re-checks for players every tick and ends at once if one comes in. * - * Look: frozen things get a grey-green tint instead of tg's inversion. Floor items, blood and the user are - * never frozen, so they keep their colour. The user's own shots stop about a tile out, keep their colour, - * and fly on when time restarts. + * No shortcuts on people. A frozen mob gets tg's own TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED and + * TRAIT_AI_PAUSED, never a fake state or a Stun. A slowed mob moves under a movespeed modifier, and every + * tick the field pushes its timing deadlines back by the time it lost (next_move, ability cooldowns, AI + * behaviour cooldowns and the blackboard deadlines listed in dilated_blackboard_keys(), ranged cooldowns). + * Guns scale their fire and burst delays for a slowed shooter (VOIDCREW EDIT in gun.dm). Frozen and slowed + * bodies take damage, wounds and bleeding through tg as normal. Not covered: do_after progress bars and + * burst timers queued before the slow-mo started. + * + * Bosses (a finisher user, or a boss-rank human NPC) are held for SANDY_STOP_BOSS_HOLD at most. + * + * Look: affected things get a grey-green tint instead of tg's inversion. Floor items, blood decals and the + * user are never touched, so they keep their colour, and so do the user's own shots. Shots the user fires + * while the world is frozen fly a tile, hang there, and go on when it starts moving again. */ -/// Another player within this many tiles of the user means no time stop. -#define SANDY_STOP_WITNESS_RANGE 10 -/// Radius of the frozen field around the user (a widescreen view is 9 tiles to each side). +/// Another player within this many tiles of the user means no dilation. +#define SANDY_STOP_WITNESS_RANGE 11 +/// Radius of the field around the user (a widescreen view is 9 tiles to each side). #define SANDY_STOP_FIELD_RANGE 9 -/// How long a boss stays frozen before it shrugs the field off. +/// How long a boss stays frozen or slowed before it shrugs the field off. #define SANDY_STOP_BOSS_HOLD (1 SECONDS) -/// How far, in pixels, the user's own shots fly before they hang. +/// How far, in pixels, the user's own shots fly before they hang in a frozen world. #define SANDY_STOP_SHOT_HANG 32 -/// Trait source for everything the time stop applies. +/// Trait source for everything the dilation applies. #define SANDY_STOP_TRAIT "sandevistan_time_stop" /// Filter name of the user's edge glow. #define SANDY_STOP_OUTLINE "sandy_time_stop_outline" -/// Whether a player other than `user` is within `range` tiles: anyone with a client, or a living body with a ckey whose player has disconnected. +/// Every atom the dilation has hold of, to its field. +GLOBAL_LIST_EMPTY(cyberware_time_dilated) + +/// How fast time runs for `thing`: 1 normally, 0 frozen, in between in slow-mo. +/proc/cyberware_time_scale(atom/thing) + if(isnull(thing)) + return 1 + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = GLOB.cyberware_time_dilated[thing] + return field ? field.time_scale : 1 + +/// Whether a player other than `user` is within `range` tiles: any living body with a ckey, connected or not. /proc/cyberware_time_stop_witnessed(mob/living/user, range = SANDY_STOP_WITNESS_RANGE) var/turf/here = get_turf(user) if(!here) return TRUE - for(var/atom/movable/found as anything in SSspatial_grid.orthogonal_range_search(here, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS, range)) + for(var/atom/movable/found as anything in SSspatial_grid.orthogonal_range_search(here, SPATIAL_GRID_CONTENTS_TYPE_HEARING, range)) if(found == user) continue if(isliving(found)) - return TRUE - if(ismob(found)) // ghosts and camera eyes don't count + var/mob/living/body = found + if(body.ckey) + return TRUE + continue + if(ismob(found) || isitem(found)) // ghosts and camera eyes don't count; radios hold nobody continue // a locker or a mech with a player in it for(var/mob/living/inside in found.get_all_contents()) - if(inside != user && inside.client) + if(inside != user && inside.ckey) return TRUE - for(var/mob/living/body in SSspatial_grid.orthogonal_range_search(here, SPATIAL_GRID_CONTENTS_TYPE_HEARING, range)) - if(body != user && body.ckey) - return TRUE return FALSE /// Whether `victim` is a boss: anything with a finisher, or a boss-rank human NPC. @@ -65,12 +84,12 @@ return person.npc_rank >= HUMAN_NPC_RANK_BOSS return FALSE -/// The grey-green wash on frozen things. +/// The grey-green wash on dilated things. /proc/cyberware_time_stop_tint() var/static/list/tint = list(0.26,0.28,0.27, 0.52,0.56,0.54, 0.10,0.11,0.105, 0.02,0.05,0.04) return tint -/// Flattens a sprite to luminance and paints it the time stop's green (#5AFF96), lifted so dark parts still glow. +/// Flattens a sprite to luminance and paints it the dilation's green (#5AFF96). /proc/cyberware_time_stop_green() var/static/list/green if(!green) @@ -88,24 +107,56 @@ ) return green +/// Blackboard keys holding world.time deadlines that slow down with their owner. +/proc/dilated_blackboard_keys() + var/static/list/keys = list( + BB_HUMAN_NPC_NEXT_SHOT, + BB_HUMAN_NPC_RELOAD_UNTIL, + BB_HUMAN_NPC_ARMING_RETRY_AT, + BB_HUMAN_NPC_NEXT_TREAT, + BB_HUMAN_NPC_FLEE_UNTIL, + BB_BOUNTY_NEXT_STEP_AT, + BB_BOUNTY_BACKSTEP_AT, + BB_BOUNTY_BOSS_HUNT_AT, + BB_BOUNTY_BOSS_RELOAD_UNTIL, + BB_BASIC_MOB_REINFORCEMENTS_COOLDOWN, + // defined and undefined inside their own modules + "convict_hit_until", + "convict_lost_since", + "pirate_windup_until", + "pirate_breach_started", + "pirate_no_path_at", + "pirate_breach_progress_at", + ) + return keys + +/datum/movespeed_modifier/cyberware_dilation + variable = TRUE + // ========================================================================= -// THE STOP +// THE EFFECT // ========================================================================= -/// Lives as long as the stop, holds the field on the user, and ends the stop when it's deleted. +/// Lives as long as the dilation, holds the field on the user, and ends it when deleted. /obj/effect/timestop/sandevistan name = "stopped moment" desc = null hidden = TRUE alpha = 0 freezerange = SANDY_STOP_FIELD_RANGE - /// Whose stop this is. The field is hosted on them. + /// Whose dilation this is. The field is hosted on them. var/mob/living/user + /// How long the world is frozen first. + var/freeze_time = 0 + /// How fast the world runs after that. + var/slow_scale = 1 -/// `immune_atoms` must be list(user). -/obj/effect/timestop/sandevistan/Initialize(mapload, radius, time, list/immune_atoms, start = TRUE, silent = TRUE) +/// `immune_atoms` must be list(user). `time` is the whole effect; `freeze` of it is frozen, the rest at `scale`. +/obj/effect/timestop/sandevistan/Initialize(mapload, radius, time, list/immune_atoms, start = TRUE, silent = TRUE, freeze = 0, scale = 1) if(length(immune_atoms)) user = immune_atoms[1] + freeze_time = freeze + slow_scale = scale return ..(mapload, radius, time, immune_atoms, start, TRUE) /obj/effect/timestop/sandevistan/Destroy() @@ -117,78 +168,207 @@ QDEL_IN(src, duration) if(QDELETED(user)) return - var/datum/proximity_monitor/advanced/timestop/sandevistan/field = new(user, freezerange, TRUE, immune, antimagic_flags, FALSE) + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = new(user, freezerange, TRUE, immune, antimagic_flags, FALSE, freeze_time > 0 ? 0 : slow_scale) field.stopper = src + field.slow_scale = slow_scale + if(freeze_time > 0) + field.thaw_at = world.time + freeze_time chronofield = field +// ========================================================================= +// THE FIELD +// ========================================================================= + /datum/proximity_monitor/advanced/timestop/sandevistan - /// The effect that owns this field. Deleting it ends the stop. + /// The effect that owns this field. Deleting it ends the dilation. var/obj/effect/timestop/sandevistan/stopper - /// Frozen bosses, to the world.time they shrug the field off. + /// How fast time runs in here right now. 0 is frozen. + var/time_scale = 0 + /// The slow-mo scale the field drops to once the freeze ends. + var/slow_scale = 1 + /// world.time the freeze ends and slow-mo starts, or 0. + var/thaw_at = 0 + /// world.time of the last tick, for how much time each tick dilated. + var/last_tick = 0 + /// Mobs in hand. + var/list/held_mobs = list() + /// Projectiles in hand, to their own speed. + var/list/held_shots = list() + /// Thrown things in hand. + var/list/held_throws = list() + /// The user's own shots: never tinted. + var/list/user_shots = list() + /// Bosses in hand, to the world.time they shrug the field off. var/list/boss_holds = list() /// Bosses that already shrugged it off. The field leaves them be. var/list/released = list() - /// The user's own shots, hanging in the air. - var/list/hung_shots = list() -/datum/proximity_monitor/advanced/timestop/sandevistan/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, list/immune, antimagic_flags, channelled) +/datum/proximity_monitor/advanced/timestop/sandevistan/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, list/immune, antimagic_flags, channelled, start_scale = 0) + time_scale = start_scale + last_tick = world.time . = ..() RegisterSignal(host, COMSIG_PROJECTILE_FIRER_BEFORE_FIRE, PROC_REF(on_user_fire)) /datum/proximity_monitor/advanced/timestop/sandevistan/Destroy() if(host) UnregisterSignal(host, COMSIG_PROJECTILE_FIRER_BEFORE_FIRE) - for(var/obj/projectile/shot as anything in hung_shots) - UnregisterSignal(shot, COMSIG_QDELETING) - shot.paused = FALSE - hung_shots.Cut() stopper = null - . = ..() + . = ..() // unfreeze_all() lets everything go released.Cut() - boss_holds.Cut() /datum/proximity_monitor/advanced/timestop/sandevistan/freeze_atom(atom/movable/A) - if(released[A]) + if(!istype(A) || GLOB.cyberware_time_dilated[A] || global_frozen_atoms[A] || released[A] || immune[A]) return FALSE - if(isprojectile(A)) - var/obj/projectile/shot = A - if(shot.firer && immune[shot.firer]) // the user's own shots hang on their own clock + if(ismob(A)) + var/mob/M = A + if(M.can_block_magic(antimagic_flags)) + immune[A] = TRUE return FALSE - return ..() + var/tint = TRUE + if(isliving(A)) + hold_mob(A) + else if(isprojectile(A)) + var/obj/projectile/shot = A + if(shot.firer && immune[shot.firer]) + if(time_scale <= 0) // hang_shot takes these + return FALSE + user_shots[shot] = TRUE + tint = FALSE + hold_shot(shot) + else if((ismachinery(A) && !istype(A, /obj/machinery/light)) || isstructure(A) || ismecha(A)) + frozen_structures += A // looks only + else if(!A.throwing) + return FALSE + if(A.throwing) + held_throws[A] = TRUE + if(time_scale <= 0) + A.throwing.paused = TRUE + GLOB.cyberware_time_dilated[A] = src + frozen_things[A] = TRUE + if(tint) + into_the_negative_zone(A) + SEND_SIGNAL(A, COMSIG_ATOM_TIMESTOP_FREEZE, src) + return TRUE -/datum/proximity_monitor/advanced/timestop/sandevistan/freeze_mob(mob/living/victim) - . = ..() - if(victim.ai_controller) - ADD_TRAIT(victim, TRAIT_AI_PAUSED, SANDY_STOP_TRAIT) +/datum/proximity_monitor/advanced/timestop/sandevistan/unfreeze_all() + for(var/atom/movable/thing as anything in frozen_things) + unfreeze_atom(thing) + for(var/turf/ground as anything in frozen_turfs) + unfreeze_turf(ground) + frozen_turfs.Cut() + +/datum/proximity_monitor/advanced/timestop/sandevistan/unfreeze_atom(atom/movable/A) + if(held_mobs[A]) + let_go_mob(A) + if(held_shots[A]) + let_go_shot(A) + if(held_throws[A]) + var/datum/thrownthing/throw_datum = A.throwing + if(throw_datum) + throw_datum.paused = FALSE + held_throws -= A + frozen_structures -= A + user_shots -= A + escape_the_negative_zone(A) + frozen_things -= A + GLOB.cyberware_time_dilated -= A + SEND_SIGNAL(A, COMSIG_ATOM_TIMESTOP_UNFREEZE, src) + +// ----- mobs ----- + +/// Takes hold of a mob: frozen or slowed, whichever the field is doing. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/hold_mob(mob/living/victim) + held_mobs[victim] = TRUE + if(time_scale <= 0) + freeze_mob(victim) + else + slow_mob(victim) if(cyberware_time_stop_boss(victim)) boss_holds[victim] = world.time + SANDY_STOP_BOSS_HOLD -/datum/proximity_monitor/advanced/timestop/sandevistan/unfreeze_mob(mob/living/victim) - REMOVE_TRAIT(victim, TRAIT_AI_PAUSED, SANDY_STOP_TRAIT) +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/let_go_mob(mob/living/victim) + unfreeze_mob(victim) + unslow_mob(victim) + held_mobs -= victim boss_holds -= victim - return ..() -/datum/proximity_monitor/advanced/timestop/sandevistan/process() - if(QDELETED(host) || cyberware_time_stop_witnessed(host)) - if(!QDELETED(stopper)) - qdel(stopper) // takes this field with it +/datum/proximity_monitor/advanced/timestop/sandevistan/freeze_mob(mob/living/victim) + frozen_mobs[victim] = TRUE + victim.add_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED, TRAIT_AI_PAUSED, TRAIT_MUTE, TRAIT_EMOTEMUTE), SANDY_STOP_TRAIT) + GLOB.move_manager.stop_looping(victim) + if(isanimal(victim)) // the old simple mob AI doesn't read TRAIT_AI_PAUSED + var/mob/living/simple_animal/animal_victim = victim + animal_victim.toggle_ai(AI_OFF) + +/datum/proximity_monitor/advanced/timestop/sandevistan/unfreeze_mob(mob/living/victim) + if(!frozen_mobs[victim]) return - for(var/mob/living/boss as anything in boss_holds.Copy()) - if(world.time < boss_holds[boss]) - continue - unfreeze_atom(boss) - released[boss] = TRUE - return ..() + frozen_mobs -= victim + victim.remove_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED, TRAIT_AI_PAUSED, TRAIT_MUTE, TRAIT_EMOTEMUTE), SANDY_STOP_TRAIT) + if(isanimal(victim)) + var/mob/living/simple_animal/animal_victim = victim + animal_victim.toggle_ai(initial(animal_victim.AIStatus)) -/datum/proximity_monitor/advanced/timestop/sandevistan/into_the_negative_zone(atom/A) - A.add_atom_colour(cyberware_time_stop_tint(), TEMPORARY_COLOUR_PRIORITY) +/// Slows a mob's walk to the field's pace. Its timers are pushed back every tick in process(). +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/slow_mob(mob/living/victim) + var/base_delay = max(victim.cached_multiplicative_slowdown, 1) + victim.add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation, multiplicative_slowdown = base_delay * (1 / time_scale - 1)) + if(istype(victim, /mob/living/simple_animal/hostile)) + var/mob/living/simple_animal/hostile/animal_victim = victim + animal_victim.move_to_delay = initial(animal_victim.move_to_delay) / time_scale + +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/unslow_mob(mob/living/victim) + victim.remove_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation) + if(istype(victim, /mob/living/simple_animal/hostile)) + var/mob/living/simple_animal/hostile/animal_victim = victim + animal_victim.move_to_delay = initial(animal_victim.move_to_delay) + +/// Pushes every timing deadline `victim` has that hasn't passed yet back by `push` deciseconds. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/push_deadlines(mob/living/victim, push) + var/now = world.time + if(victim.next_move > now) + victim.next_move += push + for(var/datum/action/cooldown/ability in victim.actions) + if(ability.next_use_time > now) + ability.next_use_time += push + var/datum/ai_controller/controller = victim.ai_controller + if(controller) + for(var/behavior in controller.behavior_cooldowns) + if(controller.behavior_cooldowns[behavior] > now) + controller.behavior_cooldowns[behavior] += push + for(var/key in dilated_blackboard_keys()) + var/deadline = controller.blackboard[key] + if(isnum(deadline) && deadline > now) + controller.blackboard[key] = deadline + push + var/datum/component/ranged_attacks/ranged = victim.GetComponent(/datum/component/ranged_attacks) + if(ranged && ranged.fire_cooldown > now) + ranged.fire_cooldown += push + if(istype(victim, /mob/living/simple_animal/hostile)) + var/mob/living/simple_animal/hostile/animal_victim = victim + if(animal_victim.ranged_cooldown > now) + animal_victim.ranged_cooldown += push + +// ----- projectiles ----- + +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/hold_shot(obj/projectile/shot) + held_shots[shot] = shot.speed + if(time_scale <= 0) + shot.paused = TRUE + else + shot.speed = held_shots[shot] * time_scale + +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/let_go_shot(obj/projectile/shot) + shot.speed = held_shots[shot] + shot.paused = FALSE + held_shots -= shot /// Signal proc for [COMSIG_PROJECTILE_FIRER_BEFORE_FIRE] on the user: watch the shot leave the barrel. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/on_user_fire(datum/source, obj/projectile/shot, atom/fired_from, atom/original) SIGNAL_HANDLER - RegisterSignal(shot, COMSIG_PROJECTILE_FIRE, PROC_REF(hang_shot)) + if(time_scale <= 0) + RegisterSignal(shot, COMSIG_PROJECTILE_FIRE, PROC_REF(hang_shot)) -/// Signal proc for [COMSIG_PROJECTILE_FIRE]: the shot flies a tile, then hangs until time restarts. +/// Signal proc for [COMSIG_PROJECTILE_FIRE]: in a frozen world the user's shot flies a tile, then hangs. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/hang_shot(obj/projectile/shot) SIGNAL_HANDLER UnregisterSignal(shot, COMSIG_PROJECTILE_FIRE) @@ -199,43 +379,100 @@ shot.process_movement(SANDY_STOP_SHOT_HANG) if(QDELETED(shot) || shot.deletion_queued) return - shot.paused = TRUE - hung_shots += shot - RegisterSignal(shot, COMSIG_QDELETING, PROC_REF(shot_gone)) + user_shots[shot] = TRUE + hold_shot(shot) + GLOB.cyberware_time_dilated[shot] = src + frozen_things[shot] = TRUE + RegisterSignal(shot, COMSIG_QDELETING, PROC_REF(held_gone)) -/datum/proximity_monitor/advanced/timestop/sandevistan/proc/shot_gone(obj/projectile/shot) +/// Forgets a held thing that got deleted. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/held_gone(atom/movable/thing) SIGNAL_HANDLER - hung_shots -= shot + held_shots -= thing + user_shots -= thing + frozen_things -= thing + GLOB.cyberware_time_dilated -= thing + +// ----- the clock ----- + +/// Moves the whole field to a new pace: frozen things start moving, slowed things change speed. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/set_time_scale(new_scale) + time_scale = new_scale + for(var/mob/living/victim as anything in held_mobs) + unfreeze_mob(victim) + if(time_scale <= 0) + unslow_mob(victim) + freeze_mob(victim) + else + slow_mob(victim) + for(var/obj/projectile/shot as anything in held_shots) + shot.paused = time_scale <= 0 + if(time_scale > 0) + shot.speed = held_shots[shot] * time_scale + for(var/atom/movable/thing as anything in held_throws) + if(thing.throwing) + thing.throwing.paused = time_scale <= 0 + +/datum/proximity_monitor/advanced/timestop/sandevistan/process() + if(QDELETED(host) || cyberware_time_stop_witnessed(host)) + if(!QDELETED(stopper)) + qdel(stopper) // takes this field with it + return + var/elapsed = world.time - last_tick + last_tick = world.time + var/push = elapsed * (1 - time_scale) + if(push > 0) + for(var/mob/living/victim as anything in held_mobs) + push_deadlines(victim, push) + for(var/atom/movable/thing as anything in held_throws) + var/datum/thrownthing/throw_datum = thing.throwing + if(throw_datum && !throw_datum.paused) + throw_datum.start_time += push + for(var/mob/living/boss as anything in boss_holds.Copy()) + if(world.time < boss_holds[boss]) + continue + unfreeze_atom(boss) + released[boss] = TRUE + if(thaw_at && world.time >= thaw_at) + thaw_at = 0 + set_time_scale(slow_scale) + +/datum/proximity_monitor/advanced/timestop/sandevistan/into_the_negative_zone(atom/A) + A.add_atom_colour(cyberware_time_stop_tint(), TEMPORARY_COLOUR_PRIORITY) // ========================================================================= // THE WINDOW'S SIDE // ========================================================================= /datum/status_effect/cyberware_cascade_window - /// How long the world stops if nobody else is near when the window opens. Copied from the lattice. - var/stop_duration = 4 SECONDS - /// The running stop, if time is stopped. + /// How long the world is frozen if nobody else is near when the window opens. Copied from the lattice. + var/dilation_freeze = 1 SECONDS + /// How long slow-mo runs after the freeze. + var/dilation_time = 3 SECONDS + /// How fast the world runs in slow-mo. + var/dilation_scale = 0.2 + /// The running dilation, if any. var/obj/effect/timestop/sandevistan/time_stop -/// Stops time if the bearer is a player and nobody else is near. Called as the window opens. +/// Slows the world if the bearer is a player and nobody else is near. Called as the window opens. /datum/status_effect/cyberware_cascade_window/proc/try_time_stop() - if(stop_duration <= 0 || !owner.ckey || time_stop) + if(dilation_freeze + dilation_time <= 0 || !owner.ckey || time_stop) return FALSE if(cyberware_time_stop_witnessed(owner)) return FALSE var/turf/here = get_turf(owner) if(!here) return FALSE - time_stop = new(here, SANDY_STOP_FIELD_RANGE, stop_duration, list(owner)) + time_stop = new(here, SANDY_STOP_FIELD_RANGE, dilation_freeze + dilation_time, list(owner), TRUE, TRUE, dilation_freeze, dilation_scale) if(QDELETED(time_stop)) time_stop = null return FALSE RegisterSignal(time_stop, COMSIG_QDELETING, PROC_REF(time_stop_over)) - owner.remove_client_colour(id) // the user sees the frozen world as it is + owner.remove_client_colour(id) // the user sees the slowed world as it is owner.add_filter(SANDY_STOP_OUTLINE, 2, outline_filter(1, "#5AFF96D2")) return TRUE -/// Signal proc for [COMSIG_QDELETING] on the stop: time restarts and the rest of the window runs as normal. +/// Signal proc for [COMSIG_QDELETING] on the dilation: time runs normally and the rest of the window carries on. /datum/status_effect/cyberware_cascade_window/proc/time_stop_over(datum/source) SIGNAL_HANDLER time_stop = null @@ -244,7 +481,7 @@ owner.remove_filter(SANDY_STOP_OUTLINE) owner.add_client_colour(colour_type, id) -/// Ends the stop early (the window is closing). +/// Ends the dilation early (the window is closing). /datum/status_effect/cyberware_cascade_window/proc/end_time_stop() if(!time_stop) return @@ -253,7 +490,7 @@ if(!QDELETED(owner)) owner.remove_filter(SANDY_STOP_OUTLINE) -/// One afterimage while time is stopped: green, gone in 0.4 s. It sits on the user's own tile, shifted back to +/// One afterimage while time is dilated: green, gone in 0.4 s. It sits on the user's own tile, shifted back to /// where they were, so it sorts on their row and draws behind them in every facing. /datum/status_effect/cyberware_cascade_window/proc/stopped_afterimage(turf/old_loc) var/turf/here = get_turf(owner) @@ -266,7 +503,7 @@ image.pixel_z = owner.pixel_z + (old_loc.y - here.y) * ICON_SIZE_Y image.layer = owner.layer - 0.01 -/// The afterimage left while time is stopped. +/// The afterimage left while time is dilated. /obj/effect/temp_visual/decoy/fading/cascade/stopped duration = 4 diff --git a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm index a6c376d4f55..b4d09339dae 100644 --- a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm +++ b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm @@ -1,26 +1,28 @@ /** - * # The Cascade line's time stop + * # The Cascade line's time dilation * - * voidcrew/modules/cyberware/ware_sandy_timestop.dm. A player's surge with no other player near stops time: - * the field spawns, a human NPC is stunned with its AI paused, the user can still act, and the NPC is let go - * when the stop runs out. Another player near means no stop at all. Uses the street rig (2 s stop) to keep - * the waits short. + * voidcrew/modules/cyberware/ware_sandy_timestop.dm. A player's surge with no other player near slows the + * world: the Cascade lattice freezes it for 1 s (a human NPC is held by tg's immobilise, hand-block and + * AI-pause traits, and its timers stop), then runs it at 20% for 3 s (the NPC walks under a movespeed + * modifier), then lets go. The user can act the whole time. Another player near means no dilation at all. + * + * Unit-test files compile before voidcrew/_DEFINES/, so literals stand in for module defines. */ /datum/unit_test/voidcrew_cyberware_timestop abstract_type = /datum/unit_test/voidcrew_cyberware_timestop -/// A player body (a key, no client) at `where` with the street rig in it +/// A player body (a key, no client) at `where` with a Cascade lattice in it /datum/unit_test/voidcrew_cyberware_timestop/proc/make_user(turf/where, key) var/mob/living/carbon/human/user = allocate(/mob/living/carbon/human/consistent, where) user.key = key - var/obj/item/organ/cyberimp/cyberware/cascade/street/rig = allocate(/obj/item/organ/cyberimp/cyberware/cascade/street) - TEST_ASSERT(rig.Insert(user, special = TRUE), "Test fixture: the reflex rig wouldn't go in") + var/obj/item/organ/cyberimp/cyberware/cascade/lattice = allocate(/obj/item/organ/cyberimp/cyberware/cascade) + TEST_ASSERT(lattice.Insert(user, special = TRUE), "Test fixture: the lattice wouldn't go in") return user -/// Opens the rig's window the way its surge button does +/// Opens the lattice's window the way its surge button does /datum/unit_test/voidcrew_cyberware_timestop/proc/surge(mob/living/carbon/human/user) - var/obj/item/organ/cyberimp/cyberware/cascade/rig = locate() in user.organs - return user.apply_status_effect(/datum/status_effect/cyberware_cascade_window, rig) + var/obj/item/organ/cyberimp/cyberware/cascade/lattice = locate() in user.organs + return user.apply_status_effect(/datum/status_effect/cyberware_cascade_window, lattice) /// A turf `dx`, `dy` from the bottom left of the test room /datum/unit_test/voidcrew_cyberware_timestop/proc/spot(dx, dy) @@ -31,40 +33,52 @@ body.key = null return ..() -/// Nobody else near: time stops, the NPC freezes with its AI paused, the user can still act, and it all ends on time +/// Nobody else near: the world freezes, then slows, then lets go on time; the user can act throughout /datum/unit_test/voidcrew_cyberware_timestop/alone /datum/unit_test/voidcrew_cyberware_timestop/alone/Run() var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") var/mob/living/carbon/human/npc/raider = allocate(/mob/living/carbon/human/npc, spot(2, 0)) + var/next_move_was = world.time + 5 SECONDS + raider.next_move = next_move_was var/datum/status_effect/cyberware_cascade_window/window = surge(user) - TEST_ASSERT_NOTNULL(window, "The rig's window didn't open") + TEST_ASSERT_NOTNULL(window, "The lattice's window didn't open") sleep(0.1 SECONDS) // the field starts async - TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't stop with nobody else near") - TEST_ASSERT(istype(window.time_stop.chronofield, /datum/proximity_monitor/advanced/timestop/sandevistan), "The stop has no field") + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") + TEST_ASSERT(istype(window.time_stop.chronofield, /datum/proximity_monitor/advanced/timestop/sandevistan), "The dilation has no field") - TEST_ASSERT(raider.IsStun(), "The NPC wasn't frozen") - TEST_ASSERT(HAS_TRAIT(raider, TRAIT_AI_PAUSED), "The NPC's AI kept running in stopped time") - TEST_ASSERT(!user.IsStun(), "The user froze in their own time stop") - TEST_ASSERT(!HAS_TRAIT(user, TRAIT_AI_PAUSED), "The user's AI was paused") + // The freeze: tg's own traits, no Stun. + TEST_ASSERT(HAS_TRAIT(raider, TRAIT_IMMOBILIZED), "The NPC wasn't held still") + TEST_ASSERT(HAS_TRAIT(raider, TRAIT_HANDS_BLOCKED), "The NPC's hands weren't stopped") + TEST_ASSERT(HAS_TRAIT(raider, TRAIT_AI_PAUSED), "The NPC's AI kept running in frozen time") + TEST_ASSERT(!raider.IsStun(), "The freeze stunned the NPC instead of holding it") + TEST_ASSERT(!HAS_TRAIT(user, TRAIT_IMMOBILIZED) && !HAS_TRAIT(user, TRAIT_HANDS_BLOCKED), "The user froze in their own time stop") var/turf/start = get_turf(user) user.Move(spot(0, 1), NORTH) - TEST_ASSERT(get_turf(user) != start, "The user couldn't move in stopped time") + TEST_ASSERT(get_turf(user) != start, "The user couldn't move in frozen time") // Frozen bodies still get hurt through tg. var/brute_before = raider.getBruteLoss() raider.apply_damage(10, BRUTE, BODY_ZONE_CHEST) TEST_ASSERT(raider.getBruteLoss() > brute_before, "A frozen NPC didn't take damage") - sleep(2.3 SECONDS) // the street rig stops time for 2 s - TEST_ASSERT_NULL(window.time_stop, "The time stop outlived its 2 s") - TEST_ASSERT(!raider.IsStun(), "The NPC stayed frozen after time restarted") - TEST_ASSERT(!HAS_TRAIT(raider, TRAIT_AI_PAUSED), "The NPC's AI stayed paused after time restarted") - TEST_ASSERT(user.has_status_effect(/datum/status_effect/cyberware_cascade_window), "The window didn't carry on after the stop") + sleep(0.8 SECONDS) // still inside the Cascade's 1 s freeze + TEST_ASSERT(raider.next_move > next_move_was + 0.5 SECONDS, "The NPC's timers ran on through frozen time") + + sleep(0.5 SECONDS) // then the world runs at 20% + TEST_ASSERT_NOTNULL(window.time_stop, "The dilation ended with the freeze") + TEST_ASSERT(!HAS_TRAIT(raider, TRAIT_IMMOBILIZED) && !HAS_TRAIT(raider, TRAIT_AI_PAUSED), "The NPC stayed frozen into the slow-mo") + TEST_ASSERT(raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "The NPC isn't slowed in slow-mo") + + sleep(2.9 SECONDS) // the rest of the 3 s of slow-mo + TEST_ASSERT_NULL(window.time_stop, "The dilation outlived its 4 s") + TEST_ASSERT(!raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "The NPC stayed slow after the dilation") + TEST_ASSERT(!HAS_TRAIT(raider, TRAIT_AI_PAUSED), "The NPC's AI stayed paused after the dilation") + TEST_ASSERT(user.has_status_effect(/datum/status_effect/cyberware_cascade_window), "The window didn't carry on after the dilation") user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) -/// Another player near: the plain surge, no stop +/// Another player near: the plain surge, no dilation /datum/unit_test/voidcrew_cyberware_timestop/witnessed /datum/unit_test/voidcrew_cyberware_timestop/witnessed/Run() @@ -73,10 +87,21 @@ other.key = "sandytimestopother" // a player body, client or not var/mob/living/carbon/human/npc/raider = allocate(/mob/living/carbon/human/npc, spot(2, 0)) var/datum/status_effect/cyberware_cascade_window/window = surge(user) - TEST_ASSERT_NOTNULL(window, "The rig's window didn't open") + TEST_ASSERT_NOTNULL(window, "The lattice's window didn't open") + sleep(0.1 SECONDS) + TEST_ASSERT_NULL(window.time_stop, "Time slowed with another player in sight") + TEST_ASSERT(!HAS_TRAIT(raider, TRAIT_IMMOBILIZED), "The NPC froze without a dilation") + user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) + user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) + +/// An NPC's surge never slows the world, whoever is near +/datum/unit_test/voidcrew_cyberware_timestop/npc_surge + +/datum/unit_test/voidcrew_cyberware_timestop/npc_surge/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), null) + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + TEST_ASSERT_NOTNULL(window, "The lattice's window didn't open") sleep(0.1 SECONDS) - TEST_ASSERT_NULL(window.time_stop, "Time stopped with another player in sight") - TEST_ASSERT(!raider.IsStun(), "The NPC froze without a time stop") - TEST_ASSERT(!(locate(/obj/effect/timestop/sandevistan) in world), "A time stop field spawned with another player in sight") + TEST_ASSERT_NULL(window.time_stop, "A surge with no player behind it slowed the world") user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) From df73bd639fa4aa68f597dbb1f2672839f002a8eb Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Tue, 10 Nov 2026 20:45:05 -0800 Subject: [PATCH 04/20] Run the dilation on its own timer and keep the user's shots in colour --- voidcrew/modules/cyberware/ware_sandy_timestop.dm | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/voidcrew/modules/cyberware/ware_sandy_timestop.dm b/voidcrew/modules/cyberware/ware_sandy_timestop.dm index ae71484ebaf..e7a2435c9c9 100644 --- a/voidcrew/modules/cyberware/ware_sandy_timestop.dm +++ b/voidcrew/modules/cyberware/ware_sandy_timestop.dm @@ -190,6 +190,8 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) var/thaw_at = 0 /// world.time of the last tick, for how much time each tick dilated. var/last_tick = 0 + /// The field's own clock. A timer, not SSfastprocess, which sleeps outside RUNLEVEL_GAME. + var/tick_timer /// Mobs in hand. var/list/held_mobs = list() /// Projectiles in hand, to their own speed. @@ -207,9 +209,13 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) time_scale = start_scale last_tick = world.time . = ..() + STOP_PROCESSING(SSfastprocess, src) // tick() runs on tick_timer instead + tick_timer = addtimer(CALLBACK(src, PROC_REF(tick)), 0.2 SECONDS, TIMER_LOOP | TIMER_STOPPABLE | TIMER_DELETE_ME) RegisterSignal(host, COMSIG_PROJECTILE_FIRER_BEFORE_FIRE, PROC_REF(on_user_fire)) /datum/proximity_monitor/advanced/timestop/sandevistan/Destroy() + deltimer(tick_timer) + tick_timer = null if(host) UnregisterSignal(host, COMSIG_PROJECTILE_FIRER_BEFORE_FIRE) stopper = null @@ -234,6 +240,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) return FALSE user_shots[shot] = TRUE tint = FALSE + escape_the_negative_zone(shot) // in case it was tinted before it had a firer hold_shot(shot) else if((ismachinery(A) && !istype(A, /obj/machinery/light)) || isstructure(A) || ismecha(A)) frozen_structures += A // looks only @@ -380,7 +387,9 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) if(QDELETED(shot) || shot.deletion_queued) return user_shots[shot] = TRUE - hold_shot(shot) + escape_the_negative_zone(shot) // in case it was tinted before it had a firer + if(!held_shots[shot]) + hold_shot(shot) GLOB.cyberware_time_dilated[shot] = src frozen_things[shot] = TRUE RegisterSignal(shot, COMSIG_QDELETING, PROC_REF(held_gone)) @@ -414,6 +423,10 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) thing.throwing.paused = time_scale <= 0 /datum/proximity_monitor/advanced/timestop/sandevistan/process() + return PROCESS_KILL + +/// Every 0.2 s: end if a player came in, push back the timers of everything held, let bosses go, end the freeze. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/tick() if(QDELETED(host) || cyberware_time_stop_witnessed(host)) if(!QDELETED(stopper)) qdel(stopper) // takes this field with it From 5d39c969996c01929d9f05ae398f359c524fdd5d Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Tue, 10 Nov 2026 20:52:40 -0800 Subject: [PATCH 05/20] Keep the user's slow-mo shots in colour and drop deleted things from the field --- .../modules/cyberware/ware_sandy_timestop.dm | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/voidcrew/modules/cyberware/ware_sandy_timestop.dm b/voidcrew/modules/cyberware/ware_sandy_timestop.dm index e7a2435c9c9..98a3190cee3 100644 --- a/voidcrew/modules/cyberware/ware_sandy_timestop.dm +++ b/voidcrew/modules/cyberware/ware_sandy_timestop.dm @@ -252,6 +252,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) A.throwing.paused = TRUE GLOB.cyberware_time_dilated[A] = src frozen_things[A] = TRUE + RegisterSignal(A, COMSIG_QDELETING, PROC_REF(held_gone), override = TRUE) if(tint) into_the_negative_zone(A) SEND_SIGNAL(A, COMSIG_ATOM_TIMESTOP_FREEZE, src) @@ -276,6 +277,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) held_throws -= A frozen_structures -= A user_shots -= A + UnregisterSignal(A, COMSIG_QDELETING) escape_the_negative_zone(A) frozen_things -= A GLOB.cyberware_time_dilated -= A @@ -372,33 +374,50 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) /// Signal proc for [COMSIG_PROJECTILE_FIRER_BEFORE_FIRE] on the user: watch the shot leave the barrel. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/on_user_fire(datum/source, obj/projectile/shot, atom/fired_from, atom/original) SIGNAL_HANDLER - if(time_scale <= 0) - RegisterSignal(shot, COMSIG_PROJECTILE_FIRE, PROC_REF(hang_shot)) + RegisterSignal(shot, COMSIG_PROJECTILE_FIRE, PROC_REF(hang_shot)) -/// Signal proc for [COMSIG_PROJECTILE_FIRE]: in a frozen world the user's shot flies a tile, then hangs. +/// Signal proc for [COMSIG_PROJECTILE_FIRE]: the user's shot keeps its colour. In a frozen world it flies a +/// tile, then hangs; in slow-mo it crawls like everything else. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/hang_shot(obj/projectile/shot) SIGNAL_HANDLER UnregisterSignal(shot, COMSIG_PROJECTILE_FIRE) if(QDELETED(shot) || shot.deletion_queued) return + if(time_scale > 0) + user_shots[shot] = TRUE + escape_the_negative_zone(shot) // in case it was tinted before it had a firer + if(!held_shots[shot]) + hold_shot(shot) + GLOB.cyberware_time_dilated[shot] = src + frozen_things[shot] = TRUE + RegisterSignal(shot, COMSIG_QDELETING, PROC_REF(held_gone), override = TRUE) + return // A hitscan beam takes its first step itself once paused (process_hitscan) and stops there. if(!shot.hitscan) + shot.paused = FALSE // it may have been caught before it had a firer shot.process_movement(SANDY_STOP_SHOT_HANG) if(QDELETED(shot) || shot.deletion_queued) return user_shots[shot] = TRUE escape_the_negative_zone(shot) // in case it was tinted before it had a firer - if(!held_shots[shot]) + if(held_shots[shot]) + shot.paused = TRUE + else hold_shot(shot) GLOB.cyberware_time_dilated[shot] = src frozen_things[shot] = TRUE - RegisterSignal(shot, COMSIG_QDELETING, PROC_REF(held_gone)) + RegisterSignal(shot, COMSIG_QDELETING, PROC_REF(held_gone), override = TRUE) /// Forgets a held thing that got deleted. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/held_gone(atom/movable/thing) SIGNAL_HANDLER held_shots -= thing user_shots -= thing + held_mobs -= thing + frozen_mobs -= thing + held_throws -= thing + boss_holds -= thing + frozen_structures -= thing frozen_things -= thing GLOB.cyberware_time_dilated -= thing From e29bb287f8caebb3d082eb17647258512a0e31d0 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Tue, 10 Nov 2026 21:59:56 -0800 Subject: [PATCH 06/20] Slow do_after, queued bursts, mechs, turrets and grenade fuses in the dilation field --- code/__HELPERS/mobs.dm | 8 + code/datums/components/ranged_attacks.dm | 6 +- code/game/objects/items/grenades/_grenade.dm | 2 +- code/modules/projectiles/gun.dm | 14 +- code/modules/vehicles/mecha/_mecha.dm | 2 +- .../mecha/equipment/mecha_equipment.dm | 2 +- .../modules/cyberware/ware_sandy_timestop.dm | 264 +++++++++++++++++- .../unit_tests/voidcrew_cyberware_timestop.dm | 74 +++++ 8 files changed, 357 insertions(+), 15 deletions(-) diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm index 252ed732ca4..06f5da9acea 100644 --- a/code/__HELPERS/mobs.dm +++ b/code/__HELPERS/mobs.dm @@ -246,10 +246,18 @@ GLOBAL_LIST_INIT(skin_tone_names, list( var/endtime = world.time + delay var/starttime = world.time + var/dilation_tick = world.time // VOIDCREW EDIT ADDITION - cyberware time dilation . = TRUE while (world.time < endtime) stoplag(1) + // VOIDCREW EDIT ADDITION START - cyberware time dilation (voidcrew/modules/cyberware/ware_sandy_timestop.dm): a slowed body works slower, a frozen one not at all + var/dilation_lost = cyberware_time_lost(user, world.time - dilation_tick) + dilation_tick = world.time + endtime += dilation_lost + starttime += dilation_lost + // VOIDCREW EDIT ADDITION END + if(!QDELETED(progbar)) progbar.update(world.time - starttime) diff --git a/code/datums/components/ranged_attacks.dm b/code/datums/components/ranged_attacks.dm index b998362cfc6..787c3b0b527 100644 --- a/code/datums/components/ranged_attacks.dm +++ b/code/datums/components/ranged_attacks.dm @@ -66,8 +66,12 @@ INVOKE_ASYNC(src, PROC_REF(async_fire_ranged_attack), firer, target, modifiers) if(isnull(burst_shots)) return + // VOIDCREW EDIT CHANGE START - cyberware time dilation (voidcrew/modules/cyberware/ware_sandy_timestop.dm): each burst shot waits for the mob's own clock, so a slowed mob's burst slows down too + cyberware_burst_start = cyberware_burst_clock(firer) for(var/i in 1 to (burst_shots - 1)) - addtimer(CALLBACK(src, PROC_REF(async_fire_ranged_attack), firer, target, modifiers), i * burst_intervals) + addtimer(CALLBACK(src, PROC_REF(cyberware_burst_shot), firer, target, modifiers, i), i * burst_intervals) + // ORIGINAL: addtimer(CALLBACK(src, PROC_REF(async_fire_ranged_attack), firer, target, modifiers), i * burst_intervals) + // VOIDCREW EDIT CHANGE END /// Actually fire the damn thing /datum/component/ranged_attacks/proc/async_fire_ranged_attack(mob/living/basic/firer, atom/target, modifiers) diff --git a/code/game/objects/items/grenades/_grenade.dm b/code/game/objects/items/grenades/_grenade.dm index 262a493cc75..dc7317e696c 100644 --- a/code/game/objects/items/grenades/_grenade.dm +++ b/code/game/objects/items/grenades/_grenade.dm @@ -166,7 +166,7 @@ active = TRUE icon_state = (base_icon_state || initial(icon_state)) + "_active" SEND_SIGNAL(src, COMSIG_GRENADE_ARMED, det_time, delayoverride) - addtimer(CALLBACK(src, PROC_REF(detonate)), isnull(delayoverride)? det_time : delayoverride) + addtimer(CALLBACK(src, PROC_REF(cyberware_fuse_done)), isnull(delayoverride)? det_time : delayoverride) // VOIDCREW EDIT CHANGE - cyberware time dilation (voidcrew/modules/cyberware/ware_sandy_timestop.dm): the fuse burns slow in a slowed field. ORIGINAL: addtimer(CALLBACK(src, PROC_REF(detonate)), isnull(delayoverride)? det_time : delayoverride) /** * detonate (formerly prime) refers to when the grenade actually delivers its payload (whether or not a boom/bang/detonation is involved) diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index 371efe8e779..52ff771aac3 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -448,9 +448,10 @@ if(!user || !firing_burst) firing_burst = FALSE return FALSE - // VOIDCREW EDIT ADDITION START - cyberware time dilation (voidcrew/modules/cyberware/ware_sandy_timestop.dm): a burst queued before the world froze waits for it to move again - if(!cyberware_time_scale(user)) - addtimer(CALLBACK(src, PROC_REF(process_burst), user, target, message, params, zone_override, random_spread, burst_spread_mult, iteration), 1) + // VOIDCREW EDIT ADDITION START - cyberware time dilation (voidcrew/modules/cyberware/ware_sandy_timestop.dm): each burst shot waits for the shooter's own clock, so a burst slows or stops with them + var/dilation_wait = cyberware_burst_wait(user, cyberware_burst_start, cyberware_burst_gap, iteration - 1) + if(dilation_wait > 0) + addtimer(CALLBACK(src, PROC_REF(process_burst), user, target, message, params, zone_override, random_spread, burst_spread_mult, iteration), dilation_wait) return TRUE // VOIDCREW EDIT ADDITION END if(!issilicon(user)) @@ -523,10 +524,9 @@ modified_burst_delay = ROUND_UP(burst_delay * 0.5) modified_fire_delay = ROUND_UP(fire_delay * 0.5) // VOIDCREW EDIT ADDITION START - cyberware time dilation (voidcrew/modules/cyberware/ware_sandy_timestop.dm): a slowed shooter's gun cycles slowly too - var/dilation = cyberware_time_scale(user) - if(dilation < 1) - modified_burst_delay /= max(dilation, 0.1) - modified_fire_delay /= max(dilation, 0.1) + cyberware_burst_start = cyberware_burst_clock(user) + cyberware_burst_gap = modified_burst_delay + modified_fire_delay = cyberware_dilated_time(user, modified_fire_delay) // VOIDCREW EDIT ADDITION END if(burst_size > 1) diff --git a/code/modules/vehicles/mecha/_mecha.dm b/code/modules/vehicles/mecha/_mecha.dm index 4c9cc36adb4..b2e64b15edf 100644 --- a/code/modules/vehicles/mecha/_mecha.dm +++ b/code/modules/vehicles/mecha/_mecha.dm @@ -708,7 +708,7 @@ SEND_SIGNAL(user, COMSIG_MOB_USED_CLICK_MECH_MELEE, src) if(target.mech_melee_attack(src, user)) - TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_MELEE_ATTACK, melee_cooldown) + TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_MELEE_ATTACK, cyberware_dilated_time(src, melee_cooldown)) // VOIDCREW EDIT CHANGE - cyberware time dilation (voidcrew/modules/cyberware/ware_sandy_timestop.dm). ORIGINAL: TIMER_COOLDOWN_START(src, COOLDOWN_MECHA_MELEE_ATTACK, melee_cooldown) /// Driver alt clicks anything while in mech /obj/vehicle/sealed/mecha/proc/on_click_alt(mob/user, atom/target, params) diff --git a/code/modules/vehicles/mecha/equipment/mecha_equipment.dm b/code/modules/vehicles/mecha/equipment/mecha_equipment.dm index c03240b4a66..223f5d09c50 100644 --- a/code/modules/vehicles/mecha/equipment/mecha_equipment.dm +++ b/code/modules/vehicles/mecha/equipment/mecha_equipment.dm @@ -114,7 +114,7 @@ return TRUE /obj/item/mecha_parts/mecha_equipment/proc/action(mob/source, atom/target, list/modifiers) - TIMER_COOLDOWN_START(chassis, COOLDOWN_MECHA_EQUIPMENT(type), equip_cooldown)//Cooldown is on the MECH so people dont bypass it by switching equipment + TIMER_COOLDOWN_START(chassis, COOLDOWN_MECHA_EQUIPMENT(type), cyberware_dilated_time(chassis, equip_cooldown)) // VOIDCREW EDIT CHANGE - cyberware time dilation (voidcrew/modules/cyberware/ware_sandy_timestop.dm): a slowed mech's weapons cycle slowly. ORIGINAL: TIMER_COOLDOWN_START(chassis, COOLDOWN_MECHA_EQUIPMENT(type), equip_cooldown)//Cooldown is on the MECH so people dont bypass it by switching equipment SEND_SIGNAL(source, COMSIG_MOB_USED_MECH_EQUIPMENT, chassis) chassis.use_energy(energy_drain) return TRUE diff --git a/voidcrew/modules/cyberware/ware_sandy_timestop.dm b/voidcrew/modules/cyberware/ware_sandy_timestop.dm index 98a3190cee3..2d13d482e49 100644 --- a/voidcrew/modules/cyberware/ware_sandy_timestop.dm +++ b/voidcrew/modules/cyberware/ware_sandy_timestop.dm @@ -20,9 +20,19 @@ * TRAIT_AI_PAUSED, never a fake state or a Stun. A slowed mob moves under a movespeed modifier, and every * tick the field pushes its timing deadlines back by the time it lost (next_move, ability cooldowns, AI * behaviour cooldowns and the blackboard deadlines listed in dilated_blackboard_keys(), ranged cooldowns). - * Guns scale their fire and burst delays for a slowed shooter (VOIDCREW EDIT in gun.dm). Frozen and slowed - * bodies take damage, wounds and bleeding through tg as normal. Not covered: do_after progress bars and - * burst timers queued before the slow-mo started. + * Frozen and slowed bodies take damage, wounds and bleeding through tg as normal. + * + * Everything else in the field runs on the same slowed clock, through a few helpers below: + * - cyberware_dilated_time() and cyberware_dilated_steps() turn a length of local time into real time, exact + * through the freeze, the slow-mo and the end. Hyperviolence gore uses them (flinches, death throes, blood + * paint, flying gibs), and so do a mech's weapon and melee cooldowns and a gun's fire delay. + * - cyberware_time_lost() stretches do_after for a slowed body (VOIDCREW EDIT in do_after). + * - Burst shots wait on a burst clock the field pushes back (cyberware_burst_wait()), so a burst queued before + * the slow-mo slows with it: guns (gun.dm) and basic mob ranged attacks (ranged_attacks.dm). + * - The field pushes back mech move cooldowns, turret fire cooldowns and lit grenade fuses (_grenade.dm), and + * shuts a mech down for the freeze. + * Things in hand stay held until the effect ends; gore and blood that merely stand in the field are slowed + * while they are within its range. The user and anything the user carries are never slowed. * * Bosses (a finisher user, or a boss-rank human NPC) are held for SANDY_STOP_BOSS_HOLD at most. * @@ -54,6 +64,95 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) var/datum/proximity_monitor/advanced/timestop/sandevistan/field = GLOB.cyberware_time_dilated[thing] return field ? field.time_scale : 1 +/// Every dilation field running now. +GLOBAL_LIST_EMPTY(cyberware_time_fields) + +/** + * The field `thing` runs slow in: the one holding it, or one it stands inside (blood, gibs, a grenade on the + * floor). Never for the user, anything the user carries, or a living thing the field didn't take. + */ +/proc/cyberware_time_field(atom/thing) + if(isnull(thing) || !length(GLOB.cyberware_time_fields)) + return null + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = GLOB.cyberware_time_dilated[thing] + if(field) + return field + if(isliving(thing)) + return null + var/turf/here = get_turf(thing) + if(!here) + return null + var/atom/on_turf = ismovable(thing) ? get_atom_on_turf(thing) : thing + for(field as anything in GLOB.cyberware_time_fields) + if(field.immune[thing] || field.immune[on_turf] || field.released[on_turf]) + continue + var/turf/centre = get_turf(field.host) + if(centre && centre.z == here.z && get_dist(centre, here) <= field.current_range) + return field + return null + +/// How long, in real time, `time` deciseconds of `thing`'s own time take from now. Exact through the freeze, +/// the slow-mo and the end of the effect, as the field stands now. +/proc/cyberware_dilated_time(atom/thing, time) + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = cyberware_time_field(thing) + if(!field) + return time + return field.real_time_for(thing, time) + +/** + * Real-time lengths for a chain of animation steps `times` on `thing`: list(hold, step 1, step 2, ...), or null + * when it isn't slowed. `hold` is how long it sits still first (the freeze). A looping chain just plays at the + * field's pace. + */ +/proc/cyberware_dilated_steps(atom/thing, list/times, loop = 1) + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = cyberware_time_field(thing) + if(!field) + return null + var/list/real = list() + if(loop != 1) + var/scale = field.time_scale > 0 ? field.time_scale : field.slow_scale + real += 0 + for(var/time in times) + real += time / max(scale, 0.05) + return real + var/hold = field.real_time_for(thing, 0) + real += hold + var/local = 0 + var/reached = hold + for(var/time in times) + local += time + var/at = field.real_time_for(thing, local) + real += max(at - reached, 0) + reached = at + return real + +/// Time `thing` lost in the last `elapsed` deciseconds: all of it frozen, none of it outside a field. +/proc/cyberware_time_lost(atom/thing, elapsed) + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = GLOB.cyberware_time_dilated[thing] + if(!field || elapsed <= 0) + return 0 + return elapsed * (1 - field.time_scale) + +/// Time a held `thing` has lost since the field last pushed its deadlines back. +/proc/cyberware_time_lag(atom/thing) + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = GLOB.cyberware_time_dilated[thing] + if(!field) + return 0 + return (world.time - field.last_tick) * (1 - field.time_scale) + +/// A burst clock's start for a burst beginning now. The field pushes it back with the shooter's deadlines. +/proc/cyberware_burst_clock(atom/shooter) + return world.time - cyberware_time_lag(shooter) + +/// How much longer shot `index` (0 for the first) of a burst begun at `start`, `gap` apart, has to wait. +/proc/cyberware_burst_wait(atom/shooter, start, gap, index) + if(!GLOB.cyberware_time_dilated[shooter]) + return max(start + gap * index - world.time, 0) + if(!cyberware_time_scale(shooter)) + return world.tick_lag + var/due = start + cyberware_time_lag(shooter) + gap * index + return due - world.time >= world.tick_lag ? due - world.time : 0 + /// Whether a player other than `user` is within `range` tiles: any living body with a ckey, connected or not. /proc/cyberware_time_stop_witnessed(mob/living/user, range = SANDY_STOP_WITNESS_RANGE) var/turf/here = get_turf(user) @@ -173,6 +272,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) field.slow_scale = slow_scale if(freeze_time > 0) field.thaw_at = world.time + freeze_time + field.ends_at = world.time + duration chronofield = field // ========================================================================= @@ -204,10 +304,19 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) var/list/boss_holds = list() /// Bosses that already shrugged it off. The field leaves them be. var/list/released = list() + /// world.time the effect ends, when time runs normally again. + var/ends_at = INFINITY + /// Mechs held, to whether the field shut them down for the freeze. + var/list/held_mechs = list() + /// Turrets held. Their fire cooldown runs slow. + var/list/held_turrets = list() + /// Armed grenades held. Their fuse runs slow. + var/list/held_grenades = list() /datum/proximity_monitor/advanced/timestop/sandevistan/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, list/immune, antimagic_flags, channelled, start_scale = 0) time_scale = start_scale last_tick = world.time + GLOB.cyberware_time_fields += src . = ..() STOP_PROCESSING(SSfastprocess, src) // tick() runs on tick_timer instead tick_timer = addtimer(CALLBACK(src, PROC_REF(tick)), 0.2 SECONDS, TIMER_LOOP | TIMER_STOPPABLE | TIMER_DELETE_ME) @@ -219,9 +328,32 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) if(host) UnregisterSignal(host, COMSIG_PROJECTILE_FIRER_BEFORE_FIRE) stopper = null + GLOB.cyberware_time_fields -= src . = ..() // unfreeze_all() lets everything go released.Cut() +/// How long, in real time from now, `local` deciseconds of `thing`'s own time take: the rest of the freeze, +/// then the slow-mo until the effect (or a boss's hold) ends, then real time. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/real_time_for(atom/thing, local) + var/now = world.time + var/ends = ends_at + if(boss_holds[thing]) + ends = min(ends, boss_holds[thing]) + if(ends <= now) + return local + var/at = now + var/scale = time_scale + if(scale <= 0) + at = thaw_at ? min(thaw_at, ends) : ends + scale = slow_scale + if(at < ends && scale > 0) + var/span = (ends - at) * scale + if(local <= span) + return at - now + local / scale + local -= span + at = ends + return at - now + local + /datum/proximity_monitor/advanced/timestop/sandevistan/freeze_atom(atom/movable/A) if(!istype(A) || GLOB.cyberware_time_dilated[A] || global_frozen_atoms[A] || released[A] || immune[A]) return FALSE @@ -242,8 +374,18 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) tint = FALSE escape_the_negative_zone(shot) // in case it was tinted before it had a firer hold_shot(shot) - else if((ismachinery(A) && !istype(A, /obj/machinery/light)) || isstructure(A) || ismecha(A)) + else if(ismecha(A)) + hold_mech(A) + else if(istype(A, /obj/machinery/porta_turret)) + frozen_structures += A + held_turrets[A] = TRUE + if(time_scale <= 0) + hold_turret_fire(A) + else if((ismachinery(A) && !istype(A, /obj/machinery/light)) || isstructure(A)) frozen_structures += A // looks only + else if(cyberware_lit_grenade(A)) + held_grenades[A] = TRUE + tint = !!A.throwing // floor items keep their colour else if(!A.throwing) return FALSE if(A.throwing) @@ -275,6 +417,11 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) if(throw_datum) throw_datum.paused = FALSE held_throws -= A + if(!isnull(held_mechs[A])) + start_mech(A) + held_mechs -= A + held_turrets -= A + held_grenades -= A frozen_structures -= A user_shots -= A UnregisterSignal(A, COMSIG_QDELETING) @@ -357,6 +504,54 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) if(animal_victim.ranged_cooldown > now) animal_victim.ranged_cooldown += push + for(var/obj/item/gun/gun in victim.held_items) + gun.cyberware_burst_start += push + if(ranged) + ranged.cyberware_burst_start += push + +// ----- mechs, turrets, grenades ----- + +/// Takes hold of a mech and whoever is driving it. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/hold_mech(obj/vehicle/sealed/mecha/mech) + frozen_structures += mech + held_mechs[mech] = FALSE + if(time_scale <= 0) + stop_mech(mech) + for(var/mob/living/pilot in mech.occupants) + freeze_atom(pilot) + +/// The freeze shuts a mech down, as tg's own time stop does. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/stop_mech(obj/vehicle/sealed/mecha/mech) + if(held_mechs[mech] || mech.completely_disabled) + return + mech.completely_disabled = TRUE + held_mechs[mech] = TRUE + +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/start_mech(obj/vehicle/sealed/mecha/mech) + if(!held_mechs[mech]) + return + mech.completely_disabled = FALSE + held_mechs[mech] = FALSE + +/// A frozen turret's cooldown stays a little ahead of the clock, so it can't fire until time moves. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/hold_turret_fire(obj/machinery/porta_turret/turret) + turret.last_fired = max(turret.last_fired, world.time + 0.3 SECONDS - turret.shot_delay) + +/// Pushes back the clocks of held machines and grenades by `push` deciseconds. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/push_machine_deadlines(push) + var/now = world.time + for(var/obj/vehicle/sealed/mecha/mech as anything in held_mechs) + if(mech.cooldown_vehicle_move > now) + mech.cooldown_vehicle_move += push + for(var/obj/machinery/porta_turret/turret as anything in held_turrets) + if(turret.last_fired + turret.shot_delay > now) + turret.last_fired += push + if(time_scale <= 0) + hold_turret_fire(turret) + for(var/obj/item/grenade/bomb as anything in held_grenades) + if(bomb.cyberware_fuse_at > now) + bomb.cyberware_fuse_at += push + // ----- projectiles ----- /datum/proximity_monitor/advanced/timestop/sandevistan/proc/hold_shot(obj/projectile/shot) @@ -416,6 +611,9 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) held_mobs -= thing frozen_mobs -= thing held_throws -= thing + held_mechs -= thing + held_turrets -= thing + held_grenades -= thing boss_holds -= thing frozen_structures -= thing frozen_things -= thing @@ -440,6 +638,11 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) for(var/atom/movable/thing as anything in held_throws) if(thing.throwing) thing.throwing.paused = time_scale <= 0 + for(var/obj/vehicle/sealed/mecha/mech as anything in held_mechs) + if(time_scale <= 0) + stop_mech(mech) + else + start_mech(mech) /datum/proximity_monitor/advanced/timestop/sandevistan/process() return PROCESS_KILL @@ -460,6 +663,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) var/datum/thrownthing/throw_datum = thing.throwing if(throw_datum && !throw_datum.paused) throw_datum.start_time += push + push_machine_deadlines(push) for(var/mob/living/boss as anything in boss_holds.Copy()) if(world.time < boss_holds[boss]) continue @@ -539,6 +743,58 @@ GLOBAL_LIST_EMPTY(cyberware_time_dilated) /obj/effect/temp_visual/decoy/fading/cascade/stopped duration = 4 +// ========================================================================= +// CLOCKS THAT RUN SLOW +// ========================================================================= + +/obj/item/gun + /// world.time the current burst began, pushed back while the shooter is slowed (cyberware time dilation). + var/cyberware_burst_start = 0 + /// The gap between the current burst's shots, in the shooter's own time. + var/cyberware_burst_gap = 0 + +/datum/component/ranged_attacks + /// world.time the current burst began, pushed back while the mob is slowed (cyberware time dilation). + var/cyberware_burst_start = 0 + +/// One shot of a burst (`index` 1 is the second shot), fired when the mob's own clock gets there. +/datum/component/ranged_attacks/proc/cyberware_burst_shot(mob/living/basic/firer, atom/target, modifiers, index) + if(QDELETED(firer)) + return + var/wait = cyberware_burst_wait(firer, cyberware_burst_start, burst_intervals, index) + if(wait > 0) + addtimer(CALLBACK(src, PROC_REF(cyberware_burst_shot), firer, target, modifiers, index), wait, TIMER_DELETE_ME) + return + async_fire_ranged_attack(firer, target, modifiers) + +/obj/item/grenade + /// world.time the fuse runs out, pushed back while the grenade sits in a dilation field. 0 when not lit. + var/cyberware_fuse_at = 0 + +/// Whether `thing` is a grenade with its fuse burning. +/proc/cyberware_lit_grenade(atom/thing) + if(!istype(thing, /obj/item/grenade)) + return FALSE + var/obj/item/grenade/bomb = thing + return bomb.active && bomb.cyberware_fuse_at + +/obj/item/grenade/arm_grenade(mob/user, delayoverride, msg = TRUE, volume = 60) + cyberware_fuse_at = world.time + (isnull(delayoverride) ? det_time : delayoverride) + . = ..() + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = cyberware_time_field(src) + field?.freeze_atom(src) + +/// The fuse timer ran out: go off, unless the grenade's own clock is still behind. +/obj/item/grenade/proc/cyberware_fuse_done() + if(!cyberware_fuse_at) + return + var/wait = cyberware_fuse_at + cyberware_time_lag(src) - world.time + if(wait >= world.tick_lag || (GLOB.cyberware_time_dilated[src] && !cyberware_time_scale(src))) + addtimer(CALLBACK(src, PROC_REF(cyberware_fuse_done)), max(wait, world.tick_lag), TIMER_DELETE_ME) + return + cyberware_fuse_at = 0 + detonate() + #undef SANDY_STOP_WITNESS_RANGE #undef SANDY_STOP_FIELD_RANGE #undef SANDY_STOP_BOSS_HOLD diff --git a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm index b4d09339dae..598188abd1c 100644 --- a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm +++ b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm @@ -105,3 +105,77 @@ TEST_ASSERT_NULL(window.time_stop, "A surge with no player behind it slowed the world") user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) + +/// Everything in the field runs on its own slowed clock: gore timing, do_after, bursts, mechs, turrets, fuses +/datum/unit_test/voidcrew_cyberware_timestop/slow_everything + +/datum/unit_test/voidcrew_cyberware_timestop/slow_everything/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/mob/living/carbon/human/npc/raider = allocate(/mob/living/carbon/human/npc, spot(2, 0)) + var/obj/item/gun/ballistic/automatic/pistol/raider_gun = allocate(/obj/item/gun/ballistic/automatic/pistol) + raider.put_in_hands(raider_gun) + var/obj/item/gun/ballistic/automatic/pistol/user_gun = allocate(/obj/item/gun/ballistic/automatic/pistol) + user.put_in_hands(user_gun) + var/mob/living/basic/mouse/rat = allocate(/mob/living/basic/mouse, spot(1, 2)) + rat.AddComponent(/datum/component/ranged_attacks, projectile_type = /obj/projectile/beam, burst_shots = 3) + var/obj/vehicle/sealed/mecha/ripley/mech = allocate(/obj/vehicle/sealed/mecha/ripley, spot(3, 2)) + var/obj/machinery/porta_turret/turret = allocate(/obj/machinery/porta_turret, spot(3, 1)) + var/obj/effect/decal/cleanable/blood/pool = allocate(/obj/effect/decal/cleanable/blood, spot(2, 1)) + var/obj/item/grenade/bomb = allocate(/obj/item/grenade, spot(1, 1)) + bomb.arm_grenade(null, 3 SECONDS, msg = FALSE) + + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + TEST_ASSERT_NOTNULL(window, "The lattice's window didn't open") + sleep(0.1 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield + + // The freeze: the NPC's own clock stands still, the user's doesn't. + TEST_ASSERT_EQUAL(cyberware_time_lost(raider, 10), 10, "A frozen NPC's clock kept running") + TEST_ASSERT_EQUAL(cyberware_time_lost(user, 10), 0, "The user's clock slowed") + TEST_ASSERT_EQUAL(cyberware_dilated_time(user, 10), 10, "The user's animations would be slowed") + TEST_ASSERT_NULL(cyberware_time_field(user_gun), "Something the user holds would be slowed") + TEST_ASSERT(cyberware_dilated_time(raider, 1) > 5, "A frozen NPC's gore wouldn't wait out the freeze") + TEST_ASSERT_NOTNULL(cyberware_time_field(pool), "Blood on the floor in the field isn't slowed") + var/list/steps = cyberware_dilated_steps(raider, list(2, 2)) + TEST_ASSERT(length(steps) == 3 && steps[1] > 5 && steps[2] > 2, "A frozen NPC's flinch wouldn't hold, then play slow") + + // Bursts queued before the freeze wait for it. + raider_gun.cyberware_burst_start = cyberware_burst_clock(raider) + raider_gun.cyberware_burst_gap = 1 + var/datum/component/ranged_attacks/rat_gun = rat.GetComponent(/datum/component/ranged_attacks) + rat_gun.cyberware_burst_start = cyberware_burst_clock(rat) + var/gun_start = raider_gun.cyberware_burst_start + var/rat_start = rat_gun.cyberware_burst_start + + // Machines and fuses. + TEST_ASSERT(mech.completely_disabled, "A mech kept running in frozen time") + TEST_ASSERT(turret.last_fired + turret.shot_delay > world.time, "A turret could fire in frozen time") + TEST_ASSERT(field.held_grenades[bomb], "A lit grenade in the field wasn't taken") + + sleep(0.5 SECONDS) + TEST_ASSERT(cyberware_burst_wait(raider, raider_gun.cyberware_burst_start, 1, 1) > 0, "A queued burst shot fired in frozen time") + TEST_ASSERT(raider_gun.cyberware_burst_start > gun_start, "The NPC's burst clock wasn't pushed back") + TEST_ASSERT(rat_gun.cyberware_burst_start > rat_start, "A basic mob's burst clock wasn't pushed back") + TEST_ASSERT(turret.last_fired + turret.shot_delay > world.time, "A turret could fire in frozen time") + + sleep(0.6 SECONDS) // into the 20% slow-mo + TEST_ASSERT(!mech.completely_disabled, "A mech stayed shut down into the slow-mo") + var/lost = cyberware_time_lost(raider, 10) + TEST_ASSERT(lost > 7.9 && lost < 8.1, "A slowed NPC's clock didn't run at 20% (lost [lost] of 10)") + mech.cooldown_vehicle_move = world.time + 5 + var/move_at = mech.cooldown_vehicle_move + sleep(0.5 SECONDS) + TEST_ASSERT(mech.cooldown_vehicle_move > move_at, "A slowed mech's move cooldown wasn't pushed back") + TEST_ASSERT(!(bomb.dud_flags & GRENADE_USED), "A grenade's fuse burned through at full speed") + + sleep(2.6 SECONDS) // past the end + TEST_ASSERT_NULL(window.time_stop, "The dilation outlived its 4 s") + TEST_ASSERT_EQUAL(cyberware_time_lost(raider, 10), 0, "The NPC's clock stayed slow after the dilation") + TEST_ASSERT_NULL(cyberware_time_field(pool), "Blood stayed slowed after the dilation") + TEST_ASSERT(!mech.completely_disabled, "The mech stayed shut down after the dilation") + TEST_ASSERT(!(bomb.dud_flags & GRENADE_USED), "A grenade's fuse didn't lose its frozen and slowed time") + TEST_ASSERT(bomb.cyberware_fuse_at > world.time, "A grenade's fuse has nothing left after losing time") + bomb.cyberware_fuse_at = 0 // don't let it go off after the test + user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) + user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) From c865602dbe2ed5529514b1ce0810abc88d974ed6 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Tue, 10 Nov 2026 21:59:56 -0800 Subject: [PATCH 07/20] Slow gore animation in the dilation field --- voidcrew/modules/hyperviolence/_core.dm | 19 +++++-- .../hyperviolence/visuals/_motion_core.dm | 57 +++++++++++++++---- .../hyperviolence/visuals/blood_motion.dm | 11 +++- .../hyperviolence/visuals/death_falls.dm | 4 +- .../hyperviolence/visuals/execution_motion.dm | 8 ++- .../hyperviolence/visuals/flying_gore.dm | 20 ++++--- 6 files changed, 90 insertions(+), 29 deletions(-) diff --git a/voidcrew/modules/hyperviolence/_core.dm b/voidcrew/modules/hyperviolence/_core.dm index 2aafc284d62..f28b6a2c84a 100644 --- a/voidcrew/modules/hyperviolence/_core.dm +++ b/voidcrew/modules/hyperviolence/_core.dm @@ -37,17 +37,26 @@ var/matrix/fallen = matrix(transform) var/matrix/upright = matrix(transform) upright.Turn(-angle) + // In a Sandevistan field (cyberware/ware_sandy_timestop.dm) it holds through the freeze and runs slow. + var/list/real = cyberware_dilated_steps(src, list(HV_DEATH_THROES_TIME, UPDATE_TRANSFORM_ANIMATION_TIME)) || list(0, HV_DEATH_THROES_TIME, UPDATE_TRANSFORM_ANIMATION_TIME) animate(src, transform = upright, time = 0) - animate(transform = upright, time = HV_DEATH_THROES_TIME) - animate(transform = fallen, time = UPDATE_TRANSFORM_ANIMATION_TIME, easing = EASE_IN|EASE_OUT) + animate(transform = upright, time = real[1] + real[2]) + animate(transform = fallen, time = real[3], easing = EASE_IN|EASE_OUT) // The shudder: sideways offsets that die down, each step relative to the last, ending where it began. var/static/list/shudder = list(3, -3, 2, -2, 2, -1, 1, 0) var/step_time = HV_DEATH_THROES_TIME / length(shudder) + var/list/shudder_times = list() + for(var/i in 1 to length(shudder)) + shudder_times += step_time + var/list/shudder_real = cyberware_dilated_steps(src, shudder_times) + if(shudder_real && shudder_real[1] > 0) + animate(src, pixel_w = 0, time = shudder_real[1], flags = ANIMATION_RELATIVE|ANIMATION_PARALLEL) var/offset = 0 for(var/i in 1 to length(shudder)) var/shift = shudder[i] - offset offset = shudder[i] - if(i == 1) - animate(src, pixel_w = shift, time = step_time, flags = ANIMATION_RELATIVE|ANIMATION_PARALLEL) + var/time = shudder_real ? shudder_real[i + 1] : step_time + if(i == 1 && !(shudder_real && shudder_real[1] > 0)) + animate(src, pixel_w = shift, time = time, flags = ANIMATION_RELATIVE|ANIMATION_PARALLEL) else - animate(pixel_w = shift, time = step_time, flags = ANIMATION_RELATIVE) + animate(pixel_w = shift, time = time, flags = ANIMATION_RELATIVE) diff --git a/voidcrew/modules/hyperviolence/visuals/_motion_core.dm b/voidcrew/modules/hyperviolence/visuals/_motion_core.dm index 9b45e989263..06986535e34 100644 --- a/voidcrew/modules/hyperviolence/visuals/_motion_core.dm +++ b/voidcrew/modules/hyperviolence/visuals/_motion_core.dm @@ -202,12 +202,30 @@ /atom/proc/hv_vis_play(tag, list/steps, loop = 1) if(!length(steps)) return + // In a Sandevistan field (cyberware/ware_sandy_timestop.dm) it plays at the field's pace, and holds still + // through the freeze once any jump to its first pose is done. + var/list/times = list() + for(var/list/step as anything in steps) + times += step[4] + var/list/real = cyberware_dilated_steps(src, times, loop) + var/hold = real ? real[1] : 0 + var/started = FALSE for(var/i in 1 to length(steps)) var/list/step = steps[i] - if(i == 1) - animate(src, pixel_w = step[1], pixel_z = step[2], transform = step[3], time = step[4], easing = step[5], loop = loop, tag = tag) + if(hold > 0 && step[4] > 0) + var/list/still = i > 1 ? steps[i - 1] : list(pixel_w, pixel_z, transform) + if(!started) + animate(src, pixel_w = still[1], pixel_z = still[2], transform = still[3], time = hold, loop = loop, tag = tag) + started = TRUE + else + animate(pixel_w = still[1], pixel_z = still[2], transform = still[3], time = hold) + hold = 0 + var/time = real ? real[i + 1] : step[4] + if(!started) + animate(src, pixel_w = step[1], pixel_z = step[2], transform = step[3], time = time, easing = step[5], loop = loop, tag = tag) + started = TRUE else - animate(pixel_w = step[1], pixel_z = step[2], transform = step[3], time = step[4], easing = step[5]) + animate(pixel_w = step[1], pixel_z = step[2], transform = step[3], time = time, easing = step[5]) /** * Plays steps that take over this atom's whole animation: each step is list(pixel_y, transform, time, @@ -218,15 +236,33 @@ /atom/proc/hv_vis_play_over(list/steps) if(!length(steps)) return + // Slowed and held like hv_vis_play() in a Sandevistan field. + var/list/times = list() + for(var/list/step as anything in steps) + times += step[3] + var/list/real = cyberware_dilated_steps(src, times) + var/hold = real ? real[1] : 0 + var/started = FALSE for(var/i in 1 to length(steps)) var/list/step = steps[i] - if(i == 1) - animate(src, pixel_y = step[1], transform = step[2], time = step[3], easing = step[4]) + if(hold > 0 && step[3] > 0) + var/list/still = i > 1 ? steps[i - 1] : list(pixel_y, transform) + if(!started) + animate(src, pixel_y = still[1], transform = still[2], time = hold) + started = TRUE + else + animate(pixel_y = still[1], transform = still[2], time = hold) + hold = 0 + var/time = real ? real[i + 1] : step[3] + if(!started) + animate(src, pixel_y = step[1], transform = step[2], time = time, easing = step[4]) + started = TRUE else - animate(pixel_y = step[1], transform = step[2], time = step[3], easing = step[4]) + animate(pixel_y = step[1], transform = step[2], time = time, easing = step[4]) /// Eases a named sequence back to where the atom really is over `time`, from wherever it has got to. /atom/proc/hv_vis_release(tag, time = 2) + time = cyberware_dilated_time(src, time) animate(src, pixel_w = pixel_w, pixel_z = pixel_z, transform = transform, time = time, easing = SINE_EASING, tag = tag) /// Stops named sequences at once. @@ -288,10 +324,11 @@ var/flash = get_filter(HV_VIS_FLASH_FILTER) if(!flash) return - animate(flash, color = list(0.6,0,0, 0,0.6,0, 0,0,0.6, 0.4,0.4,0.4), time = 0.5, easing = LINEAR_EASING) - animate(color = list(0.6,0,0, 0,0.6,0, 0,0,0.6, 0.4,0.4,0.4), time = HV_VIS_HITSTOP_TIME, easing = LINEAR_EASING) - animate(color = list(1,0,0, 0,1,0, 0,0,1, 0,0,0), time = 1, easing = LINEAR_EASING) - addtimer(CALLBACK(src, TYPE_PROC_REF(/datum, remove_filter), HV_VIS_FLASH_FILTER), 0.5 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_DELETE_ME) + var/list/real = cyberware_dilated_steps(src, list(0.5, HV_VIS_HITSTOP_TIME, 1)) || list(0, 0.5, HV_VIS_HITSTOP_TIME, 1) + animate(flash, color = list(0.6,0,0, 0,0.6,0, 0,0,0.6, 0.4,0.4,0.4), time = real[1] + real[2], easing = LINEAR_EASING) + animate(color = list(0.6,0,0, 0,0.6,0, 0,0,0.6, 0.4,0.4,0.4), time = real[3], easing = LINEAR_EASING) + animate(color = list(1,0,0, 0,1,0, 0,0,1, 0,0,0), time = real[4], easing = LINEAR_EASING) + addtimer(CALLBACK(src, TYPE_PROC_REF(/datum, remove_filter), HV_VIS_FLASH_FILTER), cyberware_dilated_time(src, 0.5 SECONDS), TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_DELETE_ME) /// The attacker's side of a hit-stop: the blow sticks for a few frames. A melee attacker stays leaned /// into the victim; a shooter stays rocked back, even with the barrel against them (`shot`). diff --git a/voidcrew/modules/hyperviolence/visuals/blood_motion.dm b/voidcrew/modules/hyperviolence/visuals/blood_motion.dm index 7133927011e..22aa43f7bdd 100644 --- a/voidcrew/modules/hyperviolence/visuals/blood_motion.dm +++ b/voidcrew/modules/hyperviolence/visuals/blood_motion.dm @@ -28,8 +28,14 @@ GLOBAL_VAR(hv_vis_spray_origin) /obj/effect/decal/cleanable/blood/proc/hv_vis_paint(list/keys) var/reached = world.time var/list/timed = list() + // In a Sandevistan field (cyberware/ware_sandy_timestop.dm) the paint waits out the freeze and runs slow. + var/list/times = list() for(var/list/key as anything in keys) - reached += key[3] + times += key[3] + var/list/real = cyberware_dilated_steps(src, times) + for(var/i in 1 to length(keys)) + var/list/key = keys[i] + reached += real ? real[i + 1] + (i == 1 ? real[1] : 0) : key[3] timed += list(list(key[1], key[2], reached, key[4])) hv_vis_painting = timed hv_vis_paint_on() @@ -58,6 +64,9 @@ GLOBAL_VAR(hv_vis_spray_origin) var/matrix/from = behind[1] var/matrix/current = from.Interpolate(ahead[1], progress) animate(src, transform = current, alpha = behind[2] + (ahead[2] - behind[2]) * progress, time = 0, tag = HV_VIS_TAG_PAINT) + if(now < behind[3]) // held by a frozen moment before it starts + animate(transform = current, alpha = behind[2], time = behind[3] - now) + now = behind[3] animate(transform = ahead[1], alpha = ahead[2], time = ahead[3] - now, easing = ahead[4]) for(var/i in heading + 1 to length(keys)) var/list/key = keys[i] diff --git a/voidcrew/modules/hyperviolence/visuals/death_falls.dm b/voidcrew/modules/hyperviolence/visuals/death_falls.dm index 667d976d1fd..c531b78d7ed 100644 --- a/voidcrew/modules/hyperviolence/visuals/death_falls.dm +++ b/voidcrew/modules/hyperviolence/visuals/death_falls.dm @@ -125,7 +125,7 @@ hv_vis_play_over(hv_vis_hold_then_fall_steps(HV_VIS_HITSTOP_TIME, 2)) if(hv_vis_hit_damage >= HV_VIS_FULL_SLIDE_DAMAGE && hv_vis_can_slide_tile(hit_dir)) // Next tick: the blow that killed it may still be resolving (a shotgun's other pellets). - addtimer(CALLBACK(src, PROC_REF(hv_vis_slide_tile), hit_dir), 1, TIMER_DELETE_ME) + addtimer(CALLBACK(src, PROC_REF(hv_vis_slide_tile), hit_dir), cyberware_dilated_time(src, 1), TIMER_DELETE_ME) // waits out a Sandevistan freeze return hv_vis_slide_in_tile(hit_dir, hv_vis_hit_damage, big ? HV_VIS_HITSTOP_TIME : 0) hv_vis_twitches(rand(3, 5), big ? 6 : 4) @@ -176,7 +176,7 @@ /mob/living/carbon/proc/hv_vis_slide_tile(slide_dir) if(QDELETED(src) || stat != DEAD) return - if(hv_vis_can_slide_tile(slide_dir) && Move(get_step(loc, slide_dir), slide_dir, DELAY_TO_GLIDE_SIZE(HV_VIS_SLIDE_TIME))) + if(hv_vis_can_slide_tile(slide_dir) && Move(get_step(loc, slide_dir), slide_dir, DELAY_TO_GLIDE_SIZE(cyberware_dilated_time(src, HV_VIS_SLIDE_TIME)))) // Thrown back as it falls: a little lift over the slide. var/matrix/base = matrix(transform) hv_vis_play(HV_VIS_TAG_FALL, list( diff --git a/voidcrew/modules/hyperviolence/visuals/execution_motion.dm b/voidcrew/modules/hyperviolence/visuals/execution_motion.dm index 35aa47d1605..fe3f5474b4a 100644 --- a/voidcrew/modules/hyperviolence/visuals/execution_motion.dm +++ b/voidcrew/modules/hyperviolence/visuals/execution_motion.dm @@ -68,9 +68,11 @@ var/loops = max(1, round(duration / 2)) var/base_w = pixel_w var/base_z = pixel_z - animate(src, pixel_w = base_w + strength, pixel_z = base_z, transform = base, time = 0.5, loop = loops, tag = HV_VIS_TAG_TREMBLE) - animate(pixel_w = base_w - strength, pixel_z = base_z, transform = base, time = 1) - animate(pixel_w = base_w, pixel_z = base_z, transform = base, time = 0.5) + // Slower in a Sandevistan field (cyberware/ware_sandy_timestop.dm). + var/list/real = cyberware_dilated_steps(src, list(0.5, 1, 0.5), loops) || list(0, 0.5, 1, 0.5) + animate(src, pixel_w = base_w + strength, pixel_z = base_z, transform = base, time = real[1] + real[2], loop = loops, tag = HV_VIS_TAG_TREMBLE) + animate(pixel_w = base_w - strength, pixel_z = base_z, transform = base, time = real[3]) + animate(pixel_w = base_w, pixel_z = base_z, transform = base, time = real[4]) /** * A muzzle flash much bigger than a normal shot's, with a flash of light. It is where the barrel's end diff --git a/voidcrew/modules/hyperviolence/visuals/flying_gore.dm b/voidcrew/modules/hyperviolence/visuals/flying_gore.dm index 60c87030104..b103ab527c4 100644 --- a/voidcrew/modules/hyperviolence/visuals/flying_gore.dm +++ b/voidcrew/modules/hyperviolence/visuals/flying_gore.dm @@ -184,7 +184,7 @@ GLOBAL_LIST(hv_vis_arcing_gibs) else if(!grounded) step_time *= 3 rest_layer = part.layer - addtimer(CALLBACK(src, PROC_REF(give_up)), 5 SECONDS, TIMER_DELETE_ME) + addtimer(CALLBACK(src, PROC_REF(give_up)), cyberware_dilated_time(part, 5 SECONDS), TIMER_DELETE_ME) /datum/component/hv_tumble/RegisterWithParent() RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved)) @@ -230,7 +230,8 @@ GLOBAL_LIST(hv_vis_arcing_gibs) /// where it is once it has dropped. /datum/component/hv_tumble/proc/launch() start_flight() - step_timer = addtimer(CALLBACK(src, PROC_REF(take_step)), length(path) ? world.tick_lag : flight_time(), TIMER_STOPPABLE|TIMER_DELETE_ME) + // In a Sandevistan field (cyberware/ware_sandy_timestop.dm) it waits out the freeze and flies slow. + step_timer = addtimer(CALLBACK(src, PROC_REF(take_step)), cyberware_dilated_time(parent, length(path) ? world.tick_lag : flight_time()), TIMER_STOPPABLE|TIMER_DELETE_ME) /datum/component/hv_tumble/proc/start_flight() var/obj/item/part = parent @@ -305,12 +306,13 @@ GLOBAL_LIST(hv_vis_arcing_gibs) steps_taken++ path.Cut(1, 2) stepping = TRUE - part.set_glide_size(DELAY_TO_GLIDE_SIZE(step_time)) + var/real_step = cyberware_dilated_time(part, step_time) + part.set_glide_size(DELAY_TO_GLIDE_SIZE(real_step)) part.forceMove(next) stepping = FALSE if(QDELETED(src)) return - step_timer = addtimer(CALLBACK(src, PROC_REF(take_step)), step_time, TIMER_STOPPABLE|TIMER_DELETE_ME) + step_timer = addtimer(CALLBACK(src, PROC_REF(take_step)), real_step, TIMER_STOPPABLE|TIMER_DELETE_ME) /// Blood on each tile it crosses. Anything else moving it (a hand, a conveyor) ends the flight. /datum/component/hv_tumble/proc/on_moved(obj/item/source, atom/old_loc, movement_dir, forced, list/old_locs) @@ -430,10 +432,12 @@ GLOBAL_LIST(hv_vis_arcing_gibs) var/list/pivot = part.hv_vis_spin_pivot() var/start_x = part.pixel_x var/start_y = part.pixel_y - animate(part, pixel_x = start_x, pixel_y = start_y, transform = base, time = 2.5, tag = HV_VIS_TAG_SPIN) - animate(pixel_x = start_x + (end_x - start_x) / 3, pixel_y = start_y + (end_y - start_y) / 3, transform = hv_vis_turned(base, spin * 120, pivot[2], pivot[1]), time = 1.5, easing = LINEAR_EASING) - animate(pixel_x = start_x + (end_x - start_x) * 2 / 3, pixel_y = start_y + (end_y - start_y) * 2 / 3, transform = hv_vis_turned(base, spin * 240, pivot[2], pivot[1]), time = 2, easing = LINEAR_EASING) - animate(pixel_x = end_x, pixel_y = end_y, transform = base, time = 3, easing = QUAD_EASING|EASE_OUT) + // Slower in a Sandevistan field (cyberware/ware_sandy_timestop.dm); the opening pause takes the freeze. + var/list/real = cyberware_dilated_steps(part, list(2.5, 1.5, 2, 3)) || list(0, 2.5, 1.5, 2, 3) + animate(part, pixel_x = start_x, pixel_y = start_y, transform = base, time = real[1] + real[2], tag = HV_VIS_TAG_SPIN) + animate(pixel_x = start_x + (end_x - start_x) / 3, pixel_y = start_y + (end_y - start_y) / 3, transform = hv_vis_turned(base, spin * 120, pivot[2], pivot[1]), time = real[3], easing = LINEAR_EASING) + animate(pixel_x = start_x + (end_x - start_x) * 2 / 3, pixel_y = start_y + (end_y - start_y) * 2 / 3, transform = hv_vis_turned(base, spin * 240, pivot[2], pivot[1]), time = real[4], easing = LINEAR_EASING) + animate(pixel_x = end_x, pixel_y = end_y, transform = base, time = real[5], easing = QUAD_EASING|EASE_OUT) // ---- Limbs and heads coming off ---- From e348ff445d3af57c08abd75faa064d4b5302b9b5 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Tue, 10 Nov 2026 22:17:20 -0800 Subject: [PATCH 08/20] Turn off the reflex dodge while the Sandevistan user has the world slowed --- voidcrew/modules/cyberware/ware_pro_combat.dm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/voidcrew/modules/cyberware/ware_pro_combat.dm b/voidcrew/modules/cyberware/ware_pro_combat.dm index 30371c5e83e..327ac8ad9f3 100644 --- a/voidcrew/modules/cyberware/ware_pro_combat.dm +++ b/voidcrew/modules/cyberware/ware_pro_combat.dm @@ -85,6 +85,8 @@ SIGNAL_HANDLER if(world.time - last_move_time > CYBERWARE_DODGE_MOVE_WINDOW) return NONE + if(cyberware_time_stopping(source)) // the slowed world is the dodge (ware_sandy_timestop.dm) + return NONE if(source.stat != CONSCIOUS || source.body_position == LYING_DOWN || source.buckled) return NONE if(!istype(hitting_projectile) || !hitting_projectile.is_hostile_projectile()) @@ -112,7 +114,7 @@ /// Signal proc for [COMSIG_LIVING_CHECK_BLOCK]: the melee dodge roll, from tg's own check_block(). /datum/component/cyberware_dodge/proc/on_check_block(mob/living/source, atom/hit_by, damage, attack_text, attack_type, armour_penetration, damage_type) SIGNAL_HANDLER - if(!length(melee_sources) || !cyberware_can_dodge_blow(source, hit_by, damage, attack_text, attack_type)) + if(!length(melee_sources) || cyberware_time_stopping(source) || !cyberware_can_dodge_blow(source, hit_by, damage, attack_text, attack_type)) return FAILED_BLOCK var/best_chance = best_chance(melee_sources) if(best_chance <= 0 || !prob(best_chance)) From 1a9b11b95ccaab70318db00850371c94625d0ce4 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Tue, 10 Nov 2026 22:17:21 -0800 Subject: [PATCH 09/20] Keep the green afterimage trail for the rest of a surge that slowed the world --- voidcrew/modules/cyberware/ware_legend.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voidcrew/modules/cyberware/ware_legend.dm b/voidcrew/modules/cyberware/ware_legend.dm index ae0a29e8df8..28ec059cfe7 100644 --- a/voidcrew/modules/cyberware/ware_legend.dm +++ b/voidcrew/modules/cyberware/ware_legend.dm @@ -252,7 +252,7 @@ steps_taken++ if(steps_taken % afterimage_interval) // cheaper chrome leaves a sparser trail return - if(time_stop) + if(time_stop || dilated) // a surge that slowed the world keeps its green trail to the end of the window stopped_afterimage(old_loc) return new /obj/effect/temp_visual/decoy/fading/cascade(old_loc, owner, trail_hue, afterimage_alpha) From 2af43b430920083d2f26d91a4a77880e07902c45 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Tue, 10 Nov 2026 22:17:21 -0800 Subject: [PATCH 10/20] Let mobs on their own Cascade surge run at full speed in a dilation field --- .../modules/cyberware/ware_sandy_timestop.dm | 101 ++++++++++++++++- .../hyperviolence/visuals/_motion_core.dm | 2 +- .../unit_tests/voidcrew_cyberware_timestop.dm | 102 ++++++++++++++++++ 3 files changed, 200 insertions(+), 5 deletions(-) diff --git a/voidcrew/modules/cyberware/ware_sandy_timestop.dm b/voidcrew/modules/cyberware/ware_sandy_timestop.dm index 2d13d482e49..6d7c10d1040 100644 --- a/voidcrew/modules/cyberware/ware_sandy_timestop.dm +++ b/voidcrew/modules/cyberware/ware_sandy_timestop.dm @@ -36,6 +36,13 @@ * * Bosses (a finisher user, or a boss-rank human NPC) are held for SANDY_STOP_BOSS_HOLD at most. * + * A mob running its own Cascade surge (a bounty NPC's lattice) is never slowed, frozen or tinted while its window + * is open, and neither are its shots. If it surges while already held, the field lets it go at once and drops + * every slow it was carrying (surge_free()). When its window closes the field may take it again. + * + * The user's reflex dodge (ware_pro_combat.dm) is off while the world is slowed: the speed gap already does that + * job. It comes back for the rest of the window. + * * Look: affected things get a grey-green tint instead of tg's inversion. Floor items, blood decals and the * user are never touched, so they keep their colour, and so do the user's own shots. Shots the user fires * while the world is frozen fly a tile, hang there, and go on when it starts moving again. @@ -84,13 +91,33 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) return null var/atom/on_turf = ismovable(thing) ? get_atom_on_turf(thing) : thing for(field as anything in GLOB.cyberware_time_fields) - if(field.immune[thing] || field.immune[on_turf] || field.released[on_turf]) + if(field.immune[thing] || field.immune[on_turf] || field.released[on_turf] || field.surging[on_turf]) continue var/turf/centre = get_turf(field.host) if(centre && centre.z == here.z && get_dist(centre, here) <= field.current_range) return field return null +/// Whether `bearer` is slowing the world around them right now (the freeze or the slow-mo). +/proc/cyberware_time_stopping(mob/living/bearer) + for(var/datum/proximity_monitor/advanced/timestop/sandevistan/field as anything in GLOB.cyberware_time_fields) + if(field.host == bearer) + return TRUE + return FALSE + +/// Whether `thing` is a mob with its own Cascade surge window open. Such a mob runs at its own speed in any field. +/proc/cyberware_own_surge(atom/thing) + if(!isliving(thing)) + return FALSE + var/mob/living/body = thing + return !!body.has_status_effect(/datum/status_effect/cyberware_cascade_window) + +/// `surger` just opened its own surge window: every field lets it go and leaves it be until the window closes. +/proc/cyberware_surge_frees(mob/living/surger) + for(var/datum/proximity_monitor/advanced/timestop/sandevistan/field as anything in GLOB.cyberware_time_fields) + if(field.host != surger) + field.surge_free(surger) + /// How long, in real time, `time` deciseconds of `thing`'s own time take from now. Exact through the freeze, /// the slow-mo and the end of the effect, as the field stands now. /proc/cyberware_dilated_time(atom/thing, time) @@ -102,12 +129,14 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) /** * Real-time lengths for a chain of animation steps `times` on `thing`: list(hold, step 1, step 2, ...), or null * when it isn't slowed. `hold` is how long it sits still first (the freeze). A looping chain just plays at the - * field's pace. + * field's pace. `tag` names the animation, so a held mob that breaks free (surge_free()) can drop it. */ -/proc/cyberware_dilated_steps(atom/thing, list/times, loop = 1) +/proc/cyberware_dilated_steps(atom/thing, list/times, loop = 1, tag) var/datum/proximity_monitor/advanced/timestop/sandevistan/field = cyberware_time_field(thing) if(!field) return null + if(tag && field.held_mobs[thing]) + LAZYOR(field.slowed_tags[thing], tag) var/list/real = list() if(loop != 1) var/scale = field.time_scale > 0 ? field.time_scale : field.slow_scale @@ -312,6 +341,10 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) var/list/held_turrets = list() /// Armed grenades held. Their fuse runs slow. var/list/held_grenades = list() + /// Mobs in range running their own surge. The field leaves them and their shots be until their window closes. + var/list/surging = list() + /// Held mobs, to the tags of their slowed hv_vis_play() animations, to drop if they break free. + var/list/slowed_tags = list() /datum/proximity_monitor/advanced/timestop/sandevistan/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, list/immune, antimagic_flags, channelled, start_scale = 0) time_scale = start_scale @@ -329,6 +362,9 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) UnregisterSignal(host, COMSIG_PROJECTILE_FIRER_BEFORE_FIRE) stopper = null GLOB.cyberware_time_fields -= src + for(var/mob/living/surger as anything in surging) + UnregisterSignal(surger, COMSIG_QDELETING) + surging.Cut() . = ..() // unfreeze_all() lets everything go released.Cut() @@ -355,7 +391,10 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) return at - now + local /datum/proximity_monitor/advanced/timestop/sandevistan/freeze_atom(atom/movable/A) - if(!istype(A) || GLOB.cyberware_time_dilated[A] || global_frozen_atoms[A] || released[A] || immune[A]) + if(!istype(A) || GLOB.cyberware_time_dilated[A] || global_frozen_atoms[A] || released[A] || immune[A] || surging[A]) + return FALSE + if(cyberware_own_surge(A)) + leave_surger(A) return FALSE if(ismob(A)) var/mob/M = A @@ -367,6 +406,8 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) hold_mob(A) else if(isprojectile(A)) var/obj/projectile/shot = A + if(shot.firer && (surging[shot.firer] || cyberware_own_surge(shot.firer))) // a surging mob's shots fly at its speed + return FALSE if(shot.firer && immune[shot.firer]) if(time_scale <= 0) // hang_shot takes these return FALSE @@ -424,6 +465,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) held_grenades -= A frozen_structures -= A user_shots -= A + slowed_tags -= A UnregisterSignal(A, COMSIG_QDELETING) escape_the_negative_zone(A) frozen_things -= A @@ -509,6 +551,50 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) if(ranged) ranged.cyberware_burst_start += push +// ----- mobs on their own surge ----- + +/// Leaves `surger` be while its own window runs. It isn't held. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/leave_surger(mob/living/surger) + surging[surger] = TRUE + RegisterSignal(surger, COMSIG_QDELETING, PROC_REF(held_gone), override = TRUE) + +/** + * `surger` opened its own surge: if the field holds it, let it go now and drop every slow it carried (its walk, + * its pauses, its last bit of lost time, its slowed animations and the shots it fired), then leave it be. + */ +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/surge_free(mob/living/surger) + if(surging[surger]) + return + if(GLOB.cyberware_time_dilated[surger] == src) + var/lag = cyberware_time_lag(surger) + if(lag > 0) + push_deadlines(surger, lag) // the time it really lost since the last tick + var/list/tags = slowed_tags[surger] + unfreeze_atom(surger) + for(var/tag in tags) + surger.hv_vis_release(tag, 1) // back to its real pose at once, not at the slowed pace + for(var/obj/projectile/shot as anything in held_shots.Copy()) + if(shot.firer == surger) + unfreeze_atom(shot) + else + var/turf/centre = get_turf(host) + var/turf/here = get_turf(surger) + if(!centre || !here || here.z != centre.z || get_dist(centre, here) > current_range) + return // out of reach: if it walks in still surging, freeze_atom() leaves it be + leave_surger(surger) + +/// Every tick: surgers whose window has closed are fair game again, if they are still in range. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/check_surgers() + var/turf/centre = get_turf(host) + for(var/mob/living/surger as anything in surging.Copy()) + if(cyberware_own_surge(surger)) + continue + surging -= surger + UnregisterSignal(surger, COMSIG_QDELETING) + var/turf/here = get_turf(surger) + if(centre && here && here.z == centre.z && get_dist(centre, here) <= current_range) + freeze_atom(surger) + // ----- mechs, turrets, grenades ----- /// Takes hold of a mech and whoever is driving it. @@ -615,6 +701,8 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) held_turrets -= thing held_grenades -= thing boss_holds -= thing + surging -= thing + slowed_tags -= thing frozen_structures -= thing frozen_things -= thing GLOB.cyberware_time_dilated -= thing @@ -664,6 +752,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) if(throw_datum && !throw_datum.paused) throw_datum.start_time += push push_machine_deadlines(push) + check_surgers() for(var/mob/living/boss as anything in boss_holds.Copy()) if(world.time < boss_holds[boss]) continue @@ -689,9 +778,12 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) var/dilation_scale = 0.2 /// The running dilation, if any. var/obj/effect/timestop/sandevistan/time_stop + /// Whether this window slowed the world. Its trail stays green after the dilation ends. + var/dilated = FALSE /// Slows the world if the bearer is a player and nobody else is near. Called as the window opens. /datum/status_effect/cyberware_cascade_window/proc/try_time_stop() + cyberware_surge_frees(owner) // anyone's surge breaks them out of someone else's field if(dilation_freeze + dilation_time <= 0 || !owner.ckey || time_stop) return FALSE if(cyberware_time_stop_witnessed(owner)) @@ -704,6 +796,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) time_stop = null return FALSE RegisterSignal(time_stop, COMSIG_QDELETING, PROC_REF(time_stop_over)) + dilated = TRUE owner.remove_client_colour(id) // the user sees the slowed world as it is owner.add_filter(SANDY_STOP_OUTLINE, 2, outline_filter(1, "#5AFF96D2")) return TRUE diff --git a/voidcrew/modules/hyperviolence/visuals/_motion_core.dm b/voidcrew/modules/hyperviolence/visuals/_motion_core.dm index 06986535e34..1acccda933b 100644 --- a/voidcrew/modules/hyperviolence/visuals/_motion_core.dm +++ b/voidcrew/modules/hyperviolence/visuals/_motion_core.dm @@ -207,7 +207,7 @@ var/list/times = list() for(var/list/step as anything in steps) times += step[4] - var/list/real = cyberware_dilated_steps(src, times, loop) + var/list/real = cyberware_dilated_steps(src, times, loop, tag) var/hold = real ? real[1] : 0 var/started = FALSE for(var/i in 1 to length(steps)) diff --git a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm index 598188abd1c..16c7b40edf5 100644 --- a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm +++ b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm @@ -179,3 +179,105 @@ bomb.cyberware_fuse_at = 0 // don't let it go off after the test user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) + +/// Whether `thing` wears the dilation's tint +/datum/unit_test/voidcrew_cyberware_timestop/proc/tinted(atom/thing) + return length(thing.atom_colours) >= TEMPORARY_COLOUR_PRIORITY && !isnull(thing.atom_colours[TEMPORARY_COLOUR_PRIORITY]) + +/// A raider NPC at `where` with its own Cascade lattice +/datum/unit_test/voidcrew_cyberware_timestop/proc/make_surger(turf/where) + var/mob/living/carbon/human/npc/raider = allocate(/mob/living/carbon/human/npc, where) + var/obj/item/organ/cyberimp/cyberware/cascade/lattice = allocate(/obj/item/organ/cyberimp/cyberware/cascade) + TEST_ASSERT(lattice.Insert(raider, special = TRUE), "Test fixture: the lattice wouldn't go in the NPC") + return raider + +/// No reflex dodge while the user's own world is slowed; it comes back for the rest of the window +/datum/unit_test/voidcrew_cyberware_timestop/no_dodge_while_slowed + +/datum/unit_test/voidcrew_cyberware_timestop/no_dodge_while_slowed/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/obj/item/knife/kitchen/knife = allocate(/obj/item/knife/kitchen, spot(1, 0)) + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + TEST_ASSERT_NOTNULL(window, "The lattice's window didn't open") + window.dodge_chance = 100 + sleep(0.1 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") + var/datum/component/cyberware_dodge/arbiter = user.GetComponent(/datum/component/cyberware_dodge) + TEST_ASSERT_NOTNULL(arbiter, "The window has no dodge") + TEST_ASSERT(cyberware_time_stopping(user), "The user isn't marked as slowing time") + TEST_ASSERT_EQUAL(arbiter.on_check_block(user, knife, 10, "the knife", MELEE_ATTACK), FAILED_BLOCK, "The user dodged a blow in frozen time") + sleep(1.2 SECONDS) + TEST_ASSERT_EQUAL(arbiter.on_check_block(user, knife, 10, "the knife", MELEE_ATTACK), FAILED_BLOCK, "The user dodged a blow in slow-mo") + sleep(3 SECONDS) // past the dilation, still in the window + TEST_ASSERT_NULL(window.time_stop, "The dilation outlived its 4 s") + TEST_ASSERT(!cyberware_time_stopping(user), "The user still counts as slowing time") + TEST_ASSERT_EQUAL(arbiter.on_check_block(user, knife, 10, "the knife", MELEE_ATTACK), SUCCESSFUL_BLOCK, "The dodge didn't come back after the dilation") + user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) + user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) + +/// An NPC already surging when the user slows the world runs at its own speed, and so do its shots +/datum/unit_test/voidcrew_cyberware_timestop/surging_npc_immune + +/datum/unit_test/voidcrew_cyberware_timestop/surging_npc_immune/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/mob/living/carbon/human/npc/raider = make_surger(spot(2, 0)) + var/obj/projectile/bullet/shot = allocate(/obj/projectile/bullet, spot(1, 1)) + shot.firer = raider + var/shot_speed = shot.speed + TEST_ASSERT_NOTNULL(surge(raider), "The NPC's window didn't open") + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + sleep(0.1 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield + TEST_ASSERT(field.surging[raider], "The field didn't notice the NPC's surge") + TEST_ASSERT(!HAS_TRAIT(raider, TRAIT_IMMOBILIZED) && !HAS_TRAIT(raider, TRAIT_AI_PAUSED), "A surging NPC was frozen") + TEST_ASSERT(!tinted(raider), "A surging NPC was tinted") + TEST_ASSERT_EQUAL(cyberware_time_lost(raider, 10), 0, "A surging NPC's clock slowed") + TEST_ASSERT_NULL(cyberware_time_field(raider), "A surging NPC's gore would be slowed") + TEST_ASSERT(!field.held_shots[shot] && !shot.paused && shot.speed == shot_speed, "A surging NPC's shot was held") + sleep(1.2 SECONDS) // into the slow-mo + TEST_ASSERT(!raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "A surging NPC was slowed") + + // Its window closes: the field takes it like anyone else. + raider.remove_status_effect(/datum/status_effect/cyberware_cascade_window) + sleep(0.3 SECONDS) + TEST_ASSERT(!field.surging[raider], "The field kept leaving the NPC be after its window closed") + TEST_ASSERT(raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "The NPC wasn't slowed once its window closed") + user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) + user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) + raider.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) + +/// An NPC held in slow-mo that surges breaks free at once and drops every slow it carried +/datum/unit_test/voidcrew_cyberware_timestop/late_surge_breaks_free + +/datum/unit_test/voidcrew_cyberware_timestop/late_surge_breaks_free/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/mob/living/carbon/human/npc/raider = make_surger(spot(2, 0)) + var/obj/projectile/bullet/shot = allocate(/obj/projectile/bullet, spot(1, 1)) + shot.firer = raider + var/shot_speed = shot.speed + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + sleep(0.1 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield + sleep(1.2 SECONDS) // into the slow-mo + TEST_ASSERT(raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "Test fixture: the NPC isn't slowed") + TEST_ASSERT(field.held_shots[shot], "Test fixture: the NPC's shot isn't held") + raider.hv_vis_play("sandy_test_flinch", list(list(2, 0, matrix(), 2, LINEAR_EASING), list(0, 0, matrix(), 2, LINEAR_EASING))) + TEST_ASSERT("sandy_test_flinch" in field.slowed_tags[raider], "A held NPC's slowed animation wasn't noted") + + TEST_ASSERT_NOTNULL(surge(raider), "The NPC's window didn't open") + TEST_ASSERT(field.surging[raider], "The field didn't let the surging NPC go") + TEST_ASSERT(!raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "The surging NPC stayed slowed") + TEST_ASSERT(!HAS_TRAIT(raider, TRAIT_IMMOBILIZED) && !HAS_TRAIT(raider, TRAIT_AI_PAUSED), "The surging NPC stayed held") + TEST_ASSERT(!tinted(raider), "The surging NPC stayed tinted") + TEST_ASSERT_NULL(GLOB.cyberware_time_dilated[raider], "The surging NPC is still in the field's hands") + TEST_ASSERT_EQUAL(cyberware_time_lost(raider, 10), 0, "The surging NPC's clock stayed slow") + TEST_ASSERT_NULL(field.slowed_tags[raider], "The surging NPC's slowed animations weren't dropped") + TEST_ASSERT(!field.held_shots[shot] && shot.speed == shot_speed, "The surging NPC's shot stayed slowed") + sleep(0.5 SECONDS) // a few field ticks + TEST_ASSERT(!raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "The field took the surging NPC back") + user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) + user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) + raider.remove_status_effect(/datum/status_effect/cyberware_cascade_window) + raider.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) From c739e14942342e73c5ca89f2a88967bc935a064e Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Tue, 10 Nov 2026 22:19:07 -0800 Subject: [PATCH 11/20] Let dermal plating deflect bullets from the front Dermal Mesh, Hardweave and Slabskin turn 15, 30 and 45 percent of bullets and buckshot pellets from the front. A glanced round turns off at a random angle and keeps flying, with sparks, a ricochet ping and a small stamina cost. Dodge and deflect together stop at most 60 percent of shots. --- tools/gear_catalog/ratings/cyberware.json | 16 +-- .../modules/cyberware/ware_military_body.dm | 12 +- voidcrew/modules/cyberware/ware_models_b.dm | 3 +- voidcrew/modules/cyberware/ware_pro_combat.dm | 132 ++++++++++++++++-- voidcrew/modules/cyberware/ware_street.dm | 16 ++- voidcrew/modules/unit_tests/_unit_tests.dm | 1 + .../unit_tests/voidcrew_cyberware_deflect.dm | 70 ++++++++++ wiki/content/cyberware.md | 7 +- 8 files changed, 230 insertions(+), 27 deletions(-) create mode 100644 voidcrew/modules/unit_tests/voidcrew_cyberware_deflect.dm diff --git a/tools/gear_catalog/ratings/cyberware.json b/tools/gear_catalog/ratings/cyberware.json index 65fdbe71618..f6116cd316f 100644 --- a/tools/gear_catalog/ratings/cyberware.json +++ b/tools/gear_catalog/ratings/cyberware.json @@ -449,11 +449,11 @@ "/obj/item/organ/cyberimp/cyberware/dermal_mesh": { "confidence": "high", "group": "Armor and frame", - "hash": "5b60dbb7a15c8084", + "hash": "", "name": "Dermal Mesh weave", - "note": "While online, adds physiology armor to a human wearer: melee 15, bullet 15, laser 10, wound 5. The armor is removed during an EMP reboot or an over-capacity brownout.", + "note": "While online, adds physiology armor to a human wearer: melee 15, bullet 15, laser 10, wound 5. Bullets and buckshot pellets from the front glance off at 15% (each pellet rolls on its own), keep flying at reduced damage and cost the wearer 5 stamina each. Never lasers, energy, melee or shots into the back. Shares the dodge arbiter: dodge plus glance never stop more than 60% of shots. The armor and the glance are off during an EMP reboot or an over-capacity brownout.", "rank": 5, - "reason": "15 melee, 15 bullet, 10 laser on the whole body, on top of whatever you wear. A clear step up for load 2 and 2,000 cr. EMP switches it off for 9 s.", + "reason": "15 melee, 15 bullet, 10 laser on the whole body, on top of whatever you wear, and one bullet in seven from the front glances off. A clear step up for load 2 and 2,000 cr. EMP switches it off for 9 s.", "tier": "yellow" }, "/obj/item/organ/cyberimp/cyberware/dermal_mesh/hardweave": { @@ -461,9 +461,9 @@ "group": "Armor and frame", "hash": "", "name": "Hardweave plating", - "note": "While online, adds physiology armor to a human wearer: melee 20, bullet 20, laser 15, energy 10, wound 10. The armor is removed during an EMP reboot or an over-capacity brownout.", + "note": "While online, adds physiology armor to a human wearer: melee 20, bullet 20, laser 15, energy 10, wound 10. Bullets and buckshot pellets from the front glance off at 30% (each pellet rolls on its own), keep flying at reduced damage and cost the wearer 5 stamina each. Never lasers, energy, melee or shots into the back. Shares the dodge arbiter: dodge plus glance never stop more than 60% of shots. The armor and the glance are off during an EMP reboot or an over-capacity brownout.", "rank": 3, - "reason": "20 melee, 20 bullet, 15 laser, 10 energy and 10 wound armor on the whole body, on top of whatever you wear. A size up from the Mesh and well short of the Slabskin Plate, which adds bomb, fire and acid. Load 3, 3,000 cr at reputation 4. EMP switches it off for 9 s.", + "reason": "20 melee, 20 bullet, 15 laser, 10 energy and 10 wound armor on the whole body, on top of whatever you wear, and nearly a third of bullets from the front glance off. A size up from the Mesh and well short of the Slabskin Plate, which adds bomb, fire, acid and a bigger glance. Load 3, 3,000 cr at reputation 4. Strong for yellow against gunfire from the front, still useless against lasers and flanking. EMP switches it off for 9 s.", "tier": "yellow" }, "/obj/item/organ/cyberimp/cyberware/doppler": { @@ -779,11 +779,11 @@ "/obj/item/organ/cyberimp/cyberware/slabskin": { "confidence": "high", "group": "Armor and frame", - "hash": "7b785a3adf8b7c37", + "hash": "", "name": "Slabskin plate", - "note": "While online it adds built-in armor to the bearer (melee 25, bullet 25, laser 25, energy 20, bomb 25, fire 20, acid 20, wound 15), and any brute or burn hit of 5 or more throws sparks and often a ricochet sound so others can see the plate. The armor switches off while the ware is EMP'd or browned out.", + "note": "While online it adds built-in armor to the bearer (melee 25, bullet 25, laser 25, energy 20, bomb 25, fire 20, acid 20, wound 15), and any brute or burn hit of 5 or more throws sparks and often a ricochet sound so others can see the plate. Bullets and buckshot pellets from the front glance off at 45% (each pellet rolls on its own), keep flying at reduced damage and cost the wearer 5 stamina each. Never lasers, energy, melee or shots into the back. Shares the dodge arbiter: dodge plus glance never stop more than 60% of shots. The armor and the glance switch off while the ware is EMP'd or browned out.", "rank": 1, - "reason": "25 armor against melee, bullets and lasers on the whole body, multiplied on top of whatever you wear, plus bomb, fire, acid and wounds. About a second armor vest that covers your head and limbs. Load 6, 3 vouchers at reputation 22. EMP switches it off for 9 s.", + "reason": "25 armor against melee, bullets and lasers on the whole body, multiplied on top of whatever you wear, plus bomb, fire, acid and wounds, and almost half of bullets from the front glance off. About a second armor vest that covers your head and limbs, and the best answer to a gunline in the game. Load 6, 3 vouchers at reputation 22. EMP switches it off for 9 s.", "tier": "red" }, "/obj/item/organ/cyberimp/cyberware/slipwire": { diff --git a/voidcrew/modules/cyberware/ware_military_body.dm b/voidcrew/modules/cyberware/ware_military_body.dm index a5cf534aba9..ffd818d7dd5 100644 --- a/voidcrew/modules/cyberware/ware_military_body.dm +++ b/voidcrew/modules/cyberware/ware_military_body.dm @@ -264,7 +264,7 @@ */ /obj/item/organ/cyberimp/cyberware/slabskin name = "\improper Slabskin plate" - desc = "A keel and rib plates of sintered ceramic, grown into the skin of the chest and back. Bullets leave scuffs and a noise like a dropped pan." + desc = "A keel and rib plates of sintered ceramic, grown into the skin of the chest and back. Bullets leave scuffs, when they don't bounce off with a noise like a dropped pan." icon_state = "slabskin" zone = BODY_ZONE_CHEST slot = ORGAN_SLOT_CYBERWARE_DERMAL @@ -276,6 +276,8 @@ /// TRUE while the plate armor is mixed into the bearer's physiology. /// Guards the failing-gated passive hooks against double add/subtract. var/plate_armor_applied = FALSE + /// Percent of bullets from the front that glance off, through the shared dodge arbiter. The top of the dermal ladder. + var/deflect_chance = 45 /obj/item/organ/cyberimp/cyberware/slabskin/on_mob_insert(mob/living/carbon/organ_owner, special = FALSE, movement_flags) . = ..() @@ -283,6 +285,7 @@ /obj/item/organ/cyberimp/cyberware/slabskin/chrome_passives_on(mob/living/carbon/bearer) . = ..() + cyberware_register_deflect_source(bearer, REF(src), CALLBACK(src, PROC_REF(get_deflect_chance))) if(plate_armor_applied || !ishuman(bearer)) return plate_armor_applied = TRUE @@ -291,6 +294,7 @@ /obj/item/organ/cyberimp/cyberware/slabskin/chrome_passives_off(mob/living/carbon/bearer) . = ..() + cyberware_unregister_deflect_source(bearer, REF(src)) if(!plate_armor_applied) return plate_armor_applied = FALSE // reset before the validity skip, or the plate never re-arms after a bearer deletes @@ -303,6 +307,12 @@ . = ..() UnregisterSignal(organ_owner, COMSIG_MOB_APPLY_DAMAGE) +/// Deflect-source callback: nothing while failing. +/obj/item/organ/cyberimp/cyberware/slabskin/proc/get_deflect_chance() + if(organ_flags & ORGAN_FAILING) + return 0 + return deflect_chance + /** * Signal proc for [COMSIG_MOB_APPLY_DAMAGE]: the telegraph. Solid brute or * burn hits spark and ping off the plate where everyone can see them. diff --git a/voidcrew/modules/cyberware/ware_models_b.dm b/voidcrew/modules/cyberware/ware_models_b.dm index 9db419c8cba..7de0991d93c 100644 --- a/voidcrew/modules/cyberware/ware_models_b.dm +++ b/voidcrew/modules/cyberware/ware_models_b.dm @@ -44,12 +44,13 @@ /obj/item/organ/cyberimp/cyberware/dermal_mesh/hardweave name = "\improper Hardweave plating" - desc = "Rib plates over the chest and back with a layered polymer weave sewn in below them, a size up from the usual mesh. The scarring is the part nobody mentions." + desc = "Rib plates over the chest and back with a layered polymer weave sewn in below them, a size up from the usual mesh. Rounds that come in at an angle skip off. The scarring is the part nobody mentions." icon_state = "dermal_mesh_hardweave" aug_overlay = "dermal_mesh_hardweave" chrome_load = 3 tier = CYBERWARE_TIER_2 mesh_armor = /datum/armor/cyberware_hardweave + deflect_chance = 30 // ========================================================================= // STRUT FRAME (T2): the bones and nothing else diff --git a/voidcrew/modules/cyberware/ware_pro_combat.dm b/voidcrew/modules/cyberware/ware_pro_combat.dm index 30371c5e83e..78b2a138ebb 100644 --- a/voidcrew/modules/cyberware/ware_pro_combat.dm +++ b/voidcrew/modules/cyberware/ware_pro_combat.dm @@ -13,7 +13,10 @@ * COMSIG_ATOM_PRE_BULLET_ACT handler. A source registered with melee = TRUE * (the Cascade line's windows) also dodges blows, through tg's own * check_block() via COMSIG_LIVING_CHECK_BLOCK, so a dodged blow is a real - * miss and never fake damage. + * miss and never fake damage. The same arbiter rolls the dermal plating's + * bullet deflection (see cyberware_register_deflect_source()), capped so a + * dodge and a deflect together never stop more than CYBERWARE_AVOID_CAP + * percent of shots. * - cyberware_is_ally(): the crew filter for auto-targeting ware (Deadeye's * tag, Widowline's cleave). Allies are people who share a ship team with * you; NPC boarders standing on your deck are NOT allies, and rival players @@ -27,6 +30,10 @@ #define CYBERWARE_DODGE_MOVE_WINDOW (1 SECONDS) /// The shortest gap between two dodge lines from a bearer nobody is playing (an NPC under fire). #define CYBERWARE_DODGE_LINE_GAP (1.5 SECONDS) +/// Dodge and deflect together never stop more than this percent of shots. +#define CYBERWARE_AVOID_CAP 60 +/// Stamina each deflected bullet costs the bearer, so focused fire still wears them down. +#define CYBERWARE_DEFLECT_STAMINA 5 /** * The one dodge roll for all cyberware. Lives on the mob; wares and status @@ -53,6 +60,8 @@ var/list/dodge_sources = list() /// key -> /datum/callback for the sources that also dodge blows. Each is in dodge_sources too. var/list/melee_sources = list() + /// key -> /datum/callback returning that source's live bullet-deflect chance (the dermal plating). + var/list/deflect_sources = list() /// world.time of the bearer's last move. var/last_move_time = 0 /// world.time a dodge was last announced: an NPC under fire says it at most once every CYBERWARE_DODGE_LINE_GAP, so chat isn't flooded @@ -73,6 +82,7 @@ /datum/component/cyberware_dodge/Destroy() dodge_sources = null melee_sources = null + deflect_sources = null return ..() /// Signal proc for [COMSIG_MOVABLE_MOVED]: timestamp the movement window. @@ -80,25 +90,83 @@ SIGNAL_HANDLER last_move_time = world.time -/// Signal proc for [COMSIG_ATOM_PRE_BULLET_ACT]: the actual dodge roll. +/** + * Signal proc for [COMSIG_ATOM_PRE_BULLET_ACT]: the dodge roll, then the + * deflect roll. One handler for both, so the cap on the two together holds + * no matter which chrome registered first. + */ /datum/component/cyberware_dodge/proc/on_pre_bullet_act(mob/living/source, obj/projectile/hitting_projectile, def_zone, piercing_hit, blocked) SIGNAL_HANDLER - if(world.time - last_move_time > CYBERWARE_DODGE_MOVE_WINDOW) - return NONE - if(source.stat != CONSCIOUS || source.body_position == LYING_DOWN || source.buckled) - return NONE if(!istype(hitting_projectile) || !hitting_projectile.is_hostile_projectile()) return NONE + var/dodge_chance = live_dodge_chance(source, hitting_projectile) + if(dodge_chance > 0 && prob(dodge_chance)) + INVOKE_ASYNC(src, PROC_REF(perform_sidestep), source, hitting_projectile) + return COMPONENT_BULLET_PIERCED + var/deflect_chance = min(live_deflect_chance(source, hitting_projectile), cyberware_deflect_cap(dodge_chance)) + if(deflect_chance <= 0 || !prob(deflect_chance)) + return NONE + glance(source, hitting_projectile) + return COMPONENT_BULLET_PIERCED + +/// The dodge chance against this shot right now: 0 unless the bearer is moving, upright and not point-blank. +/datum/component/cyberware_dodge/proc/live_dodge_chance(mob/living/source, obj/projectile/hitting_projectile) + if(!length(dodge_sources)) + return 0 + if(world.time - last_move_time > CYBERWARE_DODGE_MOVE_WINDOW) + return 0 + if(source.stat != CONSCIOUS || source.body_position == LYING_DOWN || source.buckled) + return 0 // Point-blank shots always land: an adjacent shooter is inside the // reflex loop. This is the anti-"unhittable in a brawl" rule. var/atom/shooter = hitting_projectile.firer if(shooter && shooter != source && get_dist(source, shooter) <= 1) - return NONE - var/best_chance = best_chance(dodge_sources) - if(best_chance <= 0 || !prob(best_chance)) - return NONE - INVOKE_ASYNC(src, PROC_REF(perform_sidestep), source, hitting_projectile) - return COMPONENT_BULLET_PIERCED + return 0 + return best_chance(dodge_sources) + +/** + * The deflect chance against this shot right now. Only real bullets (and + * each buckshot pellet on its own, since every pellet is its own projectile) + * glance off; lasers, energy and everything else never do. The plate only + * covers what the bearer is facing: a shot into the back always lands. + */ +/datum/component/cyberware_dodge/proc/live_deflect_chance(mob/living/source, obj/projectile/hitting_projectile) + if(!length(deflect_sources)) + return 0 + if(!istype(hitting_projectile, /obj/projectile/bullet) || hitting_projectile.armor_flag != BULLET) + return 0 + if(source.stat == DEAD) + return 0 + if(cyberware_shot_from_behind(source, hitting_projectile)) + return 0 + return best_chance(deflect_sources) + +/** + * The real glance: the bullet's heading turns back off the plate at a random + * angle and the round keeps flying (COMPONENT_BULLET_PIERCED, the same exit + * tg's own reflect takes), so it can still hit whoever stands in its new + * path. It loses energy like any tg ricochet. The bearer stays in the + * bullet's impacted list, so it can't come back for them. + */ +/datum/component/cyberware_dodge/proc/glance(mob/living/source, obj/projectile/bullet) + bullet.set_angle(SIMPLIFY_DEGREES(bullet.angle + pick(-1, 1) * rand(100, 170))) + bullet.damage *= bullet.ricochet_decay_damage + bullet.stamina *= bullet.ricochet_decay_damage + INVOKE_ASYNC(src, PROC_REF(perform_glance), source, bullet.name) + +/// The visible and physical half of a glance: sparks, a ping, and the stamina the hit still costs. +/datum/component/cyberware_dodge/proc/perform_glance(mob/living/source, bullet_name) + if(QDELETED(source)) + return + do_sparks(1, TRUE, source) + playsound(source, SFX_RICOCHET, 60, TRUE) + source.adjustStaminaLoss(CYBERWARE_DEFLECT_STAMINA) + if(source.client || world.time - last_message_at >= CYBERWARE_DODGE_LINE_GAP) + last_message_at = world.time + source.visible_message( + span_danger("\The [bullet_name] glances off [source]!"), + span_danger("\The [bullet_name] glances off you!"), + ) /// The highest live chance among these sources. Never additive. /datum/component/cyberware_dodge/proc/best_chance(list/sources) @@ -206,9 +274,45 @@ return arbiter.dodge_sources -= key arbiter.melee_sources -= key - if(!length(arbiter.dodge_sources)) + if(!length(arbiter.dodge_sources) && !length(arbiter.deflect_sources)) qdel(arbiter) +/** + * Adds (or refreshes) a keyed bullet-deflect source on the bearer, through + * the same arbiter as the dodge. The callback returns the source's CURRENT + * chance. Never additive: the highest live source rolls. + */ +/proc/cyberware_register_deflect_source(mob/living/bearer, key, datum/callback/chance_callback) + if(!istype(bearer) || !key || !chance_callback) + return + var/datum/component/cyberware_dodge/arbiter = bearer.LoadComponent(/datum/component/cyberware_dodge) + arbiter.deflect_sources[key] = chance_callback + +/// Removes a keyed deflect source; the arbiter cleans itself up with the last source of either kind. +/proc/cyberware_unregister_deflect_source(mob/living/bearer, key) + var/datum/component/cyberware_dodge/arbiter = bearer?.GetComponent(/datum/component/cyberware_dodge) + if(!arbiter) + return + arbiter.deflect_sources -= key + if(!length(arbiter.dodge_sources) && !length(arbiter.deflect_sources)) + qdel(arbiter) + +/** + * The most a deflect may roll after a dodge roll at dodge_chance missed, so + * that dodge and deflect together never stop more than CYBERWARE_AVOID_CAP + * percent of shots: dodge + (1 - dodge) * deflect <= cap. + */ +/proc/cyberware_deflect_cap(dodge_chance) + dodge_chance = clamp(dodge_chance, 0, 100) + if(dodge_chance >= CYBERWARE_AVOID_CAP) + return 0 + return (CYBERWARE_AVOID_CAP - dodge_chance) / (100 - dodge_chance) * 100 + +/// Whether this shot is travelling the way the bearer faces, so it strikes them in the back. +/proc/cyberware_shot_from_behind(mob/living/bearer, obj/projectile/shot) + var/offset = SIMPLIFY_DEGREES(shot.angle - dir2angle(bearer.dir) + 180) - 180 + return abs(offset) < 90 + // ---- Shared ally filter ------------------------------------------------ /** @@ -811,6 +915,8 @@ #undef CYBERWARE_DODGE_MOVE_WINDOW #undef CYBERWARE_DODGE_LINE_GAP +#undef CYBERWARE_AVOID_CAP +#undef CYBERWARE_DEFLECT_STAMINA #undef CYBERWARE_DEADEYE_TAG_RANGE #undef CYBERWARE_DEADEYE_TAG_SHOTS #undef CYBERWARE_DEADEYE_TAG_DURATION diff --git a/voidcrew/modules/cyberware/ware_street.dm b/voidcrew/modules/cyberware/ware_street.dm index 70d6d2a1751..9fe5c186db2 100644 --- a/voidcrew/modules/cyberware/ware_street.dm +++ b/voidcrew/modules/cyberware/ware_street.dm @@ -745,10 +745,14 @@ GLOBAL_LIST_INIT(cyberware_ink_palette, list( * add/subtract pair per flip is the whole lifecycle; re-applying on species * gain would stack the armor. The pair rides the failing-gated passive hooks * (BAL-4): EMP-scrambled or browned-out mesh armors nothing. + * + * The dermal ladder (Mesh, Hardweave, Slabskin) also turns bullets: a + * passive deflect source in the shared dodge arbiter (ware_pro_combat.dm), + * 15/30/45% by rung, front arc only, rides the same passive hooks. */ /obj/item/organ/cyberimp/cyberware/dermal_mesh name = "\improper Dermal Mesh weave" - desc = "A subdermal lattice of impact polymer. It won't stop anything serious, but it takes the edge off." + desc = "A subdermal lattice of impact polymer. It won't stop anything serious, but now and then a round skips off it." icon_state = "dermal_mesh" zone = BODY_ZONE_CHEST slot = ORGAN_SLOT_CYBERWARE_DERMAL @@ -761,9 +765,12 @@ GLOBAL_LIST_INIT(cyberware_ink_palette, list( var/datum/armor/mesh_armor = /datum/armor/cyberware_dermal_mesh /// TRUE while mesh_armor is mixed in; guards against double add/subtract. var/mesh_armor_applied = FALSE + /// Percent of bullets from the front that glance off, through the shared dodge arbiter. + var/deflect_chance = 15 /obj/item/organ/cyberimp/cyberware/dermal_mesh/chrome_passives_on(mob/living/carbon/bearer) . = ..() + cyberware_register_deflect_source(bearer, REF(src), CALLBACK(src, PROC_REF(get_deflect_chance))) if(mesh_armor_applied || !ishuman(bearer)) return mesh_armor_applied = TRUE @@ -772,6 +779,7 @@ GLOBAL_LIST_INIT(cyberware_ink_palette, list( /obj/item/organ/cyberimp/cyberware/dermal_mesh/chrome_passives_off(mob/living/carbon/bearer) . = ..() + cyberware_unregister_deflect_source(bearer, REF(src)) if(!mesh_armor_applied) return mesh_armor_applied = FALSE // reset before the validity skip, see Shock Coils @@ -780,6 +788,12 @@ GLOBAL_LIST_INIT(cyberware_ink_palette, list( var/mob/living/carbon/human/human_bearer = bearer human_bearer.physiology.armor = human_bearer.physiology.armor.subtract_other_armor(mesh_armor) +/// Deflect-source callback: nothing while failing. +/obj/item/organ/cyberimp/cyberware/dermal_mesh/proc/get_deflect_chance() + if(organ_flags & ORGAN_FAILING) + return 0 + return deflect_chance + // ---- 11. Gecko Grips --------------------------------------------------- /** diff --git a/voidcrew/modules/unit_tests/_unit_tests.dm b/voidcrew/modules/unit_tests/_unit_tests.dm index 412f552333c..c8f4bd7281b 100644 --- a/voidcrew/modules/unit_tests/_unit_tests.dm +++ b/voidcrew/modules/unit_tests/_unit_tests.dm @@ -92,6 +92,7 @@ #include "voidcrew_cyberware_anim.dm" #include "voidcrew_cyberware_anim2.dm" #include "voidcrew_cyberware_camo.dm" +#include "voidcrew_cyberware_deflect.dm" #include "voidcrew_cyberware_final.dm" #include "voidcrew_cyberware_kit.dm" #include "voidcrew_cyberware_mantis.dm" diff --git a/voidcrew/modules/unit_tests/voidcrew_cyberware_deflect.dm b/voidcrew/modules/unit_tests/voidcrew_cyberware_deflect.dm new file mode 100644 index 00000000000..48774034957 --- /dev/null +++ b/voidcrew/modules/unit_tests/voidcrew_cyberware_deflect.dm @@ -0,0 +1,70 @@ +/// The dermal ladder turns bullets from the front: a real glance that leaves +/// the bearer unhurt and keeps the round flying on a new heading, costs a +/// little stamina, never touches lasers or shots into the back, switches off +/// with the chrome, and never lifts dodge plus deflect past the shared cap. +/datum/unit_test/voidcrew_cyberware_deflect + +/datum/unit_test/voidcrew_cyberware_deflect/Run() + var/obj/item/organ/cyberimp/cyberware/dermal_mesh/mesh = allocate(/obj/item/organ/cyberimp/cyberware/dermal_mesh) + var/obj/item/organ/cyberimp/cyberware/dermal_mesh/hardweave/hardweave = allocate(/obj/item/organ/cyberimp/cyberware/dermal_mesh/hardweave) + var/obj/item/organ/cyberimp/cyberware/slabskin/slab = allocate(/obj/item/organ/cyberimp/cyberware/slabskin) + TEST_ASSERT_EQUAL(mesh.deflect_chance, 15, "Dermal Mesh deflects at the wrong chance") + TEST_ASSERT_EQUAL(hardweave.deflect_chance, 30, "Hardweave deflects at the wrong chance") + TEST_ASSERT_EQUAL(slab.deflect_chance, 45, "Slabskin deflects at the wrong chance") + + // The cap: dodge + (1 - dodge) * deflect never passes 60%. + TEST_ASSERT_EQUAL(cyberware_deflect_cap(0), 60, "The deflect cap is wrong with no dodge") + TEST_ASSERT_EQUAL(cyberware_deflect_cap(60), 0, "A full-cap dodge still left room to deflect") + for(var/dodge in list(0, 15, 30, 45, 59)) + var/deflect = min(slab.deflect_chance, cyberware_deflect_cap(dodge)) + var/combined = dodge + (100 - dodge) * deflect / 100 + TEST_ASSERT(combined <= 60.001, "Dodge [dodge] plus deflect stops [combined]% of shots") + + var/mob/living/carbon/human/patient = allocate(/mob/living/carbon/human/consistent) + var/mob/living/carbon/human/shooter = allocate(/mob/living/carbon/human/consistent, run_loc_floor_bottom_left) + TEST_ASSERT(slab.Insert(patient, TRUE), "Slabskin fixture failed") + var/datum/component/cyberware_dodge/arbiter = patient.GetComponent(/datum/component/cyberware_dodge) + TEST_ASSERT_NOTNULL(arbiter, "Slabskin didn't register a deflect source") + slab.deflect_chance = 100 + patient.setDir(SOUTH) + + // A bullet from the front (travelling north into a bearer facing south) glances off. + var/obj/projectile/bullet/c9mm/front = make_shot(/obj/projectile/bullet/c9mm, shooter, 0) + var/front_damage = front.damage + TEST_ASSERT_EQUAL(patient.bullet_act(front, BODY_ZONE_CHEST), BULLET_ACT_FORCE_PIERCE, "A bullet from the front didn't glance off") + TEST_ASSERT_EQUAL(patient.getBruteLoss(), 0, "A glanced bullet still hurt") + TEST_ASSERT(front.angle != 0, "The glanced bullet kept its heading") + TEST_ASSERT(front.damage < front_damage, "The glanced bullet lost no energy") + TEST_ASSERT(patient.getStaminaLoss() > 0, "A glance cost no stamina") + + // Every buckshot pellet is a bullet of its own and rolls on its own. + var/obj/projectile/bullet/pellet/shotgun_buckshot/pellet = make_shot(/obj/projectile/bullet/pellet/shotgun_buckshot, shooter, 0) + TEST_ASSERT_EQUAL(patient.bullet_act(pellet, BODY_ZONE_CHEST), BULLET_ACT_FORCE_PIERCE, "A buckshot pellet didn't glance off") + + // A bullet into the back always lands. + var/obj/projectile/bullet/c9mm/back = make_shot(/obj/projectile/bullet/c9mm, shooter, 180) + TEST_ASSERT_NOTEQUAL(patient.bullet_act(back, BODY_ZONE_CHEST), BULLET_ACT_FORCE_PIERCE, "A bullet into the back glanced off") + TEST_ASSERT(patient.getBruteLoss() > 0, "A bullet into the back did nothing") + + // Lasers are never deflected. + var/obj/projectile/beam/laser/laser = make_shot(/obj/projectile/beam/laser, shooter, 0) + TEST_ASSERT_NOTEQUAL(patient.bullet_act(laser, BODY_ZONE_CHEST), BULLET_ACT_FORCE_PIERCE, "A laser glanced off the plate") + TEST_ASSERT(patient.getFireLoss() > 0, "The laser did nothing") + + // A failing plate deflects nothing. + slab.organ_flags |= ORGAN_FAILING + var/obj/projectile/bullet/c9mm/failing = make_shot(/obj/projectile/bullet/c9mm, shooter, 0) + TEST_ASSERT_NOTEQUAL(patient.bullet_act(failing, BODY_ZONE_CHEST), BULLET_ACT_FORCE_PIERCE, "A failing plate still deflected") + slab.organ_flags &= ~ORGAN_FAILING + + // Taking the plate out takes the deflect with it. + slab.Remove(patient) + TEST_ASSERT_NULL(patient.GetComponent(/datum/component/cyberware_dodge), "The deflect source outlived the plate") + +/// A hostile shot from the shooter, heading at the given angle, ready for bullet_act(). +/datum/unit_test/voidcrew_cyberware_deflect/proc/make_shot(shot_type, mob/living/shooter, heading) + var/obj/projectile/shot = allocate(shot_type) + shot.firer = shooter + shot.def_zone = BODY_ZONE_CHEST + shot.set_angle(heading) + return shot diff --git a/wiki/content/cyberware.md b/wiki/content/cyberware.md index abc3f7f1a7b..aefaa0e9ca4 100644 --- a/wiki/content/cyberware.md +++ b/wiki/content/cyberware.md @@ -47,7 +47,7 @@ Prices are the same at both counters, stock is limited per round, and load is th | Cargo Cavity | 1,000 cr | 1 | A chest stash for one small item that no scanner, pat-down or Chrome Read finds | | Rockjaw drill fist | 1,200 cr | 2 | Deployable forearm mining drill; ore from walls you crack auto-pockets into a hopper | | Fixer's Fingers | 1,800 cr | 2 | Fingertip screwdriver, wrench and wirecutters, plus 25% faster timed actions full-time | -| Dermal Mesh weave | 2,000 cr | 2 | Light built-in armor: melee 15, bullet 15, laser 10 | +| Dermal Mesh weave | 2,000 cr | 2 | Light built-in armor: melee 15, bullet 15, laser 10. 15% of bullets from the front glance off | ### Tier 2: Pro chrome (credits or vouchers) @@ -76,7 +76,7 @@ Prices are the same at both counters, stock is limited per round, and load is th | Widowline monowire | 3 | 5 | A two-tile whip that ignores armor entirely and cleaves bystanders for 75% | | Popup Ronin housing | *loot only* | 5 | Deployable full-auto machine pistol in 10x24mm caseless. Reloads are sold at the parlor or printed with Experimental Ammunition | | Rigger socket | 3 | 4 | Jack into your own ship's helm from anywhere aboard and fly it from a corridor | -| Slabskin plate | 3 | 6 | Heavy built-in armor in every damage type. Solid hits visibly spark off you. Everyone can see you are plated | +| Slabskin plate | 3 | 6 | Heavy built-in armor in every damage type. 45% of bullets from the front glance off. Solid hits visibly spark off you. Everyone can see you are plated | | Mantis Blades (pair) | 4 | 3/arm | Deployable hardlight blades, plus a five-tile lunge that knocks the target down. Press and hold the lunge on a target to crouch, and let go to spring up to seven tiles; a solid hit or a stun while you crouch wastes it. A kill brings the lunge back almost at once. Three quick cuts to one limb open its artery, a two-blade lunge skewers someone already hurt, every third cut with both blades out crosses them and staggers, and a swing at the head of someone dying takes it off | | Bunker Buster housing | *loot only* | 5 | Two shaped micro-rockets: 60 damage, no splash, will never breach a hull. Reloads are sold at the parlor or printed with Experimental Ammunition | | Ghostskin weave | 4 | 5 | Ten seconds of optical camo. NPCs will not target you, and Chrome Reads scatter. Attacking, throwing, taking a hit or a shock, or an EMP drops it | @@ -118,7 +118,7 @@ The newer makers fill in the gaps under the big names. Each takes the same slot - **Brawler myomers**: a cased pair of forearms that hit hard, break rock, slam a held body down and force an airlock, slower than Gorilla Arms. - **Tinker's fingers** and the **Deepcore drill fist**: a looser set of fingertip tools, and a heavier drill on the same ore hopper. - **Holdout housing**: a snub revolver folded into the forearm, loaded one round at a time. -- **Hardweave plating** and the **Strut frame**: a heavier dermal weave, and a frame that keeps your bones whole. +- **Hardweave plating** and the **Strut frame**: a heavier dermal weave that turns 30% of bullets from the front, and a frame that keeps your bones whole. - **Deep-lung bladder**, **Heat-sink lining** and **Frostline lining**: a longer breath, a lining for the heat, a lining for the cold. - **Twitchwire** and the **Weaver shunt**: a twitchy reflex shunt, and the service model. - **Thumper ear**: a louder, shorter-ranged sonar behind the ear. @@ -137,4 +137,5 @@ Most of the roster also appears in zone loot caches. Street and utility chrome a - **EMP is the hard counter.** A pulse downs every piece you carry (nine seconds on a heavy blast, four and a half on a light one) then it reboots on its own. Gecko Grips spasm and throw whatever you are holding, Ghostskin camo collapses, and a Cascade window crashes on the spot. A Cradle tune-up cancels the reboot early. Actions and arm weapons stay unavailable while the chrome is offline. An EMP interrupting Lazarus during its charge does not spend the revival charge. - **Detection cuts both ways.** Chrome optics carry the Chrome Read, a scan that names a target's ware (street optics only count it), and the subject always feels the handshake. The Cargo Cavity never resolves on any scan, and Ghostskin scatters reads entirely. - **Death does not keep it.** Gibbing, dusting or destroying a limb ejects every piece onto the floor, and killers absolutely loot a chromed corpse. Cloning leaves the chrome on the body. +- **Plating turns bullets.** Dermal Mesh, Hardweave and Slabskin make bullets and buckshot pellets glance off with a spark and a ping, each pellet on its own. It never stops lasers, energy or blows, and never a shot into your back. The round keeps flying, so whoever stands beside you can still catch it. Each glance costs a little stamina, so steady fire still wears you down. A dodge and a glance together never stop more than 60% of shots. - **Nothing is subtle.** Slabskin sparks on every hit, the Chromatic Dermis flares whenever your chrome fires, and the heavy installs change how you look. Chromed is a fashion statement whether you want it or not. From 53d794712ee6b9ea325e93603f7a941df08d8fcf Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:36:31 -0800 Subject: [PATCH 12/20] Run surging mobs at their tier's rate relative to the Sandevistan user Each surge model carries a time multiplier (street 2x, service about 2.86x, Cascade 5x). In a user's field a mob on its own surge runs at its multiplier over the user's, capped at twice normal speed, through the freeze and the slow-mo. A faster surger runs the dilation machinery the other way: sooner deadlines, a quicker walk, faster shots and gore. Surgers are never tinted, a dead surger is let go for good, a Cascade NPC's tell shows through the tint, and a surger's rainbow trail skips the greens inside a field. The user's own shots now fly at full speed in their slow-mo, and shots hung in the freeze fly on at full speed when it ends. --- .../bounties/chrome/bounty_chrome_cascade.dm | 8 + voidcrew/modules/cyberware/ware_legend.dm | 5 + voidcrew/modules/cyberware/ware_line_sandy.dm | 2 + .../modules/cyberware/ware_sandy_timestop.dm | 223 +++++++++++++----- .../unit_tests/voidcrew_cyberware_timestop.dm | 151 +++++++++++- 5 files changed, 329 insertions(+), 60 deletions(-) diff --git a/voidcrew/modules/bounties/chrome/bounty_chrome_cascade.dm b/voidcrew/modules/bounties/chrome/bounty_chrome_cascade.dm index b4fbd38602a..862a53012c9 100644 --- a/voidcrew/modules/bounties/chrome/bounty_chrome_cascade.dm +++ b/voidcrew/modules/bounties/chrome/bounty_chrome_cascade.dm @@ -60,6 +60,14 @@ return FALSE return ..() +/datum/bounty_chrome_move/cascade/tell(atom/target) + cyberware_time_untint(owner) // its glow shows through a Sandevistan's slowed world (ware_sandy_timestop.dm) + return ..() + +/datum/bounty_chrome_move/cascade/clear_tell() + . = ..() + cyberware_time_retint(owner) + /datum/bounty_chrome_move/cascade/tell_text(atom/target) return "[owner]'s spine lights up with a rising whine." diff --git a/voidcrew/modules/cyberware/ware_legend.dm b/voidcrew/modules/cyberware/ware_legend.dm index 28ec059cfe7..bed5d4a5298 100644 --- a/voidcrew/modules/cyberware/ware_legend.dm +++ b/voidcrew/modules/cyberware/ware_legend.dm @@ -97,6 +97,9 @@ var/dilation_time = 3 SECONDS /// and how fast the world runs in slow-mo. var/dilation_scale = 0.2 + /// How much faster than normal the bearer runs in the window. A surging mob in someone else's field runs at its + /// multiplier over theirs. + var/time_multiplier = 5 /obj/item/organ/cyberimp/cyberware/cascade/on_mob_remove(mob/living/carbon/organ_owner, special = FALSE, movement_flags) . = ..() @@ -198,6 +201,7 @@ dilation_freeze = lattice.dilation_freeze dilation_time = lattice.dilation_time dilation_scale = lattice.dilation_scale + time_multiplier = lattice.time_multiplier return ..() /datum/status_effect/cyberware_cascade_window/on_apply() @@ -255,6 +259,7 @@ if(time_stop || dilated) // a surge that slowed the world keeps its green trail to the end of the window stopped_afterimage(old_loc) return + trail_hue = cyberware_time_trail_hue(owner, trail_hue) // ware_sandy_timestop.dm new /obj/effect/temp_visual/decoy/fading/cascade(old_loc, owner, trail_hue, afterimage_alpha) trail_hue = (trail_hue + CYBERWARE_CASCADE_HUE_STEP) % 360 diff --git a/voidcrew/modules/cyberware/ware_line_sandy.dm b/voidcrew/modules/cyberware/ware_line_sandy.dm index 57850ce3801..ac92f7b4efb 100644 --- a/voidcrew/modules/cyberware/ware_line_sandy.dm +++ b/voidcrew/modules/cyberware/ware_line_sandy.dm @@ -55,6 +55,7 @@ dilation_freeze = 0 dilation_time = 2 SECONDS dilation_scale = 0.5 + time_multiplier = 2 /datum/action/cooldown/cyberware/cascade_surge/street name = "Reflex Surge" @@ -95,6 +96,7 @@ dilation_freeze = 0 dilation_time = 3 SECONDS dilation_scale = 0.35 + time_multiplier = 1 / 0.35 /datum/action/cooldown/cyberware/cascade_surge/milspec name = "Reflex Surge" diff --git a/voidcrew/modules/cyberware/ware_sandy_timestop.dm b/voidcrew/modules/cyberware/ware_sandy_timestop.dm index 6d7c10d1040..4053fc844ec 100644 --- a/voidcrew/modules/cyberware/ware_sandy_timestop.dm +++ b/voidcrew/modules/cyberware/ware_sandy_timestop.dm @@ -36,16 +36,23 @@ * * Bosses (a finisher user, or a boss-rank human NPC) are held for SANDY_STOP_BOSS_HOLD at most. * - * A mob running its own Cascade surge (a bounty NPC's lattice) is never slowed, frozen or tinted while its window - * is open, and neither are its shots. If it surges while already held, the field lets it go at once and drops - * every slow it was carrying (surge_free()). When its window closes the field may take it again. + * Each surge model speeds its bearer up by a time multiplier (the lattice's time_multiplier): street 2x, service + * about 2.86x, Cascade 5x. A mob on its own surge (a bounty NPC's lattice) runs at its multiplier over the user's, + * at most SANDY_STOP_MAX_RELATIVE: a street NPC in a Cascade's field at 40%, an equal tier at 100% (left alone), + * a Cascade NPC in a street user's field at 200% (2.5x, capped). The user's freeze doesn't change that: a surging + * mob keeps its relative rate through it, so the freeze only stops those with no surge. Faster than the user runs + * the same machinery the other way (its deadlines, cooldowns, walk, shots and gore come sooner). A surging mob is + * never tinted, at any rate. If it surges while already held, the field lets it go at once, drops every slow it + * was carrying (surge_free()) and takes it back at its relative rate. When its window closes the field takes it at + * the field's own pace, unless it is dead: a corpse freed by a surge is left as it fell. * * The user's reflex dodge (ware_pro_combat.dm) is off while the world is slowed: the speed gap already does that * job. It comes back for the rest of the window. * * Look: affected things get a grey-green tint instead of tg's inversion. Floor items, blood decals and the * user are never touched, so they keep their colour, and so do the user's own shots. Shots the user fires - * while the world is frozen fly a tile, hang there, and go on when it starts moving again. + * while the world is frozen fly a tile and hang there until the freeze ends, then fly at full speed. Shots the + * user fires in the slow-mo are never held: they fly at full speed. */ /// Another player within this many tiles of the user means no dilation. @@ -56,6 +63,10 @@ #define SANDY_STOP_BOSS_HOLD (1 SECONDS) /// How far, in pixels, the user's own shots fly before they hang in a frozen world. #define SANDY_STOP_SHOT_HANG 32 +/// The fastest a surging mob runs in someone else's field, relative to normal speed. +#define SANDY_STOP_MAX_RELATIVE 2 +/// Relative rates this close to 1 count as the same speed: the mob is left alone. +#define SANDY_STOP_SAME_RATE 0.01 /// Trait source for everything the dilation applies. #define SANDY_STOP_TRAIT "sandevistan_time_stop" /// Filter name of the user's edge glow. @@ -64,12 +75,12 @@ /// Every atom the dilation has hold of, to its field. GLOBAL_LIST_EMPTY(cyberware_time_dilated) -/// How fast time runs for `thing`: 1 normally, 0 frozen, in between in slow-mo. +/// How fast time runs for `thing`: 1 normally, 0 frozen, in between in slow-mo, above 1 for a faster surger. /proc/cyberware_time_scale(atom/thing) if(isnull(thing)) return 1 var/datum/proximity_monitor/advanced/timestop/sandevistan/field = GLOB.cyberware_time_dilated[thing] - return field ? field.time_scale : 1 + return field ? field.scale_for(thing) : 1 /// Every dilation field running now. GLOBAL_LIST_EMPTY(cyberware_time_fields) @@ -112,11 +123,39 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) var/mob/living/body = thing return !!body.has_status_effect(/datum/status_effect/cyberware_cascade_window) -/// `surger` just opened its own surge window: every field lets it go and leaves it be until the window closes. -/proc/cyberware_surge_frees(mob/living/surger) +/// `surger` just opened its own surge window at `multiplier`: every field lets it go and runs it at its relative +/// rate until the window closes. +/proc/cyberware_surge_frees(mob/living/surger, multiplier) for(var/datum/proximity_monitor/advanced/timestop/sandevistan/field as anything in GLOB.cyberware_time_fields) if(field.host != surger) - field.surge_free(surger) + field.surge_free(surger, multiplier) + +/// The time multiplier of `body`'s open surge window, or 1 with none. +/proc/cyberware_surge_multiplier(mob/living/body) + if(!isliving(body)) + return 1 + var/datum/status_effect/cyberware_cascade_window/window = body.has_status_effect(/datum/status_effect/cyberware_cascade_window) + return window ? window.time_multiplier : 1 + +/// A surge's tell is about to play on `body`: let its own colours show through the field's tint while it winds up. +/proc/cyberware_time_untint(mob/living/body) + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = GLOB.cyberware_time_dilated[body] + field?.escape_the_negative_zone(body) + +/// The tell on `body` is over: if the field still holds it at its own pace, tint it again. +/proc/cyberware_time_retint(mob/living/body) + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = GLOB.cyberware_time_dilated[body] + if(field && !field.surging[body]) + field.into_the_negative_zone(body) + +/// The next hue for a surging `bearer`'s rainbow trail: inside someone's field it skips the greens, which read as the field's own tint. +/proc/cyberware_time_trail_hue(mob/living/bearer, hue) + if(hue < 70 || hue > 170) + return hue + for(var/datum/proximity_monitor/advanced/timestop/sandevistan/field as anything in GLOB.cyberware_time_fields) + if(field.surging[bearer]) + return 170 + return hue /// How long, in real time, `time` deciseconds of `thing`'s own time take from now. Exact through the freeze, /// the slow-mo and the end of the effect, as the field stands now. @@ -139,7 +178,9 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) LAZYOR(field.slowed_tags[thing], tag) var/list/real = list() if(loop != 1) - var/scale = field.time_scale > 0 ? field.time_scale : field.slow_scale + var/scale = field.scale_for(thing) + if(scale <= 0) + scale = field.slow_scale real += 0 for(var/time in times) real += time / max(scale, 0.05) @@ -155,19 +196,20 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) reached = at return real -/// Time `thing` lost in the last `elapsed` deciseconds: all of it frozen, none of it outside a field. +/// Time `thing` lost in the last `elapsed` deciseconds: all of it frozen, none of it outside a field. Negative for a +/// surger running faster than normal: it gained time. /proc/cyberware_time_lost(atom/thing, elapsed) var/datum/proximity_monitor/advanced/timestop/sandevistan/field = GLOB.cyberware_time_dilated[thing] if(!field || elapsed <= 0) return 0 - return elapsed * (1 - field.time_scale) + return elapsed * (1 - field.scale_for(thing)) /// Time a held `thing` has lost since the field last pushed its deadlines back. /proc/cyberware_time_lag(atom/thing) var/datum/proximity_monitor/advanced/timestop/sandevistan/field = GLOB.cyberware_time_dilated[thing] if(!field) return 0 - return (world.time - field.last_tick) * (1 - field.time_scale) + return (world.time - field.last_tick) * (1 - field.scale_for(thing)) /// A burst clock's start for a burst beginning now. The field pushes it back with the shooter's deadlines. /proc/cyberware_burst_clock(atom/shooter) @@ -278,13 +320,17 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) var/freeze_time = 0 /// How fast the world runs after that. var/slow_scale = 1 + /// The user's own time multiplier (their surge model's), for surging mobs' relative rates. + var/user_multiplier = 1 /// `immune_atoms` must be list(user). `time` is the whole effect; `freeze` of it is frozen, the rest at `scale`. -/obj/effect/timestop/sandevistan/Initialize(mapload, radius, time, list/immune_atoms, start = TRUE, silent = TRUE, freeze = 0, scale = 1) +/// `multiplier` is the user's surge model's time multiplier. +/obj/effect/timestop/sandevistan/Initialize(mapload, radius, time, list/immune_atoms, start = TRUE, silent = TRUE, freeze = 0, scale = 1, multiplier = 1) if(length(immune_atoms)) user = immune_atoms[1] freeze_time = freeze slow_scale = scale + user_multiplier = multiplier return ..(mapload, radius, time, immune_atoms, start, TRUE) /obj/effect/timestop/sandevistan/Destroy() @@ -296,7 +342,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) QDEL_IN(src, duration) if(QDELETED(user)) return - var/datum/proximity_monitor/advanced/timestop/sandevistan/field = new(user, freezerange, TRUE, immune, antimagic_flags, FALSE, freeze_time > 0 ? 0 : slow_scale) + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = new(user, freezerange, TRUE, immune, antimagic_flags, FALSE, freeze_time > 0 ? 0 : slow_scale, user_multiplier) field.stopper = src field.slow_scale = slow_scale if(freeze_time > 0) @@ -345,9 +391,14 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) var/list/surging = list() /// Held mobs, to the tags of their slowed hv_vis_play() animations, to drop if they break free. var/list/slowed_tags = list() + /// The user's time multiplier. A surging mob runs at its own over this. + var/user_multiplier = 1 + /// Surging mobs held at their relative rate, and their shots, to that rate. Never 0, never tinted. + var/list/own_scales = list() -/datum/proximity_monitor/advanced/timestop/sandevistan/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, list/immune, antimagic_flags, channelled, start_scale = 0) +/datum/proximity_monitor/advanced/timestop/sandevistan/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, list/immune, antimagic_flags, channelled, start_scale = 0, _user_multiplier = 1) time_scale = start_scale + user_multiplier = _user_multiplier > 0 ? _user_multiplier : 1 last_tick = world.time GLOB.cyberware_time_fields += src . = ..() @@ -366,6 +417,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) UnregisterSignal(surger, COMSIG_QDELETING) surging.Cut() . = ..() // unfreeze_all() lets everything go + own_scales.Cut() released.Cut() /// How long, in real time from now, `local` deciseconds of `thing`'s own time take: the rest of the freeze, @@ -377,6 +429,14 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) ends = min(ends, boss_holds[thing]) if(ends <= now) return local + var/own = own_scales[thing] + if(own) // a surger at its own steady rate, freeze or not + var/own_span = (ends - now) * own + if(local <= own_span) + return local / own + return ends - now + local - own_span + if(surging[thing]) + return local var/at = now var/scale = time_scale if(scale <= 0) @@ -390,11 +450,26 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) at = ends return at - now + local +/// How fast time runs for `thing` in this field: a surger's own relative rate, 1 for a surger left alone, else the field's pace. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/scale_for(atom/thing) + var/own = own_scales[thing] + if(own) + return own + if(surging[thing]) + return 1 + return time_scale + +/// The rate, against normal speed, of a mob surging at `multiplier` in this field. Capped at SANDY_STOP_MAX_RELATIVE. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/relative_rate(multiplier) + if(!multiplier || multiplier <= 0) + multiplier = 1 + return min(multiplier / user_multiplier, SANDY_STOP_MAX_RELATIVE) + /datum/proximity_monitor/advanced/timestop/sandevistan/freeze_atom(atom/movable/A) if(!istype(A) || GLOB.cyberware_time_dilated[A] || global_frozen_atoms[A] || released[A] || immune[A] || surging[A]) return FALSE if(cyberware_own_surge(A)) - leave_surger(A) + take_surger(A, cyberware_surge_multiplier(A)) return FALSE if(ismob(A)) var/mob/M = A @@ -406,14 +481,14 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) hold_mob(A) else if(isprojectile(A)) var/obj/projectile/shot = A - if(shot.firer && (surging[shot.firer] || cyberware_own_surge(shot.firer))) // a surging mob's shots fly at its speed + if(shot.firer && immune[shot.firer]) // the user's own: hang_shot takes these in the freeze; in the slow-mo they fly free return FALSE - if(shot.firer && immune[shot.firer]) - if(time_scale <= 0) // hang_shot takes these + if(shot.firer && (surging[shot.firer] || cyberware_own_surge(shot.firer))) // a surging mob's shots fly at its rate + var/rate = surging[shot.firer] ? scale_for(shot.firer) : relative_rate(cyberware_surge_multiplier(shot.firer)) + if(abs(rate - 1) < SANDY_STOP_SAME_RATE) return FALSE - user_shots[shot] = TRUE + own_scales[shot] = rate tint = FALSE - escape_the_negative_zone(shot) // in case it was tinted before it had a firer hold_shot(shot) else if(ismecha(A)) hold_mech(A) @@ -466,6 +541,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) frozen_structures -= A user_shots -= A slowed_tags -= A + own_scales -= A UnregisterSignal(A, COMSIG_QDELETING) escape_the_negative_zone(A) frozen_things -= A @@ -474,14 +550,15 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) // ----- mobs ----- -/// Takes hold of a mob: frozen or slowed, whichever the field is doing. +/// Takes hold of a mob: frozen or slowed (or sped up, for a faster surger), whichever its rate is. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/hold_mob(mob/living/victim) held_mobs[victim] = TRUE - if(time_scale <= 0) + var/rate = scale_for(victim) + if(rate <= 0) freeze_mob(victim) else slow_mob(victim) - if(cyberware_time_stop_boss(victim)) + if(rate < 1 && cyberware_time_stop_boss(victim)) boss_holds[victim] = world.time + SANDY_STOP_BOSS_HOLD /datum/proximity_monitor/advanced/timestop/sandevistan/proc/let_go_mob(mob/living/victim) @@ -507,13 +584,16 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) var/mob/living/simple_animal/animal_victim = victim animal_victim.toggle_ai(initial(animal_victim.AIStatus)) -/// Slows a mob's walk to the field's pace. Its timers are pushed back every tick in process(). +/// Paces a mob's walk to its rate in the field: slower, or faster for a faster surger. Its timers move with it every tick(). /datum/proximity_monitor/advanced/timestop/sandevistan/proc/slow_mob(mob/living/victim) - var/base_delay = max(victim.cached_multiplicative_slowdown, 1) - victim.add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation, multiplicative_slowdown = base_delay * (1 / time_scale - 1)) + var/rate = scale_for(victim) + victim.remove_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation) + // A slowed walk gets at least a decisecond's delay to stretch; a faster one divides what it really has. + var/base_delay = rate < 1 ? max(victim.cached_multiplicative_slowdown, 1) : max(victim.cached_multiplicative_slowdown, 0) + victim.add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation, multiplicative_slowdown = base_delay * (1 / rate - 1)) if(istype(victim, /mob/living/simple_animal/hostile)) var/mob/living/simple_animal/hostile/animal_victim = victim - animal_victim.move_to_delay = initial(animal_victim.move_to_delay) / time_scale + animal_victim.move_to_delay = initial(animal_victim.move_to_delay) / rate /datum/proximity_monitor/advanced/timestop/sandevistan/proc/unslow_mob(mob/living/victim) victim.remove_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation) @@ -521,7 +601,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) var/mob/living/simple_animal/hostile/animal_victim = victim animal_victim.move_to_delay = initial(animal_victim.move_to_delay) -/// Pushes every timing deadline `victim` has that hasn't passed yet back by `push` deciseconds. +/// Pushes every timing deadline `victim` has that hasn't passed yet back by `push` deciseconds (forward, if negative). /datum/proximity_monitor/advanced/timestop/sandevistan/proc/push_deadlines(mob/living/victim, push) var/now = world.time if(victim.next_move > now) @@ -553,18 +633,31 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) // ----- mobs on their own surge ----- -/// Leaves `surger` be while its own window runs. It isn't held. -/datum/proximity_monitor/advanced/timestop/sandevistan/proc/leave_surger(mob/living/surger) +/** + * `surger` runs its own surge at `multiplier`: at the same rate as the user it is left alone; otherwise the field + * holds it at its relative rate, slower or faster, untinted. The user's freeze doesn't touch it. + */ +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/take_surger(mob/living/surger, multiplier) surging[surger] = TRUE RegisterSignal(surger, COMSIG_QDELETING, PROC_REF(held_gone), override = TRUE) + var/rate = relative_rate(multiplier) + if(abs(rate - 1) < SANDY_STOP_SAME_RATE) + return + own_scales[surger] = rate + hold_mob(surger) + GLOB.cyberware_time_dilated[surger] = src + frozen_things[surger] = TRUE + SEND_SIGNAL(surger, COMSIG_ATOM_TIMESTOP_FREEZE, src) /** - * `surger` opened its own surge: if the field holds it, let it go now and drop every slow it carried (its walk, - * its pauses, its last bit of lost time, its slowed animations and the shots it fired), then leave it be. + * `surger` opened its own surge at `multiplier`: if the field holds it, let it go now and drop every slow it + * carried (its walk, its pauses, its last bit of lost time, its slowed animations and the shots it fired), then + * take it back at its relative rate, shots and all. */ -/datum/proximity_monitor/advanced/timestop/sandevistan/proc/surge_free(mob/living/surger) +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/surge_free(mob/living/surger, multiplier) if(surging[surger]) return + var/list/its_shots = list() if(GLOB.cyberware_time_dilated[surger] == src) var/lag = cyberware_time_lag(surger) if(lag > 0) @@ -576,21 +669,34 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) for(var/obj/projectile/shot as anything in held_shots.Copy()) if(shot.firer == surger) unfreeze_atom(shot) + its_shots += shot else var/turf/centre = get_turf(host) var/turf/here = get_turf(surger) if(!centre || !here || here.z != centre.z || get_dist(centre, here) > current_range) - return // out of reach: if it walks in still surging, freeze_atom() leaves it be - leave_surger(surger) - -/// Every tick: surgers whose window has closed are fair game again, if they are still in range. + return // out of reach: if it walks in still surging, freeze_atom() takes it at its rate + take_surger(surger, multiplier) + for(var/obj/projectile/shot as anything in its_shots) + if(!QDELETED(shot)) + freeze_atom(shot) + +/// Every tick: surgers whose window has closed go back to the field's pace, if they are still in range. A surger +/// that died is let go for good: its corpse is never taken back and tinted. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/check_surgers() var/turf/centre = get_turf(host) for(var/mob/living/surger as anything in surging.Copy()) - if(cyberware_own_surge(surger)) + if(surger.stat != DEAD && cyberware_own_surge(surger)) continue + if(GLOB.cyberware_time_dilated[surger] == src) + var/lag = cyberware_time_lag(surger) + if(lag) + push_deadlines(surger, lag) + unfreeze_atom(surger) surging -= surger UnregisterSignal(surger, COMSIG_QDELETING) + if(surger.stat == DEAD) + released[surger] = TRUE + continue var/turf/here = get_turf(surger) if(centre && here && here.z == centre.z && get_dist(centre, here) <= current_range) freeze_atom(surger) @@ -642,10 +748,11 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) /datum/proximity_monitor/advanced/timestop/sandevistan/proc/hold_shot(obj/projectile/shot) held_shots[shot] = shot.speed - if(time_scale <= 0) + var/rate = scale_for(shot) + if(rate <= 0) shot.paused = TRUE else - shot.speed = held_shots[shot] * time_scale + shot.speed = held_shots[shot] * rate /datum/proximity_monitor/advanced/timestop/sandevistan/proc/let_go_shot(obj/projectile/shot) shot.speed = held_shots[shot] @@ -658,20 +765,15 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) RegisterSignal(shot, COMSIG_PROJECTILE_FIRE, PROC_REF(hang_shot)) /// Signal proc for [COMSIG_PROJECTILE_FIRE]: the user's shot keeps its colour. In a frozen world it flies a -/// tile, then hangs; in slow-mo it crawls like everything else. +/// tile, then hangs until the freeze ends (tick() lets it go at full speed); in the slow-mo it flies at full speed. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/hang_shot(obj/projectile/shot) SIGNAL_HANDLER UnregisterSignal(shot, COMSIG_PROJECTILE_FIRE) if(QDELETED(shot) || shot.deletion_queued) return if(time_scale > 0) - user_shots[shot] = TRUE - escape_the_negative_zone(shot) // in case it was tinted before it had a firer - if(!held_shots[shot]) - hold_shot(shot) - GLOB.cyberware_time_dilated[shot] = src - frozen_things[shot] = TRUE - RegisterSignal(shot, COMSIG_QDELETING, PROC_REF(held_gone), override = TRUE) + if(GLOB.cyberware_time_dilated[shot] == src) + unfreeze_atom(shot) // caught before it had a firer: its speed and colour back return // A hitscan beam takes its first step itself once paused (process_hitscan) and stops there. if(!shot.hitscan) @@ -703,6 +805,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) boss_holds -= thing surging -= thing slowed_tags -= thing + own_scales -= thing frozen_structures -= thing frozen_things -= thing GLOB.cyberware_time_dilated -= thing @@ -713,6 +816,8 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) /datum/proximity_monitor/advanced/timestop/sandevistan/proc/set_time_scale(new_scale) time_scale = new_scale for(var/mob/living/victim as anything in held_mobs) + if(own_scales[victim]) // a surger keeps its own rate + continue unfreeze_mob(victim) if(time_scale <= 0) unslow_mob(victim) @@ -720,6 +825,8 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) else slow_mob(victim) for(var/obj/projectile/shot as anything in held_shots) + if(own_scales[shot]) + continue shot.paused = time_scale <= 0 if(time_scale > 0) shot.speed = held_shots[shot] * time_scale @@ -743,10 +850,12 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) return var/elapsed = world.time - last_tick last_tick = world.time + for(var/mob/living/victim as anything in held_mobs) + var/lost = elapsed * (1 - scale_for(victim)) // a faster surger's is negative: its deadlines come sooner + if(lost) + push_deadlines(victim, lost) var/push = elapsed * (1 - time_scale) if(push > 0) - for(var/mob/living/victim as anything in held_mobs) - push_deadlines(victim, push) for(var/atom/movable/thing as anything in held_throws) var/datum/thrownthing/throw_datum = thing.throwing if(throw_datum && !throw_datum.paused) @@ -760,6 +869,8 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) released[boss] = TRUE if(thaw_at && world.time >= thaw_at) thaw_at = 0 + for(var/obj/projectile/shot as anything in user_shots.Copy()) // the user's hung shots fly on at full speed + unfreeze_atom(shot) set_time_scale(slow_scale) /datum/proximity_monitor/advanced/timestop/sandevistan/into_the_negative_zone(atom/A) @@ -776,6 +887,8 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) var/dilation_time = 3 SECONDS /// How fast the world runs in slow-mo. var/dilation_scale = 0.2 + /// How much faster than normal the bearer runs: sets surging mobs' relative rates in each other's fields. + var/time_multiplier = 5 /// The running dilation, if any. var/obj/effect/timestop/sandevistan/time_stop /// Whether this window slowed the world. Its trail stays green after the dilation ends. @@ -783,7 +896,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) /// Slows the world if the bearer is a player and nobody else is near. Called as the window opens. /datum/status_effect/cyberware_cascade_window/proc/try_time_stop() - cyberware_surge_frees(owner) // anyone's surge breaks them out of someone else's field + cyberware_surge_frees(owner, time_multiplier) // anyone's surge runs at its own rate in someone else's field if(dilation_freeze + dilation_time <= 0 || !owner.ckey || time_stop) return FALSE if(cyberware_time_stop_witnessed(owner)) @@ -791,7 +904,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) var/turf/here = get_turf(owner) if(!here) return FALSE - time_stop = new(here, SANDY_STOP_FIELD_RANGE, dilation_freeze + dilation_time, list(owner), TRUE, TRUE, dilation_freeze, dilation_scale) + time_stop = new(here, SANDY_STOP_FIELD_RANGE, dilation_freeze + dilation_time, list(owner), TRUE, TRUE, dilation_freeze, dilation_scale, time_multiplier) if(QDELETED(time_stop)) time_stop = null return FALSE @@ -892,5 +1005,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) #undef SANDY_STOP_FIELD_RANGE #undef SANDY_STOP_BOSS_HOLD #undef SANDY_STOP_SHOT_HANG +#undef SANDY_STOP_MAX_RELATIVE +#undef SANDY_STOP_SAME_RATE #undef SANDY_STOP_TRAIT #undef SANDY_STOP_OUTLINE diff --git a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm index 16c7b40edf5..559000aab03 100644 --- a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm +++ b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm @@ -11,11 +11,11 @@ /datum/unit_test/voidcrew_cyberware_timestop abstract_type = /datum/unit_test/voidcrew_cyberware_timestop -/// A player body (a key, no client) at `where` with a Cascade lattice in it -/datum/unit_test/voidcrew_cyberware_timestop/proc/make_user(turf/where, key) +/// A player body (a key, no client) at `where` with a Cascade lattice (or `lattice_type`) in it +/datum/unit_test/voidcrew_cyberware_timestop/proc/make_user(turf/where, key, lattice_type = /obj/item/organ/cyberimp/cyberware/cascade) var/mob/living/carbon/human/user = allocate(/mob/living/carbon/human/consistent, where) user.key = key - var/obj/item/organ/cyberimp/cyberware/cascade/lattice = allocate(/obj/item/organ/cyberimp/cyberware/cascade) + var/obj/item/organ/cyberimp/cyberware/cascade/lattice = allocate(lattice_type) TEST_ASSERT(lattice.Insert(user, special = TRUE), "Test fixture: the lattice wouldn't go in") return user @@ -184,10 +184,10 @@ /datum/unit_test/voidcrew_cyberware_timestop/proc/tinted(atom/thing) return length(thing.atom_colours) >= TEMPORARY_COLOUR_PRIORITY && !isnull(thing.atom_colours[TEMPORARY_COLOUR_PRIORITY]) -/// A raider NPC at `where` with its own Cascade lattice -/datum/unit_test/voidcrew_cyberware_timestop/proc/make_surger(turf/where) +/// A raider NPC at `where` with its own Cascade lattice (or `lattice_type`) +/datum/unit_test/voidcrew_cyberware_timestop/proc/make_surger(turf/where, lattice_type = /obj/item/organ/cyberimp/cyberware/cascade) var/mob/living/carbon/human/npc/raider = allocate(/mob/living/carbon/human/npc, where) - var/obj/item/organ/cyberimp/cyberware/cascade/lattice = allocate(/obj/item/organ/cyberimp/cyberware/cascade) + var/obj/item/organ/cyberimp/cyberware/cascade/lattice = allocate(lattice_type) TEST_ASSERT(lattice.Insert(raider, special = TRUE), "Test fixture: the lattice wouldn't go in the NPC") return raider @@ -281,3 +281,142 @@ user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) raider.remove_status_effect(/datum/status_effect/cyberware_cascade_window) raider.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) + +/// Ends `body`'s window and crash, for the end of a test +/datum/unit_test/voidcrew_cyberware_timestop/proc/end_surge(mob/living/body) + body.remove_status_effect(/datum/status_effect/cyberware_cascade_window) + body.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) + +/// A surging mob runs at its own multiplier over the user's, at most twice normal speed +/datum/unit_test/voidcrew_cyberware_timestop/relative_rates + +/datum/unit_test/voidcrew_cyberware_timestop/relative_rates/Run() + var/obj/item/organ/cyberimp/cyberware/cascade/street/street = allocate(/obj/item/organ/cyberimp/cyberware/cascade/street) + var/obj/item/organ/cyberimp/cyberware/cascade/milspec/service = allocate(/obj/item/organ/cyberimp/cyberware/cascade/milspec) + var/obj/item/organ/cyberimp/cyberware/cascade/cascade = allocate(/obj/item/organ/cyberimp/cyberware/cascade) + TEST_ASSERT_EQUAL(street.time_multiplier, 2, "The street rig isn't 2x") + TEST_ASSERT(abs(service.time_multiplier - 2.857) < 0.01, "The service lattice isn't about 2.86x ([service.time_multiplier])") + TEST_ASSERT_EQUAL(cascade.time_multiplier, 5, "The Cascade isn't 5x") + + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + sleep(0.1 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield + TEST_ASSERT_EQUAL(field.user_multiplier, 5, "A Cascade user's field doesn't know they run at 5x") + TEST_ASSERT(abs(field.relative_rate(2) - 0.4) < 0.001, "A street surger in a Cascade's field isn't at 40%") + TEST_ASSERT_EQUAL(field.relative_rate(5), 1, "An equal surger isn't at full speed") + field.user_multiplier = 2 // as a street user's field + TEST_ASSERT_EQUAL(field.relative_rate(5), 2, "A Cascade surger in a street user's field isn't capped at 200%") + TEST_ASSERT(abs(field.relative_rate(1 / 0.35) - 1.4286) < 0.001, "A service surger in a street user's field isn't at about 143%") + field.user_multiplier = 5 + end_surge(user) + +/// A slower surger in the user's field is held at its relative rate through the freeze, never frozen or tinted +/datum/unit_test/voidcrew_cyberware_timestop/slower_surger + +/datum/unit_test/voidcrew_cyberware_timestop/slower_surger/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/mob/living/carbon/human/npc/raider = make_surger(spot(2, 0), /obj/item/organ/cyberimp/cyberware/cascade/street) + TEST_ASSERT_NOTNULL(surge(raider), "The NPC's window didn't open") + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + sleep(0.1 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield + TEST_ASSERT(field.surging[raider], "The field didn't notice the NPC's surge") + TEST_ASSERT(abs(cyberware_time_scale(raider) - 0.4) < 0.001, "A street surger in a Cascade's field isn't at 40% ([cyberware_time_scale(raider)])") + TEST_ASSERT(!HAS_TRAIT(raider, TRAIT_IMMOBILIZED) && !HAS_TRAIT(raider, TRAIT_AI_PAUSED), "A surging NPC was frozen by the user's freeze") + TEST_ASSERT(!tinted(raider), "A surging NPC was tinted") + TEST_ASSERT(raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "A slower surger isn't slowed") + var/lost = cyberware_time_lost(raider, 10) + TEST_ASSERT(lost > 5.9 && lost < 6.1, "A slower surger's clock didn't run at 40% (lost [lost] of 10)") + sleep(1.2 SECONDS) // into the user's slow-mo: the surger keeps its own rate + TEST_ASSERT(abs(cyberware_time_scale(raider) - 0.4) < 0.001, "The thaw changed a surger's rate") + end_surge(user) + end_surge(raider) + +/// A faster surger runs faster than normal: deadlines sooner, a quicker walk, quicker shots and gore +/datum/unit_test/voidcrew_cyberware_timestop/faster_surger + +/datum/unit_test/voidcrew_cyberware_timestop/faster_surger/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser", /obj/item/organ/cyberimp/cyberware/cascade/street) + var/mob/living/carbon/human/npc/raider = make_surger(spot(2, 0)) + var/obj/projectile/bullet/shot = allocate(/obj/projectile/bullet, spot(1, 1)) + shot.firer = raider + var/shot_speed = shot.speed + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + sleep(0.1 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield + TEST_ASSERT_EQUAL(field.user_multiplier, 2, "A street user's field doesn't know they run at 2x") + TEST_ASSERT(field.held_shots[shot], "Test fixture: the NPC's shot isn't held before it surges") + + TEST_ASSERT_NOTNULL(surge(raider), "The NPC's window didn't open") + TEST_ASSERT_EQUAL(cyberware_time_scale(raider), 2, "A Cascade surger in a street user's field isn't capped at 200%") + TEST_ASSERT(!tinted(raider), "A faster surger was tinted") + TEST_ASSERT_EQUAL(cyberware_time_lost(raider, 10), -10, "A faster surger's clock doesn't run at double speed") + TEST_ASSERT_EQUAL(cyberware_dilated_time(raider, 10), 5, "A faster surger's gore doesn't play at double speed") + var/datum/movespeed_modifier/pace = raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation) + TEST_ASSERT(pace && pace.multiplicative_slowdown <= 0, "A faster surger doesn't walk faster") + TEST_ASSERT(field.held_shots[shot] && !shot.paused && shot.speed == shot_speed * 2, "A faster surger's shot doesn't fly at double speed") + + var/next_move_was = world.time + 5 SECONDS + raider.next_move = next_move_was + sleep(0.5 SECONDS) // a couple of field ticks + TEST_ASSERT(raider.next_move < next_move_was - 0.3 SECONDS, "A faster surger's deadlines didn't come sooner") + end_surge(user) + end_surge(raider) + +/// The user's shots in their own slow-mo fly at full speed; shots hung in the freeze fly on at full speed when it ends +/datum/unit_test/voidcrew_cyberware_timestop/user_shots_free + +/datum/unit_test/voidcrew_cyberware_timestop/user_shots_free/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + sleep(0.1 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield + + // One hanging in the freeze, as hang_shot leaves it. + var/obj/projectile/bullet/hung = allocate(/obj/projectile/bullet, spot(1, 0)) + hung.firer = user + var/hung_speed = hung.speed + field.user_shots[hung] = TRUE + field.hold_shot(hung) + GLOB.cyberware_time_dilated[hung] = field + field.frozen_things[hung] = TRUE + TEST_ASSERT(hung.paused, "Test fixture: the hung shot isn't paused") + + sleep(1.2 SECONDS) // into the slow-mo + TEST_ASSERT(field.time_scale > 0, "Test fixture: still frozen") + TEST_ASSERT(!hung.paused && hung.speed == hung_speed && !field.held_shots[hung], "A shot hung in the freeze didn't fly on at full speed") + + var/obj/projectile/bullet/shot = allocate(/obj/projectile/bullet, spot(1, 1)) + shot.firer = user + var/shot_speed = shot.speed + TEST_ASSERT(!field.freeze_atom(shot), "The field took the user's own slow-mo shot") + field.hang_shot(shot) + TEST_ASSERT(!field.held_shots[shot] && !shot.paused && shot.speed == shot_speed, "The user's slow-mo shot was slowed") + TEST_ASSERT(!tinted(shot), "The user's slow-mo shot was tinted") + end_surge(user) + +/// A surging NPC that dies is let go for good: its corpse isn't taken back and tinted +/datum/unit_test/voidcrew_cyberware_timestop/no_corpse_retake + +/datum/unit_test/voidcrew_cyberware_timestop/no_corpse_retake/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/mob/living/carbon/human/npc/raider = make_surger(spot(2, 0), /obj/item/organ/cyberimp/cyberware/cascade/street) + TEST_ASSERT_NOTNULL(surge(raider), "The NPC's window didn't open") + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + sleep(0.1 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield + TEST_ASSERT(field.surging[raider], "Test fixture: the field didn't notice the NPC's surge") + raider.death() + sleep(0.5 SECONDS) // a couple of field ticks + TEST_ASSERT(!field.surging[raider], "The field kept a dead surger as surging") + TEST_ASSERT_NULL(GLOB.cyberware_time_dilated[raider], "The field took a dead surger back") + TEST_ASSERT(!tinted(raider), "A dead surger's corpse was tinted") + TEST_ASSERT(field.released[raider], "A dead surger wasn't let go for good") + end_surge(user) + end_surge(raider) From f52637c22dbf90bebde0f202ed91d161b6c7a916 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:45:50 -0800 Subject: [PATCH 13/20] Fly every bullet in the dilation field at the field's pace The user's shots crawl in their own slow-mo again and hang in the freeze, keeping their colour. A surging mob's shots fly at the field's pace too, untinted; the surger still fires on its own relative clock. --- .../modules/cyberware/ware_sandy_timestop.dm | 44 ++++++++------- .../unit_tests/voidcrew_cyberware_timestop.dm | 55 ++++++++++--------- 2 files changed, 53 insertions(+), 46 deletions(-) diff --git a/voidcrew/modules/cyberware/ware_sandy_timestop.dm b/voidcrew/modules/cyberware/ware_sandy_timestop.dm index 4053fc844ec..2e3c53b3d26 100644 --- a/voidcrew/modules/cyberware/ware_sandy_timestop.dm +++ b/voidcrew/modules/cyberware/ware_sandy_timestop.dm @@ -41,7 +41,7 @@ * at most SANDY_STOP_MAX_RELATIVE: a street NPC in a Cascade's field at 40%, an equal tier at 100% (left alone), * a Cascade NPC in a street user's field at 200% (2.5x, capped). The user's freeze doesn't change that: a surging * mob keeps its relative rate through it, so the freeze only stops those with no surge. Faster than the user runs - * the same machinery the other way (its deadlines, cooldowns, walk, shots and gore come sooner). A surging mob is + * the same machinery the other way (its deadlines, cooldowns, walk and gore come sooner). A surging mob is * never tinted, at any rate. If it surges while already held, the field lets it go at once, drops every slow it * was carrying (surge_free()) and takes it back at its relative rate. When its window closes the field takes it at * the field's own pace, unless it is dead: a corpse freed by a surge is left as it fell. @@ -50,9 +50,10 @@ * job. It comes back for the rest of the window. * * Look: affected things get a grey-green tint instead of tg's inversion. Floor items, blood decals and the - * user are never touched, so they keep their colour, and so do the user's own shots. Shots the user fires - * while the world is frozen fly a tile and hang there until the freeze ends, then fly at full speed. Shots the - * user fires in the slow-mo are never held: they fly at full speed. + * user are never touched, so they keep their colour, and so do the user's own shots. Every bullet in the field + * flies at the field's pace, whoever fired it: the user's and a surging mob's crawl in the slow-mo like the rest + * (a faster surger fires sooner, but its bullets still crawl), untinted. Shots the user fires while the world is + * frozen fly a tile, hang there, and go on when it starts moving again. A shot leaving the field gets its speed back. */ /// Another player within this many tiles of the user means no dilation. @@ -373,7 +374,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) var/list/held_shots = list() /// Thrown things in hand. var/list/held_throws = list() - /// The user's own shots: never tinted. + /// The user's own shots: never tinted. A surging mob's held shots aren't tinted either. var/list/user_shots = list() /// Bosses in hand, to the world.time they shrug the field off. var/list/boss_holds = list() @@ -393,7 +394,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) var/list/slowed_tags = list() /// The user's time multiplier. A surging mob runs at its own over this. var/user_multiplier = 1 - /// Surging mobs held at their relative rate, and their shots, to that rate. Never 0, never tinted. + /// Surging mobs held at their relative rate, to that rate. Never 0, never tinted. Their shots fly at the field's pace. var/list/own_scales = list() /datum/proximity_monitor/advanced/timestop/sandevistan/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, list/immune, antimagic_flags, channelled, start_scale = 0, _user_multiplier = 1) @@ -481,13 +482,13 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) hold_mob(A) else if(isprojectile(A)) var/obj/projectile/shot = A - if(shot.firer && immune[shot.firer]) // the user's own: hang_shot takes these in the freeze; in the slow-mo they fly free - return FALSE - if(shot.firer && (surging[shot.firer] || cyberware_own_surge(shot.firer))) // a surging mob's shots fly at its rate - var/rate = surging[shot.firer] ? scale_for(shot.firer) : relative_rate(cyberware_surge_multiplier(shot.firer)) - if(abs(rate - 1) < SANDY_STOP_SAME_RATE) + if(shot.firer && immune[shot.firer]) + if(time_scale <= 0) // hang_shot takes these return FALSE - own_scales[shot] = rate + user_shots[shot] = TRUE + tint = FALSE + escape_the_negative_zone(shot) // in case it was tinted before it had a firer + else if(shot.firer && (surging[shot.firer] || cyberware_own_surge(shot.firer))) // at the field's pace, like every bullet, but in colour tint = FALSE hold_shot(shot) else if(ismecha(A)) @@ -652,7 +653,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) /** * `surger` opened its own surge at `multiplier`: if the field holds it, let it go now and drop every slow it * carried (its walk, its pauses, its last bit of lost time, its slowed animations and the shots it fired), then - * take it back at its relative rate, shots and all. + * take it back at its relative rate. Its shots go back in at the field's pace, untinted. */ /datum/proximity_monitor/advanced/timestop/sandevistan/proc/surge_free(mob/living/surger, multiplier) if(surging[surger]) @@ -764,16 +765,21 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) SIGNAL_HANDLER RegisterSignal(shot, COMSIG_PROJECTILE_FIRE, PROC_REF(hang_shot)) -/// Signal proc for [COMSIG_PROJECTILE_FIRE]: the user's shot keeps its colour. In a frozen world it flies a -/// tile, then hangs until the freeze ends (tick() lets it go at full speed); in the slow-mo it flies at full speed. +/// Signal proc for [COMSIG_PROJECTILE_FIRE]: the user's shot keeps its colour but flies at the field's pace like +/// every bullet in it. In a frozen world it flies a tile, then hangs; in the slow-mo it crawls with the rest. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/hang_shot(obj/projectile/shot) SIGNAL_HANDLER UnregisterSignal(shot, COMSIG_PROJECTILE_FIRE) if(QDELETED(shot) || shot.deletion_queued) return if(time_scale > 0) - if(GLOB.cyberware_time_dilated[shot] == src) - unfreeze_atom(shot) // caught before it had a firer: its speed and colour back + user_shots[shot] = TRUE + escape_the_negative_zone(shot) // in case it was tinted before it had a firer + if(!held_shots[shot]) + hold_shot(shot) + GLOB.cyberware_time_dilated[shot] = src + frozen_things[shot] = TRUE + RegisterSignal(shot, COMSIG_QDELETING, PROC_REF(held_gone), override = TRUE) return // A hitscan beam takes its first step itself once paused (process_hitscan) and stops there. if(!shot.hitscan) @@ -825,8 +831,6 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) else slow_mob(victim) for(var/obj/projectile/shot as anything in held_shots) - if(own_scales[shot]) - continue shot.paused = time_scale <= 0 if(time_scale > 0) shot.speed = held_shots[shot] * time_scale @@ -869,8 +873,6 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) released[boss] = TRUE if(thaw_at && world.time >= thaw_at) thaw_at = 0 - for(var/obj/projectile/shot as anything in user_shots.Copy()) // the user's hung shots fly on at full speed - unfreeze_atom(shot) set_time_scale(slow_scale) /datum/proximity_monitor/advanced/timestop/sandevistan/into_the_negative_zone(atom/A) diff --git a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm index 559000aab03..22c3b15f7fc 100644 --- a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm +++ b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm @@ -358,7 +358,8 @@ TEST_ASSERT_EQUAL(cyberware_dilated_time(raider, 10), 5, "A faster surger's gore doesn't play at double speed") var/datum/movespeed_modifier/pace = raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation) TEST_ASSERT(pace && pace.multiplicative_slowdown <= 0, "A faster surger doesn't walk faster") - TEST_ASSERT(field.held_shots[shot] && !shot.paused && shot.speed == shot_speed * 2, "A faster surger's shot doesn't fly at double speed") + TEST_ASSERT(field.held_shots[shot] && !shot.paused && shot.speed == shot_speed * field.time_scale, "A faster surger's shot doesn't fly at the field's pace") + TEST_ASSERT(!tinted(shot), "A surger's shot was tinted") var/next_move_was = world.time + 5 SECONDS raider.next_move = next_move_was @@ -367,38 +368,42 @@ end_surge(user) end_surge(raider) -/// The user's shots in their own slow-mo fly at full speed; shots hung in the freeze fly on at full speed when it ends -/datum/unit_test/voidcrew_cyberware_timestop/user_shots_free +/// Every bullet in the field flies at the field's pace, whoever fired it: the user's and a surging NPC's alike, untinted +/datum/unit_test/voidcrew_cyberware_timestop/shots_at_field_pace -/datum/unit_test/voidcrew_cyberware_timestop/user_shots_free/Run() +/datum/unit_test/voidcrew_cyberware_timestop/shots_at_field_pace/Run() var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/mob/living/carbon/human/npc/raider = make_surger(spot(3, 0)) var/datum/status_effect/cyberware_cascade_window/window = surge(user) sleep(0.1 SECONDS) TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield - - // One hanging in the freeze, as hang_shot leaves it. - var/obj/projectile/bullet/hung = allocate(/obj/projectile/bullet, spot(1, 0)) - hung.firer = user - var/hung_speed = hung.speed - field.user_shots[hung] = TRUE - field.hold_shot(hung) - GLOB.cyberware_time_dilated[hung] = field - field.frozen_things[hung] = TRUE - TEST_ASSERT(hung.paused, "Test fixture: the hung shot isn't paused") - + TEST_ASSERT_NOTNULL(surge(raider), "The NPC's window didn't open") sleep(1.2 SECONDS) // into the slow-mo - TEST_ASSERT(field.time_scale > 0, "Test fixture: still frozen") - TEST_ASSERT(!hung.paused && hung.speed == hung_speed && !field.held_shots[hung], "A shot hung in the freeze didn't fly on at full speed") - - var/obj/projectile/bullet/shot = allocate(/obj/projectile/bullet, spot(1, 1)) - shot.firer = user - var/shot_speed = shot.speed - TEST_ASSERT(!field.freeze_atom(shot), "The field took the user's own slow-mo shot") - field.hang_shot(shot) - TEST_ASSERT(!field.held_shots[shot] && !shot.paused && shot.speed == shot_speed, "The user's slow-mo shot was slowed") - TEST_ASSERT(!tinted(shot), "The user's slow-mo shot was tinted") + TEST_ASSERT(field.time_scale > 0 && field.time_scale < 1, "Test fixture: not in the slow-mo") + + var/obj/projectile/bullet/user_shot = allocate(/obj/projectile/bullet, spot(1, 0)) + user_shot.firer = user + var/user_speed = user_shot.speed + field.hang_shot(user_shot) + TEST_ASSERT(field.held_shots[user_shot] && !user_shot.paused, "The user's slow-mo shot wasn't held") + TEST_ASSERT_EQUAL(user_shot.speed, user_speed * field.time_scale, "The user's slow-mo shot doesn't crawl at the field's pace") + TEST_ASSERT(!tinted(user_shot), "The user's slow-mo shot was tinted") + + var/obj/projectile/bullet/npc_shot = allocate(/obj/projectile/bullet, spot(2, 0)) + npc_shot.firer = raider + var/npc_speed = npc_shot.speed + field.freeze_atom(npc_shot) + TEST_ASSERT(field.held_shots[npc_shot] && !npc_shot.paused, "A surging NPC's shot wasn't held") + TEST_ASSERT_EQUAL(npc_shot.speed, npc_speed * field.time_scale, "A surging NPC's shot doesn't fly at the field's pace") + TEST_ASSERT(!tinted(npc_shot), "A surging NPC's shot was tinted") + + sleep(3 SECONDS) // past the end + TEST_ASSERT_NULL(window.time_stop, "The dilation outlived its 4 s") + TEST_ASSERT_EQUAL(user_shot.speed, user_speed, "The user's shot didn't get its speed back") + TEST_ASSERT_EQUAL(npc_shot.speed, npc_speed, "The NPC's shot didn't get its speed back") end_surge(user) + end_surge(raider) /// A surging NPC that dies is let go for good: its corpse isn't taken back and tinted /datum/unit_test/voidcrew_cyberware_timestop/no_corpse_retake From c0572b605bffa755a7c87be87142508896159067 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 00:10:45 -0800 Subject: [PATCH 14/20] Slow the weather and the world's sounds for the Sandevistan user during dilation --- code/game/sound/sound.dm | 8 + tgstation.dme | 1 + .../modules/cyberware/icons/sandy_weather.dmi | Bin 0 -> 85117 bytes .../modules/cyberware/ware_sandy_senses.dm | 317 ++++++++++++++++++ .../modules/cyberware/ware_sandy_timestop.dm | 7 + .../unit_tests/voidcrew_cyberware_timestop.dm | 91 +++++ 6 files changed, 424 insertions(+) create mode 100644 voidcrew/modules/cyberware/icons/sandy_weather.dmi create mode 100644 voidcrew/modules/cyberware/ware_sandy_senses.dm diff --git a/code/game/sound/sound.dm b/code/game/sound/sound.dm index 60452addb21..3e632619c9f 100644 --- a/code/game/sound/sound.dm +++ b/code/game/sound/sound.dm @@ -200,6 +200,14 @@ if(HAS_TRAIT(src, TRAIT_SOUND_DEBUGGED)) to_chat(src, span_admin("Max Range-[max_distance] Distance-[distance] Vol-[round(sound_to_use.volume, 0.01)] Sound-[sound_to_use.file]")) + // VOIDCREW EDIT CHANGE START - cyberware: a Sandevistan user hears the slowed world slowed (voidcrew/modules/cyberware/ware_sandy_senses.dm) + if(length(GLOB.cyberware_time_fields)) + var/sandy_frequency = sound_to_use.frequency + cyberware_dilate_heard(src, turf_source, sound_to_use, usr) + SEND_SOUND(src, sound_to_use) + sound_to_use.frequency = sandy_frequency // the datum can be shared with the next listener + return + // VOIDCREW EDIT CHANGE END SEND_SOUND(src, sound_to_use) /proc/sound_to_playing_players(soundin, volume = 100, vary = FALSE, frequency = 0, channel = 0, pressure_affected = FALSE, sound/S) diff --git a/tgstation.dme b/tgstation.dme index 2bf19a31c90..e628c7fdc90 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7225,6 +7225,7 @@ #include "voidcrew\modules\cyberware\ware_pro_combat.dm" #include "voidcrew\modules\cyberware\ware_pro_utility.dm" #include "voidcrew\modules\cyberware\ware_registry.dm" +#include "voidcrew\modules\cyberware\ware_sandy_senses.dm" #include "voidcrew\modules\cyberware\ware_sandy_timestop.dm" #include "voidcrew\modules\cyberware\ware_second_wind.dm" #include "voidcrew\modules\cyberware\ware_street.dm" diff --git a/voidcrew/modules/cyberware/icons/sandy_weather.dmi b/voidcrew/modules/cyberware/icons/sandy_weather.dmi new file mode 100644 index 0000000000000000000000000000000000000000..0b424049e700ba56c308e62fe91e8a61822b9c6e GIT binary patch literal 85117 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%0DTHsbE~b$9NT6MC<(5(Yvp|^i-7&>9=`2Ucx1o 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 I7@vszU&Sj4@Bjb+ literal 0 HcmV?d00001 diff --git a/voidcrew/modules/cyberware/ware_sandy_senses.dm b/voidcrew/modules/cyberware/ware_sandy_senses.dm new file mode 100644 index 00000000000..f3875b85562 --- /dev/null +++ b/voidcrew/modules/cyberware/ware_sandy_senses.dm @@ -0,0 +1,317 @@ +/** + * # What the Sandevistan user sees and hears of the slowed world + * + * Only the user's own client changes; nobody else's view or speakers do. + * + * Sound: a sound from a tile inside the field reaches the user pitched down and slowed by the field's scale + * (the `frequency` of the sound datum, set in playsound_local). The user's own sounds stay normal: anything + * from their own tile, and anything their own action set off (`usr` is the user: their gun, their swings, + * their callbacks). During the freeze the world is a low, quiet rumble. The weather and ship-hum loops already + * playing on the user's channels are slowed in place with SOUND_UPDATE, and the loop's next segments go out + * slowed too. + * + * Weather: area overlays are shared by everyone on a planet, so they are left alone. Each weathered area in + * the user's view gets one override image, shown to the user's client only: a copy of the area's appearance + * with every weather overlay swapped for a pre-baked slowed (or, in the freeze, still) copy from + * sandy_weather.dmi. There is no particle weather to slow. + */ + +#define SANDY_WEATHER_ICON 'voidcrew/modules/cyberware/icons/sandy_weather.dmi' +/// How fast world sounds play for the user while the world is frozen. +#define SANDY_FREEZE_SOUND_SPEED 0.15 +/// How loud world sounds are for the user while the world is frozen, as a share of normal. +#define SANDY_FREEZE_SOUND_VOLUME 0.25 +/// Tiles past the edge of the user's view that still get slowed weather, so a step never shows a fast edge. +#define SANDY_WEATHER_VIEW_MARGIN 1 + +/// The field `listener` is the user of, if any. +/proc/cyberware_field_of_user(mob/listener) + for(var/datum/proximity_monitor/advanced/timestop/sandevistan/field as anything in GLOB.cyberware_time_fields) + if(field.host == listener) + return field + +/** + * How fast `listener` hears a sound from `source`: 1 normally, the field's pace for a sound from inside the + * user's field, SANDY_FREEZE_SOUND_SPEED in the freeze. `actor` is whoever set the sound off (`usr`). + */ +/proc/cyberware_heard_speed(mob/listener, turf/source, mob/actor) + if(!isturf(source) || actor == listener) + return 1 + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = cyberware_field_of_user(listener) + if(!field) + return 1 + var/turf/here = get_turf(listener) + if(!here || source == here || source.z != here.z || get_dist(source, here) > field.current_range) + return 1 + return field.time_scale > 0 ? field.time_scale : SANDY_FREEZE_SOUND_SPEED + +/// Slows `heard` for `listener` if it comes from the slowed world. Called from playsound_local. +/proc/cyberware_dilate_heard(mob/listener, turf/source, sound/heard, mob/actor) + var/speed = cyberware_heard_speed(listener, source, actor) + if(speed == 1) + return + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = cyberware_field_of_user(listener) + field?.senses?.note_one_shot(heard) + heard.frequency = cyberware_scaled_frequency(heard.frequency, speed) + if(speed == SANDY_FREEZE_SOUND_SPEED) + heard.volume *= SANDY_FREEZE_SOUND_VOLUME + +/// `frequency` (0 for as-is, a multiplier within -100..100, or a rate in Hz) played `speed` times as fast. +/proc/cyberware_scaled_frequency(frequency, speed) + if(!frequency) + frequency = 1 + return frequency * speed + +// ----- looping sounds sent straight to a mob (weather) ----- + +/datum/looping_sound + /// The last file this loop sent straight to its mob, to slow it in place. + var/sandy_last_file + /// The volume it went at. + var/sandy_last_volume + /// The channel it went on (ash storm loops take a random one). + var/sandy_last_channel + +/// The direct branch of the base play(), done here so the channel a loop took is known and the sound can +/// go out slowed to a dilation user. +/datum/looping_sound/play(soundfile, volume_override) + if(!direct) + return ..() + var/sound/sound_to_play = sound(soundfile) + sound_to_play.channel = sound_channel || SSsounds.random_available_channel() + sound_to_play.volume = volume_override || volume //Use volume as fallback if theres no override + sandy_last_file = soundfile + sandy_last_volume = sound_to_play.volume + sandy_last_channel = sound_to_play.channel + if(length(GLOB.cyberware_time_fields) && ismob(parent)) + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = cyberware_field_of_user(parent) + if(field?.senses) + sound_to_play.volume *= field.senses.loop_volume + sound_to_play.frequency = field.senses.loop_speed + SEND_SOUND(parent, sound_to_play) + +// ========================================================================= +// THE USER'S SENSES +// ========================================================================= + +/// Lives with a dilation field and keeps its user's weather and sound loops in step with the field's pace. +/datum/sandy_senses + /// The field whose pace this follows. + var/datum/proximity_monitor/advanced/timestop/sandevistan/field + /// The user. + var/mob/living/user + /// The client the images are shown to. + var/client/viewer + /// Weathered areas in view, to the override image shown for each. + var/list/area_images = list() + /// Areas, to what their image was built from, to rebuild it only when that changes. + var/list/area_keys = list() + /// How fast the user's loops play now. + var/loop_speed = 1 + /// How loud the user's loops are now, as a share of normal. + var/loop_volume = 1 + /// Channels of world sounds sent slowed and maybe still playing, to an unslowed copy of each, to retune them. + var/list/one_shots = list() + /// The same channels, to when each went out. + var/list/one_shot_times = list() + +/datum/sandy_senses/New(datum/proximity_monitor/advanced/timestop/sandevistan/field) + src.field = field + user = field.host + RegisterSignal(user, COMSIG_MOB_LOGOUT, PROC_REF(on_logout)) + update() + +/datum/sandy_senses/Destroy() + if(user) + UnregisterSignal(user, COMSIG_MOB_LOGOUT) + set_loops(1, 1) + clear_images() + field = null + user = null + return ..() + +/// The field changed pace, or ticked: bring the loops and the weather in line. +/datum/sandy_senses/proc/update() + if(field.time_scale > 0) + set_loops(field.time_scale, 1) + else + set_loops(SANDY_FREEZE_SOUND_SPEED, SANDY_FREEZE_SOUND_VOLUME) + refresh_weather() + +/// Signal proc for [COMSIG_MOB_LOGOUT]: the client keeps its images when it leaves, so take them back. +/datum/sandy_senses/proc/on_logout(datum/source) + SIGNAL_HANDLER + clear_images() + +// ----- sound loops ----- + +/// Plays the user's weather and ship-hum loops at `speed` and `volume` times normal, in place. +/datum/sandy_senses/proc/set_loops(speed, volume) + if(loop_speed == speed && loop_volume == volume) + return + loop_speed = speed + loop_volume = volume + if(!user.client) + return + retune_one_shots(speed, volume) + for(var/datum/component/area_sound_manager/manager as anything in user.GetComponents(/datum/component/area_sound_manager)) + var/datum/looping_sound/loop = manager.our_loop + if(!loop?.direct || !loop.sandy_last_channel || !loop.sandy_last_file) + continue + update_channel(loop.sandy_last_file, loop.sandy_last_channel, loop.sandy_last_volume * volume, speed) + var/buzz = user.client.current_ambient_sound + var/area/here = get_area(user) + if(buzz && here?.ambient_buzz == buzz) + var/buzz_pref = user.client.prefs.read_preference(/datum/preference/numeric/volume/sound_ship_ambience_volume) + update_channel(buzz, CHANNEL_BUZZ, here.ambient_buzz_vol * (buzz_pref / 100) * volume, speed, TRUE) + +/// Notes a world sound about to go out slowed (`heard` still unslowed), to speed it back up later. +/datum/sandy_senses/proc/note_one_shot(sound/heard) + if(!heard.channel || heard.repeat) + return + var/sound/copy = sound(heard.file, 0, 0, heard.channel, heard.volume) + copy.frequency = heard.frequency + copy.x = heard.x + copy.y = heard.y + copy.z = heard.z + copy.falloff = heard.falloff + copy.environment = heard.environment + copy.echo = heard.echo + one_shots["[heard.channel]"] = copy + one_shot_times["[heard.channel]"] = world.time + +/// World sounds still playing from earlier in the effect take the new pace: full speed when it ends. +/datum/sandy_senses/proc/retune_one_shots(speed, volume) + for(var/channel in one_shots.Copy()) + if(world.time - one_shot_times[channel] > 15 SECONDS) // long finished + one_shots -= channel + one_shot_times -= channel + continue + var/sound/copy = one_shots[channel] + var/sound/update = sound(copy.file, 0, 0, copy.channel, copy.volume * volume) + update.frequency = cyberware_scaled_frequency(copy.frequency, speed) // never 0: an update with 0 keeps the old pace + update.x = copy.x + update.y = copy.y + update.z = copy.z + update.falloff = copy.falloff + update.environment = copy.environment + update.echo = copy.echo + update.status = SOUND_UPDATE + SEND_SOUND(user, update) + if(speed == 1) + one_shots.Cut() + one_shot_times.Cut() + +/// Changes the pace and volume of what is playing on `channel` for the user, without restarting it. +/datum/sandy_senses/proc/update_channel(file, channel, volume, speed, repeat = FALSE) + var/sound/update = sound(file, repeat, 0, channel, volume) + update.frequency = speed + update.status = SOUND_UPDATE + SEND_SOUND(user, update) + +// ----- weather ----- + +/// Shows slowed weather over every weathered area in the user's view, and drops images for the rest. +/datum/sandy_senses/proc/refresh_weather(force = FALSE) + var/client/now_viewer = user.client + if(viewer && viewer != now_viewer) + clear_images() + viewer = now_viewer + if(!viewer && !force) + return + var/tag = cyberware_weather_tag(field.time_scale) + var/list/wanted = list() + var/turf/centre = get_turf(user) + if(centre) + var/list/weathers = list() + for(var/datum/weather/weather as anything in SSweather.processing) + if(weather.stage != END_STAGE && length(weather.overlay_cache) && (centre.z in weather.impacted_z_levels)) + weathers += weather + if(length(weathers)) + var/list/size = getviewsize(viewer?.view || world.view) + var/reach_x = round(size[1] / 2) + SANDY_WEATHER_VIEW_MARGIN + var/reach_y = round(size[2] / 2) + SANDY_WEATHER_VIEW_MARGIN + var/list/seen = list() + for(var/turf/spot as anything in block(max(centre.x - reach_x, 1), max(centre.y - reach_y, 1), centre.z, min(centre.x + reach_x, world.maxx), min(centre.y + reach_y, world.maxy), centre.z)) + seen[spot.loc] = TRUE + for(var/area/place as anything in seen) + var/list/caches = list() + for(var/datum/weather/weather as anything in weathers) + if(weather.impacted_areas_lookup[place]) + caches += list(weather.overlay_cache) + if(length(caches)) + wanted[place] = caches + for(var/area/place as anything in area_images.Copy()) + if(!wanted[place]) + drop_image(place) + for(var/area/place as anything in wanted) + var/list/caches = wanted[place] + var/key = "[tag]|[length(place.overlays)]" + for(var/list/cache as anything in caches) + key += "|[REF(cache)]" + if(area_keys[place] == key) + continue + drop_image(place) + var/image/slowed = cyberware_slowed_weather_image(place, tag) + area_images[place] = slowed + area_keys[place] = key + viewer?.images += slowed + +/// Takes `place`'s image off the user's screen. +/datum/sandy_senses/proc/drop_image(area/place) + var/image/slowed = area_images[place] + if(slowed) + viewer?.images -= slowed + area_images -= place + area_keys -= place + +/// Takes every image off the user's screen: the real weather shows again. +/datum/sandy_senses/proc/clear_images() + if(viewer) + for(var/area/place as anything in area_images) + viewer.images -= area_images[place] + area_images.Cut() + area_keys.Cut() + viewer = null + +/// The sandy_weather.dmi suffix for a field running at `scale`: a still frame in the freeze, else the nearest baked pace. +/proc/cyberware_weather_tag(scale) + if(scale <= 0) + return "still" + if(scale <= 0.27) + return "s20" + if(scale <= 0.42) + return "s35" + return "s50" + +/// An override image of `place` as it looks now, with its weather overlays swapped for slowed (`tag`) copies. +/proc/cyberware_slowed_weather_image(area/place, tag) + var/static/list/baked + if(!baked) + baked = list() + for(var/state in icon_states(SANDY_WEATHER_ICON)) + baked[state] = TRUE + var/image/slowed = image(loc = place) + slowed.appearance = place.appearance + slowed.override = TRUE + var/list/swapped = list() + for(var/overlay in slowed.overlays) + var/state + if(overlay:icon == 'icons/effects/weather_effects.dmi') + state = "[overlay:icon_state]_[tag]" + else if(overlay:icon == 'icons/effects/glow_weather.dmi') + state = "glow_[overlay:icon_state]_[tag]" + if(!state || !baked[state]) + swapped += overlay + continue + var/mutable_appearance/copy = new(overlay) + copy.icon = SANDY_WEATHER_ICON + copy.icon_state = state + swapped += copy + slowed.overlays = swapped + return slowed + +#undef SANDY_WEATHER_ICON +#undef SANDY_FREEZE_SOUND_SPEED +#undef SANDY_FREEZE_SOUND_VOLUME +#undef SANDY_WEATHER_VIEW_MARGIN diff --git a/voidcrew/modules/cyberware/ware_sandy_timestop.dm b/voidcrew/modules/cyberware/ware_sandy_timestop.dm index 6d7c10d1040..f35ad6fc60d 100644 --- a/voidcrew/modules/cyberware/ware_sandy_timestop.dm +++ b/voidcrew/modules/cyberware/ware_sandy_timestop.dm @@ -345,6 +345,8 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) var/list/surging = list() /// Held mobs, to the tags of their slowed hv_vis_play() animations, to drop if they break free. var/list/slowed_tags = list() + /// The user's slowed weather and sound (ware_sandy_senses.dm). + var/datum/sandy_senses/senses /datum/proximity_monitor/advanced/timestop/sandevistan/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, list/immune, antimagic_flags, channelled, start_scale = 0) time_scale = start_scale @@ -354,8 +356,11 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) STOP_PROCESSING(SSfastprocess, src) // tick() runs on tick_timer instead tick_timer = addtimer(CALLBACK(src, PROC_REF(tick)), 0.2 SECONDS, TIMER_LOOP | TIMER_STOPPABLE | TIMER_DELETE_ME) RegisterSignal(host, COMSIG_PROJECTILE_FIRER_BEFORE_FIRE, PROC_REF(on_user_fire)) + if(ismob(host)) + senses = new(src) /datum/proximity_monitor/advanced/timestop/sandevistan/Destroy() + QDEL_NULL(senses) deltimer(tick_timer) tick_timer = null if(host) @@ -731,6 +736,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) stop_mech(mech) else start_mech(mech) + senses?.update() /datum/proximity_monitor/advanced/timestop/sandevistan/process() return PROCESS_KILL @@ -761,6 +767,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) if(thaw_at && world.time >= thaw_at) thaw_at = 0 set_time_scale(slow_scale) + senses?.refresh_weather() /datum/proximity_monitor/advanced/timestop/sandevistan/into_the_negative_zone(atom/A) A.add_atom_colour(cyberware_time_stop_tint(), TEMPORARY_COLOUR_PRIORITY) diff --git a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm index 16c7b40edf5..c0a86c93561 100644 --- a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm +++ b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm @@ -281,3 +281,94 @@ user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) raider.remove_status_effect(/datum/status_effect/cyberware_cascade_window) raider.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) + +/// The user hears the slowed world slowed (ware_sandy_senses.dm): world sounds in the field take its pace and +/// a quiet low rumble in the freeze; their own sounds, sounds from outside the field and other listeners don't. +/datum/unit_test/voidcrew_cyberware_timestop/heard_slowed + +/datum/unit_test/voidcrew_cyberware_timestop/heard_slowed/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/mob/living/carbon/human/npc/raider = allocate(/mob/living/carbon/human/npc, spot(3, 0)) + var/turf/far = locate(user.x + 40, user.y, user.z) || locate(1, user.y, user.z) + TEST_ASSERT_EQUAL(cyberware_heard_speed(user, get_turf(raider), raider), 1, "A sound was slowed with no dilation running") + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + sleep(0.1 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield + TEST_ASSERT_NOTNULL(field.senses, "The field has no senses for its user") + + // The freeze: a quiet, low rumble. + var/frozen_speed = cyberware_heard_speed(user, get_turf(raider), raider) + TEST_ASSERT(frozen_speed > 0 && frozen_speed < 0.5, "A world sound in the freeze played at [frozen_speed]") + var/sound/shot = sound('sound/items/weapons/gun/pistol/shot.ogg') + shot.volume = 100 + cyberware_dilate_heard(user, get_turf(raider), shot, raider) + TEST_ASSERT(shot.frequency == frozen_speed, "A world sound in the freeze went out at frequency [shot.frequency]") + TEST_ASSERT(shot.volume < 50, "A world sound in the freeze stayed loud ([shot.volume])") + TEST_ASSERT_EQUAL(field.senses.loop_speed, frozen_speed, "The user's loops didn't slow in the freeze") + + // The user's own sounds, other listeners and far sounds stay as they are. + TEST_ASSERT_EQUAL(cyberware_heard_speed(user, get_turf(user), raider), 1, "A sound on the user's own tile was slowed") + TEST_ASSERT_EQUAL(cyberware_heard_speed(user, get_turf(raider), user), 1, "A sound the user set off was slowed") + TEST_ASSERT_EQUAL(cyberware_heard_speed(raider, get_turf(raider), null), 1, "Someone other than the user heard it slowed") + if(far && get_dist(far, user) > field.current_range) + TEST_ASSERT_EQUAL(cyberware_heard_speed(user, far, null), 1, "A sound from outside the field was slowed") + TEST_ASSERT_EQUAL(cyberware_scaled_frequency(44100, 0.5), 22050, "A rate in Hz didn't scale") + TEST_ASSERT_EQUAL(cyberware_scaled_frequency(0, 0.2), 0.2, "An as-is sound didn't scale") + + sleep(1.2 SECONDS) // into the 20% slow-mo + TEST_ASSERT_EQUAL(cyberware_heard_speed(user, get_turf(raider), raider), field.time_scale, "A world sound didn't take the slow-mo pace") + TEST_ASSERT_EQUAL(field.senses.loop_speed, field.time_scale, "The user's loops didn't take the slow-mo pace") + + user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) + user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) + TEST_ASSERT_NULL(window.time_stop, "Test fixture: the dilation outlived its window") + TEST_ASSERT_EQUAL(cyberware_heard_speed(user, get_turf(raider), raider), 1, "Sounds stayed slowed after the dilation") + +/// The user sees the weather slowed through a private override image per area; the area itself never changes, +/// and the images go when the dilation ends. +/datum/unit_test/voidcrew_cyberware_timestop/weather_slowed + +/datum/unit_test/voidcrew_cyberware_timestop/weather_slowed/Run() + var/mob/living/carbon/human/user = make_user(spot(1, 1), "sandytimestopuser") + var/area/place = get_area(user) + var/datum/weather/rain_storm/rain = new(list(user.z), list("areas" = list(place), "weather_flags" = WEATHER_INDOORS)) + TEST_ASSERT(rain.impacted_areas_lookup[place], "Test fixture: the rain didn't take the test room") + rain.stage = MAIN_STAGE + rain.update_areas() + SSweather.processing |= rain + var/list/area_overlays = place.overlays.Copy() + + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + sleep(0.1 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield + field.senses.refresh_weather(force = TRUE) // no client in a unit test + var/image/frozen = field.senses.area_images[place] + TEST_ASSERT_NOTNULL(frozen, "The rained-on area got no image") + TEST_ASSERT(frozen.override, "The weather image doesn't replace the area's look") + TEST_ASSERT(weather_states(frozen).Find("rain_high_still"), "The freeze didn't show still rain: [jointext(weather_states(frozen), ",")]") + TEST_ASSERT(!weather_states(frozen).Find("rain_high"), "The fast rain is still in the freeze image") + TEST_ASSERT_EQUAL(length(place.overlays), length(area_overlays), "The area's own overlays changed") + + sleep(1.2 SECONDS) // into the 20% slow-mo + field.senses.refresh_weather(force = TRUE) + var/image/slowed = field.senses.area_images[place] + TEST_ASSERT(weather_states(slowed).Find("rain_high_s20"), "The slow-mo didn't show slowed rain: [jointext(weather_states(slowed), ",")]") + + user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) + user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) + TEST_ASSERT_NULL(window.time_stop, "Test fixture: the dilation outlived its window") + TEST_ASSERT_NULL(field.senses, "The senses outlived the dilation") + TEST_ASSERT_EQUAL(length(place.overlays), length(area_overlays), "The area's own overlays changed after the dilation") + + SSweather.processing -= rain + rain.stage = END_STAGE + rain.update_areas() + qdel(rain) + +/// The icon states of `slowed`'s overlays. +/datum/unit_test/voidcrew_cyberware_timestop/weather_slowed/proc/weather_states(image/slowed) + . = list() + for(var/overlay in slowed.overlays) + . += overlay:icon_state From 1b2b68b6ec09db26a2bf2954656e9548a81fa03b Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 01:47:55 -0800 Subject: [PATCH 15/20] Run the Sandevistan slow-mo for the whole surge with no freeze, and let a second press end it The lone surge now slows the world from the moment it opens to the end of the window (street 6 s at 50%, service 7 s at 35%, Cascade 8 s at 20%). The freeze is gone on every model: no frozen-AI traits, paused or hung shots, still weather frames, quiet freeze sound, mech shutdown, turret lock or animation hold. Pressing the surge again ends a player's window early and starts the crash; NPC lattices never turn themselves off. --- .../modules/cyberware/icons/sandy_weather.dmi | Bin 85117 -> 77562 bytes voidcrew/modules/cyberware/ware_legend.dm | 30 +- voidcrew/modules/cyberware/ware_line_sandy.dm | 4 - .../modules/cyberware/ware_sandy_senses.dm | 50 +-- .../modules/cyberware/ware_sandy_timestop.dm | 316 +++++------------- voidcrew/modules/hyperviolence/_core.dm | 14 +- .../hyperviolence/visuals/_motion_core.dm | 35 +- .../hyperviolence/visuals/blood_motion.dm | 4 +- .../hyperviolence/visuals/death_falls.dm | 2 +- .../hyperviolence/visuals/execution_motion.dm | 8 +- .../hyperviolence/visuals/flying_gore.dm | 14 +- .../unit_tests/voidcrew_cyberware_timestop.dm | 284 ++++++++++------ 12 files changed, 334 insertions(+), 427 deletions(-) diff --git a/voidcrew/modules/cyberware/icons/sandy_weather.dmi b/voidcrew/modules/cyberware/icons/sandy_weather.dmi index 0b424049e700ba56c308e62fe91e8a61822b9c6e..4d3f4fae89a385669442378cbae905aaf2f179cd 100644 GIT binary patch literal 77562 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

(`N)j-AHYEsW> zrf_NQ&c7%_2GyQcbIRVv}X9|Kc|FeZP?A@0e_RjUILq0+;5Y0^}Xm8_Y~ny z-xjF(n`p1*S>1Gesohpl=n?L4a;w;o3S!;v zXaqS z+xQ(c4sF#9t;7AF(<^`zj^FXmw*Xpiq-^U@t@Yb7a}L3iwu(YY&f z`D~!`w`expba8NPq@4bvw9XjWx~wL$40}xo7G@forQ|K-JnSc&2=v4=(I+PO76V_g zQh-LOXH^APSTCv8EnlhU#?D8)I-l)cUwEB%!2&S66tK%X z$__Kzg`dv#zV4qNyYa$DVj_uO@BJD$w`A55i!%f? z4^u}0)9C$kK<}!|`@q}8+i`Vaex27dKT66h+bTVdM$3o`tkw9>5D#bbOn)`iDlFP_ zE3YptwE6TRT)lk#{WxZ!Og>>r^8U3)%~JgmKkL=RN1A~{D4Ev&(OHlLznD0p(QhoY zVpgg0Fw5AEASC31q@*`ClO6gNsh&wSp)YYiYJ-rrthMa-{pH|s; zfA=_1mNYvsC0n9CRS8jBAQC$+me(Nt;8jOqr#{N}I1hWBUi$uGC(5|SBSd-^ghRJzdh*Au2t8KI_TN&BalaDSO(LU z1{hO@uY%b_nyHLwSGx@D6MM6Tda~!)eoRgi)|9HKYBPm3E(B+bPX;z^&(Ju0r~+w` z)5LV)v66CeGhG?YJNu^p`~=d!DQe=3nqLJcTZNlV50|BW3nVKa5vZ!Q-+O0Uii6yj z37OYF$+(QMlB&+;*1T*dA@R(R_>P9wyfqp2t}M1?{y<`U+L80-Il?o7E!58GTv;M;#57DSnIx*NstpB_;J4QI9hH)juUanc?Xg~^A%K9>&<@)lK zl%u5r(gl(2vex06z9D9S`6Wj&tgl1G z_SArLECa($^OK+9>qWYL8cT8*U@REE$e;Er4u&=J{7g^uil%0Tqu@4i3(k7#*if#Eaq_hE*iC?jp3iA*%1pgKz2<*buCB5aSgT=@(7BTRzvWY zP=sB$ig7bm09(mKGmouL@MTy(-iBf2A}SeY_lI^P4;CbNB*8nWc`@f$*307f z82;?XT6Oc*5k%DfS6tQO6Fp(agPgg8;3Bq&Q2t*Mre#01m+rG?Elt4qiYHtSurWS% zfU-eKR*}|Xf87+~q>RDzo~B0#lXZTxpoK4HL7=lU?}v|=Ay;2ijU`EW{pwj7rVYpr zvw*)a@G?$Z@NUO5e!cKmko>UPk8wyOd6x%oMlaR`X=djn01HW=3q@B%4Skwv7 zzBE(MtTfPi?l(EAr6dtyzi-EFHt4W4w2_Y<#NhMXJ^FJY@`cgE>;!cRwjtNjXsI8G zm9=>itbWv&qjY=x{)trESHjFs5384PIkPoP(D4srg8dTvRcvB3ZT5^m7CWcIM)Isu zbs8LMv7)2pbe7b`2V&z>ky_j5&u^<7OW3UnSB=J^Tf+_6d|7na5B`^< z_tpPCnr{`OA3s;sixp^{SY35-mv)VvfGymo=4&W* z=E}StW7j%8!`Z7HWi72ct3!{d2Op7U?c086^dYDrqKccEz705Aq;&SSQTl+L^%TLj z{$J+fH>97NezXi)O_9BYK7BlB{NQ@l9;>^A7>SVl=nA1v6~tARo0itu=3Ip)sOai| z>8qzqT3|qx4VW`bK(ZEeTmq!6!SI)hG_5m9&R(8rPOR`~KfeCu&hikumP1LZrrukD z;olV>IsKm3;2OnaH>CS)tiFcNE4W)0YfRRVQN9M^rz=uS5=I4PM z2TV@&Ac5=$S_E#xX3WwxWBlKTp1bN-1+Y3paMqq%pQ%8|9O5;9U7Vnl-(SvCdze+C z9UHt*G5TOs?f`amfkU9>)!9B-og_CciHd4Yk6hf5VB%_I2!$?xYl)n5rvEn!oYS*}-dmZh{^ zfHXOC8=2=oHznEUI!Lh~MF2@yDA$Z3j%N%V zi>9r#^Eg;M?{>W)QeSsq-9>AkM;-sh!pSnPPG@r9j`cKtKb|&FG1A3*6-D> zSVF&hxZK=fWcHxvwpMw{iT4sW9S?pz0F01~zKBtKJtLBIbiw%Rvr zWHb=p%SP9`B6EVquejlK6DlroU*5Y;NGhX#>Y;wLA~V*VMY%W59Du3cZ$=wZ!?}RN z_x2^#hdVbWe6+5%H{MUTxDeR&K7BqgU=29D64PbCF6n(7VKhTP35`G zIh*JgZc$Px$` z*j@J}Mz0jugAJ(SS)IqjzX^XnsQoXNBg}~PKng4w72Qavugo79-u#eVQslQ^-ElwJ zE=+v*a3@U7uBb8Nk0(VGC14ICM+IJo8)%h2z38Ip1K(y0rA7e2V~0p>1p z2cGAM9Zedma{c9f?m%a4W{1|&9h1Y7ob85KDqbdI66)GT6t{W`lN4C!80gaAFKHy= zxDb(%Bukg=K?MhWx$6A|$PUpGPdwMtcX*xOfby`>r=S&wj~73zefcpvxE?Mn&5V~l zh<+qbdB+j7#)+8)XC$pTSY(77Dw6F+E;y#4K-{)Gw1P*)Jun zj$JXpFFj#?{Fe1i>$4O{_W}8>c|gD(g1ffebCZ%rjm$WwJ_{GwXEK>P)lDTa;xTQ= z@dg>VbCv?(G7fQgR3j)z?8DmI`43<|u{&|(Btdm$71XchuaA8&d1Zhw%ua1SBwi@C z?zq<@D`Y&ziypL$E30f=Qilm^RxhK7s zDk?1k(kk83FtkY5p>qf+si7HQ82H!TaN_;GbKd{|uXA1OYsNFP*Lw1P?&q1c182{Q zE4@CP`Ee+WFRhk1)a-N$eUQncUl-|IcewApv0p3RzTuk?>lfVdig3mL3$@fn3Xdx9 zz!1?`OmMWx!)fQY5_<7-AK(AZ!flxE%xd!ryXFvkKlLFCmcG7UH?>NY*!(KJNAPQ0 zN9^@l*Y|N9pGR86%0*KPpW$u@rWW?NhxbO!*9#`gj5hL``5D?On3vnr$S<20Tr5$o z*sZkf4~xx%S<9*?X;lt0OkN+P+|?Yw(&zoR4X6*OD=$)pCOySg8)Rs21z_KfMV%|-b)Zf4^eF3sj%Q|H;-SKnG} zjvRZW+F`SG#O!NO4jmVx-X9s^tU-eeKjK6XpcJwZpUge~uvH8kei7AoXIk53pdb~` zKb0l>NI0eb{4+J(l=_^}vR=AZu98y4c z;TQ&hP(8^R;D;!;zA_$cnx*RdPQ|)aio9^VOzfJEv@SV0in;3f2c`tgFSJs|T*ORv zTV6C?oFt0-=`5wD_+oeJLpj-2P08rRDMQ=epH=Y@cj>q&ZKL}InlHQxh~+VMf&&_c z@^)%dgxE1cdwU0mWazNvtl&*8A_}wr9PWiWBsXgg1>#Y3s#e{3yS6HDU>X&cd`pv( zU$WI{`rEBxVeu!}Iz`E!TfzC{QEB~R6)Hi*9q%vf+mOcZykbG)mI$hqUcG0tVw zc2bi28sj1A&0K{sEtJx2$X7@7w#^^gKY&s>TyIiCDgoutQIwW207$!od4JWVH_rL% zg|&pM0Tx`1s9QRl$s4x2^5l88`9(IhnX3~P7c~mry=#J|8VEcNqRr8N%{8=knLW@~ zWW2`DI+LcnI?h0Q3{Ll52T-P44-&x*p(+Zt2vGu}0tu6fQu=CQ)HkO6g0pi+L;Z>8YA@|f z5yXlU9P(0yO47I9zv6i90;c#XwG0y^`f~&azr-u$k8(A*xkQW?DiGasvm>%Q7gY5( z%GrQ_SC8cbTFNwIm~yG@p^hUbeeeP&A<0-|uxX1D6U|}qdGb$T$?vlI4qra^vOeEW zEQ?3RE-iVI9;*@a{FF_@t3TeVWz}{(0FwI=h}_!4r`7V!O~4;HDJJH-{Da7Hz}|{| zFAn*3VE_368q7GAi}Q67ogrs*bD8Pt9Fm;g_^B!Bz#xtv?$?*`+Nn}>}bY%4B%c9eW`t*O?=*Y?L76hB?w$bcKoGu*6%(Rd+3>WUvLeJOx+&Mev|Lj9!_I|^H(Pc_Q3&)HW$P``HE`n z^j#QrZzb@s_~4Gvm{4Qw3YhtDvh3_9%b#n>VA!d%4?RhgAkd+AaUz94q!f>gQ~Grt zUd1viiS3ri;i@@t#;O>V7&f0}Ear`~$wYRIu=Wle(U=PU$SEMH~dA1n=4Y)1yPCD-t#1EdrSpZMfR3)39e`z;Yu#R$$Vldot9YD zQtMb(G@DR4t&m--e#%RL49r&^o+NiCAfQ3(zV=LvAtCE0r@PRuza;^~www3}BCc$o z?>+eIahGG9bZ=5ukD&cEggJT5X>d-uvtidb+P#?Hdpfx}bzf7=j%_W}GwycTRft)& zx`c?+Ejj&aESPgk3ni^|N6U3h?pLx{vwK|c2KPBnC)th>&!A%;iolhC*4Phc9kOw^ zhrM^-o%hN^pO2 ziNT*(%d>B0g{`JlbyCre{HbRWZF2^Hn{WHzQ_KY3XHIE%Q4#v&CNb@Rrr#x0j4MA5 z*?jcgw34;icA9mrE?ycGrgmhEZ?j@c;JW4trmTdo#e!@jZy@;~{MA_8#m%KgM1czD z*PJ+92r>f-Uoo;8=zbg^%K*@<#1B47Y5IfSi^U^0${0n_-pwa{67u#+eYk?u*}`pE zU%GblaNNL09D^j*8yrAeopCS@e#~NzD4mJW($zUUl<*@(+3veH?zXijxNf;6q@DVcZ%KKp>gBraW{#a+{YoRgNz-m@WkM(9@cZpAyR|_-oBcl!R$Btpn0~7St=%}M zoW(!BN(hX4AL4$_*f_BV3W?bEy`=et^gx4C3kdHOVEdPbqj6H@m~TU#h#7mL93fl6 zKh|BJr^GmU)v4}jaq~1! zPPnl}i|!RWQhDn^$%CC}&y-Sa5lZGe1A5-L%mJqR49GQ!>aIsfr-bTq>sQYXQ=g8R zUN2BMp)~{!+ZDrrU+)=Ijz8buXjtLbRg3$ajj0TLWHhBKpxORtoV{dpeZdrKeiI=o z=toM2{5rISm?a*1q*g!kD^FF%rrfMJ$B$5ruJkL~RwELRdG}kle>?H8wcW?*$DXN3 znZ4wyFsjV!)OLl~SEb>RdkVV0Pt7NbEk|c0S;}ixa?0@8Jo;!A?LqIjak6E)sBrk= zt@m>uWk!=+IZEsh$7#9e03m6SQ+=?fmP_?e7|5~Ubz1muQ=SX<6B&d8R+66tG@<24 z5foi1vksp?Y~vcnpI=wa-gn~p#&D=>biyrB+@E4@&(k8sPqlf&l=OXrIAXsY*|wNl zebBc&>gRUqUTkT@3?gL&Y{ezO=-XNwTFhJE&DsxoA(@x-DVv!#?fpu7mjIpfbu;sR z&c;*7-XJd6a>zT|pyY7t;%`U@1{M|>GZ%lEnx`zMoI^xdjL1;0t9qrz%hHYyxTQD8 zCE=BX^^cnWZ})8V@x$)=~|Fy>)PcTjJr{!f0d?B)Fq^oFxQWeyIDLC82U zyZh@&7Zk2IH6aqwi`9KO;4svehzOx7jRl9)%pBh>rAh%LCmlGT<}21L*J)g?d&De% zTm5&c}w1kCZ?Zt0e2!-8UG?#J>%fawoN$2hxln#x~ zekdy^Y4Op9x1i6=nfd8nVf^W1e$eu;M3YmNAQb5$nWs^gyk(&^QZ}XGs%+HC32H13_2xy^~E z!#0`iAxM;GG`l2E*pirzBIAk_W?mKT8jVVg5fnvsc@ERhq9Zr&VPq&cw`8R8dU1E6 z(2>irI9{aQ&8W9g{g~k!vs$j?Dj3TRRBkyP9XH<{S=2FGh3z^JkZO^WT99){7_-o^ zP(r1Sq>sxdVmI9eo@Zbk3Ksc?JfpWPeEM+tCCEq78LUq+kF#}uUl;y8cORA8htvG? zo7@ffGVyZ;H25=_baGP)`f}=Dbf=V`ap-uKC1=J)2T-5eC6-fw0mbys)iv_FA3 z#owWV(dKQZpB=e?sK=kcvC<4O#YZekN8jMaYAn!0?LG;PCeYhP4#eVrFf5{8 zR>riVovP(nqx%u|kDg*IA6g`74S5?#YL{3%F#llYMGaQGR4dX3W>%17DFu*CG~l_RnF?(L@I`D(@rFbAhHiM#Cs#YMl1BG zZ2#89P6m)EC*3@K_HUl4sOsjmZ@yfqDzL)GJ^H&`BBNC z4Ao@LqfO;IhW|8iS^TN})t(?3*Z0`pVclC0;ZCOk+uf^?hR3Kk_bVm|W z&*nz{o#!Y{`F0BVGnpyZ`0BcGLhTx9GX46b7MR7P&cr{rRI=fmE!EQfp*J1NyC86x)_ZB{&l1z}m5TDH|PTUfG+9%TGr~nwDR7-cv%n@+jQ96QUwL z#LHx1E^UDfZ5U{=>b}416=yn~!+_K)Szr2i)JRjnGB8u#yykebT(QSgZ6>K)QDD(4 z8{IQou9&Jy*yhv!WAm}-6Y)0ErIO){`zv1Nh8@py6GR=W`(u?H|Ln${!j4;!+Fn38 z3I~>^pge`rg>)Sg&@1R3r`$G{MX9|$DRVB=BR<|ZFO!Uels@VzgU8jfNZs!!Y(g)6 zSS&>b)NugZ>dx$^7Pyh*+;~tToXR)D`dw?R6>hvKK$fb!C@L2f82KiV6L?McnT; z&`lQBp()GH_HK|HQu)0~#=1&})S1FisWiDVsML1n1YJeF6r|!nx!%Zv z*ANYg8Sr|FQznl=L0yeCDbceiSts~78~cZiLp48lb#YImgkv?oKx(f8W-^6A z6^uDR0F^zS-B9;=9W*j7gO}ZDQpZ~BK28JWMu!8I+l3m#b|Qu)S9VcZ+`Zo&5=?zR z8BlH(IkJD=U!Q-r%mrkx508fus`E+PGT+B^p)v13Mfv$M65}-aREak zPdXGfW6+=|E}|bh+M3_UL5Y_5$fGgp)7q@YNj;CozJMEqaf_QfC)kYI3m_GH3_oBS3RJ)E$5LdXdrM$8{#$K(l6L`hI2K%FWOV5@8%~wYr?k2i7tos~6%(NJ!j;mQM`@t;yD1w)&LO zL&`rTmDs0>rJ1klxX6O_Z{;r7@B*Vj7@>-7Ko#+Uh)=LJ~Sf!o?fw)eH(e{ zNuieV_b%EDUiy}0+8qVEJ*~dQFEg9YZtTmwpRdoSkG3g)E>6uyMMv6He*e6wQv9J$ zK_pzuwg0*xGiUk+ss)j27E4`Sq8m4OfWr!S?2arvmgM>N$5P41pAS8Q+dJfW)`!ua zmc1JD*X(ol+3h#3oK-Zj}Nxlmv$j|ogA&+R_-}#aeK8pz#u;jYl=Z4 ziiJt9&!c0uh-X=PAN;8NnhkWS)i<_!z#o*nQj^Ish&I1At?v-NTWMcs0Sq=fE2$8?h@9YgpR4B zN)Lm-`{)jeJqIJGg{q<<%}(Wr!qglcMUC!wWVTC7_iC@T)8(G+Uh7#Fo|*KK-)&Lr zy+y(yVl0X%Da_F8RGY*{8pEN;_G>2Q(hG9VL}A!Xbt^~ycTmj^18*a&|2?}r#%s`Q*(Nvi?iJpK4zJZ$R=?tM;C8hb2RTO zETT?-402A~BOPi!>t%4)40FpHN|6roTIHcaaKMLxKIFKChh|JQb^V@q<|CG5To&G@ zV~tt&fL4m5QaP5?F$r`piP1Gy{KGL0 zxrV&UwNT6>YuZU&o_Az8b-;RUoDxJFe*bZ9K`b2|wjgF+g`GAPQY^<#t1c>)lY@m~ zr(slV45MOXly%5Il*&Mftv0864g!d?f_A2EcA<#lvRiMjrxJ>}@957RoI-Up#S!&? zq@jc$T**A6VjNiXqAQ1-V}jKTez+;78JVi^*Qdbe+no|dTUvj9kzP28rALa8A3BVf z!;4w%jY5l+JtNcl7?0eUlLz17borlm?Uuus{}HSHPg&Z4mOa?}VzodPia7=)7Y6>i zwjY1U&IPGhe4+l1AalP1rdc)jM!R4hICTYujy3$}cihiERK5K6D=c%QBB>sxSz!yP z*(MPg$6K!0pPR|0JD3PbF&*7=R8}1~?u-tt9TR9ffSO!#!?t@Jf%cbz#Gsf5uvO3v3XzI`auU!r)$C9!2VWJ_T)QAPgmyGn7@cri==HSQK`Q3I zX?i#exgYbuNF=y(;J7lz{2$BqwpTCg^R&BLu2{TK$<582129O~Y8CTjx-zC24P+jNEE9n=Hq?6kf<+Ph7sCU{iFISEjM38AlpWY=@4I1=DQsTTCXBiTs744uC!V-bU)c92cXW9T{{+ zyR5y!lG_}p#Q(Xjo`OTC639tJAD3f|3&x*fnkVPQ%EB*Z@QOO3ev4@w1_JGAuP|C> zV2zEgKR_}2W~=*{~!Ow5!0=fUc+FpMfZ#RkjO? zg>#QF&Dm1qa&920iIqKNkA4q!~)tYU1hp2hc1d@P376ZISGRZrA7>Ck7CYZTTxZ0w^P9wIwY zaPdJG*_tn2<14&drx+7+BpQl8GhVmUnrjMdm6O%QGxlM#_3AhEXydz`o3ps*h5I9` zqSPyrGE-`%eoNTYKt)%(nr!#wQ`Z+b`rJL}P50eLr;g6BKQ#DxD0pJf7}IVliTDgq z6qd%^<+ebyL^?Xgcf_;clG&qZl)zNHtmF8S0;auuY?rk}m)p?3IgPO9y#?{>Y0E>h z)qz$6W7-XVGv>Kn{Kg0oFXJDlIPt_^U!92W^I3_pttF`~x~q9Qijzn?y5P$%iT>HK zO9>yzZZLQ~caL~16=y#k&&0#%s(!Ekt0w7ZYR8@Tl^Tib3&@-m42{M>8rVz1r&{Qe z%K9vE{nwu8q!zS^#b+MSE1c?1+Sxxh^TS)2U{qFxIsK7NW@vsPU?vjp#7VXGyzA8g zP&XH?A{{5SP<(;q@ol96izK#ep1jbiex^6=-+sU7j!+>Cy{`P5|Jv4a2rfw}EjpQn zU)Xoey&^tQzrfPG*Facz=#y zhOv+9j$HA{8~Qoa=g$u@;Wg%6air_8pZr)+PZppaAQC&wv_8>liSJ## zkk{(i5lRyo^{GPHBth9=T07@8qxeVXz?RHKWYxHPt0|;1L_N=K&YW>&F=+4DoDF;U z+7_z;)Ov8hbm{nven0ZV7^j!2n0DZ=-RLTffiNT@1Dk?GkeXrSed-W<;FNzNu3t#J zELSnt0aL{>lZptZ&7F$+(Tv7oIo}z^=`p)^VfJeTw*pF4GVt}+Jj3{%Io(unzwPa3 zhSk{oH*V5x2dcXy`*4%kh-e!AbnFau&uKHB#vd*DZC~*_@BC}I;OT&)rDxt`hg78V z9ljJv+d*rl*?~=QmIT_C*6nBKkDiCZAhj=^a#|ID(@Oeu%PkY}>_}w=pH1f3;z8mr8+|moHj@H`Zr+$qlK37H5Y~)Op)>jEAD?^ygzB#0g4743>QhBBZ zGR_T~e&`aMs~Xtizvx`HKod?ifEaUxUSR?Y_>UjeQTNV!5xJP-9u8}yT|w`Uuy?)L zk;OLOu(PH@pu{y5cBkz{$Hc|1-H!XpZ-8Lr?HB4Nr}0N-lNjuJ$*$PbQ4)S`6Jk^SHa@tFci)GQ7UN+` z7lZe?gw|}5`YPEcS+R|W!OA9_vOZ@f9nOC& z`4$~_`)Od1b@|A%e{LH;HG)1Kn+%?Xf2 z8krnH352AYn?46tthyOzDvveg+k!ZhiUELuD$zXx74y}jLI$-_nsp?cufM6?SR4%K zlVS?!rxr&Sv#)P)4)aUn&JQ>Za@$PZr7Q~OS&%fBf1275a`m+Zw=7>~ZdiB-Hx2zN zTgQpH7Ex)_2kRdg17@{nWf`S$KF`Jlj-q*jsx4$fB@@O0kX?;i!Dc2cIu7m4m#-7L z9Oe-JWnPE&7CvEF=9Y7B+8r?SrjiZDn0Y5ho-}(y;UTRHp-AjXb%ITAp>sLYt`tk;t!a`Djoj4(7Ak^O zA46;1ac%A-mCc%0=0tab+SmAktxx*xzmrJ*T`*mnL8X+{eAq{lpY>_~$k2kXUX5`S zM6}cv)w(BHsp#qqY~c>F%~XLRf)kuz4~g%AW4n${I;@xqDr%sgT;a)UM-ONc57-g+ z!()Ed^L8@ob2P<>9w5tZ*qJ|E7#6Kt5CfF6Z?C7&22r&WYfybPW#Vvr-|bkWfcD4xN44b7sPlHI!UadrdvVt?lG_f;mBc-3gv$q%ob#@aOi^3=ypOPGhD9 zLsQ1tWesxuvl=5j`X4^Lk`E2tY2@I7^6V%o~dSl@<4UpfWt zc&;q>GS^4PA+gCj{ImJ_zkH1h;y9GnUL7f5MVjOu-$YC+y^2Q!8*pXFJ)B)@&do1`mljB-v3?NPELt%24 zJ_X(5=ZzB|k0#pgd)sKH<4<&(vGxAFob4zR{dzKu4MHipavP&teiH;I2TTzSCO3## z-h3YklA?&+^yn7}4zMn$b8H&sFK4EUfAW1tmza!$C@V*0XiohY=p@tnP~kBGtnozt zI5Wz^HnT-g0Fn$Uq8+xJdAlKKKM8kY_#m%;QzL!!%tr6aYt;r6Y!snLQ^}tI&+Z+P zSpxqcCFx-)gnRCN5W(5!i4;bqR&NVeyDjcVccRzQ#+VZd)*g$so=b>os)@S6U==en zeEBtwfAfa5O%u@#g#+cn@l<&cy79dO^0PbgRp zYcfW_b`SpnRY~0IDMTiSx5DEfMCIrW6l@0_Hn|2pWSk>-!a-zgnJmti%EhWnEIFoH zvTY_dQA#?cL2?`aA%mJg zz{Z*sV&4gYl+Pj~hRqv-bPpO{`SS!lb#>GkzzDCLg!Z6y`33CDuVPK07|U6C6``Wb zgNu$BC><9#L|ynhM#(FGk#7gE9rOf1SuNB&JbgbN;(6yC5)?9q^irdQI0t#QnMP=1 z>Z7F{nla6qT$9MQS(N6d6C89UQ5zEhYLERj*BFUw@^(rre2A<%%RYJB)j4;ScyLmE ziRG?{`sUzM+q;~IwMs)`MiHg$A%pu7A@yJ0kA7}Tn!ev8d;X5~L;I7`bjQBnIov@G z$-8o+&Er5OeS%Y-9KQ8YfAmo%Qy`pUs>p$zs+eDsYqz-H3yQL>c%=jsVh+~^*YL;NrhvtF0X>(Qtu9< zv?cvSI9^Mi&r~M8I_cf-vvn%94b5fRpZpLUdm(4LP#_pQF z^voG81;Sr0W>49mR3VIyD7gC)*a?&iBJzPDy}*sxRee4%=|$U$-!EQ_&DP4+Mb_O4e{NR$Yg)6$nD60Tct`h7Q;ABfv@aR8pq%fYv8{`l6J) z15cwbAk-EgW&(=QW;6_`=$wn){Dt+}RkeyyVv*%s_sI)r=O>!A$i7frFino9V6DJ# z&zkmX`AzMcI{sx!cwX-q9yI%fXEatY#9Jrfb4?D)O>5`#mkQEo$5a~JOP@|)f{jSA zLn8fx?aIf_u~+r{BX^GQMs@kGD^ND;HHLvkYJM>c1kH*pAk|={3ZVU9d(El-5s^-~ zWb#>q$_g57xV2#Q4ZA6oUITDnNm#tqZ6!dHrS@F6T#jV|-ahUL`YWc{rVAc8RJ9!| zD4nLxjW*3*P$9fs^m1d6pBWdD=+*R@#`O)=5q`bKR5pVDY9dR`_0s1eJmI~HXV|sI z;K8R;WIh4)p1~?LB%R?jVW4(mwOh@eNwb{xY{f@IIStBjxM59wXxU_MVFL?N6NMB- zR|MB1oAJ+=JcT*EXp(;yuiI4s?o8{NOv$KSYbF_( z9;gg`xS^O8VxE)D7oX{!W&|G0BVqUa1G*0gt>R15%{PPL87i z6@2BSVdtuTb(ZoX6`AKtpFf-fvrX~P;F@XYFJ+U#S)q%Wp+qi?Y3&b5inlQ%=-(AuN?~dx_;J#Xh6@66?s_4zUl+b9xuJB2ufh~=oKBQo5l+HC$yVj&J ztuB^m#3E;aNHKz6PEUvx&<8vC_L=l5g!|{wTF^Qb2li@pPC^USX%~V_w%qPGv)*!p zE!cA|7_QLNv;Kydi1Jf++o|+G8+~#78KArz6LfRMKx=m%nPspHcu&+apwUdLjWSY1 z=SjPly#$)0G1*QDr*^j8y>|OQ>e+7)i1W^KMcCu{6McA!?0Q9}dwG31*@t4L=?RA` zeIMSPpmV*{7I+0gI*42EjWm{6O0{_ne2og)-+F71)Dq+_`7-<}4P7dQBPAWDo7^C! za0A`In>8|0pfGa- zWW2L7RplM#;uusaplDYLu?{#9pdbuV(R&L8AP=tfU}!i>sn4i4Ph-Qw$>dszIptl| zz-T2E-1o`I2WN8HbsHOqZr*u%=8}4>_BoeQ+7QBg^{lt}B+tllRTFrkU)1EUTGLp~ z(U!HFIB_6)omOA^H(E#dFl_*+22naC)4P!PEw$jxYVTwU=cZ7*=g@A};(+@HyJ<%2BbYiVX5Z39^JFaP8(?t*n{$I2(?qF7iuuhOY>1n;Bjeu-&No?HXlm7D0z)pS_(f~OO^YggHgHfo()jcyKXmXjS@$RO(r z(XKt&`l~4lR4XueD|rI1Iq|6Er}TyC44$X}DDLZ4ptvC{u{?PEXmv8Cg8})M3g1$! zLCLRRG5k?``NE;XHODx~mX9BLtuL}Xc2}?h5E?W>91Eh9mH3v;lg3Mwd*W5(J;94O z{_BgIa#pI!IiyN$gY~fI43XogvR&Y zjjLvLvMlFM# zWiO&mH)F>>5Jf#F%S`t1=+A=lL3=mVrIIm0nQ}0}XL1HW4{cPcV%4FV zO}*ncd>_fNZV!$-=gafE7;tr0IPWOrw1ZQ)S+%#KI@B+|OtxtHz`z%Y=Z}3ALc+sg zbj9C-*n~6S?H|Dpy@M^+K9R>*Q&5)BVwi@w%jTNo9?>Uvgw`Va!#nimK-c*vnKn3&s+JE+|(%+j@`SwTK`f4ERCSls` zZzCK*6No+s{dwJpo_LCpM_1vP000wzS5kUU^d9JpmP|=L0vn^FLM@5mngtGjWe@Vd z4y_;5wT;Tx@0(lFt}+}-jc0n6W^zyTMX%v)vx4l2)e?U(X(|sX%XtsxNG8s;&m_Y3 z-%Ec0vwAm9m`#()hWzN*9fqtNyF)53mDj4@Y%!MzxV^grlkki5tAZ8^ThUigD_$? z-QQrS{`F#9W3_DDf5EV&hNJOqK)Z+aZ=CMHuXS?&4XMP23wVbWmu>Hl{i-r-s;dt% zdKS!e_VWw6?{reucdO3_G7`VUs(BE5e&8cS3NPxHQ5u{?WBz17SX}*?Sn z8PpyeHyGXsw?ibzX9R3+dY#W;)k&wq?7F*gI$(OVtHtleNc=@2fM33iH-Vpu5ERSb zE#fMz@4AsNKWufg`j_vNEpj1mAg|fP`c6%{?4uj28fU)#_S?DdV{{+yqHoC40Y)RO z6Zca1<#zY8+O_MMEw$u~>FzL*dOcUwrGGxU(rpNr{*7#~Oc{@Y($q7U=wsG4^G>AI zm(aNPZO-v6S0B#WNxZR98zgz}cWc2%wp0yY>fU%S_9(`T5`nf1`Lp<@k*E7ZWOs|F z*O(~yUGR4pPvB#+PI-*KCegjfFa9rGndU>7caJxx2HNRk_q ziAH=Pw7Ny|XtUw~%P6rhHw?0?O7tAL|ozC zKZZb@5E44HogClmj0`{vSJK+nC$)oO?UU!~18r7nvP+-@WYiyt-INI0V>HUCGsFwi z@T-^Y6}2iMJb_PYJGv3&Xhop^SI@$zF9hWp^PZcE__KOCb$AI6Ng&oSk`<1*MMW@( zVK_?%trPTQp?BSH7j6r*;PXeE1h(ngDg%oXbUuJh9-g0KII3s8;R|@EcRxaxyD0dB zBK@s!k6C~E)E0UJ;#A*|ic2%vYVYl`tVincWf@f#uA>Nkq?v5H<~)w8m}B?|gl&;rKdG?aMu=b;y;tw)^tTl4hq*01XMhql8dF|WpJX#jN z7T;VY;8RPzuGnM#Qn6m;)fm{Z$Cyn1BT(*!MR^JioMJ1MFKsiOcyVQCSay@ipOyac zv*0qk2NNQV4I~~ZI+0m~iZlg+lXZ4CYC1;P*xX~Y!e!fxt@oqVtldUuu=YMiYq|9y zaLd7^I|$%_*Z6^0(K2JG!I4UN8x|na8U|*LuVBk;jR4{#{r;8h5hX+CaQu-icE+bc zG||6EQ@kYe`-&usG3GVJX)@3Xa0}3+yy3$&?P8tM9skNVtY9egWm&Qh-B$7H{U*

zmz7-;<&*zx*&5FGAL)-<-i{5ol@O{5)DQ=2Bj29i$m5$Z=$z^;sT zYy;_VfD`UDVZMoAX=dcC?Hi^F_8Q zsvo|s_~CYVx_GbI{GQ@|iZxza)D(Ggx-30h^`1xoq)n^C|7F!*)TOwOwiGCW& zf}CmZYc(v&thAJ?WgAGO5}b1sf1Ljh&)Kf{A?EY`Y^=t5ZL!AogKfo9TAg)`a}P{6 zNLz0N2b?5x^IB${_?yW;BiQLq?_TgU`{!xS+g4)&vrn?$6weKKI0Xw&+w*%hn^n<* z=%Z~<(2H^Z-${JSzf*X*ddFCh=sO(a6Zm+DpHC=2sm5SQK7ynTYM^)JrgL!RB_c60fybeceOBFC-6`@V5|q zmPo>U{x)~ecb7DuF~j_CJiXq5_v2%&zg{zVla59dU|nA{-2eCV?d5i-0KvcV>Re>d zNSmu|gYV2G@eEgRR7N)S1pbIU#|;JrH9}$nr@NHyXGqB?MV&6vcC>1FOR{)eB>nHFGgHqd!l^!|D-_&Pv7ot8`0b{8#sQB0TgUH7F7MrKh?>f5w$ ztW&zUy0O@vGKQlVB!)FW$hx{L7q!B95b36|!Ba zFRIB@U^=mK%Apyit$pE0#2_99FWyYP6wAQm7YN7oH<~p2h1*bi1f2bUrN?E_Ax~w& z7(lh(c!*_`7F9p}Mf=>{s)d&`K7^KHa+j=PwR=TZV*JI(a(6%Z@Ns@wa8)l7dY^TU zC#c?L#83!vpKrok(4QNRX0*ar?Hr2xqY;NP^ewHO}?wHMTVQ$YgJ=Qr~y1F@+8X3!V?YJs|I z-sWE&+7L1ggJV&_l*WJPTaklpDJJujhgmZdUhHsIa-U{pUC!x9nE}I`i#`m${GO~E zBrkBU_}$$qB-j6u7u8f|Y;9}LU&{XvO8jK8eERehzR{JKl^hqT8EgA5FRGQp01x+T zFRo=6jzDooa0pFvgpP753P2<(@~c7STj0qqRG`oK!ge`os`Vdk{*}c{@5TwMm@u!V zr{czKj_POi?<{^-e=kJ1ryql3AACh*IUHVUq@b71PQ`*3zZRj-NO5W3)zIGjNqG+~<2v9dvL+6;B@Y zWx>K@O@)VeFfYHs-N13@gHkBd3@0xf*`SYP=b|t3<0B6l*Sz%aF0!{1#=I^1d_V=&mMB3@SlasL+TCr&Z?)mxfJa$bOjJb z)Tb>^5K;w?u;F7kvHHTFGUyY7?v=vm>a-)ZD%Qa~0S z9N;m4D=_gq(GqbEGbXs;?lis$SKOTRJgkZ!6S|-MzY19|!`mgf7kBiBe8k?%$MU*I+-XEn+b>yy`=wLU1S1 zbY}A2l$Z0YHC)eZsB}SjMe%s#^ezHUGmW%Rc9A!}|81^s5wr&9;a517@tNN~oWlS`RuGRIha7#K^B#GLg%q zW>LG`3_+ZdrWEI0!O zLC9b8qMe7FU_VYT-+;3W;u4&#`M6jC&D*-Pa8Mc>Xbe23!w)QP9>+%XAD+!xJckja zyx4l6L8eywk~v6IZpNzk677Lg*WB+{ozK~ICa3Lqxjfq5#0jcvgIS92?=)3|M<{a$ zv1j~2T>q%C@QxG8u7R`<9^F5EjiIWi8^4Qeo` zp#4ZL4!ii=9jF=nZ}bX7G1Yc^r06!1GBQ{_R)G2v(cy++%}G~(><30NDv6+HqCdFw z8yst#E*DMiKj`HbK7rO-W87rvf6!W9KpFv9{fhd~a3AnoH-Wj;xRVLk%3BVVt$D7K z3I*#;gS_bf7ZUQ&rVRzLEb5fk-E`~Dz`18Pn3eJ0SL@hrwLJlGjM8Jd@3SOKe=?GZ zlwK4)50RFe=fSvnD3ukGKc+c%XX0*bjvI*gb{a8fZ8I(?o%V=$aBGjz>->cJdA$0o z#=VYBgOoh4jjb1HK~qJhnZ)O8$jOe*;eg<>qmj{EhG?ib``_40PKM^=`G-GAphIu( zwf;I@4K-cY88e$|+%{ohSx&`z3)MI{*gL_PSgqR~fuY~b*w5A-Q0>$gUe6X-VyCSz z{5kuvD@}h#lJCTW^`b+foyD0ZcCOG8CJdDigRNEbl=MxjRf1IsVgj_rxt*%h0xc9T z*n?Cb%(mi3&a$QLgcOxI*QZWPH2VQ=a^~Yx(5HmkE0iIQX%U1eQgmL12w>Ro=zvr| zoEDf(;18 zbxLtAL+)A0u^EbbAM2nu(vO;bIdf0$<1!jK1_Y(yuB1Y^r^1`ijjRiA_aBWN5C5;Q zLw$yo%1bc(Q{06uzPzIoZVU6+n*xM|#Rfy^;fU!vs@|&)JF6XBcz$)ADf`X54lUd> z0#Dq&P4q59%{!Vtfqx8(tFHYyEKZnD{2w%^_}-{?rFVtNi?O7-DsqzCi(IC)fzpoy zO3{LLo#9adB0gA3Pf#{cz6zO#cu%Np;to-wfG;5_hA-QX;D$Eg5ex273x%1-$!B0n z_RmQJs)OhaB0$-gg6LapJ{h?6ZF3C-ZtU~HtXbs}kSE8cyV9C&bS@+6xZ zqhg2T{LPZwsH4%J)Rv=Zb`;D-(CcNxq?BDB+>@(VgkRB^Sp#STTgWjzb{O30x%-`E zeZ;0$aawCyc-aK!H?&_1QJTAH#b$E9Lt*IuM1@ogb*mIH zwHP*3itY|{5an1&LL4p z=BqCRc9Fgx+p#luwwfx6ogisR;%q4z^FOYJ=RGb*A=S0n97=zh@j* zkI!j6b@{#1=ca1LuDXF(^c;7ZK^`O2IreS86NngtkJdS&F%kI7Oyr=3Qx2R&6gD5|wHUHk@`a5!& zu7BnDFDwn(8guVXXr#EWWZ|sidHSHXUT7Q;6+L;#q;$9zNgcj@5X*p_xMHz?Wa=&v zr@K5*F8eglvsGe%X%yp}EfMJAeDy(n+Vbbdt)L0bzjlns?kx;wacIah9=0Qm^Q< zEkuS%VHg66=T>@4JcaB1Rwn?nlT`JPD6ht!xEB@r&9~ul%3anD#NMlWoa+;O6Q8Bp7~%`>7%%q)|4o)9aXL?w&+}D}Nbok?8hytQ4(zYnm1`TY zqRsoG#o(l|-HrQIanEFjPij1xs>jb47j<8Hh<9I@M6*FJ_=t=?K2IFoU9^;c8XCyk zAhL`qy(glBEthX6Dx^2#R zg|eh0`_?DrKU}o``y-`rjA`$JXz&r#ACJUq-alUykaiQl`hvH-D;^41yj0BzODi!1bpCoUWoAQl`t#iu~XZOFPt!ReUYvd zyxQ~h0ZSv#voF=QRX%lyT-S5ByoaKDAn47xEzxv)1+%Y#@BbZGf4m|0t?B%Ykvo@v zG-EE9b(HOu^?ZS{r#EM^&2hS{D~zp^^gASP_Cbm^|GT8?qWsIg#PCDkC4+sr_#CLj zof#4oT)qAlkAI6_v@#cZM;=~7*dSNs>FO$F0(TwU4useT)jLB?WAKkJ&9Lk~RVQ7t zXiJfbFR@9wW`SSc`*rwcFkfNJ4eQfUH<$jvCCtwBEcvm_>s7~8vDTS_Z^B&eVDLP zFc7`>8u2zB+ESHy3xDiSFGE9>zzdp~xu&k)3r(@yqp8e8ue1-k z|I#<@l>1ZceMb1d?VJ9QHylC>O(IZ#ME!He;x;y~o8oeA^VROQAl~O8xg763soKG> zq$@WVx2fIF%zrGT-?5T1%{MzPm4Eg^f1K}w^Lng3pamji_9{)s3=M8bK*4R`@V{gyq> z;skZ;+#Ic)^)4P^s#Rdv9fB)i(ZSDdn)qtfE)=&wxBmZVfo586sr*Edc_ikhWYttv{v?W7f^| zEQ|KekzLm;8?>)gSxb3cgzwhole{N2l8)m!I?7{`^GVsCEH-yuTz|X~NDSgbui3E( zANk)EVx`~dNU0F#hE6!{{wQQcDH0x~n)~qG^PBBgg7)H-D)!rdYcF>AkM`n>U)qZo zD{Gh^Xb6trJ~v%L^Y0^G=4y+;l|smt_<`A2oSL~D5XMZD*DiIzS{ zI&&=N+HWj6S-_1~OlyfgJud}&<2F|QX3s*wpbM~XgNz_-RVj-l6LPBI*gCe9vZ7MT z`D-$oCF{G}tY@sLzWoR@WS%kOxSHxSqeiKd>Xgp?i>h(~Ko^~QC0lheUST3v`I3xl zi=5@_vsx`@#o0Q__51x7dj8t`j{{l7OzK{H}9kTv6S}(kL*K{GGW`@}x*WY+9d$E6*<2fvsvRrQ6JgA`D zpMJXybCNx@%Ue#fFrfY99McVMlWDDW5;#hPDJ{5yAQ{@jL##e&>PU`GJ++f$>Sql8 zLuKeV*T+02@rG|uUMG?HWvjS3IOOOfl??}dAJ4zCrD)L${Oe;`U(UXand#^Du4;u! zu|k)rzE+w4yt$D(buJ4tT{k%^01+hS1XF71Nwuf*lyx=wAJifZjpAbzwsaZG;~x5+ z^FqJfKN5Je*!5X>?L9G_u5=e4UFqXgJl-1r?%#3P(r3JyX~W9! zzjq2j;?-MJvKmbb zatmzCq}nbeX4PP36LO8KGFDKy#*5wc7rfXw?=mOBLf_47@H0s^psu^}I5_+3*A(IT zS60`h6KA{kPM(%XL?mY*9=w+v3B0~l%s(FN6<6I2ItRsm?tkVtXww|ctJmzDE3jFy zb`7FqeCZm)d6GTJtRl8Q2UmM17EbJc&rz>yAV?|GX7Jh(VZB&9zod|qWbt8 z0u`0-OfC$^3+Kt5O*|!a)M3{a^wJr=hm1#4LQOyK?814o4tcsVGwRP*KAyeq;C4sR zvjEL?gY&lO*B0J5u`XjpGniFErczi%y*P*W8@mtaYdfJ!Hzxy}$vFj=^ zz#W+TJ*&O^p52z!?#W{n|D`=QRc(5EE=K*=_T0pfCEASeWp1i{bRa6LVtuHaGyq(7?W!dXsApm2$OxpCMLd|E8%82u-;|b#R;)^}%hm9GBCj>XX-B=JZ zthGWEQ95>dHcV6Xc~!rzBlL999L=D$mnZt&t@)PYtd0ePgXs0}*MxMI@Pj5zb$s<@ z&LOz-N^)n%m3_qT6+79K(qHhPy4}HQ3(e@UBZ8mIqq?(Qo z3bq`fTPi2Q-$%#a?CVr$E*-{>IZudR>hs4Znlwdj*|C?QOWwFL6TWNW1S@p3+lr>t zNfl1mKMXlGCv=FnLc~>!;EV~M@~me=LMDYNXDK+%e^-x>OmgyhWl5dGtb)fk5kGC; zcO`Vhdf%0aUab|E6+13Rg>MKi{Y6$1NjQn}`#l6cgs4X2_!P&uQEEjaJxKis%xhM40L|#^sLT zLx(><<|evA_>dvV4-0ea|TnN!1PV%LL$fw zYvHnl7zSfF!CXtxXjSR=iSglm72uh9#bKedSBS2s#VkOVTrM3A{2+7_{%gapw+vZ4 z{=rRjgh>Ry{uUZwV$OXZ&l$iEzT$Nj&4VVs?R(YHL19`7{F*%r<^^Rme%* z(52h>bZC6jWlC~5K15?n^7xgsS9K9SuR1KlIf=<7!i$0X*n5qj&~M;zYeI!S{H2<} z9Y){UGoi5om-m9f?BOxNI4e5_S;&xyG;7Hm1!0;?#Y<~4@m*^t8%3ps4w+0GmP~s* zFKSjFG)f*Pg^FL;P7`2t30h`SQ4X1NX;tBHJcWYF749&D^NOiW=By*}H^>`8=}Sdl zixS5>TXBIbNONvL4wpTj?-6M*67*#J;i;43^S+(>Akt}Wyp5|d46V(J{hWA*i_N)xKH@hhz#NAy$n*kz)ID^2fZ#8UAuiYvmLXF1+}r)@hZ-S zo^j6JeXZ|BPwDV%C_PfpU}1KyR(Ha=*J#9a=%Za$&T1x#Eyrk;Bg&t7(;212N!-wt zAfPJNmJY%0*$~&ztH_AoIj@ue=_qDbt>Sl{3&xe>ck?Z{XLgk16*;PbHh1eFEm0C$ z$R5}C*DIJ0r8DsZVs2=6pjt|!l`B@(%lw?kSj~n z!o70BvGR<{jTmPk*-U{!Ury6&r}CrohTZ_w2-BjU{gNb!V(zH zYr_AS(fl7q6p!2EBCO>lL)46b8&0#qfFpJom36R)c`622qbwF7%jtj-&JvB>!A;Hv-8$tN;zMb2Mlkd>x(FzpE#IMyTNG#fX(Jl0kX`h%vfo}w8Ug!P>m%tk3%%C3X>k;tP9rf=*~=qNk8S? z=cscZNz*=5;4=i}_|R(c79sfXBNIb+@LfGw#O!iazJ@A|jNXn6dg5Cbp-*llpDqoh zH_)fd#*=|iz3(H!V3JW;MK_Ur)QDyr+S|8~vftyG0KM!+Be2eVPdeB?_ucC7-`G!! zRP2nPo-RsH91gKwSRDL!Rf#Vug;@WLh&6Ks3r}$1|K`rrFmk{%}n!_2l(SeLV)Hk%` z>t~mw-Pim9766tgYxp6G7p(PTNH{S(OxgCQ`@L)X<6f-*y@n96%?xtx+q&zeO1+Co z3h+6+ff1x@L_6X|!@EX3@}$|9^OEq@F%siWbIrp>kfv0Z!`AaPpKj=x?o+@SauS2a zKVE`6=h7MDt50%Q3}0Wv+AjDaGSpN^XITbTYBD|&KjP5oUBXsYMP&1-DEn8TXKq<; zo@k{)92j50Qw}f}P@z3@CUUsh<-At8iF-WuXX|)HMhHQdn08?z1^%QIuc$-Q@$^Qk zU4e1+J{bahlKnY;RHCH}vFZ#OLs%LUKHX{q2K^*} z{B(Q-LWTC$6((>>J%aV~?u4EulQl@4R)@w7#!RXw)T%>0`Dr5iarfv6Jucf}-0NwF zW}W%6tpZE#872F1+$)ow-?ljM=E3}^6w|eO*Gx+n8gxG{q@@n># z_TWs7yK%f?BRHLXh?~WtvP2e5B|Hqya$B9SH7}FpQb$-;$g+?>jDu8(&_vrxDK({p zpUX3CPy49RG>f<32-3g#Qhb{^Uqgw;$ZC@Zl!XGg z(RTNEJH0Yr1xsR~s`R5J*v=*aYzO>X0)nwc8lJQd&BUlP{ATMqUc;Vpzw`w7zWN#Q zj_#q1SfP3dK0OrUDTL?MR`YJ>pK}n{j_LOw5a=sFR;8zRInZKT&P^Y{R7_GO*3W_B z?H(Sv$-^3HsO*EDYMb1^C^c#pe^ zHAI!Q+4YCU1=fv3w>Xcdaj-Qv_5!4rA21#C$#LQ+P(>2BdM?4F&5om1;BEeEsPYJd zpP|cAl)z1`qeS&ESfinHDoSCBGhX6Ovm5A=e{l5~Uv!#nt(W=jqf17#<7a|@zc?!L z4g;GBbRa+H!~@ggG^ZB%JI$|sz(SXaXyW~l7qSk$N4AuvNW$( z_+H800Dptk%vpWd-b6q@y2W7@%}AgRAsAoWHhNn?*n~kXyRj@w;dr6)GMXVK^HiZ@ z5ZHq9jBW1WUdI+ykR25mPV0$n@@0TiO>)g&2E>y`@b}L36MdK;!~5kv{E+cqt8ldX z+o9SHzSMk;<2y3Hwfk*U?JZ8r5@acWP}$c+bc+W;v?UJkrc6PWqd^7|0C*gPd*Ho{ zpnN9f9$={6w=$MuCSAaW?|M;>)A7%AyTPcT=rsW8kZ-Sv+M0=$tdtc^qBZsW&noeX z;0x%M5Z&5{AK$ACoZZ8ZQW&xVAA!kJ9Ut@Icg~DjQ_6GJVOsi+8}VI*U8O_gMB-^K zvlR=%fD$KJ5P$LmYx_0qHK%w*E2gkLFWz%JksYlDz8QMo49F0Km(DAxOv@&bGm}uy zs)+G0JUOlu&nK{>j%)|N)#)UqmyQ|KX*S8~Xqz7Lb^SuSaXg>4hWqeV`Ti#NzfBJ@ zeD#M#^6X{wri9Wp6-7x3@UHa<&IJGEbi;&s(yH_;GZf&FW%NYcMb?NoXp0kvX^TlP z<%eiW0VIeabxuRdpFkBg53>Gxi5tg>*J5MJ%`%5$m?)1rLUuULknv5c|P5%a~a2V~S_B1Yj1?tQ!+HCb*lD{l-tMCYwwL zE4YP;dLsh91YdzkhD*UN!Bq3?_V_1tc&bnC1^ki2vmgO*_uwf4w2uvZpR$R#=BxzL zhYAy$F2jyqG+h6-bTNyijK#t4o6iDFLqZ{mMG#YoURB2uy z8cpunzH)poeg!`=3~uLQi+m2~{QJC8_)-O-y_i-3=4%Fmi}iO#kjo+jCoNJFV?)S) zp)4&YPHP?wrb)J3n-yw0Z|B|OjA=;e7Ro|-<@f=Qv;O?fp|-ByZo=>3yPm8OoCG-w zs{gr(?)z@S{|@v~Al*BnYvxIdy(7a&#r^kC6$Zbkn&tKKQlHXR%1%DKYM>=88!vI1^U#T33pJZ8$5J1u=WiQKwRx!m_JPdI)Kw4+WX z8zpH1VDKgnOl#mR@gjiaY5loYLb^EZAnrG->n+$dm8`*Nu;OF|u=M`-Kp3&7eE^xr zQ)1LyNKYHGR%i*d8IgrRo~BYV#cnkg)7;VL%kV*bTO&*U;68zP!j{iw=s)06*Ra)y zQF0)W3WIZ)AXdymOsuI$BZq9c1eRHOVuNVP$da{$N4lId^OEzo+WA_fHw6q9KhUzFfeZ-%d%h=)*O(CH$PHGa zMKuz$n-Y+GGLgapR4G%C1YS20+2@^+%}BB0`#fS1(pR7~b@&={Xk`7p(;?wUq_1KU z(wPXrdmHo7Vpa!MyAsMmq0%L* zBAy2!3`y62Jb#)Gft7v5%QX({eRJP_q%+UdzGe_*IbQl;@wg`acHTQ)%$wz?P|y3< zFT#!7s^$cqBbPQ}0&qGVKlV2|I*P8zeSgby{H}-S?9I9e3-;Z)v}|wm>ku`Kf6NH1 zeapG}qD~X7iT@Hfo)-RJ1&&fGHF*4tX4j%_D7T4l6cBEUu%YaCfmBEE*~$!0?<7aB zbnt4EnIFgHHF}cRj##aiP9S3BJALrflW+Ib!`Ok;deDeza+0?f!TVb}1GJSykdF{- zC{abLS4HDb^GYW{$36A)9%5o92-HBvjCk~x>DqGvnWLus<|Pq#1~Tys7Km9|O+?1{sY6@~d>!7KFA;En zD)qpN7WMpIiV>t3g-<`=>#`XkpjZyIgQGxYaUK@JVXBABNhheauUzDhFC@{Vg(pM)^!Q6R`uz@1n%-^*oaS0CsqyGO(q=p zJv|_bL?cqxwDD>NMi?03XpG}OF@n+q@Ul&!Dv50vtbD<%hsb!T0yxNYgyxpxBf+h$ z2oa|j{ZmluK;Vo| zW;%sbI5;v3xO)cHeWqpJ}k;HVpE$+vd3)pwg&%&ffwEVGwA#{XtZiY;XY%NSp8F2O#q1gU{d z&%~*^vDrqjN7Bu|s{x2Rayz4jXpOh1xFsdx)dI+Q=QOX2&?f34XiOD19{5i#nPF)l zppYg9>4atL*)&l0Fq=5Fy^auS1g`=tp5`551alipkP-$Zjt8Qc1Gf_=em}4J!81ni zJDV3ldI7&}ONpCX6fiqB;F9xX@pj=s%4OG4bE22Rx##lbW$Adv&~=hc@+uPe+-1fuWM0H$v%TmJkN z2UJ(eFxuv3dA!*CTX|ucNjgup3hcJ~C)G{DJUI<0ND)IWy9Aw&|JpxYxp_)VbEF|5 zp>B#^buQ+YVNvvjQ|ab4!O27?$^050YqJVDTKT8ii~ z-1=-5Wp5oX26E)8H~s*6{4xrQ!1UiGn16RI1Oz5~9Z1LjIYCk^_^lR0K=%+pFjN$a zCxJ}Vh`Zdc%LJ?h%3TI?v--+;TLYw_<#r?qpCrig(SUAd^+|{_KrP`%Zn1V@mYCuH z03|)(0Bu=67^ix{gQIn>9EbLdRxbJ%4XQjC*0FuL6;53fOxA+oMqV z=>}q7oiE9+g1lUwF<+Wc64RYukB6U>i>XI1SL zWXY8lu}fu>3DLaw(QpbUc!$8%z7xsF+p7V?0lSVwj17kqNOWx|cr9d2N`PZVZ!}$k zWXwK|A{z=9IB_oFQN&e__t(0GL z(2}v2+~-nihi^{XHz?YYW&FXTNJs}WA6!T~G~<6HY+b^Pd7H=^e;vi7*Qb>6?tqfo z>--RF z!4Nq|p6D|PRj4RR3HM?rbvSTg^R(`8Yz{YVJ?6S$0>MBg$aqLmx_aDV*vzY_-svHz zWX#i^7=6w-IPTm}wG|koiPDmihxQ4w%|iE8ht@xk)6$**kiD*YVnb$AkT)NQJyakU zHL#yw9_rjcBsX}HjETHv52~z)<(Xe_Fp%L%I+K~-$XF+fw%3K7NcoVbo(VC#L-^4V zYvxR93Wpm=%>^%mbXzk5q}g^#CX~ieZmp04k757_5=m zy92)J+cYw{tsIvRy(J!eeec48ll)5Gn@lI2ihL@_*bzh^x$x;Ny=PWV7QqG6ir|*< z6JS0*840jbh7p*P#Y|D!{sL{VFYds%wiu>>Eaz?+?^Ly9zE6o&+R*O3FO)EF7<{{< z`9G)7MFF~{=dfVz3h%kLiorC`3j8~?JF;)9M9zrDW1X4^Cx$+G$!*mfXrQxgQP~R6 zR6;{*tQMe$9{v|LhG?)kT!W3u7>P}I{hZ*_8XOl{8kwjxC7d9nZ%l>a1QL!QG1x0h ziZ7W0pKnJJs4PMa7Ad#xoGSdGZXDrKU#Lh5#9$@y^FZRJp8|prQX8JEvciDm?-VCc zxL5dV|25m7tV6IJY~G~B|5LU>aO8s;Z5Zq%F3-~EK{;NaLFWg+BhUwTGKCp)VlH{v z6j*uZ1~dR!vNTCyQRc6}B`##lB1To}QOm)kOF^ErnC6j<@bmEE$WKO)oL6`gaSf4~ z-6TI%z6bRZI9-6wRklhO&_-=&@Hek ziSpu*2=atxkJ-p{Ej199F9QX3q3(g)S9k@!(zg6Zn(m@ZBdZyJORcApN_06uW>-+6 zcwYHSOqjX=cs*Tr83vNKs0*bIq7WcIytg3Ys1X^LfwU3^^KpAx#RR>qx5P6!+f{hu zBE0@`Ti}3uQpW)CIC;3aoz7qx4ETqfAJ^!hD^_axu%(L<<$U_u>GO&a`X174a$Rcu zZ-$%#;(R7yqZA{C3Pz^?zFUzwV(o~*4+-P46?62UV#S8WQW#xGuf=PGt#9#o|{8)B?aE9ZQW?~Pwl_b>zWtHhT^Gh3VS6<0x}6M95od|Vgq zmCpo)m;(3)XkuZ=Lnk+3tYsiVr;f8cv@GkOyIi=}-DO!wGs|yv46s|ovKO2=V>KnV zE>KaL)lTjmanx1c@E0Z4-(Y~&E27?e_ZLM|cYyXH{rG#ZWlw{)qGZ$n+OIWe&@L^e zVUPg6+O6*1-@|el>y)Q*RkU_^CQ1Xo98#7_!@w8WjXgoO{-M? zC4mpT3mj;FrkpK`BvGGBklzXxKr@WCW;cpbKOGtLGoLFO{KrF)oxRXxFlxCd#CKe){>`GcX8{9`Faq8IxnxzE zed*OQyc&_6H1W17rem@SGAK*|bq?4u&rNJPztZ5?W&E25d<@E1pQUB$-T|#UY%T~i z_^&Uyvkd;EAldXUaDb12soyZKVmx{GGgy@@$hT(Ect#tH8u{$)Sn*!qQq>Z zLq)8RtjQM44{HS4>>%KO;YfeQSR>JZV5xFWpX0?TTH#%61~?Y)5fL=xK)%!r&~by> zPm}Huwp%Q$iJO6nc7u@)rwFiAFvf8=q3!riA1l!eruq0%E zY9Qp#R5=6}di4@))>Uduj4e4iwKBnls*)c;&;#;km?Af;Ybi0llMAn5Y>Tj(FWf7m&i7qgE{?R@Ej;)9hbof;OV`_w3A*^yyI4 zW`hDaK=|IEikJgzTY7`s;d1b$4qkZ-B6+sAC#@<2WMR#~xqMQ9bObso{6N|cbXvLL zjIDLNTSib0=+;q49%pTErsFd{KPE<{65%5kQkAxg8|5sLhJa#LjV~7B&xeTrg*vgk z*GvX|Wr%7V>ca>Bo700D77?Q1Q!<4G{eaM`6Xk*{bB$Ykg2CTCBpxzn#RzXO9OSj( z&$^Hm8(|F6Jp`U8>y1yOwJgLCt=UsHad*u=jZmoz)^F)cd{ht&`E5&u!S9$LPptDS z+0`28&G!LyFNc~t&0WGcW($CEXGo!e{vjRijxpau@s!FqNDuDy#8ZSM) zWb~>&GqB!`>pB$I#1AiaWbV*404qX7?R-c_T+nVp9KdG@Ev5C3VOL-E%?5m1ZzI>yHyfPq4!9~n_tCS$y>1et z*Mn5yq(YQO&}(0BpNjnD2z|)M@(Y$<2{p1Z3JEfZ0*8c(~M7bg;CxegyfoT9qYO2tTnlRK8^PLUy?u9Y_ zdIJ~X&Mtf=(Bi?{L*+b47qQ#!83+8?@^L*EJH!_FSsRcmf9n8iU^pz=#Lq zV6FOFaj>B$%{kpH+a)yQ__z|%G0YB!+S=N%daLGTxr=+>Km!iRM4+KAZc!}<8MDiu zt1)5l9mDiD0*?qdWwKRw0`>5FPQojEw(3IArTOmeny6U2T<*J?5$DtnB-r}s0Ni7U zgAYhu*t8@sj?3TUAK^&=1uIB-oqrf)ABI)9q3|Y1*C0f<<#& z8f8YMUi-}hYzh0uv*Z)5!k4s_pH$8vo#2lbEpL=#tzG`wot>SJz7Ia>ve0oZ&0u*} zz7)6}e#g-R-u$%>uQYD85o3+i{TaUA5A@iSljTTSu8w5nveB>#N4Q@QnP`bGn@Ugb zRO5a4t`uILE|kuKQi#|CK-Cho96R7UsS8bc#g9FKZjj@1x;^YR7`wgPt3ydg7d%Z0 zy*z$q)o6l@4Lx;m)@feDAp0*0gKhq$g;XCn*bws(@7TM^i)&TJeGm!~gDzHuP^$1a zU3h!(l-GG*fJ2yI!@&xzWju3^%VSn>fkvrkr|=RCswg4MH_m(gOK;slG!&Tn?SY+t z1lZ~EDQSBsSallV5~O#Ur`1&VeRf&HpwOBEG=V&_6UrnMe1G|Ao;%OXK%jQf?KF?j zFbLFOuG#-|LIG6ULF1x&=I}PI+a>tO2f9a&lj7o3Y($9tDWrFh96r^<0UF;G8|V=e zBQAy112iuk$Qd7x(}q%vmuHjy;B5xxo?dR$fxzz&O;9NpNOY<+ai`Gq@a_&|4uEc> z0;>zKi}$S{JZfGdg|r(5qXx`(Ru>{F;IYRqoe%+pbWGCfB1fVZ??Yi%4Fo^PLE7dg zTl;za)%e=Dns1S5h$P7?47akQ*^667$eTkS5>4cw6}x6Q`T~cLUJIqVt&4`y@=!_G z%*i@}eJ$cCw$O}dcvx^~#{ISj4`o0uwvIEPE|D0?KCU=@tQ=Qg1Q*I#WQ#Lq<%~NLJ>H#p_wG{G4Bv*z`ebe zfz6=Yg<39dE}HT_YM3|?K`!AKCTOdrdt&Ymm;gKI+Uv9;k@t~Ju+GeJ zWC~9a9xZR3b@Ss>ZTXLv*7?P!v^d{%i7TBY(l~ANtm$wvht%}3M-bZ%BqsJW#!mv} z(!%A^P@Zb4WYYpwW+^T9cKn%N(i_PIVhc#FEcFE2Vd;OS4O9uj05$G5SlYOpsR9EX zh;YB6H@3ov6D)H9w7-YJRI#{n$%_fPL_hpmat6px@02s$`!~`d7!c*izEtSwzBM!N zSk{A=ydvkDq`K^;kIP7vuf4sm-N@xFAZFKxA}1w|Np(^#IEn59hBn>jhLKiHRS{?l z<51_8nJMqjh+Q>_#y=BXP!R*Yo59LRC#a!8rkWX~!=yM+|33B6?*%C7AhNvp_(?I) z;jSa`s*GPne$xpyRg(3NLfd$0xSSxN z*ogn|#yy@HUxhk5L%h;^{QU=Zi5G)gf$#I_OA#9s>?3uW_3O(3bvLZeH=O{2-X@8; zs1xe@M-SUF#h9I0xJC0qp%0V766JugqMix#rJ#-CW{eK6Iy3%!PufPy&#S=U8P|Aa zHk>Y&q*BDkkkn<<1+GDNy6n4wB7_vyK!HscR2x7;EuvKwqgUNSklc<&rm1Q}U+=+o z9KvpazF}x4$9ggum9%J?oxeXZb-o9svy7Oxnpj0o7KTk0rYA;Vmk!F%ETzg; zttnj~e|<@UrWsj{TV>9FP`0Y6s=1LH^{<-65)s)v^q;yYR~ub<_vmlK6Fq%+gE6u} z$IDSd__4ziy97CnD<`O&_&?cWi-@t+fu;Ku#UMP^MHiD~=KDd|C~B54SH; z3*{MF-ZiT5$vbv9uelVAJTH6V#CNnukvQApy@DN^Hh~)9<9GaZ91ExY+DASsX`5_! zb-rd_nsHZh@^eDIFO)p9zrw`8+zz~R5mWfg6LjL>LbBq*7e2EQKUEz(HEL0J#YWOO zWRYVX0hR-#O(#Zu$`IrmSX!N}-ZVb&c|1~hb>^eW_mVS)p`zi^BQtzb-ytG~gzp7< zCRy2E)piX(Hb%({KY*vMOJ(At{^a_Ot7wJ*oA9ny<9mfxf=GKBu3ocorAJ$b<>U3y zqx^v!$Mt&;e>QbbOc}>`3l=d=}<~re@6Y}vUdvn4s=wTWYCs3{*@~< zdb4TM^G}F1E+WUB{t_5#Z@T#D__F5f!`RFJEa}SsPPMys;_Z_^r=rh|YJ4`OTPZF0 z^C8!OtiU(vR`}L`)AO zn=*QjkD+|P43m$IVeKxsTb7W^Y-%H%BNZncG_&&Aij&)`?%;>@z!2a+%asQ%;L5PG z+YZKCaI@2`auA^aSzzSOLv?6}+pbI27)3U&mK>0sc;7rRsM4jyiMJm9dQ2jJ*a!x9 z3Ve?70ybT_`su{1-rK|2WzC^Y!?~9)XE;C41$3}G91!o={lFL!+|+^eqAV`1^D931 z>^b7Zsis$a8mUJd)=a*MV{_<+rC&NRDT-S#8$fX$2r88CA}}JnS2g}$4`Qpw9pH4~ z;C+q_qRUb*vTnDE zD|<&=u6@`WyfUhhc_87HuTjk8f%9XRRgP9fw)KC0V^GR(V$i;|VkwK zMb$7d;~qQZ4SQfDe`#mP&?h(~gW;Fk^D)SB{5EvLu*#xF7}U*G``S*l>{hq>3{WFh z2(hn0RMK6Z6#vk84-OwHuq+Lf<|KICj$&fbG{TUG>{HNRJUoht2{=3F2HIS{A|j3V z`YPM9n#&Yc3>$5u==^K3y>hSTaYc6VD!r5QL};SEm5uJ^(R1xXJcAIyo9swshdpDq z16}z&%dl<)=74gRM@@ZOgqxtcZKF9?&sFGEr8LikM60p$z}6~P*;#1nVc1E7(0f?U z7cUysxCx~vzxi0I^Q{Dob+_F^g2UHHwT*AgqfC1I);H>XZYoLOElnd7V zlZWoT;;%(@IE82R?hx-i<0fyL_6ON?t6~0IL;Hj($3!dpy%odi)%881wxy+_PpVov zi#8@!v~K<@2iI|DG(qc=kn}%I3C5`M5Iy8kHJ4^4{i4 z1zBNm#90{CV|F7|DptedX^ve&l^LW*u_c5Zy4KN_9nAdJRz925Y?Duu?s#?9yC}qh zF&txex}apW_Ks44qgz$*j#bIK&lX%C(5MZdo;=0)T&NH>Q%`x>AWCL)qvHjF-}eE9 zknN^V@6bPzs-v)Ubpa%e0YmumZwtyepugi@O;LR8qnMGK|Ni6D+@OCt(j8JYx5vb` zF~Ha04n2qSX!XO8rB3tTgKTDN{=Yo4dO_;Pl6@V#A_n1}dvNzgJTDD}8bmnnt~d zU$WSI{i3nb}ovMY4OCrrRM{UW&2|a_ty%!yY?G?jyBy%`$zU19J zf|Iw`=>L0B)$q-O-ZLIW7rBGOtvCK%`Y+v&*0=tWZV9bEE1Maz%sa%gmV-eYp~}%* ziEUiZ^KuIi`NJrWd-xb=_C3!qasr?`X*uOycW56H$lEs`YEpq~H3peqfF~VMH0uk( zSw^HLag`v0QFIi$B=K)95jg73C0(wBVgBN6e+PovW%KqW~&d&Y--o^yiKbgjAwTa8{R;dYQ}T~ zV^C7o*Kn48Ez~kN#w9HUm9p_!y=F*{uH{_6QI$~7)kc|Q+U*=(+M(ei5HQ+s@cc&h ztLom_^fs;U!CAq2&R?AlP+;7MN|!^mtD*%)hwko%rI2wh&O8+Out-kJPaJ%;s#??JN6*CkhIjWBz8F`qjbBP|aCy-1iaPe4NEO=&NH? zrUS%ohwnJ#+m8Zgd_H2fHw8@fYZKxw&7B@DBQx|LnNC#CtM3@=fiuo(_c_4v5^ zrFC!8a`*4U!X0i1I4@ zJ{PR6!K>RH*nO5yfaGFv9C~}0@BK|=$Gh2GqJF11gGKcCBHRl+wP+y!YBhU)(aqpm_wCR9 zb3({TY*&6_>bUM@_u)F{abrKXo(H|LPKT21`!VDv=sp4ods6!&Z}Sq&S3;u40P7NT zK0;aDq@`2trck4ZXi8=-fJkL(xlxK@ijuh?qEHH61(EMy zGhfaBTYMhHNA5l6cYe$JzUO!E#lGof*J%wKF_X=?x*PDZJ1(17<`fY>)A=z3)@bz>@$OxM2+6H+!9U zW6L=dKCRbLzd1GHYBk4qYR${rtDzJrqoo27@ON6rAwM67Lf_dwNE}^U~b<+5z-hV$nVaDd?Td}g=rw66`Sp@20DP3qH#KW8vDJL zX~bzPg)`P8sK^~}9mnvlFMvd@!h?DSWOnXJf_n^S5;s$ZPx*1_ZfO)vysnIOm$htB z=Z+VbTN+9^wSJ@BQ#qTMgOkr8Uo`-CYrYy zG1qMMSFVykD~NJL>|)PaX@EuX52Tws4LX+uqqlN`AZ@u=&UL#J6F&lo%s2lsnMFyS zC>JjQK)C+cn%@E&oh@<{)hy?{YPnkM^Y%pm&ikVFNT5mXuv{bB;R?E@GqWqaIIq$~ zYHAf#9L2y2mRIpLti*B;thG^lJbj}24nh6IopTF?n7!2ypg*ua6K z0BBqCbRa_8%;qWnRqX+#q6}6@v^GiZ1AjQ zZSB}m&r5p9@lpzN_Yoeyt0A&+1atIFT9CNdqowD?va+W^H|*F?Qp0O88uH%v86Rle z>d6<~$#ZB}teibovKF&njZ^%4H7lycsCEb%sb(>H3pIq=Ix}A8tmdU3S0V#Kj}&tz za*{V*T~H+2+sW9&MqsxVZK-*1fHl$LYLk!3IebcOsl-%jI#lVjEs2i{0s)u$(kFx5 zdKtIB(^|sj??tsM@a99QYY46~s{{`Mw~?G9M?6C&UN`d2$6SayiLmMweyvxOBA2 zyn2teL`saDgZ3A5&@PH~5X*hY$aS=JI49i@3hvWH(49w*1E=YD&*4rt0KLQ$9pG0P|o| zX+l_`J!am#?S;pFJ?QKaZ=UWRo`~*PQc=A24tMXa#W;7%`s9)QDo!n9`D_He8$fhr zO57a0gt`*2ftD{7oo3w`) zW}O-nn$(7l@kT*k2lD{2_lY6c*lG4`$UX?oe%$6g?U3KbnQ|(Hvje9)eZ}?^RduT9 zR3o4DdrJGttP1O4xrf}XS1EE`l-{s=$m9V~k>yxn7ma5X5Vn(FI!Fbv2esN&+3SAxVy=B8GumV6c!9Svk=P_s_qe7m=tv~dej*wm=dh#`&jmYJy>V_xyR_$l`M z?G6qHb60G3`+Y;PxKxO~u@im8w-3(xaZl#8a`?1$XfODTD?JhH=N)s5=huJ<|4imr zFP(+E+tdUtayzwmdv~qHxmz%u6&Mil8tAz#(^vwj^euPNZJ8zYz!HFt?}pE$g-A48 z<~EKLsK1O=cG-yigSyp&1BK9Ji-Y_kK&gA%o{08`$v5 z?E0D(&xRtVrObBMqM5p?v`G)vTJiIvtlBPxISsr2`b5-HsU@d#0ka5Z*u8_PF35>4 zlk|g-_>))OAFKE{-&}0ufXAd4Z^NF|Hyy6rsm><)x!|RY72$r@vxDO9`FzvT-@B@X zFi|N^Jb7?Fo+?Hf4t{(kE`M~s?nN^Zv*Ej038MoxTitc;glELQB-Go(p53Xw7MU3) z$9Q8EQt+M|Pgwl~3V9JE(khjCEm`{5XTb3&nYH@D2!9Fkbkn=NPBVKf)O@DGgRR|dqYTGAm+;gcQV`FUPmax?;(FfwL&~VhTb431sGcwd!>6KCxxTcBaay?YBuvC&P^&S#lW`twIH z-(Ff83Y_z;e+rLL-{t=Cf9$bW4`WzvYBv7Ih+BV`ab@&!xSt|@lzj35nbig{FPqR8 zJPIQEd;<}#)24poPid^58Lp?yv>0^_PI4uYMwaNCD<#>J`1HTT6OJ@-+pt>m+ASl_ z?*bg4>ZrlNiWk2u_BqJopQMQ~7v3N}cy;|tFQKNIR~p-iIme56GOI}*OfB1}T-&a~ z?~~Jx1aCcl08b5)K%(NsQx|xotoaGuyVkHSFvZ3xWbtf`PjJ}tlk$s^ctDtzd!s1N zf7~jRuQqc?3DJ$^qvG712)@buOb$J*_ZhauM^eoyi#Y4Vh&2;;Pf_2p(;1ugswA&v zKFfgCSdoXQ*JW9@-xkU(ym)-P*r}k*IQRR|2dNwIOIU{i&$6@o@vJTNC8S`ai3HlS zjCEa{=8BIWL`1gaAG*EE?Mk*mT=!kt2giZ|P~`q`V7in~sqOqf9~kj^;WI6;|4p~9 zy@zyAuC|x->r2GH^Hy^PU%`jpz!^!8s#%){txgMBHD&0BVhn!5wj_!G9Q8dMx4`27 zQr+{eP&%VaR28{=Ia?D*62DfrUAHq<51%Wqwl{Gud%L-C$s$>nOasFur@Y26_g(`L zEb)}@H4uB%eUrFwWJ0}c1jo|EG{&*EL)z9YA4dkcY!$Rzp9ojW_{m!3^ls#X(#iQ+ zM6px)abhk){pMRoZ{CLX^^i8SA9N2qH{N_}=XX;zBSX){x-UWI;T>jCFq(Rj;3trC z&+zI}c~1X9&Eh5R76QiqxczC~BNb;(=t2ZKd49PQX);4L>Ke+>JI;^>gx;yITFeF< z({ZCOI=0&Lf!~NccaE8F{U6M{IPm}5IGYY$@!>}~ zvd{5*K9y~FNTG;hFLQjpaRJQ~PwyJVw?+ETv?n3dkDg7RuR9unrY~K_4m>@P3 zC?@(TBO^_h?K9<(I5JbXFDN$G6B$xH$Ix&m?7BSo!2zcSp4Wd%3xN#>hs~L)=JQ@J zc^#U7p7zS$yyx}-(BB!&&Y67I85xlJqr}pNSEz07L#?-*(W0*S-&s2P{pl1Y`O|!R|T*S}LmQ8ETj&()V z&IC3|kN>FVQY@%ZTDF&D$+Hg8U&~PViEs~uh9D8N0c#BSob`rm;vxy5^OF#J35Bz?!<>>wB4>9RCVL9dn@2=-eZ;Mm{J2kJHpcBRXuFKYmB#EWE!or3SBVZPSrpcSoJ;+;r z1th=xlqu#nBH#8AJPTYDlpPW!0-Koi$Ud8WH`yUa*IVa;2^=r2DrkRSZ8y=+;aq?eIAW@d(RU&Mh z()LcZ%c{z}*>E+;pn|=IDXV2M+vz`_DzzVd%L_n4cYfzF_;)8}oDTc3Ad1uI_|2D6 zJZQT+)*+HJA)8}~BGBc{)YgXzMI3ANJg1IJ;%Pd2M7HHOTURHlRF*dG{OyL3F5hB@ zmI=K+!Ji*o*C}~1xSo!A!tM>aZvcfYSsI#tc+K{Q>|A_*KI-IQ(?+DQ`<_|r6Gx0F zsC>4<4gRHG+Jx-xZKKDNUhKm!?Qp_=it*p@;n3EUlm(CnL;vCNga-&tRYFbyYx7IJ zH^9T2bCEc7Z($a6$XC+?)-~Z$8tLA#4I@X%sSaF9cNjD9IYj&Q#JQ1W^F$nfF2#vS za4cuv-!iKQeYV{&UH0NyHS1w0kN@UDVWjT+e0ure?$-JrV6PF>UY{2`YHp^l3b$M71x`T#7v`)~FWBoW!jw&ydh(-I>%?zFT!x~(bDu#-e`=U|Z z)XWbZZ2tN|>X1}K-1`xaS+rEEXzce2Le&(V?RWi0RHW8|HK_B%^NV3&hpa9&oLTG; z@mdjm85U%q%%b%FP+HI8{Er-K0vCe{M#j)aRVN_usQo;1_th2JVYNi(*Q$QjYKVZ| z)?Y+snaXcz6?qyIv70{NGI*W*%t zz&}lNs$U5nhD5J_dBl`-C~Ld|Q}BQEFybv|e}oP>nKleLvqQF-m>@FCC0jMy6=tLm z&ULwTtx1d(EbJ1sQBnFB`YsI9P_DIqgnaS^ksdn*ZkfP)Mm2}cW>riSB(wc$F4+6~ zBY{OH!1tKv-uM^wNmu34rcW^$B{5@KgS$D?{2M&4kt7P)@jqph#?tl$q_+5>^EXmCcXWAhXdkf`ZDCU(;IblQto1nC{ z>y&}*bQJx_pTd|=byyU*ZRm(!;Aj`g%f&%9 zRL;h^HRO47!jA`0?FHKf9Frarn&izfG^tdAI~ptQ;w*hcw?_Z*FrTJOEc&l{Qudus2W$U1z>*ANQi<8(Nt$)hzIIj0g*L38#)mhX0- zR$99@^Zs}`*fPa`q+9>H(-QH-&~anChf{!)z6llCXhJ+M!APwTZ-N2SWvPWDxq)o4#_{~#+>?>M>;^V|X&LXJ=?L5UmeN-E zF|%;- zuP(Pz>W?zqC3x?PjUKyWRskVs|M`GWZC-1zW3O4o=c&6 zW0+cPimvy#W<@l9iYe*vom2%_#0aFGkLmMZs(;Vqw;r)BNCGR`tk7S@10^}!NxW`t^V~q4|Al6>}lDPHmB%_68=QzLMEv`?7JB^L0c+lXo0V*yh{%YDxz$F{k%kv zz=|S0BsuFkr^z$=59Q95=UA3bigxg=sZBy$kGswJ(Wrp=jh)P%g4+QMmL<(wJe}>q zf6R|aJb&d6rTVRH8VVv2m+ndnf^S>O<7poR=}5yl2)a zYhyk!aWh5>o^khgW0a${?IFovQ#V`0zMV{CErwZM=uK#1O z*X*-#VZSgA%2FhSZ%%i0+!Y!rY-C7Vfs3`qMffGCkiTsKFW!c;*4v(iqjJ5EF*j~g z6d>Uf0+&`wv<)z zRUlHH>)n2W*+9%Y1`b{R_mQ$ss>j;x!1&u#c4STur{aivia6=5-HhZfRXTCPwbT6qD`=T*w{HBWR=laK&N0H{qyjho%p# zNjFdwY~g#Urkc$2F3U92{rb>}I_2QEmq=W)sSRu$fjTsFE$sL4fVB-lW2@X`V5{P8 zThdr!PMd_?hb(B{r^~7ww;eQHL)T`>Y7SI}C7ec*otdvK;~e7no6{Ul1Q4yh1jIT~ zY1=F*-1tP1YDxrw%rsFHl*@Bft2Q8d6KM$3UL}L%{omRAA*N_bdG(**EE4QqwzksG zE)|D-F%0+xq=oui1UZSy_|Kjjf%OXS;=&A;=hc;ZnJM&11*L;kllk9j6TNFKyiHpv zj@iC8Dop=7^5BKAf7VD9`eV#{xb|znm_lkR1+`?@QaH@ja96sImH}@4ClBJ4Q6H3s z>*liMnq*tf~7rH>_r)2)<4L&;3MJh}NE4%r4h36~+qoNMGtPZ?5O z;8G&)Wvc-Hp9}^daa>h#&G&IgUyp|RG#nD4C^!NiQ)Lc{=-vKE4Up*<-wgikn>lFs zNumGu`u#FP|NQzeWbbp^Kj%sv)YD?F4p0c18TPXF^`J`DxBshzLOv<~THt}=_~b@e zb&)e(4{tp}c$3L;8ReR;Nh_*9{b71!tS3Yi5t3bl=QWq?3DL{DS-Jg-MSVyA@z2mv z<}Ou`M<&+)%ZEq(uBku=SOT)c&DiZO&LmKV{LBz2!pVQo(tCzrFsrlkd8SFqE4w=Y zL)s{A^i6XyEuQD#{1vQBiKer`Lqqfk+*Au(Axb6Oj~F{07!)L)ong&d-1y;zpsY$Mw}r>J zCg!lZY4{VCf^N-1M2kJnfPY6aHL+hl;mHq`OpEBrw>?B*N&SVh=fw_4SqH^HNv{dbTlefqi0KuWHe`(@ARdp{cb5;*1VUm6B2N8`=AcQ#@XRS zU`im}P;1d&D?+XgK}@H|XN@7_gmY&PXN2M2I4QpEBs)N~yORXQa5C zOEQ2(tw8}9zCy!hg()5zC|by=i8>?ht`W`*zq5{D&NMP-6bzPM0$^Jbc*6hDvN085Xw|%*vE_uf&=(dT{QcYI*k7;aI#&hAKY4UnQzW} z2jSg|+Z3^{y=UGVAudJl2mcnOm0E{Lg2hdiC0p9DhMam-lfv}Ji7ye3VNWqVy7jPg z-d!SnTbkJ0P3;M}-ZGXu=TNtrkGB2Uo2n-ivF3&D;eR9`p79i-h@h69r$tPKM2FyI znHT&0Y*!3+DrSF`y><9Vu_j*-6Ek%m#&~t`7eXKxwQB|1h#S94l0Cy>1adnQUzRPmW7rt}oS7Mh2kJ`D8 zWYaXv%S)J;0MhpJerLsYJ$Ow{j(M0~@u$1lv~<67dD%2u^K;~gkuxbz*ydrT5%+wP zY0lzI52lMFnV$P|vg1g{heLtHz(y0jhipsd20M-iifH#$&c!JkJF=f}!I%-35=Pmc zU3%NfoFc${^>&`4CUy*`bw75z9eqdcE_F%`AsL2~woC0iA33H*lL8YRQxB8f7>u5Do)5#~FhDAO_3oxJMH$}&F0gsdCB8$OaS@RZa^Hg{}W zTk4cnnimidQE>J|cP}lU%lY}-iEg~xtz?q`z3^j+?5D(&BMIQA^N1t+X{~M;nf>-p zYD;mhW17va)9y)RR96UFEAAiJ_Ca~!Q;c3l8_u6(=wc`D01h-A+X*~_>N*oX64L%O z;2qi4v#p%7b2k#qmrZjv2`DiSBcr<}I}Q;G{JRU;|NYbaFE%mX-*zli!@Vu_5ZT)KXs{q>SuKPb;HirfvqzIP`rQN$h$CUD2iO?!0g`P;@2QL-1A&Csqewer>G zVa8PZKx(gP$`cc!VTF8vyr34!lt2APlNrsXfp(Mu{JN~?HCL2`6*{6uRijmf-X?N& zSeg;X;Mc)cwR!@?h8&N9V8Zd9#89!F=Lz_=y!t*@hiV@}_)0PXb_ExP<-fqHvB(jJ zq_@8Jq}tbsdijncX9zx`B-I&T$phY}m_Ed7=*dm_0RDGNnVlz?&DMMf%v@3RAcSV$ zyDjX0%t8I2Z00GNd6x(S2-jN3kx&UMz5L6u1CN$8bm||LaKLPLtlx>`b40A zMM>`AB{q|{fRCus(;<_$3I`Dzx=A}Yc0G?oNqj`64b2|!)LaQxHGuev8T)|}1)mG{ z*?!e~ub~iq_;n7);CIJ}G0n6#NH$xEn6%v_zwyPfEYLg!!Xa%I4;*q#buNZxul0^$ zSHy15_cte1Hw_`Ec}Isr2_EwI4Pyu>#^Z~u+3!2U>|7EV3ct2AUq_7?v7y_0nNb`? zNovXjm4AO|0DjdTE{8Zt$E)CeF&l3=N0s;qH|9^@JW`|DoP@4 zTE*4jV+fCW(4Sfvna{s=FS<@i%#)vOpj~B{gP8F{3Ad?+#PAW|P=rP7MsRYBdlxkB zOa`u>{+Lq)B2HD(bL9gyn1VrstNtEPV<1z%$_*eL=P#;EmJg&JNi!xv0oK>#1D&EI z4u0@Q;MA5J@D4Rf6|ZjceUR*YbHgFi4+z&@XqSfbp%A^WuY7rU#v28ypy#cAk4}Po zKm&xjg#_jOr6Cl9F(ywxdQK#JEteRlqODJRo$ZwV9HQ8W>hjH?= zqfFLAcJnCkNd_5=Dl^~Gqf@~qrkqkmd=sSe$A3di4CRy3a;~`Ma>+fSMYG8MA}GWh zX^37jO~)S34JHhIowZx~m5y!DuJS;AzMUtikSwDs>De@pk}%V?1{&{L;qV>&+JGLO zL$?=s=IEu+GC+1zJE1G>d%c-%AY@`utEjEy(r>$A|1Uy{vPPMMx@t~CS-BI)bS5)0QY0}(&PhOBG*6| zTrI7ELbO1(>p7dI&^D@UbZCTRtyprhTv7uUepObhmEch@s(oq%p{m^6^92~KOur9I z%t_-aJ(q(Ov5vOL9;W-H$j>5(qE^hkZfN}8&vhV=ze2r4E#*2wg%uA%l7~d z8E(`cv{Q{>vPcIYCAB}j7J8C`C@JIWY$~n=HgWLTAotYxgI{|YQ>#2-@=WD`1e#Mt zcn=1?g^gsYAY}U>BBKDYgX9SC;Rz8@@^zguFV1+Yj#;(_V4X)&7FaOnuRQsHK%oz? z>0CgvXw}|$`9Lomc#LRB0={y43Z~{H(^LlV(oDD{-`<5^rMTNnV#rmk5_^rg-1YGd za5e;~R?vK0lmz$@I9Oih+z3h|_;@p!eTk4&1DWx$p@oiVLake0QmpZla^bw0Ih{Kz zqbOyothJIm-=}D=_mI!)j83X>D<{w&bG)vot-BESdq%;@{q3>uYM6ISi*3L68#}G# zRb#tYhmU`5TOYn#>MJb8ig>l15^vg`66U37ORdF;_w;_n?!TN+watz|O@HvzuNNau zj}_mFWk#K~#Cz3PlATnF8;18!%RS!*i@H3}SjWbuk)s9giOdrzxJa2+4~F5J=vzLe zx2(uP$wtaB20h-`qjFaT&M}r1B$yFV(_vAyBqWe45gSW^A zLU_fil=C0^tgp-RB@b4wNlGM?+IyXVn^lUozuJ`r8JACPwkBAJ?sNB2gHj`SRiV{H z?p}K(D4%9bjJfX&{N081H&^ODE2ReEamQ1`c^7{-H=wescglYLo@L;skoOs2BtE6DnmW1p&o;=|!(qQ|Wl*4LUY6)S%&dh<6XZi( zug0pQl(1^{mf`9RSOVB`-}G5;^MNp@~<<2sZ-P?Gs_a#W{n4Vwuzr(*ue_H;gL0W~diNMRmQfB7z5X z+;2Ok<>VPyksFb1KqVGBv`wB=$(3rLJCJQ!K${f%^W4ABF35u@j%ok=R2U_GL$WtMS!vI?rCj}+YJ~b1&=X7e^^c-9AV<);VWwP?k*2R6EWp%zwnAoE z4wnGz*Pk^6a%9DKNSd|{kRt-H0W%l1sY(oFg?5w2!X{AgyJ5jDQQPK{9}Da+4mUt% zr`j8c>{>((mqfKsT&>i527G&P`3^vk;LOHKd-3!S8lH0yF0cft6#e~!flxxV3xIqK zkU6U5JzJCs=+|G=wuMW&jwsm+y2)^FVzSL=;Q^_H#}LBO9dwk{Z~A<*H26KjG(|31 zyQc6gq4Q)X6fGHo&t-yzjaFwiA6rIG77YaRI^OiHCHedV|0Ky zX!GaP9vyL_(3dGGu~#8{Gth4UkwtBQFE=p)wO=X|mDZb#*48frXNK1~HbNl=CkMj` zRr`xGKg*BM_j4zOTEPPCkuVgo*y@CwIvW3uOXBRhcm9vL3`?(AoGu@@ENb(K4Y}*C zqG?D_Wka>!KQrgn!@7KgCe}mtGVwmy7&GItP?c3fMf36P(5~}-RW}@6Kx9r`)qe=U zWeA~JY0qO1uL6uO>%#zOHLK0EyGyQtXw6eW>u>tpF!g+3TWRm*i1Z64Y`=6TQ8((vQKdagfY8Kv+USJ= z4{pLAUJlM=8UV`+*eT%zT&2HL4I~4k&WnnM`DCUF(q!Jb+AS^!5n|^Tq5Kwl1=h0X--AcI19n|Lq z#^t!<*`K9E@j4~A+x(_alW8hQqmr-;ZMuC2SXhOv7pNruVa*)XnyZz~8%1rLj?-YB z1_eFCpk_H2z#W0-VL_~z4c>w|lvKGyK*tV!4*m4%_pUuUzA3K2(^UH) zxx_Xj$V2n9ZUxh}0ujPf)-{2~>d@k4z=|8k&{xn@doNk1-XLQ7k~p=@I((oMNU5%f8~-!EtefDpO?7NAE$fw5>#!3{8( zhkArE0k3>K$ET>&-uqH5_JPJm-ol0h&h8DzST za-By|mG%_9eE`N-n)Ta7ZAFq>MWF8zYyiAfJvo{jOu+4<%q8qe)pZK{LRkJ zsY9N3e5R<41Q$M)OH#`mX!fIEax9O&#W)OXoxBT=7y;iHI9bo^1;AjnX)kYfvZ9b` zKjf@it{+Ba-}REc3?USQbluZo#SbA==XNy>AQ+QPz>U@3wq;yW;=Vk-Gr&GpAEM{b kT>eoHfKVLYH@p9rr#|r6*Uh2xz}K-OcE1)M`t90(0o@lr&Hw-a literal 85117 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%0DTHsbE~b$9NT6MC<(5(Yvp|^i-7&>9=`2Ucx1o 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 I7@vszU&Sj4@Bjb+ diff --git a/voidcrew/modules/cyberware/ware_legend.dm b/voidcrew/modules/cyberware/ware_legend.dm index bed5d4a5298..3b5acf17a59 100644 --- a/voidcrew/modules/cyberware/ware_legend.dm +++ b/voidcrew/modules/cyberware/ware_legend.dm @@ -90,12 +90,7 @@ var/window_alert = /atom/movable/screen/alert/status_effect/cyberware_cascade /// What the bearer reads on activation. var/online_message = "CASCADE ONLINE." - // When no other player is near, the world slows (ware_sandy_timestop.dm): - /// how long it freezes first, - var/dilation_freeze = 1 SECONDS - /// how long slow-mo runs after that, - var/dilation_time = 3 SECONDS - /// and how fast the world runs in slow-mo. + /// How fast the world runs through the window when no other player is near (ware_sandy_timestop.dm). var/dilation_scale = 0.2 /// How much faster than normal the bearer runs in the window. A surging mob in someone else's field runs at its /// multiplier over theirs. @@ -135,10 +130,29 @@ cooldown_time = 60 SECONDS click_to_activate = FALSE +/// Whether a press now would end a running window: a player's own press only. An NPC's AI never turns its surge off. +/datum/action/cooldown/cyberware/cascade_surge/proc/can_cut_short() + if(!isliving(owner)) + return FALSE + var/mob/living/surger = owner + return !!surger.ckey && !!surger.has_status_effect(/datum/status_effect/cyberware_cascade_window) + +/// The cooldown starts as the window opens, so pressing again to end the window early looks past it. +/datum/action/cooldown/cyberware/cascade_surge/IsAvailable(feedback = FALSE) + if(!can_cut_short()) + return ..() + var/cooldown_end = next_use_time + next_use_time = 0 + . = ..() + next_use_time = cooldown_end + /datum/action/cooldown/cyberware/cascade_surge/Activate(atom/target) if(!isliving(owner)) return FALSE var/mob/living/surger = owner + if(can_cut_short()) // the second press: back to real time, and the crash + surger.remove_status_effect(/datum/status_effect/cyberware_cascade_window) + return TRUE if(surger.has_status_effect(/datum/status_effect/cyberware_cascade_window) || surger.has_status_effect(/datum/status_effect/cyberware_cascade_crash)) surger.balloon_alert(surger, "lattice still cycling!") return FALSE @@ -198,8 +212,6 @@ online_message = lattice.online_message crash_duration = lattice.crash_duration crash_stamina = lattice.crash_stamina - dilation_freeze = lattice.dilation_freeze - dilation_time = lattice.dilation_time dilation_scale = lattice.dilation_scale time_multiplier = lattice.time_multiplier return ..() @@ -256,7 +268,7 @@ steps_taken++ if(steps_taken % afterimage_interval) // cheaper chrome leaves a sparser trail return - if(time_stop || dilated) // a surge that slowed the world keeps its green trail to the end of the window + if(time_stop) // green while the world is slowed stopped_afterimage(old_loc) return trail_hue = cyberware_time_trail_hue(owner, trail_hue) // ware_sandy_timestop.dm diff --git a/voidcrew/modules/cyberware/ware_line_sandy.dm b/voidcrew/modules/cyberware/ware_line_sandy.dm index ac92f7b4efb..6065c983024 100644 --- a/voidcrew/modules/cyberware/ware_line_sandy.dm +++ b/voidcrew/modules/cyberware/ware_line_sandy.dm @@ -52,8 +52,6 @@ afterimage_alpha = 120 window_alert = /atom/movable/screen/alert/status_effect/cyberware_cascade/aftermarket online_message = "REFLEX RIG ONLINE." - dilation_freeze = 0 - dilation_time = 2 SECONDS dilation_scale = 0.5 time_multiplier = 2 @@ -93,8 +91,6 @@ afterimage_alpha = 160 window_alert = /atom/movable/screen/alert/status_effect/cyberware_cascade/aftermarket online_message = "LATTICE ONLINE." - dilation_freeze = 0 - dilation_time = 3 SECONDS dilation_scale = 0.35 time_multiplier = 1 / 0.35 diff --git a/voidcrew/modules/cyberware/ware_sandy_senses.dm b/voidcrew/modules/cyberware/ware_sandy_senses.dm index f3875b85562..0c2cc49b8f8 100644 --- a/voidcrew/modules/cyberware/ware_sandy_senses.dm +++ b/voidcrew/modules/cyberware/ware_sandy_senses.dm @@ -6,21 +6,16 @@ * Sound: a sound from a tile inside the field reaches the user pitched down and slowed by the field's scale * (the `frequency` of the sound datum, set in playsound_local). The user's own sounds stay normal: anything * from their own tile, and anything their own action set off (`usr` is the user: their gun, their swings, - * their callbacks). During the freeze the world is a low, quiet rumble. The weather and ship-hum loops already - * playing on the user's channels are slowed in place with SOUND_UPDATE, and the loop's next segments go out + * their callbacks). The weather and ship-hum loops already playing on the user's channels are slowed in place with SOUND_UPDATE, and the loop's next segments go out * slowed too. * * Weather: area overlays are shared by everyone on a planet, so they are left alone. Each weathered area in * the user's view gets one override image, shown to the user's client only: a copy of the area's appearance - * with every weather overlay swapped for a pre-baked slowed (or, in the freeze, still) copy from + * with every weather overlay swapped for a pre-baked slowed copy from * sandy_weather.dmi. There is no particle weather to slow. */ #define SANDY_WEATHER_ICON 'voidcrew/modules/cyberware/icons/sandy_weather.dmi' -/// How fast world sounds play for the user while the world is frozen. -#define SANDY_FREEZE_SOUND_SPEED 0.15 -/// How loud world sounds are for the user while the world is frozen, as a share of normal. -#define SANDY_FREEZE_SOUND_VOLUME 0.25 /// Tiles past the edge of the user's view that still get slowed weather, so a step never shows a fast edge. #define SANDY_WEATHER_VIEW_MARGIN 1 @@ -32,7 +27,7 @@ /** * How fast `listener` hears a sound from `source`: 1 normally, the field's pace for a sound from inside the - * user's field, SANDY_FREEZE_SOUND_SPEED in the freeze. `actor` is whoever set the sound off (`usr`). + * user's field. `actor` is whoever set the sound off (`usr`). */ /proc/cyberware_heard_speed(mob/listener, turf/source, mob/actor) if(!isturf(source) || actor == listener) @@ -43,7 +38,7 @@ var/turf/here = get_turf(listener) if(!here || source == here || source.z != here.z || get_dist(source, here) > field.current_range) return 1 - return field.time_scale > 0 ? field.time_scale : SANDY_FREEZE_SOUND_SPEED + return field.time_scale /// Slows `heard` for `listener` if it comes from the slowed world. Called from playsound_local. /proc/cyberware_dilate_heard(mob/listener, turf/source, sound/heard, mob/actor) @@ -53,8 +48,6 @@ var/datum/proximity_monitor/advanced/timestop/sandevistan/field = cyberware_field_of_user(listener) field?.senses?.note_one_shot(heard) heard.frequency = cyberware_scaled_frequency(heard.frequency, speed) - if(speed == SANDY_FREEZE_SOUND_SPEED) - heard.volume *= SANDY_FREEZE_SOUND_VOLUME /// `frequency` (0 for as-is, a multiplier within -100..100, or a rate in Hz) played `speed` times as fast. /proc/cyberware_scaled_frequency(frequency, speed) @@ -86,7 +79,6 @@ if(length(GLOB.cyberware_time_fields) && ismob(parent)) var/datum/proximity_monitor/advanced/timestop/sandevistan/field = cyberware_field_of_user(parent) if(field?.senses) - sound_to_play.volume *= field.senses.loop_volume sound_to_play.frequency = field.senses.loop_speed SEND_SOUND(parent, sound_to_play) @@ -108,8 +100,6 @@ var/list/area_keys = list() /// How fast the user's loops play now. var/loop_speed = 1 - /// How loud the user's loops are now, as a share of normal. - var/loop_volume = 1 /// Channels of world sounds sent slowed and maybe still playing, to an unslowed copy of each, to retune them. var/list/one_shots = list() /// The same channels, to when each went out. @@ -124,18 +114,15 @@ /datum/sandy_senses/Destroy() if(user) UnregisterSignal(user, COMSIG_MOB_LOGOUT) - set_loops(1, 1) + set_loops(1) clear_images() field = null user = null return ..() -/// The field changed pace, or ticked: bring the loops and the weather in line. +/// The field started: bring the loops and the weather in line with its pace. /datum/sandy_senses/proc/update() - if(field.time_scale > 0) - set_loops(field.time_scale, 1) - else - set_loops(SANDY_FREEZE_SOUND_SPEED, SANDY_FREEZE_SOUND_VOLUME) + set_loops(field.time_scale) refresh_weather() /// Signal proc for [COMSIG_MOB_LOGOUT]: the client keeps its images when it leaves, so take them back. @@ -145,25 +132,24 @@ // ----- sound loops ----- -/// Plays the user's weather and ship-hum loops at `speed` and `volume` times normal, in place. -/datum/sandy_senses/proc/set_loops(speed, volume) - if(loop_speed == speed && loop_volume == volume) +/// Plays the user's weather and ship-hum loops at `speed` times normal, in place. +/datum/sandy_senses/proc/set_loops(speed) + if(loop_speed == speed) return loop_speed = speed - loop_volume = volume if(!user.client) return - retune_one_shots(speed, volume) + retune_one_shots(speed) for(var/datum/component/area_sound_manager/manager as anything in user.GetComponents(/datum/component/area_sound_manager)) var/datum/looping_sound/loop = manager.our_loop if(!loop?.direct || !loop.sandy_last_channel || !loop.sandy_last_file) continue - update_channel(loop.sandy_last_file, loop.sandy_last_channel, loop.sandy_last_volume * volume, speed) + update_channel(loop.sandy_last_file, loop.sandy_last_channel, loop.sandy_last_volume, speed) var/buzz = user.client.current_ambient_sound var/area/here = get_area(user) if(buzz && here?.ambient_buzz == buzz) var/buzz_pref = user.client.prefs.read_preference(/datum/preference/numeric/volume/sound_ship_ambience_volume) - update_channel(buzz, CHANNEL_BUZZ, here.ambient_buzz_vol * (buzz_pref / 100) * volume, speed, TRUE) + update_channel(buzz, CHANNEL_BUZZ, here.ambient_buzz_vol * (buzz_pref / 100), speed, TRUE) /// Notes a world sound about to go out slowed (`heard` still unslowed), to speed it back up later. /datum/sandy_senses/proc/note_one_shot(sound/heard) @@ -181,14 +167,14 @@ one_shot_times["[heard.channel]"] = world.time /// World sounds still playing from earlier in the effect take the new pace: full speed when it ends. -/datum/sandy_senses/proc/retune_one_shots(speed, volume) +/datum/sandy_senses/proc/retune_one_shots(speed) for(var/channel in one_shots.Copy()) if(world.time - one_shot_times[channel] > 15 SECONDS) // long finished one_shots -= channel one_shot_times -= channel continue var/sound/copy = one_shots[channel] - var/sound/update = sound(copy.file, 0, 0, copy.channel, copy.volume * volume) + var/sound/update = sound(copy.file, 0, 0, copy.channel, copy.volume) update.frequency = cyberware_scaled_frequency(copy.frequency, speed) // never 0: an update with 0 keeps the old pace update.x = copy.x update.y = copy.y @@ -274,10 +260,8 @@ area_keys.Cut() viewer = null -/// The sandy_weather.dmi suffix for a field running at `scale`: a still frame in the freeze, else the nearest baked pace. +/// The sandy_weather.dmi suffix for a field running at `scale`: the nearest baked pace. /proc/cyberware_weather_tag(scale) - if(scale <= 0) - return "still" if(scale <= 0.27) return "s20" if(scale <= 0.42) @@ -312,6 +296,4 @@ return slowed #undef SANDY_WEATHER_ICON -#undef SANDY_FREEZE_SOUND_SPEED -#undef SANDY_FREEZE_SOUND_VOLUME #undef SANDY_WEATHER_VIEW_MARGIN diff --git a/voidcrew/modules/cyberware/ware_sandy_timestop.dm b/voidcrew/modules/cyberware/ware_sandy_timestop.dm index e7c2c901b39..0ba88177385 100644 --- a/voidcrew/modules/cyberware/ware_sandy_timestop.dm +++ b/voidcrew/modules/cyberware/ware_sandy_timestop.dm @@ -2,81 +2,75 @@ * # The Cascade line's time dilation * * When a Cascade-line surge (ware_legend.dm, ware_line_sandy.dm) opens and no other player is within - * SANDY_STOP_WITNESS_RANGE tiles, the world around the user slows down: + * SANDY_STOP_WITNESS_RANGE tiles, the world around the user runs in slow motion for the whole window: * - * model freeze then slow-mo - * street - 50% for 2 s - * service - 35% for 3 s - * Cascade T4 1 s 20% for 3 s + * model window world runs at + * street 6 s 50% + * service 7 s 35% + * Cascade T4 8 s 20% * - * The rest of the window then runs as normal, then the crash. With another player near, the surge is the - * plain one. Only a player's surge does this (the user needs a ckey): an NPC criminal's lattice never does. + * Pressing the surge again ends the window early, slow-mo and all, and the crash follows as usual. With another + * player near, the surge is the plain one; if one comes in range mid-surge, the slow-mo ends there and the surge + * carries on without it. Only a player's surge does this (the user needs a ckey): an NPC criminal's lattice never does. * * Built on tg's time stop field (code/datums/proximity_monitor/fields/timestop.dm), hosted on the user so - * it follows them. Anything it touches stays slowed (or frozen) until the effect ends, even once the user - * walks off. The field re-checks for players every tick and ends at once if one comes in. + * it follows them. Anything it touches stays slowed until the effect ends, even once the user walks off. The + * field re-checks for players every tick and ends at once if one comes in. * - * No shortcuts on people. A frozen mob gets tg's own TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED and - * TRAIT_AI_PAUSED, never a fake state or a Stun. A slowed mob moves under a movespeed modifier, and every - * tick the field pushes its timing deadlines back by the time it lost (next_move, ability cooldowns, AI - * behaviour cooldowns and the blackboard deadlines listed in dilated_blackboard_keys(), ranged cooldowns). - * Frozen and slowed bodies take damage, wounds and bleeding through tg as normal. + * No shortcuts on people. A slowed mob moves under a movespeed modifier, and every tick the field pushes its + * timing deadlines back by the time it lost (next_move, ability cooldowns, AI behaviour cooldowns and the + * blackboard deadlines listed in dilated_blackboard_keys(), ranged cooldowns). Slowed bodies take damage, + * wounds and bleeding through tg as normal. * * Everything else in the field runs on the same slowed clock, through a few helpers below: * - cyberware_dilated_time() and cyberware_dilated_steps() turn a length of local time into real time, exact - * through the freeze, the slow-mo and the end. Hyperviolence gore uses them (flinches, death throes, blood - * paint, flying gibs), and so do a mech's weapon and melee cooldowns and a gun's fire delay. + * through the slow-mo and its end. Hyperviolence gore uses them (flinches, death throes, blood paint, flying + * gibs), and so do a mech's weapon and melee cooldowns and a gun's fire delay. * - cyberware_time_lost() stretches do_after for a slowed body (VOIDCREW EDIT in do_after). * - Burst shots wait on a burst clock the field pushes back (cyberware_burst_wait()), so a burst queued before * the slow-mo slows with it: guns (gun.dm) and basic mob ranged attacks (ranged_attacks.dm). - * - The field pushes back mech move cooldowns, turret fire cooldowns and lit grenade fuses (_grenade.dm), and - * shuts a mech down for the freeze. + * - The field pushes back mech move cooldowns, turret fire cooldowns and lit grenade fuses (_grenade.dm). * Things in hand stay held until the effect ends; gore and blood that merely stand in the field are slowed * while they are within its range. The user and anything the user carries are never slowed. + * Nothing is ever frozen: the field never sends tg's time stop signals, so a supermatter keeps running. * - * Bosses (a finisher user, or a boss-rank human NPC) are held for SANDY_STOP_BOSS_HOLD at most. + * Bosses (a finisher user, or a boss-rank human NPC) are slowed for SANDY_STOP_BOSS_HOLD at most. * * Each surge model speeds its bearer up by a time multiplier (the lattice's time_multiplier): street 2x, service * about 2.86x, Cascade 5x. A mob on its own surge (a bounty NPC's lattice) runs at its multiplier over the user's, - * at most SANDY_STOP_MAX_RELATIVE: a street NPC in a Cascade's field at 40%, an equal tier at 100% (left alone), - * a Cascade NPC in a street user's field at 200% (2.5x, capped). The user's freeze doesn't change that: a surging - * mob keeps its relative rate through it, so the freeze only stops those with no surge. Faster than the user runs - * the same machinery the other way (its deadlines, cooldowns, walk and gore come sooner). A surging mob is - * never tinted, at any rate. If it surges while already held, the field lets it go at once, drops every slow it - * was carrying (surge_free()) and takes it back at its relative rate. When its window closes the field takes it at - * the field's own pace, unless it is dead: a corpse freed by a surge is left as it fell. + * at most SANDY_STOP_MAX_RELATIVE, for as long as both windows are open: a street NPC in a Cascade's field at 40%, + * an equal tier at 100% (left alone), a Cascade NPC in a street user's field at 200% (2.5x, capped). Faster than + * the user runs the same machinery the other way (its deadlines, cooldowns, walk and gore come sooner). A surging + * mob is never tinted, at any rate. If it surges while already held, the field lets it go at once, drops every + * slow it was carrying (surge_free()) and takes it back at its relative rate. When its window closes the field + * takes it at the field's own pace, unless it is dead: a corpse freed by a surge is left as it fell. * * The user's reflex dodge (ware_pro_combat.dm) is off while the world is slowed: the speed gap already does that - * job. It comes back for the rest of the window. + * job. It comes back if the slow-mo ends before the window does. * * Look: affected things get a grey-green tint instead of tg's inversion. Floor items, blood decals and the * user are never touched, so they keep their colour, and so do the user's own shots. Every bullet in the field * flies at the field's pace, whoever fired it: the user's and a surging mob's crawl in the slow-mo like the rest - * (a faster surger fires sooner, but its bullets still crawl), untinted. Shots the user fires while the world is - * frozen fly a tile, hang there, and go on when it starts moving again. A shot leaving the field gets its speed back. + * (a faster surger fires sooner, but its bullets still crawl), untinted. A shot leaving the field gets its speed back. */ /// Another player within this many tiles of the user means no dilation. #define SANDY_STOP_WITNESS_RANGE 11 /// Radius of the field around the user (a widescreen view is 9 tiles to each side). #define SANDY_STOP_FIELD_RANGE 9 -/// How long a boss stays frozen or slowed before it shrugs the field off. +/// How long a boss stays slowed before it shrugs the field off. #define SANDY_STOP_BOSS_HOLD (1 SECONDS) -/// How far, in pixels, the user's own shots fly before they hang in a frozen world. -#define SANDY_STOP_SHOT_HANG 32 /// The fastest a surging mob runs in someone else's field, relative to normal speed. #define SANDY_STOP_MAX_RELATIVE 2 /// Relative rates this close to 1 count as the same speed: the mob is left alone. #define SANDY_STOP_SAME_RATE 0.01 -/// Trait source for everything the dilation applies. -#define SANDY_STOP_TRAIT "sandevistan_time_stop" /// Filter name of the user's edge glow. #define SANDY_STOP_OUTLINE "sandy_time_stop_outline" /// Every atom the dilation has hold of, to its field. GLOBAL_LIST_EMPTY(cyberware_time_dilated) -/// How fast time runs for `thing`: 1 normally, 0 frozen, in between in slow-mo, above 1 for a faster surger. +/// How fast time runs for `thing`: 1 normally, below 1 in slow-mo, above 1 for a faster surger. /proc/cyberware_time_scale(atom/thing) if(isnull(thing)) return 1 @@ -110,7 +104,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) return field return null -/// Whether `bearer` is slowing the world around them right now (the freeze or the slow-mo). +/// Whether `bearer` is slowing the world around them right now. /proc/cyberware_time_stopping(mob/living/bearer) for(var/datum/proximity_monitor/advanced/timestop/sandevistan/field as anything in GLOB.cyberware_time_fields) if(field.host == bearer) @@ -158,8 +152,8 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) return 170 return hue -/// How long, in real time, `time` deciseconds of `thing`'s own time take from now. Exact through the freeze, -/// the slow-mo and the end of the effect, as the field stands now. +/// How long, in real time, `time` deciseconds of `thing`'s own time take from now. Exact through the slow-mo +/// and the end of the effect, as the field stands now. /proc/cyberware_dilated_time(atom/thing, time) var/datum/proximity_monitor/advanced/timestop/sandevistan/field = cyberware_time_field(thing) if(!field) @@ -167,9 +161,9 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) return field.real_time_for(thing, time) /** - * Real-time lengths for a chain of animation steps `times` on `thing`: list(hold, step 1, step 2, ...), or null - * when it isn't slowed. `hold` is how long it sits still first (the freeze). A looping chain just plays at the - * field's pace. `tag` names the animation, so a held mob that breaks free (surge_free()) can drop it. + * Real-time lengths for a chain of animation steps `times` on `thing`: list(step 1, step 2, ...), or null when it + * isn't slowed. A looping chain just plays at the field's pace. `tag` names the animation, so a held mob that + * breaks free (surge_free()) can drop it. */ /proc/cyberware_dilated_steps(atom/thing, list/times, loop = 1, tag) var/datum/proximity_monitor/advanced/timestop/sandevistan/field = cyberware_time_field(thing) @@ -180,16 +174,11 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) var/list/real = list() if(loop != 1) var/scale = field.scale_for(thing) - if(scale <= 0) - scale = field.slow_scale - real += 0 for(var/time in times) - real += time / max(scale, 0.05) + real += time / scale return real - var/hold = field.real_time_for(thing, 0) - real += hold var/local = 0 - var/reached = hold + var/reached = 0 for(var/time in times) local += time var/at = field.real_time_for(thing, local) @@ -197,8 +186,8 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) reached = at return real -/// Time `thing` lost in the last `elapsed` deciseconds: all of it frozen, none of it outside a field. Negative for a -/// surger running faster than normal: it gained time. +/// Time `thing` lost in the last `elapsed` deciseconds: none of it outside a field. Negative for a surger running +/// faster than normal: it gained time. /proc/cyberware_time_lost(atom/thing, elapsed) var/datum/proximity_monitor/advanced/timestop/sandevistan/field = GLOB.cyberware_time_dilated[thing] if(!field || elapsed <= 0) @@ -220,8 +209,6 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) /proc/cyberware_burst_wait(atom/shooter, start, gap, index) if(!GLOB.cyberware_time_dilated[shooter]) return max(start + gap * index - world.time, 0) - if(!cyberware_time_scale(shooter)) - return world.tick_lag var/due = start + cyberware_time_lag(shooter) + gap * index return due - world.time >= world.tick_lag ? due - world.time : 0 @@ -317,19 +304,16 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) freezerange = SANDY_STOP_FIELD_RANGE /// Whose dilation this is. The field is hosted on them. var/mob/living/user - /// How long the world is frozen first. - var/freeze_time = 0 - /// How fast the world runs after that. + /// How fast the world runs. var/slow_scale = 1 /// The user's own time multiplier (their surge model's), for surging mobs' relative rates. var/user_multiplier = 1 -/// `immune_atoms` must be list(user). `time` is the whole effect; `freeze` of it is frozen, the rest at `scale`. -/// `multiplier` is the user's surge model's time multiplier. -/obj/effect/timestop/sandevistan/Initialize(mapload, radius, time, list/immune_atoms, start = TRUE, silent = TRUE, freeze = 0, scale = 1, multiplier = 1) +/// `immune_atoms` must be list(user). `time` is the whole effect, all of it at `scale`. `multiplier` is the user's +/// surge model's time multiplier. +/obj/effect/timestop/sandevistan/Initialize(mapload, radius, time, list/immune_atoms, start = TRUE, silent = TRUE, scale = 1, multiplier = 1) if(length(immune_atoms)) user = immune_atoms[1] - freeze_time = freeze slow_scale = scale user_multiplier = multiplier return ..(mapload, radius, time, immune_atoms, start, TRUE) @@ -343,11 +327,8 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) QDEL_IN(src, duration) if(QDELETED(user)) return - var/datum/proximity_monitor/advanced/timestop/sandevistan/field = new(user, freezerange, TRUE, immune, antimagic_flags, FALSE, freeze_time > 0 ? 0 : slow_scale, user_multiplier) + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = new(user, freezerange, TRUE, immune, antimagic_flags, FALSE, slow_scale, user_multiplier) field.stopper = src - field.slow_scale = slow_scale - if(freeze_time > 0) - field.thaw_at = world.time + freeze_time field.ends_at = world.time + duration chronofield = field @@ -358,12 +339,8 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) /datum/proximity_monitor/advanced/timestop/sandevistan /// The effect that owns this field. Deleting it ends the dilation. var/obj/effect/timestop/sandevistan/stopper - /// How fast time runs in here right now. 0 is frozen. - var/time_scale = 0 - /// The slow-mo scale the field drops to once the freeze ends. - var/slow_scale = 1 - /// world.time the freeze ends and slow-mo starts, or 0. - var/thaw_at = 0 + /// How fast time runs in here, below 1. + var/time_scale = 1 /// world.time of the last tick, for how much time each tick dilated. var/last_tick = 0 /// The field's own clock. A timer, not SSfastprocess, which sleeps outside RUNLEVEL_GAME. @@ -382,7 +359,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) var/list/released = list() /// world.time the effect ends, when time runs normally again. var/ends_at = INFINITY - /// Mechs held, to whether the field shut them down for the freeze. + /// Mechs held. Their move cooldown runs slow. var/list/held_mechs = list() /// Turrets held. Their fire cooldown runs slow. var/list/held_turrets = list() @@ -399,8 +376,8 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) /// Surging mobs held at their relative rate, to that rate. Never 0, never tinted. Their shots fly at the field's pace. var/list/own_scales = list() -/datum/proximity_monitor/advanced/timestop/sandevistan/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, list/immune, antimagic_flags, channelled, start_scale = 0, _user_multiplier = 1) - time_scale = start_scale +/datum/proximity_monitor/advanced/timestop/sandevistan/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, list/immune, antimagic_flags, channelled, _time_scale = 1, _user_multiplier = 1) + time_scale = _time_scale > 0 ? _time_scale : 1 user_multiplier = _user_multiplier > 0 ? _user_multiplier : 1 last_tick = world.time GLOB.cyberware_time_fields += src @@ -426,35 +403,20 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) own_scales.Cut() released.Cut() -/// How long, in real time from now, `local` deciseconds of `thing`'s own time take: the rest of the freeze, -/// then the slow-mo until the effect (or a boss's hold) ends, then real time. +/// How long, in real time from now, `local` deciseconds of `thing`'s own time take: its rate in the field until +/// the effect (or a boss's hold) ends, then real time. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/real_time_for(atom/thing, local) var/now = world.time var/ends = ends_at if(boss_holds[thing]) ends = min(ends, boss_holds[thing]) - if(ends <= now) - return local - var/own = own_scales[thing] - if(own) // a surger at its own steady rate, freeze or not - var/own_span = (ends - now) * own - if(local <= own_span) - return local / own - return ends - now + local - own_span - if(surging[thing]) + var/rate = scale_for(thing) + if(ends <= now || rate == 1) return local - var/at = now - var/scale = time_scale - if(scale <= 0) - at = thaw_at ? min(thaw_at, ends) : ends - scale = slow_scale - if(at < ends && scale > 0) - var/span = (ends - at) * scale - if(local <= span) - return at - now + local / scale - local -= span - at = ends - return at - now + local + var/span = (ends - now) * rate + if(local <= span) + return local / rate + return ends - now + local - span /// How fast time runs for `thing` in this field: a surger's own relative rate, 1 for a surger left alone, else the field's pace. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/scale_for(atom/thing) @@ -488,8 +450,6 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) else if(isprojectile(A)) var/obj/projectile/shot = A if(shot.firer && immune[shot.firer]) - if(time_scale <= 0) // hang_shot takes these - return FALSE user_shots[shot] = TRUE tint = FALSE escape_the_negative_zone(shot) // in case it was tinted before it had a firer @@ -501,8 +461,6 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) else if(istype(A, /obj/machinery/porta_turret)) frozen_structures += A held_turrets[A] = TRUE - if(time_scale <= 0) - hold_turret_fire(A) else if((ismachinery(A) && !istype(A, /obj/machinery/light)) || isstructure(A)) frozen_structures += A // looks only else if(cyberware_lit_grenade(A)) @@ -512,14 +470,11 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) return FALSE if(A.throwing) held_throws[A] = TRUE - if(time_scale <= 0) - A.throwing.paused = TRUE GLOB.cyberware_time_dilated[A] = src frozen_things[A] = TRUE RegisterSignal(A, COMSIG_QDELETING, PROC_REF(held_gone), override = TRUE) if(tint) into_the_negative_zone(A) - SEND_SIGNAL(A, COMSIG_ATOM_TIMESTOP_FREEZE, src) return TRUE /datum/proximity_monitor/advanced/timestop/sandevistan/unfreeze_all() @@ -534,14 +489,8 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) let_go_mob(A) if(held_shots[A]) let_go_shot(A) - if(held_throws[A]) - var/datum/thrownthing/throw_datum = A.throwing - if(throw_datum) - throw_datum.paused = FALSE - held_throws -= A - if(!isnull(held_mechs[A])) - start_mech(A) - held_mechs -= A + held_throws -= A + held_mechs -= A held_turrets -= A held_grenades -= A frozen_structures -= A @@ -552,44 +501,21 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) escape_the_negative_zone(A) frozen_things -= A GLOB.cyberware_time_dilated -= A - SEND_SIGNAL(A, COMSIG_ATOM_TIMESTOP_UNFREEZE, src) // ----- mobs ----- -/// Takes hold of a mob: frozen or slowed (or sped up, for a faster surger), whichever its rate is. +/// Takes hold of a mob: slowed (or sped up, for a faster surger), whichever its rate is. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/hold_mob(mob/living/victim) held_mobs[victim] = TRUE - var/rate = scale_for(victim) - if(rate <= 0) - freeze_mob(victim) - else - slow_mob(victim) - if(rate < 1 && cyberware_time_stop_boss(victim)) + slow_mob(victim) + if(scale_for(victim) < 1 && cyberware_time_stop_boss(victim)) boss_holds[victim] = world.time + SANDY_STOP_BOSS_HOLD /datum/proximity_monitor/advanced/timestop/sandevistan/proc/let_go_mob(mob/living/victim) - unfreeze_mob(victim) unslow_mob(victim) held_mobs -= victim boss_holds -= victim -/datum/proximity_monitor/advanced/timestop/sandevistan/freeze_mob(mob/living/victim) - frozen_mobs[victim] = TRUE - victim.add_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED, TRAIT_AI_PAUSED, TRAIT_MUTE, TRAIT_EMOTEMUTE), SANDY_STOP_TRAIT) - GLOB.move_manager.stop_looping(victim) - if(isanimal(victim)) // the old simple mob AI doesn't read TRAIT_AI_PAUSED - var/mob/living/simple_animal/animal_victim = victim - animal_victim.toggle_ai(AI_OFF) - -/datum/proximity_monitor/advanced/timestop/sandevistan/unfreeze_mob(mob/living/victim) - if(!frozen_mobs[victim]) - return - frozen_mobs -= victim - victim.remove_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED, TRAIT_AI_PAUSED, TRAIT_MUTE, TRAIT_EMOTEMUTE), SANDY_STOP_TRAIT) - if(isanimal(victim)) - var/mob/living/simple_animal/animal_victim = victim - animal_victim.toggle_ai(initial(animal_victim.AIStatus)) - /// Paces a mob's walk to its rate in the field: slower, or faster for a faster surger. Its timers move with it every tick(). /datum/proximity_monitor/advanced/timestop/sandevistan/proc/slow_mob(mob/living/victim) var/rate = scale_for(victim) @@ -641,7 +567,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) /** * `surger` runs its own surge at `multiplier`: at the same rate as the user it is left alone; otherwise the field - * holds it at its relative rate, slower or faster, untinted. The user's freeze doesn't touch it. + * holds it at its relative rate, slower or faster, untinted. */ /datum/proximity_monitor/advanced/timestop/sandevistan/proc/take_surger(mob/living/surger, multiplier) surging[surger] = TRUE @@ -653,7 +579,6 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) hold_mob(surger) GLOB.cyberware_time_dilated[surger] = src frozen_things[surger] = TRUE - SEND_SIGNAL(surger, COMSIG_ATOM_TIMESTOP_FREEZE, src) /** * `surger` opened its own surge at `multiplier`: if the field holds it, let it go now and drop every slow it @@ -712,29 +637,10 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) /// Takes hold of a mech and whoever is driving it. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/hold_mech(obj/vehicle/sealed/mecha/mech) frozen_structures += mech - held_mechs[mech] = FALSE - if(time_scale <= 0) - stop_mech(mech) + held_mechs[mech] = TRUE for(var/mob/living/pilot in mech.occupants) freeze_atom(pilot) -/// The freeze shuts a mech down, as tg's own time stop does. -/datum/proximity_monitor/advanced/timestop/sandevistan/proc/stop_mech(obj/vehicle/sealed/mecha/mech) - if(held_mechs[mech] || mech.completely_disabled) - return - mech.completely_disabled = TRUE - held_mechs[mech] = TRUE - -/datum/proximity_monitor/advanced/timestop/sandevistan/proc/start_mech(obj/vehicle/sealed/mecha/mech) - if(!held_mechs[mech]) - return - mech.completely_disabled = FALSE - held_mechs[mech] = FALSE - -/// A frozen turret's cooldown stays a little ahead of the clock, so it can't fire until time moves. -/datum/proximity_monitor/advanced/timestop/sandevistan/proc/hold_turret_fire(obj/machinery/porta_turret/turret) - turret.last_fired = max(turret.last_fired, world.time + 0.3 SECONDS - turret.shot_delay) - /// Pushes back the clocks of held machines and grenades by `push` deciseconds. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/push_machine_deadlines(push) var/now = world.time @@ -744,8 +650,6 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) for(var/obj/machinery/porta_turret/turret as anything in held_turrets) if(turret.last_fired + turret.shot_delay > now) turret.last_fired += push - if(time_scale <= 0) - hold_turret_fire(turret) for(var/obj/item/grenade/bomb as anything in held_grenades) if(bomb.cyberware_fuse_at > now) bomb.cyberware_fuse_at += push @@ -754,50 +658,29 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) /datum/proximity_monitor/advanced/timestop/sandevistan/proc/hold_shot(obj/projectile/shot) held_shots[shot] = shot.speed - var/rate = scale_for(shot) - if(rate <= 0) - shot.paused = TRUE - else - shot.speed = held_shots[shot] * rate + shot.speed = held_shots[shot] * scale_for(shot) /datum/proximity_monitor/advanced/timestop/sandevistan/proc/let_go_shot(obj/projectile/shot) shot.speed = held_shots[shot] - shot.paused = FALSE held_shots -= shot /// Signal proc for [COMSIG_PROJECTILE_FIRER_BEFORE_FIRE] on the user: watch the shot leave the barrel. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/on_user_fire(datum/source, obj/projectile/shot, atom/fired_from, atom/original) SIGNAL_HANDLER - RegisterSignal(shot, COMSIG_PROJECTILE_FIRE, PROC_REF(hang_shot)) + RegisterSignal(shot, COMSIG_PROJECTILE_FIRE, PROC_REF(take_user_shot)) -/// Signal proc for [COMSIG_PROJECTILE_FIRE]: the user's shot keeps its colour but flies at the field's pace like -/// every bullet in it. In a frozen world it flies a tile, then hangs; in the slow-mo it crawls with the rest. -/datum/proximity_monitor/advanced/timestop/sandevistan/proc/hang_shot(obj/projectile/shot) +/// Signal proc for [COMSIG_PROJECTILE_FIRE]: the user's shot keeps its colour but crawls at the field's pace like +/// every bullet in it. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/take_user_shot(obj/projectile/shot) SIGNAL_HANDLER UnregisterSignal(shot, COMSIG_PROJECTILE_FIRE) if(QDELETED(shot) || shot.deletion_queued) return - if(time_scale > 0) - user_shots[shot] = TRUE - escape_the_negative_zone(shot) // in case it was tinted before it had a firer - if(!held_shots[shot]) - hold_shot(shot) - GLOB.cyberware_time_dilated[shot] = src - frozen_things[shot] = TRUE - RegisterSignal(shot, COMSIG_QDELETING, PROC_REF(held_gone), override = TRUE) - return - // A hitscan beam takes its first step itself once paused (process_hitscan) and stops there. - if(!shot.hitscan) - shot.paused = FALSE // it may have been caught before it had a firer - shot.process_movement(SANDY_STOP_SHOT_HANG) - if(QDELETED(shot) || shot.deletion_queued) - return user_shots[shot] = TRUE escape_the_negative_zone(shot) // in case it was tinted before it had a firer if(held_shots[shot]) - shot.paused = TRUE - else - hold_shot(shot) + return + hold_shot(shot) GLOB.cyberware_time_dilated[shot] = src frozen_things[shot] = TRUE RegisterSignal(shot, COMSIG_QDELETING, PROC_REF(held_gone), override = TRUE) @@ -808,7 +691,6 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) held_shots -= thing user_shots -= thing held_mobs -= thing - frozen_mobs -= thing held_throws -= thing held_mechs -= thing held_turrets -= thing @@ -823,36 +705,10 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) // ----- the clock ----- -/// Moves the whole field to a new pace: frozen things start moving, slowed things change speed. -/datum/proximity_monitor/advanced/timestop/sandevistan/proc/set_time_scale(new_scale) - time_scale = new_scale - for(var/mob/living/victim as anything in held_mobs) - if(own_scales[victim]) // a surger keeps its own rate - continue - unfreeze_mob(victim) - if(time_scale <= 0) - unslow_mob(victim) - freeze_mob(victim) - else - slow_mob(victim) - for(var/obj/projectile/shot as anything in held_shots) - shot.paused = time_scale <= 0 - if(time_scale > 0) - shot.speed = held_shots[shot] * time_scale - for(var/atom/movable/thing as anything in held_throws) - if(thing.throwing) - thing.throwing.paused = time_scale <= 0 - for(var/obj/vehicle/sealed/mecha/mech as anything in held_mechs) - if(time_scale <= 0) - stop_mech(mech) - else - start_mech(mech) - senses?.update() - /datum/proximity_monitor/advanced/timestop/sandevistan/process() return PROCESS_KILL -/// Every 0.2 s: end if a player came in, push back the timers of everything held, let bosses go, end the freeze. +/// Every 0.2 s: end if a player came in, push back the timers of everything held, let bosses go. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/tick() if(QDELETED(host) || cyberware_time_stop_witnessed(host)) if(!QDELETED(stopper)) @@ -868,7 +724,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) if(push > 0) for(var/atom/movable/thing as anything in held_throws) var/datum/thrownthing/throw_datum = thing.throwing - if(throw_datum && !throw_datum.paused) + if(throw_datum) throw_datum.start_time += push push_machine_deadlines(push) check_surgers() @@ -877,9 +733,6 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) continue unfreeze_atom(boss) released[boss] = TRUE - if(thaw_at && world.time >= thaw_at) - thaw_at = 0 - set_time_scale(slow_scale) senses?.refresh_weather() /datum/proximity_monitor/advanced/timestop/sandevistan/into_the_negative_zone(atom/A) @@ -890,40 +743,35 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) // ========================================================================= /datum/status_effect/cyberware_cascade_window - /// How long the world is frozen if nobody else is near when the window opens. Copied from the lattice. - var/dilation_freeze = 1 SECONDS - /// How long slow-mo runs after the freeze. - var/dilation_time = 3 SECONDS - /// How fast the world runs in slow-mo. + /// How fast the world runs in slow-mo, if nobody else is near when the window opens. Copied from the lattice. var/dilation_scale = 0.2 /// How much faster than normal the bearer runs: sets surging mobs' relative rates in each other's fields. var/time_multiplier = 5 /// The running dilation, if any. var/obj/effect/timestop/sandevistan/time_stop - /// Whether this window slowed the world. Its trail stays green after the dilation ends. - var/dilated = FALSE -/// Slows the world if the bearer is a player and nobody else is near. Called as the window opens. +/// Slows the world for the whole window if the bearer is a player and nobody else is near. Called as the window +/// opens, while `duration` is still its length. /datum/status_effect/cyberware_cascade_window/proc/try_time_stop() cyberware_surge_frees(owner, time_multiplier) // anyone's surge runs at its own rate in someone else's field - if(dilation_freeze + dilation_time <= 0 || !owner.ckey || time_stop) + if(dilation_scale <= 0 || dilation_scale >= 1 || duration <= 0 || !owner.ckey || time_stop) return FALSE if(cyberware_time_stop_witnessed(owner)) return FALSE var/turf/here = get_turf(owner) if(!here) return FALSE - time_stop = new(here, SANDY_STOP_FIELD_RANGE, dilation_freeze + dilation_time, list(owner), TRUE, TRUE, dilation_freeze, dilation_scale, time_multiplier) + time_stop = new(here, SANDY_STOP_FIELD_RANGE, duration, list(owner), TRUE, TRUE, dilation_scale, time_multiplier) if(QDELETED(time_stop)) time_stop = null return FALSE RegisterSignal(time_stop, COMSIG_QDELETING, PROC_REF(time_stop_over)) - dilated = TRUE owner.remove_client_colour(id) // the user sees the slowed world as it is owner.add_filter(SANDY_STOP_OUTLINE, 2, outline_filter(1, "#5AFF96D2")) return TRUE -/// Signal proc for [COMSIG_QDELETING] on the dilation: time runs normally and the rest of the window carries on. +/// Signal proc for [COMSIG_QDELETING] on the dilation (a player came near): time runs normally and the rest of the +/// window carries on. /datum/status_effect/cyberware_cascade_window/proc/time_stop_over(datum/source) SIGNAL_HANDLER time_stop = null @@ -1004,7 +852,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) if(!cyberware_fuse_at) return var/wait = cyberware_fuse_at + cyberware_time_lag(src) - world.time - if(wait >= world.tick_lag || (GLOB.cyberware_time_dilated[src] && !cyberware_time_scale(src))) + if(wait >= world.tick_lag) addtimer(CALLBACK(src, PROC_REF(cyberware_fuse_done)), max(wait, world.tick_lag), TIMER_DELETE_ME) return cyberware_fuse_at = 0 @@ -1013,8 +861,6 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) #undef SANDY_STOP_WITNESS_RANGE #undef SANDY_STOP_FIELD_RANGE #undef SANDY_STOP_BOSS_HOLD -#undef SANDY_STOP_SHOT_HANG #undef SANDY_STOP_MAX_RELATIVE #undef SANDY_STOP_SAME_RATE -#undef SANDY_STOP_TRAIT #undef SANDY_STOP_OUTLINE diff --git a/voidcrew/modules/hyperviolence/_core.dm b/voidcrew/modules/hyperviolence/_core.dm index f28b6a2c84a..02245a4fddf 100644 --- a/voidcrew/modules/hyperviolence/_core.dm +++ b/voidcrew/modules/hyperviolence/_core.dm @@ -37,11 +37,11 @@ var/matrix/fallen = matrix(transform) var/matrix/upright = matrix(transform) upright.Turn(-angle) - // In a Sandevistan field (cyberware/ware_sandy_timestop.dm) it holds through the freeze and runs slow. - var/list/real = cyberware_dilated_steps(src, list(HV_DEATH_THROES_TIME, UPDATE_TRANSFORM_ANIMATION_TIME)) || list(0, HV_DEATH_THROES_TIME, UPDATE_TRANSFORM_ANIMATION_TIME) + // In a Sandevistan field (cyberware/ware_sandy_timestop.dm) it runs slow. + var/list/real = cyberware_dilated_steps(src, list(HV_DEATH_THROES_TIME, UPDATE_TRANSFORM_ANIMATION_TIME)) || list(HV_DEATH_THROES_TIME, UPDATE_TRANSFORM_ANIMATION_TIME) animate(src, transform = upright, time = 0) - animate(transform = upright, time = real[1] + real[2]) - animate(transform = fallen, time = real[3], easing = EASE_IN|EASE_OUT) + animate(transform = upright, time = real[1]) + animate(transform = fallen, time = real[2], easing = EASE_IN|EASE_OUT) // The shudder: sideways offsets that die down, each step relative to the last, ending where it began. var/static/list/shudder = list(3, -3, 2, -2, 2, -1, 1, 0) var/step_time = HV_DEATH_THROES_TIME / length(shudder) @@ -49,14 +49,12 @@ for(var/i in 1 to length(shudder)) shudder_times += step_time var/list/shudder_real = cyberware_dilated_steps(src, shudder_times) - if(shudder_real && shudder_real[1] > 0) - animate(src, pixel_w = 0, time = shudder_real[1], flags = ANIMATION_RELATIVE|ANIMATION_PARALLEL) var/offset = 0 for(var/i in 1 to length(shudder)) var/shift = shudder[i] - offset offset = shudder[i] - var/time = shudder_real ? shudder_real[i + 1] : step_time - if(i == 1 && !(shudder_real && shudder_real[1] > 0)) + var/time = shudder_real ? shudder_real[i] : step_time + if(i == 1) animate(src, pixel_w = shift, time = time, flags = ANIMATION_RELATIVE|ANIMATION_PARALLEL) else animate(pixel_w = shift, time = time, flags = ANIMATION_RELATIVE) diff --git a/voidcrew/modules/hyperviolence/visuals/_motion_core.dm b/voidcrew/modules/hyperviolence/visuals/_motion_core.dm index 1acccda933b..a597b647805 100644 --- a/voidcrew/modules/hyperviolence/visuals/_motion_core.dm +++ b/voidcrew/modules/hyperviolence/visuals/_motion_core.dm @@ -202,25 +202,15 @@ /atom/proc/hv_vis_play(tag, list/steps, loop = 1) if(!length(steps)) return - // In a Sandevistan field (cyberware/ware_sandy_timestop.dm) it plays at the field's pace, and holds still - // through the freeze once any jump to its first pose is done. + // In a Sandevistan field (cyberware/ware_sandy_timestop.dm) it plays at the field's pace. var/list/times = list() for(var/list/step as anything in steps) times += step[4] var/list/real = cyberware_dilated_steps(src, times, loop, tag) - var/hold = real ? real[1] : 0 var/started = FALSE for(var/i in 1 to length(steps)) var/list/step = steps[i] - if(hold > 0 && step[4] > 0) - var/list/still = i > 1 ? steps[i - 1] : list(pixel_w, pixel_z, transform) - if(!started) - animate(src, pixel_w = still[1], pixel_z = still[2], transform = still[3], time = hold, loop = loop, tag = tag) - started = TRUE - else - animate(pixel_w = still[1], pixel_z = still[2], transform = still[3], time = hold) - hold = 0 - var/time = real ? real[i + 1] : step[4] + var/time = real ? real[i] : step[4] if(!started) animate(src, pixel_w = step[1], pixel_z = step[2], transform = step[3], time = time, easing = step[5], loop = loop, tag = tag) started = TRUE @@ -236,24 +226,15 @@ /atom/proc/hv_vis_play_over(list/steps) if(!length(steps)) return - // Slowed and held like hv_vis_play() in a Sandevistan field. + // Slowed like hv_vis_play() in a Sandevistan field. var/list/times = list() for(var/list/step as anything in steps) times += step[3] var/list/real = cyberware_dilated_steps(src, times) - var/hold = real ? real[1] : 0 var/started = FALSE for(var/i in 1 to length(steps)) var/list/step = steps[i] - if(hold > 0 && step[3] > 0) - var/list/still = i > 1 ? steps[i - 1] : list(pixel_y, transform) - if(!started) - animate(src, pixel_y = still[1], transform = still[2], time = hold) - started = TRUE - else - animate(pixel_y = still[1], transform = still[2], time = hold) - hold = 0 - var/time = real ? real[i + 1] : step[3] + var/time = real ? real[i] : step[3] if(!started) animate(src, pixel_y = step[1], transform = step[2], time = time, easing = step[4]) started = TRUE @@ -324,10 +305,10 @@ var/flash = get_filter(HV_VIS_FLASH_FILTER) if(!flash) return - var/list/real = cyberware_dilated_steps(src, list(0.5, HV_VIS_HITSTOP_TIME, 1)) || list(0, 0.5, HV_VIS_HITSTOP_TIME, 1) - animate(flash, color = list(0.6,0,0, 0,0.6,0, 0,0,0.6, 0.4,0.4,0.4), time = real[1] + real[2], easing = LINEAR_EASING) - animate(color = list(0.6,0,0, 0,0.6,0, 0,0,0.6, 0.4,0.4,0.4), time = real[3], easing = LINEAR_EASING) - animate(color = list(1,0,0, 0,1,0, 0,0,1, 0,0,0), time = real[4], easing = LINEAR_EASING) + var/list/real = cyberware_dilated_steps(src, list(0.5, HV_VIS_HITSTOP_TIME, 1)) || list(0.5, HV_VIS_HITSTOP_TIME, 1) + animate(flash, color = list(0.6,0,0, 0,0.6,0, 0,0,0.6, 0.4,0.4,0.4), time = real[1], easing = LINEAR_EASING) + animate(color = list(0.6,0,0, 0,0.6,0, 0,0,0.6, 0.4,0.4,0.4), time = real[2], easing = LINEAR_EASING) + animate(color = list(1,0,0, 0,1,0, 0,0,1, 0,0,0), time = real[3], easing = LINEAR_EASING) addtimer(CALLBACK(src, TYPE_PROC_REF(/datum, remove_filter), HV_VIS_FLASH_FILTER), cyberware_dilated_time(src, 0.5 SECONDS), TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_DELETE_ME) /// The attacker's side of a hit-stop: the blow sticks for a few frames. A melee attacker stays leaned diff --git a/voidcrew/modules/hyperviolence/visuals/blood_motion.dm b/voidcrew/modules/hyperviolence/visuals/blood_motion.dm index 22aa43f7bdd..9d07adf8be9 100644 --- a/voidcrew/modules/hyperviolence/visuals/blood_motion.dm +++ b/voidcrew/modules/hyperviolence/visuals/blood_motion.dm @@ -28,14 +28,14 @@ GLOBAL_VAR(hv_vis_spray_origin) /obj/effect/decal/cleanable/blood/proc/hv_vis_paint(list/keys) var/reached = world.time var/list/timed = list() - // In a Sandevistan field (cyberware/ware_sandy_timestop.dm) the paint waits out the freeze and runs slow. + // In a Sandevistan field (cyberware/ware_sandy_timestop.dm) the paint runs slow. var/list/times = list() for(var/list/key as anything in keys) times += key[3] var/list/real = cyberware_dilated_steps(src, times) for(var/i in 1 to length(keys)) var/list/key = keys[i] - reached += real ? real[i + 1] + (i == 1 ? real[1] : 0) : key[3] + reached += real ? real[i] : key[3] timed += list(list(key[1], key[2], reached, key[4])) hv_vis_painting = timed hv_vis_paint_on() diff --git a/voidcrew/modules/hyperviolence/visuals/death_falls.dm b/voidcrew/modules/hyperviolence/visuals/death_falls.dm index c531b78d7ed..082a1cc1a02 100644 --- a/voidcrew/modules/hyperviolence/visuals/death_falls.dm +++ b/voidcrew/modules/hyperviolence/visuals/death_falls.dm @@ -125,7 +125,7 @@ hv_vis_play_over(hv_vis_hold_then_fall_steps(HV_VIS_HITSTOP_TIME, 2)) if(hv_vis_hit_damage >= HV_VIS_FULL_SLIDE_DAMAGE && hv_vis_can_slide_tile(hit_dir)) // Next tick: the blow that killed it may still be resolving (a shotgun's other pellets). - addtimer(CALLBACK(src, PROC_REF(hv_vis_slide_tile), hit_dir), cyberware_dilated_time(src, 1), TIMER_DELETE_ME) // waits out a Sandevistan freeze + addtimer(CALLBACK(src, PROC_REF(hv_vis_slide_tile), hit_dir), cyberware_dilated_time(src, 1), TIMER_DELETE_ME) // slower in a Sandevistan field return hv_vis_slide_in_tile(hit_dir, hv_vis_hit_damage, big ? HV_VIS_HITSTOP_TIME : 0) hv_vis_twitches(rand(3, 5), big ? 6 : 4) diff --git a/voidcrew/modules/hyperviolence/visuals/execution_motion.dm b/voidcrew/modules/hyperviolence/visuals/execution_motion.dm index fe3f5474b4a..664754db19f 100644 --- a/voidcrew/modules/hyperviolence/visuals/execution_motion.dm +++ b/voidcrew/modules/hyperviolence/visuals/execution_motion.dm @@ -69,10 +69,10 @@ var/base_w = pixel_w var/base_z = pixel_z // Slower in a Sandevistan field (cyberware/ware_sandy_timestop.dm). - var/list/real = cyberware_dilated_steps(src, list(0.5, 1, 0.5), loops) || list(0, 0.5, 1, 0.5) - animate(src, pixel_w = base_w + strength, pixel_z = base_z, transform = base, time = real[1] + real[2], loop = loops, tag = HV_VIS_TAG_TREMBLE) - animate(pixel_w = base_w - strength, pixel_z = base_z, transform = base, time = real[3]) - animate(pixel_w = base_w, pixel_z = base_z, transform = base, time = real[4]) + var/list/real = cyberware_dilated_steps(src, list(0.5, 1, 0.5), loops) || list(0.5, 1, 0.5) + animate(src, pixel_w = base_w + strength, pixel_z = base_z, transform = base, time = real[1], loop = loops, tag = HV_VIS_TAG_TREMBLE) + animate(pixel_w = base_w - strength, pixel_z = base_z, transform = base, time = real[2]) + animate(pixel_w = base_w, pixel_z = base_z, transform = base, time = real[3]) /** * A muzzle flash much bigger than a normal shot's, with a flash of light. It is where the barrel's end diff --git a/voidcrew/modules/hyperviolence/visuals/flying_gore.dm b/voidcrew/modules/hyperviolence/visuals/flying_gore.dm index b103ab527c4..21a513d8242 100644 --- a/voidcrew/modules/hyperviolence/visuals/flying_gore.dm +++ b/voidcrew/modules/hyperviolence/visuals/flying_gore.dm @@ -230,7 +230,7 @@ GLOBAL_LIST(hv_vis_arcing_gibs) /// where it is once it has dropped. /datum/component/hv_tumble/proc/launch() start_flight() - // In a Sandevistan field (cyberware/ware_sandy_timestop.dm) it waits out the freeze and flies slow. + // In a Sandevistan field (cyberware/ware_sandy_timestop.dm) it flies slow. step_timer = addtimer(CALLBACK(src, PROC_REF(take_step)), cyberware_dilated_time(parent, length(path) ? world.tick_lag : flight_time()), TIMER_STOPPABLE|TIMER_DELETE_ME) /datum/component/hv_tumble/proc/start_flight() @@ -432,12 +432,12 @@ GLOBAL_LIST(hv_vis_arcing_gibs) var/list/pivot = part.hv_vis_spin_pivot() var/start_x = part.pixel_x var/start_y = part.pixel_y - // Slower in a Sandevistan field (cyberware/ware_sandy_timestop.dm); the opening pause takes the freeze. - var/list/real = cyberware_dilated_steps(part, list(2.5, 1.5, 2, 3)) || list(0, 2.5, 1.5, 2, 3) - animate(part, pixel_x = start_x, pixel_y = start_y, transform = base, time = real[1] + real[2], tag = HV_VIS_TAG_SPIN) - animate(pixel_x = start_x + (end_x - start_x) / 3, pixel_y = start_y + (end_y - start_y) / 3, transform = hv_vis_turned(base, spin * 120, pivot[2], pivot[1]), time = real[3], easing = LINEAR_EASING) - animate(pixel_x = start_x + (end_x - start_x) * 2 / 3, pixel_y = start_y + (end_y - start_y) * 2 / 3, transform = hv_vis_turned(base, spin * 240, pivot[2], pivot[1]), time = real[4], easing = LINEAR_EASING) - animate(pixel_x = end_x, pixel_y = end_y, transform = base, time = real[5], easing = QUAD_EASING|EASE_OUT) + // Slower in a Sandevistan field (cyberware/ware_sandy_timestop.dm). + var/list/real = cyberware_dilated_steps(part, list(2.5, 1.5, 2, 3)) || list(2.5, 1.5, 2, 3) + animate(part, pixel_x = start_x, pixel_y = start_y, transform = base, time = real[1], tag = HV_VIS_TAG_SPIN) + animate(pixel_x = start_x + (end_x - start_x) / 3, pixel_y = start_y + (end_y - start_y) / 3, transform = hv_vis_turned(base, spin * 120, pivot[2], pivot[1]), time = real[2], easing = LINEAR_EASING) + animate(pixel_x = start_x + (end_x - start_x) * 2 / 3, pixel_y = start_y + (end_y - start_y) * 2 / 3, transform = hv_vis_turned(base, spin * 240, pivot[2], pivot[1]), time = real[3], easing = LINEAR_EASING) + animate(pixel_x = end_x, pixel_y = end_y, transform = base, time = real[4], easing = QUAD_EASING|EASE_OUT) // ---- Limbs and heads coming off ---- diff --git a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm index 65449aa6e2c..7875c3c55db 100644 --- a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm +++ b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm @@ -2,9 +2,10 @@ * # The Cascade line's time dilation * * voidcrew/modules/cyberware/ware_sandy_timestop.dm. A player's surge with no other player near slows the - * world: the Cascade lattice freezes it for 1 s (a human NPC is held by tg's immobilise, hand-block and - * AI-pause traits, and its timers stop), then runs it at 20% for 3 s (the NPC walks under a movespeed - * modifier), then lets go. The user can act the whole time. Another player near means no dilation at all. + * world for the whole window: street 6 s at 50%, service 7 s at 35%, Cascade 8 s at 20%. Nothing is ever + * frozen: a human NPC walks under a movespeed modifier and its timers run slow, and it never gets tg's + * immobilise, hand-block or AI-pause traits. The user can act the whole time, and pressing the surge again ends + * the window early, slow-mo and all. Another player near means no dilation at all. * * Unit-test files compile before voidcrew/_DEFINES/, so literals stand in for module defines. */ @@ -33,11 +34,27 @@ body.key = null return ..() -/// Nobody else near: the world freezes, then slows, then lets go on time; the user can act throughout +/// Whether `body` carries any of the traits tg's own time stop freezes with +/datum/unit_test/voidcrew_cyberware_timestop/proc/frozen(mob/living/body) + return HAS_TRAIT(body, TRAIT_IMMOBILIZED) || HAS_TRAIT(body, TRAIT_HANDS_BLOCKED) || HAS_TRAIT(body, TRAIT_AI_PAUSED) || HAS_TRAIT(body, TRAIT_MUTE) || HAS_TRAIT(body, TRAIT_EMOTEMUTE) + +/// Sets the length of `user`'s next window, to keep a test short +/datum/unit_test/voidcrew_cyberware_timestop/proc/set_window(mob/living/carbon/human/user, length) + var/obj/item/organ/cyberimp/cyberware/cascade/lattice = locate() in user.organs + lattice.window_duration = length + +/// `user`'s surge button +/datum/unit_test/voidcrew_cyberware_timestop/proc/surge_button(mob/living/carbon/human/user) + var/obj/item/organ/cyberimp/cyberware/cascade/lattice = locate() in user.organs + return locate(/datum/action/cooldown/cyberware/cascade_surge) in lattice.actions + +/// Nobody else near: the world runs slow from the first moment to the end of the window, nothing is ever frozen, +/// and the user can act throughout /datum/unit_test/voidcrew_cyberware_timestop/alone /datum/unit_test/voidcrew_cyberware_timestop/alone/Run() var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + set_window(user, 2 SECONDS) var/mob/living/carbon/human/npc/raider = allocate(/mob/living/carbon/human/npc, spot(2, 0)) var/next_move_was = world.time + 5 SECONDS raider.next_move = next_move_was @@ -45,38 +62,125 @@ TEST_ASSERT_NOTNULL(window, "The lattice's window didn't open") sleep(0.1 SECONDS) // the field starts async TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") - TEST_ASSERT(istype(window.time_stop.chronofield, /datum/proximity_monitor/advanced/timestop/sandevistan), "The dilation has no field") - - // The freeze: tg's own traits, no Stun. - TEST_ASSERT(HAS_TRAIT(raider, TRAIT_IMMOBILIZED), "The NPC wasn't held still") - TEST_ASSERT(HAS_TRAIT(raider, TRAIT_HANDS_BLOCKED), "The NPC's hands weren't stopped") - TEST_ASSERT(HAS_TRAIT(raider, TRAIT_AI_PAUSED), "The NPC's AI kept running in frozen time") - TEST_ASSERT(!raider.IsStun(), "The freeze stunned the NPC instead of holding it") - TEST_ASSERT(!HAS_TRAIT(user, TRAIT_IMMOBILIZED) && !HAS_TRAIT(user, TRAIT_HANDS_BLOCKED), "The user froze in their own time stop") + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield + TEST_ASSERT(istype(field), "The dilation has no field") + + // Slow-mo from the start: a walk under a movespeed modifier, no freeze and no Stun. + TEST_ASSERT(abs(field.time_scale - 0.2) < 0.001, "The Cascade's world doesn't run at 20% from the start ([field.time_scale])") + TEST_ASSERT(raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "The NPC isn't slowed as the window opens") + TEST_ASSERT(!frozen(raider), "The NPC was frozen") + TEST_ASSERT(!raider.IsStun(), "The dilation stunned the NPC") + TEST_ASSERT(!frozen(user), "The user froze in their own dilation") var/turf/start = get_turf(user) user.Move(spot(0, 1), NORTH) - TEST_ASSERT(get_turf(user) != start, "The user couldn't move in frozen time") + TEST_ASSERT(get_turf(user) != start, "The user couldn't move in their own slow-mo") - // Frozen bodies still get hurt through tg. + // Slowed bodies still get hurt through tg. var/brute_before = raider.getBruteLoss() raider.apply_damage(10, BRUTE, BODY_ZONE_CHEST) - TEST_ASSERT(raider.getBruteLoss() > brute_before, "A frozen NPC didn't take damage") - - sleep(0.8 SECONDS) // still inside the Cascade's 1 s freeze - TEST_ASSERT(raider.next_move > next_move_was + 0.5 SECONDS, "The NPC's timers ran on through frozen time") + TEST_ASSERT(raider.getBruteLoss() > brute_before, "A slowed NPC didn't take damage") + + // Through the whole window: slowed, never frozen. + for(var/i in 1 to 4) + sleep(0.4 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "The slow-mo ended [i * 0.4] s into a 2 s window") + TEST_ASSERT(!frozen(raider), "The NPC was frozen [i * 0.4] s in") + TEST_ASSERT(raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "The NPC wasn't slowed [i * 0.4] s in") + TEST_ASSERT(raider.next_move > next_move_was + 1 SECONDS, "The NPC's timers ran on at full speed") + TEST_ASSERT(!length(field.frozen_mobs), "The field kept a list of frozen mobs") + + sleep(0.7 SECONDS) // past the window + TEST_ASSERT(!user.has_status_effect(/datum/status_effect/cyberware_cascade_window), "Test fixture: the window outlived its 2 s") + TEST_ASSERT_NULL(window.time_stop, "The slow-mo outlived the window") + TEST_ASSERT(!raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "The NPC stayed slow after the window") + TEST_ASSERT(!frozen(raider), "The NPC was frozen after the window") + user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) - sleep(0.5 SECONDS) // then the world runs at 20% - TEST_ASSERT_NOTNULL(window.time_stop, "The dilation ended with the freeze") - TEST_ASSERT(!HAS_TRAIT(raider, TRAIT_IMMOBILIZED) && !HAS_TRAIT(raider, TRAIT_AI_PAUSED), "The NPC stayed frozen into the slow-mo") - TEST_ASSERT(raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "The NPC isn't slowed in slow-mo") +/// The slow-mo lasts each model's whole window at that model's pace +/datum/unit_test/voidcrew_cyberware_timestop/full_window + +/datum/unit_test/voidcrew_cyberware_timestop/full_window/Run() + var/list/models = list( + /obj/item/organ/cyberimp/cyberware/cascade/street = list(6 SECONDS, 0.5), + /obj/item/organ/cyberimp/cyberware/cascade/milspec = list(7 SECONDS, 0.35), + /obj/item/organ/cyberimp/cyberware/cascade = list(8 SECONDS, 0.2), + ) + for(var/model in models) + var/list/expected = models[model] + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser", model) + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + TEST_ASSERT_NOTNULL(window, "[model]: the window didn't open") + TEST_ASSERT_EQUAL(window.duration - world.time, expected[1], "[model]: the window isn't [expected[1] / 10] s") + sleep(0.1 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "[model]: time didn't slow with nobody else near") + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield + TEST_ASSERT(abs(field.time_scale - expected[2]) < 0.001, "[model]: the world runs at [field.time_scale], not [expected[2]]") + TEST_ASSERT(abs(field.ends_at - window.duration) <= 1, "[model]: the slow-mo ends at [field.ends_at], the window at [window.duration]") + end_surge(user) + TEST_ASSERT_NULL(window.time_stop, "[model]: the slow-mo outlived the window") + user.key = null + qdel(user) + +/// Pressing the surge again ends the window early, slow-mo and all, and the crash starts. It never opens a fresh +/// window, and an NPC's lattice never turns itself off. +/datum/unit_test/voidcrew_cyberware_timestop/toggle_off + +/datum/unit_test/voidcrew_cyberware_timestop/toggle_off/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/mob/living/carbon/human/npc/raider = allocate(/mob/living/carbon/human/npc, spot(2, 0)) + var/datum/action/cooldown/cyberware/cascade_surge/button = surge_button(user) + TEST_ASSERT_NOTNULL(button, "Test fixture: the lattice has no surge button") + TEST_ASSERT(button.Trigger(), "The first press didn't surge") + var/datum/status_effect/cyberware_cascade_window/window = user.has_status_effect(/datum/status_effect/cyberware_cascade_window) + TEST_ASSERT_NOTNULL(window, "The first press didn't open the window") + var/cooldown_end = button.next_use_time + TEST_ASSERT(cooldown_end > world.time, "Test fixture: the press didn't start the cooldown") + sleep(0.5 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") + TEST_ASSERT(raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "Test fixture: the NPC isn't slowed") - sleep(2.9 SECONDS) // the rest of the 3 s of slow-mo - TEST_ASSERT_NULL(window.time_stop, "The dilation outlived its 4 s") - TEST_ASSERT(!raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "The NPC stayed slow after the dilation") - TEST_ASSERT(!HAS_TRAIT(raider, TRAIT_AI_PAUSED), "The NPC's AI stayed paused after the dilation") - TEST_ASSERT(user.has_status_effect(/datum/status_effect/cyberware_cascade_window), "The window didn't carry on after the dilation") - user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) + TEST_ASSERT(button.IsAvailable(), "The surge can't be pressed while its window runs") + TEST_ASSERT(button.Trigger(), "The second press did nothing") + TEST_ASSERT(!user.has_status_effect(/datum/status_effect/cyberware_cascade_window), "The second press didn't end the window") + TEST_ASSERT(user.has_status_effect(/datum/status_effect/cyberware_cascade_crash), "Ending the window early skipped the crash") + TEST_ASSERT_NULL(window.time_stop, "The slow-mo outlived the window") + TEST_ASSERT(!raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "The NPC stayed slow after the window was cut short") + TEST_ASSERT_EQUAL(button.next_use_time, cooldown_end, "Cutting the window short changed the cooldown") + + // In the crash, and after it, the button is back on its cooldown. + TEST_ASSERT(!button.IsAvailable(), "The surge could be pressed in the crash") + TEST_ASSERT(!button.Trigger(), "A third press went through") user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) + TEST_ASSERT(!button.Trigger(), "A press after the crash went through on cooldown") + TEST_ASSERT(!user.has_status_effect(/datum/status_effect/cyberware_cascade_window), "A press opened a fresh window on cooldown") + + // An NPC pressing its own surge again doesn't end it. + var/mob/living/carbon/human/npc/surger = make_surger(spot(3, 0)) + var/datum/action/cooldown/cyberware/cascade_surge/npc_button = surge_button(surger) + TEST_ASSERT(npc_button.Trigger(), "The NPC's press didn't surge") + TEST_ASSERT(!npc_button.IsAvailable(), "An NPC's surge reads as pressable in its window") + npc_button.Trigger() + TEST_ASSERT(surger.has_status_effect(/datum/status_effect/cyberware_cascade_window), "An NPC's second press ended its window") + end_surge(surger) + +/// A player coming in range mid-surge ends the slow-mo; the surge carries on as a plain one, with the plain trail +/datum/unit_test/voidcrew_cyberware_timestop/player_walks_in + +/datum/unit_test/voidcrew_cyberware_timestop/player_walks_in/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/mob/living/carbon/human/other = allocate(/mob/living/carbon/human/consistent, spot(3, 3)) + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + sleep(0.1 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") + other.key = "sandytimestopother" + sleep(0.5 SECONDS) // a couple of field ticks + TEST_ASSERT_NULL(window.time_stop, "The slow-mo carried on with a player in range") + TEST_ASSERT(user.has_status_effect(/datum/status_effect/cyberware_cascade_window), "The surge ended with the slow-mo") + var/turf/start = get_turf(user) + user.Move(spot(0, 1), NORTH) + TEST_ASSERT(get_turf(user) != start, "Test fixture: the user couldn't step") + TEST_ASSERT_NULL(locate(/obj/effect/temp_visual/decoy/fading/cascade/stopped) in spot(0, 1), "The green trail outlived the slow-mo") + end_surge(user) /// Another player near: the plain surge, no dilation /datum/unit_test/voidcrew_cyberware_timestop/witnessed @@ -90,7 +194,7 @@ TEST_ASSERT_NOTNULL(window, "The lattice's window didn't open") sleep(0.1 SECONDS) TEST_ASSERT_NULL(window.time_stop, "Time slowed with another player in sight") - TEST_ASSERT(!HAS_TRAIT(raider, TRAIT_IMMOBILIZED), "The NPC froze without a dilation") + TEST_ASSERT(!raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "The NPC was slowed without a dilation") user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) @@ -111,6 +215,7 @@ /datum/unit_test/voidcrew_cyberware_timestop/slow_everything/Run() var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + set_window(user, 2 SECONDS) var/mob/living/carbon/human/npc/raider = allocate(/mob/living/carbon/human/npc, spot(2, 0)) var/obj/item/gun/ballistic/automatic/pistol/raider_gun = allocate(/obj/item/gun/ballistic/automatic/pistol) raider.put_in_hands(raider_gun) @@ -130,17 +235,19 @@ TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield - // The freeze: the NPC's own clock stands still, the user's doesn't. - TEST_ASSERT_EQUAL(cyberware_time_lost(raider, 10), 10, "A frozen NPC's clock kept running") + // The NPC's clock runs at 20% from the start; the user's doesn't slow. + var/lost = cyberware_time_lost(raider, 10) + TEST_ASSERT(lost > 7.9 && lost < 8.1, "A slowed NPC's clock didn't run at 20% (lost [lost] of 10)") TEST_ASSERT_EQUAL(cyberware_time_lost(user, 10), 0, "The user's clock slowed") TEST_ASSERT_EQUAL(cyberware_dilated_time(user, 10), 10, "The user's animations would be slowed") TEST_ASSERT_NULL(cyberware_time_field(user_gun), "Something the user holds would be slowed") - TEST_ASSERT(cyberware_dilated_time(raider, 1) > 5, "A frozen NPC's gore wouldn't wait out the freeze") + var/gore = cyberware_dilated_time(raider, 1) + TEST_ASSERT(gore > 4.9 && gore < 5.1, "A slowed NPC's gore doesn't play at 20% ([gore] for 1)") TEST_ASSERT_NOTNULL(cyberware_time_field(pool), "Blood on the floor in the field isn't slowed") var/list/steps = cyberware_dilated_steps(raider, list(2, 2)) - TEST_ASSERT(length(steps) == 3 && steps[1] > 5 && steps[2] > 2, "A frozen NPC's flinch wouldn't hold, then play slow") + TEST_ASSERT(length(steps) == 2 && abs(steps[1] - 10) < 0.1 && abs(steps[2] - 10) < 0.1, "A slowed NPC's flinch doesn't play at 20% from its first step ([jointext(steps, ",")])") - // Bursts queued before the freeze wait for it. + // Bursts queued in the slow-mo wait longer. raider_gun.cyberware_burst_start = cyberware_burst_clock(raider) raider_gun.cyberware_burst_gap = 1 var/datum/component/ranged_attacks/rat_gun = rat.GetComponent(/datum/component/ranged_attacks) @@ -148,36 +255,32 @@ var/gun_start = raider_gun.cyberware_burst_start var/rat_start = rat_gun.cyberware_burst_start - // Machines and fuses. - TEST_ASSERT(mech.completely_disabled, "A mech kept running in frozen time") - TEST_ASSERT(turret.last_fired + turret.shot_delay > world.time, "A turret could fire in frozen time") + // Machines and fuses: nothing is shut down, every clock runs slow. + TEST_ASSERT(!mech.completely_disabled, "The dilation shut a mech down") + TEST_ASSERT(field.held_mechs[mech], "A mech in the field wasn't taken") + TEST_ASSERT(field.held_turrets[turret], "A turret in the field wasn't taken") TEST_ASSERT(field.held_grenades[bomb], "A lit grenade in the field wasn't taken") + turret.last_fired = world.time + var/fired_at = turret.last_fired + mech.cooldown_vehicle_move = world.time + 5 + var/move_at = mech.cooldown_vehicle_move sleep(0.5 SECONDS) - TEST_ASSERT(cyberware_burst_wait(raider, raider_gun.cyberware_burst_start, 1, 1) > 0, "A queued burst shot fired in frozen time") + TEST_ASSERT(cyberware_burst_wait(raider, raider_gun.cyberware_burst_start, 1, 1) > 0, "A queued burst shot fired at full speed") TEST_ASSERT(raider_gun.cyberware_burst_start > gun_start, "The NPC's burst clock wasn't pushed back") TEST_ASSERT(rat_gun.cyberware_burst_start > rat_start, "A basic mob's burst clock wasn't pushed back") - TEST_ASSERT(turret.last_fired + turret.shot_delay > world.time, "A turret could fire in frozen time") - - sleep(0.6 SECONDS) // into the 20% slow-mo - TEST_ASSERT(!mech.completely_disabled, "A mech stayed shut down into the slow-mo") - var/lost = cyberware_time_lost(raider, 10) - TEST_ASSERT(lost > 7.9 && lost < 8.1, "A slowed NPC's clock didn't run at 20% (lost [lost] of 10)") - mech.cooldown_vehicle_move = world.time + 5 - var/move_at = mech.cooldown_vehicle_move - sleep(0.5 SECONDS) + TEST_ASSERT(turret.last_fired > fired_at, "A slowed turret's fire cooldown wasn't pushed back") TEST_ASSERT(mech.cooldown_vehicle_move > move_at, "A slowed mech's move cooldown wasn't pushed back") + TEST_ASSERT(!mech.completely_disabled, "The dilation shut a mech down") TEST_ASSERT(!(bomb.dud_flags & GRENADE_USED), "A grenade's fuse burned through at full speed") - sleep(2.6 SECONDS) // past the end - TEST_ASSERT_NULL(window.time_stop, "The dilation outlived its 4 s") + sleep(1.8 SECONDS) // past the window + TEST_ASSERT_NULL(window.time_stop, "The slow-mo outlived the window") TEST_ASSERT_EQUAL(cyberware_time_lost(raider, 10), 0, "The NPC's clock stayed slow after the dilation") TEST_ASSERT_NULL(cyberware_time_field(pool), "Blood stayed slowed after the dilation") - TEST_ASSERT(!mech.completely_disabled, "The mech stayed shut down after the dilation") - TEST_ASSERT(!(bomb.dud_flags & GRENADE_USED), "A grenade's fuse didn't lose its frozen and slowed time") + TEST_ASSERT(!(bomb.dud_flags & GRENADE_USED), "A grenade's fuse didn't lose its slowed time") TEST_ASSERT(bomb.cyberware_fuse_at > world.time, "A grenade's fuse has nothing left after losing time") bomb.cyberware_fuse_at = 0 // don't let it go off after the test - user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) /// Whether `thing` wears the dilation's tint @@ -191,7 +294,7 @@ TEST_ASSERT(lattice.Insert(raider, special = TRUE), "Test fixture: the lattice wouldn't go in the NPC") return raider -/// No reflex dodge while the user's own world is slowed; it comes back for the rest of the window +/// No reflex dodge while the user's own world is slowed; it comes back if the slow-mo ends before the window does /datum/unit_test/voidcrew_cyberware_timestop/no_dodge_while_slowed /datum/unit_test/voidcrew_cyberware_timestop/no_dodge_while_slowed/Run() @@ -205,13 +308,13 @@ var/datum/component/cyberware_dodge/arbiter = user.GetComponent(/datum/component/cyberware_dodge) TEST_ASSERT_NOTNULL(arbiter, "The window has no dodge") TEST_ASSERT(cyberware_time_stopping(user), "The user isn't marked as slowing time") - TEST_ASSERT_EQUAL(arbiter.on_check_block(user, knife, 10, "the knife", MELEE_ATTACK), FAILED_BLOCK, "The user dodged a blow in frozen time") - sleep(1.2 SECONDS) TEST_ASSERT_EQUAL(arbiter.on_check_block(user, knife, 10, "the knife", MELEE_ATTACK), FAILED_BLOCK, "The user dodged a blow in slow-mo") - sleep(3 SECONDS) // past the dilation, still in the window - TEST_ASSERT_NULL(window.time_stop, "The dilation outlived its 4 s") + sleep(1.2 SECONDS) + TEST_ASSERT_EQUAL(arbiter.on_check_block(user, knife, 10, "the knife", MELEE_ATTACK), FAILED_BLOCK, "The user dodged a blow later in slow-mo") + qdel(window.time_stop) // as when a player comes in range + TEST_ASSERT_NULL(window.time_stop, "Test fixture: the dilation didn't end") TEST_ASSERT(!cyberware_time_stopping(user), "The user still counts as slowing time") - TEST_ASSERT_EQUAL(arbiter.on_check_block(user, knife, 10, "the knife", MELEE_ATTACK), SUCCESSFUL_BLOCK, "The dodge didn't come back after the dilation") + TEST_ASSERT_EQUAL(arbiter.on_check_block(user, knife, 10, "the knife", MELEE_ATTACK), SUCCESSFUL_BLOCK, "The dodge didn't come back after the slow-mo ended") user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) @@ -230,12 +333,12 @@ TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield TEST_ASSERT(field.surging[raider], "The field didn't notice the NPC's surge") - TEST_ASSERT(!HAS_TRAIT(raider, TRAIT_IMMOBILIZED) && !HAS_TRAIT(raider, TRAIT_AI_PAUSED), "A surging NPC was frozen") + TEST_ASSERT(!frozen(raider), "A surging NPC was frozen") TEST_ASSERT(!tinted(raider), "A surging NPC was tinted") TEST_ASSERT_EQUAL(cyberware_time_lost(raider, 10), 0, "A surging NPC's clock slowed") TEST_ASSERT_NULL(cyberware_time_field(raider), "A surging NPC's gore would be slowed") - TEST_ASSERT(!field.held_shots[shot] && !shot.paused && shot.speed == shot_speed, "A surging NPC's shot was held") - sleep(1.2 SECONDS) // into the slow-mo + TEST_ASSERT(!field.held_shots[shot] && shot.speed == shot_speed, "A surging NPC's shot was held") + sleep(0.5 SECONDS) // a couple of field ticks TEST_ASSERT(!raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "A surging NPC was slowed") // Its window closes: the field takes it like anyone else. @@ -260,7 +363,7 @@ sleep(0.1 SECONDS) TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield - sleep(1.2 SECONDS) // into the slow-mo + sleep(0.3 SECONDS) TEST_ASSERT(raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "Test fixture: the NPC isn't slowed") TEST_ASSERT(field.held_shots[shot], "Test fixture: the NPC's shot isn't held") raider.hv_vis_play("sandy_test_flinch", list(list(2, 0, matrix(), 2, LINEAR_EASING), list(0, 0, matrix(), 2, LINEAR_EASING))) @@ -269,7 +372,7 @@ TEST_ASSERT_NOTNULL(surge(raider), "The NPC's window didn't open") TEST_ASSERT(field.surging[raider], "The field didn't let the surging NPC go") TEST_ASSERT(!raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "The surging NPC stayed slowed") - TEST_ASSERT(!HAS_TRAIT(raider, TRAIT_IMMOBILIZED) && !HAS_TRAIT(raider, TRAIT_AI_PAUSED), "The surging NPC stayed held") + TEST_ASSERT(!frozen(raider), "The surging NPC was frozen") TEST_ASSERT(!tinted(raider), "The surging NPC stayed tinted") TEST_ASSERT_NULL(GLOB.cyberware_time_dilated[raider], "The surging NPC is still in the field's hands") TEST_ASSERT_EQUAL(cyberware_time_lost(raider, 10), 0, "The surging NPC's clock stayed slow") @@ -312,7 +415,7 @@ field.user_multiplier = 5 end_surge(user) -/// A slower surger in the user's field is held at its relative rate through the freeze, never frozen or tinted +/// A slower surger in the user's field is held at its relative rate for the whole window, never frozen or tinted /datum/unit_test/voidcrew_cyberware_timestop/slower_surger /datum/unit_test/voidcrew_cyberware_timestop/slower_surger/Run() @@ -325,13 +428,14 @@ var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield TEST_ASSERT(field.surging[raider], "The field didn't notice the NPC's surge") TEST_ASSERT(abs(cyberware_time_scale(raider) - 0.4) < 0.001, "A street surger in a Cascade's field isn't at 40% ([cyberware_time_scale(raider)])") - TEST_ASSERT(!HAS_TRAIT(raider, TRAIT_IMMOBILIZED) && !HAS_TRAIT(raider, TRAIT_AI_PAUSED), "A surging NPC was frozen by the user's freeze") + TEST_ASSERT(!frozen(raider), "A surging NPC was frozen") TEST_ASSERT(!tinted(raider), "A surging NPC was tinted") TEST_ASSERT(raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "A slower surger isn't slowed") var/lost = cyberware_time_lost(raider, 10) TEST_ASSERT(lost > 5.9 && lost < 6.1, "A slower surger's clock didn't run at 40% (lost [lost] of 10)") - sleep(1.2 SECONDS) // into the user's slow-mo: the surger keeps its own rate - TEST_ASSERT(abs(cyberware_time_scale(raider) - 0.4) < 0.001, "The thaw changed a surger's rate") + sleep(1.2 SECONDS) // later in the window: the surger keeps its own rate + TEST_ASSERT(abs(cyberware_time_scale(raider) - 0.4) < 0.001, "A surger's rate changed mid-window ([cyberware_time_scale(raider)])") + TEST_ASSERT(raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "A slower surger stopped being slowed mid-window") end_surge(user) end_surge(raider) @@ -358,7 +462,7 @@ TEST_ASSERT_EQUAL(cyberware_dilated_time(raider, 10), 5, "A faster surger's gore doesn't play at double speed") var/datum/movespeed_modifier/pace = raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation) TEST_ASSERT(pace && pace.multiplicative_slowdown <= 0, "A faster surger doesn't walk faster") - TEST_ASSERT(field.held_shots[shot] && !shot.paused && shot.speed == shot_speed * field.time_scale, "A faster surger's shot doesn't fly at the field's pace") + TEST_ASSERT(field.held_shots[shot] && shot.speed == shot_speed * field.time_scale, "A faster surger's shot doesn't fly at the field's pace") TEST_ASSERT(!tinted(shot), "A surger's shot was tinted") var/next_move_was = world.time + 5 SECONDS @@ -379,14 +483,13 @@ TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield TEST_ASSERT_NOTNULL(surge(raider), "The NPC's window didn't open") - sleep(1.2 SECONDS) // into the slow-mo TEST_ASSERT(field.time_scale > 0 && field.time_scale < 1, "Test fixture: not in the slow-mo") var/obj/projectile/bullet/user_shot = allocate(/obj/projectile/bullet, spot(1, 0)) user_shot.firer = user var/user_speed = user_shot.speed - field.hang_shot(user_shot) - TEST_ASSERT(field.held_shots[user_shot] && !user_shot.paused, "The user's slow-mo shot wasn't held") + field.take_user_shot(user_shot) + TEST_ASSERT(field.held_shots[user_shot] && !user_shot.paused, "The user's slow-mo shot wasn't held, or hung") TEST_ASSERT_EQUAL(user_shot.speed, user_speed * field.time_scale, "The user's slow-mo shot doesn't crawl at the field's pace") TEST_ASSERT(!tinted(user_shot), "The user's slow-mo shot was tinted") @@ -394,15 +497,14 @@ npc_shot.firer = raider var/npc_speed = npc_shot.speed field.freeze_atom(npc_shot) - TEST_ASSERT(field.held_shots[npc_shot] && !npc_shot.paused, "A surging NPC's shot wasn't held") + TEST_ASSERT(field.held_shots[npc_shot] && !npc_shot.paused, "A surging NPC's shot wasn't held, or hung") TEST_ASSERT_EQUAL(npc_shot.speed, npc_speed * field.time_scale, "A surging NPC's shot doesn't fly at the field's pace") TEST_ASSERT(!tinted(npc_shot), "A surging NPC's shot was tinted") - sleep(3 SECONDS) // past the end - TEST_ASSERT_NULL(window.time_stop, "The dilation outlived its 4 s") + end_surge(user) + TEST_ASSERT_NULL(window.time_stop, "The slow-mo outlived the window") TEST_ASSERT_EQUAL(user_shot.speed, user_speed, "The user's shot didn't get its speed back") TEST_ASSERT_EQUAL(npc_shot.speed, npc_speed, "The NPC's shot didn't get its speed back") - end_surge(user) end_surge(raider) /// A surging NPC that dies is let go for good: its corpse isn't taken back and tinted @@ -426,8 +528,8 @@ end_surge(user) end_surge(raider) -/// The user hears the slowed world slowed (ware_sandy_senses.dm): world sounds in the field take its pace and -/// a quiet low rumble in the freeze; their own sounds, sounds from outside the field and other listeners don't. +/// The user hears the slowed world slowed (ware_sandy_senses.dm): world sounds in the field take its pace at full +/// volume; their own sounds, sounds from outside the field and other listeners don't. /datum/unit_test/voidcrew_cyberware_timestop/heard_slowed /datum/unit_test/voidcrew_cyberware_timestop/heard_slowed/Run() @@ -441,15 +543,14 @@ var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield TEST_ASSERT_NOTNULL(field.senses, "The field has no senses for its user") - // The freeze: a quiet, low rumble. - var/frozen_speed = cyberware_heard_speed(user, get_turf(raider), raider) - TEST_ASSERT(frozen_speed > 0 && frozen_speed < 0.5, "A world sound in the freeze played at [frozen_speed]") + // From the start: the world at the field's pace, as loud as ever. + TEST_ASSERT_EQUAL(cyberware_heard_speed(user, get_turf(raider), raider), field.time_scale, "A world sound didn't take the slow-mo pace") var/sound/shot = sound('sound/items/weapons/gun/pistol/shot.ogg') shot.volume = 100 cyberware_dilate_heard(user, get_turf(raider), shot, raider) - TEST_ASSERT(shot.frequency == frozen_speed, "A world sound in the freeze went out at frequency [shot.frequency]") - TEST_ASSERT(shot.volume < 50, "A world sound in the freeze stayed loud ([shot.volume])") - TEST_ASSERT_EQUAL(field.senses.loop_speed, frozen_speed, "The user's loops didn't slow in the freeze") + TEST_ASSERT(shot.frequency == field.time_scale, "A world sound went out at frequency [shot.frequency]") + TEST_ASSERT_EQUAL(shot.volume, 100, "A slowed world sound was made quieter") + TEST_ASSERT_EQUAL(field.senses.loop_speed, field.time_scale, "The user's loops didn't take the slow-mo pace") // The user's own sounds, other listeners and far sounds stay as they are. TEST_ASSERT_EQUAL(cyberware_heard_speed(user, get_turf(user), raider), 1, "A sound on the user's own tile was slowed") @@ -460,10 +561,6 @@ TEST_ASSERT_EQUAL(cyberware_scaled_frequency(44100, 0.5), 22050, "A rate in Hz didn't scale") TEST_ASSERT_EQUAL(cyberware_scaled_frequency(0, 0.2), 0.2, "An as-is sound didn't scale") - sleep(1.2 SECONDS) // into the 20% slow-mo - TEST_ASSERT_EQUAL(cyberware_heard_speed(user, get_turf(raider), raider), field.time_scale, "A world sound didn't take the slow-mo pace") - TEST_ASSERT_EQUAL(field.senses.loop_speed, field.time_scale, "The user's loops didn't take the slow-mo pace") - user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) TEST_ASSERT_NULL(window.time_stop, "Test fixture: the dilation outlived its window") @@ -488,17 +585,12 @@ TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield field.senses.refresh_weather(force = TRUE) // no client in a unit test - var/image/frozen = field.senses.area_images[place] - TEST_ASSERT_NOTNULL(frozen, "The rained-on area got no image") - TEST_ASSERT(frozen.override, "The weather image doesn't replace the area's look") - TEST_ASSERT(weather_states(frozen).Find("rain_high_still"), "The freeze didn't show still rain: [jointext(weather_states(frozen), ",")]") - TEST_ASSERT(!weather_states(frozen).Find("rain_high"), "The fast rain is still in the freeze image") - TEST_ASSERT_EQUAL(length(place.overlays), length(area_overlays), "The area's own overlays changed") - - sleep(1.2 SECONDS) // into the 20% slow-mo - field.senses.refresh_weather(force = TRUE) var/image/slowed = field.senses.area_images[place] + TEST_ASSERT_NOTNULL(slowed, "The rained-on area got no image") + TEST_ASSERT(slowed.override, "The weather image doesn't replace the area's look") TEST_ASSERT(weather_states(slowed).Find("rain_high_s20"), "The slow-mo didn't show slowed rain: [jointext(weather_states(slowed), ",")]") + TEST_ASSERT(!weather_states(slowed).Find("rain_high"), "The fast rain is still in the slowed image") + TEST_ASSERT_EQUAL(length(place.overlays), length(area_overlays), "The area's own overlays changed") user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) From 5b8102e19c88e4b689eb19b1b12cdc3d840e86e5 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 02:25:49 -0800 Subject: [PATCH 16/20] Compute the deflect cap without float drift --- voidcrew/modules/cyberware/ware_pro_combat.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voidcrew/modules/cyberware/ware_pro_combat.dm b/voidcrew/modules/cyberware/ware_pro_combat.dm index 78b2a138ebb..f70189ca13e 100644 --- a/voidcrew/modules/cyberware/ware_pro_combat.dm +++ b/voidcrew/modules/cyberware/ware_pro_combat.dm @@ -306,7 +306,7 @@ dodge_chance = clamp(dodge_chance, 0, 100) if(dodge_chance >= CYBERWARE_AVOID_CAP) return 0 - return (CYBERWARE_AVOID_CAP - dodge_chance) / (100 - dodge_chance) * 100 + return (CYBERWARE_AVOID_CAP - dodge_chance) * 100 / (100 - dodge_chance) /// Whether this shot is travelling the way the bearer faces, so it strikes them in the back. /proc/cyberware_shot_from_behind(mob/living/bearer, obj/projectile/shot) From f003e3e0aba562c2061b4e8d64c1a76bf1c92ef6 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 02:44:39 -0800 Subject: [PATCH 17/20] Let the deflect test force a glance past the shared cap --- voidcrew/modules/cyberware/ware_pro_combat.dm | 12 +++++++----- .../modules/unit_tests/voidcrew_cyberware_deflect.dm | 2 ++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/voidcrew/modules/cyberware/ware_pro_combat.dm b/voidcrew/modules/cyberware/ware_pro_combat.dm index f70189ca13e..9a7e77c66a0 100644 --- a/voidcrew/modules/cyberware/ware_pro_combat.dm +++ b/voidcrew/modules/cyberware/ware_pro_combat.dm @@ -62,6 +62,8 @@ var/list/melee_sources = list() /// key -> /datum/callback returning that source's live bullet-deflect chance (the dermal plating). var/list/deflect_sources = list() + /// Dodge and deflect together never stop more than this percent of shots. + var/avoid_cap = CYBERWARE_AVOID_CAP /// world.time of the bearer's last move. var/last_move_time = 0 /// world.time a dodge was last announced: an NPC under fire says it at most once every CYBERWARE_DODGE_LINE_GAP, so chat isn't flooded @@ -103,7 +105,7 @@ if(dodge_chance > 0 && prob(dodge_chance)) INVOKE_ASYNC(src, PROC_REF(perform_sidestep), source, hitting_projectile) return COMPONENT_BULLET_PIERCED - var/deflect_chance = min(live_deflect_chance(source, hitting_projectile), cyberware_deflect_cap(dodge_chance)) + var/deflect_chance = min(live_deflect_chance(source, hitting_projectile), cyberware_deflect_cap(dodge_chance, avoid_cap)) if(deflect_chance <= 0 || !prob(deflect_chance)) return NONE glance(source, hitting_projectile) @@ -299,14 +301,14 @@ /** * The most a deflect may roll after a dodge roll at dodge_chance missed, so - * that dodge and deflect together never stop more than CYBERWARE_AVOID_CAP + * that dodge and deflect together never stop more than avoid_cap * percent of shots: dodge + (1 - dodge) * deflect <= cap. */ -/proc/cyberware_deflect_cap(dodge_chance) +/proc/cyberware_deflect_cap(dodge_chance, avoid_cap = CYBERWARE_AVOID_CAP) dodge_chance = clamp(dodge_chance, 0, 100) - if(dodge_chance >= CYBERWARE_AVOID_CAP) + if(dodge_chance >= avoid_cap) return 0 - return (CYBERWARE_AVOID_CAP - dodge_chance) * 100 / (100 - dodge_chance) + return (avoid_cap - dodge_chance) * 100 / (100 - dodge_chance) /// Whether this shot is travelling the way the bearer faces, so it strikes them in the back. /proc/cyberware_shot_from_behind(mob/living/bearer, obj/projectile/shot) diff --git a/voidcrew/modules/unit_tests/voidcrew_cyberware_deflect.dm b/voidcrew/modules/unit_tests/voidcrew_cyberware_deflect.dm index 48774034957..696e4b85e95 100644 --- a/voidcrew/modules/unit_tests/voidcrew_cyberware_deflect.dm +++ b/voidcrew/modules/unit_tests/voidcrew_cyberware_deflect.dm @@ -25,7 +25,9 @@ TEST_ASSERT(slab.Insert(patient, TRUE), "Slabskin fixture failed") var/datum/component/cyberware_dodge/arbiter = patient.GetComponent(/datum/component/cyberware_dodge) TEST_ASSERT_NOTNULL(arbiter, "Slabskin didn't register a deflect source") + // Force every glance: a 100% plate under a lifted cap. The cap itself is checked above. slab.deflect_chance = 100 + arbiter.avoid_cap = 100 patient.setDir(SOUTH) // A bullet from the front (travelling north into a bearer facing south) glances off. From b8ccd88e12aeec6e703b2d06d80a435260131beb Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 03:01:37 -0800 Subject: [PATCH 18/20] Roll the Sandevistan ring with the slow-mo, tint behind its front, flash on release, and count carried players as witnesses --- voidcrew/modules/cyberware/cyberware_anim.dm | 15 +++- .../modules/cyberware/ware_sandy_timestop.dm | 87 ++++++++++++++++++- .../unit_tests/voidcrew_cyberware_timestop.dm | 19 ++++ 3 files changed, 115 insertions(+), 6 deletions(-) diff --git a/voidcrew/modules/cyberware/cyberware_anim.dm b/voidcrew/modules/cyberware/cyberware_anim.dm index 4bb4691941b..bcc8afae920 100644 --- a/voidcrew/modules/cyberware/cyberware_anim.dm +++ b/voidcrew/modules/cyberware/cyberware_anim.dm @@ -741,12 +741,19 @@ var/turf/here = get_turf(owner) if(!here) return - var/datum/status_effect/cyberware_cascade_window/window = owner?.has_status_effect(/datum/status_effect/cyberware_cascade_window) - if(window?.time_stop) // a green ring that rolls out past the edge of the screen - new /obj/effect/temp_visual/cyberware_time_ring(here, stop_ring_scale, ring_alpha, ring_time, TRUE) - return new /obj/effect/temp_visual/cyberware_time_ring(here, ring_scale, ring_alpha, ring_time) +/// The world slows as the window opens: the green ring rolls out past the edge of the screen at once, with the +/// tint behind its front, instead of waiting for the last spine node. +/obj/item/organ/cyberimp/cyberware/cascade/proc/roll_stop_ring() + if(ring_timer) + deltimer(ring_timer) + ring_timer = null + var/turf/here = get_turf(owner) + if(!here) + return + new /obj/effect/temp_visual/cyberware_time_ring(here, stop_ring_scale, ring_alpha, ring_time, TRUE) + /// The crash: nodes die hips to neck and stay dark. /obj/item/organ/cyberimp/cyberware/cascade/proc/play_crash() if(!owner) diff --git a/voidcrew/modules/cyberware/ware_sandy_timestop.dm b/voidcrew/modules/cyberware/ware_sandy_timestop.dm index 0ba88177385..ac01b8fa50a 100644 --- a/voidcrew/modules/cyberware/ware_sandy_timestop.dm +++ b/voidcrew/modules/cyberware/ware_sandy_timestop.dm @@ -66,6 +66,8 @@ #define SANDY_STOP_SAME_RATE 0.01 /// Filter name of the user's edge glow. #define SANDY_STOP_OUTLINE "sandy_time_stop_outline" +/// Fullscreen category of the release flash. +#define SANDY_STOP_FLASH "sandy_time_stop_flash" /// Every atom the dilation has hold of, to its field. GLOBAL_LIST_EMPTY(cyberware_time_dilated) @@ -225,9 +227,9 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) if(body.ckey) return TRUE continue - if(ismob(found) || isitem(found)) // ghosts and camera eyes don't count; radios hold nobody + if(ismob(found)) // ghosts and camera eyes don't count continue - // a locker or a mech with a player in it + // a locker or a mech with a player in it, or a small player picked up and carried as an item for(var/mob/living/inside in found.get_all_contents()) if(inside != user && inside.ckey) return TRUE @@ -375,6 +377,18 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) var/user_multiplier = 1 /// Surging mobs held at their relative rate, to that rate. Never 0, never tinted. Their shots fly at the field's pace. var/list/own_scales = list() + /// Things that should wear the tint, to TRUE. + var/list/tinted = list() + /// Tinted things the ring hasn't reached yet, to the timer that tints them when it does. + var/list/tint_timers = list() + /// Where the green ring rolls out from, while it is rolling. + var/turf/ring_center + /// world.time the ring started rolling. + var/ring_start = 0 + /// How long the ring takes to reach its full size. + var/ring_time = 0 + /// The ring's full size, in tiles of radius. + var/ring_reach = 0 /datum/proximity_monitor/advanced/timestop/sandevistan/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, list/immune, antimagic_flags, channelled, _time_scale = 1, _user_multiplier = 1) time_scale = _time_scale > 0 ? _time_scale : 1 @@ -698,6 +712,10 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) boss_holds -= thing surging -= thing slowed_tags -= thing + tinted -= thing + if(tint_timers[thing]) + deltimer(tint_timers[thing]) + tint_timers -= thing own_scales -= thing frozen_structures -= thing frozen_things -= thing @@ -736,8 +754,56 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) senses?.refresh_weather() /datum/proximity_monitor/advanced/timestop/sandevistan/into_the_negative_zone(atom/A) + tinted[A] = TRUE + var/wait = ring_wait(A) + if(wait > 0) + if(!tint_timers[A]) + tint_timers[A] = addtimer(CALLBACK(src, PROC_REF(ring_reached), A), wait, TIMER_STOPPABLE) + return A.add_atom_colour(cyberware_time_stop_tint(), TEMPORARY_COLOUR_PRIORITY) +/datum/proximity_monitor/advanced/timestop/sandevistan/escape_the_negative_zone(atom/A) + if(tint_timers[A]) + deltimer(tint_timers[A]) + tint_timers -= A + tinted -= A + return ..() + +/// The ring starts rolling out from `center` to `reach` tiles over `time`: everything it hasn't reached yet loses +/// its tint until the ring's front passes it. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/sweep_ring(turf/center, reach, time) + ring_center = center + ring_start = world.time + ring_time = time + ring_reach = reach + for(var/atom/movable/thing as anything in tinted) + if(tint_timers[thing]) + continue + var/wait = ring_wait(thing) + if(wait <= 0) + continue + thing.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY) + tint_timers[thing] = addtimer(CALLBACK(src, PROC_REF(ring_reached), thing), wait, TIMER_STOPPABLE) + +/// How long until the rolling ring's front reaches `thing`, or 0 if it already has. The ring grows with a sine +/// ease-out (cyberware_time_ring). +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/ring_wait(atom/thing) + if(!ring_center || world.time >= ring_start + ring_time) + return 0 + var/turf/spot = get_turf(thing) + if(!spot || spot.z != ring_center.z) + return 0 + var/dist = sqrt((spot.x - ring_center.x) ** 2 + (spot.y - ring_center.y) ** 2) + var/reached = clamp((dist - 0.2) / max(ring_reach - 0.2, 0.1), 0, 1) + return ring_start + ring_time * arcsin(reached) / 90 - world.time + +/// The ring's front got to `thing`: on with its tint, if it still wants it. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/ring_reached(atom/movable/thing) + tint_timers -= thing + if(QDELETED(thing) || !tinted[thing]) + return + thing.add_atom_colour(cyberware_time_stop_tint(), TEMPORARY_COLOUR_PRIORITY) + // ========================================================================= // THE WINDOW'S SIDE // ========================================================================= @@ -766,6 +832,11 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) time_stop = null return FALSE RegisterSignal(time_stop, COMSIG_QDELETING, PROC_REF(time_stop_over)) + var/obj/item/organ/cyberimp/cyberware/cascade/lattice = cyberware_bearer_lattice(owner) + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = time_stop.chronofield + if(lattice) + lattice.roll_stop_ring() + field?.sweep_ring(here, lattice.stop_ring_scale, lattice.ring_time) owner.remove_client_colour(id) // the user sees the slowed world as it is owner.add_filter(SANDY_STOP_OUTLINE, 2, outline_filter(1, "#5AFF96D2")) return TRUE @@ -778,6 +849,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) if(QDELETED(owner)) return owner.remove_filter(SANDY_STOP_OUTLINE) + sandy_release_flash(owner) owner.add_client_colour(colour_type, id) /// Ends the dilation early (the window is closing). @@ -788,6 +860,16 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) QDEL_NULL(time_stop) if(!QDELETED(owner)) owner.remove_filter(SANDY_STOP_OUTLINE) + sandy_release_flash(owner) + +/// A pale green flash on the user's screen as the world speeds back up. +/proc/sandy_release_flash(mob/living/user) + user.overlay_fullscreen(SANDY_STOP_FLASH, /atom/movable/screen/fullscreen/flash/sandy_release) + user.clear_fullscreen(SANDY_STOP_FLASH, 0.22 SECONDS) + +/atom/movable/screen/fullscreen/flash/sandy_release + color = "#E1FFEB" + alpha = 140 /// One afterimage while time is dilated: green, gone in 0.4 s. It sits on the user's own tile, shifted back to /// where they were, so it sorts on their row and draws behind them in every facing. @@ -864,3 +946,4 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) #undef SANDY_STOP_MAX_RELATIVE #undef SANDY_STOP_SAME_RATE #undef SANDY_STOP_OUTLINE +#undef SANDY_STOP_FLASH diff --git a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm index 7875c3c55db..faed1647f21 100644 --- a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm +++ b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm @@ -198,6 +198,25 @@ user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) +/// A player hidden in a locker, or picked up and carried as an item, still counts as someone near +/datum/unit_test/voidcrew_cyberware_timestop/witnessed_hidden + +/datum/unit_test/voidcrew_cyberware_timestop/witnessed_hidden/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/obj/structure/closet/locker = allocate(/obj/structure/closet, spot(3, 0)) + var/mob/living/carbon/human/hider = allocate(/mob/living/carbon/human/consistent, spot(3, 0)) + hider.key = "sandytimestophider" + hider.forceMove(locker) + TEST_ASSERT(cyberware_time_stop_witnessed(user), "A player in a locker didn't count as someone near") + hider.key = null + var/mob/living/basic/mouse/pet = allocate(/mob/living/basic/mouse, spot(2, 2)) + pet.key = "sandytimestoppet" + var/obj/item/mob_holder/holder = allocate(/obj/item/mob_holder, spot(2, 2), pet) + TEST_ASSERT(pet.loc == holder, "The mouse wasn't picked up") + TEST_ASSERT(cyberware_time_stop_witnessed(user), "A player carried as an item didn't count as someone near") + pet.key = null + TEST_ASSERT(!cyberware_time_stop_witnessed(user), "Nobody's near, but the user counted as watched") + /// An NPC's surge never slows the world, whoever is near /datum/unit_test/voidcrew_cyberware_timestop/npc_surge From 5ac509ee669e088383629a44b154fc87fcd8b759 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 03:31:36 -0800 Subject: [PATCH 19/20] Keep a surging NPC's shots untinted, and bring the slow-mo tests in line with every bullet crawling --- .../modules/cyberware/ware_sandy_timestop.dm | 12 +++++++++++- .../unit_tests/voidcrew_cyberware_timestop.dm | 17 +++++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/voidcrew/modules/cyberware/ware_sandy_timestop.dm b/voidcrew/modules/cyberware/ware_sandy_timestop.dm index ac01b8fa50a..c6ed9f4160d 100644 --- a/voidcrew/modules/cyberware/ware_sandy_timestop.dm +++ b/voidcrew/modules/cyberware/ware_sandy_timestop.dm @@ -673,15 +673,25 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) /datum/proximity_monitor/advanced/timestop/sandevistan/proc/hold_shot(obj/projectile/shot) held_shots[shot] = shot.speed shot.speed = held_shots[shot] * scale_for(shot) + if(!user_shots[shot]) // a shot is made, and taken, before it has a firer + RegisterSignal(shot, COMSIG_PROJECTILE_FIRE, PROC_REF(held_shot_fired), override = TRUE) + +/// Signal proc for [COMSIG_PROJECTILE_FIRE] on a held shot: now it has a firer, a surging mob's shot loses the tint. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/held_shot_fired(obj/projectile/shot) + SIGNAL_HANDLER + UnregisterSignal(shot, COMSIG_PROJECTILE_FIRE) + if(shot.firer && (surging[shot.firer] || cyberware_own_surge(shot.firer))) + escape_the_negative_zone(shot) /datum/proximity_monitor/advanced/timestop/sandevistan/proc/let_go_shot(obj/projectile/shot) shot.speed = held_shots[shot] held_shots -= shot + UnregisterSignal(shot, COMSIG_PROJECTILE_FIRE) /// Signal proc for [COMSIG_PROJECTILE_FIRER_BEFORE_FIRE] on the user: watch the shot leave the barrel. /datum/proximity_monitor/advanced/timestop/sandevistan/proc/on_user_fire(datum/source, obj/projectile/shot, atom/fired_from, atom/original) SIGNAL_HANDLER - RegisterSignal(shot, COMSIG_PROJECTILE_FIRE, PROC_REF(take_user_shot)) + RegisterSignal(shot, COMSIG_PROJECTILE_FIRE, PROC_REF(take_user_shot), override = TRUE) /// Signal proc for [COMSIG_PROJECTILE_FIRE]: the user's shot keeps its colour but crawls at the field's pace like /// every bullet in it. diff --git a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm index faed1647f21..9db2e915d11 100644 --- a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm +++ b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm @@ -264,7 +264,8 @@ TEST_ASSERT(gore > 4.9 && gore < 5.1, "A slowed NPC's gore doesn't play at 20% ([gore] for 1)") TEST_ASSERT_NOTNULL(cyberware_time_field(pool), "Blood on the floor in the field isn't slowed") var/list/steps = cyberware_dilated_steps(raider, list(2, 2)) - TEST_ASSERT(length(steps) == 2 && abs(steps[1] - 10) < 0.1 && abs(steps[2] - 10) < 0.1, "A slowed NPC's flinch doesn't play at 20% from its first step ([jointext(steps, ",")])") + var/both = cyberware_dilated_time(raider, 4) // the second step may run past the window's end, back at full speed + TEST_ASSERT(length(steps) == 2 && abs(steps[1] - 10) < 0.1 && abs(steps[1] + steps[2] - both) < 0.1, "A slowed NPC's flinch doesn't play at 20% from its first step ([jointext(steps, ",")], [both] in all)") // Bursts queued in the slow-mo wait longer. raider_gun.cyberware_burst_start = cyberware_burst_clock(raider) @@ -285,7 +286,8 @@ var/move_at = mech.cooldown_vehicle_move sleep(0.5 SECONDS) - TEST_ASSERT(cyberware_burst_wait(raider, raider_gun.cyberware_burst_start, 1, 1) > 0, "A queued burst shot fired at full speed") + // 0.5 s at 20% is 1 decisecond of the NPC's own time: a 2 decisecond gap is still waiting, at full speed it's long gone + TEST_ASSERT(cyberware_burst_wait(raider, raider_gun.cyberware_burst_start, 2, 1) > 0, "A queued burst shot fired at full speed") TEST_ASSERT(raider_gun.cyberware_burst_start > gun_start, "The NPC's burst clock wasn't pushed back") TEST_ASSERT(rat_gun.cyberware_burst_start > rat_start, "A basic mob's burst clock wasn't pushed back") TEST_ASSERT(turret.last_fired > fired_at, "A slowed turret's fire cooldown wasn't pushed back") @@ -356,7 +358,7 @@ TEST_ASSERT(!tinted(raider), "A surging NPC was tinted") TEST_ASSERT_EQUAL(cyberware_time_lost(raider, 10), 0, "A surging NPC's clock slowed") TEST_ASSERT_NULL(cyberware_time_field(raider), "A surging NPC's gore would be slowed") - TEST_ASSERT(!field.held_shots[shot] && shot.speed == shot_speed, "A surging NPC's shot was held") + TEST_ASSERT(field.held_shots[shot] && shot.speed == shot_speed * field.time_scale, "A surging NPC's shot doesn't fly at the field's pace like every bullet") sleep(0.5 SECONDS) // a couple of field ticks TEST_ASSERT(!raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "A surging NPC was slowed") @@ -396,7 +398,7 @@ TEST_ASSERT_NULL(GLOB.cyberware_time_dilated[raider], "The surging NPC is still in the field's hands") TEST_ASSERT_EQUAL(cyberware_time_lost(raider, 10), 0, "The surging NPC's clock stayed slow") TEST_ASSERT_NULL(field.slowed_tags[raider], "The surging NPC's slowed animations weren't dropped") - TEST_ASSERT(!field.held_shots[shot] && shot.speed == shot_speed, "The surging NPC's shot stayed slowed") + TEST_ASSERT(field.held_shots[shot] && shot.speed == shot_speed * field.time_scale, "The surging NPC's shot left the field's pace") sleep(0.5 SECONDS) // a few field ticks TEST_ASSERT(!raider.has_movespeed_modifier(/datum/movespeed_modifier/cyberware_dilation), "The field took the surging NPC back") user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) @@ -506,16 +508,17 @@ var/obj/projectile/bullet/user_shot = allocate(/obj/projectile/bullet, spot(1, 0)) user_shot.firer = user - var/user_speed = user_shot.speed field.take_user_shot(user_shot) + var/user_speed = field.held_shots[user_shot] // its own speed: the field may have taken it as it appeared TEST_ASSERT(field.held_shots[user_shot] && !user_shot.paused, "The user's slow-mo shot wasn't held, or hung") TEST_ASSERT_EQUAL(user_shot.speed, user_speed * field.time_scale, "The user's slow-mo shot doesn't crawl at the field's pace") TEST_ASSERT(!tinted(user_shot), "The user's slow-mo shot was tinted") var/obj/projectile/bullet/npc_shot = allocate(/obj/projectile/bullet, spot(2, 0)) npc_shot.firer = raider - var/npc_speed = npc_shot.speed field.freeze_atom(npc_shot) + SEND_SIGNAL(npc_shot, COMSIG_PROJECTILE_FIRE) // made, then fired + var/npc_speed = field.held_shots[npc_shot] TEST_ASSERT(field.held_shots[npc_shot] && !npc_shot.paused, "A surging NPC's shot wasn't held, or hung") TEST_ASSERT_EQUAL(npc_shot.speed, npc_speed * field.time_scale, "A surging NPC's shot doesn't fly at the field's pace") TEST_ASSERT(!tinted(npc_shot), "A surging NPC's shot was tinted") @@ -626,4 +629,6 @@ /datum/unit_test/voidcrew_cyberware_timestop/weather_slowed/proc/weather_states(image/slowed) . = list() for(var/overlay in slowed.overlays) + if(overlay:icon == 'icons/effects/glow_weather.dmi') + continue // rain has no glow state: that overlay draws nothing either way . += overlay:icon_state From 3116f29aede324c9d7fcf3f61c388a3ae091a890 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 03:53:18 -0800 Subject: [PATCH 20/20] Run the Sandevistan user's own mech or vehicle at full speed in their slow-mo, and keep the slow-mo running from inside a mech --- .../modules/cyberware/ware_sandy_timestop.dm | 25 +++++++++++++++- .../unit_tests/voidcrew_cyberware_timestop.dm | 30 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/voidcrew/modules/cyberware/ware_sandy_timestop.dm b/voidcrew/modules/cyberware/ware_sandy_timestop.dm index c6ed9f4160d..5ccc21f0e1a 100644 --- a/voidcrew/modules/cyberware/ware_sandy_timestop.dm +++ b/voidcrew/modules/cyberware/ware_sandy_timestop.dm @@ -329,7 +329,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) QDEL_IN(src, duration) if(QDELETED(user)) return - var/datum/proximity_monitor/advanced/timestop/sandevistan/field = new(user, freezerange, TRUE, immune, antimagic_flags, FALSE, slow_scale, user_multiplier) + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = new(user, freezerange, FALSE, immune, antimagic_flags, FALSE, slow_scale, user_multiplier) // a user inside a mech still slows the world field.stopper = src field.ends_at = world.time + duration chronofield = field @@ -453,6 +453,8 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) if(cyberware_own_surge(A)) take_surger(A, cyberware_surge_multiplier(A)) return FALSE + if(own_ride(A)) // the user's mech or vehicle is part of them: full speed + return FALSE if(ismob(A)) var/mob/M = A if(M.can_block_magic(antimagic_flags)) @@ -756,6 +758,7 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) throw_datum.start_time += push push_machine_deadlines(push) check_surgers() + let_go_own_ride() for(var/mob/living/boss as anything in boss_holds.Copy()) if(world.time < boss_holds[boss]) continue @@ -763,6 +766,26 @@ GLOBAL_LIST_EMPTY(cyberware_time_fields) released[boss] = TRUE senses?.refresh_weather() +/// Whether `thing` is what the user is riding or piloting: a vehicle with them aboard, or anything they're buckled to. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/own_ride(atom/movable/thing) + if(!ismob(host) || !istype(thing)) + return FALSE + var/mob/living/user = host + if(istype(thing, /obj/vehicle)) + var/obj/vehicle/ride = thing + if(ride.is_occupant(user)) + return TRUE + return isliving(user) && user.buckled == thing + +/// The user climbed into a mech or onto a vehicle the field already holds: it runs at full speed from now on. +/datum/proximity_monitor/advanced/timestop/sandevistan/proc/let_go_own_ride() + if(!isliving(host)) + return + var/mob/living/user = host + for(var/atom/movable/ride in list(user.loc, user.buckled)) + if(frozen_things[ride] && own_ride(ride)) + unfreeze_atom(ride) + /datum/proximity_monitor/advanced/timestop/sandevistan/into_the_negative_zone(atom/A) tinted[A] = TRUE var/wait = ring_wait(A) diff --git a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm index 9db2e915d11..6fc3fda5f7b 100644 --- a/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm +++ b/voidcrew/modules/unit_tests/voidcrew_cyberware_timestop.dm @@ -198,6 +198,36 @@ user.remove_status_effect(/datum/status_effect/cyberware_cascade_window) user.remove_status_effect(/datum/status_effect/cyberware_cascade_crash) +/// The user's own mech runs at full speed in their slow-mo, whether they were in it at the surge or climbed in after +/datum/unit_test/voidcrew_cyberware_timestop/own_mech_free + +/datum/unit_test/voidcrew_cyberware_timestop/own_mech_free/Run() + var/mob/living/carbon/human/user = make_user(spot(0, 0), "sandytimestopuser") + var/obj/vehicle/sealed/mecha/ripley/mech = allocate(/obj/vehicle/sealed/mecha/ripley, spot(0, 0)) + var/obj/vehicle/sealed/mecha/ripley/other_mech = allocate(/obj/vehicle/sealed/mecha/ripley, spot(2, 2)) + user.forceMove(mech) // moved_inside() wants a client + if(!mech.is_occupant(user)) + mech.add_occupant(user) + TEST_ASSERT(mech.is_occupant(user), "Test fixture: the user isn't in the mech") + var/datum/status_effect/cyberware_cascade_window/window = surge(user) + sleep(0.1 SECONDS) + TEST_ASSERT_NOTNULL(window.time_stop, "Time didn't slow with nobody else near") + var/datum/proximity_monitor/advanced/timestop/sandevistan/field = window.time_stop.chronofield + TEST_ASSERT(!field.held_mechs[mech], "The user's own mech was slowed") + TEST_ASSERT(!tinted(mech), "The user's own mech was tinted") + TEST_ASSERT(field.held_mechs[other_mech], "An empty mech in the field wasn't slowed") + + // Out of the first mech and into the slowed one: it's let go on the next tick. + mech.mob_exit(user, silent = TRUE) + user.forceMove(other_mech) // moved_inside() wants a client + if(!other_mech.is_occupant(user)) + other_mech.add_occupant(user) + sleep(0.3 SECONDS) + TEST_ASSERT(!field.held_mechs[other_mech], "A mech the user climbed into stayed slowed") + TEST_ASSERT(!tinted(other_mech), "A mech the user climbed into stayed tinted") + other_mech.mob_exit(user, silent = TRUE) + end_surge(user) + /// A player hidden in a locker, or picked up and carried as an item, still counts as someone near /datum/unit_test/voidcrew_cyberware_timestop/witnessed_hidden