diff --git a/code/__DEFINES/ai/monsters.dm b/code/__DEFINES/ai/monsters.dm index ebb924e42e1..927f3736832 100644 --- a/code/__DEFINES/ai/monsters.dm +++ b/code/__DEFINES/ai/monsters.dm @@ -130,3 +130,19 @@ #define BB_AUTOMATED_MINING "automated_mining" /// key that stores the nearest dead human #define BB_NEARBY_DEAD_MINER "nearby_dead_miner" + +//seedling keys +/// the water can we will pick up +#define BB_WATERCAN_TARGET "watercan_target" +/// the hydrotray we will heal +#define BB_HYDROPLANT_TARGET "hydroplant_target" +/// minimum weed levels for us to cure +#define BB_WEEDLEVEL_THRESHOLD "weedlevel_threshold" +/// minimum water levels for us to refill +#define BB_WATERLEVEL_THRESHOLD "waterlevel_threshold" +/// key holds our solarbeam ability +#define BB_SOLARBEAM_ABILITY "solarbeam_ability" +/// key holds our rapid seeds ability +#define BB_RAPIDSEEDS_ABILITY "rapidseeds_ability" +/// key holds the tray we will beam +#define BB_BEAMABLE_HYDROPLANT_TARGET "beamable_hydroplant_target" diff --git a/code/datums/ai/hunting_behavior/hunting_behaviors.dm b/code/datums/ai/hunting_behavior/hunting_behaviors.dm index 777cb851ee4..036176dc85e 100644 --- a/code/datums/ai/hunting_behavior/hunting_behaviors.dm +++ b/code/datums/ai/hunting_behavior/hunting_behaviors.dm @@ -16,6 +16,8 @@ var/hunt_range = 2 /// What are the chances we hunt something at any given moment var/hunt_chance = 100 + ///do we finish planning subtree + var/finish_planning = TRUE /datum/ai_planning_subtree/find_and_hunt_target/New() . = ..() @@ -35,13 +37,15 @@ // We're not hunting anything, look around for something if(isnull(hunted)) controller.queue_behavior(finding_behavior, target_key, hunt_targets, hunt_range) + return // We ARE hunting something, execute the hunt. // Note that if our AI controller has multiple hunting subtrees set, // we may accidentally be executing another tree's hunt - not ideal, // try to set a unique target key if you have multiple - else - controller.queue_behavior(hunting_behavior, target_key, BB_HUNTING_COOLDOWN) + + controller.queue_behavior(hunting_behavior, target_key, BB_HUNTING_COOLDOWN) + if(finish_planning) return SUBTREE_RETURN_FINISH_PLANNING //If we're hunting we're too busy for anything else /// Finds a specific atom type to hunt. @@ -124,3 +128,19 @@ /datum/ai_behavior/hunt_target/unarmed_attack_target/target_caught(mob/living/hunter, obj/structure/cable/hunted) hunter.UnarmedAttack(hunted, TRUE) + +/datum/ai_behavior/hunt_target/use_ability_on_target + always_reset_target = TRUE + ///the ability we will use + var/ability_key + +/datum/ai_behavior/hunt_target/use_ability_on_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key) + var/datum/action/cooldown/ability = controller.blackboard[ability_key] + if(QDELETED(ability) || !ability.IsAvailable()) + finish_action(controller, FALSE, hunting_target_key) + return ..() + +/datum/ai_behavior/hunt_target/use_ability_on_target/target_caught(mob/living/hunter, atom/hunted) + var/datum/action/cooldown/ability = hunter.ai_controller.blackboard[ability_key] + ability.InterceptClickOn(hunter, null, hunted) + diff --git a/code/game/objects/effects/spawners/random/food_or_drink.dm b/code/game/objects/effects/spawners/random/food_or_drink.dm index 5338eb52897..db4b6a54c13 100644 --- a/code/game/objects/effects/spawners/random/food_or_drink.dm +++ b/code/game/objects/effects/spawners/random/food_or_drink.dm @@ -56,6 +56,7 @@ /obj/item/seeds/liberty = 5, /obj/item/seeds/replicapod = 5, /obj/item/seeds/reishi = 5, + /obj/item/seeds/seedling = 5, /obj/item/seeds/nettle/death = 1, /obj/item/seeds/plump/walkingmushroom = 1, /obj/item/seeds/cannabis/rainbow = 1, diff --git a/code/modules/cargo/packs/organic.dm b/code/modules/cargo/packs/organic.dm index c02a4d56db9..73c98cb4b3a 100644 --- a/code/modules/cargo/packs/organic.dm +++ b/code/modules/cargo/packs/organic.dm @@ -62,18 +62,20 @@ including one replica-pod seed and two mystery seeds!" cost = CARGO_CRATE_VALUE * 3 access_view = ACCESS_HYDROPONICS - contains = list(/obj/item/seeds/nettle, - /obj/item/seeds/replicapod, - /obj/item/seeds/plump, - /obj/item/seeds/liberty, - /obj/item/seeds/amanita, - /obj/item/seeds/reishi, - /obj/item/seeds/bamboo, - /obj/item/seeds/eggplant/eggy, - /obj/item/seeds/rainbow_bunch, - /obj/item/seeds/shrub, - /obj/item/seeds/random = 2, - ) + contains = list( + /obj/item/seeds/amanita, + /obj/item/seeds/bamboo, + /obj/item/seeds/eggplant/eggy, + /obj/item/seeds/liberty, + /obj/item/seeds/nettle, + /obj/item/seeds/plump, + /obj/item/seeds/replicapod, + /obj/item/seeds/reishi, + /obj/item/seeds/rainbow_bunch, + /obj/item/seeds/seedling, + /obj/item/seeds/shrub, + /obj/item/seeds/random = 2, + ) crate_name = "exotic seeds crate" crate_type = /obj/structure/closet/crate/hydroponics diff --git a/code/modules/hydroponics/grown/seedling.dm b/code/modules/hydroponics/grown/seedling.dm new file mode 100644 index 00000000000..57fd11280b6 --- /dev/null +++ b/code/modules/hydroponics/grown/seedling.dm @@ -0,0 +1,28 @@ +/obj/item/seeds/seedling + name = "pack of seedling seeds" + desc = "These seeds grow into a floral assistant which can help look after other plants!" + icon_state = "seed-seedling" + growing_icon = 'icons/obj/service/hydroponics/growing_fruits.dmi' + species = "seedling" + plantname = "Seedling Plant" + product = /mob/living/basic/seedling + lifespan = 40 + endurance = 7 + maturation = 10 + production = 1 + growthstages = 2 + yield = 1 + instability = 15 + potency = 30 + +/obj/item/seeds/seedling/harvest(mob/harvester) + var/obj/machinery/hydroponics/parent = loc + var/list/grow_locations = get_adjacent_open_turfs(parent) + var/turf/final_location = length(grow_locations) ? pick(grow_locations) : get_turf(parent) + var/mob/living/basic/seedling/seed_pet = new product(final_location) + seed_pet.befriend(harvester) + parent.update_tray(user = harvester, product_count = 1) + +/obj/item/seeds/seedling/evil + product = /mob/living/basic/seedling/meanie + icon_state = "seed-seedling-evil" diff --git a/code/modules/mob/living/basic/farm_animals/bee/bee_ai_behavior.dm b/code/modules/mob/living/basic/farm_animals/bee/bee_ai_behavior.dm index c57b190418b..017eac3cb51 100644 --- a/code/modules/mob/living/basic/farm_animals/bee/bee_ai_behavior.dm +++ b/code/modules/mob/living/basic/farm_animals/bee/bee_ai_behavior.dm @@ -93,6 +93,10 @@ return FALSE var/mob/living/mob_target = target + + if(mob_target.mob_biotypes & MOB_PLANT) + return FALSE + var/datum/ai_controller/basic_controller/bee_ai = owner.ai_controller if(isnull(bee_ai)) return FALSE diff --git a/code/modules/mob/living/basic/jungle/seedling/seedling.dm b/code/modules/mob/living/basic/jungle/seedling/seedling.dm new file mode 100644 index 00000000000..998f693d6da --- /dev/null +++ b/code/modules/mob/living/basic/jungle/seedling/seedling.dm @@ -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 diff --git a/code/modules/mob/living/basic/jungle/seedling/seedling_ai.dm b/code/modules/mob/living/basic/jungle/seedling/seedling_ai.dm new file mode 100644 index 00000000000..4d67a71d4d4 --- /dev/null +++ b/code/modules/mob/living/basic/jungle/seedling/seedling_ai.dm @@ -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 diff --git a/code/modules/mob/living/basic/jungle/seedling/seedling_projectiles.dm b/code/modules/mob/living/basic/jungle/seedling/seedling_projectiles.dm new file mode 100644 index 00000000000..726b8105933 --- /dev/null +++ b/code/modules/mob/living/basic/jungle/seedling/seedling_projectiles.dm @@ -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 diff --git a/code/modules/mob/living/simple_animal/hostile/jungle/seedling.dm b/code/modules/mob/living/simple_animal/hostile/jungle/seedling.dm deleted file mode 100644 index 5cb22b75521..00000000000 --- a/code/modules/mob/living/simple_animal/hostile/jungle/seedling.dm +++ /dev/null @@ -1,240 +0,0 @@ -#define SEEDLING_STATE_NEUTRAL 0 -#define SEEDLING_STATE_WARMUP 1 -#define SEEDLING_STATE_ACTIVE 2 -#define SEEDLING_STATE_RECOVERY 3 - -//A plant rooted in the ground that forfeits its melee attack in favor of ranged barrages. -//It will fire flurries of solar energy, and occasionally charge up a powerful blast that makes it vulnerable to attack. -/mob/living/simple_animal/hostile/jungle/seedling - name = "seedling" - desc = "This oversized, predatory flower conceals what can only be described as an organic energy cannon, and it will not die until its hidden vital organs are sliced out. \ - The concentrated streams of energy it sometimes produces require its full attention, attacking it during this time will prevent it from finishing its attack." - icon = 'icons/mob/simple/jungle/seedling.dmi' - icon_state = "seedling" - icon_living = "seedling" - icon_dead = "seedling_dead" - mob_biotypes = MOB_ORGANIC | MOB_PLANT - maxHealth = 100 - health = 100 - melee_damage_lower = 30 - melee_damage_upper = 30 - SET_BASE_PIXEL(-16, -14) - - minimum_distance = 3 - move_to_delay = 20 - vision_range = 9 - aggro_vision_range = 15 - ranged = TRUE - ranged_cooldown_time = 10 - projectiletype = /obj/projectile/seedling - projectilesound = 'sound/weapons/pierce.ogg' - robust_searching = TRUE - stat_attack = HARD_CRIT - move_resist = MOVE_FORCE_EXTREMELY_STRONG - var/combatant_state = SEEDLING_STATE_NEUTRAL - var/mob/living/beam_debuff_target - var/solar_beam_identifier = 0 - -/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 - hitsound = 'sound/weapons/sear.ogg' - hitsound_wall = 'sound/weapons/effects/searwall.ogg' - nondirectional_sprite = TRUE - -/obj/projectile/seedling/Bump(atom/A)//Stops seedlings from destroying other jungle mobs through FF - if(isliving(A)) - var/mob/living/L = A - if(FACTION_JUNGLE in L.faction) - return FALSE - 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 = 5 - randomdir = FALSE - -/datum/status_effect/seedling_beam_indicator - id = "seedling beam indicator" - duration = 3 SECONDS - status_type = STATUS_EFFECT_MULTIPLE - alert_type = null - tick_interval = 0.2 SECONDS - var/atom/movable/screen/seedling/seedling_screen_object - var/atom/target - - -/datum/status_effect/seedling_beam_indicator/on_creation(mob/living/new_owner, target_plant) - . = ..() - if(.) - target = target_plant - tick() - -/datum/status_effect/seedling_beam_indicator/on_apply() - if(owner.client) - seedling_screen_object = new /atom/movable/screen/seedling() - owner.client.screen += seedling_screen_object - tick() - return ..() - -/datum/status_effect/seedling_beam_indicator/Destroy() - if(owner) - if(owner.client) - owner.client.screen -= seedling_screen_object - return ..() - -/datum/status_effect/seedling_beam_indicator/tick(seconds_between_ticks) - var/target_angle = get_angle(owner, target) - var/matrix/final = matrix() - final.Turn(target_angle) - seedling_screen_object.transform = final - -/atom/movable/screen/seedling - icon = 'icons/mob/simple/jungle/arachnid.dmi' - icon_state = "seedling_beam_indicator" - screen_loc = "CENTER:-16,CENTER:-16" - -/mob/living/simple_animal/hostile/jungle/seedling/Goto() - if(combatant_state != SEEDLING_STATE_NEUTRAL) - return - return ..() - -/mob/living/simple_animal/hostile/jungle/seedling/AttackingTarget() - if(isliving(target)) - if(ranged_cooldown <= world.time && combatant_state == SEEDLING_STATE_NEUTRAL) - OpenFire(target) - return - return ..() - -/mob/living/simple_animal/hostile/jungle/seedling/OpenFire() - WarmupAttack() - -/mob/living/simple_animal/hostile/jungle/seedling/proc/WarmupAttack() - if(combatant_state == SEEDLING_STATE_NEUTRAL) - combatant_state = SEEDLING_STATE_WARMUP - SSmove_manager.stop_looping(src) - update_icons() - var/target_dist = get_dist(src,target) - var/living_target_check = isliving(target) - if(living_target_check) - if(target_dist > 7)//Offscreen check - SolarBeamStartup(target) - return - if(get_dist(src,target) >= 4 && prob(40)) - SolarBeamStartup(target) - return - addtimer(CALLBACK(src, PROC_REF(Volley)), 5) - -/mob/living/simple_animal/hostile/jungle/seedling/proc/SolarBeamStartup(mob/living/living_target)//It's more like requiem than final spark - if(combatant_state == SEEDLING_STATE_WARMUP && target) - combatant_state = SEEDLING_STATE_ACTIVE - living_target.apply_status_effect(/datum/status_effect/seedling_beam_indicator, src) - beam_debuff_target = living_target - playsound(src,'sound/effects/seedling_chargeup.ogg', 100, FALSE) - if(get_dist(src,living_target) > 7) - playsound(living_target,'sound/effects/seedling_chargeup.ogg', 100, FALSE) - solar_beam_identifier = world.time - addtimer(CALLBACK(src, PROC_REF(Beamu), living_target, solar_beam_identifier), 35) - -/mob/living/simple_animal/hostile/jungle/seedling/proc/Beamu(mob/living/living_target, beam_id = 0) - if(combatant_state == SEEDLING_STATE_ACTIVE && living_target && beam_id == solar_beam_identifier) - if(living_target.z == z) - update_icons() - var/obj/effect/temp_visual/solarbeam_killsat/S = new (get_turf(src)) - var/matrix/starting = matrix() - starting.Scale(1,32) - starting.Translate(0,520) - S.transform = starting - var/obj/effect/temp_visual/solarbeam_killsat/K = new (get_turf(living_target)) - var/matrix/final = matrix() - final.Scale(1,32) - final.Translate(0,512) - K.transform = final - living_target.adjustFireLoss(30) - living_target.adjust_fire_stacks(0.2)//Just here for the showmanship - living_target.ignite_mob() - playsound(living_target,'sound/weapons/sear.ogg', 50, TRUE) - addtimer(CALLBACK(src, PROC_REF(AttackRecovery)), 5) - return - AttackRecovery() - -/mob/living/simple_animal/hostile/jungle/seedling/proc/Volley() - if(combatant_state == SEEDLING_STATE_WARMUP && target) - combatant_state = SEEDLING_STATE_ACTIVE - update_icons() - var/datum/callback/cb = CALLBACK(src, PROC_REF(InaccurateShot)) - for(var/i in 1 to 13) - addtimer(cb, i) - addtimer(CALLBACK(src, PROC_REF(AttackRecovery)), 14) - -/mob/living/simple_animal/hostile/jungle/seedling/proc/InaccurateShot() - if(!QDELETED(target) && combatant_state == SEEDLING_STATE_ACTIVE && !stat) - if(get_dist(src,target) <= 3)//If they're close enough just aim straight at them so we don't miss at point blank ranges - Shoot(target) - return - var/turf/our_turf = get_turf(src) - var/obj/projectile/seedling/readied_shot = new /obj/projectile/seedling(our_turf) - readied_shot.preparePixelProjectile(target, src, null, rand(-10, 10)) - readied_shot.fire() - playsound(src, projectilesound, 100, TRUE) - -/mob/living/simple_animal/hostile/jungle/seedling/proc/AttackRecovery() - if(combatant_state == SEEDLING_STATE_ACTIVE) - combatant_state = SEEDLING_STATE_RECOVERY - update_icons() - ranged_cooldown = world.time + ranged_cooldown_time - if(target) - face_atom(target) - addtimer(CALLBACK(src, PROC_REF(ResetNeutral)), 10) - -/mob/living/simple_animal/hostile/jungle/seedling/proc/ResetNeutral() - combatant_state = SEEDLING_STATE_NEUTRAL - if(target && !stat) - update_icons() - Goto(target, move_to_delay, minimum_distance) - -/mob/living/simple_animal/hostile/jungle/seedling/adjustHealth(amount, updating_health = TRUE, forced = FALSE) - . = ..() - if(combatant_state == SEEDLING_STATE_ACTIVE && beam_debuff_target) - beam_debuff_target.remove_status_effect(/datum/status_effect/seedling_beam_indicator) - beam_debuff_target = null - solar_beam_identifier = 0 - AttackRecovery() - -/mob/living/simple_animal/hostile/jungle/seedling/update_icons() - . = ..() - if(!stat) - 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" - if(SEEDLING_STATE_RECOVERY) - icon_state = "seedling" - -/mob/living/simple_animal/hostile/jungle/seedling/GiveTarget() - if(target) - if(combatant_state == SEEDLING_STATE_WARMUP || combatant_state == SEEDLING_STATE_ACTIVE)//So it doesn't 180 and blast you in the face while it's firing at someone else - return - return ..() - -/mob/living/simple_animal/hostile/jungle/seedling/LoseTarget() - if(combatant_state == SEEDLING_STATE_WARMUP || combatant_state == SEEDLING_STATE_ACTIVE) - return - return ..() - -#undef SEEDLING_STATE_NEUTRAL -#undef SEEDLING_STATE_WARMUP -#undef SEEDLING_STATE_ACTIVE -#undef SEEDLING_STATE_RECOVERY diff --git a/code/modules/unit_tests/simple_animal_freeze.dm b/code/modules/unit_tests/simple_animal_freeze.dm index c97e034e0b3..571cecff3c0 100644 --- a/code/modules/unit_tests/simple_animal_freeze.dm +++ b/code/modules/unit_tests/simple_animal_freeze.dm @@ -136,7 +136,6 @@ /mob/living/simple_animal/hostile/jungle, /mob/living/simple_animal/hostile/jungle/leaper, /mob/living/simple_animal/hostile/jungle/mook, - /mob/living/simple_animal/hostile/jungle/seedling, /mob/living/simple_animal/hostile/megafauna, /mob/living/simple_animal/hostile/megafauna/blood_drunk_miner, /mob/living/simple_animal/hostile/megafauna/blood_drunk_miner/doom, diff --git a/code/modules/uplink/uplink_items/job.dm b/code/modules/uplink/uplink_items/job.dm index 991252a3826..7eecbbf78bf 100644 --- a/code/modules/uplink/uplink_items/job.dm +++ b/code/modules/uplink/uplink_items/job.dm @@ -325,3 +325,10 @@ cost = 14 //High cost because of the potential for extreme damage in the hands of a skilled scientist. restricted_roles = list(JOB_RESEARCH_DIRECTOR, JOB_SCIENTIST) surplus = 5 + +/datum/uplink_item/role_restricted/evil_seedling + name = "Evil Seedling" + desc = "A rare seed we have recovered that grows into a dangerous species that will aid you with your tasks!" + item = /obj/item/seeds/seedling/evil + cost = 8 + restricted_roles = list(JOB_BOTANIST) diff --git a/icons/mob/simple/jungle/seedling.dmi b/icons/mob/simple/jungle/seedling.dmi index 01e91c6c292..c4a76ebb2d1 100644 Binary files a/icons/mob/simple/jungle/seedling.dmi and b/icons/mob/simple/jungle/seedling.dmi differ diff --git a/icons/obj/service/hydroponics/growing_fruits.dmi b/icons/obj/service/hydroponics/growing_fruits.dmi index 7e535cdaf9b..92c52b55dbf 100644 Binary files a/icons/obj/service/hydroponics/growing_fruits.dmi and b/icons/obj/service/hydroponics/growing_fruits.dmi differ diff --git a/icons/obj/service/hydroponics/seeds.dmi b/icons/obj/service/hydroponics/seeds.dmi index b31590560d8..ffef219193a 100644 Binary files a/icons/obj/service/hydroponics/seeds.dmi and b/icons/obj/service/hydroponics/seeds.dmi differ diff --git a/tgstation.dme b/tgstation.dme index b653b394bff..84f1df04d46 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -3863,6 +3863,7 @@ #include "code\modules\hydroponics\grown\random.dm" #include "code\modules\hydroponics\grown\replicapod.dm" #include "code\modules\hydroponics\grown\root.dm" +#include "code\modules\hydroponics\grown\seedling.dm" #include "code\modules\hydroponics\grown\sugarcane.dm" #include "code\modules\hydroponics\grown\tea_coffee.dm" #include "code\modules\hydroponics\grown\tobacco.dm" @@ -4283,6 +4284,9 @@ #include "code\modules\mob\living\basic\jungle\mega_arachnid\mega_arachnid.dm" #include "code\modules\mob\living\basic\jungle\mega_arachnid\mega_arachnid_abilities.dm" #include "code\modules\mob\living\basic\jungle\mega_arachnid\mega_arachnid_ai.dm" +#include "code\modules\mob\living\basic\jungle\seedling\seedling.dm" +#include "code\modules\mob\living\basic\jungle\seedling\seedling_ai.dm" +#include "code\modules\mob\living\basic\jungle\seedling\seedling_projectiles.dm" #include "code\modules\mob\living\basic\lavaland\mining.dm" #include "code\modules\mob\living\basic\lavaland\basilisk\basilisk.dm" #include "code\modules\mob\living\basic\lavaland\basilisk\basilisk_overheat.dm" @@ -4616,7 +4620,6 @@ #include "code\modules\mob\living\simple_animal\hostile\jungle\_jungle_mobs.dm" #include "code\modules\mob\living\simple_animal\hostile\jungle\leaper.dm" #include "code\modules\mob\living\simple_animal\hostile\jungle\mook.dm" -#include "code\modules\mob\living\simple_animal\hostile\jungle\seedling.dm" #include "code\modules\mob\living\simple_animal\hostile\megafauna\_megafauna.dm" #include "code\modules\mob\living\simple_animal\hostile\megafauna\blood_drunk_miner.dm" #include "code\modules\mob\living\simple_animal\hostile\megafauna\bubblegum.dm"