diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm
index 4278c69a177..cb6328110c3 100644
--- a/code/__DEFINES/colors.dm
+++ b/code/__DEFINES/colors.dm
@@ -14,6 +14,7 @@
#define COLOR_FLOORTILE_GRAY "#8D8B8B"
#define COLOR_ALMOST_BLACK "#333333"
#define COLOR_BLACK "#000000"
+#define COLOR_HALF_TRANSPARENT_BLACK "#0000007A"
#define COLOR_RED "#FF0000"
#define COLOR_MOSTLY_PURE_RED "#FF3300"
diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm
index dce63618ce6..c624b3ff6b7 100644
--- a/code/__DEFINES/vv.dm
+++ b/code/__DEFINES/vv.dm
@@ -84,6 +84,7 @@
#define VV_HK_TRIGGER_EXPLOSION "explode"
#define VV_HK_AUTO_RENAME "auto_rename"
#define VV_HK_RADIATE "radiate"
+#define VV_HK_EDIT_FILTERS "edit_filters"
#define VV_HK_ADD_AI "add_ai"
// /obj
diff --git a/code/__HELPERS/filters.dm b/code/__HELPERS/filters.dm
new file mode 100644
index 00000000000..7be7ca5d732
--- /dev/null
+++ b/code/__HELPERS/filters.dm
@@ -0,0 +1,319 @@
+#define ICON_NOT_SET "Not Set"
+
+//This is stored as a nested list instead of datums or whatever because it json encodes nicely for usage in tgui
+GLOBAL_LIST_INIT(master_filter_info, list(
+ "alpha" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "icon" = ICON_NOT_SET,
+ "render_source" = "",
+ "flags" = 0
+ ),
+ "flags" = list(
+ "MASK_INVERSE" = MASK_INVERSE,
+ "MASK_SWAP" = MASK_SWAP
+ )
+ ),
+ "angular_blur" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "size" = 1
+ )
+ ),
+ /* Not supported because making a proper matrix editor on the frontend would be a huge dick pain.
+ Uncomment if you ever implement it
+ "color" = list(
+ "defaults" = list(
+ "color" = matrix(),
+ "space" = FILTER_COLOR_RGB
+ )
+ ),
+ */
+ "displace" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "size" = null,
+ "icon" = ICON_NOT_SET,
+ "render_source" = ""
+ )
+ ),
+ "drop_shadow" = list(
+ "defaults" = list(
+ "x" = 1,
+ "y" = -1,
+ "size" = 1,
+ "offset" = 0,
+ "color" = COLOR_HALF_TRANSPARENT_BLACK
+ )
+ ),
+ "blur" = list(
+ "defaults" = list(
+ "size" = 1
+ )
+ ),
+ "layer" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "icon" = ICON_NOT_SET,
+ "render_source" = "",
+ "flags" = FILTER_OVERLAY,
+ "color" = "",
+ "transform" = null,
+ "blend_mode" = BLEND_DEFAULT
+ )
+ ),
+ "motion_blur" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0
+ )
+ ),
+ "outline" = list(
+ "defaults" = list(
+ "size" = 0,
+ "color" = COLOR_BLACK,
+ "flags" = NONE
+ ),
+ "flags" = list(
+ "OUTLINE_SHARP" = OUTLINE_SHARP,
+ "OUTLINE_SQUARE" = OUTLINE_SQUARE
+ )
+ ),
+ "radial_blur" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "size" = 0.01
+ )
+ ),
+ "rays" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "size" = 16,
+ "color" = COLOR_WHITE,
+ "offset" = 0,
+ "density" = 10,
+ "threshold" = 0.5,
+ "factor" = 0,
+ "flags" = FILTER_OVERLAY | FILTER_UNDERLAY
+ ),
+ "flags" = list(
+ "FILTER_OVERLAY" = FILTER_OVERLAY,
+ "FILTER_UNDERLAY" = FILTER_UNDERLAY
+ )
+ ),
+ "ripple" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "size" = 1,
+ "repeat" = 2,
+ "radius" = 0,
+ "falloff" = 1,
+ "flags" = NONE
+ ),
+ "flags" = list(
+ "WAVE_BOUNDED" = WAVE_BOUNDED
+ )
+ ),
+ "wave" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "size" = 1,
+ "offset" = 0,
+ "flags" = NONE
+ ),
+ "flags" = list(
+ "WAVE_SIDEWAYS" = WAVE_SIDEWAYS,
+ "WAVE_BOUNDED" = WAVE_BOUNDED
+ )
+ )
+))
+
+#undef ICON_NOT_SET
+
+//Helpers to generate lists for filter helpers
+//This is the only practical way of writing these that actually produces sane lists
+/proc/alpha_mask_filter(x, y, icon/icon, render_source, flags)
+ . = list("type" = "alpha")
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+ if(!isnull(icon))
+ .["icon"] = icon
+ if(!isnull(render_source))
+ .["render_source"] = render_source
+ if(!isnull(flags))
+ .["flags"] = flags
+
+/proc/angular_blur_filter(x, y, size)
+ . = list("type" = "angular_blur")
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+ if(!isnull(size))
+ .["size"] = size
+
+/proc/color_matrix_filter(matrix/in_matrix, space)
+ . = list("type" = "color")
+ .["color"] = in_matrix
+ if(!isnull(space))
+ .["space"] = space
+
+/proc/displacement_map_filter(icon, render_source, x, y, size = 32)
+ . = list("type" = "displace")
+ if(!isnull(icon))
+ .["icon"] = icon
+ if(!isnull(render_source))
+ .["render_source"] = render_source
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+ if(!isnull(size))
+ .["size"] = size
+
+/proc/drop_shadow_filter(x, y, size, offset, color)
+ . = list("type" = "drop_shadow")
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+ if(!isnull(size))
+ .["size"] = size
+ if(!isnull(offset))
+ .["offset"] = offset
+ if(!isnull(color))
+ .["color"] = color
+
+/proc/gauss_blur_filter(size)
+ . = list("type" = "blur")
+ if(!isnull(size))
+ .["size"] = size
+
+/proc/layering_filter(icon, render_source, x, y, flags, color, transform, blend_mode)
+ . = list("type" = "layer")
+ if(!isnull(icon))
+ .["icon"] = icon
+ if(!isnull(render_source))
+ .["render_source"] = render_source
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+ if(!isnull(color))
+ .["color"] = color
+ if(!isnull(flags))
+ .["flags"] = flags
+ if(!isnull(transform))
+ .["transform"] = transform
+ if(!isnull(blend_mode))
+ .["blend_mode"] = blend_mode
+
+/proc/motion_blur_filter(x, y)
+ . = list("type" = "motion_blur")
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+
+/proc/outline_filter(size, color, flags)
+ . = list("type" = "outline")
+ if(!isnull(size))
+ .["size"] = size
+ if(!isnull(color))
+ .["color"] = color
+ if(!isnull(flags))
+ .["flags"] = flags
+
+/proc/radial_blur_filter(size, x, y)
+ . = list("type" = "radial_blur")
+ if(!isnull(size))
+ .["size"] = size
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+
+/proc/rays_filter(size, color, offset, density, threshold, factor, x, y, flags)
+ . = list("type" = "rays")
+ if(!isnull(size))
+ .["size"] = size
+ if(!isnull(color))
+ .["color"] = color
+ if(!isnull(offset))
+ .["offset"] = offset
+ if(!isnull(density))
+ .["density"] = density
+ if(!isnull(threshold))
+ .["threshold"] = threshold
+ if(!isnull(factor))
+ .["factor"] = factor
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+ if(!isnull(flags))
+ .["flags"] = flags
+
+/proc/ripple_filter(radius, size, falloff, repeat, x, y, flags)
+ . = list("type" = "ripple")
+ if(!isnull(radius))
+ .["radius"] = radius
+ if(!isnull(size))
+ .["size"] = size
+ if(!isnull(falloff))
+ .["falloff"] = falloff
+ if(!isnull(repeat))
+ .["repeat"] = repeat
+ if(!isnull(flags))
+ .["flags"] = flags
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+
+/proc/wave_filter(x, y, size, offset, flags)
+ . = list("type" = "wave")
+ if(!isnull(size))
+ .["size"] = size
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+ if(!isnull(offset))
+ .["offset"] = offset
+ if(!isnull(flags))
+ .["flags"] = flags
+
+/proc/apply_wibbly_filters(atom/in_atom, length)
+ for(var/i in 1 to 7)
+ //This is a very baffling and strange way of doing this but I am just preserving old functionality
+ var/X
+ var/Y
+ var/rsq
+ do
+ X = 60*rand() - 30
+ Y = 60*rand() - 30
+ rsq = X*X + Y*Y
+ while(rsq<100 || rsq>900) // Yeah let's just loop infinitely due to bad luck what's the worst that could happen?
+ var/random_roll = rand()
+ in_atom.add_filter("wibbly-[i]", 5, wave_filter(x = X, y = Y, size = rand() * 2.5 + 0.5, offset = random_roll))
+ var/filter = in_atom.get_filter("wibbly-[i]")
+ animate(filter, offset = random_roll, time = 0, loop = -1, flags = ANIMATION_PARALLEL)
+ animate(offset = random_roll - 1, time = rand() * 20 + 10)
+
+/proc/remove_wibbly_filters(atom/in_atom)
+ var/filter
+ for(var/i in 1 to 7)
+ filter = in_atom.get_filter("wibbly-[i]")
+ animate(filter)
+ in_atom.remove_filter("wibbly-[i]")
diff --git a/code/_onclick/hud/plane_master.dm b/code/_onclick/hud/plane_master.dm
index 35e8df2d89e..b7d66406164 100644
--- a/code/_onclick/hud/plane_master.dm
+++ b/code/_onclick/hud/plane_master.dm
@@ -26,18 +26,9 @@
/atom/movable/screen/plane_master/openspace/Initialize()
. = ..()
- filters += filter(type = "drop_shadow", color = "#04080FAA", size = -10)
- filters += filter(type = "drop_shadow", color = "#04080FAA", size = -15)
- filters += filter(type = "drop_shadow", color = "#04080FAA", size = -20)
-
-/atom/movable/screen/plane_master/proc/outline(_size, _color)
- filters += filter(type = "outline", size = _size, color = _color)
-
-/atom/movable/screen/plane_master/proc/shadow(_size, _border, _offset = 0, _x = 0, _y = 0, _color = "#04080FAA")
- filters += filter(type = "drop_shadow", x = _x, y = _y, color = _color, size = _size, offset = _offset)
-
-/atom/movable/screen/plane_master/proc/clear_filters()
- filters = list()
+ add_filter("first_stage_openspace", 1, drop_shadow_filter(color = "#04080FAA", size = -10))
+ add_filter("second_stage_openspace", 2, drop_shadow_filter(color = "#04080FAA", size = -15))
+ add_filter("third_stage_openspace", 2, drop_shadow_filter(color = "#04080FAA", size = -20))
///Contains just the floor
/atom/movable/screen/plane_master/floor
@@ -47,9 +38,9 @@
blend_mode = BLEND_OVERLAY
/atom/movable/screen/plane_master/floor/backdrop(mob/mymob)
- filters = list()
+ clear_filters()
if(istype(mymob) && mymob.eye_blurry)
- filters += GAUSSIAN_BLUR(clamp(mymob.eye_blurry*0.1,0.6,3))
+ add_filter("eye_blur", 1, gauss_blur_filter(clamp(mymob.eye_blurry * 0.1, 0.6, 3)))
///Contains most things in the game world
/atom/movable/screen/plane_master/game_world
@@ -59,11 +50,11 @@
blend_mode = BLEND_OVERLAY
/atom/movable/screen/plane_master/game_world/backdrop(mob/mymob)
- filters = list()
+ clear_filters()
if(istype(mymob) && mymob.client && mymob.client.prefs && mymob.client.prefs.ambientocclusion)
- filters += AMBIENT_OCCLUSION
+ add_filter("AO", 1, drop_shadow_filter(x = 0, y = -2, size = 4, color = "#04080FAA"))
if(istype(mymob) && mymob.eye_blurry)
- filters += GAUSSIAN_BLUR(clamp(mymob.eye_blurry*0.1,0.6,3))
+ add_filter("eye_blur", 1, gauss_blur_filter(clamp(mymob.eye_blurry * 0.1, 0.6, 3)))
///Contains all lighting objects
@@ -79,9 +70,9 @@
/atom/movable/screen/plane_master/lighting/Initialize()
. = ..()
- filters += filter(type="alpha", render_source = EMISSIVE_RENDER_TARGET, flags = MASK_INVERSE)
- filters += filter(type="alpha", render_source = EMISSIVE_UNBLOCKABLE_RENDER_TARGET, flags = MASK_INVERSE)
- filters += filter(type="alpha", render_source = O_LIGHTING_VISUAL_RENDER_TARGET, flags = MASK_INVERSE)
+ add_filter("emissives", 1, alpha_mask_filter(render_source = EMISSIVE_RENDER_TARGET, flags = MASK_INVERSE))
+ add_filter("unblockable_emissives", 2, alpha_mask_filter(render_source = EMISSIVE_UNBLOCKABLE_RENDER_TARGET, flags = MASK_INVERSE))
+ add_filter("object_lighting", 3, alpha_mask_filter(render_source = O_LIGHTING_VISUAL_RENDER_TARGET, flags = MASK_INVERSE))
/**
* Things placed on this mask the lighting plane. Doesn't render directly.
@@ -97,7 +88,7 @@
/atom/movable/screen/plane_master/emissive/Initialize()
. = ..()
- filters += filter(type="alpha", render_source=EMISSIVE_BLOCKER_RENDER_TARGET, flags=MASK_INVERSE)
+ add_filter("emissive_block", 1, alpha_mask_filter(render_source = EMISSIVE_BLOCKER_RENDER_TARGET, flags = MASK_INVERSE))
/**
* Things placed on this always mask the lighting plane. Doesn't render directly.
@@ -163,4 +154,4 @@
/atom/movable/screen/plane_master/runechat/backdrop(mob/mymob)
filters = list()
if(istype(mymob) && mymob.client?.prefs?.ambientocclusion)
- filters += AMBIENT_OCCLUSION
+ add_filter("AO", 1, drop_shadow_filter(x = 0, y = -2, size = 4, color = "#04080FAA"))
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index f9619a6abb8..b18ae480a0a 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -1102,6 +1102,7 @@
VV_DROPDOWN_OPTION(VV_HK_TRIGGER_EMP, "EMP Pulse")
VV_DROPDOWN_OPTION(VV_HK_TRIGGER_EXPLOSION, "Explosion")
VV_DROPDOWN_OPTION(VV_HK_RADIATE, "Radiate")
+ VV_DROPDOWN_OPTION(VV_HK_EDIT_FILTERS, "Edit Filters")
VV_DROPDOWN_OPTION(VV_HK_ADD_AI, "Add AI controller")
/atom/vv_do_topic(list/href_list)
@@ -1181,6 +1182,10 @@
if(newname && !(CHAT_FILTER_CHECK(newname) && alert(usr, "Your selected name contains words restricted by IC chat filters. Confirm this new name?", "IC Chat Filter Conflict", "Confirm", "Cancel") != "Confirm"))
vv_auto_rename(newname)
+ if(href_list[VV_HK_EDIT_FILTERS] && check_rights(R_VAREDIT))
+ var/client/C = usr.client
+ C?.open_filter_editor(src)
+
/atom/vv_get_header()
. = ..()
var/refid = REF(src)
@@ -1488,14 +1493,14 @@
victim.log_message(message, LOG_ATTACK, color="blue")
-/atom/movable/proc/add_filter(name,priority,list/params)
+/atom/proc/add_filter(name,priority,list/params)
LAZYINITLIST(filter_data)
var/list/p = params.Copy()
p["priority"] = priority
filter_data[name] = p
update_filters()
-/atom/movable/proc/update_filters()
+/atom/proc/update_filters()
filters = null
filter_data = sortTim(filter_data, /proc/cmp_filter_data_priority, TRUE)
for(var/f in filter_data)
@@ -1503,6 +1508,29 @@
var/list/arguments = data.Copy()
arguments -= "priority"
filters += filter(arglist(arguments))
+ UNSETEMPTY(filter_data)
+
+/atom/proc/transition_filter(name, time, list/new_params, easing, loop)
+ var/filter = get_filter(name)
+ if(!filter)
+ return
+
+ var/list/old_filter_data = filter_data[name]
+
+ var/list/params = old_filter_data.Copy()
+ for(var/thing in new_params)
+ params[thing] = new_params[thing]
+
+ animate(filter, new_params, time = time, easing = easing, loop = loop)
+ for(var/param in params)
+ filter_data[name][param] = params[param]
+
+/atom/proc/change_filter_priority(name, new_priority)
+ if(!filter_data || !filter_data[name])
+ return
+
+ filter_data[name]["priority"] = new_priority
+ update_filters()
/obj/item/update_filters()
. = ..()
@@ -1510,14 +1538,24 @@
var/datum/action/A = X
A.UpdateButtonIcon()
-/atom/movable/proc/get_filter(name)
+/atom/proc/get_filter(name)
if(filter_data && filter_data[name])
return filters[filter_data.Find(name)]
-/atom/movable/proc/remove_filter(name)
- if(filter_data && filter_data[name])
- filter_data -= name
- update_filters()
+/atom/proc/remove_filter(name_or_names)
+ if(!filter_data)
+ return
+
+ var/list/names = islist(name_or_names) ? name_or_names : list(name_or_names)
+
+ for(var/name in names)
+ if(filter_data[name])
+ filter_data -= name
+ update_filters()
+
+/atom/proc/clear_filters()
+ filter_data = null
+ filters = null
/atom/proc/intercept_zImpact(atom/movable/AM, levels = 1)
. |= SEND_SIGNAL(src, COMSIG_ATOM_INTERCEPT_Z_FALL, AM, levels)
diff --git a/code/game/objects/structures/statues.dm b/code/game/objects/structures/statues.dm
index 3131192153e..275b18cdd94 100644
--- a/code/game/objects/structures/statues.dm
+++ b/code/game/objects/structures/statues.dm
@@ -426,8 +426,6 @@ Moving interrupts
var/completion = 0
/// Greyscaled target with cutout filter
var/mutable_appearance/target_appearance_with_filters
- /// Cutout filter for main block sprite
- var/partial_uncover_filter
/// HSV color filters parameters
var/static/list/greyscale_with_value_bump = list(0,0,0, 0,0,0, 0,0,1, 0,0,-0.05)
@@ -490,20 +488,19 @@ Moving interrupts
if(!target_appearance_with_filters)
target_appearance_with_filters = new(current_target)
target_appearance_with_filters.appearance_flags |= KEEP_TOGETHER
+ //Doesn't use filter helpers because MAs aren't atoms
target_appearance_with_filters.filters = filter(type="color",color=greyscale_with_value_bump,space=FILTER_COLOR_HSV)
completion = value
var/static/icon/white = icon('icons/effects/alphacolors.dmi', "white")
switch(value)
if(0)
//delete uncovered and reset filters
- filters -= partial_uncover_filter
+ remove_filter("partial_uncover")
target_appearance_with_filters = null
else
var/mask_offset = min(world.icon_size,round(completion * world.icon_size))
- if(partial_uncover_filter)
- filters -= partial_uncover_filter
- partial_uncover_filter = filter(type="alpha",icon=white,y=-mask_offset)
- filters += partial_uncover_filter
+ remove_filter("partial_uncover")
+ add_filter("partial_uncover", 1, alpha_mask_filter(icon = white, y = -mask_offset))
target_appearance_with_filters.filters = filter(type="alpha",icon=white,y=-mask_offset,flags=MASK_INVERSE)
update_icon()
diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm
index 257f38dda48..882a43c9f48 100644
--- a/code/modules/admin/holder2.dm
+++ b/code/modules/admin/holder2.dm
@@ -28,6 +28,8 @@ GLOBAL_PROTECT(href_token)
var/deadmined
+ var/datum/filter_editor/filteriffic
+
/datum/admins/New(datum/admin_rank/R, ckey, force_active = FALSE, protected)
if(IsAdminAdvancedProcCall())
var/msg = " has tried to elevate permissions!"
diff --git a/code/modules/admin/view_variables/filterrific.dm b/code/modules/admin/view_variables/filterrific.dm
new file mode 100644
index 00000000000..e651028cbe6
--- /dev/null
+++ b/code/modules/admin/view_variables/filterrific.dm
@@ -0,0 +1,99 @@
+/datum/filter_editor
+ var/atom/target
+
+/datum/filter_editor/New(atom/target)
+ src.target = target
+
+/datum/filter_editor/ui_state(mob/user)
+ return GLOB.admin_state
+
+/datum/filter_editor/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "Filteriffic")
+ ui.open()
+
+/datum/filter_editor/ui_static_data(mob/user)
+ var/list/data = list()
+ data["filter_info"] = GLOB.master_filter_info
+ return data
+
+/datum/filter_editor/ui_data()
+ var/list/data = list()
+ data["target_name"] = target.name
+ data["target_filter_data"] = target.filter_data
+ return data
+
+/datum/filter_editor/ui_act(action, list/params)
+ . = ..()
+ if(.)
+ return
+
+ switch(action)
+ if("add_filter")
+ var/target_name = params["name"]
+ while(target.filter_data && target.filter_data[target_name])
+ target_name = "[target_name]-dupe"
+ target.add_filter(target_name, params["priority"], list("type" = params["type"]))
+ . = TRUE
+ if("remove_filter")
+ target.remove_filter(params["name"])
+ . = TRUE
+ if("rename_filter")
+ var/list/filter_data = target.filter_data[params["name"]]
+ target.remove_filter(params["name"])
+ target.add_filter(params["new_name"], filter_data["priority"], filter_data)
+ . = TRUE
+ if("edit_filter")
+ target.remove_filter(params["name"])
+ target.add_filter(params["name"], params["priority"], params["new_filter"])
+ . = TRUE
+ if("change_priority")
+ var/new_priority = params["new_priority"]
+ target.change_filter_priority(params["name"], new_priority)
+ . = TRUE
+ if("transition_filter_value")
+ target.transition_filter(params["name"], 4, params["new_data"])
+ . = TRUE
+ if("modify_filter_value")
+ var/list/old_filter_data = target.filter_data[params["name"]]
+ var/list/new_filter_data = old_filter_data.Copy()
+ for(var/entry in params["new_data"])
+ new_filter_data[entry] = params["new_data"][entry]
+ for(var/entry in new_filter_data)
+ if(entry == GLOB.master_filter_info[old_filter_data["type"]]["defaults"][entry])
+ new_filter_data.Remove(entry)
+ target.remove_filter(params["name"])
+ target.add_filter(params["name"], old_filter_data["priority"], new_filter_data)
+ . = TRUE
+ if("modify_color_value")
+ var/new_color = input(usr, "Pick new filter color", "Filteriffic Colors!") as color|null
+ if(new_color)
+ target.transition_filter(params["name"], 4, list("color" = new_color))
+ . = TRUE
+ if("modify_icon_value")
+ var/icon/new_icon = input("Pick icon:", "Icon") as null|icon
+ if(new_icon)
+ target.filter_data[params["name"]]["icon"] = new_icon
+ target.update_filters()
+ . = TRUE
+ if("mass_apply")
+ if(!check_rights_for(usr.client, R_FUN))
+ to_chat(usr, "You activate \the [src].")
playsound(src, 'sound/effects/seedling_chargeup.ogg', 100, TRUE, -6)
- var/start = user.filters.len
- var/X,Y,rsq,i,f
- for(i=1, i<=7, ++i)
- do
- X = 60*rand() - 30
- Y = 60*rand() - 30
- rsq = X*X + Y*Y
- while(rsq<100 || rsq>900)
- user.filters += filter(type="wave", x=X, y=Y, size=rand()*2.5+0.5, offset=rand())
- for(i=1, i<=7, ++i)
- f = user.filters[start+i]
- animate(f, offset=f:offset, time=0, loop=3, flags=ANIMATION_PARALLEL)
- animate(offset=f:offset-1, time=rand()*20+10)
+ apply_wibbly_filters(user)
if (do_after(user, 50, target=user) && user.cell.use(activationCost))
playsound(src, 'sound/effects/bamf.ogg', 100, TRUE, -6)
to_chat(user, "You are now disguised as the Nanotrasen engineering borg \"[friendlyName]\".")
@@ -84,10 +72,7 @@
else
to_chat(user, "The chameleon field fizzles.")
do_sparks(3, FALSE, user)
- for(i=1, i<=min(7, user.filters.len), ++i) // removing filters that are animating does nothing, we gotta stop the animations first
- f = user.filters[start+i]
- animate(f)
- user.filters = null
+ remove_wibbly_filters(user)
animation_playing = FALSE
/obj/item/borg_chameleon/process()
diff --git a/code/modules/cargo/supplypod.dm b/code/modules/cargo/supplypod.dm
index 144614357ed..af1411d6bc8 100644
--- a/code/modules/cargo/supplypod.dm
+++ b/code/modules/cargo/supplypod.dm
@@ -617,7 +617,7 @@
smoke_effects[i] = smoke_part
smoke_part.pixel_x = sin(rotation)*32 * i
smoke_part.pixel_y = abs(cos(rotation))*32 * i
- smoke_part.filters += filter(type = "blur", size = 4)
+ smoke_part.add_filter("smoke_blur", 1, gauss_blur_filter(size = 4))
var/time = (pod.delays[POD_FALLING] / length(smoke_effects))*(length(smoke_effects)-i)
addtimer(CALLBACK(smoke_part, /obj/effect/supplypod_smoke/.proc/drawSelf, i), time, TIMER_CLIENT_TIME) //Go onto the last step after a very short falling animation
QDEL_IN(smoke_part, pod.delays[POD_FALLING] + 35)
@@ -627,7 +627,7 @@
return
for (var/obj/effect/supplypod_smoke/smoke_part in smoke_effects)
animate(smoke_part, alpha = 0, time = 20, flags = ANIMATION_PARALLEL)
- animate(smoke_part.filters[1], size = 6, time = 15, easing = CUBIC_EASING|EASE_OUT, flags = ANIMATION_PARALLEL)
+ animate(smoke_part.get_filter("smoke_blur"), size = 6, time = 15, easing = CUBIC_EASING|EASE_OUT, flags = ANIMATION_PARALLEL)
/obj/effect/pod_landingzone/proc/endLaunch()
pod.tryMakeRubble(drop_location())
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index e03667d6e64..1890c591436 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -1080,3 +1080,8 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
var/datum/verbs/menu/menuitem = GLOB.menulist[thing]
if (menuitem)
menuitem.Load_checked(src)
+
+/client/proc/open_filter_editor(atom/in_atom)
+ if(holder)
+ holder.filteriffic = new /datum/filter_editor(in_atom)
+ holder.filteriffic.ui_interact(mob)
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 9c0cd93e38a..4619a8e26e8 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -610,8 +610,8 @@
if(mob_mask.Height() > world.icon_size || mob_mask.Width() > world.icon_size)
var/health_doll_icon_state = health_doll_icon ? health_doll_icon : "megasprite"
mob_mask = icon('icons/hud/screen_gen.dmi', health_doll_icon_state) //swap to something generic if they have no special doll
- livingdoll.filters += filter(type="alpha", icon = mob_mask)
- livingdoll.filters += filter(type="drop_shadow", size = -1)
+ livingdoll.add_filter("mob_shape_mask", 1, alpha_mask_filter(icon = mob_mask))
+ livingdoll.add_filter("inset_drop_shadow", 2, drop_shadow_filter(size = -1))
if(severity > 0)
overlay_fullscreen("brute", /atom/movable/screen/fullscreen/brute, severity)
else
diff --git a/tgstation.dme b/tgstation.dme
index aa3f35fb3eb..a5a08dbcbb7 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -148,6 +148,7 @@
#include "code\__HELPERS\dates.dm"
#include "code\__HELPERS\dna.dm"
#include "code\__HELPERS\files.dm"
+#include "code\__HELPERS\filters.dm"
#include "code\__HELPERS\game.dm"
#include "code\__HELPERS\global_lists.dm"
#include "code\__HELPERS\heap.dm"
@@ -1450,6 +1451,7 @@
#include "code\modules\admin\verbs\SDQL2\SDQL_2_wrappers.dm"
#include "code\modules\admin\view_variables\admin_delete.dm"
#include "code\modules\admin\view_variables\debug_variables.dm"
+#include "code\modules\admin\view_variables\filterrific.dm"
#include "code\modules\admin\view_variables\get_variables.dm"
#include "code\modules\admin\view_variables\mark_datum.dm"
#include "code\modules\admin\view_variables\mass_edit_variables.dm"
diff --git a/tgui/docs/component-reference.md b/tgui/docs/component-reference.md
index 503c012c001..19a703bbd13 100644
--- a/tgui/docs/component-reference.md
+++ b/tgui/docs/component-reference.md
@@ -361,12 +361,16 @@ and displays selected entry.
**Props:**
- See inherited props: [Box](#box)
+- See inherited props: [Icon](#icon)
- `options: string[]` - An array of strings which will be displayed in the
dropdown when open
- `selected: string` - Currently selected entry
- `width: number` - Width of dropdown button and resulting menu
-- `over: boolean` - dropdown renders over instead of below
-- `color: string` - color of dropdown button
+- `over: boolean` - Dropdown renders over instead of below
+- `color: string` - Color of dropdown button
+- `nochevron: boolean` - Whether or not the arrow on the right hand side of the dropdown button is visible
+- `noscroll: boolean` - Whether or not the dropdown menu should have a scroll bar
+- `displayText: string` - Text to always display in place of the selected text
- `onClick: (e) => void` - Called when dropdown button is clicked
- `onSelected: (value) => void` - Called when a value is picked from the list, `value` is the value that was picked
diff --git a/tgui/packages/common/math.js b/tgui/packages/common/math.js
index c3013b5a32a..df7e395ea79 100644
--- a/tgui/packages/common/math.js
+++ b/tgui/packages/common/math.js
@@ -88,3 +88,13 @@ export const keyOfMatchingRange = (value, ranges) => {
}
}
};
+
+/**
+ * Get number of digits following the decimal point in a number
+ */
+export const numberOfDecimalDigits = value => {
+ if (Math.floor(value) !== value) {
+ return value.toString().split('.')[1].length || 0;
+ }
+ return 0;
+};
diff --git a/tgui/packages/tgui/components/Dropdown.js b/tgui/packages/tgui/components/Dropdown.js
index adbb13de5b2..7486807e3f3 100644
--- a/tgui/packages/tgui/components/Dropdown.js
+++ b/tgui/packages/tgui/components/Dropdown.js
@@ -64,6 +64,9 @@ export class Dropdown extends Component {
render() {
const { props } = this;
const {
+ icon,
+ iconRotation,
+ iconSpin,
color = 'default',
over,
noscroll,
@@ -72,6 +75,7 @@ export class Dropdown extends Component {
onClick,
selected,
disabled,
+ displayText,
...boxProps
} = props;
const {
@@ -114,8 +118,15 @@ export class Dropdown extends Component {
}
this.setOpen(!this.state.open);
}}>
+ {icon && (
+ "+e+"
/gi,"\n").replace(/<\/?[a-z0-9-_]+[^>]*>/gi,"").replace(/&(nbsp|amp|quot|lt|gt|apos);/g,(function(e,n){return t[n]})).replace(/?([0-9]+);/gi,(function(e,t){var n=parseInt(t,10);return String.fromCharCode(n)})).replace(/?([0-9a-f]+);/gi,(function(e,t){var n=parseInt(t,16);return String.fromCharCode(n)}))};t.buildQueryString=function(e){return Object.keys(e).map((function(t){return encodeURIComponent(t)+"="+encodeURIComponent(e[t])})).join("&")}},function(e,t,n){"use strict";var r=n(25);e.exports=function(e){return Object(r(e))}},function(e,t,n){"use strict";var r={}.hasOwnProperty;e.exports=function(e,t){return r.call(e,t)}},function(e,t,n){"use strict";var r=n(54),o=n(67),i=n(19),a=n(13),u=n(73),c=[].push,s=function(e){var t=1==e,n=2==e,s=3==e,l=4==e,f=6==e,d=5==e||f;return function(p,h,g,v){for(var m,y,b=i(p),x=o(b),w=r(h,g,3),_=a(x.length),E=0,k=v||u,S=t?k(p,_):n?k(p,0):undefined;_>E;E++)if((d||E in x)&&(y=w(m=x[E],E,b),e))if(t)S[E]=y;else if(y)switch(e){case 3:return!0;case 5:return m;case 6:return E;case 2:c.call(S,m)}else if(l)return!1;return f?-1:s||l?l:S}};e.exports={forEach:s(0),map:s(1),filter:s(2),some:s(3),every:s(4),find:s(5),findIndex:s(6)}},function(e,t,n){"use strict";t.__esModule=!0,t.useSelector=t.useDispatch=t.createAction=t.combineReducers=t.applyMiddleware=t.createStore=void 0;var r=n(24);function o(e,t){var n;if("undefined"==typeof Symbol||null==e[Symbol.iterator]){if(Array.isArray(e)||(n=function(e,t){if(!e)return;if("string"==typeof e)return i(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);"Object"===n&&e.constructor&&(n=e.constructor.name);if("Map"===n||"Set"===n)return Array.from(e);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return i(e,t)}(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var r=0;return function(){return r>=e.length?{done:!0}:{done:!1,value:e[r++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}return(n=e[Symbol.iterator]()).next.bind(n)}function i(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);nc||n!=n?l*Infinity:l*n}},function(e,t,n){"use strict";var r=n(4),o=Math.hypot,i=Math.abs,a=Math.sqrt;r({target:"Math",stat:!0,forced:!!o&&o(Infinity,NaN)!==Infinity},{hypot:function(e,t){for(var n,r,o=0,u=0,c=arguments.length,s=0;u
\n":"'+(n?e:ee(e,!0))+"
\n"}return e}(),t.blockquote=function(){function e(e){return""+(n?e:ee(e,!0))+"\n"+e+"
\n"}return e}(),t.html=function(){function e(e){return e}return e}(),t.heading=function(){function e(e,t,n,r){return this.options.headerIds?"
\n":"
\n"}return e}(),t.list=function(){function e(e,t,n){var r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+""+r+">\n"}return e}(),t.listitem=function(){function e(e){return"\n\n"+e+"\n"+t+"
\n"}return e}(),t.tablerow=function(){function e(e){return"\n"+e+" \n"}return e}(),t.tablecell=function(){function e(e,t){var n=t.header?"th":"td";return(t.align?"<"+n+' align="'+t.align+'">':"<"+n+">")+e+""+n+">\n"}return e}(),t.strong=function(){function e(e){return""+e+""}return e}(),t.em=function(){function e(e){return""+e+""}return e}(),t.codespan=function(){function e(e){return""+e+""}return e}(),t.br=function(){function e(){return this.options.xhtml?"
":"
"}return e}(),t.del=function(){function e(e){return""+e+""}return e}(),t.link=function(){function e(e,t,n){if(null===(e=J(this.options.sanitize,this.options.baseUrl,e)))return n;var r='"+n+""}return e}(),t.image=function(){function e(e,t,n){if(null===(e=J(this.options.sanitize,this.options.baseUrl,e)))return n;var r='":">"}return e}(),t.text=function(){function e(e){return e}return e}(),e}(),ne=function(){function e(){}var t=e.prototype;return t.strong=function(){function e(e){return e}return e}(),t.em=function(){function e(e){return e}return e}(),t.codespan=function(){function e(e){return e}return e}(),t.del=function(){function e(e){return e}return e}(),t.html=function(){function e(e){return e}return e}(),t.text=function(){function e(e){return e}return e}(),t.link=function(){function e(e,t,n){return""+n}return e}(),t.image=function(){function e(e,t,n){return""+n}return e}(),t.br=function(){function e(){return""}return e}(),e}(),re=function(){function e(){this.seen={}}return e.prototype.slug=function(){function e(e){var t=e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-");if(this.seen.hasOwnProperty(t)){var n=t;do{this.seen[n]++,t=n+"-"+this.seen[n]}while(this.seen.hasOwnProperty(t))}return this.seen[t]=0,t}return e}(),e}(),oe=a.defaults,ie=I.unescape,ae=function(){function e(e){this.options=e||oe,this.options.renderer=this.options.renderer||new te,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new ne,this.slugger=new re}e.parse=function(){function t(t,n){return new e(n).parse(t)}return t}();var t=e.prototype;return t.parse=function(){function e(e,t){void 0===t&&(t=!0);var n,r,o,i,a,u,c,s,l,f,d,p,h,g,v,m,y,b,x="",w=e.length;for(n=0;n
"+se(c.message+"",!0)+"";throw c}}return pe.options=pe.setOptions=function(e){return ue(pe.defaults,e),fe(pe.defaults),pe},pe.getDefaults=le,pe.defaults=de,pe.use=function(e){var t=ue({},e);if(e.renderer&&function(){var n=pe.defaults.renderer||new te,r=function(){function t(t){var r=n[t];n[t]=function(){for(var o=arguments.length,i=new Array(o),a=0;a
'+(n?e:ee(e,!0))+"\n":""+(n?e:ee(e,!0))+"\n"}return e}(),t.blockquote=function(){function e(e){return"\n"+e+"\n"}return e}(),t.html=function(){function e(e){return e}return e}(),t.heading=function(){function e(e,t,n,r){return this.options.headerIds?"
"+e+"
\n"}return e}(),t.table=function(){function e(e,t){return t&&(t=""+t+""),""+e+""}return e}(),t.br=function(){function e(){return this.options.xhtml?""+se(c.message+"",!0)+"";throw c}}return pe.options=pe.setOptions=function(e){return ue(pe.defaults,e),fe(pe.defaults),pe},pe.getDefaults=le,pe.defaults=de,pe.use=function(e){var t=ue({},e);if(e.renderer&&function(){var n=pe.defaults.renderer||new te,r=function(){function t(t){var r=n[t];n[t]=function(){for(var o=arguments.length,i=new Array(o),a=0;a