From 2e8a37b30b9dbf07cc52f0abc639386d86735b0f Mon Sep 17 00:00:00 2001 From: InsightfulParasite Date: Fri, 1 Sep 2023 16:33:14 -0400 Subject: [PATCH] Create ai_leadership.dm Update lobotomy-corp13.dme Update ai_leadership.dm Update indigo.dm --- code/datums/components/ai_leadership.dm | 291 ++++++++++++++++++ .../simple_animal/hostile/ordeal/indigo.dm | 53 +--- .../simple_animal/hostile/ordeal/steel.dm | 120 ++------ lobotomy-corp13.dme | 1 + 4 files changed, 332 insertions(+), 133 deletions(-) create mode 100644 code/datums/components/ai_leadership.dm diff --git a/code/datums/components/ai_leadership.dm b/code/datums/components/ai_leadership.dm new file mode 100644 index 000000000000..b5105c12298a --- /dev/null +++ b/code/datums/components/ai_leadership.dm @@ -0,0 +1,291 @@ +/* Component for AI leadership. Ai will leader + other mobs of the listed type. */ + +/datum/component/ai_leadership + dupe_mode = COMPONENT_DUPE_UNIQUE + //Cooldowns for recruitment + var/recruit_cooldown = 0 + var/recruit_delay = 2 SECONDS + //Cooldowns for headcount + var/headcount_cooldown = 0 + var/headcount_delay = 25 SECONDS + //Amount of allowed followers + var/unit_amount + //If the team should only have a certain amount of type. + var/unqiue_team + //If followers will return to a base when disbanded + var/return_to_fob + //Datum that tracks all of the components + var/static/datum/collective_management/follower_tracker + //Allowed types of followers + var/list/possible_followers = list() + //This components current followers. + var/list/followers = list() + +/* This could use some improvement. + Or a full on replacement. */ +/datum/component/ai_leadership/Initialize(allowed_types, amount = 6, unique = FALSE, forwardbase = FALSE) + if(!isatom(parent)) + return COMPONENT_INCOMPATIBLE + + if(!follower_tracker) + follower_tracker = new /datum/collective_management() + var/atom/L = parent + + return_to_fob = forwardbase + unqiue_team = unique + unit_amount = amount + possible_followers += allowed_types + + follower_tracker.AddLeader(L) + ScanLocation() + START_PROCESSING(SSdcs, src) + +/datum/component/ai_leadership/RegisterWithParent() + if(isliving(parent)) + RegisterSignal(parent, COMSIG_LIVING_DEATH, .proc/RemoveLeader) + if(ishostile(parent)) + RegisterSignal(parent, COMSIG_PATROL_START, .proc/HeadCount) + +/datum/component/ai_leadership/UnregisterFromParent() + if(isliving(parent)) + UnregisterSignal(parent, COMSIG_LIVING_DEATH) + if(ishostile(parent)) + UnregisterSignal(parent, COMSIG_PATROL_START) + +//On death remove this component and disband their followers. +/datum/component/ai_leadership/proc/RemoveLeader() + SIGNAL_HANDLER + + follower_tracker.RemoveLeader(parent, followers) + STOP_PROCESSING(SSdcs, src) + qdel(src) + +/datum/component/ai_leadership/process() + //If not a hostile then it would not get a signal. + if(!ishostile(parent) && world.time > headcount_cooldown && followers.len) + HeadCount(parent) + if(world.time > recruit_cooldown && followers.len < unit_amount) + ScanLocation() + recruit_cooldown = world.time + recruit_delay + + /*----------------------- + |~ Main Function Procs ~| + -----------------------*/ + +/*Scan proc for leader. Originally this was a proc + in the hostile found since their targeting + system scans everything all the time. */ +/datum/component/ai_leadership/proc/ScanLocation() + for(var/mob/living/L in view(7, parent)) + if(followers.len >= unit_amount) + break + if(Recruitable(L)) + Recruit(L) + +/*Checks the group of creatures to see if anyone is missing. + Anyone missing will be removed from the group so that + another team can recruit them.*/ +/datum/component/ai_leadership/proc/HeadCount(atom/U) + if(world.time < headcount_cooldown || !followers.len) + return + var/turf/fob + var/list/whosehere = list() + followers = uniqueList(followers) + + for(var/mob/living/L in followers) + if(L.stat != DEAD && !L.client && L.z == U.z && get_dist(U, L) < 10) + whosehere += L + + var/list/absent_troops = difflist(followers, whosehere ,1) + if(absent_troops.len) + if(return_to_fob) + //Only run this once. + fob = get_turf(FindForwardBase()) + for(var/mob/living/S in absent_troops) + Disband(S) + if(ishostile(S)) + var/mob/living/simple_animal/hostile/R = S + if(fob && R.stat != DEAD && !R.target) + walk(R, 0) + R.patrol_to(fob) + headcount_cooldown = world.time + headcount_delay + Regroup() + + /*--------------------- + |~ Follower Commands ~| + ---------------------*/ + +//Command follower to follow leader. +/datum/component/ai_leadership/proc/FollowLeader(mob/living/L) + if(ishostile(parent)) + var/mob/living/simple_animal/hostile/H = parent + walk_to(L, parent, 2, H.move_to_delay - 1.5) + if(iscarbon(parent)) + var/mob/living/carbon/H = parent + walk_to(L, parent, 2, H.cached_multiplicative_slowdown - 1.5) + else if(ishostile(L)) + var/mob/living/simple_animal/hostile/R = L + if(!R.target) + walk_to(R, parent, 2, R.move_to_delay) + else + return FALSE + return TRUE + +//Orders all troops to follow the leader. +/datum/component/ai_leadership/proc/Regroup() + for(var/i in followers) + FollowLeader(i) + + /*----------------- + |~ System Checks ~| + -----------------*/ + +/*Determines if the creature is recruitable + based on their current status and faction.*/ +/datum/component/ai_leadership/proc/Recruitable(mob/living/L) + //Are we at maximum followers + if(followers.len >= unit_amount) + return FALSE + //Are they dead? + if(L.stat == DEAD) + return FALSE + //Player controlled, they do not recognize our authroity. + if(L.client) + return FALSE + //If we are a hostile do these checks + if(ishostile(parent)) + var/mob/living/simple_animal/hostile/H = parent + //Are they the same faction as us? + if(!H.faction_check_mob(L)) + return FALSE + //If they currently have a target let them finish up. + if(H.target) + return FALSE + var/recruit_type = CheckUnitType(possible_followers, L) + //Are they a type in our possible follower types? + if(!recruit_type) + return FALSE + //If we only have the command capacity to command more than one of them. + if(unqiue_team) + if(!TeamCheck(L)) + return FALSE + //Are they already a follower of another leader? + if(FollowerLedger(L)) + return FALSE + return TRUE + +//Register recruit with the system +/datum/component/ai_leadership/proc/Recruit(mob/living/L) + followers += L + follower_tracker.RecruitFollower(L) + FollowLeader(L) + +//Releases the recruit from our command +/datum/component/ai_leadership/proc/Disband(mob/living/L) + followers -= L + follower_tracker.DismissFollower(L) + +//Is the recruit already recruited in the system? +/datum/component/ai_leadership/proc/FollowerLedger(mob/living/recruit) + if(follower_tracker.FindRecruit(recruit)) + return TRUE + return FALSE + +//Check the type system +/datum/component/ai_leadership/proc/CheckUnitType(list/type_list, mob/living/unit_type) + for(var/i in type_list) + if(unit_type.type == i) + return i + return null + +//Checks if theres any room for this unit in the team. +/datum/component/ai_leadership/proc/TeamCheck(mob/living/M) + var/many_we_have = CountByTroopType(M) + var/our_capacity = 0 + for(var/i in possible_followers) + if(M.type == i) + our_capacity = possible_followers[i] + break + if(our_capacity <= many_we_have) + return FALSE + return TRUE + +/*Used to be count by type until i realized count by + type includes subtypes when i need a strict type.*/ +/datum/component/ai_leadership/proc/CountByTroopType(mob/living/M) + if(!M) + return 0 + var/counting_dudes = 0 + for(var/mob/living/dude in followers) + if(dude.type == M.type) + counting_dudes++ + return counting_dudes + +//Calculate a base to return to, usually is a department. +/datum/component/ai_leadership/proc/FindForwardBase() + var/mob/living/L = parent + var/turf/second_choice + for(var/turf/T in GLOB.department_centers) + if(T.z != L.z) + continue + second_choice = T + if(istype(get_area(T), /area/department_main/command)) + return T + return second_choice + + /*-----------------------\ + | Group Mind Datum | + \-----------------------*/ + +/datum/collective_management + var/list/leaders = list() + var/list/follower_list = list() + +//Called by component recruit +/datum/collective_management/proc/RecruitFollower(follower) + if(!follower) + return FALSE + LAZYADD(follower_list, follower) + return TRUE + +//Called when a follower is removed from component and list. +/datum/collective_management/proc/DismissFollower(follower) + if(!follower) + return FALSE + CheckLedger() + LAZYREMOVE(follower_list, follower) + return TRUE + +//Registers leader into the system +/datum/collective_management/proc/AddLeader(leader) + if(LAZYFIND(leaders, leader)) + return FALSE + CheckLedger() + LAZYADD(leaders, leader) + return TRUE + +//Removes leader from the ledger +/datum/collective_management/proc/RemoveLeader(leader, followers) + CheckLedger() + LAZYREMOVE(leaders, leader) + for(var/minion in followers) + DismissFollower(minion) + +//Checks to find if recruit is already in the system +/datum/collective_management/proc/FindRecruit(follower) + if(LAZYFIND(follower_list, follower)) + return TRUE + return FALSE + +//Does technical fixes such as remove nulls and duplicates +/datum/collective_management/proc/CheckLedger() + //Remove duplicates. + var/fixed_leader_list = uniqueList(leaders) + var/fixed_follower_list = uniqueList(follower_list) + //Replace list with a sorted fixed version. + leaders = sortNames(fixed_leader_list) + follower_list = sortNames(fixed_follower_list) + //Remove nulls. + listclearnulls(leaders) + listclearnulls(follower_list) diff --git a/code/modules/mob/living/simple_animal/hostile/ordeal/indigo.dm b/code/modules/mob/living/simple_animal/hostile/ordeal/indigo.dm index fd566d5f9b7f..29e255fa233a 100644 --- a/code/modules/mob/living/simple_animal/hostile/ordeal/indigo.dm +++ b/code/modules/mob/living/simple_animal/hostile/ordeal/indigo.dm @@ -65,7 +65,6 @@ attack_sound = 'sound/effects/ordeals/indigo/stab_1.ogg' damage_coeff = list(BRUTE = 1, RED_DAMAGE = 1, WHITE_DAMAGE = 1.5, BLACK_DAMAGE = 0.5, PALE_DAMAGE = 0.8) blood_volume = BLOOD_VOLUME_NORMAL - var/leader //used by indigo dusk to recruit sweepers /mob/living/simple_animal/hostile/ordeal/indigo_noon/Initialize() . = ..() @@ -79,8 +78,7 @@ /mob/living/simple_animal/hostile/ordeal/indigo_noon/LoseAggro() . = ..() - if(leader) - a_intent_change(INTENT_HELP) + a_intent_change(INTENT_HELP) /mob/living/simple_animal/hostile/ordeal/indigo_noon/AttackingTarget() . = ..() @@ -199,28 +197,20 @@ armortype = PALE_DAMAGE damage_coeff = list(BRUTE = 1, RED_DAMAGE = 1.5, WHITE_DAMAGE = 0.7, BLACK_DAMAGE = 0.7, PALE_DAMAGE = 0.5) - -/mob/living/simple_animal/hostile/ordeal/indigo_dusk/Found(atom/A) //every time she finds a sweeper that sweeper is compelled to follow her as family - if(istype(A, /mob/living/simple_animal/hostile/ordeal/indigo_noon) && troops.len < 6) - var/mob/living/simple_animal/hostile/ordeal/indigo_noon/S = A - if(S.stat != DEAD && !S.leader && !S.target && !S.client) //are you dead? do you have a leader? are you currently fighting? Are you a player? - S.Goto(src,S.move_to_delay,1) - S.leader = src - troops += S +/mob/living/simple_animal/hostile/ordeal/indigo_dusk/Initialize(mapload) + ..() + var/units_to_add = list( + /mob/living/simple_animal/hostile/ordeal/indigo_noon = 1, + ) + AddComponent(/datum/component/ai_leadership, units_to_add) /mob/living/simple_animal/hostile/ordeal/indigo_dusk/Aggro() + . = ..() a_intent_change(INTENT_HARM) - ..() - if(order_cooldown < world.time && troops.len) - order_cooldown = world.time + (10 SECONDS) - var/mob/living/simple_animal/hostile/ordeal/overachiver = locate(/mob/living/simple_animal/hostile/ordeal/indigo_noon) in troops - if(overachiver) - overachiver.TemporarySpeedChange(amount = -2, time = 5 SECONDS) /mob/living/simple_animal/hostile/ordeal/indigo_dusk/LoseAggro() . = ..() - if(troops.len) - a_intent_change(INTENT_HELP) //so that they dont get body blocked by their kin outside of combat + a_intent_change(INTENT_HELP) //so that they dont get body blocked by their kin outside of combat /mob/living/simple_animal/hostile/ordeal/indigo_dusk/AttackingTarget() . = ..() @@ -232,31 +222,6 @@ else devour(L) -/mob/living/simple_animal/hostile/ordeal/indigo_dusk/patrol_select() - if(troops.len) - headcount() - for(var/mob/living/simple_animal/hostile/ordeal/indigo_noon/family in troops) - if(family.stat == DEAD || family.client) //if you are dead or are a player your no longer active in the family. - troops -= family - Goto(src , 2, 1) //had to change it to 2 because the 3 "move to delay" leader would keep outrunning the 4 "move to delay" followers - ..() - -/mob/living/simple_animal/hostile/ordeal/indigo_dusk/death() - for(var/mob/living/simple_animal/hostile/ordeal/indigo_noon/S in troops) //The leader can no longer lead their troops into battle. - if(S) - S.leader = null - return ..() - -/mob/living/simple_animal/hostile/ordeal/indigo_dusk/proc/headcount() - var/list/whosehere = list() - for(var/mob/living/simple_animal/hostile/ordeal/indigo_noon/soldier in oview(src, 10)) - whosehere += soldier - var/list/absent_troops = difflist(troops, whosehere ,1) - if(absent_troops.len) - for(var/mob/living/simple_animal/hostile/ordeal/indigo_noon/s in absent_troops) - s.leader = null - troops -= s - /mob/living/simple_animal/hostile/ordeal/indigo_dusk/proc/devour(mob/living/L) if(!L) return FALSE diff --git a/code/modules/mob/living/simple_animal/hostile/ordeal/steel.dm b/code/modules/mob/living/simple_animal/hostile/ordeal/steel.dm index 0aa2accb59cf..619c6392d373 100644 --- a/code/modules/mob/living/simple_animal/hostile/ordeal/steel.dm +++ b/code/modules/mob/living/simple_animal/hostile/ordeal/steel.dm @@ -26,7 +26,6 @@ //similar to a human damage_coeff = list(BRUTE = 1, RED_DAMAGE = 0.8, WHITE_DAMAGE = 1.2, BLACK_DAMAGE = 1, PALE_DAMAGE = 1) butcher_results = list(/obj/item/food/meat/slab/human = 2, /obj/item/food/meat/slab/human/mutant/moth = 1) - var/leader /mob/living/simple_animal/hostile/ordeal/steel_dawn/Initialize() ..() @@ -51,8 +50,6 @@ /mob/living/simple_animal/hostile/ordeal/steel_dawn/LoseAggro() . = ..() a_intent_change(INTENT_HELP) - if(leader) - Goto(leader,move_to_delay,1) //More Mutated Subtype of Dawns, they are fast and hit faster. /mob/living/simple_animal/hostile/ordeal/steel_dawn/steel_noon @@ -206,8 +203,6 @@ vision_range = 12 move_to_delay = 3 ranged = TRUE - retreat_distance = 2 - minimum_distance = 2 damage_coeff = list(BRUTE = 1, RED_DAMAGE = 0.8, WHITE_DAMAGE = 0.8, BLACK_DAMAGE = 1.4, PALE_DAMAGE = 1) can_patrol = TRUE wander = FALSE @@ -216,17 +211,25 @@ possible_a_intents = list(INTENT_HELP, INTENT_DISARM, INTENT_HARM) deathsound = 'sound/voice/hiss5.ogg' butcher_results = list(/obj/item/food/meat/slab/human = 2, /obj/item/food/meat/slab/human/mutant/moth = 1) - var/turf/fob + //Last command issued var/last_command = 0 + //Delay on charge command var/chargecommand_cooldown = 0 - var/screech_cooldown = 0 + var/chargecommand_delay = 1 MINUTES + //Delay on general commands + var/command_cooldown = 0 + var/command_delay = 18 SECONDS + //If this creature can act. var/can_act = TRUE - var/list/troops = list() -/mob/living/simple_animal/hostile/ordeal/steel_dusk/Initialize() +/mob/living/simple_animal/hostile/ordeal/steel_dusk/Initialize(mapload) ..() - if(!fob) - fob = FindForwardBase() + var/list/units_to_add = list( + /mob/living/simple_animal/hostile/ordeal/steel_dawn = 6, + /mob/living/simple_animal/hostile/ordeal/steel_dawn/steel_noon = 2, + /mob/living/simple_animal/hostile/ordeal/steel_dawn/steel_noon/flying = 2 + ) + AddComponent(/datum/component/ai_leadership, units_to_add, 8, TRUE, TRUE) /mob/living/simple_animal/hostile/ordeal/steel_dusk/Life() . = ..() @@ -235,50 +238,26 @@ if(!target) adjustBruteLoss(-6) - //Recruitment code -/mob/living/simple_animal/hostile/ordeal/steel_dusk/Found(atom/A) - if(istype(A, /mob/living/simple_animal/hostile/ordeal/steel_dawn) && troops.len < 8) - var/mob/living/simple_animal/hostile/ordeal/steel_dawn/S = A - if(S.stat != DEAD && !S.leader && !S.target && !S.client && faction_check_mob(S)) //are you dead? do you have a leader? are you currently fighting? Are you a player? Different faction? - S.Goto(src,move_to_delay - 0.2,1) - S.leader = src - troops += S - return - -/mob/living/simple_animal/hostile/ordeal/steel_dusk/death() - for(var/mob/living/simple_animal/hostile/ordeal/steel_dawn/M in troops) - M.leader = null - ..() +/mob/living/simple_animal/hostile/ordeal/steel_dusk/handle_automated_action() + . = ..() + if(command_cooldown < world.time && target && can_act && stat != DEAD) + switch(last_command) + if(1) + Command(2) //always buff defense at start of battle + else + Command(pick(2,3)) /mob/living/simple_animal/hostile/ordeal/steel_dusk/patrol_select() - if(troops.len) - if(prob(25)) - say("Nothin here. Lets move on.") - HeadCount() - var/follower_speed = move_to_delay - 0.2 - for(var/mob/living/simple_animal/hostile/ordeal/steel_dawn/soldier in troops) - if(soldier.stat != DEAD) //second check to make sure the soldier isnt dead. First one is in headcount. - Goto(src , follower_speed, 1) //had to change it to 2 because the 3 "move to delay" leader would keep outrunning the 4 "move to delay" followers - else if(!troops.len) - var/area/forward_base = get_area(fob) - if(!istype(get_area(src), forward_base) && z == fob.z) - patrol_path = get_path_to(src, fob, /turf/proc/Distance_cardinal, 0, 200) - return + if(prob(25)) + say("Nothin here. Lets move on.") ..() /mob/living/simple_animal/hostile/ordeal/steel_dusk/Aggro() . = ..() if(chargecommand_cooldown <= world.time) Command(1) - ranged_cooldown = world.time + (6 SECONDS) - chargecommand_cooldown = world.time + (60 SECONDS) - screech_cooldown = world.time + (10 SECONDS) //prep for screech - if(!troops.len) - retreat_distance = null - minimum_distance = 1 - else if(retreat_distance <= 0) - retreat_distance = initial(retreat_distance) - minimum_distance = initial(minimum_distance) + ranged_cooldown = world.time + (10 SECONDS) + chargecommand_cooldown = world.time + chargecommand_delay a_intent_change(INTENT_HARM) /mob/living/simple_animal/hostile/ordeal/steel_dusk/LoseAggro() @@ -286,21 +265,8 @@ a_intent_change(INTENT_HELP) /mob/living/simple_animal/hostile/ordeal/steel_dusk/OpenFire() - if(can_act) - if(!troops.len && ranged_cooldown <= world.time) //your on your own boss, give em hell. - ManagerScreech() - ranged_cooldown = world.time + (10 SECONDS) - return - else if(screech_cooldown <= world.time) - ManagerScreech() - screech_cooldown = world.time + (15 SECONDS) - ranged_cooldown = world.time + ranged_cooldown_time - return - switch(last_command) - if(1) - Command(2) //always buff defense at start of battle - else - Command(pick(2,3)) + if(can_act && ranged_cooldown <= world.time) + ManagerScreech() ranged_cooldown = world.time + ranged_cooldown_time /mob/living/simple_animal/hostile/ordeal/steel_dusk/Move() @@ -308,7 +274,8 @@ return FALSE return ..() -/mob/living/simple_animal/hostile/ordeal/steel_dusk/proc/Command(manager_order) //used for attacks and commands. could possibly make this a modular spell or ability. +//used for attacks and commands. could possibly make this a modular spell or ability. +/mob/living/simple_animal/hostile/ordeal/steel_dusk/proc/Command(manager_order) playsound(loc, 'sound/effects/ordeals/steel/gcorp_chitter.ogg', 60, TRUE) switch(manager_order) if(1) @@ -331,6 +298,7 @@ if((istype(G, /mob/living/simple_animal/hostile/ordeal/steel_dawn/steel_noon) || istype(G, /mob/living/simple_animal/hostile/ordeal/steel_dawn)) && G.stat != DEAD && !has_status_effect((/datum/status_effect/all_armor_buff || /datum/status_effect/minor_damage_buff))) G.apply_status_effect(/datum/status_effect/minor_damage_buff) last_command = 3 + command_cooldown = world.time + command_delay /mob/living/simple_animal/hostile/ordeal/steel_dusk/proc/ManagerScreech() var/visual_overlay = mutable_appearance('icons/effects/effects.dmi', "blip") @@ -347,29 +315,3 @@ for(var/mob/living/L in oview(10, src)) if(!faction_check_mob(L)) L.apply_damage(120, WHITE_DAMAGE, null, L.run_armor_check(null, WHITE_DAMAGE), spread_damage = TRUE) - -/mob/living/simple_animal/hostile/ordeal/steel_dusk/proc/HeadCount() //determines what soldiers are here and if we need to disband anyone who isnt here. - var/list/whosehere = list() - for(var/mob/living/simple_animal/hostile/ordeal/steel_dawn/soldier in oview(src, 10)) - if(soldier.stat != DEAD || soldier.client) - whosehere += soldier - var/list/absent_troops = difflist(troops, whosehere ,1) - if(absent_troops.len) - for(var/mob/living/simple_animal/hostile/ordeal/s in absent_troops) - var/mob/living/simple_animal/hostile/ordeal/steel_dawn/friend = s - if(friend && friend.stat != DEAD && friend.z == fob.z) - walk(friend, 0) - friend.patrol_to(fob) - friend.leader = null - troops -= s - -//The purpose of this code is to make it so that if a soldier gets lost, in a containment cell or some other part of the facility, they will go to central command and wait for their leader to return. -/mob/living/simple_animal/hostile/ordeal/steel_dusk/proc/FindForwardBase() - var/turf/second_choice - for(var/turf/T in GLOB.department_centers) - if(T.z != z) - continue - second_choice = T - if(istype(get_area(T), /area/department_main/command)) - return T - return second_choice diff --git a/lobotomy-corp13.dme b/lobotomy-corp13.dme index 31f57e4e2233..1bc650bb8b12 100644 --- a/lobotomy-corp13.dme +++ b/lobotomy-corp13.dme @@ -460,6 +460,7 @@ #include "code\datums\brain_damage\split_personality.dm" #include "code\datums\components\_component.dm" #include "code\datums\components\acid.dm" +#include "code\datums\components\ai_leadership.dm" #include "code\datums\components\anti_magic.dm" #include "code\datums\components\aquarium.dm" #include "code\datums\components\areabound.dm"