diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index 7a510c9cdee..5f7a41c542f 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -396,3 +396,6 @@ GLOBAL_LIST_INIT(human_invader_antagonists, list( #define SPY_DIFFICULTY_MEDIUM "Medium" /// Very difficult to accomplish, almost guaranteed to require crew conflict #define SPY_DIFFICULTY_HARD "Hard" + +/// Camera net used by battle royale objective +#define BATTLE_ROYALE_CAMERA_NET "battle_royale_camera_net" diff --git a/code/__DEFINES/dcs/signals/signals_traitor.dm b/code/__DEFINES/dcs/signals/signals_traitor.dm index 4290b25b800..2752ab2363e 100644 --- a/code/__DEFINES/dcs/signals/signals_traitor.dm +++ b/code/__DEFINES/dcs/signals/signals_traitor.dm @@ -34,3 +34,6 @@ #define COMSIG_TRAITOR_GRAFFITI_SLIPPED "traitor_demoralise_event" /// For when someone is injected with the EHMS virus from /datum/traitor_objective_category/infect #define COMSIG_EHMS_INJECTOR_INJECTED "after_ehms_inject" + +/// Called by an battle royale implanter when successfully implanting someone. Passes the implanted mob. +#define COMSIG_ROYALE_IMPLANTED "royale_implanted" diff --git a/code/controllers/subsystem/battle_royale.dm b/code/controllers/subsystem/battle_royale.dm new file mode 100644 index 00000000000..139a5aa8b49 --- /dev/null +++ b/code/controllers/subsystem/battle_royale.dm @@ -0,0 +1,230 @@ +/// Global list of areas which are considered to be inside the same department for our purposes +GLOBAL_LIST_INIT(battle_royale_regions, list( + "Medical Bay" = list( + /area/station/command/heads_quarters/cmo, + /area/station/medical, + /area/station/security/checkpoint/medical, + ), + "Research Division" = list( + /area/station/command/heads_quarters/rd, + /area/station/security/checkpoint/science, + /area/station/science, + ), + "Engineering Bay" = list( + /area/station/command/heads_quarters/ce, + /area/station/engineering, + /area/station/maintenance/disposal/incinerator, + /area/station/security/checkpoint/engineering, + ), + "Cargo Bay" = list( + /area/station/cargo, + /area/station/command/heads_quarters/qm, + /area/station/security/checkpoint/supply, + ), +)) + +/// Basically just exists to hold references to datums so that they don't GC +SUBSYSTEM_DEF(battle_royale) + name = "Battle Royale" + flags = SS_NO_INIT | SS_NO_FIRE + /// List of battle royale datums currently running + var/list/active_battles + +/// Start a new battle royale using a passed list of implants +/datum/controller/subsystem/battle_royale/proc/start_battle(list/competitors) + var/datum/battle_royale_controller/controller = new() + if (!controller.start(competitors)) + return FALSE + LAZYADD(active_battles, controller) + if (LAZYLEN(active_battles) == 1) + start_broadcasting_network(BATTLE_ROYALE_CAMERA_NET) + RegisterSignal(controller, COMSIG_QDELETING, PROC_REF(battle_ended)) + return TRUE + +/// Drop reference when it kills itself +/datum/controller/subsystem/battle_royale/proc/battle_ended(datum/source) + SIGNAL_HANDLER + LAZYREMOVE(active_battles, source) + if (!LAZYLEN(active_battles)) + stop_broadcasting_network(BATTLE_ROYALE_CAMERA_NET) + + +/// Datum which controls the conflict +/datum/battle_royale_controller + /// Where is our battle taking place? + var/chosen_area + /// Is the battle currently in progress? + var/battle_running = TRUE + /// Should we let everyone know that someone has died? + var/announce_deaths = TRUE + /// List of implants involved + var/list/contestant_implants = list() + /// Ways to describe that someone has died + var/static/list/euphemisms = list( + "cashed their last paycheque.", + "didn't make it...", + "didn't make the cut.", + "had their head blown clean off!", + "has been killed!", + "has failed the challenge!", + "has passed away.", + "has died.", + "is in a better place now.", + "isn't going to be clocking in tomorrow!", + "just flatlined.", + "isn't today's winner.", + "seems to have exploded!", + "was just murdered on live tv!", + "won't be making it to retirement.", + "won't be getting back up after that one.", + ) + /// Ways to tell people not to salt in deadchat, surely effective + var/static/list/condolences = list( + "Better luck next time!", + "But stay tuned, there's still everything to play for!", + "Did you catch who did it?", + "It looked like that one really hurt...", + "Let's get that one on action replay!", + "Let's have a moment of silence, please.", + "Let's hope the next one does better.", + "Someone please notify their next of kin.", + "They had a good run.", + "Too bad!", + "What a shame!", + "What an upset!", + "What's going to happen next?", + "Who could have seen that coming?", + "Who will be next?", + ) + +/datum/battle_royale_controller/Destroy(force) + contestant_implants = null + return ..() + +/// Start a battle royale with the list of provided implants +/datum/battle_royale_controller/proc/start(list/implants, battle_time = 10 MINUTES) + chosen_area = pick(GLOB.battle_royale_regions) + for (var/obj/item/implant/explosive/battle_royale/contestant_implant in implants) + contestant_implant.start_battle(chosen_area, GLOB.battle_royale_regions[chosen_area]) + if (isnull(contestant_implant)) + continue // Might have exploded if it was removed from a person + RegisterSignal(contestant_implant, COMSIG_QDELETING, PROC_REF(implant_destroyed)) + contestant_implants |= contestant_implant + + if (length(contestant_implants) <= 1) + return FALSE // Well there's not much point is there + + priority_announce( + text = "Congratulations [station_name()], you have been chosen as the next site of the Rumble Royale! \n\ + Viewers across the sector will watch our [convert_integer_to_words(length(contestant_implants))] lucky contestants battle their way into your [chosen_area] and fight until only one is left standing! \n\ + If they don't make it in five minutes, they'll be disqualified. If you see one of our players struggling to get in, do lend them a hand... or don't, if you can live with the consequences! \n\ + As a gesture of gratitude, we will be providing our premium broadcast to your entertainment monitors at no cost so that you can watch the excitement. \n\ + Bystanders are advised not to intervene... but if you do, make it look good for the camera!", + title = "Rumble Royale Beginning", + sound = 'sound/machines/alarm.ogg', + has_important_message = TRUE, + sender_override = "Rumble Royale Pirate Broadcast Station", + color_override = "red", + ) + + for (var/obj/item/implant/explosive/battle_royale/contestant_implant as anything in contestant_implants) + contestant_implant.announce() + addtimer(CALLBACK(src, PROC_REF(limit_area)), battle_time / 2, TIMER_DELETE_ME) + addtimer(CALLBACK(src, PROC_REF(finish)), battle_time, TIMER_DELETE_ME) + return TRUE + +/// An implant was destroyed, hopefully because it exploded. Count how many competitors remain. +/datum/battle_royale_controller/proc/implant_destroyed(obj/item/implant/implant) + SIGNAL_HANDLER + contestant_implants -= implant + if (!battle_running) + return + + if (length(contestant_implants) <= 1) + announce_winner(implant) + else if (announce_deaths) + var/message = "" + if (isnull(implant.imp_in)) + message = "Looks like someone removed and destroyed their implant, that's cheating!" + else + message = "[implant.imp_in.real_name] [pick(euphemisms)] [pick(condolences)]" + priority_announce( + text = message, + title = "Rumble Royale Casualty Report", + sound = 'sound/misc/notice1.ogg', + has_important_message = TRUE, + sender_override = "Rumble Royale Pirate Broadcast Station", + color_override = "red", + ) + +/// There's only one person left, we have a winner! +/datum/battle_royale_controller/proc/announce_winner(obj/item/implant/losing_implant) + battle_running = FALSE + if (length(contestant_implants) > 1) + return + + var/message = "" + var/mob/living/loser = losing_implant.imp_in + var/obj/item/implant/winning_implant = pop(contestant_implants) + var/mob/living/winner = winning_implant?.imp_in + + if (isnull(winner) && isnull(loser)) + message = "Somehow, it seems like there's no winner tonight. What a disappointment!" + else + var/loser_text = isnull(loser) ? "With the disqualification of the other remaining contestant" : "With the death of [loser.real_name]" + var/winner_text = isnull(winner) ? "we must sadly announce that the would-be winner has also been disqualified. Such bad showmanship!" : "only [winner.real_name] remains. Congratulations, we have a winner!" + message = "[loser_text], [winner_text]" + + if (!isnull(winner)) + podspawn(list( + "target" = get_turf(winner), + "style" = STYLE_SYNDICATE, + "spawn" = /obj/item/food/roast_dinner, + )) + + priority_announce( + text = message, + title = "Rumble Royale Winner", + sound = 'sound/misc/notice1.ogg', + has_important_message = TRUE, + sender_override = "Rumble Royale Pirate Broadcast Station", + color_override = "red", + ) + + qdel(winning_implant) // You get to live! + winner?.mind?.remove_antag_datum(/datum/antagonist/survivalist/battle_royale) + qdel(src) + +/// Called halfway through the battle, if you've not made it to the designated battle zone we kill you +/datum/battle_royale_controller/proc/limit_area() + priority_announce( + text = "We're halfway done folks! And bad news to anyone who hasn't made it to the [chosen_area]... you're out!", + title = "Rumble Royale Update", + sound = 'sound/misc/notice1.ogg', + has_important_message = TRUE, + sender_override = "Rumble Royale Pirate Broadcast Station", + color_override = "red", + ) + + for (var/obj/item/implant/explosive/battle_royale/contestant_implant as anything in contestant_implants) + contestant_implant.limit_areas() + +/// Well you're out of time, bad luck +/datum/battle_royale_controller/proc/finish() + battle_running = FALSE + + priority_announce( + text = "Sorry remaining contestants, your time is up. \ + We're sorry to announce that this edition of Royal Rumble has no winner. \n\ + Better luck next time!", + title = "Rumble Royale Concluded", + sound = 'sound/misc/notice1.ogg', + has_important_message = TRUE, + sender_override = "Rumble Royale Pirate Broadcast Station", + color_override = "red", + ) + + for (var/obj/item/implant/explosive/battle_royale/contestant_implant as anything in contestant_implants) + contestant_implant.explode() + + qdel(src) diff --git a/code/datums/components/simple_bodycam.dm b/code/datums/components/simple_bodycam.dm index 81deb50649d..9d653f38a78 100644 --- a/code/datums/components/simple_bodycam.dm +++ b/code/datums/components/simple_bodycam.dm @@ -65,4 +65,5 @@ /datum/component/simple_bodycam/proc/camera_gone(datum/source) SIGNAL_HANDLER - qdel(src) + if (!QDELETED(src)) + qdel(src) diff --git a/code/datums/diseases/advance/symptoms/itching.dm b/code/datums/diseases/advance/symptoms/itching.dm index cb468a132cc..7615edb1b82 100644 --- a/code/datums/diseases/advance/symptoms/itching.dm +++ b/code/datums/diseases/advance/symptoms/itching.dm @@ -27,6 +27,7 @@ COOLDOWN_DECLARE(itching_cooldown) ///if FALSE, there is a percentage chance that the mob will emote scratching while itching_cooldown is on cooldown. If TRUE, won't emote again until after the off cooldown scratch occurs. var/off_cooldown_scratched = FALSE + /datum/symptom/itching/Start(datum/disease/advance/active_disease) . = ..() if(!.) @@ -41,17 +42,12 @@ . = ..() if(!.) return - var/mob/living/carbon/affected_mob = active_disease.affected_mob - var/obj/item/bodypart/bodypart = affected_mob.get_bodypart(affected_mob.get_random_valid_zone(even_weights = TRUE)) - if(bodypart && IS_ORGANIC_LIMB(bodypart) && !(bodypart.bodypart_flags & BODYPART_PSEUDOPART)) //robotic limbs will mean less scratching overall (why are golems able to damage themselves with self-scratching, but not androids? the world may never know) - var/can_scratch = scratch && !affected_mob.incapacitated() - if(can_scratch) - bodypart.receive_damage(0.5) - //below handles emotes, limiting the emote of emotes passed to chat - if(COOLDOWN_FINISHED(src, itching_cooldown) || !COOLDOWN_FINISHED(src, itching_cooldown) && prob(60) && !off_cooldown_scratched) - affected_mob.visible_message("[can_scratch ? span_warning("[affected_mob] scratches [affected_mob.p_their()] [bodypart.plaintext_zone].") : ""]", span_warning("Your [bodypart.plaintext_zone] itches. [can_scratch ? " You scratch it." : ""]")) - COOLDOWN_START(src, itching_cooldown, 5 SECONDS) - if(!off_cooldown_scratched && !COOLDOWN_FINISHED(src, itching_cooldown)) - off_cooldown_scratched = TRUE - else - off_cooldown_scratched = FALSE + + var/announce_scratch = COOLDOWN_FINISHED(src, itching_cooldown) || (!COOLDOWN_FINISHED(src, itching_cooldown) && prob(60) && !off_cooldown_scratched) + if (!active_disease.affected_mob.itch(silent = !announce_scratch, can_scratch = scratch) || !announce_scratch) + return + COOLDOWN_START(src, itching_cooldown, 5 SECONDS) + if(!off_cooldown_scratched && !COOLDOWN_FINISHED(src, itching_cooldown)) + off_cooldown_scratched = TRUE + else + off_cooldown_scratched = FALSE diff --git a/code/game/objects/items/devices/battle_royale.dm b/code/game/objects/items/devices/battle_royale.dm new file mode 100644 index 00000000000..b4ea15c6de7 --- /dev/null +++ b/code/game/objects/items/devices/battle_royale.dm @@ -0,0 +1,117 @@ +/// Quietly implants people with battle royale implants +/obj/item/royale_implanter + name = "royale implanter" + desc = "Subtly implants people with rumble royale implants, \ + preparing them to struggle for their life for the enjoyment of the Syndicate's paying audience. \ + Implants may cause irritation at site of implantation." + icon = 'icons/obj/medical/syringe.dmi' + icon_state = "nanite_hypo" + w_class = WEIGHT_CLASS_SMALL + /// Do we have a linked remote? Just to prevent headdesk moments + var/linked = FALSE + +/obj/item/royale_implanter/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers) + if(!isliving(interacting_with)) + if (!istype(interacting_with, /obj/item/royale_remote)) + return NONE + var/obj/item/royale_remote/remote = interacting_with + remote.link_implanter(src, user) + return ITEM_INTERACT_SUCCESS + if (!linked) + balloon_alert(user, "no linked remote!") + return ITEM_INTERACT_BLOCKING + if (DOING_INTERACTION_WITH_TARGET(user, interacting_with)) + balloon_alert(user, "busy!") + return ITEM_INTERACT_BLOCKING + var/mob/living/potential_winner = interacting_with + if (potential_winner.stat != CONSCIOUS) + balloon_alert(user, "target unconscious!") + return ITEM_INTERACT_BLOCKING + if (!potential_winner.mind) + balloon_alert(user, "target too boring!") + return ITEM_INTERACT_BLOCKING + log_combat(user, potential_winner, "tried to implant a battle royale implant into") + if (!do_after(user, 1.5 SECONDS, potential_winner)) + balloon_alert(user, "interrupted!") + return ITEM_INTERACT_BLOCKING + + var/obj/item/implant/explosive/battle_royale/encouragement_implant = new + if(!encouragement_implant.implant(potential_winner, user)) + qdel(encouragement_implant) // no balloon alert - feedback is usually provided by the implant + return ITEM_INTERACT_BLOCKING + + potential_winner.balloon_alert(user, "implanted") + SEND_SIGNAL(src, COMSIG_ROYALE_IMPLANTED, encouragement_implant) + return ITEM_INTERACT_SUCCESS + +/// Activates implants implanted by linked royale implanter +/obj/item/royale_remote + name = "royale remote" + desc = "A single use device which will activate any linked rumble royale implants, starting the show." + icon = 'icons/obj/devices/remote.dmi' + icon_state = "designator_syndicate" + w_class = WEIGHT_CLASS_SMALL + /// Minimum number of contestants we should have + var/required_contestants = 6 + /// List of implanters we are linked to + var/list/linked_implanters = list() + /// List of implants of lucky contestants + var/list/implanted_implants = list() + +/obj/item/royale_remote/Destroy(force) + linked_implanters = null + implanted_implants = null + return ..() + +/obj/item/royale_remote/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers) + if (!istype(interacting_with, /obj/item/royale_implanter)) + return NONE + link_implanter(interacting_with) + return ITEM_INTERACT_SUCCESS + +/obj/item/royale_remote/attack_self(mob/user, modifiers) + . = ..() + if (.) + return + var/contestant_count = length(implanted_implants) + if (contestant_count < required_contestants) + balloon_alert(user, "[required_contestants - contestant_count] contestants needed!") + return + + SSbattle_royale.start_battle(implanted_implants) + + for (var/obj/implanter as anything in linked_implanters) + do_sparks(3, cardinal_only = FALSE, source = implanter) + qdel(implanter) + do_sparks(3, cardinal_only = FALSE, source = src) + qdel(src) + +/// Link to an implanter +/obj/item/royale_remote/proc/link_implanter(obj/item/royale_implanter/implanter, mob/user) + if (implanter in linked_implanters) + if (user) + balloon_alert(user, "already linked!") + return + + if (user) + balloon_alert(user, "link established") + + implanter.linked = TRUE + linked_implanters += implanter + RegisterSignal(implanter, COMSIG_ROYALE_IMPLANTED, PROC_REF(record_contestant)) + RegisterSignal(implanter, COMSIG_QDELETING, PROC_REF(implanter_destroyed)) + +/// Record that someone just got implanted +/obj/item/royale_remote/proc/record_contestant(obj/item/implanter, obj/item/implant) + SIGNAL_HANDLER + implanted_implants |= implant + RegisterSignal(implant, COMSIG_QDELETING, PROC_REF(implant_destroyed)) + +/// A linked implanter was destroyed +/obj/item/royale_remote/proc/implanter_destroyed(obj/item/implanter) + SIGNAL_HANDLER + linked_implanters -= implanter + +/obj/item/royale_remote/proc/implant_destroyed(obj/item/implant) + SIGNAL_HANDLER + implanted_implants -= implant diff --git a/code/game/objects/items/implants/implant.dm b/code/game/objects/items/implants/implant.dm index cc64788e597..4541bbdbdad 100644 --- a/code/game/objects/items/implants/implant.dm +++ b/code/game/objects/items/implants/implant.dm @@ -119,7 +119,7 @@ * Arguments: * * mob/living/source - What the implant is being removed from * * silent - unused here - * * special - unused here + * * special - Set to true if removed by admin panel, should bypass any side effects */ /obj/item/implant/proc/removed(mob/living/source, silent = FALSE, special = 0) moveToNullspace() diff --git a/code/game/objects/items/implants/implant_battle_royale.dm b/code/game/objects/items/implants/implant_battle_royale.dm new file mode 100644 index 00000000000..11f68ec9345 --- /dev/null +++ b/code/game/objects/items/implants/implant_battle_royale.dm @@ -0,0 +1,142 @@ + +/// Implant used by the traitor Battle Royale objective, is not active immediately +/obj/item/implant/explosive/battle_royale + name = "rumble royale implant" + actions_types = null + instant_explosion = FALSE + master_implant = TRUE + delay = 10 SECONDS + panic_beep_sound = TRUE + announce_activation = FALSE + /// Where is this going to tell us to go to avoid death? + var/target_area_name = "" + /// Is this implant active yet? + var/battle_started = FALSE + /// Are we enforcing a specific area yet? + var/area_limited = FALSE + /// Are we presently exploding? + var/has_exploded = FALSE + /// How likely are we to blow up if removed? + var/removed_explode_chance = 70 + /// Reference to our applied camera component + var/camera + /// We will explode if we're not in here after a set time + var/list/limited_areas = list() + +/obj/item/implant/explosive/battle_royale/get_data() + return "Implant Specifications:
\ + Name: Donk Co. 'Rumble Royale' Contestant Motivation Implant
\ + Life: Activates upon death, or expiry of an internal timer.
\ + Important Notes: Explodes.
\ +
\ + Implant Details:
\ + Function: Contains a compact, electrically detonated explosive that detonates upon receiving a specially encoded signal or upon host death. \ + Upon triggering the timer, the implant will begin to broadcast the surrounding area for the purposes of televised entertainment. This signal can be detected by GPS trackers.
\ + Special Features: Exploding.
" + +/obj/item/implant/explosive/battle_royale/on_death(datum/source, gibbed) + if (!battle_started) + return + return ..() + +/obj/item/implant/explosive/battle_royale/implant(mob/living/target, mob/user, silent, force) + . = ..() + if (!.) + return + RegisterSignal(target, COMSIG_LIVING_LIFE, PROC_REF(on_life)) + if (!battle_started) + return + name = "[name] - [imp_in.real_name]" + camera = target.AddComponent( \ + /datum/component/simple_bodycam, \ + camera_name = "rumble royale tracker", \ + c_tag = "Competitor [target.real_name]", \ + network = BATTLE_ROYALE_CAMERA_NET, \ + emp_proof = TRUE, \ + ) + announce() + if (area_limited) + limit_areas() + +/obj/item/implant/explosive/battle_royale/removed(mob/target, silent, special) + . = ..() + UnregisterSignal(target, list(COMSIG_LIVING_LIFE, COMSIG_ENTER_AREA)) + QDEL_NULL(camera) + if (has_exploded || QDELETED(src)) + return + if (!special && prob(removed_explode_chance)) + target.visible_message(span_boldwarning("[src] beeps ominously.")) + playsound(loc, 'sound/items/timer.ogg', 50, vary = FALSE) + explode(target) + target?.mind?.remove_antag_datum(/datum/antagonist/survivalist/battle_royale) + +/obj/item/implant/explosive/battle_royale/emp_act(severity) + removed_explode_chance = rand(0, 100) + return ..() + +/obj/item/implant/explosive/battle_royale/explode(atom/override_explode_target = null) + has_exploded = TRUE + return ..() + +/// Give a slight tell +/obj/item/implant/explosive/battle_royale/proc/on_life(mob/living/source) + SIGNAL_HANDLER + if (prob(98)) + return + if (!source.itch() || prob(80)) + return + to_chat(source, span_boldwarning("You feel a lump which shouldn't be there.")) + +/// Start the battle royale +/obj/item/implant/explosive/battle_royale/proc/start_battle(target_area_name, list/limited_areas) + if (isnull(imp_in)) + explode() + return + src.target_area_name = target_area_name + src.limited_areas = limited_areas + battle_started = TRUE + name = "[name] - [imp_in.real_name]" + imp_in.AddComponent( \ + /datum/component/simple_bodycam, \ + camera_name = "rumble royale tracker", \ + c_tag = "Competitor [imp_in.real_name]", \ + network = BATTLE_ROYALE_CAMERA_NET, \ + emp_proof = TRUE, \ + ) + AddComponent(/datum/component/gps, "Rumble Royale - [imp_in.real_name]") + playsound(loc, 'sound/items/timer.ogg', 50, vary = FALSE) + +/// Limit the owner to the specified area +/obj/item/implant/explosive/battle_royale/proc/limit_areas() + if (isnull(imp_in)) + explode() + return + area_limited = TRUE + RegisterSignal(imp_in, COMSIG_ENTER_AREA, PROC_REF(check_area)) + check_area(imp_in) + +/// Called when our implantee moves somewhere +/obj/item/implant/explosive/battle_royale/proc/check_area(mob/living/source) + SIGNAL_HANDLER + if (!length(limited_areas)) + return + if (is_type_in_list(get_area(source), limited_areas)) + return + playsound(imp_in, 'sound/items/timer.ogg', 50, vary = FALSE) + to_chat(imp_in, span_boldwarning("You are out of bounds! Get to the [target_area_name] quickly!")) + addtimer(CALLBACK(src, PROC_REF(check_area_deadly)), 5 SECONDS, TIMER_DELETE_ME) + +/// After a grace period they're still out of bounds, killing time +/obj/item/implant/explosive/battle_royale/proc/check_area_deadly() + if (isnull(imp_in) || has_exploded) + return + var/area/our_area = get_area(imp_in) + if (is_type_in_list(our_area, limited_areas)) + return + log_combat(src, imp_in, "exploded due to out of bounds", addition = "target area was [target_area_name], area was [our_area]") + explode() + +/// Add the antag datum to our new contestant, also printing some flavour text +/obj/item/implant/explosive/battle_royale/proc/announce() + var/datum/antagonist/survivalist/battle_royale/royale = imp_in.mind?.add_antag_datum(/datum/antagonist/survivalist/battle_royale) + royale?.set_target_area(target_area_name) diff --git a/code/game/objects/items/implants/implant_explosive.dm b/code/game/objects/items/implants/implant_explosive.dm index 51564799f6e..687b8db014e 100644 --- a/code/game/objects/items/implants/implant_explosive.dm +++ b/code/game/objects/items/implants/implant_explosive.dm @@ -37,6 +37,8 @@ var/master_implant = FALSE ///Will this implant notify ghosts when activated? var/notify_ghosts = TRUE + ///Do we tell people when they activated it? + var/announce_activation = TRUE /obj/item/implant/explosive/proc/on_death(datum/source, gibbed) SIGNAL_HANDLER @@ -71,7 +73,8 @@ return FALSE if(cause == "death" && HAS_TRAIT(imp_in, TRAIT_PREVENT_IMPLANT_AUTO_EXPLOSION)) return FALSE - to_chat(imp_in, span_notice("You activate your [name].")) + if(announce_activation) + to_chat(imp_in, span_notice("You activate your [name].")) active = TRUE var/turf/boomturf = get_turf(imp_in) message_admins("[ADMIN_LOOKUPFLW(imp_in)] has activated their [name] at [ADMIN_VERBOSEJMP(boomturf)], with cause of [cause].") @@ -86,7 +89,7 @@ if(istype(target_implant, /obj/item/implant/explosive)) //we don't use our own type here, because macrobombs inherit this proc and need to be able to upgrade microbombs var/obj/item/implant/explosive/other_implant = target_implant if(other_implant.master_implant && master_implant) //we cant have two master implants at once - target.balloon_alert(target, "cannot fit implant!") + target.balloon_alert(user, "cannot fit implant!") return FALSE if(master_implant) merge_implants(src, other_implant) @@ -120,17 +123,19 @@ * Make the implantee beep a few times, keel over and explode. Usually to a devastating effect. */ /obj/item/implant/explosive/proc/timed_explosion() - imp_in.visible_message(span_warning("[imp_in] starts beeping ominously!")) - - if(notify_ghosts) - notify_ghosts( - "[imp_in] is about to detonate their explosive implant!", - source = src, - header = "Tick Tick Tick...", - notify_flags = NOTIFY_CATEGORY_NOFLASH, - ghost_sound = 'sound/machines/warning-buzzer.ogg', - notify_volume = 75, - ) + if (isnull(imp_in)) + visible_message(span_warning("[src] starts beeping ominously!")) + else + imp_in.visible_message(span_warning("[imp_in] starts beeping ominously!")) + if(notify_ghosts) + notify_ghosts( + "[imp_in] is about to detonate their explosive implant!", + source = src, + header = "Tick Tick Tick...", + notify_flags = NOTIFY_CATEGORY_NOFLASH, + ghost_sound = 'sound/machines/warning-buzzer.ogg', + notify_volume = 75, + ) playsound(loc, 'sound/items/timer.ogg', 30, FALSE) if(!panic_beep_sound) @@ -160,14 +165,15 @@ ///When called, just explodes -/obj/item/implant/explosive/proc/explode() +/obj/item/implant/explosive/proc/explode(atom/override_explode_target = null) explosion_devastate = round(explosion_devastate) explosion_heavy = round(explosion_heavy) explosion_light = round(explosion_light) - explosion(src, devastation_range = explosion_devastate, heavy_impact_range = explosion_heavy, light_impact_range = explosion_light, flame_range = explosion_light, flash_range = explosion_light, explosion_cause = src) - if(imp_in) - imp_in.investigate_log("has been gibbed by an explosive implant.", INVESTIGATE_DEATHS) - imp_in.gib(DROP_ORGANS|DROP_BODYPARTS) + explosion(override_explode_target || src, devastation_range = explosion_devastate, heavy_impact_range = explosion_heavy, light_impact_range = explosion_light, flame_range = explosion_light, flash_range = explosion_light, explosion_cause = src) + var/mob/living/kill_mob = isliving(override_explode_target) ? override_explode_target : imp_in + if(!isnull(kill_mob)) + kill_mob.investigate_log("has been gibbed by an explosive implant.", INVESTIGATE_DEATHS) + kill_mob.gib(DROP_ORGANS|DROP_BODYPARTS) qdel(src) ///Macrobomb has the strength and delay of 10 microbombs diff --git a/code/game/objects/items/storage/boxes/clothes_boxes.dm b/code/game/objects/items/storage/boxes/clothes_boxes.dm index d4d32e974c7..6b4e995a276 100644 --- a/code/game/objects/items/storage/boxes/clothes_boxes.dm +++ b/code/game/objects/items/storage/boxes/clothes_boxes.dm @@ -47,6 +47,15 @@ new /obj/item/clothing/head/syndicatefake(src) new /obj/item/clothing/suit/syndicatefake(src) +/obj/item/storage/box/syndie_kit/battle_royale + name = "rumble royale broadcast kit" + desc = "Contains everything you need to host the galaxy's greatest show; Rumble Royale." + +/obj/item/storage/box/syndie_kit/battle_royale/PopulateContents() + var/obj/item/royale_implanter/implanter = new(src) + var/obj/item/royale_remote/remote = new(src) + remote.link_implanter(implanter) + /obj/item/storage/box/deputy name = "box of deputy armbands" desc = "To be issued to those authorized to act as deputy of security." diff --git a/code/modules/admin/verbs/manipulate_organs.dm b/code/modules/admin/verbs/manipulate_organs.dm index 6ead4d3b137..30f198bfba9 100644 --- a/code/modules/admin/verbs/manipulate_organs.dm +++ b/code/modules/admin/verbs/manipulate_organs.dm @@ -70,7 +70,7 @@ organ_holder.Remove(carbon_victim) else implant_holder = organ_to_modify - implant_holder.removed(carbon_victim) + implant_holder.removed(carbon_victim, special = TRUE) organ_to_modify.forceMove(get_turf(carbon_victim)) diff --git a/code/modules/antagonists/survivalist/survivalist.dm b/code/modules/antagonists/survivalist/survivalist.dm index 2480b186600..b801076747f 100644 --- a/code/modules/antagonists/survivalist/survivalist.dm +++ b/code/modules/antagonists/survivalist/survivalist.dm @@ -3,7 +3,10 @@ show_in_antagpanel = FALSE show_name_in_check_antagonists = TRUE suicide_cry = "FOR MYSELF!!" + /// What do we display when you gain the antag datum? var/greet_message = "" + /// Should we immediately print the objectives? + var/announce_objectives = TRUE /datum/antagonist/survivalist/forge_objectives() var/datum/objective/survive/survive = new @@ -18,7 +21,8 @@ /datum/antagonist/survivalist/greet() . = ..() to_chat(owner, "[greet_message]") - owner.announce_objectives() + if (announce_objectives) + owner.announce_objectives() /datum/antagonist/survivalist/guns greet_message = "Your own safety matters above all else, and the only way to ensure your safety is to stockpile weapons! Grab as many guns as possible, by any means necessary. Kill anyone who gets in your way." @@ -52,3 +56,52 @@ /datum/antagonist/survivalist/magic/on_removal() REMOVE_TRAIT(owner, TRAIT_MAGICALLY_GIFTED, REF(src)) return..() + +/// Applied by the battle royale objective +/datum/antagonist/survivalist/battle_royale + name = "Battle Royale Contestant" + greet_message = "There has to be some way you can make it out of this alive..." + announce_objectives = FALSE + +/datum/antagonist/survivalist/battle_royale/on_gain() + . = ..() + if (isnull(owner.current)) + return + RegisterSignals(owner.current, list(COMSIG_LIVING_DEATH, COMSIG_QDELETING), PROC_REF(on_died)) + +/datum/antagonist/survivalist/battle_royale/greet() + to_chat(owner, span_warning("[span_bold("You hear a tinny voice in your ear: ")] \ + Welcome contestant to Rumble Royale, the galaxy's greatest show! \n\ + You may have already heard our announcement, but we're glad to tell you that you are on live TV! \n\ + Your objective in this contest is simple: Within ten minutes be the last contestant left alive, to win a fabulous prize! \n\ + Your fellow contestants will be hearing this too, so you should grab a GPS quick and get hunting! \n\ + Noncompliance and removal of this implant is not recommended, and remember to smile for the cameras!")) + + return ..() + +/datum/antagonist/survivalist/battle_royale/on_removal() + if (isnull(owner.current)) + return ..() + UnregisterSignal(owner.current, list(COMSIG_LIVING_DEATH, COMSIG_QDELETING)) + if (owner.current.stat == DEAD) + return ..() + to_chat(owner, span_notice("Your body is flooded with relief. Against all the odds, you've made it out alive.")) + owner.current?.mob_mood.add_mood_event("battle_royale", /datum/mood_event/royale_survivor) + return ..() + +/// Add an objective to go to a specific place. +/datum/antagonist/survivalist/battle_royale/proc/set_target_area(target_area_name) + var/datum/objective/custom/travel = new + travel.owner = owner + travel.explanation_text = "Reach the [target_area_name] before time runs out." + objectives.Insert(1, travel) + owner.announce_objectives() + +/// Called if you fail to survive. +/datum/antagonist/survivalist/battle_royale/proc/on_died() + SIGNAL_HANDLER + owner.remove_antag_datum(type) + +/datum/mood_event/royale_survivor + description = "I made it out of Rumble Royale with my life." + mood_change = 4 diff --git a/code/modules/antagonists/traitor/objectives/final_objective/battle_royale.dm b/code/modules/antagonists/traitor/objectives/final_objective/battle_royale.dm new file mode 100644 index 00000000000..9b8f519da95 --- /dev/null +++ b/code/modules/antagonists/traitor/objectives/final_objective/battle_royale.dm @@ -0,0 +1,43 @@ +/datum/traitor_objective/ultimate/battle_royale + name = "Implant crewmembers with a subtle implant, then make them fight to the death on pay-per-view TV." + description = "Go to %AREA%, and receive the Royale Broadcast Kit. \ + Use the contained implant on station personnel to subtly implant them with a micro-explosive. \ + Once you have at least six contestants, use the contained remote to start a timer and begin broadcasting live. \ + If more than one contestant remains alive after ten minutes, all of the implants will detonate." + + ///Area type the objective owner must be in to receive the tools. + var/area/kit_spawn_area + ///Whether the kit was sent already. + var/equipped = FALSE + +/datum/traitor_objective/ultimate/battle_royale/generate_objective(datum/mind/generating_for, list/possible_duplicates) + var/list/possible_areas = GLOB.the_station_areas.Copy() + for(var/area/possible_area as anything in possible_areas) + if(ispath(possible_area, /area/station/hallway) || ispath(possible_area, /area/station/security)) + possible_areas -= possible_area + if(length(possible_areas) == 0) + return FALSE + kit_spawn_area = pick(possible_areas) + replace_in_name("%AREA%", initial(kit_spawn_area.name)) + return TRUE + +/datum/traitor_objective/ultimate/battle_royale/generate_ui_buttons(mob/user) + var/list/buttons = list() + if(!equipped) + buttons += add_ui_button("", "Pressing this will call down a pod with the Royale Broadcast kit.", "biohazard", "deliver_kit") + return buttons + +/datum/traitor_objective/ultimate/battle_royale/ui_perform_action(mob/living/user, action) + . = ..() + if(action != "deliver_kit" || equipped) + return + var/area/delivery_area = get_area(user) + if(delivery_area.type != kit_spawn_area) + to_chat(user, span_warning("You must be in [initial(kit_spawn_area.name)] to receive the Royale Broadcast kit.")) + return + equipped = TRUE + podspawn(list( + "target" = get_turf(user), + "style" = STYLE_SYNDICATE, + "spawn" = /obj/item/storage/box/syndie_kit/battle_royale, + )) diff --git a/code/modules/antagonists/traitor/objectives/final_objective/final_objective.dm b/code/modules/antagonists/traitor/objectives/final_objective/final_objective.dm index 6d8bd27f9c7..cb9f4ac73aa 100644 --- a/code/modules/antagonists/traitor/objectives/final_objective/final_objective.dm +++ b/code/modules/antagonists/traitor/objectives/final_objective/final_objective.dm @@ -1,11 +1,12 @@ /datum/traitor_objective_category/final_objective name = "Final Objective" objectives = list( - /datum/traitor_objective/ultimate/romerol = 1, /datum/traitor_objective/ultimate/battlecruiser = 1, - /datum/traitor_objective/ultimate/supermatter_cascade = 1, - /datum/traitor_objective/ultimate/infect_ai = 1, + /datum/traitor_objective/ultimate/battle_royale = 1, /datum/traitor_objective/ultimate/dark_matteor = 1, + /datum/traitor_objective/ultimate/infect_ai = 1, + /datum/traitor_objective/ultimate/romerol = 1, + /datum/traitor_objective/ultimate/supermatter_cascade = 1, ) weight = 100 diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index 53d867cd148..c9c2bc53699 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -1479,3 +1479,10 @@ if(!unwagged) return FALSE return unwagged.stop_wag(src) + +/mob/living/carbon/itch(obj/item/bodypart/target_part = null, damage = 0.5, can_scratch = TRUE, silent = FALSE) + if (isnull(target_part)) + target_part = get_bodypart(get_random_valid_zone(even_weights = TRUE)) + if (!IS_ORGANIC_LIMB(target_part) || (target_part.bodypart_flags & BODYPART_PSEUDOPART)) + return FALSE + return ..() diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 150998a1817..f8a0ddf2900 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1125,6 +1125,20 @@ else return pick("trails_1", "trails_2") +/// Print a message about an annoying sensation you are feeling. Returns TRUE if successful. +/mob/living/proc/itch(obj/item/bodypart/target_part = null, damage = 0.5, can_scratch = TRUE, silent = FALSE) + if ((mob_biotypes & (MOB_ROBOTIC | MOB_SPIRIT))) + return FALSE + var/will_scratch = can_scratch && !incapacitated() + var/applied_damage = 0 + if (will_scratch && damage) + applied_damage = apply_damage(damage, damagetype = BRUTE, def_zone = target_part) + if (silent) + return applied_damage > 0 + var/visible_part = isnull(target_part) ? "side" : target_part.plaintext_zone + visible_message("[can_scratch ? span_warning("[src] scratches [p_their()] [visible_part].") : ""]", span_warning("Your [visible_part] itches. [can_scratch ? "You scratch it." : ""]")) + return TRUE + /mob/living/experience_pressure_difference(pressure_difference, direction, pressure_resistance_prob_delta = 0) playsound(src, 'sound/effects/space_wind.ogg', 50, TRUE) if(buckled || mob_negates_gravity()) diff --git a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm index 7f316a05f79..bcdee284bd7 100644 --- a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm @@ -753,19 +753,20 @@ chemical_flags = REAGENT_CAN_BE_SYNTHESIZED /datum/reagent/toxin/itching_powder/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) - var/need_mob_update = FALSE - if(SPT_PROB(8, seconds_per_tick)) - to_chat(affected_mob, span_danger("You scratch at your head.")) - need_mob_update += affected_mob.adjustBruteLoss(0.2*REM, FALSE, required_bodytype = affected_bodytype) - if(SPT_PROB(8, seconds_per_tick)) - to_chat(affected_mob, span_danger("You scratch at your leg.")) - need_mob_update += affected_mob.adjustBruteLoss(0.2*REM, FALSE, required_bodytype = affected_bodytype) - if(SPT_PROB(8, seconds_per_tick)) - to_chat(affected_mob, span_danger("You scratch at your arm.")) - need_mob_update += affected_mob.adjustBruteLoss(0.2*REM, FALSE, required_bodytype = affected_bodytype) + var/scratched = FALSE + var/scratch_damage = 0.2 * REM - if(need_mob_update) - . = UPDATE_MOB_HEALTH + var/obj/item/bodypart/head = affected_mob.get_bodypart(BODY_ZONE_HEAD) + if(!isnull(head) && SPT_PROB(8, seconds_per_tick)) + scratched = affected_mob.itch(damage = scratch_damage, target_part = head) + + var/obj/item/bodypart/leg = affected_mob.get_bodypart(pick(BODY_ZONE_L_LEG,BODY_ZONE_R_LEG)) + if(!isnull(leg) && SPT_PROB(8, seconds_per_tick)) + scratched = affected_mob.itch(damage = scratch_damage, target_part = leg, silent = scratched) || scratched + + var/obj/item/bodypart/arm = affected_mob.get_bodypart(pick(BODY_ZONE_L_ARM,BODY_ZONE_R_ARM)) + if(!isnull(arm) && SPT_PROB(8, seconds_per_tick)) + scratched = affected_mob.itch(damage = scratch_damage, target_part = arm, silent = scratched) || scratched if(SPT_PROB(1.5, seconds_per_tick)) holder.add_reagent(/datum/reagent/toxin/histamine,rand(1,3)) diff --git a/code/modules/surgery/implant_removal.dm b/code/modules/surgery/implant_removal.dm index a2b9eb33cbf..66eaf6faf73 100644 --- a/code/modules/surgery/implant_removal.dm +++ b/code/modules/surgery/implant_removal.dm @@ -56,6 +56,9 @@ display_pain(target, "You can feel your [implant.name] pulled out of you!") implant.removed(target) + if (QDELETED(implant)) + return ..() + var/obj/item/implantcase/case for(var/obj/item/implantcase/implant_case in user.held_items) case = implant_case diff --git a/tgstation.dme b/tgstation.dme index 33f9e58a08f..9a7b0be49a0 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -726,6 +726,7 @@ #include "code\controllers\subsystem\atoms.dm" #include "code\controllers\subsystem\augury.dm" #include "code\controllers\subsystem\ban_cache.dm" +#include "code\controllers\subsystem\battle_royale.dm" #include "code\controllers\subsystem\bitrunning.dm" #include "code\controllers\subsystem\blackbox.dm" #include "code\controllers\subsystem\blackmarket.dm" @@ -2451,6 +2452,7 @@ #include "code\game\objects\items\devices\aicard_evil.dm" #include "code\game\objects\items\devices\anomaly_neutralizer.dm" #include "code\game\objects\items\devices\anomaly_releaser.dm" +#include "code\game\objects\items\devices\battle_royale.dm" #include "code\game\objects\items\devices\beacon.dm" #include "code\game\objects\items\devices\chameleonproj.dm" #include "code\game\objects\items\devices\destabilizing_crystal.dm" @@ -2567,6 +2569,7 @@ #include "code\game\objects\items\grenades\syndieminibomb.dm" #include "code\game\objects\items\implants\implant.dm" #include "code\game\objects\items\implants\implant_abductor.dm" +#include "code\game\objects\items\implants\implant_battle_royale.dm" #include "code\game\objects\items\implants\implant_clown.dm" #include "code\game\objects\items\implants\implant_deathrattle.dm" #include "code\game\objects\items\implants\implant_explosive.dm" @@ -3321,6 +3324,7 @@ #include "code\modules\antagonists\traitor\objectives\sleeper_protocol.dm" #include "code\modules\antagonists\traitor\objectives\steal.dm" #include "code\modules\antagonists\traitor\objectives\abstract\target_player.dm" +#include "code\modules\antagonists\traitor\objectives\final_objective\battle_royale.dm" #include "code\modules\antagonists\traitor\objectives\final_objective\battlecruiser.dm" #include "code\modules\antagonists\traitor\objectives\final_objective\final_objective.dm" #include "code\modules\antagonists\traitor\objectives\final_objective\infect_ai.dm"