diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index 93ceb912480..babb0b64d08 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -218,6 +218,9 @@ #define COMSIG_ATOM_START_PULL "movable_start_pull" ///called on /living when someone starts pulling it (atom/movable/puller, state, force) #define COMSIG_LIVING_START_PULL "living_start_pull" +///called on /living, when pull is attempted, but before it completes, from base of [/mob/living/start_pulling]: (atom/movable/thing, force) +#define COMSIG_LIVING_TRY_PULL "living_try_pull" + #define COMSIG_LIVING_CANCEL_PULL (1 << 0) ///////////////// @@ -301,10 +304,16 @@ #define COMPONENT_MOVABLE_IMPACT_NEVERMIND (1<<1) //return true if you destroyed whatever it was you're impacting and there won't be anything for hitby() to run on ///from base of mob/living/hitby(): (mob/living/target, hit_zone) #define COMSIG_MOVABLE_IMPACT_ZONE "item_impact_zone" +///from /atom/movable/proc/buckle_mob(): (mob/living/M, force, check_loc, buckle_mob_flags) +#define COMSIG_MOVABLE_PREBUCKLE "prebuckle" // this is the last chance to interrupt and block a buckle before it finishes + #define COMPONENT_BLOCK_BUCKLE (1<<0) ///from base of atom/movable/buckle_mob(): (mob, force) #define COMSIG_MOVABLE_BUCKLE "buckle" ///from base of atom/movable/unbuckle_mob(): (mob, force) #define COMSIG_MOVABLE_UNBUCKLE "unbuckle" +///from /obj/vehicle/proc/driver_move, caught by the riding component to check and execute the driver trying to drive the vehicle +#define COMSIG_RIDDEN_DRIVER_MOVE "driver_move" + #define COMPONENT_DRIVER_BLOCK_MOVE (1<<0) ///from base of atom/movable/throw_at(): (list/args) #define COMSIG_MOVABLE_PRE_THROW "movable_pre_throw" #define COMPONENT_CANCEL_THROW (1<<0) @@ -338,6 +347,10 @@ ///called when the movable is removed from a disposal holder object: /obj/structure/disposalpipe/proc/expel(): (obj/structure/disposalholder/H, turf/T, direction) #define COMSIG_MOVABLE_EXIT_DISPOSALS "movable_exit_disposals" +/// from base of atom/movable/Process_Spacemove(): (movement_dir, continuous_move) +#define COMSIG_MOVABLE_SPACEMOVE "spacemove" + #define COMSIG_MOVABLE_STOP_SPACEMOVE (1<<0) + // /datum/mind signals ///from base of /datum/mind/proc/transfer_to(mob/living/new_character) @@ -442,6 +455,9 @@ #define COMSIG_MOB_AUTOMUTE_CHECK "automute_check" #define WAIVE_AUTOMUTE_CHECK (1<<0) +///Called when movement intent is toggled. +#define COMSIG_MOVE_INTENT_TOGGLED "move_intent_toggled" + // /mob/living signals ///from base of mob/living/resist() (/mob/living) @@ -1060,6 +1076,10 @@ /// from /obj/structure/cursed_slot_machine/determine_victor() when someone finally wins. #define COMSIG_GLOB_CURSED_SLOT_MACHINE_WON "cursed_slot_machine_won" +/// from base of /obj/item/slimepotion/speed/afterattack(): (obj/target, /obj/src, mob/user) +#define COMSIG_SPEED_POTION_APPLIED "speed_potion" + #define SPEED_POTION_STOP (1<<0) + // Signal types for the cargo shuttle // Sent before the shuttle scans its contents. diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm index 62d3df6514e..a8f303577c4 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -124,6 +124,7 @@ GLOBAL_LIST_INIT(glass_sheet_types, typecacheof(list( // Vehicles #define isvehicle(A) istype(A, /obj/vehicle) +#define istgvehicle(A) istype(A, /obj/tgvehicle) // Misc #define isclient(A) istype(A, /client) diff --git a/code/__DEFINES/vehicle_defines.dm b/code/__DEFINES/vehicle_defines.dm new file mode 100644 index 00000000000..73a5fdabe02 --- /dev/null +++ b/code/__DEFINES/vehicle_defines.dm @@ -0,0 +1,38 @@ +//Vehicle control flags. control flags describe access to actions in a vehicle. + +///controls the vehicles movement +#define VEHICLE_CONTROL_DRIVE (1<<0) +///Can't leave vehicle voluntarily, has to resist. +#define VEHICLE_CONTROL_KIDNAPPED (1<<1) +///melee attacks/shoves a vehicle may have +#define VEHICLE_CONTROL_MELEE (1<<2) +///using equipment/weapons on the vehicle +#define VEHICLE_CONTROL_EQUIPMENT (1<<3) +///changing around settings and the like. +#define VEHICLE_CONTROL_SETTINGS (1<<4) + +///ez define for giving a single pilot mech all the flags it needs. +#define FULL_MECHA_CONTROL ALL + +//Ridden vehicle flags + +/// Does our vehicle require arms to operate? Also used for piggybacking on humans to reserve arms on the rider +#define RIDER_NEEDS_ARMS (1<<0) +// As above but only reserves 1 arm instead of 2 +#define RIDER_NEEDS_ARM (1<<1) +/// Do we need legs to ride this (checks against TRAIT_FLOORED) +#define RIDER_NEEDS_LEGS (1<<2) +/// If the rider is disabled or loses their needed limbs, do they fall off? +#define UNBUCKLE_DISABLED_RIDER (1<<3) + + +/// The vehicle being ridden requires pixel offsets for all directions +#define RIDING_OFFSET_ALL "ALL" + +///Broken down, here's what this does: +/// divides the world icon_size (32) by delay divided by ticklag to get the number of pixels something should be moving each tick. +/// The division result is given a min value of 1 to prevent obscenely slow glide sizes from being set +/// Then that's multiplied by the global glide size multiplier. 1.25 by default feels pretty close to spot on. This is just to try to get byond to behave. +/// The whole result is then clamped to within the range above. +/// Not very readable but it works +#define DELAY_TO_GLIDE_SIZE(delay) (clamp(((world.icon_size / max((delay) / world.tick_lag, 1))), 1, 32)) diff --git a/code/__HELPERS/trait_helpers.dm b/code/__HELPERS/trait_helpers.dm index dc47f48dad1..3400bda3cc9 100644 --- a/code/__HELPERS/trait_helpers.dm +++ b/code/__HELPERS/trait_helpers.dm @@ -286,10 +286,15 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai ///An organ that was inserted into a dead mob, that has not been revived yet #define TRAIT_ORGAN_INSERTED_WHILE_DEAD "organ_inserted_while_dead" +//****** OBJ TRAITS *****// + ///An /obj that should not increase the "depth" of the search for adjacency, ///e.g. a storage container or a modsuit. #define TRAIT_ADJACENCY_TRANSPARENT "adjacency_transparent" +/// A trait for items that will not break glass tables if the user is buckled to them. +#define TRAIT_NO_BREAK_GLASS_TABLES "no_break_glass_tables" + //****** ATOM/MOVABLE TRAITS *****// /// A trait for determining if a atom/movable is currently crossing into another z-level by using of /turf/space z-level "destination-xyz" transfers #define TRAIT_CURRENTLY_Z_MOVING "currently_z_moving" // please dont adminbus this diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm index 841cbcebe23..21496f0d32a 100644 --- a/code/_globalvars/traits.dm +++ b/code/_globalvars/traits.dm @@ -110,6 +110,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( /obj = list( "TRAIT_ADJACENCY_TRANSPARENT" = TRAIT_ADJACENCY_TRANSPARENT, + "TRAIT_NO_BREAK_GLASS_TABLES" = TRAIT_NO_BREAK_GLASS_TABLES ), /obj/item = list( diff --git a/code/datums/components/riding/riding.dm b/code/datums/components/riding/riding.dm new file mode 100644 index 00000000000..5f6fe5ff12e --- /dev/null +++ b/code/datums/components/riding/riding.dm @@ -0,0 +1,277 @@ +/** + * This is the riding component, which is applied to a movable atom by the [ridable element][/datum/element/ridable] when a mob is successfully buckled to said movable. + * + * This component lives for as long as at least one mob is buckled to the parent. Once all mobs are unbuckled, the component is deleted, until another mob is buckled in + * and we make a new riding component, so on and so forth until the sun explodes. + */ + +/datum/component/riding + dupe_mode = COMPONENT_DUPE_UNIQUE + + var/last_move_diagonal = FALSE + /// Tick delay between movements, lower = faster, higher = slower + var/vehicle_move_delay = 2 + + /** + * If the driver needs a certain item in hand (or inserted, for vehicles) to drive this. For vehicles, this must be duplicated on the actual vehicle object in their + * [/obj/vehicle/var/key_type] variable because the vehicle objects still have a few special checks/functions of their own I'm not porting over to the riding component + * quite yet. Make sure if you define it on the vehicle, you define it here too. + */ + var/keytype + + /// position_of_user = list(dir = list(px, py)), or RIDING_OFFSET_ALL for a generic one. + var/list/riding_offsets = list() + /// ["[DIRECTION]"] = layer. Don't set it for a direction for default, set a direction to null for no change. + var/list/directional_vehicle_layers = list() + /// same as above but instead of layer you have a list(px, py) + var/list/directional_vehicle_offsets = list() + /// allow typecache for only certain turfs, forbid to allow all but those. allow only certain turfs will take precedence. + var/list/allowed_turf_typecache + /// allow typecache for only certain turfs, forbid to allow all but those. allow only certain turfs will take precedence. + var/list/forbid_turf_typecache + /// We don't need roads where we're going if this is TRUE, allow normal movement in space tiles + var/override_allow_spacemove = FALSE + /// can anyone other than the rider unbuckle the rider? + var/can_force_unbuckle = TRUE + + /** + * Ride check flags defined for the specific riding component types, so we know if we need arms, legs, or whatever. + * Takes additional flags from the ridable element and the buckle proc (buckle_mob_flags) for riding cyborgs/humans in case we need to reserve arms + */ + var/ride_check_flags = NONE + /// For telling someone they can't drive + COOLDOWN_DECLARE(message_cooldown) + /// For telling someone they can't drive + COOLDOWN_DECLARE(vehicle_move_cooldown) + +/datum/component/riding/Initialize(mob/living/riding_mob, force = FALSE, buckle_mob_flags= NONE, potion_boost = FALSE) + if(!ismovable(parent)) + return COMPONENT_INCOMPATIBLE + + handle_specials(riding_mob) + ride_check_flags |= buckle_mob_flags + + if(potion_boost) + vehicle_move_delay = round(GLOB.configuration.movement.human_delay * 0.85, 0.01) + +/datum/component/riding/RegisterWithParent() + . = ..() + RegisterSignal(parent, COMSIG_ATOM_DIR_CHANGE, PROC_REF(vehicle_turned)) + RegisterSignal(parent, COMSIG_MOVABLE_UNBUCKLE, PROC_REF(vehicle_mob_unbuckle)) + RegisterSignal(parent, COMSIG_MOVABLE_BUCKLE, PROC_REF(vehicle_mob_buckle)) + RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(vehicle_moved)) + RegisterSignal(parent, COMSIG_MOVABLE_BUMP, PROC_REF(vehicle_bump)) + if(!can_force_unbuckle) + RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(force_unbuckle)) + +/** + * This proc handles all of the proc calls to things like set_vehicle_dir_layer() that a type of riding datum needs to call on creation + * + * The original riding component had these procs all called from the ridden object itself through the use of GetComponent() and LoadComponent() + * This was obviously problematic for componentization, but while lots of the variables being set were able to be moved to component variables, + * the proc calls couldn't be. Thus, anything that has to do an initial proc call should be handled here. + */ +/datum/component/riding/proc/handle_specials() + return + +/// This proc is called when a rider unbuckles, whether they chose to or not. If there's no more riders, this will be the riding component's death knell. +/datum/component/riding/proc/vehicle_mob_unbuckle(datum/source, mob/living/rider, force = FALSE) + SIGNAL_HANDLER //COMSIG_MOVABLE_UNBUCKLE + + handle_unbuckle(rider) + +/datum/component/riding/proc/handle_unbuckle(mob/living/rider) + var/atom/movable/movable_parent = parent + restore_position(rider) + unequip_buckle_inhands(rider) + UnregisterSignal(rider, COMSIG_LIVING_TRY_PULL) + if(!movable_parent.has_buckled_mobs()) + qdel(src) + +/// This proc is called when a rider buckles, allowing for offsets to be set properly +/datum/component/riding/proc/vehicle_mob_buckle(datum/source, mob/living/rider, force = FALSE) + SIGNAL_HANDLER //COMSIG_MOVABLE_BUCKLE + + var/atom/movable/movable_parent = parent + handle_vehicle_layer(movable_parent.dir) + handle_vehicle_offsets(movable_parent.dir) + + if(rider.pulling == source) + rider.stop_pulling() + RegisterSignal(rider, COMSIG_LIVING_TRY_PULL, PROC_REF(on_rider_try_pull)) + +/// This proc is called when the rider attempts to grab the thing they're riding, preventing them from doing so. +/datum/component/riding/proc/on_rider_try_pull(mob/living/rider_pulling, atom/movable/target, force) + SIGNAL_HANDLER // COMSIG_LIVING_TRY_PULL + if(target == parent) + var/mob/living/ridden = parent + to_chat(ridden, "You can't pull [target]!") + return COMSIG_LIVING_CANCEL_PULL + +/// Some ridable atoms may want to only show on top of the rider in certain directions, like wheelchairs +/datum/component/riding/proc/handle_vehicle_layer(dir) + var/atom/movable/AM = parent + var/static/list/defaults = list(TEXT_NORTH = OBJ_LAYER, TEXT_SOUTH = ABOVE_MOB_LAYER, TEXT_EAST = ABOVE_MOB_LAYER, TEXT_WEST = ABOVE_MOB_LAYER) + . = defaults["[dir]"] + if(directional_vehicle_layers["[dir]"]) + . = directional_vehicle_layers["[dir]"] + if(isnull(.)) //you can set it to null to not change it. + . = AM.layer + AM.layer = . + +/datum/component/riding/proc/set_vehicle_dir_layer(dir, layer) + directional_vehicle_layers["[dir]"] = layer + +/// This is called after the ridden atom is successfully moved and is used to handle icon stuff +/datum/component/riding/proc/vehicle_moved(datum/source, oldloc, dir, forced) + SIGNAL_HANDLER //COMSIG_MOVABLE_MOVED + + var/atom/movable/movable_parent = parent + if(isnull(dir)) + dir = movable_parent.dir + for(var/mob/buckled_mob in movable_parent.buckled_mobs) + ride_check(buckled_mob) + if(QDELETED(src)) + return // runtimed with piggy's without this, look into this more + handle_vehicle_offsets(dir) + handle_vehicle_layer(dir) + +/// Turning is like moving +/datum/component/riding/proc/vehicle_turned(datum/source, _old_dir, new_dir) + SIGNAL_HANDLER //COMSIG_ATOM_DIR_CHANGE + + vehicle_moved(source, null, new_dir) + +/** + * Check to see if we have all of the necessary bodyparts and not-falling-over statuses we need to stay onboard. + * If not and if consequences is TRUE, well, there'll be consequences. + */ +/datum/component/riding/proc/ride_check(mob/living/rider, consequences = TRUE) + return TRUE + +/datum/component/riding/proc/handle_vehicle_offsets(dir) + var/atom/movable/AM = parent + var/AM_dir = "[dir]" + var/passindex = 0 + if(!AM.has_buckled_mobs()) + return + + for(var/mob/living/buckled_mob in AM.buckled_mobs) + passindex++ + var/list/offsets = get_offsets(passindex) + buckled_mob.setDir(dir) + dir_loop: + for(var/offsetdir in offsets) + if(offsetdir == AM_dir) + var/list/diroffsets = offsets[offsetdir] + buckled_mob.pixel_x = diroffsets[1] + if(length(diroffsets) >= 2) + buckled_mob.pixel_y = diroffsets[2] + if(length(diroffsets) == 3) + buckled_mob.layer = diroffsets[3] + break dir_loop + var/list/static/default_vehicle_pixel_offsets = list(TEXT_NORTH = list(0, 0), TEXT_SOUTH = list(0, 0), TEXT_EAST = list(0, 0), TEXT_WEST = list(0, 0)) + var/px = default_vehicle_pixel_offsets[AM_dir] + var/py = default_vehicle_pixel_offsets[AM_dir] + if(directional_vehicle_offsets[AM_dir]) + if(isnull(directional_vehicle_offsets[AM_dir])) + px = AM.pixel_x + py = AM.pixel_y + else + px = directional_vehicle_offsets[AM_dir][1] + py = directional_vehicle_offsets[AM_dir][2] + AM.pixel_x = px + AM.pixel_y = py + +/datum/component/riding/proc/set_vehicle_dir_offsets(dir, x, y) + directional_vehicle_offsets["[dir]"] = list(x, y) + +//Override this to set your vehicle's various pixel offsets +/datum/component/riding/proc/get_offsets(pass_index) // list(dir = x, y, layer) + . = list(TEXT_NORTH = list(0, 0), TEXT_SOUTH = list(0, 0), TEXT_EAST = list(0, 0), TEXT_WEST = list(0, 0)) + if(riding_offsets["[pass_index]"]) + . = riding_offsets["[pass_index]"] + else if(riding_offsets["[RIDING_OFFSET_ALL]"]) + . = riding_offsets["[RIDING_OFFSET_ALL]"] + +/datum/component/riding/proc/set_riding_offsets(index, list/offsets) + if(!islist(offsets)) + return FALSE + riding_offsets["[index]"] = offsets + +/datum/component/riding/proc/set_vehicle_offsets(list/offsets) + if(!islist(offsets)) + return FALSE + directional_vehicle_offsets = offsets + +/** + * This proc is used to see if we have the appropriate key to drive this atom, if such a key is needed. Returns FALSE if we don't have what we need to drive. + * + * Still needs to be neatened up and spruced up with proper OOP, as a result of vehicles having their own key handling from other ridable atoms + */ +/datum/component/riding/proc/keycheck(mob/user) + if(!keytype) + return TRUE + + if(istgvehicle(parent)) + var/obj/vehicle/vehicle_parent = parent + return istype(vehicle_parent.inserted_key, keytype) + + return user.is_holding_item_of_type(keytype) + +//BUCKLE HOOKS +/datum/component/riding/proc/restore_position(mob/living/buckled_mob) + if(isnull(buckled_mob)) + return + buckled_mob.pixel_x = initial(buckled_mob.pixel_x) + buckled_mob.pixel_y = initial(buckled_mob.pixel_y) + buckled_mob.plane = initial(buckled_mob.plane) + +//MOVEMENT +/datum/component/riding/proc/turf_check(turf/next, turf/current) + if(allowed_turf_typecache && !allowed_turf_typecache[next.type]) + return allowed_turf_typecache[current.type] + else if(forbid_turf_typecache && forbid_turf_typecache[next.type]) + return !forbid_turf_typecache[current.type] + return TRUE + +/// Every time the driver tries to move, this is called to see if they can actually drive and move the vehicle (via relaymove) +/datum/component/riding/proc/driver_move(atom/movable/movable_parent, mob/living/user, direction) + SIGNAL_HANDLER //COMSIG_RIDDEN_DRIVER_MOVE + SHOULD_CALL_PARENT(TRUE) + movable_parent.set_glide_size(DELAY_TO_GLIDE_SIZE(vehicle_move_delay)) + +/// So we can check all occupants when we bump a door to see if anyone has access +/datum/component/riding/proc/vehicle_bump(atom/movable/movable_parent, obj/machinery/door/possible_bumped_door) + SIGNAL_HANDLER //COMSIG_MOVABLE_BUMP + if(!istype(possible_bumped_door)) + return + for(var/occupant in movable_parent.buckled_mobs) + INVOKE_ASYNC(possible_bumped_door, TYPE_PROC_REF(/obj/machinery/door/, bumpopen), occupant) + +/datum/component/riding/proc/Unbuckle(atom/movable/M) + addtimer(CALLBACK(parent, TYPE_PROC_REF(/atom/movable/, unbuckle_mob), M), 0, TIMER_UNIQUE) + +/datum/component/riding/proc/Process_Spacemove(direction, continuous_move) + var/atom/movable/AM = parent + return override_allow_spacemove || has_gravity(AM) + +/// currently replicated from ridable because we need this behavior here too, see if we can deal with that +/datum/component/riding/proc/unequip_buckle_inhands(mob/living/carbon/user) + var/atom/movable/AM = parent + for(var/obj/item/riding_offhand/O in user.contents) + if(O.parent != AM) + CRASH("RIDING OFFHAND ON WRONG MOB") + if(O.selfdeleting) + continue + else + qdel(O) + return TRUE + +/datum/component/riding/proc/force_unbuckle(atom/movable/source, mob/living/living_hitter) + SIGNAL_HANDLER //COMSIG_ATOM_ATTACK_HAND + + if(living_hitter in source.buckled_mobs) + return + return COMPONENT_CANCEL_ATTACK_CHAIN + diff --git a/code/datums/components/riding/riding_vehicle.dm b/code/datums/components/riding/riding_vehicle.dm new file mode 100644 index 00000000000..d682f264e79 --- /dev/null +++ b/code/datums/components/riding/riding_vehicle.dm @@ -0,0 +1,175 @@ +// For any /obj/tgvehicle's that can be ridden + +/datum/component/riding/vehicle/Initialize(mob/living/riding_mob, force = FALSE, ride_check_flags = (RIDER_NEEDS_LEGS | RIDER_NEEDS_ARMS), potion_boost = FALSE) + if(!istgvehicle(parent)) + return COMPONENT_INCOMPATIBLE + return ..() + +/datum/component/riding/vehicle/RegisterWithParent() + . = ..() + RegisterSignal(parent, COMSIG_RIDDEN_DRIVER_MOVE, PROC_REF(driver_move)) + +/datum/component/riding/vehicle/driver_move(atom/movable/movable_parent, mob/living/user, direction) + if(!COOLDOWN_FINISHED(src, vehicle_move_cooldown)) + return COMPONENT_DRIVER_BLOCK_MOVE + var/obj/tgvehicle/vehicle_parent = parent + + if(!keycheck(user)) + if(COOLDOWN_FINISHED(src, message_cooldown)) + to_chat(user, "[vehicle_parent] has no key inserted!") + COOLDOWN_START(src, message_cooldown, 5 SECONDS) + return COMPONENT_DRIVER_BLOCK_MOVE + + if(user.incapacitated()) + if(ride_check_flags & UNBUCKLE_DISABLED_RIDER) + INVOKE_ASYNC(vehicle_parent, TYPE_PROC_REF(/atom/movable, unbuckle_mob), user, TRUE) + user.visible_message("[user] falls off [vehicle_parent].",\ + "You slip off [vehicle_parent] as your body slumps!") + user.Stun(3 SECONDS) + + if(COOLDOWN_FINISHED(src, message_cooldown)) + to_chat(user, "You cannot operate [vehicle_parent] right now!") + COOLDOWN_START(src, message_cooldown, 5 SECONDS) + return COMPONENT_DRIVER_BLOCK_MOVE + + if(ride_check_flags & RIDER_NEEDS_LEGS && HAS_TRAIT(user, TRAIT_FLOORED)) + if(ride_check_flags & UNBUCKLE_DISABLED_RIDER) + INVOKE_ASYNC(vehicle_parent, TYPE_PROC_REF(/atom/movable, unbuckle_mob), user, TRUE) + user.visible_message("[user] falls off [vehicle_parent].",\ + "You fall off [vehicle_parent] while trying to operate it while unable to stand!") + user.Stun(3 SECONDS) + + if(COOLDOWN_FINISHED(src, message_cooldown)) + to_chat(user, "You can't seem to manage that while unable to stand up enough to move [vehicle_parent]...") + COOLDOWN_START(src, message_cooldown, 5 SECONDS) + return COMPONENT_DRIVER_BLOCK_MOVE + + if(ride_check_flags & RIDER_NEEDS_ARMS && HAS_TRAIT(user, TRAIT_HANDS_BLOCKED)) + if(ride_check_flags & UNBUCKLE_DISABLED_RIDER) + INVOKE_ASYNC(vehicle_parent, TYPE_PROC_REF(/atom/movable, unbuckle_mob), user, TRUE) + user.visible_message("[user] falls off [vehicle_parent].",\ + "You fall off [vehicle_parent] while trying to operate it without being able to hold on!") + user.Stun(3 SECONDS) + + if(COOLDOWN_FINISHED(src, message_cooldown)) + to_chat(user, "You can't seem to hold onto [vehicle_parent] to move it...") + COOLDOWN_START(src, message_cooldown, 5 SECONDS) + return COMPONENT_DRIVER_BLOCK_MOVE + + handle_ride(user, direction) + return ..() + +/// This handles the actual movement for vehicles once [/datum/component/riding/vehicle/proc/driver_move] has given us the green light +/datum/component/riding/vehicle/proc/handle_ride(mob/user, direction) + var/atom/movable/movable_parent = parent + + var/turf/next = get_step(movable_parent, direction) + var/turf/current = get_turf(movable_parent) + if(!istype(next) || !istype(current)) + return //not happening. + if(!turf_check(next, current)) + to_chat(user, "[movable_parent] cannot go onto [next]!") + return + if(!Process_Spacemove(direction) || !isturf(movable_parent.loc)) + return + + step(movable_parent, direction) + last_move_diagonal = ((direction & (direction - 1)) && (movable_parent.loc == next)) + COOLDOWN_START(src, vehicle_move_cooldown, (last_move_diagonal ? 2 : 1) * vehicle_move_delay) + + if(QDELETED(src)) + return + handle_vehicle_layer(movable_parent.dir) + handle_vehicle_offsets(movable_parent.dir) + return TRUE + +/datum/component/riding/vehicle/scooter + ride_check_flags = RIDER_NEEDS_LEGS | RIDER_NEEDS_ARMS | UNBUCKLE_DISABLED_RIDER + +/datum/component/riding/vehicle/scooter/handle_specials(mob/living/riding_mob) + . = ..() + if(isrobot(riding_mob)) + set_riding_offsets(RIDING_OFFSET_ALL, list(TEXT_NORTH = list(0), TEXT_SOUTH = list(0), TEXT_EAST = list(0), TEXT_WEST = list(2))) + else + set_riding_offsets(RIDING_OFFSET_ALL, list(TEXT_NORTH = list(2), TEXT_SOUTH = list(-2), TEXT_EAST = list(0), TEXT_WEST = list(2))) + +/datum/component/riding/vehicle/scooter/skateboard + vehicle_move_delay = 1.5 + ride_check_flags = RIDER_NEEDS_LEGS | UNBUCKLE_DISABLED_RIDER + ///If TRUE, the vehicle will be slower (but safer) to ride on walk intent. + var/can_slow_down = TRUE + +/datum/component/riding/vehicle/scooter/skateboard/handle_specials() + . = ..() + set_vehicle_dir_layer(SOUTH, ABOVE_MOB_LAYER) + set_vehicle_dir_layer(NORTH, OBJ_LAYER) + set_vehicle_dir_layer(EAST, OBJ_LAYER) + set_vehicle_dir_layer(WEST, OBJ_LAYER) + +/datum/component/riding/vehicle/scooter/skateboard/RegisterWithParent() + . = ..() + if(can_slow_down) + RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine)) + var/obj/tgvehicle/scooter/skateboard/board = parent + if(istype(board)) + board.can_slow_down = can_slow_down + +/datum/component/riding/vehicle/scooter/skateboard/proc/on_examine(datum/source, mob/user, list/examine_list) + SIGNAL_HANDLER //COMSIG_PARENT_EXAMINE + examine_list += "[potential_rider] can't get a grip on [target_movable] because [potential_rider.p_their()] hands are full!", "You can't get a grip on [target_movable] because your hands are full!") + return COMPONENT_BLOCK_BUCKLE + + if((ride_check_flags & RIDER_NEEDS_LEGS) && HAS_TRAIT(potential_rider, TRAIT_FLOORED)) + potential_rider.visible_message("[potential_rider] can't get [potential_rider.p_their()] footing on [target_movable]!", + "You can't get your footing on [target_movable]!") + return COMPONENT_BLOCK_BUCKLE + + var/mob/living/target_living = target_movable + + target_living.AddComponent(riding_component_type, potential_rider, force, ride_check_flags, potion_boost = potion_boosted) + +/// Try putting the appropriate number of [riding offhand items][/obj/item/riding_offhand] into the target's hands, return FALSE if we can't +/datum/element/ridable/proc/equip_buckle_inhands(mob/living/carbon/human/user, amount_required = 1, atom/movable/target_movable, riding_target_override = null) + var/atom/movable/AM = target_movable + var/amount_equipped = 0 + for(var/amount_needed = amount_required, amount_needed > 0, amount_needed--) + var/obj/item/riding_offhand/inhand = new /obj/item/riding_offhand(user) + if(!riding_target_override) + inhand.rider = user + else + inhand.rider = riding_target_override + inhand.parent = AM + var/list/held_items = list() + held_items += user.l_hand + held_items += user.r_hand + for(var/obj/item/I in held_items) // delete any hand items like slappers that could still totally be used to grab on + if(I.flags & DROPDEL & ABSTRACT) + qdel(I) + + // this would be put_in_hands() if it didn't have the chance to sleep, since this proc gets called from a signal handler that relies on what this returns + var/inserted_successfully = FALSE + if(user.put_in_inactive_hand(inhand)) + inserted_successfully = TRUE + else + if(user.put_in_active_hand(inhand)) + inserted_successfully = TRUE + + if(inserted_successfully) + amount_equipped++ + else + qdel(inhand) + return FALSE + + if(amount_equipped >= amount_required) + return TRUE + else + unequip_buckle_inhands(user, target_movable) + return FALSE + +/// Checks to see if we've been hit with a red xenobio potion to make us faster. This is only registered if we're a vehicle +/datum/element/ridable/proc/check_potion(atom/movable/ridable_atom, obj/item/slimepotion/speed/speed_potion, mob/living/user) + SIGNAL_HANDLER + + if(potion_boosted) + to_chat(user, "[ridable_atom] has already been coated with red, that's as fast as it'll go!") + return + if(ridable_atom.has_buckled_mobs()) // effect won't take place til the next time someone mounts it, so just prevent that situation + to_chat(user, "It's too dangerous to smear [speed_potion] on [ridable_atom] while it's being ridden!") + return + var/speed_limit = round(GLOB.configuration.movement.human_delay * 0.85, 0.01) + var/datum/component/riding/theoretical_riding_component = riding_component_type + var/theoretical_speed = initial(theoretical_riding_component.vehicle_move_delay) + if(theoretical_speed <= speed_limit) // i say speed but this is actually move delay, so you have to be ABOVE the speed limit to pass + to_chat(user, "[ridable_atom] can't be made any faster!") + return + Detach(ridable_atom) + ridable_atom.AddElement(/datum/element/ridable, component_type = riding_component_type, potion_boost = TRUE) + to_chat(user, "You slather the red gunk over [ridable_atom], making it faster.") + ridable_atom.remove_atom_colour(WASHABLE_COLOUR_PRIORITY) + ridable_atom.add_atom_colour("#6e6e86", FIXED_COLOUR_PRIORITY) + ADD_TRAIT(ridable_atom, TRAIT_OIL_SLICKED, "potion") + qdel(speed_potion) + return SPEED_POTION_STOP + +/// Remove all of the relevant [riding offhand items][/obj/item/riding_offhand] from the target +/datum/element/ridable/proc/unequip_buckle_inhands(mob/living/carbon/user, atom/movable/target_movable) + var/atom/movable/AM = target_movable + for(var/obj/item/riding_offhand/O in user.contents) + if(O.parent != AM) + CRASH("RIDING OFFHAND ON WRONG MOB") + if(O.selfdeleting) + continue + else + qdel(O) + return TRUE + +/datum/element/ridable/proc/on_stat_change(mob/source) + SIGNAL_HANDLER + + // If we're dead, don't let anyone buckle onto us + if(source.stat == DEAD) + source.can_buckle = FALSE + INVOKE_ASYNC(src, TYPE_PROC_REF(/atom/movable, unbuckle_all_mobs), source) + + // If we're alive, back to being buckle-able + else + source.can_buckle = TRUE + +/obj/item/riding_offhand + name = "offhand" + icon_state = "offhand" + w_class = WEIGHT_CLASS_HUGE + flags = ABSTRACT | DROPDEL | NOBLUDGEON + resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF + var/mob/living/carbon/rider + var/mob/living/parent + var/selfdeleting = FALSE + +/obj/item/riding_offhand/dropped() + selfdeleting = TRUE + return ..() + +/obj/item/riding_offhand/equipped() + if(loc != rider && loc != parent) + selfdeleting = TRUE + qdel(src) + return ..() + +/obj/item/riding_offhand/Destroy() + var/atom/movable/AM = parent + if(selfdeleting) + if(rider in AM.buckled_mobs) + AM.unbuckle_mob(rider) + return ..() + diff --git a/code/game/atoms.dm b/code/game/atoms.dm index c6a1fe82a6c..b4cfe0a781f 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -1396,3 +1396,13 @@ GLOBAL_LIST_EMPTY(blood_splatter_icons) var/mouseparams = list2params(paramslist) usr_client.Click(src, loc, null, mouseparams) return TRUE + +/** + * A special case of relaymove() in which the person relaying the move may be "driving" this atom + * + * This is a special case for vehicles and ridden animals where the relayed movement may be handled + * by the riding component attached to this atom. Returns TRUE as long as there's nothing blocking + * the movement, or FALSE if the signal gets a reply that specifically blocks the movement + */ +/atom/proc/relaydrive(mob/living/user, direction) + return !(SEND_SIGNAL(src, COMSIG_RIDDEN_DRIVER_MOVE, user, direction) & COMPONENT_DRIVER_BLOCK_MOVE) diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 24b14e4d831..262113c47f5 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -199,7 +199,7 @@ Moved(old_loc, direction, TRUE) /atom/movable/Move(atom/newloc, direct = 0, movetime) - if(!loc || !newloc) + if(!loc || !newloc) return FALSE var/atom/oldloc = loc @@ -359,6 +359,9 @@ if(pulledby && !pulledby.pulling) return TRUE + if(SEND_SIGNAL(src, COMSIG_MOVABLE_SPACEMOVE, movement_dir) & COMSIG_MOVABLE_STOP_SPACEMOVE) + return TRUE + if(throwing) return TRUE @@ -474,6 +477,13 @@ return TRUE +/// This proc is recursive, and calls itself to constantly set the glide size of an atom/movable +/atom/movable/proc/set_glide_size(target = 8) + glide_size = target + + for(var/mob/buckled_mob as anything in buckled_mobs) + buckled_mob.set_glide_size(target) + //Overlays /atom/movable/overlay var/atom/master = null diff --git a/code/game/jobs/job/central.dm b/code/game/jobs/job/central.dm index 0f49925b52f..307b84c8289 100644 --- a/code/game/jobs/job/central.dm +++ b/code/game/jobs/job/central.dm @@ -82,7 +82,8 @@ box = /obj/item/storage/box/centcomofficer backpack_contents = list( /obj/item/clothing/shoes/magboots/advance = 1, - /obj/item/storage/box/zipties = 1 + /obj/item/storage/box/zipties = 1, + /obj/item/melee/skateboard/hoverboard/admin = 1 //How do you do, fellow kids? ) bio_chips = list( /obj/item/bio_chip/mindshield, diff --git a/code/game/objects/buckling.dm b/code/game/objects/buckling.dm index cd285647f84..69839ada462 100644 --- a/code/game/objects/buckling.dm +++ b/code/game/objects/buckling.dm @@ -47,6 +47,14 @@ if((!can_buckle && !force) || M.buckled || (length(buckled_mobs) >= max_buckled_mobs) || (buckle_requires_restraints && !M.restrained()) || M == src) return FALSE + + // This signal will check if the mob is mounting this atom to ride it. There are 3 possibilities for how this goes + // 1. This movable doesn't have a ridable element and can't be ridden, so nothing gets returned, so continue on + // 2. There's a ridable element but we failed to mount it for whatever reason (maybe it has no seats left, for example), so we cancel the buckling + // 3. There's a ridable element and we were successfully able to mount, so keep it going and continue on with buckling + if(SEND_SIGNAL(src, COMSIG_MOVABLE_PREBUCKLE, M, force) & COMPONENT_BLOCK_BUCKLE) + return FALSE + M.buckling = src if(!M.can_buckle() && !force) diff --git a/code/game/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm index 1016fb4af1e..9cb2b4c2db4 100644 --- a/code/game/objects/structures/tables_racks.dm +++ b/code/game/objects/structures/tables_racks.dm @@ -459,7 +459,7 @@ check_break(M) /obj/structure/table/glass/proc/check_break(mob/living/M) - if(has_gravity(M) && M.mob_size > MOB_SIZE_SMALL) + if(has_gravity(M) && M.mob_size > MOB_SIZE_SMALL && !HAS_TRAIT(M?.buckled, TRAIT_NO_BREAK_GLASS_TABLES)) table_shatter(M) /obj/structure/table/glass/flip(direction) diff --git a/code/game/turfs/simulated/floor/chasm.dm b/code/game/turfs/simulated/floor/chasm.dm index e23b6cf2101..50ba6c25ea9 100644 --- a/code/game/turfs/simulated/floor/chasm.dm +++ b/code/game/turfs/simulated/floor/chasm.dm @@ -34,7 +34,8 @@ /obj/effect/projectile_lighting, /obj/effect/dummy/slaughter, //no bloodcrawlers into chasms. /obj/effect/dummy/spell_jaunt, //No jaunters into chasms either. - /mob/living/simple_animal/hostile/megafauna //failsafe + /mob/living/simple_animal/hostile/megafauna, //failsafe + /obj/tgvehicle/scooter/skateboard/hoverboard )) var/drop_x = 1 var/drop_y = 1 @@ -130,6 +131,8 @@ var/mob/living/M = AM if(M.flying || M.floating) return FALSE + if(istype(M.buckled, /obj/tgvehicle/scooter/skateboard/hoverboard)) + return FALSE if(ishuman(AM)) var/mob/living/carbon/human/H = AM if(istype(H.belt, /obj/item/wormhole_jaunter)) diff --git a/code/modules/crafting/recipes.dm b/code/modules/crafting/recipes.dm index dcc35b7be1b..30781d19040 100644 --- a/code/modules/crafting/recipes.dm +++ b/code/modules/crafting/recipes.dm @@ -493,6 +493,15 @@ category = CAT_WEAPONRY subcategory = CAT_WEAPON +/datum/crafting_recipe/skateboard + name = "Skateboard" + result = list(/obj/tgvehicle/scooter/skateboard/improvised) + time = 1.5 SECONDS + reqs = list(/obj/item/stack/sheet/metal = 5, + /obj/item/stack/rods = 10) + tools = list(TOOL_WELDER, TOOL_WRENCH) + category = CAT_MISC + /datum/crafting_recipe/spear_primal // alternative recipe for Ash Walkers name = "Spear" result = list(/obj/item/spear) diff --git a/code/modules/economy/merch_items.dm b/code/modules/economy/merch_items.dm index b15a0eb6b30..6218506a12c 100644 --- a/code/modules/economy/merch_items.dm +++ b/code/modules/economy/merch_items.dm @@ -117,6 +117,19 @@ cost = 500 category = MERCH_CAT_TOY +/datum/merch_item/skateboard + name = "Skateboard" + desc = "A skateboard. It can be placed on its wheels and ridden, or used as a radical weapon." + typepath = /obj/item/melee/skateboard + cost = 250 + category = MERCH_CAT_TOY + +/datum/merch_item/pro_skateboard + name = "Pro Skateboard" + desc = "An EightO brand professional skateboard. It looks sturdy and well made." + typepath = /obj/item/melee/skateboard/pro + cost = 600 //Quite fast, though I expect people to fall flat on their face with this a lot. + category = MERCH_CAT_TOY /datum/merch_item/flag_slime name = "Slime People Flag" diff --git a/code/modules/mining/abandonedcrates.dm b/code/modules/mining/abandonedcrates.dm index 1246798c39f..51a04b85da7 100644 --- a/code/modules/mining/abandonedcrates.dm +++ b/code/modules/mining/abandonedcrates.dm @@ -28,13 +28,7 @@ new /obj/item/reagent_containers/drinks/bottle/whiskey(src) new /obj/item/lighter(src) if(6 to 10) - new /obj/item/bedsheet(src) - new /obj/item/kitchen/knife(src) - new /obj/item/wirecutters(src) - new /obj/item/screwdriver(src) - new /obj/item/weldingtool(src) - new /obj/item/hatchet(src) - new /obj/item/crowbar(src) + new /obj/item/melee/skateboard/pro(src) if(11 to 15) new /obj/item/reagent_containers/glass/beaker/bluespace(src) if(16 to 20) @@ -121,7 +115,7 @@ if(91) new /obj/item/organ/internal/brain/xeno(src) if(92) - new /obj/item/organ/internal/heart(src) + new /obj/item/melee/skateboard/hoverboard(src) if(93) new /obj/item/soulstone/anybody(src) if(94) diff --git a/code/modules/mining/machine_vending.dm b/code/modules/mining/machine_vending.dm index 726f74d7f44..b0bac246277 100644 --- a/code/modules/mining/machine_vending.dm +++ b/code/modules/mining/machine_vending.dm @@ -91,12 +91,13 @@ EQUIPMENT("Alien Toy", /obj/item/clothing/mask/facehugger/toy, 300), EQUIPMENT("Cigar", /obj/item/clothing/mask/cigarette/cigar/havana, 150), EQUIPMENT("GAR Meson Scanners", /obj/item/clothing/glasses/meson/gar, 500), + EQUIPMENT("Hoverboard", /obj/item/clothing/glasses/meson/gar, 4000), //Cross lava rivers in a discounted style. To buying it in cargo. Still more than jump boots. + EQUIPMENT("HRD-MDE Project Box", /obj/item/storage/box/hardmode_box, 3500), //I want miners have to pay a lot to get this, but be set once they do. EQUIPMENT("Laser Pointer", /obj/item/laser_pointer, 300), EQUIPMENT("Luxury Shelter Capsule", /obj/item/survivalcapsule/luxury, 3000), EQUIPMENT("Soap", /obj/item/soap/nanotrasen, 200), EQUIPMENT("Space Cash", /obj/item/stack/spacecash/c200, 2000), - EQUIPMENT("Whiskey", /obj/item/reagent_containers/drinks/bottle/whiskey, 100), - EQUIPMENT("HRD-MDE Project Box", /obj/item/storage/box/hardmode_box, 3500) //I want miners have to pay a lot to get this, but be set once they do. + EQUIPMENT("Whiskey", /obj/item/reagent_containers/drinks/bottle/whiskey, 100) ) prize_list["Extra"] = list() // Used in child vendors diff --git a/code/modules/mob/inventory_procs.dm b/code/modules/mob/inventory_procs.dm index 90df969b5e0..f38dabacd7e 100644 --- a/code/modules/mob/inventory_procs.dm +++ b/code/modules/mob/inventory_procs.dm @@ -32,6 +32,13 @@ /mob/proc/is_holding(obj/item/I) return istype(I) && (I == r_hand || I == l_hand) +//Checks if we're holding an item of type: typepath +/mob/proc/is_holding_item_of_type(typepath) + . = FALSE + if(istype(l_hand, typepath)) + return l_hand + if(istype(r_hand, typepath)) + return r_hand //Returns the thing in our inactive hand /mob/proc/get_inactive_hand() diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 139a1df084a..e099b08eb35 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1000,6 +1000,8 @@ return FALSE if(incapacitated()) return + if(SEND_SIGNAL(src, COMSIG_LIVING_TRY_PULL, AM, force) & COMSIG_LIVING_CANCEL_PULL) + return FALSE // If we're pulling something then drop what we're currently pulling and pull this instead. AM.add_fingerprint(src) if(pulling) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index f4c64872668..3746e37a4e7 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1403,4 +1403,7 @@ GLOBAL_LIST_INIT(holy_areas, typecacheof(list( target.mind.kudos_received_from |= ckey - +/mob/living/simple_animal/relaymove(mob/living/user, direction) + if(user.incapacitated()) + return + return relaydrive(user, direction) diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm index 7e8d94b65bd..ed5f11cf99a 100644 --- a/code/modules/mob/mob_movement.dm +++ b/code/modules/mob/mob_movement.dm @@ -513,3 +513,4 @@ hud_used.move_intent.icon_state = icon_toggle for(var/atom/movable/screen/mov_intent/selector in hud_used.static_inventory) selector.update_icon() + SEND_SIGNAL(src, COMSIG_MOVE_INTENT_TOGGLED) diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm index 7ba2fec07b2..b599a44f535 100644 --- a/code/modules/research/xenobiology/xenobiology.dm +++ b/code/modules/research/xenobiology/xenobiology.dm @@ -461,6 +461,8 @@ if(!proximity_flag) return ..() + if(SEND_SIGNAL(O, COMSIG_SPEED_POTION_APPLIED, src, user) & SPEED_POTION_STOP) + return if(!isitem(O)) if(!istype(O, /obj/structure/table)) to_chat(user, "The potion can only be used on items!") diff --git a/code/modules/supply/supply_packs/pack_miscellaneous.dm b/code/modules/supply/supply_packs/pack_miscellaneous.dm index b0722805439..74de3b62f21 100644 --- a/code/modules/supply/supply_packs/pack_miscellaneous.dm +++ b/code/modules/supply/supply_packs/pack_miscellaneous.dm @@ -526,6 +526,12 @@ /obj/item/vending_refill/exploredrobe) containername = "cargo clothing vendor crate" +/datum/supply_packs/misc/hoverboard + name = "Hoverboard Crate" + contains = list(/obj/item/melee/skateboard/hoverboard) + cost = 1000 //Price of cool + containername = "hoverboard crate" + ///////////// Station Goals /datum/supply_packs/misc/station_goal diff --git a/code/modules/vehicle/tg_vehicles/scooter.dm b/code/modules/vehicle/tg_vehicles/scooter.dm new file mode 100644 index 00000000000..e4e55e304a0 --- /dev/null +++ b/code/modules/vehicle/tg_vehicles/scooter.dm @@ -0,0 +1,345 @@ +/obj/tgvehicle/scooter + name = "scooter" + desc = "A fun way to get around." + icon_state = "scooter" + are_legs_exposed = TRUE + +/obj/tgvehicle/scooter/Initialize(mapload) + . = ..() + make_ridable() + +/obj/tgvehicle/scooter/proc/make_ridable() + AddElement(/datum/element/ridable, /datum/component/riding/vehicle/scooter) + +/obj/tgvehicle/scooter/wrench_act(mob/living/user, obj/item/I) + ..() + to_chat(user, "You begin to remove the handlebars...") + if(!I.use_tool(src, user, 40, volume = 50)) + return TRUE + var/obj/tgvehicle/scooter/skateboard/improvised/skater = new(drop_location()) + new /obj/item/stack/rods(drop_location(), 2) + to_chat(user, "You remove the handlebars from [src].") + if(has_buckled_mobs()) + var/mob/living/carbon/carbons = buckled_mobs[1] + unbuckle_mob(carbons) + skater.buckle_mob(carbons) + qdel(src) + return TRUE + +/obj/tgvehicle/scooter/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE) + . = ..() + for(var/mob/living/buckled_mob as anything in buckled_mobs) + if(buckled_mob.get_num_legs() > 0) + buckled_mob.pixel_y = 5 + else + buckled_mob.pixel_y = -4 + +/obj/tgvehicle/scooter/skateboard + name = "skateboard" + desc = "An old, battered skateboard. It's still rideable, but probably unsafe." + icon_state = "skateboard" + density = FALSE + ///Sparks datum for when we grind on tables + var/datum/effect_system/spark_spread/sparks + ///Whether the board is currently grinding + var/grinding = FALSE + ///Stores the time of the last crash plus a short cooldown, affects availability and outcome of certain actions + COOLDOWN_DECLARE(next_crash) + ///The handheld item counterpart for the board + var/board_item_type = /obj/item/melee/skateboard + ///Stamina drain multiplier + var/instability = 10 + ///If true, riding the skateboard with walk intent on will prevent crashing. + var/can_slow_down = TRUE + +/obj/tgvehicle/scooter/skateboard/Initialize(mapload) + . = ..() + sparks = new + sparks.set_up(1, 0, src) + sparks.attach(src) + +/obj/tgvehicle/scooter/skateboard/make_ridable() + AddElement(/datum/element/ridable, /datum/component/riding/vehicle/scooter/skateboard) + +/obj/tgvehicle/scooter/skateboard/Destroy() + if(sparks) + QDEL_NULL(sparks) + return ..() + +/obj/tgvehicle/scooter/skateboard/relaymove(mob/living/user, direction) + if(grinding || !COOLDOWN_FINISHED(src, next_crash)) + return FALSE + return ..() + +/obj/tgvehicle/scooter/skateboard/generate_actions() + . = ..() + initialize_controller_action_type(/datum/action/vehicle/scooter/skateboard/ollie, VEHICLE_CONTROL_DRIVE) + initialize_controller_action_type(/datum/action/vehicle/scooter/skateboard/kickflip, VEHICLE_CONTROL_DRIVE) + +/obj/tgvehicle/scooter/skateboard/post_buckle_mob(mob/living/M)//allows skateboards to be non-dense but still allows 2 skateboarders to collide with each other + set_density(TRUE) + return ..() + +/obj/tgvehicle/scooter/skateboard/post_unbuckle_mob(mob/living/M) + if(!has_buckled_mobs()) + set_density(FALSE) + return ..() + +/obj/tgvehicle/scooter/skateboard/Bump(atom/bumped_thing) + . = ..() + if(!bumped_thing.density || !has_buckled_mobs() || !COOLDOWN_FINISHED(src, next_crash)) + return + var/mob/living/rider = buckled_mobs[1] + if(rider.m_intent == MOVE_INTENT_WALK && can_slow_down) //Going slow prevents you from crashing. + return + + COOLDOWN_START(src, next_crash, 1 SECONDS) + rider.adjustStaminaLoss(instability * 6) + playsound(src, 'sound/effects/bang.ogg', 40, TRUE) + if(!iscarbon(rider) || rider.getStaminaLoss() >= 100 || grinding || iscarbon(bumped_thing)) + var/atom/throw_target = get_edge_target_turf(rider, pick(NORTH, SOUTH, EAST, WEST)) + unbuckle_mob(rider) + if((istype(bumped_thing, /obj/machinery/disposal))) + rider.Weaken(8 SECONDS) + rider.forceMove(bumped_thing) + forceMove(bumped_thing) + visible_message("[src] crashes into [bumped_thing], and gets dumped straight into it!") + return + if((istype(bumped_thing, /obj/machinery/economy/vending))) + var/obj/machinery/economy/vending/V = bumped_thing + rider.Weaken(8 SECONDS) + visible_message("[src] crashes into [V]!") + V.tilt(rider, from_combat = TRUE) + return + rider.throw_at(throw_target, 3, 2) + var/head_slot = rider.get_item_by_slot(SLOT_HUD_HEAD) + if(!head_slot || !(istype(head_slot, /obj/item/clothing/head/helmet) || istype(head_slot, /obj/item/clothing/head/hardhat))) + rider.adjustBrainLoss(5) + rider.updatehealth() + visible_message("[src] crashes into [bumped_thing], sending [rider] flying!") + rider.Weaken(8 SECONDS) + if(iscarbon(bumped_thing)) + var/mob/living/carbon/victim = bumped_thing + var/grinding_mulitipler = 1 + if(grinding) + grinding_mulitipler = 2 + victim.Weaken(2 * grinding_mulitipler SECONDS) + victim.KnockDown(4 * grinding_mulitipler SECONDS) + else + var/backdir = REVERSE_DIR(dir) + step(src, backdir) + rider.spin(4, 1) + +///Moves the vehicle forward and if it lands on a table, repeats +/obj/tgvehicle/scooter/skateboard/proc/grind() + if(!has_buckled_mobs()) + grinding = FALSE + icon_state = "[initial(icon_state)]" + return + var/mob/living/skater = buckled_mobs[1] + var/old_pass = skater.pass_flags //Re-do this, so railings don't fuck with the grinder + var/old_v_pass = pass_flags + skater.pass_flags |= PASSTABLE | PASSFENCE + pass_flags |= PASSTABLE | PASSFENCE + step(src, dir) + skater.pass_flags = old_pass + pass_flags = old_v_pass + var/piping_hot = FALSE + for(var/obj/machinery/atmospherics/P in loc.contents) + if(P.invisibility == 0 && (loc.layer == PLATING_LAYER || P.layer >= GAS_PIPE_VISIBLE_LAYER)) + piping_hot = TRUE + break + if(!(locate(/obj/structure/table) in loc.contents) && !(locate(/obj/structure/railing) in loc.contents) && !piping_hot) + grinding = FALSE + icon_state = "[initial(icon_state)]" + return + skater.adjustStaminaLoss(instability * 0.3) + if(skater.getStaminaLoss() >= 100) + playsound(src, 'sound/effects/bang.ogg', 20, TRUE) + unbuckle_mob(skater) + var/atom/throw_target = get_edge_target_turf(src, pick(NORTH, SOUTH, EAST, WEST)) + skater.throw_at(throw_target, 2, 2) + visible_message("[skater] loses [skater.p_their()] footing and slams on the ground!") + skater.Weaken(4 SECONDS) + grinding = FALSE + icon_state = "[initial(icon_state)]" + return + playsound(src, 'sound/effects/skateboard_roll.ogg', 50, TRUE) + var/turf/location = get_turf(src) + + if(location) + if(prob(33)) + location.hotspot_expose(1000, 1000) + sparks.start() //the most radical way to start plasma fires + for(var/mob/living/carbon/victim in location) + if(victim.body_position == LYING_DOWN) + playsound(location, 'sound/items/trayhit2.ogg', 40) + victim.apply_damage(damage = 25, damagetype = BRUTE, def_zone = list(BODY_ZONE_HEAD, BODY_ZONE_CHEST, BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)) + victim.Weaken(1.5 SECONDS) + skater.adjustStaminaLoss(instability) + victim.visible_message("[victim] straight up gets grinded into the ground by [skater]'s [src]! Radical!") + addtimer(CALLBACK(src, PROC_REF(grind)), 1 DECISECONDS) + +/obj/tgvehicle/scooter/skateboard/MouseDrop(atom/over_object) + . = ..() + var/mob/living/carbon/skater = usr + if(!istype(skater)) + return + if(over_object == skater) + pick_up_board(skater) + +/obj/tgvehicle/scooter/skateboard/proc/pick_up_board(mob/living/carbon/skater) + if(skater.incapacitated() || !Adjacent(skater)) + return + if(has_buckled_mobs()) + to_chat(skater, "You can't lift this up when somebody's on it.") + return + skater.put_in_hands(new board_item_type(get_turf(skater))) + qdel(src) + +/obj/tgvehicle/scooter/skateboard/pro + name = "pro skateboard" + desc = "An EightO brand professional skateboard. Looks a lot more stable than the average board." + icon_state = "skateboardpro" + board_item_type = /obj/item/melee/skateboard/pro + instability = 6 + +/obj/tgvehicle/scooter/skateboard/pro/make_ridable() + AddElement(/datum/element/ridable, /datum/component/riding/vehicle/scooter/skateboard/pro) + +/obj/tgvehicle/scooter/skateboard/hoverboard + name = "hoverboard" + desc = "A blast from the past, so retro!" + board_item_type = /obj/item/melee/skateboard/hoverboard + instability = 3 + icon_state = "hoverboard_red" + resistance_flags = LAVA_PROOF | FIRE_PROOF + +/obj/tgvehicle/scooter/skateboard/hoverboard/Initialize(mapload) + . = ..() + ADD_TRAIT(src, TRAIT_NO_BREAK_GLASS_TABLES, ROUNDSTART_TRAIT) + +/obj/tgvehicle/scooter/skateboard/hoverboard/make_ridable() + AddElement(/datum/element/ridable, /datum/component/riding/vehicle/scooter/skateboard/hover) + +/obj/tgvehicle/scooter/skateboard/hoverboard/admin + name = "\improper Board Of Directors" + desc = "The engineering complexity of a spaceship concentrated inside of a board. Just as expensive, too." + board_item_type = /obj/item/melee/skateboard/hoverboard/admin + instability = 0 + icon_state = "hoverboard_nt" + +/obj/tgvehicle/scooter/skateboard/improvised + name = "improvised skateboard" + desc = "An unfinished scooter which can only barely be called a skateboard. It's still rideable, but probably unsafe. Looks like you'll need to add a few rods to make handlebars." + board_item_type = /obj/item/melee/skateboard/improvised + instability = 12 + +//CONSTRUCTION +/obj/item/scooter_frame + name = "scooter frame" + desc = "A metal frame for building a scooter. Looks like you'll need to add some iron to make wheels." + icon = 'icons/obj/tgvehicles.dmi' + icon_state = "scooter_frame" + w_class = WEIGHT_CLASS_NORMAL + +/obj/item/scooter_frame/attackby(obj/item/I, mob/user, params) + if(!istype(I, /obj/item/stack/sheet/metal)) + return ..() + if(!I.tool_start_check(user, amount = 5)) + return + to_chat(user, "You begin to add wheels to [src].") + if(!I.use_tool(src, user, 80, volume = 50, amount = 5)) + return + to_chat(user, "You finish making wheels for [src].") + new /obj/tgvehicle/scooter/skateboard/improvised(user.loc) + qdel(src) + +/obj/item/scooter_frame/wrench_act(mob/living/user, obj/item/I) + ..() + to_chat(user, "You deconstruct [src].") + new /obj/item/stack/rods(drop_location(), 10) + I.play_tool_sound(src) + qdel(src) + return TRUE + +/obj/tgvehicle/scooter/skateboard/wrench_act(mob/living/user, obj/item/I) + return + +/obj/tgvehicle/scooter/skateboard/improvised/attackby(obj/item/I, mob/user, params) + if(!istype(I, /obj/item/stack/rods)) + return ..() + if(!I.tool_start_check(user, amount = 2)) + return + to_chat(user, "You begin making handlebars for [src].") + if(!I.use_tool(src, user, 25, volume = 50, amount = 2)) + return + to_chat(user, "You add the rods to [src], creating handlebars.") + var/obj/tgvehicle/scooter/skaterskoot = new(loc) + if(has_buckled_mobs()) + var/mob/living/carbon/skaterboy = buckled_mobs[1] + unbuckle_mob(skaterboy) + skaterskoot.buckle_mob(skaterboy) + qdel(src) + +/obj/tgvehicle/scooter/skateboard/improvised/screwdriver_act(mob/living/user, obj/item/I) + . = ..() + if(.) + return + to_chat(user, "You begin to deconstruct and remove the wheels on [src]...") + if(!I.use_tool(src, user, 20, volume = 50)) + return + to_chat(user, "You deconstruct the wheels on [src].") + new /obj/item/stack/sheet/metal(drop_location(), 5) + new /obj/item/scooter_frame(drop_location()) + if(has_buckled_mobs()) + var/mob/living/carbon/skatergirl = buckled_mobs[1] + unbuckle_mob(skatergirl) + qdel(src) + return TRUE + +/obj/item/melee/skateboard + name = "skateboard" + desc = "A skateboard. It can be placed on its wheels and ridden, or used as a radical weapon." + icon = 'icons/obj/tgvehicles.dmi' + lefthand_file = 'icons/mob/inhands/items_lefthand.dmi' + righthand_file = 'icons/mob/inhands/items_righthand.dmi' + icon_state = "skateboard_held" + item_state = "skateboard" + force = 12 + throwforce = 4 + w_class = WEIGHT_CLASS_NORMAL + attack_verb = list("smacks", "whacks", "slams", "smashes") + ///The vehicle counterpart for the board + var/board_item_type = /obj/tgvehicle/scooter/skateboard + +/obj/item/melee/skateboard/attack_self(mob/user) + var/obj/tgvehicle/scooter/skateboard/S = new board_item_type(get_turf(user))//this probably has fucky interactions with telekinesis but for the record it wasn't my fault + S.buckle_mob(user) + qdel(src) + +/obj/item/melee/skateboard/improvised + name = "improvised skateboard" + desc = "A jury-rigged skateboard. It can be placed on its wheels and ridden, or used as a radical weapon." + board_item_type = /obj/tgvehicle/scooter/skateboard/improvised + +/obj/item/melee/skateboard/pro + name = "pro skateboard" + desc = "An EightO brand professional skateboard. It looks sturdy and well made." + icon_state = "skateboardpro_held" + item_state = "skateboardpro" + board_item_type = /obj/tgvehicle/scooter/skateboard/pro + +/obj/item/melee/skateboard/hoverboard + name = "hoverboard" + desc = "A blast from the past, so retro!" + icon_state = "hoverboard_red_held" + item_state = "hoverboard_red" + board_item_type = /obj/tgvehicle/scooter/skateboard/hoverboard + +/obj/item/melee/skateboard/hoverboard/admin + name = "Board Of Directors" + desc = "The engineering complexity of a spaceship concentrated inside of a board. Just as expensive, too." + icon_state = "hoverboard_nt_held" + item_state = "hoverboard_nt" + board_item_type = /obj/tgvehicle/scooter/skateboard/hoverboard/admin diff --git a/code/modules/vehicle/tg_vehicles/tg_vehicle_actions.dm b/code/modules/vehicle/tg_vehicles/tg_vehicle_actions.dm new file mode 100644 index 00000000000..abb0cbc1f69 --- /dev/null +++ b/code/modules/vehicle/tg_vehicles/tg_vehicle_actions.dm @@ -0,0 +1,285 @@ +//VEHICLE DEFAULT HANDLING + +/** + * ## generate_actions + * + * You override this with initialize_passenger_action_type and initialize_controller_action_type calls + * To give passengers actions when they enter the vehicle. + * Read the documentation on the aforementioned procs to learn the difference + */ +/obj/tgvehicle/proc/generate_actions() + return + +/** + * ## generate_action_type + * + * A small proc to properly set up each action path. + * args: + * * actiontype: typepath of the action the proc sets up. + * returns created and set up action instance + */ +/obj/tgvehicle/proc/generate_action_type(actiontype) + var/datum/action/vehicle/A = new actiontype + if(!istype(A)) + return + A.vehicle_target = src + return A + +/** + * ## initialize_passenger_action_type + * + * Gives any passenger that enters the mech this action. + * They will lose it when they disembark. + * args: + * * actiontype: typepath of the action you want to give occupants. + */ +/obj/tgvehicle/proc/initialize_passenger_action_type(actiontype) + autogrant_actions_passenger += actiontype + for(var/i in occupants) + grant_passenger_actions(i) //refresh + +/** + * ## destroy_passenger_action_type + * + * Removes this action type from all occupants and stops autogranting it + * args: + * * actiontype: typepath of the action you want to remove from occupants and the autogrant list. + */ +/obj/tgvehicle/proc/destroy_passenger_action_type(actiontype) + autogrant_actions_passenger -= actiontype + for(var/i in occupants) + remove_action_type_from_mob(actiontype, i) + +/** + * ## initialize_controller_action_type + * + * Gives any passenger that enters the vehicle this action... IF they have the correct vehicle control flag. + * This is used so passengers cannot press buttons only drivers should have, for example. + * args: + * * actiontype: typepath of the action you want to give occupants. + */ +/obj/tgvehicle/proc/initialize_controller_action_type(actiontype, control_flag) + LAZYINITLIST(autogrant_actions_controller["[control_flag]"]) + autogrant_actions_controller["[control_flag]"] += actiontype + for(var/i in occupants) + grant_controller_actions(i) //refresh + +/** + * ## destroy_controller_action_type + * + * As the name implies, removes the actiontype from autogrant and removes it from all occupants + * args: + * * actiontype: typepath of the action you want to remove from occupants and autogrant. + */ +/obj/tgvehicle/proc/destroy_controller_action_type(actiontype, control_flag) + autogrant_actions_controller["[control_flag]"] -= actiontype + UNSETEMPTY(autogrant_actions_controller["[control_flag]"]) + for(var/i in occupants) + remove_action_type_from_mob(actiontype, i) + +/** + * ## grant_action_type_to_mob + * + * As on the tin, it does all the annoying small stuff and sanity needed + * to GRANT an action to a mob. + * args: + * * actiontype: typepath of the action you want to give to grant_to. + * * grant_to: the mob we're giving actiontype to + * returns TRUE if successfully granted + */ +/obj/tgvehicle/proc/grant_action_type_to_mob(actiontype, mob/grant_to) + if(isnull(LAZYACCESS(occupants, grant_to)) || !actiontype) + return FALSE + LAZYINITLIST(occupant_actions[grant_to]) + if(occupant_actions[grant_to][actiontype]) + return TRUE + var/datum/action/action = generate_action_type(actiontype) + action.Grant(grant_to) + occupant_actions[grant_to][action.type] = action + return TRUE + +/** + * ## remove_action_type_from_mob + * + * As on the tin, it does all the annoying small stuff and sanity needed + * to REMOVE an action to a mob. + * args: + * * actiontype: typepath of the action you want to give to grant_to. + * * take_from: the mob we're taking actiontype to + * returns TRUE if successfully removed + */ +/obj/tgvehicle/proc/remove_action_type_from_mob(actiontype, mob/take_from) + if(isnull(LAZYACCESS(occupants, take_from)) || !actiontype) + return FALSE + LAZYINITLIST(occupant_actions[take_from]) + if(occupant_actions[take_from][actiontype]) + var/datum/action/action = occupant_actions[take_from][actiontype] + // Actions don't dissipate on removal, they just sit around assuming they'll be reusued + // Gotta qdel + qdel(action) + occupant_actions[take_from] -= actiontype + return TRUE + +/** + * ## grant_passenger_actions + * + * Called on every passenger that enters the vehicle, goes through the list of actions it needs to give... + * and does that. + * args: + * * grant_to: mob that needs to get every action the vehicle grants + */ +/obj/tgvehicle/proc/grant_passenger_actions(mob/grant_to) + for(var/v in autogrant_actions_passenger) + grant_action_type_to_mob(v, grant_to) + +/** + * ## remove_passenger_actions + * + * Called on every passenger that exits the vehicle, goes through the list of actions it needs to remove... + * and does that. + * args: + * * take_from: mob that needs to get every action the vehicle grants + */ +/obj/tgvehicle/proc/remove_passenger_actions(mob/take_from) + for(var/v in autogrant_actions_passenger) + remove_action_type_from_mob(v, take_from) + +/obj/tgvehicle/proc/grant_controller_actions(mob/M) + if(!istype(M) || isnull(LAZYACCESS(occupants, M))) + return FALSE + for(var/i in GLOB.bitflags) + if(occupants[M] & i) + grant_controller_actions_by_flag(M, i) + return TRUE + +/obj/tgvehicle/proc/remove_controller_actions(mob/M) + if(!istype(M) || isnull(LAZYACCESS(occupants, M))) + return FALSE + for(var/i in GLOB.bitflags) + remove_controller_actions_by_flag(M, i) + return TRUE + +/obj/tgvehicle/proc/grant_controller_actions_by_flag(mob/M, flag) + if(!istype(M)) + return FALSE + for(var/v in autogrant_actions_controller["[flag]"]) + grant_action_type_to_mob(v, M) + return TRUE + +/obj/tgvehicle/proc/remove_controller_actions_by_flag(mob/M, flag) + if(!istype(M)) + return FALSE + for(var/v in autogrant_actions_controller["[flag]"]) + remove_action_type_from_mob(v, M) + return TRUE + +/obj/tgvehicle/proc/cleanup_actions_for_mob(mob/M) + if(!istype(M)) + return FALSE + for(var/path in occupant_actions[M]) + stack_trace("Leftover action type [path] in vehicle type [type] for mob type [M.type] - THIS SHOULD NOT BE HAPPENING!") + var/datum/action/action = occupant_actions[M][path] + action.Remove(M) + occupant_actions[M] -= path + occupant_actions -= M + return TRUE + +/***************** ACTION DATUMS *****************/ + +/datum/action/vehicle + check_flags = AB_CHECK_HANDS_BLOCKED | AB_CHECK_IMMOBILE | AB_CHECK_CONSCIOUS + button_icon = 'icons/mob/actions/actions_vehicle.dmi' + icon_icon = 'icons/mob/actions/actions_vehicle.dmi' + button_icon_state = "vehicle_eject" + var/obj/tgvehicle/vehicle_target + var/obj/tgvehicle/vehicle_ridden_target + +/datum/action/vehicle/Destroy() + vehicle_target = null + return ..() + +/datum/action/vehicle/scooter/skateboard/ollie + name = "Ollie" + desc = "Get some air! Land on a table or fence to do a gnarly grind." + button_icon_state = "skateboard_ollie" + check_flags = AB_CHECK_CONSCIOUS + +/datum/action/vehicle/scooter/skateboard/ollie/Trigger(left_click) + . = ..() + if(!.) + return + var/obj/tgvehicle/scooter/skateboard/vehicle = vehicle_target + if(vehicle.grinding) + return + var/mob/living/rider = owner + var/turf/landing_turf = get_step(vehicle.loc, vehicle.dir) + rider.adjustStaminaLoss(vehicle.instability * 0.75) + if(rider.getStaminaLoss() >= 100) + playsound(src, 'sound/effects/bang.ogg', 20, TRUE) + vehicle.unbuckle_mob(rider) + rider.throw_at(landing_turf, 2, 2) + rider.Weaken(5 SECONDS) + vehicle.visible_message("[rider] misses the landing and falls on [rider.p_their()] face!") + return + if((locate(/obj/structure/table) in landing_turf) || (locate(/obj/structure/railing) in landing_turf)) + vehicle.grinding = TRUE + vehicle.icon_state = "[initial(vehicle.icon_state)]-grind" + addtimer(CALLBACK(vehicle, TYPE_PROC_REF(/obj/tgvehicle/scooter/skateboard, grind)), 2) + for(var/obj/machinery/atmospherics/P in landing_turf.contents) + if(P.invisibility == 0 && (landing_turf.layer == PLATING_LAYER || P.layer >= GAS_PIPE_VISIBLE_LAYER)) + vehicle.grinding = TRUE + vehicle.icon_state = "[initial(vehicle.icon_state)]-grind" + addtimer(CALLBACK(vehicle, TYPE_PROC_REF(/obj/tgvehicle/scooter/skateboard, grind)), 2) + break + rider.spin(spintime = 4, speed = 1) + animate(rider, pixel_y = -6, time = 4) + animate(vehicle, pixel_y = -6, time = 3) + playsound(vehicle, 'sound/effects/skateboard_ollie.ogg', 50, TRUE) + var/old_pass = rider.pass_flags + var/old_v_pass = vehicle.pass_flags + rider.pass_flags |= PASSTABLE | PASSFENCE + vehicle.pass_flags |= PASSTABLE | PASSFENCE + + rider.Move(landing_turf, vehicle_target.dir) + rider.pass_flags = old_pass + vehicle.pass_flags = old_v_pass + +/datum/action/vehicle/scooter/skateboard/kickflip + name = "Kickflip" + desc = "Kick your board up and catch it." + button_icon_state = "skateboard_ollie" + check_flags = AB_CHECK_CONSCIOUS + +/datum/action/vehicle/scooter/skateboard/kickflip/Trigger(left_click) + var/obj/tgvehicle/scooter/skateboard/board = vehicle_target + var/mob/living/rider = owner + + rider.adjustStaminaLoss(board.instability) + if(rider.getStaminaLoss() >= 100) + playsound(src, 'sound/effects/bang.ogg', 20, vary = TRUE) + board.unbuckle_mob(rider) + rider.Weaken(5 SECONDS) + if(prob(15)) + rider.visible_message( + "[rider] misses the landing and falls on [rider.p_their()] face!", + "You smack against the board, hard.", + ) + rider.emote("scream") + rider.adjustBruteLoss(10) // thats gonna leave a mark + return + rider.visible_message( + "[rider] misses the landing and falls on [rider.p_their()] face!", + "You fall flat onto the board!", + ) + return + + rider.visible_message( + "[rider] does a sick kickflip and catches [rider.p_their()] board in midair.", + "You do a sick kickflip, catching the board in midair! Stylish.", + ) + playsound(board, 'sound/effects/skateboard_ollie.ogg', 50, vary = TRUE) + rider.spin(spintime = 4, speed = 1) + animate(rider, pixel_y = -6, time = 0.4 SECONDS) + animate(board, pixel_y = -6, time = 0.3 SECONDS) + board.unbuckle_mob(rider) + addtimer(CALLBACK(board, TYPE_PROC_REF(/obj/tgvehicle/scooter/skateboard, pick_up_board), rider), 0.5 SECONDS) // so the board can still handle "picking it up" diff --git a/code/modules/vehicle/tg_vehicles/tg_vehicles.dm b/code/modules/vehicle/tg_vehicles/tg_vehicles.dm new file mode 100644 index 00000000000..75fbb8425bf --- /dev/null +++ b/code/modules/vehicle/tg_vehicles/tg_vehicles.dm @@ -0,0 +1,241 @@ +/obj/tgvehicle + name = "generic vehicle" + desc = "Yell at coding chat." + icon = 'icons/obj/tgvehicles.dmi' + icon_state = "error" + max_integrity = 300 + armor = list(MELEE = 30, BULLET = 30, LASER = 30, ENERGY = 0, BOMB = 30, RAD = 0, FIRE = 60, ACID = 60) + density = TRUE + anchored = FALSE + blocks_emissive = EMISSIVE_BLOCK_GENERIC + buckle_lying = FALSE + can_buckle = TRUE + buckle_lying = 0 + pass_flags = PASSTABLE + COOLDOWN_DECLARE(message_cooldown) + COOLDOWN_DECLARE(cooldown_vehicle_move) + var/list/mob/occupants //mob = bitflags of their control level. + ///Maximum amount of passengers plus drivers + var/max_occupants = 1 + ////Maximum amount of drivers + var/max_drivers = 1 + /** + * If the driver needs a certain item in hand (or inserted, for vehicles) to drive this. For vehicles, this must be duplicated on their riding component subtype + * [/datum/component/riding/var/keytype] variable because only a few specific checks are handled here with this var, and the majority of it is on the riding component + * Eventually the remaining checks should be moved to the component and this var removed. + */ + var/key_type + ///The inserted key, needed on some vehicles to start the engine + var/obj/item/key/inserted_key + /// Whether the vehicle is currently able to move + var/can_move = TRUE + var/list/autogrant_actions_passenger //plain list of typepaths + var/list/autogrant_actions_controller //assoc list "[bitflag]" = list(typepaths) + var/list/mob/occupant_actions //assoc list mob = list(type = action datum assigned to mob) + ///This vehicle will follow us when we move (like atrailer duh) + var/obj/tgvehicle/trailer + var/are_legs_exposed = FALSE + +/obj/tgvehicle/Initialize(mapload) + . = ..() + occupants = list() + autogrant_actions_passenger = list() + autogrant_actions_controller = list() + occupant_actions = list() + generate_actions() + +/obj/tgvehicle/Destroy(force) + QDEL_NULL(trailer) + inserted_key = null + return ..() + +/obj/tgvehicle/Exited(atom/movable/gone, direction) + if(gone == inserted_key) + inserted_key = null + return ..() + +/obj/tgvehicle/examine(mob/user) + . = ..() + . += generate_integrity_message() + +/// Returns a readable string of the vehicle's health for examining. Overridden by subtypes who want to be more verbose with their health messages. +/obj/tgvehicle/proc/generate_integrity_message() + var/examine_text = "" + var/integrity = obj_integrity / max_integrity * 100 + switch(integrity) + if(50 to 99) + examine_text = "It looks slightly damaged." + if(25 to 50) + examine_text = "It appears heavily damaged." + if(0 to 25) + examine_text = "It's falling apart!" + + return examine_text + +/obj/tgvehicle/proc/is_key(obj/item/I) + return istype(I, key_type) + +/obj/tgvehicle/proc/return_occupants() + return length(occupants) ? occupants : list() + +/obj/tgvehicle/proc/occupant_amount() + return LAZYLEN(occupants) + +/obj/tgvehicle/proc/return_amount_of_controllers_with_flag(flag) + . = 0 + for(var/i in occupants) + if(occupants[i] & flag) + .++ + +/obj/tgvehicle/proc/return_controllers_with_flag(flag) + RETURN_TYPE(/list/mob) + . = list() + for(var/i in occupants) + if(occupants[i] & flag) + . += i + +/obj/tgvehicle/proc/return_drivers() + return return_controllers_with_flag(VEHICLE_CONTROL_DRIVE) + +/obj/tgvehicle/proc/driver_amount() + return return_amount_of_controllers_with_flag(VEHICLE_CONTROL_DRIVE) + +/obj/tgvehicle/proc/is_driver(mob/M) + return is_occupant(M) && occupants[M] & VEHICLE_CONTROL_DRIVE + +/obj/tgvehicle/proc/is_occupant(mob/M) + return !isnull(LAZYACCESS(occupants, M)) + +/obj/tgvehicle/proc/add_occupant(mob/M, control_flags) + if(!istype(M) || is_occupant(M)) + return FALSE + + LAZYSET(occupants, M, NONE) + add_control_flags(M, control_flags) + after_add_occupant(M) + grant_passenger_actions(M) + return TRUE + +/obj/tgvehicle/proc/after_add_occupant(mob/M) + auto_assign_occupant_flags(M) + +/obj/tgvehicle/proc/auto_assign_occupant_flags(mob/M) //override for each type that needs it. Default is assign driver if drivers is not at max. + if(driver_amount() < max_drivers) + add_control_flags(M, VEHICLE_CONTROL_DRIVE) + +/obj/tgvehicle/proc/remove_occupant(mob/M) + SHOULD_CALL_PARENT(TRUE) + if(!istype(M)) + return FALSE + remove_control_flags(M, ALL) + remove_passenger_actions(M) + LAZYREMOVE(occupants, M) + cleanup_actions_for_mob(M) + after_remove_occupant(M) + return TRUE + +/obj/tgvehicle/proc/after_remove_occupant(mob/M) + return + +/obj/tgvehicle/relaymove(mob/living/user, direction) + if(!can_move) + return FALSE + if(is_driver(user)) + return relaydrive(user, direction) + return FALSE + +/obj/tgvehicle/proc/after_move(direction) + return + +/obj/tgvehicle/proc/add_control_flags(mob/controller, flags) + if(!is_occupant(controller) || !flags) + return FALSE + occupants[controller] |= flags + for(var/i in GLOB.bitflags) + if(flags & i) + grant_controller_actions_by_flag(controller, i) + return TRUE + +/obj/tgvehicle/proc/remove_control_flags(mob/controller, flags) + if(!is_occupant(controller) || !flags) + return FALSE + occupants[controller] &= ~flags + for(var/i in GLOB.bitflags) + if(flags & i) + remove_controller_actions_by_flag(controller, i) + return TRUE + +/// To add a trailer to the vehicle in a manner that allows safe qdels +/obj/tgvehicle/proc/add_trailer(obj/vehicle/added_vehicle) + trailer = added_vehicle + RegisterSignal(trailer, COMSIG_PARENT_QDELETING, PROC_REF(remove_trailer)) + +/// To remove a trailer from the vehicle in a manner that allows safe qdels +/obj/tgvehicle/proc/remove_trailer() + SIGNAL_HANDLER + UnregisterSignal(trailer, COMSIG_PARENT_QDELETING) + trailer = null + +/obj/tgvehicle/Move(newloc, dir) + // It is unfortunate, but this is the way to make it not mess up + var/atom/old_loc = loc + // When we do this, it will set the loc to the new loc + . = ..() + if(trailer && .) + var/dir_to_move = get_dir(trailer.loc, old_loc) + step(trailer, dir_to_move) + +/obj/tgvehicle/generate_action_type(actiontype) + var/datum/action/vehicle/A = ..() + . = A + if(istype(A)) + A.vehicle_ridden_target = src + +/obj/tgvehicle/post_unbuckle_mob(mob/living/M) + remove_occupant(M) + return ..() + +/obj/tgvehicle/post_buckle_mob(mob/living/M) + add_occupant(M) + return ..() + +/obj/tgvehicle/attackby(obj/item/I, mob/user, params) + if(!key_type || is_key(inserted_key) || !is_key(I)) + return ..() + if(user.drop_item()) + I.forceMove(src) + to_chat(user, "You insert [I] into [src].") + if(inserted_key) //just in case there's an invalid key + inserted_key.forceMove(drop_location()) + inserted_key = I + else + to_chat(user, "[I] seems to be stuck to your hand!") + if(inserted_key) //just in case there's an invalid key + inserted_key.forceMove(drop_location()) + inserted_key = I + +/obj/tgvehicle/AltClick(mob/user) + if(!inserted_key) + return ..() + if(!is_occupant(user)) + to_chat(user, "You must be riding the [src] to remove [src]'s key!") + return + to_chat(user, "You remove [inserted_key] from [src].") + inserted_key.forceMove(drop_location()) + user.put_in_hands(inserted_key) + inserted_key = null + +/obj/tgvehicle/user_buckle_mob(mob/living/M, mob/user, check_loc = TRUE) + if(!in_range(user, src) || !in_range(M, src)) + return FALSE + return ..(M, user, FALSE) + +/obj/tgvehicle/buckle_mob(mob/living/M, force = FALSE, check_loc = TRUE) + if(!force && occupant_amount() >= max_occupants) + return FALSE + + return ..() + +/obj/tgvehicle/zap_act(power, zap_flags) + zap_buckle_check(power) + return ..() diff --git a/icons/mob/actions/actions_vehicle.dmi b/icons/mob/actions/actions_vehicle.dmi new file mode 100644 index 00000000000..3ad64040513 Binary files /dev/null and b/icons/mob/actions/actions_vehicle.dmi differ diff --git a/icons/mob/inhands/items_lefthand.dmi b/icons/mob/inhands/items_lefthand.dmi index 955ea724379..74995335a99 100644 Binary files a/icons/mob/inhands/items_lefthand.dmi and b/icons/mob/inhands/items_lefthand.dmi differ diff --git a/icons/mob/inhands/items_righthand.dmi b/icons/mob/inhands/items_righthand.dmi index c30af0033e5..8031c8a7a7f 100644 Binary files a/icons/mob/inhands/items_righthand.dmi and b/icons/mob/inhands/items_righthand.dmi differ diff --git a/icons/obj/tgvehicles.dmi b/icons/obj/tgvehicles.dmi new file mode 100644 index 00000000000..19f5c80d57d Binary files /dev/null and b/icons/obj/tgvehicles.dmi differ diff --git a/paradise.dme b/paradise.dme index 7dc54d50058..16bb90769c3 100644 --- a/paradise.dme +++ b/paradise.dme @@ -132,6 +132,7 @@ #include "code\__DEFINES\typeids.dm" #include "code\__DEFINES\uplinks_defines.dm" #include "code\__DEFINES\vampire_defines.dm" +#include "code\__DEFINES\vehicle_defines.dm" #include "code\__DEFINES\vending_defines.dm" #include "code\__DEFINES\verb_manager.dm" #include "code\__DEFINES\vv.dm" @@ -435,6 +436,8 @@ #include "code\datums\components\swarming.dm" #include "code\datums\components\tilted.dm" #include "code\datums\components\two_handed.dm" +#include "code\datums\components\riding\riding.dm" +#include "code\datums\components\riding\riding_vehicle.dm" #include "code\datums\components\zombie_regen.dm" #include "code\datums\discord\discord_manager.dm" #include "code\datums\discord\discord_webhook.dm" @@ -499,6 +502,7 @@ #include "code\datums\elements\bombable_turf.dm" #include "code\datums\elements\earhealing.dm" #include "code\datums\elements\rad_insulation.dm" +#include "code\datums\elements\ridable.dm" #include "code\datums\elements\shatters_when_thrown.dm" #include "code\datums\elements\strippable.dm" #include "code\datums\elements\waddling.dm" @@ -2908,6 +2912,9 @@ #include "code\modules\vehicle\speedbike.dm" #include "code\modules\vehicle\sportscar.dm" #include "code\modules\vehicle\vehicle.dm" +#include "code\modules\vehicle\tg_vehicles\scooter.dm" +#include "code\modules\vehicle\tg_vehicles\tg_vehicle_actions.dm" +#include "code\modules\vehicle\tg_vehicles\tg_vehicles.dm" #include "code\modules\virology\virology_goals.dm" #include "code\modules\vote\vote_datum.dm" #include "code\modules\vote\vote_presets.dm" diff --git a/sound/effects/skateboard_ollie.ogg b/sound/effects/skateboard_ollie.ogg new file mode 100644 index 00000000000..5f0f2fc30b1 Binary files /dev/null and b/sound/effects/skateboard_ollie.ogg differ diff --git a/sound/effects/skateboard_roll.ogg b/sound/effects/skateboard_roll.ogg new file mode 100644 index 00000000000..326c175d778 Binary files /dev/null and b/sound/effects/skateboard_roll.ogg differ