Adds whiteboards and gives a new UI to barsigns and its subtypes (#22133)

## About PR

Sprites made by @KingOfThePing! 
Adds whiteboards to following areas:

- CE and RD office (portable)
- Expedition EVA storage (portable)
- Intrepid flight deck (portable)
- Xenoarcheology presentation room
- Engineering breakroom
- Kitchen (as an alternative to holo-menu)

Whiteboards are also available in cargo console in the recreation tab
As an addition whiteboards can be mapped in with preset overlays and
written contents
This PR also changes sign selection list menu for barsigns and its
subtypes with a tgui image menu

## Images

<img width="699" height="534" alt="image"
src="https://github.com/user-attachments/assets/7b3de94c-76e4-491d-802c-ae9a31501805"
/>

<details><summary>video sample</summary>
<p>


https://github.com/user-attachments/assets/24de419c-e155-4623-9d08-0ff6c0b7d228

</p>
</details> 

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

| Path | Original Author | License |
| --- | --- | --- |
| icons/obj/whiteboard.dmi & icons/obj/whiteboard_portable.dmi |
KingOfThePing | CC BY-SA 3.0 |
| sound/effects/whiteboard_scribble_1.ogg,
sound/effects/whiteboard_scribble_2.ogg,
sound/effects/whiteboard_scribble_3.ogg | freesound_community
(https://pixabay.com/sound-effects/film-special-effects-marker-whiteboard-squeak03-83059/)
| Pixabay Licence |
This commit is contained in:
Kano
2026-04-06 19:03:01 +00:00
committed by GitHub
parent 4f9633a983
commit 3043bf536d
14 changed files with 605 additions and 56 deletions
+83 -32
View File
@@ -1,3 +1,8 @@
/// Lists of data passed over to tgui, obtained from numerous singletons. List contains lists of: "display_name", "icon", "icon_name", "singleton_path"
GLOBAL_LIST_EMPTY(barsign_overlay_cache)
GLOBAL_LIST_EMPTY(kitchensign_overlay_cache)
GLOBAL_LIST_EMPTY(marketsign_overlay_cache)
/obj/structure/sign/double/barsign
icon = 'icons/obj/barsigns.dmi'
icon_state = "off"
@@ -9,21 +14,86 @@
var/currently_on = FALSE
var/on_icon_state
var/off_icon_state = "off"
/// Local reference of the global list associated with this object's singletons.
var/list/target_cache = list()
/obj/structure/sign/double/barsign/mechanics_hints(mob/user, distance, is_adjacent)
. += ..()
. += "You can <b>Alt Click</b> to toggle power of \the [src]."
/obj/structure/sign/double/barsign/Initialize()
. = ..()
set_overlay_cache()
/obj/structure/sign/double/barsign/proc/get_target_cache()
return GLOB.barsign_overlay_cache
/obj/structure/sign/double/barsign/proc/set_overlay_cache()
target_cache = get_target_cache()
if(!length(target_cache))
var/list/singleton_instances = GET_SINGLETON_SUBTYPE_MAP(choice_types)
for(var/i in singleton_instances)
var/singleton/sign/double/bar/S = GET_SINGLETON(i)
var/overlay_image = image(icon = icon, icon_state = S.icon_state)
target_cache.Add(list(list(
"display_name" = S.name,
"icon" = icon2base64(getFlatIcon(overlay_image, no_anim = TRUE)), // expensive but we only do it once
"icon_name" = S.icon_state,
"singleton_path" = "[S.type]"
)))
/obj/structure/sign/double/barsign/attackby(obj/item/attacking_item, mob/user)
if(cult)
return ..()
var/obj/item/card/id/card = attacking_item.GetID()
if(istype(card))
if(check_access(card))
set_sign()
to_chat(user, SPAN_NOTICE("You change the sign."))
else
if(!check_access(card))
to_chat(user, SPAN_WARNING("Access denied."))
return
return
ui_interact(user)
return ..()
/obj/structure/sign/double/barsign/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "OverlayChoice", capitalize_first_letters(name))
ui.open()
/obj/structure/sign/double/barsign/ui_static_data(mob/user)
var/list/data = list()
data["contents"] = target_cache
return data
/obj/structure/sign/double/barsign/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
var/mob/user = ui.user
add_fingerprint(user)
switch(action)
if("select_overlay")
var/choice = params["singleton_path"]
INVOKE_ASYNC(src, PROC_REF(set_sign), choice)
SStgui.close_uis(src)
. = TRUE
/obj/structure/sign/double/barsign/proc/set_sign(choice)
var/singleton/sign/double/chosen_singleton = text2path(choice)
name = chosen_singleton.name
desc = chosen_singleton.desc
desc_extended = chosen_singleton.desc_extended
icon_state = chosen_singleton.icon_state
currently_on = TRUE
on_icon_state = icon_state
update_icon()
/obj/structure/sign/double/barsign/AltClick(mob/user) // Alt-click a sign with an empty hand to power or depower it, if it has the associated "[name]-off" state in the .dmi file.
if(!on_icon_state)
to_chat(user, SPAN_WARNING("The sign is already off!"))
@@ -35,30 +105,7 @@
icon_state = off_icon_state
currently_on = FALSE
/obj/structure/sign/double/barsign/proc/get_sign_choices()
var/list/sign_choices = GET_SINGLETON_SUBTYPE_MAP(choice_types)
return sign_choices
/obj/structure/sign/double/barsign/proc/set_sign()
var/list/sign_choices = get_sign_choices()
var/list/sign_index = list()
for(var/sign in sign_choices)
var/singleton/sign/double/B = GET_SINGLETON(sign)
sign_index["[B.name]"] = B
var/sign_choice = tgui_input_list(usr, "What should the sign be changed to?", "Bar Sign", sign_index)
if(!sign_choice)
return
var/singleton/sign/double/signselect = sign_index[sign_choice]
name = signselect.name
desc = signselect.desc
desc_extended = signselect.desc_extended
icon_state = signselect.icon_state
currently_on = TRUE
on_icon_state = icon_state
update_icon()
// ---- Kitchen sign
/obj/structure/sign/double/barsign/kitchensign
icon = 'icons/obj/kitchensigns.dmi'
@@ -66,9 +113,14 @@
req_access = list(ACCESS_GALLEY)
choice_types = /singleton/sign/double/kitchen
/obj/structure/sign/double/barsign/kitchensign/get_target_cache()
return GLOB.kitchensign_overlay_cache
/obj/structure/sign/double/barsign/kitchensign/mirrored // Visible from the other end of the sign.
pixel_x = -32
// ---- Market sign
/obj/structure/sign/double/barsign/marketsign
icon = 'icons/obj/marketsigns.dmi'
icon_state = "off"
@@ -76,9 +128,8 @@
req_one_access = list(ACCESS_CARGO, ACCESS_JANITOR, ACCESS_ROBOTICS, ACCESS_MINING, ACCESS_PARAMEDIC, ACCESS_HYDROPONICS, ACCESS_GALLEY, ACCESS_LIBRARY)
choice_types = /singleton/sign/double/market
/obj/structure/sign/double/barsign/marketsign/set_sign()
. = ..()
off_icon_state = "[icon_state]-off"
/obj/structure/sign/double/barsign/marketsign/get_target_cache()
return GLOB.marketsign_overlay_cache
/obj/structure/sign/double/barsign/marketsign/mirrored // Visible from the other end of the sign.
pixel_x = -32
+293
View File
@@ -0,0 +1,293 @@
/// List of data passed over to tgui, obtained from whiteboard singletons. List contains lists of: "display_name", "icon", "icon_name", "singleton_path"
GLOBAL_LIST_EMPTY(whiteboard_overlay_cache)
// ---- large whiteboard, 64x32
/obj/structure/whiteboard
name = "whiteboard"
desc = "A whiteboard in classic design. Often associated with its sibling glassboard, perfect material to draw incomprehensible equations."
icon = 'icons/obj/whiteboard.dmi'
icon_state = "whiteboard"
layer = OBJ_LAYER
/// Singleton path to whiteboard overlays.
var/overlay_choices_singleton = /singleton/whiteboard
/// Written things on the whiteboard.
var/written_contents
/// Is someone currently drawing things on the whiteboard?
var/busy = FALSE
/// Used to prevent pixel-shift mechanic spam.
var/currently_pixel_shifting = FALSE
/// For mapping purposes. Define this as one of the overlay icon_state's name (e.g. overlay_1).
var/preset_overlay
/obj/structure/whiteboard/mechanics_hints(mob/user, distance, is_adjacent)
. += ..()
. += "You can write or edit things on [src] with a <b>pen</b>."
. += "You can <b>ALT Click</b> to pixel-shift [src] to the wall. Only works for north direction when wheel brakes are locked!"
. += "You can engage/release wheel brakes with the help of a <b>wrench</b>."
/obj/structure/whiteboard/examine(mob/user, distance, is_adjacent, infix, suffix, show_extended)
. = ..()
if(written_contents)
to_chat(user, EXAMINE_BLOCK("It reads: \"[written_contents]\"."))
/obj/structure/whiteboard/Initialize()
. = ..()
if(!length(GLOB.whiteboard_overlay_cache))
var/list/singleton_instances = GET_SINGLETON_SUBTYPE_MAP(overlay_choices_singleton)
for(var/i in singleton_instances)
var/singleton/whiteboard/S = GET_SINGLETON(i)
var/overlay_image = image(icon = icon, icon_state = S.icon_state)
GLOB.whiteboard_overlay_cache.Add(list(list(
"display_name" = S.name,
"icon" = icon2base64(getFlatIcon(overlay_image)), // expensive but we only do it once
"icon_name" = S.icon_state,
"singleton_path" = "[S.type]"
)))
if(preset_overlay)
AddOverlays(preset_overlay)
/obj/structure/whiteboard/Move()
. = ..()
if(has_gravity())
playsound(src, 'sound/effects/roll.ogg', 50, 1)
/obj/structure/whiteboard/AltClick(mob/user)
if(!anchored)
to_chat(user, SPAN_WARNING("You need the wheels locked first in order to secure [src] to a wall!"))
return
if(currently_pixel_shifting)
return
currently_pixel_shifting = TRUE
if(!do_after(user, 0.5 SECOND))
currently_pixel_shifting = FALSE
return
currently_pixel_shifting = FALSE
animate(src, pixel_y = pixel_y == 0 ? 12 : 0, time = 0.2 SECOND)
balloon_alert(user, "shifted")
/obj/structure/whiteboard/attackby(obj/item/attacking_item, mob/user)
if(busy)
to_chat(user, SPAN_WARNING("Someone is already drawing things on this board!"))
return
var/obj/item/pen/P = attacking_item
if(istype(P))
to_chat(user, SPAN_NOTICE("You start contemplating what to draw."))
handle_drawing(user)
else if(attacking_item.tool_behaviour == TOOL_WRENCH)
animate(src, pixel_y = 0, time = 0.2 SECOND) // if we were pixel-shifted, return to our original state
anchored = !anchored
attacking_item.play_tool_sound(get_turf(src), 75)
balloon_alert(user, "[anchored ? "secure" : "unsecure"]")
user.visible_message("[user] [anchored ? "locks" : "unlocks"] [src]'s wheels.", \
"You [anchored ? "locked" : "unlocked"] [src]'s wheels.", \
"You hear a click.")
else
to_chat(user, SPAN_WARNING("This is the wrong tool to manifest your inland empire, maybe a pen would be more fitting?"))
/obj/structure/whiteboard/proc/handle_drawing(mob/user)
busy = TRUE
// Text input
var/new_written_contents = tgui_input_text(user, "Write your text here", "Whiteboard Contents", default = written_contents, multiline = TRUE)
if(new_written_contents == written_contents) // if untouched, cancel it
to_chat(user, SPAN_WARNING("You withdraw your pen in thought, [src] remains untouched."))
busy = FALSE
return
if(!new_written_contents) //if empty, erase the board
to_chat(user, SPAN_NOTICE("You erase the contents on [src]."))
ClearOverlays()
desc = initial(desc)
written_contents = null
busy = FALSE
return
written_contents = new_written_contents
ui_interact(user)
/obj/structure/whiteboard/proc/finalize_drawing(mob/user, choice)
playsound(get_turf(src), SFX_WHITEBOARD_SCRIBBLE, 60, FALSE)
if(do_after(user, 2 SECONDS))
update_icon(choice)
else
ClearOverlays()
written_contents = null
/obj/structure/whiteboard/update_icon(choice)
ClearOverlays()
AddOverlays(choice)
// ---- UI code
/obj/structure/whiteboard/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "OverlayChoice", capitalize_first_letters(name))
ui.open()
/obj/structure/whiteboard/ui_static_data(mob/user)
var/list/data = list()
data["contents"] = GLOB.whiteboard_overlay_cache
return data
/obj/structure/whiteboard/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
var/mob/user = ui.user
add_fingerprint(user)
switch(action)
if("select_overlay")
var/choice = params["choice"]
INVOKE_ASYNC(src, PROC_REF(finalize_drawing), user, choice)
busy = FALSE
SStgui.close_uis(src)
. = TRUE
/obj/structure/whiteboard/ui_close(mob/user, datum/tgui/ui)
. = ..()
if(busy) // if the ui was closed while we were still busy, it means no overlay were chosen
busy = FALSE
written_contents = null
ClearOverlays()
to_chat(user, SPAN_WARNING("You withdraw your pen in thought, [src] remains untouched."))
// ---- Portable whiteboard, 32x32
/obj/item/portable_whiteboard
name = "portable whiteboard"
desc = "A compact whiteboard folded into a portable frame, ready to be deployed on any unexpected seminar!"
icon = 'icons/obj/whiteboard_portable.dmi'
icon_state = "whiteboard_case"
item_state = "whiteboard_case"
contained_sprite = TRUE
force = 18
throw_speed = 1
throw_range = 4
w_class = WEIGHT_CLASS_BULKY
attack_verb = list("attacked", "bashed", "battered", "bludgeoned", "whacked")
drop_sound = 'sound/items/drop/toolbox.ogg'
pickup_sound = 'sound/items/pickup/toolbox.ogg'
/// Used to keep track of folding process to prevent spams.
var/currently_unfolding = FALSE
/obj/item/portable_whiteboard/mechanics_hints(mob/user, distance, is_adjacent)
. += ..()
. += "You can <b>ALT Click</b> to deploy [src]. It must be on the floor first!"
/obj/item/portable_whiteboard/AltClick(mob/user)
if(loc == user)
to_chat(user, SPAN_WARNING("[src] needs to be standing on the floor in order to be unfolded!"))
return
if(currently_unfolding)
return
currently_unfolding = TRUE
if(!do_after(user, 3 SECONDS))
currently_unfolding = FALSE
return
new /obj/structure/whiteboard/portable(get_turf(src))
playsound(get_turf(src), 'sound/items/storage/briefcase.ogg', 50)
qdel(src)
/obj/structure/whiteboard/portable
name = "portable whiteboard"
desc = "A compact whiteboard folded into a portable frame, ready to be deployed on any unexpected seminar!"
icon = 'icons/obj/whiteboard_portable.dmi'
anchored = FALSE
/// Used to keep track of folding process to prevent spams.
var/currently_folding = FALSE
/obj/structure/whiteboard/portable/AltClick(mob/user)
if(currently_folding)
return
currently_folding = TRUE
if(!do_after(user, 3 SECONDS))
currently_folding = FALSE
return
new /obj/item/portable_whiteboard(get_turf(src))
playsound(get_turf(src), 'sound/items/storage/briefcase.ogg', 50)
qdel(src)
/obj/structure/whiteboard/portable/update_icon(choice)
ClearOverlays()
var/image/overlay_image = image(icon = 'icons/obj/whiteboard.dmi', icon_state = choice)
overlay_image.transform *= 0.5
overlay_image.pixel_x = -16
overlay_image.pixel_y = 2
AddOverlays(overlay_image)
// ---- Whiteboard overlay singletons
/singleton/whiteboard
var/name = "placeholder"
var/icon_state = ""
/singleton/whiteboard/bar_chart
name = "Bar chart"
icon_state = "overlay_1"
/singleton/whiteboard/pie_chart
name = "Pie chart"
icon_state = "overlay_2"
/singleton/whiteboard/spacetime_diagram
name = "Spacetime diagram"
icon_state = "overlay_3"
/singleton/whiteboard/geometric_chart
name = "Geometric chart"
icon_state = "overlay_4"
/singleton/whiteboard/scribbles
name = "Scribbles"
icon_state = "overlay_5"
/singleton/whiteboard/gun_diagram
name = "Gun diagram"
icon_state = "overlay_6"
/singleton/whiteboard/circle_chart
name = "Circle chart"
icon_state = "overlay_7"
/singleton/whiteboard/display_chart
name = "Display chart"
icon_state = "overlay_8"
/singleton/whiteboard/todays_menu
name = "Today's menu"
icon_state = "overlay_9"
/singleton/whiteboard/calculations
name = "Calculations"
icon_state = "overlay_10"
/singleton/whiteboard/erased_board
name = "Erased board"
icon_state = "overlay_11"
/singleton/whiteboard/wavelenghth
name = "Wavelength"
icon_state = "overlay_12"
/singleton/whiteboard/grafitti
name = "Grafitti"
icon_state = "overlay_13"
/singleton/whiteboard/archeological
name = "Archeological"
icon_state = "overlay_14"