seedling basic mobs (#78224)

## About The Pull Request
this is a mob that was removed from the game since years ago. this pr
refactors them and readds them to the game. seedlings now can have
different colored petals!

![petalcolors](https://github.com/tgstation/tgstation/assets/138636438/7c657c49-9186-4563-a93f-eba35e335ce9)


to obtain this monster, botany can now grow them out of 'seedling' seeds
that they can find in the exotic seeds crate. this mob loves to help all
plants and when its grown out of the seed, it will help the botanists
look after their plants. it can water plants (if it finds a bucket),
remove weeds and remove dead plants. they will also use their solarbeam
ability to provide small healing to damaged plants. it will try to
refill its bucket with water if there is a water source near by. it will
also follow the orders of whoever harvested it.


![plantbotany](https://github.com/tgstation/tgstation/assets/138636438/4cde0613-0869-4fea-8851-2e6f4680d9d7)

traitors can buy a seed from the uplink that grows an evil version of
the seedlings, they can command them to attack people and use abilities
and to cause chaos on the station.


## Why It's Good For The Game
refacotrs the seedlings into basic mobs

## Changelog
🆑
refactor: seedlings have been refactored into basic mobs please report
any bugs
add: seedlings now can have different colored petals and can look after
botanys plants
add: seedlings are re-added to the game! they grow out of seedling seeds
obtainable from exotic seed crates or traitor uplink
/🆑
This commit is contained in:
Ben10Omintrix
2023-09-22 15:28:48 +01:00
committed by GitHub
parent a2ff18730d
commit 0976420f2f
16 changed files with 654 additions and 256 deletions
@@ -0,0 +1,348 @@
#define SEEDLING_STATE_NEUTRAL 0
#define SEEDLING_STATE_WARMUP 1
#define SEEDLING_STATE_ACTIVE 2
/**
* A mobile plant with a rapid ranged attack.
* It can pick up watering cans and look after plants.
*/
/mob/living/basic/seedling
name = "seedling"
desc = "This oversized, predatory flower conceals what can only be described as an organic energy cannon."
icon = 'icons/mob/simple/jungle/seedling.dmi'
icon_state = "seedling"
icon_living = "seedling"
icon_dead = "seedling_dead"
habitable_atmos = list("min_oxy" = 2, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
minimum_survivable_temperature = 0
maximum_survivable_temperature = 450
mob_biotypes = MOB_ORGANIC | MOB_PLANT
maxHealth = 100
health = 100
pixel_y = -14
base_pixel_y = -14
pixel_x = -14
base_pixel_x = -14
response_harm_continuous = "strikes"
response_harm_simple = "strike"
melee_damage_lower = 30
melee_damage_upper = 30
lighting_cutoff_green = 20
lighting_cutoff_blue = 25
mob_size = MOB_SIZE_LARGE
attack_sound = 'sound/weapons/bladeslice.ogg'
attack_vis_effect = ATTACK_EFFECT_SLASH
ai_controller = /datum/ai_controller/basic_controller/seedling
///the state of combat we are in
var/combatant_state = SEEDLING_STATE_NEUTRAL
///the colors our petals can have
var/static/list/possible_colors = list(COLOR_RED, COLOR_YELLOW, COLOR_OLIVE, COLOR_CYAN)
///appearance when we are in our normal state
var/mutable_appearance/petal_neutral
///appearance when we are in our warmup state
var/mutable_appearance/petal_warmup
///appearance when we are in the firing state
var/mutable_appearance/petal_active
///appearance when we are dead
var/mutable_appearance/petal_dead
///the bucket we carry
var/obj/item/reagent_containers/cup/held_can
///commands we follow
var/list/seedling_commands = list(
/datum/pet_command/idle,
/datum/pet_command/free,
/datum/pet_command/follow,
)
/mob/living/basic/seedling/Initialize(mapload)
. = ..()
var/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/seedling/seed_attack = new(src)
seed_attack.Grant(src)
ai_controller.set_blackboard_key(BB_RAPIDSEEDS_ABILITY, seed_attack)
var/datum/action/cooldown/mob_cooldown/solarbeam/beam_attack = new(src)
beam_attack.Grant(src)
ai_controller.set_blackboard_key(BB_SOLARBEAM_ABILITY, beam_attack)
var/petal_color = pick(possible_colors)
petal_neutral = mutable_appearance(icon, "[icon_state]_overlay")
petal_neutral.color = petal_color
petal_warmup = mutable_appearance(icon, "[icon_state]_charging_overlay")
petal_warmup.color = petal_color
petal_active = mutable_appearance(icon, "[icon_state]_fire_overlay")
petal_active.color = petal_color
petal_dead = mutable_appearance(icon, "[icon_state]_dead_overlay")
petal_dead.color = petal_color
AddElement(/datum/element/wall_smasher)
AddComponent(/datum/component/obeys_commands, seedling_commands)
RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(pre_attack))
RegisterSignal(src, COMSIG_KB_MOB_DROPITEM_DOWN, PROC_REF(drop_can))
update_appearance()
/mob/living/basic/seedling/proc/pre_attack(mob/living/puncher, atom/target)
SIGNAL_HANDLER
if(istype(target, /obj/machinery/hydroponics))
treat_hydro_tray(target)
return COMPONENT_HOSTILE_NO_ATTACK
if(isnull(held_can))
return
if(istype(target, /obj/structure/sink) || istype(target, /obj/structure/reagent_dispensers))
INVOKE_ASYNC(held_can, TYPE_PROC_REF(/obj/item, melee_attack_chain), src, target)
return COMPONENT_HOSTILE_NO_ATTACK
///seedlings can water trays, remove weeds, or remove dead plants
/mob/living/basic/seedling/proc/treat_hydro_tray(obj/machinery/hydroponics/hydro)
if(hydro.plant_status == HYDROTRAY_PLANT_DEAD)
balloon_alert(src, "dead plant removed")
hydro.set_seed(null)
return
if(hydro.weedlevel > 0)
balloon_alert(src, "weeds uprooted")
hydro.set_weedlevel(0)
return
var/list/can_reagents = held_can?.reagents.reagent_list
if(!length(can_reagents))
return
if((locate(/datum/reagent/water) in can_reagents) && (hydro.waterlevel < hydro.maxwater))
INVOKE_ASYNC(held_can, TYPE_PROC_REF(/obj/item, melee_attack_chain), src, hydro)
return
/mob/living/basic/seedling/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers)
. = ..()
if(!. || !proximity_flag || held_can)
return
if(!istype(attack_target, /obj/item/reagent_containers/cup/watering_can))
return
var/obj/item/can_target = attack_target
can_target.forceMove(src)
/mob/living/basic/seedling/proc/change_combatant_state(state)
combatant_state = state
update_appearance()
/mob/living/basic/seedling/attackby(obj/item/can, mob/living/carbon/human/user, list/modifiers)
if(istype(can, /obj/item/reagent_containers/cup/watering_can) && isnull(held_can))
can.forceMove(src)
return
return ..()
/mob/living/basic/seedling/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs)
if(istype(arrived, /obj/item/reagent_containers/cup/watering_can))
held_can = arrived
update_appearance()
return ..()
/mob/living/basic/seedling/update_overlays()
. = ..()
if(stat == DEAD)
. += petal_dead
return
switch(combatant_state)
if(SEEDLING_STATE_NEUTRAL)
. += petal_neutral
if(held_can)
. += mutable_appearance(icon, "seedling_can_overlay")
if(SEEDLING_STATE_WARMUP)
. += petal_warmup
if(SEEDLING_STATE_ACTIVE)
. += petal_active
/mob/living/basic/seedling/update_icon_state()
. = ..()
if(stat == DEAD)
return
switch(combatant_state)
if(SEEDLING_STATE_NEUTRAL)
icon_state = "seedling"
if(SEEDLING_STATE_WARMUP)
icon_state = "seedling_charging"
if(SEEDLING_STATE_ACTIVE)
icon_state = "seedling_fire"
/mob/living/basic/seedling/proc/drop_can(mob/living/user)
SIGNAL_HANDLER
if(isnull(held_can))
return
dropItemToGround(held_can)
return COMSIG_KB_ACTIVATED
/mob/living/basic/seedling/Exited(atom/movable/gone, direction)
. = ..()
if(gone != held_can)
return
held_can = null
update_appearance()
/mob/living/basic/seedling/death(gibbed)
. = ..()
if(isnull(held_can))
return
held_can.forceMove(drop_location())
/mob/living/basic/seedling/Destroy()
QDEL_NULL(held_can)
return ..()
/mob/living/basic/seedling/meanie
maxHealth = 400
health = 400
faction = list(FACTION_JUNGLE)
ai_controller = /datum/ai_controller/basic_controller/seedling/meanie
seedling_commands = list(
/datum/pet_command/idle,
/datum/pet_command/free,
/datum/pet_command/follow,
/datum/pet_command/point_targetting/attack,
/datum/pet_command/point_targetting/use_ability/solarbeam,
/datum/pet_command/point_targetting/use_ability/rapidseeds,
)
//abilities
/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/seedling
name = "Solar Energy"
button_icon = 'icons/obj/weapons/guns/projectiles.dmi'
button_icon_state = "seedling"
desc = "Fire small beams of solar energy."
cooldown_time = 10 SECONDS
projectile_type = /obj/projectile/seedling
default_projectile_spread = 10
shot_count = 10
shot_delay = 0.2 SECONDS
melee_cooldown_time = 0 SECONDS
shared_cooldown = NONE
///how long we must charge up before firing off
var/charge_up_timer = 3 SECONDS
///is the owner of this ability a seedling?
var/is_seedling = FALSE
/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/seedling/Grant(mob/grant_to)
. = ..()
if(isnull(owner))
return
is_seedling = istype(owner, /mob/living/basic/seedling)
/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/seedling/IsAvailable(feedback)
. = ..()
if(!.)
return FALSE
if(!is_seedling)
return TRUE
var/mob/living/basic/seedling/seed_owner = owner
if(seed_owner.combatant_state != SEEDLING_STATE_NEUTRAL)
if(feedback)
seed_owner.balloon_alert(seed_owner, "charging!")
return FALSE
return TRUE
/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/seedling/Activate(atom/target)
if(is_seedling)
var/mob/living/basic/seedling/seed_owner = owner
seed_owner.change_combatant_state(state = SEEDLING_STATE_WARMUP)
addtimer(CALLBACK(src, PROC_REF(attack_sequence), owner, target), charge_up_timer)
StartCooldown()
return TRUE
/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/seedling/attack_sequence(mob/living/firer, atom/target)
if(is_seedling)
var/mob/living/basic/seedling/seed_owner = owner
seed_owner.change_combatant_state(state = SEEDLING_STATE_ACTIVE)
addtimer(CALLBACK(seed_owner, TYPE_PROC_REF(/mob/living/basic/seedling, change_combatant_state), SEEDLING_STATE_NEUTRAL), 4 SECONDS)
return ..()
/datum/action/cooldown/mob_cooldown/solarbeam
name = "Solar Beam"
button_icon = 'icons/effects/beam.dmi'
button_icon_state = "solar_beam"
desc = "Concentrate the power of the sun onto your target!"
cooldown_time = 30 SECONDS
shared_cooldown = NONE
///how long will it take for us to charge up the beam
var/beam_charge_up = 3 SECONDS
///is the owner of this ability a seedling?
var/is_seedling = FALSE
/datum/action/cooldown/mob_cooldown/solarbeam/Grant(mob/grant_to)
. = ..()
if(isnull(owner))
return
is_seedling = istype(owner, /mob/living/basic/seedling)
/datum/action/cooldown/mob_cooldown/solarbeam/IsAvailable(feedback)
. = ..()
if(!.)
return FALSE
if(!is_seedling)
return TRUE
var/mob/living/basic/seedling/seed_owner = owner
if(seed_owner.combatant_state != SEEDLING_STATE_NEUTRAL)
if(feedback)
seed_owner.balloon_alert(seed_owner, "charging!")
return FALSE
return TRUE
/datum/action/cooldown/mob_cooldown/solarbeam/Activate(atom/target)
if(is_seedling)
var/mob/living/basic/seedling/seed_owner = owner
seed_owner.change_combatant_state(state = SEEDLING_STATE_WARMUP)
var/turf/target_turf = get_turf(target)
playsound(owner, 'sound/effects/seedling_chargeup.ogg', 100, FALSE)
var/obj/effect/temp_visual/solarbeam_killsat/owner_beam = new(get_turf(owner))
animate(owner_beam, transform = matrix().Scale(1, 32), alpha = 255, time = beam_charge_up)
var/obj/effect/temp_visual/solarbeam_killsat/target_beam = new(target_turf)
animate(target_beam, transform = matrix().Scale(2, 1), alpha = 255, time = beam_charge_up)
addtimer(CALLBACK(src, PROC_REF(launch_beam), owner, target_turf), beam_charge_up)
StartCooldown()
return TRUE
///the solarbeam will damage people, otherwise it will heal plants
/datum/action/cooldown/mob_cooldown/solarbeam/proc/launch_beam(mob/living/firer, turf/target_turf)
for(var/atom/target_atom as anything in target_turf)
if(istype(target_atom, /obj/machinery/hydroponics))
var/obj/machinery/hydroponics/hydro = target_atom
hydro.adjust_plant_health(10)
new /obj/effect/temp_visual/heal(target_turf, COLOR_HEALING_CYAN)
if(!isliving(target_atom))
continue
var/mob/living/living_target = target_atom
living_target.adjust_fire_stacks(0.2)
living_target.ignite_mob()
living_target.adjustFireLoss(30)
playsound(target_turf, 'sound/magic/lightningbolt.ogg', 50, TRUE)
if(!is_seedling)
return
var/mob/living/basic/seedling/seed_firer = firer
seed_firer.change_combatant_state(state = SEEDLING_STATE_NEUTRAL)
#undef SEEDLING_STATE_NEUTRAL
#undef SEEDLING_STATE_WARMUP
#undef SEEDLING_STATE_ACTIVE
@@ -0,0 +1,178 @@
/datum/ai_controller/basic_controller/seedling
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends,
BB_WEEDLEVEL_THRESHOLD = 3,
BB_WATERLEVEL_THRESHOLD = 90,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/pet_planning,
/datum/ai_planning_subtree/find_and_hunt_target/watering_can,
/datum/ai_planning_subtree/find_and_hunt_target/fill_watercan,
/datum/ai_planning_subtree/find_and_hunt_target/treat_hydroplants,
/datum/ai_planning_subtree/find_and_hunt_target/beamable_hydroplants,
)
/datum/ai_planning_subtree/find_and_hunt_target/watering_can
target_key = BB_WATERCAN_TARGET
finding_behavior = /datum/ai_behavior/find_hunt_target
hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target
hunt_targets = list(/obj/item/reagent_containers/cup/watering_can)
hunt_range = 7
/datum/ai_planning_subtree/find_and_hunt_target/watering_can/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/living_pawn = controller.pawn
if(locate(/obj/item/reagent_containers/cup/watering_can) in living_pawn) //we already have what we came for!
return
return ..()
/datum/ai_planning_subtree/find_and_hunt_target/treat_hydroplants
target_key = BB_HYDROPLANT_TARGET
finding_behavior = /datum/ai_behavior/find_and_set/treatable_hydro
hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target/treat_hydroplant
hunt_targets = list(/obj/machinery/hydroponics)
hunt_range = 7
/datum/ai_behavior/find_and_set/treatable_hydro
/datum/ai_behavior/find_and_set/treatable_hydro/search_tactic(datum/ai_controller/controller, locate_path, search_range)
var/list/possible_trays = list()
var/mob/living/living_pawn = controller.pawn
var/waterlevel_threshold = controller.blackboard[BB_WATERLEVEL_THRESHOLD]
var/weedlevel_threshold = controller.blackboard[BB_WEEDLEVEL_THRESHOLD]
var/watering_can = locate(/obj/item/reagent_containers/cup/watering_can) in living_pawn
for(var/obj/machinery/hydroponics/hydro in oview(search_range, controller.pawn))
if(isnull(hydro.myseed))
continue
if(hydro.waterlevel < waterlevel_threshold && watering_can)
possible_trays += hydro
continue
if(hydro.weedlevel > weedlevel_threshold || hydro.plant_status == HYDROTRAY_PLANT_DEAD)
possible_trays += hydro
continue
if(possible_trays.len)
return pick(possible_trays)
/datum/ai_behavior/hunt_target/unarmed_attack_target/treat_hydroplant
hunt_cooldown = 2 SECONDS
always_reset_target = TRUE
/datum/ai_behavior/hunt_target/unarmed_attack_target/treat_hydroplant/target_caught(mob/living/living_pawn, obj/machinery/hydroponics/hydro_target)
if(QDELETED(hydro_target) || QDELETED(hydro_target.myseed))
return
if(hydro_target.plant_status == HYDROTRAY_PLANT_DEAD)
living_pawn.manual_emote("weeps...") //weep over the dead plants
return ..()
/datum/ai_planning_subtree/find_and_hunt_target/beamable_hydroplants
target_key = BB_BEAMABLE_HYDROPLANT_TARGET
finding_behavior = /datum/ai_behavior/find_and_set/beamable_hydroplants
hunting_behavior = /datum/ai_behavior/hunt_target/use_ability_on_target/solarbeam
hunt_targets = list(/obj/machinery/hydroponics)
hunt_range = 7
/datum/ai_planning_subtree/find_and_hunt_target/beamable_hydroplants/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/datum/action/cooldown/solar_ability = controller.blackboard[BB_SOLARBEAM_ABILITY]
if(QDELETED(solar_ability) || !solar_ability.IsAvailable())
return
return ..()
/datum/ai_behavior/hunt_target/use_ability_on_target/solarbeam
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
required_distance = 2
action_cooldown = 1 MINUTES
ability_key = BB_SOLARBEAM_ABILITY
/datum/ai_behavior/hunt_target/use_ability_on_target/solarbeam/setup(datum/ai_controller/controller, target_key, ability_key)
. = ..()
var/obj/target = controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
set_movement_target(controller, target)
/datum/ai_behavior/find_and_set/beamable_hydroplants/search_tactic(datum/ai_controller/controller, locate_path, search_range)
var/list/possible_trays = list()
for(var/obj/machinery/hydroponics/hydro in oview(search_range, controller.pawn))
if(isnull(hydro.myseed))
continue
if(hydro.plant_health < hydro.myseed.endurance)
possible_trays += hydro
if(possible_trays.len)
return pick(possible_trays)
/datum/ai_planning_subtree/find_and_hunt_target/fill_watercan
target_key = BB_LOW_PRIORITY_HUNTING_TARGET
finding_behavior = /datum/ai_behavior/find_hunt_target/suitable_dispenser
hunting_behavior = /datum/ai_behavior/hunt_target/unarmed_attack_target/water_source
hunt_targets = list(/obj/structure/sink, /obj/structure/reagent_dispensers)
hunt_range = 7
/datum/ai_planning_subtree/find_and_hunt_target/fill_watercan/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/living_pawn = controller.pawn
var/obj/item/reagent_containers/can = locate(/obj/item/reagent_containers/cup/watering_can) in living_pawn
if(isnull(can))
return
if(locate(/datum/reagent/water) in can.reagents.reagent_list)
return
return ..()
/datum/ai_behavior/find_hunt_target/suitable_dispenser
/datum/ai_behavior/find_hunt_target/suitable_dispenser/valid_dinner(mob/living/source, obj/structure/water_source, radius)
if(!(locate(/datum/reagent/water) in water_source.reagents.reagent_list))
return FALSE
return can_see(source, water_source, radius)
/datum/ai_behavior/hunt_target/unarmed_attack_target/water_source
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
hunt_cooldown = 5 SECONDS
/datum/ai_controller/basic_controller/seedling/meanie
blackboard = list(
BB_TARGETTING_DATUM = new /datum/targetting_datum/basic,
BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends,
)
planning_subtrees = list(
/datum/ai_planning_subtree/pet_planning,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/targeted_mob_ability/seedling_rapid,
/datum/ai_planning_subtree/targeted_mob_ability/solarbeam,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
/datum/ai_planning_subtree/targeted_mob_ability/seedling_rapid
ability_key = BB_RAPIDSEEDS_ABILITY
finish_planning = FALSE
/datum/ai_planning_subtree/targeted_mob_ability/solarbeam
ability_key = BB_SOLARBEAM_ABILITY
finish_planning = FALSE
///pet commands
/datum/pet_command/point_targetting/use_ability/solarbeam
command_name = "Launch solarbeam"
command_desc = "Command your pet to launch a solarbeam at your target!"
radial_icon = 'icons/effects/beam.dmi'
radial_icon_state = "solar_beam"
speech_commands = list("beam", "solar")
pet_ability_key = BB_SOLARBEAM_ABILITY
/datum/pet_command/point_targetting/use_ability/rapidseeds
command_name = "Rapid seeds"
command_desc = "Command your pet to launch a volley of seeds at your target!"
radial_icon = 'icons/obj/weapons/guns/projectiles.dmi'
radial_icon_state = "seedling"
speech_commands = list("rapid", "seeds", "volley")
pet_ability_key = BB_RAPIDSEEDS_ABILITY
@@ -0,0 +1,32 @@
/obj/projectile/seedling
name = "solar energy"
icon_state = "seedling"
damage = 10
damage_type = BURN
light_range = 2
armor_flag = ENERGY
light_color = LIGHT_COLOR_DIM_YELLOW
speed = 1.6
hitsound = 'sound/weapons/sear.ogg'
hitsound_wall = 'sound/weapons/effects/searwall.ogg'
nondirectional_sprite = TRUE
/obj/projectile/seedling/on_hit(atom/target)
if(!isliving(target))
return ..()
var/mob/living/living_target = target
if(FACTION_JUNGLE in living_target.faction)
return
return ..()
/obj/effect/temp_visual/solarbeam_killsat
name = "beam of solar energy"
icon_state = "solar_beam"
icon = 'icons/effects/beam.dmi'
plane = LIGHTING_PLANE
layer = LIGHTING_PRIMARY_LAYER
duration = 3 SECONDS
alpha = 200
randomdir = FALSE