diff --git a/code/__DEFINES/components.dm b/code/__DEFINES/components.dm
index e133915c7c6..2d609fd3599 100644
--- a/code/__DEFINES/components.dm
+++ b/code/__DEFINES/components.dm
@@ -4,6 +4,8 @@
/// Arguments given here are packaged in a list and given to _SendSignal
#define SEND_SIGNAL(target, sigtype, arguments...) ( !target.comp_lookup || !target.comp_lookup[sigtype] ? NONE : target._SendSignal(sigtype, list(target, ##arguments)) )
+#define SIGNAL_HANDLER SHOULD_NOT_SLEEP(TRUE); SHOULD_NOT_OVERRIDE(TRUE)
+
#define SEND_GLOBAL_SIGNAL(sigtype, arguments...) ( SEND_SIGNAL(SSdcs, sigtype, ##arguments) )
/// Return this from `/datum/component/Initialize` or `datum/component/OnTransfer` to have the component be deleted if it's applied to an incorrect type.
diff --git a/code/__DEFINES/dcs/helpers.dm b/code/__DEFINES/dcs/helpers.dm
index 2b95de8251b..e69de29bb2d 100644
--- a/code/__DEFINES/dcs/helpers.dm
+++ b/code/__DEFINES/dcs/helpers.dm
@@ -1,4 +0,0 @@
-/// Signifies that this proc is used to handle signals.
-/// Every proc you pass to RegisterSignal must have this.
-#define SIGNAL_HANDLER SHOULD_NOT_SLEEP(TRUE)
-
diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 5640245437e..9fa870013c2 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -2,6 +2,8 @@
#define COMSIG_MOB_ATTACK "mob_attack"
#define COMSIG_MOB_SAY "mob_say"
#define COMSIG_MOB_CLICKON "mob_clickon"
+#define COMSIG_LIVING_TRY_ATTACK "living_try_attack"
+#define COMSIG_MOB_EMOTE "mob_emote"
// Item signals
#define COMSIG_ITEM_PRE_UNEQUIP "item_pre_unequip"
@@ -10,6 +12,7 @@
#define COMPONENT_CANCEL_ATTACK (1<<0)
#define COMPONENT_CANCEL_SAY (1<<0)
#define COMPONENT_ITEM_BLOCK_UNEQUIP (1<<0)
+#define COMPONENT_CANCEL_EMOTE (1<<1)
// Collar signals
#define COMSIG_CARBON_GAIN_COLLAR "carbon_gain_collar"
@@ -20,3 +23,13 @@
// Living revive signal
#define COMSIG_LIVING_REVIVE "mob_revive"
+
+// Sex Controller Signals
+#define COMSIG_SEXCONTROLLER_CLIMAX "sex_controller_climax"
+#define COMSIG_SEXCONTROLLER_AROUSAL_CHANGE "sex_controller_arousal_change"
+
+// Sex Controller Return Values
+#define COMPONENT_CANCEL_CLIMAX (1<<0)
+
+// Emote return values
+#define COMPONENT_EMOTE_MESSAGE_CHANGED (1<<0)
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index f06fcfa615d..b222b64dcff 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -519,3 +519,5 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_SLAVEBOURNE "slavebourne"
#define TRAIT_SLAVEBOURNE_EXAMINE "trait_slavebourne_examine"
+
+#define TRAIT_LOVESTRUCK "lovestruck" // For collar master's force love effect
diff --git a/code/_onclick/hud/fullscreen.dm b/code/_onclick/hud/fullscreen.dm
index 9a2610e05f9..f28ea13960b 100644
--- a/code/_onclick/hud/fullscreen.dm
+++ b/code/_onclick/hud/fullscreen.dm
@@ -263,7 +263,7 @@
blend_mode = BLEND_ADD
show_when_dead = TRUE
-/atom/movable/screen/fullscreen/maniac
+/atom/movable/screen/fullscreen/maniac
icon = 'icons/roguetown/maniac/fullscreen.dmi'
icon_state = "hall0"
alpha = 0
@@ -289,3 +289,8 @@
/atom/movable/screen/fullscreen/dreaming/waking_up
icon_state = "wake_up"
+
+/atom/movable/screen/fullscreen/arousal
+ icon = 'icons/mob/screen_full.dmi'
+ icon_state = "love"
+ layer = FULLSCREEN_LAYER
diff --git a/code/datums/traits/negative.dm b/code/datums/traits/negative.dm
index a3f94c97696..594bdaa6174 100644
--- a/code/datums/traits/negative.dm
+++ b/code/datums/traits/negative.dm
@@ -629,67 +629,31 @@
/datum/quirk/slavebourne
name = "Slavebourne"
- desc = "You have an innate need to be collared and controlled. Without a master, your abilities are diminished."
+ desc = "You want to rid yourself of the pain and harshness of choice. You hid a cursed collar..."
value = -6
mob_trait = TRAIT_SLAVEBOURNE
- gain_text = span_notice("You want to rid yourself of the pain and harshness of choice..")
- lose_text = span_notice("You feel more independent.")
+ gain_text = span_danger("You feel an overwhelming need to be controlled...")
+ lose_text = span_notice("You feel more independent!")
medical_record_text = "Patient exhibits strong submissive tendencies and a psychological need for authority."
- var/debuff_active = FALSE
+ var/debuff_active = TRUE
var/master_dead = FALSE
- var/obj/item/clothing/neck/roguetown/cursed_collar/my_collar
/datum/quirk/slavebourne/add()
- . = ..()
var/mob/living/carbon/human/H = quirk_holder
- if(!H)
+ if(!istype(H))
return
+
+ apply_debuff()
ADD_TRAIT(H, TRAIT_SLAVEBOURNE_EXAMINE, TRAIT_GENERIC)
- ADD_TRAIT(H, TRAIT_SLAVEBOURNE, QUIRK_TRAIT)
RegisterSignal(H, COMSIG_CARBON_GAIN_COLLAR, .proc/on_collared)
RegisterSignal(H, COMSIG_CARBON_LOSE_COLLAR, .proc/on_uncollared)
- // Add a delayed check for collar status
- addtimer(CALLBACK(src, .proc/check_initial_collar_status), 5 SECONDS)
-
-/datum/quirk/slavebourne/proc/check_initial_collar_status()
- var/mob/living/carbon/human/H = quirk_holder
- if(!H)
- return
-
- var/obj/item/clothing/neck/collar = H.get_item_by_slot(SLOT_NECK)
- if(!istype(collar, /obj/item/clothing/neck/roguetown/cursed_collar))
- debuff_active = TRUE
- apply_debuff()
- to_chat(H, span_warning("Without a master to serve, your abilities are diminished..."))
- return
-
- var/obj/item/clothing/neck/roguetown/cursed_collar/cursed = collar
- if(!cursed.collar_master || cursed.collar_master == H)
- debuff_active = TRUE
- apply_debuff()
- to_chat(H, span_warning("Without a master to serve, your abilities are diminished..."))
-
/datum/quirk/slavebourne/on_spawn()
var/mob/living/carbon/human/H = quirk_holder
- if(!H)
- return
-
- my_collar = new /obj/item/clothing/neck/roguetown/cursed_collar(get_turf(H))
- to_chat(H, span_notice("A cursed collar materializes on the ground near you..."))
-
-/datum/quirk/slavebourne/remove()
- var/mob/living/carbon/human/H = quirk_holder
- if(!H)
+ if(!istype(H))
return
-
- REMOVE_TRAIT(H, TRAIT_SLAVEBOURNE_EXAMINE, TRAIT_GENERIC)
- REMOVE_TRAIT(H, TRAIT_SLAVEBOURNE, QUIRK_TRAIT)
- remove_debuff()
- UnregisterSignal(H, COMSIG_CARBON_GAIN_COLLAR)
- UnregisterSignal(H, COMSIG_CARBON_LOSE_COLLAR)
- if(my_collar)
- qdel(my_collar)
+ H.mind.special_items["Cursed Collar"] = /obj/item/clothing/neck/roguetown/cursed_collar
+ to_chat(H, span_notice("You remember where you hid your cursed collar..."))
/datum/quirk/slavebourne/proc/apply_debuff()
var/mob/living/carbon/human/H = quirk_holder
@@ -719,8 +683,10 @@
H.change_stat("endurance", 4)
H.change_stat("fortune", 4)
-/datum/quirk/slavebourne/proc/on_collared(mob/living/carbon/human/source, obj/item/clothing/neck/roguetown/cursed_collar/collar)
+/datum/quirk/slavebourne/proc/on_collared(mob/living/carbon/human/source)
SIGNAL_HANDLER
+ var/obj/item/clothing/neck/roguetown/cursed_collar/collar = source.get_item_by_slot(SLOT_NECK)
+
if(master_dead) // If master died, new collars won't help
to_chat(source, span_warning("The death of your previous master has left you permanently weakened. No new master can restore your abilities..."))
return
@@ -731,12 +697,14 @@
UnregisterSignal(collar.collar_master, COMSIG_LIVING_REVIVE)
// Register new signals
- RegisterSignal(collar.collar_master, COMSIG_LIVING_DEATH, .proc/on_master_death)
- RegisterSignal(collar.collar_master, COMSIG_LIVING_REVIVE, .proc/on_master_revive)
+ RegisterSignal(collar.collar_master, COMSIG_LIVING_DEATH, PROC_REF(on_master_death))
+ RegisterSignal(collar.collar_master, COMSIG_LIVING_REVIVE, PROC_REF(on_master_revive))
- debuff_active = FALSE
- remove_debuff()
- to_chat(source, span_notice("Your master's control strengthens you!"))
+ // Ensure debuff is removed only once
+ if(debuff_active)
+ debuff_active = FALSE
+ remove_debuff()
+ to_chat(source, span_notice("Your master's control strengthens you!"))
/datum/quirk/slavebourne/proc/on_uncollared(mob/living/carbon/human/source)
SIGNAL_HANDLER
@@ -785,5 +753,3 @@
to_chat(H, span_notice("You feel your master's life force return! Your abilities are restored!"))
UnregisterSignal(master, COMSIG_LIVING_REVIVE)
-/datum/quirk/slavebourne/proc/examine(mob/living/carbon/human/H)
- return span_notice("[H.p_they(TRUE)] carries [H.p_them()]self with a submissive demeanor as if seeking direction.")
diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm
index 87a62989b83..321ad99013a 100644
--- a/code/modules/antagonists/_common/antag_datum.dm
+++ b/code/modules/antagonists/_common/antag_datum.dm
@@ -23,7 +23,7 @@ GLOBAL_LIST_EMPTY(antagonists)
var/show_in_antagpanel = TRUE //This will hide adding this antag type in antag panel, use only for internal subtypes that shouldn't be added directly but still show if possessed by mind
var/antagpanel_category = "Uncategorized" //Antagpanel will display these together, REQUIRED
var/show_name_in_check_antagonists = FALSE //Will append antagonist name in admin listings - use for categories that share more than one antag type
-
+
//RT: Whether or not this antag increases your votepwr in the end vote
var/increase_votepwr = TRUE
var/rogue_enabled = FALSE
@@ -279,3 +279,6 @@ GLOBAL_LIST_EMPTY(antagonists)
else
return
..()
+
+/datum/antagonist/proc/get_preview_icon()
+ return null
diff --git a/code/modules/antagonists/collar_master/__DEFINES/signals.dm b/code/modules/antagonists/collar_master/__DEFINES/signals.dm
new file mode 100644
index 00000000000..863271524f6
--- /dev/null
+++ b/code/modules/antagonists/collar_master/__DEFINES/signals.dm
@@ -0,0 +1 @@
+#define COMSIG_LIVING_TOGGLE_LISTEN "living_toggle_listen"
diff --git a/code/modules/antagonists/collar_master/collar_master.dm b/code/modules/antagonists/collar_master/collar_master.dm
index ad8cdc1e9af..167f57769bf 100644
--- a/code/modules/antagonists/collar_master/collar_master.dm
+++ b/code/modules/antagonists/collar_master/collar_master.dm
@@ -1,227 +1,615 @@
+#define COLLAR_TRAIT "collar_master"
+#define EMOTE_MESSAGE "emote_message"
+#define ANTAGONIST_PREVIEW_ICON_SIZE 96
+#define COMSIG_LIVING_SURRENDER "living_surrender"
+#define COLLAR_SURRENDER_TIME 10 SECONDS
+#define COMSIG_MOB_CLICK_SHIFT "mob_click_shift"
+#define COMPONENT_INTERRUPT_CLICK "interrupt_click"
+
+/datum/status_effect/surrender/collar
+ id = "collar_surrender"
+ duration = COLLAR_SURRENDER_TIME
+ alert_type = /atom/movable/screen/alert/status_effect/collar_surrender
+
+/atom/movable/screen/alert/status_effect/collar_surrender
+ name = "Forced Surrender"
+ desc = "Your collar forces you to submit!"
+ icon_state = "surrender"
+
/datum/antagonist/collar_master
name = "Collar Master"
roundend_category = "collar masters"
antagpanel_category = "Collar Master"
- 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."
+ var/list/my_pets = list()
+ var/list/temp_selected_pets = list()
+ var/listening = FALSE
+ var/deny_orgasm = FALSE
+ var/dominating = FALSE
+ var/silenced = FALSE
+ var/scrying = FALSE
+ var/last_command_time = 0
+ var/command_cooldown = 2 SECONDS
+ var/static/list/pet_sounds = list(
+ "*lets out a soft whimper",
+ "*whines quietly",
+ "*makes a needy sound",
+ "*lets out a submissive mewl",
+ "*makes a pathetic noise",
+ "*whimpers needily",
+ "*mewls submissively",
+ "*pants heavily",
+ "*lets out a desperate whine",
+ "*makes a pleading sound"
)
+ var/list/registered_pets = list()
+ var/speech_altered = FALSE
+ var/mob/living/carbon/human/original_pet_body
+ var/mob/living/carbon/human/original_master_body
+
+/datum/antagonist/collar_master/proc/add_pet(mob/living/carbon/human/pet)
+ if(!pet || (pet in my_pets))
+ return FALSE
+
+ // Add to lists
+ my_pets += pet
+ registered_pets += pet
+
+ // Register all signals including attack signals
+ RegisterSignal(pet, COMSIG_MOB_SAY, PROC_REF(on_pet_say))
+ RegisterSignal(pet, COMSIG_MOB_DEATH, PROC_REF(on_pet_death))
+ RegisterSignal(pet, COMSIG_HUMAN_MELEE_UNARMED_ATTACK, PROC_REF(on_pet_attack))
+ RegisterSignal(pet, COMSIG_MOB_ATTACK_HAND, PROC_REF(on_pet_attack))
+ RegisterSignal(pet, COMSIG_ITEM_ATTACK, PROC_REF(on_pet_attack))
+ RegisterSignal(pet, COMSIG_MOVABLE_MOVED, PROC_REF(on_pet_move))
+
+ return TRUE
+
+/datum/antagonist/collar_master/proc/on_pet_say(datum/source, list/speech_args)
+ SIGNAL_HANDLER
+ var/mob/living/carbon/human/pet = source
+ if(!pet || !(pet in my_pets))
+ return
-/datum/antagonist/collar_master/on_gain()
- . = ..()
- if(my_collar)
- RegisterSignal(my_collar.victim, COMSIG_MOB_CLICKON, PROC_REF(check_pet_attack))
- 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,
- )
+ if(speech_altered)
+ var/chosen_sound = pick(pet_sounds)
+ pet.say(chosen_sound) // This will make the pet "say" the full string including *
+ return COMPONENT_CANCEL_SAY
+
+/datum/antagonist/collar_master/proc/do_pet_emote(mob/living/carbon/human/pet)
+ if(!pet || !(pet in my_pets))
+ return
+ pet.emote("me", EMOTE_VISIBLE, pick(pet_sounds))
+
+/datum/antagonist/collar_master/proc/on_pet_death(datum/source)
+ SIGNAL_HANDLER
+ var/mob/living/carbon/human/pet = source
+ if(!pet || !(pet in my_pets))
+ return
+ addtimer(CALLBACK(src, PROC_REF(cleanup_pet), pet), 0.1 SECONDS)
+
+/datum/antagonist/collar_master/proc/remove_pet(mob/living/carbon/human/pet)
+ if(!pet || !(pet in registered_pets))
+ return FALSE
+
+ UnregisterSignal(pet, list(
+ COMSIG_MOB_SAY,
+ COMSIG_MOB_DEATH,
+ COMSIG_HUMAN_MELEE_UNARMED_ATTACK,
+ COMSIG_MOB_ATTACK_HAND,
+ COMSIG_ITEM_ATTACK
+ ))
+
+ registered_pets -= pet
+ cleanup_pet(pet)
+ return TRUE
+
+/datum/antagonist/collar_master/proc/on_pet_move()
+ SIGNAL_HANDLER
+ var/mob/living/carbon/human/pet = usr
+ if(!pet || !(pet in my_pets))
+ return
-/datum/antagonist/collar_master/proc/check_pet_attack(mob/living/carbon/human/pet, atom/target)
+ addtimer(CALLBACK(src, PROC_REF(check_pet_distance), pet), 0.3 SECONDS, TIMER_UNIQUE)
+
+/datum/antagonist/collar_master/proc/check_pet_distance(mob/living/carbon/human/pet)
+ if(!pet || !(pet in my_pets))
+ return
+ if(get_dist(pet, owner?.current) > 7)
+ step_towards(pet, owner.current)
+ if(prob(50))
+ pet.emote("me", EMOTE_VISIBLE, pick(pet_sounds))
+
+/datum/antagonist/collar_master/proc/on_pet_attack(datum/source, atom/target)
SIGNAL_HANDLER
- if(!my_collar || !my_collar.victim || pet != my_collar.victim)
+ var/mob/living/carbon/human/pet = source
+ if(!pet || !(pet in my_pets))
return NONE
- if(target == owner.current && pet.a_intent == INTENT_HARM)
- pet.electrocute_act(25, my_collar, flags = SHOCK_NOGLOVES)
- pet.Paralyze(600)
- to_chat(pet, span_warning("The collar sends painful shocks through your body as you try to attack your master!"))
- playsound(pet, 'sound/blank.ogg', 50, TRUE)
+ if(target == owner?.current)
+ addtimer(CALLBACK(src, PROC_REF(shock_pet), pet, 25), 0.1 SECONDS)
return COMPONENT_CANCEL_ATTACK
+ return NONE
+
+/datum/antagonist/collar_master/proc/shock_pet(mob/living/carbon/human/pet, intensity = 10)
+ if(!pet || !(pet in my_pets))
+ return FALSE
+
+ var/mob/living/carbon/human/master = owner?.current
+ if(!master)
+ return FALSE
+
+ // Calculate damage based on intensity
+ var/damage = intensity * 0.5
+
+ // Apply damage and effects
+ pet.adjustFireLoss(damage)
+ pet.adjustStaminaLoss(intensity * 2)
+ pet.Knockdown(intensity * 0.2 SECONDS)
+ pet.do_jitter_animation(intensity)
+
+ // Visual effects
+ pet.visible_message(span_danger("[pet]'s collar crackles with electricity!"), \
+ span_userdanger("Your collar sends searing pain through your body!"))
+
+ var/turf/T = get_turf(pet)
+ if(T)
+ new /obj/effect/temp_visual/cult/sparks(T)
+ playsound(T, list('sound/items/stunmace_hit (1).ogg','sound/items/stunmace_hit (2).ogg'), 50, TRUE)
+ do_sparks(2, FALSE, pet)
+
+ // Add a temporary overlay effect
+ pet.flash_fullscreen("redflash3")
+ addtimer(CALLBACK(pet, TYPE_PROC_REF(/mob/living, clear_fullscreen), "pain"), 2 SECONDS)
+
+ return TRUE
+
+/datum/antagonist/collar_master/proc/start_domination(mob/living/carbon/human/pet, mob/living/carbon/human/master)
+ if(!pet || !(pet in my_pets) || !master || dominating)
+ return FALSE
+
+ // Store original bodies for reference
+ master.name_archive = master.real_name
+ pet.name_archive = pet.real_name
+ original_pet_body = pet
+ original_master_body = master
+
+ // Swap minds
+ var/datum/mind/master_mind = master.mind
+ var/datum/mind/pet_mind = pet.mind
+
+ master_mind.transfer_to(pet)
+ pet_mind.transfer_to(master)
+
+ dominating = TRUE
+
+ // Visual effects
+ pet.visible_message(span_danger("[master] stares intently at [pet], [master.p_their()] eyes glowing with an otherworldly light!"))
+ to_chat(pet, span_userdanger("You feel your control slipping as [master] dominates your mind!"))
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+
+ // Set timer to end domination
+ addtimer(CALLBACK(src, PROC_REF(end_domination)), 30 SECONDS)
+ return TRUE
+
+/datum/antagonist/collar_master/proc/end_domination()
+ if(!dominating || !original_pet_body || !original_master_body)
+ return FALSE
+
+ // Get the current minds in the swapped bodies
+ var/datum/mind/mind_in_pet_body = original_pet_body.mind
+ var/datum/mind/mind_in_master_body = original_master_body.mind
+
+ if(!mind_in_pet_body || !mind_in_master_body)
+ dominating = FALSE
+ return FALSE
+
+ // Transfer minds back to their original bodies
+ mind_in_pet_body.transfer_to(original_master_body, force_key_move = TRUE)
+ mind_in_master_body.transfer_to(original_pet_body, force_key_move = TRUE)
+
+ // Restore original names
+ original_master_body.real_name = original_master_body.name_archive
+ original_pet_body.real_name = original_pet_body.name_archive
+ original_master_body.name_archive = null
+ original_pet_body.name_archive = null
+
+ // Clear references
+ original_master_body = null
+ original_pet_body = null
+ dominating = FALSE
+
+ // Visual feedback
+ to_chat(mind_in_master_body.current, span_notice("You return to your body, releasing control of your pet."))
+ to_chat(mind_in_pet_body.current, span_notice("Your mind returns to your body as the domination ends!"))
+ playsound(mind_in_pet_body.current, 'sound/misc/vampirespell.ogg', 50, TRUE)
+
+ return TRUE
+
+/datum/antagonist/collar_master/proc/select_pets(mob/user, action_name = "", allow_multiple = FALSE)
+ var/list/valid_pets = list()
+ for(var/mob/living/carbon/human/pet in my_pets)
+ if(!pet || !pet.mind || !pet.client)
+ continue
+ valid_pets += pet
+
+ if(!length(valid_pets))
+ return list()
+
+ if(allow_multiple)
+ var/list/selected = input(user, "Choose pets to [action_name]:", "Pet Selection") as null|anything in valid_pets
+ return selected ? selected : list()
+ else
+ var/mob/living/carbon/human/selected = input(user, "Choose a pet to [action_name]:", "Pet Selection") as null|anything in valid_pets
+ return selected ? list(selected) : list()
+
+/datum/antagonist/collar_master/proc/toggle_listening(mob/living/carbon/human/pet)
+ if(!pet || !pet.mind || !pet.client || !(pet in my_pets))
+ return FALSE
+
+ listening = !listening
+ if(listening)
+ RegisterSignal(pet, COMSIG_MOB_SAY, PROC_REF(on_pet_say))
+ RegisterSignal(pet, COMSIG_MOB_EMOTE, PROC_REF(on_pet_emote))
+ RegisterSignal(pet, COMSIG_MOVABLE_HEAR, PROC_REF(on_pet_hear))
+ to_chat(pet, span_warning("Your collar begins monitoring everything you hear and say!"))
+ to_chat(owner.current, span_notice("You begin monitoring [pet]'s senses."))
+ else
+ UnregisterSignal(pet, list(
+ COMSIG_MOB_SAY,
+ COMSIG_MOB_EMOTE,
+ COMSIG_MOVABLE_HEAR
+ ))
+ to_chat(pet, span_notice("Your collar stops monitoring you."))
+ to_chat(owner.current, span_notice("You stop monitoring [pet]."))
+ return TRUE
+
+/datum/antagonist/collar_master/proc/on_pet_emote(datum/source, datum/emote/emote, mob/user, intentional)
+ SIGNAL_HANDLER
+ var/mob/living/carbon/human/pet = source
+ if(!pet || !(pet in my_pets) || !listening || !owner?.current)
+ return
-/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,
- )
- . = ..()
+ to_chat(owner.current, span_notice("[pet] [emote.message]"))
+
+/datum/antagonist/collar_master/proc/on_pet_hear(datum/source, list/hearing_args)
+ SIGNAL_HANDLER
+ var/mob/living/carbon/human/pet = source
+ if(!pet || !(pet in my_pets) || !listening || !owner?.current)
+ return
-/mob/proc/collar_control_menu()
- set name = "Collar Control"
- set category = "Collar"
+ var/message = hearing_args["message"]
+ var/speaker = hearing_args["speaker"]
+ var/datum/language/speaking_language = hearing_args["language"]
- var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
- if(!CM || !CM.my_collar || !CM.my_collar.victim)
+ // Don't relay what the master says to avoid feedback loops
+ if(speaker == owner.current)
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
+ to_chat(owner.current, span_notice("Through [pet]'s collar, you hear: \"[message]\""))
- if(!length(pets))
- return null
+/datum/antagonist/collar_master/proc/force_strip(mob/living/carbon/human/pet)
+ if(!pet || !(pet in my_pets))
+ return FALSE
- var/choice = input(src, "Choose a pet:", "Pet Selection") as null|anything in pets
- if(!choice)
- return null
- return pets[choice]
+ pet.drop_all_held_items()
+ // Additional stripping logic can be added here
+ return TRUE
-/mob/proc/collar_scry()
- set name = "Scry on Pet"
- set category = "Collar"
+/datum/antagonist/collar_master/proc/toggle_hallucinations(mob/living/carbon/human/pet)
+ if(!pet || !(pet in my_pets))
+ return FALSE
- var/obj/item/clothing/neck/roguetown/cursed_collar/collar = select_pet("scry")
- if(!collar)
- return
+ if(pet.has_trauma_type(/datum/brain_trauma/mild/hallucinations))
+ pet.cure_trauma_type(/datum/brain_trauma/mild/hallucinations, TRAUMA_RESILIENCE_BASIC)
+ to_chat(pet, span_notice("Your collar pulses and the world becomes clearer."))
+ else
+ pet.gain_trauma(/datum/brain_trauma/mild/hallucinations, TRAUMA_RESILIENCE_BASIC)
+ to_chat(pet, span_warning("Your collar pulses and the world begins to shift and warp!"))
+ pet.do_jitter_animation(20)
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+ return TRUE
- 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)
+/datum/antagonist/collar_master/proc/create_illusion(mob/living/carbon/human/pet, message)
+ if(!pet || !(pet in my_pets))
+ return FALSE
-/mob/proc/collar_listen()
- set name = "Listen to Pet"
- set category = "Collar"
+ to_chat(pet, span_warning("Collar Illusion: [message]"))
+ pet.do_jitter_animation(20)
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+ return TRUE
- var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
- if(!CM || !CM.my_collar || !CM.my_collar.victim)
- return
+/datum/antagonist/collar_master/proc/force_emote(mob/living/carbon/human/pet, emote_text)
+ if(!pet || !(pet in my_pets))
+ return FALSE
- 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."))
+ pet.emote("me", EMOTE_VISIBLE, emote_text)
+ return TRUE
-/mob/proc/collar_shock()
- set name = "Shock Pet"
- set category = "Collar"
+/datum/antagonist/collar_master/proc/share_damage(mob/living/carbon/human/pet, mob/living/carbon/human/master)
+ if(!pet || !(pet in my_pets) || !master)
+ return FALSE
- 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/total_damage = master.getBruteLoss() + master.getFireLoss() + master.getOxyLoss()
+ if(total_damage <= 0)
+ return FALSE
- to_chat(src, span_warning("You cruelly shock your disobedient pet into submission."))
- 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)
+ var/damage_share = total_damage * 0.5
+ pet.adjustBruteLoss(damage_share)
+ master.adjustBruteLoss(-damage_share)
-/mob/proc/collar_message()
- set name = "Send Message"
- set category = "Collar"
+ // Share blood if applicable
+ if(master.blood_volume && pet.blood_volume)
+ var/blood_diff = BLOOD_VOLUME_NORMAL - master.blood_volume
+ if(blood_diff > 0)
+ var/blood_share = min(blood_diff * 0.5, pet.blood_volume - BLOOD_VOLUME_SAFE)
+ if(blood_share > 0)
+ pet.blood_volume -= blood_share
+ master.blood_volume += blood_share
- var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
- if(!CM || !CM.my_collar || !CM.my_collar.victim)
- return
+ return TRUE
- var/message = input("What message do you want to send to your pet?", "Collar Message") as text|null
- if(!message)
- return
+/datum/antagonist/collar_master/proc/force_surrender(mob/living/carbon/human/pet)
+ if(!pet || !(pet in my_pets))
+ return FALSE
- to_chat(CM.my_collar.victim, span_warning("Your collar tingles as you hear your master's voice: [message]"))
- to_chat(src, span_notice("You send a message to your pet: \"[message]\""))
- playsound(CM.my_collar.victim, 'sound/blank.ogg', 50, TRUE)
+ if(pet.stat >= UNCONSCIOUS)
+ return FALSE
-/mob/proc/collar_force_surrender()
- set name = "Force Surrender"
- set category = "Collar"
+ if(pet.surrendering)
+ return FALSE
- var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
- if(!CM || !CM.my_collar || !CM.my_collar.victim)
- return
+ pet.surrendering = TRUE
+ pet.toggle_cmode()
+ pet.changeNext_move(CLICK_CD_EXHAUSTED)
- to_chat(src, span_warning("You force your pet to their knees, reminding them of their place."))
- to_chat(CM.my_collar.victim, span_userdanger("The collar forces you to your knees!"))
- CM.my_collar.victim.Paralyze(600)
- playsound(CM.my_collar.victim, 'sound/blank.ogg', 50, TRUE)
+ // Create and attach the surrender flag visual
+ var/obj/effect/temp_visual/surrender/flaggy = new(pet)
+ pet.vis_contents += flaggy
-/mob/proc/collar_force_naked()
- set name = "Force Strip"
- set category = "Collar"
+ // Apply stun and status effects
+ pet.Stun(300)
+ pet.Knockdown(300)
+ pet.apply_status_effect(/datum/status_effect/debuff/breedable)
+ pet.apply_status_effect(/datum/status_effect/debuff/submissive)
- var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
- if(!CM || !CM.my_collar || !CM.my_collar.victim)
- return
+ // Visual and sound effects
+ pet.visible_message(span_warning("[pet] is forced to surrender by their collar!"), \
+ span_userdanger("Your collar forces you to submit!"))
+ playsound(pet, 'sound/misc/surrender.ogg', 100, FALSE, -1, ignore_walls=TRUE)
- to_chat(src, span_warning("You command your pet to strip, leaving them vulnerable and exposed."))
- to_chat(CM.my_collar.victim, span_userdanger("The collar's magic forces you to remove all your clothing!"))
- var/mob/living/victim = CM.my_collar.victim
- 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
+ pet.update_vision_cone()
+ addtimer(CALLBACK(pet, TYPE_PROC_REF(/mob/living, end_submit)), 600)
+
+ return TRUE
+
+/datum/antagonist/collar_master/proc/toggle_arousal(mob/living/carbon/human/pet, amount)
+ if(!pet || !(pet in my_pets))
+ return FALSE
+
+ // Initialize sex_controller if needed
+ if(!pet.sexcon)
+ pet.sexcon = new /datum/sex_controller(pet)
+
+ // Apply arousal through sexcon system
+ pet.sexcon.adjust_arousal_manual(amount)
- 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)
+ // Visual feedback
+ to_chat(pet, span_userdanger("Your collar sends waves of arousal through your body!"))
+ pet.do_jitter_animation(20)
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
-/mob/proc/collar_toggle_silence()
- set name = "Toggle Pet Speech"
- set category = "Collar"
+ return TRUE
- var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
- if(!CM || !CM.my_collar || !CM.my_collar.victim)
+/datum/antagonist/collar_master/proc/check_arousal_effects(mob/living/carbon/human/pet)
+ if(!pet?.sexcon)
return
- CM.my_collar.silenced = !CM.my_collar.silenced
- if(CM.my_collar.silenced)
- to_chat(src, span_warning("You silence your pet, reducing them to animal noises only."))
- else
- to_chat(src, span_warning("You allow your pet to speak again, for now."))
- 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)
+ var/arousal = pet.sexcon.arousal
- if(CM.my_collar.silenced)
- RegisterSignal(CM.my_collar.victim, COMSIG_MOB_SAY, PROC_REF(handle_silenced_speech))
+ if(arousal >= 80)
+ pet.overlay_fullscreen("arousal", /atom/movable/screen/fullscreen/arousal, 3)
+ else if(arousal >= 50)
+ pet.overlay_fullscreen("arousal", /atom/movable/screen/fullscreen/arousal, 2)
+ else if(arousal >= 20)
+ pet.overlay_fullscreen("arousal", /atom/movable/screen/fullscreen/arousal, 1)
else
- UnregisterSignal(CM.my_collar.victim, COMSIG_MOB_SAY)
+ pet.clear_fullscreen("arousal")
-/mob/proc/handle_silenced_speech(datum/source, list/speech_args)
- SIGNAL_HANDLER
+/datum/antagonist/collar_master/proc/force_arousal(mob/living/carbon/human/pet, amount)
+ if(!pet || !(pet in my_pets))
+ return FALSE
- var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
- if(!CM || !CM.my_collar || !CM.my_collar.silenced)
- return
+ // Initialize sex_controller if needed
+ if(!pet.sexcon)
+ pet.sexcon = new /datum/sex_controller(pet)
- speech_args[SPEECH_MESSAGE] = ""
- emote("me", EMOTE_VISIBLE, pick(CM.animal_sounds))
- return TRUE // Just return TRUE to block speech
+ // Apply arousal through sexcon system
+ pet.sexcon.adjust_arousal_manual(amount)
-/mob/proc/collar_force_emote()
- set name = "Force Emote"
- set category = "Collar"
+ // Visual feedback
+ to_chat(pet, span_userdanger("Your collar sends waves of arousal through your body!"))
+ pet.do_jitter_animation(20)
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
- var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
- if(!CM || !CM.my_collar || !CM.my_collar.victim)
- return
+ // Check arousal effects after 5 seconds
+ addtimer(CALLBACK(src, PROC_REF(check_arousal_effects), pet), 5 SECONDS)
+
+ return TRUE
+
+/datum/antagonist/collar_master/proc/force_love(mob/living/carbon/human/pet)
+ if(!pet || !(pet in my_pets))
+ return FALSE
+
+ // Apply love effects
+ pet.emote("blush")
+ to_chat(pet, span_love("Your collar fills you with overwhelming affection!"))
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+ return TRUE
+
+/datum/antagonist/collar_master/proc/permit_clothing(mob/living/carbon/human/pet, permitted = TRUE)
+ if(!pet || !(pet in my_pets))
+ return FALSE
- var/emote = input(src, "What emote should your pet perform?", "Force Emote") as text|null
- if(!emote)
+ if(permitted)
+ REMOVE_TRAIT(pet, TRAIT_NUDIST, COLLAR_TRAIT)
+ to_chat(pet, span_notice("Your collar allows you to wear clothing again."))
+ else
+ ADD_TRAIT(pet, TRAIT_NUDIST, COLLAR_TRAIT)
+ to_chat(pet, span_warning("Your collar prevents you from wearing clothing!"))
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+ return TRUE
+
+/datum/antagonist/collar_master/proc/check_pet_status(mob/living/carbon/human/pet)
+ if(!pet || !(pet in my_pets))
+ return FALSE
+
+ var/status_text = "[pet.real_name] Status:\n"
+ status_text += "Health: [pet.health]/[pet.maxHealth]\n"
+ status_text += "Location: [get_area(pet)]\n"
+ status_text += "Mental State: [pet.stat >= UNCONSCIOUS ? "Unconscious" : "Conscious"]\n"
+ status_text += "Active Traits: "
+
+ var/list/active_traits = list()
+ if(speech_altered)
+ active_traits += "Speech Altered"
+
+ status_text += active_traits.len ? english_list(active_traits) : "None"
+ status_text += ""
+
+ return status_text
+
+/datum/antagonist/collar_master/proc/mass_command(command_type, list/targets, ...)
+ if(!length(targets))
+ return FALSE
+
+ var/success_count = 0
+ for(var/mob/living/carbon/human/pet in targets)
+ if(!pet || !(pet in my_pets))
+ continue
+
+ switch(command_type)
+ if("shock")
+ var/intensity = args[1]
+ if(shock_pet(pet, intensity))
+ success_count++
+ if("surrender")
+ if(force_surrender(pet))
+ success_count++
+ if("strip")
+ if(force_strip(pet))
+ success_count++
+ if("arousal")
+ if(toggle_arousal(pet))
+ success_count++
+ if("love")
+ if(force_love(pet))
+ success_count++
+ if("hallucinate")
+ if(toggle_hallucinations(pet))
+ success_count++
+
+ return success_count
+
+/datum/antagonist/collar_master/proc/on_pet_examine(mob/living/carbon/human/pet, mob/user)
+ if(!pet || !(pet in my_pets))
return
- to_chat(src, span_warning("You force your pet to [emote]."))
- CM.my_collar.victim.say(emote, forced = TRUE)
- playsound(CM.my_collar.victim, 'sound/blank.ogg', 50, TRUE)
+ if(user == owner?.current)
+ to_chat(user, span_notice("\n[check_pet_status(pet)]"))
+ else if(user != pet)
+ to_chat(user, span_warning("\nThey wear a strange collar around their neck."))
+
+/datum/antagonist/collar_master/proc/cleanup_pet(mob/living/carbon/human/pet)
+ if(!pet || !(pet in my_pets))
+ return FALSE
+
+ // Remove all collar-related traits
+ REMOVE_TRAIT(pet, TRAIT_NUDIST, COLLAR_TRAIT)
+
+ // Remove from lists
+ my_pets -= pet
+ registered_pets -= pet
+
+ // Handle collar removal
+ var/obj/item/clothing/neck/roguetown/cursed_collar/collar = pet.get_item_by_slot(SLOT_NECK)
+ if(istype(collar))
+ pet.dropItemToGround(collar, force = TRUE)
+ REMOVE_TRAIT(collar, TRAIT_NODROP, CURSED_ITEM_TRAIT)
+
+ // Let the slavebourne trait handle its own debuff
+ // Don't apply debuff here since on_uncollared will handle it
+
+ // Feedback
+ to_chat(pet, span_notice("Your mind clears as the collar's control fades!"))
+ if(owner?.current)
+ to_chat(owner.current, span_warning("[pet] is no longer under your control!"))
+
+ return TRUE
+
+/datum/antagonist/collar_master/on_gain()
+ . = ..()
+ if(owner?.current)
+ owner.current.verbs += list(
+ /mob/proc/collar_master_control_menu,
+ /mob/proc/collar_master_help
+ )
+
+/datum/antagonist/collar_master/on_removal()
+ if(owner?.current)
+ owner.current.verbs -= list(
+ /mob/proc/collar_master_control_menu,
+ /mob/proc/collar_master_help
+ )
+ . = ..()
+
+/datum/antagonist/collar_master/proc/pass_wounds(mob/living/carbon/human/pet)
+ if(!pet || !(pet in my_pets))
+ return FALSE
+
+ var/mob/living/carbon/human/master = owner?.current
+ if(!master)
+ return FALSE
+
+ // Pass all damage types
+ pet.adjustBruteLoss(master.getBruteLoss() * 0.5)
+ pet.adjustFireLoss(master.getFireLoss() * 0.5)
+ pet.adjustOxyLoss(master.getOxyLoss() * 0.5)
+
+ // Pass blood level if it exists
+ if(pet.blood_volume && master.blood_volume)
+ pet.blood_volume = max(BLOOD_VOLUME_SAFE, pet.blood_volume - (BLOOD_VOLUME_NORMAL - master.blood_volume) * 0.5)
+
+ // Pass organ damage
+ for(var/obj/item/organ/organ in master.internal_organs)
+ var/obj/item/organ/matching_organ = pet.getorganslot(organ.slot)
+ if(matching_organ && organ.damage > 0)
+ matching_organ.applyOrganDamage(organ.damage * 0.5)
+
+ pet.updatehealth()
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+ to_chat(pet, span_userdanger("Your collar burns as your master's suffering flows into you!"))
+ pet.visible_message(span_warning("[pet] shudders as [master]'s wounds manifest on their body!"))
+ pet.do_jitter_animation(20)
+
+ // Heal the master slightly
+ master.adjustBruteLoss(-10)
+ master.adjustFireLoss(-10)
+ master.adjustOxyLoss(-10)
+
+ return TRUE
+
+/datum/antagonist/collar_master/proc/toggle_speech(mob/living/carbon/human/pet)
+ if(!pet || !(pet in my_pets))
+ return FALSE
+
+ speech_altered = !speech_altered
+ if(speech_altered)
+ to_chat(pet, span_warning("Your collar tingles, altering how you communicate!"))
+ pet.visible_message(span_warning("[pet]'s collar glows brightly as their speech is altered!"))
+ pet.emote("me", EMOTE_VISIBLE, pick(pet_sounds))
+ else
+ to_chat(pet, span_notice("Your collar allows you to speak normally again."))
+ pet.visible_message(span_notice("[pet]'s collar dims as their voice is restored."))
+
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+ return TRUE
diff --git a/code/modules/antagonists/collar_master/collar_master_verbs.dm b/code/modules/antagonists/collar_master/collar_master_verbs.dm
new file mode 100644
index 00000000000..f5bd7418351
--- /dev/null
+++ b/code/modules/antagonists/collar_master/collar_master_verbs.dm
@@ -0,0 +1,759 @@
+/mob/proc/collar_master_control_menu()
+ set name = "Collar Control"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM)
+ return
+
+ var/list/valid_pets = list()
+ for(var/mob/living/carbon/human/pet in CM.my_pets)
+ if(!pet || !pet.mind || !pet.client)
+ continue
+ valid_pets[pet.real_name] = pet
+
+ if(!length(valid_pets))
+ to_chat(src, span_warning("No valid pets available!"))
+ return
+
+ var/list/selected = input(src, "Select pets to command:", "Pet Selection") as null|anything in valid_pets
+ if(!selected || !CM)
+ return
+
+ CM.temp_selected_pets = list(valid_pets[selected])
+
+ var/list/options = list(
+ "Select pets" = /mob/proc/collar_master_select_pets,
+ "Scry on Pets" = /mob/proc/collar_master_scry,
+ "Listen to Pets" = /mob/proc/collar_master_listen,
+ "Shock Pets" = /mob/proc/collar_master_shock,
+ "Send Message" = /mob/proc/collar_master_send_message,
+ "Force Surrender" = /mob/proc/collar_master_force_surrender,
+ "Force Strip" = /mob/proc/collar_master_force_strip,
+ "Permit Clothing" = /mob/proc/collar_master_permit_clothing,
+ "Toggle Pet Speech" = /mob/proc/collar_master_toggle_speech,
+ "Force Action" = /mob/proc/collar_master_force_action,
+ "Pass Wounds" = /mob/proc/collar_master_pass_wounds,
+ "Dominate Pet" = /mob/proc/collar_master_dominate,
+ "Force Love" = /mob/proc/collar_master_force_love,
+ "Force Arousal" = /mob/proc/collar_master_force_arousal,
+ "Toggle Orgasm Denial" = /mob/proc/collar_master_toggle_denial,
+ "Toggle Pet Hallucinations" = /mob/proc/collar_master_toggle_hallucinate,
+ "Impose Will" = /mob/proc/collar_master_illusion,
+ "Free Pet" = /mob/proc/collar_master_release_pet,
+ )
+
+ var/choice = input(src, "Choose a command:", "Collar Control") as null|anything in options
+ if(!choice || !CM || !length(CM.temp_selected_pets))
+ return
+
+ var/proc_path = options[choice]
+ call(src, proc_path)()
+
+/mob/proc/collar_master_scry()
+ set name = "Scry Pet"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar's scrying crystal is still recharging!"))
+ return
+
+ var/mob/living/carbon/human/pet = CM.temp_selected_pets[1] // Use first selected pet
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ to_chat(src, span_warning("Invalid pet selected!"))
+ return
+
+ if(pet.stat >= UNCONSCIOUS)
+ to_chat(src, span_warning("[pet] must be conscious to establish a scrying link!"))
+ return
+
+ to_chat(src, span_notice("You establish a scrying link through [pet]'s collar..."))
+ to_chat(pet, span_warning("Your collar tingles as your master peers through your eyes!"))
+
+ reset_perspective(pet)
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+ addtimer(CALLBACK(src, PROC_REF(end_scrying)), 30 SECONDS)
+
+ CM.last_command_time = world.time
+
+/mob/proc/end_scrying()
+ reset_perspective()
+ to_chat(src, span_notice("The vision fades..."))
+
+/mob/proc/collar_master_listen()
+ set name = "Listen to Pets"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar is still cooling down!"))
+ return
+
+ CM.last_command_time = world.time
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+ CM.toggle_listening(pet)
+
+ to_chat(src, span_notice("You [CM.listening ? "start" : "stop"] listening to [length(CM.temp_selected_pets)] pets."))
+
+/mob/proc/collar_master_shock()
+ set name = "Shock Pet"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar's power cell is still recharging!"))
+ return
+
+ var/intensity = 15 // Fixed intensity like the scepter
+ CM.last_command_time = world.time
+ var/shocked_count = 0
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+
+ if(pet.stat >= UNCONSCIOUS)
+ to_chat(src, span_warning("[pet] must be conscious to be disciplined!"))
+ continue
+
+ if(CM.shock_pet(pet, intensity))
+ shocked_count++
+
+ if(shocked_count > 0)
+ to_chat(src, span_notice("You discipline [shocked_count > 1 ? "[shocked_count] pets" : "your pet"] with a shock."))
+ else
+ to_chat(src, span_warning("Failed to discipline any pets!"))
+
+/mob/proc/collar_master_send_message()
+ set name = "Send Message"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar's neural link is still recharging!"))
+ return
+
+ var/message = input(src, "What message should echo in your pet's mind?", "Mental Command") as text|null
+ if(!message)
+ return
+
+ CM.last_command_time = world.time
+ var/message_count = 0
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+
+ to_chat(pet, span_userdanger("Your collar resonates with your master's voice: [message]"))
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+ pet.do_jitter_animation(15)
+ message_count++
+
+ if(message_count > 0)
+ to_chat(src, span_notice("You project your will into [message_count > 1 ? "[message_count] pets" : "your pet's"] mind."))
+ else
+ to_chat(src, span_warning("Failed to reach any pets!"))
+
+/mob/proc/collar_master_force_surrender()
+ set name = "Force Surrender"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar is still cooling down!"))
+ return
+
+ CM.last_command_time = world.time
+ var/surrendered_count = 0
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+
+ if(pet.stat >= UNCONSCIOUS)
+ to_chat(src, span_warning("[pet] must be conscious to force surrender!"))
+ continue
+
+ if(CM.force_surrender(pet))
+ surrendered_count++
+
+ to_chat(src, span_notice("Forced [surrendered_count] pets to surrender."))
+
+/mob/proc/collar_master_force_strip()
+ set name = "Force Strip"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar's command circuits are still cooling down!"))
+ return
+
+ CM.last_command_time = world.time
+ var/stripped_count = 0
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+
+ // Drop held items
+ pet.drop_all_held_items()
+
+ // Remove all clothing except collar
+ for(var/obj/item/I in pet.get_equipped_items())
+ if(!(I.slot_flags & ITEM_SLOT_NECK)) // Don't remove collar
+ pet.dropItemToGround(I, TRUE)
+
+ to_chat(pet, span_userdanger("Your collar tingles as it forces you to remove your clothing!"))
+ pet.visible_message(span_warning("[pet]'s collar pulses with light as they frantically strip their clothing!"))
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+ stripped_count++
+
+ if(stripped_count > 0)
+ to_chat(src, span_notice("You command [stripped_count > 1 ? "[stripped_count] pets" : "your pet"] to strip."))
+ else
+ to_chat(src, span_warning("Failed to make any pets strip!"))
+
+/mob/proc/collar_master_permit_clothing()
+ set name = "Permit Clothing"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar's behavioral circuits need time to recalibrate!"))
+ return
+
+ CM.last_command_time = world.time
+ var/permitted_count = 0
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+
+ REMOVE_TRAIT(pet, TRAIT_NUDIST, COLLAR_TRAIT)
+ to_chat(pet, span_notice("Your collar hums softly as your master grants you permission to wear clothing."))
+ pet.visible_message(span_notice("[pet]'s collar glows briefly as they are permitted to dress."))
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+ permitted_count++
+
+ if(permitted_count > 0)
+ to_chat(src, span_notice("You grant [permitted_count > 1 ? "[permitted_count] pets" : "your pet"] permission to wear clothing."))
+ else
+ to_chat(src, span_warning("Failed to permit any pets to wear clothing!"))
+
+/mob/proc/collar_master_toggle_speech()
+ set name = "Toggle Speech"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar's vocal inhibitors need time to cycle!"))
+ return
+
+ CM.last_command_time = world.time
+ var/toggled_count = 0
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+
+ if(CM.toggle_speech(pet))
+ toggled_count++
+
+ to_chat(src, span_notice("Toggled speech for [toggled_count] pets."))
+
+/mob/proc/collar_master_force_action()
+ set name = "Force Action"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ var/message = input(src, "What action should your pets perform?", "Command Performance") as text|null
+ if(!message || !CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar's control matrix is still recharging!"))
+ return
+
+ CM.last_command_time = world.time
+ var/action_count = 0
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+
+ to_chat(pet, span_userdanger("Your collar compels you to perform an action!"))
+ pet.visible_message(span_warning("[pet]'s collar pulses as they are forced to act!"))
+ pet.say(message) // The game will automatically handle * for emotes
+ pet.do_jitter_animation(15)
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+ action_count++
+
+ if(action_count > 0)
+ to_chat(src, span_notice("You compel [action_count > 1 ? "[action_count] pets" : "your pet"] to perform your commanded action."))
+ else
+ to_chat(src, span_warning("Failed to make any pets perform the action!"))
+
+/mob/proc/collar_master_pass_wounds()
+ set name = "Pass Wounds"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(!ishuman(src))
+ to_chat(src, span_warning("You must be human to pass wounds!"))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar is still cooling down!"))
+ return
+
+ var/mob/living/carbon/human/master = src
+ CM.last_command_time = world.time
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+
+ // Pass all damage types
+ pet.adjustBruteLoss(master.getBruteLoss() * 0.5)
+ pet.adjustFireLoss(master.getFireLoss() * 0.5)
+ pet.adjustOxyLoss(master.getOxyLoss() * 0.5)
+
+ // Pass blood level if it exists
+ if(pet.blood_volume && master.blood_volume)
+ pet.blood_volume = max(BLOOD_VOLUME_SAFE, pet.blood_volume - (BLOOD_VOLUME_NORMAL - master.blood_volume) * 0.5)
+
+ // Pass organ damage
+ for(var/obj/item/organ/organ in master.internal_organs)
+ var/obj/item/organ/matching_organ = pet.getorganslot(organ.slot)
+ if(matching_organ && organ.damage > 0)
+ matching_organ.applyOrganDamage(organ.damage * 0.5)
+
+ pet.updatehealth()
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+ to_chat(pet, span_userdanger("Your collar burns as your master's suffering flows into you!"))
+
+ // Heal the master slightly
+ master.adjustBruteLoss(-10)
+ master.adjustFireLoss(-10)
+ master.adjustOxyLoss(-10)
+
+ to_chat(src, span_notice("You pass your wounds and suffering to [length(CM.temp_selected_pets)] pets."))
+
+/mob/proc/collar_master_dominate()
+ set name = "Dominate Pet"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar is still cooling down!"))
+ return
+
+ CM.last_command_time = world.time
+ var/mob/living/carbon/human/pet = CM.temp_selected_pets[1]
+
+ if(pet.stat >= UNCONSCIOUS)
+ to_chat(src, span_warning("Your pet must be conscious!"))
+ return
+
+ if(CM.dominating)
+ to_chat(src, span_warning("You are already dominating a pet!"))
+ return
+
+ CM.start_domination(pet, src)
+
+/mob/proc/collar_master_force_love()
+ set name = "Force Love"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar is still cooling down!"))
+ return
+
+ CM.last_command_time = world.time
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+
+ // Toggle love status
+ if(pet.has_status_effect(/datum/status_effect/in_love))
+ pet.remove_status_effect(/datum/status_effect/in_love)
+ REMOVE_TRAIT(pet, TRAIT_LOVESTRUCK, COLLAR_TRAIT)
+ to_chat(pet, span_notice("The overwhelming attraction fades away..."))
+ else
+ pet.apply_status_effect(/datum/status_effect/in_love, src)
+ ADD_TRAIT(pet, TRAIT_LOVESTRUCK, COLLAR_TRAIT)
+ to_chat(pet, span_love("You feel an overwhelming attraction to [src]!"))
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+
+ to_chat(src, span_notice("You toggle love status for [length(CM.temp_selected_pets)] pets."))
+
+/mob/proc/collar_master_force_arousal()
+ set name = "Force Arousal"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar is still cooling down!"))
+ return
+
+ var/arousal_amount = input(src, "Set arousal level (0-100):", "Force Arousal", 50) as num|null
+ if(arousal_amount == null || !CM || !length(CM.temp_selected_pets))
+ return
+
+ arousal_amount = clamp(arousal_amount, 0, 100)
+ CM.last_command_time = world.time
+
+ var/affected_pets = 0
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+
+ affected_pets++
+
+ // Initialize sex_controller if needed
+ if(!pet.sexcon)
+ pet.sexcon = new /datum/sex_controller(pet)
+
+ // Apply arousal through sexcon system
+ pet.sexcon.adjust_arousal_manual(arousal_amount)
+
+ // Visual feedback
+ to_chat(pet, span_userdanger("Your collar sends waves of arousal through your body!"))
+ pet.do_jitter_animation(20)
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+
+ // Add screen effects based on arousal level
+ if(arousal_amount >= 80)
+ pet.overlay_fullscreen("arousal", /atom/movable/screen/fullscreen/arousal, 3)
+ else if(arousal_amount >= 50)
+ pet.overlay_fullscreen("arousal", /atom/movable/screen/fullscreen/arousal, 2)
+ else if(arousal_amount >= 20)
+ pet.overlay_fullscreen("arousal", /atom/movable/screen/fullscreen/arousal, 1)
+ else
+ pet.clear_fullscreen("arousal")
+
+ to_chat(src, span_notice("You force arousal upon [affected_pets] pets."))
+
+/mob/proc/collar_master_toggle_denial()
+ set name = "Toggle Orgasm Denial"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar is still cooling down!"))
+ return
+
+ CM.last_command_time = world.time
+ CM.deny_orgasm = !CM.deny_orgasm
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+
+ if(CM.deny_orgasm)
+ to_chat(pet, span_warning("Your collar tightens - you feel like you won't be able to finish!"))
+ else
+ to_chat(pet, span_notice("Your collar loosens - you feel like you can finish again!"))
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+
+ to_chat(src, span_notice("You [CM.deny_orgasm ? "prevent" : "allow"] [length(CM.temp_selected_pets)] pets from reaching climax."))
+
+/mob/proc/collar_master_toggle_hallucinate()
+ set name = "Toggle Pet Hallucinations"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar is still cooling down!"))
+ return
+
+ CM.last_command_time = world.time
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+
+ if(pet.has_trauma_type(/datum/brain_trauma/mild/hallucinations))
+ pet.cure_trauma_type(/datum/brain_trauma/mild/hallucinations, TRAUMA_RESILIENCE_BASIC)
+ to_chat(pet, span_notice("Your collar pulses and the world becomes clearer."))
+ else
+ pet.gain_trauma(/datum/brain_trauma/mild/hallucinations, TRAUMA_RESILIENCE_BASIC)
+ to_chat(pet, span_warning("Your collar pulses and the world begins to shift and warp!"))
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+
+ to_chat(src, span_notice("You toggle hallucinations for [length(CM.temp_selected_pets)] pets."))
+
+/mob/proc/collar_master_illusion()
+ set name = "Create Illusion"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ var/message = input(src, "What illusion should your pets see?", "Create Illusion") as message|null
+ if(!message || !CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar is still cooling down!"))
+ return
+
+ CM.last_command_time = world.time
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+
+ // Send message directly to pet's chat
+ to_chat(pet, message)
+ playsound(pet, 'sound/misc/vampirespell.ogg', 50, TRUE)
+
+ to_chat(src, span_notice("You create an illusion for [length(CM.temp_selected_pets)] pets."))
+
+/mob/proc/collar_master_share_damage()
+ set name = "Share Damage"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(!ishuman(src))
+ to_chat(src, span_warning("You must be human to share damage!"))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar is still cooling down!"))
+ return
+
+ var/mob/living/carbon/human/master = src
+ CM.last_command_time = world.time
+ var/shared_count = 0
+
+ // Calculate total damage to share
+ var/total_brute = master.getBruteLoss()
+ var/total_burn = master.getFireLoss()
+ var/total_oxy = master.getOxyLoss()
+ var/blood_diff = 0
+ if(master.blood_volume)
+ blood_diff = BLOOD_VOLUME_NORMAL - master.blood_volume
+
+ // Only proceed if there's damage to share
+ if(total_brute <= 0 && total_burn <= 0 && total_oxy <= 0 && blood_diff <= 0)
+ to_chat(src, span_warning("You have no damage to share!"))
+ return
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+
+ if(pet.stat >= UNCONSCIOUS)
+ to_chat(src, span_warning("[pet] must be conscious to share damage!"))
+ continue
+
+ // Share damage evenly
+ if(total_brute > 0)
+ pet.adjustBruteLoss(total_brute * 0.5)
+ master.adjustBruteLoss(-(total_brute * 0.5))
+ if(total_burn > 0)
+ pet.adjustFireLoss(total_burn * 0.5)
+ master.adjustFireLoss(-(total_burn * 0.5))
+ if(total_oxy > 0)
+ pet.adjustOxyLoss(total_oxy * 0.5)
+ master.adjustOxyLoss(-(total_oxy * 0.5))
+
+ // Share blood level if applicable
+ if(blood_diff > 0 && pet.blood_volume)
+ var/blood_share = min(blood_diff * 0.5, pet.blood_volume - BLOOD_VOLUME_SAFE)
+ if(blood_share > 0)
+ pet.blood_volume -= blood_share
+ master.blood_volume += blood_share
+
+ to_chat(pet, span_warning("You feel your master's pain transfer to you!"))
+ shared_count++
+
+ if(shared_count > 0)
+ to_chat(src, span_notice("Shared damage with [shared_count] pets."))
+ else
+ to_chat(src, span_warning("Failed to share damage with any pets!"))
+
+/mob/proc/collar_master_select_pets()
+ set name = "Select Pets"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM)
+ return
+
+ if(!length(CM.my_pets))
+ to_chat(src, span_warning("You have no pets to select!"))
+ return
+
+ var/list/pet_options = list()
+ for(var/mob/living/carbon/human/pet in CM.my_pets)
+ if(!pet || pet.stat == DEAD)
+ continue
+ pet_options[pet.name] = pet
+
+ if(!length(pet_options))
+ to_chat(src, span_warning("No valid pets available!"))
+ return
+
+ var/list/selected = input(src, "Select pets to command:", "Pet Selection") as null|anything in pet_options
+ if(!selected)
+ return
+
+ CM.temp_selected_pets.Cut()
+ if(islist(selected))
+ for(var/name in selected)
+ CM.temp_selected_pets += pet_options[name]
+ else
+ CM.temp_selected_pets += pet_options[selected]
+
+ to_chat(src, span_notice("Selected [length(CM.temp_selected_pets)] pets."))
+
+/mob/proc/collar_master_toggle_orgasm()
+ set name = "Toggle Orgasm"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar is still cooling down!"))
+ return
+
+ CM.last_command_time = world.time
+ CM.deny_orgasm = !CM.deny_orgasm
+ var/toggle_count = 0
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !pet.client || !(pet in CM.my_pets))
+ continue
+
+ if(CM.deny_orgasm)
+ to_chat(pet, span_warning("Your collar prevents you from reaching climax!"))
+ else
+ to_chat(pet, span_notice("Your collar no longer restricts your pleasure."))
+ toggle_count++
+
+ to_chat(src, span_notice("Toggled orgasm restriction for [toggle_count] pets."))
+
+/mob/proc/collar_master_release_pet()
+ set name = "Release Pet"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM || !length(CM.temp_selected_pets))
+ return
+
+ if(world.time < CM.last_command_time + CM.command_cooldown)
+ to_chat(src, span_warning("The collar is still cooling down!"))
+ return
+
+ var/confirm = alert("Are you sure you want to release the selected pets?", "Release Confirmation", "Yes", "No")
+ if(confirm != "Yes")
+ return
+
+ CM.last_command_time = world.time
+ var/released_count = 0
+
+ for(var/mob/living/carbon/human/pet in CM.temp_selected_pets)
+ if(!pet || !pet.mind || !(pet in CM.my_pets))
+ continue
+
+ // Handle collar removal properly
+ var/obj/item/clothing/neck/roguetown/cursed_collar/collar = pet.get_item_by_slot(SLOT_NECK)
+ if(istype(collar))
+ REMOVE_TRAIT(collar, TRAIT_NODROP, CURSED_ITEM_TRAIT)
+ pet.dropItemToGround(collar, force = TRUE)
+
+ // Let cleanup_pet handle trait removal and slavebourne stats
+ CM.cleanup_pet(pet)
+ CM.temp_selected_pets -= pet
+
+ to_chat(pet, span_notice("You have been released from your collar's control!"))
+ released_count++
+
+ if(released_count > 0)
+ to_chat(src, span_notice("Released [released_count] pets from your control."))
+ else
+ to_chat(src, span_warning("Failed to release any pets!"))
+
+/mob/proc/collar_master_help()
+ set name = "Collar Help"
+ set category = "Collar Tab"
+
+ var/datum/antagonist/collar_master/CM = mind?.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM)
+ return
+
+ var/help_text = {"Collar Master Commands:
+ - Select Pets: Choose which pets to affect with commands
+ - Send Message: Send a message through the collar
+ - Toggle Speech: Enable/disable pet speech
+ - Force Surrender: Force pets to submit
+ - Shock Pet: Punish pets with varying intensity
+ - Share Damage: Transfer your injuries to pets
+ - Scry on Pet: See through your pets' eyes
+ - Dominate Pet: Take direct control of a pet
+ - Toggle Restrictions: Control various pet behaviors
+ - Release Pet: Free pets from your control
+
+ Note: Most commands have a [CM.command_cooldown/10] second cooldown.
+ Currently controlling [length(CM.my_pets)] pets with [length(CM.temp_selected_pets)] selected."}
+
+ to_chat(src, help_text)
diff --git a/code/modules/antagonists/collar_master/components/dominated.dm b/code/modules/antagonists/collar_master/components/dominated.dm
new file mode 100644
index 00000000000..ac953fd1394
--- /dev/null
+++ b/code/modules/antagonists/collar_master/components/dominated.dm
@@ -0,0 +1,53 @@
+#define TRAIT_DOMINATED "dominated"
+
+/datum/component/dominated
+ var/datum/weakref/master_ref
+ var/mob/living/dominated_mob = null
+ var/static/list/animal_sounds = list(
+ "lets out a soft whimper.",
+ "whines quietly.",
+ "makes a needy sound.",
+ "lets out a submissive mewl.",
+ "makes a pathetic noise.",
+ "whimpers needily.",
+ "mewls submissively.",
+ "makes an obedient sound."
+ )
+
+/datum/component/dominated/Initialize(datum/antagonist/collar_master/new_master)
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ dominated_mob = parent
+ master_ref = WEAKREF(new_master)
+ ADD_TRAIT(parent, TRAIT_DOMINATED, "collar")
+
+ RegisterSignal(parent, COMSIG_MOB_SAY, PROC_REF(handle_speech))
+ RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_qdel))
+
+/datum/component/dominated/proc/handle_speech(datum/source, list/speech_args)
+ SIGNAL_HANDLER
+ var/mob/living/carbon/human/pet = source
+ if(!pet || !istype(pet))
+ return
+
+ pet.visible_message(span_emote("[pet] [pick(animal_sounds)]"))
+ playsound(pet, 'sound/ambience/noises/thunout (1).ogg', 50, TRUE)
+ return COMPONENT_CANCEL_SAY
+
+/datum/component/dominated/proc/on_parent_qdel(datum/source)
+ SIGNAL_HANDLER
+ qdel(src)
+
+/datum/component/dominated/Destroy()
+ UnregisterSignal(parent, list(
+ COMSIG_PARENT_QDELETING,
+ COMSIG_MOB_ATTACK,
+ COMSIG_MOB_SAY,
+ COMSIG_MOB_CLICKON
+ ))
+ var/mob/living/L = parent
+ L.SetStun(0)
+ REMOVE_TRAIT(L, TRAIT_DOMINATED, "collar")
+ dominated_mob = null
+ return ..()
diff --git a/code/modules/client/loadout/neck.dm b/code/modules/client/loadout/neck.dm
new file mode 100644
index 00000000000..906c26f3fea
--- /dev/null
+++ b/code/modules/client/loadout/neck.dm
@@ -0,0 +1,5 @@
+/datum/loadout_item/neck/lamptern
+ name = "Lamptern"
+ path = /obj/item/flashlight/flare/torch/lantern
+ category = LOADOUT_CATEGORY_NECK
+ cost = 2 // Adjust cost as needed
diff --git a/code/modules/clothing/neck/_neck.dm b/code/modules/clothing/neck/_neck.dm
index 6ef282f83ff..7e6023ea564 100644
--- a/code/modules/clothing/neck/_neck.dm
+++ b/code/modules/clothing/neck/_neck.dm
@@ -1,15 +1,3 @@
-#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)
-#define COMSIG_CARBON_GAIN_COLLAR "carbon_gain_collar"
-#define COMSIG_CARBON_LOSE_COLLAR "carbon_lose_collar"
-
/obj/item/clothing/neck
name = "necklace"
icon = 'icons/obj/clothing/neck.dmi'
@@ -244,132 +232,97 @@
w_class = WEIGHT_CLASS_SMALL
slot_flags = ITEM_SLOT_NECK
body_parts_covered = NECK
+ resistance_flags = INDESTRUCTIBLE
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)
- var/locked = FALSE
- var/being_removed = FALSE
-
-/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
-
- var/mob/living/carbon/human/attacker = source
- if(attacker == victim && target == collar_master)
- to_chat(attacker, span_warning("The collar sends painful shocks through your body as you try to attack your master!"))
- attacker.electrocute_act(25, src, flags = SHOCK_NOGLOVES)
- attacker.Paralyze(600)
- playsound(attacker, 'sound/blank.ogg', 50, TRUE)
- return COMPONENT_CANCEL_ATTACK
- return NONE
-
-/obj/item/clothing/neck/roguetown/cursed_collar/proc/prevent_removal(datum/source, mob/living/carbon/human/user)
- SIGNAL_HANDLER
-
- if(being_removed)
- return NONE
+ var/applying = FALSE
- if(!user)
- user = usr
-
- if(user && (user == collar_master))
- being_removed = TRUE
- REMOVE_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT)
- UnregisterSignal(src, COMSIG_ITEM_PRE_UNEQUIP)
- if(victim)
- UnregisterSignal(victim, list(
- COMSIG_MOB_CLICKON,
- COMSIG_MOB_ATTACK,
- COMSIG_MOB_SAY
- ))
- return NONE
-
- if(user && 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
-
-/obj/item/clothing/neck/roguetown/cursed_collar/attack(mob/living/carbon/human/M, mob/living/carbon/human/user)
- if(!istype(M) || !istype(user))
+/obj/item/clothing/neck/roguetown/cursed_collar/attack(mob/living/carbon/C, mob/living/user)
+ if(!istype(C))
return ..()
- if(M == user)
- if(HAS_TRAIT(user, TRAIT_SLAVEBOURNE))
- to_chat(user, span_warning("You cannot collar yourself - your nature requires another to take control!"))
- return
- to_chat(user, span_warning("The collar resists your attempts to put it on yourself!"))
+ if(!C.mind)
+ to_chat(user, span_warning("[C] is too simple-minded to be collared!"))
return
- if(M.get_item_by_slot(SLOT_NECK))
- to_chat(user, span_warning("[M] is already wearing something around their neck!"))
+ if(C == user && HAS_TRAIT(user, TRAIT_SLAVEBOURNE))
+ to_chat(user, span_warning("No, I want someone else to collar me!"))
return
- if(!do_mob(user, M, 50))
+ if(C.get_item_by_slot(SLOT_NECK))
+ to_chat(user, span_warning("[C] is already wearing something around their neck!"))
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
+ if(applying)
return
- to_chat(M, span_userdanger("The collar snaps shut around your neck!"))
- to_chat(user, span_notice("You successfully collar [M]."))
+ applying = TRUE
+ var/surrender_mod = 1
+ if(C.surrendering)
+ surrender_mod = 0.5
+
+ C.visible_message(span_danger("[user] begins locking the cursed collar around [C]'s neck!"), \
+ span_userdanger("[user] begins locking the cursed collar around your neck!"))
+ playsound(loc, 'sound/foley/equip/equip_armor_plate.ogg', 30, TRUE, -2)
- if(user.mind)
- var/datum/antagonist/collar_master/CM = new()
- CM.my_collar = src
- user.mind.add_antag_datum(CM)
+ if(do_mob(user, C, 50 * surrender_mod))
+ if(!user.mind)
+ to_chat(user, span_warning("You need a mind to control the collar!"))
+ applying = FALSE
+ return
+
+ // Try to equip first
+ if(!C.equip_to_slot_if_possible(src, SLOT_NECK, TRUE, TRUE))
+ to_chat(user, span_warning("You fail to lock the collar around [C]'s neck!"))
+ applying = FALSE
+ return
-/obj/item/clothing/neck/roguetown/cursed_collar/equipped(mob/user, slot)
+ // Get or create collar master datum
+ var/datum/antagonist/collar_master/CM = user.mind.has_antag_datum(/datum/antagonist/collar_master)
+ if(!CM)
+ CM = new()
+ user.mind.add_antag_datum(CM)
+
+ // Add pet to the master's list
+ CM.add_pet(C)
+
+ log_combat(user, C, "collared", addition="with [src]")
+ ADD_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT)
+ SEND_SIGNAL(C, COMSIG_CARBON_GAIN_COLLAR, src)
+
+ C.visible_message(span_warning("[user] locks the cursed collar around [C]'s neck!"), \
+ span_userdanger("[user] locks the cursed collar around your neck!"))
+ playsound(loc, 'sound/foley/equip/equip_armor_plate.ogg', 30, TRUE, -2)
+ else
+ to_chat(user, span_warning("You fail to lock the collar around [C]'s neck!"))
+ applying = FALSE
+
+/obj/item/clothing/neck/roguetown/cursed_collar/equipped(mob/living/carbon/human/user, slot)
. = ..()
- if(slot == SLOT_NECK && user == victim)
- RegisterSignal(user, COMSIG_MOB_CLICKON, PROC_REF(check_attack))
- RegisterSignal(user, COMSIG_MOB_ATTACK, PROC_REF(check_attack))
- RegisterSignal(user, COMSIG_MOB_SAY, PROC_REF(handle_speech))
- RegisterSignal(src, COMSIG_ITEM_PRE_UNEQUIP, PROC_REF(prevent_removal))
-
- // Only lock if not the master
- if(user != collar_master)
- locked = TRUE
- ADD_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT)
- SEND_SIGNAL(user, COMSIG_CARBON_GAIN_COLLAR, src)
-
-/obj/item/clothing/neck/roguetown/cursed_collar/dropped(mob/user)
- being_removed = FALSE
+ if(slot == SLOT_NECK && victim == user)
+ user.visible_message(span_warning("[user] is bound by the collar's dark magic."), \
+ span_warning("The collar's magic binds you to your new master's will!"))
+ to_chat(user, span_alert("You must now obey your master's commands."))
+
+/obj/item/clothing/neck/roguetown/cursed_collar/dropped(mob/living/carbon/human/user)
. = ..()
- if(user == victim)
- UnregisterSignal(user, list(
- COMSIG_MOB_CLICKON,
- COMSIG_MOB_ATTACK,
- COMSIG_MOB_SAY
- ))
- UnregisterSignal(src, COMSIG_ITEM_PRE_UNEQUIP)
+ if(!user)
+ return
+ SEND_SIGNAL(user, COMSIG_CARBON_LOSE_COLLAR)
+
+ // Find and remove from any collar master's pet list
+ for(var/datum/mind/M in SSticker.minds)
+ var/datum/antagonist/collar_master/CM = M.has_antag_datum(/datum/antagonist/collar_master)
+ if(CM && (user in CM.my_pets))
+ CM.remove_pet(user)
+ break
+
+ REMOVE_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT)
+
+/obj/item/clothing/neck/roguetown/cursed_collar/canStrip(mob/living/carbon/human/stripper, mob/living/carbon/human/owner)
+ if(stripper == collar_master)
REMOVE_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT)
- locked = FALSE
- victim = null
- SEND_SIGNAL(user, COMSIG_CARBON_LOSE_COLLAR)
+ SEND_SIGNAL(owner, COMSIG_CARBON_LOSE_COLLAR)
+ return TRUE
+ return FALSE
diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm
index e2bb127f905..db870933507 100644
--- a/code/modules/mob/living/carbon/human/examine.dm
+++ b/code/modules/mob/living/carbon/human/examine.dm
@@ -81,7 +81,6 @@
else if(used_title)
. = list("ø ------------ ø\nThis is [used_name], the [is_returning ? "returning " : ""][custom_race_name ? "[custom_race_name] ([race_name])" : "[race_name]"] [used_title].")
- // Add slavebourne text right after introduction
if(HAS_TRAIT(src, TRAIT_SLAVEBOURNE_EXAMINE))
. += span_notice("[p_they(TRUE)] carries [p_them()]self with a submissive demeanor as if seeking direction.")
else
@@ -626,7 +625,7 @@
if(!(mobility_flags & MOBILITY_STAND) && user != src && (user.zone_selected == BODY_ZONE_CHEST))
. += "Listen to Heartbeat"
- var/list/lines = build_cool_description(get_mob_descriptors(obscure_name, user), src) //vardefine for descriptors
+ var/list/lines = build_cool_description(get_mob_descriptors(obscure_name, user), src)
var/trait_exam = common_trait_examine()
if(!isnull(trait_exam))
. += trait_exam
diff --git a/code/modules/sex/sex_controller.dm b/code/modules/sex/sex_controller.dm
new file mode 100644
index 00000000000..522a091c86b
--- /dev/null
+++ b/code/modules/sex/sex_controller.dm
@@ -0,0 +1,52 @@
+/datum/sex_controller
+ var/arousal = 0
+ var/mob/living/carbon/human/parent
+
+/datum/sex_controller/Initialize(mob/living/carbon/human/new_parent)
+ . = ..()
+ parent = new_parent
+
+/datum/sex_controller/proc/adjust_arousal_manual(amount)
+ if(!parent || !istype(parent))
+ return
+
+ // Check if parent has deny_orgasm trait from a collar master
+ var/list/collar_masters = list()
+ for(var/datum/antagonist/collar_master/CM in GLOB.antagonists)
+ if(parent in CM.my_pets && CM.deny_orgasm)
+ collar_masters += CM
+
+ // If denied, cap at 96
+ if(length(collar_masters) && amount > 96)
+ amount = 96
+
+ arousal = clamp(amount, 0, 100)
+
+ // Update screen effects based on arousal level
+ if(arousal >= 80)
+ parent.overlay_fullscreen("arousal", /atom/movable/screen/fullscreen/arousal, 3)
+ else if(arousal >= 50)
+ parent.overlay_fullscreen("arousal", /atom/movable/screen/fullscreen/arousal, 2)
+ else if(arousal >= 20)
+ parent.overlay_fullscreen("arousal", /atom/movable/screen/fullscreen/arousal, 1)
+ else
+ parent.clear_fullscreen("arousal")
+
+ // Try climax if arousal hits 100
+ if(arousal >= 100)
+ try_climax()
+
+/datum/sex_controller/proc/try_climax()
+ if(!parent || !istype(parent, /mob/living/carbon/human))
+ return
+
+ // Send signal first - if it returns COMPONENT_CANCEL_CLIMAX, stop here
+ if(SEND_SIGNAL(parent, COMSIG_SEXCONTROLLER_CLIMAX) & COMPONENT_CANCEL_CLIMAX)
+ return
+
+ // Actual climax effects
+ to_chat(parent, span_love("You reach climax!"))
+ parent.do_jitter_animation(30)
+ parent.emote("moan")
+ adjust_arousal_manual(0) // Reset arousal
+ playsound(parent, 'sound/misc/mat/segso.ogg', 50, TRUE)
diff --git a/modular_hearthstone/code/datums/loadout.dm b/modular_hearthstone/code/datums/loadout.dm
index 295d07d8a34..3486e687432 100644
--- a/modular_hearthstone/code/datums/loadout.dm
+++ b/modular_hearthstone/code/datums/loadout.dm
@@ -338,4 +338,8 @@ GLOBAL_LIST_EMPTY(loadout_items)
name = "Onderite Tabard"
path = /obj/item/clothing/cloak/templar/xylix
+/datum/loadout_item/lantern
+ name = "Lamptern"
+ path = /obj/item/flashlight/flare/torch/lantern
+
diff --git a/roguetown.dme b/roguetown.dme
index 624e4bb93f3..2267492b617 100644
--- a/roguetown.dme
+++ b/roguetown.dme
@@ -1669,6 +1669,8 @@
#include "code\modules\antagonists\changeling\powers\tiny_prick.dm"
#include "code\modules\antagonists\changeling\powers\transform.dm"
#include "code\modules\antagonists\collar_master\collar_master.dm"
+#include "code\modules\antagonists\collar_master\collar_master_verbs.dm"
+#include "code\modules\antagonists\collar_master\components\dominated.dm"
#include "code\modules\antagonists\creep\creep.dm"
#include "code\modules\antagonists\cult\blood_magic.dm"
#include "code\modules\antagonists\cult\cult.dm"