diff --git a/code/__DEFINES/cinematics.dm b/code/__DEFINES/cinematics.dm deleted file mode 100644 index f994dc82a4b..00000000000 --- a/code/__DEFINES/cinematics.dm +++ /dev/null @@ -1,14 +0,0 @@ -#define CINEMATIC_DEFAULT 1 -#define CINEMATIC_SELFDESTRUCT 2 -#define CINEMATIC_SELFDESTRUCT_MISS 3 -#define CINEMATIC_NUKE_WIN 4 -#define CINEMATIC_NUKE_MISS 5 -#define CINEMATIC_ANNIHILATION 6 -#define CINEMATIC_MALF 7 -#define CINEMATIC_CULT 8 -#define CINEMATIC_NUKE_FAKE 9 -#define CINEMATIC_NUKE_NO_CORE 10 -#define CINEMATIC_NUKE_FAR 11 -#define CINEMATIC_NUKE_CLOWNOP 12 -#define CINEMATIC_CULT_NUKE 13 -#define CINEMATIC_CULT_FAIL 14 diff --git a/code/__DEFINES/cult.dm b/code/__DEFINES/cult.dm index 9a9e0dd3f49..93437e31bb0 100644 --- a/code/__DEFINES/cult.dm +++ b/code/__DEFINES/cult.dm @@ -35,3 +35,11 @@ /// Defines for cult item_dispensers. #define PREVIEW_IMAGE "preview" #define OUTPUT_ITEMS "output" + +/// The global Nar'sie that the cult's summoned +GLOBAL_DATUM(cult_narsie, /obj/narsie) + +// Used in determining which cinematic to play when cult ends +#define CULT_VICTORY_MASS_CONVERSION 2 +#define CULT_FAILURE_NARSIE_KILLED 1 +#define CULT_VICTORY_NUKE 0 diff --git a/code/__DEFINES/machines.dm b/code/__DEFINES/machines.dm index 03c666e7e0a..5d704e243c9 100644 --- a/code/__DEFINES/machines.dm +++ b/code/__DEFINES/machines.dm @@ -127,26 +127,6 @@ #define HYPERTORUS_EMERGENCY 4 // Integrity < 25% #define HYPERTORUS_MELTING 5 // Pretty obvious. -//Nuclear bomb stuff -#define NUKESTATE_INTACT 5 -#define NUKESTATE_UNSCREWED 4 -#define NUKESTATE_PANEL_REMOVED 3 -#define NUKESTATE_WELDED 2 -#define NUKESTATE_CORE_EXPOSED 1 -#define NUKESTATE_CORE_REMOVED 0 - -#define NUKEUI_AWAIT_DISK 0 -#define NUKEUI_AWAIT_CODE 1 -#define NUKEUI_AWAIT_TIMER 2 -#define NUKEUI_AWAIT_ARM 3 -#define NUKEUI_TIMING 4 -#define NUKEUI_EXPLODED 5 - -#define NUKE_OFF_LOCKED 0 -#define NUKE_OFF_UNLOCKED 1 -#define NUKE_ON_TIMING 2 -#define NUKE_ON_EXPLODING 3 - #define MACHINE_NOT_ELECTRIFIED 0 #define MACHINE_ELECTRIFIED_PERMANENT -1 #define MACHINE_DEFAULT_ELECTRIFY_TIME 30 diff --git a/code/__DEFINES/nuclear_bomb.dm b/code/__DEFINES/nuclear_bomb.dm new file mode 100644 index 00000000000..9a0e182547a --- /dev/null +++ b/code/__DEFINES/nuclear_bomb.dm @@ -0,0 +1,28 @@ +// Nuclear bomb de/construction status +#define NUKESTATE_INTACT 5 +#define NUKESTATE_UNSCREWED 4 +#define NUKESTATE_PANEL_REMOVED 3 +#define NUKESTATE_WELDED 2 +#define NUKESTATE_CORE_EXPOSED 1 +#define NUKESTATE_CORE_REMOVED 0 + +// Nuclear bomb UI modes +#define NUKEUI_AWAIT_DISK 0 +#define NUKEUI_AWAIT_CODE 1 +#define NUKEUI_AWAIT_TIMER 2 +#define NUKEUI_AWAIT_ARM 3 +#define NUKEUI_TIMING 4 +#define NUKEUI_EXPLODED 5 + +// Nuclear bomb states +#define NUKE_OFF_LOCKED 0 +#define NUKE_OFF_UNLOCKED 1 +#define NUKE_ON_TIMING 2 +#define NUKE_ON_EXPLODING 3 + +// Nuclear bomb detonation statuses +// These line up with roundend reports +#define DETONATION_HIT_STATION STATION_DESTROYED_NUKE +#define DETONATION_HIT_SYNDIE_BASE NUKE_SYNDICATE_BASE +#define DETONATION_NEAR_MISSED_STATION NUKE_NEAR_MISS +#define DETONATION_MISSED_STATION NUKE_MISS_STATION diff --git a/code/_globalvars/game_modes.dm b/code/_globalvars/game_modes.dm index baac9e3f59d..893d5de66ce 100644 --- a/code/_globalvars/game_modes.dm +++ b/code/_globalvars/game_modes.dm @@ -5,9 +5,5 @@ GLOBAL_VAR(survivor_report) //Contains shared survivor report for roundend repor GLOBAL_VAR_INIT(wavesecret, 0) // meteor mode, delays wave progression, terrible name GLOBAL_DATUM(start_state, /datum/station_state) // Used in round-end report - -//TODO clear this one up too -GLOBAL_DATUM(cult_narsie, /obj/narsie) - /// We want reality_smash_tracker to exist only once and be accessible from anywhere. GLOBAL_DATUM_INIT(reality_smash_track, /datum/reality_smash_tracker, new) diff --git a/code/datums/cinematic.dm b/code/datums/cinematic.dm deleted file mode 100644 index eae91a847c2..00000000000 --- a/code/datums/cinematic.dm +++ /dev/null @@ -1,287 +0,0 @@ -// Use to play cinematics. -// Watcher can be world,mob, or a list of mobs -// Blocks until sequence is done. -/proc/Cinematic(id,watcher,datum/callback/special_callback) - var/datum/cinematic/playing - for(var/V in subtypesof(/datum/cinematic)) - var/datum/cinematic/C = V - if(initial(C.id) == id) - playing = new V() - break - if(!playing) - CRASH("Cinematic type not found") - if(special_callback) - playing.special_callback = special_callback - if(watcher == world) - playing.is_global = TRUE - watcher = GLOB.mob_list - playing.play(watcher) - qdel(playing) - -/atom/movable/screen/cinematic - icon = 'icons/effects/station_explosion.dmi' - icon_state = "station_intact" - plane = SPLASHSCREEN_PLANE - mouse_opacity = MOUSE_OPACITY_TRANSPARENT - screen_loc = "BOTTOM,LEFT+50%" - appearance_flags = APPEARANCE_UI | TILE_BOUND - -/datum/cinematic - var/id = CINEMATIC_DEFAULT - var/list/watching = list() //List of clients watching this - var/list/locked = list() //Who had notransform set during the cinematic - var/is_global = FALSE //Global cinematics will override mob-specific ones - var/atom/movable/screen/cinematic/screen - var/datum/callback/special_callback //For special effects synced with animation (explosions after the countdown etc) - var/cleanup_time = 300 //How long for the final screen to remain - var/stop_ooc = TRUE //Turns off ooc when played globally. - -/datum/cinematic/New() - screen = new(src) - -/datum/cinematic/Destroy() - for(var/CC in watching) - if(!CC) - continue - var/client/C = CC - C.mob.clear_fullscreen("cinematic") - C.screen -= screen - watching = null - QDEL_NULL(screen) - QDEL_NULL(special_callback) - for(var/MM in locked) - if(!MM) - continue - var/mob/M = MM - M.notransform = FALSE - locked = null - return ..() - -/datum/cinematic/proc/play(watchers) - //Check if cinematic can actually play (stop mob cinematics for global ones) - if(SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PLAY_CINEMATIC, src) & COMPONENT_GLOB_BLOCK_CINEMATIC) - return - - //We are now playing this cinematic - - //Handle what happens when a different cinematic tries to play over us - RegisterSignal(SSdcs, COMSIG_GLOB_PLAY_CINEMATIC, .proc/replacement_cinematic) - - //Pause OOC - var/ooc_toggled = FALSE - if(is_global && stop_ooc && GLOB.ooc_allowed) - ooc_toggled = TRUE - toggle_ooc(FALSE) - - //Place /atom/movable/screen/cinematic into everyone's screens, prevent them from moving - for(var/MM in watchers) - var/mob/M = MM - show_to(M, M.client) - RegisterSignal(M, COMSIG_MOB_CLIENT_LOGIN, .proc/show_to) - //Close watcher ui's - SStgui.close_user_uis(M) - - //Actually play it - content() - - //Cleanup - sleep(cleanup_time) - - //Restore OOC - if(ooc_toggled) - toggle_ooc(TRUE) - -/datum/cinematic/proc/show_to(mob/M, client/C) - SIGNAL_HANDLER - - if(!M.notransform) - locked += M - M.notransform = TRUE //Should this be done for non-global cinematics or even at all ? - if(!C) - return - watching += C - M.overlay_fullscreen("cinematic",/atom/movable/screen/fullscreen/cinematic_backdrop) - C.screen += screen - -//Sound helper -/datum/cinematic/proc/cinematic_sound(s) - if(is_global) - SEND_SOUND(world,s) - else - for(var/C in watching) - SEND_SOUND(C,s) - -//Fire up special callback for actual effects synchronized with animation (eg real nuke explosion happens midway) -/datum/cinematic/proc/special() - if(special_callback) - special_callback.Invoke() - -//Actual cinematic goes in here -/datum/cinematic/proc/content() - sleep(50) - -/datum/cinematic/proc/replacement_cinematic(datum/source, datum/cinematic/other) - SIGNAL_HANDLER - - if(!is_global && other.is_global) //Allow it to play if we're local and it's global - return NONE - return COMPONENT_GLOB_BLOCK_CINEMATIC - -/datum/cinematic/nuke_win - id = CINEMATIC_NUKE_WIN - -/datum/cinematic/nuke_win/content() - flick("intro_nuke",screen) - sleep(35) - flick("station_explode_fade_red",screen) - cinematic_sound(sound('sound/effects/explosion_distant.ogg')) - special() - screen.icon_state = "summary_nukewin" - -/datum/cinematic/nuke_miss - id = CINEMATIC_NUKE_MISS - -/datum/cinematic/nuke_miss/content() - flick("intro_nuke",screen) - sleep(35) - cinematic_sound(sound('sound/effects/explosion_distant.ogg')) - special() - flick("station_intact_fade_red",screen) - screen.icon_state = "summary_nukefail" - -//Also used for blob -/datum/cinematic/nuke_selfdestruct - id = CINEMATIC_SELFDESTRUCT - -/datum/cinematic/nuke_selfdestruct/content() - flick("intro_nuke",screen) - sleep(35) - flick("station_explode_fade_red", screen) - cinematic_sound(sound('sound/effects/explosion_distant.ogg')) - special() - screen.icon_state = "summary_selfdes" - -/datum/cinematic/nuke_selfdestruct_miss - id = CINEMATIC_SELFDESTRUCT_MISS - -/datum/cinematic/nuke_selfdestruct_miss/content() - flick("intro_nuke",screen) - sleep(35) - cinematic_sound(sound('sound/effects/explosion_distant.ogg')) - special() - screen.icon_state = "station_intact" - -/datum/cinematic/malf - id = CINEMATIC_MALF - -/datum/cinematic/malf/content() - flick("intro_malf",screen) - sleep(76) - flick("station_explode_fade_red",screen) - cinematic_sound(sound('sound/effects/explosion_distant.ogg')) - special() - screen.icon_state = "summary_malf" - -/datum/cinematic/cult - id = CINEMATIC_CULT - -/datum/cinematic/cult/content() - screen.icon_state = null - flick("intro_cult",screen) - sleep(25) - cinematic_sound(sound('sound/magic/enter_blood.ogg')) - sleep(28) - cinematic_sound(sound('sound/machines/terminal_off.ogg')) - sleep(20) - flick("station_corrupted",screen) - cinematic_sound(sound('sound/effects/ghost.ogg')) - sleep(70) - special() - -/datum/cinematic/cult_nuke - id = CINEMATIC_CULT_NUKE - -/datum/cinematic/cult_nuke/content() - flick("intro_nuke",screen) - sleep(35) - flick("station_explode_fade_red",screen) - cinematic_sound(sound('sound/effects/explosion_distant.ogg')) - special() - screen.icon_state = "summary_cult" - -/datum/cinematic/cult_fail - id = CINEMATIC_CULT_FAIL - -/datum/cinematic/cult_fail/content() - screen.icon_state = "station_intact" - sleep(20) - cinematic_sound(sound('sound/creatures/narsie_rises.ogg')) - sleep(60) - cinematic_sound(sound('sound/effects/explosion_distant.ogg')) - sleep(10) - cinematic_sound(sound('sound/magic/demon_dies.ogg')) - sleep(30) - special() - -/datum/cinematic/nuke_annihilation - id = CINEMATIC_ANNIHILATION - -/datum/cinematic/nuke_annihilation/content() - flick("intro_nuke",screen) - sleep(35) - flick("station_explode_fade_red",screen) - cinematic_sound(sound('sound/effects/explosion_distant.ogg')) - special() - screen.icon_state = "summary_totala" - -/datum/cinematic/fake - id = CINEMATIC_NUKE_FAKE - cleanup_time = 100 - -/datum/cinematic/fake/content() - flick("intro_nuke",screen) - sleep(35) - cinematic_sound(sound('sound/items/bikehorn.ogg')) - flick("summary_selfdes",screen) //??? - special() - -/datum/cinematic/no_core - id = CINEMATIC_NUKE_NO_CORE - cleanup_time = 100 - -/datum/cinematic/no_core/content() - flick("intro_nuke",screen) - sleep(35) - flick("station_intact",screen) - cinematic_sound(sound('sound/ambience/signal.ogg')) - sleep(100) - -/datum/cinematic/nuke_far - id = CINEMATIC_NUKE_FAR - cleanup_time = 0 - -/datum/cinematic/nuke_far/content() - cinematic_sound(sound('sound/effects/explosion_distant.ogg')) - special() - -/datum/cinematic/clownop - id = CINEMATIC_NUKE_CLOWNOP - cleanup_time = 100 - -/datum/cinematic/clownop/content() - flick("intro_nuke",screen) - sleep(35) - cinematic_sound(sound('sound/items/airhorn.ogg')) - flick("summary_selfdes",screen) //??? - special() - -/* Intended usage. -Nuke.Explosion() - -> Cinematic(NUKE_BOOM,world) - -> ActualExplosion() - -> Mode.OnExplosion() - - -Narsie() - -> Cinematic(CULT,world) -*/ diff --git a/code/datums/cinematics/_cinematic.dm b/code/datums/cinematics/_cinematic.dm new file mode 100644 index 00000000000..b8a3585d039 --- /dev/null +++ b/code/datums/cinematics/_cinematic.dm @@ -0,0 +1,148 @@ +/** + * Plays a cinematic, duh. Can be to a select few people, or everyone. + * + * cinematic_type - datum typepath to what cinematic you wish to play. + * watchers - a list of all mobs you are playing the cinematic to. If world, the cinematical will play globally to all players. + * special_callback - optional callback to be invoked mid-cinematic. + */ +/proc/play_cinematic(datum/cinematic/cinematic_type, watchers, datum/callback/special_callback) + if(!ispath(cinematic_type, /datum/cinematic)) + CRASH("play_cinematic called with a non-cinematic type. (Got: [cinematic_type])") + var/datum/cinematic/playing = new cinematic_type(watchers, special_callback) + + if(watchers == world) + watchers = GLOB.mob_list + + playing.start_cinematic(watchers) + + return playing + +/// The cinematic screen showed to everyone +/atom/movable/screen/cinematic + icon = 'icons/effects/station_explosion.dmi' + icon_state = "station_intact" + plane = SPLASHSCREEN_PLANE + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + screen_loc = "BOTTOM,LEFT+50%" + appearance_flags = APPEARANCE_UI | TILE_BOUND + +/// Cinematic datum. Used to show an animation to everyone. +/datum/cinematic + /// A list of all clients watching the cinematic + var/list/client/watching = list() + /// A list of all mobs who have notransform set while watching the cinematic + var/list/datum/weakref/locked = list() + /// Whether the cinematic is a global cinematic or not + var/is_global = FALSE + /// Refernce to the cinematic screen shown to everyohne + var/atom/movable/screen/cinematic/screen + /// Callbacks passed that occur during the animation + var/datum/callback/special_callback + /// How long for the final screen remains shown + var/cleanup_time = 30 SECONDS + /// Whether the cinematic turns off ooc when played globally. + var/stop_ooc = TRUE + +/datum/cinematic/New(watcher, datum/callback/special_callback) + screen = new(src) + if(watcher == world) + is_global = TRUE + + src.special_callback = special_callback + +/datum/cinematic/Destroy() + QDEL_NULL(screen) + QDEL_NULL(special_callback) + watching.Cut() + locked.Cut() + return ..() + +/// Actually goes through the process of showing the cinematic to the list of watchers. +/datum/cinematic/proc/start_cinematic(list/watchers) + if(SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PLAY_CINEMATIC, src) & COMPONENT_GLOB_BLOCK_CINEMATIC) + return + + // Register a signal to handle what happens when a different cinematic tries to play over us. + RegisterSignal(SSdcs, COMSIG_GLOB_PLAY_CINEMATIC, .proc/handle_replacement_cinematics) + + // Pause OOC + var/ooc_toggled = FALSE + if(is_global && stop_ooc && GLOB.ooc_allowed) + ooc_toggled = TRUE + toggle_ooc(FALSE) + + // Place the /atom/movable/screen/cinematic into everyone's screens, prevent them from moving + for(var/mob/watching_mob in watchers) + show_to(watching_mob, GET_CLIENT(watching_mob)) + RegisterSignal(watching_mob, COMSIG_MOB_CLIENT_LOGIN, .proc/show_to) + //Close watcher ui's + SStgui.close_user_uis(watching_mob) + + //Actually play it + play_cinematic() + + //Cleanup + sleep(cleanup_time) + + //Restore OOC + if(ooc_toggled) + toggle_ooc(TRUE) + + stop_cinematic() + +/// Whenever another cinematic starts to play over us, we have the chacne to block it. +/datum/cinematic/proc/handle_replacement_cinematics(datum/source, datum/cinematic/other) + SIGNAL_HANDLER + + // Stop our's and allow others to play if we're local and it's global + if(!is_global && other.is_global) + stop_cinematic() + return NONE + + return COMPONENT_GLOB_BLOCK_CINEMATIC + +/// Whenever a mob watching the cinematic logs in, show them the ongoing cinematic +/datum/cinematic/proc/show_to(mob/watching_mob, client/watching_client) + SIGNAL_HANDLER + + if(!watching_mob.notransform) + locked += WEAKREF(watching_mob) + watching_mob.notransform = TRUE + + if(!watching_client) + return + + watching += watching_client + watching_mob.overlay_fullscreen("cinematic", /atom/movable/screen/fullscreen/cinematic_backdrop) + watching_client.screen += screen + +/// Simple helper for playing sounds from the cinematic. +/datum/cinematic/proc/play_cinematic_sound(sound_to_play) + if(is_global) + SEND_SOUND(world, sound_to_play) + else + for(var/client/watching_client as anything in watching) + SEND_SOUND(watching_client, sound_to_play) + +/// Invoke any special callbacks for actual effects synchronized with animation. +/// (Such as a real nuke explosion happening midway) +/datum/cinematic/proc/invoke_special_callback() + special_callback?.Invoke() + +/// The actual cinematic occurs here. +/datum/cinematic/proc/play_cinematic() + return + +/// Stops the cinematic and removes it from all the viewers. +/datum/cinematic/proc/stop_cinematic() + for(var/client/viewing_client as anything in watching) + viewing_client.mob.clear_fullscreen("cinematic") + viewing_client.screen -= screen + + for(var/datum/weakref/locked_ref as anything in locked) + var/mob/locked_mob = locked_ref.resolve() + if(QDELETED(locked_mob)) + continue + locked_mob.notransform = FALSE + + qdel(src) diff --git a/code/datums/cinematics/malf_doomsday.dm b/code/datums/cinematics/malf_doomsday.dm new file mode 100644 index 00000000000..2eb330d9a48 --- /dev/null +++ b/code/datums/cinematics/malf_doomsday.dm @@ -0,0 +1,10 @@ +/// A malfunctioning AI has activated the doomsday device and wiped the station! +/datum/cinematic/malf + +/datum/cinematic/malf/play_cinematic() + flick("intro_malf", screen) + stoplag(7.6 SECONDS) + flick("station_explode_fade_red", screen) + play_cinematic_sound(sound('sound/effects/explosion_distant.ogg')) + special_callback?.Invoke() + screen.icon_state = "summary_malf" diff --git a/code/datums/cinematics/narsie_summon.dm b/code/datums/cinematics/narsie_summon.dm new file mode 100644 index 00000000000..2fecac2c63a --- /dev/null +++ b/code/datums/cinematics/narsie_summon.dm @@ -0,0 +1,29 @@ +/// A blood cult summoned Nar'sie, and most of the station was harvested or converted! +/datum/cinematic/cult_arm // Colloquially known as "the arm" + +/datum/cinematic/cult_arm/play_cinematic() + screen.icon_state = null + flick("intro_cult", screen) + stoplag(2.5 SECONDS) + play_cinematic_sound(sound('sound/magic/enter_blood.ogg')) + stoplag(2.8 SECONDS) + play_cinematic_sound(sound('sound/machines/terminal_off.ogg')) + stoplag(2 SECONDS) + flick("station_corrupted", screen) + play_cinematic_sound(sound('sound/effects/ghost.ogg')) + stoplag(7 SECONDS) + special_callback?.Invoke() + +/// A blood cult summoned Nar'sie, but some badass (or admin) managed to destroy Nar'sie themselves. +/datum/cinematic/cult_fail + +/datum/cinematic/cult_fail/play_cinematic() + screen.icon_state = "station_intact" + stoplag(2 SECONDS) + play_cinematic_sound(sound('sound/creatures/narsie_rises.ogg')) + stoplag(6 SECONDS) + play_cinematic_sound(sound('sound/effects/explosion_distant.ogg')) + stoplag(1 SECONDS) + play_cinematic_sound(sound('sound/magic/demon_dies.ogg')) + stoplag(3 SECONDS) + special_callback?.Invoke() diff --git a/code/datums/cinematics/nuke_cinematics.dm b/code/datums/cinematics/nuke_cinematics.dm new file mode 100644 index 00000000000..dd827f7c0b9 --- /dev/null +++ b/code/datums/cinematics/nuke_cinematics.dm @@ -0,0 +1,100 @@ +/// Simple, base cinematic for all animations based around a nuke detonating. +/datum/cinematic/nuke + /// If set, this is the summary screen that pops up after the nuke is done. + var/after_nuke_summary_state + +/datum/cinematic/nuke/play_cinematic() + flick("intro_nuke", screen) + stoplag(3.5 SECONDS) + play_nuke_effect() + if(special_callback) + special_callback.Invoke() + if(after_nuke_summary_state) + screen.icon_state = after_nuke_summary_state + +/// Specific effects for each type of cinematics goes here. +/datum/cinematic/nuke/proc/play_nuke_effect() + return + +/// The syndicate nuclear bomb was activated, and destroyed the station! +/datum/cinematic/nuke/ops_victory + after_nuke_summary_state = "summary_nukewin" + +/datum/cinematic/nuke/ops_victory/play_nuke_effect() + flick("station_explode_fade_red", screen) + play_cinematic_sound(sound('sound/effects/explosion_distant.ogg')) + +/// The syndicate nuclear bomb was activated, but just barely missed the station! +/datum/cinematic/nuke/ops_miss + after_nuke_summary_state = "summary_nukefail" + +/datum/cinematic/nuke/ops_miss/play_nuke_effect() + flick("station_intact_fade_red", screen) + play_cinematic_sound(sound('sound/effects/explosion_distant.ogg')) + +/// The self destruct, or another station-destroying entity like a blob, destroyed the station! +/datum/cinematic/nuke/self_destruct + after_nuke_summary_state = "summary_selfdes" + +/datum/cinematic/nuke/self_destruct/play_nuke_effect() + flick("station_explode_fade_red", screen) + play_cinematic_sound(sound('sound/effects/explosion_distant.ogg')) + +/// The self destruct was activated, yet somehow avoided destroying the station! +/datum/cinematic/nuke/self_destruct_miss + after_nuke_summary_state = "station_intact" + +/datum/cinematic/nuke/self_destruct_miss/play_nuke_effect() + play_cinematic_sound(sound('sound/effects/explosion_distant.ogg')) + special_callback?.Invoke() + +/// The syndicate nuclear bomb was activated, and the nuclear operatives failed to extract on their shuttle before it detonated on the station! +/datum/cinematic/nuke/mutual_destruction + after_nuke_summary_state = "summary_totala" + +/datum/cinematic/nuke/mutual_destruction/play_nuke_effect() + flick("station_explode_fade_red", screen) + play_cinematic_sound(sound('sound/effects/explosion_distant.ogg')) + +/// A blood cult summoned Nar'sie, but central command deployed a nuclear package to stop them. +/datum/cinematic/nuke/cult + after_nuke_summary_state = "summary_cult" + +/datum/cinematic/nuke/cult/play_nuke_effect() + flick("station_explode_fade_red", screen) + play_cinematic_sound(sound('sound/effects/explosion_distant.ogg')) + +/// A fake version of the nuclear detonation, where it winds up, but doesn't explode. +/datum/cinematic/nuke/fake + cleanup_time = 10 SECONDS + +/datum/cinematic/nuke/fake/play_nuke_effect() + play_cinematic_sound(sound('sound/items/bikehorn.ogg')) + flick("summary_selfdes", screen) //??? + +/// The clown operative nuclear bomb was activated and clowned the station! +/datum/cinematic/nuke/clown + cleanup_time = 10 SECONDS + +/datum/cinematic/nuke/clown/play_nuke_effect() + play_cinematic_sound(sound('sound/items/airhorn.ogg')) + flick("summary_selfdes", screen) //??? + +/// A fake version of the nuclear detonation, where it winds up, but doesn't explode as the nuke core within was missing. +/datum/cinematic/nuke/no_core + cleanup_time = 10 SECONDS + +/datum/cinematic/nuke/no_core/play_nuke_effect() + flick("station_intact", screen) + play_cinematic_sound(sound('sound/ambience/signal.ogg')) + stoplag(10 SECONDS) + +/// The syndicate nuclear bomb was activated, but just missed the station by a whole z-level! +/datum/cinematic/nuke/far_explosion + cleanup_time = 0 SECONDS + +/datum/cinematic/nuke/far_explosion/play_cinematic() + // This one has no intro sequence. + // It's actually just a global sound, which makes you wonder why it's a cinematic. + play_cinematic_sound(sound('sound/effects/explosion_distant.ogg')) + special_callback?.Invoke() diff --git a/code/datums/mocking/client.dm b/code/datums/mocking/client.dm index 4a665f828be..fd99e34520c 100644 --- a/code/datums/mocking/client.dm +++ b/code/datums/mocking/client.dm @@ -5,3 +5,9 @@ /// The view of the client, similar to /client/var/view. var/view = "15x15" + + /// Objects on the screen of the client + var/list/screen = list() + + /// The mob the client controls + var/mob/mob diff --git a/code/game/objects/structures/crates_lockers/closets/secure/freezer.dm b/code/game/objects/structures/crates_lockers/closets/secure/freezer.dm index 1e9e86791f4..6f80dec88c5 100644 --- a/code/game/objects/structures/crates_lockers/closets/secure/freezer.dm +++ b/code/game/objects/structures/crates_lockers/closets/secure/freezer.dm @@ -4,6 +4,7 @@ door_anim_squish = 0.22 door_anim_angle = 123 door_anim_time = 4 + /// If FALSE, we will protect the first person in the freezer from an explosion / nuclear blast. var/jones = FALSE /obj/structure/closet/secure_closet/freezer/Destroy() diff --git a/code/modules/admin/verbs/adminevents.dm b/code/modules/admin/verbs/adminevents.dm index 80340c733f5..af32ef6a1d7 100644 --- a/code/modules/admin/verbs/adminevents.dm +++ b/code/modules/admin/verbs/adminevents.dm @@ -253,8 +253,8 @@ if(!newtime) return N.timer_set = newtime - N.set_safety() - N.set_active() + N.toggle_nuke_safety() + N.toggle_nuke_armed() log_admin("[key_name(usr)] [N.timing ? "activated" : "deactivated"] a nuke at [AREACOORD(N)].") message_admins("[ADMIN_LOOKUPFLW(usr)] [N.timing ? "activated" : "deactivated"] a nuke at [ADMIN_VERBOSEJMP(N)].") @@ -323,4 +323,3 @@ message_admins("[key_name_admin(usr)] added mob ability [ability_type] to mob [marked_mob].") log_admin("[key_name(usr)] added mob ability [ability_type] to mob [marked_mob].") SSblackbox.record_feedback("tally", "admin_verb", 1, "Add Mob Ability") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - diff --git a/code/modules/admin/verbs/cinematic.dm b/code/modules/admin/verbs/cinematic.dm index 2cb8d1a0f04..6bd9bdac2b8 100644 --- a/code/modules/admin/verbs/cinematic.dm +++ b/code/modules/admin/verbs/cinematic.dm @@ -3,9 +3,12 @@ set category = "Admin.Fun" set desc = "Shows a cinematic." // Intended for testing but I thought it might be nice for events on the rare occasion Feel free to comment it out if it's not wanted. set hidden = TRUE + if(!SSticker) return - var/datum/cinematic/choice = input(src,"Cinematic","Choose",null) as null|anything in sort_list(subtypesof(/datum/cinematic), /proc/cmp_typepaths_asc) - if(choice) - Cinematic(initial(choice.id),world,null) + var/datum/cinematic/choice = tgui_input_list(usr, "Chose a cinematic to play to everyone in the server.", "Choose Cinematic", sort_list(subtypesof(/datum/cinematic), /proc/cmp_typepaths_asc)) + if(!choice || !ispath(choice, /datum/cinematic)) + return + + play_cinematic(choice, world) diff --git a/code/modules/antagonists/clown_ops/bananium_bomb.dm b/code/modules/antagonists/clown_ops/bananium_bomb.dm index 5bd38a66011..66ce818c41f 100644 --- a/code/modules/antagonists/clown_ops/bananium_bomb.dm +++ b/code/modules/antagonists/clown_ops/bananium_bomb.dm @@ -1,7 +1,6 @@ /obj/machinery/nuclearbomb/syndicate/bananium name = "bananium fission explosive" desc = "You probably shouldn't stick around to see if this is armed." - icon = 'icons/obj/machines/nuke.dmi' icon_state = "bananiumbomb_base" base_icon_state = "bananiumbomb" @@ -19,39 +18,43 @@ if(NUKE_ON_EXPLODING) icon_state = "[base_icon_state]_exploding" -/obj/machinery/nuclearbomb/syndicate/bananium/get_cinematic_type(off_station) - switch(off_station) - if(0) - return CINEMATIC_NUKE_CLOWNOP - if(1) - return CINEMATIC_NUKE_MISS - if(2) - return CINEMATIC_NUKE_FAKE //it is farther away, so just a bikehorn instead of an airhorn - return CINEMATIC_NUKE_FAKE +/obj/machinery/nuclearbomb/syndicate/bananium/get_cinematic_type(detonation_status) + switch(detonation_status) + if(DETONATION_HIT_STATION) + return /datum/cinematic/nuke/clown + if(DETONATION_NEAR_MISSED_STATION) + return /datum/cinematic/nuke/ops_miss + if(DETONATION_HIT_SYNDIE_BASE, DETONATION_MISSED_STATION) + return /datum/cinematic/nuke/fake //it is farther away, so just a bikehorn instead of an airhorn -/obj/machinery/nuclearbomb/syndicate/bananium/really_actually_explode(off_station) - Cinematic(get_cinematic_type(off_station), world) - for(var/i in GLOB.human_list) - var/mob/living/carbon/human/H = i - var/turf/T = get_turf(H) - if(!T || T.z != z) - continue - H.Stun(10) - var/obj/item/clothing/C - if(!H.w_uniform || H.dropItemToGround(H.w_uniform)) - C = new /obj/item/clothing/under/rank/civilian/clown(H) - ADD_TRAIT(C, TRAIT_NODROP, CLOWN_NUKE_TRAIT) - H.equip_to_slot_or_del(C, ITEM_SLOT_ICLOTHING) + stack_trace("[type] - get_cinematic_type got a detonation_status it was not expecting. (Got: [detonation_status])") + return /datum/cinematic/nuke/fake - if(!H.shoes || H.dropItemToGround(H.shoes)) - C = new /obj/item/clothing/shoes/clown_shoes(H) - ADD_TRAIT(C, TRAIT_NODROP, CLOWN_NUKE_TRAIT) - H.equip_to_slot_or_del(C, ITEM_SLOT_FEET) +/obj/machinery/nuclearbomb/syndicate/bananium/nuke_effects(list/affected_z_levels) + INVOKE_ASYNC(GLOBAL_PROC, /proc/callback_on_everyone_on_z, affected_z_levels, CALLBACK(GLOBAL_PROC, /proc/make_into_clown), src) - if(!H.wear_mask || H.dropItemToGround(H.wear_mask)) - C = new /obj/item/clothing/mask/gas/clown_hat(H) - ADD_TRAIT(C, TRAIT_NODROP, CLOWN_NUKE_TRAIT) - H.equip_to_slot_or_del(C, ITEM_SLOT_MASK) +/** + * Helper proc that handles making someone into a clown after a bananium nuke goes off. + */ +/proc/make_into_clown(mob/living/carbon/human/clowned_on) + if(!istype(clowned_on)) + return - H.dna.add_mutation(/datum/mutation/human/clumsy) - H.gain_trauma(/datum/brain_trauma/mild/phobia/clowns, TRAUMA_RESILIENCE_LOBOTOMY) //MWA HA HA + clowned_on.Stun(1 SECONDS) + if(!clowned_on.w_uniform || clowned_on.dropItemToGround(clowned_on.w_uniform)) + var/obj/item/clothing/clown_shirt = new /obj/item/clothing/under/rank/civilian/clown(clowned_on) + ADD_TRAIT(clown_shirt, TRAIT_NODROP, CLOWN_NUKE_TRAIT) + clowned_on.equip_to_slot_or_del(clown_shirt, ITEM_SLOT_ICLOTHING) + + if(!clowned_on.shoes || clowned_on.dropItemToGround(clowned_on.shoes)) + var/obj/item/clothing/clown_shoes = new /obj/item/clothing/shoes/clown_shoes(clowned_on) + ADD_TRAIT(clown_shoes, TRAIT_NODROP, CLOWN_NUKE_TRAIT) + clowned_on.equip_to_slot_or_del(clown_shoes, ITEM_SLOT_FEET) + + if(!clowned_on.wear_mask || clowned_on.dropItemToGround(clowned_on.wear_mask)) + var/obj/item/clothing/clown_mask = new /obj/item/clothing/mask/gas/clown_hat(clowned_on) + ADD_TRAIT(clown_mask, TRAIT_NODROP, CLOWN_NUKE_TRAIT) + clowned_on.equip_to_slot_or_del(clown_mask, ITEM_SLOT_MASK) + + clowned_on.dna.add_mutation(/datum/mutation/human/clumsy) + clowned_on.gain_trauma(/datum/brain_trauma/mild/phobia/clowns, TRAUMA_RESILIENCE_LOBOTOMY) //MWA HA HA diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_authentication_disk.dm b/code/modules/antagonists/nukeop/equipment/nuclear_authentication_disk.dm new file mode 100644 index 00000000000..d3947232ebc --- /dev/null +++ b/code/modules/antagonists/nukeop/equipment/nuclear_authentication_disk.dm @@ -0,0 +1,136 @@ +/obj/item/disk + icon = 'icons/obj/module.dmi' + w_class = WEIGHT_CLASS_TINY + inhand_icon_state = "card-id" + lefthand_file = 'icons/mob/inhands/equipment/idcards_lefthand.dmi' + righthand_file = 'icons/mob/inhands/equipment/idcards_righthand.dmi' + icon_state = "datadisk0" + drop_sound = 'sound/items/handling/disk_drop.ogg' + pickup_sound = 'sound/items/handling/disk_pickup.ogg' + +// DAT FUKKEN DISK. +/obj/item/disk/nuclear + name = "nuclear authentication disk" + desc = "Better keep this safe." + icon_state = "nucleardisk" + max_integrity = 250 + armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 30, BIO = 0, FIRE = 100, ACID = 100) + resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | ACID_PROOF + /// Whether we're a real nuke disk or not. + var/fake = FALSE + /// The last secure location the disk was at. + var/turf/last_secured_location + /// The last world time the disk moved. + var/last_disk_move + +/obj/item/disk/nuclear/Initialize(mapload) + . = ..() + AddElement(/datum/element/bed_tuckable, 6, -6, 0) + AddComponent(/datum/component/stationloving, !fake) + + if(!fake) + SSpoints_of_interest.make_point_of_interest(src) + last_disk_move = world.time + START_PROCESSING(SSobj, src) + +/obj/item/disk/nuclear/process() + if(fake) + STOP_PROCESSING(SSobj, src) + CRASH("A fake nuke disk tried to call process(). Who the fuck and how the fuck") + + var/turf/new_turf = get_turf(src) + + if (is_secured()) + last_secured_location = new_turf + last_disk_move = world.time + var/datum/round_event_control/operative/loneop = locate(/datum/round_event_control/operative) in SSevents.control + if(istype(loneop) && loneop.occurrences < loneop.max_occurrences && prob(loneop.weight)) + loneop.weight = max(loneop.weight - 1, 0) + if(loneop.weight % 5 == 0 && SSticker.totalPlayers > 1) + message_admins("[src] is secured (currently in [ADMIN_VERBOSEJMP(new_turf)]). The weight of Lone Operative is now [loneop.weight].") + log_game("[src] being secured has reduced the weight of the Lone Operative event to [loneop.weight].") + else + /// How comfy is our disk? + var/disk_comfort_level = 0 + + //Go through and check for items that make disk comfy + for(var/obj/comfort_item in loc) + if(istype(comfort_item, /obj/item/bedsheet) || istype(comfort_item, /obj/structure/bed)) + disk_comfort_level++ + + if(last_disk_move < world.time - 5000 && prob((world.time - 5000 - last_disk_move)*0.0001)) + var/datum/round_event_control/operative/loneop = locate(/datum/round_event_control/operative) in SSevents.control + if(istype(loneop) && loneop.occurrences < loneop.max_occurrences) + loneop.weight += 1 + if(loneop.weight % 5 == 0 && SSticker.totalPlayers > 1) + if(disk_comfort_level >= 2) + visible_message(span_notice("[src] sleeps soundly. Sleep tight, disky.")) + message_admins("[src] is unsecured in [ADMIN_VERBOSEJMP(new_turf)]. The weight of Lone Operative is now [loneop.weight].") + log_game("[src] is unsecured for too long in [loc_name(new_turf)], and has increased the weight of the Lone Operative event to [loneop.weight].") + +/obj/item/disk/nuclear/proc/is_secured() + if (last_secured_location == get_turf(src)) + return FALSE + + var/mob/holder = pulledby || get(src, /mob) + if (isnull(holder?.client)) + return FALSE + + return TRUE + +/obj/item/disk/nuclear/examine(mob/user) + . = ..() + if(!fake) + return + + if(isobserver(user) || HAS_TRAIT(user, TRAIT_DISK_VERIFIER) || (user.mind && HAS_TRAIT(user.mind, TRAIT_DISK_VERIFIER))) + . += span_warning("The serial numbers on [src] are incorrect.") + +/* + * You can't accidentally eat the nuke disk, bro + */ +/obj/item/disk/nuclear/on_accidental_consumption(mob/living/carbon/M, mob/living/carbon/user, obj/item/source_item, discover_after = TRUE) + M.visible_message(span_warning("[M] looks like [M.p_theyve()] just bitten into something important."), \ + span_warning("Wait, is this the nuke disk?")) + + return discover_after + +/obj/item/disk/nuclear/attackby(obj/item/weapon, mob/living/user, params) + if(istype(weapon, /obj/item/claymore/highlander) && !fake) + var/obj/item/claymore/highlander/claymore = weapon + if(claymore.nuke_disk) + to_chat(user, span_notice("Wait... what?")) + qdel(claymore.nuke_disk) + claymore.nuke_disk = null + return + + user.visible_message( + span_warning("[user] captures [src]!"), + span_userdanger("You've got the disk! Defend it with your life!"), + ) + forceMove(claymore) + claymore.nuke_disk = src + return TRUE + + return ..() + +/obj/item/disk/nuclear/suicide_act(mob/user) + user.visible_message(span_suicide("[user] is going delta! It looks like [user.p_theyre()] trying to commit suicide!")) + playsound(src, 'sound/machines/alarm.ogg', 50, -1, TRUE) + for(var/i in 1 to 100) + addtimer(CALLBACK(user, /atom/proc/add_atom_colour, (i % 2)? "#00FF00" : "#FF0000", ADMIN_COLOUR_PRIORITY), i) + addtimer(CALLBACK(src, .proc/manual_suicide, user), 101) + return MANUAL_SUICIDE + +/obj/item/disk/nuclear/proc/manual_suicide(mob/living/user) + user.remove_atom_colour(ADMIN_COLOUR_PRIORITY) + user.visible_message(span_suicide("[user] is destroyed by the nuclear blast!")) + user.adjustOxyLoss(200) + user.death(0) + +/obj/item/disk/nuclear/fake + fake = TRUE + +/obj/item/disk/nuclear/fake/obvious + name = "cheap plastic imitation of the nuclear authentication disk" + desc = "How anyone could mistake this for the real thing is beyond you." diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm new file mode 100644 index 00000000000..f97262b9ea5 --- /dev/null +++ b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm @@ -0,0 +1,645 @@ +/// Whether the station has been nuked itself. TRUE only if the station was actually hit by the nuke, otherwise FALSE +GLOBAL_VAR_INIT(station_was_nuked, FALSE) +/// The source of the last nuke that went off +GLOBAL_VAR(station_nuke_source) + +/obj/machinery/nuclearbomb + name = "nuclear fission explosive" + desc = "You probably shouldn't stick around to see if this is armed." + icon = 'icons/obj/machines/nuke.dmi' + icon_state = "nuclearbomb_base" + anchored = FALSE + density = TRUE + resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF + use_power = NO_POWER_USE + + /// What the timer is set to, in seconds + var/timer_set = 90 + /// What the min value the timer can be, in seconds + var/minimum_timer_set = 90 + /// What the max value the timer can be, in seconds + var/maximum_timer_set = 3600 + /// The current input of the numpad on the bomb + var/numeric_input = "" + /// What mode the UI currently is in + var/ui_mode = NUKEUI_AWAIT_DISK + /// Whether we're currently timing an explosive and counting down + var/timing = FALSE + /// Whether the timer has elapsed and we're currently exploding + var/exploding = FALSE + /// Whether we've actually fully exploded + var/exploded = FALSE + /// world time tracker for when we're going to explode + var/detonation_timer = null + /// The code we need to detonate this nuke. Starts as "admin", purposefully un-enterable + var/r_code = "ADMIN" + /// If TRUE, the correct code has been entered and we can start the nuke + var/yes_code = FALSE + /// Whether the nuke safety is on, can't explode if it is + var/safety = TRUE + /// The nuke disk currently inserted into the nuke + var/obj/item/disk/nuclear/auth + /// The alert level that was set before the nuke started, so we can revert to the correct level after + var/previous_level = "" + /// The nuke core within the nuke, created in initialize + var/obj/item/nuke_core/core + /// The current state of deconstructing / opening up the nuke to access the core + var/deconstruction_state = NUKESTATE_INTACT + /// Overlay - flashing lights over the nuke + var/lights = "" + /// Overlay - shows the interior of the nuke + var/interior = "" + /// if TRUE, this nuke is actually a real nuke, and not a prank or toy + var/proper_bomb = TRUE //Please + /// A reference to the countdown that goes up over the nuke + var/obj/effect/countdown/nuclearbomb/countdown + +/obj/machinery/nuclearbomb/Initialize(mapload) + . = ..() + countdown = new(src) + GLOB.nuke_list += src + core = new /obj/item/nuke_core(src) + STOP_PROCESSING(SSobj, core) + update_appearance() + SSpoints_of_interest.make_point_of_interest(src) + previous_level = get_security_level() + +/obj/machinery/nuclearbomb/Destroy() + safety = FALSE + if(!exploding) + // If we're not exploding, set the alert level back to normal + toggle_nuke_safety() + GLOB.nuke_list -= src + QDEL_NULL(countdown) + QDEL_NULL(core) + return ..() + +/obj/machinery/nuclearbomb/examine(mob/user) + . = ..() + if(exploding) + . += span_bolddanger("It is in the process of exploding. Perhaps reviewing your affairs is in order.") + if(timing) + . += span_danger("There are [get_time_left()] seconds until detonation.") + +/// Checks if the disk inserted is a real nuke disk or not. +/obj/machinery/nuclearbomb/proc/disk_check(obj/item/disk/nuclear/inserted_disk) + if(inserted_disk.fake) + say("Authentication failure; disk not recognised.") + return FALSE + + return TRUE + +/obj/machinery/nuclearbomb/attackby(obj/item/weapon, mob/user, params) + if (istype(weapon, /obj/item/disk/nuclear)) + if(!disk_check(weapon)) + return TRUE + if(!user.transferItemToLoc(weapon, src)) + return TRUE + auth = weapon + update_ui_mode() + playsound(src, 'sound/machines/terminal_insert_disc.ogg', 50, FALSE) + add_fingerprint(user) + return TRUE + + switch(deconstruction_state) + if(NUKESTATE_INTACT) + if(istype(weapon, /obj/item/screwdriver/nuke)) + to_chat(user, span_notice("You start removing [src]'s front panel's screws...")) + if(weapon.use_tool(src, user, 6 SECONDS, volume = 100)) + deconstruction_state = NUKESTATE_UNSCREWED + to_chat(user, span_notice("You remove the screws from [src]'s front panel.")) + update_appearance() + return TRUE + + if(NUKESTATE_PANEL_REMOVED) + if(weapon.tool_behaviour == TOOL_WELDER) + if(!weapon.tool_start_check(user, amount = 1)) + return TRUE + to_chat(user, span_notice("You start cutting [src]'s inner plate...")) + if(weapon.use_tool(src, user, 8 SECONDS, volume=100, amount=1)) + to_chat(user, span_notice("You cut [src]'s inner plate.")) + deconstruction_state = NUKESTATE_WELDED + update_appearance() + return TRUE + + if(NUKESTATE_CORE_EXPOSED) + if(istype(weapon, /obj/item/nuke_core_container)) + var/obj/item/nuke_core_container/core_box = weapon + to_chat(user, span_notice("You start loading the plutonium core into [core_box]...")) + if(do_after(user, 5 SECONDS, target=src)) + if(core_box.load(core, user)) + to_chat(user, span_notice("You load the plutonium core into [core_box].")) + deconstruction_state = NUKESTATE_CORE_REMOVED + update_appearance() + core = null + else + to_chat(user, span_warning("You fail to load the plutonium core into [core_box]. [core_box] has already been used!")) + return TRUE + + if(istype(weapon, /obj/item/stack/sheet/iron)) + if(!weapon.tool_start_check(user, amount = 20)) + return TRUE + + to_chat(user, span_notice("You begin repairing [src]'s inner metal plate...")) + if(weapon.use_tool(src, user, 10 SECONDS, amount = 20)) + to_chat(user, span_notice("You repair [src]'s inner metal plate. The radiation is contained.")) + deconstruction_state = NUKESTATE_PANEL_REMOVED + STOP_PROCESSING(SSobj, core) + update_appearance() + return TRUE + + return ..() + +/obj/machinery/nuclearbomb/crowbar_act(mob/user, obj/item/tool) + switch(deconstruction_state) + if(NUKESTATE_UNSCREWED) + to_chat(user, span_notice("You start removing [src]'s front panel...")) + if(tool.use_tool(src, user, 30, volume=100)) + to_chat(user, span_notice("You remove [src]'s front panel.")) + deconstruction_state = NUKESTATE_PANEL_REMOVED + update_appearance() + return TRUE + if(NUKESTATE_WELDED) + to_chat(user, span_notice("You start prying off [src]'s inner plate...")) + if(tool.use_tool(src, user, 30, volume=100)) + to_chat(user, span_notice("You pry off [src]'s inner plate. You can see the core's green glow!")) + deconstruction_state = NUKESTATE_CORE_EXPOSED + update_appearance() + START_PROCESSING(SSobj, core) + return TRUE + + return FALSE + +/obj/machinery/nuclearbomb/can_interact(mob/user) + if(HAS_TRAIT(user, TRAIT_CAN_USE_NUKE)) + return TRUE + + return ..() + +/obj/machinery/nuclearbomb/ui_state(mob/user) + if(HAS_TRAIT(user, TRAIT_CAN_USE_NUKE)) + return GLOB.physical_state + + return ..() + +/// Gets the current state of the nuke. +/obj/machinery/nuclearbomb/proc/get_nuke_state() + if(exploding) + return NUKE_ON_EXPLODING + if(timing) + return NUKE_ON_TIMING + if(safety) + return NUKE_OFF_LOCKED + else + return NUKE_OFF_UNLOCKED + +/obj/machinery/nuclearbomb/update_icon_state() + if(deconstruction_state != NUKESTATE_INTACT) + icon_state = "nuclearbomb_base" + return ..() + + switch(get_nuke_state()) + if(NUKE_OFF_LOCKED, NUKE_OFF_UNLOCKED) + icon_state = "nuclearbomb_base" + if(NUKE_ON_TIMING) + icon_state = "nuclearbomb_timing" + if(NUKE_ON_EXPLODING) + icon_state = "nuclearbomb_exploding" + + return ..() + +/obj/machinery/nuclearbomb/update_overlays() + . = ..() + + if(lights) + cut_overlay(lights) + cut_overlay(interior) + + switch(deconstruction_state) + if(NUKESTATE_UNSCREWED) + interior = "panel-unscrewed" + if(NUKESTATE_PANEL_REMOVED) + interior = "panel-removed" + if(NUKESTATE_WELDED) + interior = "plate-welded" + if(NUKESTATE_CORE_EXPOSED) + interior = "plate-removed" + if(NUKESTATE_CORE_REMOVED) + interior = "core-removed" + if(NUKESTATE_INTACT) + return + + switch(get_nuke_state()) + if(NUKE_OFF_LOCKED) + lights = "" + return + if(NUKE_OFF_UNLOCKED) + lights = "lights-safety" + if(NUKE_ON_TIMING) + lights = "lights-timing" + if(NUKE_ON_EXPLODING) + lights = "lights-exploding" + + add_overlay(lights) + add_overlay(interior) + +/obj/machinery/nuclearbomb/process() + if(!timing || exploding) + return + + if(detonation_timer < world.time) + explode() + return + + var/volume = (get_time_left() <= 20 ? 30 : 5) + playsound(loc, 'sound/items/timer.ogg', volume, FALSE) + +/// Changes what mode the UI is depending on the state of the nuke. +/obj/machinery/nuclearbomb/proc/update_ui_mode() + if(exploded) + ui_mode = NUKEUI_EXPLODED + return + + if(!auth) + ui_mode = NUKEUI_AWAIT_DISK + return + + if(timing) + ui_mode = NUKEUI_TIMING + return + + if(!safety) + ui_mode = NUKEUI_AWAIT_ARM + return + + if(!yes_code) + ui_mode = NUKEUI_AWAIT_CODE + return + + ui_mode = NUKEUI_AWAIT_TIMER + +/obj/machinery/nuclearbomb/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "NuclearBomb", name) + ui.open() + +/obj/machinery/nuclearbomb/ui_data(mob/user) + var/list/data = list() + data["disk_present"] = auth + + var/hidden_code = (ui_mode == NUKEUI_AWAIT_CODE && numeric_input != "ERROR") + + var/current_code = "" + if(hidden_code) + while(length(current_code) < length(numeric_input)) + current_code = "[current_code]*" + else + current_code = numeric_input + while(length(current_code) < 5) + current_code = "[current_code]-" + + var/first_status + var/second_status + switch(ui_mode) + if(NUKEUI_AWAIT_DISK) + first_status = "DEVICE LOCKED" + if(timing) + second_status = "TIME: [get_time_left()]" + else + second_status = "AWAIT DISK" + if(NUKEUI_AWAIT_CODE) + first_status = "INPUT CODE" + second_status = "CODE: [current_code]" + if(NUKEUI_AWAIT_TIMER) + first_status = "INPUT TIME" + second_status = "TIME: [current_code]" + if(NUKEUI_AWAIT_ARM) + first_status = "DEVICE READY" + second_status = "TIME: [get_time_left()]" + if(NUKEUI_TIMING) + first_status = "DEVICE ARMED" + second_status = "TIME: [get_time_left()]" + if(NUKEUI_EXPLODED) + first_status = "DEVICE DEPLOYED" + second_status = "THANK YOU" + + data["status1"] = first_status + data["status2"] = second_status + data["anchored"] = anchored + + return data + +/obj/machinery/nuclearbomb/ui_act(action, params) + . = ..() + if(.) + return + playsound(src, SFX_TERMINAL_TYPE, 20, FALSE) + switch(action) + if("eject_disk") + if(auth && auth.loc == src) + playsound(src, 'sound/machines/terminal_insert_disc.ogg', 50, FALSE) + playsound(src, 'sound/machines/nuke/general_beep.ogg', 50, FALSE) + auth.forceMove(get_turf(src)) + auth = null + . = TRUE + else + var/obj/item/I = usr.is_holding_item_of_type(/obj/item/disk/nuclear) + if(I && disk_check(I) && usr.transferItemToLoc(I, src)) + playsound(src, 'sound/machines/terminal_insert_disc.ogg', 50, FALSE) + playsound(src, 'sound/machines/nuke/general_beep.ogg', 50, FALSE) + auth = I + . = TRUE + update_ui_mode() + if("keypad") + if(auth) + var/digit = params["digit"] + switch(digit) + if("C") + if(auth && ui_mode == NUKEUI_AWAIT_ARM) + toggle_nuke_safety() + yes_code = FALSE + playsound(src, 'sound/machines/nuke/confirm_beep.ogg', 50, FALSE) + update_ui_mode() + else + playsound(src, 'sound/machines/nuke/general_beep.ogg', 50, FALSE) + numeric_input = "" + . = TRUE + if("E") + switch(ui_mode) + if(NUKEUI_AWAIT_CODE) + if(numeric_input == r_code) + numeric_input = "" + yes_code = TRUE + playsound(src, 'sound/machines/nuke/general_beep.ogg', 50, FALSE) + . = TRUE + else + playsound(src, 'sound/machines/nuke/angry_beep.ogg', 50, FALSE) + numeric_input = "ERROR" + if(NUKEUI_AWAIT_TIMER) + var/number_value = text2num(numeric_input) + if(number_value) + timer_set = clamp(number_value, minimum_timer_set, maximum_timer_set) + playsound(src, 'sound/machines/nuke/general_beep.ogg', 50, FALSE) + toggle_nuke_safety() + . = TRUE + else + playsound(src, 'sound/machines/nuke/angry_beep.ogg', 50, FALSE) + update_ui_mode() + if("0", "1", "2", "3", "4", "5", "6", "7", "8", "9") + if(numeric_input != "ERROR") + numeric_input += digit + if(length(numeric_input) > 5) + numeric_input = "ERROR" + else + playsound(src, 'sound/machines/nuke/general_beep.ogg', 50, FALSE) + . = TRUE + else + playsound(src, 'sound/machines/nuke/angry_beep.ogg', 50, FALSE) + if("arm") + if(auth && yes_code && !safety && !exploded) + playsound(src, 'sound/machines/nuke/confirm_beep.ogg', 50, FALSE) + toggle_nuke_armed() + update_ui_mode() + . = TRUE + else + playsound(src, 'sound/machines/nuke/angry_beep.ogg', 50, FALSE) + if("anchor") + if(auth && yes_code) + playsound(src, 'sound/machines/nuke/general_beep.ogg', 50, FALSE) + set_anchor(usr) + else + playsound(src, 'sound/machines/nuke/angry_beep.ogg', 50, FALSE) + +/// Anchors the nuke, duh. Can only be done if the disk is inside. +/obj/machinery/nuclearbomb/proc/set_anchor(mob/anchorer) + if(isinspace() && !anchored) + if(anchorer) + to_chat(anchorer, span_warning("There is nothing to anchor to!")) + return + + set_anchored(!anchored) + +/// Toggles the safety of the nuke. +/obj/machinery/nuclearbomb/proc/toggle_nuke_safety() + safety = !safety + + // We're safe now, so stop any ongoing timers + if(safety) + if(timing) + disarm_nuke() + + timing = FALSE + detonation_timer = null + countdown.stop() + +/// Arms the nuke, or disarms it if it's already active. +/obj/machinery/nuclearbomb/proc/toggle_nuke_armed() + if(safety) + to_chat(usr, span_danger("The safety is still on.")) + return + + timing = !timing + if(timing) + arm_nuke(usr) + else + disarm_nuke(usr) + +/// Arms the nuke, making it active and triggering all pinpointers to start counting down (+delta alert) +/obj/machinery/nuclearbomb/proc/arm_nuke(mob/armer) + var/turf/our_turf = get_turf(src) + message_admins("\The [src] was armed at [ADMIN_VERBOSEJMP(our_turf)] by [armer ? ADMIN_LOOKUPFLW(armer) : "an unknown user"].") + log_game("\The [src] was armed at [loc_name(our_turf)] by [armer ? key_name(armer) : "an unknown user"].") + + previous_level = get_security_level() + detonation_timer = world.time + (timer_set * 10) + for(var/obj/item/pinpointer/nuke/syndicate/nuke_pointer in GLOB.pinpointer_list) + nuke_pointer.switch_mode_to(TRACK_INFILTRATOR) + + SEND_GLOBAL_SIGNAL(COMSIG_GLOB_NUKE_DEVICE_ARMED, src) + + countdown.start() + set_security_level("delta") + update_appearance() + +/// Disarms the nuke, reverting all pinpointers and the security level +/obj/machinery/nuclearbomb/proc/disarm_nuke(mob/disarmer) + var/turf/our_turf = get_turf(src) + message_admins("\The [src] at [ADMIN_VERBOSEJMP(our_turf)] was disarmed by [disarmer ? ADMIN_LOOKUPFLW(disarmer) : "an unknown user"].") + log_game("\The [src] at [loc_name(our_turf)] was disarmed by [disarmer ? key_name(disarmer) : "an unknown user"].") + + detonation_timer = null + set_security_level(previous_level) + + for(var/obj/item/pinpointer/nuke/syndicate/nuke_pointer in GLOB.pinpointer_list) + nuke_pointer.switch_mode_to(initial(nuke_pointer.mode)) + nuke_pointer.alert = FALSE + + countdown.stop() + SEND_GLOBAL_SIGNAL(COMSIG_GLOB_NUKE_DEVICE_DISARMED, src) + update_appearance() + +/// If the nuke is active, gets how much time is left until it detonates, in seconds. +/// If the nuke is not active, gets how much time the nuke is set for, in seconds. +/obj/machinery/nuclearbomb/proc/get_time_left() + if(timing) + . = round(max(0, detonation_timer - world.time) / 10, 1) + else + . = timer_set + +/obj/machinery/nuclearbomb/blob_act(obj/structure/blob/attacking_blob) + if(exploding) + return + qdel(src) + +/obj/machinery/nuclearbomb/zap_act(power, zap_flags) + . = ..() + if(zap_flags & ZAP_MACHINE_EXPLOSIVE) + qdel(src)//like the singulo, tesla deletes it. stops it from exploding over and over + +#define NUKE_RADIUS 127 + +/** + * Begins the process of exploding the nuke. + * [proc/explode] -> [proc/actually_explode] -> [proc/really_actually_explode] + * + * Goes through a few timers and plays a cinematic. + */ +/obj/machinery/nuclearbomb/proc/explode() + if(safety) + timing = FALSE + return FALSE + + exploding = TRUE + yes_code = FALSE + safety = TRUE + update_appearance() + sound_to_playing_players('sound/machines/alarm.ogg') + + if(SSticker?.mode) + SSticker.roundend_check_paused = TRUE + addtimer(CALLBACK(src, .proc/actually_explode), 10 SECONDS) + return TRUE + +/obj/machinery/nuclearbomb/proc/actually_explode() + if(!core) + play_cinematic(/datum/cinematic/nuke/no_core, world) + SSticker.roundend_check_paused = FALSE + return + + SSlag_switch.set_measure(DISABLE_NON_OBSJOBS, TRUE) + + var/detonation_status + var/turf/bomb_location = get_turf(src) + var/area/nuke_area = get_area(bomb_location) + + // The nuke was on the station zlevel + if(bomb_location && is_station_level(bomb_location.z)) + // Nuke missed, it's in space + if(istype(nuke_area, /area/space)) + detonation_status = DETONATION_NEAR_MISSED_STATION + + // Nuke missed, it'stoo far from the station + else if((bomb_location.x < (128 - NUKE_RADIUS)) \ + || (bomb_location.x > (128 + NUKE_RADIUS)) \ + || (bomb_location.y < (128 - NUKE_RADIUS)) \ + || (bomb_location.y > (128 + NUKE_RADIUS))) + + detonation_status = DETONATION_NEAR_MISSED_STATION + + // Confirming good hits, the nuke hit the station + else + detonation_status = DETONATION_HIT_STATION + GLOB.station_was_nuked = TRUE + + // The nuke was on the syndicate base + else if(bomb_location.onSyndieBase()) + detonation_status = DETONATION_HIT_SYNDIE_BASE + + // The nuke was somewhere wacky - deep space, mining z, centcom? Whatever + else + detonation_status = DETONATION_MISSED_STATION + + /* + if(detonation_status < NUKE_MISS_STATION) + SSshuttle.registerHostileEnvironment(src) + SSshuttle.lockdown = TRUE + */ + + // Missing the station will register a hostile environment, until it actually explodes + if(detonation_status == DETONATION_MISSED_STATION) + SSshuttle.registerHostileEnvironment(src) + SSshuttle.lockdown = TRUE + + // Now go play the cinematic + GLOB.station_nuke_source = detonation_status + really_actually_explode(detonation_status) + SSticker.roundend_check_paused = FALSE + + return detonation_status + +/obj/machinery/nuclearbomb/proc/really_actually_explode(detonation_status) + play_cinematic(get_cinematic_type(detonation_status), world, CALLBACK(SSticker, /datum/controller/subsystem/ticker/proc/station_explosion_detonation, src)) + + var/turf/bomb_location = get_turf(src) + var/list/z_levels_to_blow = list() + if(detonation_status == DETONATION_HIT_STATION) + z_levels_to_blow |= SSmapping.levels_by_trait(ZTRAIT_STATION) + + // Don't kill people in the station if the nuke missed, even if we are technically on the same z-level + else if(detonation_status != DETONATION_NEAR_MISSED_STATION) + z_levels_to_blow |= bomb_location.z + + if(length(z_levels_to_blow)) + nuke_effects(z_levels_to_blow) + + return TRUE + +/// Cause nuke effects to the passed z-levels. +/obj/machinery/nuclearbomb/proc/nuke_effects(list/affected_z_levels) + INVOKE_ASYNC(GLOBAL_PROC, /proc/callback_on_everyone_on_z, affected_z_levels, CALLBACK(GLOBAL_PROC, /proc/nuke_gib), src) + +/// Gets what type of cinematic this nuke showcases depending on where we detonated. +/obj/machinery/nuclearbomb/proc/get_cinematic_type(detonation_status) + if(isnull(detonation_status)) + return /datum/cinematic/nuke/self_destruct_miss + + return /datum/cinematic/nuke/self_destruct + +#undef NUKE_RADIUS + +/** + * Helper proc that handles gibbing someone who has been nuked. + */ +/proc/nuke_gib(mob/living/gibbed, atom/source) + if(istype(gibbed.loc, /obj/structure/closet/secure_closet/freezer)) + var/obj/structure/closet/secure_closet/freezer/freezer = gibbed.loc + if(!freezer.jones) + to_chat(gibbed, span_boldannounce("You hold onto [freezer] as [source] goes off. \ + Luckily, as [freezer] is lead-lined, you survive.")) + freezer.jones = TRUE + return FALSE + + if(gibbed.stat == DEAD) + return FALSE + + to_chat(gibbed, span_userdanger("You are shredded to atoms by [source]!")) + gibbed.gib() + return TRUE + +/** + * Invokes a callback on every living mob on the provided z level. + */ +/proc/callback_on_everyone_on_z(list/z_levels, datum/callback/to_do, atom/optional_source) + if(!islist(z_levels)) + CRASH("callback_on_everyone_on_z called [z_levels ? "with an invalid z-level list":"without any z-levels"].") + + for(var/mob/living/victim as anything in GLOB.mob_living_list) + if(QDELETED(victim) || isnull(victim.loc)) + continue + + var/turf/target_turf = get_turf(victim) + if(target_turf && !(target_turf.z in z_levels)) + continue + + to_do.Invoke(victim, optional_source) diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/beer_nuke.dm b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/beer_nuke.dm new file mode 100644 index 00000000000..28d2413e154 --- /dev/null +++ b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/beer_nuke.dm @@ -0,0 +1,77 @@ +/// A fake nuke that actually contains beer. +/obj/machinery/nuclearbomb/beer + name = "\improper Nanotrasen-brand nuclear fission explosive" + desc = "One of the more successful achievements of the Nanotrasen Corporate Warfare Division, their nuclear fission explosives are renowned for being cheap to produce and devastatingly effective. Signs explain that though this particular device has been decommissioned, every Nanotrasen station is equipped with an equivalent one, just in case. All Captains carefully guard the disk needed to detonate them - at least, the sign says they do. There seems to be a tap on the back." + proper_bomb = FALSE + /// The keg located within the beer nuke. + var/obj/structure/reagent_dispensers/beerkeg/keg + +/obj/machinery/nuclearbomb/beer/Initialize(mapload) + . = ..() + keg = new(src) + QDEL_NULL(core) + +/obj/machinery/nuclearbomb/beer/examine(mob/user) + . = ..() + if(keg.reagents.total_volume) + . += span_notice("It has [keg.reagents.total_volume] unit\s left.") + else + . += span_danger("It's empty.") + +/obj/machinery/nuclearbomb/beer/attackby(obj/item/weapon, mob/user, params) + if(weapon.is_refillable()) + weapon.afterattack(keg, user, TRUE) // redirect refillable containers to the keg, allowing them to be filled + return TRUE // pretend we handled the attack, too. + + if(istype(weapon, /obj/item/nuke_core_container)) + to_chat(user, span_notice("[src] has had its plutonium core removed as a part of being decommissioned.")) + return TRUE + + return ..() + +/obj/machinery/nuclearbomb/beer/actually_explode() + //Unblock roundend, we're not actually exploding. + SSticker.roundend_check_paused = FALSE + var/turf/bomb_location = get_turf(src) + if(!bomb_location) + disarm_nuke() + return + if(is_station_level(bomb_location.z)) + addtimer(CALLBACK(src, .proc/really_actually_explode), 11 SECONDS) + else + visible_message(span_notice("[src] fizzes ominously.")) + addtimer(CALLBACK(src, .proc/local_foam), 11 SECONDS) + +/obj/machinery/nuclearbomb/beer/disarm_nuke(mob/disarmer) + exploding = FALSE + exploded = TRUE + return ..() + +/obj/machinery/nuclearbomb/beer/proc/local_foam() + var/datum/reagents/R = new/datum/reagents(1000) + R.my_atom = src + R.add_reagent(/datum/reagent/consumable/ethanol/beer, 100) + + var/datum/effect_system/fluid_spread/foam/foam = new + foam.set_up(200, location = get_turf(src), carry = R) + foam.start() + disarm_nuke() + +/obj/machinery/nuclearbomb/beer/proc/stationwide_foam() + priority_announce("The scrubbers network is experiencing a backpressure surge. Some ejection of contents may occur.", "Atmospherics alert") + + for (var/obj/machinery/atmospherics/components/unary/vent_scrubber/vent in GLOB.machines) + var/turf/vent_turf = get_turf(vent) + if (!vent_turf || !is_station_level(vent_turf.z) || vent.welded) + continue + + var/datum/reagents/beer = new /datum/reagents(1000) + beer.my_atom = vent + beer.add_reagent(/datum/reagent/consumable/ethanol/beer, 100) + beer.create_foam(/datum/effect_system/fluid_spread/foam, DIAMOND_AREA(10)) + + CHECK_TICK + +/obj/machinery/nuclearbomb/beer/really_actually_explode(detonation_status) + disarm_nuke() + stationwide_foam() diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/self_destruct.dm b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/self_destruct.dm new file mode 100644 index 00000000000..af6d92e190e --- /dev/null +++ b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/self_destruct.dm @@ -0,0 +1,23 @@ +/obj/machinery/nuclearbomb/selfdestruct + name = "station self-destruct terminal" + desc = "For when it all gets too much to bear. Do not taunt." + icon = 'icons/obj/machines/nuke_terminal.dmi' + icon_state = "nuclearbomb_base" + anchored = TRUE //stops it being moved + +/obj/machinery/nuclearbomb/selfdestruct/set_anchor() + return + +/obj/machinery/nuclearbomb/selfdestruct/toggle_nuke_safety() + . = ..() + if(timing) + SSmapping.add_nuke_threat(src) + else + SSmapping.remove_nuke_threat(src) + +/obj/machinery/nuclearbomb/selfdestruct/toggle_nuke_armed() + . = ..() + if(timing) + SSmapping.add_nuke_threat(src) + else + SSmapping.remove_nuke_threat(src) diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/syndicate_nuke.dm b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/syndicate_nuke.dm new file mode 100644 index 00000000000..a541322ab47 --- /dev/null +++ b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/syndicate_nuke.dm @@ -0,0 +1,30 @@ +/obj/machinery/nuclearbomb/syndicate + +/obj/machinery/nuclearbomb/syndicate/get_cinematic_type(detonation_status) + switch(detonation_status) + // The nuke detonated on station + if(DETONATION_HIT_STATION) + // And it was detonated by nuke ops + if(length(get_antag_minds(/datum/antagonist/nukeop))) + if(is_infiltrator_docked_at_centcom()) + // And the ops escaped, they won! + return /datum/cinematic/nuke/ops_victory + else + // And the ops failed to escape, mutually assured destruction! + return /datum/cinematic/nuke/mutual_destruction + + // It was detonated by something or someone what wasn't nuke ops, + // throw it to the default explosion animation (self destruct) + else + return ..() + + // The nuke detonated on station Z, but in space + if(DETONATION_NEAR_MISSED_STATION) + return /datum/cinematic/nuke/ops_miss + + // The nuke detonated off station Z, and/or on the syndicate base + if(DETONATION_HIT_SYNDIE_BASE, DETONATION_MISSED_STATION) + return /datum/cinematic/nuke/far_explosion + + stack_trace("[type] - get_cinematic_type got a detonation_status it was not expecting. (Got: [detonation_status])") + return /datum/cinematic/nuke/far_explosion diff --git a/code/modules/antagonists/nukeop/nukeop.dm b/code/modules/antagonists/nukeop/nukeop.dm index 927db80dd0c..28e063d00dd 100644 --- a/code/modules/antagonists/nukeop/nukeop.dm +++ b/code/modules/antagonists/nukeop/nukeop.dm @@ -338,56 +338,77 @@ O.team = src objectives += O -/datum/team/nuclear/proc/disk_rescued() - for(var/obj/item/disk/nuclear/D in SSpoints_of_interest.real_nuclear_disks) +/datum/team/nuclear/proc/is_disk_rescued() + for(var/obj/item/disk/nuclear/nuke_disk in SSpoints_of_interest.real_nuclear_disks) //If emergency shuttle is in transit disk is only safe on it if(SSshuttle.emergency.mode == SHUTTLE_ESCAPE) - if(!SSshuttle.emergency.is_in_shuttle_bounds(D)) + if(!SSshuttle.emergency.is_in_shuttle_bounds(nuke_disk)) return FALSE //If shuttle escaped check if it's on centcom side else if(SSshuttle.emergency.mode == SHUTTLE_ENDGAME) - if(!D.onCentCom()) + if(!nuke_disk.onCentCom()) return FALSE else //Otherwise disk is safe when on station - var/turf/T = get_turf(D) - if(!T || !is_station_level(T.z)) + var/turf/disk_turf = get_turf(nuke_disk) + if(!disk_turf || !is_station_level(disk_turf.z)) return FALSE return TRUE -/datum/team/nuclear/proc/operatives_dead() - for(var/I in members) - var/datum/mind/operative_mind = I +/datum/team/nuclear/proc/are_all_operatives_dead() + for(var/datum/mind/operative_mind as anything in members) if(ishuman(operative_mind.current) && (operative_mind.current.stat != DEAD)) return FALSE return TRUE /datum/team/nuclear/proc/get_result() - var/evacuation = EMERGENCY_ESCAPED_OR_ENDGAMED - var/disk_rescued = disk_rescued() - var/syndies_didnt_escape = !syndies_escaped() + var/shuttle_evacuated = EMERGENCY_ESCAPED_OR_ENDGAMED + var/disk_rescued = is_disk_rescued() + var/syndies_didnt_escape = !is_infiltrator_docked_at_centcom() + var/team_is_dead = are_all_operatives_dead() var/station_was_nuked = GLOB.station_was_nuked var/station_nuke_source = GLOB.station_nuke_source - if(station_nuke_source == NUKE_SYNDICATE_BASE) + // The nuke detonated on the syndicate base + if(station_nuke_source == DETONATION_HIT_SYNDIE_BASE) return NUKE_RESULT_FLUKE - else if(station_was_nuked && !syndies_didnt_escape) - return NUKE_RESULT_NUKE_WIN - else if (station_was_nuked && syndies_didnt_escape) - return NUKE_RESULT_NOSURVIVORS - else if (!disk_rescued && !station_was_nuked && station_nuke_source && !syndies_didnt_escape) - return NUKE_RESULT_WRONG_STATION - else if (!disk_rescued && !station_was_nuked && station_nuke_source && syndies_didnt_escape) - return NUKE_RESULT_WRONG_STATION_DEAD - else if ((disk_rescued && evacuation) && operatives_dead()) - return NUKE_RESULT_CREW_WIN_SYNDIES_DEAD - else if (disk_rescued) - return NUKE_RESULT_CREW_WIN - else if (!disk_rescued && operatives_dead()) - return NUKE_RESULT_DISK_LOST - else if (!disk_rescued && evacuation) - return NUKE_RESULT_DISK_STOLEN + + // The station was nuked + if(station_was_nuked) + // The station was nuked and the infiltrator failed to escape + if(syndies_didnt_escape) + return NUKE_RESULT_NOSURVIVORS + // The station was nuked and the infiltrator escaped, and the nuke ops won + else + return NUKE_RESULT_NUKE_WIN + + // The station was not nuked, but something was + else if(station_nuke_source && !disk_rescued) + // The station was not nuked, but something was, and the syndicates didn't escape it + if(syndies_didnt_escape) + return NUKE_RESULT_WRONG_STATION_DEAD + // The station was not nuked, but something was, and the syndicates returned to their base + else + return NUKE_RESULT_WRONG_STATION + + // No nuke went off, the station rescued the disk + else if(disk_rescued) + // No nuke went off, the shuttle left, and the team is dead + if(shuttle_evacuated && team_is_dead) + return NUKE_RESULT_CREW_WIN_SYNDIES_DEAD + // No nuke went off, but the nuke ops survived + else + return NUKE_RESULT_CREW_WIN + + // No nuke went off, but the disk was left behind else - return //Undefined result + // No nuke went off, the disk was left, but all the ops are dead + if(team_is_dead) + return NUKE_RESULT_DISK_LOST + // No nuke went off, the disk was left, there are living ops, but the shuttle left successfully + else if(shuttle_evacuated) + return NUKE_RESULT_DISK_STOLEN + + CRASH("[type] - got an undefined / unexpected result.") /datum/team/nuclear/roundend_report() var/list/parts = list() @@ -401,10 +422,10 @@ parts += "Syndicate Major Victory!" parts += "[syndicate_name] operatives have destroyed [station_name()]!" if(NUKE_RESULT_NOSURVIVORS) - parts += "Total Annihilation" + parts += "Total Annihilation!" parts += "[syndicate_name] operatives destroyed [station_name()] but did not leave the area in time and got caught in the explosion. Next time, don't lose the disk!" if(NUKE_RESULT_WRONG_STATION) - parts += "Crew Minor Victory" + parts += "Crew Minor Victory!" parts += "[syndicate_name] operatives secured the authentication disk but blew up something that wasn't [station_name()]. Next time, don't do that!" if(NUKE_RESULT_WRONG_STATION_DEAD) parts += "[syndicate_name] operatives have earned Darwin Award!" @@ -413,7 +434,7 @@ parts += "Crew Major Victory!" parts += "The Research Staff has saved the disk and killed the [syndicate_name] Operatives" if(NUKE_RESULT_CREW_WIN) - parts += "Crew Major Victory" + parts += "Crew Major Victory!" parts += "The Research Staff has saved the disk and stopped the [syndicate_name] Operatives!" if(NUKE_RESULT_DISK_LOST) parts += "Neutral Victory!" @@ -438,7 +459,7 @@ text += printplayerlist(members) text += "
" text += "(Syndicates used [TC_uses] TC) [purchases]" - if(TC_uses == 0 && GLOB.station_was_nuked && !operatives_dead()) + if(TC_uses == 0 && GLOB.station_was_nuked && !are_all_operatives_dead()) text += "[icon2html('icons/ui_icons/antags/badass.dmi', world, "badass")]" parts += text @@ -475,7 +496,8 @@ return common_part + disk_report + challenge_report /// Returns whether or not syndicate operatives escaped. -/proc/syndies_escaped() - var/obj/docking_port/mobile/S = SSshuttle.getShuttle("syndicate") - var/obj/docking_port/stationary/transit/T = locate() in S.loc - return S && (is_centcom_level(S.z) || T) +/proc/is_infiltrator_docked_at_centcom() + var/obj/docking_port/mobile/infiltrator/infiltrator_port = SSshuttle.getShuttle("syndicate") + var/obj/docking_port/stationary/transit/infiltrator_dock = locate() in infiltrator_port.loc + + return infiltrator_port && (is_centcom_level(infiltrator_port.z) || infiltrator_dock) diff --git a/code/modules/antagonists/traitor/equipment/Malf_Modules.dm b/code/modules/antagonists/traitor/equipment/Malf_Modules.dm index 8ee989ee12a..8a9b21a10bf 100644 --- a/code/modules/antagonists/traitor/equipment/Malf_Modules.dm +++ b/code/modules/antagonists/traitor/equipment/Malf_Modules.dm @@ -358,25 +358,25 @@ GLOBAL_LIST_INIT(malf_modules, subtypesof(/datum/ai_module)) var/sec_left = seconds_remaining() if(!sec_left) timing = FALSE - detonate() + sound_to_playing_players('sound/machines/alarm.ogg') + addtimer(CALLBACK(GLOBAL_PROC, /proc/play_cinematic, /datum/cinematic/malf, world, CALLBACK(src, .proc/trigger_doomsday)), 10 SECONDS) + else if(world.time >= next_announce) minor_announce("[sec_left] SECONDS UNTIL DOOMSDAY DEVICE ACTIVATION!", "ERROR ER0RR $R0RRO$!R41.%%!!(%$^^__+ @#F0E4", TRUE) next_announce += DOOMSDAY_ANNOUNCE_INTERVAL -/obj/machinery/doomsday_device/proc/detonate() - sound_to_playing_players('sound/machines/alarm.ogg') - sleep(100) - for(var/i in GLOB.mob_living_list) - var/mob/living/L = i - var/turf/T = get_turf(L) - if(!T || !is_station_level(T.z)) - continue - if(issilicon(L)) - continue - to_chat(L, span_userdanger("The blast wave from [src] tears you atom from atom!")) - L.dust() - to_chat(world, "The AI cleansed the station of life with the doomsday device!") - SSticker.force_ending = 1 +/obj/machinery/doomsday_device/proc/trigger_doomsday() + callback_on_everyone_on_z(SSmapping.levels_by_trait(ZTRAIT_STATION), CALLBACK(GLOBAL_PROC, /proc/bring_doomsday), src) + to_chat(world, span_bold("The AI cleansed the station of life with [src]!")) + SSticker.force_ending = TRUE + +/proc/bring_doomsday(mob/living/victim, atom/source) + if(issilicon(victim)) + return FALSE + + to_chat(victim, span_userdanger("The blast wave from [source] tears you atom from atom!")) + victim.dust() + return TRUE /// Hostile Station Lockdown: Locks, bolts, and electrifies every airlock on the station. After 90 seconds, the doors reset. /datum/ai_module/destructive/lockdown diff --git a/code/modules/events/wizard/fakeexplosion.dm b/code/modules/events/wizard/fakeexplosion.dm index 58580648196..3d9be6a565d 100644 --- a/code/modules/events/wizard/fakeexplosion.dm +++ b/code/modules/events/wizard/fakeexplosion.dm @@ -7,5 +7,4 @@ /datum/round_event/wizard/fake_explosion/start() sound_to_playing_players('sound/machines/alarm.ogg') - sleep(100) - Cinematic(CINEMATIC_NUKE_FAKE,world) + addtimer(CALLBACK(GLOBAL_PROC, /proc/play_cinematic, /datum/cinematic/nuke/fake, world), 10 SECONDS) diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index 7a7cd1ab262..d27ce7d9ac2 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -428,7 +428,7 @@ if((GLOB.cult_narsie.souls == GLOB.cult_narsie.soul_goal) && (GLOB.cult_narsie.resolved == FALSE)) GLOB.cult_narsie.resolved = TRUE sound_to_playing_players('sound/machines/alarm.ogg') - addtimer(CALLBACK(GLOBAL_PROC, .proc/cult_ending_helper, 1), 120) + addtimer(CALLBACK(GLOBAL_PROC, .proc/cult_ending_helper, CULT_VICTORY_MASS_CONVERSION), 120) addtimer(CALLBACK(GLOBAL_PROC, .proc/ending_helper), 270) if(client) makeNewConstruct(/mob/living/simple_animal/hostile/construct/harvester, src, cultoverride = TRUE) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index fb9118ea448..e4d8b062a34 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -43,6 +43,10 @@ ghostize() //False, since we're deleting it currently if(mind?.current == src) //Let's just be safe yeah? This will occasionally be cleared, but not always. Can't do it with ghostize without changing behavior mind.set_current(null) + + if(mock_client) + mock_client.mob = null + return ..() diff --git a/code/modules/power/singularity/narsie.dm b/code/modules/power/singularity/narsie.dm index cd98053eb45..b68052c0d86 100644 --- a/code/modules/power/singularity/narsie.dm +++ b/code/modules/power/singularity/narsie.dm @@ -218,7 +218,7 @@ if(QDELETED(GLOB.cult_narsie)) // uno priority_announce("Status report? We detected an anomaly, but it disappeared almost immediately.","Central Command Higher Dimensional Affairs", 'sound/misc/notice1.ogg') GLOB.cult_narsie = null - addtimer(CALLBACK(GLOBAL_PROC, .proc/cult_ending_helper, 2), 2 SECONDS) + addtimer(CALLBACK(GLOBAL_PROC, .proc/cult_ending_helper, CULT_FAILURE_NARSIE_KILLED), 2 SECONDS) return priority_announce("An acausal dimensional event has been detected in your sector. Event has been flagged EXTINCTION-CLASS. Directing all available assets toward simulating solutions. SOLUTION ETA: 60 SECONDS.","Central Command Higher Dimensional Affairs", 'sound/misc/airraid.ogg') addtimer(CALLBACK(GLOBAL_PROC, .proc/narsie_end_second_check), 50 SECONDS) @@ -228,7 +228,7 @@ if(QDELETED(GLOB.cult_narsie)) // dos priority_announce("Simulations aborted, sensors report that the acasual event is normalizing. Good work, crew.","Central Command Higher Dimensional Affairs", 'sound/misc/notice1.ogg') GLOB.cult_narsie = null - addtimer(CALLBACK(GLOBAL_PROC, .proc/cult_ending_helper, 2), 2 SECONDS) + addtimer(CALLBACK(GLOBAL_PROC, .proc/cult_ending_helper, CULT_FAILURE_NARSIE_KILLED), 2 SECONDS) return priority_announce("Simulations on acausal dimensional event complete. Deploying solution package now. Deployment ETA: ONE MINUTE. ","Central Command Higher Dimensional Affairs") addtimer(CALLBACK(GLOBAL_PROC, .proc/narsie_start_destroy_station), 5 SECONDS) @@ -257,7 +257,7 @@ /proc/narsie_last_second_win() set_security_level("red") SSshuttle.lockdown = FALSE - INVOKE_ASYNC(GLOBAL_PROC, .proc/cult_ending_helper, 2) + INVOKE_ASYNC(GLOBAL_PROC, .proc/cult_ending_helper, CULT_FAILURE_NARSIE_KILLED) ///Helper to set the round to end asap. Current usage Cult round end code /proc/ending_helper() @@ -267,13 +267,19 @@ * Selects cinematic to play as part of the cult end depending on the outcome then ends the round afterward * called either when narsie eats everyone, or when [/proc/begin_the_end()] reaches it's conclusion */ -/proc/cult_ending_helper(ending_type = 0) - if(ending_type == 2) //narsie fukkin died - Cinematic(CINEMATIC_CULT_FAIL,world,CALLBACK(GLOBAL_PROC,/proc/ending_helper)) - else if(ending_type) //no explosion - Cinematic(CINEMATIC_CULT,world,CALLBACK(GLOBAL_PROC,/proc/ending_helper)) - else // explosion - Cinematic(CINEMATIC_CULT_NUKE,world,CALLBACK(GLOBAL_PROC,/proc/ending_helper)) +/proc/cult_ending_helper(ending_type = CULT_VICTORY_NUKE) + switch(ending_type) + // Narsie was killed + if(CULT_FAILURE_NARSIE_KILLED) + play_cinematic(/datum/cinematic/cult_fail, world, CALLBACK(GLOBAL_PROC, /proc/ending_helper)) + + // The cult "converted" (harvested) most of the station + if(CULT_VICTORY_MASS_CONVERSION) + play_cinematic(/datum/cinematic/cult_arm, world, CALLBACK(GLOBAL_PROC, /proc/ending_helper)) + + // The cult won, but centcom deployed a nuke. Default + if(CULT_VICTORY_NUKE) + play_cinematic(/datum/cinematic/nuke/cult, world, CALLBACK(GLOBAL_PROC, /proc/ending_helper)) #undef NARSIE_CHANCE_TO_PICK_NEW_TARGET #undef NARSIE_CONSUME_RANGE diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index f7998f43cb1..5ab519d5aed 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -111,6 +111,7 @@ #include "modsuit.dm" #include "modular_map_loader.dm" #include "ntnetwork_tests.dm" +#include "nuke_cinematic.dm" #include "objectives.dm" #include "outfit_sanity.dm" #include "paintings.dm" diff --git a/code/modules/unit_tests/nuke_cinematic.dm b/code/modules/unit_tests/nuke_cinematic.dm new file mode 100644 index 00000000000..c0b377a29a7 --- /dev/null +++ b/code/modules/unit_tests/nuke_cinematic.dm @@ -0,0 +1,68 @@ +// Some defines for tracking if the correct cinematic / animation is playing. +#define PLAYING_CORRECT_ANIMATION 2 +#define PLAYING_INCORRECT_NUKE_ANIMATION 1 +#define NOT_PLAYING_ANIMATION 0 + +/** + * Unit tests that a nuke going off plays a cinematic, + * and that it actually kills people. + */ +/datum/unit_test/nuke_cinematic + /// Used to track via signal if the correct cinematic / animation is playing. + var/cinematic_playing = NOT_PLAYING_ANIMATION + /// Tracks what typepath of cinematic is being played. + var/cinematic_playing_type + +/datum/unit_test/nuke_cinematic/Run() + var/obj/machinery/nuclearbomb/syndicate/nuke = allocate(/obj/machinery/nuclearbomb/syndicate) + var/mob/living/carbon/human/nuked = allocate(/mob/living/carbon/human) + var/datum/client_interface/mock_client = new() + nuked.mock_client = mock_client + mock_client.mob = nuked + + var/obj/effect/landmark/observer_start/observer_point = locate(/obj/effect/landmark/observer_start) in GLOB.landmarks_list + TEST_ASSERT_NOTNULL(observer_point, "Nuke cinematic test couldn't find observer spawn to place the nuke.") + + var/turf/turf_on_station = get_turf(observer_point) + TEST_ASSERT(is_station_level(turf_on_station.z), "Nuke cinematic test didn't get a turf which was located on the station.") + + nuke.forceMove(turf_on_station) + nuked.forceMove(turf_on_station) + + // Pause the check so we don't, y'know, end the round + SSticker.roundend_check_paused = TRUE + RegisterSignal(SSdcs, COMSIG_GLOB_PLAY_CINEMATIC, .proc/check_cinematic) + // actually_explode calls really_actually_explode which sleeps, so this will take a moment. + var/nuke_result = nuke.actually_explode() + + TEST_ASSERT_EQUAL(nuke_result, DETONATION_HIT_STATION, "A nuke went off on station, but didn't return DETONATION_HIT_STATION (4). (Got: [nuke_result])") + TEST_ASSERT(GLOB.station_was_nuked, "A nuke went off on station, but didn't set station_was_nuked.") + // Reset the nuke var back so we don't end the round + GLOB.station_was_nuked = FALSE + SSticker.roundend_check_paused = FALSE + + switch(cinematic_playing) + if(NOT_PLAYING_ANIMATION) + TEST_FAIL("No nuke cinematic was played when a nuke was detonated.") + + if(PLAYING_INCORRECT_NUKE_ANIMATION) + TEST_FAIL("An incorrect cinematic was played on nuke detonation. (Expected: /datum/cinematic/nuke/self_destruct, Got: [cinematic_playing_type])") + + TEST_ASSERT(QDELETED(nuked), "The nuke victim next to the nuke wasn't gibbed by the nuke.") + TEST_ASSERT(QDELETED(nuke), "The nuke itself was not deleted after successfully exploding.") + mock_client.mob = null + +/// Used to track whenever a cinematic starts playing, so we can check if it's the right one. +/datum/unit_test/nuke_cinematic/proc/check_cinematic(datum/source, datum/cinematic/playing) + SIGNAL_HANDLER + + cinematic_playing_type = playing.type + if(istype(playing, /datum/cinematic/nuke/self_destruct)) + cinematic_playing = PLAYING_CORRECT_ANIMATION + + else if(istype(playing, /datum/cinematic/nuke)) + cinematic_playing = PLAYING_INCORRECT_NUKE_ANIMATION + +#undef PLAYING_CORRECT_ANIMATION +#undef PLAYING_INCORRECT_NUKE_ANIMATION +#undef NOT_PLAYING_ANIMATION diff --git a/tgstation.dme b/tgstation.dme index afc3eb5daec..b2cf315f62d 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -49,7 +49,6 @@ #include "code\__DEFINES\cargo.dm" #include "code\__DEFINES\chat.dm" #include "code\__DEFINES\chat_filter.dm" -#include "code\__DEFINES\cinematics.dm" #include "code\__DEFINES\cleaning.dm" #include "code\__DEFINES\clothing.dm" #include "code\__DEFINES\colors.dm" @@ -122,6 +121,7 @@ #include "code\__DEFINES\movespeed_modification.dm" #include "code\__DEFINES\networks.dm" #include "code\__DEFINES\nitrile.dm" +#include "code\__DEFINES\nuclear_bomb.dm" #include "code\__DEFINES\obj_flags.dm" #include "code\__DEFINES\paintings.dm" #include "code\__DEFINES\paper.dm" @@ -631,7 +631,6 @@ #include "code\datums\browser.dm" #include "code\datums\callback.dm" #include "code\datums\chatmessage.dm" -#include "code\datums\cinematic.dm" #include "code\datums\dash_weapon.dm" #include "code\datums\datacore.dm" #include "code\datums\datum.dm" @@ -758,6 +757,10 @@ #include "code\datums\brain_damage\special.dm" #include "code\datums\brain_damage\split_personality.dm" #include "code\datums\changelog\changelog.dm" +#include "code\datums\cinematics\_cinematic.dm" +#include "code\datums\cinematics\malf_doomsday.dm" +#include "code\datums\cinematics\narsie_summon.dm" +#include "code\datums\cinematics\nuke_cinematics.dm" #include "code\datums\components\_component.dm" #include "code\datums\components\acid.dm" #include "code\datums\components\admin_popup.dm" @@ -2267,9 +2270,13 @@ #include "code\modules\antagonists\nukeop\nukeop.dm" #include "code\modules\antagonists\nukeop\outfits.dm" #include "code\modules\antagonists\nukeop\equipment\borgchameleon.dm" +#include "code\modules\antagonists\nukeop\equipment\nuclear_authentication_disk.dm" #include "code\modules\antagonists\nukeop\equipment\nuclear_challenge.dm" -#include "code\modules\antagonists\nukeop\equipment\nuclearbomb.dm" #include "code\modules\antagonists\nukeop\equipment\pinpointer.dm" +#include "code\modules\antagonists\nukeop\equipment\nuclear_bomb\_nuclear_bomb.dm" +#include "code\modules\antagonists\nukeop\equipment\nuclear_bomb\beer_nuke.dm" +#include "code\modules\antagonists\nukeop\equipment\nuclear_bomb\self_destruct.dm" +#include "code\modules\antagonists\nukeop\equipment\nuclear_bomb\syndicate_nuke.dm" #include "code\modules\antagonists\pirate\pirate.dm" #include "code\modules\antagonists\revenant\revenant.dm" #include "code\modules\antagonists\revenant\revenant_abilities.dm"