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

🆑
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.
/🆑

Co-authored-by: Fikou <23585223+Fikou@users.noreply.github.com>
This commit is contained in:
Jacquerel
2023-01-07 22:36:29 +01:00
committed by GitHub
co-authored by Fikou
parent a427ec1957
commit 6200bc2360
12 changed files with 587 additions and 24 deletions
@@ -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
@@ -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)
@@ -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)