diff --git a/code/__DEFINES/client_prefs.dm b/code/__DEFINES/client_prefs.dm index d2e35f039322..a370571de69c 100644 --- a/code/__DEFINES/client_prefs.dm +++ b/code/__DEFINES/client_prefs.dm @@ -71,6 +71,21 @@ // NOTE: Don't add flags past 1<<23, it'll break things due to BYOND limitations. //================================================= +//toggles_vehicle bits from /datum/preferences +//================================================= +///Vehicle picks its own gear automatically instead of the driver shifting manually. +#define VEHICLE_SIMPLE_TRANSMISSION (1<<0) +///Vehicle controls use legacy WASD movement instead of tank-relative gas/brake/turn. +#define VEHICLE_SIMPLE_CONTROLS (1<<1) +///Vehicle acceleration uses keypress-driven momentum instead of the continuous cruise loop +#define VEHICLE_SIMPLE_ACCELERATION (1<<2) +///Driver HUD shows speed/acceleration in mph instead of the default km/h +#define VEHICLE_UNITS_MPH (1<<3) + +///Default toggles_vehicle value. +#define TOGGLES_VEHICLE_DEFAULT 0 +//================================================= + #define JOB_SLOT_RANDOMISED_SLOT -1 #define JOB_SLOT_CURRENT_SLOT 0 #define JOB_SLOT_RANDOMISED_TEXT "Randomise name and appearance" diff --git a/code/__DEFINES/conflict.dm b/code/__DEFINES/conflict.dm index 1603bba39fc4..ca487396cb75 100644 --- a/code/__DEFINES/conflict.dm +++ b/code/__DEFINES/conflict.dm @@ -34,7 +34,7 @@ /// Whether or not the bullet hits the target that was clicked or if it keeps travelling #define AMMO_HITS_TARGET_TURF (1<<17) #define AMMO_ALWAYS_FF (1<<18) -// (1<<19) unused, previously was AMMO_HOMING +#define AMMO_PASSES_OVER_VEHICLES (1<<19) /// Can't be deflected #define AMMO_NO_DEFLECT (1<<20) ///Can only hit people with criminal status @@ -209,6 +209,16 @@ #define ANTISTRUCT_DMG_MULT_WALL 2.5 #define ANTISTRUCT_DMG_MULT_TANK 1.5 +/// Acid spits and acid shotgun were too weak against tanks, so multiply damage and wound chance. +#define ACID_SPIT_TANK_DAMAGE_MULT 6 +#define ACID_SHOTGUN_TANK_DAMAGE_MULT 6 +#define ACID_SHOTGUN_TANK_WOUND_CHANCE_MULT 6 +#define ACID_SPIT_TANK_WOUND_CHANCE_MULT 6 +/// Another acid shotgun tweak to make it less shit against vehicles. +#define ACID_SHOTGUN_TANK_EFFECTIVE_PENETRATION 10 +/// Multiplies bone chips' wound chances. They still deal very little damage. +#define BONE_CHIPS_TANK_WOUND_CHANCE_MULT 2 + // human armor #define CLOTHING_ARMOR_NONE 0 #define CLOTHING_ARMOR_VERYLOW 5 diff --git a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm index 04afcd3beb90..d0fa9bdf55cd 100644 --- a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm +++ b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm @@ -1,5 +1,7 @@ ///from base of mob/set_stat(): (new_stat, old_stat) #define COMSIG_MOB_STATCHANGE "mob_statchange" +/// Sent when the player toggles a vehicle preference. +#define COMSIG_MOB_VEHICLE_PREFS_CHANGED "mob_vehicle_prefs_changed" /// From /obj/structure/machinery/door/airlock/proc/take_damage #define COMSIG_MOB_DESTROY_AIRLOCK "mob_destroy_airlock" @@ -143,6 +145,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 89393d1f8e55..43611132fc9e 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 3c48bb26ac00..69b3dfdd3a17 100644 --- a/code/__DEFINES/keybinding.dm +++ b/code/__DEFINES/keybinding.dm @@ -236,6 +236,11 @@ #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 COMSIG_KB_VEHICLE_SHIFT_GEAR_UP "keybinding_vehicle_shift_gear_up" +#define COMSIG_KB_VEHICLE_SHIFT_GEAR_DOWN "keybinding_vehicle_shift_gear_down" +#define COMSIG_KB_VEHICLE_TOGGLE_CRUISE_CONTROL "keybinding_vehicle_toggle_cruise_control" +#define COMSIG_KB_VEHICLE_TOGGLE_TURRET_SAFETY "keybinding_vehicle_toggle_turret_safety" #define CATEGORY_CLIENT "CLIENT" #define CATEGORY_EMOTE "EMOTE" @@ -247,7 +252,7 @@ #define CATEGORY_HUMAN_INVENTORY "HUMAN INVENTORY" #define CATEGORY_ROBOT "ROBOT" #define CATEGORY_YAUTJA "YAUTJA" -#define CATEGORY_VEHICLE "Vehicles" +#define CATEGORY_VEHICLE "VEHICLE" #define CATEGORY_MISC "MISC" #define CATEGORY_MOVEMENT "MOVEMENT" #define CATEGORY_COMMUNICATION "COMMUNICATION" diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm index 8f1afc5130b6..6200b12647df 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 +#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 a barricade currently covered by a vehicle on the same tile. +#define VEHICLE_COVERED_BARRICADE_LAYER 4.52 + /// For WEATHER -#define WEATHER_LAYER 4.14 +#define WEATHER_LAYER 5 //#define FLY_LAYER 5 diff --git a/code/__DEFINES/objects.dm b/code/__DEFINES/objects.dm index 33b6c3d1af88..ae4cf83652b3 100644 --- a/code/__DEFINES/objects.dm +++ b/code/__DEFINES/objects.dm @@ -172,6 +172,8 @@ GLOBAL_LIST_INIT(RESTRICTED_CAMERA_NETWORKS, list( //Those networks can only be #define FLAME_REAGENT_USE_AMOUNT 1 +#define FLAME_MOUNT_AUTO "flame_mount_auto" + #define CLEANABLE_SPLATTER "splatter" // Vomit, oil #define CLEANABLE_BLOOD "blood" #define CLEANABLE_BLOOD_SPLATTER "blood splatter" diff --git a/code/__DEFINES/vehicle.dm b/code/__DEFINES/vehicle.dm index d40e89826eff..3714ae449209 100644 --- a/code/__DEFINES/vehicle.dm +++ b/code/__DEFINES/vehicle.dm @@ -7,12 +7,273 @@ #define HDPT_TURRET "turret" #define HDPT_SPECIAL "special" //special pre-installed hardpoints with unique behaviour +// Tank Desant logistics parts +// traction, movement, acceleration, power generation +#define HDPT_ENGINE "engine" +// fuel levels & fuel pump +#define HDPT_FUEL_TANK "fuel tank" +// engine heat management, cooling of the fighting compartment +#define HDPT_RADIATOR "radiator" +// engine ignition; internal climatization; IFF, hatch locks, visual sensors, turret ring drive, air filtering +#define HDPT_BATTERY "battery" +// blocks movement into the tank. When destroyed, xenos can go in and out almost instantly. +#define HDPT_HATCH "hatch" +// can be interacted with to change IFF mode like turrets. When broken, weapons lose projectile IFF +#define HDPT_IFF_MODULE "iff module" +// external cameras. Can be hawk tuah'd by spitters and praes to obscure. Requires constant cleaning +#define HDPT_VISUAL_SENSORS "visual sensors" +// turret rotation speed +#define HDPT_TURRET_RING "turret ring" +// protects against boiler clouds & smoke from entering the tank +#define HDPT_AIR_FILTER "air filter" +// front-mounted plow +#define HDPT_SNOWPLOW "snowplow" +// Not a real install slot. Lets the UA flag use its own wound text instead of gun-part wording. +#define HDPT_UA_FLAG "ua flag" + #define HDPT_LAYER_WHEELS 0.01 // so it appears below xenomorphs and other mobs #define HDPT_LAYER_SUPPORT 2 #define HDPT_LAYER_ARMOR 3 #define HDPT_LAYER_TURRET 4 #define HDPT_LAYER_MAX 4 +/// Fixed view radius for a seated driver/gunner/passenger +#define VEHICLE_SEAT_VIEW_RADIUS 8 + +// Gear transmission +/// Torque floor so a wrecked engine/treads combo can still crawl. +#define GEAR_MIN_TORQUE_FRACTION 0.15 +/// Gear 1's own min-power floor +#define GEAR_1_MIN_POWER_FRACTION 0.6 +/// Gear 2's own min-power floor +#define GEAR_2_MIN_POWER_FRACTION 0.45 +// Reverse's own power floor. Made a little stronger than gear 2 so Crewmen can retreat more easily. +#define GEAR_REVERSE_MIN_POWER_FRACTION 0.6 +/// Below this fraction of nominal torque, is_underpowered() reports TRUE. +#define GEAR_UNDERPOWERED_FRACTION 0.5 +/// Engine and ttreads performance only degrades below this health level. +#define DEGRADE_GRACE_THRESHOLD_PCT 95 +/// Speed floor (tiles/sec) to avoid a divide-by-zero when scheduling the next move. +#define VEHICLE_MIN_CRAWL_SPEED 0.05 +/// Deciseconds between each movement loop (gear_movement_loop()) progress check. +#define GEAR_MOVEMENT_POLL_INTERVAL 1 +/// Flat deceleration (tiles/sec^2) applied by the brake key, independent of gear. +#define VEHICLE_BRAKE_DECELERATION 1.5 +/// Traction malus applied to a turf weeded at WEED_LEVEL_WEAK. +#define WEED_TRACTION_CAP_WEAK 0.8 +/// Traction malus for WEED_LEVEL_STANDARD. +#define WEED_TRACTION_CAP_STANDARD 0.6 +/// Traction malus for WEED_LEVEL_HARDY. +#define WEED_TRACTION_CAP_HARDY 0.55 +/// Traction malus for WEED_LEVEL_HIVE. +#define WEED_TRACTION_CAP_HIVE 0.4 +/// How much of a turf's traction deviation from neutral (1.0) a vehicle actually feels. +#define TRACTION_DAMPENING_TREADS 0.4 +#define TRACTION_DAMPENING_WHEELS 1.0 +/// Fuel burn multiplier for reagents other than the vehicle's intended fuel (welding fuel, cooking oil, etc etc). +#define OFF_LABEL_FUEL_PENALTY 1.5 +/// Units/sec a fuel tank or radiator drains its own contents at once fully destroyed. +#define HARDPOINT_BUSTED_LEAK_RATE 100 +/// Extra blunt damage a knockback-shoved tank takes when slammed into a wall. +#define KNOCKBACK_WALL_SLAM_DAMAGE 15 +/// Fixed physics tick (deciseconds) gear_cruise_loop() runs on. +#define GEAR_TICK_INTERVAL 2 +/// How long (deciseconds) a gas/brake key event still counts as "held". +#define GEAR_INPUT_GRACE_PERIOD 3 +/// Default cruise control granularity as a fraction of the top gear's max_speed. +#define CRUISE_CONTROL_DEFAULT_GRANULARITY_FRACTION (1/3) +/// Extra fuel (units) burned per (tile/sec) of speed changed in one tick, on top of baseline cruising cost. +#define ACCEL_FUEL_PER_SPEED_DELTA 0.3 +/// Fuel (units/sec) burned just from the engine idling. +#define ENGINE_IDLE_FUEL_USE 0.1 +/// How much of top_speed gear 1 (low range) targets. +#define GEAR_1_SPEED_FRACTION 0.5 +/// How much of top_speed gear 2 (low range) targets. +#define GEAR_2_SPEED_FRACTION 0.75 +/// Fraction of D gear's own stopping distance that gear 2 is tuned to reach its target speed within. +#define GEAR_2_DISTANCE_FRACTION (1/3) +/// Coefficient (1/sec) of speed-scaled drag decay applied while a gear-transmission vehicle has momentum. +#define SPEED_DECAY_COEFFICIENT 0.2 +/// Coefficient (1/sec) of the decay applied to drift_speed (Complex acceleration only). +#define DRIFT_DECAY_COEFFICIENT 1.2 +/// Fraction of top_speed below which a 90-degree turn creates no drift at all. +#define DRIFT_MIN_SPEED_FRACTION 0.8 + +// Hardpoint status HUD (west-side vertical icon stack, see multitile_hardpoint_hud.dm) +/// Deciseconds between each hardpoint HUD refresh. +#define HARDPOINT_HUD_UPDATE_INTERVAL 5 +/// Deciseconds each color holds during a 3-wound slot's blinking outline. +#define HARDPOINT_HUD_BLINK_HALF_PERIOD 5 +/// Pixel offset the hardpoint HUD's icon stack sits at. +#define HARDPOINT_HUD_PIXEL_X -24 +/// Vertical pixel offset applied on top of the icon stack's own centered layout. +#define HARDPOINT_HUD_PIXEL_Y_OFFSET 64 +/// tankhud.dmi's own sprite size. +#define HARDPOINT_HUD_ICON_SIZE 16 +/// Color a slot's icon renders as once destroyed or missing. +#define HARDPOINT_HUD_DESTROYED_COLOR "#404040" + +// Crew HUD (see tank_hud.dm) +/// Deciseconds between each crew HUD text refresh while a driver/gunner is seated. +#define CREW_HUD_UPDATE_INTERVAL 3 +/// Assumed real-world tile size (meters) for the driver HUD's display-only speed readout. +#define VEHICLE_HUD_METERS_PER_TILE 2 +/// Artistic-license multiplier so the driver HUD's displayed top speed reads like a real light tank. Wouln't make sense if a 22 century light tank cralwed at 20kmh, would it? +#define VEHICLE_HUD_SPEED_DISPLAY_MULT (10/3) +/// (tiles/sec) * this = km/h. +#define VEHICLE_HUD_KMH_PER_TILE (VEHICLE_HUD_METERS_PER_TILE * 3.6 * VEHICLE_HUD_SPEED_DISPLAY_MULT) +/// (tiles/sec) * this = mph. +#define VEHICLE_HUD_MPH_PER_TILE (VEHICLE_HUD_METERS_PER_TILE * 2.23694 * VEHICLE_HUD_SPEED_DISPLAY_MULT) + +// Engine temperature/overheat model +/// Kelvin above which the engine hardpoint starts taking overheat condition damage. +#define ENGINE_OVERHEAT_THRESHOLD (T20C + 60) +/// Heat generated (Kelvin/sec) per unit of nominal gear torque under full load. +#define ENGINE_HEAT_PER_LOAD 20 +/// Baseline heat generated (Kelvin/sec) just from the engine running. +#define ENGINE_HEAT_IDLE 1 +/// Torque-equivalent used for heat generation while revving in Park/Neutral. +#define ENGINE_REV_TORQUE_EQUIVALENT 0.1 +/// Fuel (units/sec) burned while actively revving in Park/Neutral. +#define ENGINE_REV_FUEL_USE 0.08 +/// Newton's-law-of-cooling coefficient (1/sec) at 100% radiator integrity and coolant. +#define ENGINE_RADIATOR_COOLING_COEFFICIENT 0.5 +/// Cooling coefficient (1/sec) with no radiator installed, or one fully wrecked/dry. +#define ENGINE_PASSIVE_COOLING_COEFFICIENT 0.05 +/// Condition damage/sec applied to the engine hardpoint while above ENGINE_OVERHEAT_THRESHOLD. +#define ENGINE_OVERHEAT_DAMAGE_PER_SEC 15 +/// Hard temperature ceiling. Reaching it forces an emergency engine shutdown. (200%) +#define ENGINE_OVERHEAT_SHUTDOWN_THRESHOLD (T20C + 120) +/// How long (deciseconds) after the engine turns on before it can build any new torque. +#define ENGINE_SPINUP_TIME (3 SECONDS) +/// Kelvin within which engine_temperature counts as "setttled" near ambient. +#define ENGINE_TEMPERATURE_SETTLE_THRESHOLD 0.5 +/// Extra heat (Kelvin/sec) added per flame mounted atop the hull. +#define ENGINE_HEAT_PER_MOUNTED_FLAME_TILE 5 +/// Extra heat (Kelvin/sec) added per flame burning on a turf the tank occupies without being mounted. +#define ENGINE_HEAT_PER_UNDERNEATH_FLAME_TILE (ENGINE_HEAT_PER_MOUNTED_FLAME_TILE * 0.25) + +// Overheat wound rolls +/// How often check_overheat_wound_trigger() actually rolls. +#define ENGINE_OVERHEAT_WOUND_ROLL_INTERVAL (1 SECONDS) +/// Temperature threshold above which the engine can gain a fresh Tier 1 (Blown Gasket) wound. +#define ENGINE_OVERHEAT_WOUND_TIER1_THRESHOLD_PCT 125 +/// Percent chance per roll of gaining a Tier 1 wound in the Tier 1 heat band. +#define ENGINE_OVERHEAT_WOUND_TIER1_CHANCE_PCT 5 +/// Temperature threshold above which the engine can advance to Tier 2 (Cracked Block). +#define ENGINE_OVERHEAT_WOUND_TIER2_THRESHOLD_PCT 150 +/// Percent chance per roll of gaining a Tier 1 wound in the Tier 2 heat band. +#define ENGINE_OVERHEAT_WOUND_TIER1_CHANCE_PCT_HIGH 10 +/// Percent chance per roll of an existing Tier 1 wound advancing to Tier 2. +#define ENGINE_OVERHEAT_WOUND_TIER2_ADVANCE_CHANCE_PCT 5 + +// Battery draw/recharge +/// How often (deciseconds) the battery charge/drain loop ticks. +#define BATTERY_TICK_INTERVAL 10 +/// Charge (units/sec) restored to the battery while the engine is on. +#define BATTERY_RECHARGE_RATE 10 +/// Baseline power draw (units/sec) for the IFF module. +#define IFF_MODULE_POWER_DRAW 0.5 +/// Baseline power draw (units/sec) for visual sensors (external cameras). +#define VISUAL_SENSORS_POWER_DRAW 0.5 +/// Baseline power draw (units/sec) for the air filter. +#define AIR_FILTER_POWER_DRAW 0.5 +/// Baseline power draw (units/sec) for the turret ring while idle. +#define TURRET_RING_IDLE_POWER_DRAW 0.2 +/// Power draw (units/sec) for the turret ring while actively rotating. +#define TURRET_RING_ROTATING_POWER_DRAW 2 + +// Simple acceleration mode +/// Number of discrete notches between 0 and a gear's max_speed under Simple acceleration mode. +#define SIMPLE_ACCEL_STEP_COUNT 4 +/// Deciseconds of no accelerate/brake input before speed_notch starts passively decaying. +#define SIMPLE_ACCEL_IDLE_GRACE 10 +/// Deciseconds between each passive decay step once idle. +#define SIMPLE_ACCEL_DECAY_INTERVAL 5 + +// Turn signals +/// Deciseconds between blink toggles for the turn signal overlay. +#define TURN_SIGNAL_BLINK_INTERVAL 5 +/// Deciseconds the turn signal's relay sound plays after being (re)activated. +#define TURN_SIGNAL_SOUND_DURATION 50 + +// Tank engine sound +/// Playback frequency at a standstill/idle. +#define ENGINE_SOUND_MIN_FREQUENCY 24000 +/// Playback frequency at the current gear's max_speed. +#define ENGINE_SOUND_MAX_FREQUENCY 44000 +/// Volume at a standstill/idle. +#define ENGINE_SOUND_MIN_VOLUME 30 +/// Volume at the current gear's max_speed. +#define ENGINE_SOUND_MAX_VOLUME 55 +/// Minimum tile range the outside-hearer copy is audible at, even at idle. +#define ENGINE_SOUND_MIN_RANGE 9 +/// How fast engine_rev_level (0-1) climbs per second while revving in Park/Neutral. +#define ENGINE_REV_RISE_RATE (1 / 1.5) +/// How fast engine_rev_level decays back down per second once gas is released. +#define ENGINE_REV_DECAY_RATE (1 / 2.5) + +// Engine exhaust smoke +/// Tint for exhaust smoke triggered by engine_cracked_block (Brute). +#define ENGINE_SMOKE_COLOR_BRUTE_WOUND "#d9d9d9" +/// Tint for exhaust smoke triggered by Fouled Carburator (tier-1 engine_fouled_injector). +#define ENGINE_SMOKE_COLOR_FOULED_CARBURATOR "#262626" +/// Tint for exhaust smoke triggered by Acid Intake (tier-2 engine_fouled_injector). +#define ENGINE_SMOKE_COLOR_ACID_WOUND "#1f2a12" +/// Tint for exhaust smoke from running on off-label fuel (welding fuel/cornoil). +#define ENGINE_SMOKE_COLOR_OFF_LABEL_FUEL "#8c8c8c" +/// Percent of a fuel ttank's total volume an off-label reagent needs before it smokes at all. +#define VEHICLE_FUEL_SMOKE_THRESHOLD_PCT 51 +/// Minimum seconds between exhaust smoke puffs from cornoil alone over the threshold. +#define ENGINE_SMOKE_INTERVAL_COOKING_OIL 8 SECONDS +/// Minimum seconds between exhaust smoke puffs from welding fuel alone over the threshold. +#define ENGINE_SMOKE_INTERVAL_WELDING_FUEL 3 SECONDS +/// Minimum seconds between exhaust smoke puffs from an active engine wound. +#define ENGINE_SMOKE_INTERVAL_WOUND 1.5 SECONDS +/// Exhaust smoke spread/linger amount for the default (off-label fuel/no wound) case. +#define ENGINE_SMOKE_AMOUNT_DEFAULT 2 +/// Exhaust smoke spread/linger amount for an actve engine wound. +#define ENGINE_SMOKE_AMOUNT_WOUND 4 + +/// Minimum seconds between a Boiler glob re-checking a non-tank vehicle it's sitting on. +#define VEHICLE_GAS_EXPOSURE_INTERVAL 8 SECONDS + +/// Minimum seconds between a Boiler glob re-checking a tank it's sitting on. +#define TANK_GLOB_EXPOSURE_INTERVAL 1 SECONDS + +/// Percent of a tank part's own remaining health dealt per full-strength acid glob tick. +#define TANK_GLOB_ACID_HEALTH_PCT 12 + +/// Flat minimum damage per full-strength acid glob tick. +#define TANK_GLOB_ACID_FLOOR 12 + +/// Multiplies a neuro-foulable slot's own wound chance for an AOE neurotoxin glob tick. +#define TANK_GLOB_NEURO_CHANCE_MULT 4 + +/// Ceiling on the per-tick neuro wound chance after TANK_GLOB_NEURO_CHANCE_MULT is applied. +#define TANK_GLOB_NEURO_CHANCE_CAP_PCT 95 + +/// Fraction of a bridged interior gas cloud that gets through a lightly damaged air filter. +#define AIR_FILTER_LEAK_FRACTION_TIER_1 0.08 +/// Fraction of a bridged interior gas cloud that gets through a badly damaged air filter. +#define AIR_FILTER_LEAK_FRACTION_TIER_2 0.35 +/// Filter integrity% below which gas leak-through starts climbing within its current wound tier. +#define AIR_FILTER_LEAK_HEALTH_THRESHOLD_PCT 75 +/// Alpha a bridged interior gas cloud needs before it's thick enough to block sight. +#define AIR_FILTER_LEAK_OPACITY_ALPHA_THRESHOLD 128 + +/// Minimum seconds between separate Acid Mine detonations hitting the same vehicle. +#define ACID_MINE_VEHICLE_DAMAGE_COOLDOWN 2 SECONDS + +/// Minimum seconds between a lingering acid puddle re-applying its hit to the same vehicle. +#define LINGERING_ACID_VEHICLE_DAMAGE_COOLDOWN 3 SECONDS + +/// Fraction of a vehicle's speed/momentum retained after driving over a Trapper Boiler trap. +#define BOILER_TRAP_VEHICLE_MOMENTUM_RETAINED 0.85 + +/// Minimum collision-damage multiplier a gear-transmission vehicle can hit for, even near a standstill. +#define COLLISION_SPEED_SCALE_FLOOR 0.2 + #define VEHICLE_DRIVER "driver" #define VEHICLE_GUNNER "primary gunner" #define VEHICLE_SUPPORT_GUNNER_ONE "1st support gunner" @@ -20,7 +281,7 @@ #define VEHICLE_SPEED_STATIC 5000 //500 seconds per tile, while not actually static, it's much better than adding check for each movement attempt. #define VEHICLE_SPEED_SLOW 30 //3 seconds per tile -#define VEHICLE_SPEED_NORMAL 10 //default 1 second per tile +#define VEHICLE_SPEED_NORMAL 10 //default 1 second per ttile #define VEHICLE_SPEED_FASTNORMAL 7 #define VEHICLE_SPEED_FAST 5 //half a second per tile #define VEHICLE_SPEED_FASTER 4 @@ -50,6 +311,17 @@ #define VEHICLE_CLASS_LIGHT (1<<2) //light class armor (APC, tank) #define VEHICLE_CLASS_MEDIUM (1<<3) //medium class armor (tank) #define VEHICLE_CLASS_HEAVY (1<<4) //heavy class armor (tank) + +// Vehicle-vs-vehicle ramming +/// Fraction of the ramming vehicle's own top speed needed for a collision to deal any damage/knockback. +#define VEHICLE_COLLISION_MOMENTUM_THRESHOLD 0.5 +/// Knockback distance (tiles) a collision deals at the ramming vehicle's own full top speed. +#define VEHICLE_COLLISION_MAX_KNOCKBACK_TILES 3 +/// Damage a collision deals at the ramming vehicle's own full top speed. +#define VEHICLE_COLLISION_MAX_DAMAGE 60 +/// Fraction of its own momentum a heavier vehicle bleeds off crashing into a lighter one. +#define VEHICLE_SOFT_CRASH_MOMENTUM_LOSS 0.25 + // Other vehicle flags /// Vehicle can bypass vehicle blockers, typically going further into maps than intended #define VEHICLE_BYPASS_BLOCKERS (1<<5) @@ -73,3 +345,186 @@ #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 rotation loop stops ticking. +#define ROTATION_SETTLE_TOLERANCE 0.5 +/// How close current_angle needs to be to target before a turret-mounted weapon can fire. +#define FIRING_GATE_TOLERANCE 5 +/// Degrees the turret can drift off a rangefinder target before Ctrl+Click rangefinding aborts. +#define RANGEFINDER_TURRET_ARC_TOLERANCE 10 +/// Baseline turn speed (degrees/decisecond) for a turret at TURRET_ARC_NORMALIZATION traverse_arc. +#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. +#define TURRET_MIN_ANGULAR_VELOCITY 2 +/// Initial max_angular_velocity before recalculate_turn_rate() first runs. +#define TURRET_DEFAULT_ANGULAR_VELOCITY 4 +/// Flat angular acceleration (degrees/decisecond^2) used to ramp angular_velocity up/down. +#define TURRET_ANGULAR_ACCEL 1 +/// Turn rate multiplier applied with no vehicle power. +#define TURRET_NO_POWER_TURN_RATE_FRACTION 0.2 +/// Half-widtth, in degrees, of a self-gimballed weapon's aim clamp around the turret's own facing. +#define SLAVED_GIMBAL_ARC_HALF_WIDTH 30 +/// Fire delay multiplier while a seconday is self-gimballed (slaved to the driver's own aim). (higher is worse) +#define SECONDARY_SLAVED_COOLDOWN_MULT 1.33 + +// Weapon/turret-ring degradation +/// Turret ring/weapon integrity% above which raw damage has no effect on performance at all. +#define WEAPON_DEGRADE_GRACE_THRESHOLD_PCT 90 +/// Accuracy multiplier floor. +#define WEAPON_MIN_ACCURACY_MULT 0.25 +/// Scatter multiplier ceiling. +#define WEAPON_MAX_SCATTER_MULT 8 +/// Projectile speed multiplier floor. +#define WEAPON_MIN_PROJECTILE_SPEED_MULT 0.4 +/// Damage multiplier floor. +#define WEAPON_MIN_DAMAGE_MULT 0.4 +/// Absolute final turn-rate floor, applied after every other multiplier. +#define TURRET_ABSOLUTE_MIN_ANGULAR_VELOCITY 0.3 +/// traverse_arc used when no primary weapon is mounted at all. +#define TURRET_EMPTY_TRAVERSE_ARC 150 + +// Flamer hardpoint fire modes +/// 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??? + +/// Flat extra tiles of range the DRG-N gets in stream mode. +#define DRG_N_STREAM_RANGE_BONUS 1 +/// Flat extra tiles of range the DRG-N gets in glob mode. +#define DRG_N_GLOB_RANGE_BONUS 2 + +/// Tiles the DRG-N's own smoke ttank disperses smoke across. +#define DRG_N_SMOKE_RANGE 6 +/// Reagent units per dispersed smoke puff for the DRG-N's own smoke tank. +#define DRG_N_SMOKE_UNITS 45 + +// Hardpoint wounds +/// A wound family gated by/rolled off the hardpoint's acid_damage_taken sub-pool. +#define WOUND_DAMTYPE_ACID "acid" +/// A wound family gated by/rolled off the hardpoint's brute_damage_taken sub-pool. +#define WOUND_DAMTYPE_BRUTE "brute" +/// A wound family gated by/rolled off overall integrity rather than either sub-pool alone. +#define WOUND_DAMTYPE_UNIFIED "unified" +/// Synthetic part_slot for the hull_melted/hull_hole families, tracking the vehicle's own frame health. +#define WOUND_SLOT_HULL "hull" + +// Single-target damage resolution +/// Fraction of a struck parent's hit that splashes into its directly-mounted external children. +#define HARDPOINT_BLEED_THROUGH_FRACTION 0.1 +/// Armor's own splash fraction from every external hit. +#define ARMOR_SPLASH_FRACTION 0.5 +/// Percent chance per point of incoming damage that a struck parent's internal-module pool also takes a hit. +#define HARDPOINT_ORGAN_DAMAGE_CHANCE_PER_DAMAGE 1.2 +/// Cap on the per-damage term alone, before wound/integrity bonuses are added. +#define HARDPOINT_ORGAN_DAMAGE_CHANCE_CAP 30 +/// Percent chance added per currently-active wound family on the struck parent. +#define HARDPOINT_ORGAN_DAMAGE_WOUND_BONUS 20 +/// Integrity percent below which low health starts adding extra internal-hit chance. +#define HARDPOINT_ORGAN_DAMAGE_INTEGRITY_THRESHOLD 60 +/// Percent chance added per integrity point below HARDPOINT_ORGAN_DAMAGE_INTEGRITY_THRESHOLD. +#define HARDPOINT_ORGAN_DAMAGE_INTEGRITY_SCALE 1.5 +/// Percent chance a neurotoxin spit at the Turret bleeds through to the Air Filter's neuro family. +#define TURRET_NEURO_SPIT_AIR_FILTER_BLEED_CHANCE_PCT 50 + +// Directional armor +/// A hit landing within 45 degres of the tank hull's own facing. +#define HIT_ZONE_FRONT "front" +/// A hit landing on either flank. +#define HIT_ZONE_SIDE "side" +/// A hit landing within 45 degrees of directly behind the tank. +#define HIT_ZONE_REAR "rear" +/// Fraction of Armor's own contribution a side hit ignores outright. +#define ARMOR_BYPASS_FRACTION_SIDE 0.25 +/// Fraction of Armor's own contribution a rear hit ignores outright. +#define ARMOR_BYPASS_FRACTION_REAR 0.75 +/// Percent chance bonus added to internal-module-damage rolls for a side hit. +#define ARMOR_BYPASS_INTERNAL_CHANCE_BONUS_SIDE 10 +/// Percent chance bonus added to internal-module-damage rolls for a rear hit. +#define ARMOR_BYPASS_INTERNAL_CHANCE_BONUS_REAR 35 + +// Hatch-wound-driven lock access +/// Entry/exit delay multiplier for an authorized marine at maximum hatch instability. +#define HATCH_WOUNDED_MARINE_MAX_DELAY_MULT 5 +/// Entry/exit delay multiplier for a xeno the moment the hatch first becomes wounded while locked. +#define HATCH_WOUNDED_XENO_START_MULT 1.5 +/// Entry/exit delay multiplier for a xeno at maximum hatch instability. +#define HATCH_WOUNDED_XENO_MIN_MULT 0.25 +/// Entry/exit delay multiplier for a non-USCM human once the hatch is wounded while locked. +#define HATCH_WOUNDED_NON_USCM_DELAY_MULT 6 + +// Caste-specific tank interactions +// Would those be better in the defines of those abilities themselves??? I don't know, so I placed them here. - BWSB +/// Damage a Crusher's Headbutt deals to the tank on contact. +#define CRUSHER_HEADBUTT_TANK_DAMAGE 20 +/// Tiles the tank is shoved back when a Crusher's Charge slams into it. +#define CRUSHER_CHARGE_TANK_KNOCKBACK_TILES 3 +/// Damage a plain Crusher Charge deals to the tank on contact. +#define CRUSHER_CHARGE_TANK_DAMAGE 60 +/// Deciseconds paused between each individual tile of a knockback shove. +#define KNOCKBACK_STEP_DELAY 3 +/// Knockback tiles for the Charger strain's toggled ram. +#define CRUSHER_CHARGER_RAM_TANK_KNOCKBACK_TILES 2 +/// Degrees a tank's turret is forcibly turned by a Warrior's Punch. +#define WARRIOR_PUNCH_TURRET_TURN_DEGREES 110 +/// Multiplier applied to a Warrior Punch's damage when it lands on the Turret Ring hardpoint. +#define WARRIOR_PUNCH_TURRET_RING_DAMAGE_MULT 2 +/// Overall multiplier on Warrior Punch's tank damage. +#define WARRIOR_PUNCH_TANK_DAMAGE_MULT 0.5 +/// Same as WARRIOR_PUNCH_TANK_DAMAGE_MULT, for Praetorian Oppressor's Dislocate. +#define OPPRESSOR_DISLOCATE_TANK_DAMAGE_MULT 0.75 +/// Angular velocity a Warrior Punch's forced turret turn uses. +#define WARRIOR_PUNCH_TURRET_ANGULAR_VELOCITY 20 +/// Angular acceleration to match WARRIOR_PUNCH_TURRET_ANGULAR_VELOCITY. +#define WARRIOR_PUNCH_TURRET_ANGULAR_ACCEL 6 +/// Fraction of Queen Screech's own stun duration applied to a tank's seated crew. +#define QUEEN_SCREECH_CREW_STUN_FRACTION 0.5 +/// Fraction of Visual Sensors' max health dealt as unblocked damage by a Queen Screech. +#define QUEEN_SCREECH_CAMERA_DAMAGE_PCT 0.05 +/// Same idea as QUEEN_SCREECH_CAMERA_DAMAGE_PCT, for the King's Doom ability. +#define KING_DOOM_CAMERA_DAMAGE_PCT 0.05 +/// Unblocked damage dealt to every part on a tank per AOE tile of a King's Destroy landing. +#define KING_JUMP_DAMAGE_PER_TILE 50 + +/// Traction malus cap for a turf covered in full-strength sticky resin. +#define STICKY_RESIN_TRACTION_CAP 0.3 +/// Traction malus cap for the weaker/temporary "thin" sticky resin variant. +#define STICKY_RESIN_WEAK_TRACTION_CAP 0.5 +/// Traction malus cap for a turf with a resin spike on it. +#define RESIN_SPIKE_TRACTION_CAP 0.8 + +/// Deciseconds a repeated acid spray hit still counts as "the same spray" for streak decay. +#define ACID_SPRAY_STREAK_WINDOW 20 + +/// Damage multiplier for Dancer's Impale/Vanguard's Pierce when aimed at an external module. +#define GUARANTEED_EXTERNAL_HIT_DAMAGE_MULT 1.5 + +/// Fraction of a tank's own momentum retained after a Praetorian Valkyrie's High Gallop clips it. +#define HIGH_GALLOP_MOMENTUM_LOSS_MULT 0.85 + +// Hull-destruction grand finale ammo cookoff +/// Seconds between the hull-death warning starting and the cookoff detonating. +#define HULL_COOKOFF_WARNING_TIME 10 SECONDS +/// Seconds between repeats of the on-screen "get out now" banner. +#define HULL_COOKOFF_PASSENGER_WARNING_INTERVAL 5 SECONDS +/// Radius of the small warning-phase napalm flame lit the instant the countdown starts. +#define HULL_COOKOFF_WARNING_FLAME_RADIUS 0 +/// cell_explosion() power at the tank's own exterior epicenter. +#define HULL_COOKOFF_EXTERIOR_EXPLOSION_POWER 150 +/// cell_explosion() falloff to pair with HULL_COOKOFF_EXTERIOR_EXPLOSION_POWER. +#define HULL_COOKOFF_EXTERIOR_EXPLOSION_FALLOFF 16 +/// cell_explosion() power for the separate interior blast if anyone's still trapped inside. +#define HULL_COOKOFF_INTERIOR_EXPLOSION_POWER 300 +/// cell_explosion() falloff for the interior blast. +#define HULL_COOKOFF_INTERIOR_EXPLOSION_FALLOFF 100 +/// Nearest a piece of launched turret wreckage is allowed to land from the tank's own turf. +#define HULL_COOKOFF_TURRET_LAUNCH_MIN_RANGE 2 +/// Farthest a piece of launched turret wreckage is allowed to land from the tank's own turf. +#define HULL_COOKOFF_TURRET_LAUNCH_MAX_RANGE 4 +/// Layer for the blinking cookoff warning icon. +#define HULL_COOKOFF_WARNING_ICON_LAYER (TANK_ABOVE_RIDER_LAYER + 0.01) diff --git a/code/__DEFINES/vendors.dm b/code/__DEFINES/vendors.dm index dd39912b020c..d6d357edec58 100644 --- a/code/__DEFINES/vendors.dm +++ b/code/__DEFINES/vendors.dm @@ -55,8 +55,9 @@ #define VEHICLE_SUPPORT_AVAILABLE 8 #define VEHICLE_ARMOR_AVAILABLE 16 #define VEHICLE_TREADS_AVAILABLE 32 +#define VEHICLE_UTILITY_AVAILABLE 64 -#define VEHICLE_ALL_AVAILABLE (VEHICLE_INTEGRAL_AVAILABLE|VEHICLE_PRIMARY_AVAILABLE|VEHICLE_SECONDARY_AVAILABLE|VEHICLE_SUPPORT_AVAILABLE|VEHICLE_ARMOR_AVAILABLE|VEHICLE_TREADS_AVAILABLE) +#define VEHICLE_ALL_AVAILABLE (VEHICLE_INTEGRAL_AVAILABLE|VEHICLE_PRIMARY_AVAILABLE|VEHICLE_SECONDARY_AVAILABLE|VEHICLE_SUPPORT_AVAILABLE|VEHICLE_ARMOR_AVAILABLE|VEHICLE_TREADS_AVAILABLE|VEHICLE_UTILITY_AVAILABLE) #define VENDOR_THEME_COMPANY 0 #define VENDOR_THEME_USCM 1 diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index 4876b8c1d0ba..a213519eca3c 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,43 @@ 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) + // Uses %% instead of % since a/b can be fractional, and % truncates toward zero. + 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 @@ -341,6 +378,57 @@ return vehicles +/** + * Finds the multitile vehicle (if any) whose footprint covers `checked_turf`. + * A plain contents check only finds a vehicle on its one real .loc tile. + */ +/proc/get_multitile_vehicle_at(turf/checked_turf) + if(!checked_turf) + return null + for(var/obj/vehicle/multitile/vehicle as anything in GLOB.all_multi_vehicles) + if(checked_turf in vehicle.locs) + return vehicle + return null + +/** + * Checks real adjacency against every turf in `vehicle.locs`, instead of just its center tile. + */ +/proc/is_adjacent_to_multitile_vehicle(atom/movable/mover, obj/vehicle/multitile/vehicle) + if(!mover || !vehicle) + return FALSE + var/turf/mover_turf = get_turf(mover) + if(!mover_turf) + return FALSE + if(mover_turf in vehicle.locs) + return TRUE + for(var/turf/vehicle_turf in vehicle.locs) + if(mover_turf.Adjacent(vehicle_turf)) + return TRUE + return FALSE + +/** + * Returns the distance to the closest tile of the vehicle's footprint, not just its center tile. + */ +/proc/get_dist_to_multitile_vehicle(atom/mover, obj/vehicle/multitile/vehicle) + var/closest = INFINITY + for(var/turf/vehicle_turf in vehicle.locs) + closest = min(closest, get_dist(mover, vehicle_turf)) + return closest + +/** + * Returns the closest tile of the vehicle's footprint to use as a get_line() endpoint, + * instead of collapsing to its center tile. Null if the vehicle has no locs. + */ +/proc/get_closest_turf_of_multitile_vehicle(atom/mover, obj/vehicle/multitile/vehicle) + var/turf/closest_turf = null + var/closest_dist = INFINITY + for(var/turf/vehicle_turf in vehicle.locs) + var/dist = get_dist(mover, vehicle_turf) + if(dist < closest_dist) + closest_dist = dist + closest_turf = vehicle_turf + return closest_turf + //Orders mobs by type then by name /proc/sortmobs() var/list/moblist = list() @@ -1353,6 +1441,9 @@ GLOBAL_LIST_INIT(WALLITEMS, list( var/step_count = get_dist(start_turf, end_turf) if(!step_count) + // get_dist() ignores z, so a target directly above/below also reports 0 steps here. + if(end_turf_fall.z != start_turf.z) + line += end_turf_fall return line //as step_count and step size (1) are known can pre-calculate a lerp step, tiny number (1e-5) for rounding consistency diff --git a/code/_onclick/click_hold.dm b/code/_onclick/click_hold.dm index 38bcd34e11b6..5d4228559666 100644 --- a/code/_onclick/click_hold.dm +++ b/code/_onclick/click_hold.dm @@ -107,6 +107,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/hud/alien.dm b/code/_onclick/hud/alien.dm index 1d29d6c4b728..5c22df4df12d 100644 --- a/code/_onclick/hud/alien.dm +++ b/code/_onclick/hud/alien.dm @@ -16,6 +16,7 @@ draw_pull(ui_alien_datum) draw_throw(ui_alien_datum) draw_zone_sel(ui_alien_datum) + draw_vehicle_zone_sel(ui_alien_datum) draw_healths(ui_alien_datum) draw_alien_nightvision(ui_alien_datum) draw_alien_plasma_display(ui_alien_datum) diff --git a/code/_onclick/hud/hud.dm b/code/_onclick/hud/hud.dm index f74a06aa876f..5db2ebf0d8f5 100644 --- a/code/_onclick/hud/hud.dm +++ b/code/_onclick/hud/hud.dm @@ -47,6 +47,7 @@ var/atom/movable/screen/unique_action var/atom/movable/screen/zone_sel/zone_sel + var/atom/movable/screen/zone_sel/vehicle/vehicle_zone_sel var/atom/movable/screen/pull_icon var/atom/movable/screen/throw_icon var/atom/movable/screen/oxygen_icon @@ -155,6 +156,7 @@ unique_action = null zone_sel = null + vehicle_zone_sel = null pull_icon = null throw_icon = null oxygen_icon = null @@ -416,6 +418,18 @@ zone_sel.update_icon(mymob) static_inventory += zone_sel +/// Vehicle-part doll, placed directly above the human doll. +/datum/hud/proc/draw_vehicle_zone_sel(datum/custom_hud/ui_datum, ui_alpha, ui_color) + vehicle_zone_sel = new /atom/movable/screen/zone_sel/vehicle() + vehicle_zone_sel.icon = ui_datum.ui_style_icon + vehicle_zone_sel.screen_loc = ui_datum.ui_vehicle_zonesel + if(ui_alpha) + vehicle_zone_sel.alpha = ui_alpha + if(ui_color) + vehicle_zone_sel.color = ui_color + vehicle_zone_sel.update_icon(mymob) + static_inventory += vehicle_zone_sel + // Re-render all alerts - also called in /datum/hud/show_hud() because it's needed there /datum/hud/proc/reorganize_alerts(mob/viewmob) var/mob/screenmob = viewmob || mymob diff --git a/code/_onclick/hud/human.dm b/code/_onclick/hud/human.dm index a42c20539923..be0493a13571 100644 --- a/code/_onclick/hud/human.dm +++ b/code/_onclick/hud/human.dm @@ -96,6 +96,7 @@ draw_nutrition(ui_datum) draw_locator_spot(owner, ui_datum) draw_zone_sel(ui_datum, ui_alpha, ui_color) + draw_vehicle_zone_sel(ui_datum, ui_alpha, ui_color) draw_gun_related(ui_datum, ui_alpha) diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm index 05c0dfc563ff..10a14cdb1148 100644 --- a/code/_onclick/hud/screen_objects.dm +++ b/code/_onclick/hud/screen_objects.dm @@ -154,6 +154,45 @@ else color = null +/// Human body zone to vehicle hardpoint slot, keeps the vehicle doll in sync with the human doll. +GLOBAL_LIST_INIT(human_zone_to_vehicle_part, list( + "head" = HDPT_TURRET, + "eyes" = HDPT_VISUAL_SENSORS, + "mouth" = HDPT_IFF_MODULE, + "chest" = WOUND_SLOT_HULL, + "r_arm" = HDPT_PRIMARY, + "r_hand" = HDPT_PRIMARY, + "l_arm" = HDPT_SECONDARY, + "l_hand" = HDPT_SECONDARY, + "groin" = WOUND_SLOT_HULL, + "r_leg" = HDPT_TREADS, + "l_leg" = HDPT_TREADS, + "r_foot" = HDPT_TREADS, + "l_foot" = HDPT_TREADS, +)) + +/// Vehicle hardpoint slot to the vehicle doll's own highlight icon_state in zone_sel.dmi. +GLOBAL_LIST_INIT(vehicle_part_to_zone_sel_icon_state, list( + (HDPT_TURRET) = "turret", + (HDPT_VISUAL_SENSORS) = "sensor", + (HDPT_IFF_MODULE) = "iff", + (WOUND_SLOT_HULL) = "hull", + (HDPT_PRIMARY) = "primary", + (HDPT_SECONDARY) = "secondary", + (HDPT_TREADS) = "treads", +)) + +/// Reverse of human_zone_to_vehicle_part. First match wins where several zones share a part. +GLOBAL_LIST_INIT(vehicle_part_to_human_zone, list( + (HDPT_TURRET) = "head", + (HDPT_VISUAL_SENSORS) = "eyes", + (HDPT_IFF_MODULE) = "mouth", + (WOUND_SLOT_HULL) = "chest", + (HDPT_PRIMARY) = "r_arm", + (HDPT_SECONDARY) = "l_arm", + (HDPT_TREADS) = "r_leg", +)) + /atom/movable/screen/zone_sel name = "damage zone" icon_state = "zone_sel" @@ -163,6 +202,119 @@ overlays.Cut() overlays += image('icons/mob/hud/zone_sel.dmi', "[selecting]") +/** + * Vehicle-part doll shown directly above the human doll, cycles together with it. + * Has its own body silhouette and 7-state highlight overlay instead of reusing the human doll's. + */ +/atom/movable/screen/zone_sel/vehicle + name = "vehicle part" + icon_state = "zone_sel_vehicle" + +/atom/movable/screen/zone_sel/vehicle/update_icon(mob/living/user) + overlays.Cut() + var/vehicle_part = GLOB.human_zone_to_vehicle_part[selecting] || WOUND_SLOT_HULL + overlays += image('icons/mob/hud/zone_sel.dmi', GLOB.vehicle_part_to_zone_sel_icon_state[vehicle_part] || "hull") + +/** + * + * Let me explain what this does and why it is needed, for anyone wondering why there is a giant tank ascii art in here. + * + * Adding a separate targeting system was a headache and half. I used an image to ascii converter online to pass the tank targeting doll sprite that Holdeman made to + * make the ascii list below, and then I slowly built it up and refined it so it was 'pixel perfect' comparing to a zoomed up version in aseprite. + * + * Why was this needed? Well, when you click the targeting doll, BYOND's clicked() will hand you the pixel you clicked in. This is a set of coordinates with X from 1-32 + * and Y from 1-32. + * + * This set of coordinates is then compared against that list, and it returns the matching ASCII key of the part it hit. D. H. I. T. C. '.' - Etc. + * + * So, why do we need to do this instead of doing whatever the human targeting doll already does??, you may ask, and to that, I reply: + * + * A vehicle's shape is more complex than a human doll. The treads aren't one continuous, rectangular shape. The hull has a big hole in the middle, which is then + * filled by another hardpoint. The shapes of the guns and other hardpoints are complex. This is compared to hte human doll which is a selection of rectangles and squares. + * If we followed the same approach that the human doll targeting resolve uses, we'd be looking at an enormous set of x_ranges and switch(icon_y) -> switch(icon_x) + * lines of code. Just take a look at how the human doll handles it and you'll understand why I didn't do it like that. Or maybe I didn't see an obvious easy way. + * + * So, + * Instead of writing code that is algebraically complex and nigh unmaintainable to represent those complex shapes, I've decided to use an ASCII hitmap, which + * is both easier to build, tweak, test, and maintain. But it is 'different', yes. + * + * In short, the way this works is: We get the XY coordinates from clicked() when clicking the doll sprite and figure out to which part they resolve... + * By comparing them to the hitmap below, grabbing whatever letter it lands on. It's fast, reliable and also pixel perfect! + * - BWSB + */ +GLOBAL_LIST_INIT(vehicle_doll_hitmap, list("................................", + "................................", + "....DDDDDD............DDDDDD....", + "....DDDDDDHHHHHHHHHHHHDDDDDD....", + "....DDDDHHHHHIHHHHHHHHHHDDDD....", + "....DDDDHHHHHIHHHHHHHHHHDDDD....", + "....DDDDHHHHHIHHHHHHHHHHDDDD....", + "....DDDDHHHHTITTTTTTHHHHDDDD....", + "....DDDDHHHTTITTTTTTTHHHDDDD....", + "....DDDDHHTTTITTTTTTTTHHDDDD....", + "....DDDDHHTTIITTTTCCTTHHDDDD....", + "....DDDDHTTIIITTTCCCCTTHDDDD....", + "....DDDDHTTIIITTTCCCCTTHDDDD....", + "....DDDDHHTIIITTTCCCCTHHDDDD....", + "....DDDDHHTTTTTTTTCCCTHHDDDD....", + "....DDDDHHHTTTTTTTCCCHHHDDDD....", + "....DDDDHHHTTTTPPTCCCHHHDDDD....", + "....DDDDHHHTTTPPPPCCCHHHDDDD....", + "....DDDDHHHTTTPPPPCCCHHHDDDD....", + "....DDDDHHHHTTPPPPTCHHHHDDDD....", + "....DDDDHHHHHHPPPPHHHHHHDDDD....", + "....DDDDHHHHHHPPPPHHHHHHDDDD....", + "....DDDDHHHHHHPPPPHHHHHHDDDD....", + "....DDDDHHHHHHPPPPSSSSSHDDDD....", + "....DDDDHHHHHHPPPPSSSSSHDDDD....", + "....DDDDDDHHHHPPPPSSSSDDDDDD....", + "....DDDDDDHHHHPPPPSSSSDDDDDD....", + "....DDDDDDHHHHHPPHSSSSDDDDDD....", + "....DDDDDDHHHHHHHHHHHHDDDDDD....", + "....DDDDDDHHHHHHHHHHHHDDDDDD....", + "....DDDDDD............DDDDDD....", + "................................")) + +/// vehicle_doll_hitmap character to hardpoint slot. +GLOBAL_LIST_INIT(vehicle_doll_hitmap_chars, list( + "I" = HDPT_IFF_MODULE, + "S" = HDPT_VISUAL_SENSORS, + "P" = HDPT_PRIMARY, + "C" = HDPT_SECONDARY, + "T" = HDPT_TURRET, + "D" = HDPT_TREADS, + "H" = WOUND_SLOT_HULL, +)) + +/atom/movable/screen/zone_sel/vehicle/proc/get_vehicle_part_at(icon_x, icon_y) + if(icon_x < 1 || icon_x > 32 || icon_y < 1 || icon_y > 32) + return null + var/row_string = GLOB.vehicle_doll_hitmap[33 - icon_y] + var/hit_char = copytext(row_string, icon_x, icon_x + 1) + return GLOB.vehicle_doll_hitmap_chars[hit_char] + +/atom/movable/screen/zone_sel/vehicle/clicked(mob/user, list/mods) + // Deliberately not calling ..(), the immediate parent is the human doll's own hit-test switch. + // Replicating the real base check directly instead. + if(!user) + return TRUE + if(isobserver(user)) + return TRUE + + var/icon_x = text2num(mods[ICON_X]) + var/icon_y = text2num(mods[ICON_Y]) + var/old_selecting = selecting + + var/vehicle_part = get_vehicle_part_at(icon_x, icon_y) + if(!vehicle_part) + return 1 + + selecting = GLOB.vehicle_part_to_human_zone[vehicle_part] || "chest" + + if(old_selecting != selecting) + user.select_body_zone(selecting) + return 1 + /atom/movable/screen/zone_sel/clicked(mob/user, list/mods) if (..()) return TRUE @@ -223,8 +375,7 @@ selecting = "eyes" if(old_selecting != selecting) - user.zone_selected = selecting - update_icon(user) + user.select_body_zone(selecting) return 1 /atom/movable/screen/gun diff --git a/code/_onclick/human.dm b/code/_onclick/human.dm index 6fdc1f80578e..fa6adbcc21fc 100644 --- a/code/_onclick/human.dm +++ b/code/_onclick/human.dm @@ -156,6 +156,23 @@ grab_level = GRAB_CARRY + var/obj/vehicle/multitile/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/controllers/subsystem/minimap.dm b/code/controllers/subsystem/minimap.dm index a96212ec9567..3c1bc58262df 100644 --- a/code/controllers/subsystem/minimap.dm +++ b/code/controllers/subsystem/minimap.dm @@ -530,6 +530,11 @@ SUBSYSTEM_DEF(minimaps) var/actual_z = target.z if(ismob(target) && target.loc && !isturf(target.loc)) actual_z = target.loc.z + else if(SSinterior.in_interior(target)) + // Starting out inside a vehicle interior, use the vehicle's real z for minimap bucketing. + var/datum/interior/interior = SSinterior.get_interior_by_coords(target.x, target.y, target.z) + if(interior?.exterior) + actual_z = interior.exterior.z if(!initialized || !(minimaps_by_z["[actual_z]"])) //the minimap doesn't exist yet, z level was probably loaded after init for(var/datum/callback/callback as anything in LAZYACCESS(earlyadds, "[actual_z]")) @@ -543,6 +548,10 @@ SUBSYSTEM_DEF(minimaps) var/turf/target_turf = get_turf(target) if(ismob(target) && target.loc && !isturf(target.loc)) target_turf = get_turf(target.loc) + else if(SSinterior.in_interior(target)) + var/datum/interior/interior = SSinterior.get_interior_by_coords(target.x, target.y, target.z) + if(interior?.exterior) + target_turf = get_turf(interior.exterior) blip.pixel_x = MINIMAP_PIXEL_FROM_WORLD(target_turf.x) + minimaps_by_z["[target_turf.z]"].x_offset + image_x blip.pixel_y = MINIMAP_PIXEL_FROM_WORLD(target_turf.y) + minimaps_by_z["[target_turf.z]"].y_offset + image_y @@ -624,20 +633,25 @@ SUBSYSTEM_DEF(minimaps) */ /image/proc/minimap_on_move(atom/movable/source, oldloc) SIGNAL_HANDLER - if(source.z) + if(source.z && !SSinterior.in_interior(source)) var/datum/hud_displays/minimap = SSminimaps.minimaps_by_z["[source.z]"] pixel_x = MINIMAP_PIXEL_FROM_WORLD(source.x) + minimap.x_offset pixel_y = MINIMAP_PIXEL_FROM_WORLD(source.y) + minimap.y_offset return - var/atom/movable/movable_loc = source.loc // How does none of this just crash if the loc isn't on map? - source.override_minimap_tracking() + var/atom/movable/movable_loc = source.loc + if(isturf(movable_loc) && SSinterior.in_interior(source)) + // Inside a vehicle's interior, track the vehicle itself instead, same as the container case below. + var/datum/interior/interior = SSinterior.get_interior_by_coords(source.x, source.y, source.z) + if(interior?.exterior) + movable_loc = interior.exterior + source.override_minimap_tracking(movable_loc) var/datum/hud_displays/minimap = SSminimaps.minimaps_by_z["[movable_loc.z]"] pixel_x = MINIMAP_PIXEL_FROM_WORLD(movable_loc.x) + minimap.x_offset pixel_y = MINIMAP_PIXEL_FROM_WORLD(movable_loc.y) + minimap.y_offset ///Used to handle minimap tracking inside other movables -/atom/movable/proc/override_minimap_tracking() +/atom/movable/proc/override_minimap_tracking(atom/movable/loc) var/image/blip = SSminimaps.images_by_source[src] blip.RegisterSignal(loc, COMSIG_MOVABLE_MOVED, TYPE_PROC_REF(/image, minimap_on_move)) RegisterSignal(loc, COMSIG_ATOM_EXITED, PROC_REF(cancel_override_minimap_tracking)) @@ -1141,7 +1155,8 @@ SUBSYSTEM_DEF(minimaps) // Apply ceiling protection overlay if client has preference enabled if(owner.client.prefs?.show_minimap_ceiling_protection) map.update_ceiling_overlay(owner.client) - locator.link_locator(map, owner) + // Must be `tracking`, not `owner`, or an active locator_override like a vehicle interior gets lost. + locator.link_locator(map, tracking) locator.update(tracking, null, null) // Show ceiling protection toggle action when minimap opens for(var/datum/action/minimap_ceiling/ceiling_action in owner.actions) diff --git a/code/datums/ammo/bullet/tank.dm b/code/datums/ammo/bullet/tank.dm index fdaf98df875b..dd8653c2501c 100644 --- a/code/datums/ammo/bullet/tank.dm +++ b/code/datums/ammo/bullet/tank.dm @@ -12,11 +12,11 @@ sound_hit = 'sound/weapons/vehicles/autocannon_flak_hit.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(P), 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 3 tile radius +/datum/ammo/bullet/shrapnel/flak + max_range = 3 /datum/ammo/bullet/tank/dualcannon name = "dualcannon bullet" @@ -90,7 +105,25 @@ 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() . = ..() damage = 15 + +/** + * Cosmetic shrapnel + */ +/datum/ammo/bullet/shrapnel/tank_wound + name = "shrapnel" + accuracy = -HIT_ACCURACY_TIER_MAX + damage = 2 + damage_var_low = 0 + damage_var_high = 0 + shrapnel_chance = 0 + // Bumped past the 20-tile angle-target distance so it covers the whole distance. + accurate_range = 20 + // This might be a bit ttoo much. Early in testing lower values were disappearing very, very fast. Would need to test more to see if its ok. + max_range = 24 diff --git a/code/datums/ammo/misc.dm b/code/datums/ammo/misc.dm index 64110516306f..b2bc66637ba5 100644 --- a/code/datums/ammo/misc.dm +++ b/code/datums/ammo/misc.dm @@ -48,22 +48,39 @@ /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() - new /obj/flamer_fire(turf, cause_data, reagent, 1) + var/datum/reagent/reagent = loaded_chem_id ? GLOB.chemical_reagents_list[loaded_chem_id] : null + if(!reagent) + reagent = new /datum/reagent/napalm/high_damage() + // Shape is forced to FLAMESHAPE_DEFAULT so the glob always spreads as a 5-tile cardinal cross. + new /obj/flamer_fire(turf, cause_data, reagent, 1, null, FLAMESHAPE_DEFAULT, null, null, 1, reagent.fire_type) - 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() + // fire_spread_amount 0, so this glob stays confined to the tile it lands on. + new /obj/flamer_fire(turf, cause_data, reagent, 0, null, reagent.flameshape, null, null, 1, reagent.fire_type) /datum/ammo/flamethrower/sentry_flamer flags_ammo_behavior = AMMO_IGNORE_ARMOR|AMMO_IGNORE_COVER|AMMO_FLAME|AMMO_NO_DEFLECT @@ -316,6 +333,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(P), 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, , 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), @@ -392,41 +432,39 @@ var/nade_type = /obj/item/explosive/grenade/high_explosive icon_state = "grenade" flags_ammo_behavior = AMMO_IGNORE_COVER|AMMO_SKIPS_ALIENS + /// Fuse (deciseconds) set on the dropped nade_type once it lands. + var/dropped_det_time = 10 damage = 15 accuracy = HIT_ACCURACY_TIER_3 max_range = 6 /datum/ammo/grenade_container/on_hit_mob(mob/M,obj/projectile/P) - drop_nade(P) + drop_nade(get_turf(M), P) /datum/ammo/grenade_container/on_hit_obj(obj/O,obj/projectile/P) - drop_nade(P) + drop_nade(get_turf(O), P) /datum/ammo/grenade_container/on_hit_turf(turf/T,obj/projectile/P) - drop_nade(P) + drop_nade(T.density ? get_turf(P) : T, P) /datum/ammo/grenade_container/do_at_max_range(obj/projectile/P) - drop_nade(P) + drop_nade(get_turf(P), P) -/datum/ammo/grenade_container/proc/drop_nade(obj/projectile/P) - var/turf/T = get_turf(P) +/datum/ammo/grenade_container/proc/drop_nade(turf/T, obj/projectile/P) var/obj/item/explosive/grenade/G = new nade_type(T) G.visible_message(SPAN_WARNING("\A [G] lands on [T]!")) - G.det_time = 10 + G.det_time = dropped_det_time G.cause_data = P.weapon_cause_data G.activate() /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 + flags_ammo_behavior = AMMO_IGNORE_COVER|AMMO_SKIPS_ALIENS|AMMO_HITS_TARGET_TURF // let me know if this is too much. Idea being that those shells are being 'lobbed up' into the air. + dropped_det_time = 3 SECONDS /datum/ammo/hugger_container name = "hugger shell" diff --git a/code/datums/ammo/rocket.dm b/code/datums/ammo/rocket.dm index db7766917190..03948b2c99aa 100644 --- a/code/datums/ammo/rocket.dm +++ b/code/datums/ammo/rocket.dm @@ -140,20 +140,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/ammo/xeno.dm b/code/datums/ammo/xeno.dm index be964cb6cd07..00dabf119dee 100644 --- a/code/datums/ammo/xeno.dm +++ b/code/datums/ammo/xeno.dm @@ -432,7 +432,7 @@ name = "tail hook" icon_state = "none" ping = null - flags_ammo_behavior = AMMO_XENO|AMMO_SKIPS_ALIENS|AMMO_STOPPED_BY_COVER + flags_ammo_behavior = AMMO_XENO|AMMO_SKIPS_ALIENS|AMMO_STOPPED_BY_COVER|AMMO_PASSES_OVER_VEHICLES damage_type = BRUTE damage = XENO_DAMAGE_TIER_5 @@ -441,7 +441,7 @@ /datum/ammo/xeno/oppressor_tail/on_bullet_generation(obj/projectile/generated_projectile, mob/bullet_generator) //The projectile has no icon, so the overlay shows up in FRONT of the projectile, and the beam connects to it in the middle. - var/image/hook_overlay = new(icon = 'icons/effects/beam.dmi', icon_state = "oppressor_tail_hook", layer = BELOW_MOB_LAYER) + var/image/hook_overlay = new(icon = 'icons/effects/beam.dmi', icon_state = "oppressor_tail_hook", layer = TANK_ABOVE_RIDER_LAYER) generated_projectile.overlays += hook_overlay /datum/ammo/xeno/oppressor_tail/on_hit_mob(mob/target, obj/projectile/fired_proj) @@ -450,7 +450,7 @@ return shake_camera(target, 5, 0.1 SECONDS) - var/obj/effect/beam/tail_beam = fired_proj.firer.beam(target, "oppressor_tail", 'icons/effects/beam.dmi', 0.5 SECONDS, 5) + var/obj/effect/beam/tail_beam = fired_proj.firer.beam(target, "oppressor_tail", 'icons/effects/beam.dmi', 0.5 SECONDS, 5, /obj/effect/ebeam/above_tank) var/image/tail_image = image('icons/effects/status_effects.dmi', "hooked") target.overlays += tail_image diff --git a/code/datums/beam.dm b/code/datums/beam.dm index 8dd2dd53184c..42b4345f0907 100644 --- a/code/datums/beam.dm +++ b/code/datums/beam.dm @@ -143,6 +143,12 @@ anchored = TRUE var/datum/beam/owner +/** + * A plain ebeam segment rendered above a tank's hull and riders, like Oppressor's Tail Seize rope. + */ +/obj/effect/ebeam/above_tank + layer = TANK_ABOVE_RIDER_LAYER + /obj/effect/ebeam/laser name = "laser beam" desc = "A laser beam!" diff --git a/code/datums/components/vehicle_rider.dm b/code/datums/components/vehicle_rider.dm new file mode 100644 index 000000000000..aa82a7713624 --- /dev/null +++ b/code/datums/components/vehicle_rider.dm @@ -0,0 +1,39 @@ +/** + * vehicle_rider tracks whether an atom/movable (a mob or a droppable obj) is currently riding atop a multitile vehicle's hull. + * + * Added by /obj/vehicle/multitile/proc/mark_on_top and obj_mark_on_top when something climbs onto, or is otherwise placed atop, a vehicle. Removed by clear_on_top/obj_clear_on_top when it leaves. + * + * Holds a weakref to the vehicle rather than a direct reference, so a rider never keeps a destroyed vehicle referenced, and so the component can detect a qdel'd vehicle and clean itself up on the next lookup. + */ +/datum/component/vehicle_rider + dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS + var/datum/weakref/vehicle_ref + +/datum/component/vehicle_rider/Initialize(obj/vehicle/multitile/vehicle) + if(!ismovable(parent) || !istype(vehicle)) + return COMPONENT_INCOMPATIBLE + vehicle_ref = WEAKREF(vehicle) + +/datum/component/vehicle_rider/InheritComponent(datum/component/vehicle_rider/new_comp, i_am_original, obj/vehicle/multitile/vehicle) + if(istype(vehicle)) + vehicle_ref = WEAKREF(vehicle) + +/datum/component/vehicle_rider/Destroy(force, silent) + vehicle_ref = null + return ..() + +/datum/component/vehicle_rider/RegisterWithParent() + RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_qdel)) + +/datum/component/vehicle_rider/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_PARENT_QDELETING) + +/datum/component/vehicle_rider/proc/on_parent_qdel() + SIGNAL_HANDLER + qdel(src) + +/datum/component/vehicle_rider/proc/get_vehicle() + var/obj/vehicle/multitile/vehicle = vehicle_ref?.resolve() + if(!vehicle) + qdel(src) + return vehicle diff --git a/code/datums/custom_hud.dm b/code/datums/custom_hud.dm index d9abdbf36097..027fe4554981 100644 --- a/code/datums/custom_hud.dm +++ b/code/datums/custom_hud.dm @@ -20,12 +20,14 @@ //Lower right, persistent menu var/ui_dropbutton = "EAST-4:22,1:5" - var/ui_drop_throw = "EAST-1:28,2:7" - var/ui_pull = "EAST-2:26,2:7" - var/ui_resist = "EAST-2:26,2:7" + var/ui_drop_throw = "EAST-1:28,3:9" + var/ui_pull = "EAST-2:26,3:9" + var/ui_resist = "EAST-2:26,3:9" var/ui_acti = "EAST-2:26,1:5" var/ui_movi = "EAST-3:24,1:5" var/ui_zonesel = "EAST-1:28,1:5" + //Vehicle-part doll, directly above ui_zonesel + var/ui_vehicle_zonesel = "EAST-1:28,2:7" //Gun buttons var/ui_gun1 = "EAST-2:26,3:7" diff --git a/code/datums/keybinding/vehicles.dm b/code/datums/keybinding/vehicles.dm index 22c20e678c4b..b54699b06199 100644 --- a/code/datums/keybinding/vehicles.dm +++ b/code/datums/keybinding/vehicles.dm @@ -70,6 +70,51 @@ vehicle_user.activate_horn() return TRUE +/datum/keybinding/vehicles/shift_gear_up + hotkey_keys = list("Unbound") + classic_keys = list("Unbound") + name = "Shift gear up" + full_name = "Shift Gear Up" + keybind_signal = COMSIG_KB_VEHICLE_SHIFT_GEAR_UP + +/datum/keybinding/vehicles/shift_gear_up/down(client/user) + . = ..() + if(.) + return + var/obj/vehicle/multitile/vehicle_user = user.mob.interactee + vehicle_user.cycle_gear_up() + return TRUE + +/datum/keybinding/vehicles/shift_gear_down + hotkey_keys = list("Unbound") + classic_keys = list("Unbound") + name = "Shift gear down" + full_name = "Shift Gear Down" + keybind_signal = COMSIG_KB_VEHICLE_SHIFT_GEAR_DOWN + +/datum/keybinding/vehicles/shift_gear_down/down(client/user) + . = ..() + if(.) + return + var/obj/vehicle/multitile/vehicle_user = user.mob.interactee + vehicle_user.cycle_gear_down() + return TRUE + +/datum/keybinding/vehicles/toggle_cruise_control + hotkey_keys = list("Unbound") + classic_keys = list("Unbound") + name = "Toggle cruise control" + full_name = "Toggle Cruise Control" + keybind_signal = COMSIG_KB_VEHICLE_TOGGLE_CRUISE_CONTROL + +/datum/keybinding/vehicles/toggle_cruise_control/down(client/user) + . = ..() + if(.) + return + var/obj/vehicle/multitile/vehicle_user = user.mob.interactee + vehicle_user.toggle_cruise_control() + return TRUE + /datum/keybinding/vehicles/reload_weapon hotkey_keys = list("Unbound") classic_keys = list("Unbound") @@ -84,3 +129,47 @@ 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) + // This bypasses the action button, so refresh its icon manually. + for(var/datum/action/human_action/vehicle_action/toggle_hardpoint_fire_mode/action in user.mob.actions) + action.update_button_icon() + return TRUE + +/// Same gunner-only toggle as the right-click verb and ALT+Click shortcut. +/datum/keybinding/vehicles/toggle_turret_safety + hotkey_keys = list("Unbound") + classic_keys = list("Unbound") + name = "Toggle turret safety" + full_name = "Toggle Turret Rotation and Hardpoint Safety" + keybind_signal = COMSIG_KB_VEHICLE_TOGGLE_TURRET_SAFETY + +/datum/keybinding/vehicles/toggle_turret_safety/down(client/user) + . = ..() + if(.) + return + var/obj/vehicle/multitile/vehicle_user = user.mob.interactee + vehicle_user.toggle_turret_safety(user.mob) + // Same icon refresh gap as toggle_hardpoint_fire_mode above. + for(var/datum/action/human_action/vehicle_action/toggle_gyro/action in user.mob.actions) + action.update_button_icon() + return TRUE diff --git a/code/datums/looping_sounds/vehicle_sounds.dm b/code/datums/looping_sounds/vehicle_sounds.dm new file mode 100644 index 000000000000..0c8526be034e --- /dev/null +++ b/code/datums/looping_sounds/vehicle_sounds.dm @@ -0,0 +1,267 @@ +// Source: https://pixabay.com/sound-effects/film-special-effects-long-blinker-loop-24863/ +/datum/looping_sound/turn_signal + mid_sounds = 'sound/vehicles/blinkerloop.ogg' + mid_length = 1.2 SECONDS + volume = 35 + +// Source: https://pixabay.com/sound-effects/film-special-effects-tank-turret-rotate-14879/ +/datum/looping_sound/tank_turret + mid_sounds = 'sound/vehicles/tankturret.ogg' + mid_length = 1 SECONDS + // Matches this file's other vehicle loops. Also applies to turretdamaged.ogg. + volume = 25 + +// Source: freesound.org/s/541240/ (Garuda1982, Attribution 4.0). See sound/ATTRIBUTION.txt +/datum/looping_sound/tank_tracks + mid_sounds = 'sound/vehicles/trackrattling.ogg' + mid_length = 1 SECONDS + volume = 25 + /// Fixed channel reused for every play(), so on_stop() can immediately cut whatever's still playing. + var/track_channel + +/datum/looping_sound/tank_tracks/play(soundfile, volume_override) + if(!track_channel) + track_channel = get_free_channel() + playsound(parent, soundfile, volume_override || volume, vary, extra_range, VOLUME_SFX, track_channel) + +/** + * Immediately silences whatever's still playing on track_channel, both to seated crew and to + * anyone near the vehicle outside it. + */ +/datum/looping_sound/tank_tracks/on_stop() + ..() + if(!track_channel || !parent) + return + + var/obj/vehicle/multitile/vehicle = parent + if(istype(vehicle)) + for(var/client/crew_client in vehicle.get_interior_mob_clients()) + SEND_SOUND(crew_client, sound(null, channel = track_channel)) + + var/turf/source_turf = get_turf(parent) + if(!source_turf) + return + var/datum/sound_template/template = new() + template.channel = track_channel + template.x = source_turf.x + template.y = source_turf.y + template.z = source_turf.z + template.range = extra_range || floor(0.25 * volume) + SSsound.queue(template) + +// Source: freesound.org/s/681435/ (Department64, Attribution 4.0). See sound/ATTRIBUTION.txt +/datum/looping_sound/tank_drift + mid_sounds = 'sound/vehicles/treadedDrift.ogg' + // treadedDrift.ogg's exact duration. mid_length must match precisely or the loop cuts short or gaps. + mid_length = 1.289478 SECONDS + volume = 25 + /// Same reasoning as tank_tracks' track_channel above. + var/drift_channel + +/datum/looping_sound/tank_drift/play(soundfile, volume_override) + if(!drift_channel) + drift_channel = get_free_channel() + playsound(parent, soundfile, volume_override || volume, vary, extra_range, VOLUME_SFX, drift_channel) + +/// Same reasoning and mechanism as tank_tracks/on_stop() above. +/datum/looping_sound/tank_drift/on_stop() + ..() + if(!drift_channel || !parent) + return + + var/obj/vehicle/multitile/vehicle = parent + if(istype(vehicle)) + for(var/client/crew_client in vehicle.get_interior_mob_clients()) + SEND_SOUND(crew_client, sound(null, channel = drift_channel)) + + var/turf/source_turf = get_turf(parent) + if(!source_turf) + return + var/datum/sound_template/template = new() + template.channel = drift_channel + template.x = source_turf.x + template.y = source_turf.y + template.z = source_turf.z + template.range = extra_range || floor(0.25 * volume) + SSsound.queue(template) + +// Source: freesound.org/s/542582/ (adr1911, Creative Commons 0). See sound/ATTRIBUTION.txt +/datum/looping_sound/tank_engine + mid_sounds = 'sound/vehicles/turbineidle.ogg' + volume = ENGINE_SOUND_MIN_VOLUME + /// turbineidle.ogg's real duration at its native frequency. Raising playback frequency for the + /// pitch effect shrinks the clip length, so this subtype reschedules off the real per-play + /// duration instead of a fixed mid_length. See sound_loop() below. + var/native_duration = 2.630260 + // Played once before the loop starts / once after it stops. Both route through this subtype's + // play() override, so they get the same speed-based volume/pitch treatment. + start_sound = 'sound/vehicles/turbinestartup.ogg' + start_length = 5.054898 SECONDS + end_sound = 'sound/vehicles/turbineoff.ogg' + /// Fixed channels reused for every play(), same reasoning as tank_tracks/track_channel above. + var/crew_channel + var/outside_channel + +/** + * Overrides the base play() entirely. Volume/frequency are computed fresh each trigger from + * whichever of current_speed or engine_rev_level is currently higher. + * + * Sends ttwo separate copies via play_direct_to_crew() and play_to_outside_hearers(), so both the + * crew and anyone standing near the tank hear the engine without doubling up. + * + * Arguments: + * * soundfile = The sound file picked by get_sound(). + * * volume_override = Unused. Volume is always computed from speed/revving for this subtype. + * + * Returns: + * * The frequency actually used to play this instance, so sound_loop() can reschedule off the + * real pitch-shifted playback duration. + */ +/datum/looping_sound/tank_engine/play(soundfile, volume_override) + var/obj/vehicle/multitile/vehicle = parent + var/speed_fraction = 0 + if(istype(vehicle) && vehicle.uses_gear_transmission) + var/list/stats = vehicle.get_current_gear_stats() + if(stats["max_speed"]) + // current_speed and drift_speed sit on perpendicular axes, so this combines them as a + // vector magnitude instead of a flat sum. + var/effective_speed = sqrt(vehicle.current_speed ** 2 + vehicle.drift_speed ** 2) + speed_fraction = clamp(effective_speed / stats["max_speed"], 0, 1) + + // Revving in Park/Neutral never moves current_speed. Blended in as a separate fraction (not + // added) so idle revving sounds as loud/pitched as reaching that fraction of top speed would. + var/rev_fraction = istype(vehicle) ? vehicle.engine_rev_level : 0 + var/effective_fraction = max(speed_fraction, rev_fraction) + + var/computed_volume = ENGINE_SOUND_MIN_VOLUME + effective_fraction * (ENGINE_SOUND_MAX_VOLUME - ENGINE_SOUND_MIN_VOLUME) + var/computed_frequency = ENGINE_SOUND_MIN_FREQUENCY + effective_fraction * (ENGINE_SOUND_MAX_FREQUENCY - ENGINE_SOUND_MIN_FREQUENCY) + + play_direct_to_crew(vehicle, soundfile, computed_volume, computed_frequency) + play_to_outside_hearers(vehicle, soundfile, computed_volume, computed_frequency) + return computed_frequency + +/** + * Queues a sound_template to every client inside the vehicle's interior, seated or not, so it's + * also audible to unbuckled crew walking around the fighting compartment. + * + * Deliberately leaves template.x/y/z unset so the sound plays centered, same as any other + * non-positional sound, rather than going through the world-posittion/stereo-pan/reverb branch. + * + * Arguments: + * * vehicle = The vehicle whose interior occupants shoul hear this. + * * soundfile = The sound file to play. + * * sound_volume = Playback volume. + * * sound_frequency = Playback frequency (pitch/speed) override. + */ +/datum/looping_sound/tank_engine/proc/play_direct_to_crew(obj/vehicle/multitile/vehicle, soundfile, sound_volume, sound_frequency) + if(!istype(vehicle)) + return + + var/list/hearer_clients = vehicle.get_interior_mob_clients() + if(!length(hearer_clients)) + return + + if(!crew_channel) + crew_channel = get_free_channel() + + var/datum/sound_template/template = new() + template.file = get_sfx(soundfile) + template.volume = sound_volume + template.frequency = sound_frequency + template.channel = crew_channel + SSsound.queue(template, hearer_clients) + +/** + * Queues a normal positional sound_template centered on the vehicle's exterior turf, for anyone + * standing near it on the main map. + * + * Built manualy rather than via playsound() to skip its automatic interior-forwarding step, + * which would otherwise double up with play_direct_to_crew()'s copy. + * + * Arguments: + * * vehicle = The vehicle the sound originates from. + * * soundfile = The sound file to play. + * * sound_volume = Playback volume. + * * sound_frequency = Playback frequency (pitch/speed) override. + */ +/datum/looping_sound/tank_engine/proc/play_to_outside_hearers(obj/vehicle/multitile/vehicle, soundfile, sound_volume, sound_frequency) + var/turf/source_turf = get_turf(vehicle) + if(!source_turf) + return + + if(!outside_channel) + outside_channel = get_free_channel() + + var/datum/sound_template/template = new() + template.file = get_sfx(soundfile) + template.volume = sound_volume + template.frequency = sound_frequency + template.channel = outside_channel + template.x = source_turf.x + template.y = source_turf.y + template.z = source_turf.z + template.range = max(ENGINE_SOUND_MIN_RANGE, floor(0.25 * sound_volume)) + // falloff is the range before volume starts dropping. Half of range keeps it strong through + // the inner half of the audible radius, only tapering near the edge. + template.falloff = template.range * 0.5 + + SSsound.queue(template) + +/** + * Immediately silences whatever's still playing on the crew/outside channels, same trick as + * tank_tracks/on_stop() doubled up for two channels. Runs before ..() so the shutdown one-shot + * starts clean instead of racing the idle loop's tail. + */ +/datum/looping_sound/tank_engine/on_stop() + if(crew_channel && parent) + var/obj/vehicle/multitile/vehicle = parent + if(istype(vehicle)) + for(var/client/crew_client in vehicle.get_interior_mob_clients()) + SEND_SOUND(crew_client, sound(null, channel = crew_channel)) + + if(outside_channel) + var/turf/source_turf = get_turf(parent) + if(source_turf) + var/datum/sound_template/template = new() + template.channel = outside_channel + template.x = source_turf.x + template.y = source_turf.y + template.z = source_turf.z + template.range = extra_range || floor(0.25 * volume) + SSsound.queue(template) + + ..() + +/// Doesn't use the base TIMER_LOOP since clip duration changes with pitch. Schedules its own +/// first play() and lets sound_loop() reschedule itself each time. +/datum/looping_sound/tank_engine/start_sound_loop() + loop_started = TRUE + sound_loop() + +/** + * Re-implements the base sound_loop() to reschedule off the actual pitch-shifted playback + * duration instead of a fixed mid_length, avoiding an audible gap between loops. + */ +/datum/looping_sound/tank_engine/sound_loop(start_time) + if(!parent) + return + + var/computed_frequency = play(get_sound()) + var/playback_duration = native_duration * (ENGINE_SOUND_MIN_FREQUENCY / computed_frequency) + timer_id = addtimer(CALLBACK(src, PROC_REF(sound_loop)), playback_duration SECONDS, TIMER_CLIENT_TIME | TIMER_STOPPABLE | TIMER_DELETE_ME, timer_subsystem = SSsound_loops) + +// Source: quicksounds.com/sound/21067/tank-m551-start-stop. See sound/ATTRIBUTION.txt +/datum/looping_sound/tank_engine/apc + mid_sounds = 'sound/vehicles/apcidle.ogg' + native_duration = 1.662653 + start_sound = 'sound/vehicles/apcstart.ogg' + start_length = 3.052517 SECONDS + end_sound = 'sound/vehicles/apcstop.ogg' + +// Source: quicksounds.com/sound/21044/tank-m2-bradley-start-and-stop. See sound/ATTRIBUTION.txt +/datum/looping_sound/tank_engine/arc + mid_sounds = 'sound/vehicles/arcidle.ogg' + native_duration = 3.058571 + start_sound = 'sound/vehicles/arcstart.ogg' + start_length = 5.019161 SECONDS + end_sound = 'sound/vehicles/arcstop.ogg' 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..b50076da7006 100644 --- a/code/datums/supply_packs/vehicle_ammo.dm +++ b/code/datums/supply_packs/vehicle_ammo.dm @@ -2,15 +2,29 @@ //Vehicle Ammo //*******************************************************************************/ +/datum/supply_packs/jp8_canisters + name = "JP-8 Canisters (x2)" + contains = list( + /obj/item/reagent_container/glass/beaker/jp8_canister/full, + /obj/item/reagent_container/glass/beaker/jp8_canister/full, + ) + cost = 15 + containertype = /obj/structure/closet/crate/ammo + containername = "JP-8 canister crate" + group = "Vehicle Ammo" + +// Uses the single-round he_shell magazine to match what the LTB Cannon actually loads. /datum/supply_packs/ammo_ltb_cannon - name = "LTB Cannon magazines (x6)" + name = "86x455mm HE Shells (x8)" contains = list( - /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, + /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, ) cost = 30 containertype = /obj/structure/closet/crate/ammo @@ -55,6 +69,19 @@ containername = "Primary Flamer fuel tanks crate" group = "Vehicle Ammo" +// Smoke-tank equivalent of the fuel-tank pack above. +/datum/supply_packs/ammo_drgn_flamer_smoke + name = "DRG-N Offensive Flamer Unit smoke tanks (x3)" + contains = list( + /obj/item/ammo_magazine/hardpoint/primary_flamer/smoke, + /obj/item/ammo_magazine/hardpoint/primary_flamer/smoke, + /obj/item/ammo_magazine/hardpoint/primary_flamer/smoke, + ) + cost = 30 + containertype = /obj/structure/closet/crate/ammo/alt/flame + containername = "Primary Flamer smoke tanks crate" + group = "Vehicle Ammo" + /datum/supply_packs/ammo_dualcannon name = "PARS-159 Boyars Dualcannon magazines (x5)" contains = list( @@ -83,18 +110,25 @@ 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 name = "TOW Launcher magazines (x3)" contains = list(/obj/item/hardpoint/secondary/towlauncher) @@ -102,17 +136,7 @@ containertype = /obj/structure/closet/crate/ammo containername = "TOW launcher ammo crate" group = "Vehicle Ammo" - -/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, - ) - cost = 20 - containertype = /obj/structure/closet/crate/ammo - containername = "M56 Cupola ammo crate" - group = "Vehicle Ammo" +*/ /datum/supply_packs/tank_flamer_ammo name = "LZR-N Flamer Unit fuel tanks (x2)" @@ -136,19 +160,6 @@ containername = "Bleihagel RE-RE700 Frontal Cannon ammo crate" group = "Vehicle Ammo" -/datum/supply_packs/ammo_flarelauncher - name = "M-87F Flare Launcher magazines (x4)" - contains = list( - /obj/item/ammo_magazine/hardpoint/flare_launcher, - /obj/item/ammo_magazine/hardpoint/flare_launcher, - /obj/item/ammo_magazine/hardpoint/flare_launcher, - /obj/item/ammo_magazine/hardpoint/flare_launcher, - ) - cost = 20 - containertype = /obj/structure/closet/crate/ammo - containername = "M-87F Flare Launcher ammo crate" - group = "Vehicle Ammo" - /datum/supply_packs/ammo_arcsentry name = "RE700 Rotary Cannon magazines (x3)" contains = list( diff --git a/code/datums/supply_packs/vehicle_equipment.dm b/code/datums/supply_packs/vehicle_equipment.dm index df106761d467..9410cff0238d 100644 --- a/code/datums/supply_packs/vehicle_equipment.dm +++ b/code/datums/supply_packs/vehicle_equipment.dm @@ -1,3 +1,5 @@ +// --- Weapons (Bundled with ammo to artificially inflate costs to make it a bit more penalizing to lose them.) --- + /datum/supply_packs/arcsentry_replacement name = "Replacement RE700 Rotary Cannon (x1)" contains = list( @@ -7,3 +9,386 @@ containertype = /obj/structure/closet/crate/weapon containername = "RE700 Rotary Cannon crate" group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_primary_flamer + name = "Replacement DRG-N Offensive Flamer Unit (x1)" + contains = list( + /obj/item/hardpoint/primary/flamer, + /obj/item/ammo_magazine/hardpoint/primary_flamer, + /obj/item/ammo_magazine/hardpoint/primary_flamer, + /obj/item/ammo_magazine/hardpoint/primary_flamer, + ) + cost = 45 // ammo_drgn_flamer (30) + 15 hardware premium + containertype = /obj/structure/closet/crate/weapon + containername = "DRG-N Offensive Flamer Unit crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_ltb_cannon + name = "Replacement LTB Cannon (x1)" + contains = list( + /obj/item/hardpoint/primary/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, + ) + cost = 45 // ammo_ltb_cannon (30) + 15 hardware premium + containertype = /obj/structure/closet/crate/weapon + containername = "LTB Cannon crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_minigun + name = "Replacement LTAA-AP Minigun (x1)" + contains = list( + /obj/item/hardpoint/primary/minigun, + /obj/item/ammo_magazine/hardpoint/ltaaap_minigun, + /obj/item/ammo_magazine/hardpoint/ltaaap_minigun, + /obj/item/ammo_magazine/hardpoint/ltaaap_minigun, + ) + cost = 45 // ammo_ltaaap_minigun (30) + 15 hardware premium + containertype = /obj/structure/closet/crate/weapon + containername = "LTAA-AP Minigun crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_autocannon + name = "Replacement AC3-E Autocannon (x1)" + contains = list( + /obj/item/hardpoint/primary/autocannon, + /obj/item/ammo_magazine/hardpoint/ace_autocannon, + /obj/item/ammo_magazine/hardpoint/ace_autocannon, + /obj/item/ammo_magazine/hardpoint/ace_autocannon, + ) + cost = 45 // ammo_ace_autocannon (30) + 15 hardware premium + containertype = /obj/structure/closet/crate/weapon + containername = "AC3-E Autocannon crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_small_flamer + name = "Replacement LZR-N Flamer Unit (x1)" + contains = list( + /obj/item/hardpoint/secondary/small_flamer, + /obj/item/ammo_magazine/hardpoint/secondary_flamer, + /obj/item/ammo_magazine/hardpoint/secondary_flamer, + ) + cost = 35 // tank_flamer_ammo (20) + 15 hardware premium + containertype = /obj/structure/closet/crate/weapon + containername = "LZR-N Flamer Unit crate" + group = "Vehicle Equipment" + +/* Commenting this out since the TOW wasn't really touched by Desant yet. +/datum/supply_packs/tank_spare_towlauncher + name = "Replacement TOW Launcher (x1)" + contains = list( + /obj/item/hardpoint/secondary/towlauncher, + /obj/item/ammo_magazine/hardpoint/towlauncher, + ) + cost = 45 // ammo_towlauncher (30) + 15 hardware premium + containertype = /obj/structure/closet/crate/weapon + containername = "TOW Launcher crate" + group = "Vehicle Equipment" +*/ + +/datum/supply_packs/tank_spare_m56cupola + name = "Replacement M56 Cupola (x1)" + contains = list( + /obj/item/hardpoint/secondary/m56cupola, + /obj/item/ammo_magazine/m56d, + ) + cost = 35 // ammo_m56_cupola (20) + 15 hardware premium + containertype = /obj/structure/closet/crate/weapon + containername = "M56 Cupola crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_grenade_launcher + name = "Replacement M92T Grenade Launcher (x1)" + contains = list( + /obj/item/hardpoint/secondary/grenade_launcher, + /obj/item/ammo_magazine/hardpoint/tank_glauncher, + /obj/item/ammo_magazine/hardpoint/tank_glauncher, + /obj/item/ammo_magazine/hardpoint/tank_glauncher, + /obj/item/ammo_magazine/hardpoint/tank_glauncher, + /obj/item/ammo_magazine/hardpoint/tank_glauncher, + ) + cost = 35 // ammo_glauncher (20) + 15 hardware premium + containertype = /obj/structure/closet/crate/weapon + containername = "M92T Grenade Launcher crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_brute_launcher + name = "Replacement M6H-BRUTE Launcher (x1)" + contains = 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, + ) + cost = 45 // brute_rockets (30) + 15 hardware premium + containertype = /obj/structure/closet/crate/weapon + containername = "M6H-BRUTE Launcher crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_flag_mount + name = "Replacement United Americas Flag Mount (x1)" + contains = list(/obj/item/hardpoint/secondary/united_americas_flag) + cost = 20 + containertype = /obj/structure/closet/crate/weapon + containername = "United Americas Flag Mount crate" + group = "Vehicle Equipment" + +/datum/supply_packs/apc_spare_dualcannon + name = "Replacement PARS-159 Boyars Dualcannon (x1)" + contains = list( + /obj/item/hardpoint/primary/dualcannon, + /obj/item/ammo_magazine/hardpoint/boyars_dualcannon, + /obj/item/ammo_magazine/hardpoint/boyars_dualcannon, + /obj/item/ammo_magazine/hardpoint/boyars_dualcannon, + /obj/item/ammo_magazine/hardpoint/boyars_dualcannon, + /obj/item/ammo_magazine/hardpoint/boyars_dualcannon, + ) + cost = 45 // ammo_dualcannon (30) + 15 hardware premium + containertype = /obj/structure/closet/crate/weapon + containername = "PARS-159 Boyars Dualcannon crate" + group = "Vehicle Equipment" + +/datum/supply_packs/apc_spare_frontalcannon + name = "Replacement Bleihagel RE-RE700 Frontal Cannon (x1)" + contains = list( + /obj/item/hardpoint/secondary/frontalcannon, + /obj/item/ammo_magazine/hardpoint/m56_cupola/frontal_cannon, + /obj/item/ammo_magazine/hardpoint/m56_cupola/frontal_cannon, + ) + cost = 35 // ammo_frontalcannon (20) + 15 hardware premium + containertype = /obj/structure/closet/crate/weapon + containername = "Bleihagel RE-RE700 Frontal Cannon crate" + group = "Vehicle Equipment" + +// --- Structural/utility hardpoints --- + +/datum/supply_packs/tank_spare_turret_mount + name = "Replacement Turret Mount (x1)" + contains = list(/obj/item/hardpoint/holder/tank_turret) + cost = 25 // bare weapon-mounting socket, no weapon included + containertype = /obj/structure/closet/crate/construction + containername = "Turret Mount crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_engine + name = "Replacement Engine (x1)" + contains = list(/obj/item/hardpoint/engine/tank) + cost = 20 + containertype = /obj/structure/closet/crate/construction + containername = "Engine crate" + group = "Vehicle Equipment" + +/datum/supply_packs/apc_spare_engine + name = "Replacement APC Engine (x1)" + contains = list(/obj/item/hardpoint/engine/apc) + cost = 20 + containertype = /obj/structure/closet/crate/construction + containername = "APC Engine crate" + group = "Vehicle Equipment" + +/datum/supply_packs/arc_spare_engine + name = "Replacement ARC Engine (x1)" + contains = list(/obj/item/hardpoint/engine/arc) + cost = 20 + containertype = /obj/structure/closet/crate/construction + containername = "ARC Engine crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_fuel_tank + name = "Replacement Fuel Tank (x1)" + contains = list(/obj/item/hardpoint/fuel_tank/uscm) + cost = 10 + containertype = /obj/structure/closet/crate/construction + containername = "Fuel Tank crate" + group = "Vehicle Equipment" + +/datum/supply_packs/arc_spare_fuel_tank + name = "Replacement ARC Fuel Tank (x1)" + contains = list(/obj/item/hardpoint/fuel_tank/arc) + cost = 10 + containertype = /obj/structure/closet/crate/construction + containername = "ARC Fuel Tank crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_radiator + name = "Replacement Radiator (x1)" + contains = list(/obj/item/hardpoint/radiator/uscm) + cost = 10 + containertype = /obj/structure/closet/crate/construction + containername = "Radiator crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_battery + name = "Replacement Battery (x1)" + contains = list(/obj/item/hardpoint/battery/uscm) + cost = 10 + containertype = /obj/structure/closet/crate/construction + containername = "Battery crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_hatch + name = "Replacement Hatch (x1)" + contains = list(/obj/item/hardpoint/hatch/armored/uscm) + cost = 5 + containertype = /obj/structure/closet/crate/construction + containername = "Hatch crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_iff_module + name = "Replacement IFF Module (x1)" + contains = list(/obj/item/hardpoint/iff_module/uscm) + cost = 10 + containertype = /obj/structure/closet/crate/construction + containername = "IFF Module crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_visual_sensors + name = "Replacement Visual Sensors (x1)" + contains = list(/obj/item/hardpoint/visual_sensors/uscm) + cost = 10 + containertype = /obj/structure/closet/crate/construction + containername = "Visual Sensors crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_turret_ring + name = "Replacement Turret Ring (x1)" + contains = list(/obj/item/hardpoint/turret_ring/uscm) + cost = 10 + containertype = /obj/structure/closet/crate/construction + containername = "Turret Ring crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_air_filter + name = "Replacement Air Filter (x1)" + contains = list(/obj/item/hardpoint/air_filter/uscm) + cost = 5 + containertype = /obj/structure/closet/crate/construction + containername = "Air Filter crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_treads + name = "Replacement Treads (x1)" + contains = list(/obj/item/hardpoint/locomotion/treads) + cost = 15 + containertype = /obj/structure/closet/crate/construction + containername = "Treads crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_treads_robust + name = "Replacement Reinforced Treads (x1)" + contains = list(/obj/item/hardpoint/locomotion/treads/robust) + cost = 15 + containertype = /obj/structure/closet/crate/construction + containername = "Reinforced Treads crate" + group = "Vehicle Equipment" + +/datum/supply_packs/apc_spare_wheels + name = "Replacement APC Wheels (x1)" + contains = list(/obj/item/hardpoint/locomotion/apc_wheels) + cost = 15 + containertype = /obj/structure/closet/crate/construction + containername = "APC Wheels crate" + group = "Vehicle Equipment" + +/datum/supply_packs/arc_spare_wheels + name = "Replacement ARC Wheels (x1)" + contains = list(/obj/item/hardpoint/locomotion/arc_wheels) + cost = 15 + containertype = /obj/structure/closet/crate/construction + containername = "ARC Wheels crate" + group = "Vehicle Equipment" + +// --- Armor plating --- + +/datum/supply_packs/tank_spare_armor_ballistic + name = "Ballistic Armor Plating (x1)" + contains = list(/obj/item/hardpoint/armor/ballistic) + cost = 20 + containertype = /obj/structure/closet/crate/construction + containername = "Ballistic Armor Plating crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_armor_caustic + name = "Caustic-Resistant Armor Plating (x1)" + contains = list(/obj/item/hardpoint/armor/caustic) + cost = 20 + containertype = /obj/structure/closet/crate/construction + containername = "Caustic-Resistant Armor Plating crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_armor_concussive + name = "Concussive-Dampening Armor Plating (x1)" + contains = list(/obj/item/hardpoint/armor/concussive) + cost = 20 + containertype = /obj/structure/closet/crate/construction + containername = "Concussive-Dampening Armor Plating crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_armor_paladin + name = "Paladin Armor Plating (x1)" + contains = list(/obj/item/hardpoint/armor/paladin) + cost = 20 + containertype = /obj/structure/closet/crate/construction + containername = "Paladin Armor Plating crate" + group = "Vehicle Equipment" + +// --- Not armor, just a utility bumper in its own hardpoint slot --- + +/datum/supply_packs/tank_spare_armor_snowplow + name = "Snowplow (x1)" + contains = list(/obj/item/hardpoint/snowplow) + cost = 5 + containertype = /obj/structure/closet/crate/construction + containername = "Snowplow crate" + group = "Vehicle Equipment" + +// --- Support modules --- + +/datum/supply_packs/tank_spare_overdrive + name = "Overdrive Enhancer (x1)" + contains = list(/obj/item/hardpoint/support/overdrive_enhancer) + cost = 20 + containertype = /obj/structure/closet/crate/construction + containername = "Overdrive Enhancer crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_iwsa + name = "Integrated Weapons Sensor Array (x1)" + contains = list(/obj/item/hardpoint/support/weapons_sensor) + cost = 20 + containertype = /obj/structure/closet/crate/construction + containername = "Integrated Weapons Sensor Array crate" + group = "Vehicle Equipment" + +/datum/supply_packs/tank_spare_artillery_module + name = "Artillery Module (x1)" + contains = list(/obj/item/hardpoint/support/artillery_module) + cost = 20 + containertype = /obj/structure/closet/crate/construction + containername = "Artillery Module crate" + group = "Vehicle Equipment" + +/datum/supply_packs/apc_spare_flare_launcher + name = "Replacement M-87F Flare Launcher (x1)" + contains = list(/obj/item/hardpoint/support/flare_launcher) + cost = 20 + containertype = /obj/structure/closet/crate/construction + containername = "M-87F Flare Launcher crate" + group = "Vehicle Equipment" + +/datum/supply_packs/arc_spare_antenna + name = "Replacement U-56 Radar Antenna (x1)" + contains = list(/obj/item/hardpoint/support/arc_antenna) + cost = 20 + containertype = /obj/structure/closet/crate/construction + containername = "U-56 Radar Antenna crate" + group = "Vehicle Equipment" diff --git a/code/datums/vehicle_hardpoint_hud.dm b/code/datums/vehicle_hardpoint_hud.dm new file mode 100644 index 000000000000..d2253c7a033a --- /dev/null +++ b/code/datums/vehicle_hardpoint_hud.dm @@ -0,0 +1,74 @@ +/** + * Global visibility gate for the vehicle hardpoint status HUD. Mirrors /datum/mob_hud, just + * re-targeted at vehicles instead of mobs. Only one kind exists, so a single global instance is enough. + */ +/datum/vehicle_hardpoint_hud + /// Every vehicle currenttly enrolled. + var/list/obj/vehicle/multitile/hudvehicles = list() + /// Viewer mob to list of sources currently granting them this HUD. Only removing the last one revokes it. + var/list/mob/hudusers = list() + +/// Allow user to see every enrolled vehicle's hardpoint HUD (e.g. flipping down a compatible helmet visor). +/datum/vehicle_hardpoint_hud/proc/add_hud_to(mob/user, source) + if(!user) + return + if(LAZYLEN(hudusers[user])) + hudusers[user] |= list(source) + else + hudusers[user] = list(source) + for(var/obj/vehicle/multitile/vehicle as anything in hudvehicles) + add_to_single_hud(user, vehicle) + +/// Stop showing every enrolled vehicle's hardpoint HUD to user, once EVERY granting source has been removed. +/datum/vehicle_hardpoint_hud/proc/remove_hud_from(mob/user, source) + if(!user || !LAZYLEN(hudusers[user])) + return + hudusers[user] -= source + if(LAZYLEN(hudusers[user])) + return + for(var/obj/vehicle/multitile/vehicle as anything in hudvehicles) + remove_from_single_hud(user, vehicle) + hudusers -= user + +/// "Enroll" a freshly-created vehicle. Every current viewer immediately sees its hardpoint images. +/datum/vehicle_hardpoint_hud/proc/add_to_hud(obj/vehicle/multitile/vehicle) + hudvehicles |= vehicle + for(var/mob/user as anything in hudusers) + add_to_single_hud(user, vehicle) + +/// "Unenroll" a vehicle that's about to be deleted. +/datum/vehicle_hardpoint_hud/proc/remove_from_hud(obj/vehicle/multitile/vehicle) + for(var/mob/user as anything in hudusers) + remove_from_single_hud(user, vehicle) + hudvehicles -= vehicle + +/datum/vehicle_hardpoint_hud/proc/add_to_single_hud(mob/user, obj/vehicle/multitile/vehicle) + if(!user.client) + return + for(var/slot in vehicle.hardpoint_hud_images) + user.client.images |= vehicle.hardpoint_hud_images[slot] + +/datum/vehicle_hardpoint_hud/proc/remove_from_single_hud(mob/user, obj/vehicle/multitile/vehicle) + if(!user.client) + return + for(var/slot in vehicle.hardpoint_hud_images) + user.client.images -= vehicle.hardpoint_hud_images[slot] + +/** + * Keeps every current viewer's client.images in sync the instant one specific slot's image is + * added, replaced, or removed on a vehicle. + * + * Arguments: + * * old_image = The image to revoke from every current viewer, or null if this slot didn't exist before. + * * new_image = The image to grant to every current viewer, or null if this slot no longer exists. + */ +/datum/vehicle_hardpoint_hud/proc/sync_single_image(obj/vehicle/multitile/vehicle, image/old_image, image/new_image) + for(var/mob/user as anything in hudusers) + if(!user.client) + continue + if(old_image) + user.client.images -= old_image + if(new_image) + user.client.images |= new_image + +GLOBAL_DATUM_INIT(vehicle_hardpoint_hud, /datum/vehicle_hardpoint_hud, new) diff --git a/code/game/atoms.dm b/code/game/atoms.dm index a76a2e442808..c0d2b4190b9a 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -651,6 +651,13 @@ Parameters are passed from New. /atom/proc/handle_tail_stab(mob/living/carbon/xenomorph/xeno, blunt_stab) return TAILSTAB_COOLDOWN_NONE +/** + * A short "attack target" description for flavor messages. Multitile vehicles override this to also name + * whichever module is being aimed at. + */ +/atom/proc/get_attack_desc(atom/attacker) + return "\the [src]" + /atom/proc/handle_flamer_fire(obj/flamer_fire/fire, damage, delta_time) return diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 68ea2b137818..17309c805257 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -39,6 +39,13 @@ /// 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 + /// If TRUE, a tank crash never scatters this atom off the hull, for effects that track the tank itself. used, for example, for flames and acid effects. + var/immune_to_tank_crash_scatter = FALSE /// A weakref to the mob currently interacting with us. var/datum/weakref/interactor @@ -500,8 +507,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 +565,12 @@ return NO_BLOCKED_MOVEMENT return ..() + +/atom/movable/proc/get_tank_on_top_of() + var/datum/component/vehicle_rider/rider = GetComponent(/datum/component/vehicle_rider) + if(!rider) + return null + return rider.get_vehicle() + +/atom/movable/proc/is_atop_vehicle() + return GetComponent(/datum/component/vehicle_rider) ? TRUE : FALSE diff --git a/code/game/jobs/job/command/auxiliary/tank_crew.dm b/code/game/jobs/job/command/auxiliary/tank_crew.dm index e9c3dd769cc9..2bda385eaaf2 100644 --- a/code/game/jobs/job/command/auxiliary/tank_crew.dm +++ b/code/game/jobs/job/command/auxiliary/tank_crew.dm @@ -5,12 +5,13 @@ allow_additional = TRUE scaled = TRUE supervisors = "the acting commanding officer" - flags_startup_parameters = ROLE_ADD_TO_DEFAULT + // ROLE_ADD_TO_SQUAD defaults them to Foxtrot, but they're still transferable to any squad afterward. + flags_startup_parameters = ROLE_ADD_TO_DEFAULT|ROLE_ADD_TO_SQUAD gear_preset = /datum/equipment_preset/uscm/tank entry_message_body = "Your job is to operate and maintain the ship's armored vehicles. You are in charge of representing the armored presence amongst the marines during the operation, as well as maintaining and repairing your own vehicles." /datum/job/command/tank_crew/set_spawn_positions(count) - if (length(GLOB.clients) >= 200) + if (length(GLOB.clients) >= 130) spawn_positions = 2 else spawn_positions = 0 @@ -18,7 +19,7 @@ /datum/job/command/tank_crew/get_total_positions(latejoin = FALSE) if(SStechtree.trees[TREE_MARINE].get_node(/datum/tech/arc).unlocked) return 0 - if(length(GLOB.clients) >= 200 || total_positions_so_far > 0) + if(length(GLOB.clients) >= 130 || total_positions_so_far > 0) return 2 return 0 diff --git a/code/game/jobs/job/marine/squads.dm b/code/game/jobs/job/marine/squads.dm index 8d6f74f22bc3..2d31f1ed41c1 100644 --- a/code/game/jobs/job/marine/squads.dm +++ b/code/game/jobs/job/marine/squads.dm @@ -89,6 +89,8 @@ var/count = 0 //Current # in the squad var/list/marines_list = list() // list of mobs (or name, not always a mob ref) in that squad. + /// Vehicles whose overwatch is assigned to this squad + var/list/overwatch_vehicles = list() var/mob/living/carbon/human/overwatch_officer = null //Who's overwatching this squad? COOLDOWN_DECLARE(next_supplydrop) @@ -184,7 +186,8 @@ omni_squad_vendor = TRUE radio_freq = CRYO_FREQ - active = FALSE + // Active from round start so overwatch can reach Foxtrot now that tank crewmen start off into them. Remind me to remove this once ASS gets merged to allow tank crewmen to get assigned in accordance to their prefs. + active = TRUE roundstart = FALSE locked = TRUE @@ -761,6 +764,16 @@ if(slot_check && !isnull(roles_cap[slot_check])) roles_in[slot_check]-- +/// Adds a vehicle to this squad's overwatch roster. No-op if already tracked. +/datum/squad/proc/add_overwatch_vehicle(obj/vehicle/multitile/V) + if(V in overwatch_vehicles) + return + overwatch_vehicles += V + +/// Removes a vehicle from this squad's overwatch roster. +/datum/squad/proc/remove_overwatch_vehicle(obj/vehicle/multitile/V) + overwatch_vehicles -= V + //proc for demoting current Squad Leader /datum/squad/proc/demote_squad_leader(leader_killed) var/mob/living/carbon/human/old_lead = squad_leader diff --git a/code/game/jobs/role_authority.dm b/code/game/jobs/role_authority.dm index ce8e2666e480..a899a99c1ccf 100644 --- a/code/game/jobs/role_authority.dm +++ b/code/game/jobs/role_authority.dm @@ -578,6 +578,15 @@ I hope it's easier to tell what the heck this proc is even doing, unlike previou intel_squad.put_marine_in_squad(human) return + // Tank crew default to Foxtrot instead of a randomized combat squad, but are still fully transferable. + if(human.job == JOB_TANK_CREW) + var/datum/squad/tank_crew_squad = get_squad_by_name(SQUAD_MARINE_CRYO) + if(!tank_crew_squad || !istype(tank_crew_squad)) //Something went horribly wrong! + to_chat(human, "Something went wrong with randomize_squad()! Tell a coder!") + return + tank_crew_squad.put_marine_in_squad(human) + return + var/slot_check if(human.job != "Reinforcements") slot_check = GET_DEFAULT_ROLE(human.job) diff --git a/code/game/machinery/doors/multi_tile.dm b/code/game/machinery/doors/multi_tile.dm index b6fbe25f214c..dfda9034fca9 100644 --- a/code/game/machinery/doors/multi_tile.dm +++ b/code/game/machinery/doors/multi_tile.dm @@ -303,6 +303,14 @@ queen_pryable = TRUE var/obj/docking_port/mobile/marine_dropship/linked_dropship +/** + * The tank's designed entry/exit point, so just open for it instead of the generic vehicle bump handling. + * Sets V.skip_crash_response_entirely, since this "blocked" attempt isn't a real crash. + */ +/obj/structure/machinery/door/airlock/multi_tile/almayer/dropshiprear/handle_vehicle_bump(obj/vehicle/multitile/V) + bumpopen(V.get_seat_mob(VEHICLE_DRIVER)) + V.skip_crash_response_entirely = TRUE + return FALSE /obj/structure/machinery/door/airlock/multi_tile/almayer/dropshiprear/ex_act(severity) return diff --git a/code/game/machinery/vending/vending_types.dm b/code/game/machinery/vending/vending_types.dm index 6500d265898f..a090d5a13208 100644 --- a/code/game/machinery/vending/vending_types.dm +++ b/code/game/machinery/vending/vending_types.dm @@ -709,6 +709,7 @@ /obj/item/device/cassette_tape/heavymetal = 10, /obj/item/device/cassette_tape/hairmetal = 10, /obj/item/device/cassette_tape/indie = 10, + /obj/item/device/cassette_tape/liberation = 10, /obj/item/device/walkman = 50, /obj/item/device/walkman/white_band = 50, /obj/item/storage/pouch/cassette = 15, @@ -743,6 +744,7 @@ /obj/item/device/cassette_tape/heavymetal = 5, /obj/item/device/cassette_tape/hairmetal = 5, /obj/item/device/cassette_tape/indie = 5, + /obj/item/device/cassette_tape/liberation = 5, /obj/item/device/walkman = 15, /obj/item/device/walkman/white_band = 15, /obj/item/storage/pouch/cassette = 10, 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..f2507e26da2e 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), @@ -153,7 +156,13 @@ GLOBAL_LIST_INIT(cm_vending_vehicle_crew_tank, list( list("Overdrive Enhancer", 0, /obj/item/hardpoint/support/overdrive_enhancer, VEHICLE_SUPPORT_AVAILABLE, VENDOR_ITEM_RECOMMENDED), list("ARMOR", 0, null, null, null), - list("Snowplow", 0, /obj/item/hardpoint/armor/snowplow, VEHICLE_ARMOR_AVAILABLE, VENDOR_ITEM_REGULAR), + list("Ballistic Armor", 0, /obj/item/hardpoint/armor/ballistic, VEHICLE_ARMOR_AVAILABLE, VENDOR_ITEM_REGULAR), + list("Caustic Armor", 0, /obj/item/hardpoint/armor/caustic, VEHICLE_ARMOR_AVAILABLE, VENDOR_ITEM_REGULAR), + list("Concussive Armor", 0, /obj/item/hardpoint/armor/concussive, VEHICLE_ARMOR_AVAILABLE, VENDOR_ITEM_REGULAR), + list("Paladin Armor", 0, /obj/item/hardpoint/armor/paladin, VEHICLE_ARMOR_AVAILABLE, VENDOR_ITEM_REGULAR), + + list("UTILITY", 0, null, null, null), + list("Snowplow", 0, /obj/item/hardpoint/snowplow, VEHICLE_UTILITY_AVAILABLE, VENDOR_ITEM_REGULAR), list("TREADS", 0, null, null, null), list("Reinforced Treads", 0, /obj/item/hardpoint/locomotion/treads/robust, VEHICLE_TREADS_AVAILABLE, VENDOR_ITEM_REGULAR), @@ -193,7 +202,7 @@ GLOBAL_LIST_INIT(cm_vending_vehicle_crew_apc_spare, list( list("M-97F Flare Launcher", 300, /obj/item/hardpoint/support/flare_launcher, null, VENDOR_ITEM_REGULAR), list("SUPPORT AMMUNITION", 0, null, null, null), - list("M-97F Flare Launcher Magazine", 50, /obj/item/ammo_magazine/hardpoint/flare_launcher, null, VENDOR_ITEM_REGULAR), + list("M74 AGM-S Star Shell Packet", 50, /obj/item/storage/box/packet/flare, null, VENDOR_ITEM_REGULAR), list("WHEELS", 0, null, null, null), list("APC Wheels", 200, /obj/item/hardpoint/locomotion/apc_wheels, null, VENDOR_ITEM_REGULAR))) @@ -270,6 +279,7 @@ GLOBAL_LIST_INIT(cm_vending_clothing_vehicle_crew, list( list("Tanker Armor", 0, /obj/item/clothing/suit/storage/marine/tanker, MARINE_CAN_BUY_ARMOR, VENDOR_ITEM_MANDATORY), list("M50 Tanker Helmet", 0, /obj/item/clothing/head/helmet/marine/tech/tanker, MARINE_CAN_BUY_HELMET, VENDOR_ITEM_MANDATORY), list("Medical Helmet Optic", 0, /obj/item/device/helmet_visor/medical, MARINE_CAN_BUY_GLASSES, VENDOR_ITEM_MANDATORY), + list("Vehicle Diagnostics Optic", 0, /obj/item/device/helmet_visor/vehicle, MARINE_CAN_BUY_GLASSES, VENDOR_ITEM_MANDATORY), list("Welding Kit", 0, /obj/item/tool/weldpack, MARINE_CAN_BUY_BACKPACK, VENDOR_ITEM_MANDATORY), list("MRE", 0, /obj/item/storage/box/mre, MARINE_CAN_BUY_MRE, VENDOR_ITEM_MANDATORY), @@ -347,6 +357,7 @@ GLOBAL_LIST_INIT(cm_vending_clothing_vehicle_crew, list( list("M44 Heavy Speed Loader (.44)", 10, /obj/item/ammo_magazine/revolver/heavy, null, VENDOR_ITEM_REGULAR), list("UTILITIES", 0, null, null, null), + list("JP-8 Fuel Canister", 25, /obj/item/reagent_container/glass/beaker/jp8_canister/full, null, VENDOR_ITEM_REGULAR), list("Binoculars", 10, /obj/item/device/binoculars, null, VENDOR_ITEM_REGULAR), list("Fire Extinguisher (Portable)", 5, /obj/item/tool/extinguisher/mini, null, VENDOR_ITEM_REGULAR), list("Fuel Tank Strap Pouch", 5, /obj/item/storage/pouch/flamertank, null, VENDOR_ITEM_REGULAR), @@ -375,11 +386,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 +445,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 +458,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 @@ -465,7 +501,6 @@ GLOBAL_LIST_INIT(cm_vending_clothing_vehicle_crew, list( /obj/effect/essentials_set/apc/flarelauncher spawned_gear_list = list( /obj/item/hardpoint/support/flare_launcher, - /obj/item/ammo_magazine/hardpoint/flare_launcher, - /obj/item/ammo_magazine/hardpoint/flare_launcher, - /obj/item/ammo_magazine/hardpoint/flare_launcher, + /obj/item/storage/box/packet/flare, + /obj/item/storage/box/packet/flare, ) diff --git a/code/game/machinery/vending/vendor_types/general.dm b/code/game/machinery/vending/vendor_types/general.dm index 9f72a2930f09..2a7751aab081 100644 --- a/code/game/machinery/vending/vendor_types/general.dm +++ b/code/game/machinery/vending/vendor_types/general.dm @@ -22,6 +22,7 @@ GLOBAL_LIST_INIT(cm_vending_walkman, list( list("Red-Black Cassette", 10, /obj/item/device/cassette_tape/heavymetal, VENDOR_ITEM_REGULAR), list("Red Striped Cassette", 10, /obj/item/device/cassette_tape/hairmetal, VENDOR_ITEM_REGULAR), list("Rising Sun Cassette", 10, /obj/item/device/cassette_tape/indie, VENDOR_ITEM_REGULAR), + list("Liberation Cassette", 10, /obj/item/device/cassette_tape/liberation, VENDOR_ITEM_REGULAR), list("Walkman", 50, /obj/item/device/walkman, VENDOR_ITEM_REGULAR), list("Walkman (White Band)", 50, /obj/item/device/walkman/white_band, VENDOR_ITEM_REGULAR), list("Cassette Pouch", 15, /obj/item/storage/pouch/cassette, VENDOR_ITEM_REGULAR), diff --git a/code/game/objects/effects/aliens.dm b/code/game/objects/effects/aliens.dm index 77865af44596..c89c1504c9c4 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 @@ -126,15 +126,16 @@ continue - if(isVehicleMultitile(atm)) - var/obj/vehicle/multitile/V = atm - V.handle_acidic_environment(src) - continue if (istype(loc, /turf/open)) var/turf/open/scorch_turf_target = loc if(scorch_turf_target.scorchable) scorch_turf_target.scorch(damage_amount) + // Needed since a multitile vehicle only really occupies its one anchor turf. + var/obj/vehicle/multitile/vehicle_here = get_multitile_vehicle_at(loc) + if(vehicle_here) + vehicle_here.handle_acidic_environment(src) + START_PROCESSING(SSobj, src) addtimer(CALLBACK(src, PROC_REF(die)), time_to_live) animate(src, time_to_live, alpha = 128) @@ -700,6 +701,11 @@ for (var/obj/structure/barricade/B in loc) B.take_acid_damage(damage*(1.15 + 0.55 * empowered)) + // Needed since a parked tank's footprint can span several of the mine's detonating tiles. + var/obj/vehicle/multitile/vehicle_here = get_multitile_vehicle_at(loc) + if(vehicle_here) + vehicle_here.expose_to_acid_mine(src) + for (var/mob/living/carbon/human in loc) if (human.stat == DEAD) continue diff --git a/code/game/objects/effects/effect_system/smoke.dm b/code/game/objects/effects/effect_system/smoke.dm index 3c975f5ae83e..2355f4f6be35 100644 --- a/code/game/objects/effects/effect_system/smoke.dm +++ b/code/game/objects/effects/effect_system/smoke.dm @@ -23,6 +23,10 @@ var/datum/cause_data/cause_data = null //does it obscure aim var/obscuring = TRUE + /// Vehicle this instance was bridged into. Null for a non-bridged placement. + var/obj/vehicle/multitile/bridged_vehicle + /// Fraction (0-1) of a bridged instance's normal damage, effects and alpha that gets through. + var/gas_leak_fraction = 1 //Remove this bit to use the old smoke icon = 'icons/effects/96x96.dmi' @@ -535,10 +539,58 @@ smokeranking = SMOKE_RANK_HARMLESS amount = 0 +/** + * Harmless, but still neat to add into vehicles for flavor. + */ +/obj/effect/particle_effect/smoke/acid_runner_harmless/apply_smoke_effect(turf/cur_turf) + if(bridged_vehicle && world.time > bridged_vehicle.next_gas_exposure_time) + qdel(src) + return + ..() + handle_vehicle_gas_exposure(cur_turf) + ///////////////////////////////////////// // BOILER SMOKES ///////////////////////////////////////// +/** + * Exposes a vehicle sitting in Boiler gas to damage and wounds, and leaks a weakened + * copy into the crew compartmentt through the air filter. + */ +/obj/effect/particle_effect/smoke/proc/handle_vehicle_gas_exposure(turf/cur_turf) + var/obj/vehicle/multitile/vehicle = get_multitile_vehicle_at(cur_turf) + if(!vehicle) + return + + var/obj/vehicle/multitile/tank/tank = istype(vehicle, /obj/vehicle/multitile/tank) ? vehicle : null + // acid_runner_harmless counts as acid too, even though it isn't a xeno_burn subtype. + var/is_acid = istype(src, /obj/effect/particle_effect/smoke/xeno_burn) || istype(src, /obj/effect/particle_effect/smoke/acid_runner_harmless) + + if(world.time < vehicle.next_gas_exposure_time) + return + vehicle.next_gas_exposure_time = world.time + (tank ? TANK_GLOB_EXPOSURE_INTERVAL : VEHICLE_GAS_EXPOSURE_INTERVAL) + + if(tank) + // Matches tile coverage against this call's own smoke type, rather than a hardcoded pick. + var/gas_type_to_match = src.type + var/covered_tiles = 0 + for(var/turf/vehicle_turf as anything in tank.locs) + if(locate(gas_type_to_match) in vehicle_turf) + covered_tiles++ + var/covered_fraction = covered_tiles / length(tank.locs) + + if(is_acid) + tank.apply_glob_acid_tick(covered_fraction, cause_data?.resolve_mob()) + else + tank.apply_glob_neuro_tick(covered_fraction) + else + if(is_acid) + vehicle.take_damage_type(15, "acid") + vehicle.expose_to_boiler_gas() + + // Interior leak-in is shared with every other smoke source that touches the vehicle. + vehicle.bridge_smoke_into_interior(src.type) + //Xeno acid smoke. /obj/effect/particle_effect/smoke/xeno_burn time_to_live = 12 @@ -546,6 +598,8 @@ anchored = TRUE spread_speed = 6 smokeranking = SMOKE_RANK_BOILER + // Sits above a tank's turret so the glob cloud visibly covers the whole tank. + layer = TANK_ABOVE_RIDER_LAYER affect_on_cross = FALSE var/hivenumber = XENO_HIVE_NORMAL @@ -562,18 +616,22 @@ return ..() /obj/effect/particle_effect/smoke/xeno_burn/apply_smoke_effect(turf/cur_turf) + if(bridged_vehicle && world.time > bridged_vehicle.next_gas_exposure_time) + qdel(src) + return ..() for(var/obj/structure/barricade/barricade in cur_turf) barricade.take_acid_damage(XENO_ACID_GAS_BARRICADE_DAMAGE) if(prob(75)) // anti sound spam playsound(src, pick("acid_sizzle", "acid_hit"), 25) - for(var/obj/vehicle/multitile/vehicle in cur_turf) - vehicle.take_damage_type(15, "acid") + // Vehicle damage is handled by handle_vehicle_gas_exposure() below instead of here. for(var/obj/structure/machinery/m56d_hmg/auto/gun in cur_turf) gun.update_health(XENO_ACID_HMG_DAMAGE) + handle_vehicle_gas_exposure(cur_turf) + /obj/effect/particle_effect/smoke/xeno_burn/affect(mob/living/carbon/affected_mob) . = ..() if(!.) @@ -588,14 +646,14 @@ return FALSE affected_mob.last_damage_data = cause_data - affected_mob.apply_damage(3, OXY) //Basic oxyloss from "can't breathe" + affected_mob.apply_damage(3 * gas_leak_fraction, OXY) //Basic oxyloss from "can't breathe" if(isxeno(affected_mob)) - affected_mob.apply_damage(gas_damage * XVX_ACID_DAMAGEMULT, BURN) //Inhalation damage + affected_mob.apply_damage(gas_damage * XVX_ACID_DAMAGEMULT * gas_leak_fraction, BURN) //Inhalation damage else - affected_mob.apply_damage(gas_damage, BURN) //Inhalation damage + affected_mob.apply_damage(gas_damage * gas_leak_fraction, BURN) //Inhalation damage - if(affected_mob.coughedtime < world.time && !affected_mob.stat && ishuman(affected_mob)) //Coughing/gasping + if(affected_mob.coughedtime < world.time && !affected_mob.stat && ishuman(affected_mob) && prob(gas_leak_fraction * 100)) //Coughing/gasping - a light leak only occasionally catches your throat affected_mob.coughedtime = world.time + 1.5 SECONDS if(issynth(affected_mob)) affected_mob.visible_message(SPAN_DANGER("[affected_mob]'s skin is sloughing off!"), @@ -610,9 +668,9 @@ to_chat(affected_mob, SPAN_DANGER("Your skin feels like it is melting away!")) if(ishuman(affected_mob)) var/mob/living/carbon/human/human = affected_mob - human.apply_armoured_damage(amount*rand(15, 20), ARMOR_BIO, BURN) //Burn damage, randomizes between various parts //Amount corresponds to upgrade level, 1 to 2.5 + human.apply_armoured_damage(amount*rand(15, 20) * gas_leak_fraction, ARMOR_BIO, BURN) //Burn damage, randomizes between various parts //Amount corresponds to upgrade level, 1 to 2.5 else - affected_mob.burn_skin(5) //Failsafe for non-humans + affected_mob.burn_skin(5 * gas_leak_fraction) //Failsafe for non-humans affected_mob.last_damage_data = cause_data return TRUE @@ -623,12 +681,20 @@ spread_speed = 5 amount = 1 //Amount depends on Boiler upgrade! smokeranking = SMOKE_RANK_BOILER + // Same layer fix as xeno_burn above. + layer = TANK_ABOVE_RIDER_LAYER affect_on_cross = FALSE /// How much neuro is dosed per tick var/neuro_dose = 6 var/msg = "Your skin tingles as the gas consumes you!" // Message given per tick. Changes depending on which species is hit. +/obj/effect/particle_effect/smoke/xeno_weak/apply_smoke_effect(turf/cur_turf) + if(bridged_vehicle && world.time > bridged_vehicle.next_gas_exposure_time) + qdel(src) + return + ..() + handle_vehicle_gas_exposure(cur_turf) /obj/effect/particle_effect/smoke/xeno_weak/affect(mob/living/carbon/moob) // This applies every tick someone is in the smoke . = ..() @@ -649,18 +715,18 @@ if(human_moob.chem_effect_flags & CHEM_EFFECT_RESIST_NEURO) return FALSE - var/effect_amt = floor(6 + amount*6) + var/effect_amt = floor((6 + amount*6) * gas_leak_fraction) moob.eye_blurry = max(moob.eye_blurry, effect_amt) moob.EyeBlur(max(moob.eye_blurry, effect_amt)) - moob.apply_damage(5, OXY) // Base "I can't breath oxyloss" Slightly more longer lasting then stamina damage + moob.apply_damage(5 * gas_leak_fraction, OXY) // Base "I can't breath oxyloss" Slightly more longer lasting then stamina damage // reworked code below if(!issynth(moob)) var/datum/effects/neurotoxin/neuro_effect = locate() in moob.effects_list if(!neuro_effect) neuro_effect = new(moob, cause_data.resolve_mob()) neuro_effect.strength = effect_amt - neuro_effect.duration += neuro_dose - if(human_moob && moob.coughedtime < world.time && !moob.stat) //Coughing/gasping + neuro_effect.duration += neuro_dose * gas_leak_fraction + if(human_moob && moob.coughedtime < world.time && !moob.stat && prob(gas_leak_fraction * 100)) //Coughing/gasping - a light leak only occasionally catches your throat moob.coughedtime = world.time + 1.5 SECONDS if(prob(50)) moob.Slow(1) @@ -669,7 +735,8 @@ moob.emote("gasp") else msg = "You are consumed by the harmless gas, it is hard to navigate in!" - moob.Slow(1) + if(prob(gas_leak_fraction * 100)) + moob.Slow(1) to_chat(moob, SPAN_DANGER(msg)) return TRUE 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/devices/binoculars.dm b/code/game/objects/items/devices/binoculars.dm index 948a0f5fc47f..4c032b80aed5 100644 --- a/code/game/objects/items/devices/binoculars.dm +++ b/code/game/objects/items/devices/binoculars.dm @@ -1,3 +1,7 @@ +/// Whether the given mob is inside a tank's interior, where binoculars don't make sense. +/proc/is_in_tank_interior(mob/user) + return user && istype(get_area(user), /area/interior/vehicle/tank) + /obj/item/device/binoculars name = "binoculars" gender = PLURAL @@ -32,6 +36,11 @@ /obj/item/device/binoculars/attack_self(mob/user) ..() + // Only blocks a real held pair, not a vehicle-owned hidden instance like gunner_rangefinder. + if((loc == user) && is_in_tank_interior(user)) + to_chat(user, SPAN_WARNING("There's not enough room to use [src] properly in here.")) + return + if(SEND_SIGNAL(user, COMSIG_BINOCULAR_ATTACK_SELF, src)) return @@ -88,6 +97,7 @@ var/laser_cooldown = 0 var/cooldown_duration = 200 //20 seconds var/obj/effect/overlay/temp/laser_coordinate/coord + var/acquiring = FALSE var/target_acquisition_delay = 100 //10 seconds var/rangefinder_popup = TRUE //Whether coordinates are displayed in a separate popup window. var/last_x = "UNKNOWN" @@ -136,6 +146,9 @@ /obj/item/device/binoculars/range/handle_click(mob/living/carbon/human/user, atom/targeted_atom, list/mods) if(!istype(user)) return + if((loc == user) && is_in_tank_interior(user)) + to_chat(user, SPAN_WARNING("There's not enough room to use [src] properly in here.")) + return FALSE if(mods[CTRL_CLICK]) if(user.stat != CONSCIOUS) to_chat(user, SPAN_WARNING("You cannot use [src] while incapacitated.")) @@ -144,14 +157,16 @@ return FALSE if(mods[CLICK_CATCHER]) return FALSE - if(user.z != targeted_atom.z && !coord) + // Use the vehicle's real-world position for a hidden gunner rangefinder instance. + var/atom/position_ref = (loc == user) ? user : loc + if(position_ref.z != targeted_atom.z && !coord) to_chat(user, SPAN_WARNING("You cannot get a direct laser from where you are.")) return FALSE if(!(is_ground_level(targeted_atom.z))) to_chat(user, SPAN_WARNING("INVALID TARGET: target must be on the surface.")) return FALSE if(user.sight & SEE_TURFS) - var/list/turf/path = get_line(user, targeted_atom, include_start_atom = FALSE) + var/list/turf/path = get_line(position_ref, targeted_atom, include_start_atom = FALSE) for(var/turf/T in path) if(T.opacity) to_chat(user, SPAN_WARNING("There is something in the way of the laser!")) @@ -171,6 +186,9 @@ if(coord) to_chat(user, SPAN_WARNING("You're already targeting something.")) return + if(acquiring) + to_chat(user, SPAN_WARNING("You're already acquiring a target.")) + return if(world.time < laser_cooldown) to_chat(user, SPAN_WARNING("[src]'s laser battery is recharging.")) return @@ -192,10 +210,38 @@ var/turf/TU = get_turf(targeted_atom) if(!istype(TU) || user.action_busy) return + + // Cleared right before every way out of the wind-up, so it never gets stuck TRUE. + acquiring = TRUE playsound(src, 'sound/effects/nightvision.ogg', 35) to_chat(user, SPAN_NOTICE("INITIATING LASER TARGETING. Stand still.")) - if(!do_after(user, acquisition_time, INTERRUPT_ALL, BUSY_ICON_GENERIC) || world.time < laser_cooldown) + + // A gunner rangefinder must keep its turret aimed at TU for the whole acquisition, aborting if it drifts. + var/obj/vehicle/multitile/tank/tracking_tank = istype(loc, /obj/vehicle/multitile/tank) ? loc : null + var/obj/item/hardpoint/holder/tank_turret/tracking_turret = tracking_tank ? (locate() in tracking_tank.hardpoints) : null + + var/success = TRUE + if(tracking_turret) + var/elapsed = 0 + while(elapsed < acquisition_time) + sleep(1) + elapsed += 1 + if(QDELETED(src) || QDELETED(user) || QDELETED(tracking_turret) || user.is_mob_incapacitated(TRUE)) + success = FALSE + break + if(abs(angle_delta(Get_Angle_Grounded(get_turf(tracking_tank), TU), tracking_turret.current_angle)) > RANGEFINDER_TURRET_ARC_TOLERANCE) + to_chat(user, SPAN_WARNING("The turret drifted off target - lasing aborted.")) + success = FALSE + break + if(success && world.time < laser_cooldown) + success = FALSE + else if(!do_after(user, acquisition_time, INTERRUPT_ALL, BUSY_ICON_GENERIC) || world.time < laser_cooldown) + success = FALSE + + acquiring = FALSE + if(!success || QDELETED(src)) return + var/obj/effect/overlay/temp/laser_coordinate/LT = new (TU, las_name, user) coord = LT last_x = obfuscate_x(coord.x) @@ -204,7 +250,16 @@ playsound(src, 'sound/effects/binoctarget.ogg', 35) show_coords(user) while(coord) - if(!do_after(user, 30, INTERRUPT_ALL, BUSY_ICON_GENERIC)) + if(tracking_turret) + sleep(30) + if(QDELETED(src) || QDELETED(coord) || QDELETED(tracking_turret) || QDELETED(user) || user.is_mob_incapacitated(TRUE)) + QDEL_NULL(coord) + break + if(abs(angle_delta(Get_Angle_Grounded(get_turf(tracking_tank), TU), tracking_turret.current_angle)) > RANGEFINDER_TURRET_ARC_TOLERANCE) + to_chat(user, SPAN_WARNING("The turret drifted off target - lasing lost.")) + QDEL_NULL(coord) + break + else if(!do_after(user, 30, INTERRUPT_ALL, BUSY_ICON_GENERIC)) QDEL_NULL(coord) break @@ -221,6 +276,9 @@ ui.open() /obj/item/device/binoculars/range/ui_state(mob/user) + // the UI silently fails to open for the gunner without this + if(loc != user) + return GLOB.always_state return GLOB.inventory_state /obj/item/device/binoculars/range/ui_data(mob/user) diff --git a/code/game/objects/items/devices/helmet_visors.dm b/code/game/objects/items/devices/helmet_visors.dm index 33ab12807b35..074baca38448 100644 --- a/code/game/objects/items/devices/helmet_visors.dm +++ b/code/game/objects/items/devices/helmet_visors.dm @@ -174,6 +174,23 @@ action_icon_string = "sec_sight_down" helmet_overlay = "sec_sight_right" +/** + * Reads hardpoint status telemetry broadcast by USCM vehicles. + */ +/obj/item/device/helmet_visor/vehicle + name = "vehicle diagnostics optic" + desc = "An insertable visor HUD into a standard USCM helmet. Reads hardpoint status telemetry broadcast by USCM vehicles, showing engine/tread/weapon/armor condition and damage at a glance." + icon_state = "veh_sight" + hud_type = null + action_icon_string = "veh_sight_down" + helmet_overlay = "veh_sight_right" + +/obj/item/device/helmet_visor/vehicle/activate_visor(obj/item/clothing/head/helmet/marine/attached_helmet, mob/living/carbon/human/user) + GLOB.vehicle_hardpoint_hud.add_hud_to(user, attached_helmet) + +/obj/item/device/helmet_visor/vehicle/deactivate_visor(obj/item/clothing/head/helmet/marine/attached_helmet, mob/living/carbon/human/user) + GLOB.vehicle_hardpoint_hud.remove_hud_from(user, attached_helmet) + /obj/item/device/helmet_visor/welding_visor name = "welding visor" icon_state = "sight_empty" diff --git a/code/game/objects/items/devices/radio/headset.dm b/code/game/objects/items/devices/radio/headset.dm index 1912e4a4fe97..3380b71c83ad 100644 --- a/code/game/objects/items/devices/radio/headset.dm +++ b/code/game/objects/items/devices/radio/headset.dm @@ -1472,11 +1472,13 @@ has_hud = TRUE hud_type = MOB_HUD_FACTION_MARINE -/obj/item/device/radio/headset/almayer/mcom/vc +// Parented under marine, not mcom, so put_marine_in_squad() keeps retuning ";" to the crew's +// assigned squad channel +/obj/item/device/radio/headset/almayer/marine/vc name = "marine vehicle crew radio headset" - desc = "Used by USCM vehicle crew, features a non-standard brace. Channels are as follows: :v - marine command, :n - engineering, :m - medbay, :u - requisitions." - initial_keys = list(/obj/item/device/encryptionkey/vc) - volume = RADIO_VOLUME_RAISED + desc = "Used by USCM vehicle crew, features a non-standard brace. Channels are as follows: :u - requisitions, :n - engineering, :j - JTAC. Speaking with a leading ';' uses your squad's channel." + icon_state = "mcom_headset" + initial_keys = list(/obj/item/device/encryptionkey/almayer, /obj/item/device/encryptionkey/jtac, /obj/item/device/encryptionkey/engi) multibroadcast_cooldown = HIGH_MULTIBROADCAST_COOLDOWN /obj/item/device/radio/headset/distress/UPP/recon diff --git a/code/game/objects/items/devices/walkman.dm b/code/game/objects/items/devices/walkman.dm index 42f7df2be166..8143ab4321fb 100644 --- a/code/game/objects/items/devices/walkman.dm +++ b/code/game/objects/items/devices/walkman.dm @@ -540,6 +540,31 @@ "sound/music/walkman/surf/10-2-3.ogg",\ "sound/music/walkman/surf/10-2-4.ogg")) +// This one might need to be moved server-side. +/obj/item/device/cassette_tape/liberation // Allies world war songs & american civil war songs. Lyrics to all songs are in the public domain. + name = "liberation cassette" + id = 19 + desc = "A plastic cassette tape with the UA Flag" + icon_state = "cassette_liberation" + side1_icon = "cassette_liberation" + songs = list( + "side1" = list( + "sound/music/walkman/liberation/10-1-1.ogg", /* "Over There" (George M. Cohan, 1917).*/\ + /* Sung in 1917 by Billy Murray. This recording is in the public domain.*/\ + "sound/music/walkman/liberation/10-1-2.ogg", /* "When Johnny Comes Marching Home" (Patrick Gilmore, 1863)*/\ + /* 1990 U.S. Military Academy Band recording, also in the public domain.*/\ + "sound/music/walkman/liberation/10-1-3.ogg", /* "Battle Cry Of Freedom" (Geo. F. Root, 1862).*/\ + /* Recording by UC Regents, licensed CC BY-NC 2.5. Cleaned up in Audacity.*/\ + "sound/music/walkman/liberation/10-1-4.ogg"), /* "Blood On The Risers" (US Paratrooper song from WW2).*/\ + /* Unsure if this specific recording, by Alorsx, is in the public domain, but the lyrics are.*/\ + "side2" = list( + "sound/music/walkman/liberation/10-2-1.ogg", /* "Chanson de l'oignon" (French marching song. Melody from 1797, lyrics from 1800).*/\ + /* Unsure if this specific recording, by the Chorale De Saint Cyr, is in the public domain, but the lyrics are.*/\ + "sound/music/walkman/liberation/10-2-2.ogg", /* "Ride of the Valkyries" (Wagner, from Die Walküre, 1870) In the US public domain.*/\ + /* This is a 2022 recording, conducted by Philip Milman, licensed under 3.0 CC*/\ + "sound/music/walkman/liberation/10-2-3.ogg")) /* "It's a Long Way to Tipperary" (British WW2 song. Albert Farrington, 1915).*/ + /* This specific recording is in the US public domain.*/ + // hotline reference /obj/item/device/cassette_tape/aesthetic name = "aesthetic cassette" diff --git a/code/game/objects/items/explosives/grenades/marines.dm b/code/game/objects/items/explosives/grenades/marines.dm index 38938cacdd38..4c6acd59a61e 100644 --- a/code/game/objects/items/explosives/grenades/marines.dm +++ b/code/game/objects/items/explosives/grenades/marines.dm @@ -321,7 +321,7 @@ R.intensityfire = burn_level R.rangefire = radius - new /obj/flamer_fire(T, cause_data, R, R.rangefire, null, flameshape, target, , , fire_type) + new /obj/flamer_fire(T, cause_data, R, R.rangefire, null, flameshape, target, , , fire_type, resolve_flame_mount(T, T, null)) /obj/item/explosive/grenade/incendiary/molotov name = "\improper improvised firebomb" diff --git a/code/game/objects/items/explosives/mine.dm b/code/game/objects/items/explosives/mine.dm index 2bb8aa68f0e7..3a6a93eb5d8b 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/pamphlets/pamphlet_upgrades.dm b/code/game/objects/items/pamphlets/pamphlet_upgrades.dm index 15eb36985c4c..9dfd21964f39 100644 --- a/code/game/objects/items/pamphlets/pamphlet_upgrades.dm +++ b/code/game/objects/items/pamphlets/pamphlet_upgrades.dm @@ -87,6 +87,13 @@ skill_upgrade = SKILL_INTEL skill_cap = 2 +/obj/item/pamphlet/upgradeable/vehicle + name = "vehicular operation instructional pamphlet" + desc = "A pamphlet used to quickly impart vital knowledge of operating vehicles and simple turret systems." + icon_state = "pamphlet_reading" + skill_upgrade = SKILL_VEHICLE + skill_cap = 1 + /obj/item/pamphlet/upgradeable/machinegunner name = "heavy machinegunner instructional pamphlet" desc = "A pamphlet used to quickly impart vital knowledge on heavier-duty equipments and firearms. You suppose you can utilize some of its teachings for some engineering practices." diff --git a/code/game/objects/items/reagent_containers/jp8_canister.dm b/code/game/objects/items/reagent_containers/jp8_canister.dm new file mode 100644 index 000000000000..7f923fcabb5e --- /dev/null +++ b/code/game/objects/items/reagent_containers/jp8_canister.dm @@ -0,0 +1,22 @@ +/obj/item/reagent_container/glass/beaker/jp8_canister + name = "\improper JP-8 canister" + desc = "A rugged steel canister for storing and transporting bulk liquids, sized for topping off a vehicle's fuel tank or radiator. Holds up to 500 units." + icon = 'icons/obj/items/tank.dmi' + icon_state = "gas" + item_state = "gas" + w_class = SIZE_MEDIUM + matter = list("metal" = 2000) + volume = 500 + amount_per_transfer_from_this = 50 + possible_transfer_amounts = list(10, 25, 50, 100, 250, 500) + +/obj/item/reagent_container/glass/beaker/jp8_canister/update_icon() + icon_state = is_open_container() ? "gas" : "gas_closed" + +/obj/item/reagent_container/glass/beaker/jp8_canister/full + desc = "A rugged steel canister for storing and transporting bulk liquids, sized for topping off a vehicle's fuel tank or radiator. Holds up to 500 units. This one is pre-filled with JP-8." + +/obj/item/reagent_container/glass/beaker/jp8_canister/full/Initialize(mapload) + . = ..() + reagents.add_reagent("jp8", volume) + update_icon() diff --git a/code/game/objects/items/storage/backpack.dm b/code/game/objects/items/storage/backpack.dm index e798568b7493..2c8bc5f11d01 100644 --- a/code/game/objects/items/storage/backpack.dm +++ b/code/game/objects/items/storage/backpack.dm @@ -1331,6 +1331,34 @@ GLOBAL_LIST_EMPTY_TYPED(radio_packs, /obj/item/storage/backpack/marine/satchel/r FTL.caliber = "UT-Napthal Fuel" to_chat(user, SPAN_NOTICE("You refill [FTL] with [FTL.caliber].")) FTL.update_icon() + else if(istype(W, /obj/item/ammo_magazine/hardpoint/primary_flamer)) + var/obj/item/ammo_magazine/hardpoint/primary_flamer/vehicle_tank = W + var/missing_volume = vehicle_tank.max_rounds - vehicle_tank.reagents.total_volume + + if((vehicle_tank.flamer_chem == fuel_type || !vehicle_tank.reagents.total_volume) && missing_volume && reagents.total_volume) + var/fuel_available = min(reagents.total_volume, missing_volume) + reagents.remove_reagent(fuel_type, fuel_available) + vehicle_tank.reagents.add_reagent(fuel_type, fuel_available) + playsound(loc, 'sound/effects/refill.ogg', 25, TRUE, 3) + vehicle_tank.caliber = fuel_type + vehicle_tank.flamer_chem = fuel_type + vehicle_tank.current_rounds = round(vehicle_tank.reagents.total_volume) + to_chat(user, SPAN_NOTICE("You refill [vehicle_tank] with [vehicle_tank.caliber].")) + vehicle_tank.update_icon() + else if(istype(W, /obj/item/ammo_magazine/hardpoint/secondary_flamer)) + var/obj/item/ammo_magazine/hardpoint/secondary_flamer/vehicle_tank = W + var/missing_volume = vehicle_tank.max_rounds - vehicle_tank.reagents.total_volume + + if((vehicle_tank.flamer_chem == fuel_type || !vehicle_tank.reagents.total_volume) && missing_volume && reagents.total_volume) + var/fuel_available = min(reagents.total_volume, missing_volume) + reagents.remove_reagent(fuel_type, fuel_available) + vehicle_tank.reagents.add_reagent(fuel_type, fuel_available) + playsound(loc, 'sound/effects/refill.ogg', 25, TRUE, 3) + vehicle_tank.caliber = fuel_type + vehicle_tank.flamer_chem = fuel_type + vehicle_tank.current_rounds = round(vehicle_tank.reagents.total_volume) + to_chat(user, SPAN_NOTICE("You refill [vehicle_tank] with [vehicle_tank.caliber].")) + vehicle_tank.update_icon() . = ..() /obj/item/storage/backpack/marine/engineerpack/flamethrower/kit diff --git a/code/game/objects/items/storage/belt.dm b/code/game/objects/items/storage/belt.dm index e94a4dde335f..f6dc00ca617a 100644 --- a/code/game/objects/items/storage/belt.dm +++ b/code/game/objects/items/storage/belt.dm @@ -2825,9 +2825,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/barricade.dm b/code/game/objects/structures/barricade/barricade.dm index 38c0916ea634..2cbf2f84800c 100644 --- a/code/game/objects/structures/barricade/barricade.dm +++ b/code/game/objects/structures/barricade/barricade.dm @@ -44,6 +44,8 @@ var/metallic = TRUE /// Lower limit of damage beyond which the barricade cannot be fixed by welder. Compared to damage_state. If null it can be repaired at any damage_state. var/welder_lower_damage_limit = null + /// TRUE while a vehicle is occupying this barricade's tile. Forces it to render above the vehicle. + var/covered_by_vehicle = FALSE /obj/structure/barricade/Initialize(mapload, mob/user) . = ..() @@ -122,6 +124,9 @@ icon_state = "[barricade_type]_closed" layer = OBJ_LAYER + if(covered_by_vehicle) + layer = VEHICLE_COVERED_BARRICADE_LAYER + // Pixelshift to indicate anchored state pixel_y = initial(pixel_y) if(!anchored) @@ -196,6 +201,9 @@ * If the object is completely solid, returns ALL */ /obj/structure/barricade/BlockedExitDirs(atom/movable/mover, target_dir) + // A multi-tile vehicle resolves barricades and windows through its own directional scan instead. + if(istype(mover, /obj/vehicle/multitile)) + return NO_BLOCKED_MOVEMENT if(closed) return NO_BLOCKED_MOVEMENT @@ -212,6 +220,9 @@ * mobs. */ /obj/structure/barricade/BlockedPassDirs(atom/movable/mover, target_dir) + // See the identical vehicle exemption/reasoning in BlockedExitDirs() just above. + if(istype(mover, /obj/vehicle/multitile)) + return NO_BLOCKED_MOVEMENT if(closed) return NO_BLOCKED_MOVEMENT 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/multiz_stairs.dm b/code/game/objects/structures/multiz_stairs.dm index 4728182bd3ab..8fc009af29c8 100644 --- a/code/game/objects/structures/multiz_stairs.dm +++ b/code/game/objects/structures/multiz_stairs.dm @@ -165,13 +165,14 @@ RegisterSignal(new_turf, COMSIG_TURF_ENTERED, PROC_REF(handle_entered)) RegisterSignal(new_turf, COMSIG_PRE_TURF_CHANGE, PROC_REF(handle_pre_turf_change)) -/// Handles COMSIG_TURF_ENTERED for mobs that enters a from_turf to register handle_movement and handle_deleted -/datum/staircase/proc/handle_entered(turf/originator, atom/what_did_it) +/// Handles COMSIG_TURF_ENTERED for mobs/vehicles that enter a from_turf to register handle_movement and handle_deleted +/datum/staircase/proc/handle_entered(turf/originator, atom/movable/what_did_it) SIGNAL_HANDLER - var/mob/mover = what_did_it - if(!istype(mover)) + // A multitile vehicle only fires this on its own anchor tile, same as any mob walking through. + if(!ismob(what_did_it) && !istype(what_did_it, /obj/vehicle/multitile)) return + var/atom/movable/mover = what_did_it if(mover in in_range_mob) return @@ -182,26 +183,38 @@ in_range_mob += mover -/// Handles COMSIG_MOVABLE_MOVED for mobs that were in a from_turf -/datum/staircase/proc/handle_movement(mob/mover, old_loc, direction) +/// Handles COMSIG_MOVABLE_MOVED for mobs/vehicles that were in a from_turf +/datum/staircase/proc/handle_movement(atom/movable/mover, old_loc, direction) SIGNAL_HANDLER + // A plain mob shows this to itself; a vehicle shows it to its seated driver/gunner instead. + // An empty list just makes the loops below no-ops. + var/list/client/viewers = mover.get_staircase_vision_clients() + var/turf/where_from = get_turf(old_loc) if(length(from_turf_to_images[where_from])) - for(var/image in from_turf_to_images[where_from]) - mover.client?.images -= image + for(var/client/viewer as anything in viewers) + for(var/image in from_turf_to_images[where_from]) + viewer.images -= image var/turf/where_to = get_turf(mover) if(length(from_turf_to_images[where_to])) - for(var/image in from_turf_to_images[where_to]) - mover.client?.images += image + for(var/client/viewer as anything in viewers) + for(var/image in from_turf_to_images[where_to]) + viewer.images += image return in_range_mob -= mover UnregisterSignal(mover, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING)) -/// Handles COMSIG_PARENT_QDELETING for mobs that were in a from_turf +/atom/movable/proc/get_staircase_vision_clients() + return list() + +/mob/get_staircase_vision_clients() + return client ? list(client) : list() + +/// Handles COMSIG_PARENT_QDELETING for mobs/vehicles that were in a from_turf /datum/staircase/proc/handle_deleted(atom/updater) SIGNAL_HANDLER 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/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm index a387a20b141b..671177bda687 100644 --- a/code/game/objects/structures/tables_racks.dm +++ b/code/game/objects/structures/tables_racks.dm @@ -235,12 +235,22 @@ setDir(SOUTH) /obj/structure/surface/table/BlockedPassDirs(atom/movable/mover, target_dir) + // Same vehicle exemption as barricade and window. Only meaningful once flipped. + if(istype(mover, /obj/vehicle/multitile) && flipped) + return NO_BLOCKED_MOVEMENT + for(var/obj/structure/S in get_turf(mover)) if(S && S.climbable && !(S.flags_atom & ON_BORDER) && climbable && isliving(mover)) //Climbable non-border objects allow you to universally climb over others return NO_BLOCKED_MOVEMENT return ..() +/// Same vehicle exemption as BlockedPassDirs() above. +/obj/structure/surface/table/BlockedExitDirs(atom/movable/mover, target_dir) + if(istype(mover, /obj/vehicle/multitile) && flipped) + return NO_BLOCKED_MOVEMENT + return ..() + //Flipping tables, nothing more, nothing less /obj/structure/surface/table/MouseDrop(over_object, src_location, over_location) . = ..() diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm index 88065349090b..23cfbad00907 100644 --- a/code/game/objects/structures/window.dm +++ b/code/game/objects/structures/window.dm @@ -51,6 +51,21 @@ if (PF) PF.flags_can_pass_all = PASS_HIGH_OVER_ONLY|PASS_GLASS +/** + * A multi-tile vehicle resolves directional windows through its own travel-direction scan instead. + * Full-tile windows aren't exempted. + */ +/obj/structure/window/BlockedExitDirs(atom/movable/mover, target_dir) + if(istype(mover, /obj/vehicle/multitile) && !is_full_window()) + return NO_BLOCKED_MOVEMENT + return ..() + +/// Same reasoning as BlockedExitDirs() above. +/obj/structure/window/BlockedPassDirs(atom/movable/mover, target_dir) + if(istype(mover, /obj/vehicle/multitile) && !is_full_window()) + return NO_BLOCKED_MOVEMENT + return ..() + /obj/structure/window/proc/set_constructed_window(start_dir) state = 0 anchored = FALSE diff --git a/code/game/supplyshuttle.dm b/code/game/supplyshuttle.dm index e65b7bdf5beb..8262e81f6c60 100644 --- a/code/game/supplyshuttle.dm +++ b/code/game/supplyshuttle.dm @@ -775,6 +775,7 @@ GLOBAL_DATUM_INIT(supply_controller, /datum/controller/supply, new()) "Weapons", "Vehicle Ammo", "Vehicle Equipment", + "Vehicle Spare Parts", "Attachments", "Ammo", "Weapons Specialist Ammo", diff --git a/code/game/turfs/auto_turf.dm b/code/game/turfs/auto_turf.dm index 9e5d256861b0..9b229212bd20 100644 --- a/code/game/turfs/auto_turf.dm +++ b/code/game/turfs/auto_turf.dm @@ -99,6 +99,7 @@ //Kutjevo & Trijent sand /turf/open/auto_turf/sand layer_name = list("red dirt", "sand", "rocky sand", "this layer does not exist", "call a coder") + traction = 0.8 /turf/open/auto_turf/sand/insert_self_into_baseturfs() baseturfs += /turf/open/auto_turf/sand/layer0 @@ -143,6 +144,7 @@ icon_state = "ice_0" icon_prefix = "ice" layer_name = list("cracked permafrost","permafrost","glacial permafrost","warn a coder","warn a coder") + traction = 0.75 /turf/open/auto_turf/ice/insert_self_into_baseturfs() baseturfs += /turf/open/auto_turf/ice/layer0 @@ -259,22 +261,27 @@ /turf/open/auto_turf/snow/layer0 //still have to manually define the layers for the editor icon_state = "snow_0" bleed_layer = 0 + traction = 0.8 /turf/open/auto_turf/snow/layer1 icon_state = "snow_1" bleed_layer = 1 + traction = 0.7 /turf/open/auto_turf/snow/layer2 icon_state = "snow_2" bleed_layer = 2 + traction = 0.6 /turf/open/auto_turf/snow/layer3 icon_state = "snow_3" bleed_layer = 3 + traction = 0.55 /turf/open/auto_turf/snow/layer4 icon_state = "snow_4" //Add sorokyne rock decals to this one bleed_layer = 4 + traction = 0.5 //new strata turfs @@ -289,22 +296,27 @@ /turf/open/auto_turf/snow/brown_base/layer0 //still have to manually define the layers for the editor icon_state = "snow_b_0" bleed_layer = 0 + traction = 0.8 /turf/open/auto_turf/snow/brown_base/layer1 icon_state = "snow_b_1" bleed_layer = 1 + traction = 0.7 /turf/open/auto_turf/snow/brown_base/layer2 icon_state = "snow_b_2" bleed_layer = 2 + traction = 0.6 /turf/open/auto_turf/snow/brown_base/layer3 icon_state = "snow_b_3" bleed_layer = 3 + traction = 0.55 /turf/open/auto_turf/snow/brown_base/layer4 icon_state = "snow_b_4" //Add sorokyne rock decals to this one bleed_layer = 4 + traction = 0.5 /turf/open/auto_turf/strata_grass name = "matted grass" @@ -313,6 +325,7 @@ icon_prefix = "grass" layer_name = list("ground","lush thick grass") desc = "Grass, dirt, mud, and other assorted high moisture cave flooring." + traction = 0.8 /turf/open/auto_turf/strata_grass/insert_self_into_baseturfs() baseturfs += /turf/open/auto_turf/strata_grass/layer0 @@ -345,6 +358,7 @@ icon_prefix = "grass" layer_name = list("ground","lush thick grass") desc = "grass, dirt, mud, and other assorted high moisture cave flooring." + traction = 0.8 /turf/open/auto_turf/tyrargo_grass/insert_self_into_baseturfs() baseturfs += /turf/open/auto_turf/tyrargo_grass/layer0 @@ -376,6 +390,7 @@ layer_name = list("wind blown dirt", "volcanic plate rock", "volcanic plate and rock", "this layer does not exist") icon = 'icons/turf/floors/auto_shale.dmi' icon_prefix = "shale" + traction = 0.8 /turf/open/auto_turf/shale/get_dirt_type() return DIRT_TYPE_SHALE diff --git a/code/game/turfs/floor.dm b/code/game/turfs/floor.dm index a695c9e44a71..bde8bbc756b5 100644 --- a/code/game/turfs/floor.dm +++ b/code/game/turfs/floor.dm @@ -7,6 +7,7 @@ turf_flags = TURF_BURNABLE|TURF_BREAKABLE var/mineral = "metal" var/image/wet_overlay + traction = 1 var/tile_type = /obj/item/stack/tile/plasteel var/plating_type = /turf/open/floor/plating diff --git a/code/game/turfs/floor_types.dm b/code/game/turfs/floor_types.dm index b3eec820b688..e5c06df9f876 100644 --- a/code/game/turfs/floor_types.dm +++ b/code/game/turfs/floor_types.dm @@ -9,6 +9,7 @@ icon_state = "plating" intact_tile = FALSE tool_flags = NO_FLAGS + traction = 1.1 /turf/open/floor/plating/is_plating() return TRUE @@ -207,6 +208,7 @@ /turf/open/floor/plating/almayer icon = 'icons/turf/almayer.dmi' + traction = 1.2 /turf/open/floor/plating/almayer/no_build allow_construction = FALSE @@ -363,6 +365,7 @@ icon = 'icons/turf/almayer.dmi' icon_state = "default" plating_type = /turf/open/floor/plating/almayer + traction = 1.2 /turf/open/floor/almayer/pod_landing_floor desc = "There's a hatch above it, presumably to allow pods to drop in." @@ -2442,6 +2445,7 @@ icon_state = "wood" tile_type = /obj/item/stack/tile/wood tool_flags = BREAK_CROWBAR|REMOVE_SCREWDRIVER + traction = 1 /turf/open/floor/wood/is_wood_floor() return TRUE @@ -2576,6 +2580,7 @@ icon_state = "grass1" tile_type = /obj/item/stack/tile/grass tool_flags = null + traction = 0.8 /turf/open/floor/grass/Initialize(mapload, ...) . = ..() @@ -2608,6 +2613,7 @@ icon_state = "carpet" tile_type = /obj/item/stack/tile/carpet tool_flags = REMOVE_SCREWDRIVER + traction = 0.9 // unlikely that a vehicle will ever step in a carpet tile, but HEY!! you never know!!! /turf/open/floor/carpet/Initialize(mapload, ...) . = ..() @@ -3478,6 +3484,7 @@ icon_state = "oldwood1" tile_type = /obj/item/stack/tile/wood turf_flags = NO_FLAGS // platingdmg && panelscorched icon_state does not exist in this icon + traction = 1 /turf/open/floor/interior/wood/is_wood_floor() return TRUE diff --git a/code/game/turfs/open.dm b/code/game/turfs/open.dm index 9cbfae93835e..80bd8f13eccb 100644 --- a/code/game/turfs/open.dm +++ b/code/game/turfs/open.dm @@ -259,6 +259,7 @@ is_groundmap_turf = TRUE minimap_color = MINIMAP_MARS_DIRT is_weedable = SEMI_WEEDABLE + traction = 0.8 /turf/open/mars_cave @@ -266,6 +267,7 @@ icon = 'icons/turf/floors/bigred.dmi' icon_state = "mars_cave_1" is_groundmap_turf = TRUE + traction = 0.9 /turf/open/mars_cave/Initialize(mapload, ...) . = ..() @@ -355,6 +357,7 @@ icon = 'icons/turf/floors/bigred.dmi' icon_state = "mars_dirt_1" minimap_color = MINIMAP_DIRT + traction = 0.85 /turf/open/mars_dirt/Initialize(mapload, ...) . = ..() @@ -432,6 +435,7 @@ name = "Beach" icon = 'icons/turf/floors/beach.dmi' supports_surgery = FALSE + traction = 0.6 /turf/open/beach/Entered(atom/movable/AM) ..() @@ -448,16 +452,19 @@ name = "Sand" icon_state = "sand" supports_surgery = TRUE + traction = 0.8 /turf/open/beach/coastline name = "Coastline" icon = 'icons/turf/beach2.dmi' icon_state = "sandwater" + traction = 0.75 /turf/open/beach/water name = "Water" icon_state = "water" can_bloody = FALSE + traction = 0.4 /turf/open/beach/water/Initialize(mapload, ...) . = ..() @@ -467,6 +474,7 @@ name = "Water" icon_state = "water" can_bloody = FALSE + traction = 0.4 /turf/open/beach/water2/Initialize(mapload, ...) . = ..() @@ -484,6 +492,7 @@ icon = 'icons/turf/ground_map.dmi' icon_state = "desert" is_groundmap_turf = TRUE + traction = 0.85 /turf/open/gm/attackby(obj/item/I, mob/user) @@ -520,6 +529,7 @@ icon_state = "desert" baseturfs = /turf/open/gm/dirt minimap_color = MINIMAP_DIRT + traction = 0.8 /turf/open/gm/dirt/beach icon_state = "beach" @@ -728,6 +738,7 @@ supports_surgery = FALSE minimap_color = MINIMAP_WATER is_weedable = NOT_WEEDABLE + traction = 0.4 /turf/open/gm/river/Initialize(mapload, ...) . = ..() @@ -884,6 +895,7 @@ baseturfs = /turf/open/gm/coast supports_surgery = FALSE is_weedable = NOT_WEEDABLE + traction = 0.75 /turf/open/gm/coast/north @@ -939,6 +951,7 @@ minimap_color = MINIMAP_WATER is_groundmap_turf = FALSE // Not real ground fishing_allowed = TRUE + traction = 0.4 /turf/open/gm/riverdeep/Initialize(mapload, ...) @@ -980,6 +993,7 @@ icon = 'icons/turf/ice.dmi' icon_state = "ice_floor" baseturfs = /turf/open/ice + traction = 0.7 //Randomize ice floor sprite @@ -995,6 +1009,7 @@ name = "asphalt" icon = 'icons/turf/floors/asphalt.dmi' icon_state = "sunbleached_asphalt" + traction = 1.2 baseturfs = /turf/open/asphalt /turf/open/asphalt/tile @@ -1150,6 +1165,7 @@ name = "wet grass" desc = "Thick, long, wet grass." icon = 'icons/turf/floors/jungle.dmi' + traction = 0.8 icon_state = "grass1" var/icon_spawn_state = "grass1" baseturfs = /turf/open/jungle @@ -1253,6 +1269,7 @@ icon_spawn_state = "water" can_bloody = FALSE supports_surgery = FALSE + traction = 0.4 /turf/open/jungle/water/Initialize(mapload, ...) diff --git a/code/game/turfs/open_space.dm b/code/game/turfs/open_space.dm index 9a71ef77368c..fa53d0b1f3eb 100644 --- a/code/game/turfs/open_space.dm +++ b/code/game/turfs/open_space.dm @@ -124,7 +124,18 @@ GLOBAL_DATUM_INIT(openspace_backdrop_one_for_all, /atom/movable/openspace_backdr user.visible_message(SPAN_WARNING("[user] climbs down."), SPAN_WARNING("You climb down.")) + 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) @@ -155,6 +166,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/game/turfs/shale.dm b/code/game/turfs/shale.dm index 9a11ede5de59..ae06dcbae4ed 100644 --- a/code/game/turfs/shale.dm +++ b/code/game/turfs/shale.dm @@ -3,6 +3,7 @@ icon = 'icons/turf/floors/auto_shale.dmi' icon_state = "shale_0" is_weedable = SEMI_WEEDABLE + traction = 0.8 /turf/open/shale/get_dirt_type() return DIRT_TYPE_SHALE diff --git a/code/game/turfs/shiva.dm b/code/game/turfs/shiva.dm index b155c46d60d2..9ae25caf2285 100644 --- a/code/game/turfs/shiva.dm +++ b/code/game/turfs/shiva.dm @@ -2,6 +2,7 @@ /turf/open/floor/shiva //Instance me! icon = 'icons/turf/floors/ice_colony/shiva_floor.dmi' icon_state = "floor" + traction = 1.1 // reasoning being that all of those floors LOOK like platings, which have a traction of 1.1 on other files. consistency! /turf/open/floor/shiva/blue icon_state = "blue" diff --git a/code/game/turfs/soro.dm b/code/game/turfs/soro.dm index 6db3721df229..e6617b22c2b2 100644 --- a/code/game/turfs/soro.dm +++ b/code/game/turfs/soro.dm @@ -68,6 +68,7 @@ icon_state = "browndirt_road" baseturfs = /turf/open/gm/road minimap_color = MINIMAP_DIRT + traction = 1.0 /turf/open/gm/grass/dirt name = "grass" @@ -113,6 +114,7 @@ icon_state = "beach" baseturfs = /turf/open/gm/coast supports_surgery = FALSE + traction = 0.75 /turf/open/gm/coast/dirt/north diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index 330fa9c30a37..7101279a6d47 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -30,6 +30,8 @@ ///Used by floors to indicate the floor is a tile (otherwise its plating) var/intact_tile = TRUE + /// How good this turf is for vehicle acceleration. 1 is neutral. + var/traction = 1.0 ///Can blood spawn on this turf? var/can_bloody = TRUE var/list/linked_pylons @@ -72,6 +74,39 @@ /// Can xenomorph weeds grow on the tile var/is_weedable = FULLY_WEEDABLE +/** + * How good this turf is for vehicle acceleration right now. + * + * Weeds and xeno resin drag traction down further than the turf's own base value. + * Hazards cap traction independently rather than stacking. + * + * Returns: + * * traction, capped further by whichever weed/resin hazard on this turf caps it hardest. + */ +/turf/proc/get_traction() + var/result = traction + if(weeds) + result = min(result, get_weed_traction_cap(weeds.weed_strength)) + for(var/obj/effect/alien/resin/R in src) + if(istype(R, /obj/effect/alien/resin/sticky/fast)) + continue + else if(istype(R, /obj/effect/alien/resin/sticky/thin/weak)) + result = min(result, STICKY_RESIN_WEAK_TRACTION_CAP) + else if(istype(R, /obj/effect/alien/resin/sticky)) + result = min(result, STICKY_RESIN_TRACTION_CAP) + else if(istype(R, /obj/effect/alien/resin/spike)) + result = min(result, RESIN_SPIKE_TRACTION_CAP) + return result + +/turf/proc/get_weed_traction_cap(strength) + if(strength >= WEED_LEVEL_HIVE) + return WEED_TRACTION_CAP_HIVE + if(strength == WEED_LEVEL_HARDY) + return WEED_TRACTION_CAP_HARDY + if(strength == WEED_LEVEL_STANDARD) + return WEED_TRACTION_CAP_STANDARD + return WEED_TRACTION_CAP_WEAK + /turf/Initialize(mapload) SHOULD_CALL_PARENT(FALSE) // this doesn't parent call for optimisation reasons if(flags_atom & INITIALIZED) 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/client/preferences.dm b/code/modules/client/preferences.dm index 342fd31686ff..b5538dce7742 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -6,6 +6,7 @@ #define MENU_MENTOR "mentor" #define MENU_SETTINGS "settings" #define MENU_SPECIAL "special" +#define MENU_VEHICLE "vehicle" GLOBAL_LIST_EMPTY(preferences_datums) @@ -81,6 +82,7 @@ GLOBAL_LIST_INIT(be_special_flags, list( var/toggles_survivor = TOGGLES_SURVIVOR_DEFAULT var/toggles_insert = TOGGLES_INSERT_DEFAULT var/toggles_ert_pred = TOGGLES_ERT_GROUNDS + var/toggles_vehicle = TOGGLES_VEHICLE_DEFAULT var/list/volume_preferences = list(1, 0.5, 1, 0.6) // Game, music, admin midis, lobby music (this is also set in sanitize_volume_preferences() call) var/chat_display_preferences = CHAT_TYPE_ALL var/item_animation_pref_level = SHOW_ITEM_ANIMATIONS_ALL @@ -418,7 +420,8 @@ GLOBAL_LIST_INIT(be_special_flags, list( if(owner.check_whitelist_status(WHITELIST_MENTOR)) dat += "Mentor - " dat += "Settings - " - dat += "Special Roles" + dat += "Special Roles - " + dat += "Vehicle" dat += "" dat += "