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"