diff --git a/code/__defines/_planes+layers.dm b/code/__defines/_planes+layers.dm index 3bee841881..c54fbe93cc 100644 --- a/code/__defines/_planes+layers.dm +++ b/code/__defines/_planes+layers.dm @@ -145,6 +145,7 @@ What is the naming convention for planes or layers? #define LAYER_HUD_ITEM 3 //Things sitting on HUD items (largely irrelevant because PLANE_PLAYER_HUD_ITEMS) #define LAYER_HUD_ABOVE 4 //Things that reside above items (highlights) #define PLANE_PLAYER_HUD_ITEMS 96 //Separate layer with which to apply colorblindness +#define PLANE_PLAYER_HUD_ABOVE 97 //Things above the player hud #define PLANE_ADMIN3 99 //Purely for shenanigans (above HUD) diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm index 7f9128a64f..e59d3765dc 100644 --- a/code/__defines/misc.dm +++ b/code/__defines/misc.dm @@ -351,3 +351,8 @@ var/global/list/##LIST_NAME = list();\ #define RAD_LEVEL_MODERATE 10 #define RAD_LEVEL_HIGH 25 #define RAD_LEVEL_VERY_HIGH 50 + +//https://secure.byond.com/docs/ref/info.html#/atom/var/mouse_opacity +#define MOUSE_OPACITY_TRANSPARENT 0 +#define MOUSE_OPACITY_ICON 1 +#define MOUSE_OPACITY_OPAQUE 2 diff --git a/code/_helpers/_lists.dm b/code/_helpers/_lists.dm index 5e322e1928..1eb8ac5456 100644 --- a/code/_helpers/_lists.dm +++ b/code/_helpers/_lists.dm @@ -758,3 +758,8 @@ proc/dd_sortedTextList(list/incoming) . |= i #define listequal(A, B) (A.len == B.len && !length(A^B)) + +/proc/popleft(list/L) + if(L.len) + . = L[1] + L.Cut(1,2) diff --git a/code/_onclick/hud/radial.dm b/code/_onclick/hud/radial.dm new file mode 100644 index 0000000000..e83e5d732c --- /dev/null +++ b/code/_onclick/hud/radial.dm @@ -0,0 +1,313 @@ +#define NEXT_PAGE_ID "__next__" +#define DEFAULT_CHECK_DELAY 20 + +GLOBAL_LIST_EMPTY(radial_menus) + +// Ported from TG + +/obj/screen/radial + icon = 'icons/mob/radial.dmi' + layer = LAYER_HUD_ABOVE + plane = PLANE_PLAYER_HUD_ABOVE + var/datum/radial_menu/parent + +/obj/screen/radial/slice + icon_state = "radial_slice" + var/choice + var/next_page = FALSE + var/tooltips = FALSE + +/obj/screen/radial/slice/MouseEntered(location, control, params) + . = ..() + icon_state = "radial_slice_focus" + if(tooltips) + openToolTip(usr, src, params, title = name) + +/obj/screen/radial/slice/MouseExited(location, control, params) + . = ..() + icon_state = "radial_slice" + if(tooltips) + closeToolTip(usr) + +/obj/screen/radial/slice/Click(location, control, params) + if(usr.client == parent.current_user) + if(next_page) + parent.next_page() + else + parent.element_chosen(choice,usr) + +/obj/screen/radial/center + name = "Close Menu" + icon_state = "radial_center" + +/obj/screen/radial/center/MouseEntered(location, control, params) + . = ..() + icon_state = "radial_center_focus" + +/obj/screen/radial/center/MouseExited(location, control, params) + . = ..() + icon_state = "radial_center" + +/obj/screen/radial/center/Click(location, control, params) + if(usr.client == parent.current_user) + parent.finished = TRUE + +/datum/radial_menu + var/list/choices = list() //List of choice id's + var/list/choices_icons = list() //choice_id -> icon + var/list/choices_values = list() //choice_id -> choice + var/list/page_data = list() //list of choices per page + + + var/selected_choice + var/list/obj/screen/elements = list() + var/obj/screen/radial/center/close_button + var/client/current_user + var/atom/anchor + var/image/menu_holder + var/finished = FALSE + var/datum/callback/custom_check_callback + var/next_check = 0 + var/check_delay = DEFAULT_CHECK_DELAY + + var/radius = 32 + var/starting_angle = 0 + var/ending_angle = 360 + var/zone = 360 + var/min_angle = 45 //Defaults are setup for this value, if you want to make the menu more dense these will need changes. + var/max_elements + var/pages = 1 + var/current_page = 1 + + var/hudfix_method = TRUE //TRUE to change anchor to user, FALSE to shift by py_shift + var/py_shift = 0 + var/entry_animation = TRUE + +//If we swap to vis_contens inventory these will need a redo +/datum/radial_menu/proc/check_screen_border(mob/user) + var/atom/movable/AM = anchor + if(!istype(AM) || !AM.screen_loc) + return + if(AM in user.client.screen) + if(hudfix_method) + anchor = user + else + py_shift = 32 + restrict_to_dir(NORTH) //I was going to parse screen loc here but that's more effort than it's worth. + +//Sets defaults +//These assume 45 deg min_angle +/datum/radial_menu/proc/restrict_to_dir(dir) + switch(dir) + if(NORTH) + starting_angle = 270 + ending_angle = 135 + if(SOUTH) + starting_angle = 90 + ending_angle = 315 + if(EAST) + starting_angle = 0 + ending_angle = 225 + if(WEST) + starting_angle = 180 + ending_angle = 45 + +/datum/radial_menu/proc/setup_menu(use_tooltips) + if(ending_angle > starting_angle) + zone = ending_angle - starting_angle + else + zone = 360 - starting_angle + ending_angle + + max_elements = round(zone / min_angle) + var/paged = max_elements < choices.len + if(elements.len < max_elements) + var/elements_to_add = max_elements - elements.len + for(var/i in 1 to elements_to_add) //Create all elements + var/obj/screen/radial/slice/new_element = new /obj/screen/radial/slice + new_element.tooltips = use_tooltips + new_element.parent = src + elements += new_element + + var/page = 1 + page_data = list(null) + var/list/current = list() + var/list/choices_left = choices.Copy() + while(choices_left.len) + if(current.len == max_elements) + page_data[page] = current + page++ + page_data.len++ + current = list() + if(paged && current.len == max_elements - 1) + current += NEXT_PAGE_ID + continue + else + current += popleft(choices_left) + if(paged && current.len < max_elements) + current += NEXT_PAGE_ID + + page_data[page] = current + pages = page + current_page = 1 + update_screen_objects(anim = entry_animation) + +/datum/radial_menu/proc/update_screen_objects(anim = FALSE) + var/list/page_choices = page_data[current_page] + var/angle_per_element = round(zone / page_choices.len) + for(var/i in 1 to elements.len) + var/obj/screen/radial/E = elements[i] + var/angle = WRAP(starting_angle + (i - 1) * angle_per_element,0,360) + if(i > page_choices.len) + HideElement(E) + else + SetElement(E,page_choices[i],angle,anim = anim,anim_order = i) + +/datum/radial_menu/proc/HideElement(obj/screen/radial/slice/E) + E.cut_overlays() + E.alpha = 0 + E.name = "None" + E.maptext = null + E.mouse_opacity = MOUSE_OPACITY_TRANSPARENT + E.choice = null + E.next_page = FALSE + +/datum/radial_menu/proc/SetElement(obj/screen/radial/slice/E,choice_id,angle,anim,anim_order) + //Position + var/py = round(cos(angle) * radius) + py_shift + var/px = round(sin(angle) * radius) + if(anim) + var/timing = anim_order * 0.5 + var/matrix/starting = matrix() + starting.Scale(0.1,0.1) + E.transform = starting + var/matrix/TM = matrix() + animate(E,pixel_x = px,pixel_y = py, transform = TM, time = timing) + else + E.pixel_y = py + E.pixel_x = px + + //Visuals + E.alpha = 255 + E.mouse_opacity = MOUSE_OPACITY_ICON + E.cut_overlays() + if(choice_id == NEXT_PAGE_ID) + E.name = "Next Page" + E.next_page = TRUE + E.add_overlay("radial_next") + else + if(istext(choices_values[choice_id])) + E.name = choices_values[choice_id] + else + var/atom/movable/AM = choices_values[choice_id] //Movables only + E.name = AM.name + E.choice = choice_id + E.maptext = null + E.next_page = FALSE + if(choices_icons[choice_id]) + E.add_overlay(choices_icons[choice_id]) + +/datum/radial_menu/New() + close_button = new + close_button.parent = src + +/datum/radial_menu/proc/Reset() + choices.Cut() + choices_icons.Cut() + choices_values.Cut() + current_page = 1 + +/datum/radial_menu/proc/element_chosen(choice_id,mob/user) + selected_choice = choices_values[choice_id] + +/datum/radial_menu/proc/get_next_id() + return "c_[choices.len]" + +/datum/radial_menu/proc/set_choices(list/new_choices, use_tooltips) + if(choices.len) + Reset() + for(var/E in new_choices) + var/id = get_next_id() + choices += id + choices_values[id] = E + if(new_choices[E]) + var/I = extract_image(new_choices[E]) + if(I) + choices_icons[id] = I + setup_menu(use_tooltips) + + +/datum/radial_menu/proc/extract_image(E) + var/mutable_appearance/MA = new /mutable_appearance(E) + if(MA) + MA.layer = LAYER_HUD_ABOVE + MA.appearance_flags |= RESET_TRANSFORM + return MA + + +/datum/radial_menu/proc/next_page() + if(pages > 1) + current_page = WRAP(current_page + 1,1,pages+1) + update_screen_objects() + +/datum/radial_menu/proc/show_to(mob/M) + if(current_user) + hide() + if(!M.client || !anchor) + return + current_user = M.client + //Blank + menu_holder = image(icon='icons/effects/effects.dmi',loc=anchor,icon_state="nothing",layer = LAYER_HUD_ABOVE) + menu_holder.appearance_flags |= KEEP_APART + menu_holder.vis_contents += elements + close_button + current_user.images += menu_holder + +/datum/radial_menu/proc/hide() + if(current_user) + current_user.images -= menu_holder + +/datum/radial_menu/proc/wait(atom/user, atom/anchor, require_near = FALSE) + while (current_user && !finished && !selected_choice) + if(require_near && !in_range(anchor, user)) + return + if(custom_check_callback && next_check < world.time) + if(!custom_check_callback.Invoke()) + return + else + next_check = world.time + check_delay + stoplag(1) + +/datum/radial_menu/Destroy() + Reset() + hide() + QDEL_NULL(custom_check_callback) + . = ..() + +/* + Presents radial menu to user anchored to anchor (or user if the anchor is currently in users screen) + Choices should be a list where list keys are movables or text used for element names and return value + and list values are movables/icons/images used for element icons +*/ +/proc/show_radial_menu(mob/user, atom/anchor, list/choices, uniqueid, radius, datum/callback/custom_check, require_near = FALSE, tooltips = FALSE) + if(!user || !anchor || !length(choices)) + return + if(!uniqueid) + uniqueid = "defmenu_[REF(user)]_[REF(anchor)]" + + if(GLOB.radial_menus[uniqueid]) + return + + var/datum/radial_menu/menu = new + GLOB.radial_menus[uniqueid] = menu + if(radius) + menu.radius = radius + if(istype(custom_check)) + menu.custom_check_callback = custom_check + menu.anchor = anchor + menu.check_screen_border(user) //Do what's needed to make it look good near borders or on hud + menu.set_choices(choices, tooltips) + menu.show_to(user) + menu.wait(user, anchor, require_near) + var/answer = menu.selected_choice + QDEL_NULL(menu) + GLOB.radial_menus -= uniqueid + return answer \ No newline at end of file diff --git a/code/_onclick/hud/radial_persistent.dm b/code/_onclick/hud/radial_persistent.dm new file mode 100644 index 0000000000..feaf17c1b2 --- /dev/null +++ b/code/_onclick/hud/radial_persistent.dm @@ -0,0 +1,75 @@ +/* + A derivative of radial menu which persists onscreen until closed and invokes a callback each time an element is clicked +*/ + +/obj/screen/radial/persistent/center + name = "Close Menu" + icon_state = "radial_center" + +/obj/screen/radial/persistent/center/Click(location, control, params) + if(usr.client == parent.current_user) + parent.element_chosen(null,usr) + +/obj/screen/radial/persistent/center/MouseEntered(location, control, params) + . = ..() + icon_state = "radial_center_focus" + +/obj/screen/radial/persistent/center/MouseExited(location, control, params) + . = ..() + icon_state = "radial_center" + + + +/datum/radial_menu/persistent + var/uniqueid + var/datum/callback/select_proc_callback + +/datum/radial_menu/persistent/New() + close_button = new /obj/screen/radial/persistent/center + close_button.parent = src + + +/datum/radial_menu/persistent/element_chosen(choice_id,mob/user) + select_proc_callback.Invoke(choices_values[choice_id]) + + +/datum/radial_menu/persistent/proc/change_choices(list/newchoices, tooltips) + if(!newchoices.len) + return + Reset() + set_choices(newchoices,tooltips) + +/datum/radial_menu/persistent/Destroy() + QDEL_NULL(select_proc_callback) + GLOB.radial_menus -= uniqueid + Reset() + hide() + . = ..() + +/* + Creates a persistent radial menu and shows it to the user, anchored to anchor (or user if the anchor is currently in users screen). + Choices should be a list where list keys are movables or text used for element names and return value + and list values are movables/icons/images used for element icons + Select_proc is the proc to be called each time an element on the menu is clicked, and should accept the chosen element as its final argument + Clicking the center button will return a choice of null +*/ +/proc/show_radial_menu_persistent(mob/user, atom/anchor, list/choices, datum/callback/select_proc, uniqueid, radius, tooltips = FALSE) + if(!user || !anchor || !length(choices) || !select_proc) + return + if(!uniqueid) + uniqueid = "defmenu_[REF(user)]_[REF(anchor)]" + + if(GLOB.radial_menus[uniqueid]) + return + + var/datum/radial_menu/persistent/menu = new + menu.uniqueid = uniqueid + GLOB.radial_menus[uniqueid] = menu + if(radius) + menu.radius = radius + menu.select_proc_callback = select_proc + menu.anchor = anchor + menu.check_screen_border(user) //Do what's needed to make it look good near borders or on hud + menu.set_choices(choices, tooltips) + menu.show_to(user) + return menu diff --git a/code/game/objects/items/bells.dm b/code/game/objects/items/bells.dm new file mode 100644 index 0000000000..43046d22b7 --- /dev/null +++ b/code/game/objects/items/bells.dm @@ -0,0 +1,90 @@ +/obj/item/weapon/deskbell + name = "desk bell" + desc = "An annoying bell. Ring for service." + icon = 'icons/obj/items.dmi' + icon_state = "deskbell" + force = 2 + throwforce = 2 + w_class = 2.0 + var/broken + attack_verb = list("annoyed") + var/static/radial_examine = image(icon = 'icons/mob/radial.dmi', icon_state = "radial_examine") + var/static/radial_use = image(icon = 'icons/mob/radial.dmi', icon_state = "radial_use") + +/obj/item/weapon/deskbell/examine(mob/user) + ..() + if(broken) + to_chat(user,"It looks damaged, the ringer is stuck firmly inside.") + +/obj/item/weapon/deskbell/attack(mob/target as mob, mob/living/user as mob) + if(!broken) + playsound(user.loc, 'sound/effects/deskbell.ogg', 50, 1) + ..() + +/obj/item/weapon/deskbell/attack_hand(mob/user) + + //This defines the radials and what call we're assiging to them. + var/list/options = list() + options["examine"] = radial_examine + if(!broken) + options["use"] = radial_use + + + // Just an example, if the bell had no options, due to conditionals, nothing would happen here. + if(length(options) < 1) + return + + // Right, if there's only one available radial... + // For example, say, the bell's broken so you can only examine, it just does that (doesn't show radial).. + var/list/choice = list() + if(length(options) == 1) + for(var/key in options) + choice = key + else + // If we have other options, it will show the radial menu for the player to decide. + choice = show_radial_menu(user, src, options, require_near = !issilicon(user)) + + // Once the player has decided their option, choose the behaviour that will happen under said option. + switch(choice) + if("examine") + examine(user) + + if("use") + if(check_ability(user)) + ring(user) + add_fingerprint(user) + +/obj/item/weapon/deskbell/proc/ring(mob/user) + if(user.a_intent == "harm") + playsound(user.loc, 'sound/effects/deskbell_rude.ogg', 50, 1) + to_chat(user,"You hammer [src] rudely!") + if (prob(2)) + break_bell(user) + else + playsound(user.loc, 'sound/effects/deskbell.ogg', 50, 1) + to_chat(user,"You gracefully ring [src].") + +/obj/item/weapon/deskbell/proc/check_ability(mob/user) + if (ishuman(user)) + var/mob/living/carbon/human/H = user + var/obj/item/organ/external/temp = H.organs_by_name["r_hand"] + if (H.hand) + temp = H.organs_by_name["l_hand"] + if(temp && !temp.is_usable()) + to_chat(H,"You try to move your [temp.name], but cannot!") + return 0 + return 1 + else + to_chat(user,"You are not able to ring [src].") + return 0 + +/obj/item/weapon/deskbell/attackby(obj/item/i, mob/user, params) + if(!istype(i)) + return + if(!broken) + ring(user) + + +/obj/item/weapon/deskbell/proc/break_bell(mob/user) + to_chat(user,"The ringing abruptly stops as [src]'s ringer gets jammed inside!") + broken = 1 diff --git a/code/modules/materials/material_recipes.dm b/code/modules/materials/material_recipes.dm index 721b54826e..f45adba787 100644 --- a/code/modules/materials/material_recipes.dm +++ b/code/modules/materials/material_recipes.dm @@ -14,6 +14,7 @@ recipes += new/datum/stack_recipe("[display_name] grave marker", /obj/item/weapon/material/gravemarker, 5, time = 50, supplied_material = "[name]") recipes += new/datum/stack_recipe("[display_name] ring", /obj/item/clothing/gloves/ring/material, 1, on_floor = 1, supplied_material = "[name]") recipes += new/datum/stack_recipe("[display_name] bracelet", /obj/item/clothing/accessory/bracelet/material, 1, on_floor = 1, supplied_material = "[name]") + recipes += new/datum/stack_recipe("[display_name] deskbell", /obj/item/weapon/deskbell, 1, on_floor = 1, supplied_material = "[name]") if(integrity>=50) recipes += new/datum/stack_recipe("[display_name] door", /obj/structure/simple_door, 10, one_per_turf = 1, on_floor = 1, supplied_material = "[name]") diff --git a/icons/mob/radial.dmi b/icons/mob/radial.dmi new file mode 100644 index 0000000000..cfdd0e549a Binary files /dev/null and b/icons/mob/radial.dmi differ diff --git a/icons/obj/items.dmi b/icons/obj/items.dmi index d2d986c205..e3de287a99 100644 Binary files a/icons/obj/items.dmi and b/icons/obj/items.dmi differ diff --git a/sound/effects/deskbell.ogg b/sound/effects/deskbell.ogg new file mode 100644 index 0000000000..ef40794cf4 Binary files /dev/null and b/sound/effects/deskbell.ogg differ diff --git a/sound/effects/deskbell_rude.ogg b/sound/effects/deskbell_rude.ogg new file mode 100644 index 0000000000..1698a15fb7 Binary files /dev/null and b/sound/effects/deskbell_rude.ogg differ diff --git a/vorestation.dme b/vorestation.dme index c5890a560a..bc9be432fc 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -142,6 +142,8 @@ #include "code\_onclick\hud\human.dm" #include "code\_onclick\hud\movable_screen_objects.dm" #include "code\_onclick\hud\other_mobs.dm" +#include "code\_onclick\hud\radial.dm" +#include "code\_onclick\hud\radial_persistent.dm" #include "code\_onclick\hud\robot.dm" #include "code\_onclick\hud\robot_vr.dm" #include "code\_onclick\hud\screen_objects.dm" @@ -933,6 +935,7 @@ #include "code\game\objects\effects\temporary_visuals\projectiles\tracer.dm" #include "code\game\objects\items\antag_spawners.dm" #include "code\game\objects\items\apc_frame.dm" +#include "code\game\objects\items\bells.dm" #include "code\game\objects\items\blueprints.dm" #include "code\game\objects\items\bodybag.dm" #include "code\game\objects\items\contraband.dm"