Siege Warfare: Adds two new craftable emplaced guns, a bigger laser musket, and a new spear for crew-sided mining. (#94102)

<img width="517" height="305" alt="image"
src="https://github.com/user-attachments/assets/e8cd8bcd-dd3c-4733-99fe-0cdfb9799182"
/>

So, been a while, hasn't it? This PR adds three things. First Up: The
Ratvarian Repeater.


![side](https://github.com/user-attachments/assets/cdbadfad-29a9-4e32-b1b6-85872db94143)

The Ratvarian repeater is a mounted gun made from bronze, stock parts,
and some drinking glasses. it is the emplaced counterpart to the laser
musket, as the pipe organ gun is to the crossbow. Being a laser musket
derivative, this means it has the benefit of _infinite ammo._ However,
this also means to reload it, you turn the crank on the back. My
personal headcanon was it works via a spring mechanism not unlike a
watch, so naturally, it turned into a clock-cult flavored weapon. Each
individual volley per winding shoots 12 laser musket shots in bursts of
two.


https://github.com/user-attachments/assets/152c3ce8-3560-4c8a-b618-7f2d81fb75a0

However, this isnt the only thing it's capable of. If you prefer your
weapons more iconoclastic, you can take a welder to the stand, and carry
it around, for the ability to shoot two laser musket shots instead of
one per trigger pull... with the price being a decreased movespeed, and
the second shot having random spread. Also you cant redeploy the stand,
because iconoclasm. Still, the power is there, if you desire it.

Overall, the repeater is the "Expensive but reliable" option of the
three craftable gun emplacements. It has the most work to get, but once
you have it, theres no need for ammo.


On the complete opposite side of the coin, we have the Improvised
Ballista.


https://github.com/user-attachments/assets/16262965-a860-4205-bc3e-eeb8ec593647

This thing is as cheap as it gets - taking iron and wire to make (plus
tools), and fires the classic spears greytiders love for 80 damage. This
sounds like a lot (and it is), but once the ballista has fired - that's
it, and reloading a second spear takes a full six seconds. Of the four
emplaced guns, it has the least damage per firing sequence, but has all
that damage in one big projectile. The ballista overall is designed to
be dirt cheap to make and generally useful, but requiring a lot of work
to keep running.



And lastly, we have the Giant-killer spear. 
<img width="417" height="287" alt="image"
src="https://github.com/user-attachments/assets/613379aa-379c-40b3-b4fc-ccdaa5ea4678"
/>

Crafted from titanium, plasteel and seven knives, and then forged by
throwing it into lava this monstrously oversized spear is designed to
fight monstrously oversized... monsters. While only doing 21 damage when
wielded, this damage is multiplied by 3.5 against lavaland fauna,
dealing damage equal to a crusher mark detonation. However, despite the
ease of use, it lacks the ability to mount trophies like the crusher, so
is much more suited to killing basic enemies like legions or goliaths,
rather than megafauna. (It still does decently versus them, but youre
prone to just dying.) Additionally, due to lacking the requirements of
mining access or lavaland monsters, it can be made by the crew at large,
providing them a meaningful defense on lavaland/icemoon. It can also be
loaded into the ballista, so you can, if you desire, show those
ashwalkers how the dark ages were REALLY fought.

<img width="202" height="236" alt="image"
src="https://github.com/user-attachments/assets/d21a7275-3d9e-427b-ac14-fee080541fb5"
/>

This PR has been six months in the making, so it feels good to finally
get it out.

Why its good for the game: Similar logic to my previous improvised
weapon prs; I feel the dichotomy of "High tech laser rifle" and "Gun
made from stuff lying around" is an interesting one. Of the damage
values, the only one that has the potential for a balance issue is the
spear, but due to being basically a plastitanium spear that does better
versus mining mobs, I don't forsee this becoming a weapon that you can
arm an entire revolution with.

## Changelog

🆑 Webcomicartist
add: Added the ratvarian repeater, a craftable emplaced crank laser
weapon that can be carried by hand, themed after clock cult.
add: Added the improvised ballista, the rebar crossbow's bigger brother,
that fires spears. Huge damage, terrible fire rate.
add: Added the giant-slayer spear, a spear made at a necropolis tendril
that does extra damage to lavaland creatures. (Ballista compatible)
image: added sprites for the above.
qol: The pipe organ gun should be easier to light now.
fix: Fixed Pipe Organ Gun firing sprites not always going back to normal
after finishing firing.
refactor: Mounted guns are now EVEN MORE flexible codewise, now
supporting secondary ammo types.
/🆑

---------

Co-authored-by: Jacquerel <hnevard@gmail.com>
This commit is contained in:
KingkumaArt
2025-12-21 03:03:54 +00:00
committed by GitHub
co-authored by Jacquerel
parent b9194f31e0
commit 47606e453e
17 changed files with 391 additions and 26 deletions
@@ -9,20 +9,35 @@
icon = 'icons/obj/weapons/cannons.dmi'
icon_state = "falconet_patina"
var/icon_state_base = "falconet_patina"
var/icon_state_loaded = "falconet_patina"
var/icon_state_fire = "falconet_patina_fire"
max_integrity = 300
///whether the cannon can be unwrenched from the ground. Anchorable_cannon equivalent.
var/anchorable_gun = TRUE
/// does this thing need ammo at all or does it just make ammo?
var/uses_ammo = TRUE
///Max shots per firing of the gun.
var/max_shots_per_fire = 1
///Delay it takes to load the gun. Set to 0 if none.
var/load_delay = 0
///Message displayed when loading gun
var/loading_message = "gun loaded"
///Shots currently loaded. Should never be more than max_shots_per_fire.
var/shots_in_gun = 1
///shots added to gun, per piece of ammo loaded.
var/shots_per_load = 1
///Does it have an alternative ammo type it can uses
var/has_alt_ammo = FALSE
//Is it loaded with said alternative ammo?
var/use_alt_ammo = FALSE
///Accepted "ammo" type
var/obj/item/ammo_type = /obj/item/ammo_casing/strilka310
///Alternative ammo types, for extra effects!
var/obj/item/alt_ammo_type = /obj/item/ammo_casing/strilka310
///Projectile from said gun. Doesnt automatically inherit said ammo's projectile in case you wanted to make a gun that shoots floor tiles or something.
var/obj/projectile/projectile_type = /obj/projectile/bullet/strilka310
///Projectile from said gun. Doesnt automatically inherit said ammo's projectile in case you wanted to make a gun that shoots floor tiles or something.
var/obj/projectile/alt_projectile_type = /obj/projectile/bullet/strilka310
///If the gun has anything in it.
var/loaded_gun = TRUE
///If the gun is currently loaded with its maximum capacity.
@@ -37,6 +52,8 @@
var/fire_sound = 'sound/items/weapons/gun/general/mountedgun.ogg'
///sound of firing for last shot
var/last_fire_sound = 'sound/items/weapons/gun/general/mountedgunend.ogg'
///So you can't reload it mid-firing
var/is_firing = FALSE
/obj/structure/mounted_gun/wrench_act(mob/living/user, obj/item/tool)
. = ..()
@@ -46,37 +63,45 @@
return ITEM_INTERACT_SUCCESS
///Covers Reloading and lighting of the gun
/obj/structure/mounted_gun/attackby(obj/item/ammo_casing/used_item, mob/user, list/modifiers, list/attack_modifiers)
var/ignition_message = used_item.ignition_effect(src, user) // Checks if item used can ignite stuff.
if(istype(used_item, ammo_type))
/obj/structure/mounted_gun/attackby(obj/item/ammo_casing/used_item, mob/user, params)
if(is_firing)
balloon_alert(user, "gun is firing!")
return
if(istype(used_item, ammo_type) && (uses_ammo == TRUE)) //see if the gun needs to be loaded in some way.
if(fully_loaded_gun)
balloon_alert(user, "already fully loaded!")
return
else
shots_in_gun = shots_in_gun + shots_per_load //Add one to the shots in the gun
loaded_gun = TRUE // Make sure it registers theres ammo in there, so it can fire.
QDEL_NULL(used_item)
if(shots_in_gun >= max_shots_per_fire)
shots_in_gun = max_shots_per_fire // in case of somehow firing only some of a guns shots, and reloading, you still cant get above the maximum ammo size.
fully_loaded_gun = TRUE //So you cant load extra.
if(load_delay > 0 && !do_after(user, load_delay, target = src))
return
else if(ignition_message) // if item the player used ignites, light the gun!
visible_message(ignition_message)
user.log_message("fired a cannon", LOG_ATTACK)
log_game("[key_name(user)] fired a cannon in [AREACOORD(src)]")
addtimer(CALLBACK(src, PROC_REF(fire)), fire_delay) //uses fire proc as shown below to shoot the gun
shots_in_gun = shots_in_gun + shots_per_load //Add one to the shots in the gun
balloon_alert(user, loading_message)
loaded_gun = TRUE // Make sure it registers theres ammo in there, so it can fire.
QDEL_NULL(used_item)
if(shots_in_gun >= max_shots_per_fire)
shots_in_gun = max_shots_per_fire // in case of somehow firing only some of a guns shots, and reloading, you still cant get above the maximum ammo size.
fully_loaded_gun = TRUE //So you cant load extra.
icon_state = icon_state_loaded
return
..()
/obj/structure/mounted_gun/attack_hand(mob/living/user, list/modifiers)
if(is_firing)
balloon_alert(user, "gun is firing!")
return
user.log_message("fired a mounted gun", LOG_ATTACK)
log_game("[key_name(user)] fired a mounted gun in [AREACOORD(src)]")
addtimer(CALLBACK(src, PROC_REF(fire)), fire_delay) //uses fire proc as shown below to shoot the gun
return
/obj/structure/mounted_gun/proc/fire()
if (!loaded_gun)
balloon_alert_to_viewers("gun is not loaded!","",2)
return
is_firing = TRUE
for(var/times_fired = 1, times_fired <= shots_in_gun, times_fired++) //The normal DM for loop structure since the times it has fired is changing in the loop itself.
for(var/mob/shaken_mob in urange(10, src))
if(shaken_mob.stat == CONSCIOUS && firing_shakes_camera == TRUE)
if(shaken_mob.stat == CONSCIOUS && firing_shakes_camera == TRUE) //is the mob awake to feel the shaking?
shake_camera(shaken_mob, 3, 1)
icon_state = icon_state_fire
if(loaded_gun)
@@ -84,23 +109,29 @@
if (times_fired < shots_in_gun)
playsound(src, fire_sound, 50, FALSE, 5)
else
playsound(src, last_fire_sound, 50, TRUE, 5)
var/obj/projectile/fired_projectile = new projectile_type(get_turf(src))
playsound(src, last_fire_sound, 50, TRUE, 5) //for the empty fire sound
var/obj/projectile/fired_projectile = (use_alt_ammo) ? new alt_projectile_type(get_turf(src)) : new projectile_type(get_turf(src))
fired_projectile.firer = src
fired_projectile.fired_from = src
fired_projectile.fire(dir2angle(dir))
sleep(shot_delay)
loaded_gun = FALSE
shots_in_gun = 0
fully_loaded_gun = FALSE
if(uses_ammo == TRUE)
loaded_gun = FALSE
use_alt_ammo = FALSE
shots_in_gun = 0
fully_loaded_gun = FALSE
is_firing = FALSE
icon_state = icon_state_base
/obj/structure/mounted_gun/pipe
/obj/structure/mounted_gun/organ_gun
name = "Pipe Organ Gun"
desc = "To become master over one who has killed, one must become a better killer. This engine of destruction is one of many things made to that end."
icon_state = "pipeorgangun"
icon_state_base = "pipeorgangun"
icon_state_loaded = "pipeorgangun"
icon_state_fire = "pipeorgangun_fire"
anchored = FALSE
anchorable_gun = TRUE
@@ -116,7 +147,7 @@
firing_shakes_camera = FALSE
custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 24.5, /datum/material/wood = SHEET_MATERIAL_AMOUNT * 15, /datum/material/glass = SMALL_MATERIAL_AMOUNT)
/obj/structure/mounted_gun/pipe/examine_more(mob/user)
/obj/structure/mounted_gun/organ_gun/examine_more(mob/user)
. = ..()
. += span_notice("<b><i>Looking down at \the [src], you recall a tale told to you in some distant memory...</i></b>")
@@ -126,7 +157,7 @@
. += span_info("As such, the man who ended another's life with a stone, was in turn smote himself by another wielding a spear. After spears, bows. Swords. Guns. Tanks. Missiles. And on and on Vengeance fed. Growing stronger. Growing Worse.")
. += span_info("Vengeance persists to this day. It sometimes may slumber, seemingly content with having gorged itself, but in the end, its ceaseless hunger can be neither numbed nor sated.")
/obj/structure/mounted_gun/pipe/fire()
/obj/structure/mounted_gun/organ_gun/fire()
if (!loaded_gun)
balloon_alert_to_viewers("Gun is not loaded!","",2)
return
@@ -165,6 +196,7 @@
desc = "''Quantity has a quality of its own.''"
icon_state = "canister_gatling"
icon_state_base = "canister_gatling"
icon_state_loaded = "canister_gatling"
icon_state_fire = "canister_gatling_fire"
anchored = FALSE
anchorable_gun = TRUE
@@ -187,3 +219,136 @@
throwforce = 0
w_class = WEIGHT_CLASS_BULKY
projectile_type = /obj/projectile/bullet/shrapnel
/obj/structure/mounted_gun/ratvarian_repeater
name = "Ratvarian Repeater"
desc = "''Brains? Bronze? Why not both?''"
icon_state = "ratvarian_repeater"
icon_state_base = "ratvarian_repeater"
icon_state_fire = "ratvarian_repeater_fire"
loading_message = "gun charged"
anchored = FALSE
anchorable_gun = TRUE
uses_ammo = FALSE
load_delay = 30
max_shots_per_fire = 12
shots_per_load = 12
shots_in_gun = 12
fire_sound = 'sound/items/weapons/thermalpistol.ogg'
last_fire_sound = 'sound/items/weapons/thermalpistol.ogg'
projectile_type = /obj/projectile/beam/laser/musket/repeater
loaded_gun = TRUE
fully_loaded_gun = TRUE
fire_delay = 1
shot_delay = 2
firing_shakes_camera = FALSE
custom_materials = list(
/datum/material/iron = SHEET_MATERIAL_AMOUNT * 5.25,
/datum/material/bronze = SHEET_MATERIAL_AMOUNT * 5,
/datum/material/glass = SHEET_MATERIAL_AMOUNT * 1.29
)
/obj/structure/mounted_gun/ratvarian_repeater/dump_contents()
return // Stub because we don't want to do anything here
/obj/structure/mounted_gun/ratvarian_repeater/attack_hand(mob/user, params) //the repeater is weird so has to have its own code since it takes no ammo.
if(is_firing)
balloon_alert(user, "gun is firing!")
return
if(!fully_loaded_gun)
if(!do_after(user, load_delay, target = src))
return
shots_in_gun = shots_per_load //Add one to the shots in the gun
balloon_alert(user, "mechanism wound.")
playsound(src, 'sound/effects/magic/clockwork/fellowship_armory.ogg', 50, FALSE, 5)
loaded_gun = TRUE // Make sure it registers theres ammo in there, so it can fire.
if(shots_in_gun >= max_shots_per_fire)
shots_in_gun = max_shots_per_fire // in case of somehow firing only some of a guns shots, and reloading, you still cant get above the maximum ammo size.
fully_loaded_gun = TRUE //So you cant load extra.
return
user.log_message("fired a ratvatian repeater", LOG_ATTACK)
log_game("[key_name(user)] fired a ratvatian repeater in [AREACOORD(src)]")
addtimer(CALLBACK(src, PROC_REF(fire)), fire_delay) //uses fire proc as shown below to shoot the gun
/obj/structure/mounted_gun/ratvarian_repeater/fire()
if (!loaded_gun)
balloon_alert_to_viewers("needs winding!", vision_distance = 2)
return
is_firing = TRUE
for(var/times_fired = 1, times_fired <= shots_in_gun, times_fired++) //The normal DM for loop structure since the times it has fired is changing in the loop itself.
if(loaded_gun)
icon_state = icon_state_fire
if (times_fired < shots_in_gun)
playsound(src, fire_sound, 50, FALSE, 5)
else
playsound(src, last_fire_sound, 50, TRUE, 5) //for the empty fire sound
var/obj/projectile/fired_projectile = new projectile_type(get_turf(src))
fired_projectile.firer = src
fired_projectile.fired_from = src
fired_projectile.fire(dir2angle(dir))
if(times_fired % 2 != 1)//Burst Fire.
sleep(shot_delay)
sleep(shot_delay)
loaded_gun = FALSE
shots_in_gun = 0
fully_loaded_gun = FALSE
is_firing = FALSE
icon_state = icon_state_base
/obj/structure/mounted_gun/ballista
name = "Improvised Ballista"
desc = "''Engineers like to solve problems. If there are no problems handily available, they will create their own problems.''"
icon_state = "Improvised_Ballista"
icon_state_base = "Improvised_Ballista"
icon_state_loaded = "Improvised_Ballista_Loaded"
icon_state_fire = "Improvised_Ballista"
anchored = FALSE
anchorable_gun = TRUE
uses_ammo = TRUE
load_delay = 60
max_shots_per_fire = 1
shots_per_load = 1
shots_in_gun = 1
fire_sound = 'sound/items/xbow_lock.ogg'
last_fire_sound = 'sound/items/xbow_lock.ogg'
has_alt_ammo = TRUE
ammo_type = /obj/item/spear
projectile_type = /obj/projectile/bullet/ballista_spear
alt_ammo_type = /obj/item/spear/dragonator
alt_projectile_type = /obj/projectile/bullet/ballista_spear_dragon
loaded_gun = FALSE
fully_loaded_gun = FALSE
fire_delay = 1
shot_delay = 1
firing_shakes_camera = FALSE
custom_materials = list(
/datum/material/iron = SHEET_MATERIAL_AMOUNT * 15.15,
/datum/material/glass = SMALL_MATERIAL_AMOUNT * 1.5
)
/obj/structure/mounted_gun/ballista/attackby(obj/item/ammo_casing/used_item, mob/user, params) //again its single shot so its kinda weird.
if(is_firing)
balloon_alert(user, "gun is firing")
return
if(!istype(used_item, ammo_type))
return
if(fully_loaded_gun)
balloon_alert(user, "already fully loaded!")
return
if(istype(used_item, alt_ammo_type))
use_alt_ammo = TRUE
playsound(src, 'sound/items/weapons/draw_bow.ogg', 50, FALSE, 5)
do_after(user, load_delay, target = src)
shots_in_gun = 1 //MAX OF ONE SHOT.
balloon_alert(user, loading_message)
loaded_gun = TRUE
QDEL_NULL(used_item)
fully_loaded_gun = TRUE
icon_state = icon_state_loaded