Files
Bubberstation/code/datums/components/tippable.dm
SkyratBot 5072a5de13 [MIRROR] Adds sanity checking to the tippable component [MDB IGNORE] (#24140)
* Adds sanity checking to the tippable component (#78771)

## About The Pull Request

The tippable component doesn't actually have any sanity checking for if
the tippable thing has already been tipped. This means that if two
people try to tip something over at the same time, it will appear
untipped but be immobilized as though it were tipped. The sprite will
also stay permanently flipped until the bug is preformed again. Now that
doesn't happen. Closes #64232.
## Why It's Good For The Game

As funny as it is to see upside-down borgs running around, its still a
bug.
## Changelog
🆑
fix: Borgs will no longer become permanently upside-down if tipped over
by multiple people at the same time.
/🆑

* Adds sanity checking to the tippable component

---------

Co-authored-by: GPeckman <21979502+GPeckman@users.noreply.github.com>
2023-10-06 14:44:13 -07:00

260 lines
8.8 KiB
Plaintext

/**
* Tippable component. For making mobs able to be tipped, like cows and medibots.
*/
/datum/component/tippable
/// Time it takes to tip the mob. Can be 0, for instant tipping.
var/tip_time = 3 SECONDS
/// Time it takes to untip the mob. Can also be 0, for instant untip.
var/untip_time = 1 SECONDS
/// Time it takes for the mob to right itself. Can be 0 for instant self-righting, or null, to never self-right.
var/self_right_time = 60 SECONDS
/// Whether the mob is currently tipped.
var/is_tipped = FALSE
/// Callback to additional behavior before being tipped (on try_tip). Return anything from this callback to cancel the tip.
var/datum/callback/pre_tipped_callback
/// Callback to additional behavior after successfully tipping the mob.
var/datum/callback/post_tipped_callback
/// Callback to additional behavior after being untipped.
var/datum/callback/post_untipped_callback
/// Callback to any extra roleplay behaviour
var/datum/callback/roleplay_callback
///The timer given until they untip themselves
var/self_untip_timer
///Should we accept roleplay?
var/roleplay_friendly
///Have we roleplayed?
var/roleplayed = FALSE
///List of emotes that will half their untip time
var/list/roleplay_emotes
/datum/component/tippable/Initialize(
tip_time = 3 SECONDS,
untip_time = 1 SECONDS,
self_right_time = 60 SECONDS,
datum/callback/pre_tipped_callback,
datum/callback/post_tipped_callback,
datum/callback/post_untipped_callback,
roleplay_friendly = FALSE,
roleplay_emotes,
datum/callback/roleplay_callback,
)
if(!isliving(parent))
return COMPONENT_INCOMPATIBLE
src.tip_time = tip_time
src.untip_time = untip_time
src.self_right_time = self_right_time
src.pre_tipped_callback = pre_tipped_callback
src.post_tipped_callback = post_tipped_callback
src.post_untipped_callback = post_untipped_callback
src.roleplay_friendly = roleplay_friendly
src.roleplay_emotes = roleplay_emotes
src.roleplay_callback = roleplay_callback
/datum/component/tippable/RegisterWithParent()
RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND_SECONDARY, PROC_REF(interact_with_tippable))
if (roleplay_friendly)
RegisterSignal(parent, COMSIG_MOB_EMOTE, PROC_REF(accept_roleplay))
/datum/component/tippable/UnregisterFromParent()
UnregisterSignal(parent, COMSIG_ATOM_ATTACK_HAND_SECONDARY)
/datum/component/tippable/Destroy()
pre_tipped_callback = null
post_tipped_callback = null
post_untipped_callback = null
roleplay_callback = null
return ..()
/**
* Attempt to interact with [source], either tipping it or helping it up.
*
* source - the mob being tipped over
* user - the mob interacting with source
*/
/datum/component/tippable/proc/interact_with_tippable(mob/living/source, mob/user)
SIGNAL_HANDLER
var/mob/living/living_user = user
if(DOING_INTERACTION_WITH_TARGET(user, source))
return
if(istype(living_user) && !living_user.combat_mode)
return
if(is_tipped)
INVOKE_ASYNC(src, PROC_REF(try_untip), source, user)
else
INVOKE_ASYNC(src, PROC_REF(try_tip), source, user)
return COMPONENT_SECONDARY_CANCEL_ATTACK_CHAIN
/**
* Try to tip over [tipped_mob].
* If the mob is dead, or optional callback returns a value, or our do-after fails, we don't tip the mob.
* Otherwise, upon completing of the do_after, tip over the mob.
*
* tipped_mob - the mob being tipped over
* tipper - the mob tipping the tipped_mob
*/
/datum/component/tippable/proc/try_tip(mob/living/tipped_mob, mob/tipper)
if(tipped_mob.stat != CONSCIOUS && !HAS_TRAIT(tipped_mob, TRAIT_FORCED_STANDING))
return
if(pre_tipped_callback?.Invoke(tipper))
return
if(tip_time > 0)
to_chat(tipper, span_warning("You begin tipping over [tipped_mob]..."))
tipped_mob.visible_message(
span_warning("[tipper] begins tipping over [tipped_mob]."),
span_userdanger("[tipper] begins tipping you over!"),
ignored_mobs = tipper
)
if(!do_after(tipper, tip_time, target = tipped_mob))
if(!isnull(tipped_mob.client))
tipped_mob.log_message("was attempted to tip over by [key_name(tipper)]", LOG_VICTIM, log_globally = FALSE)
tipper.log_message("failed to tip over [key_name(tipped_mob)]", LOG_ATTACK)
to_chat(tipper, span_danger("You fail to tip over [tipped_mob]."))
return
do_tip(tipped_mob, tipper)
/**
* Actually tip over the mob, setting it to tipped.
* Also invoking any callbacks we have, with the tipper as the argument,
* and set a timer to right our self-right our tipped mob if we can.
*
* tipped_mob - the mob who was tipped
* tipper - the mob who tipped the tipped_mob
*/
/datum/component/tippable/proc/do_tip(mob/living/tipped_mob, mob/tipper)
if(QDELETED(tipped_mob))
CRASH("Tippable component: do_tip() called with QDELETED tipped_mob!")
if (is_tipped) // sanity check in case multiple people try to tip at the same time
return
to_chat(tipper, span_warning("You tip over [tipped_mob]."))
if (!isnull(tipped_mob.client))
tipped_mob.log_message("has been tipped over by [key_name(tipper)].", LOG_ATTACK)
tipper.log_message("has tipped over [key_name(tipped_mob)].", LOG_ATTACK)
tipped_mob.visible_message(
span_warning("[tipper] tips over [tipped_mob]."),
span_userdanger("You are tipped over by [tipper]!"),
ignored_mobs = tipper
)
set_tipped_status(tipped_mob, TRUE)
post_tipped_callback?.Invoke(tipper)
if(isnull(self_right_time))
return
else if(self_right_time <= 0)
right_self(tipped_mob)
else
self_untip_timer = addtimer(CALLBACK(src, PROC_REF(right_self), tipped_mob), self_right_time, TIMER_UNIQUE | TIMER_STOPPABLE)
/**
* Try to untip a mob that has been tipped.
* After a do-after is completed, we untip the mob.
*
* tipped_mob - the mob who is tipped
* untipper - the mob who is untipping the tipped_mob
*/
/datum/component/tippable/proc/try_untip(mob/living/tipped_mob, mob/untipper)
if(untip_time > 0)
to_chat(untipper, span_notice("You begin righting [tipped_mob]..."))
tipped_mob.visible_message(
span_notice("[untipper] begins righting [tipped_mob]."),
span_notice("[untipper] begins righting you."),
ignored_mobs = untipper
)
if(!do_after(untipper, untip_time, target = tipped_mob))
to_chat(untipper, span_warning("You fail to right [tipped_mob]."))
return
do_untip(tipped_mob, untipper)
/**
* Actually untip over the mob, setting it to untipped.
* Also invoke any untip callbacks we have, with the untipper as the argument.
*
* tipped_mob - the mob who was tipped
* tipper - the mob who tipped the tipped_mob
*/
/datum/component/tippable/proc/do_untip(mob/living/tipped_mob, mob/untipper)
if(QDELETED(tipped_mob))
return
if (!is_tipped) // sanity check in case multiple people try to untip at the same time
return
to_chat(untipper, span_notice("You right [tipped_mob]."))
tipped_mob.visible_message(
span_notice("[untipper] rights [tipped_mob]."),
span_notice("You are righted by [untipper]!"),
ignored_mobs = untipper
)
if(self_untip_timer)
deltimer(self_untip_timer)
set_tipped_status(tipped_mob, FALSE)
post_untipped_callback?.Invoke(untipper)
/**
* Proc called after a timer to have a tipped mob un-tip itself after a certain length of time.
* Sets our mob to untipped and invokes the untipped callback without any arguments if we have one.
*
* tipped_mob - the mob who was tipped, and is freeing itself
*/
/datum/component/tippable/proc/right_self(mob/living/tipped_mob)
if(!is_tipped || QDELETED(tipped_mob))
return
set_tipped_status(tipped_mob, FALSE)
post_untipped_callback?.Invoke()
tipped_mob.visible_message(
span_notice("[tipped_mob] rights itself."),
span_notice("You right yourself.")
)
/**
* Toggles our tipped status between tipped or untipped (TRUE or FALSE)
* also handles rotating our mob and adding immobilization traits
*
* tipped_mob - the mob we're setting to tipped or untipped
* new_status - the tipped status we're setting the mob to - TRUE for tipped, FALSE for untipped
*/
/datum/component/tippable/proc/set_tipped_status(mob/living/tipped_mob, new_status = FALSE)
is_tipped = new_status
if(is_tipped)
tipped_mob.transform = turn(tipped_mob.transform, 180)
ADD_TRAIT(tipped_mob, TRAIT_IMMOBILIZED, TIPPED_OVER)
else
tipped_mob.transform = turn(tipped_mob.transform, -180)
REMOVE_TRAIT(tipped_mob, TRAIT_IMMOBILIZED, TIPPED_OVER)
/**
* Accepts "roleplay" in the form of emotes, which removes a quarter of the remaining time left to untip ourself.
*
* Arguments:
* * mob/living/user - The tipped mob
* * datum/emote/emote - The emote used by the mob
*/
/datum/component/tippable/proc/accept_roleplay(mob/living/user, datum/emote/emote)
SIGNAL_HANDLER
if (!is_tipped)
return
if (roleplayed)
return
if (!is_type_in_list(emote, roleplay_emotes))
return
var/time_left = timeleft(self_untip_timer)
deltimer(self_untip_timer)
self_untip_timer = addtimer(CALLBACK(src, PROC_REF(right_self), user), time_left * 0.75, TIMER_UNIQUE | TIMER_STOPPABLE)
roleplayed = TRUE
roleplay_callback?.Invoke(user)