Merges crusher retool kits into one, datumizes crusher skins, gives dagger and blaster unique animations (#90742)

## About The Pull Request

Merges sword, harpoon, glaive and dagger crusher retools into a single
item which allows you to pick the skin you want upon being applied to a
crusher by converting crusher skins into datums.
I've also improved sprites for all skins and redid the retool kit
itself, as well as gave dagger and blaster unique animations - you swing
with a dagger for normal attacks, stab to rupture the mark, and firing a
shot point-blank does a stabbing animation with the blaster.

![dreamseeker_5nPyNKqzoC](https://github.com/user-attachments/assets/c8858a71-3c0f-49a0-8577-cb8a9e64e0a0)


https://github.com/user-attachments/assets/2d243daf-f1d8-48bf-a661-8a9ac990837c

(Apologies for lack of robust gameplay, I was trying to show the
animations, not kill the goliath)

The harpoon crusher skin now does stabbing animation instead of slashing
one, and I've also fixed animations playing incorrectly when attacking
large mobs/objects

## Why It's Good For The Game

Having multiple items for reskinning crushers feels clunky, and unique
animations are just something that makes sense. Having a swinging
animation for both dagger and blaster looked odd, as you'd probably only
swing one of them at the opponent, so I decided to spice them up.

## Changelog
🆑
add: Dagger-blaster and harpoon crusher skins have received unique
attack animations.
add: Four base crusher retool kits have been merged into one item.
image: Resprited crusher skins and the crusher retool kit
fix: Fixed attack animations being offset when attacking large objects
or mobs.
/🆑
This commit is contained in:
SmArtKar
2025-05-04 15:36:27 +03:00
committed by GitHub
parent ad352a2b7e
commit b0aaeca5ae
10 changed files with 206 additions and 97 deletions
@@ -541,6 +541,9 @@
/// Sent from /obj/item/update_weight_class(), to its loc. (obj/item/changed_item, old_w_class, new_w_class)
#define COMSIG_ATOM_CONTENTS_WEIGHT_CLASS_CHANGED "atom_contents_weight_class_changed"
/// Sent from /obj/item/proc/animate_attack() : (atom/movable/attacker, atom/attacked_atom, animation_type, list/image_override, list/animation_override, list/angle_override)
#define COMSIG_ITEM_ATTACK_ANIMATION "item_attack_animation"
///Sent from /obj/item/skillchip/on_implant()
#define COMSIG_SKILLCHIP_IMPLANTED "skillchip_implanted"
@@ -588,3 +591,6 @@
/// Sent from /datum/wires/attach_assembly() : (atom/holder)
#define COMSIG_ASSEMBLY_PRE_ATTACH "assembly_pre_attach"
#define COMPONENT_CANCEL_ATTACH (1<<0)
/// Sent from /obj/item/kinetic_crusher/proc/fire_kinetic_blast() : (atom/target, mob/living/user, obj/projectile/destabilizer/destabilizer)
#define COMSIG_CRUSHER_FIRED_BLAST "crusher_fired_blast"
@@ -113,7 +113,7 @@
var/mob/mob_parent = parent
if(!isopenturf(attacked_target))
INVOKE_ASYNC(held_disk, TYPE_PROC_REF(/obj/item, melee_attack_chain), mob_parent, attacked_target)
mob_parent.do_item_attack_animation(attacked_target, used_item = held_disk)
mob_parent.do_item_attack_animation(attacked_target, used_item = held_disk, animation_type = ATTACK_ANIMATION_BLUNT)
return COMPONENT_CANCEL_ATTACK_CHAIN
held_disk.forceMove(attacked_target)
+1 -8
View File
@@ -1499,14 +1499,7 @@
/atom/movable/proc/do_attack_animation(atom/attacked_atom, visual_effect_icon, obj/item/used_item, no_effect, fov_effect = TRUE, item_animation_override = null)
if(!no_effect && (visual_effect_icon || used_item))
var/animation_type = item_animation_override || ATTACK_ANIMATION_BLUNT
if (used_item && !item_animation_override)
switch(used_item.get_sharpness())
if (SHARP_EDGED)
animation_type = ATTACK_ANIMATION_SLASH
if (SHARP_POINTY)
animation_type = ATTACK_ANIMATION_PIERCE
do_item_attack_animation(attacked_atom, visual_effect_icon, used_item, animation_type = animation_type)
do_item_attack_animation(attacked_atom, visual_effect_icon, used_item, animation_type = item_animation_override)
if(attacked_atom == src)
return //don't do an animation if attacking self
@@ -38,18 +38,6 @@
purchase_path = /obj/item/crusher_trophy/retool_kit
cost_per_order = 150
/datum/orderable_item/mining/crusher_retool_kit_harpoon
purchase_path = /obj/item/crusher_trophy/retool_kit/harpoon
cost_per_order = 150
/datum/orderable_item/mining/crusher_retool_kit_dagger
purchase_path = /obj/item/crusher_trophy/retool_kit/dagger
cost_per_order = 150
/datum/orderable_item/mining/crusher_retool_kit_glaive
purchase_path = /obj/item/crusher_trophy/retool_kit/glaive
cost_per_order = 150
/datum/orderable_item/mining/resonator
purchase_path = /obj/item/resonator
cost_per_order = 710
+48 -23
View File
@@ -1544,38 +1544,63 @@
// This is instant on byond's end, but to our clients this looks like a quick drop
animate(src, alpha = old_alpha, pixel_x = old_x, pixel_y = old_y, transform = old_transform, time = 3, easing = CUBIC_EASING)
/atom/movable/proc/do_item_attack_animation(atom/attacked_atom, visual_effect_icon, obj/item/used_item, animation_type = ATTACK_ANIMATION_BLUNT)
if (visual_effect_icon)
var/image/attack_image = image(icon = 'icons/effects/effects.dmi', icon_state = visual_effect_icon)
attack_image.plane = attacked_atom.plane + 1
// Scale the icon.
attack_image.transform *= 0.4
// The icon should not rotate.
attack_image.appearance_flags = APPEARANCE_UI
var/atom/movable/flick_visual/attack = attacked_atom.flick_overlay_view(attack_image, 1 SECONDS)
var/matrix/copy_transform = new(transform)
animate(attack, alpha = 175, transform = copy_transform.Scale(0.75), time = 0.3 SECONDS)
animate(time = 0.1 SECONDS)
animate(alpha = 0, time = 0.3 SECONDS, easing = CIRCULAR_EASING|EASE_OUT)
/atom/movable/proc/do_item_attack_animation(atom/attacked_atom, visual_effect_icon, obj/item/used_item, animation_type)
if (!visual_effect_icon)
if (used_item)
used_item.animate_attack(src, attacked_atom, animation_type)
return
if (isnull(used_item))
return
var/image/attack_image = isnull(used_item.attack_icon) ? image(icon = used_item) : image(icon = used_item.attack_icon, icon_state = used_item.attack_icon_state)
var/image/attack_image = image(icon = 'icons/effects/effects.dmi', icon_state = visual_effect_icon)
attack_image.plane = attacked_atom.plane + 1
attack_image.pixel_w = used_item.base_pixel_x + used_item.base_pixel_w
attack_image.pixel_z = used_item.base_pixel_y + used_item.base_pixel_z
// Scale the icon.
attack_image.transform *= 0.4
// The icon should not rotate.
attack_image.appearance_flags = APPEARANCE_UI
var/atom/movable/flick_visual/attack = attacked_atom.flick_overlay_view(attack_image, 1 SECONDS)
var/matrix/copy_transform = new(transform)
animate(attack, alpha = 175, transform = copy_transform.Scale(0.75), time = 0.3 SECONDS)
animate(time = 0.1 SECONDS)
animate(alpha = 0, time = 0.3 SECONDS, easing = CIRCULAR_EASING|EASE_OUT)
/obj/item/proc/animate_attack(atom/movable/attacker, atom/attacked_atom, animation_type)
var/list/image_override = list()
var/list/animation_override = list()
var/used_icon_angle = icon_angle
var/list/angle_override = list()
SEND_SIGNAL(src, COMSIG_ITEM_ATTACK_ANIMATION, attacker, attacked_atom, animation_type, image_override, animation_override, angle_override)
var/image/attack_image = null
if (!length(image_override))
attack_image = isnull(attack_icon) ? image(icon = src) : image(icon = attack_icon, icon_state = attack_icon_state)
else
attack_image = image_override[1]
if (length(animation_override))
animation_type = animation_override[1]
else if (!animation_type)
switch (get_sharpness())
if (SHARP_EDGED)
animation_type = ATTACK_ANIMATION_SLASH
if (SHARP_POINTY)
animation_type = ATTACK_ANIMATION_PIERCE
else
animation_type = ATTACK_ANIMATION_BLUNT
if (length(angle_override))
used_icon_angle = angle_override[1]
attack_image.plane = attacked_atom.plane + 1
attack_image.pixel_w = attacker.base_pixel_x + attacker.base_pixel_w - attacked_atom.base_pixel_x - attacked_atom.base_pixel_w
attack_image.pixel_z = attacker.base_pixel_y + attacker.base_pixel_z - attacked_atom.base_pixel_y - attacked_atom.base_pixel_z
// Scale the icon.
attack_image.transform *= 0.5
// The icon should not rotate.
attack_image.appearance_flags = APPEARANCE_UI
var/atom/movable/flick_visual/attack = attacked_atom.flick_overlay_view(attack_image, 1 SECONDS)
var/matrix/copy_transform = new(transform)
var/matrix/copy_transform = new(attacker.transform)
var/x_sign = 0
var/y_sign = 0
var/direction = get_dir(src, attacked_atom)
var/direction = get_dir(attacker, attacked_atom)
if (direction & NORTH)
y_sign = -1
else if (direction & SOUTH)
@@ -1606,7 +1631,7 @@
if (ATTACK_ANIMATION_PIERCE)
var/attack_angle = dir2angle(direction) + rand(-7, 7)
// Deducting 90 because we're assuming that icon_angle of 0 means an east-facing sprite
var/anim_angle = attack_angle - 90 - used_item.icon_angle
var/anim_angle = attack_angle - 90 - used_icon_angle
var/angle_mult = 1
if (x_sign && y_sign)
angle_mult = 1.4
@@ -1644,7 +1669,7 @@
var/x_rot_sign = 0
var/y_rot_sign = 0
var/attack_dir = (prob(50) ? 1 : -1)
var/anim_angle = dir2angle(direction) - 90 - used_item.icon_angle
var/anim_angle = dir2angle(direction) - 90 - used_icon_angle
if (x_sign)
y_rot_sign = attack_dir
+1 -1
View File
@@ -509,7 +509,7 @@
to_chat(taker, span_nicegreen("[offerer] gives you \a [blown_kiss][cheek_kiss ? " on the cheek" : ""]!"))
offerer.face_atom(taker)
taker.face_atom(offerer)
offerer.do_item_attack_animation(taker, used_item = src)
offerer.do_item_attack_animation(taker, used_item = src, animation_type = ATTACK_ANIMATION_BLUNT)
//We're still firing a shot here because I don't want to deal with some weird edgecase where direct impacting them with the projectile causes it to freak out because there's no angle or something
blown_kiss.original = taker
blown_kiss.fired_from = offerer
@@ -124,7 +124,7 @@
balloon_alert(user, "flask is full!")
return ITEM_INTERACT_SUCCESS
to_fill.reagents.add_reagent(/datum/reagent/eldritch, 50)
do_item_attack_animation(src, used_item = tool)
do_item_attack_animation(src, used_item = tool, animation_type = ATTACK_ANIMATION_BLUNT)
current_mass--
balloon_alert(user, "refilled flask")
return ITEM_INTERACT_SUCCESS
@@ -219,7 +219,8 @@
if(!isturf(proj_turf))
return
var/obj/projectile/destabilizer/destabilizer = new(proj_turf)
destabilizer.icon_state = "[projectile_icon]"
SEND_SIGNAL(src, COMSIG_CRUSHER_FIRED_BLAST, target, user, destabilizer)
destabilizer.icon_state = projectile_icon
for(var/obj/item/crusher_trophy/attached_trophy as anything in trophies)
attached_trophy.on_projectile_fire(destabilizer, user)
destabilizer.aim_projectile(target, user, modifiers)
@@ -1,52 +1,64 @@
/*!
* Contains crusher trophies that are not obtained from fauna
*/
//cosmetic items for changing the crusher's look
/// Cosmetic items for changing the crusher's look
/obj/item/crusher_trophy/retool_kit
name = "crusher sword retool kit"
desc = "A toolkit for changing the crusher's appearance without affecting the device's function. This one will make it look like a sword."
name = "crusher retool kit"
desc = "A toolkit for changing the crusher's appearance without affecting the device's function."
icon = 'icons/obj/mining.dmi'
icon_state = "retool_kit"
denied_type = /obj/item/crusher_trophy/retool_kit
/// Specifies the icon file in which the crusher's new state is stored.
var/retool_icon = 'icons/obj/mining.dmi'
///Specifies the sprite/icon state which the crusher is changed to as an item. Should appear in the icons/obj/mining.dmi file with accompanying "lit" and "recharging" sprites
var/retool_icon_state = "crusher_sword"
///Specifies the icon state for the crusher's appearance in hand. Should appear in both retool_lefthand_file and retool_righthand_file.
var/retool_inhand_icon = "crusher_sword"
///For if the retool kit changes the projectile's appearance. The sprite should be in icons/obj/weapons/guns/projectiles.dmi.
var/retool_projectile_icon = "pulse1"
/// Specifies the left hand inhand icon file. Don't forget to set the right hand file as well.
var/retool_lefthand_file = 'icons/mob/inhands/weapons/hammers_lefthand.dmi'
/// Specifies the right hand inhand icon file. Don't forget to set the left hand file as well.
var/retool_righthand_file = 'icons/mob/inhands/weapons/hammers_righthand.dmi'
/// Specifies the X dimensions of the new inhand, only relevant with different inhand files.
var/retool_inhand_x = 32
/// Specifies the Y dimensions of the new inhand, only relevant with different inhand files.
var/retool_inhand_y = 32
/// Currently picked crusher reskin
var/datum/crusher_skin/active_skin = /datum/crusher_skin/sword
/// If this kit forces some specific skin, or can pick between subtypes
var/forced_skin
/obj/item/crusher_trophy/retool_kit/Destroy(force)
if (istype(active_skin))
QDEL_NULL(active_skin)
return ..()
/obj/item/crusher_trophy/retool_kit/effect_desc()
return "the crusher to have the appearance of a sword"
return "the crusher to have the appearance of \a [active_skin::name]"
/obj/item/crusher_trophy/retool_kit/add_to(obj/item/kinetic_crusher/pkc, mob/user)
if (!forced_skin)
var/list/choices = list()
for (var/datum/crusher_skin/skin as anything in subtypesof(/datum/crusher_skin))
if (skin::normal_skin)
choices[skin] = icon(skin::retool_icon || 'icons/obj/mining.dmi', skin::retool_icon_state)
var/datum/crusher_skin/chosen_skin = show_radial_menu(user, src, choices, tooltips = TRUE, require_near = TRUE)
if (!chosen_skin)
return
active_skin = chosen_skin
else
active_skin = forced_skin
. = ..()
if(!.)
return
pkc.icon = retool_icon
pkc.icon_state = retool_icon_state
pkc.current_inhand_icon_state = retool_inhand_icon
pkc.projectile_icon = retool_projectile_icon
pkc.lefthand_file = retool_lefthand_file
pkc.righthand_file = retool_righthand_file
pkc.inhand_x_dimension = retool_inhand_x
pkc.inhand_y_dimension = retool_inhand_y
active_skin = new active_skin(pkc)
if (active_skin.retool_icon)
pkc.icon = active_skin.retool_icon
pkc.icon_state = active_skin.retool_icon_state
pkc.current_inhand_icon_state = active_skin.retool_inhand_icon
if (active_skin.retool_projectile_icon)
pkc.projectile_icon = active_skin.retool_projectile_icon
// Should either have both, or neither
if (active_skin.retool_lefthand_file)
pkc.lefthand_file = active_skin.retool_lefthand_file
pkc.righthand_file = active_skin.retool_righthand_file
if (active_skin.retool_inhand_x)
pkc.inhand_x_dimension = active_skin.retool_inhand_x
if (active_skin.retool_inhand_y)
pkc.inhand_y_dimension = active_skin.retool_inhand_y
pkc.update_appearance()
pkc.update_slot_icon()
/obj/item/crusher_trophy/retool_kit/remove_from(obj/item/kinetic_crusher/pkc)
var/skin_type = active_skin.type
qdel(active_skin)
active_skin = skin_type
pkc.icon = initial(pkc.icon)
pkc.icon_state = initial(pkc.icon_state)
pkc.current_inhand_icon_state = initial(pkc.current_inhand_icon_state)
@@ -59,28 +71,111 @@
pkc.update_slot_icon()
return ..()
/obj/item/crusher_trophy/retool_kit/harpoon
name = "crusher harpoon retool kit"
desc = "A toolkit for changing the crusher's appearance without affecting the device's function. This one will make it look like a harpoon."
/// Alternate PKC skins
/datum/crusher_skin
/// Name of the modification
var/name = "error that should be reported to coders"
/// Specifies the icon file in which the crusher's new state is stored.
var/retool_icon = null
///Specifies the sprite/icon state which the crusher is changed to as an item. Should appear in the icons/obj/mining.dmi file with accompanying "lit" and "recharging" sprites
var/retool_icon_state = "ipickaxe"
///Specifies the icon state for the crusher's appearance in hand. Should appear in both retool_lefthand_file and retool_righthand_file.
var/retool_inhand_icon = "ipickaxe"
///For if the retool kit changes the projectile's appearance. The sprite should be in icons/obj/weapons/guns/projectiles.dmi.
var/retool_projectile_icon = null
/// Specifies the left hand inhand icon file. Don't forget to set the right hand file as well.
var/retool_lefthand_file = null
/// Specifies the right hand inhand icon file. Don't forget to set the left hand file as well.
var/retool_righthand_file = null
/// Specifies the X dimensions of the new inhand, only relevant with different inhand files.
var/retool_inhand_x = null
/// Specifies the Y dimensions of the new inhand, only relevant with different inhand files.
var/retool_inhand_y = null
/// Can this skin be normally selected by a generic retool kit?
var/normal_skin = TRUE
/// Crusher this skin is attached to
var/obj/item/kinetic_crusher/crusher
/datum/crusher_skin/New(obj/item/kinetic_crusher/new_crusher)
. = ..()
crusher = new_crusher
/datum/crusher_skin/Destroy(force)
crusher = null
return ..()
/datum/crusher_skin/sword
name = "sword"
retool_icon_state = "crusher_sword"
retool_inhand_icon = "crusher_sword"
/datum/crusher_skin/harpoon
name = "harpoon"
retool_icon_state = "crusher_harpoon"
retool_inhand_icon = "crusher_harpoon"
retool_projectile_icon = "pulse_harpoon"
/obj/item/crusher_trophy/retool_kit/harpoon/effect_desc()
return "the crusher to have the appearance of a harpoon"
/datum/crusher_skin/harpoon/New(obj/item/kinetic_crusher/new_crusher)
. = ..()
RegisterSignal(crusher, COMSIG_ITEM_ATTACK_ANIMATION, PROC_REF(on_attack_animation))
/obj/item/crusher_trophy/retool_kit/dagger
name = "crusher dagger retool kit"
desc = "A toolkit for changing the crusher's appearance without affecting the device's function. This one will make it look like a dual dagger and mini-blaster on a chain."
/datum/crusher_skin/harpoon/Destroy(force)
UnregisterSignal(crusher, COMSIG_ITEM_ATTACK_ANIMATION)
return ..()
/datum/crusher_skin/harpoon/proc/on_attack_animation(obj/item/source, atom/movable/attacker, atom/attacked_atom, animation_type, list/image_override, list/animation_override)
SIGNAL_HANDLER
// If nothing is forcing an animation type, attack with a piercing animation because we're a harpoon
if (!animation_type)
animation_override += ATTACK_ANIMATION_PIERCE
/datum/crusher_skin/dagger
name = "dual dagger and blaster"
retool_icon_state = "crusher_dagger"
retool_inhand_icon = "crusher_dagger"
/// Are we doing a blaster animation right now?
var/blaster_strike = FALSE
/obj/item/crusher_trophy/retool_kit/dagger/effect_desc()
return "the crusher to have the appearance of a dual dagger and blaster"
/datum/crusher_skin/dagger/New(obj/item/kinetic_crusher/new_crusher)
. = ..()
RegisterSignal(crusher, COMSIG_ITEM_ATTACK_ANIMATION, PROC_REF(on_attack_animation))
RegisterSignal(crusher, COMSIG_CRUSHER_FIRED_BLAST, PROC_REF(on_fired_blast))
/obj/item/crusher_trophy/retool_kit/glaive
name = "crusher glaive retool kit"
desc = "A toolkit for changing the crusher's appearance without affecting the device's function. This one will make it look like a glaive, with a longer, thinner blade."
/datum/crusher_skin/dagger/Destroy(force)
UnregisterSignal(crusher, list(COMSIG_ITEM_ATTACK_ANIMATION, COMSIG_CRUSHER_FIRED_BLAST))
return ..()
/datum/crusher_skin/dagger/proc/on_attack_animation(obj/item/kinetic_crusher/source, atom/movable/attacker, atom/attacked_atom, animation_type, list/image_override, list/animation_override, list/angle_override)
SIGNAL_HANDLER
// If we've been forcefully assigned an animation type already, we shouldn't do the custom attack animation logic
if (animation_type)
return
if (isliving(attacked_atom))
var/mob/living/target = attacked_atom
if (blaster_strike)
image_override += image(icon = 'icons/obj/mining.dmi', icon_state = "crusher_dagger_blaster")
angle_override += 0
animation_override += ATTACK_ANIMATION_PIERCE
blaster_strike = FALSE
return
if (target.has_status_effect(/datum/status_effect/crusher_mark))
animation_override += ATTACK_ANIMATION_PIERCE
image_override += image(icon = 'icons/obj/mining.dmi', icon_state = "crusher_dagger_melee")
/datum/crusher_skin/dagger/proc/on_fired_blast(obj/item/kinetic_crusher/source, atom/target, mob/living/user, obj/projectile/destabilizer/destabilizer)
SIGNAL_HANDLER
if (isliving(target) && get_dist(target, user) <= 1)
blaster_strike = TRUE
user.do_item_attack_animation(target, used_item = source)
/datum/crusher_skin/glaive
name = "glaive"
retool_icon_state = "crusher_glaive"
retool_inhand_icon = "crusher_glaive"
retool_lefthand_file = 'icons/mob/inhands/64x64_lefthand.dmi'
@@ -88,16 +183,17 @@
retool_inhand_x = 64
retool_inhand_y = 64
/obj/item/crusher_trophy/retool_kit/glaive/effect_desc()
return "the crusher to have the appearance of a glaive"
/obj/item/crusher_trophy/retool_kit/ashenskull
name = "ashen skull"
desc = "It burns with the flame of the necropolis, whispering in your ear. It demands to be bound to a suitable weapon."
icon_state = "retool_kit_skull"
retool_icon_state = "crusher_skull"
retool_inhand_icon = "crusher_skull"
retool_projectile_icon = "pulse_skull"
forced_skin = /datum/crusher_skin/ashen_skull
/obj/item/crusher_trophy/retool_kit/ashenskull/effect_desc()
return "the crusher to appear corrupted by infernal powers"
/datum/crusher_skin/ashen_skull
retool_icon_state = "crusher_skull"
retool_inhand_icon = "crusher_skull"
retool_projectile_icon = "pulse_skull"
normal_skin = FALSE
Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 61 KiB