Skip to content

Commit

Permalink
Merge pull request #397 from Fatherless1Xx/plswork
Browse files Browse the repository at this point in the history
Slave collar
  • Loading branch information
DerFlammenwerfer authored Jan 15, 2025
2 parents dea71d1 + f609d31 commit c52d828
Show file tree
Hide file tree
Showing 7 changed files with 413 additions and 0 deletions.
58 changes: 58 additions & 0 deletions code/datums/sexcon/sex_actions/deviant/spanking.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/datum/sex_action/spanking
name = "Spank them"
check_same_tile = FALSE
continous = TRUE
stamina_cost = 0
do_time = 1 SECONDS
var/last_pain_sound = 0
var/pain_sound_cooldown = 3 SECONDS

/datum/sex_action/spanking/shows_on_menu(mob/living/carbon/human/user, mob/living/carbon/human/target)
if(user == target)
return FALSE
return TRUE

/datum/sex_action/spanking/can_perform(mob/living/user, mob/living/target)
if(user == target)
return FALSE
return TRUE

/datum/sex_action/spanking/on_start(mob/living/carbon/human/user, mob/living/carbon/human/target)
..()
user.visible_message(span_warning("[user] starts spanking [target]..."))

/datum/sex_action/spanking/on_perform(mob/living/carbon/human/user, mob/living/carbon/human/target)
. = ..()
playsound(target, 'sound/foley/slap.ogg', 50, TRUE, -1)

// Flash screen red based on force level
switch(user.sexcon.force)
if(SEX_FORCE_LOW)
target.flash_fullscreen("redflash1")
if(SEX_FORCE_MID)
target.flash_fullscreen("redflash2")
if(SEX_FORCE_HIGH)
target.flash_fullscreen("redflash3")
if(SEX_FORCE_EXTREME)
target.flash_fullscreen("redflash3")

addtimer(CALLBACK(target, TYPE_PROC_REF(/mob, clear_fullscreen), "redflash"), 0.5 SECONDS)

// Occasional pain sounds with proper emotes
if(world.time > last_pain_sound + pain_sound_cooldown && prob(25))
if(prob(50))
target.emote("whimpers", intentional = FALSE)
else
target.emote("groans", intentional = FALSE)
last_pain_sound = world.time

var/force_text = user.sexcon.get_generic_force_adjective()
user.visible_message(user.sexcon.spanify_force("[user] [force_text] spanks [target]!"))

// Pain messages
to_chat(target, span_warning("It stings!"))
to_chat(target, span_danger("It hurts!"))

/datum/sex_action/spanking/on_finish(mob/living/carbon/human/user, mob/living/carbon/human/target)
..()
user.visible_message(span_warning("[user] stops spanking [target]..."))
204 changes: 204 additions & 0 deletions code/modules/antagonists/collar_master/collar_master.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/datum/antagonist/collar_master
name = "Collar Master"
antagpanel_category = "Other"
show_in_antagpanel = FALSE
show_name_in_check_antagonists = FALSE
var/obj/item/clothing/neck/roguetown/cursed_collar/my_collar
var/static/list/animal_sounds = list(
"lets out a whimper!",
"whines softly.",
"makes a pitiful noise.",
"whimpers.",
"lets out a submissive bark.",
"mewls pathetically."
)

/datum/antagonist/collar_master/on_gain()
. = ..()
owner.current.verbs += list(
/mob/proc/collar_scry,
/mob/proc/collar_listen,
/mob/proc/collar_shock,
/mob/proc/collar_message,
/mob/proc/collar_force_surrender,
/mob/proc/collar_force_naked,
/mob/proc/collar_permit_clothing,
/mob/proc/collar_toggle_silence,
/mob/proc/collar_force_emote,
)
to_chat(owner.current, span_notice("You can now control your pet through the Collar menu."))

/datum/antagonist/collar_master/on_removal()
owner.current.verbs -= list(
/mob/proc/collar_scry,
/mob/proc/collar_listen,
/mob/proc/collar_shock,
/mob/proc/collar_message,
/mob/proc/collar_force_surrender,
/mob/proc/collar_force_naked,
/mob/proc/collar_permit_clothing,
/mob/proc/collar_toggle_silence,
/mob/proc/collar_force_emote,
)
. = ..()

/mob/proc/collar_control_menu()
set name = "Collar Control"
set category = "Collar"

var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
if(!CM || !CM.my_collar || !CM.my_collar.victim)
return

/mob/proc/select_pet(var/action)
var/list/pets = list()
for(var/datum/antagonist/collar_master/CM in mind.antag_datums)
if(CM.my_collar && CM.my_collar.victim)
pets[CM.my_collar.victim.name] = CM.my_collar

if(!length(pets))
return null

var/choice = input(src, "Choose a pet:", "Pet Selection") as null|anything in pets
if(!choice)
return null
return pets[choice]

/mob/proc/collar_scry()
set name = "Scry on Pet"
set category = "Collar"

var/obj/item/clothing/neck/roguetown/cursed_collar/collar = select_pet("scry")
if(!collar)
return

var/mob/dead/observer/screye/S = scry_ghost()
if(S)
S.ManualFollow(collar.victim)
addtimer(CALLBACK(S, TYPE_PROC_REF(/mob/dead/observer, reenter_corpse)), 8 SECONDS)

/mob/proc/collar_listen()
set name = "Listen to Pet"
set category = "Collar"

var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
if(!CM || !CM.my_collar || !CM.my_collar.victim)
return

CM.my_collar.listening = !CM.my_collar.listening
to_chat(src, span_notice("You [CM.my_collar.listening ? "attune your mind to" : "cease listening through"] the collar."))

/mob/proc/collar_shock()
set name = "Shock Pet"
set category = "Collar"

var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
if(!CM || !CM.my_collar || !CM.my_collar.victim)
return

to_chat(CM.my_collar.victim, span_danger("The collar sends painful shocks through your body!"))
CM.my_collar.victim.electrocute_act(15, CM.my_collar, flags = SHOCK_NOGLOVES)
CM.my_collar.victim.Knockdown(20)
playsound(CM.my_collar.victim, 'sound/blank.ogg', 50, TRUE)

/mob/proc/collar_message()
set name = "Send Message"
set category = "Collar"

var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
if(!CM || !CM.my_collar || !CM.my_collar.victim)
return

var/msg = input(src, "Enter a message to send to your pet:", "Collar Message") as text|null
if(msg)
to_chat(CM.my_collar.victim, span_warning("Your collar tingles as you hear your master's voice: [msg]"))

/mob/proc/collar_force_surrender()
set name = "Force Surrender"
set category = "Collar"

var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
if(!CM || !CM.my_collar || !CM.my_collar.victim)
return

to_chat(CM.my_collar.victim, span_userdanger("The collar forces you to your knees!"))
CM.my_collar.victim.Paralyze(600) // 1 minute stun
new /obj/effect/temp_visual/surrender(get_turf(CM.my_collar.victim))
playsound(CM.my_collar.victim, 'sound/blank.ogg', 50, TRUE)

/mob/proc/collar_force_naked()
set name = "Force Strip"
set category = "Collar"

var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
if(!CM || !CM.my_collar || !CM.my_collar.victim)
return

var/mob/living/victim = CM.my_collar.victim
to_chat(victim, span_userdanger("The collar's magic forces you to remove all your clothing!"))
if(ishuman(victim))
var/mob/living/carbon/human/H = victim
for(var/obj/item/I in H.get_equipped_items())
if(I == CM.my_collar) // Don't remove the collar itself
continue
if(H.dropItemToGround(I, TRUE))
H.visible_message(span_warning("[H]'s [I.name] falls to the ground!"))

ADD_TRAIT(victim, TRAIT_NUDIST, CURSED_ITEM_TRAIT)
playsound(victim, 'sound/blank.ogg', 50, TRUE)

/mob/proc/collar_permit_clothing()
set name = "Permit Clothing"
set category = "Collar"

var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
if(!CM || !CM.my_collar || !CM.my_collar.victim)
return

var/mob/living/victim = CM.my_collar.victim
to_chat(victim, span_notice("The collar's magic allows you to wear clothing again."))
REMOVE_TRAIT(victim, TRAIT_NUDIST, CURSED_ITEM_TRAIT)
playsound(victim, 'sound/blank.ogg', 50, TRUE)

/mob/proc/collar_toggle_silence()
set name = "Toggle Pet Speech"
set category = "Collar"

var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
if(!CM || !CM.my_collar || !CM.my_collar.victim)
return

CM.my_collar.silenced = !CM.my_collar.silenced
to_chat(CM.my_collar.victim, span_userdanger("The collar [CM.my_collar.silenced ? "forces you to speak like an animal!" : "allows you to speak normally again."]"))
playsound(CM.my_collar.victim, 'sound/blank.ogg', 50, TRUE)

if(CM.my_collar.silenced)
RegisterSignal(CM.my_collar.victim, COMSIG_MOB_SAY, PROC_REF(handle_silenced_speech))
else
UnregisterSignal(CM.my_collar.victim, COMSIG_MOB_SAY)

/mob/proc/handle_silenced_speech(datum/source, list/speech_args)
SIGNAL_HANDLER

var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
if(!CM || !CM.my_collar || !CM.my_collar.silenced)
return

speech_args[SPEECH_MESSAGE] = ""
emote("me", EMOTE_VISIBLE, pick(CM.animal_sounds))
return TRUE // Just return TRUE to block speech

/mob/proc/collar_force_emote()
set name = "Force Emote"
set category = "Collar"

var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
if(!CM || !CM.my_collar || !CM.my_collar.victim)
return

var/emote = input(src, "What emote should your pet perform?", "Force Emote") as text|null
if(!emote)
return

CM.my_collar.victim.say(emote, forced = TRUE)
playsound(CM.my_collar.victim, 'sound/blank.ogg', 50, TRUE)
103 changes: 103 additions & 0 deletions code/modules/clothing/neck/_neck.dm
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
#include "../../antagonists/collar_master/collar_master.dm"

#define COMSIG_MOB_ATTACK "mob_attack"
#define COMSIG_MOB_SAY "mob_say"
#define COMSIG_MOB_CLICKON "mob_clickon"
#define COMSIG_ITEM_PRE_UNEQUIP "item_pre_unequip"
#define COMPONENT_CANCEL_ATTACK "cancel_attack"
#define COMPONENT_CANCEL_SAY "cancel_say"
#define COMPONENT_ITEM_BLOCK_UNEQUIP (1<<0)

/obj/item/clothing/neck
name = "necklace"
icon = 'icons/obj/clothing/neck.dmi'
Expand Down Expand Up @@ -223,3 +233,96 @@
user.visible_message(span_notice("I untie [oldName] back into a [newBand.name]."), span_notice("[user] unties [oldName] back into a [newBand.name]."))
else
to_chat(user, span_warning("I must be holding [src] in order to untie it!"))

/obj/item/clothing/neck/roguetown/cursed_collar
name = "cursed collar"
desc = "A sinister looking collar with emerald studs. It seems to radiate a dark energy."
icon_state = "listenstone"
item_state = "listenstone"
w_class = WEIGHT_CLASS_SMALL
slot_flags = ITEM_SLOT_NECK
body_parts_covered = NECK
var/mob/living/carbon/human/victim = null
var/mob/living/carbon/human/collar_master = null
var/listening = FALSE
var/silenced = FALSE
resistance_flags = INDESTRUCTIBLE
armor = list("blunt" = 0, "slash" = 0, "stab" = 0, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 0, "acid" = 0)

/obj/item/clothing/neck/roguetown/cursed_collar/proc/handle_speech(datum/source, list/speech_args)
SIGNAL_HANDLER
if(silenced)
speech_args[SPEECH_MESSAGE] = ""
var/mob/living/carbon/human/H = source
if(istype(H))
H.say("*[pick(list(
"whines softly.",
"makes a pitiful noise.",
"whimpers.",
"lets out a submissive bark.",
"mewls pathetically."
))]")
return COMPONENT_CANCEL_SAY
return NONE

/obj/item/clothing/neck/roguetown/cursed_collar/proc/check_attack(datum/source, atom/target)
SIGNAL_HANDLER
if(!istype(target, /mob/living/carbon/human))
return NONE

if(target == collar_master)
to_chat(source, span_warning("The collar sends painful shocks through your body as you try to attack your master!"))
var/mob/living/carbon/human/H = source
H.electrocute_act(25, src, flags = SHOCK_NOGLOVES)
H.Paralyze(600) // 1 minute stun
playsound(H, 'sound/blank.ogg', 50, TRUE)
return COMPONENT_CANCEL_ATTACK
return NONE

/obj/item/clothing/neck/roguetown/cursed_collar/attack(mob/living/carbon/human/M, mob/living/carbon/human/user)
if(!istype(M) || !istype(user))
return ..()

if(M.get_item_by_slot(SLOT_NECK))
to_chat(user, span_warning("[M] is already wearing something around their neck!"))
return

if(!do_mob(user, M, 50))
return

victim = M
collar_master = user
if(!M.equip_to_slot_if_possible(src, SLOT_NECK, 0, 0, 1))
to_chat(user, span_warning("You fail to collar [M]!"))
victim = null
collar_master = null
return

ADD_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT)
to_chat(M, span_userdanger("The collar snaps shut around your neck!"))
to_chat(user, span_notice("You successfully collar [M]."))

if(user.mind)
var/datum/antagonist/collar_master/CM = new()
CM.my_collar = src
user.mind.add_antag_datum(CM)

/obj/item/clothing/neck/roguetown/cursed_collar/Destroy()
victim = null
collar_master = null
return ..()

/obj/item/clothing/neck/roguetown/cursed_collar/equipped(mob/user, slot)
. = ..()
if(slot == SLOT_NECK && user == victim)
RegisterSignal(src, COMSIG_ITEM_PRE_UNEQUIP, PROC_REF(prevent_removal))
RegisterSignal(user, COMSIG_MOB_SAY, PROC_REF(handle_speech))
RegisterSignal(user, COMSIG_MOB_CLICKON, PROC_REF(check_attack))

/obj/item/clothing/neck/roguetown/cursed_collar/proc/prevent_removal(datum/source, mob/living/carbon/human/user)
SIGNAL_HANDLER
if(user == victim)
to_chat(user, span_userdanger("The collar's magic holds it firmly in place! You can't remove it!"))
playsound(user, 'sound/blank.ogg', 50, TRUE)
return COMPONENT_ITEM_BLOCK_UNEQUIP
return NONE
Original file line number Diff line number Diff line change
Expand Up @@ -881,3 +881,9 @@
additional_items = list(/obj/item/clothing/wrists/roguetown/bracers/leather, /obj/item/roguegear)
created_item = /obj/item/clothing/wrists/roguetown/hiddenblade
i_type = "Armor"

/datum/anvil_recipe/armor/cursed_collar
name = "Cursed Collar"
req_bar = /obj/item/ingot/steel
created_item = /obj/item/clothing/neck/roguetown/cursed_collar
craftdiff = 3
Loading

0 comments on commit c52d828

Please sign in to comment.