Blow kisses!

This commit is contained in:
ariaworld
2024-02-22 12:35:21 +01:00
parent d439eeef0e
commit 0e11e93acf
12 changed files with 287 additions and 59 deletions

View File

@@ -1,4 +1,5 @@
#define SIGNAL_TRAIT(trait_ref) "trait [trait_ref]"
#define SIGNAL_REMOVETRAIT(trait_ref) "removetrait [trait_ref]"
// trait accessor defines
#define ADD_TRAIT(target, trait, source) \
@@ -59,6 +60,30 @@
};\
}\
} while (0)
#define REMOVE_TRAITS_IN(target, sources) \
do { \
var/list/_L = target.status_traits; \
var/list/_S = sources; \
if (sources && !islist(sources)) { \
_S = list(sources); \
} else { \
_S = sources\
}; \
if (_L) { \
for (var/_T in _L) { \
_L[_T] -= _S;\
if (!length(_L[_T])) { \
_L -= _T; \
SEND_SIGNAL(target, SIGNAL_REMOVETRAIT(_T)); \
}; \
};\
if (!length(_L)) { \
target.status_traits = null\
};\
}\
} while (0)
#define HAS_TRAIT(target, trait) (target.status_traits ? (target.status_traits[trait] ? TRUE : FALSE) : FALSE)
#define HAS_TRAIT_FROM(target, trait, source) (target.status_traits ? (target.status_traits[trait] ? (source in target.status_traits[trait]) : FALSE) : FALSE)
#define HAS_TRAIT_FROM_ONLY(target, trait, source) (\
@@ -236,6 +261,12 @@
#define TRAIT_NO_STAMINA_REGENERATION "block_stamina_regen" /// Prevents stamina regeneration
#define TRAIT_ARMOR_BROKEN "armor_broken" //acts as if you are wearing no clothing when taking damage, does not affect non-clothing sources of protection
#define TRAIT_IWASBATONED "iwasbatoned" //some dastardly fellow has struck you with a baton and thought to use another to strike you again, the rogue
//Given by social anxiety quirk
#define TRAIT_ANXIOUS "anxious"
/// Trait granted by lipstick
#define LIPSTICK_TRAIT "lipstick_trait"
/// Blowing kisses that actually do damage to the victim
#define TRAIT_KISS_OF_DEATH "kiss_of_death"
/// forces update_density to make us not dense
#define TRAIT_LIVING_NO_DENSITY "living_no_density"
/// forces us to not render our overlays

View File

@@ -132,6 +132,8 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_SNOWSTORM_IMMUNE" = TRAIT_SNOWSTORM_IMMUNE,
"TRAIT_VOIDSTORM_IMMUNE" = TRAIT_VOIDSTORM_IMMUNE,
"TRAIT_WEATHER_IMMUNE" = TRAIT_WEATHER_IMMUNE,
"TRAIT_KISS_OF_DEATH" = TRAIT_KISS_OF_DEATH,
"TRAIT_ANXIOUS" = TRAIT_ANXIOUS,
"TRAIT_SPACEWALK" = TRAIT_SPACEWALK,
"TRAIT_PRIMITIVE" = TRAIT_PRIMITIVE, //unable to use mechs. Given to Ash Walkers
),

View File

@@ -225,3 +225,8 @@
description = "<span class='nicegreen'>The gods are pleased with this offering!</span>\n"
mood_change = 5
timeout = 3 MINUTES
/datum/mood_event/kiss
description = span_nicegreen("Someone blew a kiss at me, I must be a real catch!\n")
mood_change = 1.5
timeout = 2 MINUTES

View File

@@ -43,11 +43,11 @@
GLOBAL_LIST_EMPTY(family_heirlooms)
/datum/quirk/family_heirloom/on_spawn()
/datum/quirk/family_heirloom/on_spawn()
// Define holder and type
var/mob/living/carbon/human/human_holder = quirk_holder
var/obj/item/heirloom_type
// The quirk holder's species - we have a 50% chance, if we have a species with a set heirloom, to choose a species heirloom.
var/datum/species/holder_species = human_holder.dna?.species
if(holder_species && LAZYLEN(holder_species.family_heirlooms) && prob(50))
@@ -61,13 +61,13 @@ GLOBAL_LIST_EMPTY(family_heirlooms)
// If we didn't find an heirloom somehow, throw them a generic one
if(!heirloom_type)
heirloom_type = pick(/obj/item/toy/cards/deck, /obj/item/lighter, /obj/item/dice/d20)
// Create the heirloom item
heirloom = new heirloom_type(get_turf(quirk_holder))
// Add to global list
GLOB.family_heirlooms += heirloom
// Determine and assign item location
var/list/slots = list(
"in your left pocket" = ITEM_SLOT_LPOCKET,
@@ -330,6 +330,7 @@ GLOBAL_LIST_EMPTY(family_heirlooms)
gain_text = "<span class='danger'>You start worrying about what you're saying.</span>"
lose_text = "<span class='notice'>You feel easier about talking again.</span>" //if only it were that easy!
medical_record_text = "Patient is usually anxious in social encounters and prefers to avoid them."
mob_trait = TRAIT_ANXIOUS
var/dumb_thing = TRUE
processing_quirk = TRUE

View File

@@ -7,6 +7,8 @@
w_class = WEIGHT_CLASS_TINY
var/colour = "red"
var/open = FALSE
/// A trait that's applied while someone has this lipstick applied, and is removed when the lipstick is removed
var/lipstick_trait
/obj/item/lipstick/purple
name = "purple lipstick"
@@ -21,6 +23,10 @@
name = "black lipstick"
colour = "black"
/obj/item/lipstick/black/death
name = "Kiss of Death"
lipstick_trait = TRAIT_KISS_OF_DEATH
/obj/item/lipstick/random
name = "lipstick"
icon_state = "random_lipstick"
@@ -33,7 +39,7 @@
/obj/item/lipstick/attack_self(mob/user)
cut_overlays()
to_chat(user, "<span class='notice'>You twist \the [src] [open ? "closed" : "open"].</span>")
to_chat(user, span_notice("You twist \the [src] [open ? "closed" : "open"]."))
open = !open
if(open)
var/mutable_appearance/colored_overlay = mutable_appearance(icon, "lipstick_uncap_color")
@@ -44,60 +50,53 @@
icon_state = "lipstick"
/obj/item/lipstick/attack(mob/M, mob/user)
if(!open)
if(!open || !ismob(M))
return
if(!ismob(M))
if(!ishuman(M))
to_chat(user, span_warning("Where are the lips on that?"))
return
if(ishuman(M))
var/mob/living/carbon/human/H = M
if(H.is_mouth_covered())
to_chat(user, "<span class='warning'>Remove [ H == user ? "your" : "[H.p_their()]" ] mask!</span>")
return
if(H.lip_style) //if they already have lipstick on
to_chat(user, "<span class='warning'>You need to wipe off the old lipstick first!</span>")
return
if(H == user)
user.visible_message("<span class='notice'>[user] does [user.p_their()] lips with \the [src].</span>", \
"<span class='notice'>You take a moment to apply \the [src]. Perfect!</span>")
H.lip_style = "lipstick"
H.lip_color = colour
H.update_body()
else
user.visible_message("<span class='warning'>[user] begins to do [H]'s lips with \the [src].</span>", \
"<span class='notice'>You begin to apply \the [src] on [H]'s lips...</span>")
if(do_after(user, 20, target = H))
user.visible_message("[user] does [H]'s lips with \the [src].", \
"<span class='notice'>You apply \the [src] on [H]'s lips.</span>")
H.lip_style = "lipstick"
H.lip_color = colour
H.update_body()
else
to_chat(user, "<span class='warning'>Where are the lips on that?</span>")
var/mob/living/carbon/human/target = M
if(target.is_mouth_covered())
to_chat(user, span_warning("Remove [ target == user ? "your" : "[target.p_their()]" ] mask!"))
return
if(target.lip_style) //if they already have lipstick on
to_chat(user, span_warning("You need to wipe off the old lipstick first!"))
return
if(target == user)
user.visible_message(span_notice("[user] does [user.p_their()] lips with \the [src]."), \
span_notice("You take a moment to apply \the [src]. Perfect!"))
target.update_lips("lipstick", colour, lipstick_trait)
return
user.visible_message(span_warning("[user] begins to do [target]'s lips with \the [src]."), \
span_notice("You begin to apply \the [src] on [target]'s lips..."))
if(!do_after(user, 2 SECONDS, target = target))
return
user.visible_message(span_notice("[user] does [target]'s lips with \the [src]."), \
span_notice("You apply \the [src] on [target]'s lips."))
target.update_lips("lipstick", colour, lipstick_trait)
//you can wipe off lipstick with paper!
/obj/item/paper/attack(mob/M, mob/user)
if(user.zone_selected == BODY_ZONE_PRECISE_MOUTH)
if(!ismob(M))
return
if(user.zone_selected != BODY_ZONE_PRECISE_MOUTH || !ishuman(M))
return ..()
if(ishuman(M))
var/mob/living/carbon/human/H = M
if(H == user)
to_chat(user, "<span class='notice'>You wipe off the lipstick with [src].</span>")
H.lip_style = null
H.update_body()
else
user.visible_message("<span class='warning'>[user] begins to wipe [H]'s lipstick off with \the [src].</span>", \
"<span class='notice'>You begin to wipe off [H]'s lipstick...</span>")
if(do_after(user, 10, target = H))
user.visible_message("[user] wipes [H]'s lipstick off with \the [src].", \
"<span class='notice'>You wipe off [H]'s lipstick.</span>")
H.lip_style = null
H.update_body()
else
..()
var/mob/living/carbon/human/target = M
if(target == user)
to_chat(user, span_notice("You wipe off the lipstick with [src]."))
target.update_lips(null)
return
user.visible_message(span_warning("[user] begins to wipe [target]'s lipstick off with \the [src]."), \
span_notice("You begin to wipe off [target]'s lipstick..."))
if(!do_after(user, 10, target = target))
return
user.visible_message(span_notice("[user] wipes [target]'s lipstick off with \the [src]."), \
span_notice("You wipe off [target]'s lipstick."))
target.update_lips(null)
/obj/item/razor
name = "electric razor"

View File

@@ -1117,6 +1117,11 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301
else
qdel(target)
/obj/item/proc/can_trigger_gun(mob/living/user)
if(!user.can_use_guns(src))
return FALSE
return TRUE
/obj/item/circlegame
name = "circled hand"
desc = "If somebody looks at this while it's below your waist, you get to bop them."
@@ -1240,10 +1245,6 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301
"<span class='notice'>You slap [M]!</span>",\
"<span class='hear'>You hear a slap.</span>")
return
/obj/item/proc/can_trigger_gun(mob/living/user)
if(!user.can_use_guns(src))
return FALSE
return TRUE
/obj/item/slapper/on_offered(mob/living/carbon/offerer)
. = TRUE
@@ -1359,6 +1360,135 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301
attempt_join_gang(taker)
qdel(src)
/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/animal.dmi'
icon_state = "heart"
/// The kind of projectile this version of the kiss blower fires
var/kiss_type = /obj/item/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/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
. = ..()
// . |= AFTERATTACK_PROCESSED_ITEM
/*
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/item/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(user = TRUE) // just to make sure we don't hit the wearer
blown_kiss.preparePixelProjectile(target, user)
blown_kiss.fire()
qdel(src)
/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/item/projectile/kiss/death
/obj/item/projectile/kiss
name = "kiss"
icon = 'icons/mob/animal.dmi'
icon_state = "heart"
hitsound = 'sound/effects/kiss.ogg'
hitsound_wall = 'sound/effects/kiss.ogg'
pass_flags = PASSTABLE | PASSGLASS | PASSGRILLE
damage_type = BRUTE
damage = 0
nodamage = TRUE // love can't actually hurt you
armour_penetration = 100 // but if it could, it would cut through even the thickest plate
flag = MAGIC // and most importantly, love is magic~
/obj/item/projectile/kiss/fire(angle, atom/direct_target)
if(firer)
name = "[name] blown by [firer]"
return ..()
/obj/item/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/item/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/item/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.confused += (rand(5, 10))
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, /atom/movable.proc/say, pick("Uhhh...", "O-oh, uhm...", "I- uhhhhh??", "You too!!", "What?")), rand(0.5 SECONDS, 1.5 SECONDS))
living_target.stuttering += rand(5, 15)
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 [firer ? "at [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/item/projectile/kiss/death
name = "kiss of death"
nodamage = FALSE // okay i kinda lied about love not being able to hurt you
damage = 15
wound_bonus = 0
sharpness = SHARP_POINTY
color = COLOR_BLACK
/obj/item/projectile/kiss/death/on_hit(atom/target, blocked, pierce_hit)
. = ..()
if(!iscarbon(target))
return
var/mob/living/carbon/heartbreakee = target
var/obj/item/organ/heart/dont_go_breakin_my_heart = heartbreakee.getorganslot(ORGAN_SLOT_HEART)
dont_go_breakin_my_heart.applyOrganDamage(15)
/obj/item/extendohand
name = "extendo-hand"
desc = "Futuristic tech has allowed these classic spring-boxing toys to essentially act as a fully functional hand-operated hand prosthetic."

View File

@@ -547,6 +547,41 @@
if(..())
dropItemToGround(I)
/**
* Used to update the makeup on a human and apply/remove lipstick traits, then store/unstore them on the head object in case it gets severed
**/
/mob/living/proc/update_lips(new_style, new_color, apply_trait, update = TRUE)
return
/mob/living/carbon/human/update_lips(new_style, new_color, apply_trait, update = TRUE)
lip_style = new_style
lip_color = new_color
var/obj/item/bodypart/head/hopefully_a_head = get_bodypart(BODY_ZONE_HEAD)
REMOVE_TRAITS_IN(src, LIPSTICK_TRAIT)
if(hopefully_a_head)
hopefully_a_head.stored_lipstick_trait = null
hopefully_a_head.lip_style = new_style
hopefully_a_head.lip_color = new_color
if(new_style && apply_trait)
ADD_TRAIT(src, apply_trait, LIPSTICK_TRAIT)
hopefully_a_head?.stored_lipstick_trait = apply_trait
if(update)
update_body_parts()
/**
* A wrapper for [mob/living/carbon/human/update_lips] that sets the lip style and color to null.
**/
/mob/living/proc/clean_lips()
return
/mob/living/carbon/human/clean_lips()
if(!lip_style)
return FALSE
update_lips(null, null, update = TRUE)
return TRUE
/mob/living/carbon/human/clean_blood()
var/mob/living/carbon/human/H = src
if(H.gloves)

View File

@@ -195,6 +195,26 @@
/datum/emote/living/kiss
key = "kiss"
key_third_person = "kisses"
/datum/emote/living/kiss/run_emote(mob/living/user, params, type_override, intentional)
. = ..()
if(!.)
return
var/kiss_type = /obj/item/hand_item/kisser
if(HAS_TRAIT(user, TRAIT_KISS_OF_DEATH))
kiss_type = /obj/item/hand_item/kisser/death
var/obj/item/kiss_blower = new kiss_type(user)
if(user.put_in_hands(kiss_blower))
to_chat(user, span_notice("You ready your kiss-blowing hand."))
else
qdel(kiss_blower)
to_chat(user, span_warning("You're incapable of blowing a kiss in your current state."))
/datum/emote/living/kiss2
key = "kiss2"
key_third_person = "kisses"
message = "blows a kiss."
message_param = "blows a kiss to %t."
emote_type = EMOTE_AUDIBLE

View File

@@ -392,8 +392,7 @@
H.hair_style = hair_style
H.facial_hair_color = facial_hair_color
H.facial_hair_style = facial_hair_style
H.lip_style = lip_style
H.lip_color = lip_color
H.update_lips(lip_style, lip_color, stored_lipstick_trait)
if(real_name)
C.real_name = real_name
real_name = ""

View File

@@ -32,6 +32,9 @@
var/lip_style = null
var/lip_color = "white"
var/stored_lipstick_trait
//If the head is a special sprite
var/custom_head
@@ -78,6 +81,7 @@
hair_style = "Bald"
facial_hair_style = "Shaved"
lip_style = null
stored_lipstick_trait = null
else if(!animal_origin)
var/mob/living/carbon/human/H = C

BIN
sound/effects/kiss.ogg Normal file

Binary file not shown.

View File

@@ -6,4 +6,6 @@ No changes were made to the sounds, and all credit goes to kMoon.
batreflect sounds are by shadoWisp on freesound.org:
https://www.freesound.org/people/shadoWisp/sounds/252044/
Small parts of the sound are cut out and used.
Small parts of the sound are cut out and used.
kiss sound: https://freesound.org/people/stereostereo/sounds/117355/