mirror of
https://github.com/Citadel-Station-13/Citadel-Station-13-RP.git
synced 2026-08-22 07:16:41 +01:00
326 lines
12 KiB
Plaintext
326 lines
12 KiB
Plaintext
//* This file is explicitly licensed under the MIT license. *//
|
|
//* Copyright (c) 2024 Citadel Station Developers *//
|
|
|
|
/**
|
|
* Shieldcall used as a listener for [/datum/component/passive_parry]
|
|
*/
|
|
/datum/shieldcall/bound/passive_parry
|
|
expected_type = /datum/component/passive_parry
|
|
|
|
/**
|
|
* generic parry provider on items
|
|
*
|
|
* this is effectively the old rng block system
|
|
*
|
|
* also known as: autoparry.
|
|
*/
|
|
/datum/component/passive_parry
|
|
registered_type = /datum/component/passive_parry
|
|
|
|
/// passive parry data
|
|
var/datum/passive_parry/parry_data
|
|
/// callback to invoke before the parry is initiated
|
|
///
|
|
/// * must return /datum/parry_frame or null.
|
|
/// * if it returns a parry frame, it'll override the frame provided by the passive parry datum
|
|
/// * if it returns null, it'll cancel the parry
|
|
/// * invoked, if existing, with (obj/item/parent, mob/defending, attack_type, datum/attack_source, datum/passive_parry/parry_data)
|
|
/// * this allows you to construct a custom parry frame
|
|
/// * this will be null'd, not qdel'd, when the component is qdel'd
|
|
var/datum/callback/parry_intercept
|
|
/// our registered shieldcall
|
|
var/datum/shieldcall/bound/passive_parry/hooked_shieldcall
|
|
/// the mob we're registered with right now
|
|
var/mob/hooked
|
|
|
|
/datum/component/passive_parry/Initialize(datum/passive_parry/data, datum/callback/intercept)
|
|
. = ..()
|
|
if(. == COMPONENT_INCOMPATIBLE)
|
|
return
|
|
if(!isitem(parent))
|
|
return COMPONENT_INCOMPATIBLE
|
|
data = fetch_data(data)
|
|
if(!data)
|
|
stack_trace("invalid data")
|
|
return COMPONENT_INCOMPATIBLE
|
|
src.parry_data = data
|
|
src.parry_intercept = intercept
|
|
|
|
/datum/component/passive_parry/Destroy()
|
|
parry_data = null // parry data holds no refs
|
|
parry_intercept = null // i'd hope this doesn't hold a ref to us.
|
|
return ..()
|
|
|
|
/datum/component/passive_parry/proc/fetch_data(datum/passive_parry/datalike)
|
|
if(IS_ANONYMOUS_TYPEPATH(datalike))
|
|
return new datalike
|
|
if(ispath(datalike))
|
|
return resolve_passive_parry_data(datalike)
|
|
if(istype(datalike))
|
|
return datalike
|
|
|
|
/**
|
|
* About to start a parry. Resolve parry_frame datum.
|
|
*/
|
|
/datum/component/passive_parry/proc/ignite(atom/defending, attack_type, datum/attack_source)
|
|
RETURN_TYPE(/datum/parry_frame)
|
|
if(parry_intercept)
|
|
return parry_intercept.invoke_no_sleep(parent, defending, attack_type, attack_source, parry_data)
|
|
else
|
|
var/obj/item/item = parent
|
|
return item.passive_parry_intercept(defending, attack_type, attack_source, parry_data)
|
|
|
|
/datum/component/passive_parry/RegisterWithParent()
|
|
. = ..()
|
|
RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equipped))
|
|
RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(on_dropped))
|
|
if(!hooked)
|
|
var/obj/item/item = parent
|
|
if(item.get_worn_mob())
|
|
on_equipped(item, item.get_worn_mob(), item.worn_slot)
|
|
|
|
/datum/component/passive_parry/UnregisterFromParent()
|
|
. = ..()
|
|
UnregisterSignal(parent, COMSIG_ITEM_EQUIPPED)
|
|
if(hooked)
|
|
var/obj/item/item = parent
|
|
on_unequipped(item, hooked)
|
|
|
|
/datum/component/passive_parry/proc/on_dropped(obj/item/source, inv_op_flags, atom/new_loc)
|
|
// delete on drop to save memory
|
|
qdel(src)
|
|
|
|
/datum/component/passive_parry/proc/on_equipped(obj/item/source, mob/user, slot)
|
|
if(!check_slot(slot))
|
|
return
|
|
if(hooked)
|
|
return
|
|
ASSERT(user)
|
|
hooked = user
|
|
hooked_shieldcall = new(src)
|
|
user.register_shieldcall(hooked_shieldcall)
|
|
RegisterSignal(user, COMSIG_ATOM_SHIELDCALL_ITERATION, PROC_REF(shieldcall_iterating))
|
|
|
|
/datum/component/passive_parry/proc/on_unequipped(obj/item/source, mob/user)
|
|
ASSERT(user == hooked)
|
|
hooked = null
|
|
user.unregister_shieldcall(hooked_shieldcall)
|
|
QDEL_NULL(hooked_shieldcall)
|
|
UnregisterSignal(user, COMSIG_ATOM_SHIELDCALL_ITERATION, PROC_REF(shieldcall_iterating))
|
|
|
|
/datum/component/passive_parry/proc/check_slot(slot_id)
|
|
return islist(parry_data.parry_slot_id)? (slot_id in parry_data.parry_slot_id) : (!parry_data.parry_slot_id || (parry_data.parry_slot_id == slot_id))
|
|
|
|
/datum/component/passive_parry/proc/shieldcall_iterating(mob/source, shieldcall_type)
|
|
SIGNAL_HANDLER
|
|
ASSERT(source == hooked)
|
|
var/datum/passive_parry/data = fetch_data(parry_data)
|
|
// normal shieldcall handlers handle it
|
|
if(!data.parry_frame_simulated)
|
|
return
|
|
var/datum/parry_frame/resolved = ignite(source)
|
|
if(!resolved)
|
|
return
|
|
// for now, we only care about if they already have a frame
|
|
// in the future, maybe this can fire as long as we aren't the source of a parry frame on them.
|
|
// todo: cooldown enforcement
|
|
// todo: mobility enforcement
|
|
// todo: full parry swing cycle?
|
|
if(!source.GetComponent(/datum/component/parry_frame))
|
|
source.AddComponent(/datum/component/parry_frame, resolved, data.parry_frame_timing)
|
|
|
|
//* Bindings - Bullet *//
|
|
|
|
/datum/shieldcall/bound/passive_parry/handle_bullet(atom/defending, shieldcall_returns, fake_attack, list/bullet_act_args)
|
|
// todo: no support for fake attacks yet
|
|
if(fake_attack)
|
|
return
|
|
// this is a definite 'do as i say, not as i do' moment
|
|
// this works because the proc names and args and types are **exactly** matching
|
|
// this is why the procs are all together
|
|
// do NOT try this at home.
|
|
return bound:handle_bullet(arglist(args))
|
|
|
|
/datum/component/passive_parry/proc/handle_bullet(atom/defending, shieldcall_returns, fake_attack, list/bullet_act_args)
|
|
var/datum/passive_parry/data = parry_data
|
|
if(data.parry_frame_simulated)
|
|
return
|
|
if(!prob(isnull(data.parry_chance_projectile) ? data.parry_chance_default : data.parry_chance_projectile))
|
|
return
|
|
if(!check_defensive_arc_tile(defending, bullet_act_args[BULLET_ACT_ARG_PROJECTILE], data.parry_arc, !data.parry_arc_round_down))
|
|
return
|
|
// - Projectile-specific -
|
|
var/obj/projectile/proj = bullet_act_args[BULLET_ACT_ARG_PROJECTILE]
|
|
if(!(proj.projectile_type & data.parry_projectile_types))
|
|
return
|
|
// - End -
|
|
var/datum/parry_frame/resolved = ignite(defending, ATTACK_TYPE_PROJECTILE, bullet_act_args[BULLET_ACT_ARG_PROJECTILE])
|
|
if(!resolved)
|
|
return
|
|
return resolved.handle_bullet(defending, shieldcall_returns | SHIELDCALL_FLAG_SINGLE_PARRY, fake_attack, data.parry_frame_efficiency, bullet_act_args, parent)
|
|
|
|
//* Bindings - Melee *//
|
|
|
|
/datum/shieldcall/bound/passive_parry/handle_melee(atom/defending, shieldcall_returns, fake_attack, datum/event_args/actor/clickchain/clickchain, clickchain_flags, obj/item/weapon, datum/melee_attack/weapon/style)
|
|
// todo: no support for fake attacks yet
|
|
if(fake_attack)
|
|
return
|
|
// this is a definite 'do as i say, not as i do' moment
|
|
// this works because the proc names and args and types are **exactly** matching
|
|
// this is why the procs are all together
|
|
// do NOT try this at home.
|
|
return bound:handle_melee(arglist(args))
|
|
|
|
/datum/component/passive_parry/proc/handle_melee(atom/defending, shieldcall_returns, fake_attack, datum/event_args/actor/clickchain/clickchain, clickchain_flags, obj/item/weapon, datum/melee_attack/weapon/style)
|
|
var/datum/passive_parry/data = parry_data
|
|
if(data.parry_frame_simulated)
|
|
return
|
|
if(!prob(isnull(data.parry_chance_melee) ? data.parry_chance_default : data.parry_chance_melee))
|
|
return
|
|
if(!check_defensive_arc_tile(defending, clickchain.performer, data.parry_arc, !data.parry_arc_round_down))
|
|
return
|
|
var/datum/parry_frame/resolved = ignite(defending, ATTACK_TYPE_MELEE, clickchain)
|
|
if(!resolved)
|
|
return
|
|
return resolved.handle_melee(defending, shieldcall_returns | SHIELDCALL_FLAG_SINGLE_PARRY, fake_attack, data.parry_frame_efficiency, clickchain, style, weapon, parent)
|
|
|
|
/datum/shieldcall/bound/passive_parry/handle_touch(atom/defending, shieldcall_returns, fake_attack, datum/event_args/actor/clickchain/clickchain, clickchain_flags, contact_flags, contact_specific)
|
|
// todo: no support for fake attacks yet
|
|
if(fake_attack)
|
|
return
|
|
// this is a definite 'do as i say, not as i do' moment
|
|
// this works because the proc names and args and types are **exactly** matching
|
|
// this is why the procs are all together
|
|
// do NOT try this at home.
|
|
return bound:handle_touch(arglist(args))
|
|
|
|
/datum/component/passive_parry/proc/handle_touch(atom/defending, shieldcall_returns, fake_attack, datum/event_args/actor/clickchain/clickchain, clickchain_flags, contact_flags, contact_specific)
|
|
var/datum/passive_parry/data = parry_data
|
|
if(data.parry_frame_simulated)
|
|
return
|
|
if(!prob(isnull(data.parry_chance_touch) ? data.parry_chance_default : data.parry_chance_touch))
|
|
return
|
|
if(!check_defensive_arc_tile(defending, clickchain.performer, data.parry_arc, !data.parry_arc_round_down))
|
|
return
|
|
var/datum/parry_frame/resolved = ignite(defending, ATTACK_TYPE_TOUCH, null)
|
|
if(!resolved)
|
|
return
|
|
return resolved.handle_touch(defending, shieldcall_returns | SHIELDCALL_FLAG_SINGLE_PARRY, fake_attack, data.parry_frame_efficiency, clickchain, contact_flags, contact_specific, parent)
|
|
|
|
//* Bindings - Thrown *//
|
|
|
|
/datum/shieldcall/bound/passive_parry/handle_throw_impact(atom/defending, shieldcall_returns, fake_attack, datum/thrownthing/thrown)
|
|
// todo: no support for fake attacks yet
|
|
if(fake_attack)
|
|
return
|
|
// this is a definite 'do as i say, not as i do' moment
|
|
// this works because the proc names and args and types are **exactly** matching
|
|
// this is why the procs are all together
|
|
// do NOT try this at home.
|
|
return bound:handle_throw_impact(arglist(args))
|
|
|
|
/datum/component/passive_parry/proc/handle_throw_impact(atom/defending, shieldcall_returns, fake_attack, datum/thrownthing/thrown)
|
|
var/datum/passive_parry/data = parry_data
|
|
if(data.parry_frame_simulated)
|
|
return
|
|
if(!prob(isnull(data.parry_chance_thrown) ? data.parry_chance_default : data.parry_chance_thrown))
|
|
return
|
|
if(!check_defensive_arc_tile(defending, thrown, data.parry_arc, !data.parry_arc_round_down))
|
|
return
|
|
var/datum/parry_frame/resolved = ignite(defending, ATTACK_TYPE_THROWN, thrown)
|
|
if(!resolved)
|
|
return
|
|
return resolved.handle_throw_impact(defending, shieldcall_returns | SHIELDCALL_FLAG_SINGLE_PARRY, fake_attack, data.parry_frame_efficiency, thrown, parent)
|
|
|
|
//* Item *//
|
|
|
|
/**
|
|
* Called by /datum/component/passive_parry when we're about to start up the parry frame
|
|
* Called if parry intercept callback isn't set.
|
|
*
|
|
* @params
|
|
* * defending - mob being defended
|
|
* * attack_type - (optional) attack type
|
|
* * weapon - (optional) the weapon
|
|
* * parry_data - (optional) the existing parry data
|
|
*
|
|
* @return parry frame datum to use, or null to cancel
|
|
*/
|
|
/obj/item/proc/passive_parry_intercept(mob/defending, attack_type, datum/attack_source, datum/passive_parry/parry_data)
|
|
return parry_data.parry_frame
|
|
|
|
//* Data *//
|
|
|
|
GLOBAL_LIST_EMPTY(passive_parry_data)
|
|
/**
|
|
* get a cached version of a passive paary datum
|
|
*/
|
|
/proc/resolve_passive_parry_data(datum/passive_parry/datalike)
|
|
if(isnull(datalike))
|
|
return
|
|
if(IS_ANONYMOUS_TYPEPATH(datalike))
|
|
return new datalike
|
|
if(istype(datalike))
|
|
return datalike
|
|
if(!GLOB.passive_parry_data[datalike])
|
|
GLOB.passive_parry_data[datalike] = new datalike
|
|
return GLOB.passive_parry_data[datalike]
|
|
|
|
/**
|
|
* datum for holding data on passive parrying
|
|
*/
|
|
/datum/passive_parry
|
|
/// parry chance for harmful melee: [0, 100]
|
|
var/parry_chance_melee
|
|
/// parry chance for (seemingly) benign melee: [0, 100]
|
|
var/parry_chance_touch
|
|
/// parry chance for inbound projectile: [0, 100]
|
|
var/parry_chance_projectile
|
|
/// parry chance for inbound throw
|
|
var/parry_chance_thrown
|
|
/// default parry chance if one of the above is null
|
|
var/parry_chance_default = 0
|
|
|
|
/// passive parry arc
|
|
var/parry_arc = 180
|
|
/// passive parry arc should round down for non-projectiles
|
|
///
|
|
/// * at 136 (1 more than 135), having this TRUE means non-projectiles can hit them from behind as behind is 180.
|
|
/// * at 136 (1 more than 135), having this be FALSE means non-projectiles **cannot** hit them from behind as behind is 180.
|
|
var/parry_arc_round_down = TRUE
|
|
|
|
/// valid slot ids; null for all, list for multiple, singular for single
|
|
var/parry_slot_id = SLOT_ID_HANDS
|
|
|
|
/// projectile types we autoparry on
|
|
var/parry_projectile_types = ALL
|
|
|
|
/// parry frame data to use by default
|
|
///
|
|
/// * can be a typepath
|
|
/// * can be an anonymous typepath
|
|
/// * can be a datum
|
|
var/parry_frame = /datum/parry_frame/passive_block
|
|
/// simulate a full parry frame with carry-through and duration, or just run the frame once
|
|
var/parry_frame_simulated = FALSE
|
|
/// if not simulated, what's our efficiency? [0, 1]
|
|
///
|
|
/// * only used if not simulated
|
|
var/parry_frame_efficiency = 1
|
|
/// if simulated, how far in do we start? [0, infinity]
|
|
///
|
|
/// * only used if simulated
|
|
var/parry_frame_timing = 0
|
|
|
|
/datum/passive_parry/New()
|
|
if(IS_ANONYMOUS_TYPEPATH(parry_frame))
|
|
parry_frame = new parry_frame
|
|
else if(ispath(parry_frame))
|
|
parry_frame = new parry_frame
|
|
else if(istype(parry_frame, /datum/parry_frame))
|
|
else
|
|
CRASH("invalid parry frame")
|
|
|
|
/datum/parry_frame/passive_block
|
|
parry_can_prevent_contact = TRUE
|