From f1998f5aaa7aed6b6118f7d79bf9fa29e4cb6624 Mon Sep 17 00:00:00 2001 From: san7890 Date: Sat, 12 Aug 2023 15:35:25 -0600 Subject: [PATCH] Refactors Morphs into Basic Mobs (there is now a swag action for morphification) (#77503) I was bored, so did this. Probably one of the neatest refactors I've done, sorry if there's some oddities because I was experimenting with some other stuff in this so just tell me to clean them up whenever I can. Anyways, morphs are basic mobs now. We are able to easily refactor the whole "eat items and corpses" stuff in the basic mob framework, but the whole "morph into objects and people" turned out to be a bit trickier. That was easily rectified with a datum mob cooldown action and copy-pasting the old code into that code, as well as doing some nice stuff with traits and signals to ensure the one-way communication from the action to the mob. Old Morph AI didn't seem to be existant whatsoever, they inappropriately leveraged some old procs and I have no idea how to make it work with new AI. They DEFINITELY don't spawn outside of admin interference/ the event anymore, and will always be controlled by a player, so this shouldn't be too bad of an issue. I gave them something to seem alive just in case though, but I think adding legitimate prop-hunt AI would be such a laborious task that I am unwilling to do it in this PR. If admins want to add the ability for Ian to assume the form of the HoP, they can do that now! The datum action cooldown is quite nice for simple and basic mobs... but it is currently not compatible with carbons. That is not within scope for this PR, but I am dwelling on ways to extend it to carbon but they all sound really awfully bad. Also morphs are smarter, and we tick another simple animal in need of refactoring off the list. :cl: refactor: Morphs are now basic mobs with a nice new ability to help you change forms rather than the old shift-click method, much more intuitive. admin: With the morph rework comes a new ability you can add to mobs, "Assume Form". Feel free to add that to any simple or basic mob for le funnies as Runtime turns into a pen or something. /:cl: ~~Does anyone know if there's a (sane) way to alias a cooldown action as a keypress? I can't think of a good way to retain the old shift-click functionality, because that does feel _kinda_ nice, but I think it can be lived without.~~ I added it. Kinda fugly but whatever. --- code/__DEFINES/dcs/signals/signals_action.dm | 3 + .../signals_atom/signals_atom_mouse.dm | 2 +- code/__DEFINES/traits.dm | 19 +- code/_onclick/click.dm | 3 +- code/datums/actions/mobs/assume_form.dm | 87 +++++++ code/datums/diseases/transformation.dm | 2 +- code/datums/memory/_memory.dm | 9 +- code/modules/events/ghost_role/morph_event.dm | 12 +- .../ruins/objects_and_mobs/sin_ruins.dm | 2 +- .../mob/living/basic/space_fauna/morph.dm | 213 ++++++++++++++++++ code/modules/mob/living/living.dm | 9 +- .../mob/living/simple_animal/hostile/morph.dm | 195 ---------------- tgstation.dme | 3 +- .../tgui/interfaces/AntagInfoMorph.tsx | 14 +- .../Scripts/77503_simple_to_basic_morph.txt | 1 + 15 files changed, 355 insertions(+), 219 deletions(-) create mode 100644 code/datums/actions/mobs/assume_form.dm create mode 100644 code/modules/mob/living/basic/space_fauna/morph.dm delete mode 100644 code/modules/mob/living/simple_animal/hostile/morph.dm create mode 100644 tools/UpdatePaths/Scripts/77503_simple_to_basic_morph.txt diff --git a/code/__DEFINES/dcs/signals/signals_action.dm b/code/__DEFINES/dcs/signals/signals_action.dm index f928975813cb08..27206e483c502a 100644 --- a/code/__DEFINES/dcs/signals/signals_action.dm +++ b/code/__DEFINES/dcs/signals/signals_action.dm @@ -42,3 +42,6 @@ ///From /datum/action/vehicle/sealed/mecha/mech_toggle_safeties/proc/update_action_icon(): () #define COMSIG_MECH_SAFETIES_TOGGLE "mech_safeties_toggle" + +/// From /datum/action/cooldown/mob_cooldown/assume_form/proc/assume_appearances(), sent to the action owner: (atom/movable/target) +#define COMSIG_ACTION_DISGUISED_APPEARANCE "mob_ability_disguise_appearance" diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_mouse.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_mouse.dm index 2f8ebc836650da..2312c42baa4e5f 100644 --- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_mouse.dm +++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_mouse.dm @@ -8,7 +8,7 @@ #define COMSIG_CLICK "atom_click" ///from base of atom/ShiftClick(): (/mob) #define COMSIG_CLICK_SHIFT "shift_click" - #define COMPONENT_ALLOW_EXAMINATE (1<<0) //Allows the user to examinate regardless of client.eye. + #define COMPONENT_ALLOW_EXAMINATE (1<<0) //! Allows the user to examinate regardless of client.eye. ///from base of atom/CtrlClickOn(): (/mob) #define COMSIG_CLICK_CTRL "ctrl_click" ///from base of atom/AltClick(): (/mob) diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index a7acdcde15cc82..f6f14810cc5805 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -228,9 +228,26 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_NOBLOOD "noblood" #define TRAIT_NOMETABOLISM "no_metabolism" // Use when you want a mob to be able to metabolize plasma temporarily (e.g. plasma fixation disease symptom) -#define TRAIT_PLASMA_LOVER_METABOLISM "plasma_lover_metabolism" +/// This just means that the carbon will always have functional liverless metabolism +#define TRAIT_LIVERLESS_METABOLISM "liverless_metabolism" +/// Humans with this trait cannot be turned into zombies +#define TRAIT_NO_ZOMBIFY "no_zombify" +/// Humans with this trait cannot be affected by changeling transformation stings +#define TRAIT_NO_TRANSFORMATION_STING "no_transformation_sting" +/// Carbons with this trait can't have their DNA copied by diseases nor changelings +#define TRAIT_NO_DNA_COPY "no_dna_copy" +/// Carbons with this trait can eat blood to regenerate their own blood volume, instead of injecting it +#define TRAIT_DRINKS_BLOOD "drinks_blood" +/// Mob is immune to clone (cellular) damage #define TRAIT_NOCLONELOSS "no_cloneloss" +/// Mob is immune to toxin damage #define TRAIT_TOXIMMUNE "toxin_immune" +/// Mob is immune to oxygen damage, does not need to breathe +#define TRAIT_NOBREATH "no_breath" +/// Mob is currently disguised as something else (like a morph being another mob or an object). Holds a reference to the thing that applied the trait. +#define TRAIT_DISGUISED "disguised" +/// Use when you want a mob to be able to metabolize plasma temporarily (e.g. plasma fixation disease symptom) +#define TRAIT_PLASMA_LOVER_METABOLISM "plasma_lover_metabolism" #define TRAIT_EASYDISMEMBER "easy_dismember" #define TRAIT_LIMBATTACHMENT "limb_attach" #define TRAIT_NOLIMBDISABLE "no_limb_disable" diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index 04327f1c214f7a..791e71addf40b2 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -343,9 +343,10 @@ /atom/proc/ShiftClick(mob/user) var/flags = SEND_SIGNAL(user, COMSIG_CLICK_SHIFT, src) + if(flags & COMSIG_MOB_CANCEL_CLICKON) + return if(user.client && (user.client.eye == user || user.client.eye == user.loc || flags & COMPONENT_ALLOW_EXAMINATE)) user.examinate(src) - return /** * Ctrl click diff --git a/code/datums/actions/mobs/assume_form.dm b/code/datums/actions/mobs/assume_form.dm new file mode 100644 index 00000000000000..b10a91c5d65a00 --- /dev/null +++ b/code/datums/actions/mobs/assume_form.dm @@ -0,0 +1,87 @@ +/// Allows a mob to assume the form of another item or mob. +/// Warning, this will likely shit the bricks if you add this action to anything more sophisticated than a basic mob- this isn't built for anything carbon-wise. +/datum/action/cooldown/mob_cooldown/assume_form + name = "Assume Form" + desc = "Choose something that you wish to blend into the environment as. Click on yourself to reset your appearance." + button_icon_state = "sniper_zoom" + background_icon_state = "bg_alien" + overlay_icon_state = "bg_alien_border" + ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' + check_flags = AB_CHECK_CONSCIOUS + cooldown_time = 1.5 SECONDS + + /// Stuff that we can not disguise as. + var/static/list/blacklist_typecache = typecacheof(list( + /atom/movable/screen, + /obj/effect, + /obj/energy_ball, + /obj/narsie, + /obj/singularity, + )) + +/datum/action/cooldown/mob_cooldown/assume_form/Grant(mob/grant_to) + . = ..() + RegisterSignal(owner, COMSIG_LIVING_DEATH, PROC_REF(reset_appearances)) + +/datum/action/cooldown/mob_cooldown/assume_form/Remove(mob/remove_from) + reset_appearances() + UnregisterSignal(owner, COMSIG_LIVING_DEATH) + return ..() + +/datum/action/cooldown/mob_cooldown/assume_form/Activate(atom/target_atom) + StartCooldown(360 SECONDS, 360 SECONDS) + determine_intent(target_atom) + StartCooldown() + return TRUE + +/// Rapid proc to test if we can assume the form of a given atom. Returns TRUE if we can, FALSE if we can't. Done like this so we can be nice and explicit. +/datum/action/cooldown/mob_cooldown/assume_form/proc/can_assume_form(atom/target_atom) + if(is_type_in_typecache(target_atom, blacklist_typecache) || (!isobj(target_atom) && !ismob(target_atom))) + return FALSE + + return TRUE + +/// Determines what our user meant by their action. If they clicked on themselves, we reset our appearance. Otherwise, we assume the appearance of the clicked-on item. +/datum/action/cooldown/mob_cooldown/assume_form/proc/determine_intent(atom/target_atom) + if(!can_assume_form(target_atom)) + return + + if(target_atom == owner) + reset_appearances() + return + + assume_appearances(target_atom) + +/// Assumes the appearance of a desired movable and applies it to our mob. Target is the movable in question. +/datum/action/cooldown/mob_cooldown/assume_form/proc/assume_appearances(atom/movable/target_atom) + owner.appearance = target_atom.appearance + owner.copy_overlays(target_atom) + owner.alpha = max(target_atom.alpha, 150) //fucking chameleons + owner.transform = initial(target_atom.transform) + owner.pixel_x = target_atom.base_pixel_x + owner.pixel_y = target_atom.base_pixel_y + + // important: do this at the very end because we might have SIGNAL_ADDTRAIT for this on the mob that's dependent on the above logic + SEND_SIGNAL(owner, COMSIG_ACTION_DISGUISED_APPEARANCE, target_atom) + ADD_TRAIT(owner, TRAIT_DISGUISED, REF(src)) + +/// Resets the appearances of the mob to the default. +/datum/action/cooldown/mob_cooldown/assume_form/proc/reset_appearances() + SIGNAL_HANDLER + + if(!HAS_TRAIT(owner, TRAIT_DISGUISED)) + return // in case we're being invoked on death and we aren't disguised (or we just click on ourselves randomly), no need to do this additional work. + + owner.animate_movement = SLIDE_STEPS + owner.maptext = null + owner.alpha = initial(owner.alpha) + owner.color = initial(owner.color) + owner.desc = initial(owner.desc) + + owner.name = initial(owner.name) + owner.icon = initial(owner.icon) + owner.icon_state = initial(owner.icon_state) + owner.cut_overlays() + + // important: do this very end because we might have SIGNAL_REMOVETRAIT for this on the mob that's dependent on the above logic + REMOVE_TRAIT(owner, TRAIT_DISGUISED, REF(src)) diff --git a/code/datums/diseases/transformation.dm b/code/datums/diseases/transformation.dm index 8f4e4069a773d5..0405b72e854e2e 100644 --- a/code/datums/diseases/transformation.dm +++ b/code/datums/diseases/transformation.dm @@ -314,7 +314,7 @@ stage3 = list(span_danger("Your appendages are melting away."), span_danger("Your limbs begin to lose their shape.")) stage4 = list(span_danger("You're ravenous.")) stage5 = list(span_danger("You have become a morph.")) - new_form = /mob/living/simple_animal/hostile/morph + new_form = /mob/living/basic/morph infectable_biotypes = MOB_ORGANIC|MOB_MINERAL|MOB_UNDEAD //magic! transformed_antag_datum = /datum/antagonist/morph diff --git a/code/datums/memory/_memory.dm b/code/datums/memory/_memory.dm index 4f9ff121fe8739..b13a0c8f99e951 100644 --- a/code/datums/memory/_memory.dm +++ b/code/datums/memory/_memory.dm @@ -235,18 +235,20 @@ /mob/living/basic/carp/magic/chaos, /mob/living/basic/cow, /mob/living/basic/cow/wisdom, - /mob/living/basic/giant_spider, - /mob/living/basic/giant_spider/hunter, - /mob/living/basic/mining/goliath, + /mob/living/basic/crab, /mob/living/basic/headslug, /mob/living/basic/killer_tomato, /mob/living/basic/lizard, + /mob/living/basic/mining/goliath, + /mob/living/basic/morph, /mob/living/basic/mouse, /mob/living/basic/mushroom, /mob/living/basic/pet/dog/breaddog, /mob/living/basic/pet/dog/corgi, /mob/living/basic/pet/dog/pug, /mob/living/basic/pet/fox, + /mob/living/basic/spider/giant, + /mob/living/basic/spider/giant/hunter, /mob/living/basic/statue, /mob/living/basic/stickman, /mob/living/basic/stickman/dog, @@ -258,7 +260,6 @@ /mob/living/simple_animal/hostile/blob/blobbernaut/independent, /mob/living/simple_animal/hostile/gorilla, /mob/living/simple_animal/hostile/megafauna/dragon/lesser, - /mob/living/simple_animal/hostile/morph, /mob/living/simple_animal/hostile/retaliate/goat, /mob/living/simple_animal/parrot, /mob/living/simple_animal/pet/cat, diff --git a/code/modules/events/ghost_role/morph_event.dm b/code/modules/events/ghost_role/morph_event.dm index 7c3a019515def1..e0b75119c282d4 100644 --- a/code/modules/events/ghost_role/morph_event.dm +++ b/code/modules/events/ghost_role/morph_event.dm @@ -26,13 +26,13 @@ if(isnull(spawn_loc)) return MAP_ERROR - var/mob/living/simple_animal/hostile/morph/S = new /mob/living/simple_animal/hostile/morph(spawn_loc) - player_mind.transfer_to(S) + var/mob/living/basic/morph/corpus_accipientis = new(spawn_loc) + player_mind.transfer_to(corpus_accipientis) player_mind.set_assigned_role(SSjob.GetJobType(/datum/job/morph)) player_mind.special_role = ROLE_MORPH player_mind.add_antag_datum(/datum/antagonist/morph) - SEND_SOUND(S, sound('sound/magic/mutate.ogg')) - message_admins("[ADMIN_LOOKUPFLW(S)] has been made into a morph by an event.") - S.log_message("was spawned as a morph by an event.", LOG_GAME) - spawned_mobs += S + SEND_SOUND(corpus_accipientis, sound('sound/magic/mutate.ogg')) + message_admins("[ADMIN_LOOKUPFLW(corpus_accipientis)] has been made into a morph by an event.") + corpus_accipientis.log_message("was spawned as a morph by an event.", LOG_GAME) + spawned_mobs += corpus_accipientis return SUCCESSFUL_SPAWN diff --git a/code/modules/mapfluff/ruins/objects_and_mobs/sin_ruins.dm b/code/modules/mapfluff/ruins/objects_and_mobs/sin_ruins.dm index acb351813cf094..c4c75293acb463 100644 --- a/code/modules/mapfluff/ruins/objects_and_mobs/sin_ruins.dm +++ b/code/modules/mapfluff/ruins/objects_and_mobs/sin_ruins.dm @@ -104,7 +104,7 @@ return TRUE else to_chat(H, span_warning("You're repulsed by even looking at [src]. Only a pig could force themselves to go through it.")) - if(istype(mover, /mob/living/simple_animal/hostile/morph)) + if(istype(mover, /mob/living/basic/morph)) return TRUE //can't be bothered to do sloth right now, will make later diff --git a/code/modules/mob/living/basic/space_fauna/morph.dm b/code/modules/mob/living/basic/space_fauna/morph.dm new file mode 100644 index 00000000000000..3f31f7f3735ac8 --- /dev/null +++ b/code/modules/mob/living/basic/space_fauna/morph.dm @@ -0,0 +1,213 @@ +/// The classic morph, Corpus Accipientis (or "The body of the recipient"). It's a blob that can disguise itself as other things simply put. +/mob/living/basic/morph + name = "morph" + real_name = "morph" + desc = "A revolting, pulsating pile of flesh." + speak_emote = list("gurgles") + icon = 'icons/mob/simple/animal.dmi' + icon_state = "morph" + icon_living = "morph" + icon_dead = "morph_dead" + combat_mode = TRUE + + mob_biotypes = MOB_BEAST + pass_flags = PASSTABLE + + maxHealth = 150 + health = 150 + habitable_atmos = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) + minimum_survivable_temperature = TCMB + + obj_damage = 50 + melee_damage_lower = 20 + melee_damage_upper = 20 + + // Oh you KNOW it's gonna be real green + lighting_cutoff_red = 10 + lighting_cutoff_green = 35 + lighting_cutoff_blue = 15 + + attack_verb_continuous = "glomps" + attack_verb_simple = "glomp" + attack_sound = 'sound/effects/blobattack.ogg' + attack_vis_effect = ATTACK_EFFECT_BITE //nom nom nom + butcher_results = list(/obj/item/food/meat/slab = 2) + + ai_controller = /datum/ai_controller/basic_controller/morph + + /// A weakref pointing to the form we are currently assumed as. + var/datum/weakref/form_weakref = null + /// A typepath pointing of the form we are currently assumed as. Remember, TYPEPATH!!! + var/atom/movable/form_typepath = null + /// The ability that allows us to disguise ourselves. + var/datum/action/cooldown/mob_cooldown/assume_form/disguise_ability = null + + /// How much damage are we doing while disguised? + var/melee_damage_disguised = 0 + /// Can we eat while disguised? + var/eat_while_disguised = FALSE + +/mob/living/basic/morph/Initialize(mapload) + . = ..() + ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT) + RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(pre_attack)) + RegisterSignal(src, COMSIG_CLICK_SHIFT, PROC_REF(trigger_ability)) + RegisterSignal(src, COMSIG_ACTION_DISGUISED_APPEARANCE, PROC_REF(on_disguise)) + RegisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_DISGUISED), PROC_REF(on_undisguise)) + + AddElement(/datum/element/ai_retaliate) + AddElement(/datum/element/content_barfer) + + disguise_ability = new(src) + disguise_ability.Grant(src) + +/mob/living/basic/morph/examine(mob/user) + if(!HAS_TRAIT(src, TRAIT_DISGUISED)) + return ..() + + var/atom/movable/form_reference = form_weakref.resolve() + if(!isnull(form_reference)) + . = form_reference.examine(user) + + if(get_dist(user, src) <= 3) // always add this because if the form_reference somehow nulls out we still want to have something look "weird" about an item when someone is close + . += span_warning("It doesn't look quite right...") + +/mob/living/basic/morph/med_hud_set_health() + if(isliving(form_typepath)) + return ..() + + //we hide medical hud while in regular state or an item + var/image/holder = hud_list[HEALTH_HUD] + holder.icon_state = null + +/mob/living/basic/morph/med_hud_set_status() + if(isliving(form_typepath)) + return ..() + + //we hide medical hud while in regular state or an item + var/image/holder = hud_list[STATUS_HUD] + holder.icon_state = null + +/mob/living/basic/morph/death(gibbed) + if(HAS_TRAIT(src, TRAIT_DISGUISED)) + visible_message( + span_warning("[src] twists and dissolves into a pile of green flesh!"), + span_userdanger("Your skin ruptures! Your flesh breaks apart! No disguise can ward off de--"), + ) + + return ..() + +/mob/living/basic/morph/can_track(mob/living/user) + if(!HAS_TRAIT(src, TRAIT_DISGUISED)) + return FALSE + return ..() + +/// Do some more logic for the morph when we disguise through the action. +/mob/living/basic/morph/proc/on_disguise(mob/living/basic/user, atom/movable/target) + SIGNAL_HANDLER + // We are now weaker + melee_damage_lower = melee_damage_disguised + melee_damage_upper = melee_damage_disguised + add_movespeed_modifier(/datum/movespeed_modifier/morph_disguised) + + med_hud_set_health() + med_hud_set_status() //we're an object honest + + visible_message( + span_warning("[src] suddenly twists and changes shape, becoming a copy of [target]!"), + span_notice("You twist your body and assume the form of [target]."), + ) + + form_weakref = WEAKREF(target) + form_typepath = target.type + +/// Do some more logic for the morph when we undisguise through the action. +/mob/living/basic/morph/proc/on_undisguise() + SIGNAL_HANDLER + visible_message( + span_warning("[src] suddenly collapses in on itself, dissolving into a pile of green flesh!"), + span_notice("You reform to your normal body."), + ) + + //Baseline stats + melee_damage_lower = initial(melee_damage_lower) + melee_damage_upper = initial(melee_damage_upper) + remove_movespeed_modifier(/datum/movespeed_modifier/morph_disguised) + + med_hud_set_health() + med_hud_set_status() //we are no longer an object + + form_weakref = null + form_typepath = null + +/// Alias for the disguise ability to be used as a keybind. +/mob/living/basic/morph/proc/trigger_ability(mob/living/basic/source, atom/target) + SIGNAL_HANDLER + + // linters hate this if it's not async for some reason even though nothing blocks + INVOKE_ASYNC(disguise_ability, TYPE_PROC_REF(/datum/action/cooldown, InterceptClickOn), caller = source, target = target) + return COMSIG_MOB_CANCEL_CLICKON + +/// Handles the logic for attacking anything. +/mob/living/basic/morph/proc/pre_attack(mob/living/basic/source, atom/target) + SIGNAL_HANDLER + + if(HAS_TRAIT(src, TRAIT_DISGUISED) && (melee_damage_disguised <= 0)) + balloon_alert(src, "can't attack while disguised!") + return COMPONENT_HOSTILE_NO_ATTACK + + if(isliving(target)) //Eat Corpses to regen health + var/mob/living/living_target = target + if(living_target.stat != DEAD) + return + + INVOKE_ASYNC(source, PROC_REF(eat), eatable = living_target, delay = 3 SECONDS, update_health = -50) + return COMPONENT_HOSTILE_NO_ATTACK + + if(isitem(target)) //Eat items just to be annoying + var/obj/item/item_target = target + if(item_target.anchored) + return + + INVOKE_ASYNC(source, PROC_REF(eat), eatable = item_target, delay = 2 SECONDS) + return COMPONENT_HOSTILE_NO_ATTACK + +/// Eat stuff. Delicious. Return TRUE if we ate something, FALSE otherwise. +/// Required: `eatable` is the thing (item or mob) that we are going to eat. +/// Optional: `delay` is the applicable time-based delay to pass into `do_after()` before the logic is ran. +/// Optional: `update_health` is an integer that will be added (or maybe subtracted if you're cruel) to our health after we eat something. Passed into `adjust_health()` so make sure what you pass in is accurate. +/mob/living/basic/morph/proc/eat(atom/movable/eatable, delay = 0 SECONDS, update_health = 0) + if(QDELETED(eatable) || eatable.loc == src) + return FALSE + + if(HAS_TRAIT(src, TRAIT_DISGUISED) && !eat_while_disguised) + balloon_alert(src, "can't eat while disguised!") + return FALSE + + balloon_alert(src, "eating...") + if((delay > 0 SECONDS) && !do_after(src, delay, target = eatable)) + return FALSE + + visible_message(span_warning("[src] swallows [eatable] whole!")) + eatable.forceMove(src) + if(update_health != 0) + adjust_health(update_health) + + return TRUE + +/// No fleshed out AI implementation, just something that make these fellers seem lively if they're just dropped into a station. +/// Only real human-powered intelligence is capable of playing prop hunt in SS13 (until further notice). +/datum/ai_controller/basic_controller/morph + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic, + ) + + ai_movement = /datum/ai_movement/basic_avoidance + idle_behavior = /datum/idle_behavior/idle_random_walk + + planning_subtrees = list( + /datum/ai_planning_subtree/target_retaliate, + /datum/ai_planning_subtree/simple_find_target, + /datum/ai_planning_subtree/attack_obstacle_in_path, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + ) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index a677389002caae..c556482778b0dd 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1416,18 +1416,20 @@ /mob/living/basic/carp/magic/chaos, /mob/living/basic/chicken, /mob/living/basic/cow, - /mob/living/basic/giant_spider, - /mob/living/basic/giant_spider/hunter, - /mob/living/basic/mining/goliath, + /mob/living/basic/crab, /mob/living/basic/headslug, /mob/living/basic/killer_tomato, /mob/living/basic/lizard, + /mob/living/basic/mining/goliath, + /mob/living/basic/morph, /mob/living/basic/mouse, /mob/living/basic/mushroom, /mob/living/basic/pet/dog/breaddog, /mob/living/basic/pet/dog/corgi, /mob/living/basic/pet/dog/pug, /mob/living/basic/pet/fox, + /mob/living/basic/spider/giant, + /mob/living/basic/spider/giant/hunter, /mob/living/basic/statue, /mob/living/basic/stickman, /mob/living/basic/stickman/dog, @@ -1437,7 +1439,6 @@ /mob/living/simple_animal/hostile/blob/blobbernaut/independent, /mob/living/simple_animal/hostile/gorilla, /mob/living/simple_animal/hostile/megafauna/dragon/lesser, - /mob/living/simple_animal/hostile/morph, /mob/living/simple_animal/hostile/retaliate/goat, /mob/living/simple_animal/hostile/blob/blobbernaut/independent, /mob/living/simple_animal/hostile/asteroid/basilisk/watcher, diff --git a/code/modules/mob/living/simple_animal/hostile/morph.dm b/code/modules/mob/living/simple_animal/hostile/morph.dm deleted file mode 100644 index 221c980cd6973d..00000000000000 --- a/code/modules/mob/living/simple_animal/hostile/morph.dm +++ /dev/null @@ -1,195 +0,0 @@ -/mob/living/simple_animal/hostile/morph - name = "morph" - real_name = "morph" - desc = "A revolting, pulsating pile of flesh." - speak_emote = list("gurgles") - emote_hear = list("gurgles") - icon = 'icons/mob/simple/animal.dmi' - icon_state = "morph" - icon_living = "morph" - icon_dead = "morph_dead" - istate = ISTATE_HARM|ISTATE_BLOCKING - stop_automated_movement = 1 - status_flags = CANPUSH - pass_flags = PASSTABLE - atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) - minbodytemp = 0 - maxHealth = 150 - health = 150 - healable = 0 - obj_damage = 50 - melee_damage_lower = 20 - melee_damage_upper = 20 - // Oh you KNOW it's gonna be real green - lighting_cutoff_red = 10 - lighting_cutoff_green = 35 - lighting_cutoff_blue = 15 - vision_range = 1 // Only attack when target is close - wander = FALSE - attack_verb_continuous = "glomps" - attack_verb_simple = "glomp" - attack_sound = 'sound/effects/blobattack.ogg' - attack_vis_effect = ATTACK_EFFECT_BITE //nom nom nom - butcher_results = list(/obj/item/food/meat/slab = 2) - - var/morphed = FALSE - var/melee_damage_disguised = 0 - var/eat_while_disguised = FALSE - var/atom/movable/form = null - var/static/list/blacklist_typecache = typecacheof(list( - /atom/movable/screen, - /obj/singularity, - /obj/energy_ball, - /obj/narsie, - /mob/living/simple_animal/hostile/morph, - /obj/effect, - )) - -/mob/living/simple_animal/hostile/morph/Initialize(mapload) - . = ..() - ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT) - AddElement(/datum/element/content_barfer) - -/mob/living/simple_animal/hostile/morph/examine(mob/user) - if(morphed) - . = form.examine(user) - if(get_dist(user,src) <= 3) - . += span_warning("It doesn't look quite right...") - else - . = ..() - -/mob/living/simple_animal/hostile/morph/med_hud_set_health() - if(morphed && !isliving(form)) - var/image/holder = hud_list[HEALTH_HUD] - holder.icon_state = null - return //we hide medical hud while morphed - ..() - -/mob/living/simple_animal/hostile/morph/med_hud_set_status() - if(morphed && !isliving(form)) - var/image/holder = hud_list[STATUS_HUD] - holder.icon_state = null - return //we hide medical hud while morphed - ..() - -/mob/living/simple_animal/hostile/morph/proc/allowed(atom/movable/A) // make it into property/proc ? not sure if worth it - return !is_type_in_typecache(A, blacklist_typecache) && (isobj(A) || ismob(A)) - -/mob/living/simple_animal/hostile/morph/proc/eat(atom/movable/A) - if(morphed && !eat_while_disguised) - to_chat(src, span_warning("You cannot eat anything while you are disguised!")) - return FALSE - if(A && A.loc != src) - visible_message(span_warning("[src] swallows [A] whole!")) - A.forceMove(src) - return TRUE - return FALSE - -/mob/living/simple_animal/hostile/morph/ShiftClickOn(atom/movable/A) - if(!stat) - if(A == src) - restore() - return - if(istype(A) && allowed(A)) - assume(A) - else - to_chat(src, span_warning("You need to be conscious to transform!")) - ..() - -/mob/living/simple_animal/hostile/morph/proc/assume(atom/movable/target) - morphed = TRUE - form = target - - visible_message(span_warning("[src] suddenly twists and changes shape, becoming a copy of [target]!"), \ - span_notice("You twist your body and assume the form of [target].")) - appearance = target.appearance - copy_overlays(target) - alpha = max(alpha, 150) //fucking chameleons - transform = initial(transform) - pixel_y = base_pixel_y - pixel_x = base_pixel_x - - //Morphed is weaker - melee_damage_lower = melee_damage_disguised - melee_damage_upper = melee_damage_disguised - add_movespeed_modifier(/datum/movespeed_modifier/morph_disguised) - - med_hud_set_health() - med_hud_set_status() //we're an object honest - return - -/mob/living/simple_animal/hostile/morph/proc/restore() - if(!morphed) - to_chat(src, span_warning("You're already in your normal form!")) - return - morphed = FALSE - form = null - alpha = initial(alpha) - color = initial(color) - desc = initial(desc) - animate_movement = SLIDE_STEPS - maptext = null - - visible_message(span_warning("[src] suddenly collapses in on itself, dissolving into a pile of green flesh!"), \ - span_notice("You reform to your normal body.")) - name = initial(name) - icon = initial(icon) - icon_state = initial(icon_state) - cut_overlays() - - //Baseline stats - melee_damage_lower = initial(melee_damage_lower) - melee_damage_upper = initial(melee_damage_upper) - remove_movespeed_modifier(/datum/movespeed_modifier/morph_disguised) - - med_hud_set_health() - med_hud_set_status() //we are not an object - -/mob/living/simple_animal/hostile/morph/death(gibbed) - if(morphed) - visible_message(span_warning("[src] twists and dissolves into a pile of green flesh!"), \ - span_userdanger("Your skin ruptures! Your flesh breaks apart! No disguise can ward off de--")) - restore() - ..() - -/mob/living/simple_animal/hostile/morph/Aggro() // automated only - ..() - if(morphed) - restore() - -/mob/living/simple_animal/hostile/morph/LoseAggro() - vision_range = initial(vision_range) - -/mob/living/simple_animal/hostile/morph/AIShouldSleep(list/possible_targets) - . = ..() - if(.) - var/list/things = list() - for(var/atom/movable/A in view(src)) - if(allowed(A)) - things += A - var/atom/movable/T = pick(things) - assume(T) - -/mob/living/simple_animal/hostile/morph/can_track(mob/living/user) - if(morphed) - return FALSE - return ..() - -/mob/living/simple_animal/hostile/morph/AttackingTarget() - if(morphed && !melee_damage_disguised) - to_chat(src, span_warning("You can not attack while disguised!")) - return - if(isliving(target)) //Eat Corpses to regen health - var/mob/living/L = target - if(L.stat == DEAD) - if(do_after(src, 30, target = L)) - if(eat(L)) - adjustHealth(-50) - return - else if(isitem(target)) //Eat items just to be annoying - var/obj/item/I = target - if(!I.anchored) - if(do_after(src, 20, target = I)) - eat(I) - return - return ..() diff --git a/tgstation.dme b/tgstation.dme index d5a09d4f3c897b..36c3631b35411c 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -777,6 +777,7 @@ #include "code\datums\actions\items\toggles.dm" #include "code\datums\actions\items\vortex_recall.dm" #include "code\datums\actions\mobs\adjust_vision.dm" +#include "code\datums\actions\mobs\assume_form.dm" #include "code\datums\actions\mobs\blood_warp.dm" #include "code\datums\actions\mobs\charge.dm" #include "code\datums\actions\mobs\charge_apc.dm" @@ -4020,6 +4021,7 @@ #include "code\modules\mob\living\basic\space_fauna\headslug.dm" #include "code\modules\mob\living\basic\space_fauna\killer_tomato.dm" #include "code\modules\mob\living\basic\space_fauna\lightgeist.dm" +#include "code\modules\mob\living\basic\space_fauna\morph.dm" #include "code\modules\mob\living\basic\space_fauna\mushroom.dm" #include "code\modules\mob\living\basic\space_fauna\spaceman.dm" #include "code\modules\mob\living\basic\space_fauna\carp\carp.dm" @@ -4275,7 +4277,6 @@ #include "code\modules\mob\living\simple_animal\hostile\hostile.dm" #include "code\modules\mob\living\simple_animal\hostile\illusion.dm" #include "code\modules\mob\living\simple_animal\hostile\mimic.dm" -#include "code\modules\mob\living\simple_animal\hostile\morph.dm" #include "code\modules\mob\living\simple_animal\hostile\nanotrasen.dm" #include "code\modules\mob\living\simple_animal\hostile\ooze.dm" #include "code\modules\mob\living\simple_animal\hostile\pirate.dm" diff --git a/tgui/packages/tgui/interfaces/AntagInfoMorph.tsx b/tgui/packages/tgui/interfaces/AntagInfoMorph.tsx index eb594cf036060e..fb98e1aa35b90a 100644 --- a/tgui/packages/tgui/interfaces/AntagInfoMorph.tsx +++ b/tgui/packages/tgui/interfaces/AntagInfoMorph.tsx @@ -23,7 +23,10 @@ export const AntagInfoMorph = (props, context) => {
...a shapeshifting abomination that can eat almost anything. You may take the form of anything you can see by{' '} - shift-clicking it. + + using your "Assume Form" ability on it. Shift-clicking + the object in question will also work. + {' '}  This process will alert any nearby observers. {' '} @@ -35,11 +38,14 @@ export const AntagInfoMorph = (props, context) => { {' '} You can attack any item or dead creature to consume it - -  creatures will restore your health. +  corpses will restore your health. {' '} Finally, you can restore yourself to your original form while - morphed by shift-clicking{' '} - yourself. + morphed by{' '} + + using the "Assume Form" ability on yourself. You can + also shift-click yourself. + {' '}
diff --git a/tools/UpdatePaths/Scripts/77503_simple_to_basic_morph.txt b/tools/UpdatePaths/Scripts/77503_simple_to_basic_morph.txt new file mode 100644 index 00000000000000..baa722be823b57 --- /dev/null +++ b/tools/UpdatePaths/Scripts/77503_simple_to_basic_morph.txt @@ -0,0 +1 @@ +/mob/living/simple_animal/hostile/morph : /mob/living/basic/morph{@OLD}