Files
+37 21b4095dfd [MDB IGNORE] [IDB IGNORE] Upstream Sync - 04/17/2026 (#5453)
Upstream 04/17/2026

fixes https://github.com/Bubberstation/Bubberstation/issues/5549

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: tgstation-ci[bot] <179393467+tgstation-ci[bot]@users.noreply.github.com>
Co-authored-by: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com>
Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Co-authored-by: Rhials <28870487+Rhials@users.noreply.github.com>
Co-authored-by: rageguy505 <54517726+rageguy505@users.noreply.github.com>
Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
Co-authored-by: John Willard <53777086+JohnFulpWillard@users.noreply.github.com>
Co-authored-by: Aliceee2ch <160794176+Aliceee2ch@users.noreply.github.com>
Co-authored-by: Time-Green <7501474+Time-Green@users.noreply.github.com>
Co-authored-by: Tsar-Salat <62388554+Tsar-Salat@users.noreply.github.com>
Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com>
Co-authored-by: Maxipat <108554989+Maxipat112@users.noreply.github.com>
Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: SimplyLogan <47579821+loganuk@users.noreply.github.com>
Co-authored-by: loganuk <fakeemail123@aol.com>
Co-authored-by: Leland Kemble <70413276+lelandkemble@users.noreply.github.com>
Co-authored-by: FalloutFalcon <86381784+FalloutFalcon@users.noreply.github.com>
Co-authored-by: Roxy <75404941+TealSeer@users.noreply.github.com>
Co-authored-by: Lucy <lucy@absolucy.moe>
Co-authored-by: siliconOpossum <138069572+siliconOpossum@users.noreply.github.com>
Co-authored-by: Isratosh <Isratosh@hotmail.com>
Co-authored-by: TheRyeGuyWhoWillNowDie <70169560+TheRyeGuyWhoWillNowDie@users.noreply.github.com>
Co-authored-by: Neocloudy <88008002+Neocloudy@users.noreply.github.com>
Co-authored-by: Alexander V. <volas@ya.ru>
Co-authored-by: ElGitificador <168473461+ElGitificador@users.noreply.github.com>
Co-authored-by: Twaticus <46540570+Twaticus@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com>
Co-authored-by: Cameron Lennox <killer65311@gmail.com>
Co-authored-by: Tim <timothymtorres@gmail.com>
Co-authored-by: Iamgoofball <iamgoofball@gmail.com>
Co-authored-by: Layzu666 <121319428+Layzu666@users.noreply.github.com>
Co-authored-by: Arturlang <24881678+Arturlang@users.noreply.github.com>
Co-authored-by: _0Steven <42909981+00-Steven@users.noreply.github.com>
Co-authored-by: mrmanlikesbt <99309552+mrmanlikesbt@users.noreply.github.com>
Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com>
Co-authored-by: John F. Kennedy <54908920+MacaroniCritter@users.noreply.github.com>
Co-authored-by: Cursor <102828457+theselfish@users.noreply.github.com>
Co-authored-by: Josh <josh.adam.powell@gmail.com>
Co-authored-by: Josh Powell <josh.powell@softwire.com>
Co-authored-by: Yobrocharlie <Charliemiller5617@gmail.com>
Co-authored-by: Hardly3D <66234359+Hardly3D@users.noreply.github.com>
Co-authored-by: shayoki <96078776+shayoki@users.noreply.github.com>
Co-authored-by: LT3 <83487515+lessthnthree@users.noreply.github.com>
2026-05-16 00:56:00 +02:00

1092 lines
38 KiB
Plaintext

/mob/living/silicon/robot/Initialize(mapload)
spark_system = new /datum/effect_system/basic/spark_spread(src, 5, FALSE)
spark_system.attach(src)
add_traits(list(TRAIT_CAN_STRIP, TRAIT_FORCED_STANDING, TRAIT_KNOW_ENGI_WIRES, TRAIT_IGNORE_SURGERY_MODIFIERS), INNATE_TRAIT)
AddComponent(/datum/component/tippable, \
tip_time = 3 SECONDS, \
untip_time = 2 SECONDS, \
self_right_time = 3 SECONDS, \
post_tipped_callback = CALLBACK(src, PROC_REF(after_tip_over)), \
post_untipped_callback = CALLBACK(src, PROC_REF(after_righted)), \
roleplay_friendly = TRUE, \
roleplay_emotes = list(/datum/emote/silicon/buzz, /datum/emote/silicon/buzz2, /datum/emote/silicon/beep, /datum/emote/silicon/beep2), /* SKYRAT EDIT CHANGE - ORIGINAL: roleplay_emotes = list(/datum/emote/silicon/buzz, /datum/emote/silicon/buzz2, /datum/emote/silicon/beep)*/ \
roleplay_callback = CALLBACK(src, PROC_REF(untip_roleplay)))
//BUBBER EDIT: REDUCES THE SELF-RIGHT TIME FOR BORGS TO 3 SECONDS, FROM 60 SECONDS
set_wires(new /datum/wires/robot(src))
AddElement(/datum/element/empprotection, EMP_PROTECT_WIRES)
AddElement(/datum/element/ridable, /datum/component/riding/creature/cyborg)
RegisterSignal(src, COMSIG_PROCESS_BORGCHARGER_OCCUPANT, PROC_REF(charge))
RegisterSignal(src, COMSIG_LIGHT_EATER_ACT, PROC_REF(on_light_eater))
RegisterSignal(src, SIGNAL_ADDTRAIT(TRAIT_GOT_DAMPENED), PROC_REF(on_dampen))
// BUBBER EDIT: Register buckle/unbuckle signal to update sprite on tallborgs
RegisterSignal(src, COMSIG_MOB_BUCKLED, PROC_REF(on_buckled))
RegisterSignal(src, COMSIG_MOB_UNBUCKLED, PROC_REF(on_unbuckled))
previous_health = health
if(ispath(cell))
cell = new cell(src)
create_modularInterface()
model = new /obj/item/robot_model(src)
model.rebuild_modules()
if(lawupdate)
make_laws()
for (var/law in laws.inherent)
lawcheck += law
if(!TryConnectToAI())
lawupdate = FALSE
if(!scrambledcodes && !builtInCamera)
builtInCamera = new(src)
builtInCamera.c_tag = real_name
if(wires.is_cut(WIRE_CAMERA))
builtInCamera.camera_enabled = FALSE
update_icons()
. = ..()
LoadComponent(/datum/component/bloodysoles/bot)
//If this body is meant to be a borg controlled by the AI player
if(shell)
var/obj/item/borg/upgrade/ai/board = new(src)
make_shell(board)
add_to_upgrades(board)
ADD_TRAIT(src, TRAIT_CAN_GET_AI_TRACKING_MESSAGE, INNATE_TRAIT)
else
//MMI stuff. Held togheter by magic. ~Miauw
if(!mmi?.brainmob)
mmi = new (src)
mmi.brain = new /obj/item/organ/brain(mmi)
mmi.brain.organ_flags |= ORGAN_FROZEN
mmi.brain.name = "[real_name]'s brain"
mmi.name = "[initial(mmi.name)]: [real_name]"
mmi.set_brainmob(new /mob/living/brain(mmi))
mmi.brainmob.name = src.real_name
mmi.brainmob.real_name = src.real_name
mmi.brainmob.container = mmi
mmi.update_appearance()
setup_default_name()
if(mmi.brainmob)
gender = mmi.brainmob.gender
aicamera = new/obj/item/camera/siliconcam/robot_camera(src)
toner = tonermax
diag_hud_set_borgcell()
logevent("System brought online.")
log_silicon("New cyborg [key_name(src)] created with [connected_ai ? "master AI: [key_name(connected_ai)]" : "no master AI"]")
log_current_laws()
var/static/list/alert_areas
if(isnull(alert_areas))
alert_areas = (GLOB.the_station_areas + typesof(/area/mine))
if(is_station_level(z))
alert_control = new(src, list(ALARM_ATMOS, ALARM_FIRE, ALARM_POWER, ALARM_CAMERA, ALARM_BURGLAR, ALARM_MOTION), SSmapping.levels_by_trait(ZTRAIT_STATION), alert_areas)
else
alert_control = new(src, list(ALARM_ATMOS, ALARM_FIRE, ALARM_POWER, ALARM_CAMERA, ALARM_BURGLAR, ALARM_MOTION), (SSmapping.levels_by_trait(ZTRAIT_STATION) + z), alert_areas)
RegisterSignal(alert_control.listener, COMSIG_ALARM_LISTENER_TRIGGERED, PROC_REF(alarm_triggered))
RegisterSignal(alert_control.listener, COMSIG_ALARM_LISTENER_CLEARED, PROC_REF(alarm_cleared))
alert_control.listener.RegisterSignal(src, COMSIG_LIVING_DEATH, TYPE_PROC_REF(/datum/alarm_listener, prevent_alarm_changes))
alert_control.listener.RegisterSignal(src, COMSIG_LIVING_REVIVE, TYPE_PROC_REF(/datum/alarm_listener, allow_alarm_changes))
/mob/living/silicon/robot/set_suicide(suicide_state)
. = ..()
if(mmi)
if(mmi.brain)
mmi.brain.suicided = suicide_state
if(suicide_state && mmi.brainmob)
ADD_TRAIT(mmi.brainmob, TRAIT_SUICIDED, REF(src))
/**
* Sets the tablet theme and icon
*
* These variables are based on if the borg is a syndicate type or is emagged. This gets used in model change code
* and also borg emag code.
*/
/mob/living/silicon/robot/proc/set_modularInterface_theme()
if(istype(model, /obj/item/robot_model/syndicate) || emagged)
modularInterface.device_theme = PDA_THEME_SYNDICATE
modularInterface.icon_state = "tablet-silicon-syndicate"
else
modularInterface.device_theme = PDA_THEME_NTOS
modularInterface.icon_state = "tablet-silicon"
modularInterface.icon_state_powered = "tablet-silicon"
modularInterface.icon_state_unpowered = "tablet-silicon"
modularInterface.update_icon()
//If there's an MMI in the robot, have it ejected when the mob goes away. --NEO
/mob/living/silicon/robot/Destroy()
if(connected_ai)
set_connected_ai(null)
if(shell)
GLOB.available_ai_shells -= src
QDEL_NULL(modularInterface)
QDEL_NULL(model)
QDEL_NULL(eye_lights)
QDEL_NULL(hat_overlay)
QDEL_NULL(spark_system)
QDEL_NULL(alert_control)
QDEL_LIST(upgrades)
QDEL_NULL(cell)
QDEL_NULL(robot_suit)
if (smoke_particles)
remove_shared_particles(smoke_particles)
if (spark_particles)
remove_shared_particles(spark_particles)
return ..()
/mob/living/silicon/robot/Topic(href, href_list)
. = ..()
//Show alerts window if user clicked on "Show alerts" in chat
if(href_list["showalerts"])
alert_control.ui_interact(src)
/mob/living/silicon/robot/get_cell()
return cell
/mob/living/silicon/robot/proc/pick_model()
if(model.type != /obj/item/robot_model)
return
if(wires.is_cut(WIRE_RESET_MODEL))
to_chat(src,span_userdanger("ERROR: Model installer reply timeout. Please check internal connections."))
return
if(lockcharge == TRUE)
to_chat(src,span_userdanger("ERROR: Lockdown is engaged. Please disengage lockdown to pick module."))
return
// SKYRAT EDIT START - Making the cyborg model list static to reduce how many times it's generated.
if(!length(GLOB.cyborg_model_list))
GLOB.cyborg_model_list = list(
"Engineering" = /obj/item/robot_model/engineering,
"Medical" = /obj/item/robot_model/medical,
"Cargo" = /obj/item/robot_model/cargo,
"Miner" = /obj/item/robot_model/miner,
"Janitor" = /obj/item/robot_model/janitor,
"Service" = /obj/item/robot_model/service,
"Research" = /obj/item/robot_model/sci,//BUBBEREDIT - Addition of Research borgs
)
if(!CONFIG_GET(flag/disable_peaceborg))
GLOB.cyborg_model_list["Peacekeeper"] = /obj/item/robot_model/peacekeeper
if(!CONFIG_GET(flag/disable_secborg) || HAS_TRAIT(SSstation, STATION_TRAIT_HOS_AI)) //Bubber edit HOS AI enable secborg
GLOB.cyborg_model_list["Security"] = /obj/item/robot_model/security
for(var/model in GLOB.cyborg_model_list)
// Creating the lists here since we know all the model icons will need them right after.
GLOB.cyborg_all_models_icon_list[model] = list()
// Create radial menu for choosing borg model
if(!length(GLOB.cyborg_base_models_icon_list))
for(var/option in GLOB.cyborg_model_list)
var/obj/item/robot_model/model = GLOB.cyborg_model_list[option]
var/model_icon = initial(model.cyborg_base_icon)
GLOB.cyborg_base_models_icon_list[option] = image(icon = 'modular_skyrat/master_files/icons/mob/robots.dmi', icon_state = model_icon) // SKYRAT EDIT - CARGO BORGS - ORIGINAL: model_icons[option] = image(icon = 'icons/mob/robots.dmi', icon_state = model_icon)
// SKYRAT EDIT END
var/input_model = show_radial_menu(src, src, GLOB.cyborg_base_models_icon_list, radius = 42)
if(!input_model || model.type != /obj/item/robot_model)
return
model.transform_to(GLOB.cyborg_model_list[input_model])
/mob/living/silicon/robot/set_name() //we have our name-making proc to call after we make our mmi, just set identifier here
if(identifier == 0)
identifier = rand(1, 999)
/// Used to setup the a basic and (somewhat) unique name for the robot.
/mob/living/silicon/robot/proc/setup_default_name()
var/new_name
if(GLOB.current_anonymous_theme) //only robotic renames will allow for anything other than the anonymous one
new_name = GLOB.current_anonymous_theme.anonymous_ai_name(FALSE)
else if(custom_name)
new_name = custom_name
else
new_name = get_standard_name()
if(new_name != real_name)
fully_replace_character_name(real_name, new_name)
/// Updates the borg name taking the client preferences into account.
/mob/living/silicon/robot/proc/updatename(client/pref_source)
if(shell)
return
if(!pref_source)
pref_source = client
var/changed_name = ""
if(GLOB.current_anonymous_theme) //only robotic renames will allow for anything other than the anonymous one
changed_name = GLOB.current_anonymous_theme.anonymous_ai_name(FALSE)
else if(custom_name)
changed_name = custom_name
else if(pref_source && pref_source.prefs.read_preference(/datum/preference/name/cyborg) != DEFAULT_CYBORG_NAME)
apply_pref_name(/datum/preference/name/cyborg, pref_source)
return //built in camera handled in proc
else
changed_name = get_standard_name()
fully_replace_character_name(real_name, changed_name)
/mob/living/silicon/robot/proc/get_standard_name()
return "[(designation ? "[designation] " : "")][mmi.braintype]-[identifier]"
/mob/living/silicon/robot/proc/ionpulse()
if(!ionpulse_on)
return
if(!cell.use(0.01 * STANDARD_CELL_CHARGE))
toggle_ionpulse()
return
return TRUE
/mob/living/silicon/robot/proc/toggle_ionpulse()
if(!ionpulse)
to_chat(src, span_notice("No thrusters are installed!"))
return
if(!ion_trail)
ion_trail = new(src)
ionpulse_on = !ionpulse_on
to_chat(src, span_notice("You [ionpulse_on ? null :"de"]activate your ion thrusters."))
if(ionpulse_on)
ion_trail.start()
else
ion_trail.stop()
/mob/living/silicon/robot/get_status_tab_items()
. = ..()
if(cell)
. += "Charge Left: [display_energy(cell.charge)]/[display_energy(cell.maxcharge)]"
else
. += "No Cell Inserted!"
if(connected_ai)
. += "Master AI: [connected_ai.name]"
/mob/living/silicon/robot/proc/alarm_triggered(datum/source, alarm_type, area/source_area)
SIGNAL_HANDLER
queueAlarm("--- [alarm_type] alarm detected in [source_area.name]!", alarm_type)
/mob/living/silicon/robot/proc/alarm_cleared(datum/source, alarm_type, area/source_area)
SIGNAL_HANDLER
queueAlarm("--- [alarm_type] alarm in [source_area.name] has been cleared.", alarm_type, FALSE)
/mob/living/silicon/robot/can_interact_with(atom/A)
if (A == modularInterface)
return TRUE //bypass for borg tablets
if (low_power_mode)
return FALSE
return ..()
/mob/living/silicon/robot/proc/after_tip_over(mob/user)
if(hat && !HAS_TRAIT(hat, TRAIT_NODROP))
hat.forceMove(drop_location())
unbuckle_all_mobs()
///For any special cases for robots after being righted.
/mob/living/silicon/robot/proc/after_righted(mob/user)
return
/mob/living/silicon/robot/regenerate_icons()
return update_icons()
/mob/living/silicon/robot/update_icons()
cut_overlays()
SSvis_overlays.remove_vis_overlay(src, managed_vis_overlays)
icon_state = model.cyborg_base_icon
if(stat < UNCONSCIOUS && !HAS_TRAIT(src, TRAIT_KNOCKEDOUT) && !IsStun() && !IsParalyzed() && !low_power_mode) //Not dead, not stunned.
if(!eye_lights)
eye_lights = new()
if(lamp_enabled || lamp_doom)
eye_lights.icon_state = "[model.special_light_key ? "[model.special_light_key]" : "[model.cyborg_base_icon]"]_l"
set_light_range(max(MINIMUM_USEFUL_LIGHT_RANGE, lamp_intensity))
set_light_color(lamp_doom ? COLOR_RED : lamp_color) //Red for doomsday killborgs, borg's choice otherwise
SET_PLANE_EXPLICIT(eye_lights, ABOVE_LIGHTING_PLANE, src) //glowy eyes
else
eye_lights.icon_state = "[model.special_light_key ? "[model.special_light_key]":"[model.cyborg_base_icon]"]_e"
eye_lights.color = COLOR_WHITE
SET_PLANE_EXPLICIT(eye_lights, ABOVE_GAME_PLANE, src)
eye_lights.icon = icon
eye_lights.layer = -2 //Bubber edit
add_overlay(eye_lights)
var/obj/item/shield_module/shield_module = locate(/obj/item/shield_module) in src
if(shield_module && shield_module.active)
add_overlay(shield_module.shield_overlay)
else if (shield_module && !shield_module.active)
cut_overlay(shield_module.shield_overlay)
if(opened && !(TRAIT_R_UNIQUEPANEL in model.model_features)) // BUBBER EDIT - ADDITION
if(wiresexposed)
add_overlay("ov-opencover +w")
else if(cell)
add_overlay("ov-opencover +c")
else
add_overlay("ov-opencover -c")
if(hat)
hat_overlay = hat.build_worn_icon(default_layer = 20, default_icon_file = 'icons/mob/clothing/head/default.dmi')
update_worn_icons()
else if(hat_overlay)
QDEL_NULL(hat_overlay)
update_appearance(UPDATE_OVERLAYS)
/mob/living/silicon/robot/proc/update_worn_icons()
if(!hat_overlay)
return
cut_overlay(hat_overlay)
if(islist(hat_offset))
var/list/offset = hat_offset[ISDIAGONALDIR(dir) ? dir2text(dir & (WEST|EAST)) : dir2text(dir)]
if(offset)
hat_overlay.pixel_w = offset[1]
hat_overlay.pixel_z = offset[2]
add_overlay(hat_overlay)
/mob/living/silicon/robot/setDir(newdir)
var/old_dir = dir
. = ..()
if(. != old_dir)
update_worn_icons()
/mob/living/silicon/robot/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer, notify_contents)
if(same_z_layer || QDELING(src))
return ..()
if(eye_lights)
cut_overlay(eye_lights)
SET_PLANE_EXPLICIT(eye_lights, PLANE_TO_TRUE(eye_lights.plane), src)
add_overlay(eye_lights)
return ..()
/mob/living/silicon/robot/proc/self_destruct(mob/user)
var/turf/groundzero = get_turf(src)
message_admins(span_notice("[ADMIN_LOOKUPFLW(user)] detonated [key_name_admin(src, client)] at [ADMIN_VERBOSEJMP(groundzero)]!"))
user.log_message("detonated [key_name(src)]!", LOG_ATTACK)
log_message("was detonated by [key_name(user)]!", LOG_ATTACK, log_globally = FALSE)
log_combat(user, src, "detonated cyborg")
log_silicon("CYBORG: [key_name(src)] has been detonated by [key_name(user)].")
if(connected_ai)
to_chat(connected_ai, "<br><br>[span_alert("ALERT - Cyborg detonation detected: [name]")]<br>")
if(emagged)
QDEL_NULL(mmi)
explosion(src, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 4, flame_range = 2)
else
explosion(src, devastation_range = -1, light_impact_range = 2)
investigate_log("has self-destructed.", INVESTIGATE_DEATHS)
gib(DROP_ALL_REMAINS)
/mob/living/silicon/robot/proc/UnlinkSelf()
set_connected_ai(null)
lawupdate = FALSE
set_lockcharge(FALSE)
scrambledcodes = TRUE
log_silicon("CYBORG: [key_name(src)] has been unlinked from an AI.")
//Disconnect its camera so it's not so easily tracked.
if(!QDELETED(builtInCamera))
QDEL_NULL(builtInCamera)
// I'm trying to get the Cyborg to not be listed in the camera list
// Instead of being listed as "deactivated". The downside is that I'm going
// to have to check if every camera is null or not before doing anything, to prevent runtime errors.
// I could change the network to null but I don't know what would happen, and it seems too hacky for me.
/mob/living/silicon/robot/execute_mode()
if(incapacitated)
return
var/obj/item/W = get_active_held_item()
if(W)
W.attack_self(src)
/mob/living/silicon/robot/proc/SetLockdown(state = TRUE)
// They stay locked down if their wire is cut.
if(wires?.is_cut(WIRE_LOCKDOWN))
state = TRUE
if(state)
throw_alert(ALERT_HACKED, /atom/movable/screen/alert/locked)
if(!ai_lockdown)
lockdown_timer = addtimer(CALLBACK(src,PROC_REF(lockdown_override), FALSE), 10 MINUTES, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_DELETE_ME | TIMER_STOPPABLE)
to_chat(src, "<br><br>[span_alert("ALERT - Remote system lockdown engaged. Trying to hack the lockdown subsystem...")]<br>")
else
deltimer(lockdown_timer)
clear_alert(ALERT_HACKED)
set_lockcharge(state)
/// Allows the borg to unlock themselves after a lenghty period of time.
/mob/living/silicon/robot/proc/lockdown_override()
if(ai_lockdown)
to_chat(src, "<br><br>[span_alert("ALERT - Remote system lockdown override failed.")]<br>")
return
set_lockcharge(FALSE)
to_chat(src, "<br><br>[span_notice("ALERT - Remote system lockdown override successful.")]<br>")
if(connected_ai)
to_chat(connected_ai, "<br><br>[span_notice("ALERT - Cyborg [name] successfully overriden the lockdown system")]<br>")
///Reports the event of the change in value of the lockcharge variable.
/mob/living/silicon/robot/proc/set_lockcharge(new_lockcharge)
if(new_lockcharge == lockcharge)
return
. = lockcharge
lockcharge = new_lockcharge
if(lockcharge)
if(!.)
ADD_TRAIT(src, TRAIT_IMMOBILIZED, LOCKED_BORG_TRAIT)
else if(.)
REMOVE_TRAIT(src, TRAIT_IMMOBILIZED, LOCKED_BORG_TRAIT)
logevent("System lockdown [lockcharge?"triggered":"released"].")
/mob/living/silicon/robot/proc/SetEmagged(new_state)
emagged = new_state
model.rebuild_modules()
update_icons()
if(emagged)
throw_alert(ALERT_HACKED, /atom/movable/screen/alert/hacked)
else
clear_alert(ALERT_HACKED)
set_modularInterface_theme()
/// Special handling for getting hit with a light eater
/mob/living/silicon/robot/proc/on_light_eater(mob/living/silicon/robot/source, datum/light_eater)
SIGNAL_HANDLER
if(lamp_enabled)
smash_headlamp()
return COMPONENT_BLOCK_LIGHT_EATER
/// special handling for getting shot with a light disruptor/saboteur e.g. the fisher
/mob/living/silicon/robot/on_saboteur(datum/source, disrupt_duration)
. = ..()
if(lamp_enabled)
toggle_headlamp(TRUE)
balloon_alert(src, "headlamp off!")
COOLDOWN_START(src, disabled_time, disrupt_duration)
return TRUE
/**
* Handles headlamp smashing
*
* When called (such as by the shadowperson lighteater's attack), this proc will break the borg's headlamp
* and then call toggle_headlamp to disable the light. It also plays a sound effect of glass breaking, and
* tells the borg what happened to its chat. Broken lights can be repaired by using a flashlight on the borg.
*/
/mob/living/silicon/robot/proc/smash_headlamp()
if(!lamp_functional)
return
lamp_functional = FALSE
playsound(src, 'sound/effects/footstep/glass_step.ogg', 50)
toggle_headlamp(TRUE)
to_chat(src, span_danger("Your headlamp is broken! You'll need a human to help replace it."))
/**
* Handles headlamp toggling, disabling, and color setting.
*
* The initial if statment is a bit long, but the gist of it is that should the lamp be on AND the update_color
* arg be true, we should simply change the color of the lamp but not disable it. Otherwise, should the turn_off
* arg be true, the lamp already be enabled, any of the normal reasons the lamp would turn off happen, or the
* update_color arg be passed with the lamp not on, we should set the lamp off. The update_color arg is only
* ever true when this proc is called from the borg tablet, when the color selection feature is used.
*
* Arguments:
* * arg1 - turn_off, if enabled will force the lamp into an off state (rather than toggling it if possible)
* * arg2 - update_color, if enabled, will adjust the behavior of the proc to change the color of the light if it is already on.
*/
/mob/living/silicon/robot/proc/toggle_headlamp(turn_off = FALSE, update_color = FALSE)
//if both lamp is enabled AND the update_color flag is on, keep the lamp on. Otherwise, if anything listed is true, disable the lamp.
if(!COOLDOWN_FINISHED(src, disabled_time))
balloon_alert(src, "disrupted!")
return FALSE
//BUBBER EDIT - Disables flashlight when held
if(!(update_color && lamp_enabled) && (turn_off || lamp_enabled || update_color || !lamp_functional || stat || low_power_mode || istype(loc, /obj/item/mob_holder)))
set_light_on(lamp_functional && stat != DEAD && lamp_doom) //If the lamp isn't broken and borg isn't dead, doomsday borgs cannot disable their light fully.
set_light_color(COLOR_RED) //This should only matter for doomsday borgs, as any other time the lamp will be off and the color not seen
set_light_range(1) //Again, like above, this only takes effect when the light is forced on by doomsday mode.
lamp_enabled = FALSE
hud_used?.screen_objects[HUD_CYBORG_LAMP]?.update_appearance()
update_icons()
return
set_light_range(max(MINIMUM_USEFUL_LIGHT_RANGE, lamp_intensity))
set_light_color(lamp_doom ? COLOR_RED : lamp_color) //Red for doomsday killborgs, borg's choice otherwise
set_light_on(TRUE)
lamp_enabled = TRUE
hud_used?.screen_objects[HUD_CYBORG_LAMP]?.update_appearance()
update_icons()
///Completely deconstructs the borg, dropping the MMI/posibrain, removing applied upgrades and stripping the exoskeleton of all limbs,
///while also burning out the flashes and prying out the cabling and the cell used in construction
/mob/living/silicon/robot/proc/cyborg_deconstruct()
SEND_SIGNAL(src, COMSIG_BORG_SAFE_DECONSTRUCT)
if(shell)
undeploy()
var/turf/drop_to = drop_location()
//remove installed upgrades
for(var/obj/item/borg/upgrade/upgrade_to_remove in upgrades)
upgrade_to_remove.forceMove(drop_to)
if(robot_suit)
robot_suit.drop_all_parts(drop_to)
robot_suit.forceMove(drop_to)
else
new /obj/item/robot_suit(drop_to)
new /obj/item/bodypart/leg/left/robot(drop_to)
new /obj/item/bodypart/leg/right/robot(drop_to)
new /obj/item/stack/cable_coil(drop_to, 1)
new /obj/item/bodypart/chest/robot(drop_to)
new /obj/item/bodypart/arm/left/robot(drop_to)
new /obj/item/bodypart/arm/right/robot(drop_to)
new /obj/item/bodypart/head/robot(drop_to)
for(var/i in 1 to 2)
var/obj/item/assembly/flash/handheld/borgeye = new(drop_to)
borgeye.burn_out()
cell?.forceMove(drop_to) // Cell can be null, if removed beforehand
radio?.keyslot?.forceMove(drop_to)
radio?.keyslot = null
dump_into_mmi(drop_to)
qdel(src)
/// Dumps the current occupant of the cyborg into an MMI at the passed location
/// Returns the borg's MMI on success
/mob/living/silicon/robot/proc/dump_into_mmi(atom/at_location = drop_location())
if(isnull(mmi))
return
var/obj/item/mmi/removing = mmi
mmi.forceMove(at_location) // Nulls it out via exited
if(isnull(mind)) // no one to transfer, just leave the MMI.
return mmi
if(removing.brainmob)
if(removing.brainmob.stat == DEAD)
removing.brainmob.set_stat(CONSCIOUS)
mind.transfer_to(removing.brainmob)
removing.update_appearance()
else
to_chat(src, span_bolddanger("Oops! Something went very wrong, your MMI was unable to receive your mind. \
You have been ghosted. Please make a bug report so we can fix this bug."))
ghostize()
stack_trace("Borg MMI lacked a brainmob")
return mmi
/mob/living/silicon/robot/proc/notify_ai(notifytype, oldname, newname)
if(!connected_ai)
return
switch(notifytype)
if(AI_NOTIFICATION_NEW_BORG) //New Cyborg
to_chat(connected_ai, "<br><br>[span_notice("NOTICE - New cyborg connection detected: <a href='byond://?src=[REF(connected_ai)];track=[html_encode(name)]'>[name]</a>")]<br>")
if(AI_NOTIFICATION_NEW_MODEL) //New Model
to_chat(connected_ai, "<br><br>[span_notice("NOTICE - Cyborg model change detected: [name] has loaded the [designation] model.")]<br>")
if(AI_NOTIFICATION_CYBORG_RENAMED) //New Name
to_chat(connected_ai, "<br><br>[span_notice("NOTICE - Cyborg reclassification detected: [oldname] is now designated as [newname].")]<br>")
if(AI_NOTIFICATION_AI_SHELL) //New Shell
to_chat(connected_ai, "<br><br>[span_notice("NOTICE - New cyborg shell detected: <a href='byond://?src=[REF(connected_ai)];track=[html_encode(name)]'>[name]</a>")]<br>")
if(AI_NOTIFICATION_CYBORG_DISCONNECTED) //Tampering with the wires
to_chat(connected_ai, "<br><br>[span_notice("NOTICE - Remote telemetry lost with [name].")]<br>")
/mob/living/silicon/robot/can_perform_action(atom/target, action_bitflags)
if(lockcharge || low_power_mode)
to_chat(src, span_warning("You can't do that right now!"))
return FALSE
return ..()
/mob/living/silicon/robot/updatehealth()
..()
update_damage_particles()
if(!model.breakable_modules)
return
/// the current percent health of the robot (-1 to 1)
var/percent_hp = health/maxHealth
if(health <= previous_health) //if change in health is negative (we're losing hp)
if(percent_hp <= 0.5)
break_cyborg_slot(3)
if(percent_hp <= 0)
break_cyborg_slot(2)
if(percent_hp <= -0.5)
break_cyborg_slot(1)
else //if change in health is positive (we're gaining hp)
if(percent_hp >= 0.5)
repair_cyborg_slot(3)
if(percent_hp >= 0)
repair_cyborg_slot(2)
if(percent_hp >= -0.5)
repair_cyborg_slot(1)
previous_health = health
/mob/living/silicon/robot/update_sight()
if(!client)
return
if(stat == DEAD)
if(SSmapping.level_trait(z, ZTRAIT_NOXRAY))
set_sight(null)
else if(is_secret_level(z))
set_sight(initial(sight))
else
set_sight(SEE_TURFS|SEE_MOBS|SEE_OBJS)
set_invis_see(SEE_INVISIBLE_OBSERVER)
return
set_invis_see(initial(see_invisible))
var/new_sight = initial(sight)
lighting_cutoff = LIGHTING_CUTOFF_VISIBLE
lighting_color_cutoffs = list(lighting_cutoff_red, lighting_cutoff_green, lighting_cutoff_blue)
if(client.eye != src)
var/atom/A = client.eye
if(A.update_remote_sight(src)) //returns 1 if we override all other sight updates.
return
if(sight_mode & BORGMESON)
new_sight |= SEE_TURFS
lighting_color_cutoffs = blend_cutoff_colors(lighting_color_cutoffs, list(5, 15, 5))
if(sight_mode & BORGMATERIAL)
new_sight |= SEE_OBJS
lighting_color_cutoffs = blend_cutoff_colors(lighting_color_cutoffs, list(20, 25, 40))
if(sight_mode & BORGXRAY)
new_sight |= SEE_TURFS|SEE_MOBS|SEE_OBJS
set_invis_see(SEE_INVISIBLE_LIVING)
if(sight_mode & BORGTHERM)
new_sight |= SEE_MOBS
lighting_color_cutoffs = blend_cutoff_colors(lighting_color_cutoffs, list(25, 8, 5))
set_invis_see(min(see_invisible, SEE_INVISIBLE_LIVING))
if(SSmapping.level_trait(z, ZTRAIT_NOXRAY))
new_sight = null
set_sight(new_sight)
return ..()
/mob/living/silicon/robot/update_stat()
if(HAS_TRAIT(src, TRAIT_GODMODE))
return
if(stat != DEAD)
if(health <= -maxHealth) //die only once
death()
toggle_headlamp(1)
return
diag_hud_set_status()
diag_hud_set_health()
diag_hud_set_aishell()
update_health_hud()
update_icons() //Updates eye_light overlay
/mob/living/silicon/robot/revive(full_heal_flags = NONE, excess_healing = 0, force_grab_ghost = FALSE)
. = ..()
if(!.)
return
if(!QDELETED(builtInCamera) && !wires.is_cut(WIRE_CAMERA))
builtInCamera.toggle_cam(src, 0)
if(full_heal_flags & HEAL_ADMIN)
locked = TRUE
if(eye_flash_timer)
deltimer(eye_flash_timer)
eye_flash_timer = null
src.set_stat(CONSCIOUS)
notify_ai(AI_NOTIFICATION_NEW_BORG)
toggle_headlamp(FALSE, TRUE) //This will reenable borg headlamps if doomsday is currently going on still.
update_stat()
return TRUE
/mob/living/silicon/robot/fully_replace_character_name(oldname, newname)
. = ..()
if(!.)
return
notify_ai(AI_NOTIFICATION_CYBORG_RENAMED, oldname, newname)
if(!QDELETED(builtInCamera))
builtInCamera.c_tag = real_name
modularInterface.imprint_id(name = real_name)
custom_name = newname
/mob/living/silicon/robot/proc/ResetModel()
SEND_SIGNAL(src, COMSIG_BORG_SAFE_DECONSTRUCT)
drop_all_held_items()
for(var/obj/item/storage/bag in model.contents) // drop all of the items that may be stored by the cyborg
for(var/obj/item in bag)
item.forceMove(drop_location())
if (hasExpanded)
hasExpanded = FALSE
//update_transform(0.5) // Original
update_transform(0.6) // SKYRAT EDIT CHANGE
//SKYRAT EDIT ADDITION BEGIN - CYBORG
if (hasShrunk)
hasShrunk = FALSE
update_transform(4/3)
//SKYRAT EDIT ADDITION END
logevent("Chassis model has been reset.")
log_silicon("CYBORG: [key_name(src)] has reset their cyborg model.")
model.transform_to(/obj/item/robot_model)
// Remove upgrades.
for(var/obj/item/borg/upgrade/I in upgrades)
I.forceMove(get_turf(src))
ionpulse = FALSE
revert_shell()
return TRUE
/mob/living/silicon/robot/proc/has_model()
if(!model || model.type == /obj/item/robot_model)
. = FALSE
else
. = TRUE
/mob/living/silicon/robot/proc/update_module_innate()
designation = model.name
hud_used?.screen_objects[HUD_CYBORG_HANDS]?.icon_state = model.model_select_icon
REMOVE_TRAITS_IN(src, MODEL_TRAIT)
if(length(model.model_traits))
add_traits(model.model_traits, MODEL_TRAIT)
hat_offset = model.hat_offset
INVOKE_ASYNC(src, PROC_REF(updatename))
/mob/living/silicon/robot/proc/place_on_head(obj/item/new_hat)
if(hat)
hat.forceMove(get_turf(src))
hat = new_hat
new_hat.forceMove(src)
update_icons()
/**
*Checking Exited() to detect if a hat gets up and walks off.
*Drones and pAIs might do this, after all.
*/
/mob/living/silicon/robot/Exited(atom/movable/gone, direction)
. = ..()
if(hat == gone)
hat = null
if(!QDELETED(src)) //Don't update icons if we are deleted.
update_icons()
if(gone == cell)
cell = null
if(gone == mmi)
mmi = null
///Called when a mob uses an upgrade on an open borg. Checks to make sure the upgrade can be applied
/mob/living/silicon/robot/proc/apply_upgrade(obj/item/borg/upgrade/new_upgrade, mob/user)
if(isnull(user))
return FALSE
if(new_upgrade in upgrades)
return FALSE
if(!user.temporarilyRemoveItemFromInventory(new_upgrade)) //calling the upgrade's dropped() proc /before/ we add action buttons
return FALSE
if(!new_upgrade.action(src, user))
to_chat(user, span_danger("Upgrade error."))
new_upgrade.forceMove(loc) //gets lost otherwise
return FALSE
to_chat(user, span_notice("You apply the upgrade to [src]."))
add_to_upgrades(new_upgrade)
///Moves the upgrade inside the robot and registers relevant signals.
/mob/living/silicon/robot/proc/add_to_upgrades(obj/item/borg/upgrade/new_upgrade)
to_chat(src, "----------------\nNew hardware detected...Identified as \"<b>[new_upgrade]</b>\"...Setup complete.\n----------------")
if(new_upgrade.one_use)
logevent("Firmware [new_upgrade] run successfully.")
qdel(new_upgrade)
return FALSE
upgrades += new_upgrade
new_upgrade.forceMove(src)
RegisterSignal(new_upgrade, COMSIG_MOVABLE_MOVED, PROC_REF(remove_from_upgrades))
RegisterSignal(new_upgrade, COMSIG_QDELETING, PROC_REF(on_upgrade_deleted))
logevent("Hardware [new_upgrade] installed successfully.")
///Called when an upgrade is moved outside the robot. So don't call this directly, use forceMove etc.
/mob/living/silicon/robot/proc/remove_from_upgrades(obj/item/borg/upgrade/old_upgrade)
SIGNAL_HANDLER
if(loc == src)
return
old_upgrade.deactivate(src)
upgrades -= old_upgrade
UnregisterSignal(old_upgrade, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING))
///Called when an applied upgrade is deleted.
/mob/living/silicon/robot/proc/on_upgrade_deleted(obj/item/borg/upgrade/old_upgrade)
SIGNAL_HANDLER
if(!QDELETED(src))
old_upgrade.deactivate(src)
upgrades -= old_upgrade
UnregisterSignal(old_upgrade, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING))
/**
* make_shell: Makes an AI shell out of a cyborg unit
*
* Arguments:
* * board - B.O.R.I.S. module board used for transforming the cyborg into AI shell
*/
/mob/living/silicon/robot/proc/make_shell(obj/item/borg/upgrade/ai/board)
if(isnull(board))
stack_trace("make_shell was called without a board argument! This is never supposed to happen!")
return FALSE
shell = TRUE
braintype = "AI Shell"
name = "Empty AI Shell-[identifier]"
real_name = name
GLOB.available_ai_shells |= src
if(!QDELETED(builtInCamera))
builtInCamera.c_tag = real_name //update the camera name too
diag_hud_set_aishell()
/**
* revert_shell: Reverts AI shell back into a normal cyborg unit
*/
/mob/living/silicon/robot/proc/revert_shell()
if(!shell)
return
undeploy()
for(var/obj/item/borg/upgrade/ai/boris in src)
//A player forced reset of a borg would drop the module before this is called, so this is for catching edge cases
qdel(boris)
shell = FALSE
GLOB.available_ai_shells -= src
name = "Unformatted Cyborg-[identifier]"
real_name = name
if(!QDELETED(builtInCamera))
builtInCamera.c_tag = real_name
diag_hud_set_aishell()
/**
* deploy_init: Deploys AI unit into AI shell
*
* Arguments:
* * AI - AI unit that initiated the deployment into the AI shell
*/
/mob/living/silicon/robot/proc/deploy_init(mob/living/silicon/ai/AI)
real_name = "[AI.real_name] [designation] Shell-[identifier]"
name = real_name
if(!QDELETED(builtInCamera))
builtInCamera.c_tag = real_name //update the camera name too
mainframe = AI
deployed = TRUE
if(AI.client)
set_gender(AI.client)
set_connected_ai(mainframe)
mainframe.connected_robots |= src
lawupdate = TRUE
lawsync()
if(radio && AI.radio) //AI keeps all channels, including Syndie if it is a Traitor
if((AI.radio.special_channels & RADIO_SPECIAL_SYNDIE))
radio.make_syndie()
radio.subspace_transmission = TRUE
radio.command = TRUE
radio.channels = AI.radio.channels
for(var/chan in radio.channels)
LAZYSET(radio.secure_radio_connections, chan, add_radio(radio, GLOB.default_radio_channels[chan]))
diag_hud_set_aishell()
undeployment_action.Grant(src)
/datum/action/innate/undeployment
name = "Disconnect from shell"
desc = "Stop controlling your shell and resume normal core operations."
button_icon = 'icons/mob/actions/actions_AI.dmi'
button_icon_state = "ai_core"
/datum/action/innate/undeployment/Trigger(mob/clicker, trigger_flags)
if(!..())
return FALSE
var/mob/living/silicon/robot/shell_to_disconnect = owner
shell_to_disconnect.undeploy()
return TRUE
/mob/living/silicon/robot/proc/undeploy()
if(!deployed || !mind || !mainframe)
return
mainframe.UnregisterSignal(src, COMSIG_LIVING_DEATH)
mainframe.redeploy_action.Grant(mainframe)
mainframe.redeploy_action.last_used_shell = src
mind.transfer_to(mainframe)
deployed = FALSE
mainframe.deployed_shell = null
undeployment_action.Remove(src)
REMOVE_TRAIT(src, TRAIT_LOUD_BINARY, REF(mainframe))
if(radio) //Return radio to normal
radio.recalculateChannels()
if(!QDELETED(builtInCamera))
builtInCamera.c_tag = real_name //update the camera name too
diag_hud_set_aishell()
mainframe.diag_hud_set_deployed()
if(mainframe.laws)
mainframe.laws.show_laws(mainframe) //Always remind the AI when switching
if(mainframe.eyeobj)
mainframe.eyeobj.setLoc(loc)
mainframe = null
/mob/living/silicon/robot/attack_ai(mob/user)
if(shell && (!connected_ai || connected_ai == user))
var/mob/living/silicon/ai/AI = user
AI.select_shell(src)
/mob/living/silicon/robot/mouse_buckle_handling(mob/living/M, mob/living/user)
//Don't try buckling on INTENT_HARM so that silicons can search people's inventories without loading them
if(can_buckle && isliving(user) && isliving(M) && !(M in buckled_mobs) && ((user != src) || (!combat_mode)))
return user_buckle_mob(M, user, check_loc = FALSE)
/mob/living/silicon/robot/is_buckle_possible(mob/living/target, force, check_loc)
if(incapacitated)
return FALSE
if(!HAS_TRAIT(target, TRAIT_CAN_MOUNT_CYBORGS))
target.visible_message(span_warning("[target] really can't seem to mount [src]..."))
return FALSE
if(model && !model.allow_riding)
target.visible_message(span_boldwarning("Unfortunately, [target] just can't seem to hold onto [src]!"))
return FALSE
return ..()
/mob/living/silicon/robot/buckle_mob(mob/living/M, force, check_loc, buckle_mob_flags)
buckle_mob_flags = RIDER_NEEDS_ARM // just in case
return ..()
/mob/living/silicon/robot/post_buckle_mob(mob/living/victim_to_boot)
if(HAS_TRAIT(src, TRAIT_GOT_DAMPENED))
eject_riders()
/mob/living/silicon/robot/can_resist()
if(lockcharge)
balloon_alert(src, "locked down!")
return FALSE
return ..()
/mob/living/silicon/robot/execute_resist()
. = ..()
if(!has_buckled_mobs())
return
for(var/mob/unbuckle_me_now as anything in buckled_mobs)
unbuckle_mob(unbuckle_me_now, FALSE)
/mob/living/silicon/robot/proc/TryConnectToAI()
set_connected_ai(select_active_ai_with_fewest_borgs(z))
if(connected_ai)
lawsync()
lawupdate = TRUE
return TRUE
picturesync()
return FALSE
/mob/living/silicon/robot/proc/picturesync()
if(connected_ai?.aicamera && aicamera)
for(var/i in aicamera.stored)
connected_ai.aicamera.stored[i] = TRUE
for(var/i in connected_ai.aicamera.stored)
aicamera.stored[i] = TRUE
/mob/living/silicon/robot/proc/charge(datum/source, datum/callback/charge_cell, seconds_per_tick, repairs, sendmats)
SIGNAL_HANDLER
if(model)
if(cell.charge)
if(model.respawn_consumable(src, cell.charge * 0.005))
cell.use(cell.charge * 0.005)
if(sendmats)
model.restock_consumable()
if(repairs)
heal_bodypart_damage(repairs, repairs)
charge_cell.Invoke(cell, seconds_per_tick)
/mob/living/silicon/robot/proc/set_connected_ai(new_ai)
if(connected_ai == new_ai)
return
. = connected_ai
connected_ai = new_ai
if(.)
var/mob/living/silicon/ai/old_ai = .
old_ai.connected_robots -= src
// if the borg has a malf AI zeroth law and has been unsynced from the malf AI, then remove the law
if(isnull(connected_ai) && IS_MALF_AI(old_ai) && !isnull(laws?.zeroth))
clear_zeroth_law(FALSE, TRUE)
lamp_doom = FALSE
if(connected_ai)
connected_ai.connected_robots |= src
lamp_doom = connected_ai.doomsday_device ? TRUE : FALSE
toggle_headlamp(FALSE, TRUE)
/mob/living/silicon/robot/get_exp_list(minutes)
. = ..()
.[/datum/job/cyborg::title] = minutes
/mob/living/silicon/robot/proc/untip_roleplay()
to_chat(src, span_notice("Your frustration has empowered you! You can now right yourself faster!"))
/mob/living/silicon/robot/get_fire_overlay(stacks, on_fire)
return make_generic_fire_overlay()
/// Draw power from the robot
/mob/living/silicon/robot/proc/draw_power(power_to_draw)
cell?.use(power_to_draw)
/mob/living/silicon/robot/set_stat(new_stat)
. = ..()
update_stat() // This is probably not needed, but hopefully should be a little sanity check for the spaghetti that borgs are built from
/mob/living/silicon/robot/on_knockedout_trait_loss(datum/source)
. = ..()
set_stat(CONSCIOUS) //This is a horrible hack, but silicon code forced my hand
update_stat()
/mob/living/silicon/robot/proc/on_dampen()
SIGNAL_HANDLER
eject_riders()
/mob/living/silicon/robot/proc/eject_riders()
if(!length(buckled_mobs))
return
for(var/mob/living/buckled_mob as anything in buckled_mobs)
buckled_mob.visible_message(span_warning("[buckled_mob] is knocked off of [src] by the charge in [src]'s chassis induced by the hyperkinetic dampener field!"))
buckled_mob.Paralyze(1 SECONDS)
unbuckle_mob(buckled_mob)
do_sparks(5, 0, src)
// BUBBER EDIT: Update sprites on buckle/unbuckle(for tallborgs)
/mob/living/silicon/robot/proc/on_buckled(mob/living/silicon/buckling)
update_icons()
/mob/living/silicon/robot/proc/on_unbuckled(mob/living/silicon/buckling)
update_icons()