diff --git a/_maps/RandomRuins/SpaceRuins/derelict_sulaco.dmm b/_maps/RandomRuins/SpaceRuins/derelict_sulaco.dmm
index 1f616ced8d279..f8ec15738e1dc 100644
--- a/_maps/RandomRuins/SpaceRuins/derelict_sulaco.dmm
+++ b/_maps/RandomRuins/SpaceRuins/derelict_sulaco.dmm
@@ -824,7 +824,7 @@
/turf/open/floor/carpet,
/area/ruin/space/has_grav/derelictsulaco)
"BQ" = (
-/mob/living/simple_animal/pet/cat{
+/mob/living/basic/pet/cat{
name = "Jonesy";
desc = "Old and grumpy cat- wait, how the hell is he still alive?!"
},
diff --git a/_maps/RandomRuins/SpaceRuins/mrow_thats_right.dmm b/_maps/RandomRuins/SpaceRuins/mrow_thats_right.dmm
index 9597a976d84b0..0a51efbd58a67 100644
--- a/_maps/RandomRuins/SpaceRuins/mrow_thats_right.dmm
+++ b/_maps/RandomRuins/SpaceRuins/mrow_thats_right.dmm
@@ -245,7 +245,7 @@
/obj/structure/bed/dogbed{
name = "kitty bed"
},
-/mob/living/simple_animal/pet/cat/space,
+/mob/living/basic/pet/cat/space,
/turf/open/floor/carpet,
/area/ruin/space/has_grav/powered/cat_man)
"aS" = (
diff --git a/_maps/RandomZLevels/moonoutpost19.dmm b/_maps/RandomZLevels/moonoutpost19.dmm
index 848b32d9c75b4..0f10d2ca8bf52 100644
--- a/_maps/RandomZLevels/moonoutpost19.dmm
+++ b/_maps/RandomZLevels/moonoutpost19.dmm
@@ -5120,7 +5120,7 @@
},
/area/awaymission/moonoutpost19/research)
"HP" = (
-/mob/living/simple_animal/pet/cat/space{
+/mob/living/basic/pet/cat/space{
desc = "With survival instincts like these, it's no wonder cats survived to the 26th century.";
name = "Jones"
},
diff --git a/_maps/map_files/Birdshot/birdshot.dmm b/_maps/map_files/Birdshot/birdshot.dmm
index c05a949ae18c1..b4eb506262e67 100644
--- a/_maps/map_files/Birdshot/birdshot.dmm
+++ b/_maps/map_files/Birdshot/birdshot.dmm
@@ -36376,7 +36376,7 @@
},
/obj/machinery/airalarm/directional/north,
/obj/structure/bed/dogbed/runtime,
-/mob/living/simple_animal/pet/cat/runtime,
+/mob/living/basic/pet/cat/runtime,
/turf/open/floor/iron/white/small,
/area/station/command/heads_quarters/cmo)
"mVt" = (
diff --git a/_maps/map_files/Deltastation/DeltaStation2.dmm b/_maps/map_files/Deltastation/DeltaStation2.dmm
index 163ac35717f58..95d0385c4ddc7 100644
--- a/_maps/map_files/Deltastation/DeltaStation2.dmm
+++ b/_maps/map_files/Deltastation/DeltaStation2.dmm
@@ -81835,7 +81835,7 @@
/obj/effect/turf_decal/tile/neutral/half/contrasted{
dir = 1
},
-/mob/living/simple_animal/pet/cat/runtime,
+/mob/living/basic/pet/cat/runtime,
/turf/open/floor/iron,
/area/station/command/heads_quarters/cmo)
"utK" = (
diff --git a/_maps/map_files/IceBoxStation/IceBoxStation.dmm b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
index 9806894be95f2..80cddd26ab545 100644
--- a/_maps/map_files/IceBoxStation/IceBoxStation.dmm
+++ b/_maps/map_files/IceBoxStation/IceBoxStation.dmm
@@ -73215,7 +73215,7 @@
/obj/structure/bed/dogbed/runtime,
/obj/item/toy/cattoy,
/obj/machinery/newscaster/directional/north,
-/mob/living/simple_animal/pet/cat/runtime,
+/mob/living/basic/pet/cat/runtime,
/turf/open/floor/iron/dark,
/area/station/command/heads_quarters/cmo)
"wwn" = (
diff --git a/_maps/map_files/MetaStation/MetaStation.dmm b/_maps/map_files/MetaStation/MetaStation.dmm
index f19007be991c3..19e7ca563b93b 100644
--- a/_maps/map_files/MetaStation/MetaStation.dmm
+++ b/_maps/map_files/MetaStation/MetaStation.dmm
@@ -26189,7 +26189,7 @@
/obj/effect/turf_decal/tile/blue/anticorner/contrasted{
dir = 4
},
-/mob/living/simple_animal/pet/cat/runtime,
+/mob/living/basic/pet/cat/runtime,
/turf/open/floor/iron/white,
/area/station/command/heads_quarters/cmo)
"juV" = (
diff --git a/_maps/map_files/NorthStar/north_star.dmm b/_maps/map_files/NorthStar/north_star.dmm
index e529a9a0a50d4..8d35d36b85e34 100644
--- a/_maps/map_files/NorthStar/north_star.dmm
+++ b/_maps/map_files/NorthStar/north_star.dmm
@@ -71828,7 +71828,7 @@
/obj/effect/turf_decal/trimline/blue/filled/line{
dir = 5
},
-/mob/living/simple_animal/pet/cat/runtime,
+/mob/living/basic/pet/cat/runtime,
/turf/open/floor/iron/white,
/area/station/command/heads_quarters/cmo)
"sPk" = (
diff --git a/_maps/map_files/tramstation/tramstation.dmm b/_maps/map_files/tramstation/tramstation.dmm
index 1a4dccbb5672d..140960a695582 100644
--- a/_maps/map_files/tramstation/tramstation.dmm
+++ b/_maps/map_files/tramstation/tramstation.dmm
@@ -32242,7 +32242,7 @@
desc = "A comfy-looking cat bed. You can even strap your pet in, in case the gravity turns off.";
name = "cat bed"
},
-/mob/living/simple_animal/pet/cat/jerry,
+/mob/living/basic/pet/cat/jerry,
/turf/open/floor/iron,
/area/station/maintenance/tram/mid)
"kxF" = (
@@ -51713,7 +51713,7 @@
/obj/structure/bed/dogbed/runtime,
/obj/structure/sign/clock/directional/north,
/obj/machinery/light/cold/directional/north,
-/mob/living/simple_animal/pet/cat/runtime,
+/mob/living/basic/pet/cat/runtime,
/turf/open/floor/iron/dark,
/area/station/command/heads_quarters/cmo)
"rsz" = (
diff --git a/_maps/shuttles/emergency_hugcage.dmm b/_maps/shuttles/emergency_hugcage.dmm
index fc47476440151..6e68506c4332e 100644
--- a/_maps/shuttles/emergency_hugcage.dmm
+++ b/_maps/shuttles/emergency_hugcage.dmm
@@ -289,7 +289,7 @@
/turf/open/floor/mineral/titanium/blue,
/area/shuttle/escape)
"Hf" = (
-/mob/living/simple_animal/pet/cat/kitten,
+/mob/living/basic/pet/cat/kitten,
/turf/open/floor/mineral/titanium/blue,
/area/shuttle/escape)
"IH" = (
diff --git a/_maps/virtual_domains/pipedream.dmm b/_maps/virtual_domains/pipedream.dmm
index 779af98c98813..16a4c7c9fb241 100644
--- a/_maps/virtual_domains/pipedream.dmm
+++ b/_maps/virtual_domains/pipedream.dmm
@@ -1819,7 +1819,7 @@
/turf/open/floor/plating,
/area/virtual_domain)
"Ym" = (
-/mob/living/simple_animal/pet/cat/space,
+/mob/living/basic/pet/cat/space,
/obj/structure/bed/dogbed{
name = "cat bed"
},
diff --git a/code/__DEFINES/ai/monsters.dm b/code/__DEFINES/ai/monsters.dm
index 6f5f0364bec16..aff00260705cb 100644
--- a/code/__DEFINES/ai/monsters.dm
+++ b/code/__DEFINES/ai/monsters.dm
@@ -220,6 +220,34 @@
/// Key for the next time we can cast a spell
#define BB_WIZARD_SPELL_COOLDOWN "BB_wizard_spell_cooldown"
+
+//cat AI keys
+/// key that holds the target we will battle over our turf
+#define BB_TRESSPASSER_TARGET "tresspasser_target"
+/// key that holds angry meows
+#define BB_HOSTILE_MEOWS "hostile_meows"
+/// key that holds the mouse target
+#define BB_MOUSE_TARGET "mouse_target"
+/// key that holds our dinner target
+#define BB_CAT_FOOD_TARGET "cat_food_target"
+/// key that holds the food we must deliver
+#define BB_FOOD_TO_DELIVER "food_to_deliver"
+/// key that holds things we can hunt
+#define BB_HUNTABLE_PREY "huntable_prey"
+/// key that holds target kitten to feed
+#define BB_KITTEN_TO_FEED "kitten_to_feed"
+/// key that holds our hungry meows
+#define BB_HUNGRY_MEOW "hungry_meows"
+/// key that holds maximum distance food is to us so we can pursue it
+#define BB_MAX_DISTANCE_TO_FOOD "max_distance_to_food"
+/// key that holds the stove we must turn off
+#define BB_STOVE_TARGET "stove_target"
+/// key that holds the donut we will decorate
+#define BB_DONUT_TARGET "donut_target"
+/// key that holds our home...
+#define BB_CAT_HOME "cat_home"
+/// key that holds the human we will beg
+#define BB_HUMAN_BEG_TARGET "human_beg_target"
//netguardians
/// rocket launcher
#define BB_NETGUARDIAN_ROCKET_ABILITY "netguardian_rocket"
diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm
index e9963ca1f6a29..8eec04ebe06cf 100644
--- a/code/__DEFINES/is_helpers.dm
+++ b/code/__DEFINES/is_helpers.dm
@@ -178,7 +178,7 @@ GLOBAL_LIST_INIT(turfs_pass_meteor, typecacheof(list(
#define isdrone(A) (istype(A, /mob/living/basic/drone))
-#define iscat(A) (istype(A, /mob/living/simple_animal/pet/cat))
+#define iscat(A) (istype(A, /mob/living/basic/pet/cat))
#define isdog(A) (istype(A, /mob/living/basic/pet/dog))
diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm
index 7734c07132d2d..8f40257debb20 100644
--- a/code/__DEFINES/traits/declarations.dm
+++ b/code/__DEFINES/traits/declarations.dm
@@ -987,6 +987,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_MOB_BREEDER "mob_breeder"
/// Trait given to mobs that we do not want to mindswap
#define TRAIT_NO_MINDSWAP "no_mindswap"
+///trait given to food that can be baked by /datum/component/bakeable
+#define TRAIT_BAKEABLE "bakeable"
/// Trait given to foam darts that have an insert in them
#define TRAIT_DART_HAS_INSERT "dart_has_insert"
diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm
index 4c4c0169f0dce..0e3bf13693452 100644
--- a/code/_globalvars/traits/_traits.dm
+++ b/code/_globalvars/traits/_traits.dm
@@ -475,6 +475,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_TRANSFORM_ACTIVE" = TRAIT_TRANSFORM_ACTIVE,
"TRAIT_UNCATCHABLE" = TRAIT_UNCATCHABLE,
"TRAIT_WIELDED" = TRAIT_WIELDED,
+ "TRAIT_BAKEABLE" = TRAIT_BAKEABLE,
),
/obj/item/ammo_casing = list(
"TRAIT_DART_HAS_INSERT" = TRAIT_DART_HAS_INSERT,
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/find_food.dm b/code/datums/ai/basic_mobs/basic_subtrees/find_food.dm
index b02ec8eaa85aa..9e3cd557b6437 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/find_food.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/find_food.dm
@@ -1,5 +1,9 @@
/// similar to finding a target but looks for food types in the // the what?
/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
/datum/ai_planning_subtree/find_food/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
@@ -7,4 +11,4 @@
// Busy with something
return
- controller.queue_behavior(/datum/ai_behavior/find_and_set/in_list, BB_BASIC_MOB_CURRENT_TARGET, controller.blackboard[BB_BASIC_FOODS])
+ controller.queue_behavior(finding_behavior, BB_BASIC_MOB_CURRENT_TARGET, controller.blackboard[food_list_key])
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm b/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm
index 7a3d5470b1a43..5bd0f8404883d 100644
--- a/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm
+++ b/code/datums/ai/basic_mobs/basic_subtrees/speech_subtree.dm
@@ -214,6 +214,14 @@
emote_hear = list("rawrs.","grumbles.","grawls.", "stomps!")
emote_see = list("stares ferociously.")
+/datum/ai_planning_subtree/random_speech/cats
+ speech_chance = 10
+ speak = list(
+ "mrawww!",
+ "meow!",
+ "maw!",
+ )
+
/datum/ai_planning_subtree/random_speech/blackboard //literal tower of babel, subtree form
speech_chance = 1
diff --git a/code/datums/ai/generic/find_and_set.dm b/code/datums/ai/generic/find_and_set.dm
index d368641ce0ca0..84a007c255906 100644
--- a/code/datums/ai/generic/find_and_set.dm
+++ b/code/datums/ai/generic/find_and_set.dm
@@ -62,6 +62,13 @@
var/mob/living/living_pawn = controller.pawn
return locate(locate_path) in living_pawn.held_items
+/datum/ai_behavior/find_and_set/in_hands/given_list
+
+/datum/ai_behavior/find_and_set/in_hands/given_list/search_tactic(datum/ai_controller/controller, locate_paths)
+ var/list/found = typecache_filter_list(controller.pawn, locate_paths)
+ if(length(found))
+ return pick(found)
+
/**
* Variant of find and set that takes a list of things to find.
*/
diff --git a/code/datums/ai/hunting_behavior/hunting_behaviors.dm b/code/datums/ai/hunting_behavior/hunting_behaviors.dm
index 3ea9feb2b3489..3e747640be3e2 100644
--- a/code/datums/ai/hunting_behavior/hunting_behaviors.dm
+++ b/code/datums/ai/hunting_behavior/hunting_behaviors.dm
@@ -60,10 +60,10 @@
if(!valid_dinner(living_mob, possible_dinner, hunt_range))
continue
controller.set_blackboard_key(hunting_target_key, possible_dinner)
- finish_action(controller, TRUE)
+ finish_action(controller, TRUE, hunting_target_key)
return
- finish_action(controller, FALSE)
+ finish_action(controller, FALSE, hunting_target_key)
/datum/ai_behavior/find_hunt_target/proc/valid_dinner(mob/living/source, atom/dinner, radius)
if(isliving(dinner))
diff --git a/code/datums/components/bakeable.dm b/code/datums/components/bakeable.dm
index b4cde3c5752e9..a745be2b1a579 100644
--- a/code/datums/components/bakeable.dm
+++ b/code/datums/components/bakeable.dm
@@ -26,6 +26,8 @@
src.required_bake_time = required_bake_time
src.positive_result = positive_result
src.added_reagents = added_reagents
+ if(positive_result)
+ ADD_TRAIT(parent, TRAIT_BAKEABLE, REF(src))
// Inherit the new values passed to the component
/datum/component/bakeable/InheritComponent(datum/component/bakeable/new_comp, original, bake_result, required_bake_time, positive_result, use_large_steam_sprite)
@@ -45,6 +47,7 @@
/datum/component/bakeable/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_ITEM_OVEN_PLACED_IN, COMSIG_ITEM_OVEN_PROCESS, COMSIG_ATOM_EXAMINE))
+ REMOVE_TRAIT(parent, TRAIT_BAKEABLE, REF(src))
/// Signal proc for [COMSIG_ITEM_OVEN_PLACED_IN] when baking starts (parent enters an oven)
/datum/component/bakeable/proc/on_baking_start(datum/source, atom/used_oven, mob/baker)
diff --git a/code/datums/elements/consumable_mob.dm b/code/datums/elements/consumable_mob.dm
new file mode 100644
index 0000000000000..1a7c67a431220
--- /dev/null
+++ b/code/datums/elements/consumable_mob.dm
@@ -0,0 +1,32 @@
+/**
+ * element for mobs that can be consumed!
+ */
+/datum/element/consumable_mob
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ ///reagents to give our consumer
+ var/list/reagents_list
+
+/datum/element/consumable_mob/Attach(datum/target, list/reagents_list)
+ . = ..()
+ if(!isliving(target))
+ return ELEMENT_INCOMPATIBLE
+ if(isnull((reagents_list)))
+ stack_trace("No valid reagents list provided!")
+
+ src.reagents_list = reagents_list
+ RegisterSignal(target, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_consume))
+
+/datum/element/consumable_mob/Detach(datum/target)
+ . = ..()
+ UnregisterSignal(target, COMSIG_ATOM_ATTACK_HAND)
+
+/datum/element/consumable_mob/proc/on_consume(atom/movable/source, mob/living/consumer)
+ SIGNAL_HANDLER
+ if(!consumer.combat_mode || !consumer.reagents)
+ return
+ for(var/reagent_type in reagents_list)
+ if(isnull(reagents_list[reagent_type]))
+ return
+ consumer.reagents.add_reagent(reagent_type, reagents_list[reagent_type])
+
diff --git a/code/datums/memory/_memory.dm b/code/datums/memory/_memory.dm
index dd571c85746c8..5731277477678 100644
--- a/code/datums/memory/_memory.dm
+++ b/code/datums/memory/_memory.dm
@@ -267,6 +267,8 @@
/mob/living/basic/mouse,
/mob/living/basic/mushroom,
/mob/living/basic/parrot,
+ /mob/living/basic/pet/cat,
+ /mob/living/basic/pet/cat/cak,
/mob/living/basic/pet/dog/breaddog,
/mob/living/basic/pet/dog/corgi,
/mob/living/basic/pet/dog/pug,
@@ -277,8 +279,6 @@
/mob/living/basic/stickman,
/mob/living/basic/stickman/dog,
/mob/living/simple_animal/hostile/megafauna/dragon/lesser,
- /mob/living/simple_animal/pet/cat,
- /mob/living/simple_animal/pet/cat/cak,
/obj/item/food/sausage/american,
/obj/item/skub,
)
diff --git a/code/game/machinery/dna_infuser/infuser_entries/infuser_tier_zero_entries.dm b/code/game/machinery/dna_infuser/infuser_entries/infuser_tier_zero_entries.dm
index b078b5c76edf9..235986cbd0ddb 100644
--- a/code/game/machinery/dna_infuser/infuser_entries/infuser_tier_zero_entries.dm
+++ b/code/game/machinery/dna_infuser/infuser_entries/infuser_tier_zero_entries.dm
@@ -78,7 +78,7 @@
"oh, let me guess, you're a big fan of those japanese tourist bots",
)
input_obj_or_mob = list(
- /mob/living/simple_animal/pet/cat,
+ /mob/living/basic/pet/cat,
)
output_organs = list(
/obj/item/organ/internal/ears/cat,
diff --git a/code/game/objects/items/devices/laserpointer.dm b/code/game/objects/items/devices/laserpointer.dm
index c56dec6c74fc4..a198139570112 100644
--- a/code/game/objects/items/devices/laserpointer.dm
+++ b/code/game/objects/items/devices/laserpointer.dm
@@ -270,20 +270,6 @@
target_felinid.visible_message(span_notice("[target_felinid] looks briefly distracted by the light."), span_warning("You're briefly tempted by the shiny light..."))
else
target_felinid.visible_message(span_notice("[target_felinid] stares at the light."), span_warning("You stare at the light..."))
-
- //cats! - chance for any cat near the target to pounce at the light, stepping to the target
- for(var/mob/living/simple_animal/pet/cat/target_kitty in view(1, targloc))
- if(target_kitty.stat == DEAD)
- continue
- if(prob(effectchance * diode.rating))
- if(target_kitty.resting)
- target_kitty.set_resting(FALSE, instant = TRUE)
- target_kitty.visible_message(span_notice("[target_kitty] pounces on the light!"), span_warning("LIGHT!"))
- target_kitty.Move(targloc)
- target_kitty.Immobilize(1 SECONDS)
- else
- target_kitty.visible_message(span_notice("[target_kitty] looks uninterested in your games."), span_warning("You spot [user] shining [src] at you. How insulting!"))
-
//The pointer is shining, change its sprite to show
icon_state = "pointer_[pointer_icon_state]"
diff --git a/code/game/objects/items/stacks/sheets/sheet_types.dm b/code/game/objects/items/stacks/sheets/sheet_types.dm
index 641ba6cc77399..3d502e007002c 100644
--- a/code/game/objects/items/stacks/sheets/sheet_types.dm
+++ b/code/game/objects/items/stacks/sheets/sheet_types.dm
@@ -304,6 +304,7 @@ GLOBAL_LIST_INIT(wood_recipes, list ( \
new/datum/stack_recipe("wooden door", /obj/structure/mineral_door/wood, 10, time = 2 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_DOORS), \
new/datum/stack_recipe("wooden stairs frame", /obj/structure/stairs_frame/wood, 10, time = 5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_STRUCTURE), \
new/datum/stack_recipe("wooden fence", /obj/structure/railing/wooden_fence, 2, time = 5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_STRUCTURE), \
+ new/datum/stack_recipe("cat house", /obj/structure/cat_house, 5, time = 5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_STRUCTURE), \
new/datum/stack_recipe("coffin", /obj/structure/closet/crate/coffin, 5, time = 1.5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_FURNITURE), \
new/datum/stack_recipe("book case", /obj/structure/bookcase, 4, time = 1.5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_FURNITURE), \
new/datum/stack_recipe("drying rack", /obj/machinery/smartfridge/drying_rack, 10, time = 1.5 SECONDS, one_per_turf = TRUE, on_solid_ground = TRUE, category = CAT_TOOLS), \
diff --git a/code/game/objects/structures/cat_house.dm b/code/game/objects/structures/cat_house.dm
new file mode 100644
index 0000000000000..8baa9ce241601
--- /dev/null
+++ b/code/game/objects/structures/cat_house.dm
@@ -0,0 +1,46 @@
+/obj/structure/cat_house
+ name = "cat house"
+ desc = "cozy home for cats"
+ icon = 'icons/mob/simple/pets.dmi'
+ icon_state = "cat_house"
+ density = TRUE
+ anchored = TRUE
+ ///cat residing in this house
+ var/mob/living/resident_cat
+
+/obj/structure/cat_house/Initialize(mapload)
+ . = ..()
+ RegisterSignal(src, COMSIG_ATOM_ATTACK_BASIC_MOB, PROC_REF(enter_home))
+
+/obj/structure/cat_house/proc/enter_home(datum/source, mob/living/attacker)
+ SIGNAL_HANDLER
+
+ if(isnull(resident_cat) && istype(attacker, /mob/living/basic/pet/cat))
+ attacker.forceMove(src)
+ return
+ if(resident_cat == attacker)
+ attacker.forceMove(drop_location())
+
+/obj/structure/cat_house/Entered(atom/movable/mover)
+ . = ..()
+ if(!istype(mover, /mob/living/basic/pet/cat))
+ return
+ resident_cat = mover
+ update_appearance(UPDATE_OVERLAYS)
+
+/obj/structure/cat_house/Exited(atom/movable/mover)
+ . = ..()
+ if(mover != resident_cat)
+ return
+ resident_cat = null
+ update_appearance(UPDATE_OVERLAYS)
+
+/obj/structure/cat_house/update_overlays()
+ . = ..()
+ if(isnull(resident_cat))
+ return
+ var/image/cat_icon = image(icon = resident_cat.icon, icon_state = resident_cat.icon_state, layer = LOW_ITEM_LAYER)
+ cat_icon.transform = cat_icon.transform.Scale(0.7, 0.7)
+ cat_icon.pixel_x = 0
+ cat_icon.pixel_y = -9
+ . += cat_icon
diff --git a/code/modules/antagonists/heretic/magic/eldritch_shapeshift.dm b/code/modules/antagonists/heretic/magic/eldritch_shapeshift.dm
index b8a8f39fbc370..e598f1f9215b9 100644
--- a/code/modules/antagonists/heretic/magic/eldritch_shapeshift.dm
+++ b/code/modules/antagonists/heretic/magic/eldritch_shapeshift.dm
@@ -14,8 +14,8 @@
possible_shapes = list(
/mob/living/basic/carp,
/mob/living/basic/mouse,
+ /mob/living/basic/pet/cat,
/mob/living/basic/pet/dog/corgi,
/mob/living/basic/pet/fox,
/mob/living/simple_animal/bot/secbot,
- /mob/living/simple_animal/pet/cat,
)
diff --git a/code/modules/antagonists/traitor/objectives/kill_pet.dm b/code/modules/antagonists/traitor/objectives/kill_pet.dm
index 01ab042f11b03..21bf06eb38681 100644
--- a/code/modules/antagonists/traitor/objectives/kill_pet.dm
+++ b/code/modules/antagonists/traitor/objectives/kill_pet.dm
@@ -23,7 +23,7 @@
/mob/living/basic/pet/dog/corgi/puppy/ian
),
JOB_CAPTAIN = /mob/living/basic/pet/fox/renault,
- JOB_CHIEF_MEDICAL_OFFICER = /mob/living/simple_animal/pet/cat/runtime,
+ JOB_CHIEF_MEDICAL_OFFICER = /mob/living/basic/pet/cat/runtime,
JOB_CHIEF_ENGINEER = /mob/living/basic/parrot/poly,
JOB_QUARTERMASTER = list(
/mob/living/basic/gorilla/cargorilla,
diff --git a/code/modules/cargo/packs/livestock.dm b/code/modules/cargo/packs/livestock.dm
index 942b1414cf9f5..676dfb0a2f721 100644
--- a/code/modules/cargo/packs/livestock.dm
+++ b/code/modules/cargo/packs/livestock.dm
@@ -31,18 +31,22 @@
name = "Cat Crate"
desc = "The cat goes meow! Comes with a collar and a nice cat toy! Cheeseburger not included."//i can't believe im making this reference
cost = CARGO_CRATE_VALUE * 10 //Cats are worth as much as corgis.
- contains = list(/mob/living/simple_animal/pet/cat,
- /obj/item/clothing/neck/petcollar,
- /obj/item/toy/cattoy,
- )
+ contains = list(
+ /mob/living/basic/pet/cat,
+ /obj/item/clothing/neck/petcollar,
+ /obj/item/toy/cattoy,
+ )
crate_name = "cat crate"
/datum/supply_pack/critter/cat/generate()
. = ..()
- if(prob(50))
- var/mob/living/simple_animal/pet/cat/C = locate() in .
- qdel(C)
- new /mob/living/simple_animal/pet/cat/_proc(.)
+ if(!prob(50))
+ return
+ var/mob/living/basic/pet/cat/delete_cat = locate() in .
+ if(isnull(delete_cat))
+ return
+ qdel(delete_cat)
+ new /mob/living/basic/pet/cat/_proc(.)
/datum/supply_pack/critter/chick
name = "Chicken Crate"
diff --git a/code/modules/experisci/experiment/experiments.dm b/code/modules/experisci/experiment/experiments.dm
index 577ae59a900ec..31a8fb8ce556c 100644
--- a/code/modules/experisci/experiment/experiments.dm
+++ b/code/modules/experisci/experiment/experiments.dm
@@ -40,12 +40,12 @@
total_requirement = 3
max_requirement_per_type = 2
possible_types = list(
+ /mob/living/basic/pet/cat,
/mob/living/basic/carp,
/mob/living/basic/chicken,
/mob/living/basic/cow,
/mob/living/basic/pet/dog/corgi,
/mob/living/basic/snake,
- /mob/living/simple_animal/pet/cat,
)
/datum/experiment/scanning/random/cytology/medium/one
diff --git a/code/modules/explorer_drone/loot.dm b/code/modules/explorer_drone/loot.dm
index 121880af7ce48..3f3d6f6c520d7 100644
--- a/code/modules/explorer_drone/loot.dm
+++ b/code/modules/explorer_drone/loot.dm
@@ -96,10 +96,10 @@ GLOBAL_LIST_INIT(adventure_loot_generator_index,generate_generator_index())
id = "pets"
var/carrier_type = /obj/item/pet_carrier/biopod
var/list/possible_pets = list(
+ /mob/living/basic/pet/cat/space,
/mob/living/basic/pet/dog/corgi,
/mob/living/basic/pet/dog/pug,
/mob/living/basic/pet/penguin/baby,
- /mob/living/simple_animal/pet/cat/space,
)
/datum/adventure_loot_generator/pet/generate()
diff --git a/code/modules/food_and_drinks/recipes/tablecraft/recipes_bread.dm b/code/modules/food_and_drinks/recipes/tablecraft/recipes_bread.dm
index e78efd293ffcd..c05446d35218e 100644
--- a/code/modules/food_and_drinks/recipes/tablecraft/recipes_bread.dm
+++ b/code/modules/food_and_drinks/recipes/tablecraft/recipes_bread.dm
@@ -222,7 +222,7 @@
/datum/reagent/blood = 50,
/datum/reagent/medicine/strange_reagent = 5
)
- result = /mob/living/simple_animal/pet/cat/breadcat
+ result = /mob/living/basic/pet/cat/breadcat
category = CAT_BREAD
/datum/crafting_recipe/food/frenchtoast
diff --git a/code/modules/food_and_drinks/recipes/tablecraft/recipes_cake.dm b/code/modules/food_and_drinks/recipes/tablecraft/recipes_cake.dm
index 4a2c9f2935d3d..b34cc5f36e965 100644
--- a/code/modules/food_and_drinks/recipes/tablecraft/recipes_cake.dm
+++ b/code/modules/food_and_drinks/recipes/tablecraft/recipes_cake.dm
@@ -226,7 +226,7 @@
/datum/reagent/consumable/sprinkles = 5,
/datum/reagent/teslium = 1 //To shock the whole thing into life
)
- result = /mob/living/simple_animal/pet/cat/cak
+ result = /mob/living/basic/pet/cat/cak
category = CAT_CAKE //Cat! Haha, get it? CAT? GET IT? We get it - Love Felines
/datum/crafting_recipe/food/fruitcake
diff --git a/code/modules/holodeck/holo_effect.dm b/code/modules/holodeck/holo_effect.dm
index 76b3d320774e4..6b727a2c750a7 100644
--- a/code/modules/holodeck/holo_effect.dm
+++ b/code/modules/holodeck/holo_effect.dm
@@ -94,8 +94,8 @@
/mob/living/basic/pet/dog/pug,
)
mobtype += pick(
- /mob/living/simple_animal/pet/cat,
- /mob/living/simple_animal/pet/cat/kitten,
+ /mob/living/basic/pet/cat,
+ /mob/living/basic/pet/cat/kitten,
)
/obj/effect/holodeck_effect/mobspawner/bee
diff --git a/code/modules/mob/living/basic/pets/cat/bread_cat_ai.dm b/code/modules/mob/living/basic/pets/cat/bread_cat_ai.dm
new file mode 100644
index 0000000000000..35a5d9e12afcf
--- /dev/null
+++ b/code/modules/mob/living/basic/pets/cat/bread_cat_ai.dm
@@ -0,0 +1,62 @@
+/datum/ai_controller/basic_controller/cat/bread
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/find_and_hunt_target/turn_off_stove,
+ /datum/ai_planning_subtree/find_and_hunt_target/hunt_mice,
+ /datum/ai_planning_subtree/find_and_hunt_target/find_cat_food,
+ /datum/ai_planning_subtree/haul_food_to_young,
+ /datum/ai_planning_subtree/random_speech/cats,
+ )
+
+/datum/ai_planning_subtree/find_and_hunt_target/turn_off_stove
+ target_key = BB_STOVE_TARGET
+ hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target/stove_target
+ finding_behavior = /datum/ai_behavior/find_hunt_target/stove
+ hunt_targets = list(/obj/machinery/oven/range)
+ hunt_range = 9
+
+/datum/ai_behavior/find_hunt_target/stove
+
+/datum/ai_behavior/find_hunt_target/stove/valid_dinner(mob/living/source, obj/machinery/oven/range/stove, radius)
+ if(!length(stove.used_tray?.contents) || stove.open)
+ return FALSE
+ //something in there is still baking...
+ for(var/atom/baking in stove.used_tray)
+ if(HAS_TRAIT(baking, TRAIT_BAKEABLE))
+ return FALSE
+ return TRUE
+
+/datum/ai_behavior/hunt_target/unarmed_attack_target/stove_target
+ always_reset_target = TRUE
+
+/datum/ai_behavior/hunt_target/unarmed_attack_target/stove_target/target_caught(mob/living/hunter, obj/machinery/oven/range/stove)
+ if(stove.open)
+ return
+ return ..()
+
+/datum/ai_controller/basic_controller/cat/cake
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/find_and_hunt_target/turn_off_stove,
+ /datum/ai_planning_subtree/find_and_hunt_target/decorate_donuts,
+ /datum/ai_planning_subtree/find_and_hunt_target/hunt_mice,
+ /datum/ai_planning_subtree/find_and_hunt_target/find_cat_food,
+ /datum/ai_planning_subtree/haul_food_to_young,
+ /datum/ai_planning_subtree/random_speech/cats,
+ )
+
+/datum/ai_planning_subtree/find_and_hunt_target/decorate_donuts
+ target_key = BB_DONUT_TARGET
+ hunting_behavior = /datum/ai_behavior/hunt_target/decorate_donuts
+ finding_behavior = /datum/ai_behavior/find_hunt_target/decorate_donuts
+ hunt_targets = list(/obj/item/food/donut)
+ hunt_range = 9
+
+/datum/ai_behavior/find_hunt_target/decorate_donuts/valid_dinner(mob/living/source, obj/item/food/donut/target, radius)
+ if(!target.is_decorated)
+ return FALSE
+ return can_see(source, target, radius)
+
+/datum/ai_behavior/hunt_target/decorate_donuts
+ always_reset_target = TRUE
+
+/datum/ai_behavior/hunt_target/decorate_donuts/target_caught(mob/living/hunter, atom/target)
+ hunter.spin(spintime = 4, speed = 1)
diff --git a/code/modules/mob/living/basic/pets/cat/cat.dm b/code/modules/mob/living/basic/pets/cat/cat.dm
new file mode 100644
index 0000000000000..425dd428ce5d6
--- /dev/null
+++ b/code/modules/mob/living/basic/pets/cat/cat.dm
@@ -0,0 +1,189 @@
+/mob/living/basic/pet/cat
+ name = "cat"
+ desc = "Kitty!!"
+ icon = 'icons/mob/simple/pets.dmi'
+ icon_state = "cat2"
+ icon_living = "cat2"
+ icon_dead = "cat2_dead"
+ speak_emote = list("purrs", "meows")
+ pass_flags = PASSTABLE
+ mob_size = MOB_SIZE_SMALL
+ mob_biotypes = MOB_ORGANIC|MOB_BEAST
+ unsuitable_atmos_damage = 0.5
+ butcher_results = list(
+ /obj/item/food/meat/slab = 1,
+ /obj/item/organ/internal/ears/cat = 1,
+ /obj/item/organ/external/tail/cat = 1,
+ /obj/item/stack/sheet/animalhide/cat = 1
+ )
+ response_help_continuous = "pets"
+ response_help_simple = "pet"
+ response_disarm_continuous = "gently pushes aside"
+ response_disarm_simple = "gently push aside"
+ response_harm_continuous = "kicks"
+ response_harm_simple = "kick"
+ mobility_flags = MOBILITY_FLAGS_REST_CAPABLE_DEFAULT
+ gold_core_spawnable = FRIENDLY_SPAWN
+ collar_icon_state = "cat"
+ has_collar_resting_icon_state = TRUE
+ can_be_held = TRUE
+ ai_controller = /datum/ai_controller/basic_controller/cat
+ held_state = "cat2"
+ attack_verb_continuous = "claws"
+ attack_verb_simple = "claw"
+ attack_sound = 'sound/weapons/slash.ogg'
+ attack_vis_effect = ATTACK_EFFECT_CLAW
+ ///can this cat breed?
+ var/can_breed = TRUE
+ ///can hold items?
+ var/can_hold_item = TRUE
+ ///can this cat interact with stoves?
+ var/can_interact_with_stove = FALSE
+ ///list of items we can carry
+ var/static/list/huntable_items = list(
+ /obj/item/fish,
+ /obj/item/food/deadmouse,
+ /obj/item/food/fishmeat,
+ )
+ ///item we are currently holding
+ var/obj/item/held_food
+ ///mutable appearance for held item
+ var/mutable_appearance/held_item_overlay
+
+/mob/living/basic/pet/cat/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/ai_retaliate)
+ AddElement(/datum/element/pet_bonus, "purrs!")
+ add_verb(src, /mob/living/proc/toggle_resting)
+ ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
+ ai_controller.set_blackboard_key(BB_HUNTABLE_PREY, typecacheof(huntable_items))
+ if(can_breed)
+ add_breeding_component()
+ if(can_hold_item)
+ RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(pre_attack))
+ if(can_interact_with_stove)
+ RegisterSignal(src, COMSIG_LIVING_EARLY_UNARMED_ATTACK, PROC_REF(pre_unarmed_attack))
+
+/mob/living/basic/pet/cat/proc/pre_attack(mob/living/source, atom/movable/target)
+ SIGNAL_HANDLER
+ if(!is_type_in_list(target, huntable_items) || held_food)
+ return
+ target.forceMove(src)
+
+/mob/living/basic/pet/cat/proc/pre_unarmed_attack(mob/living/hitter, atom/target, proximity, modifiers)
+ SIGNAL_HANDLER
+
+ if(istype(target, /obj/machinery/oven/range))
+ target.attack_hand(src)
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/mob/living/basic/pet/cat/Exited(atom/movable/gone, direction)
+ . = ..()
+ if(gone != held_food)
+ return
+ held_food = null
+ update_appearance(UPDATE_OVERLAYS)
+
+/mob/living/basic/pet/cat/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ if(is_type_in_list(arrived, huntable_items))
+ held_food = arrived
+ update_appearance(UPDATE_OVERLAYS)
+ return ..()
+
+/mob/living/basic/pet/cat/update_overlays()
+ . = ..()
+ if(stat == DEAD || resting || !held_food)
+ return
+ if(istype(held_food, /obj/item/fish))
+ held_item_overlay = mutable_appearance(icon, "cat_fish_overlay")
+ if(istype(held_food, /obj/item/food/deadmouse))
+ held_item_overlay = mutable_appearance(icon, "cat_mouse_overlay")
+ . += held_item_overlay
+
+/mob/living/basic/pet/cat/update_resting()
+ . = ..()
+ if(stat == DEAD)
+ return
+ update_appearance(UPDATE_ICON_STATE)
+
+/mob/living/basic/pet/cat/update_icon_state()
+ . = ..()
+ if (resting)
+ icon_state = "[icon_living]_rest"
+ return
+ icon_state = "[icon_living]"
+
+/mob/living/basic/pet/cat/proc/add_breeding_component()
+ AddComponent(\
+ /datum/component/breed,\
+ can_breed_with = typecacheof(list(/mob/living/basic/pet/cat)),\
+ baby_path = /mob/living/basic/pet/cat/kitten,\
+ )
+
+/mob/living/basic/pet/cat/space
+ name = "space cat"
+ desc = "They're a cat... in space!"
+ icon_state = "spacecat"
+ icon_living = "spacecat"
+ icon_dead = "spacecat_dead"
+ minimum_survivable_temperature = TCMB
+ maximum_survivable_temperature = T0C + 40
+ held_state = "spacecat"
+
+/mob/living/basic/pet/cat/breadcat
+ name = "bread cat"
+ desc = "They're a cat... with a bread!"
+ icon_state = "breadcat"
+ icon_living = "breadcat"
+ icon_dead = "breadcat_dead"
+ ai_controller = /datum/ai_controller/basic_controller/cat/bread
+ collar_icon_state = null
+ held_state = "breadcat"
+ can_interact_with_stove = TRUE
+ butcher_results = list(
+ /obj/item/food/meat/slab = 2,
+ /obj/item/organ/internal/ears/cat = 1,
+ /obj/item/organ/external/tail/cat = 1,
+ /obj/item/food/breadslice/plain = 1
+ )
+
+
+/mob/living/basic/pet/cat/original
+ name = "Batsy"
+ desc = "The product of alien DNA and bored geneticists."
+ gender = FEMALE
+ icon_state = "original"
+ icon_living = "original"
+ icon_dead = "original_dead"
+ collar_icon_state = null
+ unique_pet = TRUE
+ held_state = "original"
+
+/mob/living/basic/pet/cat/kitten
+ name = "kitten"
+ desc = "D'aaawwww."
+ icon_state = "kitten"
+ icon_living = "kitten"
+ icon_dead = "kitten_dead"
+ density = FALSE
+ pass_flags = PASSMOB
+ mob_size = MOB_SIZE_SMALL
+ collar_icon_state = "kitten"
+ can_breed = FALSE
+ ai_controller = /datum/ai_controller/basic_controller/cat/kitten
+ can_hold_item = FALSE
+
+/mob/living/basic/pet/cat/kitten/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/basic_eating, food_types = huntable_items)
+
+/mob/living/basic/pet/cat/_proc
+ name = "Proc"
+ gender = MALE
+ gold_core_spawnable = NO_SPAWN
+ unique_pet = TRUE
+
+/mob/living/basic/pet/cat/jerry //Holy shit we left jerry on donut ~ Arcane ~Fikou
+ name = "Jerry"
+ desc = "Tom is VERY amused."
+ gender = MALE
diff --git a/code/modules/mob/living/basic/pets/cat/cat_ai.dm b/code/modules/mob/living/basic/pets/cat/cat_ai.dm
new file mode 100644
index 0000000000000..b9436b555e505
--- /dev/null
+++ b/code/modules/mob/living/basic/pets/cat/cat_ai.dm
@@ -0,0 +1,299 @@
+/datum/ai_controller/basic_controller/cat
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_HOSTILE_MEOWS = list("Mawwww", "Mrewwww", "mhhhhng..."),
+ BB_BABIES_CHILD_TYPES = list(/mob/living/basic/pet/cat/kitten),
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/reside_in_home,
+ /datum/ai_planning_subtree/flee_target/from_flee_key/cat_struggle,
+ /datum/ai_planning_subtree/find_and_hunt_target/hunt_mice,
+ /datum/ai_planning_subtree/find_and_hunt_target/find_cat_food,
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/haul_food_to_young,
+ /datum/ai_planning_subtree/territorial_struggle,
+ /datum/ai_planning_subtree/make_babies,
+ /datum/ai_planning_subtree/random_speech/cats,
+ )
+
+/datum/ai_planning_subtree/reside_in_home
+ ///chance we enter our home
+ var/reside_chance = 5
+ ///chance we leave our home
+ var/leave_home_chance = 15
+
+/datum/ai_planning_subtree/reside_in_home/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/mob/living/living_pawn = controller.pawn
+
+ if(controller.blackboard_key_exists(BB_CAT_HOME))
+ controller.queue_behavior(/datum/ai_behavior/enter_cat_home, BB_CAT_HOME)
+ return
+
+ if(istype(living_pawn.loc, /obj/structure/cat_house))
+ if(SPT_PROB(leave_home_chance, seconds_per_tick))
+ controller.set_blackboard_key(BB_CAT_HOME, living_pawn.loc)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+ if(SPT_PROB(reside_chance, seconds_per_tick))
+ controller.queue_behavior(/datum/ai_behavior/find_and_set/valid_home, BB_CAT_HOME, /obj/structure/cat_house)
+
+/datum/ai_behavior/find_and_set/valid_home/search_tactic(datum/ai_controller/controller, locate_path, search_range)
+ for(var/obj/structure/cat_house/home in oview(search_range, controller.pawn))
+ if(home.resident_cat)
+ continue
+ return home
+
+ return null
+
+/datum/ai_behavior/enter_cat_home
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION | AI_BEHAVIOR_REQUIRE_REACH
+
+/datum/ai_behavior/enter_cat_home/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/enter_cat_home/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ . = ..()
+ var/obj/structure/cat_house/home = controller.blackboard[target_key]
+ var/mob/living/basic/living_pawn = controller.pawn
+ if(living_pawn == home.resident_cat || isnull(home.resident_cat))
+ living_pawn.melee_attack(home)
+ finish_action(controller, TRUE, target_key)
+ return
+
+ finish_action(controller, FALSE, target_key)
+
+/datum/ai_behavior/enter_cat_home/finish_action(datum/ai_controller/controller, success, target_key)
+ . = ..()
+ controller.clear_blackboard_key(target_key)
+
+
+/datum/ai_planning_subtree/flee_target/from_flee_key/cat_struggle
+ flee_behaviour = /datum/ai_behavior/run_away_from_target/cat_struggle
+
+/datum/ai_behavior/run_away_from_target/cat_struggle
+ clear_failed_targets = TRUE
+
+/datum/ai_planning_subtree/territorial_struggle
+ ///chance we become hostile to another cat
+ var/hostility_chance = 5
+
+/datum/ai_planning_subtree/territorial_struggle/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/mob/living/living_pawn = controller.pawn
+ if(living_pawn.gender != MALE || !SPT_PROB(hostility_chance, seconds_per_tick))
+ return
+ if(controller.blackboard_key_exists(BB_TRESSPASSER_TARGET))
+ controller.queue_behavior(/datum/ai_behavior/territorial_struggle, BB_TRESSPASSER_TARGET, BB_HOSTILE_MEOWS)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+ controller.queue_behavior(/datum/ai_behavior/find_and_set/cat_tresspasser, BB_TRESSPASSER_TARGET, /mob/living/basic/pet/cat)
+
+/datum/ai_behavior/find_and_set/cat_tresspasser/search_tactic(datum/ai_controller/controller, locate_path, search_range)
+ var/list/ignore_types = controller.blackboard[BB_BABIES_CHILD_TYPES]
+ for(var/mob/living/basic/pet/cat/potential_enemy in oview(search_range, controller.pawn))
+ if(potential_enemy.gender != MALE)
+ continue
+ if(is_type_in_list(potential_enemy, ignore_types))
+ continue
+ var/datum/ai_controller/basic_controller/enemy_controller = potential_enemy.ai_controller
+ if(isnull(enemy_controller))
+ continue
+ //theyre already engaged in a battle, leave them alone!
+ if(enemy_controller.blackboard_key_exists(BB_TRESSPASSER_TARGET))
+ continue
+ //u choose me and i choose u
+ enemy_controller.set_blackboard_key(BB_TRESSPASSER_TARGET, controller.pawn)
+ return potential_enemy
+ return null
+
+/datum/ai_behavior/territorial_struggle
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION | AI_BEHAVIOR_REQUIRE_REACH
+ action_cooldown = 5 SECONDS
+ ///chance the battle ends!
+ var/end_battle_chance = 25
+
+/datum/ai_behavior/territorial_struggle/setup(datum/ai_controller/controller, target_key)
+ . = ..()
+ var/mob/living/living_pawn = controller.pawn
+ var/mob/living/target = controller.blackboard[target_key]
+ if(QDELETED(target))
+ return FALSE
+ if(target.ai_controller?.blackboard[target_key] != living_pawn)
+ return FALSE
+ set_movement_target(controller, target)
+
+/datum/ai_behavior/territorial_struggle/perform(seconds_per_tick, datum/ai_controller/controller, target_key, cries_key)
+ . = ..()
+ var/mob/living/target = controller.blackboard[target_key]
+
+ if(QDELETED(target))
+ finish_action(controller, TRUE, target_key)
+ return
+
+ var/mob/living/living_pawn = controller.pawn
+ var/list/threaten_list = controller.blackboard[cries_key]
+ if(length(threaten_list))
+ living_pawn.say(pick(threaten_list), forced = "ai_controller")
+
+ if(!prob(end_battle_chance))
+ return
+
+ //50 50 chance we lose
+ var/datum/ai_controller/loser_controller = prob(50) ? controller : target.ai_controller
+
+ loser_controller.set_blackboard_key(BB_BASIC_MOB_FLEE_TARGET, target)
+ target.ai_controller.clear_blackboard_key(BB_TRESSPASSER_TARGET)
+ finish_action(controller, TRUE, target_key)
+
+/datum/ai_behavior/territorial_struggle/finish_action(datum/ai_controller/controller, success, target_key)
+ . = ..()
+ controller.clear_blackboard_key(target_key)
+
+/datum/ai_planning_subtree/find_and_hunt_target/hunt_mice
+ target_key = BB_MOUSE_TARGET
+ hunting_behavior = /datum/ai_behavior/play_with_mouse
+ finding_behavior = /datum/ai_behavior/find_hunt_target/hunt_mice
+ hunt_targets = list(/mob/living/basic/mouse)
+ hunt_chance = 75
+ hunt_range = 9
+
+/datum/ai_planning_subtree/find_and_hunt_target/hunt_mice/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/mob/living/living_pawn = controller.pawn
+ var/list/items_we_carry = typecache_filter_list(living_pawn, controller.blackboard[BB_HUNTABLE_PREY])
+ if(length(items_we_carry))
+ return
+ return ..()
+
+
+/datum/ai_behavior/find_hunt_target/hunt_mice/valid_dinner(mob/living/source, mob/living/mouse, radius)
+ if(mouse.stat == DEAD || mouse.mind)
+ return FALSE
+ return can_see(source, mouse, radius)
+
+//play as in kill
+/datum/ai_behavior/play_with_mouse
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION | AI_BEHAVIOR_REQUIRE_REACH
+ action_cooldown = 10 SECONDS
+ ///chance we hunt the mouse!
+ var/consume_chance = 70
+
+/datum/ai_behavior/play_with_mouse/setup(datum/ai_controller/controller, target_key)
+ . = ..()
+ var/mob/living/target = controller.blackboard[target_key]
+ if(QDELETED(target))
+ return FALSE
+ set_movement_target(controller, target)
+
+/datum/ai_behavior/play_with_mouse/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ . = ..()
+ var/mob/living/basic/mouse/target = controller.blackboard[target_key]
+
+ if(QDELETED(target))
+ finish_action(controller, TRUE, target_key)
+ return
+
+ consume_chance = istype(target, /mob/living/basic/mouse/brown/tom) ? 5 : initial(consume_chance)
+ if(prob(consume_chance))
+ target.splat()
+ finish_action(controller, TRUE, target_key)
+ return
+ finish_action(controller, FALSE, target_key)
+
+/datum/ai_behavior/play_with_mouse/finish_action(datum/ai_controller/controller, success, target_key)
+ . = ..()
+ var/mob/living/living_pawn = controller.pawn
+ var/atom/target = controller.blackboard[target_key]
+ controller.clear_blackboard_key(target_key)
+ if(isnull(target))
+ return
+ var/manual_emote = "attempts to hunt [target]..."
+ var/end_result = success ? "and succeeds!" : "but fails!"
+ manual_emote += end_result
+ living_pawn.manual_emote(manual_emote)
+
+/datum/ai_planning_subtree/find_and_hunt_target/find_cat_food
+ target_key = BB_CAT_FOOD_TARGET
+ hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target/find_cat_food
+ finding_behavior = /datum/ai_behavior/find_hunt_target/find_cat_food
+ hunt_targets = list(/obj/item/fish, /obj/item/food/deadmouse, /obj/item/food/fishmeat)
+ hunt_chance = 75
+ hunt_range = 9
+
+/datum/ai_behavior/hunt_target/unarmed_attack_target/find_cat_food
+ always_reset_target = TRUE
+
+/datum/ai_behavior/find_hunt_target/find_cat_food/valid_dinner(mob/living/source, atom/dinner, radius)
+ //this food is already near a kitten, let the kitten eat it
+ var/mob/living/nearby_kitten = locate(/mob/living/basic/pet/cat/kitten) in oview(2, dinner)
+ if(nearby_kitten && nearby_kitten != source)
+ return FALSE
+ return can_see(source, dinner, radius)
+
+/datum/ai_planning_subtree/haul_food_to_young/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(!controller.blackboard_key_exists(BB_FOOD_TO_DELIVER))
+ controller.queue_behavior(/datum/ai_behavior/find_and_set/in_hands/given_list, BB_FOOD_TO_DELIVER, controller.blackboard[BB_HUNTABLE_PREY])
+ return
+ if(!controller.blackboard_key_exists(BB_KITTEN_TO_FEED))
+ controller.queue_behavior(/datum/ai_behavior/find_and_set/valid_kitten, BB_KITTEN_TO_FEED, /mob/living/basic/pet/cat/kitten)
+ return
+
+ controller.queue_behavior(/datum/ai_behavior/deliver_food_to_kitten, BB_KITTEN_TO_FEED, BB_FOOD_TO_DELIVER)
+
+/datum/ai_behavior/find_and_set/valid_kitten
+
+/datum/ai_behavior/find_and_set/valid_kitten/search_tactic(datum/ai_controller/controller, locate_path, search_range)
+ var/mob/living/kitten = locate(locate_path) in oview(search_range, controller.pawn)
+ //kitten already has food near it, go feed another hungry kitten
+
+ if(isnull(kitten))
+ return null
+
+ var/list/nearby_food = typecache_filter_list(oview(2, kitten), controller.blackboard[BB_HUNTABLE_PREY])
+ if(kitten.stat != DEAD && !length(nearby_food))
+ return kitten
+ return null
+
+/datum/ai_behavior/deliver_food_to_kitten
+/datum/ai_behavior/deliver_food_to_kitten
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION | AI_BEHAVIOR_REQUIRE_REACH
+ action_cooldown = 5 SECONDS
+
+/datum/ai_behavior/deliver_food_to_kitten/setup(datum/ai_controller/controller, target_key, food_key)
+ . = ..()
+ var/mob/living/target = controller.blackboard[target_key]
+ if(QDELETED(target))
+ return FALSE
+ set_movement_target(controller, target)
+
+/datum/ai_behavior/deliver_food_to_kitten/perform(seconds_per_tick, datum/ai_controller/controller, target_key, food_key)
+ . = ..()
+ var/mob/living/target = controller.blackboard[target_key]
+
+ if(QDELETED(target))
+ finish_action(controller, FALSE, target_key, food_key)
+ return
+
+ var/mob/living/living_pawn = controller.pawn
+ var/atom/movable/food = controller.blackboard[food_key]
+
+ if(isnull(food) || !(food in living_pawn))
+ finish_action(controller, FALSE, target_key, food_key)
+ return
+
+ food.forceMove(get_turf(living_pawn))
+ finish_action(controller, TRUE, target_key, food_key)
+
+/datum/ai_behavior/deliver_food_to_kitten/finish_action(datum/ai_controller/controller, success, target_key, food_key)
+ . = ..()
+ controller.clear_blackboard_key(target_key)
+ controller.clear_blackboard_key(food_key)
+
+
+
+
diff --git a/code/modules/mob/living/basic/pets/cat/keeki.dm b/code/modules/mob/living/basic/pets/cat/keeki.dm
new file mode 100644
index 0000000000000..5cf16552e9a48
--- /dev/null
+++ b/code/modules/mob/living/basic/pets/cat/keeki.dm
@@ -0,0 +1,59 @@
+/mob/living/basic/pet/cat/cak
+ name = "Keeki"
+ desc = "She is a cat made out of cake."
+ icon_state = "cak"
+ icon_living = "cak"
+ icon_dead = "cak_dead"
+ health = 50
+ maxHealth = 50
+ gender = FEMALE
+ butcher_results = list(
+ /obj/item/organ/internal/brain = 1,
+ /obj/item/organ/internal/heart = 1,
+ /obj/item/food/cakeslice/birthday = 3,
+ /obj/item/food/meat/slab = 2
+ )
+ response_harm_continuous = "takes a bite out of"
+ response_harm_simple = "take a bite out of"
+ ai_controller = /datum/ai_controller/basic_controller/cat/cake
+ attacked_sound = 'sound/items/eatfood.ogg'
+ death_message = "loses her false life and collapses!"
+ death_sound = SFX_BODYFALL
+ held_state = "cak"
+ can_interact_with_stove = TRUE
+
+/mob/living/basic/pet/cat/cak/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/regenerator,\
+ regeneration_delay = 1 SECONDS,\
+ brute_per_second = 5,\
+ outline_colour = COLOR_PINK,\
+ )
+ var/static/list/on_consume = list(
+ /datum/reagent/consumable/nutriment = 0.4,
+ /datum/reagent/consumable/nutriment/vitamin = 0.4,
+ )
+ AddElement(/datum/element/consumable_mob, reagents_list = on_consume)
+
+/mob/living/basic/pet/cat/cak/CheckParts(list/parts)
+ . = ..()
+ var/obj/item/organ/internal/brain/candidate = locate(/obj/item/organ/internal/brain) in contents
+ if(isnull(candidate?.brainmob?.mind))
+ return
+ var/datum/mind/candidate_mind = candidate.brainmob.mind
+ candidate_mind.transfer_to(src)
+ candidate_mind.grab_ghost()
+ to_chat(src, "[span_boldbig("You are a cak!")] You're a harmless cat/cake hybrid that everyone loves. People can take bites out of you if they're hungry, but you regenerate health \
+ so quickly that it generally doesn't matter. You're remarkably resilient to any damage besides this and it's hard for you to really die at all. You should go around and bring happiness and \
+ free cake to the station!")
+ var/default_name = initial(name)
+ var/new_name = sanitize_name(reject_bad_text(tgui_input_text(src, "You are the [name]. Would you like to change your name to something else?", "Name change", default_name, MAX_NAME_LEN)), cap_after_symbols = FALSE)
+ if(new_name)
+ to_chat(src, span_notice("Your name is now [new_name]!"))
+ name = new_name
+
+/mob/living/basic/pet/cat/cak/spin(spintime, speed)
+ . = ..()
+ for(var/obj/item/food/donut/target in oview(1, src))
+ if(!target.is_decorated)
+ target.decorate_donut()
diff --git a/code/modules/mob/living/basic/pets/cat/kitten_ai.dm b/code/modules/mob/living/basic/pets/cat/kitten_ai.dm
new file mode 100644
index 0000000000000..355ec4d3e488d
--- /dev/null
+++ b/code/modules/mob/living/basic/pets/cat/kitten_ai.dm
@@ -0,0 +1,67 @@
+
+/datum/ai_controller/basic_controller/cat/kitten
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_HUNGRY_MEOW = list("mrrp...", "mraw..."),
+ BB_MAX_DISTANCE_TO_FOOD = 2,
+ )
+
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/flee_target,
+ /datum/ai_planning_subtree/beg_human,
+ /datum/ai_planning_subtree/find_and_hunt_target/find_cat_food/kitten,
+ /datum/ai_planning_subtree/random_speech/cats,
+ )
+
+//if the food is too far away, point at it or meow. if its near us then go eat it
+
+/datum/ai_planning_subtree/find_and_hunt_target/find_cat_food/kitten
+
+
+/datum/ai_planning_subtree/find_and_hunt_target/find_cat_food/kitten/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ var/atom/target = controller.blackboard[BB_CAT_FOOD_TARGET]
+ if(target && get_dist(target, controller.pawn) > controller.blackboard[BB_MAX_DISTANCE_TO_FOOD])
+ controller.queue_behavior(/datum/ai_behavior/beacon_for_food, BB_CAT_FOOD_TARGET, BB_HUNGRY_MEOW)
+ return
+ return ..()
+
+/datum/ai_behavior/beacon_for_food
+ action_cooldown = 5 SECONDS
+
+/datum/ai_behavior/beacon_for_food/perform(seconds_per_tick, datum/ai_controller/controller, target_key, meows_key)
+ . = ..()
+ var/atom/target = controller.blackboard[target_key]
+ if(QDELETED(target))
+ finish_action(controller, FALSE)
+ var/mob/living/living_pawn = controller.pawn
+ var/list/meowing_list = controller.blackboard[meows_key]
+ if(length(meowing_list))
+ living_pawn.say(pick(meowing_list), forced = "ai_controller")
+ living_pawn._pointed(target)
+ finish_action(controller, TRUE)
+
+/datum/ai_behavior/beacon_for_food/finish_action(datum/ai_controller/controller, success, target_key)
+ . = ..()
+ controller.clear_blackboard_key(target_key)
+
+/datum/ai_planning_subtree/beg_human
+
+/datum/ai_planning_subtree/beg_human/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+
+ if(controller.blackboard_key_exists(BB_HUMAN_BEG_TARGET))
+ controller.queue_behavior(/datum/ai_behavior/beacon_for_food, BB_HUMAN_BEG_TARGET, BB_HUNGRY_MEOW)
+ return
+
+ controller.queue_behavior(/datum/ai_behavior/find_and_set/human_beg, BB_HUMAN_BEG_TARGET, /mob/living/carbon/human)
+
+/datum/ai_behavior/find_and_set/human_beg/search_tactic(datum/ai_controller/controller, locate_path, search_range)
+ var/list/locate_items = controller.blackboard[BB_HUNTABLE_PREY]
+ for(var/mob/living/carbon/human/human_target in oview(search_range, controller.pawn))
+ if(human_target.stat != CONSCIOUS || isnull(human_target.mind))
+ continue
+ if(!length(typecache_filter_list(human_target.held_items, locate_items)))
+ continue
+ return human_target
+
+ return null
diff --git a/code/modules/mob/living/basic/pets/cat/runtime.dm b/code/modules/mob/living/basic/pets/cat/runtime.dm
new file mode 100644
index 0000000000000..9b3adac5a2aeb
--- /dev/null
+++ b/code/modules/mob/living/basic/pets/cat/runtime.dm
@@ -0,0 +1,99 @@
+#define RUNTIME_SAVE_DATA "data/npc_saves/Runtime.sav"
+#define RUNTIME_JSON_DATA "data/npc_saves/Runtime.json"
+#define MAX_CAT_DEPLOY 50
+
+/mob/living/basic/pet/cat/runtime
+ name = "Runtime"
+ desc = "GCAT"
+ icon_state = "cat"
+ icon_living = "cat"
+ icon_dead = "cat_dead"
+ gender = FEMALE
+ gold_core_spawnable = NO_SPAWN
+ unique_pet = TRUE
+ ///the family we will bring in when a round starts
+ var/list/family = null
+ ///saved list of kids
+ var/list/children = null
+ /// have we deployed the cats?
+ var/cats_deployed = FALSE
+ /// have we saved memory?
+ var/memory_saved = FALSE
+ ///callback we use to register our family
+ var/datum/callback/register_family
+
+/mob/living/basic/pet/cat/runtime/Initialize(mapload)
+ . = ..()
+ register_family = CALLBACK(src, PROC_REF(Write_Memory))
+ SSticker.OnRoundend(register_family)
+ if(mapload)
+ read_memory()
+ deploy_the_cats()
+
+ if(!prob(5))
+ return
+ icon_state = "original"
+ icon_living = "original"
+ icon_dead = "original_dead"
+ update_appearance()
+
+
+/mob/living/basic/pet/cat/runtime/add_breeding_component()
+ AddComponent(\
+ /datum/component/breed,\
+ can_breed_with = typecacheof(list(/mob/living/basic/pet/cat)),\
+ baby_path = /mob/living/basic/pet/cat/kitten,\
+ post_birth = CALLBACK(src, PROC_REF(after_birth)),\
+ )
+
+/mob/living/basic/pet/cat/runtime/proc/after_birth(mob/living/baby)
+ if(isnull(baby))
+ return
+ LAZYADD(children, baby)
+
+/mob/living/basic/pet/cat/runtime/proc/read_memory()
+ if(fexists(RUNTIME_SAVE_DATA))
+ var/savefile/save_data = new(RUNTIME_SAVE_DATA)
+ save_data["family"] >> family
+ fdel(RUNTIME_SAVE_DATA)
+ return
+ var/json_file = file(RUNTIME_JSON_DATA)
+ if(!fexists(json_file))
+ return
+ var/list/json_list = json_decode(file2text(json_file))
+ family = json_list["family"]
+
+/mob/living/basic/pet/cat/runtime/Destroy()
+ LAZYREMOVE(SSticker.round_end_events, register_family)
+ register_family = null
+ return ..()
+
+/mob/living/basic/pet/cat/runtime/Write_Memory(dead, gibbed)
+ . = ..()
+ if(!.)
+ return
+ var/json_file = file(RUNTIME_JSON_DATA)
+ var/list/file_data = list()
+ if(!dead)
+ for(var/mob/living/basic/pet/cat/kitten/kitten in children)
+ if(kitten.stat == DEAD)
+ continue
+ if(kitten.type in family)
+ family[kitten.type] += 1
+ continue
+ family[kitten.type] = 1
+ file_data["family"] = family
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(file_data, JSON_PRETTY_PRINT))
+
+/mob/living/basic/pet/cat/runtime/proc/deploy_the_cats()
+ cats_deployed = TRUE
+ for(var/cat_type in family)
+ if(isnull(family[cat_type]))
+ return
+ for(var/index in 1 to min(family[cat_type], MAX_CAT_DEPLOY))
+ new cat_type(loc)
+
+#undef RUNTIME_SAVE_DATA
+#undef RUNTIME_JSON_DATA
+#undef MAX_CAT_DEPLOY
diff --git a/code/modules/mob/living/basic/space_fauna/bear/_bear.dm b/code/modules/mob/living/basic/space_fauna/bear/_bear.dm
index 8ba6699648e60..e23e022d00e61 100644
--- a/code/modules/mob/living/basic/space_fauna/bear/_bear.dm
+++ b/code/modules/mob/living/basic/space_fauna/bear/_bear.dm
@@ -133,6 +133,11 @@
brute_per_second = 5,\
outline_colour = COLOR_YELLOW,\
)
+ var/static/list/on_consume = list(
+ /datum/reagent/consumable/nutriment = 1,
+ /datum/reagent/consumable/nutriment/vitamin = 0.1,
+ )
+ AddElement(/datum/element/consumable_mob, reagents_list = on_consume)
/mob/living/basic/bear/butter/attack_hand(mob/living/user, list/modifiers) //Borrowed code from Cak, feeds people if they hit you. More nutriment but less vitamin to represent BUTTER.
. = ..()
diff --git a/code/modules/mob/living/basic/space_fauna/demon/demon_subtypes.dm b/code/modules/mob/living/basic/space_fauna/demon/demon_subtypes.dm
index d8c419ea2e3bc..e7fbfa5c2ecf1 100644
--- a/code/modules/mob/living/basic/space_fauna/demon/demon_subtypes.dm
+++ b/code/modules/mob/living/basic/space_fauna/demon/demon_subtypes.dm
@@ -149,7 +149,7 @@
/// We do our own special thing on death, which is to spawn a kitten.
/mob/living/basic/demon/slaughter/laughter/proc/on_death()
SIGNAL_HANDLER
- var/mob/living/simple_animal/pet/cat/kitten/kitty = new(drop_location())
+ var/mob/living/basic/pet/cat/kitten/kitty = new(drop_location())
kitty.name = "Laughter"
/mob/living/basic/demon/slaughter/laughter/ex_act(severity)
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 8a91913809189..8fed63177313f 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -1442,6 +1442,8 @@
/mob/living/basic/mouse,
/mob/living/basic/mushroom,
/mob/living/basic/parrot,
+ /mob/living/basic/pet/cat,
+ /mob/living/basic/pet/cat/cak,
/mob/living/basic/pet/dog/breaddog,
/mob/living/basic/pet/dog/corgi,
/mob/living/basic/pet/dog/pug,
@@ -1452,11 +1454,8 @@
/mob/living/basic/stickman,
/mob/living/basic/stickman/dog,
/mob/living/simple_animal/hostile/megafauna/dragon/lesser,
- /mob/living/simple_animal/pet/cat,
- /mob/living/simple_animal/pet/cat/cak,
)
new_mob = new picked_animal(loc)
-
if(WABBAJACK_HUMAN)
var/mob/living/carbon/human/new_human = new(loc)
diff --git a/code/modules/mob/living/simple_animal/friendly/cat.dm b/code/modules/mob/living/simple_animal/friendly/cat.dm
deleted file mode 100644
index f1857a00e2c46..0000000000000
--- a/code/modules/mob/living/simple_animal/friendly/cat.dm
+++ /dev/null
@@ -1,332 +0,0 @@
-//Cat
-/mob/living/simple_animal/pet/cat
- name = "cat"
- desc = "Kitty!!"
- icon = 'icons/mob/simple/pets.dmi'
- icon_state = "cat2"
- icon_living = "cat2"
- icon_dead = "cat2_dead"
- speak = list("Meow!", "Esp!", "Purr!", "HSSSSS")
- speak_emote = list("purrs", "meows")
- emote_hear = list("meows.", "mews.")
- emote_see = list("shakes their head.", "shivers.")
- speak_chance = 1
- turns_per_move = 5
- pass_flags = PASSTABLE
- mob_size = MOB_SIZE_SMALL
- mob_biotypes = MOB_ORGANIC|MOB_BEAST
- minbodytemp = 200
- maxbodytemp = 400
- unsuitable_atmos_damage = 0.5
- animal_species = /mob/living/simple_animal/pet/cat
- childtype = list(/mob/living/simple_animal/pet/cat/kitten = 1)
- butcher_results = list(/obj/item/food/meat/slab = 1, /obj/item/organ/internal/ears/cat = 1, /obj/item/organ/external/tail/cat = 1, /obj/item/stack/sheet/animalhide/cat = 1)
- response_help_continuous = "pets"
- response_help_simple = "pet"
- response_disarm_continuous = "gently pushes aside"
- response_disarm_simple = "gently push aside"
- response_harm_continuous = "kicks"
- response_harm_simple = "kick"
- mobility_flags = MOBILITY_FLAGS_REST_CAPABLE_DEFAULT
- var/mob/living/basic/mouse/movement_target
- gold_core_spawnable = FRIENDLY_SPAWN
- collar_icon_state = "cat"
- has_collar_resting_icon_state = TRUE
- can_be_held = TRUE
- held_state = "cat2"
- attack_verb_continuous = "claws"
- attack_verb_simple = "claw"
- attack_sound = 'sound/weapons/slash.ogg'
- attack_vis_effect = ATTACK_EFFECT_CLAW
-
- footstep_type = FOOTSTEP_MOB_CLAW
-
-/mob/living/simple_animal/pet/cat/Initialize(mapload)
- . = ..()
- AddElement(/datum/element/pet_bonus, "purrs!")
- add_verb(src, /mob/living/proc/toggle_resting)
- add_cell_sample()
- ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
-
-/mob/living/simple_animal/pet/cat/add_cell_sample()
- AddElement(/datum/element/swabable, CELL_LINE_TABLE_CAT, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
-
-
-/mob/living/simple_animal/pet/cat/space
- name = "space cat"
- desc = "They're a cat... in space!"
- icon_state = "spacecat"
- icon_living = "spacecat"
- icon_dead = "spacecat_dead"
- unsuitable_atmos_damage = 0
- minbodytemp = TCMB
- maxbodytemp = T0C + 40
- held_state = "spacecat"
-
-/mob/living/simple_animal/pet/cat/breadcat
- name = "bread cat"
- desc = "They're a cat... with a bread!"
- icon_state = "breadcat"
- icon_living = "breadcat"
- icon_dead = "breadcat_dead"
- collar_icon_state = null
- held_state = "breadcat"
- butcher_results = list(/obj/item/food/meat/slab = 2, /obj/item/organ/internal/ears/cat = 1, /obj/item/organ/external/tail/cat = 1, /obj/item/food/breadslice/plain = 1)
-
-/mob/living/simple_animal/pet/cat/breadcat/add_cell_sample()
- return
-
-/mob/living/simple_animal/pet/cat/original
- name = "Batsy"
- desc = "The product of alien DNA and bored geneticists."
- gender = FEMALE
- icon_state = "original"
- icon_living = "original"
- icon_dead = "original_dead"
- collar_icon_state = null
- unique_pet = TRUE
- held_state = "original"
-
-/mob/living/simple_animal/pet/cat/original/add_cell_sample()
- return
-
-/mob/living/simple_animal/pet/cat/kitten
- name = "kitten"
- desc = "D'aaawwww."
- icon_state = "kitten"
- icon_living = "kitten"
- icon_dead = "kitten_dead"
- density = FALSE
- pass_flags = PASSMOB
- mob_size = MOB_SIZE_SMALL
- collar_icon_state = "kitten"
-
-//RUNTIME IS ALIVE! SQUEEEEEEEE~
-/mob/living/simple_animal/pet/cat/runtime
- name = "Runtime"
- desc = "GCAT"
- icon_state = "cat"
- icon_living = "cat"
- icon_dead = "cat_dead"
- gender = FEMALE
- gold_core_spawnable = NO_SPAWN
- unique_pet = TRUE
- var/list/family = list()//var restored from savefile, has count of each child type
- var/list/children = list()//Actual mob instances of children
- var/static/cats_deployed = 0
- var/memory_saved = FALSE
- held_state = "cat"
-
-/mob/living/simple_animal/pet/cat/runtime/Initialize(mapload)
- if(prob(5))
- icon_state = "original"
- icon_living = "original"
- icon_dead = "original_dead"
- Read_Memory()
- . = ..()
-
-/mob/living/simple_animal/pet/cat/runtime/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- if(!cats_deployed && SSticker.current_state >= GAME_STATE_SETTING_UP)
- Deploy_The_Cats()
- if(!stat && SSticker.current_state == GAME_STATE_FINISHED && !memory_saved)
- Write_Memory()
- memory_saved = TRUE
- ..()
-
-/mob/living/simple_animal/pet/cat/runtime/make_babies()
- var/mob/baby = ..()
- if(baby)
- children += baby
- return baby
-
-/mob/living/simple_animal/pet/cat/runtime/death()
- if(!memory_saved)
- Write_Memory(TRUE)
- ..()
-
-/mob/living/simple_animal/pet/cat/runtime/proc/Read_Memory()
- if(fexists("data/npc_saves/Runtime.sav")) //legacy compatability to convert old format to new
- var/savefile/S = new /savefile("data/npc_saves/Runtime.sav")
- S["family"] >> family
- fdel("data/npc_saves/Runtime.sav")
- else
- var/json_file = file("data/npc_saves/Runtime.json")
- if(!fexists(json_file))
- return
- var/list/json = json_decode(file2text(json_file))
- family = json["family"]
- if(isnull(family))
- family = list()
-
-/mob/living/simple_animal/pet/cat/runtime/Write_Memory(dead, gibbed)
- . = ..()
- if(!.)
- return
- var/json_file = file("data/npc_saves/Runtime.json")
- var/list/file_data = list()
- family = list()
- if(!dead)
- for(var/mob/living/simple_animal/pet/cat/kitten/C in children)
- if(istype(C,type) || C.stat || !C.z || (C.flags_1 & HOLOGRAM_1))
- continue
- if(C.type in family)
- family[C.type] += 1
- else
- family[C.type] = 1
- file_data["family"] = family
- fdel(json_file)
- WRITE_FILE(json_file, json_encode(file_data))
-
-/mob/living/simple_animal/pet/cat/runtime/proc/Deploy_The_Cats()
- cats_deployed = 1
- for(var/cat_type in family)
- if(family[cat_type] > 0)
- for(var/i in 1 to min(family[cat_type],100)) //Limits to about 500 cats, you wouldn't think this would be needed (BUT IT IS)
- new cat_type(loc)
-
-/mob/living/simple_animal/pet/cat/_proc
- name = "Proc"
- gender = MALE
- gold_core_spawnable = NO_SPAWN
- unique_pet = TRUE
-
-
-/mob/living/simple_animal/pet/cat/update_resting()
- . = ..()
- if(stat == DEAD)
- return
- if (resting)
- icon_state = "[icon_living]_rest"
- else
- icon_state = "[icon_living]"
-
-
-/mob/living/simple_animal/pet/cat/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- if(!stat && !buckled && !client)
- if(SPT_PROB(0.5, seconds_per_tick))
- manual_emote(pick("stretches out for a belly rub.", "wags [p_their()] tail.", "lies down."))
- set_resting(TRUE)
- else if(SPT_PROB(0.5, seconds_per_tick))
- manual_emote(pick("sits down.", "crouches on [p_their()] hind legs.", "looks alert."))
- set_resting(TRUE)
- icon_state = "[icon_living]_sit"
- cut_overlays() // No collar support in sitting state
- else if(SPT_PROB(0.5, seconds_per_tick))
- if (resting)
- manual_emote(pick("gets up and meows.", "walks around.", "stops resting."))
- set_resting(FALSE)
- else
- manual_emote(pick("grooms [p_their()] fur.", "twitches [p_their()] whiskers.", "shakes out [p_their()] coat."))
-
- //MICE! RATS! OH MY!
- if((src.loc) && isturf(src.loc))
- if(!stat && !buckled)
- //Targeting anything in the rat faction nearby
- for(var/mob/living/M in view(1,src))
- if(!M.stat && Adjacent(M))
- if (FACTION_RAT in M.faction)
- //Jerry can never catch Tom snowflaking
- if(istype(M, /mob/living/basic/mouse/brown/tom) && inept_hunter)
- if(COOLDOWN_FINISHED(src, emote_cooldown))
- visible_message(span_warning("[src] chases [M] around, to no avail!"))
- step(M, pick(GLOB.cardinals))
- COOLDOWN_START(src, emote_cooldown, 1 MINUTES)
- break
- //Mouse splatting
- if(ismouse(M))
- manual_emote("splats \the [M]!")
- var/mob/living/basic/mouse/snack = M
- snack.splat()
- movement_target = null
- stop_automated_movement = 0
- break
- for(var/obj/item/toy/cattoy/T in view(1,src))
- if (T.cooldown < (world.time - 400))
- manual_emote("bats \the [T] around with \his paw!")
- T.cooldown = world.time
-
- ..()
-
- make_babies()
-
- if(!stat && !resting && !buckled)
- turns_since_scan++
- if(turns_since_scan > 5)
- SSmove_manager.stop_looping(src)
- turns_since_scan = 0
- if((movement_target) && !(isturf(movement_target.loc) || ishuman(movement_target.loc) ))
- movement_target = null
- stop_automated_movement = 0
- if( !movement_target || !(movement_target.loc in oview(src, 3)) )
- movement_target = null
- stop_automated_movement = 0
- //Targeting mice and mobs in the rat faction
- for(var/mob/living/target in oview(src,3))
- if(isturf(target.loc) && !target.stat)
- if(FACTION_RAT in target.faction)
- movement_target = target
- break
- if(movement_target)
- stop_automated_movement = 1
- SSmove_manager.move_to(src, movement_target, 0, 3)
-
-/mob/living/simple_animal/pet/cat/jerry //Holy shit we left jerry on donut ~ Arcane ~Fikou
- name = "Jerry"
- desc = "Tom is VERY amused."
- inept_hunter = TRUE
- gender = MALE
-
-/mob/living/simple_animal/pet/cat/cak //I told you I'd do it, Remie
- name = "Keeki"
- desc = "She is a cat made out of cake."
- icon_state = "cak"
- icon_living = "cak"
- icon_dead = "cak_dead"
- health = 50
- maxHealth = 50
- gender = FEMALE
- harm_intent_damage = 10
- butcher_results = list(/obj/item/organ/internal/brain = 1, /obj/item/organ/internal/heart = 1, /obj/item/food/cakeslice/birthday = 3, \
- /obj/item/food/meat/slab = 2)
- response_harm_continuous = "takes a bite out of"
- response_harm_simple = "take a bite out of"
- attacked_sound = 'sound/items/eatfood.ogg'
- death_message = "loses her false life and collapses!"
- death_sound = SFX_BODYFALL
- held_state = "cak"
-
-/mob/living/simple_animal/pet/cat/cak/add_cell_sample()
- return
-
-/mob/living/simple_animal/pet/cat/cak/CheckParts(list/parts)
- ..()
- var/obj/item/organ/internal/brain/candidate = locate(/obj/item/organ/internal/brain) in contents
- if(!candidate || !candidate.brainmob || !candidate.brainmob.mind)
- return
- var/datum/mind/candidate_mind = candidate.brainmob.mind
- candidate_mind.transfer_to(src)
- candidate_mind.grab_ghost()
- to_chat(src, "[span_boldbig("You are a cak!")] You're a harmless cat/cake hybrid that everyone loves. People can take bites out of you if they're hungry, but you regenerate health \
- so quickly that it generally doesn't matter. You're remarkably resilient to any damage besides this and it's hard for you to really die at all. You should go around and bring happiness and \
- free cake to the station!")
- var/default_name = "Keeki"
- var/new_name = sanitize_name(reject_bad_text(tgui_input_text(src, "You are the [name]. Would you like to change your name to something else?", "Name change", default_name, MAX_NAME_LEN)), cap_after_symbols = FALSE)
- if(new_name)
- to_chat(src, span_notice("Your name is now [new_name]!"))
- name = new_name
-
-/mob/living/simple_animal/pet/cat/cak/Life(seconds_per_tick = SSMOBS_DT, times_fired)
- ..()
- if(stat)
- return
- if(health < maxHealth)
- adjustBruteLoss(-4 * seconds_per_tick) //Fast life regen
- for(var/obj/item/food/donut/D in range(1, src)) //Frosts nearby donuts!
- if(!D.is_decorated)
- D.decorate_donut()
-
-/mob/living/simple_animal/pet/cat/cak/attack_hand(mob/living/user, list/modifiers)
- ..()
- if(user.combat_mode && user.reagents && !stat)
- user.reagents.add_reagent(/datum/reagent/consumable/nutriment, 0.4)
- user.reagents.add_reagent(/datum/reagent/consumable/nutriment/vitamin, 0.4)
diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm
index 6136f8c818964..1c5543bbb59dd 100644
--- a/code/modules/mob/transform_procs.dm
+++ b/code/modules/mob/transform_procs.dm
@@ -380,7 +380,7 @@
return FALSE //Verbs do not appear for players.
//Good mobs!
- if(ispath(MP, /mob/living/simple_animal/pet/cat))
+ if(ispath(MP, /mob/living/basic/pet/cat))
return TRUE
if(ispath(MP, /mob/living/basic/pet/dog/corgi))
return TRUE
diff --git a/code/modules/research/experimentor.dm b/code/modules/research/experimentor.dm
index 51f6c65a85664..f2dd21e7d77fa 100644
--- a/code/modules/research/experimentor.dm
+++ b/code/modules/research/experimentor.dm
@@ -90,7 +90,7 @@
. = ..()
tracked_ian_ref = WEAKREF(locate(/mob/living/basic/pet/dog/corgi/ian) in GLOB.mob_living_list)
- tracked_runtime_ref = WEAKREF(locate(/mob/living/simple_animal/pet/cat/runtime) in GLOB.mob_living_list)
+ tracked_runtime_ref = WEAKREF(locate(/mob/living/basic/pet/cat/runtime) in GLOB.mob_living_list)
critical_items_typecache = typecacheof(list(
/obj/item/construction/rcd,
@@ -511,7 +511,7 @@
tracked_runtime.forceMove(drop_location())
investigate_log("Experimentor has stolen Runtime!", INVESTIGATE_EXPERIMENTOR)
else
- new /mob/living/simple_animal/pet/cat(loc)
+ new /mob/living/basic/pet/cat(loc)
investigate_log("Experimentor failed to steal runtime, and instead spawned a new cat.", INVESTIGATE_EXPERIMENTOR)
ejectItem(TRUE)
if(globalMalf > 76 && globalMalf < 98)
@@ -642,10 +642,10 @@
/mob/living/basic/lizard,
/mob/living/basic/mouse,
/mob/living/basic/parrot,
+ /mob/living/basic/pet/cat,
/mob/living/basic/pet/dog/corgi,
/mob/living/basic/pet/dog/pug,
/mob/living/basic/pet/fox,
- /mob/living/simple_animal/pet/cat,
)
for(var/counter in 1 to rand(1, 25))
var/mobType = pick(valid_animals)
diff --git a/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm b/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
index 5e9045e751f98..3230f44dabb42 100644
--- a/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
+++ b/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
@@ -108,7 +108,7 @@
/datum/reagent/consumable/milk/chocolate_milk = -1)
virus_suspectibility = 1.5
- resulting_atoms = list(/mob/living/simple_animal/pet/cat = 1) //The basic cat mobs are all male, so you mightt need a gender swap potion if you want to fill the fortress with kittens.
+ resulting_atoms = list(/mob/living/basic/pet/cat = 1)
/datum/micro_organism/cell_line/corgi
desc = "Canid cells"
diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm
index 3066ed2f49888..04b5bbf7c6ca9 100644
--- a/code/modules/unit_tests/simple_animal_freeze.dm
+++ b/code/modules/unit_tests/simple_animal_freeze.dm
@@ -93,15 +93,6 @@
/mob/living/simple_animal/hostile/vatbeast,
/mob/living/simple_animal/hostile/zombie,
/mob/living/simple_animal/pet,
- /mob/living/simple_animal/pet/cat,
- /mob/living/simple_animal/pet/cat/_proc,
- /mob/living/simple_animal/pet/cat/breadcat,
- /mob/living/simple_animal/pet/cat/cak,
- /mob/living/simple_animal/pet/cat/jerry,
- /mob/living/simple_animal/pet/cat/kitten,
- /mob/living/simple_animal/pet/cat/original,
- /mob/living/simple_animal/pet/cat/runtime,
- /mob/living/simple_animal/pet/cat/space,
/mob/living/simple_animal/pet/gondola,
/mob/living/simple_animal/pet/gondola/gondolapod,
/mob/living/simple_animal/pet/gondola/virtual_domain,
diff --git a/icons/mob/pets.dmi b/icons/mob/pets.dmi
index 7c6800d602de3..8ddeaa0c3f40c 100644
Binary files a/icons/mob/pets.dmi and b/icons/mob/pets.dmi differ
diff --git a/icons/mob/simple/pets.dmi b/icons/mob/simple/pets.dmi
index 78212b93c769e..9bd7d69c06bc5 100644
Binary files a/icons/mob/simple/pets.dmi and b/icons/mob/simple/pets.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 7275909035d8a..33a57719ea8e2 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -1339,6 +1339,7 @@
#include "code\datums\elements\cliff_walker.dm"
#include "code\datums\elements\climbable.dm"
#include "code\datums\elements\connect_loc.dm"
+#include "code\datums\elements\consumable_mob.dm"
#include "code\datums\elements\content_barfer.dm"
#include "code\datums\elements\crackable.dm"
#include "code\datums\elements\crusher_loot.dm"
@@ -2511,6 +2512,7 @@
#include "code\game\objects\structures\billboard.dm"
#include "code\game\objects\structures\bonfire.dm"
#include "code\game\objects\structures\broken_flooring.dm"
+#include "code\game\objects\structures\cat_house.dm"
#include "code\game\objects\structures\chess.dm"
#include "code\game\objects\structures\containers.dm"
#include "code\game\objects\structures\deployable_turret.dm"
@@ -4593,6 +4595,12 @@
#include "code\modules\mob\living\basic\pets\penguin.dm"
#include "code\modules\mob\living\basic\pets\pet.dm"
#include "code\modules\mob\living\basic\pets\sloth.dm"
+#include "code\modules\mob\living\basic\pets\cat\bread_cat_ai.dm"
+#include "code\modules\mob\living\basic\pets\cat\cat.dm"
+#include "code\modules\mob\living\basic\pets\cat\cat_ai.dm"
+#include "code\modules\mob\living\basic\pets\cat\keeki.dm"
+#include "code\modules\mob\living\basic\pets\cat\kitten_ai.dm"
+#include "code\modules\mob\living\basic\pets\cat\runtime.dm"
#include "code\modules\mob\living\basic\pets\dog\_dog.dm"
#include "code\modules\mob\living\basic\pets\dog\corgi.dm"
#include "code\modules\mob\living\basic\pets\dog\dog_subtypes.dm"
@@ -4870,7 +4878,6 @@
#include "code\modules\mob\living\simple_animal\bot\secbot.dm"
#include "code\modules\mob\living\simple_animal\bot\SuperBeepsky.dm"
#include "code\modules\mob\living\simple_animal\bot\vibebot.dm"
-#include "code\modules\mob\living\simple_animal\friendly\cat.dm"
#include "code\modules\mob\living\simple_animal\friendly\gondola.dm"
#include "code\modules\mob\living\simple_animal\friendly\pet.dm"
#include "code\modules\mob\living\simple_animal\hostile\alien.dm"
diff --git a/tools/UpdatePaths/Scripts/79800_basic_cats.txt b/tools/UpdatePaths/Scripts/79800_basic_cats.txt
new file mode 100644
index 0000000000000..7a990f790865d
--- /dev/null
+++ b/tools/UpdatePaths/Scripts/79800_basic_cats.txt
@@ -0,0 +1 @@
+/mob/living/simple_animal/pet/cat/@SUBTYPES : /mob/living/basic/pet/cat/@SUBTYPES{@OLD}
\ No newline at end of file