diff --git a/code/__DEFINES/credits.dm b/code/__DEFINES/credits.dm index 2b3f0f734d2a..77ecd66f2705 100644 --- a/code/__DEFINES/credits.dm +++ b/code/__DEFINES/credits.dm @@ -1 +1,3 @@ #define BLACKBOX_FEEDBACK_NUM(key) (SSblackbox.feedback[key] ? SSblackbox.feedback[key].json["data"] : null) + +#define BLACKBOX_FEEDBACK_NESTED_TALLY(key) (SSblackbox.feedback[key] ? SSblackbox.feedback[key].json["data"] : null) diff --git a/code/__DEFINES/dcs/signals/signals_area.dm b/code/__DEFINES/dcs/signals/signals_area.dm index e82027baab2f..406d168a51e2 100644 --- a/code/__DEFINES/dcs/signals/signals_area.dm +++ b/code/__DEFINES/dcs/signals/signals_area.dm @@ -18,3 +18,8 @@ #define COMSIG_ALARM_TRIGGERED "comsig_alarm_triggered" ///Send when an alarm source is cleared (alarm_type, area/source_area) #define COMSIG_ALARM_CLEARED "comsig_alarm_clear" + + +// Spook level signals +///from base of area/proc/adjust_spook_level(): (area, old_spook_level) +#define AREA_SPOOK_LEVEL_CHANGED "comsig_area_spook_level_changed" diff --git a/code/__DEFINES/spooky_defines.dm b/code/__DEFINES/spooky_defines.dm new file mode 100644 index 000000000000..5de5edd00f77 --- /dev/null +++ b/code/__DEFINES/spooky_defines.dm @@ -0,0 +1,17 @@ +#define SPOOK_LEVEL_WEAK_POWERS 10 +#define SPOOK_LEVEL_MEDIUM_POWERS 30 +#define SPOOK_LEVEL_DESTRUCTIVE_POWERS 70 + +#define SPOOK_LEVEL_OBJECT_ROTATION SPOOK_LEVEL_WEAK_POWERS + +// Spook amounts +#define SPOOK_AMT_CORPSE 5 +#define SPOOK_AMT_BLOOD_SPLATTER 2 +#define SPOOK_AMT_BLOOD_STREAK 1 +#define SPOOK_AMT_BLOOD_DROP 0.5 + +#define RECORD_GHOST_POWER(power) \ + do {\ + var/area/A = get_area(power.owner); \ + SSblackbox.record_feedback("nested tally", "ghost_power_used", 1, list(A?.name || "NULL", power.name)); \ + } while (FALSE) diff --git a/code/_onclick/hud/rendering/plane_master.dm b/code/_onclick/hud/rendering/plane_master.dm index c4c1f2c54bb3..3b8ba4000df0 100644 --- a/code/_onclick/hud/rendering/plane_master.dm +++ b/code/_onclick/hud/rendering/plane_master.dm @@ -34,14 +34,12 @@ /atom/movable/screen/plane_master/floor name = "floor plane master" plane = FLOOR_PLANE - appearance_flags = PLANE_MASTER blend_mode = BLEND_OVERLAY ///Contains most things in the game world /atom/movable/screen/plane_master/game_world name = "game world plane master" plane = GAME_PLANE - appearance_flags = PLANE_MASTER //should use client color blend_mode = BLEND_OVERLAY /atom/movable/screen/plane_master/seethrough @@ -52,20 +50,17 @@ /atom/movable/screen/plane_master/massive_obj name = "massive object plane master" plane = MASSIVE_OBJ_PLANE - appearance_flags = PLANE_MASTER //should use client color blend_mode = BLEND_OVERLAY /atom/movable/screen/plane_master/ghost name = "ghost plane master" plane = GHOST_PLANE - appearance_flags = PLANE_MASTER //should use client color blend_mode = BLEND_OVERLAY render_relay_plane = RENDER_PLANE_NON_GAME /atom/movable/screen/plane_master/point name = "point plane master" plane = POINT_PLANE - appearance_flags = PLANE_MASTER //should use client color blend_mode = BLEND_OVERLAY /** diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm index 74cfb1b446a2..87c37194aa69 100644 --- a/code/controllers/configuration/entries/game_options.dm +++ b/code/controllers/configuration/entries/game_options.dm @@ -266,8 +266,6 @@ min_val = 0 max_val = 100 -/datum/config_entry/flag/ghost_interaction - /datum/config_entry/flag/near_death_experience //If carbons can hear ghosts when unconscious and very close to death /datum/config_entry/flag/silent_ai diff --git a/code/controllers/subsystem/blackbox.dm b/code/controllers/subsystem/blackbox.dm index 08d11d1d2557..ff9516c61866 100644 --- a/code/controllers/subsystem/blackbox.dm +++ b/code/controllers/subsystem/blackbox.dm @@ -233,27 +233,35 @@ Versioning return if(!islist(FV.json["data"])) FV.json["data"] = list() + if(overwrite) FV.json["data"] = data else FV.json["data"] |= data + if("amount") FV.json["data"] += increment + if("tally") if(!islist(FV.json["data"])) FV.json["data"] = list() FV.json["data"]["[data]"] += increment + if("nested tally") if(!islist(data)) return + if(!islist(FV.json["data"])) FV.json["data"] = list() FV.json["data"] = record_feedback_recurse_list(FV.json["data"], data, increment) + if("associative") if(!islist(data)) return + if(!islist(FV.json["data"])) FV.json["data"] = list() + var/pos = length(FV.json["data"]) + 1 FV.json["data"]["[pos]"] = list() //in 512 "pos" can be replaced with "[FV.json["data"].len+1]" for(var/i in data) diff --git a/code/controllers/subsystem/credits.dm b/code/controllers/subsystem/credits.dm index 46fcc1cabd05..3f54e302a5aa 100644 --- a/code/controllers/subsystem/credits.dm +++ b/code/controllers/subsystem/credits.dm @@ -117,22 +117,23 @@ SUBSYSTEM_DEF(credits) if(customized_name) episode_name = customized_name return + var/list/drafted_names = list() - var/list/name_reasons = list() - var/list/is_rare_assoc_list = list() + for(var/datum/episode_name/N as anything in episode_names) - drafted_names["[N.thename]"] = N.weight - name_reasons["[N.thename]"] = N.reason - is_rare_assoc_list["[N.thename]"] = N.rare - episode_name = pick_weight(drafted_names) - episode_reason = name_reasons[episode_name] - if(is_rare_assoc_list[episode_name] == TRUE) + drafted_names[N] = N.weight + + var/datum/episode_name/chosen = pick_weight(drafted_names) + episode_name = chosen.thename + episode_reason = chosen.reason + if(chosen.rare) rare_episode_name = TRUE /datum/controller/subsystem/credits/proc/finalize_episodestring() var/season = time2text(world.timeofday,"YY") var/episodenum = GLOB.round_id || 1 - episode_string = "

SEASON [season] EPISODE [episodenum]
[episode_name]


" + var/reason = episode_reason ? "

[episode_reason]

" : "" + episode_string = "

SEASON [season] EPISODE [episodenum]
[episode_name]

[reason]
" log_game("So ends [is_rerun() ? "another rerun of " : ""]SEASON [season] EPISODE [episodenum] - [episode_name] ... [customized_ss]") /datum/controller/subsystem/credits/proc/finalize_disclaimerstring() diff --git a/code/datums/actions/cooldown_action.dm b/code/datums/actions/cooldown_action.dm index d185ff33b6a3..4daa9d435b94 100644 --- a/code/datums/actions/cooldown_action.dm +++ b/code/datums/actions/cooldown_action.dm @@ -5,6 +5,9 @@ check_flags = NONE transparent_when_unavailable = FALSE + /// If TRUE, will log action usage to the owner's action log. + var/write_log = FALSE + /// The actual next time this ability can be used var/next_use_time = 0 /// The stat panel this action shows up in the stat panel in. If null, will not show up. @@ -216,13 +219,20 @@ /// Intercepts client owner clicks to activate the ability /datum/action/cooldown/proc/InterceptClickOn(mob/living/caller, params, atom/target) - if(!IsAvailable(feedback = TRUE)) - return FALSE - if(!target) - return FALSE + . = TRUE + if(istext(params)) + params = params2list(params) + + if(params?[RIGHT_CLICK]) + unset_click_ability(caller, TRUE) + return + + if(!target || !IsAvailable(feedback = TRUE)) + return + // The actual action begins here if(!PreActivate(target)) - return FALSE + return TRUE // And if we reach here, the action was complete successfully if(unset_after_click) @@ -235,13 +245,21 @@ /datum/action/cooldown/proc/PreActivate(atom/target) if(SEND_SIGNAL(owner, COMSIG_MOB_ABILITY_STARTED, src) & COMPONENT_BLOCK_ABILITY_START) return + + if(!is_valid_target(target)) + return + // Note, that PreActivate handles no cooldowns at all by default. // Be sure to call StartCooldown() in Activate() where necessary. . = Activate(target) + // There is a possibility our action (or owner) is qdeleted in Activate(). if(!QDELETED(src) && !QDELETED(owner)) SEND_SIGNAL(owner, COMSIG_MOB_ABILITY_FINISHED, src) + if(owner?.ckey) + owner.log_message("used action [name][target != owner ? " on / at [target]":""].", LOG_ATTACK) + /// To be implemented by subtypes (if not generic) /datum/action/cooldown/proc/Activate(atom/target) var/total_delay = 0 @@ -251,6 +269,7 @@ addtimer(CALLBACK(ability, PROC_REF(Activate), target), total_delay) total_delay += initialized_actions[ability] StartCooldown() + return TRUE /// Cancels melee attacks if they are on cooldown. /datum/action/cooldown/proc/handle_melee_attack(mob/source, mob/target) @@ -330,3 +349,13 @@ SEND_SIGNAL(src, COMSIG_ACTION_SET_STATPANEL, stat_panel_data) return stat_panel_data + +/** + * Check if the target we're casting on is a valid target. + * For no-target (self cast) actions, the target being checked (cast_on) is the caster. + * For click_to_activate actions, the target being checked is the clicked atom. + * + * Return TRUE if cast_on is valid, FALSE otherwise + */ +/datum/action/cooldown/proc/is_valid_target(atom/cast_on) + return TRUE diff --git a/code/datums/components/rotation.dm b/code/datums/components/rotation.dm index 7d4e5514e06d..88113730db4f 100644 --- a/code/datums/components/rotation.dm +++ b/code/datums/components/rotation.dm @@ -118,8 +118,10 @@ /datum/component/simple_rotation/proc/CanUserRotate(mob/user, degrees) if(isliving(user) && user.canUseTopic(parent, USE_CLOSE|USE_DEXTERITY)) return TRUE - if((rotation_flags & ROTATION_GHOSTS_ALLOWED) && isobserver(user) && CONFIG_GET(flag/ghost_interaction)) - return TRUE + if((rotation_flags & ROTATION_GHOSTS_ALLOWED) && isobserver(user)) + var/area/A = get_area(parent) + if(A?.spook_level >= SPOOK_LEVEL_OBJECT_ROTATION) + return TRUE return FALSE /datum/component/simple_rotation/proc/CanBeRotated(mob/user, degrees) diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm index 9b2ccbd72849..034ff05da256 100644 --- a/code/game/area/areas.dm +++ b/code/game/area/areas.dm @@ -47,6 +47,9 @@ /// If a room is too big it doesn't have beauty. var/beauty_threshold = 150 + /// Used by ghosts to grant new powers. See /datum/component/spook_factor + var/spook_level + /// For space, the asteroid, etc. Used with blueprints or with weather to determine if we are adding a new area (vs editing a station room) var/outdoors = FALSE @@ -530,3 +533,9 @@ GLOBAL_LIST_EMPTY(teleportlocs) for(var/datum/listener in airalarms + firealarms + firedoors) SEND_SIGNAL(listener, COMSIG_FIRE_ALERT, code) + +/// Adjusts the spook level and sends out a signal +/area/proc/adjust_spook_level(adj) + var/old = spook_level + spook_level += adj + SEND_SIGNAL(src, AREA_SPOOK_LEVEL_CHANGED, src, old) diff --git a/code/game/area/areas/station.dm b/code/game/area/areas/station.dm index 0d2d22ef3497..ea348be3122b 100644 --- a/code/game/area/areas/station.dm +++ b/code/game/area/areas/station.dm @@ -935,6 +935,8 @@ min_ambience_cooldown = 90 SECONDS max_ambience_cooldown = 180 SECONDS + spook_level = SPOOK_AMT_CORPSE * -2 // We can expect like two dudes to be dead in here at all times. + /area/station/medical/abandoned name = "\improper Abandoned Medbay" icon_state = "abandoned_medbay" @@ -1010,6 +1012,8 @@ sound_environment = SOUND_AREA_SMALL_ENCLOSED lightswitch = FALSE + spook_level = SPOOK_AMT_CORPSE * -10 // The morgue lays spirits to rest or something + /area/station/medical/chemistry name = "Chemistry" icon_state = "chem" diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm index 3632d2270814..3f638c4e7670 100644 --- a/code/game/machinery/doors/door.dm +++ b/code/game/machinery/doors/door.dm @@ -575,7 +575,7 @@ DEFINE_INTERACTABLE(/obj/machinery/door) . = ..() /obj/machinery/door/proc/knock_on(mob/user) - user.changeNext_move(CLICK_CD_MELEE) + user?.changeNext_move(CLICK_CD_MELEE) playsound(src, knock_sound, 100, TRUE) #undef DOOR_CLOSE_WAIT diff --git a/code/game/objects/effects/decals/cleanable/humans.dm b/code/game/objects/effects/decals/cleanable/humans.dm index 9d1f98e0eda6..3cee7e72a266 100644 --- a/code/game/objects/effects/decals/cleanable/humans.dm +++ b/code/game/objects/effects/decals/cleanable/humans.dm @@ -3,12 +3,16 @@ desc = "It's red and gooey. Perhaps it's the chef's cooking?" icon = 'icons/effects/blood.dmi' icon_state = "floor1" + appearance_flags = TILE_BOUND|PIXEL_SCALE|LONG_GLIDE|NO_CLIENT_COLOR random_icon_states = list("floor1", "floor2", "floor3", "floor4", "floor5", "floor6", "floor7") blood_state = BLOOD_STATE_HUMAN bloodiness = BLOOD_AMOUNT_PER_DECAL beauty = -100 clean_type = CLEAN_TYPE_BLOOD + var/smell_intensity = INTENSITY_STRONG + var/spook_factor = SPOOK_AMT_BLOOD_SPLATTER + var/should_dry = TRUE /// How long should it take for blood to dry? var/dry_duration = 10 MINUTES @@ -54,6 +58,8 @@ bloodiness = 0 color = COLOR_GRAY //not all blood splatters have their own sprites... It still looks pretty nice qdel(GetComponent(/datum/component/smell)) + if(spook_factor) + AddComponent(/datum/component/spook_factor, spook_factor) return PROCESS_KILL /obj/effect/decal/cleanable/blood/replace_decal(obj/effect/decal/cleanable/blood/C) @@ -69,6 +75,7 @@ /obj/effect/decal/cleanable/blood/old/Initialize(mapload, list/datum/disease/diseases) add_blood_DNA(list("Non-human DNA" = random_blood_type())) // Needs to happen before ..() . = ..() + AddComponent(/datum/component/spook_factor, SPOOK_AMT_BLOOD_SPLATTER) /obj/effect/decal/cleanable/blood/splatter icon_state = "gibbl1" @@ -125,6 +132,7 @@ drydesc = "They look bloody and gruesome while some terrible smell fills the air." decal_reagent = /datum/reagent/liquidgibs reagent_amount = 5 + smell_intensity = INTENSITY_STRONG ///Information about the diseases our streaking spawns var/list/streak_diseases @@ -132,6 +140,7 @@ /obj/effect/decal/cleanable/blood/gibs/Initialize(mapload, list/datum/disease/diseases) . = ..() RegisterSignal(src, COMSIG_MOVABLE_PIPE_EJECTING, PROC_REF(on_pipe_eject)) + AddComponent(/datum/component/spook_factor, SPOOK_AMT_BLOOD_STREAK) /obj/effect/decal/cleanable/blood/gibs/replace_decal(obj/effect/decal/cleanable/C) return FALSE //Never fail to place us @@ -233,6 +242,7 @@ smell_intensity = INTENSITY_SUBTLE dry_duration = 4 MINUTES + spook_factor = SPOOK_AMT_BLOOD_DROP /// Keeps track of how many drops of blood this decal has. See blood.dm var/drips = 1 @@ -467,6 +477,8 @@ GLOBAL_LIST_EMPTY(bloody_footprints_cache) color = "#ff0000" smell_intensity = INTENSITY_SUBTLE + spook_factor = SPOOK_AMT_BLOOD_STREAK + /obj/effect/decal/cleanable/blood/squirt/Initialize(mapload, direction, list/blood_dna) . = ..() dir = direction diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm index e12e95f8b95c..3ea318e65ac7 100644 --- a/code/game/objects/structures/window.dm +++ b/code/game/objects/structures/window.dm @@ -128,6 +128,9 @@ return TRUE +/obj/structure/window/proc/knock_on() + playsound(src, knock_sound, 50, TRUE) + /obj/structure/window/proc/on_exit(datum/source, atom/movable/leaving, direction) SIGNAL_HANDLER @@ -151,7 +154,7 @@ user.changeNext_move(CLICK_CD_MELEE) user.visible_message(span_notice("Something knocks on [src].")) add_fingerprint(user) - playsound(src, knock_sound, 50, TRUE) + knock_on() return COMPONENT_CANCEL_ATTACK_CHAIN @@ -171,7 +174,7 @@ if(!user.combat_mode) user.visible_message(span_notice("[user] knocks on [src]."), \ span_notice("You knock on [src].")) - playsound(src, knock_sound, 50, TRUE) + knock_on() else user.visible_message(span_warning("[user] bashes [src]!"), \ span_warning("You bash [src]!")) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index fe24ab1c43a3..2322eac3cc33 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -388,7 +388,7 @@ GLOBAL_PROTECT(admin_verbs_hideable) log_admin("[key_name(usr)] admin ghosted.") message_admins("[key_name_admin(usr)] admin ghosted.") var/mob/body = mob - body.ghostize(1) + body.ghostize(TRUE, TRUE) init_verbs() if(body && !body.key) body.key = "@[key]" //Haaaaaaaack. But the people have spoken. If it breaks; blame adminbus diff --git a/code/modules/client/client_colour.dm b/code/modules/client/client_colour.dm index 5a5b86674dd7..83bac561832e 100644 --- a/code/modules/client/client_colour.dm +++ b/code/modules/client/client_colour.dm @@ -216,6 +216,18 @@ /datum/client_colour/rave priority = PRIORITY_LOW +/datum/client_colour/ghostmono + colour = list( + 0.3,0.3,0.3,0, + 0.3,0.3,0.3,0, + 0.3,0.3,0.3,0, + 0.0,0.0,0.0,1, + ) + priority = PRIORITY_ABSOLUTE + override = TRUE + fade_in = 20 + fade_out = 20 + #undef PRIORITY_ABSOLUTE #undef PRIORITY_HIGH #undef PRIORITY_NORMAL diff --git a/code/modules/client/preferences/monochrome_ghost.dm b/code/modules/client/preferences/monochrome_ghost.dm new file mode 100644 index 000000000000..726795c8d791 --- /dev/null +++ b/code/modules/client/preferences/monochrome_ghost.dm @@ -0,0 +1,16 @@ +/datum/preference/toggle/monochrome_ghost + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "monochrome_ghost" + savefile_identifier = PREFERENCE_PLAYER + + default_value = TRUE + +/datum/preference/toggle/monochrome_ghost/apply_to_client(client/client, value) + var/mob/dead/observer/M = client.mob + if(!istype(M)) + return + + if(value && !M.started_as_observer) + M.add_client_colour(/datum/client_colour/ghostmono) + else + M.remove_client_colour(/datum/client_colour/ghostmono) diff --git a/code/modules/credits_roll/episode_name.dm b/code/modules/credits_roll/episode_name.dm index 0cb66a349457..42a85dae13e0 100644 --- a/code/modules/credits_roll/episode_name.dm +++ b/code/modules/credits_roll/episode_name.dm @@ -52,6 +52,8 @@ "THE CURIOUS CASE OF [uppr_name]", "ONE HELL OF A PARTY", "FOR YOUR CONSIDERATION", "PRESS YOUR LUCK", "A STATION CALLED [uppr_name]", "CRIME AND PUNISHMENT", "MY DINNER WITH [uppr_name]", "UNFINISHED BUSINESS", "THE ONLY STATION THAT'S NOT ON FIRE (YET)", "SOMEONE'S GOTTA DO IT", "THE [uppr_name] MIX-UP", "PILOT", "PROLOGUE", "FINALE", "UNTITLED", "THE END")]") episode_names += new /datum/episode_name("[pick("SPACE", "SEXY", "DRAGON", "WARLOCK", "LAUNDRY", "GUN", "ADVERTISING", "DOG", "CARBON MONOXIDE", "NINJA", "WIZARD", "SOCRATIC", "JUVENILE DELIQUENCY", "POLITICALLY MOTIVATED", "RADTACULAR SICKNASTY", "CORPORATE", "MEGA")] [pick("QUEST", "FORCE", "ADVENTURE")]", weight=25) + draft_spooky_episodes() + switch(GLOB.start_state.score(GLOB.end_state)) if(-INFINITY to -2000) episode_names += new /datum/episode_name("[pick("THE CREW'S PUNISHMENT", "A PUBLIC RELATIONS NIGHTMARE", "[uppr_name]: A NATIONAL CONCERN", "WITH APOLOGIES TO THE CREW", "THE CREW BITES THE DUST", "THE CREW BLOWS IT", "THE CREW GIVES UP THE DREAM", "THE CREW IS DONE FOR", "THE CREW SHOULD NOT BE ALLOWED ON TV", "THE END OF [uppr_name] AS WE KNOW IT")]", "Extremely low score of [GLOB.start_state.score(GLOB.end_state)].", 250) @@ -301,7 +303,7 @@ if(human_escapees == 2) if(lawyercount == 2) - episode_names += new /datum/episode_name/rare("DOUBLE JEOPARDY", "The only two survivors were IAAs or lawyers.", 2500) + episode_names += new /datum/episode_name/rare("DOUBLE JEOPARDY", "The only two survivors were lawyers.", 2500) if(chefcount == 2) episode_names += new /datum/episode_name/rare("CHEF WARS", "The only two survivors were chefs.", 2500) if(minercount == 2) @@ -346,6 +348,31 @@ break */ +/datum/controller/subsystem/credits/proc/draft_spooky_episodes() + var/list/areas_spooked = BLACKBOX_FEEDBACK_NESTED_TALLY("ghost_power_used") + if(!length(areas_spooked)) + return + + var/uppr_name = uppertext(station_name()) + var/did_general_spooky + for(var/area_name in areas_spooked) + if(length(areas_spooked[area_name]) > 10) + did_general_spooky = TRUE + episode_names += new /datum/episode_name("THE HAUNTED [uppertext(area_name)]", "Large amounts of paranormal activity present.", 500) + + + if(did_general_spooky) + var/list/spooky_names = list( + "CARMEN MIRANDA'S GHOST IS HAUNTING [uppr_name]", + "DON'T CROSS THE STREAMS", + "BAD TO THE BONE", + "NIGHTMARE ON [uppr_name]", + ) + episode_names += new /datum/episode_name(pick(spooky_names), "Large amounts of paranormal activity present.", 250) + + if(findtext(uppr_name, "13")) + episode_names += new /datum/episode_name/rare("UNLUCKY NUMBERS", "The station's name contained \"13\".", 1000) + /proc/get_station_avg_temp() var/avg_temp = 0 var/avg_divide = 0 diff --git a/code/modules/flufftext/Dreaming.dm b/code/modules/flufftext/Dreaming.dm index c86dbe4707f5..1e401dc2560b 100644 --- a/code/modules/flufftext/Dreaming.dm +++ b/code/modules/flufftext/Dreaming.dm @@ -62,7 +62,7 @@ return var/next_message = dream_fragments[1] dream_fragments.Cut(1,2) - to_chat(src, span_notice("... [next_message] ...")) + to_chat(src, span_obviousnotice("... [next_message] ...")) if(LAZYLEN(dream_fragments)) addtimer(CALLBACK(src, PROC_REF(dream_sequence), dream_fragments), rand(10,30)) else diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index 8393a6f48dd8..f14ccca5d087 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -202,10 +202,9 @@ new_player_panel() return FALSE - var/mob/dead/observer/observer = new + var/mob/dead/observer/observer = new(null, TRUE) spawning = TRUE - observer.started_as_observer = TRUE close_spawn_windows() var/obj/effect/landmark/observer_start/O = locate(/obj/effect/landmark/observer_start) in GLOB.landmarks_list to_chat(src, span_notice("Now teleporting.")) diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index 4852cb00858a..305f76e44cac 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -48,7 +48,9 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER) var/datum/spawners_menu/spawners_menu var/datum/minigames_menu/minigames_menu -/mob/dead/observer/Initialize(mapload) +/mob/dead/observer/Initialize(mapload, started_as_observer = FALSE) + src.started_as_observer = started_as_observer + set_invisibility(GLOB.observer_default_invisibility) add_verb(src, list( @@ -115,6 +117,18 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER) SSpoints_of_interest.make_point_of_interest(src) ADD_TRAIT(src, TRAIT_HEAR_THROUGH_DARKNESS, ref(src)) + if(!started_as_observer) + var/static/list/powers = list( + /datum/action/cooldown/ghost_whisper = SPOOK_LEVEL_WEAK_POWERS, + /datum/action/cooldown/flicker = SPOOK_LEVEL_WEAK_POWERS, + /datum/action/cooldown/knock_sound = SPOOK_LEVEL_WEAK_POWERS, + /datum/action/cooldown/ghost_light = SPOOK_LEVEL_MEDIUM_POWERS, + /datum/action/cooldown/chilling_presence = SPOOK_LEVEL_MEDIUM_POWERS, + /datum/action/cooldown/shatter_light = SPOOK_LEVEL_DESTRUCTIVE_POWERS, + /datum/action/cooldown/shatter_glass = SPOOK_LEVEL_DESTRUCTIVE_POWERS, + ) + AddComponent(/datum/component/spooky_powers, powers) + /mob/dead/observer/get_photo_description(obj/item/camera/camera) if(!invisibility || camera.see_ghosts) return "You can also see a g-g-g-g-ghooooost!" @@ -180,7 +194,7 @@ Transfer_mind is there to check if mob is being deleted/not going to have a body Works together with spawning an observer, noted above. */ -/mob/proc/ghostize(can_reenter_corpse = TRUE) +/mob/proc/ghostize(can_reenter_corpse = TRUE, admin_ghost) if(!key) return if(key[1] == "@") // Skip aghosts. @@ -197,7 +211,7 @@ Works together with spawning an observer, noted above. ethereal_heart.stop_crystalization_process(crystal_fella) //stops the crystallization process stop_sound_channel(CHANNEL_HEARTBEAT) //Stop heartbeat sounds because You Are A Ghost Now - var/mob/dead/observer/ghost = new(src, src) // Transfer safety to observer spawning proc. + var/mob/dead/observer/ghost = new(src) // Transfer safety to observer spawning proc. SStgui.on_transfer(src, ghost) // Transfer NanoUIs. ghost.can_reenter_corpse = can_reenter_corpse ghost.key = key @@ -205,9 +219,12 @@ Works together with spawning an observer, noted above. if(!can_reenter_corpse)// Disassociates observer mind from the body mind ghost.mind = null + if(!admin_ghost) + ghost.add_client_colour(/datum/client_colour/ghostmono) + return ghost -/mob/living/ghostize(can_reenter_corpse = TRUE) +/mob/living/ghostize(can_reenter_corpse = TRUE, admin_ghost) . = ..() if(. && can_reenter_corpse) var/mob/dead/observer/ghost = . diff --git a/code/modules/mob/living/death.dm b/code/modules/mob/living/death.dm index ba82fda8f6c1..7767b27afd03 100644 --- a/code/modules/mob/living/death.dm +++ b/code/modules/mob/living/death.dm @@ -71,6 +71,7 @@ /mob/living/proc/death(gibbed) set_stat(DEAD) unset_machine() + timeofdeath = world.time tod = stationtime2text() var/turf/T = get_turf(src) @@ -79,8 +80,10 @@ if(SSlag_switch.measures[DISABLE_DEAD_KEYLOOP] && !client?.holder) to_chat(src, span_deadsay(span_big("Observer freelook is disabled.\nPlease use Orbit, Teleport, and Jump to look around."))) ghostize(TRUE) + set_disgust(0) SetSleeping(0, 0) + reset_perspective(null) reload_fullscreen() update_mob_action_buttons() @@ -88,6 +91,7 @@ update_health_hud() med_hud_set_health() med_hud_set_status() + release_all_grabs() set_ssd_indicator(FALSE) @@ -99,4 +103,6 @@ if (client) client.move_delay = initial(client.move_delay) + if(!gibbed) + AddComponent(/datum/component/spook_factor, SPOOK_AMT_CORPSE) return TRUE diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 7a62a6cb1d2a..b1554e462406 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -662,6 +662,9 @@ updatehealth() get_up(TRUE) + if(.) + qdel(GetComponent(/datum/component/spook_factor)) + // The signal is called after everything else so components can properly check the updated values SEND_SIGNAL(src, COMSIG_LIVING_REVIVE, full_heal, admin_revive) diff --git a/code/modules/power/lighting/light.dm b/code/modules/power/lighting/light.dm index 134bd4269237..a8fe4a62704b 100644 --- a/code/modules/power/lighting/light.dm +++ b/code/modules/power/lighting/light.dm @@ -440,6 +440,7 @@ DEFINE_INTERACTABLE(/obj/machinery/light) set waitfor = FALSE if(flickering) return + flickering = TRUE if(on && status == LIGHT_OK) for(var/i in 1 to amount) diff --git a/code/modules/spells/spell.dm b/code/modules/spells/spell.dm index a716084fd3fa..1b0c50e6b54e 100644 --- a/code/modules/spells/spell.dm +++ b/code/modules/spells/spell.dm @@ -210,16 +210,6 @@ return TRUE -/** - * Check if the target we're casting on is a valid target. - * For self-casted spells, the target being checked (cast_on) is the caster. - * For click_to_activate spells, the target being checked is the clicked atom. - * - * Return TRUE if cast_on is valid, FALSE otherwise - */ -/datum/action/cooldown/spell/proc/is_valid_target(atom/cast_on) - return TRUE - // The actual cast chain occurs here, in Activate(). // You should generally not be overriding or extending Activate() for spells. // Defer to any of the cast chain procs instead. diff --git a/code/modules/spooky/abilities/chilling_presence.dm b/code/modules/spooky/abilities/chilling_presence.dm new file mode 100644 index 000000000000..569535e937c2 --- /dev/null +++ b/code/modules/spooky/abilities/chilling_presence.dm @@ -0,0 +1,30 @@ +/datum/action/cooldown/chilling_presence + name = "Chilling Presence" + desc = "Send a chill up your target's spine." + + click_to_activate = TRUE + cooldown_time = 120 SECONDS + write_log = TRUE + ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' + +/datum/action/cooldown/chilling_presence/is_valid_target(atom/cast_on) + . = ..() + if(!isliving(cast_on)) + return FALSE + + var/mob/living/L = cast_on + if(L.stat == DEAD) + to_chat(owner, span_warning("That one has no life force left.")) + return FALSE + + if(L.stat == CONSCIOUS) + to_chat(owner, span_warning("I cannot influence those that are awake.")) + return FALSE + +/datum/action/cooldown/chilling_presence/Activate(atom/target) + . = ..() + var/mob/living/L = target + to_chat(target, span_obviousnotice("You feel a chill run up your spine.")) + L.emote("shiver") + + RECORD_GHOST_POWER(src) diff --git a/code/modules/spooky/abilities/flicker.dm b/code/modules/spooky/abilities/flicker.dm new file mode 100644 index 000000000000..56a96239d229 --- /dev/null +++ b/code/modules/spooky/abilities/flicker.dm @@ -0,0 +1,18 @@ +/datum/action/cooldown/flicker + name = "Flicker" + desc = "Use your electromagnetic influence to disrupt a nearby light fixture." + + click_to_activate = TRUE + cooldown_time = 120 SECONDS + write_log = TRUE + ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' + +/datum/action/cooldown/flicker/is_valid_target(atom/cast_on) + return istype(cast_on, /obj/machinery/light) + +/datum/action/cooldown/flicker/Activate(atom/target) + . = ..() + var/obj/machinery/light/L = target + L.flicker() + + RECORD_GHOST_POWER(src) diff --git a/code/modules/spooky/abilities/ghost_light.dm b/code/modules/spooky/abilities/ghost_light.dm new file mode 100644 index 000000000000..1503d188d72f --- /dev/null +++ b/code/modules/spooky/abilities/ghost_light.dm @@ -0,0 +1,28 @@ +/datum/action/cooldown/ghost_light + name = "Ghastly Light" + desc = "Emit feint light for a short period of time" + + click_to_activate = TRUE + cooldown_time = 5 MINUTES + write_log = TRUE + ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' + + var/timer + +/datum/action/cooldown/ghost_light/Activate(atom/target) + . = ..() + + timer = addtimer(CALLBACK(src, PROC_REF(disable_light)), 60 SECONDS, TIMER_STOPPABLE) + owner.set_light_color("#7bfc6a") + owner.set_light_on(TRUE) + + RECORD_GHOST_POWER(src) + +/datum/action/cooldown/ghost_light/Remove(mob/removed_from) + deltimer(timer) + disable_light() + return ..() + +/datum/action/cooldown/ghost_light/proc/disable_light() + owner.set_light_color(initial(owner.light_color)) + owner.set_light_on(FALSE) diff --git a/code/modules/spooky/abilities/knock_on_window_or_door.dm b/code/modules/spooky/abilities/knock_on_window_or_door.dm new file mode 100644 index 000000000000..dd948953d121 --- /dev/null +++ b/code/modules/spooky/abilities/knock_on_window_or_door.dm @@ -0,0 +1,24 @@ +/datum/action/cooldown/knock_sound + name = "Knock" + desc = "Create an audio phenomena centered on a door or window." + + click_to_activate = TRUE + cooldown_time = 120 SECONDS + write_log = TRUE + ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' + +/datum/action/cooldown/knock_sound/is_valid_target(atom/cast_on) + return istype(cast_on, /obj/structure/window) || istype(cast_on, /obj/machinery/door) + +/datum/action/cooldown/knock_sound/Activate(atom/target) + . = ..() + if(istype(target, /obj/machinery/door)) + var/obj/machinery/door/D = target + D.knock_on() + + else if(istype(target, /obj/structure/window)) + var/obj/structure/window/W = target + W.knock_on() + + RECORD_GHOST_POWER(src) + diff --git a/code/modules/spooky/abilities/shatter_glass.dm b/code/modules/spooky/abilities/shatter_glass.dm new file mode 100644 index 000000000000..b56c7da3024e --- /dev/null +++ b/code/modules/spooky/abilities/shatter_glass.dm @@ -0,0 +1,25 @@ +/datum/action/cooldown/shatter_glass + name = "Resonance" + desc = "Vibrate a nearby glass object enough to cause integrity failure." + + click_to_activate = TRUE + cooldown_time = 30 MINUTES + write_log = TRUE + ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' + +/datum/action/cooldown/shatter_glass/is_valid_target(atom/cast_on) + if(istype(target, /obj/structure/window)) + var/obj/structure/window/W = target + if(W.reinf) + to_chat(owner, span_warning("I cannot shatter that, it is too strong.")) + return FALSE + return TRUE + + return istype(target, /obj/structure/table/glass) + +/datum/action/cooldown/shatter_glass/Activate(atom/target) + . = ..() + var/obj/structure/S = target + S.deconstruct() + + RECORD_GHOST_POWER(src) diff --git a/code/modules/spooky/abilities/shatter_light.dm b/code/modules/spooky/abilities/shatter_light.dm new file mode 100644 index 000000000000..f1223a79a171 --- /dev/null +++ b/code/modules/spooky/abilities/shatter_light.dm @@ -0,0 +1,18 @@ +/datum/action/cooldown/shatter_light + name = "Snuff Light" + desc = "Overload a nearby source of light." + + click_to_activate = TRUE + cooldown_time = 20 MINUTES + write_log = TRUE + ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' + +/datum/action/cooldown/shatter_light/is_valid_target(atom/cast_on) + return istype(cast_on, /obj/machinery/light) + +/datum/action/cooldown/shatter_light/Activate(atom/target) + . = ..() + var/obj/machinery/light/L = target + L.break_light_tube() + + RECORD_GHOST_POWER(src) diff --git a/code/modules/spooky/abilities/sleep_whisper.dm b/code/modules/spooky/abilities/sleep_whisper.dm new file mode 100644 index 000000000000..bbbd6463a2b2 --- /dev/null +++ b/code/modules/spooky/abilities/sleep_whisper.dm @@ -0,0 +1,49 @@ +/datum/action/cooldown/ghost_whisper + name = "Dream Daemon" + desc = "Allows you to influence the dreams of a sleeping creature." + + click_to_activate = TRUE + cooldown_time = 60 SECONDS + write_log = TRUE + ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' + + var/message_to_send + +/datum/action/cooldown/ghost_whisper/is_valid_target(atom/cast_on) + . = ..() + if(!isliving(cast_on)) + return FALSE + + var/mob/living/L = cast_on + if(L.stat == DEAD) + to_chat(owner, span_warning("That one has no life force left.")) + return FALSE + + if(L.stat == CONSCIOUS) + to_chat(owner, span_warning("I cannot influence those that are awake.")) + return FALSE + + if(!L.client) + to_chat(owner, span_warning("There is nobody inside of there to listen.")) + return FALSE + +/datum/action/cooldown/ghost_whisper/PreActivate(atom/target, list/params) + message_to_send = "" + var/input = tgui_input_text(owner, "What do you want to say?", max_length = 20) + if(!input) + return FALSE + + var/confirmation = tgui_alert(owner, "\"... [input] ...\"", "Confirm Whisper", list("Ok", "Cancel")) + if(confirmation != "Ok") + return FALSE + + if(!IsAvailable()) + return FALSE + return ..() + +/datum/action/cooldown/ghost_whisper/Activate(atom/target) + . = ..() + to_chat(target, span_obviousnotice("... [message_to_send] ...")) + message_to_send = "" + + RECORD_GHOST_POWER(src) diff --git a/code/modules/spooky/components/spook_factor.dm b/code/modules/spooky/components/spook_factor.dm new file mode 100644 index 000000000000..a53c730c4c60 --- /dev/null +++ b/code/modules/spooky/components/spook_factor.dm @@ -0,0 +1,54 @@ +/datum/component/spook_factor + dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS + + var/spook_contribution = 0 + var/area/affecting_area + +/datum/component/spook_factor/Initialize(spook_contribution) + . = ..() + if(!ismovable(parent)) + return INITIALIZE_HINT_QDEL + + src.spook_contribution = spook_contribution + var/area/A = get_area(parent) + if(!A) + return + + affect_area(A) + +/datum/component/spook_factor/Destroy(force, silent) + affect_area(null) + return ..() + +/datum/component/spook_factor/RegisterWithParent() + var/atom/movable/movable_parent = parent + movable_parent.become_area_sensitive(REF(src)) + RegisterSignal(movable_parent, COMSIG_ENTER_AREA, PROC_REF(enter_area)) + RegisterSignal(movable_parent, COMSIG_EXIT_AREA, PROC_REF(exit_area)) + +/datum/component/spook_factor/UnregisterFromParent() + var/atom/movable/movable_parent = parent + UnregisterSignal(movable_parent, list(COMSIG_EXIT_AREA, COMSIG_ENTER_AREA)) + movable_parent.lose_area_sensitivity(REF(src)) + +/datum/component/spook_factor/InheritComponent(datum/component/C, i_am_original, spook_contribution) + var/area/A = affecting_area + affect_area(null) + src.spook_contribution = spook_contribution + affect_area(A) + +/datum/component/spook_factor/proc/affect_area(area/A) + if(affecting_area == A) + return + + affecting_area?.adjust_spook_level(-spook_contribution) + affecting_area = A + affecting_area?.adjust_spook_level(spook_contribution) + +/datum/component/spook_factor/proc/enter_area(atom/movable/source, area/A) + SIGNAL_HANDLER + affect_area(A) + +/datum/component/spook_factor/proc/exit_area(atom/movable/source, area/A) + SIGNAL_HANDLER + affect_area(null) diff --git a/code/modules/spooky/components/spooky_powers.dm b/code/modules/spooky/components/spooky_powers.dm new file mode 100644 index 000000000000..633829cb97ea --- /dev/null +++ b/code/modules/spooky/components/spooky_powers.dm @@ -0,0 +1,84 @@ +/// Grants powers to the parent mob based on the spookiness of their area. +/datum/component/spooky_powers + dupe_mode = COMPONENT_DUPE_UNIQUE + + var/list/powers = list() + var/area/current_area + +/datum/component/spooky_powers/Initialize(power_list) + . = ..() + if(!ismob(parent)) + return INITIALIZE_HINT_QDEL + + for(var/action_type in power_list) + var/datum/action/new_action = new action_type + powers[new_action] = power_list[action_type] + +/datum/component/spooky_powers/Destroy(force, silent) + powers = null + return ..() + +/datum/component/spooky_powers/RegisterWithParent() + . = ..() + var/atom/movable/movable_parent = parent + if(isobserver(movable_parent)) + RegisterSignal(movable_parent, COMSIG_MOVABLE_MOVED, PROC_REF(observer_moved)) + else + movable_parent.become_area_sensitive(REF(src)) + RegisterSignal(movable_parent, COMSIG_ENTER_AREA, PROC_REF(enter_area)) + + update_area(get_area(parent)) + +/datum/component/spooky_powers/UnregisterFromParent() + . = ..() + var/atom/movable/movable_parent = parent + if(isobserver(movable_parent)) + UnregisterSignal(movable_parent, COMSIG_MOVABLE_MOVED) + else + UnregisterSignal(movable_parent, list(COMSIG_ENTER_AREA)) + movable_parent.lose_area_sensitivity(REF(src)) + + update_area(null) + +/datum/component/spooky_powers/proc/update_powers() + if(QDELETED(parent)) + return + + var/spook = current_area?.spook_level || 0 + for(var/datum/action/power in powers) + if((powers[power] > spook) && (power.owner == parent)) + power.Remove(parent) + continue + + if((powers[power] <= spook) && (power.owner != parent)) + power.Grant(parent) + +/datum/component/spooky_powers/proc/update_area(area/A) + if(A == current_area) + return + + if(current_area) + UnregisterSignal(current_area, AREA_SPOOK_LEVEL_CHANGED) + + current_area = A + + if(current_area) + RegisterSignal(current_area, AREA_SPOOK_LEVEL_CHANGED, PROC_REF(area_spook_changed)) + + update_powers() + +/datum/component/spooky_powers/proc/area_spook_changed(area/source, old_spook_level) + SIGNAL_HANDLER + update_powers() + +/datum/component/spooky_powers/proc/enter_area(atom/movable/source, area/A) + SIGNAL_HANDLER + update_area(A) + +/datum/component/spooky_powers/proc/observer_moved(atom/movable/source, old_loc, movement_dir, forced, old_locs, momentum_change) + SIGNAL_HANDLER + var/area/A = get_area(source) + if(A == current_area) + return + + update_area(A) diff --git a/config/game_options.txt b/config/game_options.txt index 40048e7af12f..9ecac2b25a8a 100644 --- a/config/game_options.txt +++ b/config/game_options.txt @@ -192,11 +192,6 @@ MINIMAL_ACCESS_THRESHOLD 20 ## Comment this out this to make security officers spawn in departmental security posts #SEC_START_BRIG - -## GHOST INTERACTION ### -## Uncomment to let ghosts spin chairs. You may be wondering why this is a config option. Don't ask. -#GHOST_INTERACTION - ## NEAR-DEATH EXPERIENCE ### ## Comment this out to disable mobs hearing ghosts when unconscious and very close to death NEAR_DEATH_EXPERIENCE diff --git a/daedalus.dme b/daedalus.dme index 7dd11e1fb640..5bd9169dd60e 100644 --- a/daedalus.dme +++ b/daedalus.dme @@ -197,6 +197,7 @@ #include "code\__DEFINES\spatial_gridmap.dm" #include "code\__DEFINES\species_clothing_paths.dm" #include "code\__DEFINES\speech_controller.dm" +#include "code\__DEFINES\spooky_defines.dm" #include "code\__DEFINES\stamina.dm" #include "code\__DEFINES\stat.dm" #include "code\__DEFINES\stat_tracking.dm" @@ -2584,6 +2585,7 @@ #include "code\modules\client\preferences\jobless_role.dm" #include "code\modules\client\preferences\loadout_override.dm" #include "code\modules\client\preferences\mod_select.dm" +#include "code\modules\client\preferences\monochrome_ghost.dm" #include "code\modules\client\preferences\names.dm" #include "code\modules\client\preferences\occupation.dm" #include "code\modules\client\preferences\ooc.dm" @@ -4292,6 +4294,15 @@ #include "code\modules\spells\spell_types\touch\duffelbag_curse.dm" #include "code\modules\spells\spell_types\touch\flesh_to_stone.dm" #include "code\modules\spells\spell_types\touch\smite.dm" +#include "code\modules\spooky\abilities\chilling_presence.dm" +#include "code\modules\spooky\abilities\flicker.dm" +#include "code\modules\spooky\abilities\ghost_light.dm" +#include "code\modules\spooky\abilities\knock_on_window_or_door.dm" +#include "code\modules\spooky\abilities\shatter_glass.dm" +#include "code\modules\spooky\abilities\shatter_light.dm" +#include "code\modules\spooky\abilities\sleep_whisper.dm" +#include "code\modules\spooky\components\spook_factor.dm" +#include "code\modules\spooky\components\spooky_powers.dm" #include "code\modules\station_goals\bsa.dm" #include "code\modules\station_goals\dna_vault.dm" #include "code\modules\station_goals\shield.dm" diff --git a/modular_pariah/modules/indicators/code/ssd_indicator.dm b/modular_pariah/modules/indicators/code/ssd_indicator.dm index 0bc5ea784937..5141ac419419 100644 --- a/modular_pariah/modules/indicators/code/ssd_indicator.dm +++ b/modular_pariah/modules/indicators/code/ssd_indicator.dm @@ -25,14 +25,6 @@ GLOBAL_VAR_INIT(ssd_indicator_overlay, mutable_appearance('modular_pariah/module . = ..() //Temporary, look below for the reason -/mob/living/ghostize(can_reenter_corpse = TRUE) +/mob/living/ghostize(can_reenter_corpse = TRUE, admin_ghost) . = ..() set_ssd_indicator(FALSE) - -/* -//EDIT - TRANSFER CKEY IS NOT A THING ON THE TG CODEBASE, if things break too bad because of it, consider implementing it -//This proc should stop mobs from having the overlay when someone keeps jumping control of mobs, unfortunately it causes Aghosts to have their character without the SSD overlay, I wasn't able to find a better proc unfortunately -/mob/living/transfer_ckey(mob/new_mob, send_signal = TRUE) - ..() - set_ssd_indicator(FALSE) -*/ diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/monochrome_ghost.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/monochrome_ghost.tsx new file mode 100644 index 000000000000..581dc2ef4e89 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/monochrome_ghost.tsx @@ -0,0 +1,7 @@ +import { CheckboxInput, FeatureToggle } from "../base"; + +export const monochrome_ghost: FeatureToggle = { + name: "See Monochrome as Ghost", + category: "GHOST", + component: CheckboxInput, +};