Skip to content

Commit

Permalink
Simplified bodyzone targeting preference (#9845)
Browse files Browse the repository at this point in the history
* Zone selector for patches

* Preference

* Adds in a generic bodyzone part getter

* Combat zone targetting

* Update mob_helpers.dm

* Fixes some mistakes

* Bug fixes, makes the task not have a delay

* Makes the bodyzone wheel represent the positions of the limb

* Mechanical repair interaction

* Adds in the flashlight interaction

* Hotkeys can now be hidden based on the values of preferences

* Scroll hotkey for cycling the selected area

* Surgery, cleans up the priority code and spelling mistake correction

* Update mob_helpers.dm

* Fixes the logic in the technophile cult

* Voodoo Doll

* Update code/__DEFINES/preferences.dm

Co-authored-by: itsmeow <[email protected]>

* Review addresses

* Adjacency checks

* Changes the dropdown to buttons

* Fix

* Named parameter arguments

* Changes the required preference to a typepath

* Update _basemap.dm

* Fixes wrong variable name

* You now have to be adjacent to medical items to use them as well as the target

* You can't select a bodyzone to heal while healing

* Removes the ERP message when using simplified zone targeting since it makes less sense on the legs

* Fixes holoparasite missing parameters

* Makes it so that mechanical repairs repeat

* Fixes some linter catches

* Fixes surgery on groin being impossible

---------

Co-authored-by: itsmeow <[email protected]>
  • Loading branch information
PowerfulBacon and itsmeow authored Dec 19, 2023
1 parent e54b1ed commit 3518341
Show file tree
Hide file tree
Showing 116 changed files with 1,293 additions and 747 deletions.
2 changes: 2 additions & 0 deletions beestation.dme
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@
#include "code\__HELPERS\level_traits.dm"
#include "code\__HELPERS\maths.dm"
#include "code\__HELPERS\matrices.dm"
#include "code\__HELPERS\mob_bodyzone.dm"
#include "code\__HELPERS\mobs.dm"
#include "code\__HELPERS\mouse_control.dm"
#include "code\__HELPERS\names.dm"
Expand Down Expand Up @@ -2342,6 +2343,7 @@
#include "code\modules\client\preferences\entries\player\tooltips.dm"
#include "code\modules\client\preferences\entries\player\ui_style.dm"
#include "code\modules\client\preferences\entries\player\window_flashing.dm"
#include "code\modules\client\preferences\entries\player\zone_selection.dm"
#include "code\modules\client\preferences\middleware\_middleware.dm"
#include "code\modules\client\preferences\middleware\antags.dm"
#include "code\modules\client\preferences\middleware\jobs.dm"
Expand Down
4 changes: 4 additions & 0 deletions code/__DEFINES/async.dm
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@
#define ASYNC_RETURN(value) created_task.mark_completed(value);\
return;

#define ASYNC_RETURN_TASK(value) return value;

/// Waits for the provided task to be completed, or the timeout to expire.
/// Returns null if the timeout expires, or the task's result otherwise.
/// Note that if a task's result is null, then null will be returned.
/// This adds a delay for long periods of waiting, so using continue_with
/// is preferred.
#define AWAIT(TASK, TIMEOUT) get_result(TASK, TIMEOUT)

/proc/get_result(datum/task/task, timeout)
Expand Down
7 changes: 7 additions & 0 deletions code/__DEFINES/bodyparts.dm
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
#define IS_ORGANIC_LIMB(A) (A.bodytype & BODYTYPE_ORGANIC)

#define BODYZONE_STYLE_DEFAULT 0
#define BODYZONE_STYLE_MEDICAL 1

#define BODYZONE_CONTEXT_COMBAT 0
#define BODYZONE_CONTEXT_INJECTION 1
#define BODYZONE_CONTEXT_ROBOTIC_LIMB_HEALING 2
4 changes: 4 additions & 0 deletions code/__DEFINES/combat.dm
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ GLOBAL_LIST_INIT(shove_disarming_types, typecacheof(list(
#define GRENADE_NONCLUMSY_FUMBLE 2
#define GRENADE_NO_FUMBLE 3

#define BODY_GROUP_CHEST_HEAD "chesthead"
#define BODY_GROUP_LEGS "legs"
#define BODY_GROUP_ARMS "arms"

#define BODY_ZONE_HEAD "head"
#define BODY_ZONE_CHEST "chest"
#define BODY_ZONE_L_ARM "l_arm"
Expand Down
2 changes: 2 additions & 0 deletions code/__DEFINES/keybinding.dm
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
#define COMSIG_KB_MOB_TARGETRIGHTLEG_DOWN "keybinding_mob_targetrightleg_down"
#define COMSIG_KB_MOB_TARGETBODYGROIN_DOWN "keybinding_mob_targetbodygroin_down"
#define COMSIG_KB_MOB_TARGETLEFTLEG_DOWN "keybinding_mob_targetleftleg_down"
#define COMSIG_KB_MOB_TARGETCYCLEUP_DOWN "keybinding_mob_targetcycleup_down"
#define COMSIG_KB_MOB_TARGETCYCLEDOWN_DOWN "keybinding_mob_targetcycledown_down"
#define COMSIG_KB_MOB_PREVENTMOVEMENT_DOWN "keybinding_mob_preventmovement_down"
#define COMSIG_KB_MOB_MOVEUP_DOWN "keybinding_mob_moveup_down"
#define COMSIG_KB_MOB_MOVEDOWN_DOWN "keybinding_mob_movedown_down"
Expand Down
4 changes: 4 additions & 0 deletions code/__DEFINES/preferences.dm
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ GLOBAL_PROTECT(undatumized_preference_tags_character)
#define PREFERENCE_SHEET_LARGE "preferences_l"
#define PREFERENCE_SHEET_HUGE "preferences_h"

#define PREFERENCE_BODYZONE_SIMPLIFIED "Simplified Targeting" // Use the simplified system
#define PREFERENCE_BODYZONE_INTENT "Precise Targeting" // Use the bodyzone intent system

/// Stop loading immediately, inform the user. Do not save the data.
#define PREFERENCE_LOAD_ERROR 0
/// There is no data to load, they are a guest and will never have this data.
Expand All @@ -186,3 +189,4 @@ GLOBAL_PROTECT(undatumized_preference_tags_character)
#define PREFERENCE_LOAD_NO_DATA 2
/// Normal behavior - success!
#define PREFERENCE_LOAD_SUCCESS 3

142 changes: 142 additions & 0 deletions code/__HELPERS/mob_bodyzone.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@

#define NOT_PRESENT_COLOUR "#0d0d0d"
#define FULL_HEALTH_COLOUR "#11ff00"
#define LOW_DAMAGE_COLOUR "#d9ff00"
#define POOR_HEALTH_COLOUR "#ff0000"

/// Displays a UI around the target allowing the user to select which bodypart
/// that they want to act on.
/// Target: The location to show the user interface.
/// Precise: Toggle to include groin, eyes and mouth. If true, implies hide_non_present will be forced to false.
/// Icon Callback: The callback to run in order to get the selection zone overlay.
/// If you want to wait for the result use: AWAIT(select_bodyzone(target))
/mob/proc/select_bodyzone_from_wheel(atom/target, precise = FALSE, datum/callback/icon_callback, override_zones = null)
DECLARE_ASYNC
if (!client || !client.prefs)
ASYNC_RETURN(null)
if (!icon_callback)
icon_callback = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(select_bodyzone_limb_default))
// Determine what parts we want to show
var/list/bodyzone_options = list()
var/list/parts = list(BODY_ZONE_HEAD, BODY_ZONE_L_ARM, BODY_ZONE_L_LEG, BODY_ZONE_CHEST, BODY_ZONE_R_LEG, BODY_ZONE_R_ARM)
if (override_zones)
parts = override_zones
for (var/bodyzone in parts)
var/image/created_image = image(icon = ui_style2icon(client.prefs?.read_player_preference(/datum/preference/choiced/ui_style)), icon_state = "zone_sel")
var/selection_overlay = icon_callback.Invoke(src, target, bodyzone, FALSE)
created_image.overlays += selection_overlay
bodyzone_options[bodyzone] = created_image
var/result = show_radial_menu(src, target, bodyzone_options, radius = 40, require_near = TRUE, tooltips = TRUE)

// Disconnected or no result
if (!result || !client)
ASYNC_RETURN(null)

// Let the user choose more zones
var/list/suboptions = null
if (precise && result == BODY_ZONE_HEAD)
suboptions = list(BODY_ZONE_HEAD, BODY_ZONE_PRECISE_EYES, BODY_ZONE_PRECISE_MOUTH)
else if (precise && result == BODY_ZONE_CHEST)
suboptions = list(BODY_ZONE_CHEST, BODY_ZONE_PRECISE_GROIN)

if (suboptions)
bodyzone_options = list()
for (var/bodyzone in suboptions)
var/image/created_image = image(icon = ui_style2icon(client.prefs?.read_player_preference(/datum/preference/choiced/ui_style)), icon_state = "zone_sel")
var/selection_overlay = icon_callback.Invoke(src, target, bodyzone, !(bodyzone in parts))
created_image.overlays += selection_overlay
bodyzone_options[bodyzone] = created_image
result = show_radial_menu(src, target, bodyzone_options, radius = 40, require_near = TRUE, tooltips = TRUE)

// Disconnected or no result
if (!result || !client)
ASYNC_RETURN(null)
if (!(result in parts) && !(result in suboptions))
ASYNC_RETURN(null)
ASYNC_RETURN(result)

/proc/select_bodyzone_limb_default( mob/user, atom/target, bodyzone, is_precise_part = FALSE)
// Create the overlay
var/image/selection_overlay = image(icon = 'icons/mob/zone_sel.dmi', icon_state = bodyzone)
return selection_overlay

/proc/select_bodyzone_limb_health(accurate_health = FALSE, mob/user, atom/target, bodyzone, is_precise_part = FALSE)
// Get the colours
var/list/healthy = rgb2num(FULL_HEALTH_COLOUR)
var/list/damaged = rgb2num(LOW_DAMAGE_COLOUR)
var/list/unhealthy = rgb2num(POOR_HEALTH_COLOUR)
var/list/not_present = rgb2num(NOT_PRESENT_COLOUR)
// Create the overlay
var/image/selection_overlay = image(icon = 'icons/mob/zone_sel.dmi', icon_state = bodyzone)
// If the target is a mob, then colour the parts according to the part's health
if (isliving(target))
var/mob/living/living_target = target
var/obj/item/bodypart/target_part = living_target.get_bodypart(bodyzone)
// Determine what colour to make the indicator
var/list/new_colour
var/flash = FALSE
if (!target_part)
if (is_precise_part)
new_colour = healthy
else
new_colour = not_present
else
// 0 = healthy, 1 = dead
var/proportion = target_part.get_damage() / target_part.max_damage
if (!accurate_health)
proportion = proportion > 0 ? max(round(proportion, 1), 0.05) : 0
else
if (target_part.burn_dam > 0)
var/image/dam_indicator = image(icon = 'icons/mob/zone_dam.dmi', icon_state = "burn")
dam_indicator.appearance_flags = RESET_COLOR | RESET_ALPHA
selection_overlay.overlays += dam_indicator
if (target_part.brute_dam > 0)
var/image/dam_indicator = image(icon = 'icons/mob/zone_dam.dmi', icon_state = "brute")
dam_indicator.appearance_flags = RESET_COLOR | RESET_ALPHA
selection_overlay.overlays += dam_indicator
if (proportion > 0)
new_colour = list(
proportion * unhealthy[1] + (1 - proportion) * damaged[1],
proportion * unhealthy[2] + (1 - proportion) * damaged[2],
proportion * unhealthy[3] + (1 - proportion) * damaged[3],
)
else
new_colour = healthy
flash = proportion >= 0.01
// Set the colour
selection_overlay.color = list(
new_colour[1] / 255,
new_colour[2] / 255,
new_colour[3] / 255,
0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
)
if (flash)
animate(selection_overlay, time = 1 SECONDS, loop = -1, easing = SINE_EASING, color = list(
new_colour[1] / 255,
new_colour[2] / 255,
new_colour[3] / 255,
0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 2,
))
animate(time = 1 SECONDS, loop = -1, easing = SINE_EASING, color = list(
new_colour[1] / 255,
new_colour[2] / 255,
new_colour[3] / 255,
0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
))
return selection_overlay

/mob/proc/_is_holding(obj/item/item)
return get_active_held_item() == item

#undef NOT_PRESENT_COLOUR
#undef FULL_HEALTH_COLOUR
#undef POOR_HEALTH_COLOUR
11 changes: 10 additions & 1 deletion code/_onclick/click.dm
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@
playsound(usr.loc, 'sound/weapons/taser2.ogg', 75, 1)

LE.firer = src
LE.def_zone = ran_zone(zone_selected)
LE.def_zone = ran_zone(get_combat_bodyzone(A))
LE.preparePixelProjectile(A, src, params)
LE.fire()

Expand Down Expand Up @@ -473,6 +473,15 @@

/mob/proc/MouseWheelOn(atom/A, delta_x, delta_y, params)
SEND_SIGNAL(src, COMSIG_MOB_MOUSE_SCROLL_ON, A, delta_x, delta_y, params)
if (!client)
return
// Send the hotkey action
if (delta_y > 0)
client.keyDown("ScrollUp")
client.keyUp("ScrollUp")
else if (delta_y < 0)
client.keyDown("ScrollDown")
client.keyUp("ScrollDown")

/mob/dead/observer/proc/mouse_wheeled(atom/A, delta_x, delta_y, params)
SIGNAL_HANDLER
Expand Down
35 changes: 18 additions & 17 deletions code/_onclick/hud/screen_objects.dm
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@
var/list/modifiers = params2list(params)
var/icon_x = text2num(LAZYACCESS(modifiers, ICON_X))
var/icon_y = text2num(LAZYACCESS(modifiers, ICON_Y))
var/choice = get_zone_at(icon_x, icon_y)
var/choice = get_zone_at(usr, icon_x, icon_y)
if (!choice)
return 1

Expand All @@ -490,7 +490,7 @@
var/list/modifiers = params2list(params)
var/icon_x = text2num(LAZYACCESS(modifiers, ICON_X))
var/icon_y = text2num(LAZYACCESS(modifiers, ICON_Y))
var/choice = get_zone_at(icon_x, icon_y)
var/choice = get_zone_at(usr, icon_x, icon_y)

if(hovering == choice)
return
Expand All @@ -517,43 +517,44 @@
vis_contents -= hover_overlays_cache[hovering]
hovering = null

/atom/movable/screen/zone_sel/proc/get_zone_at(icon_x, icon_y)
/atom/movable/screen/zone_sel/proc/get_zone_at(mob/user, icon_x, icon_y)
var/simple_mode = user.client?.prefs.read_player_preference(/datum/preference/choiced/zone_select) == PREFERENCE_BODYZONE_SIMPLIFIED
switch(icon_y)
if(1 to 9) //Legs
switch(icon_x)
if(10 to 15)
return BODY_ZONE_R_LEG
return simple_mode ? BODY_GROUP_LEGS : BODY_ZONE_R_LEG
if(17 to 22)
return BODY_ZONE_L_LEG
return simple_mode ? BODY_GROUP_LEGS : BODY_ZONE_L_LEG
if(10 to 13) //Hands and groin
switch(icon_x)
if(8 to 11)
return BODY_ZONE_R_ARM
return simple_mode ? BODY_GROUP_ARMS : BODY_ZONE_R_ARM
if(12 to 20)
return BODY_ZONE_PRECISE_GROIN
return simple_mode ? BODY_GROUP_ARMS : BODY_ZONE_PRECISE_GROIN
if(21 to 24)
return BODY_ZONE_L_ARM
return simple_mode ? BODY_GROUP_ARMS : BODY_ZONE_L_ARM
if(14 to 22) //Chest and arms to shoulders
switch(icon_x)
if(8 to 11)
return BODY_ZONE_R_ARM
return simple_mode ? BODY_GROUP_ARMS : BODY_ZONE_R_ARM
if(12 to 20)
return BODY_ZONE_CHEST
return simple_mode ? BODY_GROUP_CHEST_HEAD : BODY_ZONE_CHEST
if(21 to 24)
return BODY_ZONE_L_ARM
return simple_mode ? BODY_GROUP_ARMS : BODY_ZONE_L_ARM
if(23 to 30) //Head, but we need to check for eye or mouth
if(icon_x in 12 to 20)
switch(icon_y)
if(23 to 24)
if(icon_x in 15 to 17)
return BODY_ZONE_PRECISE_MOUTH
return simple_mode ? BODY_GROUP_CHEST_HEAD : BODY_ZONE_PRECISE_MOUTH
if(26) //Eyeline, eyes are on 15 and 17
if(icon_x in 14 to 18)
return BODY_ZONE_PRECISE_EYES
return simple_mode ? BODY_GROUP_CHEST_HEAD : BODY_ZONE_PRECISE_EYES
if(25 to 27)
if(icon_x in 15 to 17)
return BODY_ZONE_PRECISE_EYES
return BODY_ZONE_HEAD
return simple_mode ? BODY_GROUP_CHEST_HEAD : BODY_ZONE_PRECISE_EYES
return simple_mode ? BODY_GROUP_CHEST_HEAD : BODY_ZONE_HEAD

/atom/movable/screen/zone_sel/proc/set_selected_zone(choice, mob/user)
if(user != hud?.mymob)
Expand All @@ -570,7 +571,7 @@
cut_overlay(selecting_appearance)
selecting_appearance = mutable_appearance('icons/mob/screen_gen.dmi', "[selecting]")
add_overlay(selecting_appearance)
hud?.mymob?.zone_selected = selecting
hud?.mymob?._set_zone_selected(selecting)

/atom/movable/screen/zone_sel/alien
icon = 'icons/mob/screen_alien.dmi'
Expand All @@ -580,7 +581,7 @@
cut_overlay(selecting_appearance)
selecting_appearance = mutable_appearance('icons/mob/screen_alien.dmi', "[selecting]")
add_overlay(selecting_appearance)
hud?.mymob?.zone_selected = selecting
hud?.mymob?._set_zone_selected(selecting)

/atom/movable/screen/zone_sel/robot
icon = 'icons/mob/screen_cyborg.dmi'
Expand Down
2 changes: 1 addition & 1 deletion code/datums/components/butchering.dm
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
user.show_message("<span class='danger'>[H]'s neck has already been already cut, you can't make the bleeding any worse!", 1, \
"<span class='danger'>Their neck has already been already cut, you can't make the bleeding any worse!")
return COMPONENT_ITEM_NO_ATTACK
if((H.health <= H.crit_threshold || (user.pulling == H && user.grab_state >= GRAB_NECK) || H.IsSleeping()) && user.zone_selected == BODY_ZONE_HEAD) // Only sleeping, neck grabbed, or crit, can be sliced.
if((H.health <= H.crit_threshold || (user.pulling == H && user.grab_state >= GRAB_NECK) || H.IsSleeping())) // Only sleeping, neck grabbed, or crit, can be sliced.
INVOKE_ASYNC(src, PROC_REF(startNeckSlice), source, H, user)
return COMPONENT_ITEM_NO_ATTACK

Expand Down
2 changes: 1 addition & 1 deletion code/datums/components/embedded.dm
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@
/datum/component/embedded/proc/checkRemoval(mob/living/carbon/victim, obj/item/I, mob/user)
SIGNAL_HANDLER

if(!istype(victim) || user.zone_selected != limb.body_zone || user.a_intent != INTENT_HELP)
if(!istype(victim) || user.is_zone_selected(limb.body_zone) || user.a_intent != INTENT_HELP)
return

var/damage_multiplier = 1
Expand Down
2 changes: 1 addition & 1 deletion code/datums/components/jousting.dm
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
var/msg
if(damage)
msg += "[user] [sharp? "impales" : "slams into"] [target] [sharp? "on" : "with"] their [parent]"
target.apply_damage(damage, BRUTE, user.zone_selected, 0)
target.apply_damage(damage, BRUTE, user.get_combat_bodyzone(target), 0)
if(prob(knockdown_chance))
msg += " and knocks [target] [target_buckled? "off of [target.buckled]" : "down"]"
if(target_buckled)
Expand Down
2 changes: 1 addition & 1 deletion code/datums/components/pellet_cloud.dm
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
shooter = user
var/turf/targloc = get_turf(target)
if(!zone_override)
zone_override = shooter.zone_selected
zone_override = shooter.get_combat_bodyzone(target)

for(var/i in 1 to num_pellets)
shell.ready_proj(target, user, SUPPRESSED_VERY, zone_override, fired_from)
Expand Down
Loading

0 comments on commit 3518341

Please sign in to comment.