Smooth Progress Bar (#23093)

* Progress bars now use smooth, colour-changing fills and support visual
themes.


https://github.com/user-attachments/assets/ac493f6d-dce7-4b11-a105-985a076f4904


Ported from https://github.com/NebulaSS13/Nebula/pull/1574, which ported
from the original PR:
https://github.com/ParadiseSS13/Paradise/pull/15695

### Asset Licenses
The following assets that **have not** been created by myself are
included in this PR:

| Path | Original Author | License |
| --- | --- | --- |
| <img width="532" height="309" alt="image"
src="https://github.com/user-attachments/assets/c75a9e8f-83f4-4d28-9888-3da075598f88"
/> | [dearmochi](https://github.com/dearmochi) (ParadiseSS13) | Creative
Commons 3.0 BY-SA |

AI usage disclosure: Ported using GPT 5.6 Sol.
This commit is contained in:
Geeves
2026-08-19 23:04:35 +00:00
committed by GitHub
parent 40e88e0d2d
commit 67eef8efa7
8 changed files with 173 additions and 79 deletions
+9 -7
View File
@@ -585,8 +585,9 @@ Turf and target are seperate in case you want to teleport some distance from a t
* * display_progress - Boolean, if the progress bar is shown
* * extra_checks - A `/datum/callback` that is invoked to perform extra checks and validate that the action can continue to be performed,
* if it returns `FALSE` or an algebraic equivalent the action is aborted
* * progressbar_type - The progress bar theme to display
*/
/proc/do_mob(mob/user, mob/target, delay = 30, needhand = TRUE, display_progress = TRUE, datum/callback/extra_checks) //This is quite an ugly solution but i refuse to use the old request system.
/proc/do_mob(mob/user, mob/target, delay = 30, needhand = TRUE, display_progress = TRUE, datum/callback/extra_checks, progressbar_type = /datum/progressbar/default) //This is quite an ugly solution but i refuse to use the old request system.
if(!user || !target)
stack_trace("do_mob called without either an user or a target!")
return FALSE
@@ -600,7 +601,7 @@ Turf and target are seperate in case you want to teleport some distance from a t
var/atom/loc_check = target
for(var/i = 0; !isturf(loc_check.loc) && i < 5; i++)
loc_check = target.loc
progbar = new(user, delay, loc_check)
progbar = new progressbar_type(user, delay, loc_check)
var/endtime = world.time + delay
var/starttime = world.time
@@ -621,7 +622,7 @@ Turf and target are seperate in case you want to teleport some distance from a t
break
if (progbar)
qdel(progbar)
progbar.end_progress()
/**
* Timed actions involving one mob user and (optionally) one target.
@@ -635,17 +636,18 @@ Turf and target are seperate in case you want to teleport some distance from a t
* * do_flags: Flags that determine what the user and target can and cannot do, defined in [mobs.dm]. Defaults to DO_DEFAULT.
* * incapacitation_flags: Incapacitation flags that determines if the user can be incapacitated. Defaults to INCAPACITATION_DEFAULT.
* * extra_checks: Optional extra checks, that uses a callback. See [datum/callback].
* * progressbar_type: The progress bar theme to display.
*
*/
/proc/do_after(mob/user, delay, atom/target, do_flags = DO_DEFAULT, incapacitation_flags = INCAPACITATION_DEFAULT, datum/callback/extra_checks)
return !do_after_detailed(user, delay, target, do_flags, incapacitation_flags, extra_checks)
/proc/do_after(mob/user, delay, atom/target, do_flags = DO_DEFAULT, incapacitation_flags = INCAPACITATION_DEFAULT, datum/callback/extra_checks, progressbar_type = /datum/progressbar/default)
return !do_after_detailed(user, delay, target, do_flags, incapacitation_flags, extra_checks, progressbar_type)
/**
* See [/proc/do_after]
* Returns the exact error, defined in [mobs.dm] for custom error messages.
* Overlaps with do_flags, with some extra error messages available.
*/
/proc/do_after_detailed(mob/user, delay, atom/target, do_flags = DO_DEFAULT, incapacitation_flags = INCAPACITATION_DEFAULT, datum/callback/extra_checks)
/proc/do_after_detailed(mob/user, delay, atom/target, do_flags = DO_DEFAULT, incapacitation_flags = INCAPACITATION_DEFAULT, datum/callback/extra_checks, progressbar_type = /datum/progressbar/default)
if(!delay)
return FALSE
@@ -686,7 +688,7 @@ Turf and target are seperate in case you want to teleport some distance from a t
var/datum/progressbar/progbar
if ((do_flags & DO_SHOW_PROGRESS) && user.client && (user.client.prefs.toggles_secondary & PROGRESS_BARS))
var/progbar_pos = (do_flags & DO_PLACE_PROGRESSBAR_ON_USER) ? user : (target || user)
progbar = new(user, delay, progbar_pos)
progbar = new progressbar_type(user, delay, progbar_pos)
SEND_SIGNAL(user, COMSIG_DO_AFTER_BEGAN)
+158 -72
View File
@@ -1,25 +1,45 @@
#define PROGRESSBAR_HEIGHT 6
#define PROGRESSBAR_ANIMATION_TIME 5
/datum/progressbar
///The progress bar visual element.
/// The icon file containing the backdrop, mask, and fill states for this theme.
var/icon = 'icons/effects/progress_bar/default.dmi'
/// The total height in pixels of the themed progress bar.
var/height = 7
/// The width in pixels of the theme's fill sprite.
var/fill_width = 22
/// The time taken for the progress bar to appear.
var/animation_time_appear = 0.5 SECONDS
/// The time taken for the progress bar to fade away.
var/animation_time_fade = 0.5 SECONDS
/// The time taken for stacked progress bars to shift down.
var/animation_time_shift = 0.5 SECONDS
/// The visual element containing the backdrop and fill.
var/image/bar
///The target where this progress bar is applied and where it is shown.
/// The backdrop visual element.
var/image/backdrop
/// The fill visual element.
var/image/fill
/// The target where this progress bar is applied and where it is shown.
var/atom/bar_loc
///The mob whose client sees the progress bar.
/// The mob whose client sees the progress bar.
var/mob/user
///The client seeing the progress bar.
/// The client seeing the progress bar.
var/client/user_client
///Effectively the number of steps the progress bar will need to do before reaching completion.
/// The number of steps the progress bar needs to reach completion.
var/goal = 1
///Control check to see if the progress was interrupted before reaching its goal.
/// The most recently displayed progress value.
var/last_progress = 0
///Variable to ensure smooth visual stacking on multiple progress bars.
/// The progress bar's position in the stack over its target.
var/listindex = 0
///The type of our last value for bar_loc, for debugging
/// The type of our last value for bar_loc, for debugging.
var/location_type
///Where to draw the progress bar above the icon
/// Horizontal offset needed to centre the bar over oversized icons.
var/offset_x
/// Vertical offset needed to place the bar over oversized icons.
var/offset_y
/// Whether the bar has started its ending animation.
var/stopping = FALSE
/// Whether this bar has already been removed from the user's stack.
var/removed_from_stack = FALSE
/datum/progressbar/New(mob/User, goal_number, atom/target)
. = ..()
@@ -27,33 +47,30 @@
stack_trace("Invalid target [target] passed in")
qdel(src)
return
if(QDELETED(User) || !istype(User))
if (QDELETED(User) || !istype(User))
stack_trace("/datum/progressbar created with [isnull(User) ? "null" : "invalid"] user")
qdel(src)
return
if(!isnum(goal_number))
if (!isnum(goal_number))
stack_trace("/datum/progressbar created with [isnull(User) ? "null" : "invalid"] goal_number")
qdel(src)
return
goal = goal_number
bar_loc = target
location_type = bar_loc.type
user = User
var/list/icon_offsets = target.get_oversized_icon_offsets()
var/offset_x = icon_offsets["x"]
offset_x = icon_offsets["x"]
offset_y = icon_offsets["y"]
bar = image('icons/effects/progressbar.dmi', bar_loc, "prog_bar_0", pixel_x = offset_x)
// SET_PLANE_EXPLICIT(bar, ABOVE_HUD_PLANE, User)
bar.plane = HUD_PLANE
bar.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA
user = User
init_images()
LAZYADDASSOCLIST(user.progressbars, bar_loc, src)
var/list/bars = user.progressbars[bar_loc]
listindex = bars.len
listindex = length(bars)
if(user.client)
if (user.client)
user_client = user.client
add_prog_bar_image_to_client()
@@ -62,111 +79,180 @@
RegisterSignal(user, COMSIG_MOB_LOGIN, PROC_REF(on_user_login))
RegisterSignal(bar_loc, COMSIG_QDELETING, PROC_REF(on_bar_loc_delete))
/datum/progressbar/Destroy()
if(user)
if (user)
UnregisterSignal(user, list(COMSIG_QDELETING, COMSIG_MOB_LOGOUT, COMSIG_MOB_LOGIN))
if(isliving(user))
var/mob/living/L = user
if(L.stamina_bar == src)
L.stamina_bar = null
if (isliving(user))
var/mob/living/living_user = user
if (living_user.stamina_bar == src)
living_user.stamina_bar = null
for(var/pb in user.progressbars[bar_loc])
var/datum/progressbar/progress_bar = pb
if(progress_bar == src || progress_bar.listindex <= listindex)
continue
progress_bar.listindex--
progress_bar.bar.pixel_y = world.icon_size + offset_y + (PROGRESSBAR_HEIGHT * (progress_bar.listindex - 1))
var/dist_to_travel = world.icon_size + offset_y + (PROGRESSBAR_HEIGHT * (progress_bar.listindex - 1)) - PROGRESSBAR_HEIGHT
animate(progress_bar.bar, pixel_y = dist_to_travel, time = PROGRESSBAR_ANIMATION_TIME, easing = SINE_EASING)
LAZYREMOVEASSOC(user.progressbars, bar_loc, src)
remove_from_stack()
user = null
if(user_client)
if (user_client)
clean_user_client()
if(bar_loc)
if (bar_loc)
UnregisterSignal(bar_loc, COMSIG_QDELETING)
bar_loc = null
bar = null
backdrop = null
fill = null
return ..()
/// Initializes the holder, backdrop, and masked fill images used by the theme.
/datum/progressbar/proc/init_images()
bar = image('icons/effects/effects.dmi', bar_loc, "nothing", HUD_ABOVE_ITEM_LAYER, pixel_x = offset_x)
bar.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
bar.alpha = 0
bar.plane = HUD_PLANE
bar.appearance_flags = KEEP_TOGETHER | APPEARANCE_UI_IGNORE_ALPHA
///Called right before the user's Destroy()
backdrop = image(icon, bar_loc, "backdrop")
fill = image(icon, bar_loc, "fill")
fill.filters = filter(type = "alpha", icon = icon(icon, "mask"), x = -fill_width)
bar.overlays += list(backdrop, fill)
/// Removes this bar from its stack and smoothly shifts any bars above it down.
/datum/progressbar/proc/remove_from_stack()
if (removed_from_stack || !user || !bar_loc)
return
var/list/bars = user.progressbars?[bar_loc]
if (bars)
for (var/pb in bars)
var/datum/progressbar/progress_bar = pb
if (progress_bar == src || progress_bar.listindex <= listindex)
continue
progress_bar.listindex--
progress_bar.shift_down()
LAZYREMOVEASSOC(user.progressbars, bar_loc, src)
removed_from_stack = TRUE
/// Shifts this progress bar down one position in its stack.
/datum/progressbar/proc/shift_down()
animate(bar,
pixel_y = world.icon_size + offset_y + (height * (listindex - 1)),
time = animation_time_shift,
easing = SINE_EASING | EASE_OUT,
flags = ANIMATION_PARALLEL
)
/// Called right before the user's Destroy().
/datum/progressbar/proc/on_user_delete(datum/source)
SIGNAL_HANDLER
user.progressbars = null //We can simply nuke the list and stop worrying about updating other prog bars if the user itself is gone.
user.progressbars = null
removed_from_stack = TRUE
user = null
qdel(src)
///Called right before the bar_loc's Destroy()
/// Called right before the bar_loc's Destroy().
/datum/progressbar/proc/on_bar_loc_delete(datum/source)
SIGNAL_HANDLER
qdel(src)
///Removes the progress bar image from the user_client and nulls the variable, if it exists.
/// Removes the progress bar image from the current client.
/datum/progressbar/proc/clean_user_client(datum/source)
SIGNAL_HANDLER
if(!user_client) //Disconnected, already gone.
if (!user_client)
return
user_client.images -= bar
user_client = null
///Called by user's Login(), it transfers the progress bar image to the new client.
/// Transfers the progress bar image to the user's new client after login.
/datum/progressbar/proc/on_user_login(datum/source)
SIGNAL_HANDLER
if(user_client)
if(user_client == user.client) //If this was not client handling I'd condemn this sanity check. But clients are fickle things.
if (user_client)
if (user_client == user.client)
return
clean_user_client()
if(!user.client) //Clients can vanish at any time, the bastards.
if (!user.client)
return
user_client = user.client
add_prog_bar_image_to_client()
///Adds a smoothly-appearing progress bar image to the player's screen.
/// Adds a smoothly appearing progress bar image to the player's client.
/datum/progressbar/proc/add_prog_bar_image_to_client()
bar.pixel_y = 0
bar.alpha = 0
user_client.images += bar
animate(bar, pixel_y = world.icon_size + offset_y + (PROGRESSBAR_HEIGHT * (listindex - 1)), alpha = 255, time = PROGRESSBAR_ANIMATION_TIME, easing = SINE_EASING)
animate(bar,
pixel_y = world.icon_size + offset_y + (height * (listindex - 1)),
alpha = 255,
time = animation_time_appear,
easing = SINE_EASING | EASE_OUT
)
///Updates the progress bar image visually.
/// Updates the progress bar's masked fill.
/datum/progressbar/proc/update(progress)
if (QDELETED(bar_loc))
qdel(src)
return
progress = clamp(progress, 0, goal)
if(progress == last_progress)
if (progress == last_progress)
return
last_progress = progress
bar.icon_state = "prog_bar_[round(((progress / goal) * 100), 5)]"
refresh_fill()
/// Removes and re-adds the fill so its filtered appearance updates immediately.
/datum/progressbar/proc/refresh_fill()
if (!fill || !bar)
return
bar.overlays -= fill
update_fill(clamp(last_progress / goal, 0, 1))
bar.overlays += fill
///Called on progress end, be it successful or a failure. Wraps up things to delete the datum and bar.
/// Updates the fill image for the current theme.
/datum/progressbar/proc/update_fill(fraction)
UNLINT(fill?.filters[1]?.x = -fill_width * (1 - fraction))
if (!stopping)
fill.color = rgb_gradient(fraction, 0, "#cc0033", 0.25, "#cc6633", 0.5, "#d1cc33", 0.75, "#00cc33")
else if (last_progress == goal)
fill.color = COLOR_YELLOW
/// Ends the progress bar and schedules its deletion after the fade animation.
/datum/progressbar/proc/end_progress()
if(last_progress != goal)
bar.icon_state = "[bar.icon_state]_fail"
if (stopping)
return
stopping = TRUE
refresh_fill()
remove_from_stack()
animate(bar, alpha = 0, time = animation_time_fade, flags = ANIMATION_PARALLEL)
QDEL_IN(src, animation_time_fade)
animate(bar, alpha = 0, time = PROGRESSBAR_ANIMATION_TIME)
QDEL_IN(src, PROGRESSBAR_ANIMATION_TIME)
///Progress bars are very generic, and what hangs a ref to them depends heavily on the context in which they're used
///So let's make hunting harddels easier yeah?
/// Progress bars are generic, so record the target type to make harddel debugging easier.
/datum/progressbar/dump_harddel_info()
if(harddel_deets_dumped)
if (harddel_deets_dumped)
return
harddel_deets_dumped = TRUE
return "Owner's type: [location_type]"
#undef PROGRESSBAR_ANIMATION_TIME
#undef PROGRESSBAR_HEIGHT
/datum/progressbar/default
/datum/progressbar/default/slim
icon = 'icons/effects/progress_bar/default_slim.dmi'
height = 5
/// A warning theme that pulses between yellow and red after its halfway point.
/datum/progressbar/warning
icon = 'icons/effects/progress_bar/warning.dmi'
/datum/progressbar/warning/update_fill(fraction)
. = ..()
if (fraction <= 0.5)
fill.color = COLOR_YELLOW
else
fill.color = rgb_gradient(fraction * 10, 0.5, COLOR_YELLOW, 0.5, COLOR_RED, "loop")
/datum/progressbar/warning/slim
icon = 'icons/effects/progress_bar/warning_slim.dmi'
height = 5
+6
View File
@@ -0,0 +1,6 @@
author: Geeves
delete-after: True
changes:
- rscadd: "Progress bars now use smooth, colour-changing fills and support visual themes."
Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1013 B