Files
Bubberstation/code/datums/components/scope.dm
Ghom 339616ae78 You can now interact with held mobs beside wearing them (feat: "minor" melee attack chain cleanup) (#90080)
## About The Pull Request
People can now pet held mothroaches and pugs if they want to, or use
items on them, hopefully without causing many issues. After all, it only
took about a couple dozen lines of code to make...

...Oh, did the 527 files changed or the 850~ lines added/removed perhaps
catch your eye? Made you wonder if I accidentally pushed the wrong
branch? or skewed something up big time? Well, nuh uh. I just happen to
be fed up with the melee attack chain still using stringized params
instead of an array/list. It was frankly revolting to see how I'd have
had to otherwise call `list2params` for what I'm trying to accomplish
here, and make this PR another tessera to the immense stupidity of our
attack chain procs calling `params2list` over and over and over instead
of just using that one call instance from `ClickOn` as an argument. It's
2025, honey, wake up!

I also tried to replace some of those single letter vars/args but there
are just way too many of them.

## Why It's Good For The Game
Improving old code. And I want to be able to pet mobroaches while
holding them too.

## Changelog

🆑
qol: You can now interact with held mobs in more ways beside wearing
them.
/🆑
2025-04-23 20:18:26 +00:00

274 lines
9.5 KiB
Plaintext

///Used to allow reaching the maximum offset range without exiting the boundaries of the game screen.
#define MOUSE_POINTER_OFFSET_MULT 1.1
///A component that allows players to use the item to zoom out. Mainly intended for firearms, but now works with other items too.
/datum/component/scope
/// How far we can extend, with modifier of 1, up to our vision edge, higher numbers multiply.
var/range_modifier = 1
/// Fullscreen object we use for tracking.
var/atom/movable/screen/fullscreen/cursor_catcher/scope/tracker
/// The owner of the tracker's ckey. For comparing with the current owner mob, in case the client has left it (e.g. ghosted).
var/tracker_owner_ckey
/// The method which we zoom in and out
var/zoom_method = ZOOM_METHOD_RIGHT_CLICK
/// if not null, an item action will be added. Redundant if the mode is ZOOM_METHOD_RIGHT_CLICK or ZOOM_METHOD_WIELD.
var/item_action_type
/datum/component/scope/Initialize(range_modifier = 1, zoom_method = ZOOM_METHOD_RIGHT_CLICK, item_action_type)
if(!isitem(parent))
return COMPONENT_INCOMPATIBLE
src.range_modifier = range_modifier
src.zoom_method = zoom_method
src.item_action_type = item_action_type
/datum/component/scope/Destroy(force)
if(tracker)
stop_zooming(tracker.owner)
return ..()
/datum/component/scope/RegisterWithParent()
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_move))
switch(zoom_method)
if(ZOOM_METHOD_RIGHT_CLICK)
RegisterSignal(parent, COMSIG_RANGED_ITEM_INTERACTING_WITH_ATOM_SECONDARY, PROC_REF(do_secondary_zoom))
if(ZOOM_METHOD_WIELD)
RegisterSignal(parent, SIGNAL_ADDTRAIT(TRAIT_WIELDED), PROC_REF(on_wielded))
RegisterSignal(parent, SIGNAL_REMOVETRAIT(TRAIT_WIELDED), PROC_REF(on_unwielded))
if(item_action_type)
var/obj/item/parent_item = parent
var/datum/action/item_action/scope = parent_item.add_item_action(item_action_type)
RegisterSignal(scope, COMSIG_ACTION_TRIGGER, PROC_REF(on_action_trigger))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
if(isgun(parent))
RegisterSignal(parent, COMSIG_GUN_TRY_FIRE, PROC_REF(on_gun_fire))
/datum/component/scope/UnregisterFromParent()
if(item_action_type)
var/obj/item/parent_item = parent
var/datum/action/item_action/scope = locate(item_action_type) in parent_item.actions
parent_item.remove_item_action(scope)
UnregisterSignal(parent, list(
COMSIG_MOVABLE_MOVED,
COMSIG_RANGED_ITEM_INTERACTING_WITH_ATOM_SECONDARY,
SIGNAL_ADDTRAIT(TRAIT_WIELDED),
SIGNAL_REMOVETRAIT(TRAIT_WIELDED),
COMSIG_GUN_TRY_FIRE,
COMSIG_ATOM_EXAMINE,
))
/datum/component/scope/process(seconds_per_tick)
var/mob/user_mob = tracker.owner
var/client/user_client = user_mob.client
if(!user_client)
stop_zooming(user_mob)
return
tracker.calculate_params()
if(!user_client.intended_direction)
user_mob.face_atom(tracker.given_turf)
animate(user_client, world.tick_lag, pixel_x = tracker.given_x, pixel_y = tracker.given_y)
/datum/component/scope/proc/on_move(atom/movable/source, atom/oldloc, dir, forced)
SIGNAL_HANDLER
if(!tracker)
return
stop_zooming(tracker.owner)
/datum/component/scope/proc/do_secondary_zoom(datum/source, mob/user, atom/target, list/modifiers)
SIGNAL_HANDLER
if(tracker)
stop_zooming(user)
else
zoom(user)
return ITEM_INTERACT_BLOCKING
/datum/component/scope/proc/on_action_trigger(datum/action/source)
SIGNAL_HANDLER
var/obj/item/item = source.target
var/mob/living/user = item.loc
if(tracker)
stop_zooming(user)
else
zoom(user)
/datum/component/scope/proc/on_wielded(obj/item/source, trait)
SIGNAL_HANDLER
var/mob/living/user = source.loc
zoom(user)
/datum/component/scope/proc/on_unwielded(obj/item/source, trait)
SIGNAL_HANDLER
var/mob/living/user = source.loc
stop_zooming(user)
/datum/component/scope/proc/on_gun_fire(obj/item/gun/source, mob/living/user, atom/target, flag, params)
SIGNAL_HANDLER
if(!tracker?.given_turf || target == get_target(tracker.given_turf))
return NONE
INVOKE_ASYNC(source, TYPE_PROC_REF(/obj/item/gun, fire_gun), get_target(tracker.given_turf), user)
return COMPONENT_CANCEL_GUN_FIRE
/datum/component/scope/proc/on_examine(datum/source, mob/user, list/examine_list)
SIGNAL_HANDLER
var/scope = isgun(parent) ? "scope in" : "zoom out"
switch(zoom_method)
if(ZOOM_METHOD_RIGHT_CLICK)
examine_list += span_notice("You can [scope] with <b>right-click</b>.")
if(ZOOM_METHOD_WIELD)
examine_list += span_notice("You can [scope] by wielding it with both hands.")
/**
* We find and return the best target to hit on a given turf.
*
* Arguments:
* * target_turf: The turf we are looking for targets on.
*/
/datum/component/scope/proc/get_target(turf/target_turf)
var/list/object_targets = list()
var/list/non_dense_targets = list()
for(var/atom/movable/possible_target in target_turf)
if(possible_target.layer <= PROJECTILE_HIT_THRESHHOLD_LAYER)
continue
if(possible_target.invisibility > tracker.owner.see_invisible)
continue
if(!possible_target.mouse_opacity)
continue
if(iseffect(possible_target))
continue
if(ismob(possible_target))
if(possible_target == tracker.owner)
continue
return possible_target
if(!possible_target.density)
non_dense_targets += possible_target
continue
object_targets += possible_target
for(var/obj/important_object as anything in object_targets)
return important_object
for(var/obj/unimportant_object as anything in non_dense_targets)
return unimportant_object
return target_turf
/**
* We start zooming by adding our tracker overlay and starting our processing.
*
* Arguments:
* * user: The mob we are starting zooming on.
*/
/datum/component/scope/proc/zoom(mob/user)
if(isnull(user.client))
return
if(HAS_TRAIT(user, TRAIT_USER_SCOPED))
user.balloon_alert(user, "already zoomed!")
return
user.playsound_local(parent, 'sound/items/weapons/scope.ogg', 75, TRUE)
tracker = user.overlay_fullscreen("scope", /atom/movable/screen/fullscreen/cursor_catcher/scope, isgun(parent))
tracker.assign_to_mob(user, range_modifier)
tracker_owner_ckey = user.ckey
if(user.is_holding(parent))
RegisterSignals(user, list(COMSIG_MOB_SWAP_HANDS, COMSIG_QDELETING), PROC_REF(stop_zooming))
RegisterSignal(user, COMSIG_ATOM_ENTERING, PROC_REF(on_enter_new_loc))
else // The item is likely worn (eg. mothic cap)
RegisterSignal(user, COMSIG_QDELETING, PROC_REF(stop_zooming))
RegisterSignal(user, COMSIG_ATOM_ENTERING, PROC_REF(on_enter_new_loc))
var/static/list/capacity_signals = list(
COMSIG_LIVING_STATUS_KNOCKDOWN,
COMSIG_LIVING_STATUS_PARALYZE,
COMSIG_LIVING_STATUS_STUN,
)
RegisterSignals(user, capacity_signals, PROC_REF(on_incapacitated))
START_PROCESSING(SSprojectiles, src)
ADD_TRAIT(user, TRAIT_USER_SCOPED, REF(src))
return TRUE
///Stop scoping if the `newloc` we move to is not a turf
/datum/component/scope/proc/on_enter_new_loc(datum/source, atom/newloc, atom/old_loc, list/atom/old_locs)
SIGNAL_HANDLER
if(!isturf(newloc))
stop_zooming(tracker.owner)
/datum/component/scope/proc/on_incapacitated(mob/living/source, amount = 0, ignore_canstun = FALSE)
SIGNAL_HANDLER
if(amount > 0)
stop_zooming(source)
/**
* We stop zooming, canceling processing, resetting stuff back to normal and deleting our tracker.
*
* Arguments:
* * user: The mob we are canceling zooming on.
*/
/datum/component/scope/proc/stop_zooming(mob/user)
SIGNAL_HANDLER
if(!HAS_TRAIT(user, TRAIT_USER_SCOPED))
return
STOP_PROCESSING(SSprojectiles, src)
UnregisterSignal(user, list(
COMSIG_LIVING_STATUS_KNOCKDOWN,
COMSIG_LIVING_STATUS_PARALYZE,
COMSIG_LIVING_STATUS_STUN,
COMSIG_MOB_SWAP_HANDS,
COMSIG_QDELETING,
COMSIG_ATOM_ENTERING,
))
REMOVE_TRAIT(user, TRAIT_USER_SCOPED, REF(src))
user.playsound_local(parent, 'sound/items/weapons/scope.ogg', 75, TRUE, frequency = -1)
user.clear_fullscreen("scope")
// if the client has ended up in another mob, find that mob so we can fix their cursor
var/mob/true_user
if(user.ckey != tracker_owner_ckey)
true_user = get_mob_by_ckey(tracker_owner_ckey)
if(!isnull(true_user))
user = true_user
if(user.client)
animate(user.client, 0.2 SECONDS, pixel_x = 0, pixel_y = 0)
tracker = null
tracker_owner_ckey = null
/atom/movable/screen/fullscreen/cursor_catcher/scope
icon_state = "scope"
/// Multiplier for given_X an given_y.
var/range_modifier = 1
/atom/movable/screen/fullscreen/cursor_catcher/scope/assign_to_mob(mob/new_owner, range_modifier)
src.range_modifier = range_modifier
return ..()
/atom/movable/screen/fullscreen/cursor_catcher/scope/Click(location, control, params)
if(usr == owner)
calculate_params()
return ..()
/atom/movable/screen/fullscreen/cursor_catcher/scope/calculate_params()
var/list/modifiers = params2list(mouse_params)
var/icon_x = text2num(LAZYACCESS(modifiers, VIS_X))
if(isnull(icon_x))
icon_x = text2num(LAZYACCESS(modifiers, ICON_X))
if(isnull(icon_x))
icon_x = view_list[1]*ICON_SIZE_X/2
var/icon_y = text2num(LAZYACCESS(modifiers, VIS_Y))
if(isnull(icon_y))
icon_y = text2num(LAZYACCESS(modifiers, ICON_Y))
if(isnull(icon_y))
icon_y = view_list[2]*ICON_SIZE_Y/2
var/x_cap = range_modifier * view_list[1]*ICON_SIZE_X / 2
var/y_cap = range_modifier * view_list[2]*ICON_SIZE_Y / 2
var/uncapped_x = round(range_modifier * (icon_x - view_list[1]*ICON_SIZE_X/2) * MOUSE_POINTER_OFFSET_MULT)
var/uncapped_y = round(range_modifier * (icon_y - view_list[2]*ICON_SIZE_Y/2) * MOUSE_POINTER_OFFSET_MULT)
given_x = clamp(uncapped_x, -x_cap, x_cap)
given_y = clamp(uncapped_y, -y_cap, y_cap)
given_turf = locate(owner.x+round(given_x/ICON_SIZE_X, 1),owner.y+round(given_y/ICON_SIZE_Y, 1),owner.z)
#undef MOUSE_POINTER_OFFSET_MULT