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 b4edd672ac9..b66fa7b7aaf 100644 Binary files a/icons/obj/weapons/guns/projectiles_tracer.dmi and b/icons/obj/weapons/guns/projectiles_tracer.dmi differ diff --git a/sound/effects/coin2.ogg b/sound/effects/coin2.ogg new file mode 100644 index 00000000000..e746bb4b591 Binary files /dev/null and b/sound/effects/coin2.ogg differ