Merge branch 'master' of https://github.com/tgstation/tgstation into upstream-23-10-2025

This commit is contained in:
Roxy
2025-10-23 17:38:23 -04:00
453 changed files with 20635 additions and 15897 deletions
@@ -0,0 +1,673 @@
#define ENGINE_UNWRENCHED 0
#define ENGINE_WRENCHED 1
#define ENGINE_WELDED 2
#define CARGO_HITBOX_LAYER (ABOVE_ALL_MOB_LAYER)
#define CART_ROOF_LAYER (CARGO_HITBOX_LAYER + 0.01)
#define CART_LOWER_LAYER (OBJ_LAYER)
#define FAKE_GLIDE_INITIAL_SOURCE "fake_glide_initial"
#define GOLFCART_RUN_OVER_DAMAGE (25)
/obj/vehicle/ridden/golfcart
name = "golf cart"
desc = "An all-purpose cargo hauling vehicle."
icon = 'icons/obj/toys/golfcart_split.dmi'
icon_state = "front"
max_integrity = 100
armor_type = /datum/armor/none
pass_flags_self = parent_type::pass_flags_self | LETPASSCLICKS
integrity_failure = 0.5
layer = ABOVE_MOB_LAYER
max_occupants = 1
key_type = /obj/item/key/golfcart
///Perform an extra step after movement finishes?
var/perform_extra_step = FALSE
///Base movespeed before any modifiers. Humans run at 1.5 movedelay.
var/static/base_movedelay = 1.25
///Base movespeed for the hotrod before any modifiers
var/static/hotrod_base_movedelay = 0.65
///Particle holder for low integrity smoking
var/obj/effect/abstract/particle_holder/smoke = null
///Seperate image for the cargo buckled to the rear
var/image/cargo_image = null
///The power source for the cart. Can be replaced with an engine.
var/obj/item/stock_parts/power_store/cell/cell = null
///A more powerful power source for the cart.
var/obj/item/v8_engine/engine = null
///Can be unwrenched, wrenched, or welded
var/engine_state = null
///An invisible sprite that exists as a hitbox
var/obj/golfcart_rear/child = null
///Objects that can be buckled to the cargo hitch
var/static/list/allowed_cargo = typecacheof(list(
/obj/structure/closet/crate,
/obj/structure/reagent_dispensers,
/obj/structure/flatpack_cart,
/obj/machinery,
/obj/item/kirbyplants,
))
///Each movement requires this much energy to be drawn from the internal cell
var/charge_per_move = STANDARD_CELL_CHARGE / 300
///Has the final say on whether something can be buckled.
var/static/list/banned_cargo = typecacheof(list(
/obj/structure/reagent_dispensers/wall,
// i mean it's a fucking door
/obj/machinery/door,
))
///Is the hood open?
var/hood_open = FALSE
/obj/item/key/golfcart
name = "golfcart key"
desc = "A small grey key for using the golf cart."
icon = 'icons/obj/toys/golfcart_split.dmi'
/obj/item/golfcart_kit
name = "golfcart parts kit"
desc = "A box containing a golf cart. Some assembly required. Batteries not included."
icon = 'icons/obj/toys/golfcart_split.dmi'
icon_state = "parts_kit"
w_class = WEIGHT_CLASS_HUGE
throw_range = 2
item_flags = SLOWS_WHILE_IN_HAND | IMMUTABLE_SLOW
slowdown = 2.5
drag_slowdown = 3.5
/obj/item/golfcart_kit/examine(mob/user)
. = ..()
. += span_notice("The instructions say that it needs to be [EXAMINE_HINT("screwed")] together.")
/obj/item/golfcart_kit/proc/play_building_noises(mob/living/user, duration)
duration = max(duration - (1 SECONDS), 0.5 SECONDS)
playsound(src, 'sound/items/poster/poster_ripped.ogg', 50, TRUE)
sleep(1 SECONDS)
if (!DOING_INTERACTION_WITH_TARGET(user, src))
return
playsound(src, 'sound/items/tools/screwdriver_operating.ogg', 50, TRUE)
sleep(duration / 2)
if (!DOING_INTERACTION_WITH_TARGET(user, src))
return
playsound(src, 'sound/items/tools/ratchet.ogg', 50, TRUE)
sleep(duration / 2)
if (!DOING_INTERACTION_WITH_TARGET(user, src))
return
playsound(src, 'sound/items/tools/screwdriver.ogg', 50, TRUE)
/obj/item/golfcart_kit/screwdriver_act(mob/living/user, obj/item/tool)
if (!isturf(loc))
user.balloon_alert(user, "set down first!")
return ITEM_INTERACT_BLOCKING
user.visible_message(span_notice("[user] starts putting together the [src]..."), span_notice("You start assembling the [src]..."))
var/unboxing_duration = 7 SECONDS
INVOKE_ASYNC(src, PROC_REF(play_building_noises), user, unboxing_duration * tool.toolspeed)
if(!tool.use_tool(src, user, unboxing_duration))
return ITEM_INTERACT_BLOCKING
if (!isturf(loc))
return ITEM_INTERACT_BLOCKING
var/obj/vehicle/ridden/golfcart/cart = new(get_turf(src))
user.visible_message(span_notice("[user] assembles the [cart]!"), span_notice("You assemble the [cart]."))
qdel(src)
/obj/vehicle/ridden/golfcart/atom_break()
. = ..()
if (smoke)
return
smoke = new(src, /particles/smoke/ash)
/obj/vehicle/ridden/golfcart/atom_fix()
. = ..()
if (smoke)
QDEL_NULL(smoke)
/obj/vehicle/ridden/golfcart/atom_destruction()
explosion(src, devastation_range = -1, light_impact_range = 2, flame_range = 3, flash_range = 4)
return ..()
///Jiggles the cargo_image up and down.
/obj/vehicle/ridden/golfcart/proc/shake_cargo(pixelshiftx = 2, pixelshifty = 2, duration)
if (!cargo_image)
return
var/inital_pixel_x = cargo_image.pixel_x
var/inital_pixel_y = cargo_image.pixel_y
animate(cargo_image, pixel_x = inital_pixel_x + rand(-pixelshiftx, pixelshiftx), pixel_y = inital_pixel_y + rand(pixelshifty/2, pixelshifty), time=duration, flags=ANIMATION_PARALLEL)
animate(pixel_x = inital_pixel_x, pixel_y = inital_pixel_y, time=duration)
/obj/vehicle/ridden/golfcart/proc/is_hotrod()
return engine && engine_state == ENGINE_WELDED
///Called when something we crash into lands after being flinged
/obj/vehicle/ridden/golfcart/proc/thrown_mob_landed(atom/thrown_atom)
if (!isliving(thrown_atom))
UnregisterSignal(thrown_atom, COMSIG_MOVABLE_THROW_LANDED)
return
var/mob/living/thrown_mob = thrown_atom
thrown_mob.Knockdown(3 SECONDS)
UnregisterSignal(thrown_atom, COMSIG_MOVABLE_THROW_LANDED)
/obj/vehicle/ridden/golfcart/Bump(atom/bumped_atom)
if (..())
return
if (!is_hotrod())
return
if(!isliving(bumped_atom))
return
var/mob/living/mob = bumped_atom
mob.throw_at(get_edge_target_turf(mob, dir), 2, 3)
RegisterSignal(mob, COMSIG_MOVABLE_THROW_LANDED, PROC_REF(thrown_mob_landed))
mob.visible_message(
span_danger("[src] hits [mob] at full speed!"),
span_userdanger("[src] slams into you!"),
)
///Called when a resting victim is run over
/obj/vehicle/ridden/golfcart/proc/run_over(mob/living/victim)
if (!has_gravity())
victim.throw_at(get_edge_target_turf(victim, dir), 4, 3)
RegisterSignal(victim, COMSIG_MOVABLE_THROW_LANDED, PROC_REF(thrown_mob_landed))
return
if (istype(victim, /mob/living/carbon))
var/mob/living/carbon/person = victim
if (person.body_position == LYING_DOWN)
log_combat(src, victim, "run over", addition = "(DAMTYPE: [uppertext(BRUTE)])")
playsound(src, 'sound/effects/pop_expl.ogg', 50, TRUE)
playsound(src, 'sound/effects/splat.ogg', 50, TRUE)
victim.visible_message(
span_danger("[src] drives over [victim]!"),
span_userdanger("[src] drives over you!"),
)
var/damage = rand(GOLFCART_RUN_OVER_DAMAGE - GOLFCART_RUN_OVER_DAMAGE / 5, GOLFCART_RUN_OVER_DAMAGE + GOLFCART_RUN_OVER_DAMAGE / 5)
//adds up to exactly 1
person.apply_damage((1/3) * damage, BRUTE, BODY_ZONE_HEAD)
person.apply_damage((1/3) * damage, BRUTE, BODY_ZONE_CHEST)
person.apply_damage((1/12) * damage, BRUTE, BODY_ZONE_L_LEG)
person.apply_damage((1/12) * damage, BRUTE, BODY_ZONE_R_LEG)
person.apply_damage((1/12) * damage, BRUTE, BODY_ZONE_L_ARM)
person.apply_damage((1/12) * damage, BRUTE, BODY_ZONE_R_ARM)
add_mob_blood(person)
var/turf/below_us = get_turf(src)
below_us.add_mob_blood(person)
AddComponent(/datum/component/blood_walk, \
blood_type = /obj/effect/decal/cleanable/blood/tracks, \
target_dir_change = TRUE, \
transfer_blood_dna = TRUE, \
max_blood = 4)
/obj/vehicle/ridden/golfcart/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
. = ..()
if (!is_hotrod())
return
for(var/mob/living/future_pancake in loc)
run_over(future_pancake)
/obj/vehicle/ridden/golfcart/mouse_drop_receive(atom/dropped, mob/user, params)
if (child.can_load(dropped))
return child.mouse_drop_receive(dropped, user, params)
return ..()
/obj/vehicle/ridden/golfcart/item_interaction(mob/living/user, obj/item/attacking_item, list/modifiers)
if (!hood_open)
return ..()
if (istype(attacking_item, /obj/item/v8_engine))
if (engine || cell)
balloon_alert(user, "already has an engine!")
return ITEM_INTERACT_BLOCKING
user.transferItemToLoc(attacking_item, src)
engine = attacking_item
engine_state = ENGINE_UNWRENCHED
balloon_alert(user, "installed \the [engine]")
return ITEM_INTERACT_SUCCESS
if (istype(attacking_item, /obj/item/stock_parts/power_store/cell))
if (cell || engine)
balloon_alert(user, "already has an engine!")
return ITEM_INTERACT_BLOCKING
user.transferItemToLoc(attacking_item, src)
cell = attacking_item
balloon_alert(user, "installed \the [cell]")
return ITEM_INTERACT_SUCCESS
return ..()
/obj/vehicle/ridden/golfcart/attack_hand(mob/living/user, list/modifiers)
if (!hood_open)
return ..()
if (engine && engine_state == ENGINE_UNWRENCHED)
. = TRUE
var/obj/item/engine_item = engine
engine_state = null
engine = null
if (user.put_in_hands(engine_item))
return
engine_item.forceMove(drop_location())
return
if (isnull(cell))
return ..()
var/obj/item/stock_parts/power_store/cell/cell_to_take = cell
cell = null
. = TRUE
if (user.put_in_hands(cell_to_take))
return
cell_to_take.forceMove(drop_location())
/obj/vehicle/ridden/golfcart/proc/can_wrench_engine()
return hood_open && engine && (engine_state == ENGINE_UNWRENCHED || engine_state == ENGINE_WRENCHED)
/obj/vehicle/ridden/golfcart/proc/can_weld_engine()
return hood_open && engine && (engine_state == ENGINE_WRENCHED || engine_state == ENGINE_WELDED)
/obj/vehicle/ridden/golfcart/proc/set_engine_state(state)
engine_state = state
if (engine_state == ENGINE_WELDED && icon != 'icons/obj/toys/golfcart_hotrod_split.dmi')
icon = 'icons/obj/toys/golfcart_hotrod_split.dmi'
update_appearance(UPDATE_ICON)
else if (engine_state != ENGINE_WELDED && icon != initial(icon))
icon = initial(icon)
/obj/vehicle/ridden/golfcart/wrench_act(mob/living/user, obj/item/tool)
if (!can_wrench_engine())
return ..()
tool.play_tool_sound(src, 50)
if (!tool.use_tool(src, user, 3 SECONDS, extra_checks = CALLBACK(src, PROC_REF(can_wrench_engine))))
return ITEM_INTERACT_BLOCKING
if (engine_state == ENGINE_WRENCHED)
set_engine_state(ENGINE_UNWRENCHED)
else if (engine_state == ENGINE_UNWRENCHED)
set_engine_state(ENGINE_WRENCHED)
playsound(src, 'sound/items/deconstruct.ogg', 50, TRUE)
return ITEM_INTERACT_SUCCESS
/obj/vehicle/ridden/golfcart/welder_act(mob/living/user, obj/item/tool)
if (user.combat_mode)
return
if(!tool.tool_start_check(user, heat_required = HIGH_TEMPERATURE_REQUIRED, amount = 1))
return ITEM_INTERACT_BLOCKING
. = ITEM_INTERACT_SUCCESS
if (hood_open)
if(!tool.use_tool(src, user, 3 SECONDS, amount = 1, volume = 50, extra_checks = CALLBACK(src, PROC_REF(can_weld_engine))))
return ITEM_INTERACT_BLOCKING
if (engine_state == ENGINE_WRENCHED)
set_engine_state(ENGINE_WELDED)
else if (engine_state == ENGINE_WELDED)
set_engine_state(ENGINE_WRENCHED)
else
if(DOING_INTERACTION(user, src))
balloon_alert(user, "already repairing it!")
return
if(atom_integrity >= max_integrity)
balloon_alert(user, "it's not damaged!")
return
// takes 10 seconds to repair from full
balloon_alert(user, "started repairing")
if (!tool.use_tool(src, user, ((max_integrity - atom_integrity) / max_integrity * 10) SECONDS, volume = 50))
balloon_alert(user, "repair interrupted!")
return
repair_damage(max_integrity - atom_integrity)
balloon_alert(user, "repaired")
return
/obj/vehicle/ridden/golfcart/proc/toggle_hood()
hood_open = !hood_open
update_appearance(UPDATE_ICON)
/obj/vehicle/ridden/golfcart/click_alt(mob/user)
if (user in buckled_mobs)
return ..()
else
to_chat(user, span_warning("You must be sitting down to remove the key!"))
. = CLICK_ACTION_SUCCESS
toggle_hood()
if (hood_open)
to_chat(user, span_notice("You pop \the [src]'s hood."))
else
to_chat(user, span_notice("You shut \the [src]'s hood."))
/obj/vehicle/ridden/golfcart/examine_more(mob/user)
. = ..()
if (!child.cargo)
return
. += span_slightly_larger("It is currently transporting the [child.cargo]")
. += child.cargo.examine(user)
/obj/vehicle/ridden/golfcart/examine(mob/user)
. = ..()
. += span_notice("Pop the hood by alt-clicking while not riding it.")
if (child.cargo)
. += span_info("The bed is holding \the [child.cargo].")
if(!in_range(user, src) && !issilicon(user) && !isobserver(user))
. += span_warning("You're too far away to examine [src] closely.")
return
if (!engine)
var/power = 0
if (cell)
power = floor(cell.charge / cell.maxcharge * 100)
. += span_info("It is currently is at [power]% charge.")
if (hood_open)
. += span_warning("The hood is open!")
if (engine)
. += span_info("You can see \the [engine] inside.")
if (engine_state == ENGINE_UNWRENCHED)
. += span_notice("It needs to be [EXAMINE_HINT("wrenched")] into place.")
else if (engine_state == ENGINE_WRENCHED)
. += span_notice("It needs to be [EXAMINE_HINT("welded")] down.")
// last state is ENGINE_WELDED
else if (cell)
. += span_info("You can see \the [cell] inside.")
. += span_smallnotice("If you remove the cell you could probably install another power source...")
else
. += span_info("There is no power cell installed.")
///Called when something tries to pass us. Returns TRUE if it is trying to crawl past us.
/obj/vehicle/ridden/golfcart/proc/allow_crawler_through(atom/crawler)
if (!isliving(crawler))
return FALSE
var/mob/living/living_crawler = crawler
return living_crawler.body_position == LYING_DOWN
/obj/vehicle/ridden/golfcart/CanAllowThrough(atom/movable/mover, border_dir)
. = ..()
if (mover == child)
return TRUE
if (mover in child.buckled_mobs)
return TRUE
if (allow_crawler_through(mover))
return TRUE
///Called for COMSIG_MOVABLE_PRE_MOVE on the golfcart front. If the rear of the cart doesn't fit where we want to go, block movement.
/obj/vehicle/ridden/golfcart/proc/pre_move(atom/source, atom/new_loc)
SIGNAL_HANDLER
// see if space behind new loc is free
var/atom/behind = get_step(new_loc, turn(dir, 180))
if ((!behind.Enter(child, child.loc)) && behind != get_step(src, 0))
return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
// otherwise permit move
return
///Makes movedelay a factor of base_movedelay
/obj/vehicle/ridden/golfcart/proc/set_movedelay_effect(modification)
var/base_movedelay_effect = base_movedelay
if (is_hotrod())
base_movedelay_effect = hotrod_base_movedelay
movedelay = base_movedelay_effect * modification
child.set_glide_size(DELAY_TO_GLIDE_SIZE(movedelay))
///Sets perform_extra_step to TRUE if we are going up stairs.
/obj/vehicle/ridden/golfcart/zMove(dir, turf/target, z_move_flags)
var/can_do_extra_step = FALSE
if (currently_z_moving == CURRENTLY_Z_ASCENDING)
can_do_extra_step = TRUE
. = ..()
if (!.)
return
perform_extra_step = perform_extra_step || can_do_extra_step
///Creates a fake glide effect on the golfcart and anything buckled to it. Called when moving up stairs as moving up stairs creates a two-tile move.
/obj/vehicle/ridden/golfcart/proc/fake_glide(direct)
var/px = 0
var/py = 0
if (direct & NORTH)
py -= 32
else if (direct & SOUTH)
py += 32
if (direct & EAST)
px -= 32
else if (direct & WEST)
px += 32
pixel_x += px
pixel_y += py
child.pixel_x += px
child.pixel_y += py
animate(src, movedelay, pixel_x=0, pixel_y=0)
animate(child, movedelay, pixel_x=0, pixel_y=0)
for (var/mob/buckled in (buckled_mobs + child.buckled_mobs))
var/inital_pixel_x = buckled.pixel_x
var/inital_pixel_y = buckled.pixel_y
buckled.pixel_x += px
buckled.pixel_y += py
animate(buckled, movedelay, pixel_x=inital_pixel_x, pixel_y=inital_pixel_y)
if (buckled.client)
var/client/client = buckled.client
var/initial_client_pixel_x = client.pixel_x
var/initial_client_pixel_y = client.pixel_y
client.pixel_x += px
client.pixel_y += py
animate(client, movedelay, pixel_x=initial_client_pixel_x, pixel_y=initial_client_pixel_y)
/obj/vehicle/ridden/golfcart/Move(atom/newloc, direct, glide_size_override = 0, update_dir = TRUE)
perform_extra_step = FALSE
var/atom/old_loc = get_turf(src)
var/old_dir = dir
if (get_turf(child) == newloc)
set_movedelay_effect(2)
. = ..(newloc, turn(direct, 180))
else
set_movedelay_effect(1)
. = ..()
var/atom/behind = get_step(src, turn(dir, 180))
if (old_dir != dir && get_turf(src) == old_loc)
if (!behind.Enter(child, child.loc))
setDir(old_dir)
behind = get_step(src, turn(dir, 180))
update_appearance(UPDATE_ICON)
child.move_from_parent(behind)
// working theory is that somehow the cart can't move onto stairs -> perform_extra_step is true but loc is never set to newloc
if ((perform_extra_step && loc.z != old_loc.z) || loc.z < old_loc.z)
// this should be called when descending Z levels, or when going up stairs
if (!Move(get_step(src, dir), dir, glide_size_override, update_dir))
return
fake_glide(dir)
///Called for COMSIG_ATOM_TRIED_PASS on the golfcart front. Allows mobs buckled to the rear of the cart to not get blocked by the front of the cart.
/obj/vehicle/ridden/golfcart/proc/allow_movement_between_passengers(atom/source, atom/mover)
SIGNAL_HANDLER
if (mover in child.buckled_mobs)
return COMSIG_COMPONENT_PERMIT_PASSAGE
/obj/vehicle/ridden/golfcart/Initialize(mapload, direction = null)
. = ..()
AddElement(/datum/element/ridable, /datum/component/riding/vehicle/golfcart)
RegisterSignal(src, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(pre_move))
child = new /obj/golfcart_rear(null, src)
if (isnull(direction))
if (get_step(src, NORTH).Enter(child))
direction = NORTH
else if (get_step(src, EAST).Enter(child))
direction = EAST
else if (get_step(src, WEST).Enter(child))
direction = WEST
else if (get_step(src, SOUTH).Enter(child))
direction = SOUTH
else
direction = SOUTH
direction = turn(direction, 180)
setDir(direction)
child.loc = get_step(src, turn(dir, 180))
update_appearance()
/obj/vehicle/ridden/golfcart/loaded/Initialize(mapload)
. = ..()
cell = new /obj/item/stock_parts/power_store/cell/lead(src)
/obj/vehicle/ridden/golfcart/hotrod/Initialize(mapload)
. = ..()
engine = new /obj/item/v8_engine(src)
set_engine_state(ENGINE_WELDED)
/obj/vehicle/ridden/golfcart/update_appearance(updates=ALL)
. = ..()
child.setDir(dir)
child.update_appearance(updates)
/obj/vehicle/ridden/golfcart/proc/get_cargo_offsets()
var/crate_x_offset = 0
var/crate_y_offset = 0
if (dir & NORTH)
crate_y_offset += -30
else if (dir & SOUTH)
crate_y_offset += 30
else if (dir & EAST)
crate_x_offset += -32
else if (dir & WEST)
crate_x_offset += 32
return vector(crate_x_offset, crate_y_offset)
///Flattens the attached cargo into a list of mutable_appearances with proper layering to fit between layer and max_layer
/obj/vehicle/ridden/golfcart/proc/generate_cargo_overlay(crate_x_offset = 0, crate_y_offset = 0, layer=null, max_layer=null, shift_all=TRUE)
if (!child.cargo)
return
var/obj/cargo = child.cargo
if (!layer)
layer = src.layer
if (!max_layer)
max_layer = ABOVE_ALL_MOB_LAYER
var/vector/offsets = get_cargo_offsets()
offsets.x += crate_x_offset
offsets.y += crate_y_offset
var/crate_layer_offset = 0
if (dir & NORTH)
crate_layer_offset = 0.01
else if (dir & SOUTH)
crate_layer_offset = -0.01
var/list/overlays = list()
if (cargo.icon)
overlays += mutable_appearance(cargo.icon, cargo.icon_state, cargo.layer)
overlays += cargo.update_overlays()
var/base_cargo_layer = layer + crate_layer_offset
for(var/i in 1 to overlays.len)
var/entry = overlays[i]
var/mutable_appearance/overlay = entry
if(istext(entry))
overlay = mutable_appearance(cargo.icon, entry, base_cargo_layer)
overlays[i] = overlay
// nested lists may exist and i can't be asked to flatten them
if (!isnull(overlay))
// preserves relative offsets
if (overlay.layer > 0)
overlay.layer = min(base_cargo_layer + (overlay.layer - cargo.layer), max_layer)
else
overlay.layer = min(base_cargo_layer + (overlay.layer * -0.01) - 0.01, max_layer)
if (shift_all || i == 1)
overlay.pixel_x += offsets.x
overlay.pixel_y += offsets.y
overlay.pixel_z += 11
return overlays
///Gets the pixel offsets of the rear part of the golf cart
/obj/vehicle/ridden/golfcart/proc/get_rear_offset()
var/x = 0
var/y = 0
if (dir & NORTH)
y = -32
else if (dir & SOUTH)
y = 32
else if (dir & EAST)
x = -32
else if (dir & WEST)
x = 32
return vector(x, y)
/obj/vehicle/ridden/golfcart/update_overlays()
. = ..()
// the overlays for the cart are fairly complicated.
// the main three are the front, rear, and lower overlays
// the front/rear overlay are the same layer
// but the lower overlay is always below buckled mobs and the cargo
var/mutable_appearance/lower_overlay = mutable_appearance(icon, "lower", CART_LOWER_LAYER)
var/mutable_appearance/roof_overlay = null
var/mutable_appearance/rear_overlay = mutable_appearance(icon, "rear", layer)
var/vector/rear_offsets = get_rear_offset()
rear_overlay.pixel_x = rear_offsets.x
rear_overlay.pixel_y = rear_offsets.y
if (dir & SOUTH)
// however, specifically when facing south, we require another overlay.
// it is effectively an extension of the lower overlay, but it has to be on a different tile so it has to be a different overlay
var/mutable_appearance/floor_overlay = mutable_appearance(icon, "floor", CART_LOWER_LAYER)
floor_overlay.pixel_y += 25
. += floor_overlay
lower_overlay.pixel_y = 32
roof_overlay = mutable_appearance(icon, "roof", CART_ROOF_LAYER)
roof_overlay.pixel_y = 32
else if (dir & EAST)
lower_overlay.pixel_x = -32
lower_overlay.layer -= 0.02
roof_overlay = mutable_appearance(icon, "roof", CART_ROOF_LAYER)
roof_overlay.pixel_y = 31
roof_overlay.pixel_x = -10
else if (dir & WEST)
lower_overlay.pixel_x = 32
lower_overlay.layer -= 0.02
roof_overlay = mutable_appearance(icon, "roof", CART_ROOF_LAYER)
roof_overlay.pixel_y = 31
roof_overlay.pixel_x = 10
. += lower_overlay
. += rear_overlay
if (hood_open)
. += mutable_appearance(icon, "hood", layer + 0.01)
if (roof_overlay)
. += roof_overlay
if (child.cargo)
// the cargo is a seperate vis_overlay so that it can be animate()d
vis_contents -= cargo_image
cargo_image = null
var/vector/offsets = get_cargo_offsets()
var/list/overlays = generate_cargo_overlay(max_layer=CARGO_HITBOX_LAYER, shift_all=FALSE)
if (overlays.len)
var/mutable_appearance/base_overlay = overlays[1]
overlays.Remove(base_overlay)
cargo_image = SSvis_overlays.add_vis_overlay(src, base_overlay.icon, base_overlay.icon_state, base_overlay.layer, plane, dir)
cargo_image.overlays = overlays
cargo_image.pixel_x = offsets.x
cargo_image.pixel_y = offsets.y
cargo_image.pixel_z = 11
cargo_image.layer = base_overlay.layer
vis_contents += cargo_image
else
cargo_image = null
///Allows mobs sitting in the rear of the cart to not shoot either the front of the cart or the driver of the cart.
/obj/vehicle/ridden/golfcart/proc/dodge_friendly_fire(mob/source, obj/projectile/projectile)
SIGNAL_HANDLER
if (!projectile.firer)
return
if (QDELETED(projectile.firer))
return
// so that you don't murder your driver when shooting off the back
if (projectile.firer in child.buckled_mobs)
return PROJECTILE_INTERRUPT_HIT_PHASE
/obj/vehicle/ridden/golfcart/post_buckle_mob(mob/living/M)
if (M.pulling)
M.stop_pulling()
RegisterSignal(M, COMSIG_PROJECTILE_PREHIT, PROC_REF(dodge_friendly_fire))
RegisterSignal(M, COMSIG_ATOM_TRIED_PASS, PROC_REF(allow_movement_between_passengers))
return ..()
/obj/vehicle/ridden/golfcart/post_unbuckle_mob(mob/living/M)
update_appearance(UPDATE_ICON) // because for some reason the overlays aren't properly redrawn
UnregisterSignal(M, COMSIG_ATOM_TRIED_PASS)
UnregisterSignal(M, COMSIG_PROJECTILE_PREHIT)
return ..()
/obj/vehicle/ridden/golfcart/Destroy()
UnregisterSignal(src, COMSIG_MOVABLE_PRE_MOVE)
if (!QDELETED(child))
qdel(child)
child = null
return ..()
#undef ENGINE_UNWRENCHED
#undef ENGINE_WRENCHED
#undef ENGINE_WELDED
#undef CARGO_HITBOX_LAYER
#undef CART_ROOF_LAYER
#undef CART_LOWER_LAYER
#undef FAKE_GLIDE_INITIAL_SOURCE
#undef GOLFCART_RUN_OVER_DAMAGE
@@ -0,0 +1,388 @@
#define GOLFCART_RIDING_SOURCE "riding_golfcart"
#define HUMAN_LOWER_LAYER (MOB_LAYER)
#define CARGO_HITBOX_LAYER (ABOVE_ALL_MOB_LAYER)
#define BELOW_HUMAN_HITBOX_LAYER (ABOVE_MOB_LAYER + 0.01)
#define HUMAN_RIDING_LAYER (BELOW_HUMAN_HITBOX_LAYER + 0.02)
/obj/golfcart_rear
name = "golf cart bed"
icon = 'icons/obj/toys/golfcart_split.dmi'
icon_state = "rear_hitbox"
density = TRUE
alpha = 1
can_buckle = TRUE
max_buckled_mobs = 2
glide_size = MAX_GLIDE_SIZE
layer = 0
///Currently buckled cargo
var/obj/cargo = null
///Was this move triggered by the parent?
var/moving_from_parent = FALSE
var/obj/vehicle/ridden/golfcart/parent = null
///List of offsets for buckled passengers. Indexed by passenger index, then by direction string.
var/static/list/list/vector/passenger_offsets = list(
list(
TEXT_NORTH = vector(-4, 0, 24),
TEXT_SOUTH = vector(4, 0, 4),
TEXT_EAST = vector(4, 0, 13),
TEXT_WEST = vector(-4, 0, 13)
),
list(
TEXT_NORTH = vector(4, 0, 20),
TEXT_SOUTH = vector(-4, 8, 0),
TEXT_EAST = vector(-8, 0, 13),
TEXT_WEST = vector(8, 0, 13)
)
)
///Same as [/obj/golfcart_rear/passenger_offsets], except for when the passenger is lying down.
var/static/list/list/vector/lying_down_passenger_offsets = list(
list(
TEXT_NORTH = vector(0, 0, 16),
TEXT_SOUTH = vector(0, 0, 8),
TEXT_EAST = vector(2, 0, 8),
TEXT_WEST = vector(-2, 0, 8),
)
)
///Try to load something onto the cart. This proc may fail if the obj is not in allowed_cargo or is in banned_cargo.
/obj/golfcart_rear/proc/load(obj/to_load)
if (!to_load)
return
if (cargo)
return
if (to_load.anchored)
return
if (to_load.has_buckled_mobs())
// can't stack buckles and whatever
return
if (istype(to_load, /obj/structure/closet))
var/obj/structure/closet/crate = to_load
crate.close()
to_load.forceMove(src)
cargo = to_load
layer = CARGO_HITBOX_LAYER
parent.update_appearance(UPDATE_ICON)
/obj/golfcart_rear/proc/unload()
if (!cargo)
return
var/list/candidates = list(
get_step(src, turn(dir, 180)),
get_step(src, turn(dir, 90)),
get_step(src, turn(dir, 270)),
)
var/atom/dropoff = get_turf(src)
for (var/atom/turf in candidates)
if (turf.Enter(cargo, src))
dropoff = turf
break
cargo.forceMove(dropoff)
cargo = null
layer = BELOW_HUMAN_HITBOX_LAYER
parent.update_appearance(UPDATE_ICON)
///Jiggles the cargo_image as long as someone is trying to jiggle it.
/obj/golfcart_rear/proc/check_if_shake()
if (!cargo)
return FALSE
// Assuming we decide to shake again, how long until we check to shake again
var/next_check_time = 0.75 SECONDS
// How long we shake between different calls of Shake(), so that it starts shaking and stops, instead of a steady shake
var/shake_duration = 0.125 SECONDS
for(var/mob/living/mob in cargo.contents)
if(DOING_INTERACTION_WITH_TARGET(mob, src))
// Shake and queue another check_if_shake
parent.shake_cargo(1, 6, shake_duration)
addtimer(CALLBACK(src, PROC_REF(check_if_shake)), next_check_time)
return TRUE
// If we reach here, nobody is resisting, so don't shake
return FALSE
/obj/golfcart_rear/proc/after_escape(obj/container, mob/living/user)
user?.visible_message(
span_danger("The [container] falls off of the [src]!"),
span_userdanger("You knock the crate off the [src]!")
)
container.SpinAnimation(5, 1)
if (user && istype(container, /obj/structure/closet))
var/obj/structure/closet/closet = container
if (closet.can_open(user))
closet.open()
///Unload the container from the golfcart if it is cargo
/obj/golfcart_rear/proc/easy_escape(mob/living/user, obj/container)
if (!cargo || cargo != container)
return
unload()
after_escape(container, user)
///Unload the container from the golfcart if it is cargo and after a little jiggling and a some time
/obj/golfcart_rear/proc/hard_escape(mob/living/user, obj/container)
addtimer(CALLBACK(src, PROC_REF(check_if_shake)), 0)
if (do_after(user, 5 SECONDS, target=src, timed_action_flags=IGNORE_USER_LOC_CHANGE))
if (!cargo || cargo != container || !(user in cargo))
return
unload()
after_escape(container, user)
///Called when someone resists inside of the cargo hitch.
/obj/golfcart_rear/relay_container_resist_act(mob/living/user, obj/container)
user.visible_message(
span_danger("[user] tries to escape the [container]!"),
span_userdanger("You try to escape the [container]!"),
)
if (parent.has_buckled_mobs())
for (var/mob/driver in parent.buckled_mobs)
if (!parent.is_driver(driver))
continue
driver.show_message(span_userdanger("The [container] shakes violently!"))
if (istype(container, /obj/structure/closet))
var/obj/structure/closet/closet = container
if (!closet.welded)
return easy_escape(user, container)
return hard_escape(user, container)
return easy_escape(user, container)
/obj/golfcart_rear/take_damage(damage_amount, damage_type, damage_flag, sound_effect, attack_dir, armour_penetration)
if (!parent)
return
return parent.take_damage(damage_amount, damage_type, damage_flag, sound_effect, attack_dir, armour_penetration)
/obj/golfcart_rear/attack_hand(mob/user, list/modifiers)
if(!isnull(cargo))
unload()
return TRUE
return ..()
/obj/golfcart_rear/crowbar_act(mob/living/user, obj/item/tool)
. = ..()
if (!cargo)
return
tool.play_tool_sound(src, 50)
unload()
return ITEM_INTERACT_SUCCESS
/obj/golfcart_rear/proc/can_load(thing)
return is_type_in_typecache(thing, parent.allowed_cargo) && (!is_type_in_typecache(thing, parent.banned_cargo)) && (!has_buckled_mobs())
/obj/golfcart_rear/mouse_drop_receive(atom/dropped, mob/user, params)
if (!can_load(dropped))
if (has_buckled_mobs())
balloon_alert(user, "blocked!")
return ..()
var/obj/dropped_obj = dropped
return load(dropped_obj)
/obj/golfcart_rear/examine(mob/user)
if (!parent)
. = ..()
. += span_warning("A lone golf cart bed must be a bad omen...")
return
return parent.examine(user)
/obj/golfcart_rear/examine_more(mob/user)
if (!parent)
return ..()
return parent.examine_more(user)
///Called if the rear of the golfcart MUST move to destination and must NOT notify the parent about it.
/obj/golfcart_rear/proc/move_from_parent(atom/destination)
moving_from_parent = TRUE
currently_z_moving = destination.z != loc.z
. = forceMove(destination)
moving_from_parent = FALSE
/obj/golfcart_rear/doMove(atom/destination)
. = ..()
if (!moving_from_parent)
return
for (var/mob/buckled_mob in buckled_mobs)
if (currently_z_moving)
buckled_mob.currently_z_moving = currently_z_moving
buckled_mob.forceMove(destination)
else
// this is not a good hack - this should never happen
// but stairs are a particularly problematic area
if (!buckled_mob.Move(destination, dir, glide_size))
// this is a terrible hack because mob/living forwards forceMove calls to buckled
// unless currently_z_moving is non-null
buckled_mob.currently_z_moving = CURRENTLY_Z_MOVING_GENERIC
buckled_mob.forceMove(destination)
buckled_mob.currently_z_moving = FALSE
/obj/golfcart_rear/CanAllowThrough(atom/movable/mover, border_dir)
. = ..()
if (!parent)
return
if (mover == parent)
return TRUE
if (mover in parent.buckled_mobs)
return TRUE
if (parent.allow_crawler_through(mover))
return TRUE
/obj/golfcart_rear/Move(atom/newloc, direct, glide_size_override = 0, update_dir = TRUE)
if (moving_from_parent)
return
if(pulledby)
var/olddir = dir
var/newdir
if (direct & (EAST | WEST))
newdir = (direct & EAST) ? EAST : WEST
else if (direct & (NORTH | SOUTH))
newdir = (direct & NORTH) ? NORTH : SOUTH
else
newdir = direct
set_glide_size(glide_size_override ? glide_size_override : pulledby.glide_size)
. = ..()
dir = newdir
if (get_step(src, turn(dir, 180)) != get_turf(pulledby))
setDir(turn(dir, 180))
var/atom/behind = get_step(src, dir)
if (!behind.Enter(parent))
setDir(olddir)
behind = get_step(src, dir)
parent.set_glide_size(glide_size_override ? glide_size_override : pulledby.glide_size)
parent.forceMove(behind)
parent.setDir(dir)
parent.update_appearance(UPDATE_ICON)
return
return parent.Move(get_step(parent, get_dir(loc, newloc)), direct)
///Called for COMSIG_ATOM_TRIED_PASS on passengers buckled to the cart. Allows them to not block each other's movement / get blocked by the cart.
/obj/golfcart_rear/proc/allow_movement_between_bed_passengers(atom/source, atom/mover)
SIGNAL_HANDLER
if (mover == parent)
return COMSIG_COMPONENT_PERMIT_PASSAGE
if (parent && (mover in parent.buckled_mobs))
return COMSIG_COMPONENT_PERMIT_PASSAGE
if ((source in buckled_mobs) && (mover in buckled_mobs))
return COMSIG_COMPONENT_PERMIT_PASSAGE
///Called when the golfcart rear turns in order to keep the buckled mobs in the right places
/obj/golfcart_rear/proc/update_passenger_layers(new_dir)
if (isnull(new_dir))
new_dir = dir
var/layer = HUMAN_RIDING_LAYER
var/invert_layer = FALSE
if (new_dir & SOUTH)
invert_layer = TRUE
new_dir = "[new_dir]"
for(var/i in 1 to buckled_mobs.len)
var/mob/living/passenger = buckled_mobs[i]
var/vector/offset
if (passenger.body_position == LYING_DOWN)
offset = lying_down_passenger_offsets[i][new_dir]
else
offset = passenger_offsets[i][new_dir]
passenger.add_offsets(GOLFCART_RIDING_SOURCE,
x_add = offset.x,
y_add = offset.y,
z_add = offset.z,
animate = FALSE)
passenger.layer = layer + ((i * 0.01) - 0.01) * (-invert_layer)
///Called from COMSIG_ATOM_POST_DIR_CHANGE on the rear of the cart. Only used to change buckled mobs' position / layers.
/obj/golfcart_rear/proc/on_dir_changed(datum/source, old_dir, new_dir)
SIGNAL_HANDLER
if (!has_buckled_mobs())
return
update_passenger_layers(new_dir)
/obj/golfcart_rear/Initialize(mapload, obj/vehicle/ridden/golfcart/progenitor)
. = ..()
parent = progenitor
layer = BELOW_HUMAN_HITBOX_LAYER
RegisterSignal(parent, COMSIG_ATOM_POST_DIR_CHANGE, PROC_REF(on_dir_changed))
/obj/golfcart_rear/update_overlays()
. = ..()
if (dir & NORTH)
var/mutable_appearance/hitbox_overlay = mutable_appearance(icon, "rear_hitbox_overlay", layer)
hitbox_overlay.pixel_y += 32
. += hitbox_overlay
else if (dir & SOUTH)
. += mutable_appearance(icon, "rear_hitbox_lower", OBJ_LAYER + 0.01)
if(!cargo)
return
var/vector/rear_offsets = parent.get_rear_offset()
. += parent.generate_cargo_overlay(-rear_offsets.x, -rear_offsets.y, layer=layer)
///Called when a passenger tries lying down/getting up. Automatically drops out people who can't stay on
/obj/golfcart_rear/proc/passenger_falling_down(atom/source, new_bodypos)
SIGNAL_HANDLER
if (!isliving(source))
return // should runtime?
if (new_bodypos == STANDING_UP)
update_passenger_layers()
return
if (buckled_mobs.len <= 1)
update_passenger_layers()
return // allow 1 laying down mob
var/mob/living/passenger = source
unbuckle_mob(passenger, TRUE)
/obj/golfcart_rear/is_buckle_possible(mob/living/target, force, check_loc)
. = ..()
// these are to_viewers because you can buckle someone on their behalf
if (parent && cargo)
balloon_alert_to_viewers("blocked!")
return FALSE
if (target.body_position != STANDING_UP)
if (!has_buckled_mobs())
return TRUE
balloon_alert_to_viewers("stand up!")
return FALSE
for (var/mob/blocker in buckled_mobs)
if (!isliving(blocker))
balloon_alert_to_viewers("blocked!")
return FALSE
var/mob/living/living_blocker = blocker
if (living_blocker.body_position != STANDING_UP)
balloon_alert_to_viewers("blocked!")
return FALSE
return TRUE
/obj/golfcart_rear/post_buckle_mob(mob/living/buckled_mob)
buckled_mob.pulledby?.stop_pulling()
RegisterSignal(buckled_mob, COMSIG_ATOM_TRIED_PASS, PROC_REF(allow_movement_between_bed_passengers))
RegisterSignal(buckled_mob, COMSIG_LIVING_SET_BODY_POSITION, PROC_REF(passenger_falling_down))
. = ..()
update_passenger_layers()
/obj/golfcart_rear/post_unbuckle_mob(mob/living/buckled_mob)
UnregisterSignal(buckled_mob, COMSIG_ATOM_TRIED_PASS)
UnregisterSignal(buckled_mob, COMSIG_LIVING_SET_BODY_POSITION)
buckled_mob.remove_offsets(GOLFCART_RIDING_SOURCE)
if (buckled_mob.body_position == LYING_DOWN)
buckled_mob.layer = LYING_MOB_LAYER
else
buckled_mob.layer = initial(buckled_mob.layer)
return ..()
/obj/golfcart_rear/Destroy()
if (parent)
UnregisterSignal(parent, COMSIG_ATOM_POST_DIR_CHANGE)
if (!QDELETED(parent))
qdel(parent)
parent = null
if (cargo && !QDELETED(cargo))
cargo.forceMove(drop_location())
cargo = null
return ..()
#undef CARGO_HITBOX_LAYER
#undef HUMAN_LOWER_LAYER
#undef HUMAN_RIDING_LAYER
#undef GOLFCART_RIDING_SOURCE
#undef BELOW_HUMAN_HITBOX_LAYER
+75
View File
@@ -416,6 +416,81 @@
initialize_controller_action_type(/datum/action/vehicle/sealed/mecha/mech_view_stats, VEHICLE_CONTROL_SETTINGS)
initialize_controller_action_type(/datum/action/vehicle/sealed/mecha/strafe, VEHICLE_CONTROL_DRIVE)
/obj/vehicle/sealed/mecha/add_occupant(mob/M, control_flags, forced)
if(..())
generate_equipment_actions(M)
/obj/vehicle/sealed/mecha/remove_occupant(mob/M)
remove_all_equipment_actions(M)
return ..()
///Generates action buttons for all eligible equipment and grants them to the occupant with VEHICLE_CONTROL_SETTINGS flag.
/obj/vehicle/sealed/mecha/proc/generate_equipment_actions(mob/occupant)
if(!(occupant in occupants) || !(occupants[occupant] & VEHICLE_CONTROL_SETTINGS))
return
for(var/obj/item/mecha_parts/mecha_equipment/equipment in flat_equipment)
if(!is_equipment_valid_for_action(equipment))
continue
grant_equipment_action(occupant, equipment)
///Removes all equipment actions from a specific occupant.
/obj/vehicle/sealed/mecha/proc/remove_all_equipment_actions(mob/occupant)
var/list/actions = LAZYACCESS(occupant_actions, occupant)
if(!actions)
return
for(var/equipment_type in actions)
if(!ispath(equipment_type, /obj/item/mecha_parts/mecha_equipment))
continue
remove_action_type_from_mob(equipment_type, occupant)
/**
* Grants a specific equipment action to an occupant.
* Creates a new action, sets up the chassis and equipment references, and grants it to the mob.
*/
/obj/vehicle/sealed/mecha/proc/grant_equipment_action(mob/occupant, obj/item/mecha_parts/mecha_equipment/equipment)
var/datum/action/vehicle/sealed/mecha/equipment/action = new // We cannot use grant_action_type_to_mob() because:
action.set_chassis(src) // 1. grant_action_type_to_mob() works with a single predefined action type
action.set_equipment(equipment) // 2. We create unique action instances for each equipment with specific equipment references
action.Grant(occupant)
LAZYINITLIST(occupant_actions[occupant])
// Use equipment type as actiontype for remove_action_type_from_mob() compatibility
occupant_actions[occupant][equipment.type] = action
/**
* Called when equipment is attached to the mecha.
* Grants equipment actions to current occupants with VEHICLE_CONTROL_SETTINGS flag.
*/
/obj/vehicle/sealed/mecha/proc/on_equipment_attach(obj/item/mecha_parts/mecha_equipment/equipment)
if(!is_equipment_valid_for_action(equipment))
return
for(var/mob/occupant in occupants)
if(!(occupants[occupant] & VEHICLE_CONTROL_SETTINGS))
continue
grant_equipment_action(occupant, equipment)
/**
* Called when equipment is detached from the mecha.
* Removes equipment actions from all current occupants.
*/
/obj/vehicle/sealed/mecha/proc/on_equipment_detach(obj/item/mecha_parts/mecha_equipment/equipment)
for(var/mob/occupant in occupants)
remove_action_type_from_mob(equipment.type, occupant)
/// Create actions only for equipment that can be toggled or triggered, excluding air tanks.
/obj/vehicle/sealed/mecha/proc/is_equipment_valid_for_action(obj/item/mecha_parts/mecha_equipment/equipment)
if(!(equipment.can_be_toggled || equipment.can_be_triggered))
return FALSE
if(istype(equipment, /obj/item/mecha_parts/mecha_equipment/air_tank)) // this thing has its own button
return FALSE
return TRUE
/obj/vehicle/sealed/mecha/proc/get_mecha_occupancy_state()
if((mecha_flags & SILICON_PILOT) && silicon_icon_state)
return silicon_icon_state
@@ -214,6 +214,7 @@
SEND_SIGNAL(src, COMSIG_MECHA_EQUIPMENT_ATTACHED)
forceMove(new_mecha)
log_message("[src] initialized.", LOG_MECHA)
chassis.on_equipment_attach(src)
/**
* called to detach this equipment
@@ -221,6 +222,7 @@
* * moveto: optional target to move this equipment to
*/
/obj/item/mecha_parts/mecha_equipment/proc/detach(atom/moveto)
chassis.on_equipment_detach(src)
moveto = moveto || get_turf(chassis)
forceMove(moveto)
playsound(chassis, 'sound/items/weapons/tap.ogg', 50, TRUE)
@@ -273,9 +273,7 @@
to_chat(M, "<font color='red' size='7'>HONK</font>")
M.SetSleeping(0)
M.adjust_stutter(40 SECONDS)
var/obj/item/organ/ears/ears = M.get_organ_slot(ORGAN_SLOT_EARS)
if(ears)
ears.adjustEarDamage(0, 30)
M.sound_damage(deafen = 30 SECONDS)
M.Paralyze(60)
if(prob(30))
M.Stun(200)
@@ -144,10 +144,12 @@
chassis.balloon_alert(owner, "controlling gunner seat")
chassis.remove_control_flags(owner, VEHICLE_CONTROL_DRIVE|VEHICLE_CONTROL_SETTINGS)
chassis.add_control_flags(owner, VEHICLE_CONTROL_MELEE|VEHICLE_CONTROL_EQUIPMENT)
chassis.remove_all_equipment_actions(owner)
else
chassis.balloon_alert(owner, "controlling pilot seat")
chassis.remove_control_flags(owner, VEHICLE_CONTROL_MELEE|VEHICLE_CONTROL_EQUIPMENT)
chassis.add_control_flags(owner, VEHICLE_CONTROL_DRIVE|VEHICLE_CONTROL_SETTINGS)
chassis.generate_equipment_actions(owner)
chassis.update_icon_state()
/datum/action/vehicle/sealed/mecha/mech_overclock
@@ -162,3 +164,33 @@
chassis.toggle_overclock(forced_state)
button_icon_state = "mech_overload_[chassis.overclock_mode ? "on" : "off"]"
build_all_button_icons()
/datum/action/vehicle/sealed/mecha/equipment
name = "Mech Equipment"
button_icon_state = null
background_icon_state = "bg_tech"
var/obj/item/mecha_parts/mecha_equipment/equipment
/datum/action/vehicle/sealed/mecha/equipment/Destroy()
equipment = null
return ..()
/datum/action/vehicle/sealed/mecha/equipment/Trigger(mob/clicker, trigger_flags)
if(!..())
return
if(!chassis || !(owner in chassis.occupants) || !equipment)
return
equipment.set_active(!equipment.active)
equipment.handle_ui_act(action = "toggle")
chassis.balloon_alert(owner, "[equipment.name] [equipment.active ? "on" : "off"]!")
/datum/action/vehicle/sealed/mecha/equipment/proc/set_equipment(passed_equipment)
equipment = passed_equipment
name = "Toggle [equipment.name]"
desc = equipment.desc
target = equipment
if(target)
AddComponent(/datum/component/action_item_overlay, equipment)
build_button_icon()