diff --git a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm index 04afcd3beb90..1b272624d18f 100644 --- a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm +++ b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm @@ -143,6 +143,7 @@ #define COMSIG_MOB_MOUSEDOWN "mob_mousedown" //from /client/MouseDown(): (atom/object, turf/location, control, params) #define COMSIG_MOB_MOUSEUP "mob_mouseup" //from /client/MouseUp(): (atom/object, turf/location, control, params) #define COMSIG_MOB_MOUSEDRAG "mob_mousedrag" //from /client/MouseDrag(): (atom/src_object, atom/over_object, turf/src_location, turf/over_location, src_control, over_control, params) +#define COMSIG_MOB_MOUSEMOVE "mob_mousemove" //from /client/MouseMove(): (atom/object, turf/location, control, params) #define COMSIG_MOB_CLICK_CANCELED (1<<0) #define COMSIG_MOB_CLICK_HANDLED (1<<1) diff --git a/code/__DEFINES/dcs/signals/atom/signals_projectile.dm b/code/__DEFINES/dcs/signals/atom/signals_projectile.dm index 46014d5351d5..f9030051cb89 100644 --- a/code/__DEFINES/dcs/signals/atom/signals_projectile.dm +++ b/code/__DEFINES/dcs/signals/atom/signals_projectile.dm @@ -29,6 +29,8 @@ #define COMSIG_BULLET_PRE_HANDLE_TURF "bullet_pre_handle_turf" #define COMPONENT_BULLET_PASS_THROUGH (1<<0) #define COMSIG_BULLET_TERMINAL "bullet_terminal" +/// From /obj/projectile/fly(), every tick of flight (unlike COMSIG_BULLET_TERMINAL, which only fires once, right before impact): () +#define COMSIG_BULLET_STEP "bullet_step" /// Called when a bullet hits a living mob on a sprite click (original target is final target) #define COMSIG_BULLET_DIRECT_HIT "bullet_direct_hit" diff --git a/code/__DEFINES/keybinding.dm b/code/__DEFINES/keybinding.dm index bcf1b06b6fe4..cffa10c309ee 100644 --- a/code/__DEFINES/keybinding.dm +++ b/code/__DEFINES/keybinding.dm @@ -232,6 +232,7 @@ #define COMSIG_KB_VEHICLE_CHANGE_SELECTED_WEAPON "keybinding_change_selected_weapon" #define COMSIG_KB_VEHICLE_ACTIVATE_HORN "keybinding_activate_horn" #define COMSIG_KB_VEHICLE_RELOAD_WEAPON "keybinding_reload_weapon" +#define COMSIG_KB_VEHICLE_TOGGLE_HARDPOINT_FIRE_MODE "keybinding_vehicle_toggle_hardpoint_fire_mode" #define CATEGORY_CLIENT "CLIENT" #define CATEGORY_EMOTE "EMOTE" diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm index 8f1afc5130b6..8613755e2d89 100644 --- a/code/__DEFINES/layers.dm +++ b/code/__DEFINES/layers.dm @@ -132,8 +132,21 @@ #define FACEHUGGER_LAYER 4.13 /// For Signs above everything but below weather #define BILLBOARD_LAYER 4.13 + +/// For objs atop a tank +#define TANK_RIDER_OBJ_LAYER 4.505 +/// Mostly for bodybags atop a tank +/// Not used yet. This is a reminder for me to add a specific tank_layer var to OBJs to keep track of +/// ...which specific layer they need to go when brought atop. +#define TANK_RIDER_ABOVE_OBJ_LAYER 4.506 +/// For mobs riding atop a tank +#define TANK_BELOW_RIDER_LAYER 4.508 +#define TANK_LYING_RIDER_LAYER 4.509 +#define TANK_RIDER_LAYER 4.51 +#define TANK_ABOVE_RIDER_LAYER 4.515 + /// For WEATHER -#define WEATHER_LAYER 4.14 +#define WEATHER_LAYER 5 //#define FLY_LAYER 5 diff --git a/code/__DEFINES/vehicle.dm b/code/__DEFINES/vehicle.dm index d40e89826eff..4ab95812473c 100644 --- a/code/__DEFINES/vehicle.dm +++ b/code/__DEFINES/vehicle.dm @@ -73,3 +73,29 @@ #define BUCKLE_PREVENTS_PULL (1<<3) #define BUCKLE_NEEDS_HAND (1<<4) #define BUCKLE_NEEDS_TWO_HANDS (1<<5) + +//Tank turret mouse-aim rotation + +/// How close current_angle needs to be to desired_angle before the turret rotation loop considers itself settled and stops ticking. +#define ROTATION_SETTLE_TOLERANCE 0.5 +/// How close current_angle needs to be to the angle-to-target before a turret-mounted weapon is allowed to fire. +#define FIRING_GATE_TOLERANCE 5 +/// Baseline turn speed (degrees per decisecond) for a turret whose median mounted traverse_arc equals TURRET_ARC_NORMALIZATION. +#define TURRET_BASE_ANGULAR_VELOCITY 6 +/// The traverse_arc value that maps 1:1 onto TURRET_BASE_ANGULAR_VELOCITY. +#define TURRET_ARC_NORMALIZATION 90 +/// Turn speed floor, so a turret can never be fully immobilized by a degenerate (e.g. 0) traverse_arc value. +#define TURRET_MIN_ANGULAR_VELOCITY 2 +/// Turn speed used when the turret has no weapons mounted at all. +#define TURRET_DEFAULT_ANGULAR_VELOCITY 4 +/// Flat angular acceleration (degrees per decisecond^2) used to ramp angular_velocity up/down. +#define TURRET_ANGULAR_ACCEL 1 +/// Half-width, in degrees, of the arc a self_gimballed weapon's driver-controlled aim is clamped to around the turret's own current facing (see /obj/item/hardpoint/proc/update_desired_angle()) - keeps the visual swivel (see /obj/item/hardpoint/holder/tank_turret/get_hardpoint_image()) to a modest look-around instead of a full swing to any absolute angle. +#define SLAVED_GIMBAL_ARC_HALF_WIDTH 30 + +// Flamer hardpoint fire modes - see /obj/item/hardpoint/proc/toggle_fire_mode() +/// Directional line stream (walks toward the target, self-propagating tile by tile). +#define FLAME_MODE_STREAM "stream" +/// Single traveling projectile that plants one AOE ignition point on impact. +#define FLAME_MODE_GLOB "glob" +/// should these go into a _DEFINES for hardpoints instead??? diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index f562f6b913c4..46c73c019fba 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -63,11 +63,14 @@ // GLOBAL PROCS // /// Gives X position on pixel grid of an object, accounting for offsets -/proc/get_pixel_position_x(atom/subject, relative = FALSE) +/proc/get_pixel_position_x(atom/subject, relative = FALSE, include_size_adjustment = TRUE) . = subject.pixel_x + subject.base_pixel_x if(!relative) . += world.icon_size * subject.x + if(!include_size_adjustment) + return + if(ismob(subject)) // Mobs use baked in icon_size due to eg. Xenos only using a visual size var/mob/mob_subject = subject . += (mob_subject.icon_size - world.icon_size) / 2 @@ -77,11 +80,14 @@ . += (big_subject.bound_width - world.icon_size) / 2 /// Gives Y position on pixel grid of an object, accounting for offsets -/proc/get_pixel_position_y(atom/subject, relative = FALSE) +/proc/get_pixel_position_y(atom/subject, relative = FALSE, include_size_adjustment = TRUE) . = subject.pixel_y + subject.base_pixel_y if(!relative) . += world.icon_size * subject.y + if(!include_size_adjustment) + return + if(ismob(subject)) // Mobs use baked in icon_size due to eg. Xenos only using a visual size var/mob/mob_subject = subject . += (mob_subject.icon_size - world.icon_size) / 2 @@ -106,12 +112,42 @@ var/dx = get_pixel_position_x(end) - get_pixel_position_x(start) return delta_to_angle(dx, dy) +/** + * Same as Get_Angle(), but skips the automatic "sprite is bigger than a tile, so nudge the position + * by half the extra size" compensation baked into get_pixel_position_x/y() + * + * only used for actual tturret aiming/hit-resolution math (update_desired_angle(), track_and_charge()), not for purely + * cosmetic uses (LTB reticle placement, muzzle flashes, beams). + * + * That automatic guess assumes a sprite bigger than world.icon_size is centered on its tile, which + * is wrong for a mob like the Queen (icon_size 64) whose sprite is anchored at the feet and simply + * extends upward... +*/ +/proc/Get_Angle_Grounded(atom/start, atom/end) + if(!start || !end) + return 0 + if(!start.z) + start = get_turf(start) + if(!start) + return 0 + if(!end.z) + end = get_turf(end) + if(!end) + return 0 + var/dy = get_pixel_position_y(end, include_size_adjustment = FALSE) - get_pixel_position_y(start, include_size_adjustment = FALSE) + var/dx = get_pixel_position_x(end, include_size_adjustment = FALSE) - get_pixel_position_x(start, include_size_adjustment = FALSE) + return delta_to_angle(dx, dy) + /// Calculate the angle produced by a pair of x and y deltas. Uses north-clockwise convention: NORTH = 0, EAST = 90, etc. /proc/delta_to_angle(dx, dy) . = arctan(dy, dx) //y-then-x results in north-clockwise convention: https://en.wikipedia.org/wiki/Atan2#East-counterclockwise,_north-clockwise_and_south-clockwise_conventions,_etc. if(. < 0) . += 360 +/// Shortest signed angle difference from b to a, in the range -180..180. Positive means a is clockwise of b. +/proc/angle_delta(a, b) + return ((a - b + 540) % 360) - 180 + /proc/angle_to_dir(angle) angle = ((angle % 360) + 382.5) % 360 switch(angle) //diagonal directions get priority over straight directions in edge cases diff --git a/code/_onclick/click_hold.dm b/code/_onclick/click_hold.dm index 2963dddfd9a3..da8b6edd5422 100644 --- a/code/_onclick/click_hold.dm +++ b/code/_onclick/click_hold.dm @@ -103,6 +103,19 @@ // Add the hovered atom to the trace LAZYADD(mouse_trace_history, over_obj) +// Fires at near-pixel granularity while the cursor moves over the map, even with no button held. +// if this ever gets used anywhere else, be careful: Listeners are expected to rate-limit themselves - BWSB +/client/MouseMove(atom/object, turf/location, control, params) + if(!object) + return + + var/click_catcher_click = FALSE + CONVERT_CLICK_CATCHER(object, location, click_catcher_click) + if(click_catcher_click) + params += CLICK_CATCHER_ADD_PARAM + + SEND_SIGNAL(mob, COMSIG_MOB_MOUSEMOVE, object, location, control, params) + /client/MouseDrop(datum/src_object, datum/over_object, src_location, over_location, src_control, over_control, params) . = ..() if(HAS_TRAIT(usr, TRAIT_HAULED)) diff --git a/code/_onclick/human.dm b/code/_onclick/human.dm index 6fdc1f80578e..1b7ae362098e 100644 --- a/code/_onclick/human.dm +++ b/code/_onclick/human.dm @@ -156,6 +156,23 @@ grab_level = GRAB_CARRY + var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of() + if(tank && !target.get_tank_on_top_of()) + + if(istype(tank)) + var/turf/target_turf = get_turf(target) + var/adjacent_to_tank = FALSE + for(var/turf/tank_turf in tank.locs) + if(get_dist(target_turf, tank_turf) <= 1 && target_turf != tank_turf) + adjacent_to_tank = TRUE + break + + if(adjacent_to_tank) + target.forceMove(user.loc) + tank.mark_on_top(target) + target.update_transform(TRUE) + return + target.Move(user.loc, get_dir(target.loc, user.loc)) target.update_transform(TRUE) diff --git a/code/datums/ammo/bullet/tank.dm b/code/datums/ammo/bullet/tank.dm index e9da5d110d97..268ca21d44bb 100644 --- a/code/datums/ammo/bullet/tank.dm +++ b/code/datums/ammo/bullet/tank.dm @@ -12,11 +12,11 @@ sound_hit = 'sound/weapons/sting_boom_small1.ogg' damage_falloff = 0 flags_ammo_behavior = AMMO_BALLISTIC - accurate_range_min = 4 + accurate_range_min = 3 accuracy = HIT_ACCURACY_TIER_8 scatter = 0 - damage = 60 + damage = 140 damage_var_high = PROJECTILE_VARIANCE_TIER_8 penetration = ARMOR_PENETRATION_TIER_6 accurate_range = 32 @@ -24,21 +24,36 @@ shell_speed = AMMO_SPEED_TIER_6 /datum/ammo/bullet/tank/flak/on_hit_mob(mob/M,obj/projectile/P) - burst(get_turf(M),P,damage_type, 2 , 3) - burst(get_turf(M),P,damage_type, 1 , 3 , 0) + create_shrapnel(get_turf(M), 24, rand(0, 359), 180, /datum/ammo/bullet/shrapnel/flak, P.weapon_cause_data, FALSE, 0.15, TRUE) + apply_micro_stun(M) /datum/ammo/bullet/tank/flak/on_near_target(turf/T, obj/projectile/P) - burst(get_turf(T),P,damage_type, 2 , 3) - burst(get_turf(T),P,damage_type, 1 , 3, 0) + create_shrapnel(get_turf(T), 24, rand(0, 359), 180, /datum/ammo/bullet/shrapnel/flak, P.weapon_cause_data, FALSE, 0.15, TRUE) return 1 /datum/ammo/bullet/tank/flak/on_hit_obj(obj/O,obj/projectile/P) - burst(get_turf(P),P,damage_type, 2 , 3) - burst(get_turf(P),P,damage_type, 1 , 3 , 0) + create_shrapnel(get_turf(P), 24, rand(0, 359), 180, /datum/ammo/bullet/shrapnel/flak, P.weapon_cause_data, FALSE, 0.15, TRUE) /datum/ammo/bullet/tank/flak/on_hit_turf(turf/T,obj/projectile/P) - burst(get_turf(T),P,damage_type, 2 , 3) - burst(get_turf(T),P,damage_type, 1 , 3 , 0) + create_shrapnel(get_turf(T), 24, rand(0, 359), 180, /datum/ammo/bullet/shrapnel/flak, P.weapon_cause_data, FALSE, 0.15, TRUE) + +// applies a micro-stun modelled after the AMR SPEC's second shot. +// unlike the AMR spec, this only works on xenos that aren't big sized. +/datum/ammo/bullet/tank/flak/proc/apply_micro_stun(mob/living/living_mob) + if(!isxeno(living_mob) || living_mob.mob_size >= MOB_SIZE_BIG) + return + var/mob/living/carbon/xenomorph/target = living_mob + target.KnockDown(0.15) + +/datum/ammo/bullet/tank/flak/set_bullet_traits() + . = ..() + LAZYADD(traits_to_give, list( + BULLET_TRAIT_ENTRY(/datum/element/bullet_trait_iff) + )) + +/// same shrapnel as any other fragmentation source, just capped to a 4 tile radius +/datum/ammo/bullet/shrapnel/flak + max_range = 4 /datum/ammo/bullet/tank/dualcannon name = "dualcannon bullet" @@ -90,6 +105,9 @@ damage = 40 penetration = ARMOR_PENETRATION_TIER_6 damage_armor_punch = 1 + // Close-range weapon by design: full damage out to 4 tiles, roughly halved by 7 tiles, tapers to nothing by 10 tiles. + effective_range_max = 4 + damage_falloff = 6.7 /datum/ammo/bullet/tank/setup_faction_clash_values() . = ..() diff --git a/code/datums/ammo/misc.dm b/code/datums/ammo/misc.dm index 8f7659dd9871..c69aa18bc4b6 100644 --- a/code/datums/ammo/misc.dm +++ b/code/datums/ammo/misc.dm @@ -48,16 +48,41 @@ /datum/ammo/flamethrower/do_at_max_range(obj/projectile/P) drop_flame(get_turf(P), P.weapon_cause_data) +// "Glob shot" mode ammo for the tank's primary/secondary flamer hardpoints /datum/ammo/flamethrower/tank_flamer flamer_reagent_id = "highdamagenapalm" max_range = 8 shell_speed = 1.5 + var/loaded_chem_id /datum/ammo/flamethrower/tank_flamer/drop_flame(turf/turf, datum/cause_data/cause_data) if(!istype(turf)) return - var/datum/reagent/napalm/high_damage/reagent = new() + var/datum/reagent/reagent = loaded_chem_id ? GLOB.chemical_reagents_list[loaded_chem_id] : null + if(!reagent) + reagent = new /datum/reagent/napalm/high_damage() + new /obj/flamer_fire(turf, cause_data, reagent, 1) + + var/datum/effect_system/smoke_spread/landingsmoke = new /datum/effect_system/smoke_spread + landingsmoke.set_up(1, 0, turf, null, 4, cause_data) + landingsmoke.start() + landingsmoke = null + +/// Same as tank_flamer above, but for the secondary flamer hardpoint's smaller glob. +/datum/ammo/flamethrower/tank_flamer_secondary + flamer_reagent_id = "highdamagenapalm" + max_range = 8 + shell_speed = 1.5 + var/loaded_chem_id + +/datum/ammo/flamethrower/tank_flamer_secondary/drop_flame(turf/turf, datum/cause_data/cause_data) + if(!istype(turf)) + return + + var/datum/reagent/reagent = loaded_chem_id ? GLOB.chemical_reagents_list[loaded_chem_id] : null + if(!reagent) + reagent = new /datum/reagent/napalm/high_damage() new /obj/flamer_fire(turf, cause_data, reagent, 1) var/datum/effect_system/smoke_spread/landingsmoke = new /datum/effect_system/smoke_spread @@ -336,6 +361,29 @@ damage = 2.5 flare_type = /obj/item/device/flashlight/flare/on/starshell_ash +// Fired from the tank turret's flare launcher +/datum/ammo/flare/starshell/burst + name = "star shell burst" + max_range = 7 + +/datum/ammo/flare/starshell/burst/on_hit_mob(mob/M, obj/projectile/P) + detonate(get_turf(M), P) + +/datum/ammo/flare/starshell/burst/on_hit_obj(obj/O, obj/projectile/P) + detonate(get_turf(O), P) + +/datum/ammo/flare/starshell/burst/on_hit_turf(turf/T, obj/projectile/P) + if(T.density && isturf(P.loc)) + detonate(P.loc, P) + else + detonate(T, P) + +/datum/ammo/flare/starshell/burst/do_at_max_range(obj/projectile/P, mob/firer) + detonate(get_turf(P), P) + +/datum/ammo/flare/starshell/burst/proc/detonate(turf/hit_turf, obj/projectile/fired_projectile) + create_shrapnel(hit_turf, 8, fired_projectile.dir, 360, /datum/ammo/flare/starshell, fired_projectile.weapon_cause_data, FALSE, 0) + /datum/ammo/flare/starshell/set_bullet_traits() LAZYADD(traits_to_give, list( BULLET_TRAIT_ENTRY(/datum/element/bullet_trait_iff), @@ -440,11 +488,6 @@ /datum/ammo/grenade_container/rifle flags_ammo_behavior = NO_FLAGS -/datum/ammo/grenade_container/smoke - name = "smoke grenade shell" - nade_type = /obj/item/explosive/grenade/smokebomb - icon_state = "smoke_shell" - /datum/ammo/grenade_container/tank_glauncher max_range = 8 diff --git a/code/datums/ammo/rocket.dm b/code/datums/ammo/rocket.dm index ec74c8622a7a..2c5aeef2a097 100644 --- a/code/datums/ammo/rocket.dm +++ b/code/datums/ammo/rocket.dm @@ -159,20 +159,26 @@ shell_speed = AMMO_SPEED_TIER_3 /datum/ammo/rocket/ltb/on_hit_mob(mob/mob, obj/projectile/projectile) - cell_explosion(get_turf(mob), 220, 50, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) - cell_explosion(get_turf(mob), 200, 100, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) + cell_explosion(get_turf(mob), 200, 200, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) + cell_explosion(get_turf(mob), 100, 25, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) /datum/ammo/rocket/ltb/on_hit_obj(obj/object, obj/projectile/projectile) - cell_explosion(get_turf(object), 220, 50, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) - cell_explosion(get_turf(object), 200, 100, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) + cell_explosion(get_turf(object), 200, 200, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) + cell_explosion(get_turf(object), 100, 25, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) /datum/ammo/rocket/ltb/on_hit_turf(turf/turf, obj/projectile/projectile) - cell_explosion(get_turf(turf), 220, 50, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) - cell_explosion(get_turf(turf), 200, 100, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) + cell_explosion(get_turf(turf), 200, 200, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) + cell_explosion(get_turf(turf), 100, 25, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) /datum/ammo/rocket/ltb/do_at_max_range(obj/projectile/projectile) - cell_explosion(get_turf(projectile), 220, 50, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) - cell_explosion(get_turf(projectile), 200, 100, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) + cell_explosion(get_turf(projectile), 200, 200, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) + cell_explosion(get_turf(projectile), 100, 25, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data) + +/datum/ammo/rocket/ltb/set_bullet_traits() + . = ..() + LAZYADD(traits_to_give, list( + BULLET_TRAIT_ENTRY(/datum/element/bullet_trait_iff) + )) /datum/ammo/rocket/wp name = "white phosphorous rocket" diff --git a/code/datums/components/tank_rider.dm b/code/datums/components/tank_rider.dm new file mode 100644 index 000000000000..8d9ae3d1c1e0 --- /dev/null +++ b/code/datums/components/tank_rider.dm @@ -0,0 +1,41 @@ +/** + * tank_rider tracks whether an atom/movable (a mob or a droppable obj) is currently riding atop a tank's hull. + * + * Added by /obj/vehicle/multitile/tank/proc/mark_on_top and obj_mark_on_top when something climbs onto, + * or is otherwise placed atop, a tank. Removed by clear_on_top/obj_clear_on_top whenit leaves + * + * holds a weakref to the tank rather than a direct reference so a rider never keeps a destroyed tank + * referenced, and so the component can detect a qdeld tank and clean ittself up on the next lookup + */ +/datum/component/tank_rider + dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS + var/datum/weakref/tank_ref + +/datum/component/tank_rider/Initialize(obj/vehicle/multitile/tank/tank) + if(!ismovable(parent) || !istype(tank)) + return COMPONENT_INCOMPATIBLE + tank_ref = WEAKREF(tank) + +/datum/component/tank_rider/InheritComponent(datum/component/tank_rider/new_comp, i_am_original, obj/vehicle/multitile/tank/tank) + if(istype(tank)) + tank_ref = WEAKREF(tank) + +/datum/component/tank_rider/Destroy(force, silent) + tank_ref = null + return ..() + +/datum/component/tank_rider/RegisterWithParent() + RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_qdel)) + +/datum/component/tank_rider/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_PARENT_QDELETING) + +/datum/component/tank_rider/proc/on_parent_qdel() + SIGNAL_HANDLER + qdel(src) + +/datum/component/tank_rider/proc/get_tank() + var/obj/vehicle/multitile/tank/tank = tank_ref?.resolve() + if(!tank) + qdel(src) + return tank diff --git a/code/datums/elements/bloody_feet.dm b/code/datums/elements/bloody_feet.dm index 8964eac5c882..2990e360ad99 100644 --- a/code/datums/elements/bloody_feet.dm +++ b/code/datums/elements/bloody_feet.dm @@ -11,6 +11,7 @@ /// Whether the human has moved into the turf giving them bloody feet /// Necessary because of how Crossed is called before Moved var/list/entered_bloody_turf + var/dry_timer_id /datum/element/bloody_feet/Attach(datum/target, dry_time, obj/item/clothing/shoes, steps, bcolor) . = ..() @@ -32,9 +33,11 @@ RegisterSignal(shoes, COMSIG_ITEM_DROPPED, PROC_REF(on_shoes_removed), override = TRUE) if(dry_time) - addtimer(CALLBACK(src, PROC_REF(clear_blood), target), dry_time) + dry_timer_id = addtimer(CALLBACK(src, PROC_REF(clear_blood), target), dry_time, TIMER_STOPPABLE) /datum/element/bloody_feet/Detach(datum/target, force) + deltimer(dry_timer_id) + dry_timer_id = null UnregisterSignal(target, list( COMSIG_MOVABLE_MOVED, COMSIG_HUMAN_BLOOD_CROSSED, diff --git a/code/datums/keybinding/vehicles.dm b/code/datums/keybinding/vehicles.dm index 22c20e678c4b..dcc4b95e6c59 100644 --- a/code/datums/keybinding/vehicles.dm +++ b/code/datums/keybinding/vehicles.dm @@ -84,3 +84,25 @@ var/obj/vehicle/multitile/vehicle_user = user.mob.interactee vehicle_user.reload_firing_port_weapon() return TRUE + +/** + * Toggles the fire mode of hardpoints with more than one fire mode. currently only used for the primary and secondary + * flamers + */ +/datum/keybinding/vehicles/toggle_hardpoint_fire_mode + hotkey_keys = list("Unbound") + classic_keys = list("Unbound") + name = "Toggle hardpoint fire mode" + full_name = "Toggle Hardpoint Fire Mode" + keybind_signal = COMSIG_KB_VEHICLE_TOGGLE_HARDPOINT_FIRE_MODE + +/datum/keybinding/vehicles/toggle_hardpoint_fire_mode/down(client/user) + . = ..() + if(.) + return + var/obj/vehicle/multitile/vehicle_user = user.mob.interactee + var/obj/item/hardpoint/HP = vehicle_user.get_mob_hp(user.mob) + if(!HP) + return + HP.toggle_fire_mode(user.mob) + return TRUE diff --git a/code/datums/supply_packs/explosives.dm b/code/datums/supply_packs/explosives.dm index bf17a60a7880..b987103b2bfb 100644 --- a/code/datums/supply_packs/explosives.dm +++ b/code/datums/supply_packs/explosives.dm @@ -238,8 +238,14 @@ contains = list( /obj/item/storage/box/packet/flare, /obj/item/storage/box/packet/flare, + /obj/item/storage/box/packet/flare, + /obj/item/storage/box/packet/flare, + /obj/item/storage/box/packet/flare, + /obj/item/storage/box/packet/flare, + /obj/item/storage/box/packet/flare, + /obj/item/storage/box/packet/flare, ) - cost = 40 + cost = 10 containertype = /obj/structure/closet/crate/explosives containername = "M74 AGM-Star Shell Grenade Crate" group = "Explosives" diff --git a/code/datums/supply_packs/vehicle_ammo.dm b/code/datums/supply_packs/vehicle_ammo.dm index 43ce36ec2b64..9e783d424ec7 100644 --- a/code/datums/supply_packs/vehicle_ammo.dm +++ b/code/datums/supply_packs/vehicle_ammo.dm @@ -83,16 +83,22 @@ containername = "M92T Grenade Launcher ammo crate" group = "Vehicle Ammo" +// Mimicks the star shell pack in the explosive sections. Also, does it really make sense to have those cost 4k ?? /datum/supply_packs/ammo_slauncher - name = "M34A2-A Multipurpose Turret smoke screen magazines (x4)" + name = "M74 AGM-S star shell packets (x8)" contains = list( - /obj/item/ammo_magazine/hardpoint/turret_smoke, - /obj/item/ammo_magazine/hardpoint/turret_smoke, - /obj/item/ammo_magazine/hardpoint/turret_smoke, + /obj/item/storage/box/packet/flare, + /obj/item/storage/box/packet/flare, + /obj/item/storage/box/packet/flare, + /obj/item/storage/box/packet/flare, + /obj/item/storage/box/packet/flare, + /obj/item/storage/box/packet/flare, + /obj/item/storage/box/packet/flare, + /obj/item/storage/box/packet/flare, ) - cost = 20 - containertype = /obj/structure/closet/crate/ammo - containername = "M34A2-A Multipurpose Turret smoke screen ammo crate" + cost = 10 + containertype = /obj/structure/closet/crate/explosives + containername = "M34A2-A Multipurpose Turret flare ammo crate" group = "Vehicle Ammo" /datum/supply_packs/ammo_towlauncher @@ -106,8 +112,8 @@ /datum/supply_packs/ammo_m56_cupola name = "M56 Cupola magazines (x2)" contains = list( - /obj/item/ammo_magazine/hardpoint/m56_cupola, - /obj/item/ammo_magazine/hardpoint/m56_cupola, + /obj/item/ammo_magazine/m56d, + /obj/item/ammo_magazine/m56d, ) cost = 20 containertype = /obj/structure/closet/crate/ammo diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 68ea2b137818..200a76f3cc22 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -39,6 +39,11 @@ /// Holds a reference to the emissive blocker overlay var/emissive_overlay + /// Is this item allowed atop a climbable vehicle? + /// Not at all intended for structures: This is meant for grabbable items. Weapons, ammo, pizza crates, etc... + /// ...However, defining it as part of obj instead of obj/item helps us handle exceptions. + /// E.G: Exceptionally, as QoL for corpsmen: bodybags, stasis bags, and roller beds are allowed. + var/is_allowed_atop_vehicle = FALSE /// A weakref to the mob currently interacting with us. var/datum/weakref/interactor @@ -500,8 +505,18 @@ SEND_SIGNAL(src, COMSIG_OBJ_AFTER_BUCKLE, buckled_mob) if(!buckled_mob) UnregisterSignal(buckle_target, COMSIG_PARENT_QDELETING) + if(isliving(buckle_target)) + var/mob/living/living_M = buckle_target + var/obj/vehicle/multitile/tank/tank = living_M.get_tank_on_top_of() + if(tank) + tank.clear_on_top(living_M) else RegisterSignal(buckled_mob, COMSIG_PARENT_QDELETING, PROC_REF(unbuckle)) + if(isliving(buckled_mob)) + var/mob/living/living_M = buckled_mob + var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of() + if(tank) + tank.mark_on_top(living_M) return buckled_mob /atom/movable/proc/handle_rotation() @@ -548,3 +563,12 @@ return NO_BLOCKED_MOVEMENT return ..() + +/atom/movable/proc/get_tank_on_top_of() + var/datum/component/tank_rider/rider = GetComponent(/datum/component/tank_rider) + if(!rider) + return null + return rider.get_tank() + +/atom/movable/proc/is_atop_vehicle() + return GetComponent(/datum/component/tank_rider) ? TRUE : FALSE diff --git a/code/game/machinery/vending/vendor_types/crew/vehicle_crew.dm b/code/game/machinery/vending/vendor_types/crew/vehicle_crew.dm index 3ced90bfe322..836f1eb586c4 100644 --- a/code/game/machinery/vending/vendor_types/crew/vehicle_crew.dm +++ b/code/game/machinery/vending/vendor_types/crew/vehicle_crew.dm @@ -141,11 +141,14 @@ GLOBAL_LIST_INIT(cm_vending_vehicle_crew_tank, list( list("AC3-E Autocannon", 0, /obj/effect/essentials_set/tank/autocannon, VEHICLE_PRIMARY_AVAILABLE, VENDOR_ITEM_RECOMMENDED), list("DRG-N Offensive Flamer Unit", 0, /obj/effect/essentials_set/tank/dragonflamer, VEHICLE_PRIMARY_AVAILABLE, VENDOR_ITEM_REGULAR), list("LTAA-AP Minigun", 0, /obj/effect/essentials_set/tank/gatling, VEHICLE_PRIMARY_AVAILABLE, VENDOR_ITEM_REGULAR), + list("LTB Cannon", 0, /obj/effect/essentials_set/tank/ltb, VEHICLE_PRIMARY_AVAILABLE, VENDOR_ITEM_RECOMMENDED), list("SECONDARY WEAPON", 0, null, null, null), list("M92T Grenade Launcher", 0, /obj/effect/essentials_set/tank/tankgl, VEHICLE_SECONDARY_AVAILABLE, VENDOR_ITEM_REGULAR), - list("M56 Cupola", 0, /obj/effect/essentials_set/tank/m56cupola, VEHICLE_SECONDARY_AVAILABLE, VENDOR_ITEM_REGULAR), - list("LZR-N Flamer Unit", 0, /obj/effect/essentials_set/tank/tankflamer, VEHICLE_SECONDARY_AVAILABLE, VENDOR_ITEM_RECOMMENDED), + list("M56 Cupola", 0, /obj/effect/essentials_set/tank/m56cupola, VEHICLE_SECONDARY_AVAILABLE, VENDOR_ITEM_RECOMMENDED), + list("M6H-BRUTE Launcher", 0, /obj/effect/essentials_set/tank/brute_launcher, VEHICLE_SECONDARY_AVAILABLE, VENDOR_ITEM_REGULAR), + list("LZR-N Flamer Unit", 0, /obj/effect/essentials_set/tank/tankflamer, VEHICLE_SECONDARY_AVAILABLE, VENDOR_ITEM_REGULAR), + list("Mounted UA Flag", 0, /obj/effect/essentials_set/tank/united_americas_flag, VEHICLE_SECONDARY_AVAILABLE, VENDOR_ITEM_RECOMMENDED), list("SUPPORT MODULE", 0, null, null, null), list("Artillery Module", 0, /obj/item/hardpoint/support/artillery_module, VEHICLE_SUPPORT_AVAILABLE, VENDOR_ITEM_REGULAR), @@ -375,11 +378,18 @@ GLOBAL_LIST_INIT(cm_vending_clothing_vehicle_crew, list( desc = "A giant cannon firing explosive 86mm shells. You'd be lucky if this even leaves the dust of whatever you hit with it." spawned_gear_list = list( /obj/item/hardpoint/primary/cannon, - /obj/item/ammo_magazine/hardpoint/ltb_cannon, - /obj/item/ammo_magazine/hardpoint/ltb_cannon, - /obj/item/ammo_magazine/hardpoint/ltb_cannon, - /obj/item/ammo_magazine/hardpoint/ltb_cannon, - /obj/item/ammo_magazine/hardpoint/ltb_cannon, + /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell, + /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell, + /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell, + /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell, + /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell, + /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell, + /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell, + /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell, + /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell, + /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell, + /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell, + /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell, ) /obj/effect/essentials_set/tank/gatling @@ -427,7 +437,7 @@ GLOBAL_LIST_INIT(cm_vending_clothing_vehicle_crew, list( desc = "A permanently fixed M56D, firing standard issue 10x28mm rounds." spawned_gear_list = list( /obj/item/hardpoint/secondary/m56cupola, - /obj/item/ammo_magazine/hardpoint/m56_cupola, + /obj/item/ammo_magazine/m56d, ) /obj/effect/essentials_set/tank/tankgl @@ -440,11 +450,29 @@ GLOBAL_LIST_INIT(cm_vending_clothing_vehicle_crew, list( /obj/item/ammo_magazine/hardpoint/tank_glauncher, ) +/obj/effect/essentials_set/tank/brute_launcher + desc = "A tank-mounted BRUTE breaching rocket launcher. Laser-guides a single 90mm shaped-charge rocket onto structures and walls, given a few seconds to lock on." + spawned_gear_list = list( + /obj/item/hardpoint/secondary/brute_launcher, + /obj/item/ammo_magazine/rocket/brute, + /obj/item/ammo_magazine/rocket/brute, + /obj/item/ammo_magazine/rocket/brute, + /obj/item/ammo_magazine/rocket/brute, + /obj/item/ammo_magazine/rocket/brute, + /obj/item/ammo_magazine/rocket/brute, + ) + +/obj/effect/essentials_set/tank/united_americas_flag + desc = "An United Americas Flag for mounting atop the tank." + spawned_gear_list = list( + /obj/item/hardpoint/secondary/united_americas_flag, + ) + /obj/effect/essentials_set/tank/turret spawned_gear_list = list( /obj/item/hardpoint/holder/tank_turret, - /obj/item/ammo_magazine/hardpoint/turret_smoke, - /obj/item/ammo_magazine/hardpoint/turret_smoke, + /obj/item/storage/box/packet/flare, + /obj/item/storage/box/packet/flare, ) /obj/effect/essentials_set/apc/dualcannon diff --git a/code/game/objects/effects/aliens.dm b/code/game/objects/effects/aliens.dm index e7b39d45da5c..1e1feb3b2da0 100644 --- a/code/game/objects/effects/aliens.dm +++ b/code/game/objects/effects/aliens.dm @@ -43,7 +43,7 @@ density = FALSE opacity = FALSE anchored = TRUE - layer = ABOVE_OBJ_LAYER + layer = TANK_RIDER_LAYER mouse_opacity = MOUSE_OPACITY_TRANSPARENT var/datum/cause_data/cause_data diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index fe0e01e50186..8ef8ba3507f4 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -167,6 +167,9 @@ /// Special storages this item prioritizes var/list/preferred_storage + /// Rifles, shells, grenades, metal sheets, et-cetera should all be able to 'fit' atop the tank. + is_allowed_atop_vehicle = TRUE + /obj/item/Initialize(mapload, ...) . = ..() @@ -410,6 +413,18 @@ playsound(src, drop_sound, dropvol, drop_vary) src.do_drop_animation(user) + if(isliving(user)) + var/mob/living/living_mob = user + var/obj/vehicle/multitile/tank/tank = living_mob.get_tank_on_top_of() + var/obj/vehicle/multitile/tank/current_tank = src.get_tank_on_top_of() + if(tank && !current_tank) // tank exists, our user is atop the tank, we are not marked as atop the tank yet. + tank.obj_mark_on_top(src) + else if (!tank && current_tank) // only remove from vehicle if it is atop a vehicle to begin with + current_tank.obj_clear_on_top(src) + else if (tank && current_tank) // User on tank AND already marked + src.pixel_y = initial(src.pixel_y) + 12 // Just refresh the pixel offset and layer to avoid a visual bug with animations + src.layer = TANK_RIDER_OBJ_LAYER + appearance_flags &= ~NO_CLIENT_COLOR //So saturation/desaturation etc. effects affect it. /// Called just as an item is picked up (loc is not yet changed) and will return TRUE if the pickup wasn't canceled. diff --git a/code/game/objects/items/bodybag.dm b/code/game/objects/items/bodybag.dm index 937867a7f443..4f70bd0150b8 100644 --- a/code/game/objects/items/bodybag.dm +++ b/code/game/objects/items/bodybag.dm @@ -26,9 +26,17 @@ deploy_bodybag(user, T) /obj/item/bodybag/proc/deploy_bodybag(mob/user, atom/location) - var/obj/structure/closet/bodybag/R = new unfolded_path(location, src) - R.add_fingerprint(user) - R.open(user) + var/obj/structure/closet/bodybag/bodybag = new unfolded_path(location, src) + bodybag.add_fingerprint(user) + bodybag.open(user) + if(isliving(user)) + var/mob/living/living_mob = user + var/obj/vehicle/multitile/tank/tank = living_mob.get_tank_on_top_of() + var/obj/vehicle/multitile/tank/current_tank = bodybag.get_tank_on_top_of() + if(tank && !current_tank) // tank exists, our user is atop the tank, we are not marked as atop the tank yet. + tank.obj_mark_on_top(bodybag) + else if (!tank && current_tank) // only remove from vehicle if it is atop a vehicle to begin with + current_tank.obj_clear_on_top(bodybag) user.temp_drop_inv_item(src) qdel(src) @@ -83,6 +91,7 @@ /// How many extra pixels to offset the bag by when buckled, since rollerbeds are set up to offset a centered horizontal human sprite. var/buckle_offset = 5 store_items = FALSE + is_allowed_atop_vehicle = TRUE // Allows bodybags, exceptionally, as structures, to be used atop vehicles such as the tank. /obj/structure/closet/bodybag/Initialize() . = ..() @@ -208,7 +217,8 @@ /obj/structure/closet/bodybag/forceMove(atom/destination) if(roller_buckled) - roller_buckled.unbuckle() + if(!is_atop_vehicle() && !roller_buckled.is_atop_vehicle()) + roller_buckled.unbuckle() . = ..() @@ -272,10 +282,16 @@ overlays.Cut() // makes sure any previous triage cards are removed if(!stasis_mob) - layer = initial(layer) + if(is_atop_vehicle()) + layer = TANK_RIDER_OBJ_LAYER + else + layer = initial(layer) return - layer = LYING_BETWEEN_MOB_LAYER + if(is_atop_vehicle()) + layer = TANK_RIDER_OBJ_LAYER + else + layer = LYING_BETWEEN_MOB_LAYER if(stasis_mob.holo_card_color && !opened) var/image/holo_card_icon = image('icons/obj/bodybag.dmi', src, "cryocard_[stasis_mob.holo_card_color]") @@ -297,6 +313,12 @@ if(stasis_mob) stasis_mob.in_stasis = FALSE UnregisterSignal(stasis_mob, COMSIG_HUMAN_TRIAGE_CARD_UPDATED) + var/obj/vehicle/multitile/tank/tank = src.get_tank_on_top_of() + var/obj/vehicle/multitile/tank/mob_tank = stasis_mob.get_tank_on_top_of() + if(tank) + tank.mark_on_top(stasis_mob) + else if (mob_tank) + mob_tank.clear_on_top(stasis_mob) stasis_mob = null STOP_PROCESSING(SSobj, src) if(used > max_uses) diff --git a/code/game/objects/items/explosives/mine.dm b/code/game/objects/items/explosives/mine.dm index 61d72151355e..b904d78d9157 100644 --- a/code/game/objects/items/explosives/mine.dm +++ b/code/game/objects/items/explosives/mine.dm @@ -235,6 +235,8 @@ return if(HAS_TRAIT(enemy, TRAIT_ABILITY_BURROWED)) return + if(enemy.is_on_tank_hull()) + return enemy.visible_message(SPAN_DANGER("[icon2html(src, viewers(src))] The [name] clicks as [enemy] moves in front of it."), SPAN_DANGER("[icon2html(src, enemy)] The [name] clicks as you move in front of it."), diff --git a/code/game/objects/items/storage/belt.dm b/code/game/objects/items/storage/belt.dm index 4e6511195a5a..a389f5774128 100644 --- a/code/game/objects/items/storage/belt.dm +++ b/code/game/objects/items/storage/belt.dm @@ -2830,9 +2830,9 @@ /obj/item/ammo_magazine/hardpoint/secondary_flamer, /obj/item/ammo_magazine/hardpoint/ace_autocannon, /obj/item/ammo_magazine/hardpoint/towlauncher, - /obj/item/ammo_magazine/hardpoint/m56_cupola, + /obj/item/ammo_magazine/m56d, /obj/item/ammo_magazine/hardpoint/tank_glauncher, - /obj/item/ammo_magazine/hardpoint/turret_smoke, + /obj/item/storage/box/packet/flare, /obj/item/ammo_magazine/hardpoint/boyars_dualcannon, /obj/item/ammo_magazine/hardpoint/flare_launcher, ) diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index b635123a1b0c..966cd010cf02 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -36,7 +36,6 @@ vis_flags = VIS_INHERIT_PLANE - /obj/Initialize(mapload, ...) . = ..() if(garbage) @@ -235,6 +234,55 @@ buckle_mob(M, user) else . = ..() +/obj/Moved(atom/oldloc, direction, Forced = FALSE) + . = ..() + var/obj/vehicle/multitile/tank/tank = src.get_tank_on_top_of() + if(tank && isturf(loc)) + var/still_on_tank = FALSE + for(var/obj/vehicle/multitile/tank/candidate in loc) + if(loc in candidate.locs) + still_on_tank = TRUE + break + if(!still_on_tank) + tank.obj_clear_on_top(src) + +/obj/Move(NewLoc, direct) + . = ..() + handle_rotation() + if(src.is_atop_vehicle()) + var/still_on_tank = FALSE + if(isturf(NewLoc)) + for(var/obj/vehicle/multitile/tank/T in NewLoc) + if(NewLoc in T.locs) + still_on_tank = TRUE + break + + if(!still_on_tank && buckled_mob && ismob(buckled_mob)) + var/mob/living/buckled_living = buckled_mob + var/obj/vehicle/multitile/tank/temp_tank = buckled_living.get_tank_on_top_of() + if(temp_tank) + temp_tank.clear_on_top(buckled_living) + + src.forceMove(NewLoc) + if(buckled_mob) + buckled_mob.forceMove(NewLoc) + return + if(. && buckled_mob && !handle_buckled_mob_movement(loc,direct)) + . = FALSE + +/obj/forceMove(atom/dest) + . = ..() + + // Bring the buckled_mob with us. No Move(), on_move callbacks, or any of this bullshit, we just got teleported + if(buckled_mob && loc == dest) + buckled_mob.forceMove(dest) + +/obj/BlockedPassDirs(atom/movable/mover, target_dir) + if(mover == buckled_mob) //can't collide with the thing you're buckled to + return NO_BLOCKED_MOVEMENT + + return ..() + /obj/item/proc/get_mob_overlay(mob/user_mob, slot, default_bodytype = "Default") var/bodytype = default_bodytype var/mob/living/carbon/human/user_human @@ -332,6 +380,10 @@ return /obj/handle_flamer_fire(obj/flamer_fire/fire, damage, delta_time) + if(isitem(src)) + var/obj/item/dropped_item = src + if(dropped_item.is_atop_vehicle()) // don't process flamer fire if the item is atop a climbable vehicle. + return . = ..() flamer_fire_act(damage, fire.weapon_cause_data) diff --git a/code/game/objects/structures/barricade/deployable.dm b/code/game/objects/structures/barricade/deployable.dm index 727629f763b9..28f62aecc2c4 100644 --- a/code/game/objects/structures/barricade/deployable.dm +++ b/code/game/objects/structures/barricade/deployable.dm @@ -147,6 +147,12 @@ to_chat(usr, SPAN_WARNING("[singular_name] cannot be built here!")) return + if(isliving(user)) + var/mob/living/L = user + if(L.is_on_tank_hull()) + to_chat(user, SPAN_WARNING("[singular_name] cannot be built here!")) + return + user.visible_message(SPAN_NOTICE("[user] begins deploying [singular_name]."), SPAN_NOTICE("You begin deploying [singular_name].")) diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm index f817ba2e196e..7edefcf837b3 100644 --- a/code/game/objects/structures/crates_lockers/closets.dm +++ b/code/game/objects/structures/crates_lockers/closets.dm @@ -76,8 +76,17 @@ /obj/structure/closet/proc/dump_contents() - for(var/obj/I in src) - I.forceMove(loc) + var/obj/vehicle/multitile/tank/tank = src.get_tank_on_top_of() + + for(var/obj/object in src) + object.forceMove(loc) + // closets can't go atop vehicles, but this sets behavior for bodybags, which can. + if(tank) + tank.obj_mark_on_top(object) + else + var/obj/vehicle/multitile/tank/obj_tank = object.get_tank_on_top_of() + if(obj_tank) + obj_tank.obj_clear_on_top(object) for(var/mob/M in src) M.forceMove(loc) @@ -88,6 +97,13 @@ if(living_M.mobility_flags & MOBILITY_MOVE) M.visible_message(SPAN_WARNING("[M] suddenly gets out of [src]!"), SPAN_WARNING("You get out of [src] and get your bearings!")) + // closets can't go atop vehicles, but this sets behavior for bodybags, which can. + if(tank) + tank.mark_on_top(living_M) + else + var/obj/vehicle/multitile/tank/mob_tank = living_M.get_tank_on_top_of() + if(mob_tank) + mob_tank.clear_on_top(living_M) /// Attempts to open this closet by user, skipping checks that prevent opening if forced /obj/structure/closet/proc/open(mob/user, force) diff --git a/code/game/objects/structures/stool_bed_chair_nest/bed.dm b/code/game/objects/structures/stool_bed_chair_nest/bed.dm index ce3f9c6993a6..e3d68055e101 100644 --- a/code/game/objects/structures/stool_bed_chair_nest/bed.dm +++ b/code/game/objects/structures/stool_bed_chair_nest/bed.dm @@ -60,14 +60,20 @@ /obj/structure/bed/afterbuckle(mob/M) . = ..() if(. && buckled_mob == M) - M.pixel_y = buckling_y + if(is_atop_vehicle()) + M.pixel_y = buckling_y + 12 // Magic number bad. TODO: Make tank pixel offset its own variable. + else + M.pixel_y = buckling_y M.old_y = buckling_y M.pixel_x = buckling_x M.old_x = buckling_x if(base_bed_icon) density = TRUE else - M.pixel_y = initial(buckled_mob.pixel_y) + if(is_atop_vehicle()) + M.pixel_y = initial(buckled_mob.pixel_y) + 12 // Magic number bad. TODO: Make tank pixel offset its own variable. + else + M.pixel_y = initial(buckled_mob.pixel_y) M.old_y = initial(buckled_mob.pixel_y) M.pixel_x = initial(buckled_mob.pixel_x) M.old_x = initial(buckled_mob.pixel_x) @@ -85,7 +91,11 @@ buckled_bodybag = B density = TRUE update_icon() - if(buckling_y) + var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of() + if(tank) + tank.obj_mark_on_top(buckled_bodybag) + buckled_bodybag.pixel_y = buckled_bodybag.buckle_offset + 12 // Magic number bad. TODO: Make tank pixel offset its own variable. + else if(buckling_y) buckled_bodybag.pixel_y = buckled_bodybag.buckle_offset + buckling_y add_fingerprint(user) var/mob/living/carbon/human/contained_mob = locate() in B.contents @@ -94,7 +104,10 @@ /obj/structure/bed/unbuckle() if(buckled_bodybag) - buckled_bodybag.pixel_y = initial(buckled_bodybag.pixel_y) + if(is_atop_vehicle()) + buckled_bodybag.pixel_y = initial(buckled_bodybag.pixel_y) + 12 // Magic number bad. TODO: Make tank pixel offset its own variable. + else + buckled_bodybag.pixel_y = initial(buckled_bodybag.pixel_y) buckled_bodybag.roller_buckled = null buckled_bodybag = null density = FALSE @@ -214,6 +227,7 @@ foldabletype = /obj/item/roller accepts_bodybag = TRUE base_bed_icon = "roller" + is_allowed_atop_vehicle = TRUE // Allows roller beds, exceptionally, as structures, to be used atop vehicles such as the tank. /obj/structure/bed/roller/Initialize(mapload, ...) . = ..() @@ -311,6 +325,14 @@ roller.add_fingerprint(user) user.temp_drop_inv_item(src) forceMove(roller) + if(isliving(user)) + var/mob/living/living_mob = user + var/obj/vehicle/multitile/tank/tank = living_mob.get_tank_on_top_of() + var/obj/vehicle/multitile/tank/roller_tank = roller.get_tank_on_top_of() + if(tank && !roller_tank) // tank exists, our user is atop the tank, we are not marked as atop the tank yet. + tank.obj_mark_on_top(roller) + else if (!tank && roller_tank) // only remove from vehicle if it is atop a vehicle to begin with + roller_tank.obj_clear_on_top(roller) SEND_SIGNAL(user, COMSIG_MOB_ITEM_ROLLER_DEPLOYED, roller) if(target_mob) roller.buckle_mob(target_mob, user) @@ -467,6 +489,11 @@ GLOBAL_LIST_EMPTY(activated_medevac_stretchers) matter = list("plastic" = 5000, "metal" = 5000) /obj/item/roller/medevac/deploy_roller(mob/user, atom/location) + if(isliving(user)) + var/mob/living/L = user + if(L.is_on_tank_hull()) + to_chat(user, SPAN_WARNING("You can't set up \the [src] here.")) + return var/obj/structure/bed/medevac_stretcher/medevac_stretcher = new rollertype(location) medevac_stretcher.add_fingerprint(user) user.temp_drop_inv_item(src) diff --git a/code/game/turfs/open_space.dm b/code/game/turfs/open_space.dm index dfbffbc77c2d..490682e7d316 100644 --- a/code/game/turfs/open_space.dm +++ b/code/game/turfs/open_space.dm @@ -105,7 +105,18 @@ GLOBAL_DATUM_INIT(openspace_backdrop_one_for_all, /atom/movable/openspace_backdr while(istype(below, /turf/open_space)) below = SSmapping.get_turf_below(below) + var/obj/vehicle/multitile/tank/tank_at_destination = null + for(var/obj/vehicle/multitile/tank/T in below.contents) + if(below in T.locs) + tank_at_destination = T + break + user.forceMove(below) + + if(tank_at_destination && isliving(user)) + var/mob/living/L = user + tank_at_destination.mark_on_top(L) + for(var/atom/movable/thing as anything in grabbed_things) // grabbed things aren't moved to the tile immediately to: make the animation better, preserve the grab thing.forceMove(below) below.on_climb_down(user) @@ -128,6 +139,16 @@ GLOBAL_DATUM_INIT(openspace_backdrop_one_for_all, /atom/movable/openspace_backdr var/mob/living/living_mob = movable living_mob.death(create_cause_data("falling from a high place")) + var/obj/vehicle/multitile/tank/tank_at_destination = null + for(var/obj/vehicle/multitile/tank/T in below.contents) + if(below in T.locs) + tank_at_destination = T + break + + if(tank_at_destination && isliving(movable)) + var/mob/living/L = movable + tank_at_destination.mark_on_top(L) + /// Returns a boolean whether the turf is closed and has no opening in any cardinal direction /turf/proc/check_blocked() return FALSE diff --git a/code/modules/animations/animation_library.dm b/code/modules/animations/animation_library.dm index 5a176eaf8b64..db3ddd8b3720 100644 --- a/code/modules/animations/animation_library.dm +++ b/code/modules/animations/animation_library.dm @@ -207,7 +207,10 @@ Can look good elsewhere as well.*/ pixel_x_diff = -pixel_offset pixel_y_diff = -pixel_offset animate(src, pixel_x = pixel_x + pixel_x_diff, pixel_y = pixel_y + pixel_y_diff, time = 2, flags = ANIMATION_PARALLEL) - animate(pixel_x = initial(pixel_x), pixel_y = initial(pixel_y), time = 2) + if(isliving(src) && src:is_on_tank_hull()) + animate(pixel_x = initial(pixel_x), pixel_y = initial(pixel_y) + 12, time = 2) + else + animate(pixel_x = initial(pixel_x), pixel_y = initial(pixel_y), time = 2) /atom/proc/animation_spin(speed = 5, loop_amount = -1, clockwise = TRUE, sections = 3, angular_offset = 0, pixel_fuzz = 0) if(!sections) diff --git a/code/modules/cm_aliens/XenoStructures.dm b/code/modules/cm_aliens/XenoStructures.dm index cf1ce914e46b..0bccaa437f55 100644 --- a/code/modules/cm_aliens/XenoStructures.dm +++ b/code/modules/cm_aliens/XenoStructures.dm @@ -227,6 +227,10 @@ if(HAS_TRAIT(H, TRAIT_HAULED)) return + // mobs atop the tank shouldn't get foot-stabbed + if(H.is_on_tank_hull()) + return + H.apply_armoured_damage(damage, penetration = penetration, def_zone = pick(target_limbs)) H.last_damage_data = construction_data @@ -408,7 +412,7 @@ return 1 /obj/structure/mineral_door/resin/proc/take_damage(dam, mob/mob) - health -= dam + update_health(dam) healthcheck() /obj/structure/mineral_door/resin/attackby(obj/item/W, mob/living/user) diff --git a/code/modules/cm_aliens/structures/trap.dm b/code/modules/cm_aliens/structures/trap.dm index 5caf6f0ff713..067e54dd5390 100644 --- a/code/modules/cm_aliens/structures/trap.dm +++ b/code/modules/cm_aliens/structures/trap.dm @@ -94,6 +94,10 @@ if(RESIN_TRAP_HUGGER) if(can_hug(victim, hivenumber) && !isyautja(victim) && !issynth(victim) && !isthrall(victim)) var/mob/living/victim_mob = victim + // tank treads block the hugger's hole. This behavior can be also added to normal acid traps. + var/obj/vehicle/multitile/tank/victim_tank = victim_mob.get_tank_on_top_of() + if(victim_tank && (get_turf(victim_mob) in victim_tank.locs)) + return victim_mob.visible_message(SPAN_WARNING("[victim_mob] trips on [src]!")) to_chat(victim_mob, SPAN_DANGER("You trip on [src]!")) victim_mob.apply_effect(1, WEAKEN) @@ -109,6 +113,10 @@ if(isxeno(victim)) var/mob/living/carbon/xenomorph/victim_xeno = victim if(victim_xeno.hivenumber != hivenumber) + // just in case a Greeno decides to ride on the tank. + var/obj/vehicle/multitile/tank/victim_tank = victim_xeno.get_tank_on_top_of() + if(victim_tank && (get_turf(victim_xeno) in victim_tank.locs)) + return trigger_trap() if(isVehicleMultitile(victim) && trap_type != RESIN_TRAP_GAS) trigger_trap() diff --git a/code/modules/cm_aliens/structures/xeno_structures_boilertrap.dm b/code/modules/cm_aliens/structures/xeno_structures_boilertrap.dm index 0194ea74d3a6..ef2368f57133 100644 --- a/code/modules/cm_aliens/structures/xeno_structures_boilertrap.dm +++ b/code/modules/cm_aliens/structures/xeno_structures_boilertrap.dm @@ -68,6 +68,10 @@ if (X.hivenumber != hivenumber) trigger_trap(A) else if(ishuman(A)) + // boiler's foul resin 'spikes' do not stab you through the tank + var/mob/living/M = A + if(M && M.is_on_tank_hull()) + return trigger_trap(A) return ..() @@ -91,4 +95,8 @@ return if(ishuman(A)) + // mobs atop the tank shouldn't trigger tripwires + var/mob/living/M = A + if(M && M.is_on_tank_hull()) + return linked_trap.trigger_trap(A) diff --git a/code/modules/cm_marines/equipment/mortar/mortars.dm b/code/modules/cm_marines/equipment/mortar/mortars.dm index badb9cb5df49..567c180120b9 100644 --- a/code/modules/cm_marines/equipment/mortar/mortars.dm +++ b/code/modules/cm_marines/equipment/mortar/mortars.dm @@ -624,6 +624,12 @@ if(CEILING_IS_PROTECTED(area.ceiling, CEILING_PROTECTION_TIER_1) && is_ground_level(deploy_turf.z)) to_chat(user, SPAN_WARNING("You probably shouldn't deploy [src] indoors.")) return + if(isliving(user)) + var/mob/living/L = user + if(L.is_on_tank_hull()) + to_chat(user, SPAN_WARNING("You can't deploy [src] here!")) + return + user.visible_message(SPAN_NOTICE("[user] starts deploying [src]."), SPAN_NOTICE("You start deploying [src].")) playsound(deploy_turf, 'sound/items/Deconstruct.ogg', 25, 1) diff --git a/code/modules/cm_marines/m2c.dm b/code/modules/cm_marines/m2c.dm index 5d51c42d83e8..2ce367860d13 100644 --- a/code/modules/cm_marines/m2c.dm +++ b/code/modules/cm_marines/m2c.dm @@ -111,6 +111,11 @@ if(OT.density || !isturf(OT) || !OT.allow_construction) to_chat(user, SPAN_WARNING("You can't set up \the [src] here.")) return FALSE + if(isliving(user)) + var/mob/living/L = user + if(L.is_on_tank_hull()) + to_chat(user, SPAN_WARNING("You can't set up \the [src] here.")) + return FALSE if(rotate_check.density) to_chat(user, SPAN_WARNING("You can't set up \the [src] that way, there's a wall behind you!")) return FALSE diff --git a/code/modules/cm_tech/techs/marine/tier1/arc.dm b/code/modules/cm_tech/techs/marine/tier1/arc.dm index 5a7b84a9aa0a..20497b7d431a 100644 --- a/code/modules/cm_tech/techs/marine/tier1/arc.dm +++ b/code/modules/cm_tech/techs/marine/tier1/arc.dm @@ -16,11 +16,14 @@ /datum/tech/arc/can_unlock(mob/unlocking_mob) . = ..() - var/obj/structure/machinery/cm_vending/gear/vehicle_crew/gearcomp = GLOB.VehicleGearConsole - - if(gearcomp.selected_vehicle == "TANK") - to_chat(unlocking_mob, SPAN_WARNING ("A vehicle has already been selected for this operation.")) - return FALSE + // ARC can be bought even after the Tank has been selected. + // With all of the TANK's gear being taken at roundstart (hopefully), this means that the ARC ... + // ... can overwrite the vehicle ASRS' contents without much of an issue. + + // var/obj/structure/machinery/cm_vending/gear/vehicle_crew/gearcomp = GLOB.VehicleGearConsole + // if(gearcomp.selected_vehicle == "TANK") + // to_chat(unlocking_mob, SPAN_WARNING ("A vehicle has already been selected for this operation.")) + // return FALSE return TRUE diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index 7ff77d825c5c..eecb9ee648cb 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -29,6 +29,15 @@ halimage = null halbody = null +// Same as the parent proc, but adds 1 additional check for climb_down +/mob/living/carbon/_handle_tank_edge_move(NewLoc, direct) + var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of() + var/result = ..() + if(result) + var/turf/edge = get_step(get_turf(src), direct) + if(tank.climb_down(src, edge)) + return TRUE + return result /mob/living/carbon/Move(NewLoc, direct) . = ..() @@ -539,6 +548,9 @@ var/location = get_turf(loc) remove_traits(list(TRAIT_HAULED, TRAIT_NO_STRAY, TRAIT_FLOORED, TRAIT_IMMOBILIZED), TRAIT_SOURCE_XENO_HAUL) pixel_y = 0 + var/obj/vehicle/multitile/tank/TANK = null + if(hauling_xeno.is_on_tank_hull()) + TANK = hauling_xeno.get_tank_on_top_of() UnregisterSignal(src, list(COMSIG_ATTEMPT_MOB_PULL, COMSIG_LIVING_PREIGNITION, COMSIG_LIVING_FLAMER_CROSSED, COMSIG_LIVING_FLAMER_FLAMED)) UnregisterSignal(hauling_xeno, COMSIG_MOB_DEATH) hauling_xeno = null @@ -552,7 +564,8 @@ object.Crossed(src) next_haul_resist = 0 SEND_SIGNAL(src, COMSIG_MOB_UNHAULED) - + if(TANK) + TANK.mark_on_top(src) /mob/living/carbon/proc/extinguish_mob(mob/living/carbon/C) adjust_fire_stacks(-5, min_stacks = 0) diff --git a/code/modules/mob/living/carbon/carbon_debug_verbs.dm b/code/modules/mob/living/carbon/carbon_debug_verbs.dm new file mode 100644 index 000000000000..84d26ce7c87d --- /dev/null +++ b/code/modules/mob/living/carbon/carbon_debug_verbs.dm @@ -0,0 +1,195 @@ +/// Shared lookup for the debug verbs below: finds the tank_turret on the tank this mob is currently seated in, if any. Reports why to src and returns null on failure. +/mob/living/carbon/proc/get_seated_tank_turret() + if(!istype(buckled, /obj/structure/bed/chair/comfy/vehicle)) + to_chat(src, SPAN_WARNING("You're not seated in a vehicle.")) + return null + + var/obj/structure/bed/chair/comfy/vehicle/seat_chair = buckled + var/obj/vehicle/multitile/tank/tank_vehicle = seat_chair.vehicle + if(!istype(tank_vehicle)) + to_chat(src, SPAN_WARNING("You're not seated in a tank.")) + return null + + var/obj/item/hardpoint/holder/tank_turret/turret + for(var/obj/item/hardpoint/holder/tank_turret/TT in tank_vehicle.hardpoints) + turret = TT + break + if(!turret) + to_chat(src, SPAN_WARNING("This tank has no turret installed.")) + return null + + return turret + +// Debug tool for live-tuning tank turret weapon sprite alignment +/mob/living/carbon/verb/set_tank_weapon_rotation_pivot() + set name = "Set Tank Weapon Rotation Pivot" + set category = "Debug" + set desc = "Live-tunes the rotation_pivot of a weapon mounted on the turret of the tank you're seated in." + + if(!check_rights(R_DEBUG)) + return + + var/obj/item/hardpoint/holder/tank_turret/turret = get_seated_tank_turret() + if(!turret) + return + + if(!LAZYLEN(turret.hardpoints)) + to_chat(src, SPAN_WARNING("This turret has no weapons mounted.")) + return + + var/datum/tank_pivot_tuner/tuner = new + tuner.turret = turret + for(var/obj/item/hardpoint/H in turret.hardpoints) + tuner.weapon = H + break + tuner.tgui_interact(src) + +/datum/tank_pivot_tuner + var/obj/item/hardpoint/holder/tank_turret/turret + var/obj/item/hardpoint/weapon + +/datum/tank_pivot_tuner/ui_state(mob/user) + return GLOB.always_state + +/datum/tank_pivot_tuner/ui_close(mob/user) + . = ..() + qdel(src) + +/datum/tank_pivot_tuner/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "TankPivotTuner") + ui.open() + ui.set_autoupdate(TRUE) + +/// The nearest of the 8 cardinal/diagonal dir constants to the turret's current continuous facing +/datum/tank_pivot_tuner/proc/get_turret_octant() + return angle_to_dir(turret.current_angle) + +/datum/tank_pivot_tuner/ui_data(mob/user) + . = list() + if(QDELETED(turret)) + return + + var/turret_octant = get_turret_octant() + .["dir_name"] = dir2text(turret_octant) + + var/dir_key = "[turret_octant]" + var/list/weapon_list = list() + for(var/obj/item/hardpoint/H in turret.hardpoints) + var/list/pivot = LAZYACCESS(H.rotation_pivot, dir_key) || list(0, 0) + weapon_list += list(list( + "ref" = "\ref[H]", + "name" = H.name, + "pivot_x" = pivot[1], + "pivot_y" = pivot[2], + "selected" = (H == weapon), + )) + .["weapons"] = weapon_list + + if(QDELETED(weapon)) + return + var/list/selected_pivot = LAZYACCESS(weapon.rotation_pivot, dir_key) || list(0, 0) + .["tuning_label"] = "turret facing: [dir2text(turret_octant)]" + .["selected_name"] = weapon.name + .["pivot_x"] = selected_pivot[1] + .["pivot_y"] = selected_pivot[2] + + var/list/selected_offset = LAZYACCESS(weapon.px_offsets, dir_key) || list(0, 0) + .["offset_x"] = selected_offset[1] + .["offset_y"] = selected_offset[2] + + .["is_gimballed"] = weapon.self_gimballed + if(weapon.self_gimballed) + var/list/selected_gimbal_pivot = LAZYACCESS(weapon.gimbal_pivot, dir_key) || list(0, 0) + .["gimbal_pivot_x"] = selected_gimbal_pivot[1] + .["gimbal_pivot_y"] = selected_gimbal_pivot[2] + +/datum/tank_pivot_tuner/ui_act(action, list/params, datum/tgui/ui) + . = ..() + if(.) + return + if(QDELETED(turret)) + return + + switch(action) + if("select_weapon") + var/obj/item/hardpoint/H = locate(params["ref"]) in turret.hardpoints + if(!H) + return + weapon = H + . = TRUE + if("adjust") + if(QDELETED(weapon)) + return + var/axis = params["axis"] + var/delta = params["delta"] + var/target = params["target"] + if(!delta || !(axis == "x" || axis == "y") || !(target == "rotation" || target == "gimbal" || target == "offset")) + return + if(target == "gimbal" && !weapon.self_gimballed) + return + + var/list/pivot_list + switch(target) + if("gimbal") + pivot_list = weapon.gimbal_pivot + if("offset") + pivot_list = weapon.px_offsets + else + pivot_list = weapon.rotation_pivot + var/dir_key = "[get_turret_octant()]" + var/list/pivot = LAZYACCESS(pivot_list, dir_key) || list(0, 0) + var/list/new_pivot = list(pivot[1], pivot[2]) + if(axis == "x") + new_pivot[1] = clamp(pivot[1] + delta, -200, 200) + else + new_pivot[2] = clamp(pivot[2] + delta, -200, 200) + LAZYSET(pivot_list, dir_key, new_pivot) + switch(target) + if("gimbal") + weapon.gimbal_pivot = pivot_list + if("offset") + weapon.px_offsets = pivot_list + else + weapon.rotation_pivot = pivot_list + + turret.owner.update_icon() + . = TRUE + +// instantly swaps whichever primary/secondary weapon is mounted on the tank turret + +/mob/living/carbon/verb/cycle_tank_weapon_hardpoint() + set name = "Cycle Tank Weapon Hardpoint" + set category = "Debug" + set desc = "Swaps the turret's primary/secondary weapon (whichever slot the next type in the cycle belongs to) to the next available type." + + if(!check_rights(R_DEBUG)) + return + + var/obj/item/hardpoint/holder/tank_turret/turret = get_seated_tank_turret() + if(!turret) + return + + if(!LAZYLEN(turret.accepted_hardpoints)) + to_chat(src, SPAN_WARNING("This turret doesn't accept any hardpoints.")) + return + + turret.debug_cycle_index = (turret.debug_cycle_index % length(turret.accepted_hardpoints)) + 1 + var/next_type = turret.accepted_hardpoints[turret.debug_cycle_index] + + var/obj/item/hardpoint/next_hardpoint = new next_type() + + var/obj/item/hardpoint/existing + for(var/obj/item/hardpoint/H in turret.hardpoints) + if(H.slot == next_hardpoint.slot) + existing = H + break + if(existing) + turret.remove_hardpoint(existing, get_turf(turret)) + qdel(existing) + + turret.add_hardpoint(next_hardpoint) + turret.owner.update_icon() + + to_chat(src, SPAN_NOTICE("Installed [next_hardpoint.name] on the turret ([turret.debug_cycle_index]/[length(turret.accepted_hardpoints)]).")) diff --git a/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm b/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm index a8d19062ff71..a1a374ea5d12 100644 --- a/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm +++ b/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm @@ -233,11 +233,24 @@ return . = ..() +// this proc only serves to fix a visual bug where hauled mobs get layered below the tank. +// this is a rare edge case, because xenos won't usually be hauling mobs atop the tank, but just for completeness... +/mob/living/carbon/xenomorph/proc/check_on_tank_hauled_mob(mob/victim) + var/obj/vehicle/multitile/tank/tank = null + if(src.is_on_tank_hull()) + tank = src.get_tank_on_top_of() + tank._apply_rider_visuals(victim) + else + victim.layer = initial(victim.layer) + victim.plane = initial(victim.plane) + victim.pixel_y = initial(victim.pixel_y) + /mob/living/carbon/xenomorph/Move(NewLoc, direct) . = ..() var/mob/user = hauled_mob?.resolve() if(user) user.forceMove(loc) + check_on_tank_hauled_mob(user) /mob/living/carbon/xenomorph/forceMove(atom/destination) . = ..() @@ -245,8 +258,10 @@ if(user) if(!isturf(destination)) user.forceMove(src) + check_on_tank_hauled_mob(user) else user.forceMove(loc) + check_on_tank_hauled_mob(user) /mob/living/carbon/xenomorph/relaymove(mob/user, direction) . = ..() @@ -354,6 +369,14 @@ ADD_TRAIT(src, TRAIT_IMMOBILIZED, TRAIT_SOURCE_ABILITY("Pounce")) pounceAction.freeze_timer_id = addtimer(CALLBACK(src, PROC_REF(unfreeze_pounce)), pounceAction.freeze_time, TIMER_STOPPABLE) pounceAction.additional_effects(carbon_mob) + var/obj/vehicle/multitile/tank/pounced_tank = pounced_mob.get_tank_on_top_of() + if(pounced_tank) + src.forceMove(get_turf(pounced_mob)) + pounced_tank.mark_on_top(src) + else + var/obj/vehicle/multitile/tank/tank = src.get_tank_on_top_of() + if(tank) + tank.clear_on_top(src) if(pounceAction.slash) carbon_mob.attack_alien(src, pounceAction.slash_bonus_damage) @@ -374,6 +397,21 @@ obj_launch_collision(O) return + // Castes that can pounce can use this ability to instantly climb ontop or down a tank with no delay. + var/obj/vehicle/multitile/tank/T = null + if(istype(O, /obj/vehicle/multitile/tank)) + T = O + if(T) + var/turf/current = get_turf(src) + var/turf/facing = get_step(current, dir) + if(facing && (facing in T.locs)) + forceMove(facing) + T.mark_on_top(src) + else + var/obj/vehicle/multitile/tank/current_tank = src.get_tank_on_top_of() + if(current_tank && !(locate(/obj/vehicle/multitile/tank) in get_turf(src))) + current_tank.clear_on_top(src) // if we're not atop the tank still, clear us from it. + if (pounceAction.should_destroy_objects) if(istype(O, /obj/structure/surface/table) || istype(O, /obj/structure/surface/rack) || istype(O, /obj/structure/window_frame)) var/obj/structure/S = O diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/ability_helper_procs.dm b/code/modules/mob/living/carbon/xenomorph/abilities/ability_helper_procs.dm index 4d008f6dc2b1..d49e9f17e7b6 100644 --- a/code/modules/mob/living/carbon/xenomorph/abilities/ability_helper_procs.dm +++ b/code/modules/mob/living/carbon/xenomorph/abilities/ability_helper_procs.dm @@ -301,7 +301,8 @@ var/atom/movable/temp = new spray_path() var/atom/movable/blocker = LinkBlocked(temp, prev_turf, turf) qdel(temp) - if(blocker) + // despite the tank being a blocker, acid sprays should still go through it to hit marines ontop. + if(blocker && !locate(/obj/vehicle/multitile/tank) in turf.contents) blocker.acid_spray_act(src) break diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/general_abilities.dm b/code/modules/mob/living/carbon/xenomorph/abilities/general_abilities.dm index 04fa1f7fe9b9..c561e161de0d 100644 --- a/code/modules/mob/living/carbon/xenomorph/abilities/general_abilities.dm +++ b/code/modules/mob/living/carbon/xenomorph/abilities/general_abilities.dm @@ -408,12 +408,18 @@ /datum/action/xeno_action/onclick/xenohide/can_use_action() var/mob/living/carbon/xenomorph/xeno = owner - if(xeno && !xeno.buckled && !xeno.is_mob_incapacitated() && !LAZYLEN(xeno.buckled_mobs)) - if(!(SEND_SIGNAL(xeno, COMSIG_LIVING_SHIMMY_LAYER) & COMSIG_LIVING_SHIMMY_LAYER_CANCEL)) - return TRUE - + if(xeno) + if(xeno.get_tank_on_top_of()) // Prevents cheesy layering underneath the tank. + return FALSE + if(!xeno.buckled && !xeno.is_mob_incapacitated() && !LAZYLEN(xeno.buckled_mobs)) + if(!(SEND_SIGNAL(xeno, COMSIG_LIVING_SHIMMY_LAYER) & COMSIG_LIVING_SHIMMY_LAYER_CANCEL)) + return TRUE /// remove hide and apply modified attack cooldown /datum/action/xeno_action/onclick/xenohide/proc/post_attack() + remove_hide_status() + apply_cooldown(4) //2 second cooldown after attacking + +/datum/action/xeno_action/onclick/xenohide/proc/remove_hide_status() var/mob/living/carbon/xenomorph/xeno = owner UnregisterSignal(xeno, COMSIG_MOB_STATCHANGE) if(xeno.layer == XENO_HIDING_LAYER) @@ -421,7 +427,6 @@ button.icon_state = "template_xeno" xeno.update_wounds() xeno.update_layer() - apply_cooldown(4) //2 second cooldown after attacking /datum/action/xeno_action/onclick/xenohide/give_to(mob/living/living_mob) . = ..() diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm b/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm index 08a0dcebc2bf..e431482ae7fc 100644 --- a/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm +++ b/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm @@ -1126,19 +1126,31 @@ if(distance > stab_range) return FALSE + // allows tailstabs into mobs riding atop the tank + var/obj/vehicle/multitile/tank/skip_tank = null + if(ismob(targetted_atom)) + var/mob/living/target_mob = targetted_atom + if(istype(target_mob)) + skip_tank = target_mob.get_tank_on_top_of() + var/list/turf/path = get_line(stabbing_xeno, targetted_atom, include_start_atom = FALSE) for(var/turf/path_turf as anything in path) if(path_turf.density) to_chat(stabbing_xeno, SPAN_WARNING("There's something blocking our strike!")) return FALSE for(var/obj/path_contents in path_turf.contents) + if(skip_tank && path_contents == skip_tank) + continue if(path_contents != targetted_atom && path_contents.density && !path_contents.throwpass) to_chat(stabbing_xeno, SPAN_WARNING("There's something blocking our strike!")) return FALSE var/atom/barrier = path_turf.handle_barriers(stabbing_xeno, null, (PASS_MOB_THRU_XENO|PASS_OVER_THROW_MOB|PASS_TYPE_CRAWLER)) if(barrier != path_turf) + if(skip_tank && barrier == skip_tank) + continue var/tail_stab_cooldown_multiplier = barrier.handle_tail_stab(stabbing_xeno, blunt_stab) + if(!tail_stab_cooldown_multiplier) to_chat(stabbing_xeno, SPAN_WARNING("There's something blocking our strike!")) else diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 9ef0a24c2eab..558e7f9e8739 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -156,6 +156,76 @@ return +/** + * _handle_tank_edge_move blocks consensual movement outside of tank tiles, unless you invoke climb_down() + * + * This proc makes it so that you cannot instantly dismount the tank, nor accidentally walk out of it. + * Marines and xenos alike become unable to dismount unless: + * - They are dismounted forcefully (e.g, by an explosion, grab, et-cetera) + * > Marines eat a 3 second stun if this happens. Xenos currently do not get punished. + * - They use some kind of skill to dismount (e.g pouncing outside) + * - They wait 1 second until climb_down finishes. + * + * OOPS! As a fix for LINTERS, climb_down() is called on the proc in carbon.dm + * + * Arguments: + * * NewLoc - Wherever we're trying to step down towards. + * * Direct - Direction we're facing + * + * Returns: + * * TRUE - When dismounting is impossible (blocked turf) or consensual (climb_down) + * * FALSE - When the mob dismounts improperly, and must be punished. + */ +/mob/living/proc/_handle_tank_edge_move(NewLoc, direct) + var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of() + + if(!tank) + return FALSE + + var/turf/here = get_turf(src) + if(!(here in tank.locs)) + tank.clear_on_top(src) + return FALSE + + // only care about steps that would leave the hull. + if(isturf(NewLoc) && !(NewLoc in tank.locs)) + if(!(flags_atom & DIRLOCK)) + setDir(direct) + SEND_SIGNAL(src, COMSIG_MOB_MOVE_OR_LOOK, TRUE, direct, direct) + if(pulledby || throwing) + return FALSE + var/turf/edge = get_step(here, direct) + if(!edge || edge.z != z || (edge in tank.locs)) + return FALSE + if(tank._blocked_except_mobs(edge)) + return TRUE + // special interaction for simple_animals and (edge case) silicons... + // ...to prevent SpacemanDMM from sleeping and still allow dismounts. + if(!iscarbon(src)) + tank.clear_on_top(src) + src.forceMove(NewLoc) + return TRUE + return TRUE + + return FALSE + +/** + * is_on_tank_hull checks if a mob has a valid tank rider component. + * + * This proc checks the tank this mob's tank_rider component (if any) points to. + * If there is a valid tank, it means this mob is atop a tank tile. + * + * Returns: + * * TRUE - If there is a valid tile under the tank, and if mob and tank are on the same Z-level. + * * FALSE - If the mob has no tank_rider component, if the Z-levels of the mob and Tank mistmach, if the tile Tu is invalid, + * * ...Or if the tile Tu is not, in fact, underneath a tank. + */ +/mob/living/proc/is_on_tank_hull() + var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of() + if(!tank || z != tank.z) return FALSE + var/turf/Tu = get_turf(src) + return Tu && (Tu in tank.locs) + /mob/living/Move(NewLoc, direct) if(lying_angle != 0) lying_angle_on_movement(direct) @@ -165,6 +235,20 @@ else return FALSE + if(is_on_tank_hull()) + if(_handle_tank_edge_move(NewLoc, direct)) + return FALSE + if(isturf(NewLoc)) + var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of() + if(tank && !(NewLoc in tank.locs)) + if(ishuman(src)) + var/mob/living/carbon/human/H = src + H.apply_effect(1.5, WEAKEN) + H.apply_effect(3, SUPERSLOW) + H.apply_effect(5, SLOW) + playsound(src, "punch", 25, TRUE) + tank.clear_on_top(src) + var/atom/movable/pullee = pulling if(pullee && get_dist(src, pullee) > 1) //Is the pullee adjacent? if(!pullee.clone || (pullee.clone && get_dist(src, pullee.clone) > 2)) //Be lenient with the close @@ -389,6 +473,23 @@ now_pushing = FALSE return + // tank-related collision rules + var/src_on_tank = is_on_tank_hull() + var/target_on_tank = living_mob.is_on_tank_hull() + + if(src_on_tank != target_on_tank) + now_pushing = FALSE + return + + if(src_on_tank && target_on_tank) + var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of() + if(tank == living_mob.get_tank_on_top_of()) + var/turf/destination = get_step(living_mob.loc, dir) + if(destination && !(destination in tank.locs)) + if(src.a_intent != INTENT_HELP || living_mob.a_intent != INTENT_HELP) + now_pushing = FALSE + return + if(!living_mob.buckled && !living_mob.anchored) var/mob_swap //the puller can always swap with its victim if on grab intent @@ -632,11 +733,20 @@ /mob/living/proc/update_layer() //so mob lying always appear behind standing mobs, but dead ones appear behind living ones if(pulledby && pulledby.grab_level == GRAB_CARRY) - layer = ABOVE_MOB_LAYER + if(is_on_tank_hull()) + layer = TANK_ABOVE_RIDER_LAYER + else + layer = ABOVE_MOB_LAYER else if (body_position == LYING_DOWN && stat == DEAD) - layer = LYING_DEAD_MOB_LAYER // Dead mobs should layer under living ones + if(is_on_tank_hull()) + layer = TANK_BELOW_RIDER_LAYER + else + layer = LYING_DEAD_MOB_LAYER // Dead mobs should layer under living ones else if(body_position == LYING_DOWN && layer == initial(layer)) //to avoid things like hiding larvas. //i have no idea what this means - layer = LYING_LIVING_MOB_LAYER + if(is_on_tank_hull()) + layer = TANK_LYING_RIDER_LAYER + else + layer = LYING_LIVING_MOB_LAYER /// Called when mob changes from a standing position into a prone while lacking the ability to stand up at the moment. /mob/living/proc/on_fall() diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index 4f8c1eccbbf9..9786b3433ad9 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -231,9 +231,14 @@ clear_fullscreen("weather") /mob/living/handle_flamer_fire(obj/flamer_fire/fire, damage, delta_time) + if(is_on_tank_hull()) + return . = ..() fire.set_on_fire(src) /mob/living/handle_flamer_fire_crossed(obj/flamer_fire/fire) + if(is_on_tank_hull()) + return . = ..() fire.set_on_fire(src) + diff --git a/code/modules/mob/mob_grab.dm b/code/modules/mob/mob_grab.dm index 097fba18a8a7..872d8aa12c22 100644 --- a/code/modules/mob/mob_grab.dm +++ b/code/modules/mob/mob_grab.dm @@ -116,6 +116,25 @@ playsound(loc, 'sound/weapons/thudswoosh.ogg', 25, 1, 7) user.visible_message(SPAN_WARNING("[user] holds [victim] by the neck and starts choking them!"), null, null, 5) msg_admin_attack("[key_name(user)] started to choke [key_name(victim)] at [get_area_name(victim)]", victim.loc.x, victim.loc.y, victim.loc.z) + + // allows pulling ppl onto the tank if you are already ontop of it.. + // the idea is to allow the tank to charge into an incend OB and for corpsmen ontop to recover people. + var/obj/vehicle/multitile/tank/tank = user.get_tank_on_top_of() + if(tank && !victim.get_tank_on_top_of()) + if(istype(tank)) + var/turf/victim_turf = get_turf(victim) + var/adjacent_to_tank = FALSE + for(var/turf/tank_turf in tank.locs) + if(get_dist(victim_turf, tank_turf) <= 1 && victim_turf != tank_turf) + adjacent_to_tank = TRUE + break + + if(adjacent_to_tank) + victim.forceMove(user.loc) + tank.mark_on_top(victim) + victim.update_transform(TRUE) + return + victim.Move(user.loc, get_dir(victim.loc, user.loc)) victim.update_transform(TRUE) diff --git a/code/modules/mob/mob_verbs.dm b/code/modules/mob/mob_verbs.dm index c42b79562c6a..e3e112971512 100644 --- a/code/modules/mob/mob_verbs.dm +++ b/code/modules/mob/mob_verbs.dm @@ -253,6 +253,12 @@ if(observed_atom) QDEL_NULL(observed_atom) + if(buckled && istype(buckled, /obj/structure/bed/chair/comfy/vehicle)) + var/obj/structure/bed/chair/comfy/vehicle/vehicle_seat = buckled + if(vehicle_seat.vehicle && (vehicle_seat.seat == VEHICLE_DRIVER || vehicle_seat.seat == VEHICLE_GUNNER)) + if(client) + client.change_view(8, vehicle_seat.vehicle) + vehicle_seat.vehicle.set_seated_mob(vehicle_seat.seat, src) return if(!client) @@ -271,6 +277,15 @@ to_chat(src, SPAN_WARNING("We cannot look up here, we are burrowed!")) return + var/turf/vehicle_turf = null + if(buckled && istype(buckled, /obj/structure/bed/chair/comfy/vehicle)) + var/obj/structure/bed/chair/comfy/vehicle/vehicle_seat = buckled + if(vehicle_seat.vehicle && (vehicle_seat.seat == VEHICLE_DRIVER || vehicle_seat.seat == VEHICLE_GUNNER)) + vehicle_turf = get_turf(vehicle_seat.vehicle) + else + to_chat(src, SPAN_WARNING("You cannot look up from this position.")) + return + if(!isturf(loc)) to_chat(src, SPAN_WARNING("You cannot look up here.")) return @@ -280,6 +295,10 @@ to_chat(src, SPAN_WARNING("You cannot look up here.")) return + // this is probably a bit hacky. + if(!(vehicle_turf == null)) + above = locate(vehicle_turf.x, vehicle_turf.y, vehicle_turf.z+1) + if(!istransparentturf(above)) to_chat(src, SPAN_WARNING("You cannot look up here.")) return diff --git a/code/modules/movement/launching/launching.dm b/code/modules/movement/launching/launching.dm index b5c177371b9e..a1ceb2a6b80f 100644 --- a/code/modules/movement/launching/launching.dm +++ b/code/modules/movement/launching/launching.dm @@ -211,6 +211,8 @@ break if (LM.dist >= LM.range) break + if(locate(/obj/vehicle/multitile/tank) in T) + layer = TANK_RIDER_OBJ_LAYER // prevents thrown items from being layered under a vehicle during throw. if (!Move(T)) // If this returns FALSE, then a collision happened break last_loc = loc @@ -219,10 +221,23 @@ sleep(delay) //done throwing, either because it hit something or it finished moving + // prevents xeno pounces from layering under the tank, as well as other species with pounces. + if (isliving(src)) + var/mob/living/living_mob = src + living_mob.update_layer() + var/obj/vehicle/multitile/tank/rider_tank = living_mob.get_tank_on_top_of() + if(rider_tank) + if(!(locate(/obj/vehicle/multitile/tank) in get_turf(living_mob))) + rider_tank.clear_on_top(living_mob) // if we're not atop the tank still, clear us from it. + else + src.layer = initial(src.layer) if ((isobj(src) || ismob(src)) && throwing && !early_exit) var/turf/T = get_turf(src) if(!istype(T)) return + var/obj/vehicle/multitile/tank/tank = locate(/obj/vehicle/multitile/tank) + if(tank && (tank in T)) + tank.obj_mark_on_top(src) var/atom/hit_atom = ismob(LM.target) ? null : T // TODO, just check for LM.target, the ismob is to prevent funky behavior with grenades 'n crates if(!hit_atom) for(var/atom/A in T) diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index d3c2a715f3bb..89f31cd8a0bd 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -1278,6 +1278,8 @@ and you're good to go. var/atom/original_target = target //This is for burst mode, in case the target changes per scatter chance in between fired bullets. + var/obj/vehicle/multitile/tank/rider_tank = user.get_tank_on_top_of() // Used to calculate inaccuracy when firing atop a moving tank. + if(loc != user || (flags_gun_features & GUN_WIELDED_FIRING_ONLY && !(flags_item & WIELDED))) return TRUE @@ -1290,7 +1292,7 @@ and you're good to go. var/original_scatter = projectile_to_fire.scatter var/original_accuracy = projectile_to_fire.accuracy - apply_bullet_scatter(projectile_to_fire, user, reflex, dual_wield) //User can be passed as null. + apply_bullet_scatter(projectile_to_fire, user, reflex, dual_wield, rider_tank) //User can be passed as null. curloc = get_turf(user) if(QDELETED(original_target)) //If the target's destroyed, shoot at where it was last. @@ -1914,13 +1916,23 @@ not all weapons use normal magazines etc. load_into_chamber() itself is designed return TRUE //This proc calculates scatter and accuracy -/obj/item/weapon/gun/proc/apply_bullet_scatter(obj/projectile/projectile_to_fire, mob/user, reflex = 0, dual_wield = 0) +/obj/item/weapon/gun/proc/apply_bullet_scatter(obj/projectile/projectile_to_fire, mob/user, reflex = 0, dual_wield = 0, obj/vehicle/multitile/tank/rider_tank = null) var/gun_accuracy_mult = accuracy_mult_unwielded var/gun_scatter = scatter_unwielded if(flags_item & WIELDED || flags_gun_features & GUN_ONE_HAND_WIELDED) gun_accuracy_mult = accuracy_mult gun_scatter = scatter + // increases scatter to penalize marines firing atop a moving tank instead of outright restricting it + // only for wielded firearms. + if(rider_tank) + var/obj/vehicle/multitile/tank/TANK = rider_tank + if(world.time < TANK.on_top_mobs_shooting_inaccuracy_time) + if(world.time % 3) + to_chat(gun_user, SPAN_DANGER("You struggle to keep your aim centered as the [TANK] moves!")) + gun_accuracy_mult = max(0.1, gun_accuracy_mult * 0.4) // 60% accuracy loss + gun_scatter += SCATTER_AMOUNT_TIER_4 + else if(user && world.time - user.l_move_time < 5) //moved during the last half second //accuracy and scatter penalty if the user fires unwielded right after moving gun_accuracy_mult = max(0.1, gun_accuracy_mult - max(0,movement_onehanded_acc_penalty_mult * HIT_ACCURACY_MULT_TIER_3)) diff --git a/code/modules/projectiles/homing_projectile_component.dm b/code/modules/projectiles/homing_projectile_component.dm index 8fd36fd67f31..903bff54deab 100644 --- a/code/modules/projectiles/homing_projectile_component.dm +++ b/code/modules/projectiles/homing_projectile_component.dm @@ -22,17 +22,30 @@ src.homing_target = homing_target /datum/component/homing_projectile/RegisterWithParent() + RegisterSignal(parent, COMSIG_BULLET_STEP, PROC_REF(step_retarget)) RegisterSignal(parent, COMSIG_BULLET_TERMINAL, PROC_REF(terminal_retarget)) /datum/component/homing_projectile/UnregisterFromParent() - UnregisterSignal(parent, COMSIG_BULLET_TERMINAL) + UnregisterSignal(parent, list(COMSIG_BULLET_STEP, COMSIG_BULLET_TERMINAL)) homing_target = null /datum/component/homing_projectile/Destroy() homing_target = null return ..() +// Continuously re-aims toward the target's live position every tick of flight +/datum/component/homing_projectile/proc/step_retarget() + SIGNAL_HANDLER + if(QDELETED(homing_target)) + return + var/obj/projectile/projectile = parent + var/turf/homing_turf = get_turf(homing_target) + if(!homing_turf || homing_turf == get_turf(projectile)) + return + projectile.retarget(homing_turf, keep_angle = FALSE) + /datum/component/homing_projectile/proc/terminal_retarget() + SIGNAL_HANDLER var/obj/projectile/projectile = parent var/turf/homing_turf = get_turf(homing_target) projectile.speed *= 2 // Double speed to ensure hitting next tick despite eventual movement diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index 83ecb9b43ce1..c41576dd938c 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -91,6 +91,9 @@ var/damage_boosted = 0 var/last_damage_mult = 1 + /// Tracks if the projectile is inside obj/vehicle/multitile/tank + var/obj/vehicle/multitile/tank/inside_tank = null + /obj/projectile/Initialize(mapload, datum/cause_data/cause_data) . = ..() path = list() @@ -358,6 +361,11 @@ SHOULD_NOT_SLEEP(TRUE) PRIVATE_PROC(TRUE) var/turf/current_turf = get_turf(src) + + // Lets homing-style components continuously re-aim toward their live ttarget + // keeps the shot actually curving to follow a moving target over the whole flight. + SEND_SIGNAL(src, COMSIG_BULLET_STEP) + var/turf/next_turf = popleft(path) // Terminal projectiles (about to hit) are handled firer for retarget logic @@ -408,6 +416,7 @@ path.Cut(1, 2) // remove the turf we're already on var/atom/source = keep_angle ? original : current_turf update_angle(source, new_target) + target_turf = get_turf(new_target) /obj/projectile/proc/scan_a_turf(turf/turf, proj_dir) . = TRUE // Sleep safeguard: stop the bullet @@ -419,6 +428,26 @@ if(!istype(turf)) return FALSE + // Checkc if we're inside a tank and trying to exit + if(inside_tank) + var/tank_found = FALSE + for(var/obj/obj in turf) + var/obj/vehicle/multitile/tank/M = _owning_tank_of(obj) + if(M == inside_tank || obj == inside_tank) + tank_found = TRUE + break + + // hits the tank if there are no tank parts inside the turf + if(!tank_found) + // unless it was shot by a mob atop the tank. + if(isliving(firer)) + var/mob/living/M = firer + if(M.is_on_tank_hull()) + return FALSE + ammo.on_hit_obj(inside_tank, src) + inside_tank.bullet_act(src) + return TRUE + if(turf.density) // Handle wall hit if(turf in permutated) return FALSE @@ -478,6 +507,21 @@ return FALSE permutated |= obj + var/obj/vehicle/multitile/tank/M = _owning_tank_of(obj) + if(!M) + if(istype(obj, /obj/vehicle/multitile/tank)) + M = obj + + // this block allows projectiles fired from outside the tank to travel inside it. + if(M) + if(!inside_tank) + inside_tank = M + return FALSE + else if(inside_tank == M) + return FALSE + else // if inside_tank exists but is not M, it means we're firing on another tank, so, we return true. + return TRUE + var/hit_chance = obj.get_projectile_hit_boolean(src) if(hit_chance) // Calculated from combination of both ammo accuracy and gun accuracy SEND_SIGNAL(src, COMSIG_BULLET_PRE_HANDLE_OBJ, obj) @@ -1349,6 +1393,12 @@ if(dx == -1 || dx == 1) return TRUE +// helper proc to see if we are about to hit a tank +/obj/projectile/proc/_owning_tank_of(atom/A) + if(istype(A, /obj/vehicle/multitile/tank)) + return A + return null + /obj/projectile/vulture accuracy_range_falloff = 10 /// The odds of hitting a xeno in less than your gun's range. Doesn't apply to humans. diff --git a/code/modules/vehicles/hardpoints/hardpoint.dm b/code/modules/vehicles/hardpoints/hardpoint.dm index a75fb9353b43..05e411ac7685 100644 --- a/code/modules/vehicles/hardpoints/hardpoint.dm +++ b/code/modules/vehicles/hardpoints/hardpoint.dm @@ -33,6 +33,11 @@ /// List of pixel offsets for each direction. var/list/px_offsets + // pivots used to calculate the correctt rotation both for secondary hardpoints and for primary hardpoints. + // because the secondary hardpoints are rotating WITH the turret, but also AROUND themselves, they need two pivots. + var/list/rotation_pivot + var/list/gimbal_pivot + /// Visual layer of hardpoint when on vehicle. var/hdpt_layer = HDPT_LAYER_WHEELS @@ -68,8 +73,35 @@ /// Used to prevent welder click spam. var/being_repaired = FALSE - /// The firing arc of this hardpoint. - var/firing_arc = 0 //in degrees. 0 skips whole arc of fire check + /// How fast this weapon can traverse. On a turret-mounted weapon, contributes to the shared turret's turn speed (see recalculate_turn_rate()). On a fixed-mount weapon, this is a static firing cone instead (see in_firing_arc()). + var/traverse_arc = 0 //in degrees. 0 skips whole arc of fire check on fixed mounts + + /// If TRUE, this hardpoint runs its own independent mouse-aim rotation (current_angle/rotation_loop below) instead of following its holder's rotate() cascade - see /obj/item/hardpoint/secondary/proc/toggle_slaved_to_driver(). + var/self_gimballed = FALSE + /// The hardpoint's live, continuously-simulated facing (0-360, north-clockwise). Distinct from dir, which stays snapped to a cardinal for sprite selection. Only meaningful for the turret itself, or a self_gimballed hardpoint. + var/current_angle = 0 + /// Where the mouse currently wants this hardpoint to face (0-360, north-clockwise). current_angle chases this. + var/desired_angle = 0 + /// Current turn speed, in degrees per tick, ramped toward max_angular_velocity. + var/angular_velocity = 0 + /// Turn speed cap. For the turret, derived from the median traverse_arc of mounted weapons (see recalculate_turn_rate()). For a self_gimballed weapon, derived from its own traverse_arc. + var/max_angular_velocity = TURRET_DEFAULT_ANGULAR_VELOCITY + /// How fast angular_velocity ramps up/down, in degrees per tick^2. + var/angular_accel = TURRET_ANGULAR_ACCEL + /// Guards against spawning more than one concurrent rotation_loop(). + var/rotation_active = FALSE + /// world.time of the last processed mouse-move, used to rate-limit update_desired_angle(). + var/last_desired_update_time = 0 + /// While TRUE, update_desired_angle() no-ops - set on whichever object get_rotation_owner() resolves + /// to while a track_and_charge() lock-on is in progress, so live mouse input can't fight it. + var/aim_locked = FALSE + /// Guards against spawning more than one concurrent track_and_charge() on this hardpoint. + var/charging = FALSE + /// Set by cancel_charge() (COMSIG_GUN_STOP_FIRE/COMSIG_GUN_INTERRUPT_FIRE) to abort an in-progress + /// track_and_charge() loop. Distinct from fire_wait_cancelled, which guards a different wait + /// (arc-entry, not an active charge) and could otherwise interfere if both were active at once. + var/charge_cancelled = FALSE + var/trigger_held = FALSE // Muzzleflash var/use_muzzle_flash = FALSE @@ -81,6 +113,11 @@ /// Currently loaded ammo that we shoot from. var/obj/item/ammo_magazine/hardpoint/ammo + /// The type of magazine this hardpoint accepts, cached from ammo's class-default in Initialize() + /// unlike ammo itself, this stays set even while ammo is temporary null, so + /// get_hardpoints_with_ammo()/try_add_clip()/the weapons loader can still recognize this + /// hardpoint as reloadable and know whatt magazine type to accept + var/ammo_type /// Spare magazines that we can reload from. var/list/backup_clips /// Maximum amount of spare mags. @@ -96,6 +133,8 @@ var/shots_fired = 0 /// Delay before a new firing sequence can start. COOLDOWN_DECLARE(fire_cooldown) + /// Set by cancel_firing_arc_wait() to break out of wait_for_firing_arc() early. + var/fire_wait_cancelled = FALSE // Firemodes. /// Current selected firemode of the gun. @@ -127,6 +166,8 @@ var/atom/target /// The type of projectile to fire var/projectile_type = /obj/projectile + /// How many ammo.current_rounds a single base handle_fire() shot costs. The flamer hardpoints (primary/flamer.dm, secondary/flamer.dm) set this to 0 and do their own reagent-based fuel deduction instead, since their ammo tracks real chemical volume rather than plain integer rounds. + var/ammo_cost_per_shot = 1 //----------------------------- //------GENERAL PROCS---------- @@ -141,6 +182,8 @@ /obj/item/hardpoint/Initialize() . = ..() set_bullet_traits() + if(ammo) + ammo_type = ammo.type AddComponent(/datum/component/automatedfire/autofire, fire_delay, burst_delay, burst_amount, gun_firemode, autofire_slow_mult, CALLBACK(src, PROC_REF(set_burst_firing)), CALLBACK(src, PROC_REF(reset_fire)), CALLBACK(src, PROC_REF(fire_wrapper)), callback_set_firing = CALLBACK(src, PROC_REF(set_auto_firing))) /obj/item/hardpoint/Destroy() @@ -169,6 +212,8 @@ /obj/item/hardpoint/proc/generate_bullet(mob/user, turf/origin_turf) var/obj/projectile/P = new projectile_type(origin_turf, create_cause_data(initial(name), user)) P.generate_bullet(new ammo.default_ammo) + P.effective_range_max = P.ammo.effective_range_max + P.effective_range_min = P.ammo.effective_range_min // Apply bullet traits from gun for(var/entry in traits_to_give) var/list/L @@ -354,17 +399,23 @@ sleep(20) - forceMove(ammo, get_turf(src)) - ammo.update_icon() + if(ammo) + forceMove(ammo, get_turf(src)) + ammo.update_icon() ammo = A LAZYREMOVE(backup_clips, A) + owner?.update_icon() to_chat(user, SPAN_NOTICE("You reload \the [name].")) +/// Whether this hardpoint will accept a given magazine for reloading. Eg, m56d cupola accepts m56d drums. +/obj/item/hardpoint/proc/accepts_magazine(obj/item/ammo_magazine/magazine) + return istype(magazine, ammo_type) + //try adding magazine to hardpoint's backup clips. Called via weapons loader /obj/item/hardpoint/proc/try_add_clip(obj/item/ammo_magazine/A, mob/user) - if(!ammo) - to_chat(user, SPAN_WARNING("\The [name] doesn't use ammunition.")) + if(!ammo_type) + to_chat(user, SPAN_WARNING("\The [name] doesn't use ammunition.")) // UA flag return FALSE if(max_clips == 0) to_chat(user, SPAN_WARNING("\The [name] does not have room for additional ammo.")) @@ -387,7 +438,7 @@ playsound(loc, 'sound/machines/hydraulics_2.ogg', 50) LAZYADD(backup_clips, A) - to_chat(user, SPAN_NOTICE("You load \the [A] into \the [name]. Ammo: [SPAN_HELPFUL(ammo.current_rounds)]/[SPAN_HELPFUL(ammo.max_rounds)] | Mags: [SPAN_HELPFUL(LAZYLEN(backup_clips))]/[SPAN_HELPFUL(max_clips)]")) + to_chat(user, SPAN_NOTICE("You load \the [A] into \the [name]. Ammo: [SPAN_HELPFUL(ammo ? ammo.current_rounds : 0)]/[SPAN_HELPFUL(ammo ? ammo.max_rounds : 0)] | Mags: [SPAN_HELPFUL(LAZYLEN(backup_clips))]/[SPAN_HELPFUL(max_clips)]")) return TRUE /obj/item/hardpoint/attackby(obj/item/O, mob/user) @@ -523,6 +574,7 @@ /// Reset variables used in firing and remove the gun from the autofire system. /obj/item/hardpoint/proc/stop_fire(datum/source, atom/object, turf/location, control, params) + trigger_held = FALSE SEND_SIGNAL(src, COMSIG_GUN_STOP_FIRE) if(auto_firing) reset_fire() //automatic fire doesn't reset itself from COMSIG_GUN_STOP_FIRE @@ -545,16 +597,20 @@ return set_target(get_turf_on_clickcatcher(object, source, params)) + trigger_held = TRUE if(gun_firemode == GUN_FIREMODE_SEMIAUTO) - var/fire_return = try_fire(object, source, params) - //end-of-fire, show ammo (if changed) - if(fire_return == AUTOFIRE_CONTINUE) - reset_fire() - display_ammo(source) + INVOKE_ASYNC(src, PROC_REF(fire_semiauto), object, source, params) else SEND_SIGNAL(src, COMSIG_GUN_FIRE) +// Fires a single semi-auto shot and handles its result so it can be invoked asynchronous for linters +/obj/item/hardpoint/proc/fire_semiauto(atom/target, mob/living/user, params) + var/fire_return = try_fire(target, user, params) + if(fire_return == AUTOFIRE_CONTINUE) + reset_fire() + display_ammo(user) + /// Wrapper proc for the autofire system to ensure the important args aren't null. /obj/item/hardpoint/proc/fire_wrapper(atom/target, mob/living/user, params) if(!target) @@ -572,23 +628,198 @@ to_chat(user, SPAN_WARNING("\The [name] is broken!")) return NONE - if(ammo && ammo.current_rounds <= 0) + // A hardpoint can legitimately have no magazine installed at all. This is needed especifically so the + // DRGN flamer and the secondary version can 'eject' their magazines to be refueled from a fuel source.. + // so, other guns can now eject their full magazines and have nothing loaded. + if(!ammo) + click_empty(user) + return NONE + + if(ammo.current_rounds <= 0) click_empty(user) return NONE - if(!in_firing_arc(target)) - to_chat(user, SPAN_WARNING("The target is not within your firing arc!")) + // stops you from firing at your own hull. prevents a bug found in testing where guns shoot straight up. + if(owner && (get_turf(target) in owner.locs)) + to_chat(user, SPAN_WARNING("Invalid target!")) return NONE - return handle_fire(target, user, params) + var/atom/original_target = target + var/obj/item/hardpoint/holder/tank_turret/turret = loc + var/turret_mounted = istype(turret) + if((self_gimballed || turret_mounted) && gun_firemode != GUN_FIREMODE_SEMIAUTO) + // Held-trigger fire modes (burst/automatic) don't wait for the turret to finish swinging onto target + var/obj/item/hardpoint/facing_source = self_gimballed ? src : turret + target = facing_source.redirect_to_current_facing(get_origin_turf(), target) + else if(!in_firing_arc(target)) + // Turret-mounted weapons wait for the turret (or their own gimbal) to finish swinging onto the target before firing + if(!turret_mounted || !wait_for_firing_arc(target, user)) + to_chat(user, SPAN_WARNING("The target is not within your firing arc!")) + return NONE + + return handle_fire(target, user, params, original_target) + +/** + * Waits for in_firing_arc(target) to become true, instead of instantly rejecting the shot. + * + * Returns TRUE once in arc and ready to fire, FALSE if the wait was cancelled or the shot became + * impossible for some other reason while waiting (weapon/target gone, broke, ran dry, trigger let go). + */ +/obj/item/hardpoint/proc/wait_for_firing_arc(atom/target, mob/user) + fire_wait_cancelled = FALSE + RegisterSignal(src, list(COMSIG_GUN_STOP_FIRE, COMSIG_GUN_INTERRUPT_FIRE), PROC_REF(cancel_firing_arc_wait)) + + . = TRUE + while(!in_firing_arc(target)) + sleep(1) + if(fire_wait_cancelled || !trigger_held || QDELETED(src) || QDELETED(target) || health <= 0 || (ammo && ammo.current_rounds <= 0)) + . = FALSE + break + + UnregisterSignal(src, list(COMSIG_GUN_STOP_FIRE, COMSIG_GUN_INTERRUPT_FIRE)) + +/obj/item/hardpoint/proc/cancel_firing_arc_wait() + SIGNAL_HANDLER + fire_wait_cancelled = TRUE + +/** + * Resolves which object actually owns continuous rotation (current_angle/desired_angle/rotation_loop) + * for this hardpoint - itself if self_gimballed, or the shared turret holder if turret-mounted. + * Mirrors the same dispatch already used implicitly by in_firing_arc()/check_gimbal_firing_arc()/ + * in_turret_firing_arc() above. + */ +/obj/item/hardpoint/proc/get_rotation_owner() + RETURN_TYPE(/obj/item/hardpoint) + if(self_gimballed) + return src + var/obj/item/hardpoint/holder/tank_turret/turret = loc + if(istype(turret)) + return turret + return src + +/// Single choke point for locking/unlocking mouse-driven aim on whichever object get_rotation_owner() +/// resolves to, so ttrack_and_charge() below can lock/unlock symmetrically. +/obj/item/hardpoint/proc/lock_rotation(lock) + get_rotation_owner().aim_locked = lock + +/** + * Locks onto target and keeps re-aiming at its live position for aim_time, cancellable if the target + * drifts further behind the current facing than this hardpoint's own traverse_arc allows, giving the + * mount a litttle bit of allowance to catch up before the shot is considered lost + * + * Recomputes the true world-space angle to the target fresh every tick from this hardpoint's current + * position, so it needs no special-casing for the vehicle itself moving/turning mid-charge + * + * + * Returns TRUE if the charge completed with the target still valid and in arc, FALSE if cancelled. + */ +/obj/item/hardpoint/proc/track_and_charge(atom/target, mob/living/user, aim_time) + if(charging) + return FALSE + charging = TRUE + + set_target(target) + lock_rotation(TRUE) + charge_cancelled = FALSE + RegisterSignal(src, list(COMSIG_GUN_STOP_FIRE, COMSIG_GUN_INTERRUPT_FIRE), PROC_REF(cancel_charge)) + start_aim_visuals(target, user) + + . = TRUE + var/elapsed = 0 + while(elapsed < aim_time) + sleep(1) + elapsed += 1 + + if(charge_cancelled || !trigger_held || QDELETED(src) || QDELETED(target) || health <= 0 || (ammo && ammo.current_rounds <= 0)) + . = FALSE + break + + if(isliving(target)) + var/mob/living/living_target = target + if(living_target.is_dead()) + . = FALSE + break + + var/turf/origin_turf = get_origin_turf() + var/turf/target_turf = get_turf(target) + if(origin_turf == target_turf) + . = FALSE + break + + try + // Angle uses the target atom itself, not target_turf + var/angle_to_target = Get_Angle_Grounded(origin_turf, target) + var/obj/item/hardpoint/rotation_owner = get_rotation_owner() + var/obj/item/hardpoint/holder/tank_turret/mount = self_gimballed ? loc : null + + if(istype(mount)) // self-gimballed, mounted on a turret + var/turret_facing = mount.current_angle + var/raw_delta = angle_delta(angle_to_target, turret_facing) + if(abs(raw_delta) > SLAVED_GIMBAL_ARC_HALF_WIDTH) + to_chat(user, SPAN_WARNING("Target moved out of the firing arc!")) + . = FALSE + else + var/swing = clamp(raw_delta, -SLAVED_GIMBAL_ARC_HALF_WIDTH, SLAVED_GIMBAL_ARC_HALF_WIDTH) + rotation_owner.desired_angle = ((turret_facing + swing) % 360 + 360) % 360 + else + rotation_owner.desired_angle = angle_to_target + if(traverse_arc && abs(angle_delta(angle_to_target, rotation_owner.current_angle)) > traverse_arc * 0.5) + to_chat(user, SPAN_WARNING("Target moved out of the firing arc!")) + . = FALSE + if(. != FALSE) + rotation_owner.start_rotation_if_needed() + on_track_tick(target) + catch(var/exception/tick_error) + log_runtime(tick_error) + . = FALSE + + if(. == FALSE) + break + + UnregisterSignal(src, list(COMSIG_GUN_STOP_FIRE, COMSIG_GUN_INTERRUPT_FIRE)) + end_aim_visuals(target, user, .) + lock_rotation(FALSE) + charging = FALSE + +/obj/item/hardpoint/proc/cancel_charge() + SIGNAL_HANDLER + charge_cancelled = TRUE + +/// No-op hook - override to spawn lock-on visuals (overlays/beams) when a track_and_charge() begins. +// currently overriden by LTB and mounted BRUTE launcher only. Same goes for the other hooks below. - BWSB +/obj/item/hardpoint/proc/start_aim_visuals(atom/target, mob/living/user) + return + +/// No-op hook - override to tear down whatever start_aim_visuals() spawned. Always called, success or not. +/obj/item/hardpoint/proc/end_aim_visuals(atom/target, mob/living/user, success) + return + +/// No-op hook - called once per tick during track_and_charge(), after desired_angle has been updated. +/// Override to keep any tick-dependent visuals (e.g. a beam anchor) following the live muzzle position. +/obj/item/hardpoint/proc/on_track_tick(atom/target) + return /// Actually fires the gun, sets up the projectile and fires it. -/obj/item/hardpoint/proc/handle_fire(atom/target, mob/living/user, params) +/obj/item/hardpoint/proc/handle_fire(atom/target, mob/living/user, params, atom/original_target) + if(isnull(original_target)) + original_target = target var/turf/origin_turf = get_origin_turf() - var/obj/projectile/projectile_to_fire = generate_bullet(user, origin_turf) - ammo.current_rounds-- + // Spawn projectile outside the vehicle hull so riders aren't hit. + var/turf/spawn_turf = origin_turf + if(owner && length(owner.locs) > 1) + var/turf/target_turf = get_turf(target) + var/list/path = get_line(origin_turf, target_turf) + for(var/turf/T as anything in path) + // preventts a bug where the projectile disappears if it occupies the same tile the projectile spawns in + if(!(T in owner.locs) && T != target_turf) + spawn_turf = T + break + + var/obj/projectile/projectile_to_fire = generate_bullet(user, spawn_turf) + ammo.current_rounds -= ammo_cost_per_shot SEND_SIGNAL(projectile_to_fire, COMSIG_BULLET_USER_EFFECTS, user) + projectile_to_fire.original = original_target // turf-targeted projectiles are fired without scatter, because proc would raytrace them further away var/ammo_flags = projectile_to_fire.ammo.flags_ammo_behavior | projectile_to_fire.projectile_override_flags @@ -608,6 +839,51 @@ return AUTOFIRE_CONTINUE +/** + * No-op by default - only the flamer hardpoints (primary/flamer.dm, secondary/flamer.dm) override + * this to flip between FLAME_MODE_STTREAM and FLAME_MODE_GLOB + */ +/obj/item/hardpoint/proc/toggle_fire_mode(mob/user) + return + +/** + * Proc that makes the flamer stream work more closely to how the M240 Incinerator is implemented in game + * instead of drawing from an arbitrary ammo pool, it now draws from the chem reagents inside a tank, just like the m240. + */ +/obj/item/hardpoint/proc/fire_flame_stream(atom/target, mob/living/user, max_range, flameshape = FLAMESHAPE_LINE) + //step forward along path so flame starts outside hull + var/list/turfs = get_line(get_origin_turf(), get_turf(target)) + var/turf/origin_turf + for(var/turf/turf as anything in turfs) + if(turf in owner.locs) + continue + origin_turf = turf + break + + var/distance = get_dist(origin_turf, get_turf(target)) + var/fire_amount = min(distance+1, max_range) + + var/datum/reagent/chem = LAZYACCESS(ammo.reagents?.reagent_list, 1) + new /obj/flamer_fire(origin_turf, create_cause_data(initial(name), user), chem, fire_amount, ammo.reagents, flameshape, target, CALLBACK(src, PROC_REF(sync_ammo_from_reagents))) + sync_ammo_from_reagents() + + play_firing_sounds() + + COOLDOWN_START(src, fire_cooldown, fire_delay) + + return AUTOFIRE_CONTINUE + +/** + * Re-syncs ammo.current_rounds to match ammo.reagents.total_volume.sync_ammo_from_reagents() + * Called both right after firing and again once a since reagent consumption happens asynchronously tile by tile + * as the flame propagates, not all at once at the moment of firing. Also refreshes the mounted sprite + * immediately. + */ +/obj/item/hardpoint/proc/sync_ammo_from_reagents() + if(ammo && ammo.reagents) + ammo.current_rounds = round(ammo.reagents.total_volume) + owner?.update_icon() + /// Start cooldown to respect delay of firemode. /obj/item/hardpoint/proc/set_fire_cooldown(firemode) var/cooldown_time = 0 @@ -622,6 +898,10 @@ /// Adjust target based on random scatter angle. /obj/item/hardpoint/proc/simulate_scatter(obj/projectile/projectile_to_fire, atom/target, turf/curloc, turf/targloc) + // without this, clicking a point-blank target that resolves to the same turf as the muzzle + if(curloc == targloc) + return target + var/fire_angle = Get_Angle(curloc, targloc) var/total_scatter_angle = projectile_to_fire.scatter @@ -630,6 +910,9 @@ fire_angle += rand(-total_scatter_angle, total_scatter_angle) target = get_angle_target_turf(curloc, fire_angle, 30) + if(curloc.z != targloc.z) + target = locate(target.x, target.y, targloc.z) + return target /// Get turf at hardpoint origin offset, used as the muzzle. @@ -649,7 +932,14 @@ /// Determines whether something is in firing arc of a hardpoint. /obj/item/hardpoint/proc/in_firing_arc(atom/target) - if(!firing_arc || !ISINRANGE_EX(firing_arc, 0, 360)) + if(self_gimballed) + return check_gimbal_firing_arc(target) + + var/obj/item/hardpoint/holder/tank_turret/turret = loc + if(istype(turret)) + return turret.in_turret_firing_arc(src, target) + + if(!traverse_arc || !ISINRANGE_EX(traverse_arc, 0, 360)) return TRUE var/turf/muzzle_turf = get_origin_turf() @@ -665,7 +955,247 @@ else if(angle_diff > 180) angle_diff -= 360 - return abs(angle_diff) <= (firing_arc * 0.5) + return abs(angle_diff) <= (traverse_arc * 0.5) + +/** + * Firing-arc gate for a self-gimballed hardpoint (see toggle_slaved_to_driver()): current_angle + * must have caught up to within FIRING_GATE_TOLERANCE of the angle from the weapon's own muzzle to + * target. Same shape as /obj/item/hardpoint/holder/tank_turret/proc/in_turret_firing_arc(), but + * measured against this hardpoint's own current_angle instead of a parent turret's. + */ +/obj/item/hardpoint/proc/check_gimbal_firing_arc(atom/target) + var/turf/muzzle_turf = get_origin_turf() + var/turf/target_turf = get_turf(target) + + //same tile angle is undefined for Gett_Angle, returning FALSE to match the legacy static-arc check + if(muzzle_turf == target_turf) + return FALSE + + var/target_angle = Get_Angle(muzzle_turf, target_turf) + return abs(angle_delta(target_angle, current_angle)) <= FIRING_GATE_TOLERANCE + +/** + * records where the mouse currently wants this hardpoint to face. Called on every processed + * mouse-move while a relevant crew member is seated. + * + * A self_gimballed weapon's desired_angle gets clamped to within SLAVED_GIMBAL_ARC_HALF_WIDTH + * degrees of the turret's own current continuous facing. the turret can sit anywhere within its own lean, e.g. 168 + * degrees, and the allowed swing needs to follow that actual angle, not snap to whichever cardinal it's nearest to. + */ +/obj/item/hardpoint/proc/update_desired_angle(atom/object, mob/user, params) + if(health <= 0) + return + if(aim_locked) // a track_and_charge() lock-on is in progress - mouse input can't fight it + return + if(world.time == last_desired_update_time) // collapses same-tick MouseMove spam into one update + return + last_desired_update_time = world.time + + // Aim at the hovered mob/object itself, not its bare turf + var/atom/aim_target + if(ismob(object) || isobj(object)) + aim_target = object + else + aim_target = get_turf_on_clickcatcher(object, user, params) + if(!aim_target) + return + var/turf/target_turf = get_turf(aim_target) + var/turf/origin_turf = get_origin_turf() + if(!origin_turf || target_turf == origin_turf) + return + + // Using the raw pixel-precise angle here aims at a tall sprite's visual midpoint instead of the tile it's + // actually standing on, which is what actually needs to be hit. very shittty bug to fix during testing + // which made the autocannon and minigun DPS plummet. + desired_angle = Get_Angle_Grounded(origin_turf, aim_target) + desired_angle = Get_Angle_Grounded(origin_turf, target_turf) + + if(self_gimballed) + var/obj/item/hardpoint/holder/tank_turret/turret = loc + if(istype(turret)) + var/turret_facing = turret.current_angle + var/swing = clamp(angle_delta(desired_angle, turret_facing), -SLAVED_GIMBAL_ARC_HALF_WIDTH, SLAVED_GIMBAL_ARC_HALF_WIDTH) + desired_angle = ((turret_facing + swing) % 360 + 360) % 360 + + start_rotation_if_needed() + +/obj/item/hardpoint/proc/start_rotation_if_needed() + if(rotation_active) + return + rotation_active = TRUE + spawn(0) + rotation_loop() + +/** + * Ticks current_angle toward desired_angle with inertia (accelerating up to max_angular_velocity, + * decelerating on approach). Self-terminates once current_angle settles within ROTATION_SETTLE_TOLERANCE of + * desired_angle, and restarts on demand from update_desired_angle(). + */ +/obj/item/hardpoint/proc/rotation_loop() + while(TRUE) + sleep(1) + + if(QDELETED(src) || health <= 0) + angular_velocity = 0 + rotation_active = FALSE + return + + var/delta = angle_delta(desired_angle, current_angle) + if(abs(delta) < ROTATION_SETTLE_TOLERANCE) + current_angle = desired_angle + angular_velocity = 0 + drag_self_gimballed_weapons(delta) + apply_current_angle() + rotation_active = FALSE + return + + // Accelerate toward max_angular_velocity, decelerate once we're close enough to stop in time. + var/stopping_distance = (angular_velocity ** 2) / (2 * angular_accel) + if(abs(delta) <= stopping_distance) + angular_velocity = max(angular_velocity - angular_accel, 0) + else + angular_velocity = min(angular_velocity + angular_accel, max_angular_velocity) + + var/turn_sign = (delta > 0) - (delta < 0) + var/step = min(angular_velocity, abs(delta)) * turn_sign + current_angle = ((current_angle + step) % 360 + 360) % 360 + drag_self_gimballed_weapons(step) + apply_current_angle() + +/** + * No-op by default. /obj/item/hardpoint/holder/tank_turret overrides this to drag every + * self_gimballed mounted weapon's own current_angle/desired_angle along by the same delta this + * hardpoint's current_angle just moved by. + * + * Without this, a slaved weapon's current_angle stays a fixed absolute world angle. + * This would be fine, but, since we can slave the secondaries to the driver, then they also need to drag by themselves. + */ +/obj/item/hardpoint/proc/drag_self_gimballed_weapons(delta) + return + +/// Snaps a continuous angle down to the nearest 90-degree cardinal quadrant +/obj/item/hardpoint/proc/angle_to_cardinal(angle) + angle = ((angle % 360) + 360) % 360 + if(angle >= 315 || angle < 45) + return NORTH + if(angle < 135) + return EAST + if(angle < 225) + return SOUTH + return WEST + +/** + * Keeps dir/origins in sync with current_angle's quadrant, then refreshes the vehicle's sprite. + */ +/obj/item/hardpoint/proc/apply_current_angle() + if(owner) + owner.update_icon() + +/** + * Builds a matrix that rotates by tilt degrees around pivot (a local x,y point relative to the + * icon's own center) instead of the icon's default center. pivot defaults to (0,0) if null/unset. + * + * (Translate(pivot), called last, applied last) - Translate(-pivot) -> Turn(tilt) -> Translate(pivot). + */ +/obj/item/hardpoint/proc/build_tilt_matrix(tilt, list/pivot) + var/matrix/tilt_matrix = matrix() + if(pivot && (pivot[1] || pivot[2])) + tilt_matrix.Translate(-pivot[1], -pivot[2]) + tilt_matrix.Turn(tilt) + tilt_matrix.Translate(pivot[1], pivot[2]) + else + tilt_matrix.Turn(tilt) + return tilt_matrix + +/** + * Like build_tilt_matrix(), but composes two nested rotations onto one matrix instead of one: + * inner_tilt around inner_pivot is applied first (closest to the raw icon), then outter_tilt around + * outer_pivot is applied on top of that, carrying the already-tilted icon along with it. Used to + * render a self_gimballed weapon, its own idependent swivel (inner, pivoting around its own local + * mount joint, gimbal_pivot) rides along with the turret's own lean (outer, pivoting around the + * turret's own rotation center, rotation_pivot) instead of replacing it - two separate pivots of + * rotation, one nested inside the other, same as a real two-stage gimbal mountted on a turret. + */ +/obj/item/hardpoint/proc/build_nested_tilt_matrix(inner_tilt, list/inner_pivot, outer_tilt, list/outer_pivot) + var/matrix/tilt_matrix = build_tilt_matrix(inner_tilt, inner_pivot) + if(outer_tilt) + if(outer_pivot && (outer_pivot[1] || outer_pivot[2])) + tilt_matrix.Translate(-outer_pivot[1], -outer_pivot[2]) + tilt_matrix.Turn(outer_tilt) + tilt_matrix.Translate(outer_pivot[1], outer_pivot[2]) + else + tilt_matrix.Turn(outer_tilt) + return tilt_matrix + +/** + * Looks up the pivot for a given angle out of up to 8 keyed keyframes (dir constants, same + * convention as dir2angle()), blending between a cardinal and an adjacent diagonal if that + * diagonal has a tuned entry - otherwise hard-snaps to the cardinal (old angle_to_cardinal() + * behavior), so untuned weapons render unchanged. Returns null if pivot_data is empty. + * + * Uses a direct equality switch rather than angle_to_dir() that proc rounds to the NEARESTT dir + * and misresolves exact cardinal angles (0/90/180/270) to an adjacent diagonal, which a multiple of 45 avoids + */ +/obj/item/hardpoint/proc/interpolate_pivot(list/pivot_data, angle) + if(!pivot_data) + return null + + angle = ((angle % 360) + 360) % 360 + var/lower_anchor = FLOOR(angle, 45) + var/upper_anchor = (lower_anchor + 45) % 360 + + var/list/lower_pivot = LAZYACCESS(pivot_data, "[anchor_angle_to_dir(lower_anchor)]") + var/list/upper_pivot = LAZYACCESS(pivot_data, "[anchor_angle_to_dir(upper_anchor)]") + + if(!lower_pivot) + return upper_pivot + if(!upper_pivot) + return lower_pivot + + var/frac = (angle - lower_anchor) / 45 + return list( + lower_pivot[1] + (upper_pivot[1] - lower_pivot[1]) * frac, + lower_pivot[2] + (upper_pivot[2] - lower_pivot[2]) * frac, + ) + +/// Exact reverse of dir2angle() for one of the 8 cardinal/diagonal 45-degree-multiple angles - see interpolate_pivot()'s doc comment for why this can't just reuse the global angle_to_dir() nearest-neighbor helper. +/obj/item/hardpoint/proc/anchor_angle_to_dir(angle) + switch(angle) + if(0) + return NORTH + if(45) + return NORTHEAST + if(90) + return EAST + if(135) + return SOUTHEAST + if(180) + return SOUTH + if(225) + return SOUTHWEST + if(270) + return WEST + if(315) + return NORTHWEST + return NORTH + +/** + * Redirects a target to preserve its range from origin_turf but along this hardpoint's actual + * current facing (current_angle) instead of the originally-aimed direction. Used by held-ttrigger + * fire modes (see try_fire()), which don't wait for the turret (or a self-gimballed weapon's own gimbal) to finish swinging onto target. + */ +/obj/item/hardpoint/proc/redirect_to_current_facing(turf/origin_turf, atom/target) + var/turf/target_turf = get_turf(target) + if(!origin_turf || !target_turf || origin_turf == target_turf) + return target + + // Once the barrel has fully caught up to its own tracked aim point, there's no + // rotational lag left to simulate. fire directly at the target. + if(abs(angle_delta(desired_angle, current_angle)) <= FIRING_GATE_TOLERANCE) + return target + + var/range = get_dist_euclidian(origin_turf, target_turf) + return get_angle_target_turf(origin_turf, current_angle, range) //----------------------------- //------ICON PROCS---------- diff --git a/code/modules/vehicles/hardpoints/hardpoint_ammo/flare_ammo.dm b/code/modules/vehicles/hardpoints/hardpoint_ammo/flare_ammo.dm new file mode 100644 index 000000000000..099fef47c49e --- /dev/null +++ b/code/modules/vehicles/hardpoints/hardpoint_ammo/flare_ammo.dm @@ -0,0 +1,13 @@ +/obj/item/ammo_magazine/hardpoint/turret_flare + name = "Turret Flare Screen Magazine" + desc = "The turret's internal star shell holders. Loaded directly with individual M74 AGM-S star shells or star shell packets, not swapped as a whole magazine." + caliber = "flare" + icon = 'icons/obj/items/weapons/guns/ammo_by_faction/USCM/vehicles.dmi' + icon_state = "slauncher_1" + w_class = SIZE_LARGE + default_ammo = /datum/ammo/flare/starshell/burst + max_rounds = 6 + gun_type = /obj/item/hardpoint/holder/tank_turret + +/obj/item/ammo_magazine/hardpoint/turret_flare/update_icon() + icon_state = "slauncher_[current_rounds <= 0 ? "0" : "1"]" diff --git a/code/modules/vehicles/hardpoints/hardpoint_ammo/ltb_ammo.dm b/code/modules/vehicles/hardpoints/hardpoint_ammo/ltb_ammo.dm index ee6de433eb36..aa7b0803dad9 100644 --- a/code/modules/vehicles/hardpoints/hardpoint_ammo/ltb_ammo.dm +++ b/code/modules/vehicles/hardpoints/hardpoint_ammo/ltb_ammo.dm @@ -11,3 +11,13 @@ /obj/item/ammo_magazine/hardpoint/ltb_cannon/update_icon() icon_state = "ltbcannon_[current_rounds]" + +// Single-round alternative magazine, still istype()-matched against the hardpoint's cached ammo_type +/obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell + name = "86x455mm HE Shell" + desc = "A single high-explosive 86x455mm shell for the LTB Cannon." + icon_state = "lbtshell_1" + max_rounds = 1 + +/obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell/update_icon() + icon_state = "lbtshell_[current_rounds]" diff --git a/code/modules/vehicles/hardpoints/hardpoint_ammo/primary_flamer_ammo.dm b/code/modules/vehicles/hardpoints/hardpoint_ammo/primary_flamer_ammo.dm index 8384b3f6d94e..1c596f54c207 100644 --- a/code/modules/vehicles/hardpoints/hardpoint_ammo/primary_flamer_ammo.dm +++ b/code/modules/vehicles/hardpoints/hardpoint_ammo/primary_flamer_ammo.dm @@ -5,12 +5,103 @@ icon = 'icons/obj/items/weapons/guns/ammo_by_faction/USCM/vehicles.dmi' icon_state = "drgn_flametank" w_class = SIZE_LARGE - max_rounds = 60 + max_rounds = 300 gun_type = /obj/item/hardpoint/primary/flamer default_ammo = /datum/ammo/flamethrower/tank_flamer + // Reagent id this tank starts filled with + var/flamer_chem = "highdamagenapalm" +/obj/item/ammo_magazine/hardpoint/primary_flamer/Initialize(mapload, ...) + . = ..() + create_reagents(max_rounds) + if(flamer_chem) + reagents.add_reagent(flamer_chem, max_rounds) + current_rounds = round(reagents.total_volume) + update_icon() + +/** + * Refuels this tank from a fuel source, exactly mirroring /obj/item/ammo_magazine/flamer_tank/afterattack() + * + * also follows the same rules regarding what fuel types are or aren't allowed. + */ +/obj/item/ammo_magazine/hardpoint/primary_flamer/afterattack(obj/target, mob/user, flag) + if(get_dist(user, target) > 1) + return ..() + if(!istype(target, /obj/structure/reagent_dispensers/tank/fuel) && !istype(target, /obj/item/tool/weldpack) && !istype(target, /obj/item/storage/backpack/marine/engineerpack) && !istype(target, /obj/item/ammo_magazine/hardpoint/primary_flamer)) + return ..() + + if(!target.reagents || length(target.reagents.reagent_list) < 1) + to_chat(user, SPAN_WARNING("[target] is empty!")) + return + + if(!reagents) + create_reagents(max_rounds) + + var/datum/reagent/to_add = target.reagents.reagent_list[1] + + if(!istype(to_add) || (length(reagents.reagent_list) && flamer_chem != to_add.id) || length(target.reagents.reagent_list) > 1) + to_chat(user, SPAN_WARNING("You can't mix fuel mixtures!")) + return + + if(!to_add.intensityfire && to_add.id != "stablefoam") + to_chat(user, SPAN_WARNING("This chemical is not potent enough to be used in a flamethrower!")) + return + + var/fuel_amt_to_remove = clamp(to_add.volume, 0, max_rounds - reagents.get_reagent_amount(to_add.id)) + if(!fuel_amt_to_remove) + if(!max_rounds) + to_chat(user, SPAN_WARNING("[target] is empty!")) + return + + target.reagents.remove_reagent(to_add.id, fuel_amt_to_remove) + reagents.add_reagent(to_add.id, fuel_amt_to_remove) + playsound(loc, 'sound/effects/refill.ogg', 25, 1, 3) + caliber = to_add.name + flamer_chem = to_add.id + current_rounds = round(reagents.total_volume) + + to_chat(user, SPAN_NOTICE("You refill [src] with [caliber].")) + update_icon() + + +// Colored fuel strip overlay /obj/item/ammo_magazine/hardpoint/primary_flamer/update_icon() + overlays.Cut() if(current_rounds > 0) icon_state = "drgn_flametank" + var/image/strip = image(icon, icon_state = "drgn_flametank_strip") + if(reagents) + strip.color = mix_color_from_reagents(reagents.reagent_list) + overlays += strip else icon_state = "drgn_flametank_empty" + +// Lists loaded fuel by name/volume on examine +/obj/item/ammo_magazine/hardpoint/primary_flamer/get_examine_text(mob/user) + . = ..() + . += SPAN_NOTICE("It contains:") + if(reagents && length(reagents.reagent_list)) + for(var/datum/reagent/reagent in reagents.reagent_list) + . += SPAN_NOTICE(" [reagent.volume] units of [reagent.name].") + else + . += SPAN_NOTICE(" Nothing.") + +// "Empty canister" verb, also found in the m240 flamer. +/obj/item/ammo_magazine/hardpoint/primary_flamer/verb/remove_reagents() + set name = "Empty canister" + set category = "Object" + + set src in usr + + if(usr.get_active_hand() != src) + return + + if(alert(usr, "Do you really want to empty out [src]?", "Empty canister", "Yes", "No") != "Yes") + return + + reagents.clear_reagents() + current_rounds = round(reagents.total_volume) + + playsound(loc, 'sound/effects/refill.ogg', 25, 1, 3) + to_chat(usr, SPAN_NOTICE("You empty out [src]")) + update_icon() diff --git a/code/modules/vehicles/hardpoints/hardpoint_ammo/secondary_flamer_ammo.dm b/code/modules/vehicles/hardpoints/hardpoint_ammo/secondary_flamer_ammo.dm index 4a09046c9678..d2d3cb34a872 100644 --- a/code/modules/vehicles/hardpoints/hardpoint_ammo/secondary_flamer_ammo.dm +++ b/code/modules/vehicles/hardpoints/hardpoint_ammo/secondary_flamer_ammo.dm @@ -7,3 +7,97 @@ w_class = SIZE_LARGE max_rounds = 150 gun_type = /obj/item/hardpoint/secondary/small_flamer + // Only used when the hardpoint is toggled to FLAME_MODE_GLOB + default_ammo = /datum/ammo/flamethrower/tank_flamer_secondary + // Reagent id this tank starts filled with + var/flamer_chem = "napalm" + +/obj/item/ammo_magazine/hardpoint/secondary_flamer/Initialize(mapload, ...) + . = ..() + create_reagents(max_rounds) + if(flamer_chem) + reagents.add_reagent(flamer_chem, max_rounds) + current_rounds = round(reagents.total_volume) + update_icon() + +// Colored fuel strip overlay +/obj/item/ammo_magazine/hardpoint/secondary_flamer/update_icon() + overlays.Cut() + var/image/strip = image(icon, icon_state = "flametank_large_custom_strip") + if(reagents && length(reagents.reagent_list)) + strip.color = mix_color_from_reagents(reagents.reagent_list) + overlays += strip + +/** + * Refuels this tank from a fuel source, exactly mirroring /obj/item/ammo_magazine/flamer_tank/afterattack() + * + * also follows the same rules regarding what fuel types are or aren't allowed. + */ +/obj/item/ammo_magazine/hardpoint/secondary_flamer/afterattack(obj/target, mob/user, flag) + if(get_dist(user, target) > 1) + return ..() + if(!istype(target, /obj/structure/reagent_dispensers/tank/fuel) && !istype(target, /obj/item/tool/weldpack) && !istype(target, /obj/item/storage/backpack/marine/engineerpack) && !istype(target, /obj/item/ammo_magazine/hardpoint/secondary_flamer)) + return ..() + + if(!target.reagents || length(target.reagents.reagent_list) < 1) + to_chat(user, SPAN_WARNING("[target] is empty!")) + return + + if(!reagents) + create_reagents(max_rounds) + + var/datum/reagent/to_add = target.reagents.reagent_list[1] + + if(!istype(to_add) || (length(reagents.reagent_list) && flamer_chem != to_add.id) || length(target.reagents.reagent_list) > 1) + to_chat(user, SPAN_WARNING("You can't mix fuel mixtures!")) + return + + if(!to_add.intensityfire && to_add.id != "stablefoam") + to_chat(user, SPAN_WARNING("This chemical is not potent enough to be used in a flamethrower!")) + return + + var/fuel_amt_to_remove = clamp(to_add.volume, 0, max_rounds - reagents.get_reagent_amount(to_add.id)) + if(!fuel_amt_to_remove) + if(!max_rounds) + to_chat(user, SPAN_WARNING("[target] is empty!")) + return + + target.reagents.remove_reagent(to_add.id, fuel_amt_to_remove) + reagents.add_reagent(to_add.id, fuel_amt_to_remove) + playsound(loc, 'sound/effects/refill.ogg', 25, 1, 3) + caliber = to_add.name + flamer_chem = to_add.id + current_rounds = round(reagents.total_volume) + + to_chat(user, SPAN_NOTICE("You refill [src] with [caliber].")) + update_icon() + +// Lists loaded fuel by name/volume when examining +/obj/item/ammo_magazine/hardpoint/secondary_flamer/get_examine_text(mob/user) + . = ..() + . += SPAN_NOTICE("It contains:") + if(reagents && length(reagents.reagent_list)) + for(var/datum/reagent/reagent in reagents.reagent_list) + . += SPAN_NOTICE(" [reagent.volume] units of [reagent.name].") + else + . += SPAN_NOTICE(" Nothing.") + +// "Empty canister" verb, also found in the m240 flamer. +/obj/item/ammo_magazine/hardpoint/secondary_flamer/verb/remove_reagents() + set name = "Empty canister" + set category = "Object" + + set src in usr + + if(usr.get_active_hand() != src) + return + + if(alert(usr, "Do you really want to empty out [src]?", "Empty canister", "Yes", "No") != "Yes") + return + + reagents.clear_reagents() + current_rounds = round(reagents.total_volume) + + playsound(loc, 'sound/effects/refill.ogg', 25, 1, 3) + to_chat(usr, SPAN_NOTICE("You empty out [src]")) + update_icon() diff --git a/code/modules/vehicles/hardpoints/hardpoint_ammo/smoke_ammo.dm b/code/modules/vehicles/hardpoints/hardpoint_ammo/smoke_ammo.dm deleted file mode 100644 index 04846d74c423..000000000000 --- a/code/modules/vehicles/hardpoints/hardpoint_ammo/smoke_ammo.dm +++ /dev/null @@ -1,13 +0,0 @@ -/obj/item/ammo_magazine/hardpoint/turret_smoke - name = "Turret Smoke Screen Magazine" - desc = "A smoke grenades magazine used by tank turret." - caliber = "grenade" - icon = 'icons/obj/items/weapons/guns/ammo_by_faction/USCM/vehicles.dmi' - icon_state = "slauncher_1" - w_class = SIZE_LARGE - default_ammo = /datum/ammo/grenade_container/smoke - max_rounds = 10 - gun_type = /obj/item/hardpoint/holder/tank_turret - -/obj/item/ammo_magazine/hardpoint/turret_smoke/update_icon() - icon_state = "slauncher_[current_rounds <= 0 ? "0" : "1"]" diff --git a/code/modules/vehicles/hardpoints/holder/holder.dm b/code/modules/vehicles/hardpoints/holder/holder.dm index a1797cb68b8b..9fa4cd3a5775 100644 --- a/code/modules/vehicles/hardpoints/holder/holder.dm +++ b/code/modules/vehicles/hardpoints/holder/holder.dm @@ -142,6 +142,11 @@ H.on_uninstall(owner) H.reset_rotation() + + if(H.self_gimballed) + H.self_gimballed = FALSE + H.allowed_seat = initial(H.allowed_seat) + hardpoints -= H H.owner = null @@ -161,7 +166,7 @@ /obj/item/hardpoint/holder/proc/get_hardpoints_with_ammo(seat) var/list/hps = list() for(var/obj/item/hardpoint/H in hardpoints) - if(!H.ammo || seat && seat != H.allowed_seat) + if(!H.ammo_type || seat && seat != H.allowed_seat) continue hps += H return hps diff --git a/code/modules/vehicles/hardpoints/holder/tank_turret.dm b/code/modules/vehicles/hardpoints/holder/tank_turret.dm index edb119cc1327..bc949ec6a396 100644 --- a/code/modules/vehicles/hardpoints/holder/tank_turret.dm +++ b/code/modules/vehicles/hardpoints/holder/tank_turret.dm @@ -1,6 +1,6 @@ /obj/item/hardpoint/holder/tank_turret name = "\improper M34A2-A Multipurpose Turret" - desc = "The centerpiece of the tank. Designed to support quick installation and deinstallation of various tank weapon modules. Has inbuilt smoke screen deployment system." + desc = "The centerpiece of the tank. Designed to support quick installation and deinstallation of various tank weapon modules. Has inbuilt flare launcher." icon = 'icons/obj/vehicles/tank.dmi' icon_state = "tank_turret_0" @@ -14,8 +14,8 @@ activatable = TRUE - ammo = new /obj/item/ammo_magazine/hardpoint/turret_smoke - max_clips = 2 + ammo = new /obj/item/ammo_magazine/hardpoint/turret_flare + max_clips = 0 use_muzzle_flash = FALSE w_class = SIZE_MASSIVE @@ -40,7 +40,9 @@ /obj/item/hardpoint/secondary/small_flamer, /obj/item/hardpoint/secondary/towlauncher, /obj/item/hardpoint/secondary/m56cupola, - /obj/item/hardpoint/secondary/grenade_launcher + /obj/item/hardpoint/secondary/grenade_launcher, + /obj/item/hardpoint/secondary/brute_launcher, + /obj/item/hardpoint/secondary/united_americas_flag ) hdpt_layer = HDPT_LAYER_TURRET @@ -53,19 +55,24 @@ var/gyro = FALSE - // How long the windup is before the turret rotates - var/rotation_windup = 15 - // Used during the windup - var/rotating = FALSE + /// Index into accepted_hardpoints, used by /mob/living/carbon/verb/cycle_tank_weapon_hardpoint() to track cycling progress. + var/debug_cycle_index = 0 scatter = 4 - gun_firemode = GUN_FIREMODE_BURSTFIRE - gun_firemode_list = list( - GUN_FIREMODE_BURSTFIRE, - ) - burst_amount = 2 - burst_delay = 1.0 SECONDS - extra_delay = 13.0 SECONDS + fire_delay = 7.0 SECONDS + + // Total width, in degrees, of the arc in front of the hull that each star shell can land within. + var/flare_spread = 45 + // Minimum/maximum tiles in front of the hull (not the turret's own facing) each star shell can land. + var/flare_range_min = 5 + var/flare_range_max = 7 + +/obj/item/hardpoint/holder/tank_turret/Initialize() + . = ..() + current_angle = dir2angle(dir) + desired_angle = current_angle + // Loaded shell-by-shell rather than swapped as a whole magazine. + ammo_type = null /obj/item/hardpoint/holder/tank_turret/update_icon() var/broken = (health <= 0) @@ -92,11 +99,53 @@ return I +/** + * Layers a continuous visual tilt on top of the turret's (and every mounted weapon's) cardinal + * sprite. The base dir still snaps every 90 degrees for sprite selection. + */ +/obj/item/hardpoint/holder/tank_turret/get_hardpoint_image() + var/list/images = ..() + + var/tilt = angle_delta(current_angle, dir2angle(dir)) + + var/image/turret_image = images[1] + if(turret_image && tilt) + turret_image.transform = build_tilt_matrix(tilt, interpolate_pivot(rotation_pivot, current_angle)) + + var/image_index = 2 + for(var/obj/item/hardpoint/weapon in hardpoints) + var/image/weapon_image = images[image_index] + image_index++ + if(!weapon_image) + continue + + // A self_gimballed weapon needs TWO nested rotations around TWO genuinely different pivots. + // Collapsing these into one combined rotation only works if both + // pivots happen to coincide, which isn't the case. + if(weapon.self_gimballed) + var/own_tilt = angle_delta(weapon.current_angle, current_angle) + if(own_tilt || tilt) + var/list/inner_pivot = weapon.interpolate_pivot(weapon.gimbal_pivot, current_angle) || weapon.interpolate_pivot(weapon.rotation_pivot, current_angle) + var/list/outer_pivot = weapon.interpolate_pivot(weapon.rotation_pivot, current_angle) + weapon_image.transform = weapon.build_nested_tilt_matrix(own_tilt, inner_pivot, tilt, outer_pivot) + else if(tilt) + weapon_image.transform = build_tilt_matrix(tilt, weapon.interpolate_pivot(weapon.rotation_pivot, current_angle)) + + return images + // no picking this big beast up /obj/item/hardpoint/holder/tank_turret/attack_hand(mob/user) return /obj/item/hardpoint/holder/tank_turret/attackby(obj/item/I, mob/user) + if(istype(I, /obj/item/explosive/grenade/high_explosive/airburst/starshell)) + load_star_shell(I, user) + return TRUE + + if(istype(I, /obj/item/storage/box/packet/flare)) + load_star_shell_packet(I, user) + return TRUE + if(istype(I, /obj/item/powerloader_clamp)) var/obj/item/powerloader_clamp/PC = I if(!PC.linked_powerloader) @@ -113,16 +162,53 @@ return TRUE ..() +// Loads a single loose star shell directly into the turret's internal holders. +/obj/item/hardpoint/holder/tank_turret/proc/load_star_shell(obj/item/explosive/grenade/high_explosive/airburst/starshell/shell, mob/user) + if(shell.active) + to_chat(user, SPAN_WARNING("You can't load a primed star shell into \the [src]!")) + return + if(ammo.current_rounds >= ammo.max_rounds) + to_chat(user, SPAN_WARNING("\The [src]'s shell holders are already full.")) + return + user.visible_message(SPAN_NOTICE("[user] loads \a [shell] into \the [src]."), SPAN_NOTICE("You load \a [shell] into \the [src]'s shell holders.")) + playsound(loc, 'sound/weapons/gun_shotgun_shell_insert.ogg', 25, TRUE) + ammo.current_rounds++ + qdel(shell) + +// Loads as many star shells as fit out of a star shell packet into the turret's internal holders. +/obj/item/hardpoint/holder/tank_turret/proc/load_star_shell_packet(obj/item/storage/box/packet/flare/packet, mob/user) + var/space_left = ammo.max_rounds - ammo.current_rounds + if(space_left <= 0) + to_chat(user, SPAN_WARNING("\The [src]'s shell holders are already full.")) + return + + var/list/shells_to_load = list() + for(var/obj/item/explosive/grenade/high_explosive/airburst/starshell/shell in packet.contents) + shells_to_load += shell + if(length(shells_to_load) >= space_left) + break + + if(!length(shells_to_load)) + to_chat(user, SPAN_WARNING("\The [packet] has no star shells left to load.")) + return + + for(var/obj/item/explosive/grenade/high_explosive/airburst/starshell/shell as anything in shells_to_load) + qdel(shell) + + ammo.current_rounds += length(shells_to_load) + packet.update_icon() + user.visible_message(SPAN_NOTICE("[user] loads [length(shells_to_load)] star shell\s from \the [packet] into \the [src]."), SPAN_NOTICE("You load [length(shells_to_load)] star shell\s from \the [packet] into \the [src]'s shell holders.")) + playsound(loc, 'sound/weapons/gun_shotgun_shell_insert.ogg', 25, TRUE) /obj/item/hardpoint/holder/tank_turret/get_tgui_info() var/list/data = list() - data += list(list( // turret smokescreen data - "name" = "M34A2-A Turret Smoke Screen", + data += list(list( // turret flare launcher data + "name" = "M34A2-A Turret Flare Launcher", "health" = health <= 0 ? null : floor(get_integrity_percent()), "uses_ammo" = TRUE, - "current_rounds" = ammo.current_rounds / 2, - "max_rounds"= ammo.max_rounds / 2, + "current_rounds" = ammo.current_rounds, + "max_rounds"= ammo.max_rounds, "mags" = LAZYLEN(backup_clips), "max_mags" = max_clips, )) @@ -141,74 +227,94 @@ gyro = !gyro to_chat(user, SPAN_NOTICE("You toggle \the [src]'s gyroscopic stabilizer [gyro ? "ON" :"OFF"].")) -/obj/item/hardpoint/holder/tank_turret/proc/user_rotation(mob/user, deg) - // no rotating a broken turret - if(health <= 0) - return +/// Keeps dir/origins in sync with current_angle's quadrant, then refreshes the vehicle's sprite. +/obj/item/hardpoint/holder/tank_turret/apply_current_angle() + var/target_cardinal = angle_to_cardinal(current_angle) + if(target_cardinal != dir) + rotate(turning_angle(dir, target_cardinal), override_gyro = TRUE, sync_angle = FALSE) + owner.update_icon() - if(rotating) +// See base proc's doc comment (hardpoint.dm) - drags any self_gimballed mounted weapon's angle along with this turret's own, so it keeps riding the turret's motion instead of holding a fixed absolute world angle. +/obj/item/hardpoint/holder/tank_turret/drag_self_gimballed_weapons(delta) + if(!delta) + return + for(var/obj/item/hardpoint/mounted_weapon in hardpoints) + if(!mounted_weapon.self_gimballed) + continue + mounted_weapon.current_angle = ((mounted_weapon.current_angle + delta) % 360 + 360) % 360 + mounted_weapon.desired_angle = ((mounted_weapon.desired_angle + delta) % 360 + 360) % 360 + +/** + * Gates firing for a weapon mounted on this turret. current_angle must have caught up to within + * FIRING_GATE_TOLERANCE of the angle from tthe weapon's own muzzle to target. + */ +/obj/item/hardpoint/holder/tank_turret/proc/in_turret_firing_arc(obj/item/hardpoint/weapon, atom/target) + var/turf/muzzle_turf = weapon.get_origin_turf() + var/turf/target_turf = get_turf(target) + + //same tile angle is undefined for Get_Angle, returning FALSE to match the legacy static-arc check + if(muzzle_turf == target_turf) + return FALSE + + var/target_angle = Get_Angle(muzzle_turf, target_turf) + return abs(angle_delta(target_angle, current_angle)) <= FIRING_GATE_TOLERANCE + +// Recomputes max_angular_velocity from the median traverse_arc of currently-mounted weapons. +/obj/item/hardpoint/holder/tank_turret/proc/recalculate_turn_rate() + var/list/arcs = list() + for(var/obj/item/hardpoint/mounted_weapon in hardpoints) + if(!mounted_weapon.traverse_arc) + continue + arcs += mounted_weapon.traverse_arc + + if(!length(arcs)) + max_angular_velocity = TURRET_DEFAULT_ANGULAR_VELOCITY return - rotating = TRUE - to_chat(user, SPAN_NOTICE("You begin rotating the turret towards the [dir2text(turn(dir,deg))].")) + sortTim(arcs) + var/weapon_count = length(arcs) + var/median_arc = (weapon_count % 2) ? arcs[(weapon_count + 1) / 2] : (arcs[weapon_count / 2] + arcs[weapon_count / 2 + 1]) / 2 - if(!do_after(user, rotation_windup, INTERRUPT_ALL, BUSY_ICON_GENERIC)) - rotating = FALSE - return - rotating = FALSE + max_angular_velocity = max(TURRET_BASE_ANGULAR_VELOCITY * (median_arc / TURRET_ARC_NORMALIZATION), TURRET_MIN_ANGULAR_VELOCITY) + +/obj/item/hardpoint/holder/tank_turret/add_hardpoint(obj/item/hardpoint/new_hardpoint) + . = ..() + recalculate_turn_rate() - rotate(deg, TRUE) +/obj/item/hardpoint/holder/tank_turret/remove_hardpoint(obj/item/hardpoint/removed_hardpoint, turf/uninstall_to) + . = ..() + recalculate_turn_rate() -/obj/item/hardpoint/holder/tank_turret/rotate(deg, override_gyro = FALSE) +/** + * override_gyro - bypasses the gyro lock (used by mouse-driven aiming, which should always work). + * sync_angle - whether to drag current_angle along by deg. TRUE for hull-turn cascades (rotate_hardpoints), + * FALSE when apply_current_angle() calls this to snap dir to a quadrant it already accounted for. + */ +/obj/item/hardpoint/holder/tank_turret/rotate(deg, override_gyro = FALSE, sync_angle = TRUE) if(gyro && !override_gyro) return ..(deg) + if(sync_angle) + // deg is in turn()'s rotational convention, which is the opposite + // sign of dir2angle()'s north-clockwise convention that current_angle is tracked in + // subtract, don't add, or current_angle drifts away from the direction dir actually turned. + current_angle = ((current_angle - deg) % 360 + 360) % 360 + drag_self_gimballed_weapons(-deg) + var/obj/vehicle/multitile/tank/C = owner var/obj/item/hardpoint/support/artillery_module/AM for(var/obj/item/hardpoint/support/artillery_module/A in C.hardpoints) AM = A - if(AM && AM.is_active) - var/mob/user = C.seats[VEHICLE_GUNNER] - if(user && user.client) - user = C.seats[VEHICLE_GUNNER] - user.client.change_view(AM.view_buff, src) - - switch(dir) - if(NORTH) - user.client.set_pixel_x(0) - user.client.set_pixel_y(AM.view_tile_offset * 32) - if(SOUTH) - user.client.set_pixel_x(0) - user.client.set_pixel_y(-1 * AM.view_tile_offset * 32) - if(EAST) - user.client.set_pixel_x(AM.view_tile_offset * 32) - user.client.set_pixel_y(0) - if(WEST) - user.client.set_pixel_x(-1 * AM.view_tile_offset * 32) - user.client.set_pixel_y(0) + if(AM) + var/mob/living/user = C.seats[VEHICLE_GUNNER] + if(user && (user in AM.optics_users)) + AM.apply_gunner_view(user) +// lobs a single starshell forwards /obj/item/hardpoint/holder/tank_turret/try_fire(atom/target, mob/living/user, params) - var/turf/L - var/turf/R - switch(owner.dir) - if(NORTH) - L = locate(owner.x - 2, owner.y + 4, owner.z) - R = locate(owner.x + 2, owner.y + 4, owner.z) - if(SOUTH) - L = locate(owner.x + 2, owner.y - 4, owner.z) - R = locate(owner.x - 2, owner.y - 4, owner.z) - if(EAST) - L = locate(owner.x + 4, owner.y + 2, owner.z) - R = locate(owner.x + 4, owner.y - 2, owner.z) - else - L = locate(owner.x - 4, owner.y + 2, owner.z) - R = locate(owner.x - 4, owner.y - 2, owner.z) - - if(shots_fired) - target = R - else - target = L - + var/fire_angle = dir2angle(owner.dir) + rand(-flare_spread * 0.5, flare_spread * 0.5) + var/fire_range = rand(flare_range_min, flare_range_max) + target = get_angle_target_turf(owner, fire_angle, fire_range) return ..() diff --git a/code/modules/vehicles/hardpoints/primary/autocannon.dm b/code/modules/vehicles/hardpoints/primary/autocannon.dm index 8148e5357dd8..863c7eef5c17 100644 --- a/code/modules/vehicles/hardpoints/primary/autocannon.dm +++ b/code/modules/vehicles/hardpoints/primary/autocannon.dm @@ -8,7 +8,7 @@ activation_sounds = list('sound/weapons/vehicles/autocannon_fire.ogg') health = 500 - firing_arc = 60 + traverse_arc = 60 ammo = new /obj/item/ammo_magazine/hardpoint/ace_autocannon max_clips = 2 @@ -19,10 +19,16 @@ "4" = list(32, 0), "8" = list(-32, 0) ) + rotation_pivot = list( + "1" = list(0, -22), + "2" = list(0, 32), + "4" = list(-32, 0), + "8" = list(32, 0) + ) scatter = 1 gun_firemode = GUN_FIREMODE_AUTOMATIC gun_firemode_list = list( GUN_FIREMODE_AUTOMATIC, ) - fire_delay = 0.7 SECONDS + fire_delay = 1.4 SECONDS diff --git a/code/modules/vehicles/hardpoints/primary/dual_cannon.dm b/code/modules/vehicles/hardpoints/primary/dual_cannon.dm index 9f4c7286afeb..33e483721644 100644 --- a/code/modules/vehicles/hardpoints/primary/dual_cannon.dm +++ b/code/modules/vehicles/hardpoints/primary/dual_cannon.dm @@ -12,7 +12,7 @@ damage_multiplier = 0.2 health = 500 - firing_arc = 60 + traverse_arc = 60 origins = list(0, 1) diff --git a/code/modules/vehicles/hardpoints/primary/flamer.dm b/code/modules/vehicles/hardpoints/primary/flamer.dm index 60a7979cd5fb..e541d6dc643c 100644 --- a/code/modules/vehicles/hardpoints/primary/flamer.dm +++ b/code/modules/vehicles/hardpoints/primary/flamer.dm @@ -8,23 +8,37 @@ activation_sounds = list('sound/weapons/vehicles/flamethrower.ogg') health = 400 - firing_arc = 90 + traverse_arc = 90 ammo = new /obj/item/ammo_magazine/hardpoint/primary_flamer max_clips = 1 px_offsets = list( - "1" = list(0, 21), - "2" = list(0, -32), + "1" = list(0, 27), + "2" = list(0, -26), "4" = list(32, 1), "8" = list(-32, 1) ) + rotation_pivot = list( + "1" = list(0, -27), + "2" = list(0, 26), + "4" = list(-32, -1), + "8" = list(32, -1) + ) use_muzzle_flash = FALSE scatter = 5 fire_delay = 2.0 SECONDS + // Range for FLAME_MODE_STREAM + var/max_range = 8 + var/flame_mode = FLAME_MODE_GLOB + // Flat fuel cost for a single FLAME_MODE_GLOB shot + var/glob_fuel_cost = 5 + // The reagent-based ammo/fuel deduction is done manually in handle_fire() below + ammo_cost_per_shot = 0 + /obj/item/hardpoint/primary/flamer/set_bullet_traits() ..() LAZYADD(traits_to_give, list( @@ -37,3 +51,39 @@ return NONE return ..() + +// Toggles between FLAME_MODE_GLOB and FLAME_MODE_STREAM +/obj/item/hardpoint/primary/flamer/toggle_fire_mode(mob/user) + flame_mode = (flame_mode == FLAME_MODE_STREAM) ? FLAME_MODE_GLOB : FLAME_MODE_STREAM + to_chat(user, SPAN_NOTICE("You switch \the [src] to [flame_mode == FLAME_MODE_STREAM ? "stream" : "glob shot"] mode.")) + +/obj/item/hardpoint/primary/flamer/handle_fire(atom/target, mob/living/user, params) + if(flame_mode == FLAME_MODE_STREAM) + return fire_flame_stream(target, user, max_range, FLAMESHAPE_TRIANGLE) + + var/datum/reagent/chem = LAZYACCESS(ammo.reagents?.reagent_list, 1) + if(!chem || ammo.reagents.get_reagent_amount(chem.id) < glob_fuel_cost) + click_empty(user) + return NONE + ammo.reagents.remove_reagent(chem.id, glob_fuel_cost) + sync_ammo_from_reagents() + return ..() + +// Tags the freshly-generated glob projectile's ammo datum with the id of whichever fuel is actually loaded + +/obj/item/hardpoint/primary/flamer/generate_bullet(mob/user, turf/origin_turf) + . = ..() + var/obj/projectile/fired = . + if(istype(fired?.ammo, /datum/ammo/flamethrower/tank_flamer)) + var/datum/ammo/flamethrower/tank_flamer/glob_ammo = fired.ammo + var/datum/reagent/chem = LAZYACCESS(ammo.reagents?.reagent_list, 1) + glob_ammo.loaded_chem_id = chem?.id + +// Layers an overlay on the mounted/turret sprite, colored via mix_color_from_reagents() to match whatever fuel is currently loaded +/obj/item/hardpoint/primary/flamer/get_icon_image(x_offset, y_offset, new_dir) + var/image/base_image = ..() + var/image/strip = image(icon = disp_icon, icon_state = "drgn_flamer_strip", dir = new_dir) + if(ammo?.reagents && length(ammo.reagents.reagent_list)) + strip.color = mix_color_from_reagents(ammo.reagents.reagent_list) + base_image.overlays += strip + return base_image diff --git a/code/modules/vehicles/hardpoints/primary/ltb.dm b/code/modules/vehicles/hardpoints/primary/ltb.dm index 6c818f30b8d2..02af0955eff8 100644 --- a/code/modules/vehicles/hardpoints/primary/ltb.dm +++ b/code/modules/vehicles/hardpoints/primary/ltb.dm @@ -1,3 +1,6 @@ +/// Low end of the LTB's aiming laser opacity ramp +#define LTB_LASER_MIN_ALPHA 50 + /obj/item/hardpoint/primary/cannon name = "\improper LTB Cannon" desc = "A primary cannon for tanks that shoots explosive rounds." @@ -8,10 +11,10 @@ activation_sounds = list('sound/weapons/vehicles/cannon_fire1.ogg', 'sound/weapons/vehicles/cannon_fire2.ogg') health = 500 - firing_arc = 60 + traverse_arc = 60 - ammo = new /obj/item/ammo_magazine/hardpoint/ltb_cannon - max_clips = 3 + ammo = new /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell + max_clips = 1 px_offsets = list( "1" = list(0, 21), @@ -19,6 +22,12 @@ "4" = list(32, 0), "8" = list(-32, 0) ) + rotation_pivot = list( + "1" = list(0, -21), + "2" = list(0, 32), + "4" = list(-32, 0), + "8" = list(32, 0) + ) muzzle_flash_pos = list( "1" = list(0, 59), @@ -28,4 +37,161 @@ ) scatter = 2 - fire_delay = 20.0 SECONDS + fire_delay = 15.0 SECONDS + + /// How long the lock-on charge takes before firing is allowed. + var/f_aiming_time = 4 SECONDS + /// Stashed for generate_bullet() below, which doesn't receive target directly. + var/atom/homing_target + + var/image/lockon_icon + var/image/lockon_direction_icon + var/lockon_pixel_x = 0 + var/lockon_pixel_y = 0 + var/datum/beam/laser_beam + var/obj/effect/beam_anchor/anchor + /// Incrementted every start_aim_visuals(), lets a stale cycle_laser_color() invocation from a + /// previous charge recognize it's outdated and stop, even if a new charge starts before it wakes + /// up from its own sleep() and notices laser beam has already been replaced. fixes a very annoying bug where more than one reticle can appear. + var/aim_generation = 0 + /// world.time the current charge started - used to ramp the beam's opacity up gradually across the whole charge + var/charge_start_time = 0 + +/** + * Same lock-on/tracking charge as the secondary BRUTE launcher. Targets any living mob, blocked by the same things Aimed Shot is + * (dense+opaque turfs, opaque objects, obscuring smoke... + * The fired round homes in on the target same as Aimed Shot's own /datum/component/homing_projectile. + */ +/obj/item/hardpoint/primary/cannon/handle_fire(atom/target, mob/living/user, params) + if(charging) + SEND_SIGNAL(src, COMSIG_GUN_INTERRUPT_FIRE) + return NONE + + if(!isliving(target)) + to_chat(user, SPAN_WARNING("Invalid target!")) + return NONE + + var/mob/living/living_target = target + if(living_target.is_dead()) + to_chat(user, SPAN_WARNING("Invalid target!")) + return NONE + + if(check_shot_is_blocked(target)) + to_chat(user, SPAN_WARNING("Target obscured!")) + return NONE + + if(!track_and_charge(target, user, f_aiming_time)) + return NONE + + // Re-validate once more after the charge + if(QDELETED(target) || living_target.is_dead() || check_shot_is_blocked(target)) + to_chat(user, SPAN_WARNING("You lose the shot!")) + return NONE + + homing_target = target + . = ..() + homing_target = null + return . + +/obj/item/hardpoint/primary/cannon/generate_bullet(mob/user, turf/origin_turf) + var/obj/projectile/fired_projectile = ..() + if(homing_target) + fired_projectile.AddComponent(/datum/component/homing_projectile, homing_target, user) + return fired_projectile + +/obj/item/hardpoint/primary/cannon/proc/check_shot_is_blocked(atom/target) + var/turf/origin_turf = get_origin_turf() + var/turf/target_turf = get_turf(target) + var/list/turf/path = get_line(origin_turf, target_turf, include_start_atom = FALSE) + if(!length(path)) + return TRUE + + for(var/turf/path_turf in path) + if(path_turf.density && path_turf.opacity) + return TRUE + + for(var/obj/path_obj in path_turf) + if(path_obj.get_projectile_hit_boolean(null) && path_obj.opacity) + return TRUE + + for(var/obj/effect/particle_effect/smoke/smoke in path_turf) + if(smoke.obscuring) + return TRUE + + return FALSE + +/obj/item/hardpoint/primary/cannon/start_aim_visuals(atom/target, mob/living/user) + anchor = new(get_origin_turf()) + charge_start_time = world.time + + var/mob/living_target = target + lockon_pixel_x = -living_target.pixel_x + living_target.base_pixel_x + lockon_pixel_y = (living_target.icon_size - world.icon_size) * 0.5 - living_target.pixel_y + living_target.base_pixel_y + + apply_beam_visuals(target, TRUE) + aim_generation++ + INVOKE_ASYNC(src, PROC_REF(cycle_laser_color), target, aim_generation) + +/** + * Recolors the (already-built) reticle icons and rebuilds the beam using the AMR's focused-fire beam, + * the flicker alternates between this sprite's native blue and a darker shade of the same blue. + * + * Also refreshes the direction arrow's facing (the target/tank can have moved since the last cycle) + * and ramps the beam's opacity based on total elapsed charge time (charge_start_time). + * + * The intent, with the flickering, and the blue color of the AMR focused shot, is to make it clear for xenos that + * they are being targeted by something dangerous. + */ +/obj/item/hardpoint/primary/cannon/proc/apply_beam_visuals(atom/target, use_intense) + var/turf/origin_turf = get_origin_turf() + + if(lockon_icon) + target.overlays -= lockon_icon + if(lockon_direction_icon) + target.overlays -= lockon_direction_icon + + lockon_icon = image(icon = 'icons/effects/Targeted.dmi', icon_state = use_intense ? "sniper_lockon_intense" : "sniper_lockon") + lockon_icon.pixel_x = lockon_pixel_x + lockon_icon.pixel_y = lockon_pixel_y + target.overlays += lockon_icon + + lockon_direction_icon = image(icon = 'icons/effects/Targeted.dmi', icon_state = use_intense ? "sniper_lockon_intense_direction" : "sniper_lockon_direction", dir = get_cardinal_dir(target, origin_turf)) + lockon_direction_icon.pixel_x = lockon_pixel_x + lockon_direction_icon.pixel_y = lockon_pixel_y + target.overlays += lockon_direction_icon + + QDEL_NULL(laser_beam) + laser_beam = anchor.beam(target, use_intense ? "laser_beam_intense" : "laser_beam", 'icons/effects/beam.dmi', (f_aiming_time + 1 SECONDS), beam_type = /obj/effect/ebeam/laser/intense/tank_mounted) + + var/elapsed = world.time - charge_start_time + var/current_progress = clamp(elapsed / f_aiming_time, 0, 1) + var/next_progress = clamp((elapsed + 5) / f_aiming_time, 0, 1) + laser_beam.visuals.alpha = LTB_LASER_MIN_ALPHA + (255 - LTB_LASER_MIN_ALPHA) * current_progress + animate(laser_beam.visuals, alpha = LTB_LASER_MIN_ALPHA + (255 - LTB_LASER_MIN_ALPHA) * next_progress, 0.5 SECONDS, easing = SINE_EASING|EASE_OUT) + +/// Flickers the reticle/beam between red and blue about twice a second while charging +/obj/item/hardpoint/primary/cannon/proc/cycle_laser_color(atom/locked_target, my_generation) + var/use_intense = TRUE + while(laser_beam && !QDELETED(laser_beam) && !QDELETED(locked_target) && aim_generation == my_generation) + sleep(5) + if(!laser_beam || QDELETED(laser_beam) || QDELETED(locked_target) || aim_generation != my_generation) + break + use_intense = !use_intense + apply_beam_visuals(locked_target, use_intense) + +/obj/item/hardpoint/primary/cannon/on_track_tick(atom/target) + if(!anchor) + return + var/turf/origin_turf = get_origin_turf() + if(anchor.loc != origin_turf) + anchor.forceMove(origin_turf) + +/obj/item/hardpoint/primary/cannon/end_aim_visuals(atom/target, mob/living/user, success) + if(lockon_icon) + target.overlays -= lockon_icon + lockon_icon = null + if(lockon_direction_icon) + target.overlays -= lockon_direction_icon + lockon_direction_icon = null + QDEL_NULL(laser_beam) + QDEL_NULL(anchor) diff --git a/code/modules/vehicles/hardpoints/primary/minigun.dm b/code/modules/vehicles/hardpoints/primary/minigun.dm index fd1f81990445..b7a9d923e95f 100644 --- a/code/modules/vehicles/hardpoints/primary/minigun.dm +++ b/code/modules/vehicles/hardpoints/primary/minigun.dm @@ -7,7 +7,7 @@ disp_icon_state = "ltaaap_minigun" health = 350 - firing_arc = 90 + traverse_arc = 90 ammo = new /obj/item/ammo_magazine/hardpoint/ltaaap_minigun max_clips = 1 @@ -18,6 +18,12 @@ "4" = list(32, 0), "8" = list(-32, 0) ) + rotation_pivot = list( + "1" = list(0, -21), + "2" = list(0, 32), + "4" = list(-32, 0), + "8" = list(32, 0) + ) muzzle_flash_pos = list( "1" = list(0, 57), @@ -26,7 +32,7 @@ "8" = list(-77, 0) ) - scatter = 18 //base scatter, modified by stake_delay_mult + scatter = 2 gun_firemode = GUN_FIREMODE_AUTOMATIC gun_firemode_list = list( GUN_FIREMODE_AUTOMATIC, @@ -35,7 +41,7 @@ activation_sounds = list('sound/weapons/gun_minigun.ogg') /// Active firing time to reach max spin_stage. - var/spinup_time = 10 SECONDS + var/spinup_time = 3 SECONDS /// Grace period before losing spin_stage. var/spindown_grace_time = 2 SECONDS COOLDOWN_DECLARE(spindown_grace_cooldown) @@ -43,8 +49,9 @@ var/spindown_time = 3 SECONDS /// Index of stage_rate. var/spin_stage = 1 - /// Shots fired per fire_delay at a particular spin_stage. - var/list/stage_rate = list(1, 1, 2, 2, 3, 3, 3, 4, 4, 4, 5) + /// this is meant to be an already-fearsome close range weapon even before it spins + /// all the way up, not a weapon that needs several seconds of commitment before it does anything. + var/list/stage_rate = list(3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 5) /// Fire delay multiplier for current spin_stage. var/stage_delay_mult = 1 /// When it was last fired, related to world.time. @@ -52,7 +59,7 @@ /obj/item/hardpoint/primary/minigun/set_fire_delay(value) fire_delay = value - SEND_SIGNAL(src, COMSIG_GUN_AUTOFIREDELAY_MODIFIED, fire_delay * stage_delay_mult, scatter * stage_delay_mult) + SEND_SIGNAL(src, COMSIG_GUN_AUTOFIREDELAY_MODIFIED, fire_delay * stage_delay_mult) /obj/item/hardpoint/primary/minigun/set_fire_cooldown() calculate_stage_delay_mult() //needs to check grace_cooldown before refreshed @@ -79,7 +86,7 @@ var/new_stage_rate = stage_rate[floor(spin_stage)] if(old_stage_rate != new_stage_rate) - scatter = initial(scatter) * (1/new_stage_rate) + // deliberate low scatter at every stage instead of the old huge-scatter-while-spinning-up behavior. stage_delay_mult = 1 / new_stage_rate SEND_SIGNAL(src, COMSIG_GUN_AUTOFIREDELAY_MODIFIED, fire_delay * stage_delay_mult) diff --git a/code/modules/vehicles/hardpoints/secondary/brute_launcher.dm b/code/modules/vehicles/hardpoints/secondary/brute_launcher.dm new file mode 100644 index 000000000000..164d7fd2a484 --- /dev/null +++ b/code/modules/vehicles/hardpoints/secondary/brute_launcher.dm @@ -0,0 +1,118 @@ +/obj/item/hardpoint/secondary/brute_launcher + name = "\improper M6H-BRUTE Launcher" + desc = "A tank-mounted variant of the M6H-BRUTE breaching rocket system. Laser-guides a 90mm shaped-charge rocket onto a fortified position, given a few seconds to lock on." + + icon_state = "brute_launcher" + disp_icon = "tank" + disp_icon_state = "brutelauncher" + + health = 500 + traverse_arc = 120 + activation_sounds = list('sound/weapons/gun_rocketlauncher.ogg') + + // reuses the handheld BRUTE launcher's own rocket item directly + // It's very silly that the M56D also needed its own 'vehicle magazines' instead of just using the default drum. This has properly been changed. + ammo = new /obj/item/ammo_magazine/rocket/brute + max_clips = 4 + + px_offsets = list( + "1" = list(1, 10), + "2" = list(-1, 5), + "4" = list(0, 0), + "8" = list(0, 18) + ) + rotation_pivot = list( + "1" = list(-1, -10), + "2" = list(1, -5), + "4" = list(0, 0), + "8" = list(0, -18) + ) + gimbal_pivot = list( + "1" = list(6, -6), + "2" = list(-6, 6), + "4" = list(-6, 6), + "8" = list(6, 6) + ) + muzzle_flash_pos = list( + "1" = list(8, -1), + "2" = list(-8, -16), + "4" = list(5, -8), + "8" = list(-5, 10) + ) + + scatter = 4 // irrelevant - /datum/ammo/rocket/brute sets AMMO_HITS_TARGET_TURF, which skips scatter entirely + fire_delay = 15.0 SECONDS + + // How long the lock-on charge takes before firing is allowed. + var/f_aiming_time = 5 SECONDS + + var/image/lockon_icon + var/image/lockon_direction_icon + var/datum/beam/laser_beam + var/obj/effect/beam_anchor/anchor + +/obj/item/hardpoint/secondary/brute_launcher/handle_fire(atom/target, mob/living/user, params) + if(charging) + SEND_SIGNAL(src, COMSIG_GUN_INTERRUPT_FIRE) + return NONE + + if(!(istype(target, /obj/structure) || istype(target, /turf/closed/wall))) + to_chat(user, SPAN_WARNING("Invalid target!")) + return NONE + + var/turf/origin_turf = get_origin_turf() + var/list/turf/path = get_line(origin_turf, get_turf(target), include_start_atom = FALSE) + for(var/turf/turf_path in path) + if(turf_path.opacity && turf_path != target) + to_chat(user, SPAN_WARNING("Target obscured!")) + return NONE + + if(!track_and_charge(target, user, f_aiming_time)) + return NONE + + return ..() + +/obj/item/hardpoint/secondary/brute_launcher/start_aim_visuals(atom/target, mob/living/user) + var/turf/origin_turf = get_origin_turf() + + lockon_icon = image(icon = 'icons/effects/Targeted.dmi', icon_state = "sniper_lockon_guided") + target.overlays += lockon_icon + + lockon_direction_icon = image(icon = 'icons/effects/Targeted.dmi', icon_state = "sniper_lockon_guided_direction", dir = get_cardinal_dir(target, origin_turf)) + target.overlays += lockon_direction_icon + + anchor = new(origin_turf) + laser_beam = anchor.beam(target, "laser_beam_guided", 'icons/effects/beam.dmi', (f_aiming_time + 1 SECONDS), beam_type = /obj/effect/ebeam/laser/intense/tank_mounted) + laser_beam.visuals.alpha = 0 + animate(laser_beam.visuals, alpha = initial(laser_beam.visuals.alpha), f_aiming_time, easing = SINE_EASING|EASE_OUT) + +/obj/item/hardpoint/secondary/brute_launcher/on_track_tick(atom/target) + if(!anchor) + return + var/turf/origin_turf = get_origin_turf() + if(anchor.loc != origin_turf) + anchor.forceMove(origin_turf) + +/obj/item/hardpoint/secondary/brute_launcher/end_aim_visuals(atom/target, mob/living/user, success) + if(lockon_icon) + target.overlays -= lockon_icon + lockon_icon = null + if(lockon_direction_icon) + target.overlays -= lockon_direction_icon + lockon_direction_icon = null + QDEL_NULL(laser_beam) + QDEL_NULL(anchor) + +// Plain /obj/effect/ebeam/laser/intense defaults to the base /obj layer, which renders underneath the +// tank's own ABOVE_MOB_LAYER (4.1) sprite, so, we bump it above it. +/obj/effect/ebeam/laser/intense/tank_mounted + layer = 4.501 + +// Small invisible movable used solely as a beam origin that actually moves with the muzzle +/obj/effect/beam_anchor + name = "" + icon = null + anchored = FALSE + density = FALSE + invisibility = INVISIBILITY_MAXIMUM + mouse_opacity = MOUSE_OPACITY_TRANSPARENT diff --git a/code/modules/vehicles/hardpoints/secondary/cupola.dm b/code/modules/vehicles/hardpoints/secondary/cupola.dm index cf32340070b0..c4e185fdba9f 100644 --- a/code/modules/vehicles/hardpoints/secondary/cupola.dm +++ b/code/modules/vehicles/hardpoints/secondary/cupola.dm @@ -8,11 +8,19 @@ activation_sounds = list('sound/weapons/gun_smartgun1.ogg', 'sound/weapons/gun_smartgun2.ogg', 'sound/weapons/gun_smartgun3.ogg', 'sound/weapons/gun_smartgun4.ogg') health = 350 - firing_arc = 120 + traverse_arc = 120 - ammo = new /obj/item/ammo_magazine/hardpoint/m56_cupola + // Uses the real M56D drum magazine. + ammo = new /obj/item/ammo_magazine/m56d max_clips = 1 + gimbal_pivot = list( + "1" = list(8, 4), + "2" = list(-7, 11), + "4" = list(-6, 6), + "8" = list(5, 24) + ) + muzzle_flash_pos = list( "1" = list(8, -1), "2" = list(-7, -15), diff --git a/code/modules/vehicles/hardpoints/secondary/flag.dm b/code/modules/vehicles/hardpoints/secondary/flag.dm new file mode 100644 index 000000000000..6f3c03b46178 --- /dev/null +++ b/code/modules/vehicles/hardpoints/secondary/flag.dm @@ -0,0 +1,151 @@ +#define PLANTED_FLAG_BUFF 4 +#define PLANTED_FLAG_RANGE 11 + +/obj/item/hardpoint/secondary/united_americas_flag + name = "\improper Mounted UA Flag" + desc = "An United Americas flag on a mounting support for the M34A2-A Multipurpose Turret. You feel a burst of energy by its mere sight." + + icon_state = "mounted_flag" + disp_icon = "tank" + disp_icon_state = "mountedflag" + activatable = FALSE // Cannot be toggled - always active when mounted + health = 300 + use_muzzle_flash = FALSE + var/buff_range = PLANTED_FLAG_RANGE + var/buff_strength = PLANTED_FLAG_BUFF + var/faction = FACTION_MARINE + var/flag_active = FALSE + var/datum/shape/range_bounds + var/luminosity_strength = 3 + + /// lets a fitted tank be selected as a laser/airstrike aiming point, same system as a planted flag turret + var/datum/cas_signal/signal + /// Dedicated per-tank-flag counter for the display name below + var/static/tank_flag_count = 0 + var/flag_number + + px_offsets = list( + "1" = list(0, 0), + "2" = list(0, 1), + "4" = list(0, 0), + "8" = list(0, 1) + ) + + +/obj/item/hardpoint/secondary/united_americas_flag/Destroy() + if(flag_active) + deactivate_flag() + range_bounds = null + return ..() + +/obj/item/hardpoint/secondary/united_americas_flag/on_install(obj/vehicle/multitile/vehicle) + ..() + activate_flag() + +/obj/item/hardpoint/secondary/united_americas_flag/on_uninstall(obj/vehicle/multitile/vehicle) + if(flag_active) + deactivate_flag() + ..() + +/obj/item/hardpoint/secondary/united_americas_flag/proc/activate_flag() + if(flag_active) + return + if(!owner) + return + flag_active = TRUE + set_light(luminosity_strength) + start_processing() + activate_signal() + +/obj/item/hardpoint/secondary/united_americas_flag/proc/deactivate_flag() + if(!flag_active) + return + + flag_active = FALSE + set_light(0) + stop_processing() + range_bounds = null + deactivate_signal() + +/obj/item/hardpoint/secondary/united_americas_flag/proc/activate_signal() + if(signal || !owner) + return + if(!faction || !GLOB.cas_groups[faction]) + return + + if(!flag_number) + flag_number = ++tank_flag_count + + // signal_loc = owner (the tank itself, a movable atom) rather than a snapshotted turf + signal = new(owner) + signal.target_id = ++GLOB.cas_tracking_id_increment + signal.name = "TANK-[flag_number < 10 ? "0[flag_number]" : "[flag_number]"]" + signal.linked_cam = new(get_turf(owner), signal.name) + GLOB.cas_groups[faction].add_signal(signal) + +/obj/item/hardpoint/secondary/united_americas_flag/proc/deactivate_signal() + if(!signal) + return + if(faction && GLOB.cas_groups[faction]) + GLOB.cas_groups[faction].remove_signal(signal) + QDEL_NULL(signal) + +/obj/item/hardpoint/secondary/united_americas_flag/proc/start_processing() + START_PROCESSING(SSobj, src) + +/obj/item/hardpoint/secondary/united_americas_flag/proc/stop_processing() + STOP_PROCESSING(SSobj, src) + +/obj/item/hardpoint/secondary/united_americas_flag/process() + if(!flag_active || !owner) + stop_processing() + return + + if(health <= 0) + deactivate_flag() + return + + apply_area_effect() + +/obj/item/hardpoint/secondary/united_americas_flag/proc/apply_area_effect() + if(!owner || !flag_active) + return + + // Update range bounds to follow the vehicle + var/turf/owner_turf = get_turf(owner) + if(!owner_turf) + return + + // Keep the CAS signal's z-lock in sync with the tank's real position + if(signal && signal.z_initial != owner_turf.z) + signal.z_initial = owner_turf.z + + // Keep the CAS camera view following the tank. + if(signal?.linked_cam && signal.linked_cam.loc != owner_turf) + signal.linked_cam.forceMove(owner_turf) + + range_bounds = SQUARE(owner_turf.x, owner_turf.y, buff_range) + + var/list/targets = SSquadtree.players_in_range(range_bounds, owner_turf.z, QTREE_SCAN_MOBS | QTREE_FILTER_LIVING) + if(!targets) + return + + for(var/mob/living/carbon/human/H in targets) + // Check if the human is of the correct faction + if(!H.faction || !(faction in H.faction_group)) + continue + + apply_buff_to_player(H) + +/obj/item/hardpoint/secondary/united_americas_flag/proc/apply_buff_to_player(mob/living/carbon/human/H) + H.activate_order_buff(COMMAND_ORDER_HOLD, buff_strength, 5 SECONDS) + H.activate_order_buff(COMMAND_ORDER_FOCUS, buff_strength, 5 SECONDS) + H.activate_order_buff(COMMAND_ORDER_MOVE, buff_strength, 5 SECONDS) + +/obj/item/hardpoint/secondary/united_americas_flag/on_destroy() + if(flag_active) + deactivate_flag() + ..() + +#undef PLANTED_FLAG_BUFF +#undef PLANTED_FLAG_RANGE diff --git a/code/modules/vehicles/hardpoints/secondary/flamer.dm b/code/modules/vehicles/hardpoints/secondary/flamer.dm index 56a9995b60c5..661c9724667a 100644 --- a/code/modules/vehicles/hardpoints/secondary/flamer.dm +++ b/code/modules/vehicles/hardpoints/secondary/flamer.dm @@ -8,7 +8,7 @@ activation_sounds = list('sound/weapons/vehicles/flamethrower.ogg') health = 300 - firing_arc = 120 + traverse_arc = 120 ammo = new /obj/item/ammo_magazine/hardpoint/secondary_flamer max_clips = 1 @@ -23,10 +23,28 @@ "4" = list(3, 0), "8" = list(-3, 18) ) + rotation_pivot = list( + "1" = list(-2, -14), + "2" = list(2, -3), + "4" = list(-3, 0), + "8" = list(3, -18) + ) + + gimbal_pivot = list( + "1" = list(6, -6), + "2" = list(-6, 6), + "4" = list(-6, 6), + "8" = list(6, 6) + ) scatter = 6 fire_delay = 3.0 SECONDS + var/flame_mode = FLAME_MODE_STREAM + var/glob_fuel_cost = 3 + /// The reagent-based ammo/fuel deduction is done manually in handle_fire() + ammo_cost_per_shot = 0 + /obj/item/hardpoint/secondary/small_flamer/try_fire(atom/target, mob/living/user, params) if(get_turf(target) in owner.locs) to_chat(user, SPAN_WARNING("The target is too close.")) @@ -34,24 +52,29 @@ return ..() -/obj/item/hardpoint/secondary/small_flamer/handle_fire(atom/target, mob/living/user, params) - //step forward along path so flame starts outside hull - var/list/turfs = get_line(get_origin_turf(), get_turf(target)) - var/turf/origin_turf - for(var/turf/turf as anything in turfs) - if(turf in owner.locs) - continue - origin_turf = turf - break - var/distance = get_dist(origin_turf, get_turf(target)) - var/fire_amount = min(ammo.current_rounds, distance+1, max_range) - ammo.current_rounds -= fire_amount +// Toggles between FLAME_MODE_STREAM and FLAME_MODE_GLOB +/obj/item/hardpoint/secondary/small_flamer/toggle_fire_mode(mob/user) + flame_mode = (flame_mode == FLAME_MODE_STREAM) ? FLAME_MODE_GLOB : FLAME_MODE_STREAM + to_chat(user, SPAN_NOTICE("You switch \the [src] to [flame_mode == FLAME_MODE_STREAM ? "stream" : "glob shot"] mode.")) - new /obj/flamer_fire(origin_turf, create_cause_data(initial(name), user), null, fire_amount, null, FLAMESHAPE_LINE, target) - - play_firing_sounds() +/obj/item/hardpoint/secondary/small_flamer/handle_fire(atom/target, mob/living/user, params) + if(flame_mode == FLAME_MODE_STREAM) + return fire_flame_stream(target, user, max_range) - COOLDOWN_START(src, fire_cooldown, fire_delay) + var/datum/reagent/chem = LAZYACCESS(ammo.reagents?.reagent_list, 1) + if(!chem || ammo.reagents.get_reagent_amount(chem.id) < glob_fuel_cost) + click_empty(user) + return NONE + ammo.reagents.remove_reagent(chem.id, glob_fuel_cost) + sync_ammo_from_reagents() + return ..() - return AUTOFIRE_CONTINUE +// Tags the freshly-generated glob projectile's ammo datum with the id of whichever fuel is actually loaded +/obj/item/hardpoint/secondary/small_flamer/generate_bullet(mob/user, turf/origin_turf) + . = ..() + var/obj/projectile/fired = . + if(istype(fired?.ammo, /datum/ammo/flamethrower/tank_flamer_secondary)) + var/datum/ammo/flamethrower/tank_flamer_secondary/glob_ammo = fired.ammo + var/datum/reagent/chem = LAZYACCESS(ammo.reagents?.reagent_list, 1) + glob_ammo.loaded_chem_id = chem?.id diff --git a/code/modules/vehicles/hardpoints/secondary/frontal_cannon.dm b/code/modules/vehicles/hardpoints/secondary/frontal_cannon.dm index 57ce6e2e5ff8..9ca5ec2a3237 100644 --- a/code/modules/vehicles/hardpoints/secondary/frontal_cannon.dm +++ b/code/modules/vehicles/hardpoints/secondary/frontal_cannon.dm @@ -11,7 +11,7 @@ damage_multiplier = 0.11 health = 350 - firing_arc = 120 + traverse_arc = 120 origins = list(0, -1) diff --git a/code/modules/vehicles/hardpoints/secondary/grenade_launcher.dm b/code/modules/vehicles/hardpoints/secondary/grenade_launcher.dm index ee06e7405e07..486350bee25b 100644 --- a/code/modules/vehicles/hardpoints/secondary/grenade_launcher.dm +++ b/code/modules/vehicles/hardpoints/secondary/grenade_launcher.dm @@ -8,7 +8,7 @@ activation_sounds = list('sound/weapons/gun_m92_attachable.ogg') health = 500 - firing_arc = 90 + traverse_arc = 120 ammo = new /obj/item/ammo_magazine/hardpoint/tank_glauncher max_clips = 3 @@ -22,6 +22,20 @@ "8" = list(-6, 17) ) + rotation_pivot = list( + "1" = list(0, -17), + "2" = list(0, 0), + "4" = list(-6, 0), + "8" = list(6, -17) + ) + + gimbal_pivot = list( + "1" = list(6, -6), + "2" = list(-6, 6), + "4" = list(-6, 6), + "8" = list(6, 6) + ) + scatter = 10 fire_delay = 3.0 SECONDS diff --git a/code/modules/vehicles/hardpoints/secondary/secondary.dm b/code/modules/vehicles/hardpoints/secondary/secondary.dm index 366c7fcfa9fb..61114ca14c04 100644 --- a/code/modules/vehicles/hardpoints/secondary/secondary.dm +++ b/code/modules/vehicles/hardpoints/secondary/secondary.dm @@ -7,3 +7,48 @@ damage_multiplier = 0.125 activatable = TRUE + +/** + * Toggles control of this secondary between the gunner (default,, follows the turret's shared + * rotation like any other mounted weapon) and the driver (self-gimballed, its own independent unrestricted 360-degree mouse-aim + */ +/obj/item/hardpoint/secondary/proc/toggle_slaved_to_driver(mob/user) + if(health <= 0) + to_chat(user, SPAN_WARNING("\The [src]'s controls are busted!")) + return + + var/obj/item/hardpoint/holder/tank_turret/turret = loc + if(!istype(turret)) + return + + self_gimballed = !self_gimballed + SEND_SIGNAL(src, COMSIG_GUN_INTERRUPT_FIRE) + + if(self_gimballed) + allowed_seat = VEHICLE_DRIVER + max_angular_velocity = max(TURRET_BASE_ANGULAR_VELOCITY * (traverse_arc / TURRET_ARC_NORMALIZATION), TURRET_MIN_ANGULAR_VELOCITY) + current_angle = dir2angle(dir) + desired_angle = current_angle + angular_velocity = 0 + rotation_active = FALSE + + if(owner.active_hp[VEHICLE_GUNNER] == src) + owner.active_hp[VEHICLE_GUNNER] = null + + to_chat(user, SPAN_NOTICE("You slave \the [src]'s controls to the driver's seat.")) + var/mob/driver = owner.seats[VEHICLE_DRIVER] + if(driver) + to_chat(driver, SPAN_NOTICE("\The [src] has been slaved to your controls. Use \"Cycle Active Hardpoint\" to select it.")) + else + allowed_seat = initial(allowed_seat) + + if(owner.active_hp[VEHICLE_DRIVER] == src) + owner.active_hp[VEHICLE_DRIVER] = null + + // dir already always mirrors the turret's own (unaffected by self_gimballed) so just settle current_angle back to zero swivel. + current_angle = dir2angle(dir) + desired_angle = current_angle + angular_velocity = 0 + rotation_active = FALSE + + to_chat(user, SPAN_NOTICE("You return \the [src]'s controls to the gunner's seat.")) diff --git a/code/modules/vehicles/hardpoints/secondary/tow.dm b/code/modules/vehicles/hardpoints/secondary/tow.dm index c15f8aa1ef63..2247a6f80d96 100644 --- a/code/modules/vehicles/hardpoints/secondary/tow.dm +++ b/code/modules/vehicles/hardpoints/secondary/tow.dm @@ -7,7 +7,7 @@ disp_icon_state = "towlauncher" health = 500 - firing_arc = 60 + traverse_arc = 120 ammo = new /obj/item/ammo_magazine/hardpoint/towlauncher max_clips = 1 @@ -19,6 +19,20 @@ "8" = list(0, 18) ) + rotation_pivot = list( + "1" = list(-1, -10), + "2" = list(1, -5), + "4" = list(0, 0), + "8" = list(0, -18) + ) + + gimbal_pivot = list( + "1" = list(6, -6), + "2" = list(-6, 6), + "4" = list(-6, 6), + "8" = list(6, 6) + ) + muzzle_flash_pos = list( "1" = list(8, -1), "2" = list(-8, -16), diff --git a/code/modules/vehicles/hardpoints/special/firing_port_weapon.dm b/code/modules/vehicles/hardpoints/special/firing_port_weapon.dm index d9d68e0af237..9e3688d972a8 100644 --- a/code/modules/vehicles/hardpoints/special/firing_port_weapon.dm +++ b/code/modules/vehicles/hardpoints/special/firing_port_weapon.dm @@ -10,7 +10,7 @@ activation_sounds = list('sound/weapons/gun_smartgun1.ogg', 'sound/weapons/gun_smartgun2.ogg', 'sound/weapons/gun_smartgun3.ogg', 'sound/weapons/gun_smartgun4.ogg') health = 100 - firing_arc = 120 + traverse_arc = 120 //FPWs reload automatically var/reloading = FALSE var/reload_time = 10 SECONDS diff --git a/code/modules/vehicles/hardpoints/support/artillery.dm b/code/modules/vehicles/hardpoints/support/artillery.dm index 758f6b828cb3..cbdb9804f2b2 100644 --- a/code/modules/vehicles/hardpoints/support/artillery.dm +++ b/code/modules/vehicles/hardpoints/support/artillery.dm @@ -1,6 +1,6 @@ /obj/item/hardpoint/support/artillery_module name = "\improper Artillery Module" - desc = "Allows the user to look far into the distance." + desc = "Allows the user to look far into the distance, and offers a night vision overlay." icon_state = "artillery" disp_icon = "tank" @@ -8,30 +8,62 @@ health = 250 - activatable = TRUE + // Not fired/toggled through the normal active-hardpoint cycle anymore. Uses itts own verbs. + activatable = FALSE - var/is_active = 0 var/view_buff = 10 //This way you can VV for more or less fun - var/view_tile_offset = 7 + var/view_tile_offset = 2 -/obj/item/hardpoint/support/artillery_module/handle_fire(atom/target, mob/living/user, params) - if(!user.client) + /// Mobs (driver and/or gunner) that currently have the magnified optics view toggled on for themselves. + var/list/mob/living/optics_users = list() + /// Mobs (driver and/or gunner) that currently have the night vision overlay toggled on for themselves. + var/list/mob/living/nvg_users = list() + + /// Color used for the night vision overlay's tint. + var/nvg_color = NV_COLOR_GREEN + /// The alpha of darkness the user is set to see through while night vision is active. + var/nvg_lighting_alpha = 100 + +/obj/item/hardpoint/support/artillery_module/proc/toggle_optics(mob/living/user) + if(health <= 0) + to_chat(user, SPAN_WARNING("\The [src] is broken!")) + return + if(!user?.client) return - if(is_active) - user.client.change_view(8, owner) - user.client.set_pixel_x(0) - user.client.set_pixel_y(0) - is_active = FALSE + if(user in optics_users) + disable_optics(user) + else + enable_optics(user) + +/obj/item/hardpoint/support/artillery_module/proc/enable_optics(mob/living/user) + if(!user?.client || (user in optics_users)) return + optics_users += user - var/atom/holder = owner - for(var/obj/item/hardpoint/holder/tank_turret/T in owner.hardpoints) - holder = T - break + var/obj/vehicle/multitile/vehicle = owner + if(user == vehicle.seats[VEHICLE_GUNNER]) + apply_gunner_view(user) + else + apply_driver_view(user) + +/obj/item/hardpoint/support/artillery_module/proc/disable_optics(mob/living/user) + if(!(user in optics_users)) + return + optics_users -= user + reset_view(user) + +/// Zooms the gunner's view and shifts it forward along the turret's current facing. +/obj/item/hardpoint/support/artillery_module/proc/apply_gunner_view(mob/living/user) + if(!user || !user.client) + return user.client.change_view(view_buff, owner) - is_active = TRUE + + var/atom/holder = owner + for(var/obj/item/hardpoint/holder/tank_turret/turret in owner.hardpoints) + holder = turret + break switch(holder.dir) if(NORTH) @@ -47,25 +79,76 @@ user.client.set_pixel_x(-1 * view_tile_offset * 32) user.client.set_pixel_y(0) -/obj/item/hardpoint/support/artillery_module/deactivate() - if(!is_active) +/// Zooms the driver's view, centered on the tank instead of shifted forwards +/obj/item/hardpoint/support/artillery_module/proc/apply_driver_view(mob/living/user) + if(!user || !user.client) return + user.client.change_view(view_buff, owner) + user.client.set_pixel_x(0) + user.client.set_pixel_y(0) - var/obj/vehicle/multitile/C = owner - for(var/seat in C.seats) - if(!ismob(C.seats[seat])) - continue - var/mob/user = C.seats[seat] - if(!user.client) - continue - user.client.change_view(GLOB.world_view_size, owner) - user.client.set_pixel_x(0) - user.client.set_pixel_y(0) - is_active = FALSE - -/obj/item/hardpoint/support/artillery_module/try_fire(target, user, params) +/obj/item/hardpoint/support/artillery_module/proc/reset_view(mob/living/user) + if(!user || !user.client) + return + user.client.change_view(8, owner) + user.client.set_pixel_x(0) + user.client.set_pixel_y(0) + +/obj/item/hardpoint/support/artillery_module/proc/toggle_nvg(mob/living/user) if(health <= 0) - to_chat(usr, SPAN_WARNING("\The [src] is broken!")) - return NONE + to_chat(user, SPAN_WARNING("\The [src] is broken!")) + return + if(!ishuman(user)) + return + + if(user in nvg_users) + disable_nvg(user) + else + enable_nvg(user) + +/obj/item/hardpoint/support/artillery_module/proc/enable_nvg(mob/living/carbon/human/user) + if(!ishuman(user) || (user in nvg_users)) + return + nvg_users += user + + RegisterSignal(user, COMSIG_HUMAN_POST_UPDATE_SIGHT, PROC_REF(on_update_sight)) + user.add_client_color_matrix("tank_artillery_nvg", 99, color_matrix_multiply(color_matrix_saturation(0), color_matrix_from_string(nvg_color))) + user.overlay_fullscreen("tank_artillery_nvg", /atom/movable/screen/fullscreen/flash/noise/nvg) + user.overlay_fullscreen("tank_artillery_nvg_blur", /atom/movable/screen/fullscreen/brute/nvg, 3) + user.update_sight() + to_chat(user, SPAN_NOTICE("You activate \the [src]'s night vision overlay.")) + +/obj/item/hardpoint/support/artillery_module/proc/disable_nvg(mob/living/carbon/human/user) + if(!(user in nvg_users)) + return + nvg_users -= user + + UnregisterSignal(user, COMSIG_HUMAN_POST_UPDATE_SIGHT) + user.remove_client_color_matrix("tank_artillery_nvg", 1 SECONDS) + user.clear_fullscreen("tank_artillery_nvg", 0.5 SECONDS) + user.clear_fullscreen("tank_artillery_nvg_blur", 0.5 SECONDS) + user.update_sight() + to_chat(user, SPAN_NOTICE("You deactivate \the [src]'s night vision overlay.")) + +/obj/item/hardpoint/support/artillery_module/proc/on_update_sight(mob/living/carbon/human/user) + SIGNAL_HANDLER + if(nvg_lighting_alpha < 255) + user.see_in_dark = 12 + user.lighting_alpha = nvg_lighting_alpha + user.sync_lighting_plane_alpha() + +/// Clears both effects off of one specific user +/obj/item/hardpoint/support/artillery_module/proc/clear_user_effects(mob/living/user) + disable_optics(user) + disable_nvg(user) + +/// Clears both effects off of everyone currently using them if the module gets broken or uninstalled when used +/obj/item/hardpoint/support/artillery_module/deactivate() + for(var/mob/living/user as anything in optics_users.Copy()) + disable_optics(user) + for(var/mob/living/user as anything in nvg_users.Copy()) + disable_nvg(user) - return handle_fire(target, user, params) +/obj/item/hardpoint/support/artillery_module/on_uninstall(obj/vehicle/multitile/vehicle) + deactivate() + ..() diff --git a/code/modules/vehicles/hardpoints/support/flare.dm b/code/modules/vehicles/hardpoints/support/flare.dm index 0fa37de34d2f..bfc36e9d54b3 100644 --- a/code/modules/vehicles/hardpoints/support/flare.dm +++ b/code/modules/vehicles/hardpoints/support/flare.dm @@ -13,7 +13,7 @@ activatable = TRUE health = 500 - firing_arc = 120 + traverse_arc = 120 ammo = new /obj/item/ammo_magazine/hardpoint/flare_launcher max_clips = 3 diff --git a/code/modules/vehicles/interior/interactable/intercom.dm b/code/modules/vehicles/interior/interactable/intercom.dm new file mode 100644 index 000000000000..ef5064a00289 --- /dev/null +++ b/code/modules/vehicles/interior/interactable/intercom.dm @@ -0,0 +1,82 @@ +/obj/structure/vehicle_intercom + name = "vehicle intercom" + desc = "A wall-mounted intercom wired into the vehicle's hull, relaying speech between the crew compartment and the outside world." + icon = 'icons/obj/structures/phone.dmi' + icon_state = "wall_phone" + anchored = TRUE + density = FALSE + layer = 3.2 + + unacidable = TRUE + unslashable = TRUE + explo_proof = TRUE + + flags_atom = USES_HEARING + + var/obj/vehicle/multitile/vehicle = null + var/obj/item/phone/handset + + // Mic = picks up interior speech to broadcast outside. Speaker = plays exterior speech inside. + var/mic_muted = FALSE + var/speaker_muted = FALSE + +/obj/structure/vehicle_intercom/Initialize(mapload, ...) + . = ..() + handset = new /obj/item/phone(src) + +/obj/structure/vehicle_intercom/Destroy() + QDEL_NULL(handset) + vehicle = null + return ..() + +/obj/structure/vehicle_intercom/attack_hand(mob/user) + . = ..() + if(.) + return + + var/choice = tgui_input_list(user, "Adjust the intercom's microphone and speaker.", "Vehicle Intercom", + list("Toggle Microphone ([mic_muted ? "Muted" : "Live"])", "Toggle Speaker ([speaker_muted ? "Muted" : "Live"])", "Cancel")) + + if(!choice || choice == "Cancel" || !Adjacent(user)) + return + + if(findtext(choice, "Microphone")) + mic_muted = !mic_muted + to_chat(user, SPAN_NOTICE("You [mic_muted ? "mute" : "unmute"] the intercom's microphone. [mic_muted ? "Nobody outside will hear the crew compartment." : "The crew compartment can be heard outside again."]")) + else if(findtext(choice, "Speaker")) + speaker_muted = !speaker_muted + to_chat(user, SPAN_NOTICE("You [speaker_muted ? "mute" : "unmute"] the intercom's speaker. [speaker_muted ? "The crew compartment will no longer hear the outside." : "The crew compartment can hear the outside again."]")) + +// anyone speaking near the inttercom inside the interior gets relayed outside. +/obj/structure/vehicle_intercom/hear_talk(mob/living/M, msg, verb = "says", datum/language/speaking, italics = 0) + if(!mic_muted) + relay_to_exterior(M, msg, verb, speaking, italics) + ..() + +/obj/structure/vehicle_intercom/proc/relay_to_exterior(mob/living/M, msg, verb, datum/language/speaking, italics) + if(!vehicle) + return + var/turf/exterior_turf = get_turf(vehicle) + if(!exterior_turf) + return + var/list/listeners = get_mobs_in_view(7, exterior_turf) + if(!length(listeners)) + return + for(var/mob/L in listeners) + L.hear_radio(msg, verb, speaking, part_a = "", + part_b = " ", vname = "[vehicle] Intercom", + speaker = M, no_paygrade = TRUE) + vehicle.langchat_speech(msg, listeners, speaking, M.langchat_color, FALSE, LANGCHAT_FAST_POP, list(M.langchat_styles)) + +// Called by /obj/vehicle/multitile/hear_talk() when someone speaks near the vehicle's exterior. +/obj/structure/vehicle_intercom/proc/relay_exterior_speech(mob/living/M, msg, verb, datum/language/speaking, italics) + if(speaker_muted || !vehicle || !vehicle.interior) + return + var/list/listeners = vehicle.interior.get_passengers() + if(!length(listeners)) + return + for(var/mob/L in listeners) + L.hear_radio(msg, verb, speaking, part_a = "", + part_b = " ", vname = "Intercom", + speaker = M, no_paygrade = TRUE) + langchat_speech(msg, listeners, speaking, M.langchat_color, FALSE, LANGCHAT_FAST_POP, list(M.langchat_styles)) diff --git a/code/modules/vehicles/interior/interactable/seats.dm b/code/modules/vehicles/interior/interactable/seats.dm index 7048aa44ccc3..74b9a07cb7ef 100644 --- a/code/modules/vehicles/interior/interactable/seats.dm +++ b/code/modules/vehicles/interior/interactable/seats.dm @@ -47,6 +47,10 @@ M.client.set_pixel_x(0) M.client.set_pixel_y(0) M.reset_view() + if(isliving(M)) + var/mob/living/living_mob = M + if(living_mob.observed_atom) + QDEL_NULL(living_mob.observed_atom) else if(M.stat == DEAD) unbuckle() diff --git a/code/modules/vehicles/interior/interactable/weapons_loader.dm b/code/modules/vehicles/interior/interactable/weapons_loader.dm index 18b3f894e2a2..7b77865c6820 100644 --- a/code/modules/vehicles/interior/interactable/weapons_loader.dm +++ b/code/modules/vehicles/interior/interactable/weapons_loader.dm @@ -14,7 +14,19 @@ // Loading new magazines /obj/structure/weapons_loader/attackby(obj/item/I, mob/user) - if(!istype(I, /obj/item/ammo_magazine/hardpoint)) + // The tank turret's flare launcher isn't loaded via a swappable magazine so it + // shouldn't be picked up by the generic magazine-matching loop below. Delegate loose star shells and + // star shell packets straight to the turret's own attackby(). + if(istype(I, /obj/item/explosive/grenade/high_explosive/airburst/starshell) || istype(I, /obj/item/storage/box/packet/flare)) + if(!skillcheck(user, SKILL_VEHICLE, SKILL_VEHICLE_LARGE)) + to_chat(user, SPAN_NOTICE("You have no idea how to operate this thing!")) + return + var/obj/item/hardpoint/holder/tank_turret/turret = locate() in vehicle?.hardpoints + if(!turret) + return ..() + return turret.attackby(I, user) + + if(!istype(I, /obj/item/ammo_magazine)) return ..() if(!skillcheck(user, SKILL_VEHICLE, SKILL_VEHICLE_LARGE)) @@ -24,10 +36,10 @@ // Check if any of the hardpoints accept the magazine var/obj/item/hardpoint/reloading_hardpoint = null for(var/obj/item/hardpoint/H in vehicle.get_hardpoints_with_ammo()) - if(QDELETED(H) || QDELETED(H.ammo)) + if(QDELETED(H)) continue - if(istype(I, H.ammo.type)) + if(H.accepts_magazine(I)) reloading_hardpoint = H break @@ -88,7 +100,16 @@ return if(!LAZYLEN(HP.backup_clips)) - to_chat(user, SPAN_WARNING("\The [HP] has no remaining backup magazines!")) + if(!HP.ammo) + to_chat(user, SPAN_WARNING("\The [HP] has no remaining backup magazines, and no magazine currently loaded!")) + return + + to_chat(user, SPAN_WARNING("\The [HP] has no remaining backup magazines, so you remove the current magazine instead.")) + HP.ammo.forceMove(get_turf(user)) + HP.ammo.update_icon() + HP.ammo = null + HP.owner?.update_icon() + playsound(loc, 'sound/machines/hydraulics_3.ogg', 50) return var/obj/item/ammo_magazine/M = LAZYACCESS(HP.backup_clips, 1) @@ -102,10 +123,12 @@ to_chat(user, SPAN_WARNING("Something interrupted you while reloading \the [HP].")) return - HP.ammo.forceMove(get_turf(src)) - HP.ammo.update_icon() + if(HP.ammo) + HP.ammo.forceMove(get_turf(src)) + HP.ammo.update_icon() HP.ammo = M LAZYREMOVE(HP.backup_clips, M) + HP.owner?.update_icon() playsound(loc, 'sound/machines/hydraulics_3.ogg', 50) to_chat(user, SPAN_NOTICE("You reload \the [HP]. Ammo: [SPAN_HELPFUL(HP.ammo.current_rounds)]/[SPAN_HELPFUL(HP.ammo.max_rounds)] | Mags: [SPAN_HELPFUL(LAZYLEN(HP.backup_clips))]/[SPAN_HELPFUL(HP.max_clips)]")) diff --git a/code/modules/vehicles/interior/interior_landmarks.dm b/code/modules/vehicles/interior/interior_landmarks.dm index b57defea5f4b..02cf7a68cb55 100644 --- a/code/modules/vehicles/interior/interior_landmarks.dm +++ b/code/modules/vehicles/interior/interior_landmarks.dm @@ -247,6 +247,32 @@ /obj/effect/landmark/interior/spawn/telephone/wy icon = 'icons/obj/vehicles/interiors/general_wy.dmi' +// Landmark for spawning the ambient vehicle intercom +/obj/effect/landmark/interior/spawn/intercom + name = "vehicle intercom spawner" + icon = 'icons/obj/vehicles/interiors/general.dmi' + icon_state = "wall_phone" + color = "cyan" + +/obj/effect/landmark/interior/spawn/intercom/on_load(datum/interior/interior_data) + var/obj/structure/vehicle_intercom/Intercom = new(loc) + var/obj/vehicle/multitile/vehicle = interior_data.exterior + + Intercom.icon = icon + Intercom.icon_state = icon_state + Intercom.layer = layer + Intercom.setDir(dir) + Intercom.alpha = alpha + Intercom.update_icon() + Intercom.pixel_x = pixel_x + Intercom.pixel_y = pixel_y + Intercom.vehicle = vehicle + + vehicle.intercom = Intercom + vehicle.flags_atom |= USES_HEARING + + qdel(src) + // Landmark for spawning the reloader /obj/effect/landmark/interior/spawn/weapons_loader name = "vehicle weapons reloader spawner" diff --git a/code/modules/vehicles/multitile/multitile.dm b/code/modules/vehicles/multitile/multitile.dm index d04e1e0e8f3e..d33230478483 100644 --- a/code/modules/vehicles/multitile/multitile.dm +++ b/code/modules/vehicles/multitile/multitile.dm @@ -53,7 +53,13 @@ // Determines how much slower the vehicle is when it lacks its full momentum // When the vehicle has 0 momentum, it's movement delay will be move_delay * momentum_build_factor // The movement delay gradually reduces up to move_delay when momentum increases + //// This is mostly vestigial and it looks like it cancels itself out when calculating movement delay??? -bwsb var/move_momentum_build_factor = 1.3 + // How fast momentum decays when you stop moving. + var/move_momentum_loss_factor = 1 + // How long do you have to go without moving for momentum decay to start + // Slower moving vehicles like the tank require more time, or else they won't get past minimum speed. + var/idle_time_required = 10 // 10 seconds. Tested to be OK on APC and Van. //Sound to play when moving var/movement_sound @@ -89,6 +95,7 @@ // Map file name of the vehicle interior var/interior_map = null var/datum/interior/interior = null + var/obj/structure/vehicle_intercom/intercom = null //common passenger slots var/passengers_slots = 2 @@ -174,12 +181,23 @@ ///Minimap iconstate to use for this vehicle var/minimap_icon_state + // Structures that we should collide with, but that aren't being collided with when we call T.Enter in multitile_movement + // associative list should guarantee an O(1) lookup in case this needs to be expanded. + var/static/list/blocking_structures = list( + /obj/structure/shuttle/part = TRUE, + /obj/structure/mineral_door/resin = TRUE, + ) + + var/momentum_decay_active = FALSE // Track if momentum decay loop is running + var/last_input_time = 0 // Track when last movement input was received + /obj/vehicle/multitile/Initialize() . = ..() var/angle_to_turn = turning_angle(SOUTH, dir) rotate_entrances(angle_to_turn) rotate_bounds(angle_to_turn) + update_langchat_height() if(bound_width > world.icon_size || bound_height > world.icon_size) lighting_holder = new(src) @@ -225,6 +243,24 @@ return ..() +// No-op unless an intercom landmark spawned one into this vehicle's interior. +/obj/vehicle/multitile/hear_talk(mob/living/speaker, msg, verb = "says", datum/language/speaking, italics = 0) + if(intercom) + intercom.relay_exterior_speech(speaker, msg, verb, speaking, italics) + if(!intercom.speaker_muted) + for(var/seat_key in seats) + var/mob/seated = seats[seat_key] + if(seated?.client?.prefs && !seated.client.prefs.lang_chat_disabled && !seated.ear_deaf && seated.say_understands(speaker, speaking)) + speaker.langchat_display_image(seated) + ..() + +// offsets langchat height to show, properly, above and around the middle of the vehicle. +/obj/vehicle/multitile/proc/update_langchat_height() + langchat_height = bound_height - bound_y - 8 + +/obj/vehicle/multitile/get_maxptext_x_offset(image/maptext_image) + return ..() - bound_x - 16 + /obj/vehicle/multitile/proc/initialize_cameras() return @@ -468,6 +504,12 @@ forceMove(get_turf(mover)) +/obj/vehicle/multitile/proc/is_blocking_structure(atom/A) + for(var/blocked_type in blocking_structures) + if(ispath(A.type, blocked_type)) + return TRUE + return FALSE + ///Updates the vehicles minimap icon /obj/vehicle/multitile/proc/update_minimap_icon(modules_broken) if(!minimap_icon_state) diff --git a/code/modules/vehicles/multitile/multitile_bump.dm b/code/modules/vehicles/multitile/multitile_bump.dm index fd1b6ec870b9..38fed407120f 100644 --- a/code/modules/vehicles/multitile/multitile_bump.dm +++ b/code/modules/vehicles/multitile/multitile_bump.dm @@ -46,6 +46,12 @@ return TRUE return FALSE +/obj/structure/mineral_door/resin/handle_vehicle_bump(obj/vehicle/multitile/V) + take_damage(V.wall_ram_damage) + visible_message(SPAN_DANGER("\The [V] rams [src]!")) + playsound(V, 'sound/effects/metal_crash.ogg', 20) + return FALSE + /obj/structure/barricade/handle_vehicle_bump(obj/vehicle/multitile/V) if(!(V.vehicle_flags & VEHICLE_CLASS_WEAK)) take_damage(maxhealth) @@ -56,12 +62,12 @@ return FALSE /obj/structure/barricade/plasteel/handle_vehicle_bump(obj/vehicle/multitile/V) + if(V.vehicle_flags & VEHICLE_CLASS_MEDIUM || V.vehicle_flags & VEHICLE_CLASS_HEAVY) + close(src) + return TRUE if(V.seats[VEHICLE_DRIVER]) close(src) - return FALSE - else - . = ..() - return FALSE + return ..() /obj/structure/barricade/deployable/handle_vehicle_bump(obj/vehicle/multitile/V) visible_message(SPAN_DANGER("\The [V] crushes [src]!")) @@ -524,6 +530,17 @@ playsound(V, 'sound/effects/metal_crash.ogg', 35) return FALSE +// legacy cargo train gets ran over and crushed without a fuss by medium and heavy vehicles +/obj/vehicle/train/cargo/handle_vehicle_bump(obj/vehicle/multitile/V) + if(V.vehicle_flags & VEHICLE_CLASS_MEDIUM || V.vehicle_flags & VEHICLE_CLASS_HEAVY) + health = 0 + healthcheck() + + visible_message(SPAN_DANGER("\The [V] crushes into \the [src]!")) + playsound(V, 'sound/effects/metal_crash.ogg', 35) + return TRUE + . = ..() + //----------------------------------------------------------- //-------------------------MOBS------------------------------ //----------------------------------------------------------- @@ -615,8 +632,9 @@ apply_damage(10 + rand(0, 10), BRUTE) else if(V.vehicle_flags & VEHICLE_CLASS_MEDIUM) - apply_effect(3, WEAKEN) - apply_damage(10 + rand(0, 10), BRUTE) + apply_effect((1.5 * (V.move_momentum / V.move_max_momentum)), WEAKEN) // Even with the nerfs, still by far the most frustrating thing about the tank. + apply_effect((3 * (V.move_momentum / V.move_max_momentum)), SLOW) + apply_damage(5 + rand(0, 5), BRUTE) dmg = TRUE else if(V.vehicle_flags & VEHICLE_CLASS_HEAVY) diff --git a/code/modules/vehicles/multitile/multitile_hardpoints.dm b/code/modules/vehicles/multitile/multitile_hardpoints.dm index d40749aedcf5..f15eb383da03 100644 --- a/code/modules/vehicles/multitile/multitile_hardpoints.dm +++ b/code/modules/vehicles/multitile/multitile_hardpoints.dm @@ -29,7 +29,7 @@ var/obj/item/hardpoint/holder/HP = H if(HP.hardpoints) hps += HP.get_hardpoints_with_ammo(seat) - if(!H.ammo || seat && seat != H.allowed_seat) + if(!H.ammo_type || seat && seat != H.allowed_seat) continue hps += H return hps diff --git a/code/modules/vehicles/multitile/multitile_interaction.dm b/code/modules/vehicles/multitile/multitile_interaction.dm index d3106ea8183c..d069dca5861f 100644 --- a/code/modules/vehicles/multitile/multitile_interaction.dm +++ b/code/modules/vehicles/multitile/multitile_interaction.dm @@ -373,9 +373,10 @@ RegisterSignal(user, COMSIG_MOB_MOUSEDOWN, PROC_REF(crew_mousedown)) RegisterSignal(user, COMSIG_MOB_MOUSEDRAG, PROC_REF(crew_mousedrag)) RegisterSignal(user, COMSIG_MOB_MOUSEUP, PROC_REF(crew_mouseup)) + RegisterSignal(user, COMSIG_MOB_MOUSEMOVE, PROC_REF(crew_mousemove)) /obj/vehicle/multitile/on_unset_interaction(mob/user) - UnregisterSignal(user, list(COMSIG_MOB_MOUSEUP, COMSIG_MOB_MOUSEDOWN, COMSIG_MOB_MOUSEDRAG)) + UnregisterSignal(user, list(COMSIG_MOB_MOUSEUP, COMSIG_MOB_MOUSEDOWN, COMSIG_MOB_MOUSEDRAG, COMSIG_MOB_MOUSEMOVE)) var/obj/item/hardpoint/hardpoint = get_mob_hp(user) if(hardpoint) @@ -399,6 +400,43 @@ hardpoint.change_target(source, src_object, over_object, src_location, over_location, src_control, over_control, params) + // BYOND doesn't send MouseMove while a button is held - MouseDrag takes over. Feed it into + // the same tracking patths so aiming doesn't freeze while the crew is firing/holding M1 + track_gunner_mouse(source, over_object, params) + track_driver_mouse(source, over_object, params) + +/// Relays live cursor position to the gunner's turret / driver's slaved secondary, if any, so they can track the mouse. +/obj/vehicle/multitile/proc/crew_mousemove(datum/source, atom/object, turf/location, control, params) + SIGNAL_HANDLER + track_gunner_mouse(source, object, params) + track_driver_mouse(source, object, params) + +/// Updates the gunner's turret (if any) to aim toward the given screen position. +/obj/vehicle/multitile/proc/track_gunner_mouse(mob/source, atom/object, params) + if(get_mob_seat(source) != VEHICLE_GUNNER) + return + + var/obj/item/hardpoint/holder/tank_turret/turret = locate() in hardpoints + if(!turret) + return + + turret.update_desired_angle(object, source, params) + +/// Updates the driver's slaved secondary (if any) to aim toward the given screen position. +/obj/vehicle/multitile/proc/track_driver_mouse(mob/source, atom/object, params) + if(get_mob_seat(source) != VEHICLE_DRIVER) + return + + var/obj/item/hardpoint/holder/tank_turret/turret = locate() in hardpoints + if(!turret) + return + + for(var/obj/item/hardpoint/secondary/secondary_weapon in turret.hardpoints) + if(!secondary_weapon.self_gimballed) + continue + secondary_weapon.update_desired_angle(object, source, params) + return + /// Checks for special control keybinds, else relays crew mouse press to active hardpoint. /obj/vehicle/multitile/proc/crew_mousedown(datum/source, atom/object, turf/location, control, params) SIGNAL_HANDLER diff --git a/code/modules/vehicles/multitile/multitile_movement.dm b/code/modules/vehicles/multitile/multitile_movement.dm index b4faf7f5ba9a..1e943e2b20f4 100644 --- a/code/modules/vehicles/multitile/multitile_movement.dm +++ b/code/modules/vehicles/multitile/multitile_movement.dm @@ -34,6 +34,8 @@ if(health <= 0) return FALSE + last_input_time = world.time + return pre_movement(direction) // This determines what type of movement to execute @@ -54,6 +56,9 @@ if(move_on_turn) try_move(direction) + if(success) + start_momentum_decay_if_needed() + return success // Attempts to execute the given movement input @@ -102,6 +107,7 @@ rotate_hardpoints(deg) rotate_entrances(deg) rotate_bounds(deg) + update_langchat_height() setDir(turn(dir, deg), TRUE) last_move_dir = dir @@ -121,10 +127,6 @@ // Increases/decreases the vehicle's momentum according to whether or not the user is steppin' on the gas or not /obj/vehicle/multitile/proc/update_momentum(direction) - // If we've stood still for long enough we go back to 0 momentum - if(world.time > next_move + move_delay*move_momentum_build_factor) - move_momentum = 0 - if(direction == dir) move_momentum = min(move_momentum + 1, move_max_momentum) else @@ -140,12 +142,12 @@ /obj/vehicle/multitile/proc/update_next_move() // 1/((m/M)*b) where m is momentum, M is max momentum and b is the build factor + //// move_momentum_build_factor seems to cancel itself out here. It's worth to revisit this section and maybe refactor it. var/anti_build_factor = 1/((max(abs(move_momentum), 1)/move_max_momentum) * move_momentum_build_factor) next_move = world.time + move_delay * move_momentum_build_factor * anti_build_factor * misc_multipliers["move"] l_move_time = world.time - // This just checks if the vehicle can physically move in the given direction /obj/vehicle/multitile/proc/can_move(direction) var/can_move = TRUE @@ -158,6 +160,11 @@ var/bound_height_tiles = bound_height / world.icon_size var/list/old_turfs = CORNER_BLOCK(min_turf, bound_width_tiles, bound_height_tiles) + for(var/turf/T as anything in old_turfs) + for(var/obj/structure/barricade/B in T) + if(!QDELETED(B) && B.last_bumped != world.time && (B.dir & direction)) + Collide(B) + var/turf/new_loc = get_step(src, direction) min_turf = locate(new_loc.x + bound_x_tiles, new_loc.y + bound_y_tiles, z) @@ -166,11 +173,31 @@ if(T in old_turfs) continue + // This first istype() check is probably a bad practice but it'll allow V-TOL to still enter... + // ... open_space turfs in case Tank Desant and VTOL get TM'd together. + if(istype(src, /obj/vehicle/multitile/tank) && istype(T, /turf/open_space)) + // early return so we skip crash behavior. + move_momentum = floor(move_momentum/2) + update_next_move() + return FALSE + if(!T.Enter(src)) can_move = FALSE + for(var/obj/structure/barricade/B in T) + if(!QDELETED(B) && B.last_bumped != world.time) + if(REVERSE_DIR(B.dir) & direction) + Collide(B) + + // any other tile-blocking items that weren't caught by !T.Enter + for(var/atom/A in T) + if(is_blocking_structure(A)) + can_move = FALSE + break + // Crashed with something that stopped us if(!can_move) + on_crash() move_momentum = floor(move_momentum/2) update_next_move() interior_crash_effect() @@ -307,3 +334,59 @@ return else cell_explosion(target, explosion_strength, explosion_falloff, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, cause_data) + +/** + * Base proc for defining special behavior when crashing + * + * on_crash is a proc meant to be overridable by child classes + * That way, all of the effects that happen when crashing are in the same spot. + */ +/obj/vehicle/multitile/proc/on_crash() + return + + +/** + * Starts the momentum decay loop if not already running and momentum exists. + * This should be called after any movement to ensure decay starts when player stops. + */ +/obj/vehicle/multitile/proc/start_momentum_decay_if_needed() + if(abs(move_momentum) > 0 && !momentum_decay_active) + momentum_decay_active = TRUE + spawn(0) + momentum_decay_loop() + +/** + * The main momentum decay loop - runs continuously while vehicle has momentum. + * Checks every second if the player has stopped giving input, and decays momentum accordingly. + * + * decay_interval controls how fast mometum will decay. By default, it decays every second. + * + * idle_time_required is how long you have to go without pressing a movement key. + * + * So, for example, if idle time is 20 and decay interval is 5, you will start losing momentum twice per second after you spend 2 seconds without moving. + * + */ +/obj/vehicle/multitile/proc/momentum_decay_loop() + var/decay_interval = 10 // Decay every 1 second + + while(abs(move_momentum) > 0) + sleep(decay_interval) + + var/time_since_input = world.time - last_input_time + if(time_since_input < idle_time_required) + continue + + var/momentum_abs = abs(move_momentum) + momentum_abs -= move_momentum_loss_factor + if(momentum_abs <= 0) + move_momentum = 0 + momentum_decay_active = FALSE + return + + if(move_momentum > 0) + move_momentum = momentum_abs + + else + move_momentum = -momentum_abs + + momentum_decay_active = FALSE diff --git a/code/modules/vehicles/multitile/multitile_verbs.dm b/code/modules/vehicles/multitile/multitile_verbs.dm index df53803f9f78..7df1f0897210 100644 --- a/code/modules/vehicles/multitile/multitile_verbs.dm +++ b/code/modules/vehicles/multitile/multitile_verbs.dm @@ -4,7 +4,7 @@ //Used to swap which module a position is using -//e.g. swapping primary gunner from the minigun to the smoke launcher +//e.g. swapping primary gunner from the minigun to the flare launcher /obj/vehicle/multitile/proc/switch_hardpoint() set name = "Change Active Hardpoint" set category = "Vehicle" @@ -242,6 +242,99 @@ return T.toggle_gyro(usr) +//toggles the artillery module's magnified optics for your own view - available to both driver and gunner +/obj/vehicle/multitile/proc/toggle_artillery_optics() + set name = "Toggle Artillery Optics" + set desc = "Toggles the artillery module's magnified optics for your own view." + set category = "Vehicle" + + var/mob/living/user = usr + if(!user || !istype(user)) + return + + var/obj/vehicle/multitile/vehicle = user.interactee + if(!istype(vehicle)) + return + + var/obj/item/hardpoint/support/artillery_module/AM = locate() in vehicle.hardpoints + if(!AM) + to_chat(user, SPAN_WARNING("No artillery module installed.")) + return + + AM.toggle_optics(user) + +//toggles the artillery module's night vision overlay for your own view - available to both driver and gunner +/obj/vehicle/multitile/proc/toggle_artillery_nvg() + set name = "Toggle Artillery Night Vision" + set desc = "Toggles the artillery module's night vision overlay for your own view." + set category = "Vehicle" + + var/mob/living/user = usr + if(!user || !istype(user)) + return + + var/obj/vehicle/multitile/vehicle = user.interactee + if(!istype(vehicle)) + return + + var/obj/item/hardpoint/support/artillery_module/AM = locate() in vehicle.hardpoints + if(!AM) + to_chat(user, SPAN_WARNING("No artillery module installed.")) + return + + AM.toggle_nvg(user) + +//hands control of the secondary hardpoint (aim + fire) between the gunner and the driver +/obj/vehicle/multitile/proc/toggle_slave_secondary_to_driver() + set name = "Slave Secondary to Driver" + set desc = "Toggles handing control of the secondary weapon's aim and fire between yourself and the driver." + set category = "Vehicle" + + var/mob/user = usr + if(!user || !istype(user)) + return + + var/obj/vehicle/multitile/vehicle = user.interactee + if(!istype(vehicle)) + return + + var/obj/item/hardpoint/holder/tank_turret/turret = locate() in vehicle.hardpoints + if(!turret) + return + + var/obj/item/hardpoint/secondary/secondary_weapon = null + for(var/obj/item/hardpoint/secondary/candidate in turret.hardpoints) + if(!candidate.is_activatable()) // passive hardpoints (e.g. the mounted flag) can't be slaved/selected + continue + secondary_weapon = candidate + break + if(!secondary_weapon) + to_chat(user, SPAN_WARNING("No secondary weapon installed.")) + return + + secondary_weapon.toggle_slaved_to_driver(usr) + +//toggles the fire mode for hardpoints. currently only used for the primary and secondary flamers to switch between glob and stream +/obj/vehicle/multitile/proc/toggle_hardpoint_fire_mode() + set name = "Toggle Hardpoint Fire Mode" + set desc = "Toggles the fire mode of your currently selected hardpoint, if it supports one." + set category = "Vehicle" + + var/mob/user = usr + if(!user || !istype(user)) + return + + var/obj/vehicle/multitile/vehicle = user.interactee + if(!istype(vehicle)) + return + + var/obj/item/hardpoint/selected_hardpoint = vehicle.get_mob_hp(user) + if(!selected_hardpoint) + to_chat(user, SPAN_WARNING("You have no hardpoint selected.")) + return + + selected_hardpoint.toggle_fire_mode(usr) + //single use verb that allows VCs to add a nickname in "" at the end of their vehicle name /obj/vehicle/multitile/proc/name_vehicle() set name = "Name Vehicle" diff --git a/code/modules/vehicles/tank/tank.dm b/code/modules/vehicles/tank/tank.dm index b462e31658ae..fc5a3818b769 100644 --- a/code/modules/vehicles/tank/tank.dm +++ b/code/modules/vehicles/tank/tank.dm @@ -35,6 +35,7 @@ move_max_momentum = 3 move_momentum_build_factor = 1.8 move_turn_momentum_loss_factor = 0.6 + idle_time_required = 20 vehicle_light_range = 7 @@ -76,6 +77,15 @@ explosive_resistance = 400 minimap_icon_state = "tank" + var/list/on_top_mobs = list() /// keeps track of all mobs currently atop the tank + var/list/on_top_obj = list() /// keeps track of all objs currently atop the tank + var/on_top_mobs_shooting_inaccuracy_time = 0 /// world_time must be bigger than this so mobs don't get penalized for shooting while atop the tank. + +/obj/vehicle/multitile/tank/update_next_move() + var/anti_build_factor = 1/((max(abs(move_momentum), 1)/move_max_momentum) * move_momentum_build_factor) + on_top_mobs_shooting_inaccuracy_time = world.time + move_delay * move_momentum_build_factor * anti_build_factor * misc_multipliers["move"] * 5 + . = ..() + /obj/vehicle/multitile/tank/initialize_cameras(change_tag = FALSE) if(!camera) camera = new /obj/structure/machinery/camera/vehicle(src) @@ -111,11 +121,20 @@ add_verb(M.client, list( /obj/vehicle/multitile/proc/toggle_door_lock, /obj/vehicle/multitile/proc/activate_horn, + /obj/vehicle/multitile/proc/cycle_hardpoint, + /obj/vehicle/multitile/proc/toggle_hardpoint_fire_mode, )) else if(seat == VEHICLE_GUNNER) add_verb(M.client, list( /obj/vehicle/multitile/proc/cycle_hardpoint, /obj/vehicle/multitile/proc/toggle_gyrostabilizer, + /obj/vehicle/multitile/proc/toggle_slave_secondary_to_driver, + /obj/vehicle/multitile/proc/toggle_hardpoint_fire_mode, + )) + if(seat == VEHICLE_DRIVER || seat == VEHICLE_GUNNER) + add_verb(M.client, list( + /obj/vehicle/multitile/proc/toggle_artillery_optics, + /obj/vehicle/multitile/proc/toggle_artillery_nvg, )) @@ -133,12 +152,28 @@ remove_verb(M.client, list( /obj/vehicle/multitile/proc/toggle_door_lock, /obj/vehicle/multitile/proc/activate_horn, + /obj/vehicle/multitile/proc/cycle_hardpoint, + /obj/vehicle/multitile/proc/toggle_hardpoint_fire_mode, )) else if(seat == VEHICLE_GUNNER) remove_verb(M.client, list( /obj/vehicle/multitile/proc/cycle_hardpoint, /obj/vehicle/multitile/proc/toggle_gyrostabilizer, + /obj/vehicle/multitile/proc/toggle_slave_secondary_to_driver, + /obj/vehicle/multitile/proc/toggle_hardpoint_fire_mode, )) + if(seat == VEHICLE_DRIVER || seat == VEHICLE_GUNNER) + remove_verb(M.client, list( + /obj/vehicle/multitile/proc/toggle_artillery_optics, + /obj/vehicle/multitile/proc/toggle_artillery_nvg, + )) + var/obj/item/hardpoint/support/artillery_module/AM = get_artillery_module() + AM?.clear_user_effects(M) + +/obj/vehicle/multitile/tank/proc/get_artillery_module() + for(var/obj/item/hardpoint/support/artillery_module/AM in hardpoints) + return AM + return null //Called when players try to move vehicle //Another wrapper for try_move() @@ -148,59 +183,463 @@ if(!(locate(/obj/item/hardpoint/locomotion/treads) in hardpoints)) return FALSE + // this if statement seems redundant with var/success = ..() below + // but it prevents a massive performance overhead: + // we only run transform math when we are ready to move... + // ...instead of every instant while a movement key is held + if(!(world.time < next_move)) + // PRE-STATE snapshot + var/list/oldc = _current_center() + var/old_cx = oldc[1] + var/old_cy = oldc[2] + var/old_dir = dir + + var/success = ..() + if(success) + // POST-STATE snapshot + var/list/newc = _current_center() + var/new_cx = newc[1] + var/new_cy = newc[2] + var/new_dir = dir + if(new_cx != old_cx || new_cy != old_cy || new_dir != old_dir) + _update_riders_after_motion(old_cx, old_cy, old_dir, new_cx, new_cy, new_dir) + revalidate_on_top() + return TRUE + + // Gunner turret aiming is mouse-driven now (see crew_mousemove() in multitile_interaction.dm). + return FALSE + +// !!!! No point in keeping this now that you can freely climb onto the tank. !!!! +// Unless, maybe, you end up stuck in the center (turret) tile. + +// /obj/vehicle/multitile/tank/MouseDrop_T(mob/dropped, mob/user) +// . = ..() +// if((dropped != user) || !isxeno(user)) +// return + +// if(health > 0) +// to_chat(user, SPAN_XENO("We can't jump over [src] until it is destroyed!")) +// return + +// var/turf/current_turf = get_turf(user) +// var/dir_to_go = get_dir(current_turf, src) +// for(var/i in 1 to 3) +// current_turf = get_step(current_turf, dir_to_go) +// if(!(current_turf in locs)) +// break + +// if(current_turf.density) +// to_chat(user, SPAN_XENO("The path over [src] is obstructed!")) +// return + +// // Now we check to make sure the turf on the other side of the tank isn't dense too +// current_turf = get_step(current_turf, dir_to_go) +// if(current_turf.density) +// to_chat(user, SPAN_XENO("The path over [src] is obstructed!")) +// return + +// to_chat(user, SPAN_XENO("We begin to jump over [src]...")) +// if(!do_after(user, 3 SECONDS, INTERRUPT_ALL, BUSY_ICON_HOSTILE)) +// to_chat(user, SPAN_XENO("We stop jumping over [src].")) +// return + +// user.forceMove(current_turf) +// to_chat(user, SPAN_XENO("We jump to the other side of [src].")) + +/** + * Applies a jousting effect when crashing above a certain speed. + * + * Because momentum defines how close a vehicle is to its top speed, 0.5 should mean that the tank will joust its riders... + * ...After it crashes at anything while above half of its max speed. + * + * Keep in mind that momentum decays while standing still, and it can be negative in case we are moving backwards. + */ +/obj/vehicle/multitile/tank/on_crash() + if(abs(move_momentum) < move_max_momentum * 0.5) + return + _scatter_riders_on_crash() + revalidate_on_top() + +/** + * Collided handles what happens when something bumps into the tank. + * + * If we are being bumped by our riders, we'll do nothing. + * Otherwise, we will attempt to climb if we pass through the edge cases on each if statement. + * + * Arguments: + * * atom/movable/AM - The atom bumping into the tank. + */ +/obj/vehicle/multitile/tank/Collided(atom/movable/AM) + if(_is_our_rider(AM)) + return + + if(!ismob(AM)) + if(istype(AM, /obj)) + var/obj/O = AM + if(O.buckled_mob) + Collided(O.buckled_mob) return ..() - if(user != seats[VEHICLE_GUNNER]) - return FALSE + var/mob/living/M = AM + if(!istype(M)) + return ..() + if(M.action_busy) + return + if(M.pulledby || M.throwing) + return + var/turf/facing_turf = get_step(get_turf(M), M.dir) + if(!facing_turf) + return ..() + if(M.resting) + M.forceMove(facing_turf) + return ..() + if(get_turf(M) in locs) + return - var/obj/item/hardpoint/holder/tank_turret/T = null - for(var/obj/item/hardpoint/holder/tank_turret/TT in hardpoints) - T = TT - break - if(!T) - return FALSE + climb_onto(M, facing_turf) + return - if(direction == GLOB.reverse_dir[T.dir] || direction == T.dir) - return FALSE +/** + * climb_onto is called by Collided to allow a mob to climb onto the tank. + * + * We run a number of edge case checks first, then we begin the process of climbing properly. + * It takes a few seconds or less, and, after that, if the spot is still reachable (AKA, if the tank hasn't driven away) + * then we'll climb up! + * + * We also call _carry_move_with_grabs to pull grabbed mobs in with us. + * ^^ Kind of how you can grab people when stepping into a window frame to pull them in. + * + * Arguments: + * * mob/living/user - The mob trying to get atop the tank + * * turf/preferred - The exact 'turf' of the tank we want to climb upon. + * * As in: Bumping into the middle tile of a side shouldn't make you climb up onto another tile. + */ +/obj/vehicle/multitile/tank/proc/climb_onto(mob/living/user, turf/preferred) + if(!istype(user)) + return + + if(user.action_busy) + return - T.user_rotation(user, turning_angle(T.dir, direction)) - update_icon() + if(!_validate_climb_target(user, preferred, TRUE)) + to_chat(user, SPAN_WARNING("You can't climb there.")) + return - return TRUE + user.visible_message( + SPAN_WARNING("[user] starts climbing onto [src]."), + SPAN_WARNING("You start climbing onto [src].")) -/obj/vehicle/multitile/tank/MouseDrop_T(mob/dropped, mob/user) - . = ..() - if((dropped != user) || !isxeno(user)) + var/climb_speed_factor = (move_momentum / move_max_momentum) // % of our max speed attained. + + if(health == 0) // When broken, everyone takes 0.20 seconds to climb up. Keep in mind that the real perceived value is around 2-3x higher when you take in account lag. + if(!do_after (user, 0.20 SECONDS, INTERRUPT_MOVED, BUSY_ICON_CLIMBING, numticks = 1)) + to_chat(user, SPAN_WARNING("You stop climbing onto [src].")) + return + + else if(isxeno(user)) // Xenos take longer to climb to avoid accidentally getting on top while slashing inside boiler gas. + // takes between 0.8 seconds when still and 2 seconds when at full speed. (You'd have to climb through one of the sides) OR pounce on it. + if(!do_after(user, max((2 * climb_speed_factor), 0.80) SECONDS, INTERRUPT_MOVED, BUSY_ICON_CLIMBING, numticks = 4)) + to_chat(user, SPAN_WARNING("You stop climbing onto [src].")) + return + + // Humans and preds climb up faster. Humans can't climb down instantly. + // takes between 0.25 seconds when still and 1.20 second when at full speed (You'd have to climb through one of the sides) + else if(!do_after(user, max((1.20 * climb_speed_factor), 0.30) SECONDS, INTERRUPT_MOVED, BUSY_ICON_CLIMBING, numticks = 3)) + to_chat(user, SPAN_WARNING("You stop climbing onto [src].")) + return + + if(!_validate_climb_target(user, preferred, TRUE)) + to_chat(user, SPAN_WARNING("The spot on [src] is no longer reachable.")) + return + + _carry_move_with_grabs(user, preferred, TRUE) + user.visible_message( + SPAN_WARNING("[user] climbs onto [src]."), + SPAN_WARNING("You climb onto [src].")) + return + +/** + * climb_down is called inside living.dm by _handle_tank_edge_move(), which is called by Move() + * + * This proc handles climbing down normally, and, if allowed to run, will result in a successful disembark. + * + * Climbing down was a bit more tricky to implement than climbing up, since we need to block movement into- + * what would otherwise be a 'free', open tile. + * + * The code is pretty self-explanatory. Of note is the fact that we only interrupt on an incapacitation. This is to allow + * easier disembarking even when the tank is moving. + * Arguments: + * * mob/living/user - The mob trying to get down the tank + * * turf/target - The exact 'turf' of the tank we want to climb down onto. + */ +/obj/vehicle/multitile/tank/proc/climb_down(mob/living/user, turf/target) + if(user.action_busy) return - if(health > 0) - to_chat(user, SPAN_XENO("We can't jump over [src] until it is destroyed!")) + if(!_validate_climb_target(user, target, FALSE)) return - var/turf/current_turf = get_turf(user) - var/dir_to_go = get_dir(current_turf, src) - for(var/i in 1 to 3) - current_turf = get_step(current_turf, dir_to_go) - if(!(current_turf in locs)) - break + var/climb_speed_factor = (move_momentum / move_max_momentum) // % of our max speed attained. + + user.visible_message( + SPAN_WARNING("[user] starts climbing down from [src]."), + SPAN_WARNING("You start climbing down from [src].")) - if(current_turf.density) - to_chat(user, SPAN_XENO("The path over [src] is obstructed!")) + if(!ishuman(user)) // Xenos and preds can always climb down with almost no delay to avoid situations where they get stuck atop the tank. + // The tiny delay here is meant to prevent fast castes from accidentally disembarking from a misinput + if(!do_after(user, 0.20 SECONDS, INTERRUPT_INCAPACITATED, BUSY_ICON_GENERIC, numticks = 1)) + to_chat(user, SPAN_WARNING("You stop climbing down from [src].")) return - // Now we check to make sure the turf on the other side of the tank isn't dense too - current_turf = get_step(current_turf, dir_to_go) - if(current_turf.density) - to_chat(user, SPAN_XENO("The path over [src] is obstructed!")) + // Humans take at most 0.9 seconds to climb down from a tank at full speed, and 0.30 when standing still. + else if(!do_after(user, max((0.9 * climb_speed_factor), 0.30) SECONDS, INTERRUPT_INCAPACITATED, BUSY_ICON_GENERIC, numticks = 3)) + to_chat(user, SPAN_WARNING("You stop climbing down from [src].")) return - to_chat(user, SPAN_XENO("We begin to jump over [src]...")) - if(!do_after(user, 3 SECONDS, INTERRUPT_ALL, BUSY_ICON_HOSTILE)) - to_chat(user, SPAN_XENO("We stop jumping over [src].")) + + // edge case where tank moves while we're climbing down + if(!_validate_climb_target(user, target, FALSE)) + to_chat(user, SPAN_WARNING("The spot is no longer reachable.")) return + _carry_remove_with_grabs(user, target) + user.visible_message( + SPAN_WARNING("[user] climbs down from [src]."), + SPAN_WARNING("You climb down from [src].")) + return + +/** + * mark_on_top marks mobs as riders of a specific tank. + * + * This proc does three things: + * + * It adds a mob to the tank's list of passengers (on_top_mobs) + * It attaches a /datum/component/tank_rider to the mob, marking us as the tank it's atop of + * It calls _apply_rider_visuals() to set the layer atop the tank's + * + * If a xeno is hidden, it gets un-hidden. + * + * Arguments: + * * mob/living/rider_mob - The mob being marked ontop. + */ +/obj/vehicle/multitile/tank/proc/mark_on_top(mob/living/rider_mob) + if(!istype(rider_mob)) + return + if(rider_mob.z != z) + return + if(!(get_turf(rider_mob) in locs)) + return + if(rider_mob.get_tank_on_top_of() == src) + return + on_top_mobs |= rider_mob + rider_mob.AddComponent(/datum/component/tank_rider, src) + _apply_rider_visuals(rider_mob) + + if(isxeno(rider_mob)) + var/mob/living/carbon/xenomorph/X = rider_mob + if(X.layer == XENO_HIDING_LAYER) + var/datum/action/xeno_action/onclick/xenohide/hide = get_action(X, /datum/action/xeno_action/onclick/xenohide) + if(hide) + hide.remove_hide_status() // prevents cheesing the layer system + +/** + * obj_mark_on_top WOULD be an ad-hoc polymorph of mark_on_top. It does the same thing, but with an obj/ type. + * + * This proc does three things: + * + * Checks if the obj can be brought atop a tank. + * It adds the obj to the tank's list of objs (on_top_objs) + * It attaches a /datum/component/tank_rider to the obj, marking us as the tank it's atop of + * It calls _obj_apply_rider_visuals() to set the layer atop the tank's + * + * Arguments: + * * obj/rider_obj - The obj being marked ontop. + */ +/obj/vehicle/multitile/tank/proc/obj_mark_on_top(obj/rider_obj) + if(!istype(rider_obj)) + return + if(!rider_obj.is_allowed_atop_vehicle) + return + if(rider_obj.z != z) + return + if(!(get_turf(rider_obj) in locs)) + return + if(rider_obj.get_tank_on_top_of() == src) + rider_obj.layer = TANK_RIDER_OBJ_LAYER // prevents a visual bug with layering + return + on_top_obj |= rider_obj + rider_obj.AddComponent(/datum/component/tank_rider, src) + rider_obj.layer = TANK_RIDER_OBJ_LAYER + if(istype(rider_obj, /obj/structure/closet/bodybag)) + var/obj/structure/closet/bodybag/BB = rider_obj + if(BB.roller_buckled) + BB.pixel_y = BB.buckle_offset + 12 + else + BB.pixel_y = initial(rider_obj.pixel_y) + 12 + else + rider_obj.pixel_y = initial(rider_obj.pixel_y) + 12 + +/** + * clear_on_top removes rider effects from a mob who was previously atop the tank. + * + * This proc resets the layer and plane of a rider. + * This proc removes a rider from the on_top_mobs list of the tank. + * This proc removes the mob's /datum/component/tank_rider. + * + * Arguments: + * * mob/living/rider_mob - The mob who has disembarked. + */ +/obj/vehicle/multitile/tank/proc/clear_on_top(mob/living/rider_mob) + if(!istype(rider_mob)) + return + on_top_mobs -= rider_mob + qdel(rider_mob.GetComponent(/datum/component/tank_rider)) + rider_mob.layer = initial(rider_mob.layer) + rider_mob.plane = initial(rider_mob.plane) + rider_mob.pixel_y = initial(rider_mob.pixel_y) + +/** + * obj_clear_on_top removes rider effects from an obj atop the tank. + * + * This behaves as an ad-hoc polymorphic version of clear_on_top. + */ +/obj/vehicle/multitile/tank/proc/obj_clear_on_top(obj/rider_obj) + if(!istype(rider_obj)) + return + on_top_obj -= rider_obj + qdel(rider_obj.GetComponent(/datum/component/tank_rider)) + rider_obj.layer = initial(rider_obj.layer) + if(istype(rider_obj, /obj/structure/closet/bodybag)) + var/obj/structure/closet/bodybag/BB = rider_obj + if(BB.roller_buckled) + BB.pixel_y = BB.buckle_offset + else + BB.pixel_y = initial(rider_obj.pixel_y) + else + rider_obj.pixel_y = initial(rider_obj.pixel_y) + +/** + * Destroy proc. This shouldn't normally be called, but just in case. + * + * This proc ensures all mobs atop the tank are cleared. + * we also cut the on_top_mobs and on_top_obj list, just in case DM doesn't have garbage collection. + * + */ +/obj/vehicle/multitile/tank/Destroy() + for(var/mob/living/M in on_top_mobs.Copy()) + if(M) + clear_on_top(M) + for(var/obj/O in on_top_obj.Copy()) + if(O) + obj_clear_on_top(O) + on_top_mobs.Cut() + on_top_obj.Cut() + return ..() + +/** + * revalidate_on_top checks if a mob is still atop the tank. + * + * This proc goes through all of the mobs in the tank's on_top_mobs list and either: + * - removes them from the list if they're not on the same tile anymore + * - refreshes rider visuals + * + * This proc probably doesn't need to be called, and you might be able to get away with removing it if it's a performance issue. + * It's just there for safety. + */ +/obj/vehicle/multitile/tank/proc/revalidate_on_top() + for(var/mob/living/M in on_top_mobs.Copy()) + if(!M || M.z != z || !(get_turf(M) in locs)) + clear_on_top(M) + else + _apply_rider_visuals(M) + +/** + * BlockedPassDirs allows us to move atop the tank if we're on top of it. + * + * You might've noticed that this method is in UpperCamelCase instead of snake_case. + * This is because we're overriding a method from /atom. + * + * This proc checks if the atom mover is a mob, and, if it is, allows it to move freely atop the tank. + * + * * Arguments: + * * atom/movable/mover - The atom attempting to move in a tank turf. + * * target_dir - The direction we're moving towards. + */ +/obj/vehicle/multitile/tank/BlockedPassDirs(atom/movable/mover, target_dir) + if(ismob(mover)) + var/mob/living/mover_mob = mover + if(istype(mover_mob) && mover_mob.get_tank_on_top_of() == src) + var/turf/start = get_turf(mover_mob) + var/turf/target = get_step(start, target_dir) + if(target && (target in src.locs)) + return NO_BLOCKED_MOVEMENT + else if(isobj(mover)) + var/obj/mover_obj = mover + if(mover_obj.is_atop_vehicle()) + var/turf/start = get_turf(mover_obj) + var/turf/target = get_step(start, target_dir) + if(target && (target in src.locs)) + return NO_BLOCKED_MOVEMENT + var/ret = ..() + if(ret != null) + return ret + return null + +/** + * take_damage_type handles taking damage when the tank gets attacked. + * + * Because xenos can now hop atop the tank to attack it in a fairly one-sided display of force, I've introduced + * this function to make destroying the tank a bit slower. This might be removed if/once someone (i) get(s) around to + * making tank hardpoints repairable. + * + * Xenos atop the tank are flat-out unable to hit the Treads module. This is to allow the tank to run back + * towards marines. Also, it's impossible to break the treads from ontop. How are you going to reach them? + * + * Xenos atop the tank also suffer a 30% damage penalty, again, to give time for tank crewmembers to retreat to + * safety. + * + * * Arguments: + * * damage - Numeric value of damage. + * * type - Damage type. Brute, Slash, Acid... + * * atom/attacker - Whoever is damaging the tank + */ +/obj/vehicle/multitile/tank/take_damage_type(damage, type, atom/attacker) + if(type == "slash" && istype(attacker, /mob/living/carbon/xenomorph)) + var/mob/living/carbon/xenomorph/attacker_xeno = attacker + + // must actually be on top of THIS tank (exterior), same z, and stading on one of our tiles + if(attacker_xeno.get_tank_on_top_of() == src && attacker_xeno.z == z) + var/turf/xturf = get_turf(attacker_xeno) + if(xturf && (xturf in src.locs)) + var/adj_damage = damage * 0.7 + var/all_broken = TRUE + var/dmg_multi = get_dmg_multi(type) + + for(var/obj/item/hardpoint/H in hardpoints) + if(istype(H, /obj/item/hardpoint/locomotion/treads)) + continue + // turret doesn't count as a 'hardpoint' for all_broken, otherwise the tank is... too tanky. + if(istype(H, /obj/item/hardpoint/holder/tank_turret)) + H.take_damage(floor(adj_damage * dmg_multi)) + continue + if(H.can_take_damage()) + H.take_damage(floor(adj_damage * dmg_multi)) + all_broken = FALSE + + // If all *eligible* hardpoints are broken, the frame begins taking full damage... + // ...otherwise, the frame takes one tenth, just like the base behavior. + if(all_broken) + health = max(0, health - adj_damage * dmg_multi) + else + health = max(0, health - floor(adj_damage * dmg_multi / 10)) + if(ismob(attacker)) + var/mob/M = attacker + log_attack("[src] took [adj_damage] [type] damage (top-hit penalty) from [M] ([M.client ? M.client.ckey : "disconnected"]).") + else + log_attack("[src] took [adj_damage] [type] damage (top-hit penalty) from [attacker].") + update_icon() + return + return ..() - user.forceMove(current_turf) - to_chat(user, SPAN_XENO("We jump to the other side of [src].")) /* ** PRESETS SPAWNERS */ diff --git a/code/modules/vehicles/tank/tank_helpers.dm b/code/modules/vehicles/tank/tank_helpers.dm new file mode 100644 index 000000000000..3bee83afa067 --- /dev/null +++ b/code/modules/vehicles/tank/tank_helpers.dm @@ -0,0 +1,595 @@ +// --- Helper for crash response + +/** + * _scatter_riders_on_crash joustles mobs riding atop the tank in a random direction. + * + * This proc applies a 1.5s WEAKEN effect to all mobs atop it when the tank crashes into something with sufficient speed. + * This includes large mobs such as ravagers, queens, kings, et-cetera. + * + * If a mob happens to be thrown OFF of a tank tile, they will instead get a 3 second stun. + * ^^ See Move() function inside living.dm + * + * The intent of this proc is double: Allowing tank drivers to defend themselves and have a chance against xenos + * atop the tank and also punish tank drivers for bad driving by fucking up the marines on top. + * + * Also scatters objs on top! + * + */ +/obj/vehicle/multitile/tank/proc/_scatter_riders_on_crash() + if(abs(move_momentum) < 1.6) + return + + var/sweep_range = 3 + var/src_z = src.z + var/list/src_locs = src.locs + + for(var/obj/O in on_top_obj.Copy()) + if(!O || O.z != src_z) + obj_clear_on_top(O) + continue + + var/turf/start = get_turf(O) + var/throw_dir = get_dir(src, start) + if(!throw_dir) + throw_dir = pick(GLOB.cardinals) + + var/turf/target = get_step(start, throw_dir) + if(!target || (target in src_locs) || target.density) + step_away(O, src, sweep_range, 3) + var/turf/cur = get_turf(O) + target = get_step(cur, throw_dir) + + if(target && !(target in src_locs) && !target.density) + obj_clear_on_top(O) + O.throw_atom(target, sweep_range, SPEED_FAST) + else if(!(start in src_locs)) + obj_clear_on_top(O) + + for(var/mob/living/M in on_top_mobs.Copy()) + if(!M || M.z != src_z) + clear_on_top(M) + continue + + var/turf/start = get_turf(M) + var/throw_dir = get_dir(src, start) + if(!throw_dir) + throw_dir = pick(GLOB.cardinals) + + var/turf/next_out = get_step(start, throw_dir) + + if(next_out && !(next_out in src_locs) && !next_out.density) + M.forceMove(next_out) + clear_on_top(M) + else + step_away(M, src, sweep_range, 3) + var/turf/cur = get_turf(M) + var/turf/next2 = get_step(cur, throw_dir) + if(next2 && !(next2 in src_locs) && !next2.density) + M.forceMove(next2) + clear_on_top(M) + + to_chat(M, SPAN_WARNING("You're thrown from [src]!")) + playsound(M, "punch", 25, TRUE) + shake_camera(M, 2, 1) + + var/turf/final_pos = get_turf(M) + + if(!(final_pos in src_locs)) + M.apply_effect(3, WEAKEN) + clear_on_top(M) + continue + + var/list/hull_neighbors = list() + for(var/d in GLOB.cardinals) + var/turf/H = get_step(final_pos, d) + if(H && (H in src_locs) && !H.density) + hull_neighbors += H + + if(hull_neighbors.len) + var/turf/spot = pick(hull_neighbors) + M.forceMove(spot) + M.apply_effect(1.5, WEAKEN) + mark_on_top(M) + +// --- motion geometry and transform math + +/** + * _current_center() builds a matrix of the tank's tiles and returns the center pair. + * + * The tank occupies a 3x3 set of tiles. We can imagine it as a matrix: + * + *(10,20) (11,20) (12,20) + *(10,21) (11,21) (12,21) + *(10,22) (11,22) (12,22) + * + * This proc gets the middle (1,1) element of the matrix and returns it, in this case, (11,21) + * + * Returns: + * * A LIST of two integer elements. [11,21] + */ +/obj/vehicle/multitile/tank/proc/_current_center() + var/first = TRUE + var/minx + var/miny + var/maxx + var/maxy + for(var/turf/T in locs) + if(first) + minx = maxx = T.x + miny = maxy = T.y + first = FALSE + else + if(T.x < minx) + minx = T.x + if(T.x > maxx) + maxx = T.x + if(T.y < miny) + miny = T.y + if(T.y > maxy) + maxy = T.y + + return list(minx + 1, miny + 1) + +/** + * calculates how many 90-degree rotations are needed to go from one cardinal direction to another. + * + * NORTH → EAST = +1 (90° CW) + * NORTH → WEST = -1 (90° CCW) + * NORTH → SOUTH = +2 (180°) + * EAST → SOUTH = +1 (90° CW) + * SOUTH → WEST = +1 (90° CW) + * + * This proc is a key part of the positioning system when the vehicle rotates. + * + * Arguments: + * * old_dir = The direction we are initially at + * * new_dir = The direction we want to be at + * + * Returns: + * * 0 : No rotation needed + * * 1 : 90° clockwise rotation + * * -1: 90° counter-clockwise rotation + * * 2 : 180° rotation. Will never happen because the tank can't turn over like that in one move. + */ +/obj/vehicle/multitile/tank/proc/_quarter_turns(old_dir, new_dir) + if(old_dir == new_dir) + return 0 + switch(old_dir) + if(NORTH) + if(new_dir == EAST) + return 1 + if(new_dir == WEST) + return -1 + if(new_dir == SOUTH) + return 2 + if(SOUTH) + if(new_dir == WEST) + return 1 + if(new_dir == EAST) + return -1 + if(new_dir == NORTH) + return 2 + if(EAST) + if(new_dir == SOUTH) + return 1 + if(new_dir == NORTH) + return -1 + if(new_dir == WEST) + return 2 + if(WEST) + if(new_dir == NORTH) + return 1 + if(new_dir == SOUTH) + return -1 + if(new_dir == EAST) + return 2 + return 0 + +/** + * _rotated_offset takes an offset (dx, dy) and rotates it by 90% degree increments + * + * this proc applies 2D coordinate rotation mathematics to transform a position offset + * based on the rotation amount calculated by _quarter_turns. + * + * Arguments: + * * dx = x axis position, relative to the tank + * * dy = y axis position, relative to the tank + * * k = a numeric value acquired from _quarter_turns to know how many turns we have to do + * + * Returns: + * * a LIST, containing a 2-slot permutation of {{dx, -dx} {dy, -dy}} depending on K. + */ +/obj/vehicle/multitile/tank/proc/_rotated_offset(dx, dy, k) + if(!k) + return list(dx, dy) + if(k == 2) + return list(-dx, -dy) // 180° + if(k == 1) + return list(dy, -dx) // CW 90° + // k == -1 (CCW 90°) + return list(-dy, dx) + +/** + * _update_riders_after_motion coordinates moving all mobs atop the tank whenever it moves or rotates. + * + * This proc is the main coordination function that handles moving all riders atop the tank. + * It combines translation, rotation, and collision detection to keep riders properly positioned on the hull. + * + * First it calls _quarter_turns to understand what kind of rotations it'll need to turn from one orientation to the other. + * Then it calls _rotated_offset to know where to position the riders. + * Lastly, it applies those changes. + * + * Arguments: + * * old_cx = the X axis position currently occupied by the tank + * * old_cy = the y axis position currently occupied by the tank + * * old_dir = the direction the tank is currently facing. N/E/S/W + * * new_cx = the X axis position that we are moving to + * * new_cy = the y axis position that we are moving to + * * new_dir = the direction we want to face. N/E/S/W + * + */ +/obj/vehicle/multitile/tank/proc/_update_riders_after_motion(old_cx, old_cy, old_dir, new_cx, new_cy, new_dir) + var/k = _quarter_turns(old_dir, new_dir) // -1,0,1,2 + var/src_z = src.z + var/list/src_locs = src.locs + + // simpler version for objs + for(var/obj/O in on_top_obj.Copy()) + if(!O || O.z != src_z) + obj_clear_on_top(O) + continue + + // Don't move body bags atop roller beds. We will handle those when we move the roller beds themselves. + if(istype(O, /obj/structure/closet/bodybag)) + var/obj/structure/closet/bodybag/BB = O + if(BB.roller_buckled) + continue + + var/turf/from = get_turf(O) + if(!from) + obj_clear_on_top(O) + continue + + var/list/rd = _rotated_offset(from.x - old_cx, from.y - old_cy, k) + var/turf/target = locate(new_cx + rd[1], new_cy + rd[2], src_z) + + if(target && (target in src_locs) && target != from) + O.forceMove(target) + obj_mark_on_top(O) + if(istype(O, /obj/structure/bed/roller)) + var/obj/structure/bed/roller/R = O + if(R.buckled_bodybag) + R.buckled_bodybag.forceMove(target) + obj_mark_on_top(R.buckled_bodybag) + else if(from in src_locs) + obj_mark_on_top(O) + else + obj_clear_on_top(O) + + for(var/mob/living/M in on_top_mobs.Copy()) + if(!M || M.z != src_z) + clear_on_top(M) + continue + + // If the mob is buckled to something, we'll skip it, because the buckled obj has already moved with it. + if(M.buckled) + continue + var/turf/from = get_turf(M) + if(!from) + clear_on_top(M) + continue + + // Offset from OLD center -> rotate by k -> translate to NEW center + var/list/rd = _rotated_offset(from.x - old_cx, from.y - old_cy, k) + var/turf/target = locate(new_cx + rd[1], new_cy + rd[2], src_z) + + // this shouldn't happen, but just to be safe. + if(!target || !(target in src_locs)) + if(from in src_locs) + mark_on_top(M) + else + clear_on_top(M) + continue + + // If target is blocked by a non-rider dense atom, (shouldn't happen cuz tank runs over shit) keep if possible + var/blocked = target.density // Start with turf density check + if(!blocked) + for(var/atom/A in target) + if(A != src && A.density && (!ismob(A) || !(A in on_top_mobs))) + blocked = TRUE + break + + if(blocked) + if(from in src_locs) + mark_on_top(M) + else + clear_on_top(M) + continue + + // Move and keep elevation + if(target != from) + M.forceMove(target) + mark_on_top(M) + +// --- Helper for layering effects + +/** + * simply brings a rider's layer atop the tanks. + * + * TANK_RIDER_LAYER being at 4.51 is not arbitrary. + * The tank chassis is set at 4.1, but the turret is at 4.5. + * Therefore, 4.51 is needed to avoid clipping. + * + * Arguments: + * * mob/living/M = Mob whose layer priority is being increased. + */ +/obj/vehicle/multitile/tank/proc/_apply_rider_visuals(mob/living/M) + M.layer = TANK_RIDER_LAYER + M.pixel_y = M.old_y + 12 + +// --- helpers for handlign interactions with grabbed mobs + +/** + * This proc allows a marine to pull another one up the tank once he finishes climbing. + * + * _carry_move_with_grabs does something similar to grabbing someone as you climb a window frame: + * You'll pull the marine you're grabbing up with you as soon as YOUR climb_onto finishes. + * The marine pulled up will get a 2 second weaken, but they can be shaked out of it. + * + * !!!! The order of the operations of clearing, marking and moving is not arbitrary !!!! + * + * Arguments: + * * mob/living/user = Mob doing the pulling. + * * turf/dest = Which turf on the tank we're moving to + */ +/obj/vehicle/multitile/tank/proc/_carry_move_with_grabs(mob/living/user, turf/dest) + var/list/grabbed_things = list() + for(var/obj/item/grab/G in list(user.l_hand, user.r_hand)) + if(G?.grabbed_thing) + grabbed_things += G.grabbed_thing + + for(var/atom/movable/thing as anything in grabbed_things) + if(isliving(thing)) + var/mob/living/L = thing + L.apply_effect(2, WEAKEN) + _add_to_top_buckled_to(L, dest) + L.forceMove(dest) + mark_on_top(L) + + else if(isobj(thing)) + var/obj/O = thing + if(O.is_allowed_atop_vehicle) + _add_to_top_buckled_entity(O, dest) + + user.forceMove(dest) + mark_on_top(user) + +/** + * Handles the addition of a roller bed whom a Mob is buckled to onto the top of the tank + * + * Mobs can be buckled to roller beds and pulled atop the tank. If this happens, we need to take the roller bed up with us + * + * Arguments: + * * mob/living/L = The mob that may be buckled to a roller bed. + * * turf/dest = Which turf onto the tank we're moving to + */ + +/obj/vehicle/multitile/tank/proc/_add_to_top_buckled_to(mob/living/L, turf/dest) + if(L.buckled && istype(L.buckled, /obj/structure/bed/roller)) + var/obj/structure/bed/roller/R = L.buckled + R.forceMove(dest) + obj_mark_on_top(R) + +/** + * Handles the addition of entities buckled to an obj to atop the tank + * + * Roller beds can have mobs and body or stasis bags attached to it. If we are pulling a roller bed onhto the tank with an attached... + * bag or mob, we should ensure those go up with it correctly. + * + * Arguments: + * * obj/object = The object that may have living beings buckled to it + * * turf/dest = Which turf onto the tank we're moving to + */ + +/obj/vehicle/multitile/tank/proc/_add_to_top_buckled_entity(obj/object, turf/dest) + if(istype(object, /obj/structure/bed/roller)) + var/obj/structure/bed/roller/R = object + + R.forceMove(dest) + obj_mark_on_top(R) + if(R.buckled_mob) + var/mob/living/L = R.buckled_mob + L.forceMove(dest) + mark_on_top(L) + else if (R.buckled_bodybag) + var/obj/structure/closet/bodybag/BB = R.buckled_bodybag + BB.forceMove(dest) + obj_mark_on_top(BB) + else if (istype(object, /obj/structure/closet/bodybag)) // Grab is on a bodybag, possibly atop a roller. + var/obj/structure/closet/bodybag/BB = object + + if (BB.roller_buckled) + var/obj/structure/bed/roller/R = BB.roller_buckled + R.forceMove(dest) + obj_mark_on_top(R) // mark ASAP to prevent bodybag from unbuckling + BB.forceMove(dest) + obj_mark_on_top(BB) + + else // lone bodybag + BB.forceMove(dest) + obj_mark_on_top(BB) + + else // any other obj + object.forceMove(dest) + obj_mark_on_top(object) + +/** + * This proc allows a marine to pull another one DOWN the tank once he finishes climbing down. + * + * Opposite of carry_move_with_grabs. This one does the same thing but for climbing DOWN. + * + * !!!! The order of the operations of clearing, marking and moving is not arbitrary !!!! + * + * Arguments: + * * mob/living/user = Mob doing the pulling. + * * turf/dest = Which turf on the tank we're moving to + */ +/obj/vehicle/multitile/tank/proc/_carry_remove_with_grabs(mob/living/user, turf/dest) + var/list/grabbed_things = list() + for(var/obj/item/grab/G in list(user.l_hand, user.r_hand)) + if(G?.grabbed_thing) + grabbed_things += G.grabbed_thing + + for(var/atom/movable/thing as anything in grabbed_things) + if(isliving(thing)) // GRAB is on MOB + var/mob/living/L = thing + _remove_from_top_buckled_to(L, dest) + clear_on_top(L) + L.forceMove(dest) + + else if(isobj(thing)) // GRAB is on OBJ + var/obj/O = thing + if(O.is_allowed_atop_vehicle) + _remove_from_top_buckled_entity(O, dest) + + clear_on_top(user) + user.forceMove(dest) + +/** + * Handles the removal of a roller bed whom a Mob is buckled to off the top of the tank + * + * Mobs can be buckled to roller beds and pulled off of the tank. If this happens, we need to take the roller bed down with us + * + * Arguments: + * * mob/living/L = The mob that may be buckled to a roller bed. + * * turf/dest = Which turf off the tank we're moving to + */ + +/obj/vehicle/multitile/tank/proc/_remove_from_top_buckled_to(mob/living/L, turf/dest) + if(L.buckled && istype(L.buckled, /obj/structure/bed/roller)) + var/obj/structure/bed/roller/R = L.buckled + R.forceMove(dest) + obj_clear_on_top(R) + +/** + * Handles the removal of entities buckled to an obj from atop the tank + * + * Roller beds can have mobs and body or stasis bags attached to it. If we are pulling a roller bed down the tank with an attached... + * bag or mob, we should ensure those go down with it correctly. + * + * Arguments: + * * obj/object = The object that may have living beings buckled to it + * * turf/dest = Which turf off the tank we're moving to + */ + +/obj/vehicle/multitile/tank/proc/_remove_from_top_buckled_entity(obj/object, turf/dest) + if(istype(object, /obj/structure/bed/roller)) + var/obj/structure/bed/roller/R = object + + if(R.buckled_mob) + var/mob/living/L = R.buckled_mob + obj_clear_on_top(object) // clear first then move to prevent buckled mobs from getting debuffed for improperly dismounting. + object.forceMove(dest) + clear_on_top(L) + + else if (R.buckled_bodybag) + var/obj/structure/closet/bodybag/BB = R.buckled_bodybag + object.forceMove(dest) + obj_clear_on_top(object) + BB.forceMove(dest) // must be moved last to avoid unbuckling + obj_clear_on_top(BB) + + else // empty roller bed + object.forceMove(dest) + obj_clear_on_top(object) + + else if (istype(object, /obj/structure/closet/bodybag)) // Grab is on a bodybag, possibly atop a roller. + var/obj/structure/closet/bodybag/BB = object + + if (BB.roller_buckled) + var/obj/structure/bed/roller/R = BB.roller_buckled + R.forceMove(dest) + obj_clear_on_top(R) + BB.forceMove(dest) // must be moved last to avoid unbuckling + obj_clear_on_top(BB) + + else // lone bodybag + object.forceMove(dest) + obj_clear_on_top(object) + + else // any other item + object.forceMove(dest) + obj_clear_on_top(object) + +// --- blocking helpers. Checks if turf is blocked, etc. + +/** + * This proc checks a specific turf and returns false if anything dense is on it, except for a mob. + * + * _blocked_except_mobs is a helper that check if a turf/T has anything dense in it. + * If there is something dense in it and it is NOT a mob, it returns TRUE, otherwise, it returns false. + * + * Arguments: + * * turf/T = Which turf we're checking for dense atoms in + * + * Returns: + * * TRUE = If something dense is present, and it is not a mob. + * * FALSE = If the turf is clear of dense objects, or if the only dense objects in it are mobs. + */ +/obj/vehicle/multitile/tank/proc/_blocked_except_mobs(turf/T) + if(!istype(T)) + return TRUE + if(T.density) + return TRUE + for(var/atom/A in T) + if(A == src) + continue + if(ismob(A)) + continue + if(A.density) + return TRUE + return FALSE + +/** + * This proc checks if a specific turf/spot is a valid climbing target + * + * We are mostly interested in ensuring that the tank hasn't moved too far away, and this function helps with that. + * It also has a couple of other edge cases such as being in a different Z, or having a climb-spot blocked by + * anything other than a mob, juuuuust to be safe. + * + * Arguments: + * * turf/spot = Which turf we want to climb onto + * * mob/living/user = Whoever wants to climb onto spot + * + * Returns: + * * TRUE = If nothing is blocking the turf + * * FALSE = If something is blocking the turf. + */ +/obj/vehicle/multitile/tank/proc/_validate_climb_target(mob/living/user, turf/spot) + if(!istype(user) || !istype(spot)) + return FALSE + if(user.z != z) + return FALSE + if(get_dist(spot, get_turf(user)) != 1) + return FALSE + if(_blocked_except_mobs(spot)) + return FALSE + return TRUE + +/** + * Helper function to know if a mob is a rider of this specific tank + * + * Checks if this tank is the tank the given mob's /datum/component/tank_rider is tracking. + * + * Arguments: + * * atom/movable/AM = whatever mob/atom we want to check if its a rider. + * + * Returns: + * * TRUE = If AM is a mob/living and this tank is what its tank_rider component points to. + * * FALSE = If AM is not a mob/living, or if this tank isn't what its tank_rider component points to. + */ +/obj/vehicle/multitile/tank/proc/_is_our_rider(atom/movable/AM) + if(!ismob(AM)) + return FALSE + var/mob/living/rider_mob = AM + return istype(rider_mob) && rider_mob.get_tank_on_top_of() == src diff --git a/colonialmarines.dme b/colonialmarines.dme index c92d4825079d..706807b60cae 100644 --- a/colonialmarines.dme +++ b/colonialmarines.dme @@ -460,6 +460,7 @@ #include "code\datums\components\speed_modifier.dm" #include "code\datums\components\status_effect_component.dm" #include "code\datums\components\tacmap.dm" +#include "code\datums\components\tank_rider.dm" #include "code\datums\components\temporary_mute.dm" #include "code\datums\components\toxin_buildup.dm" #include "code\datums\components\tracker_bullets.dm" @@ -2112,6 +2113,7 @@ #include "code\modules\mob\living\brain\MMI.dm" #include "code\modules\mob\living\brain\say.dm" #include "code\modules\mob\living\carbon\carbon.dm" +#include "code\modules\mob\living\carbon\carbon_debug_verbs.dm" #include "code\modules\mob\living\carbon\carbon_defines.dm" #include "code\modules\mob\living\carbon\carbon_helpers.dm" #include "code\modules\mob\living\carbon\give.dm" @@ -2651,6 +2653,7 @@ #include "code\modules\vehicles\hardpoints\hardpoint_ammo\cupola_ammo.dm" #include "code\modules\vehicles\hardpoints\hardpoint_ammo\dualcannon_ammo.dm" #include "code\modules\vehicles\hardpoints\hardpoint_ammo\firing_port_weapon_ammo.dm" +#include "code\modules\vehicles\hardpoints\hardpoint_ammo\flare_ammo.dm" #include "code\modules\vehicles\hardpoints\hardpoint_ammo\flare_launcher_ammo.dm" #include "code\modules\vehicles\hardpoints\hardpoint_ammo\gl_ammo.dm" #include "code\modules\vehicles\hardpoints\hardpoint_ammo\hardpoint_ammo.dm" @@ -2658,7 +2661,6 @@ #include "code\modules\vehicles\hardpoints\hardpoint_ammo\minigun_ammo.dm" #include "code\modules\vehicles\hardpoints\hardpoint_ammo\primary_flamer_ammo.dm" #include "code\modules\vehicles\hardpoints\hardpoint_ammo\secondary_flamer_ammo.dm" -#include "code\modules\vehicles\hardpoints\hardpoint_ammo\smoke_ammo.dm" #include "code\modules\vehicles\hardpoints\hardpoint_ammo\tow_ammo.dm" #include "code\modules\vehicles\hardpoints\holder\holder.dm" #include "code\modules\vehicles\hardpoints\holder\tank_turret.dm" @@ -2669,7 +2671,9 @@ #include "code\modules\vehicles\hardpoints\primary\ltb.dm" #include "code\modules\vehicles\hardpoints\primary\minigun.dm" #include "code\modules\vehicles\hardpoints\primary\primary.dm" +#include "code\modules\vehicles\hardpoints\secondary\brute_launcher.dm" #include "code\modules\vehicles\hardpoints\secondary\cupola.dm" +#include "code\modules\vehicles\hardpoints\secondary\flag.dm" #include "code\modules\vehicles\hardpoints\secondary\flamer.dm" #include "code\modules\vehicles\hardpoints\secondary\frontal_cannon.dm" #include "code\modules\vehicles\hardpoints\secondary\grenade_launcher.dm" @@ -2693,6 +2697,7 @@ #include "code\modules\vehicles\interior\interior_hull.dm" #include "code\modules\vehicles\interior\interior_landmarks.dm" #include "code\modules\vehicles\interior\interactable\doors.dm" +#include "code\modules\vehicles\interior\interactable\intercom.dm" #include "code\modules\vehicles\interior\interactable\seats.dm" #include "code\modules\vehicles\interior\interactable\vehicle_locker.dm" #include "code\modules\vehicles\interior\interactable\vendors.dm" @@ -2706,6 +2711,7 @@ #include "code\modules\vehicles\multitile\multitile_verbs.dm" #include "code\modules\vehicles\tank\interior.dm" #include "code\modules\vehicles\tank\tank.dm" +#include "code\modules\vehicles\tank\tank_helpers.dm" #include "code\modules\vehicles\van\box_van.dm" #include "code\modules\vehicles\van\clf_van.dm" #include "code\modules\vehicles\van\interior.dm" diff --git a/icons/obj/items/weapons/guns/ammo_by_faction/USCM/vehicles.dmi b/icons/obj/items/weapons/guns/ammo_by_faction/USCM/vehicles.dmi index 612d4cc9f43a..ed1df2927236 100644 Binary files a/icons/obj/items/weapons/guns/ammo_by_faction/USCM/vehicles.dmi and b/icons/obj/items/weapons/guns/ammo_by_faction/USCM/vehicles.dmi differ diff --git a/icons/obj/vehicles/hardpoints/tank.dmi b/icons/obj/vehicles/hardpoints/tank.dmi index c9af30849eb8..eb6512e61629 100644 Binary files a/icons/obj/vehicles/hardpoints/tank.dmi and b/icons/obj/vehicles/hardpoints/tank.dmi differ diff --git a/icons/obj/vehicles/tank.dmi b/icons/obj/vehicles/tank.dmi index 1991bc9caa70..9ec706743234 100644 Binary files a/icons/obj/vehicles/tank.dmi and b/icons/obj/vehicles/tank.dmi differ diff --git a/maps/interiors/tank.dmm b/maps/interiors/tank.dmm index e5c019e01993..e36eead70aab 100644 --- a/maps/interiors/tank.dmm +++ b/maps/interiors/tank.dmm @@ -13,7 +13,13 @@ phone_category = "Vehicles"; phone_id = "M34A2 Longstreet Light Tank"; pixel_x = 22; - pixel_y = -14 + pixel_y = -6 + }, +/obj/effect/landmark/interior/spawn/intercom{ + pixel_x = 22; + pixel_y = -15; + dir = 8; + drag_delay = 3.05 }, /turf/open/shuttle/vehicle/floor_1_12, /area/interior/vehicle/tank) @@ -67,7 +73,8 @@ alpha = 50; icon_state = "exterior_2"; layer = 5.2; - pixel_y = 32 + pixel_y = 32; + pixel_x = 0 }, /turf/open/void/vehicle, /area/space) diff --git a/maps/map_files/Tyrargo_Rift/Tyrargo_Rift.dmm b/maps/map_files/Tyrargo_Rift/Tyrargo_Rift.dmm index cf030992a157..f80ac950d5e5 100644 --- a/maps/map_files/Tyrargo_Rift/Tyrargo_Rift.dmm +++ b/maps/map_files/Tyrargo_Rift/Tyrargo_Rift.dmm @@ -56373,8 +56373,8 @@ /area/tyrargo/oob/outdoors) "cKo" = ( /obj/structure/surface/rack, -/obj/item/ammo_magazine/hardpoint/turret_smoke, -/obj/item/ammo_magazine/hardpoint/turret_smoke, +/obj/item/storage/box/packet/flare, +/obj/item/storage/box/packet/flare, /obj/item/ammo_magazine/hardpoint/m56_cupola, /obj/item/ammo_magazine/hardpoint/m56_cupola, /turf/open/auto_turf/tyrargo_grass/layer0_mud, diff --git a/tgui/packages/tgui/interfaces/TankPivotTuner.tsx b/tgui/packages/tgui/interfaces/TankPivotTuner.tsx new file mode 100644 index 000000000000..329cd3258533 --- /dev/null +++ b/tgui/packages/tgui/interfaces/TankPivotTuner.tsx @@ -0,0 +1,115 @@ +import type { BooleanLike } from 'common/react'; +import { useBackend } from 'tgui/backend'; +import { Button, LabeledList, Section, Stack } from 'tgui/components'; +import { Window } from 'tgui/layouts'; + +type Weapon = { + ref: string; + name: string; + pivot_x: number; + pivot_y: number; + selected: BooleanLike; +}; + +type Data = { + dir_name: string; + weapons: Weapon[]; + selected_name: string; + tuning_label: string; + pivot_x: number; + pivot_y: number; + offset_x: number; + offset_y: number; + is_gimballed: BooleanLike; + gimbal_pivot_x: number; + gimbal_pivot_y: number; +}; + +const AXIS_STEPS = [-10, -1, 1, 10]; + +const AxisRow = (props: { + readonly axis: 'x' | 'y'; + readonly value: number; + readonly target: 'rotation' | 'gimbal' | 'offset'; +}) => { + const { act } = useBackend(); + const { axis, value, target } = props; + return ( + + + {AXIS_STEPS.map((step) => ( + + + + ))} + + + ); +}; + +export const TankPivotTuner = (props) => { + const { act, data } = useBackend(); + const { + dir_name, + weapons = [], + selected_name, + tuning_label, + pivot_x, + pivot_y, + offset_x, + offset_y, + is_gimballed, + gimbal_pivot_x, + gimbal_pivot_y, + } = data; + + return ( + + +
+ + {weapons.map((weaponEntry) => ( + + + + ))} + +
+ {selected_name && ( +
+ + + + +
+ )} + {selected_name && ( +
+ + + + +
+ )} + {selected_name && is_gimballed && ( +
+ + + + +
+ )} +
+
+ ); +};