[Like 99% Modular] Soulcatchers (#20576)

* wew

* Update soulcatcher.dm

* Update soulcatcher.dm

* Update soulcatcher.dm

* Update soulcatcher.dm

* Update soulcatcher.dm

* file moving

* tgui

* tgui_work

* wew

* more tweaks

* more stuff

* fancy

* nifsoft

* mob

* whoopsie

* man

* mind transfer

* ooc and flavor text

* Update soulcatcher_component.dm

* Update soulcatcher_mob.dm

* persistence

* hud

* a little bit of admin logging, as a treat

* Update soulcatcher_component.dm

* Update devices.dmi

* research

* some things I missed.

* whooops

* Update soulcatcher_component.dm

* Update soulcatcher_items.dm

* Update soulcatcher_component.dm

* lore

* Update soulcatcher_component.dm

* Update soulcatcher.dm

* Apply suggestions from code review

Co-authored-by: GoldenAlpharex <58045821+GoldenAlpharex@users.noreply.github.com>

* Update soulcatcher_items.dm

* Apply suggestions from code review

Co-authored-by: GoldenAlpharex <58045821+GoldenAlpharex@users.noreply.github.com>
Co-authored-by: nikothedude <59709059+nikothedude@users.noreply.github.com>

* Update soulcatcher_component.dm

* Update tgui/packages/tgui/interfaces/Soulcatcher.js

Co-authored-by: nikothedude <59709059+nikothedude@users.noreply.github.com>

* >w>

* moves the body component

* Update tgstation.dme

* renaming

* file move

* oh wow, I didn't need to make this non modular

* signals

* Update handheld_soulcatcher.dm

* more touches

* Update soulcatcher_mob.dm

* code suggestions

* Update handheld_soulcatcher.dm

* Update handheld_soulcatcher.dm

* Apply suggestions from code review

Co-authored-by: nikothedude <59709059+nikothedude@users.noreply.github.com>

* Update soulcatcher_component.dm

* a

* more signals :3

* This is a surprise tool that will be added back later.

* better UX

* Update soulcatcher.dm

* QoL

* Spamming reduction.

* Update nifs.dm

* Update handheld_soulcatcher.dm

* Update soulcatcher_component.dm

* Update soulcatcher_component.dm

* Update modular_skyrat/modules/modular_implants/code/soulcatcher/soulcatcher_component.dm

Co-authored-by: GoldenAlpharex <58045821+GoldenAlpharex@users.noreply.github.com>

* Update soulcatcher_component.dm

* Update soulcatcher.dm

* no more empty bodies

* 1984

* whoopsie

* transfer fix

* Update mobs.dm

* more logging

* Update soulcatcher_component.dm

* QoL

---------

Co-authored-by: GoldenAlpharex <58045821+GoldenAlpharex@users.noreply.github.com>
Co-authored-by: nikothedude <59709059+nikothedude@users.noreply.github.com>
This commit is contained in:
Pinta
2023-06-01 17:28:29 -04:00
committed by GitHub
parent 31e9815677
commit fa5136cad8
22 changed files with 1317 additions and 3 deletions

View File

@@ -82,3 +82,9 @@
/// Engineering Override Access manual toggle
#define COMSIG_GLOB_FORCE_ENG_OVERRIDE "force_engineering_override"
/// Whenever we need to check if a mob is currently inside of soulcatcher.
#define COMSIG_SOULCATCHER_CHECK_SOUL "soulcatcher_check_soul"
/// Whenever we need to get the soul of the mob inside of the soulcatcher.
#define COMSIG_SOULCATCHER_SCAN_BODY "soulcatcher_scan_body"

View File

@@ -606,6 +606,10 @@ GLOBAL_LIST_EMPTY(species_list)
moblist += mob_to_sort
for(var/mob/living/basic/mob_to_sort in sortmob)
moblist += mob_to_sort
// SKYRAT EDIT START - SOULCATCHERS
for(var/mob/living/soulcatcher_soul/mob_to_sort in sortmob)
moblist += mob_to_sort
// SKYRAT EDIT END - SOULCATCHERS
return moblist
///returns a mob type controlled by a specified ckey

View File

@@ -266,6 +266,7 @@
/obj/item/surgicaldrill,
/obj/item/tank/internals/emergency_oxygen,
/obj/item/weaponcell/medical, //SKYRAT EDIT MEDIGUNS
/obj/item/handheld_soulcatcher, // SKYRAT EDIT SOULCATCHERS
/obj/item/wrench/medical,
))

View File

@@ -2,7 +2,7 @@
/* EMOTE DATUMS */
/datum/emote/living
mob_type_allowed_typecache = /mob/living
mob_type_blacklist_typecache = list(/mob/living/brain)
mob_type_blacklist_typecache = list(/mob/living/brain) //SKYRAT EDIT - OVERWIRTTEN BY `modular_skyrat\modules\modular_implants\code\soulcatcher\soulcatcher_mob.dm`
/datum/emote/living/blush
key = "blush"

View File

@@ -969,6 +969,7 @@
prereq_ids = list("biotech", "datatheory")
design_ids = list(
"skill_station",
"soulcatcher_device", //SKYRAT EDIT SOULCATCHERS
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)

View File

@@ -48,3 +48,9 @@
name = "medical labcoat"
desc = "A suit that protects against minor chemical spills. Has a blue stripe on the shoulder."
icon_state = "labcoat_gen"
/obj/item/clothing/suit/toggle/labcoat/Initialize(mapload)
. = ..()
allowed += list(
/obj/item/handheld_soulcatcher,
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 482 B

After

Width:  |  Height:  |  Size: 543 B

View File

@@ -18,3 +18,13 @@
category = list(RND_CATEGORY_EQUIPMENT)
departmental_flags = DEPARTMENT_BITFLAG_CARGO
/datum/design/soulcatcher_device
name = "Evoker-Type RSD"
desc = "An RSD instrument that lets the user pull the consciousness from a body and store it virtually."
id = "soulcatcher_device"
build_type = PROTOLATHE | AWAY_LATHE
build_path = /obj/item/handheld_soulcatcher
materials = list(/datum/material/iron = 6000, /datum/material/silver = 2000, /datum/material/bluespace = 2000)
category = list(RND_CATEGORY_EQUIPMENT)
departmental_flags = DEPARTMENT_BITFLAG_MEDICAL

View File

@@ -503,6 +503,7 @@
new /obj/item/disk/nifsoft_uploader/summoner(src)
new /obj/item/disk/nifsoft_uploader/money_sense(src)
new /obj/item/disk/nifsoft_uploader/dorms(src)
new /obj/item/disk/nifsoft_uploader/soulcatcher(src)
#undef NIF_CALIBRATION_STAGE_1
#undef NIF_CALIBRATION_STAGE_1_END

View File

@@ -4,6 +4,7 @@ GLOBAL_LIST_INIT(purchasable_nifsofts, list(
/datum/nifsoft/shapeshifter,
/datum/nifsoft/summoner/dorms,
/datum/nifsoft/soul_poem,
/datum/nifsoft/soulcatcher,
))
/datum/computer_file/program/nifsoft_downloader

View File

@@ -1,5 +1,5 @@
/obj/item/disk/nifsoft_uploader/soul_poem
name = "Automatic Appraisal"
name = "Soul Poem"
loaded_nifsoft = /datum/nifsoft/soul_poem
//Modular Persistence variables for the soul_poem NIFSoft

View File

@@ -0,0 +1,110 @@
/obj/item/disk/nifsoft_uploader/soulcatcher
name = "Soulcatcher"
loaded_nifsoft = /datum/nifsoft/soulcatcher
/datum/nifsoft/soulcatcher
name = "Soulcatcher"
program_desc = "The 'Soulcatcher' coreware is a near-complete upgrade of the nanomachine systems in a NIF, meant for one purpose; supposedly, channeling the dead. This upgrade, in truth, functions as a Resonance Simulation Device; an RSD for short, an instrument capable of hosting someone's consciousness, context or otherwise. 'Resonance', a term for the specific pattern of neural activity that gives way to someone's consciousness, was discovered in the early 2500s by researchers Yun-Seo Jin and Kamakshi Padmanabhan, coining what is now called 'Jin-Padmanabhan Resonance,' or 'JP/Soul Resonance.' This 'Resonance' gives off a sophont's consciousness, their sense of continuation, and their 'I am me.' This Resonance can vary in structure and 'strength' from person to person, and even change over someone's life. When the brain of a sophont undergoes death and stops neural activity, then Resonance dissipates entirely and lingering consciousness becomes essentially an echo, rapidly fading over time.\n\nThe earliest RSDs were massive machines, drawing incredible power and utilizing bleeding-edge, clunky software to 'play' someone's Resonance at 1:1 accuracy with their original brain. However, complications arose that are still being studied. Resonance is replicable and can be re-created artificially; however, like trying to duplicate genetic code, the capture needs to be extremely accurate, and rapidly put into place. Instruments such as RSDs are capable of picking up on lingering consciousness after the end of Resonance, and resuming it through artificial neural activity can give it strength to continue once more. RSDs such as Soulcatchers can only work at such a distance, otherwise running the risk of the Resonance essentially corrupting due to poor signal.\n\nIt is currently impossible to run Resonance in two places at once, because the same Resonance over two places experiences interference; like noise canceling headphones. Slimes and other gestalt consciousnesses can modulate their harmonics to a degree, bearing a partial disconnect and bringing themselves into constructive interference with similar harmonic signatures. A deepscan of the person's brain is necessary to give their consciousness 'context;' running their Resonance and capturing their consciousness alone results in a person with their same original intelligence, but zero memories or identity. These scans rapidly become outdated due to the growth of the brain, and it is prohibitively complex to store them in their entirety.\n\nThe first portable RSD, or Soulcatcher, was developed by the Spider Clan. These were initially designed for the captive interrogation of a person's consciousness without having to worry about the struggling of their body, and for dead or aging members of the mysterious group of orbital shinobi to be able to guide field operatives. These Soulcatchers are the main instrument to play Resonance, but recent advances in medical science have been leading to more. Occasionally, it is known for unusual sources of 'wild' Resonance, called Phantoms, to end up inside of the nearest Soulcatcher, a key finding its own lock; with a wide array of theories as to how these come into existence. Much as how some people intentionally become stable Engrams to achieve digital immortality, such as the witches of the Altspace Coven, it is possible for others to forcibly enter a Soulcatcher and act as a sort of Phantom by hacking their way in."
purchase_price = 150 //RP tool
persistence = TRUE
ui_icon = "ghost"
/// What is the linked soulcatcher datum used by this NIFSoft?
var/datum/weakref/linked_soulcatcher
/// What action to bring up the soulcatcher is linked with this NIFSoft?
var/datum/action/innate/soulcatcher/soulcatcher_action
/// a list containing saved soulcatcher rooms
var/list/saved_soulcatcher_rooms = list()
/datum/nifsoft/soulcatcher/New()
. = ..()
soulcatcher_action = new
soulcatcher_action.Grant(linked_mob)
soulcatcher_action.parent_nifsoft = WEAKREF(src)
var/obj/item/organ/internal/cyberimp/brain/nif/target_nif = parent_nif
var/datum/component/soulcatcher/new_soulcatcher = target_nif.AddComponent(/datum/component/soulcatcher/nifsoft)
for(var/room in saved_soulcatcher_rooms)
new_soulcatcher.create_room(room, saved_soulcatcher_rooms[room])
if(length(new_soulcatcher.soulcatcher_rooms) > 1) //We don't need the default room anymore.
new_soulcatcher.soulcatcher_rooms -= new_soulcatcher.soulcatcher_rooms[1]
new_soulcatcher.name = "[linked_mob]'s soulcatcher"
RegisterSignal(new_soulcatcher, COMSIG_PARENT_QDELETING, .proc/no_soulcatcher_component)
linked_soulcatcher = WEAKREF(new_soulcatcher)
/datum/nifsoft/soulcatcher/activate()
. = ..()
if(!linked_soulcatcher)
return FALSE
var/datum/component/soulcatcher/current_soulcatcher = linked_soulcatcher.resolve()
if(!current_soulcatcher)
return FALSE
current_soulcatcher.ui_interact(linked_mob)
return TRUE
/// If the linked soulcatcher is being deleted we want to set the current linked soulcatcher to `FALSE`
/datum/nifsoft/soulcatcher/proc/no_soulcatcher_component()
SIGNAL_HANDLER
linked_soulcatcher = null
/datum/nifsoft/soulcatcher/Destroy()
if(soulcatcher_action)
soulcatcher_action.Remove()
qdel(soulcatcher_action)
if(linked_soulcatcher)
var/datum/component/soulcatcher/current_soulcatcher = linked_soulcatcher.resolve()
if(current_soulcatcher)
qdel(current_soulcatcher)
return ..()
/datum/nifsoft/soulcatcher/load_persistence_data()
. = ..()
var/datum/modular_persistence/persistence = .
if(!persistence)
return FALSE
saved_soulcatcher_rooms = params2list(persistence.nif_soulcatcher_rooms)
return TRUE
/datum/nifsoft/soulcatcher/save_persistence_data(datum/modular_persistence/persistence)
. = ..()
if(!.)
return FALSE
var/list/room_list = list()
var/datum/component/soulcatcher/current_soulcatcher = linked_soulcatcher.resolve()
for(var/datum/soulcatcher_room/room in current_soulcatcher.soulcatcher_rooms)
room_list[room.name] = room.room_description
persistence.nif_soulcatcher_rooms = list2params(room_list)
return TRUE
/datum/modular_persistence
///A param string containing soulcatcher rooms
var/nif_soulcatcher_rooms = ""
/datum/action/innate/soulcatcher
name = "Soulcatcher"
background_icon = 'modular_skyrat/master_files/icons/mob/actions/action_backgrounds.dmi'
background_icon_state = "android"
button_icon = 'modular_skyrat/master_files/icons/mob/actions/actions_nif.dmi'
button_icon_state = "soulcatcher"
/// The weakref of the parent NIFSoft we belong to.
var/datum/weakref/parent_nifsoft
/datum/action/innate/soulcatcher/Activate()
. = ..()
var/datum/nifsoft/soulcatcher/soulcatcher_nifsoft = parent_nifsoft.resolve()
if(!soulcatcher_nifsoft)
return FALSE
soulcatcher_nifsoft.activate()

View File

@@ -0,0 +1,108 @@
#define RSD_ATTEMPT_COOLDOWN 2 MINUTES
/obj/item/handheld_soulcatcher
name = "\improper Evoker-type RSD"
desc = "The Evoker-Type Resonance Simulation Device is a sort of 'Soulcatcher' instrument that's been designated for handheld usage. These RSDs were designed with the Medical field in mind, a tool meant to offer comfort to the temporarily-departed while their bodies are being repaired, healed, or produced. The Evoker is essentially a very specialized handheld NIF, still using the same nanomachinery for the software and hardware. This careful instrument is able to host a virtual space for a great number of Engrams for an essentially indefinite amount of time in an unlimited variety of simulations, even able to transfer them to and from a NIF. However, it's best Medical practice to not lollygag."
icon = 'modular_skyrat/modules/modular_implants/icons/obj/devices.dmi'
icon_state = "soulcatcher-device"
inhand_icon_state = "electronic"
worn_icon_state = "electronic"
lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi'
slot_flags = ITEM_SLOT_BELT
/// What soulcatcher datum is associated with this item?
var/datum/component/soulcatcher/linked_soulcatcher
/// The cooldown for the RSD on scanning a body if the ghost refuses. This is here to prevent spamming.
COOLDOWN_DECLARE(rsd_scan_cooldown)
/obj/item/handheld_soulcatcher/attack_self(mob/user, modifiers)
linked_soulcatcher.ui_interact(user)
/obj/item/handheld_soulcatcher/New(loc, ...)
. = ..()
linked_soulcatcher = AddComponent(/datum/component/soulcatcher)
linked_soulcatcher.name = "[src] soulcatcher"
/obj/item/handheld_soulcatcher/Destroy(force)
if(linked_soulcatcher)
qdel(linked_soulcatcher)
return ..()
/obj/item/handheld_soulcatcher/attack(mob/living/target_mob, mob/living/user, params)
if(!target_mob)
return ..()
if(target_mob.GetComponent(/datum/component/previous_body))
linked_soulcatcher.scan_body(target_mob, user)
return TRUE
if(!target_mob.mind)
to_chat(user, span_warning("You are unable to remove a mind from an empty body."))
return FALSE
if(!COOLDOWN_FINISHED(src, rsd_scan_cooldown))
var/time_left = round((COOLDOWN_TIMELEFT(src, rsd_scan_cooldown)) / (1 MINUTES), 0.01)
to_chat(user, span_warning("You are currently unable to grab the soul of [target_mob], please wait [time_left] minutes before trying again."))
return FALSE
if(target_mob.stat == DEAD) //We can temporarily store souls of dead mobs.
target_mob.ghostize(TRUE) //Incase they are staying in the body.
var/mob/dead/observer/target_ghost = target_mob.get_ghost(TRUE, TRUE)
if(!target_ghost)
to_chat(user, span_warning("You are unable to get the soul of [target_mob]!"))
return FALSE
var/datum/soulcatcher_room/target_room = tgui_input_list(user, "Choose a room to send [target_mob]'s soul to.", name, linked_soulcatcher.soulcatcher_rooms, timeout = 30 SECONDS)
if(!target_room)
return FALSE
SEND_SOUND(target_ghost, 'sound/misc/notice2.ogg')
window_flash(target_ghost.client)
if(tgui_alert(target_ghost, "[user] wants to transfer you to [target_room] inside of a soulcatcher, do you accept?", name, list("Yes", "No"), 30 SECONDS, autofocus = FALSE) != "Yes")
to_chat(user, span_warning("[target_mob] doesn't seem to want to enter."))
COOLDOWN_START(src, rsd_scan_cooldown, RSD_ATTEMPT_COOLDOWN)
return FALSE
if(!target_room.add_soul_from_ghost(target_ghost))
return FALSE
if(!target_mob.GetComponent(/datum/component/previous_body))
return FALSE
var/turf/source_turf = get_turf(user)
log_admin("[key_name(user)] used [src] to put [key_name(target_mob)]'s mind into a soulcatcher at [AREACOORD(source_turf)]")
linked_soulcatcher.scan_body(target_mob, user)
return TRUE
var/datum/soulcatcher_room/target_room = tgui_input_list(user, "Choose a room to send [target_mob]'s soul to.", name, linked_soulcatcher.soulcatcher_rooms, timeout = 30 SECONDS)
if(!target_room)
return FALSE
SEND_SOUND(target_mob, 'sound/misc/notice2.ogg')
window_flash(target_mob.client)
if((tgui_alert(target_mob, "Do you wish to enter [target_room]? This will remove you from your body until you leave.", name, list("Yes", "No"), 30 SECONDS, FALSE) != "Yes") || (tgui_alert(target_mob, "Are you sure about this?", name, list("Yes", "No"), 30 SECONDS, FALSE) != "Yes"))
COOLDOWN_START(src, rsd_scan_cooldown, RSD_ATTEMPT_COOLDOWN)
to_chat(user, span_warning("[target_mob] doesn't seem to want to enter."))
return FALSE
if(!target_mob.mind)
return FALSE
target_room.add_soul(target_mob.mind, TRUE)
playsound(src, 'modular_skyrat/modules/modular_implants/sounds/default_good.ogg', 50, FALSE, ignore_walls = FALSE)
visible_message(span_notice("[src] beeps: [target_mob]'s mind transfer is now complete."))
if(!target_mob.GetComponent(/datum/component/previous_body))
return FALSE
linked_soulcatcher.scan_body(target_mob, user)
var/turf/source_turf = get_turf(user)
log_admin("[key_name(user)] used [src] to put [key_name(target_mob)]'s mind into a soulcatcher while they were still alive at [AREACOORD(source_turf)]")
return TRUE
#undef RSD_ATTEMPT_COOLDOWN

View File

@@ -0,0 +1,63 @@
/// A component that is given to a body when the soul inside is inhabiting a soulcatcher. this is mostly here so that the bodies of souls can be revived.
/datum/component/previous_body
/// What soulcatcher soul do we need to return to the body?
var/datum/weakref/soulcatcher_soul
/// Do we want to try and restore the mind when this is destroyed?
var/restore_mind = TRUE
/datum/component/previous_body/Initialize(...)
. = ..()
if(!ismob(parent))
return COMPONENT_INCOMPATIBLE
RegisterSignal(parent, COMSIG_SOULCATCHER_CHECK_SOUL, .proc/signal_destroy)
RegisterSignal(parent, COMSIG_SOULCATCHER_SCAN_BODY, .proc/scan_body)
/// Destroys the source component through a signal. `mind_restored` controls whether or not the mind will be grabbed upon deletion.
/datum/component/previous_body/proc/signal_destroy(mob/source_mob, mind_restored = TRUE)
SIGNAL_HANDLER
if(!mind_restored)
restore_mind = FALSE
qdel(src)
return TRUE
/// Attempts to scan the soul referenced in the `soulcatcher_soul` variable. Returns TRUE if the soul has been scanned, otherwise returns FALSE
/datum/component/previous_body/proc/scan_body(mob/source_mob)
SIGNAL_HANDLER
if(!soulcatcher_soul)
return FALSE
var/mob/living/soulcatcher_soul/target_soul = soulcatcher_soul.resolve()
if(!target_soul || !target_soul.body_scan_needed)
return FALSE
to_chat(target_soul, span_cyan("Your body has scanned, revealing your true identity."))
target_soul.name = source_mob.real_name
target_soul.body_scan_needed = FALSE
var/datum/preferences/preferences = target_soul.client?.prefs
if(preferences)
target_soul.soul_desc = preferences.read_preference(/datum/preference/text/flavor_text)
return TRUE
/// Attempts to destroy the component. If `restore_mind` is true, it will attempt to place the mind back inside of the body and delete the soulcatcher soul.
/datum/component/previous_body/Destroy(force, silent)
UnregisterSignal(parent, COMSIG_SOULCATCHER_CHECK_SOUL)
UnregisterSignal(parent, COMSIG_SOULCATCHER_SCAN_BODY)
if(restore_mind)
var/mob/living/original_body = parent
var/mob/living/soulcatcher_soul/soul = soulcatcher_soul.resolve()
if(original_body && soul && !original_body.mind)
var/datum/mind/mind_to_tranfer = soul.mind
if(mind_to_tranfer)
mind_to_tranfer.transfer_to(original_body)
soul.previous_body = FALSE
qdel(soul)
return ..()

View File

@@ -0,0 +1,350 @@
///Global list containing any and all soulcatchers
GLOBAL_LIST_EMPTY(soulcatchers)
#define SOULCATCHER_DEFAULT_COLOR "#75D5E1"
#define SOULCATCHER_WARNING_MESSAGE "You have entered a soulcatcher, do not share any information you have received while a ghost. If you have died within the round, you do not know your identity until your body has been scanned, standard blackout policy also applies."
/**
* Soulcatcher Component
*
* This component functions as a bridge between the `soulcatcher_room` attached to itself and the parented datum.
* It handles the creation of new soulcatcher rooms, TGUI, and relaying messages to the parent datum.
* If the component is deleted, any soulcatcher rooms inside of `soulcatcher_rooms` will be deleted.
*/
/datum/component/soulcatcher
/// What is the name of the soulcatcher?
var/name = "soulcatcher"
/// What rooms are linked to this soulcatcher
var/list/soulcatcher_rooms = list()
/// Are ghosts currently able to join this soulcatcher?
var/ghost_joinable = TRUE
/// Do we want to ask the user permission before the ghost joins?
var/require_approval = TRUE
/datum/component/soulcatcher/New()
. = ..()
if(!parent)
return COMPONENT_INCOMPATIBLE
create_room()
GLOB.soulcatchers += src
/datum/component/soulcatcher/Destroy(force, ...)
GLOB.soulcatchers -= src
for(var/datum/soulcatcher_room as anything in soulcatcher_rooms)
soulcatcher_rooms -= soulcatcher_room
qdel(soulcatcher_room)
return ..()
/**
* Creates a `/datum/soulcatcher_room` and adds it to the `soulcatcher_rooms` list.
*
* Arguments
* * target_name - The name that we want to assign to the created room.
* * target_desc - The description that we want to assign to the created room.
*/
/datum/component/soulcatcher/proc/create_room(target_name = "default room", target_desc = "it's a room")
var/datum/soulcatcher_room/created_room = new(src)
created_room.name = target_name
created_room.room_description = target_desc
soulcatcher_rooms += created_room
created_room.master_soulcatcher = WEAKREF(src)
/// Tries to find out who is currently using the soulcatcher, returns the holder. If no holder can be found, returns FALSE
/datum/component/soulcatcher/proc/get_current_holder()
var/mob/living/holder
if(istype(parent, /obj/item/organ/internal/cyberimp/brain/nif))
var/obj/item/organ/internal/cyberimp/brain/nif/target_nif = parent
holder = target_nif.linked_mob
else if(istype(parent, /obj/item))
var/obj/item/parent_item = parent
holder = parent_item.loc
if(!istype(holder))
return FALSE
return holder
/// Recieves a message from a soulcatcher room.
/datum/component/soulcatcher/proc/recieve_message(message_to_recieve)
if(!message_to_recieve)
return FALSE
var/mob/living/soulcatcher_owner = get_current_holder()
if(!soulcatcher_owner)
return FALSE
to_chat(soulcatcher_owner, message_to_recieve)
return TRUE
/// Attempts to ping the current user of the soulcatcher, asking them if `joiner_name` is allowed in. If they are, the proc returns `TRUE`, otherwise returns FALSE
/datum/component/soulcatcher/proc/get_approval(joiner_name)
if(!require_approval)
return TRUE
var/mob/living/soulcatcher_owner = get_current_holder()
if(!soulcatcher_owner)
return FALSE
if(tgui_alert(soulcatcher_owner, "Do you wish to allow [joiner_name] into your soulcatcher?", name, list("Yes", "No"), autofocus = FALSE) != "Yes")
return FALSE
return TRUE
/// Attempts to scan the body for the `previous_body component`, returns FALSE if the body is unable to be scanned, otherwise returns TRUE
/datum/component/soulcatcher/proc/scan_body(mob/living/parent_body, mob/living/user)
if(!parent_body || !user)
return FALSE
var/signal_result = SEND_SIGNAL(parent_body, COMSIG_SOULCATCHER_SCAN_BODY, parent_body)
if(!signal_result)
to_chat(user, span_warning("[parent_body] has already been scanned!"))
return FALSE
if(istype(parent, /obj/item/handheld_soulcatcher))
var/obj/item/handheld_soulcatcher/parent_device = parent
playsound(parent_device, 'modular_skyrat/modules/modular_implants/sounds/default_good.ogg', 50, FALSE, ignore_walls = FALSE)
parent_device.visible_message(span_notice("[parent_device] beeps: [parent_body] is now scanned."))
return TRUE
/**
* Soulcatcher Room
*
* This datum is where souls are sent to when joining soulcatchers.
* It handles sending messages to souls from the outside along with adding new souls, transfering, and removing souls.
*
*/
/datum/soulcatcher_room
/// What is the name of the room?
var/name = "Default Room"
/// What is the description of the room?
var/room_description = "An orange platform suspended in space orbited by reflective cubes of various sizes. There really isn't much here at the moment."
/// What souls are currently inside of the room?
var/list/current_souls = list()
/// Weakref for the master soulcatcher datum
var/datum/weakref/master_soulcatcher
/// What is the name of the person sending the messages?
var/outside_voice = "Host"
/// Can the room be joined at all?
var/joinable = TRUE
/// What is the color of chat messages sent by the room?
var/room_color = SOULCATCHER_DEFAULT_COLOR
/// Attemps to add a ghost to the soulcatcher room.
/datum/soulcatcher_room/proc/add_soul_from_ghost(mob/dead/observer/ghost)
if(!ghost || !ghost.ckey)
return FALSE
if(!ghost.mind)
ghost.mind = new /datum/mind(ghost.key)
ghost.mind.name = ghost.name
ghost.mind.active = TRUE
if(!add_soul(ghost.mind))
return FALSE
return TRUE
/// Converts a mind into a soul and adds the resulting soul to the room.
/datum/soulcatcher_room/proc/add_soul(datum/mind/mind_to_add)
if(!mind_to_add)
return FALSE
var/datum/component/soulcatcher/parent_soulcatcher = master_soulcatcher.resolve()
var/datum/parent_object = parent_soulcatcher.parent
if(!parent_object)
return FALSE
var/mob/living/soulcatcher_soul/new_soul = new(parent_object)
new_soul.name = mind_to_add.name
if(mind_to_add.current)
var/datum/component/previous_body/body_component = mind_to_add.current.AddComponent(/datum/component/previous_body)
body_component.soulcatcher_soul = WEAKREF(new_soul)
new_soul.round_participant = TRUE
new_soul.body_scan_needed = TRUE
new_soul.previous_body = WEAKREF(mind_to_add.current)
new_soul.name = pick(GLOB.last_names) //Until the body is discovered, the soul is a new person.
new_soul.soul_desc = "[new_soul] lacks a discernible form."
mind_to_add.transfer_to(new_soul, TRUE)
current_souls += new_soul
new_soul.current_room = WEAKREF(src)
var/datum/preferences/preferences = new_soul.client?.prefs
if(preferences)
new_soul.ooc_notes = preferences.read_preference(/datum/preference/text/ooc_notes)
if(!new_soul.body_scan_needed)
new_soul.soul_desc = preferences.read_preference(/datum/preference/text/flavor_text)
to_chat(new_soul, span_cyan("You find yourself now inside of: [name]"))
to_chat(new_soul, span_notice(room_description))
to_chat(new_soul, span_doyourjobidiot("You have entered a soulcatcher, do not share any information you have received while a ghost. If you have died within the round, you do not know your identity until your body has been scanned, standard blackout policy also applies."))
to_chat(new_soul, span_notice("While inside of a soulcatcher, you are able to speak and emote by using the normal hotkeys and verbs, unless disabled by the owner."))
to_chat(new_soul, span_notice("You may use the leave soulcatcher verb to leave the soulcatcher and return to your body at any time."))
var/atom/parent_atom = parent_object
if(istype(parent_atom))
var/turf/soulcatcher_turf = get_turf(parent_soulcatcher.parent)
var/message_to_log = "[key_name(new_soul)] joined [src] inside of [parent_atom] at [loc_name(soulcatcher_turf)]"
parent_atom.log_message(message_to_log, LOG_GAME)
new_soul.log_message(message_to_log, LOG_GAME)
return TRUE
/// Removes a soul from a soulcatcher room, leaving it as a ghost. Returns `FALSE` if the `soul_to_remove` cannot be found, otherwise returns `TRUE` after a successful deletion.
/datum/soulcatcher_room/proc/remove_soul(mob/living/soulcatcher_soul/soul_to_remove)
if(!soul_to_remove || !(soul_to_remove in current_souls))
return FALSE
current_souls -= soul_to_remove
soul_to_remove.current_room = null
qdel(soul_to_remove)
return TRUE
/// Transfers a soul from a soulcatcher room to another soulcatcher room. Returns `FALSE` if the target room or target soul cannot be found.
/datum/soulcatcher_room/proc/transfer_soul(mob/living/soulcatcher_soul/target_soul, datum/soulcatcher_room/target_room)
if(!(target_soul in current_souls) || !target_room)
return FALSE
var/datum/component/soulcatcher/target_master_soulcatcher = target_room.master_soulcatcher.resolve()
if(target_master_soulcatcher != master_soulcatcher.resolve())
target_soul.forceMove(target_master_soulcatcher.parent)
target_soul.current_room = WEAKREF(target_room)
current_souls -= target_soul
target_room.current_souls += target_soul
to_chat(target_soul, span_cyan("you've been transfered to [target_room]!"))
to_chat(target_soul, span_notice(target_room.room_description))
return TRUE
/**
* Sends a message or emote to all of the souls currently located inside of the soulcatcher room. Returns `FALSE` if a message cannot be sent, otherwise returns `TRUE`.
*
* Arguments
* * message_to_send - The message we want to send to the occupants of the room
* * message_sender - The person that is sending the message. This is not required.
* * emote - Is the message sent an emote or not?
*/
/datum/soulcatcher_room/proc/send_message(message_to_send, message_sender, emote = FALSE)
if(!message_to_send) //Why say nothing?
return FALSE
var/sender_name = ""
if(message_sender)
sender_name = "[message_sender] "
var/datum/asset/spritesheet/sheet = get_asset_datum(/datum/asset/spritesheet/chat)
var/tag = sheet.icon_tag("nif-soulcatcher")
var/soulcatcher_icon = ""
if(tag)
soulcatcher_icon = tag
var/first_room_name_word = splittext(name, " ")
var/message = ""
var/owner_message = ""
if(!emote)
message = "<font color=[room_color]>\ [soulcatcher_icon] <b>[sender_name]</b>says, \"[message_to_send]\"</font>"
owner_message = "<font color=[room_color]>\ <b>([first_room_name_word[1]])</b> [soulcatcher_icon] <b>[sender_name]</b>says, \"[message_to_send]\"</font>"
log_say("[sender_name] in [name] soulcatcher room said: [message_to_send]")
else
message = "<font color=[room_color]>\ [soulcatcher_icon] <b>[sender_name]</b>[message_to_send]</font>"
owner_message = "<font color=[room_color]>\ <b>([first_room_name_word[1]])</b> [soulcatcher_icon] <b>[sender_name]</b>[message_to_send]</font>"
log_emote("[sender_name] in [name] soulcatcher room emoted: [message_to_send]")
for(var/mob/living/soulcatcher_soul/soul as anything in current_souls)
if((emote && !soul.internal_sight) || (!emote && !soul.internal_hearing))
continue
to_chat(soul, message)
relay_message_to_soulcatcher(owner_message)
return TRUE
/// Relays a message sent from the send_message proc to the parent soulcatcher datum
/datum/soulcatcher_room/proc/relay_message_to_soulcatcher(message)
if(!message)
return FALSE
var/datum/component/soulcatcher/recepient_soulcatcher = master_soulcatcher.resolve()
recepient_soulcatcher.recieve_message(message)
return TRUE
/datum/soulcatcher_room/Destroy(force, ...)
for(var/mob/living/soulcatcher_soul/soul as anything in current_souls)
remove_soul(soul)
return ..()
/mob/dead/observer/verb/join_soulcatcher()
set name = "Enter Soulcatcher"
set category = "Ghost"
var/list/joinable_soulcatchers = list()
for(var/datum/component/soulcatcher/soulcatcher in GLOB.soulcatchers)
if(!soulcatcher.ghost_joinable)
continue
joinable_soulcatchers += soulcatcher
if(!length(joinable_soulcatchers))
to_chat(src, span_warning("No soulcatchers are joinable."))
return FALSE
var/datum/component/soulcatcher/soulcatcher_to_join = tgui_input_list(src, "Choose a soulcatcher to join", "Enter a soulcatcher", joinable_soulcatchers)
if(!soulcatcher_to_join || !(soulcatcher_to_join in joinable_soulcatchers))
return FALSE
var/list/rooms_to_join = list()
for(var/datum/soulcatcher_room/room in soulcatcher_to_join.soulcatcher_rooms)
if(!room.joinable)
continue
rooms_to_join += room
var/datum/soulcatcher_room/room_to_join
if(length(rooms_to_join) < 1)
return FALSE
if(length(rooms_to_join) == 1)
room_to_join = rooms_to_join[1]
else
room_to_join = tgui_input_list(src, "Choose a room to enter", "Enter a room", rooms_to_join)
if(!room_to_join)
to_chat(src, span_warning("There no rooms that you can join."))
return FALSE
if(soulcatcher_to_join.require_approval)
var/ghost_name = name
if(mind?.current)
ghost_name = "unknown"
if(!soulcatcher_to_join.get_approval(ghost_name))
to_chat(src, span_warning("The owner of [soulcatcher_to_join.name] declined your request to join."))
return FALSE
room_to_join.add_soul_from_ghost(src)
return TRUE
/mob/grab_ghost(force)
SEND_SIGNAL(src, COMSIG_SOULCATCHER_CHECK_SOUL)
return ..()
/mob/get_ghost(even_if_they_cant_reenter, ghosts_with_clients)
if(GetComponent(/datum/component/previous_body)) //Is the soul currently within a soulcatcher?
return TRUE
return ..()

View File

@@ -0,0 +1,157 @@
/mob/living/soulcatcher_soul
/// What does our soul look like?
var/soul_desc = "It's a soul."
/// What are the ooc notes for the soul?
var/ooc_notes = ""
/// Assuming we died inside of the round? What is our previous body?
var/datum/weakref/previous_body
/// What is the weakref of the soulcatcher room are we currently in?
var/datum/weakref/current_room
/// Is the soul able to see things in the outside world?
var/outside_sight = TRUE
/// Is the soul able to hear things from the outside world?
var/outside_hearing = TRUE
/// Is the soul able to "see" things from inside of the soulcatcher?
var/internal_sight = TRUE
/// Is the soul able to "hear" things from inside of the soulcatcher?
var/internal_hearing = TRUE
/// Is the soul able to emote inside the soulcatcher room?
var/able_to_emote = TRUE
/// Is the soul able to speak inside the soulcatcher room?
var/able_to_speak = TRUE
/// Is the soul able to leave the soulcatcher?
var/able_to_leave = TRUE
/// Did the soul live within the round? This is checked if we want to transfer the soul to another body.
var/round_participant = FALSE
/// Does the body need scanned?
var/body_scan_needed = FALSE
/mob/living/soulcatcher_soul/Initialize(mapload)
. = ..()
if(!outside_sight)
become_blind(NO_EYES)
if(!outside_hearing)
ADD_TRAIT(src, TRAIT_DEAF, INNATE_TRAIT)
/// Toggles whether or not the soul inside the soulcatcher can see the outside world. Returns the state of the `outside_sight` variable.
/mob/living/soulcatcher_soul/proc/toggle_sight()
outside_sight = !outside_sight
if(outside_sight)
cure_blind(NO_EYES)
else
become_blind(NO_EYES)
return outside_sight
/// Toggles whether or not the soul inside the soulcatcher can see the outside world. Returns the state of the `outside_hearing` variable.
/mob/living/soulcatcher_soul/proc/toggle_hearing()
outside_hearing = !outside_hearing
if(outside_hearing)
REMOVE_TRAIT(src, TRAIT_DEAF, INNATE_TRAIT)
else
ADD_TRAIT(src, TRAIT_DEAF, INNATE_TRAIT)
return outside_hearing
/// Attemp to leave the soulcatcher.
/mob/living/soulcatcher_soul/verb/leave_soulcatcher()
set name = "Leave Soulcatcher"
set category = "IC"
if(!able_to_leave)
to_chat(src, span_warning("You are unable to leave the soulcatcher."))
return FALSE
if(tgui_alert(src, "Are you sure you wish to leave the soulcatcher? IF you had a body, this will return you to your body", "Soulcatcher", list("Yes", "No")) != "Yes")
return FALSE
if(tgui_alert(src, "Are you really sure about this?", "Soulcatcher", list("Yes", "No")) != "Yes")
return FALSE
qdel(src)
/mob/living/soulcatcher_soul/ghost()
. = ..()
qdel(src)
/mob/living/soulcatcher_soul/say(message, bubble_type, list/spans, sanitize, datum/language/language, ignore_spam, forced, filterproof, message_range, datum/saymode/saymode)
message = trim(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN))
if(!message || message == "")
return
if(!able_to_speak)
to_chat(src, span_warning("You are unable to speak!"))
return FALSE
var/datum/soulcatcher_room/room = current_room.resolve()
if(!room)
return FALSE
room.send_message(message, src, FALSE)
return TRUE
/mob/living/soulcatcher_soul/me_verb(message as text)
message = trim(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN))
if(!message)
return FALSE
if(!able_to_emote)
to_chat(src, span_warning("You are unable to emote!"))
return FALSE
var/datum/soulcatcher_room/room = current_room.resolve()
if(!room)
return FALSE
room.send_message(message, src, TRUE)
return TRUE
/mob/living/soulcatcher_soul/subtle()
set hidden = TRUE
return FALSE
/mob/living/soulcatcher_soul/subtler()
set hidden = TRUE
return FALSE
/mob/living/soulcatcher_soul/whisper_verb()
set hidden = TRUE
return FALSE
/mob/living/soulcatcher_soul/container_emote()
set hidden = TRUE
return FALSE
/mob/living/soulcatcher_soul/resist()
set hidden = TRUE
return FALSE
/mob/living/soulcatcher_soul/Destroy()
log_message("[key_name(src)] has exited a soulcatcher.", LOG_GAME)
if(current_room)
var/datum/soulcatcher_room/room = current_room.resolve()
if(room)
room.current_souls -= src
current_room = null
if(previous_body && mind)
var/mob/target_body = previous_body.resolve()
if(!target_body)
return FALSE
mind.transfer_to(target_body)
SEND_SIGNAL(target_body, COMSIG_SOULCATCHER_CHECK_SOUL, FALSE)
if(target_body.stat != DEAD)
target_body.grab_ghost(TRUE)
return ..()
/datum/emote/living
mob_type_blacklist_typecache = list(/mob/living/brain, /mob/living/soulcatcher_soul)

View File

@@ -0,0 +1,198 @@
/datum/component/soulcatcher/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(usr, src, ui)
if(!ui)
ui = new(usr, src, "Soulcatcher", name)
ui.open()
/datum/component/soulcatcher/nifsoft/ui_state(mob/user)
return GLOB.conscious_state
/datum/component/soulcatcher/ui_data(mob/user)
var/list/data = list()
data["ghost_joinable"] = ghost_joinable
data["require_approval"] = require_approval
data["current_rooms"] = list()
for(var/datum/soulcatcher_room/room in soulcatcher_rooms)
var/list/room_data = list(
"name" = html_decode(room.name),
"description" = html_decode(room.room_description),
"reference" = REF(room),
"joinable" = room.joinable,
"color" = room.room_color,
)
for(var/mob/living/soulcatcher_soul/soul in room.current_souls)
var/list/soul_list = list(
"name" = soul.name,
"description" = soul.soul_desc,
"reference" = REF(soul),
"internal_hearing" = soul.internal_hearing,
"internal_sight" = soul.internal_sight,
"outside_hearing" = soul.outside_hearing,
"outside_sight" = soul.outside_sight,
"able_to_emote" = soul.able_to_emote,
"able_to_speak" = soul.able_to_speak,
"ooc_notes" = soul.ooc_notes
)
room_data["souls"] += list(soul_list)
data["current_rooms"] += list(room_data)
return data
/datum/component/soulcatcher/ui_static_data(mob/user)
var/list/data = list()
data["current_vessel"] = parent
return data
/datum/component/soulcatcher/ui_act(action, list/params)
. = ..()
if(.)
return
var/datum/soulcatcher_room/target_room
if(params["room_ref"])
target_room = locate(params["room_ref"]) in soulcatcher_rooms
if(!target_room)
return FALSE
var/mob/living/soulcatcher_soul/target_soul
if(params["target_soul"])
target_soul = locate(params["target_soul"]) in target_room.current_souls
if(!target_soul)
return FALSE
switch(action)
if("delete_room")
if(length(soulcatcher_rooms) <= 1)
return FALSE
soulcatcher_rooms -= target_room
qdel(target_room)
return TRUE
if("create_room")
create_room()
return TRUE
if("rename_room")
var/new_room_name = tgui_input_text(usr,"Choose a new name for the room", name, target_room.name)
if(!new_room_name)
return FALSE
target_room.name = new_room_name
return TRUE
if("redescribe_room")
var/new_room_desc = tgui_input_text(usr,"Choose a new description for the room", name, target_room.room_description, multiline = TRUE)
if(!new_room_desc)
return FALSE
target_room.room_description = new_room_desc
return TRUE
if("toggle_joinable_room")
target_room.joinable = !target_room.joinable
return TRUE
if("toggle_joinable")
ghost_joinable = !ghost_joinable
return TRUE
if("toggle_approval")
require_approval = !require_approval
return TRUE
if("modify_name")
var/new_name = tgui_input_text(usr,"Choose a new name to send messages as", name, target_room.room_description, multiline = TRUE)
if(!new_name)
return FALSE
target_room.outside_voice = new_name
return TRUE
if("remove_soul")
target_room.remove_soul(target_soul)
return TRUE
if("transfer_soul")
var/list/available_rooms = soulcatcher_rooms.Copy()
available_rooms -= target_room
if(ishuman(usr))
var/mob/living/carbon/human/human_user = usr
var/datum/nifsoft/soulcatcher/soulcatcher_nifsoft = human_user.find_nifsoft(/datum/nifsoft/soulcatcher)
if(soulcatcher_nifsoft && (parent != soulcatcher_nifsoft.parent_nif))
var/datum/component/soulcatcher/nifsoft_soulcatcher = soulcatcher_nifsoft.linked_soulcatcher.resolve()
if(istype(nifsoft_soulcatcher))
available_rooms.Add(nifsoft_soulcatcher.soulcatcher_rooms)
for(var/obj/item/held_item in human_user.held_items)
if(parent == held_item)
continue
var/datum/component/soulcatcher/soulcatcher_component = held_item.GetComponent(/datum/component/soulcatcher)
if(!soulcatcher_component)
continue
for(var/datum/soulcatcher_room/room in soulcatcher_component.soulcatcher_rooms)
available_rooms += room
var/datum/soulcatcher_room/transfer_room = tgui_input_list(usr, "Choose a room to transfer to", name, available_rooms)
if(!(transfer_room in available_rooms))
return FALSE
target_room.transfer_soul(target_soul, transfer_room)
return TRUE
if("change_room_color")
var/new_room_color = input(usr, "", "Choose Color", SOULCATCHER_DEFAULT_COLOR) as color
if(!new_room_color)
return FALSE
target_room.room_color = new_room_color
if("toggle_soul_outside_sense")
if(params["sense_to_change"] == "hearing")
target_soul.toggle_hearing()
else
target_soul.toggle_sight()
return TRUE
if("toggle_soul_sense")
if(params["sense_to_change"] == "hearing")
target_soul.internal_hearing = !target_soul.internal_hearing
else
target_soul.internal_sight = !target_soul.internal_sight
return TRUE
if("toggle_soul_communication")
if(params["communication_type"] == "emote")
target_soul.able_to_emote = !target_soul.able_to_emote
else
target_soul.able_to_speak = !target_soul.able_to_speak
return TRUE
if("send_message")
var/message_to_send = ""
var/emote = params["emote"]
var/message_sender = target_room.outside_voice
if(params["narration"])
message_sender = FALSE
message_to_send = tgui_input_text(usr, "Input the message you want to send", name, multiline = TRUE)
if(!message_to_send)
return FALSE
target_room.send_message(message_to_send, message_sender, emote)
return TRUE

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 B

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 987 B

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -6583,6 +6583,12 @@
#include "modular_skyrat\modules\modular_implants\code\nifsofts\prop_summoner.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\shapeshifter.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\soul_poem.dm"
#include "modular_skyrat\modules\modular_implants\code\nifsofts\soulcatcher.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\handheld_soulcatcher.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\soulcatcher_body_component.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\soulcatcher_component.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\soulcatcher_mob.dm"
#include "modular_skyrat\modules\modular_implants\code\soulcatcher\soulcatcher_tgui.dm"
#include "modular_skyrat\modules\modular_items\code\bags.dm"
#include "modular_skyrat\modules\modular_items\code\cash.dm"
#include "modular_skyrat\modules\modular_items\code\ciggies.dm"

View File

@@ -107,7 +107,9 @@ export const NifPanel = (props, context) => {
</TableRow>
</Table>
<br />
<BlockQuote>{nifsoft.desc}</BlockQuote>
<BlockQuote preserveWhitespace>
{nifsoft.desc}
</BlockQuote>
<box>
<br />
<Button.Confirm

View File

@@ -0,0 +1,290 @@
import { useBackend } from '../backend';
import { Window } from '../layouts';
import { BlockQuote, Button, Divider, Section, Box, Flex, Collapsible, LabeledList } from '../components';
export const Soulcatcher = (props, context) => {
const { act, data } = useBackend(context);
const { require_approval, current_rooms = [], ghost_joinable } = data;
return (
<Window width={520} height={400} resizable>
<Window.Content scrollable>
{current_rooms.map((room) => (
<Section
key={room.key}
title={<span style={{ color: room.color }}>{room.name}</span>}
buttons={
<>
<Button
icon="palette"
tooltip="Change the color of the room"
onClick={() =>
act('change_room_color', { room_ref: room.reference })
}>
Recolor
</Button>
<Button
icon="pen"
tooltip="Change the name of the room"
onClick={() =>
act('rename_room', { room_ref: room.reference })
}>
Rename
</Button>
<Button
icon="trash"
tooltip="Delete the room"
color="red"
onClick={() =>
act('delete_room', { room_ref: room.reference })
}>
Delete
</Button>
</>
}>
<BlockQuote preserveWhitespace> {room.description}</BlockQuote>
<Box>
<Button
icon="scroll"
tooltip="Performs an emote, without sending a name."
onClick={() =>
act('send_message', {
room_ref: room.reference,
emote: true,
narration: true,
})
}>
Narrate
</Button>
<Button
icon="comment"
tooltip="Speak inside of the room."
onClick={() =>
act('send_message', {
room_ref: room.reference,
emote: false,
})
}>
Say
</Button>
<Button
icon="face-smile"
tooltip="Do an emote inside of the room."
onClick={() =>
act('send_message', {
room_ref: room.reference,
emote: true,
})
}>
Emote
</Button>
<Button
icon="user-gear"
tooltip="Edits the name that is sent when emoting and saying."
onClick={() =>
act('modify_name', {
room_ref: room.reference,
})
}>
Edit Name
</Button>
<Button
icon="book"
tooltip="Changes the description of the room"
onClick={() =>
act('redescribe_room', { room_ref: room.reference })
}>
Redecorate
</Button>
<Button
color={room.joinable ? 'green' : 'red'}
icon={room.joinable ? 'door-open' : 'door-closed'}
onClick={() =>
act('toggle_joinable_room', { room_ref: room.reference })
}>
{room.joinable ? 'Room joinable' : 'Room unjoinable'}
</Button>
</Box>
{room.souls ? (
<>
<br />
<Box textAlign="center" fontSize="15px" opacity={0.8}>
<b>Current Souls</b>
</Box>
<Divider />
<Flex direction="column">
{room.souls.map((soul) => (
<Flex.Item key={soul.key}>
<Collapsible
title={soul.name}
buttons={
<Button
icon="paper-plane"
tooltip="Transfer a soul to another room"
onClick={() =>
act('transfer_soul', {
room_ref: room.reference,
target_soul: soul.reference,
})
}
/>
}>
<Box textAlign="center" fontSize="13px" opacity={0.8}>
<b>Flavor Text</b>
</Box>
<Divider />
<BlockQuote preserveWhitespace>
{soul.description}
</BlockQuote>
<br />
<Box textAlign="center" fontSize="13px" opacity={0.8}>
<b>OOC Notes</b>
</Box>
<Divider />
<BlockQuote preserveWhitespace>
{soul.ooc_notes}
</BlockQuote>
<br />
<LabeledList>
<LabeledList.Item label="Outside Hearing">
<Button
color={soul.outside_hearing ? 'green' : 'red'}
fluid
tooltip="Is the soul able to hear the outside world?"
onClick={() =>
act('toggle_soul_outside_sense', {
target_soul: soul.reference,
sense_to_change: 'hearing',
room_ref: room.reference,
})
}>
{soul.outside_hearing ? 'Enabled' : 'Disabled'}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Outside Sight">
<Button
color={soul.outside_sight ? 'green' : 'red'}
fluid
tooltip="Is the soul able to see the outside world?"
onClick={() =>
act('toggle_soul_outside_sense', {
target_soul: soul.reference,
sense_to_change: 'sight',
room_ref: room.reference,
})
}>
{soul.outside_sight ? 'Enabled' : 'Disabled'}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Hearing">
<Button
color={soul.internal_hearing ? 'green' : 'red'}
fluid
tooltip="Is the soul able to hear inside the room?"
onClick={() =>
act('toggle_soul_sense', {
target_soul: soul.reference,
sense_to_change: 'hearing',
room_ref: room.reference,
})
}>
{soul.internal_hearing ? 'Enabled' : 'Disabled'}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Sight">
<Button
color={soul.internal_sight ? 'green' : 'red'}
fluid
tooltip="Is the soul able to see inside the room?"
onClick={() =>
act('toggle_soul_sense', {
target_soul: soul.reference,
sense_to_change: 'sight',
room_ref: room.reference,
})
}>
{soul.internal_sight ? 'Enabled' : 'Disabled'}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Speech">
<Button
color={soul.able_to_speak ? 'green' : 'red'}
fluid
tooltip="Is the soul able to speak?"
onClick={() =>
act('toggle_soul_communication', {
target_soul: soul.reference,
communication_type: 'speech',
room_ref: room.reference,
})
}>
{soul.able_to_speak ? 'Enabled' : 'Disabled'}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Emote">
<Button
color={soul.able_to_emote ? 'green' : 'red'}
fluid
tooltip="Is the soul able to emote?"
onClick={() =>
act('toggle_soul_communication', {
target_soul: soul.reference,
communication_type: 'emote',
room_ref: room.reference,
})
}>
{soul.able_to_emote ? 'Enabled' : 'Disabled'}
</Button>
</LabeledList.Item>
</LabeledList>
<br />
<Button
fluid
icon="eject"
color="red"
onClick={() =>
act('remove_soul', {
target_soul: soul.reference,
room_ref: room.reference,
})
}>
Remove Soul
</Button>
</Collapsible>
</Flex.Item>
))}
</Flex>
</>
) : (
<> </>
)}
</Section>
))}
<Button
fluid
color="green"
icon="plus"
onClick={() => act('create_room', {})}>
Create new room
</Button>
<Button
fluid
color={ghost_joinable ? 'green' : 'red'}
icon={ghost_joinable ? 'door-open' : 'door-closed'}
onClick={() => act('toggle_joinable', {})}>
{ghost_joinable ? 'Opened' : 'Closed'} to ghosts
</Button>
<Button
fluid
color={require_approval ? 'green' : 'red'}
icon={require_approval ? 'lock' : 'lock-open'}
onClick={() => act('toggle_approval', {})}>
Approval is {require_approval ? '' : 'not'} required to join
</Button>
</Window.Content>
</Window>
);
};