diff --git a/code/game/objects/items/weapons/stunbaton.dm b/code/game/objects/items/weapons/stunbaton.dm index e877bb94694..bac1b027e70 100644 --- a/code/game/objects/items/weapons/stunbaton.dm +++ b/code/game/objects/items/weapons/stunbaton.dm @@ -12,6 +12,7 @@ origin_tech = "combat=2" attack_verb = list("beaten") armor = list(MELEE = 0, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 50, RAD = 0, FIRE = 80, ACID = 80) + new_attack_chain = TRUE /// How many seconds does the knockdown last for? var/knockdown_duration = 10 SECONDS /// how much stamina damage does this baton do? @@ -51,10 +52,12 @@ if(unlink) cell = null return + if(isrobot(loc?.loc)) // First loc is the module var/mob/living/silicon/robot/R = loc.loc cell = R.cell return + if(!cell) var/powercell = /obj/item/stock_parts/cell/high cell = new powercell(src) @@ -110,37 +113,46 @@ /obj/item/melee/baton/proc/deductcharge(amount) if(!cell) return + cell.use(amount) if(cell.rigged) cell = null turned_on = FALSE update_icon(UPDATE_ICON_STATE) return + if(cell.charge < (hitcost)) // If after the deduction the baton doesn't have enough charge for a stun hit it turns off. turned_on = FALSE update_icon() playsound(src, "sparks", 75, TRUE, -1) -/obj/item/melee/baton/attackby__legacy__attackchain(obj/item/I, mob/user, params) - if(istype(I, /obj/item/stock_parts/cell)) - var/obj/item/stock_parts/cell/C = I +/obj/item/melee/baton/item_interaction(mob/living/user, obj/item/used, list/modifiers) + . = ..() + if(istype(used, /obj/item/stock_parts/cell)) + var/obj/item/stock_parts/cell/C = used if(cell) to_chat(user, "[src] already has a cell!") - return + return ITEM_INTERACT_COMPLETE + if(C.maxcharge < hitcost) to_chat(user, "[src] requires a higher capacity cell!") - return - if(!user.unequip(I)) - return - I.forceMove(src) - cell = I - to_chat(user, "You install [I] into [src].") + return ITEM_INTERACT_COMPLETE + + if(!user.unequip(used)) + to_chat(user, "[used] is stuck to your hand!") + return ITEM_INTERACT_COMPLETE + + used.forceMove(src) + cell = used + to_chat(user, "You install [used] into [src].") update_icon(UPDATE_ICON_STATE) + return ITEM_INTERACT_COMPLETE /obj/item/melee/baton/screwdriver_act(mob/living/user, obj/item/I) if(!cell) to_chat(user, "There's no cell installed!") return + if(!I.use_tool(src, user, volume = I.tool_volume)) return @@ -151,7 +163,15 @@ turned_on = FALSE update_icon(UPDATE_ICON_STATE) -/obj/item/melee/baton/attack_self__legacy__attackchain(mob/user) +/obj/item/melee/baton/activate_self(mob/user) + if(..()) + return FINISH_ATTACK + + // Sometimes the borg baton spawns without linking to the cyborg's cell for reasons beyond my ken. That is VERY bad. This will fix it on the spot. + // They have to turn it on to use it, after all. + if(isrobot(loc) && !cell) + link_new_cell() + if(cell?.charge >= hitcost) turned_on = !turned_on to_chat(user, "[src] is now [turned_on ? "on" : "off"].") @@ -171,43 +191,67 @@ if(!. && turned_on && istype(hit_mob)) thrown_baton_stun(hit_mob) -/obj/item/melee/baton/attack__legacy__attackchain(mob/M, mob/living/user) +/obj/item/melee/baton/pre_attack(atom/A, mob/living/user, params) + if(..()) + return FINISH_ATTACK + if(turned_on && HAS_TRAIT(user, TRAIT_CLUMSY) && prob(50)) - if(baton_stun(user, user, skip_cooldown = TRUE)) // for those super edge cases where you clumsy baton yourself in quick succession - user.visible_message("[user] accidentally hits [user.p_themselves()] with [src]!", - "You accidentally hit yourself with [src]!") - return + // For those super edge cases where you clumsy baton yourself in quick succession. + if(baton_stun(user, user, skip_cooldown = TRUE)) + user.visible_message( + "[user] accidentally hits [user.p_themselves()] with [src]!", + "You accidentally hit yourself with [src]!" + ) + return FINISH_ATTACK + if(user.mind?.martial_art?.no_baton && user.mind?.martial_art?.can_use(user)) to_chat(user, user.mind.martial_art.no_baton_reason) - return - if(issilicon(M)) // Can't stunbaton borgs and AIs - return ..() + return FINISH_ATTACK - if(!isliving(M)) + if(!ismob(A)) return - var/mob/living/L = M + + user.changeNext_move(CLICK_CD_MELEE) + var/mob/living/target = A if(user.a_intent == INTENT_HARM) - . = ..() // Whack them too if in harm intent - if(!isnull(.)) // Attack returns null when successful - return - if(turned_on) - baton_stun(L, user, ignore_shield_check = TRUE) + // Harmbaton! return if(!turned_on) - user.do_attack_animation(L) - L.visible_message("[user] has prodded [L] with [src]. Luckily it was off.", - "[L == user ? "You prod yourself" : "[user] has prodded you"] with [src]. Luckily it was off.") - return + user.do_attack_animation(target) + target.visible_message( + "[user] has prodded [target] with [src]. Luckily it was off.", + "[target == user ? "You prod yourself" : "[user] has prodded you"] with [src]. Luckily it was off." + ) + playsound(loc, 'sound/weapons/tap.ogg', 50, TRUE, -1) + return FINISH_ATTACK - if(baton_stun(L, user)) - user.do_attack_animation(L) + // Only human mobs can be stunned. + if(!ishuman(target)) + user.do_attack_animation(target) + target.visible_message( + "[user] has prodded [target] with [src]. It doesn't seem to have an effect.", + "[target == user ? "You prod yourself" : "[user] has prodded you"] with [src]. It doesn't seem to have an effect." + ) + playsound(loc, 'sound/weapons/tap.ogg', 50, TRUE, -1) + return FINISH_ATTACK + + if(baton_stun(target, user)) + user.do_attack_animation(target) + return FINISH_ATTACK + +/obj/item/melee/baton/after_attack(atom/target, mob/user, proximity_flag, click_parameters) + . = ..() + if(ishuman(target) && turned_on) + var/mob/living/carbon/human/H = target + baton_stun(H, user, ignore_shield_check = TRUE) /// returning false results in no baton attack animation, returning true results in an animation. If ignore_shield_check is true, the baton will not run check shields, and will hit if not on cooldown. /obj/item/melee/baton/proc/baton_stun(mob/living/L, mob/user, skip_cooldown = FALSE, ignore_shield_check = FALSE) if(cooldown > world.time && !skip_cooldown) return FALSE + var/user_UID = user.UID() if(HAS_TRAIT_FROM(L, TRAIT_WAS_BATONNED, user_UID)) // prevents double baton cheese. return FALSE @@ -222,6 +266,7 @@ if(!ignore_shield_check && H.check_shields(src, 0, "[user]'s [name]", MELEE_ATTACK)) //No message; check_shields() handles that playsound(L, 'sound/weapons/genhit.ogg', 50, TRUE) return FALSE + H.Confused(10 SECONDS) H.Jitter(10 SECONDS) H.apply_damage(stam_damage, STAMINA) @@ -236,8 +281,10 @@ if(user) L.lastattacker = user.real_name L.lastattackerckey = user.ckey - L.visible_message("[user] has stunned [L] with [src]!", - "[L == user ? "You stun yourself" : "[user] has stunned you"] with [src]!") + L.visible_message( + "[user] has stunned [L] with [src]!", + "[L == user ? "You stun yourself" : "[user] has stunned you"] with [src]!" + ) add_attack_logs(user, L, "stunned") play_hit_sound() deductcharge(hitcost) @@ -292,8 +339,10 @@ if(turned_on && cell?.charge) flick("baton_active", source) baton_stun(user, user, skip_cooldown = TRUE) - user.visible_message("[user] shocks [user.p_themselves()] while attempting to wash the active [src]!", - "You unwisely attempt to wash [src] while it's still on.") + user.visible_message( + "[user] shocks [user.p_themselves()] while attempting to wash the active [src]!", + "You unwisely attempt to wash [src] while it's still on." + ) return TRUE ..() @@ -359,20 +408,15 @@ /obj/item/melee/baton/flayerprod/Initialize(mapload) // We are not making a flayerprod without a cell link_new_cell() + RegisterSignal(src, COMSIG_ACTIVATE_SELF, TYPE_PROC_REF(/datum, signal_cancel_activate_self)) return ..() /obj/item/melee/baton/flayerprod/update_icon_state() return -/obj/item/melee/baton/flayerprod/attackby__legacy__attackchain(obj/item/I, mob/user, params) - return - /obj/item/melee/baton/flayerprod/screwdriver_act(mob/living/user, obj/item/I) return -/obj/item/melee/baton/flayerprod/attack_self__legacy__attackchain(mob/user) - return - /obj/item/melee/baton/flayerprod/play_hit_sound() playsound(src, 'sound/weapons/egloves.ogg', 25, TRUE, -1, ignore_walls = FALSE) diff --git a/code/game/objects/items/weapons/teleprod.dm b/code/game/objects/items/weapons/teleprod.dm index 69038742800..a22016ea55e 100644 --- a/code/game/objects/items/weapons/teleprod.dm +++ b/code/game/objects/items/weapons/teleprod.dm @@ -7,13 +7,22 @@ base_icon = "teleprod" origin_tech = "combat=2;bluespace=4;materials=3" -/obj/item/melee/baton/cattleprod/teleprod/attack__legacy__attackchain(mob/living/carbon/M, mob/living/carbon/user)//handles making things teleport when hit - ..() +/obj/item/melee/baton/cattleprod/teleprod/pre_attack(atom/A, mob/living/user, params) + if(..()) + return FINISH_ATTACK + if(!turned_on) + return FINISH_ATTACK + + if(!ismob(A)) return + + var/mob/living/carbon/M if(HAS_TRAIT(user, TRAIT_CLUMSY) && prob(50)) - user.visible_message("[user] accidentally hits [user.p_themselves()] with [src]!", - "You accidentally hit yourself with [src]!") + user.visible_message( + "[user] accidentally hits [user.p_themselves()] with [src]!", + "You accidentally hit yourself with [src]!" + ) deductcharge(hitcost) do_teleport(user, get_turf(user), 50)//honk honk else if(isliving(M) && !M.anchored) diff --git a/code/modules/mob/living/simple_animal/bot/ed209bot.dm b/code/modules/mob/living/simple_animal/bot/ed209bot.dm index a92dc4b75ec..e8134df8278 100644 --- a/code/modules/mob/living/simple_animal/bot/ed209bot.dm +++ b/code/modules/mob/living/simple_animal/bot/ed209bot.dm @@ -560,7 +560,7 @@ var/threat = C.assess_threat(src) var/prev_intent = a_intent a_intent = INTENT_HELP - baton.attack__legacy__attackchain(C, src) + baton.pre_attack(C, src) a_intent = prev_intent baton_delayed = TRUE addtimer(VARSET_CALLBACK(src, baton_delayed, FALSE), BATON_COOLDOWN) diff --git a/code/modules/mob/living/simple_animal/bot/secbot.dm b/code/modules/mob/living/simple_animal/bot/secbot.dm index f47a7fa6b50..709943b2313 100644 --- a/code/modules/mob/living/simple_animal/bot/secbot.dm +++ b/code/modules/mob/living/simple_animal/bot/secbot.dm @@ -267,7 +267,7 @@ var/threat = C.assess_threat(src) var/prev_intent = a_intent a_intent = harmbaton ? INTENT_HARM : INTENT_HELP - baton.attack__legacy__attackchain(C, src) + baton.pre_attack(C, src) a_intent = prev_intent baton_delayed = TRUE addtimer(VARSET_CALLBACK(src, baton_delayed, FALSE), BATON_COOLDOWN) diff --git a/code/tests/attack_chain/test_attack_chain_stunbaton.dm b/code/tests/attack_chain/test_attack_chain_stunbaton.dm new file mode 100644 index 00000000000..755dce70b93 --- /dev/null +++ b/code/tests/attack_chain/test_attack_chain_stunbaton.dm @@ -0,0 +1,25 @@ +/datum/game_test/attack_chain_stunbaton/Run() + var/datum/test_puppeteer/player = new(src) + var/datum/test_puppeteer/target = player.spawn_puppet_nearby() + player.spawn_obj_in_hand(/obj/item/melee/baton/loaded) + + // Prod with inactive baton. + player.set_intent("help") + player.click_on(target) + TEST_ASSERT_EQUAL(FALSE, target.puppet.getStaminaLoss(), "Inactive baton delt stamina damage.") + TEST_ASSERT_EQUAL(target.puppet.health, target.puppet.getMaxHealth(), "Inactive help intent baton did damage.") + + // Prod with active baton. + player.use_item_in_hand() + player.click_on(target) + TEST_ASSERT_NOTEQUAL(FALSE, target.puppet.getStaminaLoss(), "Active help baton did not deal stamina damage.") + TEST_ASSERT_EQUAL(target.puppet.health, target.puppet.getMaxHealth(), "Active help intent baton did damage.") + + target.rejuvenate() + sleep(4 SECONDS) // Baton cooldown + anti baton timer. + + // Police brutality with active baton. + player.set_intent("harm") + player.click_on(target) + TEST_ASSERT_NOTEQUAL(FALSE, target.puppet.getStaminaLoss(), "Active harm baton did not deal stamina damage.") + TEST_ASSERT_NOTEQUAL(target.puppet.health, target.puppet.getMaxHealth(), "harm baton did no damage.") diff --git a/code/tests/game_tests.dm b/code/tests/game_tests.dm index ff155d0c96f..9795c4ccb9a 100644 --- a/code/tests/game_tests.dm +++ b/code/tests/game_tests.dm @@ -12,6 +12,7 @@ #include "atmos\test_ventcrawl.dm" #include "attack_chain\test_attack_chain_cult_dagger.dm" #include "attack_chain\test_attack_chain_machinery.dm" +#include "attack_chain\test_attack_chain_stunbaton.dm" #include "attack_chain\test_attack_chain_turf.dm" #include "attack_chain\test_attack_chain_vehicles.dm" #include "games\test_cards.dm"