/obj/vehicle/train/cargo/engine name = "cargo train tug" desc = "A ridable electric car designed for pulling cargo trolleys." icon = 'icons/obj/vehicles.dmi' icon_state = "cargo_engine" on = 0 powered = 1 locked = 0 load_item_visible = 1 load_offset_x = 0 mob_offset_y = 10 var/tgui_template = "TrainEngine" var/car_limit = 3 //how many cars an engine can pull before performance degrades active_engines = 1 var/obj/item/key/key var/key_type = /obj/item/key/cargo_train light_range = 3 light_power = 1 /obj/vehicle/train/cargo/engine/Destroy() QDEL_NULL(key) return ..() /obj/vehicle/train/cargo/engine/mechanics_hints(mob/user, distance, is_adjacent) . += ..() . += "Click-drag yourself onto the truck to climb onto it." . += "CTRL-click the truck to open the ignition and controls menu." . += "ALT-click the truck to remove the key from the ignition." . += "Click the truck to open a UI menu." . += "Click the resist button or type \"resist\" in the command bar at the bottom of your screen to get off the truck." . += "If latched, you can use a wrench to unlatch." . += "Click-drag on a trolley to latch and tow it." /obj/vehicle/train/cargo/engine/feedback_hints(mob/user, distance, is_adjacent) . += ..() if(distance > 1) return if(!ishuman(user)) return . += "The power light is [on ? "on" : "off"].\nThere are[key ? "" : " no"] keys in the ignition." . += "The charge meter reads [cell? round(cell.percent(), 0.01) : 0]%." /obj/item/key/cargo_train name = "key" desc = "A keyring with a small steel key, and a yellow fob reading \"Choo Choo!\"." icon = 'icons/obj/vehicles.dmi' icon_state = "train_keys" w_class = WEIGHT_CLASS_TINY /obj/vehicle/train/cargo/trolley name = "cargo train trolley" icon = 'icons/obj/vehicles.dmi' icon_state = "cargo_trailer" anchored = 0 passenger_allowed = 0 locked = 0 load_item_visible = 1 load_offset_x = 0 load_offset_y = 5 mob_offset_y = 8 /obj/vehicle/train/cargo/trolley/mechanics_hints(mob/user, distance, is_adjacent) . += ..() . += "You can use a wrench to unlatch this, click-drag to link it to another trolley to tow." //------------------------------------------- // Standard procs //------------------------------------------- /obj/vehicle/train/cargo/engine/setup_vehicle() ..() setup_engine() /obj/vehicle/train/cargo/engine/proc/setup_engine() cell = new /obj/item/cell/high(src) if(ispath(key_type)) key = new key_type(src) var/image/I = new(icon = icon, icon_state = "[icon_state]_overlay", layer = src.layer + 0.2) //over mobs AddOverlays(I) turn_off() /obj/vehicle/train/cargo/engine/attack_hand(mob/user) if(use_check_and_message(user)) return if(!load || user == load) // no driver, or the user is the driver ui_interact(user) return ..() /obj/vehicle/train/cargo/engine/ui_interact(mob/user, var/datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) ui = new(user, src, tgui_template, capitalize_first_letters(initial(name)), 600, 400) ui.open() /obj/vehicle/train/cargo/engine/ui_data(mob/user) var/list/data = list() data["is_on"] = on data["has_key"] = !!key data["has_cell"] = !!cell if(cell) data["cell_charge"] = cell.percent() data["is_towing"] = !!tow if(tow) data["tow"] = tow.name return data /obj/vehicle/train/cargo/engine/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) . = ..() if(.) return if(load && load != usr) to_chat(usr, SPAN_WARNING("You can't interact with \the [src] while its in use.")) return switch(action) if("toggle_engine") turn_on(usr) . = TRUE if("key") remove_key(usr) . = TRUE if("unlatch") if(tow) tow.unattach(usr) . = TRUE /obj/vehicle/train/cargo/engine/Move(var/turf/destination) if(on && cell.charge < charge_use) turn_off() update_stats() if(load && is_train_head()) to_chat(load, "The drive motor briefly whines, then drones to a stop.") if(is_train_head() && !on) return 0 //space check ~no flying space trains sorry if(on && istype(destination, /turf/space)) return 0 return ..() /obj/vehicle/train/cargo/trolley/attackby(obj/item/attacking_item, mob/user) if(open && attacking_item.tool_behaviour == TOOL_WIRECUTTER) passenger_allowed = !passenger_allowed user.visible_message(SPAN_NOTICE("[user] [passenger_allowed ? "cuts" : "mends"] a cable in [src]."), SPAN_NOTICE("You [passenger_allowed ? "cut" : "mend"] the load limiter cable.")) else ..() /obj/vehicle/train/cargo/engine/attackby(obj/item/attacking_item, mob/user) if(istype(attacking_item, key_type)) if(!key) user.drop_from_inventory(attacking_item, src) key = attacking_item // put the key in the ignition to_chat(user, SPAN_NOTICE("You slide \the [attacking_item] into \the [src]\s ignition.")) playsound(src, 'sound/machines/vehicles/key_in.ogg', 50, FALSE) else to_chat(user, SPAN_NOTICE("There is already a key in \the [src]\s ignition.")) return ..() // Cargo trains are open topped, so you can shoot at the driver. // Or you can shoot at the tug itself, if you're good. /obj/vehicle/train/cargo/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit) if (buckled && hitting_projectile.original == buckled) buckled.bullet_act(arglist(args)) else . = ..() /obj/vehicle/train/cargo/update_icon() if(open) icon_state = initial(icon_state) + "_open" else icon_state = initial(icon_state) /obj/vehicle/train/cargo/trolley/insert_cell(var/obj/item/cell/C, var/mob/living/carbon/human/H) return /obj/vehicle/train/cargo/engine/insert_cell(var/obj/item/cell/C, var/mob/living/carbon/human/H) ..() update_stats() /obj/vehicle/train/cargo/engine/remove_cell(var/mob/living/carbon/human/H) ..() update_stats() /obj/vehicle/train/cargo/engine/Collide(atom/Obstacle) var/obj/structure/machinery/door/D = Obstacle var/mob/living/carbon/human/H = load if(istype(D) && istype(H)) H.Collide(D) //a little hacky, but hey, it works, and respects access rights . = ..() /obj/vehicle/train/cargo/trolley/Collide(atom/Obstacle) if(!lead) return //so people can't knock others over by pushing a trolley around . = ..() //------------------------------------------- // Train procs //------------------------------------------- /obj/vehicle/train/cargo/engine/turn_on(var/mob/user) if(!key) audible_message("\The [src] whirrs, but the lack of a key causes it to shut down.") return if(!on) ..() to_chat(user, SPAN_NOTICE("You turn on \the [src]\s ignition.")) playsound(src, 'sound/machines/vehicles/button.ogg', 50, FALSE) addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(playsound), src, 'sound/machines/vehicles/start.ogg', 50, FALSE), 1 SECONDS) else turn_off(user) update_stats() /obj/vehicle/train/cargo/engine/turn_off(var/mob/user) ..() to_chat(user, SPAN_NOTICE("You turn off \the [src]\s ignition.")) playsound(src, 'sound/machines/vehicles/button.ogg', 50, FALSE) /obj/vehicle/train/cargo/RunOver(var/mob/living/carbon/human/H) if(HAS_TRAIT(H, TRAIT_LEANING)) return var/list/parts = list(BP_HEAD, BP_CHEST, BP_L_LEG, BP_R_LEG, BP_L_ARM, BP_R_ARM) H.apply_effects(5, 5) for(var/i = 0, i < rand(1,5), i++) var/def_zone = pick(parts) H.apply_damage(rand(5,10), DAMAGE_BRUTE, def_zone) /obj/vehicle/train/cargo/trolley/RunOver(var/mob/living/carbon/human/H) ..() if(HAS_TRAIT(H, TRAIT_LEANING)) return attack_log += "\[[time_stamp()]\] ran over [H.name] ([H.ckey])" /obj/vehicle/train/cargo/engine/RunOver(var/mob/living/carbon/human/H) ..() if(HAS_TRAIT(H, TRAIT_LEANING)) return if(is_train_head() && istype(load, /mob/living/carbon/human)) var/mob/living/carbon/human/D = load to_chat(D, SPAN_DANGER("You ran over [H]!")) visible_message(SPAN_DANGER("\The [src] ran over [H]!")) attack_log += "\[[time_stamp()]\] ran over [H.name] ([H.ckey]), driven by [D.name] ([D.ckey])" msg_admin_attack("[D.name] ([D.ckey]) ran over [H.name] ([H.ckey]). (JMP)",ckey=key_name(D),ckey_target=key_name(H)) else attack_log += "\[[time_stamp()]\] ran over [H.name] ([H.ckey])" //------------------------------------------- // Interaction procs //------------------------------------------- /obj/vehicle/train/cargo/engine/relaymove(mob/user, direction) if(user != load) return 0 if(user.restrained()) return 0 if(is_train_head()) if(direction == REVERSE_DIR(dir) && tow) //Allow the engine to rotate, but only if there's not another piece in the new direction //Basically, to allow the first rotation at spawn to align with the rest of the convoy, without it being a CBT if(!(locate(/obj/vehicle/train) in get_step(src, direction))) set_dir(direction) return 0 if(Move(get_step(src, direction))) return 1 return 0 else return ..() /obj/vehicle/train/cargo/engine/CtrlClick(mob/user) if(load && load != user) to_chat(user, SPAN_WARNING("You can't interact with \the [src] while it's in use.")) return var/list/options = list( "Toggle Ignition" = image(src, "train_keys"), "Toggle Latching" = image(src, "cargo_trailer", NORTH) ) var/chosen_option = show_radial_menu(user, src, options, radius = 42, require_near = TRUE, tooltips = TRUE) if (!chosen_option) return switch(chosen_option) if("Toggle Ignition") turn_on(user) if("Toggle Latching") if(tow) tow.unattach(user) else var/obj/vehicle/train/cargo/trolley/nearby_trolley = locate() in orange(src, 1) if(nearby_trolley) src.latch(nearby_trolley) /obj/vehicle/train/cargo/engine/AltClick(var/mob/user) if(Adjacent(user)) remove_key(user) else return ..() /obj/vehicle/train/cargo/engine/proc/remove_key(var/mob/user) if(!key) to_chat(usr, SPAN_WARNING("\The [src] doesn't have a key inserted!")) return if(load && load != usr) to_chat(usr, SPAN_WARNING("You can't remove \the [key] from \the [src] while it's in use.")) return to_chat(user, SPAN_NOTICE("You take out \the [key] out of \the [src]\s ignition.")) playsound(src, 'sound/machines/vehicles/key_out.ogg', 50, FALSE) if(on) turn_off() user.put_in_hands(key) key = null //------------------------------------------- // Loading/unloading procs //------------------------------------------- /obj/vehicle/train/cargo/trolley/load(var/atom/movable/C) if(ismob(C) && !passenger_allowed) return 0 if(!istype(C,/obj/structure/machinery) && !istype(C,/obj/structure/closet) && !istype(C,/obj/structure/largecrate) && !istype(C,/obj/structure/reagent_dispensers) && !istype(C,/obj/structure/ore_box) && !istype(C, /mob/living/carbon/human)) return 0 //if there are any items you don't want to be able to interact with, add them to this check // ~no more shielded, emitter armed death trains if(istype(C, /obj/structure/machinery)) load_object(C) else ..() if(load) return 1 /obj/vehicle/train/cargo/engine/load(var/atom/movable/C) if(!istype(C, /mob/living/carbon/human)) return 0 return ..() //Load the object "inside" the trolley and add an overlay of it. //This prevents the object from being interacted with until it has // been unloaded. A dummy object is loaded instead so the loading // code knows to handle it correctly. /obj/vehicle/train/cargo/trolley/proc/load_object(var/atom/movable/C) if(!isturf(C.loc)) //To prevent loading things from someone's inventory, which wouldn't get handled properly. return 0 if(load || C.anchored) return 0 var/datum/vehicle_dummy_load/dummy_load = new() load = dummy_load if(!load) return dummy_load.actual_load = C C.forceMove(src) if(load_item_visible) var/mutable_appearance/MA = new(C) MA.pixel_x += load_offset_x MA.pixel_y += load_offset_y MA.layer = VEHICLE_LOAD_LAYER AddOverlays(MA) /obj/vehicle/train/cargo/trolley/unload(var/mob/user, var/direction) if(istype(load, /datum/vehicle_dummy_load)) var/datum/vehicle_dummy_load/dummy_load = load load = dummy_load.actual_load dummy_load.actual_load = null qdel(dummy_load) ClearOverlays() ..() //------------------------------------------------------- // Stat update procs // // Update the trains stats for speed calculations. // The longer the train, the slower it will go. car_limit // sets the max number of cars one engine can pull at // full speed. Adding more cars beyond this will slow the // train proportionate to the length of the train. Adding // more engines increases this limit by car_limit per // engine. //------------------------------------------------------- /obj/vehicle/train/cargo/engine/update_car(var/train_length, var/active_engines) src.train_length = train_length src.active_engines = active_engines //Update move delay if(!is_train_head() || !on) move_delay = initial(move_delay) //so that engines that have been turned off don't lag behind else move_delay = max(0, (-car_limit * active_engines) + train_length - active_engines) //limits base overweight so you cant overspeed trains move_delay *= (1 / max(1, active_engines)) * 2 //overweight penalty (scaled by the number of engines) move_delay += GLOB.config.walk_speed //base reference speed move_delay *= GLOB.config.vehicle_delay_multiplier //makes cargo trains 10% slower than running when not overweight move_delay -= 1 /obj/vehicle/train/cargo/trolley/update_car(var/train_length, var/active_engines) src.train_length = train_length src.active_engines = active_engines if(!lead && !tow) anchored = 0 else anchored = 1