mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-22 20:48:56 +01:00
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:
@@ -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.
Reference in New Issue
Block a user