Files
+37 21b4095dfd [MDB IGNORE] [IDB IGNORE] Upstream Sync - 04/17/2026 (#5453)
Upstream 04/17/2026

fixes https://github.com/Bubberstation/Bubberstation/issues/5549

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: tgstation-ci[bot] <179393467+tgstation-ci[bot]@users.noreply.github.com>
Co-authored-by: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com>
Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Co-authored-by: Rhials <28870487+Rhials@users.noreply.github.com>
Co-authored-by: rageguy505 <54517726+rageguy505@users.noreply.github.com>
Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
Co-authored-by: John Willard <53777086+JohnFulpWillard@users.noreply.github.com>
Co-authored-by: Aliceee2ch <160794176+Aliceee2ch@users.noreply.github.com>
Co-authored-by: Time-Green <7501474+Time-Green@users.noreply.github.com>
Co-authored-by: Tsar-Salat <62388554+Tsar-Salat@users.noreply.github.com>
Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com>
Co-authored-by: Maxipat <108554989+Maxipat112@users.noreply.github.com>
Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: SimplyLogan <47579821+loganuk@users.noreply.github.com>
Co-authored-by: loganuk <fakeemail123@aol.com>
Co-authored-by: Leland Kemble <70413276+lelandkemble@users.noreply.github.com>
Co-authored-by: FalloutFalcon <86381784+FalloutFalcon@users.noreply.github.com>
Co-authored-by: Roxy <75404941+TealSeer@users.noreply.github.com>
Co-authored-by: Lucy <lucy@absolucy.moe>
Co-authored-by: siliconOpossum <138069572+siliconOpossum@users.noreply.github.com>
Co-authored-by: Isratosh <Isratosh@hotmail.com>
Co-authored-by: TheRyeGuyWhoWillNowDie <70169560+TheRyeGuyWhoWillNowDie@users.noreply.github.com>
Co-authored-by: Neocloudy <88008002+Neocloudy@users.noreply.github.com>
Co-authored-by: Alexander V. <volas@ya.ru>
Co-authored-by: ElGitificador <168473461+ElGitificador@users.noreply.github.com>
Co-authored-by: Twaticus <46540570+Twaticus@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com>
Co-authored-by: Cameron Lennox <killer65311@gmail.com>
Co-authored-by: Tim <timothymtorres@gmail.com>
Co-authored-by: Iamgoofball <iamgoofball@gmail.com>
Co-authored-by: Layzu666 <121319428+Layzu666@users.noreply.github.com>
Co-authored-by: Arturlang <24881678+Arturlang@users.noreply.github.com>
Co-authored-by: _0Steven <42909981+00-Steven@users.noreply.github.com>
Co-authored-by: mrmanlikesbt <99309552+mrmanlikesbt@users.noreply.github.com>
Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com>
Co-authored-by: John F. Kennedy <54908920+MacaroniCritter@users.noreply.github.com>
Co-authored-by: Cursor <102828457+theselfish@users.noreply.github.com>
Co-authored-by: Josh <josh.adam.powell@gmail.com>
Co-authored-by: Josh Powell <josh.powell@softwire.com>
Co-authored-by: Yobrocharlie <Charliemiller5617@gmail.com>
Co-authored-by: Hardly3D <66234359+Hardly3D@users.noreply.github.com>
Co-authored-by: shayoki <96078776+shayoki@users.noreply.github.com>
Co-authored-by: LT3 <83487515+lessthnthree@users.noreply.github.com>
2026-05-16 00:56:00 +02:00

380 lines
12 KiB
Plaintext

/// Fire all loaded contents at once.
#define PCANNON_FIREALL 1
/// First in, Last out.
#define PCANNON_FILO 2
/// First in, first out. Default value.
#define PCANNON_FIFO 3
///Defines for the pressure strength of the cannon
#define LOW_PRESSURE 1
#define MID_PRESSURE 2
#define HIGH_PRESSURE 3
/obj/item/pneumatic_cannon
name = "pneumatic cannon"
desc = "A gas-powered cannon that can fire any object loaded into it."
w_class = WEIGHT_CLASS_BULKY
force = 8 //Very heavy
attack_verb_continuous = list("bludgeons", "smashes", "beats")
attack_verb_simple = list("bludgeon", "smash", "beat")
icon = 'icons/obj/weapons/pneumaticCannon.dmi'
icon_state = "pneumaticCannon"
inhand_icon_state = "bulldog"
lefthand_file = 'icons/mob/inhands/weapons/guns_lefthand.dmi'
righthand_file = 'icons/mob/inhands/weapons/guns_righthand.dmi'
armor_type = /datum/armor/item_pneumatic_cannon
custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 6)
/// The max weight of items that can fit into the cannon
var/maxWeightClass = 20
/// The weight of items currently in the cannon
var/loadedWeightClass = 0
/// The gas tank that is drawn from to fire things
var/obj/item/tank/internals/tank = null
/// How much gas is drawn from a tank's pressure to fire
var/gasPerThrow = 3
/// The items loaded into the cannon that will be fired out
var/list/loadedItems = list()
/// How powerful the cannon is - higher pressure = more gas but more powerful throws
var/pressure_setting = LOW_PRESSURE
/// Additional multiplier that adjusts how much farther thrown objects can travel.
var/range_multiplier = 1
/// How many items to throw per fire
var/throw_amount = 1
/// What methodology should be used when firing? See #defines at top of file.
var/fire_mode = PCANNON_FIFO
/// Allows you to hold down LMB to continuously fire.
var/automatic = FALSE
/// Determines if a pneumatic cannon needs an air tank to fire. False for things like the pie cannons.
var/needs_air = TRUE
/// If true, has side effects if fired with the clumsy trait (read: clowns)
var/clumsyCheck = TRUE
///Leave as null to allow all. Otherwise whitelists what can be inserted into the cannon.
var/list/allowed_typecache
var/charge_amount = 1
var/charge_ticks = 1
var/charge_tick = 0
var/atom/charge_type
var/selfcharge = FALSE
var/fire_sound = 'sound/items/weapons/sonic_jackhammer.ogg'
///Do the projectiles spin when launched?
var/spin_item = TRUE
trigger_guard = TRIGGER_GUARD_NORMAL
/datum/armor/item_pneumatic_cannon
fire = 60
acid = 50
/obj/item/pneumatic_cannon/Initialize(mapload)
. = ..()
if(selfcharge)
init_charge()
/obj/item/pneumatic_cannon/proc/init_charge() //wrapper so it can be vv'd easier
START_PROCESSING(SSobj, src)
/obj/item/pneumatic_cannon/process()
charge_tick++
if(charge_tick >= charge_ticks && charge_type)
fill_with_type(charge_type, charge_amount)
/obj/item/pneumatic_cannon/Destroy()
STOP_PROCESSING(SSobj, src)
return ..()
/obj/item/pneumatic_cannon/CanItemAutoclick()
return automatic
/obj/item/pneumatic_cannon/proc/pressure_setting_to_text(pressure_setting)
switch(pressure_setting)
if(LOW_PRESSURE)
return "low"
if(MID_PRESSURE)
return "medium"
if(HIGH_PRESSURE)
return "high"
else
CRASH("Invalid pressure setting: [pressure_setting]!")
/obj/item/pneumatic_cannon/examine(mob/user)
. = ..()
var/list/out = list()
if(!in_range(user, src))
out += span_notice("You'll need to get closer to see any more.")
return
if(selfcharge)
if(length(loadedItems))
out += span_info("[icon2html(pick(loadedItems), user)] It has [length(loadedItems)] [charge_type::name] loaded.")
else
for(var/obj/item/I in loadedItems)
out += span_info("[icon2html(I, user)] It has \a [I] loaded.")
CHECK_TICK
if(!length(loadedItems))
out += span_info("The chamber has nothing loaded.")
if(tank)
out += span_notice("[icon2html(tank, user)] It has \a [tank] mounted onto it. It could be removed with a <b>screwdriver</b>.")
if(needs_air == TRUE)
. += span_notice("Use a <b>wrench</b> to change the pressure level. Current output level is <b>[pressure_setting_to_text(pressure_setting)]</b>.")
. += out.Join("\n")
/obj/item/pneumatic_cannon/screwdriver_act(mob/living/user, obj/item/tool)
if(tank)
tool.play_tool_sound(src)
updateTank(tank, 1, user)
return TRUE
/obj/item/pneumatic_cannon/wrench_act(mob/living/user, obj/item/tool)
if(needs_air == FALSE)
return
playsound(src, 'sound/items/tools/ratchet.ogg', 50, TRUE)
pressure_setting = pressure_setting >= HIGH_PRESSURE ? LOW_PRESSURE : pressure_setting + 1
balloon_alert(user, "output level set to [pressure_setting_to_text(pressure_setting)]")
return TRUE
/obj/item/pneumatic_cannon/attackby(obj/item/W, mob/living/user, list/modifiers, list/attack_modifiers)
if(user.combat_mode)
return ..()
if(istype(W, /obj/item/tank/internals))
if(needs_air == FALSE)
return
if(!tank)
var/obj/item/tank/internals/IT = W
if(IT.volume <= 3)
to_chat(user, span_warning("\The [IT] is too small for \the [src]."))
return
updateTank(W, 0, user)
else if(W.type == type)
to_chat(user, span_warning("You're fairly certain that putting a pneumatic cannon inside another pneumatic cannon would cause a spacetime disruption."))
else if(loadedWeightClass >= maxWeightClass)
to_chat(user, span_warning("\The [src] can't hold any more items!"))
else if(isitem(W))
var/obj/item/IW = W
load_item(IW, user)
/obj/item/pneumatic_cannon/proc/can_load_item(obj/item/I, mob/user)
if(!istype(I)) //Players can't load non items, this allows for admin varedit inserts.
return TRUE
if(allowed_typecache && !is_type_in_typecache(I, allowed_typecache))
if(user)
to_chat(user, span_warning("[I] won't fit into [src]!"))
return
if((loadedWeightClass + I.w_class) > maxWeightClass) //Only make messages if there's a user
if(user)
to_chat(user, span_warning("\The [I] won't fit into \the [src]!"))
return FALSE
if(I.w_class > w_class)
if(user)
to_chat(user, span_warning("\The [I] is too large to fit into \the [src]!"))
return FALSE
return TRUE
/obj/item/pneumatic_cannon/proc/load_item(obj/item/I, mob/user)
if(!can_load_item(I, user))
return FALSE
if(user) //Only use transfer proc if there's a user, otherwise just set loc.
if(!user.transferItemToLoc(I, src))
return FALSE
to_chat(user, span_notice("You load \the [I] into \the [src]."))
else
I.forceMove(src)
loadedItems += I
if(isitem(I))
loadedWeightClass += I.w_class
else
loadedWeightClass++
return TRUE
/obj/item/pneumatic_cannon/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if(user.combat_mode)
return ITEM_INTERACT_SKIP_TO_ATTACK
if(!pre_fire(user, interacting_with))
return NONE
Fire(user, interacting_with)
return ITEM_INTERACT_SUCCESS
/obj/item/pneumatic_cannon/ranged_interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
Fire(user, interacting_with)
return ITEM_INTERACT_SUCCESS
/** Checks */
/obj/item/pneumatic_cannon/proc/pre_fire(mob/living/user, atom/target)
if(user.Adjacent(target))
if(target in user.contents)
return FALSE
if(!ismob(target))
return FALSE
return TRUE
/obj/item/pneumatic_cannon/proc/Fire(mob/living/user, atom/target)
if(!istype(user) && !target)
return
var/discharge = 0
if(!can_trigger_gun(user))
return
if(!loadedItems || !loadedWeightClass)
to_chat(user, span_warning("\The [src] has nothing loaded."))
return
if(!tank && needs_air)
to_chat(user, span_warning("\The [src] can't fire without a source of gas."))
return
if(HAS_TRAIT(user, TRAIT_PACIFISM))
to_chat(user, span_warning("You can't bring yourself to fire \the [src]! You don't want to risk harming anyone...") )
return
if(tank && !tank.remove_air(gasPerThrow * pressure_setting))
to_chat(user, span_warning("\The [src] lets out a weak hiss and doesn't react!"))
return
if(HAS_TRAIT(user, TRAIT_CLUMSY) && prob(75) && clumsyCheck && iscarbon(user))
var/mob/living/carbon/C = user
C.visible_message(span_warning("[C] loses [C.p_their()] grip on [src], causing it to go off!"), span_userdanger("[src] slips out of your hands and goes off!"))
C.dropItemToGround(src, TRUE)
if(prob(10))
target = get_turf(user)
else
var/list/possible_targets = range(3,src)
target = pick(possible_targets)
discharge = 1
if(!discharge)
user.visible_message(span_danger("[user] fires \the [src]!"), \
span_danger("You fire \the [src]!"))
log_combat(user, target, "fired at", src)
var/turf/T = get_target(target, get_turf(src))
playsound(src, fire_sound, 50, TRUE)
fire_items(T, user)
if(pressure_setting >= 3 && iscarbon(user))
var/mob/living/carbon/C = user
C.visible_message(span_warning("[C] is thrown down by the force of the cannon!"), span_userdanger("[src] slams into your shoulder, knocking you down!"))
C.Paralyze(60)
/obj/item/pneumatic_cannon/proc/fire_items(turf/target, mob/user)
if(fire_mode == PCANNON_FIREALL)
for(var/obj/item/ITD in loadedItems) //Item To Discharge
if(!throw_item(target, ITD, user))
break
else
for(var/i in 1 to throw_amount)
if(!loadedItems.len)
break
var/atom/movable/I
if(fire_mode == PCANNON_FILO)
I = loadedItems[loadedItems.len]
else
I = loadedItems[1]
if(!throw_item(target, I, user))
break
/obj/item/pneumatic_cannon/proc/throw_item(turf/target, atom/movable/AM, mob/user)
if(!istype(AM))
return FALSE
loadedItems -= AM
if(isitem(AM))
var/obj/item/I = AM
loadedWeightClass -= I.w_class
else
loadedWeightClass--
AM.forceMove(get_turf(src))
AM.throw_at(target, pressure_setting * 10 * range_multiplier, pressure_setting * 2, user, spin_item)
return TRUE
/obj/item/pneumatic_cannon/proc/get_target(turf/target, turf/starting)
if(range_multiplier == 1)
return target
var/x_o = (target.x - starting.x)
var/y_o = (target.y - starting.y)
var/new_x = clamp((starting.x + (x_o * range_multiplier)), 0, world.maxx)
var/new_y = clamp((starting.y + (y_o * range_multiplier)), 0, world.maxy)
var/turf/newtarget = locate(new_x, new_y, starting.z)
return newtarget
/obj/item/pneumatic_cannon/Exited(atom/movable/gone, direction)
. = ..()
if(loadedItems.Remove(gone))
var/obj/item/item = gone
if(istype(item))
loadedWeightClass -= item.w_class
else
loadedWeightClass--
else if (gone == tank)
tank = null
update_appearance()
/obj/item/pneumatic_cannon/ghetto //Obtainable by improvised methods; more gas per use, less capacity
name = "improvised pneumatic cannon"
desc = "A gas-powered, object-firing cannon made out of common parts."
force = 5
maxWeightClass = 10
gasPerThrow = 5
/obj/item/pneumatic_cannon/proc/updateTank(obj/item/tank/internals/thetank, removing = 0, mob/living/carbon/human/user)
if(removing)
if(!tank)
return
to_chat(user, span_notice("You detach \the [thetank] from \the [src]."))
tank.forceMove(user.drop_location())
user.put_in_hands(tank)
tank = null
if(!removing)
if(tank)
to_chat(user, span_warning("\The [src] already has a tank."))
return
if(!user.transferItemToLoc(thetank, src))
return
to_chat(user, span_notice("You hook \the [thetank] up to \the [src]."))
tank = thetank
update_appearance()
/obj/item/pneumatic_cannon/update_overlays()
. = ..()
if(!tank)
return
. += tank.icon_state
/obj/item/pneumatic_cannon/proc/fill_with_type(type, amount)
if(!ispath(type, /obj) && !ispath(type, /mob))
return FALSE
var/loaded = 0
for(var/i in 1 to amount)
var/obj/item/I = new type
if(!load_item(I, null))
qdel(I)
return loaded
loaded++
CHECK_TICK
/obj/item/pneumatic_cannon/pie
name = "pie cannon"
desc = "Load cream pie for optimal results."
force = 10
icon_state = "piecannon"
gasPerThrow = 0
range_multiplier = 3
throw_amount = 1
maxWeightClass = (/obj/item/food/pie::w_class * 50) //50 pies. :^)
needs_air = FALSE
clumsyCheck = FALSE
var/static/list/pie_typecache = typecacheof(/obj/item/food/pie)
/obj/item/pneumatic_cannon/pie/Initialize(mapload)
. = ..()
allowed_typecache = pie_typecache
/obj/item/pneumatic_cannon/pie/selfcharge
automatic = TRUE
selfcharge = TRUE
charge_type = /obj/item/food/pie/cream
maxWeightClass = (/obj/item/food/pie::w_class * 20) //20 pies.
/obj/item/pneumatic_cannon/pie/selfcharge/cyborg
name = "low velocity pie cannon"
automatic = FALSE
charge_type = /obj/item/food/pie/cream/nostun
maxWeightClass = (/obj/item/food/pie::w_class * 2) //2 pies
charge_ticks = 2 //4 second/pie
#undef PCANNON_FIREALL
#undef PCANNON_FILO
#undef PCANNON_FIFO
#undef LOW_PRESSURE
#undef MID_PRESSURE
#undef HIGH_PRESSURE