basic cats and mini kitchen helpers (#79800)

## About The Pull Request
this pr transforms cats into basic pets! cats now have some new
behavior. they can carry fish and hunted mice in their mouths to deliver
it to kittens, and kittens will eat them.


![catmouse](https://github.com/tgstation/tgstation/assets/138636438/8f146be4-c7b2-41d3-8301-734be49b5efc)

![catfish](https://github.com/tgstation/tgstation/assets/138636438/f8df54f2-9183-406d-afbd-f90f415f7f3d)

if a kitten sees you holding food, it will point at you and meow loudly
until u give it the food.
becareful when putting male cats near each other, there is a small
chance they get into a heated argument and meow loudly at each other
until one of them flees.
also added a new small cat house for cats. cats will use these homes if
u build one near them (using 5 wood planks)


![cathouse](https://github.com/tgstation/tgstation/assets/138636438/9515a78c-fdfe-461b-bad2-6b497117c694)

Chefs can craft the cake cat and breadcat. these are useful cats because
they can help the chef around in the kitchen. they will turn stoves and
grills off when food is ready, so they dont burn. and the cake cat will
help the chef decorate his donuts

## Why It's Good For The Game
refactors cats into basic mobs and gives them a deeper ai

## Changelog
🆑
refactor: cats are now basic pets. please report any bugs.
add: the cake cat and bread cat can now help the chef around in the
kitchen
/🆑
This commit is contained in:
Ben10Omintrix
2023-11-28 21:51:37 -07:00
committed by GitHub
parent 3516674802
commit ce6f2724cd
52 changed files with 968 additions and 400 deletions
@@ -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)
@@ -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
@@ -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)
@@ -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!")]<b> 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!</b>")
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 <b>[new_name]</b>!"))
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()
@@ -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
@@ -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