diff --git a/code/datums/elements/strippable.dm b/code/datums/elements/strippable.dm index d30135dcf2a..5c4385105c4 100644 --- a/code/datums/elements/strippable.dm +++ b/code/datums/elements/strippable.dm @@ -16,7 +16,9 @@ if(!isatom(target)) return ELEMENT_INCOMPATIBLE - RegisterSignal(target, COMSIG_DO_MOB_STRIP, PROC_REF(mouse_drop_onto)) + // TODO: override = TRUE because strippable can get reattached to dead mobs after + // revival. Will fix for basic mobs probably maybe. + RegisterSignal(target, COMSIG_DO_MOB_STRIP, PROC_REF(mouse_drop_onto), override = TRUE) src.items = items diff --git a/code/modules/martial_arts/martial.dm b/code/modules/martial_arts/martial.dm index fa82a4f47ee..209329cf7f7 100644 --- a/code/modules/martial_arts/martial.dm +++ b/code/modules/martial_arts/martial.dm @@ -73,7 +73,8 @@ streak += intent_to_streak(step) var/mob/living/carbon/human/owner = locateUID(owner_UID) if(istype(owner) && !QDELETED(owner)) - owner.hud_used.combo_display.update_icon(ALL, streak) + if(owner.hud_used) + owner.hud_used.combo_display.update_icon(ALL, streak) return check_combos(step, user, target, could_start_new_combo) return FALSE diff --git a/code/modules/mob/living/simple_animal/friendly/dog.dm b/code/modules/mob/living/simple_animal/friendly/dog.dm index 07b75d1cb4a..76774275d45 100644 --- a/code/modules/mob/living/simple_animal/friendly/dog.dm +++ b/code/modules/mob/living/simple_animal/friendly/dog.dm @@ -22,6 +22,7 @@ var/last_eaten = 0 footstep_type = FOOTSTEP_MOB_CLAW var/next_spin_message = 0 + var/razor_shave_delay = 5 SECONDS /mob/living/simple_animal/pet/dog/npc_safe(mob/user) return TRUE @@ -193,7 +194,7 @@ to_chat(user, "You can't shave this corgi, it doesn't have a fur coat!") return user.visible_message("[user] starts to shave [src] using \the [O].", "You start to shave [src] using \the [O]...") - if(do_after(user, 50, target = src)) + if(do_after(user, razor_shave_delay, target = src)) user.visible_message("[user] shaves [src]'s hair using \the [O].") playsound(loc, O.usesound, 20, TRUE) shaved = TRUE diff --git a/code/modules/mob/mob_misc_procs.dm b/code/modules/mob/mob_misc_procs.dm index 9ffcbc99bc4..b092e7b01cc 100644 --- a/code/modules/mob/mob_misc_procs.dm +++ b/code/modules/mob/mob_misc_procs.dm @@ -211,7 +211,9 @@ // Do not use this if someone is intentionally trying to hit a specific body part. // Use get_zone_with_miss_chance() for that. /proc/ran_zone(zone, probability = 80) - +#ifdef GAME_TESTS + probability = 100 +#endif zone = check_zone(zone) if(prob(probability)) diff --git a/code/modules/tgui/tgui_input/alert_input.dm b/code/modules/tgui/tgui_input/alert_input.dm index f349dd48c76..53bb23f56f8 100644 --- a/code/modules/tgui/tgui_input/alert_input.dm +++ b/code/modules/tgui/tgui_input/alert_input.dm @@ -20,8 +20,10 @@ var/client/client = user user = client.mob +#ifndef GAME_TESTS if(isnull(user.client)) return +#endif // A gentle nudge - you should not be using TGUI alert for anything other than a simple message. if(length(buttons) > 3) @@ -37,11 +39,15 @@ var/datum/tgui_alert/alert = new(user, message, title, buttons, timeout, autofocus, ui_state) +#ifdef GAME_TESTS + LAZYADD(GLOB.game_test_tguis[user.mind.key], alert) +#else alert.ui_interact(user) alert.wait() if(alert) . = alert.choice qdel(alert) +#endif /** * # tgui_alert @@ -87,6 +93,10 @@ SStgui.close_uis(src) state = null deltimer(deletion_timer) +#ifdef GAME_TESTS + for(var/test_user_key in GLOB.game_test_tguis) + GLOB.game_test_tguis[test_user_key] -= src +#endif return ..() /** diff --git a/code/tests/_game_test.dm b/code/tests/_game_test.dm index 24a5d2a32e9..f6413bd89cd 100644 --- a/code/tests/_game_test.dm +++ b/code/tests/_game_test.dm @@ -1,4 +1,5 @@ GLOBAL_LIST_EMPTY(game_test_chats) +GLOBAL_LIST_EMPTY(game_test_tguis) /// For advanced cases, fail unconditionally but don't return (so a test can return multiple results) #define TEST_FAIL(reason) (Fail(reason || "No reason", __FILE__, __LINE__)) @@ -21,6 +22,8 @@ GLOBAL_LIST_EMPTY(game_test_chats) #define TEST_ASSERT_NOT_CHATLOG(puppet, text) if(puppet.any_chatlog_has_text(text)) { return Fail("Didn't expect `[text]` in any chatlog but got [jointext(puppet.get_chatlogs(), "\n")]", __FILE__, __LINE__) } +#define TEST_ASSERT_SUBSTRING(haystack, needle) if(!findtextEx(haystack, needle)) { return Fail("`[needle]` not found in string `[haystack]`", __FILE__, __LINE__) } + /// Asserts that the two parameters passed are equal, fails otherwise /// Optionally allows an additional message in the case of a failure #define TEST_ASSERT_EQUAL(a, b, message) do { \ diff --git a/code/tests/_game_test_puppeteer.dm b/code/tests/_game_test_puppeteer.dm index 9074310ee69..25b956a75d0 100644 --- a/code/tests/_game_test_puppeteer.dm +++ b/code/tests/_game_test_puppeteer.dm @@ -129,6 +129,14 @@ if(istype(A, atom_type)) return A +/datum/test_puppeteer/proc/get_last_tgui() + if(!(puppet.mind.key in GLOB.game_test_tguis)) + return null + var/list/tgui_list = GLOB.game_test_tguis[puppet.mind.key] + if(!length(tgui_list)) + return null + return tgui_list[length(tgui_list)] + // No we don't technically need to put these things into an actual backpack and // so forth, we could just leave them lying around and teleport them to the // player but this keeps things realistic and may surface issues we wouldn't diff --git a/code/tests/attack_chain/test_attack_chain_borgs.dm b/code/tests/attack_chain/test_attack_chain_borgs.dm new file mode 100644 index 00000000000..5cf8721b786 --- /dev/null +++ b/code/tests/attack_chain/test_attack_chain_borgs.dm @@ -0,0 +1,36 @@ +/datum/game_test/attack_chain_borgs/Run() + var/datum/test_puppeteer/player = new(src) + player.puppet.name = "Player" + + var/mob/living/silicon/robot/borg = player.spawn_mob_nearby(/mob/living/silicon/robot) + borg.name = "BORGO" + + player.set_intent(INTENT_HARM) + player.click_on(borg) + TEST_ASSERT_LAST_CHATLOG(player, "but doesn't leave a dent") + + player.set_intent(INTENT_HELP) + player.spawn_obj_in_hand(/obj/item/card/id/roboticist) + player.click_on(borg) + TEST_ASSERT_LAST_CHATLOG(player, "You unlock BORGO's interface") + player.puppet.swap_hand() + var/obj/item/crowbar = player.spawn_obj_in_hand(/obj/item/crowbar) + crowbar.attack_verb = list("bashed") + player.click_on(borg) + TEST_ASSERT_LAST_CHATLOG(player, "You open the cover") + player.put_away(crowbar) + player.click_on(borg) + TEST_ASSERT_LAST_CHATLOG(player, "You remove the high-capacity power cell") + player.click_on(borg) + TEST_ASSERT_LAST_CHATLOG(player, "You insert the power cell") + player.retrieve(crowbar) + player.click_on(borg) + TEST_ASSERT_LAST_CHATLOG(player, "You close the cover") + player.puppet.swap_hand() + player.click_on(borg) + TEST_ASSERT_LAST_CHATLOG(player, "You lock BORGO's interface") + player.puppet.swap_hand() + player.set_intent(INTENT_HARM) + player.click_on(borg) + TEST_ASSERT_LAST_CHATLOG(player, "Player has bashed BORGO with the crowbar") + TEST_ASSERT(borg.health <= borg.maxHealth, "borg did not take damage") diff --git a/code/tests/attack_chain/test_attack_chain_mobs.dm b/code/tests/attack_chain/test_attack_chain_mobs.dm new file mode 100644 index 00000000000..fd69cfd3bb0 --- /dev/null +++ b/code/tests/attack_chain/test_attack_chain_mobs.dm @@ -0,0 +1,120 @@ +/datum/game_test/attack_chain_mobs/Run() + var/datum/test_puppeteer/player = new(src) + player.puppet.name = "Player" + // To ensure punches do damage without knockdowns + player.puppet.dna.species.punchdamagelow = 5 + player.puppet.dna.species.punchdamagehigh = 5 + + var/mob/living/simple_animal/hostile/morph/morph = player.spawn_mob_nearby(/mob/living/simple_animal/hostile/morph) + morph.ambush_prepared = TRUE + player.spawn_obj_in_hand(/obj/item/screwdriver) + player.click_on(morph) + + TEST_ASSERT_ANY_CHATLOG(player, "You try to use") + TEST_ASSERT_ANY_CHATLOG(player, "suddenly collapses in on itself") + qdel(morph) + // Get us back up + player.rejuvenate() + + var/datum/test_puppeteer/victim = player.spawn_puppet_nearby() + victim.puppet.name = "Victim" + player.click_on(victim) + TEST_ASSERT_LAST_CHATLOG(player, "You hug Victim") + player.set_intent(INTENT_HARM) + player.click_on(victim) + TEST_ASSERT_LAST_CHATLOG(player, "Player punched Victim") + victim.rejuvenate() + var/obj/knife = player.spawn_obj_in_hand(/obj/item/kitchen/knife) + player.puppet.zone_selected = BODY_ZONE_PRECISE_L_FOOT + player.click_on(victim) + TEST_ASSERT_LAST_CHATLOG(player, "Victim in the left foot") + victim.rejuvenate() + player.put_away(knife) + + // Test CQC attack interception + var/datum/martial_art/cqc/cqc = new() + cqc.teach(player.puppet) + player.puppet.swap_hand() + victim.puppet.lay_down() + player.click_on(victim) + TEST_ASSERT_LAST_CHATLOG(player, "stomps on Victim") + victim.rejuvenate() + cqc.remove(player.puppet) + + player.set_intent(INTENT_HELP) + var/mob/mining_drone = player.spawn_mob_nearby(/mob/living/simple_animal/hostile/mining_drone) + var/obj/scanner = player.spawn_obj_in_hand(/obj/item/mining_scanner) + player.click_on(mining_drone) + TEST_ASSERT_LAST_CHATLOG(player, "drop any collected ore.") + qdel(scanner) + player.spawn_obj_in_hand(/obj/item/borg/upgrade/modkit/cooldown/minebot) + player.click_on(mining_drone) + TEST_ASSERT_LAST_CHATLOG(player, "You install the modkit") + player.retrieve(knife) + player.set_intent(INTENT_HARM) + player.click_on(mining_drone) + TEST_ASSERT_LAST_CHATLOG(player, "the nanotrasen minebot with the kitchen knife") + qdel(mining_drone) + + RegisterSignal(victim.puppet, COMSIG_HUMAN_ATTACKED, PROC_REF(cancel_attack_chain)) + player.click_on(victim) + TEST_ASSERT_LAST_CHATLOG(player, "Attack chain cancelled by signal") + UnregisterSignal(victim.puppet, COMSIG_HUMAN_ATTACKED) + + var/mob/living/simple_animal/pet/dog/corgi/corgi = player.spawn_mob_nearby(/mob/living/simple_animal/pet/dog/corgi) + corgi.anchored = TRUE + player.click_on(corgi) + TEST_ASSERT_LAST_CHATLOG(player, "corgi with the kitchen knife") + player.put_away(knife) + corgi.rejuvenate() + player.set_intent(INTENT_HELP) + var/obj/collar = player.spawn_obj_in_hand(/obj/item/petcollar) + player.click_on(corgi) + TEST_ASSERT(collar in corgi.contents, "did not put collar on corgi") + var/obj/razor = player.spawn_obj_in_hand(/obj/item/razor) + corgi.razor_shave_delay = 0 + player.click_on(corgi) + TEST_ASSERT(corgi.shaved, "corgi was not shaved") + player.put_away(razor) + corgi.death() + var/obj/laz_injector = player.spawn_obj_in_hand(/obj/item/lazarus_injector) + player.click_on(corgi) + TEST_ASSERT_LAST_CHATLOG(player, "injects the corgi") + qdel(laz_injector) + + var/mob/slime = player.spawn_mob_nearby(/mob/living/simple_animal/slime) + slime.anchored = TRUE + + player.spawn_obj_in_hand(/obj/item/slimepotion/slime/docility) + player.click_on(corgi) + TEST_ASSERT_LAST_CHATLOG(player, "only works on slimes!") + player.click_on(slime) + TEST_ASSERT_LAST_CHATLOG(player, "You feed the slime the potion") + + var/obj/mind_transfer_slime_potion = player.spawn_obj_in_hand(/obj/item/slimepotion/transference) + player.click_on(corgi) + var/datum/tgui_alert/alert = player.get_last_tgui() + TEST_ASSERT_NOTNULL(alert, "no TGUI") + TEST_ASSERT_SUBSTRING(alert.message, "transfer your consciousness to the corgi") + alert.set_choice("No") + + player.click_on(slime) + alert = player.get_last_tgui() + TEST_ASSERT_NOTNULL(alert, "no TGUI") + TEST_ASSERT_SUBSTRING(alert.message, "transfer your consciousness to the pet slime") + alert.set_choice("No") + + qdel(mind_transfer_slime_potion) + qdel(corgi) + qdel(slime) + + victim.puppet.death() + player.spawn_obj_in_hand(/obj/item/kitchen/knife/butcher/meatcleaver) + player.set_intent(INTENT_HARM) + player.click_on(victim) + // Even before this test, butchering items included a second attack message + TEST_ASSERT_ANY_CHATLOG(player, "You hack off a chunk of meat from Victim") + +/datum/game_test/attack_chain_mobs/proc/cancel_attack_chain(datum/source, mob/user) + to_chat(user, "Attack chain cancelled by signal") + return COMPONENT_CANCEL_ATTACK_CHAIN diff --git a/code/tests/game_tests.dm b/code/tests/game_tests.dm index 9795c4ccb9a..7fa7da132d9 100644 --- a/code/tests/game_tests.dm +++ b/code/tests/game_tests.dm @@ -10,9 +10,11 @@ #ifdef GAME_TESTS #include "atmos\test_ventcrawl.dm" +#include "attack_chain\test_attack_chain_borgs.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_mobs.dm" #include "attack_chain\test_attack_chain_turf.dm" #include "attack_chain\test_attack_chain_vehicles.dm" #include "games\test_cards.dm"