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}