Converts a whole lot of friendly mobs to basic mob AI (#29815)

* Butterflies, Cockroaches, Slothes, Snakes, Penguins, and Bunny Basic Mobs

* Lizards too

* Oops

* Lizard emote keybind

* Updatepaths

* Apply suggestions from code review

Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>

* Moved lizard emote to basic_emotes.dm, simplified hide action

* New linters

* Merge conflict resolution

---------

Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>
Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
This commit is contained in:
PollardTheDragon
2025-08-06 01:17:13 +00:00
committed by GitHub
co-authored by Luc
parent 265003e919
commit 3b0a9c4381
48 changed files with 513 additions and 359 deletions
@@ -0,0 +1,8 @@
/datum/emote/lizard
mob_type_allowed_typecache = /mob/living/basic/lizard
mob_type_blacklist_typecache = list()
/datum/emote/lizard/whicker
key = "tongue"
message = "sticks its tongue out contentedly!"
emote_type = EMOTE_VISIBLE | EMOTE_AUDIBLE
@@ -175,6 +175,9 @@ RESTRICT_TYPE(/mob/living/basic)
apply_temperature_requirements()
if(step_type)
AddComponent(/datum/component/footstep, step_type)
if(can_hide)
var/datum/action/innate/hide/hide = new()
hide.Grant(src)
if(is_ranged)
AddComponent(/datum/component/ranged_attacks, projectile_type = projectile_type, projectile_sound = projectile_sound, burst_shots = ranged_burst_count, burst_intervals = ranged_burst_interval)
@@ -0,0 +1,17 @@
/mob/living/basic/pet
icon = 'icons/mob/pets.dmi'
mob_size = MOB_SIZE_SMALL
mob_biotypes = MOB_ORGANIC | MOB_BEAST
blood_volume = BLOOD_VOLUME_NORMAL
speed = 0 // same speed as a person.
hud_type = /datum/hud/corgi
/mob/living/basic/pet/item_interaction(mob/living/user, obj/item/O, list/modifiers)
if(!istype(O, /obj/item/newspaper))
return ..()
var/obj/item/newspaper/paper = O
if(stat != CONSCIOUS || !paper.rolled)
return ..()
user.visible_message("<span class='notice'>[user] baps [name] on the nose with the rolled up [O].</span>")
INVOKE_ASYNC(src, PROC_REF(spin), 7 DECISECONDS, 1)
return ITEM_INTERACT_COMPLETE
@@ -0,0 +1,64 @@
/mob/living/basic/bunny
name = "bunny"
real_name = "bunny"
desc = "Awww a cute bunny."
icon_state = "m_bunny"
icon_living = "m_bunny"
icon_dead = "bunny_dead"
icon_resting = "bunny_stretch"
faction = list("neutral", "jungle")
see_in_dark = 6
maxHealth = 10
health = 10
ai_controller = /datum/ai_controller/basic_controller/bunny
butcher_results = list(/obj/item/food/meat = 1)
response_help_continuous = "pets"
response_help_simple = "pet"
response_disarm_continuous = "gently pushes aside"
response_disarm_simple = "gently push aside"
response_harm_continuous = "kicks"
response_harm_simple = "kick"
density = FALSE
pass_flags = PASSTABLE | PASSGRILLE | PASSMOB
mob_biotypes = MOB_ORGANIC | MOB_BEAST
mob_size = MOB_SIZE_TINY
atmos_requirements = list("min_oxy" = 16, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0)
minimum_survivable_temperature = 223 // Below -50 Degrees Celcius
maximum_survivable_temperature = 323 // Above 50 Degrees Celcius
can_hide = TRUE
holder_type = /obj/item/holder/bunny
gold_core_spawnable = FRIENDLY_SPAWN
ventcrawler = VENTCRAWLER_ALWAYS
/mob/living/basic/bunny/Initialize(mapload)
. = ..()
AddElement(/datum/element/wears_collar)
AddElement(/datum/element/ai_retaliate)
/mob/living/basic/bunny/attack_hand(mob/living/carbon/human/M)
if(M.a_intent == INTENT_HELP)
get_scooped(M, TRUE)
return ..()
/mob/living/basic/bunny/syndi // for the syndicake factory bunny so its not being shot
faction = list("syndicate")
gold_core_spawnable = NO_SPAWN
/datum/ai_controller/basic_controller/bunny
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
)
ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/random_speech/rabbit,
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee,
/datum/ai_planning_subtree/flee_target,
)
/datum/ai_planning_subtree/random_speech/rabbit
speech_chance = 10
speak = list("Mrrp.", "CHIRP!", "Mrrp?") // rabbits make some weird noises dude i don't know what to tell you
emote_hear = list("hops.", "thumps.")
emote_see = list("hops around.", "bounces up and down.", "flips their ears up")
@@ -0,0 +1,33 @@
/mob/living/basic/butterfly
name = "butterfly"
desc = "A colorful butterfly, how'd it get up here?"
icon_state = "butterfly"
icon_living = "butterfly"
icon_dead = "butterfly_dead"
faction = list("neutral", "jungle")
response_help_continuous = "shoos"
response_help_simple = "shoo"
response_disarm_continuous = "brushes aside"
response_disarm_simple = "brush aside"
response_harm_continuous = "squashes"
response_harm_simple = "squash"
ai_controller = /datum/ai_controller/basic_controller/butterfly
maxHealth = 2
health = 2
harm_intent_damage = 1
density = FALSE
pass_flags = PASSTABLE | PASSGRILLE | PASSMOB
ventcrawler = VENTCRAWLER_ALWAYS
mob_size = MOB_SIZE_TINY
mob_biotypes = MOB_ORGANIC | MOB_BUG
butcher_results = list(/obj/item/food/meat = 0)
gold_core_spawnable = FRIENDLY_SPAWN
initial_traits = list(TRAIT_FLYING, TRAIT_EDIBLE_BUG)
/mob/living/basic/butterfly/Initialize(mapload)
. = ..()
color = rgb(rand(0, 255), rand(0, 255), rand(0, 255))
/datum/ai_controller/basic_controller/butterfly
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
@@ -0,0 +1,75 @@
/mob/living/basic/cockroach
name = "cockroach"
desc = "This station is just crawling with bugs."
icon_state = "cockroach"
icon_dead = "cockroach"
health = 1
maxHealth = 1
atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
minimum_survivable_temperature = 270
maximum_survivable_temperature = INFINITY
pass_flags = PASSTABLE | PASSGRILLE | PASSMOB
mob_biotypes = MOB_ORGANIC | MOB_BUG
mob_size = MOB_SIZE_TINY
ai_controller = /datum/ai_controller/basic_controller/cockroach
response_disarm_continuous = "shoos"
response_disarm_simple = "shoo"
response_harm_continuous = "splats"
response_harm_simple = "splat"
density = FALSE
ventcrawler = VENTCRAWLER_ALWAYS
gold_core_spawnable = FRIENDLY_SPAWN
loot = list(/obj/effect/decal/cleanable/insectguts)
basic_mob_flags = DEL_ON_DEATH
/// Chance to get squished
var/squish_chance = 50
/mob/living/basic/cockroach/can_die()
return ..() && !SSticker.cinematic // If the nuke is going off, then cockroaches are invincible. Keeps the nuke from killing them, cause cockroaches are immune to nukes.
/mob/living/basic/cockroach/Initialize(mapload) // Lizards are a great way to deal with cockroaches
. = ..()
ADD_TRAIT(src, TRAIT_EDIBLE_BUG, "edible_bug")
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_atom_entered)
)
AddElement(/datum/element/connect_loc, loc_connections)
/mob/living/basic/cockroach/proc/on_atom_entered(datum/source, atom/movable/entered)
if(isliving(entered))
var/mob/living/A = entered
if(A.mob_size > MOB_SIZE_SMALL)
if(prob(squish_chance))
A.visible_message("<span class='notice'>\The [A] squashed \the [name].</span>", "<span class='notice'>You squashed \the [name].</span>")
death()
else
visible_message("<span class='notice'>\The [name] avoids getting crushed.</span>")
else if(isstructure(entered))
visible_message("<span class='notice'>As \the [entered] moved over \the [name], it was crushed.</span>")
death()
/mob/living/basic/cockroach/ex_act() // Explosions are a terrible way to handle a cockroach.
return
// mining pet
/mob/living/basic/cockroach/brad
name = "Brad"
desc = "Lavaland's most resilient cockroach. Seeing this little guy walk through the wastes almost makes you wish for nuclear winter."
response_help_continuous = "carefully pets"
response_help_simple = "carefully pet"
weather_immunities = list("ash")
gold_core_spawnable = NO_SPAWN
/datum/ai_controller/basic_controller/cockroach
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
)
ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/find_and_hunt_target/roach,
)
/datum/ai_planning_subtree/find_and_hunt_target/roach
hunt_targets = list(/obj/effect/decal/cleanable/ants)
@@ -0,0 +1,105 @@
/mob/living/basic/lizard
name = "lizard"
desc = "A cute tiny lizard."
icon = 'icons/mob/critter.dmi'
icon_state = "lizard"
icon_living = "lizard"
icon_dead = "lizard-dead"
speak_emote = list("hisses")
health = 10
maxHealth = 10
attack_verb_continuous = "bites"
attack_verb_simple = "bite"
attack_sound = 'sound/effects/bite.ogg'
melee_damage_lower = 1
melee_damage_upper = 2
ai_controller = /datum/ai_controller/basic_controller/lizard
response_help_continuous = "pets"
response_help_simple = "pet"
response_disarm_continuous = "shoos"
response_disarm_simple = "shoo"
response_harm_continuous = "stomps on"
response_harm_simple = "stomp on"
faction = list("neutral", "jungle")
ventcrawler = VENTCRAWLER_ALWAYS
density = FALSE
pass_flags = PASSTABLE | PASSMOB
mob_size = MOB_SIZE_SMALL
can_hide = TRUE
pass_door_while_hidden = TRUE
butcher_results = list(/obj/item/food/meat = 1)
mob_biotypes = MOB_ORGANIC | MOB_BEAST | MOB_REPTILE
gold_core_spawnable = FRIENDLY_SPAWN
/// List of stuff that we want to eat
var/static/list/edibles = list(
/mob/living/basic/butterfly,
/mob/living/basic/cockroach,
/obj/structure/spider/spiderling
)
/// Lizards start with a tail
var/has_tail = TRUE
/mob/living/basic/lizard/Initialize(mapload)
. = ..()
AddElement(/datum/element/wears_collar)
AddElement(/datum/element/basic_eating, heal_amt_ = 5, food_types_ = edibles)
ai_controller.set_blackboard_key(BB_BASIC_FOODS, typecacheof(edibles))
/datum/ai_controller/basic_controller/lizard
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
)
ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/find_food,
/datum/ai_planning_subtree/random_speech/lizard,
)
/datum/ai_planning_subtree/random_speech/lizard // all of these have to be three words long or i'm killing you. you're dead.
speech_chance = 3
emote_hear = list("stamps around some.", "hisses a bit.")
emote_see = list("blehs the tongue.", "tilts the head.", "does a spin.")
/mob/living/basic/lizard/verb/lose_tail()
set name = "Lose tail"
set desc = "Allows you to lose your tail and escape a sticky situation. Takes 5 minutes for your tail to grow back."
set category = "Animal"
if(stat != CONSCIOUS)
return
if(!has_tail)
to_chat(usr, "<span class='warning'>You have no tail to shed!</span>")
return
for(var/obj/item/grab/G in usr.grabbed_by)
var/mob/living/carbon/M = G.assailant
usr.visible_message("<span class='warning'>[usr] suddenly sheds their tail and slips out of [M]'s grasp!</span>")
M.KnockDown(3 SECONDS) // FUCK I TRIPPED
playsound(usr.loc, "sound/misc/slip.ogg", 75, 1)
has_tail = FALSE
addtimer(CALLBACK(src, PROC_REF(new_tail)), 5 MINUTES)
/mob/living/basic/lizard/proc/new_tail()
has_tail = TRUE
to_chat(src, "<span class='notice'>You regrow your tail!</span>")
/mob/living/basic/lizard/decompile_act(obj/item/matter_decompiler/C, mob/user)
if(!isdrone(user))
user.visible_message("<span class='notice'>[user] sucks [src] into its decompiler. There's a horrible crunching noise.</span>", \
"<span class='warning'>It's a bit of a struggle, but you manage to suck [src] into your decompiler. It makes a series of visceral crunching noises.</span>")
new/obj/effect/decal/cleanable/blood/splatter(get_turf(src))
C.stored_comms["metal"] += 2 // having more metal than glass because blood has iron in it
C.stored_comms["glass"] += 1
qdel(src)
return TRUE
return ..()
/mob/living/basic/lizard/wags_his_tail
name = "Wags-His-Tail"
real_name = "Wags-His-Tail"
gold_core_spawnable = NO_SPAWN
/mob/living/basic/lizard/wags_his_tail/update_desc()
. = ..()
desc = "The official mascot of Janitalia."
@@ -0,0 +1,90 @@
// Penguins
/mob/living/basic/pet/penguin
name = "penguin"
real_name = "penguin"
response_help_continuous = "pets"
response_help_simple = "pet"
response_disarm_continuous = "bops"
response_disarm_simple = "bop"
response_harm_continuous = "kicks"
response_harm_simple = "kick"
speak_emote = list("squawks", "gakkers")
faction = list("penguin")
ai_controller = /datum/ai_controller/basic_controller/penguin
see_in_dark = 5
icon = 'icons/mob/penguins.dmi'
step_type = FOOTSTEP_MOB_BAREFOOT
/mob/living/basic/pet/penguin/Initialize(mapload)
. = ..()
AddElement(/datum/element/wears_collar)
AddElement(/datum/element/waddling)
/mob/living/basic/pet/penguin/emperor
name = "Emperor penguin"
desc = "Emperor of all he surveys."
icon_state = "penguin"
icon_living = "penguin"
icon_dead = "penguin_dead"
butcher_results = list()
gold_core_spawnable = FRIENDLY_SPAWN
/mob/living/basic/pet/penguin/eldrich
name = "Albino penguin"
desc = "Found in the depths of mountains."
response_help_continuous = "taps"
response_help_simple = "tap"
response_disarm_continuous = "pokes"
response_disarm_simple = "poke"
response_harm_continuous = "flails at"
response_harm_simple = "flail at"
icon_state = "penguin_elder"
icon_living = "penguin_elder"
icon_dead = "penguin_dead"
faction = list("penguin", "cult")
/mob/living/basic/pet/penguin/emperor/shamebrero
name = "Shamebrero penguin"
desc = "Shameful of all he surveys."
icon_state = "penguin_shamebrero"
icon_living = "penguin_shamebrero"
gold_core_spawnable = NO_SPAWN
/mob/living/basic/pet/penguin/baby
name = "Penguin chick"
desc = "Can't fly and can barely waddles, but the prince of all chicks."
icon_state = "penguin_baby"
icon_living = "penguin_baby"
icon_dead = "penguin_baby_dead"
density = FALSE
pass_flags = PASSMOB
/datum/ai_controller/basic_controller/penguin
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
)
ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee,
/datum/ai_planning_subtree/flee_target,
/datum/ai_planning_subtree/random_speech/penguin,
)
/datum/ai_planning_subtree/random_speech/penguin
speech_chance = 5
speak = list("Gah Gah!", "NOOT NOOT!", "NOOT!", "Noot", "noot", "Prah!", "Grah!")
emote_hear = list("squawks", "gakkers")
emote_see = list("shakes its beak.", "flaps it's wings.", "preens itself.")
/datum/ai_controller/basic_controller/penguin/eldrich
planning_subtrees = list(
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee,
/datum/ai_planning_subtree/flee_target,
/datum/ai_planning_subtree/random_speech/penguin/eldrich,
)
/datum/ai_planning_subtree/random_speech/penguin/eldrich
speak = list("Gah Gah!", "Tekeli-li! Tekeli-li!", "Tekeli-li!", "Teke", "li")
@@ -0,0 +1,55 @@
/mob/living/basic/pet/sloth
name = "sloth"
desc = "An adorable, sleepy creature."
icon_state = "sloth"
icon_living = "sloth"
icon_dead = "sloth_dead"
gender = MALE
speak_emote = list("yawns")
faction = list("neutral", "jungle")
butcher_results = list(/obj/item/food/meat = 3)
response_help_continuous = "pets"
response_help_simple = "pet"
response_disarm_continuous = "gently pushes aside"
response_disarm_simple = "gently push aside"
response_harm_continuous = "kicks"
response_harm_simple = "kick"
ai_controller = /datum/ai_controller/basic_controller/sloth
gold_core_spawnable = FRIENDLY_SPAWN
melee_damage_upper = 5
health = 50
maxHealth = 50
speed = 2
step_type = FOOTSTEP_MOB_CLAW
/mob/living/basic/pet/sloth/Initialize(mapload)
. = ..()
AddElement(/datum/element/wears_collar)
AddElement(/datum/element/ai_retaliate)
//IAA Sloth
/mob/living/basic/pet/sloth/paperwork
name = "Paperwork"
desc = "Internal Affairs' pet sloth. About as useful as the rest of the agents."
gold_core_spawnable = NO_SPAWN
/datum/ai_controller/basic_controller/sloth
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_FLEE_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
)
ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/target_retaliate/to_flee,
/datum/ai_planning_subtree/flee_target/from_flee_key,
/datum/ai_planning_subtree/random_speech/sloth,
)
/datum/ai_planning_subtree/random_speech/sloth
speech_chance = 1
speak = list("Ahhhh")
emote_hear = list("snores.", "yawns.")
emote_see = list("dozes off.", "looks around sleepily.")
@@ -0,0 +1,72 @@
/mob/living/basic/snake
name = "snake"
desc = "A slithery snake. These legless reptiles are the bane of mice and adventurers alike."
icon_state = "snake"
icon_living = "snake"
icon_dead = "snake_dead"
speak_emote = list("hisses")
health = 20
maxHealth = 20
attack_verb_continuous = "bites"
attack_verb_simple = "bite"
attack_sound = 'sound/effects/bite.ogg'
melee_damage_lower = 5
melee_damage_upper = 6
response_help_continuous = "pets"
response_help_simple = "pet"
response_disarm_continuous = "shoos"
response_disarm_simple = "shoo"
response_harm_continuous = "steps on"
response_harm_simple = "step on"
faction = list("hostile", "jungle")
ventcrawler = VENTCRAWLER_ALWAYS
density = FALSE
pass_flags = PASSTABLE | PASSMOB
mob_biotypes = MOB_ORGANIC | MOB_BEAST | MOB_REPTILE
mob_size = MOB_SIZE_SMALL
gold_core_spawnable = FRIENDLY_SPAWN
ai_controller = /datum/ai_controller/basic_controller/snake
/// How much poison?
var/poison_per_bite = 0
/// What poison
var/poison_type = "toxin"
/// List of stuff (mice) that we want to eat
var/static/list/edibles = list(
/mob/living/simple_animal/mouse,
)
/mob/living/basic/snake/Initialize(mapload)
. = ..()
AddElement(/datum/element/ai_retaliate)
AddElement(/datum/element/basic_eating, heal_amt_ = 2, food_types_ = edibles)
ai_controller.set_blackboard_key(BB_BASIC_FOODS, typecacheof(edibles))
/mob/living/basic/snake/melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(. && isliving(target))
var/mob/living/L = target
if(L.reagents && poison_per_bite > 0)
L.reagents.add_reagent(poison_type, poison_per_bite)
/// Snakes are primarily concerned with getting those tasty, tasty mice, but aren't afraid to strike back at those who attack them
/datum/ai_controller/basic_controller/snake
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
)
ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED
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/basic_melee_attack_subtree,
/datum/ai_planning_subtree/find_food,
/datum/ai_planning_subtree/random_speech/snake,
)
/datum/ai_planning_subtree/random_speech/snake
speech_chance = 5
speak = list("hsssss", "sssSSsssss...", "hiisssss")
sound = list('sound/creatures/snake_hissing1.ogg', 'sound/creatures/snake_hissing2.ogg')
emote_hear = list("hisses.")
emote_see = list("slithers around.", "glances.", "stares.")