Files
Bubberstation/code/datums/components/rotation.dm
John Willard 1389351ef9 You can now move and talk through statues and mannequins (#81188)
## About The Pull Request

I recently played a game where I rotated my skeleton model while
rotating my own character at the same time and it being in sync gave me
the realization on how cool it would be if the Coroner was able to
simply control the skeleton body.

I find skeleton displays very funny and I want to see more funny things
happen with them, so I thought this would be a good place to start, with
the benefits that it also works on mannequins and statues too so they
aren't left out.

Basically, while it is unanchored, if you have a statue/mannequin
grabbed, it will change its direction as you do, and speak the same
words you do. Your own messages can only be heard if the person is
directly next to you, revealing that it was you talking through it all
along.

I was originally gonna add this to the simple rotation component but
moved off when I decided to add talking through it, I left in the code
improvements I made to the component though since it is one of the
oldest components and hasn't been touched in a while.

Video demonstration (before I added the person also talking, just ignore
that missing)

https://github.com/tgstation/tgstation/assets/53777086/27242fc3-9649-418d-95cb-b31619319e97

While fixing the Toilet bong's rotation stuff I noticed a lot of it
wasn't up to proper code standards so I went over it and fixed issues I
had with it. It now doesn't give text saying you found something nasty
to species that still likes mice (like flypeople), and fixed its update
appearance to match the codebase standard set by the introduction of
``update_appearance`` many years ago.

## Why It's Good For The Game

It's a funny small idea I had and got inspired to add, it's a niche
mechanic that I think fits the aesthetic I am going for with Coroner and
also give a funny interaction with the human-like inanimate objects.

## Changelog

🆑
fix: Species that can eat mice don't get disgusted from seeing one in
the toilet bong.
add: Grabbing an unwrenched statue/mannequin/skeleton model will now
move its direction as you move yours, and you can talk through it.
/🆑
2024-02-08 14:19:31 +01:00

138 lines
5.1 KiB
Plaintext

/datum/component/simple_rotation
/// Additional stuff to do after rotation
var/datum/callback/post_rotation
/// Rotation flags for special behavior
var/rotation_flags = NONE
/**
* Adds the ability to rotate an object by Alt-click or using Right-click verbs.
*
* args:
* * rotation_flags (optional) Bitflags that determine behavior for rotation (defined at the top of this file)
* * post_rotation (optional) Callback proc that is used after the object is rotated (sound effects, balloon alerts, etc.)
**/
/datum/component/simple_rotation/Initialize(rotation_flags = NONE, post_rotation)
if(!ismovable(parent))
return COMPONENT_INCOMPATIBLE
var/atom/movable/source = parent
source.flags_1 |= HAS_CONTEXTUAL_SCREENTIPS_1
src.rotation_flags = rotation_flags
src.post_rotation = post_rotation || CALLBACK(src, PROC_REF(default_post_rotation))
/datum/component/simple_rotation/RegisterWithParent()
RegisterSignal(parent, COMSIG_CLICK_ALT, PROC_REF(rotate_left))
RegisterSignal(parent, COMSIG_CLICK_ALT_SECONDARY, PROC_REF(rotate_right))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(ExamineMessage))
RegisterSignal(parent, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM, PROC_REF(on_requesting_context_from_item))
return ..()
/datum/component/simple_rotation/PostTransfer()
//Because of the callbacks which we don't track cleanly we can't transfer this
//item cleanly, better to let the new of the new item create a new rotation datum
//instead (there's no real state worth transferring)
return COMPONENT_NOTRANSFER
/datum/component/simple_rotation/UnregisterFromParent()
UnregisterSignal(parent, list(
COMSIG_CLICK_ALT,
COMSIG_CLICK_ALT_SECONDARY,
COMSIG_ATOM_EXAMINE,
COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM,
))
return ..()
/datum/component/simple_rotation/Destroy()
post_rotation = null
return ..()
/datum/component/simple_rotation/proc/ExamineMessage(datum/source, mob/user, list/examine_list)
SIGNAL_HANDLER
if(rotation_flags & ROTATION_REQUIRE_WRENCH)
examine_list += span_notice("This requires a wrench to be rotated.")
/datum/component/simple_rotation/proc/rotate_right(datum/source, mob/user)
SIGNAL_HANDLER
rotate(user, ROTATION_CLOCKWISE)
/datum/component/simple_rotation/proc/rotate_left(datum/source, mob/user)
SIGNAL_HANDLER
rotate(user, ROTATION_COUNTERCLOCKWISE)
/datum/component/simple_rotation/proc/rotate(mob/user, degrees)
if(QDELETED(user))
CRASH("[src] is being rotated [user ? "with a qdeleting" : "without a"] user")
if(!istype(user))
CRASH("[src] is being rotated without a user of the wrong type: [user.type]")
if(!isnum(degrees))
CRASH("[src] is being rotated without providing the amount of degrees needed")
if(!can_be_rotated(user, degrees) || !can_user_rotate(user, degrees))
return
var/obj/rotated_obj = parent
rotated_obj.setDir(turn(rotated_obj.dir, degrees))
if(rotation_flags & ROTATION_REQUIRE_WRENCH)
playsound(rotated_obj, 'sound/items/ratchet.ogg', 50, TRUE)
post_rotation.Invoke(user, degrees)
/datum/component/simple_rotation/proc/can_user_rotate(mob/user, degrees)
if(isliving(user) && user.can_perform_action(parent, NEED_DEXTERITY))
return TRUE
if((rotation_flags & ROTATION_GHOSTS_ALLOWED) && isobserver(user) && CONFIG_GET(flag/ghost_interaction))
return TRUE
return FALSE
/datum/component/simple_rotation/proc/can_be_rotated(mob/user, degrees, silent=FALSE)
var/obj/rotated_obj = parent
if(!rotated_obj.Adjacent(user))
silent = TRUE
if(rotation_flags & ROTATION_REQUIRE_WRENCH)
if(!isliving(user))
return FALSE
var/obj/item/tool = user.get_active_held_item()
if(!tool || tool.tool_behaviour != TOOL_WRENCH)
if(!silent)
rotated_obj.balloon_alert(user, "need a wrench!")
return FALSE
if(!(rotation_flags & ROTATION_IGNORE_ANCHORED) && rotated_obj.anchored)
if(istype(rotated_obj, /obj/structure/window) && !silent)
rotated_obj.balloon_alert(user, "need to unscrew!")
else if(!silent)
rotated_obj.balloon_alert(user, "need to unwrench!")
return FALSE
if(rotation_flags & ROTATION_NEEDS_ROOM)
var/target_dir = turn(rotated_obj.dir, degrees)
var/obj/structure/window/rotated_window = rotated_obj
var/fulltile = istype(rotated_window) ? rotated_window.fulltile : FALSE
if(!valid_build_direction(rotated_obj.loc, target_dir, is_fulltile = fulltile))
if(!silent)
rotated_obj.balloon_alert(user, "can't rotate in that direction!")
return FALSE
return TRUE
/datum/component/simple_rotation/proc/default_post_rotation(mob/user, degrees)
return
// maybe we don't need the item context proc but instead the hand one? since we don't need to check held_item
/datum/component/simple_rotation/proc/on_requesting_context_from_item(atom/source, list/context, obj/item/held_item, mob/user)
SIGNAL_HANDLER
var/rotation_screentip = FALSE
if(can_be_rotated(user, ROTATION_CLOCKWISE, silent=TRUE))
context[SCREENTIP_CONTEXT_ALT_LMB] = "Rotate left"
rotation_screentip = TRUE
if(can_be_rotated(user, ROTATION_COUNTERCLOCKWISE, silent=TRUE))
context[SCREENTIP_CONTEXT_ALT_RMB] = "Rotate right"
rotation_screentip = TRUE
if(rotation_screentip)
return CONTEXTUAL_SCREENTIP_SET
return NONE