Files
Bubberstation/code/game/objects/items/hand_items.dm
SmArtKar 9cdea961cc Refactors projectile parrying and makes it actually possible. (#84973)
## About The Pull Request

Due to how projectile code currently operates its (almost) impossible to
parry projectiles and completely impossible to projectile boost them, as
they move away from you instantly after being shot. Parrying others
projectiles requires them to be slow enough and to land on a tile right
in front of you on their last move of their tick, as else they instantly
hit you.
I moved parrying component from being on user to being on projectiles
themselves and added TRAIT_MINING_PARRYING which allows users to
actually perform the parries. This allows me to get rid of parry code
existing on all projectiles and add a "grace period" mechanic - now you
can parry projectiles by clicking on any turf they moved through in the
last 0.25 seconds. Parrying actually runs when projectiles hit you if
you clicked on the correct turf during same grace period timer. This
also acts as lag compensation, allowing players with high ping to
utilize this mechanic. Projectile boosting occurs when you hit a tile
the projectile moved through in first 0.25 seconds of its existence.
Parry also occurs on click instead of attack_hand due to this, so you
can parry projectiles while holding your
accelerator/crusher/resonator(what even?) - which is also important to
make projectile boosting possible as firing sets your click cooldown -
making attacking the turf impossible (another roadblock on ultrakill
players' path)

Projectile boosting now grants x1.5 damage multiplier and +40% speed
instead of x1.15 and +20% like parrying, since its significantly harder
to perform.

Crusher projectiles now can be parried/boosted. Crushers are ***the***
badass miner weapon, but they weren't possible to boost - probably
because it'd be pointless due to their lack of damage. Now boosting a
crusher blast gives it 10 damage, +3 range and makes the mark it applies
always count as backstab for additional style (although doing so is very
tricky due to speed/range)

Crusher marks are now a STATUS_EFFECT_REFRESH effect and no longer
per-user. This means that anyone can blow up anyone else's mark and
applying new ones refreshes the timer. Trophy effects of the person who
detonated the mark are the ones actually applied.

also cleaned up pixel_move a bit

## Why It's Good For The Game

Parrying refactor - parries are a very cool mechanic which is almost
impossible to execute in-game (and boosting is physically impossible)
and even if "fixed" would require perfect server performance and single
digit ping. Making them slightly easier to execute and adding a grace
period will allow more players to utilize it and also make it possible
to do parrying/boosting on highpop.

Boosting damage/speed buff - Boosting is harder to perform, and while
with parrying damage/speed is a nice bonus to the fact that your face
doesn't explode outwards from a death bolt, for boosting its the only
reason why you'd do it (beyond style points)

Crusher boosting - Crushers are more often used by skilled miners as
they are a high risk/high reward weapon. Them not being boostable is
rather disappointing as boosting is supposed to be a thing for extremely
skilled miners to do, essentially turning crushers into deadweight for
those who wanted to use style meters.

Unified crusher marks - Mobs being able to have multiple crusher marks
made fighting very confusing for rare coop crusher miners, as you
weren't able to tell if you could detonate the mark or not. This should
make coop fighting more viable as marks can now be comboed if you have
good cooperation with your teammate.

## Changelog
🆑
refactor: Refactored parrying to be projectile-side and not user-side.
add: You can now parry crusher projectiles to give them some more range,
add damage and make the mark always count as a backstab.
add: Projectile parrying has been significantly improved, making
parrying and boosting projectiles actually possible. Includes a small
grace period as lag compensation for players with higher pings.
balance: Crusher marks can now be detonated by any crusher, not only the
one that applied it, as to incentivise coop mining for crusher users.
balance: Projectile boosting now gives a significantly bigger damage and
speed increase
/🆑
2024-07-20 00:48:07 +02:00

677 lines
28 KiB
Plaintext

/// For all of the items that are really just the user's hand used in different ways, mostly (all, really) from emotes
/obj/item/hand_item
icon = 'icons/obj/weapons/hand.dmi'
icon_state = "latexballoon"
force = 0
throwforce = 0
item_flags = DROPDEL | ABSTRACT | HAND_ITEM
/obj/item/hand_item/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_NO_STORAGE_INSERT, TRAIT_GENERIC)
/obj/item/hand_item/circlegame
name = "circled hand"
desc = "If somebody looks at this while it's below your waist, you get to bop them."
icon_state = "madeyoulook"
attack_verb_continuous = list("bops")
attack_verb_simple = list("bop")
/obj/item/hand_item/circlegame/Initialize(mapload)
. = ..()
var/mob/living/owner = loc
if(!istype(owner))
return
RegisterSignal(owner, COMSIG_ATOM_EXAMINE, PROC_REF(ownerExamined))
/obj/item/hand_item/circlegame/Destroy()
var/mob/owner = loc
if(istype(owner))
UnregisterSignal(owner, COMSIG_ATOM_EXAMINE)
return ..()
/obj/item/hand_item/circlegame/dropped(mob/user)
UnregisterSignal(user, COMSIG_ATOM_EXAMINE) //loc will have changed by the time this is called, so Destroy() can't catch it
// this is a dropdel item.
return ..()
/// Stage 1: The mistake is made
/obj/item/hand_item/circlegame/proc/ownerExamined(mob/living/owner, mob/living/sucker)
SIGNAL_HANDLER
if(!istype(sucker) || !in_range(owner, sucker))
return
addtimer(CALLBACK(src, PROC_REF(waitASecond), owner, sucker), 0.4 SECONDS)
/// Stage 2: Fear sets in
/obj/item/hand_item/circlegame/proc/waitASecond(mob/living/owner, mob/living/sucker)
if(QDELETED(sucker) || QDELETED(src) || QDELETED(owner))
return
if(owner == sucker) // big mood
to_chat(owner, span_danger("Wait a second... you just looked at your own [src.name]!"))
addtimer(CALLBACK(src, PROC_REF(selfGottem), owner), 1 SECONDS)
else
to_chat(sucker, span_danger("Wait a second... was that a-"))
addtimer(CALLBACK(src, PROC_REF(GOTTEM), owner, sucker), 0.6 SECONDS)
/// Stage 3A: We face our own failures
/obj/item/hand_item/circlegame/proc/selfGottem(mob/living/owner)
if(QDELETED(src) || QDELETED(owner))
return
playsound(get_turf(owner), 'sound/effects/hit_punch.ogg', 50, TRUE, -1)
owner.visible_message(span_danger("[owner] shamefully bops [owner.p_them()]self with [owner.p_their()] [src.name]."), span_userdanger("You shamefully bop yourself with your [src.name]."), \
span_hear("You hear a dull thud!"))
log_combat(owner, owner, "bopped", src.name, "(self)")
owner.do_attack_animation(owner)
owner.apply_damage(100, STAMINA)
owner.Knockdown(10)
qdel(src)
/// Stage 3B: We face our reckoning (unless we moved away or they're incapacitated)
/obj/item/hand_item/circlegame/proc/GOTTEM(mob/living/owner, mob/living/sucker)
if(QDELETED(sucker))
return
if(QDELETED(src) || QDELETED(owner))
to_chat(sucker, span_warning("Nevermind... must've been your imagination..."))
return
if(!in_range(owner, sucker) || !(owner.mobility_flags & MOBILITY_USE))
to_chat(sucker, span_notice("Phew... you moved away before [owner] noticed you saw [owner.p_their()] [src.name]..."))
return
to_chat(owner, span_warning("[sucker] looks down at your [src.name] before trying to avert [sucker.p_their()] eyes, but it's too late!"))
to_chat(sucker, span_danger("<b>[owner] sees the fear in your eyes as you try to look away from [owner.p_their()] [src.name]!</b>"))
owner.face_atom(sucker)
if(owner.client)
owner.client.give_award(/datum/award/achievement/misc/gottem, owner) // then everybody clapped
playsound(get_turf(owner), 'sound/effects/hit_punch.ogg', 50, TRUE, -1)
owner.do_attack_animation(sucker)
if(HAS_TRAIT(owner, TRAIT_HULK))
owner.visible_message(span_danger("[owner] bops [sucker] with [owner.p_their()] [src.name] much harder than intended, sending [sucker.p_them()] flying!"), \
span_danger("You bop [sucker] with your [src.name] much harder than intended, sending [sucker.p_them()] flying!"), span_hear("You hear a sickening sound of flesh hitting flesh!"), ignored_mobs=list(sucker))
to_chat(sucker, span_userdanger("[owner] bops you incredibly hard with [owner.p_their()] [src.name], sending you flying!"))
sucker.apply_damage(50, STAMINA)
sucker.Knockdown(50)
log_combat(owner, sucker, "bopped", src.name, "(setup- Hulk)")
var/atom/throw_target = get_edge_target_turf(sucker, owner.dir)
sucker.throw_at(throw_target, 6, 3, owner)
else
owner.visible_message(span_danger("[owner] bops [sucker] with [owner.p_their()] [src.name]!"), span_danger("You bop [sucker] with your [src.name]!"), \
span_hear("You hear a dull thud!"), ignored_mobs=list(sucker))
sucker.apply_damage(15, STAMINA)
log_combat(owner, sucker, "bopped", src.name, "(setup)")
to_chat(sucker, span_userdanger("[owner] bops you with [owner.p_their()] [src.name]!"))
qdel(src)
/obj/item/hand_item/noogie
name = "noogie"
desc = "Get someone in an aggressive grab then use this on them to ruin their day."
inhand_icon_state = "nothing"
/obj/item/hand_item/noogie/attack(mob/living/carbon/target, mob/living/carbon/human/user)
if(!istype(target))
to_chat(user, span_warning("You don't think you can give this a noogie!"))
return
if(HAS_TRAIT(user, TRAIT_PACIFISM))
to_chat(user, span_warning("You can't bring yourself to noogie [target]! You don't want to risk harming anyone..."))
return
if(!(target?.get_bodypart(BODY_ZONE_HEAD)) || user.pulling != target || user.grab_state < GRAB_AGGRESSIVE || user.getStaminaLoss() > 80)
return FALSE
var/obj/item/bodypart/head/the_head = target.get_bodypart(BODY_ZONE_HEAD)
if(!(the_head.biological_state & BIO_FLESH))
to_chat(user, span_warning("You can't noogie [target], [target.p_they()] [target.p_have()] no skin on [target.p_their()] head!"))
return
// [user] gives [target] a [prefix_desc] noogie[affix_desc]!
var/brutal_noogie = FALSE // was it an extra hard noogie?
var/prefix_desc = "rough"
var/affix_desc = ""
var/affix_desc_target = ""
if(HAS_TRAIT(target, TRAIT_ANTENNAE))
prefix_desc = "violent"
affix_desc = "on [target.p_their()] sensitive antennae"
affix_desc_target = "on your highly sensitive antennae"
brutal_noogie = TRUE
if(HAS_TRAIT(user, TRAIT_HULK))
prefix_desc = "sickeningly brutal"
brutal_noogie = TRUE
var/message_others = "[prefix_desc] noogie[affix_desc]"
var/message_target = "[prefix_desc] noogie[affix_desc_target]"
user.visible_message(span_danger("[user] begins giving [target] a [message_others]!"), span_warning("You start giving [target] a [message_others]!"), vision_distance=COMBAT_MESSAGE_RANGE, ignored_mobs=target)
to_chat(target, span_userdanger("[user] starts giving you a [message_target]!"))
if(!do_after(user, 1.5 SECONDS, target))
to_chat(user, span_warning("You fail to give [target] a noogie!"))
to_chat(target, span_danger("[user] fails to give you a noogie!"))
return
if(brutal_noogie)
target.add_mood_event("noogie_harsh", /datum/mood_event/noogie_harsh)
else
target.add_mood_event("noogie", /datum/mood_event/noogie)
noogie_loop(user, target, 0)
/// The actual meat and bones of the noogie'ing
/obj/item/hand_item/noogie/proc/noogie_loop(mob/living/carbon/human/user, mob/living/carbon/target, iteration)
if(!(target?.get_bodypart(BODY_ZONE_HEAD)) || user.pulling != target)
return FALSE
if(user.getStaminaLoss() > 80)
to_chat(user, span_warning("You're too tired to continue giving [target] a noogie!"))
to_chat(target, span_danger("[user] is too tired to continue giving you a noogie!"))
return
var/damage = rand(1, 5)
if(HAS_TRAIT(target, TRAIT_ANTENNAE))
damage += rand(3,7)
if(HAS_TRAIT(user, TRAIT_HULK))
damage += rand(3,7)
if(damage >= 5)
target.emote("scream")
log_combat(user, target, "given a noogie to", addition = "([damage] brute before armor)")
target.apply_damage(damage, BRUTE, BODY_ZONE_HEAD)
user.adjustStaminaLoss(iteration + 5)
playsound(get_turf(user), pick('sound/effects/rustle1.ogg','sound/effects/rustle2.ogg','sound/effects/rustle3.ogg','sound/effects/rustle4.ogg','sound/effects/rustle5.ogg'), 50)
if(prob(33))
user.visible_message(span_danger("[user] continues noogie'ing [target]!"), span_warning("You continue giving [target] a noogie!"), vision_distance=COMBAT_MESSAGE_RANGE, ignored_mobs=target)
to_chat(target, span_userdanger("[user] continues giving you a noogie!"))
if(!do_after(user, 1 SECONDS + (iteration * 2), target))
to_chat(user, span_warning("You fail to give [target] a noogie!"))
to_chat(target, span_danger("[user] fails to give you a noogie!"))
return
iteration++
noogie_loop(user, target, iteration)
/obj/item/hand_item/slapper
name = "slapper"
desc = "This is how real men fight."
inhand_icon_state = "nothing"
attack_verb_continuous = list("slaps")
attack_verb_simple = list("slap")
hitsound = 'sound/effects/snap.ogg'
/// How many smaller table smacks we can do before we're out
var/table_smacks_left = 3
/obj/item/hand_item/slapper/Initialize(mapload)
. = ..()
AddElement(/datum/element/high_fiver)
/obj/item/hand_item/slapper/attack(mob/living/slapped, mob/living/carbon/human/user)
SEND_SIGNAL(user, COMSIG_LIVING_SLAP_MOB, slapped)
if(iscarbon(slapped))
var/mob/living/carbon/potential_tailed = slapped
potential_tailed.unwag_tail()
user.do_attack_animation(slapped)
var/slap_volume = 50
var/datum/status_effect/offering/kiss_check = slapped.has_status_effect(/datum/status_effect/offering)
if(kiss_check && istype(kiss_check.offered_item, /obj/item/hand_item/kisser) && (user in kiss_check.possible_takers))
user.visible_message(
span_danger("[user] scoffs at [slapped]'s advance, winds up, and smacks [slapped.p_them()] hard to the ground!"),
span_notice("The nerve! You wind back your hand and smack [slapped] hard enough to knock [slapped.p_them()] over!"),
span_hear("You hear someone get the everloving shit smacked out of them!"),
ignored_mobs = slapped,
)
to_chat(slapped, span_userdanger("You see [user] scoff and pull back [user.p_their()] arm, then suddenly you're on the ground with an ungodly ringing in your ears!"))
slap_volume = 120
SEND_SOUND(slapped, sound('sound/weapons/flash_ring.ogg'))
shake_camera(slapped, 2, 2)
slapped.Paralyze(2.5 SECONDS)
slapped.adjust_confusion(7 SECONDS)
slapped.adjustStaminaLoss(40)
else if(user.zone_selected == BODY_ZONE_HEAD || user.zone_selected == BODY_ZONE_PRECISE_MOUTH)
if(user == slapped)
user.visible_message(
span_notice("[user] facepalms!"),
span_notice("You facepalm."),
span_hear("You hear a slap."),
)
else
if(slapped.IsSleeping() || slapped.IsUnconscious())
user.visible_message(
span_notice("[user] slaps [slapped] in the face, trying to wake [slapped.p_them()] up!"),
span_notice("You slap [slapped] in the face, trying to wake [slapped.p_them()] up!"),
span_hear("You hear a slap."),
)
// Worse than just help intenting people.
slapped.AdjustSleeping(-75)
slapped.AdjustUnconscious(-50)
else
user.visible_message(
span_danger("[user] slaps [slapped] in the face!"),
span_notice("You slap [slapped] in the face!"),
span_hear("You hear a slap."),
)
else if(user.zone_selected == BODY_ZONE_L_ARM || user.zone_selected == BODY_ZONE_R_ARM)
user.visible_message(
span_danger("[user] gives [slapped] a slap on the wrist!"),
span_notice("You give [slapped] a slap on the wrist!"),
span_hear("You hear a slap."),
)
else
user.visible_message(
span_danger("[user] slaps [slapped]!"),
span_notice("You slap [slapped]!"),
span_hear("You hear a slap."),
)
playsound(slapped, 'sound/weapons/slap.ogg', slap_volume, TRUE, -1)
return
/obj/item/hand_item/slapper/pre_attack_secondary(atom/target, mob/living/user, params)
if(!loc.Adjacent(target) || !istype(target, /obj/structure/table))
return ..()
slam_table(target, user)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/item/hand_item/slapper/pre_attack(atom/target, mob/living/user, params)
if(!loc.Adjacent(target) || !istype(target, /obj/structure/table))
return ..()
slap_table(target, user)
return TRUE
/// Slap the table, get some attention
/obj/item/hand_item/slapper/proc/slap_table(obj/structure/table/table, mob/living/user)
user.do_attack_animation(table)
playsound(get_turf(table), 'sound/effects/tableslam.ogg', 40, TRUE)
user.visible_message(span_notice("[user] slaps [user.p_their()] hand on [table]."), span_notice("You slap your hand on [table]."), vision_distance=COMBAT_MESSAGE_RANGE)
table_smacks_left--
if(table_smacks_left <= 0)
qdel(src)
/// Slam the table, demand some attention
/obj/item/hand_item/slapper/proc/slam_table(obj/structure/table/table, mob/living/user)
if(table_smacks_left < initial(table_smacks_left))
return slap_table(table, user)
user.do_attack_animation(table)
transform = transform.Scale(5) // BIG slap
if(HAS_TRAIT(user, TRAIT_HULK))
transform = transform.Scale(2)
color = COLOR_GREEN
SEND_SIGNAL(user, COMSIG_LIVING_SLAM_TABLE, table)
SEND_SIGNAL(table, COMSIG_TABLE_SLAMMED, user)
playsound(get_turf(table), 'sound/effects/tableslam.ogg', 110, TRUE)
user.visible_message("<b>[span_danger("[user] slams [user.p_their()] fist down on [table]!")]</b>", "<b>[span_danger("You slam your fist down on [table]!")]</b>")
qdel(src)
// Successful takes will qdel our hand after
/obj/item/hand_item/slapper/on_offer_taken(mob/living/carbon/offerer, mob/living/carbon/taker)
. = ..()
if(!.)
return
qdel(src)
/obj/item/hand_item/hand
name = "hand"
desc = "Sometimes, you just want to act gentlemanly."
inhand_icon_state = "nothing"
/obj/item/hand_item/hand/pre_attack(mob/living/carbon/help_target, mob/living/carbon/helper, params)
if(!loc.Adjacent(help_target) || !istype(helper) || !istype(help_target))
return ..()
if(helper.resting)
to_chat(helper, span_warning("You can't act gentlemanly when you're lying down!"))
return TRUE
/obj/item/hand_item/hand/pre_attack_secondary(mob/living/carbon/help_target, mob/living/carbon/helper, params)
if(!loc.Adjacent(help_target) || !istype(helper) || !istype(help_target))
return ..()
if(helper.resting)
to_chat(helper, span_warning("You can't act gentlemanly when you're lying down!"))
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
return SECONDARY_ATTACK_CALL_NORMAL
/obj/item/hand_item/hand/attack(mob/living/carbon/target_mob, mob/living/carbon/user, params)
if(!loc.Adjacent(target_mob) || !istype(user) || !istype(target_mob))
return TRUE
user.give(target_mob)
return TRUE
/obj/item/hand_item/hand/on_offered(mob/living/carbon/offerer, mob/living/carbon/offered)
. = TRUE
if(!istype(offerer))
return
if(offerer.body_position == LYING_DOWN)
to_chat(offerer, span_warning("You can't act gentlemanly when you're lying down!"))
return
if(!offered)
offered = locate(/mob/living/carbon) in orange(1, offerer)
if(offered && istype(offered) && offered.body_position == LYING_DOWN)
offerer.visible_message(span_notice("[offerer] offers [offerer.p_their()] hand to [offered], looking to help them up!"),
span_notice("You offer [offered] your hand, to try to help them up!"), null, 2)
offerer.apply_status_effect(/datum/status_effect/offering/no_item_received/needs_resting, src, /atom/movable/screen/alert/give/hand/helping, offered)
return
offerer.visible_message(span_notice("[offerer] extends out [offerer.p_their()] hand."),
span_notice("You extend out your hand."), null, 2)
offerer.apply_status_effect(/datum/status_effect/offering/no_item_received, src, /atom/movable/screen/alert/give/hand)
return
/obj/item/hand_item/hand/on_offer_taken(mob/living/carbon/offerer, mob/living/carbon/taker)
. = TRUE
if(taker.body_position == LYING_DOWN)
taker.help_shake_act(offerer)
if(taker.body_position == LYING_DOWN)
return // That didn't help them. Awkwaaaaard.
offerer.visible_message(span_notice("[offerer] helps [taker] up!"), span_nicegreen("You help [taker] up!"), span_hear("You hear someone helping someone else up!"), ignored_mobs = taker)
to_chat(taker, span_nicegreen("You take [offerer]'s hand, letting [offerer.p_them()] help your up! How nice of them!"))
offerer.add_mob_memory(/datum/memory/helped_up, protagonist = offerer, deuteragonist = taker)
taker.add_mob_memory(/datum/memory/helped_up, protagonist = offerer, deuteragonist = taker)
offerer.add_mood_event("helping_up", /datum/mood_event/helped_up, taker, TRUE) // Different IDs because you could be helped up and then help someone else up.
taker.add_mood_event("helped_up", /datum/mood_event/helped_up, offerer, FALSE)
qdel(src)
return
if(taker.buckled?.buckle_prevents_pull)
return // Can't start pulling them if they're buckled and that prevents pulls.
// We do a little switcheroo to ensure that it displays the pulling message that mentions
// taking taker by their hands.
var/offerer_zone_selected = offerer.zone_selected
offerer.zone_selected = "r_arm"
var/did_we_pull = offerer.start_pulling(taker) // Will return either null or FALSE. We only want to silence FALSE.
offerer.zone_selected = offerer_zone_selected
if(did_we_pull == FALSE)
return // That didn't work for one reason or the other. No need to display anything.
to_chat(offerer, span_notice("[taker] takes your hand, allowing you to pull [taker.p_them()] along."))
to_chat(taker, span_notice("You take [offerer]'s hand, which allows [offerer.p_them()] to pull you along. How polite!"))
qdel(src)
/obj/item/hand_item/stealer
name = "steal"
desc = "Your filthy little fingers are ready to commit crimes."
inhand_icon_state = "nothing"
attack_verb_continuous = list("steals")
attack_verb_simple = list("steal")
/obj/item/hand_item/stealer/attack(mob/living/target_mob, mob/living/user, params)
. = ..()
if (!ishuman(target_mob))
return
var/mob/living/carbon/human/target_human = target_mob
if(target_human == user)
to_chat(user, span_notice("Why would you try stealing your own shoes?"))
return
if (!target_human.shoes)
return
if (user.body_position != LYING_DOWN)
return
var/obj/item/clothing/shoes/item_to_strip = target_human.shoes
user.visible_message(span_warning("[user] starts stealing [target_human]'s [item_to_strip.name]!"), \
span_danger("You start stealing [target_human]'s [item_to_strip.name]..."))
to_chat(target_human, span_userdanger("[user] starts stealing your [item_to_strip.name]!"))
if (!do_after(user, item_to_strip.strip_delay, target_human))
return
if(!target_human.dropItemToGround(item_to_strip))
return
user.put_in_hands(item_to_strip)
user.visible_message(span_warning("[user] stole [target_human]'s [item_to_strip.name]!"), \
span_notice("You stole [target_human]'s [item_to_strip.name]!"))
to_chat(target_human, span_userdanger("[user] stole your [item_to_strip.name]!"))
/obj/item/hand_item/kisser
name = "kiss"
desc = "I want you all to know, everyone and anyone, to seal it with a kiss."
icon = 'icons/mob/simple/animal.dmi'
icon_state = "heart"
inhand_icon_state = "nothing"
/// The kind of projectile this version of the kiss blower fires
var/kiss_type = /obj/projectile/kiss
/// TRUE if the user was aiming anywhere but the mouth when they offer the kiss, if it's offered
var/cheek_kiss
/obj/item/hand_item/kisser/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
return ranged_interact_with_atom(interacting_with, user, modifiers)
/obj/item/hand_item/kisser/ranged_interact_with_atom(atom/target, mob/living/user, list/modifiers)
if(HAS_TRAIT(user, TRAIT_GARLIC_BREATH))
kiss_type = /obj/projectile/kiss/french
if(HAS_TRAIT(user, TRAIT_CHEF_KISS))
kiss_type = /obj/projectile/kiss/chef
var/obj/projectile/blown_kiss = new kiss_type(get_turf(user))
user.visible_message("<b>[user]</b> blows \a [blown_kiss] at [target]!", span_notice("You blow \a [blown_kiss] at [target]!"))
//Shooting Code:
blown_kiss.original = target
blown_kiss.fired_from = user
blown_kiss.firer = user // don't hit ourself that would be really annoying
blown_kiss.impacted = list(WEAKREF(user) = TRUE) // just to make sure we don't hit the wearer
blown_kiss.preparePixelProjectile(target, user)
blown_kiss.fire()
qdel(src)
return ITEM_INTERACT_SUCCESS
/obj/item/hand_item/kisser/on_offered(mob/living/carbon/offerer, mob/living/carbon/offered)
if(!(locate(/mob/living/carbon) in orange(1, offerer)))
return TRUE
cheek_kiss = (offerer.zone_selected != BODY_ZONE_PRECISE_MOUTH)
offerer.visible_message(span_notice("[offerer] leans in slightly, offering a kiss[cheek_kiss ? " on the cheek" : ""]!"),
span_notice("You lean in slightly, indicating you'd like to offer a kiss[cheek_kiss ? " on the cheek" : ""]!"), null, 2)
offerer.apply_status_effect(/datum/status_effect/offering/no_item_received, src)
return TRUE
/obj/item/hand_item/kisser/on_offer_taken(mob/living/carbon/offerer, mob/living/carbon/taker)
var/obj/projectile/blown_kiss = new kiss_type(get_turf(offerer))
offerer.visible_message("<b>[offerer]</b> gives [taker] \a [blown_kiss][cheek_kiss ? " on the cheek" : ""]!!", span_notice("You give [taker] \a [blown_kiss][cheek_kiss ? " on the cheek" : ""]!"), ignored_mobs = taker)
to_chat(taker, span_nicegreen("[offerer] gives you \a [blown_kiss][cheek_kiss ? " on the cheek" : ""]!"))
offerer.face_atom(taker)
taker.face_atom(offerer)
offerer.do_item_attack_animation(taker, used_item=src)
//We're still firing a shot here because I don't want to deal with some weird edgecase where direct impacting them with the projectile causes it to freak out because there's no angle or something
blown_kiss.original = taker
blown_kiss.fired_from = offerer
blown_kiss.firer = offerer // don't hit ourself that would be really annoying
blown_kiss.impacted = list(WEAKREF(offerer) = TRUE) // just to make sure we don't hit the wearer
blown_kiss.preparePixelProjectile(taker, offerer)
blown_kiss.suppressed = SUPPRESSED_VERY // this also means it's a direct offer
blown_kiss.fire()
qdel(src)
return TRUE // so the core offering code knows to halt
/obj/item/hand_item/kisser/death
name = "kiss of death"
desc = "If looks could kill, they'd be this."
color = COLOR_BLACK
kiss_type = /obj/projectile/kiss/death
/obj/projectile/kiss
name = "kiss"
icon = 'icons/mob/simple/animal.dmi'
icon_state = "heart"
hitsound = 'sound/effects/kiss.ogg'
hitsound_wall = 'sound/effects/kiss.ogg'
pass_flags = PASSTABLE | PASSGLASS | PASSGRILLE
speed = 1.6
damage_type = BRUTE
damage = 0 // love can't actually hurt you
armour_penetration = 100 // but if it could, it would cut through even the thickest plate
/obj/projectile/kiss/Initialize(mapload)
. = ..()
AddComponent(/datum/component/parriable_projectile)
/obj/projectile/kiss/fire(angle, atom/direct_target)
if(firer)
name = "[name] blown by [firer]"
return ..()
/obj/projectile/kiss/Impact(atom/A)
if(damage > 0 || !isliving(A)) // if we do damage or we hit a nonliving thing, we don't have to worry about a harmless hit because we can't wrongly do damage anyway
return ..()
harmless_on_hit(A)
qdel(src)
return FALSE
/**
* To get around shielded modsuits & such being set off by kisses when they shouldn't, we take a page from hallucination projectiles
* and simply fake our on hit effects. This lets kisses remain incorporeal without having to make some new trait for this one niche situation.
* This fake hit only happens if we can deal damage and if we hit a living thing. Otherwise, we just do normal on hit effects.
*/
/obj/projectile/kiss/proc/harmless_on_hit(mob/living/living_target)
playsound(get_turf(living_target), hitsound, 100, TRUE)
if(!suppressed) // direct
living_target.visible_message(span_danger("[living_target] is hit by \a [src]."), span_userdanger("You're hit by \a [src]!"), vision_distance=COMBAT_MESSAGE_RANGE)
living_target.add_mob_memory(/datum/memory/kissed, deuteragonist = firer)
living_target.add_mood_event("kiss", /datum/mood_event/kiss, firer, suppressed)
if(isliving(firer))
var/mob/living/kisser = firer
kisser.add_mob_memory(/datum/memory/kissed, protagonist = living_target, deuteragonist = firer)
try_fluster(living_target)
/obj/projectile/kiss/proc/try_fluster(mob/living/living_target)
// people with the social anxiety quirk can get flustered when hit by a kiss
if(!HAS_TRAIT(living_target, TRAIT_ANXIOUS) || (living_target.stat > SOFT_CRIT) || living_target.is_blind())
return
if(HAS_TRAIT(living_target, TRAIT_FEARLESS) || prob(50)) // 50% chance for it to apply, also immune while on meds
return
var/other_msg
var/self_msg
var/roll = rand(1, 3)
switch(roll)
if(1)
other_msg = "stumbles slightly, turning a bright red!"
self_msg = "You lose control of your limbs for a moment as your blood rushes to your face, turning it bright red!"
living_target.adjust_confusion(rand(5 SECONDS, 10 SECONDS))
if(2)
other_msg = "stammers softly for a moment before choking on something!"
self_msg = "You feel your tongue disappear down your throat as you fight to remember how to make words!"
addtimer(CALLBACK(living_target, TYPE_PROC_REF(/atom/movable, say), pick("Uhhh...", "O-oh, uhm...", "I- uhhhhh??", "You too!!", "What?")), rand(0.5 SECONDS, 1.5 SECONDS))
living_target.adjust_stutter(rand(10 SECONDS, 30 SECONDS))
if(3)
other_msg = "locks up with a stunned look on [living_target.p_their()] face, staring at [firer ? firer : "the ceiling"]!"
self_msg = "Your brain completely fails to process what just happened, leaving you rooted in place staring at [firer ? "[firer]" : "the ceiling"] for what feels like an eternity!"
living_target.face_atom(firer)
living_target.Stun(rand(3 SECONDS, 8 SECONDS))
living_target.visible_message("<b>[living_target]</b> [other_msg]", span_userdanger("Whoa! [self_msg]"))
/obj/projectile/kiss/on_hit(atom/target, blocked, pierce_hit)
def_zone = BODY_ZONE_HEAD // let's keep it PG, people
. = ..()
if(isliving(target))
var/mob/living/living_target = target
living_target.add_mood_event("kiss", /datum/mood_event/kiss, firer, suppressed)
try_fluster(living_target)
/obj/projectile/kiss/death
name = "kiss of death"
damage = 35 // okay i kinda lied about love not being able to hurt you
wound_bonus = 0
sharpness = SHARP_POINTY
color = COLOR_BLACK
/obj/projectile/kiss/death/on_hit(atom/target, blocked, pierce_hit)
. = ..()
if(!iscarbon(target))
return
var/mob/living/carbon/heartbreakee = target
var/obj/item/organ/internal/heart/dont_go_breakin_my_heart = heartbreakee.get_organ_slot(ORGAN_SLOT_HEART)
dont_go_breakin_my_heart.apply_organ_damage(999)
/obj/projectile/kiss/french
name = "french kiss (is that a hint of garlic?)"
color = "#f2e9d2" //Scientifically proven to be the colour of garlic
/obj/projectile/kiss/french/harmless_on_hit(mob/living/living_target)
. = ..()
if(isnull(living_target.reagents))
return
//Don't stack the garlic
if(!living_target.has_reagent(/datum/reagent/consumable/garlic))
//Phwoar
living_target.reagents.add_reagent(/datum/reagent/consumable/garlic, 1)
living_target.visible_message("[living_target] has a funny look on [living_target.p_their()] face.", "Wow, that is a strong after taste of garlic!", vision_distance=COMBAT_MESSAGE_RANGE)
/obj/projectile/kiss/chef
name = "chef's kiss"
// If our chef's kiss hits a food item, we will improve it with love.
/obj/projectile/kiss/chef/on_hit(atom/target, blocked, pierce_hit)
. = ..()
if(!IS_EDIBLE(target) || !target.reagents)
return
if(!firer || !target.Adjacent(firer) || !ismob(firer))
return
var/mob/kisser = firer
// From here on, no message
suppressed = SUPPRESSED_VERY
if(!(kisser.mind && HAS_TRAIT_FROM(target, TRAIT_FOOD_CHEF_MADE, REF(kisser.mind))))
to_chat(firer, span_warning("Wait a second, you didn't make this [target.name]. How can you claim it as your own?"))
return
if(target.reagents.has_reagent(/datum/reagent/love))
to_chat(firer, span_warning("You've already blessed [target.name] with your heart and soul."))
return
var/amount_nutriment = target.reagents.get_reagent_amount(/datum/reagent/consumable/nutriment, type_check = REAGENT_PARENT_TYPE)
if(amount_nutriment <= 0)
to_chat(firer, span_warning("There's not enough nutrition in [target.name] for it to be a proper meal."))
return
to_chat(firer, span_green("You deliver a chef's kiss over [target], declaring it perfect."))
target.visible_message(span_notice("[firer] delivers a chef's kiss over [target]."), ignored_mobs = firer)
target.reagents.add_reagent(/datum/reagent/love, clamp(amount_nutriment / 4, 1, 10)) // clamped to about half of the most dense food I think we have (super bite burger)