Adds the surplus energy sword; the mall katana of the future. (#92544)

## About The Pull Request

<img width="911" height="517" alt="image"
src="https://github.com/user-attachments/assets/631d5254-95a4-42d5-ae07-f58a815a1976"
/>

Adds the surplus energy sword. This is found in the black market with a
reasonable probability of being stocked but at a pretty high cost.

The weapon otherwise functions as an energy sword, but with a
significantly lower amount of damage if you're not hitting someone who
is in a compromised position; such as being prone, staggered, attacked
from behind or incapacitated (like stuns).

After 20 hits, you need to recharge the sword by clicking it with the
right mouse button. After an interaction timer, the weapon recharges to
full. You can do this early if you'd like.

Energy swords default to WEIGHT_CLASS_HUGE while active.

## Why It's Good For The Game

I've seen a few folk complain about a lack of particularly interesting
and potent melee weapons that might be floating around to find and
acquire. It'd also be somewhat funny for an actual traitor to have to
get through a pack of goons wielding shitty knockoffs of their own
energy sword.

This was actually my first ever idea for a PR back when I was first
starting the game but had absolutely no idea as to how to code or
anything. I was hardstuck on this for a long ass time. Now I guess I'm
doing it because I was reminded abotu that fact.

## Changelog
🆑
add: Adds the surplus energy sword; the mall katana of the future. It
cuts like shit, but if you're desperate enough, you could kill someone
with it. Just remember to keep it charged.
balance: Energy swords are huge objects while activated.
/🆑

---------

Co-authored-by: ATH1909 <42606352+ATH1909@users.noreply.github.com>
Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
This commit is contained in:
necromanceranne
2025-08-22 04:08:50 +10:00
committed by GitHub
parent a920519c43
commit 89ac4472f5
8 changed files with 171 additions and 3 deletions

View File

@@ -8,3 +8,4 @@
#define DOAFTER_SOURCE_PLANTING_DEVICE "doafter_planting_device" #define DOAFTER_SOURCE_PLANTING_DEVICE "doafter_planting_device"
#define DOAFTER_SOURCE_CHARGE_CRANKRECHARGE "doafter_charge_crank_recharge" #define DOAFTER_SOURCE_CHARGE_CRANKRECHARGE "doafter_charge_crank_recharge"
#define DOAFTER_SOURCE_REMOVING_HOOK "doafter_removing_hook" #define DOAFTER_SOURCE_REMOVING_HOOK "doafter_removing_hook"
#define DOAFTER_SOURCE_CHARGING_ESWORD "doafter_charging_esword"

View File

@@ -12,12 +12,15 @@
var/alt_attacking = FALSE var/alt_attacking = FALSE
/// Trait required for us to trigger /// Trait required for us to trigger
var/required_trait = null var/required_trait = null
/// Hitsound that overrides our current hitsound if defined.
var/alt_hitsound = null
// Old values before we overrode them // Old values before we overrode them
var/base_continuous = null var/base_continuous = null
var/base_simple = null var/base_simple = null
var/base_sharpness = NONE var/base_sharpness = NONE
var/base_hitsound = null
/datum/component/alternative_sharpness/Initialize(alt_sharpness, verbs_continuous = null, verbs_simple = null, force_mod = 0, required_trait = null) /datum/component/alternative_sharpness/Initialize(alt_sharpness, verbs_continuous = null, verbs_simple = null, force_mod = 0, required_trait = null, alt_hitsound = null,)
if (!isitem(parent)) if (!isitem(parent))
return COMPONENT_INCOMPATIBLE return COMPONENT_INCOMPATIBLE
var/obj/item/weapon = parent var/obj/item/weapon = parent
@@ -26,11 +29,21 @@
src.verbs_simple = verbs_simple src.verbs_simple = verbs_simple
src.force_mod = force_mod src.force_mod = force_mod
src.required_trait = required_trait src.required_trait = required_trait
src.alt_hitsound = alt_hitsound
base_continuous = weapon.attack_verb_continuous base_continuous = weapon.attack_verb_continuous
base_simple = weapon.attack_verb_simple base_simple = weapon.attack_verb_simple
base_hitsound = weapon.hitsound
/datum/component/alternative_sharpness/RegisterWithParent() /datum/component/alternative_sharpness/RegisterWithParent()
RegisterSignal(parent, COMSIG_ITEM_PRE_ATTACK_SECONDARY, PROC_REF(on_secondary_attack)) RegisterSignal(parent, COMSIG_ITEM_PRE_ATTACK_SECONDARY, PROC_REF(on_secondary_attack))
RegisterSignal(parent, COMSIG_TRANSFORMING_ON_TRANSFORM, PROC_REF(on_transform))
/datum/component/alternative_sharpness/UnregisterFromParent()
. = ..()
UnregisterSignal(parent, list(
COMSIG_ITEM_PRE_ATTACK_SECONDARY,
COMSIG_TRANSFORMING_ON_TRANSFORM,
))
/datum/component/alternative_sharpness/proc/on_secondary_attack(obj/item/source, atom/target, mob/user, list/modifiers, list/attack_modifiers) /datum/component/alternative_sharpness/proc/on_secondary_attack(obj/item/source, atom/target, mob/user, list/modifiers, list/attack_modifiers)
SIGNAL_HANDLER SIGNAL_HANDLER
@@ -48,6 +61,9 @@
if (!isnull(verbs_simple)) if (!isnull(verbs_simple))
source.attack_verb_simple = verbs_simple source.attack_verb_simple = verbs_simple
if(!isnull(alt_hitsound))
source.hitsound = alt_hitsound
// I absolutely despise this but this is geniunely the best way to do this without creating and hooking up to a dozen signals and still risking failure edge cases // I absolutely despise this but this is geniunely the best way to do this without creating and hooking up to a dozen signals and still risking failure edge cases
addtimer(CALLBACK(src, PROC_REF(disable_alt_attack)), 1) addtimer(CALLBACK(src, PROC_REF(disable_alt_attack)), 1)
@@ -57,3 +73,13 @@
weapon.attack_verb_continuous = base_continuous weapon.attack_verb_continuous = base_continuous
weapon.attack_verb_simple = base_simple weapon.attack_verb_simple = base_simple
weapon.sharpness = base_sharpness weapon.sharpness = base_sharpness
weapon.hitsound = base_hitsound
// If our weapon is transforming, we listen for the transformation to adjust our base_hitsound as needed so we're not caught out by the callback adding inappropriate values.
/datum/component/alternative_sharpness/proc/on_transform(obj/item/source, mob/user, active)
SIGNAL_HANDLER
base_continuous = source.attack_verb_continuous
base_simple = source.attack_verb_simple
base_sharpness = source.sharpness
base_hitsound = source.hitsound

View File

@@ -27,7 +27,7 @@
/// Hitsound played attacking while active. /// Hitsound played attacking while active.
var/active_hitsound = 'sound/items/weapons/blade1.ogg' var/active_hitsound = 'sound/items/weapons/blade1.ogg'
/// Weight class while active. /// Weight class while active.
var/active_w_class = WEIGHT_CLASS_BULKY var/active_w_class = WEIGHT_CLASS_HUGE
/// The heat given off when active. /// The heat given off when active.
var/active_heat = 3500 var/active_heat = 3500
@@ -196,12 +196,15 @@
embed_type = /datum/embedding/esword embed_type = /datum/embedding/esword
var/list/alt_continuous = list("stabs", "pierces", "impales") var/list/alt_continuous = list("stabs", "pierces", "impales")
var/list/alt_simple = list("stab", "pierce", "impale") var/list/alt_simple = list("stab", "pierce", "impale")
var/alt_sharpness = SHARP_POINTY
var/alt_force_mod = -10
var/alt_hitsound = null
/obj/item/melee/energy/sword/Initialize(mapload) /obj/item/melee/energy/sword/Initialize(mapload)
. = ..() . = ..()
alt_continuous = string_list(alt_continuous) alt_continuous = string_list(alt_continuous)
alt_simple = string_list(alt_simple) alt_simple = string_list(alt_simple)
AddComponent(/datum/component/alternative_sharpness, SHARP_POINTY, alt_continuous, alt_simple, -10, TRAIT_TRANSFORM_ACTIVE) AddComponent(/datum/component/alternative_sharpness, alt_sharpness, alt_continuous, alt_simple, alt_force_mod, TRAIT_TRANSFORM_ACTIVE, alt_hitsound)
/obj/item/melee/energy/sword/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE) /obj/item/melee/energy/sword/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK, damage_type = BRUTE)
if(!HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE)) if(!HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE))
@@ -374,3 +377,131 @@
inhand_icon_state = "lightblade" inhand_icon_state = "lightblade"
base_icon_state = "lightblade" base_icon_state = "lightblade"
icon_angle = 0 icon_angle = 0
/obj/item/melee/energy/sword/surplus
name = "\improper Type I 'Iaito' energy sword"
desc = "Oversized, overengineered, and mass-produced. The two blades help make up for the poor cutting plane the emitter generates. Hopefully. \
Supposedly, this version of the energy sword was a Waffle Corp prototype that was first trialed in a variety of armed conflicts around the interstellar \
frontier. The success rate, and survival of its users, were abysmally low. To make matters worse, they had made so many of these swords (accidentally) that \
it would cost the company more disposing of them than trying to offload them to raise a quick buck. Thus, the 'Iaito' was 'born'. Often found in the hands of \
grunts, mooks, goons, criminals, wannabe assassins or lunatics. You may or may not fit into one of these categories if you are genuinely attempting to kill someone\
with this sword."
icon_state = "surplus_e_sword"
inhand_icon_state = "surplus_e_sword"
base_icon_state = "surplus_e_sword"
lefthand_file = 'icons/mob/inhands/64x64_lefthand.dmi'
righthand_file = 'icons/mob/inhands/64x64_righthand.dmi'
inhand_x_dimension = 64
inhand_y_dimension = 64
active_force = 15 // This force is augmented by the state of our target.
active_throwforce = 15
alt_continuous = list("whacks", "smacks", "bashes")
alt_simple = list("whack", "smack", "bash")
alt_sharpness = NONE
alt_force_mod = -12
alt_hitsound = SFX_SWING_HIT
/// Battery used to determine how many hits we can make before our sword switches off and can't be turned back on without a do_after.
var/charge = 20
/// Our battery maximum.
var/max_charge = 20
/// The amount of time it takes to recharge the sword.
var/charge_time = 5 SECONDS
/// The cooldown between instances of vigorous jiggling to get your shitty sword back on.
COOLDOWN_DECLARE(jiggle_cooldown)
/obj/item/melee/energy/sword/surplus/Initialize(mapload)
. = ..()
RegisterSignal(src, COMSIG_TRANSFORMING_PRE_TRANSFORM, PROC_REF(check_power))
/obj/item/melee/energy/sword/surplus/examine(mob/user)
. = ..()
if(charge)
. += span_notice("[src] has [charge] hits left before it must be recharged.")
else
. += span_warning("[src] needs to be recharged.")
. += span_info("You get the sense that this weapon isn't very effective unless you hit someone while they are exposed in some way, like attacking from behind or while they're staggered.")
/obj/item/melee/energy/sword/surplus/add_context(atom/source, list/context, obj/item/held_item, mob/user)
. = ..()
if(charge < max_charge)
context[SCREENTIP_CONTEXT_RMB] = "Recharge"
return CONTEXTUAL_SCREENTIP_SET
return NONE
// A weapon best employed by someone in a desperate struggle
/obj/item/melee/energy/sword/surplus/pre_attack(atom/target, mob/living/user, list/modifiers, list/attack_modifiers)
if(!isliving(target))
return ..()
if(sharpness == NONE)
return ..()
var/mob/living/living_target = target
var/vulnerable_target = FALSE
if(living_target.stat == DEAD) // I know it doesn't make a lot of sense but it makes it a bit too good for dismemberment otherwise
return ..()
if(living_target.get_timed_status_effect_duration(/datum/status_effect/staggered))
vulnerable_target = TRUE
if(HAS_TRAIT(living_target, TRAIT_INCAPACITATED))
vulnerable_target = TRUE
if(check_behind(user, living_target))
vulnerable_target = TRUE
if(vulnerable_target)
MODIFY_ATTACK_FORCE_MULTIPLIER(attack_modifiers, 2)
return ..()
/obj/item/melee/energy/sword/surplus/attack_self_secondary(mob/user, list/modifiers)
. = ..()
if (.)
return
if(charge == max_charge)
return SECONDARY_ATTACK_CALL_NORMAL
if(DOING_INTERACTION(user, DOAFTER_SOURCE_CHARGING_ESWORD))
user.balloon_alert(user, "busy!")
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
if(charge <= max_charge)
user.balloon_alert(user, "attempting recharge...")
if(!do_after(user, charge_time, target = src, extra_checks = CALLBACK(src, PROC_REF(do_jiggle), user), interaction_key = DOAFTER_SOURCE_CHARGING_ESWORD, iconstate = "beat_the_heat"))
user.balloon_alert(user, "interrupted!")
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
charge = max_charge
user.balloon_alert(user, "recharge successful")
playsound(src, 'sound/machines/ping.ogg', 40, TRUE)
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/item/melee/energy/sword/surplus/afterattack(atom/target, mob/user, list/modifiers, list/attack_modifiers)
if(!HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE) || charge <= 0)
return
charge--
if(charge <= 0)
user.balloon_alert(user, "out of charge!")
attack_self(user)
/obj/item/melee/energy/sword/surplus/proc/check_power(obj/item/source, mob/user, active)
SIGNAL_HANDLER
if(charge <= 0 && !HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE))
balloon_alert(user, "no charge!")
return COMPONENT_BLOCK_TRANSFORM
/obj/item/melee/energy/sword/surplus/proc/do_jiggle(mob/user)
if(!COOLDOWN_FINISHED(src, jiggle_cooldown))
return TRUE
user.Shake(2, 1, 0.3 SECONDS, shake_interval = 0.1 SECONDS)
playsound(src, 'sound/items/baton/telescopic_baton_folded_pickup.ogg', 40, TRUE)
COOLDOWN_START(src, jiggle_cooldown, 1 SECONDS)
return TRUE

View File

@@ -143,3 +143,13 @@
price_max = CARGO_CRATE_VALUE * 5 price_max = CARGO_CRATE_VALUE * 5
stock_max = 1 stock_max = 1
availability_prob = 35 availability_prob = 35
/datum/market_item/weapon/surplus_esword
name = "Type I 'Iaito' Energy Sword"
desc = "A mass-produced energy sword. It is functionally worse than a milspec energy sword commonly found amongst paramilitary organizations. \
But hey, better than nothing. Does have some power supply problems, but nothing that a bit of percussive maintenance can't fix."
item = /obj/item/melee/energy/sword/surplus
price_min = CARGO_CRATE_VALUE * 2
price_max = CARGO_CRATE_VALUE * 5
stock_max = 2
availability_prob = 80

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB