diff --git a/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm b/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm index 0c5e4d9a216..ccf3792a703 100644 --- a/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm +++ b/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm @@ -2334,7 +2334,7 @@ }, /area/ruin/unpowered/syndicate_space_base/virology) "nt" = ( -/mob/living/simple_animal/cow{ +/mob/living/basic/cow{ faction = list("neutral","syndicate") }, /obj/structure/extinguisher_cabinet{ diff --git a/_maps/map_files/stations/boxstation.dmm b/_maps/map_files/stations/boxstation.dmm index f534abc6cce..8d41e09c0ad 100644 --- a/_maps/map_files/stations/boxstation.dmm +++ b/_maps/map_files/stations/boxstation.dmm @@ -18198,7 +18198,7 @@ /turf/simulated/floor/plating, /area/station/maintenance/fsmaint) "bjx" = ( -/mob/living/simple_animal/cow/betsy, +/mob/living/basic/cow/betsy, /turf/simulated/floor/grass, /area/station/service/hydroponics) "bjy" = ( @@ -20946,15 +20946,6 @@ /obj/structure/chair/comfy/black, /turf/simulated/floor/wood, /area/station/service/library) -"brz" = ( -/obj/structure/closet/walllocker/emerglocker/directional/north, -/obj/structure/rack{ - dir = 8; - layer = 2.9 - }, -/obj/item/extinguisher, -/turf/simulated/floor/plating, -/area/station/public/storage/emergency/port) "brA" = ( /obj/structure/chair/office/dark, /obj/effect/landmark/start/librarian, @@ -50494,6 +50485,14 @@ /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers, /turf/simulated/floor/plating, /area/station/maintenance/aft) +"eOg" = ( +/obj/structure/disposalpipe/segment, +/obj/machinery/economy/atm/directional/west, +/turf/simulated/floor/plasteel{ + dir = 1; + icon_state = "redcorner" + }, +/area/station/security/lobby) "eOh" = ( /obj/machinery/economy/vending/cigarette, /obj/structure/window/reinforced, @@ -52942,6 +52941,11 @@ icon_state = "white" }, /area/station/maintenance/asmaint) +"gaW" = ( +/obj/effect/spawner/random/blood/often, +/obj/machinery/economy/atm/directional/north, +/turf/simulated/floor/wood, +/area/station/maintenance/apmaint2) "gba" = ( /obj/structure/table/reinforced, /obj/machinery/door/firedoor, @@ -60058,14 +60062,6 @@ }, /turf/simulated/floor/plasteel, /area/station/science/xenobiology) -"jPg" = ( -/obj/structure/disposalpipe/segment, -/obj/machinery/economy/atm/directional/west, -/turf/simulated/floor/plasteel{ - dir = 1; - icon_state = "redcorner" - }, -/area/station/security/lobby) "jPk" = ( /obj/structure/cable{ icon_state = "0-2" @@ -61446,13 +61442,6 @@ /obj/effect/mapping_helpers/turfs/rust/maybe, /turf/simulated/wall/r_wall, /area/station/engineering/gravitygenerator) -"kyJ" = ( -/obj/machinery/economy/atm/directional/north, -/turf/simulated/floor/plasteel{ - dir = 4; - icon_state = "yellowcorner" - }, -/area/station/hallway/primary/aft/south) "kzB" = ( /obj/effect/spawner/random/food_or_drink/three_course_meal, /obj/structure/closet/crate/freezer, @@ -64385,6 +64374,10 @@ }, /turf/simulated/floor/plating, /area/station/maintenance/fpmaint2) +"lVm" = ( +/obj/item/kirbyplants/large, +/turf/simulated/floor/wood, +/area/station/service/bar) "lVr" = ( /obj/machinery/atmospherics/pipe/simple/visible/supply{ dir = 10 @@ -68127,6 +68120,15 @@ }, /turf/simulated/floor/plating, /area/station/maintenance/aft) +"nZt" = ( +/obj/machinery/atmospherics/pipe/simple/hidden/supply{ + dir = 4 + }, +/obj/effect/turf_decal/stripes/corner{ + dir = 1 + }, +/turf/simulated/floor/plasteel, +/area/station/hallway/secondary/entry/north) "nZF" = ( /obj/machinery/door/firedoor, /obj/machinery/door/airlock/research{ @@ -73532,12 +73534,6 @@ /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers, /turf/simulated/floor/plating, /area/station/maintenance/aft) -"qEe" = ( -/obj/effect/turf_decal/stripes/corner{ - dir = 1 - }, -/turf/simulated/floor/plasteel, -/area/station/hallway/secondary/entry/north) "qEg" = ( /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers{ dir = 4 @@ -75080,10 +75076,6 @@ icon_state = "dark" }, /area/station/telecomms/chamber) -"rsw" = ( -/obj/item/kirbyplants/large, -/turf/simulated/floor/wood, -/area/station/service/bar) "rsz" = ( /obj/structure/cable{ icon_state = "1-2" @@ -75518,6 +75510,13 @@ }, /turf/simulated/floor/plating, /area/station/maintenance/aft) +"rED" = ( +/obj/machinery/economy/atm/directional/north, +/turf/simulated/floor/plasteel{ + dir = 4; + icon_state = "yellowcorner" + }, +/area/station/hallway/primary/aft/south) "rET" = ( /obj/machinery/atmospherics/pipe/simple/hidden/supply{ dir = 5 @@ -82842,6 +82841,12 @@ }, /turf/simulated/floor/wood, /area/station/command/office/ntrep) +"vGS" = ( +/obj/effect/turf_decal/stripes/corner{ + dir = 1 + }, +/turf/simulated/floor/plasteel, +/area/station/hallway/secondary/entry/north) "vHd" = ( /obj/effect/spawner/window/reinforced/grilled, /obj/structure/cable{ @@ -82988,15 +82993,6 @@ icon_state = "bluecorner" }, /area/station/hallway/secondary/exit) -"vKU" = ( -/obj/machinery/atmospherics/pipe/simple/hidden/supply{ - dir = 4 - }, -/obj/effect/turf_decal/stripes/corner{ - dir = 1 - }, -/turf/simulated/floor/plasteel, -/area/station/hallway/secondary/entry/north) "vLb" = ( /obj/effect/spawner/window/reinforced/grilled, /obj/structure/cable{ @@ -84470,6 +84466,15 @@ icon_state = "whitehall" }, /area/station/hallway/primary/starboard/west) +"wBH" = ( +/obj/structure/closet/walllocker/emerglocker/directional/north, +/obj/structure/rack{ + dir = 8; + layer = 2.9 + }, +/obj/item/extinguisher, +/turf/simulated/floor/plating, +/area/station/public/storage/emergency/port) "wCg" = ( /obj/structure/cable, /obj/structure/cable{ @@ -85837,11 +85842,6 @@ }, /turf/simulated/floor/plasteel, /area/station/maintenance/assembly_line) -"xnt" = ( -/obj/effect/spawner/random/blood/often, -/obj/machinery/economy/atm/directional/north, -/turf/simulated/floor/wood, -/area/station/maintenance/apmaint2) "xnF" = ( /obj/structure/cable{ icon_state = "1-2" @@ -97898,7 +97898,7 @@ aJP aRm aOR aQa -qEe +vGS aMD fay fay @@ -98926,7 +98926,7 @@ aJR aRV aOR aQa -vKU +nZt aRd nff aUm @@ -109221,7 +109221,7 @@ bgA blO iPh blR -brz +wBH bsD bui blR @@ -110294,7 +110294,7 @@ gwb uWp tGO cgQ -xnt +gaW xRm lnh kym @@ -116205,7 +116205,7 @@ cBp cAL cDY cvx -kyJ +rED mqV ePP ePP @@ -116644,7 +116644,7 @@ avd axD axA ayj -jPg +eOg azP aBY aCv @@ -124390,7 +124390,7 @@ bqk bqk bqk bAV -rsw +lVm aUS bAo bHy diff --git a/_maps/map_files/stations/cerestation.dmm b/_maps/map_files/stations/cerestation.dmm index cef76f10c1d..e1bcf73a79a 100644 --- a/_maps/map_files/stations/cerestation.dmm +++ b/_maps/map_files/stations/cerestation.dmm @@ -108724,7 +108724,7 @@ /area/station/maintenance/port) "xnz" = ( /obj/structure/flora/ausbushes/ywflowers, -/mob/living/simple_animal/cow/betsy, +/mob/living/basic/cow/betsy, /turf/simulated/floor/grass, /area/station/service/hydroponics) "xnA" = ( diff --git a/_maps/map_files/stations/deltastation.dmm b/_maps/map_files/stations/deltastation.dmm index f37ec015f2c..69e94bf855f 100644 --- a/_maps/map_files/stations/deltastation.dmm +++ b/_maps/map_files/stations/deltastation.dmm @@ -10570,7 +10570,7 @@ /turf/simulated/floor/carpet, /area/station/service/theatre) "aPX" = ( -/mob/living/simple_animal/cow/betsy, +/mob/living/basic/cow/betsy, /turf/simulated/floor/grass, /area/station/service/kitchen) "aQb" = ( diff --git a/_maps/map_files/stations/emeraldstation.dmm b/_maps/map_files/stations/emeraldstation.dmm index 1e041014261..7e6c4fbb791 100644 --- a/_maps/map_files/stations/emeraldstation.dmm +++ b/_maps/map_files/stations/emeraldstation.dmm @@ -48901,7 +48901,7 @@ }, /area/station/service/theatre) "jGJ" = ( -/mob/living/simple_animal/cow/betsy, +/mob/living/basic/cow/betsy, /obj/machinery/atmospherics/pipe/simple/hidden/supply{ dir = 9 }, diff --git a/_maps/map_files/stations/metastation.dmm b/_maps/map_files/stations/metastation.dmm index e3deeb7c29a..f6987d8f389 100644 --- a/_maps/map_files/stations/metastation.dmm +++ b/_maps/map_files/stations/metastation.dmm @@ -30253,6 +30253,12 @@ /obj/effect/mapping_helpers/turfs/damage, /turf/simulated/floor/wood, /area/station/maintenance/starboard2) +"cqj" = ( +/obj/machinery/hologram/holopad, +/turf/simulated/floor/plasteel{ + icon_state = "darkgrey" + }, +/area/station/service/chapel) "cqk" = ( /obj/structure/window/reinforced, /turf/simulated/floor/engine, @@ -37710,6 +37716,15 @@ icon_state = "white" }, /area/station/science/xenobiology) +"cXX" = ( +/obj/structure/chair/comfy/black{ + dir = 8 + }, +/turf/simulated/floor/plasteel{ + dir = 4; + icon_state = "chapel" + }, +/area/station/service/chapel) "cXY" = ( /obj/structure/filingcabinet/filingcabinet, /obj/machinery/camera{ @@ -44565,6 +44580,25 @@ }, /turf/simulated/floor/plating, /area/station/maintenance/aft2) +"fxB" = ( +/obj/machinery/light/small, +/obj/machinery/light_switch{ + dir = 1; + name = "custom placement"; + pixel_x = 6; + pixel_y = -24 + }, +/obj/machinery/button/windowtint{ + dir = 1; + id = "CHAP"; + pixel_x = -6; + pixel_y = -24 + }, +/turf/simulated/floor/plasteel{ + dir = 1; + icon_state = "chapel" + }, +/area/station/service/chapel) "fxD" = ( /obj/machinery/smartfridge/secure/chemistry/virology/preloaded, /obj/machinery/firealarm/directional/south, @@ -50307,25 +50341,6 @@ /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers, /turf/simulated/floor/plasteel, /area/station/hallway/secondary/garden) -"hVk" = ( -/obj/machinery/light/small, -/obj/machinery/light_switch{ - dir = 1; - name = "custom placement"; - pixel_x = 6; - pixel_y = -24 - }, -/obj/machinery/button/windowtint{ - dir = 1; - id = "CHAP"; - pixel_x = -6; - pixel_y = -24 - }, -/turf/simulated/floor/plasteel{ - dir = 1; - icon_state = "chapel" - }, -/area/station/service/chapel) "hWf" = ( /obj/effect/decal/cleanable/dirt, /obj/machinery/atmospherics/pipe/simple/hidden/supply{ @@ -73849,15 +73864,6 @@ }, /turf/simulated/floor/engine/vacuum, /area/station/science/toxins/mixing) -"shF" = ( -/obj/structure/chair/comfy/black{ - dir = 8 - }, -/turf/simulated/floor/plasteel{ - dir = 4; - icon_state = "chapel" - }, -/area/station/service/chapel) "sjx" = ( /obj/structure/chair{ dir = 1 @@ -77995,12 +78001,6 @@ icon_state = "whitepurple" }, /area/station/science/toxins/mixing) -"ubm" = ( -/obj/machinery/hologram/holopad, -/turf/simulated/floor/plasteel{ - icon_state = "darkgrey" - }, -/area/station/service/chapel) "ubu" = ( /obj/structure/grille/broken, /turf/simulated/floor/plating, @@ -82580,7 +82580,7 @@ }, /area/station/science/toxins/mixing) "wgT" = ( -/mob/living/simple_animal/cow/betsy, +/mob/living/basic/cow/betsy, /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers, /obj/machinery/atmospherics/pipe/simple/hidden/supply{ dir = 5 @@ -84693,12 +84693,6 @@ icon_state = "yellowfull" }, /area/station/hallway/primary/aft/north) -"xdC" = ( -/obj/effect/spawner/window/reinforced/polarized/grilled{ - id = "CHAP" - }, -/turf/simulated/floor/plating, -/area/station/service/chapel) "xdT" = ( /obj/item/radio/intercom{ name = "south bump"; @@ -86075,6 +86069,12 @@ icon_state = "neutralcorner" }, /area/station/hallway/primary/central/nw) +"xKK" = ( +/obj/effect/spawner/window/reinforced/polarized/grilled{ + id = "CHAP" + }, +/turf/simulated/floor/plating, +/area/station/service/chapel) "xKQ" = ( /obj/machinery/firealarm/directional/east, /obj/machinery/atmospherics/unary/vent_pump/on{ @@ -109802,7 +109802,7 @@ gDK cFD bdq oiv -xdC +xKK aaa aaa aaa @@ -110059,7 +110059,7 @@ cDc cFD cUX oiv -xdC +xKK aaa aaa aaa @@ -110315,8 +110315,8 @@ fPa cJM cyN cUK -ubm -xdC +cqj +xKK aaa aaa aaa @@ -110573,7 +110573,7 @@ csw cFD cUX oiv -xdC +xKK aaa aaa aaa @@ -110830,7 +110830,7 @@ xJB cFD bdq oiv -xdC +xKK aaa aaa aaa @@ -111086,7 +111086,7 @@ cVa cNM bYK cof -hVk +fxB cSO abq aaa @@ -111343,7 +111343,7 @@ cVX cJn lMo iyv -shF +cXX cSO abq abq diff --git a/code/__DEFINES/bitfields_defines.dm b/code/__DEFINES/_bitfields_defines.dm similarity index 100% rename from code/__DEFINES/bitfields_defines.dm rename to code/__DEFINES/_bitfields_defines.dm diff --git a/code/__DEFINES/_math.dm b/code/__DEFINES/_math.dm index 45dc7d6c82c..f3cdb06cbdb 100644 --- a/code/__DEFINES/_math.dm +++ b/code/__DEFINES/_math.dm @@ -131,6 +131,13 @@ //Currently, this is used for hydroponics-produce sprite transforming, but could be useful for other transform functions. #define TRANSFORM_USING_VARIABLE(input, max) ( sin((90*(input))/(max))**2 ) +/// Converts a probability/second chance to probability/seconds_per_tick chance +/// For example, if you want an event to happen with a 10% per second chance, but your proc only runs every 5 seconds, do `if(prob(100*SPT_PROB_RATE(0.1, 5)))` +#define SPT_PROB_RATE(prob_per_second, seconds_per_tick) (1 - (1 - (prob_per_second)) ** (seconds_per_tick)) + +/// Like SPT_PROB_RATE but easier to use, simply put `if(SPT_PROB(10, 5))` +#define SPT_PROB(prob_per_second_percent, seconds_per_tick) (prob(100*SPT_PROB_RATE((prob_per_second_percent)/100, (seconds_per_tick)))) + //converts a uniform distributed random number into a normal distributed one //since this method produces two random numbers, one is saved for subsequent calls //(making the cost negligble for every second call) diff --git a/code/__DEFINES/ai/blackboard_defines.dm b/code/__DEFINES/ai/blackboard_defines.dm index 47d18482e0b..6d48c601acc 100644 --- a/code/__DEFINES/ai/blackboard_defines.dm +++ b/code/__DEFINES/ai/blackboard_defines.dm @@ -1,4 +1,42 @@ // Generic blackboard keys. -// This file will get a lot larger as AI subtrees are added. -#define BB_CURRENT_MIN_MOVE_DISTANCE "min_move_distance" +// Movement + +/// The closest we're allowed to / required to get to a target. +#define BB_CURRENT_MIN_MOVE_DISTANCE "BB_min_move_distance" +/// How likely is this mob to move when idle per tick? +#define BB_BASIC_MOB_IDLE_WALK_CHANCE "BB_basic_idle_walk_chance" + +// Searching + +/// key holding a range to look for stuff in +#define BB_SEARCH_RANGE "BB_search_range" + +// Targeting + +/// Key for the minimum status at which we want to target mobs (does not need to be specified if CONSCIOUS) +#define BB_TARGET_MINIMUM_STAT "BB_target_minimum_stat" +/// Should we skip the faction check for the targeting strategy? +#define BB_ALWAYS_IGNORE_FACTION "BB_always_ignore_factions" +/// Are we in some kind of temporary state of ignoring factions when targeting? can result in volatile results if multiple behaviours touch this +#define BB_TEMPORARILY_IGNORE_FACTION "BB_temporarily_ignore_factions" + +// Food and eating + +/// list of foods this mob likes +#define BB_BASIC_FOODS "BB_basic_foods" +/// key holding any food we've found +#define BB_TARGET_FOOD "BB_target_food" +/// key holding emotes we play after eating +#define BB_EAT_EMOTES "BB_eat_emotes" +/// key holding the next time we eat +#define BB_NEXT_FOOD_EAT "BB_next_food_eat" +/// key holding our eating cooldown +#define BB_EAT_FOOD_COOLDOWN "BB_eat_food_cooldown" + +// Tipped blackboards + +/// Bool that means a basic mob will start reacting to being tipped in its planning +#define BB_BASIC_MOB_TIP_REACTING "BB_basic_tip_reacting" +/// the motherfucker who tipped us +#define BB_BASIC_MOB_TIPPER "BB_basic_tip_tipper" diff --git a/code/__DEFINES/basic_mob_flags.dm b/code/__DEFINES/basic_mob_flags.dm new file mode 100644 index 00000000000..3cec828b1f3 --- /dev/null +++ b/code/__DEFINES/basic_mob_flags.dm @@ -0,0 +1,27 @@ +///Basic mob flags + +/// Delete mob upon death +#define DEL_ON_DEATH (1<<0) +/// Rotate mob 180 degrees while it is dead +#define FLIP_ON_DEATH (1<<1) +/// Mob remains dense while dead +#define REMAIN_DENSE_WHILE_DEAD (1<<2) +/// Mob can be set on fire +#define FLAMMABLE_MOB (1<<3) +/// Mob never takes damage from unarmed attacks +#define IMMUNE_TO_FISTS (1<<4) +/// Disables the function of attacking random body zones +#define PRECISE_ATTACK_ZONES (1<<5) + +DEFINE_BITFIELD(basic_mob_flags, list( + "DEL_ON_DEATH" = DEL_ON_DEATH, + "FLIP_ON_DEATH" = FLIP_ON_DEATH, + "REMAIN_DENSE_WHILE_DEAD" = REMAIN_DENSE_WHILE_DEAD, + "FLAMMABLE_MOB" = FLAMMABLE_MOB, + "IMMUNE_TO_FISTS" = IMMUNE_TO_FISTS, + "PRECISE_ATTACK_ZONES" = PRECISE_ATTACK_ZONES, +)) + +///hunger cooldown for basic mobs +#define EAT_FOOD_COOLDOWN 45 SECONDS + diff --git a/code/__DEFINES/dcs/mob_signals.dm b/code/__DEFINES/dcs/mob_signals.dm index bcbdc4fbf73..b978c2dcde8 100644 --- a/code/__DEFINES/dcs/mob_signals.dm +++ b/code/__DEFINES/dcs/mob_signals.dm @@ -177,6 +177,8 @@ ///from base of /mob/living/can_track(): (mob/user) #define COMSIG_LIVING_CAN_TRACK "mob_cantrack" #define COMPONENT_CANT_TRACK (1<<0) +/// from mob/living/*/UnarmedAttack(): (mob/living/source, atom/target, proximity, modifiers) +#define COMSIG_LIVING_UNARMED_ATTACK "living_unarmed_attack" ///from base of mob/living/Write_Memory() #define COMSIG_LIVING_WRITE_MEMORY "living_write_memory" @@ -198,6 +200,16 @@ // Sent when a mob spawner is attacked directly or via projectile. #define COMSIG_SPAWNER_SET_TARGET "spawner_set_target" +/// From /datum/element/basic_eating/try_eating() +#define COMSIG_MOB_PRE_EAT "mob_pre_eat" + ///cancel eating attempt + #define COMSIG_MOB_CANCEL_EAT (1<<0) + +/// From /datum/element/basic_eating/finish_eating() +#define COMSIG_MOB_ATE "mob_ate" + ///cancel post eating + #define COMSIG_MOB_TERMINATE_EAT (1<<0) + // ghost signals /// from observer_base/do_observe(): (mob/now_followed) diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm index 5f49361842e..2bf287766b3 100644 --- a/code/__DEFINES/is_helpers.dm +++ b/code/__DEFINES/is_helpers.dm @@ -33,7 +33,7 @@ // Simple animals -#define issimple_animal(A) (istype(A, /mob/living/simple_animal)) +#define isanimal(A) (istype(A, /mob/living/simple_animal)) #define ismegafauna(A) istype(A, /mob/living/simple_animal/hostile/megafauna) @@ -47,6 +47,14 @@ #define isshadowdemon(A) (istype(A, /mob/living/simple_animal/demon/shadow)) +// Basic mobs + +#define isbasicmob(A) (istype(A, /mob/living/basic)) + +// Simple animal -> basic mob migration helpers + +#define isanimal_or_basicmob(A) (isanimal(A) || isbasicmob(A)) + // Objects #define isobj(A) istype(A, /obj) //override the byond proc because it returns true on children of /atom/movable that aren't objs diff --git a/code/__DEFINES/mob_defines.dm b/code/__DEFINES/mob_defines.dm index 1de98034992..0195dca6ad6 100644 --- a/code/__DEFINES/mob_defines.dm +++ b/code/__DEFINES/mob_defines.dm @@ -241,7 +241,6 @@ #define isdrask(A) (is_species(A, /datum/species/drask)) #define ismoth(A) (is_species(A, /datum/species/moth)) -#define isanimal(A) (istype((A), /mob/living/simple_animal)) #define isdog(A) (istype((A), /mob/living/simple_animal/pet/dog)) #define iscorgi(A) (istype((A), /mob/living/simple_animal/pet/dog/corgi)) #define ismouse(A) (istype((A), /mob/living/simple_animal/mouse)) @@ -384,3 +383,8 @@ #define GRAB_PIXEL_SHIFT_PASSIVE 6 #define GRAB_PIXEL_SHIFT_AGGRESSIVE 12 #define GRAB_PIXEL_SHIFT_NECK 16 + +/// Default minimum body temperature mobs can exist in before taking damage +#define NPC_DEFAULT_MIN_TEMP 250 +/// Default maximum body temperature mobs can exist in before taking damage +#define NPC_DEFAULT_MAX_TEMP 350 diff --git a/code/__HELPERS/string_assoc_lists.dm b/code/__HELPERS/string_assoc_lists.dm index cded3a34969..864e05de3cf 100644 --- a/code/__HELPERS/string_assoc_lists.dm +++ b/code/__HELPERS/string_assoc_lists.dm @@ -15,3 +15,18 @@ GLOBAL_LIST_EMPTY(string_assoc_lists) return return GLOB.string_assoc_lists[string_id] = values + +GLOBAL_LIST_EMPTY(string_lists) + +/** + * Caches lists with non-numeric stringify-able values (text or typepath). + */ +/proc/string_list(list/values) + var/string_id = values.Join("-") + + . = GLOB.string_lists[string_id] + + if(.) + return . + + return GLOB.string_lists[string_id] = values diff --git a/code/__HELPERS/trait_helpers.dm b/code/__HELPERS/trait_helpers.dm index ffc14b71a79..054eccfbc72 100644 --- a/code/__HELPERS/trait_helpers.dm +++ b/code/__HELPERS/trait_helpers.dm @@ -255,6 +255,11 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_UNKNOWN "unknown" // The person with this trait always appears as 'unknown'. #define TRAIT_CRYO_DESPAWNING "cryo_despawning" // dont adminbus this please #define TRAIT_EXAMINE_HALLUCINATING "examine_hallucinating" +/// Trait given when a mob has been tipped +#define TRAIT_MOB_TIPPED "mob_tipped" +/// Trait given to mobs that have the basic eating element +#define TRAIT_MOB_EATER "mob_eater" +#define TRAIT_XENOBIO_SPAWNED "xenobio_spawned" /// Trait that prevents AI controllers from planning detached from ai_status to prevent weird state stuff. #define TRAIT_AI_PAUSED "trait_ai_paused" @@ -384,6 +389,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define PULSEDEMON_TRAIT "pulse_demon" /// Mentor observing #define MENTOR_OBSERVING "mobserving" +#define TIPPED_OVER "tipped_over" //quirk traits #define TRAIT_ALCOHOL_TOLERANCE "alcohol_tolerance" diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm index d9cbe7244db..364cdee730d 100644 --- a/code/_onclick/other_mobs.dm +++ b/code/_onclick/other_mobs.dm @@ -59,8 +59,14 @@ /* Animals & All Unspecified */ -/mob/living/UnarmedAttack(atom/A) - A.attack_animal(src) +/mob/living/UnarmedAttack(atom/target, proximity_flag, modifiers) + var/sigreturn = SEND_SIGNAL(src, COMSIG_LIVING_UNARMED_ATTACK, target, proximity_flag, modifiers) + if(sigreturn & COMPONENT_CANCEL_ATTACK_CHAIN) + return TRUE + if(sigreturn & COMPONENT_SKIP_ATTACK) + return FALSE + + target.attack_animal(src) /mob/living/simple_animal/hostile/UnarmedAttack(atom/A) target = A diff --git a/code/controllers/subsystem/SSai_controllers.dm b/code/controllers/subsystem/SSai_controllers.dm index 1e5c5bdaa42..8e2b1953227 100644 --- a/code/controllers/subsystem/SSai_controllers.dm +++ b/code/controllers/subsystem/SSai_controllers.dm @@ -39,14 +39,14 @@ SUBSYSTEM_DEF(ai_controllers) timer = TICK_USAGE_REAL for(var/datum/ai_controller/ai_controller as anything in ai_controllers_by_status[AI_STATUS_ON]) - if(!COOLDOWN_FINISHED(ai_controller, failed_planning_cooldown)) - continue - if(!ai_controller.able_to_plan()) continue ai_controller.select_behaviors(wait / (1 SECONDS)) if(!LAZYLEN(ai_controller.current_behaviors)) //Still no plan - COOLDOWN_START(ai_controller, failed_planning_cooldown, AI_FAILED_PLANNING_COOLDOWN) + ai_controller.planning_failed() + + if(MC_TICK_CHECK) + break cost_on = MC_AVERAGE(cost_on, TICK_DELTA_TO_MS(TICK_USAGE_REAL - timer)) diff --git a/code/datums/ai/ai_controller.dm b/code/datums/ai/ai_controller.dm index 3ef36676574..7b5e4ef7b49 100644 --- a/code/datums/ai/ai_controller.dm +++ b/code/datums/ai/ai_controller.dm @@ -70,6 +70,9 @@ RESTRICT_TYPE(/datum/ai_controller) var/can_idle = TRUE /// What distance should we be checking for interesting things when considering idling/deidling? Defaults to AI_DEFAULT_INTERESTING_DIST var/interesting_dist = AI_DEFAULT_INTERESTING_DIST + /// are we currently on failed planning timeout? + var/on_failed_planning_timeout = FALSE + /datum/ai_controller/New(atom/new_pawn) change_ai_movement_type(ai_movement) @@ -186,7 +189,7 @@ RESTRICT_TYPE(/datum/ai_controller) if(!pawn_turf) CRASH("AI controller [src] controlling pawn ([pawn]) is not on a turf.") #endif - if(!length(SSmobs.clients_by_zlevel[pawn_turf.z])) + if(!SSmobs.clients_by_zlevel || !length(SSmobs.clients_by_zlevel[pawn_turf.z]) || on_failed_planning_timeout) . = AI_STATUS_OFF /// Called when the AI controller pawn changes z levels. @@ -238,9 +241,38 @@ RESTRICT_TYPE(/datum/ai_controller) return FALSE return TRUE +/datum/ai_controller/proc/ai_can_interact() + SHOULD_CALL_PARENT(TRUE) + return !QDELETED(pawn) + +///Interact with objects +/datum/ai_controller/proc/ai_interact(target, intent, list/modifiers) + if(!ai_can_interact()) + return FALSE + + var/atom/final_target = isdatum(target) ? target : blackboard[target] //incase we got a blackboard key instead + + if(QDELETED(final_target)) + return FALSE + var/params = list2params(modifiers) + var/mob/living/living_pawn = pawn + if(isnull(intent)) + living_pawn.ClickOn(final_target, params) + return TRUE + + var/old_intent = living_pawn.a_intent + living_pawn.a_intent = intent + living_pawn.ClickOn(final_target, params) + living_pawn.a_intent = old_intent + return TRUE /// Runs any actions that are currently running /datum/ai_controller/process(seconds_per_tick) + // AI controllers were implemented on /tg/ after the deltatime conversion: + // https://github.com/tgstation/tgstation/pull/52981 + // The practical side-effect of this is that the values we call "seconds_per_tick" + // in the AI controller implementation are actually the managing subsystem's `wait`. + seconds_per_tick /= (1 SECONDS) if(!able_to_run()) GLOB.move_manager.stop_looping(pawn) //stop moving @@ -450,6 +482,15 @@ RESTRICT_TYPE(/datum/ai_controller) minimum_distance = iter_behavior.required_distance return minimum_distance +/datum/ai_controller/proc/planning_failed() + on_failed_planning_timeout = TRUE + set_ai_status(get_expected_ai_status()) + addtimer(CALLBACK(src, PROC_REF(resume_planning)), AI_FAILED_PLANNING_COOLDOWN) + +/datum/ai_controller/proc/resume_planning() + on_failed_planning_timeout = FALSE + set_ai_status(get_expected_ai_status()) + /// Returns true if we have a blackboard key with the provided key and it is not qdeleting. /datum/ai_controller/proc/blackboard_key_exists(key) var/datum/key_value = blackboard[key] diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/interact_with_target.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/interact_with_target.dm new file mode 100644 index 00000000000..d52e5c38ad1 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/interact_with_target.dm @@ -0,0 +1,29 @@ +/// Behavior for general interactions with any targets +/datum/ai_behavior/interact_with_target + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH + /// Should we be clearing the target after the fact? + var/clear_target = TRUE + /// What intent should we be on? + var/intent = INTENT_HELP + +/datum/ai_behavior/interact_with_target/setup(datum/ai_controller/controller, target_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/interact_with_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target) || !pre_interact(controller, target)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + controller.ai_interact(target, intent) + return AI_BEHAVIOR_SUCCEEDED | AI_BEHAVIOR_DELAY + +/datum/ai_behavior/interact_with_target/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + if(clear_target || !succeeded) + controller.clear_blackboard_key(target_key) + +/datum/ai_behavior/interact_with_target/proc/pre_interact(datum/ai_controller/controller, target) + return TRUE diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/tipped_reaction.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/tipped_reaction.dm new file mode 100644 index 00000000000..1ea29108250 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/tipped_reaction.dm @@ -0,0 +1,33 @@ +/// Type of tipped reaction that is akin to puppy dog eyes. +/datum/ai_behavior/tipped_reaction + +/datum/ai_behavior/tipped_reaction/perform(seconds_per_tick, datum/ai_controller/controller, tipper_key, reacting_key) + var/mob/living/carbon/tipper = controller.blackboard[tipper_key] + + // visible part of the visible message + var/seen_message = "" + // self part of the visible message + var/self_message = "" + // the mob we're looking to for aid + var/mob/living/carbon/savior + // look for someone in a radius around us for help. If our original tipper is in range, prioritize them + for(var/mob/living/carbon/potential_aid in oview(3, get_turf(controller.pawn))) + if(potential_aid == tipper) + savior = tipper + break + savior = potential_aid + + if(prob(75) && savior) + var/text = pick("imploringly", "pleadingly", "with a resigned expression") + seen_message = "[controller.pawn] looks at [savior] [text]." + self_message = "You look at [savior] [text]." + else + seen_message = "[controller.pawn] seems resigned to its fate." + self_message = "You resign yourself to your fate." + controller.pawn.visible_message("[seen_message]", "[self_message]") + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/tipped_reaction/finish_action(datum/ai_controller/controller, succeeded, tipper_key, reacting_key) + . = ..() + controller.set_blackboard_key(reacting_key, FALSE) + controller.clear_blackboard_key(tipper_key) diff --git a/code/datums/ai/basic_mobs/basic_controller.dm b/code/datums/ai/basic_mobs/basic_controller.dm new file mode 100644 index 00000000000..0dd0ea31cd2 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_controller.dm @@ -0,0 +1,19 @@ +/datum/ai_controller/basic_controller + movement_delay = 0.4 SECONDS + +/datum/ai_controller/basic_controller/try_possess_pawn(atom/new_pawn) + if(!isliving(new_pawn)) + return AI_CONTROLLER_INCOMPATIBLE + var/mob/living/basic/basic_mob = new_pawn + + update_speed(basic_mob) + RegisterSignal(basic_mob, COMSIG_MOB_ATE, PROC_REF(on_mob_eat)) + return ..() + +/datum/ai_controller/basic_controller/proc/update_speed(mob/living/basic_mob) + movement_delay = basic_mob.movement_delay() + +/datum/ai_controller/basic_controller/proc/on_mob_eat() + SIGNAL_HANDLER + var/food_cooldown = blackboard[BB_EAT_FOOD_COOLDOWN] || EAT_FOOD_COOLDOWN + set_blackboard_key(BB_NEXT_FOOD_EAT, world.time + food_cooldown) diff --git a/code/datums/ai/basic_mobs/basic_subtrees/find_food.dm b/code/datums/ai/basic_mobs/basic_subtrees/find_food.dm new file mode 100644 index 00000000000..a776e6e6a3b --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/find_food.dm @@ -0,0 +1,43 @@ +/// Similar to finding a target but looks for food types in the blackboard. +/datum/ai_planning_subtree/find_food + /// Behavior we use to find the food + var/datum/ai_behavior/finding_behavior = /datum/ai_behavior/find_and_set/in_list + /// Key of foods list + var/food_list_key = BB_BASIC_FOODS + /// Key where we store our food + var/found_food_key = BB_TARGET_FOOD + /// Key holding any emotes we play after eating food + var/emotes_blackboard_list = BB_EAT_EMOTES + /// Key where we store our search range + var/search_range = BB_SEARCH_RANGE + +/datum/ai_planning_subtree/find_food/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + var/list/foods_list = controller.blackboard[food_list_key] + if(!length(foods_list)) + CRASH("the types of food has not been supplied in the [food_list_key] key!") + if(controller.blackboard[BB_NEXT_FOOD_EAT] > world.time) + return + + if(!controller.blackboard_key_exists(found_food_key)) + controller.queue_behavior(finding_behavior, found_food_key, foods_list, controller.blackboard[BB_SEARCH_RANGE]) + return + + controller.queue_behavior(/datum/ai_behavior/interact_with_target/eat_food, found_food_key, emotes_blackboard_list) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/interact_with_target/eat_food + /// default list of actions we take after eating + var/list/food_actions = list( + "eats up happily!", + "chomps with glee!", + ) + +/datum/ai_behavior/interact_with_target/eat_food/perform(seconds_per_tick, datum/ai_controller/controller, target_key, emotes_blackboard_list) + . = ..() + if(. & AI_BEHAVIOR_FAILED) + return + var/list/emotes_to_pick = controller.blackboard[emotes_blackboard_list] || food_actions + if(!length(emotes_to_pick)) + return + var/mob/living/living_pawn = controller.pawn + living_pawn.custom_emote(EMOTE_VISIBLE, pick(emotes_to_pick)) diff --git a/code/datums/ai/basic_mobs/basic_subtrees/random_speech.dm b/code/datums/ai/basic_mobs/basic_subtrees/random_speech.dm new file mode 100644 index 00000000000..b2084f56002 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/random_speech.dm @@ -0,0 +1,63 @@ +/datum/ai_planning_subtree/random_speech + /// The chance of an emote occurring each second + var/speech_chance = 0 + /// Hearable emotes + var/list/emote_hear = list() + /// Unlike speak_emote, the list of things in this variable only show by themselves with no spoken text. IE: Ian barks, Ian yaps + var/list/emote_see = list() + /// Possible lines of speech the AI can have + var/list/speak = list() + /// The sound effects associated with this speech, if any + var/list/sound = list() + /// The possible verbs used to perform the speech, if any. Defaults to "says" elsewhere in code. + var/list/speak_verbs = list() + +/datum/ai_planning_subtree/random_speech/New() + . = ..() + if(speak) + speak = string_list(speak) + if(sound) + sound = string_list(sound) + if(emote_hear) + emote_hear = string_list(emote_hear) + if(emote_see) + emote_see = string_list(emote_see) + if(speak_verbs) + speak_verbs = string_list(speak_verbs) + +/datum/ai_planning_subtree/random_speech/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + if(!SPT_PROB(speech_chance, seconds_per_tick)) + return + speak(controller) + +/// Actually perform an action +/datum/ai_planning_subtree/random_speech/proc/speak(datum/ai_controller/controller) + var/audible_emotes_length = length(emote_hear) + var/non_audible_emotes_length = length(emote_see) + var/speak_lines_length = length(speak) + + var/total_choices_length = audible_emotes_length + non_audible_emotes_length + speak_lines_length + + if(total_choices_length == 0) + return + + var/random_number_in_range = rand(1, total_choices_length) + var/sound_to_play = length(sound) > 0 ? pick(sound) : null + + if(random_number_in_range <= audible_emotes_length) + controller.queue_behavior(/datum/ai_behavior/perform_emote, pick(emote_hear), sound_to_play) + else if(random_number_in_range <= (audible_emotes_length + non_audible_emotes_length)) + controller.queue_behavior(/datum/ai_behavior/perform_emote, pick(emote_see)) + else + var/speak_verb = "says" + if(length(speak_verbs)) + speak_verb = pick(speak_verbs) + controller.queue_behavior(/datum/ai_behavior/perform_speech, pick(speak), sound_to_play, speak_verb) + +/datum/ai_planning_subtree/random_speech/cow + speech_chance = 2 + speak = list("Moo?", "Moo", "MOOOOOO") + speak_verbs = list("moos", "moos hauntingly") + sound = list('sound/creatures/cow.ogg') + emote_hear = list("brays.") + emote_see = list("shakes her head.") diff --git a/code/datums/ai/basic_mobs/basic_subtrees/tip_reaction.dm b/code/datums/ai/basic_mobs/basic_subtrees/tip_reaction.dm new file mode 100644 index 00000000000..9aba872c7e7 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/tip_reaction.dm @@ -0,0 +1,10 @@ +///used by cows +/datum/ai_planning_subtree/tip_reaction + +/datum/ai_planning_subtree/tip_reaction/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + var/tip_reacting = controller.blackboard[BB_BASIC_MOB_TIP_REACTING] + if(!tip_reacting) + return + controller.queue_behavior(/datum/ai_behavior/tipped_reaction, BB_BASIC_MOB_TIPPER, BB_BASIC_MOB_TIP_REACTING) + return SUBTREE_RETURN_FINISH_PLANNING //no point in trying, boy. you're TIPPED. diff --git a/code/datums/ai/generic/find_and_set.dm b/code/datums/ai/generic/find_and_set.dm new file mode 100644 index 00000000000..52972c7bfca --- /dev/null +++ b/code/datums/ai/generic/find_and_set.dm @@ -0,0 +1,31 @@ +/** + * Finds an item near themselves, sets a blackboard key as it. Very useful for AIs that need to use machines or something. + * If you want to do something more complicated than find a single atom, change the search_tactic() proc + * cool tip: search_tactic() can set lists + */ +/datum/ai_behavior/find_and_set + action_cooldown = 2 SECONDS + +/datum/ai_behavior/find_and_set/perform(seconds_per_tick, datum/ai_controller/controller, set_key, locate_path, search_range) + if(controller.blackboard_key_exists(set_key)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + if(QDELETED(controller.pawn)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + var/find_this_thing = search_tactic(controller, locate_path, search_range) + if(isnull(find_this_thing)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + controller.set_blackboard_key(set_key, find_this_thing) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/find_and_set/proc/search_tactic(datum/ai_controller/controller, locate_path, search_range = 3) + return locate(locate_path) in oview(search_range, controller.pawn) + +/** + * Variant of find and set that takes a list of things to find. + */ +/datum/ai_behavior/find_and_set/in_list + +/datum/ai_behavior/find_and_set/in_list/search_tactic(datum/ai_controller/controller, locate_paths, search_range) + var/list/found = typecache_filter_list(oview(search_range, controller.pawn), locate_paths) + if(length(found)) + return pick(found) diff --git a/code/datums/ai/generic/generic_behaviors.dm b/code/datums/ai/generic/generic_behaviors.dm new file mode 100644 index 00000000000..8d87229f674 --- /dev/null +++ b/code/datums/ai/generic/generic_behaviors.dm @@ -0,0 +1,26 @@ +/datum/ai_behavior/perform_emote + +/datum/ai_behavior/perform_emote/perform(seconds_per_tick, datum/ai_controller/controller, emote, speech_sound) + var/mob/living/living_pawn = controller.pawn + if(!istype(living_pawn)) + return AI_BEHAVIOR_INSTANT + if(emote in GLOB.emote_list) + living_pawn.emote(emote) + else + living_pawn.custom_emote(EMOTE_VISIBLE, emote) + if(speech_sound) // Only audible emotes will pass in a sound + playsound(living_pawn, speech_sound, 80, vary = TRUE, pressure_affected =TRUE, ignore_walls = FALSE) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/perform_speech + +/datum/ai_behavior/perform_speech/perform(seconds_per_tick, datum/ai_controller/controller, speech, speech_sound, speech_verb) + . = ..() + + var/mob/living/living_pawn = controller.pawn + if(!istype(living_pawn)) + return AI_BEHAVIOR_INSTANT + living_pawn.say(speech, speech_verb) + if(speech_sound) + playsound(living_pawn, speech_sound, 80, vary = TRUE) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED diff --git a/code/datums/ai/idle_behaviors/idle_random_walk.dm b/code/datums/ai/idle_behaviors/idle_random_walk.dm new file mode 100644 index 00000000000..a16ebbd3115 --- /dev/null +++ b/code/datums/ai/idle_behaviors/idle_random_walk.dm @@ -0,0 +1,16 @@ +/// Randomly choose a direction to walk in. +/datum/idle_behavior/idle_random_walk + /// Chance that the mob random walks per second + var/walk_chance = 25 + +/datum/idle_behavior/idle_random_walk/perform_idle_behavior(seconds_per_tick, datum/ai_controller/controller) + . = ..() + var/mob/living/living_pawn = controller.pawn + var/actual_chance = controller.blackboard[BB_BASIC_MOB_IDLE_WALK_CHANCE] || walk_chance + if(SPT_PROB(actual_chance, seconds_per_tick) && (living_pawn.mobility_flags & MOBILITY_MOVE) && isturf(living_pawn.loc) && !living_pawn.pulledby) + var/move_dir = pick(GLOB.alldirs) + var/turf/destination_turf = get_step(living_pawn, move_dir) + if(!destination_turf?.can_cross_safely(living_pawn)) + return FALSE + living_pawn.Move(destination_turf, move_dir) + return TRUE diff --git a/code/datums/ai/movement/basic_avoidance.dm b/code/datums/ai/movement/basic_avoidance.dm new file mode 100644 index 00000000000..ab091e0eeeb --- /dev/null +++ b/code/datums/ai/movement/basic_avoidance.dm @@ -0,0 +1,24 @@ +/// Uses Byond's basic obstacle avoidance movement +/datum/ai_movement/basic_avoidance + max_pathing_attempts = 10 + /// Movement flags to pass to the loop + var/move_flags = NONE + +/datum/ai_movement/basic_avoidance/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance) + . = ..() + var/atom/movable/moving = controller.pawn + var/min_dist = controller.blackboard[BB_CURRENT_MIN_MOVE_DISTANCE] + var/delay = controller.movement_delay + var/datum/move_loop/loop = GLOB.move_manager.move_to(moving, current_movement_target, min_dist, delay, flags = move_flags, subsystem = SSai_movement, extra_info = controller) + RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move)) + RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move)) + +/datum/ai_movement/basic_avoidance/allowed_to_move(datum/move_loop/has_target/dist_bound/source) + var/turf/target_turf = get_step_towards(source.moving, source.target) + if(!target_turf?.can_cross_safely(source.moving)) + return FALSE + return ..() + +/// Move immediately and don't update our facing +/datum/ai_movement/basic_avoidance/backstep + move_flags = MOVEMENT_LOOP_START_FAST | MOVEMENT_LOOP_NO_DIR_UPDATE diff --git a/code/datums/ai/targeting_strategy/basic_targeting_strategy.dm b/code/datums/ai/targeting_strategy/basic_targeting_strategy.dm new file mode 100644 index 00000000000..33bba4749e2 --- /dev/null +++ b/code/datums/ai/targeting_strategy/basic_targeting_strategy.dm @@ -0,0 +1,82 @@ +// TODO: /tg/ has factions defined at the atom level and faction_check_atom +// which allows this sort of strategy to work more broadly. A refactor here +// would be necessary to take advantage of that, but unsurprisingly it is a +// pain in the ass because there are snowflake faction checks in many spots +/datum/targeting_strategy/basic + /// When we do our basic faction check, do we look for exact faction matches? + var/check_factions_exactly = FALSE + /// Whether we care for seeing the target or not + var/ignore_sight = FALSE + /// Blackboard key containing the minimum stat of a living mob to target + var/minimum_stat_key = BB_TARGET_MINIMUM_STAT + /// If this blackboard key is TRUE, makes us only target wounded mobs + var/target_wounded_key + +/datum/targeting_strategy/basic/can_attack(mob/living/living_mob, atom/the_target, vision_range) + var/datum/ai_controller/basic_controller/our_controller = living_mob.ai_controller + + if(isnull(our_controller)) + return FALSE + + if(isturf(the_target) || isnull(the_target)) // bail out on invalids + return FALSE + + if(isobj(the_target.loc)) + var/obj/container = the_target.loc + if(container.resistance_flags & INDESTRUCTIBLE) + return FALSE + + var/mob/mob_target = the_target + if(istype(mob_target)) //Target is in godmode, ignore it. + if(living_mob.loc == the_target) + return FALSE // We've either been eaten or are shapeshifted, let's assume the latter because we're still alive + if(mob_target.status_flags & GODMODE) + return FALSE + + if(vision_range && get_dist(living_mob, the_target) > vision_range) + return FALSE + + if(!ignore_sight && !can_see(living_mob, the_target, vision_range)) //Target has moved behind cover and we have lost line of sight to it + return FALSE + + if(living_mob.see_invisible < the_target.invisibility) //Target's invisible to us, forget it + return FALSE + + if(!isturf(living_mob.loc)) + return FALSE + if(isturf(the_target.loc) && living_mob.z != the_target.z || iseffect(the_target.loc)) // z check will always fail if target is in a mech or pawn is shapeshifted or jaunting + return FALSE + + if(isliving(the_target)) //Targeting vs living mobs + var/mob/living/living_target = the_target + if(faction_check(our_controller, living_mob, living_target)) + return FALSE + if(living_target.stat > our_controller.blackboard[minimum_stat_key]) + return FALSE + if(target_wounded_key && our_controller.blackboard[target_wounded_key] && living_target.health == living_target.maxHealth) + return FALSE + + return TRUE + + if(ismecha(the_target)) //Targeting vs mechas + var/obj/mecha/M = the_target + if(can_attack(living_mob, M.occupant)) //Can we attack any of the occupants? + return TRUE + + if(istype(the_target, /obj/machinery/porta_turret)) //Cringe turret! kill it! + var/obj/machinery/porta_turret/P = the_target + if(P.in_faction(living_mob)) //Don't attack if the turret is in the same faction + return FALSE + if(P.has_cover && !P.raised) //Don't attack invincible turrets + return FALSE + if(P.stat & BROKEN) //Or turrets that are already broken + return FALSE + return TRUE + + return FALSE + +/// Returns true if the mob and target share factions +/datum/targeting_strategy/basic/proc/faction_check(datum/ai_controller/controller, mob/living/living_mob, mob/living/the_target) + if(controller.blackboard[BB_ALWAYS_IGNORE_FACTION] || controller.blackboard[BB_TEMPORARILY_IGNORE_FACTION]) + return FALSE + return living_mob.faction_check_mob(the_target, exact_match = check_factions_exactly) diff --git a/code/datums/ai/targeting_strategy.dm b/code/datums/ai/targeting_strategy/targeting_strategy.dm similarity index 100% rename from code/datums/ai/targeting_strategy.dm rename to code/datums/ai/targeting_strategy/targeting_strategy.dm diff --git a/code/datums/components/tippable.dm b/code/datums/components/tippable.dm new file mode 100644 index 00000000000..4e8d3634905 --- /dev/null +++ b/code/datums/components/tippable.dm @@ -0,0 +1,210 @@ +/** + * Tippable component. For making mobs able to be tipped, like cows and medibots. + */ +/datum/component/tippable + /// Time it takes to tip the mob. Can be 0, for instant tipping. + var/tip_time = 3 SECONDS + /// Time it takes to untip the mob. Can also be 0, for instant untip. + var/untip_time = 1 SECONDS + /// Time it takes for the mob to right itself. Can be 0 for instant self-righting, or null, to never self-right. + var/self_right_time = 60 SECONDS + /// Whether the mob is currently tipped. + var/is_tipped = FALSE + /// Callback to additional behavior before being tipped (on try_tip). Return anything from this callback to cancel the tip. + var/datum/callback/pre_tipped_callback + /// Callback to additional behavior after successfully tipping the mob. + var/datum/callback/post_tipped_callback + /// Callback to additional behavior after being untipped. + var/datum/callback/post_untipped_callback + ///The timer given until they untip themselves + var/self_untip_timer + +/datum/component/tippable/Initialize( + tip_time = 3 SECONDS, + untip_time = 1 SECONDS, + self_right_time = 60 SECONDS, + datum/callback/pre_tipped_callback, + datum/callback/post_tipped_callback, + datum/callback/post_untipped_callback, +) + + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + + src.tip_time = tip_time + src.untip_time = untip_time + src.self_right_time = self_right_time + src.pre_tipped_callback = pre_tipped_callback + src.post_tipped_callback = post_tipped_callback + src.post_untipped_callback = post_untipped_callback + +/datum/component/tippable/RegisterWithParent() + RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(interact_with_tippable)) + +/datum/component/tippable/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_ATOM_ATTACK_HAND) + +/datum/component/tippable/Destroy() + pre_tipped_callback = null + post_tipped_callback = null + post_untipped_callback = null + return ..() + +/** + * Attempt to interact with [source], either tipping it or helping it up. + * + * source - the mob being tipped over + * user - the mob interacting with source + */ +/datum/component/tippable/proc/interact_with_tippable(mob/living/source, mob/user) + SIGNAL_HANDLER + + if(is_tipped) + if(user.a_intent == INTENT_HELP) + INVOKE_ASYNC(src, PROC_REF(try_untip), source, user) + else + if(user.a_intent == INTENT_DISARM) + INVOKE_ASYNC(src, PROC_REF(try_tip), source, user) + + return COMPONENT_CANCEL_ATTACK_CHAIN + +/** + * Try to tip over [tipped_mob]. + * If the mob is dead, or optional callback returns a value, or our do-after fails, we don't tip the mob. + * Otherwise, upon completing of the do_after, tip over the mob. + * + * tipped_mob - the mob being tipped over + * tipper - the mob tipping the tipped_mob + */ +/datum/component/tippable/proc/try_tip(mob/living/tipped_mob, mob/tipper) + if(tipped_mob.stat != CONSCIOUS && !HAS_TRAIT(tipped_mob, TRAIT_FORCED_STANDING)) + return + + if(pre_tipped_callback?.Invoke(tipper)) + return + + if(tip_time > 0) + to_chat(tipper, "You begin tipping over [tipped_mob]...") + tipped_mob.visible_message( + "[tipper] begins tipping over [tipped_mob].", + "[tipper] begins tipping you over!" + ) + + if(!do_after(tipper, tip_time, target = tipped_mob)) + if(!isnull(tipped_mob.client)) + add_attack_logs(tipper, tipped_mob, "attempted to tip over") + to_chat(tipper, "You fail to tip over [tipped_mob].") + return + do_tip(tipped_mob, tipper) + +/** + * Actually tip over the mob, setting it to tipped. + * Also invoking any callbacks we have, with the tipper as the argument, + * and set a timer to right our self-right our tipped mob if we can. + * + * tipped_mob - the mob who was tipped + * tipper - the mob who tipped the tipped_mob + */ +/datum/component/tippable/proc/do_tip(mob/living/tipped_mob, mob/tipper) + if(QDELETED(tipped_mob)) + CRASH("Tippable component: do_tip() called with QDELETED tipped_mob!") + if(is_tipped) // sanity check in case multiple people try to tip at the same time + return + + to_chat(tipper, "You tip over [tipped_mob].") + if(!isnull(tipped_mob.client)) + add_attack_logs(tipper, tipped_mob, "tipped over") + tipped_mob.visible_message( + "[tipper] tips over [tipped_mob].", + "You are tipped over by [tipper]!" + ) + + set_tipped_status(tipped_mob, TRUE) + post_tipped_callback?.Invoke(tipper) + if(isnull(self_right_time)) + return + else if(self_right_time <= 0) + right_self(tipped_mob) + else + self_untip_timer = addtimer(CALLBACK(src, PROC_REF(right_self), tipped_mob), self_right_time, TIMER_UNIQUE | TIMER_STOPPABLE) + +/** + * Try to untip a mob that has been tipped. + * After a do-after is completed, we untip the mob. + * + * tipped_mob - the mob who is tipped + * untipper - the mob who is untipping the tipped_mob + */ +/datum/component/tippable/proc/try_untip(mob/living/tipped_mob, mob/untipper) + if(untip_time > 0) + to_chat(untipper, "You begin righting [tipped_mob]...") + tipped_mob.visible_message( + "[untipper] begins righting [tipped_mob].", + "[untipper] begins righting you.", + ) + + if(!do_after(untipper, untip_time, target = tipped_mob)) + to_chat(untipper, "You fail to right [tipped_mob].") + return + + do_untip(tipped_mob, untipper) + +/** + * Actually untip over the mob, setting it to untipped. + * Also invoke any untip callbacks we have, with the untipper as the argument. + * + * tipped_mob - the mob who was tipped + * tipper - the mob who tipped the tipped_mob + */ +/datum/component/tippable/proc/do_untip(mob/living/tipped_mob, mob/untipper) + if(QDELETED(tipped_mob)) + return + if(!is_tipped) // sanity check in case multiple people try to untip at the same time + return + + to_chat(untipper, "You right [tipped_mob].") + tipped_mob.visible_message( + "[untipper] rights [tipped_mob].", + "You are righted by [untipper]!" + ) + + if(self_untip_timer) + deltimer(self_untip_timer) + set_tipped_status(tipped_mob, FALSE) + post_untipped_callback?.Invoke(untipper) + +/** + * Proc called after a timer to have a tipped mob un-tip itself after a certain length of time. + * Sets our mob to untipped and invokes the untipped callback without any arguments if we have one. + * + * tipped_mob - the mob who was tipped, and is freeing itself + */ +/datum/component/tippable/proc/right_self(mob/living/tipped_mob) + if(!is_tipped || QDELETED(tipped_mob)) + return + + set_tipped_status(tipped_mob, FALSE) + post_untipped_callback?.Invoke() + + tipped_mob.visible_message( + "[tipped_mob] rights [tipped_mob.p_themselves()].", + "You right yourself." + ) + +/** + * Toggles our tipped status between tipped or untipped (TRUE or FALSE) + * also handles rotating our mob and adding immobilization traits + * + * tipped_mob - the mob we're setting to tipped or untipped + * new_status - the tipped status we're setting the mob to - TRUE for tipped, FALSE for untipped + */ +/datum/component/tippable/proc/set_tipped_status(mob/living/tipped_mob, new_status = FALSE) + is_tipped = new_status + if(is_tipped) + tipped_mob.transform = turn(tipped_mob.transform, 180) + ADD_TRAIT(tipped_mob, TRAIT_MOB_TIPPED, TIPPED_OVER) + ADD_TRAIT(tipped_mob, TRAIT_IMMOBILIZED, TIPPED_OVER) + return + + tipped_mob.transform = turn(tipped_mob.transform, -180) + REMOVE_TRAITS_IN(tipped_mob, TIPPED_OVER) diff --git a/code/datums/elements/atmos_requirements.dm b/code/datums/elements/atmos_requirements.dm index ae6bd40bdea..90fd7422be5 100644 --- a/code/datums/elements/atmos_requirements.dm +++ b/code/datums/elements/atmos_requirements.dm @@ -12,7 +12,7 @@ /datum/element/atmos_requirements/Attach(datum/target, list/atmos_requirements_, unsuitable_atmos_damage_ = 5) . = ..() - if(!issimple_animal(target)) + if(!isanimal_or_basicmob(target)) return ELEMENT_INCOMPATIBLE if(!atmos_requirements_) diff --git a/code/datums/elements/basic_eating.dm b/code/datums/elements/basic_eating.dm new file mode 100644 index 00000000000..c0e9cc7d45d --- /dev/null +++ b/code/datums/elements/basic_eating.dm @@ -0,0 +1,118 @@ +/** + * ## basic eating element! + * + * Small behavior for non-carbons to eat certain stuff they interact with + */ +/datum/element/basic_eating + element_flags = ELEMENT_BESPOKE + argument_hash_start_idx = 2 + /// Amount to heal + var/heal_amt + /// Amount to hurt + var/damage_amount + /// Type of hurt to apply + var/damage_type + /// Whether to flavor it as drinking rather than eating. + var/drinking + /// If true, we put food in our tummy instead of deleting it + var/add_to_contents + /// Types the animal can eat. + var/list/food_types + +/datum/element/basic_eating/Attach(datum/target, heal_amt_ = 0, damage_amount_ = 0, damage_type_ = null, drinking_ = FALSE, add_to_contents_ = FALSE, food_types_ = list()) + . = ..() + + if(!isliving(target)) + return ELEMENT_INCOMPATIBLE + + ADD_TRAIT(target, TRAIT_MOB_EATER, UID()) + heal_amt = heal_amt_ + damage_amount = damage_amount_ + damage_type = damage_type_ + drinking = drinking_ + add_to_contents = add_to_contents_ + food_types = food_types_ + + RegisterSignal(target, COMSIG_INTERACT_TARGET, PROC_REF(try_feed)) + RegisterSignal(target, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_unarm_attack)) + +/datum/element/basic_eating/Detach(datum/target) + REMOVE_TRAIT(target, TRAIT_MOB_EATER, UID()) + + UnregisterSignal(target, list( + COMSIG_LIVING_UNARMED_ATTACK, + COMSIG_INTERACT_TARGET, + )) + return ..() + +/datum/element/basic_eating/proc/try_feed(atom/source, mob/living/user, atom/possible_food) + SIGNAL_HANDLER + if(!is_type_in_list(possible_food, food_types)) + return NONE + var/mob/living/living_source = source + if(living_source.stat != CONSCIOUS) + return NONE + return try_eating(source, possible_food, user) ? ITEM_INTERACT_COMPLETE : NONE + +/datum/element/basic_eating/proc/on_unarm_attack(mob/living/eater, atom/target, proximity, modifiers) + SIGNAL_HANDLER + if(!proximity) + return NONE + + if(try_eating(eater, target)) + return COMPONENT_CANCEL_ATTACK_CHAIN + return NONE + +/datum/element/basic_eating/proc/try_eating(mob/living/eater, atom/target, mob/living/feeder) + if(!is_type_in_list(target, food_types)) + return FALSE + if(SEND_SIGNAL(eater, COMSIG_MOB_PRE_EAT, target, feeder) & COMSIG_MOB_CANCEL_EAT) + return FALSE + if(add_to_contents && !ismovable(target)) + return FALSE + var/eat_verb + if(drinking) + eat_verb = pick("slurp","sip","guzzle","drink","quaff") + else + eat_verb = pick("bite","chew","nibble","gnaw","gobble","chomp") + + if(heal_amt > 0) + var/healed = heal_amt && eater.health < eater.maxHealth + eater.heal_overall_damage(heal_amt) + eater.visible_message( + "[eater] [eat_verb]s [target].", + "You [eat_verb] [target][healed ? ", restoring some health" : ""].") + + else if(damage_amount > 0 && damage_type) + eater.apply_damage(damage_amount, damage_type) + eater.visible_message( + "[eater] [eat_verb]s [target], and seems to hurt itself.", + "You [eat_verb] [target], hurting yourself in the process.") + + else + eater.visible_message( + "[eater] [eat_verb]s [target].", + "You [eat_verb] [target].") + + finish_eating(eater, target, feeder) + return TRUE + +/datum/element/basic_eating/proc/finish_eating(mob/living/eater, atom/target, mob/living/feeder) + if(drinking) + playsound(eater.loc,'sound/items/drink.ogg', rand(10,50), TRUE) + else + playsound(eater.loc,'sound/items/eatfood.ogg', rand(10,50), TRUE) + var/atom/final_target = target + if(SEND_SIGNAL(eater, COMSIG_MOB_ATE, final_target, feeder) & COMSIG_MOB_TERMINATE_EAT) + return + if(isstack(target)) + var/obj/item/stack/food_stack = target + final_target = food_stack.split(eater, 1) + + add_attack_logs(eater, target, "eaten, [add_to_contents ? "consuming it" : "destroying it"]") + + if(add_to_contents) + var/atom/movable/movable_target = final_target + movable_target.forceMove(eater) + else + qdel(final_target) diff --git a/code/datums/elements/body_temperature.dm b/code/datums/elements/body_temperature.dm index 57a8dd7e51e..506821e9600 100644 --- a/code/datums/elements/body_temperature.dm +++ b/code/datums/elements/body_temperature.dm @@ -23,7 +23,7 @@ /datum/element/body_temperature/Attach(datum/target, min_body_temp_, max_body_temp_, cold_damage_per_tick_, heat_damage_per_tick_) . = ..() - if(!issimple_animal(target)) + if(!isanimal_or_basicmob(target)) return ELEMENT_INCOMPATIBLE if(isnum(min_body_temp)) diff --git a/code/datums/emote.dm b/code/datums/emote.dm index f97f08652b0..2af8e079ee3 100644 --- a/code/datums/emote.dm +++ b/code/datums/emote.dm @@ -413,7 +413,7 @@ . = message_AI else if(ismonkeybasic(user) && message_monkey) . = message_monkey - else if(isanimal(user) && message_simple) + else if(isanimal_or_basicmob(user) && message_simple) . = message_simple else if(isobserver(user) && message_observer) . = message_observer diff --git a/code/datums/keybindings/emote_keybinds.dm b/code/datums/keybindings/emote_keybinds.dm index 52546e86ca5..969fd74bd0f 100644 --- a/code/datums/keybindings/emote_keybinds.dm +++ b/code/datums/keybindings/emote_keybinds.dm @@ -618,7 +618,7 @@ category = KB_CATEGORY_EMOTE_ANIMAL /datum/keybinding/emote/simple_animal/can_use(client/C, mob/M) - return isanimal(M) && ..() + return isanimal_or_basicmob(M) && ..() /datum/keybinding/emote/simple_animal/diona_chirp linked_emote = /datum/emote/living/simple_animal/diona_chirp diff --git a/code/datums/radio_frequency.dm b/code/datums/radio_frequency.dm index 89ffaebf1bc..89add36e2ef 100644 --- a/code/datums/radio_frequency.dm +++ b/code/datums/radio_frequency.dm @@ -106,7 +106,7 @@ . = "Slime" else if(isbot(M)) . = "Bot" - else if(isanimal(M)) + else if(isanimal_or_basicmob(M)) . = "Domestic Animal" else . = "Unidentifiable" diff --git a/code/datums/spell_targeting/matter_eater_targeting.dm b/code/datums/spell_targeting/matter_eater_targeting.dm index 7592f5a0b23..74868a289ce 100644 --- a/code/datums/spell_targeting/matter_eater_targeting.dm +++ b/code/datums/spell_targeting/matter_eater_targeting.dm @@ -17,7 +17,7 @@ /mob/living/simple_animal/chick, /mob/living/simple_animal/chicken, /mob/living/simple_animal/lizard, - /mob/living/simple_animal/cow, + /mob/living/basic/cow, /mob/living/simple_animal/spiderbot ) var/list/own_blacklist = list( @@ -38,6 +38,10 @@ var/mob/living/simple_animal/SA = O if(!SA.gold_core_spawnable) continue + if(isbasicmob(O)) + var/mob/living/basic/B = O + if(!B.gold_core_spawnable) + continue possible_targets += O var/atom/movable/target = tgui_input_list(user, "Choose the target of your hunger", "Targeting", possible_targets) diff --git a/code/datums/spells/bloodcrawl.dm b/code/datums/spells/bloodcrawl.dm index 6a9c05b2682..538a85842f7 100644 --- a/code/datums/spells/bloodcrawl.dm +++ b/code/datums/spells/bloodcrawl.dm @@ -163,7 +163,7 @@ L.adjustBruteLoss(-50) if(!isslaughterdemon(L)) L.adjustFireLoss(-50) - else if(isanimal(victim)) + else if(isanimal_or_basicmob(victim)) to_chat(L, "You devour [victim], but this measly meal barely sates your appetite!") L.adjustBruteLoss(-25) if(!isslaughterdemon(L)) diff --git a/code/datums/status_effects/buffs.dm b/code/datums/status_effects/buffs.dm index 29870c172a3..e7327a184f9 100644 --- a/code/datums/status_effects/buffs.dm +++ b/code/datums/status_effects/buffs.dm @@ -413,10 +413,10 @@ L.adjustBruteLoss(-3.5) L.adjustFireLoss(-3.5) heal_points-- - else if(isanimal(L)) - var/mob/living/simple_animal/SM = L - if(SM.health < SM.maxHealth) - SM.adjustHealth(-3.5) + else if(isanimal_or_basicmob(L)) + var/mob/living/animal = L + if(animal.health < animal.maxHealth) + animal.adjustHealth(-3.5) force_particle = TRUE if(prob(50)) // Animals are simpler heal_points-- diff --git a/code/datums/weather/weather_types/solar_flare.dm b/code/datums/weather/weather_types/solar_flare.dm index a2ae2947e7f..eb0834dc246 100644 --- a/code/datums/weather/weather_types/solar_flare.dm +++ b/code/datums/weather/weather_types/solar_flare.dm @@ -39,7 +39,7 @@ SSsun.solar_gen_rate = initial(SSsun.solar_gen_rate) * 40 /datum/weather/solar_flare/can_weather_act(mob/living/L) - if(isanimal(L)) //while this might break immersion, I don't want to spam the server with calling this on simplemobs + if(isanimal_or_basicmob(L)) //while this might break immersion, I don't want to spam the server with calling this on simplemobs return FALSE if(isdrone(L)) //same with poor maint drones who just wanna have fun return FALSE diff --git a/code/game/atoms.dm b/code/game/atoms.dm index c3198228344..e6741c1cd1f 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -265,6 +265,9 @@ managed_overlays = null + if(ai_controller) + QDEL_NULL(ai_controller) + QDEL_NULL(light) return ..() diff --git a/code/game/dna/dna_modifier.dm b/code/game/dna/dna_modifier.dm index 92d13eda81b..ed0ebb93a1a 100644 --- a/code/game/dna/dna_modifier.dm +++ b/code/game/dna/dna_modifier.dm @@ -160,7 +160,7 @@ return if(!ismob(O)) //humans only return - if(isanimal(O) || issilicon(O)) //animals and robutts dont fit + if(isanimal_or_basicmob(O) || issilicon(O)) //animals and robutts dont fit return if(!ishuman(user) && !isrobot(user)) //No ghosts or mice putting people into the sleeper return diff --git a/code/game/gamemodes/wizard/artefact.dm b/code/game/gamemodes/wizard/artefact.dm index 4397361deb2..be0db5bc087 100644 --- a/code/game/gamemodes/wizard/artefact.dm +++ b/code/game/gamemodes/wizard/artefact.dm @@ -104,7 +104,7 @@ icon_state = "rift" density = TRUE anchored = TRUE - var/spawn_path = /mob/living/simple_animal/cow //defaulty cows to prevent unintentional narsies + var/spawn_path = /mob/living/basic/cow //defaulty cows to prevent unintentional narsies var/spawn_amt_left = 20 /obj/effect/rend/New(loc, spawn_type, spawn_amt, desc) @@ -140,7 +140,7 @@ /obj/item/veilrender/vealrender name = "veal render" desc = "A wicked curved blade of alien origin, recovered from the ruins of a vast farm." - spawn_type = /mob/living/simple_animal/cow + spawn_type = /mob/living/basic/cow spawn_amt = 20 activate_descriptor = "hunger" rend_desc = "Reverberates with the sound of ten thousand moos." diff --git a/code/game/machinery/Sleeper.dm b/code/game/machinery/Sleeper.dm index 9f65294b5bf..6569687e7a2 100644 --- a/code/game/machinery/Sleeper.dm +++ b/code/game/machinery/Sleeper.dm @@ -210,6 +210,12 @@ occupantData["temperatureSuitability"] = -3 else if(silly.bodytemperature > silly.maxbodytemp) occupantData["temperatureSuitability"] = 3 + else if(isbasicmob(occupant)) + var/mob/living/basic/basicmob = occupant + if(basicmob.bodytemperature < basicmob.minimum_survivable_temperature) + occupantData["temperatureSuitability"] = -3 + else if(basicmob.bodytemperature > basicmob.maximum_survivable_temperature) + occupantData["temperatureSuitability"] = 3 // Blast you, imperial measurement system occupantData["btCelsius"] = occupant.bodytemperature - T0C occupantData["btFaren"] = ((occupant.bodytemperature - T0C) * (9.0/5.0))+ 32 @@ -515,7 +521,7 @@ return if(!ismob(O)) //humans only return - if(isanimal(O) || issilicon(O)) //animals and robots dont fit + if(isanimal_or_basicmob(O) || issilicon(O)) //animals and robots dont fit return if(!ishuman(user) && !isrobot(user)) //No ghosts or mice putting people into the sleeper return diff --git a/code/game/machinery/computer/Operating.dm b/code/game/machinery/computer/Operating.dm index 8a87ba6c046..5cbf7b2f80a 100644 --- a/code/game/machinery/computer/Operating.dm +++ b/code/game/machinery/computer/Operating.dm @@ -112,6 +112,12 @@ occupantData["temperatureSuitability"] = -3 else if(silly.bodytemperature > silly.maxbodytemp) occupantData["temperatureSuitability"] = 3 + else if(isbasicmob(occupant)) + var/mob/living/basic/basicmob = occupant + if(basicmob.bodytemperature < basicmob.minimum_survivable_temperature) + occupantData["temperatureSuitability"] = -3 + else if(basicmob.bodytemperature > basicmob.maximum_survivable_temperature) + occupantData["temperatureSuitability"] = 3 // Blast you, imperial measurement system occupantData["btCelsius"] = occupant.bodytemperature - T0C occupantData["btFaren"] = ((occupant.bodytemperature - T0C) * (9.0/5.0))+ 32 diff --git a/code/game/machinery/portable_turret.dm b/code/game/machinery/portable_turret.dm index ed547342ada..4465c3e1930 100644 --- a/code/game/machinery/portable_turret.dm +++ b/code/game/machinery/portable_turret.dm @@ -633,7 +633,7 @@ GLOBAL_LIST_EMPTY(turret_icons) if(iscuffed(L)) // If the target is handcuffed, leave it alone return TURRET_NOT_TARGET - if(isanimal(L) || issmall(L)) // Animals are not so dangerous + if(isanimal_or_basicmob(L) || issmall(L)) // Animals are not so dangerous return check_anomalies ? TURRET_SECONDARY_TARGET : TURRET_NOT_TARGET if(isalien(L)) // Xenos are dangerous diff --git a/code/game/objects/effects/spiders.dm b/code/game/objects/effects/spiders.dm index 8a08f0ae9b3..9ae076b7535 100644 --- a/code/game/objects/effects/spiders.dm +++ b/code/game/objects/effects/spiders.dm @@ -99,8 +99,6 @@ var/player_spiders = FALSE var/list/faction = list("spiders") var/selecting_player = 0 - ///Is this spiderling created from a xenobiology mob? - var/xenobiology_spawned = FALSE /obj/structure/spider/spiderling/Initialize(mapload) . = ..() @@ -181,7 +179,7 @@ if(isturf(loc)) amount_grown += rand(0,2) if(amount_grown >= 100) - if(SSmobs.xenobiology_mobs > MAX_GOLD_CORE_MOBS && xenobiology_spawned) + if(SSmobs.xenobiology_mobs > MAX_GOLD_CORE_MOBS && HAS_TRAIT(src, TRAIT_XENOBIO_SPAWNED)) qdel(src) return if(!grow_as) @@ -189,8 +187,8 @@ var/mob/living/simple_animal/hostile/poison/giant_spider/S = new grow_as(loc) S.faction = faction.Copy() S.master_commander = master_commander - S.xenobiology_spawned = xenobiology_spawned - if(xenobiology_spawned) + if(HAS_TRAIT(src, TRAIT_XENOBIO_SPAWNED)) + ADD_TRAIT(S, TRAIT_XENOBIO_SPAWNED, "xenobio") SSmobs.xenobiology_mobs++ if(player_spiders && !selecting_player) selecting_player = 1 diff --git a/code/game/objects/items/devices/radio/headset.dm b/code/game/objects/items/devices/radio/headset.dm index 8e44c3682bb..d8e4f776581 100644 --- a/code/game/objects/items/devices/radio/headset.dm +++ b/code/game/objects/items/devices/radio/headset.dm @@ -71,7 +71,7 @@ var/mob/living/carbon/human/H = loc if(H.l_ear == src || H.r_ear == src) return ..() - else if(isanimal(loc) || is_ai(loc)) + else if(isanimal_or_basicmob(loc) || is_ai(loc)) return ..() return FALSE diff --git a/code/game/objects/items/devices/scanners.dm b/code/game/objects/items/devices/scanners.dm index e54d482c80d..3bbf948cb05 100644 --- a/code/game/objects/items/devices/scanners.dm +++ b/code/game/objects/items/devices/scanners.dm @@ -199,7 +199,7 @@ SLIME SCANNER if(HAS_TRAIT(user, TRAIT_MED_MACHINE_HALLUCINATING) && prob(10) && IS_HORIZONTAL(M)) probably_dead = TRUE - if(issimple_animal(M)) + if(isanimal_or_basicmob(M)) // No box here, keep it simple. if(probably_dead) to_chat(user, "Analyzing Results for [M]:\nOverall Status: Dead") diff --git a/code/game/objects/items/robot/cyborg_gripper.dm b/code/game/objects/items/robot/cyborg_gripper.dm index 6bfb28825bc..c2301201542 100644 --- a/code/game/objects/items/robot/cyborg_gripper.dm +++ b/code/game/objects/items/robot/cyborg_gripper.dm @@ -74,8 +74,8 @@ // This is required to ensure that the forceMove checks on some objects don't rip the gripper out of the borg's inventory and toss it on the floor. That would hurt, a lot! /obj/item/gripper/forceMove(atom/destination) return - -/obj/item/gripper/interact_with_atom(atom/target, mob/living/user, list/modifiers) + +/obj/item/gripper/interact_with_atom(atom/target, mob/living/user, list/modifiers) if(!target) return ITEM_INTERACT_COMPLETE @@ -170,7 +170,7 @@ if(gripped_item) gripped_item.attack(A, user) return TRUE - + if(!ismob(A)) return ..() @@ -208,7 +208,7 @@ return // Checks if holder_type exists to prevent picking up animals like mice, because we're about to use the hands that borgs secretly have. - if(isanimal(target) && !target.holder_type) + if(isanimal_or_basicmob(target) && !target.holder_type) var/list/modifiers = params2list(params) // This enables borgs to get the floating heart icon and mob emote from simple_animals that have petbonus == TRUE. target.attack_hand(user, modifiers) diff --git a/code/game/objects/items/stacks/medical_packs.dm b/code/game/objects/items/stacks/medical_packs.dm index c6f92f52d3a..56119f68b74 100644 --- a/code/game/objects/items/stacks/medical_packs.dm +++ b/code/game/objects/items/stacks/medical_packs.dm @@ -23,7 +23,7 @@ to_chat(user, "You don't have enough energy to dispense more [singular_name]\s!") return TRUE - if(!iscarbon(M) && !isanimal(M)) + if(!iscarbon(M) && !isanimal_or_basicmob(M)) to_chat(user, "[src] cannot be applied to [M]!") return TRUE @@ -53,8 +53,8 @@ return TRUE return - if(isanimal(M)) - var/mob/living/simple_animal/critter = M + if(isanimal_or_basicmob(M)) + var/mob/living/critter = M if(!(critter.healable)) to_chat(user, "You cannot use [src] on [critter]!") return diff --git a/code/game/objects/items/weapons/legcuffs.dm b/code/game/objects/items/weapons/legcuffs.dm index d5ec39a013b..bb0f9cd93e0 100644 --- a/code/game/objects/items/weapons/legcuffs.dm +++ b/code/game/objects/items/weapons/legcuffs.dm @@ -108,7 +108,7 @@ if(!armed || !isturf(loc) || !istype(entered)) return - if((iscarbon(entered) || isanimal(entered)) && !HAS_TRAIT(entered, TRAIT_FLYING)) + if((iscarbon(entered) || isanimal_or_basicmob(entered)) && !HAS_TRAIT(entered, TRAIT_FLYING)) spring_trap(entered) if(ishuman(entered)) diff --git a/code/game/objects/structures/crates_lockers/closets/secure/depot_closets.dm b/code/game/objects/structures/crates_lockers/closets/secure/depot_closets.dm index 42f2caed559..faec4fe0ae1 100644 --- a/code/game/objects/structures/crates_lockers/closets/secure/depot_closets.dm +++ b/code/game/objects/structures/crates_lockers/closets/secure/depot_closets.dm @@ -31,7 +31,7 @@ depotarea.armory_locker_looted() /obj/structure/closet/secure_closet/depot/attack_animal(mob/M) - if(isanimal(M) && ("syndicate" in M.faction)) + if(isanimal_or_basicmob(M) && ("syndicate" in M.faction)) to_chat(M, "[src] resists your attack!") return return ..() diff --git a/code/game/objects/structures/crates_lockers/crittercrate.dm b/code/game/objects/structures/crates_lockers/crittercrate.dm index a77e8302b81..702614172d0 100644 --- a/code/game/objects/structures/crates_lockers/crittercrate.dm +++ b/code/game/objects/structures/crates_lockers/crittercrate.dm @@ -47,7 +47,7 @@ /obj/structure/closet/critter/random/populate_contents() content_mob = pick(/mob/living/simple_animal/pet/dog/corgi, /mob/living/simple_animal/pet/dog/corgi/lisa, - /mob/living/simple_animal/cow, + /mob/living/basic/cow, /mob/living/simple_animal/pig, /mob/living/simple_animal/hostile/retaliate/goat, /mob/living/simple_animal/turkey, @@ -68,7 +68,7 @@ /obj/structure/closet/critter/cow name = "cow crate" - content_mob = /mob/living/simple_animal/cow + content_mob = /mob/living/basic/cow /obj/structure/closet/critter/pig name = "pig crate" diff --git a/code/game/objects/structures/crates_lockers/largecrate.dm b/code/game/objects/structures/crates_lockers/largecrate.dm index d9642b87886..254c7d12bd4 100644 --- a/code/game/objects/structures/crates_lockers/largecrate.dm +++ b/code/game/objects/structures/crates_lockers/largecrate.dm @@ -75,7 +75,7 @@ /obj/structure/largecrate/cow/crowbar_act(mob/living/user, obj/item/I) if(!I.use_tool(src, user, I.tool_volume)) return - new /mob/living/simple_animal/cow(loc) + new /mob/living/basic/cow(loc) return ..() /obj/structure/largecrate/goat diff --git a/code/game/objects/structures/kitchen_spike.dm b/code/game/objects/structures/kitchen_spike.dm index 7710ec94b4e..3d782aafc03 100644 --- a/code/game/objects/structures/kitchen_spike.dm +++ b/code/game/objects/structures/kitchen_spike.dm @@ -89,7 +89,7 @@ /obj/structure/kitchenspike/MouseDrop_T(mob/living/victim, mob/living/user) if(!user.Adjacent(src) || !user.Adjacent(victim) || is_ai(user) || !ismob(victim)) return - if(isanimal(user) && victim != user) + if(isanimal_or_basicmob(user) && victim != user) return // animals cannot put mobs other than themselves onto spikes add_fingerprint(user) INVOKE_ASYNC(src, TYPE_PROC_REF(/obj/structure/kitchenspike, start_spike), victim, user) diff --git a/code/game/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm index 2aaaef97190..905bcf838f9 100644 --- a/code/game/objects/structures/tables_racks.dm +++ b/code/game/objects/structures/tables_racks.dm @@ -360,7 +360,7 @@ /obj/structure/table/proc/get_flip_speed(mob/living/flipper) if(!istype(flipper)) return 0 SECONDS // sure - if(!issimple_animal(flipper)) + if(!isanimal_or_basicmob(flipper)) return 0 SECONDS if(istype(flipper, /mob/living/simple_animal/revenant)) return 0 SECONDS // funny ghost table diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index cf8139689b7..747dd98e239 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -791,3 +791,10 @@ /turf/_clear_signal_refs() return + +/// Returns whether it is safe for an atom to move across this turf +/// TODO: Things like lava will need to have more specialized code +/// but that can wait for when we port basic mobs that may actually +/// encounter lava +/turf/proc/can_cross_safely(atom/movable/crossing) + return TRUE diff --git a/code/modules/admin/misc_admin_procs.dm b/code/modules/admin/misc_admin_procs.dm index e5b4e167908..36246994bd9 100644 --- a/code/modules/admin/misc_admin_procs.dm +++ b/code/modules/admin/misc_admin_procs.dm @@ -210,7 +210,7 @@ GLOBAL_VAR_INIT(disable_explosions, FALSE) "} //Simple Animals - if(isanimal(M)) + if(isanimal_or_basicmob(M)) body += "Re-Animalize | " else body += "Animalize | " diff --git a/code/modules/admin/player_panel.dm b/code/modules/admin/player_panel.dm index 62c31b5a3db..fe334c45d30 100644 --- a/code/modules/admin/player_panel.dm +++ b/code/modules/admin/player_panel.dm @@ -264,7 +264,7 @@ else M_job = "Silicon-based" - else if(isanimal(M)) //simple animals + else if(isanimal_or_basicmob(M)) //simple animals if(iscorgi(M)) M_job = "Corgi" else diff --git a/code/modules/antagonists/cult/team_cult.dm b/code/modules/antagonists/cult/team_cult.dm index 39fb80f62b6..a53e987fd51 100644 --- a/code/modules/antagonists/cult/team_cult.dm +++ b/code/modules/antagonists/cult/team_cult.dm @@ -259,7 +259,7 @@ RESTRICT_TYPE(/datum/team/cult) return TRUE //can't convert it unless the owner is converted if(isgolem(mind.current)) return FALSE - if(isanimal(mind.current)) + if(isanimal_or_basicmob(mind.current)) return FALSE return TRUE diff --git a/code/modules/events/meaty_ores.dm b/code/modules/events/meaty_ores.dm index 6c18e9de0d3..543e3bd0bfd 100644 --- a/code/modules/events/meaty_ores.dm +++ b/code/modules/events/meaty_ores.dm @@ -36,7 +36,7 @@ else if(prob(10)) explosion(where, 0, pick(0,1), pick(2,3), 0, cause = "Meaty space dust") else - new /mob/living/simple_animal/cow(where) + new /mob/living/basic/cow(where) /obj/effect/space_dust/meaty/Process_Spacemove(movement_dir = 0, continuous_move = FALSE) return TRUE diff --git a/code/modules/mob/hear_say.dm b/code/modules/mob/hear_say.dm index 3d84a6a8bf1..9233e26cb36 100644 --- a/code/modules/mob/hear_say.dm +++ b/code/modules/mob/hear_say.dm @@ -23,6 +23,10 @@ piece = pick(S.speak) else piece = stars(piece) + else if(isbasicmob(speaker)) + var/mob/living/basic/B = speaker + if(LAZYLEN(B.unintelligble_phrases)) + piece = pick(B.unintelligble_phrases) else piece = SP.speaking.scramble(piece) if(always_stars) @@ -30,11 +34,16 @@ piece = SP.speaking.format_message(piece) else if(!say_understands(speaker, null)) - piece = stars(piece) if(isanimal(speaker)) var/mob/living/simple_animal/S = speaker if(LAZYLEN(S.speak)) piece = pick(S.speak) + else if(isbasicmob(speaker)) + var/mob/living/basic/B = speaker + if(LAZYLEN(B.unintelligble_phrases)) + piece = pick(B.unintelligble_phrases) + else + piece = stars(piece) if(always_stars) piece = stars(piece) piece = "[piece]" @@ -109,6 +118,15 @@ emote("me", EMOTE_AUDIBLE, message_clean, TRUE) return + // horrid horrid horrid + // better handling for basicmob interpretation is needed everywhere + // but it doesn't help that all the animal interpretation is snowflaked + // into core /mob procs + if(isbasicmob(speaker) && !say_understands(speaker, null)) + var/mob/living/basic/B = speaker + if(LAZYLEN(B.unintelligble_speak_verbs)) + verb = pick(B.unintelligble_speak_verbs) + if(!can_hear()) // INNATE is the flag for audible-emote-language, so we don't want to show an "x talks but you cannot hear them" message if it's set // if(!language || !(language.flags & INNATE)) diff --git a/code/modules/mob/living/basic/basic_mob.dm b/code/modules/mob/living/basic/basic_mob.dm new file mode 100644 index 00000000000..5a929e8d349 --- /dev/null +++ b/code/modules/mob/living/basic/basic_mob.dm @@ -0,0 +1,258 @@ +RESTRICT_TYPE(/mob/living/basic) + +/// # Basic Mob +/// +/// Basic mobs are a modern replacement for the simple_animal/"simple mob" +/// system. Rather than deep type trees with overrides for AI, basic mobs rely +/// on their AI controllers for the majority of their behavior. +/// +/// It is also intended that as little implementation as possible be part of the +/// basic mob definition directly; rather, as much behavior as possible should +/// be delegated to components and elements, such as +/// [/datum/element/atmos_requirements] for handling reacting to atmosphere, and +/// [/datum/element/body_temperature] for handling changes in body temperature. +/// +/// Migrating simple mobs to basic mobs is an incremental effort; and refactors +/// should be considered before making large changes to existing simple mob +/// code. +/mob/living/basic + name = "basic mob" + desc = "If you can see this, make an issue report on GitHub." + healable = TRUE + + var/basic_mob_flags + + // State changes and data for alive/dead/gibbed. + // TODO: Refactor into an element + + /// Icon to use when the animal is alive. + var/icon_living + /// Icon when the animal is dead. + var/icon_dead + /// We only try to show a gibbing animation if this exists. + var/icon_gib + /// The sound played on death + var/death_sound + /// The message displayed on death + var/death_message + + // Xenobiology/sentience + + /// What is heard by mobs who cannot understand this one when speaking. + /// This doesn't feel like it belongs here but say code and language code + /// is a mess to untangle so maybe it can move somewhere later. + var/list/unintelligble_phrases + var/list/unintelligble_speak_verbs + /// If the mob can be spawned with a gold slime core. HOSTILE_SPAWN are spawned with plasma, FRIENDLY_SPAWN are spawned with blood + var/gold_core_spawnable = NO_SPAWN + /// Holding var for determining who own/controls a sentient simple animal (for sentience potions). + var/mob/living/carbon/human/master_commander = null + + /// Higher speed is slower, negative speed is faster + var/speed = 1 + + /// Leaving something at 0 means it's off - has no maximum. + var/list/atmos_requirements = list( + "min_oxy" = 5, + "max_oxy" = 0, + "min_plas" = 0, + "max_plas" = 1, + "min_co2" = 0, + "max_co2" = 5, + "min_n2" = 0, + "max_n2" = 0 + ) + /// This damage is taken when atmos doesn't fit all the requirements above. + /// Set to 0 to avoid adding the atmos_requirements element. + var/unsuitable_atmos_damage = 1 + + /// Minimal body temperature without receiving damage + var/minimum_survivable_temperature = NPC_DEFAULT_MIN_TEMP + /// Maximal body temperature without receiving damage + var/maximum_survivable_temperature = NPC_DEFAULT_MAX_TEMP + /// This damage is taken when the body temp is too cold. Set both this and unsuitable_heat_damage to 0 to avoid adding the body_temp_sensitive element. + var/unsuitable_cold_damage = 1 + /// This damage is taken when the body temp is too hot. Set both this and unsuitable_cold_damage to 0 to avoid adding the body_temp_sensitive element. + var/unsuitable_heat_damage = 1 + + // String interpolation for actions. + // + // Currently only the continuous tense forms are used because there's + // typically no specific to_chat message for the instigator of the action. + // Rather, they get the same message as everyone around them, e.g. "Foo + // pokes the cow" instead of "You poke the cow." Properly integrating the + // simple tense would require ensuring visible_message doesn't show to the + // instigator via something like a list of ignored mobs. + + /// When someone interacts with the simple animal. + /// Help-intent verb in present continuous tense. + var/response_help_continuous = "pokes" + /// Help-intent verb in present simple tense. + var/response_help_simple = "poke" + /// Disarm-intent verb in present continuous tense. + var/response_disarm_continuous = "shoves" + /// Disarm-intent verb in present simple tense. + var/response_disarm_simple = "shove" + /// Harm-intent verb in present continuous tense. + var/response_harm_continuous = "hits" + /// Harm-intent verb in present simple tense. + var/response_harm_simple = "hit" + + /// Basic mob's own attacks verbs, + /// Attacking verb in present continuous tense. + var/attack_verb_continuous = "attacks" + /// Attacking verb in present simple tense. + var/attack_verb_simple = "attack" + /// Attacking, but without damage, verb in present continuous tense. + var/friendly_verb_continuous = "nuzzles" + /// Attacking, but without damage, verb in present simple tense. + var/friendly_verb_simple = "nuzzle" + + // Attack related vars. + // Unclear how much of this should either be componentized or pulled to /mob/living + var/attacked_sound = 'sound/weapons/punch1.ogg' + /// The amount of damage done to the mob when hand-attacked on harm intent. + var/harm_intent_damage = 3 + /// 1 for full damage, 0 for none, -1 for 1:1 heal from that source. + var/list/damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, STAMINA = 0, OXY = 1) + /// Minimum force required to deal any damage + var/force_threshold = 0 + +/mob/living/basic/Initialize(mapload) + . = ..() + + if(!loc) + stack_trace("Basic mob being instantiated in nullspace") + + apply_atmos_requirements() + apply_temperature_requirements() + +/mob/living/basic/movement_delay() + . = speed + if(forced_look) + . += DIRECTION_LOCK_SLOWDOWN + . += GLOB.configuration.movement.animal_delay + +/mob/living/basic/proc/apply_atmos_requirements() + if(unsuitable_atmos_damage == 0) + return + + atmos_requirements = string_assoc_list(atmos_requirements) + AddElement(/datum/element/atmos_requirements, atmos_requirements, unsuitable_atmos_damage) + +/mob/living/basic/proc/apply_temperature_requirements() + AddElement(/datum/element/body_temperature, minimum_survivable_temperature, maximum_survivable_temperature, unsuitable_cold_damage, unsuitable_heat_damage) + +/mob/living/basic/vv_edit_var(vname, vval) + switch(vname) + if("atmos_requirements", "unsuitable_atmos_damage") + RemoveElement(/datum/element/atmos_requirements, atmos_requirements, unsuitable_atmos_damage) + . = TRUE + if("minimum_survivable_temperature", "maximum_survivable_temperature", "unsuitable_cold_damage", "unsuitable_heat_damage") + RemoveElement(/datum/element/body_temperature, minimum_survivable_temperature, maximum_survivable_temperature, unsuitable_cold_damage, unsuitable_heat_damage) + . = TRUE + + . = ..() + + switch(vname) + if("habitable_atmos", "unsuitable_atmos_damage") + apply_atmos_requirements() + if("minimum_survivable_temperature", "maximum_survivable_temperature", "unsuitable_cold_damage", "unsuitable_heat_damage") + apply_temperature_requirements() + +/// Return whether or not ghosts can take over this mob via "Respawn as NPC" +/mob/living/basic/proc/valid_respawn_target_for(mob/user) + return FALSE + +/mob/living/basic/handle_environment(datum/gas_mixture/readonly_environment) + SEND_SIGNAL(src, COMSIG_SIMPLEANIMAL_HANDLE_ENVIRONMENT, readonly_environment) + +/mob/living/basic/update_stat(reason = "none given") + if(status_flags & GODMODE) + return + if(stat != DEAD) + if(health <= 0) + death() + create_debug_log("died of damage, trigger reason: [reason]") + else + if(HAS_TRAIT(src, TRAIT_KNOCKEDOUT)) + if(stat == CONSCIOUS) + KnockOut() + create_debug_log("knocked out, trigger reason: [reason]") + else if(stat == UNCONSCIOUS) + WakeUp() + create_debug_log("woke up, trigger reason: [reason]") + med_hud_set_status() + +/mob/living/basic/death(gibbed) + . = ..() + if(!.) + return FALSE + if(!gibbed) + if(death_sound) + playsound(get_turf(src), death_sound, 200, 1) + if(death_message) + visible_message("\The [src] [death_message]") + else if(!(basic_mob_flags & DEL_ON_DEATH)) + visible_message("\The [src] stops moving...") + if(HAS_TRAIT(src, TRAIT_XENOBIO_SPAWNED)) + SSmobs.xenobiology_mobs-- + if(basic_mob_flags & DEL_ON_DEATH) + // From simplemob implementation; prevent infinite loops if the mob + // Destroy() is overridden in such a manner as to cause a call to + // death() again. One hopes this isn't still necessary but whatevs + basic_mob_flags &= ~DEL_ON_DEATH + ghostize() + qdel(src) + else + health = 0 + icon_state = icon_dead + if(basic_mob_flags & FLIP_ON_DEATH) + transform = transform.Turn(180) + density = FALSE + +/mob/living/basic/attack_hand(mob/living/carbon/human/M) + ..() + switch(M.a_intent) + + if(INTENT_HELP) + if(health > 0) + visible_message( + "[M] [response_help_continuous] [src].", + "[M] [response_help_continuous] you." + ) + playsound(loc, 'sound/weapons/thudswoosh.ogg', 50, TRUE, -1) + + if(INTENT_GRAB) + grabbedby(M) + + if(INTENT_HARM, INTENT_DISARM) + if(HAS_TRAIT(M, TRAIT_PACIFISM)) + to_chat(M, "You don't want to hurt [src]!") + return + M.do_attack_animation(src, ATTACK_EFFECT_PUNCH) + visible_message( + "[M] [response_harm_continuous] [src]!", + "[M] [response_harm_continuous] you!" + ) + playsound(loc, attacked_sound, 25, TRUE, -1) + attack_threshold_check(harm_intent_damage) + add_attack_logs(M, src, "Melee attacked with fists") + updatehealth() + return TRUE + +// TODO: Could probably be moved to /mob/living but would need more testing to +// ensure it doesn't interfere with carbons. +/mob/living/basic/proc/attack_threshold_check(damage, damagetype = BRUTE, armorcheck = MELEE) + var/temp_damage = damage + if(!damage_coeff[damagetype]) + temp_damage = 0 + else + temp_damage *= damage_coeff[damagetype] + + if(temp_damage >= 0 && temp_damage <= force_threshold) + visible_message("[src] looks unharmed.") + return FALSE + else + apply_damage(damage, damagetype, null, getarmor(null, armorcheck)) + return TRUE diff --git a/code/modules/mob/living/basic/cow/cow.dm b/code/modules/mob/living/basic/cow/cow.dm new file mode 100644 index 00000000000..7715db88b53 --- /dev/null +++ b/code/modules/mob/living/basic/cow/cow.dm @@ -0,0 +1,90 @@ +/mob/living/basic/cow + name = "cow" + desc = "Known for their milk, just don't tip them over." + icon = 'icons/mob/animal.dmi' + icon_state = "cow" + icon_living = "cow" + icon_dead = "cow_dead" + icon_gib = "cow_gib" + ai_controller = /datum/ai_controller/basic_controller/cow + speak_emote = list("moos","moos hauntingly") + speed = 1.1 + see_in_dark = 6 + butcher_results = list(/obj/item/food/meat/slab = 6) + health = 50 + maxHealth = 50 + gold_core_spawnable = FRIENDLY_SPAWN + blood_volume = BLOOD_VOLUME_NORMAL + unintelligble_phrases = list("Moo?", "Moo", "MOOOOOOO") + unintelligble_speak_verbs = list("moos", "moos hauntingly") + + gender = FEMALE + mob_biotypes = MOB_ORGANIC | MOB_BEAST + + /// what this cow munches on + var/list/food_types = list(/obj/item/food/grown/wheat) + + var/obj/item/udder/cow/udder = null + +/mob/living/basic/cow/Initialize(mapload) + . = ..() + udder = new() + AddComponent(/datum/component/tippable, \ + tip_time = 0.5 SECONDS, \ + untip_time = 0.5 SECONDS, \ + self_right_time = rand(25 SECONDS, 50 SECONDS), \ + post_tipped_callback = CALLBACK(src, PROC_REF(after_cow_tipped))) + + // Have all eating components share the same exact list per mob subtype + var/static/list/static_food_types + if(!static_food_types) + static_food_types = food_types.Copy() + + AddElement(/datum/element/basic_eating, food_types_ = static_food_types) + AddComponent(/datum/component/footstep, FOOTSTEP_MOB_SHOE) + ai_controller.set_blackboard_key(BB_BASIC_FOODS, typecacheof(static_food_types)) + +/mob/living/basic/cow/Destroy() + QDEL_NULL(udder) + return ..() + +/mob/living/basic/cow/item_interaction(mob/living/user, obj/item/O, list/modifiers) + if(stat == CONSCIOUS && istype(O, /obj/item/reagent_containers/glass)) + udder.milkAnimal(O, user) + return ITEM_INTERACT_COMPLETE + +/mob/living/basic/cow/Life(seconds, times_fired) + . = ..() + if(stat == CONSCIOUS) + udder.generateMilk() + +/mob/living/basic/cow/valid_respawn_target_for(mob/user) + return TRUE + +/* + * Proc called via callback after the cow is tipped by the tippable component. + * Begins a timer for us pleading for help. + * + * tipper - the mob who tipped us + */ +/mob/living/basic/cow/proc/after_cow_tipped(mob/living/carbon/tipper) + addtimer(CALLBACK(src, PROC_REF(set_tip_react_blackboard), tipper), rand(10 SECONDS, 20 SECONDS)) + +/* + * We've been waiting long enough, we're going to tell our AI to begin pleading. + * + * tipper - the mob who originally tipped us + */ +/mob/living/basic/cow/proc/set_tip_react_blackboard(mob/living/carbon/tipper) + if(!HAS_TRAIT_FROM(src, TRAIT_IMMOBILIZED, TIPPED_OVER) || !ai_controller) + return + ai_controller.set_blackboard_key(BB_BASIC_MOB_TIP_REACTING, TRUE) + ai_controller.set_blackboard_key(BB_BASIC_MOB_TIPPER, tipper) + +/mob/living/basic/cow/betsy + name = "Betsy" + real_name = "Betsy" + +// Don't deprive the chef of their prized cow +/mob/living/basic/cow/betsy/valid_respawn_target_for(mob/user) + return FALSE diff --git a/code/modules/mob/living/basic/cow/cow_ai.dm b/code/modules/mob/living/basic/cow/cow_ai.dm new file mode 100644 index 00000000000..34e26e57c9a --- /dev/null +++ b/code/modules/mob/living/basic/cow/cow_ai.dm @@ -0,0 +1,15 @@ +/datum/ai_controller/basic_controller/cow + blackboard = list( + BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, + BB_BASIC_MOB_TIP_REACTING = FALSE, + BB_BASIC_MOB_TIPPER = null, + ) + + 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/tip_reaction, + /datum/ai_planning_subtree/find_food, + /datum/ai_planning_subtree/random_speech/cow, + ) diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index e85da8a348a..9495aedd4f2 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -13,6 +13,8 @@ //Health and life related vars var/maxHealth = 100 //Maximum health that should be possible. var/health = 100 //A mob's health + /// Healable by medical stacks? + var/healable = FALSE //Damage related vars, NOTE: THESE SHOULD ONLY BE MODIFIED BY PROCS VAR_PROTECTED/bruteloss = 0 //Brutal damage caused by brute force (punching, being clubbed by a toolbox ect... this also accounts for pressure damage) diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm index a501a7e7508..c2974a2a9b5 100644 --- a/code/modules/mob/living/living_say.dm +++ b/code/modules/mob/living/living_say.dm @@ -105,7 +105,7 @@ GLOBAL_LIST_EMPTY(channel_to_radio_key) return returns -/mob/living/say(message, verb = "says", sanitize = TRUE, ignore_speech_problems = FALSE, ignore_atmospherics = FALSE, ignore_languages = FALSE, automatic = FALSE) +/mob/living/say(message, verb = null, sanitize = TRUE, ignore_speech_problems = FALSE, ignore_atmospherics = FALSE, ignore_languages = FALSE, automatic = FALSE) if(client) if(check_mute(client.ckey, MUTE_IC)) to_chat(src, "You cannot speak in IC (Muted).") @@ -166,7 +166,8 @@ GLOBAL_LIST_EMPTY(channel_to_radio_key) return TRUE var/datum/multilingual_say_piece/first_piece = message_pieces[1] - verb = say_quote(message, first_piece.speaking) + if(isnull(verb)) + verb = say_quote(message, first_piece.speaking) if(is_muzzled()) var/obj/item/clothing/mask/muzzle/G = wear_mask diff --git a/code/modules/mob/living/simple_animal/friendly/farm_animals.dm b/code/modules/mob/living/simple_animal/friendly/farm_animals.dm index 4d87bb98c0c..3690c0c2618 100644 --- a/code/modules/mob/living/simple_animal/friendly/farm_animals.dm +++ b/code/modules/mob/living/simple_animal/friendly/farm_animals.dm @@ -106,85 +106,6 @@ desc = "Pete, the Chef's pet goat from the Caribbean. Not known for their pleasant disposition." unique_pet = TRUE -//cow -/mob/living/simple_animal/cow - name = "cow" - desc = "Known for their milk, just don't tip them over." - icon_state = "cow" - icon_living = "cow" - icon_dead = "cow_dead" - icon_gib = "cow_gib" - speak = list("Moo?","Moo","MOOOOOO") - speak_emote = list("moos","moos hauntingly") - emote_hear = list("brays") - emote_see = list("shakes its head") - speak_chance = 1 - turns_per_move = 5 - see_in_dark = 6 - butcher_results = list(/obj/item/food/meat/slab = 6) - response_help = "pets" - response_disarm = "gently pushes aside" - response_harm = "kicks" - attacktext = "kicks" - attack_sound = 'sound/weapons/punch1.ogg' - health = 50 - maxHealth = 50 - can_collar = TRUE - gold_core_spawnable = FRIENDLY_SPAWN - blood_volume = BLOOD_VOLUME_NORMAL - var/obj/item/udder/cow/udder = null - gender = FEMALE - mob_biotypes = MOB_ORGANIC | MOB_BEAST - footstep_type = FOOTSTEP_MOB_SHOE - -/mob/living/simple_animal/cow/Initialize(mapload) - udder = new() - . = ..() - -/mob/living/simple_animal/cow/Destroy() - QDEL_NULL(udder) - return ..() - -/mob/living/simple_animal/cow/item_interaction(mob/living/user, obj/item/O, list/modifiers) - if(stat == CONSCIOUS && istype(O, /obj/item/reagent_containers/glass)) - udder.milkAnimal(O, user) - return ITEM_INTERACT_COMPLETE - -/mob/living/simple_animal/cow/Life(seconds, times_fired) - . = ..() - if(stat == CONSCIOUS) - udder.generateMilk() - -/mob/living/simple_animal/cow/attack_hand(mob/living/carbon/M as mob) - if(stat == CONSCIOUS && M.a_intent == INTENT_DISARM && icon_state != icon_dead) - M.visible_message("[M] tips over [src].","You tip over [src].") - Weaken(60 SECONDS) - icon_state = icon_dead - spawn(rand(20,50)) - if(stat == CONSCIOUS && M) - icon_state = icon_living - var/list/responses = list( - "[src] looks at you imploringly.", - "[src] looks at you pleadingly", - "[src] looks at you with a resigned expression.", - "[src] seems resigned to its fate." - ) - to_chat(M, pick(responses)) - else - ..() - - -/mob/living/simple_animal/cow/npc_safe(mob/user) - return TRUE - -/mob/living/simple_animal/cow/betsy - name = "Betsy" - real_name = "Betsy" - unique_pet = TRUE - -/mob/living/simple_animal/cow/betsy/npc_safe(mob/user) // depriving the chef of his animals is not cool - return FALSE - /mob/living/simple_animal/chick name = "chick" desc = "Adorable! They make such a racket though." diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index 076f4c8ea7a..17a5637720a 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -9,6 +9,7 @@ universal_understand = TRUE universal_speak = FALSE status_flags = CANPUSH + healable = TRUE var/icon_living = "" var/icon_dead = "" @@ -54,9 +55,6 @@ /// Damage the mob will take if it is on fire var/fire_damage = 2 - /// Healable by medical stacks? Defaults to yes. - var/healable = TRUE - /// Atmos effect - Yes, you can make creatures that require plasma or co2 to survive. N2O is a trace gas and handled separately, hence why it isn't here. It'd be hard to add it. Hard and me don't mix (Yes, yes make all the dick jokes you want with that.) - Errorage var/list/atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) //Leaving something at 0 means it's off - has no maximum /// This damage is taken when atmos doesn't fit all the requirements above @@ -104,8 +102,6 @@ var/current_offspring = 0 var/max_offspring = DEFAULT_MAX_OFFSPRING - /// Was this mob spawned by xenobiology magic? Used for mobcapping. - var/xenobiology_spawned = FALSE /// If the mob can be spawned with a gold slime core. HOSTILE_SPAWN are spawned with plasma, FRIENDLY_SPAWN are spawned with blood var/gold_core_spawnable = NO_SPAWN /// Holding var for determining who own/controls a sentient simple animal (for sentience potions). @@ -378,7 +374,7 @@ visible_message("\The [src] [deathmessage]") else if(!del_on_death) visible_message("\The [src] stops moving...") - if(xenobiology_spawned) + if(HAS_TRAIT(src, TRAIT_XENOBIO_SPAWNED)) SSmobs.xenobiology_mobs-- if(del_on_death) //Prevent infinite loops if the mob Destroy() is overridden in such diff --git a/code/modules/mob/living/simple_animal/simple_animal_damage.dm b/code/modules/mob/living/simple_animal/simple_animal_damage.dm index d2ddc3c894f..bd851f58352 100644 --- a/code/modules/mob/living/simple_animal/simple_animal_damage.dm +++ b/code/modules/mob/living/simple_animal/simple_animal_damage.dm @@ -1,16 +1,6 @@ -/mob/living/simple_animal/proc/adjustHealth(amount, updating_health = TRUE) - if(status_flags & GODMODE) - return FALSE - var/oldbruteloss = bruteloss - bruteloss = clamp(bruteloss + amount, 0, maxHealth) - if(oldbruteloss == bruteloss) - updating_health = FALSE - . = STATUS_UPDATE_NONE - else - . = STATUS_UPDATE_HEALTH - if(updating_health) - updatehealth() +/mob/living/simple_animal/adjustHealth(amount, updating_health = TRUE) + . = ..() if(!ckey && stat == CONSCIOUS)//Not unconscious if(AIStatus == AI_IDLE) toggle_ai(AI_ON) diff --git a/code/modules/mob/living/simple_animal/slime/slime_life.dm b/code/modules/mob/living/simple_animal/slime/slime_life.dm index e22ee2a5a54..6d447e27666 100644 --- a/code/modules/mob/living/simple_animal/slime/slime_life.dm +++ b/code/modules/mob/living/simple_animal/slime/slime_life.dm @@ -210,7 +210,7 @@ if(!H.dna.species.tox_mod && !H.dna.species.clone_mod) Feedstop(0, 0) return - + add_nutrition(rand(7, 15)) //Heal yourself. adjustBruteLoss(-3) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 100ef35f84a..2c1824e08ce 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1058,6 +1058,12 @@ GLOBAL_LIST_INIT(slot_equipment_priority, list( \ continue if(L.npc_safe(src) && L.stat != DEAD && !L.key) creatures += L + // Dumb duplicate code until we have no more simple mobs + for(var/mob/living/basic/B in GLOB.alive_mob_list) + if(!(is_station_level(B.z) || is_admin_level(B.z))) // Prevents players from spawning in space + continue + if(B.valid_respawn_target_for(src) && B.stat != DEAD && !B.key) + creatures += B var/picked = tgui_input_list(usr, "Please select an NPC to respawn as", "Respawn as NPC", creatures) switch(picked) if("Mouse") @@ -1606,3 +1612,16 @@ GLOBAL_LIST_INIT(holy_areas, typecacheof(list( /mob/living/proc/remove_recent_magic_block() REMOVE_TRAIT(src, TRAIT_RECENTLY_BLOCKED_MAGIC, MAGIC_TRAIT) + +/mob/living/proc/adjustHealth(amount, updating_health = TRUE) + if(status_flags & GODMODE) + return FALSE + var/oldbruteloss = bruteloss + bruteloss = clamp(bruteloss + amount, 0, maxHealth) + if(oldbruteloss == bruteloss) + updating_health = FALSE + . = STATUS_UPDATE_NONE + else + . = STATUS_UPDATE_HEALTH + if(updating_health) + updatehealth() diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm index 3f238a8f4c6..70111688695 100644 --- a/code/modules/mob/transform_procs.dm +++ b/code/modules/mob/transform_procs.dm @@ -232,6 +232,7 @@ /mob/proc/Animalize() var/list/mobtypes = typesof(/mob/living/simple_animal) + mobtypes |= subtypesof(/mob/living/basic) var/mobpath = tgui_input_list(usr, "Which type of mob should [src] turn into?", "Choose a Type", mobtypes) if(!mobpath) diff --git a/code/modules/projectiles/projectile/magic_projectiles.dm b/code/modules/projectiles/projectile/magic_projectiles.dm index 456caa8412e..2f9989eb0b5 100644 --- a/code/modules/projectiles/projectile/magic_projectiles.dm +++ b/code/modules/projectiles/projectile/magic_projectiles.dm @@ -232,7 +232,7 @@ GLOBAL_LIST_INIT(wabbajack_docile_animals, list( "cat" = /mob/living/simple_animal/pet/cat, "mouse" = /mob/living/simple_animal/mouse, "chicken" = /mob/living/simple_animal/chicken, - "cow" = /mob/living/simple_animal/cow, + "cow" = /mob/living/basic/cow, "lizard" = /mob/living/simple_animal/lizard, "fox" = /mob/living/simple_animal/pet/dog/fox, "chick" = /mob/living/simple_animal/chick, diff --git a/code/modules/reagents/chemistry/chemical_reaction.dm b/code/modules/reagents/chemistry/chemical_reaction.dm index 9d43a501142..98083b73dd8 100644 --- a/code/modules/reagents/chemistry/chemical_reaction.dm +++ b/code/modules/reagents/chemistry/chemical_reaction.dm @@ -58,17 +58,17 @@ C.flash_eyes() for(var/i in 1 to amount_to_spawn) - var/mob/living/simple_animal/S + var/mob/living/new_mob if(random) - S = create_random_mob(get_turf(holder.my_atom), mob_class) + new_mob = create_random_mob(get_turf(holder.my_atom), mob_class) else - S = new mob_class(get_turf(holder.my_atom))//Spawn our specific mob_class + new_mob = new mob_class(get_turf(holder.my_atom))//Spawn our specific mob_class if(gold_core_spawn) //For tracking xenobiology mobs - S.xenobiology_spawned = TRUE - S.faction |= mob_faction + ADD_TRAIT(new_mob, TRAIT_XENOBIO_SPAWNED, "xenobio") + new_mob.faction |= mob_faction if(prob(50)) for(var/j = 1, j <= rand(1, 3), j++) - step(S, pick(NORTH,SOUTH,EAST,WEST)) + step(new_mob, pick(NORTH,SOUTH,EAST,WEST)) /** * Throws or pulls objects to/from a chem reaction diff --git a/paradise.dme b/paradise.dme index 077ab4e163d..c03b57c1787 100644 --- a/paradise.dme +++ b/paradise.dme @@ -18,6 +18,7 @@ #include "code\_compile_options.dm" #include "code\world_definition.dm" #include "code\__DEFINES\__proc_refs.dm" +#include "code\__DEFINES\_bitfields_defines.dm" #include "code\__DEFINES\_click.dm" #include "code\__DEFINES\_globals.dm" #include "code\__DEFINES\_init.dm" @@ -40,8 +41,8 @@ #include "code\__DEFINES\asset_defines.dm" #include "code\__DEFINES\atmospherics_defines.dm" #include "code\__DEFINES\atom_states.dm" +#include "code\__DEFINES\basic_mob_flags.dm" #include "code\__DEFINES\bio_chip_defines.dm" -#include "code\__DEFINES\bitfields_defines.dm" #include "code\__DEFINES\bots.dm" #include "code\__DEFINES\callbacks.dm" #include "code\__DEFINES\chat.dm" @@ -443,9 +444,20 @@ #include "code\datums\ai\ai_behavior.dm" #include "code\datums\ai\ai_controller.dm" #include "code\datums\ai\ai_planning_subtree.dm" -#include "code\datums\ai\targeting_strategy.dm" +#include "code\datums\ai\basic_mobs\basic_controller.dm" +#include "code\datums\ai\basic_mobs\basic_ai_behaviors\interact_with_target.dm" +#include "code\datums\ai\basic_mobs\basic_ai_behaviors\tipped_reaction.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\find_food.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\random_speech.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\tip_reaction.dm" +#include "code\datums\ai\generic\find_and_set.dm" +#include "code\datums\ai\generic\generic_behaviors.dm" #include "code\datums\ai\idle_behaviors\idle_behavior.dm" +#include "code\datums\ai\idle_behaviors\idle_random_walk.dm" #include "code\datums\ai\movement\ai_movement.dm" +#include "code\datums\ai\movement\basic_avoidance.dm" +#include "code\datums\ai\targeting_strategy\basic_targeting_strategy.dm" +#include "code\datums\ai\targeting_strategy\targeting_strategy.dm" #include "code\datums\cache\air_alarm.dm" #include "code\datums\cache\apc_cache.dm" #include "code\datums\cache\cache.dm" @@ -497,6 +509,7 @@ #include "code\datums\components\surgery_initiator.dm" #include "code\datums\components\swarming.dm" #include "code\datums\components\tilted.dm" +#include "code\datums\components\tippable.dm" #include "code\datums\components\two_handed.dm" #include "code\datums\components\zombie_regen.dm" #include "code\datums\components\riding\riding.dm" @@ -562,6 +575,7 @@ #include "code\datums\diseases\advance\symptoms\youth.dm" #include "code\datums\elements\_element.dm" #include "code\datums\elements\atmos_requirements.dm" +#include "code\datums\elements\basic_eating.dm" #include "code\datums\elements\body_temperature.dm" #include "code\datums\elements\bombable_turf.dm" #include "code\datums\elements\butchers_humans.dm" @@ -2309,6 +2323,9 @@ #include "code\modules\mob\living\login.dm" #include "code\modules\mob\living\stat_states.dm" #include "code\modules\mob\living\taste.dm" +#include "code\modules\mob\living\basic\basic_mob.dm" +#include "code\modules\mob\living\basic\cow\cow.dm" +#include "code\modules\mob\living\basic\cow\cow_ai.dm" #include "code\modules\mob\living\brain\brain_death.dm" #include "code\modules\mob\living\brain\brain_emote.dm" #include "code\modules\mob\living\brain\brain_life.dm" diff --git a/sound/creatures/cow.ogg b/sound/creatures/cow.ogg new file mode 100644 index 00000000000..23d44e81496 Binary files /dev/null and b/sound/creatures/cow.ogg differ diff --git a/tools/UpdatePaths/Scripts/28667_cow.txt b/tools/UpdatePaths/Scripts/28667_cow.txt new file mode 100644 index 00000000000..fa0f5fe4603 --- /dev/null +++ b/tools/UpdatePaths/Scripts/28667_cow.txt @@ -0,0 +1 @@ +/mob/living/simple_animal/cow/@SUBTYPES : /mob/living/basic/cow/@SUBTYPES{@OLD}