From 6200bc23602dc058da7a552d8a0c2d2b67f29e60 Mon Sep 17 00:00:00 2001 From: Jacquerel Date: Sat, 7 Jan 2023 21:36:29 +0000 Subject: [PATCH] Basic Mob Carp IX: Carp Rifts & Migration (#72265) ## About The Pull Request The almost-final part of the much larger PR I tried to make a month ago (there's actually one more thing but I'm waiting on a dog PR to get merged first). This adds _new_ behaviour and abilities to carp. Now when a Carp Migration occurs, all of the space carp who are spawned are given a path through the station. Specifically, each carp which shares a z level will try to path to a specific station area, then back out into space. https://www.youtube.com/watch?v=0KtTI4_7a0c Here in this video we follow one carp and its friends as it attempts to navigate "Kilo Station" in order to return to its ancestral spawning grounds, via the dormitories. Why are there walls underneath those windows? That's a question nature has no answer for. In order to ensure that they don't destroy Arrivals, Departures, and anywhere else with windows in the process of trying to get inside they have also gained a new "Lesser Carp Rift" ability. This allows carp to teleport a short distance once per minute, leaving a rift at their exit point. Any other mob can enter the rift to travel to a similar location to the space carp (within the same 3x3 area). https://user-images.githubusercontent.com/7483112/209584254-afb5839b-a1cd-4c5a-b701-dbb47a024272.mp4 Teleporting puts their attack on a one second cooldown so you won't be _immediately_ bitten for 20 damage out of nowhere. Their AI has been updated appropriately and they will use these abilities: - If they're trying to migrate through the station and encounter an obstacle. - If they're trying to atack something and encounter an obstacle. - If they're trying to run away, as soon as possible. https://user-images.githubusercontent.com/7483112/209584287-4402bf5b-3c41-4603-9205-5c4da8b4cd1c.mp4 That last point includes the HoS's pet Lia, who is an occasional target of traitor objectives, which can either work in your favour (scaring her to a less secure location) or against it (wait, where did she go?). Also this fixes an embarassing bug where space carp weren't spaceproof but I am going to pull that out into its own PR so it can be merged without also needing to review this. ## Why It's Good For The Game Carp are an iconic space animal but also quite boring, which this hopefully remedies. In the current game the Carp Migration event announces itself to the crew and usually advertises to ghosts that a cool shark mob has spawned and this changes essentially nothing about the round, the only people Carp will usually attack are people who go out to set up the solars, and the occasional wandering curator or lone operative. This should make the announcement mean something, as suddenly it means a belligerent animal might unexpectedly try to pass through your workplace. Non-magical space carp are weak enough that even an unarmed spaceman can take on one or two at a time (and even being mildly armed with makeshift weapons you have around makes them fairly non-threatening) but it can give you a bit of excitement. The ability for Carp to teleport allows them to do this without causing _too much_ property damage or breaching the station, in my tests they will _generally_ find a way in which doesn't involve them busting windows open en masse. Also it just makes them a bit more interesting. Traitors with dehydrated carp are not much able to make use of the Carp Rift ability as there isn't any way to get them to do it on demand, but you could spawn one which is not allied to you and then try to scare it in an appropriate direction which I think is a fun use of the item. This undoubtedly will make Space Dragon player-controlled carp more dangerous in a way which is difficult to predict, but it also makes playing as them more fun and might encourage some guerilla tactics and cooperation which wasn't previously possible. ## Changelog :cl: add: Space Carp seem to have begun associating the station with food and attempting to enter from the outside, rather than simply congregating around solar panels. Employees are advised that these are wild animals, and should not be fed. add: Space Carp can intermittently teleport short distances, leaving a short lived rift which other nearby carp will be attracted to follow them through. /:cl: Co-authored-by: Fikou <23585223+Fikou@users.noreply.github.com> --- code/__DEFINES/ai_carp.dm | 6 + .../basic_ai_behaviors/step_towards_turf.dm | 76 +++++++ .../basic_ai_behaviors/travel_towards.dm | 39 ++++ .../basic_subtrees/attack_obstacle_in_path.dm | 3 +- code/modules/events/carp_migration.dm | 43 +++- code/modules/events/wizard/magicarp.dm | 22 +- .../mob/living/basic/space_fauna/carp/carp.dm | 18 ++ .../basic/space_fauna/carp/carp_abilities.dm | 120 ++++++++++ .../space_fauna/carp/carp_ai_migration.dm | 62 ++++++ .../space_fauna/carp/carp_ai_rift_actions.dm | 207 ++++++++++++++++++ .../space_fauna/carp/carp_controllers.dm | 11 + tgstation.dme | 4 + 12 files changed, 587 insertions(+), 24 deletions(-) create mode 100644 code/datums/ai/basic_mobs/basic_ai_behaviors/step_towards_turf.dm create mode 100644 code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm create mode 100644 code/modules/mob/living/basic/space_fauna/carp/carp_ai_migration.dm create mode 100644 code/modules/mob/living/basic/space_fauna/carp/carp_ai_rift_actions.dm diff --git a/code/__DEFINES/ai_carp.dm b/code/__DEFINES/ai_carp.dm index 575d18939d2..8286f326207 100644 --- a/code/__DEFINES/ai_carp.dm +++ b/code/__DEFINES/ai_carp.dm @@ -1,6 +1,12 @@ ///Carp AI blackboard keys +#define BB_CARP_RIFT "BB_carp_rift" #define BB_MAGICARP_SPELL "BB_magicarp_spell" +/// List of weakrefs to turfs we want to travel to +#define BB_CARP_MIGRATION_PATH "BB_carp_migration_path" +/// Current target turf in your migration +#define BB_CARP_MIGRATION_TARGET "BB_carp_migration_target" + /// Targetting keys for magicarp spells #define BB_MAGICARP_SPELL_TARGET "BB_magicarp_spell_target" #define BB_MAGICARP_SPELL_SPECIAL_TARGETTING "BB_magicarp_spell_special_targetting" diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/step_towards_turf.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/step_towards_turf.dm new file mode 100644 index 00000000000..f40bd739541 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/step_towards_turf.dm @@ -0,0 +1,76 @@ +/** + * # Step towards turf + * Moves a short distance towards a location repeatedly until you arrive at the destination. + * You'd use this over travel_towards if you're travelling a long distance over a long time, because the AI controller has a maximum range. + */ +/datum/ai_behavior/step_towards_turf + required_distance = 0 + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + /// How far ahead do we plot movement per action? Further means longer until we return to the decision tree, fewer means jerkier movement + /// This can still result in long moves because this is "a tile x tiles away" not "only move x tiles", you might path around some walls + var/step_distance = 3 + +/datum/ai_behavior/step_towards_turf/setup(datum/ai_controller/controller, turf_key) + var/datum/weakref/weak_turf = controller.blackboard[turf_key] + var/turf/target_turf = weak_turf?.resolve() + if (!target_turf || target_turf.is_blocked_turf(exclude_mobs = TRUE)) + target_turf = find_destination_turf(args) + if (!target_turf) + return FALSE + controller.blackboard[turf_key] = WEAKREF(target_turf) + + if (target_turf.z != controller.pawn.z) + return FALSE + + var/turf/destination = plot_movement(controller, target_turf) + if (!destination) + return FALSE + set_movement_target(controller, destination) + return ..() + +/** + * Get a turf to aim towards if we don't already have one, the default behaviour is actually to not do this but we want to extend it + * Gets passed all of the arguments from `setup` + */ +/datum/ai_behavior/step_towards_turf/proc/find_destination_turf() + return null + +/** + * Figure out where we're going to move to, which isn't all the way to the destination in one go + */ +/datum/ai_behavior/step_towards_turf/proc/plot_movement(datum/ai_controller/controller, turf/target_turf) + var/distance_to_destination = get_dist(controller.pawn, target_turf) + if (distance_to_destination <= step_distance) + return target_turf + + var/direction_to_destination = get_dir(controller.pawn, target_turf) + return get_ranged_target_turf(controller.pawn, direction_to_destination, step_distance) + +// We actually only wanted the movement so if we've arrived we're done +/datum/ai_behavior/step_towards_turf/perform(delta_time, datum/ai_controller/controller, area_key, turf_key) + . = ..() + finish_action(controller, succeeded = TRUE) + +/** + * # Step towards turf in area + * Moves a short distance towards a location in an area + * Unlike step_towards_turf it will reacquire a new turf from the area if it loses its target + */ +/datum/ai_behavior/step_towards_turf/in_area + +/datum/ai_behavior/step_towards_turf/in_area/setup(datum/ai_controller/controller, turf_key, area_key) + var/area/target_area = controller.blackboard[area_key] + if (!target_area) + return FALSE + + return ..() + +// Return the first valid turf in the area to replace a lost target +/datum/ai_behavior/step_towards_turf/in_area/find_destination_turf(datum/ai_controller/controller, turf_key, area_key) + var/area/target_area = controller.blackboard[area_key] + var/list/target_area_turfs = get_area_turfs(target_area.type) + for (var/turf/potential_target as anything in target_area_turfs) + if (potential_target.is_blocked_turf(exclude_mobs = TRUE)) + continue + return potential_target + return null diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm new file mode 100644 index 00000000000..fcbf1d328e0 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm @@ -0,0 +1,39 @@ +/** + * # Travel Towards + * Moves towards the atom in the passed blackboard key. + * Planning continues during this action so it can be interrupted by higher priority actions. + */ +/datum/ai_behavior/travel_towards + required_distance = 0 + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/travel_towards/setup(datum/ai_controller/controller, target_key) + . = ..() + var/datum/weakref/weak_target = controller.blackboard[target_key] + var/atom/target = weak_target?.resolve() + if(isnull(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/travel_towards/perform(delta_time, datum/ai_controller/controller, target_key) + . = ..() + finish_action(controller, TRUE) + +/** + * # Travel Towards Atom + * Travel towards an atom you pass directly from the controller rather than a blackboard key. + * You might need to do this to avoid repeating some checks in both a controller and an action. + */ +/datum/ai_behavior/travel_towards_atom + required_distance = 0 + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/travel_towards_atom/setup(datum/ai_controller/controller, atom/target_atom) + . = ..() + if(isnull(target_atom)) + return FALSE + set_movement_target(controller, target_atom) + +/datum/ai_behavior/travel_towards_atom/perform(delta_time, datum/ai_controller/controller, atom/target_atom) + . = ..() + finish_action(controller, TRUE) diff --git a/code/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm b/code/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm index 6ff6c9f1ae8..8a4e566d791 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/attack_obstacle_in_path.dm @@ -10,7 +10,7 @@ var/datum/weakref/weak_target = controller.blackboard[target_key] var/atom/target = weak_target?.resolve() - if(QDELETED(target)) + if(isnull(target)) return var/turf/next_step = get_step_towards(controller.pawn, target) @@ -50,6 +50,7 @@ for (var/direction in dirs_to_move) if (attack_in_direction(controller, basic_mob, direction)) return + finish_action(controller, succeeded = TRUE) /datum/ai_behavior/attack_obstructions/proc/attack_in_direction(datum/ai_controller/controller, mob/living/basic/basic_mob, direction) var/turf/next_step = get_step(basic_mob, direction) diff --git a/code/modules/events/carp_migration.dm b/code/modules/events/carp_migration.dm index 2ce29cdbd47..f7f0f18abe5 100644 --- a/code/modules/events/carp_migration.dm +++ b/code/modules/events/carp_migration.dm @@ -16,29 +16,60 @@ max_occurrences *= 2 earliest_start *= 0.5 - /datum/round_event/carp_migration announce_when = 3 start_when = 50 + /// Set to true when we announce something to ghosts, to prevent duplicate announcements var/hasAnnounced = FALSE + /// Most common mob type to spawn, must be a child of /mob/living/basic/carp + var/carp_type = /mob/living/basic/carp + /// Rarer mob type to spawn, must also be a child of /mob/living/basic/carp. If one of these is created, it will take priority to show ghosts. + var/boss_type = /mob/living/basic/carp/mega + /// What to describe detecting near the station + var/fluff_signal = "Unknown biological entities" /datum/round_event/carp_migration/setup() start_when = rand(40, 60) /datum/round_event/carp_migration/announce(fake) - priority_announce("Unknown biological entities have been detected near [station_name()], please stand-by.", "Lifesign Alert") - + priority_announce("[fluff_signal] have been detected near [station_name()], please stand-by.", "Lifesign Alert") /datum/round_event/carp_migration/start() + // Stores the most recent fish we spawn var/mob/living/basic/carp/fish - for(var/obj/effect/landmark/carpspawn/C in GLOB.landmarks_list) + // Associated lists of z level to a list of points to travel to, so that grouped fish move to the same places + var/list/z_migration_paths = list() + + for(var/obj/effect/landmark/carpspawn/spawn_point in GLOB.landmarks_list) if(prob(95)) - fish = new (C.loc) + fish = new carp_type(spawn_point.loc) else - fish = new /mob/living/basic/carp/mega(C.loc) + fish = new boss_type(spawn_point.loc) fishannounce(fish) //Prefer to announce the megacarps over the regular fishies + + var/z_level_key = "[spawn_point.z]" + if (!z_migration_paths[z_level_key]) + z_migration_paths[z_level_key] = pick_carp_migration_points(z_level_key) + if (z_migration_paths[z_level_key]) // Still possible we failed to set anything here if we're unlucky + fish.migrate_to(z_migration_paths[z_level_key]) + fishannounce(fish) +/// Generate two locations for carp to travel to, one in the station and one off in space +/datum/round_event/carp_migration/proc/pick_carp_migration_points(z_level_key) + var/list/valid_areas = list() + var/list/station_areas = GLOB.the_station_areas + for (var/area/potential_area as anything in SSmapping.areas_in_z[z_level_key]) + if (!is_type_in_list(potential_area, station_areas)) + continue + valid_areas += potential_area + + var/turf/station_turf = get_safe_random_station_turf(valid_areas) + if (!station_turf) + return list() + var/turf/exit_turf = get_edge_target_turf(station_turf, pick(GLOB.alldirs)) + return list(WEAKREF(station_turf), WEAKREF(exit_turf)) + /datum/round_event/carp_migration/proc/fishannounce(atom/fish) if (!hasAnnounced) announce_to_ghosts(fish) //Only anounce the first fish diff --git a/code/modules/events/wizard/magicarp.dm b/code/modules/events/wizard/magicarp.dm index 5a49a441fb9..42b4bf86c44 100644 --- a/code/modules/events/wizard/magicarp.dm +++ b/code/modules/events/wizard/magicarp.dm @@ -1,24 +1,12 @@ /datum/round_event_control/wizard/magicarp //these fish is loaded name = "Magicarp" weight = 1 - typepath = /datum/round_event/wizard/magicarp + typepath = /datum/round_event/carp_migration/wizard max_occurrences = 1 earliest_start = 0 MINUTES description = "Summons a school of carps with magic projectiles." -/datum/round_event/wizard/magicarp - announce_when = 3 - start_when = 50 - -/datum/round_event/wizard/magicarp/setup() - start_when = rand(40, 60) - -/datum/round_event/wizard/magicarp/announce(fake) - priority_announce("Unknown magical entities have been detected near [station_name()], please stand-by.", "Lifesign Alert") - -/datum/round_event/wizard/magicarp/start() - for(var/obj/effect/landmark/carpspawn/C in GLOB.landmarks_list) - if(prob(5)) - new /mob/living/basic/carp/magic/chaos(C.loc) - else - new /mob/living/basic/carp/magic(C.loc) +/datum/round_event/carp_migration/wizard + carp_type = /mob/living/basic/carp/magic + boss_type = /mob/living/basic/carp/magic/chaos + fluff_signal = "Unknown magical entities" diff --git a/code/modules/mob/living/basic/space_fauna/carp/carp.dm b/code/modules/mob/living/basic/space_fauna/carp/carp.dm index 205f199375e..24d75d0e23f 100644 --- a/code/modules/mob/living/basic/space_fauna/carp/carp.dm +++ b/code/modules/mob/living/basic/space_fauna/carp/carp.dm @@ -48,6 +48,8 @@ var/cell_line = CELL_LINE_TABLE_CARP /// What colour is our 'healing' outline? var/regenerate_colour = COLOR_PALE_GREEN + /// Ability which lets carp teleport around + var/datum/action/cooldown/mob_cooldown/lesser_carp_rift/teleport /// Information to apply when treating this carp as a vehicle var/ridable_data = /datum/component/riding/creature/carp /// Commands you can give this carp once it is tamed, not static because subtypes can modify it @@ -101,6 +103,14 @@ else AddComponent(/datum/component/tameable, food_types = list(/obj/item/food/meat), tame_chance = 10, bonus_tame_chance = 5, after_tame = CALLBACK(src, PROC_REF(on_tamed))) + teleport = new(src) + teleport.Grant(src) + ai_controller.blackboard[BB_CARP_RIFT] = teleport + +/mob/living/basic/carp/Destroy() + QDEL_NULL(teleport) + return ..() + /// Tell the elements and the blackboard what food we want to eat /mob/living/basic/carp/proc/setup_eating() AddElement(/datum/element/basic_eating, 10, 0, null, desired_food) @@ -123,6 +133,14 @@ spin(spintime = 10, speed = 1) visible_message("[src] spins in a circle as it seems to bond with [tamer].") +/// Teleport when you right click away from you +/mob/living/basic/carp/ranged_secondary_attack(atom/atom_target, modifiers) + teleport.Trigger(target = atom_target) + +/// Gives the carp a list of destinations to try and travel between when it has nothing better to do +/mob/living/basic/carp/proc/migrate_to(list/migration_points) + ai_controller.blackboard[BB_CARP_MIGRATION_PATH] = migration_points + /** * Holographic carp from the holodeck */ diff --git a/code/modules/mob/living/basic/space_fauna/carp/carp_abilities.dm b/code/modules/mob/living/basic/space_fauna/carp/carp_abilities.dm index abb24df73b4..94fc89ca9da 100644 --- a/code/modules/mob/living/basic/space_fauna/carp/carp_abilities.dm +++ b/code/modules/mob/living/basic/space_fauna/carp/carp_abilities.dm @@ -27,3 +27,123 @@ return projectile_type = pick(permitted_projectiles) return ..() + +/** + * # Lesser Carp Rift + * Teleport a short distance and leave a short-lived portal for people to follow through + */ +/datum/action/cooldown/mob_cooldown/lesser_carp_rift + name = "Lesser Carp Rift" + button_icon = 'icons/effects/effects.dmi' + button_icon_state = "rift" + desc = "Open a rift through the carp stream, allowing passage to somewhere close by." + cooldown_time = 1 MINUTES + melee_cooldown_time = 2 SECONDS + /// How far away can you place a rift? + var/max_range = 6 + +/datum/action/cooldown/mob_cooldown/lesser_carp_rift/Activate(atom/target_atom) + if (!make_rift(target_atom)) + return FALSE + StartCooldown() + return TRUE + +/datum/action/cooldown/mob_cooldown/lesser_carp_rift/proc/make_rift(atom/target_atom) + if (owner.Adjacent(target_atom)) + owner.balloon_alert(owner, "too close!") + return FALSE + + var/turf/owner_turf = get_turf(owner) + var/turf/target_turf = get_turf(target_atom) + if (!target_turf) + return FALSE + + if (get_dist(owner_turf, target_turf) > max_range) + owner.balloon_alert(owner, "too far!") + return FALSE + + if (!target_turf) + return FALSE + + var/list/open_exit_turfs = list() + for (var/turf/potential_exit in orange(1, target_turf)) + if (potential_exit.is_blocked_turf(exclude_mobs = TRUE)) + continue + open_exit_turfs += potential_exit + + if (!length(open_exit_turfs)) + owner.balloon_alert(owner, "no exit!") + return FALSE + if (!target_turf.is_blocked_turf(exclude_mobs = TRUE)) + open_exit_turfs += target_turf + + new /obj/effect/temp_visual/lesser_carp_rift/exit(target_turf) + var/obj/effect/temp_visual/lesser_carp_rift/entrance/enter = new(owner_turf) + enter.exit_locs = open_exit_turfs + enter.on_entered(enter, owner) + return TRUE + +/// If you touch the entrance you are teleported to the exit, exit doesn't do anything +/obj/effect/temp_visual/lesser_carp_rift + name = "lesser carp rift" + icon_state = "rift" + duration = 5 SECONDS + /// Holds a reference to a timer until this gets deleted + var/destroy_timer + +/obj/effect/temp_visual/lesser_carp_rift/Initialize(mapload) + destroy_timer = addtimer(CALLBACK(src, PROC_REF(animate_out)), duration - 1, TIMER_STOPPABLE) + return ..() + +/obj/effect/temp_visual/lesser_carp_rift/proc/animate_out() + var/obj/effect/temp_visual/lesser_carp_rift_dissipating/animate_out = new(loc) + animate_out.setup_animation(alpha) + +/obj/effect/temp_visual/lesser_carp_rift/Destroy() + . = ..() + deltimer(destroy_timer) + +/// If you touch this you are taken to the exit +/obj/effect/temp_visual/lesser_carp_rift/entrance + /// Where you get teleported to + var/list/exit_locs + +/obj/effect/temp_visual/lesser_carp_rift/entrance/Initialize(mapload) + . = ..() + var/static/list/loc_connections = list( + COMSIG_ATOM_ENTERED = PROC_REF(on_entered), + ) + AddElement(/datum/element/connect_loc, loc_connections) + +/obj/effect/temp_visual/lesser_carp_rift/entrance/proc/on_entered(datum/source, atom/movable/entered_atom) + SIGNAL_HANDLER + + if (!length(exit_locs)) + return + if (!ismob(entered_atom) && !isobj(entered_atom)) + return + if (entered_atom.anchored) + return + if(!entered_atom.loc) + return + if (isobserver(entered_atom)) + return + + var/turf/destination = pick(exit_locs) + do_teleport(entered_atom, destination, channel = TELEPORT_CHANNEL_MAGIC) + playsound(src, 'sound/magic/wand_teleport.ogg', 50) + playsound(destination, 'sound/magic/wand_teleport.ogg', 50) + +/// Doesn't actually do anything, just a visual marker +/obj/effect/temp_visual/lesser_carp_rift/exit + alpha = 125 + +/// Just an animation +/obj/effect/temp_visual/lesser_carp_rift_dissipating + name = "lesser carp rift" + icon_state = "rift" + duration = 1 SECONDS + +/obj/effect/temp_visual/lesser_carp_rift_dissipating/proc/setup_animation(new_alpha) + alpha = new_alpha + animate(src, alpha = 0, time = duration - 1) diff --git a/code/modules/mob/living/basic/space_fauna/carp/carp_ai_migration.dm b/code/modules/mob/living/basic/space_fauna/carp/carp_ai_migration.dm new file mode 100644 index 00000000000..86f57687456 --- /dev/null +++ b/code/modules/mob/living/basic/space_fauna/carp/carp_ai_migration.dm @@ -0,0 +1,62 @@ +/// How close you need to get to the destination in order to consider yourself there +#define CARP_DESTINATION_SEARCH_RANGE 3 +/// If there's a portal this close to us we'll enter it just on the basis that the carp who made it probably knew where they were going +#define CARP_PORTAL_SEARCH_RANGE 2 + +/** + * # Carp Migration + * Will try to plan a path between a list of locations for carp to travel through + */ +/datum/ai_planning_subtree/carp_migration + +/datum/ai_planning_subtree/carp_migration/SelectBehaviors(datum/ai_controller/controller, delta_time) + . = ..() + + // If there's a rift nearby take a ride, then cancel everything else because it's not valid any more + var/obj/effect/temp_visual/lesser_carp_rift/entrance/rift = locate(/obj/effect/temp_visual/lesser_carp_rift/entrance) in orange(controller.pawn, CARP_PORTAL_SEARCH_RANGE) + if (rift) + controller.queue_behavior(/datum/ai_behavior/travel_towards_atom, get_turf(rift)) + return SUBTREE_RETURN_FINISH_PLANNING + + var/list/migration_points = controller.blackboard[BB_CARP_MIGRATION_PATH] + if (!length(migration_points)) + return + + var/datum/weakref/weak_target = controller.blackboard[BB_CARP_MIGRATION_TARGET] + var/turf/moving_to = weak_target?.resolve() + + // If we don't have a target or are close enough to it, pick a new one + if (isnull(moving_to) || get_dist(controller.pawn, moving_to) <= CARP_DESTINATION_SEARCH_RANGE) + controller.queue_behavior(/datum/ai_behavior/find_next_carp_migration_step, BB_CARP_MIGRATION_PATH, BB_CARP_MIGRATION_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + + var/turf/next_step = get_step_towards(controller.pawn, moving_to) + if (next_step.is_blocked_turf(exclude_mobs = TRUE)) + controller.queue_behavior(/datum/ai_behavior/make_carp_rift/towards/unvalidated, BB_CARP_RIFT, BB_CARP_MIGRATION_TARGET) + controller.queue_behavior(/datum/ai_behavior/attack_obstructions/carp, BB_CARP_MIGRATION_TARGET) + controller.queue_behavior(/datum/ai_behavior/step_towards_turf, BB_CARP_MIGRATION_TARGET) + + return SUBTREE_RETURN_FINISH_PLANNING + +/** + * # Find next carp migration step + * Records the next turf we want to travel to into the blackboard for other actions + */ +/datum/ai_behavior/find_next_carp_migration_step + +/datum/ai_behavior/find_next_carp_migration_step/perform(delta_time, datum/ai_controller/controller, path_key, target_key) + var/list/blackboard_points = controller.blackboard[path_key] + var/list/potential_migration_points = blackboard_points.Copy() + while (length(potential_migration_points)) + var/datum/weakref/weak_destination = popleft(potential_migration_points) + var/turf/potential_destination = weak_destination.resolve() + if (!isnull(potential_destination) && get_dist(controller.pawn, potential_destination) > CARP_DESTINATION_SEARCH_RANGE) + controller.blackboard[target_key] = weak_destination + finish_action(controller, succeeded = TRUE) + return + controller.blackboard[path_key] = potential_migration_points.Copy() + + finish_action(controller, succeeded = FALSE) + +#undef CARP_DESTINATION_SEARCH_RANGE +#undef CARP_PORTAL_SEARCH_RANGE diff --git a/code/modules/mob/living/basic/space_fauna/carp/carp_ai_rift_actions.dm b/code/modules/mob/living/basic/space_fauna/carp/carp_ai_rift_actions.dm new file mode 100644 index 00000000000..4f8cf327e75 --- /dev/null +++ b/code/modules/mob/living/basic/space_fauna/carp/carp_ai_rift_actions.dm @@ -0,0 +1,207 @@ +/** + * # Make carp rift + * Plan a carp rift action, so basically teleport somewhere if the action is available + */ +/datum/ai_planning_subtree/make_carp_rift + /// Chiefly describes where we are placing this teleport + var/datum/ai_behavior/rift_behaviour + /// If true we finish planning after this + var/finish_planning = FALSE + +/datum/ai_planning_subtree/make_carp_rift/SelectBehaviors(datum/ai_controller/controller, delta_time) + if (!rift_behaviour) + CRASH("Forgot to specify rift behaviour for [src]") + + var/datum/weakref/weak_target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + var/mob/living/target = weak_target?.resolve() + if (!target) + return + + var/datum/action/cooldown/using_action = controller.blackboard[BB_CARP_RIFT] + if (isnull(using_action)) + return + if (!using_action.IsAvailable()) + return + + controller.queue_behavior(rift_behaviour, BB_CARP_RIFT, BB_BASIC_MOB_CURRENT_TARGET) + if (finish_planning) + return SUBTREE_RETURN_FINISH_PLANNING + +/** + * # Make carp rift (panic) + * Plan to teleport away from our target so they can't fuck us up + */ +/datum/ai_planning_subtree/make_carp_rift/panic_teleport + rift_behaviour = /datum/ai_behavior/make_carp_rift/away + finish_planning = TRUE + +/datum/ai_planning_subtree/make_carp_rift/panic_teleport/SelectBehaviors(datum/ai_controller/controller, delta_time) + if (!controller.blackboard[BB_BASIC_MOB_FLEEING]) + return + return ..() + +/** + * # Make carp rift (aggressive) + * Plan to teleport towards our target so we can fuck them up + */ +/datum/ai_planning_subtree/make_carp_rift/aggressive_teleport + rift_behaviour = /datum/ai_behavior/make_carp_rift/towards/aggressive + +/** + * # Make carp rift + * Make a carp rift somewhere + */ +/datum/ai_behavior/make_carp_rift + +/datum/ai_behavior/make_carp_rift/setup(datum/ai_controller/controller, ability_key, target_key) + var/datum/action/cooldown/mob_cooldown/lesser_carp_rift/ability = controller.blackboard[ability_key] + if (!ability) + return FALSE + var/datum/weakref/weak_target = controller.blackboard[target_key] + var/atom/target = weak_target?.resolve() + return target + +/datum/ai_behavior/make_carp_rift/perform(delta_time, datum/ai_controller/controller, ability_key, target_key) + . = ..() + var/datum/action/cooldown/mob_cooldown/lesser_carp_rift/ability = controller.blackboard[ability_key] + var/datum/weakref/weak_target = controller.blackboard[target_key] + var/atom/target = weak_target?.resolve() + + if (!validate_target(controller, target, ability)) + finish_action(controller, FALSE, ability_key, target_key) + return + + var/turf/target_destination = find_target_turf(controller, target, ability) + if (!target_destination) + finish_action(controller, FALSE, ability_key, target_key) + return + + var/result = ability.InterceptClickOn(controller.pawn, null, target_destination) + finish_action(controller, result, ability_key, target_key) + +/// Return true if your target is valid for the action +/datum/ai_behavior/make_carp_rift/proc/validate_target(datum/ai_controller/controller, atom/target, datum/action/cooldown/mob_cooldown/lesser_carp_rift/ability) + if (!ability) + return FALSE + if (!target) + return FALSE + return TRUE + +/// Return the turf to teleport to, implement this or the behaviour won't do anything +/datum/ai_behavior/make_carp_rift/proc/find_target_turf(datum/ai_controller/controller, atom/target, datum/action/cooldown/mob_cooldown/lesser_carp_rift/ability) + CRASH("Called unimplemented target finding proc on carp rift behaviour") + +/** + * # Make carp rift away + * Make a rift bringing you further away from your target + */ +/datum/ai_behavior/make_carp_rift/away + +/datum/ai_behavior/make_carp_rift/away/find_target_turf(datum/ai_controller/controller, atom/target, datum/action/cooldown/mob_cooldown/lesser_carp_rift/ability) + var/run_direction = get_dir(controller.pawn, get_step_away(controller.pawn, target)) + return get_ranged_target_turf(controller.pawn, run_direction, ability.max_range) + +/** + * # Make carp rift forwards + * Make a rift bringing you closer to your target + */ +/datum/ai_behavior/make_carp_rift/towards + /// Drop rift at least this many tiles away from target + var/teleport_buffer_distance = 0 + /// Teleport simply if you are far away + var/teleport_if_far = TRUE + /// Teleport if the turf in front of you is blocked + var/teleport_if_blocked = TRUE + +/datum/ai_behavior/make_carp_rift/towards/validate_target(datum/ai_controller/controller, atom/target, datum/action/cooldown/mob_cooldown/lesser_carp_rift/ability) + . = ..() + if (!.) + return FALSE + + if (teleport_if_far) + var/distance = get_dist(get_turf(controller.pawn), get_turf(target)) + if (distance >= ability.max_range + teleport_buffer_distance) // Perform if we are far away + return TRUE + + if (teleport_if_blocked) + var/turf/next_move = get_step_towards(controller.pawn, target) + if (next_move.is_blocked_turf(exclude_mobs = TRUE)) // Perform if target is behind cover + return TRUE + + return FALSE + +/datum/ai_behavior/make_carp_rift/towards/find_target_turf(datum/ai_controller/controller, atom/target, datum/action/cooldown/mob_cooldown/lesser_carp_rift/ability) + var/turf/target_turf = get_turf(target) + var/distance = get_dist(get_turf(controller.pawn), target_turf) + + var/turf/chosen_turf + + if (distance <= ability.max_range) + chosen_turf = target_turf + else + var/run_direction = get_dir(controller.pawn, get_step_towards(controller.pawn, target_turf)) + chosen_turf = get_ranged_target_turf(controller.pawn, run_direction, ability.max_range) + + if (!chosen_turf) + return + + // Subtract some distance so we don't drop carp directly on top of someone + var/rift_to_target_distance = get_dist(target_turf, chosen_turf) + if (rift_to_target_distance < teleport_buffer_distance) + var/away_direction = get_dir(controller.pawn, get_step_away(controller.pawn, target_turf)) + var/turf/backed_away_turf = get_ranged_target_turf(controller.pawn, away_direction, teleport_buffer_distance - rift_to_target_distance) + if (distance > get_dist(backed_away_turf, target_turf)) + chosen_turf = backed_away_turf // Avoid edge case pointless teleports from being up against a wall + + return chosen_turf + +/** + * # Make carp rift forwards (aggressive) + * Make a rift towards your target if you are blocked from moving or if it is far away + */ +/datum/ai_behavior/make_carp_rift/towards/aggressive + teleport_buffer_distance = 2 // Don't aggressively drop carps directly on top of a target mob + +/** + * # Make carp rift forwards (unvalidated) + * Skip validation checks because we already did them in the controller + */ +/datum/ai_behavior/make_carp_rift/towards/unvalidated + +/datum/ai_behavior/make_carp_rift/towards/unvalidated/validate_target(datum/ai_controller/controller, atom/target, datum/action/cooldown/mob_cooldown/lesser_carp_rift/ability) + return TRUE + +/datum/ai_behavior/make_carp_rift/towards/unvalidated/finish_action(datum/ai_controller/controller, succeeded, ...) + . = ..() + if (succeeded) + controller.CancelActions() + +/** + * # Shortcut to target through carp rift + * If there's a carp rift heading your way, plan to ride it to your target + */ +/datum/ai_planning_subtree/shortcut_to_target_through_carp_rift + /// How far away do we look for rifts? + var/search_distance = 2 + /// Minimum distance we should be from the target before we bother performing this action + var/minimum_distance = 3 + +/datum/ai_planning_subtree/shortcut_to_target_through_carp_rift/SelectBehaviors(datum/ai_controller/controller, delta_time) + var/datum/weakref/weak_target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + var/mob/living/target = weak_target?.resolve() + if (isnull(target)) + return + + var/distance_to_target = get_dist(controller.pawn, target) + if (distance_to_target <= minimum_distance) + return + + for (var/obj/effect/temp_visual/lesser_carp_rift/entrance/rift in orange(controller.pawn, search_distance)) + var/exit_count = length(rift.exit_locs) + if (!exit_count) + continue + var/turf/rift_exit = rift.exit_locs[exit_count] + if (get_dist(rift_exit, target) >= distance_to_target) + continue + controller.queue_behavior(/datum/ai_behavior/travel_towards_atom, get_turf(rift)) + return SUBTREE_RETURN_FINISH_PLANNING diff --git a/code/modules/mob/living/basic/space_fauna/carp/carp_controllers.dm b/code/modules/mob/living/basic/space_fauna/carp/carp_controllers.dm index 4cb0ea4f476..4210853c02a 100644 --- a/code/modules/mob/living/basic/space_fauna/carp/carp_controllers.dm +++ b/code/modules/mob/living/basic/space_fauna/carp/carp_controllers.dm @@ -18,11 +18,15 @@ planning_subtrees = list( /datum/ai_planning_subtree/pet_planning, /datum/ai_planning_subtree/simple_find_nearest_target_to_flee, + /datum/ai_planning_subtree/make_carp_rift/panic_teleport, /datum/ai_planning_subtree/flee_target, /datum/ai_planning_subtree/find_food, /datum/ai_planning_subtree/simple_find_target, /datum/ai_planning_subtree/attack_obstacle_in_path/carp, + /datum/ai_planning_subtree/shortcut_to_target_through_carp_rift, + /datum/ai_planning_subtree/make_carp_rift/aggressive_teleport, /datum/ai_planning_subtree/basic_melee_attack_subtree/carp, + /datum/ai_planning_subtree/carp_migration, ) /** @@ -38,9 +42,12 @@ planning_subtrees = list( /datum/ai_planning_subtree/pet_planning, /datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee, + /datum/ai_planning_subtree/make_carp_rift/panic_teleport, /datum/ai_planning_subtree/flee_target, /datum/ai_planning_subtree/target_retaliate, /datum/ai_planning_subtree/attack_obstacle_in_path/carp, + /datum/ai_planning_subtree/shortcut_to_target_through_carp_rift, + /datum/ai_planning_subtree/make_carp_rift/aggressive_teleport, /datum/ai_planning_subtree/basic_melee_attack_subtree/carp, ) @@ -52,11 +59,15 @@ planning_subtrees = list( /datum/ai_planning_subtree/pet_planning, /datum/ai_planning_subtree/simple_find_nearest_target_to_flee, + /datum/ai_planning_subtree/make_carp_rift/panic_teleport, /datum/ai_planning_subtree/flee_target, /datum/ai_planning_subtree/find_food, /datum/ai_planning_subtree/find_nearest_magicarp_spell_target, /datum/ai_planning_subtree/targetted_mob_ability/magicarp, /datum/ai_planning_subtree/simple_find_target, /datum/ai_planning_subtree/attack_obstacle_in_path/carp, + /datum/ai_planning_subtree/shortcut_to_target_through_carp_rift, + /datum/ai_planning_subtree/make_carp_rift/aggressive_teleport, /datum/ai_planning_subtree/basic_melee_attack_subtree/magicarp, + /datum/ai_planning_subtree/carp_migration, ) diff --git a/tgstation.dme b/tgstation.dme index b35053773ae..d1c3b94bcd4 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -729,8 +729,10 @@ #include "code\datums\ai\basic_mobs\basic_ai_behaviors\nearest_targetting.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\pick_up_item.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\run_away_from_target.dm" +#include "code\datums\ai\basic_mobs\basic_ai_behaviors\step_towards_turf.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\targetting.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\tipped_reaction.dm" +#include "code\datums\ai\basic_mobs\basic_ai_behaviors\travel_towards.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\try_mob_ability.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\attack_obstacle_in_path.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\find_food.dm" @@ -3680,6 +3682,8 @@ #include "code\modules\mob\living\basic\space_fauna\carp\carp.dm" #include "code\modules\mob\living\basic\space_fauna\carp\carp_abilities.dm" #include "code\modules\mob\living\basic\space_fauna\carp\carp_ai_actions.dm" +#include "code\modules\mob\living\basic\space_fauna\carp\carp_ai_migration.dm" +#include "code\modules\mob\living\basic\space_fauna\carp\carp_ai_rift_actions.dm" #include "code\modules\mob\living\basic\space_fauna\carp\carp_controllers.dm" #include "code\modules\mob\living\basic\space_fauna\carp\magicarp.dm" #include "code\modules\mob\living\basic\space_fauna\carp\megacarp.dm"