NT researchers make shocking breakthrough in flux anomalogics! Tesla Cannon Resprite / Resound (#92031)

## About The Pull Request

![tesla_card](https://github.com/user-attachments/assets/27d0777b-3014-4785-b06e-f5e7fe0d5a36)

Resprites:

Tesla Cannon 
Tesla Cannon crafting kit

### New SFX / VFX

The tesla cannon now uses a new type of beam effect that randomly picks
sprite variants for each segment instead of a tracer.

This makes the arc look more dynamic and less distorted. 

Autofire guns can now choose to use a looping sound datum when firing.


![image](https://github.com/user-attachments/assets/b9c0c494-fce6-48bc-9d09-ea2e6257c86c)

#### Balance changes

The tesla cannon must now have its stock unfolded before firing, this
takes 1.5 seconds and makes the gun bulky.

It is still normal sized when folded, and folding it is instant.

### Bug fixes

Fixed a bug where looping_sound.stop() would fail to stop sounds.

The tesla cannon is an incredibly powerfu 

## Why It's Good For The Game

### My reasons for respriting

The old sprite was not bad, by all means but I had a few gripes with it.

* The old sprite does not incorporate the flux anomaly yellow colour.

* The old sprite looks a bit much like a real, professionally produced
sci-fi weapon,

* The old sprite looks pretty small for such a ultra high dps full auto
weapon.

* The old inhand is quite indistinct for something that can game end you
in like one second.

### My design

I think that anomaly items should be very mad science coded and, since
anomaly science is by definition a poorly studied field, they should
look more like prototypes created by a scientist rather than something
professionally made in a factory.

## Changelog

🆑
image: The tesla cannon has new sprites.
image: The tesla parts kit has new sprites.
image: The tesla cannon has a new shocking beam effect when firing.
sound: The tesla cannon has new sounds.
balance: The tesla cannon must now be unfolded to fire.
fix: looping sounds now stop playing sounds when commaned to do so.
/🆑
This commit is contained in:
Krysonism
2025-08-12 23:43:28 +02:00
committed by nevimer
parent c47e3412b4
commit 17006b220f
22 changed files with 171 additions and 36 deletions

View File

@@ -18,7 +18,7 @@
base_pixel_z = z;
#define _SET_BASE_PIXEL_VISUAL_NO_OFFSET(w, z) \
base_pixel_z = w; \
base_pixel_w = w; \
base_pixel_z = z;
/// Much like [SET_BASE_PIXEL], except it will not effect pixel offsets in mapping programs

View File

@@ -80,13 +80,7 @@
*/
/datum/beam/proc/Start()
visuals = new beam_type()
visuals.icon = icon
visuals.icon_state = icon_state
visuals.color = beam_color
visuals.vis_flags = VIS_INHERIT_PLANE|VIS_INHERIT_LAYER
visuals.emissive = emissive
visuals.layer = beam_layer
visuals.update_appearance()
set_up_effect(visuals, icon_state)
Draw()
RegisterSignals(origin, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING), PROC_REF(redrawing))
RegisterSignals(target, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING), PROC_REF(redrawing))
@@ -146,15 +140,14 @@
var/obj/effect/ebeam/segment = new beam_type(origin_turf, src)
elements += segment
//Assign our single visual ebeam to each ebeam's vis_contents
//ends are cropped by a transparent box icon of length-N pixel size laid over the visuals obj
if(N+32>length) //went past the target, we draw a box of space to cut away from the beam sprite so the icon actually ends at the center of the target sprite
var/icon/II = new(icon, icon_state)//this means we exclude the overshooting object from the visual contents which does mean those visuals don't show up for the final bit of the beam...
II.DrawBox(null,1,(length-N),32,32)//in the future if you want to improve this, remove the drawbox and instead use a 513 filter to cut away at the final object's icon
segment.icon = II
var/icon/terminal_icon = new(icon, icon_state)//this means we exclude the overshooting object from the visual contents which does mean those visuals don't show up for the final bit of the beam...
terminal_icon.DrawBox(null,1,(length-N),32,32)//in the future if you want to improve this, remove the drawbox and instead use a 513 filter to cut away at the final object's icon
segment.icon = terminal_icon
segment.color = beam_color
else
segment.vis_contents += visuals
set_subsegment_appearance(segment)
segment.transform = rot_matrix
//Calculate pixel offsets (If necessary)
@@ -184,6 +177,50 @@
segment.pixel_y = origin_py + Pixel_y
CHECK_TICK
/datum/beam/proc/set_up_effect(obj/effect/ebeam/beam_effect, effect_icon_state)
beam_effect.icon = icon
beam_effect.icon_state = effect_icon_state
beam_effect.color = beam_color
beam_effect.vis_flags = VIS_INHERIT_PLANE|VIS_INHERIT_LAYER
beam_effect.emissive = emissive
beam_effect.layer = beam_layer
beam_effect.update_appearance()
///sets the sprite of the segment, using the more performant viscontents by default
/datum/beam/proc/set_subsegment_appearance(obj/effect/ebeam/segment)
//Assign our single visual ebeam to each ebeam's vis_contents
segment.vis_contents += visuals
//for when you don't want each segment to look identital
/datum/beam/varied
//how many variants do we have in addition to the unnumbered state we use as a base icon state and terminal segment
var/icon_state_variants = 1
/datum/beam/varied/New(
origin,
target,
icon = 'icons/effects/beam.dmi',
icon_state = "b_beam",
time = INFINITY,
max_distance = INFINITY,
beam_type = /obj/effect/ebeam,
beam_color = null,
emissive = TRUE,
override_origin_pixel_x = null,
override_origin_pixel_y = null,
override_target_pixel_x = null,
override_target_pixel_y = null,
beam_layer = ABOVE_ALL_MOB_LAYER,
icon_state_variants = 1
)
. = ..()
src.icon_state_variants = icon_state_variants
/datum/beam/varied/set_subsegment_appearance(obj/effect/ebeam/segment)
//we use reall ass icon states here.
set_up_effect(segment, "[icon_state][rand(1, icon_state_variants)]")
/obj/effect/ebeam
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
layer = ABOVE_ALL_MOB_LAYER
@@ -298,8 +335,14 @@
override_origin_pixel_y = null,
override_target_pixel_x = null,
override_target_pixel_y = null,
layer = ABOVE_ALL_MOB_LAYER
layer = ABOVE_ALL_MOB_LAYER,
icon_state_variants = 0,
)
var/datum/beam/newbeam = new(src,BeamTarget,icon,icon_state,time,maxdistance,beam_type, beam_color, emissive, override_origin_pixel_x, override_origin_pixel_y, override_target_pixel_x, override_target_pixel_y, layer)
var/datum/beam/newbeam
if(icon_state_variants <= 0)
newbeam = new(src,BeamTarget,icon,icon_state,time,maxdistance,beam_type, beam_color, emissive, override_origin_pixel_x, override_origin_pixel_y, override_target_pixel_x, override_target_pixel_y, layer)
else
newbeam = new /datum/beam/varied(src,BeamTarget,icon,icon_state,time,maxdistance,beam_type, beam_color, emissive, override_origin_pixel_x, override_origin_pixel_y, override_target_pixel_x, override_target_pixel_y, layer, icon_state_variants)
INVOKE_ASYNC(newbeam, TYPE_PROC_REF(/datum/beam/, Start))
return newbeam

View File

@@ -70,6 +70,7 @@
/obj/item/weaponcrafting/gunkit/tesla
name = "tesla cannon parts kit (lethal)"
desc = "A suitcase containing the necessary gun parts to construct a tesla cannon around a stabilized flux anomaly. Handle with care."
icon_state = "weaponskit_tesla"
/obj/item/weaponcrafting/gunkit/xray
name = "x-ray laser gun parts kit (lethal)"

View File

@@ -28,9 +28,12 @@
var/windup_spindown = 3 SECONDS
///Timer for tracking the spindown reset timings
var/timerid
///Looping sound while firing.
var/datum/looping_sound/autofire_sound_loop
COOLDOWN_DECLARE(next_shot_cd)
/datum/component/automatic_fire/Initialize(autofire_shot_delay, windup_autofire, windup_autofire_reduction_multiplier, windup_autofire_cap, windup_spindown, allow_akimbo = TRUE)
/datum/component/automatic_fire/Initialize(autofire_shot_delay, windup_autofire, windup_autofire_reduction_multiplier, windup_autofire_cap, windup_spindown, allow_akimbo = TRUE, firing_sound_loop)
. = ..()
if(!isgun(parent))
return COMPONENT_INCOMPATIBLE
@@ -48,8 +51,12 @@
var/mob/user = gun.loc
wake_up(src, user)
if(firing_sound_loop)
autofire_sound_loop = new firing_sound_loop(parent)
/datum/component/automatic_fire/Destroy()
QDEL_NULL(autofire_sound_loop)
autofire_off()
return ..()
@@ -191,6 +198,8 @@
START_PROCESSING(SSprojectiles, src)
RegisterSignal(clicker, COMSIG_CLIENT_MOUSEDRAG, PROC_REF(on_mouse_drag))
if(autofire_sound_loop)
autofire_sound_loop.start(shooter)
/datum/component/automatic_fire/proc/on_mouse_up(datum/source, atom/object, turf/location, control, params)
SIGNAL_HANDLER
@@ -217,6 +226,9 @@
target_loc = null
mouse_parameters = null
if(autofire_sound_loop)
autofire_sound_loop.stop()
/datum/component/automatic_fire/proc/on_mouse_drag(client/source, atom/src_object, atom/over_object, turf/src_location, turf/over_location, src_control, over_control, params)
SIGNAL_HANDLER
if(isnull(over_location)) //This happens when the mouse is over an inventory or screen object, or on entering deep darkness, for example.
@@ -238,7 +250,6 @@
target_loc = get_turf(over_object)
mouse_parameters = params
/datum/component/automatic_fire/proc/process_shot()
if(autofire_stat != AUTOFIRE_STAT_FIRING)
return FALSE

View File

@@ -63,6 +63,10 @@
var/direct
/// Sound channel to play on, random if not provided
var/sound_channel
///If we want to reserve a random channel when we start playing sounds. Good for when there could be several sources of the same looping sound heard by the same
var/reserve_random_channel = FALSE
//If we reserve a random sound channel, store the channel number here so we can clean it up later.
var/reserved_channel
/datum/looping_sound/New(_parent, start_immediately = FALSE, _direct = FALSE, _skip_starting_sounds = FALSE)
if(!mid_sounds)
@@ -91,6 +95,11 @@
set_parent(on_behalf_of)
if(timer_id)
return
if(!sound_channel && reserve_random_channel)
sound_channel = SSsounds.reserve_sound_channel_datumless()
reserved_channel = sound_channel
on_start()
/**
@@ -110,6 +119,11 @@
timer_id = null
loop_started = FALSE
if(reserved_channel)
sound_channel = null
SSsounds.free_sound_channel(reserved_channel)
/// The proc that handles starting the actual core sound loop.
/datum/looping_sound/proc/start_sound_loop()
loop_started = TRUE
@@ -165,7 +179,8 @@
pressure_affected = pressure_affected,
ignore_walls = ignore_walls,
falloff_distance = falloff_distance,
use_reverb = use_reverb
use_reverb = use_reverb,
channel = sound_channel || SSsounds.random_available_channel()
)
/// Returns the sound we should now be playing.

View File

@@ -47,3 +47,15 @@
/datum/looping_sound/zipline
mid_sounds = list('sound/items/weapons/zipline_mid.ogg' = 1)
volume = 5
/datum/looping_sound/tesla_cannon
start_sound = list('sound/items/weapons/gun/tesla/tesla_start.ogg' = 1)
start_volume = 100
start_length = 200 MILLISECONDS
mid_sounds = list('sound/items/weapons/gun/tesla/tesla_loop.ogg' = 1)
mid_length = 3.8 SECONDS
volume = 100
end_sound = list('sound/items/weapons/gun/tesla/power_breaker_fan.ogg' = 1)
end_volume = 15
ignore_walls = FALSE
reserve_random_channel = TRUE

View File

@@ -92,15 +92,3 @@
/obj/effect/projectile/tracer/sniper
icon_state = "sniper"
/obj/effect/projectile/tracer/lightning
icon = 'icons/effects/beam.dmi'
icon_state = "lightning2"
/obj/effect/projectile/tracer/lightning/Initialize(mapload)
. = ..()
update_appearance()
/obj/effect/projectile/tracer/lightning/update_icon_state()
. = ..()
icon_state = "lightning[rand(1, 12)]"

View File

@@ -579,6 +579,9 @@
return FALSE
if(!(flags & SHOCK_ILLUSION))
adjustFireLoss(shock_damage)
if(getFireLoss() > 100)
add_shared_particles(/particles/smoke/burning)
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom/movable, remove_shared_particles), /particles/smoke/burning), 10 SECONDS)
else
adjustStaminaLoss(shock_damage)
if(!(flags & SHOCK_SUPPRESS_MESSAGE))

View File

@@ -63,11 +63,11 @@
harmful = FALSE
/obj/item/ammo_casing/energy/tesla_cannon
fire_sound = 'sound/effects/magic/lightningshock.ogg'
fire_sound = null
e_cost = LASER_SHOTS(33, STANDARD_CELL_CHARGE)
select_name = "shock"
projectile_type = /obj/projectile/energy/tesla_cannon
firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/blue
firing_effect_type = null
/obj/item/ammo_casing/energy/shrink
projectile_type = /obj/projectile/magic/shrink/alien

View File

@@ -360,18 +360,72 @@
return FALSE
return ..()
/**
-----------------Tesla Cannon--------------------------------
An advanced weapon that provides extremely high dps output at pinpoint accuracy due to its hitscan nature.
Due to its normal w_class when folded it is suitable as a heavy reinforcement weapon, since the cell drains very quickly when firing.
The power level is somewhat tempered by several drawbacks such as research requirements, anomalock, two handed firing requirement, and insultation providing damage reduction.
it is often confused with the mech weapon of the same name, since it is a bit more obscure despite being very powerful. Formerly called the tesla revolver.
**/
/obj/item/gun/energy/tesla_cannon
name = "tesla cannon"
icon = 'icons/obj/weapons/guns/wide_guns.dmi'
icon_state = "tesla"
inhand_icon_state = "tesla"
desc = "A gun powered by a flux anomaly that shoots lightning bolts. Electrically insulating clothing may protect from some of the damage."
lefthand_file = 'icons/mob/inhands/weapons/64x_guns_left.dmi'
righthand_file = 'icons/mob/inhands/weapons/64x_guns_right.dmi'
inhand_icon_state = null //null so we build the correct inhand.
desc = "A high voltage flux projector prototype created using the latest advancements in the anomaly science.\n\nThe anomalous nature of the flux core allows the tesla arc to be guided from the electrode to the target without being diverted to stray conductors outside the target field."
SET_BASE_VISUAL_PIXEL(-8, 0)
ammo_type = list(/obj/item/ammo_casing/energy/tesla_cannon)
inhand_x_dimension = 64
shaded_charge = TRUE
charge_sections = 2
display_empty = FALSE
weapon_weight = WEAPON_HEAVY
w_class = WEIGHT_CLASS_BULKY
///if our stpck is extended and we are ready to fire.
var/ready_to_fire = FALSE
/obj/item/gun/energy/tesla_cannon/Initialize(mapload)
. = ..()
AddComponent(/datum/component/automatic_fire, 0.1 SECONDS)
AddComponent(/datum/component/automatic_fire, autofire_shot_delay = 100 MILLISECONDS, firing_sound_loop = /datum/looping_sound/tesla_cannon)
/obj/item/gun/energy/tesla_cannon/can_trigger_gun(mob/living/user, akimbo_usage)
if(ready_to_fire)
return ..()
//If we have charge, but the stock is folded, do sparks.
if(can_shoot())
balloon_alert(user, "electricity arcing to stock!")
if(prob(75)) //fake sparks to cut on spark spam
playsound(user, 'sound/effects/sparks/sparks1.ogg', 50, TRUE)
else
do_sparks(3, FALSE, user)
return FALSE
/obj/item/gun/energy/tesla_cannon/attack_self(mob/living/user)
. = ..()
if(ready_to_fire)
w_class = WEIGHT_CLASS_NORMAL
ready_to_fire = FALSE
icon_state = "tesla"
playsound(user, 'sound/items/weapons/gun/tesla/squeak_latch.ogg', 100)
else
playsound(user, 'sound/items/weapons/gun/tesla/click_creak.ogg', 100)
if(!do_after(user, 1.5 SECONDS))
return
w_class = WEIGHT_CLASS_BULKY
ready_to_fire = TRUE
icon_state = "tesla_unfolded"
playsound(user, 'sound/items/weapons/gun/tesla/squeak_latch.ogg', 100)
update_appearance()
balloon_alert_to_viewers("[ready_to_fire ? "unfolded" : "folded"] stock")
/obj/item/gun/energy/marksman_revolver
name = "marksman revolver"

View File

@@ -29,13 +29,14 @@
name = "tesla bolt"
icon_state = null
hitscan = TRUE
tracer_type = /obj/effect/projectile/tracer/lightning
impact_effect_type = null
damage = 5
var/shock_damage = 10
/obj/projectile/energy/tesla_cannon/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
firer.Beam(target, icon_state = "tesla", time = 1, icon_state_variants = 24)
if(isliving(target))
var/mob/living/victim = target
victim.electrocute_act(shock_damage, src, siemens_coeff = 1, flags = SHOCK_NOSTUN|SHOCK_TESLA)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 928 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,7 @@
{
tesla_loop.ogg - https://freesound.org/people/Department64/sounds/554749/, License: CC BY 4.0
tesla_start.ogg - https://freesound.org/people/Department64/sounds/554749/, https://freesound.org/people/iainmccurdy/sounds/759257/, License: CC BY 4.0
click_creak.ogg - https://freesound.org/people/Sonicquinn/sounds/435834/, https://freesound.org/people/holisoysilvi/sounds/610182/, License: CC 0
squeak_latch.ogg - https://freesound.org/people/Sonicquinn/sounds/435834/, https://freesound.org/people/holisoysilvi/sounds/610182/, License: CC 0
power_breaker_fan.ogg - https://freesound.org/people/Yoyodaman234/sounds/183544/, https://freesound.org/people/jessepash/sounds/139970/, https://freesound.org/people/sribubba/sounds/130154/, License: CC 0
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.