Skip to content

Commit

Permalink
Meat Hook Rework (Accidental Features) (tgstation#80002)
Browse files Browse the repository at this point in the history
## About The Pull Request

Or, how I tried to kill `/datum/forced_movement` but got absolutely
clonged.

Actually, I did kill `/datum/forced_movement`. It was only used in one
spot so I just went ahead and cooked it into a special utility datum
that should only be used in one spot. We can optimize the code later or
something, but I like the way it is right now (pretty much status quo
without the potential of someone using a depreciated framework).

Alright, there were also like 3 `TODO`s (4 if you count the move loop
comment (which is ehhhh)). I naively tried to tackle them a very hard
way, but then I just realized I could use the fancy new datum I cooked
up and wow they're all solved now. The hook looks so fucking good now.

* The code is overall more streamlined, with all of the post-projectile
work being handled by a special datum (I wanted it to be handled by the
hook but the timings were all based on SSFastprocess so datum it is).
Forced movement is killed but we just salvaged whatever we needed from
it.
* More traits to ensure we don't cheese in a way we're not supposed to
* A very sexy chain will spawn when you drag your kill in (this wasn't
there before but I fixeded it :3)
* The firer will actually get grounded when they try and shoot the chain
out. They get grounded for 0.25 seconds unless they hit something. I
don't know how the timing is but I think it's fair.
* We also add a unique suicide act, because I noticed we did the
"magical" one previously, which big-league sucked.
* More traits to ensure less cheese! I like how nice it is now.
## Why It's Good For The Game

The meat hook really makes you _feel_ like Roadhog from Overwatch.
Resolves a bunch of old TODOs while getting rid of a completely obsolete
framework in a really neat way. I don't typically like mixing balances
and refactors but these TODOs were getting crusty man i just wanted to
get them out of the way
## Changelog
:cl:
balance: The Meat Hook will now "ground" you whenever you fire it out at
someone. You get a very small immobilization every time you fire, which
"upgrades" to a full immobilization whenever you actually hit an atom
and start to drag it in.
fix: A chain should now show up as you drag in something with the meat
hooks.
fix: Meat hooks should no longer play the "magical gun" suicide if you
were to use it, but instead do their own unique thing.
/:cl:
  • Loading branch information
san7890 authored Nov 30, 2023
1 parent 2dfda5a commit 39d9c45
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 125 deletions.
11 changes: 0 additions & 11 deletions code/datums/elements/climbable.dm
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
RegisterSignal(target, COMSIG_ATOM_ATTACK_HAND, PROC_REF(attack_hand))
RegisterSignal(target, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
RegisterSignal(target, COMSIG_MOUSEDROPPED_ONTO, PROC_REF(mousedrop_receive))
RegisterSignal(target, COMSIG_ATOM_BUMPED, PROC_REF(try_speedrun))
ADD_TRAIT(target, TRAIT_CLIMBABLE, ELEMENT_TRAIT(type))

/datum/element/climbable/Detach(datum/target)
Expand Down Expand Up @@ -104,8 +103,6 @@
var/x_dist = (text2num(LAZYACCESS(modifiers, ICON_X)) - world.icon_size/2) * (climbed_thing.dir & WEST ? -1 : 1)
var/y_dist = (text2num(LAZYACCESS(modifiers, ICON_Y)) - world.icon_size/2) * (climbed_thing.dir & SOUTH ? -1 : 1)
dir_step = (x_dist >= y_dist ? (EAST|WEST) : (NORTH|SOUTH)) & climbed_thing.dir
else //user is being moved by a forced_movement datum. dir_step will be the direction to the forced movement target.
dir_step = get_dir(user, user.force_moving.target)
else
dir_step = get_dir(user, get_step(climbed_thing, climbed_thing.dir))
. = step(user, dir_step)
Expand All @@ -121,11 +118,3 @@
var/mob/living/living_target = dropped_atom
if(living_target.mobility_flags & MOBILITY_MOVE)
INVOKE_ASYNC(src, PROC_REF(climb_structure), climbed_thing, living_target, params)

///Tries to climb onto the target if the forced movement of the mob allows it
/datum/element/climbable/proc/try_speedrun(datum/source, mob/bumpee)
SIGNAL_HANDLER
if(!istype(bumpee))
return
if(bumpee.force_moving?.allow_climbing)
do_climb(source, bumpee)
89 changes: 0 additions & 89 deletions code/datums/forced_movement.dm

This file was deleted.

1 change: 0 additions & 1 deletion code/game/atoms_movable.dm
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
var/atom/movable/moving_from_pull
///Holds information about any movement loops currently running/waiting to run on the movable. Lazy, will be null if nothing's going on
var/datum/movement_packet/move_packet
var/datum/forced_movement/force_moving = null //handled soley by forced_movement.dm
/**
* an associative lazylist of relevant nested contents by "channel", the list is of the form: list(channel = list(important nested contents of that type))
* each channel has a specific purpose and is meant to replace potentially expensive nested contents iteration.
Expand Down
188 changes: 165 additions & 23 deletions code/modules/projectiles/guns/special/meat_hook.dm
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//Meat Hook
#define TRAIT_HOOKED "hooked"
#define IMMOBILIZATION_TIMER (0.25 SECONDS) //! How long we immobilize the firer after firing - we do cancel the immobilization early if nothing is hit.

/// Meat Hook
/obj/item/gun/magic/hook
name = "meat hook"
desc = "Mid or feed."
Expand All @@ -22,8 +24,22 @@
/obj/item/gun/magic/hook/can_trigger_gun(mob/living/user, akimbo_usage) // This isn't really a gun, so it shouldn't be checking for TRAIT_NOGUNS, a firing pin (pinless), or a trigger guard (guardless)
if(akimbo_usage)
return FALSE //this would be kinda weird while shooting someone down.
if(HAS_TRAIT(user, TRAIT_IMMOBILIZED))
return FALSE
return TRUE

/obj/item/gun/magic/hook/suicide_act(mob/living/user)
var/obj/item/bodypart/head/removable = user.get_bodypart(BODY_ZONE_HEAD)
if(isnull(removable))
user.visible_message(span_suicide("[user] stuffs the chain of the [src] down the hole where their head should be! It looks like [user.p_theyre()] trying to commit suicide!"))
return OXYLOSS

playsound(get_turf(src), fire_sound, 50, TRUE, -1)
user.visible_message(span_suicide("[user] is using the [src] on their [user.p_their()] head! It looks like [user.p_theyre()] trying to commit suicide!"))
playsound(get_turf(src), 'sound/weapons/bladeslice.ogg', 70)
removable.dismember(silent = FALSE)
return BRUTELOSS

/obj/item/ammo_casing/magic/hook
name = "hook"
desc = "A hook."
Expand All @@ -41,36 +57,153 @@
armour_penetration = 60
damage_type = BRUTE
hitsound = 'sound/effects/splat.ogg'
var/chain
var/knockdown_time = (0.5 SECONDS)
/// The chain we send out while we are in motion, referred to as "initial" to not get confused with the chain we use to reel the victim in.
var/datum/beam/initial_chain

/obj/projectile/hook/fire(setAngle)
if(firer)
chain = firer.Beam(src, icon_state = "chain", emissive = FALSE)
..()
//TODO: root the firer until the chain returns
initial_chain = firer.Beam(src, icon_state = "chain", emissive = FALSE)
ADD_TRAIT(firer, TRAIT_IMMOBILIZED, REF(src))
addtimer(TRAIT_CALLBACK_REMOVE(firer, TRAIT_IMMOBILIZED, REF(src)), IMMOBILIZATION_TIMER) // safety if we miss, if we get a hit we stay immobilized
return ..()

/obj/projectile/hook/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(ismovable(target))
var/atom/movable/A = target
if(A.anchored)
return
A.visible_message(span_danger("[A] is snagged by [firer]'s hook!"))
//Should really be a movement loop, but I don't want to support moving 5 tiles a tick
//It just looks bad
new /datum/forced_movement(A, get_turf(firer), 5, TRUE)
if (isliving(target))
var/mob/living/fresh_meat = target
fresh_meat.Knockdown(knockdown_time)
return
//TODO: keep the chain beamed to A
//TODO: needs a callback to delete the chain

/obj/projectile/hook/Destroy()
qdel(chain)
if(!ismovable(target))
return

var/atom/movable/victim = target
if(victim.anchored || HAS_TRAIT_FROM(victim, TRAIT_HOOKED, REF(firer)))
return

victim.visible_message(span_danger("[victim] is snagged by [firer]'s hook!"))

var/datum/hook_and_move/puller = new
puller.begin_pulling(firer, victim, get_turf(firer))
REMOVE_TRAIT(firer, TRAIT_IMMOBILIZED, REF(src))

/obj/projectile/hook/Destroy(force)
QDEL_NULL(initial_chain)
return ..()

/// Lightweight datum that just handles moving a target for the hook.
/// For the love of God, do not use this outside this file.
/datum/hook_and_move
/// Weakref to the victim we are dragging
var/datum/weakref/victim_ref = null
/// Weakref of the destination that the victim is heading towards.
var/datum/weakref/destination_ref = null
/// Weakref to the firer of the hook
var/datum/weakref/firer_ref = null
/// String to the REF() of the dude that fired us so we can ensure we always cleanup our traits
var/firer_ref_string = null

/// The last time our movement fired.
var/last_movement = 0
/// The chain beam we currently own.
var/datum/beam/return_chain = null

/// How many steps we force the victim to take per tick
var/steps_per_tick = 5
/// How long we knockdown the victim for.
var/knockdown_time = (0.5 SECONDS)

/// List of traits that prevent the user from moving. More restrictive than attempting to fire the hook by design.
var/static/list/prevent_movement_traits = list(
TRAIT_IMMOBILIZED,
TRAIT_UI_BLOCKED,
)

/datum/hook_and_move/Destroy(force)
STOP_PROCESSING(SSfastprocess, src)
QDEL_NULL(return_chain)
return ..()

/// Uses fastprocessing to move our victim to the destination at a rather fast speed.
/datum/hook_and_move/proc/begin_pulling(atom/movable/firer, atom/movable/victim, atom/destination)
return_chain = firer.Beam(victim, icon_state = "chain", emissive = FALSE)

firer_ref_string = REF(firer)
ADD_TRAIT(victim, TRAIT_HOOKED, firer_ref_string)
firer.add_traits(prevent_movement_traits, REF(src))
if(isliving(victim))
var/mob/living/fresh_meat = victim
fresh_meat.Knockdown(knockdown_time)

destination_ref = WEAKREF(destination)
victim_ref = WEAKREF(victim)
firer_ref = WEAKREF(firer)

START_PROCESSING(SSfastprocess, src)

/// Cancels processing and removes the trait from the victim.
/datum/hook_and_move/proc/end_movement()
var/atom/movable/firer = firer_ref?.resolve()
if(!QDELETED(firer))
firer.remove_traits(prevent_movement_traits, REF(src))

var/atom/movable/victim = victim_ref?.resolve()
if(!QDELETED(victim))
REMOVE_TRAIT(victim, TRAIT_HOOKED, firer_ref_string)

qdel(src)

/datum/hook_and_move/process(seconds_per_tick)
var/atom/movable/victim = victim_ref?.resolve()
var/atom/destination = destination_ref?.resolve()
if(QDELETED(victim) || QDELETED(destination))
end_movement()
return

var/steps_to_take = round(steps_per_tick * (world.time - last_movement))
if(steps_to_take <= 0)
return

var/movement_result = attempt_movement(victim, destination)
if(!movement_result || (victim.loc == destination.loc)) // either we failed our movement or our mission is complete
end_movement()

/// Attempts to move the victim towards the destination. Returns TRUE if we do a successful movement, FALSE otherwise.
/// second_attempt is a boolean to prevent infinite recursion.
/// If this whole series of events wasn't reliant on SSfastprocess firing as fast as it does, it would have been more useful to make this a move loop datum. But, we need the speed.
/datum/hook_and_move/proc/attempt_movement(atom/movable/subject, atom/target, second_attempt = FALSE)
var/actually_moved = FALSE
if(!second_attempt)
actually_moved = step_towards(subject, target)

if(actually_moved)
return TRUE

// alright now the code fucking sucks
var/subject_x = subject.x
var/subject_y = subject.y
var/target_x = target.x
var/target_y = target.y

//If we're going x, step x
if((target_x > subject_x) && step(subject, EAST))
actually_moved = TRUE
else if((target_x < subject_x) && step(subject, WEST))
actually_moved = TRUE

if(actually_moved)
return TRUE

//If the x step failed, go y
if((target_y > subject_y) && step(subject, NORTH))
actually_moved = TRUE
else if((target_y < subject_y) && step(subject, SOUTH))
actually_moved = TRUE

if(actually_moved)
return TRUE

// if we fail twice, abort. otherwise queue up the second attempt.
if(second_attempt)
return FALSE

return attempt_movement(subject, target, second_attempt = TRUE)

//just a nerfed version of the real thing for the bounty hunters.
/obj/item/gun/magic/hook/bounty
name = "hook"
Expand All @@ -82,3 +215,12 @@
/obj/projectile/hook/bounty
damage = 0
stamina = 40

/// Debug hook for fun (AKA admin abuse). doesn't do any more damage or anything just lets you wildfire it.
/obj/item/gun/magic/hook/debug
name = "super meat hook"
max_charges = 100
recharge_rate = 1

#undef TRAIT_HOOKED
#undef IMMOBILIZATION_TIMER
1 change: 0 additions & 1 deletion tgstation.dme
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,6 @@
#include "code\datums\ductnet.dm"
#include "code\datums\emotes.dm"
#include "code\datums\ert.dm"
#include "code\datums\forced_movement.dm"
#include "code\datums\hailer_phrase.dm"
#include "code\datums\holocall.dm"
#include "code\datums\hotkeys_help.dm"
Expand Down

0 comments on commit 39d9c45

Please sign in to comment.