Files
runecap 441ef5805d Splits Pilot: SpaCecRaft intO three levelS and Adds Piloting Education BacKgrounds (#22673)
These were some of the first given feedback for skills, so I may as well
implement it.

Piloting is now split into three levels like other skills which note
what pilot class of vessel you may comfortably pilot.
(Familiar) **Shuttle Pilot** is for all landables: Spark, Canary,
Intrepid, the Quark, and the like are all equal under it.
(Trained) **Class II Pilot** covers most ships, which are "overmap-only"
and unable to land or dock.
(Professional) **Class IV Pilot** covers only the Horizon normally, but
design-wise extends to whatever is the main player ship, or for anything
massive

**Antags get Professional Pilot, so they can always do ship hijack
gimmicks without worry.**

As a convenience to make Bridge personnel stand out, Professional/Class
IV Pilots can deduce their current overmap coordinate from examining
space on help intent for 3s. There's no way to directly link to the
overmap, so on the backend, this ability briefly references an existing
ship console.

### Design

Class II and IV are pre-existing terms in-lore, used on the Interstellar
Travel page to refer to civilian size classes.
With how important each level of piloting is, they cost _+4 points_ each
instead of going 2 -> 4 -> 8. This lets there exist the occasional pilot
character from any background, but it's _always_ an investment and
piloting the Horizon stays hard without training.
As Familiar currently costs 6 points, this is effectively a 2-point buff
for most cases as you'll rarely have to pilot non-shuttles.
(**EDIT**: Piloting levels are now 4 -> 8 -> 10 so there's a bit of
flexibility to have an extra skill for Professional Pilots from
Expeditionary Trade)

#### The _**real meat**_ of this PR...
is that you can now attempt to pilot vessels a level higher than you
with penalties, so there's now a failure spectrum should you manage to
access an adjacent-class ship rather than always being hard-locked. The
penalties affect acceleration, slowing, turning, and rolling. They're
most extreme for Shuttle and Class IV to represent how clueless an
unlicensed pilot would be and how complex the Horizon realistically is
to operate.

With smart usage of autopilot, you can actually find a way to travel
normal-ish. I think this is a fun interaction to have for underskilled
characters, and makes autopilot actually have a use as it otherwise
kills you. Maybe Bridge personnel can give lessons to Class II pilots
with it.

### Piloting Education Backgrounds

Coming are THREE piloting education backgrounds: Flight Academy, Fleet
Training, and Expeditionary Trade.

#### ALL GET _Familiar Electrical Engineering,_ _Familiar Atmospheric_

(EDIT: Flight Academy is now the only background that gets Familiar
Mechanical Engineering, this makes them stand out as being notably
handy/utility-ready for emergencies, but other backgrounds can still
easily match them with points.
Fleet Training has Tenacity down to Trained for character variety,
though tenacity is very slight buffs and hard to notice.
Expeditionary Trade now uses Familiar Firearms over Familiar Armed
Combat as it's more immediately important.)

Electrical Engineering now lets you use a power cell on an unpowered
ship console to give it emergency battery life based on the charge. This
helps with cases of being stranded from an APC broken by a meteor, a
high-capacity cell can give 10m or more based on Electrical level. I may
expand this bypass mechanic to all consoles in the future.
Atmospherics skill has no implementation currently, but dealing with
breaches or cycling on shuttles is too important.

Flight Academy - BC, XO, Captain
*Professional Piloting, Trained Leadership, Familiar Mechanical
Engineering*
> "You are at least 25 years of age, with the best navigation training
the Spur can offer, vessel handiness in a pinch, and the know-how for
reining in or assuring passengers for the voyage."

Flight Academy has Leadership honestly just because I didn't want Fleet
Training to look too good, and because I think they can rock the "this
is your pilot speaking" in a cool way. Bridge personnel are usually the
ones that have to wraggle people on away sites, especially during
events.
Mechanical Engineering allows them to make machine frames of ship
consoles in emergencies.


Fleet Training - BC, XO, Captain 
*Professional Piloting, Trained Firearms, Trained Tenacity*
> "You are at least 25 years of age, with the most extensive navigation
training possible and military discipline to back it up. You're a steady
shot in emergencies and, damn it-- You. Go. Down. With. The. Ship."

This was made because a player informed me many BC and XOs choose
military backgrounds, and the +4 points design would prevent that. Fleet
Training isn't the same as Military Training, but having all of Piloting
gives you 6 points free to match MT if wanted. The high Tenacity is
primarily for surviving in ship combat, where you'll probably
bleed/suffocate to death.

Expeditionary Trade - Miners, Xenology roles, OM, RD
*Familiar Piloting, Familiar Firearms, Familiar Medicine*
> "You've developed trade skills for a career of expeditions. You are at
home in a shuttle piloting to and from for an average day's work. You
can even hit a shot or tend to wounds when things eventually get
unsafe."

This is for Miner and Xenoarch primarily. An expedition is just any
journey with an objective, all roles revolving around piloting shuttles
to an objective needs to be able to hold their own if they ever find
carp or have an accident, especially since they'll likely be alone.

#### MISC stuff

corrected shaper gear comments I left wrong

Hotwiring piloting consoles now checks for Familiar in Electrical
Engineering. Its visible emote also now looks identical to attempting an
emergency bypass at first glance, so you can sneakily attempt one with
an unpowered console.

The Doctor of Psychology education background now gives Trained in
Leadership. This was intended to be uncommented, but the person forgot
to do it.

Research Directors now need to have Familiar/Shuttle Pilot. According to
the Guide to Piloting wikipage, they're supposed to be able to fly the
Intrepid and Quark for expeditions, and likely mirroring how the OM can
fly the Spark, but an RD can spawn completely incapable of piloting
currently.

---------

Signed-off-by: runecap <43975590+runecap@users.noreply.github.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>
2026-07-08 17:13:18 +00:00

424 lines
15 KiB
Plaintext

#define OVERMAP_SPEED_CONSTANT (1 SECOND)
#define SHIP_MOVE_RESOLUTION 0.00001
#define MOVING(speed) abs(speed) >= min_speed
#define SANITIZE_SPEED(speed) SIGN(speed) * clamp(abs(speed), 0, max_speed)
#define CHANGE_SPEED_BY(speed_var, v_diff) \
v_diff = SANITIZE_SPEED(v_diff);\
if(!MOVING(speed_var + v_diff)) \
{speed_var = 0};\
else \
{speed_var = SANITIZE_SPEED((speed_var + v_diff)/(1 + speed_var*v_diff/(max_speed ** 2)))}
// Uses Lorentzian dynamics to avoid going too fast.
/obj/effect/overmap/visitable/ship
name = "generic ship"
desc = "Space faring vessel."
icon = 'icons/obj/overmap/overmap_ships.dmi'
icon_state = "generic"
requires_contact = TRUE
obfuscated_name = "unidentified vessel"
sensor_range_override = FALSE
hide_from_reports = TRUE
generic_object = FALSE
var/moving_state = "ship_moving"
/*For Pilot: Spacecraft skill. Denotes SHUTTLE for landables, CLASS_TWO for standard "overmap-only" ships, or CLASS_MAX for massive
ones which is just the Horizon/main player ship. When null, the skill only cares for being Unfamiliar; stations are null.*/
var/pilot_class = PILOTING_CLASS_TWO
layer = OVERMAP_SHIP_LAYER
//RP fluff details to appear on scan readouts for mobile objects.
var/propulsion = "Chemical Composite Gas Thrust" //Slower than light propulsion method. No variation in this currently exists yet except the Horizon which heats its gas.
var/drive = "None equipped, FTL incapable" //Faster than light propulsion method, will usually be warp drives for third party ships and nothing for shuttles
var/list/known_ships = list() //List of ships known at roundstart - put types here.
var/base_sensor_visibility
/// Tonnes, arbitrary number, affects acceleration provided by engines. Will help determine the speed of the ship.
vessel_mass = 10000
/// Arbitrary number, affects how likely the ship is to evade meteors.
var/vessel_size = SHIP_SIZE_LARGE
/// The "speed of light" for the ship, in turfs/tick.
var/max_speed = 1/(1 SECOND)
/// Below this, we round speed to 0 to avoid math errors.
var/min_speed = 1/(2 MINUTES)
/// Ship speed in x,y direction
var/list/speed = list(0,0)
/// The ship's position within a tile.
var/list/position = list(0,0)
/// The worldtime when ship last accelerated
var/last_burn = 0
/// Determines how often the ship can accelerate or decelerate.
var/burn_delay = 1 SECOND
/// The direction ships fly towards. Make sure this is correct or else entry points will not work correctly.
var/fore_dir = NORTH
var/last_combat_roll = 0
var/last_turn = 0
var/last_combat_turn = 0
var/list/engines = list()
var/engines_state = 0 // Global on/off toggle for all engines.
var/thrust_limit = 1 // Global thrust limit for all engines, 0..1
var/halted = 0 // Admin halt or other stop.
comms_support = TRUE
var/list/colors = list() // Pick a color from this list on init.
/obj/effect/overmap/visitable/ship/Initialize()
. = ..()
glide_size = world.icon_size
min_speed = round(min_speed, SHIP_MOVE_RESOLUTION)
max_speed = round(max_speed, SHIP_MOVE_RESOLUTION)
base_sensor_visibility = round((vessel_mass/SENSOR_COEFFICENT),1)
SSshuttle.ships += src
if(LAZYLEN(colors))
color = pick(colors)
/// Parent object for stationary objects that still need to access ship systems (such as the Sensor Relays)
/obj/effect/overmap/visitable/ship/stationary
name = "generic station"
desc = "A generic space station, see if you can find the other twelve."
obfuscated_name = "unidentified stationary object"
unknown_id = "Unknown artificial structure"
static_vessel = TRUE
pilot_class = null
propulsion = "None equipped, flight incapable"
halted = TRUE // Cannot fly under any circumstances
/obj/effect/overmap/visitable/ship/find_z_levels(var/fore_direction)
. = ..(fore_dir)
/obj/effect/overmap/visitable/ship/Destroy()
SSshuttle.ships -= src
for(var/obj/structure/machinery/computer/ship/S in SSmachinery.machinery)
if(S.linked == src)
S.linked = null
for(var/obj/structure/machinery/computer/shuttle_control/explore/C in SSmachinery.machinery)
if(C.linked == src)
C.linked = null
. = ..()
/obj/effect/overmap/visitable/ship/relaymove(mob/living/user, direction, accel_limit)
. = ..()
INVOKE_ASYNC(src, PROC_REF(accelerate), direction, accel_limit)
/obj/effect/overmap/visitable/ship/proc/is_still()
return !MOVING(speed[1]) && !MOVING(speed[2])
/obj/effect/overmap/visitable/ship/get_scan_data(mob/user)
. = ..()
if (static_vessel) // full data already acquired from parent proc
return
if(!is_still())
. += "<br>Heading: [dir2angle(get_heading())], speed [get_speed() * 1000]"
if(instant_contact)
. += "<br>It is broadcasting a distress signal."
. += "<hr>"
. += "<br><center><large><b>Scan Details</b></large>"
. += "<br><large><b>[name]</b></large></center>"
. += "<br><small><b>Estimated Mass:</b> [vessel_mass]"
. += "<br><b>Projected Volume:</b> [volume]"
. += "<br><b>STL Propulsion:</b> [propulsion]"
. += "<br><b>FTL Drive:</b> [drive]</small>"
. += "<hr>"
. += "<br><center><b>Native Database Specifications</b>"
. += "<br><img src = [scanimage]></center>"
. += "<br><small><b>Manufacturer:</b> [designer]"
. += "<br><b>Class Designation:</b> [sizeclass]"
. += "<br><b>Designated Purpose:</b> [shiptype]"
. += "<br><b>Weapons Estimation:</b> [weapons]</small>"
. += "<hr>"
. += "<br><center><b>Native Database Notes</b></center>"
. += "<br><small>[desc]</small>"
/obj/effect/overmap/visitable/ship/get_tooltip_description()
var/description = "<ul>"
description += "<li><b>Manufacturer:</b> [designer]</li>"
description += "<li><b>Class Designation:</b> [sizeclass]</li>"
description += "<li><b>Designated Purpose:</b> [shiptype]</li>"
description += "<li><b>Weapons Estimation:</b> [weapons]</li>"
description += "</ul>"
return description
//Projected acceleration based on information from engines
/obj/effect/overmap/visitable/ship/proc/get_acceleration()
return round(get_total_thrust()/get_vessel_mass(), SHIP_MOVE_RESOLUTION)
//Does actual burn and returns the resulting acceleration
/obj/effect/overmap/visitable/ship/proc/get_burn_acceleration()
return round(burn() / get_vessel_mass(), SHIP_MOVE_RESOLUTION)
/obj/effect/overmap/visitable/ship/proc/get_vessel_mass()
. = vessel_mass
for(var/obj/effect/overmap/visitable/ship/ship in src)
. += ship.get_vessel_mass()
/obj/effect/overmap/visitable/ship/proc/get_speed()
return round(sqrt(speed[1] ** 2 + speed[2] ** 2), SHIP_MOVE_RESOLUTION)
// returns a two-item list with the speed of the ship on x and y axes
/obj/effect/overmap/visitable/ship/proc/get_speed_xy()
return list(round(speed[1], SHIP_MOVE_RESOLUTION), round(speed[2], SHIP_MOVE_RESOLUTION))
/obj/effect/overmap/visitable/ship/get_heading()
var/res = 0
if(MOVING(speed[1]))
if(speed[1] > 0)
res |= EAST
else
res |= WEST
if(MOVING(speed[2]))
if(speed[2] > 0)
res |= NORTH
else
res |= SOUTH
return res
/obj/effect/overmap/visitable/ship/proc/adjust_speed(n_x, n_y)
CHANGE_SPEED_BY(speed[1], n_x)
CHANGE_SPEED_BY(speed[2], n_y)
for(var/zz in map_z)
if(is_still())
toggle_move_stars(zz)
else
toggle_move_stars(zz, fore_dir)
update_icon()
/obj/effect/overmap/visitable/ship/proc/get_brake_path()
if(!get_acceleration())
return INFINITY
if(is_still() || !burn_delay || !get_speed())
return 0
var/num_burns = get_speed() / get_acceleration() + 2 // some padding in case acceleration drops form fuel usage
var/burns_per_grid = 1 / (burn_delay * get_speed())
return round(num_burns / burns_per_grid)
/obj/effect/overmap/visitable/ship/proc/decelerate()
if((!speed[1] && !speed[2]) || !can_burn())
return
// Pythagorean theorem gives us the magnitude of the ship's velocity, which is always an absolute value.
// This is also the mathematical definition for Vector.size
var/magnitude_velocity = ((speed[1] ** 2) + (speed[2] **2)) ** (1/2)
// Get the magnitude of our desired change in velocity
var/alpha = min(get_burn_acceleration(), magnitude_velocity)
// First we "Normalize" the current velocity to get the direction without a distance
// Then we take the exact negative of this direction to get its true opposite
// And finally multiply by the magnitude of our desired delta_v to get the true delta_v
var/delta_x = -(speed[1] / magnitude_velocity) * alpha
var/delta_y = -(speed[2] / magnitude_velocity) * alpha
adjust_speed(delta_x, delta_y)
last_burn = world.time
/obj/effect/overmap/visitable/ship/proc/accelerate(direction, accel_limit)
if(!can_burn())
return
last_burn = world.time
// Get our "Alpha" value as the ship's desired acceleration (change in Velocity)
var/acceleration = min(get_burn_acceleration(), accel_limit)
// Convert from cardinal directions to an angle (in degrees)
// !This is absolutely terrible and should at some point be swapped to Radians
// !But for now it's "Okay" until overmap ships are updated to work on time differentials properly.
var/theta = dir2degree(direction)
// This comes from the actual definition of a Vector2d, <Acos(theta), Asin(theta)>, where theta is an Angle, and A is a constant multiplier that traditionally represents distance.
// In this case A is our DeltaVelocity, or Acceleration.
adjust_speed(acceleration * cos(theta), acceleration * sin(theta))
/obj/effect/overmap/visitable/ship/process()
..()
if(!halted && !is_still())
var/list/deltas = list(0,0)
for(var/i = 1 to 2)
if(MOVING(speed[i]))
position[i] += speed[i] * OVERMAP_SPEED_CONSTANT
if(position[i] < 0)
deltas[i] = CEILING(position[i], 1)
else if(position[i] > 0)
deltas[i] = FLOOR(position[i], 1)
if(deltas[i] != 0)
position[i] -= deltas[i]
position[i] += (deltas[i] > 0) ? -1 : 1
update_icon()
var/turf/newloc = locate(x + deltas[1], y + deltas[2], z)
if(newloc && loc != newloc)
Move(newloc)
handle_wraparound()
sensor_visibility = min(round(base_sensor_visibility + get_speed_sensor_increase(), 1), 100)
/obj/effect/overmap/visitable/ship/update_icon()
pixel_x = position[1] * (world.icon_size/2)
pixel_y = position[2] * (world.icon_size/2)
if(!is_still())
icon_state = moving_state
else
icon_state = initial(icon_state)
for(var/obj/structure/machinery/computer/ship/machine in consoles)
if(machine.z in map_z)
for(var/datum/weakref/W in machine.viewers)
var/mob/M = W.resolve()
if(istype(M) && M.client)
M.client.pixel_x = pixel_x
M.client.pixel_y = pixel_y
..()
/obj/effect/overmap/visitable/ship/proc/burn(var/power_modifier = 1)
for(var/datum/ship_engine/E in engines)
. += E.burn(power_modifier)
/obj/effect/overmap/visitable/ship/proc/get_total_thrust()
for(var/datum/ship_engine/E in engines)
. += E.get_thrust()
/obj/effect/overmap/visitable/ship/proc/can_burn()
if(halted)
return 0
if (world.time < last_burn + burn_delay)
return 0
for(var/datum/ship_engine/E in engines)
. |= E.can_burn()
//deciseconds to next step
/obj/effect/overmap/visitable/ship/proc/ETA()
. = INFINITY
for(var/i = 1 to 2)
if(MOVING(speed[i]))
. = min(., ((speed[i] > 0 ? 1 : -1) - position[i]) / speed[i])
if(. == INFINITY)
. = 0
. = max(CEILING(., 1),0)
/obj/effect/overmap/visitable/ship/proc/halt()
adjust_speed(-speed[1], -speed[2])
halted = 1
/obj/effect/overmap/visitable/ship/proc/unhalt()
if(!SSshuttle.overmap_halted)
halted = 0
/obj/effect/overmap/visitable/ship/Bump(var/atom/A)
if(istype(A,/turf/unsimulated/map/edge))
handle_wraparound()
..()
/obj/effect/overmap/visitable/ship/populate_sector_objects()
..()
for(var/obj/structure/machinery/computer/ship/S in SSmachinery.machinery)
S.attempt_hook_up(src)
for(var/obj/structure/machinery/computer/shuttle_control/explore/C in SSmachinery.machinery)
C.attempt_hook_up(src)
for(var/datum/ship_engine/E in GLOB.ship_engines)
if(check_ownership(E.holder))
engines |= E
/obj/effect/overmap/visitable/ship/proc/get_landed_info()
return "This ship cannot land."
/obj/effect/overmap/visitable/ship/proc/can_combat_roll()
if(!can_burn())
return FALSE
var/cooldown = min(vessel_mass / 100, 100) SECONDS //max 100s for horizon, 50s for Intrepid
if(world.time >= (last_combat_roll + cooldown))
return TRUE
return FALSE
/obj/effect/overmap/visitable/ship/proc/can_combat_turn()
if(!can_burn())
return FALSE
var/cooldown = min(vessel_mass / 200, 20) SECONDS //max 20s for horizon
if(world.time >= (last_combat_turn + cooldown))
return TRUE
return FALSE
/obj/effect/overmap/visitable/ship/proc/can_turn()
if(!can_burn())
return FALSE
var/cooldown = min(vessel_mass / 10, 1) SECONDS //max 1s for horizon
if(world.time >= (last_turn + cooldown))
return TRUE
return FALSE
/obj/effect/overmap/visitable/ship/proc/turn_ship(var/new_dir)
burn(0.25)
var/angle = new_dir == WEST ? 45 : -45
dir = turn(dir, angle)
update_icon()
last_turn = world.time
/obj/effect/overmap/visitable/ship/proc/combat_turn(var/new_dir)
burn(1.0)
var/angle = -45
if(new_dir == WEST)
angle = 45
var/direction = turn(dir, angle)
accelerate(direction, 1000)
if(direction & EAST)
speed[1] = abs(speed[1])
else if(direction & WEST)
if(speed[1] > 0)
speed[1] = -speed[1]
else
speed[1] = 0
if(direction & NORTH)
speed[2] = abs(speed[2])
else if(direction & SOUTH)
if(speed[2] > 0)
speed[2] = -speed[2]
else
speed[2] = 0
update_icon()
dir = get_heading()
for(var/mob/living/L in GLOB.living_mob_list)
if(L.z in map_z)
if(!gravity_generator?.on && !L.anchored)
to_chat(L, SPAN_DANGER("The ship rapidly turns beneath you!"))
if(!L.buckled_to)
L.Weaken(3)
else
to_chat(L, SPAN_WARNING("The ship turns beneath you, but the artificial gravity keeps you on your feet."))
shake_camera(L, 1 SECOND, 2)
L.playsound_local(soundin = 'sound/machines/thruster.ogg', vol = 50)
last_combat_turn = world.time
/obj/effect/overmap/visitable/ship/proc/combat_roll(var/new_dir)
burn()
var/dir_to_move = turn(dir, new_dir == WEST ? 90 : -90)
forceMove(get_step(src, dir_to_move))
for(var/mob/living/L in GLOB.living_mob_list)
if(L.z in map_z)
if(!gravity_generator?.on && !L.anchored)
to_chat(L, SPAN_DANGER("<font size=4>The ship rapidly inclines beneath you!</font>"))
if(!L.buckled_to)
var/turf/T = get_step_away(get_turf(L), get_step(L, new_dir), 10)
L.throw_at(T, 10, 10)
else
to_chat(L, SPAN_WARNING("The ship inclines beneath you, but the artificial gravity keeps you on your feet."))
shake_camera(L, 2 SECONDS, 10)
sound_to(L, sound('sound/effects/combatroll.ogg'))
last_combat_roll = world.time
/obj/effect/overmap/visitable/ship/signal_hit(var/list/hit_data)
for(var/obj/structure/machinery/computer/ship/targeting/TR in consoles)
TR.visible_message(SPAN_NOTICE("[icon2html(src, viewers(get_turf(TR)))] Hit confirmed on [hit_data["target_name"]] in [hit_data["target_area"]] at coordinates [hit_data["coordinates"]]."), range = 2)
/obj/effect/overmap/visitable/ship/proc/get_speed_sensor_increase()
return min(get_speed() * 1000, 50) //Engines should never increase sensor visibility by more than 50.
#undef OVERMAP_SPEED_CONSTANT
#undef MOVING
#undef SANITIZE_SPEED
#undef CHANGE_SPEED_BY