From 73ddd8c156a5ae0962daa2e8efc14922767fcd64 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 01:37:24 -0800 Subject: [PATCH 01/16] Add the Rig Bay seam: stand, mech SKUs, stand pools and defines --- tgstation.dme | 3 + voidcrew/_DEFINES/rig_bay.dm | 28 +++ .../modules/trade/rig_bay/rig_bay_skus.dm | 112 +++++++++ .../modules/trade/rig_bay/rig_bay_stand.dm | 235 ++++++++++++++++++ 4 files changed, 378 insertions(+) create mode 100644 voidcrew/_DEFINES/rig_bay.dm create mode 100644 voidcrew/modules/trade/rig_bay/rig_bay_skus.dm create mode 100644 voidcrew/modules/trade/rig_bay/rig_bay_stand.dm diff --git a/tgstation.dme b/tgstation.dme index cf7dcde4fe3..f969d380ffc 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -6733,6 +6733,7 @@ #include "voidcrew\_DEFINES\overmap_zones.dm" #include "voidcrew\_DEFINES\planet_defines.dm" #include "voidcrew\_DEFINES\player_outposts.dm" +#include "voidcrew\_DEFINES\rig_bay.dm" #include "voidcrew\_DEFINES\ship_combat.dm" #include "voidcrew\_DEFINES\ship_construction.dm" #include "voidcrew\_DEFINES\ship_defines.dm" @@ -7739,6 +7740,8 @@ #include "voidcrew\modules\trade\favor_uniques\outfitter.dm" #include "voidcrew\modules\trade\favor_uniques\pike.dm" #include "voidcrew\modules\trade\favor_uniques\vex.dm" +#include "voidcrew\modules\trade\rig_bay\rig_bay_skus.dm" +#include "voidcrew\modules\trade\rig_bay\rig_bay_stand.dm" #include "voidcrew\modules\transporter\_transporter.dm" #include "voidcrew\modules\transporter\transporter_console.dm" #include "voidcrew\modules\transporter\transporter_designs.dm" diff --git a/voidcrew/_DEFINES/rig_bay.dm b/voidcrew/_DEFINES/rig_bay.dm new file mode 100644 index 00000000000..e0bbae65df4 --- /dev/null +++ b/voidcrew/_DEFINES/rig_bay.dm @@ -0,0 +1,28 @@ +// The Rig Bay: exosuits sold off chained stands at Wick's outfitter (voidcrew/modules/trade/rig_bay/). + +/// On a display mech while it hangs from its stand's chains (set and cleared by the clamp component). +#define TRAIT_RIG_BAY_CLAMPED "rig_bay_clamped" +/// Trait source for TRAIT_RIG_BAY_CLAMPED. +#define RIG_BAY_TRAIT "rig_bay" + +/// Sent on a mech when a mob tries to climb in: (mob/entering). Hooked in rig_bay_mecha.dm. +#define COMSIG_MECHA_TRY_ENTER "mecha_try_enter" + /// Return from COMSIG_MECHA_TRY_ENTER to refuse the entry. + #define COMPONENT_MECHA_BLOCK_ENTER (1<<0) + +/// Longest a sold mech stays locked to the buyer's crew while it is on outpost ground. +#define RIG_BAY_HOLD_TIME (20 MINUTES) +/// Pixels a chained mech hangs above its stand. Release lowers it back to 0. +#define RIG_BAY_HANG_LIFT 4 + +// Stand states (/obj/structure/rig_bay_stand var/stand_state) +/// Nothing on the stand; waits for the next due convoy. +#define RIG_BAY_STAND_EMPTY "empty" +/// A mech is beaming in. Its SKU has stock 0 until the beam-in finishes. +#define RIG_BAY_STAND_ARRIVING "arriving" +/// A mech hangs on the stand for sale. Its SKU has stock 1. +#define RIG_BAY_STAND_CLAMPED "clamped" +/// A sold mech is being lowered. Its SKU has stock 0. +#define RIG_BAY_STAND_RELEASING "releasing" +/// A sold mech is down and free but hasn't left the stand tile yet. Restock skips the stand. +#define RIG_BAY_STAND_SOLD_ON_STAND "sold_on_stand" diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_skus.dm b/voidcrew/modules/trade/rig_bay/rig_bay_skus.dm new file mode 100644 index 00000000000..c484877db70 --- /dev/null +++ b/voidcrew/modules/trade/rig_bay/rig_bay_skus.dm @@ -0,0 +1,112 @@ +// The Rig Bay's goods: whole exosuits sold off stands, and the pools each stand deals from. +// Prices and gates come from data/rig-bay/spec.md section 3. Tiers are in tools/gear_catalog/ratings/. + +// ===== STAND POOLS ===== + +/** + * The mechs one kind of stand can show. Listed in the suit fitter's `rotations` so the gear + * catalog sees every mech a stand can roll; `slots = 0` keeps the base shop from dealing them + * as loose shelf lines. Each /obj/structure/rig_bay_stand deals its own pick with pick_one(). + */ +/datum/shop_rotation/rig_bay + slots = 0 + /// Convoys an empty stand of this pool waits before it takes a new mech. 1 refills on every convoy. + var/convoys_per_refill = 1 + +/// Working mechs. Refills every convoy. +/datum/shop_rotation/rig_bay/work + pool = list( + /datum/shop_sku/rig_bay_mech/ripley = 4, + /datum/shop_sku/rig_bay_mech/clarke = 3, + /datum/shop_sku/rig_bay_mech/odysseus = 2, + /datum/shop_sku/rig_bay_mech/ripley_mk2 = 2, + ) + +/// Combat mechs. Refills every second convoy. +/datum/shop_rotation/rig_bay/combat + convoys_per_refill = 2 + pool = list( + /datum/shop_sku/rig_bay_mech/gygax = 3, + /datum/shop_sku/rig_bay_mech/durand = 1, + ) + +/// Yellow exosuit arms: four of the pool on the shelf at once, one each. P1 fills the pool (spec section 4). +/datum/shop_rotation/rig_bay_arms + slots = 4 + pool = list() + +// ===== WHOLE MECHS ===== + +/** + * One mech hanging on one stand. The stand creates the SKU, owns its stock (1 while the mech + * hangs there, 0 otherwise) and hands the mech over on purchase. Never refilled by the convoy + * top-up, never a contract reward and never discounted. + */ +/datum/shop_sku/rig_bay_mech + category = "Exosuits" + shelf = SHELF_CORE + restock_interval = INFINITY + contract_reward = FALSE + fixed_price = TRUE + stock_min = 0 + stock_max = 1 + /// The stand this mech hangs on. Cleared when either side is deleted. + var/obj/structure/rig_bay_stand/stand + /// Equipment typepaths attached to the mech when the stand spawns it, on top of its tg default kit. + var/list/spawn_kit = list() + +/datum/shop_sku/rig_bay_mech/Destroy() + if(stand?.sku == src) + stand.sku = null + stand = null + return ..() + +/datum/shop_sku/rig_bay_mech/ripley + item_path = /obj/vehicle/sealed/mecha/ripley + price_credits = 4000 + spawn_kit = list( + /obj/item/mecha_parts/mecha_equipment/hydraulic_clamp, + /obj/item/mecha_parts/mecha_equipment/drill, + ) + +/datum/shop_sku/rig_bay_mech/clarke + item_path = /obj/vehicle/sealed/mecha/clarke + price_credits = 5500 + spawn_kit = list( + /obj/item/mecha_parts/mecha_equipment/drill, + /obj/item/mecha_parts/mecha_equipment/mining_scanner, + ) + +/datum/shop_sku/rig_bay_mech/odysseus + item_path = /obj/vehicle/sealed/mecha/odysseus + price_credits = 4500 + spawn_kit = list( + /obj/item/mecha_parts/mecha_equipment/sleeper/medical, + ) + +/datum/shop_sku/rig_bay_mech/ripley_mk2 + item_path = /obj/vehicle/sealed/mecha/ripley/mk2 + price_credits = 6500 + spawn_kit = list( + /obj/item/mecha_parts/mecha_equipment/hydraulic_clamp, + /obj/item/mecha_parts/mecha_equipment/drill, + ) + +/datum/shop_sku/rig_bay_mech/gygax + item_path = /obj/vehicle/sealed/mecha/gygax + zone_permission = ZONE_PERMISSION_YELLOW + price_credits = 11000 + crew_limit = 2 + +/datum/shop_sku/rig_bay_mech/durand + item_path = /obj/vehicle/sealed/mecha/durand + zone_permission = ZONE_PERMISSION_ADVANCED + price_credits = 18000 + price_vouchers = 3 + crew_limit = 1 + +// ===== ARMS, AMMO AND FITTINGS ===== + +/// Base for the ordinary exosuit gear lines (Exosuit Arms, Ammo and Fittings tabs). P1 adds the subtypes (spec section 4). +/datum/shop_sku/rig_bay_gear + category = "Exosuit Fittings" diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_stand.dm b/voidcrew/modules/trade/rig_bay/rig_bay_stand.dm new file mode 100644 index 00000000000..514e7db3718 --- /dev/null +++ b/voidcrew/modules/trade/rig_bay/rig_bay_stand.dm @@ -0,0 +1,235 @@ +/** + * # Rig Bay stand + * + * A gantry at Wick's that holds one exosuit for sale, hung on chains RIG_BAY_HANG_LIFT pixels up. + * The stand owns its mech's SKU on the suit fitter's shop: stock 1 while the mech hangs there, + * 0 otherwise. Buying it lowers the mech and frees it for the buyer's crew; the crew drives it off. + * + * Seam contract (data/rig-bay/spec.md sections 5, 6, 11): + * - P1 adds the clamp/hold components and the shop wiring (attach_stand, restock_stands, dispense). + * - P4 overrides animate_release(), animate_beam_in() and apply_hang_pose() in rig_bay_stand_visuals.dm. + * + * Always dense, so nobody stands or parks on it. A mech already on the tile can still drive off. + */ +/obj/structure/rig_bay_stand + name = "rig stand" + desc = "A heavy gantry with chains for hanging an exosuit." + icon = 'icons/obj/machines/telepad.dmi' + icon_state = "mechpad" + anchored = TRUE + density = TRUE + move_resist = INFINITY + /// Number painted on the stand and shown in the shop line ("Ripley, bay 2"). Set on the map. + var/bay_number = 1 + /// /datum/shop_rotation/rig_bay typepath this stand deals its mechs from. Set on the map. + var/pool = /datum/shop_rotation/rig_bay/work + /// The mech on the stand: hanging for sale, being lowered, or sold but not yet driven off. + var/obj/vehicle/sealed/mecha/held_mech + /// This stand's line in the shop. Kept at stock 0 after a sale until the stand deals a new mech. + var/datum/shop_sku/rig_bay_mech/sku + /// The trader outpost this stand belongs to. Set by link_outpost(). + var/obj/structure/overmap/trader_outpost/outpost + /// The suit fitter's shop that lists this stand's mech. Set by link_outpost(). + var/datum/outpost_shop/vendor/suit_fitter/shop + /// Convoys this stand has waited while empty, counted against its pool's convoys_per_refill. + var/convoys_waited = 0 + /// One of the RIG_BAY_STAND_* states. + var/stand_state = RIG_BAY_STAND_EMPTY + +/obj/structure/rig_bay_stand/Destroy() + if(held_mech) + var/obj/vehicle/sealed/mecha/mech = held_mech + var/unsold = (stand_state == RIG_BAY_STAND_ARRIVING || stand_state == RIG_BAY_STAND_CLAMPED) + forget_mech() + // Shop stock never leaks out; a sold mech belongs to its buyer. + if(unsold) + qdel(mech) + drop_sku() + shop = null + outpost = null + return ..() + +/** + * Called once from the outpost's interior link sweep (round start and lazy load). Finds the suit + * fitter's shop and deals the first mech without animation. The deal waits for atom init, because + * the round-start pre-load runs before SSatoms. + */ +/obj/structure/rig_bay_stand/proc/link_outpost(obj/structure/overmap/trader_outpost/new_outpost) + if(outpost || !new_outpost) + return + outpost = new_outpost + shop = outpost.get_shop(/datum/outpost_shop/vendor/suit_fitter) + // P1: shop.attach_stand(src) registers the stand for convoy restocks. + if(SSatoms.initialized == INITIALIZATION_INNEW_REGULAR) + deal_mech(animate = FALSE) + else + addtimer(CALLBACK(src, PROC_REF(deal_mech), FALSE), 1) + +/** + * Picks a mech from the pool (preferring one no other stand of the same pool is showing), swaps + * the stand's old SKU for a new one in the shop and puts the mech on the stand. + * Returns the new mech, or null if the stand isn't empty, isn't linked or its tile is blocked. + */ +/obj/structure/rig_bay_stand/proc/deal_mech(animate = TRUE) + if(QDELETED(src) || !shop || stand_state != RIG_BAY_STAND_EMPTY || !ispath(pool, /datum/shop_rotation/rig_bay)) + return null + if(!tile_clear()) + return null + var/datum/shop_rotation/rig_bay/rotation = locate(pool) in shop.live_rotations + var/temporary_rotation = !rotation + if(temporary_rotation) + rotation = new pool + var/list/showing = list() + for(var/datum/shop_sku/rig_bay_mech/other in shop.skus) + if(other.stand && other.stand != src && other.stand.pool == pool) + showing += other.type + var/sku_type = rotation.pick_one(showing) || rotation.pick_one() + if(temporary_rotation) + qdel(rotation) + if(!sku_type) + return null + drop_sku() + shop.add_sku(sku_type, SHELF_CORE) + return receive_mech(shop.skus[length(shop.skus)], animate) + +/** + * Takes ownership of `new_sku` (already in shop.skus) and spawns its mech on the stand with its kit, + * anchored and marked as trader goods. With `animate` it beams in through animate_beam_in(); + * the SKU stays at stock 0 until finish_arrival(). Returns the mech, or null on a bad SKU or blocked tile. + */ +/obj/structure/rig_bay_stand/proc/receive_mech(datum/shop_sku/rig_bay_mech/new_sku, animate = TRUE) + if(!istype(new_sku) || !ispath(new_sku.item_path, /obj/vehicle/sealed/mecha) || held_mech || !tile_clear()) + return null + sku = new_sku + new_sku.stand = src + new_sku.stock = 0 + var/obj/vehicle/sealed/mecha/mech_type = new_sku.item_path + new_sku.name = "[initial(mech_type.name)], bay [bay_number]" + var/obj/vehicle/sealed/mecha/mech = new mech_type(loc) + held_mech = mech + for(var/kit_type in new_sku.spawn_kit) + var/obj/item/mecha_parts/mecha_equipment/part = new kit_type(mech) + if(part.can_attach(mech)) + part.attach(mech) + else + qdel(part) + mark_trader_goods(mech) + mech.setDir(dir) + mech.set_anchored(TRUE) + RegisterSignal(mech, COMSIG_QDELETING, PROC_REF(on_mech_deleted)) + RegisterSignal(mech, COMSIG_MOVABLE_MOVED, PROC_REF(on_mech_moved)) + // P1: mech.AddComponent(/datum/component/rig_bay_clamp, src) + stand_state = RIG_BAY_STAND_ARRIVING + if(animate) + mech.alpha = 0 + animate_beam_in(mech, CALLBACK(src, PROC_REF(finish_arrival), mech)) + else + apply_hang_pose(mech) + finish_arrival(mech) + return mech + +/// Beam-in done: the mech hangs for sale and its shop line goes to stock 1. +/obj/structure/rig_bay_stand/proc/finish_arrival(obj/vehicle/sealed/mecha/mech) + if(QDELETED(src) || QDELETED(mech) || mech != held_mech || stand_state != RIG_BAY_STAND_ARRIVING) + return + stand_state = RIG_BAY_STAND_CLAMPED + if(sku) + sku.stock = 1 + shop?.trader_npc?.shop_ui?.update_static_data_for_all_viewers() + +/** + * The sale: lowers the hanging mech and frees it for `buyer`'s crew. Called from the SKU's + * dispense() after the shop has charged. Returns FALSE if there is no hanging mech to hand over, + * so the caller refunds. + */ +/obj/structure/rig_bay_stand/proc/release_mech(mob/living/buyer) + if(stand_state != RIG_BAY_STAND_CLAMPED || QDELETED(held_mech)) + return FALSE + stand_state = RIG_BAY_STAND_RELEASING + if(sku) + sku.stock = 0 + // P1: swap /datum/component/rig_bay_clamp for /datum/component/rig_bay_hold (buyer's crew) here. + to_chat(buyer, span_notice("The chains on bay [bay_number] drop.")) + animate_release(held_mech, CALLBACK(src, PROC_REF(finish_release), held_mech)) + return TRUE + +/// Lowering done: the mech is free to drive off. The stand stays SOLD_ON_STAND until it leaves. +/obj/structure/rig_bay_stand/proc/finish_release(obj/vehicle/sealed/mecha/mech) + if(QDELETED(src) || QDELETED(mech) || mech != held_mech || stand_state != RIG_BAY_STAND_RELEASING) + return + mech.set_anchored(FALSE) + stand_state = RIG_BAY_STAND_SOLD_ON_STAND + +/** + * Release animation. Default: instant. P4 overrides it (draft 1.1 s). + * Must end with `mech.pixel_z` at 0 and then invoke `on_done` exactly once. + */ +/obj/structure/rig_bay_stand/proc/animate_release(obj/vehicle/sealed/mecha/mech, datum/callback/on_done) + mech.pixel_z = 0 + on_done?.Invoke() + +/** + * Beam-in animation. Default: instant. P4 overrides it (draft 3.0 s). + * The mech arrives at alpha 0, anchored, with its SKU at stock 0. Move it with pixel_z only, + * end in the hang pose (apply_hang_pose) and then invoke `on_done` exactly once. + */ +/obj/structure/rig_bay_stand/proc/animate_beam_in(obj/vehicle/sealed/mecha/mech, datum/callback/on_done) + apply_hang_pose(mech) + on_done?.Invoke() + +/// The idle look of a mech hanging on the stand. P4 adds the chains here. +/obj/structure/rig_bay_stand/proc/apply_hang_pose(obj/vehicle/sealed/mecha/mech) + mech.alpha = 255 + mech.pixel_z = RIG_BAY_HANG_LIFT + +/// TRUE if a new mech can be placed: no mob, mech or other dense thing on the stand tile. +/obj/structure/rig_bay_stand/proc/tile_clear() + var/turf/tile = get_turf(src) + if(!tile) + return FALSE + for(var/atom/movable/thing as anything in tile) + if(thing == src) + continue + if(isliving(thing) || ismecha(thing) || thing.density) + return FALSE + return TRUE + +/// Drops this stand's SKU from the shop and deletes it. +/obj/structure/rig_bay_stand/proc/drop_sku() + if(!sku) + return + var/datum/shop_sku/rig_bay_mech/old_sku = sku + sku = null + if(shop) + shop.skus -= old_sku + qdel(old_sku) + +/// Lets go of the held mech's signals and refs. Leaves the SKU listed (stock 0) until the next deal. +/obj/structure/rig_bay_stand/proc/forget_mech() + if(!held_mech) + return + UnregisterSignal(held_mech, list(COMSIG_QDELETING, COMSIG_MOVABLE_MOVED)) + held_mech = null + stand_state = RIG_BAY_STAND_EMPTY + if(sku) + sku.stock = 0 + +/// The mech was deleted (gibbed, admin delete): the stand goes empty. +/obj/structure/rig_bay_stand/proc/on_mech_deleted(datum/source) + SIGNAL_HANDLER + forget_mech() + +/// The mech left the stand tile: the stand goes empty and waits for a convoy. +/obj/structure/rig_bay_stand/proc/on_mech_moved(datum/source) + SIGNAL_HANDLER + if(held_mech && held_mech.loc != loc) + forget_mech() + +// ===== MAP MARKERS ===== + +/** + * An open doorway that counts as a room edge for outpost ambient workers (outpost_work_barrier()), + * so a worker's room fill stops here. Invisible and not dense; customers walk over it. + */ +/obj/effect/landmark/outpost_work_threshold + name = "outpost work threshold" From 3f6faf4492a94e71e743c54deb6f900e1b46de8f Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 01:42:02 -0800 Subject: [PATCH 02/16] Open the outfitter's mech showroom: three rig stands and work thresholds --- .../outposts/trader_outpost_outfitter.dmm | 590 +++++++----------- 1 file changed, 228 insertions(+), 362 deletions(-) diff --git a/voidcrew/_maps/map_files/outposts/trader_outpost_outfitter.dmm b/voidcrew/_maps/map_files/outposts/trader_outpost_outfitter.dmm index 2d4848184b2..a7f249012a1 100644 --- a/voidcrew/_maps/map_files/outposts/trader_outpost_outfitter.dmm +++ b/voidcrew/_maps/map_files/outposts/trader_outpost_outfitter.dmm @@ -127,6 +127,17 @@ }, /turf/open/indestructible/dark/textured, /area/voidcrew/trader_outpost/checkpoint) +"at" = ( +/obj/structure/rack{ + name = "tool rack" + }, +/obj/item/storage/toolbox/mechanical, +/obj/item/wrench{ + pixel_x = 6; + pixel_y = -4 + }, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "au" = ( /obj/structure/railing/unbreakable, /obj/effect/turf_decal/tile/yellow/half/contrasted, @@ -216,6 +227,10 @@ /obj/structure/marker_beacon/yellow, /turf/open/floor/engine/hull/reinforced, /area/space) +"aG" = ( +/obj/effect/decal/cleanable/blood/oil, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "aH" = ( /obj/effect/turf_decal/bot, /obj/structure/closet/crate, @@ -307,6 +322,9 @@ /obj/effect/decal/cleanable/dirt, /turf/open/indestructible, /area/voidcrew/trader_outpost) +"aQ" = ( +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "aR" = ( /obj/machinery/door/airlock/outpost{ name = "West Yard Gate" @@ -319,13 +337,17 @@ /turf/open/indestructible/plating, /area/voidcrew/trader_outpost) "aT" = ( -/obj/effect/turf_decal/trimline/red/filled/line{ - dir = 8 - }, -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 8 +/obj/structure/rig_bay_stand{ + bay_number = 1; + dir = 2; + pool = /datum/shop_rotation/rig_bay/work }, -/obj/effect/decal/cleanable/dirt/dust, +/obj/effect/turf_decal/delivery, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) +"aU" = ( +/obj/machinery/recharge_station, +/obj/machinery/light/directional/west, /turf/open/indestructible/dark/textured, /area/voidcrew/trader_outpost) "aV" = ( @@ -356,11 +378,12 @@ /turf/open/indestructible, /area/voidcrew/trader_outpost) "aY" = ( -/obj/effect/turf_decal/tile/red/anticorner/contrasted{ - dir = 4 +/obj/structure/rig_bay_stand{ + bay_number = 2; + dir = 2; + pool = /datum/shop_rotation/rig_bay/work }, -/obj/effect/decal/cleanable/dirt, -/obj/effect/decal/cleanable/dirt/dust, +/obj/effect/turf_decal/delivery, /turf/open/indestructible/dark/textured, /area/voidcrew/trader_outpost) "aZ" = ( @@ -372,12 +395,27 @@ /obj/effect/landmark/outpost_network_pad_spot, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost/checkpoint) +"bb" = ( +/obj/effect/turf_decal/stripes/line{ + dir = 1 + }, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) +"bc" = ( +/obj/effect/decal/cleanable/dirt, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "bd" = ( /obj/effect/turf_decal/arrows{ dir = 2 }, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost/checkpoint) +"be" = ( +/obj/structure/table/reinforced, +/obj/machinery/cell_charger, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "bf" = ( /obj/structure/closet/wardrobe/mixed, /obj/effect/turf_decal/siding/wideplating_new/dark{ @@ -404,6 +442,29 @@ /obj/effect/turf_decal/bot, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost/checkpoint) +"bj" = ( +/obj/structure/table/reinforced, +/obj/structure/showcase{ + name = "half-built endoskeleton"; + desc = "A cyborg frame with no arms, no head and no warranty."; + icon = 'icons/mob/augmentation/augments.dmi'; + icon_state = "robo_suit"; + density = 0; + pixel_y = 6 + }, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) +"bk" = ( +/obj/structure/table/reinforced, +/obj/item/screwdriver{ + pixel_x = -6; + pixel_y = 4 + }, +/obj/item/stack/cable_coil/cut{ + pixel_x = 6 + }, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "bl" = ( /obj/structure/filingcabinet/security, /obj/machinery/light/small/directional/north, @@ -417,6 +478,15 @@ /obj/effect/decal/cleanable/dirt, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost/checkpoint) +"bo" = ( +/obj/structure/showcase/cyborg/old{ + dir = 2; + name = "stripped cyborg"; + desc = "A cyborg shell with its panel off and its innards on the floor." + }, +/obj/effect/decal/cleanable/blood/oil, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "bp" = ( /mob/living/basic/outpost_trader/checkpoint_guard/quartermain{ checkpoint_id = "quartermain_gate" @@ -673,9 +743,61 @@ /obj/effect/decal/cleanable/dirt/dust, /turf/open/indestructible/plating, /area/voidcrew/trader_outpost/checkpoint) +"bW" = ( +/obj/structure/sign/painting/library, +/obj/machinery/light/directional/north, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) "bX" = ( -/obj/structure/table/reinforced, -/obj/effect/turf_decal/siding/thinplating_new, +/obj/structure/rig_bay_stand{ + bay_number = 3; + dir = 2; + pool = /datum/shop_rotation/rig_bay/combat + }, +/obj/effect/turf_decal/delivery, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"bY" = ( +/obj/effect/decal/cleanable/blood/oil, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"ca" = ( +/obj/effect/turf_decal/stripes/line{ + dir = 1 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"cb" = ( +/obj/structure/rack{ + name = "parts rack" + }, +/obj/item/stack/cable_coil{ + pixel_x = -4 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"cc" = ( +/obj/structure/showcase{ + name = "exosuit fabricator"; + desc = "Wick's own fabricator. The access panel is padlocked shut."; + icon = 'icons/obj/machines/robotics.dmi'; + icon_state = "fab-idle" + }, +/obj/machinery/light/directional/south, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"cd" = ( +/obj/structure/chair/office/light{ + dir = 8 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"ce" = ( +/obj/machinery/suit_storage_unit, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"cf" = ( +/obj/machinery/suit_storage_unit/industrial, /turf/open/indestructible/large, /area/voidcrew/trader_outpost) "cg" = ( @@ -686,6 +808,54 @@ }, /turf/open/indestructible/dark/smooth_large, /area/voidcrew/trader_outpost) +"ch" = ( +/obj/machinery/light/directional/north, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) +"ci" = ( +/obj/machinery/light/directional/north, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"cj" = ( +/obj/item/radio/intercom/directional/north, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"ck" = ( +/obj/effect/landmark/outpost_work_threshold, +/obj/effect/turf_decal/siding/thinplating_new{ + dir = 8 + }, +/obj/effect/turf_decal/tile/yellow/half/contrasted{ + dir = 8 + }, +/obj/effect/turf_decal/trimline/yellow/line, +/obj/effect/turf_decal/trimline/yellow/line{ + dir = 1 + }, +/turf/open/indestructible, +/area/voidcrew/trader_outpost) +"cl" = ( +/obj/effect/landmark/outpost_work_threshold, +/obj/effect/turf_decal/trimline/yellow/line, +/obj/effect/turf_decal/trimline/yellow/line{ + dir = 1 + }, +/turf/open/indestructible, +/area/voidcrew/trader_outpost) +"cm" = ( +/obj/effect/landmark/outpost_work_threshold, +/obj/effect/turf_decal/siding/thinplating_new{ + dir = 4 + }, +/obj/effect/turf_decal/tile/yellow/half/contrasted{ + dir = 4 + }, +/obj/effect/turf_decal/trimline/yellow/line, +/obj/effect/turf_decal/trimline/yellow/line{ + dir = 1 + }, +/turf/open/indestructible, +/area/voidcrew/trader_outpost) "cp" = ( /obj/structure/lattice/catwalk, /turf/open/space/basic, @@ -992,14 +1162,6 @@ }, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) -"fV" = ( -/obj/effect/turf_decal/tile/red/half/contrasted{ - dir = 8 - }, -/obj/effect/decal/cleanable/dirt, -/obj/effect/decal/cleanable/dirt/dust, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "gg" = ( /obj/effect/turf_decal/siding/wideplating_new/dark{ dir = 4 @@ -1007,26 +1169,6 @@ /obj/effect/decal/cleanable/dirt, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) -"gh" = ( -/obj/effect/turf_decal/tile/red/half/contrasted{ - dir = 4 - }, -/obj/effect/decal/cleanable/dirt/dust, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) -"gq" = ( -/obj/structure/rack, -/obj/effect/turf_decal/trimline/red/filled/line{ - dir = 4 - }, -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 4 - }, -/obj/effect/decal/cleanable/dirt/dust, -/obj/machinery/light/small/red/directional/west, -/obj/item/gun/energy/laser/redtag, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "gD" = ( /obj/effect/turf_decal/siding/wideplating_new/dark/corner, /obj/effect/turf_decal/siding/wideplating_new/dark/corner{ @@ -1048,13 +1190,6 @@ /obj/effect/decal/cleanable/dirt, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) -"gV" = ( -/obj/effect/turf_decal/trimline/yellow/line, -/obj/effect/turf_decal/trimline/yellow/line{ - dir = 1 - }, -/turf/open/indestructible, -/area/voidcrew/trader_outpost) "ha" = ( /obj/effect/turf_decal/siding/thinplating_new/dark{ dir = 4 @@ -1335,18 +1470,6 @@ }, /turf/open/indestructible/hoteltile, /area/voidcrew/trader_outpost) -"kQ" = ( -/obj/structure/table/reinforced, -/obj/machinery/cell_charger, -/obj/item/stock_parts/power_store/battery, -/obj/effect/turf_decal/siding/thinplating_new{ - dir = 5 - }, -/obj/effect/turf_decal/trimline/yellow/filled/line, -/obj/effect/turf_decal/trimline/yellow/filled/mid_joiner, -/obj/effect/decal/cleanable/cobweb/cobweb2, -/turf/open/indestructible/large, -/area/voidcrew/trader_outpost) "kS" = ( /obj/effect/turf_decal/siding/wideplating_new/dark{ dir = 1 @@ -1362,33 +1485,6 @@ }, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) -"kY" = ( -/obj/structure/table/reinforced, -/obj/item/multitool{ - pixel_w = -4; - pixel_z = 3 - }, -/obj/item/assembly/signaler{ - pixel_w = 5; - pixel_z = -1 - }, -/obj/effect/turf_decal/siding/thinplating_new{ - dir = 8 - }, -/turf/open/indestructible/large, -/area/voidcrew/trader_outpost) -"lw" = ( -/obj/structure/rack, -/obj/effect/turf_decal/trimline/red/filled/line{ - dir = 4 - }, -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 4 - }, -/obj/effect/decal/cleanable/dirt/dust, -/obj/item/gun/energy/laser/bluetag, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "lx" = ( /obj/effect/turf_decal/tile/yellow/half/contrasted{ dir = 8 @@ -1425,13 +1521,6 @@ }, /turf/open/indestructible, /area/voidcrew/trader_outpost) -"lR" = ( -/obj/effect/turf_decal/tile/red/half/contrasted{ - dir = 1 - }, -/obj/effect/decal/cleanable/dirt/dust, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "lT" = ( /obj/effect/turf_decal/stripes, /turf/open/indestructible/dark/textured, @@ -1493,13 +1582,6 @@ /obj/effect/decal/cleanable/dirt, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) -"nH" = ( -/obj/effect/turf_decal/tile/red/half/contrasted{ - dir = 8 - }, -/obj/effect/decal/cleanable/dirt/dust, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "nO" = ( /obj/effect/turf_decal/siding/thinplating_new{ dir = 8 @@ -1562,13 +1644,6 @@ /obj/effect/decal/cleanable/dirt, /turf/open/indestructible, /area/voidcrew/trader_outpost) -"oA" = ( -/obj/structure/showcase/mecha/ripley, -/obj/effect/turf_decal/trimline/red/filled/line, -/obj/effect/turf_decal/siding/thinplating_new/dark, -/obj/effect/decal/cleanable/dirt/dust, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "oU" = ( /obj/effect/turf_decal/siding/wideplating_new/dark{ dir = 8 @@ -1691,13 +1766,6 @@ }, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) -"qe" = ( -/obj/machinery/mech_bay_recharge_port, -/obj/effect/turf_decal/trimline/red/filled/line, -/obj/effect/turf_decal/siding/thinplating_new/dark, -/obj/effect/decal/cleanable/dirt/dust, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "qk" = ( /obj/effect/turf_decal/siding/thinplating_new/dark{ dir = 1 @@ -1716,21 +1784,6 @@ /obj/structure/extinguisher_cabinet/directional/south, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) -"qo" = ( -/obj/item/toy/mecha/honk{ - pixel_w = 0; - pixel_z = 11 - }, -/obj/structure/table/reinforced, -/obj/effect/turf_decal/trimline/red/filled/line{ - dir = 8 - }, -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 8 - }, -/obj/effect/decal/cleanable/dirt/dust, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "qN" = ( /obj/structure/closet/crate/secure{ name = "chassis crate"; @@ -1746,17 +1799,6 @@ /obj/effect/decal/cleanable/dirt, /turf/open/indestructible, /area/voidcrew/trader_outpost) -"qS" = ( -/obj/effect/turf_decal/siding/thinplating_new, -/obj/effect/turf_decal/trimline/yellow/filled/line{ - dir = 4 - }, -/obj/effect/turf_decal/trimline/yellow/filled/mid_joiner{ - dir = 4 - }, -/obj/effect/decal/cleanable/dirt, -/turf/open/indestructible/large, -/area/voidcrew/trader_outpost) "rd" = ( /obj/effect/turf_decal/trimline/yellow/filled/line{ dir = 8 @@ -1796,17 +1838,6 @@ /obj/machinery/light/directional/east, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) -"rw" = ( -/obj/structure/showcase/fakesec{ - dir = 2 - }, -/obj/effect/turf_decal/bot, -/obj/effect/turf_decal/siding/thinplating_new{ - dir = 9 - }, -/obj/effect/decal/cleanable/cobweb, -/turf/open/indestructible/large, -/area/voidcrew/trader_outpost) "rZ" = ( /obj/effect/turf_decal/tile/yellow/half/contrasted, /obj/effect/turf_decal/siding/thinplating_new, @@ -1867,10 +1898,6 @@ /obj/machinery/newscaster/directional/south, /turf/open/indestructible/hoteltile, /area/voidcrew/trader_outpost) -"st" = ( -/obj/effect/decal/cleanable/dirt/dust, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "su" = ( /obj/effect/turf_decal/bot, /obj/effect/decal/cleanable/dirt, @@ -1910,24 +1937,6 @@ }, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) -"sU" = ( -/obj/effect/turf_decal/tile/red/half/contrasted{ - dir = 4 - }, -/obj/effect/decal/cleanable/dirt, -/obj/effect/decal/cleanable/dirt/dust, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) -"tb" = ( -/obj/structure/showcase/horrific_experiment, -/obj/effect/turf_decal/trimline/yellow/filled/line, -/obj/effect/turf_decal/trimline/yellow/filled/mid_joiner, -/obj/effect/turf_decal/bot, -/obj/effect/turf_decal/siding/thinplating_new{ - dir = 1 - }, -/turf/open/indestructible/large, -/area/voidcrew/trader_outpost) "tj" = ( /obj/structure/closet/secure_closet/outpost_rental, /obj/effect/turf_decal/siding/wideplating_new/dark{ @@ -1981,24 +1990,6 @@ /obj/machinery/light/directional/west, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) -"tO" = ( -/obj/structure/table/reinforced, -/obj/item/reagent_containers/cup/beaker/large{ - pixel_w = -3; - pixel_z = 6 - }, -/obj/item/reagent_containers/cup/beaker{ - pixel_w = 6; - pixel_z = 3 - }, -/obj/effect/turf_decal/siding/thinplating_new{ - dir = 9 - }, -/obj/effect/turf_decal/trimline/yellow/filled/line, -/obj/effect/turf_decal/trimline/yellow/filled/mid_joiner, -/obj/effect/decal/cleanable/dirt, -/turf/open/indestructible/large, -/area/voidcrew/trader_outpost) "tQ" = ( /obj/effect/turf_decal/tile/dark/full, /obj/structure/sign/poster/official/work_for_a_future{ @@ -2101,15 +2092,6 @@ /obj/machinery/light/directional/east, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) -"vy" = ( -/obj/structure/chair/office{ - dir = 1 - }, -/obj/effect/turf_decal/siding/thinplating_new{ - dir = 8 - }, -/turf/open/indestructible/large, -/area/voidcrew/trader_outpost) "vz" = ( /turf/open/indestructible/dark/smooth_large, /area/voidcrew/trader_outpost) @@ -2285,19 +2267,6 @@ /obj/effect/turf_decal/tile/yellow/half/contrasted, /turf/open/indestructible, /area/voidcrew/trader_outpost) -"xA" = ( -/obj/structure/safe, -/obj/effect/turf_decal/trimline/red/filled/corner{ - dir = 8 - }, -/obj/effect/turf_decal/siding/thinplating_new/dark/corner{ - dir = 8 - }, -/obj/effect/decal/cleanable/cobweb/cobweb2, -/obj/effect/decal/cleanable/dirt/dust, -/obj/item/hand_item/circlegame, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "xC" = ( /obj/effect/turf_decal/siding/wideplating_new/dark{ dir = 4 @@ -2361,20 +2330,6 @@ /obj/item/kirbyplants/organic/plant22, /turf/open/indestructible, /area/voidcrew/trader_outpost) -"yP" = ( -/obj/effect/turf_decal/trimline/red/filled/line, -/obj/effect/turf_decal/siding/thinplating_new/dark, -/obj/effect/decal/cleanable/dirt/dust, -/obj/machinery/light/small/red/directional/north, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) -"zo" = ( -/obj/structure/showcase/fakeid, -/obj/effect/turf_decal/bot, -/obj/item/radio/intercom/directional/north, -/obj/machinery/light/directional/north, -/turf/open/indestructible/large, -/area/voidcrew/trader_outpost) "zw" = ( /obj/structure/chair{ dir = 1 @@ -2551,16 +2506,6 @@ }, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) -"BN" = ( -/obj/structure/table/reinforced, -/obj/item/stack/cable_coil, -/obj/effect/spawner/random/engineering/tool, -/obj/effect/turf_decal/siding/thinplating_new{ - dir = 10 - }, -/obj/machinery/light/directional/south, -/turf/open/indestructible/large, -/area/voidcrew/trader_outpost) "BO" = ( /obj/effect/turf_decal/siding/thinplating_new, /turf/open/indestructible/large, @@ -2872,14 +2817,6 @@ }, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) -"GF" = ( -/obj/effect/turf_decal/tile/red/anticorner/contrasted{ - dir = 1 - }, -/obj/effect/decal/cleanable/dirt, -/obj/effect/decal/cleanable/dirt/dust, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "GI" = ( /obj/effect/turf_decal/siding/thinplating_new{ dir = 4 @@ -2891,21 +2828,6 @@ /obj/effect/turf_decal/siding/wideplating_new/dark, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) -"He" = ( -/obj/structure/table/reinforced, -/obj/item/soap/omega{ - pixel_w = 0; - pixel_z = 3 - }, -/obj/effect/turf_decal/trimline/red/filled/line{ - dir = 8 - }, -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 8 - }, -/obj/effect/decal/cleanable/dirt/dust, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "Hg" = ( /obj/effect/turf_decal/siding/thinplating_new{ dir = 4 @@ -3108,19 +3030,6 @@ /obj/structure/table/reinforced, /turf/open/indestructible/large, /area/voidcrew/trader_outpost) -"Ks" = ( -/obj/structure/rack, -/obj/item/gun/ballistic/rifle/boltaction, -/obj/effect/turf_decal/trimline/red/filled/line{ - dir = 4 - }, -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 4 - }, -/obj/effect/decal/cleanable/cobweb, -/obj/effect/decal/cleanable/dirt/dust, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "Kt" = ( /obj/structure/sign/painting/large/library{ dir = 4 @@ -3565,17 +3474,6 @@ }, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) -"St" = ( -/obj/effect/turf_decal/trimline/red/filled/line{ - dir = 8 - }, -/obj/effect/turf_decal/siding/thinplating_new/dark{ - dir = 8 - }, -/obj/effect/decal/cleanable/dirt, -/obj/effect/decal/cleanable/dirt/dust, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "SC" = ( /obj/effect/turf_decal/tile/yellow/half/contrasted{ dir = 1 @@ -3645,12 +3543,6 @@ /obj/effect/turf_decal/tile/blue/opposingcorners, /turf/open/indestructible/white, /area/voidcrew/trader_outpost) -"TB" = ( -/obj/structure/chair/office{ - dir = 1 - }, -/turf/open/indestructible/large, -/area/voidcrew/trader_outpost) "TG" = ( /turf/open/indestructible{ base_icon_state = "stairs-m"; @@ -3684,15 +3576,6 @@ /obj/effect/turf_decal/siding/thinplating_new, /turf/open/indestructible, /area/voidcrew/trader_outpost) -"UD" = ( -/obj/effect/turf_decal/trimline/yellow/filled/line{ - dir = 4 - }, -/obj/effect/turf_decal/trimline/yellow/filled/mid_joiner{ - dir = 4 - }, -/turf/open/indestructible/large, -/area/voidcrew/trader_outpost) "UU" = ( /obj/machinery/medical_kiosk, /obj/effect/turf_decal/trimline/blue/filled/line{ @@ -3782,23 +3665,6 @@ /obj/effect/turf_decal/tile/blue/opposingcorners, /turf/open/indestructible/white, /area/voidcrew/trader_outpost) -"WA" = ( -/obj/structure/showcase/machinery/signal_decrypter, -/obj/effect/turf_decal/trimline/yellow/filled/line{ - dir = 4 - }, -/obj/effect/turf_decal/trimline/yellow/filled/mid_joiner{ - dir = 4 - }, -/obj/effect/turf_decal/bot, -/obj/effect/turf_decal/siding/thinplating_new{ - dir = 1 - }, -/obj/structure/sign/painting/library{ - pixel_y = 32 - }, -/turf/open/indestructible/large, -/area/voidcrew/trader_outpost) "WJ" = ( /obj/machinery/door/airlock/multi_tile/public/glass{ dir = 4; @@ -4481,9 +4347,9 @@ ao ao dU dU -Ks -gq -lw +at +aU +be NJ lE dP @@ -4522,11 +4388,11 @@ ao ao ao dU -qe -GF -nH -fV -nH +ch +aT +bb +aQ +bj lZ bs Um @@ -4564,11 +4430,11 @@ ao ao ao dU -oA -lR -st -st -st +aQ +aG +bc +aQ +bk lZ bs Um @@ -4606,11 +4472,11 @@ ao ao ao dU -yP -aY -gh -gh -sU +aQ +aQ +aQ +aG +bc lZ vo cX @@ -4648,11 +4514,11 @@ ao ao ao dU -xA -aT -St -He -qo +ch +aY +bb +aQ +bo lE Lx oX @@ -4691,9 +4557,9 @@ ao ao dU dU -lE -lE -lE +aQ +aQ +aQ dU dU dU @@ -4732,11 +4598,11 @@ ao ao ao dU -rw -vy -HF -kY -BN +ci +bY +Xe +cb +cc dU uD lx @@ -4774,11 +4640,11 @@ ao ao aF dU -zo -TB -ks -Xe +cj bX +ca +Xe +cd lE aJ fp @@ -4816,11 +4682,11 @@ ao ao dU dU -WA -UD -UD -UD -qS +bW +Xe +Xe +ks +Xe lE aJ fp @@ -4857,13 +4723,13 @@ ao (23,1,1) = {" ao lE -tO +ce lM Ao Ao Ao Ao -qa +ck hu fp au @@ -4899,13 +4765,13 @@ ao (24,1,1) = {" ao lE -tb +ce ai fp fp fp fp -gV +cl fp fp au @@ -4941,13 +4807,13 @@ aF (25,1,1) = {" ao lE -kQ +cf sC Jm Jm Jm Jm -ey +cm nm fp au From fa49953d82f80852b4f435201108f5c4cfa918b3 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 01:51:57 -0800 Subject: [PATCH 03/16] Give Wick a work routine in the Rig Bay Wick walks between the mechs on their stands and works on them, waits at his counter between jobs, and stops to serve any player who comes up to him or waits at his counter. Work thresholds now close off a worker's room. --- tgstation.dme | 1 + .../ambient_npcs/strings/outpost_patrons.json | 7 +- .../player_outposts/outpost_ambient_work.dm | 1 + .../modules/trade/rig_bay/rig_bay_wick.dm | 563 ++++++++++++++++++ voidcrew/modules/unit_tests/_unit_tests.dm | 1 + .../unit_tests/voidcrew_rig_bay_wick.dm | 116 ++++ 6 files changed, 687 insertions(+), 2 deletions(-) create mode 100644 voidcrew/modules/trade/rig_bay/rig_bay_wick.dm create mode 100644 voidcrew/modules/unit_tests/voidcrew_rig_bay_wick.dm diff --git a/tgstation.dme b/tgstation.dme index f969d380ffc..268e2c61093 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7742,6 +7742,7 @@ #include "voidcrew\modules\trade\favor_uniques\vex.dm" #include "voidcrew\modules\trade\rig_bay\rig_bay_skus.dm" #include "voidcrew\modules\trade\rig_bay\rig_bay_stand.dm" +#include "voidcrew\modules\trade\rig_bay\rig_bay_wick.dm" #include "voidcrew\modules\transporter\_transporter.dm" #include "voidcrew\modules\transporter\transporter_console.dm" #include "voidcrew\modules\transporter\transporter_designs.dm" diff --git a/voidcrew/modules/ambient_npcs/strings/outpost_patrons.json b/voidcrew/modules/ambient_npcs/strings/outpost_patrons.json index 2d9667bb1b5..373f718cb38 100644 --- a/voidcrew/modules/ambient_npcs/strings/outpost_patrons.json +++ b/voidcrew/modules/ambient_npcs/strings/outpost_patrons.json @@ -182,13 +182,16 @@ "I'd like the good chassis. Yes, I know I haven't been measured.", "The seal on the left glove keeps letting go.", "Have you got plating that isn't safety orange?", - "A jetpack module. My captain says no. My captain isn't here." + "A jetpack module. My captain says no. My captain isn't here.", + "Is that Ripley spoken for?", + "How much to take the yellow one off the chains?" ], "trader_reply": [ "Stand on the mark and lift your arms. Higher.", "That noise is your warranty leaving.", "Bench install's free. Corridor installs cost fingers.", - "Hold still. Breathe in. Now don't." + "Hold still. Breathe in. Now don't.", + "Money on the counter first." ] }, "vex": { diff --git a/voidcrew/modules/player_outposts/outpost_ambient_work.dm b/voidcrew/modules/player_outposts/outpost_ambient_work.dm index 6b381093a3a..a53f98ffab2 100644 --- a/voidcrew/modules/player_outposts/outpost_ambient_work.dm +++ b/voidcrew/modules/player_outposts/outpost_ambient_work.dm @@ -188,6 +188,7 @@ /proc/outpost_work_barrier(turf/tile) var/static/list/barriers = typecacheof(list( /obj/effect/landmark/outpost_elevator_alcove, + /obj/effect/landmark/outpost_work_threshold, /obj/machinery/door, /obj/structure/fence, /obj/structure/grille, diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm b/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm new file mode 100644 index 00000000000..6d3b6323ce5 --- /dev/null +++ b/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm @@ -0,0 +1,563 @@ +/** + * # Wick's routine + * + * Wick (/mob/living/basic/outpost_trader/suit_fitter) works on the mechs hanging in the Rig Bay + * between customers. He walks to a stand, welds or ratchets at the mech for a while, walks back + * to his counter and waits there before the next job. Nothing he does changes the mech: it is for + * show, like the rest of the ambient work loop (voidcrew/modules/player_outposts/outpost_ambient_work.dm). + * + * Serving comes first. While a player is within RIG_BAY_WICK_SERVE_RANGE tiles of him in the bay, or + * has his shop window open, he drops what he is doing, faces them and stays put. He takes no new job + * until RIG_BAY_WICK_RESUME after they have gone. A player waiting at the counter while he is out on + * the floor calls him back. + * + * Nothing here knows the map. His room is the floor he can reach from his spawn tile, closed off by + * walls, doors, windows and /obj/effect/landmark/outpost_work_threshold. Stands are found in it at + * runtime. He never stands on a stand or on the tile in front of one (the lane a sold mech drives + * out along), not even in passing: he walks his own cardinal path over his room, which leaves those + * tiles out. + * + * Hooked up from setup_from_shop() below, which the outpost's interior link (or Initialize, on the + * round-start pre-load) already calls. The shop and stand code need no call into this file. + */ + +/// How far from his spawn tile Wick's room may reach. A bound, not a layout: walls and thresholds close it. +#define RIG_BAY_WICK_RANGE 14 +/// A player this close to him (or to his counter, see below) is a customer +#define RIG_BAY_WICK_SERVE_RANGE 3 +/// No new job until this long after the last customer left +#define RIG_BAY_WICK_RESUME (30 SECONDS) +/// Rest at the counter between two jobs +#define RIG_BAY_WICK_REST_LOW (20 SECONDS) +#define RIG_BAY_WICK_REST_HIGH (60 SECONDS) +/// One step of his walk +#define RIG_BAY_WICK_STEP_TIME (0.5 SECONDS) +/// Beat of the work itself, as the base work loop +#define RIG_BAY_WICK_WORK_BEAT (2 SECONDS) +/// Failed steps in a row before he gives up on a walk +#define RIG_BAY_WICK_STUCK_LIMIT 6 +/// Which of the outfit's people Wick is, so he looks the same every round +#define RIG_BAY_WICK_LOOK_NUMBER 3 +/// Chance he mutters something when he starts on a mech +#define RIG_BAY_WICK_WORK_LINE_CHANCE 25 + +#define BB_RIG_BAY_WICK_CUSTOMER "rig_bay_wick_customer" +/// Blackboard: failed steps in a row on the current walk +#define BB_RIG_BAY_WICK_STUCK "rig_bay_wick_stuck" + +// ========================================================================= +// THE MOB +// ========================================================================= + +/mob/living/basic/outpost_trader/suit_fitter + ai_controller = /datum/ai_controller/basic_controller/rig_bay_wick + /// What he holds right now: "weld", "tool" or null + var/rig_bay_look + /// Bumped per look build, so a slow build never lands over a newer one + var/rig_bay_look_serial = 0 + +/** + * As the parent, but dressed through the outpost worker looks (one person, three poses: idle, + * welding, wrench), so his face doesn't change when he picks up a tool. The parent's + * apply_dynamic_human_appearance() is async too and would race the work looks, so it is not called. + */ +/mob/living/basic/outpost_trader/suit_fitter/setup_from_shop() + if(shop_setup_done || !shop) + return + shop_setup_done = TRUE + name = shop.trader_name + real_name = shop.trader_name + gender = shop.trader_gender + if(shop.trader_voice_pack) + set_bark_voice_pack(shop.trader_voice_pack) + var/datum/atom_voice/bark_voice = get_bark_voice() + if(bark_voice) + bark_voice.pitch = shop.trader_voice_pitch + show_rig_bay_look(null) + start_rig_bay_routine() + +/// Gives him the worker component. Home is the tile he stands on now (his mapped spot). +/mob/living/basic/outpost_trader/suit_fitter/proc/start_rig_bay_routine() + if(GetComponent(/datum/component/outpost_ambient_worker)) + return + AddComponent(/datum/component/outpost_ambient_worker/rig_bay_wick, list( + /datum/outpost_ambient_work/mech_service = 6, + /datum/outpost_ambient_work/panel = 1, + ), TRUE, CALLBACK(src, PROC_REF(show_rig_bay_look))) + +/// The worker component's look callback: "weld", "tool" or null for free hands +/mob/living/basic/outpost_trader/suit_fitter/proc/show_rig_bay_look(look) + rig_bay_look = look + INVOKE_ASYNC(src, PROC_REF(build_rig_bay_look)) + +/// Can sleep: the first build dresses a dummy +/mob/living/basic/outpost_trader/suit_fitter/proc/build_rig_bay_look() + if(QDELETED(src) || !shop) + return + var/serial = ++rig_bay_look_serial + var/list/looks = get_outpost_worker_looks(shop.trader_outfit, shop.trader_gender, RIG_BAY_WICK_LOOK_NUMBER) + if(QDELETED(src) || serial != rig_bay_look_serial || !length(looks)) + return + apply_outpost_npc_look(src, looks[rig_bay_look] || looks["idle"]) + // Applying a look wipes every overlay, the welding sparks included + var/datum/component/outpost_ambient_worker/worker = GetComponent(/datum/component/outpost_ambient_worker) + if(worker?.work_overlay) + add_overlay(worker.work_overlay) + +// ========================================================================= +// THE WORKER +// ========================================================================= + +/** + * The ambient worker with a bigger room, no stand or lane tiles in it, his own walk over it, a + * longer rest and a way back to the counter. + */ +/datum/component/outpost_ambient_worker/rig_bay_wick + /// The way he faces at his counter + var/home_dir = SOUTH + /// Stand tiles in reach of home, as an assoc list. Rebuilt with the room. + var/list/stand_tiles + /// The tile in front of each stand, as an assoc list. Rebuilt with the room. + var/list/lane_tiles + +/datum/component/outpost_ambient_worker/rig_bay_wick/Initialize(list/work_weights, keep_off_doorways = FALSE, datum/callback/on_look) + . = ..() + if(. == COMPONENT_INCOMPATIBLE) + return + var/atom/movable/body = parent + home_dir = body.dir + +/datum/component/outpost_ambient_worker/rig_bay_wick/Destroy(force) + stand_tiles = null + lane_tiles = null + return ..() + +/** + * As the parent's room, out to RIG_BAY_WICK_RANGE, less every stand tile and every stand's front + * tile. Stands and thresholds are whatever the map has. + */ +/datum/component/outpost_ambient_worker/rig_bay_wick/get_room() + if(room && world.time < room_built_at + OUTPOST_WORK_ROOM_REFRESH) + return room + room_built_at = world.time + room = list() + stand_tiles = list() + lane_tiles = list() + if(!isopenturf(home)) + return room + for(var/obj/structure/rig_bay_stand/stand in range(RIG_BAY_WICK_RANGE, home)) + var/turf/stand_tile = get_turf(stand) + stand_tiles[stand_tile] = TRUE + var/turf/front = get_step(stand_tile, stand.dir) + if(front) + lane_tiles[front] = TRUE + var/area/home_area = get_area(home) + var/list/docking_floor = docking_floor_near(home) + var/list/lifts = lift_turfs_near(home, RIG_BAY_WICK_RANGE) + room[home] = TRUE + if(docking_floor[home]) + return room + var/list/seen = list() + seen[home] = TRUE + var/list/queue = list(home) + var/index = 1 + while(index <= length(queue)) + var/turf/tile = queue[index++] + for(var/direction in GLOB.cardinals) + var/turf/next = get_step(tile, direction) + if(isnull(next) || seen[next]) + continue + seen[next] = TRUE + if(stand_tiles[next] || lane_tiles[next]) + continue + if(get_dist(next, home) > RIG_BAY_WICK_RANGE || !room_tile_allowed(next, home_area, docking_floor, lifts)) + continue + room[next] = TRUE + queue += next + return room + +/// Whether `tile` is part of the bay: his room, a stand or a lane +/datum/component/outpost_ambient_worker/rig_bay_wick/proc/in_bay(turf/tile) + var/list/tiles = get_room() + return tiles[tile] || stand_tiles[tile] || lane_tiles[tile] + +/// Mechs on the stands first, then the parent's search around him for anything else (wall panels) +/datum/component/outpost_ambient_worker/rig_bay_wick/find_work() + if(isnull(get_turf(parent)) || !length(work_weights)) + return null + var/list/all_weights = work_weights + var/list/weights = all_weights.Copy() + . = null + while(length(weights) && isnull(.)) + var/work_type = pick_weight(weights) + weights -= work_type + if(ispath(work_type, /datum/outpost_ambient_work/mech_service)) + . = find_mech_job(outpost_ambient_work(work_type)) + continue + work_weights = list() + work_weights[work_type] = 1 + . = ..() + work_weights = all_weights + work_weights = all_weights + +/// A mech on one of his stands that the job accepts, and a free tile beside it, as find_work() returns +/datum/component/outpost_ambient_worker/rig_bay_wick/proc/find_mech_job(datum/outpost_ambient_work/kind) + get_room() + var/list/options = list() + for(var/turf/stand_tile as anything in stand_tiles) + for(var/obj/vehicle/sealed/mecha/mech in stand_tile) + if(!IS_WEAKREF_OF(mech, last_target) && kind.accepts(mech)) + options += mech + // The last one worked on only when it is the only one + if(!length(options) && last_target) + var/obj/vehicle/sealed/mecha/last_mech = last_target.resolve() + if(istype(last_mech) && stand_tiles[get_turf(last_mech)] && kind.accepts(last_mech)) + options += last_mech + for(var/obj/vehicle/sealed/mecha/mech as anything in shuffle(options)) + var/turf/spot = spot_for(mech) + if(spot) + return list(mech, spot, kind) + return null + +/datum/component/outpost_ambient_worker/rig_bay_wick/stop_work() + var/was_working = !isnull(work) + ..() + if(was_working) + next_work_at = world.time + rand(RIG_BAY_WICK_REST_LOW, RIG_BAY_WICK_REST_HIGH) + +/** + * The next tile on the shortest cardinal walk over his room to `goal`, or null. Tiles with + * something solid on them (a customer included) are walked round. + */ +/datum/component/outpost_ambient_worker/rig_bay_wick/proc/next_step_to(turf/goal) + var/turf/here = get_turf(parent) + if(isnull(here) || isnull(goal) || here == goal) + return null + var/list/tiles = get_room() + if(!tiles[goal]) + return null + // Searched from the goal back, so the tile that reaches `here` is the step to take + var/list/seen = list() + seen[goal] = TRUE + var/list/queue = list(goal) + var/index = 1 + while(index <= length(queue)) + var/turf/tile = queue[index++] + for(var/direction in GLOB.cardinals) + var/turf/next = get_step(tile, direction) + if(isnull(next) || seen[next]) + continue + if(next == here) + return tile + seen[next] = TRUE + if(!tiles[next] || next.is_blocked_turf(exclude_mobs = FALSE, ignore_atoms = list(parent))) + continue + queue += next + return null + +/// One step towards `goal`. FALSE if there is no way or the step failed. +/datum/component/outpost_ambient_worker/rig_bay_wick/proc/walk_toward(turf/goal) + var/turf/next = next_step_to(goal) + if(isnull(next)) + return FALSE + var/atom/movable/body = parent + var/turf/before = body.loc + step_to_tile(next) + return body.loc != before + +// ========================================================================= +// AI +// ========================================================================= + +/** + * Passive, like the ambient NPCs: no targeting, so the outpost turrets read him as harmless, and + * nothing at all while no player is near. + */ +/datum/ai_controller/basic_controller/rig_bay_wick + blackboard = list() + ai_traits = PASSIVE_AI_FLAGS + ai_movement = /datum/ai_movement/basic_avoidance + idle_behavior = null + planning_subtrees = list( + /datum/ai_planning_subtree/rig_bay_serve, + /datum/ai_planning_subtree/rig_bay_wick_work, + ) + /// When a customer was last seen + var/last_customer_at = -RIG_BAY_WICK_RESUME + /// The customer being served now, if any + var/datum/weakref/customer_ref + +/datum/ai_controller/basic_controller/rig_bay_wick/TryPossessPawn(atom/new_pawn) + if(!istype(new_pawn, /mob/living/basic/outpost_trader)) + return AI_CONTROLLER_INCOMPATIBLE + return ..() + +/datum/ai_controller/basic_controller/rig_bay_wick/proc/get_worker() + return pawn?.GetComponent(/datum/component/outpost_ambient_worker/rig_bay_wick) + +/** + * A player within RIG_BAY_WICK_SERVE_RANGE of `center`, standing in the bay (so someone walking + * past outside the windows doesn't count), on foot or in a vehicle. Null if there is none. + */ +/datum/ai_controller/basic_controller/rig_bay_wick/proc/find_customer(atom/center) + var/datum/component/outpost_ambient_worker/rig_bay_wick/worker = get_worker() + if(isnull(worker) || isnull(get_turf(center))) + return null + for(var/atom/movable/thing in range(RIG_BAY_WICK_SERVE_RANGE, center)) + if(thing == pawn || !worker.in_bay(get_turf(thing))) + continue + if(isliving(thing)) + if(is_customer(thing)) + return thing + else if(istype(thing, /obj/vehicle)) + var/obj/vehicle/ride = thing + for(var/mob/living/rider in ride.occupants) + if(is_customer(rider)) + return rider + return null + +/datum/ai_controller/basic_controller/rig_bay_wick/proc/is_customer(mob/living/someone) + return someone != pawn && someone.stat != DEAD && !!GET_CLIENT(someone) + +/// Someone with his shop window open, wherever they stand +/datum/ai_controller/basic_controller/rig_bay_wick/proc/find_shopper() + var/mob/living/basic/outpost_trader/wick = pawn + if(!istype(wick) || !wick.shop_ui) + return null + for(var/datum/tgui/window as anything in wick.shop_ui.open_uis) + var/mob/living/viewer = window.user + if(istype(viewer) && is_customer(viewer)) + return viewer + return null + +/// Whether a customer is at him right now. Checked by his jobs on every beat. +/datum/ai_controller/basic_controller/rig_bay_wick/proc/customer_at_hand() + return find_customer(pawn) || find_shopper() + +/** + * First in planning. With a customer at him: drop everything, face them and stay. With one + * waiting at his counter while he is out on the floor: walk back. Either way no new job until + * RIG_BAY_WICK_RESUME after the last of them has gone. + */ +/datum/ai_planning_subtree/rig_bay_serve + +/datum/ai_planning_subtree/rig_bay_serve/SelectBehaviors(datum/ai_controller/basic_controller/rig_bay_wick/controller, seconds_per_tick) + var/datum/component/outpost_ambient_worker/rig_bay_wick/worker = controller.get_worker() + if(isnull(worker) || controller.ai_status != AI_STATUS_ON) + return + var/mob/living/customer = controller.customer_at_hand() + var/calling = FALSE + if(isnull(customer) && get_turf(controller.pawn) != worker.home) + customer = controller.find_customer(worker.home) + calling = !isnull(customer) + var/mob/living/old_customer = controller.customer_ref?.resolve() + if(isnull(customer)) + controller.customer_ref = null + controller.clear_blackboard_key(BB_RIG_BAY_WICK_CUSTOMER) + return + // A new face, or someone back after he'd gone back to work, gets a word + var/greet = customer != old_customer && world.time > controller.last_customer_at + RIG_BAY_WICK_RESUME + controller.last_customer_at = world.time + worker.next_work_at = max(worker.next_work_at, world.time + RIG_BAY_WICK_RESUME) + controller.customer_ref = WEAKREF(customer) + controller.set_blackboard_key(BB_RIG_BAY_WICK_CUSTOMER, customer) + if(greet) + var/mob/living/basic/outpost_trader/wick = controller.pawn + wick.speak_line(TRADER_LINE_IDLE, rig_bay_wick_greeting(calling)) + if(calling) + controller.queue_behavior(/datum/ai_behavior/rig_bay_wick_go_home) + return SUBTREE_RETURN_FINISH_PLANNING + controller.queue_behavior(/datum/ai_behavior/rig_bay_wick_serve, BB_RIG_BAY_WICK_CUSTOMER) + return SUBTREE_RETURN_FINISH_PLANNING + +/// What he says to a new customer: from across the bay, or face to face +/proc/rig_bay_wick_greeting(calling) + if(calling) + return pick("Be right with you.", "Coming. Don't touch anything.", "One second.") + return pick("Yeah?", "What do you need?", "Help you?", "What can I get you?") + +/// Mech work when it's due, the walk back to the counter after it, and waiting there between jobs +/datum/ai_planning_subtree/rig_bay_wick_work + +/datum/ai_planning_subtree/rig_bay_wick_work/SelectBehaviors(datum/ai_controller/basic_controller/rig_bay_wick/controller, seconds_per_tick) + var/datum/component/outpost_ambient_worker/rig_bay_wick/worker = controller.get_worker() + if(worker && controller.ai_status == AI_STATUS_ON && !worker.check_leash()) + if(world.time >= worker.next_work_at) + var/list/job = worker.find_work() + if(job) + worker.planned_work = job[3] + controller.set_blackboard_key(BB_OUTPOST_WORK_TARGET, job[1]) + controller.set_blackboard_key(BB_OUTPOST_WORK_SPOT, job[2]) + controller.queue_behavior(/datum/ai_behavior/rig_bay_wick_job, BB_OUTPOST_WORK_TARGET, BB_OUTPOST_WORK_SPOT) + return SUBTREE_RETURN_FINISH_PLANNING + worker.next_work_at = world.time + OUTPOST_WORK_RETRY + if(get_turf(controller.pawn) != worker.home) + controller.queue_behavior(/datum/ai_behavior/rig_bay_wick_go_home) + return SUBTREE_RETURN_FINISH_PLANNING + // Queued even with nothing to do, so planning never fails and churns the controller + controller.queue_behavior(/datum/ai_behavior/rig_bay_wick_idle) + return SUBTREE_RETURN_FINISH_PLANNING + +/// Walks to the spot by his own path, then works until the job is done or a customer turns up +/datum/ai_behavior/rig_bay_wick_job + action_cooldown = RIG_BAY_WICK_STEP_TIME + +/datum/ai_behavior/rig_bay_wick_job/get_cooldown(datum/ai_controller/basic_controller/rig_bay_wick/cooldown_for) + var/datum/component/outpost_ambient_worker/worker = cooldown_for.pawn?.GetComponent(/datum/component/outpost_ambient_worker) + return worker?.work ? RIG_BAY_WICK_WORK_BEAT : RIG_BAY_WICK_STEP_TIME + +/datum/ai_behavior/rig_bay_wick_job/setup(datum/ai_controller/controller, target_key, spot_key) + if(isnull(controller.blackboard[spot_key]) || isnull(controller.blackboard[target_key])) + return FALSE + controller.set_blackboard_key(BB_RIG_BAY_WICK_STUCK, 0) + return TRUE + +/datum/ai_behavior/rig_bay_wick_job/perform(seconds_per_tick, datum/ai_controller/basic_controller/rig_bay_wick/controller, target_key, spot_key) + var/datum/component/outpost_ambient_worker/rig_bay_wick/worker = controller.get_worker() + var/atom/target = controller.blackboard[target_key] + var/turf/spot = controller.blackboard[spot_key] + if(isnull(worker) || QDELETED(target) || controller.ai_status != AI_STATUS_ON || controller.customer_at_hand()) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + if(isnull(worker.work)) + if(get_turf(controller.pawn) == spot) + return worker.start_work(target) ? AI_BEHAVIOR_DELAY : (AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED) + if(!worker.can_stand_at(spot) || !rig_bay_wick_step(controller, worker, spot)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + return AI_BEHAVIOR_DELAY + if(worker.continue_work()) + return AI_BEHAVIOR_DELAY + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/rig_bay_wick_job/finish_action(datum/ai_controller/controller, succeeded, target_key, spot_key) + . = ..() + var/datum/component/outpost_ambient_worker/worker = controller.pawn?.GetComponent(/datum/component/outpost_ambient_worker) + if(worker) + worker.planned_work = null + worker.stop_work() + if(!succeeded) + worker.next_work_at = max(worker.next_work_at, world.time + OUTPOST_WORK_RETRY) + controller.clear_blackboard_key(target_key) + controller.clear_blackboard_key(spot_key) + +/// One step of a walk. FALSE once he has been stuck too long. +/proc/rig_bay_wick_step(datum/ai_controller/controller, datum/component/outpost_ambient_worker/rig_bay_wick/worker, turf/goal) + if(worker.walk_toward(goal)) + controller.set_blackboard_key(BB_RIG_BAY_WICK_STUCK, 0) + return TRUE + var/stuck = controller.blackboard[BB_RIG_BAY_WICK_STUCK] + 1 + controller.set_blackboard_key(BB_RIG_BAY_WICK_STUCK, stuck) + return stuck < RIG_BAY_WICK_STUCK_LIMIT + +/// Back to the counter, then faces the way he was mapped +/datum/ai_behavior/rig_bay_wick_go_home + action_cooldown = RIG_BAY_WICK_STEP_TIME + +/datum/ai_behavior/rig_bay_wick_go_home/setup(datum/ai_controller/controller) + controller.set_blackboard_key(BB_RIG_BAY_WICK_STUCK, 0) + return TRUE + +/datum/ai_behavior/rig_bay_wick_go_home/perform(seconds_per_tick, datum/ai_controller/basic_controller/rig_bay_wick/controller) + var/datum/component/outpost_ambient_worker/rig_bay_wick/worker = controller.get_worker() + if(isnull(worker) || controller.ai_status != AI_STATUS_ON) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + var/atom/movable/body = controller.pawn + if(get_turf(body) == worker.home) + body.setDir(worker.home_dir) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + // Someone came up to him on the way: stop here and serve them + if(controller.customer_at_hand()) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + if(!rig_bay_wick_step(controller, worker, worker.home)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + return AI_BEHAVIOR_DELAY + +/// Faces the customer and stays put. Planning runs on, and replaces it once they've gone. +/datum/ai_behavior/rig_bay_wick_serve + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + action_cooldown = 1 SECONDS + +/datum/ai_behavior/rig_bay_wick_serve/perform(seconds_per_tick, datum/ai_controller/controller, customer_key) + var/mob/living/customer = controller.blackboard[customer_key] + var/atom/movable/body = controller.pawn + if(!QDELETED(customer) && get_turf(customer) != get_turf(body)) + body.setDir(get_cardinal_dir(body, customer)) + return AI_BEHAVIOR_DELAY + +/// Nothing to do: stand there. Planning runs on and replaces it. +/datum/ai_behavior/rig_bay_wick_idle + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + action_cooldown = 2 SECONDS + +/datum/ai_behavior/rig_bay_wick_idle/perform(seconds_per_tick, datum/ai_controller/controller) + return AI_BEHAVIOR_DELAY + +// ========================================================================= +// THE WORK +// ========================================================================= + +/** + * Working on a mech hanging on a stand: a welder at a seam or a ratchet on the joints, picked per + * job. For show only; the mech is untouched. + */ +/datum/outpost_ambient_work/mech_service + works_on = list(/obj/vehicle/sealed/mecha) + duration_low = 15 SECONDS + duration_high = 30 SECONDS + look = "tool" + sounds = list('sound/items/tools/ratchet.ogg', 'sound/items/tools/ratchet_slow.ogg', 'sound/items/tools/crowbar.ogg') + /// Played instead of `sounds` on a welding job + var/list/weld_sounds = list('sound/items/tools/welder.ogg', 'sound/items/tools/welder2.ogg') + +/// Only a mech hanging on its stand, never one that's sold or driven in by a customer +/datum/outpost_ambient_work/mech_service/accepts(atom/target) + return ..() && HAS_TRAIT(target, TRAIT_RIG_BAY_CLAMPED) + +/datum/outpost_ambient_work/mech_service/start(datum/component/outpost_ambient_worker/worker, atom/target) + var/atom/movable/body = worker.parent + if(prob(50)) + // The welder: start_work already showed the wrench, so swap it, then the sparks + worker.on_look?.Invoke("weld") + var/mutable_appearance/sparks = mutable_appearance('icons/effects/welding_effect.dmi', "welding_sparks", GASFIRE_LAYER, body, ABOVE_LIGHTING_PLANE) + var/direction = worker.work_dir + sparks.pixel_x = direction & EAST ? 12 : (direction & WEST ? -12 : 0) + sparks.pixel_y = direction & NORTH ? 12 : (direction & SOUTH ? -6 : 0) + worker.set_work_overlay(sparks) + playsound(body, 'sound/items/tools/welderactivate.ogg', 25, TRUE, OUTPOST_WORK_SOUND_RANGE) + if(prob(RIG_BAY_WICK_WORK_LINE_CHANCE) && istype(body, /mob/living/basic/outpost_trader)) + var/mob/living/basic/outpost_trader/wick = body + wick.speak_line(TRADER_LINE_IDLE, pick( + "Who torqued this? A child?", + "That's not a bolt. That's a suggestion.", + "Hydraulics are weeping again.", + "Come on. Come on. There.", + "Who keeps parking these in the rain?", + )) + +/// A welding job is the one with sparks on +/datum/outpost_ambient_work/mech_service/tick(datum/component/outpost_ambient_worker/worker, atom/target) + var/atom/movable/body = worker.parent + if(!worker.work_overlay) + return ..() + body.flash_lighting_fx(1.5, 0.8, OUTPOST_WORK_WELD_LIGHT, 0.5 SECONDS) + if(prob(sound_chance)) + playsound(body, pick(weld_sounds), sound_volume, TRUE, OUTPOST_WORK_SOUND_RANGE) + return TRUE + return FALSE + +/datum/outpost_ambient_work/mech_service/stop(datum/component/outpost_ambient_worker/worker) + if(QDELETED(worker.parent) || !worker.work_overlay) + return + playsound(worker.parent, 'sound/items/tools/welderdeactivate.ogg', 25, TRUE, OUTPOST_WORK_SOUND_RANGE) + +#undef RIG_BAY_WICK_RANGE +#undef RIG_BAY_WICK_SERVE_RANGE +#undef RIG_BAY_WICK_RESUME +#undef RIG_BAY_WICK_REST_LOW +#undef RIG_BAY_WICK_REST_HIGH +#undef RIG_BAY_WICK_STEP_TIME +#undef RIG_BAY_WICK_WORK_BEAT +#undef RIG_BAY_WICK_STUCK_LIMIT +#undef RIG_BAY_WICK_LOOK_NUMBER +#undef RIG_BAY_WICK_WORK_LINE_CHANCE +#undef BB_RIG_BAY_WICK_CUSTOMER +#undef BB_RIG_BAY_WICK_STUCK diff --git a/voidcrew/modules/unit_tests/_unit_tests.dm b/voidcrew/modules/unit_tests/_unit_tests.dm index e70ad1b8e66..c72e3464c9a 100644 --- a/voidcrew/modules/unit_tests/_unit_tests.dm +++ b/voidcrew/modules/unit_tests/_unit_tests.dm @@ -201,6 +201,7 @@ #include "voidcrew_research_movement.dm" #include "voidcrew_respawn_timer.dm" #include "voidcrew_restaurant_portal.dm" +#include "voidcrew_rig_bay_wick.dm" #include "voidcrew_rnd_kit.dm" #include "voidcrew_round_end.dm" #include "voidcrew_round_metrics.dm" diff --git a/voidcrew/modules/unit_tests/voidcrew_rig_bay_wick.dm b/voidcrew/modules/unit_tests/voidcrew_rig_bay_wick.dm new file mode 100644 index 00000000000..5f28005e191 --- /dev/null +++ b/voidcrew/modules/unit_tests/voidcrew_rig_bay_wick.dm @@ -0,0 +1,116 @@ +/** + * Wick's routine in the Rig Bay (voidcrew/modules/trade/rig_bay/rig_bay_wick.dm), on a layout built in + * the test room rather than the outfitter map: + * + * y+4 . [S] . . . S: a stand facing south, a mech hanging on it + * y+3 . L . . . L: the stand's front tile (its lane) + * y+2 . . . . W W: Wick's spawn tile + * y+1 T T T T T T: work thresholds, the edge of his room + * y+0 . . . . . a stand-in player, outside the room + * + * Voidcrew defines are not visible from test files: the trait on a hanging mech is "rig_bay_clamped" + * (TRAIT_RIG_BAY_CLAMPED), his serve range is 3 tiles and he waits 30 s after a customer leaves. + */ +/datum/unit_test/voidcrew_rig_bay_wick + /// Stand-ins for players, counted on their level so his AI runs + var/list/mob/living/present = list() + +/datum/unit_test/voidcrew_rig_bay_wick/Destroy() + for(var/mob/living/someone as anything in present) + for(var/level in 1 to length(SSmobs.clients_by_zlevel)) + SSmobs.clients_by_zlevel[level] -= someone + present = null + return ..() + +/datum/unit_test/voidcrew_rig_bay_wick/proc/at(dx, dy) + return locate(run_loc_floor_bottom_left.x + dx, run_loc_floor_bottom_left.y + dy, run_loc_floor_bottom_left.z) + +/datum/unit_test/voidcrew_rig_bay_wick/Run() + // Landmarks survive the test room sweep, so these go through allocate() to be cleaned up + for(var/dx in 0 to 4) + allocate(/obj/effect/landmark/outpost_work_threshold, at(dx, 1)) + var/turf/stand_tile = at(1, 4) + var/obj/structure/rig_bay_stand/stand = allocate(/obj/structure/rig_bay_stand, stand_tile) + stand.setDir(SOUTH) + var/turf/lane = get_step(stand_tile, SOUTH) + var/obj/vehicle/sealed/mecha/mech = allocate(/obj/vehicle/sealed/mecha/ripley, stand_tile) + ADD_TRAIT(mech, "rig_bay_clamped", "rig_bay") + var/turf/home = at(4, 2) + var/mob/living/basic/outpost_trader/suit_fitter/wick = allocate(/mob/living/basic/outpost_trader/suit_fitter, home) + wick.start_rig_bay_routine() + var/datum/component/outpost_ambient_worker/rig_bay_wick/worker = wick.GetComponent(/datum/component/outpost_ambient_worker/rig_bay_wick) + TEST_ASSERT_NOTNULL(worker, "Wick has no work routine") + var/datum/ai_controller/basic_controller/rig_bay_wick/brain = wick.ai_controller + TEST_ASSERT(istype(brain), "Wick has the wrong AI controller: [wick.ai_controller]") + + // His room: everything above the thresholds, less the stand and its lane + var/list/room = worker.get_room() + TEST_ASSERT(room[home], "Wick's spawn tile is not in his room") + TEST_ASSERT(!room[stand_tile], "Wick's room takes in the stand") + TEST_ASSERT(!room[lane], "Wick's room takes in the stand's front tile") + for(var/turf/tile as anything in room) + TEST_ASSERT(tile.y >= run_loc_floor_bottom_left.y + 2, "Wick's room runs past the threshold to [tile.x],[tile.y]") + TEST_ASSERT_EQUAL(length(room), 13, "Wick's room has [length(room)] tiles, not 13") + TEST_ASSERT(!worker.can_stand_at(lane), "Wick may stand on the stand's front tile") + for(var/attempt in 1 to 30) + var/turf/spot = worker.spot_for(mech) + TEST_ASSERT_NOTNULL(spot, "Wick finds nowhere to stand to work on the mech") + TEST_ASSERT(spot != lane, "Wick picked the stand's front tile to work on the mech from") + + // A player in the outpost, outside the bay, so his AI runs. No client: not a customer. + var/mob/living/carbon/human/bystander = allocate(/mob/living/carbon/human/consistent, at(0, 0)) + present += bystander + SSmobs.clients_by_zlevel[bystander.z] += bystander + brain.can_idle = FALSE + brain.reset_ai_status() + TEST_ASSERT_EQUAL(brain.ai_status, AI_STATUS_ON, "Wick's AI does not run with a player near") + + // Free to work: he walks to the mech and works on it, and never leaves his room or stands in the lane + worker.next_work_at = 0 + var/worked = FALSE + for(var/beat in 1 to 60) + sleep(0.5 SECONDS) + check_place(wick, room, stand_tile, lane, "while working") + if(istype(worker.work, /datum/outpost_ambient_work/mech_service)) + worked = TRUE + break + TEST_ASSERT(worked, "Wick never started work on the hanging mech") + TEST_ASSERT(get_dist(wick, mech) == 1, "Wick is working on the mech from [get_dist(wick, mech)] tiles away") + + // A customer walks up: he stops, and takes no job however long they stay + var/turf/customer_tile + for(var/turf/tile as anything in room) + if(tile != get_turf(wick) && get_dist(tile, wick) <= 3 && !tile.is_blocked_turf(exclude_mobs = FALSE)) + customer_tile = tile + break + TEST_ASSERT_NOTNULL(customer_tile, "No free tile near Wick for a customer") + var/mob/living/carbon/human/customer = allocate(/mob/living/carbon/human/consistent, customer_tile) + customer.mock_client = new /datum/client_interface() + var/stopped = FALSE + for(var/beat in 1 to 12) + sleep(0.5 SECONDS) + if(isnull(worker.work)) + stopped = TRUE + break + TEST_ASSERT(stopped, "Wick kept working with a customer [get_dist(wick, customer)] tiles away") + for(var/beat in 1 to 16) + // Whatever rest he was owed, he still waits on the customer + worker.next_work_at = 0 + sleep(0.5 SECONDS) + check_place(wick, room, stand_tile, lane, "while serving") + TEST_ASSERT(isnull(worker.work) && isnull(worker.planned_work), "Wick took a job with a customer [get_dist(wick, customer)] tiles away") + TEST_ASSERT_EQUAL(brain.blackboard["rig_bay_wick_customer"], customer, "Wick is not serving the customer beside him") + + // They leave: he holds off a while before the next job + customer.mock_client = null + qdel(customer) + sleep(2 SECONDS) + TEST_ASSERT(isnull(worker.work), "Wick went back to work the moment the customer left") + TEST_ASSERT(worker.next_work_at >= world.time + 20 SECONDS, "Wick will be back at work [(worker.next_work_at - world.time) / 10] s after the customer left, not about 30 s") + check_place(wick, room, stand_tile, lane, "after serving") + +/datum/unit_test/voidcrew_rig_bay_wick/proc/check_place(mob/living/wick, list/room, turf/stand_tile, turf/lane, when) + var/turf/here = get_turf(wick) + TEST_ASSERT(room[here], "Wick left his room [when], to [here?.x],[here?.y]") + TEST_ASSERT(here != lane, "Wick stood on the stand's front tile [when]") + TEST_ASSERT(here != stand_tile, "Wick stood on the stand [when]") From f2ae640412ed9de7a8519488cc3838de4acfd19e Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 01:57:09 -0800 Subject: [PATCH 04/16] Sell mechs off the Rig Bay stands at Wick's Stands hang their mechs on chains until sold, hand the exact mech to the buyer's crew, and refill at the convoy (combat stand every second convoy). Wick's shop becomes The Rig Bay with exosuit arms, ammo and fittings tabs. --- tgstation.dme | 3 + voidcrew/_DEFINES/rig_bay.dm | 2 + .../outpost_checkpoint_snapshot.dm | 2 + voidcrew/modules/trade/outpost.dm | 3 + voidcrew/modules/trade/outpost_security.dm | 4 + .../modules/trade/rig_bay/rig_bay_clamp.dm | 324 ++++++++++++++++++ .../modules/trade/rig_bay/rig_bay_mecha.dm | 10 + .../modules/trade/rig_bay/rig_bay_shop.dm | 43 +++ .../modules/trade/rig_bay/rig_bay_skus.dm | 189 +++++++++- .../modules/trade/rig_bay/rig_bay_stand.dm | 54 ++- .../modules/trade/shop_catalog_suit_vendor.dm | 32 +- 11 files changed, 656 insertions(+), 10 deletions(-) create mode 100644 voidcrew/modules/trade/rig_bay/rig_bay_clamp.dm create mode 100644 voidcrew/modules/trade/rig_bay/rig_bay_mecha.dm create mode 100644 voidcrew/modules/trade/rig_bay/rig_bay_shop.dm diff --git a/tgstation.dme b/tgstation.dme index f969d380ffc..66010c1a92e 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7740,6 +7740,9 @@ #include "voidcrew\modules\trade\favor_uniques\outfitter.dm" #include "voidcrew\modules\trade\favor_uniques\pike.dm" #include "voidcrew\modules\trade\favor_uniques\vex.dm" +#include "voidcrew\modules\trade\rig_bay\rig_bay_clamp.dm" +#include "voidcrew\modules\trade\rig_bay\rig_bay_mecha.dm" +#include "voidcrew\modules\trade\rig_bay\rig_bay_shop.dm" #include "voidcrew\modules\trade\rig_bay\rig_bay_skus.dm" #include "voidcrew\modules\trade\rig_bay\rig_bay_stand.dm" #include "voidcrew\modules\transporter\_transporter.dm" diff --git a/voidcrew/_DEFINES/rig_bay.dm b/voidcrew/_DEFINES/rig_bay.dm index e0bbae65df4..f0efc0a3a10 100644 --- a/voidcrew/_DEFINES/rig_bay.dm +++ b/voidcrew/_DEFINES/rig_bay.dm @@ -14,6 +14,8 @@ #define RIG_BAY_HOLD_TIME (20 MINUTES) /// Pixels a chained mech hangs above its stand. Release lowers it back to 0. #define RIG_BAY_HANG_LIFT 4 +/// How often a due restock retries while something blocks the stand tile. +#define RIG_BAY_RETRY_DELAY (10 SECONDS) // Stand states (/obj/structure/rig_bay_stand var/stand_state) /// Nothing on the stand; waits for the next due convoy. diff --git a/voidcrew/modules/player_outposts/outpost_checkpoint_snapshot.dm b/voidcrew/modules/player_outposts/outpost_checkpoint_snapshot.dm index 84f9ef147fd..6d56e7549cb 100644 --- a/voidcrew/modules/player_outposts/outpost_checkpoint_snapshot.dm +++ b/voidcrew/modules/player_outposts/outpost_checkpoint_snapshot.dm @@ -46,6 +46,8 @@ GLOBAL_LIST_INIT(outpost_checkpoint_excluded, typecacheof(list( /obj/structure/trap, // Made of the spear and head it holds; clear_stock() takes those and it falls apart. /obj/structure/headpike, + // Trader outpost fixture: a saved one would deal shop mechs onto a hull. + /obj/structure/rig_bay_stand, // Outpost property that works a ship bay's side rooms, never a ship's fitting. /obj/structure/outpost_yard_droid, ))) diff --git a/voidcrew/modules/trade/outpost.dm b/voidcrew/modules/trade/outpost.dm index b74a47b3ce5..bedd25cb207 100644 --- a/voidcrew/modules/trade/outpost.dm +++ b/voidcrew/modules/trade/outpost.dm @@ -254,6 +254,9 @@ GLOBAL_LIST_EMPTY(trader_outposts) traders += npc if(isnull(npc.shop_type)) // the main shop's trader is the outpost's face trader = npc + // The Rig Bay's stands find Wick's shop and hang their first mechs (rig_bay/) + for(var/obj/structure/rig_bay_stand/stand in interior_turf) + stand.link_outpost(src) for(var/obj/machinery/machine in interior_turf) if(istype(machine, /obj/machinery/porta_turret/outpost)) var/obj/machinery/porta_turret/outpost/turret = machine diff --git a/voidcrew/modules/trade/outpost_security.dm b/voidcrew/modules/trade/outpost_security.dm index 0f6eeb321c7..c85a7bd56f6 100644 --- a/voidcrew/modules/trade/outpost_security.dm +++ b/voidcrew/modules/trade/outpost_security.dm @@ -498,6 +498,10 @@ GLOBAL_DATUM_INIT(outpost_pvp_enforcement, /datum/outpost_pvp_enforcement, new) /// Shoves, bare hands and stamina hits aren't vandalism; only a real damaging hit is /datum/element/outpost_property/proc/register_hit(obj/source, atom/attacker) + register_outpost_property_hit(source, attacker) + +/// A damaging hit on outpost property (or a Rig Bay mech) counts as aggression at the outpost it stands in. +/proc/register_outpost_property_hit(atom/source, atom/attacker) if(!isliving(attacker)) return var/obj/structure/overmap/trader_outpost/outpost = get_trader_outpost_for_turf(get_turf(source)) diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_clamp.dm b/voidcrew/modules/trade/rig_bay/rig_bay_clamp.dm new file mode 100644 index 00000000000..c030e995b9b --- /dev/null +++ b/voidcrew/modules/trade/rig_bay/rig_bay_clamp.dm @@ -0,0 +1,324 @@ +// The Rig Bay's two locks on a mech: the chains while it hangs for sale, and the buyer's hold after the sale. +// See data/rig-bay/spec.md section 5. Both save every flag they change and put it back when removed. + +/// Tools that open, pry, cut, weld, bolt or pulse a mech. A chained mech refuses them all. +#define RIG_BAY_BLOCKED_TOOLS list(TOOL_CROWBAR, TOOL_SCREWDRIVER, TOOL_WRENCH, TOOL_WELDER, TOOL_WIRECUTTER, TOOL_MULTITOOL) +/// What a chained mech gets on top of its own resistance flags. +#define RIG_BAY_PROTECTION_FLAGS (INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF) +/// Mech flags that let something take a mech over without climbing in. Off while it hangs on the chains. +#define RIG_BAY_REMOTE_FLAGS (AI_COMPATIBLE | MMI_COMPATIBLE | BEACON_CONTROLLABLE) + +/** + * # Rig Bay clamp + * + * On a display mech from the moment its stand spawns it until the sale. Nobody gets in, opens it, + * fits anything to it, damages it, EMPs it, moves it or teleports it. Hostile hits still count as + * aggression at the outpost, like hits on any outpost property. + */ +/datum/component/rig_bay_clamp + dupe_mode = COMPONENT_DUPE_UNIQUE + /// The stand the mech hangs from. + var/datum/weakref/stand_ref + /// The mech's resistance flags before the chains went on. + var/saved_resistance_flags = NONE + /// Which RIG_BAY_REMOTE_FLAGS the mech had before the chains went on. + var/saved_remote_flags = NONE + +/datum/component/rig_bay_clamp/Initialize(obj/structure/rig_bay_stand/stand) + if(!ismecha(parent)) + return COMPONENT_INCOMPATIBLE + stand_ref = WEAKREF(stand) + +/datum/component/rig_bay_clamp/RegisterWithParent() + var/obj/vehicle/sealed/mecha/mech = parent + saved_resistance_flags = mech.resistance_flags + mech.resistance_flags |= RIG_BAY_PROTECTION_FLAGS + saved_remote_flags = mech.mecha_flags & RIG_BAY_REMOTE_FLAGS + mech.mecha_flags &= ~RIG_BAY_REMOTE_FLAGS + mech.AddElement(/datum/element/empprotection, EMP_PROTECT_ALL) + ADD_TRAIT(mech, TRAIT_RIG_BAY_CLAMPED, RIG_BAY_TRAIT) + + var/list/tool_signals = list() + for(var/tool in RIG_BAY_BLOCKED_TOOLS) + tool_signals += COMSIG_ATOM_TOOL_ACT(tool) + tool_signals += COMSIG_ATOM_SECONDARY_TOOL_ACT(tool) + RegisterSignals(mech, tool_signals, PROC_REF(block_tool)) + // Sent before the mech's own item_interaction and the item's interact_with_atom, in and out of + // combat mode: ID cards, MMIs, intellicards, equipment, ammo, cells, stock parts, weapons. + RegisterSignals(mech, list(COMSIG_ATOM_ITEM_INTERACTION, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY), PROC_REF(block_item)) + RegisterSignal(mech, COMSIG_MECHA_TRY_ENTER, PROC_REF(block_entry)) + RegisterSignal(mech, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(block_move)) + RegisterSignal(mech, COMSIG_MOVABLE_TELEPORTING, PROC_REF(block_teleport)) + RegisterSignal(mech, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) + RegisterSignal(mech, COMSIG_PROJECTILE_PREHIT, PROC_REF(on_projectile_hit)) + RegisterSignal(mech, COMSIG_ATOM_PREHITBY, PROC_REF(on_thrown_hit)) + RegisterSignal(mech, COMSIG_ATOM_HULK_ATTACK, PROC_REF(on_hulk_attack)) + RegisterSignal(mech, COMSIG_ATOM_ATTACK_MECH, PROC_REF(on_mech_attack)) + +/datum/component/rig_bay_clamp/UnregisterFromParent() + var/obj/vehicle/sealed/mecha/mech = parent + var/list/tool_signals = list() + for(var/tool in RIG_BAY_BLOCKED_TOOLS) + tool_signals += COMSIG_ATOM_TOOL_ACT(tool) + tool_signals += COMSIG_ATOM_SECONDARY_TOOL_ACT(tool) + UnregisterSignal(mech, tool_signals + list( + COMSIG_ATOM_ITEM_INTERACTION, + COMSIG_ATOM_ITEM_INTERACTION_SECONDARY, + COMSIG_MECHA_TRY_ENTER, + COMSIG_MOVABLE_PRE_MOVE, + COMSIG_MOVABLE_TELEPORTING, + COMSIG_ATOM_EXAMINE, + COMSIG_PROJECTILE_PREHIT, + COMSIG_ATOM_PREHITBY, + COMSIG_ATOM_HULK_ATTACK, + COMSIG_ATOM_ATTACK_MECH, + )) + REMOVE_TRAIT(mech, TRAIT_RIG_BAY_CLAMPED, RIG_BAY_TRAIT) + mech.RemoveElement(/datum/element/empprotection, EMP_PROTECT_ALL) + mech.mecha_flags |= saved_remote_flags + mech.resistance_flags = saved_resistance_flags + +/datum/component/rig_bay_clamp/proc/block_tool(obj/vehicle/sealed/mecha/source, mob/living/user, obj/item/tool) + SIGNAL_HANDLER + source.balloon_alert(user, "chained up!") + return ITEM_INTERACT_BLOCKING + +/datum/component/rig_bay_clamp/proc/block_item(obj/vehicle/sealed/mecha/source, mob/living/user, obj/item/tool, list/modifiers) + SIGNAL_HANDLER + source.balloon_alert(user, "chained up!") + return ITEM_INTERACT_BLOCKING + +/datum/component/rig_bay_clamp/proc/block_entry(obj/vehicle/sealed/mecha/source, mob/entering) + SIGNAL_HANDLER + source.balloon_alert(entering, "chained up!") + return COMPONENT_MECHA_BLOCK_ENTER + +/datum/component/rig_bay_clamp/proc/block_move(atom/movable/source, atom/new_loc) + SIGNAL_HANDLER + return COMPONENT_MOVABLE_BLOCK_PRE_MOVE + +/datum/component/rig_bay_clamp/proc/block_teleport(datum/source, atom/destination, channel) + SIGNAL_HANDLER + return TRUE + +/datum/component/rig_bay_clamp/proc/on_examine(atom/source, mob/user, list/examine_list) + SIGNAL_HANDLER + examine_list += span_notice("It hangs from the gantry chains.") + +/datum/component/rig_bay_clamp/proc/on_projectile_hit(atom/source, obj/projectile/hit_projectile) + SIGNAL_HANDLER + if(!hit_projectile.is_hostile_projectile() || hit_projectile.damage_type == STAMINA) + return + register_outpost_property_hit(source, hit_projectile.firer) + +/datum/component/rig_bay_clamp/proc/on_thrown_hit(atom/source, atom/movable/hit_atom, datum/thrownthing/throwingdatum) + SIGNAL_HANDLER + if(!isitem(hit_atom)) + return + var/obj/item/hit_item = hit_atom + if(!hit_item.throwforce || hit_item.damtype == STAMINA) + return + register_outpost_property_hit(source, throwingdatum?.get_thrower()) + +/datum/component/rig_bay_clamp/proc/on_hulk_attack(atom/source, mob/attacker) + SIGNAL_HANDLER + register_outpost_property_hit(source, attacker) + +/datum/component/rig_bay_clamp/proc/on_mech_attack(atom/source, obj/vehicle/sealed/mecha/mecha_attacker, mob/living/pilot) + SIGNAL_HANDLER + register_outpost_property_hit(source, pilot) + +/** + * # Rig Bay hold + * + * On a sold mech while it is still on outpost ground, for RIG_BAY_HOLD_TIME at most. Only the buyer's + * crew can climb in or load a brain or AI into it. While it is empty, unarmed and past the checkpoint + * it can't be damaged, so nobody wrecks it before the crew gets it out; that drops as soon as someone + * climbs in, a weapon is fitted, or it leaves the concourse. Hostile hits count as aggression while it + * is on protected outpost ground. The hold ends when the mech leaves outpost ground or the time runs out. + */ +/datum/component/rig_bay_hold + dupe_mode = COMPONENT_DUPE_UNIQUE + /// The buyer's crew. Null means nobody is locked out. + var/datum/weakref/crew_ref + /// TRUE once a weapon has been fitted. The mech never goes back to being protected. + var/armed = FALSE + /// TRUE while RIG_BAY_PROTECTION_FLAGS are on the mech. + var/protecting = FALSE + /// Which RIG_BAY_PROTECTION_FLAGS the mech had on its own before the hold protected it. + var/saved_protection_flags = NONE + +/datum/component/rig_bay_hold/Initialize(datum/voidcrew_crew_record/crew) + if(!ismecha(parent)) + return COMPONENT_INCOMPATIBLE + crew_ref = crew ? WEAKREF(crew) : null + var/obj/vehicle/sealed/mecha/mech = parent + for(var/obj/item/mecha_parts/mecha_equipment/equipment as anything in mech.flat_equipment) + if(istype(equipment, /obj/item/mecha_parts/mecha_equipment/weapon)) + armed = TRUE + break + addtimer(CALLBACK(src, PROC_REF(expire)), RIG_BAY_HOLD_TIME, TIMER_DELETE_ME) + +/datum/component/rig_bay_hold/RegisterWithParent() + var/obj/vehicle/sealed/mecha/mech = parent + RegisterSignal(mech, COMSIG_MECHA_TRY_ENTER, PROC_REF(on_try_enter)) + RegisterSignals(mech, list(COMSIG_ATOM_ITEM_INTERACTION, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY), PROC_REF(on_item_interaction)) + RegisterSignal(mech, COMSIG_MOVABLE_PRE_MOVE, PROC_REF(on_pre_move)) + RegisterSignal(mech, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved)) + // Equipment is forceMoved into the mech when it attaches, whatever route fitted it. + RegisterSignal(mech, COMSIG_ATOM_ENTERED, PROC_REF(on_entered)) + RegisterSignal(mech, COMSIG_ATOM_EXITED, PROC_REF(on_exited)) + RegisterSignal(mech, COMSIG_PROJECTILE_PREHIT, PROC_REF(on_projectile_hit)) + RegisterSignal(mech, COMSIG_ATOM_AFTER_ATTACKEDBY, PROC_REF(on_melee_attack)) + RegisterSignal(mech, COMSIG_ATOM_PREHITBY, PROC_REF(on_thrown_hit)) + RegisterSignal(mech, COMSIG_ATOM_HULK_ATTACK, PROC_REF(on_hulk_attack)) + RegisterSignal(mech, COMSIG_ATOM_ATTACK_MECH, PROC_REF(on_mech_attack)) + update_protection() + +/datum/component/rig_bay_hold/UnregisterFromParent() + set_protected(FALSE) + UnregisterSignal(parent, list( + COMSIG_MECHA_TRY_ENTER, + COMSIG_ATOM_ITEM_INTERACTION, + COMSIG_ATOM_ITEM_INTERACTION_SECONDARY, + COMSIG_MOVABLE_PRE_MOVE, + COMSIG_MOVABLE_MOVED, + COMSIG_ATOM_ENTERED, + COMSIG_ATOM_EXITED, + COMSIG_PROJECTILE_PREHIT, + COMSIG_ATOM_AFTER_ATTACKEDBY, + COMSIG_ATOM_PREHITBY, + COMSIG_ATOM_HULK_ATTACK, + COMSIG_ATOM_ATTACK_MECH, + )) + +/// The time ran out: the mech is anyone's. +/datum/component/rig_bay_hold/proc/expire() + qdel(src) + +/// Whether the mech is anywhere a trader outpost watches: its concourse or one of its hangars. +/datum/component/rig_bay_hold/proc/on_outpost_ground() + return !isnull(get_trader_outpost_for_turf(get_turf(parent))) + +/// Whether the mech is past the checkpoint, where a fresh sale sits until the crew drives it out. +/datum/component/rig_bay_hold/proc/on_secured_ground() + var/turf/place = get_turf(parent) + var/obj/structure/overmap/trader_outpost/outpost = get_trader_outpost_for_turf(place) + return !!outpost?.is_secured_turf(place) + +/// Whether `user` is on the buyer's crew. Always TRUE when the sale had no crew. +/datum/component/rig_bay_hold/proc/is_buyer_crew(mob/user) + if(!crew_ref) + return TRUE + var/datum/voidcrew_crew_record/crew = crew_ref.resolve() + if(QDELETED(crew)) + return TRUE + return user?.mind?.get_reputation_crew() == crew + +/datum/component/rig_bay_hold/proc/update_protection() + var/obj/vehicle/sealed/mecha/mech = parent + set_protected(!armed && !LAZYLEN(mech.occupants) && on_secured_ground()) + +/datum/component/rig_bay_hold/proc/set_protected(protect) + if(protect == protecting) + return + var/obj/vehicle/sealed/mecha/mech = parent + protecting = protect + if(protect) + saved_protection_flags = mech.resistance_flags & RIG_BAY_PROTECTION_FLAGS + mech.resistance_flags |= RIG_BAY_PROTECTION_FLAGS + else + mech.resistance_flags = (mech.resistance_flags & ~RIG_BAY_PROTECTION_FLAGS) | saved_protection_flags + +/datum/component/rig_bay_hold/proc/on_try_enter(obj/vehicle/sealed/mecha/source, mob/entering) + SIGNAL_HANDLER + if(!on_outpost_ground()) + qdel(src) + return NONE + // Still coming down off the chains + if(source.anchored) + source.balloon_alert(entering, "still lowering!") + return COMPONENT_MECHA_BLOCK_ENTER + if(is_buyer_crew(entering)) + return NONE + source.balloon_alert(entering, "sold!") + return COMPONENT_MECHA_BLOCK_ENTER + +/// A brain in a jar or an AI card is another way in. +/datum/component/rig_bay_hold/proc/on_item_interaction(obj/vehicle/sealed/mecha/source, mob/living/user, obj/item/tool, list/modifiers) + SIGNAL_HANDLER + if(!istype(tool, /obj/item/mmi) && !istype(tool, /obj/item/aicard)) + return NONE + if(!on_outpost_ground()) + qdel(src) + return NONE + if(!source.anchored && is_buyer_crew(user)) + return NONE + source.balloon_alert(user, source.anchored ? "still lowering!" : "sold!") + return ITEM_INTERACT_BLOCKING + +/// Nothing moves the mech while the stand is still lowering it. +/datum/component/rig_bay_hold/proc/on_pre_move(atom/movable/source, atom/new_loc) + SIGNAL_HANDLER + if(source.anchored) + return COMPONENT_MOVABLE_BLOCK_PRE_MOVE + return NONE + +/datum/component/rig_bay_hold/proc/on_moved(atom/movable/source, atom/old_loc, dir, forced, list/old_locs) + SIGNAL_HANDLER + if(!on_outpost_ground()) + qdel(src) + return + update_protection() + +/datum/component/rig_bay_hold/proc/on_entered(atom/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs) + SIGNAL_HANDLER + if(istype(arrived, /obj/item/mecha_parts/mecha_equipment/weapon)) + armed = TRUE + else if(!ismob(arrived)) + return + update_protection() + +/datum/component/rig_bay_hold/proc/on_exited(atom/source, atom/movable/gone, direction) + SIGNAL_HANDLER + if(ismob(gone)) + update_protection() + +/// Hostile hits on a sold mech count as aggression while it is on protected outpost ground. +/datum/component/rig_bay_hold/proc/register_hit(atom/source, atom/attacker) + if(!is_trader_outpost_protected(source)) + return + register_outpost_property_hit(source, attacker) + +/datum/component/rig_bay_hold/proc/on_projectile_hit(atom/source, obj/projectile/hit_projectile) + SIGNAL_HANDLER + if(!hit_projectile.is_hostile_projectile() || hit_projectile.damage_type == STAMINA) + return + register_hit(source, hit_projectile.firer) + +/datum/component/rig_bay_hold/proc/on_melee_attack(atom/source, obj/item/weapon, mob/attacker, list/modifiers, list/attack_modifiers) + SIGNAL_HANDLER + if(!weapon.force || weapon.damtype == STAMINA) + return + register_hit(source, attacker) + +/datum/component/rig_bay_hold/proc/on_thrown_hit(atom/source, atom/movable/hit_atom, datum/thrownthing/throwingdatum) + SIGNAL_HANDLER + if(!isitem(hit_atom)) + return + var/obj/item/hit_item = hit_atom + if(!hit_item.throwforce || hit_item.damtype == STAMINA) + return + register_hit(source, throwingdatum?.get_thrower()) + +/datum/component/rig_bay_hold/proc/on_hulk_attack(atom/source, mob/attacker) + SIGNAL_HANDLER + register_hit(source, attacker) + +/datum/component/rig_bay_hold/proc/on_mech_attack(atom/source, obj/vehicle/sealed/mecha/mecha_attacker, mob/living/pilot) + SIGNAL_HANDLER + register_hit(source, pilot) + +#undef RIG_BAY_BLOCKED_TOOLS +#undef RIG_BAY_PROTECTION_FLAGS +#undef RIG_BAY_REMOTE_FLAGS diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_mecha.dm b/voidcrew/modules/trade/rig_bay/rig_bay_mecha.dm new file mode 100644 index 00000000000..9a912be9807 --- /dev/null +++ b/voidcrew/modules/trade/rig_bay/rig_bay_mecha.dm @@ -0,0 +1,10 @@ +/** + * Climbing into a mech has no signal in tg. The Rig Bay's chains and the buyer's hold + * (rig_bay_clamp.dm) refuse entry through COMSIG_MECHA_TRY_ENTER. + * + * This file is included after code/, so this override chains onto the tg one. + */ +/obj/vehicle/sealed/mecha/mob_try_enter(mob/M) + if(SEND_SIGNAL(src, COMSIG_MECHA_TRY_ENTER, M) & COMPONENT_MECHA_BLOCK_ENTER) + return FALSE + return ..() diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_shop.dm b/voidcrew/modules/trade/rig_bay/rig_bay_shop.dm new file mode 100644 index 00000000000..a77ab28699d --- /dev/null +++ b/voidcrew/modules/trade/rig_bay/rig_bay_shop.dm @@ -0,0 +1,43 @@ +// The Rig Bay side of Wick's shop: the stand pools, the stands it lists, and their convoy refills. +// The shelf lists (categories, sku_types) and Wick's lines stay in shop_catalog_suit_vendor.dm. + +/datum/outpost_shop/vendor/suit_fitter + // The stand pools deal nothing on their own (slots = 0); each stand deals its own pick. + rotations = list( + /datum/shop_rotation/rig_bay/work, + /datum/shop_rotation/rig_bay/combat, + /datum/shop_rotation/rig_bay_arms, + ) + /// The Rig Bay stands linked to this shop. Each lists its mech as one line under Exosuits. + var/list/obj/structure/rig_bay_stand/stands = list() + +/datum/outpost_shop/vendor/suit_fitter/Destroy() + for(var/obj/structure/rig_bay_stand/stand as anything in stands) + UnregisterSignal(stand, COMSIG_QDELETING) + if(stand.shop == src) + stand.stop_retry() + stand.shop = null + stands.Cut() + return ..() + +/// Registers a stand for convoy refills. Called from the stand's link_outpost(). +/datum/outpost_shop/vendor/suit_fitter/proc/attach_stand(obj/structure/rig_bay_stand/stand) + if(QDELETED(stand) || (stand in stands)) + return + stands += stand + RegisterSignal(stand, COMSIG_QDELETING, PROC_REF(on_stand_deleted)) + +/datum/outpost_shop/vendor/suit_fitter/proc/on_stand_deleted(obj/structure/rig_bay_stand/stand) + SIGNAL_HANDLER + stands -= stand + +/datum/outpost_shop/vendor/suit_fitter/convoy_restock() + . = ..() + restock_stands() + +/// Every stand hears the convoy. Empty ones whose pool is due get a new mech, beamed in. +/datum/outpost_shop/vendor/suit_fitter/proc/restock_stands() + for(var/obj/structure/rig_bay_stand/stand as anything in stands) + if(QDELETED(stand)) + continue + stand.convoy_arrived() diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_skus.dm b/voidcrew/modules/trade/rig_bay/rig_bay_skus.dm index c484877db70..ff36fa7c4a6 100644 --- a/voidcrew/modules/trade/rig_bay/rig_bay_skus.dm +++ b/voidcrew/modules/trade/rig_bay/rig_bay_skus.dm @@ -30,10 +30,19 @@ /datum/shop_sku/rig_bay_mech/durand = 1, ) -/// Yellow exosuit arms: four of the pool on the shelf at once, one each. P1 fills the pool (spec section 4). +/// Yellow exosuit arms: four of the pool on the shelf at once, one each. /datum/shop_rotation/rig_bay_arms slots = 4 - pool = list() + pool = list( + /datum/shop_sku/rig_bay_gear/arm/lmg = 3, + /datum/shop_sku/rig_bay_gear/arm/laser = 3, + /datum/shop_sku/rig_bay_gear/arm/laser_heavy = 2, + /datum/shop_sku/rig_bay_gear/arm/scattershot = 2, + /datum/shop_sku/rig_bay_gear/arm/carbine = 2, + /datum/shop_sku/rig_bay_gear/arm/ion = 1, + /datum/shop_sku/rig_bay_gear/arm/tesla = 1, + /datum/shop_sku/rig_bay_gear/arm/flashbang_launcher = 1, + ) // ===== WHOLE MECHS ===== @@ -61,6 +70,42 @@ stand = null return ..() +/// TRUE while this line's mech hangs on its stand, ready to hand over. +/datum/shop_sku/rig_bay_mech/proc/mech_ready() + return !QDELETED(stand) && stand.sku == src && stand.stand_state == RIG_BAY_STAND_CLAMPED && !QDELETED(stand.held_mech) + +/datum/shop_sku/rig_bay_mech/get_denial_reason(mob/living/user) + if(!mech_ready()) + return "Out of stock." + return ..() + +/datum/shop_sku/rig_bay_mech/try_purchase(mob/living/user, mob/living/basic/outpost_trader/vendor) + if(!mech_ready()) + return FALSE + return ..() + +/// No crate over the counter: the stand lowers this exact mech for the buyer's crew. +/datum/shop_sku/rig_bay_mech/dispense(mob/living/user, mob/living/basic/outpost_trader/vendor) + var/obj/vehicle/sealed/mecha/mech = stand?.held_mech + if(mech_ready() && stand.release_mech(user)) + return mech + refund(user) + return null + +/// The mech vanished between the charge and the hand-over: give back exactly what was taken. +/datum/shop_sku/rig_bay_mech/proc/refund(mob/living/user) + var/obj/structure/overmap/ship/crew_ship = get_crew_ship(user) + var/credit_price = get_credit_price(user, crew_ship) + if(credit_price > 0) + var/datum/bank_account/account = get_account(user, crew_ship) + account?.adjust_money(credit_price, "Trader Outpost: [name] refund") + if(price_vouchers > 0) + var/obj/item/stack/trade_voucher/vouchers = new(user.drop_location(), price_vouchers) + user.put_in_hands(vouchers) + if(crew_limit > 0) + crew_ship?.get_crew_record()?.release_purchase(type) + to_chat(user, span_warning("Something's wrong with that one. Your money's back.")) + /datum/shop_sku/rig_bay_mech/ripley item_path = /obj/vehicle/sealed/mecha/ripley price_credits = 4000 @@ -107,6 +152,144 @@ // ===== ARMS, AMMO AND FITTINGS ===== -/// Base for the ordinary exosuit gear lines (Exosuit Arms, Ammo and Fittings tabs). P1 adds the subtypes (spec section 4). +/// Base for the ordinary exosuit gear lines (Exosuit Arms, Ammo and Fittings tabs). /datum/shop_sku/rig_bay_gear category = "Exosuit Fittings" + stock_min = 1 + stock_max = 3 + +// Exosuit Arms. The green arms and the missile rack sit on the core shelf; the yellow arms +// come through /datum/shop_rotation/rig_bay_arms, one of each. + +/datum/shop_sku/rig_bay_gear/arm + category = "Exosuit Arms" + stock_min = 1 + stock_max = 2 + +/datum/shop_sku/rig_bay_gear/arm/kinetic + item_path = /obj/item/mecha_parts/mecha_equipment/weapon/energy/mecha_kineticgun + price_credits = 600 + +/datum/shop_sku/rig_bay_gear/arm/plasma_cutter + item_path = /obj/item/mecha_parts/mecha_equipment/weapon/energy/plasma + price_credits = 500 + +/datum/shop_sku/rig_bay_gear/arm/disabler + item_path = /obj/item/mecha_parts/mecha_equipment/weapon/energy/disabler + price_credits = 900 + +/datum/shop_sku/rig_bay_gear/arm/missile_rack + item_path = /obj/item/mecha_parts/mecha_equipment/weapon/ballistic/missile_rack/breaching + zone_permission = ZONE_PERMISSION_ADVANCED + price_credits = 4000 + price_vouchers = 2 + stock_min = 1 + stock_max = 1 + convoys_per_unit = 3 + +/datum/shop_sku/rig_bay_gear/arm/lmg + item_path = /obj/item/mecha_parts/mecha_equipment/weapon/ballistic/lmg + zone_permission = ZONE_PERMISSION_YELLOW + price_credits = 4000 + stock_max = 1 + +/datum/shop_sku/rig_bay_gear/arm/laser + item_path = /obj/item/mecha_parts/mecha_equipment/weapon/energy/laser + zone_permission = ZONE_PERMISSION_YELLOW + price_credits = 2500 + stock_max = 1 + +/datum/shop_sku/rig_bay_gear/arm/laser_heavy + item_path = /obj/item/mecha_parts/mecha_equipment/weapon/energy/laser/heavy + zone_permission = ZONE_PERMISSION_YELLOW + price_credits = 3000 + stock_max = 1 + +/datum/shop_sku/rig_bay_gear/arm/scattershot + item_path = /obj/item/mecha_parts/mecha_equipment/weapon/ballistic/scattershot + zone_permission = ZONE_PERMISSION_YELLOW + price_credits = 3000 + stock_max = 1 + +/datum/shop_sku/rig_bay_gear/arm/carbine + item_path = /obj/item/mecha_parts/mecha_equipment/weapon/ballistic/carbine + zone_permission = ZONE_PERMISSION_YELLOW + price_credits = 3500 + stock_max = 1 + +/datum/shop_sku/rig_bay_gear/arm/ion + item_path = /obj/item/mecha_parts/mecha_equipment/weapon/energy/ion + zone_permission = ZONE_PERMISSION_YELLOW + price_credits = 3500 + stock_max = 1 + +/datum/shop_sku/rig_bay_gear/arm/tesla + item_path = /obj/item/mecha_parts/mecha_equipment/weapon/energy/tesla + zone_permission = ZONE_PERMISSION_YELLOW + price_credits = 2500 + stock_max = 1 + +/datum/shop_sku/rig_bay_gear/arm/flashbang_launcher + item_path = /obj/item/mecha_parts/mecha_equipment/weapon/ballistic/launcher/flashbang + zone_permission = ZONE_PERMISSION_YELLOW + price_credits = 3000 + stock_max = 1 + +// Exosuit Ammo + +/datum/shop_sku/rig_bay_gear/ammo + category = "Exosuit Ammo" + stock_min = 4 + stock_max = 6 + +/datum/shop_sku/rig_bay_gear/ammo/lmg + item_path = /obj/item/mecha_ammo/lmg + zone_permission = ZONE_PERMISSION_YELLOW + price_credits = 150 + +/datum/shop_sku/rig_bay_gear/ammo/scattershot + item_path = /obj/item/mecha_ammo/scattershot + zone_permission = ZONE_PERMISSION_YELLOW + price_credits = 150 + +/datum/shop_sku/rig_bay_gear/ammo/incendiary + item_path = /obj/item/mecha_ammo/incendiary + zone_permission = ZONE_PERMISSION_YELLOW + price_credits = 250 + +/datum/shop_sku/rig_bay_gear/ammo/flashbang + item_path = /obj/item/mecha_ammo/flashbang + zone_permission = ZONE_PERMISSION_YELLOW + price_credits = 300 + +/datum/shop_sku/rig_bay_gear/ammo/missiles + item_path = /obj/item/mecha_ammo/missiles_pep + zone_permission = ZONE_PERMISSION_ADVANCED + price_credits = 800 + +// Exosuit Fittings + +/datum/shop_sku/rig_bay_gear/projectile_shielding + item_path = /obj/item/mecha_parts/mecha_equipment/armor/antiproj_armor_booster + zone_permission = ZONE_PERMISSION_YELLOW + price_credits = 2500 + +/datum/shop_sku/rig_bay_gear/impact_plates + item_path = /obj/item/mecha_parts/mecha_equipment/armor/anticcw_armor_booster + price_credits = 900 + +/datum/shop_sku/rig_bay_gear/ablative_insulation + item_path = /obj/item/mecha_parts/mecha_equipment/armor/antiemp_armor_booster + price_credits = 1200 + +/datum/shop_sku/rig_bay_gear/drill + item_path = /obj/item/mecha_parts/mecha_equipment/drill + price_credits = 300 + +/datum/shop_sku/rig_bay_gear/hydraulic_clamp + item_path = /obj/item/mecha_parts/mecha_equipment/hydraulic_clamp + price_credits = 300 + +/datum/shop_sku/rig_bay_gear/repair_droid + item_path = /obj/item/mecha_parts/mecha_equipment/repair_droid + price_credits = 1200 diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_stand.dm b/voidcrew/modules/trade/rig_bay/rig_bay_stand.dm index 514e7db3718..afcead8b172 100644 --- a/voidcrew/modules/trade/rig_bay/rig_bay_stand.dm +++ b/voidcrew/modules/trade/rig_bay/rig_bay_stand.dm @@ -35,6 +35,8 @@ var/convoys_waited = 0 /// One of the RIG_BAY_STAND_* states. var/stand_state = RIG_BAY_STAND_EMPTY + /// While a due restock waits for the stand tile to clear: retries the deal every RIG_BAY_RETRY_DELAY. + var/retry_timer /obj/structure/rig_bay_stand/Destroy() if(held_mech) @@ -45,6 +47,7 @@ if(unsold) qdel(mech) drop_sku() + stop_retry() shop = null outpost = null return ..() @@ -59,7 +62,7 @@ return outpost = new_outpost shop = outpost.get_shop(/datum/outpost_shop/vendor/suit_fitter) - // P1: shop.attach_stand(src) registers the stand for convoy restocks. + shop?.attach_stand(src) if(SSatoms.initialized == INITIALIZATION_INNEW_REGULAR) deal_mech(animate = FALSE) else @@ -118,7 +121,7 @@ mech.set_anchored(TRUE) RegisterSignal(mech, COMSIG_QDELETING, PROC_REF(on_mech_deleted)) RegisterSignal(mech, COMSIG_MOVABLE_MOVED, PROC_REF(on_mech_moved)) - // P1: mech.AddComponent(/datum/component/rig_bay_clamp, src) + mech.AddComponent(/datum/component/rig_bay_clamp, src) stand_state = RIG_BAY_STAND_ARRIVING if(animate) mech.alpha = 0 @@ -148,7 +151,10 @@ stand_state = RIG_BAY_STAND_RELEASING if(sku) sku.stock = 0 - // P1: swap /datum/component/rig_bay_clamp for /datum/component/rig_bay_hold (buyer's crew) here. + // Chains off, the buyer's hold on, then the lowering. The hold keeps the mech still until it is down. + var/datum/voidcrew_crew_record/crew = get_crew_ship(buyer)?.get_crew_record() || buyer?.mind?.get_reputation_crew() + qdel(held_mech.GetComponent(/datum/component/rig_bay_clamp)) + held_mech.AddComponent(/datum/component/rig_bay_hold, crew) to_chat(buyer, span_notice("The chains on bay [bay_number] drop.")) animate_release(held_mech, CALLBACK(src, PROC_REF(finish_release), held_mech)) return TRUE @@ -182,6 +188,48 @@ mech.alpha = 255 mech.pixel_z = RIG_BAY_HANG_LIFT +/** + * The convoy came through. An empty stand counts it toward its pool's convoys_per_refill and deals a + * new mech once the count is reached. If the tile is blocked it retries every RIG_BAY_RETRY_DELAY + * until the next convoy. A stand with a mech on it (hanging or sold) waits. + */ +/obj/structure/rig_bay_stand/proc/convoy_arrived() + stop_retry() + if(stand_state != RIG_BAY_STAND_EMPTY) + convoys_waited = 0 + return + if(++convoys_waited < get_convoys_per_refill()) + return + if(deal_mech(animate = TRUE)) + convoys_waited = 0 + return + if(stand_state == RIG_BAY_STAND_EMPTY && shop) + retry_timer = addtimer(CALLBACK(src, PROC_REF(retry_deal)), RIG_BAY_RETRY_DELAY, TIMER_STOPPABLE | TIMER_LOOP | TIMER_DELETE_ME) + +/// A due restock found the tile blocked. Tries again until it deals or the next convoy takes over. +/obj/structure/rig_bay_stand/proc/retry_deal() + if(stand_state != RIG_BAY_STAND_EMPTY) + stop_retry() + return + if(deal_mech(animate = TRUE)) + convoys_waited = 0 + stop_retry() + +/obj/structure/rig_bay_stand/proc/stop_retry() + if(retry_timer) + deltimer(retry_timer) + retry_timer = null + +/// Convoys an empty stand waits before its next mech, from its pool. +/obj/structure/rig_bay_stand/proc/get_convoys_per_refill() + var/datum/shop_rotation/rig_bay/rotation = shop ? (locate(pool) in shop.live_rotations) : null + if(rotation) + return max(1, rotation.convoys_per_refill) + if(!ispath(pool, /datum/shop_rotation/rig_bay)) + return 1 + var/datum/shop_rotation/rig_bay/rotation_type = pool + return max(1, initial(rotation_type.convoys_per_refill)) + /// TRUE if a new mech can be placed: no mob, mech or other dense thing on the stand tile. /obj/structure/rig_bay_stand/proc/tile_clear() var/turf/tile = get_turf(src) diff --git a/voidcrew/modules/trade/shop_catalog_suit_vendor.dm b/voidcrew/modules/trade/shop_catalog_suit_vendor.dm index 5563133983a..1fe3d89652c 100644 --- a/voidcrew/modules/trade/shop_catalog_suit_vendor.dm +++ b/voidcrew/modules/trade/shop_catalog_suit_vendor.dm @@ -49,8 +49,8 @@ /// Wick, who will not sell you a suit until he has measured you for it /datum/outpost_shop/vendor/suit_fitter - outpost_name = "\improper The Fitting Bay" - outpost_desc = "The depot's modsuit shop, and the only bench in the lanes that'll fit one properly." + outpost_name = "\improper The Rig Bay" + outpost_desc = "The depot's modsuit counter, with exosuits hanging from the gantries out back." trader_name = "Wick" trader_outfit = /datum/outfit/fitting_bay_wick trader_gender = MALE @@ -67,8 +67,28 @@ "Medical Modules", "Visors & Sensors", "Environment Gear", + "Exosuits", + "Exosuit Arms", + "Exosuit Ammo", + "Exosuit Fittings", ) sku_types = list( + // The Rig Bay: whole mechs come off the stands (rig_bay/), the rest is shelf stock + /datum/shop_sku/rig_bay_gear/arm/kinetic, + /datum/shop_sku/rig_bay_gear/arm/plasma_cutter, + /datum/shop_sku/rig_bay_gear/arm/disabler, + /datum/shop_sku/rig_bay_gear/arm/missile_rack, + /datum/shop_sku/rig_bay_gear/ammo/lmg, + /datum/shop_sku/rig_bay_gear/ammo/scattershot, + /datum/shop_sku/rig_bay_gear/ammo/incendiary, + /datum/shop_sku/rig_bay_gear/ammo/flashbang, + /datum/shop_sku/rig_bay_gear/ammo/missiles, + /datum/shop_sku/rig_bay_gear/projectile_shielding, + /datum/shop_sku/rig_bay_gear/impact_plates, + /datum/shop_sku/rig_bay_gear/ablative_insulation, + /datum/shop_sku/rig_bay_gear/drill, + /datum/shop_sku/rig_bay_gear/hydraulic_clamp, + /datum/shop_sku/rig_bay_gear/repair_droid, // MOD Chassis: two finished suits plus the whole build route /datum/shop_sku/fitter/suit_civilian, /datum/shop_sku/fitter/suit_engineering, @@ -212,6 +232,7 @@ "Good fit. Bring it back when the cell's flat, not after.", "Signed for. Bench is right there if you want the modules seated properly.", "Yours. Service it every few runs and it'll outlast the ship.", + "Signed for. Drive it out slow, the concourse has corners.", ), TRADER_LINE_REFUSAL = list( "You're flagged. The depot won't clear the sale and I don't argue with the depot.", @@ -224,11 +245,14 @@ "Bench install is free. Doing it yourself in a corridor is how people lose fingers.", "The civilian chassis is not spaceworthy. It says so on the plate. People still ask.", "Bring me a wrecked control unit and I'll take it, modules and all.", + "Hold still. Not you. The Ripley.", + "Somebody's been feeding this one plasma cutter grit.", + "Don't lean on the chains.", ), TRADER_LINE_RESTOCK = list( - "Convoy's in. New chassis on the rack and the plating rotation changed.", + "Convoy's in. New frames on the stands.", "Resupply landed. Cores came through intact this time, which isn't guaranteed.", - "Fresh stock. Look now, before the mining crews strip the rack.", + "Fresh rigs. Mind the chains.", ), ) From ef0cb39aabe045b4e46d58d3d0a9040082998a65 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 01:57:14 -0800 Subject: [PATCH 05/16] Rate whole exosuits and add exosuit and mech price bands Whole mechs get their own exosuits category, rated as their chassis, and the gear check now reads mech types and their shop routes. --- tools/ci/check_gear_ratings.py | 4 +- tools/gear_catalog/categories.py | 4 +- tools/gear_catalog/policy.json | 28 ++++++++ tools/gear_catalog/ratings/exosuits.json | 88 ++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 tools/gear_catalog/ratings/exosuits.json diff --git a/tools/ci/check_gear_ratings.py b/tools/ci/check_gear_ratings.py index d4b10295e1d..204b31f6830 100644 --- a/tools/ci/check_gear_ratings.py +++ b/tools/ci/check_gear_ratings.py @@ -227,7 +227,9 @@ def find_problems(static, docs, policy, exceptions, exceptions_text=""): voucher_value = dm.eval("VOUCHER_CREDIT_VALUE") if not isinstance(voucher_value, (int, float)): raise ValueError(f"VOUCHER_CREDIT_VALUE should be a plain number, not {voucher_value!r}") - paths = sorted({*dm.compiled_types("/obj/item"), *(path for path in static.routed_types() if path.startswith("/obj/item"))}) + # Whole mechs are vehicles, not items, but the Rig Bay sells them like gear. + roots = ("/obj/item", "/obj/vehicle/sealed/mecha") + paths = sorted({*(path for root in roots for path in dm.compiled_types(root)), *(path for path in static.routed_types() if path.startswith(roots))}) ordinary = [row["path"] for row in policy.get("ordinary_ingredients") or []] steps, covers = reachable(static, paths, checked, ordinary) entries = {} diff --git a/tools/gear_catalog/categories.py b/tools/gear_catalog/categories.py index 7e804a834c1..00e88fbb379 100644 --- a/tools/gear_catalog/categories.py +++ b/tools/gear_catalog/categories.py @@ -22,6 +22,8 @@ ('/obj/item/ammo_casing', 'ammo', '/obj/item/ammo_casing'), ('/obj/item/mecha_ammo', 'mech', '/obj/item/mecha_ammo'), ('/obj/item/mecha_parts', 'mech', '/obj/item/mecha_parts'), + # Whole exosuits, sold off the Rig Bay stands. Not items, so the checker adds their paths itself. + ('/obj/vehicle/sealed/mecha', 'exosuits', '/obj/vehicle/sealed/mecha'), ('/obj/item/clothing/glasses', 'eyewear', '/obj/item/clothing/glasses'), ('/obj/item/clothing/head', 'headwear', '/obj/item/clothing/head'), ('/obj/item/clothing/suit', 'suits', '/obj/item/clothing/suit'), @@ -314,7 +316,7 @@ GEAR_CATEGORIES = frozenset({ 'guns', 'gun_parts', 'eyewear', 'melee', 'shields', 'explosives', 'headwear', 'suits', 'uniforms', 'gloves', 'shoes', 'masks', 'neckwear', 'accessories', 'earwear', 'modsuits', 'implants', 'cyberware', 'organs', 'ammo', 'tools', 'toolboxes', 'medical', - 'belts', 'backpacks', 'storage', 'mech', 'robotics', 'comms', 'ship_parts', 'relics', + 'belts', 'backpacks', 'storage', 'mech', 'exosuits', 'robotics', 'comms', 'ship_parts', 'relics', }) NOT_GEAR_CATEGORIES = frozenset({ 'food', 'botany', 'paper', 'decor', 'toys', 'trash', 'boards', 'machine_parts', 'stacks', 'circuits', 'slimes', 'genetics', 'cards', diff --git a/tools/gear_catalog/policy.json b/tools/gear_catalog/policy.json index 296b8a12537..db1f15b7d8e 100644 --- a/tools/gear_catalog/policy.json +++ b/tools/gear_catalog/policy.json @@ -112,6 +112,20 @@ 4000 ] }, + "exosuits": { + "green": [ + 3000, + 9000 + ], + "red": [ + 15000, + 35000 + ], + "yellow": [ + 8000, + 18000 + ] + }, "explosives": { "green": [ 100, @@ -162,6 +176,20 @@ 1200 ] }, + "mech": { + "green": [ + 100, + 3000 + ], + "red": [ + 500, + 12000 + ], + "yellow": [ + 100, + 7000 + ] + }, "melee": { "green": [ 200, diff --git a/tools/gear_catalog/ratings/exosuits.json b/tools/gear_catalog/ratings/exosuits.json new file mode 100644 index 00000000000..cf43405d4a3 --- /dev/null +++ b/tools/gear_catalog/ratings/exosuits.json @@ -0,0 +1,88 @@ +{ + "category": "exosuits", + "groups": [ + "combat exosuits", + "working and medical exosuits" + ], + "items": { + "/obj/vehicle/sealed/mecha/clarke": { + "confidence": "high", + "group": "working and medical exosuits", + "hash": "", + "name": "Clarke", + "rank": 1, + "reason": "Tracked mining mech with an ore box manager and sleeper, sold with a drill and mining scanner. Rated as its chassis.", + "tier": "green" + }, + "/obj/vehicle/sealed/mecha/durand": { + "confidence": "medium", + "group": "combat exosuits", + "hash": "", + "name": "Durand", + "rank": 3, + "reason": "Heavy combat mech with a frontal shield, sold bare. Rated as its chassis.", + "tier": "red" + }, + "/obj/vehicle/sealed/mecha/gygax": { + "confidence": "medium", + "group": "combat exosuits", + "hash": "", + "name": "Gygax", + "rank": 5, + "reason": "The entry combat mech, sold bare: 250 health, 20 bullet and 30 laser armor, with an overclock for speed. Rated as its chassis.", + "tier": "yellow" + }, + "/obj/vehicle/sealed/mecha/odysseus": { + "confidence": "high", + "group": "working and medical exosuits", + "hash": "", + "name": "Odysseus", + "rank": 4, + "reason": "Medical mech, sold with a mounted sleeper. Rated as its chassis.", + "tier": "green" + }, + "/obj/vehicle/sealed/mecha/ripley": { + "confidence": "high", + "group": "working and medical exosuits", + "hash": "", + "name": "APLU MK-I \"Ripley\"", + "rank": 3, + "reason": "The open-cabin cargo and mining mech, sold with a hydraulic clamp and a drill. Rated as its chassis.", + "tier": "green" + }, + "/obj/vehicle/sealed/mecha/ripley/mk2": { + "confidence": "high", + "group": "working and medical exosuits", + "hash": "", + "name": "APLU MK-II \"Ripley\"", + "rank": 2, + "reason": "The enclosed Ripley, sold with a hydraulic clamp and a drill. Rated as the MK-II conversion kit on a Ripley.", + "tier": "green" + } + }, + "method": "A whole mech rates as its chassis does in mech.json: how many vested men it takes to match its health and armor. Its tg default kit and the stand's working kit are tools, not weapons; arms are sold and rated separately.", + "unrated": { + "/obj/vehicle/sealed/mecha": "abstract", + "/obj/vehicle/sealed/mecha/gygax/dark": "no route", + "/obj/vehicle/sealed/mecha/honker": "no route", + "/obj/vehicle/sealed/mecha/honker/dark": "no route", + "/obj/vehicle/sealed/mecha/honker/dark/loaded": "no route", + "/obj/vehicle/sealed/mecha/justice": "no route", + "/obj/vehicle/sealed/mecha/justice/loaded": "no route", + "/obj/vehicle/sealed/mecha/marauder": "no route", + "/obj/vehicle/sealed/mecha/marauder/loaded": "no route", + "/obj/vehicle/sealed/mecha/marauder/mauler": "no route", + "/obj/vehicle/sealed/mecha/marauder/mauler/loaded": "no route", + "/obj/vehicle/sealed/mecha/marauder/seraph": "no route", + "/obj/vehicle/sealed/mecha/phazon": "no route", + "/obj/vehicle/sealed/mecha/reticence": "no route", + "/obj/vehicle/sealed/mecha/reticence/loaded": "no route", + "/obj/vehicle/sealed/mecha/ripley/cargo": "no route", + "/obj/vehicle/sealed/mecha/ripley/deathripley": "no route", + "/obj/vehicle/sealed/mecha/ripley/deathripley/real": "no route", + "/obj/vehicle/sealed/mecha/ripley/mining": "no route", + "/obj/vehicle/sealed/mecha/ripley/paddy": "no route", + "/obj/vehicle/sealed/mecha/ripley/paddy/preset": "no route", + "/obj/vehicle/sealed/mecha/savannah_ivanov": "no route" + } +} From 854bd8527593b9180b3dc4bf2242df5ef9f9ab0f Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 01:57:14 -0800 Subject: [PATCH 06/16] Test the Rig Bay stands, chains, hold and restock --- voidcrew/modules/unit_tests/_unit_tests.dm | 1 + .../modules/unit_tests/voidcrew_rig_bay.dm | 230 ++++++++++++++++++ .../modules/unit_tests/voidcrew_zone_shops.dm | 19 +- 3 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 voidcrew/modules/unit_tests/voidcrew_rig_bay.dm diff --git a/voidcrew/modules/unit_tests/_unit_tests.dm b/voidcrew/modules/unit_tests/_unit_tests.dm index e70ad1b8e66..6ab262745cf 100644 --- a/voidcrew/modules/unit_tests/_unit_tests.dm +++ b/voidcrew/modules/unit_tests/_unit_tests.dm @@ -201,6 +201,7 @@ #include "voidcrew_research_movement.dm" #include "voidcrew_respawn_timer.dm" #include "voidcrew_restaurant_portal.dm" +#include "voidcrew_rig_bay.dm" #include "voidcrew_rnd_kit.dm" #include "voidcrew_round_end.dm" #include "voidcrew_round_metrics.dm" diff --git a/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm b/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm new file mode 100644 index 00000000000..6c821ab7645 --- /dev/null +++ b/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm @@ -0,0 +1,230 @@ +// The Rig Bay (voidcrew/modules/trade/rig_bay/). Fork defines compile after the tests, so the stand states, +// trait and signal names are written out: "clamped", "sold_on_stand", "rig_bay_clamped", "mecha_try_enter". + +/datum/unit_test/voidcrew_rig_bay + parent_type = /datum/unit_test/voidcrew_zone_shops + abstract_type = /datum/unit_test/voidcrew_rig_bay + +/datum/unit_test/voidcrew_rig_bay/proc/bay_turf(dx, dy) + var/turf/place = locate(run_loc_floor_bottom_left.x + dx, run_loc_floor_bottom_left.y + dy, run_loc_floor_bottom_left.z) + TEST_ASSERT(isfloorturf(place), "The test room has no floor at +[dx],+[dy]") + return place + +/// A stand linked to `shop`, as the outpost's link sweep would, without dealing a mech yet. +/datum/unit_test/voidcrew_rig_bay/proc/make_stand(datum/outpost_shop/vendor/suit_fitter/shop, turf/place, pool, bay_number = 1) + var/obj/structure/rig_bay_stand/stand = allocate(/obj/structure/rig_bay_stand, place) + stand.pool = pool + stand.bay_number = bay_number + stand.shop = shop + shop.attach_stand(stand) + return stand + +/// Deals and tracks a mech, so a sold one is cleaned up with the test. +/datum/unit_test/voidcrew_rig_bay/proc/deal(obj/structure/rig_bay_stand/stand) + var/obj/vehicle/sealed/mecha/mech = stand.deal_mech(FALSE) + TEST_ASSERT_NOTNULL(mech, "Bay [stand.bay_number] dealt no mech") + allocated += mech + return mech + +/// Every stand mech spawns with the kit its line lists. +/datum/unit_test/voidcrew_rig_bay/roster/Run() + var/datum/outpost_shop/vendor/suit_fitter/shop = allocate(/datum/outpost_shop/vendor/suit_fitter) + var/obj/structure/rig_bay_stand/stand = make_stand(shop, bay_turf(1, 1), /datum/shop_rotation/rig_bay/work) + for(var/sku_type in subtypesof(/datum/shop_sku/rig_bay_mech)) + shop.add_sku(sku_type, "core") + var/datum/shop_sku/rig_bay_mech/sku = shop.skus[length(shop.skus)] + var/obj/vehicle/sealed/mecha/mech = stand.receive_mech(sku, FALSE) + TEST_ASSERT_NOTNULL(mech, "[sku_type] put no mech on the stand") + allocated += mech + TEST_ASSERT_EQUAL(mech.type, sku.item_path, "[sku_type] hung the wrong mech") + for(var/kit_type in sku.spawn_kit) + TEST_ASSERT(locate(kit_type) in mech.flat_equipment, "[sku_type] came without its [kit_type]") + for(var/obj/item/mecha_parts/mecha_equipment/equipment as anything in mech.flat_equipment) + TEST_ASSERT(!istype(equipment, /obj/item/mecha_parts/mecha_equipment/weapon), "[sku_type] hangs armed with [equipment.type]") + TEST_ASSERT(is_trader_good(mech), "[sku_type] isn't marked as trader goods") + TEST_ASSERT(mech.cell && is_trader_good(mech.cell), "[sku_type]'s cell isn't marked as trader goods") + stand.drop_sku() + qdel(mech) + TEST_ASSERT_EQUAL(stand.stand_state, "empty", "Deleting the mech didn't empty the stand") + +/// A hanging mech can't be entered, opened, fitted, damaged, moved or teleported. +/datum/unit_test/voidcrew_rig_bay/clamp/Run() + var/datum/outpost_shop/vendor/suit_fitter/shop = allocate(/datum/outpost_shop/vendor/suit_fitter) + var/turf/place = bay_turf(1, 1) + var/obj/structure/rig_bay_stand/stand = make_stand(shop, place, /datum/shop_rotation/rig_bay/combat, 3) + var/obj/vehicle/sealed/mecha/mech = deal(stand) + TEST_ASSERT_EQUAL(stand.stand_state, "clamped", "A dealt mech isn't hanging") + TEST_ASSERT(stand.sku in shop.skus, "The stand's line isn't in the shop") + TEST_ASSERT_EQUAL(stand.sku.stock, 1, "A hanging mech isn't in stock") + TEST_ASSERT(findtext(stand.sku.name, ", bay 3"), "The line doesn't name its bay: [stand.sku.name]") + TEST_ASSERT(HAS_TRAIT(mech, "rig_bay_clamped"), "A hanging mech isn't chained") + TEST_ASSERT(mech.anchored, "A hanging mech isn't anchored") + TEST_ASSERT(!(mech.mecha_flags & (AI_COMPATIBLE | MMI_COMPATIBLE | BEACON_CONTROLLABLE)), "A hanging mech takes remote pilots") + + var/mob/living/carbon/human/consistent/visitor = allocate(/mob/living/carbon/human/consistent, bay_turf(1, 2)) + TEST_ASSERT(!mech.mob_try_enter(visitor), "Someone climbed into a hanging mech") + TEST_ASSERT(!(visitor in mech.occupants), "A hanging mech took a pilot") + + var/obj/item/screwdriver/screwdriver = allocate(/obj/item/screwdriver) + visitor.put_in_active_hand(screwdriver) + click_wrapper(visitor, mech) + TEST_ASSERT(!(mech.mecha_flags & PANEL_OPEN), "A screwdriver opened a hanging mech") + qdel(screwdriver) + + var/obj/item/mecha_parts/mecha_equipment/weapon/energy/laser/laser = allocate(/obj/item/mecha_parts/mecha_equipment/weapon/energy/laser) + visitor.put_in_active_hand(laser) + click_wrapper(visitor, mech) + TEST_ASSERT(!(laser in mech.flat_equipment), "A weapon was fitted to a hanging mech") + visitor.set_combat_mode(TRUE) + var/integrity = mech.get_integrity() + click_wrapper(visitor, mech) + TEST_ASSERT_EQUAL(mech.get_integrity(), integrity, "A hanging mech took a melee hit") + visitor.set_combat_mode(FALSE) + qdel(laser) + + mech.take_damage(200, BRUTE, BULLET) + TEST_ASSERT_EQUAL(mech.get_integrity(), integrity, "A hanging mech took damage") + var/charge = mech.cell.charge + mech.emp_act(EMP_HEAVY) + TEST_ASSERT_EQUAL(mech.cell.charge, charge, "An EMP drained a hanging mech") + + var/turf/next_door = bay_turf(2, 1) + mech.Move(next_door, EAST) + TEST_ASSERT_EQUAL(mech.loc, place, "A hanging mech moved off its stand") + do_teleport(mech, next_door, no_effects = TRUE, channel = TELEPORT_CHANNEL_BLUESPACE, forced = TRUE) + TEST_ASSERT_EQUAL(mech.loc, place, "A hanging mech was teleported off its stand") + +/// Buying hands over that exact mech; a second buyer is refused; a vanished mech refunds. +/datum/unit_test/voidcrew_rig_bay/purchase/Run() + var/obj/structure/overmap/ship/ship = make_ship() + var/datum/voidcrew_crew_record/crew = record_for(ship) + var/mob/living/carbon/human/buyer = make_member() + ship.enlist_crewmember(buyer) + var/datum/bank_account/account = fund(buyer, 40000) + var/datum/outpost_shop/vendor/suit_fitter/shop = allocate(/datum/outpost_shop/vendor/suit_fitter) + var/obj/structure/rig_bay_stand/stand = make_stand(shop, bay_turf(1, 1), /datum/shop_rotation/rig_bay/work) + var/obj/vehicle/sealed/mecha/mech = deal(stand) + var/datum/shop_sku/rig_bay_mech/sku = stand.sku + var/price = sku.price_credits + + TEST_ASSERT(sku.try_purchase(buyer, null), "The buyer couldn't buy the hanging mech") + TEST_ASSERT_EQUAL(account.account_balance, 40000 - price, "The mech cost the wrong amount") + TEST_ASSERT_EQUAL(sku.stock, 0, "A sold mech stayed in stock") + TEST_ASSERT_EQUAL(stand.held_mech, mech, "The sale swapped the mech") + TEST_ASSERT(stand.stand_state == "releasing" || stand.stand_state == "sold_on_stand", "The stand didn't lower the sold mech ([stand.stand_state])") + TEST_ASSERT(!HAS_TRAIT(mech, "rig_bay_clamped"), "A sold mech is still chained") + TEST_ASSERT_NOTNULL(mech.GetComponent(/datum/component/rig_bay_hold), "A sold mech has no hold for its buyer") + + TEST_ASSERT(!sku.try_purchase(buyer, null), "The same mech sold twice") + TEST_ASSERT_EQUAL(sku.get_denial_reason(buyer), "Out of stock.", "A sold mech didn't say it's out of stock") + TEST_ASSERT_EQUAL(account.account_balance, 40000 - price, "A refused second sale took money") + + // The refund: the mech is gone between the charge and the hand-over + set_permission(crew, 2) + mech.forceMove(bay_turf(2, 2)) + TEST_ASSERT_EQUAL(stand.stand_state, "empty", "Driving the sold mech off didn't empty the stand") + stand.drop_sku() + shop.add_sku(/datum/shop_sku/rig_bay_mech/durand, "core") + var/datum/shop_sku/rig_bay_mech/durand = shop.skus[length(shop.skus)] + var/obj/vehicle/sealed/mecha/durand_mech = stand.receive_mech(durand, FALSE) + allocated += durand_mech + var/balance = account.account_balance + TEST_ASSERT(crew.claim_purchase(durand.type, durand.crew_limit), "Couldn't claim the Durand cap") + TEST_ASSERT(account.adjust_money(-durand.price_credits, "test charge"), "Couldn't charge for the Durand") + TEST_ASSERT(consume_trade_vouchers(buyer, durand.price_vouchers), "Couldn't take the Durand vouchers") + qdel(durand_mech) + TEST_ASSERT_NULL(durand.dispense(buyer, null), "A vanished mech was handed over") + TEST_ASSERT_EQUAL(account.account_balance, balance, "The refund missed credits") + TEST_ASSERT_EQUAL(count_trade_vouchers(buyer), 20, "The refund missed vouchers") + TEST_ASSERT_EQUAL(crew.favor_unique_purchases[durand.type] || 0, 0, "The refund kept the crew cap") + +/// A test hold that is told where it stands, since the test room isn't an outpost. +/datum/component/rig_bay_hold/unit_test + var/test_on_ground = TRUE + var/test_secured = TRUE + +/datum/component/rig_bay_hold/unit_test/on_outpost_ground() + return test_on_ground + +/datum/component/rig_bay_hold/unit_test/on_secured_ground() + return test_on_ground && test_secured + +/// The hold lets the buyer's crew in and nobody else, drops protection when armed, and ends off outpost ground. +/datum/unit_test/voidcrew_rig_bay/hold/Run() + var/obj/structure/overmap/ship/ship = make_ship() + var/datum/voidcrew_crew_record/crew = record_for(ship) + var/mob/living/carbon/human/buyer = make_member() + ship.enlist_crewmember(buyer) + var/obj/structure/overmap/ship/other_ship = make_ship() + record_for(other_ship) + var/mob/living/carbon/human/stranger = make_member("rigbay_stranger") + other_ship.enlist_crewmember(stranger) + + var/obj/vehicle/sealed/mecha/ripley/mech = allocate(/obj/vehicle/sealed/mecha/ripley, bay_turf(1, 1)) + var/initial_flags = mech.resistance_flags + mech.AddComponent(/datum/component/rig_bay_hold/unit_test, crew) + var/datum/component/rig_bay_hold/unit_test/hold = mech.GetComponent(/datum/component/rig_bay_hold) + TEST_ASSERT_NOTNULL(hold, "The hold didn't attach") + + TEST_ASSERT(!mech.mob_try_enter(stranger), "Another crew climbed into a sold mech") + TEST_ASSERT(!(stranger in mech.occupants), "A sold mech took another crew's pilot") + TEST_ASSERT(!(SEND_SIGNAL(mech, "mecha_try_enter", buyer) & 1), "The hold refused the buyer's own crew") + TEST_ASSERT(mech.resistance_flags & INDESTRUCTIBLE, "An empty sold mech past the checkpoint isn't protected") + + var/obj/item/mecha_parts/mecha_equipment/weapon/energy/laser/laser = new(mech.loc) + laser.attach(mech) + TEST_ASSERT(!(mech.resistance_flags & INDESTRUCTIBLE), "An armed sold mech stayed protected") + + hold.test_on_ground = FALSE + mech.forceMove(bay_turf(1, 2)) + TEST_ASSERT_NULL(mech.GetComponent(/datum/component/rig_bay_hold), "The hold outlived outpost ground") + TEST_ASSERT_EQUAL(mech.resistance_flags, initial_flags, "The hold left its flags behind") + TEST_ASSERT(!(SEND_SIGNAL(mech, "mecha_try_enter", stranger) & 1), "The lock outlived the hold") + +/// Restock fills only empty stands; the combat stand waits for every second convoy. +/datum/unit_test/voidcrew_rig_bay/restock/Run() + var/datum/outpost_shop/vendor/suit_fitter/shop = allocate(/datum/outpost_shop/vendor/suit_fitter) + var/obj/structure/rig_bay_stand/work = make_stand(shop, bay_turf(1, 1), /datum/shop_rotation/rig_bay/work, 1) + var/obj/structure/rig_bay_stand/parked = make_stand(shop, bay_turf(2, 1), /datum/shop_rotation/rig_bay/work, 2) + var/obj/structure/rig_bay_stand/combat = make_stand(shop, bay_turf(3, 1), /datum/shop_rotation/rig_bay/combat, 3) + var/mob/living/carbon/human/consistent/buyer = allocate(/mob/living/carbon/human/consistent, bay_turf(1, 3)) + var/obj/vehicle/sealed/mecha/work_mech = deal(work) + var/obj/vehicle/sealed/mecha/parked_mech = deal(parked) + var/obj/vehicle/sealed/mecha/combat_mech = deal(combat) + + // A hanging mech stays put through a convoy + shop.convoy_restock() + TEST_ASSERT_EQUAL(work.held_mech, work_mech, "A convoy replaced a mech nobody bought") + + for(var/obj/structure/rig_bay_stand/stand as anything in list(work, parked, combat)) + TEST_ASSERT(stand.release_mech(buyer), "Bay [stand.bay_number] wouldn't release its mech") + TEST_ASSERT_EQUAL(stand.stand_state, "sold_on_stand", "Bay [stand.bay_number] didn't finish lowering") + work_mech.forceMove(bay_turf(1, 2)) + combat_mech.forceMove(bay_turf(3, 2)) + TEST_ASSERT_EQUAL(work.stand_state, "empty", "Driving off didn't empty bay 1") + TEST_ASSERT_EQUAL(combat.stand_state, "empty", "Driving off didn't empty bay 3") + + shop.convoy_restock() + TEST_ASSERT_EQUAL(work.stand_state, "clamped", "The work stand didn't refill on the convoy") + TEST_ASSERT_EQUAL(work.sku?.stock, 1, "The refilled work stand isn't in stock") + TEST_ASSERT(work.sku in shop.skus, "The refilled line isn't in the shop") + allocated += work.held_mech + TEST_ASSERT_EQUAL(parked.held_mech, parked_mech, "A sold mech left on its stand was replaced") + TEST_ASSERT_EQUAL(parked.stand_state, "sold_on_stand", "A stand with a sold mech on it took a new one") + TEST_ASSERT_EQUAL(combat.stand_state, "empty", "The combat stand refilled on the first convoy") + var/rig_bay_lines = 0 + for(var/datum/shop_sku/rig_bay_mech/line in shop.skus) + rig_bay_lines++ + TEST_ASSERT_EQUAL(rig_bay_lines, 3, "The shop lists the wrong number of stand lines") + + shop.convoy_restock() + TEST_ASSERT_EQUAL(combat.stand_state, "clamped", "The combat stand didn't refill on the second convoy") + allocated += combat.held_mech + TEST_ASSERT(ispath(combat.sku?.item_path, /obj/vehicle/sealed/mecha/gygax) || ispath(combat.sku?.item_path, /obj/vehicle/sealed/mecha/durand), "The combat stand dealt a work mech") + +/// Ship saves never keep a stand or a mech. +/datum/unit_test/voidcrew_rig_bay/saves/Run() + var/obj/structure/rig_bay_stand/stand = allocate(/obj/structure/rig_bay_stand, bay_turf(1, 1)) + var/obj/vehicle/sealed/mecha/ripley/mech = allocate(/obj/vehicle/sealed/mecha/ripley, bay_turf(2, 1)) + TEST_ASSERT(!outpost_checkpoint_saves(stand), "Ship saves keep Rig Bay stands") + TEST_ASSERT(!outpost_checkpoint_saves(mech), "Ship saves keep mechs") diff --git a/voidcrew/modules/unit_tests/voidcrew_zone_shops.dm b/voidcrew/modules/unit_tests/voidcrew_zone_shops.dm index d1a2677ee4c..a6c62e9e233 100644 --- a/voidcrew/modules/unit_tests/voidcrew_zone_shops.dm +++ b/voidcrew/modules/unit_tests/voidcrew_zone_shops.dm @@ -125,11 +125,28 @@ /datum/shop_sku/ripperdoc/governor_delete = 3, /datum/shop_sku/ripperdoc/redline = 3, /datum/shop_sku/ripperdoc/cascade = 3, + /datum/shop_sku/rig_bay_mech/gygax = 1, + /datum/shop_sku/rig_bay_mech/durand = 2, + /datum/shop_sku/rig_bay_gear/arm/missile_rack = 2, + /datum/shop_sku/rig_bay_gear/arm/lmg = 1, + /datum/shop_sku/rig_bay_gear/arm/laser = 1, + /datum/shop_sku/rig_bay_gear/arm/laser_heavy = 1, + /datum/shop_sku/rig_bay_gear/arm/scattershot = 1, + /datum/shop_sku/rig_bay_gear/arm/carbine = 1, + /datum/shop_sku/rig_bay_gear/arm/ion = 1, + /datum/shop_sku/rig_bay_gear/arm/tesla = 1, + /datum/shop_sku/rig_bay_gear/arm/flashbang_launcher = 1, + /datum/shop_sku/rig_bay_gear/ammo/lmg = 1, + /datum/shop_sku/rig_bay_gear/ammo/scattershot = 1, + /datum/shop_sku/rig_bay_gear/ammo/incendiary = 1, + /datum/shop_sku/rig_bay_gear/ammo/flashbang = 1, + /datum/shop_sku/rig_bay_gear/ammo/missiles = 2, + /datum/shop_sku/rig_bay_gear/projectile_shielding = 1, ) /datum/unit_test/voidcrew_zone_shops/catalog/Run() var/list/expected = expected_tags() - TEST_ASSERT_EQUAL(length(expected), 99, "The retained permission catalog is incomplete") + TEST_ASSERT_EQUAL(length(expected), 116, "The retained permission catalog is incomplete") var/datum/voidcrew_crew_record/crew = allocate(/datum/voidcrew_crew_record) for(var/datum/shop_sku/sku_type as anything in subtypesof(/datum/shop_sku)) var/permission = expected[sku_type] || 0 From 2d892e63fc5502fd97dd33f3676417d69ed64da0 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 01:58:43 -0800 Subject: [PATCH 07/16] Don't count a crew's hits on its own new mech as aggression; add the Rig Bay to the wiki --- voidcrew/modules/trade/rig_bay/rig_bay_clamp.dm | 4 ++++ wiki/content/trader-outposts.md | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_clamp.dm b/voidcrew/modules/trade/rig_bay/rig_bay_clamp.dm index c030e995b9b..f6f5ec53e2c 100644 --- a/voidcrew/modules/trade/rig_bay/rig_bay_clamp.dm +++ b/voidcrew/modules/trade/rig_bay/rig_bay_clamp.dm @@ -288,6 +288,10 @@ /datum/component/rig_bay_hold/proc/register_hit(atom/source, atom/attacker) if(!is_trader_outpost_protected(source)) return + // The buyer's crew can bash their own mech about + var/mob/hitter = attacker + if(ismob(hitter) && crew_ref && hitter.mind?.get_reputation_crew() == crew_ref.resolve()) + return register_outpost_property_hit(source, attacker) /datum/component/rig_bay_hold/proc/on_projectile_hit(atom/source, obj/projectile/hit_projectile) diff --git a/wiki/content/trader-outposts.md b/wiki/content/trader-outposts.md index e14561a5173..5b94d7101fc 100644 --- a/wiki/content/trader-outposts.md +++ b/wiki/content/trader-outposts.md @@ -72,14 +72,25 @@ Each outpost's concourse has its own side vendors, each with their own shopkeep | Halcyon | Pike's Bait & Tackle | Pike | Rods, bait and tackle, beside an indoor pond | | Halcyon | The Chowder Pot | Roux | The diner. Hot meals in tiers, galley staples, buys home cooking | | Quartermain | The Skunkworks | Boffin | Research and fabrication gear, Strong-Arm implants, robotics and xenobiology starter packs, raw anomaly cores and anomaly charts | -| Quartermain | The Fitting Bay | Wick | Civilian through advanced MODsuits | +| Quartermain | The Rig Bay | Wick | Civilian through advanced MODsuits, whole exosuits, exosuit arms, ammo and fittings | | The Undertow | The Dregs | Dram | The cantina | | The Undertow | Patch-Up Clinic | Sawbones | Medical supplies, the genetics starter pack | | The Undertow | The Chop Shop | Splice | Cyberware implants in four tiers, installed in the parlor's own Chrome Cradle | **The Chowder Pot** at Halcyon is the waystation diner, and the menu is a quality ladder with real teeth. *Counter grub* is cheap factory food. It fills you up and does nothing else. The *blue plate specials* and the *chef's table* come off Roux's own pass, and a meal an actual cook made gives a genuine mood lift that scales with the depth of the recipe, so the expensive plate really does eat better than the hot dog. The *pantry* shelf stocks the raw staples a ship galley cannot print, flour, rice, milk, eggs, butter, enzyme, meat, even monkey cubes, which makes Halcyon the place to provision a working kitchen. Roux also **buys** home-cooked dishes, hand-made pies and whole cakes over the counter. She pays for real cooking only: vending-machine food is refused, and so are her own plates coming back around. -**The Fitting Bay** at Quartermain has a **MOD fitting bench**, a powered frame you back into while still wearing your suit. It swaps modules for free with no screwdriver work, sells permanent upgrades (plating, module capacity, actuator tuning, power regulation, cell swaps) and per-visit servicing, gives away paint and engraving, and trickle-charges your cell while the frame is shut. Reinforced plating needs 4 [reputation](zone-reputation.md) and composite plating needs 11. It will not convert a suit from one theme to another. For the tier Wick won't touch, syndicate and elite chassis, Interdyne and infiltrator control units, the antag modules. Go to **Vex**: 3600 credits plus 2 vouchers for a syndicate chassis, 5400 plus 3 for an elite, 5 vouchers for a fully equipped syndicate suit. +**The Rig Bay** at Quartermain has a **MOD fitting bench**, a powered frame you back into while still wearing your suit. It swaps modules for free with no screwdriver work, sells permanent upgrades (plating, module capacity, actuator tuning, power regulation, cell swaps) and per-visit servicing, gives away paint and engraving, and trickle-charges your cell while the frame is shut. Reinforced plating needs 4 [reputation](zone-reputation.md) and composite plating needs 11. It will not convert a suit from one theme to another. For the tier Wick won't touch, syndicate and elite chassis, Interdyne and infiltrator control units, the antag modules. Go to **Vex**: 3600 credits plus 2 vouchers for a syndicate chassis, 5400 plus 3 for an elite, 5 vouchers for a fully equipped syndicate suit. + +Wick also sells **whole exosuits**, each one hanging on chains from a gantry in the back of the Rig Bay. Each gantry has its bay number painted on it, and the Exosuits tab lists one line per bay ("Ripley, bay 2"). Buying a line lowers that exact mech off its chains. Nobody can climb into, open, fit or move a mech while it hangs there. + +- **Work bays** show a Ripley MK-I, Ripley MK-II, Clarke or Odysseus, from 4000 to 6500 credits, with a clamp and drill, a drill and mining scanner, or a sleeper already fitted. They restock on every convoy. +- **The combat bay** shows a Gygax (11000 credits, 4 reputation, two per crew each round) or a Durand (18000 credits plus 3 vouchers, 11 reputation, one per crew each round). Combat mechs come bare; buy the arms separately. The bay restocks on every second convoy. +- A bay only restocks once the last mech has been driven off it. +- After the sale only your crew can climb in, while the mech is on the outpost and for 20 minutes at most. An empty, unarmed mech can't be damaged while it is still inside the concourse. +- Drive it out through the exit gate and take the lobby elevator down to the hangars, like anyone else. +- Wick never buys a mech back, and a ship save does not keep mechs aboard. + +The **Exosuit Arms**, **Exosuit Ammo** and **Exosuit Fittings** tabs carry the printable mech guns (a few yellow-reputation arms at a time, one each), their ammo boxes, armor boosters, drills, clamps and repair droids. Weapons bought past the checkpoint come in a sealed case, as guns do. **The Chop Shop** at the Undertow is the galaxy's only cyberware parlor, a neon annex off the Dregs. Splice sells chrome implants off four shelves, street chrome for credits, pro chrome for credits or vouchers, military and legend chrome for vouchers only, and the **Chrome Cradle** in the middle of the room does the installing: you lie on the slab, pick a piece off the rack, and the rig does the surgery in seconds. Removals, repairs and ink re-keying run on the same console, an ammunition shelf keeps implanted weapons fed, and Splice buys back pulled street chrome at about half price. The full catalog is on [Cyberware](cyberware.md). From b080b80c9b2e25dd41aaee7aed834ea5c55fb2e7 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 01:58:53 -0800 Subject: [PATCH 08/16] Rebuild the Rig Bay as a mech showroom with a workshop and a central desk --- .../outposts/trader_outpost_outfitter.dmm | 627 +++++++++++++++--- 1 file changed, 548 insertions(+), 79 deletions(-) diff --git a/voidcrew/_maps/map_files/outposts/trader_outpost_outfitter.dmm b/voidcrew/_maps/map_files/outposts/trader_outpost_outfitter.dmm index a7f249012a1..8efec13791c 100644 --- a/voidcrew/_maps/map_files/outposts/trader_outpost_outfitter.dmm +++ b/voidcrew/_maps/map_files/outposts/trader_outpost_outfitter.dmm @@ -62,6 +62,29 @@ }, /turf/open/indestructible, /area/voidcrew/trader_outpost) +"aj" = ( +/obj/structure/rack, +/obj/structure/showcase{ + density = 0; + desc = "A display drill, bolted to the rack."; + icon = 'icons/obj/devices/mecha_equipment.dmi'; + icon_state = "mecha_drill"; + name = "exosuit drill"; + pixel_x = -3; + pixel_y = 5 + }, +/obj/structure/showcase{ + density = 0; + desc = "A display clamp, bolted to the rack."; + icon = 'icons/obj/devices/mecha_equipment.dmi'; + icon_state = "mecha_clamp"; + name = "hydraulic clamp"; + pixel_x = 3; + pixel_y = -4 + }, +/obj/effect/turf_decal/bot, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "ak" = ( /obj/effect/turf_decal/box/red, /obj/effect/landmark/outpost_checkpoint_chamber{ @@ -91,6 +114,14 @@ /obj/structure/extinguisher_cabinet/directional/south, /turf/open/indestructible/white/smooth_large, /area/voidcrew/trader_outpost) +"an" = ( +/obj/structure/sign/poster/official/build/directional/north, +/obj/effect/turf_decal/stripes/line{ + dir = 4 + }, +/obj/effect/decal/cleanable/dirt, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "ao" = ( /turf/template_noop, /area/template_noop) @@ -163,6 +194,14 @@ }, /turf/open/indestructible/dark/textured, /area/voidcrew/trader_outpost) +"ax" = ( +/obj/machinery/light/directional/north, +/obj/effect/turf_decal/stripes/line{ + dir = 8 + }, +/obj/effect/decal/cleanable/blood/oil, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "ay" = ( /obj/effect/decal/cleanable/dirt/dust, /obj/machinery/light/floor, @@ -336,6 +375,17 @@ /obj/effect/decal/cleanable/dirt/dust, /turf/open/indestructible/plating, /area/voidcrew/trader_outpost) +"aS" = ( +/obj/machinery/computer/terminal{ + content = list("Gantry one and two. Chains greased, load cells zeroed. Keep your hands out of the links. - W."); + desc = "An old terminal wired into the chain motors overhead."; + dir = 2; + name = "gantry console"; + upperinfo = "QUARTERMAIN RIG BAY" + }, +/obj/effect/turf_decal/bot, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "aT" = ( /obj/structure/rig_bay_stand{ bay_number = 1; @@ -591,6 +641,12 @@ }, /turf/open/indestructible/dark/textured, /area/voidcrew/trader_outpost/checkpoint) +"bC" = ( +/obj/effect/turf_decal/stripes/line{ + dir = 4 + }, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "bD" = ( /obj/effect/landmark/outpost_elevator_alcove, /obj/effect/turf_decal/stripes/line{ @@ -724,6 +780,14 @@ /obj/effect/decal/cleanable/dirt/dust, /turf/open/indestructible/plating, /area/voidcrew/trader_outpost/checkpoint) +"bT" = ( +/obj/machinery/light/directional/north, +/obj/effect/turf_decal/stripes/line{ + dir = 8 + }, +/obj/effect/decal/cleanable/dirt, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "bU" = ( /obj/machinery/door/airlock/outpost/checkpoint/outer{ checkpoint_id = "quartermain_east_yard"; @@ -761,6 +825,17 @@ /obj/effect/decal/cleanable/blood/oil, /turf/open/indestructible/large, /area/voidcrew/trader_outpost) +"bZ" = ( +/obj/machinery/computer/terminal{ + content = list("Gantry three. Rated for the heavy frames. Do not hang two. - W."); + desc = "An old terminal wired into the chain motors overhead."; + dir = 2; + name = "gantry console"; + upperinfo = "QUARTERMAIN RIG BAY" + }, +/obj/effect/turf_decal/bot, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "ca" = ( /obj/effect/turf_decal/stripes/line{ dir = 1 @@ -856,10 +931,114 @@ }, /turf/open/indestructible, /area/voidcrew/trader_outpost) +"cn" = ( +/obj/effect/turf_decal/stripes/red/line{ + dir = 4 + }, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) +"co" = ( +/obj/structure/rig_bay_stand{ + bay_number = 3; + dir = 2; + pool = /datum/shop_rotation/rig_bay/combat + }, +/obj/effect/turf_decal/delivery/red, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "cp" = ( /obj/structure/lattice/catwalk, /turf/open/space/basic, /area/space) +"cq" = ( +/obj/effect/turf_decal/stripes/red/line{ + dir = 8 + }, +/obj/effect/decal/cleanable/blood/oil, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) +"cr" = ( +/obj/structure/table/reinforced, +/obj/structure/showcase{ + density = 0; + desc = "A display piece, bolted to the table."; + icon = 'icons/obj/clothing/modsuit/mod_construction.dmi'; + icon_state = "chestplate"; + name = "MOD chestplate"; + pixel_x = -5; + pixel_y = 4 + }, +/obj/structure/showcase{ + density = 0; + desc = "A display helmet. The visor is a sticker."; + icon = 'icons/obj/clothing/modsuit/mod_construction.dmi'; + icon_state = "helmet"; + name = "MOD helmet"; + pixel_x = 6; + pixel_y = 2 + }, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) +"cs" = ( +/obj/machinery/porta_turret/outpost, +/obj/effect/turf_decal/box/red/corners{ + dir = 8 + }, +/obj/effect/turf_decal/box/red/corners, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) +"ct" = ( +/obj/structure/showcase{ + density = 1; + desc = "Wick's own fabricator. The access panel is padlocked shut."; + icon = 'icons/obj/machines/robotics.dmi'; + icon_state = "fab-idle"; + name = "exosuit fabricator" + }, +/obj/effect/turf_decal/bot, +/obj/machinery/light/directional/west, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cu" = ( +/obj/effect/turf_decal/trimline/yellow/line{ + dir = 1 + }, +/obj/effect/decal/cleanable/dirt/dust, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cv" = ( +/obj/effect/turf_decal/stripes/line{ + dir = 1 + }, +/obj/effect/turf_decal/caution/stand_clear{ + dir = 2 + }, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cw" = ( +/obj/effect/turf_decal/trimline/yellow/line{ + dir = 1 + }, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cx" = ( +/obj/effect/turf_decal/trimline/yellow/line{ + dir = 1 + }, +/obj/effect/turf_decal/stripes/line{ + dir = 4 + }, +/obj/effect/turf_decal/arrows{ + dir = 4 + }, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cy" = ( +/obj/effect/turf_decal/trimline/yellow/line{ + dir = 1 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) "cz" = ( /obj/effect/turf_decal/siding/wideplating_new/dark/corner, /obj/effect/turf_decal/siding/wideplating_new/dark/corner{ @@ -875,6 +1054,81 @@ /obj/effect/decal/cleanable/dirt, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) +"cB" = ( +/obj/effect/turf_decal/stripes/line{ + dir = 1 + }, +/obj/effect/turf_decal/caution/stand_clear{ + dir = 2 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"cC" = ( +/obj/effect/turf_decal/trimline/yellow/line{ + dir = 1 + }, +/obj/effect/turf_decal/arrows{ + dir = 4 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"cD" = ( +/obj/effect/turf_decal/stripes/line{ + dir = 1 + }, +/obj/effect/turf_decal/caution/stand_clear{ + dir = 2 + }, +/turf/open/indestructible, +/area/voidcrew/trader_outpost) +"cE" = ( +/obj/structure/showcase{ + density = 1; + desc = "A sealed MOD suit locker. Wick has the key."; + icon = 'icons/obj/machines/suit_storage.dmi'; + icon_state = "industrial"; + name = "suit locker" + }, +/obj/effect/turf_decal/bot, +/obj/structure/sign/poster/contraband/borg_fancy_1/directional/east, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"cF" = ( +/obj/structure/table/reinforced, +/obj/structure/showcase{ + density = 0; + desc = "A cyborg frame with no arms, no head and no warranty."; + icon = 'icons/mob/augmentation/augments.dmi'; + icon_state = "robo_suit"; + name = "half-built endoskeleton"; + pixel_y = 6 + }, +/obj/structure/sign/poster/official/safety_eye_protection/directional/west, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cG" = ( +/obj/structure/table/reinforced, +/obj/structure/showcase{ + density = 0; + desc = "Wick's toolbox. It's chained to the bench."; + icon = 'icons/obj/storage/toolbox.dmi'; + icon_state = "blue"; + name = "toolbox"; + pixel_y = 3 + }, +/obj/machinery/light/directional/west, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cH" = ( +/obj/structure/table/reinforced, +/obj/structure/showcase/cyborg/old{ + desc = "A cyborg shell with its panel off and its innards on the bench."; + dir = 2; + name = "stripped cyborg"; + pixel_y = 6 + }, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) "cI" = ( /obj/structure/closet/secure_closet/outpost_rental, /obj/effect/turf_decal/siding/wideplating_new/dark{ @@ -886,6 +1140,125 @@ /obj/effect/decal/cleanable/cobweb, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) +"cJ" = ( +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cK" = ( +/obj/effect/decal/cleanable/blood/oil, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cL" = ( +/obj/structure/rack, +/obj/structure/showcase{ + density = 0; + desc = "A display unit, bolted to the rack."; + icon = 'icons/obj/devices/mecha_equipment.dmi'; + icon_state = "thrusters"; + name = "thruster pack"; + pixel_x = -4; + pixel_y = 3 + }, +/obj/structure/showcase{ + density = 0; + desc = "A display unit, bolted to the rack."; + icon = 'icons/obj/devices/mecha_equipment.dmi'; + icon_state = "mecha_air_tank"; + name = "exosuit air tank"; + pixel_x = 6; + pixel_y = -2 + }, +/obj/effect/turf_decal/bot, +/obj/effect/turf_decal/stripes/line{ + dir = 4 + }, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cM" = ( +/obj/effect/decal/cleanable/dirt, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cN" = ( +/obj/structure/mecha_wreckage/ripley{ + anchored = 1; + desc = "A Ripley stripped to the frame. Most of it is on the bench."; + icon = 'icons/mob/rideables/mech_construction.dmi'; + icon_state = "ripley13"; + name = "Ripley frame" + }, +/obj/effect/turf_decal/box, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cO" = ( +/obj/effect/turf_decal/stripes/line{ + dir = 4 + }, +/obj/effect/decal/cleanable/dirt/dust, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cP" = ( +/obj/structure/showcase{ + density = 1; + desc = "Half a Durand. The other half is on back order."; + icon = 'icons/mob/rideables/mech_construction.dmi'; + icon_state = "durand10"; + name = "Durand frame" + }, +/obj/effect/turf_decal/box, +/obj/effect/turf_decal/stripes/line{ + dir = 4 + }, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cQ" = ( +/obj/effect/turf_decal/stripes/line{ + dir = 2 + }, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cR" = ( +/obj/effect/turf_decal/stripes/line{ + dir = 2 + }, +/obj/effect/decal/cleanable/dirt, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cS" = ( +/obj/effect/turf_decal/stripes/line{ + dir = 6 + }, +/turf/open/indestructible/textured, +/area/voidcrew/trader_outpost) +"cT" = ( +/obj/structure/table/reinforced/rglass, +/obj/structure/showcase{ + density = 0; + desc = "A Mk-II conversion kit. The box is empty. Wick is not daft."; + icon = 'icons/obj/devices/mecha_equipment.dmi'; + icon_state = "ripleyupgrade"; + name = "conversion kit"; + pixel_y = 4 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"cU" = ( +/obj/structure/table/reinforced/rglass, +/obj/structure/showcase{ + density = 0; + desc = "A display core, glued to the counter."; + icon = 'icons/obj/clothing/modsuit/mod_construction.dmi'; + icon_state = "mod-core"; + name = "MOD core"; + pixel_y = 4 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"cV" = ( +/mob/living/basic/outpost_trader/suit_fitter, +/obj/structure/chair/office/light{ + dir = 4 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) "cW" = ( /obj/effect/turf_decal/siding/wideplating_new/dark{ dir = 1 @@ -912,12 +1285,108 @@ }, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) +"cZ" = ( +/obj/structure/table/reinforced/rglass, +/obj/structure/desk_bell{ + pixel_w = 0; + pixel_z = 4 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"da" = ( +/obj/structure/filingcabinet{ + pixel_w = -5; + pixel_z = 0 + }, +/obj/structure/filingcabinet{ + pixel_w = 5; + pixel_z = 0 + }, +/obj/machinery/light/directional/south, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"db" = ( +/obj/structure/table/reinforced/rglass, +/obj/structure/showcase{ + density = 0; + desc = "A display helmet. The visor is a sticker."; + icon = 'icons/obj/clothing/modsuit/mod_construction.dmi'; + icon_state = "helmet"; + name = "MOD helmet"; + pixel_y = 4 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) "dc" = ( /obj/effect/turf_decal/siding/thinplating_new/dark{ dir = 1 }, /turf/open/indestructible/dark/smooth_large, /area/voidcrew/trader_outpost) +"dd" = ( +/obj/structure/showcase{ + density = 1; + desc = "A sealed MOD suit locker. Wick has the key."; + icon = 'icons/obj/machines/suit_storage.dmi'; + icon_state = "industrial"; + name = "suit locker" + }, +/obj/effect/turf_decal/bot, +/obj/machinery/light/directional/east, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"de" = ( +/obj/structure/table/reinforced/rglass, +/obj/structure/showcase{ + density = 0; + desc = "A display paint kit. Somebody has had the paint out of it."; + icon = 'icons/obj/clothing/modsuit/mod_construction.dmi'; + icon_state = "paintkit"; + name = "paint kit"; + pixel_y = 4 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"df" = ( +/obj/structure/showcase{ + density = 1; + desc = "A sealed MOD suit locker. Wick has the key."; + icon = 'icons/obj/machines/suit_storage.dmi'; + icon_state = "classic"; + name = "suit locker" + }, +/obj/effect/turf_decal/bot, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"dg" = ( +/obj/machinery/newscaster/directional/east, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"dh" = ( +/obj/structure/chair/sofa/corp/left{ + dir = 1 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"di" = ( +/obj/structure/chair/sofa/corp{ + dir = 1 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"dj" = ( +/obj/structure/chair/sofa/corp/right{ + dir = 1 + }, +/obj/machinery/light/directional/south, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) +"dk" = ( +/obj/effect/turf_decal/arrows{ + dir = 2 + }, +/turf/open/indestructible, +/area/voidcrew/trader_outpost) "dD" = ( /obj/effect/turf_decal/stripes, /obj/effect/turf_decal/siding/thinplating_new/dark{ @@ -4302,8 +4771,8 @@ ao "} (13,1,1) = {" ao -ao -ao +dU +dU dU dU dU @@ -4344,12 +4813,12 @@ ao "} (14,1,1) = {" ao -ao dU -dU -at -aU -be +aj +ct +cF +cG +cH NJ lE dP @@ -4386,13 +4855,13 @@ ao "} (15,1,1) = {" ao -ao dU -ch -aT -bb -aQ -bj +an +cu +cJ +cM +cJ +cQ lZ bs Um @@ -4428,13 +4897,13 @@ ao "} (16,1,1) = {" ao -ao -dU -aQ -aG -bc -aQ -bk +lE +aT +cv +cK +cN +cK +cR lZ bs Um @@ -4470,13 +4939,13 @@ ao "} (17,1,1) = {" ao -ao dU -aQ -aQ -aQ -aG -bc +ax +cw +cJ +cJ +cJ +cQ lZ vo cX @@ -4512,13 +4981,13 @@ ao "} (18,1,1) = {" ao -ao dU -ch -aY -bb -aQ -bo +aS +cx +cL +cO +cP +cS lE Lx oX @@ -4554,13 +5023,13 @@ ao "} (19,1,1) = {" ao -ao -dU -dU -aQ -aQ -aQ dU +bC +cy +Xe +Xe +ks +da dU dU lE @@ -4596,14 +5065,14 @@ ao "} (20,1,1) = {" ao -ao -dU -ci -bY +lE +aY +cB Xe -cb -cc -dU +Xe +Xe +Xe +lE uD lx tU @@ -4638,13 +5107,13 @@ ao "} (21,1,1) = {" ao -aF dU -cj -bX -ca +bT +cy +Xe +cT +cV Xe -cd lE aJ fp @@ -4681,12 +5150,12 @@ ao (22,1,1) = {" ao dU -dU -bW -Xe -Xe +bZ +cC ks -Xe +cU +cZ +db lE aJ fp @@ -4723,8 +5192,8 @@ ao (23,1,1) = {" ao lE -ce -lM +cn +Ao Ao Ao Ao @@ -4765,10 +5234,10 @@ ao (24,1,1) = {" ao lE -ce -ai -fp +co +cD fp +dk fp fp cl @@ -4807,8 +5276,8 @@ aF (25,1,1) = {" ao lE -cf -sC +cq +Jm Jm Jm Jm @@ -4849,12 +5318,12 @@ aV (26,1,1) = {" ao dU -dU -hL -rd -zC -rj -im +cr +Xe +Xe +Xe +Xe +dh lE SC fp @@ -4890,13 +5359,13 @@ aV "} (27,1,1) = {" ao -aV dU -BS +ch +Xe Xe -PW -OK -BO +de +ks +di lE yK fp @@ -4932,13 +5401,13 @@ aV "} (28,1,1) = {" ao -aV dU -OP -ea -GI -ea -jA +cs +cE +dd +df +dg +dj dU FV fp From f215fe0c6c8af5ac616c72c59131a66a388d0aee Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 02:00:42 -0800 Subject: [PATCH 09/16] Rig Bay stand: gantry, chains and blocking animations for release and beam-in --- tgstation.dme | 1 + .../modules/trade/icons/rig_bay_stand.dmi | Bin 0 -> 1984 bytes .../trade/rig_bay/rig_bay_stand_visuals.dm | 246 ++++++++++++++++++ 3 files changed, 247 insertions(+) create mode 100644 voidcrew/modules/trade/icons/rig_bay_stand.dmi create mode 100644 voidcrew/modules/trade/rig_bay/rig_bay_stand_visuals.dm diff --git a/tgstation.dme b/tgstation.dme index f969d380ffc..42a60774f2b 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7742,6 +7742,7 @@ #include "voidcrew\modules\trade\favor_uniques\vex.dm" #include "voidcrew\modules\trade\rig_bay\rig_bay_skus.dm" #include "voidcrew\modules\trade\rig_bay\rig_bay_stand.dm" +#include "voidcrew\modules\trade\rig_bay\rig_bay_stand_visuals.dm" #include "voidcrew\modules\transporter\_transporter.dm" #include "voidcrew\modules\transporter\transporter_console.dm" #include "voidcrew\modules\transporter\transporter_designs.dm" diff --git a/voidcrew/modules/trade/icons/rig_bay_stand.dmi b/voidcrew/modules/trade/icons/rig_bay_stand.dmi new file mode 100644 index 0000000000000000000000000000000000000000..29bb2d0f7ce7505280ddbf456189b2132cce0edb GIT binary patch literal 1984 zcma)6c~sL^7XD>{gf(fTNB~2i!NEBJl_CfR_{pMCK~T^^kfoMI3=o8X5FmunuoT+H zqEb-=aRFpUXp|rs0SOQk(6EcFVo2CT1F{4r4AavyGw1Zo`{Ta*&iCHE_kHiXFU|G% zVKrrKWdH!wj*uNr0sutr0(nS9xgEz=k>pb!-Q9!c5D@u&L~uAQm>LEEF}Hv0ex}^8 zM*|#v8v8@%?CUtU4aMz6WW=iS$&hMnNvGlq4o74(*8fX58Ly{*b7*$)RH(o{V3wiP zSjy4yYLCby+39eD$Ap9&RF(;2=l3D`xye%xrpD(q#RA5cUYpT|{J_DluiNWKzLsuL|X2vt2?r*1HiVK^;U- zskc6BQheOJ zpNA~+LS%y4O*3xCK1Fo#+Pc8lV~Qmkrw*@mXz@SPcL;>>s(xkE3JD7E_tQpWwgK#4 z0cH~%AnXM2d*zvx0J76o?mvMZTlhhx7elgK4w!rpyWO-|O7Jm(u$%tITUkMr^HzH~ zwo|KhPEdSWM?zl0h*JM{>p0m(U9JW7UB-hQKl4AzR+q*HIaGc`J?njhWw(j+s4Rjg zJw`dfTdR988I@j^NW$F`mh(OpuB1yW5Ml`)31qOEdjUk1LJi{{qfrjCa1 zcVdN6sRfBkQo8jH&R}S!%KqFAJ&LW3&2c`L%bi~uX>7F5v~qEA@xJdU>&!AvqogAt z5Dj}E#|_a*d!L$(%{PmO(iz&6=4Vqk1n=Fs^>b~N!ri5gvZalL4quLF<^83u{B^!B zU8sP}n^VF=%*@sE=HVES4qnG~R>q=?61I~8rMz~?1kMT7qUAc~Rqc^>d4>3BChUG_mPcy7gNB)ML#St<(gDmQs9u?A>QdqF z;lax)d1vF^cw;tQ&Mb5a(vKrW0{yr(tU15E(sJ^A-2B{AgunuMA3>wW&69si&i>Hj zN(U`=(ZTd-Z@m0+C#fi04hSv$Z(1-;#=7b$MXocfl3vNwlcBI%Xa8{qh`LLJo2s40 z!mJ+Zz)X?X;?YK{W_`cL8=i}SVp*Etb->CKNnj3`yy$_o?Ep2Q){SN!KLuCpT_*OS zo@`b!p*d|M)#}Vw3Bi-SA$=GHJD!c_%GvJZ+EM4mP|ZKFvjL z+W|H$xQ_&hAT>((bH*h0cXv?!Ef}y#Inh8~xE!sreliprgayscQ3{uxF2@qSe%X(E zOIkY%WiM)WT?YFLp0_t{Yqi5e{3F|&i1632DzwpFm84%f8V@fqQv>u}7h=DN0D~JW zFs1P`&qpokk3JCKDKZGhpkS>UH>NY{QqtXuJPyirx#F>kldGYh7i% 1 && isnull(profile[right])) + right-- + var/width = right - left + 1 + var/inset = round(width * 0.22) + var/list/anchors = list() + for(var/column in list(left + inset, right - inset)) // 1-based icon columns + column = clamp(column, 2, 31) + var/row = 0 + for(var/c in column - 1 to column + 1) + row = max(row, profile[c] || 0) + anchors += list(list(column - 1, row + 1)) + return anchors + +/// For each of a mech sprite's 32 columns, the first opaque row from the top (0-based), or null. Cached. +/obj/structure/rig_bay_stand/proc/mech_top_profile(obj/vehicle/sealed/mecha/mech) + var/static/list/cache = list() + var/key = "[mech.icon]|[mech.icon_state]|[mech.dir]" + if(cache[key]) + return cache[key] + var/list/profile = scan_profile(icon(mech.icon, mech.icon_state, mech.dir, 1)) + if(!profile) + profile = scan_profile(icon(mech.icon, mech.icon_state, SOUTH, 1)) // single-dir states + if(!profile) + profile = new /list(32) + for(var/i in 1 to 32) + profile[i] = 0 + cache[key] = profile + return profile + +/// Top-edge scan of a 32x32 icon; null if it is empty. +/obj/structure/rig_bay_stand/proc/scan_profile(icon/sprite) + var/list/profile = new /list(32) + var/found = FALSE + for(var/x in 1 to 32) + for(var/row in 0 to 31) + if(sprite.GetPixel(x, 32 - row)) + profile[x] = row + found = TRUE + break + return found ? profile : null + +/// One chain hanging from a Rig Bay gantry. Pure visual; the stand owns it. +/obj/effect/rig_bay_chain + name = "chain" + icon = 'voidcrew/modules/trade/icons/rig_bay_stand.dmi' + icon_state = "chain_5" + layer = RIG_BAY_CHAIN_LAYER + anchored = TRUE + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + blocks_emissive = EMISSIVE_BLOCK_NONE + /// Tile-local x of the shackle's centre. + var/anchor_x = 7 + /// Rows in the current chain_N state. + var/length = RIG_BAY_CHAIN_STOWED + +/// Snaps the chain to a pose, no animation. +/obj/effect/rig_bay_chain/proc/set_pose(new_anchor_x, new_length, new_top) + anchor_x = new_anchor_x + length = new_length + pixel_w = anchor_x - RIG_BAY_CHAIN_HALF + pixel_z = new_top + icon_state = "chain_[length]" + +/// Queues animate() steps that change the chain's length from `from_length` to `to_length` over `time` ds (top stays put). +/obj/effect/rig_bay_chain/proc/queue_length_steps(from_length, to_length, time) + var/steps = max(1, round(time / 0.5)) + for(var/step in 1 to steps) + animate(icon_state = "chain_[round(from_length + (to_length - from_length) * step / steps)]", time = time / steps) + +/// The blue-white column a restocked mech beams in through: 1.5 tiles wide, 3 tall, standing on the pad. +/// Sorts on the pad's row (pixel_z only), so it stays behind the mech and anything south of the stand. +/obj/effect/rig_bay_beam_column + name = "beam" + icon = 'icons/effects/beam.dmi' + icon_state = "bsa_beam" + layer = RIG_BAY_BEAM_LAYER + anchored = TRUE + alpha = 0 + pixel_z = 32 + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + blocks_emissive = EMISSIVE_BLOCK_NONE + +/obj/effect/rig_bay_beam_column/Initialize(mapload) + . = ..() + transform = matrix(1.5, 0, 0, 0, 3, 0) + +#undef RIG_BAY_CHAIN_LAYER +#undef RIG_BAY_GANTRY_LAYER +#undef RIG_BAY_BEAM_LAYER +#undef RIG_BAY_CHAIN_TOP +#undef RIG_BAY_CHAIN_STOWED +#undef RIG_BAY_CHAIN_HALF +#undef RIG_BAY_CHAIN_MAX From 4611ec9e71e1650f3a329656160244695c19f4b3 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 02:07:46 -0800 Subject: [PATCH 10/16] Fit Wick's routine to the new Rig Bay layout His room keeps to the usual worker range, and he works on any prop in it (wrecks, racks, showcases, wall panels), not only those next to him. --- .../modules/trade/rig_bay/rig_bay_wick.dm | 55 ++++++++++++++----- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm b/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm index 6d3b6323ce5..c1316bb9120 100644 --- a/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm +++ b/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm @@ -21,8 +21,8 @@ * round-start pre-load) already calls. The shop and stand code need no call into this file. */ -/// How far from his spawn tile Wick's room may reach. A bound, not a layout: walls and thresholds close it. -#define RIG_BAY_WICK_RANGE 14 +/// How far from his spawn tile Wick's room may reach: the ordinary worker range. Walls and thresholds close it. +#define RIG_BAY_WICK_RANGE OUTPOST_WORK_RANGE /// A player this close to him (or to his counter, see below) is a customer #define RIG_BAY_WICK_SERVE_RANGE 3 /// No new job until this long after the last customer left @@ -82,6 +82,8 @@ return AddComponent(/datum/component/outpost_ambient_worker/rig_bay_wick, list( /datum/outpost_ambient_work/mech_service = 6, + /datum/outpost_ambient_work/weld = 1, + /datum/outpost_ambient_work/wrench = 1, /datum/outpost_ambient_work/panel = 1, ), TRUE, CALLBACK(src, PROC_REF(show_rig_bay_look))) @@ -181,24 +183,47 @@ var/list/tiles = get_room() return tiles[tile] || stand_tiles[tile] || lane_tiles[tile] -/// Mechs on the stands first, then the parent's search around him for anything else (wall panels) +/** + * Mechs on the stands, or any other prop in or against his room (a wreck, a rack, a wall panel). + * Unlike the parent, the whole room is searched, not just a few tiles round where he stands. + */ /datum/component/outpost_ambient_worker/rig_bay_wick/find_work() if(isnull(get_turf(parent)) || !length(work_weights)) return null - var/list/all_weights = work_weights - var/list/weights = all_weights.Copy() - . = null - while(length(weights) && isnull(.)) + var/list/weights = work_weights.Copy() + while(length(weights)) var/work_type = pick_weight(weights) weights -= work_type - if(ispath(work_type, /datum/outpost_ambient_work/mech_service)) - . = find_mech_job(outpost_ambient_work(work_type)) - continue - work_weights = list() - work_weights[work_type] = 1 - . = ..() - work_weights = all_weights - work_weights = all_weights + var/datum/outpost_ambient_work/kind = outpost_ambient_work(work_type) + var/list/job = istype(kind, /datum/outpost_ambient_work/mech_service) ? find_mech_job(kind) : find_room_job(kind) + if(job) + return job + return null + +/// Something in or against his room that `kind` works on, never a stand's mech, as find_work() returns +/datum/component/outpost_ambient_worker/rig_bay_wick/proc/find_room_job(datum/outpost_ambient_work/kind) + if(kind.works_on_floor) + return null + var/list/tiles = get_room() + var/area/home_area = get_area(home) + var/list/search = list() + for(var/turf/tile as anything in tiles) + search[tile] = TRUE + for(var/direction in GLOB.cardinals) + var/turf/next = get_step(tile, direction) + if(next && !stand_tiles[next] && get_area(next) == home_area) + search[next] = TRUE + var/checked = 0 + for(var/turf/tile as anything in shuffle(search)) + for(var/obj/thing in tile) + if(++checked > OUTPOST_WORK_SEARCH_LIMIT) + return null + if(thing.invisibility || IS_WEAKREF_OF(thing, last_target) || !kind.accepts(thing)) + continue + var/turf/spot = spot_for(thing) + if(spot) + return list(thing, spot, kind) + return null /// A mech on one of his stands that the job accepts, and a free tile beside it, as find_work() returns /datum/component/outpost_ambient_worker/rig_bay_wick/proc/find_mech_job(datum/outpost_ambient_work/kind) From f9e2f4788cd4d00734cde4811e88c1d6c4ef755d Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 02:13:22 -0800 Subject: [PATCH 11/16] Give the Rig Bay hold test keyless crew members --- voidcrew/modules/unit_tests/voidcrew_rig_bay.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm b/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm index 6c821ab7645..8d9711701aa 100644 --- a/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm +++ b/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm @@ -157,7 +157,7 @@ ship.enlist_crewmember(buyer) var/obj/structure/overmap/ship/other_ship = make_ship() record_for(other_ship) - var/mob/living/carbon/human/stranger = make_member("rigbay_stranger") + var/mob/living/carbon/human/stranger = make_member() other_ship.enlist_crewmember(stranger) var/obj/vehicle/sealed/mecha/ripley/mech = allocate(/obj/vehicle/sealed/mecha/ripley, bay_turf(1, 1)) From 53f4d5102fb5564a2124eee04d7a47d1ba12f508 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 02:39:06 -0800 Subject: [PATCH 12/16] Rig Bay stand: full build of the release and beam-in (lamps, flash, sparks, tint, sounds, outlined chains) --- .../modules/trade/icons/rig_bay_stand.dmi | Bin 1984 -> 2460 bytes .../trade/rig_bay/rig_bay_stand_visuals.dm | 159 +++++++++++++++--- 2 files changed, 133 insertions(+), 26 deletions(-) diff --git a/voidcrew/modules/trade/icons/rig_bay_stand.dmi b/voidcrew/modules/trade/icons/rig_bay_stand.dmi index 29bb2d0f7ce7505280ddbf456189b2132cce0edb..4a380d30dbd20a209008c17eee59c59a2ba21e56 100644 GIT binary patch literal 2460 zcmaJ>c~sKd7XE=GD5*H)ENYoUnx=Ve5+%aa2B(LKnvG6nTGt#<6G6)H7Iq`;zPgH* zX0IG_h*HrkU*1pyGfhS1mK7)>;w&gUz3ct))?4en{l_``taJAM);iz!?OZ<}Pc>CT zRR93gyuGji003G~f!8qQ^;IZhMy_vjsW^NBHsb2}_zPDOE?kZS0K$Wt{mhGRcj=(N z45g~xp4z^&V}i{ox1eo)Z>!(f+PJAPG`zQfLA@Z*$zxmQQXo=9h_q-_C>JoYnX>1u z6*cYdvV~Dr;NwrUxW7XZ1EZdZScy)B_)CWN!+UAhsu z$uD0Sm z<)`)flm!aZqupWvfQ5Tw58?<9M5B%|5uFCge?IJzlW?C8d+gYPi7>ppJy*w)jLUzY znUAw+foyg>{K)4524{1^Knv!NSb{z}(xDp@rK0~eB415eFUB))79Mv`yJz&F?Uw~D zXWp|`KDleimdwXD@%y_U?3+_lvgzgxo@`x`I}|qa6kqCAWr1xOef-656ZL`3N-<^n+;E`}Hm^A<4fGYiCQde;{KUq? zuI~Q1WlA>U)58Z+*GN^TfdVp@RB&${2m)AG0%nfuKq`ROF#vrI3}|Ts|M!Z8R{10v ztY|hVs_75=?bKp=oCBz!W3{K}sF>YGlpo>ole7TumZ{97_T5RcBk8l1CfDj;oaXLK z{PMU)m+#N5R`3=ECyiMdiuZj{Pm?M-a~7Z4iP!@%w9mueYPc?AHT6ydZp$;201e{H zw52Ku{?yKQe>RGwX&Li^I}&`u{%l{xB4Jq&)P{7wx`1bwHm20E*Dj?>#A1>90%dSN zqz=lF-WZ4n`v52(BXm%l(&6q^RXPD2%~8sWAKhEquTNmENiPnq6E=Agjn%Lg8Xxn| z5I3$3w8<*>nL#*{hpb61eVYpIlJrJ-zl*AF4y)OBjz4_cFDu;K-VJJ8HO50ZzCc)N z#sqB$oD&~*|<`6@vTQuMc%XHLEKx?)t;Imtrq0A;SwU#L-zFJnQ}YA zPVKN;7}gLJq`c?rbcxt+|h=475B^?UzU>e<+4Yxg-%s+$4TBmb(t zl9>~E*%V|!(glH>^L^;v0LofX-I*pHF{L(`lRgA2k|KXkBqL8kI0Olt?h4@qdjVBH zfB}ca!aHQJmxlH5!H?bo+dCwGNCDC>5_%Bi9O_%PwQD)*2%tj$C9*F+Bja-NNd)fQ zm?r!zgagky^Me#3IA>k}++bxX4s`DAt` z)-9h5grvhZaUz?5$shLUgr(%Dw3vrUlrG7#;L1Yl!mEb3K7^eZF|}^W@ z**~aN4JMMk@xNcMW?9UCoev+H#N6|p?qtLn1?+}8CpbbVv>&-vrD`BAvFi$A^M4XX zFu{JdDsh9F_?r^fj3;f+`M$11O_VV@@y4tJ-r~%}-740kd6LtCoDf(; zG1j9{!lnUQXoIj2)s{BKfw*q}cM46T02p0wbGvJP@0(}YYWpC5t|)rDMFLH@=n?bE zir26v%^TFNojy9)Bod%NY0(+TmsaP$73aX{{n>s%Yz3SiHx0%nXC%L1xLn-x(wItB z4-$u#92L5#wa==zPd7wgzq|!qG-hgQlC?T`gsaKT{Om3G$1%*h+6$}fudjR~UK74B z|LH3FklvwPUKff9TEmF-Nqa}5eS=TZF$TKgG_{h7>?h*Vmlv*9Zs==kVVONoF)_Qc z^x|zKvxvs$n4A2OlIU9{uOCi*42gyYtPk7jpWyV{P6d8Tr(l0Mz$ZP#i z`HPlb*j@=wnUQ?0=}c9>az9bBw-%_g-Q`Gwifu`sJm^nuE_l-ewmvfs9k9Azd?+vr zTxI_zRsB0Y3s}g!w9j0Bh-f0Em8{^pO5E3rf{?BaQ0z1NA3z;XIlo22H}2nLI|U$H zw0~2|*~_A68=^ZVW6cY=X9{A~(LgW^yBSV~_=TyWs%4d(P3qUVJs3J%7aR0=vT=Y};z(CMi<29X6 z7&8!d)*B59w{a$e{H!Q-K5`iSC9rj{VZU#>2d?5t|IZ-|6e>H!;Bg|iSL7L}Tvl(6 z#(w=g)+ryGerbAYrK_(=uMDcQ2a0`gV4;Xv&uo;QOWBya@tAVaKt_562zM6#Ck4t~R_uyB}iu8J8Z8|YxULAG(Y?#sP(EnC?juT}&s+IK?%|2tmK{gf(fTNB~2i!NEBJl_CfR_{pMCK~T^^kfoMI3=o8X5FmunuoT+H zqEb-=aRFpUXp|rs0SOQk(6EcFVo2CT1F{4r4AavyGw1Zo`{Ta*&iCHE_kHiXFU|G% zVKrrKWdH!wj*uNr0sutr0(nS9xgEz=k>pb!-Q9!c5D@u&L~uAQm>LEEF}Hv0ex}^8 zM*|#v8v8@%?CUtU4aMz6WW=iS$&hMnNvGlq4o74(*8fX58Ly{*b7*$)RH(o{V3wiP zSjy4yYLCby+39eD$Ap9&RF(;2=l3D`xye%xrpD(q#RA5cUYpT|{J_DluiNWKzLsuL|X2vt2?r*1HiVK^;U- zskc6BQheOJ zpNA~+LS%y4O*3xCK1Fo#+Pc8lV~Qmkrw*@mXz@SPcL;>>s(xkE3JD7E_tQpWwgK#4 z0cH~%AnXM2d*zvx0J76o?mvMZTlhhx7elgK4w!rpyWO-|O7Jm(u$%tITUkMr^HzH~ zwo|KhPEdSWM?zl0h*JM{>p0m(U9JW7UB-hQKl4AzR+q*HIaGc`J?njhWw(j+s4Rjg zJw`dfTdR988I@j^NW$F`mh(OpuB1yW5Ml`)31qOEdjUk1LJi{{qfrjCa1 zcVdN6sRfBkQo8jH&R}S!%KqFAJ&LW3&2c`L%bi~uX>7F5v~qEA@xJdU>&!AvqogAt z5Dj}E#|_a*d!L$(%{PmO(iz&6=4Vqk1n=Fs^>b~N!ri5gvZalL4quLF<^83u{B^!B zU8sP}n^VF=%*@sE=HVES4qnG~R>q=?61I~8rMz~?1kMT7qUAc~Rqc^>d4>3BChUG_mPcy7gNB)ML#St<(gDmQs9u?A>QdqF z;lax)d1vF^cw;tQ&Mb5a(vKrW0{yr(tU15E(sJ^A-2B{AgunuMA3>wW&69si&i>Hj zN(U`=(ZTd-Z@m0+C#fi04hSv$Z(1-;#=7b$MXocfl3vNwlcBI%Xa8{qh`LLJo2s40 z!mJ+Zz)X?X;?YK{W_`cL8=i}SVp*Etb->CKNnj3`yy$_o?Ep2Q){SN!KLuCpT_*OS zo@`b!p*d|M)#}Vw3Bi-SA$=GHJD!c_%GvJZ+EM4mP|ZKFvjL z+W|H$xQ_&hAT>((bH*h0cXv?!Ef}y#Inh8~xE!sreliprgayscQ3{uxF2@qSe%X(E zOIkY%WiM)WT?YFLp0_t{Yqi5e{3F|&i1632DzwpFm84%f8V@fqQv>u}7h=DN0D~JW zFs1P`&qpokk3JCKDKZGhpkS>UH>NY{QqtXuJPyirx#F>kldGYh7i% Date: Wed, 11 Nov 2026 02:45:26 -0800 Subject: [PATCH 13/16] Rig Bay integration: wait out stand animations in tests, keep chains and lamps right when a stand or mech goes, move a work line to Wick's bench --- voidcrew/modules/trade/rig_bay/rig_bay_stand_visuals.dm | 8 ++++++-- voidcrew/modules/trade/rig_bay/rig_bay_wick.dm | 1 + voidcrew/modules/trade/shop_catalog_suit_vendor.dm | 2 -- voidcrew/modules/unit_tests/voidcrew_rig_bay.dm | 9 +++++++++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_stand_visuals.dm b/voidcrew/modules/trade/rig_bay/rig_bay_stand_visuals.dm index dd545e8c976..03bb20d2d97 100644 --- a/voidcrew/modules/trade/rig_bay/rig_bay_stand_visuals.dm +++ b/voidcrew/modules/trade/rig_bay/rig_bay_stand_visuals.dm @@ -43,8 +43,9 @@ update_appearance(UPDATE_OVERLAYS) /obj/structure/rig_bay_stand/Destroy() + // The parent forgets the mech first; forget_mech() below leaves the chains alone while the stand is going. + . = ..() QDEL_LIST(chains) - return ..() /// Makes sure both chains exist and stand on the stand's tile. Rebuilds any that were deleted or moved off. /obj/structure/rig_bay_stand/proc/ensure_chains() @@ -97,6 +98,8 @@ /// A mech that leaves (driven off, deleted) takes nothing with it: the chains go back up to the bar. /obj/structure/rig_bay_stand/forget_mech() ..() + if(QDELING(src)) + return ensure_chains() for(var/obj/effect/rig_bay_chain/chain as anything in chains) chain.set_pose(chain.anchor_x, RIG_BAY_CHAIN_STOWED, RIG_BAY_CHAIN_TOP - RIG_BAY_HANG_LIFT) @@ -142,7 +145,8 @@ /obj/structure/rig_bay_stand/proc/end_release(obj/vehicle/sealed/mecha/mech, datum/callback/on_done) if(!QDELETED(mech)) mech.pixel_z = 0 - set_lamp("green") + if(mech == held_mech) + set_lamp("green") on_done?.Invoke() /** diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm b/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm index c1316bb9120..c1a2a048aab 100644 --- a/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm +++ b/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm @@ -556,6 +556,7 @@ "Hydraulics are weeping again.", "Come on. Come on. There.", "Who keeps parking these in the rain?", + "Somebody's been feeding this one plasma cutter grit.", )) /// A welding job is the one with sparks on diff --git a/voidcrew/modules/trade/shop_catalog_suit_vendor.dm b/voidcrew/modules/trade/shop_catalog_suit_vendor.dm index 1fe3d89652c..11e109618fc 100644 --- a/voidcrew/modules/trade/shop_catalog_suit_vendor.dm +++ b/voidcrew/modules/trade/shop_catalog_suit_vendor.dm @@ -245,8 +245,6 @@ "Bench install is free. Doing it yourself in a corridor is how people lose fingers.", "The civilian chassis is not spaceworthy. It says so on the plate. People still ask.", "Bring me a wrecked control unit and I'll take it, modules and all.", - "Hold still. Not you. The Ripley.", - "Somebody's been feeding this one plasma cutter grit.", "Don't lean on the chains.", ), TRADER_LINE_RESTOCK = list( diff --git a/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm b/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm index 8d9711701aa..f88a90575b7 100644 --- a/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm +++ b/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm @@ -26,6 +26,12 @@ allocated += mech return mech +/// Waits out a stand's release or beam-in animation (at most 5 s) until it reaches `state`. +/datum/unit_test/voidcrew_rig_bay/proc/wait_for_stand(obj/structure/rig_bay_stand/stand, state) + var/give_up = world.time + 5 SECONDS + while(stand.stand_state != state && world.time < give_up) + sleep(1) + /// Every stand mech spawns with the kit its line lists. /datum/unit_test/voidcrew_rig_bay/roster/Run() var/datum/outpost_shop/vendor/suit_fitter/shop = allocate(/datum/outpost_shop/vendor/suit_fitter) @@ -198,6 +204,7 @@ for(var/obj/structure/rig_bay_stand/stand as anything in list(work, parked, combat)) TEST_ASSERT(stand.release_mech(buyer), "Bay [stand.bay_number] wouldn't release its mech") + wait_for_stand(stand, "sold_on_stand") TEST_ASSERT_EQUAL(stand.stand_state, "sold_on_stand", "Bay [stand.bay_number] didn't finish lowering") work_mech.forceMove(bay_turf(1, 2)) combat_mech.forceMove(bay_turf(3, 2)) @@ -205,6 +212,7 @@ TEST_ASSERT_EQUAL(combat.stand_state, "empty", "Driving off didn't empty bay 3") shop.convoy_restock() + wait_for_stand(work, "clamped") TEST_ASSERT_EQUAL(work.stand_state, "clamped", "The work stand didn't refill on the convoy") TEST_ASSERT_EQUAL(work.sku?.stock, 1, "The refilled work stand isn't in stock") TEST_ASSERT(work.sku in shop.skus, "The refilled line isn't in the shop") @@ -218,6 +226,7 @@ TEST_ASSERT_EQUAL(rig_bay_lines, 3, "The shop lists the wrong number of stand lines") shop.convoy_restock() + wait_for_stand(combat, "clamped") TEST_ASSERT_EQUAL(combat.stand_state, "clamped", "The combat stand didn't refill on the second convoy") allocated += combat.held_mech TEST_ASSERT(ispath(combat.sku?.item_path, /obj/vehicle/sealed/mecha/gygax) || ispath(combat.sku?.item_path, /obj/vehicle/sealed/mecha/durand), "The combat stand dealt a work mech") From 714d73f08393468680fc8a6e36037978a6ef2c16 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 03:30:03 -0800 Subject: [PATCH 14/16] Rig Bay: turn the stand lamps on the client in step with the animation, drop the beam-in sparks to the feet --- .../trade/rig_bay/rig_bay_stand_visuals.dm | 109 +++++++++++++----- 1 file changed, 83 insertions(+), 26 deletions(-) diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_stand_visuals.dm b/voidcrew/modules/trade/rig_bay/rig_bay_stand_visuals.dm index 03bb20d2d97..4d6fa8b4541 100644 --- a/voidcrew/modules/trade/rig_bay/rig_bay_stand_visuals.dm +++ b/voidcrew/modules/trade/rig_bay/rig_bay_stand_visuals.dm @@ -2,18 +2,27 @@ // (chains lower the mech, then wind up) and the beam-in (a blue-white column, the mech fades in, the chains // come down and hoist it). Logic lives in rig_bay_stand.dm; this file only draws and plays sounds. // -// Draw order on the stand's tile (all on one turf, so they sort by layer; everything moves by pixel_z/pixel_w, -// never pixel_y, so nothing changes rows): -// stand pad (2.9) < chains (RIG_BAY_CHAIN_LAYER) < gantry + post lamps (RIG_BAY_GANTRY_LAYER) -// < beam column (RIG_BAY_BEAM_LAYER) < mech (VEHICLE_LAYER) < flash, sparks (RIG_BAY_FRONT_LAYER) < mobs +// Draw order on the stand's tile. The world uses SIDE_MAP (code/world.dm), which sorts by screen row (turf y plus +// pixel_y) before layer. Every part sits on the stand's own turf and moves by pixel_z/pixel_w only, never +// pixel_y, so all of them stay on the stand's row and sort among themselves by layer: +// stand pad (2.9) < chains (RIG_BAY_CHAIN_LAYER) < gantry (RIG_BAY_GANTRY_LAYER) < post lamps +// (RIG_BAY_LAMP_LAYER) < beam column (RIG_BAY_BEAM_LAYER) < mech (VEHICLE_LAYER) +// < flash, sparks (RIG_BAY_FRONT_LAYER) < mobs // The chains sit behind the mech, so their shackles tuck behind its shoulders, and behind the gantry bar, so // their tops tuck into it. The flash and sparks sit just in front of the mech and still behind anyone -// standing on the stand's own tile row. +// standing on the stand's own tile row. The gantry bar and the beam column reach up over the tiles north of +// the stand, but stay on its row, so anyone south of the stand draws in front of them. +// +// Timing: every change that has to land on a beat (mech, chains, column, flash, sparks, lamps) is queued with +// animate() when the move starts, so the client plays it on time even when the server lags. The server-side +// timers at the end only settle state for logic. /// Chains hang behind the gantry and the mech. #define RIG_BAY_CHAIN_LAYER 3.86 -/// The gantry posts, bar and lamps: in front of the chains, behind the beam and the mech. +/// The gantry posts and bar: in front of the chains, behind the lamps, the beam and the mech. #define RIG_BAY_GANTRY_LAYER 3.87 +/// The lamps on the gantry posts: just in front of the posts. +#define RIG_BAY_LAMP_LAYER 3.875 /// The beam-in column: in front of the gantry, behind the mech. #define RIG_BAY_BEAM_LAYER 3.88 /// The arrival flash and the sparks at the mech's feet: in front of the mech, behind mobs. @@ -34,7 +43,10 @@ /obj/structure/rig_bay_stand /// The two chains hanging from the gantry, left then right (screen space). var/list/obj/effect/rig_bay_chain/chains + /// The two lamps on the gantry posts, left then right. + var/list/obj/effect/rig_bay_lamp/lamps /// Colour of the lamps on the gantry posts: "red" (held), "amber" (working), "green" (released), "grey" (empty). + /// The server's state; a release or beam-in queues the lamps' own change on the client ahead of it. var/lamp = "grey" /obj/structure/rig_bay_stand/Initialize(mapload) @@ -46,8 +58,10 @@ // The parent forgets the mech first; forget_mech() below leaves the chains alone while the stand is going. . = ..() QDEL_LIST(chains) + QDEL_LIST(lamps) -/// Makes sure both chains exist and stand on the stand's tile. Rebuilds any that were deleted or moved off. +/// Makes sure both chains and both lamps exist and stand on the stand's tile. Rebuilds any that were deleted +/// or moved off. /obj/structure/rig_bay_stand/proc/ensure_chains() LAZYINITLIST(chains) for(var/obj/effect/rig_bay_chain/chain as anything in chains.Copy()) @@ -60,6 +74,18 @@ var/obj/effect/rig_bay_chain/chain = new(loc) chain.set_pose(default_anchors[length(chains) + 1], RIG_BAY_CHAIN_STOWED, RIG_BAY_CHAIN_TOP - RIG_BAY_HANG_LIFT) chains += chain + LAZYINITLIST(lamps) + for(var/obj/effect/rig_bay_lamp/post_lamp as anything in lamps.Copy()) + if(QDELETED(post_lamp) || post_lamp.loc != loc) + lamps -= post_lamp + if(!QDELETED(post_lamp)) + qdel(post_lamp) + var/static/list/lamp_spots = list(-5, 35) + while(length(lamps) < 2) + var/obj/effect/rig_bay_lamp/post_lamp = new(loc) + post_lamp.pixel_w = lamp_spots[length(lamps) + 1] + post_lamp.show(lamp) + lamps += post_lamp /obj/structure/rig_bay_stand/update_overlays() . = ..() @@ -75,25 +101,22 @@ part.pixel_w = offset[1] part.pixel_z = offset[2] . += part - // A lamp on each post, just under the bar. - for(var/lamp_x in list(-5, 35)) - var/mutable_appearance/light = mutable_appearance(icon_file, "lamp_[lamp]", RIG_BAY_GANTRY_LAYER, src) - light.pixel_w = lamp_x - light.pixel_z = 20 - . += light - if(lamp == "grey") - continue - var/mutable_appearance/glow = emissive_appearance(icon_file, "lamp_[lamp]", src, RIG_BAY_GANTRY_LAYER) - glow.pixel_w = lamp_x - glow.pixel_z = 20 - . += glow - -/// Sets the post lamps. + +/// Sets the post lamps now. Drops any lamp change still queued on the client, unless the lamps already show +/// (or are queued to show) this colour. /obj/structure/rig_bay_stand/proc/set_lamp(new_lamp) - if(lamp == new_lamp) - return lamp = new_lamp - update_appearance(UPDATE_OVERLAYS) + ensure_chains() + for(var/obj/effect/rig_bay_lamp/post_lamp as anything in lamps) + post_lamp.show(new_lamp) + +/// Queues the post lamps to turn `new_lamp` `delay` ds from now, on the client, in step with the move's other +/// animate() chains. Only between lit colours. The server's `lamp` follows later through set_lamp(). +/obj/structure/rig_bay_stand/proc/queue_lamp(new_lamp, delay) + ensure_chains() + for(var/obj/effect/rig_bay_lamp/post_lamp as anything in lamps) + animate(post_lamp, time = delay) + animate(icon_state = "lamp_[new_lamp]", time = 1) /// A mech that leaves (driven off, deleted) takes nothing with it: the chains go back up to the bar. /obj/structure/rig_bay_stand/forget_mech() @@ -139,9 +162,11 @@ animate(pixel_z = top - lift, time = 4) chain.queue_length_steps(length, RIG_BAY_CHAIN_STOWED, 4) chain.length = RIG_BAY_CHAIN_STOWED + queue_lamp("green", 11) addtimer(CALLBACK(src, PROC_REF(end_release), mech, on_done), 1.1 SECONDS) -/// Release end: makes sure the mech is on the floor, lamps green, then hands over. +/// Release end: makes sure the mech is on the floor and the lamps are green (the client already turned them), +/// then hands over. /obj/structure/rig_bay_stand/proc/end_release(obj/vehicle/sealed/mecha/mech, datum/callback/on_done) if(!QDELETED(mech)) mech.pixel_z = 0 @@ -203,6 +228,7 @@ animate(pixel_z = RIG_BAY_CHAIN_TOP, time = 4) chain.anchor_x = anchor[1] chain.length = hang + queue_lamp("red", 30) addtimer(CALLBACK(src, PROC_REF(end_beam_in), mech, on_done), 3 SECONDS) /// Beam-in end: settles the hang pose (lamps red), then hands over. @@ -302,6 +328,36 @@ for(var/step in 1 to steps) animate(icon_state = "chain_[round(from_length + (to_length - from_length) * step / steps)]", time = time / steps) +/// One lamp on a Rig Bay gantry post, just under the bar. Pure visual; the stand owns it. A separate object +/// (not an overlay) so a release or beam-in can queue its colour change with animate(). +/obj/effect/rig_bay_lamp + name = "lamp" + icon = 'voidcrew/modules/trade/icons/rig_bay_stand.dmi' + icon_state = "lamp_grey" + layer = RIG_BAY_LAMP_LAYER + anchored = TRUE + pixel_z = 20 + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + blocks_emissive = EMISSIVE_BLOCK_NONE + /// Whether the glow overlay is on. Every lit colour has the same shape, so one glow serves them all. + var/lit = FALSE + +/// Shows `colour` now. Does nothing when the lamp already shows it or is queued to (animate() sets the end +/// state on the server at once), so a server-side settle never cuts a queued change short. +/obj/effect/rig_bay_lamp/proc/show(colour) + var/new_state = "lamp_[colour]" + var/now_lit = colour != "grey" + if(icon_state == new_state && lit == now_lit) + return + animate(src) + icon_state = new_state + if(lit == now_lit) + return + lit = now_lit + cut_overlays() + if(lit) + add_overlay(emissive_appearance(icon, "lamp_red", src)) + /// The blue-white column a restocked mech beams in through: 1.5 tiles wide, 3 tall, standing on the pad. /// Sorts on the pad's row (pixel_z only), so it stays behind the mech and anything south of the stand. /obj/effect/rig_bay_beam_column @@ -338,7 +394,7 @@ icon = 'icons/effects/effects.dmi' icon_state = "nothing" layer = RIG_BAY_FRONT_LAYER - pixel_z = -6 + pixel_z = -10 duration = 3 SECONDS /obj/effect/temp_visual/rig_bay_spark/Initialize(mapload) @@ -347,6 +403,7 @@ #undef RIG_BAY_CHAIN_LAYER #undef RIG_BAY_GANTRY_LAYER +#undef RIG_BAY_LAMP_LAYER #undef RIG_BAY_BEAM_LAYER #undef RIG_BAY_FRONT_LAYER #undef RIG_BAY_CHAIN_TOP From 4a6436790a958b7d41ca55bb7c1d7c690de29b15 Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 03:30:04 -0800 Subject: [PATCH 15/16] Rig Bay: Wick only works on a mech once it hangs on its stand --- voidcrew/modules/trade/rig_bay/rig_bay_wick.dm | 8 ++++++-- voidcrew/modules/unit_tests/voidcrew_rig_bay_wick.dm | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm b/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm index c1a2a048aab..064bc8b7fea 100644 --- a/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm +++ b/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm @@ -533,9 +533,13 @@ /// Played instead of `sounds` on a welding job var/list/weld_sounds = list('sound/items/tools/welder.ogg', 'sound/items/tools/welder2.ogg') -/// Only a mech hanging on its stand, never one that's sold or driven in by a customer +/// Only a mech hanging on its stand, never one that's beaming in, sold or driven in by a customer. +/// The clamp trait goes on at arrival, so the stand's state is checked too. /datum/outpost_ambient_work/mech_service/accepts(atom/target) - return ..() && HAS_TRAIT(target, TRAIT_RIG_BAY_CLAMPED) + if(!..() || !HAS_TRAIT(target, TRAIT_RIG_BAY_CLAMPED)) + return FALSE + var/obj/structure/rig_bay_stand/stand = locate() in get_turf(target) + return stand?.held_mech == target && stand.stand_state == RIG_BAY_STAND_CLAMPED /datum/outpost_ambient_work/mech_service/start(datum/component/outpost_ambient_worker/worker, atom/target) var/atom/movable/body = worker.parent diff --git a/voidcrew/modules/unit_tests/voidcrew_rig_bay_wick.dm b/voidcrew/modules/unit_tests/voidcrew_rig_bay_wick.dm index 5f28005e191..40470752c4f 100644 --- a/voidcrew/modules/unit_tests/voidcrew_rig_bay_wick.dm +++ b/voidcrew/modules/unit_tests/voidcrew_rig_bay_wick.dm @@ -35,6 +35,14 @@ var/turf/lane = get_step(stand_tile, SOUTH) var/obj/vehicle/sealed/mecha/mech = allocate(/obj/vehicle/sealed/mecha/ripley, stand_tile) ADD_TRAIT(mech, "rig_bay_clamped", "rig_bay") + stand.held_mech = mech + // The chains' trait goes on as a mech starts beaming in: he leaves it alone until it hangs + var/datum/outpost_ambient_work/mech_service/service = new + stand.stand_state = "arriving" + TEST_ASSERT(!service.accepts(mech), "Wick would work on a mech that is still beaming in") + stand.stand_state = "clamped" + TEST_ASSERT(service.accepts(mech), "Wick won't work on a hanging mech") + qdel(service) var/turf/home = at(4, 2) var/mob/living/basic/outpost_trader/suit_fitter/wick = allocate(/mob/living/basic/outpost_trader/suit_fitter, home) wick.start_rig_bay_routine() From 3174e81edb840e668b602dc3cd41d18c1c6677de Mon Sep 17 00:00:00 2001 From: Jackriip <97582790+Jackriip@users.noreply.github.com> Date: Wed, 11 Nov 2026 13:40:22 -0800 Subject: [PATCH 16/16] Rig Bay: add a showpiece stand that sells one super mech a round Bay 4 deals a H.O.N.K, Paddy, Justice, Phazon, Savannah-Ivanov or Marauder, sold as built at advanced reputation. The pool deals once per round for the whole server, and each crew can buy one showpiece a round. --- tools/gear_catalog/exceptions.json | 24 ++++ tools/gear_catalog/ratings/exosuits.json | 62 +++++++++-- voidcrew/_DEFINES/rig_bay.dm | 2 + .../outposts/trader_outpost_outfitter.dmm | 105 +++++++++--------- .../modules/trade/rig_bay/rig_bay_shop.dm | 1 + .../modules/trade/rig_bay/rig_bay_skus.dm | 69 ++++++++++++ .../modules/trade/rig_bay/rig_bay_stand.dm | 23 +++- .../modules/unit_tests/voidcrew_rig_bay.dm | 91 ++++++++++++++- .../modules/unit_tests/voidcrew_zone_shops.dm | 8 +- wiki/content/trader-outposts.md | 1 + 10 files changed, 319 insertions(+), 67 deletions(-) diff --git a/tools/gear_catalog/exceptions.json b/tools/gear_catalog/exceptions.json index 10192c66af9..1303eb9b639 100644 --- a/tools/gear_catalog/exceptions.json +++ b/tools/gear_catalog/exceptions.json @@ -538,5 +538,29 @@ "rule": "above_on_route", "route": "/datum/design/beamrifle", "reason": "Deliberate: reputation 22 and six anomaly cores are a real gate, so the Event Horizon stays craftable." + }, + { + "path": "/obj/vehicle/sealed/mecha/honker", + "rule": "price_out_of_band", + "route": "/datum/shop_sku/rig_bay_mech/showpiece/honker", + "reason": "Deliberate: owner's showpiece stand pricing. Every super mech sells at 25,000 to 31,000 credits plus 3 vouchers, one a round, whatever its tier." + }, + { + "path": "/obj/vehicle/sealed/mecha/justice/loaded", + "rule": "above_on_route", + "route": "/datum/shop_sku/rig_bay_mech/showpiece/justice", + "reason": "Deliberate: owner's decision to sell the super mechs as they are on the showpiece stand, one a round for the server and one per crew, at advanced reputation." + }, + { + "path": "/obj/vehicle/sealed/mecha/marauder/loaded", + "rule": "above_on_route", + "route": "/datum/shop_sku/rig_bay_mech/showpiece/marauder", + "reason": "Deliberate: owner's decision to sell the super mechs as they are on the showpiece stand, one a round for the server and one per crew, at advanced reputation." + }, + { + "path": "/obj/vehicle/sealed/mecha/ripley/paddy/preset", + "rule": "price_out_of_band", + "route": "/datum/shop_sku/rig_bay_mech/showpiece/paddy", + "reason": "Deliberate: owner's showpiece stand pricing. Every super mech sells at 25,000 to 31,000 credits plus 3 vouchers, one a round, whatever its tier." } ] diff --git a/tools/gear_catalog/ratings/exosuits.json b/tools/gear_catalog/ratings/exosuits.json index cf43405d4a3..482b64b2298 100644 --- a/tools/gear_catalog/ratings/exosuits.json +++ b/tools/gear_catalog/ratings/exosuits.json @@ -32,6 +32,33 @@ "reason": "The entry combat mech, sold bare: 250 health, 20 bullet and 30 laser armor, with an overclock for speed. Rated as its chassis.", "tier": "yellow" }, + "/obj/vehicle/sealed/mecha/honker": { + "confidence": "medium", + "group": "combat exosuits", + "hash": "", + "name": "H.O.N.K", + "rank": 7, + "reason": "Clown mech, sold bare: 140 health and no armor, weaker than a Gygax. It takes any combat gun, but the Honker that makes it dangerous is not included.", + "tier": "yellow" + }, + "/obj/vehicle/sealed/mecha/justice/loaded": { + "confidence": "high", + "group": "combat exosuits", + "hash": "", + "name": "Justice", + "rank": 1, + "reason": "Sold with its stock thrusters, radio, air tank and top parts. Reflects every shot while its shields are up, cloaks, charges and beheads anyone downed. Rated as its chassis; the stock syndicate lock is removed at sale.", + "tier": "above" + }, + "/obj/vehicle/sealed/mecha/marauder/loaded": { + "confidence": "high", + "group": "combat exosuits", + "hash": "", + "name": "Marauder", + "rank": 1, + "reason": "Sold with its stock pulse rifle, missile rack, thrusters, smoke and top parts: 500 health, 60 bullet and laser armor and 70 melee. Well past the Durand in every number.", + "tier": "above" + }, "/obj/vehicle/sealed/mecha/odysseus": { "confidence": "high", "group": "working and medical exosuits", @@ -41,6 +68,15 @@ "reason": "Medical mech, sold with a mounted sleeper. Rated as its chassis.", "tier": "green" }, + "/obj/vehicle/sealed/mecha/phazon": { + "confidence": "medium", + "group": "combat exosuits", + "hash": "", + "name": "Phazon", + "rank": 2, + "reason": "Sold bare: 200 health, 30 armor all round, walks through walls and switches damage type. Rated as its chassis.", + "tier": "red" + }, "/obj/vehicle/sealed/mecha/ripley": { "confidence": "high", "group": "working and medical exosuits", @@ -58,31 +94,43 @@ "rank": 2, "reason": "The enclosed Ripley, sold with a hydraulic clamp and a drill. Rated as the MK-II conversion kit on a Ripley.", "tier": "green" + }, + "/obj/vehicle/sealed/mecha/ripley/paddy/preset": { + "confidence": "medium", + "group": "combat exosuits", + "hash": "", + "name": "APLU \"Paddy\"", + "rank": 6, + "reason": "Security Ripley sold with its stock disabler, cuffing claw and four-person cell: 250 health, faster than a Gygax, open cage. Takes people out of a fight rather than killing them. The stock security lock is removed at sale.", + "tier": "yellow" + }, + "/obj/vehicle/sealed/mecha/savannah_ivanov": { + "confidence": "high", + "group": "combat exosuits", + "hash": "", + "name": "Savannah-Ivanov", + "rank": 1, + "reason": "Sold bare: 450 health, 40 bullet armor, leaps and flattens walls and anyone downed where it lands, and calls a missile strike. Needs two pilots. Rated as its chassis.", + "tier": "red" } }, "method": "A whole mech rates as its chassis does in mech.json: how many vested men it takes to match its health and armor. Its tg default kit and the stand's working kit are tools, not weapons; arms are sold and rated separately.", "unrated": { "/obj/vehicle/sealed/mecha": "abstract", "/obj/vehicle/sealed/mecha/gygax/dark": "no route", - "/obj/vehicle/sealed/mecha/honker": "no route", "/obj/vehicle/sealed/mecha/honker/dark": "no route", "/obj/vehicle/sealed/mecha/honker/dark/loaded": "no route", "/obj/vehicle/sealed/mecha/justice": "no route", - "/obj/vehicle/sealed/mecha/justice/loaded": "no route", "/obj/vehicle/sealed/mecha/marauder": "no route", - "/obj/vehicle/sealed/mecha/marauder/loaded": "no route", "/obj/vehicle/sealed/mecha/marauder/mauler": "no route", "/obj/vehicle/sealed/mecha/marauder/mauler/loaded": "no route", "/obj/vehicle/sealed/mecha/marauder/seraph": "no route", - "/obj/vehicle/sealed/mecha/phazon": "no route", "/obj/vehicle/sealed/mecha/reticence": "no route", "/obj/vehicle/sealed/mecha/reticence/loaded": "no route", "/obj/vehicle/sealed/mecha/ripley/cargo": "no route", "/obj/vehicle/sealed/mecha/ripley/deathripley": "no route", "/obj/vehicle/sealed/mecha/ripley/deathripley/real": "no route", "/obj/vehicle/sealed/mecha/ripley/mining": "no route", - "/obj/vehicle/sealed/mecha/ripley/paddy": "no route", - "/obj/vehicle/sealed/mecha/ripley/paddy/preset": "no route", - "/obj/vehicle/sealed/mecha/savannah_ivanov": "no route" + "/obj/vehicle/sealed/mecha/ripley/paddy": "no route" } } diff --git a/voidcrew/_DEFINES/rig_bay.dm b/voidcrew/_DEFINES/rig_bay.dm index f0efc0a3a10..1b56f3cb54d 100644 --- a/voidcrew/_DEFINES/rig_bay.dm +++ b/voidcrew/_DEFINES/rig_bay.dm @@ -14,6 +14,8 @@ #define RIG_BAY_HOLD_TIME (20 MINUTES) /// Pixels a chained mech hangs above its stand. Release lowers it back to 0. #define RIG_BAY_HANG_LIFT 4 +/// Shared per-crew purchase key for the showpiece stand's whole line (voidcrew_crew_record favor_unique_purchases). +#define RIG_BAY_SHOWPIECE_CAP "rig_bay_showpiece" /// How often a due restock retries while something blocks the stand tile. #define RIG_BAY_RETRY_DELAY (10 SECONDS) diff --git a/voidcrew/_maps/map_files/outposts/trader_outpost_outfitter.dmm b/voidcrew/_maps/map_files/outposts/trader_outpost_outfitter.dmm index 8efec13791c..4979953fbeb 100644 --- a/voidcrew/_maps/map_files/outposts/trader_outpost_outfitter.dmm +++ b/voidcrew/_maps/map_files/outposts/trader_outpost_outfitter.dmm @@ -883,10 +883,6 @@ }, /turf/open/indestructible/dark/smooth_large, /area/voidcrew/trader_outpost) -"ch" = ( -/obj/machinery/light/directional/north, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "ci" = ( /obj/machinery/light/directional/north, /turf/open/indestructible/large, @@ -957,36 +953,6 @@ /obj/effect/decal/cleanable/blood/oil, /turf/open/indestructible/dark/textured, /area/voidcrew/trader_outpost) -"cr" = ( -/obj/structure/table/reinforced, -/obj/structure/showcase{ - density = 0; - desc = "A display piece, bolted to the table."; - icon = 'icons/obj/clothing/modsuit/mod_construction.dmi'; - icon_state = "chestplate"; - name = "MOD chestplate"; - pixel_x = -5; - pixel_y = 4 - }, -/obj/structure/showcase{ - density = 0; - desc = "A display helmet. The visor is a sticker."; - icon = 'icons/obj/clothing/modsuit/mod_construction.dmi'; - icon_state = "helmet"; - name = "MOD helmet"; - pixel_x = 6; - pixel_y = 2 - }, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) -"cs" = ( -/obj/machinery/porta_turret/outpost, -/obj/effect/turf_decal/box/red/corners{ - dir = 8 - }, -/obj/effect/turf_decal/box/red/corners, -/turf/open/indestructible/dark/textured, -/area/voidcrew/trader_outpost) "ct" = ( /obj/structure/showcase{ density = 1; @@ -1081,18 +1047,6 @@ }, /turf/open/indestructible, /area/voidcrew/trader_outpost) -"cE" = ( -/obj/structure/showcase{ - density = 1; - desc = "A sealed MOD suit locker. Wick has the key."; - icon = 'icons/obj/machines/suit_storage.dmi'; - icon_state = "industrial"; - name = "suit locker" - }, -/obj/effect/turf_decal/bot, -/obj/structure/sign/poster/contraband/borg_fancy_1/directional/east, -/turf/open/indestructible/large, -/area/voidcrew/trader_outpost) "cF" = ( /obj/structure/table/reinforced, /obj/structure/showcase{ @@ -1638,6 +1592,14 @@ /obj/effect/decal/cleanable/dirt, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) +"gk" = ( +/obj/machinery/light/directional/north, +/obj/effect/turf_decal/stripes/white/line{ + dir = 8 + }, +/obj/effect/decal/cleanable/dirt, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "gD" = ( /obj/effect/turf_decal/siding/wideplating_new/dark/corner, /obj/effect/turf_decal/siding/wideplating_new/dark/corner{ @@ -2196,6 +2158,12 @@ /obj/machinery/light/directional/north, /turf/open/indestructible/dark/smooth_large, /area/voidcrew/trader_outpost) +"pK" = ( +/obj/effect/turf_decal/stripes/white/line{ + dir = 4 + }, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "pN" = ( /obj/effect/spawner/random/vending/snackvend, /obj/effect/turf_decal/siding/wideplating_new/dark{ @@ -2513,6 +2481,15 @@ /obj/machinery/light/directional/west, /turf/open/indestructible/white/smooth_large, /area/voidcrew/trader_outpost) +"up" = ( +/obj/structure/rig_bay_stand{ + bay_number = 4; + dir = 2; + pool = /datum/shop_rotation/rig_bay/showpiece + }, +/obj/effect/turf_decal/delivery/white, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) "uC" = ( /obj/effect/turf_decal/siding/thinplating_new/dark{ dir = 8 @@ -2745,6 +2722,15 @@ }, /turf/open/indestructible/dark, /area/voidcrew/trader_outpost) +"xL" = ( +/obj/machinery/porta_turret/outpost, +/obj/effect/turf_decal/box/red/corners{ + dir = 8 + }, +/obj/effect/turf_decal/box/red/corners, +/obj/structure/sign/poster/contraband/borg_fancy_1/directional/east, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) "xZ" = ( /obj/effect/turf_decal/stripes{ dir = 4 @@ -2799,6 +2785,15 @@ /obj/item/kirbyplants/organic/plant22, /turf/open/indestructible, /area/voidcrew/trader_outpost) +"zf" = ( +/obj/effect/turf_decal/stripes/white/line{ + dir = 1 + }, +/obj/effect/turf_decal/caution/stand_clear/white{ + dir = 2 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) "zw" = ( /obj/structure/chair{ dir = 1 @@ -3079,6 +3074,12 @@ }, /turf/open/indestructible/large, /area/voidcrew/trader_outpost) +"Dq" = ( +/obj/effect/turf_decal/arrows{ + dir = 8 + }, +/turf/open/indestructible/large, +/area/voidcrew/trader_outpost) "Dy" = ( /mob/living/basic/outpost_trader, /obj/structure/chair/office/light{ @@ -5318,8 +5319,8 @@ aV (26,1,1) = {" ao dU -cr -Xe +pK +Dq Xe Xe Xe @@ -5360,8 +5361,8 @@ aV (27,1,1) = {" ao dU -ch -Xe +up +zf Xe de ks @@ -5402,8 +5403,8 @@ aV (28,1,1) = {" ao dU -cs -cE +gk +xL dd df dg diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_shop.dm b/voidcrew/modules/trade/rig_bay/rig_bay_shop.dm index a77ab28699d..a3ba5c53a6a 100644 --- a/voidcrew/modules/trade/rig_bay/rig_bay_shop.dm +++ b/voidcrew/modules/trade/rig_bay/rig_bay_shop.dm @@ -6,6 +6,7 @@ rotations = list( /datum/shop_rotation/rig_bay/work, /datum/shop_rotation/rig_bay/combat, + /datum/shop_rotation/rig_bay/showpiece, /datum/shop_rotation/rig_bay_arms, ) /// The Rig Bay stands linked to this shop. Each lists its mech as one line under Exosuits. diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_skus.dm b/voidcrew/modules/trade/rig_bay/rig_bay_skus.dm index ff36fa7c4a6..441c00c9dbe 100644 --- a/voidcrew/modules/trade/rig_bay/rig_bay_skus.dm +++ b/voidcrew/modules/trade/rig_bay/rig_bay_skus.dm @@ -12,6 +12,8 @@ slots = 0 /// Convoys an empty stand of this pool waits before it takes a new mech. 1 refills on every convoy. var/convoys_per_refill = 1 + /// Mechs this pool may deal in a round, across every stand on the server. 0 means no limit. + var/deals_per_round = 0 /// Working mechs. Refills every convoy. /datum/shop_rotation/rig_bay/work @@ -30,6 +32,18 @@ /datum/shop_sku/rig_bay_mech/durand = 1, ) +/// Super mechs for the showpiece stand. It deals one a round for the whole server and never refills. +/datum/shop_rotation/rig_bay/showpiece + deals_per_round = 1 + pool = list( + /datum/shop_sku/rig_bay_mech/showpiece/honker = 1, + /datum/shop_sku/rig_bay_mech/showpiece/paddy = 1, + /datum/shop_sku/rig_bay_mech/showpiece/justice = 1, + /datum/shop_sku/rig_bay_mech/showpiece/phazon = 1, + /datum/shop_sku/rig_bay_mech/showpiece/savannah_ivanov = 1, + /datum/shop_sku/rig_bay_mech/showpiece/marauder = 1, + ) + /// Yellow exosuit arms: four of the pool on the shelf at once, one each. /datum/shop_rotation/rig_bay_arms slots = 4 @@ -150,6 +164,61 @@ price_vouchers = 3 crew_limit = 1 +// ===== SHOWPIECE MECHS ===== + +/** + * Super mechs, sold as tg builds them: their stock kit, or bare where tg gives them none. + * One per crew per round across the whole line, on top of each mech's own cap of 1. + * Each subtype sets its own gate so the abstract base stays out of the reputation catalog. + */ +/datum/shop_sku/rig_bay_mech/showpiece + crew_limit = 1 + price_vouchers = 3 + +/datum/shop_sku/rig_bay_mech/showpiece/get_favor_denial(mob/living/user, obj/structure/overmap/ship/crew_ship = get_crew_ship(user)) + . = ..() + if(.) + return + var/datum/voidcrew_crew_record/crew = crew_ship?.get_crew_record() + if((crew?.favor_unique_purchases[RIG_BAY_SHOWPIECE_CAP] || 0) >= 1) + return "I've filled that order already." + +/// The shop has charged and nothing in between can yield, so the shared cap is taken only on a real hand-over. +/datum/shop_sku/rig_bay_mech/showpiece/dispense(mob/living/user, mob/living/basic/outpost_trader/vendor) + . = ..() + if(.) + get_crew_ship(user)?.get_crew_record()?.claim_purchase(RIG_BAY_SHOWPIECE_CAP, 1) + +/datum/shop_sku/rig_bay_mech/showpiece/honker + item_path = /obj/vehicle/sealed/mecha/honker + zone_permission = ZONE_PERMISSION_ADVANCED + price_credits = 25000 + +/datum/shop_sku/rig_bay_mech/showpiece/paddy + item_path = /obj/vehicle/sealed/mecha/ripley/paddy/preset + zone_permission = ZONE_PERMISSION_ADVANCED + price_credits = 26000 + +/datum/shop_sku/rig_bay_mech/showpiece/justice + item_path = /obj/vehicle/sealed/mecha/justice/loaded + zone_permission = ZONE_PERMISSION_ADVANCED + price_credits = 28000 + +/datum/shop_sku/rig_bay_mech/showpiece/phazon + item_path = /obj/vehicle/sealed/mecha/phazon + zone_permission = ZONE_PERMISSION_ADVANCED + price_credits = 29000 + +/datum/shop_sku/rig_bay_mech/showpiece/savannah_ivanov + item_path = /obj/vehicle/sealed/mecha/savannah_ivanov + zone_permission = ZONE_PERMISSION_ADVANCED + price_credits = 30000 + +/datum/shop_sku/rig_bay_mech/showpiece/marauder + item_path = /obj/vehicle/sealed/mecha/marauder/loaded + zone_permission = ZONE_PERMISSION_ADVANCED + price_credits = 31000 + // ===== ARMS, AMMO AND FITTINGS ===== /// Base for the ordinary exosuit gear lines (Exosuit Arms, Ammo and Fittings tabs). diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_stand.dm b/voidcrew/modules/trade/rig_bay/rig_bay_stand.dm index afcead8b172..ab6d207d8d8 100644 --- a/voidcrew/modules/trade/rig_bay/rig_bay_stand.dm +++ b/voidcrew/modules/trade/rig_bay/rig_bay_stand.dm @@ -1,3 +1,6 @@ +/// Mechs each stand pool has dealt this round, server-wide: pool typepath -> count. See deals_per_round. +GLOBAL_LIST_EMPTY(rig_bay_pool_deals) + /** * # Rig Bay stand * @@ -76,7 +79,7 @@ /obj/structure/rig_bay_stand/proc/deal_mech(animate = TRUE) if(QDELETED(src) || !shop || stand_state != RIG_BAY_STAND_EMPTY || !ispath(pool, /datum/shop_rotation/rig_bay)) return null - if(!tile_clear()) + if(!tile_clear() || !pool_can_deal()) return null var/datum/shop_rotation/rig_bay/rotation = locate(pool) in shop.live_rotations var/temporary_rotation = !rotation @@ -93,7 +96,9 @@ return null drop_sku() shop.add_sku(sku_type, SHELF_CORE) - return receive_mech(shop.skus[length(shop.skus)], animate) + . = receive_mech(shop.skus[length(shop.skus)], animate) + if(.) + GLOB.rig_bay_pool_deals[pool] = (GLOB.rig_bay_pool_deals[pool] || 0) + 1 /** * Takes ownership of `new_sku` (already in shop.skus) and spawns its mech on the stand with its kit, @@ -116,6 +121,8 @@ part.attach(mech) else qdel(part) + // Some stock builds come locked to an access no crew carries (Paddy, Justice). Sold mechs open to whoever buys them. + mech.mecha_flags &= ~ID_LOCK_ON mark_trader_goods(mech) mech.setDir(dir) mech.set_anchored(TRUE) @@ -195,7 +202,7 @@ */ /obj/structure/rig_bay_stand/proc/convoy_arrived() stop_retry() - if(stand_state != RIG_BAY_STAND_EMPTY) + if(stand_state != RIG_BAY_STAND_EMPTY || !pool_can_deal()) convoys_waited = 0 return if(++convoys_waited < get_convoys_per_refill()) @@ -208,7 +215,7 @@ /// A due restock found the tile blocked. Tries again until it deals or the next convoy takes over. /obj/structure/rig_bay_stand/proc/retry_deal() - if(stand_state != RIG_BAY_STAND_EMPTY) + if(stand_state != RIG_BAY_STAND_EMPTY || !pool_can_deal()) stop_retry() return if(deal_mech(animate = TRUE)) @@ -230,6 +237,14 @@ var/datum/shop_rotation/rig_bay/rotation_type = pool return max(1, initial(rotation_type.convoys_per_refill)) +/// FALSE once this stand's pool has dealt its deals_per_round for the whole server. +/obj/structure/rig_bay_stand/proc/pool_can_deal() + if(!ispath(pool, /datum/shop_rotation/rig_bay)) + return FALSE + var/datum/shop_rotation/rig_bay/rotation_type = pool + var/limit = initial(rotation_type.deals_per_round) + return limit <= 0 || (GLOB.rig_bay_pool_deals[pool] || 0) < limit + /// TRUE if a new mech can be placed: no mob, mech or other dense thing on the stand tile. /obj/structure/rig_bay_stand/proc/tile_clear() var/turf/tile = get_turf(src) diff --git a/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm b/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm index f88a90575b7..6dcb2839e8d 100644 --- a/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm +++ b/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm @@ -32,11 +32,14 @@ while(stand.stand_state != state && world.time < give_up) sleep(1) -/// Every stand mech spawns with the kit its line lists. +/// Every stand mech spawns with the kit its line lists. Only showpieces hang armed, with their stock kit. /datum/unit_test/voidcrew_rig_bay/roster/Run() var/datum/outpost_shop/vendor/suit_fitter/shop = allocate(/datum/outpost_shop/vendor/suit_fitter) var/obj/structure/rig_bay_stand/stand = make_stand(shop, bay_turf(1, 1), /datum/shop_rotation/rig_bay/work) for(var/sku_type in subtypesof(/datum/shop_sku/rig_bay_mech)) + if(sku_type == /datum/shop_sku/rig_bay_mech/showpiece) + continue + var/showpiece = ispath(sku_type, /datum/shop_sku/rig_bay_mech/showpiece) shop.add_sku(sku_type, "core") var/datum/shop_sku/rig_bay_mech/sku = shop.skus[length(shop.skus)] var/obj/vehicle/sealed/mecha/mech = stand.receive_mech(sku, FALSE) @@ -45,8 +48,14 @@ TEST_ASSERT_EQUAL(mech.type, sku.item_path, "[sku_type] hung the wrong mech") for(var/kit_type in sku.spawn_kit) TEST_ASSERT(locate(kit_type) in mech.flat_equipment, "[sku_type] came without its [kit_type]") - for(var/obj/item/mecha_parts/mecha_equipment/equipment as anything in mech.flat_equipment) - TEST_ASSERT(!istype(equipment, /obj/item/mecha_parts/mecha_equipment/weapon), "[sku_type] hangs armed with [equipment.type]") + if(!showpiece) + for(var/obj/item/mecha_parts/mecha_equipment/equipment as anything in mech.flat_equipment) + TEST_ASSERT(!istype(equipment, /obj/item/mecha_parts/mecha_equipment/weapon), "[sku_type] hangs armed with [equipment.type]") + TEST_ASSERT(!(mech.mecha_flags & ID_LOCK_ON), "[sku_type] hangs locked to an access") + if(sku_type == /datum/shop_sku/rig_bay_mech/showpiece/paddy) + TEST_ASSERT(locate(/obj/item/mecha_parts/mecha_equipment/weapon/paddy_claw) in mech.flat_equipment, "The Paddy came without its claw") + if(sku_type == /datum/shop_sku/rig_bay_mech/showpiece/marauder) + TEST_ASSERT(locate(/obj/item/mecha_parts/mecha_equipment/weapon/ballistic/missile_rack) in mech.flat_equipment, "The Marauder came without its missile rack") TEST_ASSERT(is_trader_good(mech), "[sku_type] isn't marked as trader goods") TEST_ASSERT(mech.cell && is_trader_good(mech.cell), "[sku_type]'s cell isn't marked as trader goods") stand.drop_sku() @@ -237,3 +246,79 @@ var/obj/vehicle/sealed/mecha/ripley/mech = allocate(/obj/vehicle/sealed/mecha/ripley, bay_turf(2, 1)) TEST_ASSERT(!outpost_checkpoint_saves(stand), "Ship saves keep Rig Bay stands") TEST_ASSERT(!outpost_checkpoint_saves(mech), "Ship saves keep mechs") + +/// Showpiece tests start each with the server's showpiece deal unspent and put the real count back after. +/datum/unit_test/voidcrew_rig_bay/showpiece + abstract_type = /datum/unit_test/voidcrew_rig_bay/showpiece + var/saved_deals + +/datum/unit_test/voidcrew_rig_bay/showpiece/New() + . = ..() + saved_deals = GLOB.rig_bay_pool_deals[/datum/shop_rotation/rig_bay/showpiece] + GLOB.rig_bay_pool_deals[/datum/shop_rotation/rig_bay/showpiece] = 0 + +/datum/unit_test/voidcrew_rig_bay/showpiece/Destroy() + GLOB.rig_bay_pool_deals[/datum/shop_rotation/rig_bay/showpiece] = saved_deals + return ..() + +/// The showpiece stand deals one super mech a round for the whole server and never refills. +/datum/unit_test/voidcrew_rig_bay/showpiece/refill/Run() + var/datum/outpost_shop/vendor/suit_fitter/shop = allocate(/datum/outpost_shop/vendor/suit_fitter) + var/obj/structure/rig_bay_stand/stand = make_stand(shop, bay_turf(1, 1), /datum/shop_rotation/rig_bay/showpiece, 4) + var/obj/structure/rig_bay_stand/second = make_stand(shop, bay_turf(3, 1), /datum/shop_rotation/rig_bay/showpiece, 5) + var/obj/vehicle/sealed/mecha/mech = deal(stand) + TEST_ASSERT(istype(stand.sku, /datum/shop_sku/rig_bay_mech/showpiece), "The showpiece stand dealt [stand.sku?.type]") + TEST_ASSERT_EQUAL(stand.sku.zone_permission, 2, "A showpiece isn't gated at advanced") + TEST_ASSERT_NULL(second.deal_mech(FALSE), "A second showpiece was dealt in the same round") + + var/mob/living/carbon/human/consistent/buyer = allocate(/mob/living/carbon/human/consistent, bay_turf(1, 3)) + TEST_ASSERT(stand.release_mech(buyer), "The showpiece stand wouldn't release its mech") + wait_for_stand(stand, "sold_on_stand") + mech.forceMove(bay_turf(1, 2)) + TEST_ASSERT_EQUAL(stand.stand_state, "empty", "Driving off didn't empty the showpiece stand") + for(var/convoy in 1 to 4) + shop.convoy_restock() + TEST_ASSERT_EQUAL(stand.stand_state, "empty", "The showpiece stand refilled after its sale") + TEST_ASSERT_NULL(stand.retry_timer, "The showpiece stand keeps retrying a deal it may not make") + TEST_ASSERT_EQUAL(second.stand_state, "empty", "Another showpiece stand dealt after the round's showpiece") + TEST_ASSERT_EQUAL(GLOB.rig_bay_pool_deals[/datum/shop_rotation/rig_bay/showpiece], 1, "The round's showpiece deal count is wrong") + + // The other pools keep no such count + var/obj/structure/rig_bay_stand/work = make_stand(shop, bay_turf(3, 3), /datum/shop_rotation/rig_bay/work, 1) + deal(work) + TEST_ASSERT(work.pool_can_deal(), "The work pool ran out of deals") + +/// One showpiece per crew per round, whichever mech it is. Another crew can still buy. +/datum/unit_test/voidcrew_rig_bay/showpiece/crew_cap/Run() + var/obj/structure/overmap/ship/ship = make_ship() + var/datum/voidcrew_crew_record/crew = record_for(ship) + var/mob/living/carbon/human/buyer = make_member() + ship.enlist_crewmember(buyer) + set_permission(crew, 2) + var/datum/bank_account/account = fund(buyer, 100000) + var/obj/structure/overmap/ship/other_ship = make_ship() + var/datum/voidcrew_crew_record/other_crew = record_for(other_ship) + var/mob/living/carbon/human/other_buyer = make_member() + other_ship.enlist_crewmember(other_buyer) + set_permission(other_crew, 2) + fund(other_buyer, 100000) + + var/datum/outpost_shop/vendor/suit_fitter/shop = allocate(/datum/outpost_shop/vendor/suit_fitter) + var/obj/structure/rig_bay_stand/first_stand = make_stand(shop, bay_turf(1, 1), /datum/shop_rotation/rig_bay/showpiece, 4) + var/obj/structure/rig_bay_stand/second_stand = make_stand(shop, bay_turf(3, 1), /datum/shop_rotation/rig_bay/showpiece, 5) + shop.add_sku(/datum/shop_sku/rig_bay_mech/showpiece/honker, "core") + var/datum/shop_sku/rig_bay_mech/showpiece/honker = shop.skus[length(shop.skus)] + allocated += first_stand.receive_mech(honker, FALSE) + shop.add_sku(/datum/shop_sku/rig_bay_mech/showpiece/phazon, "core") + var/datum/shop_sku/rig_bay_mech/showpiece/phazon = shop.skus[length(shop.skus)] + allocated += second_stand.receive_mech(phazon, FALSE) + + set_permission(crew, 1) + TEST_ASSERT(!honker.try_purchase(buyer, null), "A showpiece sold below advanced reputation") + set_permission(crew, 2) + TEST_ASSERT(honker.try_purchase(buyer, null), "The crew couldn't buy its first showpiece") + TEST_ASSERT_EQUAL(account.account_balance, 100000 - honker.price_credits, "The showpiece cost the wrong amount") + TEST_ASSERT_EQUAL(phazon.get_denial_reason(buyer), "I've filled that order already.", "A second showpiece wasn't refused") + TEST_ASSERT(!phazon.try_purchase(buyer, null), "A crew bought a second showpiece in one round") + TEST_ASSERT_EQUAL(account.account_balance, 100000 - honker.price_credits, "A refused showpiece took money") + TEST_ASSERT(phazon.try_purchase(other_buyer, null), "The first crew's showpiece blocked another crew") diff --git a/voidcrew/modules/unit_tests/voidcrew_zone_shops.dm b/voidcrew/modules/unit_tests/voidcrew_zone_shops.dm index a6c62e9e233..f5b3ffd5e14 100644 --- a/voidcrew/modules/unit_tests/voidcrew_zone_shops.dm +++ b/voidcrew/modules/unit_tests/voidcrew_zone_shops.dm @@ -127,6 +127,12 @@ /datum/shop_sku/ripperdoc/cascade = 3, /datum/shop_sku/rig_bay_mech/gygax = 1, /datum/shop_sku/rig_bay_mech/durand = 2, + /datum/shop_sku/rig_bay_mech/showpiece/honker = 2, + /datum/shop_sku/rig_bay_mech/showpiece/paddy = 2, + /datum/shop_sku/rig_bay_mech/showpiece/justice = 2, + /datum/shop_sku/rig_bay_mech/showpiece/phazon = 2, + /datum/shop_sku/rig_bay_mech/showpiece/savannah_ivanov = 2, + /datum/shop_sku/rig_bay_mech/showpiece/marauder = 2, /datum/shop_sku/rig_bay_gear/arm/missile_rack = 2, /datum/shop_sku/rig_bay_gear/arm/lmg = 1, /datum/shop_sku/rig_bay_gear/arm/laser = 1, @@ -146,7 +152,7 @@ /datum/unit_test/voidcrew_zone_shops/catalog/Run() var/list/expected = expected_tags() - TEST_ASSERT_EQUAL(length(expected), 116, "The retained permission catalog is incomplete") + TEST_ASSERT_EQUAL(length(expected), 122, "The retained permission catalog is incomplete") var/datum/voidcrew_crew_record/crew = allocate(/datum/voidcrew_crew_record) for(var/datum/shop_sku/sku_type as anything in subtypesof(/datum/shop_sku)) var/permission = expected[sku_type] || 0 diff --git a/wiki/content/trader-outposts.md b/wiki/content/trader-outposts.md index 5b94d7101fc..4984a87b60e 100644 --- a/wiki/content/trader-outposts.md +++ b/wiki/content/trader-outposts.md @@ -85,6 +85,7 @@ Wick also sells **whole exosuits**, each one hanging on chains from a gantry in - **Work bays** show a Ripley MK-I, Ripley MK-II, Clarke or Odysseus, from 4000 to 6500 credits, with a clamp and drill, a drill and mining scanner, or a sleeper already fitted. They restock on every convoy. - **The combat bay** shows a Gygax (11000 credits, 4 reputation, two per crew each round) or a Durand (18000 credits plus 3 vouchers, 11 reputation, one per crew each round). Combat mechs come bare; buy the arms separately. The bay restocks on every second convoy. +- **The showpiece bay**, bay 4 with the white stripes, shows one rare exosuit a round: a H.O.N.K, Paddy, Justice, Phazon, Savannah-Ivanov or Marauder, from 25000 to 31000 credits plus 3 vouchers, 11 reputation, one per crew. Each one comes as it was built, guns included where it has them. Once it's sold, the bay stays empty until the next round. - A bay only restocks once the last mech has been driven off it. - After the sale only your crew can climb in, while the mech is on the outpost and for 20 minutes at most. An empty, unarmed mech can't be damaged while it is still inside the concourse. - Drive it out through the exit gate and take the lobby elevator down to the hangars, like anyone else.