New PVE Major Midround: Demonic Incursion (#29053)

* Initial commit. Event.

* Started converting mobs to basic mobs. Migo and Creature. Needs aggressiveness AI

* Makes CI happy

* Fixes some file names

* Ticks files

* Fixes file again

* Update code/modules/events/demon_incursion.dm

Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>

* Announcement sound, spawn adjustments

* Reduced spread when portals multiply

* Elite spawns when portal count gets high

* Fixes missing comma

* Converted migo, blank, and most of hellhounds to basic mobs. Added new controllers and behaviors

* Fixes

* Linters!

* Faithless moved to basic mob

* LINTERS

* Makes skeletons basic mobs

* Fixes, Ranged Attacks, Ranged variants of nether mobs

* Handles portal expansion chance

* Linters

* Fixes mobs not breaking shit

* Fixes ranged attacks

* Fixes ranged attacks

* oops

* Another oops. No config changes are needed here

* Twenty percent chance that a nether mob is a grappler

* Dimensional tear fixes

* Adjusts awaken distance for hostile mobs, makes variable melee attack rate for basic mobs

* Variable initial spawns, variable spread rates.

* Whole lot of fixes from merge, hellhound completion

* Updated lavaland winter biodome

* Makes basic mobs able to hurt other mobs

* Makes spawners properly rally basic mobs to beat up the attacker

* Extra line

* Removed comment

* Makes hellhounds stop resting when attacked or when they find a new target

* Fixed initial portal spawn amounts

* Borgs now affected by basic mobs

* Nerfs portal spawn rate, nerfs portal integrity, nerfs portal max mobs

* Grapplers now teleport to missed turfs

* Removes duplicate notices

* Buff portals a small bit

* Makes nether portals no longer RR - the body is now recoverable from the blank it became

* Makes portals layer above mobs

* Removed excess ranged attack var

* Changes list for determining start count to rely on mobs with client instead of all clients

* Nerfs portal max mobs

* Portals can no longer spread to within 3 tiles of another portal except on initial event start

* Adjusts target portal count for big mobs

* Spawners now properly remove nest values of basic mobs

* Portals now glow an evil red. When portals are destroyed, 50% chance per mob to slay the mob

* add prowling and return to home behaviors

* cut this down

* be a tiny bit smarter

* Some code cleanup

* Removes hostile base type, removing excess code

* Gives /obj/ a basic mob attack handler. Fixes turrets

* Gives basic mobs a HUD

* Fixes skeleton death flag

* Adjusted initial spawns

* Incursion portals now slowly convert turfs, up to range 3 of them, to hellish flooring

* Increases reward per destroyed portal

* Makes the final portal of an incursion play a sound on destruction, couple portal fixes

* Fixes basic mob xenobiology interactions

* Non-shit portal sprites

* Adds hostile machine element, mobs now actively target turrets and emitters

* Properly gibs things when they should gib

* Linter fix

* Portals now layer under living mobs but over dead ones

* Adds blackbox checking for demon incursion portal counts

* Increases mob sight range slightly to account for widescreen, adds alt-color for grappler, delays incursion announcement a bit more

* Portals now are more likely to spread the less there are

* Incursion portals now repair themselves after not being damaged for some time

* Grilles now shock basic mobs

* Portals will now clean up basic mob corpses near them by gibbing them

* Portal spread chance is now exponential regression

* Portal mob spawns now linearly scale in time

* Fixes some skeleton oversights in ruin mapping

* Demon incursions no longer can spread to tiles in a space area, such as near brig plating

* Moves corpse cleanup to mobs via component

* Portals now drop bodies that are being eaten when they're destroyed.

* Addresses code review

* Docs some vars

---------

Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>
Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
Co-authored-by: warriorstar-orion <orion@snowfrost.garden>
This commit is contained in:
PollardTheDragon
2025-07-16 00:19:23 +00:00
committed by GitHub
co-authored by Luc warriorstar-orion
parent 3342288f79
commit 05791861b7
50 changed files with 1265 additions and 288 deletions
@@ -6,6 +6,7 @@ GLOBAL_LIST_EMPTY_TYPED(hostile_machines, /atom)
GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(
/mob,
/obj/machinery/porta_turret,
/obj/machinery/power/emitter,
/obj/mecha,
)))
@@ -13,7 +14,7 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(
action_cooldown = 2 SECONDS
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/// How far can we see stuff?
var/vision_range = 9
var/vision_range = 11
/// Blackboard key for aggro range, uses vision range if not specified
var/aggro_range_key = BB_AGGRO_RANGE
@@ -156,3 +157,6 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(
/// Returns the desired final target from the filtered list of targets
/datum/ai_behavior/find_potential_targets/proc/pick_final_target(datum/ai_controller/controller, list/filtered_targets)
return pick(filtered_targets)
/datum/ai_behavior/find_potential_targets/bigger_range
vision_range = 16
@@ -0,0 +1,100 @@
/// If there's something between us and our target then we need to queue a behaviour to make it not be there
/datum/ai_planning_subtree/attack_obstacle_in_path
/// Blackboard key containing current target
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/// The action to execute, extend to add a different cooldown or something
var/attack_behaviour = /datum/ai_behavior/attack_obstructions
/datum/ai_planning_subtree/attack_obstacle_in_path/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return
var/turf/next_step = get_step_towards(controller.pawn, target)
if(!next_step.is_blocked_turf(exclude_mobs = TRUE, source_atom = controller.pawn))
return
controller.queue_behavior(attack_behaviour, target_key)
// Don't cancel future planning, maybe we can move now
/// Something is in our way, get it outta here
/datum/ai_behavior/attack_obstructions
action_cooldown = 2 SECONDS
/// If we should attack walls, be prepared for complaints about breaches
var/can_attack_turfs = FALSE
/// For if you want your mob to be able to attack dense objects
var/can_attack_dense_objects = TRUE
/datum/ai_behavior/attack_obstructions/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/mob/living/basic/basic_mob = controller.pawn
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/turf/next_step = get_step_towards(basic_mob, target)
var/dir_to_next_step = get_dir(basic_mob, next_step)
// If moving diagonally we need to punch both ways, or more accurately the one we are blocked in
var/list/dirs_to_move = list()
if(dir_to_next_step && GLOB.diagonals)
for(var/direction in GLOB.cardinal)
if(direction & dir_to_next_step)
dirs_to_move += direction
else
dirs_to_move += dir_to_next_step
for(var/direction in dirs_to_move)
if(attack_in_direction(controller, basic_mob, direction))
return AI_BEHAVIOR_DELAY
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/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)
if(!next_step.is_blocked_turf(exclude_mobs = TRUE, source_atom = controller.pawn))
return FALSE
for(var/obj/object as anything in next_step.contents)
if(!can_smash_object(basic_mob, object))
continue
basic_mob.melee_attack(object)
return TRUE
if(can_attack_turfs)
basic_mob.melee_attack(next_step)
return TRUE
return FALSE
/datum/ai_behavior/attack_obstructions/proc/can_smash_object(mob/living/basic/basic_mob, obj/object)
if(!object.density && !can_attack_dense_objects)
return FALSE
if(object.IsObscured())
return FALSE
if(basic_mob.see_invisible < object.invisibility)
return FALSE
var/list/whitelist = basic_mob.ai_controller.blackboard[BB_OBSTACLE_TARGETING_WHITELIST]
if(whitelist && !is_type_in_typecache(object, whitelist))
return FALSE
if((!ismachinery(object) && !isstructure(object)))
return FALSE
return TRUE // It's in our way, let's get it out of our way
/datum/ai_behavior/attack_obstructions/avoid_breaches
/datum/ai_behavior/attack_obstructions/avoid_breaches/can_smash_object(mob/living/basic/basic_mob, obj/object)
. = ..()
if(istype(object, /obj/structure/window) || istype(object, /obj/machinery/door/airlock/external))
for(var/dir in GLOB.alldirs)
if(isspaceturf(get_step(object, dir)))
return FALSE
/datum/ai_planning_subtree/attack_obstacle_in_path/low_priority_target
target_key = BB_LOW_PRIORITY_HUNTING_TARGET
/datum/ai_planning_subtree/attack_obstacle_in_path/pet_target
target_key = BB_CURRENT_PET_TARGET
/datum/ai_planning_subtree/attack_obstacle_in_path/prowl
attack_behaviour = /datum/ai_behavior/attack_obstructions/avoid_breaches
target_key = BB_PROWL_TARGET
@@ -0,0 +1,28 @@
/datum/ai_behavior/find_hunt_target/prowl
search_turf_types = TRUE
/datum/ai_behavior/find_hunt_target/prowl/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, types_to_hunt, hunt_range)
var/mob/living/living_mob = controller.pawn
var/list/interesting_objects = search_turf_types ? RANGE_TURFS(hunt_range, living_mob) : oview(hunt_range, living_mob)
shuffle_inplace(interesting_objects)
for(var/atom/possible_dinner as anything in typecache_filter_list(interesting_objects, types_to_hunt))
if(!valid_dinner(living_mob, possible_dinner, hunt_range, controller, seconds_per_tick))
continue
controller.set_blackboard_key(hunting_target_key, possible_dinner)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/datum/ai_behavior/hunt_target/prowl
always_reset_target = TRUE
behavior_flags = parent_type::behavior_flags | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/hunt_target/prowl/target_caught(mob/living/hunter, atom/hunted)
return // We're just going there
/datum/ai_planning_subtree/find_and_hunt_target/prowl
target_key = BB_PROWL_TARGET
finding_behavior = /datum/ai_behavior/find_hunt_target/prowl
hunting_behavior = /datum/ai_behavior/hunt_target/prowl
hunt_targets = list(/turf/simulated/floor/plasteel)
hunt_range = 8
hunt_chance = 50
@@ -0,0 +1,50 @@
/// Fire a ranged attack without interrupting movement.
/datum/ai_planning_subtree/ranged_skirmish
operational_datums = list(/datum/component/ranged_attacks)
/// Blackboard key holding target atom
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/// What AI behaviour do we actually run?
var/attack_behavior = /datum/ai_behavior/ranged_skirmish
/// If target is further away than this we don't fire
var/max_range = 9
/// If target is closer than this we don't fire
var/min_range = 2
/datum/ai_planning_subtree/ranged_skirmish/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
if(!controller.blackboard_key_exists(target_key))
return
controller.queue_behavior(attack_behavior, target_key, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION, max_range, min_range)
/// How often will we try to perform our ranged attack?
/datum/ai_behavior/ranged_skirmish
action_cooldown = 0.5 SECONDS
/datum/ai_behavior/ranged_skirmish/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key, max_range, min_range)
. = ..()
var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
return !QDELETED(target)
/datum/ai_behavior/ranged_skirmish/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key, max_range, min_range)
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
if(!targeting_strategy.can_attack(controller.pawn, target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/hiding_target = targeting_strategy.find_hidden_mobs(controller.pawn, target)
controller.set_blackboard_key(hiding_location_key, hiding_target)
target = hiding_target || target
var/distance = get_dist(controller.pawn, target)
if(distance > max_range || distance < min_range)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.ai_interact(target = target, intent = INTENT_HARM)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_planning_subtree/ranged_skirmish/no_minimum
min_range = 0
@@ -0,0 +1,35 @@
/// Find a target, walk at target, attack intervening obstacles
/datum/ai_controller/basic_controller/simple/simple_hostile_obstacles
planning_subtrees = list(
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
/datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/demonic_incursion
planning_subtrees = list(
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/find_and_hunt_target/prowl,
/datum/ai_planning_subtree/attack_obstacle_in_path/prowl,
)
/// Find a target, walk towards it AND shoot it
/datum/ai_controller/basic_controller/simple/simple_skirmisher
planning_subtrees = list(
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/ranged_skirmish,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
/datum/ai_controller/basic_controller/simple/simple_skirmisher/demon_incursion
planning_subtrees = list(
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/ranged_skirmish,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/find_and_hunt_target/prowl,
/datum/ai_planning_subtree/attack_obstacle_in_path/prowl,
)
@@ -0,0 +1,8 @@
/datum/ai_controller/basic_controller/simple
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
interesting_dist = AI_SIMPLE_INTERESTING_DIST
@@ -73,6 +73,12 @@
return FALSE
return TRUE
if(istype(the_target, /obj/machinery/power/emitter)) // Targetting emitters
var/obj/machinery/power/emitter/E = the_target
if(!E.active) // Don't attack if the emitter isn't active
return FALSE
return TRUE
return FALSE
/// Returns true if the mob and target share factions
@@ -0,0 +1,16 @@
/datum/component/incursion_mob_death
var/gib_delay = 2 MINUTES
/datum/component/incursion_mob_death/RegisterWithParent()
RegisterSignal(parent, COMSIG_MOB_DEATH, PROC_REF(extra_death_effect))
RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(extra_death_effect))
/datum/component/incursion_mob_death/proc/extra_death_effect(datum/source)
SIGNAL_HANDLER
if(prob(95)) // Save some corpses for xenobio
addtimer(CALLBACK(src, PROC_REF(delayed_gib)), gib_delay)
/datum/component/incursion_mob_death/proc/delayed_gib()
var/mob/living/demon = parent
new /obj/effect/temp_visual/demonic_grasp(demon.loc)
demon.gib()
+32 -4
View File
@@ -35,14 +35,17 @@
for(var/mob/living/simple_animal/L in spawned_mobs)
if(L.nest == src)
L.nest = null
for(var/mob/living/basic/L in spawned_mobs)
if(L.nest == src)
L.nest = null
spawned_mobs = null
/datum/component/spawner/proc/try_spawn_mob()
var/atom/P = parent
if(length(spawned_mobs) >= max_mobs)
return 0
return
if(spawn_delay > world.time)
return 0
return
spawn_delay = world.time + spawn_time
var/chosen_mob_type = pick(mob_types)
var/mob/living/simple_animal/L = new chosen_mob_type(P.loc)
@@ -52,6 +55,7 @@
L.faction = src.faction
P.visible_message("<span class='danger'>[L] [spawn_text] [P].</span>")
P.on_mob_spawn(L)
return L
/datum/component/spawner/proc/rally_spawned_mobs(parent, mob/living/target)
SIGNAL_HANDLER // COMSIG_SPAWNER_SET_TARGET
@@ -62,5 +66,29 @@
// start the cooldown first, because a rallied mob might fire on
// ourselves while this is happening, causing confusion
COOLDOWN_START(src, last_rally, 30 SECONDS)
for(var/mob/living/simple_animal/hostile/rallied as anything in spawned_mobs)
INVOKE_ASYNC(rallied, TYPE_PROC_REF(/mob/living/simple_animal/hostile, aggro_fast), target)
for(var/mob/living/rallied as anything in spawned_mobs)
if(istype(rallied, /mob/living/basic))
var/mob/living/basic/basic = rallied
basic.ai_controller.cancel_actions()
basic.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, target)
if(istype(rallied, /mob/living/simple_animal/hostile))
var/mob/living/simple_animal/hostile/simple = rallied
INVOKE_ASYNC(simple, TYPE_PROC_REF(/mob/living/simple_animal/hostile, aggro_fast), target)
/datum/component/spawner/demon_incursion_portal
/datum/component/spawner/demon_incursion_portal/try_spawn_mob()
var/atom/result = ..()
if(result && istype(result.ai_controller))
result.ai_controller.set_blackboard_key(BB_INCURSION_HOME_PORTAL, parent)
return result
/datum/component/spawner/demon_incursion_portal/rally_spawned_mobs(parent, mob/living/target)
if(!(COOLDOWN_FINISHED(src, last_rally)))
return
COOLDOWN_START(src, last_rally, 30 SECONDS)
for(var/mob/living/basic/rallied as anything in spawned_mobs)
rallied.ai_controller.cancel_actions()
rallied.ai_controller.queue_behavior(/datum/ai_behavior/return_home/incursion_portal, BB_INCURSION_HOME_PORTAL)
+1 -1
View File
@@ -82,7 +82,7 @@
return
if(!IS_HORIZONTAL(L) && !can_start_on_stander)
return
if(IS_HORIZONTAL(L) && !on_operable_surface(L) && !isanimal(L))
if(IS_HORIZONTAL(L) && !on_operable_surface(L) && !(isanimal(L) || isbasicmob(L)))
return
if(iscarbon(target))
var/mob/living/carbon/C = target
+19
View File
@@ -0,0 +1,19 @@
/// AIs will attack this as a potential target if they see it
/datum/element/hostile_machine
element_flags = ELEMENT_DETACH_ON_HOST_DESTROY
/datum/element/hostile_machine/Attach(datum/target)
. = ..()
if(!isatom(target))
return ELEMENT_INCOMPATIBLE
#ifdef UNIT_TESTS
if(!GLOB.target_interested_atoms[target.type])
stack_trace("Tried to make a hostile machine without updating ai targeting to include it, they must be synced")
#endif
GLOB.hostile_machines += target
/datum/element/hostile_machine/Detach(datum/source)
GLOB.hostile_machines -= source
return ..()
+6 -6
View File
@@ -112,14 +112,14 @@
action_icon_state = "glare"
gain_desc = "You have gained the ability to shapeshift into lesser hellhound form. This is a combat form with different abilities, tough but not invincible. It can regenerate itself over time by resting."
shapeshift_type = /mob/living/simple_animal/hostile/hellhound
current_shapes = list(/mob/living/simple_animal/hostile/hellhound)
shapeshift_type = /mob/living/basic/hellhound
current_shapes = list(/mob/living/basic/hellhound)
current_casters = list()
possible_shapes = list(/mob/living/simple_animal/hostile/hellhound)
possible_shapes = list(/mob/living/basic/hellhound)
/datum/spell/shapeshift/hellhound/greater
name = "Greater Hellhound Form"
shapeshift_type = /mob/living/simple_animal/hostile/hellhound/greater
current_shapes = list(/mob/living/simple_animal/hostile/hellhound/greater)
shapeshift_type = /mob/living/basic/hellhound/greater
current_shapes = list(/mob/living/basic/hellhound/greater)
current_casters = list()
possible_shapes = list(/mob/living/simple_animal/hostile/hellhound/greater)
possible_shapes = list(/mob/living/basic/hellhound/greater)