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"