From 2b76197397b4241957e93a9779fdd9dfbada2688 Mon Sep 17 00:00:00 2001 From: Jacquerel Date: Sat, 25 Feb 2023 01:14:04 +0000 Subject: [PATCH] Makes Lesser Form into one ability & unit tests it (#73572) ## About The Pull Request Fixes #73491 Every time I have used this ability lately it's been fucked. It would vanish from my actions at arbitrary moments, and also sometimes transform me into a horrible monkey-man thing instead of a monkey. This is a shame because being able to become a monkey can be pretty fun, even if it makes you very vulnerable to being butchered. Refactoring it into being one action instead of two actions which add and remove each other fixes the part where the action just disappears. It reliably sticks between transformations now, regardless of whether or not they were voluntary. I also noticed that when I was turning into a monkey it wasn't dropping the changeling "fake clothes" outfit pieces I had on as a human, leading to a really fucked up looking monkey. I fixed this by adding `force = TRUE` in the drop to ground proc in the check for if the equipment you have is still valid after your species changes. I don't _think_ this has any side effects but I never do and then someone finds some. For good measure I also made all of the changeling equipment abilities which don't work if you are a monkey detect if you become a monkey and retract themselves. I also noticed that for a long time Last Resort has been trying and failing to give you Lesser Form (well, Human Form rather) as a Headcrab, so I fixed that and now you actually get the ability. Finally I did a _little_ bit of housekeeping in general on the changeling actions, mostly balloon alerts. I think these definitely need more attention than I gave them though. I left a lot of the `to_chat`s in place because many of them give information you want to be a little sticky, or refer back to in order to double check what you just did. I also added a unit test which flips back and forth a few times to ensure the ability still works. This required adding an "instant" flag to the monkeyize/humanize procs to skip the timers, and idenitified a couple of weird issues. First point: Humanising a monkey would remove the monkey mutation and then call humanise again, which would not skip itself because it still regarded you as being a monkey. I changed the order of operations here slightly so that it will early return. Second point: Calling `domutcheck` on `human/consistent` would runtime because we skip the bit which sets up any mutations in their DNA. This is a part of changeling transformation, so I just made it return instantly. ## Why It's Good For The Game You can use this ability again without getting stuck permanently as a monkey, or it just deleting itself from your list of abilities for no reason. Turning into a monkey with fake outfit pieces on won't turn you into an abomination. ## Changelog :cl: refactor: Changeling's Lesser Form is now one ability instead of two which keep swapping, which should consistently turn you back and forth without deleting itself from your action bar. fix: Hatching from an egg left by a Last Resort headcrab should correctly grant you Lesser Form in addition to your other abilities. fix: Turning into a monkey while using the Changeling space suit won't leave you as a monkey with a weird inflated head. qol: Using lesser form as a monkey with only one stored DNA profile will skip asking which profile you want and will simply transform you immediately into the only option. /:cl: --------- Co-authored-by: Zephyr <12817816+ZephyrTFA@users.noreply.github.com> --- code/datums/dna.dm | 7 +- .../antagonists/changeling/changeling.dm | 2 +- .../changeling/changeling_power.dm | 20 ++--- .../antagonists/changeling/powers/absorb.dm | 8 +- .../changeling/powers/augmented_eyesight.dm | 4 +- .../changeling/powers/biodegrade.dm | 2 +- .../changeling/powers/fakedeath.dm | 2 +- .../changeling/powers/fleshmend.dm | 2 +- .../changeling/powers/humanform.dm | 61 ------------- .../changeling/powers/lesserform.dm | 81 +++++++++++------ .../changeling/powers/mutations.dm | 88 ++++++++++++------- .../changeling/powers/regenerate.dm | 2 +- .../changeling/powers/tiny_prick.dm | 8 +- code/modules/mob/living/carbon/human/dummy.dm | 3 + .../mob/living/carbon/human/species.dm | 2 +- .../living/simple_animal/hostile/headcrab.dm | 4 +- code/modules/mob/transform_procs.dm | 13 ++- code/modules/unit_tests/_unit_tests.dm | 1 + code/modules/unit_tests/lesserform.dm | 23 +++++ tgstation.dme | 1 - 20 files changed, 178 insertions(+), 156 deletions(-) delete mode 100644 code/modules/antagonists/changeling/powers/humanform.dm create mode 100644 code/modules/unit_tests/lesserform.dm diff --git a/code/datums/dna.dm b/code/datums/dna.dm index 0237d4c3590..7d58d1de2d6 100644 --- a/code/datums/dna.dm +++ b/code/datums/dna.dm @@ -480,11 +480,12 @@ GLOBAL_LIST_INIT(total_uf_len_by_block, populate_total_uf_len_by_block()) return death_sound = new_race.death_sound - if (dna.species.properly_gained) - dna.species.on_species_loss(src, new_race, pref_load) - var/datum/species/old_species = dna.species dna.species = new_race + + if (old_species.properly_gained) + old_species.on_species_loss(src, new_race, pref_load) + dna.species.on_species_gain(src, old_species, pref_load) if(ishuman(src)) qdel(language_holder) diff --git a/code/modules/antagonists/changeling/changeling.dm b/code/modules/antagonists/changeling/changeling.dm index 89a61825946..d8d2cb8e5c8 100644 --- a/code/modules/antagonists/changeling/changeling.dm +++ b/code/modules/antagonists/changeling/changeling.dm @@ -321,7 +321,7 @@ var/cap_to = isnum(override_cap) ? override_cap : total_chem_storage chem_charges = clamp(chem_charges + amount, 0, cap_to) - lingchemdisplay.maptext = FORMAT_CHEM_CHARGES_TEXT(chem_charges) + lingchemdisplay?.maptext = FORMAT_CHEM_CHARGES_TEXT(chem_charges) /* * Remove changeling powers from the current Changeling's purchased_powers list. diff --git a/code/modules/antagonists/changeling/changeling_power.dm b/code/modules/antagonists/changeling/changeling_power.dm index 3c16ae3ebe2..9ae7ac977be 100644 --- a/code/modules/antagonists/changeling/changeling_power.dm +++ b/code/modules/antagonists/changeling/changeling_power.dm @@ -69,21 +69,21 @@ the same goes for Remove(). if you override Remove(), call parent or else your p /datum/action/changeling/proc/can_sting(mob/living/user, mob/living/target) if(!can_be_used_by(user)) return FALSE - var/datum/antagonist/changeling/c = user.mind.has_antag_datum(/datum/antagonist/changeling) - if(c.chem_charges < chemical_cost) - to_chat(user, span_warning("We require at least [chemical_cost] unit\s of chemicals to do that!")) + var/datum/antagonist/changeling/changeling = user.mind.has_antag_datum(/datum/antagonist/changeling) + if(changeling.chem_charges < chemical_cost) + user.balloon_alert(user, "needs [chemical_cost] chemicals!") return FALSE - if(c.absorbed_count < req_dna) - to_chat(user, span_warning("We require at least [req_dna] sample\s of compatible DNA.")) + if(changeling.absorbed_count < req_dna) + user.balloon_alert(user, "needs [req_dna] dna sample\s!") return FALSE - if(c.true_absorbs < req_absorbs) - to_chat(user, span_warning("We require at least [req_absorbs] sample\s of DNA gained through our Absorb ability.")) + if(changeling.true_absorbs < req_absorbs) + user.balloon_alert(user, "needs [req_absorbs] absorption\s!") return FALSE if(req_stat < user.stat) - to_chat(user, span_warning("We are incapacitated.")) + user.balloon_alert(user, "incapacitated!") return FALSE if((HAS_TRAIT(user, TRAIT_DEATHCOMA)) && (!ignores_fakedeath)) - to_chat(user, span_warning("We are incapacitated.")) + user.balloon_alert(user, "playing dead!") return FALSE return TRUE @@ -93,6 +93,6 @@ the same goes for Remove(). if you override Remove(), call parent or else your p if(!ishuman(user)) return FALSE if(req_human && ismonkey(user)) - to_chat(user, span_warning("This ability requires you be in human form!")) + user.balloon_alert(user, "become human!") return FALSE return TRUE diff --git a/code/modules/antagonists/changeling/powers/absorb.dm b/code/modules/antagonists/changeling/powers/absorb.dm index 3e6e952ce7f..9a295fc8ac2 100644 --- a/code/modules/antagonists/changeling/powers/absorb.dm +++ b/code/modules/antagonists/changeling/powers/absorb.dm @@ -13,14 +13,14 @@ return if(is_absorbing) - to_chat(owner, span_warning("We are already absorbing!")) + owner.balloon_alert(owner, "already absorbing!") return if(!owner.pulling || !iscarbon(owner.pulling)) - to_chat(owner, span_warning("We must be grabbing a creature to absorb them!")) + owner.balloon_alert(owner, "needs grab!") return if(owner.grab_state <= GRAB_NECK) - to_chat(owner, span_warning("We must have a tighter grip to absorb this creature!")) + owner.balloon_alert(owner, "needs tighter grip!") return var/mob/living/carbon/target = owner.pulling @@ -161,7 +161,7 @@ SSblackbox.record_feedback("nested tally", "changeling_powers", 1, list("Absorb DNA", "[absorbing_iteration]")) if(!do_after(owner, 15 SECONDS, target)) - to_chat(owner, span_warning("Our absorption of [target] has been interrupted!")) + owner.balloon_alert(owner, "interrupted!") is_absorbing = FALSE return FALSE return TRUE diff --git a/code/modules/antagonists/changeling/powers/augmented_eyesight.dm b/code/modules/antagonists/changeling/powers/augmented_eyesight.dm index 29ce2f94113..afc4f441c63 100644 --- a/code/modules/antagonists/changeling/powers/augmented_eyesight.dm +++ b/code/modules/antagonists/changeling/powers/augmented_eyesight.dm @@ -37,8 +37,8 @@ active = FALSE user.update_sight() else - to_chat(user, "We can't adjust our eyes if we don't have any!") - return 1 + user.balloon_alert(user, "no eyes!") + return TRUE /datum/action/changeling/augmented_eyesight/Remove(mob/user) //Get rid of x-ray vision and flash protection when the user refunds this ability diff --git a/code/modules/antagonists/changeling/powers/biodegrade.dm b/code/modules/antagonists/changeling/powers/biodegrade.dm index da8cce9607b..16bd707831b 100644 --- a/code/modules/antagonists/changeling/powers/biodegrade.dm +++ b/code/modules/antagonists/changeling/powers/biodegrade.dm @@ -10,7 +10,7 @@ /datum/action/changeling/biodegrade/sting_action(mob/living/carbon/human/user) var/used = FALSE // only one form of shackles removed per use if(!HAS_TRAIT(user, TRAIT_RESTRAINED) && !user.legcuffed && isopenturf(user.loc)) - to_chat(user, span_warning("We are already free!")) + user.balloon_alert(user, "already free!") return FALSE if(user.handcuffed) diff --git a/code/modules/antagonists/changeling/powers/fakedeath.dm b/code/modules/antagonists/changeling/powers/fakedeath.dm index c83345ffdd9..8166e23f310 100644 --- a/code/modules/antagonists/changeling/powers/fakedeath.dm +++ b/code/modules/antagonists/changeling/powers/fakedeath.dm @@ -63,7 +63,7 @@ /datum/action/changeling/fakedeath/can_sting(mob/living/user) if(HAS_TRAIT_FROM(user, TRAIT_DEATHCOMA, "changeling") && !revive_ready) - to_chat(user, span_warning("We are already reviving.")) + user.balloon_alert(user, "already reviving!") return if(!user.stat && !revive_ready) //Confirmation for living changelings if they want to fake their death switch(tgui_alert(usr,"Are we sure we wish to fake our own death?", "Feign Death", list("Yes", "No"))) diff --git a/code/modules/antagonists/changeling/powers/fleshmend.dm b/code/modules/antagonists/changeling/powers/fleshmend.dm index 2a9c89f1235..0f1b5d8f2f1 100644 --- a/code/modules/antagonists/changeling/powers/fleshmend.dm +++ b/code/modules/antagonists/changeling/powers/fleshmend.dm @@ -11,7 +11,7 @@ //Can be used whilst unconscious. /datum/action/changeling/fleshmend/sting_action(mob/living/user) if(user.has_status_effect(/datum/status_effect/fleshmend)) - to_chat(user, span_warning("We are already fleshmending!")) + user.balloon_alert(user, "already fleshmending!") return ..() to_chat(user, span_notice("We begin to heal rapidly.")) diff --git a/code/modules/antagonists/changeling/powers/humanform.dm b/code/modules/antagonists/changeling/powers/humanform.dm deleted file mode 100644 index d96323899da..00000000000 --- a/code/modules/antagonists/changeling/powers/humanform.dm +++ /dev/null @@ -1,61 +0,0 @@ -/datum/action/changeling/humanform - name = "Human Form" - desc = "We change into a human. Costs 5 chemicals." - button_icon_state = "human_form" - chemical_cost = 5 - req_dna = 1 - -//Transform into a human. -/datum/action/changeling/humanform/sting_action(mob/living/carbon/user) - if(user.movement_type & VENTCRAWLING) - to_chat(user, span_notice("We must exit the pipes before we can transform back!")) - return FALSE - var/datum/antagonist/changeling/changeling = user.mind.has_antag_datum(/datum/antagonist/changeling) - var/datum/changeling_profile/chosen_prof = changeling.select_dna() - if(!chosen_prof) - return - if(!user || user.notransform) - return FALSE - to_chat(user, span_notice("We transform our appearance.")) - ..() - changeling.purchased_powers -= src - Remove(user) - - var/datum/dna/chosen_dna = chosen_prof.dna - var/datum/species/chosen_species = chosen_dna.species - user.humanize(chosen_species) - - changeling.transform(user, chosen_prof) - user.regenerate_icons() - // Delete ourselves when we're done. - qdel(src) - return TRUE - -// Subtype used when a changeling uses lesser form. -/datum/action/changeling/humanform/from_monkey - desc = "We change back into a human. Costs 5 chemicals." - -/datum/action/changeling/humanform/from_monkey/Grant(mob/granted_to) - . = ..() - RegisterSignal(granted_to, COMSIG_MONKEY_HUMANIZE, PROC_REF(give_lesserform)) - -/datum/action/changeling/humanform/from_monkey/Remove(mob/remove_from) - UnregisterSignal(remove_from, COMSIG_MONKEY_HUMANIZE) - return ..() - -/** - * Called on COMSIG_MONKEY_HUMANIZE - * Handles giving the new lesserform ability - * Removing ourselves is handled by parent already, so it's not needed here like it is on lesserform. - * - * Args: - * source - Monkey user who is now turning into a human - */ -/datum/action/changeling/humanform/from_monkey/proc/give_lesserform(mob/living/carbon/source) - SIGNAL_HANDLER - - var/datum/antagonist/changeling/changeling = source.mind.has_antag_datum(/datum/antagonist/changeling) - var/datum/action/changeling/lesserform/monkey_form_ability = new() - changeling.purchased_powers += monkey_form_ability - - monkey_form_ability.Grant(source) diff --git a/code/modules/antagonists/changeling/powers/lesserform.dm b/code/modules/antagonists/changeling/powers/lesserform.dm index acac97322ea..f4aab1c8968 100644 --- a/code/modules/antagonists/changeling/powers/lesserform.dm +++ b/code/modules/antagonists/changeling/powers/lesserform.dm @@ -5,14 +5,17 @@ button_icon_state = "lesser_form" chemical_cost = 5 dna_cost = 1 - req_human = TRUE + /// Whether to allow the transformation animation to play + var/transform_instantly = FALSE /datum/action/changeling/lesserform/Grant(mob/granted_to) . = ..() - RegisterSignal(granted_to, COMSIG_HUMAN_MONKEYIZE, PROC_REF(swap_powers)) + if (!owner) + return + RegisterSignals(granted_to, list(COMSIG_HUMAN_MONKEYIZE, COMSIG_MONKEY_HUMANIZE), PROC_REF(changed_form)) /datum/action/changeling/lesserform/Remove(mob/remove_from) - UnregisterSignal(remove_from, COMSIG_HUMAN_MONKEYIZE) + UnregisterSignal(remove_from, list(COMSIG_HUMAN_MONKEYIZE, COMSIG_MONKEY_HUMANIZE)) return ..() //Transform into a monkey. @@ -20,34 +23,54 @@ if(!user || user.notransform) return FALSE ..() - to_chat(user, span_warning("Our genes cry out!")) - user.monkeyize() + return ismonkey(user) ? unmonkey(user) : become_monkey(user) + +/// Stop being a monkey +/datum/action/changeling/lesserform/proc/unmonkey(mob/living/carbon/human/user) + if(user.movement_type & VENTCRAWLING) + user.balloon_alert(user, "can't transform in pipes!") + return FALSE + var/datum/antagonist/changeling/changeling = user.mind.has_antag_datum(/datum/antagonist/changeling) + var/datum/changeling_profile/chosen_form = select_form(changeling, user) + if(!chosen_form) + return FALSE + to_chat(user, span_notice("We transform our appearance.")) + var/datum/dna/chosen_dna = chosen_form.dna + var/datum/species/chosen_species = chosen_dna.species + user.humanize(species = chosen_species, instant = transform_instantly) + + changeling.transform(user, chosen_form) + user.regenerate_icons() return TRUE -/** - * Called on COMSIG_HUMAN_MONKEYIZE - * Handles giving the new human force ability and removing ourselves - * - * Args: - * source - Human user who is now turning into a monkey - */ -/datum/action/changeling/lesserform/proc/swap_powers(mob/living/carbon/source) +/// Returns the form to transform back into, automatically selects your only profile if you only have one +/datum/action/changeling/lesserform/proc/select_form(datum/antagonist/changeling/changeling, mob/living/carbon/human/user) + if (!changeling) + return + if (length(changeling.stored_profiles) == 1) + return changeling.first_profile + return changeling?.select_dna() + +/// Become a monkey +/datum/action/changeling/lesserform/proc/become_monkey(mob/living/carbon/human/user) + to_chat(user, span_warning("Our genes cry out!")) + user.monkeyize(instant = transform_instantly) + return TRUE + +/// Called when you become a human or monkey, whether or not it was voluntary +/datum/action/changeling/lesserform/proc/changed_form() SIGNAL_HANDLER + build_all_button_icons(update_flags = UPDATE_BUTTON_NAME | UPDATE_BUTTON_ICON) - var/datum/antagonist/changeling/changeling = source.mind.has_antag_datum(/datum/antagonist/changeling) - // Drops all flesh disguise items after monkeyizing, because they don't drop automatically like real clothing. - for(var/slot in changeling.slot2type) - if(istype(source.vars[slot], changeling.slot2type[slot])) - qdel(source.vars[slot]) - for(var/datum/scar/iter_scar as anything in source.all_scars) - if(iter_scar.fake) - qdel(iter_scar) - source.regenerate_icons() +/datum/action/changeling/lesserform/update_button_name(atom/movable/screen/movable/action_button/button, force) + if (ismonkey(owner)) + name = "Human Form" + desc = "We change back into a human. Costs 5 chemicals." + else + name = initial(name) + desc = initial(desc) + return ..() - var/datum/action/changeling/humanform/from_monkey/human_form_ability = new() - changeling.purchased_powers += human_form_ability - changeling.purchased_powers -= src - - human_form_ability.Grant(source) - Remove(source) - qdel(src) +/datum/action/changeling/lesserform/apply_button_icon(atom/movable/screen/movable/action_button/current_button, force) + button_icon_state = ismonkey(owner) ? "human_form" : initial(button_icon_state) + return ..() diff --git a/code/modules/antagonists/changeling/powers/mutations.dm b/code/modules/antagonists/changeling/powers/mutations.dm index 01930613fd9..edea630e015 100644 --- a/code/modules/antagonists/changeling/powers/mutations.dm +++ b/code/modules/antagonists/changeling/powers/mutations.dm @@ -21,10 +21,32 @@ var/weapon_type var/weapon_name_simple +/datum/action/changeling/weapon/Grant(mob/granted_to) + . = ..() + if (!owner || !req_human) + return + RegisterSignal(granted_to, COMSIG_HUMAN_MONKEYIZE, PROC_REF(became_monkey)) + +/datum/action/changeling/weapon/Remove(mob/remove_from) + UnregisterSignal(remove_from, COMSIG_HUMAN_MONKEYIZE) + unequip_held(remove_from) + return ..() + +/// Remove weapons if we become a monkey +/datum/action/changeling/weapon/proc/became_monkey(mob/source) + SIGNAL_HANDLER + unequip_held(source) + +/// Removes weapon if it exists, returns true if we removed something +/datum/action/changeling/weapon/proc/unequip_held(mob/user) + var/found_weapon = FALSE + for(var/obj/item/held in user.held_items) + found_weapon = check_weapon(user, held) || found_weapon + return found_weapon + /datum/action/changeling/weapon/try_to_sting(mob/user, mob/target) - for(var/obj/item/I in user.held_items) - if(check_weapon(user, I)) - return + if (unequip_held(user)) + return ..(user, target) /datum/action/changeling/weapon/proc/check_weapon(mob/user, obj/item/hand_item) @@ -34,15 +56,15 @@ playsound(user, 'sound/effects/blobattack.ogg', 30, TRUE) user.visible_message(span_warning("With a sickening crunch, [user] reforms [user.p_their()] [weapon_name_simple] into an arm!"), span_notice("We assimilate the [weapon_name_simple] back into our body."), "