Files
FenodyreeAvandGitHub 21b9d5861a Fixes bluespace inhibitor interaction with telescience (#22967)
Bluespace inhibitors prevent you from teleporting out of them in a
square.
They prevent you teleporting into them in a circle.

Previously, if you got very unlucky, you could teleport into an area but
not back out again.

This makes both checks a square.

It also drops you one additional tile further out of the teleport zone,
so if you step off your portal in the direction of the inhibitor, it
doesn't destroy the portal.

Telescience portals also didn't care and would form inside the inhibitor
radius.

changes:
- bugfix: "Fixes bluespace inhibitors inconsistantly blocking teleports
in a square, but finding the alternate desination in a circle. Its all
square now."
- bugfix: "Fixes telescience portals not respecting bluespace
inhibitors."
2026-08-02 01:24:10 +00:00

621 lines
19 KiB
Plaintext

/obj/structure/machinery/computer/telescience
name = "\improper Telepad Control Console"
desc = "Used to create bluespace portals using the telescience telepad."
icon_screen = "teleport"
icon_keyboard = "lightblue_key"
icon_keyboard_emis = "lightblue_key_mask"
light_color = LIGHT_COLOR_BLUE
circuit = /obj/item/circuitboard/telesci_console
var/sending = 1
/**
* The telepad that this console controls
*/
var/obj/structure/machinery/telepad/telepad = null
var/temp_msg = "Telescience control console initialized.<BR>Welcome."
// VARIABLES //
///How many teleports left until it becomes uncalibrated
var/teles_left
///Projectile data, info about the latest teleport/aim that was done
var/datum/projectile_data/last_tele_data = null
///The zlevel aim offset, that was inputted by the user as a target
var/zlevel_offset = 0
///The Zlevel that is being targeted, aka the real one
var/target_zlevel
///The rotation that was inputted by the user as a target
var/rotation = 0
///The angle that was inputted by the user as a target
var/angle = 45
///The power that was inputted by the user as a target
var/power = 5
///The power offset
var/power_off
///The rotation offset
var/rotation_off
//var/angle_off
///The target of the last teleportation, a `/turf`
var/turf/last_target
// Based on the power used
var/teleport_cooldown = 0 // every index requires a bluespace crystal
/**
* A `/list` of power options that this console provides, for the projectile portal calculation
*
* Each added telecrystal (in `crystals`) unlocks an additional option, up to `max_crystals`
*/
var/list/power_options = list(5, 10, 20, 25, 30, 40, 50, 80, 100)
///Boolean, if we're teleporting (aka opening a portal) right now
var/teleporting = FALSE
var/starting_crystals = 0 //Edit this on the map, seriously.
var/max_crystals = 5
/**
* A `/list` of `/obj/item/bluespace_crystal` that this console contains
*/
var/list/obj/item/bluespace_crystal/crystals = list()
///A GPS that this console contains
var/obj/item/gps/inserted_gps
///A list of Zlevels that belong to the visitable (generally a ship) we are in
var/list/our_zlevels = list()
/**
* A list of currently known Zlevels translations below our ship/visitable
*
* K -> String, the fake zlevel that can be aimed at, it's an absolute number (the console will show it as negative),
* starts at the offset of the lowest zlevel of the visitable the console is in
* (eg: console at the second zlevel of a ship, with one zlevel below it of the same ship, this will start with an index of 2, the console will show it as -2)
*
* V -> Number, the actual zlevel
*/
var/list/overmap_contacts_zlevels = list()
/**
* The portal that is the origin, above the telepad
*
* Do *not* set this directly, use `set_origin_portal()`
*/
var/obj/effect/portal/origin_portal
/**
* The portal that is the destination
*
* Do *not* set this directly, use `set_destination_portal()`
*/
var/obj/effect/portal/destination_portal
/obj/structure/machinery/computer/telescience/feedback_hints(mob/user, distance, is_adjacent)
. += ..()
. += "There are [length(crystals) ? length(crystals) : "no"] bluespace crystal\s in the crystal slots."
/obj/structure/machinery/computer/telescience/Initialize()
. = ..()
recalibrate()
for(var/i = 1; i <= starting_crystals; i++)
crystals += new /obj/item/bluespace_crystal/artificial(null) // starting crystals
return INITIALIZE_HINT_LATELOAD
/obj/structure/machinery/computer/telescience/LateInitialize()
. = ..()
if(SSatlas.current_map.use_overmap && !linked)
var/my_sector = GLOB.map_sectors["[z]"]
if(istype(my_sector, /obj/effect/overmap/visitable))
attempt_hook_up(my_sector)
//If we got hooked up correctly, populate the list of our zlevels
if(linked)
for(var/zlevel in GLOB.map_sectors)
if(GLOB.map_sectors["[zlevel]"] == linked)
our_zlevels += text2num(zlevel)
/obj/structure/machinery/computer/telescience/Destroy()
eject()
if(inserted_gps)
inserted_gps.forceMove(loc)
inserted_gps = null
if(!QDELETED(origin_portal))
QDEL_NULL(origin_portal)
if(!QDELETED(destination_portal))
QDEL_NULL(destination_portal)
QDEL_LIST(crystals)
telepad = null
return ..()
/obj/structure/machinery/computer/telescience/attackby(obj/item/attacking_item, mob/user, params)
if(istype(attacking_item, /obj/item/bluespace_crystal))
if(length(crystals) >= max_crystals)
to_chat(user, SPAN_WARNING("There are not enough crystal slots."))
return
user.drop_item(src)
crystals += attacking_item
attacking_item.forceMove(null)
user.visible_message("[user] inserts [attacking_item] into \the [src]'s crystal slot.",
SPAN_NOTICE("You insert [attacking_item] into \the [src]'s crystal slot."))
updateDialog()
else if(istype(attacking_item, /obj/item/gps))
if(!inserted_gps)
inserted_gps = attacking_item
user.unEquip(attacking_item)
attacking_item.forceMove(src)
user.visible_message("[user] inserts [attacking_item] into \the [src]'s GPS device slot.",
SPAN_NOTICE("You insert [attacking_item] into \the [src]'s GPS device slot."))
else if(attacking_item.tool_behaviour == TOOL_MULTITOOL)
var/obj/item/multitool/M = attacking_item
if(M.buffer && istype(M.buffer, /obj/structure/machinery/telepad))
telepad = M.buffer
M.buffer = null
to_chat(user, SPAN_CAUTION("You upload the data from the [attacking_item.name]'s buffer."))
else
..()
/obj/structure/machinery/computer/telescience/attack_ai(mob/user)
if(!ai_can_interact(user))
return
src.attack_hand(user)
/obj/structure/machinery/computer/telescience/attack_hand(mob/user)
if(..())
return
interact(user)
/obj/structure/machinery/computer/telescience/interact(mob/user)
var/t
if(!telepad)
in_use = 0 //Yeah so if you deconstruct teleporter while its in the process of shooting it wont disable the console
t += "<div class='statusDisplay'>No telepad located. <BR>Please add telepad data via use of Multitool.</div><BR>"
else
if(inserted_gps)
t += "<A href='byond://?src=[REF(src)];ejectGPS=1'>Eject GPS</A>"
t += "<A href='byond://?src=[REF(src)];setMemory=1'>Set GPS memory</A>"
else
t += "<span class='linkOff'>Eject GPS</span>"
t += "<span class='linkOff'>Set GPS memory</span>"
t += "<div class='statusDisplay'>[temp_msg]</div><BR>"
t += "<A href='byond://?src=[REF(src)];setrotation=1'>Set Bearing</A>"
t += "<div class='statusDisplay'>[rotation]&deg;</div>"
t += "<A href='byond://?src=[REF(src)];setangle=1'>Set Elevation</A>"
t += "<div class='statusDisplay'>[angle]&deg;</div>"
t += "<span class='linkOn'>Set Power</span>"
t += "<div class='statusDisplay'>"
for(var/i = 1; i <= length(power_options); i++)
if(length(crystals) + telepad.efficiency < i)
t += "<span class='linkOff'>[power_options[i]]</span>"
continue
if(power == power_options[i])
t += "<span class='linkOn'>[power_options[i]]</span>"
continue
t += "<A href='byond://?src=[REF(src)];setpower=[i]'>[power_options[i]]</A>"
t += "</div>"
t += "<A href='byond://?src=[REF(src)];setz=1'>Set Vertical Offset</A>"
t += "<div class='statusDisplay'>[!isnull(zlevel_offset) ? zlevel_offset : "No Data"]</div>"
t += "<BR><A href='byond://?src=[REF(src)];send=1'>Open Portal</A>"
t += "<BR><A href='byond://?src=[REF(src)];recal=1'>Recalibrate Crystals</A> <A href='byond://?src=[REF(src)];eject=1'>Eject Crystals</A>"
// Information about the last teleport
t += "<BR><div class='statusDisplay'>"
if(!last_tele_data)
t += "No teleport data found."
else
t += "Source Location: ([last_tele_data.src_x], [last_tele_data.src_y])<BR>"
//t += "Distance: [round(last_tele_data.distance, 0.1)]m<BR>"
t += "Time: [round(last_tele_data.time, 0.1)] secs<BR>"
t += "</div>"
var/datum/browser/popup = new(user, "telesci", name, 300, 500)
popup.set_content(t)
popup.open()
return
/**
* Causes sparks to be emitted from the linked telepad
*/
/obj/structure/machinery/computer/telescience/proc/sparks()
SHOULD_NOT_SLEEP(TRUE)
if(telepad)
spark(telepad, 5, GLOB.alldirs)
else
return
/**
* Called when the portal fails to be created
*/
/obj/structure/machinery/computer/telescience/proc/telefail()
SHOULD_NOT_SLEEP(TRUE)
sparks()
visible_message(SPAN_WARNING("The telepad weakly fizzles."))
return
/obj/structure/machinery/computer/telescience/proc/check_bluespace_inhibitors(atom/target)
var/turf/target_turf = get_turf(target)
if(!target_turf)
return null
var/list/turf/good_turfs = list()
var/list/turf/bad_turfs = list()
var/list/invalid_inhibitors
for(var/found_inhibitor in GLOB.bluespace_inhibitors)
if(!istype(found_inhibitor, /obj/structure/machinery/anti_bluespace))
LAZYADD(invalid_inhibitors, found_inhibitor)
continue
var/obj/structure/machinery/anti_bluespace/AB = found_inhibitor
if(QDELETED(AB))
LAZYADD(invalid_inhibitors, found_inhibitor)
continue
if(target_turf.z != AB.z || get_dist(target_turf, AB) > 8 || (AB.stat & (NOPOWER | BROKEN)))
continue
AB.use_power_oneoff(AB.active_power_usage)
bad_turfs += RANGE_TURFS(9, get_turf(AB))
good_turfs += RANGE_TURFS(10, get_turf(AB)) //One extra tile away, so stepping off the portal in the direction of the inhibitor doesn't break the portal.
if(invalid_inhibitors)
GLOB.bluespace_inhibitors -= invalid_inhibitors
if(length(good_turfs) && length(bad_turfs))
good_turfs -= bad_turfs
if(length(good_turfs))
temp_msg = "ERROR!<BR>Interference detected. Portal off target."
return pick(good_turfs)
return target_turf
/obj/structure/machinery/computer/telescience/proc/doteleport(mob/user)
SHOULD_NOT_SLEEP(TRUE)
if(teleport_cooldown > world.time)
temp_msg = "Telepad is recharging power.<BR>Please wait [round((teleport_cooldown - world.time) / 10)] seconds."
return
if(teleporting)
temp_msg = "Telepad is in use.<BR>Please wait."
return
if(telepad)
var/truePower = clamp(power + power_off, 1, 1000)
var/trueRotation = rotation + rotation_off
var/trueAngle = clamp(angle, 1, 90)
var/datum/projectile_data/proj_data = projectile_trajectory(telepad.x, telepad.y, trueRotation, trueAngle, truePower)
last_tele_data = proj_data
var/trueX = clamp(round(proj_data.dest_x, 1), 1, world.maxx)
var/trueY = clamp(round(proj_data.dest_y, 1), 1, world.maxy)
var/spawn_time = round(proj_data.time)
var/turf/target = locate(trueX, trueY, target_zlevel)
target = check_bluespace_inhibitors(target)
last_target = target
flick("pad-beam", telepad)
if(spawn_time > 1.5 SECONDS)
playsound(telepad.loc, 'sound/weapons/flash.ogg', 25, 1)
// Wait depending on the time the projectile took to get there
teleporting = 1
temp_msg = "Powering up bluespace crystals.<BR>Please wait."
addtimer(CALLBACK(src, PROC_REF(complete_doteleport), user, target), spawn_time SECONDS)
/**
* Completes the teleportation process, after the timer has expired, to be called only by
* the timer set by `doteleport()`
*/
/obj/structure/machinery/computer/telescience/proc/complete_doteleport(mob/user, turf/target)
SHOULD_NOT_SLEEP(TRUE)
if(!telepad)
return
if(telepad.stat & NOPOWER)
return
teleporting = 0
teleport_cooldown = world.time + (power * 2)
teles_left -= 1
// use a lot of power
var/power_to_use = (power KILO WATTS)
if(overmap_contacts_zlevels["[abs(zlevel_offset)]"])
power_to_use += (300 KILO WATTS * abs(zlevel_offset))
use_power_oneoff(power_to_use)
spark(telepad, 5, GLOB.alldirs)
temp_msg = "Bluespace portal creation successful.<BR>"
if(teles_left < 10)
temp_msg += "<BR>Calibration required soon."
else
temp_msg += "Data printed below."
spark(telepad, 5, GLOB.alldirs)
var/turf/source = target
var/turf/dest = get_turf(telepad)
var/log_msg = ""
log_msg += ": [key_name(user)] has teleported "
if(sending)
source = dest
dest = target
flick("pad-beam", telepad)
playsound(telepad.loc, 'sound/weapons/emitter2.ogg', 25, 1, extrarange = 3, falloff_distance = 5)
var/total_lifespawn = 25 * length(crystals)
var/obj/effect/portal/origin = new /obj/effect/portal(dest, null, null, total_lifespawn, 0)
set_origin_portal(origin)
var/obj/effect/portal/destination = new /obj/effect/portal(source, null, null, total_lifespawn, 0)
set_destination_portal(destination)
origin.set_target(destination)
destination.set_target(origin)
origin.has_failed = FALSE
destination.has_failed = FALSE
updateDialog()
/obj/structure/machinery/computer/telescience/proc/teleport(mob/user)
//If the offset was never changed, use the current zlevel
if(isnull(target_zlevel))
target_zlevel = GET_Z(src)
if(rotation == null || angle == null)
temp_msg = "ERROR!<BR>Set a angle, rotation and sector."
return
if(power <= 0)
telefail()
temp_msg = "ERROR!<BR>No power selected!"
return
if(telepad)
if(locate(/obj/effect/portal, telepad.loc))
temp_msg = "ERROR!<BR>Bluespace portal located at \the [telepad] location!"
return
if(angle < 1 || angle > 90)
telefail()
temp_msg = "ERROR!<BR>Elevation is less than 1 or greater than 90."
return
if(teles_left > 0)
doteleport(user)
else
telefail()
temp_msg = "ERROR!<BR>Calibration required."
return
/**
* Ejects the telecrystals from the console
*/
/obj/structure/machinery/computer/telescience/proc/eject()
SHOULD_NOT_SLEEP(TRUE)
for(var/obj/item/I in crystals)
I.forceMove(src.loc)
crystals -= I
power = 0
/obj/structure/machinery/computer/telescience/Topic(href, href_list)
if(..())
return
if(!telepad)
updateDialog()
return
if(telepad.panel_open)
temp_msg = "Telepad undergoing physical maintenance operations."
if(href_list["setrotation"])
var/new_rot = input("Please input desired bearing in degrees.", name, rotation) as num
if(..()) // Check after we input a value, as they could've moved after they entered something
return
rotation = clamp(new_rot, -900, 900)
rotation = round(rotation, 0.01)
if(href_list["setangle"])
var/new_angle = input("Please input desired elevation in degrees.", name, angle) as num
if(..())
return
angle = clamp(round(new_angle, 0.1), 1, 9999)
if(href_list["setpower"])
var/index = href_list["setpower"]
index = text2num(index)
if(index != null && power_options[index])
if(length(crystals) + telepad.efficiency >= index)
power = power_options[index]
if(href_list["setz"])
var/inputed_new_z = tgui_input_number(usr, "Please input desired height offset", "Section Selection", 0, max_value = world.maxz, min_value = world.maxz*-1, round_value = TRUE)
if(..())
return
//Use a holder var to do the calculations, detached from the user input, as we vary it
var/new_z = inputed_new_z
//If we crossed the 0 mark, get a free additional zlevel (so we don't have to deal with a zlevel 0)
if(new_z < 0 && abs(new_z) >= src.z)
new_z--
//Prevent people from targeting admin levels
if(isAdminLevel(src.z + new_z))
to_chat(usr, SPAN_WARNING("Bluespace forces prevent this offset from being used."))
return
//If we are going into the negatives, those are "fake" zlevels that we use to refer
//to outside areas (like other ships, planets, sites, you name it)
//Same goes for not connected zlevels, in case we ever add them above us
if((src.z + new_z) < 0 || !AreConnectedZLevels(src.z, (src.z + new_z)))
overmap_contacts_zlevels = list()
var/list/obj/effect/overmap/visitable/already_added_visitables = list()
//Get the minimum zlevel that we need to offset to be extraneous of our ship
//eg. if we are on Zlevel 4, and our ship covers Z 1 to 5, we have 3 Zlevels
//that are ours, so we need to offset by 4 to start being outside our zlevels
//(of course those zlevels would be virtual ones, to place the translation to the real ones for overmap visitables)
var/our_zlevel = src.z
var/min_zlevel_below_us = 1
while((our_zlevel-=1) in our_zlevels)
min_zlevel_below_us++
for(var/obj/structure/machinery/computer/ship/sensors/S in SSmachinery.machinery)
//If we have ownership of this
if(linked.check_ownership(S))
//If the deep scan is toggled, we do not allow it, as the range is too large
//print a message to the user and break the loop
var/obj/structure/machinery/shipsensors/ship_sensors = S.sensor_ref?.resolve()
if((ship_sensors?.deep_scan_toggled))
to_chat(usr, SPAN_WARNING("The deep scan is currently enabled and interferes with the teleporter lock ability."))
break
for(var/obj/effect/overmap/visitable/known_visitable in S.objects_in_view)
//If not fully scanned and identified, skip
if(S.objects_in_view[known_visitable] < 100)
continue
//If it's us or some BS, skip
if(known_visitable.map_z == our_zlevels)
continue
//If we added this already, skip
if(known_visitable in already_added_visitables)
continue
//Ok, we found what would be the offset below us to start adding shit, let's add shit
for(var/contact_zlevel in known_visitable.map_z)
overmap_contacts_zlevels["[min_zlevel_below_us+length(overmap_contacts_zlevels)]"] = contact_zlevel
//Mark it as added
already_added_visitables += known_visitable
//One console gave us the zlevels, we can break the loop
if(length(overmap_contacts_zlevels))
break
if((src.z + new_z) < 0 && (num2text(abs(new_z)-1) in overmap_contacts_zlevels))
target_zlevel = overmap_contacts_zlevels["[abs(new_z)-1]"]
zlevel_offset = inputed_new_z
else
to_chat(usr, SPAN_WARNING("There is nothing targetable at this height."))
return
//Same area as us
else
if((src.z+new_z) in our_zlevels)
target_zlevel = (src.z+new_z)
zlevel_offset = inputed_new_z
else
to_chat(usr, SPAN_WARNING("There is nothing targetable at this height."))
if(href_list["ejectGPS"])
if(inserted_gps)
inserted_gps.forceMove(loc)
inserted_gps = null
if(href_list["setMemory"])
if(last_target && inserted_gps)
inserted_gps.locked_location = last_target
temp_msg = "Location saved.<BR>Target Location: ([inserted_gps.locked_location.x], [inserted_gps.locked_location.y])"
else
temp_msg = "ERROR!<BR>No data was stored."
if(href_list["send"])
sending = 1
teleport(usr)
if(href_list["recal"])
recalibrate()
sparks()
temp_msg = "NOTICE:<BR>Calibration successful."
if(href_list["eject"])
eject()
temp_msg = "NOTICE:<BR>Bluespace crystals ejected."
updateDialog()
/obj/structure/machinery/computer/telescience/process(seconds_per_tick)
if((stat & NOPOWER) || (telepad?.stat & NOPOWER))
QDEL_NULL(origin_portal)
/**
* Recomputes the parameters used for aiming
*/
/obj/structure/machinery/computer/telescience/proc/recalibrate()
SHOULD_NOT_SLEEP(TRUE)
teles_left = rand(30, 40)
//angle_off = rand(-25, 25)
power_off = rand(-4, 0)
rotation_off = rand(-10, 10)
/**
* Sets the origin portal
*
* * orig_portal - An `/obj/effect/portal` that will act as the "origin"
*/
/obj/structure/machinery/computer/telescience/proc/set_origin_portal(obj/effect/portal/orig_portal)
SHOULD_NOT_SLEEP(TRUE)
origin_portal = orig_portal
RegisterSignal(origin_portal, COMSIG_QDELETING, PROC_REF(handle_portal_qdel))
/**
* Sets the origin portal
*
* * dest_portal - An `/obj/effect/portal` that will act as the "destination"
*/
/obj/structure/machinery/computer/telescience/proc/set_destination_portal(obj/effect/portal/dest_portal)
SHOULD_NOT_SLEEP(TRUE)
destination_portal = dest_portal
RegisterSignal(destination_portal, COMSIG_QDELETING, PROC_REF(handle_portal_qdel))
/**
* Handles a portal being deleted
*/
/obj/structure/machinery/computer/telescience/proc/handle_portal_qdel()
SIGNAL_HANDLER
destination_portal = null
origin_portal = null