diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index 259bcf07500..306a00811d1 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -389,7 +389,8 @@ #define COMSIG_MOB_SWAP_HANDS "mob_swap_hands" #define COMPONENT_BLOCK_SWAP (1<<0) - +#define COMSIG_MOB_AUTOMUTE_CHECK "automute_check" + #define WAIVE_AUTOMUTE_CHECK (1<<0) // /mob/living signals diff --git a/code/__DEFINES/misc_defines.dm b/code/__DEFINES/misc_defines.dm index fd6abbb1f44..932dcead2f7 100644 --- a/code/__DEFINES/misc_defines.dm +++ b/code/__DEFINES/misc_defines.dm @@ -540,3 +540,12 @@ #define REFLECTABILITY_NEVER 0 #define REFLECTABILITY_PHYSICAL 1 #define REFLECTABILITY_ENERGY 2 + +// Deadchat control defines + +/// Will execute a single command after the cooldown based on player votes. +#define DEADCHAT_DEMOCRACY_MODE (1<<0) +/// Allows each player to do a single command every cooldown. +#define DEADCHAT_ANARCHY_MODE (1<<1) +/// Mutes the democracy mode messages send to orbiters at the end of each cycle. Useful for when the cooldown is so low it'd get spammy. +#define MUTE_DEADCHAT_DEMOCRACY_MESSAGES (1<<2) diff --git a/code/datums/components/deadchat_control.dm b/code/datums/components/deadchat_control.dm new file mode 100644 index 00000000000..8608af566d0 --- /dev/null +++ b/code/datums/components/deadchat_control.dm @@ -0,0 +1,251 @@ + +/** + * Deadchat Plays Things - The Componenting + * + * Allows deadchat to control stuff and things by typing commands into chat. + * These commands will then trigger callbacks to execute procs! + */ +/datum/component/deadchat_control + dupe_mode = COMPONENT_DUPE_UNIQUE + + /// The id for the DEADCHAT_DEMOCRACY_MODE looping vote timer. + var/timerid + /// Assoc list of key-chat command string, value-callback pairs. list("right" = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(_step), src, EAST)) + var/list/datum/callback/inputs = list() + /// Assoc list of ckey:value pairings. In DEADCHAT_DEMOCRACY_MODE, value is the player's vote. In DEADCHAT_ANARCHY_MODE, value is world.time when their cooldown expires. + var/list/ckey_to_cooldown = list() + /// List of everything orbitting this component's parent. + var/orbiters = list() + /// A bitfield containing the mode which this component uses (DEADCHAT_DEMOCRACY_MODE or DEADCHAT_ANARCHY_MODE) and other settings) + var/deadchat_mode = DEADCHAT_DEMOCRACY_MODE + /// In DEADCHAT_DEMOCRACY_MODE, this is how long players have to vote on an input. In DEADCHAT_ANARCHY_MODE, this is how long between inputs for each unique player. + var/input_cooldown + ///Set to true if a point of interest was created for an object, and needs to be removed if deadchat control is removed. Needed for preventing objects from having two points of interest. + var/generated_point_of_interest = FALSE + /// Callback invoked when this component is Destroy()ed to allow the parent to return to a non-deadchat controlled state. + var/datum/callback/on_removal + +/datum/component/deadchat_control/Initialize(_deadchat_mode, _inputs, _input_cooldown = 12 SECONDS, _on_removal) + if(!isatom(parent)) + return COMPONENT_INCOMPATIBLE + RegisterSignal(parent, COMSIG_ATOM_ORBIT_BEGIN, PROC_REF(orbit_begin)) + RegisterSignal(parent, COMSIG_ATOM_ORBIT_STOP, PROC_REF(orbit_stop)) + RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine)) + deadchat_mode = _deadchat_mode + inputs = _inputs + input_cooldown = _input_cooldown + on_removal = _on_removal + if(deadchat_mode & DEADCHAT_DEMOCRACY_MODE) + if(deadchat_mode & DEADCHAT_ANARCHY_MODE) // Choose one, please. + stack_trace("deadchat_control component added to [parent.type] with both democracy and anarchy modes enabled.") + timerid = addtimer(CALLBACK(src, PROC_REF(democracy_loop)), input_cooldown, TIMER_STOPPABLE | TIMER_LOOP) + + var/list/input_names = list() + for(var/item in inputs) + input_names |= item + notify_ghosts("[parent] is now deadchat controllable! Possible commands are: [english_list(input_names)]", source = parent, action = NOTIFY_FOLLOW, title="Deadchat control!") + if(!ismob(parent) && !(parent in GLOB.poi_list)) + GLOB.poi_list |= parent + generated_point_of_interest = TRUE + message_admins("[parent] has been given deadchat control in [deadchat_mode == DEADCHAT_ANARCHY_MODE ? "anarchy" : "democracy"] mode with a cooldown of [input_cooldown SECONDS] second\s.") + +/datum/component/deadchat_control/Destroy(force, silent) + var/message = "[parent] is no longer controllable." + for(var/mob/dead/observer/M in orbiters) + to_chat(M, message) + on_removal?.Invoke() + inputs = null + orbiters = null + ckey_to_cooldown = null + if(generated_point_of_interest) + GLOB.poi_list -= parent + return ..() + +/datum/component/deadchat_control/proc/deadchat_react(mob/source, message) + SIGNAL_HANDLER // COMSIG_MOB_DEADSAY + + message = lowertext(message) + + if(!inputs[message]) + return + + if(deadchat_mode & DEADCHAT_ANARCHY_MODE) + if(!source || !source.ckey) + return + var/cooldown = ckey_to_cooldown[source.ckey] - world.time + if(cooldown > 0) + to_chat(source, "Your deadchat control inputs are still on cooldown for another [CEILING(cooldown * 0.1, 1)] second\s.") + return MOB_DEADSAY_SIGNAL_INTERCEPT + ckey_to_cooldown[source.ckey] = world.time + input_cooldown + addtimer(CALLBACK(src, PROC_REF(end_cooldown), source.ckey), input_cooldown) + inputs[message].Invoke() + to_chat(source, "\"[message]\" input accepted. You are now on cooldown for [input_cooldown * 0.1] second\s.") + return MOB_DEADSAY_SIGNAL_INTERCEPT + + if(deadchat_mode & DEADCHAT_DEMOCRACY_MODE) + ckey_to_cooldown[source.ckey] = message + to_chat(source, "You have voted for \"[message]\".") + return MOB_DEADSAY_SIGNAL_INTERCEPT + +/datum/component/deadchat_control/proc/democracy_loop() + if(QDELETED(parent) || !(deadchat_mode & DEADCHAT_DEMOCRACY_MODE)) + deltimer(timerid) + return + var/result = count_democracy_votes() + if(!isnull(result)) + inputs[result].Invoke() + if(!(deadchat_mode & MUTE_DEADCHAT_DEMOCRACY_MESSAGES)) + var/message = "[parent] has done action [result]!
New vote started. It will end in [input_cooldown * 0.1] second\s.
" + for(var/mob/dead/observer/M in orbiters) + to_chat(M, message) + else if(!(deadchat_mode & MUTE_DEADCHAT_DEMOCRACY_MESSAGES)) + var/message = "No votes were cast this cycle." + for(var/mob/dead/observer/M in orbiters) + to_chat(M, message) + +/datum/component/deadchat_control/proc/count_democracy_votes() + if(!length(ckey_to_cooldown)) + return + var/list/votes = list() + for(var/command in inputs) + votes["[command]"] = 0 + for(var/vote in ckey_to_cooldown) + votes[ckey_to_cooldown[vote]]++ + ckey_to_cooldown.Remove(vote) + + // Solve which had most votes. + var/prev_value = 0 + var/result + for(var/vote in votes) + if(votes[vote] > prev_value) + prev_value = votes[vote] + result = vote + + if(result in inputs) + return result + +/datum/component/deadchat_control/vv_edit_var(var_name, var_value) + . = ..() + if(!.) + return + if(var_name != NAMEOF(src, deadchat_mode)) + return + ckey_to_cooldown = list() + if(var_value == DEADCHAT_DEMOCRACY_MODE) + timerid = addtimer(CALLBACK(src, PROC_REF(democracy_loop)), input_cooldown, TIMER_STOPPABLE | TIMER_LOOP) + else + deltimer(timerid) + +/datum/component/deadchat_control/proc/orbit_begin(atom/source, atom/orbiter) + SIGNAL_HANDLER // COMSIG_ATOM_ORBIT_BEGIN + + if(isobserver(orbiter)) + var/mob/dead/observer/O = orbiter + if(O.client && !(O.client.prefs.toggles & PREFTOGGLE_CHAT_DEAD)) + to_chat(O, "You have deadchat muted, and as such will not receive messages related to, nor be able to participate in, controlling this object.") + to_chat(O, "If you would like to participate, unmute deadchat and follow this object again.") + return + + RegisterSignal(orbiter, COMSIG_MOB_DEADSAY, PROC_REF(deadchat_react)) + RegisterSignal(orbiter, COMSIG_MOB_AUTOMUTE_CHECK, PROC_REF(waive_automute)) + orbiters |= orbiter + + +/datum/component/deadchat_control/proc/orbit_stop(atom/source, atom/orbiter) + SIGNAL_HANDLER // COMSIG_ATOM_ORBIT_STOP + + if(orbiter in orbiters) + UnregisterSignal(orbiter, list( + COMSIG_MOB_DEADSAY, + COMSIG_MOB_AUTOMUTE_CHECK, + )) + orbiters -= orbiter + +/** + * Prevents messages used to control the parent from counting towards the automute threshold for repeated identical messages. + * + * Arguments: + * - [speaker][/client]: The mob that is trying to speak. + * - [client][/client]: The client that is trying to speak. + * - message: The message that the speaker is trying to say. + * - mute_type: Which type of mute the message counts towards. + */ +/datum/component/deadchat_control/proc/waive_automute(mob/speaker, client/client, message, mute_type) + SIGNAL_HANDLER // COMSIG_MOB_AUTOMUTE_CHECK + if(mute_type == MUTE_DEADCHAT && inputs[lowertext(message)]) + return WAIVE_AUTOMUTE_CHECK + return NONE + + +/// Informs any examiners to the inputs available as part of deadchat control, as well as the current operating mode and cooldowns. +/datum/component/deadchat_control/proc/on_examine(atom/A, mob/user, list/examine_list) + SIGNAL_HANDLER // COMSIG_PARENT_EXAMINE + + if(!isobserver(user)) + return + + examine_list += "[A.p_theyre(TRUE)] currently under deadchat control using the [(deadchat_mode & DEADCHAT_DEMOCRACY_MODE) ? "democracy" : "anarchy"] ruleset!" + + if(user.client && !(user.client.prefs.toggles & PREFTOGGLE_CHAT_DEAD)) + examine_list += "As you have deadchat disabled, you will not see vote messages, nor be able to participate in voting." + return + + if(deadchat_mode & DEADCHAT_DEMOCRACY_MODE) + examine_list += "Type a command into chat to vote on an action. This happens once every [input_cooldown * 0.1] second\s." + else if(deadchat_mode & DEADCHAT_ANARCHY_MODE) + examine_list += "Type a command into chat to perform. You may do this once every [input_cooldown * 0.1] second\s." + + var/extended_examine = "Command list:" + + for(var/possible_input in inputs) + extended_examine += " [possible_input]" + + extended_examine += "." + + examine_list += extended_examine + +/// Removes the ghost from the ckey_to_cooldown list and lets them know they are free to submit a command for the parent again. +/datum/component/deadchat_control/proc/end_cooldown(ghost_ckey) + ckey_to_cooldown -= ghost_ckey + var/mob/ghost = get_mob_by_ckey(ghost_ckey) + if(!ghost || isliving(ghost)) + return + to_chat(ghost, "Your deadchat control inputs for [parent] ([ghost_follow_link(parent, ghost)]) are no longer on cooldown.") + +/// Dummy to call since we can't proc reference builtins +/datum/component/deadchat_control/proc/_step(ref, dir) + step(ref, dir) + +/** + * Deadchat Moves Things + * + * A special variant of the deadchat_control component that comes pre-baked with all the hottest inputs for a spicy + * singularity or vomit goose. + */ +/datum/component/deadchat_control/cardinal_movement/Initialize(_deadchat_mode, _inputs, _input_cooldown, _on_removal) + if(!ismovable(parent)) + return COMPONENT_INCOMPATIBLE + + _inputs["up"] = CALLBACK(src, PROC_REF(_step), parent, NORTH) + _inputs["down"] = CALLBACK(src, PROC_REF(_step), parent, SOUTH) + _inputs["left"] = CALLBACK(src, PROC_REF(_step), parent, WEST) + _inputs["right"] = CALLBACK(src, PROC_REF(_step), parent, EAST) + + return ..() + +/** + * Deadchat Moves Things + * + * A special variant of the deadchat_control component that comes pre-baked with all the hottest inputs for spicy + * immovable rod. + */ +/datum/component/deadchat_control/immovable_rod/Initialize(_deadchat_mode, _inputs, _input_cooldown, _on_removal) + if(!istype(parent, /obj/effect/immovablerod)) + return COMPONENT_INCOMPATIBLE + + _inputs["up"] = CALLBACK(parent, TYPE_PROC_REF(/obj/effect/immovablerod, walk_in_direction), NORTH) + _inputs["down"] = CALLBACK(parent, TYPE_PROC_REF(/obj/effect/immovablerod, walk_in_direction), SOUTH) + _inputs["left"] = CALLBACK(parent, TYPE_PROC_REF(/obj/effect/immovablerod, walk_in_direction), WEST) + _inputs["right"] = CALLBACK(parent, TYPE_PROC_REF(/obj/effect/immovablerod, walk_in_direction), EAST) + + return ..() diff --git a/code/datums/datumvars.dm b/code/datums/datumvars.dm index f31d0f014f8..80d22911c58 100644 --- a/code/datums/datumvars.dm +++ b/code/datums/datumvars.dm @@ -1236,6 +1236,63 @@ var/datum/ui_module/colour_matrix_tester/CMT = new(target=target) CMT.ui_interact(usr) + if(href_list["grantdeadchatcontrol"]) + if(!check_rights(R_EVENT)) + return + + var/atom/movable/A = locateUID(href_list["grantdeadchatcontrol"]) + if(!istype(A)) + return + + if(!GLOB.dsay_enabled) + // TODO verify what happens when deadchat is muted + to_chat(usr, "Deadchat is globally muted, un-mute deadchat before enabling this.") + return + + if(A.GetComponent(/datum/component/deadchat_control)) + to_chat(usr, "[A] is already under deadchat control!") + return + + var/control_mode = input(usr, "Please select the control mode","Deadchat Control", null) as null|anything in list("democracy", "anarchy") + + var/selected_mode + switch(control_mode) + if("democracy") + selected_mode = DEADCHAT_DEMOCRACY_MODE + if("anarchy") + selected_mode = DEADCHAT_ANARCHY_MODE + else + return + + var/cooldown = input(usr, "Please enter a cooldown time in seconds. For democracy, it's the time between actions (must be greater than zero). For anarchy, it's the time between each user's actions, or -1 for no cooldown.", "Cooldown", null) as null|num + if(isnull(cooldown) || (cooldown == -1 && selected_mode == DEADCHAT_DEMOCRACY_MODE)) + return + if(cooldown < 0 && selected_mode == DEADCHAT_DEMOCRACY_MODE) + to_chat(usr, "The cooldown for democracy mode must be greater than zero.") + return + if(cooldown == -1) + cooldown = 0 + else + cooldown = cooldown SECONDS + + A.deadchat_plays(selected_mode, cooldown) + message_admins("[key_name_admin(usr)] provided deadchat control to [A].") + + if(href_list["removedeadchatcontrol"]) + if(!check_rights(R_EVENT)) + return + + var/atom/movable/A = locateUID(href_list["removedeadchatcontrol"]) + if(!istype(A)) + return + + if(!A.GetComponent(/datum/component/deadchat_control)) + to_chat(usr, "[A] is not currently under deadchat control!") + return + + A.stop_deadchat_plays() + message_admins("[key_name_admin(usr)] removed deadchat control from [A].") + /client/proc/view_var_Topic_list(href, href_list, hsrc) if(href_list["VarsList"]) debug_variables(locate(href_list["VarsList"])) diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 5e312ce96da..3281abf80ba 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -624,3 +624,27 @@ /// called when a mob gets shoved into an items turf. false means the mob will be shoved backwards normally, true means the mob will not be moved by the disarm proc. /atom/movable/proc/shove_impact(mob/living/target, mob/living/attacker) return FALSE + +/** + * Adds the deadchat_plays component to this atom with simple movement commands. + * + * Returns the component added. + * Arguments: + * * mode - Either DEADCHAT_ANARCHY_MODE or DEADCHAT_DEMOCRACY_MODE passed to the deadchat_control component. See [/datum/component/deadchat_control] for more info. + * * cooldown - The cooldown between command inputs passed to the deadchat_control component. See [/datum/component/deadchat_control] for more info. + */ +/atom/movable/proc/deadchat_plays(mode = DEADCHAT_ANARCHY_MODE, cooldown = 12 SECONDS) + return AddComponent(/datum/component/deadchat_control/cardinal_movement, mode, list(), cooldown) + +/// Easy way to remove the component when the fun has been played out +/atom/movable/proc/stop_deadchat_plays() + var/datum/component/deadchat_control/comp = GetComponent(/datum/component/deadchat_control) + qdel(comp) + +/atom/movable/vv_get_dropdown() + . = ..() + if(!GetComponent(/datum/component/deadchat_control)) + .["Give deadchat control"] = "?_src_=vars;grantdeadchatcontrol=[UID()]" + else + .["Remove deadchat control"] = "?_src_=vars;removedeadchatcontrol=[UID()]" + diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index e465d8ae24f..30f8b8569e5 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -216,6 +216,8 @@ if(GLOB.configuration.general.enable_auto_mute && !check_rights(R_ADMIN, 0) && last_message == message) last_message_count++ + if(SEND_SIGNAL(mob, COMSIG_MOB_AUTOMUTE_CHECK, src, last_message, mute_type) & WAIVE_AUTOMUTE_CHECK) + return FALSE if(last_message_count >= SPAM_TRIGGER_AUTOMUTE) to_chat(src, "You have exceeded the spam filter limit for identical messages. An auto-mute was applied.") cmd_admin_mute(mob, mute_type, 1) diff --git a/code/modules/events/immovable_rod.dm b/code/modules/events/immovable_rod.dm index b30a9111c20..9c7169df52b 100644 --- a/code/modules/events/immovable_rod.dm +++ b/code/modules/events/immovable_rod.dm @@ -102,3 +102,16 @@ In my current plan for it, 'solid' will be defined as anything with density == 1 T.ex_act(EXPLODE_HEAVY) if(loc == destination) qdel(src) + +/obj/effect/immovablerod/deadchat_plays(mode = DEADCHAT_DEMOCRACY_MODE, cooldown = 6 SECONDS) + return AddComponent(/datum/component/deadchat_control/immovable_rod, mode, list(), cooldown) + +/** + * Rod will walk towards edge turf in the specified direction. + * + * Arguments: + * * direction - The direction to walk the rod towards: NORTH, SOUTH, EAST, WEST. + */ +/obj/effect/immovablerod/proc/walk_in_direction(direction) + destination = get_edge_target_turf(src, direction) + walk_towards(src, destination) diff --git a/code/modules/mob/living/carbon/human/human_mob.dm b/code/modules/mob/living/carbon/human/human_mob.dm index 24c799a48f6..84e145521e2 100644 --- a/code/modules/mob/living/carbon/human/human_mob.dm +++ b/code/modules/mob/living/carbon/human/human_mob.dm @@ -2065,3 +2065,83 @@ Eyes need to have significantly high darksight to shine unless the mob has the X set category = "IC" update_flavor_text() + +// Behavior for deadchat control + +/mob/living/carbon/human/proc/dchat_emote() + var/list/possible_emotes = list("scream", "clap", "snap", "crack", "dap", "burp") + emote(pick(possible_emotes)) + +/mob/living/carbon/human/proc/dchat_attack() + var/turf/ahead = get_turf(get_step(src, dir)) + var/mob/living/victim = locate(/mob/living) in ahead + var/in_hand = get_active_hand() + if(victim) + victim.attacked_by(in_hand, src, BODY_ZONE_CHEST) + return + var/obj/structure/other_victim = locate(/obj/structure) in ahead + if(other_victim) + do_attack_animation(other_victim, used_item = in_hand) + other_victim.attacked_by(in_hand, src) + return + + visible_message("[src] swings [isnull(in_hand) ? "[p_their()] fists" : in_hand] wildly!") + +/mob/living/carbon/human/proc/dchat_pickup() + var/turf/ahead = get_step(src, dir) + var/obj/item/thing = locate(/obj/item) in ahead + if(!thing) + return + + var/old_loc = thing.loc + var/obj/item/in_hand = get_active_hand() + + if(in_hand) + visible_message("[src] drops [in_hand] and picks up [thing] instead!") + unEquip(in_hand) + in_hand.forceMove(old_loc) + else + visible_message("[src] picks up [thing]!") + put_in_active_hand(thing) + +/mob/living/carbon/human/proc/dchat_throw() + var/in_hand = get_active_hand() + if(!in_hand) + visible_message("[src] makes a throwing motion!") + return + var/atom/possible_target + var/cur_turf = get_turf(src) + for(var/i in 1 to 5) + cur_turf = get_step(cur_turf, dir) + possible_target = locate(/mob/living) in cur_turf + if(possible_target) + break + + possible_target = locate(/obj/structure) in cur_turf + if(possible_target) + break + + if(!possible_target) + throw_item(cur_turf) + else + throw_item(possible_target) + +/mob/living/carbon/human/proc/dchat_shove() + var/turf/ahead = get_turf(get_step(src, dir)) + var/mob/living/carbon/human/H = locate(/mob/living/carbon/human) in ahead + if(!H) + visible_message("[src] tries to shove something away!") + return + dna?.species.disarm(src, H) + + +/mob/living/carbon/human/deadchat_plays(mode = DEADCHAT_DEMOCRACY_MODE, cooldown = 7 SECONDS) + var/list/inputs = list( + "emote" = CALLBACK(src, PROC_REF(dchat_emote)), + "attack" = CALLBACK(src, PROC_REF(dchat_attack)), + "pickup" = CALLBACK(src, PROC_REF(dchat_pickup)), + "throw" = CALLBACK(src, PROC_REF(dchat_throw)), + "disarm" = CALLBACK(src, PROC_REF(dchat_shove)), + ) + + AddComponent(/datum/component/deadchat_control/cardinal_movement, mode, inputs, cooldown) diff --git a/code/modules/mob/living/simple_animal/friendly/dog.dm b/code/modules/mob/living/simple_animal/friendly/dog.dm index 2b131b53e29..646b048218c 100644 --- a/code/modules/mob/living/simple_animal/friendly/dog.dm +++ b/code/modules/mob/living/simple_animal/friendly/dog.dm @@ -105,6 +105,48 @@ ..(gibbed) regenerate_icons() +/mob/living/simple_animal/pet/dog/corgi/deadchat_plays(mode = DEADCHAT_ANARCHY_MODE, cooldown = 12 SECONDS) + . = AddComponent(/datum/component/deadchat_control/cardinal_movement, mode, list( + "speak" = CALLBACK(src, PROC_REF(handle_automated_speech), TRUE), + "wear_hat" = CALLBACK(src, PROC_REF(find_new_hat)), + "drop_hat" = CALLBACK(src, PROC_REF(drop_hat)), + "spin" = CALLBACK(src, TYPE_PROC_REF(/mob, emote), "spin")), cooldown, CALLBACK(src, PROC_REF(end_dchat_plays))) + + if(. == COMPONENT_INCOMPATIBLE) + return + + stop_automated_movement = TRUE + +///Deadchat plays command that picks a new hat for Ian. +/mob/living/simple_animal/pet/dog/corgi/proc/find_new_hat() + if(!isturf(loc)) + return + var/list/possible_headwear = list() + for(var/obj/item/item in loc) + if(ispath(item.dog_fashion, /datum/dog_fashion/head)) + possible_headwear += item + if(!length(possible_headwear)) + for(var/obj/item/item in orange(1)) + if(ispath(item.dog_fashion, /datum/dog_fashion/head) && Adjacent(item)) + possible_headwear += item + if(!length(possible_headwear)) + return + if(inventory_head) + inventory_head.forceMove(drop_location()) + inventory_head = null + place_on_head(pick(possible_headwear)) + visible_message("[src] puts [inventory_head] on [p_their()] own head, somehow.") + +///Deadchat plays command that drops the current hat off Ian. +/mob/living/simple_animal/pet/dog/corgi/proc/drop_hat() + if(!inventory_head) + return + visible_message("[src] vigorously shakes [p_their()] head, dropping [inventory_head] to the ground.") + inventory_head.forceMove(drop_location()) + inventory_head = null + update_corgi_fluff() + regenerate_icons() + /mob/living/simple_animal/pet/dog/corgi/show_inv(mob/user) if(user.incapacitated() || !Adjacent(user)) return diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index e7638b81c79..0b859c30460 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -635,3 +635,14 @@ /mob/living/simple_animal/proc/npc_safe(mob/user) return FALSE + +/mob/living/simple_animal/deadchat_plays(mode = DEADCHAT_ANARCHY_MODE, cooldown = 12 SECONDS) + . = AddComponent(/datum/component/deadchat_control/cardinal_movement, mode, list(), cooldown, CALLBACK(src, PROC_REF(end_dchat_plays))) + + if(. == COMPONENT_INCOMPATIBLE) + return + + stop_automated_movement = TRUE + +/mob/living/simple_animal/proc/end_dchat_plays() + stop_automated_movement = FALSE diff --git a/code/modules/mob/mob_say_base.dm b/code/modules/mob/mob_say_base.dm index 54a4d195734..58135cceab3 100644 --- a/code/modules/mob/mob_say_base.dm +++ b/code/modules/mob/mob_say_base.dm @@ -62,6 +62,8 @@ if(client.handle_spam_prevention(message, MUTE_DEADCHAT)) return + if(SEND_SIGNAL(src, COMSIG_MOB_DEADSAY, message) & MOB_DEADSAY_SIGNAL_INTERCEPT) + return if(message in USABLE_DEAD_EMOTES) emote(copytext(message, 2), intentional = TRUE) diff --git a/code/modules/power/singularity/singularity.dm b/code/modules/power/singularity/singularity.dm index 5684ba4823d..53a645a3169 100644 --- a/code/modules/power/singularity/singularity.dm +++ b/code/modules/power/singularity/singularity.dm @@ -522,3 +522,20 @@ projectile_angle += angle_to_projectile / (distance ** 2) P.damage += 10 / distance P.set_angle(projectile_angle) + +/obj/singularity/proc/end_deadchat_plays() + move_self = TRUE + + +/obj/singularity/deadchat_plays(mode = DEADCHAT_DEMOCRACY_MODE, cooldown = 12 SECONDS) + . = AddComponent(/datum/component/deadchat_control/cardinal_movement, mode, list(), cooldown, CALLBACK(src, TYPE_PROC_REF(/atom/movable, stop_deadchat_plays))) + + if(. == COMPONENT_INCOMPATIBLE) + return + + move_self = FALSE + + +/obj/singularity/deadchat_controlled/Initialize(mapload, starting_energy) + . = ..() + deadchat_plays(mode = DEADCHAT_DEMOCRACY_MODE) diff --git a/paradise.dme b/paradise.dme index c4344b48316..8e1b71ab716 100644 --- a/paradise.dme +++ b/paradise.dme @@ -353,6 +353,7 @@ #include "code\datums\cache\powermonitor.dm" #include "code\datums\components\_component.dm" #include "code\datums\components\caltrop.dm" +#include "code\datums\components\deadchat_control.dm" #include "code\datums\components\decal.dm" #include "code\datums\components\defibrillator.dm" #include "code\datums\components\ducttape.dm"