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/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/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/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/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"