## Why It's Good For The Game
Adds more funny/unique things for admins to spawn in.
## Changelog
:cl: Ryll/Shaps
add: Added the Marksman Revolver, for admins to spawn.
/:cl:
---
code/__DEFINES/traits.dm | 3 +
.../temporary_visuals/projectiles/muzzle.dm | 3 +
.../temporary_visuals/projectiles/tracer.dm | 6 +
.../projectiles/ammunition/energy/special.dm | 6 +
.../projectiles/guns/energy/special.dm | 61 ++++++
.../projectiles/projectile/bullets/special.dm | 173 ++++++++++++++++++
icons/obj/weapons/guns/projectiles_tracer.dmi | Bin 5572 -> 5564 bytes
sound/effects/coin2.ogg | Bin 0 -> 12198 bytes
8 files changed, 252 insertions(+)
create mode 100644 sound/effects/coin2.ogg
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index e0ff56dd136..4cb05789f4e 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -976,3 +976,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define LEFT_ARM_TRAIT "left_arm"
#define RIGHT_LEG_TRAIT "right_leg"
#define LEFT_LEG_TRAIT "left_leg"
+
+/// This target has recently been shot by a marksman coin and is very briefly immune to being hit by one again to prevent recursion
+#define TRAIT_RECENTLY_COINED "trait_recently_coined"
diff --git a/code/game/objects/effects/temporary_visuals/projectiles/muzzle.dm b/code/game/objects/effects/temporary_visuals/projectiles/muzzle.dm
index 9b7794bf61c..808a4f50559 100644
--- a/code/game/objects/effects/temporary_visuals/projectiles/muzzle.dm
+++ b/code/game/objects/effects/temporary_visuals/projectiles/muzzle.dm
@@ -38,3 +38,6 @@
/obj/effect/projectile/muzzle/sniper
icon_state = "sniper"
+
+/obj/effect/projectile/muzzle/bullet
+ icon_state = "muzzle_bullet"
diff --git a/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm b/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm
index ed350c25c69..14b7f43a822 100644
--- a/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm
+++ b/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm
@@ -59,6 +59,12 @@
name = "solar beam"
icon_state = "solar"
+/obj/effect/projectile/tracer/solar/thin
+ icon_state = "solar_thin"
+
+/obj/effect/projectile/tracer/solar/thinnest
+ icon_state = "solar_thinnest"
+
//BEAM RIFLE
/obj/effect/projectile/tracer/tracer/beam_rifle
icon_state = "tracer_beam"
diff --git a/code/modules/projectiles/ammunition/energy/special.dm b/code/modules/projectiles/ammunition/energy/special.dm
index 6f3ddb5fd86..bedfab7b235 100644
--- a/code/modules/projectiles/ammunition/energy/special.dm
+++ b/code/modules/projectiles/ammunition/energy/special.dm
@@ -68,3 +68,9 @@
projectile_type = /obj/projectile/beam/shrink
select_name = "shrink ray"
e_cost = 200
+
+/obj/item/ammo_casing/energy/marksman
+ projectile_type = /obj/projectile/bullet/marksman
+ select_name = "marksman nanoshot"
+ e_cost = 0
+ fire_sound = 'sound/weapons/gun/revolver/shot_alt.ogg'
diff --git a/code/modules/projectiles/guns/energy/special.dm b/code/modules/projectiles/guns/energy/special.dm
index bc362f23471..1d6a02144a1 100644
--- a/code/modules/projectiles/guns/energy/special.dm
+++ b/code/modules/projectiles/guns/energy/special.dm
@@ -375,3 +375,64 @@
/obj/item/gun/energy/tesla_cannon/Initialize(mapload)
. = ..()
AddComponent(/datum/component/automatic_fire, 0.1 SECONDS)
+
+/obj/item/gun/energy/marksman_revolver
+ name = "marksman revolver"
+ desc = "Uses electric pulses to fire microscopic pieces of metal at incredibly high speeds. Alternate fire flips a coin that can be targeted for extra firepower."
+ icon = 'icons/obj/weapons/guns/ballistic.dmi'
+ icon_state = "revolver"
+ ammo_type = list(/obj/item/ammo_casing/energy/marksman)
+ fire_sound = 'sound/weapons/gun/revolver/shot_alt.ogg'
+ automatic_charge_overlays = FALSE
+ /// How many coins we can have at a time. Set to 0 for infinite
+ var/max_coins = 4
+ /// How many coins we currently have available
+ var/coin_count = 0
+ /// How long it takes to regen a coin
+ var/coin_regen_rate = 2 SECONDS
+ /// The cooldown for regenning coins
+ COOLDOWN_DECLARE(coin_regen_cd)
+
+/obj/item/gun/energy/marksman_revolver/Initialize(mapload)
+ . = ..()
+ coin_count = max_coins
+
+/obj/item/gun/energy/marksman_revolver/examine(mob/user)
+ . = ..()
+ if(max_coins)
+ . += "It currently has [coin_count] out of [max_coins] coins, and takes [coin_regen_rate/10] seconds to recharge each one."
+ else
+ . += "It has infinite coins available for use."
+
+/obj/item/gun/energy/marksman_revolver/process(delta_time)
+ if(!max_coins || coin_count >= max_coins)
+ STOP_PROCESSING(SSobj, src)
+ return
+
+ if(COOLDOWN_FINISHED(src, coin_regen_cd))
+ if(ismob(loc))
+ var/mob/owner = loc
+ owner.playsound_local(owner, 'sound/machines/ding.ogg', 20)
+ coin_count++
+ COOLDOWN_START(src, coin_regen_cd, coin_regen_rate)
+
+/obj/item/gun/energy/marksman_revolver/afterattack_secondary(atom/target, mob/living/user, params)
+ if(!can_see(user, get_turf(target), length = 9))
+ return ..()
+
+ if(max_coins && coin_count <= 0)
+ to_chat(user, span_warning("You don't have any coins right now!"))
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+ if(max_coins)
+ START_PROCESSING(SSobj, src)
+ coin_count = max(0, coin_count - 1)
+
+ var/turf/target_turf = get_offset_target_turf(target, rand(-1, 1), rand(-1, 1)) // choose a random tile adjacent to the clicked one
+ playsound(user.loc, 'sound/effects/coin2.ogg', 50, TRUE)
+ user.visible_message(span_warning("[user] flips a coin towards [target]!"), span_danger("You flip a coin towards [target]!"))
+ var/obj/projectile/bullet/coin/new_coin = new(get_turf(user), target_turf, user)
+ new_coin.preparePixelProjectile(target_turf, user)
+ new_coin.fire()
+
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
diff --git a/code/modules/projectiles/projectile/bullets/special.dm b/code/modules/projectiles/projectile/bullets/special.dm
index 06dc98ed84d..09a0fbf3bc4 100644
--- a/code/modules/projectiles/projectile/bullets/special.dm
+++ b/code/modules/projectiles/projectile/bullets/special.dm
@@ -38,3 +38,176 @@
var/mob/living/living_target = target
living_target.set_silence_if_lower(20 SECONDS)
+
+
+// Marksman Revolver + Ricochet Coin
+
+/// Marksman Shot
+/obj/projectile/bullet/marksman
+ name = "marksman nanoshot"
+ hitscan = TRUE
+ damage = 30
+ tracer_type = /obj/effect/projectile/tracer/solar
+ muzzle_type = /obj/effect/projectile/muzzle/bullet
+ impact_type = /obj/effect/projectile/impact/sniper
+ /// How many ricochets deep this is, for tracer size
+ var/ricoshot_level = 0
+
+/obj/projectile/bullet/marksman/Initialize(mapload, obj/item/ammo_casing/casing, incoming_ricoshot_level)
+ . = ..()
+ if(isnum(incoming_ricoshot_level))
+ ricoshot_level = incoming_ricoshot_level
+
+ switch(ricoshot_level)
+ if(0)
+ tracer_type = /obj/effect/projectile/tracer/solar/thinnest
+ if(1)
+ tracer_type = /obj/effect/projectile/tracer/solar/thin
+ if(2 to INFINITY)
+ tracer_type = /obj/effect/projectile/tracer/solar
+
+/obj/projectile/bullet/marksman/scan_moved_turf()
+ var/turf/cur_turf = get_turf(src) // check to see if we're passing over a turf with a coin on it
+ var/obj/projectile/bullet/coin/coin_check = cur_turf ? locate(/obj/projectile/bullet/coin) in cur_turf.contents : null
+
+ if(!coin_check || (ricoshot_level == 0 && get_dist(coin_check.target_turf, coin_check) >= 1) || coin_check.used) // no coin, keep trucking
+ return ..()
+
+ coin_check.check_splitshot(firer, src)
+ Impact(coin_check)
+
+/// Marksman Coin
+/obj/projectile/bullet/coin
+ name = "marksman coin"
+ icon_state = "coinshot"
+ pixel_speed_multiplier = 0.333
+ speed = 1
+ damage = 5
+ color = "#dbdd4c"
+
+ /// Save the turf we're aiming for for future use
+ var/turf/target_turf
+ /// Coins are valid while within a tile of their target tile, and can only be directly ricoshot during this time.
+ var/valid = FALSE
+ /// When a coin has been activated, is is marked as used, so that it is taken out of consideration for any further ricochets
+ var/used = FALSE
+ /// When this coin is targeted with a valid splitshot, it creates this many child splitshots
+ var/num_of_splitshots = 2
+ /// The crosshair icon put on the targeted turf for the user- so we can remove it from their images when done
+ var/image/crosshair_indicator
+ /// The mob who originally flipped this coin, as a matter of convenience, may be able tto be removed
+ var/mob/original_firer
+
+/obj/projectile/bullet/coin/Initialize(mapload, turf/the_target, mob/original_firer)
+ src.original_firer = original_firer
+ target_turf = the_target
+ range = (get_dist(original_firer, target_turf) + 3) * 3 // 3 tiles past the origin (the *3 is because Range() ticks 3 times a tile because of the slower speed)
+
+ . = ..()
+
+ if(!istype(original_firer) || !original_firer.client)
+ return
+
+ var/client/firing_client = original_firer.client
+ crosshair_indicator = image('icons/obj/supplypods_32x32.dmi', target_turf, "LZ")
+ firing_client.images += crosshair_indicator
+
+/obj/projectile/bullet/coin/Destroy()
+ remove_crosshair_indicator()
+ return ..()
+
+// the coin must be on the target turf to be directly targetable
+/obj/projectile/bullet/coin/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+ if(!valid && get_dist(loc, target_turf) < 1)
+ original_firer?.playsound_local(src, 'sound/machines/ping.ogg', 30)
+ valid = TRUE
+ else if(valid && get_dist(loc, target_turf) > 1)
+ valid = FALSE
+ remove_crosshair_indicator()
+
+/// Remove the crosshair indicator from the original firer if it exists
+/obj/projectile/bullet/coin/proc/remove_crosshair_indicator()
+ if(original_firer?.client && crosshair_indicator)
+ original_firer.client.images -= crosshair_indicator
+ QDEL_NULL(crosshair_indicator)
+
+/// We've been shot by a marksman revolver shot, or the ricochet off another coin, check if we can actually ricochet. The forced var being TRUE means it's a ricochet from another coin
+/obj/projectile/bullet/coin/proc/check_splitshot(mob/living/shooter, obj/projectile/bullet/marksman/incoming_shot, forced = FALSE)
+ if(!forced && get_dist(src, target_turf) > 1)
+ return FALSE
+
+ used = TRUE
+ var/turf/cur_tur = get_turf(src)
+ cur_tur.visible_message(span_nicegreen("[incoming_shot] impacts [src] and splits!"))
+ iterate_splitshots(shooter, incoming_shot)
+ QDEL_IN(src, 0.25 SECONDS) // may not be needed
+
+/// Now we actually create all the splitshots, loop through however many splits we'll create and fire them
+/obj/projectile/bullet/coin/proc/iterate_splitshots(mob/living/shooter, obj/projectile/incoming_shot)
+ for(var/i in 1 to num_of_splitshots)
+ fire_splitshot(incoming_shot)
+
+/// Shoot an individual splitshot at a new target
+/obj/projectile/bullet/coin/proc/fire_splitshot(obj/projectile/bullet/marksman/incoming_shot)
+ var/atom/next_target = find_next_target()
+
+ ADD_TRAIT(next_target, TRAIT_RECENTLY_COINED, "[type]")
+ addtimer(TRAIT_CALLBACK_REMOVE(next_target, TRAIT_RECENTLY_COINED, "[type]"), 0.5 SECONDS)
+ var/outgoing_ricoshot_level = incoming_shot.ricoshot_level + 1
+ var/obj/projectile/bullet/marksman/new_splitshot = new /obj/projectile/bullet/marksman(get_turf(src), null, outgoing_ricoshot_level)
+ //Shooting Code:
+ new_splitshot.original = next_target
+ new_splitshot.fired_from = incoming_shot.fired_from
+ new_splitshot.firer = incoming_shot.firer
+ new_splitshot.damage = 2 * incoming_shot.damage
+
+ var/current_turf = get_turf(src)
+ var/target_turf = get_turf(next_target)
+
+ if(Adjacent(current_turf, target_turf))
+ new_splitshot.fire(get_angle(current_turf, target_turf), direct_target = next_target)
+ else
+ new_splitshot.preparePixelProjectile(next_target, get_turf(src))
+ new_splitshot.fire()
+
+ if(istype(next_target, /obj/projectile/bullet/coin)) // handle further splitshot checks
+ var/obj/projectile/bullet/coin/our_coin = next_target
+ our_coin.check_splitshot(incoming_shot.firer, new_splitshot, forced = TRUE)
+
+/// Find what the splitshots will want to target next, with the order roughly based off the UK coin
+/obj/projectile/bullet/coin/proc/find_next_target()
+ var/list/valid_targets = shuffle(oview(4, loc))
+ valid_targets -= firer
+
+ for(var/obj/projectile/bullet/coin/iter_coin in valid_targets)
+ if(!iter_coin.used) // this will prevent shooting itself as well
+ return iter_coin
+
+ var/list/possible_victims = list()
+
+ for(var/mob/living/iter_living in valid_targets)
+ if(HAS_TRAIT(iter_living, TRAIT_RECENTLY_COINED) || iter_living.stat != CONSCIOUS)
+ continue
+
+ if(get_dist(src, iter_living) <= 2) // prioritize close mobs
+ return iter_living
+ possible_victims += iter_living
+
+ if(possible_victims.len)
+ return pick(possible_victims)
+
+ var/list/static/prioritized_targets = list(/obj/structure/reagent_dispensers, /obj/item/grenade, /obj/structure/window)
+ for(var/iter_type in prioritized_targets)
+ for(var/already_coined_tries in 1 to 3)
+ var/atom/iter_type_check = locate(iter_type) in valid_targets
+ if(iter_type_check)
+ if(HAS_TRAIT(iter_type_check, TRAIT_RECENTLY_COINED))
+ valid_targets -= iter_type_check
+ continue
+ else
+ return iter_type_check
+
+ for(var/atom/last_ditch in valid_targets)
+ if(!HAS_TRAIT(last_ditch, TRAIT_RECENTLY_COINED))
+ return last_ditch
diff --git a/icons/obj/weapons/guns/projectiles_tracer.dmi b/icons/obj/weapons/guns/projectiles_tracer.dmi
index b4edd672ac9faa854b511bfa41b0d7fd414aa5ef..b66fa7b7aaf969929721fa6513e18fa0468a62e9 100644
GIT binary patch
literal 5564
zcmbVQbyQSuw>~pNia1C}hlq4{I;6mmq5?l!8cAswV(5^TMkGWKDG8-vkZuHF$U&qV
zX&B<7-@X5QYu&rncmFv1>~r4zKI@!kpS|~4Pn5Q%GVv{%TL1tMtEwpK+?-K2M=k;0
zjWuAmFa`h+q3<(&cSS2V3s*ZAcROb%0Pubro7DfZou5PrJ*s!RS2VeycOvT-Ns~%|
zT+DXV!;yB|CGjgPFIaf6chA11jHwfm0~HTJaI7Z#sJcivW2Aja}>
z2xmMg>JA}e4W@;QTZ}1svW_Z4$%e!n$~G-gQhQfc+`R`w-Zcik!Rl-qg!E
z$~pY@KIvDO;8i?CKlZDeFLrWmK`(8h%(>#@>Fv+tA1~#UmP3|0wZH9+CW7_g>iF(N
z?u{I^zxpN)`|_?fW=D5*FXwf8bcr&`{jGb4ckadL5$W7TC4}Bf`As qb1uDQz4Sz-lKaKsqyr=spO1auhp$_&Mb!
zhZu&(;|JY{%{MeN=Jwr4dRfzC-o;|{Ojgcbxm)SnA)mKI`ea?`vN%$U!tSyHIkB2f
zd9dK!wF9SpnQ`Ew)e~%=)|EpOV!2$PYfPi-8{OxO48+fJWV5RvzS3XIm;X53K4p{F
z0Eqeo=jlYNN1Ztm^$p09-S;h(L@ooBXNSeS$_RUWpE60D6ybmRtZ+GA(BOx}U80NM
zd-9Pg0FL;uP=wOIYZVlZm7#*B_AdJFK=Nx+Y+HTTyzY9rbuZvvVNQj0tk5AvrRxU6
zJdO!d2V23#jA@}4hn0-2