From 9d14e327b53ba01b5b3148e6207bb7d0caba4f4e Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:48:57 -0700 Subject: [PATCH] Begins Improving Sparks/Flares Somewhat, Adds Animatable Light Overlays (#95362) ## About The Pull Request [Adds a visual tick helper, integrates it into SSmove and such](https://github.com/tgstation/tgstation/commit/e97035f9f74fad5c67c5bf19d8d5d3bb4bd476b4) Basically, if we do "stuff" during verb time then the next chance clients have to actually see it is on the next visual tick (rather then the normal "this tick"). This is cause clients get their next frame during maptick, and maptick runs before verbs. We want to be able to handle this properly because if you say, create an object and then move it on the same VISUAL tick (NOT game tick), it will just teleport instead of playing out the move. I don't want this for stuff like sparks, so we need a way to work around it. [Moves most users of the _FAST flag to _INSTANT](https://github.com/tgstation/tgstation/commit/6f96daac00519c69adc7554f52114798a65f3ad5) These are the kids that don't immediately spawn something and the move it, and we want to allow them to move actually as soon as possible (important for stuff like space) [Improves basic effect systems, makes their products delete when they stop moving](https://github.com/tgstation/tgstation/commit/172cb25d80ed34e1ec523172a1677fb524239fba) Moves some stuff out to getters or vars so children can better decide how long effects should last/how fast they should move. Uses this to clean up weird dupe code used by explosions. Makes all these effects delete on contact with something that stops them. I'm doing this because an effect just hanging in the air looks really really odd. Does have consequences for sparks that are already moving at a wall though, might need a better way to handle that. Makes all these effects use _FAST loops so they don't just hang in the air for a second on spawn Adds a setter proc on sparks for their duration, gonna use this to improve their effects some [Refactors overlay lights, adds support for animating their images](https://github.com/tgstation/tgstation/commit/3ad0083cf2b536df51a6d93dca40eac20c1d62d1) Implements light_render_source and relevant setters, this allows us to replace the components of an overlay light with basically whatever we want Refactors overlay lighting to handle its images more consistently, allowing us to hook into an image being modified Combining the two of these will allow us to consistently copy a light's image, modify it in some way, and then relay that modification back down. Allowing us to animate it or do more advanced effects painlessly Also, fixes ranges of 1 or less not rendering at all on initial set (thank you kapu) [In which I get fed up and add a macro helper for UID generation](https://github.com/tgstation/tgstation/commit/aab48b03d407104d4f9cf9acb034494237def911) [adds vv hooking for all existing lighting vars](https://github.com/tgstation/tgstation/commit/b81c6200a0d74c36b440aa3f4c1f22c422090a2d) [Upgrade effect system's dir picking to avoid duplicates when possible](https://github.com/tgstation/tgstation/commit/18b622586b509c6be4c4bca4e3e7c175ad75fe91) [Uses the technique described above to animate spark's lights out as they move](https://github.com/tgstation/tgstation/commit/67ba177982213799984a70e89536c5efb3d17e14) This is a decently nice effect imo, it allows us to bump their power (read, alpha) since it'll get animated away. I try to sync the animation to the actual icon state's flow (it's 0.7s long). I also sped them up somewhat to hopefully have a nicer looking effect? we'll see. [Abstracts away intercepting overlay lights into a holder datum](https://github.com/tgstation/tgstation/pull/95362/commits/b3f1fe74f2c3bab1d8912ab8a666bd05677ad032) This should make it far easier to reuse this pattern! [Fixes overlay lights flashing to double intensity when picked up off the ground](https://github.com/tgstation/tgstation/pull/95362/commits/1d83f2031fa2b33312b2aea4359c0c37c9d04ac7) We needed to clear out their underlays BEFORE the animation [Adds a flickering effect to flares and their children](https://github.com/tgstation/tgstation/pull/95362/commits/b7a858e04a607c58b6c7fbe1476ffe2239e63bde) I'm still not 100% happy with this, I was trying to avoid it feeling like a heartbeat with random noise and I.. THINK it worked? it's honestly quite hard to tell [Adds the same flickering to lighters, welding tools and life candles](https://github.com/tgstation/tgstation/pull/95362/commits/3ec44027e17835ae96702cec5f0b12d1f4deb32b) Also, updated light candles to mirror the appearance of normal candles and use overlay lighting EDIT: I realized while working on flares that I accidentally double applied color, so if you saw the sparks animations before now it was different (less vibrant). IDK if I like this better or worse but it is RIGHT and that's what matters. ## Why It's Good For The Game I got mad about how bad these looked, and this is a start at improving them. Also, adds a framework for more dynamic effects applied to overlay lights (you could use this to apply a sort of "emergency rotating" effect, or flicker/buzz for example).
Before https://github.com/user-attachments/assets/66437f27-ee3c-4f14-a7ee-4a1c3e68533a https://github.com/user-attachments/assets/ed14fff8-a7eb-47fe-bab5-9a490ac96629
After https://github.com/user-attachments/assets/fb24ff2e-c745-42a5-8e11-c8a1eeef35a5 https://github.com/user-attachments/assets/fd8c2116-cb92-4fe6-ad3e-786a6538e52a
## Changelog :cl: add: Reworks how sparks render. They're now a bit brighter, will fade out as they move/if they hit something, will stack with each other less and also won't start hang in the air on spawn. add: Added a flickering effect to lighters, welding tools, flares, torches and candles (since they're flames). fix: Overlay based lights (think flashlights) will no longer flash to double intensity while being picked up. refactor: Reworked how some effects (explosion particles, sparks, some reagent stuff) function, report any bugs! /:cl: --- .../signals_atom/signals_atom_lighting.dm | 11 ++ .../dcs/signals/signals_light_intercept.dm | 2 + code/__DEFINES/dcs/signals/signals_object.dm | 4 + code/__DEFINES/lighting.dm | 5 + code/__DEFINES/maths.dm | 6 + code/__DEFINES/movement.dm | 13 +- code/__DEFINES/visual_helpers.dm | 5 + code/controllers/subsystem/atoms.dm | 2 +- .../subsystem/movement/movement_types.dm | 11 +- .../movement/ai_movement_basic_avoidance.dm | 2 +- code/datums/components/overlay_lighting.dm | 119 ++++++++----- code/datums/drift_handler.dm | 2 +- code/game/atom/_atom.dm | 14 +- code/game/atom/atom_vv.dm | 21 +++ .../effects/effect_system/effect_system.dm | 53 ++++-- .../effect_system/effects_explosion.dm | 22 +-- .../effects/effect_system/effects_sparks.dm | 51 +++++- .../effects/effect_system/effects_water.dm | 3 +- code/game/objects/items.dm | 2 + code/game/objects/items/devices/flashlight.dm | 12 ++ code/game/objects/items/lighter.dm | 15 ++ .../items/tools/engineering/weldingtool.dm | 14 ++ code/game/objects/items/tools/extinguisher.dm | 2 +- code/game/objects/structures/life_candle.dm | 22 ++- code/game/world.dm | 6 + .../atmospherics/machinery/datum_pipeline.dm | 2 +- code/modules/library/lib_machines.dm | 2 +- code/modules/lighting/light_middleman.dm | 160 ++++++++++++++++++ code/modules/lighting/lighting_atom.dm | 58 ++++--- code/modules/mining/mine_items.dm | 2 +- .../mecha/equipment/tools/other_tools.dm | 2 +- tgstation.dme | 2 + 32 files changed, 534 insertions(+), 113 deletions(-) create mode 100644 code/__DEFINES/dcs/signals/signals_light_intercept.dm create mode 100644 code/modules/lighting/light_middleman.dm diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_lighting.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_lighting.dm index 391ed0112ac..8e713f0260f 100644 --- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_lighting.dm +++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_lighting.dm @@ -39,6 +39,17 @@ #define COMSIG_ATOM_SET_LIGHT_FLAGS "atom_set_light_flags" ///Called right after the atom changes the value of light_flags to a different one, from base of [/atom/proc/set_light_flags]: (old_flags) #define COMSIG_ATOM_UPDATE_LIGHT_FLAGS "atom_update_light_flags" +///Called right before the atom changes the value of light_render_source to a different one, from base [atom/proc/set_light_render_source]: (new_render_source) +#define COMSIG_ATOM_SET_LIGHT_RENDER_SOURCE "atom_set_light_render_source" +///Called right after the atom changes the value of light_render_source to a different one, from base of [/atom/proc/set_light_render_source]: (old_render_source) +#define COMSIG_ATOM_UPDATE_LIGHT_RENDER_SOURCE "atom_update_light_render_source" ///Called when an atom has a light template applied to it. Frombase of [/datum/light_template/proc/mirror_onto]: () #define COMSIG_ATOM_LIGHT_TEMPLATE_MIRRORED "atom_light_template_mirrored" + +///Called when an atom's overlay component applies visuals, from base of [/datum/component/overlay_lighting/proc/show_to_holder]: (atom/movable/light_holder) +#define COMSIG_ATOM_OVERLAY_LIGHT_APPLIED "atom_overlay_light_applied" + +///Called when an atom's overlay component hides its visuals, from base of [/datum/component/overlay_lighting/proc/hide_from_holder]: (atom/movable/light_holder) +#define COMSIG_ATOM_OVERLAY_LIGHT_REMOVED "atom_overlay_light_removed" + diff --git a/code/__DEFINES/dcs/signals/signals_light_intercept.dm b/code/__DEFINES/dcs/signals/signals_light_intercept.dm new file mode 100644 index 00000000000..8b4694d9d0e --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_light_intercept.dm @@ -0,0 +1,2 @@ +/// from base of [/datum/light_middleman/proc/light_modified] : () +#define COMSIG_LIGHT_MIDDLEMAN_UPDATED "light_middleman_updated" diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index 3bd83c546d6..319e933cd07 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -151,6 +151,10 @@ #define COMSIG_ITEM_STORED "item_stored" ///from base of datum/storage/handle_exit(): (datum/storage/storage) #define COMSIG_ITEM_UNSTORED "item_unstored" +///from base of obj/item/do_pickup_animation(): () +#define COMSIG_ITEM_BEFORE_PICKUP_ANIMATION "item_before_pickup_animation" +///from base of obj/item/do_drop_animation(): () +#define COMSIG_ITEM_BEFORE_DROP_ANIMATION "item_before_drop_animation" /** * From base of datum/strippable_item/get_alternate_actions(): (atom/owner, mob/user, list/alt_actions) diff --git a/code/__DEFINES/lighting.dm b/code/__DEFINES/lighting.dm index 08e76678fff..9b843a69519 100644 --- a/code/__DEFINES/lighting.dm +++ b/code/__DEFINES/lighting.dm @@ -11,6 +11,11 @@ /// Nonesensical value for light color, used for null checks. #define NONSENSICAL_VALUE -99999 +// Light systems that use the overlay light component +#define IS_OVERLAY_LIGHT_SYSTEM(system) (system == OVERLAY_LIGHT || system == OVERLAY_LIGHT_DIRECTIONAL || system == OVERLAY_LIGHT_BEAM) +// Light systems that use the cone image of the overlay light component +#define IS_OVERLAY_CONE_LIGHT_SYSTEM(system) (system == OVERLAY_LIGHT_DIRECTIONAL || system == OVERLAY_LIGHT_BEAM) + /// Is our overlay light source attached to another movable (its loc), meaning that the lighting component should go one level deeper. #define LIGHT_ATTACHED (1<<0) /// Freezes a light in its current state, blocking any attempts at modification diff --git a/code/__DEFINES/maths.dm b/code/__DEFINES/maths.dm index c4f95c49eae..d2fe40a502c 100644 --- a/code/__DEFINES/maths.dm +++ b/code/__DEFINES/maths.dm @@ -53,6 +53,9 @@ /// Increments a value and wraps it if it exceeds some value. Can be used to circularly iterate through a list through `idx = WRAP_UP(idx, length_of_list)`. #define WRAP_UP(val, max) (((val) % (max)) + 1) +/// Helper that increments and wraps the passed in number when it hits the integer limit +#define WRAP_UID(val) WRAP_UP(val, SHORT_REAL_LIMIT - 1) + // Real modulus that handles decimals #define MODULUS(x, y) ( (x) - FLOOR(x, y)) @@ -233,6 +236,9 @@ #define LORENTZ_DISTRIBUTION(x, s) ( s*tan(TODEGREES(PI*(rand()-0.5))) + x ) #define LORENTZ_CUMULATIVE_DISTRIBUTION(x, y, s) ( (1/PI)*TORADIANS(arctan((x-(y))/s)) + 1/2 ) +/// Fucked up like upside down cauchy dist that I've pinned to 1 so I can use it as a multiplier +/// https://www.desmos.com/calculator/bt4tfavvi7 +#define ANCHORED_INVERSE_CAUCHY(s) (2 - ( 1 / (s * (1 + ((rand() - 0.5) / s) ** 2 ))) * (s + (0.5 ** 2) / s)) #define RULE_OF_THREE(a, b, x) ((a*x)/b) diff --git a/code/__DEFINES/movement.dm b/code/__DEFINES/movement.dm index e07fe65552d..57b7825bcc9 100644 --- a/code/__DEFINES/movement.dm +++ b/code/__DEFINES/movement.dm @@ -29,15 +29,18 @@ GLOBAL_VAR_INIT(glide_size_multiplier, 1.0) //Movement loop flags ///Should the loop act immediately following its addition? -#define MOVEMENT_LOOP_START_FAST (1<<0) +#define MOVEMENT_LOOP_START_INSTANT (1<<0) +///Should the loop act as soon as is reasonable? +///(always 1 tick after the next visual tick, makes behavior consistent regardless of when the SS fires in the tick) +#define MOVEMENT_LOOP_START_FAST (1<<1) ///Do we not use the priority system? -#define MOVEMENT_LOOP_IGNORE_PRIORITY (1<<1) +#define MOVEMENT_LOOP_IGNORE_PRIORITY (1<<2) ///Should we override the loop's glide? -#define MOVEMENT_LOOP_IGNORE_GLIDE (1<<2) +#define MOVEMENT_LOOP_IGNORE_GLIDE (1<<3) ///Should we not update our movables dir on move? -#define MOVEMENT_LOOP_NO_DIR_UPDATE (1<<3) +#define MOVEMENT_LOOP_NO_DIR_UPDATE (1<<4) ///Is the loop moving the movable outside its control, like it's an external force? e.g. footsteps won't play if enabled. -#define MOVEMENT_LOOP_OUTSIDE_CONTROL (1<<4) +#define MOVEMENT_LOOP_OUTSIDE_CONTROL (1<<5) // Movement loop status flags /// Has the loop been paused, soon to be resumed? diff --git a/code/__DEFINES/visual_helpers.dm b/code/__DEFINES/visual_helpers.dm index fda7f664809..d3cd58f672b 100644 --- a/code/__DEFINES/visual_helpers.dm +++ b/code/__DEFINES/visual_helpers.dm @@ -23,3 +23,8 @@ /// Much like [SET_BASE_PIXEL], except it will not effect pixel offsets in mapping programs #define SET_BASE_PIXEL_NOMAP(x, y) MAP_SWITCH(SET_BASE_PIXEL(x, y), _SET_BASE_PIXEL_NO_OFFSET(x, y)) + +/// What world.time will we next complete a visual tick? +/// This is important because we often want to avoid moving an object twice in a tick, since this can lead to teleportation instead of gliding +/// (such as when you move an atom that just spawned) +#define NEXT_VISUAL_TICK (GLOB.last_maptick_time + world.tick_lag) diff --git a/code/controllers/subsystem/atoms.dm b/code/controllers/subsystem/atoms.dm index ad09a0caeb2..0ea51fe9624 100644 --- a/code/controllers/subsystem/atoms.dm +++ b/code/controllers/subsystem/atoms.dm @@ -47,7 +47,7 @@ SUBSYSTEM_DEF(atoms) // Generate a unique mapload source for this run of InitializeAtoms var/static/uid = 0 - uid = (uid + 1) % (SHORT_REAL_LIMIT - 1) + uid = WRAP_UID(uid + 1) var/source = "subsystem init [uid]" set_tracked_initalized(INITIALIZATION_INNEW_MAPLOAD, source) diff --git a/code/controllers/subsystem/movement/movement_types.dm b/code/controllers/subsystem/movement/movement_types.dm index 6acce747bbe..1aa16a42fa0 100644 --- a/code/controllers/subsystem/movement/movement_types.dm +++ b/code/controllers/subsystem/movement/movement_types.dm @@ -59,11 +59,16 @@ SEND_SIGNAL(src, COMSIG_MOVELOOP_START) status |= MOVELOOP_STATUS_RUNNING //If this is our first time starting to move with this loop - //And we're meant to start instantly + //And we want to start consistently fast if(!timer && flags & MOVEMENT_LOOP_START_FAST) - timer = world.time + // + tick_lag because we want to avoid weird jumping in atoms that were just created (and avoid inconsistencies around subsystem timing) + timer = NEXT_VISUAL_TICK + world.tick_lag return - timer = world.time + delay + //And we're meant to start instantly + if(!timer && flags & MOVEMENT_LOOP_START_INSTANT) + timer = NEXT_VISUAL_TICK + return + timer = NEXT_VISUAL_TICK + delay ///Called when a loop is stopped, doesn't stop the loop itself /datum/move_loop/proc/loop_stopped() diff --git a/code/datums/ai/movement/ai_movement_basic_avoidance.dm b/code/datums/ai/movement/ai_movement_basic_avoidance.dm index 6659da244f3..169301f7047 100644 --- a/code/datums/ai/movement/ai_movement_basic_avoidance.dm +++ b/code/datums/ai/movement/ai_movement_basic_avoidance.dm @@ -21,4 +21,4 @@ /// Move immediately and don't update our facing /datum/ai_movement/basic_avoidance/backstep - move_flags = MOVEMENT_LOOP_START_FAST | MOVEMENT_LOOP_NO_DIR_UPDATE + move_flags = MOVEMENT_LOOP_START_INSTANT | MOVEMENT_LOOP_NO_DIR_UPDATE diff --git a/code/datums/components/overlay_lighting.dm b/code/datums/components/overlay_lighting.dm index ca8d4e7b23d..8f07b57d912 100644 --- a/code/datums/components/overlay_lighting.dm +++ b/code/datums/components/overlay_lighting.dm @@ -64,6 +64,8 @@ var/beam = FALSE ///A cone overlay for directional light, its alpha and color are dependent on the light var/image/cone + /// Are we currently displaying light on our holder? + var/currently_displaying = FALSE ///Current tracked direction for the directional cast behaviour var/current_direction ///Tracks current directional x offset so we don't update unnecessarily @@ -78,7 +80,7 @@ return COMPONENT_INCOMPATIBLE var/atom/movable/movable_parent = parent - if(!force && movable_parent.light_system != OVERLAY_LIGHT && movable_parent.light_system != OVERLAY_LIGHT_DIRECTIONAL && movable_parent.light_system != OVERLAY_LIGHT_BEAM) + if(!force && !IS_OVERLAY_LIGHT_SYSTEM(movable_parent.light_system)) stack_trace("[type] added to [parent], with [movable_parent.light_system] value for the light_system var. Use [OVERLAY_LIGHT], [OVERLAY_LIGHT_DIRECTIONAL] or [OVERLAY_LIGHT_BEAM] instead.") return COMPONENT_INCOMPATIBLE @@ -111,7 +113,7 @@ set_color(parent, movable_parent.light_color) if(!isnull(starts_on)) movable_parent.set_light_on(starts_on) - + set_light_render_source(parent, "") /datum/component/overlay_lighting/RegisterWithParent() . = ..() @@ -122,10 +124,12 @@ RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_COLOR, PROC_REF(set_color)) RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_ON, PROC_REF(on_toggle)) RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_FLAGS, PROC_REF(on_light_flags_change)) + RegisterSignal(parent, COMSIG_ATOM_SET_LIGHT_RENDER_SOURCE, PROC_REF(set_light_render_source)) RegisterSignal(parent, COMSIG_ATOM_USED_IN_CRAFT, PROC_REF(on_parent_crafted)) RegisterSignal(parent, COMSIG_LIGHT_EATER_QUEUE, PROC_REF(on_light_eater)) RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_parent_moved)) RegisterSignal(parent, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(on_z_move)) + RegisterSignal(parent, COMSIG_ITEM_BEFORE_PICKUP_ANIMATION, PROC_REF(on_pickup_anim)) var/atom/movable/movable_parent = parent if(movable_parent.light_flags & LIGHT_ATTACHED) overlay_lighting_flags |= LIGHTING_ATTACHED @@ -200,18 +204,44 @@ ///Adds the luminosity and source for the affected movable atoms to keep track of their visibility. /datum/component/overlay_lighting/proc/add_dynamic_lumi() LAZYSET(current_holder.affected_dynamic_lights, src, lumcount_range + 1) - current_holder.underlays += visible_mask + show_to_holder() current_holder.update_dynamic_luminosity() - if(directional) - current_holder.underlays += cone ///Removes the luminosity and source for the affected movable atoms to keep track of their visibility. /datum/component/overlay_lighting/proc/remove_dynamic_lumi() LAZYREMOVE(current_holder.affected_dynamic_lights, src) - current_holder.underlays -= visible_mask + hide_from_holder() current_holder.update_dynamic_luminosity() + +/// Adds our overlays to our holder, assuming everything's setup proper +/datum/component/overlay_lighting/proc/show_to_holder() + if(currently_displaying) + return + if(isnull(current_holder) || !(overlay_lighting_flags & LIGHTING_ON)) + currently_displaying = FALSE + return + current_holder.underlays += visible_mask + if(directional) + current_holder.underlays += cone + currently_displaying = TRUE + // These are very intentionally copies so recipients cannot + // Accidentially brick lighting overlays by mutating them + var/mutable_appearance/mask_clone = new (visible_mask) + var/mutable_appearance/cone_clone = directional ? new /mutable_appearance(cone) : null + SEND_SIGNAL(parent, COMSIG_ATOM_OVERLAY_LIGHT_APPLIED, mask_clone, cone_clone, current_holder) + +/// Removes our overlay from our holder, assuming everything's setup proper +/// MUST be called before modifying cone or visible_mask, or you will cause stuck lighting +/datum/component/overlay_lighting/proc/hide_from_holder() + if(!currently_displaying) + return + if(isnull(current_holder) || !(overlay_lighting_flags & LIGHTING_ON)) + return + current_holder.underlays -= visible_mask if(directional) current_holder.underlays -= cone + currently_displaying = FALSE + SEND_SIGNAL(parent, COMSIG_ATOM_OVERLAY_LIGHT_REMOVED, current_holder) ///Called to change the value of parent_attached_to. /datum/component/overlay_lighting/proc/set_parent_attached_to(atom/movable/new_parent_attached_to) @@ -321,15 +351,16 @@ /datum/component/overlay_lighting/proc/on_z_move(atom/source) SIGNAL_HANDLER - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays -= visible_mask - current_holder.underlays -= cone + hide_from_holder() SET_PLANE_EXPLICIT(visible_mask, O_LIGHTING_VISUAL_PLANE, source) if(cone) SET_PLANE_EXPLICIT(cone, O_LIGHTING_VISUAL_PLANE, source) - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays += visible_mask - current_holder.underlays += cone + show_to_holder() + +// Avoids duplicate overlays (one from our NEXT holder, selected after the animation, one from the pickup animation) +/datum/component/overlay_lighting/proc/on_pickup_anim(atom/source) + SIGNAL_HANDLER + hide_from_holder() ///Called when the current_holder is qdeleted, to remove the light effect. /datum/component/overlay_lighting/proc/on_parent_attached_to_qdel(atom/movable/source, force) @@ -364,20 +395,18 @@ range = clamp(CEILING(new_range, 0.5), 1, 6) var/pixel_bounds = ((range - 1) * 64) + 32 lumcount_range = CEILING(range, 1) - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays -= visible_mask + hide_from_holder() + visible_mask.icon = light_overlays["[pixel_bounds]"] if(pixel_bounds == 32) - if(!directional) // it's important that we make it to the end of this function if we are a directional light - visible_mask.transform = null - return + visible_mask.transform = null else var/offset = (pixel_bounds - 32) * 0.5 var/matrix/transform = new transform.Translate(-offset, -offset) visible_mask.transform = transform - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays += visible_mask + + show_to_holder() if(directional) if(beam) cast_range = max(round(new_range * 0.5), 1) @@ -393,39 +422,24 @@ var/new_power = source.light_power set_lum_power(new_power >= 0 ? 0.5 : -0.5) set_alpha = min(230, (abs(new_power) * 120) + 30) - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays -= visible_mask + hide_from_holder() visible_mask.alpha = set_alpha visible_mask.blend_mode = new_power > 0 ? BLEND_ADD : BLEND_SUBTRACT - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays += visible_mask - if(!directional) - return - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays -= cone - cone.alpha = min(120, (abs(new_power) * 60) + 15) - cone.blend_mode = new_power > 0 ? BLEND_ADD : BLEND_SUBTRACT - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays += cone + if(directional) + cone.alpha = min(120, (abs(new_power) * 60) + 15) + cone.blend_mode = new_power > 0 ? BLEND_ADD : BLEND_SUBTRACT + show_to_holder() ///Changes the light's color, pretty straightforward. /datum/component/overlay_lighting/proc/set_color(atom/source, old_color) SIGNAL_HANDLER var/new_color = source.light_color - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays -= visible_mask + hide_from_holder() visible_mask.color = new_color - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays += visible_mask - if(!directional) - return - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays -= cone - cone.color = new_color - if(current_holder && overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays += cone - + if(directional) + cone.color = new_color + show_to_holder() ///Toggles the light on and off. /datum/component/overlay_lighting/proc/on_toggle(atom/source, old_value) @@ -436,7 +450,6 @@ return turn_off() //Falsey value, turn off. - ///Triggered right after the parent light flags change. /datum/component/overlay_lighting/proc/on_light_flags_change(atom/source, old_flags) SIGNAL_HANDLER @@ -453,6 +466,18 @@ overlay_lighting_flags &= ~LIGHTING_ATTACHED set_parent_attached_to(null) +///Changes the light's color, pretty straightforward. +/datum/component/overlay_lighting/proc/set_light_render_source(atom/source, old_render_source) + SIGNAL_HANDLER + var/new_source = source.light_render_source + hide_from_holder() + visible_mask.render_source = new_source + if(directional) + var/new_cone_source = "" + if(new_source) + new_cone_source = "[new_source]_cone" + cone.render_source = new_cone_source + show_to_holder() ///Toggles the light on. /datum/component/overlay_lighting/proc/turn_on() @@ -504,7 +529,7 @@ break scanning = next_turf - current_holder.underlays -= visible_mask + hide_from_holder() var/translate_x = -((range - 1) * 32) var/translate_y = translate_x @@ -536,8 +561,8 @@ transform.Scale(scale_x, scale_y) transform.Translate(translate_x, translate_y) visible_mask.transform = transform - if(overlay_lighting_flags & LIGHTING_ON) - current_holder.underlays += visible_mask + + show_to_holder() ///Called when current_holder changes loc. /datum/component/overlay_lighting/proc/on_holder_dir_change(atom/movable/source, olddir, newdir) diff --git a/code/datums/drift_handler.dm b/code/datums/drift_handler.dm index 53133025657..0508123c138 100644 --- a/code/datums/drift_handler.dm +++ b/code/datums/drift_handler.dm @@ -23,7 +23,7 @@ parent.drift_handler = src var/flags = MOVEMENT_LOOP_OUTSIDE_CONTROL if(instant) - flags |= MOVEMENT_LOOP_START_FAST + flags |= MOVEMENT_LOOP_START_INSTANT src.drift_force = drift_force drifting_loop = GLOB.move_manager.smooth_move(moving = parent, angle = inertia_angle, delay = get_loop_delay(parent), subsystem = SSnewtonian_movement, priority = MOVEMENT_SPACE_PRIORITY, flags = flags) diff --git a/code/game/atom/_atom.dm b/code/game/atom/_atom.dm index 73a29c11eca..76c5744c0f9 100644 --- a/code/game/atom/_atom.dm +++ b/code/game/atom/_atom.dm @@ -73,17 +73,23 @@ var/light_power = 1 ///Hexadecimal RGB string representing the colour of the light. White by default. var/light_color = COLOR_WHITE + ///Boolean variable for toggleable lights. Has no effect without the proper light_system, light_range and light_power values. + var/light_on = TRUE + ///Bitflags to determine lighting-related atom properties. + var/light_flags = NONE + + // OVERLAY_LIGHT only values + /// An optional render_source to apply to this atom's light overlay + var/light_render_source = "" + + // COMPLEX_LIGHT only values /// Angle of light to show in light_dir /// 360 is a circle, 90 is a cone, etc. var/light_angle = 360 /// What angle to project light in var/light_dir = NORTH - ///Boolean variable for toggleable lights. Has no effect without the proper light_system, light_range and light_power values. - var/light_on = TRUE /// How many tiles "up" this light is. 1 is typical, should only really change this if it's a floor light var/light_height = LIGHTING_HEIGHT - ///Bitflags to determine lighting-related atom properties. - var/light_flags = NONE ///Our light source. Don't fuck with this directly unless you have a good reason! var/tmp/datum/light_source/light ///Any light sources that are "inside" of us, for example, if src here was a mob that's carrying a flashlight, that flashlight's light source would be part of this list. diff --git a/code/game/atom/atom_vv.dm b/code/game/atom/atom_vv.dm index 14e20d23b9d..d9600d4bae9 100644 --- a/code/game/atom/atom_vv.dm +++ b/code/game/atom/atom_vv.dm @@ -263,6 +263,27 @@ // I'm sorry old_light_flags = var_value . = TRUE + if(NAMEOF(src, light_render_source)) + set_light_render_source(var_value) + . = TRUE + if(NAMEOF(src, light_angle)) + if(light_system == COMPLEX_LIGHT) + set_light(l_angle = var_value) + else + set_light_angle(var_value) + . = TRUE + if(NAMEOF(src, light_dir)) + if(light_system == COMPLEX_LIGHT) + set_light(l_dir = var_value) + else + set_light_dir(var_value) + . = TRUE + if(NAMEOF(src, light_height)) + if(light_system == COMPLEX_LIGHT) + set_light(l_height = var_value) + else + set_light_height(var_value) + . = TRUE if(NAMEOF(src, smoothing_junction)) set_smoothed_icon_state(var_value) . = TRUE diff --git a/code/game/objects/effects/effect_system/effect_system.dm b/code/game/objects/effects/effect_system/effect_system.dm index c91b2168454..5bd2f3267ca 100644 --- a/code/game/objects/effects/effect_system/effect_system.dm +++ b/code/game/objects/effects/effect_system/effect_system.dm @@ -57,6 +57,16 @@ var/total_effects = 0 /// Should the system delete itself after finishing? var/autocleanup = FALSE + /// Should the system delete effects that stop moving? + var/delete_on_stop = FALSE + /// How frequently (in deciseconds) should we move our particles? + var/step_delay = 0.5 SECONDS + + // Internal use + /// The length of the previous assigned moveloop in deciseconds + var/last_loop_length = 0 + /// List of dirs avalible to pick, used to avoid accidential duplicates + var/list/pickable_dirs = list() /datum/effect_system/basic/New(turf/location, amount = null, cardinals_only = null) . = ..() @@ -73,25 +83,48 @@ return generate_effect() +/// Returns how many steps to attempt to move a generated effect +/datum/effect_system/basic/proc/get_step_count() + return rand(1, 3) + +/// Generates a effect for our system to control, returns the generated effect /datum/effect_system/basic/proc/generate_effect() if(holder) location = get_turf(holder) var/obj/effect/effect = new effect_type(location) total_effects++ - var/direction - if(cardinals_only) - direction = pick(GLOB.cardinals) - else - direction = pick(GLOB.alldirs) - var/step_amt = rand(1, 3) - var/step_delay = 5 - var/datum/move_loop/loop = GLOB.move_manager.move(effect, direction, step_delay, timeout = step_delay * step_amt, priority = MOVEMENT_ABOVE_SPACE_PRIORITY) - RegisterSignal(loop, COMSIG_QDELETING, PROC_REF(decrement_total_effect)) + if(!length(pickable_dirs)) + if(cardinals_only) + pickable_dirs = GLOB.cardinals.Copy() + else + pickable_dirs = GLOB.alldirs.Copy() + // Try not to reuse dirs if possible to avoid weird stacking + var/direction = pick_n_take(pickable_dirs) -/datum/effect_system/basic/proc/decrement_total_effect(datum/source) + var/step_count = get_step_count() + var/datum/move_loop/loop = GLOB.move_manager.move(effect, direction, step_delay, timeout = step_delay * step_count, priority = MOVEMENT_ABOVE_SPACE_PRIORITY, flags = MOVEMENT_LOOP_START_FAST) + RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move)) + RegisterSignal(loop, COMSIG_QDELETING, PROC_REF(loop_end)) + last_loop_length = loop.lifetime + return effect + +/datum/effect_system/basic/proc/post_move(datum/move_loop/source, result, visual_delay) + SIGNAL_HANDLER + if(result == MOVELOOP_FAILURE) + move_failed(source, source.moving) + +/// Allows us to hook into being unable to automatically move +/datum/effect_system/basic/proc/move_failed(datum/move_loop/loop, obj/effect/failed) + if(QDELETED(failed) || !delete_on_stop) + return + qdel(failed) + +/datum/effect_system/basic/proc/loop_end(datum/move_loop/source) SIGNAL_HANDLER total_effects-- + if(delete_on_stop && !QDELETED(source.moving)) + qdel(source.moving) if(autocleanup && total_effects == 0) QDEL_IN(src, 2 SECONDS) diff --git a/code/game/objects/effects/effect_system/effects_explosion.dm b/code/game/objects/effects/effect_system/effects_explosion.dm index 9f4db05d967..5e9ff4e1ade 100644 --- a/code/game/objects/effects/effect_system/effects_explosion.dm +++ b/code/game/objects/effects/effect_system/effects_explosion.dm @@ -4,14 +4,6 @@ opacity = TRUE anchored = TRUE -/obj/effect/particle_effect/expl_particles/Initialize(mapload) - ..() - return INITIALIZE_HINT_LATELOAD - -/obj/effect/particle_effect/expl_particles/LateInitialize() - var/step_amt = pick(25;1, 50;2, 100;3, 200;4) - var/datum/move_loop/loop = GLOB.move_manager.move(src, pick(GLOB.alldirs), 1, timeout = step_amt, priority = MOVEMENT_ABOVE_SPACE_PRIORITY) - RegisterSignal(loop, COMSIG_QDELETING, PROC_REF(end_particle)) /obj/effect/particle_effect/expl_particles/proc/end_particle(datum/source) SIGNAL_HANDLER @@ -19,10 +11,20 @@ qdel(src) /datum/effect_system/basic/expl_particles + effect_type = /obj/effect/particle_effect/expl_particles amount = 10 + step_delay = 0.1 SECONDS + delete_on_stop = TRUE -/datum/effect_system/basic/expl_particles/generate_effect() - new /obj/effect/particle_effect/expl_particles(location) +/datum/effect_system/basic/expl_particles/get_step_count() + return pick(25;1, 50;2, 100;3, 200;4) + +/datum/effect_system/basic/expl_particles/loop_end(datum/move_loop/source) + . = ..() + var/obj/effect/explosion_particle = source.moving + if(QDELETED(explosion_particle)) + return + qdel(explosion_particle) /obj/effect/explosion name = "fire" diff --git a/code/game/objects/effects/effect_system/effects_sparks.dm b/code/game/objects/effects/effect_system/effects_sparks.dm index 8a40659a003..aaf1efa3fba 100644 --- a/code/game/objects/effects/effect_system/effects_sparks.dm +++ b/code/game/objects/effects/effect_system/effects_sparks.dm @@ -18,11 +18,20 @@ anchored = TRUE light_system = OVERLAY_LIGHT light_range = 1.5 - light_power = 0.8 + light_power = 2 light_color = LIGHT_COLOR_FIRE + /// Should this spark's effect be animated + var/animated = TRUE + /// Timer id for the timer that will wipe us out + var/delete_timer_id = TIMER_ID_NULL + /// Middleman object we're using to animate our light + var/datum/light_middleman/middleman /obj/effect/particle_effect/sparks/Initialize(mapload) ..() + if(animated) + middleman = new(src, "sparks") + middleman.being_overriding_light() return INITIALIZE_HINT_LATELOAD /obj/effect/particle_effect/sparks/LateInitialize() @@ -32,7 +41,29 @@ var/turf/location = loc if(isturf(location)) affect_location(location, just_initialized = TRUE) - QDEL_IN(src, 2 SECONDS) + decay_in(2 SECONDS) + +/obj/effect/particle_effect/sparks/Destroy() + if(!isnull(middleman)) + QDEL_NULL(middleman) + return ..() + +/// Sets up our death effects given the passed in duration +/obj/effect/particle_effect/sparks/proc/decay_in(decay_time) + if(delete_timer_id != TIMER_ID_NULL) + deltimer(delete_timer_id) + delete_timer_id = QDEL_IN_STOPPABLE(src, decay_time + world.tick_lag) + if(!animated) + return + var/obj/effect/abstract/main_light = middleman.primary_intercept + // We're going to fade our light out so it's less jarring when we fully disappear + // Note, a refresh of the overlay light would break this, we're basically just sorta assuming that won't happen + // Would need to track time and sort of "replay" where we should be otherwise + if(decay_time >= 0.7 SECONDS) // duration of all animated spark's actual icon state animation + animate(main_light, alpha = 220, time = 0.4 SECONDS) + animate(alpha = 0, time = decay_time - 0.4 SECONDS, easing = CIRCULAR_EASING | EASE_IN) + else + animate(main_light, alpha = 0, time = decay_time) /obj/effect/particle_effect/sparks/Destroy() var/turf/location = loc @@ -91,6 +122,20 @@ /datum/effect_system/basic/spark_spread effect_type = /obj/effect/particle_effect/sparks + step_delay = 0.35 SECONDS // chosen so we will always take at least the duration of our animation to finish + +/datum/effect_system/basic/spark_spread/generate_effect() + var/obj/effect/particle_effect/sparks/spark = ..() + spark.decay_in(last_loop_length) + +/datum/effect_system/basic/spark_spread/get_step_count() + return rand(2, 3) // never 1 cause 1 looks dumb + +/datum/effect_system/basic/spark_spread/move_failed(datum/move_loop/loop, obj/effect/failed) + if(QDELETED(failed)) + return + var/obj/effect/particle_effect/sparks/spark = failed + spark.decay_in(0.1 SECONDS) /datum/effect_system/basic/spark_spread/quantum effect_type = /obj/effect/particle_effect/sparks/quantum @@ -100,6 +145,8 @@ /obj/effect/particle_effect/sparks/electricity name = "lightning" icon_state = "electricity" + animated = FALSE /datum/effect_system/basic/lightning_spread + delete_on_stop = TRUE effect_type = /obj/effect/particle_effect/sparks/electricity diff --git a/code/game/objects/effects/effect_system/effects_water.dm b/code/game/objects/effects/effect_system/effects_water.dm index f8f62c13b1b..cbe4472614f 100644 --- a/code/game/objects/effects/effect_system/effects_water.dm +++ b/code/game/objects/effects/effect_system/effects_water.dm @@ -39,7 +39,7 @@ /// Starts the effect moving at a target with a delay in deciseconds, and a lifetime in moves /// Returns the created loop /obj/effect/particle_effect/water/extinguisher/proc/move_at(atom/target, delay, lifetime) - var/datum/move_loop/loop = GLOB.move_manager.move_towards_legacy(src, target, delay, timeout = delay * lifetime, flags = MOVEMENT_LOOP_START_FAST, priority = MOVEMENT_ABOVE_SPACE_PRIORITY) + var/datum/move_loop/loop = GLOB.move_manager.move_towards_legacy(src, target, delay, timeout = delay * lifetime, priority = MOVEMENT_ABOVE_SPACE_PRIORITY, flags = MOVEMENT_LOOP_START_FAST) RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_forcemove)) RegisterSignal(loop, COMSIG_QDELETING, PROC_REF(movement_stopped)) return loop @@ -88,4 +88,5 @@ QDEL_IN(src, 2 SECONDS) /datum/effect_system/basic/steam_spread + delete_on_stop = TRUE effect_type = /obj/effect/particle_effect/steam diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index ec0ff3340cd..21df3b187f2 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -1515,6 +1515,7 @@ if(!istype(loc, /turf)) return source = loc + SEND_SIGNAL(src, COMSIG_ITEM_BEFORE_PICKUP_ANIMATION) var/image/pickup_animation = image(icon = src) SET_PLANE(pickup_animation, GAME_PLANE, source) pickup_animation.transform.Scale(0.75) @@ -1551,6 +1552,7 @@ if(!istype(moving_from)) return + SEND_SIGNAL(src, COMSIG_ITEM_BEFORE_DROP_ANIMATION) var/turf/current_turf = get_turf(src) var/direction = get_dir(moving_from, current_turf) var/from_x = moving_from.base_pixel_x diff --git a/code/game/objects/items/devices/flashlight.dm b/code/game/objects/items/devices/flashlight.dm index f5a29b9f477..73e774205b5 100644 --- a/code/game/objects/items/devices/flashlight.dm +++ b/code/game/objects/items/devices/flashlight.dm @@ -465,9 +465,15 @@ /datum/material/plasma = SMALL_MATERIAL_AMOUNT * 0.5, /datum/material/plastic = SMALL_MATERIAL_AMOUNT * 0.5, ) + /// Lighting middleman, lets us do a flicker effect + var/datum/light_middleman/middleman /obj/item/flashlight/flare/Initialize(mapload) . = ..() + if(IS_OVERLAY_LIGHT_SYSTEM(light_system)) + middleman = new(src, "flashlight") + RegisterSignal(middleman, COMSIG_LIGHT_MIDDLEMAN_UPDATED, PROC_REF(light_updated)) + middleman.being_overriding_light() if(randomize_fuel) fuel = rand(10 MINUTES, 15 MINUTES) if(light_on) @@ -486,6 +492,8 @@ /obj/item/flashlight/flare/Destroy() STOP_PROCESSING(SSobj, src) + if(middleman) + QDEL_NULL(middleman) return ..() /obj/item/flashlight/flare/afterattack(atom/target, mob/user, click_parameters) @@ -519,6 +527,10 @@ damtype = initial(damtype) update_brightness() +/obj/item/flashlight/flare/proc/light_updated(datum/source) + SIGNAL_HANDLER + fire_flicker_middleman(middleman) + /obj/item/flashlight/flare/extinguish() . = ..() if((fuel != INFINITY) && can_be_extinguished) diff --git a/code/game/objects/items/lighter.dm b/code/game/objects/items/lighter.dm index 60a7bf5271e..77b373617ed 100644 --- a/code/game/objects/items/lighter.dm +++ b/code/game/objects/items/lighter.dm @@ -38,9 +38,15 @@ ) /// Whether the lighter starts with fuel var/spawns_with_reagent = TRUE + /// Lighting middleman, lets us do a flicker effect + var/datum/light_middleman/middleman /obj/item/lighter/Initialize(mapload) . = ..() + if(IS_OVERLAY_LIGHT_SYSTEM(light_system)) + middleman = new(src, "flashlight") + RegisterSignal(middleman, COMSIG_LIGHT_MIDDLEMAN_UPDATED, PROC_REF(light_updated)) + middleman.being_overriding_light() create_reagents(maximum_fuel, REFILLABLE | DRAINABLE) if(spawns_with_reagent) reagents.add_reagent(/datum/reagent/fuel, maximum_fuel) @@ -54,6 +60,11 @@ ) update_appearance() +/obj/item/lighter/Destroy(force) + if(!isnull(middleman)) + QDEL_NULL(middleman) + return ..() + /obj/item/lighter/grind_results() return list(/datum/reagent/iron = 1, /datum/reagent/fuel = 5, /datum/reagent/fuel/oil = 5) @@ -64,6 +75,10 @@ else . += span_notice("It contains [get_fuel()] units of fuel out of [maximum_fuel].") +/obj/item/lighter/proc/light_updated(datum/source) + SIGNAL_HANDLER + fire_flicker_middleman(middleman) + /// Destroy the lighter when it's shot by a bullet /obj/item/lighter/proc/on_intercepted_bullet(mob/living/victim, obj/projectile/bullet) victim.visible_message(span_warning("\The [bullet] shatters on [victim]'s lighter!")) diff --git a/code/game/objects/items/tools/engineering/weldingtool.dm b/code/game/objects/items/tools/engineering/weldingtool.dm index a82218e845d..68e6020b919 100644 --- a/code/game/objects/items/tools/engineering/weldingtool.dm +++ b/code/game/objects/items/tools/engineering/weldingtool.dm @@ -48,6 +48,8 @@ var/activation_sound = 'sound/items/tools/welderactivate.ogg' var/deactivation_sound = 'sound/items/tools/welderdeactivate.ogg' + /// Lighting middleman, lets us do a flicker effect + var/datum/light_middleman/middleman /datum/armor/item_weldingtool fire = 100 @@ -55,6 +57,10 @@ /obj/item/weldingtool/Initialize(mapload) . = ..() + if(IS_OVERLAY_LIGHT_SYSTEM(light_system)) + middleman = new(src, "flashlight") + RegisterSignal(middleman, COMSIG_LIGHT_MIDDLEMAN_UPDATED, PROC_REF(light_updated)) + middleman.being_overriding_light() AddElement(/datum/element/update_icon_updates_onmob) AddElement(/datum/element/tool_flash, light_range) AddElement(/datum/element/falling_hazard, damage = force, wound_bonus = wound_bonus, hardhat_safety = TRUE, crushes = FALSE, impact_sound = hitsound) @@ -64,6 +70,10 @@ reagents.add_reagent(/datum/reagent/fuel, max_fuel) update_appearance() +/obj/item/weldingtool/Destroy(force) + QDEL_NULL(middleman) + return ..() + /obj/item/weldingtool/update_icon_state() if(welding) inhand_icon_state = "[initial(inhand_icon_state)]1" @@ -145,6 +155,10 @@ return try_heal_loop(interacting_with, user) +/obj/item/weldingtool/proc/light_updated(datum/source) + SIGNAL_HANDLER + fire_flicker_middleman(middleman) + /obj/item/weldingtool/proc/try_heal_loop(atom/interacting_with, mob/living/user, repeating = FALSE) var/mob/living/carbon/human/attacked_humanoid = interacting_with var/obj/item/bodypart/affecting = attacked_humanoid.get_bodypart(check_zone(user.zone_selected)) diff --git a/code/game/objects/items/tools/extinguisher.dm b/code/game/objects/items/tools/extinguisher.dm index 9f58808a752..689d064c031 100644 --- a/code/game/objects/items/tools/extinguisher.dm +++ b/code/game/objects/items/tools/extinguisher.dm @@ -343,7 +343,7 @@ //Chair movement loop /obj/item/extinguisher/proc/move_chair(obj/buckled_object, movementdirection) - var/datum/move_loop/loop = GLOB.move_manager.move(buckled_object, movementdirection, 1, timeout = 9, flags = MOVEMENT_LOOP_START_FAST, priority = MOVEMENT_ABOVE_SPACE_PRIORITY) + var/datum/move_loop/loop = GLOB.move_manager.move(buckled_object, movementdirection, 1, timeout = 9, flags = MOVEMENT_LOOP_START_INSTANT, priority = MOVEMENT_ABOVE_SPACE_PRIORITY) //This means the chair slowing down is dependant on the extinguisher existing, which is weird //Couldn't figure out a better way though RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(manage_chair_speed)) diff --git a/code/game/objects/structures/life_candle.dm b/code/game/objects/structures/life_candle.dm index d9eb81c783c..b21d14619db 100644 --- a/code/game/objects/structures/life_candle.dm +++ b/code/game/objects/structures/life_candle.dm @@ -3,7 +3,11 @@ desc = "You are dead. Insert quarter to continue." icon = 'icons/obj/candle.dmi' icon_state = "candle1" + light_system = OVERLAY_LIGHT light_color = LIGHT_COLOR_FIRE + light_power = 1.5 + light_range = 2 + light_on = FALSE var/icon_state_active = "candle1_lit" var/icon_state_inactive = "candle1" @@ -23,11 +27,21 @@ // How long until we respawn them after their death. var/respawn_time = 50 var/respawn_sound = 'sound/effects/magic/staff_animation.ogg' + /// Lighting middleman, lets us do a flicker effect + var/datum/light_middleman/middleman /obj/structure/life_candle/Initialize(mapload) . = ..() + if(IS_OVERLAY_LIGHT_SYSTEM(light_system)) + middleman = new(src, "flashlight") + RegisterSignal(middleman, COMSIG_LIGHT_MIDDLEMAN_UPDATED, PROC_REF(light_updated)) + middleman.being_overriding_light() AddElement(/datum/element/movetype_handler) +/obj/structure/life_candle/Destroy(force) + QDEL_NULL(middleman) + return ..() + /obj/structure/life_candle/attack_hand(mob/user, list/modifiers) . = ..() if(.) @@ -48,10 +62,10 @@ update_appearance() if(linked_minds.len) START_PROCESSING(SSobj, src) - set_light(lit_luminosity) + set_light_on(TRUE) else STOP_PROCESSING(SSobj, src) - set_light(0) + set_light_on(FALSE) /obj/structure/life_candle/update_icon_state() icon_state = linked_minds.len ? icon_state_active : icon_state_inactive @@ -74,6 +88,10 @@ if(!mind.current || (mind.current && mind.current.stat == DEAD)) addtimer(CALLBACK(src, PROC_REF(respawn), mind), respawn_time, TIMER_UNIQUE) +/obj/structure/life_candle/proc/light_updated(datum/source) + SIGNAL_HANDLER + fire_flicker_middleman(middleman) + /obj/structure/life_candle/proc/respawn(datum/mind/mind) var/turf/T = get_turf(src) var/mob/living/body diff --git a/code/game/world.dm b/code/game/world.dm index e2370fedb1f..a243dd09e38 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -276,6 +276,12 @@ GLOBAL_VAR(restart_counter) world.log = file("[GLOB.log_directory]/dd.log") //not all runtimes trigger world/Error, so this is the only way to ensure we can see all of them. #endif +/// The world.time we last ran maptick, used for stupid reasons +GLOBAL_VAR_INIT(last_maptick_time, 0) +/world/Tick() + // We need a hook for if maptick has happen yet + GLOB.last_maptick_time = world.time + /world/Topic(T, addr, master, key) TGS_TOPIC //redirect to server tools if necessary diff --git a/code/modules/atmospherics/machinery/datum_pipeline.dm b/code/modules/atmospherics/machinery/datum_pipeline.dm index 9d9a5a6292f..1c713f47852 100644 --- a/code/modules/atmospherics/machinery/datum_pipeline.dm +++ b/code/modules/atmospherics/machinery/datum_pipeline.dm @@ -259,7 +259,7 @@ var/volume_sum = 0 var/static/process_id = 0 - process_id = (process_id + 1) % (SHORT_REAL_LIMIT - 1) + process_id = WRAP_UID(process_id + 1) for(var/datum/gas_mixture/gas_mixture as anything in gas_mixture_list) // Ensure we never walk the same mix twice diff --git a/code/modules/library/lib_machines.dm b/code/modules/library/lib_machines.dm index 62266481659..50e6d0ac22a 100644 --- a/code/modules/library/lib_machines.dm +++ b/code/modules/library/lib_machines.dm @@ -13,7 +13,7 @@ GLOBAL_VAR_INIT(library_table_modified, 0) /// Increments every time WE update the library db table, causes all existing consoles to repull when they next check /proc/library_updated() - GLOB.library_table_modified = (GLOB.library_table_modified + 1) % (SHORT_REAL_LIMIT - 1) + GLOB.library_table_modified = WRAP_UID(GLOB.library_table_modified + 1) /* * Library Public Computer diff --git a/code/modules/lighting/light_middleman.dm b/code/modules/lighting/light_middleman.dm new file mode 100644 index 00000000000..229c49a4edf --- /dev/null +++ b/code/modules/lighting/light_middleman.dm @@ -0,0 +1,160 @@ +/// Allows us to intercept overlay lighting's well, light overlays +/// Normally these are static, but by giving them a render source and copying their base appearance +/// Animating this datum's child objects allows us to do SO much fun stuff +/datum/light_middleman + /// Owning parent we're interceeding for + /// Could in theory be a turf but lies to areas means we have to pick something to type it as + var/atom/movable/parent + /// The holder we are currently displaying our light on + var/atom/movable/light_holder + /// Holds the primary light source + var/obj/effect/abstract/light_middleman/primary_intercept + /// Exists to hold the cone so children can modify it if they want + var/obj/effect/abstract/light_middleman/cone_intercept + /// Are we overriding the light already? + var/overriding = FALSE + /// Weakref to the object we are displaying our effects on + var/datum/weakref/holder_ref + +/datum/light_middleman/New(atom/parent, unique_string) + . = ..() + if(!IS_OVERLAY_LIGHT_SYSTEM(parent.light_system)) + stack_trace("Attempted to create a light middleman with a parent [parent.type] that does not use overlay lighting! This will not work.") + if(isturf(parent)) + stack_trace("Warning, becuase overlay lights are basically never used on turfs, since they don't move,\ + vis contents replacement has not yet been implemented for them (see changeturf for why this is needed)!") + src.parent = parent + primary_intercept = new() + cone_intercept = new() + var/static/uuid = 0 + uuid = WRAP_UID(uuid + 1) + primary_intercept.render_target = "*[unique_string]_[uuid]_target" + cone_intercept.render_target = "[primary_intercept.render_target]_cone" // made to mirror how overlay lights work + +/datum/light_middleman/Destroy(force) + stop_overriding_light() + QDEL_NULL(primary_intercept) + QDEL_NULL(cone_intercept) + parent = null + light_holder = null + return ..() + +/datum/light_middleman/proc/being_overriding_light(unique_string) + if(overriding) + return + overriding = TRUE + // We register here because our later set render source will always trigger a refresh and thus let us capture appearances properly + // Assuming there's an overlay light on the other side + RegisterSignal(parent, COMSIG_ATOM_OVERLAY_LIGHT_APPLIED, PROC_REF(light_applied)) + RegisterSignal(parent, COMSIG_ATOM_OVERLAY_LIGHT_REMOVED, PROC_REF(light_removed)) + parent.set_light_render_source(primary_intercept.render_target) + +/datum/light_middleman/proc/stop_overriding_light() + if(!overriding) + return + overriding = FALSE + UnregisterSignal(parent, COMSIG_ATOM_OVERLAY_LIGHT_APPLIED) + UnregisterSignal(parent, COMSIG_ATOM_OVERLAY_LIGHT_REMOVED) + var/atom/movable/old_holder = holder_ref?.resolve() + if(old_holder) + old_holder.vis_contents -= primary_intercept + old_holder.vis_contents -= cone_intercept + holder_ref = null + parent.set_light_render_source("") + +/datum/light_middleman/proc/light_applied(datum/source, image/visible_mask, image/cone, atom/movable/light_holder) + SIGNAL_HANDLER + var/atom/movable/old_holder = holder_ref?.resolve() + // If we were somewhere before, clean us out + if(old_holder) + old_holder.vis_contents -= primary_intercept + old_holder.vis_contents -= cone_intercept + holder_ref = null + + // how we make sure we're in the client's view + light_holder.vis_contents += primary_intercept + // Avoids unneeded effects clientside + if(IS_OVERLAY_CONE_LIGHT_SYSTEM(parent.light_system)) + light_holder.vis_contents += cone_intercept + + old_holder = WEAKREF(light_holder) + + var/old_target = primary_intercept.render_target + var/old_cone_target = cone_intercept.render_target + // This will halt any animations we have ongoing so if you care about that you've gotta react to it properly + primary_intercept.appearance = visible_mask + cone_intercept.appearance = cone + // set ourselves up to render back onto the visible mask + primary_intercept.render_source = "" + primary_intercept.render_target = old_target + cone_intercept.render_source = "" + cone_intercept.render_target = old_cone_target + // Dir is important I'm told + primary_intercept.vis_flags |= VIS_INHERIT_DIR + cone_intercept.vis_flags |= VIS_INHERIT_DIR + // Will double apply, here we go gang + primary_intercept.transform = null + cone_intercept.transform = null + primary_intercept.color = null + cone_intercept.color = null + primary_intercept.alpha = 255 + cone_intercept.alpha = 255 + // Sometimes can be BLEND_SUBTRACT, we don't want that + primary_intercept.blend_mode = BLEND_ADD + cone_intercept.blend_mode = BLEND_ADD + /// Allows users to hook into a refresh so they can remake their modifications to our intercepts + SEND_SIGNAL(src, COMSIG_LIGHT_MIDDLEMAN_UPDATED) + +/datum/light_middleman/proc/light_removed(datum/source, atom/movable/light_holder) + SIGNAL_HANDLER + light_holder.vis_contents -= primary_intercept + light_holder.vis_contents -= cone_intercept + holder_ref = null + +/// Just... cause it's better then not having a bespoke type +/obj/effect/abstract/light_middleman + +// Procs for reuse on multiple types +/proc/fire_flicker_middleman(datum/light_middleman/middleman) + var/obj/effect/abstract/main_light = middleman.primary_intercept + // Just in case a subtype is wildin + var/obj/effect/abstract/cone_light = middleman.cone_intercept + + /// Applies a nice random flicker to flares and their subtypes which will I hope sell the fire effect better + var/list/random_times = list() + for(var/i in 1 to 17) + // Makes a nice upside down U distribution + var/random_down = LERP(-0.075 SECONDS, 0.075 SECONDS, ANCHORED_INVERSE_CAUCHY(0.55)) + var/random_bottom = LERP(-0.05 SECONDS, 0.05 SECONDS, ANCHORED_INVERSE_CAUCHY(0.55)) + var/random_up = LERP(-0.075 SECONDS, 0.075 SECONDS, ANCHORED_INVERSE_CAUCHY(0.55)) + // We want a potentially quite long "top end" so the flicker can be an actual flicker instead of a heartbeat (that's the goal at least) + var/random_top = LERP(-0.225 SECONDS, 0.225 SECONDS, ANCHORED_INVERSE_CAUCHY(0.55)) + random_times += list(list( + 0.125 SECONDS + random_down, + 0 SECONDS + random_bottom, + 0.125 SECONDS + random_up, + 0.275 SECONDS + random_top, + )) + // Going to loop our alpha high and low semi quickly to mimik a flickering fire/flare + var/list/first_time = random_times[1] + animate(main_light, alpha = 235, time = first_time[1], easing = CUBIC_EASING|EASE_OUT, loop = -1) + animate(alpha = 235, time = first_time[2]) + animate(alpha = 255, time = first_time[3], easing = CUBIC_EASING|EASE_OUT) + animate(alpha = 255, time = first_time[4]) + for(var/list/time in random_times - first_time) + animate(alpha = 235, time = time[1], easing = CUBIC_EASING|EASE_OUT) + animate(alpha = 235, time = first_time[2]) + animate(alpha = 255, time = first_time[3], easing = CUBIC_EASING|EASE_OUT) + animate(alpha = 255, time = first_time[4]) + + // I'd really love to do both these in the same loop but parallel animations are the devil from the bible + // I don't think any of these are directional but just in case + animate(cone_light, alpha = 235, time = first_time[1], easing = CUBIC_EASING|EASE_OUT, loop = -1) + animate(alpha = 235, time = first_time[2]) + animate(alpha = 255, time = first_time[3], easing = CUBIC_EASING|EASE_OUT) + animate(alpha = 255, time = first_time[4]) + for(var/list/time in random_times - first_time) + animate(alpha = 235, time = time[1], easing = CUBIC_EASING|EASE_OUT) + animate(alpha = 235, time = first_time[2]) + animate(alpha = 255, time = first_time[3], easing = CUBIC_EASING|EASE_OUT) + animate(alpha = 255, time = first_time[4]) diff --git a/code/modules/lighting/lighting_atom.dm b/code/modules/lighting/lighting_atom.dm index b671de56cfc..fef97f72942 100644 --- a/code/modules/lighting/lighting_atom.dm +++ b/code/modules/lighting/lighting_atom.dm @@ -139,6 +139,43 @@ SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_COLOR, .) return . +/// Setter for whether or not this atom's light is on. +/atom/proc/set_light_on(new_value) + if(new_value == light_on || light_flags & LIGHT_FROZEN) + return + if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_ON, new_value) & COMPONENT_BLOCK_LIGHT_UPDATE) + return + . = light_on + light_on = new_value + SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_ON, .) + return . + +/// Setter for the light flags of this atom. +/atom/proc/set_light_flags(new_value) + if(new_value == light_flags || (light_flags & LIGHT_FROZEN && new_value & LIGHT_FROZEN)) + return + if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_FLAGS, new_value) & COMPONENT_BLOCK_LIGHT_UPDATE) + return + . = light_flags + light_flags = new_value + SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_FLAGS, .) + return . + +// procs that only apply to OVERLAY_LIGHT + +/// Setter for an optional render_source to apply to this atom's light overlay +/atom/proc/set_light_render_source(new_source) + if(new_source == light_flags || light_flags & LIGHT_FROZEN) + return + if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_RENDER_SOURCE, new_source) & COMPONENT_BLOCK_LIGHT_UPDATE) + return + . = light_render_source + light_render_source = new_source + SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_RENDER_SOURCE, .) + return . + +// procs that only apply to COMPLEX_LIGHT + /// Setter for the light angle of this atom /atom/proc/set_light_angle(new_value) if(new_value == light_angle || light_flags & LIGHT_FROZEN) @@ -162,17 +199,6 @@ SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_DIR, .) return . -/// Setter for whether or not this atom's light is on. -/atom/proc/set_light_on(new_value) - if(new_value == light_on || light_flags & LIGHT_FROZEN) - return - if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_ON, new_value) & COMPONENT_BLOCK_LIGHT_UPDATE) - return - . = light_on - light_on = new_value - SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_ON, .) - return . - /// Setter for the height of our light /atom/proc/set_light_height(new_value) if(new_value == light_height || light_flags & LIGHT_FROZEN) @@ -184,16 +210,6 @@ SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_HEIGHT, .) return . -/// Setter for the light flags of this atom. -/atom/proc/set_light_flags(new_value) - if(new_value == light_flags || (light_flags & LIGHT_FROZEN && new_value & LIGHT_FROZEN)) - return - if(SEND_SIGNAL(src, COMSIG_ATOM_SET_LIGHT_FLAGS, new_value) & COMPONENT_BLOCK_LIGHT_UPDATE) - return - . = light_flags - light_flags = new_value - SEND_SIGNAL(src, COMSIG_ATOM_UPDATE_LIGHT_FLAGS, .) - return . /atom/proc/get_light_offset() return list(0, 0) diff --git a/code/modules/mining/mine_items.dm b/code/modules/mining/mine_items.dm index ea0345f78d0..b23f7b9ba9a 100644 --- a/code/modules/mining/mine_items.dm +++ b/code/modules/mining/mine_items.dm @@ -411,7 +411,7 @@ return setDir(movedir) - var/datum/move_loop/loop = GLOB.move_manager.move(src, dir, delay = calculate_delay(), subsystem = SSconveyors, flags = MOVEMENT_LOOP_START_FAST|MOVEMENT_LOOP_IGNORE_PRIORITY) + var/datum/move_loop/loop = GLOB.move_manager.move(src, dir, delay = calculate_delay(), subsystem = SSconveyors, flags = MOVEMENT_LOOP_START_INSTANT|MOVEMENT_LOOP_IGNORE_PRIORITY) RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(check_rail)) RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(decay_momentum)) diff --git a/code/modules/vehicles/mecha/equipment/tools/other_tools.dm b/code/modules/vehicles/mecha/equipment/tools/other_tools.dm index 3a878ad6609..6b4e4e77ffe 100644 --- a/code/modules/vehicles/mecha/equipment/tools/other_tools.dm +++ b/code/modules/vehicles/mecha/equipment/tools/other_tools.dm @@ -127,7 +127,7 @@ /obj/item/mecha_parts/mecha_equipment/gravcatapult/proc/do_scatter(atom/movable/scatter, atom/movable/target) var/dist = 5 - get_dist(scatter, target) var/delay = 2 - GLOB.move_manager.move_away(scatter, target, delay = delay, timeout = delay * dist, flags = MOVEMENT_LOOP_START_FAST, priority = MOVEMENT_ABOVE_SPACE_PRIORITY) + GLOB.move_manager.move_away(scatter, target, delay = delay, timeout = delay * dist, flags = MOVEMENT_LOOP_START_INSTANT, priority = MOVEMENT_ABOVE_SPACE_PRIORITY) /obj/item/mecha_parts/mecha_equipment/gravcatapult/get_snowflake_data() return list( diff --git a/tgstation.dme b/tgstation.dme index 67e316b9900..b28377361c5 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -352,6 +352,7 @@ #include "code\__DEFINES\dcs\signals\signals_leash.dm" #include "code\__DEFINES\dcs\signals\signals_lift.dm" #include "code\__DEFINES\dcs\signals\signals_light_eater.dm" +#include "code\__DEFINES\dcs\signals\signals_light_intercept.dm" #include "code\__DEFINES\dcs\signals\signals_lockable_storage.dm" #include "code\__DEFINES\dcs\signals\signals_market.dm" #include "code\__DEFINES\dcs\signals\signals_material_container.dm" @@ -4826,6 +4827,7 @@ #include "code\modules\library\skill_learning\job_skillchips\research_director.dm" #include "code\modules\library\skill_learning\job_skillchips\roboticist.dm" #include "code\modules\library\skill_learning\job_skillchips\station_engineer.dm" +#include "code\modules\lighting\light_middleman.dm" #include "code\modules\lighting\lighting_area.dm" #include "code\modules\lighting\lighting_atom.dm" #include "code\modules\lighting\lighting_corner.dm"