diff --git a/code/__defines/faction.dm b/code/__defines/faction.dm
index 0d46529297b..2d975b6cbb6 100644
--- a/code/__defines/faction.dm
+++ b/code/__defines/faction.dm
@@ -21,6 +21,7 @@
#define FACTION_ALTEVIAN "altevian"
#define FACTION_CULT "cult"
+#define FACTION_GLAMOUR "glamour"
#define FACTION_PIRATE "pirate"
#define FACTION_SHADEKIN "shadekin"
#define FACTION_SPACE_GHOST "space ghost"
diff --git a/code/__defines/jobs.dm b/code/__defines/jobs.dm
index 40916e9ed40..6d625e96092 100644
--- a/code/__defines/jobs.dm
+++ b/code/__defines/jobs.dm
@@ -345,7 +345,7 @@
//___________________________________________________________
-// Talom jobs
+// Talon jobs
#define JOB_TALON_CAPTAIN "Talon Captain"
// Talon Captain alt titles
#define JOB_ALT_TALON_COMMANDER "Talon Commander"
diff --git a/code/datums/underwear/top.dm b/code/datums/underwear/top.dm
index 336bebd1cc5..a977717f425 100644
--- a/code/datums/underwear/top.dm
+++ b/code/datums/underwear/top.dm
@@ -78,6 +78,10 @@
name = "Binder"
icon_state = "binder_s"
+/datum/category_item/underwear/top/binder_hyper
+ name = "HYPER Binder"
+ icon_state = "hyper_binder"
+
/datum/category_item/underwear/top/straplessbinder
name = "Binder Strapless"
icon_state = "straplessbinder_s"
diff --git a/code/game/jobs/access_datum.dm b/code/game/jobs/access_datum.dm
index 32773362a01..d5d9e201392 100644
--- a/code/game/jobs/access_datum.dm
+++ b/code/game/jobs/access_datum.dm
@@ -493,6 +493,7 @@ var/const/access_pilot = 67
/var/const/access_syndicate = 150//General Syndicate Access
/datum/access/syndicate
id = access_syndicate
+ desc = "Syndicate"
access_type = ACCESS_TYPE_SYNDICATE
/*******
@@ -507,11 +508,13 @@ var/const/access_pilot = 67
/var/const/access_crate_cash = 200
/datum/access/crate_cash
id = access_crate_cash
+ desc = "Crate cash"
access_type = ACCESS_TYPE_NONE
/var/const/access_trader = 160//General Beruang Trader Access
/datum/access/trader
id = access_trader
+ desc = "Trader"
access_type = ACCESS_TYPE_PRIVATE
/var/const/access_alien = 300 // For things like crashed ships.
diff --git a/code/game/jobs/job/civilian_vr.dm b/code/game/jobs/job/civilian_vr.dm
index c7e2a2c7857..bd4093e4033 100644
--- a/code/game/jobs/job/civilian_vr.dm
+++ b/code/game/jobs/job/civilian_vr.dm
@@ -74,7 +74,7 @@
/datum/alt_title/supply_courier
title = JOB_ALT_SUPPLY_COURIER
- title_blurb = "A " + JOB_ALT_SUPPLY_COURIER + " is usually tasked with devlivering packages or cargo directly to whoever requires it."
+ title_blurb = "A " + JOB_ALT_SUPPLY_COURIER + " is usually tasked with delivering packages or cargo directly to whoever requires it."
/datum/alt_title/cargo_loader
title = JOB_ALT_CARGO_LOADER
diff --git a/code/game/objects/micro_structures.dm b/code/game/objects/micro_structures.dm
index ba05bf5724e..1f67efd6ed5 100644
--- a/code/game/objects/micro_structures.dm
+++ b/code/game/objects/micro_structures.dm
@@ -1,3 +1,5 @@
+var/global/list/micro_tunnels = list()
+
/obj/structure/micro_tunnel
name = "mouse hole"
desc = "A tiny little hole... where does it go?"
@@ -18,6 +20,10 @@
/mob/living/simple_mob/slime
)
+/obj/structure/micro_tunnel/New()
+ . = ..()
+ micro_tunnels.Add(src)
+
/obj/structure/micro_tunnel/Initialize()
. = ..()
if(name == initial(name))
@@ -34,6 +40,8 @@
thing.forceMove(get_turf(src.loc))
thing.cancel_camera()
+ micro_tunnels.Remove(src)
+
return ..()
/obj/structure/micro_tunnel/set_dir(new_dir)
@@ -63,7 +71,7 @@
if(myturf.z in P.expected_z_levels)
planet = P
else
- for(var/obj/structure/micro_tunnel/t in world)
+ for(var/obj/structure/micro_tunnel/t in micro_tunnels)
if(t == src)
continue
if(magic || t.magic)
@@ -342,6 +350,8 @@
return
if(QDELETED(src))
return
+ if(usr.loc != src)
+ return
var/obj/our_choice = choice
var/list/new_contained_mobs = list()
diff --git a/code/game/turfs/simulated/water.dm b/code/game/turfs/simulated/water.dm
index fc6f5b261e4..f6477e9764a 100644
--- a/code/game/turfs/simulated/water.dm
+++ b/code/game/turfs/simulated/water.dm
@@ -73,7 +73,7 @@
water_breath.adjust_gas(gasid, BREATH_MOLES) // They have no oxygen, but non-zero moles and temp
water_breath.temperature = above_air.temperature
return water_breath
- if(L && L.is_bad_swimmer() && depth >= 2 && !L.buckled())
+ if(L && L.is_bad_swimmer() && depth >= 2 && !L.buckled() && !L.flying)
if(prob(10))
L.visible_message("[L] splashes wildly.","You struggle to keep your head above the water!")
if(L.can_breathe_water())
diff --git a/code/modules/admin/modify_robot.dm b/code/modules/admin/modify_robot.dm
new file mode 100644
index 00000000000..3c1682f40ee
--- /dev/null
+++ b/code/modules/admin/modify_robot.dm
@@ -0,0 +1,641 @@
+/client/proc/modify_robot(var/mob/living/silicon/robot/target in silicon_mob_list)
+ set name = "Modify Robot"
+ set desc = "Allows to add or remove modules to/from robots."
+ set category = "Admin"
+ if(!check_rights(R_ADMIN|R_FUN|R_VAREDIT|R_EVENT))
+ return
+
+ var/datum/eventkit/modify_robot/modify_robot = new()
+ modify_robot.target = target
+ modify_robot.tgui_interact(src.mob)
+
+/datum/eventkit/modify_robot
+ var/mob/living/silicon/robot/target
+ var/mob/living/silicon/robot/source
+ var/ion_law = "IonLaw"
+ var/zeroth_law = "ZerothLaw"
+ var/inherent_law = "InherentLaw"
+ var/supplied_law = "SuppliedLaw"
+ var/supplied_law_position = MIN_SUPPLIED_LAW_NUMBER
+ var/list/datum/ai_laws/law_list
+
+/datum/eventkit/modify_robot/New()
+ . = ..()
+ log_and_message_admins("has used modify robot and is modifying [target]")
+ law_list = new()
+ init_subtypes(/datum/ai_laws, law_list)
+ law_list = dd_sortedObjectList(law_list)
+
+/datum/eventkit/modify_robot/tgui_close()
+ if(source)
+ qdel(source)
+
+/datum/eventkit/modify_robot/tgui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "ModifyRobot", "Modify Robot")
+ ui.open()
+
+/datum/eventkit/modify_robot/Destroy()
+ if(source)
+ qdel(source)
+ . = ..()
+
+/datum/eventkit/modify_robot/tgui_data(mob/user)
+ . = list()
+ // Target section for general data
+ if(target)
+ .["target"] = list()
+ .["target"]["name"] = target.name
+ .["target"]["ckey"] = target.ckey
+ .["target"]["module"] = target.module
+ .["target"]["crisis_override"] = target.crisis_override
+ .["target"]["active_restrictions"] = target.restrict_modules_to
+ var/list/possible_restrictions = list()
+ for(var/entry in robot_modules)
+ if(!target.restrict_modules_to.Find(entry))
+ possible_restrictions += entry
+ .["target"]["possible_restrictions"] = possible_restrictions
+ // Target section for options once a module has been selected
+ if(target.module)
+ .["target"]["active"] = target.icon_selected
+ .["target"]["front"] = icon2base64(get_flat_icon(target,dir=SOUTH,no_anim=TRUE))
+ .["target"]["side"] = icon2base64(get_flat_icon(target,dir=WEST,no_anim=TRUE))
+ .["target"]["side_alt"] = icon2base64(get_flat_icon(target,dir=EAST,no_anim=TRUE))
+ .["target"]["back"] = icon2base64(get_flat_icon(target,dir=NORTH,no_anim=TRUE))
+ .["target"]["modules"] = get_target_items(user)
+ var/list/module_options = list()
+ for(var/module in robot_modules)
+ module_options += module
+ .["model_options"] = module_options
+ // Data for the upgrade options
+ .["target"] += get_upgrades()
+ var/obj/item/weapon/gun/energy/kinetic_accelerator/kin = locate() in target.module.modules
+ if(kin)
+ .["target"]["pka"] += get_pka(kin)
+ // Radio section
+ var/list/radio_channels = list()
+ for(var/channel in target.radio.channels)
+ radio_channels += channel
+ var/list/availalbe_channels = list()
+ for(var/channel in (radiochannels - target.radio.channels))
+ availalbe_channels += channel
+ .["target"]["radio_channels"] = radio_channels
+ .["target"]["availalbe_channels"] = availalbe_channels
+ // Components
+ .["target"]["components"] = get_components()
+ .["cell"] = target.cell?.name
+ .["cell_options"] = get_cells()
+ // Access
+ .["id_icon"] = icon2html(target.idcard, user, sourceonly=TRUE)
+ var/list/active_access = list()
+ for(var/access in target.idcard?.GetAccess())
+ active_access += list(list("id" = access, "name" = get_access_desc(access)))
+ .["target"]["active_access"] = active_access
+ var/list/access_options = list()
+ for(var/datum/access/acc)
+ if(acc.id in target.idcard?.GetAccess())
+ continue
+ access_options += list(list("id" = acc.id, "name" = acc.desc))
+ .["access_options"] = access_options
+ // Section for source data for the module we might want to salvage
+ if(source)
+ .["source"] += get_module_source(user)
+ var/list/all_robots = list()
+ for(var/mob/living/silicon/robot/R in silicon_mob_list)
+ if(!R.loc)
+ continue
+ all_robots += list(list("displayText" = "[R]", "value" = "\ref[R]"))
+ .["all_robots"] = all_robots
+ // Law data
+ .["ion_law_nr"] = ionnum()
+ .["ion_law"] = ion_law
+ .["zeroth_law"] = zeroth_law
+ .["inherent_law"] = inherent_law
+ .["supplied_law"] = supplied_law
+ .["supplied_law_position"] = supplied_law_position
+
+ package_laws(., "zeroth_laws", list(target.laws.zeroth_law))
+ package_laws(., "ion_laws", target.laws.ion_laws)
+ package_laws(., "inherent_laws", target.laws.inherent_laws)
+ package_laws(., "supplied_laws", target.laws.supplied_laws)
+
+ .["isAI"] = isAI(target)
+
+ var/list/channels = list()
+ for(var/ch_name in target.law_channels())
+ channels[++channels.len] = list("channel" = ch_name)
+ .["channel"] = target.lawchannel
+ .["channels"] = channels
+ .["law_sets"] = package_multiple_laws(law_list)
+
+
+/datum/eventkit/modify_robot/tgui_state(mob/user)
+ return GLOB.tgui_admin_state
+
+/datum/eventkit/modify_robot/tgui_act(action, params)
+ . = ..()
+ if(.)
+ return
+ switch(action)
+ if("rename")
+ target.name = params["new_name"]
+ target.custom_name = params["new_name"]
+ target.real_name = params["new_name"]
+ return TRUE
+ if("select_target")
+ target = locate(params["new_target"])
+ log_and_message_admins("changed robot modifictation target to [target]")
+ return TRUE
+ if("toggle_crisis")
+ target.crisis_override = !target.crisis_override
+ return TRUE
+ if("add_restriction")
+ target.restrict_modules_to += params["new_restriction"]
+ return TRUE
+ if("remove_restriction")
+ target.restrict_modules_to -= params["rem_restriction"]
+ return TRUE
+ if("select_source")
+ if(source)
+ qdel(source)
+ source = new /mob/living/silicon/robot(null)
+ var/module_type = robot_modules[params["new_source"]]
+ source.modtype = params["new_source"]
+ var/obj/item/weapon/robot_module/robot/robot_type = new module_type(source)
+ source.sprite_datum = pick(SSrobot_sprites.get_module_sprites(source.modtype, source))
+ source.update_icon()
+ source.emag_items = 1
+ if(!istype(robot_type, /obj/item/weapon/robot_module/robot))
+ QDEL_NULL(source)
+ return TRUE
+ return TRUE
+ if("reset_module")
+ target.module_reset(FALSE)
+ return TRUE
+ if("add_module")
+ var/obj/item/add_item = locate(params["module"])
+ if(!add_item)
+ return TRUE
+ source.module.emag.Remove(add_item)
+ source.module.modules.Remove(add_item)
+ source.module.contents.Remove(add_item)
+ target.module.modules.Add(add_item)
+ target.module.contents.Add(add_item)
+ spawn(0)
+ SEND_SIGNAL(add_item, COMSIG_OBSERVER_MOVED)
+ target.hud_used.update_robot_modules_display()
+ if(istype(add_item, /obj/item/stack/))
+ var/obj/item/stack/item_with_synth = add_item
+ for(var/synth in item_with_synth.synths)
+ var/found = target.module.synths.Find(synth)
+ if(!found)
+ source.module.synths.Remove(synth)
+ target.module.synths.Add(synth)
+ else
+ item_with_synth.synths = list(target.module.synths[found])
+ return TRUE
+ if(istype(add_item, /obj/item/weapon/matter_decompiler/) || istype(add_item, /obj/item/device/dogborg/sleeper/compactor/decompiler/))
+ var/obj/item/weapon/matter_decompiler/item_with_matter = add_item
+ if(item_with_matter.metal)
+ var/found = target.module.synths.Find(item_with_matter.metal)
+ if(!found)
+ source.module.synths.Remove(item_with_matter.metal)
+ target.module.synths.Add(item_with_matter.metal)
+ else
+ item_with_matter.metal = target.module.synths[found]
+ if(item_with_matter.glass)
+ var/found = target.module.synths.Find(item_with_matter.glass)
+ if(!found)
+ source.module.synths.Remove(item_with_matter.glass)
+ target.module.synths.Add(item_with_matter.glass)
+ else
+ item_with_matter.glass = target.module.synths[found]
+ if(item_with_matter.wood)
+ var/found = target.module.synths.Find(item_with_matter.wood)
+ if(!found)
+ source.module.synths.Remove(item_with_matter.wood)
+ target.module.synths.Add(item_with_matter.wood)
+ else
+ item_with_matter.wood = target.module.synths[found]
+ if(item_with_matter.plastic)
+ var/found = target.module.synths.Find(item_with_matter.plastic)
+ if(!found)
+ source.module.synths.Remove(item_with_matter.plastic)
+ target.module.synths.Add(item_with_matter.plastic)
+ else
+ item_with_matter.plastic = target.module.synths[found]
+ return TRUE
+ if("rem_module")
+ var/obj/item/rem_item = locate(params["module"])
+ target.uneq_all()
+ target.hud_used.update_robot_modules_display(TRUE)
+ target.module.emag.Remove(rem_item)
+ target.module.modules.Remove(rem_item)
+ target.module.contents.Remove(rem_item)
+ qdel(rem_item)
+ return TRUE
+ if("swap_module")
+ if(!source)
+ return FALSE
+ var/mod_type = source.modtype
+ qdel(source.module)
+ var/module_type = robot_modules[target.modtype]
+ source.modtype = target.modtype
+ new module_type(source)
+ source.sprite_datum = target.sprite_datum
+ source.update_icon()
+ source.emag_items = 1
+ // Target
+ target.uneq_all()
+ target.hud_used.update_robot_modules_display(TRUE)
+ qdel(target.module)
+ target.modtype = mod_type
+ module_type = robot_modules[mod_type]
+ target.transform_with_anim()
+ new module_type(target)
+ target.hands.icon_state = target.get_hud_module_icon()
+ target.hud_used.update_robot_modules_display()
+ return TRUE
+ if("ert_toggle")
+ target.crisis_override = !target.crisis_override
+ target.module_reset(FALSE)
+ return TRUE
+ if("add_compatibility")
+ target.module.supported_upgrades |= text2path(params["upgrade"])
+ return TRUE
+ if("rem_compatibility")
+ target.module.supported_upgrades.Remove(text2path(params["upgrade"]))
+ return TRUE
+ if("add_upgrade")
+ var/new_upgrade = text2path(params["upgrade"])
+ if(new_upgrade == /obj/item/borg/upgrade/utility/reset)
+ var/obj/item/borg/upgrade/utility/reset/rmodul = new_upgrade
+ if(tgui_alert(usr, "Are you sure that you want to install [initial(rmodul.name)] and reset the robot's module?","Confirm",list("Yes","No"))!="Yes")
+ return FALSE
+ var/obj/item/borg/upgrade/U = new new_upgrade(null)
+ if(new_upgrade == /obj/item/borg/upgrade/utility/rename)
+ var/obj/item/borg/upgrade/utility/rename/UN = U
+ var/new_name = sanitizeSafe(tgui_input_text(usr, "Enter new robot name", "Robot Reclassification", UN.heldname, MAX_NAME_LEN), MAX_NAME_LEN)
+ if(new_name)
+ UN.heldname = new_name
+ U = UN
+ if(istype(U, /obj/item/borg/upgrade/restricted))
+ target.module.supported_upgrades |= new_upgrade
+ if(!U.action(target))
+ return FALSE
+ U.loc = target
+ target.hud_used.update_robot_modules_display()
+ return TRUE
+ if("install_modkit")
+ var/new_modkit = text2path(params["modkit"])
+ var/obj/item/weapon/gun/energy/kinetic_accelerator/kin = locate() in target.module.modules
+ var/obj/item/borg/upgrade/modkit/M = new new_modkit(null)
+ M.install(kin, target)
+ return TRUE
+ if("remove_modkit")
+ var/obj/item/weapon/gun/energy/kinetic_accelerator/kin = locate() in target.module.modules
+ var/obj/item/rem_kit = locate(params["modkit"])
+ kin.modkits.Remove(rem_kit)
+ qdel(rem_kit)
+ return TRUE
+ if("add_channel")
+ var/selected_radio_channel = params["channel"]
+ if(selected_radio_channel == CHANNEL_SPECIAL_OPS)
+ target.radio.centComm = 1
+ if(selected_radio_channel == CHANNEL_RAIDER)
+ qdel(target.radio.keyslot)
+ target.radio.keyslot = new /obj/item/device/encryptionkey/raider(target)
+ target.radio.syndie = 1
+ if(selected_radio_channel == CHANNEL_MERCENARY)
+ qdel(target.radio.keyslot)
+ target.radio.keyslot = new /obj/item/device/encryptionkey/syndicate(target)
+ target.radio.syndie = 1
+ target.module.channels += list("[selected_radio_channel]" = 1)
+ target.radio.channels[selected_radio_channel] += target.module.channels[selected_radio_channel]
+ target.radio.secure_radio_connections[selected_radio_channel] += radio_controller.add_object(target.radio, radiochannels[selected_radio_channel], RADIO_CHAT)
+ return TRUE
+ if("rem_channel")
+ var/selected_radio_channel = params["channel"]
+ if(selected_radio_channel == CHANNEL_SPECIAL_OPS)
+ target.radio.centComm = 0
+ target.module.channels -= selected_radio_channel
+ if((selected_radio_channel == CHANNEL_MERCENARY || selected_radio_channel == CHANNEL_RAIDER) && !(target.module.channels[CHANNEL_RAIDER] || target.module.channels[CHANNEL_MERCENARY]))
+ qdel(target.radio.keyslot)
+ target.radio.keyslot = null
+ target.radio.syndie = 0
+ target.radio.channels = list()
+ for(var/n_chan in target.module.channels)
+ target.radio.channels[n_chan] -= target.module.channels[n_chan]
+ radio_controller.remove_object(target.radio, radiochannels[selected_radio_channel])
+ target.radio.secure_radio_connections -= selected_radio_channel
+ return TRUE
+ if("add_component")
+ var/datum/robot_component/C = locate(params["component"])
+ if(C.wrapped)
+ qdel(C.wrapped)
+ if(istype(C, /datum/robot_component/actuator))
+ C.wrapped = new /obj/item/robot_parts/robot_component/actuator(target)
+ else if(istype(C, /datum/robot_component/radio))
+ C.wrapped = new /obj/item/robot_parts/robot_component/radio(target)
+ else if(istype(C, /datum/robot_component/cell))
+ var/new_cell = text2path(params["cell"])
+ target.cell = new new_cell(target)
+ C.wrapped = target.cell
+ else if(istype(C, /datum/robot_component/diagnosis_unit))
+ C.wrapped = new /obj/item/robot_parts/robot_component/diagnosis_unit(target)
+ else if(istype(C, /datum/robot_component/camera))
+ C.wrapped = new /obj/item/robot_parts/robot_component/camera(target)
+ else if(istype(C, /datum/robot_component/binary_communication))
+ C.wrapped = new /obj/item/robot_parts/robot_component/binary_communication_device(target)
+ else if(istype(C, /datum/robot_component/armour))
+ C.wrapped = new /obj/item/robot_parts/robot_component/armour(target)
+ C.brute_damage = 0
+ C.electronics_damage = 0
+ C.install()
+ C.installed = 1
+ return TRUE
+ if("rem_component")
+ var/datum/robot_component/C = locate(params["component"])
+ if(!C.wrapped)
+ return FALSE
+ C.uninstall()
+ C.brute_damage = 0
+ C.electronics_damage = 0
+ C.installed = 0
+ qdel(C.wrapped)
+ C.wrapped = null
+ if(istype(C, /datum/robot_component/cell))
+ target.cell = null
+ return TRUE
+ if("add_access")
+ target.idcard.access += text2num(params["access"])
+ return TRUE
+ if("rem_access")
+ target.idcard.access -= text2num(params["access"])
+ return TRUE
+ if("add_centcom")
+ target.idcard.access |= get_all_centcom_access()
+ return TRUE
+ if("rem_centcom")
+ target.idcard.access -= get_all_centcom_access()
+ return TRUE
+ if("add_station")
+ target.idcard.access |= get_all_station_access()
+ target.idcard.access |= access_synth
+ return TRUE
+ if("rem_station")
+ target.idcard.access -= get_all_station_access()
+ target.idcard.access -= access_synth
+ return TRUE
+ if("law_channel")
+ if(params["law_channel"] in target.law_channels())
+ target.lawchannel = params["law_channel"]
+ return TRUE
+ if("state_law")
+ var/datum/ai_law/AL = locate(params["ref"]) in target.laws.all_laws()
+ if(AL)
+ var/state_law = text2num(params["state_law"])
+ target.laws.set_state_law(AL, state_law)
+ return TRUE
+ if("add_zeroth_law")
+ if(zeroth_law && !target.laws.zeroth_law)
+ target.set_zeroth_law(zeroth_law)
+ target.lawsync()
+ return TRUE
+ if("add_ion_law")
+ if(ion_law)
+ target.add_ion_law(ion_law)
+ target.lawsync()
+ return TRUE
+ if("add_inherent_law")
+ if(inherent_law)
+ target.add_inherent_law(inherent_law)
+ target.lawsync()
+ return TRUE
+ if("add_supplied_law")
+ if(supplied_law && supplied_law_position >= 1 && MIN_SUPPLIED_LAW_NUMBER <= MAX_SUPPLIED_LAW_NUMBER)
+ target.add_supplied_law(supplied_law_position, supplied_law)
+ target.lawsync()
+ return TRUE
+ if("change_zeroth_law")
+ var/new_law = sanitize(params["val"])
+ if(new_law && new_law != zeroth_law)
+ zeroth_law = new_law
+ target.lawsync()
+ return TRUE
+ if("change_ion_law")
+ var/new_law = sanitize(params["val"])
+ if(new_law && new_law != ion_law)
+ ion_law = new_law
+ target.lawsync()
+ return TRUE
+ if("change_inherent_law")
+ var/new_law = sanitize(params["val"])
+ if(new_law && new_law != inherent_law)
+ inherent_law = new_law
+ target.lawsync()
+ return TRUE
+ if("change_supplied_law")
+ var/new_law = sanitize(params["val"])
+ if(new_law && new_law != supplied_law)
+ supplied_law = new_law
+ target.lawsync()
+ return TRUE
+ if("change_supplied_law_position")
+ var/new_position = tgui_input_number(usr, "Enter new supplied law position between 1 and [MAX_SUPPLIED_LAW_NUMBER], inclusive. Inherent laws at the same index as a supplied law will not be stated.", "Law Position", supplied_law_position, MAX_SUPPLIED_LAW_NUMBER, 1)
+ if(isnum(new_position))
+ supplied_law_position = CLAMP(new_position, 1, MAX_SUPPLIED_LAW_NUMBER)
+ target.lawsync()
+ return TRUE
+ if("edit_law")
+ var/datum/ai_law/AL = locate(params["edit_law"]) in target.laws.all_laws()
+ if(AL)
+ var/new_law = sanitize(tgui_input_text(usr, "Enter new law. Leaving the field blank will cancel the edit.", "Edit Law", AL.law))
+ if(new_law && new_law != AL.law)
+ AL.law = new_law
+ target.lawsync()
+ return TRUE
+ if("delete_law")
+ var/datum/ai_law/AL = locate(params["delete_law"]) in target.laws.all_laws()
+ if(AL)
+ target.delete_law(AL)
+ target.lawsync()
+ return TRUE
+ if("state_laws")
+ target.statelaws(target.laws)
+ return TRUE
+ if("state_law_set")
+ var/datum/ai_laws/ALs = locate(params["state_law_set"]) in law_list
+ if(ALs)
+ target.statelaws(ALs)
+ return TRUE
+ if("transfer_laws")
+ var/datum/ai_laws/ALs = locate(params["transfer_laws"]) in law_list
+ if(ALs)
+ ALs.sync(target, 0)
+ target.lawsync()
+ return TRUE
+ if("notify_laws")
+ to_chat(target, "Law Notice")
+ target.laws.show_laws(target)
+ if(isAI(target))
+ var/mob/living/silicon/ai/AI = target
+ for(var/mob/living/silicon/robot/R in AI.connected_robots)
+ to_chat(R, "Law Notice")
+ R.laws.show_laws(R)
+ if(usr != target)
+ to_chat(usr, "Laws displayed.")
+ return TRUE
+
+/datum/eventkit/modify_robot/proc/get_target_items(var/mob/user)
+ var/list/target_items = list()
+ for(var/obj/item in target.module.modules)
+ target_items += list(list("name" = item.name, "ref" = "\ref[item]", "icon" = icon2html(item, user, sourceonly=TRUE), "desc" = item.desc))
+ return target_items
+
+/datum/eventkit/modify_robot/proc/get_module_source(var/mob/user)
+ var/list/source_list = list()
+ source_list["model"] = source.module
+ source_list["front"] = icon2base64(get_flat_icon(source,dir=SOUTH,no_anim=TRUE))
+ var/list/source_items = list()
+ for(var/obj/item in (source.module.modules | source.module.emag))
+ var/exists
+ for(var/obj/has_item in (target.module.modules + target.module.emag))
+ if(has_item.name == item.name)
+ exists = TRUE
+ break
+ if(exists)
+ continue
+ source_items += list(list("name" = item.name, "ref" = "\ref[item]", "icon" = icon2html(item, user, sourceonly=TRUE), "desc" = item.desc))
+ source_list["modules"] = source_items
+ return source_list
+
+/datum/eventkit/modify_robot/proc/get_upgrades()
+ var/list/all_upgrades = list()
+ var/list/whitelisted_upgrades = list()
+ var/list/blacklisted_upgrades = list()
+ for(var/datum/design/item/prosfab/robot_upgrade/restricted/upgrade)
+ if(!upgrade.name)
+ continue
+ if(!(initial(upgrade.build_path) in target.module.supported_upgrades))
+ whitelisted_upgrades += list(list("name" = initial(upgrade.name), "path" = "[initial(upgrade.build_path)]"))
+ else
+ blacklisted_upgrades += list(list("name" = initial(upgrade.name), "path" = "[initial(upgrade.build_path)]"))
+ all_upgrades["whitelisted_upgrades"] = whitelisted_upgrades
+ all_upgrades["blacklisted_upgrades"] = blacklisted_upgrades
+ var/list/utility_upgrades = list()
+ for(var/datum/design/item/prosfab/robot_upgrade/utility/upgrade)
+ if(!upgrade.name)
+ continue
+ if(!(target.has_upgrade(initial(upgrade.build_path))))
+ utility_upgrades += list(list("name" = initial(upgrade.name), "path" = "[initial(upgrade.build_path)]"))
+ all_upgrades["utility_upgrades"] = utility_upgrades
+ var/list/basic_upgrades = list()
+ for(var/datum/design/item/prosfab/robot_upgrade/basic/upgrade)
+ if(!upgrade.name)
+ continue
+ if(!(target.has_upgrade(initial(upgrade.build_path))))
+ basic_upgrades += list(list("name" = initial(upgrade.name), "path" = "[initial(upgrade.build_path)]", "installed" = 0))
+ else
+ basic_upgrades += list(list("name" = initial(upgrade.name), "path" = "[initial(upgrade.build_path)]", "installed" = 1))
+ all_upgrades["basic_upgrades"] = basic_upgrades
+ var/list/advanced_upgrades = list()
+ for(var/datum/design/item/prosfab/robot_upgrade/advanced/upgrade)
+ if(!upgrade.name)
+ continue
+ if(!(target.has_upgrade(initial(upgrade.build_path))))
+ advanced_upgrades += list(list("name" = initial(upgrade.name), "path" = "[initial(upgrade.build_path)]", "installed" = 0))
+ else
+ advanced_upgrades += list(list("name" = initial(upgrade.name), "path" = "[initial(upgrade.build_path)]", "installed" = 1))
+ all_upgrades["advanced_upgrades"] = advanced_upgrades
+ var/list/restricted_upgrades = list()
+ for(var/datum/design/item/prosfab/robot_upgrade/restricted/upgrade)
+ if(!upgrade.name)
+ continue
+ if(!(target.has_upgrade(initial(upgrade.build_path))))
+ if(!(initial(upgrade.build_path) in target.module.supported_upgrades))
+ restricted_upgrades += list(list("name" = initial(upgrade.name), "path" = "[initial(upgrade.build_path)]", "installed" = 2))
+ continue
+ restricted_upgrades += list(list("name" = initial(upgrade.name), "path" = "[initial(upgrade.build_path)]", "installed" = 0))
+ else
+ restricted_upgrades += list(list("name" = initial(upgrade.name), "path" = "[initial(upgrade.build_path)]", "installed" = 1))
+ all_upgrades["restricted_upgrades"] = restricted_upgrades
+ return all_upgrades
+
+/datum/eventkit/modify_robot/proc/get_pka(var/obj/item/weapon/gun/energy/kinetic_accelerator/kin)
+ var/list/pka = list()
+ pka["name"] = kin.name
+ var/list/installed_modkits = list()
+ for(var/obj/item/borg/upgrade/modkit/modkit in kin.modkits)
+ installed_modkits += list(list("name" = modkit.name, "ref" = "\ref[modkit]", "costs" = modkit.cost))
+ pka["installed_modkits"] = installed_modkits
+ var/list/modkits = list()
+ for(var/modkit in typesof(/obj/item/borg/upgrade/modkit))
+ var/obj/item/borg/upgrade/modkit/single_modkit = modkit
+ if(single_modkit == /obj/item/borg/upgrade/modkit)
+ continue
+ if(kin.get_remaining_mod_capacity() < initial(single_modkit.cost))
+ modkits += list(list("name" = initial(single_modkit.name), "path" = single_modkit, "costs" = initial(single_modkit.cost), "denied" = TRUE, "denied_by" = "Insufficient capacity!"))
+ continue
+ if(initial(single_modkit.denied_type))
+ var/number_of_denied = 0
+ var/denied = FALSE
+ for(var/A in kin.get_modkits())
+ var/obj/item/borg/upgrade/modkit/M = A
+ if(istype(M, initial(single_modkit.denied_type)))
+ number_of_denied++
+ if(number_of_denied >= initial(single_modkit.maximum_of_type))
+ var/obj/item/denied_type = initial(single_modkit.denied_type)
+ modkits += list(list("name" = initial(single_modkit.name), "path" = single_modkit, "costs" = initial(single_modkit.cost), "denied" = TRUE, "denied_by" = "[initial(denied_type.name)]"))
+ denied = TRUE
+ break
+ if(denied)
+ continue
+ modkits += list(list("name" = initial(single_modkit.name), "path" = single_modkit, "costs" = initial(single_modkit.cost)))
+ pka["modkits"] = modkits
+ pka["capacity"] = kin.get_remaining_mod_capacity()
+ pka["max_capacity"] = kin.max_mod_capacity
+ return pka
+
+/datum/eventkit/modify_robot/proc/get_cells()
+ var/list/cell_options = list()
+ for(var/cell in typesof(/obj/item/weapon/cell))
+ var/obj/item/weapon/cell/C = cell
+ if(initial(C.name) == "power cell")
+ continue
+ if(ispath(C, /obj/item/weapon/cell/standin))
+ continue
+ if(ispath(C, /obj/item/weapon/cell/device))
+ continue
+ if(ispath(C, /obj/item/weapon/cell/mech))
+ continue
+ if(cell_options[initial(C.name)]) // empty cells are defined after normal cells!
+ continue
+ cell_options += list(initial(C.name) = list("path" = "[C]", "charge" = initial(C.maxcharge), "max_charge" = initial(C.maxcharge), "charge_amount" = initial(C.charge_amount) , "self_charge" = initial(C.self_recharge))) // our cells do not have their charge predefined, they do it on init, so both maaxcharge for now
+ return cell_options
+
+/datum/eventkit/modify_robot/proc/get_components()
+ var/list/components = list()
+ for(var/entry in target.components)
+ var/datum/robot_component/C = target.components[entry]
+ components += list(list("name" = C.name, "ref" = "\ref[C]", "brute_damage" = C.brute_damage, "electronics_damage" = C.electronics_damage, "max_damage" = C.max_damage, "installed" = C.installed, "exists" = (C.wrapped ? TRUE : FALSE)))
+ return components
+
+/datum/eventkit/modify_robot/proc/package_laws(var/list/data, var/field, var/list/datum/ai_law/laws)
+ var/list/packaged_laws = list()
+ for(var/datum/ai_law/AL in laws)
+ packaged_laws[++packaged_laws.len] = list("law" = AL.law, "index" = AL.get_index(), "state" = target.laws.get_state_law(AL), "ref" = "\ref[AL]")
+ data[field] = packaged_laws
+ data["has_[field]"] = packaged_laws.len
+
+/datum/eventkit/modify_robot/proc/package_multiple_laws(var/list/datum/ai_laws/laws)
+ var/list/law_sets = list()
+ for(var/datum/ai_laws/ALs in laws)
+ var/list/packaged_laws = list()
+ package_laws(packaged_laws, "zeroth_laws", list(ALs.zeroth_law, ALs.zeroth_law_borg))
+ package_laws(packaged_laws, "ion_laws", ALs.ion_laws)
+ package_laws(packaged_laws, "inherent_laws", ALs.inherent_laws)
+ package_laws(packaged_laws, "supplied_laws", ALs.supplied_laws)
+ law_sets[++law_sets.len] = list("name" = ALs.name, "header" = ALs.law_header, "ref" = "\ref[ALs]","laws" = packaged_laws)
+ return law_sets
diff --git a/code/modules/admin/player_effects.dm b/code/modules/admin/player_effects.dm
index b2d0a83d3de..1a77dc227a7 100644
--- a/code/modules/admin/player_effects.dm
+++ b/code/modules/admin/player_effects.dm
@@ -74,7 +74,7 @@
return
Tar.adjust_fire_stacks(10)
Tar.IgniteMob()
- Tar.visible_message("[target] bursts into flames!")
+ Tar.visible_message(span_danger("[target] bursts into flames!"))
if("lightning_strike")
var/mob/living/carbon/human/Tar = target
@@ -83,7 +83,7 @@
var/turf/T = get_step(get_step(target, NORTH), NORTH)
T.Beam(target, icon_state="lightning[rand(1,12)]", time = 5)
Tar.electrocute_act(75,def_zone = BP_HEAD)
- target.visible_message("[target] is struck by lightning!")
+ target.visible_message(span_danger("[target] is struck by lightning!"))
if("shadekin_attack")
var/turf/Tt = get_turf(target) //Turf for target
@@ -180,7 +180,7 @@
shadekin.ckey = target.ckey
else //Permakin'd
- to_chat(target,"You're carried off into The Dark by the [shadekin]. Who knows if you'll find your way back?")
+ to_chat(target,span_danger("You're carried off into The Dark by the [shadekin]. Who knows if you'll find your way back?"))
target.ghostize()
qdel(target)
qdel(shadekin)
@@ -202,7 +202,7 @@
if("peppernade")
var/obj/item/weapon/grenade/chem_grenade/teargas/grenade = new /obj/item/weapon/grenade/chem_grenade/teargas
grenade.loc = target.loc
- to_chat(target,"GRENADE?!")
+ to_chat(target,span_warning("GRENADE?!"))
grenade.detonate()
if("spicerequest")
@@ -248,6 +248,39 @@
target.ClearTransform()
target.update_transform()
+ if("pie_splat")
+ new/obj/effect/decal/cleanable/pie_smudge(get_turf(target))
+ playsound(target, 'sound/effects/slime_squish.ogg', 100, 1, get_rand_frequency(), falloff = 5)
+ target.Weaken(1)
+ target.visible_message(span_danger("[target] is struck by pie!"))
+
+ if("spicy_air")
+ to_chat(target, span_warning("Spice spice baby!"))
+ target.eye_blurry = max(target.eye_blurry, 25)
+ target.Blind(10)
+ target.Stun(5)
+ target.Weaken(5)
+ playsound(target, 'sound/effects/spray2.ogg', 100, 1, get_rand_frequency(), falloff = 5)
+
+ if("hot_dog")
+ playsound(target, 'sound/effects/whistle.ogg', 50, 1, get_rand_frequency(), falloff = 5)
+ sleep(2 SECONDS)
+ target.Stun(10)
+ if(!ishuman(target))
+ return
+ var/mob/living/carbon/human/H = target
+ if(H.head)
+ H.unEquip(H.head)
+ if(H.wear_suit)
+ H.unEquip(H.wear_suit)
+ var/obj/item/clothing/suit = new /obj/item/clothing/suit/storage/hooded/foodcostume/hotdog
+ var/obj/item/clothing/hood = new /obj/item/clothing/head/hood_vr/hotdog_hood
+ H.equip_to_slot_if_possible(suit, slot_wear_suit, 0, 0, 1)
+ H.equip_to_slot_if_possible(hood, slot_head, 0, 0, 1)
+ sleep(5 SECONDS)
+ qdel(suit)
+ qdel(hood)
+
////////MEDICAL//////////////
if("appendicitis")
@@ -528,10 +561,10 @@
return
var/input_NIF
if(!Tar.get_organ(BP_HEAD))
- to_chat(user,"Target is unsuitable.")
+ to_chat(user,span_warning("Target is unsuitable."))
return
if(Tar.nif)
- to_chat(user,"Target already has a NIF.")
+ to_chat(user,span_warning("Target already has a NIF."))
return
if(Tar.species.flags & NO_SCAN)
var/obj/item/device/nif/S = /obj/item/device/nif/bioadap
diff --git a/code/modules/client/preference_setup/loadout/loadout_accessories.dm b/code/modules/client/preference_setup/loadout/loadout_accessories.dm
index 81b6d810448..64e5e86cce3 100644
--- a/code/modules/client/preference_setup/loadout/loadout_accessories.dm
+++ b/code/modules/client/preference_setup/loadout/loadout_accessories.dm
@@ -569,3 +569,61 @@ Talon pin
)
gear_tweaks += new/datum/gear_tweak/path(wristband_lists)
gear_tweaks += gear_tweak_free_color_choice
+
+/datum/gear/accessory/plushie
+ display_name = "Plushie Selection"
+ description = "A single plushie of your selection!"
+ path = /obj/item/toy/plushie/nymph
+
+/datum/gear/accessory/plushie/New()
+ ..()
+ var/plushie = list(
+ "Nymph" = /obj/item/toy/plushie/nymph,
+ "Mouse" = /obj/item/toy/plushie/mouse,
+ "Kitten" = /obj/item/toy/plushie/kitten,
+ "Lizard" = /obj/item/toy/plushie/lizard,
+ "Black cat" = /obj/item/toy/plushie/black_cat,
+ "Black fox" = /obj/item/toy/plushie/black_fox,
+ "Blue fox" = /obj/item/toy/plushie/blue_fox,
+ "Carp plushie" = /obj/random/carp_plushie,
+ "Coffee fox" = /obj/item/toy/plushie/coffee_fox,
+ "Corgi" = /obj/item/toy/plushie/corgi,
+ "Crimson fox" = /obj/item/toy/plushie/crimson_fox,
+ "Deer" = /obj/item/toy/plushie/deer,
+ "Girly corgi" = /obj/item/toy/plushie/girly_corgi,
+ "Grey cat" = /obj/item/toy/plushie/grey_cat,
+ "Marble fox" = /obj/item/toy/plushie/marble_fox,
+ "Octopus" = /obj/item/toy/plushie/octopus,
+ "Orange cat" = /obj/item/toy/plushie/orange_cat,
+ "Orange fox" = /obj/item/toy/plushie/orange_fox,
+ "Pink fox" = /obj/item/toy/plushie/pink_fox,
+ "Purple fox" = /obj/item/toy/plushie/purple_fox,
+ "Red fox" = /obj/item/toy/plushie/red_fox,
+ "Robo corgi" = /obj/item/toy/plushie/robo_corgi,
+ "Siamese cat" = /obj/item/toy/plushie/siamese_cat,
+ "Spider" = /obj/item/toy/plushie/spider,
+ "Tabby cat" = /obj/item/toy/plushie/tabby_cat,
+ "Tuxedo cat" = /obj/item/toy/plushie/tuxedo_cat,
+ "White cat" = /obj/item/toy/plushie/white_cat,
+ "Lizard" = /obj/item/toy/plushie/lizardplushie,
+ "Kobold" = /obj/item/toy/plushie/lizardplushie/kobold,
+ "Resh" = /obj/item/toy/plushie/lizardplushie/resh,
+ "Slime" = /obj/item/toy/plushie/slimeplushie,
+ "Box" = /obj/item/toy/plushie/box,
+ "Robot" = /obj/item/toy/plushie/borgplushie,
+ "Medihound" = /obj/item/toy/plushie/borgplushie/medihound,
+ "Scrubpuppy" = /obj/item/toy/plushie/borgplushie/scrubpuppy,
+ "Foxbear" = /obj/item/toy/plushie/foxbear,
+ "Operative" = /obj/item/toy/plushie/nukeplushie,
+ "Otter" = /obj/item/toy/plushie/otter,
+ "Vox" = /obj/item/toy/plushie/vox,
+ "Shark" = /obj/item/toy/plushie/shark,
+ "Sec Drake" = /obj/item/toy/plushie/borgplushie/drake/sec,
+ "Med Drake" = /obj/item/toy/plushie/borgplushie/drake/med,
+ "Sci Drake" = /obj/item/toy/plushie/borgplushie/drake/sci,
+ "Jani Drake" = /obj/item/toy/plushie/borgplushie/drake/jani,
+ "Eng Drake" = /obj/item/toy/plushie/borgplushie/drake/eng,
+ "Mine Drake" = /obj/item/toy/plushie/borgplushie/drake/mine,
+ "Trauma Drake" = /obj/item/toy/plushie/borgplushie/drake/trauma,
+ )
+ gear_tweaks += new/datum/gear_tweak/path(plushie)
diff --git a/code/modules/eventkit/generic_objects/generic_item.dm b/code/modules/eventkit/generic_objects/generic_item.dm
index 48432ee370a..02fa8e264c5 100644
--- a/code/modules/eventkit/generic_objects/generic_item.dm
+++ b/code/modules/eventkit/generic_objects/generic_item.dm
@@ -29,7 +29,8 @@
else
icon = 'icons/obj/props/items.dmi'
icon_state = icon_state_on
- src.visible_message("[text_activated]")
+ if(user)
+ user.visible_message("[text_activated]")
update_icon()
if(effect == 1)
var/datum/effect/effect/system/spark_spread/s = new /datum/effect/effect/system/spark_spread
@@ -83,7 +84,8 @@
icon = icon_off
else
icon = 'icons/obj/props/items.dmi'
- src.visible_message("[text_deactivated]")
+ if(user)
+ user.visible_message("[text_deactivated]")
update_icon()
return ..()
diff --git a/code/modules/eventkit/generic_objects/generic_structure.dm b/code/modules/eventkit/generic_objects/generic_structure.dm
index 402d21bbf08..cfd8cfd85fc 100644
--- a/code/modules/eventkit/generic_objects/generic_structure.dm
+++ b/code/modules/eventkit/generic_objects/generic_structure.dm
@@ -77,6 +77,14 @@
if(effect == 4)
var/atom/o = new object(get_turf(src))
src.visible_message("[src] has produced [o]!")
+ if(effect == 5)
+ for (var/mob/O in viewers(src, null))
+ if(get_dist(src, O) > 7)
+ continue
+
+ if(istype(O, /mob/living/carbon/human))
+ var/mob/living/carbon/human/H = O
+ H.fear = 200
if(sound_activated)
playsound(src, sound_activated, 50, 1)
else if(togglable)
@@ -96,8 +104,8 @@
/obj/structure/generic_structure/attackby(obj/item/weapon/W as obj, mob/user as mob)
if(wrenchable && W.has_tool_quality(TOOL_WRENCH))
add_fingerprint(user)
- anchored = !anchored
to_chat(user, "You [anchored? "un" : ""]secured \the [src]!")
+ anchored = !anchored
/client/proc/generic_structure()
set category = "EventKit"
@@ -268,7 +276,7 @@
if(s_icon_state_on == "Upload Own Sprite")
s_icon2 = input(usr, "Choose an image file to upload. Images that are not 32x32 will need to have their positions offset.","Upload Icon") as null|file
s_delay = tgui_input_number(src, "Do you want it to take time to put turn on? Choose a number of deciseconds to activate, or 0 for instant.", "Delay")
- var/check_effect = tgui_alert(src, "Produce an effect on activation?", "Effect?", list("No", "Spark", "Flicker Lights", "Flash", "Spawn Item", "Cancel"))
+ var/check_effect = tgui_alert(src, "Produce an effect on activation?", "Effect?", list("No", "Spark", "Flicker Lights", "Flash", "Spawn Item", "Fear", "Cancel"))
if(!check_effect || check_effect == "Cancel")
return
if(check_effect == "No")
@@ -282,6 +290,8 @@
if(check_effect == "Spawn Item")
s_effect = 4
s_object = get_path_from_partial_text()
+ if(check_effect == "Fear")
+ s_effect = 5
var/check_sound = tgui_alert(src, "Play a sound when turning on?", "Sound", list("Yes", "No", "Cancel"))
if(!check_sound || check_sound == "Cancel")
return
diff --git a/code/modules/events/event_dynamic.dm b/code/modules/events/event_dynamic.dm
index 110e17421df..95612d2fd72 100644
--- a/code/modules/events/event_dynamic.dm
+++ b/code/modules/events/event_dynamic.dm
@@ -30,9 +30,9 @@ var/list/event_last_fired = list()
var/minutes_passed = world.time/600
var/list/active_with_role = number_active_with_role()
- //var/engineer_count = number_active_with_role(JOB_ENGINEER)
- //var/security_count = number_active_with_role("Security")
- //var/medical_count = number_active_with_role("Medical")
+ //var/engineer_count = number_active_with_role(DEPARTMENT_ENGINEERING)
+ //var/security_count = number_active_with_role(DEPARTMENT_SECURITY)
+ //var/medical_count = number_active_with_role(DEPARTMENT_MEDICAL)
//var/AI_count = number_active_with_role(JOB_AI)
//var/janitor_count = number_active_with_role(JOB_JANITOR)
@@ -56,33 +56,33 @@ var/list/event_last_fired = list()
possibleEvents[/datum/event/money_hacker] = max(min(25, player_list.len) * 4, 200)
- possibleEvents[/datum/event/carp_migration] = 20 + 10 * active_with_role[JOB_ENGINEER]
+ possibleEvents[/datum/event/carp_migration] = 20 + 10 * active_with_role[DEPARTMENT_ENGINEERING]
possibleEvents[/datum/event/brand_intelligence] = 20 + 25 * active_with_role[JOB_JANITOR]
- possibleEvents[/datum/event/rogue_drone] = 5 + 25 * active_with_role[JOB_ENGINEER] + 25 * active_with_role["Security"]
+ possibleEvents[/datum/event/rogue_drone] = 5 + 25 * active_with_role[DEPARTMENT_ENGINEERING] + 25 * active_with_role[DEPARTMENT_SECURITY]
possibleEvents[/datum/event/infestation] = 100 + 100 * active_with_role[JOB_JANITOR]
- possibleEvents[/datum/event/communications_blackout] = 50 + 25 * active_with_role[JOB_AI] + active_with_role[JOB_SCIENTIST] * 25
- possibleEvents[/datum/event/ionstorm] = active_with_role[JOB_AI] * 25 + active_with_role[JOB_CYBORG] * 25 + active_with_role[JOB_ENGINEER] * 10 + active_with_role[JOB_SCIENTIST] * 5
- possibleEvents[/datum/event/grid_check] = 25 + 10 * active_with_role[JOB_ENGINEER]
- possibleEvents[/datum/event/electrical_storm] = 15 * active_with_role[JOB_JANITOR] + 5 * active_with_role[JOB_ENGINEER]
- possibleEvents[/datum/event/wallrot] = 30 * active_with_role[JOB_ENGINEER] + 50 * active_with_role[JOB_ALT_GARDENER]
+ possibleEvents[/datum/event/communications_blackout] = 50 + 25 * active_with_role[JOB_AI] + active_with_role[DEPARTMENT_RESEARCH] * 25
+ possibleEvents[/datum/event/ionstorm] = active_with_role[JOB_AI] * 25 + active_with_role[JOB_CYBORG] * 25 + active_with_role[DEPARTMENT_ENGINEERING] * 10 + active_with_role[DEPARTMENT_RESEARCH] * 5
+ possibleEvents[/datum/event/grid_check] = 25 + 10 * active_with_role[DEPARTMENT_ENGINEERING]
+ possibleEvents[/datum/event/electrical_storm] = 15 * active_with_role[JOB_JANITOR] + 5 * active_with_role[DEPARTMENT_ENGINEERING]
+ possibleEvents[/datum/event/wallrot] = 30 * active_with_role[DEPARTMENT_ENGINEERING] + 50 * active_with_role[JOB_ALT_GARDENER]
if(!spacevines_spawned)
- possibleEvents[/datum/event/spacevine] = 10 + 5 * active_with_role[JOB_ENGINEER]
+ possibleEvents[/datum/event/spacevine] = 10 + 5 * active_with_role[DEPARTMENT_ENGINEERING]
if(minutes_passed >= 30) // Give engineers time to set up engine
- possibleEvents[/datum/event/meteor_wave] = 10 * active_with_role[JOB_ENGINEER]
- possibleEvents[/datum/event/blob] = 10 * active_with_role[JOB_ENGINEER]
+ possibleEvents[/datum/event/meteor_wave] = 10 * active_with_role[DEPARTMENT_ENGINEERING]
+ possibleEvents[/datum/event/blob] = 10 * active_with_role[DEPARTMENT_ENGINEERING]
- if(active_with_role["Medical"] > 0)
- possibleEvents[/datum/event/radiation_storm] = active_with_role["Medical"] * 10
- possibleEvents[/datum/event/spontaneous_appendicitis] = active_with_role["Medical"] * 10
+ if(active_with_role[DEPARTMENT_MEDICAL] > 0)
+ possibleEvents[/datum/event/radiation_storm] = active_with_role[DEPARTMENT_MEDICAL] * 10
+ possibleEvents[/datum/event/spontaneous_appendicitis] = active_with_role[DEPARTMENT_MEDICAL] * 10
- possibleEvents[/datum/event/prison_break] = active_with_role["Security"] * 50
- if(active_with_role["Security"] > 0)
+ possibleEvents[/datum/event/prison_break] = active_with_role[DEPARTMENT_SECURITY] * 50
+ if(active_with_role[DEPARTMENT_SECURITY] > 0)
if(!sent_spiders_to_station)
- possibleEvents[/datum/event/spider_infestation] = max(active_with_role["Security"], 5) + 5
- possibleEvents[/datum/event/random_antag] = max(active_with_role["Security"], 5) + 2.5
+ possibleEvents[/datum/event/spider_infestation] = max(active_with_role[DEPARTMENT_SECURITY], 5) + 5
+ possibleEvents[/datum/event/random_antag] = max(active_with_role[DEPARTMENT_SECURITY], 5) + 2.5
for(var/event_type in event_last_fired) if(possibleEvents[event_type])
var/time_passed = world.time - event_last_fired[event_type]
@@ -179,10 +179,10 @@ var/list/event_last_fired = list()
// Note that this isn't sorted by department, because e.g. having a roboticist shouldn't make meteors spawn.
/proc/number_active_with_role()
var/list/active_with_role = list()
- active_with_role[JOB_ENGINEER] = 0
- active_with_role["Medical"] = 0
- active_with_role["Security"] = 0
- active_with_role[JOB_SCIENTIST] = 0
+ active_with_role[DEPARTMENT_ENGINEERING] = 0
+ active_with_role[DEPARTMENT_MEDICAL] = 0
+ active_with_role[DEPARTMENT_SECURITY] = 0
+ active_with_role[DEPARTMENT_RESEARCH] = 0
active_with_role[JOB_AI] = 0
active_with_role[JOB_CYBORG] = 0
active_with_role[JOB_JANITOR] = 0
@@ -198,29 +198,29 @@ var/list/event_last_fired = list()
var/mob/living/silicon/robot/R = M
if(R.module)
if(istype(R.module, /obj/item/weapon/robot_module/robot/engineering))
- active_with_role[JOB_ENGINEER]++
+ active_with_role[DEPARTMENT_ENGINEERING]++
else if(istype(R.module, /obj/item/weapon/robot_module/robot/security))
- active_with_role["Security"]++
+ active_with_role[DEPARTMENT_SECURITY]++
else if(istype(R.module, /obj/item/weapon/robot_module/robot/medical))
- active_with_role["Medical"]++
+ active_with_role[DEPARTMENT_MEDICAL]++
else if(istype(R.module, /obj/item/weapon/robot_module/robot/research))
- active_with_role[JOB_SCIENTIST]++
+ active_with_role[DEPARTMENT_RESEARCH]++
else if(istype(R.module, /obj/item/weapon/robot_module/robot/janitor))
active_with_role[JOB_JANITOR]++
else if(istype(R.module, /obj/item/weapon/robot_module/robot/clerical/butler))
active_with_role[JOB_BOTANIST]++
if(M.mind.assigned_role in SSjob.get_job_titles_in_department(DEPARTMENT_ENGINEERING))
- active_with_role[JOB_ENGINEER]++
+ active_with_role[DEPARTMENT_ENGINEERING]++
if(M.mind.assigned_role in SSjob.get_job_titles_in_department(DEPARTMENT_MEDICAL))
- active_with_role["Medical"]++
+ active_with_role[DEPARTMENT_MEDICAL]++
if(M.mind.assigned_role in SSjob.get_job_titles_in_department(DEPARTMENT_SECURITY))
- active_with_role["Security"]++
+ active_with_role[DEPARTMENT_SECURITY]++
if(M.mind.assigned_role in SSjob.get_job_titles_in_department(DEPARTMENT_RESEARCH))
- active_with_role[JOB_SCIENTIST]++
+ active_with_role[DEPARTMENT_RESEARCH]++
if(M.mind.assigned_role == JOB_AI)
active_with_role[JOB_AI]++
diff --git a/code/modules/mob/living/carbon/human/human_movement.dm b/code/modules/mob/living/carbon/human/human_movement.dm
index 65b4039cddc..1239ddfc613 100644
--- a/code/modules/mob/living/carbon/human/human_movement.dm
+++ b/code/modules/mob/living/carbon/human/human_movement.dm
@@ -147,7 +147,7 @@
if(!T)
return 0
- if(T.movement_cost)
+ if(T.movement_cost && !flying) //VOREStation Add: If you are flying you are probably not affected by the terrain on the ground.
var/turf_move_cost = T.movement_cost
if(istype(T, /turf/simulated/floor/water))
if(species.water_movement)
diff --git a/code/modules/mob/living/carbon/human/species/lleill/hanner.dm b/code/modules/mob/living/carbon/human/species/lleill/hanner.dm
index 8269104dab2..1a684a477b5 100644
--- a/code/modules/mob/living/carbon/human/species/lleill/hanner.dm
+++ b/code/modules/mob/living/carbon/human/species/lleill/hanner.dm
@@ -88,6 +88,7 @@
/mob/living/carbon/human/proc/shapeshifter_select_wings,
/mob/living/carbon/human/proc/shapeshifter_select_tail,
/mob/living/carbon/human/proc/shapeshifter_select_ears,
+ /mob/living/carbon/human/proc/shapeshifter_select_eye_colour,
/mob/living/proc/set_size,
// /mob/living/carbon/human/proc/lleill_contact,
// /mob/living/carbon/human/proc/lleill_alchemy,
diff --git a/code/modules/mob/living/silicon/robot/sprites/civilian.dm b/code/modules/mob/living/silicon/robot/sprites/civilian.dm
index 18f371accfc..e390e389fce 100644
--- a/code/modules/mob/living/silicon/robot/sprites/civilian.dm
+++ b/code/modules/mob/living/silicon/robot/sprites/civilian.dm
@@ -121,7 +121,7 @@
/datum/robot_sprite/service/drone
name = "AG Model-Serv"
- sprite_icon_state = "drone-crisis"
+ sprite_icon_state = "drone"
has_custom_open_sprites = TRUE
/datum/robot_sprite/service/drone_hydro
diff --git a/code/modules/mob/living/simple_mob/subtypes/glamour/homunculus.dm b/code/modules/mob/living/simple_mob/subtypes/glamour/homunculus.dm
index 3a65edf7b6f..febdbe3dd61 100644
--- a/code/modules/mob/living/simple_mob/subtypes/glamour/homunculus.dm
+++ b/code/modules/mob/living/simple_mob/subtypes/glamour/homunculus.dm
@@ -33,3 +33,92 @@
/mob/living/simple_mob/homunculus/update_icons()
return
+/mob/living/simple_mob/homunculus/evil
+ var/seen_melee
+ var/seen_gun
+ var/transformed
+ var/seen_armour
+ maxHealth = 100
+ health = 100
+
+ faction = FACTION_GLAMOUR
+
+ melee_damage_lower = 5
+ melee_damage_upper = 7
+
+ ai_holder_type = /datum/ai_holder/simple_mob/hostile
+
+/mob/living/simple_mob/homunculus/evil/Life()
+ handle_homunculus()
+ return ..()
+
+/mob/living/simple_mob/homunculus/evil/proc/handle_homunculus()
+ if(!transformed)
+ for(var/mob/living/carbon/human/H in range(8, src))
+ if(H.stat == DEAD)
+ continue
+ name = H.name
+ desc = H.desc
+ icon = H.icon
+ icon_state = H.icon_state
+ copy_overlays(H, TRUE)
+ resize(H.size_multiplier, ignore_prefs = TRUE)
+ var/obj/item/A = H.get_active_hand()
+ var/obj/item/I = H.get_inactive_hand()
+ if(istype(A,/obj/item/weapon/material) || istype(A,/obj/item/weapon/melee) || istype(I,/obj/item/weapon/material) || istype(I,/obj/item/weapon/melee))
+ seen_melee = 1
+ melee_damage_lower = 20
+ melee_damage_upper = 30
+ if(istype(A,/obj/item/weapon/gun/projectile) || istype(I,/obj/item/weapon/gun/projectile))
+ seen_gun = 1
+ projectiletype = /obj/item/projectile/bullet/pistol/medium
+ projectilesound = 'sound/weapons/Gunshot_light.ogg'
+ if(istype(A,/obj/item/weapon/gun/projectile/shotgun) || istype(I,/obj/item/weapon/gun/projectile/shotgun))
+ projectiletype = /obj/item/projectile/bullet/pellet/shotgun
+ projectilesound = 'sound/weapons/Gunshot_shotgun.ogg'
+ reload_time = 1.5 SECONDS
+ ranged_attack_delay = 1.5 SECONDS
+ projectile_dispersion = 8
+ projectile_accuracy = -40
+ if(istype(A,/obj/item/weapon/gun/projectile/automatic) || istype(I,/obj/item/weapon/gun/projectile/automatic))
+ base_attack_cooldown = 5
+ projectile_dispersion = 7
+ projectile_accuracy = -20
+ if(istype(A,/obj/item/weapon/gun/projectile/heavysniper) || istype(I,/obj/item/weapon/gun/projectile/heavysniper))
+ projectiletype = /obj/item/projectile/bullet/rifle/a145/highvel // Do not get seen with a big ass sniper!
+ projectilesound = 'sound/weapons/Gunshot_cannon.ogg'
+ ranged_attack_delay = 2.5 SECONDS
+ reload_time = 5 SECONDS
+ projectile_accuracy = 75
+ if(istype(A,/obj/item/weapon/gun/energy) || istype(I,/obj/item/weapon/gun/energy))
+ seen_gun = 1
+ projectiletype = /obj/item/projectile/beam/midlaser
+ projectilesound = 'sound/weapons/Laser.ogg'
+ projectile_dispersion = 5
+ projectile_accuracy = -20
+ if(istype(A,/obj/item/weapon/gun/energy/ionrifle) || istype(I,/obj/item/weapon/gun/energy/ionrifle))
+ projectiletype = /obj/item/projectile/ion
+ if(istype(A,/obj/item/weapon/gun/energy/lasercannon) || istype(I,/obj/item/weapon/gun/energy/lasercannon))
+ projectiletype = /obj/item/projectile/beam/heavylaser
+ ranged_attack_delay = 2.5 SECONDS
+ reload_time = 5 SECONDS
+ projectilesound = 'sound/weapons/lasercannonfire.ogg'
+ if(istype(A,/obj/item/weapon/gun/energy/sniperrifle) || istype(I,/obj/item/weapon/gun/energy/sniperrifle))
+ projectiletype = /obj/item/projectile/beam/sniper // Do not get seen with a big ass sniper!
+ projectilesound = 'sound/weapons/gauss_shoot.ogg'
+ ranged_attack_delay = 2.5 SECONDS
+ reload_time = 5 SECONDS
+ projectile_accuracy = 75
+ if(istype(A,/obj/item/weapon/gun/magnetic) || istype(I,/obj/item/weapon/gun/magnetic))
+ seen_gun = 1
+ projectiletype = /obj/item/projectile/bullet/magnetic/bore
+ projectilesound = 'sound/weapons/railgun.ogg'
+ ranged_attack_delay = 1.5 SECONDS
+ projectile_dispersion = 5
+ projectile_accuracy = 20
+ var/obj/item/clothing/suit/S = H.get_equipped_item(slot_wear_suit)
+ if(istype(S,/obj/item/clothing/suit/armor) || istype(S,/obj/item/clothing/suit/space/rig/))
+ armor = list(melee = 40, bullet = 30, laser = 30, energy = 10, bomb = 10, bio = 100, rad = 100)
+ transformed = 1
+ return TRUE
+
diff --git a/code/modules/mob/living/simple_mob/subtypes/vore/corrupt_hounds.dm b/code/modules/mob/living/simple_mob/subtypes/vore/corrupt_hounds.dm
index b9f58b315be..cd01e044cf6 100644
--- a/code/modules/mob/living/simple_mob/subtypes/vore/corrupt_hounds.dm
+++ b/code/modules/mob/living/simple_mob/subtypes/vore/corrupt_hounds.dm
@@ -184,3 +184,48 @@
/datum/ai_holder/simple_mob/melee/evasive/corrupthound
violent_breakthrough = TRUE
can_breakthrough = TRUE
+
+/mob/living/simple_mob/vore/aggressive/corrupthound/fire
+ icon_state = "badboi_fire"
+ icon_living = "badboi_fire"
+ icon_dead = "badboi_fire-dead"
+ icon_rest = "badboi_fire_rest"
+
+ maxHealth = 150
+ health = 150
+
+ ai_holder_type = /datum/ai_holder/simple_mob/ranged/aggressive
+
+ projectiletype = /obj/item/projectile/scatter/flamethrower
+ ranged_attack_delay = 2 SECONDS
+
+/mob/living/simple_mob/vore/aggressive/corrupthound/laser
+ icon_state = "badboi_laser"
+ icon_living = "badboi_laser"
+ icon_dead = "badboi_laser-dead"
+ icon_rest = "badboi_laser_rest"
+
+ maxHealth = 150
+ health = 150
+
+ ai_holder_type = /datum/ai_holder/simple_mob/ranged
+
+ ranged_attack_delay = 2 SECONDS
+ projectiletype = /obj/item/projectile/beam/midlaser
+ projectilesound = 'sound/weapons/Laser.ogg'
+ projectile_dispersion = 5
+ projectile_accuracy = -20
+
+/mob/living/simple_mob/vore/aggressive/corrupthound/fast
+ icon_state = "badboi_fast"
+ icon_living = "badboi_fast"
+ icon_dead = "badboi_fast-dead"
+ icon_rest = "badboi_fast_rest"
+
+ maxHealth = 100
+ health = 100
+
+ movement_cooldown = -1
+
+ melee_damage_lower = 7
+ melee_damage_upper = 15
diff --git a/code/modules/mob/living/simple_mob/subtypes/vore/meowl.dm b/code/modules/mob/living/simple_mob/subtypes/vore/meowl.dm
index 62ab96e07ce..f7ec3a56686 100644
--- a/code/modules/mob/living/simple_mob/subtypes/vore/meowl.dm
+++ b/code/modules/mob/living/simple_mob/subtypes/vore/meowl.dm
@@ -137,6 +137,8 @@
/datum/ai_holder/simple_mob/vore/meowl
var/last_friend_time = 0
+ violent_breakthrough = FALSE
+ destructive = FALSE
/datum/ai_holder/simple_mob/vore/meowl/engage_target()
ai_log("engage_target() : Entering.", AI_LOG_DEBUG)
diff --git a/code/modules/vore/eating/silicon_vr.dm b/code/modules/vore/eating/silicon_vr.dm
index 2334e186e87..d6622039ede 100644
--- a/code/modules/vore/eating/silicon_vr.dm
+++ b/code/modules/vore/eating/silicon_vr.dm
@@ -100,3 +100,16 @@
if(master.ooc_notes)
. += "OOC Notes: \[View\] - \[Print\]"
+
+// Allow dissipating ai holograms by attacking them
+/obj/effect/overlay/aiholo/attack_hand(mob/living/user)
+ if(user.a_intent == I_HURT)
+ to_chat(user, span_attack("You dissipate [src]."))
+ master?.holo?.clear_holo(master)
+ return ..()
+
+/obj/effect/overlay/aiholo/attackby(obj/item/I, mob/user)
+ if(user.a_intent == I_HURT)
+ to_chat(user, span_attack("You dissipate [src] with [I]."))
+ master?.holo?.clear_holo(master)
+ return ..()
diff --git a/icons/mob/human.dmi b/icons/mob/human.dmi
index c98035743d5..1fc987773e6 100644
Binary files a/icons/mob/human.dmi and b/icons/mob/human.dmi differ
diff --git a/icons/mob/vore64x32.dmi b/icons/mob/vore64x32.dmi
index a530a26d00f..072c39795ab 100644
Binary files a/icons/mob/vore64x32.dmi and b/icons/mob/vore64x32.dmi differ
diff --git a/maps/southern_cross/loadout/loadout_accessories.dm b/maps/southern_cross/loadout/loadout_accessories.dm
index 4604d6354dd..9011a5c8493 100644
--- a/maps/southern_cross/loadout/loadout_accessories.dm
+++ b/maps/southern_cross/loadout/loadout_accessories.dm
@@ -1,34 +1,34 @@
/datum/gear/accessory/brown_vest
display_name = "webbing, brown"
path = /obj/item/clothing/accessory/storage/brown_vest
- allowed_roles = list(JOB_ENGINEER,JOB_ATMOSPHERIC_TECHNICIAN,JOB_CHIEF_ENGINEER,JOB_SECURITY_OFFICER,JOB_DETECTIVE,JOB_HEAD_OF_SECURITY,JOB_WARDEN,JOB_PARAMEDIC,JOB_CHIEF_MEDICAL_OFFICER,JOB_MEDICAL_DOCTOR,"Search and Rescue","Explorer",JOB_SHAFT_MINER)
+ allowed_roles = list(JOB_ENGINEER,JOB_ATMOSPHERIC_TECHNICIAN,JOB_CHIEF_ENGINEER,JOB_SECURITY_OFFICER,JOB_DETECTIVE,JOB_HEAD_OF_SECURITY,JOB_WARDEN,JOB_ALT_SEARCH_AND_RESCUE,JOB_CHIEF_MEDICAL_OFFICER,JOB_MEDICAL_DOCTOR,JOB_PARAMEDIC,JOB_EXPLORER,JOB_SHAFT_MINER)
/datum/gear/accessory/black_vest
display_name = "webbing, black"
path = /obj/item/clothing/accessory/storage/black_vest
- allowed_roles = list(JOB_ENGINEER,JOB_ATMOSPHERIC_TECHNICIAN,JOB_CHIEF_ENGINEER,JOB_SECURITY_OFFICER,JOB_DETECTIVE,JOB_HEAD_OF_SECURITY,JOB_WARDEN,JOB_PARAMEDIC,JOB_CHIEF_MEDICAL_OFFICER,JOB_MEDICAL_DOCTOR,"Search and Rescue","Explorer",JOB_SHAFT_MINER)
+ allowed_roles = list(JOB_ENGINEER,JOB_ATMOSPHERIC_TECHNICIAN,JOB_CHIEF_ENGINEER,JOB_SECURITY_OFFICER,JOB_DETECTIVE,JOB_HEAD_OF_SECURITY,JOB_WARDEN,JOB_ALT_SEARCH_AND_RESCUE,JOB_CHIEF_MEDICAL_OFFICER,JOB_MEDICAL_DOCTOR,JOB_PARAMEDIC,JOB_EXPLORER,JOB_SHAFT_MINER)
/datum/gear/accessory/white_vest
display_name = "webbing, white"
path = /obj/item/clothing/accessory/storage/white_vest
- allowed_roles = list(JOB_ENGINEER,JOB_ATMOSPHERIC_TECHNICIAN,JOB_CHIEF_ENGINEER,JOB_SECURITY_OFFICER,JOB_DETECTIVE,JOB_HEAD_OF_SECURITY,JOB_WARDEN,JOB_PARAMEDIC,JOB_CHIEF_MEDICAL_OFFICER,JOB_MEDICAL_DOCTOR,"Search and Rescue","Explorer",JOB_SHAFT_MINER)
+ allowed_roles = list(JOB_ENGINEER,JOB_ATMOSPHERIC_TECHNICIAN,JOB_CHIEF_ENGINEER,JOB_SECURITY_OFFICER,JOB_DETECTIVE,JOB_HEAD_OF_SECURITY,JOB_WARDEN,JOB_ALT_SEARCH_AND_RESCUE,JOB_CHIEF_MEDICAL_OFFICER,JOB_MEDICAL_DOCTOR,JOB_PARAMEDIC,JOB_EXPLORER,JOB_SHAFT_MINER)
/datum/gear/accessory/brown_drop_pouches
display_name = "drop pouches, brown"
path = /obj/item/clothing/accessory/storage/brown_drop_pouches
- allowed_roles = list(JOB_ENGINEER,JOB_ATMOSPHERIC_TECHNICIAN,JOB_CHIEF_ENGINEER,JOB_SECURITY_OFFICER,JOB_DETECTIVE,JOB_HEAD_OF_SECURITY,JOB_WARDEN,JOB_PARAMEDIC,JOB_CHIEF_MEDICAL_OFFICER,JOB_MEDICAL_DOCTOR,"Search and Rescue","Explorer",JOB_SHAFT_MINER)
+ allowed_roles = list(JOB_ENGINEER,JOB_ATMOSPHERIC_TECHNICIAN,JOB_CHIEF_ENGINEER,JOB_SECURITY_OFFICER,JOB_DETECTIVE,JOB_HEAD_OF_SECURITY,JOB_WARDEN,JOB_ALT_SEARCH_AND_RESCUE,JOB_CHIEF_MEDICAL_OFFICER,JOB_MEDICAL_DOCTOR,JOB_PARAMEDIC,JOB_EXPLORER,JOB_SHAFT_MINER)
/datum/gear/accessory/black_drop_pouches
display_name = "drop pouches, black"
path = /obj/item/clothing/accessory/storage/black_drop_pouches
- allowed_roles = list(JOB_ENGINEER,JOB_ATMOSPHERIC_TECHNICIAN,JOB_CHIEF_ENGINEER,JOB_SECURITY_OFFICER,JOB_DETECTIVE,JOB_HEAD_OF_SECURITY,JOB_WARDEN,JOB_PARAMEDIC,JOB_CHIEF_MEDICAL_OFFICER,JOB_MEDICAL_DOCTOR,"Search and Rescue","Explorer",JOB_SHAFT_MINER)
+ allowed_roles = list(JOB_ENGINEER,JOB_ATMOSPHERIC_TECHNICIAN,JOB_CHIEF_ENGINEER,JOB_SECURITY_OFFICER,JOB_DETECTIVE,JOB_HEAD_OF_SECURITY,JOB_WARDEN,JOB_ALT_SEARCH_AND_RESCUE,JOB_CHIEF_MEDICAL_OFFICER,JOB_MEDICAL_DOCTOR,JOB_PARAMEDIC,JOB_EXPLORER,JOB_SHAFT_MINER)
/datum/gear/accessory/white_drop_pouches
display_name = "drop pouches, white"
path = /obj/item/clothing/accessory/storage/white_drop_pouches
- allowed_roles = list(JOB_ENGINEER,JOB_ATMOSPHERIC_TECHNICIAN,JOB_CHIEF_ENGINEER,JOB_SECURITY_OFFICER,JOB_DETECTIVE,JOB_HEAD_OF_SECURITY,JOB_WARDEN,JOB_PARAMEDIC,JOB_CHIEF_MEDICAL_OFFICER,JOB_MEDICAL_DOCTOR,"Search and Rescue","Explorer",JOB_SHAFT_MINER)
+ allowed_roles = list(JOB_ENGINEER,JOB_ATMOSPHERIC_TECHNICIAN,JOB_CHIEF_ENGINEER,JOB_SECURITY_OFFICER,JOB_DETECTIVE,JOB_HEAD_OF_SECURITY,JOB_WARDEN,JOB_ALT_SEARCH_AND_RESCUE,JOB_CHIEF_MEDICAL_OFFICER,JOB_MEDICAL_DOCTOR,JOB_PARAMEDIC,JOB_EXPLORER,JOB_SHAFT_MINER)
/datum/gear/accessory/holster
display_name = "holster selection (Security, CD, HoP)"
path = /obj/item/clothing/accessory/holster
- allowed_roles = list(JOB_SITE_MANAGER,JOB_HEAD_OF_PERSONNEL,JOB_SECURITY_OFFICER,JOB_WARDEN,JOB_HEAD_OF_SECURITY,JOB_DETECTIVE,"Explorer")
+ allowed_roles = list(JOB_SITE_MANAGER,JOB_HEAD_OF_PERSONNEL,JOB_SECURITY_OFFICER,JOB_WARDEN,JOB_HEAD_OF_SECURITY,JOB_DETECTIVE,JOB_PATHFINDER,JOB_EXPLORER)
diff --git a/maps/southern_cross/loadout/loadout_head.dm b/maps/southern_cross/loadout/loadout_head.dm
index bf35942c106..137f9ef8b42 100644
--- a/maps/southern_cross/loadout/loadout_head.dm
+++ b/maps/southern_cross/loadout/loadout_head.dm
@@ -1,4 +1,4 @@
/datum/gear/head/pilot
display_name = "helmet, pilot (Pilot)"
path = /obj/item/clothing/head/ompilot/alt //VOREStation Edit
- allowed_roles = list("Pilot")
\ No newline at end of file
+ allowed_roles = list(JOB_PILOT)
diff --git a/tgui/packages/tgui-panel/package.json b/tgui/packages/tgui-panel/package.json
index d435245e3fe..8a3c4b562c7 100644
--- a/tgui/packages/tgui-panel/package.json
+++ b/tgui/packages/tgui-panel/package.json
@@ -6,7 +6,7 @@
"@types/node": "^20.12.3",
"@types/react": "^18.2.74",
"common": "workspace:*",
- "dompurify": "^2.4.9",
+ "dompurify": "^2.5.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tgui": "workspace:*",
diff --git a/tgui/packages/tgui/interfaces/Canvas.tsx b/tgui/packages/tgui/interfaces/Canvas.tsx
index d54eb7e66cd..ad49add4333 100644
--- a/tgui/packages/tgui/interfaces/Canvas.tsx
+++ b/tgui/packages/tgui/interfaces/Canvas.tsx
@@ -1,8 +1,9 @@
+import { clamp } from 'common/math';
import { BooleanLike } from 'common/react';
-import { Component, createRef, RefObject } from 'react';
+import { Component, createRef, RefObject, useState } from 'react';
import { useBackend } from '../backend';
-import { Box, Button } from '../components';
+import { Box, Button, Stack } from '../components';
import { Window } from '../layouts';
const PX_PER_UNIT = 24;
@@ -12,11 +13,16 @@ type PaintCanvasProps = Partial<{
value: string[][];
dotsize: number;
res: number;
+ tool: Tool;
}>;
class PaintCanvas extends Component {
canvasRef: RefObject;
+ mouseIsDown: boolean;
+ lastSuccessfulPaint: number;
onCVClick: (x: number, y: number) => void;
+ lineStart: [number, number] | null;
+ lastHovered: [number, number] | null;
constructor(props) {
super(props);
@@ -32,10 +38,128 @@ class PaintCanvas extends Component {
this.drawCanvas(this.props);
}
+ getLinePixels() {
+ if (!this.lineStart || !this.lastHovered) {
+ return;
+ }
+
+ let [x0, y0] = this.lineStart;
+ let [x1, y1] = this.lastHovered;
+
+ x0 = x0 - 1;
+ x1 = x1 - 1;
+ y0 = y0 - 1;
+ y1 = y1 - 1;
+
+ let dx = Math.abs(x1 - x0);
+ let sx = x0 < x1 ? 1 : -1;
+ let dy = -Math.abs(y1 - y0);
+ let sy = y0 < y1 ? 1 : -1;
+ let error = dx + dy;
+
+ let pixels: [number, number][] = [];
+
+ while (true) {
+ pixels.push([x0, y0]);
+ // grid[x0][y0] = '#000';
+
+ if (x0 === x1 && y0 === y1) {
+ break;
+ }
+
+ let e2 = error * 2;
+ if (e2 >= dy) {
+ error = error + dy;
+ x0 = x0 + sx;
+ }
+ if (e2 <= dx) {
+ error = error + dx;
+ y0 = y0 + sy;
+ }
+ }
+
+ return pixels;
+ }
+
+ drawLine(grid: string[][]) {
+ let pixels = this.getLinePixels();
+ if (!pixels) {
+ return;
+ }
+
+ for (let [x, y] of pixels) {
+ if (x < grid.length && y < grid[0].length) {
+ grid[x][y] = '#000000';
+ }
+ }
+ }
+
+ areaFill(grid: string[][]) {
+ if (!this.lastHovered) {
+ return;
+ }
+
+ let [x_start, y_start] = this.lastHovered;
+ x_start = clamp(x_start - 1, 0, grid.length - 1);
+ y_start = clamp(y_start - 1, 0, grid[0].length - 1);
+
+ const origPixelColor = grid[x_start][y_start];
+ const inside = (x_c, y_c) => {
+ if (x_c < 0 || y_c < 0 || x_c >= grid.length || y_c >= grid[0].length) {
+ return false;
+ }
+ return grid[x_c][y_c] === origPixelColor;
+ };
+
+ let s: [number, number, number, number][] = [];
+ s.push([x_start, x_start, y_start, 1]);
+ s.push([x_start, x_start, y_start - 1, -1]);
+
+ let pixels_touched: [number, number][] = [];
+
+ while (s.length) {
+ // This can't fail because of our while condition
+ let [x1, x2, y, dy] = s.pop()!;
+
+ let x = x1;
+ if (inside(x, y)) {
+ while (inside(x - 1, y)) {
+ grid[x - 1][y] = '#000000';
+ pixels_touched.push([x - 1, y]);
+ x = x - 1;
+ }
+ if (x < x1) {
+ s.push([x, x1 - 1, y - dy, -dy]);
+ }
+ }
+
+ while (x1 <= x2) {
+ while (inside(x1, y)) {
+ grid[x1][y] = '#000000';
+ pixels_touched.push([x1, y]);
+ x1 = x1 + 1;
+ }
+ if (x1 > x) {
+ s.push([x, x1 - 1, y + dy, dy]);
+ }
+ if (x1 - 1 > x2) {
+ s.push([x2 + 1, x1 - 1, y - dy, -dy]);
+ }
+ x1 = x1 + 1;
+ while (x1 < x2 && !inside(x1, y)) {
+ x1 = x1 + 1;
+ }
+ x = x1;
+ }
+ }
+
+ return pixels_touched;
+ }
+
drawCanvas(propSource: PaintCanvasProps) {
const canvas = this.canvasRef.current!;
const ctx = canvas.getContext('2d')!;
- const grid = propSource.value;
+ const grid = JSON.parse(JSON.stringify(propSource.value));
if (!grid) {
return;
}
@@ -48,6 +172,13 @@ class PaintCanvas extends Component {
const y_scale = Math.round(canvas.height / y_size);
ctx.save();
ctx.scale(x_scale, y_scale);
+
+ if (this.mouseIsDown && propSource.tool === Tool.Line) {
+ this.drawLine(grid);
+ } else if (propSource.tool === Tool.Fill) {
+ this.areaFill(grid);
+ }
+
for (let x = 0; x < grid.length; x++) {
const element = grid[x];
for (let y = 0; y < element.length; y++) {
@@ -59,22 +190,96 @@ class PaintCanvas extends Component {
ctx.restore();
}
- clickwrapper(event: React.MouseEvent) {
+ getCoord(
+ event: React.MouseEvent,
+ ): [number, number] | null {
const value = this.props.value;
if (!value) {
- return;
+ return null;
}
const x_size = value.length;
if (!x_size) {
- return;
+ return null;
}
+
const y_size = this.props.value[0].length;
const canvas = this.canvasRef.current!;
const x_scale = canvas.width / x_size;
const y_scale = canvas.height / y_size;
const x = Math.floor(event.nativeEvent.offsetX / x_scale) + 1;
const y = Math.floor(event.nativeEvent.offsetY / y_scale) + 1;
- this.onCVClick(x, y);
+
+ return [x, y];
+ }
+
+ mouseDown(event: React.MouseEvent) {
+ this.mouseIsDown = true;
+
+ let coord = this.getCoord(event);
+ if (!coord) {
+ return;
+ }
+
+ this.lineStart = coord;
+ this.lastHovered = coord;
+
+ if (this.props.tool === Tool.Paintbrush) {
+ this.onCVClick(coord[0], coord[1]);
+ } else if (this.props.tool === Tool.Fill) {
+ let gridCopy = JSON.parse(JSON.stringify(this.props.value));
+ let pixels_touched = this.areaFill(gridCopy);
+ if (pixels_touched) {
+ for (let [x, y] of pixels_touched) {
+ this.onCVClick(x + 1, y + 1);
+ }
+ }
+ }
+ }
+
+ mouseUp(event: React.MouseEvent) {
+ this.mouseIsDown = false;
+
+ let coord = this.getCoord(event);
+ if (!coord) {
+ return;
+ }
+
+ this.lastHovered = coord;
+
+ if (this.props.tool === Tool.Line) {
+ let line = this.getLinePixels();
+ if (line) {
+ for (let [x, y] of line) {
+ this.onCVClick(x + 1, y + 1);
+ }
+ }
+ }
+ }
+
+ mouseMove(event: React.MouseEvent) {
+ const coord = this.getCoord(event);
+ if (!coord) {
+ return;
+ }
+
+ this.lastHovered = coord;
+
+ let time = new Date().getTime();
+
+ if (this.lastSuccessfulPaint + 50 > time) {
+ return;
+ }
+
+ if (this.mouseIsDown && this.props.tool === Tool.Paintbrush) {
+ this.onCVClick(coord[0], coord[1]);
+ this.lastSuccessfulPaint = time;
+ } else if (this.mouseIsDown && this.props.tool === Tool.Line) {
+ // Force redraw
+ this.drawCanvas(this.props);
+ this.lastSuccessfulPaint = time;
+ } else if (this.props.tool === Tool.Fill) {
+ this.drawCanvas(this.props);
+ }
}
render() {
@@ -86,7 +291,10 @@ class PaintCanvas extends Component {
width={width * dotsize || 300}
height={height * dotsize || 300}
{...rest}
- onClick={(e) => this.clickwrapper(e)}
+ // onClick={(e) => this.clickwrapper(e)}
+ onMouseDown={(e) => this.mouseDown(e)}
+ onMouseMove={(e) => this.mouseMove(e)}
+ onMouseUp={(e) => this.mouseUp(e)}
>
Canvas failed to render.
@@ -106,9 +314,17 @@ type Data = {
finalized: BooleanLike;
};
+enum Tool {
+ Paintbrush,
+ Line,
+ Fill,
+}
+
export const Canvas = (props) => {
const { act, data } = useBackend();
+ const [tool, setTool] = useState(Tool.Paintbrush);
+
const dotsize = PX_PER_UNIT;
const [width, height] = getImageSize(data.grid);
return (
@@ -117,20 +333,56 @@ export const Canvas = (props) => {
height={Math.min(700, height * dotsize + 72)}
>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ act('paint', { x, y })}
+ />
+
+
- act('paint', { x, y })}
- />
-
- {!data.finalized && (
- act('finalize')}>
- Finalize
-
- )}
- {data.name}
-
+ {!data.finalized && (
+ act('finalize')}>
+ Finalize
+
+ )}
+ {data.name}
diff --git a/tgui/packages/tgui/interfaces/LawManager.tsx b/tgui/packages/tgui/interfaces/LawManager.tsx
index 3632166b60f..305e6a81a4e 100644
--- a/tgui/packages/tgui/interfaces/LawManager.tsx
+++ b/tgui/packages/tgui/interfaces/LawManager.tsx
@@ -1,4 +1,7 @@
+import { filter } from 'common/collections';
+import { flow } from 'common/fp';
import { BooleanLike } from 'common/react';
+import { createSearch } from 'common/string';
import { useBackend, useSharedState } from '../backend';
import {
@@ -76,30 +79,12 @@ export const LawManager = (props) => {
};
const LawManagerContent = (props) => {
+ const { data } = useBackend();
const [tabIndex, setTabIndex] = useSharedState('lawsTabIndex', 0);
-
- const tab: React.JSX.Element[] = [];
-
- tab[0] = ;
- tab[1] = ;
-
- return (
- <>
-
- setTabIndex(0)}>
- Law Management
-
- setTabIndex(1)}>
- Law Sets
-
-
- {tab[tabIndex]}
- >
+ const [searchLawName, setSearchLawName] = useSharedState(
+ 'searchLawName',
+ '',
);
-};
-
-const LawManagerLaws = (props) => {
- const { act, data } = useBackend();
const {
ion_law_nr,
@@ -121,9 +106,109 @@ const LawManagerLaws = (props) => {
isAdmin,
channel,
channels,
+ law_sets,
} = data;
- let allLaws = zeroth_laws
+ const tab: React.JSX.Element[] = [];
+
+ tab[0] = (
+
+ );
+ tab[1] = (
+
+ );
+
+ return (
+ <>
+
+ setTabIndex(0)}>
+ Law Management
+
+ setTabIndex(1)}>
+ Law Sets
+
+
+ {tab[tabIndex]}
+ >
+ );
+};
+
+export const LawManagerLaws = (props: {
+ ion_law_nr: string;
+ ion_law: string;
+ zeroth_law: string;
+ inherent_law: string;
+ supplied_law: string;
+ supplied_law_position: number;
+ zeroth_laws: law[];
+ ion_laws: law[];
+ inherent_laws: law[];
+ supplied_laws: law[];
+ has_zeroth_laws: number;
+ has_ion_laws: number;
+ has_inherent_laws: number;
+ has_supplied_laws: number;
+ isAI: BooleanLike;
+ isMalf: BooleanLike;
+ isAdmin: BooleanLike;
+ channel: string;
+ channels: { channel: string }[];
+ hasScroll?: boolean;
+ sectionHeight?: string;
+}) => {
+ const { act } = useBackend();
+ const {
+ ion_law_nr,
+ ion_law,
+ zeroth_law,
+ inherent_law,
+ supplied_law,
+ supplied_law_position,
+ zeroth_laws,
+ has_zeroth_laws,
+ ion_laws,
+ has_ion_laws,
+ inherent_laws,
+ has_inherent_laws,
+ supplied_laws,
+ has_supplied_laws,
+ isAI,
+ isMalf,
+ isAdmin,
+ channel,
+ channels,
+ hasScroll,
+ sectionHeight,
+ } = props;
+
+ const allLaws = zeroth_laws
.map((law) => {
law.zero = true;
return law;
@@ -131,19 +216,37 @@ const LawManagerLaws = (props) => {
.concat(inherent_laws);
return (
-
+
{has_ion_laws ? (
-
+
) : (
''
)}
{has_zeroth_laws || has_inherent_laws ? (
-
+
) : (
''
)}
{has_supplied_laws ? (
-
+
) : (
''
)}
@@ -280,15 +383,16 @@ const LawManagerLaws = (props) => {
};
const LawsTable = (props: {
+ laws: law[];
title: string;
noButtons?: BooleanLike;
[rest: string]: any;
+ isMalf: BooleanLike;
+ isAdmin: BooleanLike;
}) => {
- const { act, data } = useBackend();
+ const { act } = useBackend();
- const { isMalf, isAdmin } = data;
-
- const { laws, title, noButtons, ...rest } = props;
+ const { laws, title, noButtons, isMalf, isAdmin, ...rest } = props;
return (
@@ -360,10 +464,24 @@ const LawsTable = (props: {
);
};
-const LawManagerLawSets = (props) => {
- const { act, data } = useBackend();
+export const LawManagerLawSets = (props: {
+ law_sets: law_pack[];
+ ion_law_nr: string;
+ searchLawName: string;
+ onSearchLawName: Function;
+ isAdmin: BooleanLike;
+ isMalf: BooleanLike;
+}) => {
+ const { act } = useBackend();
- const { isMalf, law_sets, ion_law_nr } = data;
+ const {
+ law_sets,
+ ion_law_nr,
+ searchLawName,
+ onSearchLawName,
+ isMalf,
+ isAdmin,
+ } = props;
return (
<>
@@ -371,8 +489,14 @@ const LawManagerLawSets = (props) => {
Remember: Stating laws other than those currently loaded may be grounds
for decommissioning! - NanoTrasen
+ onSearchLawName(value)}
+ />
{law_sets.length
- ? law_sets.map((laws) => (
+ ? prepareSearch(law_sets, searchLawName).map((laws) => (
{
noButtons
laws={laws.laws.ion_laws}
title={ion_law_nr + ' Laws:'}
+ isAdmin={isAdmin}
+ isMalf={isMalf}
/>
) : (
''
@@ -412,6 +538,8 @@ const LawManagerLawSets = (props) => {
noButtons
laws={laws.laws.zeroth_laws.concat(laws.laws.inherent_laws)}
title={laws.header}
+ isAdmin={isAdmin}
+ isMalf={isMalf}
/>
) : (
''
@@ -421,6 +549,8 @@ const LawManagerLawSets = (props) => {
noButtons
laws={laws.laws.supplied_laws}
title="Supplied Laws"
+ isAdmin={isAdmin}
+ isMalf={isMalf}
/>
) : (
''
@@ -431,3 +561,23 @@ const LawManagerLawSets = (props) => {
>
);
};
+
+const prepareSearch = (
+ laws: law_pack[],
+ searchText: string = '',
+): law_pack[] => {
+ const testSearch = createSearch(
+ searchText,
+ (law: law_pack) => law.name + law.header,
+ );
+ return flow([
+ (laws: law_pack[]) => {
+ // Optional search term
+ if (!searchText) {
+ return laws;
+ } else {
+ return filter(laws, testSearch);
+ }
+ },
+ ])(laws);
+};
diff --git a/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotNoModule.tsx b/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotNoModule.tsx
new file mode 100644
index 00000000000..364bd2ff2d6
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotNoModule.tsx
@@ -0,0 +1,109 @@
+import { useBackend } from 'tgui/backend';
+import {
+ Button,
+ Divider,
+ Flex,
+ Icon,
+ NoticeBox,
+ Section,
+ Stack,
+} from 'tgui/components';
+
+import { RankIcon } from '../common/RankIcon';
+import { Target } from './types';
+
+export const ModifyRobotNoModule = (props: { target: Target }) => {
+ const { target } = props;
+ const { act } = useBackend();
+
+ return (
+ <>
+
+ Target has no active module. Limited options available.
+
+
+
+
+
+
+
+
+
+
+ {target.active_restrictions.map((active_restriction, i) => {
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ {target.possible_restrictions.map((possible_restriction, i) => {
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotAccess.tsx b/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotAccess.tsx
new file mode 100644
index 00000000000..a5780f93e3e
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotAccess.tsx
@@ -0,0 +1,180 @@
+import { capitalize } from 'common/string';
+import { useState } from 'react';
+import { useBackend } from 'tgui/backend';
+import {
+ Button,
+ Divider,
+ Flex,
+ Icon,
+ Image,
+ Input,
+ Section,
+ Stack,
+} from 'tgui/components';
+
+import { NoSpriteWarning } from '../components';
+import { prepareSearch } from '../functions';
+import { Access, Target } from '../types';
+
+export const ModifyRobotAccess = (props: {
+ target: Target;
+ tab_icon: string;
+ all_access: Access[];
+}) => {
+ const { act } = useBackend();
+ const { target, tab_icon, all_access } = props;
+ const [searchAccessAll, setSearchAccessAll] = useState('');
+ const [searchAccessActive, setSearchAccessActive] = useState('');
+
+ return (
+ <>
+ {!target.active && }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+const AccessSection = (props: {
+ title: string;
+ searchText: string;
+ onSearchText: Function;
+ access: Access[];
+ action: string;
+ buttonColor: string;
+ buttonIcon: string;
+}) => {
+ const { act } = useBackend();
+ const {
+ title,
+ searchText,
+ onSearchText,
+ access,
+ action,
+ buttonColor,
+ buttonIcon,
+ } = props;
+ return (
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotComponent.tsx b/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotComponent.tsx
new file mode 100644
index 00000000000..4450c6009cd
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotComponent.tsx
@@ -0,0 +1,301 @@
+import { capitalize } from 'common/string';
+import { useState } from 'react';
+import { useBackend } from 'tgui/backend';
+import {
+ Box,
+ Button,
+ Divider,
+ Dropdown,
+ Flex,
+ Icon,
+ Image,
+ Input,
+ Section,
+ Stack,
+} from 'tgui/components';
+
+import { NoSpriteWarning } from '../components';
+import { prepareSearch } from '../functions';
+import { Cell, Component, Target } from '../types';
+
+export const ModifyRobotComponent = (props: {
+ target: Target;
+ cell: string | null;
+ cells: Record;
+}) => {
+ const { target, cell, cells } = props;
+ const [searchComponentReplaceText, setSearchComponentReplaceText] =
+ useState('');
+ const [searchComponentRemoveText, setSearchComponentRemoveText] =
+ useState('');
+ const [selectedCell, setSelectedCell] = useState(cell || '');
+ const cell_options = Object.keys(cells) as Array;
+
+ return (
+ <>
+ {!target.active && }
+
+
+
+
+
+
+
+
+
+
+
+ Current cell:{' '}
+ {cell ? (
+ capitalize(cell)
+ ) : (
+
+ No cell installed!
+
+ )}
+
+
+
+ Charge State: {cells[selectedCell]?.charge} /{' '}
+ {cells[selectedCell]?.max_charge}
+
+
+ Charge Rate: {cells[selectedCell]?.charge_amount}
+
+
+ Self Charge:{' '}
+ {cells[selectedCell]?.self_charge ? 'Yes' : 'No'}
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+const ComponentSection = (props: {
+ title: string;
+ searchText: string;
+ onSearchText: Function;
+ components: Component[];
+ action: string;
+ buttonColor: string;
+ buttonIcon: string;
+ celltype?: string;
+ selected_cell?: string;
+ cell?: string;
+}) => {
+ const { act } = useBackend();
+ const {
+ title,
+ searchText,
+ onSearchText,
+ components,
+ action,
+ buttonColor,
+ buttonIcon,
+ celltype,
+ selected_cell,
+ cell,
+ } = props;
+ return (
+
+ );
+};
+
+function checkDisabled(
+ component: Component,
+ action: string,
+ selected_cell: string | undefined,
+ cell: string | undefined,
+): boolean {
+ switch (action) {
+ case 'rem_component':
+ return !component.exists;
+ case 'add_component':
+ if (
+ selected_cell &&
+ cell &&
+ component.name === 'power cell' &&
+ selected_cell !== cell
+ ) {
+ return false;
+ }
+ return (
+ component.installed === 1 &&
+ !!component.exists &&
+ component.brute_damage === 0 &&
+ component.electronics_damage === 0
+ );
+ }
+ return false;
+}
+
+function getComponentTooltip(
+ component: Component,
+ action: string,
+ selected_cell: string | undefined,
+ cell: string | undefined,
+): string {
+ switch (action) {
+ case 'add_component':
+ if (component.installed === 0 || !component.exists) {
+ return 'Component missing!';
+ }
+ if (
+ component.installed === -1 ||
+ component.brute_damage + component.electronics_damage >=
+ component.max_damage
+ ) {
+ return 'Component destroyed!';
+ }
+ if (checkDisabled(component, action, selected_cell, cell)) {
+ return 'Disabled due to fully intact component!';
+ }
+ return '';
+ case 'rem_component':
+ return component.exists ? '' : 'Disabled due to missing component!';
+ }
+ return '';
+}
+
+function getComponentColor(
+ component: Component,
+ action: string,
+): string | undefined {
+ switch (action) {
+ case 'add_component':
+ if (
+ component.brute_damage + component.electronics_damage >=
+ component.max_damage ||
+ component.installed !== 1 ||
+ !component.exists
+ ) {
+ return 'black';
+ }
+ if (
+ (component.brute_damage + component.electronics_damage) /
+ component.max_damage >
+ 0.66
+ ) {
+ return 'red';
+ }
+ if (
+ (component.brute_damage + component.electronics_damage) /
+ component.max_damage >
+ 0.33
+ ) {
+ return 'orange';
+ }
+ if (
+ (component.brute_damage + component.electronics_damage) /
+ component.max_damage >
+ 0
+ ) {
+ return 'yellow';
+ }
+ return undefined;
+ case 'rem_component':
+ return undefined;
+ }
+ return '';
+}
diff --git a/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotModules.tsx b/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotModules.tsx
new file mode 100644
index 00000000000..67ed0ac3b78
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotModules.tsx
@@ -0,0 +1,201 @@
+import { capitalize } from 'common/string';
+import { useState } from 'react';
+import { useBackend } from 'tgui/backend';
+import {
+ Box,
+ Button,
+ Divider,
+ Dropdown,
+ Flex,
+ Icon,
+ Image,
+ Input,
+ Section,
+ Stack,
+} from 'tgui/components';
+
+import { NoSpriteWarning } from '../components';
+import { prepareSearch } from '../functions';
+import { Module, Source, Target } from '../types';
+
+export const ModifyRobotModules = (props: {
+ target: Target;
+ source: Source;
+ model_options: string[];
+}) => {
+ const { target, source, model_options } = props;
+ const { act } = useBackend();
+ const [searchSourceText, setSearchSourceText] = useState('');
+ const [searchModuleText, setSearchModulText] = useState('');
+
+ return (
+ <>
+ {!target.active && }
+
+
+
+ Robot to salvage
+
+ act('select_source', {
+ new_source: value,
+ })
+ }
+ />
+ {!!source && (
+
+ )}
+
+
+
+
+
+
+ act('swap_module')}
+ />
+
+
+ act('ert_toggle')}
+ />
+
+
+
+
+
+
+ {target ? target.module : ''}
+ act('reset_module')}
+ tooltip="Allows to reset the module back to default."
+ >
+ Reset Module
+
+
+
+
+
+
+ >
+ );
+};
+
+const SelectionField = (props: {
+ previewImage: string | undefined;
+ searchText: string;
+ onSearchText: Function;
+ action: string;
+ buttonIcon: string;
+ buttonColor: string;
+ modules: Module[];
+}) => {
+ const { act } = useBackend();
+ const {
+ previewImage,
+ searchText,
+ onSearchText,
+ action,
+ modules,
+ buttonIcon,
+ buttonColor,
+ } = props;
+
+ return (
+ <>
+
+
+
+ onSearchText(value)}
+ />
+
+
+
+ {prepareSearch(modules, searchText).map((modul_option, i) => {
+ return (
+
+ );
+ })}
+
+
+ >
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotPKA.tsx b/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotPKA.tsx
new file mode 100644
index 00000000000..1662c8053ca
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotPKA.tsx
@@ -0,0 +1,112 @@
+import { capitalize } from 'common/string';
+import { useState } from 'react';
+import { useBackend } from 'tgui/backend';
+import {
+ Box,
+ Button,
+ Divider,
+ Flex,
+ Image,
+ Input,
+ NoticeBox,
+} from 'tgui/components';
+
+import { NoSpriteWarning } from '../components';
+import { getModuleIcon, prepareSearch } from '../functions';
+import { Target } from '../types';
+
+export const ModifyRobotPKA = (props: { target: Target }) => {
+ const { act } = useBackend();
+ const { target } = props;
+ const [searchModkitText, setSearchModkitText] = useState('');
+ const [searchInstalledtext, setSearchInstalledtext] = useState('');
+
+ return (
+ <>
+ {!target.active && }
+ {!target.pka ? (
+ {target.name} has no PKA installed.
+ ) : (
+
+
+
+ Remaining Capacity: {target.pka.capacity}
+
+ setSearchModkitText(value)}
+ />
+
+ {prepareSearch(target.pka.modkits, searchModkitText).map(
+ (modkit, i) => {
+ return (
+
+ );
+ },
+ )}
+
+
+
+
+
+
+
+
+
+ Used Capacity: {target.pka.max_capacity - target.pka.capacity}
+
+
+ setSearchInstalledtext(value)}
+ />
+
+ {prepareSearch(
+ target.pka.installed_modkits,
+ searchInstalledtext,
+ ).map((modkit, i) => {
+ return (
+
+ );
+ })}
+
+
+ )}
+ >
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotRadio.tsx b/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotRadio.tsx
new file mode 100644
index 00000000000..5af3a8cd063
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotRadio.tsx
@@ -0,0 +1,126 @@
+import { capitalize } from 'common/string';
+import { useState } from 'react';
+import { useBackend } from 'tgui/backend';
+import {
+ Button,
+ Divider,
+ Flex,
+ Icon,
+ Image,
+ Input,
+ Section,
+ Stack,
+} from 'tgui/components';
+
+import { NoSpriteWarning } from '../components';
+import { prepareSearch } from '../functions';
+import { Target } from '../types';
+
+export const ModifyRobotRadio = (props: { target: Target }) => {
+ const { target } = props;
+ const [searchChannelAddText, setSearchChannelAddText] = useState('');
+ const [searchChannelRemoveText, setSearchChannelRemoveText] =
+ useState('');
+
+ return (
+ <>
+ {!target.active && }
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+const RadioSection = (props: {
+ title: string;
+ searchText: string;
+ onSearchText: Function;
+ channels: string[];
+ action: string;
+ buttonColor: string;
+ buttonIcon: string;
+}) => {
+ const { act } = useBackend();
+ const {
+ title,
+ searchText,
+ onSearchText,
+ channels,
+ action,
+ buttonColor,
+ buttonIcon,
+ } = props;
+ return (
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotUpgrades.tsx b/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotUpgrades.tsx
new file mode 100644
index 00000000000..d34337828a2
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ModifyRobot/ModifyRobotTabs/ModifyRobotUpgrades.tsx
@@ -0,0 +1,154 @@
+import { capitalize } from 'common/string';
+import { useState } from 'react';
+import { useBackend } from 'tgui/backend';
+import {
+ Button,
+ Divider,
+ Flex,
+ Image,
+ Input,
+ Section,
+ Stack,
+} from 'tgui/components';
+
+import { NoSpriteWarning } from '../components';
+import { install2col } from '../constants';
+import { prepareSearch } from '../functions';
+import { Target, Upgrade } from '../types';
+
+export const ModifyRobotUpgrades = (props: { target: Target }) => {
+ const { target } = props;
+ const [searchAddCompatibilityText, setSearchAddCompatibilityText] =
+ useState('');
+ const [searchRemoveCompatibilityText, setSearchRemoveCompatibilityText] =
+ useState('');
+ const [searchUtilityUpgradeText, setsearchUtilityUpgradeText] =
+ useState('');
+ const [searchBasicUpgradeText, setSearchBasicUpgradeText] =
+ useState('');
+ const [searchAdvancedUpgradeText, setSearchAdvancedUpgradeText] =
+ useState('');
+ const [searchRestrictedUpgradeText, setSearchRestrictedUpgradeText] =
+ useState('');
+
+ return (
+ <>
+ {!target.active && }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+const UpgradeSection = (props: {
+ title: string;
+ searchText: string;
+ onSearchText: Function;
+ upgrades: Upgrade[];
+ action: string;
+}) => {
+ const { act } = useBackend();
+ const { title, searchText, onSearchText, upgrades, action } = props;
+ return (
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ModifyRobot/components.tsx b/tgui/packages/tgui/interfaces/ModifyRobot/components.tsx
new file mode 100644
index 00000000000..60d3219e4db
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ModifyRobot/components.tsx
@@ -0,0 +1,12 @@
+import { NoticeBox } from 'tgui/components';
+
+export const NoSpriteWarning = (props: { name: string }) => {
+ const { name } = props;
+
+ return (
+
+ Warning, {name} has not yet chosen a sprite. Functionality might be
+ limited.
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ModifyRobot/constants.ts b/tgui/packages/tgui/interfaces/ModifyRobot/constants.ts
new file mode 100644
index 00000000000..ab554eb3c3e
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ModifyRobot/constants.ts
@@ -0,0 +1,6 @@
+export const install2col = {
+ undefined: '',
+ 0: 'red',
+ 1: 'green',
+ 2: 'grey',
+};
diff --git a/tgui/packages/tgui/interfaces/ModifyRobot/functions.ts b/tgui/packages/tgui/interfaces/ModifyRobot/functions.ts
new file mode 100644
index 00000000000..81386eb291f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ModifyRobot/functions.ts
@@ -0,0 +1,40 @@
+import { filter } from 'common/collections';
+import { flow } from 'common/fp';
+import { createSearch } from 'common/string';
+
+import { Module } from './types';
+
+type SearchObject = string | { name: string };
+
+export function prepareSearch(
+ objects: T[],
+ searchText: string = '',
+): T[] {
+ const testSearch = createSearch(searchText, (object: T): string => {
+ if (typeof object === 'string') {
+ return object;
+ } else {
+ return object['name'];
+ }
+ });
+ return flow([
+ (objects: T[]) => {
+ // Optional search term
+ if (!searchText) {
+ return objects as any;
+ } else {
+ return filter(objects, testSearch) as any;
+ }
+ },
+ ])(objects);
+}
+
+export function getModuleIcon(modules: Module[], name: string) {
+ if (modules) {
+ const module = modules.filter((module) => module.name === name);
+ if (module.length > 0) {
+ return module[0].icon;
+ }
+ }
+ return '';
+}
diff --git a/tgui/packages/tgui/interfaces/ModifyRobot/index.tsx b/tgui/packages/tgui/interfaces/ModifyRobot/index.tsx
new file mode 100644
index 00000000000..dba66ce648a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ModifyRobot/index.tsx
@@ -0,0 +1,217 @@
+import { useEffect, useState } from 'react';
+import { useBackend } from 'tgui/backend';
+import {
+ Button,
+ Divider,
+ Dropdown,
+ Input,
+ LabeledList,
+ NoticeBox,
+ Section,
+ Stack,
+ Tabs,
+} from 'tgui/components';
+import { Window } from 'tgui/layouts';
+
+import { LawManagerLaws, LawManagerLawSets } from '../LawManager';
+import { ModifyRobotNoModule } from './ModifyRobotNoModule';
+import { ModifyRobotAccess } from './ModifyRobotTabs/ModifyRobotAccess';
+import { ModifyRobotComponent } from './ModifyRobotTabs/ModifyRobotComponent';
+import { ModifyRobotModules } from './ModifyRobotTabs/ModifyRobotModules';
+import { ModifyRobotPKA } from './ModifyRobotTabs/ModifyRobotPKA';
+import { ModifyRobotRadio } from './ModifyRobotTabs/ModifyRobotRadio';
+import { ModifyRobotUpgrades } from './ModifyRobotTabs/ModifyRobotUpgrades';
+import { Data } from './types';
+
+export const ModifyRobot = (props) => {
+ const { act, data } = useBackend();
+
+ const {
+ target,
+ all_robots,
+ source,
+ model_options,
+ cell,
+ cell_options,
+ id_icon,
+ access_options,
+ ion_law_nr,
+ ion_law,
+ zeroth_law,
+ inherent_law,
+ supplied_law,
+ supplied_law_position,
+ zeroth_laws,
+ ion_laws,
+ inherent_laws,
+ supplied_laws,
+ has_zeroth_laws,
+ has_ion_laws,
+ has_inherent_laws,
+ has_supplied_laws,
+ isAI,
+ channel,
+ channels,
+ law_sets,
+ } = data;
+
+ const [tab, setTab] = useState(0);
+ const [robotName, setRobotName] = useState(target ? target.name : '');
+ const [searchLawName, setSearchLawName] = useState('');
+
+ useEffect(() => {
+ if (target?.name) {
+ setRobotName(target.name);
+ }
+ }, [target?.name]);
+
+ const tabs: React.JSX.Element[] = [];
+
+ tabs[0] = (
+
+ );
+ tabs[1] = ;
+ tabs[2] = ;
+ tabs[3] = ;
+ tabs[4] = (
+
+ );
+ tabs[5] = (
+
+ );
+ tabs[6] = (
+
+ );
+ tabs[7] = (
+
+ );
+
+ return (
+
+
+ {target ? (
+
+ {target.name}
+ {!!target.ckey && ' played by ' + target.ckey}.
+
+ ) : (
+ No target selected. Please pick one.
+ )}
+
+
+
+
+
+ act('select_target', {
+ new_target: value,
+ })
+ }
+ />
+
+ {!!target?.module && (
+ <>
+
+ setRobotName(value)}
+ />
+
+
+
+
+ >
+ )}
+
+
+
+
+ {!!target &&
+ (!target.module ? (
+
+ ) : (
+ <>
+
+ setTab(0)}>
+ Module Manager
+
+ setTab(1)}>
+ Upgrade Manager
+
+ setTab(2)}>
+ PKA
+
+ setTab(3)}>
+ Radio Manager
+
+ setTab(4)}>
+ Component Manager
+
+ setTab(5)}>
+ Access Manager
+
+ setTab(6)}>
+ Law Manager
+
+ setTab(7)}>
+ Law Sets
+
+
+ {tabs[tab]}
+ >
+ ))}
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ModifyRobot/types.ts b/tgui/packages/tgui/interfaces/ModifyRobot/types.ts
new file mode 100644
index 00000000000..02b840948ba
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ModifyRobot/types.ts
@@ -0,0 +1,133 @@
+import { BooleanLike } from 'common/react';
+
+export type Data = {
+ source: Source;
+ target: Target | null;
+ all_robots: DropdownEntry[];
+ model_options: string[] | null;
+ cell: string | null;
+ cell_options: Record;
+ id_icon: string;
+ access_options: Access[] | undefined;
+ ion_law_nr: string;
+ ion_law: string;
+ zeroth_law: string;
+ inherent_law: string;
+ supplied_law: string;
+ supplied_law_position: number;
+ zeroth_laws: law[];
+ ion_laws: law[];
+ inherent_laws: law[];
+ supplied_laws: law[];
+ has_zeroth_laws: number;
+ has_ion_laws: number;
+ has_inherent_laws: number;
+ has_supplied_laws: number;
+ isAI: BooleanLike;
+ channel: string;
+ channels: { channel: string }[];
+ law_sets: law_pack[];
+};
+
+export type DropdownEntry = {
+ displayText: string;
+ value: string;
+};
+
+export type Target = {
+ name: string;
+ ckey: string;
+ module: string;
+ active: BooleanLike;
+ crisis_override: BooleanLike;
+ active_restrictions: string[];
+ possible_restrictions: string[];
+ front: string | undefined;
+ side: string | undefined;
+ side_alt: string | undefined;
+ back: string | undefined;
+ modules: Module[];
+ whitelisted_upgrades: Upgrade[];
+ blacklisted_upgrades: Upgrade[];
+ utility_upgrades: Upgrade[];
+ basic_upgrades: Upgrade[];
+ advanced_upgrades: Upgrade[];
+ restricted_upgrades: Upgrade[];
+ radio_channels: string[];
+ availalbe_channels: string[];
+ pka: PKA | undefined;
+ components: Component[];
+ active_access: Access[];
+};
+
+export type Upgrade = {
+ name: string;
+ path: string;
+ installed: number | undefined;
+};
+
+export type Source = {
+ model: string;
+ front: string;
+ modules: Module[];
+} | null;
+
+export type Module = { name: string; ref: string; icon: string; desc: string };
+
+export type Component = {
+ name: string;
+ ref: string;
+ brute_damage: number;
+ electronics_damage: number;
+ max_damage: number;
+ installed: number;
+ exists: BooleanLike;
+};
+
+export type PKA = {
+ name: string;
+ modkits: {
+ name: string;
+ path: string;
+ costs: number;
+ denied: BooleanLike;
+ denied_by: string;
+ }[];
+ installed_modkits: { name: string; ref: string; costs: number }[];
+ capacity: number;
+ max_capacity: number;
+};
+
+export type Cell = {
+ path: string;
+ charge: number;
+ max_charge: number;
+ charge_amount: number;
+ self_charge: BooleanLike;
+};
+
+export type Access = { id: number; name: string };
+
+type law_pack = {
+ name: string;
+ header: string;
+ ref: string;
+ laws: {
+ zeroth_laws: law[];
+ has_zeroth_laws: number;
+ ion_laws: law[];
+ has_ion_laws: number;
+ inherent_laws: law[];
+ has_inherent_laws: number;
+ supplied_laws: law[];
+ has_supplied_laws: number;
+ };
+};
+
+type law = {
+ law: string;
+ index: number;
+ state: number;
+ ref: string;
+ zero: boolean; // Local UI var
+};
diff --git a/tgui/packages/tgui/interfaces/PlayerEffects/PlayerEffectsTabs/ControlSmites.tsx b/tgui/packages/tgui/interfaces/PlayerEffects/PlayerEffectsTabs/ControlSmites.tsx
index 8334dfdd384..bfd83aa3e2e 100644
--- a/tgui/packages/tgui/interfaces/PlayerEffects/PlayerEffectsTabs/ControlSmites.tsx
+++ b/tgui/packages/tgui/interfaces/PlayerEffects/PlayerEffectsTabs/ControlSmites.tsx
@@ -54,6 +54,15 @@ export const ControlSmites = (props) => {
+
+
+
);
};
diff --git a/tgui/packages/tgui/interfaces/common/RankIcon.tsx b/tgui/packages/tgui/interfaces/common/RankIcon.tsx
index 646a4e43ad7..76d5dd46b9f 100644
--- a/tgui/packages/tgui/interfaces/common/RankIcon.tsx
+++ b/tgui/packages/tgui/interfaces/common/RankIcon.tsx
@@ -1,103 +1,637 @@
import { Icon } from '../../components';
const rank2icon = {
- // Command
- 'Colony Director': 'user-tie',
+ // Command Site Manager
'Site Manager': 'user-tie',
Overseer: 'user-tie',
+ 'Facility Director': 'user-tie',
+ 'Chief Supervisor': 'user-tie',
+ Captain: 'user-tie',
+ 'Colony Director': 'user-tie',
+ // HOP
'Head of Personnel': 'briefcase',
'Crew Resources Officer': 'briefcase',
'Deputy Director': 'briefcase',
- 'Command Secretary': 'user-tie',
- // Security
+ 'Staff Manager': 'briefcase',
+ 'Facility Steward': 'briefcase',
+ 'First Mate': 'briefcase',
+ 'Performance Management Supervisor': 'briefcase',
+ // Secretary
+ 'Command Secretary': 'address-card',
+ 'Command Liaison': 'address-card',
+ 'Command Assistant': 'address-card',
+ 'Command Intern': 'address-card',
+ 'Bridge Secretary': 'address-card',
+ 'Bridge Assistant': 'address-card',
+ 'Bridge Officer': 'address-card',
+ // Security HOS
'Head of Security': 'user-shield',
'Security Commander': 'user-shield',
'Chief of Security': 'user-shield',
- Warden: ['city', 'shield-alt'],
- Detective: 'search',
- 'Forensic Technician': 'search',
- 'Security Officer': 'user-shield',
- 'Junior Officer': 'user-shield',
- // Engineering
- 'Chief Engineer': 'toolbox',
+ 'Security Managery': 'user-shield',
+ // Warden
+ Warden: ['city', 'shield-halved'],
+ 'Brig Sentry': ['city', 'shield-halved'],
+ 'Armory Superintendent': ['city', 'shield-halved'],
+ 'Master-at-Arms': ['city', 'shield-halved'],
+ // Detective
+ Detective: 'magnifying-glass',
+ Investigator: 'magnifying-glass',
+ 'Security Inspector': 'magnifying-glass',
+ 'Forensic Technician': 'magnifying-glass',
+ // Security Officer
+ 'Security Officer': 'shield',
+ 'Patrol Officer': 'shield',
+ 'Security Guard': 'shield',
+ 'Security Deputy': 'shield',
+ 'Junior Officer': 'shield',
+ 'Security Contractor': 'shield',
+ // Engineering CE
+ 'Chief Engineer': 'screwdriver-wrench',
+ 'Head Engineer': 'screwdriver-wrench',
+ Foreman: 'screwdriver-wrench',
+ 'Maintenance Manager': 'screwdriver-wrench',
+ // Atmospheric Technician
'Atmospheric Technician': 'wind',
- 'Station Engineer': 'toolbox',
+ 'Atmospheric Engineer': 'wind',
+ 'Atmospheric Maintainer': 'wind',
+ 'Disposals Technician': 'wind',
+ 'Fuel Technician': 'wind',
+ // Engineer
+ Engineer: 'toolbox',
'Maintenance Technician': 'wrench',
'Engine Technician': 'toolbox',
- Electrician: 'toolbox',
- // Medical
- 'Chief Medical Officer': 'user-md',
+ Electrician: 'screwdriver',
+ 'Construction Engineer': 'trowel-bricks',
+ 'Engineering Contractor': 'ruler',
+ // Medical CMO
+ 'Chief Medical Officer': 'user-doctor',
+ 'Chief Physician': 'user-doctor',
+ 'Medical Director': 'user-doctor',
+ 'Healthcare Manager': 'user-doctor',
+ // Chemist
Chemist: 'mortar-pestle',
Pharmacist: 'mortar-pestle',
- 'Medical Doctor': 'user-md',
- Surgeon: 'user-md',
- 'Emergency Physician': 'user-md',
- Nurse: 'user-md',
+ Pharmacologist: 'mortar-pestle',
+ // Medical Doctor
+ 'Medical Doctor': 'suitcase-medical',
+ Physician: 'suitcase-medical',
+ 'Medical Practitioner': 'suitcase-medical',
+ Surgeon: 'syringe',
+ 'Emergency Physician': 'suitcase-medical',
+ Nurse: 'user-nurse',
+ Orderly: 'book-medical',
Virologist: 'disease',
- Paramedic: 'ambulance',
- 'Emergency Medical Technician': 'ambulance',
+ 'Medical Contractor': 'notes-medical',
+ // Paramedic
+ Paramedic: 'truck-medical',
+ 'Emergency Medical Technician': 'truck-medical',
+ 'Medical Responder': 'truck-medical',
+ 'Search and Rescue': 'truck-droplet',
+ // Psychiatrist
Psychiatrist: 'couch',
Psychologist: 'couch',
- // Science
+ Psychoanalyst: 'couch',
+ Psychotherapist: 'couch',
+ // Genetecist
+ Geneticist: 'dna',
+ // Brig Physician
+ 'Brig Physician': ['kit-medical', 'shield-halved'],
+ 'Security Medic': ['kit-medical', 'shield-halved'],
+ 'Security Medic Care Unit': ['kit-medical', 'shield-halved'],
+ 'Penitentiary Medical Care Unit': ['kit-medical', 'shield-halved'],
+ 'Junior Brig Physician': ['kit-medical', 'shield-halved'],
+ 'Detention Health Officer': ['kit-medical', 'shield-halved'],
+ // Science RD
'Research Director': 'user-graduate',
'Research Supervisor': 'user-graduate',
+ 'Research Manager': 'user-graduate',
+ 'Head of Development': 'user-graduate',
+ 'Head Scientist': 'user-graduate',
+ // Roboticist
Roboticist: 'robot',
+ 'Assembly Technician': 'screwdriver',
'Biomechanical Engineer': ['wrench', 'heartbeat'],
'Mechatronic Engineer': 'wrench',
+ // Scientist
Scientist: 'flask',
- Xenoarchaeologist: 'flask',
- Anomalist: 'flask',
- 'Phoron Researcher': 'flask',
+ Researcher: 'flask-vial',
+ 'Lab Assistant': 'flask',
+ Xenoarchaeologist: 'bone',
+ Xenopaleontologist: 'bone',
+ Anomalist: 'atom',
+ 'Phoron Researcher': 'vials',
+ 'Gas Physicist': 'microscope',
'Circuit Designer': 'car-battery',
+ 'Circuit Programmer': 'laptop-file',
+ // Xenobiologist
Xenobiologist: 'meteor',
+ Xenozoologist: 'locust',
+ Xenoanthropologist: 'bugs',
+ // Xenobotanist
Xenobotanist: ['biohazard', 'seedling'],
- // Cargo
+ Xenohydroponicist: ['biohazard', 'droplet'],
+ Xenoflorist: ['biohazard', 'clover'],
+ // Cargo QM
Quartermaster: 'box-open',
'Supply Chief': 'warehouse',
- 'Cargo Technician': 'box-open',
- 'Shaft Miner': 'hard-hat',
- 'Drill Technician': 'hard-hat',
- // Exploration
+ 'Logistics Manager': 'warehouse',
+ 'Cargo Supervisor': 'box-open',
+ // Cargo Technician
+ 'Cargo Technician': 'box',
+ 'Cargo Loader': 'dolly',
+ 'Cargo Handler': 'boxes-stacked',
+ 'Supply Courier': 'people-carry-box',
+ 'Disposals Sorter': 'recycle',
+ Mailman: 'envelopes-bulk',
+ // Shaft Miner
+ 'Shaft Miner': 'helmet-safety',
+ 'Deep Space Miner': 'bore-hole',
+ 'Drill Technician': 'oil-well',
+ Prospector: 'helmet-safety',
+ Excavator: 'bore-hole',
+ // Exploration Lead
Pathfinder: 'binoculars',
+ 'Expedition Lead': 'binoculars',
+ 'Exploration Manager': 'binoculars',
+ // Explorer
Explorer: 'user-astronaut',
- 'Field Medic': ['user-md', 'user-astronaut'],
- Pilot: 'space-shuttle',
- // Civvies
+ Surveyor: 'user-astronaut',
+ 'Offsite Scout': 'user-astronaut',
+ 'Explorer Medic': ['user-astronaut', 'kit-medical'],
+ 'Explorer Technician': ['user-astronaut', 'screwdriver'],
+ // Field Medic
+ 'Field Medic': ['suitcase-medical', 'user-astronaut'],
+ 'Expedition Medic': ['suitcase-medical', 'user-astronaut'],
+ 'Offsite Medic': ['suitcase-medical', 'user-astronaut'],
+ // Pilot
+ Pilot: 'shuttle-space',
+ 'Co-Pilot': 'shuttle-space',
+ Navigator: 'shuttle-space',
+ Helmsman: 'shuttle-space',
+ // Barkeeper
Bartender: 'glass-martini',
+ Barkeeper: 'wine-glass',
+ Barmaid: 'whiskey-glass',
Barista: 'coffee',
+ Mixologist: 'martini-glass-citrus',
+ // Botanist
Botanist: 'leaf',
+ Hydroponicist: 'droplet',
Gardener: 'leaf',
- Chaplain: 'place-of-worship',
- Counselor: 'couch',
+ Cultivator: 'spa',
+ Farmer: 'plant-wilt',
+ Florist: 'spa',
+ Rancher: 'leaf',
+ // Chef
Chef: 'utensils',
+ 'Sous-chef': 'spoon',
Cook: 'utensils',
+ 'Kitchen Worker': 'kitchen-set',
+ // Chaplain
+ Chaplain: 'place-of-worship',
+ Counselor: 'cross',
+ Preacher: 'cross',
+ Missionary: 'cross',
+ Priest: 'cross',
+ Nun: 'church',
+ Monk: 'place-of-worship',
+ Guru: 'place-of-worship',
+ // Entertainer
Entertainer: 'smile-beam',
Performer: 'smile-beam',
Musician: 'guitar',
Stagehand: 'smile-beam',
+ Actor: 'face-laugh-wink',
+ Dancer: 'face-smile',
+ Singer: 'smusic',
+ Magician: 'wand-magic-sparkles',
+ Comedian: 'face-laugh-wink',
+ Tragedian: 'face-sad-tear',
+ Artist: 'smile-beam',
+ 'Game Master': 'dice',
+ // Entrepreneur
+ Entrepreneur: 'building',
+ Lawyer: 'gavel',
+ 'Private Eye': 'user-secret',
+ Bodyguard: 'person-military-pointing',
+ 'Personal Physician': 'star-of-life',
+ Dentist: 'teeth',
+ 'Fitness Instructor': 'dumbbell',
+ 'Yoga Teacher': 'person-walking',
+ Masseuse: 'bottle-droplet',
+ Tradesperson: 'money-bill-trend-up',
+ Streamer: 'desktop',
+ Influencer: 'computer',
+ 'Paranormal Investigator': 'magnifying-glass-arrow-right',
+ 'Personal Secretary': 'pen-to-square',
+ Stylist: 'hat-cowboy-side',
+ Fisher: 'fish-fins',
+ 'Fortune Teller': 'golf-ball-tee',
+ 'Spirit Healer': 'ghost',
// All of the interns
Intern: 'school',
'Apprentice Engineer': ['school', 'wrench'],
- 'Medical Intern': ['school', 'user-md'],
- 'Lab Assistant': ['school', 'flask'],
- 'Security Cadet': ['school', 'shield-alt'],
+ 'Research Intern': ['school', 'flask'],
+ 'Security Cadet': ['school', 'shield-halved'],
'Jr. Cargo Tech': ['school', 'box'],
'Jr. Explorer': ['school', 'user-astronaut'],
+ Assistant: ['school', 'address-card'],
Server: ['school', 'utensils'],
- // Back to civvies
+ 'Technical Assistant': ['school', 'screwdriver'],
+ 'Medical Intern': ['school', 'user-nurse'],
+ 'Research Assistant"': ['school', 'flask'],
+ Visitor: 'user',
+ Resident: 'user',
+ // IAA
'Internal Affairs Agent': 'balance-scale',
+ 'Internal Affairs Liaison': 'balance-scale',
+ 'Internal Affairs Delegate': 'balance-scale',
+ 'Internal Affairs Investigator': 'balance-scale',
+ // Janitor
Janitor: 'broom',
Custodian: 'broom',
'Sanitation Technician': 'hand-sparkles',
Maid: 'broom',
+ 'Garbage Collector': 'dumpster',
+ // Librarian
Librarian: 'book',
Journalist: 'newspaper',
+ Reporter: 'newspaper',
Writer: 'book',
Historian: 'chalkboard-teacher',
+ Archivist: 'book',
Professor: 'chalkboard-teacher',
- Visitor: 'user',
+ Academic: 'chalkboard-teacher',
+ Philosopher: 'book',
+ Curator: 'book',
+ // Off duty
+ 'Off-duty Officer': 'tree-city',
+ 'Off-duty Engineer': 'tree-city',
+ 'Off-duty Medic': 'tree-city',
+ 'Off-duty Scientist': 'tree-city',
+ 'Off-duty Cargo': 'tree-city',
+ 'Off-duty Explorer': 'tree-city',
+ 'Off-duty Worker': 'tree-city',
+ // AI / Robot
+ AI: 'display',
+ Cyborg: 'robot',
+ Robot: 'robot',
+ Drone: 'robot',
+ // Clown / Mime
+ Clown: 'bullhorn',
+ Jester: 'bullhorn',
+ Fool: 'bullhorn',
+ Mime: 'face-grin-tears',
+ Poseur: 'face-grin-tears',
// Special roles
'Emergency Responder': 'fighter-jet',
+ // Talon
+ 'Talon Captain': ['location-arrow', 'user-tie'],
+ 'Talon Commander': ['location-arrow', 'user-tie'],
+ 'Talon Doctor': ['location-arrow', 'suitcase-medical'],
+ 'Talon Medic': ['location-arrow', 'suitcase-medical'],
+ 'Talon Engineer': ['location-arrow', 'wrench'],
+ 'Talon Technician': ['location-arrow', 'screwdriver'],
+ 'Talon Guard': ['location-arrow', 'shield'],
+ 'Talon Security': ['location-arrow', 'shield'],
+ 'Talon Marine': ['location-arrow', 'shield'],
+ 'Talon Pilot': ['location-arrow', 'shuttle-space'],
+ 'Talon Helmsman': ['location-arrow', 'shuttle-space'],
+ 'Talon Miner': ['location-arrow', 'helmet-safety'],
+ 'Talon Excavator': ['location-arrow', 'helmet-safety'],
+ // Robot Modules
+ Standard: 'robot',
+ Service: 'glass-martini',
+ Clerical: 'pen-to-square',
+ Research: 'flask',
+ Miner: 'helmet-safety',
+ Crisis: 'kit-medical',
+ Security: 'shield',
+ Combat: 'gun',
+ Engineering: 'wrench',
+ Gravekeeper: 'square-xmark',
+ Lost: 'location-crosshairs',
+ Protector: 'building-shield',
+ Mechanist: 'gears',
+ 'Combat Medic': 'x-ray',
+};
+
+const rank2color = {
+ // Command Site Manager
+ 'Site Manager': 'blue',
+ Overseer: 'blue',
+ 'Facility Director': 'blue',
+ 'Chief Supervisor': 'blue',
+ Captain: 'blue',
+ 'Colony Director': 'blue',
+ // HOP
+ 'Head of Personnel': 'blue',
+ 'Crew Resources Officer': 'blue',
+ 'Deputy Director': 'blue',
+ 'Staff Manager': 'blue',
+ 'Facility Steward': 'blue',
+ 'First Mate': 'blue',
+ 'Performance Management Supervisor': 'blue',
+ // Secretary
+ 'Command Secretary': 'blue',
+ 'Command Liaison': 'blue',
+ 'Command Assistant': 'blue',
+ 'Command Intern': 'blue',
+ 'Bridge Secretary': 'blue',
+ 'Bridge Assistant': 'blue',
+ 'Bridge Officer': 'blue',
+ // Security HOS
+ 'Head of Security': 'blue',
+ 'Security Commander': 'blue',
+ 'Chief of Security': 'blue',
+ 'Security Managery': 'blue',
+ // Warden
+ Warden: 'red',
+ 'Brig Sentry': 'red',
+ 'Armory Superintendent': 'red',
+ 'Master-at-Arms': 'red',
+ // Detective
+ Detective: 'red',
+ Investigator: 'red',
+ 'Security Inspector': 'red',
+ 'Forensic Technician': 'red',
+ // Security Officer
+ 'Security Officer': 'red',
+ 'Patrol Officer': 'red',
+ 'Security Guard': 'red',
+ 'Security Deputy': 'red',
+ 'Junior Officer': 'red',
+ 'Security Contractor': 'red',
+ // Engineering CE
+ 'Chief Engineer': 'blue',
+ 'Head Engineer': 'blue',
+ Foreman: 'blue',
+ 'Maintenance Manager': 'blue',
+ // Atmospheric Technician
+ 'Atmospheric Technician': 'orange',
+ 'Atmospheric Engineer': 'orange',
+ 'Atmospheric Maintainer': 'orange',
+ 'Disposals Technician': 'orange',
+ 'Fuel Technician': 'orange',
+ // Engineer
+ Engineer: 'orange',
+ 'Maintenance Technician': 'orange',
+ 'Engine Technician': 'orange',
+ Electrician: 'orange',
+ 'Construction Engineer': 'orange',
+ 'Engineering Contractor': 'orange',
+ // Medical CMO
+ 'Chief Medical Officer': 'blue',
+ 'Chief Physician': 'blue',
+ 'Medical Director': 'blue',
+ 'Healthcare Manager': 'blue',
+ // Chemist
+ Chemist: 'teal',
+ Pharmacist: 'teal',
+ Pharmacologist: 'teal',
+ // Medical Doctor
+ 'Medical Doctor': 'teal',
+ Physician: 'teal',
+ 'Medical Practitioner': 'teal',
+ Surgeon: 'teal',
+ 'Emergency Physician': 'teal',
+ Nurse: 'teal',
+ Orderly: 'teal',
+ Virologist: 'teal',
+ 'Medical Contractor': 'teal',
+ // Paramedic
+ Paramedic: 'teal',
+ 'Emergency Medical Technician': 'teal',
+ 'Medical Responder': 'teal',
+ 'Search and Rescue': 'teal',
+ // Psychiatrist
+ Psychiatrist: 'teal',
+ Psychologist: 'teal',
+ Psychoanalyst: 'teal',
+ Psychotherapist: 'teal',
+ // Genetecist
+ Geneticist: 'teal',
+ // Brig Physician
+ 'Brig Physician': 'teal',
+ 'Security Medic': 'teal',
+ 'Security Medic Care Unit': 'teal',
+ 'Penitentiary Medical Care Unit': 'teal',
+ 'Junior Brig Physician': 'teal',
+ 'Detention Health Officer': 'teal',
+ // Science RD
+ 'Research Director': 'blue',
+ 'Research Supervisor': 'blue',
+ 'Research Manager': 'blue',
+ 'Head of Development': 'blue',
+ 'Head Scientist': 'blue',
+ // Roboticist
+ Roboticist: 'purple',
+ 'Assembly Technician': 'purple',
+ 'Biomechanical Engineer': 'purple',
+ 'Mechatronic Engineer': 'purple',
+ // Scientist
+ Scientist: 'purple',
+ Researcher: 'purple',
+ 'Lab Assistant': 'purple',
+ Xenoarchaeologist: 'purple',
+ Xenopaleontologist: 'purple',
+ Anomalist: 'purple',
+ 'Phoron Researcher': 'purple',
+ 'Gas Physicist': 'purple',
+ 'Circuit Designer': 'purple',
+ 'Circuit Programmer': 'purple',
+ // Xenobiologist
+ Xenobiologist: 'purple',
+ Xenozoologist: 'purple',
+ Xenoanthropologist: 'purple',
+ // Xenobotanist
+ Xenobotanist: 'purple',
+ Xenohydroponicist: 'purple',
+ Xenoflorist: 'purple',
+ // Cargo QM
+ Quartermaster: 'brown',
+ 'Supply Chief': 'brown',
+ 'Logistics Manager': 'brown',
+ 'Cargo Supervisor': 'brown',
+ // Cargo Technician
+ 'Cargo Technician': 'brown',
+ 'Cargo Loader': 'brown',
+ 'Cargo Handler': 'brown',
+ 'Supply Courier': 'brown',
+ 'Disposals Sorter': 'brown',
+ Mailman: 'brown',
+ // Shaft Miner
+ 'Shaft Miner': 'brown',
+ 'Deep Space Miner': 'brown',
+ 'Drill Technician': 'brown',
+ Prospector: 'brown',
+ Excavator: 'brown',
+ // Exploration Lead
+ Pathfinder: 'blue',
+ 'Expedition Lead': 'blue',
+ 'Exploration Manager': 'blue',
+ // Explorer
+ Explorer: 'grey',
+ Surveyor: 'grey',
+ 'Offsite Scout': 'grey',
+ 'Explorer Medic': 'grey',
+ 'Explorer Technician': 'grey',
+ // Field Medic
+ 'Field Medic': 'grey',
+ 'Expedition Medic': 'grey',
+ 'Offsite Medic': 'grey',
+ // Pilot
+ Pilot: 'grey',
+ 'Co-Pilot': 'grey',
+ Navigator: 'grey',
+ Helmsman: 'grey',
+ // Barkeeper
+ Bartender: 'green',
+ Barkeeper: 'green',
+ Barmaid: 'green',
+ Barista: 'green',
+ Mixologist: 'green',
+ // Botanist
+ Botanist: 'green',
+ Hydroponicist: 'green',
+ Gardener: 'green',
+ Cultivator: 'green',
+ Farmer: 'green',
+ Florist: 'green',
+ Rancher: 'green',
+ // Chef
+ Chef: 'green',
+ 'Sous-chef': 'green',
+ Cook: 'green',
+ 'Kitchen Worker': 'green',
+ // Chaplain
+ Chaplain: 'green',
+ Counselor: 'green',
+ Preacher: 'green',
+ Missionary: 'green',
+ Priest: 'green',
+ Nun: 'green',
+ Monk: 'green',
+ Guru: 'green',
+ // Entertainer
+ Entertainer: 'green',
+ Performer: 'green',
+ Musician: 'green',
+ Stagehand: 'green',
+ Actor: 'green',
+ Dancer: 'green',
+ Singer: 'green',
+ Magician: 'green',
+ Comedian: 'green',
+ Tragedian: 'green',
+ Artist: 'green',
+ 'Game Master': 'green',
+ // Entrepreneur
+ Entrepreneur: 'green',
+ Lawyer: 'green',
+ 'Private Eye': 'green',
+ Bodyguard: 'green',
+ 'Personal Physician': 'green',
+ Dentist: 'green',
+ 'Fitness Instructor': 'green',
+ 'Yoga Teacher': 'green',
+ Masseuse: 'green',
+ Tradesperson: 'green',
+ Streamer: 'green',
+ Influencer: 'green',
+ 'Paranormal Investigator': 'green',
+ 'Personal Secretary': 'green',
+ Stylist: 'green',
+ Fisher: 'green',
+ 'Fortune Teller': 'green',
+ 'Spirit Healer': 'green',
+ // All of the interns
+ Intern: 'green',
+ 'Apprentice Engineer': 'green',
+ 'Research Intern': 'green',
+ 'Security Cadet': 'green',
+ 'Jr. Cargo Tech': 'green',
+ 'Jr. Explorer': 'green',
+ Assistant: 'green',
+ Server: 'green',
+ 'Technical Assistant': 'green',
+ 'Medical Intern': 'green',
+ 'Research Assistant"': 'green',
+ Visitor: 'green',
+ Resident: 'green',
+ // IAA
+ 'Internal Affairs Agent': 'blue',
+ 'Internal Affairs Liaison': 'blue',
+ 'Internal Affairs Delegate': 'blue',
+ 'Internal Affairs Investigator': 'blue',
+ // Janitor
+ Janitor: 'green',
+ Custodian: 'green',
+ 'Sanitation Technician': 'green',
+ Maid: 'green',
+ 'Garbage Collector': 'green',
+ // Librarian
+ Librarian: 'green',
+ Journalist: 'green',
+ Reporter: 'green',
+ Writer: 'green',
+ Historian: 'green',
+ Archivist: 'green',
+ Professor: 'green',
+ Academic: 'green',
+ Philosopher: 'green',
+ Curator: 'green',
+ // Off duty
+ 'Off-duty Officer': 'white',
+ 'Off-duty Engineer': 'white',
+ 'Off-duty Medic': 'white',
+ 'Off-duty Scientist': 'white',
+ 'Off-duty Cargo': 'white',
+ 'Off-duty Explorer': 'white',
+ 'Off-duty Worker': 'white',
+ // AI / Robot
+ AI: 'darkgrey',
+ Cyborg: 'darkgrey',
+ Robot: 'darkgrey',
+ Drone: 'darkgrey',
+ // Clown / Mime
+ Clown: 'green',
+ Jester: 'green',
+ Fool: 'green',
+ Mime: 'green',
+ Poseur: 'green',
+ // Special roles
+ 'Emergency Responder': 'yellow',
+ // Talon
+ 'Talon Captain': 'grey',
+ 'Talon Commander': 'grey',
+ 'Talon Doctor': 'grey',
+ 'Talon Medic': 'grey',
+ 'Talon Engineer': 'grey',
+ 'Talon Technician': 'grey',
+ 'Talon Guard': 'grey',
+ 'Talon Security': 'grey',
+ 'Talon Marine': 'grey',
+ 'Talon Pilot': 'grey',
+ 'Talon Helmsman': 'grey',
+ 'Talon Miner': 'grey',
+ 'Talon Excavator': 'grey',
+ // Robot Modules
+ Standard: 'grey',
+ Service: 'green',
+ Clerical: 'blue',
+ Research: 'purple',
+ Miner: 'brown',
+ Crisis: 'teal',
+ Security: 'red',
+ Combat: 'yellow',
+ Engineering: 'orange',
+ Gravekeeper: 'dark-grey',
+ Lost: 'grey',
+ Protector: 'darkred',
+ Mechanist: 'darkred',
+ 'Combat Medic': 'darkred',
};
type rank_icon = { rank: string; color: string };
diff --git a/tgui/packages/tgui/package.json b/tgui/packages/tgui/package.json
index efe74af8123..b933df189d7 100644
--- a/tgui/packages/tgui/package.json
+++ b/tgui/packages/tgui/package.json
@@ -8,7 +8,7 @@
"@types/react": "^18.2.74",
"common": "workspace:*",
"dateformat": "^4.6.3",
- "dompurify": "^2.4.9",
+ "dompurify": "^2.5.4",
"highlight.js": "^11.9.0",
"jest": "^29.7.0",
"js-yaml": "^4.1.0",
diff --git a/tgui/yarn.lock b/tgui/yarn.lock
index 3ec34d15025..c53812c4aec 100644
--- a/tgui/yarn.lock
+++ b/tgui/yarn.lock
@@ -3119,10 +3119,10 @@ __metadata:
languageName: node
linkType: hard
-"dompurify@npm:^2.4.9":
- version: 2.5.3
- resolution: "dompurify@npm:2.5.3"
- checksum: 10c0/0ed513212cd7c51a14357cbf8135bbc58b13c8e7053cb0762ade33e60ba8ccd5ad5826734ee1d09cbd893fefcca2230b4c76f1f7742c863c691e64f51f75db54
+"dompurify@npm:^2.5.4":
+ version: 2.5.6
+ resolution: "dompurify@npm:2.5.6"
+ checksum: 10c0/ee7e7d17982b1017a20982a2d57a0463d7fbb67f7b92a13ecf772e5e6acf0a529a19e3e31d725b05d5a2524d40e0aeb7ebc4be0aff396a6345bd6f2749fe560d
languageName: node
linkType: hard
@@ -8255,7 +8255,7 @@ __metadata:
"@types/node": "npm:^20.12.3"
"@types/react": "npm:^18.2.74"
common: "workspace:*"
- dompurify: "npm:^2.4.9"
+ dompurify: "npm:^2.5.4"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
tgui: "workspace:*"
@@ -8336,7 +8336,7 @@ __metadata:
"@types/react": "npm:^18.2.74"
common: "workspace:*"
dateformat: "npm:^4.6.3"
- dompurify: "npm:^2.4.9"
+ dompurify: "npm:^2.5.4"
highlight.js: "npm:^11.9.0"
jest: "npm:^29.7.0"
js-yaml: "npm:^4.1.0"
diff --git a/vorestation.dme b/vorestation.dme
index 11313c0b1d0..9f8a730d4f0 100644
--- a/vorestation.dme
+++ b/vorestation.dme
@@ -1738,6 +1738,7 @@
#include "code\modules\admin\holder2.dm"
#include "code\modules\admin\IsBanned.dm"
#include "code\modules\admin\map_capture.dm"
+#include "code\modules\admin\modify_robot.dm"
#include "code\modules\admin\NewBan.dm"
#include "code\modules\admin\news.dm"
#include "code\modules\admin\persistence.dm"
@@ -1814,7 +1815,6 @@
#include "code\modules\admin\verbs\lightning_strike.dm"
#include "code\modules\admin\verbs\map_template_loadverb.dm"
#include "code\modules\admin\verbs\mapping.dm"
-#include "code\modules\admin\verbs\modify_robot.dm"
#include "code\modules\admin\verbs\panicbunker.dm"
#include "code\modules\admin\verbs\playsound.dm"
#include "code\modules\admin\verbs\possess.dm"