Adds the Marksman Revolver from Ultrakill, with coinflipping and splitshotting! (#70612)

## About The Pull Request
Ultrakill is a neat game that I like, and I like to add fun things from
things I like. This PR adds the marksman revolver as an admin-only
weapon, complete with coin flipping and splitshotting mechanics! [Have a
video demonstration!](https://streamable.com/5x4mnj)

Basically, right clicking on a visible tile flips a coin towards a
random adjacent tile to the one you targeted. Shooting the coin
projectile with the hitscan primary fire while it's within one tile of
the target will trigger a splitshot. Those two child splitshots will
roughly follow the priority order of Ultrakill (prioritizing coins [for
even more splits!], then nearby mobs, then less nearby mobs, then other
random stuff nearby). I plan to add other stuff for the shots to
prioritize like fuel tanks, windows, and others.

I'm still working out some of the finer details of balance and more
closely matching how it works to UK (while still working in the realm of
SS13), and of course I hope to get unique sprites/sounds/whatever.

<img src='https://imgur.com/XUTqksD.png'>

## Why It's Good For The Game
Adds more funny/unique things for admins to spawn in.

## Changelog

🆑 Ryll/Shaps
add: Added the Marksman Revolver, for admins to spawn.
/🆑
This commit is contained in:
Ryll Ryll
2022-11-21 23:21:18 -08:00
committed by GitHub
parent 4331bd6763
commit f55aafcc68
8 changed files with 252 additions and 0 deletions
+3
View File
@@ -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"
@@ -38,3 +38,6 @@
/obj/effect/projectile/muzzle/sniper
icon_state = "sniper"
/obj/effect/projectile/muzzle/bullet
icon_state = "muzzle_bullet"
@@ -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"
@@ -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'
@@ -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
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.