diff --git a/code/datums/brain_damage/imaginary_friend.dm b/code/datums/brain_damage/imaginary_friend.dm index fc230632bdd..48fabd701f4 100644 --- a/code/datums/brain_damage/imaginary_friend.dm +++ b/code/datums/brain_damage/imaginary_friend.dm @@ -45,15 +45,26 @@ /datum/brain_trauma/special/imaginary_friend/proc/make_friend() friend = new(get_turf(owner), owner) +/// Tries an orbit poll for the imaginary friend /datum/brain_trauma/special/imaginary_friend/proc/get_ghost() - set waitfor = FALSE - var/list/mob/dead/observer/candidates = poll_candidates_for_mob("Do you want to play as [owner.real_name]'s imaginary friend?", ROLE_PAI, null, 7.5 SECONDS, friend, POLL_IGNORE_IMAGINARYFRIEND) - if(LAZYLEN(candidates)) - var/mob/dead/observer/C = pick(candidates) - friend.key = C.key - friend_initialized = TRUE - else + var/datum/callback/to_call = CALLBACK(src, PROC_REF(add_friend)) + owner.AddComponent(/datum/component/orbit_poll, \ + ignore_key = POLL_IGNORE_IMAGINARYFRIEND, \ + job_bans = ROLE_PAI, \ + title = "[owner.real_name]'s imaginary friend", \ + to_call = to_call, \ + ) + +/// Yay more friends! +/datum/brain_trauma/special/imaginary_friend/proc/add_friend(mob/dead/observer/ghost) + if(isnull(ghost)) qdel(src) + return + + friend.key = ghost.key + friend_initialized = TRUE + friend.log_message("became [key_name(owner)]'s split personality.", LOG_GAME) + message_admins("[ADMIN_LOOKUPFLW(friend)] became [ADMIN_LOOKUPFLW(owner)]'s split personality.") /mob/camera/imaginary_friend name = "imaginary friend" diff --git a/code/datums/brain_damage/split_personality.dm b/code/datums/brain_damage/split_personality.dm index 05b4343a01c..9176884b816 100644 --- a/code/datums/brain_damage/split_personality.dm +++ b/code/datums/brain_damage/split_personality.dm @@ -32,17 +32,26 @@ var/datum/action/cooldown/spell/personality_commune/owner_spell = new(src) owner_spell.Grant(owner_backseat) - +/// Attempts to get a ghost to play the personality /datum/brain_trauma/severe/split_personality/proc/get_ghost() - set waitfor = FALSE - var/list/mob/dead/observer/candidates = poll_candidates_for_mob("Do you want to play as [owner.real_name]'s [poll_role]?", ROLE_PAI, null, 7.5 SECONDS, stranger_backseat, POLL_IGNORE_SPLITPERSONALITY) - if(LAZYLEN(candidates)) - var/mob/dead/observer/C = pick(candidates) - stranger_backseat.key = C.key - stranger_backseat.log_message("became [key_name(owner)]'s split personality.", LOG_GAME) - message_admins("[ADMIN_LOOKUPFLW(stranger_backseat)] became [ADMIN_LOOKUPFLW(owner)]'s split personality.") - else + var/datum/callback/to_call = CALLBACK(src, PROC_REF(schism)) + owner.AddComponent(/datum/component/orbit_poll, \ + ignore_key = POLL_IGNORE_SPLITPERSONALITY, \ + job_bans = ROLE_PAI, \ + title = "[owner.real_name]'s [poll_role]", \ + to_call = to_call, \ + ) + +/// Ghost poll has concluded +/datum/brain_trauma/severe/split_personality/proc/schism(mob/dead/observer/ghost) + if(isnull(ghost)) qdel(src) + return + + stranger_backseat.key = ghost.key + stranger_backseat.log_message("became [key_name(owner)]'s split personality.", LOG_GAME) + message_admins("[ADMIN_LOOKUPFLW(stranger_backseat)] became [ADMIN_LOOKUPFLW(owner)]'s split personality.") + /datum/brain_trauma/severe/split_personality/on_life(seconds_per_tick, times_fired) if(owner.stat == DEAD) diff --git a/code/datums/components/orbit_poll.dm b/code/datums/components/orbit_poll.dm new file mode 100644 index 00000000000..d9563aaab95 --- /dev/null +++ b/code/datums/components/orbit_poll.dm @@ -0,0 +1,108 @@ +/** + * A replacement for the standard poll_ghost_candidate. + * Use this to subtly ask players to join - it picks from orbiters. + * Please use named arguments for this. + * + * @params ignore_key - Required so it doesn't spam + * @params job_bans - You can insert a list or single items here. + * @params cb - Invokes this proc and appends the poll winner as the last argument, mob/dead/observer/ghost + * @params title - Optional. Useful if the role name does not match the parent. + * + * @usage + * ``` + * var/datum/callback/cb = CALLBACK(src, PROC_REF(do_stuff), arg1, arg2) + * AddComponent(/datum/component/orbit_poll, \ + * ignore_key = POLL_IGNORE_EXAMPLE, \ + * job_bans = ROLE_EXAMPLE or list(ROLE_EXAMPLE, ROLE_EXAMPLE2), \ + * title = "Use this if you want something other than the parent name", \ + * to_call = cb, \ + * ) + */ +/datum/component/orbit_poll + /// Prevent players with this ban from being selected + var/list/job_bans = list() + /// Title of the role to announce after it's done + var/title + /// Proc to invoke whenever the poll is complete + var/datum/callback/to_call + +/datum/component/orbit_poll/Initialize( \ + ignore_key, \ + list/job_bans, \ + datum/callback/to_call, \ + title, \ + header = "Ghost Poll", \ + custom_message, \ + timeout = 20 SECONDS \ +) + . = ..() + if (!isatom(parent)) + return COMPONENT_INCOMPATIBLE + + var/atom/owner = parent + + src.job_bans |= job_bans + src.title = title || owner.name + src.to_call = to_call + + var/message = custom_message || "[capitalize(src.title)] is looking for volunteers" + + notify_ghosts("[message]. An orbiter will be chosen in [DisplayTimeText(timeout)].\n", \ + action = NOTIFY_ORBIT, \ + enter_link = "(Ignore)", \ + flashwindow = FALSE, \ + header = "Volunteers requested", \ + ignore_key = ignore_key, \ + source = parent \ + ) + + addtimer(CALLBACK(src, PROC_REF(end_poll)), timeout, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE|TIMER_DELETE_ME) + +/datum/component/orbit_poll/Topic(href, list/href_list) + if(!href_list["ignore"]) + return + + var/mob/user = usr + + var/ignore_key = href_list["ignore"] + if(tgui_alert(user, "Ignore further [title] alerts?", "Ignore Alert", list("Yes", "No"), 20 SECONDS, TRUE) != "Yes") + return + + GLOB.poll_ignore[ignore_key] |= user.ckey + +/// Concludes the poll, picking one of the orbiters +/datum/component/orbit_poll/proc/end_poll() + if(QDELETED(parent)) + return + + var/list/candidates = list() + var/atom/owner = parent + + var/datum/component/orbiter/orbiter_comp = owner.GetComponent(/datum/component/orbiter) + if(isnull(orbiter_comp)) + phone_home() + return + + for(var/mob/dead/observer/ghost as anything in orbiter_comp.orbiter_list) + if(QDELETED(ghost) || isnull(ghost.client)) + continue + if(is_banned_from(ghost.ckey, job_bans)) + continue + + candidates += ghost + + if(!length(candidates)) + phone_home() + return + + var/mob/dead/observer/chosen = pick(candidates) + + if(chosen) + deadchat_broadcast("[key_name(chosen, include_name = FALSE)] was selected for the role ([title]).", "Ghost Poll: ", parent) + + phone_home(chosen) + +/// Make sure to call your parents my dude +/datum/component/orbit_poll/proc/phone_home(mob/dead/observer/chosen) + to_call.Invoke(chosen) + qdel(src) diff --git a/code/datums/components/spirit_holding.dm b/code/datums/components/spirit_holding.dm index a4d0e029137..82c37e8f26b 100644 --- a/code/datums/components/spirit_holding.dm +++ b/code/datums/components/spirit_holding.dm @@ -57,21 +57,30 @@ attempting_awakening = TRUE to_chat(awakener, span_notice("You attempt to wake the spirit of [parent]...")) - var/mob/dead/observer/candidates = poll_ghost_candidates("Do you want to play as the spirit of [awakener.real_name]'s blade?", ROLE_PAI, FALSE, 100, POLL_IGNORE_POSSESSED_BLADE) - if(!LAZYLEN(candidates)) - to_chat(awakener, span_warning("[parent] is dormant. Maybe you can try again later.")) - attempting_awakening = FALSE - return + var/datum/callback/to_call = CALLBACK(src, PROC_REF(affix_spirit), awakener) + parent.AddComponent(/datum/component/orbit_poll, \ + ignore_key = POLL_IGNORE_POSSESSED_BLADE, \ + job_bans = ROLE_PAI, \ + to_call = to_call, \ + title = "Spirit of [awakener.real_name]'s blade", \ + ) //Immediately unregister to prevent making a new spirit UnregisterSignal(parent, COMSIG_ITEM_ATTACK_SELF) - var/mob/dead/observer/chosen_spirit = pick(candidates) +/// On conclusion of the ghost poll +/datum/component/spirit_holding/proc/affix_spirit(mob/awakener, mob/dead/observer/ghost) + if(isnull(ghost)) + to_chat(awakener, span_warning("[parent] is dormant. Maybe you can try again later.")) + attempting_awakening = FALSE + return + if(QDELETED(parent)) //if the thing that we're conjuring a spirit in has been destroyed, don't create a spirit - to_chat(chosen_spirit, span_userdanger("The new vessel for your spirit has been destroyed! You remain an unbound ghost.")) + to_chat(ghost, span_userdanger("The new vessel for your spirit has been destroyed! You remain an unbound ghost.")) return + bound_spirit = new(parent) - bound_spirit.ckey = chosen_spirit.ckey + bound_spirit.ckey = ghost.ckey bound_spirit.fully_replace_character_name(null, "The spirit of [parent]") bound_spirit.status_flags |= GODMODE bound_spirit.copy_languages(awakener, LANGUAGE_MASTER) //Make sure the sword can understand and communicate with the awakener. diff --git a/code/game/objects/items/devices/aicard_evil.dm b/code/game/objects/items/devices/aicard_evil.dm index 1a5fce6897a..f91150bb086 100644 --- a/code/game/objects/items/devices/aicard_evil.dm +++ b/code/game/objects/items/devices/aicard_evil.dm @@ -28,26 +28,29 @@ finding_candidate = FALSE return TRUE +/// Sets up the ghost poll /obj/item/aicard/syndie/loaded/proc/procure_ai(mob/user) var/datum/antagonist/nukeop/op_datum = user.mind?.has_antag_datum(/datum/antagonist/nukeop,TRUE) if(isnull(op_datum)) balloon_alert(user, "invalid access!") return - var/list/nuke_candidates = poll_ghost_candidates( - question = "Do you want to play as a nuclear operative MODsuit AI?", - jobban_type = ROLE_OPERATIVE, - be_special_flag = ROLE_OPERATIVE_MIDROUND, - poll_time = 15 SECONDS, - ignore_category = POLL_IGNORE_SYNDICATE, + + var/datum/callback/to_call = CALLBACK(src, PROC_REF(on_poll_concluded), user, op_datum) + AddComponent(/datum/component/orbit_poll, \ + ignore_key = POLL_IGNORE_SYNDICATE, \ + job_bans = ROLE_OPERATIVE, \ + to_call = to_call, \ + title = "Nuclear Operative Modsuit AI" \ ) - if(QDELETED(src)) - return - if(!LAZYLEN(nuke_candidates)) + +/// Poll has concluded with a ghost, create the AI +/obj/item/aicard/syndie/loaded/proc/on_poll_concluded(mob/user, datum/antagonist/nukeop/op_datum, mob/dead/observer/ghost) + if(isnull(ghost)) to_chat(user, span_warning("Unable to connect to S.E.L.F. dispatch. Please wait and try again later or use the intelliCard on your uplink to get your points refunded.")) return + // pick ghost, create AI and transfer - var/mob/dead/observer/ghos = pick(nuke_candidates) - var/mob/living/silicon/ai/weak_syndie/new_ai = new /mob/living/silicon/ai/weak_syndie(get_turf(src), new /datum/ai_laws/syndicate_override, ghos) + var/mob/living/silicon/ai/weak_syndie/new_ai = new /mob/living/silicon/ai/weak_syndie(get_turf(src), new /datum/ai_laws/syndicate_override, ghost) // create and apply syndie datum var/datum/antagonist/nukeop/nuke_datum = new() nuke_datum.send_to_spawnpoint = FALSE diff --git a/code/modules/antagonists/blob/powers.dm b/code/modules/antagonists/blob/powers.dm index 04054f6df85..b35308d092f 100644 --- a/code/modules/antagonists/blob/powers.dm +++ b/code/modules/antagonists/blob/powers.dm @@ -189,14 +189,22 @@ to_chat(src, span_notice("You attempt to produce a blobbernaut.")) pick_blobbernaut_candidate(factory) -/** Polls ghosts to get a blobbernaut candidate. */ +/// Polls ghosts to get a blobbernaut candidate. /mob/camera/blob/proc/pick_blobbernaut_candidate(obj/structure/blob/special/factory/factory) - if(!factory) + if(isnull(factory)) return - var/list/mob/dead/observer/candidates = poll_ghost_candidates("Do you want to play as a [blobstrain.name] blobbernaut?", ROLE_BLOB, ROLE_BLOB, 50) - - if(!length(candidates)) + var/datum/callback/to_call = CALLBACK(src, PROC_REF(on_poll_concluded), factory) + factory.AddComponent(/datum/component/orbit_poll, \ + ignore_key = POLL_IGNORE_BLOB, \ + job_bans = ROLE_BLOB, \ + to_call = to_call, \ + title = "Blobbernaut", \ + ) + +/// Called when the ghost poll concludes +/mob/camera/blob/proc/on_poll_concluded(obj/structure/blob/special/factory/factory, mob/dead/observer/ghost) + if(isnull(ghost)) to_chat(src, span_warning("You could not conjure a sentience for your blobbernaut. Your points have been refunded. Try again later.")) add_points(BLOBMOB_BLOBBERNAUT_RESOURCE_COST) factory.assign_blobbernaut(null) @@ -205,8 +213,7 @@ var/mob/living/basic/blob_minion/blobbernaut/minion/blobber = new(get_turf(factory)) assume_direct_control(blobber) factory.assign_blobbernaut(blobber) - var/mob/dead/observer/player = pick(candidates) - blobber.assign_key(player.key, blobstrain) + blobber.assign_key(ghost.key, blobstrain) RegisterSignal(blobber, COMSIG_HOSTILE_POST_ATTACKINGTARGET, PROC_REF(on_blobbernaut_attacked)) /// When one of our boys attacked something, we sometimes want to perform extra effects diff --git a/code/modules/antagonists/wizard/equipment/soulstone.dm b/code/modules/antagonists/wizard/equipment/soulstone.dm index f7be579ad1a..80dd5a1b2d9 100644 --- a/code/modules/antagonists/wizard/equipment/soulstone.dm +++ b/code/modules/antagonists/wizard/equipment/soulstone.dm @@ -323,7 +323,15 @@ return TRUE to_chat(user, "[span_userdanger("Capture failed!")]: The soul has already fled its mortal frame. You attempt to bring it back...") - INVOKE_ASYNC(src, PROC_REF(get_ghost_to_replace_shade), victim, user) + + var/datum/callback/to_call = CALLBACK(src, PROC_REF(on_poll_concluded), user, victim) + AddComponent(/datum/component/orbit_poll, \ + ignore_key = POLL_IGNORE_SHADE, \ + job_bans = ROLE_CULTIST, \ + to_call = to_call, \ + title = "A shade" \ + ) + return TRUE //it'll probably get someone ;) ///captures a shade that was previously released from a soulstone. @@ -435,33 +443,19 @@ shade_datum = shade.mind.add_antag_datum(/datum/antagonist/shade_minion) shade_datum.update_master(user.real_name) -/** - * Gets a ghost from dead chat to replace a missing player when a shade is created. - * - * Gets ran if a soulstone is used on a body that has no client to take over the shade. - * - * victim - the body that's being shaded - * user - the mob shading the body - * - * Returns FALSE if no ghosts are available or the replacement fails. - * Returns TRUE otherwise. - */ -/obj/item/soulstone/proc/get_ghost_to_replace_shade(mob/living/carbon/victim, mob/user) - var/mob/dead/observer/chosen_ghost - var/list/consenting_candidates = poll_ghost_candidates("Would you like to play as a Shade?", "Cultist", ROLE_CULTIST, 5 SECONDS, POLL_IGNORE_SHADE) - if(length(consenting_candidates)) - chosen_ghost = pick(consenting_candidates) - - if(!victim || user.incapacitated() || !user.is_holding(src) || !user.CanReach(victim, src)) +/// Called when a ghost is chosen to become a shade. +/obj/item/soulstone/proc/on_poll_concluded(mob/living/master, mob/living/victim, mob/dead/observer/ghost) + if(isnull(victim) || master.incapacitated() || !master.is_holding(src) || !master.CanReach(victim, src)) return FALSE - if(!chosen_ghost || !chosen_ghost.client) - to_chat(user, span_danger("There were no spirits willing to become a shade.")) + if(isnull(ghost?.client)) + to_chat(master, span_danger("There were no spirits willing to become a shade.")) return FALSE - if(contents.len) //If they used the soulstone on someone else in the meantime + if(length(contents)) //If they used the soulstone on someone else in the meantime return FALSE - to_chat(user, "[span_info("Capture successful!:")] A spirit has entered [src], \ + to_chat(master, "[span_info("Capture successful!:")] A spirit has entered [src], \ taking upon the identity of [victim].") - init_shade(victim, user, shade_controller = chosen_ghost) + init_shade(victim, master, shade_controller = ghost) + return TRUE /proc/make_new_construct_from_class(construct_class, theme, mob/target, mob/creator, cultoverride, loc_override) diff --git a/code/modules/mining/lavaland/megafauna_loot.dm b/code/modules/mining/lavaland/megafauna_loot.dm index bdcf089e606..554002fed54 100644 --- a/code/modules/mining/lavaland/megafauna_loot.dm +++ b/code/modules/mining/lavaland/megafauna_loot.dm @@ -425,19 +425,31 @@ using = TRUE balloon_alert(user, "you hold the scythe up...") ADD_TRAIT(src, TRAIT_NODROP, type) - var/list/mob/dead/observer/candidates = poll_ghost_candidates("Do you want to play as [user.real_name]'s soulscythe?", ROLE_PAI, FALSE, 100, POLL_IGNORE_POSSESSED_BLADE) - if(LAZYLEN(candidates)) - var/mob/dead/observer/picked_ghost = pick(candidates) - soul.ckey = picked_ghost.ckey - soul.copy_languages(user, LANGUAGE_MASTER) //Make sure the sword can understand and communicate with the user. - soul.faction = list("[REF(user)]") - balloon_alert(user, "the scythe glows up") - add_overlay("soulscythe_gem") - density = TRUE - if(!ismob(loc)) - reset_spin() - else - balloon_alert(user, "the scythe is dormant!") + + var/datum/callback/to_call = CALLBACK(src, PROC_REF(on_poll_concluded), user) + AddComponent(/datum/component/orbit_poll, \ + ignore_key = POLL_IGNORE_POSSESSED_BLADE, \ + job_bans = ROLE_PAI, \ + to_call = to_call, \ + ) + +/// Ghost poll has concluded and a candidate has been chosen. +/obj/item/soulscythe/proc/on_poll_concluded(mob/living/master, mob/dead/observer/ghost) + if(isnull(ghost)) + balloon_alert(master, "the scythe is dormant!") + REMOVE_TRAIT(src, TRAIT_NODROP, type) + using = FALSE + return + + soul.ckey = ghost.ckey + soul.copy_languages(master, LANGUAGE_MASTER) //Make sure the sword can understand and communicate with the master. + soul.faction = list("[REF(master)]") + balloon_alert(master, "the scythe glows") + add_overlay("soulscythe_gem") + density = TRUE + if(!ismob(loc)) + reset_spin() + REMOVE_TRAIT(src, TRAIT_NODROP, type) using = FALSE diff --git a/code/modules/mob/living/carbon/alien/special/alien_embryo.dm b/code/modules/mob/living/carbon/alien/special/alien_embryo.dm index 3f3809c89b2..19434d888a1 100644 --- a/code/modules/mob/living/carbon/alien/special/alien_embryo.dm +++ b/code/modules/mob/living/carbon/alien/special/alien_embryo.dm @@ -84,26 +84,33 @@ return attempt_grow() -///Attempt to burst an alien outside of the host, getting a ghost to play as the xeno. +/// Attempt to burst an alien outside of the host, getting a ghost to play as the xeno. /obj/item/organ/internal/body_egg/alien_embryo/proc/attempt_grow(gib_on_success = TRUE) - if(!owner || bursting) + if(QDELETED(owner) || bursting) return bursting = TRUE - var/list/candidates = poll_ghost_candidates("Do you want to play as an alien larva that will burst out of [owner.real_name]?", ROLE_ALIEN, ROLE_ALIEN, 100, POLL_IGNORE_ALIEN_LARVA) - - if(QDELETED(src) || QDELETED(owner)) + var/datum/callback/to_call = CALLBACK(src, PROC_REF(on_poll_concluded), gib_on_success) + owner.AddComponent(/datum/component/orbit_poll, \ + ignore_key = POLL_IGNORE_ALIEN_LARVA, \ + job_bans = ROLE_ALIEN, \ + to_call = to_call, \ + custom_message = "An alien is bursting out of [owner.real_name]", \ + title = "alien larva" \ + ) + +/// Poll has concluded with a suitor +/obj/item/organ/internal/body_egg/alien_embryo/proc/on_poll_concluded(gib_on_success, mob/dead/observer/ghost) + if(QDELETED(owner)) return - if(!candidates.len || !owner) + if(isnull(ghost)) bursting = FALSE stage = 5 // If no ghosts sign up for the Larva, let's regress our growth by one minute, we will try again! addtimer(CALLBACK(src, PROC_REF(advance_embryo_stage)), growth_time) return - var/mob/dead/observer/ghost = pick(candidates) - var/mutable_appearance/overlay = mutable_appearance('icons/mob/nonhuman-player/alien.dmi', "burst_lie") owner.add_overlay(overlay) diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm index b3945fe60c4..2bbefb64ec3 100644 --- a/code/modules/research/xenobiology/xenobiology.dm +++ b/code/modules/research/xenobiology/xenobiology.dm @@ -705,19 +705,28 @@ balloon_alert(user, "offering...") being_used = TRUE - var/list/candidates = poll_candidates_for_mob("Do you want to play as [dumb_mob.name]?", ROLE_SENTIENCE, ROLE_SENTIENCE, 5 SECONDS, dumb_mob, POLL_IGNORE_SENTIENCE_POTION) // see poll_ignore.dm - if(!LAZYLEN(candidates)) + var/datum/callback/to_call = CALLBACK(src, PROC_REF(on_poll_concluded), user, dumb_mob) + dumb_mob.AddComponent(/datum/component/orbit_poll, \ + ignore_key = POLL_IGNORE_SENTIENCE_POTION, \ + job_bans = ROLE_SENTIENCE, \ + to_call = to_call, \ + ) + +/// Assign the chosen ghost to the mob +/obj/item/slimepotion/slime/sentience/proc/on_poll_concluded(mob/user, mob/living/dumb_mob, mob/dead/observer/ghost) + if(isnull(ghost)) balloon_alert(user, "try again later!") being_used = FALSE - return ..() + return - var/mob/dead/observer/C = pick(candidates) - dumb_mob.key = C.key + dumb_mob.key = ghost.key dumb_mob.mind.enslave_mind_to_creator(user) SEND_SIGNAL(dumb_mob, COMSIG_SIMPLEMOB_SENTIENCEPOTION, user) + if(isanimal(dumb_mob)) var/mob/living/simple_animal/smart_animal = dumb_mob smart_animal.sentience_act() + dumb_mob.mind.add_antag_datum(/datum/antagonist/sentient_creature) balloon_alert(user, "success") after_success(user, dumb_mob) diff --git a/tgstation.dme b/tgstation.dme index 2034e030059..6dc3ffb2b2a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1155,6 +1155,7 @@ #include "code\datums\components\omen.dm" #include "code\datums\components\on_hit_effect.dm" #include "code\datums\components\onwear_mood.dm" +#include "code\datums\components\orbit_poll.dm" #include "code\datums\components\orbiter.dm" #include "code\datums\components\overlay_lighting.dm" #include "code\datums\components\palette.dm"