Improved Cain & Abel wisp animations (#93294)

## About The Pull Request
 
Cleaned up C&A code and made wisps orbit the user instead of being
positioned around them at static offsets (also firing now removes them
one by one as they're fired)


https://github.com/user-attachments/assets/18a45b44-739e-4d7f-9269-98c84f5342e7

The animate code is mildly cursed because BYOND has... quirks, when it
comes to looping tag animations, so we need to explicitly stop the
animation, and cannot use a blank frame (time = 0) to start it (and
SINE_EASING just looks better despite not being correct)

## Why It's Good For The Game

Looks prettier

## Changelog
🆑
image: Cain & Abel wisps now orbit around the user instead of being
static
code: Cleaned up Cain & Abel code
/🆑
This commit is contained in:
SmArtKar
2025-10-06 11:01:21 +02:00
committed by GitHub
parent b0f8cb6fc8
commit c1b3e56ede
2 changed files with 84 additions and 41 deletions
@@ -24,39 +24,40 @@
reach = 2
attack_icon = 'icons/effects/effects.dmi'
attack_icon_state = "cain_abel_attack"
///our current combo count
/// Our current combo count
var/combo_count = 0
///the maximum combo we can reach
/// The maximum combo we can reach
var/max_combo = 6
///percentage boost we get on every combo
/// Percentage boost we get on every combo
var/damage_boost = 1.15
///pixel offsets of our wisps
var/static/list/wisp_offsets = list(
list(9, 12),
list(14, 0),
list(9, -12),
list(-9, 12),
list(-14, 0),
list(-9, -12),
)
///what throw mode we're using
/// Animation positions used by wisps
var/static/list/animation_steps
/// What throw mode we're using
var/throw_mode = THROW_MODE_CRYSTALS
///flames we have up!
/// Flames we have up!
var/list/current_wisps = list()
///cooldown till we can throw blades again
/// Cooldown till we can throw blades again
COOLDOWN_DECLARE(throw_cooldown)
/obj/item/cain_and_abel/Initialize(mapload)
. = ..()
AddComponent(/datum/component/two_handed, require_twohands = TRUE, force_unwielded = force, force_wielded = force)
if (length(animation_steps))
return
animation_steps = list(
new /datum/abel_wisp_frame(-18, -2, MOB_LAYER, EASE_OUT, EASE_IN),
new /datum/abel_wisp_frame(-6, -6, ABOVE_MOB_LAYER, EASE_IN, EASE_OUT),
new /datum/abel_wisp_frame(6, -6, ABOVE_MOB_LAYER, EASE_IN, EASE_OUT),
new /datum/abel_wisp_frame(18, -2, MOB_LAYER, EASE_OUT, EASE_IN),
new /datum/abel_wisp_frame(6, 2, BELOW_MOB_LAYER, EASE_IN, EASE_OUT),
new /datum/abel_wisp_frame(-6, 2, BELOW_MOB_LAYER, EASE_IN, EASE_OUT),
)
/obj/item/cain_and_abel/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
. = ..()
if(!isliving(old_loc))
return
unset_user(old_loc)
if(isliving(old_loc))
unset_user(old_loc)
/obj/item/cain_and_abel/proc/unset_user(mob/living/source)
if(HAS_TRAIT(source, TRAIT_RELAYING_ATTACKER))
@@ -65,7 +66,6 @@
set_combo(new_value = 0, user = source)
UnregisterSignal(source, list(COMSIG_ATOM_WAS_ATTACKED))
/obj/item/cain_and_abel/equipped(mob/user, slot)
. = ..()
@@ -92,8 +92,6 @@
for(var/index in 0 to (length(current_wisps) - 1))
addtimer(CALLBACK(src, PROC_REF(fire_wisp), user, interacting_with), index * 0.15 SECONDS)
set_combo(new_value = 0, user = user)
return ITEM_INTERACT_SUCCESS
/obj/item/cain_and_abel/attack(mob/living/target, mob/living/carbon/human/user)
@@ -117,29 +115,55 @@
user.balloon_alert(user, "crystals [throw_mode == THROW_MODE_CRYSTALS ? "activated" : "deactivated"]")
return TRUE
/obj/item/cain_and_abel/proc/set_combo(new_value, mob/living/user)
combo_count = (new_value <= max_combo) ? new_value : 0
handle_wisps(user)
/obj/item/cain_and_abel/proc/set_combo(new_value, mob/living/user, instant = FALSE)
new_value = clamp(new_value, 0, max_combo)
if (new_value == combo_count)
return
/obj/item/cain_and_abel/proc/handle_wisps(mob/living/user)
var/should_remove = length(current_wisps) > combo_count
var/wisps_to_alter = abs(combo_count - length(current_wisps))
for(var/i = 1, i <= wisps_to_alter, i++)
if(!should_remove)
if (new_value > combo_count)
for (var/i in 1 to new_value - combo_count)
add_wisp(user)
continue
var/obj/my_wisp = current_wisps[i]
remove_wisp(my_wisp)
else
for (var/i in 1 to combo_count - new_value)
remove_wisp(current_wisps[i], instant)
combo_count = new_value
/obj/item/cain_and_abel/proc/add_wisp(mob/living/user)
var/obj/effect/overlay/blood_wisp/new_wisp = new(src)
current_wisps += new_wisp
var/list/position = wisp_offsets[length(current_wisps)]
user.vis_contents += new_wisp
new_wisp.pixel_x = position[1]
new_wisp.pixel_y = position[2]
RegisterSignal(new_wisp, COMSIG_QDELETING, PROC_REF(on_wisp_delete))
for (var/wisp_index in 1 to length(current_wisps))
var/obj/effect/overlay/blood_wisp/wisp = current_wisps[wisp_index]
var/spawn_index = floor(length(animation_steps) / length(current_wisps) * wisp_index)
var/datum/abel_wisp_frame/spawn_position = animation_steps[spawn_index]
// Latest added wisp is teleported to its spawn position, others animate from their current position
if (wisp_index == length(current_wisps))
wisp.pixel_w = spawn_position.x
wisp.pixel_z = spawn_position.y
wisp.layer = spawn_position.layer
else
animate(wisp, tag = "wisp_anim_x")
animate(wisp, tag = "wisp_anim_y")
var/start_index = spawn_index % length(animation_steps) + 1
var/datum/abel_wisp_frame/position = animation_steps[start_index]
animate(wisp, pixel_w = position.x, layer = position.layer, time = 0.6 SECONDS, loop = -1, tag = "wisp_anim_x")
// We need to animate x and y coordinates separately as they have different easing steps
for (var/frame_index in 1 to length(animation_steps) - 1)
// Get actual index starting from our *next* animation step, not initial position
var/anim_index = (start_index + frame_index - 1) % length(animation_steps) + 1
position = animation_steps[anim_index]
animate(time = 0.6 SECONDS, pixel_w = position.x, layer = position.layer, easing = SINE_EASING | position.x_easing)
animate(wisp, time = 0.6 SECONDS, pixel_z = position.y, loop = -1, tag = "wisp_anim_y")
for (var/frame_index in 1 to length(animation_steps) - 1)
// Get actual index starting from our *next* animation step, not initial position
var/anim_index = (start_index + frame_index - 1) % length(animation_steps) + 1
position = animation_steps[anim_index]
animate(time = 0.6 SECONDS, pixel_z = position.y, easing = SINE_EASING | position.y_easing)
/obj/item/cain_and_abel/proc/on_wisp_delete(datum/source)
SIGNAL_HANDLER
@@ -148,7 +172,27 @@
/obj/item/cain_and_abel/proc/fire_wisp(atom/user, atom/target)
user.fire_projectile(/obj/projectile/dagger_wisp, target)
set_combo(combo_count - 1, user, TRUE)
/obj/item/cain_and_abel/proc/remove_wisp(obj/wisp_to_remove)
/obj/item/cain_and_abel/proc/remove_wisp(obj/wisp_to_remove, instant = FALSE)
if (instant)
qdel(wisp_to_remove)
return
animate(wisp_to_remove, alpha = 0, time = 0.2 SECONDS)
QDEL_IN(wisp_to_remove, 0.2 SECONDS)
/// Frame data for cain & abel wisps so we don't have to make list monstrocities
/datum/abel_wisp_frame
var/x = 0
var/y = 0
var/layer = MOB_LAYER
var/x_easing = NONE
var/y_easing = NONE
/datum/abel_wisp_frame/New(x, y, layer, x_easing, y_easing)
. = ..()
src.x = x
src.y = y
src.layer = layer
src.x_easing = x_easing
src.y_easing = y_easing
@@ -1,6 +1,5 @@
//effect when we're swinging wildly around
/obj/effect/temp_visual/dagger_slash
name = "Blood Wisp"
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
anchored = TRUE
vis_flags = VIS_INHERIT_DIR | VIS_INHERIT_PLANE
@@ -19,10 +18,10 @@
//flames we collect around our body
/obj/effect/overlay/blood_wisp
name = "Blood Wisp"
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
anchored = TRUE
vis_flags = VIS_INHERIT_DIR | VIS_INHERIT_PLANE
layer = ABOVE_HUD_PLANE
vis_flags = VIS_INHERIT_PLANE
icon = 'icons/effects/effects.dmi'
icon_state = "blood_wisp"
light_power = 2