diff --git a/code/__DEFINES/conflict.dm b/code/__DEFINES/conflict.dm index 17fd8b32c2..24d86e5d49 100644 --- a/code/__DEFINES/conflict.dm +++ b/code/__DEFINES/conflict.dm @@ -3,6 +3,8 @@ #define GRAB_AGGRESSIVE 1 #define GRAB_CARRY 2 #define GRAB_CHOKE 3 +/// Alien-specific grab, somewhat between an aggressive and choke. Limited to AI only for the moment. +#define GRAB_XENO 4 //Ammo defines for gun/projectile related things. diff --git a/code/__DEFINES/pain.dm b/code/__DEFINES/pain.dm index 8c84dd1070..5867eb52cb 100644 --- a/code/__DEFINES/pain.dm +++ b/code/__DEFINES/pain.dm @@ -15,6 +15,11 @@ #define PAIN_REDUCTION_FULL 80 //oxycodone, neuraline // Pain amount supplied by the action + +/// How much pain is dealt continually to the mob being dragged by the AI as well as when initially grabbing. +#define PAIN_XENO_DRAG 10 +/// How much additional pain is dealt when lunging/throat grabbing. +#define PAIN_XENO_GRAB 15 #define PAIN_BONE_BREAK 25 #define PAIN_BONE_BREAK_SPLINTED 15 #define PAIN_DELIMB 40 diff --git a/code/__DEFINES/xeno.dm b/code/__DEFINES/xeno.dm index 7e30291260..fd43b47990 100644 --- a/code/__DEFINES/xeno.dm +++ b/code/__DEFINES/xeno.dm @@ -689,8 +689,9 @@ #define XENO_CASTE_HIVELORD "Hivelord" #define XENO_CASTE_LURKER "Lurker" #define XENO_CASTE_WARRIOR "Warrior" +#define XENO_CASTE_SOLDIER "Soldier" #define XENO_CASTE_SPITTER "Spitter" -#define XENO_T2_CASTES list(XENO_CASTE_BURROWER, XENO_CASTE_CARRIER, XENO_CASTE_HIVELORD, XENO_CASTE_LURKER, XENO_CASTE_WARRIOR, XENO_CASTE_SPITTER) +#define XENO_T2_CASTES list(XENO_CASTE_BURROWER, XENO_CASTE_CARRIER, XENO_CASTE_HIVELORD, XENO_CASTE_LURKER, XENO_CASTE_WARRIOR, XENO_CASTE_SPITTER, XENO_CASTE_SOLDIER) //t3 #define XENO_CASTE_BOILER "Boiler" #define XENO_CASTE_PRAETORIAN "Praetorian" @@ -704,7 +705,7 @@ #define XENO_CASTE_HELLHOUND "Hellhound" #define XENO_SPECIAL_CASTES list(XENO_CASTE_QUEEN, XENO_CASTE_PREDALIEN, XENO_CASTE_HELLHOUND) -#define ALL_XENO_CASTES list(XENO_CASTE_LARVA, XENO_CASTE_PREDALIEN_LARVA, XENO_CASTE_FACEHUGGER, XENO_CASTE_LESSER_DRONE, XENO_CASTE_DRONE, XENO_CASTE_RUNNER, XENO_CASTE_SENTINEL, XENO_CASTE_DEFENDER, XENO_CASTE_BURROWER, XENO_CASTE_CARRIER, XENO_CASTE_HIVELORD, XENO_CASTE_LURKER, XENO_CASTE_WARRIOR, XENO_CASTE_SPITTER, XENO_CASTE_BOILER, XENO_CASTE_PRAETORIAN, XENO_CASTE_CRUSHER, XENO_CASTE_RAVAGER, XENO_CASTE_QUEEN, XENO_CASTE_PREDALIEN, XENO_CASTE_HELLHOUND, XENO_CASTE_KING) +#define ALL_XENO_CASTES list(XENO_CASTE_LARVA, XENO_CASTE_PREDALIEN_LARVA, XENO_CASTE_FACEHUGGER, XENO_CASTE_LESSER_DRONE, XENO_CASTE_DRONE, XENO_CASTE_RUNNER, XENO_CASTE_SENTINEL, XENO_CASTE_DEFENDER, XENO_CASTE_BURROWER, XENO_CASTE_CARRIER, XENO_CASTE_HIVELORD, XENO_CASTE_LURKER, XENO_CASTE_WARRIOR, XENO_CASTE_SPITTER, XENO_CASTE_BOILER, XENO_CASTE_PRAETORIAN, XENO_CASTE_CRUSHER, XENO_CASTE_RAVAGER, XENO_CASTE_QUEEN, XENO_CASTE_PREDALIEN, XENO_CASTE_HELLHOUND, XENO_CASTE_SOLDIER, XENO_CASTE_KING) // Checks if two hives are allied to each other. // PARAMETERS: diff --git a/code/game/jobs/role_authority.dm b/code/game/jobs/role_authority.dm index 859ec0647a..49123d3db9 100644 --- a/code/game/jobs/role_authority.dm +++ b/code/game/jobs/role_authority.dm @@ -744,6 +744,8 @@ I hope it's easier to tell what the heck this proc is even doing, unlike previou M = /mob/living/carbon/xenomorph/predalien if(XENO_CASTE_HELLHOUND) M = /mob/living/carbon/xenomorph/hellhound + if(XENO_CASTE_SOLDIER) + M = /mob/living/carbon/xenomorph/soldieR if(XENO_CASTE_KING) M = /mob/living/carbon/xenomorph/king return M diff --git a/code/game/objects/effects/aliens.dm b/code/game/objects/effects/aliens.dm index 45fbd5d4ba..8753fa7a35 100644 --- a/code/game/objects/effects/aliens.dm +++ b/code/game/objects/effects/aliens.dm @@ -322,6 +322,12 @@ flare_damage = 1875 icon_state = "acid_strong" +//Similar to strong acid, just not quite as strong other than barricade damage. +/obj/effect/xenomorph/acid/spatter + acid_delay = 0.6 + barricade_damage = 100 + flare_damage = 1000 + /obj/effect/xenomorph/acid/Initialize(mapload, atom/target) . = ..() acid_t = target @@ -334,6 +340,21 @@ RegisterSignal(acid_t, COMSIG_PARENT_QDELETING, PROC_REF(cleanup)) START_PROCESSING(SSoldeffects, src) +/obj/effect/xenomorph/acid/spatter/Initialize(mapload, atom/target) + . = ..() + if(!acid_t) + var/obj/structure/barricade/B = locate() in loc + if(B && !B.unacidable) acid_t = B + else + for(var/obj/O in loc) //Find the first thing. + if(O.unacidable || istype(O, /obj/effect)) continue //Not unacidable things or effects. Don't want to melt xenogibs. + acid_t = O + break + if(acid_t) layer = acid_t.layer + else + animate(src, alpha = 0, 1 SECONDS) + QDEL_IN(src, 1 SECONDS) //No point in keeping it if it's not melting anything. + /obj/effect/xenomorph/acid/Destroy() acid_t = null STOP_PROCESSING(SSoldeffects, src) @@ -428,8 +449,11 @@ else for(var/mob/mob in acid_t) mob.forceMove(loc) - qdel(acid_t) - qdel(src) + animate(acid_t, alpha = 0, 1 SECONDS) + QDEL_IN(acid_t, 1 SECONDS) + + animate(src, alpha = 0, 1 SECONDS) + QDEL_IN(src, 1 SECONDS) /obj/effect/xenomorph/boiler_bombard name = "???" diff --git a/code/modules/admin/game_master/game_master.dm b/code/modules/admin/game_master/game_master.dm index 5370701893..330c7ef855 100644 --- a/code/modules/admin/game_master/game_master.dm +++ b/code/modules/admin/game_master/game_master.dm @@ -29,7 +29,7 @@ GLOBAL_VAR_INIT(radio_communication_clarity, 100) // Spawn stuff #define DEFAULT_SPAWN_XENO_STRING XENO_CASTE_DRONE -#define GAME_MASTER_AI_XENOS list(XENO_CASTE_DRONE, XENO_CASTE_RUNNER, XENO_CASTE_LURKER, XENO_CASTE_CRUSHER, XENO_CASTE_FACEHUGGER) +#define GAME_MASTER_AI_XENOS list(XENO_CASTE_DRONE, XENO_CASTE_SOLDIER, XENO_CASTE_RUNNER, XENO_CASTE_LURKER, XENO_CASTE_CRUSHER, XENO_CASTE_FACEHUGGER) #define DEFAULT_SPAWN_HIVE_STRING XENO_HIVE_NORMAL #define DEFAULT_XENO_AMOUNT_TO_SPAWN 1 diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm index f202e0a6ec..2becfa0066 100644 --- a/code/modules/admin/holder2.dm +++ b/code/modules/admin/holder2.dm @@ -98,7 +98,7 @@ you will have to do something like if(client.admin_holder.rights & R_ADMIN) your if(!other) return FALSE if(rights_required && other.admin_holder?.rank) - if(check_client_rights(usr.client, rights_required, show_msg)) + if(check_client_rights(other, rights_required, show_msg)) return TRUE else if(show_msg) to_chat(usr, SPAN_WARNING("You do not have sufficient rights to do that. You require one of the following flags:[rights2text(rights_required," ")].")) diff --git a/code/modules/mob/living/carbon/human/human_helpers.dm b/code/modules/mob/living/carbon/human/human_helpers.dm index cfb81066d2..31e5a7f270 100644 --- a/code/modules/mob/living/carbon/human/human_helpers.dm +++ b/code/modules/mob/living/carbon/human/human_helpers.dm @@ -201,7 +201,7 @@ if(istype(brain)) return TRUE return FALSE - +f /mob/living/carbon/human/has_eyes() var/datum/internal_organ/eyes = LAZYACCESS(internal_organs_by_name, "eyes") if(istype(eyes) && !eyes.cut_away) @@ -210,7 +210,7 @@ /mob/living/carbon/human/is_mob_restrained(check_grab = TRUE) - if(check_grab && pulledby && pulledby.grab_level >= GRAB_AGGRESSIVE) + if(check_grab && pulledby && pulledby.grab_level > GRAB_PASSIVE && pulledby.grab_level != GRAB_XENO) return TRUE if (handcuffed) return TRUE diff --git a/code/modules/mob/living/carbon/human/life/handle_breath.dm b/code/modules/mob/living/carbon/human/life/handle_breath.dm index 7dafdb6e17..6e5f99badd 100644 --- a/code/modules/mob/living/carbon/human/life/handle_breath.dm +++ b/code/modules/mob/living/carbon/human/life/handle_breath.dm @@ -8,7 +8,7 @@ return if(istype(loc, /obj/structure/machinery/cryo_cell)) return - if(species && (species.flags & NO_BREATHE || species.flags & IS_SYNTHETIC)) + if(species && (species.flags & (NO_BREATHE|IS_SYNTHETIC))) return var/list/air_info diff --git a/code/modules/mob/living/carbon/human/life/handle_grabbed.dm b/code/modules/mob/living/carbon/human/life/handle_grabbed.dm index 0c588da7ca..6f8d2526a4 100644 --- a/code/modules/mob/living/carbon/human/life/handle_grabbed.dm +++ b/code/modules/mob/living/carbon/human/life/handle_grabbed.dm @@ -1,15 +1,19 @@ - +// Why is this proc in its own .dm file? /mob/living/carbon/proc/handle_grabbed() if(!pulledby) return - if(pulledby.grab_level >= GRAB_AGGRESSIVE) - drop_held_items() - if(pulledby.grab_level >= GRAB_CHOKE) - apply_damage(3, OXY) - apply_stamina_damage(5) + /// Rewrote this to be a little different, so that the normal effects don't apply to xeno grabs. + switch(pulledby.grab_level) + if(GRAB_AGGRESSIVE to GRAB_CARRY) + drop_held_items() + if(GRAB_CHOKE) + drop_held_items() + apply_damage(3, OXY) + apply_stamina_damage(5) + if(GRAB_XENO) /// Alien grabs inflict pain when the human is not incapacitated, but not the other effects. + /// Ignoring restrained with (TRUE). The grab does *not* restrain by itself. See /mob/living/carbon/human/is_mob_restrained(check_grab = TRUE) + /// This is too allow the human a chance to fight back, with guns or melee. + return is_mob_incapacitated(TRUE) || pain.apply_pain(PAIN_XENO_DRAG) - log_attack("[key_name(pulledby)] choked [key_name(src)] at [get_area_name(src)]") - attack_log += text("\[[time_stamp()]\] was choked by [key_name(pulledby)]") - pulledby.attack_log += text("\[[time_stamp()]\] choked [key_name(src)]") diff --git a/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm b/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm index 10df5ae197..b9e4dca999 100644 --- a/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm +++ b/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm @@ -243,6 +243,8 @@ var/evolving = FALSE // Whether the xeno is in the process of evolving /// The damage dealt by a xeno whenever they take damage near someone var/acid_blood_damage = 15 + /// Does their blood also create an acid object to melt the environment? + var/acid_blood_spatter = FALSE var/nocrit = FALSE var/deselect_timer = 0 // Much like Carbon.last_special is a short tick record to prevent accidental deselects of abilities var/got_evolution_message = FALSE diff --git a/code/modules/mob/living/carbon/xenomorph/ai/movement/base_define.dm b/code/modules/mob/living/carbon/xenomorph/ai/movement/base_define.dm index ef7cae6e23..affb2ab5a6 100644 --- a/code/modules/mob/living/carbon/xenomorph/ai/movement/base_define.dm +++ b/code/modules/mob/living/carbon/xenomorph/ai/movement/base_define.dm @@ -8,6 +8,9 @@ var/home_locate_range = 15 var/turf/home_turf + /// Should the alien try climbing barricades and other structures, if able? + var/do_climb_structures = FALSE + /datum/xeno_ai_movement/New(mob/living/carbon/xenomorph/parent) . = ..() if(!parent) diff --git a/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm b/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm index e87de58155..4724ba9dc8 100644 --- a/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm +++ b/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm @@ -30,6 +30,9 @@ /// Cooldown declaration for delaying finding a new path if no path was found COOLDOWN_DECLARE(no_path_found_cooldown) + var/ai_active_intent = INTENT_HARM + var/target_unconscious = FALSE + /mob/living/carbon/xenomorph/proc/init_movement_handler() return new /datum/xeno_ai_movement(src) @@ -63,14 +66,14 @@ var/datum/component/ai_behavior_override/behavior_override = check_overrides() - if(behavior_override?.process_override_behavior(src, delta_time)) + if(is_mob_incapacitated(TRUE)) ///If they are incapacitated, the rest doesn't matter. + current_path = null return TRUE - if(is_mob_incapacitated(TRUE)) - current_path = null + if(behavior_override?.process_override_behavior(src, delta_time)) return TRUE - if(QDELETED(current_target) || !current_target.ai_check_stat() || get_dist(current_target, src) > ai_range || COOLDOWN_FINISHED(src, forced_retarget_cooldown)) + if(QDELETED(current_target) || !current_target.ai_check_stat(src) || get_dist(current_target, src) > ai_range || COOLDOWN_FINISHED(src, forced_retarget_cooldown)) current_target = get_target(ai_range) COOLDOWN_START(src, forced_retarget_cooldown, forced_retarget_time) if(QDELETED(src)) @@ -81,7 +84,7 @@ if(prob(5)) emote("hiss") - a_intent = INTENT_HARM + a_intent = ai_active_intent if(!current_target) ai_move_idle(delta_time) @@ -133,7 +136,7 @@ return 0 return INFINITY -/atom/proc/ai_check_stat() +/atom/proc/ai_check_stat(mob/living/carbon/xenomorph/X) return TRUE // So we aren't trying to find a new target on attack override // Called whenever an obstacle is encountered but xeno_ai_obstacle returned something else than infinite @@ -267,28 +270,6 @@ #undef EXTRA_CHECK_DISTANCE_MULTIPLIER -/mob/living/carbon/proc/ai_can_target(mob/living/carbon/xenomorph/X) - if(!ai_check_stat(X)) - return FALSE - - if(X.can_not_harm(src)) - return FALSE - - if(alpha <= 45 && get_dist(X, src) > 2) - return FALSE - - if(isfacehugger(X)) - if(status_flags & XENO_HOST) - return FALSE - - if(istype(wear_mask, /obj/item/clothing/mask/facehugger)) - return FALSE - - else if(HAS_TRAIT(src, TRAIT_NESTED)) - return FALSE - - return TRUE - /mob/living/carbon/xenomorph/proc/make_ai() SHOULD_CALL_PARENT(TRUE) create_hud() @@ -343,3 +324,7 @@ if(cycled_turf.x == min_x_value) min_x_turfs += cycled_turf return min_x_turfs + +/// Override as necessary to check for more specific triggers for an ability activation. +/mob/living/carbon/xenomorph/proc/check_additional_ai_activation() + return TRUE diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Runner.dm b/code/modules/mob/living/carbon/xenomorph/castes/Runner.dm index 2404823fad..0c31e59d5d 100644 --- a/code/modules/mob/living/carbon/xenomorph/castes/Runner.dm +++ b/code/modules/mob/living/carbon/xenomorph/castes/Runner.dm @@ -106,7 +106,7 @@ add_temp_negative_pass_flags(PASS_FLAGS_CRAWLER) -/mob/living/carbon/xenomorph/runner/stop_pulling() +/mob/living/carbon/xenomorph/runner/stop_pulling(bumped_movement = FALSE) . = ..() remove_temp_negative_pass_flags(PASS_FLAGS_CRAWLER) diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Soldier.dm b/code/modules/mob/living/carbon/xenomorph/castes/Soldier.dm new file mode 100644 index 0000000000..2b8b856ae7 --- /dev/null +++ b/code/modules/mob/living/carbon/xenomorph/castes/Soldier.dm @@ -0,0 +1,281 @@ +/datum/caste_datum/soldier + caste_type = XENO_CASTE_SOLDIER + tier = 2 + melee_damage_lower = XENO_DAMAGE_TIER_3 + melee_damage_upper = XENO_DAMAGE_TIER_4 + melee_vehicle_damage = XENO_DAMAGE_TIER_3 + max_health = XENO_HEALTH_TIER_2 + plasma_gain = XENO_PLASMA_GAIN_TIER_8 + plasma_max = XENO_PLASMA_TIER_10 + crystal_max = XENO_CRYSTAL_LOW + xeno_explosion_resistance = XENO_EXPLOSIVE_ARMOR_TIER_1 + armor_deflection = XENO_ARMOR_TIER_1 + evasion = XENO_EVASION_MEDIUM + speed = XENO_SPEED_HELLHOUND //Faster than drones, slower than lurkers. + + caste_desc = "A warrior of the hive." + evolves_to = list(XENO_CASTE_QUEEN, XENO_CASTE_BURROWER, XENO_CASTE_CARRIER, XENO_CASTE_HIVELORD) //Add more here separated by commas + deevolves_to = list("Drone") + + tackle_min = 2 + tackle_max = 4 + + minimap_icon = "warrior" + +/mob/living/carbon/xenomorph/soldier + caste_type = XENO_CASTE_SOLDIER + name = XENO_CASTE_SOLDIER + desc = "An alien warrior." + icon = 'icons/mob/xenos/soldier.dmi' + icon_size = 48 + icon_state = "Soldier Walking" + plasma_types = list(PLASMA_PURPLE) + tier = 2 + pixel_x = -12 + old_x = -12 + + gib_chance = 100 + claw_type = CLAW_TYPE_SHARP + pull_multiplier = 0.2 /// Pretty much no pull delay, for those quick drags. + + acid_blood_damage = 35 /// Strong acid blood. Should be a define in the future. + acid_blood_spatter = TRUE /// Testing variable, means that their blood can melt objects in the environment. Primarily barricades. + + mutation_type = SOLDIER_NORMAL + icon_xeno = 'icons/mob/xenos/soldier.dmi' + icon_xenonid = 'icons/mob/xenonids/drone.dmi' + + target_unconscious = TRUE + +/* + * ==========================================================================| + * BASE DEFINES AND PROCS + * --------------------------------------------------------------------------| + * ==========================================================================| +*/ + +/// The aggression the alien starts with by default. +#define AGGRESSION_MINIMUM 0 +/// The maximum aggression it is possible to accumulate. +#define AGGRESSION_MAXIMUM 100 +/// Aggression required to go full-on lethals instead of capture. +#define AGGRESSION_ENABLE_LETHAL 50 +/// If there is no hive to drag people to, this is the aggression the alien starts with. +#define AGGRESSION_NO_HIVE AGGRESSION_ENABLE_LETHAL +/// The cap on aggression increase when something significant happens, like taking damage. +#define AGGRESSION_INCREMENT_CAP 10 + +/// Threshold for lunging at a target. Low as the alien wants to do this early. +#define AGGRESSION_LUNGE 10 +/// Threshold for throwing humans around. +#define AGGRESSION_FLING 35 +/// Threshold for stabbing them with the tail. +#define AGGRESSION_TAIL_STAB 70 +/// Threshold for headbiting/instakilling a downed human. +#define AGGRESSION_HEADBITE 90 + + /// AI behavior variables should probably go into their own datum. I thought about placing this into movement handler, but these govern general behavior. + var/aggression_cur = AGGRESSION_MINIMUM ///How angry this alien is. Higher aggression means more lethal options are open. + var/aggression_min = AGGRESSION_MINIMUM ///Aggression cannot fall below this value. + +/mob/living/carbon/xenomorph/soldier/Initialize() + base_actions = list( + /datum/action/xeno_action/onclick/xeno_resting, + /datum/action/xeno_action/onclick/regurgitate, + /datum/action/xeno_action/watch_xeno, + /datum/action/xeno_action/activable/tail_stab/soldier, + /datum/action/xeno_action/activable/fling/soldier, + /datum/action/xeno_action/activable/lunge/soldier, + /datum/action/xeno_action/activable/headbite/soldier, + /datum/action/xeno_action/onclick/tacmap, + ) + inherent_verbs = list( + /mob/living/carbon/xenomorph/proc/vent_crawl, + ) + + . = ..() + + if(!length(GLOB.ai_hives)) ///We check for this first. If there is a hive, we want to make sure the aliens drag people there. + aggression_cur = AGGRESSION_NO_HIVE ///If we do not have a hive, bump up aggression to go lethal instead. + aggression_min = AGGRESSION_NO_HIVE + + /// This is so stupid. Mutators are set inline with new, and are not created on Initialize(). However, pull_multiplier is then overriden by the personal mutator pull_multiplier on Initialize() with recalculate_actions(). + mutators.pull_multiplier = initial(pull_multiplier) + pull_multiplier = mutators.pull_multiplier + +/mob/living/carbon/xenomorph/soldier/apply_damage(damage = 0, damagetype = BRUTE, def_zone = null, used_weapon = null, sharp = 0, edge = 0, force = FALSE) + . = ..() //It will take the previous arguments. + + if(.) + /// Fire gives the largest aggression increase. + aggression_cur = min(AGGRESSION_MAXIMUM, aggression_cur + min(AGGRESSION_INCREMENT_CAP, damage * (damagetype == BURN ? 0.2 : 0.13) ) ) /// Multiplying is faster than dividing. Go figure. + +/* + * ==========================================================================| + * MOVEMENT LOOPS AND BEHAVIOR LOGIC + * --------------------------------------------------------------------------| + * ==========================================================================| +*/ + +/mob/living/carbon/xenomorph/soldier/process_ai(delta_time) + var/mob/living/pulling_target = pulling /// Let's see if the alien is pulling anyone. + var/mob/living/potential_target = current_target + + if(istype(pulling_target)) /// Our soldier is pulling someone. + if(get_active_hand()) swap_hand() /// Swap hand to either tackle or harm. + ai_active_intent = INTENT_DISARM /// If we are pulling someone and are not too aggressive, switch to disarm. + if(prob(5)) emote("tail") + + else if(istype(potential_target)) /// We have a target. We'll do more thorough checking in the main loop, for now we only need to know if they are being pulled by a hostile or friendly xeno. + var/mob/living/carbon/xenomorph/other_xenomorph = potential_target.pulledby /// Are they being pulled by an alien? + /// Need to make sure the alien dragging is friendly to us. If it is not friendly, or not a xeno, our alien will try to grab back. + ai_active_intent = (istype(other_xenomorph) && IS_SAME_HIVENUMBER(src, other_xenomorph)) ? INTENT_DISARM : INTENT_GRAB + + /// I had it set up for slightly faster assignment, but this is easier to read. + ai_active_intent = (aggression_cur >= AGGRESSION_ENABLE_LETHAL) ? INTENT_HARM : ai_active_intent /// Override harm or continue with the previous intent. + + return ..() + +/datum/xeno_ai_movement/assault + do_climb_structures = TRUE /// The alien will climb structures, if able. + +/mob/living/carbon/xenomorph/soldier/init_movement_handler() + return new /datum/xeno_ai_movement/assault(src) + +/mob/living/carbon/xenomorph/soldier/ai_move_idle(delta_time) + if(!ai_movement_handler) + CRASH("No valid movement handler for [src]!") + + var/mob/living/pulling_target = pulling + return (istype(pulling_target) && length(GLOB.ai_hives)) ? ai_movement_handler.ai_move_hive(delta_time) : ai_movement_handler.ai_move_idle(delta_time) + +/mob/living/carbon/xenomorph/soldier/ai_move_target(delta_time) + if(!ai_movement_handler) + CRASH("No valid movement handler for [src]!") + + var/mob/living/pulling_target = pulling + return (istype(pulling_target) && length(GLOB.ai_hives)) ? ai_movement_handler.ai_move_hive(delta_time) : ai_movement_handler.ai_move_target(delta_time) + +/datum/xeno_ai_movement/assault/ai_move_target(delta_time) + var/mob/living/carbon/xenomorph/soldier/current_parent = parent + + /// Moving toward target grows aggression. + current_parent.aggression_cur = min(AGGRESSION_MAXIMUM, current_parent.aggression_cur + 0.1) ///Fractions for a more gradual aggression gain/loss. + return ..() + +/datum/xeno_ai_movement/assault/ai_move_idle(delta_time) + var/mob/living/carbon/xenomorph/soldier/current_parent = parent + + /// Being idle drains aggression. + current_parent.aggression_cur = max(current_parent.aggression_min, current_parent.aggression_cur - 0.1) + return ..() + +/datum/xeno_ai_movement/assault/ai_move_hive(delta_time) + . = ..() + + /// This will mean if the alien is pulling someone to the hive, they will keep tackling them with their free claw. + if(. && parent.pulling && DT_PROB(XENO_SLASH, delta_time)) INVOKE_ASYNC(parent, TYPE_PROC_REF(/mob, do_click), parent.pulling, "", list()) + +/datum/xeno_ai_movement/assault/ai_strap_host(turf/closest_hive, hive_radius, delta_time) + /// Want to make sure when nesting, they actually have the grab active in their main claw. + /// This can lead to some funny behavior of the alien standing around with the victim next to them, but it should be fine for the moment. + /// Something to address later perhaps. + if(parent.pulling && parent.get_active_hand()) parent.swap_hand() + return ..() + +/* + * ==========================================================================| + * SPECIAL ACTIONS + * --------------------------------------------------------------------------| + * ==========================================================================| +*/ + +/mob/living/carbon/xenomorph/soldier/check_additional_ai_activation(activation_threshold) + return (aggression_cur >= activation_threshold) + +/datum/action/xeno_action/activable/tail_stab/soldier /// Specific to soldiers, so that not all xenos get it. + default_ai_action = TRUE + ai_prob_chance = 65 //So they are not spamming it quite as often. + charge_time = null /// AI soldiers can just use this whenever instead of having to charge it up. + xeno_cooldown = 11 SECONDS + +/datum/action/xeno_action/activable/tail_stab/process_ai(mob/living/carbon/xenomorph/parent, delta_time) + /// Short-circuit. Will return the last thing checked or FALSE if it fails at any step. + /// We do not need to check for distance here as the tailstab itself will do that; that distance being 2. + return parent.check_additional_ai_activation(AGGRESSION_TAIL_STAB) && DT_PROB(ai_prob_chance, delta_time) && use_ability_async(parent.current_target) + +/datum/action/xeno_action/activable/fling/soldier + default_ai_action = TRUE + ai_prob_chance = 70 + xeno_cooldown = 8 SECONDS + +/datum/action/xeno_action/activable/fling/process_ai(mob/living/carbon/xenomorph/parent, delta_time) + /// We have a home turf to fling to. + if(parent.check_additional_ai_activation(AGGRESSION_FLING) && DT_PROB(ai_prob_chance, delta_time)) + parent.dir = parent.ai_movement_handler.home_turf ? get_dir(parent, parent.ai_movement_handler.home_turf) : pick(NORTH, SOUTH, EAST, WEST) /// Pick at random if there is no valid direction. + use_ability_async(parent.current_target) + +/datum/action/xeno_action/activable/lunge/soldier + default_ai_action = TRUE + grab_range = 2 + ai_prob_chance = 90 // Want to do this often, as it's their way of saying hello. + xeno_cooldown = 10 SECONDS + +/datum/action/xeno_action/activable/lunge/process_ai(mob/living/carbon/xenomorph/parent, delta_time) + /// Want to make sure no obstacles are in the way so that the alien is not lunging for no reason, or bonking into barricades like an idiot. + /// Maybe in the future the actual lunge can be stripped down for the AI only? + if( parent.check_additional_ai_activation(AGGRESSION_LUNGE) && DT_PROB(ai_prob_chance, delta_time) && get_dist(parent, parent.current_target) == grab_range ) + /// get_step_to() should return the turf nearest the target if successful, with no obstacles to block movement there with the lunge. + var/turf/T = get_step_to(parent, parent.current_target) + return T?.AdjacentQuick(parent.current_target.loc) && use_ability_async(parent.current_target) + +/// Override for soldier lunges. Similar to the woyer grab, but this one only inflicts pain without stuns and has a shorter range. +/mob/living/carbon/xenomorph/soldier/start_pulling(atom/movable/AM, lunge) + if (!check_state() || agility || !isliving(AM)) return FALSE + + var/mob/living/L = AM + var/should_neckgrab = !can_not_harm(L) && lunge + + if(!QDELETED(L) && !QDELETED(L.pulledby) && L != src ) //override pull of other mobs + visible_message(SPAN_WARNING("[src] has broken [L.pulledby]'s grip on [L]!"), null, null, 5) + L.pulledby.stop_pulling() + + . = ..(L, lunge, should_neckgrab) + + if(.) + var/pain_to_cause = PAIN_XENO_DRAG /// Basic amount of pain caused with each grab. + if(should_neckgrab && L.mob_size < MOB_SIZE_BIG) + pain_to_cause += PAIN_XENO_GRAB /// Neck grabs cause even more pain. + L.pulledby = src + visible_message(SPAN_XENOWARNING("\The [src] grabs [L] by the throat!"), \ + SPAN_XENOWARNING("You grab [L] by the throat!")) + + L.pain.apply_pain(pain_to_cause) + grab_level = GRAB_XENO /// Alien-specific grab level, with its own logic for escaping. AI only for the moment. See /mob/living/resist_grab() + if(prob(10)) emote("growl") + /// The actual pain processing for humans is handled in: /mob/living/carbon/proc/handle_grabbed() Other mobs don't process the effects of the grab, like other xenomorphs. + +/mob/living/carbon/xenomorph/soldier/stop_pulling(bumped_movement = FALSE) + //Let's see if we can ignore this. If our direction is the same as where the mob went, we likely bumped into them. So we lasso them back. + if(bumped_movement && grab_level == GRAB_XENO && get_dir(src, pulling) == dir) /// Only on xeno grabs, so if they are on GRAB_PASSIVE, this doesn't trigger. + pulling.loc = get_step_towards(src, pulling) // GET OVER HERE! + else return ..() + +/datum/action/xeno_action/activable/headbite/soldier + default_ai_action = TRUE + ai_prob_chance = 95 // Absolutely DEAD if the alien is angry enough. + xeno_cooldown = 30 SECONDS // Don't want to chain these, as unlikely as that could be. + +/datum/action/xeno_action/activable/headbite/soldier/process_ai(mob/living/carbon/xenomorph/parent, delta_time) + return parent.check_additional_ai_activation(AGGRESSION_HEADBITE) && DT_PROB(ai_prob_chance, delta_time) && use_ability_async(parent.current_target) + +#undef AGGRESSION_MINIMUM +#undef AGGRESSION_MAXIMUM +#undef AGGRESSION_INCREMENT_CAP +#undef AGGRESSION_ENABLE_LETHAL +#undef AGGRESSION_NO_HIVE + +#undef AGGRESSION_LUNGE +#undef AGGRESSION_FLING +#undef AGGRESSION_TAIL_STAB +#undef AGGRESSION_HEADBITE diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Warrior.dm b/code/modules/mob/living/carbon/xenomorph/castes/Warrior.dm index 2885dd6ac5..7db17cf54e 100644 --- a/code/modules/mob/living/carbon/xenomorph/castes/Warrior.dm +++ b/code/modules/mob/living/carbon/xenomorph/castes/Warrior.dm @@ -69,7 +69,7 @@ /mob/living/carbon/xenomorph/warrior/throw_item(atom/target) toggle_throw_mode(THROW_MODE_OFF) -/mob/living/carbon/xenomorph/warrior/stop_pulling() +/mob/living/carbon/xenomorph/warrior/stop_pulling(bumped_movement = FALSE) if(isliving(pulling) && lunging) lunging = FALSE // To avoid extreme cases of stopping a lunge then quickly pulling and stopping to pull someone else var/mob/living/lunged = pulling diff --git a/code/modules/mob/living/carbon/xenomorph/damage_procs.dm b/code/modules/mob/living/carbon/xenomorph/damage_procs.dm index 9d4de90072..ad01de1fb6 100644 --- a/code/modules/mob/living/carbon/xenomorph/damage_procs.dm +++ b/code/modules/mob/living/carbon/xenomorph/damage_procs.dm @@ -292,6 +292,11 @@ playsound(victim, "acid_sizzle", 25, TRUE) animation_flash_color(victim, "#FF0000") //pain hit flicker + if(damtype == BRUTE && acid_blood_spatter && prob(splash_chance)) /// Only brute damage makes sense for the spatter. + var/turf/acid_loc = pick(get_step(src,rand(0,8))) + if(acid_loc && !(locate(/obj/effect/xenomorph/acid) in acid_loc)) + new /obj/effect/xenomorph/acid/spatter(acid_loc) ///Don't want to double up. It will target barricades first. + /mob/living/carbon/xenomorph/get_target_lock(access_to_check) if(isnull(access_to_check)) return diff --git a/code/modules/mob/living/carbon/xenomorph/xeno_ai_interaction.dm b/code/modules/mob/living/carbon/xenomorph/xeno_ai_interaction.dm index 9627ac60c0..599faa5c27 100644 --- a/code/modules/mob/living/carbon/xenomorph/xeno_ai_interaction.dm +++ b/code/modules/mob/living/carbon/xenomorph/xeno_ai_interaction.dm @@ -140,6 +140,30 @@ At bare minimum, make sure the relevant checks from parent types gets copied in /mob/living/ai_check_stat(mob/living/carbon/xenomorph/X) return stat == CONSCIOUS && !(locate(/datum/effects/crit) in effects_list) +///////////////////////////// +// CARBON // +///////////////////////////// +/mob/living/carbon/proc/ai_can_target(mob/living/carbon/xenomorph/X) + if(!ai_check_stat(X)) + return FALSE + + if(X.can_not_harm(src)) + return FALSE + + if(alpha <= 45 && get_dist(X, src) > 2) + return FALSE + + if(isfacehugger(X)) + if(status_flags & XENO_HOST) + return FALSE + + if(istype(wear_mask, /obj/item/clothing/mask/facehugger)) + return FALSE + + else if(HAS_TRAIT(src, TRAIT_NESTED)) + return FALSE + + return TRUE ///////////////////////////// // HUMANS // @@ -161,14 +185,10 @@ At bare minimum, make sure the relevant checks from parent types gets copied in /mob/living/carbon/human/ai_can_target(mob/living/carbon/xenomorph/X) . = ..() - if(!.) - return FALSE if(species.flags & IS_SYNTHETIC) return FALSE - return TRUE - /mob/living/carbon/human/ai_check_stat(mob/living/carbon/xenomorph/X) . = ..() if(isfacehugger(X)) @@ -232,6 +252,16 @@ At bare minimum, make sure the relevant checks from parent types gets copied in return SENTRY_PENALTY +///////////////////////////// +// STRUCTURE // +///////////////////////////// +/// Allows this xenomorph to climb most structures that can be climbed, if they are capable of it. +/obj/structure/xeno_ai_act(mob/living/carbon/xenomorph/X) + if(X.ai_movement_handler.do_climb_structures && can_climb(X)) + do_climb(X) + else + X.do_click(src, "", list()) + return TRUE ///////////////////////////// // WINDOW FRAME // diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 27697a8938..5842f614b8 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -177,34 +177,34 @@ var/pull_dir = get_dir(src, pulling) - if(grab_level >= GRAB_CARRY) - switch(grab_level) - if(GRAB_CARRY) - var/direction_to_face = EAST - - if(direct & WEST) - direction_to_face = WEST - - pulling.Move(NewLoc, direction_to_face) - var/mob/living/pmob = pulling - if(istype(pmob)) - SEND_SIGNAL(pmob, COMSIG_MOB_MOVE_OR_LOOK, TRUE, direction_to_face, direction_to_face) - else - pulling.Move(NewLoc, direct) - else if(get_dist(src, pulling) > 1 || ((pull_dir - 1) & pull_dir)) //puller and pullee more than one tile away or in diagonal position - var/pulling_dir = get_dir(pulling, T) - pulling.Move(T, pulling_dir) //the pullee tries to reach our previous position - if(pulling && get_dist(src, pulling) > 1) //the pullee couldn't keep up - stop_pulling() - else + switch(grab_level) + if(GRAB_CARRY) + var/direction_to_face = EAST + + if(direct & WEST) + direction_to_face = WEST + + pulling.Move(NewLoc, direction_to_face) var/mob/living/pmob = pulling if(istype(pmob)) - SEND_SIGNAL(pmob, COMSIG_MOB_MOVE_OR_LOOK, TRUE, pulling_dir, pulling_dir) - if(!(flags_atom & DIRLOCK)) - setDir(turn(direct, 180)) //face the pullee + SEND_SIGNAL(pmob, COMSIG_MOB_MOVE_OR_LOOK, TRUE, direction_to_face, direction_to_face) + if(GRAB_CHOKE) + pulling.Move(NewLoc, direct) + else + if(get_dist(src, pulling) > 1 || ((pull_dir - 1) & pull_dir)) //puller and pullee more than one tile away or in diagonal position + var/pulling_dir = get_dir(pulling, T) + pulling.Move(T, pulling_dir) //the pullee tries to reach our previous position + if(pulling && get_dist(src, pulling) > 1) //the pullee couldn't keep up + stop_pulling() + else + var/mob/living/pmob = pulling + if(istype(pmob)) + SEND_SIGNAL(pmob, COMSIG_MOB_MOVE_OR_LOOK, TRUE, pulling_dir, pulling_dir) + if(!(flags_atom & DIRLOCK)) + setDir(turn(direct, 180)) //face the pullee if(pulledby && get_dist(src, pulledby) > 1)//separated from our puller and not in the middle of a diagonal move. - pulledby.stop_pulling() + pulledby.stop_pulling(TRUE) /// Mob was likely bumped by the pulledby. Can lead to additional checking, if we don't want the victim to get out of jail for free. if (s_active && !( s_active in contents ) && get_turf(s_active) != get_turf(src)) //check !( s_active in contents ) first so we hopefully don't have to call get_turf() so much. s_active.storage_close(src) @@ -216,42 +216,48 @@ if(back && (back.flags_item & ITEM_OVERRIDE_NORTHFACE)) update_inv_back() - - /mob/proc/resist_grab(moving_resist) return //returning 1 means we successfully broke free /mob/living/resist_grab(moving_resist) if(!pulledby) return - // vars for checks of strengh - var/pulledby_is_strong = HAS_TRAIT(pulledby, TRAIT_SUPER_STRONG) - var/src_is_strong = HAS_TRAIT(src, TRAIT_SUPER_STRONG) - if(!pulledby.grab_level && (!pulledby_is_strong || src_is_strong)) // if passive grab, check if puller is stronger than src, and if not, break free - pulledby.stop_pulling() - return TRUE - - // Chance for person to break free of grip, defaults to 50. - var/chance = 50 - if(src_is_strong && !isxeno(pulledby)) // no extra chance to resist warrior grabs - chance += 30 // you are strong, you can overpower them easier - if(pulledby_is_strong) - chance -= 30 // stronger grip - // above code means that if you are super strong, 80% chance to resist, otherwise, 20 percent. if both are super strong, standard 50. - - if(prob(chance)) - playsound(loc, 'sound/weapons/thudswoosh.ogg', 25, 1, 7) - visible_message(SPAN_DANGER("[src] has broken free of [pulledby]'s grip!"), max_distance = 5) - pulledby.stop_pulling() - return TRUE - if(moving_resist && client) //we resisted by trying to move - visible_message(SPAN_DANGER("[src] struggles to break free of [pulledby]'s grip!"), max_distance = 5) - // +1 delay if super strong, also done as passive grabs would have a modifier of 0 otherwise, causing spam - if(pulledby_is_strong && !src_is_strong) - client.next_movement = world.time + (10*(pulledby.grab_level + 1)) + client.move_delay + switch(pulledby.grab_level) + if(GRAB_AGGRESSIVE to GRAB_CHOKE) + /// Probably could use some refactoring for the CQC skill, traits, and the like. + if(prob(50)) + playsound(src.loc, 'sound/weapons/thudswoosh.ogg', 25, 1, 7) + visible_message(SPAN_DANGER("[src] has broken free of [pulledby]'s grip!"), null, null, 5) + pulledby.stop_pulling() + return TRUE + if(moving_resist && client) //we resisted by trying to move + visible_message(SPAN_DANGER("[src] struggles to break free of [pulledby]'s grip!"), null, null, 5) + client.next_movement = world.time + (10*pulledby.grab_level) + client.move_delay + if(GRAB_XENO) /// Specific to some xenomorphs. + if(HAS_TRAIT(src, TRAIT_SUPER_STRONG)) /// Superstrength means you get out of jail for free. + . = TRUE + else + /// If the living mob is skilled in CQC, they will get a bonus to escape the grab, 7% per skill level, or 35% if maxed out. + var/skill_bonus = (skills?.get_skill_level(SKILL_CQC)) * 7 + /// Then we need to determine if they loosen the grip or escape outright, or struggle in vain. + if(prob((65 - SKILL_CQC_MAX * 7) + skill_bonus)) /// At most a 65% chance to escape outright. + . = TRUE + else + if(prob((100 - SKILL_CQC_MAX * 7) + skill_bonus)) /// Maximum 100% to loosen the grip. + visible_message(SPAN_DANGER("[src] loosens [pulledby]'s grip!"), null, null, 5) + pulledby.grab_level = GRAB_PASSIVE /// Loosens the grab into a passive grab, being able to escape on the next go. + else + visible_message(SPAN_DANGER("[src] struggles to break free of [pulledby]'s grip!"), null, null, 5) + client.next_movement = world.time + (10 * GRAB_XENO) + client.move_delay /// Grab level may get reset, so we want to keep it at a good delay. + + if(.) /// Our living mob successfully broke away. + playsound(src.loc, 'sound/weapons/thudswoosh.ogg', 25, 1, 7) + visible_message(SPAN_DANGER("[src] has broken free of [pulledby]'s grip!"), null, null, 5) + pulledby.stop_pulling() else - client.next_movement = world.time + (10*pulledby.grab_level) + client.move_delay + pulledby.stop_pulling() + return TRUE /mob/living/movement_delay() . = ..() diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 0c9b34bbb8..9a86b4855e 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -485,7 +485,7 @@ return do_pull(AM, lunge, no_msg) -/mob/proc/stop_pulling() +/mob/proc/stop_pulling(bumped_movement = FALSE) if(!pulling) return diff --git a/code/modules/mob/mob_grab.dm b/code/modules/mob/mob_grab.dm index c326cc81f6..e3d56c4751 100644 --- a/code/modules/mob/mob_grab.dm +++ b/code/modules/mob/mob_grab.dm @@ -24,8 +24,11 @@ /obj/item/grab/Destroy() grabbed_thing = null - if(ismob(loc)) - var/mob/M = loc + var/mob/M = loc + if(istype(M)) + /// If a mob is qdeleted while grabbing, the stack will call for stop_pulling(), which will call for this item to be deleted on drop. + /// But it is already being deleted through previous procs in the stack; so we don't want to call the function again when it is dropped. + if(M.gc_destroyed == GC_CURRENTLY_BEING_QDELETED) flags_item &= ~DELONDROP M.grab_level = 0 M.stop_pulling() . = ..() diff --git a/colonialmarines.dme b/colonialmarines.dme index 2acf1dec00..af1730fa65 100644 --- a/colonialmarines.dme +++ b/colonialmarines.dme @@ -2127,6 +2127,7 @@ #include "code\modules\mob\living\carbon\xenomorph\castes\Ravager.dm" #include "code\modules\mob\living\carbon\xenomorph\castes\Runner.dm" #include "code\modules\mob\living\carbon\xenomorph\castes\Sentinel.dm" +#include "code\modules\mob\living\carbon\xenomorph\castes\Soldier.dm" #include "code\modules\mob\living\carbon\xenomorph\castes\Spitter.dm" #include "code\modules\mob\living\carbon\xenomorph\castes\Warrior.dm" #include "code\modules\mob\living\carbon\xenomorph\items\iff_tag.dm" diff --git a/icons/mob/xenos/soldier.dmi b/icons/mob/xenos/soldier.dmi new file mode 100644 index 0000000000..b61fab7b72 Binary files /dev/null and b/icons/mob/xenos/soldier.dmi differ diff --git a/icons/mob/xenos/wounds.dmi b/icons/mob/xenos/wounds.dmi index 2501196f75..7b47a9fbfa 100644 Binary files a/icons/mob/xenos/wounds.dmi and b/icons/mob/xenos/wounds.dmi differ