From cbc3350224f550636af37a88bdeae5c84b28d2bb Mon Sep 17 00:00:00 2001 From: Y0SH1M4S73R Date: Sun, 23 Feb 2025 01:18:43 -0500 Subject: [PATCH] Custom Shuttles Redux: Allows for the construction of custom shuttles. (#88493) ## About The Pull Request This incredibly detailed PR adds the ability to construct custom shuttles, which function similarly to whiteships. To construct a custom shuttle, you need the following items: - Shuttle frame rods These rods can be hand-crafted by using 5 rods on 1 sheet of titanium, or printed at a sci/engi/cargo lathe after researching the Shuttle Engineering techweb node. Lattices built with these rods, and catwalks/floors built on top of these lattices, are valid for shuttle construction. - Shuttle engines Did you know shuttle engines have boards that weren't normally obtainable? Well the board for one specific engine type is now available from the sci/engi/cargo lathe after researching Shuttle Engineering. Of course, the old options remain. You can steal engines from other shuttles, including escape pods (it's not like engines are strictly necessary for *those* shuttles anyways). Alternatively, the shuttle engine supply pack is no longer locked behind the purchase of the BYOS. - Flight Control & Navigation Console boards These boards are printed at the sci/engi/cargo lathe after researching the Shuttle Engineering techweb node. If built on a custom shuttle, it will automatically link to it, unless the shuttle already has such a console. If built on a turf that is valid for custom shuttle construction, it will automatically link to any shuttle constructed from or expanded with that turf. - Shuttle blueprints Standard shuttle blueprints can be printed at the sci/engi/cargo lathe after researching the Shuttle Engineering techweb node. A cyborg upgrade granting access to a shuttle blueprint database can be printed at the exofab after researching the aforementioned node. Crude shuttle blueprints can be crafted by hand with a sheet of paper and either a rainbow crayon or 10 uses of a blue crayon or spraycan. If Science won't research the tech, you can also buy a goody pack containing a flight control board, a docker board, two engine boards, and a set of shuttle blueprints for 1200 credits, if you have aux base access. A shuttle can be constructed atop any continuous region of turfs containing a shuttle rod lattice or a catwalk/tile built upon such. Currently, this region cannot intersect any area other than space, lavaland, the icemoon, or the station asteroid. Preexisting custom areas can be included in the construction of the shuttle, but only if every turf in the custom area is valid for shuttle construction. In the shuttle blueprint UI, you can toggle a visualizer to display which turfs fulfill all of the aforementioned conditions. The following video goes through the basic process of shuttle construction. https://github.com/user-attachments/assets/3283422e-a201-4978-972d-67527b5df4ee The blueprint used to construct the shuttle will be its master blueprint. The master blueprint can be copied to other blank shuttle blueprints (or to engiborgs with the shuttle database upgrade), and allows the holder to perform a christening ritual on the shuttle to rename it. If a shuttle's master blueprint ceases to exist, a blank blueprint can be linked to the shuttle to become the new master blueprint, or an existing blueprint associated with that shuttle can be promoted to the master blueprint. Once constructed, the following options are available from the blueprint UI to modify it: - Create Area Convert a continuous open area of the shuttle into a new area with the name written in the above text input. This operates very similarly to regular area construction. - Rename Area Change the name of the area you're currently in to the name written in the above text input. - Expand Area Add a continuous open area of the shuttle to the neighboring area selected from the dropdown to the left. This operates like regular area expansion. - Expand Shuttle Expand a shuttle with valid frame turfs as defined above. These turfs must be physically connected to the shuttle. - Remove Area Remove an area, giving its tiles to the default shuttle area. - Cleanup Empty Space (implemented after the above video was recorded) Removes all completely empty turfs from the shuttle. If all the turfs in one of the shuttle's areas were removed, that area is deleted. If absolutely no turfs of the shuttle remain, the shuttle itself is deleted. Due to the ability for this action to delete the shuttle, only the master blueprint can do it. As mentioned above, the shuttle's master blueprint can be used to christen its associated shuttle. To do this, fill a glass drink bottle with some amount of reagents, then hit it against one of the shuttle's walls from outside while holding the master blueprint. You will be prompted to enter a new name for the shuttle. The variety of things that can happen while inputting a new name can cause the christening rite to fail in one of several humorous ways. ### Optional (Unless specifically requested by a maintainer) Todo's - [x] A way for shuttle circuits to be obtainable without techweb nodes - [x] A more convenient way to carry around shuttle engines or the means to deploy them - [ ] A shuttle construction guide available as a reference book - [ ] Allow boards to be linked to shuttles before construction so they can be used outside the shuttle ## Why It's Good For The Game Shuttles have been part of the sandbox for an incredibly long time, but their limited accessibility has rendered them the exclusive territory of lucky space explorers or the few antagonists who get one off the bat (nukies and pirates). Giving players the means to construct shuttles to their liking opens up a variety of possibilities for gimmicks for antags and non-antags alike. Besides the applications for antaggery and crew-sided gimmicks, this provides side content for several departments to engage with during the relatively-frequent periods of time where they have little else to do as part of their intended roles. With respect to engineering, if the station isn't actively being damaged, the supermatter is in perfect working order, and nobody is clamoring for machine upgrades, engineers have little else to resort to other than construction projects. While the BSA station goal provides an incentive for engineers to construct dedicated rooms for the cannon, it will not necessarily be available every round. Custom shuttles not only provide such a construction project to pursue, but provide the rare opportunity, as well as a very good reason, to set up an independent power network, complete with its own power source. While atmos techs have a lot to do with gas mixing and the crystallizer, they rarely get the opportunity to set up working life support systems outside of repairing the ones that get blown up. Custom shuttles will frequently start with no air, and unless the design settled upon is an open floor plan, it will have several independent chambers that cannot so easily be profused with a proper airmix by just opening a canister. Furthermore, if the air in a custom shuttle gets messed up, a proper scrubber and distro network is a significantly less tedious method of rectifying the problem than cleaning the air manually with portable scrubbers and pumps. Scientists, it can be argued, with their access to RPDs through ordnance, have similar opportunities to atmos techs, even though the act in and of itself is not exactly part of their duties. But compared to the other job content they could be working with after they've completed most of their gameplay loop, custom shuttle construction is a substantially more active endeavor. And I know how much people complain about late-game science content just being sitting around at a console and making gamer gear. Roboticists can have a part to play in this too. They can put their mech RCDs to a use other than 2D topdown Fortnite, and with the shuttle database upgrade, they can help interested cyborgs get in on the action. Cargo is yet another department known for having significant amounts of downtime during a considerable number of rounds. If every other department has gone through their initial rounds of departmental orders, and there isn't an active need for cargo to order lots of one thing or another, cargo techs have little to do besides mail (at least on the days where there **is** mail to deliver). Usually, if cargo techs do, in fact, do something as a department when not presented with more pressing duties, they order guns and other contraband. As funny as this is, there's not a lot of variety in how this behavior manifests. With custom shuttles, cargo can use their free time to plan, and execute, a unique collective expression of design sensibilities, not limited by the size and shape constraints of the cargo bay itself. ## Changelog :cl: Y0SH1_M4S73R (with special thanks to Vect0r, whose original PR inspired the implementation of these changes) add: Shuttle blueprints, the tool used to construct and modify custom shuttles. Print a set at a science, engineering, or cargo techfab after researching Shuttle Engineering, or craft a crude set from the crafting menu. add: Shuttle blueprint database upgrade for engineering cyborgs, printable from the Exosuit Fabricator after researching Shuttle Engineering. A version of shuttle blueprints designed for use by cyborgs. add: Shuttle frame rods, usable to construct custom shuttles. Hand-craft by using 5 rods on 1 titanium sheet, or by printing them at a science, engineering, or cargo techfab after researching Shuttle Engineering. add: Custom shuttle flight control and navigation boards, printable from a science, engineering, or cargo techfab after researching Shuttle Engineering. add: Shuttle engine boards can be printed from a science, engineering, or cargo techfab after researching shuttle engineering. add: The shuttle engine supply pack is no longer locked behind the purchase of the Build Your Own Shuttle kit. add: Shuttle Construction Starter Kit goodie pack, containing a set of shuttle blueprints, flight control and navigation console boards, and two engine boards, can be purchased from cargo for 1200 credits. Requires aux base access to purchase. refactor: Shuttles now keep track of what areas are underneath each of their individual turfs, so that the areas left behind on movement are consistent with what they were beforehand. refactor: Shuttle ceilings now place themselves down as baseturfs, instead of only appearing if the turf above is open space. /:cl: --------- Co-authored-by: vect0r <71346830+Vect0r2@users.noreply.github.com> Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com> Co-authored-by: necromanceranne <40847847+necromanceranne@users.noreply.github.com> --- code/__DEFINES/crafting.dm | 2 + code/__DEFINES/dcs/signals/signals_area.dm | 6 + code/__DEFINES/dcs/signals/signals_lattice.dm | 4 + code/__DEFINES/dcs/signals/signals_shuttle.dm | 4 + code/__DEFINES/dcs/signals/signals_turf.dm | 11 + code/__DEFINES/research/techweb_nodes.dm | 1 + code/__DEFINES/shuttles.dm | 18 + code/__DEFINES/traits/declarations.dm | 14 + code/__DEFINES/turfs.dm | 1 + code/__HELPERS/areas.dm | 55 +- code/__HELPERS/pronouns.dm | 52 + code/__HELPERS/spatial_info.dm | 10 + code/__HELPERS/string_lists.dm | 1 + code/_globalvars/lists/mapping.dm | 2 + code/_globalvars/traits/_traits.dm | 5 + .../configuration/entries/game_options.dm | 9 + code/controllers/subsystem/shuttle.dm | 2 + code/datums/components/connect_containers.dm | 2 +- code/datums/components/connect_inventory.dm | 59 + code/datums/components/connect_range.dm | 11 +- code/datums/components/crafting/tools.dm | 31 + code/datums/components/custom_area.dm | 23 + .../elements/shuttle_construction_lattice.dm | 55 + .../elements/shuttle_construction_turf.dm | 43 + code/datums/shuttles/emergency.dm | 6 - code/game/area/areas.dm | 2 + code/game/area/areas/shuttles.dm | 9 +- .../circuitboards/computer_circuitboards.dm | 21 + .../game/objects/items/robot/items/storage.dm | 5 +- .../objects/items/robot/robot_upgrades.dm | 10 + code/game/objects/items/stacks/rods.dm | 19 + .../objects/items/stacks/sheets/mineral.dm | 20 + code/game/objects/structures/lattice.dm | 15 +- code/game/shuttle_engines.dm | 10 + code/game/turfs/baseturfs.dm | 10 + code/game/turfs/change_turf.dm | 16 +- code/game/turfs/open/_open.dm | 9 +- code/game/turfs/open/floor/hull.dm | 5 - code/game/turfs/turf.dm | 3 + code/modules/cargo/goodies.dm | 13 + code/modules/cargo/packs/engineering.dm | 1 - code/modules/hallucination/_hallucination.dm | 15 +- .../research/designs/comp_board_designs.dm | 16 + .../research/designs/machine_designs.dm | 10 + .../designs/mechfabricator_designs.dm | 14 + code/modules/research/designs/misc_designs.dm | 11 + code/modules/research/designs/tool_designs.dm | 10 + .../research/techweb/nodes/engi_nodes.dm | 16 + .../shuttle/mobile_port/mobile_port.dm | 60 +- .../shuttle/mobile_port/shuttle_move.dm | 73 +- .../mobile_port/shuttle_move_callbacks.dm | 37 +- .../mobile_port/variants/assault_pod.dm | 2 +- .../mobile_port/variants/custom/blueprints.dm | 1051 +++++++++++++++++ .../mobile_port/variants/custom/custom.dm | 19 + .../variants/custom/custom_consoles.dm | 174 +++ .../shuttle/mobile_port/variants/supply.dm | 2 +- .../shuttle_consoles/navigation_computer.dm | 2 +- .../shuttle_consoles/shuttle_console.dm | 1 + config/game_options.txt | 4 + icons/mob/actions/actions_shuttle.dmi | Bin 0 -> 3034 bytes icons/obj/items_cyborg.dmi | Bin 11940 -> 12370 bytes icons/obj/scrolls.dmi | Bin 1654 -> 4281 bytes tgstation.dme | 8 + tgui/packages/tgui/constants.ts | 15 + .../tgui/interfaces/ShuttleBlueprints.tsx | 422 +++++++ 65 files changed, 2450 insertions(+), 107 deletions(-) create mode 100644 code/__DEFINES/dcs/signals/signals_lattice.dm create mode 100644 code/datums/components/connect_inventory.dm create mode 100644 code/datums/components/custom_area.dm create mode 100644 code/datums/elements/shuttle_construction_lattice.dm create mode 100644 code/datums/elements/shuttle_construction_turf.dm create mode 100644 code/modules/shuttle/mobile_port/variants/custom/blueprints.dm create mode 100644 code/modules/shuttle/mobile_port/variants/custom/custom.dm create mode 100644 code/modules/shuttle/mobile_port/variants/custom/custom_consoles.dm create mode 100644 icons/mob/actions/actions_shuttle.dmi create mode 100644 tgui/packages/tgui/interfaces/ShuttleBlueprints.tsx diff --git a/code/__DEFINES/crafting.dm b/code/__DEFINES/crafting.dm index cb7930e9d1f..e71004f7f53 100644 --- a/code/__DEFINES/crafting.dm +++ b/code/__DEFINES/crafting.dm @@ -6,6 +6,8 @@ #define CRAFTING_MACHINERY_USE 0 ///If the structure is only "used" i.e. it checks to see if it's nearby and allows crafting, but doesn't delete it #define CRAFTING_STRUCTURE_USE 0 +///If the ingredient is only "used" i.e. it checks to see if it's nearby and allows crafting, but doesn't delete it +#define CRAFTING_INGREDIENT_USE 0 //stack recipe placement check types /// Checks if there is an object of the result type in any of the cardinal directions diff --git a/code/__DEFINES/dcs/signals/signals_area.dm b/code/__DEFINES/dcs/signals/signals_area.dm index b136f46467f..5636a4c35a0 100644 --- a/code/__DEFINES/dcs/signals/signals_area.dm +++ b/code/__DEFINES/dcs/signals/signals_area.dm @@ -38,3 +38,9 @@ ///From base of area/update_beauty() #define COMSIG_AREA_BEAUTY_UPDATED "area_beauty_updated" + +/// From base of turf/change_area(area/old_area) +#define COMSIG_AREA_TURF_ADDED "area_turf_added" + +/// From base of turf/change_area(area/new_area) +#define COMSIG_AREA_TURF_REMOVED "area_turf_removed" diff --git a/code/__DEFINES/dcs/signals/signals_lattice.dm b/code/__DEFINES/dcs/signals/signals_lattice.dm new file mode 100644 index 00000000000..fe45ad5cf52 --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_lattice.dm @@ -0,0 +1,4 @@ +//From base of /obj/structure/lattice/proc/replace_with_catwalk() : (list/post_replacement_callbacks) +/// `post_replacement_callbacks` is a list that signal handlers can mutate to append `/datum/callback` objects. +/// They will be called with the new catwalk after it has been initialized. +#define COMSIG_LATTICE_PRE_REPLACE_WITH_CATWALK "lattice_pre_replace_with_catwalk" diff --git a/code/__DEFINES/dcs/signals/signals_shuttle.dm b/code/__DEFINES/dcs/signals/signals_shuttle.dm index 336117fb868..69aa13c3900 100644 --- a/code/__DEFINES/dcs/signals/signals_shuttle.dm +++ b/code/__DEFINES/dcs/signals/signals_shuttle.dm @@ -1,3 +1,4 @@ + // Shuttle signals. this file is empty because shuttle code is ancient, feel free to // add more signals where its appropriate to have them @@ -5,3 +6,6 @@ #define COMSIG_SHUTTLE_SHOULD_MOVE "shuttle_should_move" /// Return this when the shuttle move should be blocked. #define BLOCK_SHUTTLE_MOVE (1<<0) + +//from base of /proc/expand_shuttle() : (list/turfs) +#define COMSIG_SHUTTLE_EXPANDED "shuttle_expanded" diff --git a/code/__DEFINES/dcs/signals/signals_turf.dm b/code/__DEFINES/dcs/signals/signals_turf.dm index 321fb503cbf..fb16d8c6b35 100644 --- a/code/__DEFINES/dcs/signals/signals_turf.dm +++ b/code/__DEFINES/dcs/signals/signals_turf.dm @@ -43,3 +43,14 @@ ///Called when turf no longer blocks light from passing through #define COMSIG_TURF_NO_LONGER_BLOCK_LIGHT "turf_no_longer_block_light" + +///from /turf/proc/attempt_lattice_replacement() : (list/post_successful_replacement_callbacks) +/// `post_successful_replacement_callbacks` is a list that signal handlers can mutate to append `/datum/callback` objects. +/// They will be called with the new catwalk if it is actually created. +#define COMSIG_TURF_ATTEMPT_LATTICE_REPLACEMENT "turf_replaced_with_lattice" + +///from /turf/proc/change_area() : (area/old_area) +#define COMSIG_TURF_AREA_CHANGED "turf_area_changed" + +///from /proc/create_shuttle() and /obj/docking_port/mobile/proc/add_turf() : (obj/docking_port/mobile/shuttle) +#define COMSIG_TURF_ADDED_TO_SHUTTLE "turf_added_to_shuttle" diff --git a/code/__DEFINES/research/techweb_nodes.dm b/code/__DEFINES/research/techweb_nodes.dm index f27225f1fed..c44a3f4b71a 100644 --- a/code/__DEFINES/research/techweb_nodes.dm +++ b/code/__DEFINES/research/techweb_nodes.dm @@ -107,6 +107,7 @@ #define TECHWEB_NODE_SANITATION "sanitation" #define TECHWEB_NODE_SEC_EQUIP "sec_equip" #define TECHWEB_NODE_SELECTION "selection" +#define TECHWEB_NODE_SHUTTLE_ENG "shuttle_eng" #define TECHWEB_NODE_SPEC_ENG "spec_eng" #define TECHWEB_NODE_STICKY_ADVANCED "sticky_advanced" #define TECHWEB_NODE_SURGERY "surgery" diff --git a/code/__DEFINES/shuttles.dm b/code/__DEFINES/shuttles.dm index 12f15ab1e68..8a31acfc087 100644 --- a/code/__DEFINES/shuttles.dm +++ b/code/__DEFINES/shuttles.dm @@ -130,3 +130,21 @@ #define HIJACK_STAGE_3 3 #define HIJACK_STAGE_4 4 #define HIJACK_COMPLETED 5 + +// Custom shuttle construction errors +#define ORIGIN_NOT_ON_SHUTTLE 1 << 0 + +// The following two errors occur in mutually exclusive contexts, so they can share the same bitflag +#define TOO_MANY_SHUTTLES 1 << 1 +#define FRAME_NOT_ADJACENT_TO_LINKED_SHUTTLE 1 << 1 + +#define ABOVE_MAX_SHUTTLE_SIZE 1 << 2 +#define CUSTOM_AREA_NOT_COMPLETELY_CONTAINED 1 << 3 +#define INTERSECTS_NON_WHITELISTED_AREA 1 << 4 + +// Custom shuttle engine defines +#define CUSTOM_ENGINE_COEFF_MIN 0.5 +#define CUSTOM_ENGINE_COEFF_MAX 10 + +#define CUSTOM_ENGINE_POWER_MULTIPLIER 25 +#define CUSTOM_ENGINE_POWER_TURF_COUNT_OFFSET 5 diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index db594d235c8..1c081f14c54 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -1438,4 +1438,18 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// Prevents fishing achievement from being granted by catching one of these #define TRAIT_NO_FISHING_ACHIEVEMENT "no_fishing_achievement" +/** + * This trait is given to turfs that have had shuttle frame parts built on them, but are not yet part of a shuttle. + * When attempting custom shuttle creation, a flood fill algorithm + * checks for turfs with this trait to determine the turfs + * that will constitute the created shuttle. + */ +#define TRAIT_SHUTTLE_CONSTRUCTION_TURF "shuttle_construction_turf" + +// Trait given to areas with a shuttle construction turf in them +#define TRAIT_HAS_SHUTTLE_CONSTRUCTION_TURF "has_shuttle_construction_turf" + +///A trait given to users as a mutex to prevent repeated unresolved attempts to christen a shuttle +#define TRAIT_ATTEMPTING_CHRISTENING "attempting_christening" + // END TRAIT DEFINES diff --git a/code/__DEFINES/turfs.dm b/code/__DEFINES/turfs.dm index ac4f62f10ee..f66edbed8ad 100644 --- a/code/__DEFINES/turfs.dm +++ b/code/__DEFINES/turfs.dm @@ -5,6 +5,7 @@ #define CHANGETURF_INHERIT_AIR (1<<4) // Inherit air from previous turf. Implies CHANGETURF_IGNORE_AIR #define CHANGETURF_RECALC_ADJACENT (1<<5) //Immediately recalc adjacent atmos turfs instead of queuing. #define CHANGETURF_TRAPDOOR_INDUCED (1<<6) // Caused by a trapdoor, for trapdoor to know that this changeturf was caused by itself +#define CHANGETURF_GENERATE_SHUTTLE_CEILING (1<<7) // Generate a shuttle ceiling on the above turf #define IS_OPAQUE_TURF(turf) (turf.directional_opacity == ALL_CARDINALS) diff --git a/code/__HELPERS/areas.dm b/code/__HELPERS/areas.dm index 8e818e0e7f4..2395bcbde95 100644 --- a/code/__HELPERS/areas.dm +++ b/code/__HELPERS/areas.dm @@ -1,4 +1,6 @@ #define BP_MAX_ROOM_SIZE 300 +#define EXTRA_ROOM_CHECK_SKIP 1 +#define EXTRA_ROOM_CHECK_FAIL 2 GLOBAL_LIST_INIT(typecache_powerfailure_safe_areas, typecacheof(list( /area/station/engineering/main, @@ -13,8 +15,9 @@ GLOBAL_LIST_INIT(typecache_powerfailure_safe_areas, typecacheof(list( // Returns an associative list of turf|dirs pairs // The dirs are connected turfs in the same space // break_if_found is a typecache of turf/area types to return false if found +// extra_check is an optional callback to invoke on each turf checked, and can specify whether to skip processing the turf or return false // Please keep this proc type agnostic. If you need to restrict it do it elsewhere or add an arg. -/proc/detect_room(turf/origin, list/break_if_found = list(), max_size=INFINITY) +/proc/detect_room(turf/origin, list/break_if_found = list(), max_size=INFINITY, datum/callback/extra_check) if(origin.blocks_air) return list(origin) @@ -35,6 +38,11 @@ GLOBAL_LIST_INIT(typecache_powerfailure_safe_areas, typecacheof(list( continue checked_turfs[sourceT] |= dir checked_turfs[checkT] |= REVERSE_DIR(dir) + switch(extra_check?.Invoke(checkT)) + if(EXTRA_ROOM_CHECK_SKIP) + continue + if(EXTRA_ROOM_CHECK_FAIL) + return FALSE .[sourceT] |= dir .[checkT] |= REVERSE_DIR(dir) if(break_if_found[checkT.type] || break_if_found[checkT.loc.type]) @@ -81,6 +89,28 @@ GLOBAL_LIST_INIT(typecache_powerfailure_safe_areas, typecacheof(list( return counter += 1 //increment by one so the next loop will start at the next position in the list +/proc/set_turfs_to_area(list/turf/turfs, area/new_area, list/area/affected_areas = list()) + for(var/turf/the_turf as anything in turfs) + var/area/old_area = the_turf.loc + + //keep rack of all areas affected by turf changes + affected_areas[old_area.name] = old_area + + //move the turf to its new area and unregister it from the old one + the_turf.change_area(old_area, new_area) + + //inform atoms on the turf that their area has changed + for(var/atom/stuff as anything in the_turf) + //unregister the stuff from its old area + SEND_SIGNAL(stuff, COMSIG_EXIT_AREA, old_area) + + //register the stuff to its new area. special exception for apc as its not registered to this signal + if(istype(stuff, /obj/machinery/power/apc)) + var/obj/machinery/power/apc/area_apc = stuff + area_apc.assign_to_area() + else + SEND_SIGNAL(stuff, COMSIG_ENTER_AREA, new_area) + /proc/create_area(mob/creator, new_area_type = /area) // Passed into the above proc as list/break_if_found var/static/list/area_or_turf_fail_types = typecacheof(list( @@ -135,8 +165,10 @@ GLOBAL_LIST_INIT(typecache_powerfailure_safe_areas, typecacheof(list( if(!str) return newA = new area_choice + newA.AddComponent(/datum/component/custom_area) newA.setup(str) newA.default_gravity = oldA.default_gravity + GLOB.custom_areas[newA] = TRUE require_area_resort() //new area registered. resort the names else newA = area_choice @@ -151,26 +183,7 @@ GLOBAL_LIST_INIT(typecache_powerfailure_safe_areas, typecacheof(list( * we use this to keep track of what areas are affected by the blueprints & what machinery of these areas needs to be reconfigured accordingly */ var/list/area/affected_areas = list() - for(var/turf/the_turf as anything in turfs) - var/area/old_area = the_turf.loc - - //keep rack of all areas affected by turf changes - affected_areas[old_area.name] = old_area - - //move the turf to its new area and unregister it from the old one - the_turf.change_area(old_area, newA) - - //inform atoms on the turf that their area has changed - for(var/atom/stuff as anything in the_turf) - //unregister the stuff from its old area - SEND_SIGNAL(stuff, COMSIG_EXIT_AREA, old_area) - - //register the stuff to its new area. special exception for apc as its not registered to this signal - if(istype(stuff, /obj/machinery/power/apc)) - var/obj/machinery/power/apc/area_apc = stuff - area_apc.assign_to_area() - else - SEND_SIGNAL(stuff, COMSIG_ENTER_AREA, newA) + set_turfs_to_area(turfs, newA, affected_areas) newA.reg_in_areas_in_z() diff --git a/code/__HELPERS/pronouns.dm b/code/__HELPERS/pronouns.dm index 28bb4c11662..6568909c964 100644 --- a/code/__HELPERS/pronouns.dm +++ b/code/__HELPERS/pronouns.dm @@ -57,6 +57,9 @@ /datum/proc/p_es(temp_gender) return "es" +/datum/proc/p_themselves(temp_gender) + return "itself" + /datum/proc/plural_s(pluralize) switch(copytext_char(pluralize, -2)) if ("ss") @@ -178,6 +181,19 @@ if(temp_gender != PLURAL && temp_gender != NEUTER) return "es" +/client/p_themselves(temp_gender) + if(!temp_gender) + temp_gender = gender + switch(temp_gender) + if(FEMALE) + return "herself" + if(MALE) + return "himself" + if(PLURAL) + return "themselves" + else + return "itself" + //mobs(and atoms but atoms don't really matter write your own proc overrides) also have gender! /mob/p_they(temp_gender) if(!temp_gender) @@ -271,6 +287,19 @@ if(temp_gender != PLURAL) return "es" +/mob/p_themselves(temp_gender) + if(!temp_gender) + temp_gender = gender + switch(temp_gender) + if(FEMALE) + return "herself" + if(MALE) + return "himself" + if(PLURAL) + return "themselves" + else + return "itself" + //humans need special handling, because they can have their gender hidden /mob/living/carbon/human/p_they(temp_gender) var/obscured = check_obscured_slots() @@ -342,6 +371,13 @@ temp_gender = PLURAL return ..() +/mob/living/carbon/human/p_themselves(temp_gender) + var/obscured = check_obscured_slots() + var/skipface = (wear_mask && (wear_mask.flags_inv & HIDEFACE)) || (head && (head.flags_inv & HIDEFACE)) + if((obscured & ITEM_SLOT_ICLOTHING) && skipface) + temp_gender = PLURAL + return ..() + //clothing need special handling due to pairs of items, ie gloves vs a singular glove, shoes, ect. /obj/item/clothing/p_they(temp_gender) if(!temp_gender) @@ -411,6 +447,19 @@ if(temp_gender != PLURAL) return "es" +/obj/item/clothing/p_themselves(temp_gender) + if(!temp_gender) + temp_gender = gender + switch(temp_gender) + if(FEMALE) + return "herself" + if(MALE) + return "himself" + if(PLURAL) + return "themselves" + else + return "itself" + /datum/mind/p_they(temp_gender) return current?.p_they(temp_gender) || ..() @@ -440,3 +489,6 @@ /datum/mind/p_es(temp_gender) return current?.p_es(temp_gender) || ..() + +/datum/mind/p_themselves(temp_gender) + return current?.p_themselves(temp_gender) || ..() diff --git a/code/__HELPERS/spatial_info.dm b/code/__HELPERS/spatial_info.dm index 050c569cbdf..3125f380902 100644 --- a/code/__HELPERS/spatial_info.dm +++ b/code/__HELPERS/spatial_info.dm @@ -443,6 +443,16 @@ ) list_clear_nulls(.) +///Returns a list of all turfs that are adjacent to the center atom's turf, clear the list of nulls at the end. +/proc/get_adjacent_turfs(atom/center) + . = list( + get_step(center, NORTH), + get_step(center, SOUTH), + get_step(center, EAST), + get_step(center, WEST) + ) + list_clear_nulls(.) + ///Checks if the mob provided (must_be_alone) is alone in an area /proc/alone_in_area(area/the_area, mob/must_be_alone, check_type = /mob/living/carbon) var/area/our_area = get_area(the_area) diff --git a/code/__HELPERS/string_lists.dm b/code/__HELPERS/string_lists.dm index 99ce28fba1d..eae730213f7 100644 --- a/code/__HELPERS/string_lists.dm +++ b/code/__HELPERS/string_lists.dm @@ -22,6 +22,7 @@ GLOBAL_LIST_EMPTY(string_lists) stack_trace("The baseturfs list of [baseturf_holder] at [baseturf_holder.x], [baseturf_holder.y], [baseturf_holder.x] is [length(values)], it should never be this long, investigate. I've set baseturfs to a flashing wall as a visual queue") baseturf_holder.ChangeTurf(/turf/closed/indestructible/baseturfs_ded, list(/turf/closed/indestructible/baseturfs_ded), flags = CHANGETURF_FORCEOP) return string_list(list(/turf/closed/indestructible/baseturfs_ded)) //I want this reported god damn it + return string_list(values) /turf/closed/indestructible/baseturfs_ded diff --git a/code/_globalvars/lists/mapping.dm b/code/_globalvars/lists/mapping.dm index 78680d4b216..35a18fffdd3 100644 --- a/code/_globalvars/lists/mapping.dm +++ b/code/_globalvars/lists/mapping.dm @@ -147,6 +147,8 @@ GLOBAL_LIST_EMPTY(areas) GLOBAL_LIST_EMPTY(sortedAreas) /// An association from typepath to area instance. Only includes areas with `unique` set. GLOBAL_LIST_EMPTY_TYPED(areas_by_type, /area) +/// A list of player-created areas. +GLOBAL_LIST_EMPTY_TYPED(custom_areas, /area) GLOBAL_LIST_EMPTY(all_abstract_markers) diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index 820cf66fa69..9151dcb5bc7 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -88,6 +88,9 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_WAS_RENAMED" = TRAIT_WAS_RENAMED, "TRAIT_WEATHER_IMMUNE" = TRAIT_WEATHER_IMMUNE, ), + /area = list( + "TRAIT_HAS_SHUTTLE_CONSTRUCTION_TURF" = TRAIT_HAS_SHUTTLE_CONSTRUCTION_TURF, + ), /datum/controller/subsystem/economy = list( "TRAIT_MARKET_CRASHING" = TRAIT_MARKET_CRASHING, ), @@ -157,6 +160,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_ANTIMAGIC" = TRAIT_ANTIMAGIC, "TRAIT_ANTIMAGIC_NO_SELFBLOCK" = TRAIT_ANTIMAGIC_NO_SELFBLOCK, "TRAIT_ANXIOUS" = TRAIT_ANXIOUS, + "TRAIT_ATTEMPTING_CHRISTENING" = TRAIT_ATTEMPTING_CHRISTENING, "TRAIT_BADDNA" = TRAIT_BADDNA, "TRAIT_BADTOUCH" = TRAIT_BADTOUCH, "TRAIT_BALD" = TRAIT_BALD, @@ -740,6 +744,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_IMMERSE_STOPPED" = TRAIT_IMMERSE_STOPPED, "TRAIT_LAVA_STOPPED" = TRAIT_LAVA_STOPPED, "TRAIT_NO_TERRAFORM" = TRAIT_NO_TERRAFORM, + "TRAIT_SHUTTLE_CONSTRUCTION_TURF" = TRAIT_SHUTTLE_CONSTRUCTION_TURF, "TRAIT_SPINNING_WEB_TURF" = TRAIT_SPINNING_WEB_TURF, "TRAIT_TURF_IGNORE_SLIPPERY" = TRAIT_TURF_IGNORE_SLIPPERY, "TRAIT_TURF_IGNORE_SLOWDOWN" = TRAIT_TURF_IGNORE_SLOWDOWN, diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm index e2dd97935e0..0a9cd40bddd 100644 --- a/code/controllers/configuration/entries/game_options.dm +++ b/code/controllers/configuration/entries/game_options.dm @@ -494,3 +494,12 @@ integer = FALSE default = 1 min_val = 0.05 + + +//Custom Shuttles +//Shuttle size limiter +/datum/config_entry/number/max_shuttle_count + default = 6 + +/datum/config_entry/number/max_shuttle_size + default = 250 diff --git a/code/controllers/subsystem/shuttle.dm b/code/controllers/subsystem/shuttle.dm index 71d9ed0189e..060e50e4159 100644 --- a/code/controllers/subsystem/shuttle.dm +++ b/code/controllers/subsystem/shuttle.dm @@ -17,6 +17,8 @@ SUBSYSTEM_DEF(shuttle) var/list/mobile_docking_ports = list() /// A list of all the stationary docking ports. var/list/stationary_docking_ports = list() + /// A list of all the custom shuttles. + var/list/custom_shuttles = list() /// A list of all the beacons that can be docked to. var/list/beacon_list = list() /// A list of all the transit docking ports. diff --git a/code/datums/components/connect_containers.dm b/code/datums/components/connect_containers.dm index 22efc634359..59eabcf6658 100644 --- a/code/datums/components/connect_containers.dm +++ b/code/datums/components/connect_containers.dm @@ -33,7 +33,7 @@ /datum/component/connect_containers/proc/set_tracked(atom/movable/new_tracked) if(tracked) UnregisterSignal(tracked, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING)) - unregister_signals(tracked.loc) + unregister_signals(tracked) tracked = new_tracked if(!tracked) return diff --git a/code/datums/components/connect_inventory.dm b/code/datums/components/connect_inventory.dm new file mode 100644 index 00000000000..c2e688171b4 --- /dev/null +++ b/code/datums/components/connect_inventory.dm @@ -0,0 +1,59 @@ +/// Behaves similar to connect_loc_behalf, but hooks into signals on items in the user's inventory +/datum/component/connect_inventory + dupe_mode = COMPONENT_DUPE_UNIQUE + + var/list/connections + + var/mob/living/tracked + + var/allowed_slots + +/datum/component/connect_inventory/Initialize(mob/living/tracked, connections, allowed_slots = ALL) + . = ..() + if(!istype(tracked)) + return COMPONENT_INCOMPATIBLE + src.connections = connections + src.tracked = tracked + src.allowed_slots = allowed_slots + +/datum/component/connect_inventory/RegisterWithParent() + RegisterSignal(tracked, COMSIG_MOB_EQUIPPED_ITEM, PROC_REF(on_equipped_item)) + RegisterSignal(tracked, COMSIG_QDELETING, PROC_REF(handle_tracked_qdel)) + update_signals() + +/datum/component/connect_inventory/UnregisterFromParent() + unregister_signals() + UnregisterSignal(tracked, list(COMSIG_MOB_EQUIPPED_ITEM, COMSIG_MOB_UNEQUIPPED_ITEM)) + +/datum/component/connect_inventory/proc/handle_tracked_qdel() + SIGNAL_HANDLER + qdel(src) + +/datum/component/connect_inventory/proc/update_signals() + unregister_signals() + + for(var/obj/item/item as anything in tracked.get_equipped_items(INCLUDE_POCKETS | INCLUDE_HELD)) + if(!(allowed_slots & tracked.get_slot_by_item(item))) + continue + RegisterSignal(item, COMSIG_ITEM_DROPPED, PROC_REF(on_unequipped_item)) + for(var/signal in connections) + parent.RegisterSignal(item, signal, connections[signal]) + +/datum/component/connect_inventory/proc/unregister_signals() + for(var/obj/item/item as anything in tracked.get_equipped_items(INCLUDE_POCKETS | INCLUDE_HELD)) + UnregisterSignal(item, COMSIG_ITEM_DROPPED) + parent.UnregisterSignal(item, connections) + +/datum/component/connect_inventory/proc/on_equipped_item(datum/source, obj/item/equipped, slot) + SIGNAL_HANDLER + if(!(allowed_slots & slot)) + return + // This handler has to be registered on the component itself because users may have their own COMSIG_ITEM_DROPPED handler for the equipped item + RegisterSignal(equipped, COMSIG_ITEM_DROPPED, PROC_REF(on_unequipped_item)) + for(var/signal in connections) + parent.RegisterSignal(equipped, signal, connections[signal]) + +/datum/component/connect_inventory/proc/on_unequipped_item(obj/item/unequipped) + SIGNAL_HANDLER + UnregisterSignal(unequipped, COMSIG_ITEM_DROPPED) + parent.UnregisterSignal(unequipped, connections) diff --git a/code/datums/components/connect_range.dm b/code/datums/components/connect_range.dm index af8ec247eb2..2a9225523d3 100644 --- a/code/datums/components/connect_range.dm +++ b/code/datums/components/connect_range.dm @@ -75,14 +75,13 @@ turfs = list() return - if(ismovable(target.loc)) + var/loc_is_movable = ismovable(target.loc) + + if(loc_is_movable) if(!works_in_containers) unregister_signals(old_loc, turfs) turfs = list() return - //Keep track of possible movement of all movables the target is in. - for(var/atom/movable/container as anything in get_nested_locs(target)) - RegisterSignal(container, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved)) //Only register/unregister turf signals if it's moved to a new turf. if(current_turf == get_turf(old_loc)) @@ -91,6 +90,10 @@ var/list/old_turfs = turfs turfs = RANGE_TURFS(range, current_turf) unregister_signals(old_loc, old_turfs - turfs) + if(loc_is_movable) + //Keep track of possible movement of all movables the target is in. + for(var/atom/movable/container as anything in get_nested_locs(target)) + RegisterSignal(container, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved)) for(var/turf/target_turf as anything in turfs - old_turfs) for(var/signal in connections) parent.RegisterSignal(target_turf, signal, connections[signal]) diff --git a/code/datums/components/crafting/tools.dm b/code/datums/components/crafting/tools.dm index d1d303daf80..a1e1fd1ab76 100644 --- a/code/datums/components/crafting/tools.dm +++ b/code/datums/components/crafting/tools.dm @@ -78,3 +78,34 @@ reqs = list(/obj/item/stack/sheet/bone = 1) time = 2 SECONDS category = CAT_TOOLS + +/datum/crafting_recipe/shuttle_blueprints + name = "Crude Shuttle Blueprints" + result = /obj/item/shuttle_blueprints/crude + reqs = list( + /obj/item/paper = 1, + /obj/item/toy/crayon = CRAFTING_INGREDIENT_USE, + ) + steps = list( + "You must use either a a blue crayon, a rainbow crayon, or a spray can.", + "The crayon or spray can you use must have at least 10 uses remaining." + ) + time = 10 SECONDS + category = CAT_TOOLS + +/datum/crafting_recipe/shuttle_blueprints/check_requirements(mob/user, list/collected_requirements) + var/list/crayons = collected_requirements[/obj/item/toy/crayon] + for(var/obj/item/toy/crayon/crayon as anything in crayons) + if(!is_type_in_list(crayon, list(/obj/item/toy/crayon/blue, /obj/item/toy/crayon/rainbow, /obj/item/toy/crayon/spraycan))) + continue + if(!crayon.check_empty(user, 10)) + return TRUE + +/datum/crafting_recipe/shuttle_blueprints/on_craft_completion(mob/user, atom/result) + var/static/list/valid_types = list(/obj/item/toy/crayon/blue, /obj/item/toy/crayon/rainbow, /obj/item/toy/crayon/spraycan) + for(var/valid_type in valid_types) + var/obj/item/toy/crayon/crayon = locate(valid_type) in range(1) + if(!crayon) + continue + if(crayon.use_charges(user, 10)) + return diff --git a/code/datums/components/custom_area.dm b/code/datums/components/custom_area.dm new file mode 100644 index 00000000000..f46a168bb86 --- /dev/null +++ b/code/datums/components/custom_area.dm @@ -0,0 +1,23 @@ +/** + * Container for data necessary to track custom areas. + * Currently this is just the areas this area was created on top of, in case the area is used to create a custom shuttle. + */ +/datum/component/custom_area + var/list/previous_areas = list() + +/datum/component/custom_area/Initialize(...) + if(!isarea(parent)) + return COMPONENT_INCOMPATIBLE + +/datum/component/custom_area/RegisterWithParent() + RegisterSignal(parent, COMSIG_AREA_TURF_ADDED, PROC_REF(on_turf_added)) + RegisterSignal(parent, COMSIG_AREA_TURF_REMOVED, PROC_REF(on_turf_removed)) + +/datum/component/custom_area/UnregisterFromParent() + UnregisterSignal(parent, list(COMSIG_AREA_TURF_ADDED, COMSIG_AREA_TURF_REMOVED)) + +/datum/component/custom_area/proc/on_turf_added(area/source, turf/turf, area/old_area) + previous_areas[turf] = old_area + +/datum/component/custom_area/proc/on_turf_removed(area/source, turf/turf) + previous_areas -= turf diff --git a/code/datums/elements/shuttle_construction_lattice.dm b/code/datums/elements/shuttle_construction_lattice.dm new file mode 100644 index 00000000000..0c89b7fe110 --- /dev/null +++ b/code/datums/elements/shuttle_construction_lattice.dm @@ -0,0 +1,55 @@ +/// Element used to specify that a lattice is part of an incomplete shuttle frame +/datum/element/shuttle_construction_lattice + element_flags = ELEMENT_DETACH_ON_HOST_DESTROY + var/list/lattices_by_turf = list() + +/datum/element/shuttle_construction_lattice/Attach(obj/structure/lattice/target) + . = ..() + if(!istype(target)) + return ELEMENT_INCOMPATIBLE + var/turf/target_turf = target.loc + if(!istype(target_turf)) + return ELEMENT_INCOMPATIBLE + target_turf.AddElementTrait(TRAIT_SHUTTLE_CONSTRUCTION_TURF, REF(target), eletype = /datum/element/shuttle_construction_turf) + lattices_by_turf[target_turf] = target + RegisterSignal(target, COMSIG_ATOM_EXAMINE, PROC_REF(on_examined)) + RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(on_moved)) + RegisterSignal(target, COMSIG_LATTICE_PRE_REPLACE_WITH_CATWALK, PROC_REF(on_replacing_with_catwalk)) + RegisterSignal(target_turf, COMSIG_TURF_ADDED_TO_SHUTTLE, PROC_REF(on_turf_added_to_shuttle)) + +/datum/element/shuttle_construction_lattice/Detach(obj/source, ...) + . = ..() + var/turf/source_turf = source.loc + if(istype(source_turf)) + REMOVE_TRAIT(source_turf, TRAIT_SHUTTLE_CONSTRUCTION_TURF, REF(source)) + lattices_by_turf -= source_turf + UnregisterSignal(source_turf, COMSIG_TURF_ADDED_TO_SHUTTLE) + UnregisterSignal(source, list(COMSIG_ATOM_EXAMINE, COMSIG_MOVABLE_MOVED, COMSIG_LATTICE_PRE_REPLACE_WITH_CATWALK)) + +/datum/element/shuttle_construction_lattice/proc/on_examined(obj/source, mob/user, list/examine_list) + SIGNAL_HANDLER + examine_list += span_notice("Cutting this [source.name] will ruin the treatment that makes it suitable for shuttle construction.") + +/datum/element/shuttle_construction_lattice/proc/on_moved(obj/source, atom/old_loc) + SIGNAL_HANDLER + var/trait_source = REF(source) + if(isturf(old_loc)) + REMOVE_TRAIT(old_loc, TRAIT_SHUTTLE_CONSTRUCTION_TURF, trait_source) + UnregisterSignal(old_loc, COMSIG_TURF_ADDED_TO_SHUTTLE) + lattices_by_turf -= old_loc + var/turf/new_turf = source.loc + if(istype(new_turf)) + new_turf.AddElementTrait(TRAIT_SHUTTLE_CONSTRUCTION_TURF, trait_source, /datum/element/shuttle_construction_turf) + RegisterSignal(new_turf, COMSIG_TURF_ADDED_TO_SHUTTLE, PROC_REF(on_turf_added_to_shuttle)) + lattices_by_turf[new_turf] = source + +/datum/element/shuttle_construction_lattice/proc/on_replacing_with_catwalk(obj/source, list/callbacks) + SIGNAL_HANDLER + callbacks += CALLBACK(src, PROC_REF(register_catwalk)) + +/datum/element/shuttle_construction_lattice/proc/register_catwalk(obj/structure/lattice/catwalk/new_catwalk) + new_catwalk.AddElement(/datum/element/shuttle_construction_lattice) + +/datum/element/shuttle_construction_lattice/proc/on_turf_added_to_shuttle(turf/source) + var/obj/structure/lattice/turf_lattice = lattices_by_turf[source] + turf_lattice?.RemoveElement(/datum/element/shuttle_construction_lattice) diff --git a/code/datums/elements/shuttle_construction_turf.dm b/code/datums/elements/shuttle_construction_turf.dm new file mode 100644 index 00000000000..3c345e5dbf6 --- /dev/null +++ b/code/datums/elements/shuttle_construction_turf.dm @@ -0,0 +1,43 @@ +/// Element used to track the conditions for a turf being part of an incomplete shuttle frame +/datum/element/shuttle_construction_turf + element_flags = ELEMENT_DETACH_ON_HOST_DESTROY + +/datum/element/shuttle_construction_turf/Attach(turf/target) + . = ..() + if(!istype(target)) + return ELEMENT_INCOMPATIBLE + RegisterSignal(target, COMSIG_TURF_CHANGE, PROC_REF(pre_turf_changed)) + RegisterSignal(target, COMSIG_TURF_ATTEMPT_LATTICE_REPLACEMENT, PROC_REF(pre_lattice_replacement)) + RegisterSignal(target, COMSIG_TURF_ADDED_TO_SHUTTLE, PROC_REF(on_turF_added_to_shuttle)) + ADD_TRAIT((get_area(target)), TRAIT_HAS_SHUTTLE_CONSTRUCTION_TURF, REF(target)) + +/datum/element/shuttle_construction_turf/Detach(turf/source, ...) + . = ..() + UnregisterSignal(source, list(COMSIG_TURF_CHANGE, COMSIG_TURF_ATTEMPT_LATTICE_REPLACEMENT, COMSIG_TURF_ADDED_TO_SHUTTLE, SIGNAL_REMOVETRAIT(TRAIT_SHUTTLE_CONSTRUCTION_TURF))) + REMOVE_TRAIT((get_area(source)), TRAIT_HAS_SHUTTLE_CONSTRUCTION_TURF, REF(source)) + +///Changing or destroying the turf detaches the element, also we need to reapply the traits since they don't get passed down. +/datum/element/shuttle_construction_turf/proc/pre_turf_changed(turf/source, path, list/new_baseturfs, flags, list/post_change_callbacks) + SIGNAL_HANDLER + var/list/old_trait_sources = GET_TRAIT_SOURCES(source, TRAIT_SHUTTLE_CONSTRUCTION_TURF) + old_trait_sources = old_trait_sources.Copy() + post_change_callbacks += CALLBACK(src, PROC_REF(post_turf_changed), old_trait_sources) + +/datum/element/shuttle_construction_turf/proc/pre_lattice_replacement(turf/source, list/post_successful_replacement_callbacks) + SIGNAL_HANDLER + post_successful_replacement_callbacks += CALLBACK(src, PROC_REF(register_lattice)) + +/datum/element/shuttle_construction_turf/proc/post_turf_changed(list/trait_sources, turf/new_turf) + if(isfloorturf(new_turf) || iswallturf(new_turf)) + trait_sources |= ELEMENT_TRAIT(type) + else + trait_sources -= ELEMENT_TRAIT(type) + if(length(trait_sources)) + for(var/source in trait_sources) + new_turf.AddElementTrait(TRAIT_SHUTTLE_CONSTRUCTION_TURF, source, type) + +/datum/element/shuttle_construction_turf/proc/register_lattice(obj/structure/lattice/new_lattice) + new_lattice.AddElement(/datum/element/shuttle_construction_lattice) + +/datum/element/shuttle_construction_turf/proc/on_turF_added_to_shuttle(turf/source) + REMOVE_TRAIT(source, TRAIT_SHUTTLE_CONSTRUCTION_TURF, ELEMENT_TRAIT(type)) diff --git a/code/datums/shuttles/emergency.dm b/code/datums/shuttles/emergency.dm index 70161d2622b..75e1d56f6e1 100644 --- a/code/datums/shuttles/emergency.dm +++ b/code/datums/shuttles/emergency.dm @@ -53,12 +53,6 @@ who_can_purchase = list(ACCESS_CAPTAIN, ACCESS_CE) occupancy_limit = "Flexible" -/datum/map_template/shuttle/emergency/construction/post_load() - . = ..() - //enable buying engines from cargo - var/datum/supply_pack/P = SSshuttle.supply_packs[/datum/supply_pack/engineering/shuttle_engine] - P.special_enabled = TRUE - /datum/map_template/shuttle/emergency/asteroid suffix = "asteroid" name = "Asteroid Station Emergency Shuttle" diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm index de2b9ebb6c2..5ef0f578120 100644 --- a/code/game/area/areas.dm +++ b/code/game/area/areas.dm @@ -355,6 +355,8 @@ GLOBAL_LIST_EMPTY(teleportlocs) //just for sanity sake cause why not if(!isnull(GLOB.areas)) GLOB.areas -= src + if(!isnull(GLOB.custom_areas)) + GLOB.custom_areas -= src //machinery cleanup STOP_PROCESSING(SSobj, src) QDEL_NULL(alarm_manager) diff --git a/code/game/area/areas/shuttles.dm b/code/game/area/areas/shuttles.dm index f93f0ca4c5a..c82f04e7027 100644 --- a/code/game/area/areas/shuttles.dm +++ b/code/game/area/areas/shuttles.dm @@ -21,10 +21,17 @@ . = ..() if(ispath(added_layer, /turf/open/floor/plating)) new_baseturfs.Add(/turf/baseturf_skipover/shuttle) + . |= CHANGETURF_GENERATE_SHUTTLE_CEILING else if(ispath(new_baseturfs[1], /turf/open/floor/plating)) new_baseturfs.Insert(1, /turf/baseturf_skipover/shuttle) + . |= CHANGETURF_GENERATE_SHUTTLE_CEILING -////////////////////////////Multi-area shuttles//////////////////////////// +////////////////////////////Custom Shuttles//////////////////////////// + +/area/shuttle/custom + requires_power = TRUE + +////////////////////////////Multi-area shuttles////////////////////////////// ////////////////////////////Syndicate infiltrator//////////////////////////// diff --git a/code/game/objects/items/circuitboards/computer_circuitboards.dm b/code/game/objects/items/circuitboards/computer_circuitboards.dm index 82d96329540..e77e36a0dac 100644 --- a/code/game/objects/items/circuitboards/computer_circuitboards.dm +++ b/code/game/objects/items/circuitboards/computer_circuitboards.dm @@ -650,3 +650,24 @@ /obj/item/circuitboard/computer/exodrone_console name = "Exploration Drone Control Console" build_path = /obj/machinery/computer/exodrone_control_console + +/obj/item/circuitboard/computer/shuttle + var/shuttle_id + +/obj/item/circuitboard/computer/shuttle/configure_machine(obj/machinery/machine) + var/obj/docking_port/mobile/custom/shuttle = shuttle_id ? SSshuttle.getShuttle(shuttle_id) : SSshuttle.get_containing_shuttle(machine) + if(!shuttle) + var/on_shuttle_frame = HAS_TRAIT((get_turf(machine)), TRAIT_SHUTTLE_CONSTRUCTION_TURF) + machine.say(on_shuttle_frame ? "Console will automatically link on shuttle completion." : "No shuttle available for linking.") + else if(!istype(shuttle)) + machine.say("Cannot link to this kind of shuttle!") + else + machine.connect_to_shuttle(TRUE, shuttle) + +/obj/item/circuitboard/computer/shuttle/flight_control + name = "Shuttle Flight Control (Computer Board)" + build_path = /obj/machinery/computer/shuttle/custom_shuttle + +/obj/item/circuitboard/computer/shuttle/docker + name = "Shuttle Navigation Computer (Computer Board)" + build_path = /obj/machinery/computer/camera_advanced/shuttle_docker/custom diff --git a/code/game/objects/items/robot/items/storage.dm b/code/game/objects/items/robot/items/storage.dm index 4d90c28de6d..e0310d62584 100644 --- a/code/game/objects/items/robot/items/storage.dm +++ b/code/game/objects/items/robot/items/storage.dm @@ -251,10 +251,11 @@ ///Apparatus to allow Engineering/Sabo borgs to manipulate any material sheets. /obj/item/borg/apparatus/sheet_manipulator name = "material manipulation apparatus" - desc = "An apparatus for carrying, deploying, and manipulating sheets of material. The device can also carry custom floor tiles." + desc = "An apparatus for carrying, deploying, and manipulating sheets of material. The device can also carry custom floor tiles and shuttle frame rods." icon_state = "borg_stack_apparatus" storable = list(/obj/item/stack/sheet, - /obj/item/stack/tile) + /obj/item/stack/tile, + /obj/item/stack/rods/shuttle) /obj/item/borg/apparatus/sheet_manipulator/Initialize(mapload) update_appearance() diff --git a/code/game/objects/items/robot/robot_upgrades.dm b/code/game/objects/items/robot/robot_upgrades.dm index f448ec0ed59..a6702259316 100644 --- a/code/game/objects/items/robot/robot_upgrades.dm +++ b/code/game/objects/items/robot/robot_upgrades.dm @@ -803,6 +803,16 @@ items_to_add = list(/obj/item/storage/bag/plants/cyborg, /obj/item/borg/cyborg_omnitool/botany, /obj/item/plant_analyzer) +/obj/item/borg/upgrade/shuttle_blueprints + name = "Engineering Cyborg Shuttle Blueprint Database" + desc = "An upgrade to the engineering model cyborg allowing for the construction and expansion of shuttles." + icon_state = "module_engineer" + require_model = TRUE + model_type = list(/obj/item/robot_model/engineering, /obj/item/robot_model/saboteur) + model_flags = BORG_MODEL_ENGINEERING + + items_to_add = list(/obj/item/shuttle_blueprints/borg) + ///This isn't an upgrade or part of the same path, but I'm gonna just stick it here because it's a tool used on cyborgs. //A reusable tool that can bring borgs back to life. They gotta be repaired first, though. diff --git a/code/game/objects/items/stacks/rods.dm b/code/game/objects/items/stacks/rods.dm index 6523a52308c..127f39bfb81 100644 --- a/code/game/objects/items/stacks/rods.dm +++ b/code/game/objects/items/stacks/rods.dm @@ -145,3 +145,22 @@ GLOBAL_LIST_INIT(rod_recipes, list ( \ /obj/item/stack/rods/lava/thirty amount = 30 + +/obj/item/stack/rods/shuttle + name = "shuttle frame rods" + desc = "Treated, specialized iron rods suitable for the construction of shuttle frames or the expansion of existing shuttles." + singular_name = "shuttle frame rod" + mats_per_unit = list(/datum/material/iron=HALF_SHEET_MATERIAL_AMOUNT, /datum/material/titanium=SMALL_MATERIAL_AMOUNT) + merge_type = /obj/item/stack/rods/shuttle + +/obj/item/stack/rods/shuttle/five + amount = 5 + +/obj/item/stack/rods/shuttle/ten + amount = 10 + +/obj/item/stack/rods/shuttle/twentyfive + amount = 25 + +/obj/item/stack/rods/shuttle/fifty + amount = 50 diff --git a/code/game/objects/items/stacks/sheets/mineral.dm b/code/game/objects/items/stacks/sheets/mineral.dm index 4b9990ea04e..14a9b5dcfb7 100644 --- a/code/game/objects/items/stacks/sheets/mineral.dm +++ b/code/game/objects/items/stacks/sheets/mineral.dm @@ -296,6 +296,26 @@ GLOBAL_LIST_INIT(titanium_recipes, list ( \ . = ..() . += GLOB.titanium_recipes +/obj/item/stack/sheet/mineral/titanium/attackby(obj/item/W, mob/user, params) + add_fingerprint(user) + if(istype(W, /obj/item/stack/rods)) + var/obj/item/stack/rods/old_rods = W + if(old_rods.merge_type != /obj/item/stack/rods) + to_chat(user, span_warning("You can't craft shuttle frame rods with this type of rod!")) + if (old_rods.get_amount() >= 5 && get_amount() >= 1) + var/obj/item/stack/rods/shuttle/five/new_rods = new (get_turf(user)) + if(!QDELETED(new_rods)) + new_rods.add_fingerprint(user) + var/replace = user.get_inactive_held_item() == src + old_rods.use(5) + use(1) + if(QDELETED(src) && replace && !QDELETED(new_rods)) + user.put_in_hands(new_rods) + else + to_chat(user, span_warning("You need five rods and one sheet of titanium to make shuttle frame rods!")) + return + return ..() + /obj/item/stack/sheet/mineral/titanium/fifty amount = 50 diff --git a/code/game/objects/structures/lattice.dm b/code/game/objects/structures/lattice.dm index f22ef3f7e2b..40bc738e2fb 100644 --- a/code/game/objects/structures/lattice.dm +++ b/code/game/objects/structures/lattice.dm @@ -76,16 +76,14 @@ /obj/structure/lattice/rcd_act(mob/user, obj/item/construction/rcd/the_rcd, list/rcd_data) if(rcd_data["[RCD_DESIGN_MODE]"] == RCD_TURF) var/design_structure = rcd_data["[RCD_DESIGN_PATH]"] - if(design_structure == /turf/open/floor/plating) + if(design_structure == /turf/open/floor/plating/rcd) var/turf/T = src.loc if(isgroundlessturf(T)) T.place_on_top(/turf/open/floor/plating, flags = CHANGETURF_INHERIT_AIR) qdel(src) return TRUE if(design_structure == /obj/structure/lattice/catwalk) - var/turf/turf = loc - qdel(src) - new /obj/structure/lattice/catwalk(turf) + replace_with_catwalk() return TRUE return FALSE @@ -93,6 +91,15 @@ if(current_size >= STAGE_FOUR) deconstruct() +/obj/structure/lattice/proc/replace_with_catwalk() + var/list/post_replacement_callbacks = list() + SEND_SIGNAL(src, COMSIG_LATTICE_PRE_REPLACE_WITH_CATWALK, post_replacement_callbacks) + var/turf/turf = loc + qdel(src) + var/new_catwalk = new /obj/structure/lattice/catwalk(turf) + for(var/datum/callback/callback as anything in post_replacement_callbacks) + callback.Invoke(new_catwalk) + /obj/structure/lattice/catwalk name = "catwalk" desc = "A catwalk for easier EVA maneuvering and cable placement." diff --git a/code/game/shuttle_engines.dm b/code/game/shuttle_engines.dm index 726b7274834..f8d2a9db058 100644 --- a/code/game/shuttle_engines.dm +++ b/code/game/shuttle_engines.dm @@ -26,6 +26,8 @@ ///The mobile ship we are connected to. var/datum/weakref/connected_ship_ref + var/static/list/connections = list(COMSIG_TURF_ADDED_TO_SHUTTLE = PROC_REF(on_turf_added_to_shuttle)) + /datum/armor/power_shuttle_engine melee = 100 bullet = 10 @@ -35,6 +37,7 @@ /obj/machinery/power/shuttle_engine/Initialize(mapload) . = ..() + AddComponent(/datum/component/simple_rotation) register_context() /obj/machinery/power/shuttle_engine/connect_to_shuttle(mapload, obj/docking_port/mobile/port, obj/docking_port/stationary/dock) @@ -83,6 +86,7 @@ port.engine_list -= src port.current_engine_power -= initial(engine_power) connected_ship_ref = null + RemoveElement(/datum/element/connect_loc, connections) //Ugh this is a lot of copypasta from emitters, welding need some boilerplate reduction /obj/machinery/power/shuttle_engine/can_be_unfasten_wrench(mob/user, silent) @@ -97,11 +101,17 @@ if(. == SUCCESSFUL_UNFASTEN) if(anchored) connect_to_shuttle(port = SSshuttle.get_containing_shuttle(src)) //connect to a new ship, if needed + if(!connected_ship_ref?.resolve()) + AddElement(/datum/element/connect_loc, connections) engine_state = ENGINE_WRENCHED else unsync_ship() //not part of the ship anymore engine_state = ENGINE_UNWRENCHED +/obj/machinery/power/shuttle_engine/proc/on_turf_added_to_shuttle(turf/source, obj/docking_port/mobile/port) + SIGNAL_HANDLER + connect_to_shuttle(port = port) + /obj/machinery/power/shuttle_engine/wrench_act(mob/living/user, obj/item/tool) . = ..() default_unfasten_wrench(user, tool) diff --git a/code/game/turfs/baseturfs.dm b/code/game/turfs/baseturfs.dm index 29a288b1b59..832b7c1def5 100644 --- a/code/game/turfs/baseturfs.dm +++ b/code/game/turfs/baseturfs.dm @@ -171,3 +171,13 @@ var/floor_position = baseturfs.Find(floor) if(floor_position != 0) insert_baseturf(floor_position + 1, roof) + +/// Places a baseturf below a searched for baseturf. +/turf/proc/stack_below_baseturf(search_type, stack_type) + if(!islist(baseturfs)) + baseturfs = list(baseturfs) + var/search_position = baseturfs.Find(search_type) + if(search_position != 0) + insert_baseturf(search_position, stack_type) + else if(type == search_type) + insert_baseturf(turf_type = stack_type) diff --git a/code/game/turfs/change_turf.dm b/code/game/turfs/change_turf.dm index dc2c257ad38..9e00c034678 100644 --- a/code/game/turfs/change_turf.dm +++ b/code/game/turfs/change_turf.dm @@ -128,6 +128,16 @@ GLOBAL_LIST_INIT(blacklisted_automated_baseturfs, typecacheof(list( if(!(flags & CHANGETURF_DEFER_CHANGE)) new_turf.AfterChange(flags, old_type) + if(flags & CHANGETURF_GENERATE_SHUTTLE_CEILING) + var/turf/above = get_step_multiz(src, UP) + if(above) + if(!(istype(above, /turf/open/floor/engine/hull/ceiling) || above.depth_to_find_baseturf(/turf/open/floor/engine/hull/ceiling))) + if(istype(above, /turf/open/openspace) || istype(above, /turf/open/space/openspace)) + above.place_on_top(/turf/open/floor/engine/hull/ceiling) + else + above.stack_ontop_of_baseturf(/turf/open/openspace, /turf/open/floor/engine/hull/ceiling) + above.stack_ontop_of_baseturf(/turf/open/space/openspace, /turf/open/floor/engine/hull/ceiling) + new_turf.blueprint_data = old_bp new_turf.rcd_memory = old_rcd_memory new_turf.explosion_throw_details = old_explosion_throw_details @@ -284,8 +294,12 @@ GLOBAL_LIST_INIT(blacklisted_automated_baseturfs, typecacheof(list( /// Attempts to replace a tile with lattice. Amount is the amount of tiles to scrape away. /turf/proc/attempt_lattice_replacement(amount = 2) if(lattice_underneath) + var/list/successful_replacement_callbacks = list() + SEND_SIGNAL(src, COMSIG_TURF_ATTEMPT_LATTICE_REPLACEMENT, successful_replacement_callbacks) var/turf/new_turf = ScrapeAway(amount, flags = CHANGETURF_INHERIT_AIR) if(!istype(new_turf, /turf/open/floor)) - new /obj/structure/lattice(src) + var/new_lattice = new /obj/structure/lattice(src) + for(var/datum/callback/callback as anything in successful_replacement_callbacks) + callback.Invoke(new_lattice) else ScrapeAway(amount, flags = CHANGETURF_INHERIT_AIR) diff --git a/code/game/turfs/open/_open.dm b/code/game/turfs/open/_open.dm index 8f99f494eeb..6e0d91dbcad 100644 --- a/code/game/turfs/open/_open.dm +++ b/code/game/turfs/open/_open.dm @@ -434,10 +434,9 @@ if(catwalk_bait) if(used_rods.use(1)) - qdel(catwalk_bait) to_chat(user, span_notice("You construct a catwalk.")) playsound(src, 'sound/items/weapons/genhit.ogg', 50, TRUE) - new /obj/structure/lattice/catwalk(src) + catwalk_bait.replace_with_catwalk() else to_chat(user, span_warning("You need two rods to build a catwalk!")) return @@ -445,7 +444,9 @@ if(used_rods.use(1)) to_chat(user, span_notice("You construct a lattice.")) playsound(src, 'sound/items/weapons/genhit.ogg', 50, TRUE) - new /obj/structure/lattice(src) + var/obj/structure/lattice/new_lattice = new (src) + if(istype(used_rods, /obj/item/stack/rods/shuttle) && !istype(loc, /area/shuttle)) + new_lattice.AddElement(/datum/element/shuttle_construction_lattice) else to_chat(user, span_warning("You need one rod to build a lattice.")) @@ -462,6 +463,8 @@ playsound(src, 'sound/items/weapons/genhit.ogg', 50, TRUE) var/turf/open/floor/plating/new_plating = place_on_top(/turf/open/floor/plating, flags = CHANGETURF_INHERIT_AIR) + if(istype(loc, /area/shuttle)) + new_plating.insert_baseturf(turf_type = /turf/baseturf_skipover/shuttle) if(lattice) qdel(lattice) else diff --git a/code/game/turfs/open/floor/hull.dm b/code/game/turfs/open/floor/hull.dm index 6836c29b960..2aec1f97fd3 100644 --- a/code/game/turfs/open/floor/hull.dm +++ b/code/game/turfs/open/floor/hull.dm @@ -8,7 +8,6 @@ /turf/open/floor/engine/hull/ceiling name = "shuttle ceiling plating" - var/old_turf_type /turf/open/floor/engine/hull/ceiling/Initialize(mapload) . = ..() @@ -20,10 +19,6 @@ ///datum/unit_test/mapping_nearstation_test.dm SHOULD fail this case automatically //this is just here so the mapper responsible can easily see where the issues are directly on the map. -/turf/open/floor/engine/hull/ceiling/AfterChange(flags, oldType) - . = ..() - old_turf_type = oldType - /turf/open/floor/engine/hull/reinforced name = "exterior reinforced hull plating" desc = "Extremely sturdy exterior hull plating that separates you from the uncaring vacuum of space." diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index 7e839e9d709..c5a2f8fe423 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -244,6 +244,9 @@ GLOBAL_LIST_EMPTY(station_turfs) old_area.turfs_to_uncontain_by_zlevel[z] += src new_area.turfs_by_zlevel[z] += src new_area.contents += src + SEND_SIGNAL(src, COMSIG_TURF_AREA_CHANGED, old_area) + SEND_SIGNAL(new_area, COMSIG_AREA_TURF_ADDED, src, old_area) + SEND_SIGNAL(old_area, COMSIG_AREA_TURF_REMOVED, src, new_area) //changes to make after turf has moved on_change_area(old_area, new_area) diff --git a/code/modules/cargo/goodies.dm b/code/modules/cargo/goodies.dm index ee48e48e403..3b3aec6680c 100644 --- a/code/modules/cargo/goodies.dm +++ b/code/modules/cargo/goodies.dm @@ -366,3 +366,16 @@ desc = "Many people consider mice to be vermin, or dirty lab animals for experimentation, or a culinary delicacy. That's why we're not asking any questions, here." cost = PAYCHECK_CREW * 1.5 contains = list(/obj/item/pet_carrier/small/mouse) + +/datum/supply_pack/goody/shuttle_construction_kit + name = "Shuttle Construction Starter Kit" + desc = "Contains a set of shuttle blueprints, and the circuitboards necessary for constructing your own shuttle. \ + Well at least the ones you can't source yourself without Science's help." + cost = PAYCHECK_COMMAND * 12 //You assistants with shipwrighting ambitions can do a couple bounties, can't you? + access_view = ACCESS_AUX_BASE //Engineers have it, QM can give it to whoever, and scientists can just research the tech. + contains = list( + /obj/item/shuttle_blueprints, + /obj/item/circuitboard/computer/shuttle/flight_control, + /obj/item/circuitboard/computer/shuttle/docker, + /obj/item/circuitboard/machine/engine/propulsion = 2, + ) diff --git a/code/modules/cargo/packs/engineering.dm b/code/modules/cargo/packs/engineering.dm index dd376ec2017..0173fd69e7f 100644 --- a/code/modules/cargo/packs/engineering.dm +++ b/code/modules/cargo/packs/engineering.dm @@ -106,7 +106,6 @@ contains = list(/obj/machinery/power/shuttle_engine/propulsion/burst/cargo) crate_name = "shuttle engine crate" crate_type = /obj/structure/closet/crate/secure/engineering - special = TRUE /datum/supply_pack/engineering/tools name = "Toolbox Crate" diff --git a/code/modules/hallucination/_hallucination.dm b/code/modules/hallucination/_hallucination.dm index b3f33fc3191..df3bbe74757 100644 --- a/code/modules/hallucination/_hallucination.dm +++ b/code/modules/hallucination/_hallucination.dm @@ -107,6 +107,8 @@ var/image_layer = MOB_LAYER /// The plane of the image var/image_plane = GAME_PLANE + /// Should this image holder persist if there are no seers for it? + var/persist_without_seers = FALSE /obj/effect/client_image_holder/Initialize(mapload, list/mobs_which_see_us) . = ..() @@ -121,10 +123,7 @@ who_sees_us = list() for(var/mob/seer as anything in mobs_which_see_us) - RegisterSignal(seer, COMSIG_MOB_LOGIN, PROC_REF(show_image_to)) - RegisterSignal(seer, COMSIG_QDELETING, PROC_REF(remove_seer)) - who_sees_us += seer - show_image_to(seer) + add_seer(seer) /obj/effect/client_image_holder/Destroy(force) if(shown_image) @@ -141,6 +140,12 @@ return SET_PLANE(shown_image, PLANE_TO_TRUE(shown_image.plane), new_turf) +/obj/effect/client_image_holder/proc/add_seer(mob/new_seer) + RegisterSignal(new_seer, COMSIG_MOB_LOGIN, PROC_REF(show_image_to)) + RegisterSignal(new_seer, COMSIG_QDELETING, PROC_REF(remove_seer)) + who_sees_us += new_seer + show_image_to(new_seer) + /// Signal proc to clean up references if people who see us are deleted. /obj/effect/client_image_holder/proc/remove_seer(mob/source) SIGNAL_HANDLER @@ -150,7 +155,7 @@ who_sees_us -= source // No reason to exist, anymore - if(!QDELETED(src) && !length(who_sees_us)) + if(!QDELETED(src) && !length(who_sees_us) && !persist_without_seers) qdel(src) /// Generates the image which we take on. diff --git a/code/modules/research/designs/comp_board_designs.dm b/code/modules/research/designs/comp_board_designs.dm index 6db2a6bb24e..50ed6e43f61 100644 --- a/code/modules/research/designs/comp_board_designs.dm +++ b/code/modules/research/designs/comp_board_designs.dm @@ -395,3 +395,19 @@ RND_CATEGORY_COMPUTER + RND_SUBCATEGORY_COMPUTER_RECORDS ) departmental_flags = DEPARTMENT_BITFLAG_SERVICE + +/datum/design/board/shuttle + category = list("Computer Boards", "Shuttle Machinery") + departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO + +/datum/design/board/shuttle/flight_control + name = "Computer Design (Shuttle Flight Controls)" + desc = "Allows for the construction of circuit boards used to build a console that enables shuttle flight" + id = "shuttle_control" + build_path = /obj/item/circuitboard/computer/shuttle/flight_control + +/datum/design/board/shuttle/shuttle_docker + name = "Computer Design (Shuttle Navigation Computer)" + desc = "Allows for the construction of circuit boards used to build a console that enables the targetting of custom flight locations" + id = "shuttle_docker" + build_path = /obj/item/circuitboard/computer/shuttle/docker diff --git a/code/modules/research/designs/machine_designs.dm b/code/modules/research/designs/machine_designs.dm index 6364025924b..5fb67b7356e 100644 --- a/code/modules/research/designs/machine_designs.dm +++ b/code/modules/research/designs/machine_designs.dm @@ -1357,3 +1357,13 @@ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_CARGO ) departmental_flags = DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_ENGINEERING + +/datum/design/board/propulsion_engine + name = "Propulsion Engine Board" + desc = "The circuit for a propulsion engine." + id = "propulsion_engine" + build_path = /obj/item/circuitboard/machine/engine/propulsion + category = list( + RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ENGINEERING + ) + departmental_flags = DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING diff --git a/code/modules/research/designs/mechfabricator_designs.dm b/code/modules/research/designs/mechfabricator_designs.dm index c06eb40204e..7c288770240 100644 --- a/code/modules/research/designs/mechfabricator_designs.dm +++ b/code/modules/research/designs/mechfabricator_designs.dm @@ -1705,6 +1705,20 @@ RND_CATEGORY_MECHFAB_CYBORG_MODULES + RND_SUBCATEGORY_MECHFAB_CYBORG_MODULES_SERVICE ) +/datum/design/borg_upgrade_shuttle_blueprints + name = "Engineering Shuttle Blueprints" + id = "borg_upgrade_engineering_shuttle_blueprints" + build_type = MECHFAB + build_path = /obj/item/borg/upgrade/shuttle_blueprints + materials = list( + /datum/material/iron = SHEET_MATERIAL_AMOUNT*5, + /datum/material/glass = SHEET_MATERIAL_AMOUNT*2.5, + ) + construction_time = 4 SECONDS + category = list( + RND_CATEGORY_MECHFAB_CYBORG_MODULES + RND_SUBCATEGORY_MECHFAB_CYBORG_MODULES_ENGINEERING + ) + /datum/design/borg_upgrade_expand name = "Expand Module" id = "borg_upgrade_expand" diff --git a/code/modules/research/designs/misc_designs.dm b/code/modules/research/designs/misc_designs.dm index c7441285fc3..6221444ab9d 100644 --- a/code/modules/research/designs/misc_designs.dm +++ b/code/modules/research/designs/misc_designs.dm @@ -1116,6 +1116,17 @@ ) departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_CARGO +/datum/design/shuttle_rods + name = "Shuttle Frame Rods" + id = "shuttlerods" + build_type = PROTOLATHE + materials = list(/datum/material/iron =HALF_SHEET_MATERIAL_AMOUNT, /datum/material/titanium = SMALL_MATERIAL_AMOUNT) + build_path = /obj/item/stack/rods/shuttle + category = list( + RND_CATEGORY_CONSTRUCTION + RND_SUBCATEGORY_CONSTRUCTION_MATERIALS + ) + departmental_flags = DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING + // Experimental designs /datum/design/polymorph_belt diff --git a/code/modules/research/designs/tool_designs.dm b/code/modules/research/designs/tool_designs.dm index 901bc304dc1..fdd96513848 100644 --- a/code/modules/research/designs/tool_designs.dm +++ b/code/modules/research/designs/tool_designs.dm @@ -425,3 +425,13 @@ RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_ENGINEERING_ADVANCED ) departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING + +/datum/design/shuttle_blueprints + name = "Shuttle Blueprints" + desc = "Blueprints suitable for constructing shuttles" + id = "shuttle_blueprints" + build_type = PROTOLATHE + materials = list(/datum/material/plastic = HALF_SHEET_MATERIAL_AMOUNT) + build_path = /obj/item/shuttle_blueprints + category = list(RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_ENGINEERING) + departmental_flags = DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_SCIENCE diff --git a/code/modules/research/techweb/nodes/engi_nodes.dm b/code/modules/research/techweb/nodes/engi_nodes.dm index 75c9459771c..69f653d8db8 100644 --- a/code/modules/research/techweb/nodes/engi_nodes.dm +++ b/code/modules/research/techweb/nodes/engi_nodes.dm @@ -182,6 +182,22 @@ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_1_POINTS) announce_channels = list(RADIO_CHANNEL_ENGINEERING) +/datum/techweb_node/shuttle_engineering + id = TECHWEB_NODE_SHUTTLE_ENG + display_name = "Shuttle Engineering" + description = "Materials and equipment for constructing shuttles" + prereq_ids = list(TECHWEB_NODE_ENERGY_MANIPULATION, TECHWEB_NODE_APPLIED_BLUESPACE) + design_ids = list( + "borg_upgrade_engineering_shuttle_blueprints", + "propulsion_engine", + "shuttle_blueprints", + "shuttle_control", + "shuttle_docker", + "shuttlerods", + ) + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_1_POINTS) + announce_channels = list(RADIO_CHANNEL_ENGINEERING, RADIO_CHANNEL_SCIENCE, RADIO_CHANNEL_SUPPLY) + /datum/techweb_node/holographics id = TECHWEB_NODE_HOLOGRAPHICS display_name = "Holographics" diff --git a/code/modules/shuttle/mobile_port/mobile_port.dm b/code/modules/shuttle/mobile_port/mobile_port.dm index 8679c1f7cf7..b98528d2e08 100644 --- a/code/modules/shuttle/mobile_port/mobile_port.dm +++ b/code/modules/shuttle/mobile_port/mobile_port.dm @@ -56,7 +56,12 @@ ///List of shuttle events that can run or are running var/list/datum/shuttle_event/event_list = list() -/obj/docking_port/mobile/Initialize(mapload) + var/list/underlying_areas_by_turf = list() + + ///How many turfs this shuttle has. Used to check against max shuttle size when expanding expandable shuttles. + var/turf_count = 0 + +/obj/docking_port/mobile/Initialize(mapload, list/areas) . = ..() if(!shuttle_id) @@ -71,12 +76,17 @@ shuttle_id = "[tmp_id]_[counter]" name = "[tmp_name] [counter]" - var/list/all_turfs = return_ordered_turfs(x, y, z, dir) - for(var/i in 1 to all_turfs.len) - var/turf/curT = all_turfs[i] - var/area/cur_area = curT.loc - if(istype(cur_area, area_type)) - shuttle_areas[cur_area] = TRUE + if(areas) + for(var/area/area as anything in areas) + shuttle_areas[area] = TRUE + else + var/list/all_turfs = return_ordered_turfs(x, y, z, dir) + for(var/i in 1 to all_turfs.len) + var/turf/curT = all_turfs[i] + var/area/cur_area = curT.loc + if(istype(cur_area, area_type)) + turf_count++ + shuttle_areas[cur_area] = TRUE #ifdef TESTING highlight("#0f0") @@ -111,28 +121,28 @@ if(!length(shuttle_areas)) CRASH("Attempted to calculate a docking port's information without a template before it was assigned any areas!") // no template given, use shuttle_areas to calculate width and height - var/min_x = -1 - var/min_y = -1 - var/max_x = WORLDMAXX_CUTOFF - var/max_y = WORLDMAXY_CUTOFF + var/min_x = WORLDMAXX_CUTOFF + var/min_y = WORLDMAXY_CUTOFF + var/max_x = -1 + var/max_y = -1 for(var/area/shuttle_area as anything in shuttle_areas) for (var/list/zlevel_turfs as anything in shuttle_area.get_zlevel_turf_lists()) for(var/turf/turf as anything in zlevel_turfs) - min_x = max(turf.x, min_x) - max_x = min(turf.x, max_x) - min_y = max(turf.y, min_y) - max_y = min(turf.y, max_y) + min_x = min(turf.x, min_x) + max_x = max(turf.x, max_x) + min_y = min(turf.y, min_y) + max_y = max(turf.y, max_y) CHECK_TICK - if(min_x == -1 || max_x == WORLDMAXX_CUTOFF) + if(min_x == WORLDMAXX_CUTOFF || max_x == -1) CRASH("Failed to locate shuttle boundaries when iterating through shuttle areas, somehow.") - if(min_y == -1 || max_y == WORLDMAXY_CUTOFF) + if(min_y == WORLDMAXY_CUTOFF || max_y == -1) CRASH("Failed to locate shuttle boundaries when iterating through shuttle areas, somehow.") width = (max_x - min_x) + 1 height = (max_y - min_y) + 1 - port_x_offset = min_x - x - port_y_offset = min_y - y + port_x_offset = x - min_x + 1 + port_y_offset = y - min_y + 1 if(dir in list(EAST, WEST)) src.width = height @@ -157,13 +167,19 @@ #undef WORLDMAXX_CUTOFF #undef WORLDMAXY_CUTOFF +/obj/docking_port/mobile/is_in_shuttle_bounds(atom/A) + . = ..() + if(. && !shuttle_areas[get_area(A)]) + return FALSE + /** * Actions to be taken after shuttle is loaded but before it has been moved out of transit z-level to its final location * * Arguments: * * replace - TRUE if this shuttle is replacing an existing one. FALSE by default. + * * custom - TRUE if this shuttle should be added to the custom shuttle list. FALSE by default. */ -/obj/docking_port/mobile/register(replace = FALSE) +/obj/docking_port/mobile/register(replace = FALSE, custom = FALSE) . = ..() if(!shuttle_id) shuttle_id = "shuttle" @@ -185,6 +201,9 @@ SSshuttle.mobile_docking_ports += src + if(custom) + SSshuttle.custom_shuttles += src + /** * Actions to be taken after shuttle is loaded and has been moved to its final location * @@ -197,6 +216,7 @@ /obj/docking_port/mobile/unregister() . = ..() SSshuttle.mobile_docking_ports -= src + SSshuttle.custom_shuttles -= src diff --git a/code/modules/shuttle/mobile_port/shuttle_move.dm b/code/modules/shuttle/mobile_port/shuttle_move.dm index b7e125826dc..5c748bc2cfd 100644 --- a/code/modules/shuttle/mobile_port/shuttle_move.dm +++ b/code/modules/shuttle/mobile_port/shuttle_move.dm @@ -17,11 +17,11 @@ var/obj/docking_port/stationary/old_dock = get_docked() - // The area that gets placed under where the shuttle moved from - var/underlying_area_type = SHUTTLE_DEFAULT_UNDERLYING_AREA + // The area that gets placed under shuttle turfs that do not have their own area to place down + var/fallback_area_type = SHUTTLE_DEFAULT_UNDERLYING_AREA if(old_dock) //Dock overwrites - underlying_area_type = old_dock.area_type + fallback_area_type = old_dock.area_type /************************************************************************************************************** Both lists are associative with a turf:bitflag structure. (new_turfs bitflag space unused currently) @@ -33,11 +33,11 @@ CHECK_TICK /**************************************************************************************************************/ - // The underlying old area is the area assumed to be under the shuttle's starting location + // The fallback area is the area for shuttle turfs that have no area underneath them // If it no longer/has never existed it will be created - var/area/underlying_old_area = GLOB.areas_by_type[underlying_area_type] - if(!underlying_old_area) - underlying_old_area = new underlying_area_type(null) + var/area/fallback_area = GLOB.areas_by_type[fallback_area_type] + if(!fallback_area) + fallback_area = new fallback_area_type(null) var/rotation = 0 if(new_dock.dir != dir) //Even when the dirs are the same rotation is coming out as not 0 for some reason @@ -51,8 +51,9 @@ var/list/moved_atoms = list() //Everything not a turf that gets moved in the shuttle var/list/areas_to_move = list() //unique assoc list of areas on turfs being moved + var/list/underlying_areas = list() //unique assoc list of areas beneath turfs being moved - . = preflight_check(old_turfs, new_turfs, areas_to_move, rotation) + . = preflight_check(old_turfs, new_turfs, areas_to_move, underlying_areas, rotation) if(.) remove_ripples() return @@ -81,11 +82,11 @@ // Moving to the new location will trample the ripples there at the exact // same time any mobs there are trampled, to avoid any discrepancy where // the ripples go away before it is safe. - takeoff(old_turfs, new_turfs, moved_atoms, rotation, movement_direction, old_dock, underlying_old_area) + takeoff(old_turfs, new_turfs, moved_atoms, rotation, movement_direction, old_dock, fallback_area) CHECK_TICK - cleanup_runway(new_dock, old_turfs, new_turfs, areas_to_move, moved_atoms, rotation, movement_direction, underlying_old_area) + cleanup_runway(new_dock, old_turfs, new_turfs, areas_to_move, underlying_areas, moved_atoms, rotation, movement_direction, fallback_area) CHECK_TICK @@ -103,7 +104,7 @@ remove_ripples() return DOCKING_SUCCESS -/obj/docking_port/mobile/proc/preflight_check(list/old_turfs, list/new_turfs, list/areas_to_move, rotation) +/obj/docking_port/mobile/proc/preflight_check(list/old_turfs, list/new_turfs, list/areas_to_move, list/underlying_areas, rotation) for(var/i in 1 to length(old_turfs)) CHECK_TICK var/turf/oldT = old_turfs[i] @@ -126,11 +127,14 @@ move_mode = newT.toShuttleMove(oldT, move_mode, src) //turfs if(move_mode & MOVE_AREA) + var/area/underlying_area = underlying_areas_by_turf[oldT] + if(underlying_area) + underlying_areas[underlying_area] = TRUE areas_to_move[old_area] = TRUE old_turfs[oldT] = move_mode -/obj/docking_port/mobile/proc/takeoff(list/old_turfs, list/new_turfs, list/moved_atoms, rotation, movement_direction, old_dock, area/underlying_old_area) +/obj/docking_port/mobile/proc/takeoff(list/old_turfs, list/new_turfs, list/moved_atoms, rotation, movement_direction, old_dock, area/fallback_area) for(var/i in 1 to old_turfs.len) var/turf/oldT = old_turfs[i] var/turf/newT = new_turfs[i] @@ -141,7 +145,7 @@ if(move_mode & MOVE_AREA) var/area/shuttle_area = oldT.loc - shuttle_area.onShuttleMove(oldT, newT, underlying_old_area) //areas + shuttle_area.onShuttleMove(oldT, newT, src, fallback_area) //areas if(move_mode & MOVE_CONTENTS) for(var/k in oldT) @@ -152,8 +156,12 @@ moved_atoms[moving_atom] = oldT -/obj/docking_port/mobile/proc/cleanup_runway(obj/docking_port/stationary/new_dock, list/old_turfs, list/new_turfs, list/areas_to_move, list/moved_atoms, rotation, movement_direction, area/underlying_old_area) - underlying_old_area.afterShuttleMove(0) +/obj/docking_port/mobile/proc/cleanup_runway(obj/docking_port/stationary/new_dock, list/old_turfs, list/new_turfs, list/areas_to_move, list/underlying_areas, list/moved_atoms, rotation, movement_direction, area/fallback_area) + fallback_area.afterShuttleMove(0) + for(var/i in 1 to underlying_areas.len) + CHECK_TICK + var/area/underlying_area = underlying_areas[i] + underlying_area.afterShuttleMove(0) // Parallax handling // This needs to be done before the atom after move @@ -167,24 +175,28 @@ for(var/i in 1 to old_turfs.len) CHECK_TICK - if(!(old_turfs[old_turfs[i]] & MOVE_TURF)) - continue var/turf/old_turf = old_turfs[i] + var/old_move_mode = old_turfs[old_turf] + var/turf/old_ceiling = get_step_multiz(old_turf, UP) + if(old_ceiling) // check if a ceiling was generated previously + // remove old ceiling + if(istype(old_ceiling, /turf/open/floor/engine/hull/ceiling)) + old_ceiling.ScrapeAway() + else + old_ceiling.remove_baseturfs_from_typecache(list(/turf/open/floor/engine/hull/ceiling = TRUE)) + if(!(old_move_mode & MOVE_TURF)) + continue var/turf/new_turf = new_turfs[i] new_turf.afterShuttleMove(old_turf, rotation) //turfs var/turf/new_ceiling = get_step_multiz(new_turf, UP) // check if a ceiling is needed if(new_ceiling) // generate ceiling - if(istype(new_ceiling, /turf/open/openspace)) // why is this needed? because we have 2 different typepaths for openspace - new_ceiling.ChangeTurf(/turf/open/floor/engine/hull/ceiling, list(/turf/open/openspace)) - else if (istype(new_ceiling, /turf/open/space/openspace)) - new_ceiling.ChangeTurf(/turf/open/floor/engine/hull/ceiling, list(/turf/open/space/openspace)) - var/turf/old_ceiling = get_step_multiz(old_turf, UP) - if(old_ceiling && istype(old_ceiling, /turf/open/floor/engine/hull/ceiling)) // check if a ceiling was generated previously - // remove old ceiling - var/turf/open/floor/engine/hull/ceiling/old_shuttle_ceiling = old_ceiling - old_shuttle_ceiling.ChangeTurf(old_shuttle_ceiling.old_turf_type) - + if(!(istype(new_ceiling, /turf/open/floor/engine/hull/ceiling) || new_ceiling.depth_to_find_baseturf(/turf/open/floor/engine/hull/ceiling))) + if(istype(new_ceiling, /turf/open/openspace) || istype(new_ceiling, /turf/open/space/openspace)) + new_ceiling.place_on_top(/turf/open/floor/engine/hull/ceiling) + else + new_ceiling.stack_ontop_of_baseturf(/turf/open/openspace, /turf/open/floor/engine/hull/ceiling) + new_ceiling.stack_ontop_of_baseturf(/turf/open/space/openspace, /turf/open/floor/engine/hull/ceiling) for(var/i in 1 to moved_atoms.len) CHECK_TICK var/atom/movable/moved_object = moved_atoms[i] @@ -195,13 +207,18 @@ // lateShuttleMove (There had better be a really good reason for additional stages beyond this) - underlying_old_area.lateShuttleMove() + fallback_area.lateShuttleMove() for(var/i in 1 to areas_to_move.len) CHECK_TICK var/area/internal_area = areas_to_move[i] internal_area.lateShuttleMove() + for(var/i in 1 to underlying_areas.len) + CHECK_TICK + var/area/underlying_area = underlying_areas[i] + underlying_area.lateShuttleMove() + for(var/i in 1 to old_turfs.len) CHECK_TICK if(!(old_turfs[old_turfs[i]] & MOVE_CONTENTS | MOVE_TURF)) diff --git a/code/modules/shuttle/mobile_port/shuttle_move_callbacks.dm b/code/modules/shuttle/mobile_port/shuttle_move_callbacks.dm index 4dc5e8142b0..07d9cf65669 100644 --- a/code/modules/shuttle/mobile_port/shuttle_move_callbacks.dm +++ b/code/modules/shuttle/mobile_port/shuttle_move_callbacks.dm @@ -89,6 +89,11 @@ All ShuttleMove procs go here ///////////////////////////////////////////////////////////////////////////////////// +// Return the move_move (based on the old), without any side effects. +// This is for checking what would be moved if src is on a shuttle being moved. +/atom/movable/proc/hypotheticalShuttleMove(rotation, move_mode, obj/docking_port/mobile/moving_dock) + return move_mode + // Called on every atom in shuttle turf contents before anything has been moved // returns the new move_mode (based on the old) // WARNING: Do not leave turf contents in beforeShuttleMove or dock() will runtime @@ -141,16 +146,19 @@ All ShuttleMove procs go here return MOVE_AREA // Called on areas to move their turf between areas -/area/proc/onShuttleMove(turf/oldT, turf/newT, area/underlying_old_area) +/area/proc/onShuttleMove(turf/oldT, turf/newT, obj/docking_port/mobile/shuttle, area/fallback_area) if(newT == oldT) // In case of in place shuttle rotation shenanigans. return TRUE - oldT.change_area(src, underlying_old_area) + var/area/underlying_area = shuttle.underlying_areas_by_turf[oldT] + oldT.change_area(src, underlying_area || fallback_area) + shuttle.underlying_areas_by_turf -= oldT //The old turf has now been given back to the area that turf originaly belonged to var/area/old_dest_area = newT.loc parallax_movedir = old_dest_area.parallax_movedir newT.change_area(old_dest_area, src) + shuttle.underlying_areas_by_turf[newT] = old_dest_area return TRUE // Called on areas after everything has been moved @@ -218,6 +226,11 @@ All ShuttleMove procs go here break +/obj/machinery/camera/hypotheticalShuttleMove(rotation, move_mode, obj/docking_port/mobile/moving_dock) + . = ..() + if(. & MOVE_AREA) + . |= MOVE_CONTENTS + /obj/machinery/camera/beforeShuttleMove(turf/newT, rotation, move_mode, obj/docking_port/mobile/moving_dock) . = ..() if(. & MOVE_AREA) @@ -323,11 +336,21 @@ All ShuttleMove procs go here /************************************Structure move procs************************************/ +/obj/structure/grille/hypotheticalShuttleMove(rotation, move_mode, obj/docking_port/mobile/moving_dock) + . = ..() + if(. & MOVE_AREA) + . |= MOVE_CONTENTS + /obj/structure/grille/beforeShuttleMove(turf/newT, rotation, move_mode, obj/docking_port/mobile/moving_dock) . = ..() if(. & MOVE_AREA) . |= MOVE_CONTENTS +/obj/structure/lattice/hypotheticalShuttleMove(rotation, move_mode, obj/docking_port/mobile/moving_dock) + . = ..() + if(. & MOVE_AREA) + . |= MOVE_CONTENTS + /obj/structure/lattice/beforeShuttleMove(turf/newT, rotation, move_mode, obj/docking_port/mobile/moving_dock) . = ..() if(. & MOVE_AREA) @@ -342,6 +365,11 @@ All ShuttleMove procs go here Connect_cable(TRUE) propagate_if_no_network() +/obj/machinery/power/shuttle_engine/hypotheticalShuttleMove(move_mode) + . = ..() + if(. & MOVE_AREA) + . |= MOVE_CONTENTS + /obj/machinery/power/shuttle_engine/beforeShuttleMove(turf/newT, rotation, move_mode, obj/docking_port/mobile/moving_dock) . = ..() if(. & MOVE_AREA) @@ -365,6 +393,11 @@ All ShuttleMove procs go here /************************************Misc move procs************************************/ +/obj/docking_port/mobile/hypotheticalShuttleMove(rotation, move_mode, obj/docking_port/mobile/moving_dock) + . = ..() + if(moving_dock == src) + . |= MOVE_CONTENTS + /obj/docking_port/mobile/beforeShuttleMove(turf/newT, rotation, move_mode, obj/docking_port/mobile/moving_dock) . = ..() if(moving_dock == src) diff --git a/code/modules/shuttle/mobile_port/variants/assault_pod.dm b/code/modules/shuttle/mobile_port/variants/assault_pod.dm index 75baee5c392..aa1b7ea5259 100644 --- a/code/modules/shuttle/mobile_port/variants/assault_pod.dm +++ b/code/modules/shuttle/mobile_port/variants/assault_pod.dm @@ -7,7 +7,7 @@ return ..() -/obj/docking_port/mobile/assault_pod/initiate_docking(obj/docking_port/stationary/S1) +/obj/docking_port/mobile/assault_pod/initiate_docking(obj/docking_port/stationary/S1, force=FALSE) . = ..() if(!istype(S1, /obj/docking_port/stationary/transit)) playsound(get_turf(src.loc), 'sound/effects/explosion/explosion1.ogg',50,TRUE) diff --git a/code/modules/shuttle/mobile_port/variants/custom/blueprints.dm b/code/modules/shuttle/mobile_port/variants/custom/blueprints.dm new file mode 100644 index 00000000000..5185d268b8e --- /dev/null +++ b/code/modules/shuttle/mobile_port/variants/custom/blueprints.dm @@ -0,0 +1,1051 @@ +GLOBAL_LIST_INIT(shuttle_construction_area_whitelist, list(/area/space, /area/lavaland, /area/icemoon, /area/station/asteroid)) + +/obj/effect/client_image_holder/shuttle_construction_visualization + image_icon = 'icons/effects/alphacolors.dmi' + image_state = "transparent" + image_layer = ABOVE_NORMAL_TURF_LAYER + image_plane = ABOVE_GAME_PLANE + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + persist_without_seers = TRUE + +/datum/proximity_monitor/advanced/shuttle_construction_visualizer + edge_is_a_field = TRUE + var/mob/user + var/list/image_holders = list() + var/obj/item/shuttle_blueprints/parent + +/datum/proximity_monitor/advanced/shuttle_construction_visualizer/New(atom/_host, range, _ignore_if_not_on_turf) + . = ..() + parent = _host + +/datum/proximity_monitor/advanced/shuttle_construction_visualizer/Destroy() + . = ..() + parent = null + QDEL_LIST_ASSOC_VAL(image_holders) + +/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/set_user(mob/new_user) + if(user) + for(var/turf in image_holders) + var/obj/effect/client_image_holder/holder = image_holders[turf] + holder.remove_seer(user) + UnregisterSignal(user, list(COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT)) + var/client/client = user.client + if(client) + UnregisterSignal(client, list(COMSIG_VIEW_SET, COMSIG_CLIENT_SET_EYE)) + if(new_user) + user = new_user + for(var/turf in image_holders) + var/obj/effect/client_image_holder/holder = image_holders[turf] + holder.add_seer(user) + RegisterSignal(user, COMSIG_MOB_LOGIN, PROC_REF(on_user_login)) + RegisterSignal(user, COMSIG_MOB_LOGOUT, PROC_REF(unregister_client)) + var/client/client = user.client + if(client) + register_client(client) + else + unregister_client() + +/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/register_client(client/client) + var/atom/eye = client.eye + if(eye) + set_host(client.eye) + var/list/view_size = getviewsize(client.view) + set_range(CEILING(max(view_size[1], view_size[2])/2, 1)+1) + RegisterSignal(client, COMSIG_VIEW_SET, PROC_REF(on_view_set)) + RegisterSignal(client, COMSIG_CLIENT_SET_EYE, PROC_REF(on_set_eye)) + +/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/unregister_client() + SIGNAL_HANDLER + set_host(parent) + set_range(0) + +/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/on_user_login(mob/source) + SIGNAL_HANDLER + register_client(source.client) + +/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/on_view_set(datum/source, new_view) + SIGNAL_HANDLER + var/list/view_size = getviewsize(new_view) + set_range(CEILING(max(view_size[1], view_size[2])/2, 1)+1) + +/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/on_set_eye(datum/source, atom/old_eye, atom/new_eye) + SIGNAL_HANDLER + set_host(new_eye) + +/datum/proximity_monitor/advanced/shuttle_construction_visualizer/setup_field_turf(turf/target) + var/obj/effect/client_image_holder/shuttle_construction_visualization/holder = new(target, list()) + image_holders[target] = holder + evaluate_turf_overlay(holder, target) + RegisterSignals(target, list( + COMSIG_TURF_CHANGE, + COMSIG_TURF_AREA_CHANGED, + SIGNAL_ADDTRAIT(TRAIT_SHUTTLE_CONSTRUCTION_TURF), + SIGNAL_REMOVETRAIT(TRAIT_SHUTTLE_CONSTRUCTION_TURF) + ), PROC_REF(on_turf_updated)) + if(user) + holder.add_seer(user) + +/datum/proximity_monitor/advanced/shuttle_construction_visualizer/cleanup_field_turf(turf/target) + qdel(image_holders[target]) + image_holders -= target + UnregisterSignal(target, list( + COMSIG_TURF_CHANGE, + COMSIG_TURF_AREA_CHANGED, + SIGNAL_ADDTRAIT(TRAIT_SHUTTLE_CONSTRUCTION_TURF), + SIGNAL_REMOVETRAIT(TRAIT_SHUTTLE_CONSTRUCTION_TURF) + )) + +/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/on_turf_updated(turf/source) + SIGNAL_HANDLER + evaluate_turf_overlay(image_holders[source], source) + +/datum/proximity_monitor/advanced/shuttle_construction_visualizer/proc/evaluate_turf_overlay(obj/effect/client_image_holder/holder, turf/target) + var/area/turf_area = target.loc + if(HAS_TRAIT(target, TRAIT_SHUTTLE_CONSTRUCTION_TURF)) + if(is_type_in_list(turf_area, GLOB.shuttle_construction_area_whitelist) || GLOB.custom_areas[turf_area]) + holder.image_state = "green" + else + holder.image_state = "red" + else + if(GLOB.custom_areas[turf_area] && HAS_TRAIT(turf_area, TRAIT_HAS_SHUTTLE_CONSTRUCTION_TURF)) + holder.image_state = "red" + else + holder.image_state = "transparent" + holder.regenerate_image() + +/obj/item/shuttle_blueprints + name = "shuttle blueprints" + desc = "A blank sheet of synthetic engineering-grade paper." + icon = 'icons/obj/scrolls.dmi' + icon_state = "shuttle_blueprints0" + base_icon_state = "shuttle_blueprints" + inhand_icon_state = "blueprints" + attack_verb_continuous = list("attacks", "baps", "hits") + attack_verb_simple = list("attack", "bap", "hit") + interaction_flags_atom = parent_type::interaction_flags_atom | INTERACT_ATOM_ALLOW_USER_LOCATION | INTERACT_ATOM_IGNORE_MOBILITY + + var/base_desc = "A blank sheet of synthetic engineering-grade paper." + var/linked_desc = "A sheet of synthetic engineering-grade paper with shuttle schematics printed on it." + + //A weakref to the mobile docking port of the shuttle these blueprints are linked to, if any. + var/datum/weakref/shuttle_ref + + //Whether the holder can visualize shuttle frames (and any turfs preventing them from becoming complete shuttles) + var/visualize_frame_turfs = FALSE + + var/offset_x = 0 + var/offset_y = 0 + + var/datum/proximity_monitor/advanced/shuttle_construction_visualizer/prox_monitor + +/obj/item/shuttle_blueprints/Initialize(mapload) + . = ..() + prox_monitor = new(src, 0, FALSE) + update_appearance() + +/obj/item/shuttle_blueprints/equipped(mob/user, slot, initial) + . = ..() + var/static/list/connections = list(COMSIG_ITEM_PRE_ATTACK = PROC_REF(christen_check)) + if(slot == ITEM_SLOT_HANDS) + AddComponent(/datum/component/connect_inventory, user, connections, allowed_slots = ITEM_SLOT_HANDS) + +/obj/item/shuttle_blueprints/dropped(mob/user, silent) + . = ..() + stop_visualizing(user) + qdel(GetComponent(/datum/component/connect_inventory)) + +/obj/item/shuttle_blueprints/proc/christen_check(obj/item/reagent_containers/cup/glass/bottle/source, atom/attacked, mob/living/user) + SIGNAL_HANDLER + var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve() + if(!istype(shuttle)) + return + if(!iswallturf(attacked)) + return + if(!istype(source)) + return + if(SSshuttle.get_containing_shuttle(attacked) != shuttle) + return + if(HAS_TRAIT_FROM(user, TRAIT_ATTEMPTING_CHRISTENING, REF(shuttle))) + return + . = COMPONENT_CANCEL_ATTACK_CHAIN + if(shuttle.master_blueprint?.resolve() != src) + to_chat(user, span_warning("Only the master blueprint for \the [shuttle] grants you the right to rechristen [shuttle.p_them()]!")) + return + var/turf/user_turf = get_turf(user) + if(user_turf && isshuttleturf(user_turf)) + to_chat(user, span_warning("You can't rechristen \the [shuttle] from inside of [shuttle.p_them()]!")) + return + if(!source.isGlass) + to_chat(user, span_warning("You can't break [source] against [attacked]!")) + return + if(source.reagents.total_volume < CHEMICAL_QUANTISATION_LEVEL) + to_chat(user, span_warning("You should put some christening fluid in [source]!")) + return + INVOKE_ASYNC(src, PROC_REF(christen), user, shuttle, attacked, user.active_hand_index) + +/obj/item/shuttle_blueprints/proc/christen(mob/living/user, obj/docking_port/mobile/custom/shuttle, atom/attacked, hand) + var/trait_source = REF(shuttle) + ADD_TRAIT(user, TRAIT_ATTEMPTING_CHRISTENING, trait_source) + var/new_name = reject_bad_name(tgui_input_text(user, "What would you like to rechristen \the [shuttle] as?", "Shuttle Rechristening", max_length = 128), allow_numbers = TRUE, strict = TRUE, cap_after_symbols = FALSE) + if(QDELETED(user)) + return + REMOVE_TRAIT(user, TRAIT_ATTEMPTING_CHRISTENING, trait_source) + if(!new_name) + user.balloon_alert(user, "cancelled") + return + new_name = apply_text_macros(new_name) + var/obj/item/hitting_implement = (locate(/obj/item/reagent_containers/cup/glass/bottle) in user.held_items) || user.get_item_for_held_index(hand) + if(!user.CanReach(attacked, hitting_implement)) + user.balloon_alert(user, "out of range!") + return + var/obj/item/reagent_containers/cup/glass/bottle/bottle = hitting_implement + bottle = istype(bottle) && bottle.isGlass && bottle + var/shuttle_exists = !QDELETED(shuttle) + if(!shuttle_exists) + to_chat(user, span_warning("Wasn't there supposed to be a shuttle here?")) + var/has_blueprints = !QDELETED(src) && (src in user.gather_belongings()) + var/turf/attacked_turf = attacked + var/is_closed_turf = isclosedturf(attacked_turf) + var/is_shuttle_turf = isshuttleturf(attacked_turf) + var/off_shuttle = SSshuttle.get_containing_shuttle(get_turf(user)) != shuttle + var/has_bottle = istype(bottle) + var/bottle_empty = bottle && bottle.reagents.total_volume < CHEMICAL_QUANTISATION_LEVEL + var/is_clumsy = HAS_TRAIT(user, TRAIT_CLUMSY) + var/clumsy_check = is_clumsy && prob(25) + if(!(has_blueprints && is_closed_turf && is_shuttle_turf && off_shuttle && has_bottle && !clumsy_check && !bottle_empty)) + var/msg = "[user] tries to christen the shuttle with [hitting_implement ? "[hitting_implement.get_examine_name()]" : "[user.p_their()] empty fist"]" + var/self_msg = "You try to christen the shuttle with [hitting_implement ? "[hitting_implement.get_examine_name()]" : "your empty fist"]" + if(!is_closed_turf || clumsy_check) + msg += ", but miss[user.p_es()], hitting [user.p_themselves()] instead" + self_msg += ", but miss, hitting yourself instead" + msg += "!" + self_msg += "!" + if(is_clumsy) + msg += " What a klutz!" + var/old_combat_mode = user.combat_mode + user.set_combat_mode(TRUE) + // Stupid code to bypass pacifism checks, since unintentional hits like this one shouldn't be blocked by pacifism + var/list/pacifism_sources = GET_TRAIT_SOURCES(user, TRAIT_PACIFISM) + REMOVE_TRAIT(user, TRAIT_PACIFISM, pacifism_sources) + if(hitting_implement) + hitting_implement.attack(user, user) + bottle?.smash(user, user) // Because smashing a bottle isn't part of its main melee attack chain + else + user.resolve_unarmed_attack(user) + if(length(pacifism_sources)) + ADD_TRAIT(user, TRAIT_PACIFISM, pacifism_sources) + user.set_combat_mode(old_combat_mode) + else + if(!shuttle_exists) + msg += ", but there was no shuttle to christen!" + self_msg += ", but there was no shuttle to christen!" + else if(!off_shuttle) + msg += ", but you're suppossed to do that outside of [shuttle.p_them()]!" + self_msg += ", but you're suppossed to do that outside of [shuttle.p_them()]!" + else if(!is_shuttle_turf) + msg += ", but miss[user.p_es()], hitting [attacked] instead!" + self_msg += ", but miss, hitting [attacked] instead!" + else if(!has_bottle) + if(hitting_implement) + msg += " - which isn't a glass bottle!" + self_msg += " - which isn't a glass bottle!" + else + msg += "! That's not how you're supposed to do it!" + self_msg += "! Aren't you supposed to use a glass bottle?" + else if(bottle_empty) + msg += ", but there's no christening fluid in [bottle]!" + self_msg += ", but there's no christening fluid in [bottle]!" + else if(!has_blueprints) + msg += ", but [user.p_have()] no right to do so!" + msg += ", but have no right to do so!" + user.do_attack_animation(attacked, used_item = hitting_implement) + if(hitting_implement && !bottle) + if(hitting_implement.force) + playsound(hitting_implement, 'sound/items/weapons/smash.ogg', hitting_implement.get_clamped_volume(), TRUE, hitting_implement.stealthy_audio ? SILENCED_SOUND_EXTRARANGE : -1, falloff_distance = 0) + else + playsound(hitting_implement, 'sound/items/weapons/tap.ogg', hitting_implement.get_clamped_volume(), TRUE, -1) + else if(bottle) + bottle.smash(attacked, user) + else + playsound(user, 'sound/effects/bang.ogg', 50, TRUE) + user.visible_message(span_warning(msg), span_warning(self_msg)) + else + user.visible_message( + span_notice("[user] christens the shuttle as \the [new_name] with [hitting_implement.get_examine_name()]\ + [(bottle.reagents.total_volume < 30) ? "" : ", though the dearth of christening fluid makes for an unimpressive display"]."), + span_notice("You christen the shuttle as \the [new_name] with [hitting_implement.get_examine_name()].") + ) + user.do_attack_animation(attacked, used_item = bottle) + bottle.smash(attacked, user) + shuttle.name = new_name + rename_area(shuttle.default_area, new_name) + +/obj/item/shuttle_blueprints/proc/start_visualizing(mob/user) + visualize_frame_turfs = TRUE + RegisterSignal(user, SIGNAL_ADDTRAIT(TRAIT_USER_SCOPED), PROC_REF(stop_visualizing)) + prox_monitor.set_user(user) + prox_monitor.recalculate_field() + +/obj/item/shuttle_blueprints/proc/stop_visualizing(mob/user) + SIGNAL_HANDLER + visualize_frame_turfs = FALSE + UnregisterSignal(user, SIGNAL_ADDTRAIT(TRAIT_USER_SCOPED)) + prox_monitor.set_user(null) + +/obj/item/shuttle_blueprints/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ShuttleBlueprints", name) + ui.open() + RegisterSignal(user, COMSIG_ENTER_AREA, PROC_REF(on_user_enter_area)) + +/obj/item/shuttle_blueprints/ui_close(mob/user) + . = ..() + UnregisterSignal(user, COMSIG_ENTER_AREA) + +/obj/item/shuttle_blueprints/proc/on_user_enter_area(mob/source, area/new_area) + var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve() + if(shuttle && shuttle.default_area == new_area) + update_static_data(source) + +/obj/item/shuttle_blueprints/ui_state(mob/user) + return GLOB.hands_state + +/obj/item/shuttle_blueprints/ui_static_data(mob/user) + var/list/data = list() + var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve() + if(get_area(user) == shuttle?.default_area && length(shuttle?.shuttle_areas) > 1) + var/list/neighboring_areas = list() + var/room_turfs = detect_room(get_turf(user), max_size = CONFIG_GET(number/max_shuttle_size), extra_check = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(custom_shuttle_room_check), shuttle, neighboring_areas)) + if(room_turfs) + data["apcInMergeRegion"] = shuttle.default_area.apc && room_turfs[get_turf(shuttle.default_area.apc)] + var/list/area_refs = list() + for(var/area/neighboring_area in neighboring_areas) + area_refs[REF(neighboring_area)] = neighboring_area.name + data["neighboringAreas"] = area_refs + return data + +/obj/item/shuttle_blueprints/ui_data(mob/user) + var/data = list() + var/turf/current_turf = get_turf(user) + var/obj/docking_port/mobile/custom/linked_shuttle = shuttle_ref?.resolve() + var/linked_to_shuttle = istype(linked_shuttle) + data["linkedShuttle"] = linked_to_shuttle && shuttle_ref.reference + data["visualizing"] = visualize_frame_turfs + data["onShuttleFrame"] = HAS_TRAIT(current_turf, TRAIT_SHUTTLE_CONSTRUCTION_TURF) + if(!linked_to_shuttle) + var/obj/docking_port/mobile/custom/loc_shuttle = SSshuttle.get_containing_shuttle(current_turf) + data["tooManyShuttles"] = length(SSshuttle.custom_shuttles) >= CONFIG_GET(number/max_shuttle_count) + data["onCustomShuttle"] = istype(loc_shuttle) + data["masterExists"] = loc_shuttle?.master_blueprint?.resolve() + else + var/obj/item/shuttle_blueprints/master = linked_shuttle.master_blueprint?.resolve() + data["masterExists"] = master + data["isMaster"] = master == src + var/area/current_area = get_area(current_turf) + var/area/default_area = linked_shuttle.default_area + data["onShuttle"] = linked_shuttle.shuttle_areas[current_area] + data["inDefaultArea"] = default_area == current_area + data["currentArea"] = list(name = current_area.name, ref = REF(current_area)) + data["defaultApc"] = !!default_area.apc + var/list/apcs = list() + for(var/area/area as anything in linked_shuttle.shuttle_areas - default_area) + apcs[REF(area)] = !!area.apc + data["apcs"] = apcs + data["idle"] = linked_shuttle.mode == SHUTTLE_IDLE + return data + +/obj/item/shuttle_blueprints/proc/link_to_shuttle(obj/docking_port/mobile/custom/shuttle, is_master = FALSE) + shuttle_ref = WEAKREF(shuttle) + if(is_master) + shuttle.master_blueprint = WEAKREF(src) + RegisterSignal(shuttle, COMSIG_QDELETING, PROC_REF(on_shuttle_deleted)) + update_appearance() + +/obj/item/shuttle_blueprints/proc/on_shuttle_deleted() + SIGNAL_HANDLER + unlink(removing = TRUE) + +/obj/item/shuttle_blueprints/proc/unlink(removing = FALSE) + var/obj/docking_port/mobile/custom/shuttle = shuttle_ref.resolve() + if(!QDELETED(shuttle)) + UnregisterSignal(shuttle, COMSIG_QDELETING) + shuttle_ref = null + update_appearance() + +/obj/item/shuttle_blueprints/update_name(updates) + . = ..() + var/obj/docking_port/mobile/shuttle = shuttle_ref?.resolve() + if(shuttle) + name = get_linked_name(shuttle) + else + name = initial(name) + +/obj/item/shuttle_blueprints/proc/get_linked_name(obj/docking_port/mobile/shuttle) + return "\improper [shuttle.name] blueprints" + +/obj/item/shuttle_blueprints/update_desc(updates) + . = ..() + desc = shuttle_ref?.resolve() ? linked_desc : base_desc + +/obj/item/shuttle_blueprints/update_icon_state() + . = ..() + icon_state = "[base_icon_state][!!shuttle_ref]" + +/obj/item/shuttle_blueprints/vv_edit_var(vname, vval) + . = ..() + if(!.) + return + if(vname == NAMEOF(src, desc)) + if(shuttle_ref?.resolve()) + linked_desc = vval + else + base_desc = vval + update_desc() + +/obj/item/shuttle_blueprints/examine(mob/user) + . = ..() + . += get_shuttle_tip() + +/obj/item/shuttle_blueprints/proc/get_shuttle_tip() + . = list() + if(!shuttle_ref) + . += span_notice("It can be used to construct a custom shuttle.") + return + var/obj/docking_port/mobile/custom/shuttle = shuttle_ref.resolve() + if(!shuttle) + . += span_notice("It has the plans for a shuttle that no longer exists. It can be reused to construct a new shuttle.") + else + . += span_notice("It has the plans for \the [shuttle] on it, and can be used to expand [shuttle.p_them()] or modify [shuttle.p_their()] areas.") + if(shuttle.master_blueprint.resolve() == src) + . += span_notice("This is the master blueprint for \the [shuttle]. You can copy it to a blank set of blueprints, or to an engineering cyborg with a shuttle database module installed.") + +/obj/item/shuttle_blueprints/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers) + . = ..() + var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve() + if(!istype(shuttle)) + return + if(shuttle.master_blueprint?.resolve() != src) + return + if(istype(interacting_with, /obj/item/shuttle_blueprints)) + var/obj/item/shuttle_blueprints/other_blueprints = interacting_with + var/obj/docking_port/mobile/other_shuttle = other_blueprints.shuttle_ref?.resolve() + if(istype(other_shuttle)) + return + balloon_alert(user, "copying blueprints...") + if(!do_after(user, 5 SECONDS, other_blueprints)) + balloon_alert(user, "interrupted!") + return ITEM_INTERACT_FAILURE + other_blueprints.link_to_shuttle(shuttle) + balloon_alert(user, "copied") + return ITEM_INTERACT_SUCCESS + if(istype(interacting_with, /mob/living/silicon/robot)) + var/mob/living/silicon/robot/borg = interacting_with + var/obj/item/shuttle_blueprints/borg/other_blueprints = (locate() in borg.model.modules) || (locate() in borg.held_items) + if(!other_blueprints) + return + if(other_blueprints.shuttles.Find(shuttle_ref)) + balloon_alert(user, "already has these blueprints!") + balloon_alert(user, "copying blueprints...") + if(!do_after(user, 5 SECONDS, borg)) + balloon_alert(user, "interrupted") + return ITEM_INTERACT_FAILURE + if(QDELETED(other_blueprints)) + return ITEM_INTERACT_FAILURE + other_blueprints.shuttles |= shuttle_ref + balloon_alert(user, "copied") + return ITEM_INTERACT_SUCCESS + +/obj/item/shuttle_blueprints/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + switch(action) + if("toggleVisualization") + if(visualize_frame_turfs) + stop_visualizing(usr) + else + start_visualizing(usr) + return TRUE + if("tryBuildShuttle") + var/shuttle_dir = params["dir"] + if(!(shuttle_dir in GLOB.cardinals)) + return TRUE + var/list/shuttle_turfs = list() + var/list/shuttle_areas = list() + var/turf/shuttle_origin = get_turf(usr) + var/check_status = shuttle_build_check(shuttle_origin, shuttle_turfs, shuttle_areas) + if(check_status & ORIGIN_NOT_ON_SHUTTLE) + balloon_alert(usr, "not on shuttle frame!") + return TRUE + if(check_status & TOO_MANY_SHUTTLES) + balloon_alert(usr, "too many shuttles exist!") + return TRUE + if(check_status & ABOVE_MAX_SHUTTLE_SIZE) + balloon_alert(usr, "frame too big!") + return TRUE + if(check_status & CUSTOM_AREA_NOT_COMPLETELY_CONTAINED) + balloon_alert(usr, "frame must completely enclose custom areas!") + return TRUE + if(check_status & INTERSECTS_NON_WHITELISTED_AREA) + balloon_alert(usr, "frame overlaps disallowed areas!") + return TRUE + var/obj/docking_port/mobile/custom/shuttle = create_shuttle( + usr, + shuttle_origin, + shuttle_turfs, + shuttle_areas, + shuttle_dir, + name = "\improper Unnamed Shuttle", + id = "custom_[length(SSshuttle.custom_shuttles)+1]" + ) + link_to_shuttle(shuttle, TRUE) + return TRUE + if("tryLinkShuttle") + if(shuttle_ref?.resolve()) + balloon_alert(usr, "already linked!") + return TRUE + var/obj/docking_port/mobile/custom/shuttle = SSshuttle.get_containing_shuttle(usr) + if(!shuttle) + balloon_alert(usr, "not on shuttle!") + return TRUE + if(!istype(shuttle)) + balloon_alert(usr, "incompatible shuttle type!") + return TRUE + var/obj/item/shuttle_blueprints/master = shuttle.master_blueprint?.resolve() + if(master && (master != src)) + balloon_alert(usr, "master blueprint already exists!") + return TRUE + link_to_shuttle(shuttle, TRUE) + return TRUE + if("promoteToMaster") + var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve() + if(!shuttle) + balloon_alert(usr, "not linked!") + return TRUE + var/obj/item/shuttle_blueprints/master = shuttle.master_blueprint?.resolve() + if(master) + balloon_alert(usr, "master blueprint already exists!") + return TRUE + shuttle.master_blueprint = WEAKREF(src) + return TRUE + if("createNewArea") + var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve() + if(!shuttle) + balloon_alert(usr, "not linked!") + return TRUE + var/area_name = params["name"] + if(!area_name) + balloon_alert(usr, "no name given!") + return TRUE + var/area/current_area = get_area(usr) + var/area/default_area = shuttle.default_area + if(current_area != default_area) + balloon_alert(usr, "must be in default area!") + return TRUE + var/list/turfs = detect_room(get_turf(usr), max_size = CONFIG_GET(number/max_shuttle_size), extra_check = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(custom_shuttle_room_check), shuttle, null)) + if(!length(turfs)) + balloon_alert(usr, "invalid room!") + return TRUE + var/area/shuttle/custom/new_area = new() + new_area.name = area_name + shuttle.shuttle_areas[new_area] = TRUE + set_turfs_to_area(turfs, new_area) + new_area.reg_in_areas_in_z() + new_area.create_area_lighting_objects() + for(var/obj/machinery/door/firedoor/firelock as anything in default_area.firedoors) + firelock.CalculateAffectingAreas() + new_area.power_change() + return TRUE + if("releaseArea") + var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve() + if(!shuttle) + balloon_alert(usr, "not linked!") + return TRUE + var/area/current_area = get_area(usr) + if(!shuttle.shuttle_areas[current_area]) + balloon_alert(usr, "not on shuttle!") + return TRUE + var/area/default_area = shuttle.default_area + if(current_area == default_area) + balloon_alert(usr, "can't release default area!") + return TRUE + var/obj/machinery/power/apc/current_area_apc = current_area.apc + var/obj/machinery/power/apc/default_area_apc = default_area.apc + if(current_area_apc && default_area_apc) + balloon_alert(usr, "remove the apc first!") + return TRUE + var/list/turfs = current_area.get_turfs_by_zlevel(shuttle.z) + set_turfs_to_area(turfs, default_area) + for(var/obj/machinery/door/firedoor/firelock as anything in default_area.firedoors) + firelock.CalculateAffectingAreas() + shuttle.shuttle_areas -= current_area + qdel(current_area) + return TRUE + if("mergeIntoArea") + var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve() + if(!shuttle) + balloon_alert(usr, "not linked!") + return TRUE + var/area/current_area = get_area(usr) + var/area/default_area = shuttle.default_area + if(current_area != default_area) + balloon_alert(usr, "must be in default area!") + return TRUE + var/area/merge_area = locate(params["area"]) + if(!istype(merge_area)) + return TRUE + if(!shuttle.shuttle_areas[merge_area]) + return TRUE + if(merge_area == default_area) + return TRUE + var/list/actual_adjacent_areas = list() + var/list/turfs = detect_room(get_turf(usr), max_size = CONFIG_GET(number/max_shuttle_size), extra_check = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(custom_shuttle_room_check), shuttle, actual_adjacent_areas)) + if(!length(turfs)) + balloon_alert(usr, "invalid room!") + return TRUE + if(!actual_adjacent_areas[merge_area]) + balloon_alert(usr, "selected area not connected to room!") + return TRUE + if(merge_area.apc && default_area.apc && turfs[get_turf(default_area.apc)]) + balloon_alert(usr, "remove the apc first!") + return TRUE + set_turfs_to_area(turfs, merge_area) + for(var/obj/machinery/door/firedoor/firelock as anything in merge_area.firedoors + default_area.firedoors) + firelock.CalculateAffectingAreas() + return TRUE + if("renameArea") + var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve() + if(!shuttle) + balloon_alert(usr, "not linked!") + return TRUE + var/area/current_area = get_area(usr) + if(!shuttle.shuttle_areas[current_area]) + balloon_alert(usr, "not on shuttle!") + return TRUE + var/area/default_area = shuttle.default_area + if(current_area == default_area) + balloon_alert(usr, "can't rename default area!") + return TRUE + var/new_name = params["name"] + if(!new_name) + balloon_alert(usr, "no name given!") + return TRUE + rename_area(current_area, new_name) + return TRUE + if("expandWithFrame") + var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve() + if(!shuttle) + balloon_alert(usr, "not linked!") + return TRUE + var/list/turfs = list() + var/list/areas = list() + var/turf/origin = get_turf(usr) + var/check_status = shuttle_expand_check(origin, shuttle, turfs, areas) + if(check_status & ORIGIN_NOT_ON_SHUTTLE) + balloon_alert(usr, "not on shuttle frame!") + return TRUE + if(check_status & FRAME_NOT_ADJACENT_TO_LINKED_SHUTTLE) + balloon_alert(usr, "not connected to linked shuttle!") + return TRUE + if(check_status & ABOVE_MAX_SHUTTLE_SIZE) + balloon_alert(usr, "frame too big!") + return TRUE + if(check_status & CUSTOM_AREA_NOT_COMPLETELY_CONTAINED) + balloon_alert(usr, "frame must completely enclose custom areas!") + return TRUE + if(check_status & INTERSECTS_NON_WHITELISTED_AREA) + balloon_alert(usr, "frame overlaps disallowed areas!") + return TRUE + expand_shuttle(usr, shuttle, turfs, areas) + return TRUE + if("cleanupEmptyTurfs") + var/obj/docking_port/mobile/custom/shuttle = shuttle_ref?.resolve() + if(!shuttle) + balloon_alert(usr, "not linked!") + return TRUE + var/obj/item/shuttle_blueprints/master = shuttle.master_blueprint?.resolve() + if(master && master != src) + balloon_alert(usr, "not master blueprints!") + var/shuttle_z = shuttle.z + var/bounds_need_recalculation + var/docking_port_needs_relocated + var/list/bounds = shuttle.return_coords() + var/x0 = bounds[1] + var/y0 = bounds[2] + var/x1 = bounds[3] + var/y1 = bounds[4] + for(var/area/area as anything in shuttle.shuttle_areas) + var/list/turfs = area.get_turfs_by_zlevel(shuttle_z) + turfs = turfs.Copy() + for(var/turf/turf as anything in turfs) + var/move_mode = turf.fromShuttleMove(move_mode = MOVE_AREA) + if(move_mode & (MOVE_TURF | MOVE_CONTENTS)) + continue + for(var/atom/movable/movable as anything in turf.contents) + //CHECK_TICK + if(movable.loc != turf) + continue + if(movable == shuttle) + continue + move_mode = movable.hypotheticalShuttleMove(0, move_mode, shuttle) + if(move_mode & (MOVE_TURF | MOVE_CONTENTS)) + continue + if(shuttle.loc == turf) + docking_port_needs_relocated = TRUE + bounds_need_recalculation = TRUE + var/area/new_area = shuttle.underlying_areas_by_turf[turf] + if(!istype(new_area)) + new_area = GLOB.areas_by_type[SHUTTLE_DEFAULT_UNDERLYING_AREA] + if(!istype(new_area)) + new_area = new SHUTTLE_DEFAULT_UNDERLYING_AREA(null) + shuttle.underlying_areas_by_turf -= turf + shuttle.turf_count-- + turfs -= turf + turf.change_area(area, new_area) + if(bounds_need_recalculation) + continue + if(turf.x == x0 || turf.x == x1 || turf.y == y0 || turf.y == y1) + bounds_need_recalculation = TRUE + if((area != shuttle.default_area) && !length(turfs)) + shuttle.shuttle_areas -= area + qdel(area) + if(!shuttle.turf_count) + qdel(shuttle.default_area) + qdel(shuttle) + return + if(docking_port_needs_relocated) + shuttle.forceMove(pick(shuttle.underlying_areas_by_turf)) + if(bounds_need_recalculation) + QDEL_NULL(shuttle.assigned_transit) + shuttle.calculate_docking_port_information() + +/obj/item/shuttle_blueprints/crude + name = "crude shuttle blueprints" + desc = "This is just a sheet of paper thoroughly covered in what could either be crayon or spraypaint." + icon_state = "shuttle_blueprints_crude0" + base_icon_state = "shuttle_blueprints_crude" + base_desc = "This is just a sheet of paper thoroughly covered in what could either be crayon or spraypaint." + linked_desc = "This is just a crude doodle of a shuttle drawn on a background of what could either be crayon or spraypaint." + +/obj/item/shuttle_blueprints/borg + name = "shuttle blueprint database" + desc = "A module designed to store the plans for one or more shuttles." + icon = 'icons/obj/items_cyborg.dmi' + icon_state = "shuttle_database0" + base_icon_state = "shuttle_database" + damtype = BURN // In case fantasy affixes or adminbus end up making this actually capable of hurting someone. + attack_verb_continuous = list("attacks", "scans", "analyzes") + attack_verb_simple = list("attack", "scan", "analyze") + base_desc = "A module designed to store the plans for one or more shuttles." + linked_desc = "A module designed to store the plans for one or more shuttles." + var/list/shuttles = list() + +/obj/item/shuttle_blueprints/borg/get_shuttle_tip() + . = list() + if(!shuttle_ref) + if(!length(shuttles)) + . += span_notice("It does not have any shuttle plans stored.") + else + . += span_notice("It does not currently have a shuttle plan loaded.") + . += span_notice("In this mode, it can be used to construct a custom shuttle.") + return + var/obj/docking_port/mobile/custom/shuttle = shuttle_ref.resolve() + if(!shuttle) + . += span_notice("The currently loaded plans are for a shuttle that no longer exists. It will default to shuttle construction mode.") + else + . += span_notice("It has the plans for \the [shuttle] currently loaded, and can be used to expand [shuttle.p_them()] or modify [shuttle.p_their()] areas.") + if(shuttle.master_blueprint.resolve() == src) + . += span_notice("This is the master blueprint for \the [shuttle]. You can copy it to a blank set of blueprints, or to another engineering cyborg with a shuttle database module installed.") + +/obj/item/shuttle_blueprints/borg/unlink(removing) + if(removing) + shuttles -= shuttle_ref + ..() + +/obj/item/shuttle_blueprints/borg/ui_data(mob/user) + var/list/data = ..() + var/list/shuttle_data = list() + var/list/shuttle_name_count = list() + for(var/datum/weakref/shuttle_weakref as anything in shuttles) + var/obj/docking_port/mobile/shuttle = shuttle_weakref.resolve() + if(!shuttle) + continue + var/shuttle_name = shuttle.name + if(!shuttle_name_count[shuttle_name]) + shuttle_name_count[shuttle_name] = 0 + var/ref = shuttle_weakref.reference + shuttle_data[ref] = shuttle_name + if(shuttle_name_count[shuttle_name]++ > 1) + shuttle_data[ref] += " ([shuttle_name_count[shuttle_name]])" + if(length(shuttle_data)) + data["shuttles"] = shuttle_data + return data + +/obj/item/shuttle_blueprints/borg/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + switch(action) + if("switchShuttle") + var/ref = params["ref"] + var/obj/docking_port/mobile/shuttle = locate(ref) + if(!shuttle) + return TRUE + if(shuttles.Find(WEAKREF(shuttle))) + link_to_shuttle(shuttle) + return TRUE + if("unsetShuttle") + unlink() + return TRUE + +/obj/item/shuttle_blueprints/borg/link_to_shuttle(obj/docking_port/mobile/custom/shuttle, is_master) + . = ..() + shuttles |= WEAKREF(shuttle) + +/obj/item/shuttle_blueprints/borg/get_linked_name(obj/docking_port/mobile/shuttle) + name = "shuttle blueprint database ([shuttle.name])" + +/proc/custom_shuttle_room_check(obj/docking_port/mobile/custom/shuttle, list/neighboring_areas = list(), turf/check_turf) + if(SSshuttle.get_containing_shuttle(check_turf) != shuttle) + return EXTRA_ROOM_CHECK_SKIP + var/area/check_area = check_turf.loc + if(check_area != shuttle.default_area) + neighboring_areas[check_area] = TRUE + return EXTRA_ROOM_CHECK_SKIP + var/move_mode = MOVE_AREA + move_mode = check_turf.fromShuttleMove(move_mode = move_mode) + if(move_mode & (MOVE_CONTENTS | MOVE_TURF)) + return + for(var/atom/movable/movable as anything in check_turf.contents) + CHECK_TICK + if(movable.loc != check_turf) + continue + move_mode = movable.hypotheticalShuttleMove(0, move_mode, shuttle) + if(!(move_mode & (MOVE_CONTENTS | MOVE_TURF))) + return EXTRA_ROOM_CHECK_SKIP + +/proc/get_connected_shuttle_frame_turfs(turf/starting_turf, max_size = INFINITY, list/adjacent_shuttles) + . = list() + var/list/check_turfs = list(starting_turf) + while(length(check_turfs)) + var/turf/check_turf = check_turfs[1] + check_turfs.Cut(1,2) + .[check_turf] = TRUE + if(length(.) > max_size) + return + for(var/check_dir in GLOB.cardinals) + var/turf/neighbor = get_step(check_turf, check_dir) + if(neighbor) + if(HAS_TRAIT(neighbor, TRAIT_SHUTTLE_CONSTRUCTION_TURF) && !.[neighbor]) + check_turfs[neighbor] = TRUE + else if(islist(adjacent_shuttles)) + var/obj/docking_port/mobile/custom/shuttle = SSshuttle.get_containing_shuttle(neighbor) + if(istype(shuttle)) + adjacent_shuttles |= shuttle + +/proc/shuttle_build_check(turf/origin, list/turfs, list/areas) + var/z = origin.z + var/skip_flood_fill = !!length(turfs) + if(skip_flood_fill && !(turfs[origin])) + . |= ORIGIN_NOT_ON_SHUTTLE + if(length(SSshuttle.custom_shuttles) >= CONFIG_GET(number/max_shuttle_count)) + . |= TOO_MANY_SHUTTLES + var/max_turfs = CONFIG_GET(number/max_shuttle_size) + if(!skip_flood_fill) + turfs += get_connected_shuttle_frame_turfs(origin, max_turfs) + var/turf_count = length(turfs) + if(!skip_flood_fill && !(turfs[origin])) + . |= ORIGIN_NOT_ON_SHUTTLE + if(turf_count > max_turfs) + . |= ABOVE_MAX_SHUTTLE_SIZE + . |= shuttle_area_check(turfs.Copy(), areas, z) + +/proc/shuttle_expand_check(turf/origin, obj/docking_port/mobile/shuttle, list/turfs, list/areas) + var/z = origin.z + var/skip_flood_fill = !!length(turfs) + if(skip_flood_fill && !(turfs[origin])) + . |= ORIGIN_NOT_ON_SHUTTLE + var/max_turfs = CONFIG_GET(number/max_shuttle_size) - shuttle.turf_count + var/list/adjacent_shuttles = list() + if(!skip_flood_fill) + turfs += get_connected_shuttle_frame_turfs(origin, max_turfs, adjacent_shuttles) + var/turf_count = length(turfs) + if(!skip_flood_fill && !(turfs[origin])) + . |= ORIGIN_NOT_ON_SHUTTLE + if(turf_count > max_turfs) + . |= ABOVE_MAX_SHUTTLE_SIZE + if(!adjacent_shuttles.Find(shuttle)) + . |= FRAME_NOT_ADJACENT_TO_LINKED_SHUTTLE + . |= shuttle_area_check(turfs.Copy(), areas, z) + +/* + * Check to see if the following conditions are met: + * 1. Any custom areas that overlap with the the region defined by `turf` are contained entirely within that region + * 2. All other turfs in the region are within whitelisted areas + */ +/proc/shuttle_area_check(list/turfs, list/areas, z) + for(var/area/custom_area as anything in GLOB.custom_areas) + var/list/area_turfs = custom_area.get_turfs_by_zlevel(z) + var/turf_count = length(area_turfs) + if(!turf_count) + continue + var/list/turfs_not_in_frame = area_turfs - turfs + var/turfs_not_in_frame_count = length(turfs_not_in_frame) + if(turfs_not_in_frame_count == turf_count) + continue + areas[custom_area] = area_turfs - turfs_not_in_frame + if(turfs_not_in_frame_count) + . |= CUSTOM_AREA_NOT_COMPLETELY_CONTAINED + turfs -= area_turfs + while(length(turfs)) + var/turf/checked_turf = pick(turfs) + var/area/checked_area = checked_turf.loc + var/list/area_turfs = checked_area.get_turfs_by_zlevel(z) + if(!is_type_in_list(checked_area, GLOB.shuttle_construction_area_whitelist)) + . |= INTERSECTS_NON_WHITELISTED_AREA + turfs -= area_turfs + +/proc/convert_areas_to_shuttle_areas(list/turfs, list/in_areas, list/out_areas, list/underlying_areas, area_type = /area/shuttle/custom) + for(var/area/area as anything in in_areas) + var/area/new_area = new area_type() + new_area.setup(area.name) + out_areas += new_area + var/list/area_turfs = in_areas[area] + var/datum/component/custom_area/custom_area = area.GetComponent(/datum/component/custom_area) + if(custom_area) + underlying_areas += (custom_area.previous_areas & area_turfs) + set_turfs_to_area(area_turfs, new_area) + turfs -= area_turfs + new_area.reg_in_areas_in_z() + new_area.create_area_lighting_objects() + new_area.power_change() + for(var/obj/machinery/door/firedoor/firelock as anything in area.firedoors) + firelock.CalculateAffectingAreas() + if(!area.has_contained_turfs()) + qdel(area) + +/proc/create_shuttle(mob/user, turf/origin, list/turfs, list/areas, shuttle_dir, port_dir = NORTH, area_type = /area/shuttle/custom, docking_port_type = /obj/docking_port/mobile/custom, obj/docking_port/stationary/dock_at, name, id, replace, custom = TRUE, force) + if(!ispath(docking_port_type, /obj/docking_port/mobile)) + CRASH("docking_port_type must be /obj/docking_port/mobile or a subpath") + if(!ispath(area_type, /area/shuttle)) + CRASH("area_type must be /area/shuttle or a subpath") + var/list/default_area_turfs = turfs.Copy() + // Convert each custom area into a shuttle area, then remove the affected turfs from the list of turfs to add to the default area + var/list/shuttle_areas = list() + var/list/underlying_areas = list() + convert_areas_to_shuttle_areas(default_area_turfs, areas, shuttle_areas, underlying_areas, area_type) + for(var/turf/turf as anything in default_area_turfs) + underlying_areas[turf] = turf.loc + + // Merge the remaining frame turfs into a default shuttle area + var/list/affected_areas = list() + var/area/default_area = new area_type() + default_area.setup(name) + set_turfs_to_area(default_area_turfs, default_area, affected_areas) + default_area.reg_in_areas_in_z() + default_area.create_area_lighting_objects() + default_area.power_change() + for(var/area_name in affected_areas) + var/area/merged_area = affected_areas[area_name] + for(var/obj/machinery/door/firedoor/firelock as anything in merged_area.firedoors) + firelock.CalculateAffectingAreas() + if(!merged_area.has_contained_turfs()) + qdel(merged_area) + shuttle_areas.Insert(1, default_area) + + var/obj/docking_port/mobile/mobile_port = new docking_port_type(origin, shuttle_areas) + mobile_port.underlying_areas_by_turf += underlying_areas + mobile_port.name = name + mobile_port.shuttle_id = id + mobile_port.port_direction = REVERSE_DIR(shuttle_dir) + mobile_port.dir = port_dir + mobile_port.calculate_docking_port_information() + mobile_port.turf_count = length(turfs) + + for(var/turf/turf as anything in turfs) + turf.stack_below_baseturf(/turf/open/floor/plating, /turf/baseturf_skipover/shuttle) + SEND_SIGNAL(turf, COMSIG_TURF_ADDED_TO_SHUTTLE, mobile_port) + if(!turf.depth_to_find_baseturf(/turf/baseturf_skipover/shuttle)) + continue + var/turf/new_ceiling = get_step_multiz(turf, UP) // check if a ceiling is needed + if(new_ceiling) + // generate ceiling + if(!(istype(new_ceiling, /turf/open/floor/engine/hull/ceiling) || new_ceiling.depth_to_find_baseturf(/turf/open/floor/engine/hull/ceiling))) + if(istype(new_ceiling, /turf/open/openspace) || istype(new_ceiling, /turf/open/space/openspace)) + new_ceiling.place_on_top(/turf/open/floor/engine/hull/ceiling) + else + new_ceiling.stack_ontop_of_baseturf(/turf/open/openspace, /turf/open/floor/engine/hull/ceiling) + new_ceiling.stack_ontop_of_baseturf(/turf/open/space/openspace, /turf/open/floor/engine/hull/ceiling) + + if(!istype(dock_at)) + dock_at = new(origin) + var/area/origin_area = get_area(origin) + dock_at.name = origin_area.name + dock_at.dir = port_dir + + mobile_port.register(replace, custom) + + message_admins("[key_name(user)] has created a shuttle at [ADMIN_VERBOSEJMP(origin)].") + log_shuttle("[key_name(user)] has created a shuttle at [get_area(origin)].") + + return mobile_port + +/proc/expand_shuttle(mob/user, obj/docking_port/mobile/shuttle, list/turfs, list/areas) + var/list/default_area_turfs = turfs.Copy() + // Convert each custom area into a shuttle area, then remove the affected turfs from the list of turfs to add to the default area + var/list/shuttle_areas = list() + var/list/underlying_areas = list() + convert_areas_to_shuttle_areas(default_area_turfs, areas, shuttle_areas, underlying_areas, shuttle.area_type) + for(var/turf/turf as anything in default_area_turfs) + underlying_areas[turf] = turf.loc + + var/list/affected_areas = list() + var/area/default_area = shuttle.shuttle_areas[1] + set_turfs_to_area(default_area_turfs, default_area, affected_areas) + default_area.power_change() + + for(var/area_name in affected_areas) + var/area/merged_area = affected_areas[area_name] + for(var/obj/machinery/door/firedoor/firelock as anything in merged_area.firedoors) + firelock.CalculateAffectingAreas() + if(!merged_area.has_contained_turfs()) + qdel(merged_area) + + for(var/area/shuttle_area as anything in shuttle_areas) + shuttle.shuttle_areas[shuttle_area] = TRUE + + var/list/bounds = shuttle.return_coords() + var/x0 = bounds[1] + var/y0 = bounds[2] + var/x1 = bounds[3] + var/y1 = bounds[4] + var/bounds_need_recalculation + for(var/turf/turf as anything in turfs) + turf.stack_below_baseturf(/turf/open/floor/plating, /turf/baseturf_skipover/shuttle) + SEND_SIGNAL(turf, COMSIG_TURF_ADDED_TO_SHUTTLE, shuttle) + if(turf.depth_to_find_baseturf(/turf/baseturf_skipover/shuttle)) + var/turf/new_ceiling = get_step_multiz(turf, UP) // check if a ceiling is needed + if(new_ceiling) + // generate ceiling + if(!(istype(new_ceiling, /turf/open/floor/engine/hull/ceiling) || new_ceiling.depth_to_find_baseturf(/turf/open/floor/engine/hull/ceiling))) + if(istype(new_ceiling, /turf/open/openspace) || istype(new_ceiling, /turf/open/space/openspace)) + new_ceiling.place_on_top(/turf/open/floor/engine/hull/ceiling) + else + new_ceiling.stack_ontop_of_baseturf(/turf/open/openspace, /turf/open/floor/engine/hull/ceiling) + new_ceiling.stack_ontop_of_baseturf(/turf/open/space/openspace, /turf/open/floor/engine/hull/ceiling) + if(bounds_need_recalculation) + continue + if(!(ISINRANGE(turf.x, x0, x1) && ISINRANGE(turf.y, y0, y1))) + bounds_need_recalculation = TRUE + + shuttle.turf_count += length(turfs) + shuttle.underlying_areas_by_turf += underlying_areas + SEND_SIGNAL(shuttle, COMSIG_SHUTTLE_EXPANDED, turfs) + if(bounds_need_recalculation) + QDEL_NULL(shuttle.assigned_transit) + shuttle.calculate_docking_port_information() + shuttle.initiate_docking(shuttle.get_docked(), force = TRUE) + + message_admins("[key_name(user)] has expanded [shuttle] at [ADMIN_VERBOSEJMP(user)].") + log_shuttle("[key_name(user)] expanded [shuttle] at [get_area(user)].") diff --git a/code/modules/shuttle/mobile_port/variants/custom/custom.dm b/code/modules/shuttle/mobile_port/variants/custom/custom.dm new file mode 100644 index 00000000000..1a5a1df71a1 --- /dev/null +++ b/code/modules/shuttle/mobile_port/variants/custom/custom.dm @@ -0,0 +1,19 @@ +/obj/docking_port/mobile/custom + name = "custom shuttle" + shuttle_id = "custom" + var/datum/weakref/master_blueprint + var/area/default_area + var/datum/weakref/control_console + var/datum/weakref/navigation_console + +/obj/docking_port/mobile/custom/Initialize(mapload, list/areas) + . = ..() + default_area = areas[1] + +/obj/docking_port/mobile/custom/canMove() + return ..() && (current_engine_power > 0) + +/obj/docking_port/mobile/custom/get_engine_coeff() + var/thrust_ratio = (current_engine_power * CUSTOM_ENGINE_POWER_MULTIPLIER)/(turf_count + CUSTOM_ENGINE_POWER_TURF_COUNT_OFFSET) + var/calculated_multiplier = 2*(1-(NUM_E ** -thrust_ratio)) + return clamp(1/calculated_multiplier, CUSTOM_ENGINE_COEFF_MIN, CUSTOM_ENGINE_COEFF_MAX) diff --git a/code/modules/shuttle/mobile_port/variants/custom/custom_consoles.dm b/code/modules/shuttle/mobile_port/variants/custom/custom_consoles.dm new file mode 100644 index 00000000000..c3ec5a05414 --- /dev/null +++ b/code/modules/shuttle/mobile_port/variants/custom/custom_consoles.dm @@ -0,0 +1,174 @@ +GLOBAL_LIST_INIT(custom_shuttle_station_area_whitelist, list(/area/station/asteroid)) + +/obj/machinery/computer/shuttle/custom_shuttle + desc = "A shuttle control computer." + icon_screen = "shuttle" + icon_keyboard = "tech_key" + shuttleId = "" + light_color = LIGHT_COLOR_CYAN + req_access = list() + interaction_flags_machine = INTERACT_MACHINE_ALLOW_SILICON + possible_destinations = "whiteship_home;" + var/static/list/connections = list(COMSIG_TURF_ADDED_TO_SHUTTLE = PROC_REF(on_loc_added_to_shuttle)) + +/obj/machinery/computer/shuttle/custom_shuttle/on_construction(mob/user) + circuit.configure_machine(src) + if(!shuttleId) + AddElement(/datum/element/connect_loc, connections) + +/obj/machinery/computer/shuttle/custom_shuttle/proc/on_loc_added_to_shuttle(turf/source, obj/docking_port/mobile/custom/port) + SIGNAL_HANDLER + if(!istype(port)) + say("Cannot link to this kind of shuttle!") + else + if(connect_to_shuttle(TRUE, port)) + RemoveElement(/datum/element/connect_loc, connections) + +/obj/machinery/computer/shuttle/custom_shuttle/connect_to_shuttle(mapload, obj/docking_port/mobile/port, obj/docking_port/stationary/dock) + var/obj/docking_port/mobile/custom/custom_port = port + if(istype(custom_port)) + if(custom_port.control_console?.resolve()) + say("Control console already present!") + return FALSE + . = ..() + if(!.) + return + if(istype(custom_port)) + custom_port.control_console = WEAKREF(src) + name = "[port.name] console" + +/obj/machinery/computer/shuttle/custom_shuttle/proc/linkShuttle(new_id) + if(shuttleId=="") + shuttleId = new_id + possible_destinations = "whiteship_home;shuttle[new_id]_custom;" + return TRUE + return FALSE + + +//docking cam +/obj/machinery/computer/camera_advanced/shuttle_docker/custom + lock_override = NONE + jump_to_ports = list("whiteship_home" = 1) + designate_time = 100 + circuit = /obj/item/circuitboard/computer/shuttle/docker + var/static/list/connections = list(COMSIG_TURF_ADDED_TO_SHUTTLE = PROC_REF(on_loc_added_to_shuttle)) + +/obj/machinery/computer/camera_advanced/shuttle_docker/custom/on_construction(mob/user) + circuit.configure_machine(src) + if(!shuttleId) + AddElement(/datum/element/connect_loc, connections) + +/obj/machinery/computer/camera_advanced/shuttle_docker/custom/proc/on_loc_added_to_shuttle(turf/source, obj/docking_port/mobile/custom/port) + SIGNAL_HANDLER + if(!istype(port)) + say("Cannot link to this kind of shuttle!") + else + if(connect_to_shuttle(TRUE, port)) + RemoveElement(/datum/element/connect_loc, connections) + +/obj/machinery/computer/camera_advanced/shuttle_docker/custom/Initialize(mapload) + . = ..() + GLOB.jam_on_wardec += src + +/obj/machinery/computer/camera_advanced/shuttle_docker/custom/Destroy() + GLOB.jam_on_wardec -= src + return ..() + +/obj/machinery/computer/camera_advanced/shuttle_docker/custom/connect_to_shuttle(mapload, obj/docking_port/mobile/port, obj/docking_port/stationary/dock) + if(shuttleId) //We normally should only be connecting newly-created consoles to shuttles, but just in case... + var/obj/docking_port/mobile/old_shuttle = SSshuttle.getShuttle(shuttleId) + if(old_shuttle) + UnregisterSignal(old_shuttle, COMSIG_SHUTTLE_EXPANDED) + var/obj/docking_port/mobile/custom/custom_port = port + if(istype(custom_port)) + if(custom_port.navigation_console?.resolve()) + say("Navigation console already present!") + return FALSE + . = ..() + if(!.) + return + name = "[port.name] navigation computer" + if(istype(custom_port)) + custom_port.navigation_console = WEAKREF(src) + RegisterSignal(port, COMSIG_SHUTTLE_EXPANDED, PROC_REF(on_shuttle_expanded)) + recalculate_eye_view(port) + +/obj/machinery/computer/camera_advanced/shuttle_docker/custom/proc/on_shuttle_expanded(obj/docking_port/mobile/source, list/turfs) + SIGNAL_HANDLER + recalculate_eye_view(source) + if(my_port) + var/here_x = source.x + var/here_y = source.y + var/there_x = my_port.x + var/there_y = my_port.y + var/target_z = my_port.z + var/rotation = angle2dir_cardinal(dir2angle(my_port.dir) - dir2angle(source.dir)) + var/docked = my_port.get_docked() == source + for(var/turf/turf as anything in turfs) + var/turf/checked_turf + if(docked) + checked_turf = turf + else + var/offset_x = turf.x - here_x + var/offset_y = turf.y - here_y + var/target_x = there_x + var/target_y = there_y + switch(rotation) + if(NORTH) + target_x += offset_x + target_y += offset_y + if(SOUTH) + target_x -= offset_x + target_y -= offset_y + if(EAST) + target_x -= offset_y + target_y += offset_x + if(WEST) + target_x += offset_y + target_y -= offset_x + checked_turf = locate(target_x, target_y, target_z) + if(checkLandingTurf(checked_turf) != SHUTTLE_DOCKER_LANDING_CLEAR) + if(docked) + my_port.unregister() + my_port.delete_after = TRUE + my_port.shuttle_id = null + my_port.name = "Old [my_port.name]" + my_port = null + else + QDEL_NULL(my_port) + break + +/obj/machinery/computer/camera_advanced/shuttle_docker/custom/proc/recalculate_eye_view(obj/docking_port/mobile/shuttle) + var/bigger_shuttle_dimension = max(shuttle.width, shuttle.height) + var/list/viewsize = getviewsize(world.view) + var/smaller_view_dimension = min(viewsize[1], viewsize[2]) + var/new_view_range = max(bigger_shuttle_dimension - smaller_view_dimension, 0) + if(new_view_range != view_range) + view_range = new_view_range + +/obj/machinery/computer/camera_advanced/shuttle_docker/custom/placeLandingSpot() + if(!shuttleId) + return //Only way this would happen is if someone else delinks the console while in use somehow + var/obj/docking_port/mobile/M = SSshuttle.getShuttle(shuttleId) + if(M?.mode != SHUTTLE_IDLE) + to_chat(usr, "You cannot target locations while in transit.") + return + ..() + +/obj/machinery/computer/camera_advanced/shuttle_docker/custom/checkLandingTurf(turf/T, list/overlappers) + . = ..() + var/area/area = get_area(T) + if(!is_type_in_list(area, GLOB.custom_shuttle_station_area_whitelist) && is_type_in_list(area, GLOB.the_station_areas)) + return SHUTTLE_DOCKER_BLOCKED + +/obj/machinery/computer/camera_advanced/shuttle_docker/custom/attack_hand(mob/user) + if(!shuttleId) + to_chat(user, "You must link the console to a shuttle first.") + return + return ..() + +/obj/machinery/computer/camera_advanced/shuttle_docker/custom/proc/linkShuttle(new_id) + if(shuttleId=="") + shuttleId = new_id + return TRUE + return FALSE diff --git a/code/modules/shuttle/mobile_port/variants/supply.dm b/code/modules/shuttle/mobile_port/variants/supply.dm index c45d4285523..54f7548aff2 100644 --- a/code/modules/shuttle/mobile_port/variants/supply.dm +++ b/code/modules/shuttle/mobile_port/variants/supply.dm @@ -119,7 +119,7 @@ GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list( message_admins("Blacklisted item found on in-transit Cargo Shuttle. See cargo logs for more details.") SSshuttle.centcom_message = "Contraband found on Cargo Shuttle. This has been returned via drop pod." -/obj/docking_port/mobile/supply/initiate_docking() +/obj/docking_port/mobile/supply/initiate_docking(obj/docking_port/stationary/new_dock, force=FALSE) if(getDockedId() == "cargo_away") // Buy when we leave home. buy() create_mail() diff --git a/code/modules/shuttle/shuttle_consoles/navigation_computer.dm b/code/modules/shuttle/shuttle_consoles/navigation_computer.dm index 3e9bf0cbe3b..033c6309aae 100644 --- a/code/modules/shuttle/shuttle_consoles/navigation_computer.dm +++ b/code/modules/shuttle/shuttle_consoles/navigation_computer.dm @@ -24,7 +24,7 @@ var/x_offset = 0 ///y offset for where the camera eye will spawn. Starts from the shuttle's docking port var/y_offset = 0 - var/list/whitelist_turfs = list(/turf/open/space, /turf/open/floor/plating, /turf/open/lava, /turf/open/openspace) + var/list/whitelist_turfs = list(/turf/open/space, /turf/open/floor/plating, /turf/open/lava, /turf/open/openspace, /turf/open/misc) var/see_hidden = FALSE var/designate_time = 0 var/turf/designating_target_loc diff --git a/code/modules/shuttle/shuttle_consoles/shuttle_console.dm b/code/modules/shuttle/shuttle_consoles/shuttle_console.dm index e269e360ba4..cf5ccb2c1c4 100644 --- a/code/modules/shuttle/shuttle_consoles/shuttle_console.dm +++ b/code/modules/shuttle/shuttle_consoles/shuttle_console.dm @@ -239,6 +239,7 @@ possible_destinations = replacetext(replacetextEx(possible_destinations, "[shuttleId]_custom", ""), ";;", ";") shuttleId = port.shuttle_id possible_destinations += ";[port.shuttle_id]_custom" + return TRUE #undef SHUTTLE_CONSOLE_ACCESSDENIED #undef SHUTTLE_CONSOLE_ENDGAME diff --git a/config/game_options.txt b/config/game_options.txt index 93f060c3759..97799dd8656 100644 --- a/config/game_options.txt +++ b/config/game_options.txt @@ -560,3 +560,7 @@ MAX_POSITIVE_QUIRKS 6 # If the value is lower than 1, it'll tend to even out the odds # If higher than 1, it'll lean toward common spawns even more. RANDOM_LOOT_WEIGHT_MODIFIER 1 + +## Custom shuttle spam prevention. Changine these numbers allows you to change the maxsize and amount of custom shuttles. +MAX_SHUTTLE_COUNT 6 +MAX_SHUTTLE_SIZE 300 diff --git a/icons/mob/actions/actions_shuttle.dmi b/icons/mob/actions/actions_shuttle.dmi new file mode 100644 index 0000000000000000000000000000000000000000..ef35786a2b2b9ccb981a3ec79ef57e0786f8e00c GIT binary patch literal 3034 zcmV<03nlc4P)V=-0C=2@jynp&Fc1Z6=@g6Ihx}SKLB^=CuV5BwZ7p$ZSZ$K)r*In*cW+=A zUJbXV4L$fS*cVCRbpZ$0!_k^IC(YpCa<(QDPgyr8ib~+1N>q4SP-#FRtgJ~a1st5L z83iA-?Jv4MW&EqhnNaqX3(XXDK$dxp_nqZmCu(SV555cb1DJPH?bmvdGynh#07*na zRCt{2TW@F-=^6h`qe-J%vukojt{ep=%2NfR*pH88M_sfQ(!HY++a*=V=Cy#-twU z)HQBFG6MOZ38&@f!|5Ny;NNkAix>{<7sG-5PSCc3w##j&+Ag=9Qr_cSoG2^bX$1KF z2=H3@`7km^+8WplF&x;BFnZ7eh~WU3K=``ZJJD2>QMsc$&~~})6ye{7HpGs`&M1{n z{JvfMG7#Wp`4QlOFnb+p<+A{I(%g{|fEE-drcn4(m0v*q$W*=xwi|#6gs=ah2MAS0^=iury+vo;i(Oixc^+tUFs z^W*S^R(r_Uu^aMlW`11tSCt%*pFN&)Af`IN#&@P=5Z_79N5y00CW8VYNP)#DHA*UkNocw>eIv@kct)n9T#4 zysvVWKOqdVzsD`A{3MPjp~-?)(@;SW)GceIf5gXa=<%Zp|Hk6tA{G}HrFRCpSOBO4 zu7cdKRPu_sMbj1KsoE z0HnD>a%VT(_xj{^Kj9@h%6Wy?>jgT`^TbF>a4t^7gt1QsfR3Iq*+@qGH%6mO2x_Bx zy5ToTE3+!)*{>Kv!*svnqwS80GI16eweLvqlT;nXL*$RoAluwXt7G%2t za)GL3qyXE91~DKn<>vxtsv>elk@j#23axw~V+0(xv|Q{MG(}TR*M`!Nu3Rga9@GXX zW-X7Bf1zqaH7G`XDD!Q3dU3%3Vo^%738K+hQT#mt$8p7EpLRh-c0Jnq{Kn`xRo%Yb z*IGWsgd78o?EER#ty^dMLgLXQc(!Z-01OWgqrKfcx7XrjGKtH7|C`G2O8*y z%cq!-4WLxM%jH5vMTLC5va*6yN~+PQtgN&|*oek=-EQhTF-?!O`}c!^xi`t35Q*|j2`U396&Hz&94GgEK7W5j zMFmo+6huX@2Iy{x^n5K1K@g}$XpEj?>~aUR^8rfb6Te>y?gvoJg!Pnfgd<0e;NZc7 z7#fL_qYj4NC_IA@6PvvB*lzTow)+1nfc^QCYgzD;QtgNi${ShQk zDoexx)x$+9{)=HU>K6+ySll=^Sn%J=);WHWQ0N?hLjalI(BfYw3N{LWT(#ulw=HOc>4*2$4}|4 zUVDm$x1Zp7T%&B=7@(%r`sJ5sTe}utaGXpFAF1Cd_W6KfA#WEH{!J8t-*VJv6%`ez ztEwG}T#(>(_J=8{e_le?ZMyP$=1D@wmUtbSE zT5XlGAUZ8GOmZj01nA2P1v($#2xJ3{emaNvZF=q=zg>n=%wb(Ja~?pJmX)z!**$~! z?J`6nBgZ3}wAKfI|0w2E_so2?i}eZ8EUAz z`Tx#6_nfotf8D#*%wB8H{^qOwzVGwA&-;DUc%g`YpZY!k0QkyE@~;2@!U%o=Y)tSM z65iq(@TSk_wXVCog`2snjkCLrlOq6lr=@Gp#RT#Z`Gvw6QGL{o4eFPbBlfa^&$=!2YD@?JKaoq-KEnl#4r5`K|-Tbi+IP<(G`^s3<s+9-7>}&L|SetuC@k(E~^VV}wn zDLtzbn9|PqsT}}#s>#~O zNJQvUt){h%_%D#%Zi5^@+H<}R}ZUKW|-P|yPAQ(@HY@KoU&;l8M z!LS1v(V73_f)~z+9e8&UU<~}{@^cP$H99tvyF`<$A9CjD7g#qlf?UfSlsu(PmHa`M z9ui{c5&1Xd6nt{1g5IZB5o$56$A8)}RsVwQ{>~l>W2qW5p~?&&9i}z^?y7tn4;{9* z)9tF_>F+ej7T)%Yd*8G9(d3 z$s(_s0|56_#p|~d*CO2gohI0@;X;fq8xqDI515$R>bxuPcT9|Pehqibo#2?2cX*sh zGBzJaOL(tb=EU#sdj!4mc;Pe!{wWsLO(*y#8kt^0cE8%x1TW#ruPGX zbQl75T@)qn8uREf`f4k*c=KzN#6X|ok=LlDb_?V~a4s%d*)reEc9>q~P`DVX$G%+) zV_F`Z)G&!G4`+X)C+Al>cFTRfFR|noAf8dXL(*v)2@~i${t)cSgtqMSo>eg57B>?i zdOU4zPv(`Q?&o^B*M7O816P5r9)jZQYxLDSebNke6s$u- zgurR3_r&+I6C`SBKHn;caEJbD_k`~ZgClDLLX443O+9-LJ@(!4djH?c%7sQRmt1v} zAO?$%gYWFf=QziydEE_dmWO@^lLgq}Fi-b~p;x)jXC3HYkWyz$pn4W}E|t$0OR{-t zw?5t^X4Vz7Aa}AIoyJ(MA6s4LeU!WOcrG=d`?#^gBoSqD-gB>Z@>mx!;8P!2fVv)ulg-5Y)z87J(+K$8PQB>}72|I7S$nUy zeW21$M!$0=r+WYT##%XB9dorSnPw+TDve#0pmo7x#ed<9wo}U&;dK3@V}CnGw82)5BZW>#A1qvvVFDKTzJfylVK3qE-8fQXJx z4eWDK5M!nO?}ArJRSvq!*WvAn;;)0cK^oDRi0g9}`k9L8-(ieDEwIYVf!*id zk9PG~!*D(h6r9Wojd#kgT$WwMviSS^FF5v0G|BAGVdwx!JP!n{8G%D24(428BxEe~xTTk?`ST&^bdp@nq1@ z*T+PMnGC9hF7?9?H%TIc574f!vq$DdoqnDa)IM~|$~{;>y_MM~&sa&$t00!XIz$JQ zEM<#z;FdQxH1K-wfH6_y``3966lO=z*f>RhMm8CMmM7pj=w){?fg(5t2HqS6Q&SJ?&6v}v|n6<`h7Ejx70XyW6biC;mZ+c z_E9w5RE$JyS|dMgI_=}14BH|YBP0a?usDMxxX;=5i9U1MfhEBpU{QnH(?lq~Sq0TZ z7tjYe`C#DGq%~^zX~LGwRTx}c-3psBrIrg`sEfHvKs1iWZGA+rLL?s-6J+l1@z>YS zIz^gWM3R7~Ugm~xDWNx;Z~#_}FzL~}<;6WY@QXM{KsnMzwg&U$k_Np0n@=W zF~7%hpbbT4KHwCD#`j_X!&~m(g{k>FdkAlu`9+wUyE~Llr@oEb?w^vuoH5<4u-6N3 zVw}|9EE%n6jcs@|aE84?Eqsx)@#}H*{)Q-kmQGb!2YsM9#QfnfE0_pj%$7u)b{?ot zLI`hVT}ifMA<^SIdR1+$-2SID!n>1aOB)+g#TQBr@-{Y9_M3aP%ct3LM;=qLdIwZu z4-h6z5m^Unre^~k7x2gPi;I3^mP>6|X7huPUxf$*f?(u?^i7aBju8k$76}{Z>{t&N zzg`?-zShwRYus-rHo-&=Cy4Na!yKV@;Aih7eiGAh@L>MAs0v85w5Zr0gvpi_mDw$p zXC2APxhb58+>%b7#k#|fJ~w;q5bC4H#othW5=C*KRupUu z^DFD>L0%(!?t(*9hDExHcp` z1Y}jzQ$^u!x){ZHm?D3D_UwO=As(U*y^xQ?DzUmbi^KyKZ;lyA157XD)uFxfg}0Xn zExZOZ+LoJCz|RCHjl7#BxuMg#!I|PE`l(F3@XbWMD$9y$yD2o_23a6oeR#j!>yKpud5$_*8U&0 zu#L@;98*I;bn-)ZI5w@cKP?1Ov!tr1741Rwr5)U{T10~(g8yiKdVbgSYb_2`WKop-vV7D>mEG%>g2EzWF^^WM0*QkN<9mbQM#;~X61fe1^r<=L zD?{G@?PZ%GipIPoq{je$m^6IOx@oXeIz16VWaZ?@RrViZP!TQN+jJLiJezpNa=+Go zsgqSuK8HAwSl%R?mS|*;53hU;CcL)xd4%Y#OmHPD1`Nbn(3H)bWp)04O}VZfk6m6Q zqfpOgl+AA&Wo#|upyOn9_iem?ldWO_2Gfagw$}n#L<`=No+q9YgW@2av)v|nOsx^_ zC|N8tiJQq8o7RrpB2J5ojzub)aQn$BpnvODN4EfcxB zei1^2g@wC8`K;D>m|CIEi&_7)J!m-Y>grm;0n~6GQv#Hp-EMa|#BfUp=;rawJA+t& zi;D|oUT?@7E6D;Ajcg@??o_1E__Zw_(Tid>Ns=;e5 z?{-9#R8>0?xeWjS^f7jB?ylvoNU_l~J~WjS7ZJmBzl8gZcW+@>bEhWZ)DuPrSt z3zzv<`7|c$=v9LQvc1UCvNm01*jEM`1Pt z4NYWn*=ByD0DB%UPY1VDddN8KaXfU3vlKqIJPV@Cl145JnKvsW0Y z1S`h_^c%!8yUg0y7y|Gjy8>%!gu#@^de!7BHEKWMDUM%{KwT8fHu^*fobanSD?VV_ zxPeeb7z&zH-igTCnoEq>C%Y+lhi&z&BKq3502M%`ds_TkUTfgiSiwqbTadMbLx|-s zq8~Liv)2bRRaO^OEss$^19#9XRE&l5S9I4epL{N&xg!Mcx)eG|vb5j_UepuPo0ymY zGM1LCmFiY3OdT~pr4+(0jyKGw%8b~#xK@=~7?iDV{;&;Y-~;RnjTE34k#iZAS+8gs zAl?cv?R>R*Jwd^S(WEB*?$my@=^}p_73}K%o(c-yzFl1ffPD~Y0aB@R)_CRNA(IyR ziDx;>C=(G878W)|EkEFw&pK7tNmoy1Pbw0}VpAB$sKE=aDci@-&(5|Sth=??RJ64L zH-V@4-=I#4wvzT2I5&%3uHN}o&oX4kLGFOE{LuL|O*BX?JeLm2KR@-1mWkO?!%S)R zKF|B%4|ZS-ha->J61sS>9^T+%1A^`r#drG&9IhpuJeLqA?RbW{BwNL;EW|S!7GHS; zB3*Hw`u9)h5y+{kUztws7phY=r=Fw~6mEm&hX=&8TP(^D(DXr<##F+{JwWwdv#aqe zFE2aqPV+Yg@rwN8+vuj)n9~{lr=8zl7SZ|sYs+!I;3-{AD^yd{gI=MJ3RVHB1c7#H z*5Zd)

N_@khf%QHNyo<-H&IZ*AF+ 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 D=9lzk literal 11940 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 diff --git a/icons/obj/scrolls.dmi b/icons/obj/scrolls.dmi index ae488cab2f7661ab35f06415a6fd75287e349bdc..84805f239d9ca79ba9b71a638ed59022c26f916c 100644 GIT binary patch literal 4281 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 HQ{?{u-FGMq delta 1650 zcmV-&295c-A@&TA7k^*~0{{R3;-rdu0001iP)t-sz`(%Zx@q^}pYzpt*S~k-(v<%0 zyn1IPnE*4)05jZZ#PXBs=H}+yt3x$FRy#s#LPUzG5ox+MlG}eZ_xJbc!*|!!xnlw{ z|FLxc!;tFd$eCgQU|gAbT$vXaW*Hf7R<7eVfxG1M`pp0TAb&|{A7-G|zhxfFDZ*Bkpc$}4zu@1s8 z3`FPTE24G=L1AS{Q>k|5c9W#?`NIc++9U_Z&0!;8FHEw2;FeBVZ(&`t2Z_r?80Z1NizJVJ-r8yicB9dEoONhSdKE+~jG?m^^JS0&W|bhGU+#{-?M+ zZRG)kP{7H@5qO_{;fet3qFLkc8il*ZZxN94tbY#<`;mJiVhL!+06IMHG&Venq8>XT zPr#Aq+Qc^ut$6eZ3XaM_HWbZ8V_uE1PnU3n}DVdle2l90S4e0fDr@&+@Y2E zH1B?zPR08WXjwOlrs8e_K5mX^3L9*z^WwcIT-ogSVv8;Iht0VY&aVI6kNti0{rU-h zJ%6o7=zsIrzu9cJn+^NeZ!UO_ce`EuEb06GVQts{FJuhf$n)d}Si(K_Hy6Ay{!KY0S-6vyqN+5gIZWA>Z+W_?_BL()a0s+Fru5BefSvzMp4+jR0N{ z|3&Rx>|jmpAmQ0hYDe)4ij>CZi{d@q*YZ!f#e2gfs zPd^`ExgmF@D9B2d-2F@}J(yx#MC0k}>MFiU_UP*g~y zfLE7+t-O9YUl2uNo|6L_%d26)4Ew4{MA26}JZXAGH wM>?>YDzC`_xC`G$o)4I64eRCmf&ZWV3yv^oT>d_ux&QzG07*qoM6N<$g5ioRUjP6A diff --git a/tgstation.dme b/tgstation.dme index 27a7d558e7d..5b6e4d22c1f 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -330,6 +330,7 @@ #include "code\__DEFINES\dcs\signals\signals_janitor.dm" #include "code\__DEFINES\dcs\signals\signals_key.dm" #include "code\__DEFINES\dcs\signals\signals_ladder.dm" +#include "code\__DEFINES\dcs\signals\signals_lattice.dm" #include "code\__DEFINES\dcs\signals\signals_lazy_templates.dm" #include "code\__DEFINES\dcs\signals\signals_leash.dm" #include "code\__DEFINES\dcs\signals\signals_lift.dm" @@ -1098,6 +1099,7 @@ #include "code\datums\components\combo_attacks.dm" #include "code\datums\components\combustible_flooder.dm" #include "code\datums\components\connect_containers.dm" +#include "code\datums\components\connect_inventory.dm" #include "code\datums\components\connect_loc_behalf.dm" #include "code\datums\components\connect_mob_behalf.dm" #include "code\datums\components\connect_range.dm" @@ -1110,6 +1112,7 @@ #include "code\datums\components\cult_ritual_item.dm" #include "code\datums\components\curse_of_hunger.dm" #include "code\datums\components\curse_of_polymorph.dm" +#include "code\datums\components\custom_area.dm" #include "code\datums\components\customizable_reagent_holder.dm" #include "code\datums\components\damage_aura.dm" #include "code\datums\components\damage_chain.dm" @@ -1575,6 +1578,8 @@ #include "code\datums\elements\selfknockback.dm" #include "code\datums\elements\series.dm" #include "code\datums\elements\shiny_bait.dm" +#include "code\datums\elements\shuttle_construction_lattice.dm" +#include "code\datums\elements\shuttle_construction_turf.dm" #include "code\datums\elements\sideway_movement.dm" #include "code\datums\elements\simple_flying.dm" #include "code\datums\elements\skill_reward.dm" @@ -5981,6 +5986,9 @@ #include "code\modules\shuttle\mobile_port\variants\ferry.dm" #include "code\modules\shuttle\mobile_port\variants\infiltrator.dm" #include "code\modules\shuttle\mobile_port\variants\supply.dm" +#include "code\modules\shuttle\mobile_port\variants\custom\blueprints.dm" +#include "code\modules\shuttle\mobile_port\variants\custom\custom.dm" +#include "code\modules\shuttle\mobile_port\variants\custom\custom_consoles.dm" #include "code\modules\shuttle\mobile_port\variants\emergency\emergency.dm" #include "code\modules\shuttle\mobile_port\variants\emergency\emergency_console.dm" #include "code\modules\shuttle\mobile_port\variants\emergency\emergency_types.dm" diff --git a/tgui/packages/tgui/constants.ts b/tgui/packages/tgui/constants.ts index 0d58cb5cbe3..1b55c62c945 100644 --- a/tgui/packages/tgui/constants.ts +++ b/tgui/packages/tgui/constants.ts @@ -69,6 +69,21 @@ export const CSS_COLORS = [ 'yellow', ] as const; +export enum Direction { + NONE = 0, + NORTH = 1, + SOUTH = 2, + EAST = 4, + WEST = 8, + NORTHEAST = NORTH | EAST, + NORTHWEST = NORTH | WEST, + SOUTHEAST = SOUTH | EAST, + SOUTHWEST = SOUTH | WEST, + VERTICAL = NORTH | SOUTH, + HORIZONTAL = EAST | WEST, + ALL = NORTH | SOUTH | EAST | WEST, +} + export type CssColor = (typeof CSS_COLORS)[number]; /* IF YOU CHANGE THIS KEEP IT IN SYNC WITH CHAT CSS */ diff --git a/tgui/packages/tgui/interfaces/ShuttleBlueprints.tsx b/tgui/packages/tgui/interfaces/ShuttleBlueprints.tsx new file mode 100644 index 00000000000..c2bcca2e852 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ShuttleBlueprints.tsx @@ -0,0 +1,422 @@ +import { ReactNode, useState } from 'react'; +import { + Box, + Button, + Dropdown, + Input, + Section, + Stack, + Tooltip, +} from 'tgui-core/components'; +import { BooleanLike } from 'tgui-core/react'; + +import { useBackend } from '../backend'; +import { Direction } from '../constants'; +import { Window } from '../layouts'; + +type AreaData = { name: string; ref: string }; + +type VisualizationToggleProps = { visualizing: BooleanLike }; + +type ShuttleConstructionUnieuqData = { + linkedShuttle: 0; + tooManyShuttles: BooleanLike; + onCustomShuttle: BooleanLike; +}; + +type ShuttleConfigurationUniqueData = { + linkedShuttle: string; + onShuttle: BooleanLike; + inDefaultArea: BooleanLike; + currentArea: AreaData; + defaultApc: BooleanLike; + apcInMergeRegion: BooleanLike; + apcs: Record; + neighboringAreas: Record; + idle: BooleanLike; +}; + +type ShuttleBlueprintsData = { + shuttles?: Record; + visualizing: BooleanLike; + onShuttleFrame: BooleanLike; + masterExists: BooleanLike; + isMaster: BooleanLike; +} & (ShuttleConstructionUnieuqData | ShuttleConfigurationUniqueData); + +type DirectionPadProps = { + title: string; + tooltip?: ReactNode; + enabledDirections: Direction; + selectedDirection: Direction; + onSelect: (direction: Direction) => void; +}; + +const directionData: [Direction, string][] = [ + [Direction.NORTH, 'up'], + [Direction.SOUTH, 'down'], + [Direction.EAST, 'right'], + [Direction.WEST, 'left'], +]; + +const DirectionPad = (props: DirectionPadProps) => { + const { title, tooltip, enabledDirections, selectedDirection, onSelect } = + props; + const [north, south, east, west] = directionData.map( + ([direction, icon_suffix], i) => ( + +