mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-01-10 00:43:14 +00:00
About The Pull Request
This PR adds medical wounds, new forms of injuries that people can suffer that cause debilitation and complications, and often require more than what can be found in a medkit to treat. But let's be honest, big complicated walls of text about medical changes make people's eyes glaze over easily- so I created a handy infograph to explain the basics!
Also there's a full guide here!
dreamseeker_2020-04-18_20-42-19.png
The infograph may not be fully up to date with the specifics of the PR's status, but it'll be updated along with major changes so people have something to use as a crash course for familiarizing themselves with how wounds function. I also have another infograph with all 9 of the possible initial wounds coming, and will be up soon. You can also find the longform design doc here with more info on the broad details, including descriptions of treatments: hackmd whee
What this does
There's a lot to cover, but here's the bullet points of the main features and changes:
Getting lots of damage on a limb can result in wounds, with more damage causing worse wounds. These can range from dislocated joints and minor cuts to compound fractures and fourth degree burns, and can affect you in different ways depending on what bodypart they're applied to (namely with broken bones).
You can damage individual bodyparts on clothing (only jumpsuits for now) through the use of lasers and sharp weapons. Bodyparts that reach max damage are considered "shredded" and will not apply any protection for that zone until it is repaired with cloth. If all zones are disabled, the entire piece of clothing is shredded and unwearable until repaired with 3 cloth. Jumpsuits give a small amount of wound protection, and since sharp weapons and lasers generally get extra wound bonuses against bare flesh, even a plain jumpsuit provides decent protection from a few laser shots or scalpel stabs.
Lasers gain a powerful niche versus unarmored/lightly armored carbons! As noted above, lasers can shred clothing and burn away zones of jumpsuits in 2 shots each, after which the target's bare flesh is exposed (barring other clothing), and lasers excel at dealing burn wounds against uncovered skin. Think big, nasty charring!
Bleeding is now totally limb based, and gauze is as well. Bleeding is also 95% cut wound based, meaning sharp weapons make you bleed rather than just having 40+ brute on a limb.
The more wounds and damage you get on a bodypart, the easier it'll be to gain more severe wounds. Wounds are arranged from Moderate, to Severe, to Critical in increasing severity, and you'll generally have to suffer the lesser ones before getting the worse ones.
dreamseeker_2020-05-15_03-15-59.png
Above: Someone having an incredibly bad day from bloodloss
dreamseeker_2020-05-04_22-29-29.png
Above: Scars from healed wounds
ShareX_2020-05-15_06-55-20.png
Above: Actual combat involving someone's head getting cracked
Here's a quick, if non-exhaustive, list of things I have left to do before I consider it feature complete
Finish adding treatments for each wound type/severity (mostly surgeries/triage for critical wounds)
Add second winds for bad injuries to give the victim a chance to get away
Flesh out severe & critical injuries in general
Find sprites for the bonesetter, bone gel, and anything else that might be needed
Add the medical items for treating the less severe wounds to the station
Polish code and remove any redundancies I left behind
Quick balance pass to make sure nothing is horribly abuseable
Why It's Good For The Game
Adds a flexible new system for representing damage on carbons with injuries that can be treated in different ways. Moderate wounds from getting toolboxed or sliced with a scalpel can usually be treated by a buddy or even by yourself with the right tools, but getting flayed with a fireaxe or a laser gun emptied into your bare skin may require extra attention or even surgery in bad cases! Also makes laser guns cooler and more like 40k lasguns that can flash fry people (cool!)
This should also make spessmen more resilient and harder to kill outright, while still adding consequences and complications to getting hurt. Wounds aren't immediately fatal, but they can do things like slow down interactions, deal damage over time through infections, and generally make you more fragile until fixed. They can also give you a "second wind" on being applied that gives you a small adrenaline boost (or whatever) to help disengage and escape immediate danger.
Changelog
🆑 Ryll/Shaps
add: Introduces medical wounds, new injuries that can happen to fleshy carbons when they sustain lots of damage on a bodypart. There's quite a lot of change here, but you can read the guide at: https://tgstation13.org/wiki/Guide_to_wounds and an extended changelog is available here: https://hackmd.io/l_FI9b3tSqa_woDadewJXA
add: Introduces scars and temporal scarring! Healing a wound leaves a scar behind that can be seen by examining someone twice rapidly, and if Temporal Scarring is enabled in character prefs, surviving a round with scars will save them to be granted at roundstart another round! Let your body tell stories!
tweak: Bleeding is now fully bodypart-focused, and 95% of bleeding comes from cut wounds from sharp weapons. Gauze is applied on a limb-by-limb basis, and helps staunch bloodflow rather than totally stop it. Notably, you no longer bleed just from having 40+ brute damage on a limb.
del: Organic bodyparts are no longer disabled at maximum damage, but are easier to cause wounds to
add: O2 medkits in emergency lockers have been replaced with new emergency medkits with basic tools for diagnosing and treating wounds and basic damage
tweak: Herapin now rapidly increases bleeding on all open cuts, rather than causing bleeding by itself. The more cuts on the target, the more it will affect them.
tweak: Neckgrab table slams now hit the targeted limb rather than just the head, with a large chance to dislocate or break a bone
tweak: Sharp weapons and burning weapons can now shred zones on jumpsuits, disabling protection on that limb. Damaged clothes can be repaired with cloth.
tweak: Slaughter demons now deal less raw damage, but gain the ability to cause cut wounds, which becomes more powerful with each attack on a humanoid but resets when bloodcrawling.
/🆑
894 lines
30 KiB
Plaintext
894 lines
30 KiB
Plaintext
#define MAX_TRANSIT_REQUEST_RETRIES 10
|
|
|
|
SUBSYSTEM_DEF(shuttle)
|
|
name = "Shuttle"
|
|
wait = 10
|
|
init_order = INIT_ORDER_SHUTTLE
|
|
flags = SS_KEEP_TIMING|SS_NO_TICK_CHECK
|
|
runlevels = RUNLEVEL_SETUP | RUNLEVEL_GAME
|
|
|
|
var/list/mobile = list()
|
|
var/list/stationary = list()
|
|
var/list/beacons = list()
|
|
var/list/transit = list()
|
|
|
|
var/list/transit_requesters = list()
|
|
var/list/transit_request_failures = list()
|
|
|
|
//emergency shuttle stuff
|
|
var/obj/docking_port/mobile/emergency/emergency
|
|
var/obj/docking_port/mobile/arrivals/arrivals
|
|
var/obj/docking_port/mobile/emergency/backup/backup_shuttle
|
|
var/emergencyCallTime = 6000 //time taken for emergency shuttle to reach the station when called (in deciseconds)
|
|
var/emergencyDockTime = 1800 //time taken for emergency shuttle to leave again once it has docked (in deciseconds)
|
|
var/emergencyEscapeTime = 1200 //time taken for emergency shuttle to reach a safe distance after leaving station (in deciseconds)
|
|
var/area/emergencyLastCallLoc
|
|
var/emergencyCallAmount = 0 //how many times the escape shuttle was called
|
|
var/emergencyNoEscape
|
|
var/emergencyNoRecall = FALSE
|
|
var/list/hostileEnvironments = list() //Things blocking escape shuttle from leaving
|
|
var/list/tradeBlockade = list() //Things blocking cargo from leaving.
|
|
var/supplyBlocked = FALSE
|
|
|
|
//supply shuttle stuff
|
|
var/obj/docking_port/mobile/supply/supply
|
|
var/ordernum = 1 //order number given to next order
|
|
var/points = 5000 //number of trade-points we have
|
|
var/centcom_message = "" //Remarks from CentCom on how well you checked the last order.
|
|
var/list/discoveredPlants = list() //Typepaths for unusual plants we've already sent CentCom, associated with their potencies
|
|
|
|
var/list/supply_packs = list()
|
|
var/list/shoppinglist = list()
|
|
var/list/requestlist = list()
|
|
var/list/orderhistory = list()
|
|
|
|
var/list/hidden_shuttle_turfs = list() //all turfs hidden from navigation computers associated with a list containing the image hiding them and the type of the turf they are pretending to be
|
|
var/list/hidden_shuttle_turf_images = list() //only the images from the above list
|
|
|
|
var/datum/round_event/shuttle_loan/shuttle_loan
|
|
|
|
var/shuttle_purchased = FALSE //If the station has purchased a replacement escape shuttle this round
|
|
var/list/shuttle_purchase_requirements_met = list() //For keeping track of ingame events that would unlock new shuttles, such as defeating a boss or discovering a secret item
|
|
|
|
var/lockdown = FALSE //disallow transit after nuke goes off
|
|
|
|
var/datum/map_template/shuttle/selected
|
|
|
|
var/obj/docking_port/mobile/existing_shuttle
|
|
|
|
var/obj/docking_port/mobile/preview_shuttle
|
|
var/datum/map_template/shuttle/preview_template
|
|
|
|
var/datum/turf_reservation/preview_reservation
|
|
|
|
/datum/controller/subsystem/shuttle/Initialize(timeofday)
|
|
ordernum = rand(1, 9000)
|
|
|
|
for(var/pack in subtypesof(/datum/supply_pack))
|
|
var/datum/supply_pack/P = new pack()
|
|
if(!P.contains)
|
|
continue
|
|
supply_packs[P.type] = P
|
|
|
|
initial_load()
|
|
|
|
if(!arrivals)
|
|
WARNING("No /obj/docking_port/mobile/arrivals placed on the map!")
|
|
if(!emergency)
|
|
WARNING("No /obj/docking_port/mobile/emergency placed on the map!")
|
|
if(!backup_shuttle)
|
|
WARNING("No /obj/docking_port/mobile/emergency/backup placed on the map!")
|
|
if(!supply)
|
|
WARNING("No /obj/docking_port/mobile/supply placed on the map!")
|
|
return ..()
|
|
|
|
/datum/controller/subsystem/shuttle/proc/initial_load()
|
|
for(var/s in stationary)
|
|
var/obj/docking_port/stationary/S = s
|
|
S.load_roundstart()
|
|
CHECK_TICK
|
|
|
|
/datum/controller/subsystem/shuttle/fire()
|
|
for(var/thing in mobile)
|
|
if(!thing)
|
|
mobile.Remove(thing)
|
|
continue
|
|
var/obj/docking_port/mobile/P = thing
|
|
P.check()
|
|
for(var/thing in transit)
|
|
var/obj/docking_port/stationary/transit/T = thing
|
|
if(!T.owner)
|
|
qdel(T, force=TRUE)
|
|
// This next one removes transit docks/zones that aren't
|
|
// immediately being used. This will mean that the zone creation
|
|
// code will be running a lot.
|
|
var/obj/docking_port/mobile/owner = T.owner
|
|
if(owner)
|
|
var/idle = owner.mode == SHUTTLE_IDLE
|
|
var/not_centcom_evac = owner.launch_status == NOLAUNCH
|
|
var/not_in_use = (!T.get_docked())
|
|
if(idle && not_centcom_evac && not_in_use)
|
|
qdel(T, force=TRUE)
|
|
CheckAutoEvac()
|
|
|
|
if(!SSmapping.clearing_reserved_turfs)
|
|
while(transit_requesters.len)
|
|
var/requester = popleft(transit_requesters)
|
|
var/success = generate_transit_dock(requester)
|
|
if(!success) // BACK OF THE QUEUE
|
|
transit_request_failures[requester]++
|
|
if(transit_request_failures[requester] < MAX_TRANSIT_REQUEST_RETRIES)
|
|
transit_requesters += requester
|
|
else
|
|
var/obj/docking_port/mobile/M = requester
|
|
M.transit_failure()
|
|
if(MC_TICK_CHECK)
|
|
break
|
|
|
|
/datum/controller/subsystem/shuttle/proc/CheckAutoEvac()
|
|
if(emergencyNoEscape || emergencyNoRecall || !emergency || !SSticker.HasRoundStarted())
|
|
return
|
|
|
|
var/threshold = CONFIG_GET(number/emergency_shuttle_autocall_threshold)
|
|
if(!threshold)
|
|
return
|
|
|
|
var/alive = 0
|
|
for(var/I in GLOB.player_list)
|
|
var/mob/M = I
|
|
if(M.stat != DEAD)
|
|
++alive
|
|
|
|
var/total = GLOB.joined_player_list.len
|
|
if(total <= 0)
|
|
return //no players no autoevac
|
|
|
|
if(alive / total <= threshold)
|
|
var/msg = "Automatically dispatching emergency shuttle due to crew death."
|
|
message_admins(msg)
|
|
log_shuttle("[msg] Alive: [alive], Roundstart: [total], Threshold: [threshold]")
|
|
emergencyNoRecall = TRUE
|
|
priority_announce("Catastrophic casualties detected: crisis shuttle protocols activated - jamming recall signals across all frequencies.")
|
|
if(emergency.timeLeft(1) > emergencyCallTime * 0.4)
|
|
emergency.request(null, set_coefficient = 0.4)
|
|
|
|
/datum/controller/subsystem/shuttle/proc/block_recall(lockout_timer)
|
|
emergencyNoRecall = TRUE
|
|
addtimer(CALLBACK(src, .proc/unblock_recall), lockout_timer)
|
|
|
|
/datum/controller/subsystem/shuttle/proc/unblock_recall()
|
|
emergencyNoRecall = FALSE
|
|
|
|
/datum/controller/subsystem/shuttle/proc/getShuttle(id)
|
|
for(var/obj/docking_port/mobile/M in mobile)
|
|
if(M.id == id)
|
|
return M
|
|
WARNING("couldn't find shuttle with id: [id]")
|
|
|
|
/datum/controller/subsystem/shuttle/proc/getDock(id)
|
|
for(var/obj/docking_port/stationary/S in stationary)
|
|
if(S.id == id)
|
|
return S
|
|
WARNING("couldn't find dock with id: [id]")
|
|
|
|
/datum/controller/subsystem/shuttle/proc/canEvac(mob/user)
|
|
var/srd = CONFIG_GET(number/shuttle_refuel_delay)
|
|
if(world.time - SSticker.round_start_time < srd)
|
|
to_chat(user, "<span class='alert'>The emergency shuttle is refueling. Please wait [DisplayTimeText(srd - (world.time - SSticker.round_start_time))] before trying again.</span>")
|
|
return FALSE
|
|
|
|
switch(emergency.mode)
|
|
if(SHUTTLE_RECALL)
|
|
to_chat(user, "<span class='alert'>The emergency shuttle may not be called while returning to CentCom.</span>")
|
|
return FALSE
|
|
if(SHUTTLE_CALL)
|
|
to_chat(user, "<span class='alert'>The emergency shuttle is already on its way.</span>")
|
|
return FALSE
|
|
if(SHUTTLE_DOCKED)
|
|
to_chat(user, "<span class='alert'>The emergency shuttle is already here.</span>")
|
|
return FALSE
|
|
if(SHUTTLE_IGNITING)
|
|
to_chat(user, "<span class='alert'>The emergency shuttle is firing its engines to leave.</span>")
|
|
return FALSE
|
|
if(SHUTTLE_ESCAPE)
|
|
to_chat(user, "<span class='alert'>The emergency shuttle is moving away to a safe distance.</span>")
|
|
return FALSE
|
|
if(SHUTTLE_STRANDED)
|
|
to_chat(user, "<span class='alert'>The emergency shuttle has been disabled by CentCom.</span>")
|
|
return FALSE
|
|
|
|
return TRUE
|
|
|
|
/datum/controller/subsystem/shuttle/proc/requestEvac(mob/user, call_reason)
|
|
if(!emergency)
|
|
WARNING("requestEvac(): There is no emergency shuttle, but the \
|
|
shuttle was called. Using the backup shuttle instead.")
|
|
if(!backup_shuttle)
|
|
CRASH("requestEvac(): There is no emergency shuttle, \
|
|
or backup shuttle! The game will be unresolvable. This is \
|
|
possibly a mapping error, more likely a bug with the shuttle \
|
|
manipulation system, or badminry. It is possible to manually \
|
|
resolve this problem by loading an emergency shuttle template \
|
|
manually, and then calling register() on the mobile docking port. \
|
|
Good luck.")
|
|
emergency = backup_shuttle
|
|
|
|
if(!canEvac(user))
|
|
return
|
|
|
|
call_reason = trim(html_encode(call_reason))
|
|
|
|
if(length(call_reason) < CALL_SHUTTLE_REASON_LENGTH && seclevel2num(get_security_level()) > SEC_LEVEL_GREEN)
|
|
to_chat(user, "<span class='alert'>You must provide a reason.</span>")
|
|
return
|
|
|
|
var/area/signal_origin = get_area(user)
|
|
var/emergency_reason = "\nNature of emergency:\n\n[call_reason]"
|
|
var/security_num = seclevel2num(get_security_level())
|
|
switch(security_num)
|
|
if(SEC_LEVEL_RED,SEC_LEVEL_DELTA)
|
|
emergency.request(null, signal_origin, html_decode(emergency_reason), 1) //There is a serious threat we gotta move no time to give them five minutes.
|
|
else
|
|
emergency.request(null, signal_origin, html_decode(emergency_reason), 0)
|
|
|
|
var/datum/radio_frequency/frequency = SSradio.return_frequency(FREQ_STATUS_DISPLAYS)
|
|
|
|
if(!frequency)
|
|
return
|
|
|
|
var/datum/signal/status_signal = new(list("command" = "update")) // Start processing shuttle-mode displays to display the timer
|
|
frequency.post_signal(src, status_signal)
|
|
|
|
var/area/A = get_area(user)
|
|
|
|
log_shuttle("[key_name(user)] has called the emergency shuttle.")
|
|
deadchat_broadcast(" has called the shuttle at <span class='name'>[A.name]</span>.", "<span class='name'>[user.real_name]</span>", user, message_type=DEADCHAT_ANNOUNCEMENT)
|
|
if(call_reason)
|
|
SSblackbox.record_feedback("text", "shuttle_reason", 1, "[call_reason]")
|
|
log_shuttle("Shuttle call reason: [call_reason]")
|
|
message_admins("[ADMIN_LOOKUPFLW(user)] has called the shuttle. (<A HREF='?_src_=holder;[HrefToken()];trigger_centcom_recall=1'>TRIGGER CENTCOM RECALL</A>)")
|
|
|
|
/datum/controller/subsystem/shuttle/proc/centcom_recall(old_timer, admiral_message)
|
|
if(emergency.mode != SHUTTLE_CALL || emergency.timer != old_timer)
|
|
return
|
|
emergency.cancel()
|
|
|
|
if(!admiral_message)
|
|
admiral_message = pick(GLOB.admiral_messages)
|
|
var/intercepttext = "<font size = 3><b>Nanotrasen Update</b>: Request For Shuttle.</font><hr>\
|
|
To whom it may concern:<br><br>\
|
|
We have taken note of the situation upon [station_name()] and have come to the \
|
|
conclusion that it does not warrant the abandonment of the station.<br>\
|
|
If you do not agree with our opinion we suggest that you open a direct \
|
|
line with us and explain the nature of your crisis.<br><br>\
|
|
<i>This message has been automatically generated based upon readings from long \
|
|
range diagnostic tools. To assure the quality of your request every finalized report \
|
|
is reviewed by an on-call rear admiral.<br>\
|
|
<b>Rear Admiral's Notes:</b> \
|
|
[admiral_message]"
|
|
print_command_report(intercepttext, announce = TRUE)
|
|
|
|
// Called when an emergency shuttle mobile docking port is
|
|
// destroyed, which will only happen with admin intervention
|
|
/datum/controller/subsystem/shuttle/proc/emergencyDeregister()
|
|
// When a new emergency shuttle is created, it will override the
|
|
// backup shuttle.
|
|
src.emergency = src.backup_shuttle
|
|
|
|
/datum/controller/subsystem/shuttle/proc/cancelEvac(mob/user)
|
|
if(canRecall())
|
|
emergency.cancel(get_area(user))
|
|
log_shuttle("[key_name(user)] has recalled the shuttle.")
|
|
message_admins("[ADMIN_LOOKUPFLW(user)] has recalled the shuttle.")
|
|
deadchat_broadcast(" has recalled the shuttle from <span class='name'>[get_area_name(user, TRUE)]</span>.", "<span class='name'>[user.real_name]</span>", user, message_type=DEADCHAT_ANNOUNCEMENT)
|
|
return 1
|
|
|
|
/datum/controller/subsystem/shuttle/proc/canRecall()
|
|
if(!emergency || emergency.mode != SHUTTLE_CALL || emergencyNoRecall || SSticker.mode.name == "meteor")
|
|
return
|
|
var/security_num = seclevel2num(get_security_level())
|
|
switch(security_num)
|
|
if(SEC_LEVEL_GREEN)
|
|
if(emergency.timeLeft(1) < emergencyCallTime)
|
|
return
|
|
if(SEC_LEVEL_BLUE)
|
|
if(emergency.timeLeft(1) < emergencyCallTime * 0.5)
|
|
return
|
|
else
|
|
if(emergency.timeLeft(1) < emergencyCallTime * 0.25)
|
|
return
|
|
return 1
|
|
|
|
/datum/controller/subsystem/shuttle/proc/autoEvac()
|
|
if (!SSticker.IsRoundInProgress())
|
|
return
|
|
|
|
var/callShuttle = 1
|
|
|
|
for(var/thing in GLOB.shuttle_caller_list)
|
|
if(isAI(thing))
|
|
var/mob/living/silicon/ai/AI = thing
|
|
if(AI.deployed_shell && !AI.deployed_shell.client)
|
|
continue
|
|
if(AI.stat || !AI.client)
|
|
continue
|
|
else if(istype(thing, /obj/machinery/computer/communications))
|
|
var/obj/machinery/computer/communications/C = thing
|
|
if(C.machine_stat & BROKEN)
|
|
continue
|
|
|
|
var/turf/T = get_turf(thing)
|
|
if(T && is_station_level(T.z))
|
|
callShuttle = 0
|
|
break
|
|
|
|
if(callShuttle)
|
|
if(EMERGENCY_IDLE_OR_RECALLED)
|
|
emergency.request(null, set_coefficient = 2.5)
|
|
log_shuttle("There is no means of calling the emergency shuttle anymore. Shuttle automatically called.")
|
|
message_admins("All the communications consoles were destroyed and all AIs are inactive. Shuttle called.")
|
|
|
|
/datum/controller/subsystem/shuttle/proc/registerHostileEnvironment(datum/bad)
|
|
hostileEnvironments[bad] = TRUE
|
|
checkHostileEnvironment()
|
|
|
|
/datum/controller/subsystem/shuttle/proc/clearHostileEnvironment(datum/bad)
|
|
hostileEnvironments -= bad
|
|
checkHostileEnvironment()
|
|
|
|
|
|
/datum/controller/subsystem/shuttle/proc/registerTradeBlockade(datum/bad)
|
|
tradeBlockade[bad] = TRUE
|
|
checkTradeBlockade()
|
|
|
|
/datum/controller/subsystem/shuttle/proc/clearTradeBlockade(datum/bad)
|
|
tradeBlockade -= bad
|
|
checkTradeBlockade()
|
|
|
|
|
|
/datum/controller/subsystem/shuttle/proc/checkTradeBlockade()
|
|
for(var/datum/d in tradeBlockade)
|
|
if(!istype(d) || QDELETED(d))
|
|
tradeBlockade -= d
|
|
supplyBlocked = tradeBlockade.len
|
|
|
|
if(supplyBlocked && (supply.mode == SHUTTLE_IGNITING))
|
|
supply.mode = SHUTTLE_STRANDED
|
|
supply.timer = null
|
|
//Make all cargo consoles speak up
|
|
if(!supplyBlocked && (supply.mode == SHUTTLE_STRANDED))
|
|
supply.mode = SHUTTLE_DOCKED
|
|
//Make all cargo consoles speak up
|
|
|
|
/datum/controller/subsystem/shuttle/proc/checkHostileEnvironment()
|
|
for(var/datum/d in hostileEnvironments)
|
|
if(!istype(d) || QDELETED(d))
|
|
hostileEnvironments -= d
|
|
emergencyNoEscape = hostileEnvironments.len
|
|
|
|
if(emergencyNoEscape && (emergency.mode == SHUTTLE_IGNITING))
|
|
emergency.mode = SHUTTLE_STRANDED
|
|
emergency.timer = null
|
|
emergency.sound_played = FALSE
|
|
priority_announce("Hostile environment detected. \
|
|
Departure has been postponed indefinitely pending \
|
|
conflict resolution.", null, 'sound/misc/notice1.ogg', "Priority")
|
|
if(!emergencyNoEscape && (emergency.mode == SHUTTLE_STRANDED))
|
|
emergency.mode = SHUTTLE_DOCKED
|
|
emergency.setTimer(emergencyDockTime)
|
|
priority_announce("Hostile environment resolved. \
|
|
You have 3 minutes to board the Emergency Shuttle.",
|
|
null, 'sound/ai/shuttledock.ogg', "Priority")
|
|
|
|
//try to move/request to dockHome if possible, otherwise dockAway. Mainly used for admin buttons
|
|
/datum/controller/subsystem/shuttle/proc/toggleShuttle(shuttleId, dockHome, dockAway, timed)
|
|
var/obj/docking_port/mobile/M = getShuttle(shuttleId)
|
|
if(!M)
|
|
return 1
|
|
var/obj/docking_port/stationary/dockedAt = M.get_docked()
|
|
var/destination = dockHome
|
|
if(dockedAt && dockedAt.id == dockHome)
|
|
destination = dockAway
|
|
if(timed)
|
|
if(M.request(getDock(destination)))
|
|
return 2
|
|
else
|
|
if(M.initiate_docking(getDock(destination)) != DOCKING_SUCCESS)
|
|
return 2
|
|
return 0 //dock successful
|
|
|
|
|
|
/datum/controller/subsystem/shuttle/proc/moveShuttle(shuttleId, dockId, timed)
|
|
var/obj/docking_port/mobile/M = getShuttle(shuttleId)
|
|
var/obj/docking_port/stationary/D = getDock(dockId)
|
|
|
|
if(!M)
|
|
return 1
|
|
if(timed)
|
|
if(M.request(D))
|
|
return 2
|
|
else
|
|
if(M.initiate_docking(D) != DOCKING_SUCCESS)
|
|
return 2
|
|
return 0 //dock successful
|
|
|
|
/datum/controller/subsystem/shuttle/proc/request_transit_dock(obj/docking_port/mobile/M)
|
|
if(!istype(M))
|
|
CRASH("[M] is not a mobile docking port")
|
|
|
|
if(M.assigned_transit)
|
|
return
|
|
else
|
|
if(!(M in transit_requesters))
|
|
transit_requesters += M
|
|
|
|
/datum/controller/subsystem/shuttle/proc/generate_transit_dock(obj/docking_port/mobile/M)
|
|
// First, determine the size of the needed zone
|
|
// Because of shuttle rotation, the "width" of the shuttle is not
|
|
// always x.
|
|
var/travel_dir = M.preferred_direction
|
|
// Remember, the direction is the direction we appear to be
|
|
// coming from
|
|
var/dock_angle = dir2angle(M.preferred_direction) + dir2angle(M.port_direction) + 180
|
|
var/dock_dir = angle2dir(dock_angle)
|
|
|
|
var/transit_width = SHUTTLE_TRANSIT_BORDER * 2
|
|
var/transit_height = SHUTTLE_TRANSIT_BORDER * 2
|
|
|
|
// Shuttles travelling on their side have their dimensions swapped
|
|
// from our perspective
|
|
switch(dock_dir)
|
|
if(NORTH, SOUTH)
|
|
transit_width += M.width
|
|
transit_height += M.height
|
|
if(EAST, WEST)
|
|
transit_width += M.height
|
|
transit_height += M.width
|
|
|
|
/*
|
|
to_chat(world, "The attempted transit dock will be [transit_width] width, and \)
|
|
[transit_height] in height. The travel dir is [travel_dir]."
|
|
*/
|
|
|
|
var/transit_path = /turf/open/space/transit
|
|
switch(travel_dir)
|
|
if(NORTH)
|
|
transit_path = /turf/open/space/transit/north
|
|
if(SOUTH)
|
|
transit_path = /turf/open/space/transit/south
|
|
if(EAST)
|
|
transit_path = /turf/open/space/transit/east
|
|
if(WEST)
|
|
transit_path = /turf/open/space/transit/west
|
|
|
|
var/datum/turf_reservation/proposal = SSmapping.RequestBlockReservation(transit_width, transit_height, null, /datum/turf_reservation/transit, transit_path)
|
|
|
|
if(!istype(proposal))
|
|
return FALSE
|
|
|
|
var/turf/bottomleft = locate(proposal.bottom_left_coords[1], proposal.bottom_left_coords[2], proposal.bottom_left_coords[3])
|
|
// Then create a transit docking port in the middle
|
|
var/coords = M.return_coords(0, 0, dock_dir)
|
|
/* 0------2
|
|
| |
|
|
| |
|
|
| x |
|
|
3------1
|
|
*/
|
|
|
|
var/x0 = coords[1]
|
|
var/y0 = coords[2]
|
|
var/x1 = coords[3]
|
|
var/y1 = coords[4]
|
|
// Then we want the point closest to -infinity,-infinity
|
|
var/x2 = min(x0, x1)
|
|
var/y2 = min(y0, y1)
|
|
|
|
// Then invert the numbers
|
|
var/transit_x = bottomleft.x + SHUTTLE_TRANSIT_BORDER + abs(x2)
|
|
var/transit_y = bottomleft.y + SHUTTLE_TRANSIT_BORDER + abs(y2)
|
|
|
|
var/turf/midpoint = locate(transit_x, transit_y, bottomleft.z)
|
|
if(!midpoint)
|
|
return FALSE
|
|
var/area/shuttle/transit/A = new()
|
|
A.parallax_movedir = travel_dir
|
|
A.contents = proposal.reserved_turfs
|
|
var/obj/docking_port/stationary/transit/new_transit_dock = new(midpoint)
|
|
new_transit_dock.reserved_area = proposal
|
|
new_transit_dock.name = "Transit for [M.id]/[M.name]"
|
|
new_transit_dock.owner = M
|
|
new_transit_dock.assigned_area = A
|
|
|
|
// Add 180, because ports point inwards, rather than outwards
|
|
new_transit_dock.setDir(angle2dir(dock_angle))
|
|
|
|
M.assigned_transit = new_transit_dock
|
|
return new_transit_dock
|
|
|
|
/datum/controller/subsystem/shuttle/Recover()
|
|
if (istype(SSshuttle.mobile))
|
|
mobile = SSshuttle.mobile
|
|
if (istype(SSshuttle.stationary))
|
|
stationary = SSshuttle.stationary
|
|
if (istype(SSshuttle.transit))
|
|
transit = SSshuttle.transit
|
|
if (istype(SSshuttle.transit_requesters))
|
|
transit_requesters = SSshuttle.transit_requesters
|
|
if (istype(SSshuttle.transit_request_failures))
|
|
transit_request_failures = SSshuttle.transit_request_failures
|
|
|
|
if (istype(SSshuttle.emergency))
|
|
emergency = SSshuttle.emergency
|
|
if (istype(SSshuttle.arrivals))
|
|
arrivals = SSshuttle.arrivals
|
|
if (istype(SSshuttle.backup_shuttle))
|
|
backup_shuttle = SSshuttle.backup_shuttle
|
|
|
|
if (istype(SSshuttle.emergencyLastCallLoc))
|
|
emergencyLastCallLoc = SSshuttle.emergencyLastCallLoc
|
|
|
|
if (istype(SSshuttle.hostileEnvironments))
|
|
hostileEnvironments = SSshuttle.hostileEnvironments
|
|
|
|
if (istype(SSshuttle.supply))
|
|
supply = SSshuttle.supply
|
|
|
|
if (istype(SSshuttle.discoveredPlants))
|
|
discoveredPlants = SSshuttle.discoveredPlants
|
|
|
|
if (istype(SSshuttle.shoppinglist))
|
|
shoppinglist = SSshuttle.shoppinglist
|
|
if (istype(SSshuttle.requestlist))
|
|
requestlist = SSshuttle.requestlist
|
|
if (istype(SSshuttle.orderhistory))
|
|
orderhistory = SSshuttle.orderhistory
|
|
|
|
if (istype(SSshuttle.shuttle_loan))
|
|
shuttle_loan = SSshuttle.shuttle_loan
|
|
|
|
if (istype(SSshuttle.shuttle_purchase_requirements_met))
|
|
shuttle_purchase_requirements_met = SSshuttle.shuttle_purchase_requirements_met
|
|
|
|
var/datum/bank_account/D = SSeconomy.get_dep_account(ACCOUNT_CAR)
|
|
centcom_message = SSshuttle.centcom_message
|
|
ordernum = SSshuttle.ordernum
|
|
points = D.account_balance
|
|
emergencyNoEscape = SSshuttle.emergencyNoEscape
|
|
emergencyCallAmount = SSshuttle.emergencyCallAmount
|
|
shuttle_purchased = SSshuttle.shuttle_purchased
|
|
lockdown = SSshuttle.lockdown
|
|
|
|
selected = SSshuttle.selected
|
|
|
|
existing_shuttle = SSshuttle.existing_shuttle
|
|
|
|
preview_shuttle = SSshuttle.preview_shuttle
|
|
preview_template = SSshuttle.preview_template
|
|
|
|
preview_reservation = SSshuttle.preview_reservation
|
|
|
|
/datum/controller/subsystem/shuttle/proc/is_in_shuttle_bounds(atom/A)
|
|
var/area/current = get_area(A)
|
|
if(istype(current, /area/shuttle) && !istype(current, /area/shuttle/transit))
|
|
return TRUE
|
|
for(var/obj/docking_port/mobile/M in mobile)
|
|
if(M.is_in_shuttle_bounds(A))
|
|
return TRUE
|
|
|
|
/datum/controller/subsystem/shuttle/proc/get_containing_shuttle(atom/A)
|
|
var/list/mobile_cache = mobile
|
|
for(var/i in 1 to mobile_cache.len)
|
|
var/obj/docking_port/port = mobile_cache[i]
|
|
if(port.is_in_shuttle_bounds(A))
|
|
return port
|
|
|
|
/datum/controller/subsystem/shuttle/proc/get_containing_dock(atom/A)
|
|
. = list()
|
|
var/list/stationary_cache = stationary
|
|
for(var/i in 1 to stationary_cache.len)
|
|
var/obj/docking_port/port = stationary_cache[i]
|
|
if(port.is_in_shuttle_bounds(A))
|
|
. += port
|
|
|
|
/datum/controller/subsystem/shuttle/proc/get_dock_overlap(x0, y0, x1, y1, z)
|
|
. = list()
|
|
var/list/stationary_cache = stationary
|
|
for(var/i in 1 to stationary_cache.len)
|
|
var/obj/docking_port/port = stationary_cache[i]
|
|
if(!port || port.z != z)
|
|
continue
|
|
var/list/bounds = port.return_coords()
|
|
var/list/overlap = get_overlap(x0, y0, x1, y1, bounds[1], bounds[2], bounds[3], bounds[4])
|
|
var/list/xs = overlap[1]
|
|
var/list/ys = overlap[2]
|
|
if(xs.len && ys.len)
|
|
.[port] = overlap
|
|
|
|
/datum/controller/subsystem/shuttle/proc/update_hidden_docking_ports(list/remove_turfs, list/add_turfs)
|
|
var/list/remove_images = list()
|
|
var/list/add_images = list()
|
|
|
|
if(remove_turfs)
|
|
for(var/T in remove_turfs)
|
|
var/list/L = hidden_shuttle_turfs[T]
|
|
if(L)
|
|
remove_images += L[1]
|
|
hidden_shuttle_turfs -= remove_turfs
|
|
|
|
if(add_turfs)
|
|
for(var/V in add_turfs)
|
|
var/turf/T = V
|
|
var/image/I
|
|
if(remove_images.len)
|
|
//we can just reuse any images we are about to delete instead of making new ones
|
|
I = remove_images[1]
|
|
remove_images.Cut(1, 2)
|
|
I.loc = T
|
|
else
|
|
I = image(loc = T)
|
|
add_images += I
|
|
I.appearance = T.appearance
|
|
I.override = TRUE
|
|
hidden_shuttle_turfs[T] = list(I, T.type)
|
|
|
|
hidden_shuttle_turf_images -= remove_images
|
|
hidden_shuttle_turf_images += add_images
|
|
|
|
for(var/V in GLOB.navigation_computers)
|
|
var/obj/machinery/computer/camera_advanced/shuttle_docker/C = V
|
|
C.update_hidden_docking_ports(remove_images, add_images)
|
|
|
|
QDEL_LIST(remove_images)
|
|
|
|
|
|
/datum/controller/subsystem/shuttle/proc/action_load(datum/map_template/shuttle/loading_template, obj/docking_port/stationary/destination_port)
|
|
// Check for an existing preview
|
|
if(preview_shuttle && (loading_template != preview_template))
|
|
preview_shuttle.jumpToNullSpace()
|
|
preview_shuttle = null
|
|
preview_template = null
|
|
QDEL_NULL(preview_reservation)
|
|
|
|
if(!preview_shuttle)
|
|
if(load_template(loading_template))
|
|
preview_shuttle.linkup(loading_template, destination_port)
|
|
preview_template = loading_template
|
|
|
|
// get the existing shuttle information, if any
|
|
var/timer = 0
|
|
var/mode = SHUTTLE_IDLE
|
|
var/obj/docking_port/stationary/D
|
|
|
|
if(istype(destination_port))
|
|
D = destination_port
|
|
else if(existing_shuttle)
|
|
timer = existing_shuttle.timer
|
|
mode = existing_shuttle.mode
|
|
D = existing_shuttle.get_docked()
|
|
|
|
if(!D)
|
|
D = generate_transit_dock(preview_shuttle)
|
|
|
|
if(!D)
|
|
CRASH("No dock found for preview shuttle ([preview_template.name]), aborting.")
|
|
|
|
var/result = preview_shuttle.canDock(D)
|
|
// truthy value means that it cannot dock for some reason
|
|
// but we can ignore the someone else docked error because we'll
|
|
// be moving into their place shortly
|
|
if((result != SHUTTLE_CAN_DOCK) && (result != SHUTTLE_SOMEONE_ELSE_DOCKED))
|
|
WARNING("Template shuttle [preview_shuttle] cannot dock at [D] ([result]).")
|
|
return
|
|
|
|
if(existing_shuttle)
|
|
existing_shuttle.jumpToNullSpace()
|
|
|
|
var/list/force_memory = preview_shuttle.movement_force
|
|
preview_shuttle.movement_force = list("KNOCKDOWN" = 0, "THROW" = 0)
|
|
preview_shuttle.initiate_docking(D)
|
|
preview_shuttle.movement_force = force_memory
|
|
|
|
. = preview_shuttle
|
|
|
|
// Shuttle state involves a mode and a timer based on world.time, so
|
|
// plugging the existing shuttles old values in works fine.
|
|
preview_shuttle.timer = timer
|
|
preview_shuttle.mode = mode
|
|
|
|
preview_shuttle.register()
|
|
|
|
// TODO indicate to the user that success happened, rather than just
|
|
// blanking the modification tab
|
|
preview_shuttle = null
|
|
preview_template = null
|
|
existing_shuttle = null
|
|
selected = null
|
|
QDEL_NULL(preview_reservation)
|
|
|
|
/datum/controller/subsystem/shuttle/proc/load_template(datum/map_template/shuttle/S)
|
|
. = FALSE
|
|
// load shuttle template, centred at shuttle import landmark,
|
|
preview_reservation = SSmapping.RequestBlockReservation(S.width, S.height, SSmapping.transit.z_value, /datum/turf_reservation/transit)
|
|
if(!preview_reservation)
|
|
CRASH("failed to reserve an area for shuttle template loading")
|
|
var/turf/BL = TURF_FROM_COORDS_LIST(preview_reservation.bottom_left_coords)
|
|
S.load(BL, centered = FALSE, register = FALSE)
|
|
|
|
var/affected = S.get_affected_turfs(BL, centered=FALSE)
|
|
|
|
var/found = 0
|
|
// Search the turfs for docking ports
|
|
// - We need to find the mobile docking port because that is the heart of
|
|
// the shuttle.
|
|
// - We need to check that no additional ports have slipped in from the
|
|
// template, because that causes unintended behaviour.
|
|
for(var/T in affected)
|
|
for(var/obj/docking_port/P in T)
|
|
if(istype(P, /obj/docking_port/mobile))
|
|
found++
|
|
if(found > 1)
|
|
qdel(P, force=TRUE)
|
|
log_world("Map warning: Shuttle Template [S.mappath] has multiple mobile docking ports.")
|
|
else
|
|
preview_shuttle = P
|
|
if(istype(P, /obj/docking_port/stationary))
|
|
log_world("Map warning: Shuttle Template [S.mappath] has a stationary docking port.")
|
|
if(!found)
|
|
var/msg = "load_template(): Shuttle Template [S.mappath] has no mobile docking port. Aborting import."
|
|
for(var/T in affected)
|
|
var/turf/T0 = T
|
|
T0.empty()
|
|
|
|
message_admins(msg)
|
|
WARNING(msg)
|
|
return
|
|
//Everything fine
|
|
S.post_load(preview_shuttle)
|
|
return TRUE
|
|
|
|
/datum/controller/subsystem/shuttle/proc/unload_preview()
|
|
if(preview_shuttle)
|
|
preview_shuttle.jumpToNullSpace()
|
|
preview_shuttle = null
|
|
|
|
|
|
/datum/controller/subsystem/shuttle/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.admin_state)
|
|
ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
|
|
if(!ui)
|
|
ui = new(user, src, ui_key, "ShuttleManipulator", name, 800, 600, master_ui, state)
|
|
ui.open()
|
|
|
|
|
|
/datum/controller/subsystem/shuttle/ui_data(mob/user)
|
|
var/list/data = list()
|
|
data["tabs"] = list("Status", "Templates", "Modification")
|
|
|
|
// Templates panel
|
|
data["templates"] = list()
|
|
var/list/templates = data["templates"]
|
|
data["templates_tabs"] = list()
|
|
data["selected"] = list()
|
|
|
|
for(var/shuttle_id in SSmapping.shuttle_templates)
|
|
var/datum/map_template/shuttle/S = SSmapping.shuttle_templates[shuttle_id]
|
|
|
|
if(!templates[S.port_id])
|
|
data["templates_tabs"] += S.port_id
|
|
templates[S.port_id] = list(
|
|
"port_id" = S.port_id,
|
|
"templates" = list())
|
|
|
|
var/list/L = list()
|
|
L["name"] = S.name
|
|
L["shuttle_id"] = S.shuttle_id
|
|
L["port_id"] = S.port_id
|
|
L["description"] = S.description
|
|
L["admin_notes"] = S.admin_notes
|
|
|
|
if(selected == S)
|
|
data["selected"] = L
|
|
|
|
templates[S.port_id]["templates"] += list(L)
|
|
|
|
data["templates_tabs"] = sortList(data["templates_tabs"])
|
|
|
|
data["existing_shuttle"] = null
|
|
|
|
// Status panel
|
|
data["shuttles"] = list()
|
|
for(var/i in mobile)
|
|
var/obj/docking_port/mobile/M = i
|
|
var/timeleft = M.timeLeft(1)
|
|
var/list/L = list()
|
|
L["name"] = M.name
|
|
L["id"] = M.id
|
|
L["timer"] = M.timer
|
|
L["timeleft"] = M.getTimerStr()
|
|
if (timeleft > 1 HOURS)
|
|
L["timeleft"] = "Infinity"
|
|
L["can_fast_travel"] = M.timer && timeleft >= 50
|
|
L["can_fly"] = TRUE
|
|
if(istype(M, /obj/docking_port/mobile/emergency))
|
|
L["can_fly"] = FALSE
|
|
else if(!M.destination)
|
|
L["can_fast_travel"] = FALSE
|
|
if (M.mode != SHUTTLE_IDLE)
|
|
L["mode"] = capitalize(M.mode)
|
|
L["status"] = M.getDbgStatusText()
|
|
if(M == existing_shuttle)
|
|
data["existing_shuttle"] = L
|
|
|
|
data["shuttles"] += list(L)
|
|
|
|
return data
|
|
|
|
/datum/controller/subsystem/shuttle/ui_act(action, params)
|
|
if(..())
|
|
return
|
|
|
|
var/mob/user = usr
|
|
|
|
// Preload some common parameters
|
|
var/shuttle_id = params["shuttle_id"]
|
|
var/datum/map_template/shuttle/S = SSmapping.shuttle_templates[shuttle_id]
|
|
|
|
switch(action)
|
|
if("select_template")
|
|
if(S)
|
|
existing_shuttle = getShuttle(S.port_id)
|
|
selected = S
|
|
. = TRUE
|
|
if("jump_to")
|
|
if(params["type"] == "mobile")
|
|
for(var/i in mobile)
|
|
var/obj/docking_port/mobile/M = i
|
|
if(M.id == params["id"])
|
|
user.forceMove(get_turf(M))
|
|
. = TRUE
|
|
break
|
|
|
|
if("fly")
|
|
for(var/i in mobile)
|
|
var/obj/docking_port/mobile/M = i
|
|
if(M.id == params["id"])
|
|
. = TRUE
|
|
M.admin_fly_shuttle(user)
|
|
break
|
|
|
|
if("fast_travel")
|
|
for(var/i in mobile)
|
|
var/obj/docking_port/mobile/M = i
|
|
if(M.id == params["id"] && M.timer && M.timeLeft(1) >= 50)
|
|
M.setTimer(50)
|
|
. = TRUE
|
|
message_admins("[key_name_admin(usr)] fast travelled [M]")
|
|
log_admin("[key_name(usr)] fast travelled [M]")
|
|
SSblackbox.record_feedback("text", "shuttle_manipulator", 1, "[M.name]")
|
|
break
|
|
|
|
if("preview")
|
|
if(S)
|
|
. = TRUE
|
|
unload_preview()
|
|
load_template(S)
|
|
if(preview_shuttle)
|
|
preview_template = S
|
|
user.forceMove(get_turf(preview_shuttle))
|
|
if("load")
|
|
if(existing_shuttle == backup_shuttle)
|
|
// TODO make the load button disabled
|
|
WARNING("The shuttle that the selected shuttle will replace \
|
|
is the backup shuttle. Backup shuttle is required to be \
|
|
intact for round sanity.")
|
|
else if(S)
|
|
. = TRUE
|
|
// If successful, returns the mobile docking port
|
|
var/obj/docking_port/mobile/mdp = action_load(S)
|
|
if(mdp)
|
|
user.forceMove(get_turf(mdp))
|
|
message_admins("[key_name_admin(usr)] loaded [mdp] with the shuttle manipulator.")
|
|
log_admin("[key_name(usr)] loaded [mdp] with the shuttle manipulator.</span>")
|
|
SSblackbox.record_feedback("text", "shuttle_manipulator", 1, "[mdp.name]")
|
|
|