diff --git a/_maps/map_files/domotan/old_doma.dmm b/_maps/map_files/domotan/old_doma.dmm
index a64d3ca21..ed246492e 100644
--- a/_maps/map_files/domotan/old_doma.dmm
+++ b/_maps/map_files/domotan/old_doma.dmm
@@ -2171,14 +2171,18 @@
/area/rogue/outdoors/beach)
"cxZ" = (
/obj/structure/closet/crate/roguecloset,
-/obj/item/quiver/arrows,
-/obj/item/quiver/arrows,
-/obj/item/quiver/arrows,
-/obj/item/quiver/arrows,
-/obj/item/quiver/arrows,
/obj/effect/decal/cobbleedge{
icon_state = "cobbleedge-sread"
},
+/obj/item/quiver/shotpouch/ironshots,
+/obj/item/quiver/shotpouch/ironshots,
+/obj/item/quiver/shotpouch/ironshots,
+/obj/item/gunpowderhorn,
+/obj/item/gunpowderhorn,
+/obj/item/gunpowderhorn,
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/advanced,
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/advanced,
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/advanced,
/turf/open/floor/rogue/blocks,
/area/rogue/indoors/town/garrison)
"czX" = (
diff --git a/code/__DEFINES/font_changers.dm b/code/__DEFINES/font_changers.dm
index b6a9279a1..e06a9b37c 100644
--- a/code/__DEFINES/font_changers.dm
+++ b/code/__DEFINES/font_changers.dm
@@ -9,7 +9,7 @@
#define FONT_LARGE(str) ("[str]")
//colour changes
-#define FONT_BRIGHTRED(str) ("[str]")
+#define FONT_BRIGHTRED(str) ("[str]")
#define FONT_YELLOW(str) ("[str]")
#define FONT_PURPLE(str) ("[str]")
#define FONT_GREEN(str) ("[str]")
diff --git a/code/datums/skills/combat.dm b/code/datums/skills/combat.dm
index b2a4cef77..8c57b6de1 100644
--- a/code/datums/skills/combat.dm
+++ b/code/datums/skills/combat.dm
@@ -105,6 +105,13 @@
"...the crossbow is a deadly marvel of engineering, waiting for your guidance. You steady your breath, finger poised on the trigger. The world narrows as you take aim, the perfect shot soon to come..."
)
+/datum/skill/combat/firearms
+ name = "Firearms"
+ dreams = list(
+ "...'HOLD!' your commander yells, you're just able to maintain your balance and your cannon's aim trained on the behemoth of an airborne demon just off the airship's port-side. 'FIRE!' your cannon looses its payload, the demon is hit in the eye - blinded, it begins to fall out of the air...",
+ "...an instructor drills the same mantra over and over into you and your comrades: 'fill the flash pan with the powder, put your ball in, pack it into the powder with your ramrod, close the flash pan. Fail any of these steps and you may as well be blowing air at your enemy'..."
+ )
+
/datum/skill/combat/wrestling
name = "Wrestling"
dreams = list(
diff --git a/code/game/objects/effects/temporary_visuals/miscellaneous.dm b/code/game/objects/effects/temporary_visuals/miscellaneous.dm
index 6eba2400a..d40cb6c81 100644
--- a/code/game/objects/effects/temporary_visuals/miscellaneous.dm
+++ b/code/game/objects/effects/temporary_visuals/miscellaneous.dm
@@ -230,6 +230,9 @@
icon_state = "smoke"
duration = 50
+/obj/effect/temp_visual/small_smoke/gunpowdersmoke
+ duration = 15
+
/obj/effect/temp_visual/small_smoke/halfsecond
duration = 5
diff --git a/code/game/objects/items/rogueweapons/ranged/ammo.dm b/code/game/objects/items/rogueweapons/ranged/ammo.dm
index b5b1e895a..a3b667b19 100644
--- a/code/game/objects/items/rogueweapons/ranged/ammo.dm
+++ b/code/game/objects/items/rogueweapons/ranged/ammo.dm
@@ -212,7 +212,7 @@
poisonfeel = "burning" //Ditto
poisonamount = 5 //Support and balance for bodkins, which will hold less poison due to how
-//pyro bolts - stonekeep port
+//pyro bolts - stonekeep port
/obj/item/ammo_casing/caseless/rogue/bolt/pyro
name = "pyroclastic bolt"
@@ -327,28 +327,26 @@
addtimer(CALLBACK(M, TYPE_PROC_REF(/mob/living, adjustToxLoss), 100), 10 SECONDS)
addtimer(CALLBACK(M, TYPE_PROC_REF(/atom, visible_message), span_danger("[M] appears greatly weakened by the poison!")), 10 SECONDS)
-/obj/projectile/bullet/reusable/bullet
- name = "lead ball"
- damage = 50
+/obj/item/ammo_casing/caseless/rogue/bullet/ironshot
+ name = "iron ball"
+ desc = "A small iron ball, often seen being propelled with great force from various firearms."
+ projectile_type = /obj/projectile/bullet/ironshot
+ caliber = "musketball"
+ icon = 'icons/roguetown/weapons/ammo.dmi'
+ icon_state = "musketball"
+ dropshrink = 0.5
+
+/obj/projectile/bullet/ironshot
+ name = "iron ball"
+ damage = 80
damage_type = BRUTE
+ armor_penetration = 40 // this bullet does more damage than a bolt, so less armour-pen is a fair trade-off
icon = 'icons/roguetown/weapons/ammo.dmi'
icon_state = "musketball_proj"
- ammo_type = /obj/item/ammo_casing/caseless/rogue/bullet
+ ammo_type = /obj/item/ammo_casing/caseless/rogue/bullet/ironshot
range = 30
- hitsound = 'sound/combat/hits/hi_arrow2.ogg'
+ hitsound = 'sound/combat/hits/hi_bolt (3).ogg'
embedchance = 100
- woundclass = BCLASS_STAB
+ woundclass = BCLASS_SMASH // doesn't really cause severed arteries, but feels like it dislocates and fractures bones a lot more
flag = "bullet"
- armor_penetration = 200
speed = 0.1
-
-/obj/item/ammo_casing/caseless/rogue/bullet
- name = "lead sphere"
- desc = "A small lead sphere. This should go well with gunpowder."
- projectile_type = /obj/projectile/bullet/reusable/bullet
- caliber = "musketball"
- icon = 'icons/roguetown/weapons/ammo.dmi'
- icon_state = "musketball"
- dropshrink = 0.5
- possible_item_intents = list(/datum/intent/use)
- max_integrity = 0.1
diff --git a/code/game/objects/items/rogueweapons/ranged/powder_guns.dm b/code/game/objects/items/rogueweapons/ranged/powder_guns.dm
new file mode 100644
index 000000000..05ab70551
--- /dev/null
+++ b/code/game/objects/items/rogueweapons/ranged/powder_guns.dm
@@ -0,0 +1,322 @@
+#define UNLOADED 0 // nothing in the barrel
+#define DRY_LOADED 1 // there's a round in the barrel, but no gunpowder
+#define SEMI_LOADED 2 // a round is in the barrel but it hasn't been packed into the powder yet
+#define LOADED 3 // the round is ready to go
+
+#define EMPTY 0 // no gunpowder
+#define FILLED 1 // the powder is in the flash pan or barrel
+#define PACKED 2 // the musket shot has been packed into the powder
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun // this is going to become a proper handcannon later on, but for now i want to focus on getting muskets implemented - Hocka
+ name = "hand cannon"
+ desc = "A basic, primordial iteration of a firearm."
+ possible_item_intents = list(/datum/intent/mace/smash/wood, /datum/intent/shoot/powdergun, /datum/intent/arc/powdergun)
+ mag_type = /obj/item/ammo_box/magazine/internal/shot/powdergun // pretty much every handheld powder gun i can think of will use these, thank god
+ slot_flags = ITEM_SLOT_BACK
+ w_class = WEIGHT_CLASS_BULKY
+ experimental_inhand = TRUE
+ experimental_onback = TRUE
+ randomspread = 1
+ spread = 0
+ can_parry = TRUE
+ pin = /obj/item/firing_pin
+ force = 10
+ cartridge_wording = "bullet"
+ fire_sound = 'sound/combat/Ranged/musket-shot.ogg'
+ dry_fire_sound = 'sound/combat/Ranged/musket-shot-unpowdered.ogg'
+ anvilrepair = /datum/skill/craft/engineering
+ var/two_handed = FALSE //does the gun require both hands to fire it
+ var/powder = FALSE
+ var/damfactor = 1 //lets one gun do more damage than another gun with the same projectile
+ var/reload_status = 0
+
+/obj/item/ammo_box/magazine/internal/shot/powdergun
+ ammo_type = /obj/item/ammo_casing/caseless/rogue/bullet
+ caliber = "musketball"
+ max_ammo = 1
+ start_empty = TRUE
+
+/datum/intent/shoot/powdergun
+ chargedrain = 0
+
+/datum/intent/shoot/powdergun/can_charge()
+ if(mastermob)
+ if(mastermob.get_num_arms(FALSE) < 2)
+ return FALSE
+ return TRUE // we aren't checking for an empty second hand here because manually-lit firearms literally require that you're holding a fire source in your other hand
+
+/datum/intent/shoot/powdergun/get_chargetime() // i should probably rewrite this and take advantage of it for when hand cannons and their changeable fuses are added
+ if(mastermob && chargetime)
+ var/newtime = chargetime
+ //skill block
+ newtime = newtime + 80
+ newtime = newtime - (mastermob.mind.get_skill_level(/datum/skill/combat/firearms) * 20)
+ //stat block
+ newtime = newtime + 20
+ newtime = newtime - ((mastermob.STAPER)*1.5)
+ if(newtime > 0)
+ return newtime
+ else
+ return 5
+ return chargetime
+
+/datum/intent/arc/powdergun
+ chargedrain = 0
+
+/datum/intent/arc/powdergun/can_charge()
+ if(mastermob)
+ if(mastermob.get_num_arms(FALSE) < 2)
+ return FALSE
+ return TRUE
+
+/datum/intent/shoot/powdergun/get_chargetime()
+ if(mastermob && chargetime)
+ var/newtime = chargetime
+
+ newtime = newtime + 80
+ newtime = newtime - (mastermob.mind.get_skill_level(/datum/skill/combat/firearms) * 20)
+
+ newtime = newtime + 20
+ newtime = newtime - ((mastermob.STAPER)*1.5)
+ if(newtime > 0)
+ return newtime
+ else
+ return 5
+ return chargetime
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/proc/update_reload_status()
+ if(chambered)
+ switch(powder) //we already know there's a ball in the barrel at this point, so the rest of the context is entirely dependant on what state the gunpowder is in
+ if(EMPTY)
+ reload_status = DRY_LOADED
+ if(FILLED)
+ reload_status = SEMI_LOADED
+ if(PACKED)
+ reload_status = LOADED
+ else
+ reload_status = UNLOADED
+ if(powder == PACKED)
+ powder = FILLED // the bullet got removed, so the powder isn't packed anymore and it has to be redone
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/examine(mob/user)
+ . = ..()
+ switch(reload_status)
+ if(UNLOADED)
+ . += span_info("Barrel is " + FONT_BRIGHTRED("empty"))
+ if(DRY_LOADED)
+ . += span_info("Barrel is " + FONT_BRIGHTRED("dry-loaded"))
+ if(SEMI_LOADED)
+ . += span_info("Barrel is " + FONT_YELLOW("partially loaded"))
+ if(LOADED)
+ . += span_info("Barrel is " + FONT_GREEN("loaded"))
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/attack_self()
+ ..()
+ update_reload_status()
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/MiddleClick(mob/living/user)
+ user.changeNext_move(user.used_intent.clickcd)
+ if(reload_status == SEMI_LOADED)
+ var/pack_timer = 30 - (user.mind.get_skill_level(/datum/skill/combat/firearms) * 3) // people who are better with guns can pack the powder faster
+ user.visible_message("[user] starts packing \the [src]'s gunpowder.")
+ playsound(src.loc, 'sound/combat/Ranged/gunpowder-packing.ogg', 100, FALSE)
+ if(do_after(user, pack_timer, FALSE))
+ powder = PACKED
+ update_reload_status()
+ else
+ to_chat(user, "You need to add ball and powder first!")
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/attackby(obj/item/A, mob/user, params)
+ ..()
+
+ if(istype(A, /obj/item/gunpowderhorn))
+ if(chambered)
+ to_chat(user, span_warn("The ammunition is in the way, remove it first!"))
+ return
+ if(!powder)
+ user.visible_message("[user] starts powdering \the [src].")
+ playsound(src.loc, 'sound/combat/Ranged/gunpowder-pouring.ogg', 100, FALSE)
+ if(do_after(user, 10, FALSE))
+ powder = FILLED
+
+ update_reload_status()
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/shoot_with_empty_chamber(mob/living/user)
+ if(powder)
+ playsound(src.loc, 'sound/combat/Ranged/musket-shot.ogg', 100, FALSE)
+ new /obj/effect/temp_visual/small_smoke/gunpowdersmoke(get_step(user, user.dir))
+ powder = EMPTY
+ else
+ playsound(src.loc, 'sound/combat/Ranged/musket-shot-unpowdered.ogg', 100, FALSE)
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/shoot_live_shot(mob/living/user)
+ ..()
+ new /obj/effect/temp_visual/small_smoke/gunpowdersmoke(get_step(user, user.dir))
+ powder = EMPTY
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/can_shoot()
+ if(reload_status == LOADED)
+ . = ..()
+ else
+ return FALSE
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/process_fire(atom/target, mob/living/user)
+
+ if(user.get_num_arms(FALSE) < 2)
+ return FALSE
+ if(two_handed && user.get_inactive_held_item())
+ return FALSE
+ if(user.client)
+ if(user.client.chargedprog >= 100)
+ spread = 0
+ else
+ spread = 150 - (150 * (user.client.chargedprog / 100))
+ else
+ spread = 75
+
+ for(var/obj/item/ammo_casing/CB in get_ammo_list(FALSE, TRUE))
+ var/obj/projectile/BB = CB.BB
+ BB.damage = BB.damage * damfactor
+
+ ..()
+ update_reload_status() // calling this on the shoot_live_shot proc causes it to read the chambered var before it gets updated, which results in the update proc incorrectly thinking a round is still loaded
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/advanced // this is to separate powder guns with a complex firing mechanism from those that don't - so we don't run into conflicts where a hand cannon starts checking for a flash pan that it shouldn't have
+ name = "musket"
+ desc = "A simple two-handed firearm developed by cutting edge industrial minds."
+ icon = 'icons/roguetown/weapons/guns.dmi'
+ possible_item_intents = list(/datum/intent/mace/smash/wood, /datum/intent/shoot/powdergun/advanced, /datum/intent/arc/powdergun/advanced)
+ icon_state = "arquebus"
+ slot_flags = ITEM_SLOT_BACK
+ w_class = WEIGHT_CLASS_BULKY
+ experimental_inhand = TRUE
+ experimental_onback = TRUE
+ randomspread = 0
+ spread = 0
+ can_parry = TRUE
+ pin = /obj/item/firing_pin
+ fire_sound = 'sound/combat/Ranged/musket-shot.ogg'
+ damfactor = 1.5
+ two_handed = TRUE
+ var/pan_open = FALSE
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/advanced/getonmobprop(tag)
+ . = ..()
+ if(tag)
+ switch(tag)
+ if("gen")
+ return list(
+ "shrink" = 0.7,
+ "sx" = 2,
+ "sy" = -4,
+ "nx" = 2,
+ "ny" = -4,
+ "wx" = -2,
+ "wy" = -4,
+ "ex" = 3,
+ "ey" = -4,
+ "northabove" = 0,
+ "southabove" = 1,
+ "eastabove" = 0,
+ "westabove" = 1,
+ "nturn" = 90,
+ "sturn" = 90,
+ "wturn" = 90,
+ "eturn" = 90,
+ "nflip" = 1,
+ "sflip" = 1,
+ "wflip" = 1,
+ "eflip" = 0)
+ if("onback")
+ return list(
+ "shrink" = 0.7,
+ "sx" = 0,
+ "sy" = 0,
+ "nx" = 0,
+ "ny" = 0,
+ "wx" = 5,
+ "wy" = 0,
+ "ex" = -5,
+ "ey" = 0,
+ "northabove" = 1,
+ "southabove" = 0,
+ "eastabove" = 1,
+ "westabove" = 0,
+ "nturn" = 90,
+ "sturn" = 90,
+ "wturn" = 135,
+ "eturn" = 45,
+ "nflip" = 0,
+ "sflip" = 1,
+ "wflip" = 0,
+ "eflip" = 1)
+
+/datum/intent/shoot/powdergun/advanced/can_charge()
+ if(mastermob)
+ if(mastermob.get_num_arms(FALSE) < 2)
+ return FALSE
+ if(mastermob.get_inactive_held_item())
+ return FALSE
+ return TRUE
+
+/datum/intent/arc/powdergun/advanced/can_charge()
+ if(mastermob)
+ if(mastermob.get_num_arms(FALSE) < 2)
+ return FALSE
+ if(mastermob.get_inactive_held_item())
+ return FALSE
+ return TRUE
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/advanced/examine(mob/user)
+ . = ..()
+ var/pan_status
+ if(pan_open)
+ pan_status = FONT_BRIGHTRED("open")
+ else
+ pan_status = FONT_GREEN("closed")
+
+ if(powder)
+ . += span_info("Flash pan is " + FONT_GREEN("full ") + "and " + pan_status)
+ else
+ . += span_info("Flash pan is " + FONT_BRIGHTRED("empty ") + "and " + pan_status)
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/advanced/attack_right(mob/living/user)
+ if(!pan_open)
+ to_chat(user, "You flick open \the [src]'s flash pan.")
+ playsound(user, 'sound/combat/Ranged/musket-flashpan-open.ogg', 100, FALSE)
+ pan_open = TRUE
+ else
+ to_chat(user, "You close \the [src]'s flash pan.")
+ playsound(user, 'sound/combat/Ranged/musket-flashpan-close.ogg', 100, FALSE)
+ pan_open = FALSE
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/advanced/can_shoot()
+ if(!pan_open)
+ . = ..()
+ else
+ return FALSE
+
+/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/advanced/attackby(obj/item/A, mob/user, params)
+ if(istype(A, /obj/item/gunpowderhorn))
+ if(pan_open)
+ ..()
+ else
+ to_chat(user, span_warning("Flash pan is closed!"))
+ return
+ ..()
+
+/obj/item/gunpowderhorn
+ name = "gunpowder horn"
+ desc = "a bag filled with gunpowder, used for priming powder-based firearms."
+ icon = 'icons/roguetown/weapons/guns.dmi'
+ icon_state = "powderhorn"
+ w_class = WEIGHT_CLASS_NORMAL
+ slot_flags = ITEM_SLOT_HIP|ITEM_SLOT_NECK
+
+#undef UNLOADED
+#undef DRY_LOADED
+#undef SEMI_LOADED
+#undef LOADED
+#undef EMPTY
+#undef FILLED
+#undef PACKED
diff --git a/code/modules/cargo/packsrogue/weapons.dm b/code/modules/cargo/packsrogue/weapons.dm
index e0b735731..98ad7d640 100644
--- a/code/modules/cargo/packsrogue/weapons.dm
+++ b/code/modules/cargo/packsrogue/weapons.dm
@@ -94,6 +94,11 @@
/obj/item/gun/ballistic/revolver/grenadelauncher/bow,
)
+/datum/supply_pack/rogue/weapons/musket
+ name = "Musket"
+ cost = 100
+ contains = list(/obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/advanced)
+
/datum/supply_pack/rogue/weapons/quiver
name = "Quiver"
cost = 15
@@ -129,6 +134,32 @@
/obj/item/ammo_casing/caseless/rogue/arrow/iron,
)
+/datum/supply_pack/rogue/weapons/ironshots
+ name = "Iron Shots"
+ cost = 20
+ contains = list(
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ /obj/item/ammo_casing/caseless/rogue/bullet/ironshot,
+ )
+
/datum/supply_pack/rogue/weapons/quivers/arrows
name = "Quiver of Arrows"
cost = 50
@@ -147,6 +178,11 @@
/obj/item/quiver/bolts,
)
+/datum/supply_pack/rogue/weapons/shotpouch
+ name = "Loaded Shot Pouch"
+ cost = 30
+ contains = list(/obj/item/quiver/shotpouch/ironshots)
+
/*
/datum/supply_pack/rogue/weapons/Parrows
name = "Poisoned Arrows"
@@ -207,3 +243,7 @@
/obj/item/ammo_casing/caseless/rogue/bolt/poison)
*/
+/datum/supply_pack/rogue/weapons/gunpowderhorn
+ name = "Gunpowder Horn"
+ cost = 10
+ contains = list(/obj/item/gunpowderhorn)
diff --git a/code/modules/clothing/rogueclothes/quiver.dm b/code/modules/clothing/rogueclothes/quiver.dm
index 7465c32eb..8eb645791 100644
--- a/code/modules/clothing/rogueclothes/quiver.dm
+++ b/code/modules/clothing/rogueclothes/quiver.dm
@@ -128,3 +128,27 @@
var/obj/item/ammo_casing/caseless/rogue/bolt/silver/A = new()
ammo += A
update_icon()
+
+/obj/item/quiver/shotpouch
+ name = "shot pouch"
+ desc = "a bag used for carrying musket shots."
+ icon = 'icons/roguetown/weapons/guns.dmi'
+ icon_state = "shotpouch"
+ w_class = WEIGHT_CLASS_SMALL
+ slot_flags = ITEM_SLOT_HIP|ITEM_SLOT_NECK
+ max_integrity = 0
+ bloody_icon_state = "bodyblood"
+ alternate_worn_layer = UNDER_CLOAK_LAYER
+ strip_delay = 10
+ max_storage = 20
+ allowed_ammo = list(/obj/item/ammo_casing/caseless/rogue/bullet/ironshot)
+ sewrepair = TRUE
+
+/obj/item/quiver/shotpouch/update_icon()
+ return
+
+/obj/item/quiver/shotpouch/ironshots/Initialize()
+ . = ..()
+ for(var/i in 1 to max_storage)
+ var/obj/item/ammo_casing/caseless/rogue/bullet/ironshot/B = new()
+ ammo += B
diff --git a/code/modules/jobs/job_types/roguetown/mercenaries/classes/cudaomarksman.dm b/code/modules/jobs/job_types/roguetown/mercenaries/classes/cudaomarksman.dm
new file mode 100644
index 000000000..250674cfe
--- /dev/null
+++ b/code/modules/jobs/job_types/roguetown/mercenaries/classes/cudaomarksman.dm
@@ -0,0 +1,38 @@
+/datum/advclass/mercenary/cudaomarksman
+ name = "Cudao Marksman"
+ tutorial = "Well-trained and disciplined, these musketeers are reputed around the Goblet for the crushing blows and thunderous roars that their gunpowder-fuelled weapons emit. Any would-be assailant of a fort or keep would think twice when seeing firearms being mounted on the battlements by these mercenaries."
+ allowed_sexes = list(MALE, FEMALE)
+ allowed_races = RACES_ALL_KINDS
+ outfit = /datum/outfit/job/roguetown/mercenary/cudaomarksman
+ category_tags = list(CTAG_MERCENARY)
+ cmode_music = 'sound/music/combat_cudaomarksman.ogg'
+
+/datum/outfit/job/roguetown/mercenary/cudaomarksman/pre_equip(mob/living/carbon/human/H)
+ ..()
+
+ H.mind.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/combat/firearms, 4, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/combat/swords, 3, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/combat/crossbows, 3, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/combat/wrestling, 2, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/combat/unarmed, 2, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/misc/reading, 1, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/misc/riding, 2, TRUE)
+ H.mind.adjust_skillrank(/datum/skill/misc/athletics, 3, TRUE)
+ H.change_stat("perception", 3)
+ H.change_stat("endurance", 1)
+ ADD_TRAIT(H, TRAIT_MEDIUMARMOR, TRAIT_GENERIC)
+ head = /obj/item/clothing/head/roguetown/bardhat
+ shoes = /obj/item/clothing/shoes/roguetown/boots/leather
+ pants = /obj/item/clothing/under/roguetown/trou/leather
+ shirt = /obj/item/clothing/suit/roguetown/shirt/undershirt
+ belt = /obj/item/storage/belt/rogue/leather
+ armor = /obj/item/clothing/suit/roguetown/armor/plate/half/iron
+ backl = /obj/item/storage/backpack/rogue/satchel
+ beltl = /obj/item/rogueweapon/sword/iron/messer
+ backr = /obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/advanced
+ beltr = /obj/item/quiver/shotpouch/ironshots
+ neck = /obj/item/gunpowderhorn
+ gloves = /obj/item/clothing/gloves/roguetown/leather
+ backpack_contents = list(/obj/item/flashlight/flare/torch/lantern = 1, /obj/item/storage/belt/rogue/pouch/coins/poor = 1)
diff --git a/code/modules/projectiles/guns/ballistic.dm b/code/modules/projectiles/guns/ballistic.dm
index 4f7b0d29b..9ba9afef3 100644
--- a/code/modules/projectiles/guns/ballistic.dm
+++ b/code/modules/projectiles/guns/ballistic.dm
@@ -85,6 +85,7 @@
var/tac_reloads = TRUE //Snowflake mechanic no more.
///Whether the gun can be sawn off by sawing tools
var/can_be_sawn_off = FALSE
+ var/ammo_to_hand = TRUE // when we empty the gun's chamber, will our character try to grab it? useful for weapons like muskets, crossbows and bows
var/verbage = "load"
/obj/item/gun/ballistic/Initialize()
@@ -347,10 +348,16 @@
return
if(bolt_type == BOLT_TYPE_NO_BOLT)
chambered = null
+ var/to_hand = FALSE
+ if(!user.get_inactive_held_item() && ammo_to_hand) // checking this here instead of in the for loop so we're not potentially calling it multiple times, when it only needs to be checked once
+ to_hand = TRUE
var/num_unloaded = 0
for(var/obj/item/ammo_casing/CB in get_ammo_list(FALSE, TRUE))
- CB.forceMove(drop_location())
- CB.bounce_away(FALSE, NONE)
+ if(to_hand)
+ user.put_in_inactive_hand(CB)
+ else
+ CB.forceMove(drop_location())
+ CB.bounce_away(FALSE, NONE)
num_unloaded++
var/turf/T = get_turf(drop_location())
if(T && is_station_level(T.z))
diff --git a/code/modules/roguetown/roguecrafting/items.dm b/code/modules/roguetown/roguecrafting/items.dm
index d012413f7..941b9a353 100644
--- a/code/modules/roguetown/roguecrafting/items.dm
+++ b/code/modules/roguetown/roguecrafting/items.dm
@@ -537,6 +537,14 @@
/obj/item/storage/roguebag/crafted
sellprice = 4
+/datum/crafting_recipe/roguetown/shotpouch
+ name = "shot pouch"
+ result = list(/obj/item/quiver/shotpouch)
+ reqs = list(/obj/item/natural/fibers = 5,
+ /obj/item/natural/cloth = 5)
+ tools = list(/obj/item/needle)
+ skillcraft = /datum/skill/misc/sewing
+ req_table = FALSE
/datum/crafting_recipe/roguetown/bait
name = "bait (wheat)"
diff --git a/code/modules/roguetown/roguejobs/blacksmith/anvil_recipes/weapons.dm b/code/modules/roguetown/roguejobs/blacksmith/anvil_recipes/weapons.dm
index e51f452a9..d07204244 100644
--- a/code/modules/roguetown/roguejobs/blacksmith/anvil_recipes/weapons.dm
+++ b/code/modules/roguetown/roguejobs/blacksmith/anvil_recipes/weapons.dm
@@ -455,6 +455,16 @@
i_type = "Ammo"
craftdiff = 0
+// GUNS
+/datum/anvil_recipe/weapons/iron/bullets
+ name = "Iron balls x5"
+ req_bar = /obj/item/ingot/iron
+ created_item = /obj/item/ammo_casing/caseless/rogue/bullet/ironshot
+ createditem_num = 5
+ appro_skill = /datum/skill/craft/engineering
+ i_type = "Ammo"
+ craftdiff = 1
+
//Rarity
/datum/anvil_recipe/valuables/steel/execution
name = "Execution Sword (+ 1 Steel, + 1 Iron)"
diff --git a/code/modules/roguetown/roguejobs/engineer/anvil_recipes/mechanical.dm b/code/modules/roguetown/roguejobs/engineer/anvil_recipes/mechanical.dm
index 06fa9e3c0..a26fa2895 100644
--- a/code/modules/roguetown/roguejobs/engineer/anvil_recipes/mechanical.dm
+++ b/code/modules/roguetown/roguejobs/engineer/anvil_recipes/mechanical.dm
@@ -65,4 +65,24 @@
req_bar = /obj/item/ingot/bronze
additional_items = list(/obj/item/ingot/bronze, /obj/item/roguegear, /obj/item/roguegear)
created_item = /obj/item/bodypart/r_leg/prosthetic/bronze
- craftdiff = 4
\ No newline at end of file
+ craftdiff = 4
+
+// ------------ FIREARMS ----------------
+
+/datum/crafting_recipe/roguetown/gunpowderhorn
+ name = "Gunpowder Horn"
+ result = list(/obj/item/gunpowderhorn)
+ reqs = list(/obj/item/reagent_containers/glass/bottle/waterskin = 1,
+ /obj/item/roguegear = 1,
+ /obj/item/ash = 2,
+ /obj/item/rogueore/coal = 1
+ )
+ skillcraft = /datum/skill/craft/engineering
+ craftdiff = 4
+
+/datum/anvil_recipe/engineering/steel/firearms/powdergun_advanced
+ name = "Musket"
+ req_bar = /obj/item/ingot/steel
+ additional_items = list(/obj/item/grown/log/tree/small)
+ created_item = /obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/advanced
+ craftdiff = 4
diff --git a/html/changelogs/hocka-gunsgunsungs.yml b/html/changelogs/hocka-gunsgunsungs.yml
new file mode 100644
index 000000000..146d611ba
--- /dev/null
+++ b/html/changelogs/hocka-gunsgunsungs.yml
@@ -0,0 +1,15 @@
+author: "Hocka"
+
+delete-after: True
+
+changes:
+ - rscadd: "Added muskets to the game - as well as the gunpowder horn, required for priming it. Iron shots (and shot pouches to carry them in more conveniently) have of course also been added - the musketeer virtue and Cudao Marksman mercenary subclass spawn with these items. They can also be crafted, bought from the merchant, or found in the town watch's armoury."
+ - rscadd: "Musketeer Virtue - gives you a little bit of firearms skill and gives you a musket kit in your stash."
+ - rscadd: "Cudao Marksman mercenary subclass - basically a class dedicated to using the newly added musket (and other firearms down the line, most likely.)"
+ - rscadd: "Added the firearms skill - this skill impacts how quickly you pack your musket's gunpowder and how quickly you can get a perfect aim with firearms."
+ - imageadd: "Added sprites for the gunpowder horn, shot pouch and musket, made by Lancer."
+ - maptweak: "Added three muskets and the equipment needed to operate them to the garrison armoury."
+ - soundadd: "Added musket firing sound, dry firing sound, pan opening/closing sound, musket barrel ram-rodding sound. Also added combat music for the Cudao Marksman."
+ - refactor: "Fixed the colour on the 'red' font define to look a bit more appropriate compared to the rest of the game, mimicking red text from other areas."
+ - refactor: "Refactoed how gun code handles removing rounds from a gun, so that the bullet goes straight to your inactive hand (if it's empty) - this should also affect bows and crossbows!"
+ - refactor: "Added a special variety of the smoke effect specifically for powder gun blasts."
diff --git a/icons/roguetown/weapons/guns.dmi b/icons/roguetown/weapons/guns.dmi
new file mode 100644
index 000000000..ec4d4db6f
Binary files /dev/null and b/icons/roguetown/weapons/guns.dmi differ
diff --git a/modular_azurepeak/virtues/combat.dm b/modular_azurepeak/virtues/combat.dm
index ece349c20..fbce92bdf 100644
--- a/modular_azurepeak/virtues/combat.dm
+++ b/modular_azurepeak/virtues/combat.dm
@@ -142,6 +142,18 @@
added_skills = list(list(/datum/skill/misc/tracking, 1, 3))
handle_skills(recipient)
+/datum/virtue/combat/musketeer
+ name = "Musket Enthusiast"
+ desc = "Cudao's firearm technology greatly fascinates you - so much so that you managed to procure your own musket along with the tools to operate it."
+ custom_text = "Grants novice firearms skill."
+ added_stashed_items = list("Musket" = /obj/item/gun/ballistic/revolver/grenadelauncher/powdergun/advanced,
+ "Shot pouch (Iron shots)" = /obj/item/quiver/shotpouch/ironshots,
+ "Gunpowder horn" = /obj/item/gunpowderhorn)
+
+/datum/virtue/combat/musketeer/apply_to_human(mob/living/carbon/human/recipient)
+ if(recipient.mind?.get_skill_level(/datum/skill/combat/firearms) < SKILL_LEVEL_NOVICE)
+ recipient.mind?.adjust_skillrank_up_to(/datum/skill/combat/firearms, SKILL_LEVEL_NOVICE, silent = TRUE)
+
/*/datum/virtue/combat/tavern_brawler
name = "Tavern Brawler"
diff --git a/roguetown.dme b/roguetown.dme
index 928a1de23..c16260193 100644
--- a/roguetown.dme
+++ b/roguetown.dme
@@ -1229,6 +1229,7 @@
#include "code\game\objects\items\rogueweapons\ranged\ammo.dm"
#include "code\game\objects\items\rogueweapons\ranged\bows.dm"
#include "code\game\objects\items\rogueweapons\ranged\crossbows.dm"
+#include "code\game\objects\items\rogueweapons\ranged\powder_guns.dm"
#include "code\game\objects\items\rogueweapons\ranged\slings.dm"
#include "code\game\objects\items\stacks\bscrystal.dm"
#include "code\game\objects\items\stacks\cash.dm"
@@ -2264,6 +2265,7 @@
#include "code\modules\jobs\job_types\roguetown\mercenaries\mercenary.dm"
#include "code\modules\jobs\job_types\roguetown\mercenaries\classes\blackoak.dm"
#include "code\modules\jobs\job_types\roguetown\mercenaries\classes\condottiero.dm"
+#include "code\modules\jobs\job_types\roguetown\mercenaries\classes\cudaomarksman.dm"
#include "code\modules\jobs\job_types\roguetown\mercenaries\classes\desertrider.dm"
#include "code\modules\jobs\job_types\roguetown\mercenaries\classes\grenzelhoft.dm"
#include "code\modules\jobs\job_types\roguetown\mercenaries\classes\steppesman.dm"
diff --git a/sound/combat/Ranged/gunpowder-packing.ogg b/sound/combat/Ranged/gunpowder-packing.ogg
new file mode 100644
index 000000000..e2aea0239
Binary files /dev/null and b/sound/combat/Ranged/gunpowder-packing.ogg differ
diff --git a/sound/combat/Ranged/gunpowder-pouring.ogg b/sound/combat/Ranged/gunpowder-pouring.ogg
new file mode 100644
index 000000000..0d7b51b38
Binary files /dev/null and b/sound/combat/Ranged/gunpowder-pouring.ogg differ
diff --git a/sound/combat/Ranged/musket-flashpan-close.ogg b/sound/combat/Ranged/musket-flashpan-close.ogg
new file mode 100644
index 000000000..6734cfb53
Binary files /dev/null and b/sound/combat/Ranged/musket-flashpan-close.ogg differ
diff --git a/sound/combat/Ranged/musket-flashpan-open.ogg b/sound/combat/Ranged/musket-flashpan-open.ogg
new file mode 100644
index 000000000..0389fe095
Binary files /dev/null and b/sound/combat/Ranged/musket-flashpan-open.ogg differ
diff --git a/sound/combat/Ranged/musket-shot-unpowdered.ogg b/sound/combat/Ranged/musket-shot-unpowdered.ogg
new file mode 100644
index 000000000..00257dc9e
Binary files /dev/null and b/sound/combat/Ranged/musket-shot-unpowdered.ogg differ
diff --git a/sound/combat/Ranged/musket-shot.ogg b/sound/combat/Ranged/musket-shot.ogg
new file mode 100644
index 000000000..ae6ab5716
Binary files /dev/null and b/sound/combat/Ranged/musket-shot.ogg differ
diff --git a/sound/music/combat_cudaomarksman.ogg b/sound/music/combat_cudaomarksman.ogg
new file mode 100644
index 000000000..b91b252ae
Binary files /dev/null and b/sound/music/combat_cudaomarksman.ogg differ
diff --git a/sound/music/credits.txt b/sound/music/credits.txt
index c4c06beca..febd07e3e 100644
--- a/sound/music/credits.txt
+++ b/sound/music/credits.txt
@@ -38,3 +38,4 @@ shirleigh_keep.ogg: Tavern (Undead) - World of Warcraft: Classic
weeping_woods.ogg: Darkshire - World of Warcraft: Classic
combat_templar.ogg: Nebakov Siege Theme (Poverty and Famine var.) - Kingdom Come: Deliverance II
combat_yinbladesmen.ogg: Duel Theme - Ghosts of Tsushima
+combat_cudaomarksman.ogg: Battle at Arcole - Total War: NAPOLEON