Merge pull request #6169 from GeneriedJenelle/patch-1

Ports Radial Menus
This commit is contained in:
Neerti
2019-05-28 14:25:17 -04:00
committed by VirgoBot
parent fd340b1104
commit f5f20628fd
12 changed files with 493 additions and 0 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

313
code/_onclick/hud/radial.dm Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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,"<b>It looks damaged, the ringer is stuck firmly inside.</b>")
/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,"<span class='notice'>You hammer [src] rudely!</span>")
if (prob(2))
break_bell(user)
else
playsound(user.loc, 'sound/effects/deskbell.ogg', 50, 1)
to_chat(user,"<span class='notice'>You gracefully ring [src].</span>")
/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,"<span class='notice'>You try to move your [temp.name], but cannot!</span>")
return 0
return 1
else
to_chat(user,"<span class='notice'>You are not able to ring [src].</span>")
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,"<span class='notice'>The ringing abruptly stops as [src]'s ringer gets jammed inside!</span>")
broken = 1

View File

@@ -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]")

BIN
icons/mob/radial.dmi Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

BIN
sound/effects/deskbell.ogg Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -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"