## About The Pull Request
adds turtles to the game! but these aren't your typical turtles. 

![turtlestates](https://github.com/user-attachments/assets/b9d35a32-5f04-4255-bbac-d1ed57b2abb1)

these are flora-turtles, with giant trees growing on their shells. These
trees can emit fields which affects nearby hydroponic plants. Initially,
the trees start out as young buds, from there the tree can evolve into
different types depending on what you feed the turtle.

Feeding them pesticides causes the tree to blossom to be purple. this
tree's fields will help kill some pests and weeds in nearby plants.

Feeding them nutrients gives you the green tree, the fields will heal
nearby plants

Feeding them mutators like uranium or left 4 zed gives you the yellow
tree. the fields increase instability of plants

The turtle will emit these fields every once in a while, ONLY when its
feeling happy. therefore you'll have to pet it, clean it and feed it
every once in a while to keep it satisfied. You can view the turtle's
happiness by shift clicking it.


https://github.com/user-attachments/assets/a47136a1-06a1-419e-acc2-2f6f4468e296

The turtle only eats seeds. after eating a seed, itll process it and
spit out its corresponding fruit! (for example, feeding it an apple seed
gives you an apple). itll also sometimes playfully headbutt your legs
and it loves going around smelling the scent from nearby plants

you can get these turtles by fishing the hydroponics tray or by ordering
them through cargo.

## Why It's Good For The Game
adds a new fun way for botanists to take care of their plants. While
these turtles alone arent enough to fully replace plant dedicated
nutrients, they add small extra support.

## Changelog
🆑
add: adds flora-turtles. obtainable through cargo or by fishing from the
hydroponics tray
/🆑
This commit is contained in:
Ben10Omintrix
2024-12-23 00:35:45 +01:00
committed by GitHub
parent 2ca491e194
commit 7d9386bdce
14 changed files with 511 additions and 7 deletions
@@ -0,0 +1,240 @@
#define PATH_PEST_KILLER "path_pest_killer"
#define PATH_PLANT_HEALER "path_plant_healer"
#define PATH_PLANT_MUTATOR "path_plant_mutator"
#define REQUIRED_TREE_GROWTH 250
#define UPPER_BOUND_VOLUME 50
#define LOWER_BOUND_VOLUME 10
/mob/living/basic/turtle
name = "turtle"
desc = "Dog."
icon_state = "turtle"
icon_living = "turtle"
icon_dead = "turtle_dead"
base_icon_state = "turtle"
icon = 'icons/mob/simple/pets.dmi'
butcher_results = list(/obj/item/food/meat/slab = 3, /obj/item/food/pickle = 1, /obj/item/stack/sheet/mineral/wood = 10)
mob_biotypes = MOB_ORGANIC | MOB_PLANT
mobility_flags = MOBILITY_FLAGS_REST_CAPABLE_DEFAULT
health = 100
maxHealth = 100
speed = 5
verb_say = "snaps"
verb_ask = "snaps curiously"
verb_exclaim = "snaps loudly"
verb_yell = "snaps loudly"
faction = list(FACTION_NEUTRAL)
ai_controller = /datum/ai_controller/basic_controller/turtle
///our displayed tree
var/mutable_appearance/grown_tree
///growth progress of our tree
var/list/path_growth_progress = list(
PATH_PLANT_HEALER = 0,
PATH_PLANT_MUTATOR = 0,
PATH_PEST_KILLER = 0,
)
///what nutrients leads to each evolution path
var/static/list/path_requirements = list(
//plant healers
/datum/reagent/plantnutriment/eznutriment = PATH_PLANT_HEALER,
/datum/reagent/plantnutriment/robustharvestnutriment = PATH_PLANT_HEALER,
/datum/reagent/plantnutriment/endurogrow = PATH_PLANT_HEALER,
//plant mutators
/datum/reagent/plantnutriment/left4zednutriment = PATH_PLANT_MUTATOR,
/datum/reagent/uranium = PATH_PLANT_MUTATOR,
//pest killers
/datum/reagent/toxin/pestkiller = PATH_PEST_KILLER,
)
///if we are fully grown, what is our path
var/developed_path
///our last east/west direction
var/last_direction = WEST
/mob/living/basic/turtle/Initialize(mapload)
. = ..()
desc = pick(
"Likely Dog...",
"Praise the Dog!",
"Dog ahead.",
"Could this be a Dog?",
)
var/static/list/eatable_food = list(/obj/item/seeds)
ai_controller.set_blackboard_key(BB_BASIC_FOODS, typecacheof(eatable_food))
AddElement(/datum/element/basic_eating, food_types = eatable_food)
AddComponent(/datum/component/happiness)
RegisterSignal(src, COMSIG_MOB_PRE_EAT, PROC_REF(pre_eat_food))
update_appearance()
create_reagents(150, REAGENT_HOLDER_ALIVE)
add_verb(src, /mob/living/proc/toggle_resting)
START_PROCESSING(SSprocessing, src)
/mob/living/basic/turtle/setDir(newdir)
if(REVERSE_DIR(last_direction) & newdir)
transform = transform.Scale(-1, 1)
last_direction = REVERSE_DIR(last_direction)
return ..()
/mob/living/basic/turtle/proc/retrieve_destined_path()
var/current_max_growth = 0
var/destined_path
for(var/evolution_path in path_growth_progress)
if(path_growth_progress[evolution_path] > current_max_growth)
destined_path = evolution_path
current_max_growth = path_growth_progress[evolution_path]
if(isnull(destined_path))
destined_path = PATH_PLANT_HEALER
return destined_path
/mob/living/basic/turtle/process(seconds_per_tick)
if(isnull(reagents) || !length(reagents.reagent_list)) //if we have no reagents, default to our highest destined path
set_plant_growth(retrieve_destined_path(), 0.5)
return
for(var/datum/reagent/existing_reagent as anything in reagents.reagent_list)
var/evolution_path = path_requirements[existing_reagent.type]
switch(existing_reagent.volume)
if(UPPER_BOUND_VOLUME to INFINITY)
set_plant_growth(evolution_path, 3)
if(LOWER_BOUND_VOLUME to UPPER_BOUND_VOLUME)
set_plant_growth(evolution_path, 2)
if(1 to LOWER_BOUND_VOLUME)
set_plant_growth(evolution_path, 1)
reagents.remove_reagent(existing_reagent.type, 0.5)
/mob/living/basic/turtle/proc/set_plant_growth(evolution_path, amount)
path_growth_progress[evolution_path] += amount
if(path_growth_progress[evolution_path] >= REQUIRED_TREE_GROWTH)
evolve_turtle(evolution_path)
/mob/living/basic/turtle/examine(mob/user)
. = ..()
if(stat == DEAD)
. += span_notice("Its tree seems to be all withered...")
return
var/destined_path = retrieve_destined_path()
var/current_max_growth = path_growth_progress[destined_path]
var/text_to_display = "Its tree seems to be exuding "
switch(destined_path)
if(PATH_PEST_KILLER)
text_to_display += "pest killing"
if(PATH_PLANT_HEALER)
text_to_display += "plant healing"
if(PATH_PLANT_MUTATOR)
text_to_display += "plant mutating"
text_to_display += " properties... which [current_max_growth >= REQUIRED_TREE_GROWTH ? "seems to be fully grown" : "is yet to develop"]."
. += span_notice(text_to_display)
/mob/living/basic/turtle/proc/evolve_turtle(evolution_path)
var/static/list/evolution_gains = list(
PATH_PLANT_HEALER = list(
"tree_appearance" = "healer_tree",
"tree_ability" = /datum/action/cooldown/mob_cooldown/turtle_tree/healer,
),
PATH_PEST_KILLER = list(
"tree_appearance" = "killer_tree",
"tree_ability" = /datum/action/cooldown/mob_cooldown/turtle_tree/killer,
),
PATH_PLANT_MUTATOR = list(
"tree_appearance" = "mutator_tree",
"tree_ability" = /datum/action/cooldown/mob_cooldown/turtle_tree/mutator,
),
)
var/tree_icon_state = evolution_gains[evolution_path]["tree_appearance"]
grown_tree = mutable_appearance(icon = 'icons/mob/simple/turtle_trees.dmi', icon_state = tree_icon_state)
var/new_ability_path = evolution_gains[evolution_path]["tree_ability"]
developed_path = evolution_path
var/datum/action/cooldown/tree_ability = new new_ability_path(src)
tree_ability?.Grant(src)
ai_controller?.set_blackboard_key(BB_TURTLE_TREE_ABILITY, tree_ability)
STOP_PROCESSING(SSprocessing, src)
update_appearance()
/mob/living/basic/turtle/update_icon_state()
. = ..()
if(stat == DEAD)
return
icon_state = resting ? "[base_icon_state]_resting" : base_icon_state
/mob/living/basic/turtle/update_overlays()
. = ..()
if(stat == DEAD)
var/mutable_appearance/dead_overlay = mutable_appearance(icon = 'icons/mob/simple/pets.dmi', icon_state = developed_path ? "dead_tree" : "growing_tree")
dead_overlay.pixel_y = -2
. += dead_overlay
return
var/pixel_offset = resting ? -2 : 2
var/mutable_appearance/living_tree = grown_tree ? grown_tree : mutable_appearance(icon = icon, icon_state = "growing_tree")
living_tree.pixel_y = pixel_offset
. += living_tree
/mob/living/basic/turtle/update_resting()
. = ..()
if(stat == DEAD)
return
update_appearance()
/mob/living/basic/turtle/item_interaction(mob/living/user, obj/item/used_item, list/modifiers)
if(!istype(used_item, /obj/item/reagent_containers))
return NONE
if(isnull(used_item.reagents))
balloon_alert(user, "empty!")
return ITEM_INTERACT_SUCCESS
if(stat == DEAD)
balloon_alert(user, "its dead!")
return ITEM_INTERACT_SUCCESS
var/should_transfer = FALSE
for(var/reagent in path_requirements)
if(used_item.reagents.has_reagent(reagent))
should_transfer = TRUE
break
if(!should_transfer)
balloon_alert(user, "refuses to drink!")
return ITEM_INTERACT_SUCCESS
if(!do_after(user, 1.5 SECONDS, target = src))
return ITEM_INTERACT_SUCCESS
used_item.reagents.trans_to(reagents, 5)
balloon_alert(user, "drinks happily")
playsound(src, 'sound/items/drink.ogg', vol = 25, vary = TRUE)
return ITEM_INTERACT_SUCCESS
/mob/living/basic/turtle/proc/pre_eat_food(datum/source, obj/item/seeds/potential_food)
SIGNAL_HANDLER
if(!istype(potential_food))
return NONE
if(ispath(potential_food.product, /obj/item/food/grown))
addtimer(CALLBACK(src, PROC_REF(process_food), potential_food.product), 30 SECONDS)
return NONE
/mob/living/basic/turtle/proc/process_food(food_path)
if(QDELETED(src) || stat != CONSCIOUS)
return
new food_path(drop_location())
balloon_alert_to_viewers("spits out some food")
/mob/living/basic/turtle/death(gibbed)
. = ..()
STOP_PROCESSING(SSprocessing, src)
#undef PATH_PEST_KILLER
#undef PATH_PLANT_HEALER
#undef PATH_PLANT_MUTATOR
#undef REQUIRED_TREE_GROWTH
#undef UPPER_BOUND_VOLUME
#undef LOWER_BOUND_VOLUME
@@ -0,0 +1,140 @@
#define TREE_FIELD_DURATION_EFFECT 10 SECONDS
#define WARP_ANIMATE_TIME 0.35 SECONDS
/datum/action/cooldown/mob_cooldown/turtle_tree
name = "Tree Ability"
desc = "Invoke your tree's special ability."
cooldown_time = 2 MINUTES
click_to_activate = FALSE
button_icon = 'icons/mob/simple/pets.dmi'
button_icon_state = "turtle"
///type of effect our tree releases
var/effect_path
///how many times our ability affects surroundings
var/maximum_intervals = 5
///time between each interval
var/time_between_intervals = 3 SECONDS
///range our tree affects
var/tree_range = 5
///warp effect to apply some distortion to our field
var/atom/movable/warp_effect/turtle_field/warp
/datum/action/cooldown/mob_cooldown/turtle_tree/Activate(atom/target)
. = ..()
warp = new(owner)
RegisterSignal(warp, COMSIG_QDELETING, PROC_REF(remove_warp))
warp.alpha = 0
owner.vis_contents += warp
for(var/index in 0 to maximum_intervals)
addtimer(CALLBACK(src, PROC_REF(tree_effect)), time_between_intervals * index)
animate(warp, transform = matrix(), alpha = warp::alpha, time = WARP_ANIMATE_TIME)
addtimer(CALLBACK(src, PROC_REF(warp_extinguish)), (time_between_intervals * maximum_intervals) + 3 SECONDS)
/datum/action/cooldown/mob_cooldown/turtle_tree/proc/warp_extinguish()
if(QDELETED(warp))
return
animate(warp, alpha = 0, time = WARP_ANIMATE_TIME)
addtimer(CALLBACK(src, PROC_REF(remove_warp)), WARP_ANIMATE_TIME)
/datum/action/cooldown/mob_cooldown/turtle_tree/proc/remove_warp()
SIGNAL_HANDLER
UnregisterSignal(warp, COMSIG_QDELETING)
warp = null
///effect we apply on our trees
/datum/action/cooldown/mob_cooldown/turtle_tree/proc/tree_effect()
SHOULD_CALL_PARENT(TRUE)
return (pre_effect_apply())
///things we should check for before applying our effects
/datum/action/cooldown/mob_cooldown/turtle_tree/proc/pre_effect_apply()
if(QDELETED(owner) || owner.stat == DEAD)
return FALSE
var/obj/effect/tree_effect = new effect_path
owner.vis_contents += tree_effect
return TRUE
///healer tree, heals nearby plants by small amounts
/datum/action/cooldown/mob_cooldown/turtle_tree/healer
effect_path = /obj/effect/temp_visual/circle_wave/tree/healer
///amount we heal plants by
var/heal_amount = 5
/datum/action/cooldown/mob_cooldown/turtle_tree/healer/tree_effect()
. = ..()
if(!.)
return
for(var/obj/machinery/hydroponics/hydro in oview(tree_range, owner))
if(isnull(hydro.myseed))
continue
hydro.adjust_plant_health(heal_amount)
///killer tree, kills plant's pests and weeds, aswell as nearby vermin
/datum/action/cooldown/mob_cooldown/turtle_tree/killer
effect_path = /obj/effect/temp_visual/circle_wave/tree/killer
///amount we heal plants by
var/vermin_damage_amount = 20
///type of vermin our field affects
var/static/list/vermin_mob_targets = typecacheof(list(
/mob/living/basic/cockroach,
/mob/living/basic/mouse/rat,
))
///how much we reduce weed levels
var/weed_level_reduce = 2
/datum/action/cooldown/mob_cooldown/turtle_tree/killer/tree_effect()
. = ..()
if(!.)
return
for(var/atom/possible_target as anything in oview(tree_range, owner))
if(is_type_in_typecache(possible_target, vermin_mob_targets))
var/mob/living/living_target = possible_target
living_target.apply_damage(vermin_damage_amount)
continue
if(!istype(possible_target, /obj/machinery/hydroponics))
continue
var/obj/machinery/hydroponics/hydro = possible_target
if(isnull(hydro.myseed))
continue
hydro.set_weedlevel(hydro.weedlevel - weed_level_reduce)
///mutator tree, mutates nearby plants!
/datum/action/cooldown/mob_cooldown/turtle_tree/mutator
effect_path = /obj/effect/temp_visual/circle_wave/tree/mutator
///how much we mutate plants
var/mutator_boost = 1
/datum/action/cooldown/mob_cooldown/turtle_tree/mutator/tree_effect()
. = ..()
if(!.)
return
for(var/obj/machinery/hydroponics/hydro in oview(tree_range, owner))
hydro.myseed?.adjust_instability(mutator_boost)
/atom/movable/warp_effect/turtle_field
alpha = 75
///effects we give our tree abilities depending on their type
/obj/effect/temp_visual/circle_wave/tree
vis_flags = VIS_INHERIT_DIR | VIS_INHERIT_PLANE
duration = 10 SECONDS
amount_to_scale = 3
/obj/effect/temp_visual/circle_wave/tree/healer
color = "#28a3bc"
/obj/effect/temp_visual/circle_wave/tree/killer
color = "#ce3ebf"
/obj/effect/temp_visual/circle_wave/tree/mutator
color = "#c49f26"
#undef TREE_FIELD_DURATION_EFFECT
#undef WARP_ANIMATE_TIME
@@ -0,0 +1,92 @@
/datum/ai_controller/basic_controller/turtle
blackboard = list(
BB_HAPPY_EMOTIONS = list(
"wiggles its tree in excitement!",
"raises its head up high!",
"wags its tail enthusiastically!",
),
BB_MODERATE_EMOTIONS = list(
"keeps its head level, eyes half-closed.",
"basks in the light peacefully.",
),
BB_SAD_EMOTIONS = list(
"looks towards the floor in dissapointment...",
"the leaves on its tree droop...",
),
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk/less_walking
planning_subtrees = list(
/datum/ai_planning_subtree/find_food,
/datum/ai_planning_subtree/express_happiness,
/datum/ai_planning_subtree/use_mob_ability/turtle_tree,
/datum/ai_planning_subtree/find_and_hunt_target/headbutt_people, //playfully headbutt people's legs
/datum/ai_planning_subtree/find_and_hunt_target/sniff_flora, //mmm the aroma
)
/datum/ai_planning_subtree/use_mob_ability/turtle_tree
ability_key = BB_TURTLE_TREE_ABILITY
/datum/ai_planning_subtree/use_mob_ability/turtle_tree/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/happiness_count = controller.blackboard[BB_BASIC_HAPPINESS] * 100
if(happiness_count > 75)
return ..()
if(!SPT_PROB(happiness_count / 50, seconds_per_tick))
return
return ..()
/datum/ai_planning_subtree/find_and_hunt_target/sniff_flora
target_key = BB_TURTLE_FLORA_TARGET
finding_behavior = /datum/ai_behavior/find_hunt_target/sniff_flora
hunting_behavior = /datum/ai_behavior/hunt_target/sniff_flora
hunt_targets = list(
/obj/machinery/hydroponics,
/obj/item/kirbyplants,
)
hunt_range = 5
hunt_chance = 45
/datum/ai_behavior/find_hunt_target/sniff_flora
action_cooldown = 1 MINUTES
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/find_hunt_target/sniff_flora/valid_dinner(mob/living/source, obj/machinery/hydroponics/dinner, radius, datum/ai_controller/controller, seconds_per_tick)
if(!istype(dinner))
return TRUE
if(isnull(dinner.myseed))
return FALSE
if(dinner.weedlevel > 5 || dinner.pestlevel > 5) //too smelly
return FALSE
return can_see(source, dinner, radius)
/datum/ai_behavior/hunt_target/sniff_flora
always_reset_target = TRUE
/datum/ai_behavior/hunt_target/sniff_flora/target_caught(mob/living/hunter, atom/hunted)
hunter.manual_emote("Enjoys the sweet scent eminating from [hunted::name]!")
/datum/ai_planning_subtree/find_and_hunt_target/headbutt_people
target_key = BB_TURTLE_HEADBUTT_VICTIM
finding_behavior = /datum/ai_behavior/find_hunt_target/human_to_headbutt
hunting_behavior = /datum/ai_behavior/hunt_target/headbutt_leg
hunt_targets = list(/mob/living/carbon/human)
hunt_range = 4
hunt_chance = 45
/datum/ai_behavior/find_hunt_target/human_to_headbutt
action_cooldown = 2 MINUTES
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/find_hunt_target/human_to_headbutt/valid_dinner(mob/living/source, mob/living/carbon/human/dinner, radius, datum/ai_controller/controller, seconds_per_tick)
if(dinner.stat != CONSCIOUS)
return FALSE
if(isnull(dinner.get_bodypart(BODY_ZONE_R_LEG)) && isnull(dinner.get_bodypart(BODY_ZONE_L_LEG))) //no legs to headbutt!
return FALSE
return can_see(source, dinner, radius)
/datum/ai_behavior/hunt_target/headbutt_leg
always_reset_target = TRUE
/datum/ai_behavior/hunt_target/headbutt_leg/target_caught(mob/living/hunter, atom/hunted)
hunter.manual_emote("playfully headbutts [hunted]'s legs!")