diff --git a/tgstation.dme b/tgstation.dme index cf7dcde4fe3..4deccb7920f 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,13 @@ #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\trade\rig_bay\rig_bay_stand_visuals.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/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/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/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..482b64b2298 --- /dev/null +++ b/tools/gear_catalog/ratings/exosuits.json @@ -0,0 +1,136 @@ +{ + "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/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", + "hash": "", + "name": "Odysseus", + "rank": 4, + "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", + "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" + }, + "/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/dark": "no route", + "/obj/vehicle/sealed/mecha/honker/dark/loaded": "no route", + "/obj/vehicle/sealed/mecha/justice": "no route", + "/obj/vehicle/sealed/mecha/marauder": "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/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" + } +} diff --git a/voidcrew/_DEFINES/rig_bay.dm b/voidcrew/_DEFINES/rig_bay.dm new file mode 100644 index 00000000000..1b56f3cb54d --- /dev/null +++ b/voidcrew/_DEFINES/rig_bay.dm @@ -0,0 +1,32 @@ +// 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 +/// 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) + +// 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/_maps/map_files/outposts/trader_outpost_outfitter.dmm b/voidcrew/_maps/map_files/outposts/trader_outpost_outfitter.dmm index 2d4848184b2..4979953fbeb 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) @@ -127,6 +158,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, @@ -152,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, @@ -216,6 +266,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 +361,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" @@ -318,14 +375,29 @@ /obj/effect/decal/cleanable/dirt/dust, /turf/open/indestructible/plating, /area/voidcrew/trader_outpost) -"aT" = ( -/obj/effect/turf_decal/trimline/red/filled/line{ - dir = 8 +"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/siding/thinplating_new/dark{ - dir = 8 +/obj/effect/turf_decal/bot, +/turf/open/indestructible/dark/textured, +/area/voidcrew/trader_outpost) +"aT" = ( +/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 +428,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 +445,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 +492,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 +528,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" @@ -521,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{ @@ -654,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"; @@ -673,9 +807,72 @@ /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) +"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 + }, +/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,10 +883,128 @@ }, /turf/open/indestructible/dark/smooth_large, /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) +"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) +"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{ @@ -705,6 +1020,69 @@ /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) +"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{ @@ -716,6 +1094,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 @@ -742,12 +1239,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{ @@ -992,14 +1585,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,24 +1592,12 @@ /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 +"gk" = ( +/obj/machinery/light/directional/north, +/obj/effect/turf_decal/stripes/white/line{ + dir = 8 }, -/obj/effect/decal/cleanable/dirt/dust, -/obj/machinery/light/small/red/directional/west, -/obj/item/gun/energy/laser/redtag, +/obj/effect/decal/cleanable/dirt, /turf/open/indestructible/dark/textured, /area/voidcrew/trader_outpost) "gD" = ( @@ -1048,13 +1621,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 +1901,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 +1916,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 +1952,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, @@ -1490,15 +2010,8 @@ /obj/effect/turf_decal/tile/yellow/half/contrasted{ dir = 4 }, -/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, +/obj/effect/decal/cleanable/dirt, +/turf/open/indestructible/dark, /area/voidcrew/trader_outpost) "nO" = ( /obj/effect/turf_decal/siding/thinplating_new{ @@ -1562,13 +2075,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 @@ -1652,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{ @@ -1691,13 +2203,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 +2221,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 +2236,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 +2275,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 +2335,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 +2374,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 +2427,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{ @@ -2053,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 @@ -2101,15 +2538,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 +2713,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 @@ -2307,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 @@ -2361,18 +2785,13 @@ /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, +"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" = ( @@ -2551,16 +2970,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, @@ -2665,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{ @@ -2872,14 +3287,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 +3298,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 +3500,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 +3944,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 +4013,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 +4046,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 +4135,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; @@ -4436,8 +4772,8 @@ ao "} (13,1,1) = {" ao -ao -ao +dU +dU dU dU dU @@ -4478,12 +4814,12 @@ ao "} (14,1,1) = {" ao -ao dU -dU -Ks -gq -lw +aj +ct +cF +cG +cH NJ lE dP @@ -4520,13 +4856,13 @@ ao "} (15,1,1) = {" ao -ao dU -qe -GF -nH -fV -nH +an +cu +cJ +cM +cJ +cQ lZ bs Um @@ -4562,13 +4898,13 @@ ao "} (16,1,1) = {" ao -ao -dU -oA -lR -st -st -st +lE +aT +cv +cK +cN +cK +cR lZ bs Um @@ -4604,13 +4940,13 @@ ao "} (17,1,1) = {" ao -ao dU -yP -aY -gh -gh -sU +ax +cw +cJ +cJ +cJ +cQ lZ vo cX @@ -4646,13 +4982,13 @@ ao "} (18,1,1) = {" ao -ao dU -xA -aT -St -He -qo +aS +cx +cL +cO +cP +cS lE Lx oX @@ -4688,13 +5024,13 @@ ao "} (19,1,1) = {" ao -ao -dU -dU -lE -lE -lE dU +bC +cy +Xe +Xe +ks +da dU dU lE @@ -4730,14 +5066,14 @@ ao "} (20,1,1) = {" ao -ao -dU -rw -vy -HF -kY -BN -dU +lE +aY +cB +Xe +Xe +Xe +Xe +lE uD lx tU @@ -4772,13 +5108,13 @@ ao "} (21,1,1) = {" ao -aF dU -zo -TB -ks +bT +cy +Xe +cT +cV Xe -bX lE aJ fp @@ -4815,12 +5151,12 @@ ao (22,1,1) = {" ao dU -dU -WA -UD -UD -UD -qS +bZ +cC +ks +cU +cZ +db lE aJ fp @@ -4857,13 +5193,13 @@ ao (23,1,1) = {" ao lE -tO -lM +cn Ao Ao Ao Ao -qa +Ao +ck hu fp au @@ -4899,13 +5235,13 @@ ao (24,1,1) = {" ao lE -tb -ai -fp +co +cD fp +dk fp fp -gV +cl fp fp au @@ -4941,13 +5277,13 @@ aF (25,1,1) = {" ao lE -kQ -sC +cq Jm Jm Jm Jm -ey +Jm +cm nm fp au @@ -4983,12 +5319,12 @@ aV (26,1,1) = {" ao dU -dU -hL -rd -zC -rj -im +pK +Dq +Xe +Xe +Xe +dh lE SC fp @@ -5024,13 +5360,13 @@ aV "} (27,1,1) = {" ao -aV dU -BS +up +zf Xe -PW -OK -BO +de +ks +di lE yK fp @@ -5066,13 +5402,13 @@ aV "} (28,1,1) = {" ao -aV dU -OP -ea -GI -ea -jA +gk +xL +dd +df +dg +dj dU FV fp 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/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/icons/rig_bay_stand.dmi b/voidcrew/modules/trade/icons/rig_bay_stand.dmi new file mode 100644 index 00000000000..4a380d30dbd Binary files /dev/null and b/voidcrew/modules/trade/icons/rig_bay_stand.dmi differ 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..f6f5ec53e2c --- /dev/null +++ b/voidcrew/modules/trade/rig_bay/rig_bay_clamp.dm @@ -0,0 +1,328 @@ +// 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 + // 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) + 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..a3ba5c53a6a --- /dev/null +++ b/voidcrew/modules/trade/rig_bay/rig_bay_shop.dm @@ -0,0 +1,44 @@ +// 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/showpiece, + /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 new file mode 100644 index 00000000000..441c00c9dbe --- /dev/null +++ b/voidcrew/modules/trade/rig_bay/rig_bay_skus.dm @@ -0,0 +1,364 @@ +// 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 + /// 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 + 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, + ) + +/// 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 + 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 ===== + +/** + * 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 ..() + +/// 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 + 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 + +// ===== 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). +/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 new file mode 100644 index 00000000000..ab6d207d8d8 --- /dev/null +++ b/voidcrew/modules/trade/rig_bay/rig_bay_stand.dm @@ -0,0 +1,298 @@ +/// 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 + * + * 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 + /// 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) + 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() + stop_retry() + 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) + shop?.attach_stand(src) + 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() || !pool_can_deal()) + 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) + . = 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, + * 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) + // 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) + RegisterSignal(mech, COMSIG_QDELETING, PROC_REF(on_mech_deleted)) + RegisterSignal(mech, COMSIG_MOVABLE_MOVED, PROC_REF(on_mech_moved)) + 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 + // 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 + +/// 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 + +/** + * 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 || !pool_can_deal()) + 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 || !pool_can_deal()) + 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)) + +/// 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) + 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" diff --git a/voidcrew/modules/trade/rig_bay/rig_bay_stand_visuals.dm b/voidcrew/modules/trade/rig_bay/rig_bay_stand_visuals.dm new file mode 100644 index 00000000000..4d6fa8b4541 --- /dev/null +++ b/voidcrew/modules/trade/rig_bay/rig_bay_stand_visuals.dm @@ -0,0 +1,414 @@ +// The Rig Bay stand's look: an overhead gantry with two chains that hold the display mech up, the release +// (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. 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. 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 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. +#define RIG_BAY_FRONT_LAYER 3.93 +/// pixel_z of a chain frame whose top row sits at the top of the gantry bar (26 px above the tile). +#define RIG_BAY_CHAIN_TOP 26 +/// Rows in a stowed chain: only its shackle shows under the bar. +#define RIG_BAY_CHAIN_STOWED 5 +/// Column of a chain frame's centre line (frames are 9 px wide: outline, 7 px of links, outline). +#define RIG_BAY_CHAIN_HALF 4 +/// Longest chain_N state in the stand's DMI. +#define RIG_BAY_CHAIN_MAX 32 +/// Volume of the stand's sounds. It's a shop floor: keep it low. +#define RIG_BAY_SOUND_VOLUME 30 +/// Tint a beamed-in mech starts with, fading to its own colours. +#define RIG_BAY_BEAM_TINT "#7fa8ff" + +/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) + . = ..() + ensure_chains() + 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) + QDEL_LIST(lamps) + +/// 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()) + if(QDELETED(chain) || chain.loc != loc) + chains -= chain + if(!QDELETED(chain)) + qdel(chain) + var/static/list/default_anchors = list(7, 24) + while(length(chains) < 2) + 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() + . = ..() + var/icon_file = 'voidcrew/modules/trade/icons/rig_bay_stand.dmi' + // The gantry is 3 tiles wide and 2 tall: posts in the side tiles, bar across the tile above. + var/static/list/pieces = list( + "gantry_nw" = list(-32, 32), "gantry_n" = list(0, 32), "gantry_ne" = list(32, 32), + "gantry_w" = list(-32, 0), "gantry_e" = list(32, 0), + ) + for(var/piece in pieces) + var/mutable_appearance/part = mutable_appearance(icon_file, piece, RIG_BAY_GANTRY_LAYER, src) + var/list/offset = pieces[piece] + part.pixel_w = offset[1] + part.pixel_z = offset[2] + . += part + +/// 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) + lamp = new_lamp + 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() + ..() + 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) + set_lamp("grey") + +/// Idle look: the mech hangs RIG_BAY_HANG_LIFT px up with both chains hooked onto its shoulders. +/obj/structure/rig_bay_stand/apply_hang_pose(obj/vehicle/sealed/mecha/mech) + mech.alpha = 255 + mech.pixel_z = RIG_BAY_HANG_LIFT + ensure_chains() + var/list/anchors = chain_anchors(mech) + for(var/i in 1 to length(chains)) + var/obj/effect/rig_bay_chain/chain = chains[i] + var/list/anchor = anchors[i] + chain.set_pose(anchor[1], hang_length(anchor[2]), RIG_BAY_CHAIN_TOP) + set_lamp("red") + +/** + * Release, 1.1 s. 0-3 ds the bolts let go (lamps amber); 3-7 the chains pay out and the mech lowers 4 px to + * the floor (mech and chains move together); 7-11 the shackles let go and the chains wind up into the bar. + * Lamps go green at 11. pixel_z is 0 from the 7 ds mark; on_done fires once at 11 ds. + */ +/obj/structure/rig_bay_stand/animate_release(obj/vehicle/sealed/mecha/mech, datum/callback/on_done) + ensure_chains() + set_lamp("amber") + playsound(src, 'sound/machines/airlock/boltsup.ogg', RIG_BAY_SOUND_VOLUME, FALSE, SHORT_RANGE_SOUND_EXTRARANGE) + addtimer(CALLBACK(src, PROC_REF(stand_sound), 'sound/vehicles/mecha/hydraulic.ogg'), 0.3 SECONDS) + addtimer(CALLBACK(src, PROC_REF(stand_sound), 'sound/vehicles/mecha/mechstep.ogg'), 0.7 SECONDS) + addtimer(CALLBACK(src, PROC_REF(stand_sound), SFX_REEL), 0.7 SECONDS) + var/lift = mech.pixel_z + animate(mech, time = 3) + animate(pixel_z = 0, time = 4) + for(var/obj/effect/rig_bay_chain/chain as anything in chains) + var/length = chain.length + var/top = chain.pixel_z + animate(chain, time = 3) + 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 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 + if(mech == held_mech) + set_lamp("green") + on_done?.Invoke() + +/** + * Beam-in, 3.0 s. Lamps amber and the pad charges (0-10 ds) while a blue-white column builds; a flash at 10; + * the mech fades in on the floor inside the column, tinted blue and clearing (14-20) while the stowed chains + * slide along the bar over its shoulders; sparks at its feet and the column fades (20-24); the chains come + * down to the shoulders (20-26) and hoist the mech 4 px (26-30). Lamps red and on_done once at 30 ds. + */ +/obj/structure/rig_bay_stand/animate_beam_in(obj/vehicle/sealed/mecha/mech, datum/callback/on_done) + ensure_chains() + set_lamp("amber") + flick("mechpad-launch", src) + playsound(src, 'sound/machines/beep/triple_beep.ogg', RIG_BAY_SOUND_VOLUME, FALSE, SHORT_RANGE_SOUND_EXTRARANGE) + addtimer(CALLBACK(src, PROC_REF(stand_sound), 'sound/effects/phasein.ogg'), 1 SECONDS) + addtimer(CALLBACK(src, PROC_REF(stand_sound), SFX_SPARKS), 2 SECONDS) + addtimer(CALLBACK(src, PROC_REF(stand_sound), SFX_REEL), 2 SECONDS) + // The flash and sparks are made now and switched on by animate(), so they keep time with the column and + // the mech on the client even when the server lags. + var/obj/effect/temp_visual/rig_bay_phase/flash = new(loc) + animate(flash, time = 10) + animate(icon_state = "phasein", time = 5) + for(var/foot_x in list(-8, 8)) + var/obj/effect/temp_visual/rig_bay_spark/spark = new(loc) + spark.pixel_w = foot_x + animate(spark, time = 20) + animate(icon_state = "sparks", time = 7) + addtimer(CALLBACK(src, PROC_REF(stand_sound), 'sound/vehicles/mecha/hydraulic.ogg'), 2.6 SECONDS) + + mech.pixel_z = 0 + mech.alpha = 0 + mech.color = RIG_BAY_BEAM_TINT + var/obj/effect/rig_bay_beam_column/column = new(loc) + animate(column, alpha = 77, time = 10) + animate(alpha = 140, time = 4) + animate(alpha = 115, time = 6) + animate(alpha = 0, time = 4) + QDEL_IN(column, 2.5 SECONDS) + + animate(mech, time = 14) + animate(alpha = 255, color = "#ffffff", time = 6) + animate(time = 6) + animate(pixel_z = RIG_BAY_HANG_LIFT, time = 4) + + var/list/anchors = chain_anchors(mech) + for(var/i in 1 to length(chains)) + var/obj/effect/rig_bay_chain/chain = chains[i] + var/list/anchor = anchors[i] + var/hang = hang_length(anchor[2]) + chain.pixel_z = RIG_BAY_CHAIN_TOP - RIG_BAY_HANG_LIFT + chain.icon_state = "chain_[RIG_BAY_CHAIN_STOWED]" + animate(chain, time = 14) + animate(pixel_w = anchor[1] - RIG_BAY_CHAIN_HALF, time = 6) + chain.queue_length_steps(RIG_BAY_CHAIN_STOWED, hang, 6) + 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. +/obj/structure/rig_bay_stand/proc/end_beam_in(obj/vehicle/sealed/mecha/mech, datum/callback/on_done) + if(!QDELETED(mech)) + mech.update_atom_colour() + if(mech == held_mech) + apply_hang_pose(mech) + stand_sound('sound/machines/airlock/boltsdown.ogg') + on_done?.Invoke() + +/// Plays one of the stand's sounds at shop volume. +/obj/structure/rig_bay_stand/proc/stand_sound(sound_file) + playsound(src, sound_file, RIG_BAY_SOUND_VOLUME, FALSE, SHORT_RANGE_SOUND_EXTRARANGE) + +/// Rows a chain needs to reach a shackle row `attach_row` (tile-local, y down) with its top in the bar. +/obj/structure/rig_bay_stand/proc/hang_length(attach_row) + return clamp(attach_row - RIG_BAY_HANG_LIFT + RIG_BAY_CHAIN_TOP + 1, RIG_BAY_CHAIN_STOWED, RIG_BAY_CHAIN_MAX) + +/** + * Where the two chains hook onto `mech`, from its sprite's top edge: list(list(x, row), list(x, row)), left + * then right. x is the shackle's centre column, row is the shackle's last row with the mech on the floor: + * one row inside the mech's silhouette under all three shackle columns, so it tucks behind the shoulder. + */ +/obj/structure/rig_bay_stand/proc/chain_anchors(obj/vehicle/sealed/mecha/mech) + var/list/profile = mech_top_profile(mech) + var/left = 1 + var/right = 32 + while(left < 32 && isnull(profile[left])) + left++ + while(right > 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 and rebuilds it if it goes missing. +/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) + +/// 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 + 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) + // A beam of light glows in a dim room; the glow follows the column's own fade. + add_overlay(emissive_appearance(icon, icon_state, src)) + +/// The flash where a beamed-in mech appears: in front of the mech, behind mobs. Blank until the stand's +/// animate() switches it to "phasein" 1 s into the beam-in. +/obj/effect/temp_visual/rig_bay_phase + icon = 'icons/effects/effects.dmi' + icon_state = "nothing" + layer = RIG_BAY_FRONT_LAYER + color = "#cfe4ff" + pixel_z = 2 + duration = 2 SECONDS + randomdir = FALSE + +/// Picture-only sparks at a beamed-in mech's feet (no heat: these never light anything). Blank until the +/// stand's animate() switches them on 2 s into the beam-in. Shrunk and dropped so they burst at foot level. +/obj/effect/temp_visual/rig_bay_spark + icon = 'icons/effects/effects.dmi' + icon_state = "nothing" + layer = RIG_BAY_FRONT_LAYER + pixel_z = -10 + duration = 3 SECONDS + +/obj/effect/temp_visual/rig_bay_spark/Initialize(mapload) + . = ..() + transform = matrix(0.6, 0, 0, 0, 0.6, 0) + +#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 +#undef RIG_BAY_CHAIN_STOWED +#undef RIG_BAY_CHAIN_HALF +#undef RIG_BAY_CHAIN_MAX +#undef RIG_BAY_SOUND_VOLUME +#undef RIG_BAY_BEAM_TINT 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..064bc8b7fea --- /dev/null +++ b/voidcrew/modules/trade/rig_bay/rig_bay_wick.dm @@ -0,0 +1,593 @@ +/** + * # 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: 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 +#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/weld = 1, + /datum/outpost_ambient_work/wrench = 1, + /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, 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/weights = work_weights.Copy() + while(length(weights)) + var/work_type = pick_weight(weights) + weights -= work_type + 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) + 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 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) + 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 + 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?", + "Somebody's been feeding this one plasma cutter grit.", + )) + +/// 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/trade/shop_catalog_suit_vendor.dm b/voidcrew/modules/trade/shop_catalog_suit_vendor.dm index 5563133983a..11e109618fc 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,12 @@ "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.", + "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.", ), ) diff --git a/voidcrew/modules/unit_tests/_unit_tests.dm b/voidcrew/modules/unit_tests/_unit_tests.dm index e70ad1b8e66..17e9c9c0559 100644 --- a/voidcrew/modules/unit_tests/_unit_tests.dm +++ b/voidcrew/modules/unit_tests/_unit_tests.dm @@ -201,6 +201,8 @@ #include "voidcrew_research_movement.dm" #include "voidcrew_respawn_timer.dm" #include "voidcrew_restaurant_portal.dm" +#include "voidcrew_rig_bay.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.dm b/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm new file mode 100644 index 00000000000..6dcb2839e8d --- /dev/null +++ b/voidcrew/modules/unit_tests/voidcrew_rig_bay.dm @@ -0,0 +1,324 @@ +// 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 + +/// 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. 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) + 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]") + 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() + 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() + 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") + 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)) + 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() + 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") + 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() + 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") + +/// 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") + +/// 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_rig_bay_wick.dm b/voidcrew/modules/unit_tests/voidcrew_rig_bay_wick.dm new file mode 100644 index 00000000000..40470752c4f --- /dev/null +++ b/voidcrew/modules/unit_tests/voidcrew_rig_bay_wick.dm @@ -0,0 +1,124 @@ +/** + * 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") + 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() + 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]") diff --git a/voidcrew/modules/unit_tests/voidcrew_zone_shops.dm b/voidcrew/modules/unit_tests/voidcrew_zone_shops.dm index d1a2677ee4c..f5b3ffd5e14 100644 --- a/voidcrew/modules/unit_tests/voidcrew_zone_shops.dm +++ b/voidcrew/modules/unit_tests/voidcrew_zone_shops.dm @@ -125,11 +125,34 @@ /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_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, + /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), 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 e14561a5173..4984a87b60e 100644 --- a/wiki/content/trader-outposts.md +++ b/wiki/content/trader-outposts.md @@ -72,14 +72,26 @@ 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. +- **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. +- 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).