diff --git a/code/__DEFINES/armor.dm b/code/__DEFINES/armor.dm index 4d7ccaeb285..2acd96b17c9 100644 --- a/code/__DEFINES/armor.dm +++ b/code/__DEFINES/armor.dm @@ -369,6 +369,25 @@ "wound" = 0, \ "damage_threshold" = 11) +/* Robot Heavy Milbot (Boss-tier) + * High DR across the board + * Still weak to lasers (sensors and shit) + * Very resistant to bullets (thick armor plating) + * Very high DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_ROBOT_MILITARY_HEAVY list(\ + "melee" = 25, \ + "bullet" = 45, \ + "laser" = -15, \ + "energy" = -15, \ + "bomb" = 10, \ + "bio" = 100, \ + "rad" = 100, \ + "fire" = 20, \ + "acid" = 20, \ + "wound" = 0, \ + "damage_threshold" = 15) + /* Deathclaw Commonboy * Just about impervious to bullets * Melee is... okay ish @@ -664,6 +683,452 @@ "wound" = 5, \ "damage_threshold" = 3) +/* Mirelurk armor + * Hard shell - resistant to bullets and melee + * Weak to energy weapons + * Water creature - good vs bio/rad + * Medium DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_MIRELURK list(\ + "melee" = 35, \ + "bullet" = 40, \ + "laser" = 10, \ + "energy" = 10, \ + "bomb" = 15, \ + "bio" = 100, \ + "rad" = 100, \ + "fire" = 5, \ + "acid" = 50, \ + "wound" = 10, \ + "damage_threshold" = 8) + +/* Mirelurk Hunter armor + * Tougher shell - even better defense + * Higher DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_MIRELURK_HUNTER list(\ + "melee" = 45, \ + "bullet" = 50, \ + "laser" = 15, \ + "energy" = 15, \ + "bomb" = 20, \ + "bio" = 100, \ + "rad" = 100, \ + "fire" = 10, \ + "acid" = 60, \ + "wound" = 15, \ + "damage_threshold" = 12) + +/* Mirelurk Baby armor + * Soft shell - much weaker + * Low DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_MIRELURK_BABY list(\ + "melee" = 15, \ + "bullet" = 20, \ + "laser" = 5, \ + "energy" = 5, \ + "bomb" = 5, \ + "bio" = 100, \ + "rad" = 100, \ + "fire" = 0, \ + "acid" = 25, \ + "wound" = 5, \ + "damage_threshold" = 3) + +/* Mirelurk Queen armor + * Massive armored carapace + * Very high DT + * Boss-tier + * * * * * * * * * * * */ +#define ARMOR_VALUE_MIRELURK_QUEEN list(\ + "melee" = 60, \ + "bullet" = 65, \ + "laser" = 25, \ + "energy" = 25, \ + "bomb" = 35, \ + "bio" = 100, \ + "rad" = 100, \ + "fire" = 20, \ + "acid" = 80, \ + "wound" = 25, \ + "damage_threshold" = 18) + +/* Mirelurk Softshell armor + * Weak variant - hasn't hardened yet + * Low DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_MIRELURK_SOFT list(\ + "melee" = 20, \ + "bullet" = 15, \ + "laser" = 5, \ + "energy" = 5, \ + "bomb" = 5, \ + "bio" = 100, \ + "rad" = 100, \ + "fire" = 0, \ + "acid" = 30, \ + "wound" = 5, \ + "damage_threshold" = 4) + +/* Mirelurk King armor + * Humanoid variant - different armor profile + * More balanced, less bullet resistance + * High DT, boss-tier + * * * * * * * * * * * */ +#define ARMOR_VALUE_MIRELURK_KING list(\ + "melee" = 50, \ + "bullet" = 45, \ + "laser" = 35, \ + "energy" = 35, \ + "bomb" = 30, \ + "bio" = 100, \ + "rad" = 100, \ + "fire" = 25, \ + "acid" = 70, \ + "wound" = 20, \ + "damage_threshold" = 15) + +/* Ant Queen armor + * Boss variant - heavily armored matriarch + * Very high melee/bullet resistance + * High DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_ANTS_QUEEN list(\ + "melee" = 45, \ + "bullet" = 40, \ + "laser" = 10, \ + "energy" = 10, \ + "bomb" = 5, \ + "bio" = 0, \ + "rad" = 0, \ + "fire" = 5, \ + "acid" = 0, \ + "wound" = 0, \ + "damage_threshold" = 8) + +/* Radscorpion armor + * Armored carapace - good all-around protection + * Strong melee/bullet resistance + * Medium DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_RADSCORPION list(\ + "melee" = 35, \ + "bullet" = 30, \ + "laser" = 15, \ + "energy" = 15, \ + "bomb" = 10, \ + "bio" = 0, \ + "rad" = 0, \ + "fire" = 10, \ + "acid" = 25, \ + "wound" = 5, \ + "damage_threshold" = 7) + +/* Black Radscorpion armor + * Tougher variant - thicker black exoskeleton + * Higher armor across the board + * Higher DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_RADSCORPION_BLACK list(\ + "melee" = 45, \ + "bullet" = 40, \ + "laser" = 20, \ + "energy" = 20, \ + "bomb" = 15, \ + "bio" = 0, \ + "rad" = 0, \ + "fire" = 15, \ + "acid" = 35, \ + "wound" = 10, \ + "damage_threshold" = 10) + +/* Cazador armor + * Flying insect - weak armor, relies on speed + * Low protection overall + * Very low DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_CAZADOR list(\ + "melee" = 10, \ + "bullet" = 5, \ + "laser" = 5, \ + "energy" = 5, \ + "bomb" = 0, \ + "bio" = 0, \ + "rad" = 0, \ + "fire" = 0, \ + "acid" = 10, \ + "wound" = 0, \ + "damage_threshold" = 2) + +/* Tunneler armor + * Deadly swarm creature with tough hide + * Moderate melee/bullet resistance + * Medium DT, relies on pack tactics + * * * * * * * * * * * */ +#define ARMOR_VALUE_TUNNELER list(\ + "melee" = 30, \ + "bullet" = 25, \ + "laser" = 15, \ + "energy" = 15, \ + "bomb" = 10, \ + "bio" = 0, \ + "rad" = 0, \ + "fire" = 5, \ + "acid" = 20, \ + "wound" = 10, \ + "damage_threshold" = 6) + +/* Centaur armor + * FEV mutant, ranged spitter + * Light armor coverage + * Low DT, relies on poison attacks + * * * * * * * * * * * */ +#define ARMOR_VALUE_CENTAUR list(\ + "melee" = 20, \ + "bullet" = 15, \ + "laser" = 10, \ + "energy" = 10, \ + "bomb" = 5, \ + "bio" = 100, \ + "rad" = 100, \ + "fire" = 10, \ + "acid" = 30, \ + "wound" = 5, \ + "damage_threshold" = 3) + +/* Abomination armor + * Massive FEV horror + * Extremely heavy armor + * Very high DT, nearly unstoppable + * * * * * * * * * * * */ +#define ARMOR_VALUE_ABOMINATION list(\ + "melee" = 60, \ + "bullet" = 55, \ + "laser" = 40, \ + "energy" = 40, \ + "bomb" = 30, \ + "bio" = 100, \ + "rad" = 100, \ + "fire" = 20, \ + "acid" = 50, \ + "wound" = 30, \ + "damage_threshold" = 15) + +/* Horror armor + * Failed FEV experiment + * Heavy armor coverage + * High DT, dangerous melee combatant + * * * * * * * * * * * */ +#define ARMOR_VALUE_HORROR list(\ + "melee" = 50, \ + "bullet" = 45, \ + "laser" = 30, \ + "energy" = 30, \ + "bomb" = 20, \ + "bio" = 100, \ + "rad" = 100, \ + "fire" = 15, \ + "acid" = 40, \ + "wound" = 20, \ + "damage_threshold" = 12) + +/* * * * * * * * * * * * * * * + * FACTION NPC ARMOR VALUES * + * * * * * * * * * * * * * * */ + +/* Tribal armor + * Very basic protection + * Cloth and leather scraps + * Minimal resistance + * Low DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_TRIBAL list(\ + "melee" = 15, \ + "bullet" = 10, \ + "laser" = 0, \ + "energy" = 0, \ + "bomb" = 0, \ + "bio" = 0, \ + "rad" = 0, \ + "fire" = 5, \ + "acid" = 0, \ + "wound" = 5, \ + "damage_threshold" = 1) + +/* Legion recruit armor + * Sports equipment and leather + * Better melee protection than bullet + * Weak to energy weapons + * Low DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_LEGION list(\ + "melee" = 25, \ + "bullet" = 20, \ + "laser" = -5, \ + "energy" = -10, \ + "bomb" = 0, \ + "bio" = 0, \ + "rad" = 0, \ + "fire" = 0, \ + "acid" = 0, \ + "wound" = 10, \ + "damage_threshold" = 2) + +/* Legion veteran armor + * Reinforced sports armor with metal pieces + * Good melee protection + * Decent bullet resistance + * Still weak to energy weapons + * Decent DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_LEGION_VETERAN list(\ + "melee" = 40, \ + "bullet" = 30, \ + "laser" = 10, \ + "energy" = 0, \ + "bomb" = 5, \ + "bio" = 0, \ + "rad" = 0, \ + "fire" = 10, \ + "acid" = 0, \ + "wound" = 15, \ + "damage_threshold" = 5) + +/* NCR trooper armor + * Standard combat armor + * Balanced protection + * Decent against most threats + * Decent DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_NCR list(\ + "melee" = 30, \ + "bullet" = 35, \ + "laser" = 25, \ + "energy" = 10, \ + "bomb" = 10, \ + "bio" = 0, \ + "rad" = 0, \ + "fire" = 10, \ + "acid" = 10, \ + "wound" = 15, \ + "damage_threshold" = 4) + +/* NCR Ranger armor + * Elite riot gear + * Heavy protection across the board + * Well-rounded defense + * High DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_NCR_RANGER list(\ + "melee" = 50, \ + "bullet" = 55, \ + "laser" = 45, \ + "energy" = 20, \ + "bomb" = 25, \ + "bio" = 20, \ + "rad" = 20, \ + "fire" = 35, \ + "acid" = 30, \ + "wound" = 25, \ + "damage_threshold" = 8) + +/* Brotherhood of Steel armor + * T-45d or T-51b Power Armor + * Strong protection + * Good energy resistance + * High DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_BOS list(\ + "melee" = 70, \ + "bullet" = 75, \ + "laser" = 70, \ + "energy" = 30, \ + "bomb" = 50, \ + "bio" = 80, \ + "rad" = 40, \ + "fire" = 70, \ + "acid" = 70, \ + "wound" = 40, \ + "damage_threshold" = 10) + +/* Brotherhood Paladin armor + * Superior Power Armor (T-51b or T-60) + * Enhanced protection + * Better energy resistance + * Very high DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_BOS_PALADIN list(\ + "melee" = 85, \ + "bullet" = 90, \ + "laser" = 85, \ + "energy" = 40, \ + "bomb" = 70, \ + "bio" = 90, \ + "rad" = 50, \ + "fire" = 85, \ + "acid" = 85, \ + "wound" = 45, \ + "damage_threshold" = 12) + +/* Enclave soldier armor + * Advanced Combat Armor or X-01 Power Armor + * Excellent protection + * Superior energy resistance + * Very high DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_ENCLAVE list(\ + "melee" = 80, \ + "bullet" = 85, \ + "laser" = 80, \ + "energy" = 35, \ + "bomb" = 65, \ + "bio" = 85, \ + "rad" = 45, \ + "fire" = 80, \ + "acid" = 80, \ + "wound" = 40, \ + "damage_threshold" = 11) + +/* Enclave Advanced Power Armor + * Top-tier X-02 Advanced Power Armor + * Near-impenetrable protection + * Best energy resistance + * Extremely high DT + * * * * * * * * * * * */ +#define ARMOR_VALUE_ENCLAVE_APA list(\ + "melee" = 95, \ + "bullet" = 100, \ + "laser" = 95, \ + "energy" = 45, \ + "bomb" = 80, \ + "bio" = 100, \ + "rad" = 55, \ + "fire" = 95, \ + "acid" = 95, \ + "wound" = 50, \ + "damage_threshold" = 14) + +/* Chinese Remnant Soldier + * Ghoulified Chinese soldiers with military equipment + * Decent bullet resistance from worn armor + * Laser/energy resistance from ghoul biology + * Weak to melee due to degraded condition + * Low DT from aged equipment + * * * * * * * * * * * */ +#define ARMOR_VALUE_CHINESE_REMNANT list(\ + "melee" = -25, \ + "bullet" = 30, \ + "laser" = 20, \ + "energy" = 25, \ + "bomb" = 0, \ + "bio" = 100, \ + "rad" = 100, \ + "fire" = 10, \ + "acid" = 10, \ + "wound" = 0, \ + "damage_threshold" = 2) + /* Armor Subclass multipliers * Modifies base armor slots by these values * Environmental and wound armors are multiplied by these values diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm index d8a21f8fe23..9282768c3be 100644 --- a/code/__DEFINES/mobs.dm +++ b/code/__DEFINES/mobs.dm @@ -11,6 +11,15 @@ #define MOVE_INTENT_WALK "walk" #define MOVE_INTENT_RUN "run" +// Vision cone definitions (cone-based directional vision system) +#define CONE_FRONT 1 // 90° front cone - best vision +#define CONE_PERIPHERAL 2 // 45° side cones - reduced vision +#define CONE_REAR 3 // Behind mob - no vision, sound only + +// Sound cone definitions (rear sound detection) +#define SOUND_REAR_CENTER 1 // 90° rear cone - normal sound detection +#define SOUND_REAR_PERIPHERAL 2 // 45° rear side cones - reduced sound detection + /// Normal baseline blood volume #define BLOOD_VOLUME_NORMAL 1000 /// The amount blood typically regenerates to on its own @@ -176,6 +185,7 @@ #define MOB_SIZE_SMALL 1 #define MOB_SIZE_HUMAN 2 #define MOB_SIZE_LARGE 3 +#define MOB_SIZE_HUGE 5 //Ventcrawling defines #define VENTCRAWLER_NONE 0 diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 3651a8a7453..6ba363c34c5 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -198,6 +198,7 @@ #define TRAIT_NO_MIDROUND_ANTAG "no-midround-antag" //can't be turned into an antag by random events #define TRAIT_PUGILIST "pugilist" //This guy punches people for a living #define TRAIT_KI_VAMPIRE "ki-vampire" //when someone with this trait rolls maximum damage on a punch and stuns the target, they regain some stamina and do clone damage +#define TRAIT_ASSASSIN "assassin" //Can perform deadly assassination strikes on unaware enemies #define TRAIT_PASSTABLE "passtable" #define TRAIT_GIANT "giant" #define TRAIT_DWARF "dwarf" diff --git a/code/controllers/subsystem/atoms.dm b/code/controllers/subsystem/atoms.dm index 008195fc540..57f222efd4a 100644 --- a/code/controllers/subsystem/atoms.dm +++ b/code/controllers/subsystem/atoms.dm @@ -17,6 +17,11 @@ SUBSYSTEM_DEF(atoms) /datum/controller/subsystem/atoms/Initialize(timeofday) GLOB.fire_overlay.appearance_flags = RESET_COLOR setupGenetics() + + // OPTIMIZATION: Pre-allocate GLOB.machines to avoid repeated list resizing during init + // Typical maps have 1000-2500 machines, so pre-allocate to avoid performance hits + GLOB.machines = new /list(2500) + initialized = INITIALIZATION_INNEW_MAPLOAD InitializeAtoms() return ..() @@ -30,6 +35,7 @@ SUBSYSTEM_DEF(atoms) LAZYINITLIST(late_loaders) var/count + var/batch_count = 0 var/list/mapload_arg = list(TRUE) if(atoms) count = atoms.len @@ -37,14 +43,18 @@ SUBSYSTEM_DEF(atoms) var/atom/A = I if(!(A.flags_1 & INITIALIZED_1)) InitAtom(I, mapload_arg) - CHECK_TICK + if(++batch_count >= 100) + batch_count = 0 + CHECK_TICK else count = 0 for(var/atom/A in world) if(!(A.flags_1 & INITIALIZED_1)) InitAtom(A, mapload_arg) ++count - CHECK_TICK + if(++batch_count >= 100) + batch_count = 0 + CHECK_TICK testing("Initialized [count] atoms") pass(count) @@ -52,12 +62,37 @@ SUBSYSTEM_DEF(atoms) initialized = INITIALIZATION_INNEW_REGULAR if(late_loaders.len) + batch_count = 0 for(var/I in late_loaders) var/atom/A = I A.LateInitialize() + if(++batch_count >= 100) + batch_count = 0 + CHECK_TICK testing("Late initialized [late_loaders.len] atoms") - late_loaders.Cut() - + late_loaders.Cut() + // OPTIMIZATION: Batch camera visibility updates after all atoms initialized + if(atoms) // Only during mapload + testing("Updating camera visibility for all turfs...") + batch_count = 0 + for(var/turf/T in world) + T.visibilityChanged() + if(++batch_count >= 100) + batch_count = 0 + CHECK_TICK + testing("Camera visibility updated") + + // OPTIMIZATION: Batch sunlight border smoothing after all turfs exist + if(atoms) + testing("Smoothing sunlight borders...") + batch_count = 0 + for(var/turf/T in world) + if(T.sunlight_state == SUNLIGHT_BORDER && isnull(T.border_neighbors)) + T.smooth_sunlight_border() + if(++batch_count >= 100) + batch_count = 0 + CHECK_TICK + testing("Sunlight borders smoothed") /datum/controller/subsystem/atoms/proc/InitAtom(atom/A, list/arguments) var/the_type = A.type if(QDELING(A)) diff --git a/code/controllers/subsystem/icon_smooth.dm b/code/controllers/subsystem/icon_smooth.dm index 6f4040299ae..3ab6d65b242 100644 --- a/code/controllers/subsystem/icon_smooth.dm +++ b/code/controllers/subsystem/icon_smooth.dm @@ -32,11 +32,14 @@ SUBSYSTEM_DEF(icon_smooth) smooth_zlevel(2,TRUE) var/queue = smooth_queue smooth_queue = list() + var/batch_count = 0 for(var/V in queue) var/atom/A = V if(!A || A.z <= 2) continue smooth_icon(A) - CHECK_TICK + if(++batch_count >= 100) + batch_count = 0 + CHECK_TICK return ..() diff --git a/code/controllers/subsystem/idlenpcpool.dm b/code/controllers/subsystem/idlenpcpool.dm index 64419b443f0..9c84cf50ada 100644 --- a/code/controllers/subsystem/idlenpcpool.dm +++ b/code/controllers/subsystem/idlenpcpool.dm @@ -2,7 +2,7 @@ SUBSYSTEM_DEF(idlenpcpool) name = "Idling NPC Pool" flags = SS_POST_FIRE_TIMING|SS_BACKGROUND|SS_NO_INIT priority = FIRE_PRIORITY_IDLE_NPC - wait = 60 + wait = 2 SECONDS // Reduced from 6s to 2s for faster initial mob response runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME var/list/currentrun = list() diff --git a/code/controllers/subsystem/lighting.dm b/code/controllers/subsystem/lighting.dm index d9c52f12567..2cf27b5e42f 100644 --- a/code/controllers/subsystem/lighting.dm +++ b/code/controllers/subsystem/lighting.dm @@ -32,6 +32,7 @@ SUBSYSTEM_DEF(lighting) if(!init_tick_checks) MC_SPLIT_TICK var/list/queue = GLOB.lighting_update_lights + var/batch_count = 0 while(length(queue)) var/datum/light_source/light_datum = queue[length(queue)] queue.len-- @@ -41,7 +42,9 @@ SUBSYSTEM_DEF(lighting) light_datum.needs_update = LIGHTING_NO_UPDATE if(init_tick_checks) - CHECK_TICK + if(++batch_count >= 100) + batch_count = 0 + CHECK_TICK else if (MC_TICK_CHECK) break @@ -49,6 +52,7 @@ SUBSYSTEM_DEF(lighting) MC_SPLIT_TICK queue = GLOB.lighting_update_corners + batch_count = 0 while(length(queue)) var/datum/lighting_corner/corner_datum = queue[length(queue)] queue.len-- @@ -56,7 +60,9 @@ SUBSYSTEM_DEF(lighting) corner_datum.update_objects() corner_datum.needs_update = FALSE if(init_tick_checks) - CHECK_TICK + if(++batch_count >= 100) + batch_count = 0 + CHECK_TICK else if (MC_TICK_CHECK) break @@ -64,6 +70,7 @@ SUBSYSTEM_DEF(lighting) MC_SPLIT_TICK queue = GLOB.lighting_update_objects + batch_count = 0 while(length(queue)) var/atom/movable/lighting_object/lighting_object = queue[length(queue)] queue.len-- @@ -74,7 +81,9 @@ SUBSYSTEM_DEF(lighting) lighting_object.update() lighting_object.needs_update = FALSE if(init_tick_checks) - CHECK_TICK + if(++batch_count >= 100) + batch_count = 0 + CHECK_TICK else if (MC_TICK_CHECK) break diff --git a/code/datums/traits/good.dm b/code/datums/traits/good.dm index e5424cb7858..d0f01d88903 100644 --- a/code/datums/traits/good.dm +++ b/code/datums/traits/good.dm @@ -1,12 +1,14 @@ -GLOBAL_LIST_INIT(chemwhiz_recipes, list( +GLOBAL_LIST_INIT(chemwhiz_recipes_basic, list( + /datum/crafting_recipe/cheap_stimpak, + /datum/crafting_recipe/medx/chemistry)) + +GLOBAL_LIST_INIT(chemwhiz_recipes_advanced, list( /datum/crafting_recipe/jet, /datum/crafting_recipe/turbo, /datum/crafting_recipe/psycho, /datum/crafting_recipe/medx, - /datum/crafting_recipe/medx/chemistry, /datum/crafting_recipe/stimpak/chemistry, /datum/crafting_recipe/stimpak5/chemistry, - /datum/crafting_recipe/cheap_stimpak, /datum/crafting_recipe/buffout, /datum/crafting_recipe/steady)) @@ -89,7 +91,7 @@ GLOBAL_LIST_INIT(pa_repair, list( /datum/crafting_recipe/repair_t45_helm, /datum/crafting_recipe/scrap_pa, /datum/crafting_recipe/scrap_pa_helm)) - + GLOBAL_LIST_INIT(white_legs_recipes, list( /datum/crafting_recipe/tribalwar/whitelegs/lightarmour, /datum/crafting_recipe/tribalwar/whitelegs/armour, @@ -397,7 +399,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_WEAPONSMITH gain_text = span_notice("You are adept at crafting makeshift weapons.") lose_text = span_danger("You feel less adept at crafting makeshift weapons.") - locked = TRUE + locked = TRUE /datum/quirk/gunsmith/add() var/mob/living/carbon/human/H = quirk_holder @@ -456,7 +458,6 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( gain_text = span_notice("The shadows seem a little less dark.") lose_text = span_danger("Everything seems a little darker.") - /datum/quirk/night_vision/on_spawn() var/mob/living/carbon/human/H = quirk_holder H.update_sight() @@ -510,27 +511,43 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( lose_text = span_danger("You feel like skipping practice.") locked = TRUE */ + /datum/quirk/chemwhiz name = "Chem Whiz" - desc = "You've been playing around with chemicals all your life. You know how to use chemistry machinery." + desc = "You've been playing around with chemicals all your life. You know how to use chemistry machinery. High intelligence unlocks advanced formulas." value = 3 mob_trait = TRAIT_CHEMWHIZ gain_text = span_notice("The mysteries of chemistry are revealed to you.") lose_text = span_danger("You forget how the periodic table works.") - locked = TRUE + locked = TRUE /datum/quirk/chemwhiz/add() var/mob/living/carbon/human/H = quirk_holder - // I made the quirks add the same recipes as the trait books. Feel free to nerf this if(!H.mind.learned_recipes) H.mind.learned_recipes = list() - H.mind.learned_recipes |= GLOB.chemwhiz_recipes + H.mind.learned_recipes |= GLOB.chemwhiz_recipes_basic + if(H.special_i >= 6) + H.mind.learned_recipes |= GLOB.chemwhiz_recipes_advanced + to_chat(H, span_notice("Your sharp mind unlocks advanced chemical formulas.")) + else + to_chat(H, span_notice("You can make basic stabilization supplies, but advanced formulas are beyond you for now. (Requires 6 INT)")) /datum/quirk/chemwhiz/remove() var/mob/living/carbon/human/H = quirk_holder if(H) - H.mind.learned_recipes -= GLOB.chemwhiz_recipes - + H.mind.learned_recipes -= GLOB.chemwhiz_recipes_basic + H.mind.learned_recipes -= GLOB.chemwhiz_recipes_advanced + +// Call this wherever INT (special_i) changes +/mob/living/carbon/human/proc/update_chemwhiz_recipes() + if(!HAS_TRAIT(src, TRAIT_CHEMWHIZ) || !mind) + return + if(!mind.learned_recipes) + mind.learned_recipes = list() + if(special_i >= 6) + mind.learned_recipes |= GLOB.chemwhiz_recipes_advanced + else + mind.learned_recipes -= GLOB.chemwhiz_recipes_advanced /datum/quirk/pa_wear name = "Power Armor Training" @@ -745,7 +762,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_WHITELEGS_TRAD gain_text = span_notice("The mysteries of your ancestors are revealed to you.") lose_text = span_danger("You forget how your ancestors created their garments.") - locked = FALSE + locked = FALSE /datum/quirk/whitelegstraditions/add() var/mob/living/carbon/human/H = quirk_holder @@ -765,7 +782,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_DEADHORSES_TRAD gain_text = span_notice("The mysteries of your ancestors are revealed to you.") lose_text = span_danger("You forget how your ancestors created their garments.") - locked = FALSE + locked = FALSE /datum/quirk/deadhorsestraditions/add() var/mob/living/carbon/human/H = quirk_holder @@ -785,7 +802,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_RUSTWALKERS_TRAD gain_text = span_notice("The mysteries of your ancestors are revealed to you.") lose_text = span_danger("You forget how your ancestors created their garments.") - locked = FALSE + locked = FALSE /datum/quirk/rustwalkerstraditions/add() var/mob/living/carbon/human/H = quirk_holder @@ -805,7 +822,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_EIGHTIES_TRAD gain_text = span_notice("The mysteries of your ancestors are revealed to you.") lose_text = span_danger("You forget how your ancestors created their garments.") - locked = FALSE + locked = FALSE /datum/quirk/eightiestraditions/add() var/mob/living/carbon/human/H = quirk_holder @@ -825,7 +842,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_SORROWS_TRAD gain_text = span_notice("The mysteries of your ancestors are revealed to you.") lose_text = span_danger("You forget how your ancestors created their garments.") - locked = FALSE + locked = FALSE /datum/quirk/sorrowstraditions/add() var/mob/living/carbon/human/H = quirk_holder @@ -845,7 +862,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_WAYFARER_TRAD gain_text = span_notice("The mysteries of your ancestors are revealed to you.") lose_text = span_danger("You forget how your ancestors created their garments.") - locked = FALSE + locked = FALSE /datum/quirk/wayfarertraditions/add() var/mob/living/carbon/human/H = quirk_holder @@ -865,7 +882,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_BONEDANCER_TRAD gain_text = span_notice("The mysteries of your ancestors are revealed to you.") lose_text = span_danger("You forget how your ancestors created their garments.") - locked = FALSE + locked = FALSE /datum/quirk/bonedancertraditions/add() var/mob/living/carbon/human/H = quirk_holder @@ -885,7 +902,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_PUSHIMMUNE gain_text = span_notice("You feel stronger than a brick wall.") lose_text = span_danger("Your feel like you could get thrown down again.") - locked = FALSE + locked = FALSE /datum/quirk/heatresist name = "Heat Resistant" @@ -894,7 +911,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_RESISTHEAT gain_text = span_notice("It could be a little warmer in here.") lose_text = span_danger("You know? Being hot kind of sucks actually.") - locked = FALSE + locked = FALSE /datum/quirk/coldresist name = "Cold Resistant" @@ -903,7 +920,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_RESISTCOLD gain_text = span_notice("It could be a little colder in here.") lose_text = span_danger("You know? Being cold kind of sucks actually.") - locked = FALSE + locked = FALSE /* /datum/quirk/radimmune @@ -913,7 +930,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_RADIMMUNE gain_text = span_notice("You've decided radiation just doesn't matter.") lose_text = span_danger("You no longer feel like you could probably live in a microwave while it's on.") - locked = FALSE + locked = FALSE /datum/quirk/radimmuneish name = "Radiation - Mostly Immune" @@ -922,8 +939,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_75_RAD_RESIST gain_text = span_notice("You've decided radiation just doesn't matter much.") lose_text = span_danger("You no longer feel like you could roll around in a rad puddle for a while.") - locked = FALSE - + locked = FALSE /datum/quirk/radimmunesorta name = "Radiation - Sorta Immune" @@ -932,8 +948,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_50_RAD_RESIST gain_text = span_notice("You've decided radiation only kind of matters.") lose_text = span_danger("You no longer think you should hang out next to rad puddles.") - locked = TRUE - + locked = TRUE /datum/quirk/nohunger name = "Does not Eat" @@ -942,7 +957,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_NOHUNGER gain_text = span_notice("Your need for food has left you.") lose_text = span_danger("GOD YOU WANT A BURGER SO BAD.") - locked = FALSE + locked = FALSE /datum/quirk/thickskin name = "Thick Skin" @@ -951,7 +966,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_PIERCEIMMUNE gain_text = span_notice("Your skin feels way stronger.") lose_text = span_danger("You feel like your skin is about as tough as tissue paper.") - locked = TRUE + locked = TRUE */ /datum/quirk/barbedwire @@ -970,7 +985,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_QUICKER_CARRY gain_text = span_notice("You feel like a MASTER fireman!") lose_text = span_danger("Your ability to carry folk seems massively diminished.") - locked = FALSE + locked = FALSE /datum/quirk/quickcarry name = "Quick Carry" @@ -979,7 +994,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_QUICK_CARRY gain_text = span_notice("You feel like an ACCEPTABLE fireman!") lose_text = span_danger("Your ability to carry folk seems a bit diminished.") - locked = FALSE + locked = FALSE /datum/quirk/builder name = "Experienced Builder" @@ -988,7 +1003,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_QUICK_BUILD gain_text = span_notice("You could throw up a house if you wanted to!") lose_text = span_danger("What's a two by four again?") - locked = FALSE + locked = FALSE /* /datum/quirk/grappler @@ -998,7 +1013,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_STRONG_GRABBER gain_text = span_notice("You could wrassle a deathclaw!!") lose_text = span_danger("You no longer feel like you should wrestle deathclaws.") - locked = FALSE + locked = FALSE /datum/quirk/mastermartialartist name = "Master Martial Artist" @@ -1007,7 +1022,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_KI_VAMPIRE gain_text = span_notice("They are already dead.") lose_text = span_danger("Your fists no longer feel so powerful.") - locked = FALSE + locked = FALSE /datum/quirk/surestrike name = "Sure Strike" @@ -1016,7 +1031,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_PERFECT_ATTACKER gain_text = span_notice("They are already dead.") lose_text = span_danger("Your fists no longer feel so powerful.") - locked = FALSE + locked = FALSE */ /datum/quirk/quietstep @@ -1026,7 +1041,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_SILENT_STEP gain_text = span_notice("Your footsteps fade away.") lose_text = span_danger("You find yourself surprised by the sound of your own footsteps.") - locked = FALSE + locked = FALSE /* /datum/quirk/deadeye name = "Dead Eye" @@ -1035,7 +1050,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_INSANE_AIM gain_text = span_notice("Your aim is legendary, and you know it.") lose_text = span_danger("Your aim could use some work...") - locked = FALSE + locked = FALSE /datum/quirk/straightshooter name = "Straight Shooter" @@ -1044,8 +1059,9 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_NICE_SHOT gain_text = span_notice("Your aim is amazing, and you know it.") lose_text = span_danger("Your aim could use some work...") - locked = TRUE + locked = TRUE */ + /datum/quirk/bowtrained name = "Bow Trained" desc = "You've trained quite a bit with bows of many types, and are pretty good with them for it." @@ -1053,7 +1069,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_AUTO_DRAW gain_text = span_notice("You feel like all that training with bows has paid off.") lose_text = span_danger("Guns were always better...") - locked = FALSE + locked = FALSE /datum/quirk/masterrifleman name = "Bolt Worker" @@ -1062,7 +1078,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_FAST_PUMP gain_text = span_notice("In a sudden haze you realize that the Mosin-Nagant was God's gift to mankind.") lose_text = span_danger("After picking some 250 year old cosmoline out from under one of your nails you realize that... Uh, no, the Mosin-Nagant is a piece of shit.") - locked = FALSE + locked = FALSE /datum/quirk/playdead name = "Class Act" @@ -1071,7 +1087,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( mob_trait = TRAIT_PLAY_DEAD gain_text = span_notice("You feel confident at playing dead.") lose_text = span_danger("You feel that laying down in a field of gunfire may not be such a good idea after all.") - locked = FALSE + locked = FALSE /datum/quirk/ratfriend name = "Beast Friend - Rats" @@ -1079,7 +1095,7 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( value = 2 mob_trait = TRAIT_BEASTFRIEND_RAT gain_text = span_notice("Rats are friends!") - lose_text = span_danger("God of rats curses your name...") // Perhaps make killing related mobs lose the quirk? + lose_text = span_danger("God of rats curses your name...") locked = FALSE /datum/quirk/ratfriend/add() @@ -1185,7 +1201,6 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( gain_text = span_notice("You remember the old ways of your tribe...") lose_text = span_notice("You've forgotten the ways of your ancestors...") - /datum/quirk/tribespeak/add() var/mob/living/carbon/human/H = quirk_holder H.grant_language(/datum/language/tribal) @@ -1202,7 +1217,6 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( gain_text = span_notice("You remember the old tongue of the Mexican cartels.") lose_text = span_notice("You've forgotten the tongue of the Mexican cartels.") - /datum/quirk/spanishspeak/add() var/mob/living/carbon/human/H = quirk_holder H.grant_language(/datum/language/spanish) @@ -1243,3 +1257,198 @@ GLOBAL_LIST_INIT(bone_dancer_recipes, list( var/mob/living/carbon/human/H = quirk_holder if(!QDELETED(H)) H.remove_language(/datum/language/japanese) + +/datum/quirk/assassin + name = "Assassin" + desc = "Years of practice have made you lethal at close range. While sneaking with a melee weapon in your active hand, you can assassinate unaware enemies within 2 tiles by attacking from behind." + value = 4 + mob_trait = TRAIT_ASSASSIN + gain_text = span_notice("You feel like you could kill someone very quietly.") + lose_text = span_danger("You feel less lethal.") + locked = FALSE + var/datum/action/cooldown/assassinate/assassinate_action + +/datum/quirk/assassin/add() + assassinate_action = new() + assassinate_action.Grant(quirk_holder) + assassinate_action.UpdateButtonIcon() + +/datum/quirk/assassin/remove() + if(assassinate_action) + assassinate_action.Remove(quirk_holder) + QDEL_NULL(assassinate_action) + +// ========== ASSASSINATE ACTION ========== +/datum/action/cooldown/assassinate + name = "Assassinate" + desc = "Strike a killing blow on an unaware enemy within 2 tiles from behind. Requires a melee weapon in your active hand and sneak mode. Target must not be alerted to your presence." + button_icon_state = "dagger" + icon_icon = 'icons/mob/actions/actions_cult.dmi' + check_flags = AB_CHECK_CONSCIOUS + cooldown_time = 15 SECONDS + +/// Check if assassin is behind the target (in their rear cone) +/datum/action/cooldown/assassinate/proc/is_behind_target(mob/attacker, mob/target) + if(!attacker || !target) + return FALSE + + var/turf/attacker_turf = get_turf(attacker) + var/turf/target_turf = get_turf(target) + + if(!attacker_turf || !target_turf) + return FALSE + + if(attacker_turf == target_turf) + return FALSE + + var/dx = attacker_turf.x - target_turf.x + var/dy = attacker_turf.y - target_turf.y + + var/target_angle = 0 + switch(target.dir) + if(NORTH) + target_angle = 0 + if(SOUTH) + target_angle = 180 + if(EAST) + target_angle = 90 + if(WEST) + target_angle = 270 + + var/attacker_angle = arctan(dy, dx) + var/relative_angle = attacker_angle - target_angle + + while(relative_angle > 180) + relative_angle -= 360 + while(relative_angle < -180) + relative_angle += 360 + + if(relative_angle > 90 || relative_angle < -90) + return TRUE + + return FALSE + +/datum/action/cooldown/assassinate/IsAvailable(silent = FALSE) + if(!..()) + return FALSE + + if(!ishuman(owner)) + return FALSE + + var/mob/living/carbon/human/H = owner + + if(!H.sneaking) + return FALSE + + var/obj/item/held_item = H.get_active_held_item() + if(!held_item) + return FALSE + + if(!istype(held_item, /obj/item/melee) && !istype(held_item, /obj/item/kitchen) && held_item.force <= 0) + return FALSE + + for(var/mob/living/L in range(2, H)) + if(L == H) + continue + if(L.stat != CONSCIOUS) + continue + if(!is_behind_target(H, L)) + continue + if(istype(L, /mob/living/simple_animal/hostile)) + var/mob/living/simple_animal/hostile/enemy = L + if(enemy.faction_check_mob(H)) + continue + if(enemy.target == H) + continue + return TRUE + else if(ishuman(L) && L != H) + return TRUE + + return FALSE + +/datum/action/cooldown/assassinate/Grant(mob/M) + ..() + if(!ishuman(M)) + return + UpdateButtonIcon() + +/datum/action/cooldown/assassinate/Trigger() + if(!..()) + return FALSE + + if(!ishuman(owner)) + return FALSE + + var/mob/living/carbon/human/H = owner + var/mob/living/target = null + var/closest_dist = 999 + + for(var/mob/living/L in range(2, H)) + if(L == H) + continue + if(L.stat != CONSCIOUS) + continue + if(!is_behind_target(H, L)) + continue + + var/dist = get_dist(H, L) + + if(istype(L, /mob/living/simple_animal/hostile)) + var/mob/living/simple_animal/hostile/enemy = L + if(enemy.faction_check_mob(H)) + continue + if(enemy.target == H) + continue + if(dist < closest_dist) + closest_dist = dist + target = L + else if(ishuman(L)) + if(dist < closest_dist) + closest_dist = dist + target = L + + if(!target) + to_chat(H, span_warning("No valid target found (must attack from behind).")) + return FALSE + + var/obj/item/weapon = H.get_active_held_item() + if(!weapon) + to_chat(H, span_warning("You need a weapon in your active hand!")) + return FALSE + + if(!istype(weapon, /obj/item/melee) && !istype(weapon, /obj/item/kitchen) && weapon.force <= 0) + to_chat(H, span_warning("[weapon] is not suitable for assassination!")) + return FALSE + + var/current_distance = get_dist(H, target) + if(current_distance == 2) + var/lunge_dir = get_dir(H, target) + step(H, lunge_dir) + H.visible_message( + span_danger("[H] lunges at [target]!"), + span_userdanger("You lunge at [target]!") + ) + playsound(H.loc, 'sound/weapons/thudswoosh.ogg', 50, TRUE) + + H.do_attack_animation(target) + H.visible_message( + span_danger("[H] strikes with lethal precision!"), + span_userdanger("You strike at [target]'s vitals!") + ) + + var/base_damage = weapon.force + var/assassination_damage = base_damage + 40 + var/secondary_damage = round(assassination_damage * 0.4) + + target.apply_damage(assassination_damage, BRUTE, ran_zone()) + target.apply_damage(secondary_damage, BRUTE, ran_zone()) + + shake_camera(H, 3, 1.5) + + if(isliving(target)) + target.Stun(20) + + log_combat(H, target, "assassinated") + StartCooldown() + UpdateButtonIcon() + return TRUE diff --git a/code/game/objects/effects/overlays.dm b/code/game/objects/effects/overlays.dm index 2e5d6717fb5..791a2f22fd4 100644 --- a/code/game/objects/effects/overlays.dm +++ b/code/game/objects/effects/overlays.dm @@ -635,3 +635,283 @@ mouse_opacity = MOUSE_OPACITY_TRANSPARENT plane = WALL_PLANE layer = ABOVE_OBJ_LAYER + +// SNEAKING MODE OVERLAYS +// Visual effects for the sneak mode system + +// Sneak icon overlay that appears above player's head +// Sneak mode indicator - overlay appearance for add_overlay +/obj/effect/overlay/sneak_icon + name = "" + icon = 'icons/mob/actions/actions_changeling.dmi' + icon_state = "ling_augmented_eyesight" + layer = ABOVE_MOB_LAYER + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + appearance_flags = RESET_COLOR | TILE_BOUND | PIXEL_SCALE + alpha = 102 // 60% transparent (40% opaque) + +/obj/effect/overlay/sneak_icon/Initialize() + . = ..() + pixel_y = 20 // Above the head + // Make 30% smaller + transform = matrix().Scale(0.7, 0.7) + + +// VISION CONE OVERLAY - Fixed angle calculation version +// Properly fills cone areas using dot product comparison + +/obj/effect/overlay/vision_cone + name = "" + icon = null + layer = BELOW_MOB_LAYER + plane = CHAT_PLANE // Use CHAT_PLANE to bypass FoV hiding + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + anchored = TRUE + alpha = 80 + + var/list/cone_images = list() // Images shown to the viewer's client + var/mob/living/carbon/human/viewer = null // The player viewing these cones + var/mob/living/simple_animal/hostile/tracked_mob = null + var/cone_dir = NORTH + var/cone_range = 9 + var/cone_icon_state = "white" // Use white base for color tinting + var/cone_alpha = 80 // Alpha value for this cone + var/cone_angle = 62.5 // Default: 125° large center cones (62.5° half-angle) + + // Sector mode variables + var/sector_start_angle = 0 // Start angle for sector mode + var/sector_end_angle = 0 // End angle for sector mode + var/use_sector_mode = FALSE // Use angular sector instead of cone + +/obj/effect/overlay/vision_cone/Destroy() + // Remove images from client + if(viewer && viewer.client) + viewer.client.images -= cone_images + cone_images.Cut() + return ..() + +/obj/effect/overlay/vision_cone/proc/setup_cone(mob/living/simple_animal/hostile/H, mob_dir, range_tiles, icon_state_name = "white") + tracked_mob = H + cone_dir = mob_dir + cone_range = range_tiles + cone_icon_state = icon_state_name + cone_alpha = alpha // Capture the alpha value that was set on this cone + cone_angle = 62.5 // 125° large center cones with overlap + +// Setup a narrow cone (for peripheral/side zones) +/obj/effect/overlay/vision_cone/proc/setup_narrow_cone(mob/living/simple_animal/hostile/H, mob_dir, range_tiles, angle_width = 16.5) + tracked_mob = H + cone_dir = mob_dir + cone_range = range_tiles + cone_icon_state = "white" + cone_alpha = alpha + cone_angle = angle_width // Use specified angle width + use_sector_mode = FALSE + +// Setup for sector mode +/obj/effect/overlay/vision_cone/proc/setup_sector(mob/living/simple_animal/hostile/H, base_dir, start_deg, end_deg, range_tiles, icon_state_name = "white") + tracked_mob = H + cone_dir = base_dir + cone_range = range_tiles + sector_start_angle = start_deg + sector_end_angle = end_deg + use_sector_mode = TRUE + cone_alpha = alpha + cone_icon_state = icon_state_name + +// Generate sector images using quadrant-based angle calculation +/obj/effect/overlay/vision_cone/proc/generate_cone_image() + var/turf/center = get_turf(tracked_mob) + if(!center) + return FALSE + + // Remove old images from client + if(viewer && viewer.client) + viewer.client.images -= cone_images + cone_images.Cut() + + // Get all turfs in the sector using quadrant-based calculation + var/list/cone_turfs + if(use_sector_mode) + cone_turfs = get_sector_turfs(center, cone_dir, cone_range, sector_start_angle, sector_end_angle) + else + return FALSE + + if(!cone_turfs || !cone_turfs.len) + return FALSE + + // Create images at each turf location on CHAT_PLANE + for(var/turf/T in cone_turfs) + var/image/img = image('icons/effects/alphacolors.dmi', T, cone_icon_state) + img.layer = BELOW_MOB_LAYER + img.plane = CHAT_PLANE // CHAT_PLANE bypasses FoV + img.alpha = cone_alpha + cone_images += img + + // Add all images to viewer's client at once + if(viewer && viewer.client) + viewer.client.images += cone_images + + return TRUE + +/obj/effect/overlay/vision_cone/proc/get_sector_turfs(turf/center, base_direction, range, start_angle_deg, end_angle_deg) + var/list/turfs = list() + + // Get base direction angle (0° = NORTH, clockwise) + var/base_angle = dir_to_angle(base_direction) + + // Convert sector angles to absolute angles + var/absolute_start = base_angle + start_angle_deg + var/absolute_end = base_angle + end_angle_deg + + // Normalize angles to 0-360 + while(absolute_start < 0) + absolute_start += 360 + while(absolute_start >= 360) + absolute_start -= 360 + while(absolute_end < 0) + absolute_end += 360 + while(absolute_end >= 360) + absolute_end -= 360 + + // Check all turfs in range + for(var/turf/T in range(range, center)) + if(T == center) + continue + + var/dx = T.x - center.x + var/dy = T.y - center.y + + if(dx == 0 && dy == 0) + continue + + // QUADRANT-BASED ANGLE CALCULATION + var/turf_angle = calculate_angle_from_offset(dx, dy) + + // Check if turf angle is within sector + var/in_sector = FALSE + if(absolute_start < absolute_end) + in_sector = (turf_angle >= absolute_start && turf_angle < absolute_end) + else + in_sector = (turf_angle >= absolute_start || turf_angle < absolute_end) + + if(in_sector) + turfs += T + + return turfs + +/obj/effect/overlay/vision_cone/proc/get_turf_in_direction(turf/start, direction, distance) + var/turf/current = start + for(var/i = 1 to distance) + current = get_step(current, direction) + if(!current) + return null + return current + +// Helper: Convert BYOND direction to angle in degrees +// 0° = NORTH (up), increases CLOCKWISE +// NORTH = 0°, EAST = 90°, SOUTH = 180°, WEST = 270° +/obj/effect/overlay/vision_cone/proc/dir_to_angle(byond_dir) + switch(byond_dir) + if(NORTH) + return 0 // Up + if(NORTHEAST) + return 45 // Up-right diagonal + if(EAST) + return 90 // Right + if(SOUTHEAST) + return 135 // Down-right diagonal + if(SOUTH) + return 180 // Down + if(SOUTHWEST) + return 225 // Down-left diagonal + if(WEST) + return 270 // Left + if(NORTHWEST) + return 315 // Up-left diagonal + return 0 + +// Helper: Calculate angle from dx/dy offset using quadrant-based logic +// Returns angle in degrees: 0° = NORTH, clockwise +/obj/effect/overlay/vision_cone/proc/calculate_angle_from_offset(dx, dy) + var/angle = 0 + + // Handle cardinal directions first (avoids arctan edge cases) + if(dx == 0) + if(dy > 0) + return 0 // Pure NORTH + else + return 180 // Pure SOUTH + else if(dy == 0) + if(dx > 0) + return 90 // Pure EAST + else + return 270 // Pure WEST + + // Diagonal - use arctan on absolute values to get acute angle + var/acute_angle = arctan(abs(dy), abs(dx)) + + // Map to correct quadrant based on dx/dy signs + if(dx > 0 && dy > 0) + // NE quadrant (0° to 90°) + angle = acute_angle + else if(dx < 0 && dy > 0) + // NW quadrant (270° to 360°) + angle = 360 - acute_angle + else if(dx > 0 && dy < 0) + // SE quadrant (90° to 180°) + angle = 180 - acute_angle + else // dx < 0 && dy < 0 + // SW quadrant (180° to 270°) + angle = 180 + acute_angle + + // Normalize + while(angle < 0) + angle += 360 + while(angle >= 360) + angle -= 360 + + return angle + +/obj/effect/overlay/vision_cone/proc/get_turf_perpendicular(turf/start, direction, offset) + // Get turf perpendicular to the facing direction + var/perp_dir + var/abs_offset = abs(offset) + + switch(direction) + if(NORTH) + perp_dir = offset > 0 ? EAST : WEST + if(SOUTH) + perp_dir = offset > 0 ? WEST : EAST + if(EAST) + perp_dir = offset > 0 ? SOUTH : NORTH + if(WEST) + perp_dir = offset > 0 ? NORTH : SOUTH + if(NORTHEAST) + if(offset > 0) + perp_dir = SOUTHEAST + else + perp_dir = NORTHWEST + if(NORTHWEST) + if(offset > 0) + perp_dir = NORTHEAST + else + perp_dir = SOUTHWEST + if(SOUTHEAST) + if(offset > 0) + perp_dir = NORTHEAST + else + perp_dir = SOUTHWEST + if(SOUTHWEST) + if(offset > 0) + perp_dir = NORTHWEST + else + perp_dir = SOUTHEAST + + var/turf/result = start + for(var/i = 1 to abs_offset) + result = get_step(result, perp_dir) + if(!result) + return null + + return result diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index f12c94bed07..b81d110b190 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -736,6 +736,16 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb playsound(hit_atom, 'sound/weapons/genhit.ogg',volume, TRUE, -1) else playsound(hit_atom, 'sound/weapons/throwtap.ogg', 1, volume, -1) + + // HOSTILE MOB DETECTION: Alert nearby hostile mobs about thrown item + if(throwingdatum && throwingdatum.thrower) + for(var/mob/living/simple_animal/hostile/H in range(7, src)) + if(H.stat == DEAD || H.ckey) + continue + // Only alert mobs that can actually SEE the thrown item + if(!can_see(H, src, 7)) + continue + H.detect_thrown_item(src, throwingdatum.thrower) return hit_atom.hitby(src, 0, itempush, throwingdatum=throwingdatum) diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index e7da571b701..37a44ec4eb6 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -78,30 +78,54 @@ assemble_baseturfs() - levelupdate() + // OPTIMIZATION: Merge levelupdate() and Entered() into single loop to avoid iterating objects twice + for(var/atom/movable/AM in src) + Entered(AM) + // Handle level 1 object hiding (was in levelupdate()) + if(isobj(AM)) + var/obj/O = AM + if(O.level == 1 && (O.flags_1 & INITIALIZED_1)) + O.hide(src.intact) + if(smooth) queue_smooth(src) - visibilityChanged() + + // OPTIMIZATION: Skip camera updates during mapload - batched update happens after all turfs init + if(!mapload) + visibilityChanged() if(initial(opacity)) // Could be changed by the initialization of movable atoms in the turf. base_opacity = initial(opacity) directional_opacity = ALL_CARDINALS - for(var/atom/movable/AM in src) - Entered(AM) - + // Cache area lookup - used multiple times below var/area/A = loc + if(!IS_DYNAMIC_LIGHTING(src) && IS_DYNAMIC_LIGHTING(A)) add_overlay(/obj/effect/fullbright) else if(A.outdoors == TRUE) sunlight_state = SUNLIGHT_SOURCE - switch(sunlight_state) - if(SUNLIGHT_SOURCE) - setup_sunlight_source() - if(SUNLIGHT_BORDER) - border_neighbors = null - smooth_sunlight_border() + // OPTIMIZATION: Defer sunlight smoothing during mapload - batched after all turfs exist + if(mapload) + // Store state for batch processing, but skip the expensive neighbor checks + if(sunlight_state == SUNLIGHT_SOURCE) + vis_contents += SSnightcycle.sunlight_source_object + luminosity = 1 + // Mark neighbors, but don't smooth yet + for(var/dir in GLOB.alldirs) + var/turf/neighbor = get_step(src, dir) + if(!neighbor || !neighbor.type || neighbor?.sunlight_state && neighbor?.sunlight_state != NO_SUNLIGHT) + continue + neighbor.sunlight_state = SUNLIGHT_BORDER + else + // Not mapload, do normal smoothing + switch(sunlight_state) + if(SUNLIGHT_SOURCE) + setup_sunlight_source() + if(SUNLIGHT_BORDER) + border_neighbors = null + smooth_sunlight_border() if(requires_activation) CALCULATE_ADJACENT_TURFS(src) diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index bf672c49741..3df0dddf80e 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -189,3 +189,5 @@ /// If the client is currently under the restrictions of the interview system var/interviewee = FALSE var/is_fullscreen = 0 + /// Cached hash of current verb list to prevent unnecessary rebuilds + var/cached_verb_hash = null diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 192a0418d0e..91eb27014a2 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -358,7 +358,9 @@ GLOBAL_LIST_INIT(warning_ckeys, list()) // Initialize tgui panel tgui_panel.initialize() - // Initialize statbrowser properly + // Initialize statbrowser on first client connection + // This only runs once per connection - body switches (ghosting/re-entering) only refresh verbs + // Use "Fix Statpanel" verb if issues occur during play load_statbrowser() @@ -1110,11 +1112,16 @@ GLOBAL_LIST_EMPTY(every_fucking_sound_file) return prefs.pref_species.mutant_bodyparts[part_name] || (part_name in GLOB.unlocked_mutant_parts) /// compiles a full list of verbs and sends it to the browser -/client/proc/init_verbs() +/// Only rebuilds if verbs have changed to prevent unnecessary statpanel refreshes +/client/proc/init_verbs(force = FALSE) if(IsAdminAdvancedProcCall()) return + + // Build verb list and calculate hash var/list/verblist = list() - verb_tabs.Cut() + var/list/new_verb_tabs = list() + var/verb_hash = "" + for(var/thing in (verbs + mob?.verbs)) var/procpath/verb_to_init = thing if(!verb_to_init) @@ -1123,8 +1130,17 @@ GLOBAL_LIST_EMPTY(every_fucking_sound_file) continue if(!istext(verb_to_init.category)) continue - verb_tabs |= verb_to_init.category + new_verb_tabs |= verb_to_init.category verblist[++verblist.len] = list(verb_to_init.category, verb_to_init.name) + verb_hash += "[verb_to_init.category]:[verb_to_init.name];" + + // Only rebuild if verbs have changed or forced + if(!force && verb_hash == cached_verb_hash) + return // Verbs haven't changed, skip rebuild + + // Update cache and send to browser + cached_verb_hash = verb_hash + verb_tabs = new_verb_tabs src << output("[url_encode(json_encode(verb_tabs))];[url_encode(json_encode(verblist))]", "statbrowser:init_verbs") /client/proc/check_panel_loaded() diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm index 63d35322703..31302561fcb 100644 --- a/code/modules/client/verbs/ooc.dm +++ b/code/modules/client/verbs/ooc.dm @@ -311,7 +311,7 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8") set name = "Fix Stat Panel" set hidden = TRUE - init_verbs() + init_verbs(force = TRUE) /client/proc/GetOOCName() return key diff --git a/code/modules/jobs/job_types/bos.dm b/code/modules/jobs/job_types/bos.dm index 4ea7d5b8162..5aee2324a42 100644 --- a/code/modules/jobs/job_types/bos.dm +++ b/code/modules/jobs/job_types/bos.dm @@ -621,7 +621,10 @@ Senior Scribe H.mind.teach_crafting_recipe(/datum/crafting_recipe/bos_rca_convert) H.mind.teach_crafting_recipe(/datum/crafting_recipe/bos_riot_convert) H.mind.teach_crafting_recipe(/datum/crafting_recipe/bos_riot_helm_convert) - H.mind.teach_crafting_recipe(GLOB.chemwhiz_recipes) + if(!H.mind.learned_recipes) + H.mind.learned_recipes = list() + H.mind.learned_recipes |= GLOB.chemwhiz_recipes_basic + H.mind.learned_recipes |= GLOB.chemwhiz_recipes_advanced ADD_TRAIT(H, TRAIT_CHEMWHIZ, src) ADD_TRAIT(H, TRAIT_SURGERY_HIGH, src) ADD_TRAIT(H, TRAIT_CYBERNETICIST, src) @@ -721,7 +724,10 @@ Scribe H.mind.teach_crafting_recipe(/datum/crafting_recipe/bos_rca_convert) H.mind.teach_crafting_recipe(/datum/crafting_recipe/bos_riot_convert) H.mind.teach_crafting_recipe(/datum/crafting_recipe/bos_riot_helm_convert) - H.mind.teach_crafting_recipe(GLOB.chemwhiz_recipes) + if(!H.mind.learned_recipes) + H.mind.learned_recipes = list() + H.mind.learned_recipes |= GLOB.chemwhiz_recipes_basic + H.mind.learned_recipes |= GLOB.chemwhiz_recipes_advanced ADD_TRAIT(H, TRAIT_CHEMWHIZ, src) ADD_TRAIT(H, TRAIT_SURGERY_HIGH, src) ADD_TRAIT(H, TRAIT_CYBERNETICIST, src) diff --git a/code/modules/jobs/job_types/eastwood.dm b/code/modules/jobs/job_types/eastwood.dm index 9a7493c8d33..f386212e0cd 100644 --- a/code/modules/jobs/job_types/eastwood.dm +++ b/code/modules/jobs/job_types/eastwood.dm @@ -705,11 +705,15 @@ Mayor /obj/item/storage/firstaid/regular, /obj/item/clothing/accessory/armband/medblue ) + /datum/outfit/job/den/f13dendoc/post_equip(mob/living/carbon/human/H, visualsOnly = FALSE) ..() if(visualsOnly) return - H.mind.teach_crafting_recipe(GLOB.chemwhiz_recipes) + if(!H.mind.learned_recipes) + H.mind.learned_recipes = list() + H.mind.learned_recipes |= GLOB.chemwhiz_recipes_basic + H.mind.learned_recipes |= GLOB.chemwhiz_recipes_advanced H.mind.teach_crafting_recipe(/datum/crafting_recipe/pico_manip) H.mind.teach_crafting_recipe(/datum/crafting_recipe/super_matter_bin) H.mind.teach_crafting_recipe(/datum/crafting_recipe/phasic_scanning) diff --git a/code/modules/jobs/job_types/holiday.dm b/code/modules/jobs/job_types/holiday.dm index a15a9483665..a196b6f7dad 100644 --- a/code/modules/jobs/job_types/holiday.dm +++ b/code/modules/jobs/job_types/holiday.dm @@ -705,11 +705,15 @@ Mayor /obj/item/storage/firstaid/regular, /obj/item/clothing/accessory/armband/medblue ) + /datum/outfit/job/denholiday/f13dendoc/post_equip(mob/living/carbon/human/H, visualsOnly = FALSE) ..() if(visualsOnly) return - H.mind.teach_crafting_recipe(GLOB.chemwhiz_recipes) + if(!H.mind.learned_recipes) + H.mind.learned_recipes = list() + H.mind.learned_recipes |= GLOB.chemwhiz_recipes_basic + H.mind.learned_recipes |= GLOB.chemwhiz_recipes_advanced H.mind.teach_crafting_recipe(/datum/crafting_recipe/pico_manip) H.mind.teach_crafting_recipe(/datum/crafting_recipe/super_matter_bin) H.mind.teach_crafting_recipe(/datum/crafting_recipe/phasic_scanning) @@ -719,6 +723,7 @@ Mayor ADD_TRAIT(H, TRAIT_GENERIC, src) ADD_TRAIT(H, TRAIT_SURGERY_HIGH, src) + /datum/outfit/loadout/rescueranger name = "Search and Rescue" backpack_contents = list(/obj/item/clothing/head/f13/police/sergeant = 1, diff --git a/code/modules/keybindings/keybind/movement.dm b/code/modules/keybindings/keybind/movement.dm index 0f3199ef6a8..b409f1d7772 100644 --- a/code/modules/keybindings/keybind/movement.dm +++ b/code/modules/keybindings/keybind/movement.dm @@ -219,3 +219,19 @@ var/mob/M = user.mob M.tilt_left() return TRUE + +/datum/keybinding/mob/toggle_sneak + hotkey_keys = list("K") + name = "toggle_sneak" + full_name = "Toggle Sneak Mode" + description = "Enter/exit sneak mode. Shows enemy vision cones and reduces sound." + category = CATEGORY_MOVEMENT + +/datum/keybinding/mob/toggle_sneak/down(client/user) + . = ..() + if(.) + return + var/mob/living/carbon/human/H = user.mob + if(istype(H)) + H.toggle_sneak_mode() + return TRUE diff --git a/code/modules/lighting/lighting_setup.dm b/code/modules/lighting/lighting_setup.dm index 5086b0c9d29..136f953bf2a 100644 --- a/code/modules/lighting/lighting_setup.dm +++ b/code/modules/lighting/lighting_setup.dm @@ -10,4 +10,3 @@ new/atom/movable/lighting_object(T) CHECK_TICK - CHECK_TICK diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index bb06bf85bb1..cd1302a6fad 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -437,7 +437,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp // Check if mind.current exists and isn't being deleted if(!mind.current || QDELETED(mind.current)) - to_chat(src, span_warning("You have no body.")) + to_chat(src, span_warning("You have no body. Your corpse may have been destroyed, or you may be too far from respawn.")) return // Check if we can re-enter diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index e55e87ebd9b..a3018d8007b 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -495,6 +495,7 @@ W.plane = initial(W.plane) SetNextAction(0) update_equipment_speed_mods() // In case cuffs ever change speed + update_mobility() //update mobility flags so we can use items/move properly after removing restraints /mob/living/carbon/proc/clear_cuffs(obj/item/I, cuff_break) if(!I.loc || buckled) @@ -521,6 +522,7 @@ legcuffed = null I.dropped(src) update_inv_legcuffed() + update_mobility() //update mobility flags so we can use items/move properly after removing restraints return else dropItemToGround(I) @@ -960,6 +962,7 @@ update_action_buttons_icon() //some of our action buttons might be unusable when we're handcuffed. update_inv_handcuffed() update_hud_handcuffed() + update_mobility() //update mobility flags so we can use items again after breaking free /mob/living/carbon/proc/can_revive(ignore_timelimit = FALSE, maximum_brute_dam = MAX_REVIVE_BRUTE_DAMAGE, maximum_fire_dam = MAX_REVIVE_FIRE_DAMAGE, ignore_heart = FALSE) //var/tlimit = DEFIB_TIME_LIMIT * 10 diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 47d746ebb41..c5d76c3edc4 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -16,6 +16,13 @@ GLOBAL_VAR_INIT(crotch_call_cooldown, 0) var/movement_fatigue = 0 var/last_armor_warning_time = 0 COOLDOWN_DECLARE(movement_fatigue_recovery) + + // Sneaking mode system + var/sneaking = FALSE // Is the player in sneak mode? + var/mutable_appearance/sneak_indicator = null // Ninja icon overlay above head + var/list/visible_vision_cones = list() // List of vision cone overlays we're showing + var/vision_cone_timer = null // Timer for periodic cone updates + var/previous_move_intent = null // Saves movement speed before sneak mode /mob/living/carbon/human/Initialize() add_verb(src, /mob/living/proc/mob_sleep) @@ -82,22 +89,10 @@ GLOBAL_VAR_INIT(crotch_call_cooldown, 0) gloves = null shoes = null - var/result = ..() - QDEL_NULL(physiology) - if(client && client.screen && client.screen.len) - addtimer(CALLBACK(src, PROC_REF(defer_screen_cleanup)), 0, TIMER_DELETE_ME) - - return result + return ..() -/mob/living/carbon/human/proc/defer_screen_cleanup() - // Called next tick to avoid GC spike from screen deletion - // Clears all screen objects attached to this mob's client - if(!src || QDELETED(src) || !client) - return - if(client.screen && client.screen.len) - QDEL_LIST(client.screen) /mob/living/carbon/human/prepare_data_huds() //Update med hud images... @@ -278,6 +273,356 @@ GLOBAL_VAR_INIT(crotch_call_cooldown, 0) ghostize() qdel(src) +// Calculate movement sound level for rear detection by hostile mobs +// Returns a multiplier based on movement speed and armor weight +/mob/living/carbon/human/proc/get_movement_sound_level() + // Base sound level: 1.0 for walking, 2.0 for running + var/base_movement = 1.0 + if(m_intent == MOVE_INTENT_RUN) + base_movement = 2.0 + + // Calculate armor weight modifier from slowdown values + var/armor_slowdown = 0 + if(wear_suit) + armor_slowdown += wear_suit.slowdown + if(shoes) + armor_slowdown += shoes.slowdown + if(head) + armor_slowdown += head.slowdown + + // Light armor (low slowdown) makes you quieter, heavy armor makes you louder + // 0 slowdown = 0.5x (very quiet), 0.25 slowdown = 1.0x (normal), 0.5+ slowdown = increasingly loud + var/armor_multiplier = 0.5 + (armor_slowdown * 2) + + var/sound_level = base_movement * armor_multiplier + + // Sneaking no longer reduces sound - it only shows vision cones + + return sound_level + +// Toggle sneak mode when K is pressed +/mob/living/carbon/human/verb/toggle_sneak_mode() + set name = "Toggle Sneak" + set category = "IC" + set hidden = TRUE // Only accessible via keybind + + if(stat != CONSCIOUS) + return + + sneaking = !sneaking + + // Update assassination button availability + for(var/datum/action/cooldown/assassinate/A in actions) + A.UpdateButtonIcon() + + if(sneaking) + // Add movement slowdown (2x slower) + add_movespeed_modifier(/datum/movespeed_modifier/sneak_mode) + // Entered sneak mode + to_chat(src, span_notice("You enter sneak mode. Enemy vision cones are now visible.")) + + // Create ninja icon as overlay above head + if(!sneak_indicator) + var/obj/effect/overlay/sneak_icon/indicator_obj = new() + sneak_indicator = mutable_appearance(indicator_obj.icon, indicator_obj.icon_state, indicator_obj.layer) + sneak_indicator.alpha = 102 // 60% transparent + sneak_indicator.pixel_y = 20 // Above head + sneak_indicator.transform = matrix().Scale(0.7, 0.7) // 30% smaller + sneak_indicator.appearance_flags = RESET_COLOR | TILE_BOUND | PIXEL_SCALE + add_overlay(sneak_indicator) + + // Start showing vision cones + update_vision_cones() + + // Start periodic refresh timer (every 1 second) to catch mob deaths and lighting changes + vision_cone_timer = addtimer(CALLBACK(src, PROC_REF(update_vision_cones)), 10, TIMER_STOPPABLE | TIMER_LOOP) + + // Save current movement intent and auto-switch to walk if running + previous_move_intent = m_intent + if(m_intent == MOVE_INTENT_RUN) + toggle_move_intent() + to_chat(src, span_warning("You automatically switch to walking.")) + else + // Remove movement slowdown + remove_movespeed_modifier(/datum/movespeed_modifier/sneak_mode) + // Exited sneak mode + to_chat(src, span_notice("You exit sneak mode.")) + + // Restore previous movement intent if it was different + if(previous_move_intent && previous_move_intent != m_intent) + toggle_move_intent() + to_chat(src, span_notice("Movement speed restored to [previous_move_intent].")) + previous_move_intent = null + + // Stop periodic refresh timer + if(vision_cone_timer) + deltimer(vision_cone_timer) + vision_cone_timer = null + + // Remove ninja icon overlay + if(sneak_indicator) + cut_overlay(sneak_indicator) + sneak_indicator = null + + // Clear all vision cones + clear_vision_cones() + +// Force exit sneak mode (used when exhausted or other forced exit) +/mob/living/carbon/human/proc/force_exit_sneak_mode() + if(!sneaking) + return + + sneaking = FALSE + + // Update assassination button + for(var/datum/action/cooldown/assassinate/A in actions) + A.UpdateButtonIcon() + + // Remove movement slowdown + remove_movespeed_modifier(/datum/movespeed_modifier/sneak_mode) + + // Stop periodic refresh timer + if(vision_cone_timer) + deltimer(vision_cone_timer) + vision_cone_timer = null + + // Remove ninja icon overlay + if(sneak_indicator) + cut_overlay(sneak_indicator) + sneak_indicator = null + + // Clear all vision cones + clear_vision_cones() + + // Don't restore previous move intent when forced out + previous_move_intent = null + +// Clean up when player dies/disconnects +/mob/living/carbon/human/death() + . = ..( ) + if(sneaking) + force_exit_sneak_mode() + +// Update visible vision cones for nearby hostile mobs +// TWO-LAYER SYSTEM: +// Layer 1: ACTIVATION - Based on mob's base vision_range (ignores YOUR penalties) +// Layer 2: DISPLAY - Shows ACTUAL detection range (includes YOUR darkness/direction penalties) +/mob/living/carbon/human/proc/update_vision_cones() + if(!sneaking || !client) + return + + // Clear old cones + clear_vision_cones() + + // ACTIVATION RANGE: Find all hostile mobs within their BASE vision range + // This ignores YOUR darkness penalties - it's based on THEIR potential awareness + for(var/mob/living/simple_animal/hostile/H in view(15, src)) // Extended view to catch edge cases + if(H.stat == DEAD || H.ckey) + continue + + // Check if they're hostile to us + if(H.faction_check_mob(src)) + continue // Friendly, don't show cone + + // Hide cones if mob is actively targeting the player + if(H.target == src) + continue // Aggroed onto us, hide cone + + var/actual_distance = get_dist(src, H) + + // ACTIVATION TRIGGER: Use mob's BASE vision_range (9 tiles typically) + // This is their "awareness bubble" - you can see their cone when you're in it + // EVEN IF they can't see you yet due to darkness + var/activation_range = H.vision_range // Base range, no penalties + + // Also activate if we're within sound detection range + // Use the LARGER center range (3x multiplier) for activation + var/max_sound_range = 0 + if(last_move_time && (world.time - last_move_time) <= 20) + var/sound_cone = H.get_sound_cone(src) + if(sound_cone) + var/sound_level = get_movement_sound_level() + max_sound_range = round(sound_level * 3) // Use center multiplier (largest) + + // Show cone if within activation range OR sound range + if(actual_distance <= activation_range || (max_sound_range > 0 && actual_distance <= max_sound_range)) + show_vision_cone_for_mob(H) + +// Show vision cone with ACTUAL detection range (affected by darkness/direction/sound) +// DISPLAY LAYER: Shows how far they can ACTUALLY detect you +// PIE CHART SECTORS - Creates 6 clean non-overlapping 60° sectors +/mob/living/carbon/human/proc/show_vision_cone_for_mob(mob/living/simple_animal/hostile/H) + if(!H || !sneaking) + return + + if(H.stat == DEAD) + return + + var/mob_dir = H.dir + if(!mob_dir || mob_dir == 0) + return + + // Use the correct vision range function based on low-light vision + var/effective_range = 0 + if(H.has_low_light_vision) + effective_range = H.get_effective_vision_range_lowlight(src) + else + effective_range = H.get_effective_vision_range(src) + + var/actual_distance = get_dist(src, H) + var/your_cone = H.get_vision_cone(src) + var/currently_detected = (actual_distance <= effective_range && effective_range > 0) + + // Calculate BOTH sound ranges (center and peripheral have different multipliers) + var/sound_level = 0.0 + var/sound_center_range = 0 + var/sound_peripheral_range = 0 + + if(last_move_time && (world.time - last_move_time) <= 20) + // Get movement sound level + sound_level = get_movement_sound_level() + + // Always calculate ranges when moving (shows potential detection from any angle) + // The color/alpha logic will indicate whether detection is actually relevant + sound_center_range = round(sound_level * 3) // Rear center: sound travels clearly + sound_peripheral_range = round(sound_level * 2.5) // Rear peripheral: slightly muffled + + // ============================================ + // CENTERED SECTOR LAYOUT (6 slices, 60° each) + // ============================================ + // Sectors are CENTERED on cardinal directions: + // + // FRONT CENTER (RED): -30° to +30° from facing + // RIGHT PERIPHERAL (YELLOW): 30° to 90° + // RIGHT REAR (CYAN): 90° to 150° + // REAR CENTER (GRAY): 150° to 210° (±30° from opposite) + // LEFT REAR (CYAN): 210° to 270° + // LEFT PERIPHERAL (YELLOW): 270° to 330° + // ============================================ + + // Calculate ranges for each sector + var/front_range = H.vision_range + var/front_alpha = 100 // Medium opacity for vision detection + + if(your_cone == CONE_FRONT) + front_range = effective_range + if(currently_detected) + front_alpha = 180 // Brighten significantly when detected + else + front_alpha = 100 + else + var/turf/front_check = get_step(H, mob_dir) + if(front_check) + var/light_amount = front_check.get_lumcount() + if(light_amount >= 0.5) + front_range = H.vision_range + else if(light_amount >= 0.2) + front_range = max(round(H.vision_range * 0.6), 3) + else + front_range = max(round(H.vision_range * 0.4), 3) + + // Apply stealth boy penalty consistently with darkness + if(alpha < 100) // Cloaked - reduce range to 20% + front_range = max(round(front_range * 0.2), 1) + + front_alpha = 100 + + var/peripheral_range = max(round(front_range * 0.6), 2) + var/peripheral_alpha = 70 // Lower opacity than front cone + if(your_cone == CONE_PERIPHERAL) + peripheral_range = effective_range + if(currently_detected) + peripheral_alpha = 150 // Brighten when detected + else + peripheral_alpha = 70 + else + peripheral_alpha = 70 + + var/rear_peripheral_range = 3 + var/rear_peripheral_alpha = 60 // Light opacity for sound detection + + // Show sound detection ranges when moving (always show potential range for visibility) + // Use alpha to indicate whether ACTUALLY detected + // Use PERIPHERAL multiplier (2.5x) for cyan zones + if(sound_peripheral_range > 0) + rear_peripheral_range = sound_peripheral_range + var/sound_cone = H.get_sound_cone(src) + if(actual_distance <= sound_peripheral_range && sound_cone == SOUND_REAR_PERIPHERAL) + rear_peripheral_alpha = 130 // Brighten when detected in sound zone + else if(actual_distance <= sound_peripheral_range && sound_cone) + rear_peripheral_alpha = 100 // Within range but wrong zone + else + rear_peripheral_alpha = 60 // Light opacity base + + var/rear_range = 3 + var/rear_color = "#808080" // Gray for sound zones + var/rear_alpha = 60 // Light opacity for sound detection + + // Show sound detection ranges when moving (always show potential range for visibility) + // Use alpha to indicate whether ACTUALLY detected + // Use CENTER multiplier (3x) for gray zone - sound travels clearly from behind + if(sound_center_range > 0) + rear_range = sound_center_range + + // Keep gray color for sound zones (no color change needed) + rear_color = "#808080" + + // Alpha: Brightness indicates detection state + var/sound_cone = H.get_sound_cone(src) + if(actual_distance <= sound_center_range && sound_cone == SOUND_REAR_CENTER) + rear_alpha = 130 // Brighten when detected in sound zone + else if(actual_distance <= sound_center_range && sound_cone) + rear_alpha = 100 // Within range but wrong zone + else + rear_alpha = 60 // Light opacity base + + // Create 6 sectors, each 60° wide, CENTERED on cardinal/diagonal directions + // Each sector covers exactly 60° with NO overlap + // Vision zones use "red" sprite, sound zones use "blurry" sprite + + // FRONT CENTER (RED): -30° to +30° from facing (wraps around 0°) + create_sector(H, mob_dir, -30, 30, front_range, "#FF0000", front_alpha, "red") + + // RIGHT PERIPHERAL (YELLOW): 30° to 90° from facing + create_sector(H, mob_dir, 30, 90, peripheral_range, "#FFCC00", peripheral_alpha, "red") + + // RIGHT REAR (GRAY): 90° to 150° from facing - sound detection + create_sector(H, mob_dir, 90, 150, rear_peripheral_range, "#808080", rear_peripheral_alpha, "blurry") + + // REAR CENTER (GRAY): 150° to 210° from facing (±30° from opposite) - sound detection + create_sector(H, mob_dir, 150, 210, rear_range, rear_color, rear_alpha, "blurry") + + // LEFT REAR (GRAY): 210° to 270° from facing - sound detection + create_sector(H, mob_dir, 210, 270, rear_peripheral_range, "#808080", rear_peripheral_alpha, "blurry") + + // LEFT PERIPHERAL (YELLOW): 270° to 330° from facing + create_sector(H, mob_dir, 270, 330, peripheral_range, "#FFCC00", peripheral_alpha, "red") + +// Create a sector between two angles (in degrees, clockwise from facing direction) +// Angles can be negative (e.g., -30 to 30 for front-centered cone that wraps around 0°) +/mob/living/carbon/human/proc/create_sector(mob/living/simple_animal/hostile/H, base_dir, start_angle, end_angle, sector_range, sector_color, sector_alpha, icon_state_name = "white") + if(sector_range <= 0) + return + + var/obj/effect/overlay/vision_cone/cone = new /obj/effect/overlay/vision_cone(get_turf(H)) + cone.viewer = src + cone.setup_sector(H, base_dir, start_angle, end_angle, sector_range, icon_state_name) + cone.alpha = sector_alpha + + if(cone.generate_cone_image()) + for(var/image/img in cone.cone_images) + img.color = sector_color + img.alpha = sector_alpha + + visible_vision_cones += cone + +// Clear all vision cone overlays +/mob/living/carbon/human/proc/clear_vision_cones() + for(var/obj/effect/overlay/vision_cone/cone in visible_vision_cones) + // Tiles are deleted when the cone is deleted + qdel(cone) + visible_vision_cones.Cut() + /mob/living/carbon/human/Topic(href, href_list) if(usr.canUseTopic(src, BE_CLOSE, NO_DEXTERY)) if(href_list["embedded_object"]) diff --git a/code/modules/mob/living/carbon/human/human_movement.dm b/code/modules/mob/living/carbon/human/human_movement.dm index 082318132e3..61770079b9e 100644 --- a/code/modules/mob/living/carbon/human/human_movement.dm +++ b/code/modules/mob/living/carbon/human/human_movement.dm @@ -1,3 +1,7 @@ +// Sneak mode movement slowdown - makes player move 2x slower +/datum/movespeed_modifier/sneak_mode + multiplicative_slowdown = 1 // 2x slower (base movement + 1 = 2x the delay) + /mob/living/carbon/human/get_movespeed_modifiers() var/list/considering = ..() if(HAS_TRAIT(src, TRAIT_IGNORESLOWDOWN)) @@ -149,6 +153,26 @@ for(var/obj/item/I in held_items) accident(I) DefaultCombatKnockdown(80) + + // Update vision cones and drain stamina when player moves in sneak mode + if(. && sneaking) + update_vision_cones() // Update vision cones when we move + // Drain sprint buffer while sneaking and moving + if(has_gravity(loc) && CHECK_ALL_MOBILITY(src, MOBILITY_MOVE|MOBILITY_STAND)) + doSprintBufferRegen(FALSE) // Reset idle timer + sprint_idle_time = 0 + var/sneak_cost = 0.5 // 0.5 tiles of sprint buffer per tile moved + sprint_buffer = max(0, sprint_buffer - sneak_cost) + update_hud_sprint_bar() // Update sprint bar display + + // Force out of sneak mode if sprint buffer depleted + if(sprint_buffer <= 0) + force_exit_sneak_mode() + to_chat(src, span_warning("You're too exhausted to continue sneaking!")) + // Update assassination button availability (position changed) + for(var/datum/action/cooldown/assassinate/A in actions) + A.UpdateButtonIcon() + if(shoes) if(!lying && !buckled) if(loc == NewLoc) diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm index 2f290fb3006..d80f0ada26c 100644 --- a/code/modules/mob/living/carbon/life.dm +++ b/code/modules/mob/living/carbon/life.dm @@ -17,7 +17,14 @@ if(bprv & BODYPART_LIFE_UPDATE_HEALTH) updatehealth() update_stamina() - doSprintBufferRegen() + + // Don't regenerate sprint buffer while sneaking + if(ishuman(src)) + var/mob/living/carbon/human/H = src + if(!H.sneaking) + doSprintBufferRegen() + else + doSprintBufferRegen() if(stat != DEAD) handle_brain_damage() @@ -503,7 +510,12 @@ GLOBAL_LIST_INIT(ballmer_windows_me_msg, list("Yo man, what if, we like, uh, put //this updates all special effects: stun, sleeping, knockdown, druggy, stuttering, etc.. /mob/living/carbon/handle_status_effects() ..() - if(getStaminaLoss()) //CIT CHANGE - prevents stamina regen while combat mode is active (Stam regen is currently enabled) + // Don't regenerate stamina while sneaking + if(ishuman(src)) + var/mob/living/carbon/human/H = src + if(!H.sneaking && getStaminaLoss()) + adjustStaminaLoss(!CHECK_MOBILITY(src, MOBILITY_STAND) ? ((combat_flags & COMBAT_FLAG_HARD_STAMCRIT) ? STAM_RECOVERY_STAM_CRIT : STAM_RECOVERY_RESTING) : STAM_RECOVERY_NORMAL) + else if(getStaminaLoss()) //CIT CHANGE - prevents stamina regen while combat mode is active (Stam regen is currently enabled) adjustStaminaLoss(!CHECK_MOBILITY(src, MOBILITY_STAND) ? ((combat_flags & COMBAT_FLAG_HARD_STAMCRIT) ? STAM_RECOVERY_STAM_CRIT : STAM_RECOVERY_RESTING) : STAM_RECOVERY_NORMAL) if(!(combat_flags & COMBAT_FLAG_HARD_STAMCRIT) && incomingstammult != 1) diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm index d197898f6db..5cd97dde54c 100644 --- a/code/modules/mob/living/say.dm +++ b/code/modules/mob/living/say.dm @@ -75,7 +75,10 @@ return if(stat == DEAD) - say_dead(original_message) + // Only route to dead chat if the mob has a player (ckey) + // NPCs should not broadcast to dead chat + if(ckey) + say_dead(original_message) return if(check_emote(original_message, just_runechat = just_chat) || !can_speak_basic(original_message, ignore_spam)) diff --git a/code/modules/mob/living/simple_animal/friendly/farm_animals.dm b/code/modules/mob/living/simple_animal/friendly/farm_animals.dm index a94c95d381e..bd7db98c0fc 100644 --- a/code/modules/mob/living/simple_animal/friendly/farm_animals.dm +++ b/code/modules/mob/living/simple_animal/friendly/farm_animals.dm @@ -436,7 +436,7 @@ // Flee like the bucking code spawn(0) - for(var/i in 1 to rand(6,10)) + for(var/i in 1 to rand(9,13)) if(stat) break var/flee_dir = pick(NORTH, SOUTH, EAST, WEST, NORTHEAST, NORTHWEST, SOUTHEAST, SOUTHWEST) @@ -1580,7 +1580,7 @@ ride_move_delay = 2.5 young_type = /mob/living/simple_animal/cow/brahmin/nightstalker food_types = list( - /obj/item/reagent_containers/food/snacks/meat/slab/gecko, + /obj/item/reagent_containers/food/snacks/meat/slab, /obj/item/reagent_containers/food/snacks/f13/canned/dog ) milk_reagent = /datum/reagent/toxin diff --git a/code/modules/mob/living/simple_animal/hostile/bear.dm b/code/modules/mob/living/simple_animal/hostile/bear.dm index 1c3be25596b..85126bf9405 100644 --- a/code/modules/mob/living/simple_animal/hostile/bear.dm +++ b/code/modules/mob/living/simple_animal/hostile/bear.dm @@ -177,6 +177,9 @@ mob/living/simple_animal/hostile/bear/butter/AttackingTarget() //Makes some atta wound_bonus = 5 bare_wound_bonus = 25 faction = list("yaoguai") + + // Mutant bear with enhanced night vision + has_low_light_vision = TRUE /*mob/living/simple_animal/hostile/bear/yaoguai/frozen name = "frozen yao guai" diff --git a/code/modules/mob/living/simple_animal/hostile/bees.dm b/code/modules/mob/living/simple_animal/hostile/bees.dm index fa6dd34db6d..ea3a6542629 100644 --- a/code/modules/mob/living/simple_animal/hostile/bees.dm +++ b/code/modules/mob/living/simple_animal/hostile/bees.dm @@ -311,7 +311,7 @@ /mob/living/simple_animal/hostile/poison/bees/short/Initialize() . = ..() - addtimer(CALLBACK(src, PROC_REF(death)), 50 SECONDS) + addtimer(CALLBACK(src, PROC_REF(death)), 50 SECONDS, TIMER_DELETE_ME) /mob/living/simple_animal/hostile/poison/bees/short/frenly //these bees need to be frenly or they'd murder everyone faction = list("neutral") diff --git a/code/modules/mob/living/simple_animal/hostile/f13/Securitron.dm b/code/modules/mob/living/simple_animal/hostile/f13/Securitron.dm index cefca9ac114..77ab8d33a73 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/Securitron.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/Securitron.dm @@ -1,9 +1,10 @@ -/* IN THIS FILE: --Securitron --Sentry Bot -*/ +// In this document: Securitron, Sentry Bot, and variants -//Securitron TV Head jackass +///////////////// +// SECURITRON // +///////////////// + +// BASE SECURITRON - TV-head wheeled robot, ranged burst fire /mob/living/simple_animal/hostile/securitron name = "securitron" desc = "A pre-War type of securitron.
Extremely dangerous machine." @@ -11,62 +12,86 @@ icon_state = "securitron" icon_living = "securitron" icon_dead = "securitron_dead" + + mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC mob_armor = ARMOR_VALUE_ROBOT_SECURITY - maxHealth = 100 + + maxHealth = 100 health = 100 - sentience_type = SENTIENCE_BOSS + move_to_delay = 2.75 + turns_per_move = 5 stamcrit_threshold = SIMPLEMOB_NO_STAMCRIT + sentience_type = SENTIENCE_BOSS + + melee_damage_lower = 5 + melee_damage_upper = 10 + harm_intent_damage = 8 + + aggro_vision_range = 7 + vision_range = 8 + robust_searching = TRUE + + response_help_simple = "pokes" + response_disarm_simple = "shoves" + response_harm_simple = "hits" + attack_verb_simple = "punches" + attack_sound = 'sound/weapons/punch1.ogg' + + speak = list("Stop Right There Criminal.") + speak_chance = 1 + emote_hear = list("Beeps.") + emote_taunt = list("readies its arm gun") + taunt_chance = 30 + emp_flags = list( MOB_EMP_STUN, MOB_EMP_BERSERK, MOB_EMP_DAMAGE, MOB_EMP_SCRAMBLE - ) - speak_chance = 1 - turns_per_move = 5 - environment_smash = 0 - response_help_simple = "pokes" - response_disarm_simple = "shoves" - response_harm_simple = "hits" - robust_searching = TRUE - blood_volume = 0 + ) + + damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) + atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) + + faction = list("wastebot") + a_intent = INTENT_HARM + check_friendly_fire = TRUE del_on_death = TRUE healable = FALSE - faction = list("wastebot") - mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC - move_to_delay = 3.25 - // m2d 3 = standard, less is fast, more is slower. - - retreat_distance = 2 - //how far they pull back + blood_volume = 0 - minimum_distance = 5 - // how close you can get before they try to pull back - - aggro_vision_range = 7 - //tiles within they start attacking, doesn't count the mobs tile - - vision_range = 8 - //tiles within they start making noise, does count the mobs tile + move_resist = MOVE_FORCE_OVERPOWERING + environment_smash = ENVIRONMENT_SMASH_NONE + + loot = list( + /obj/effect/decal/cleanable/robot_debris, + /obj/item/stack/crafting/electronicparts/three + ) + + can_ghost_into = TRUE + pop_required_to_jump_into = MED_MOB_MIN_PLAYERS + + // Z-movement - wheels, no ladders + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 30 - emote_hear = list("Beeps.") - speak = list("Stop Right There Criminal.") - harm_intent_damage = 8 - melee_damage_lower = 5 - melee_damage_upper = 10 - extra_projectiles = 2 - auto_fire_delay = GUN_AUTOFIRE_DELAY_SLOW + can_open_doors = TRUE + can_open_airlocks = TRUE + + // Pure ranged - burst fire pistol + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 5 + minimum_distance = 1 + ranged_ignores_vision = TRUE - attack_verb_simple = "punches" - attack_sound = "punch" - a_intent = "harm" - atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) + auto_fire_delay = GUN_AUTOFIRE_DELAY_SLOW + extra_projectiles = 2 projectiletype = /obj/item/projectile/bullet/c9mm/simple projectilesound = 'sound/f13weapons/varmint_rifle.ogg' - emote_taunt = list("readies its arm gun") - check_friendly_fire = TRUE - ranged = TRUE - move_resist = MOVE_FORCE_OVERPOWERING projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(PISTOL_LIGHT_VOLUME), @@ -78,24 +103,38 @@ SP_DISTANT_RANGE(PISTOL_LIGHT_RANGE_DISTANT) ) - can_z_move = TRUE - z_move_delay = 30 +/mob/living/simple_animal/hostile/securitron/Aggro() + . = ..() + if(.) + return + summon_backup(10) -/mob/living/simple_animal/hostile/securitron/nsb //NSB + Raider Bunker specific - name = "Securitron" - faction = list("raider") - obj_damage = 300 - retreat_distance = 0 //perish, mortal +/mob/living/simple_animal/hostile/securitron/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() +// Friendly fire resistance + defensive grenade release /mob/living/simple_animal/hostile/securitron/bullet_act(obj/item/projectile/Proj) if(!Proj) CRASH("[src] securitron invoked bullet_act() without a projectile") + + if(Proj.firer && istype(Proj.firer, /mob/living/simple_animal/hostile/securitron)) + var/original_damage = Proj.damage + Proj.damage *= 0.2 + . = ..() + Proj.damage = original_damage + return + + // Chance to release defensive grenade when shot if(prob(5) && health > 1) var/flashbang_turf = get_turf(src) if(!flashbang_turf) - return + return ..() var/obj/item/grenade/S - switch(rand(1,10)) + switch(rand(1, 10)) if(1) S = new /obj/item/grenade/flashbang/sentry(flashbang_turf) if(2) @@ -104,14 +143,16 @@ S = new /obj/item/grenade/smokebomb(flashbang_turf) visible_message(span_danger("\The [src] releases a defensive [S]!")) S.preprime(user = null) - ..() + + return ..() +// Death sequence - beeps then explodes /mob/living/simple_animal/hostile/securitron/proc/do_death_beep() playsound(src, 'sound/machines/triple_beep.ogg', 75, TRUE) visible_message(span_warning("You hear an ominous beep coming from [src]!"), span_warning("You hear an ominous beep!")) /mob/living/simple_animal/hostile/securitron/proc/self_destruct() - explosion(src,1,2,4,4) + explosion(src, 1, 2, 4, 4) /mob/living/simple_animal/hostile/securitron/death() do_sparks(3, TRUE, src) @@ -120,32 +161,40 @@ addtimer(CALLBACK(src, PROC_REF(self_destruct)), 4 SECONDS) return ..() -/mob/living/simple_animal/hostile/securitron/Aggro() - . = ..() - summon_backup(15) +// NSB SECURITRON - raider bunker variant, doesn't retreat +/mob/living/simple_animal/hostile/securitron/nsb + name = "Securitron" + faction = list("raider") + obj_damage = 300 + can_ghost_into = FALSE + + retreat_distance = null // Perish, mortal + +///////////////// +// SENTRY BOT // +///////////////// -//Sentry Bot +// BASE SENTRY BOT - heavy gatling laser platform /mob/living/simple_animal/hostile/securitron/sentrybot name = "sentry bot" desc = "A pre-war military robot armed with a deadly gatling laser and covered in thick armor plating." icon_state = "sentrybot" icon_living = "sentrybot" icon_dead = "sentrybot_dead" - mob_armor = ARMOR_VALUE_ROBOT_SECURITY - maxHealth = 150 + + mob_armor = ARMOR_VALUE_ROBOT_MILITARY + + maxHealth = 150 health = 150 stat_attack = UNCONSCIOUS - del_on_death = FALSE + del_on_death = FALSE // Explodes instead + melee_damage_lower = 24 melee_damage_upper = 55 - extra_projectiles = 2 //5 projectiles - ranged_cooldown_time = 40 //brrrrrrrrrrrrt - retreat_distance = 5 - minimum_distance = 5 // SENTRY bot, not run up to your face and magdump you bot attack_verb_simple = "pulverizes" attack_sound = 'sound/weapons/punch1.ogg' - projectilesound = 'sound/weapons/laser.ogg' - projectiletype = /obj/item/projectile/beam/laser/pistol/ultraweak + + emote_taunt = list("spins its barrels") emote_taunt_sound = list( 'sound/f13npc/sentry/taunt1.ogg', 'sound/f13npc/sentry/taunt2.ogg', @@ -153,27 +202,38 @@ 'sound/f13npc/sentry/taunt4.ogg', 'sound/f13npc/sentry/taunt5.ogg', 'sound/f13npc/sentry/taunt6.ogg' - ) - emote_taunt = list("spins its barrels") + ) aggrosound = list( 'sound/f13npc/sentry/aggro1.ogg', 'sound/f13npc/sentry/aggro2.ogg', 'sound/f13npc/sentry/aggro3.ogg', 'sound/f13npc/sentry/aggro4.ogg', 'sound/f13npc/sentry/aggro5.ogg' - ) + ) idlesound = list( 'sound/f13npc/sentry/idle1.ogg', 'sound/f13npc/sentry/idle2.ogg', 'sound/f13npc/sentry/idle3.ogg', 'sound/f13npc/sentry/idle4.ogg' - ) - var/warned = FALSE + ) + loot = list( /obj/effect/decal/cleanable/robot_debris, /obj/item/stack/crafting/electronicparts/five, /obj/item/stock_parts/cell/ammo/mfc - ) + ) + + pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS + + // Pure ranged - gatling laser + combat_mode = COMBAT_MODE_RANGED + retreat_distance = 5 + minimum_distance = 1 + + ranged_cooldown_time = 40 // Brrrrrrrt + extra_projectiles = 2 // 3 shots per burst + projectiletype = /obj/item/projectile/beam/laser/pistol/ultraweak + projectilesound = 'sound/weapons/laser.ogg' projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(LASER_VOLUME), @@ -184,63 +244,31 @@ SP_DISTANT_SOUND(LASER_DISTANT_SOUND), SP_DISTANT_RANGE(LASER_RANGE_DISTANT) ) + + var/warned = FALSE // For low health warning sound /mob/living/simple_animal/hostile/securitron/sentrybot/Life() - ..() - if (!warned) - if (health <= 50) - warned = TRUE - playsound(src, 'sound/f13npc/sentry/systemfailure.ogg', 75, FALSE) - -// Lil chew-chew -/mob/living/simple_animal/hostile/securitron/sentrybot/chew - name = "lil' chew-chew" - desc = "An oddly scorched pre-war military robot armed with a deadly gatling laser and covered in thick, oddly blue armor plating, the name Lil' Chew-Chew scratched onto it's front armour crudely, highlighted by small bits of white paint. There seems to be an odd pack on the monstrosity of a sentrie's back, a chute at the bottom of it - there's the most scorch-marks on the robot here, so it's safe to assume this robot is capable of explosions. Better watch out!" - extra_projectiles = 6 - health = 1000 - maxHealth = 1000 //CHONK - obj_damage = 300 - retreat_distance = 0 - environment_smash = ENVIRONMENT_SMASH_RWALLS //wall-obliterator. perish. - color = "#75FFE2" - aggro_vision_range = 15 - flags_1 = PREVENT_CONTENTS_EXPLOSION_1 //cannot self-harm with it's explosion spam - -/mob/living/simple_animal/hostile/securitron/sentrybot/chew/bullet_act(obj/item/projectile/Proj) - if(!Proj) - CRASH("[src] sentrybot invoked bullet_act() without a projectile") - if(prob(10) && health > 1) - visible_message(span_danger("\The [src] releases a defensive explosive!")) - explosion(get_turf(src),-1,-1,2, flame_range = 4) //perish, mortal - explosion size identical to craftable IED - ..() - -/mob/living/simple_animal/hostile/securitron/sentrybot/chew/strong //For use as a main bunker boss. Essentially a mildly edited OverSEER port - name = "big chew-chew" - desc = "An oddly scorched pre-war military robot armed with a deadly gatling laser firing high-penetration experimental lasers and covered in thick, dark blue armor plating, the name Big Chew-Chew scratched onto it's front armour crudely, highlighted by small bits of white paint. There seems to be an odd pack on the monstrosity of a sentrie's back, a chute at the bottom of it - there's the most scorch-marks on the robot here, so it's safe to assume this robot is capable of explosions. Better watch out!" - extra_projectiles = 4 //Fires a bit less - health = 1500 //more HP than its smaller brother - maxHealth = 1500 - armour_penetration = 1 //Punches harder - move_to_delay = 3.0 //Is deceptively quick - retreat_distance = 0 //Is going to punch you - rapid_melee = 2 //Punches faster - color = "#3444C8" //dark blue - emp_flags = list() //no emp instakill for you - projectiletype = /obj/item/projectile/beam/laser/pistol/ultraweak/chew/strong + . = ..() + if(!warned && health <= 50) + warned = TRUE + playsound(src, 'sound/f13npc/sentry/systemfailure.ogg', 75, FALSE) -//Raider friendly Sentry bot +// NSB SENTRY BOT - raider faction /mob/living/simple_animal/hostile/securitron/sentrybot/nsb name = "sentry bot" obj_damage = 300 + can_ghost_into = FALSE -//Raider friendly Sentry bot with non-lethals -/mob/living/simple_animal/hostile/securitron/sentrybot/nsb/riot //NSB + Raider Bunker specific. +// NSB RIOT SENTRY - non-lethal breacher +/mob/living/simple_animal/hostile/securitron/sentrybot/nsb/riot name = "riot-control sentry bot" desc = "A pre-war military robot armed with a modified breacher shotgun and covered in thick armor plating." - projectilesound = 'sound/f13weapons/riot_shotgun.ogg' - projectiletype = /obj/item/projectile/bullet/shotgun_beanbag - retreat_distance = 0 + + retreat_distance = null // Gets right in your face extra_projectiles = 0 + + projectiletype = /obj/item/projectile/bullet/shotgun_beanbag + projectilesound = 'sound/f13weapons/riot_shotgun.ogg' projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(SHOTGUN_VOLUME), @@ -252,36 +280,87 @@ SP_DISTANT_RANGE(SHOTGUN_RANGE_DISTANT) ) -//Playable Sentrybot +// PLAYABLE SENTRY BOT /mob/living/simple_animal/hostile/securitron/sentrybot/playable - health = 50 //El Beef maxHealth = 50 + health = 50 speed = 1 attack_verb_simple = "clamps" + see_in_dark = 8 + environment_smash = ENVIRONMENT_SMASH_RWALLS + wander = FALSE + force_threshold = 15 + anchored = FALSE + del_on_death = FALSE + ranged = FALSE emote_taunt_sound = null emote_taunt = null aggrosound = null idlesound = null - ranged = FALSE - see_in_dark = 8 - environment_smash = 2 //can smash walls - wander = 0 - force_threshold = 15 -/mob/living/simple_animal/hostile/securitron/sentrybot/playable/death() +// LIL' CHEW-CHEW - unique named boss +/mob/living/simple_animal/hostile/securitron/sentrybot/chew + name = "lil' chew-chew" + desc = "An oddly scorched pre-war military robot armed with a deadly gatling laser and covered in thick, oddly blue armor plating, the name Lil' Chew-Chew scratched onto its front armour crudely, highlighted by small bits of white paint. There seems to be an odd pack on the monstrosity's back with a chute at the bottom - there's the most scorch-marks on the robot here, so it's safe to assume this robot is capable of explosions. Better watch out!" + + mob_armor = ARMOR_VALUE_ROBOT_MILITARY_HEAVY + + maxHealth = 1000 + health = 1000 + obj_damage = 300 + extra_projectiles = 6 + + retreat_distance = null // Perish, mortal + environment_smash = ENVIRONMENT_SMASH_RWALLS + + color = "#75FFE2" + aggro_vision_range = 15 + can_ghost_into = FALSE + + flags_1 = PREVENT_CONTENTS_EXPLOSION_1 // Can't self-harm with explosion spam + +/mob/living/simple_animal/hostile/securitron/sentrybot/chew/bullet_act(obj/item/projectile/Proj) + if(!Proj) + CRASH("[src] sentrybot invoked bullet_act() without a projectile") + if(prob(10) && health > 1) + visible_message(span_danger("\The [src] releases a defensive explosive!")) + explosion(get_turf(src), -1, -1, 2, flame_range = 4) return ..() -//Junkers +// BIG CHEW-CHEW - bunker boss variant +/mob/living/simple_animal/hostile/securitron/sentrybot/chew/strong + name = "big chew-chew" + desc = "An oddly scorched pre-war military robot armed with a deadly gatling laser firing high-penetration experimental lasers and covered in thick, dark blue armor plating, the name Big Chew-Chew scratched onto its front armour crudely, highlighted by small bits of white paint. There seems to be an odd pack on the monstrosity's back with a chute at the bottom - it's safe to assume this robot is capable of explosions. Better watch out!" + + maxHealth = 1500 + health = 1500 + extra_projectiles = 4 // Fires a bit less than lil chew-chew + armour_penetration = 1 + move_to_delay = 2.75 + rapid_melee = 2 + + retreat_distance = null // Is going to punch you + + color = "#3444C8" // Dark blue + + emp_flags = list() // No EMP instakill + projectiletype = /obj/item/projectile/beam/laser/pistol/ultraweak/chew/strong + +// SUICIDE SENTRY - rushes in and explodes /mob/living/simple_animal/hostile/securitron/sentrybot/suicide name = "explosive sentry bot" desc = "A pre-war military robot armed with a deadly gatling laser and covered in thick armor plating. Don't get too close to this one, it looks like it's rigged to blow!" + maxHealth = 160 health = 160 color = "#B85C00" + + // Rushes target retreat_distance = null minimum_distance = 1 /mob/living/simple_animal/hostile/securitron/sentrybot/suicide/AttackingTarget() + . = ..() if(ishuman(target)) addtimer(CALLBACK(src, PROC_REF(do_death_beep)), 1 SECONDS) addtimer(CALLBACK(src, PROC_REF(self_destruct)), 2 SECONDS) diff --git a/code/modules/mob/living/simple_animal/hostile/f13/centaur.dm b/code/modules/mob/living/simple_animal/hostile/f13/centaur.dm index 2e296230117..bcad5e9cd15 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/centaur.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/centaur.dm @@ -1,8 +1,10 @@ -// In this document: Freaks, the centaur, abomination and horror. +// In this document: Freaks - Centaur, Abomination, Horror -// ------------------------------------- -// CENTAUR +////////////// +// CENTAUR // +////////////// +// BASE CENTAUR - FEV mutation, ranged poison spitter /mob/living/simple_animal/hostile/centaur name = "centaur" desc = "The result of infection by FEV gone horribly wrong." @@ -11,121 +13,228 @@ icon_living = "centaur" icon_dead = "centaur_dead" icon_gib = "centaur_g" - tastes = list("sadness" = 1, "nastyness" = 1) - can_ghost_into = TRUE + + mob_biotypes = MOB_ORGANIC|MOB_HUMANOID + mob_armor = ARMOR_VALUE_CENTAUR + maxHealth = 80 - sentience_type = SENTIENCE_BOSS health = 80 speed = 2 + move_to_delay = 4 // Slower than average + turns_per_move = 5 + + melee_damage_lower = 4 + melee_damage_upper = 20 harm_intent_damage = 8 - melee_damage_lower = 4 // damage range is punch min, average is 15 when in melee - melee_damage_upper = 20 - ranged = TRUE wound_bonus = 0 - footstep_type = FOOTSTEP_MOB_CRAWL - - mob_biotypes = MOB_ORGANIC|MOB_HUMANOID + + aggro_vision_range = 7 + vision_range = 7 robust_searching = TRUE - move_to_delay = 6 //slower than average, but not a lot. //Needs to be slower than a protectron - // m2d 4 = standard, less is fast, more is slower. - - retreat_distance = 0 // Mob doesn't retreat - //how far they pull back + stat_attack = CONSCIOUS + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/human/centaur = 3, + /obj/item/stack/sheet/animalhide/human = 2, + /obj/item/stack/sheet/bone = 2 + ) + + footstep_type = FOOTSTEP_MOB_CRAWL + tastes = list("sadness" = 1, "nastyness" = 1) - minimum_distance = 0 //Mob pushes up to melee, leading with its ranged attacks to soften up player. - // how close you can get before they try to pull back - - aggro_vision_range = 7 //Will start attacking within player sight, but gives wiggle room to avoid if moving slow and carefully - //tiles within they start attacking - - vision_range = 7 //will start attacking within player sight, but like aggro gives wiggle room. So they just don't see players outside of 7 tiles and start screeching. - //tiles within they start making noise - turns_per_move = 5 speak_emote = list("growls") emote_see = list("screeches", "screams", "howls", "bellows", "flails", "fidgets", "festers") - a_intent = INTENT_HARM attack_verb_simple = list("whipped", "whacked", "whomped", "wailed on", "smacked", "smashed", "bapped") + attack_sound = 'sound/f13npc/centaur/lash.ogg' + + emote_taunt = list("grunts", "gurgles", "wheezes", "flops", "scrabbles") + emote_taunt_sound = list('sound/f13npc/centaur/taunt.ogg') + taunt_chance = 30 + aggrosound = list('sound/f13npc/centaur/aggro1.ogg') + idlesound = list('sound/f13npc/centaur/idle1.ogg', 'sound/f13npc/centaur/idle2.ogg') + death_sound = 'sound/f13npc/centaur/centaur_death.ogg' + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) unsuitable_atmos_damage = 20 - stat_attack = CONSCIOUS - gold_core_spawnable = HOSTILE_SPAWN + faction = list("hostile", "supermutant") - guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/human/centaur = 3, - /obj/item/stack/sheet/animalhide/human = 2, - /obj/item/stack/sheet/bone = 2) + a_intent = INTENT_HARM + gold_core_spawnable = HOSTILE_SPAWN + sentience_type = SENTIENCE_BOSS + + can_ghost_into = TRUE + pop_required_to_jump_into = MED_MOB_MIN_PLAYERS + + // Z-movement + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 50 + + can_open_doors = FALSE + can_open_airlocks = FALSE + + // Mixed combat - poison spit + melee + combat_mode = COMBAT_MODE_MIXED + ranged = TRUE + retreat_distance = 3 + minimum_distance = 1 + + ranged_message = "spits poison" + ranged_cooldown_time = 3 SECONDS projectiletype = /obj/item/projectile/neurotox projectilesound = 'sound/f13npc/centaur/spit.ogg' - emote_taunt_sound = list('sound/f13npc/centaur/taunt.ogg') - emote_taunt = list("grunts", "gurgles", "wheezes", "flops", "scrabbles") - taunt_chance = 30 - aggrosound = list('sound/f13npc/centaur/aggro1.ogg', ) - idlesound = list('sound/f13npc/centaur/idle1.ogg', 'sound/f13npc/centaur/idle2.ogg') - death_sound = 'sound/f13npc/centaur/centaur_death.ogg' - attack_sound = 'sound/f13npc/centaur/lash.ogg' +/mob/living/simple_animal/hostile/centaur/Aggro() + . = ..() + if(.) + return + summon_backup(10) + +/mob/living/simple_animal/hostile/centaur/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + +// Friendly fire resistance - centaurs fight together +/mob/living/simple_animal/hostile/centaur/bullet_act(obj/item/projectile/P) + if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/centaur)) + // Friendly fire from another centaur - take only 20% damage + var/original_damage = P.damage + P.damage *= 0.2 + . = ..() + P.damage = original_damage + return + return ..() + +// NEUROTOXIN PROJECTILE /obj/item/projectile/neurotox - name = "spitball" + name = "poison spit" damage = 25 icon_state = "toxin" -/mob/living/simple_animal/hostile/centaur/strong // Mostly for FEV mutation +/obj/item/projectile/neurotox/on_hit(atom/target) + . = ..() + if(iscarbon(target)) + var/mob/living/carbon/M = target + M.reagents.add_reagent(/datum/reagent/toxin, 3) + +// STRONG CENTAUR - for FEV mutation event +/mob/living/simple_animal/hostile/centaur/strong maxHealth = 400 health = 400 stat_attack = UNCONSCIOUS + melee_damage_lower = 35 melee_damage_upper = 35 armour_penetration = 0.1 + + pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS -/mob/living/simple_animal/pet/dog/centaur //Cutie +// DOUG - friendly pet centaur +/mob/living/simple_animal/pet/dog/centaur name = "Doug" - desc = "A docile centaur. Was brought here along with the warband, Isn't he adorable?" + desc = "A docile centaur. Was brought here along with the warband. Isn't he adorable?" icon = 'icons/fallout/mobs/monsters/freaks.dmi' icon_state = "centaur" icon_living = "centaur" icon_dead = "centaur_dead" icon_gib = "centaur_g" + maxHealth = 200 health = 200 turns_per_move = 5 + speak_emote = list("growls") emote_see = list("screeches", "screams", "howls", "bellows", "flails", "fidgets", "festers") + idlesound = list('sound/f13npc/centaur/idle1.ogg', 'sound/f13npc/centaur/idle2.ogg') death_sound = 'sound/f13npc/centaur/centaur_death.ogg' + response_help_simple = "pet" response_disarm_simple = "push" response_harm_simple = "punch" +///////////////// +// ABOMINATION // +///////////////// -// ----------------------------------- -// ABOMINATION - +// BASE ABOMINATION - FEV nightmare, wall smasher /mob/living/simple_animal/hostile/abomination name = "abomination" - desc = "A horrible fusion of man, animal, and something entirely different. It quakes and shudders, looking to be in an immense amount of pain. Blood and other fluids ooze from various gashes and lacerations on its body, punctuated by mouths that gnash and scream." + desc = "A horrible fusion of man, animal, and something entirely different. It quakes and shudders, looking to be in an immense amount of pain. Blood and other fluids oo ze from various gashes and lacerations on its body, punctuated by mouths that gnash and scream." icon = 'icons/fallout/mobs/monsters/freaks.dmi' icon_state = "abomination" icon_living = "abomination" icon_dead = "abomination_dead" - + + mob_biotypes = MOB_ORGANIC|MOB_HUMANOID + mob_armor = ARMOR_VALUE_ABOMINATION + maxHealth = 1000 health = 1000 + speed = -0.5 // Fast stat_attack = UNCONSCIOUS - speed = -0.5 - harm_intent_damage = 8 + melee_damage_lower = 30 melee_damage_upper = 40 + harm_intent_damage = 8 armour_penetration = 0.1 - - mob_biotypes = MOB_ORGANIC|MOB_HUMANOID + environment_smash = ENVIRONMENT_SMASH_RWALLS robust_searching = TRUE + + speak_emote = list("screams", "clicks", "chitters", "barks", "moans", "growls", "meows", "reverberates", "roars", "squeaks", "rattles", "exclaims", "yells", "remarks", "mumbles", "jabbers", "stutters", "seethes") attack_verb_simple = "eviscerates" attack_sound = 'sound/weapons/bladeslice.ogg' - speak_emote = list("screams", "clicks", "chitters", "barks", "moans", "growls", "meows", "reverberates", "roars", "squeaks", "rattles", "exclaims", "yells", "remarks", "mumbles", "jabbers", "stutters", "seethes") - var/static/list/abom_sounds - deathmessage = "wails as its form shudders and violently comes to a stop." death_sound = 'sound/voice/abomburning.ogg' - despawns_when_lonely = FALSE // too ANGRY to despawn + deathmessage = "wails as its form shudders and violently comes to a stop." + + faction = list("hostile") + a_intent = INTENT_HARM + gold_core_spawnable = HOSTILE_SPAWN + + can_ghost_into = TRUE + pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS + despawns_when_lonely = FALSE // Too ANGRY to despawn + + // Z-movement + can_z_move = TRUE + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 50 + + can_open_doors = FALSE + can_open_airlocks = FALSE + + // Pure melee - wall smashing tank + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + + // Sound system + var/static/list/abom_sounds + +/mob/living/simple_animal/hostile/abomination/Initialize() + . = ..() + abom_sounds = list('sound/voice/abomination1.ogg', 'sound/voice/abomscream.ogg', 'sound/voice/abommoan.ogg', 'sound/voice/abomscream2.ogg', 'sound/voice/abomscream3.ogg') + +/mob/living/simple_animal/hostile/abomination/Aggro() + . = ..() + if(.) + return + summon_backup(12) + +/mob/living/simple_animal/hostile/abomination/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() /mob/living/simple_animal/hostile/abomination/AttackingTarget() . = ..() @@ -134,89 +243,134 @@ var/choice = pick(1, 1, 2, 2, 3, 4) H.reagents.add_reagent(/datum/reagent/toxin/FEV_solution, choice) -/mob/living/simple_animal/hostile/abomination/Initialize() +/mob/living/simple_animal/hostile/abomination/say(message, datum/language/language = null, list/spans = list(), sanitize, ignore_spam, forced = null, just_chat) . = ..() - abom_sounds = list('sound/voice/abomination1.ogg', 'sound/voice/abomscream.ogg', 'sound/voice/abommoan.ogg', 'sound/voice/abomscream2.ogg', 'sound/voice/abomscream3.ogg') - -/mob/living/simple_animal/hostile/abomination/say(message, datum/language/language = null, list/spans = list(), language, sanitize, ignore_spam, forced = null, just_chat) - ..() if(stat) return var/chosen_sound = pick(abom_sounds) playsound(src, chosen_sound, 50, TRUE) /mob/living/simple_animal/hostile/abomination/Life() - ..() + . = ..() if(stat) return if(prob(10)) var/chosen_sound = pick(abom_sounds) playsound(src, chosen_sound, 70, TRUE) -/mob/living/simple_animal/hostile/abomination/weak // For FEV mutation. - environment_smash = ENVIRONMENT_SMASH_STRUCTURES // So you don't break walls +// WEAK ABOMINATION - for FEV mutation event +/mob/living/simple_animal/hostile/abomination/weak + environment_smash = ENVIRONMENT_SMASH_STRUCTURES // Can't break walls + maxHealth = 500 health = 500 - harm_intent_damage = 8 + speed = 2 // Slower + melee_damage_lower = 20 melee_damage_upper = 30 - speed = 2 - + + pop_required_to_jump_into = MED_MOB_MIN_PLAYERS -// ------------------------------------------ -// HORROR +//////////// +// HORROR // +//////////// +// BASE HORROR - weaker FEV fusion /mob/living/simple_animal/hostile/abomhorror name = "failed experiment" desc = "A terrible fusion of man, animal, and something else entirely. It looks to be in great pain." - speak_emote = list("screams", "clicks", "chitters", "barks", "moans", "growls", "meows", "reverberates", "roars", "squeaks", "rattles", "exclaims", "yells", "remarks", "mumbles", "jabbers", "stutters", "seethes") icon = 'icons/fallout/mobs/monsters/freaks.dmi' icon_state = "horror" icon_living = "horror" icon_dead = "horror_dead" - - speed = -0.5 + + mob_biotypes = MOB_ORGANIC|MOB_HUMANOID + mob_armor = ARMOR_VALUE_HORROR + maxHealth = 700 health = 700 + speed = -0.5 // Fast stat_attack = UNCONSCIOUS - harm_intent_damage = 8 + melee_damage_lower = 30 melee_damage_upper = 40 - - mob_biotypes = MOB_ORGANIC|MOB_HUMANOID + harm_intent_damage = 8 + robust_searching = TRUE + + speak_emote = list("screams", "clicks", "chitters", "barks", "moans", "growls", "meows", "reverberates", "roars", "squeaks", "rattles", "exclaims", "yells", "remarks", "mumbles", "jabbers", "stutters", "seethes") attack_verb_simple = "eviscerates" attack_sound = 'sound/weapons/punch1.ogg' - var/static/list/abom_sounds deathmessage = "wails as its form shudders and violently comes to a stop." + + faction = list("hostile") + a_intent = INTENT_HARM + gold_core_spawnable = HOSTILE_SPAWN + + can_ghost_into = TRUE + pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS + + // Z-movement + can_z_move = TRUE + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 50 -// NSB variant, some sort of bulletsponge + can_open_doors = FALSE + can_open_airlocks = FALSE + + // Pure melee + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + + var/static/list/abom_sounds + +/mob/living/simple_animal/hostile/abomhorror/Aggro() + . = ..() + if(.) + return + summon_backup(10) + +/mob/living/simple_animal/hostile/abomhorror/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + +// NSB HORROR - bunker variant, bulletsponge /mob/living/simple_animal/hostile/abomhorror/nsb + desc = "A terrible fusion of man, animal, and something else entirely. It looks to be in great pain, constantly shuddering violently and seeming relatively docile to the robots and raiders of the bunker. Huh." + maxHealth = 1000 health = 1000 - desc = "A terrible fusion of man, animal, and something else entirely. It looks to be in great pain, constantly shuddering violently and seeming relatively docile to the robots and raiders of the bunker. Huh." - harm_intent_damage = 8 + speed = -1 // Even faster + melee_damage_lower = 40 melee_damage_upper = 50 obj_damage = 300 - faction = list("raider") wound_bonus = 20 - speed = -1 - deathmessage = "wails as its form shudders and violently comes to a stop." + + faction = list("raider") + + pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS + despawns_when_lonely = FALSE /mob/living/simple_animal/hostile/abomhorror/nsb/Initialize() . = ..() abom_sounds = list('sound/voice/abomination1.ogg', 'sound/voice/abomscream.ogg', 'sound/voice/abommoan.ogg', 'sound/voice/abomscream2.ogg', 'sound/voice/abomscream3.ogg') -/mob/living/simple_animal/hostile/abomhorror/nsb/say(message, datum/language/language = null, list/spans = list(), language, sanitize, ignore_spam, forced = null, just_chat) - ..() +/mob/living/simple_animal/hostile/abomhorror/nsb/say(message, datum/language/language = null, list/spans = list(), sanitize, ignore_spam, forced = null, just_chat) + . = ..() if(stat) return var/chosen_sound = pick(abom_sounds) playsound(src, chosen_sound, 50, TRUE) /mob/living/simple_animal/hostile/abomhorror/nsb/Life() - ..() + . = ..() if(stat) return if(prob(10)) diff --git a/code/modules/mob/living/simple_animal/hostile/f13/chinese.dm b/code/modules/mob/living/simple_animal/hostile/f13/chinese.dm index e4583879917..9e20bfcff9f 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/chinese.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/chinese.dm @@ -1,3 +1,10 @@ +// In this document: Chinese Remnant Soldiers + +///////////////////////////// +// CHINESE REMNANT SOLDIERS // +///////////////////////////// + +// BASE - melee ghoul soldier /mob/living/simple_animal/hostile/chinese name = "chinese remnant soldier" desc = "Chinese soldiers who survived the Great War via ghoulification, and now shoot anything that isn't their own on sight." @@ -5,49 +12,108 @@ icon_state = "chinesesoldier" icon_living = "chinesesoldier" icon_gib = "syndicate_gib" + mob_biotypes = MOB_ORGANIC|MOB_HUMANOID - speak_chance = 0 - turns_per_move = 5 - response_help_simple = "pokes" - response_disarm_simple = "shoves" - response_harm_simple = "hits" - speed = 1 + mob_armor = ARMOR_VALUE_CHINESE_REMNANT + maxHealth = 80 health = 80 - harm_intent_damage = 8 + speed = 1 + move_to_delay = 3 + turns_per_move = 5 + melee_damage_lower = 20 melee_damage_upper = 38 + harm_intent_damage = 8 + + robust_searching = TRUE + + response_help_simple = "pokes" + response_disarm_simple = "shoves" + response_harm_simple = "hits" attack_verb_simple = "punches" attack_sound = 'sound/weapons/bladeslice.ogg' - a_intent = INTENT_HARM - loot = list(/obj/effect/mob_spawn/human/corpse/chineseremnant, /obj/item/melee/onehanded/knife/survival) + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) unsuitable_atmos_damage = 15 + + tastes = list("people" = 1, "dust" = 2) + faction = list("china") - check_friendly_fire = 1 + a_intent = INTENT_HARM + check_friendly_fire = TRUE status_flags = CANPUSH - del_on_death = 1 - tastes = list("people" = 1, "dust" = 2) + del_on_death = TRUE + speak_chance = 0 + + loot = list( + /obj/effect/mob_spawn/human/corpse/chineseremnant, + /obj/item/melee/onehanded/knife/survival + ) + + // Z-movement + can_z_move = TRUE + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 30 + + can_open_doors = TRUE + can_open_airlocks = TRUE + + // Pure melee + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 /mob/living/simple_animal/hostile/chinese/Aggro() . = ..() if(.) return - summon_backup(15) + summon_backup(10) if(!ckey) - say(pick("操你祖宗十八代", "乡巴佬", "傻逼" , "妈你个", "操你大爷", "祝你生孩子没屁眼", "扯鸡巴蛋", "狗改不了吃屎", "爆你菊花" )) + say(pick("操你祖宗十八代", "乡巴佬", "傻逼", "妈你个", "操你大爷", "祝你生孩子没屁眼", "扯鸡巴蛋", "狗改不了吃屎", "爆你菊花")) +/mob/living/simple_animal/hostile/chinese/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + +// Friendly fire resistance - chinese soldiers fight in squads +/mob/living/simple_animal/hostile/chinese/bullet_act(obj/item/projectile/P) + if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/chinese)) + var/original_damage = P.damage + P.damage *= 0.2 + . = ..() + P.damage = original_damage + return + return ..() + +// PISTOL SOLDIER - ranged variant /mob/living/simple_animal/hostile/chinese/ranged + name = "chinese remnant soldier" icon_state = "chinesepistol" icon_living = "chinesepistol" - loot = list(/obj/effect/mob_spawn/human/corpse/chineseremnant/pistol, /obj/item/gun/ballistic/automatic/pistol/type17) - ranged = 1 + maxHealth = 110 health = 110 - retreat_distance = 4 - minimum_distance = 6 + + loot = list( + /obj/effect/mob_spawn/human/corpse/chineseremnant/pistol, + /obj/item/gun/ballistic/automatic/pistol/type17 + ) + + // Pure ranged - pistol + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 5 + minimum_distance = 1 + + ranged_cooldown_time = 2 SECONDS projectiletype = /obj/item/projectile/bullet/c9mm/simple - projectilesound = 'sound/f13weapons/ninemil.ogg' + projectilesound = 'sound/f13weapons/ninemil.ogg' projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(PISTOL_LIGHT_VOLUME), @@ -59,14 +125,24 @@ SP_DISTANT_RANGE(PISTOL_LIGHT_RANGE_DISTANT) ) +// ASSAULT SOLDIER - burst fire rifle /mob/living/simple_animal/hostile/chinese/ranged/assault name = "chinese remnant assault soldier" icon_state = "chineseassault" icon_living = "chineseassault" + maxHealth = 160 health = 160 extra_projectiles = 2 - loot = list(/obj/effect/mob_spawn/human/corpse/chineseremnant/assault, /obj/item/gun/ballistic/automatic/type93, /obj/item/ammo_box/magazine/m556/rifle/assault) + + loot = list( + /obj/effect/mob_spawn/human/corpse/chineseremnant/assault, + /obj/item/gun/ballistic/automatic/type93, + /obj/item/ammo_box/magazine/m556/rifle/assault + ) + + // Pure ranged - assault rifle + ranged_cooldown_time = 15 projectiletype = /obj/item/projectile/bullet/a556/simple projectilesound = 'sound/f13weapons/assaultrifle_fire.ogg' projectile_sound_properties = list( @@ -81,5 +157,7 @@ ) /mob/living/simple_animal/hostile/chinese/ranged/assault/Aggro() - ..() - summon_backup(15) + . = ..() + if(.) + return + summon_backup(10) diff --git a/code/modules/mob/living/simple_animal/hostile/f13/deathclaw.dm b/code/modules/mob/living/simple_animal/hostile/f13/deathclaw.dm index 3ed988bff08..f66c4edd2c9 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/deathclaw.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/deathclaw.dm @@ -13,8 +13,8 @@ icon_gib = "deathclaw_gib" mob_armor = ARMOR_VALUE_DEATHCLAW_COMMON sentience_type = SENTIENCE_BOSS - maxHealth = 600 - health = 600 + maxHealth = 500 // Reduced from 600 + health = 500 stat_attack = UNCONSCIOUS reach = 2 speed = 1 @@ -22,28 +22,38 @@ melee_damage_lower = 30 melee_damage_upper = 40 footstep_type = FOOTSTEP_MOB_HEAVY - move_to_delay = 2.75 //hahahahahahahaaaaa + move_to_delay = 2.75 gender = MALE - a_intent = INTENT_HARM //So we can not move past them. + a_intent = INTENT_HARM mob_biotypes = MOB_ORGANIC|MOB_BEAST robust_searching = TRUE + + // Apex predator with excellent vision in low light + has_low_light_vision = TRUE + low_light_bonus = 4 + speak = list("ROAR!","Rawr!","GRRAAGH!","Growl!") speak_emote = list("growls", "roars") emote_hear = list("grumbles.","grawls.") emote_taunt = list("stares ferociously", "stomps") speak_chance = 10 taunt_chance = 25 + tastes = list("a bad time" = 5, "dirt" = 1) - environment_smash = ENVIRONMENT_SMASH_STRUCTURES | ENVIRONMENT_SMASH_WALLS | ENVIRONMENT_SMASH_RWALLS //can smash walls + environment_smash = ENVIRONMENT_SMASH_STRUCTURES // Only structures, not walls normally var/color_mad = "#ffc5c5" see_in_dark = 8 decompose = FALSE - wound_bonus = 0 //This might be a TERRIBLE idea + wound_bonus = 0 bare_wound_bonus = 0 sharpness = SHARP_EDGED - guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/deathclaw = 4, - /obj/item/stack/sheet/animalhide/deathclaw = 2, - /obj/item/stack/sheet/bone = 4) + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/deathclaw = 4, + /obj/item/stack/sheet/animalhide/deathclaw = 2, + /obj/item/stack/sheet/bone = 4 + ) + response_help_simple = "pets" response_disarm_simple = "gently pushes aside" response_harm_simple = "hits" @@ -51,23 +61,41 @@ attack_sound = 'sound/weapons/bladeslice.ogg' faction = list("deathclaw") gold_core_spawnable = HOSTILE_SPAWN - //var/charging = FALSE move_resist = MOVE_FORCE_OVERPOWERING + emote_taunt_sound = list('sound/f13npc/deathclaw/taunt.ogg') - aggrosound = list('sound/f13npc/deathclaw/aggro1.ogg', 'sound/f13npc/deathclaw/aggro2.ogg', ) - idlesound = list('sound/f13npc/deathclaw/idle.ogg',) + aggrosound = list('sound/f13npc/deathclaw/aggro1.ogg', 'sound/f13npc/deathclaw/aggro2.ogg') + idlesound = list('sound/f13npc/deathclaw/idle.ogg') death_sound = 'sound/f13npc/deathclaw/death.ogg' + low_health_threshold = 0.5 + despawns_when_lonely = FALSE + + // Charge mechanic vars + var/charging = FALSE + var/charge_cooldown = 0 + var/charge_cooldown_time = 10 SECONDS + variation_list = list( MOB_RETREAT_DISTANCE_LIST(0, 0, 0, 3, 3), MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(65), MOB_MINIMUM_DISTANCE_LIST(0, 0, 0, 1), MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(30), ) - despawns_when_lonely = FALSE + // Z-movement can_z_move = TRUE - z_move_delay = 20 // 2 seconds - fast pursuit + can_climb_ladders = FALSE // Too big! + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 20 // Fast pursuit + + can_open_doors = FALSE + + // Pure melee + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 /mob/living/simple_animal/hostile/deathclaw/playable emote_taunt_sound = null @@ -77,39 +105,46 @@ see_in_dark = 8 wander = FALSE -/// Override this with what should happen when going from high health to low health -/mob/living/simple_animal/hostile/deathclaw/make_low_health() - visible_message(span_danger("[src] lets out a vicious roar!!!")) - playsound(src, 'sound/f13npc/deathclaw/aggro2.ogg', 100, 1, SOUND_DISTANCE(20)) - color = color_mad - reach += 1 - speed *= 0.8 - obj_damage += 200 - melee_damage_lower *= 1.5 - melee_damage_upper *= 1.4 - see_in_dark += 8 - environment_smash = ENVIRONMENT_SMASH_STRUCTURES | ENVIRONMENT_SMASH_WALLS | ENVIRONMENT_SMASH_RWALLS //can smash walls - wound_bonus += 25 - bare_wound_bonus += 50 - sound_pitch = -50 - alternate_attack_prob = 75 - is_low_health = TRUE +// Override to ignore unconscious/dead targets +/mob/living/simple_animal/hostile/deathclaw/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() -/// Override this with what should happen when going from low health to high health -/mob/living/simple_animal/hostile/deathclaw/make_high_health() - visible_message(span_danger("[src] calms down.")) - color = initial(color) - reach = initial(reach) - speed = initial(speed) - obj_damage = initial(obj_damage) - melee_damage_lower = initial(melee_damage_lower) - melee_damage_upper = initial(melee_damage_upper) - see_in_dark = initial(see_in_dark) - environment_smash = initial(environment_smash) - wound_bonus = initial(wound_bonus) - bare_wound_bonus = initial(bare_wound_bonus) - alternate_attack_prob = initial(alternate_attack_prob) - is_low_health = FALSE +/// RAGE MODE - when going from high health to low health +/mob/living/simple_animal/hostile/deathclaw/mother/make_low_health() + if(!target) + return + ..() + +/// Calming down when going from low health to high health +/mob/living/simple_animal/hostile/deathclaw/mother/make_high_health() + if(!target) + // If we somehow have rage active without a target, clean it up + if(is_low_health) + color = initial(color) + reach = initial(reach) + speed = initial(speed) + obj_damage = initial(obj_damage) + melee_damage_lower = initial(melee_damage_lower) + melee_damage_upper = initial(melee_damage_upper) + see_in_dark = initial(see_in_dark) + environment_smash = initial(environment_smash) + wound_bonus = initial(wound_bonus) + bare_wound_bonus = initial(bare_wound_bonus) + alternate_attack_prob = initial(alternate_attack_prob) + sound_pitch = initial(sound_pitch) + is_low_health = FALSE + return + ..() + +// Deactivate rage when target is lost +/mob/living/simple_animal/hostile/deathclaw/mother/LoseTarget() + ..() + if(is_low_health) + make_high_health() /mob/living/simple_animal/hostile/deathclaw/AlternateAttackingTarget(atom/the_target) if(!ismovable(the_target)) @@ -119,43 +154,116 @@ return var/atom/throw_target = get_ranged_target_turf(throwee, get_dir(src, the_target), rand(2,10), 4) throwee.safe_throw_at(throw_target, 10, 1, src, TRUE) - playsound(get_turf(throwee), 'sound/effects/Flesh_Break_1.ogg') + playsound(get_turf(throwee), 'sound/effects/Flesh_Break_1.ogg', 50, 1) + visible_message(span_danger("[src] hurls [the_target] across the room!")) + +// CHARGE MECHANIC - trigger on getting shot +/mob/living/simple_animal/hostile/deathclaw/bullet_act(obj/item/projectile/Proj) + if(!Proj) + return + + // Chance to charge when shot, if not on cooldown + if(!charging && world.time > charge_cooldown && prob(30)) + visible_message(span_danger("\The [src] roars in rage!")) + addtimer(CALLBACK(src, PROC_REF(Charge)), 0.3 SECONDS) + + return ..() + +/mob/living/simple_animal/hostile/deathclaw/do_attack_animation(atom/A, visual_effect_icon, obj/item/used_item, no_effect) + if(!charging) + ..() + +/mob/living/simple_animal/hostile/deathclaw/AttackingTarget() + if(!charging) + return ..() + +/mob/living/simple_animal/hostile/deathclaw/Goto(target, delay, minimum_distance) + if(!charging) + ..() /mob/living/simple_animal/hostile/deathclaw/Move() if(is_low_health && health > 0) new /obj/effect/temp_visual/decoy/fading(loc,src) DestroySurroundings() . = ..() + if(charging) + new /obj/effect/temp_visual/decoy/fading(loc,src) + DestroySurroundings() + +/mob/living/simple_animal/hostile/deathclaw/proc/Charge() + if(!target) + return + + var/turf/T = get_turf(target) + if(!T || T == loc) + return + + charging = TRUE + charge_cooldown = world.time + charge_cooldown_time + + visible_message(span_danger("[src] charges with terrifying speed!")) + DestroySurroundings() + walk(src, 0) + setDir(get_dir(src, T)) + + var/obj/effect/temp_visual/decoy/D = new /obj/effect/temp_visual/decoy(loc,src) + animate(D, alpha = 0, color = "#FF0000", transform = matrix()*2, time = 0.1 SECONDS) + + throw_at(T, get_dist(src, T), 2, src, FALSE, callback = CALLBACK(src, PROC_REF(charge_end))) + +/mob/living/simple_animal/hostile/deathclaw/proc/charge_end(list/effects_to_destroy) + charging = FALSE + if(target) + Goto(target, move_to_delay, minimum_distance) /mob/living/simple_animal/hostile/deathclaw/Bump(atom/A) - if(is_low_health) + if(charging) if((isturf(A) || isobj(A)) && A.density) A.ex_act(EXPLODE_HEAVY) playsound(src, 'sound/effects/meteorimpact.ogg', 100, 1) - if(stat || health <= 0) + + // Chance to die on wall impact if low health + if(is_low_health && health < (maxHealth * 0.2) && prob(25)) playsound(get_turf(src), 'sound/effects/Flesh_Break_2.ogg', 100, 1, ignore_walls = TRUE) - visible_message(span_danger("[src] smashes into \the [A] and explodes in a violent spray of gore![prob(25) ? " Holy shit!" : ""]")) + visible_message(span_danger("[src] smashes into \the [A] with such force that it explodes in a violent spray of gore! Holy shit!")) gib() return DestroySurroundings() ..() -// Mother death claw +/mob/living/simple_animal/hostile/deathclaw/throw_impact(atom/A) + if(!charging) + return ..() + + if(isliving(A)) + var/mob/living/L = A + L.visible_message(span_danger("[src] slams into [L] with incredible force!"), span_userdanger("[src] slams into you!")) + L.apply_damage(melee_damage_upper, BRUTE) + playsound(get_turf(L), 'sound/effects/meteorimpact.ogg', 100, 1) + shake_camera(L, 4, 3) + shake_camera(src, 2, 3) + var/throwtarget = get_edge_target_turf(src, get_dir(src, get_step_away(L, src))) + L.throw_at(throwtarget, 3, 2) + + charging = FALSE + +// Mother deathclaw /mob/living/simple_animal/hostile/deathclaw/mother name = "mother deathclaw" desc = "A massive, reptilian creature with powerful muscles, razor-sharp claws, and aggression to match. This one is an angry mother." gender = FEMALE mob_armor = ARMOR_VALUE_DEATHCLAW_MOTHER - maxHealth = 1500 - health = 1500 + maxHealth = 1000 // Reduced from 1500 + health = 1000 stat_attack = CONSCIOUS melee_damage_lower = 25 melee_damage_upper = 55 - footstep_type = FOOTSTEP_MOB_HEAVY color = rgb(95,104,94) color_mad = rgb(113, 105, 100) - guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/deathclaw = 6, - /obj/item/stack/sheet/animalhide/deathclaw = 3) + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/deathclaw = 6, + /obj/item/stack/sheet/animalhide/deathclaw = 3 + ) /mob/living/simple_animal/hostile/deathclaw/butter name = "butterclaw" @@ -164,22 +272,23 @@ icon_living = "deathclaw_butter" icon_dead = "deathclaw_butter_dead" color_mad = rgb(133, 98, 87) - guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/butter = 10, - /obj/item/stack/sheet/animalhide/deathclaw = 3) + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/butter = 10, + /obj/item/stack/sheet/animalhide/deathclaw = 3 + ) //Legendary Deathclaw /mob/living/simple_animal/hostile/deathclaw/legendary name = "legendary deathclaw" desc = "A massive, reptilian creature with powerful muscles, razor-sharp claws, and aggression to match. This one is a legendary enemy." mob_armor = ARMOR_VALUE_DEATHCLAW_MOTHER - maxHealth = 2400 - health = 2400 + maxHealth = 1800 // Reduced from 2400 + health = 1800 color = "#FFFF00" color_mad = rgb(133, 98, 87) stat_attack = CONSCIOUS melee_damage_lower = 25 melee_damage_upper = 55 - footstep_type = FOOTSTEP_MOB_HEAVY /mob/living/simple_animal/hostile/deathclaw/legendary/death(gibbed) var/turf/T = get_turf(src) @@ -187,83 +296,16 @@ new /obj/item/melee/unarmed/deathclawgauntlet(T) . = ..() -//Power Armor Deathclaw the tankest and the scariest deathclaw in the West. One mistake will end you. May the choice be with you. +//Power Armor Deathclaw - the tankiest deathclaw /mob/living/simple_animal/hostile/deathclaw/power_armor name = "power armored deathclaw" desc = "A massive, reptilian creature with powerful muscles, razor-sharp claws, and aggression to match. Someone had managed to put power armor on him." icon_state = "combatclaw" icon_living = "combatclaw" icon_dead = "combatclaw_dead" - mob_armor = ARMOR_VALUE_DEATHCLAW_PA // ha get fucked - maxHealth = 3000 // ha get turbofucked - health = 3000 + mob_armor = ARMOR_VALUE_DEATHCLAW_PA + maxHealth = 2000 // Reduced from 3000 + health = 2000 stat_attack = CONSCIOUS melee_damage_lower = 40 melee_damage_upper = 60 - footstep_type = FOOTSTEP_MOB_HEAVY - - -/// Code for deathclaw charging. It barely works -/* /mob/living/simple_animal/hostile/deathclaw/bullet_act(obj/item/projectile/Proj) - if(!Proj) - return - if(!charging) - visible_message(span_danger("\The [src] growls, enraged!")) - addtimer(CALLBACK(src, PROC_REF(Charge)), 3) - . = ..() // I swear I looked at this like 10 times before, never once noticed this wasnt here, fmdakm - -/mob/living/simple_animal/hostile/deathclaw/do_attack_animation(atom/A, visual_effect_icon, obj/item/used_item, no_effect) - if(!charging) - ..() - -/mob/living/simple_animal/hostile/deathclaw/AttackingTarget() - if(!charging) - return ..() - -/mob/living/simple_animal/hostile/deathclaw/Goto(target, delay, minimum_distance) - if(!charging) - ..() - -/mob/living/simple_animal/hostile/deathclaw/proc/Charge() - var/turf/T = get_turf(target) - if(!T || T == loc) - return - charging = TRUE - visible_message(span_danger(">[src] charges!")) - DestroySurroundings() - walk(src, 0) - setDir(get_dir(src, T)) - var/obj/effect/temp_visual/decoy/D = new /obj/effect/temp_visual/decoy(loc,src) - animate(D, alpha = 0, color = "#FF0000", transform = matrix()*2, time = 1) - throw_at(T, get_dist(src, T), 1, src, 0, callback = CALLBACK(src, PROC_REF(charge_end))) - -/mob/living/simple_animal/hostile/deathclaw/proc/charge_end(list/effects_to_destroy) - charging = FALSE - if(target) - Goto(target, move_to_delay, minimum_distance) - -/mob/living/simple_animal/hostile/deathclaw/Bump(atom/A) - if(charging) - if(isturf(A) || isobj(A) && A.density) - A.ex_act(EXPLODE_HEAVY) - DestroySurroundings() - ..() - -/mob/living/simple_animal/hostile/deathclaw/throw_impact(atom/A) - if(!charging) - return ..() - - else if(isliving(A)) - var/mob/living/L = A - L.visible_message(span_danger("[src] slams into [L]!"), span_userdanger("[src] slams into you!")) - L.apply_damage(melee_damage_lower/2, BRUTE) - playsound(get_turf(L), 'sound/effects/meteorimpact.ogg', 100, 1) - shake_camera(L, 4, 3) - shake_camera(src, 2, 3) - var/throwtarget = get_edge_target_turf(src, get_dir(src, get_step_away(L, src))) - L.throw_at(throwtarget, 3) - - - charging = FALSE - charging = FALSE - */ diff --git a/code/modules/mob/living/simple_animal/hostile/f13/eyebot.dm b/code/modules/mob/living/simple_animal/hostile/f13/eyebot.dm index ef81f572511..c836c558aaf 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/eyebot.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/eyebot.dm @@ -1,7 +1,10 @@ +// In this document: Eyebots, Floating Eyes, Propaganda Eyebot + ///////////// // EYEBOTS // ///////////// +// BASE EYEBOT - hovering laser recon drone /mob/living/simple_animal/hostile/eyebot name = "eyebot" desc = "A hovering, propaganda-spewing reconnaissance and surveillance robot with radio antennas pointing out its back and loudspeakers blaring out the front." @@ -9,59 +12,79 @@ icon_state = "eyebot" icon_living = "eyebot" icon_dead = "eyebot_d" - can_ghost_into = TRUE - speak_chance = 0 + + mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC + mob_armor = ARMOR_VALUE_ROBOT_CIVILIAN + + maxHealth = 40 + health = 40 + move_to_delay = 2.75 turns_per_move = 6 - environment_smash = 0 + stamcrit_threshold = SIMPLEMOB_NO_STAMCRIT + + melee_damage_lower = 2 + melee_damage_upper = 3 + harm_intent_damage = 8 + + aggro_vision_range = 7 + vision_range = 7 + robust_searching = TRUE + response_help_simple = "touches" response_disarm_simple = "shoves" response_harm_simple = "hits" - move_to_delay = 3 - robust_searching = 1 - mob_armor = ARMOR_VALUE_ROBOT_CIVILIAN - maxHealth = 40 - health = 40 - stamcrit_threshold = SIMPLEMOB_NO_STAMCRIT + attack_verb_simple = "punches" + attack_sound = 'sound/weapons/punch1.ogg' + + speak_emote = list("states") + speak_chance = 0 + aggrosound = list('sound/f13npc/eyebot/aggro.ogg') + idlesound = list('sound/f13npc/eyebot/idle1.ogg', 'sound/f13npc/eyebot/idle2.ogg') + death_sound = 'sound/f13npc/eyebot/robo_death.ogg' + emp_flags = list( MOB_EMP_STUN, MOB_EMP_BERSERK, MOB_EMP_DAMAGE, MOB_EMP_SCRAMBLE - ) - healable = 0 - mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC - blood_volume = 0 - faction = list( - "hostile", - "enclave", - "wastebot", - "ghoul", - "cazador", - "supermutant", - "bighorner" - ) - harm_intent_damage = 8 - melee_damage_lower = 2 - melee_damage_upper = 3 - minimum_distance = 6 - retreat_distance = 14 - attack_verb_simple = "punches" - attack_sound = "punch" - a_intent = "harm" - tastes = list("metal" = 1, "glass" = 1) + ) + + damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) - unsuitable_atmos_damage = 15 - status_flags = CANPUSH + tastes = list("metal" = 1, "glass" = 1) - vision_range = 7 //reduced from 13 to 7 because who needs that kind of shit in their life - aggro_vision_range = 7 //as above - ranged = 1 + + faction = list("hostile", "enclave", "wastebot", "ghoul", "cazador", "supermutant", "bighorner") + a_intent = INTENT_HARM + check_friendly_fire = TRUE + healable = FALSE + blood_volume = 0 + status_flags = CANPUSH + environment_smash = ENVIRONMENT_SMASH_NONE + + can_ghost_into = TRUE + desc_short = "A flying metal meatball with lasers." + pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS + + // Z-movement - flies + can_z_move = TRUE + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 50 + + can_open_doors = FALSE + can_open_airlocks = TRUE // Small enough to slip through + + // Pure ranged - kites at extreme distance + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 14 + minimum_distance = 1 + + ranged_cooldown_time = 2 SECONDS projectiletype = /obj/item/projectile/beam/laser/pistol/wattz projectilesound = 'sound/weapons/resonator_fire.ogg' - aggrosound = list('sound/f13npc/eyebot/aggro.ogg') - idlesound = list('sound/f13npc/eyebot/idle1.ogg', 'sound/f13npc/eyebot/idle2.ogg') - death_sound = 'sound/f13npc/eyebot/robo_death.ogg' - speak_emote = list("states") projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(LASER_VOLUME), @@ -72,47 +95,100 @@ SP_DISTANT_SOUND(LASER_DISTANT_SOUND), SP_DISTANT_RANGE(LASER_RANGE_DISTANT) ) - desc_short = "A flying metal meatball with lasers." + var/obj/machinery/camera/portable/builtInCamera - /mob/living/simple_animal/hostile/eyebot/New() ..() name = "ED-[rand(1,99)]" +/mob/living/simple_animal/hostile/eyebot/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + +// Friendly fire resistance - eyebots are networked +/mob/living/simple_animal/hostile/eyebot/bullet_act(obj/item/projectile/P) + if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/eyebot)) + var/original_damage = P.damage + P.damage *= 0.2 + . = ..() + P.damage = original_damage + return + return ..() + /mob/living/simple_animal/hostile/eyebot/become_the_mob(mob/user) send_mobs = /obj/effect/proc_holder/mob_common/direct_mobs/robot call_backup = /obj/effect/proc_holder/mob_common/summon_backup/robot . = ..() +// PLAYABLE EYEBOT /mob/living/simple_animal/hostile/eyebot/playable - ranged = FALSE - health = 30 maxHealth = 30 + health = 30 + speed = -1 attack_verb_simple = "zaps" - emote_taunt_sound = null - emote_taunt = null - aggrosound = null - idlesound = null see_in_dark = 8 - wander = 0 + wander = FALSE force_threshold = 10 anchored = FALSE del_on_death = FALSE dextrous = TRUE + ranged = FALSE + emote_taunt_sound = null + emote_taunt = null + aggrosound = null + idlesound = null possible_a_intents = list(INTENT_HELP, INTENT_HARM) - speed = -1 +// REINFORCED EYEBOT - tougher raider variant +/mob/living/simple_animal/hostile/eyebot/reinforced + name = "reinforced eyebot" + desc = "An eyebot with beefier protection, and extra electronic aggression." + color = "#B85C00" + + mob_armor = ARMOR_VALUE_ROBOT_SECURITY + + maxHealth = 100 + health = 100 + + melee_damage_lower = 5 + melee_damage_upper = 10 + + faction = list("raider", "wastebot") + + // Pure ranged - closer than standard + retreat_distance = 6 + minimum_distance = 1 + + extra_projectiles = 1 + auto_fire_delay = GUN_AUTOFIRE_DELAY_SLOWER + +/mob/living/simple_animal/hostile/eyebot/reinforced/become_the_mob(mob/user) + send_mobs = null + call_backup = null + . = ..() + +/////////////////// +// FLOATING EYES // +/////////////////// + +// FLOATING EYE - taser variant, BOS faction /mob/living/simple_animal/hostile/eyebot/floatingeye name = "floating eyebot" desc = "A quick-observation robot commonly found in pre-War military installations.
The floating eyebot uses a powerful taser to keep intruders in line." icon_state = "floatingeye" icon_living = "floatingeye" icon_dead = "floatingeye_d" - - retreat_distance = 4 + faction = list("hostile", "bs") - + + // Pure ranged - closer engagement than standard eyebot + retreat_distance = 6 + minimum_distance = 1 + projectiletype = /obj/item/projectile/energy/electrode projectilesound = 'sound/weapons/resonator_blast.ogg' @@ -125,7 +201,12 @@ call_backup = null . = ..() -/mob/living/simple_animal/pet/dog/eyebot //It's a propaganda eyebot, not a dog, but... +////////////////////// +// PROPAGANDA EYEBOT // +////////////////////// + +// PROPAGANDA EYEBOT - unarmed pet variant +/mob/living/simple_animal/pet/dog/eyebot name = "propaganda eyebot" desc = "This eyebot's weapons module has been removed and replaced with a loudspeaker. It appears to be shouting Pre-War propaganda." icon = 'icons/fallout/mobs/robots/eyebots.dmi' @@ -133,22 +214,28 @@ icon_living = "eyebot" icon_dead = "eyebot_d" icon_gib = "eyebot_d" + + mob_biotypes = MOB_ROBOTIC + maxHealth = 60 health = 60 speak_chance = 8 gender = NEUTER - mob_biotypes = MOB_ROBOTIC + blood_volume = 0 + faction = list("hostile", "enclave", "wastebot", "ghoul", "cazador", "supermutant", "bighorner") + speak = list() speak_emote = list("states") emote_hear = list() - emote_see = list("buzzes.","pings.","floats in place") - response_help_simple = "shakes the radio of" + emote_see = list("buzzes.", "pings.", "floats in place") + + response_help_simple = "shakes the radio of" response_disarm_simple = "pushes" - response_harm_simple = "punches" + response_harm_simple = "punches" attack_sound = 'sound/voice/liveagain.ogg' + butcher_results = list(/obj/effect/gibspawner/robot = 1) - blood_volume = 0 /mob/living/simple_animal/pet/dog/eyebot/ComponentInitialize() . = ..() @@ -158,47 +245,27 @@ . = ..() if(. & EMP_PROTECT_SELF) return - var/emp_damage = round((maxHealth * 0.1) * (severity * 0.1)) // 10% of max HP * 10% of severity(Usually around 20-40) + var/emp_damage = round((maxHealth * 0.1) * (severity * 0.1)) adjustBruteLoss(emp_damage) +// PLAYABLE PROPAGANDA EYEBOT /mob/living/simple_animal/pet/dog/eyebot/playable - health = 200 maxHealth = 200 + health = 200 + speed = 1 attack_verb_simple = "zaps" - aggrosound = null - speak_chance = 0 - idlesound = null see_in_dark = 8 - wander = 0 + speak_chance = 0 + wander = FALSE force_threshold = 10 anchored = FALSE del_on_death = FALSE dextrous = TRUE + aggrosound = null + idlesound = null possible_a_intents = list(INTENT_HELP, INTENT_HARM) - speed = 1 /mob/living/simple_animal/pet/dog/eyebot/playable/become_the_mob(mob/user) send_mobs = null call_backup = null . = ..() - -//Junkers -/mob/living/simple_animal/hostile/eyebot/reinforced - name = "reinforced eyebot" - desc = "An eyebot with beefier protection, and extra electronic aggression." - color = "#B85C00" - mob_armor = ARMOR_VALUE_ROBOT_CIVILIAN - maxHealth = 100 - health = 100 - faction = list("raider", "wastebot") - extra_projectiles = 1 - auto_fire_delay = GUN_AUTOFIRE_DELAY_SLOWER - melee_damage_lower = 5 - melee_damage_upper = 10 - minimum_distance = 4 - retreat_distance = 6 - -/mob/living/simple_animal/hostile/eyebot/reinforced/become_the_mob(mob/user) - send_mobs = null - call_backup = null - . = ..() diff --git a/code/modules/mob/living/simple_animal/hostile/f13/fallout_NPC.dm b/code/modules/mob/living/simple_animal/hostile/f13/fallout_NPC.dm index 0c6851f7956..ae1376f9198 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/fallout_NPC.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/fallout_NPC.dm @@ -1,55 +1,98 @@ +// In this document: Vault Dwellers, Enclave, Brotherhood, NCR, Legion, Tribals + /////////////// // VAULT NPC // /////////////// +// BASE VAULT DWELLER /mob/living/simple_animal/hostile/vault name = "Vault Dweller" - desc = "Just a Vault Dweller" + desc = "Just a Vault Dweller." icon = 'icons/fallout/mobs/humans/fallout_npc.dmi' icon_state = "vault_dweller" icon_living = "vault_dweller" icon_dead = "vault_dweller" - turns_per_move = 5 + mob_biotypes = MOB_ORGANIC|MOB_HUMANOID - response_help_simple = "pokes" - response_disarm_simple = "shoves" - response_harm_simple = "hits" - speed = 1 - stat_attack = 1 - robust_searching = 1 + maxHealth = 100 health = 100 - harm_intent_damage = 8 + speed = 1 + turns_per_move = 5 + melee_damage_lower = 5 melee_damage_upper = 10 + harm_intent_damage = 8 + + robust_searching = TRUE + stat_attack = CONSCIOUS + + response_help_simple = "pokes" + response_disarm_simple = "shoves" + response_harm_simple = "hits" attack_verb_simple = "punches" attack_sound = 'sound/weapons/punch1.ogg' - a_intent = INTENT_HARM - loot = list(/obj/effect/mob_spawn/human/corpse/vault) + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) unsuitable_atmos_damage = 15 + faction = list("vault", "city") - check_friendly_fire = 1 + a_intent = INTENT_HARM + check_friendly_fire = TRUE status_flags = CANPUSH del_on_death = TRUE speak_chance = 1 despawns_when_lonely = FALSE - -/obj/effect/mob_spawn/human/corpse/vault - name = "Vault Dweller" - gloves = /obj/item/pda - uniform = /obj/item/clothing/under/f13/vault/v13 - shoes = /obj/item/clothing/shoes/jackboots - + + loot = list(/obj/effect/mob_spawn/human/corpse/vault) + + // Z-movement + can_z_move = TRUE + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 30 + + can_open_doors = TRUE + can_open_airlocks = TRUE + + // Pure melee + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + +/mob/living/simple_animal/hostile/vault/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + +// Friendly fire resistance - vault dwellers work together +/mob/living/simple_animal/hostile/vault/bullet_act(obj/item/projectile/P) + if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/vault)) + var/original_damage = P.damage + P.damage *= 0.2 + . = ..() + P.damage = original_damage + return + return ..() + +// VAULT DWELLER VARIANTS - flee when attacked /mob/living/simple_animal/hostile/vault/dweller - minimum_distance = 10 + // Cowardly melee - flees when attacked + combat_mode = COMBAT_MODE_MELEE retreat_distance = 10 + minimum_distance = 1 + obj_damage = 0 - environment_smash = 0 + environment_smash = ENVIRONMENT_SMASH_NONE /mob/living/simple_animal/hostile/vault/dweller/Aggro() - ..() - summon_backup(15) + . = ..() + if(.) + return + summon_backup(10) say("HELP!!") /mob/living/simple_animal/hostile/vault/dweller/dweller1 @@ -77,22 +120,29 @@ icon_living = "vault_dweller5" icon_dead = "vault_dweller5" +// VAULT SECURITY - armed guards /mob/living/simple_animal/hostile/vault/security name = "Vault Security" - desc = "Just a Vault Security" + desc = "Just a Vault Security officer." icon_state = "vault_dweller_sec" icon_living = "vault_dweller_sec" icon_dead = "vault_dweller_sec" + maxHealth = 160 health = 160 - retreat_distance = 5 - minimum_distance = 5 + loot = list(/obj/effect/mob_spawn/human/corpse/vault/security) - healable = 1 - ranged = 1 + healable = TRUE + + // Pure ranged - laser pistol + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 5 + minimum_distance = 1 + + ranged_cooldown_time = 2 SECONDS projectiletype = /obj/item/projectile/beam projectilesound = 'sound/weapons/resonator_fire.ogg' - speak_chance = 1 projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(LASER_VOLUME), @@ -104,10 +154,18 @@ SP_DISTANT_RANGE(LASER_RANGE_DISTANT) ) - /mob/living/simple_animal/hostile/vault/security/Aggro() - ..() - summon_backup(15) + . = ..() + if(.) + return + summon_backup(10) + +// VAULT CORPSES +/obj/effect/mob_spawn/human/corpse/vault + name = "Vault Dweller" + gloves = /obj/item/pda + uniform = /obj/item/clothing/under/f13/vault/v13 + shoes = /obj/item/clothing/shoes/jackboots /obj/effect/mob_spawn/human/corpse/vault/security name = "Vault Security" @@ -122,48 +180,70 @@ // ENCLAVE NPC // ///////////////// -// Enclave specialist, basic fighter +// BASE ENCLAVE /mob/living/simple_animal/hostile/enclave name = "enclave specialist" - desc = "A Enclave soldier with combat armor and a G-11 rifle." + desc = "An Enclave soldier with combat armor and a G-11 rifle." icon = 'icons/fallout/mobs/humans/fallout_npc.dmi' icon_state = "enclave_specialist" icon_living = "enclave_specialist" - del_on_death = TRUE + icon_dead = "enclave_specialist" + mob_biotypes = MOB_ORGANIC|MOB_HUMANOID - speak_chance = 0 - turns_per_move = 5 - response_help_simple = "pokes" - response_disarm_simple = "shoves" - response_harm_simple = "hits" - retreat_distance = 6 - minimum_distance = 6 - speed = 0 - ranged_cooldown_time = 22 - extra_projectiles = 2 - stat_attack = 1 - ranged = TRUE - robust_searching = TRUE - healable = TRUE + mob_armor = ARMOR_VALUE_ENCLAVE + maxHealth = 200 health = 200 + speed = 0 + turns_per_move = 5 + melee_damage_lower = 15 melee_damage_upper = 35 harm_intent_damage = 8 - - projectiletype = /obj/item/projectile/bullet/c46x30mm - projectilesound = 'sound/weapons/gunshot_smg.ogg' + + robust_searching = TRUE + stat_attack = CONSCIOUS + healable = TRUE + + response_help_simple = "pokes" + response_disarm_simple = "shoves" + response_harm_simple = "hits" attack_verb_simple = "pistol-whips" - attack_sound = 'sound/weapons/punch1.ogg' - a_intent = INTENT_HARM + + speak = list("For the Enclave!", "Stars and Stripes!", "Liberty or death!") + speak_emote = list("pulls out a weapon", "shouts") + speak_chance = 0 + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) unsuitable_atmos_damage = 15 + faction = list("enclave") - check_friendly_fire = 1 + a_intent = INTENT_HARM + check_friendly_fire = TRUE status_flags = CANPUSH - speak = list("For the Enclave!", "Stars and Stripes!", "Liberty or death!") - speak_emote = list("pulls out a weapon", "shouts") + del_on_death = TRUE + + // Z-movement + can_z_move = TRUE + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 30 + + can_open_doors = TRUE + can_open_airlocks = TRUE + + // Pure ranged - G-11 + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 6 + minimum_distance = 1 + + ranged_cooldown_time = 22 + extra_projectiles = 2 + projectiletype = /obj/item/projectile/bullet/c46x30mm + projectilesound = 'sound/weapons/gunshot_smg.ogg' projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(PISTOL_LIGHT_VOLUME), @@ -175,26 +255,57 @@ SP_DISTANT_RANGE(PISTOL_LIGHT_RANGE_DISTANT) ) -// Enclave Scientist +/mob/living/simple_animal/hostile/enclave/Aggro() + . = ..() + if(.) + return + summon_backup(10) + +/mob/living/simple_animal/hostile/enclave/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + +// Friendly fire resistance - enclave soldiers are well-trained +/mob/living/simple_animal/hostile/enclave/bullet_act(obj/item/projectile/P) + if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/enclave)) + var/original_damage = P.damage + P.damage *= 0.2 + . = ..() + P.damage = original_damage + return + return ..() + +// ENCLAVE SCIENTIST - cowardly ranged /mob/living/simple_animal/hostile/enclave/scientist name = "enclave scientist" desc = "An Enclave Scientist wearing an advanced radiation suit. While they may run from you, that does not exempt them from the evil they have committed." icon_state = "enclave_scientist" icon_living = "enclave_scientist" + icon_dead = "enclave_scientist" + maxHealth = 120 health = 120 - minimum_distance = 10 - retreat_distance = 10 - obj_damage = 0 - environment_smash = 0 - loot = list(/obj/effect/mob_spawn/human/corpse/enclavescientist) + melee_damage_lower = 5 melee_damage_upper = 15 + obj_damage = 0 + environment_smash = ENVIRONMENT_SMASH_NONE + + loot = list(/obj/effect/mob_spawn/human/corpse/enclavescientist) + + // Pure ranged - flees when threatened + combat_mode = COMBAT_MODE_RANGED + retreat_distance = 10 + minimum_distance = 1 + ranged_cooldown_time = 30 - projectiletype = /obj/item/projectile/f13plasma/pistol/adam - projectilesound = 'sound/weapons/wave.ogg' extra_projectiles = 0 attack_verb_simple = "thrusts" + projectiletype = /obj/item/projectile/f13plasma/pistol/adam + projectilesound = 'sound/weapons/wave.ogg' projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(PLASMA_VOLUME), @@ -206,23 +317,32 @@ SP_DISTANT_RANGE(PLASMA_RANGE_DISTANT) ) -// Enclave Armored Infantry +// ENCLAVE ARMORED INFANTRY - heavy trooper /mob/living/simple_animal/hostile/enclave/soldier name = "enclave armored infantry" desc = "An Enclave Soldier wearing Advanced Power Armor and a plasma multi-caster. Play time's over, mutie." icon_state = "enclave_armored" icon_living = "enclave_armored" + icon_dead = "enclave_armored" + + mob_armor = ARMOR_VALUE_ENCLAVE_APA + maxHealth = 560 - health = 650 + health = 560 stat_attack = UNCONSCIOUS + melee_damage_lower = 20 melee_damage_upper = 47 - extra_projectiles = 2 + + loot = list(/obj/effect/mob_spawn/human/corpse/enclave/soldier) + + // Pure ranged - aggressive + combat_mode = COMBAT_MODE_RANGED retreat_distance = 3 - minimum_distance = 5 + minimum_distance = 1 + ranged_cooldown_time = 12 - loot = list(/obj/effect/mob_spawn/human/corpse/enclave/soldier) - healable = 1 + extra_projectiles = 2 attack_verb_simple = "power-fists" projectiletype = /obj/item/projectile/f13plasma/repeater projectilesound = 'sound/f13weapons/plasmarifle.ogg' @@ -237,7 +357,7 @@ SP_DISTANT_RANGE(PLASMA_RANGE_DISTANT) ) -// Enclave corpses +// ENCLAVE CORPSES /obj/effect/mob_spawn/human/corpse/enclavescientist name = "enclave scientist" uniform = /obj/item/clothing/under/f13/enclave/science @@ -261,66 +381,107 @@ gloves = /obj/item/clothing/gloves/f13/military mask = /obj/item/clothing/mask/gas/enclave - ///////////////////// // BROTHERHOOD NPC // ///////////////////// +// BASE BROTHERHOOD /mob/living/simple_animal/hostile/bs - name = "BS" + name = "Brotherhood Knight" desc = "The brotherhood never fails." icon = 'icons/fallout/mobs/humans/fallout_npc.dmi' icon_state = "bs_knight" icon_living = "bs_knight" icon_dead = "bs_knight" + mob_biotypes = MOB_ORGANIC|MOB_HUMANOID - faction = list("BOS") - turns_per_move = 5 - response_help_simple = "pokes" - response_disarm_simple = "shoves" - response_harm_simple = "hits" - speed = 1 - stat_attack = 1 - robust_searching = 1 + mob_armor = ARMOR_VALUE_BOS + maxHealth = 200 health = 200 - harm_intent_damage = 8 + speed = 1 + turns_per_move = 5 + melee_damage_lower = 7 melee_damage_upper = 15 + harm_intent_damage = 8 + + robust_searching = TRUE + stat_attack = CONSCIOUS + + response_help_simple = "pokes" + response_disarm_simple = "shoves" + response_harm_simple = "hits" attack_verb_simple = "pistol-whips" attack_sound = 'sound/weapons/punch1.ogg' - a_intent = INTENT_HARM - loot = list(/obj/effect/mob_spawn/human/corpse/bs) + + speak = list("Semper Invicta!") + speak_emote = list("rushes") + speak_chance = 1 + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) unsuitable_atmos_damage = 15 - check_friendly_fire = 1 + + faction = list("BOS") + a_intent = INTENT_HARM + check_friendly_fire = TRUE status_flags = CANPUSH del_on_death = TRUE - speak = list("Semper Invicta!") - speak_emote = list("rushes") - speak_chance = 1 - -/obj/effect/mob_spawn/human/corpse/bs - name = "Brotherhood Knight" - uniform = /obj/item/clothing/under/syndicate/brotherhood - suit = /obj/item/clothing/suit/armor/medium/combat/brotherhood - shoes = /obj/item/clothing/shoes/combat/swat - gloves = /obj/item/clothing/gloves/combat - belt = /obj/item/storage/belt/army/assault - mask = /obj/item/clothing/mask/gas/sechailer - head = /obj/item/clothing/head/helmet/f13/combat/brotherhood - + + loot = list(/obj/effect/mob_spawn/human/corpse/bs) + + // Z-movement + can_z_move = TRUE + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 30 + + can_open_doors = TRUE + can_open_airlocks = TRUE + + // Pure melee + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + +/mob/living/simple_animal/hostile/bs/Aggro() + . = ..() + if(.) + return + summon_backup(10) + +/mob/living/simple_animal/hostile/bs/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + +// Friendly fire resistance - brotherhood members are well-trained +/mob/living/simple_animal/hostile/bs/bullet_act(obj/item/projectile/P) + if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/bs)) + var/original_damage = P.damage + P.damage *= 0.2 + . = ..() + P.damage = original_damage + return + return ..() + +// BROTHERHOOD KNIGHT - laser pistol /mob/living/simple_animal/hostile/bs/knight name = "Brotherhood Knight" desc = "A Brotherhood Knight wielding a laser pistol and older issue Brotherhood combat armor." - icon_state = "bs_knight" - icon_living = "bs_knight" - icon_dead = "bs_knight" + + healable = TRUE + + // Pure ranged + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE retreat_distance = 5 - minimum_distance = 5 - loot = list(/obj/effect/mob_spawn/human/corpse/bs) - healable = 1 - ranged = 1 + minimum_distance = 1 + + ranged_cooldown_time = 2 SECONDS projectiletype = /obj/item/projectile/beam/laser/pistol/hitscan projectilesound = 'sound/f13weapons/aep7fire.ogg' projectile_sound_properties = list( @@ -334,20 +495,30 @@ SP_DISTANT_RANGE(LASER_RANGE_DISTANT) ) +// BROTHERHOOD PALADIN - laser rifle + power armor /mob/living/simple_animal/hostile/bs/paladin name = "Brotherhood Paladin" desc = "A Paladin equipped with an AER9 and T-51b power armor. The Brotherhood has arrived." icon_state = "bs_paladin" icon_living = "bs_paladin" icon_dead = "bs_paladin" - retreat_distance = 5 - minimum_distance = 5 - loot = list(/obj/effect/mob_spawn/human/corpse/bs/paladin) + + mob_armor = ARMOR_VALUE_BOS_PALADIN + maxHealth = 480 health = 480 stat_attack = UNCONSCIOUS - healable = 1 - ranged = 1 + + loot = list(/obj/effect/mob_spawn/human/corpse/bs/paladin) + healable = TRUE + + // Pure ranged + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 5 + minimum_distance = 1 + + ranged_cooldown_time = 2 SECONDS projectiletype = /obj/item/projectile/beam/laser/lasgun/hitscan projectilesound = 'sound/f13weapons/aer9fire.ogg' projectile_sound_properties = list( @@ -361,74 +532,128 @@ SP_DISTANT_RANGE(LASER_RANGE_DISTANT) ) +// BROTHERHOOD CORPSES +/obj/effect/mob_spawn/human/corpse/bs + name = "Brotherhood Knight" + uniform = /obj/item/clothing/under/syndicate/brotherhood + suit = /obj/item/clothing/suit/armor/medium/combat/brotherhood + shoes = /obj/item/clothing/shoes/combat/swat + gloves = /obj/item/clothing/gloves/combat + belt = /obj/item/storage/belt/army/assault + mask = /obj/item/clothing/mask/gas/sechailer + head = /obj/item/clothing/head/helmet/f13/combat/brotherhood + /obj/effect/mob_spawn/human/corpse/bs/paladin name = "Brotherhood Paladin" uniform = /obj/item/clothing/under/f13/recon suit = /obj/item/clothing/suit/armor/power_armor/t51b/bos shoes = /obj/item/clothing/shoes/combat/swat gloves = /obj/item/clothing/gloves/combat - belt = /obj/item/storage/belt/army/assault + belt = /obj/item/storage/belt/army/assault mask = /obj/item/clothing/mask/gas/sechailer head = /obj/item/clothing/head/helmet/f13/power_armor/t51b/bos - /////////////// // NCR = NPC // /////////////// +// BASE NCR /mob/living/simple_animal/hostile/ncr - name = "NCR" + name = "NCR Trooper" desc = "For the Republic!" icon = 'icons/fallout/mobs/humans/fallout_npc.dmi' icon_state = "ncr_trooper" icon_living = "ncr_trooper" icon_dead = "ncr_trooper" - faction = list("NCR") + mob_biotypes = MOB_ORGANIC|MOB_HUMANOID - turns_per_move = 5 - response_help_simple = "pokes" - response_disarm_simple = "shoves" - response_harm_simple = "hits" - speed = 1 - stat_attack = 1 - robust_searching = 1 + mob_armor = ARMOR_VALUE_NCR + maxHealth = 120 health = 120 - harm_intent_damage = 8 + speed = 1 + turns_per_move = 5 + melee_damage_lower = 8 melee_damage_upper = 15 - attack_verb_simple = "áüåò" + harm_intent_damage = 8 + + robust_searching = TRUE + stat_attack = CONSCIOUS + + response_help_simple = "pokes" + response_disarm_simple = "shoves" + response_harm_simple = "hits" + attack_verb_simple = "attacks" attack_sound = 'sound/weapons/punch1.ogg' - a_intent = INTENT_HARM - loot = list(/obj/effect/mob_spawn/human/corpse/ncr) + + speak = list("Patrolling the Mojave almost makes you wish for a nuclear winter.", "When I got this assignment I was hoping there would be more gambling.", "It's been a long tour, all I can think about now is going back home.", "You know, if you were serving, you'd probably be halfway to general by now.", "You oughtta think about enlisting. We need you here.") + speak_emote = list("says") + speak_chance = 1 + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) unsuitable_atmos_damage = 15 - check_friendly_fire = 1 + + faction = list("NCR") + a_intent = INTENT_HARM + check_friendly_fire = TRUE status_flags = CANPUSH del_on_death = TRUE - speak = list("Patrolling the Mojave almost makes you wish for a nuclear winter.", "When I got this assignment I was hoping there would be more gambling.", "It's been a long tour, all I can think about now is going back home.", "You know, if you were serving, you'd probably be halfway to general by now.", "You oughtta think about enlisting. We need you here.") - speak_emote = list("says") - speak_chance = 1 - -/obj/effect/mob_spawn/human/corpse/ncr - name = "NCR Trooper" - uniform = /obj/item/clothing/under/f13/ncr - suit = /obj/item/clothing/suit/armor/ncrarmor - belt = /obj/item/storage/belt/army/assault/ncr - shoes = /obj/item/clothing/shoes/f13/military/ncr - head = /obj/item/clothing/head/f13/ncr - + + loot = list(/obj/effect/mob_spawn/human/corpse/ncr) + + // Z-movement + can_z_move = TRUE + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 30 + + can_open_doors = TRUE + can_open_airlocks = TRUE + + // Pure melee + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + +/mob/living/simple_animal/hostile/ncr/Aggro() + . = ..() + if(.) + return + summon_backup(10) + +/mob/living/simple_animal/hostile/ncr/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + +// Friendly fire resistance - NCR troopers are trained soldiers +/mob/living/simple_animal/hostile/ncr/bullet_act(obj/item/projectile/P) + if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/ncr)) + var/original_damage = P.damage + P.damage *= 0.2 + . = ..() + P.damage = original_damage + return + return ..() + +// NCR TROOPER - service rifle /mob/living/simple_animal/hostile/ncr/trooper name = "NCR Trooper" desc = "A standard NCR Trooper wielding a service rifle and equipped with a patrol vest." - icon_state = "ncr_trooper" - icon_living = "ncr_trooper" - icon_dead = "ncr_trooper" + + healable = TRUE + + // Pure ranged + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE retreat_distance = 5 - minimum_distance = 5 - loot = list(/obj/effect/mob_spawn/human/corpse/ncr) - healable = 1 - ranged = 1 + minimum_distance = 1 + + ranged_cooldown_time = 2 SECONDS projectiletype = /obj/item/projectile/bullet/a556/simple projectilesound = 'sound/f13weapons/varmint_rifle.ogg' casingtype = /obj/item/ammo_casing/a556 @@ -443,19 +668,29 @@ SP_DISTANT_RANGE(RIFLE_LIGHT_RANGE_DISTANT) ) +// NCR RANGER - revolver /mob/living/simple_animal/hostile/ncr/ranger name = "NCR Ranger" desc = "A Ranger of the NCRA, wielding a big iron on his hip and equipped with a ranger patrol vest." icon_state = "ncr_sergeant" icon_living = "ncr_sergeant" icon_dead = "ncr_sergeant" - retreat_distance = 5 - minimum_distance = 5 - loot = list(/obj/effect/mob_spawn/human/corpse/ncr/ranger) + + mob_armor = ARMOR_VALUE_NCR_RANGER + maxHealth = 160 health = 160 - healable = 1 - ranged = 1 + + loot = list(/obj/effect/mob_spawn/human/corpse/ncr/ranger) + healable = TRUE + + // Pure ranged + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 5 + minimum_distance = 1 + + ranged_cooldown_time = 2 SECONDS projectiletype = /obj/item/projectile/bullet/m44/simple projectilesound = 'sound/f13weapons/44mag.ogg' casingtype = /obj/item/ammo_casing/m44 @@ -469,6 +704,16 @@ SP_DISTANT_SOUND(PISTOL_HEAVY_DISTANT_SOUND), SP_DISTANT_RANGE(PISTOL_HEAVY_RANGE_DISTANT) ) + +// NCR CORPSES +/obj/effect/mob_spawn/human/corpse/ncr + name = "NCR Trooper" + uniform = /obj/item/clothing/under/f13/ncr + suit = /obj/item/clothing/suit/armor/ncrarmor + belt = /obj/item/storage/belt/army/assault/ncr + shoes = /obj/item/clothing/shoes/f13/military/ncr + head = /obj/item/clothing/head/f13/ncr + /obj/effect/mob_spawn/human/corpse/ncr/ranger name = "NCR Ranger" uniform = /obj/item/clothing/under/f13/ranger/patrol @@ -481,59 +726,103 @@ // LEGION NPC // //////////////// +// BASE LEGION /mob/living/simple_animal/hostile/legion - name = "Legion" + name = "Legion Prime" desc = "True to Caesar." icon = 'icons/fallout/mobs/humans/fallout_npc.dmi' icon_state = "legion_prime" icon_living = "legion_prime" icon_dead = "legion_prime" - faction = list("Legion") + mob_biotypes = MOB_ORGANIC|MOB_HUMANOID - turns_per_move = 5 - response_help_simple = "pokes" - response_disarm_simple = "shoves" - response_harm_simple = "hits" - speed = 1 - stat_attack = 1 - robust_searching = 1 + mob_armor = ARMOR_VALUE_LEGION + maxHealth = 120 health = 120 - harm_intent_damage = 8 + speed = 1 + turns_per_move = 5 + melee_damage_lower = 8 melee_damage_upper = 15 + harm_intent_damage = 8 + + robust_searching = TRUE + stat_attack = CONSCIOUS + + response_help_simple = "pokes" + response_disarm_simple = "shoves" + response_harm_simple = "hits" attack_verb_simple = "attacks" attack_sound = 'sound/weapons/punch1.ogg' - a_intent = INTENT_HARM - loot = list(/obj/effect/mob_spawn/human/corpse/legion) + + speak = list("Ave, true to Caesar.", "True to Caesar.", "Ave, Amicus.", "The new slave girls are quite beautiful.", "Give me cause, Profligate.", "Degenerates like you belong on a cross.") + speak_emote = list("says") + speak_chance = 1 + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) unsuitable_atmos_damage = 15 - check_friendly_fire = 1 + + faction = list("Legion") + a_intent = INTENT_HARM + check_friendly_fire = TRUE status_flags = CANPUSH del_on_death = TRUE - speak = list("Ave, true to Caesar.", "True to Caesar.", "Ave, Amicus.", "The new slave girls are quite beautiful.", "Give me cause, Profligate.", "Degenerates like you belong on a cross.") - speak_emote = list("says") - speak_chance = 1 - -/obj/effect/mob_spawn/human/corpse/legion - name = "Legion Prime" - uniform = /obj/item/clothing/under/f13/legskirt - suit = /obj/item/clothing/suit/armor/legion/prime - shoes = /obj/item/clothing/shoes/f13/military/legion - head = /obj/item/clothing/head/helmet/f13/legion/prime - + + loot = list(/obj/effect/mob_spawn/human/corpse/legion) + + // Z-movement + can_z_move = TRUE + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 30 + + can_open_doors = TRUE + can_open_airlocks = TRUE + + // Pure melee + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + +/mob/living/simple_animal/hostile/legion/Aggro() + . = ..() + if(.) + return + summon_backup(10) + +/mob/living/simple_animal/hostile/legion/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + +// Friendly fire resistance - legion soldiers fight in formation +/mob/living/simple_animal/hostile/legion/bullet_act(obj/item/projectile/P) + if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/legion)) + var/original_damage = P.damage + P.damage *= 0.2 + . = ..() + P.damage = original_damage + return + return ..() + +// LEGION PRIME - hunting rifle /mob/living/simple_animal/hostile/legion/prime name = "Legion Prime" desc = "A Prime Legionary, equipped with a hunting rifle." - icon_state = "legion_prime" - icon_living = "legion_prime" - icon_dead = "legion_prime" - icon_gib = "legion_prime" + + healable = TRUE + + // Pure ranged + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE retreat_distance = 5 - minimum_distance = 5 - loot = list(/obj/effect/mob_spawn/human/corpse/legion) - healable = 1 - ranged = 1 + minimum_distance = 1 + + ranged_cooldown_time = 2 SECONDS projectiletype = /obj/item/projectile/bullet/a762/sport/simple projectilesound = 'sound/f13weapons/hunting_rifle.ogg' casingtype = /obj/item/ammo_casing/a308 @@ -548,6 +837,7 @@ SP_DISTANT_RANGE(RIFLE_MEDIUM_RANGE_DISTANT) ) +// LEGION DECANUS - veteran officer /mob/living/simple_animal/hostile/legion/decan name = "Legion Decanus" desc = "A Prime Decanus, equipped with a hunting rifle." @@ -555,13 +845,22 @@ icon_living = "legion_decan" icon_dead = "legion_decan" icon_gib = "legion_decan" - retreat_distance = 5 - minimum_distance = 5 - loot = list(/obj/effect/mob_spawn/human/corpse/legion/decan) + + mob_armor = ARMOR_VALUE_LEGION_VETERAN + maxHealth = 180 health = 180 - healable = 1 - ranged = 1 + + loot = list(/obj/effect/mob_spawn/human/corpse/legion/decan) + healable = TRUE + + // Pure ranged + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 5 + minimum_distance = 1 + + ranged_cooldown_time = 2 SECONDS projectiletype = /obj/item/projectile/bullet/a762/sport/simple projectilesound = 'sound/f13weapons/hunting_rifle.ogg' casingtype = /obj/item/ammo_casing/a308 @@ -576,7 +875,13 @@ SP_DISTANT_RANGE(RIFLE_MEDIUM_RANGE_DISTANT) ) -/mob/living/simple_animal/hostile/legion/decan +// LEGION CORPSES +/obj/effect/mob_spawn/human/corpse/legion + name = "Legion Prime" + uniform = /obj/item/clothing/under/f13/legskirt + suit = /obj/item/clothing/suit/armor/legion/prime + shoes = /obj/item/clothing/shoes/f13/military/legion + head = /obj/item/clothing/head/helmet/f13/legion/prime /obj/effect/mob_spawn/human/corpse/legion/decan name = "Legion Decanus" @@ -597,26 +902,63 @@ icon_state = "tribal_raider" icon_living = "tribal_raider" icon_dead = "tribal_raider_dead" - faction = list("Tribe") + mob_biotypes = MOB_ORGANIC|MOB_HUMANOID - turns_per_move = 5 - response_help_simple = "pokes" - response_disarm_simple = "shoves" - response_harm_simple = "hits" - speed = 1 - stat_attack = 1 - robust_searching = 1 + mob_armor = ARMOR_VALUE_TRIBAL + maxHealth = 160 health = 160 - harm_intent_damage = 8 + speed = 1 + turns_per_move = 5 + melee_damage_lower = 22 melee_damage_upper = 47 + harm_intent_damage = 8 + + robust_searching = TRUE + stat_attack = CONSCIOUS + + response_help_simple = "pokes" + response_disarm_simple = "shoves" + response_harm_simple = "hits" attack_verb_simple = "attacks" attack_sound = 'sound/weapons/bladeslice.ogg' - a_intent = INTENT_HARM - atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) - unsuitable_atmos_damage = 15 - status_flags = CANPUSH + speak = list("For our kin!", "This will be a good hunt.", "The gods look upon me today.") speak_emote = list("says") speak_chance = 1 + + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) + unsuitable_atmos_damage = 15 + + faction = list("Tribe") + a_intent = INTENT_HARM + status_flags = CANPUSH + + // Z-movement + can_z_move = TRUE + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 30 + + can_open_doors = TRUE + can_open_airlocks = TRUE + + // Pure melee - glaive fighter + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + +/mob/living/simple_animal/hostile/tribe/Aggro() + . = ..() + if(.) + return + summon_backup(10) + +/mob/living/simple_animal/hostile/tribe/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() diff --git a/code/modules/mob/living/simple_animal/hostile/f13/ghoul.dm b/code/modules/mob/living/simple_animal/hostile/f13/ghoul.dm index ef8f558945f..db1c135675f 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/ghoul.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/ghoul.dm @@ -1,8 +1,11 @@ /* IN THIS FILE --Ghouls +- Ghouls */ -//Base Ghoul +// ============================================================ +// BASE GHOUL +// ============================================================ + /mob/living/simple_animal/hostile/ghoul name = "feral ghoul" desc = "A ghoul that has lost its mind and become aggressive." @@ -13,38 +16,27 @@ can_ghost_into = TRUE mob_biotypes = MOB_ORGANIC|MOB_HUMANOID mob_armor = ARMOR_VALUE_GHOUL_NAKED - maxHealth = 40 + maxHealth = 40 health = 40 robust_searching = 1 - move_to_delay = 2.0 + move_to_delay = 3 turns_per_move = 5 waddle_amount = 2 waddle_up_time = 1 waddle_side_time = 1 - speak_emote = list( - "growls", - ) - emote_see = list( - "sniffs the air", - "growls", - "foams at the mouth", - ) + + speak_emote = list("growls") + emote_see = list("sniffs the air", "growls", "foams at the mouth") + a_intent = INTENT_HARM speed = 1 harm_intent_damage = 8 melee_damage_lower = 6 melee_damage_upper = 12 - attack_verb_simple = list( - "claws", - "maims", - "bites", - "mauls", - "slashes", - "thrashes", - "bashes", - "glomps" - ) + + attack_verb_simple = list("claws", "maims", "bites", "mauls", "slashes", "thrashes", "bashes", "glomps") attack_sound = 'sound/hallucinations/growl1.ogg' + atmos_requirements = list( "min_oxy" = 5, "max_oxy" = 0, @@ -54,49 +46,38 @@ "max_co2" = 5, "min_n2" = 0, "max_n2" = 0 - ) - + ) + unsuitable_atmos_damage = 20 gold_core_spawnable = HOSTILE_SPAWN faction = list("hostile", "ghoul") decompose = TRUE - sharpness = SHARP_EDGED //They need to cut their finger nails + sharpness = SHARP_EDGED + guaranteed_butcher_results = list( /obj/item/reagent_containers/food/snacks/meat/slab/human/ghoul = 2, /obj/item/stack/sheet/animalhide/human = 1, /obj/item/stack/sheet/bone = 1 - ) + ) emote_taunt_sound = list('sound/f13npc/ghoul/taunt.ogg') - emote_taunt = list( - "gurgles", - "stares", - "foams at the mouth", - "groans", - "growls", - "jibbers", - "howls madly", - "screeches", - "charges" - ) + emote_taunt = list("gurgles", "stares", "foams at the mouth", "groans", "growls", "jibbers", "howls madly", "screeches", "charges") tastes = list("decay" = 1, "mud" = 1) taunt_chance = 30 aggrosound = list('sound/f13npc/ghoul/aggro1.ogg', 'sound/f13npc/ghoul/aggro2.ogg') idlesound = list('sound/f13npc/ghoul/idle.ogg', 'sound/effects/scrungy.ogg') death_sound = 'sound/f13npc/ghoul/ghoul_death.ogg' + loot = list(/obj/item/stack/f13Cash/random/low/lowchance) - /// How many things to drop on death? Set to MOB_LOOT_ALL to just drop everything in the list loot_drop_amount = 1 - /// Drop 1 - loot_drop_amount? False always drops loot_drop_amount items loot_amount_random = TRUE - /// slots in a list of trash loot var/random_trash_loot = TRUE + footstep_type = FOOTSTEP_MOB_BAREFOOT - can_ghost_into = TRUE desc_short = "A flimsy creature that may or may not be a reanimated corpse." pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS - + variation_list = list( MOB_COLOR_VARIATION(150, 150, 150, 255, 255, 255), MOB_SPEED_LIST(2.3, 2.5, 2.8, 2.9, 3.0), @@ -109,30 +90,55 @@ ) can_z_move = TRUE - z_move_delay = 50 // 5 seconds - slower + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 50 + + can_open_doors = TRUE + can_open_airlocks = FALSE + + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 /mob/living/simple_animal/hostile/ghoul/Initialize() . = ..() if(random_trash_loot) loot = GLOB.trash_ammo + GLOB.trash_chem + GLOB.trash_clothing + GLOB.trash_craft + GLOB.trash_gun + GLOB.trash_misc + GLOB.trash_money + GLOB.trash_mob + GLOB.trash_part + GLOB.trash_tool + GLOB.trash_attachment - /mob/living/simple_animal/hostile/ghoul/Aggro() . = ..() if(.) return - summon_backup(15) + summon_backup(10) if(!ckey) say(pick("*scrungy", "*mbark")) +// Friendly fire resistance - ghouls are tough and fight in packs +/mob/living/simple_animal/hostile/ghoul/bullet_act(obj/item/projectile/P) + if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/ghoul)) + var/original_damage = P.damage + P.damage *= 0.2 + . = ..() + P.damage = original_damage + return + return ..() + +// The parent /hostile already handles stat_attack = CONSCIOUS correctly. +// The override was also incorrectly blocking legendary ghoul's stat_attack = UNCONSCIOUS +// from working, since it ran before the parent check. /mob/living/simple_animal/hostile/ghoul/become_the_mob(mob/user) call_backup = /obj/effect/proc_holder/mob_common/summon_backup/ghoul send_mobs = /obj/effect/proc_holder/mob_common/direct_mobs/ghoul . = ..() +// ============================================================ +// GHOUL REAVER +// Armored, throws rocks at range, stronger than base ghoul +// ============================================================ -// Ghoul Reaver /mob/living/simple_animal/hostile/ghoul/reaver name = "feral ghoul reaver" desc = "A ghoul that has lost its mind and become aggressive. This one is strapped with metal armor, and appears far stronger." @@ -144,21 +150,16 @@ maxHealth = 50 health = 50 rapid_melee = 2 - retreat_distance = 3 - minimum_distance = 1 - ranged = TRUE - ranged_message = "throws a rock" - ranged_cooldown_time = 3 SECONDS - projectiletype = /obj/item/projectile/bullet/ghoul_rock - projectilesound = 'sound/weapons/punchmiss.ogg' + move_to_delay = 2.5 + harm_intent_damage = 8 melee_damage_lower = 8 melee_damage_upper = 14 + loot = list(/obj/item/stack/f13Cash/random/low/medchance) loot_drop_amount = 2 - footstep_type = FOOTSTEP_MOB_BAREFOOT - can_ghost_into = TRUE pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS + desc_short = "A beefy creature that may or may not be a reanimated corpse." variation_list = list( MOB_COLOR_VARIATION(200, 200, 200, 255, 255, 255), @@ -175,35 +176,52 @@ MOB_PROJECTILE_ENTRY(/obj/item/projectile/bullet/ghoul_rock/jagged_scrap, 1)\ ) ) - desc_short = "A beefy creature that may or may not be a reanimated corpse." -/mob/living/simple_animal/hostile/ghoul/reaver/Initialize() - . = ..() + combat_mode = COMBAT_MODE_MIXED + ranged = TRUE + retreat_distance = 3 + minimum_distance = 1 + ranged_message = "throws a rock" + ranged_cooldown_time = 3 SECONDS + projectiletype = /obj/item/projectile/bullet/ghoul_rock + projectilesound = 'sound/weapons/punchmiss.ogg' /mob/living/simple_animal/hostile/ghoul/reaver/Aggro() - ..() - summon_backup(10) + . = ..() + if(.) + return + summon_backup(8) +// Military ghoul variants - former US Army, can't be ghost-inhabited /mob/living/simple_animal/hostile/ghoul/reaver/ncr name = "feral ghoul soldier" desc = "A former US Army combatant, now ghoulified and insane. The armor that failed it in life still packs some good defense." maxHealth = 60 + health = 60 can_ghost_into = FALSE /mob/living/simple_animal/hostile/ghoul/reaver/ncr_helmet name = "plated feral ghoul soldier" desc = "A former US Army combatant, now ghoulified and insane. The armor that failed it in life still packs some good defense." maxHealth = 60 + health = 60 can_ghost_into = FALSE /mob/living/simple_animal/hostile/ghoul/reaver/ncr_officer name = "feral ghoul officer" desc = "A former US Army officer, now ghoulified and insane. The armor that failed it in life still packs some good defense." maxHealth = 60 + health = 60 speed = 3 can_ghost_into = FALSE -//Cold Feral Ghoul +// ============================================================ +// COLD / FROZEN GHOULS +// FIX: frozenreaver now inherits from coldferal to avoid duplication +// NOTE: Neither has atmos_requirements override - they still need oxygen. +// Add vacuum-safe atmos list here if they're meant for cold/airless maps. +// ============================================================ + /mob/living/simple_animal/hostile/ghoul/coldferal name = "cold ghoul feral" desc = "A ghoul that has lost its mind and become aggressive. This one is strapped with metal armor, and appears far stronger." @@ -218,28 +236,21 @@ melee_damage_upper = 15 loot = list(/obj/item/stack/f13Cash/random/low/medchance) loot_drop_amount = 2 - footstep_type = FOOTSTEP_MOB_BAREFOOT can_ghost_into = FALSE -//Frozen Feral Ghoul -/mob/living/simple_animal/hostile/ghoul/frozenreaver +/mob/living/simple_animal/hostile/ghoul/coldferal/frozenreaver name = "frozen ghoul reaver" desc = "A ghoul that has lost its mind and become aggressive. This one is strapped with metal armor, and appears far stronger." icon_state = "frozen_reaver" icon_living = "frozen_reaver" icon_dead = "frozen_reaver_dead" - speed = 1.5 - maxHealth = 80 - health = 80 - harm_intent_damage = 8 - melee_damage_lower = 10 - melee_damage_upper = 15 - loot = list(/obj/item/stack/f13Cash/random/low/medchance) loot_drop_amount = 4 - footstep_type = FOOTSTEP_MOB_BAREFOOT - can_ghost_into = FALSE -//Legendary Ghoul +// ============================================================ +// LEGENDARY GHOUL +// Enrages at 30% HP - faster and more dangerous +// ============================================================ + /mob/living/simple_animal/hostile/ghoul/legendary name = "legendary ghoul" desc = "A ghoul that has lost its mind and become aggressive. This one has exceptionally large, bulging muscles. It looks quite strong." @@ -249,30 +260,68 @@ color = "#FFFF00" mob_armor = ARMOR_VALUE_GHOUL_LEGEND can_ghost_into = FALSE - maxHealth = 200 - health = 200 - stat_attack = UNCONSCIOUS + maxHealth = 160 + health = 160 + stat_attack = UNCONSCIOUS // Can attack downed players speed = 2.5 harm_intent_damage = 8 melee_damage_lower = 20 melee_damage_upper = 35 - mob_size = 5 + mob_size = MOB_SIZE_HUGE // FIX: was raw 5, use the defined constant wound_bonus = 0 bare_wound_bonus = 0 loot = list(/obj/item/stack/f13Cash/random/med) loot_drop_amount = 5 loot_amount_random = FALSE - footstep_type = FOOTSTEP_MOB_BAREFOOT - can_ghost_into = FALSE //heeeeeell no pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS desc_short = "A deadly creature that may or may not be reanimated jerky." + low_health_threshold = 0.3 // Enrages at 30% HP + var/color_rage = "#ff6666" + /mob/living/simple_animal/hostile/ghoul/legendary/become_the_mob(mob/user) call_backup = null send_mobs = null . = ..() -//Glowing Ghoul +/// RAGE MODE - legendary ghoul becomes faster and deadlier at low health +/mob/living/simple_animal/hostile/ghoul/legendary/make_low_health() + if(!target) + return // Not in combat, don't rage + ..() + visible_message(span_danger("[src] howls with primal fury!!!")) + playsound(src, pick(aggrosound), 100, 1, SOUND_DISTANCE(15)) + color = color_rage + speed *= 0.7 + melee_damage_lower = round(melee_damage_lower * 1.4) + melee_damage_upper = round(melee_damage_upper * 1.4) + wound_bonus += 15 + bare_wound_bonus += 30 + is_low_health = TRUE + +/// Calming down from rage (health recovered above threshold) +/mob/living/simple_animal/hostile/ghoul/legendary/make_high_health() + visible_message(span_notice("[src]'s fury subsides.")) + color = initial(color) + speed = initial(speed) + melee_damage_lower = initial(melee_damage_lower) + melee_damage_upper = initial(melee_damage_upper) + wound_bonus = initial(wound_bonus) + bare_wound_bonus = initial(bare_wound_bonus) + is_low_health = FALSE + +/mob/living/simple_animal/hostile/ghoul/legendary/Aggro() + . = ..() + if(is_low_health) + return + if(health < (maxHealth * low_health_threshold)) + make_low_health() + +// ============================================================ +// GLOWING GHOUL +// Ranged radiation attacker, heals nearby ghouls passively +// ============================================================ + /mob/living/simple_animal/hostile/ghoul/glowing name = "glowing feral ghoul" desc = "A feral ghoul that has absorbed massive amounts of radiation, causing them to glow in the dark and radiate constantly." @@ -280,22 +329,14 @@ icon_living = "glowinghoul" icon_dead = "glowinghoul_dead" mob_armor = ARMOR_VALUE_GHOUL_GLOWING - maxHealth = 40 + maxHealth = 40 health = 40 speed = 2 - retreat_distance = 4 - minimum_distance = 4 - ranged_message = "emits radiation" - ranged = TRUE - projectiletype = /obj/item/projectile/radiation_thing - projectilesound = 'sound/weapons/etherealhit.ogg' harm_intent_damage = 8 melee_damage_lower = 10 melee_damage_upper = 22 light_system = MOVABLE_LIGHT light_range = 2 - footstep_type = FOOTSTEP_MOB_BAREFOOT - can_ghost_into = TRUE pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS desc_short = "A glowing creature that may or may not be a reanimated corpse." loot_drop_amount = 2 @@ -311,23 +352,25 @@ MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(50) ) + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 4 + minimum_distance = 4 + ranged_message = "emits radiation" + ranged_cooldown_time = 2 SECONDS + projectiletype = /obj/item/projectile/radiation_thing + projectilesound = 'sound/weapons/etherealhit.ogg' + /mob/living/simple_animal/hostile/ghoul/glowing/Initialize(mapload) . = ..() - // we only heal BRUTELOSS because each type directly heals a simplemob's health - // therefore setting it to BRUTELOSS | FIRELOSS | TOXLOSS | OXYLOSS would mean healing 4x as much - // aka 40% of max life every tick, which is basically unkillable - // TODO: refactor this if simple_animals ever get damage types + // restrict_faction = null means ALL nearby ghouls benefit from the aura AddComponent(/datum/component/glow_heal, chosen_targets = /mob/living/simple_animal/hostile/ghoul, allow_revival = FALSE, restrict_faction = null, type_healing = BRUTELOSS) -/obj/item/projectile/radiation_thing - name = "radiation" - damage = 0 - irradiate = 20 - icon_state = "declone" - /mob/living/simple_animal/hostile/ghoul/glowing/Aggro() - ..() - summon_backup(10) + . = ..() + if(.) + return + summon_backup(8) /mob/living/simple_animal/hostile/ghoul/glowing/AttackingTarget() . = ..() @@ -335,16 +378,20 @@ var/mob/living/carbon/human/H = target H.apply_effect(20, EFFECT_IRRADIATE, 0) -/mob/living/simple_animal/hostile/ghoul/glowing/strong // FEV mutation - maxHealth = 256 - health = 256 - speed = 1.4 // Nyooom +// FEV-mutated glowing ghoul - much tougher +/mob/living/simple_animal/hostile/ghoul/glowing/strong + maxHealth = 180 + health = 180 + speed = 1.4 can_ghost_into = FALSE melee_damage_lower = 25 melee_damage_upper = 30 armour_penetration = 0.1 -//Alive Ghoul +// ============================================================ +// SOLDIER GHOULS (living/allied) +// ============================================================ + /mob/living/simple_animal/hostile/ghoul/soldier name = "ghoul soldier" desc = "Have you ever seen a living ghoul before?
Ghouls are necrotic post-humans - decrepit, rotting, zombie-like mutants." @@ -352,15 +399,12 @@ icon_living = "soldier_ghoul" icon_dead = "soldier_ghoul_d" icon_gib = "syndicate_gib" - mob_armor = ARMOR_VALUE_GHOUL_NAKED - maxHealth = 60 + maxHealth = 60 health = 60 loot = list(/obj/item/stack/f13Cash/random/low/medchance) loot_drop_amount = 2 - footstep_type = FOOTSTEP_MOB_BAREFOOT can_ghost_into = FALSE -//Alive Ghoul /mob/living/simple_animal/hostile/ghoul/soldier/armored name = "armored ghoul soldier" desc = "Have you ever seen a living ghoul before?
Ghouls are necrotic post-humans - decrepit, rotting, zombie-like mutants." @@ -368,14 +412,15 @@ icon_living = "soldier_ghoul_a" icon_dead = "soldier_ghoul_a_d" icon_gib = "syndicate_gib" - mob_armor = ARMOR_VALUE_GHOUL_NAKED - maxHealth = 80 + maxHealth = 80 health = 80 - footstep_type = FOOTSTEP_MOB_BAREFOOT - can_ghost_into = FALSE loot_drop_amount = 3 -//Alive Ghoul +// ============================================================ +// SCORCHED GHOULS +// Faction "scorched" - separate from base ghoul faction +// ============================================================ + /mob/living/simple_animal/hostile/ghoul/scorched name = "scorched ghoul soldier" desc = "Have you ever seen a living ghoul before?
Ghouls are necrotic post-humans - decrepit, rotting, zombie-like mutants." @@ -384,98 +429,88 @@ icon_dead = "scorched_m_d" icon_gib = "syndicate_gib" speak_chance = 1 - environment_smash = 0 + environment_smash = 0 // Scorched don't smash furniture unlike feral ghouls response_help_simple = "hugs" response_disarm_simple = "pushes aside" response_harm_simple = "growl" - move_to_delay = 3.0 + move_to_delay = 3 faction = list("scorched", "hostile") death_sound = null melee_damage_upper = 20 aggro_vision_range = 10 attack_verb_simple = "punches" attack_sound = "punch" - footstep_type = FOOTSTEP_MOB_BAREFOOT can_ghost_into = FALSE loot_drop_amount = 4 -//Alive Ghoul Ranged /mob/living/simple_animal/hostile/ghoul/scorched/ranged - name = "Ranged Ghoul Solder" + name = "ranged ghoul soldier" desc = "Have you ever seen a living ghoul before?
Ghouls are necrotic post-humans - decrepit, rotting, zombie-like mutants." icon_state = "scorched_r" icon_living = "scorched_r" icon_dead = "scorched_r_d" icon_gib = "syndicate_gib" - speak_chance = 1 turns_per_move = 5 - environment_smash = 0 - response_help_simple = "hugs" - response_disarm_simple = "pushes aside" - response_harm_simple = "ow" - move_to_delay = 3.0 - ranged = TRUE - ranged_cooldown_time = 200 - projectiletype = /obj/item/projectile/bullet/c9mm/simple - projectilesound = 'sound/f13weapons/hunting_rifle.ogg' - faction = list("scorched", "hostile") melee_damage_lower = 15 melee_damage_upper = 20 - aggro_vision_range = 10 attack_verb_simple = "shoots" - attack_sound = "punch" - footstep_type = FOOTSTEP_MOB_BAREFOOT - can_ghost_into = FALSE loot_drop_amount = 5 -//Sunset mob of some sort? + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 5 + minimum_distance = 1 + ranged_cooldown_time = 2 SECONDS + projectiletype = /obj/item/projectile/bullet/c9mm/simple + projectilesound = 'sound/f13weapons/hunting_rifle.ogg' + +// ============================================================ +// WYOMING GHOST SOLDIER +// NOTE: faction includes "supermutant" intentionally - this mob is +// allied with both supermutants and ghouls as a Sunset event enemy. +// ============================================================ + /mob/living/simple_animal/hostile/ghoul/wyomingghost name = "ghost soldier" desc = "A figure clad in armor that stands silent except for the slight wheezing coming from them, a dark orange and black liquid pumps through a clear tube into the gas mask. The armor they wear seems to be sealed to their skin." icon_state = "wyomingghost" icon_living = "wyomingghost" icon_dead = "wyomingghost_dead" - mob_biotypes = MOB_ORGANIC|MOB_HUMANOID robust_searching = 1 turns_per_move = 5 speak_emote = list("wheezes") emote_see = list("stares") - a_intent = INTENT_HARM - maxHealth = 150 - health = 150 + maxHealth = 140 + health = 140 speed = 2 harm_intent_damage = 8 melee_damage_lower = 15 melee_damage_upper = 15 attack_verb_simple = "attacks" - attack_sound = 'sound/hallucinations/growl1.ogg' - atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) - unsuitable_atmos_damage = 20 - gold_core_spawnable = HOSTILE_SPAWN - faction = list("supermutant","ghoul") + faction = list("supermutant", "ghoul") // Allied with both factions - intentional for Sunset event decompose = FALSE - sharpness = SHARP_EDGED //They need to cut their finger nails - footstep_type = FOOTSTEP_MOB_BAREFOOT can_ghost_into = FALSE loot_drop_amount = 5 -//Halloween Event Ghouls +// ============================================================ +// ZOMBIE GHOULS (Halloween event) +// Infect humans on hit - spreads ghoulification +// ============================================================ + /mob/living/simple_animal/hostile/ghoul/zombie name = "ravenous feral ghoul" desc = "A ferocious feral ghoul, hungry for human meat." faction = list("ghoul") stat_attack = CONSCIOUS - can_ghost_into = FALSE - maxHealth = 200 - health = 200 - footstep_type = FOOTSTEP_MOB_BAREFOOT + maxHealth = 170 + health = 170 can_ghost_into = FALSE /mob/living/simple_animal/hostile/ghoul/zombie/AttackingTarget() . = ..() if(. && ishuman(target)) var/mob/living/carbon/human/H = target - try_to_ghoul_zombie_infect(H) + try_to_ghoul_zombie_infect(H) // Defined in disease/infection code /mob/living/simple_animal/hostile/ghoul/zombie/reaver name = "ravenous feral ghoul reaver" @@ -484,44 +519,56 @@ icon_living = "ghoulreaver" icon_dead = "ghoulreaver_dead" speed = 2 - maxHealth = 216 - health = 216 - can_ghost_into = FALSE + maxHealth = 130 + health = 130 harm_intent_damage = 8 melee_damage_lower = 30 melee_damage_upper = 30 - footstep_type = FOOTSTEP_MOB_BAREFOOT can_ghost_into = FALSE + combat_mode = COMBAT_MODE_MIXED + ranged = TRUE + retreat_distance = 3 + minimum_distance = 1 + ranged_message = "throws a rock" + ranged_cooldown_time = 3 SECONDS + projectiletype = /obj/item/projectile/bullet/ghoul_rock + projectilesound = 'sound/weapons/punchmiss.ogg' + /mob/living/simple_animal/hostile/ghoul/zombie/glowing name = "ravenous glowing feral ghoul" desc = "A ferocious feral ghoul, hungry for human meat. This one has absorbed massive amounts of radiation, causing them to glow in the dark and radiate constantly." icon_state = "glowinghoul" icon_living = "glowinghoul" icon_dead = "glowinghoul_dead" - maxHealth = 192 - health = 192 + maxHealth = 120 + health = 120 speed = 2 - can_ghost_into = FALSE harm_intent_damage = 8 melee_damage_lower = 30 melee_damage_upper = 30 light_system = MOVABLE_LIGHT light_range = 2 - footstep_type = FOOTSTEP_MOB_BAREFOOT can_ghost_into = FALSE + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 4 + minimum_distance = 4 + ranged_message = "emits radiation" + ranged_cooldown_time = 2 SECONDS + projectiletype = /obj/item/projectile/radiation_thing + projectilesound = 'sound/weapons/etherealhit.ogg' + /mob/living/simple_animal/hostile/ghoul/zombie/glowing/Initialize(mapload) . = ..() - // we only heal BRUTELOSS because each type directly heals a simplemob's health - // therefore setting it to BRUTELOSS | FIRELOSS | TOXLOSS | OXYLOSS would mean healing 4x as much - // aka 40% of max life every tick, which is basically unkillable - // TODO: refactor this if simple_animals ever get damage types AddComponent(/datum/component/glow_heal, chosen_targets = /mob/living/simple_animal/hostile/ghoul, allow_revival = FALSE, restrict_faction = null, type_healing = BRUTELOSS) /mob/living/simple_animal/hostile/ghoul/zombie/glowing/Aggro() - ..() - summon_backup(10) + . = ..() + if(.) + return + summon_backup(8) /mob/living/simple_animal/hostile/ghoul/zombie/glowing/AttackingTarget() . = ..() @@ -536,16 +583,13 @@ icon_living = "glowinghoul" icon_dead = "glowinghoul_dead" color = "#FFFF00" - maxHealth = 200 - health = 200 - can_ghost_into = FALSE + maxHealth = 170 + health = 170 speed = 2.5 harm_intent_damage = 8 melee_damage_lower = 30 melee_damage_upper = 35 - mob_size = 5 + mob_size = MOB_SIZE_HUGE wound_bonus = 0 bare_wound_bonus = 0 - footstep_type = FOOTSTEP_MOB_BAREFOOT can_ghost_into = FALSE - diff --git a/code/modules/mob/living/simple_animal/hostile/f13/insects.dm b/code/modules/mob/living/simple_animal/hostile/f13/insects.dm index da78c49ba2e..1345360a0e0 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/insects.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/insects.dm @@ -1,124 +1,191 @@ // INSECTS - In this document: Giant ant, Radscorpion, Cazador, Radroach, Bloatfly, cazador venom - /////////////// // GIANT ANT // /////////////// +// BASE GIANT ANT /mob/living/simple_animal/hostile/giantant name = "giant ant" - desc = "A giant ant with twitching, darting antennae. Its outsides are a mixture of crusted, unrotting rock and chitin that bounce off bullets and melee weapons. Hardened insides compact once valueless sand and dirt to gemstones. Many a fool in their search for wealth have become part of the gemstones. Can be butchered down the thorax for minerals and shinies." + desc = "A giant ant with twitching, darting antennae. Its outsides are a mixture of crusted, unrotting rock and chitin that bounce off bullets and melee weapons. Hardened insides compact once valueless sand and dirt to gemstones. Can be butchered down the thorax for minerals and shinies." icon = 'icons/fallout/mobs/animals/insects.dmi' icon_state = "GiantAnt" icon_living = "GiantAnt" icon_dead = "GiantAnt_dead" icon_gib = "GiantAnt_gib" + mob_biotypes = MOB_ORGANIC|MOB_BEAST mob_armor = ARMOR_VALUE_ANTS - speak_chance = 0 + + maxHealth = 110 + health = 110 + speed = 1 move_to_delay = 3 - // m2d 3 = standard, less is fast, more is slower. - - retreat_distance = 0 - //how far they pull back - - minimum_distance = 0 - // how close you can get before they try to pull back - - aggro_vision_range = 4 //due to ants poor eyesight - //tiles within they start attacking, doesn't count the mobs tile - - vision_range = 5 - //tiles within they start making noise, does count the mobs tile - - speak_emote = list("clacks", "chitters", "snips", "snaps") - emote_see = list("waggles its antenna", "clicks its mandibles", "picks up your scent", "goes on the hunt") - attack_verb_simple = list ("rips", "tears", "stings") turns_per_move = 5 - guaranteed_butcher_results = list(/obj/item/stack/sheet/sinew = 1, /obj/item/reagent_containers/food/snacks/meat/slab/ant_meat = 2, /obj/item/stack/sheet/animalhide/chitin = 1) - butcher_results = list(/obj/item/stack/sheet/animalhide/chitin = 1) + + melee_damage_lower = 6 + melee_damage_upper = 20 + harm_intent_damage = 8 + obj_damage = 20 + + aggro_vision_range = 4 // Poor eyesight + vision_range = 5 + + guaranteed_butcher_results = list( + /obj/item/stack/sheet/sinew = 1, + /obj/item/reagent_containers/food/snacks/meat/slab/ant_meat = 2, + /obj/item/stack/sheet/animalhide/chitin = 1, + /obj/item/stack/sheet/bone = 1 + ) butcher_difficulty = 1.5 + + waddle_amount = 2 + waddle_up_time = 1 + waddle_side_time = 1 + response_help_simple = "pets" response_disarm_simple = "gently pushes aside" response_harm_simple = "hits" + + speak_emote = list("skitters", "clacks", "chitters", "snips", "snaps") + emote_see = list("waggles its antenna", "clicks its mandibles", "picks up your scent", "goes on the hunt") + attack_verb_simple = list("rips", "tears", "stings") + attack_sound = 'sound/creatures/radroach_attack.ogg' + emote_taunt = list("chitters") emote_taunt_sound = 'sound/creatures/radroach_chitter.ogg' taunt_chance = 30 - speed = 1 - waddle_amount = 2 - waddle_up_time = 1 - waddle_side_time = 1 - maxHealth = 110 - health = 110 - harm_intent_damage = 8 - obj_damage = 20 - melee_damage_lower = 6 - melee_damage_upper = 20 - attack_verb_simple = "stings" - attack_sound = 'sound/creatures/radroach_attack.ogg' - speak_emote = list("skitters") + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) + faction = list("ant") - gold_core_spawnable = HOSTILE_SPAWN a_intent = INTENT_HARM + gold_core_spawnable = HOSTILE_SPAWN blood_volume = 0 decompose = FALSE tastes = list("dirt" = 1, "sand" = 1, "metal?" = 1) + + can_ghost_into = TRUE + pop_required_to_jump_into = MED_MOB_MIN_PLAYERS + + // Z-movement + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 40 + + // Can open doors + can_open_doors = TRUE + can_open_airlocks = TRUE + + // Pure melee + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 -/mob/living/simple_animal/hostile/giantant/Initialize() +/mob/living/simple_animal/hostile/giantant/Aggro() . = ..() + if(.) + return + summon_backup(8) -/mob/living/simple_animal/hostile/giantant/Aggro() - ..() - summon_backup(10) +/mob/living/simple_animal/hostile/giantant/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() -// FIREANT +// FIRE ANT - applies burning reagent /mob/living/simple_animal/hostile/fireant - name = "fireant" - desc = "A large reddish ant. The furnace it holds inside itself blasts intruders and the dirt it chews with flaming heat. Its insides contain more gemstones than its unremarkable kin, accessible by butchering them straight down the thorax." + name = "fire ant" + desc = "A large reddish ant. The furnace it holds inside itself blasts intruders and the dirt it chews with flaming heat. Its insides contain more gemstones than its unremarkable kin." icon = 'icons/fallout/mobs/animals/insects.dmi' icon_state = "FireAnt" icon_living = "FireAnt" icon_dead = "FireAnt_dead" icon_gib = "FireAnt_gib" + mob_biotypes = MOB_ORGANIC|MOB_BEAST - speak_chance = 0 + mob_armor = ARMOR_VALUE_ANTS + + maxHealth = 90 + health = 90 + speed = 1 turns_per_move = 5 + + melee_damage_lower = 8 + melee_damage_upper = 16 + harm_intent_damage = 8 + obj_damage = 20 + + aggro_vision_range = 4 + vision_range = 5 + + guaranteed_butcher_results = list( + /obj/item/stack/sheet/sinew = 1, + /obj/item/reagent_containers/food/snacks/meat/slab/fireant_meat = 2, + /obj/item/reagent_containers/food/snacks/rawantbrain = 1, + /obj/item/stack/sheet/animalhide/chitin = 2, + /obj/item/stack/sheet/bone = 1 + ) + butcher_difficulty = 1.5 + waddle_amount = 2 waddle_up_time = 1 waddle_side_time = 1 - guaranteed_butcher_results = list(/obj/item/stack/sheet/sinew = 1, /obj/item/reagent_containers/food/snacks/meat/slab/fireant_meat = 2, /obj/item/reagent_containers/food/snacks/rawantbrain = 1, /obj/item/stack/sheet/animalhide/chitin = 2) - butcher_results = list(/obj/item/stack/sheet/animalhide/chitin = 2) - butcher_difficulty = 1.5 + response_help_simple = "pets" response_disarm_simple = "gently pushes aside" response_harm_simple = "hits" + + speak_emote = list("skitters") + attack_verb_simple = "stings" + attack_sound = 'sound/creatures/radroach_attack.ogg' + emote_taunt = list("chitters") emote_taunt_sound = 'sound/creatures/radroach_chitter.ogg' taunt_chance = 30 - speed = 1 - maxHealth = 90 - health = 90 - harm_intent_damage = 8 - obj_damage = 20 - melee_damage_lower = 8 - melee_damage_upper = 16 - attack_verb_simple = "stings" - attack_sound = 'sound/creatures/radroach_attack.ogg' - speak_emote = list("skitters") + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) + faction = list("ant") + a_intent = INTENT_HARM gold_core_spawnable = HOSTILE_SPAWN decompose = FALSE - a_intent = INTENT_HARM blood_volume = 0 + + can_ghost_into = TRUE + pop_required_to_jump_into = MED_MOB_MIN_PLAYERS + + // Z-movement + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 40 + + can_open_doors = TRUE + can_open_airlocks = TRUE + + // Pure melee + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 -/mob/living/simple_animal/hostile/fireant/Initialize() +/mob/living/simple_animal/hostile/fireant/Aggro() . = ..() + if(.) + return + summon_backup(8) -/mob/living/simple_animal/hostile/fireant/Aggro() - ..() - summon_backup(10) +/mob/living/simple_animal/hostile/fireant/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() /mob/living/simple_animal/hostile/fireant/AttackingTarget() . = ..() @@ -126,60 +193,102 @@ var/mob/living/carbon/human/H = target H.reagents.add_reagent(/datum/reagent/hellwater, 1) -// ANT QUEEN +// ANT QUEEN - boss variant, spawns ants /mob/living/simple_animal/hostile/giantantqueen name = "giant ant queen" - desc = "The queen of a giant ant colony. Butchering it seems like a good way to a pretty penny." + desc = "The queen of a giant ant colony. Butchering it seems like a good way to make a pretty penny." icon = 'icons/fallout/mobs/animals/antqueen.dmi' icon_state = "antqueen" icon_living = "antqueen" icon_dead = "antqueen_dead" icon_gib = "GiantAnt_gib" + mob_biotypes = MOB_ORGANIC|MOB_BEAST - speak_chance = 0 + mob_armor = ARMOR_VALUE_ANTS_QUEEN + + maxHealth = 350 // Reduced from 560 + health = 350 + speed = 5 // Slow turns_per_move = 5 - guaranteed_butcher_results = list(/obj/item/stack/sheet/sinew = 3, /obj/item/reagent_containers/food/snacks/meat/slab/ant_meat = 6, /obj/item/stack/sheet/animalhide/chitin = 6, /obj/item/reagent_containers/food/snacks/rawantbrain = 1, /obj/item/stack/sheet/animalhide/chitin = 5) - butcher_results = list(/obj/item/stack/sheet/animalhide/chitin = 6, /obj/item/reagent_containers/food/snacks/meat/slab/ant_meat = 3) + stat_attack = UNCONSCIOUS + + melee_damage_lower = 15 + melee_damage_upper = 25 // Increased from 15 + harm_intent_damage = 8 + obj_damage = 20 + + aggro_vision_range = 5 + vision_range = 6 + + guaranteed_butcher_results = list( + /obj/item/stack/sheet/sinew = 3, + /obj/item/reagent_containers/food/snacks/meat/slab/ant_meat = 6, + /obj/item/stack/sheet/animalhide/chitin = 8, + /obj/item/reagent_containers/food/snacks/rawantbrain = 1, + /obj/item/stack/sheet/bone = 3 + ) butcher_difficulty = 1.5 + loot = list(/obj/item/reagent_containers/food/snacks/f13/giantantegg = 10) + loot_drop_amount = 10 + response_help_simple = "pets" response_disarm_simple = "gently pushes aside" response_harm_simple = "hits" + + speak_emote = list("skitters") + attack_verb_simple = "stings" + attack_sound = 'sound/creatures/radroach_attack.ogg' + emote_taunt = list("chitters") emote_taunt_sound = 'sound/creatures/radroach_chitter.ogg' taunt_chance = 30 - speed = 5 - maxHealth = 560 - health = 560 - stat_attack = UNCONSCIOUS - ranged = 1 - harm_intent_damage = 8 - obj_damage = 20 - melee_damage_lower = 15 - melee_damage_upper = 15 - attack_verb_simple = "stings" - attack_sound = 'sound/creatures/radroach_attack.ogg' - projectiletype = /obj/item/projectile/bile - projectilesound = 'sound/f13npc/centaur/spit.ogg' - extra_projectiles = 2 - speak_emote = list("skitters") - retreat_distance = 5 - minimum_distance = 7 + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) + faction = list("ant") + a_intent = INTENT_HARM gold_core_spawnable = HOSTILE_SPAWN decompose = FALSE - a_intent = INTENT_HARM + blood_volume = 0 + + can_ghost_into = TRUE + pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS + despawns_when_lonely = FALSE + + // Z-movement + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 60 + + can_open_doors = FALSE + can_open_airlocks = FALSE + + // Mixed combat - bile spit + melee + combat_mode = COMBAT_MODE_MIXED + ranged = TRUE + retreat_distance = 5 + minimum_distance = 1 + + ranged_message = "spits bile" + ranged_cooldown_time = 4 SECONDS + projectiletype = /obj/item/projectile/bile + projectilesound = 'sound/f13npc/centaur/spit.ogg' + extra_projectiles = 2 + + // Spawner vars var/max_mobs = 2 var/mob_types = list(/mob/living/simple_animal/hostile/giantant) var/spawn_time = 30 SECONDS var/spawn_text = "hatches from" - blood_volume = 0 - /mob/living/simple_animal/hostile/giantantqueen/Initialize() . = ..() AddComponent(/datum/component/spawner, mob_types, spawn_time, faction, spawn_text, max_mobs, _range = 7) + resize = 1.2 + update_transform() /mob/living/simple_animal/hostile/giantantqueen/death() RemoveComponentByType(/datum/component/spawner) @@ -190,18 +299,35 @@ . = ..() /mob/living/simple_animal/hostile/giantantqueen/Aggro() - ..() + . = ..() + if(.) + return summon_backup(10) +/mob/living/simple_animal/hostile/giantantqueen/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + +// BILE PROJECTILE /obj/item/projectile/bile - name = "spit" + name = "bile spit" damage = 20 icon_state = "toxin" +/obj/item/projectile/bile/on_hit(atom/target) + . = ..() + if(iscarbon(target)) + var/mob/living/carbon/M = target + M.reagents.add_reagent(/datum/reagent/toxin, 2) + ///////////////// // RADSCORPION // ///////////////// +// BASE RADSCORPION /mob/living/simple_animal/hostile/radscorpion name = "giant radscorpion" desc = "A mutated arthropod with an armored carapace and a powerful sting." @@ -209,63 +335,81 @@ icon_state = "radscorpion" icon_living = "radscorpion" icon_dead = "radscorpion_dead" - - speed = 1.25 + + mob_biotypes = MOB_ORGANIC|MOB_BEAST + mob_armor = ARMOR_VALUE_RADSCORPION + maxHealth = 120 health = 120 - harm_intent_damage = 8 - obj_damage = 20 + speed = 1.25 + move_to_delay = 3 + turns_per_move = 5 + melee_damage_lower = 15 melee_damage_upper = 28 + harm_intent_damage = 8 + obj_damage = 20 + + aggro_vision_range = 4 // Poor eyesight + vision_range = 5 + + // Nocturnal hunter, but limited by poor eyesight + has_low_light_vision = TRUE + low_light_bonus = 2 + + butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/radscorpion_meat = 2, + /obj/item/stack/sheet/bone = 1 + ) + waddle_amount = 3 waddle_up_time = 1 waddle_side_time = 1 - move_to_delay = 3 - // m2d 3 = standard, less is fast, more is slower. - - retreat_distance = 0 - //how far they pull back - - minimum_distance = 0 - // how close you can get before they try to pull back - - aggro_vision_range = 4 //due to scorpions poor eyesight - //tiles within they start attacking, doesn't count the mobs tile - - vision_range = 5 - //tiles within they start making noise, does count the mobs tile - - mob_biotypes = MOB_ORGANIC|MOB_BEAST - speak_chance = 0 - turns_per_move = 5 - butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/radscorpion_meat = 2) + response_help_simple = "pets" response_disarm_simple = "gently pushes aside" response_harm_simple = "hits" - taunt_chance = 30 - a_intent = INTENT_HARM + + speak_emote = list("hisses") attack_verb_simple = "stings" attack_sound = 'sound/creatures/radscorpion_attack.ogg' - speak_emote = list("hisses") + + emote_taunt = list("snips") + emote_taunt_sound = list('sound/f13npc/scorpion/taunt1.ogg', 'sound/f13npc/scorpion/taunt2.ogg', 'sound/f13npc/scorpion/taunt3.ogg') + taunt_chance = 30 + aggrosound = list('sound/f13npc/scorpion/aggro.ogg') + idlesound = list('sound/creatures/radscorpion_snip.ogg') + death_sound = 'sound/f13npc/scorpion/death.ogg' + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) + faction = list("radscorpion") + a_intent = INTENT_HARM gold_core_spawnable = HOSTILE_SPAWN - var/scorpion_color = "radscorpion" //holder for icon set - var/list/icon_sets = list("radscorpion", "radscorpion_blue", "radscorpion_black") blood_volume = 0 - emote_taunt = list("snips") - - emote_taunt_sound = list('sound/f13npc/scorpion/taunt1.ogg', 'sound/f13npc/scorpion/taunt2.ogg', 'sound/f13npc/scorpion/taunt3.ogg') - aggrosound = list('sound/f13npc/scorpion/aggro.ogg', ) - idlesound = list('sound/creatures/radscorpion_snip.ogg', ) - death_sound = 'sound/f13npc/scorpion/death.ogg' - - -/mob/living/simple_animal/hostile/radscorpion/AttackingTarget() - . = ..() - if(. && ishuman(target)) - var/mob/living/carbon/human/H = target - H.reagents.add_reagent(/datum/reagent/toxin, 5) + footstep_type = FOOTSTEP_MOB_CLAW + + can_ghost_into = TRUE + pop_required_to_jump_into = MED_MOB_MIN_PLAYERS + + // Z-movement + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 40 + + can_open_doors = FALSE + can_open_airlocks = FALSE + + // Pure melee - venomous sting + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + + // Color randomization + var/scorpion_color = "radscorpion" + var/list/icon_sets = list("radscorpion", "radscorpion_blue", "radscorpion_black") /mob/living/simple_animal/hostile/radscorpion/Initialize() . = ..() @@ -278,40 +422,60 @@ icon_living = "[scorpion_color]" icon_dead = "[scorpion_color]_dead" -// BLACK RADSCORPION - a little tougher and slower +/mob/living/simple_animal/hostile/radscorpion/Aggro() + . = ..() + if(.) + return + summon_backup(8) + +/mob/living/simple_animal/hostile/radscorpion/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + +/mob/living/simple_animal/hostile/radscorpion/AttackingTarget() + . = ..() + if(. && ishuman(target)) + var/mob/living/carbon/human/H = target + H.reagents.add_reagent(/datum/reagent/toxin, 5) + +// BLACK RADSCORPION - tougher, slower /mob/living/simple_animal/hostile/radscorpion/black - name = "giant rad scorpion" - desc = "A giant irradiated scorpion with a black exoskeleton. Its appearance makes you shudder in fear.
This one has giant pincers." + name = "black radscorpion" + desc = "A giant irradiated scorpion with a black exoskeleton. Its appearance makes you shudder in fear. This one has giant pincers." icon_state = "radscorpion_black" icon_living = "radscorpion_black" icon_dead = "radscorpion_black_d" - speed = 1.2 + + mob_armor = ARMOR_VALUE_RADSCORPION_BLACK + maxHealth = 160 health = 160 + speed = 1.2 + melee_damage_lower = 10 melee_damage_upper = 28 - move_to_delay = 4 - footstep_type = FOOTSTEP_MOB_CLAW - -// BLUE RADSCORPION - a little weaker and faster +// BLUE RADSCORPION - weaker, faster /mob/living/simple_animal/hostile/radscorpion/blue - name = "giant rad scorpion" - desc = "A giant irradiated scorpion with a bluish exoskeleton. Slighly smaller and faster than its reddish cousin." + name = "blue radscorpion" + desc = "A giant irradiated scorpion with a bluish exoskeleton. Slightly smaller and faster than its reddish cousin." icon_state = "radscorpion_blue" icon_living = "radscorpion_blue" icon_dead = "radscorpion_blue_d" icon_gib = "radscorpion_blue_gib" - speed = 1.35 + maxHealth = 110 health = 110 - move_to_delay = 4 - footstep_type = FOOTSTEP_MOB_CLAW + speed = 1.35 ///////////// // CAZADOR // ///////////// +// BASE CAZADOR - flying venomous terror /mob/living/simple_animal/hostile/cazador name = "cazador" desc = "A mutated insect known for its fast speed, deadly sting, and being huge bastards." @@ -319,57 +483,92 @@ icon_state = "cazador" icon_living = "cazador" icon_dead = "cazador_dead1" + mob_biotypes = MOB_ORGANIC|MOB_BEAST - speak_chance = 0 + mob_armor = ARMOR_VALUE_CAZADOR + + maxHealth = 24 + health = 24 + speed = 1 + move_to_delay = 2.5 turns_per_move = 5 - - move_to_delay = 2.0 - // m2d 3 = standard, less is fast, more is slower. - - retreat_distance = 3 - //how far they pull back - - minimum_distance = 1 - // how close you can get before they try to pull back - - aggro_vision_range = 7 //due to scorpions poor eyesight - //tiles within they start attacking, doesn't count the mobs tile - - vision_range = 8 - //tiles within they start making noise, does count the mobs tile + + melee_damage_lower = 5 + melee_damage_upper = 10 + harm_intent_damage = 8 + obj_damage = 20 rapid_melee = 2 - - guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/cazador_meat = 2, /obj/item/stack/sheet/sinew = 2, /obj/item/stack/sheet/animalhide/chitin = 2) - butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/cazador_meat = 1, /obj/item/stack/sheet/animalhide/chitin = 1) + + aggro_vision_range = 7 + vision_range = 8 + + // Hunting insect with compound eyes adapted for low light + has_low_light_vision = TRUE + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/cazador_meat = 2, + /obj/item/stack/sheet/sinew = 2, + /obj/item/stack/sheet/animalhide/chitin = 2 + ) butcher_difficulty = 1.5 + response_help_simple = "pets" response_disarm_simple = "gently pushes aside" response_harm_simple = "hits" + + speak_emote = list("buzzes") + attack_verb_simple = "stings" + attack_sound = 'sound/creatures/cazador_attack.ogg' + emote_taunt = list("buzzes") emote_taunt_sound = list('sound/f13npc/cazador/cazador_alert.ogg') + taunt_chance = 30 aggrosound = list('sound/f13npc/cazador/cazador_charge1.ogg', 'sound/f13npc/cazador/cazador_charge2.ogg', 'sound/f13npc/cazador/cazador_charge3.ogg') idlesound = list('sound/creatures/cazador_buzz.ogg') - stat_attack = CONSCIOUS - robust_searching = TRUE - taunt_chance = 30 - speed = 1 - maxHealth = 24 - health = 24 - harm_intent_damage = 8 - obj_damage = 20 - melee_damage_lower = 5 - melee_damage_upper = 10 - attack_verb_simple = "stings" - attack_sound = 'sound/creatures/cazador_attack.ogg' - speak_emote = list("buzzes") + death_sound = 'sound/f13npc/cazador/cazador_death.ogg' + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) + faction = list("cazador") - movement_type = FLYING a_intent = INTENT_HARM - pass_flags = PASSTABLE | PASSMOB + stat_attack = CONSCIOUS + robust_searching = TRUE gold_core_spawnable = HOSTILE_SPAWN - death_sound = 'sound/f13npc/cazador/cazador_death.ogg' blood_volume = 0 + + movement_type = FLYING + pass_flags = PASSTABLE | PASSMOB + + can_ghost_into = TRUE + pop_required_to_jump_into = MED_MOB_MIN_PLAYERS + + // Z-movement - can fly + can_z_move = TRUE + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 20 // Fast flier + + can_open_doors = FALSE + can_open_airlocks = FALSE + + // Pure melee - flying ambush + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + +/mob/living/simple_animal/hostile/cazador/Aggro() + . = ..() + if(.) + return + summon_backup(8) + +/mob/living/simple_animal/hostile/cazador/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() /mob/living/simple_animal/hostile/cazador/AttackingTarget() . = ..() @@ -385,33 +584,40 @@ if(!Proj) return if(prob(50)) - return ..() - else visible_message(span_danger("[src] dodges [Proj]!")) - return 0 - + return BULLET_ACT_FORCE_PIERCE + return ..() +// YOUNG CAZADOR - smaller, weaker /mob/living/simple_animal/hostile/cazador/young name = "young cazador" desc = "A mutated insect known for its fast speed, deadly sting, and being huge bastards. This one's little." + maxHealth = 20 health = 20 speed = 1 + melee_damage_lower = 5 melee_damage_upper = 10 - guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/cazador_meat = 1, /obj/item/stack/sheet/animalhide/chitin = 1, /obj/item/stack/sheet/sinew = 1) - butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/cazador_meat = 1, /obj/item/stack/sheet/animalhide/chitin = 1) - butcher_difficulty = 1.5 + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/cazador_meat = 1, + /obj/item/stack/sheet/animalhide/chitin = 1, + /obj/item/stack/sheet/sinew = 1 + ) + + pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS /mob/living/simple_animal/hostile/cazador/young/Initialize() . = ..() resize = 0.8 update_transform() +// CAZADOR VENOM /datum/reagent/toxin/cazador_venom name = "Cazador venom" description = "A potent toxin resulting from cazador stings that quickly kills if too much remains in the body." - color = "#801E28" // rgb: 128, 30, 40 + color = "#801E28" toxpwr = 1 taste_description = "pain" taste_mult = 1.3 @@ -438,60 +644,101 @@ icon_living = "bloatfly" icon_dead = "bloatfly_dead" icon_gib = null - ranged = TRUE - - speed = 1 + + mob_biotypes = MOB_ORGANIC|MOB_BEAST + maxHealth = 20 health = 20 - harm_intent_damage = 8 - obj_damage = 15 + speed = 1 + turns_per_move = 5 + melee_damage_lower = 4 melee_damage_upper = 7 + harm_intent_damage = 8 + obj_damage = 15 + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/bloatfly_meat = 2, + /obj/item/stack/sheet/sinew = 1, + /obj/item/stack/sheet/animalhide/chitin = 1 + ) + butcher_difficulty = 1.5 + waddle_amount = 4 waddle_up_time = 3 waddle_side_time = 2 - can_ghost_into = TRUE - - mob_biotypes = MOB_ORGANIC|MOB_BEAST - speak_chance = 0 - turns_per_move = 5 - guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/bloatfly_meat = 2, /obj/item/stack/sheet/sinew = 1) - butcher_results = list(/obj/item/stack/sheet/animalhide/chitin = 1) - butcher_difficulty = 1.5 + response_help_simple = "pets" response_disarm_simple = "gently pushes aside" response_harm_simple = "bites" - emote_taunt = list("growls") - taunt_chance = 30 + + speak_emote = list("chitters") attack_verb_simple = "bites" attack_sound = 'sound/creatures/bloatfly_attack.ogg' - speak_emote = list("chitters") + + emote_taunt = list("buzzes") + taunt_chance = 30 + idlesound = list('sound/f13npc/bloatfly/fly.ogg') + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) + faction = list("hostile", "gecko", "critter-friend") + a_intent = INTENT_HARM gold_core_spawnable = HOSTILE_SPAWN + blood_volume = 0 + + movement_type = FLYING pass_flags = PASSTABLE | PASSMOB density = FALSE - a_intent = INTENT_HARM - idlesound = list('sound/f13npc/bloatfly/fly.ogg') - blood_volume = 0 + + can_ghost_into = TRUE + desc_short = "A gigantic fly that's more disgusting than actually threatening. Tends to dodge bullets." + pop_required_to_jump_into = MED_MOB_MIN_PLAYERS + + // Z-movement - can fly + can_z_move = TRUE + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 20 + + can_open_doors = TRUE + can_open_airlocks = TRUE + + // Pure ranged - flies and shoots maggots + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 4 + minimum_distance = 1 + + ranged_cooldown_time = 3 SECONDS + auto_fire_delay = GUN_BURSTFIRE_DELAY_NORMAL casingtype = /obj/item/ammo_casing/shotgun/bloatfly projectiletype = null projectilesound = 'sound/f13npc/bloatfly/shoot2.ogg' - //sound_after_shooting = 'sound/f13npc/bloatfly/afterfire1.ogg' - //sound_after_shooting_delay = 1 SECONDS extra_projectiles = 1 - auto_fire_delay = GUN_BURSTFIRE_DELAY_NORMAL - ranged_cooldown_time = 3 SECONDS + variation_list = list( MOB_COLOR_VARIATION(200, 200, 200, 255, 255, 255), - MOB_CASING_LIST(\ - MOB_CASING_ENTRY(/obj/item/ammo_casing/shotgun/bloatfly, 4),\ - MOB_CASING_ENTRY(/obj/item/ammo_casing/shotgun/bloatfly/two, 3),\ - MOB_CASING_ENTRY(/obj/item/ammo_casing/shotgun/bloatfly/three, 3)\ + "varied_projectile" = list( + /obj/item/ammo_casing/shotgun/bloatfly = 4, + /obj/item/ammo_casing/shotgun/bloatfly/two = 3, + /obj/item/ammo_casing/shotgun/bloatfly/three = 3 ) ) - desc_short = "A gigantic fly that's more disgusting than actually threatening. Tends to dodge bullets." - pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS + +/mob/living/simple_animal/hostile/bloatfly/Aggro() + . = ..() + if(.) + return + summon_backup(6) + +/mob/living/simple_animal/hostile/bloatfly/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() /mob/living/simple_animal/hostile/bloatfly/bullet_act(obj/item/projectile/Proj) if(!Proj) @@ -499,8 +746,7 @@ if(prob(50)) visible_message(span_danger("[src] dodges [Proj]!")) return BULLET_ACT_FORCE_PIERCE - else - . = ..() + return ..() /mob/living/simple_animal/hostile/bloatfly/become_the_mob(mob/user) call_backup = /obj/effect/proc_holder/mob_common/summon_backup/small_critter @@ -519,59 +765,96 @@ icon_living = "radroach" icon_dead = "radroach_dead" icon_gib = "radroach_gib" - can_ghost_into = TRUE - waddle_amount = 1 - waddle_up_time = 1 - waddle_side_time = 1 - - speed = 1 + + mob_biotypes = MOB_ORGANIC|MOB_BEAST + maxHealth = 20 health = 20 - harm_intent_damage = 8 - obj_damage = 20 + speed = 1 + turns_per_move = 5 + melee_damage_lower = 4 melee_damage_upper = 6 - - mob_biotypes = MOB_ORGANIC|MOB_BEAST - speak_chance = 0 - turns_per_move = 5 - guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/radroach_meat = 2, /obj/item/stack/sheet/sinew = 1) - butcher_results = list(/obj/item/stack/sheet/animalhide/chitin = 1) + harm_intent_damage = 8 + obj_damage = 20 + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/radroach_meat = 2, + /obj/item/stack/sheet/sinew = 1, + /obj/item/stack/sheet/animalhide/chitin = 1 + ) butcher_difficulty = 1.5 + + waddle_amount = 1 + waddle_up_time = 1 + waddle_side_time = 1 + response_help_simple = "pets" response_disarm_simple = "gently pushes aside" response_harm_simple = "hits" + + speak_emote = list("skitters") attack_verb_simple = "nips" attack_sound = 'sound/creatures/radroach_attack.ogg' - speak_emote = list("skitters") + + aggrosound = list('sound/creatures/radroach_chitter.ogg') + idlesound = list('sound/f13npc/roach/idle1.ogg', 'sound/f13npc/roach/idle2.ogg', 'sound/f13npc/roach/idle3.ogg') + death_sound = 'sound/f13npc/roach/roach_death.ogg' + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) + faction = list("gecko", "critter-friend") a_intent = INTENT_HARM + gold_core_spawnable = HOSTILE_SPAWN + pass_flags = PASSTABLE | PASSMOB density = FALSE - gold_core_spawnable = HOSTILE_SPAWN randpixel = 12 + + can_ghost_into = TRUE + desc_short = "One of countless bugs that move in gross hordes." + pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS + + // Z-movement + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 30 + + can_open_doors = TRUE + can_open_airlocks = TRUE + + // Pure melee + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + variation_list = list( MOB_COLOR_VARIATION(50, 50, 50, 255, 255, 255), MOB_SPEED_LIST(2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8), MOB_SPEED_CHANGE_PER_TURN_CHANCE(100), - MOB_HEALTH_LIST(5, 10, 1), - MOB_RETREAT_DISTANCE_LIST(0, 2, 3), - MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(100), - MOB_MINIMUM_DISTANCE_LIST(0, 1, 1), - MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(5), + MOB_HEALTH_LIST(5, 10, 15, 20), ) - aggrosound = list('sound/creatures/radroach_chitter.ogg',) - idlesound = list('sound/f13npc/roach/idle1.ogg', 'sound/f13npc/roach/idle2.ogg', 'sound/f13npc/roach/idle3.ogg',) - death_sound = 'sound/f13npc/roach/roach_death.ogg' - desc_short = "One of countless bugs that move in gross hordes." - pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS +/mob/living/simple_animal/hostile/radroach/Aggro() + . = ..() + if(.) + return + summon_backup(6) + +/mob/living/simple_animal/hostile/radroach/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() /mob/living/simple_animal/hostile/radroach/become_the_mob(mob/user) call_backup = /obj/effect/proc_holder/mob_common/summon_backup/small_critter send_mobs = /obj/effect/proc_holder/mob_common/direct_mobs/small_critter . = ..() +// JUNGLE RADROACH - friendly variant /mob/living/simple_animal/hostile/radroach/jungle faction = list("gecko", "critter-friend", "jungle") diff --git a/code/modules/mob/living/simple_animal/hostile/f13/mirelurks.dm b/code/modules/mob/living/simple_animal/hostile/f13/mirelurks.dm index 5add294a382..56af2babfa9 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/mirelurks.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/mirelurks.dm @@ -1,11 +1,10 @@ // In this document: Mirelurks - /////////////// // MIRELURKS // /////////////// -// MIRELURK ADULT +// BASE MIRELURK - shared properties /mob/living/simple_animal/hostile/mirelurk name = "mirelurk" desc = "A giant mutated crustacean, with a hardened exo-skeleton." @@ -13,80 +12,132 @@ icon_state = "mirelurk" icon_living = "mirelurk" icon_dead = "mirelurk_d" + + mob_biotypes = MOB_ORGANIC|MOB_BEAST + mob_armor = ARMOR_VALUE_MIRELURK // Hard shell - resistant to bullets + + maxHealth = 120 + health = 120 speed = 1 - can_ghost_into = TRUE move_to_delay = 3 - // m2d 3 = standard, less is fast, more is slower. - - retreat_distance = 0 - //how far they pull back + turns_per_move = 5 + + melee_damage_lower = 5 + melee_damage_upper = 18 + harm_intent_damage = 8 + rapid_melee = 1 - minimum_distance = 0 - // how close you can get before they try to pull back - aggro_vision_range = 7 - //tiles within they start attacking, doesn't count the mobs tile - vision_range = 8 - //tiles within they start making noise, does count the mobs tile - - guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 2, /obj/item/stack/sheet/sinew = 1) + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 2, + /obj/item/stack/sheet/sinew = 1, + /obj/item/stack/sheet/bone = 1 + ) + butcher_results = list(/obj/item/stack/sheet/bone = 1) + butcher_difficulty = 2 + speak_emote = list("foams", "clacks", "chitters", "snips", "snaps") - emote_see = list("clack its claws", "foam at the mouth", "woobs", "extends its eyestalks") - attack_verb_simple = list ("pinches", "rends", "snips", "snaps", "snibbity-snaps", "clonks", "disects") - maxHealth = 120 - health = 120 - melee_damage_lower = 5 - melee_damage_upper = 18 + emote_see = list("clacks its claws", "foams at the mouth", "woobs", "extends its eyestalks") + attack_verb_simple = list("pinches", "rends", "snips", "snaps", "snibbity-snaps", "clonks", "dissects") + waddle_amount = 2 waddle_up_time = 1 waddle_side_time = 1 + + faction = list("mirelurk") gold_core_spawnable = HOSTILE_SPAWN blood_volume = 0 footstep_type = FOOTSTEP_MOB_CLAW + + can_ghost_into = TRUE pop_required_to_jump_into = MED_MOB_MIN_PLAYERS + + // Z-movement - can climb stairs + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 50 + + // Can't open doors - but can smash them + can_open_doors = FALSE + can_open_airlocks = FALSE + environment_smash = ENVIRONMENT_SMASH_STRUCTURES + + // Pure melee ambusher - hides and rushes + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + + // Sound effects - using existing crustacean/creature sounds + emote_taunt_sound = list('sound/creatures/radscorpion_snip.ogg') + emote_taunt = list("clacks menacingly", "snaps its claws") + taunt_chance = 30 + aggrosound = list('sound/creatures/radscorpion_attack.ogg') + idlesound = list('sound/creatures/radroach_chitter.ogg') + death_sound = 'sound/creatures/rattle.ogg' + attack_sound = 'sound/creatures/radscorpion_snip.ogg' + variation_list = list( MOB_COLOR_VARIATION(100, 100, 100, 255, 255, 255), MOB_SPEED_LIST(3.3, 3.4, 3.5), - MOB_SPEED_CHANGE_PER_TURN_CHANCE(100), + MOB_SPEED_CHANGE_PER_TURN_CHANCE(10), MOB_HEALTH_LIST(110, 115, 120, 130), - MOB_RETREAT_DISTANCE_LIST(0, 1, 2), - MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(70), - MOB_MINIMUM_DISTANCE_LIST(0, 0, 1), - MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(70), ) +/mob/living/simple_animal/hostile/mirelurk/Aggro() + . = ..() + if(.) + return + summon_backup(8) + +// Override CanAttack to ignore unconscious/dead targets +/mob/living/simple_animal/hostile/mirelurk/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() -// MIRELURK HUNTER MALES +// MIRELURK HUNTER - bigger, meaner, faster /mob/living/simple_animal/hostile/mirelurk/hunter name = "mirelurk hunter" - desc = "A giant mutated crustacean, with a hardened exoskeleton. Its appearance makes you shudder in fear. This one has giant, razor sharp claw pincers." + desc = "A giant mutated crustacean, with a hardened exoskeleton. Its appearance makes you shudder in fear. This one has giant, razor-sharp claw pincers." icon_state = "mirelurkhunter" icon_living = "mirelurkhunter" - speed = 1 icon_dead = "mirelurkhunter_d" icon_gib = "gib" - guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 4, /obj/item/stack/sheet/sinew = 2) + + mob_armor = ARMOR_VALUE_MIRELURK_HUNTER // Even tougher shell + maxHealth = 160 health = 160 - stat_attack = UNCONSCIOUS + speed = 1 + melee_damage_lower = 15 melee_damage_upper = 28 - gold_core_spawnable = HOSTILE_SPAWN - footstep_type = FOOTSTEP_MOB_CLAW + rapid_melee = 2 // Attacks faster + + stat_attack = UNCONSCIOUS // Will attack downed targets + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 4, + /obj/item/stack/sheet/sinew = 2, + /obj/item/stack/sheet/bone = 2 + ) + pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS + variation_list = list( MOB_COLOR_VARIATION(100, 100, 100, 255, 255, 255), - MOB_SPEED_LIST(3, 3.1, 3.2), - MOB_SPEED_CHANGE_PER_TURN_CHANCE(100), + MOB_SPEED_LIST(3.0, 3.1, 3.2), + MOB_SPEED_CHANGE_PER_TURN_CHANCE(10), MOB_HEALTH_LIST(140, 150, 160, 170), - MOB_RETREAT_DISTANCE_LIST(0, 1, 2), - MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(70), - MOB_MINIMUM_DISTANCE_LIST(0, 0, 1), - MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(70), ) -// MIRELURK BABY +// MIRELURK BABY - small, weak, calls for help /mob/living/simple_animal/hostile/mirelurk/baby name = "mirelurk baby" desc = "A neophyte mirelurk baby, mostly harmless. Adults respond to their chittering if distressed." @@ -94,42 +145,311 @@ icon_living = "mirelurkbaby" icon_dead = "mirelurkbaby_d" icon_gib = "gib" - butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 1) - speed = 1 + + mob_armor = ARMOR_VALUE_MIRELURK_BABY // Softer shell + maxHealth = 40 health = 40 + speed = 1 + melee_damage_lower = 5 melee_damage_upper = 10 + rapid_melee = 1 + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 1, + /obj/item/stack/sheet/sinew = 1 + ) + butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 1) + waddle_amount = 3 - waddle_up_time = 1 - waddle_side_time = 1 - gold_core_spawnable = HOSTILE_SPAWN - footstep_type = FOOTSTEP_MOB_CLAW - pop_required_to_jump_into = 0 + pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS + + // Babies run away and call for help + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + variation_list = list( MOB_COLOR_VARIATION(100, 100, 100, 255, 255, 255), MOB_SPEED_LIST(2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8), MOB_SPEED_CHANGE_PER_TURN_CHANCE(100), MOB_HEALTH_LIST(35, 39, 40, 41), - MOB_RETREAT_DISTANCE_LIST(5, 6, 7), - MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(100), - MOB_MINIMUM_DISTANCE_LIST(3, 4, 5), - MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(100), ) +/mob/living/simple_animal/hostile/mirelurk/baby/Aggro() + ..() + summon_backup(12) // Babies call louder for help + if(!ckey) + visible_message(span_warning("[src] chitters frantically for help!")) + +// MIRELURK QUEEN - rare, powerful, spawns babies +/mob/living/simple_animal/hostile/mirelurk/queen + name = "mirelurk queen" + desc = "A massive mirelurk matriarch. Her hardened carapace glistens with toxic secretions, and she guards her brood fiercely." + icon_state = "mirelurk" + icon_living = "mirelurk" + icon_dead = "mirelurk_d" + color = "#88ccaa" // Greenish-teal coloration for breeding female + + mob_armor = ARMOR_VALUE_MIRELURK_QUEEN // Nearly impenetrable + + maxHealth = 400 + health = 400 + speed = 2.5 // Slower but tankier + stat_attack = UNCONSCIOUS + + melee_damage_lower = 20 + melee_damage_upper = 35 + rapid_melee = 1 + obj_damage = 150 + + environment_smash = ENVIRONMENT_SMASH_WALLS // Can break through walls + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 8, + /obj/item/stack/sheet/sinew = 4, + /obj/item/stack/sheet/bone = 4 + ) + + loot = list(/obj/item/stack/f13Cash/random/high) + loot_drop_amount = 5 + + // Mixed combat - acid spit at range, claw up close + combat_mode = COMBAT_MODE_MIXED + ranged = TRUE + retreat_distance = 3 + minimum_distance = 1 + + ranged_message = "spits acid" + ranged_cooldown_time = 4 SECONDS + projectiletype = /obj/item/projectile/mirelurk_acid + projectilesound = 'sound/effects/splat.ogg' + + pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS + despawns_when_lonely = FALSE + + // Spawner component + var/max_babies = 3 + var/baby_types = list(/mob/living/simple_animal/hostile/mirelurk/baby) + var/spawn_time = 20 SECONDS + var/spawn_text = "emerges from" + +/mob/living/simple_animal/hostile/mirelurk/queen/Initialize() + . = ..() + AddComponent(/datum/component/spawner, baby_types, spawn_time, faction, spawn_text, max_babies, _range = 5) + resize = 1.3 + update_transform() -/mob/living/simple_animal/hostile/mirelurk/baby/Initialize() +/mob/living/simple_animal/hostile/mirelurk/queen/death() + RemoveComponentByType(/datum/component/spawner) . = ..() -/mob/living/simple_animal/hostile/mirelurk/baby/Aggro() - ..() - summon_backup(10) +/mob/living/simple_animal/hostile/mirelurk/queen/Destroy() + RemoveComponentByType(/datum/component/spawner) + . = ..() -// OBSOLETE MARKED FOR DEATH, YOU HAVE 3 DAYS +/mob/living/simple_animal/hostile/mirelurk/queen/Aggro() + . = ..() + if(.) + return + summon_backup(15) + if(!ckey) + visible_message(span_danger("[src] screeches, calling the brood!")) + +/mob/living/simple_animal/hostile/mirelurk/queen/AttackingTarget() + . = ..() + if(. && ishuman(target)) + var/mob/living/carbon/human/H = target + H.reagents.add_reagent(/datum/reagent/toxin, 3) + +// SOFTSHELL MIRELURK - weaker variant, less armor +/mob/living/simple_animal/hostile/mirelurk/softshell + name = "softshell mirelurk" + desc = "A mirelurk that hasn't fully hardened its shell yet. Still dangerous, but more vulnerable." + icon_state = "mirelurk" + icon_living = "mirelurk" + icon_dead = "mirelurk_d" + color = "#ffaa99" // Reddish-pink for vulnerable molting state + + mob_armor = ARMOR_VALUE_MIRELURK_SOFT // Much weaker armor + + maxHealth = 80 + health = 80 + speed = 0.8 // Faster to compensate + + melee_damage_lower = 8 + melee_damage_upper = 15 + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 3, + /obj/item/stack/sheet/sinew = 2 + ) + + variation_list = list( + MOB_COLOR_VARIATION(150, 150, 150, 200, 200, 200), + MOB_SPEED_LIST(2.8, 2.9, 3.0), + MOB_SPEED_CHANGE_PER_TURN_CHANCE(10), + MOB_HEALTH_LIST(70, 75, 80, 85), + ) + +// KING MIRELURK - rare boss variant +/mob/living/simple_animal/hostile/mirelurk/king + name = "mirelurk king" + desc = "A towering humanoid mirelurk with a distinctive crown-like crest. Highly intelligent and extremely dangerous." + icon_state = "mirelurkhunter" + icon_living = "mirelurkhunter" + icon_dead = "mirelurkhunter_d" + color = "#6633aa" // Dark purple for intimidating alpha male + + mob_armor = ARMOR_VALUE_MIRELURK_KING + + maxHealth = 350 + health = 350 + speed = 1.5 + stat_attack = UNCONSCIOUS + + melee_damage_lower = 25 + melee_damage_upper = 40 + rapid_melee = 2 + obj_damage = 200 + + environment_smash = ENVIRONMENT_SMASH_WALLS + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/mirelurk = 6, + /obj/item/stack/sheet/sinew = 3, + /obj/item/stack/sheet/bone = 3 + ) + + loot = list(/obj/item/stack/f13Cash/random/high) + loot_drop_amount = 8 + + // Mixed combat - sonic scream at range + combat_mode = COMBAT_MODE_MIXED + ranged = TRUE + retreat_distance = 4 + minimum_distance = 1 + + ranged_message = "unleashes a sonic scream" + ranged_cooldown_time = 5 SECONDS + projectiletype = /obj/item/projectile/mirelurk_sonic + projectilesound = 'sound/effects/supermatter.ogg' // Sonic scream sound + extra_projectiles = 2 + + pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS + despawns_when_lonely = FALSE + + // Low health rage mode + low_health_threshold = 0.35 + var/color_rage = "#FF4444" + +/mob/living/simple_animal/hostile/mirelurk/king/Initialize() + . = ..() + resize = 1.4 + update_transform() + +/mob/living/simple_animal/hostile/mirelurk/king/make_low_health() + visible_message(span_danger("[src] roars with fury, its crest glowing red!")) + playsound(src, pick(aggrosound), 100, 1, SOUND_DISTANCE(15)) + color = color_rage + speed *= 0.75 // Faster + melee_damage_lower = round(melee_damage_lower * 1.3) + melee_damage_upper = round(melee_damage_upper * 1.3) + rapid_melee = 3 + is_low_health = TRUE + +/mob/living/simple_animal/hostile/mirelurk/king/make_high_health() + visible_message(span_notice("[src]'s fury subsides.")) + color = initial(color) + speed = initial(speed) + melee_damage_lower = initial(melee_damage_lower) + melee_damage_upper = initial(melee_damage_upper) + rapid_melee = initial(rapid_melee) + is_low_health = FALSE + +/mob/living/simple_animal/hostile/mirelurk/king/Aggro() + . = ..() + if(.) + return + summon_backup(15) + if(!ckey) + visible_message(span_danger("[src] bellows a deafening challenge!")) + +/mob/living/simple_animal/hostile/mirelurk/king/AttackingTarget() + . = ..() + if(. && ishuman(target)) + var/mob/living/carbon/human/H = target + H.reagents.add_reagent(/datum/reagent/toxin, 5) + H.Knockdown(20) // Powerful hits knock down + +// PROJECTILES +/obj/item/projectile/mirelurk_acid + name = "acid spit" + damage = 20 + icon_state = "toxin" + +/obj/item/projectile/mirelurk_acid/on_hit(atom/target) + . = ..() + if(iscarbon(target)) + var/mob/living/carbon/M = target + M.reagents.add_reagent(/datum/reagent/toxin, 3) + M.emote("scream") + +/obj/item/projectile/mirelurk_sonic + name = "sonic blast" + damage = 15 + stamina = 30 + icon_state = "sound" + +/obj/item/projectile/mirelurk_sonic/on_hit(atom/target) + . = ..() + if(iscarbon(target)) + var/mob/living/carbon/M = target + M.Stun(20) + M.soundbang_act(1, 4, 10, 5) + +// LEGACY EGG STRUCTURE (marked for removal) /obj/structure/mirelurkegg name = "mirelurk eggs" - desc = "A fresh clutch of mirelurk eggs." + desc = "A fresh clutch of mirelurk eggs. They pulse with life." icon = 'icons/mob/wastemobsdrops.dmi' icon_state = "mirelurkeggs" density = 1 anchored = 0 + max_integrity = 50 + +/obj/structure/mirelurkegg/attack_hand(mob/user) + . = ..() + if(.) + return + to_chat(user, span_notice("You crack open [src].")) + new /obj/item/reagent_containers/food/snacks/mirelurk_egg(get_turf(src)) + qdel(src) + +/obj/structure/mirelurkegg/attackby(obj/item/I, mob/user, params) + if(I.force >= 5) + visible_message(span_warning("[src] cracks open!")) + new /obj/item/reagent_containers/food/snacks/mirelurk_egg(get_turf(src)) + qdel(src) + return + return ..() + +// MIRELURK EGG FOOD ITEM +/obj/item/reagent_containers/food/snacks/mirelurk_egg + name = "mirelurk egg" + desc = "A large, protein-rich egg from a mirelurk." + icon = 'icons/obj/food/egg.dmi' + icon_state = "egg" + list_reagents = list(/datum/reagent/consumable/nutriment = 8, /datum/reagent/consumable/nutriment/vitamin = 2) + filling_color = "#FFCC99" + tastes = list("egg" = 1, "ocean" = 1) + foodtype = MEAT | RAW + cooked_type = /obj/item/reagent_containers/food/snacks/mirelurk_egg/cooked + +/obj/item/reagent_containers/food/snacks/mirelurk_egg/cooked + name = "cooked mirelurk egg" + desc = "A perfectly cooked mirelurk egg. Delicious and nutritious." + icon_state = "egg" + list_reagents = list(/datum/reagent/consumable/nutriment = 10, /datum/reagent/consumable/nutriment/vitamin = 3) + foodtype = MEAT diff --git a/code/modules/mob/living/simple_animal/hostile/f13/raider.dm b/code/modules/mob/living/simple_animal/hostile/f13/raider.dm index 3b450234020..645385fdf53 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/raider.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/raider.dm @@ -1,10 +1,9 @@ -// IN THIS FILE: -All Raider Mobs +// IN THIS FILE: All Raider Mobs -/////////////////// -// BASIC RAIDERS // -/////////////////// +// ============================================================ +// BASE RAIDER +// ============================================================ -// BASIC MELEE RAIDER /mob/living/simple_animal/hostile/raider name = "Raider" desc = "Another murderer churned out by the wastes." @@ -13,7 +12,6 @@ icon_living = "raider_melee" icon_dead = "raider_dead" mob_biotypes = MOB_ORGANIC|MOB_HUMANOID - turns_per_move = 5 mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET maxHealth = 100 health = 100 @@ -26,60 +24,77 @@ check_friendly_fire = TRUE status_flags = CANPUSH del_on_death = FALSE + loot = list(/obj/item/melee/onehanded/knife/survival, /obj/item/stack/f13Cash/random/med) - /// How many things to drop on death? Set to MOB_LOOT_ALL to just drop everything in the list loot_drop_amount = 2 - /// Drop 1 - loot_drop_amount? False always drops loot_drop_amount items loot_amount_random = TRUE - /// slots in a list of trash loot var/random_trash_loot = TRUE + footstep_type = FOOTSTEP_MOB_SHOE rapid_melee = 2 melee_queue_distance = 5 - move_to_delay = 3.0 + move_to_delay = 3 + turns_per_move = 5 waddle_amount = 2 waddle_up_time = 1 waddle_side_time = 1 - retreat_distance = 1 //mob retreats 1 tile when in min distance - minimum_distance = 1 //Mob pushes up to melee, then backs off to avoid player attack? - aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first. - vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer. - variation_list = list( - MOB_NAME_FROM_GLOBAL_LIST(\ - MOB_RANDOM_NAME(MOB_NAME_RANDOM_MALE, 1)\ - )) + + aggro_vision_range = 6 + vision_range = 8 + can_z_move = TRUE - z_move_delay = 40 // 4 seconds - more cautious + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 40 + + can_open_doors = TRUE + can_open_airlocks = TRUE + + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + + variation_list = list( + MOB_NAME_FROM_GLOBAL_LIST(MOB_RANDOM_NAME(MOB_NAME_RANDOM_MALE, 1)) + ) /mob/living/simple_animal/hostile/raider/Initialize() . = ..() if(random_trash_loot) loot = GLOB.trash_ammo + GLOB.trash_chem + GLOB.trash_clothing + GLOB.trash_craft + GLOB.trash_gun + GLOB.trash_misc + GLOB.trash_money + GLOB.trash_mob + GLOB.trash_part + GLOB.trash_tool + GLOB.trash_attachment -/obj/effect/mob_spawn/human/corpse/raider - name = "Raider" - uniform = /obj/item/clothing/under/f13/rag - suit = /obj/item/clothing/suit/armor/medium/raider/iconoclast - shoes = /obj/item/clothing/shoes/f13/explorer - gloves = /obj/item/clothing/gloves/f13/leather - head = /obj/item/clothing/head/helmet/f13/firefighter - /mob/living/simple_animal/hostile/raider/Aggro() . = ..() if(.) return - summon_backup(15) + summon_backup(10) if(!ckey) - say(pick("*insult", "Fuck off!!", "Back off!!" , "Keep moving!!", "Get lost, asshole!!", "Call a doctor, we got a bleeder!!", "Fuck around and find out!!" )) + say(pick("*insult", "Fuck off!!", "Back off!!", "Keep moving!!", "Get lost, asshole!!", "Call a doctor, we got a bleeder!!", "Fuck around and find out!!")) + +// Friendly fire resistance - raiders are used to fighting in groups +/mob/living/simple_animal/hostile/raider/bullet_act(obj/item/projectile/P) + if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/raider)) + var/original_damage = P.damage + P.damage *= 0.2 + . = ..() + P.damage = original_damage + return + return ..() + +// ============================================================ +// THIEF RAIDER +// Steals items from downed players and runs +// ============================================================ -// THIEF RAIDER - nabs stuff and runs /mob/living/simple_animal/hostile/raider/thief desc = "Another murderer churned out by the wastes. This one looks like they have sticky fingers..." /mob/living/simple_animal/hostile/raider/thief/movement_delay() - return -2 + return ..() - 2 /mob/living/simple_animal/hostile/raider/thief/AttackingTarget() + . = ..() if(ishuman(target)) var/mob/living/carbon/human/H = target if(H.stat == SOFT_CRIT) @@ -95,41 +110,40 @@ if(shoe_target) H.dropItemToGround(shoe_target, TRUE) src.transferItemToLoc(shoe_target, src, TRUE) - retreat_distance = 50 - else - . = ..() + retreat_distance = 50 // Run after stealing /mob/living/simple_animal/hostile/raider/thief/death(gibbed) for(var/obj/I in contents) src.dropItemToGround(I) . = ..() -// BASIC RANGED RAIDER +// ============================================================ +// RANGED RAIDER +// ============================================================ + /mob/living/simple_animal/hostile/raider/ranged icon_state = "raider_ranged" icon_living = "raider_ranged" - ranged = TRUE - mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET maxHealth = 85 health = 85 - rapid_melee = 2 - melee_queue_distance = 5 - move_to_delay = 3.0 //faster than average, but not a lot - retreat_distance = 1 //mob retreats 1 tile when in min distance - minimum_distance = 1 //Mob pushes up to melee, then backs off to avoid player attack? - aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first. - vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer. + + loot = list(/obj/effect/spawner/lootdrop/f13/npc_raider, /obj/item/stack/f13Cash/random/med) + loot_drop_amount = 3 + + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 5 + minimum_distance = 1 + ranged_cooldown_time = 2 SECONDS auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL projectiletype = /obj/item/projectile/bullet/c9mm/simple projectilesound = 'sound/f13weapons/ninemil.ogg' - loot = list(/obj/effect/spawner/lootdrop/f13/npc_raider, /obj/item/stack/f13Cash/random/med) - loot_drop_amount = 3 - footstep_type = FOOTSTEP_MOB_SHOE + variation_list = list( - MOB_NAME_FROM_GLOBAL_LIST(\ - MOB_RANDOM_NAME(MOB_NAME_RANDOM_FEMALE, 1)\ - )) + MOB_NAME_FROM_GLOBAL_LIST(MOB_RANDOM_NAME(MOB_NAME_RANDOM_FEMALE, 1)) + ) + projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(PISTOL_LIGHT_VOLUME), @@ -141,49 +155,48 @@ SP_DISTANT_RANGE(PISTOL_LIGHT_RANGE_DISTANT) ) +// ============================================================ +// LEGENDARY RAIDERS +// stat_attack = UNCONSCIOUS lets them finish off downed targets +// NOTE: This only works correctly now that the CanAttack override is removed +// ============================================================ -// LEGENDARY MELEE RAIDER /mob/living/simple_animal/hostile/raider/legendary name = "Legendary Raider" desc = "Another murderer churned out by the wastes - this one seems a bit faster than the average..." color = "#FFFF00" - mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET - maxHealth = 300 - health = 300 + maxHealth = 225 + health = 225 stat_attack = UNCONSCIOUS speed = 1.2 obj_damage = 300 rapid_melee = 1 + loot = list(/obj/item/melee/onehanded/knife/survival, /obj/item/reagent_containers/food/snacks/kebab/human, /obj/item/stack/f13Cash/random/high) loot_drop_amount = MOB_LOOT_ALL loot_amount_random = FALSE random_trash_loot = FALSE - footstep_type = FOOTSTEP_MOB_SHOE -// LEGENDARY RANGED RAIDER /mob/living/simple_animal/hostile/raider/ranged/legendary name = "Legendary Raider" desc = "Another murderer churned out by the wastes, wielding a decent pistol and looking very strong" color = "#FFFF00" - mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET - maxHealth = 240 - health = 240 + maxHealth = 180 + health = 180 stat_attack = UNCONSCIOUS - retreat_distance = 1 - minimum_distance = 2 - rapid_melee = 1 + ranged_cooldown_time = 2 SECONDS - auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL sight_shoot_delay_time = 0 SECONDS projectiletype = /obj/item/projectile/bullet/m44/simple projectilesound = 'sound/f13weapons/44mag.ogg' extra_projectiles = 1 obj_damage = 300 + loot = list(/obj/item/gun/ballistic/revolver/m29, /obj/item/stack/f13Cash/random/high) loot_drop_amount = MOB_LOOT_ALL loot_amount_random = FALSE random_trash_loot = FALSE - footstep_type = FOOTSTEP_MOB_SHOE + projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(PISTOL_HEAVY_VOLUME), @@ -195,7 +208,10 @@ SP_DISTANT_RANGE(PISTOL_HEAVY_RANGE_DISTANT) ) +// ============================================================ // RAIDER BOSS +// ============================================================ + /mob/living/simple_animal/hostile/raider/ranged/boss name = "Raider Boss" icon_state = "raiderboss" @@ -205,26 +221,24 @@ maxHealth = 150 health = 150 stat_attack = UNCONSCIOUS + extra_projectiles = 2 - rapid_melee = 1 waddle_amount = 4 waddle_up_time = 2 waddle_side_time = 1 + ranged_cooldown_time = 2 SECONDS auto_fire_delay = GUN_AUTOFIRE_DELAY_FAST projectiletype = /obj/item/projectile/bullet/c10mm/improvised + loot = list(/obj/item/gun/ballistic/automatic/smg/smg10mm, /obj/item/clothing/head/helmet/f13/combat/mk2/raider, /obj/effect/spawner/lootdrop/f13/armor/random, /obj/item/clothing/under/f13/ravenharness, /obj/item/stack/f13Cash/random/high) loot_drop_amount = MOB_LOOT_ALL loot_amount_random = FALSE random_trash_loot = FALSE - footstep_type = FOOTSTEP_MOB_SHOE + sentience_type = SENTIENCE_BOSS - move_to_delay = 3.0 //faster than average, but not a lot - retreat_distance = 4 //mob retreats 1 tile when in min distance - minimum_distance = 2 //Mob pushes up to melee, then backs off to avoid player attack? - aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first. - vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer. despawns_when_lonely = FALSE + projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(PISTOL_MEDIUM_VOLUME), @@ -237,9 +251,9 @@ ) variation_list = list( - MOB_RETREAT_DISTANCE_LIST(0, 1, 3, 4), + MOB_RETREAT_DISTANCE_LIST(3, 4, 5), MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(50), - MOB_MINIMUM_DISTANCE_LIST(0, 2, 4), + MOB_MINIMUM_DISTANCE_LIST(1, 2, 3), MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(40), ) @@ -247,61 +261,38 @@ . = ..() if(.) return - summon_backup(15) + summon_backup(10) if(!ckey) say("KILL 'EM, FELLAS!") +// ============================================================ +// NAMED BOSSES +// ============================================================ + /mob/living/simple_animal/hostile/raider/ranged/boss/mangomatt name = "Mango Mathew and his Merry Meth Madlads" desc = "Hi, Mango Mathew and his Merry Meth Madlads." icon_state = "mango_matt" icon_living = "mango_matt" icon_dead = "mango_matt_dead" - mob_armor = ARMOR_VALUE_RAIDER_COMBAT_ARMOR_BOSS maxHealth = 165 health = 165 extra_projectiles = 2 ranged_cooldown_time = 1 SECONDS sight_shoot_delay_time = 0 SECONDS auto_fire_delay = GUN_AUTOFIRE_DELAY_FAST - speak_emote = list( - "growls", - "murrs", - "purrs", - "mrowls", - "yowls", - "prowls" - ) - emote_see = list( - "laughs", - "nyas", - "" - ) - attack_verb_simple = list( - "claws", - "maims", - "bites", - "mauls", - "slashes", - "thrashes", - "bashes", - "glomps", - "beats their greasegun against the face of" - ) - variation_list = list() // so he keeps his stupid name + + speak_emote = list("growls", "murrs", "purrs", "mrowls", "yowls", "prowls") + emote_see = list("laughs", "nyas", "") + attack_verb_simple = list("claws", "maims", "bites", "mauls", "slashes", "thrashes", "bashes", "glomps", "beats their greasegun against the face of") + variation_list = list() // Prevents random name override /mob/living/simple_animal/hostile/raider/ranged/boss/mangomatt/Aggro() - ..() - summon_backup(15) - say(pick(\ - "*nya",\ - "*mrowl",\ - "*lynx",\ - "*cougar",\ - "*growl",\ - "*come",\ - "Fuck em' up!"\ - )) + . = ..() + if(.) + return + if(!ckey) + say(pick("*nya", "*mrowl", "*lynx", "*cougar", "*growl", "*come", "Fuck em' up!")) /mob/living/simple_animal/hostile/raider/ranged/boss/blueberrybates name = "Blueberry Bates and his Bottom-Feeder Buys" @@ -309,58 +300,36 @@ icon_state = "blueberry_bates" icon_living = "blueberry_bates" icon_dead = "blueberry_bates_dead" - mob_armor = ARMOR_VALUE_RAIDER_COMBAT_ARMOR_BOSS - move_to_delay = 3.0 //S L O W + maxHealth = 200 + health = 200 + sight_shoot_delay_time = 0 SECONDS ranged_cooldown_time = 1 SECONDS auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL projectiletype = /obj/item/projectile/bullet/incendiary/shotgun projectilesound = 'sound/f13weapons/shotgun.ogg' - maxHealth = 200 //bit beefier since his arena is significantly shittier for him and he's more of an annoyance - health = 200 extra_projectiles = 0 - retreat_distance = 3 - minimum_distance = 3 + + retreat_distance = 4 + loot = list(/obj/item/stack/f13Cash/random/high, /obj/item/ammo_box/shotgun/incendiary, /obj/item/gun/ballistic/shotgun/police) - speak_emote = list( - "mutters", - "counts his caps to himself", - "yells at someone to pick up the pace", - "barks", - "grumbles", - "grouches" - ) - emote_see = list( - "chitters", - "idly gnaws on a hat", - ) - attack_verb_simple = list( - "bayonets", - "smacks", - "bites", - "mauls", - "slashes", - "thrashes", - "bashes", - "glomps", - "robusts on" - ) - variation_list = list() // so he keeps his stupid name + speak_emote = list("mutters", "counts his caps to himself", "yells at someone to pick up the pace", "barks", "grumbles", "grouches") + emote_see = list("chitters", "idly gnaws on a hat") + attack_verb_simple = list("bayonets", "smacks", "bites", "mauls", "slashes", "thrashes", "bashes", "glomps", "robusts on") + variation_list = list() +// FIX: Same double-summon issue as mangomatt. Added proper return check. /mob/living/simple_animal/hostile/raider/ranged/boss/blueberrybates/Aggro() - ..() - summon_backup(15) - say(pick(\ - "TO ME, BOYS!",\ - "KICK THEIR ASS!",\ - "Fuck 'em up!",\ - "*chitter",\ - "*kyaa",\ - "*come",\ - "YOU'RE ABOUT TO GET A DISCOUNT ON A GRAVE, BUDDY!",\ - )) - -// RANGED RAIDER WITH ARMOR + . = ..() + if(.) + return + if(!ckey) + say(pick("TO ME, BOYS!", "KICK THEIR ASS!", "Fuck 'em up!", "*chitter", "*kyaa", "*come", "YOU'RE ABOUT TO GET A DISCOUNT ON A GRAVE, BUDDY!")) + +// ============================================================ +// SPECIALTY RAIDERS +// ============================================================ + /mob/living/simple_animal/hostile/raider/ranged/sulphiteranged icon_state = "metal_raider" icon_living = "metal_raider" @@ -368,14 +337,14 @@ mob_armor = ARMOR_VALUE_RAIDER_METAL_ARMOR maxHealth = 60 health = 60 - rapid_melee = 1 + ranged_cooldown_time = 2 SECONDS - auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL projectiletype = /obj/item/projectile/bullet/c45/simple projectilesound = 'sound/weapons/gunshot.ogg' + loot = list(/obj/item/gun/ballistic/automatic/pistol/m1911/custom, /obj/item/clothing/suit/armor/heavy/metal/reinforced, /obj/item/clothing/head/helmet/f13/metalmask/mk2, /obj/item/stack/f13Cash/random/med) loot_drop_amount = 5 - footstep_type = FOOTSTEP_MOB_SHOE + projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(PISTOL_MEDIUM_VOLUME), @@ -387,7 +356,6 @@ SP_DISTANT_RANGE(PISTOL_MEDIUM_RANGE_DISTANT) ) -// FIREFIGHTER RAIDER /mob/living/simple_animal/hostile/raider/firefighter icon_state = "firefighter_raider" icon_living = "firefighter_raider" @@ -395,12 +363,10 @@ mob_armor = ARMOR_VALUE_RAIDER_ARMOR maxHealth = 100 health = 100 + rapid_melee = 1 loot = list(/obj/item/twohanded/fireaxe, /obj/item/stack/f13Cash/random/med) loot_drop_amount = 3 - footstep_type = FOOTSTEP_MOB_SHOE - rapid_melee = 1 -// BIKER RAIDER /mob/living/simple_animal/hostile/raider/ranged/biker icon_state = "biker_raider" icon_living = "biker_raider" @@ -410,14 +376,14 @@ mob_armor = ARMOR_VALUE_RAIDER_COMBAT_ARMOR_RUSTY maxHealth = 125 health = 125 - rapid_melee = 1 + ranged_cooldown_time = 2 SECONDS - auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL projectiletype = /obj/item/projectile/bullet/a762/sport/simple projectilesound = 'sound/f13weapons/magnum_fire.ogg' + loot = list(/obj/item/gun/ballistic/revolver/thatgun, /obj/item/clothing/suit/armor/medium/combat/rusted, /obj/item/clothing/head/helmet/f13/raidercombathelmet, /obj/item/stack/f13Cash/random/med) loot_drop_amount = 5 - footstep_type = FOOTSTEP_MOB_SHOE + projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(RIFLE_LIGHT_VOLUME), @@ -429,22 +395,11 @@ SP_DISTANT_RANGE(RIFLE_LIGHT_RANGE_DISTANT) ) -/obj/effect/mob_spawn/human/corpse/raider/ranged/biker - uniform = /obj/item/clothing/under/f13/ncrcf - suit = /obj/item/clothing/suit/armor/medium/combat/rusted - shoes = /obj/item/clothing/shoes/f13/explorer - gloves = /obj/item/clothing/gloves/f13/leather/fingerless - head = /obj/item/clothing/head/helmet/f13/raidercombathelmet - neck = /obj/item/clothing/neck/mantle/brown - -// YANKEE RAIDER - +// Yankee Raider - baseball bat specialist /mob/living/simple_animal/hostile/raider/baseball icon_state = "baseball_raider" icon_living = "baseball_raider" icon_dead = "baseball_raider_dead" - retreat_distance = 1 - minimum_distance = 1 melee_damage_lower = 15 melee_damage_upper = 33 mob_armor = ARMOR_VALUE_RAIDER_ARMOR @@ -453,17 +408,10 @@ rapid_melee = 1 loot = list(/obj/item/twohanded/baseball, /obj/item/stack/f13Cash/random/med) loot_drop_amount = 3 - footstep_type = FOOTSTEP_MOB_SHOE - -/obj/effect/mob_spawn/human/corpse/raider/baseball - uniform = /obj/item/clothing/under/f13/mechanic - suit = /obj/item/clothing/suit/armor/medium/raider/yankee - shoes = /obj/item/clothing/shoes/f13/explorer - gloves = /obj/item/clothing/gloves/f13/leather/fingerless - head = /obj/item/clothing/head/helmet/f13/raider/yankee - -// TRIBAL RAIDER +/mob/living/simple_animal/hostile/raider/baseball/outlaw + name = "Baseball Outlaw" + faction = list("raider", "wastelander") /mob/living/simple_animal/hostile/raider/tribal icon_state = "tribal_raider" @@ -474,28 +422,30 @@ health = 125 melee_damage_lower = 12 melee_damage_upper = 37 + rapid_melee = 1 loot = list(/obj/item/twohanded/spear) loot_drop_amount = 3 - footstep_type = FOOTSTEP_MOB_SHOE - rapid_melee = 1 -/obj/effect/mob_spawn/human/corpse/raider/tribal - uniform = /obj/item/clothing/under/f13/raiderrags - suit = /obj/item/clothing/suit/armor/light/tribal - shoes = /obj/item/clothing/shoes/f13/rag - mask = /obj/item/clothing/mask/facewrap - head = /obj/item/clothing/head/helmet/f13/fiend +// only chosen for its damage values - using base is cleaner and more intentional. +/mob/living/simple_animal/hostile/raider/outlaw + name = "Bouncer" + faction = list("raider", "wastelander") // FIX: was list("Raiders","Wastelander") - wrong case, broke faction checks + icon_state = "badland_raider" + icon_living = "badland_raider" + icon_dead = "badland_raider_dead" + melee_damage_lower = 15 // Preserved from baseball parent + melee_damage_upper = 33 -////////////// -// SULPHITE // -////////////// +// ============================================================ +// SULPHITE RAIDERS +// ============================================================ /mob/living/simple_animal/hostile/raider/sulphite name = "Sulphite Brawler" desc = "A raider with low military grade armor and a shishkebab" icon_state = "sulphite" icon_living = "sulphite" - icon_dead= "sulphite_dead" + icon_dead = "sulphite_dead" mob_armor = ARMOR_VALUE_RAIDER_COMBAT_ARMOR_RUSTY maxHealth = 135 health = 135 @@ -504,11 +454,10 @@ melee_damage_upper = 37 loot = list(/obj/item/stack/f13Cash/random/med) loot_drop_amount = 5 - footstep_type = FOOTSTEP_MOB_SHOE -///////////// -// JUNKERS // -///////////// +// ============================================================ +// JUNKER GANG +// ============================================================ /mob/living/simple_animal/hostile/raider/junker name = "Junker" @@ -523,7 +472,6 @@ rapid_melee = 1 melee_damage_lower = 18 melee_damage_upper = 42 - footstep_type = FOOTSTEP_MOB_SHOE /mob/living/simple_animal/hostile/raider/ranged/boss/junker name = "Junker Footman" @@ -539,7 +487,6 @@ rapid_melee = 1 melee_damage_lower = 20 melee_damage_upper = 38 - footstep_type = FOOTSTEP_MOB_SHOE loot_drop_amount = 5 /mob/living/simple_animal/hostile/raider/junker/creator @@ -551,23 +498,24 @@ mob_armor = ARMOR_VALUE_RAIDER_COMBAT_ARMOR_RUSTY maxHealth = 150 health = 150 + + combat_mode = COMBAT_MODE_RANGED ranged = TRUE retreat_distance = 6 - minimum_distance = 8 + minimum_distance = 1 + rapid_melee = 1 ranged_cooldown_time = 2 SECONDS - auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL projectiletype = /obj/item/projectile/bullet/c45/op projectilesound = 'sound/weapons/gunshot.ogg' + var/list/spawned_mobs = list() var/max_mobs = 3 var/mob_types = list(/mob/living/simple_animal/hostile/eyebot/reinforced) var/spawn_time = 15 SECONDS var/spawn_text = "flies from" - footstep_type = FOOTSTEP_MOB_SHOE loot_drop_amount = 5 - /mob/living/simple_animal/hostile/raider/junker/creator/Initialize() . = ..() AddComponent(/datum/component/spawner, mob_types, spawn_time, faction, spawn_text, max_mobs, _range = 7) @@ -576,15 +524,11 @@ RemoveComponentByType(/datum/component/spawner) . = ..() -/mob/living/simple_animal/hostile/raider/junker/creator/Destroy() - RemoveComponentByType(/datum/component/spawner) - . = ..() - /mob/living/simple_animal/hostile/raider/junker/creator/Aggro() . = ..() if(.) return - summon_backup(10) + summon_backup(8) /mob/living/simple_animal/hostile/raider/junker/boss name = "Junker Boss" @@ -596,92 +540,65 @@ maxHealth = 165 health = 165 stat_attack = UNCONSCIOUS + + combat_mode = COMBAT_MODE_RANGED ranged = TRUE + retreat_distance = 5 + minimum_distance = 1 + rapid_melee = 1 - retreat_distance = 4 - minimum_distance = 6 extra_projectiles = 2 ranged_cooldown_time = 2 SECONDS - auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL projectiletype = /obj/item/projectile/bullet/shrapnel/simple projectilesound = 'sound/f13weapons/auto5.ogg' + loot = list(/obj/item/stack/f13Cash/random/high) - footstep_type = FOOTSTEP_MOB_SHOE - loot_drop_amount = 10 + loot_drop_amount = 10 // Intentional - boss drops a big pile of caps loot_amount_random = FALSE - -//Outlaw Raiders +// ============================================================ +// CULTIST RAIDERS +// implicit empty type that won't inherit random_trash_loot, faction, etc. cleanly. +// ============================================================ -/mob/living/simple_animal/hostile/raider/baseball/outlaw - name = "Bouncer" - faction = list("Raiders","Wastelander") - icon_state = "badland_raider" - icon_living = "badland_raider" - icon_dead = "badland_raider_dead" - -// Cultist Stuff +/mob/living/simple_animal/hostile/raider/cultist + faction = list("cultist") // Cultists are separate from the raider faction + random_trash_loot = FALSE // Cultists don't carry general wasteland trash /mob/living/simple_animal/hostile/raider/cultist/melee name = "Cultist Shredder" desc = "A nightmare in a robe. Now with 100% less conversion!" - icon = 'icons/fallout/mobs/humans/raider.dmi' icon_state = "cult_axeghoul" icon_living = "cult_axeghoul" icon_dead = "cult_dead" - mob_biotypes = MOB_ORGANIC|MOB_HUMANOID - turns_per_move = 5 - mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET maxHealth = 100 health = 100 melee_damage_lower = 10 melee_damage_upper = 24 - attack_verb_simple = "clobbers" - attack_sound = 'sound/weapons/bladeslice.ogg' - a_intent = INTENT_HARM - faction = list("raider") - check_friendly_fire = TRUE - status_flags = CANPUSH - del_on_death = FALSE loot = list(/obj/item/melee/onehanded/knife/survival, /obj/item/stack/f13Cash/random/med) - loot_drop_amount = 2 - footstep_type = FOOTSTEP_MOB_SHOE - rapid_melee = 2 - melee_queue_distance = 5 - move_to_delay = 3.0 - waddle_amount = 2 - waddle_up_time = 1 - waddle_side_time = 1 - retreat_distance = 1 //mob retreats 1 tile when in min distance - minimum_distance = 1 //Mob pushes up to melee, then backs off to avoid player attack? - aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first. - vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer. /mob/living/simple_animal/hostile/raider/cultist/ranged name = "Cultist Gunner" desc = "A nightmare in a robe. Now with 100% less conversion!" - icon = 'icons/fallout/mobs/humans/raider.dmi' icon_state = "cultist_pistol" icon_living = "cultist_pistol" icon_dead = "cultist_dead" - ranged = TRUE - mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET maxHealth = 85 health = 85 - rapid_melee = 2 - melee_queue_distance = 5 - move_to_delay = 3.0 //faster than average, but not a lot - retreat_distance = 4 //mob retreats 1 tile when in min distance - minimum_distance = 2 //Mob pushes up to melee, then backs off to avoid player attack? - aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first. - vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer. + + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 5 + minimum_distance = 1 + ranged_cooldown_time = 2 SECONDS - auto_fire_delay = GUN_AUTOFIRE_DELAY_NORMAL + // FIX: c10mm projectile was paired with ninemil.ogg sound. Using 10mm sound. projectiletype = /obj/item/projectile/bullet/c10mm/simple - projectilesound = 'sound/f13weapons/ninemil.ogg' + projectilesound = 'sound/f13weapons/10mm_fire_02.ogg' + loot = list(/obj/item/gun/ballistic/automatic/pistol/n99, /obj/item/stack/f13Cash/random/med) loot_drop_amount = 3 - footstep_type = FOOTSTEP_MOB_SHOE + projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(PISTOL_LIGHT_VOLUME), @@ -695,31 +612,20 @@ /mob/living/simple_animal/hostile/raider/cultist/ranged/shotgun name = "Cultist Crowd Controller" - desc = "A nightmare in a robe. Now with 100% less conversion!" - icon = 'icons/fallout/mobs/humans/raider.dmi' icon_state = "cultist_shotgun" icon_living = "cultist_shotgun" - icon_dead = "cultist_dead" - ranged = TRUE - mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET - maxHealth = 85 - health = 85 - rapid_melee = 2 - melee_queue_distance = 5 - move_to_delay = 3.0 //faster than average, but not a lot - retreat_distance = 4 //mob retreats 1 tile when in min distance - minimum_distance = 2 //Mob pushes up to melee, then backs off to avoid player attack? - aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first. - vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer. + icon_dead = "cultist_shotgun_dead" // FIX: was missing, fell back to wrong parent dead sprite + ranged_cooldown_time = 4 SECONDS auto_fire_delay = GUN_AUTOFIRE_DELAY_SLOW projectiletype = /obj/item/projectile/bullet/pellet/shotgun_buckshot projectilesound = 'sound/f13weapons/shotgun.ogg' sound_after_shooting = 'sound/weapons/shotguninsert.ogg' extra_projectiles = 1 + loot = list(/obj/item/gun/ballistic/shotgun/trench, /obj/item/stack/f13Cash/random/med) loot_drop_amount = 6 - footstep_type = FOOTSTEP_MOB_SHOE + projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(SHOTGUN_VOLUME), @@ -733,114 +639,87 @@ /mob/living/simple_animal/hostile/raider/cultist/ranged/smg name = "Cultist Bulletmage" - desc = "A nightmare in a robe. Now with 100% less conversion!" - icon = 'icons/fallout/mobs/humans/raider.dmi' icon_state = "cultist2_smg" icon_living = "cultist2_smg" icon_dead = "cultist2_dead" - ranged = TRUE - mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET - maxHealth = 85 - health = 85 - rapid_melee = 2 - melee_queue_distance = 5 - move_to_delay = 3.0 //faster than average, but not a lot - retreat_distance = 4 //mob retreats 1 tile when in min distance - minimum_distance = 2 //Mob pushes up to melee, then backs off to avoid player attack? - aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first. - vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer. + ranged_cooldown_time = 1 SECONDS auto_fire_delay = GUN_AUTOFIRE_DELAY_FAST projectiletype = /obj/item/projectile/bullet/c22 projectilesound = 'sound/f13weapons/assaultrifle_fire.ogg' sound_after_shooting = 'sound/weapons/shotguninsert.ogg' extra_projectiles = 2 + loot = list(/obj/item/gun/ballistic/automatic/smg/mini_uzi/smg22, /obj/item/stack/f13Cash/random/med) loot_drop_amount = 8 - footstep_type = FOOTSTEP_MOB_SHOE - projectile_sound_properties = list( - SP_VARY(FALSE), - SP_VOLUME(PISTOL_LIGHT_VOLUME), - SP_VOLUME_SILENCED(PISTOL_LIGHT_VOLUME * SILENCED_VOLUME_MULTIPLIER), - SP_NORMAL_RANGE(PISTOL_LIGHT_RANGE), - SP_NORMAL_RANGE_SILENCED(SILENCED_GUN_RANGE), - SP_IGNORE_WALLS(TRUE), - SP_DISTANT_SOUND(PISTOL_LIGHT_DISTANT_SOUND), - SP_DISTANT_RANGE(PISTOL_LIGHT_RANGE_DISTANT) - ) /mob/living/simple_animal/hostile/raider/cultist/ranged/tesla name = "Cultist Lasermage" - desc = "A nightmare in a robe. Now with 100% less conversion!" - icon = 'icons/fallout/mobs/humans/raider.dmi' icon_state = "cultist3_tesla" icon_living = "cultist3_tesla" icon_dead = "cultist3_dead" - ranged = TRUE - mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET maxHealth = 150 health = 150 - rapid_melee = 2 - melee_queue_distance = 5 - move_to_delay = 3.0 //faster than average, but not a lot - retreat_distance = 4 //mob retreats 1 tile when in min distance - minimum_distance = 2 //Mob pushes up to melee, then backs off to avoid player attack? - aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first. - vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer. + ranged_cooldown_time = 2 SECONDS auto_fire_delay = GUN_AUTOFIRE_DELAY_FAST projectiletype = /obj/item/projectile/energy/teslacannon/eastwood projectilesound = 'sound/weapons/resonator_fire.ogg' sound_after_shooting = 'sound/f13weapons/rcwfire.ogg' extra_projectiles = 2 + loot = list(/obj/item/gun/energy/laser/auto/eastwood, /obj/item/stack/f13Cash/random/high) loot_drop_amount = 8 - footstep_type = FOOTSTEP_MOB_SHOE - projectile_sound_properties = list( - SP_VARY(FALSE), - SP_VOLUME(PISTOL_LIGHT_VOLUME), - SP_VOLUME_SILENCED(PISTOL_LIGHT_VOLUME * SILENCED_VOLUME_MULTIPLIER), - SP_NORMAL_RANGE(PISTOL_LIGHT_RANGE), - SP_NORMAL_RANGE_SILENCED(SILENCED_GUN_RANGE), - SP_IGNORE_WALLS(TRUE), - SP_DISTANT_SOUND(PISTOL_LIGHT_DISTANT_SOUND), - SP_DISTANT_RANGE(PISTOL_LIGHT_RANGE_DISTANT) - ) /mob/living/simple_animal/hostile/raider/cultist/ranged/radiation name = "Cultist Converter" desc = "A nightmare in a robe. Now with 69% more conversion!" - icon = 'icons/fallout/mobs/humans/raider.dmi' icon_state = "cultist3_tesla" icon_living = "cultist3_tesla" icon_dead = "cultist3_dead" - ranged = TRUE - mob_armor = ARMOR_VALUE_RAIDER_LEATHER_JACKET maxHealth = 150 health = 150 - rapid_melee = 2 - melee_queue_distance = 5 - move_to_delay = 3.0 //faster than average, but not a lot - retreat_distance = 4 //mob retreats 1 tile when in min distance - minimum_distance = 2 //Mob pushes up to melee, then backs off to avoid player attack? - aggro_vision_range = 6 //mob waits to attack if the player chooses to close distance, or if the player attacks first. - vision_range = 8 //will see the player at max view range, and communicate that they've been seen but won't aggro unless they get closer. + ranged_cooldown_time = 2 SECONDS auto_fire_delay = GUN_AUTOFIRE_DELAY_FAST projectiletype = /obj/item/projectile/energy/nuclear_particle projectilesound = 'sound/weapons/resonator_fire.ogg' sound_after_shooting = 'sound/f13weapons/rcwfire.ogg' extra_projectiles = 1 + loot = list(/obj/item/gun/energy/gammagun, /obj/item/stack/f13Cash/random/high) loot_drop_amount = 10 - footstep_type = FOOTSTEP_MOB_SHOE - projectile_sound_properties = list( - SP_VARY(FALSE), - SP_VOLUME(PISTOL_LIGHT_VOLUME), - SP_VOLUME_SILENCED(PISTOL_LIGHT_VOLUME * SILENCED_VOLUME_MULTIPLIER), - SP_NORMAL_RANGE(PISTOL_LIGHT_RANGE), - SP_NORMAL_RANGE_SILENCED(SILENCED_GUN_RANGE), - SP_IGNORE_WALLS(TRUE), - SP_DISTANT_SOUND(PISTOL_LIGHT_DISTANT_SOUND), - SP_DISTANT_RANGE(PISTOL_LIGHT_RANGE_DISTANT) - ) + +// ============================================================ +// CORPSE SPAWNERS +// ============================================================ + +/obj/effect/mob_spawn/human/corpse/raider + name = "Raider" + uniform = /obj/item/clothing/under/f13/rag + suit = /obj/item/clothing/suit/armor/medium/raider/iconoclast + shoes = /obj/item/clothing/shoes/f13/explorer + gloves = /obj/item/clothing/gloves/f13/leather + head = /obj/item/clothing/head/helmet/f13/firefighter + +/obj/effect/mob_spawn/human/corpse/raider/ranged/biker + uniform = /obj/item/clothing/under/f13/ncrcf + suit = /obj/item/clothing/suit/armor/medium/combat/rusted + shoes = /obj/item/clothing/shoes/f13/explorer + gloves = /obj/item/clothing/gloves/f13/leather/fingerless + head = /obj/item/clothing/head/helmet/f13/raidercombathelmet + neck = /obj/item/clothing/neck/mantle/brown + +/obj/effect/mob_spawn/human/corpse/raider/baseball + uniform = /obj/item/clothing/under/f13/mechanic + suit = /obj/item/clothing/suit/armor/medium/raider/yankee + shoes = /obj/item/clothing/shoes/f13/explorer + gloves = /obj/item/clothing/gloves/f13/leather/fingerless + head = /obj/item/clothing/head/helmet/f13/raider/yankee + +/obj/effect/mob_spawn/human/corpse/raider/tribal + uniform = /obj/item/clothing/under/f13/raiderrags + suit = /obj/item/clothing/suit/armor/light/tribal + shoes = /obj/item/clothing/shoes/f13/rag + mask = /obj/item/clothing/mask/facewrap + head = /obj/item/clothing/head/helmet/f13/fiend diff --git a/code/modules/mob/living/simple_animal/hostile/f13/rattler.dm b/code/modules/mob/living/simple_animal/hostile/f13/rattler.dm index 4cbc3440100..f16a89988b8 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/rattler.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/rattler.dm @@ -1,10 +1,10 @@ -/*IN THIS FILE: --Rattler -as per originally proposed concept: less powerful directly than deathclaws, but faster, and venom is rough. don't yell at me if this is OP -using ant armor b/c it just kinda works here and i don't want it to be super beefy lol. -*/ +// In this document: Texas Rattler +// Less powerful directly than deathclaws, but faster and venom is rough. + +/////////////////// +// TEXAS RATTLER // +/////////////////// -//Rattler /mob/living/simple_animal/hostile/texas_rattler name = "texas rattler" desc = "Keratin gleams and articulates over its massive sixty-foot body. Distended venom glands behind its upper pterygoid shudder and pressure deadly venom into its victims. A coil of thick muscle allows it to pounce. In layman's terms: don't get bit." @@ -12,34 +12,74 @@ using ant armor b/c it just kinda works here and i don't want it to be super bee icon_state = "texasrattler" icon_living = "texasrattler" icon_dead = "texasrattler_dead" - icon_gib = "texasrattler_gib" //TODO: this is terrible - mob_armor = ARMOR_VALUE_ANTS + icon_gib = "texasrattler_gib" + + mob_biotypes = MOB_ORGANIC|MOB_BEAST + mob_armor = ARMOR_VALUE_ANTS // Lightly armored - speed is the threat + maxHealth = 150 health = 150 + speed = -1 // Very fast + move_to_delay = 2.5 stat_attack = UNCONSCIOUS - reach = 2 - speed = -1 - move_to_delay = 2.1 - tastes = list("weird oil" = 5, "dirt" = 1) - - speak_emote = list("hisses", "shakes its rattle") - emote_hear = list("flicks its tongue.") - emote_taunt = list("flicks its tongue", "hisses and shakes its ratttle") - - speak_chance = 10 - taunt_chance = 25 - + + reach = 2 // Long striking range + see_in_dark = 8 + melee_damage_lower = 18 melee_damage_upper = 38 - + + speak_emote = list("hisses", "shakes its rattle") + emote_hear = list("flicks its tongue") + emote_taunt = list("flicks its tongue", "hisses and shakes its rattle") attack_verb_simple = "bites and constricts" - see_in_dark = 8 - - retreat_distance = 2 - + + speak_chance = 10 + taunt_chance = 25 + + tastes = list("weird oil" = 5, "dirt" = 1) + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab = 4, + /obj/item/stack/sheet/sinew = 3, + /obj/item/stack/sheet/animalhide = 2 + ) + butcher_difficulty = 2 + faction = list("hostile", "wastebot", "ghoul", "cazador", "supermutant", "bighorner") + a_intent = INTENT_HARM + gold_core_spawnable = HOSTILE_SPAWN + + can_ghost_into = TRUE + pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS + + // Z-movement + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 30 + can_open_doors = FALSE + can_open_airlocks = FALSE + + // Pure melee - ambush predator with reach + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + +/mob/living/simple_animal/hostile/texas_rattler/Aggro() + . = ..() + if(.) + return + summon_backup(8) +/mob/living/simple_animal/hostile/texas_rattler/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() /mob/living/simple_animal/hostile/texas_rattler/AttackingTarget() . = ..() diff --git a/code/modules/mob/living/simple_animal/hostile/f13/supermutant.dm b/code/modules/mob/living/simple_animal/hostile/f13/supermutant.dm index 7062359c144..9440f2a485b 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/supermutant.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/supermutant.dm @@ -1,4 +1,8 @@ -//Fallout 13 super mutants directory +// Fallout 13 super mutants + +// ============================================================ +// BASE SUPER MUTANT +// ============================================================ /mob/living/simple_animal/hostile/supermutant name = "super mutant" @@ -11,75 +15,148 @@ mob_biotypes = MOB_ORGANIC|MOB_HUMANOID mob_armor = ARMOR_VALUE_SUPERMUTANT_BASE sentience_type = SENTIENCE_BOSS - maxHealth = 130 - health = 130 - stat_attack = UNCONSCIOUS - speak_chance = 10 - speak = list( - "GRRRRRR!", - "ARGH!", - "NNNNNGH!", - "HMPH!", - "ARRRRR!" - ) - speak_emote = list( - "shouts", - "yells" - ) - move_to_delay = 5 + maxHealth = 110 + health = 110 stat_attack = CONSCIOUS robust_searching = 1 - environment_smash = ENVIRONMENT_SMASH_WALLS + check_friendly_fire = FALSE + environment_smash = ENVIRONMENT_SMASH_STRUCTURES + + speak_chance = 10 + speak = list("GRRRRRR!", "ARGH!", "NNNNNGH!", "HMPH!", "ARRRRR!") + speak_emote = list("shouts", "yells") + emote_taunt = list("yells") emote_taunt_sound = list( 'sound/f13npc/supermutant/attack1.ogg', 'sound/f13npc/supermutant/attack2.ogg', 'sound/f13npc/supermutant/attack3.ogg' - ) - emote_taunt = list("yells") + ) taunt_chance = 30 + turns_per_move = 5 + move_to_delay = 4 response_help_simple = "touches" response_disarm_simple = "pushes" response_harm_simple = "hits" - faction = list( - "hostile", - "supermutant" - ) + + faction = list("hostile", "supermutant") + melee_damage_lower = 20 melee_damage_upper = 35 + aggro_vision_range = 7 - //tiles within they start attacking, doesn't count the mobs tile vision_range = 8 - //tiles within they start making noise, does count the mobs tile + mob_size = MOB_SIZE_LARGE move_resist = MOVE_FORCE_OVERPOWERING attack_verb_simple = "smashes" attack_sound = "punch" a_intent = INTENT_GRAB + idlesound = list( 'sound/f13npc/supermutant/idle1.ogg', 'sound/f13npc/supermutant/idle2.ogg', 'sound/f13npc/supermutant/idle3.ogg', 'sound/f13npc/supermutant/idle4.ogg' - ) + ) death_sound = list( 'sound/f13npc/supermutant/death1.ogg', 'sound/f13npc/supermutant/death2.ogg' - ) + ) aggrosound = list( 'sound/f13npc/supermutant/alert1.ogg', 'sound/f13npc/supermutant/alert2.ogg', 'sound/f13npc/supermutant/alert3.ogg', 'sound/f13npc/supermutant/alert4.ogg' - ) + ) + wound_bonus = 0 bare_wound_bonus = 0 footstep_type = FOOTSTEP_MOB_HEAVY + // Rage mode vars + low_health_threshold = 0.4 + var/color_rage = "#ff9999" + + can_z_move = TRUE + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + + can_open_doors = TRUE + can_open_airlocks = FALSE + + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + +// FIX: Consolidated death() icon swap here so all subtypes inherit it. +// Previously every subtype copy-pasted: icon = dead_dmi, icon_state = icon_dead, anchored = FALSE. +// Now they all inherit this single override. +/mob/living/simple_animal/hostile/supermutant/death(gibbed) + icon = 'icons/fallout/mobs/supermutant_dead.dmi' + icon_state = icon_dead + . = ..() + +// FIX: Added if(.) return guard. Previously summon_backup fired unconditionally, +// meaning player-controlled supermutants would also call for backup on aggro. +/mob/living/simple_animal/hostile/supermutant/Aggro() + . = ..() + if(is_low_health) + return // Already raged, skip + if(health < (maxHealth * low_health_threshold)) + make_low_health() + +// Friendly fire resistance +/mob/living/simple_animal/hostile/supermutant/bullet_act(obj/item/projectile/P) + if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/supermutant)) + var/original_damage = P.damage + P.damage *= 0.2 + . = ..() + P.damage = original_damage + return + return ..() + +/// RAGE MODE - faster, harder hitting, can smash through walls +/mob/living/simple_animal/hostile/supermutant/make_low_health() + if(!target) + return // Not in combat, don't rage + ..() + visible_message(span_danger("[src] roars with primal fury!!!")) + playsound(src, pick(aggrosound), 100, 1, SOUND_DISTANCE(15)) + color = color_rage + speed *= 0.8 + melee_damage_lower = round(melee_damage_lower * 1.3) + melee_damage_upper = round(melee_damage_upper * 1.3) + obj_damage *= 1.5 + environment_smash = ENVIRONMENT_SMASH_STRUCTURES | ENVIRONMENT_SMASH_WALLS + sound_pitch = -30 + is_low_health = TRUE + +/// Calming down from rage +/mob/living/simple_animal/hostile/supermutant/make_high_health() + visible_message(span_notice("[src]'s rage subsides.")) + color = initial(color) + speed = initial(speed) + melee_damage_lower = initial(melee_damage_lower) + melee_damage_upper = initial(melee_damage_upper) + obj_damage = initial(obj_damage) + environment_smash = initial(environment_smash) + sound_pitch = initial(sound_pitch) + is_low_health = FALSE + +/mob/living/simple_animal/hostile/supermutant/Move() + if(is_low_health && health > 0) + DestroySurroundings() + . = ..() + +// ============================================================ +// PLAYABLE SUPERMUTANT +// ============================================================ + /mob/living/simple_animal/hostile/supermutant/playable - mob_armor = ARMOR_VALUE_SUPERMUTANT_BASE - maxHealth = 130 - health = 130 + maxHealth = 110 + health = 110 emote_taunt_sound = null emote_taunt = null aggrosound = null @@ -90,17 +167,11 @@ dextrous = TRUE possible_a_intents = list(INTENT_HELP, INTENT_HARM) -/mob/living/simple_animal/hostile/supermutant/Aggro() - ..() - summon_backup(15) - -/mob/living/simple_animal/hostile/supermutant/death(gibbed) - icon = 'icons/fallout/mobs/supermutant_dead.dmi' - icon_state = icon_dead - anchored = FALSE - ..() +// ============================================================ +// BRAH-MIN WATCHER +// ============================================================ -/mob/living/simple_animal/pet/dog/mutant //This is a supermutant, totally not a dog, and he is friendly +/mob/living/simple_animal/pet/dog/mutant name = "Brah-Min" desc = "A large, docile supermutant. Adopted by Shale's Army as a sort of watch dog for their brahmin herd." icon = 'icons/fallout/mobs/supermutant.dmi' @@ -109,68 +180,81 @@ icon_dead = "hulk_113_s" maxHealth = 240 health = 240 - speak_chance = 7 //30 //Oh my god he never shuts up. + speak_chance = 7 move_resist = MOVE_FORCE_OVERPOWERING mob_size = MOB_SIZE_LARGE - speak = list("Hey! These my brahmins!", "And I say, HEY-YEY-AAEYAAA-EYAEYAA! HEY-YEY-AAEYAAA-EYAEYAA! I SAID HEY, what's going on?", "What do you want from my brahmins?!", "Me gonna clean brahmin poop again now!", "I love brahmins, brahmins are good, just poop much!", "Do not speak to my brahmins ever again, you hear?!", "Bad raiders come to steal my brahmins - I crush with shovel!", "Do not come to my brahmins! Do not touch my brahmins! Do not look at my brahmins!", "I'm watching you, and my brahmins watch too!", "Brahmins say moo, and I'm saying - hey, get your ugly face out of my way!", "I... I remember, before the fire... THERE WERE NO BRAHMINS!", "No! No wind brahmin here! Wind brahmin lie!") + speak = list( + "Hey! These my brahmins!", + "And I say, HEY-YEY-AAEYAAA-EYAEYAA! HEY-YEY-AAEYAAA-EYAEYAA! I SAID HEY, what's going on?", + "What do you want from my brahmins?!", + "Me gonna clean brahmin poop again now!", + "I love brahmins, brahmins are good, just poop much!", + "Do not speak to my brahmins ever again, you hear?!", + "Bad raiders come to steal my brahmins - I crush with shovel!", + "Do not come to my brahmins! Do not touch my brahmins! Do not look at my brahmins!", + "I'm watching you, and my brahmins watch too!", + "Brahmins say moo, and I'm saying - hey, get your ugly face out of my way!", + "I... I remember, before the fire... THERE WERE NO BRAHMINS!", + "No! No wind brahmin here! Wind brahmin lie!" + ) speak_emote = list("shouts", "yells") - emote_hear = list("yawns", "mumbles","sighs") + emote_hear = list("yawns", "mumbles", "sighs") emote_see = list("raises his shovel", "shovels some dirt away", "waves his shovel above his head angrily") response_help_simple = "pat" response_disarm_simple = "push" response_harm_simple = "punch" -// butcher_results = list(/obj/item/weapon/reagent_containers/food/snacks/bearsteak = 3) /mob/living/simple_animal/pet/dog/mutant/death(gibbed) icon = 'icons/fallout/mobs/supermutant_dead.dmi' icon_state = icon_dead - anchored = FALSE if(!gibbed) visible_message(span_danger("\the [src] shouts something incoherent about brahmins for the last time and stops moving...")) - ..() + . = ..() + +// ============================================================ +// MELEE SUPERMUTANT +// ============================================================ /mob/living/simple_animal/hostile/supermutant/meleemutant name = "sledgehammer supermutant" desc = "An enormous, green tank of a humanoid wrapped in thick sheets of metal and boiled leather from hopefully a brahmin or two. \ They're a mountain of furry muscle, and their fists look like they could punch through solid steel. \ If that wasn't bad enough, this monstrous critter is wielding a sledgehammer. Lovely." - icon = 'icons/fallout/mobs/supermutant.dmi' icon_state = "hulk_melee_s" icon_living = "hulk_melee_s" icon_dead = "hulk_melee_s" mob_armor = ARMOR_VALUE_SUPERMUTANT_MELEE - maxHealth = 130 - health = 130 + maxHealth = 110 + health = 110 mob_armor_tokens = list( ARMOR_MODIFIER_UP_MELEE_T1, ARMOR_MODIFIER_DOWN_LASER_T2, ARMOR_MODIFIER_UP_DT_T2 - ) + ) melee_damage_lower = 18 melee_damage_upper = 44 attack_sound = "hit_swing" - footstep_type = FOOTSTEP_MOB_HEAVY -/mob/living/simple_animal/hostile/supermutant/meleemutant/death(gibbed) - icon = 'icons/fallout/mobs/supermutant_dead.dmi' - icon_state = icon_dead - anchored = FALSE - ..() +// ============================================================ +// RANGED SUPERMUTANT +// ============================================================ /mob/living/simple_animal/hostile/supermutant/rangedmutant desc = "An enormous green mass of a humanoid wrapped in thick sheets of metal and boiled leather from hopefully a brahmin or two. \ They're a mountain of furry muscle, and their fists look like they could punch through solid steel. \ If that wasn't bad enough, this monstrous critter is wielding a crude shotgun. Lovely." - icon = 'icons/fallout/mobs/supermutant.dmi' icon_state = "hulk_ranged_s" icon_living = "hulk_ranged_s" icon_dead = "hulk_ranged_s" - ranged = 1 mob_armor = ARMOR_VALUE_SUPERMUTANT_RANGER - maxHealth = 130 - health = 130 - retreat_distance = 1 + maxHealth = 110 + health = 110 + + combat_mode = COMBAT_MODE_MIXED + ranged = TRUE + retreat_distance = 3 minimum_distance = 1 + casingtype = /obj/item/ammo_casing/shotgun/improvised projectiletype = null projectilesound = 'sound/f13weapons/shotgun.ogg' @@ -179,11 +263,12 @@ extra_projectiles = 1 auto_fire_delay = GUN_BURSTFIRE_DELAY_FAST ranged_cooldown_time = 4 SECONDS + loot = list( /obj/item/ammo_box/shotgun/improvised, /obj/item/gun/ballistic/revolver/widowmaker - ) - footstep_type = FOOTSTEP_MOB_HEAVY + ) + projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(SHOTGUN_VOLUME), @@ -195,6 +280,7 @@ SP_DISTANT_RANGE(SHOTGUN_RANGE_DISTANT) ) +// Varmint rifle variant /mob/living/simple_animal/hostile/supermutant/rangedmutant/varmint desc = "An enormous green mass of a humanoid wrapped in thick sheets of metal and boiled leather from hopefully a brahmin or two. \ They're a mountain of furry muscle, and their fists look like they could punch through solid steel. \ @@ -203,93 +289,87 @@ projectiletype = /obj/item/projectile/bullet/a556/simple projectilesound = 'sound/f13weapons/assaultrifle_fire.ogg' sound_after_shooting = null - sound_after_shooting_delay = 1 SECONDS extra_projectiles = 0 ranged_cooldown_time = 2 SECONDS - loot = list( - /obj/item/gun/ballistic/automatic/varmint - ) + loot = list(/obj/item/gun/ballistic/automatic/varmint) -/mob/living/simple_animal/hostile/supermutant/rangedmutant/death(gibbed) - icon = 'icons/fallout/mobs/supermutant_dead.dmi' - icon_state = icon_dead - anchored = FALSE - ..() +// ============================================================ +// LEGENDARY SUPERMUTANT +// ============================================================ /mob/living/simple_animal/hostile/supermutant/legendary name = "legendary super mutant" - desc = "A huge and ugly mutant humanoid.He has a faint yellow glow to him, scars adorn his body. This super mutant is a grizzled vetern of combat. Look out!" + desc = "A huge and ugly mutant humanoid. He has a faint yellow glow to him, scars adorn his body. This super mutant is a grizzled veteran of combat. Look out!" color = "#FFFF00" + color_rage = "#ffcc66" mob_armor = ARMOR_VALUE_SUPERMUTANT_LEGEND - maxHealth = 150 - health = 150 + maxHealth = 130 + health = 130 icon_state = "hulk_113_s" icon_living = "hulk_113_s" icon_dead = "hulk_113_s" melee_damage_lower = 27 melee_damage_upper = 57 - mob_size = 5 - footstep_type = FOOTSTEP_MOB_HEAVY + mob_size = MOB_SIZE_HUGE // FIX: was raw 5 -/mob/living/simple_animal/hostile/supermutant/legendary/death(gibbed) - icon = 'icons/fallout/mobs/supermutant_dead.dmi' - icon_state = icon_dead - anchored = FALSE - ..() +// ============================================================ +// NIGHTKIN +// Semi-invisible supermutant variants +// ============================================================ /mob/living/simple_animal/hostile/supermutant/nightkin name = "nightkin" - desc = "A blue variant of the standard Super Mutant, equiped with steathboys." - icon = 'icons/fallout/mobs/supermutant.dmi' + desc = "A blue variant of the standard Super Mutant, equipped with stealth boys." icon_state = "night_s" icon_living = "night_s" icon_dead = "night_s" mob_armor = ARMOR_VALUE_SUPERMUTANT_MELEE - maxHealth = 140 - health = 140 + maxHealth = 120 + health = 120 alpha = 80 force_threshold = 15 melee_damage_lower = 27 melee_damage_upper = 50 attack_verb_simple = "slashes" attack_sound = "sound/weapons/bladeslice.ogg" - footstep_type = FOOTSTEP_MOB_HEAVY + color_rage = "#9999ff" + + // Nightkin have excellent night vision for stealth operations + has_low_light_vision = TRUE /mob/living/simple_animal/hostile/supermutant/nightkin/Aggro() - ..() - summon_backup(15) + . = ..() + if(.) + return + summon_backup(10) alpha = 255 -/mob/living/simple_animal/hostile/supermutant/nightkin/death(gibbed) - icon = 'icons/fallout/mobs/supermutant_dead.dmi' - icon_state = icon_dead - anchored = FALSE - ..() - /mob/living/simple_animal/hostile/supermutant/nightkin/rangedmutant name = "nightkin veteran" - desc = "A blue variant of the standard Super Mutant, equiped with steathboys. This one is holding an Assault Rifle." - icon = 'icons/fallout/mobs/supermutant.dmi' + desc = "A blue variant of the standard Super Mutant, equipped with stealth boys. This one is holding an Assault Rifle." icon_state = "night_ranged_s" icon_living = "night_ranged_s" icon_dead = "night_ranged_s" mob_armor = ARMOR_VALUE_SUPERMUTANT_RANGER - maxHealth = 140 - health = 140 - ranged = 1 + maxHealth = 120 + health = 120 alpha = 80 force_threshold = 15 melee_damage_lower = 25 melee_damage_upper = 37 attack_verb_simple = "smashes" attack_sound = "punch" - extra_projectiles = 1 + + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE retreat_distance = 4 - minimum_distance = 6 + minimum_distance = 4 + + extra_projectiles = 1 projectiletype = /obj/item/projectile/bullet/a556/simple projectilesound = 'sound/f13weapons/assaultrifle_fire.ogg' loot = list(/obj/item/ammo_box/magazine/m556/rifle) - footstep_type = FOOTSTEP_MOB_HEAVY + projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(RIFLE_LIGHT_VOLUME), @@ -302,39 +382,37 @@ ) /mob/living/simple_animal/hostile/supermutant/nightkin/rangedmutant/Aggro() - ..() - summon_backup(15) + . = ..() + if(.) + return + summon_backup(10) alpha = 255 -/mob/living/simple_animal/hostile/supermutant/nightkin/rangedmutant/death(gibbed) - icon = 'icons/fallout/mobs/supermutant_dead.dmi' - icon_state = icon_dead - anchored = FALSE - ..() - /mob/living/simple_animal/hostile/supermutant/nightkin/elitemutant name = "nightkin elite" desc = "A blue variant of the standard Super Mutant, and a remnant of the Masters Army." - icon = 'icons/fallout/mobs/supermutant.dmi' icon_state = "night_boss_s" icon_living = "night_boss_s" icon_dead = "night_boss_s" - ranged = 1 mob_armor = ARMOR_VALUE_SUPERMUTANT_LEGEND - maxHealth = 130 - health = 130 + maxHealth = 110 + health = 110 alpha = 80 force_threshold = 15 melee_damage_lower = 20 melee_damage_upper = 47 attack_verb_simple = "smashes" attack_sound = "punch" + + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE retreat_distance = 5 - minimum_distance = 7 + minimum_distance = 5 + projectiletype = /obj/item/projectile/f13plasma/repeater projectilesound = 'sound/f13weapons/plasma_rifle.ogg' loot = list(/obj/item/stock_parts/cell/ammo/mfc) - footstep_type = FOOTSTEP_MOB_HEAVY + projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(PLASMA_VOLUME), @@ -347,148 +425,158 @@ ) /mob/living/simple_animal/hostile/supermutant/nightkin/elitemutant/Aggro() - ..() - summon_backup(15) + . = ..() + if(.) // FIX: same guard as nightkin/Aggro + return + summon_backup(10) alpha = 255 -/mob/living/simple_animal/hostile/supermutant/nightkin/elitemutant/death(gibbed) - icon = 'icons/fallout/mobs/supermutant_dead.dmi' - icon_state = icon_dead - anchored = FALSE - ..() +// ============================================================ +// CULT OF RAIN VARIANTS +// ============================================================ -//Cult Of Rain /mob/living/simple_animal/hostile/supermutant/meleemutant/rain name = "super mutant rain cultist" desc = "A super mutant covered in blue markings that has been indoctrinated into the Cult Of Rain. This one wields a sledgehammer blessed by the rain gods." color = "#6B87C0" + color_rage = "#4488cc" speak_chance = 10 speak = list("The rain cleanses!", "Sacrifices for the rain gods!", "The thunder guides my fury!", "I am become the storm, destroyer of all heretics!", "The priests will be pleased with my sacrifices!") - maxHealth = 360 - health = 360 - damage_coeff = list(BRUTE = 0.5, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) + maxHealth = 140 + health = 140 + damage_coeff = list(BRUTE = 0.9, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) melee_damage_lower = 24 melee_damage_upper = 48 - footstep_type = FOOTSTEP_MOB_HEAVY - + /mob/living/simple_animal/hostile/supermutant/rangedmutant/rain name = "super mutant rain cultist" desc = "A super mutant covered in blue markings that has been indoctrinated into the Cult Of Rain. This one wields a hunting rifle blessed by the rain gods." color = "#6B87C0" + color_rage = "#4488cc" speak_chance = 10 speak = list("The rain cleanses!", "Sacrifices for the rain gods!", "The thunder guides my fury!", "I am become the storm, destroyer of all heretics!", "The priests will be pleased with my sacrifices!") - maxHealth = 360 - health = 360 - damage_coeff = list(BRUTE = 1, BURN = 0.5, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) + maxHealth = 140 + health = 140 + damage_coeff = list(BRUTE = 1, BURN = 0.9, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) melee_damage_lower = 24 melee_damage_upper = 48 - footstep_type = FOOTSTEP_MOB_HEAVY +// Berserker nightkin - charges when shot, leaves destruction in its wake /mob/living/simple_animal/hostile/supermutant/nightkin/rain name = "nightkin berserker rain priest" desc = "A nightkin that spreads the word of the Cult Of Rain. They are covered in dark blue markings, indicating that they have been blessed by the rain god Odile." color = "#6666FF" + color_rage = "#3333ff" speak_chance = 10 speak = list("The rain speaks through me!", "Witness the gifts of rain!", "The great flood will come upon us! Do not fear it!", "My life for the rain gods!", "The rain gods can always use more sacrifices!") - maxHealth = 360 - health = 360 - damage_coeff = list(BRUTE = -0.1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) + maxHealth = 180 + health = 180 + damage_coeff = list(BRUTE = 0.8, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) melee_damage_lower = 24 melee_damage_upper = 48 var/charging = FALSE - footstep_type = FOOTSTEP_MOB_HEAVY + var/charge_cooldown = 0 + var/charge_cooldown_time = 8 SECONDS /mob/living/simple_animal/hostile/supermutant/nightkin/rain/bullet_act(obj/item/projectile/Proj) if(!Proj) return - if(prob(30)) + + if(!charging && world.time > charge_cooldown && prob(20)) visible_message(span_danger("\The [src] lets out a vicious war cry!")) - addtimer(3) - Charge() + addtimer(CALLBACK(src, PROC_REF(Charge)), 0.3 SECONDS) + if(prob(85) || Proj.damage > 30) return ..() - else - visible_message(span_danger("\The [Proj] is abosrbed by \the [src]'s thick skin, strengthening it!")) - return 0 + visible_message(span_danger("\The [Proj] is absorbed by \the [src]'s thick skin!")) + return 0 /mob/living/simple_animal/hostile/supermutant/nightkin/rain/do_attack_animation(atom/A, visual_effect_icon, obj/item/used_item, no_effect) if(!charging) - ..() + . = ..() /mob/living/simple_animal/hostile/supermutant/nightkin/rain/AttackingTarget() if(!charging) - return ..() + . = ..() +// Blocks pathfinding during charge throw so AI doesn't fight the physics /mob/living/simple_animal/hostile/supermutant/nightkin/rain/Goto(target, delay, minimum_distance) if(!charging) - ..() + . = ..() /mob/living/simple_animal/hostile/supermutant/nightkin/rain/Move() - if(charging) - new /obj/effect/temp_visual/decoy/fading(loc,src) + if(charging || (is_low_health && health > 0)) + new /obj/effect/temp_visual/decoy/fading(loc, src) DestroySurroundings() . = ..() - if(charging) - DestroySurroundings() /mob/living/simple_animal/hostile/supermutant/nightkin/rain/proc/Charge() + if(!target) + return + var/turf/T = get_turf(target) if(!T || T == loc) return + charging = TRUE - visible_message(span_danger(">[src] charges!")) + charge_cooldown = world.time + charge_cooldown_time + + visible_message(span_danger("[src] charges with terrifying fury!")) DestroySurroundings() walk(src, 0) setDir(get_dir(src, T)) - var/obj/effect/temp_visual/decoy/D = new /obj/effect/temp_visual/decoy(loc,src) - animate(D, alpha = 0, color = "#FF0000", transform = matrix()*2, time = 1) - addtimer(3) - throw_at(T, get_dist(src, T), 1, src, 0, callback = CALLBACK(src, PROC_REF(charge_end))) -/mob/living/simple_animal/hostile/supermutant/nightkin/rain/proc/charge_end(list/effects_to_destroy) + var/obj/effect/temp_visual/decoy/D = new /obj/effect/temp_visual/decoy(loc, src) + animate(D, alpha = 0, color = "#FF0000", transform = matrix() * 2, time = 0.1 SECONDS) + + throw_at(T, get_dist(src, T), 2, src, FALSE, callback = CALLBACK(src, PROC_REF(charge_end))) + +/mob/living/simple_animal/hostile/supermutant/nightkin/rain/proc/charge_end(atom/hit_atom) charging = FALSE if(target) Goto(target, move_to_delay, minimum_distance) /mob/living/simple_animal/hostile/supermutant/nightkin/rain/Bump(atom/A) if(charging) - if(isturf(A) || isobj(A) && A.density) + if((isturf(A) || isobj(A)) && A.density) A.ex_act(EXPLODE_HEAVY) + playsound(src, 'sound/effects/meteorimpact.ogg', 100, 1) DestroySurroundings() - ..() + . = ..() /mob/living/simple_animal/hostile/supermutant/nightkin/rain/throw_impact(atom/A) if(!charging) return ..() - else if(isliving(A)) + if(isliving(A)) var/mob/living/L = A L.visible_message(span_danger("[src] slams into [L]!"), span_userdanger("[src] slams into you!")) - L.apply_damage(melee_damage_lower/2, BRUTE) + L.apply_damage(melee_damage_upper, BRUTE) playsound(get_turf(L), 'sound/effects/meteorimpact.ogg', 100, 1) shake_camera(L, 4, 3) shake_camera(src, 2, 3) var/throwtarget = get_edge_target_turf(src, get_dir(src, get_step_away(L, src))) - L.throw_at(throwtarget, 3) - + L.throw_at(throwtarget, 3, 2) charging = FALSE - charging = FALSE - +// Guardian nightkin - ranged fire-release aura when hit /mob/living/simple_animal/hostile/supermutant/nightkin/rangedmutant/rain name = "nightkin guardian rain priest" desc = "A nightkin that spreads the word of the Cult Of Rain. They are covered in dark blue markings, indicating that they have been blessed by the rain god Ignacio." color = "#6666FF" + color_rage = "#3333ff" speak_chance = 10 speak = list("The rain speaks through me!", "Witness the gifts of rain!", "The great flood will come upon us! Do not fear it!", "My life for the rain gods!", "The rain gods can always use more sacrifices!") - maxHealth = 380 - health = 380 - damage_coeff = list(BRUTE = 1, BURN = -0.25, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) + maxHealth = 160 + health = 160 + damage_coeff = list(BRUTE = 1, BURN = 0.8, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) melee_damage_lower = 35 melee_damage_upper = 60 extra_projectiles = 2 - retreat_distance = 2 + + combat_mode = COMBAT_MODE_RANGED + retreat_distance = 4 minimum_distance = 4 /mob/living/simple_animal/hostile/supermutant/nightkin/rangedmutant/rain/Initialize(mapload) @@ -498,18 +586,16 @@ /mob/living/simple_animal/hostile/supermutant/nightkin/rangedmutant/rain/bullet_act(obj/item/projectile/Proj) if(!Proj) return - if(prob(20)) + if(prob(15)) visible_message(span_danger("\The [src] lets out a vicious war cry!")) fire_release() if(prob(85) || Proj.damage > 30) return ..() - else - visible_message(span_danger("\The [Proj] is absorbed by \the [src]'s thick skin, strengthening it!")) - return 0 + visible_message(span_danger("\The [Proj] is absorbed by \the [src]'s thick skin!")) + return 0 /mob/living/simple_animal/hostile/supermutant/nightkin/rangedmutant/rain/proc/fire_release() - playsound(get_turf(src),'sound/magic/fireball.ogg', 200, 1) - + playsound(get_turf(src), 'sound/magic/fireball.ogg', 200, 1) for(var/d in GLOB.cardinals) INVOKE_ASYNC(src, PROC_REF(fire_release_wall), d) @@ -518,7 +604,7 @@ var/turf/E = get_edge_target_turf(src, dir) var/range = 10 var/turf/previousturf = get_turf(src) - for(var/turf/J in getline(src,E)) + for(var/turf/J in getline(src, E)) if(!range || (J != previousturf && (!previousturf.atmos_adjacent_turfs || !previousturf.atmos_adjacent_turfs[J]))) break range-- @@ -531,22 +617,23 @@ to_chat(L, span_userdanger("You're hit by the nightkin's release of energy!")) hit_things += L previousturf = J - addtimer(1) + sleep(1) // FIX: was addtimer(1) with no callback - invalid call that would runtime +// Rain lord nightkin - most powerful, dual resistance, healing aura /mob/living/simple_animal/hostile/supermutant/nightkin/elitemutant/rain name = "nightkin rain lord" desc = "A nightkin that writes the word of the Cult Of Rain. They are covered in dark blue markings and are adorned in pieces of bone armor, indicating that they are blessed by the rain god Hyacinth." color = "#6666FF" + color_rage = "#3333ff" speak_chance = 10 speak = list("The great flood will come, I will make sure of it!", "Rain god Odile, I call upon you for wrath!", "Rain god Hyacinth, I call upon you for a tranquil mind!", "Rain god Ignacio, I call upon you for protection!", "The storm rages within!") - maxHealth = 440 - health = 440 - damage_coeff = list(BRUTE = 0.5, BURN = 0.5, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) + maxHealth = 200 + health = 200 + damage_coeff = list(BRUTE = 0.8, BURN = 0.8, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) melee_damage_lower = 28 melee_damage_upper = 62 extra_projectiles = 1 - /mob/living/simple_animal/hostile/supermutant/nightkin/elitemutant/rain/Initialize(mapload) . = ..() AddComponent(/datum/component/glow_heal, chosen_targets = /mob/living/simple_animal/hostile/supermutant, allow_revival = FALSE, restrict_faction = null, type_healing = BRUTELOSS | FIRELOSS) diff --git a/code/modules/mob/living/simple_animal/hostile/f13/tunnelers.dm b/code/modules/mob/living/simple_animal/hostile/f13/tunnelers.dm index 9850ddcb6c1..8abe376e4ae 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/tunnelers.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/tunnelers.dm @@ -1,82 +1,169 @@ +// In this document: Trogs, Spore Carriers, Tunnelers, Blind One + +///////////// +// TROGS // +///////////// + +// BASE TROG /mob/living/simple_animal/hostile/trog name = "trog" - desc = "A human who has mutated and regressed back to a primal, cannibalistic state. Rumor says they're poisonous as well. Want to find out? " + desc = "A human who has mutated and regressed back to a primal, cannibalistic state. Rumor says they're poisonous as well. Want to find out?" icon = 'icons/fallout/mobs/monsters/tunnelers.dmi' icon_state = "troglodyte" icon_living = "troglodyte" icon_dead = "trog_dead" - - speed = 2 + + mob_biotypes = MOB_ORGANIC|MOB_HUMANOID + maxHealth = 40 health = 40 - obj_damage = 30 + speed = 2 + move_to_delay = 3 + turns_per_move = 5 + melee_damage_lower = 5 - melee_damage_upper = 15 + melee_damage_upper = 12 harm_intent_damage = 5 + obj_damage = 30 + + robust_searching = TRUE + stat_attack = CONSCIOUS + waddle_amount = 2 waddle_up_time = 1 waddle_side_time = 1 - mob_biotypes = MOB_ORGANIC|MOB_HUMANOID - robust_searching = TRUE - turns_per_move = 5 + speak_emote = list("growls") emote_see = list("screeches") - a_intent = INTENT_HARM attack_verb_simple = "lunges at" attack_sound = 'sound/hallucinations/growl1.ogg' + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) unsuitable_atmos_damage = 20 - stat_attack = CONSCIOUS - gold_core_spawnable = HOSTILE_SPAWN + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/human = 2, + /obj/item/stack/sheet/animalhide/human = 1, + /obj/item/stack/sheet/bone = 1 + ) + faction = list("trog") - guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/human = 2, - /obj/item/stack/sheet/animalhide/human = 1, - /obj/item/stack/sheet/bone = 1) + a_intent = INTENT_HARM + gold_core_spawnable = HOSTILE_SPAWN + + can_ghost_into = TRUE + pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS + + // Z-movement + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 30 + can_open_doors = FALSE + can_open_airlocks = FALSE + + // Pure melee - swarm ambusher + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + +/mob/living/simple_animal/hostile/trog/Aggro() + . = ..() + if(.) + return + summon_backup(8) + +/mob/living/simple_animal/hostile/trog/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + +// SPORE CARRIER - fungal zombie /mob/living/simple_animal/hostile/trog/sporecarrier name = "spore carrier" - desc = "A victim of the beauveria mordicana fungus, these corpses sole purpose is to spread its spores." + desc = "A victim of the beauveria mordicana fungus. This corpse's sole purpose is to spread its spores." icon_state = "spore_carrier" icon_living = "spore_carrier" icon_dead = "spore_dead" - health = 80 + maxHealth = 80 - harm_intent_damage = 5 + health = 80 + melee_damage_lower = 10 melee_damage_upper = 25 - attack_sound = 'sound/hallucinations/growl1.ogg' + atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) unsuitable_atmos_damage = 0 + faction = list("plants") + guaranteed_butcher_results = list(/obj/item/stack/sheet/bone = 1) + + pop_required_to_jump_into = MED_MOB_MIN_PLAYERS + +/mob/living/simple_animal/hostile/trog/sporecarrier/AttackingTarget() + . = ..() + if(. && ishuman(target)) + var/mob/living/carbon/human/H = target + H.reagents.add_reagent(/datum/reagent/toxin/spore_toxin, 2) + +/mob/living/simple_animal/hostile/trog/sporecarrier/death(gibbed) + // Release spores on death + if(!gibbed) + visible_message(span_warning("[src]'s body ruptures, releasing a cloud of spores!")) + var/datum/effect_system/smoke_spread/chem/S = new + S.set_up(1, get_turf(src)) + S.start() + . = ..() +/////////////// +// TUNNELERS // +/////////////// + +// BASE TUNNELER - deadly swarm creature /mob/living/simple_animal/hostile/trog/tunneler name = "tunneler" desc = "A mutated creature that is sensitive to light, but can swarm and kill even Deathclaws." icon_state = "tunneler" icon_living = "tunneler" icon_dead = "tunneler_dead" - robust_searching = TRUE - stat_attack = CONSCIOUS - health = 144 + + mob_armor = ARMOR_VALUE_TUNNELER + maxHealth = 144 + health = 144 speed = 1 + melee_damage_lower = 18 melee_damage_upper = 32 armour_penetration = 0.25 obj_damage = 150 + see_in_dark = 8 + attack_sound = 'sound/weapons/bladeslice.ogg' - atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) - damage_coeff = list(BRUTE = 1, BURN = 1, TOX = -1, CLONE = 0, STAMINA = 0, OXY = 0) - unsuitable_atmos_damage = 5 - faction = list("tunneler") - guaranteed_butcher_results = list(/obj/item/stack/sheet/bone = 1) death_sound = 'sound/f13npc/ghoul/ghoul_death.ogg' + + damage_coeff = list(BRUTE = 1, BURN = 1, TOX = -1, CLONE = 0, STAMINA = 0, OXY = 0) // Immune to toxins + + faction = list("tunneler") + + guaranteed_butcher_results = list( + /obj/item/stack/sheet/bone = 1, + /obj/item/stack/sheet/sinew = 1 + ) + + pop_required_to_jump_into = MED_MOB_MIN_PLAYERS /mob/living/simple_animal/hostile/trog/tunneler/Aggro() - ..() - summon_backup(15) + . = ..() + if(.) + return + summon_backup(10) /mob/living/simple_animal/hostile/trog/tunneler/AttackingTarget() . = ..() @@ -84,46 +171,53 @@ var/mob/living/carbon/human/H = target H.reagents.add_reagent(/datum/reagent/toxin, 5) - +// BLIND ONE - boss tunneler /mob/living/simple_animal/hostile/trog/tunneler/blindone name = "Blind One" desc = "A...tunneler? Her scales reflect the light oddly, almost making her transparent, but her eyes are solid. She moves blindingly quickly, darting in and out of view despite her size. Overfilled, swelling venom-sacs line her throat." - icon = 'icons/fallout/mobs/monsters/tunnelers.dmi' icon_state = "blindone" icon_living = "blindone" icon_dead = "trog_dead" + mob_armor = ARMOR_VALUE_DEATHCLAW_MOTHER + gender = FEMALE - resize = 1.3 - alpha = 150 - speed = 1 + alpha = 150 // Semi-transparent + maxHealth = 150 health = 150 - has_field_of_vision = FALSE - obj_damage = 30 + speed = 1 + melee_damage_lower = 18 melee_damage_upper = 40 + obj_damage = 30 // Reduced from 150 + vision_range = 9 aggro_vision_range = 18 - retreat_distance = 6 - turns_per_move = 5 + has_field_of_vision = FALSE // 360 degree vision + speak_emote = list("mumbles incoherently") - emote_see = list("screeches") - a_intent = INTENT_HARM - attack_verb_simple = "lunges at" attack_sound = 'sound/hallucinations/veryfar_noise.ogg' - atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) - unsuitable_atmos_damage = 20 - stat_attack = CONSCIOUS - gold_core_spawnable = HOSTILE_SPAWN - faction = list("tunneler") - guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/human = 2, - /obj/item/stack/sheet/animalhide/human = 1, - /obj/item/stack/sheet/bone = 1) + + pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS + despawns_when_lonely = FALSE + + // Rage mode at low health + low_health_threshold = 0.3 + var/color_rage = "#FF3333" + +/mob/living/simple_animal/hostile/trog/tunneler/blindone/Initialize() + . = ..() + resize = 1.3 + update_transform() /mob/living/simple_animal/hostile/trog/tunneler/blindone/Aggro() - ..() - summon_backup(15) + . = ..() + if(.) + return + summon_backup(12) + if(!ckey) + visible_message(span_danger("[src] lets out an otherworldly screech!")) /mob/living/simple_animal/hostile/trog/tunneler/blindone/AttackingTarget() . = ..() @@ -133,3 +227,64 @@ H.reagents.add_reagent(/datum/reagent/toxin/venom, 5) H.reagents.add_reagent(/datum/reagent/toxin/mindbreaker, 3) +// RAGE MODE - Blind One becomes faster and more aggressive +/mob/living/simple_animal/hostile/trog/tunneler/blindone/make_low_health() + visible_message(span_danger("[src] writhes in pain, moving with renewed fury!")) + playsound(src, 'sound/hallucinations/veryfar_noise.ogg', 100, 1) + alpha = 100 // More transparent + speed *= 0.75 // Faster + melee_damage_lower = round(melee_damage_lower * 1.3) + melee_damage_upper = round(melee_damage_upper * 1.3) + armour_penetration = 0.35 // Increased from 0.25 + is_low_health = TRUE + +/mob/living/simple_animal/hostile/trog/tunneler/blindone/make_high_health() + visible_message(span_notice("[src]'s movements slow slightly.")) + alpha = initial(alpha) + speed = initial(speed) + melee_damage_lower = initial(melee_damage_lower) + melee_damage_upper = initial(melee_damage_upper) + armour_penetration = initial(armour_penetration) + is_low_health = FALSE + +// TUNNELER SWARM - weak but numerous +/mob/living/simple_animal/hostile/trog/tunneler/swarm + name = "tunneler spawn" + desc = "A young tunneler, still developing its deadly capabilities." + + maxHealth = 60 + health = 60 + speed = 0.8 // Faster + + melee_damage_lower = 10 + melee_damage_upper = 18 + armour_penetration = 0.1 + obj_damage = 50 + + pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS + +/mob/living/simple_animal/hostile/trog/tunneler/swarm/Initialize() + . = ..() + resize = 0.7 + update_transform() + +/mob/living/simple_animal/hostile/trog/tunneler/swarm/Aggro() + . = ..() + if(.) + return + summon_backup(15) // Call for MORE help + +// REAGENTS +/datum/reagent/toxin/spore_toxin + name = "Fungal Spores" + description = "Airborne spores from the beauveria mordicana fungus." + color = "#228B22" + toxpwr = 0.5 + taste_description = "decay" + +/datum/reagent/toxin/spore_toxin/on_mob_life(mob/living/M) + if(volume >= 10) + M.adjustToxLoss(2, 0) + if(prob(5)) + to_chat(M, span_warning("You feel strange spores growing in your lungs...")) + ..() diff --git a/code/modules/mob/living/simple_animal/hostile/f13/wasteanimals.dm b/code/modules/mob/living/simple_animal/hostile/f13/wasteanimals.dm index 31a19b5f4c1..c591263ec7f 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/wasteanimals.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/wasteanimals.dm @@ -2,6 +2,7 @@ // GECKO // /////////// +// BASE GECKO - all shared properties /mob/living/simple_animal/hostile/gecko name = "gecko" desc = "A large mutated reptile with sharp teeth." @@ -9,450 +10,177 @@ icon_state = "gekko" icon_living = "gekko" icon_dead = "gekko_dead" + mob_biotypes = MOB_ORGANIC|MOB_BEAST - speak_chance = 0 - turns_per_move = 5 - guaranteed_butcher_results = list( - /obj/item/reagent_containers/food/snacks/meat/slab/gecko = 2, - /obj/item/stack/sheet/animalhide/gecko = 1) - butcher_results = list(/obj/item/stack/sheet/bone = 1) - butcher_difficulty = 1 - response_help_simple = "pets" - response_disarm_simple = "gently pushes aside" - response_harm_simple = "hits" - taunt_chance = 30 - speed = 0 + maxHealth = 35 health = 35 - harm_intent_damage = 8 - obj_damage = 20 + speed = 0 + move_to_delay = 2.5 + turns_per_move = 5 + melee_damage_lower = 4 melee_damage_upper = 12 - move_to_delay = 1.5 - retreat_distance = 0 - minimum_distance = 0 + harm_intent_damage = 8 + obj_damage = 20 + aggro_vision_range = 7 vision_range = 8 + + // Reptilian eyes adapted to wasteland conditions + has_low_light_vision = TRUE + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/gecko = 2, + /obj/item/stack/sheet/animalhide/gecko = 1, + /obj/item/stack/sheet/bone = 1 + ) + butcher_difficulty = 1 + waddle_amount = 3 waddle_up_time = 1 waddle_side_time = 2 pass_flags = PASSTABLE - speak_emote = list( - "squeaks", - "cackles", - "snickers", - "shriek", - "scream", - "skrem", - "scrambles", - "warbles", - "chirps", - "cries", - "kyaas", - "chortles", - "gecks" - ) - emote_see = list( - "screeches", - "licks its eyes", - "twitches", - "scratches its frills", - "gonks", - "honks", - "scronks", - "sniffs", - "gecks" - ) - attack_verb_simple = list( - "bites", - "claws", - "tears at", - "dabs", - "scratches", - "gnaws", - "chews", - "chomps", - "lunges", - "gecks" - ) - atmos_requirements = list( - "min_oxy" = 5, - "max_oxy" = 0, - "min_tox" = 0, - "max_tox" = 1, - "min_co2" = 0, - "max_co2" = 5, - "min_n2" = 0, - "max_n2" = 0 - ) - faction = list("gecko", "critter-friend") // critter-friend is a flag for related beast friend/master quirk. Makes hostile mob passive for quirk holder. + + response_help_simple = "pets" + response_disarm_simple = "gently pushes aside" + response_harm_simple = "hits" + + speak_emote = list("squeaks", "cackles", "snickers", "shrieks", "screams", "warbles", "chirps", "cries", "kyaas", "chortles", "gecks") + emote_see = list("screeches", "licks its eyes", "twitches", "scratches its frills", "gonks", "honks", "sniffs", "gecks") + attack_verb_simple = list("bites", "claws", "tears at", "scratches", "gnaws", "chews", "chomps", "lunges", "gecks") + + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) + + faction = list("gecko", "critter-friend") a_intent = INTENT_HARM gold_core_spawnable = HOSTILE_SPAWN footstep_type = FOOTSTEP_MOB_CLAW - idlesound = list( - 'sound/f13npc/gecko/geckocall1.ogg', - 'sound/f13npc/gecko/geckocall2.ogg', - 'sound/f13npc/gecko/geckocall3.ogg', - 'sound/f13npc/gecko/geckocall4.ogg', - 'sound/f13npc/gecko/geckocall5.ogg' - ) - + emote_taunt = list("screeches") - emote_taunt_sound = list( - 'sound/f13npc/gecko/gecko_charge1.ogg', - 'sound/f13npc/gecko/gecko_charge2.ogg', - 'sound/f13npc/gecko/gecko_charge3.ogg' - ) + emote_taunt_sound = list('sound/f13npc/gecko/gecko_charge1.ogg', 'sound/f13npc/gecko/gecko_charge2.ogg', 'sound/f13npc/gecko/gecko_charge3.ogg') + taunt_chance = 30 aggrosound = list('sound/f13npc/gecko/gecko_alert.ogg') + idlesound = list('sound/f13npc/gecko/geckocall1.ogg', 'sound/f13npc/gecko/geckocall2.ogg', 'sound/f13npc/gecko/geckocall3.ogg', 'sound/f13npc/gecko/geckocall4.ogg', 'sound/f13npc/gecko/geckocall5.ogg') death_sound = 'sound/f13npc/gecko/gecko_death.ogg' - can_ghost_into = TRUE // not a bad idea at all + + can_ghost_into = TRUE desc_short = "Short, angry, and as confused as they are tasty." - desc_important = "Still in development! Report wierdness on the discord!" - + pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS + + // Z-movement + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 40 + + can_open_doors = FALSE + can_open_airlocks = FALSE + + // Pure melee + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + variation_list = list( MOB_COLOR_VARIATION(50, 50, 50, 255, 255, 255), MOB_SPEED_LIST(1.5, 1.8, 2.0, 2.2, 2.6, 3.0, 3.3, 3.7), MOB_SPEED_CHANGE_PER_TURN_CHANCE(50), MOB_HEALTH_LIST(30, 35, 40, 45), - MOB_RETREAT_DISTANCE_LIST(0, 1, 3, 5, 7, 9), - MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(100), - MOB_MINIMUM_DISTANCE_LIST(0, 0, 4, 6), - MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(100), ) +/mob/living/simple_animal/hostile/gecko/Aggro() + . = ..() + if(.) + return + summon_backup(10) + +/mob/living/simple_animal/hostile/gecko/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + /mob/living/simple_animal/hostile/gecko/become_the_mob(mob/user) make_a_nest = /obj/effect/proc_holder/mob_common/make_nest/gecko call_backup = /obj/effect/proc_holder/mob_common/summon_backup/small_critter send_mobs = /obj/effect/proc_holder/mob_common/direct_mobs/small_critter . = ..() -//Fire Geckos// - +// FIRE GECKO - ranged variant that spits fire /mob/living/simple_animal/hostile/gecko/fire - name = "fire spitter gecko" + name = "fire gecko" desc = "A large mutated reptile with sharp teeth and a warm disposition. Sorta smells like sulphur." - icon = 'icons/fallout/mobs/animals/wasteanimals.dmi' - icon_state = "gekko" - icon_living = "gekko" - icon_dead = "gekko_dead" - mob_biotypes = MOB_ORGANIC|MOB_BEAST - speak_chance = 0 - turns_per_move = 5 - guaranteed_butcher_results = list( - /obj/item/reagent_containers/food/snacks/meat/slab/gecko = 2, - /obj/item/stack/sheet/animalhide/gecko = 1) - butcher_results = list(/obj/item/stack/sheet/bone = 1) - butcher_difficulty = 1 - response_help_simple = "pets" - response_disarm_simple = "gently pushes aside" - response_harm_simple = "hits" - taunt_chance = 30 - speed = 0 - maxHealth = 35 - health = 35 - harm_intent_damage = 8 - obj_damage = 20 - melee_damage_lower = 4 - melee_damage_upper = 12 - move_to_delay = 1.5 - retreat_distance = 0 - minimum_distance = 0 - aggro_vision_range = 7 - vision_range = 8 - waddle_amount = 3 - waddle_up_time = 1 - waddle_side_time = 2 - pass_flags = PASSTABLE - speak_emote = list( - "squeaks", - "cackles", - "snickers", - "shriek", - "scream", - "skrem", - "scrambles", - "warbles", - "chirps", - "cries", - "kyaas", - "chortles", - "gecks" - ) - emote_see = list( - "screeches", - "licks its eyes", - "twitches", - "scratches its frills", - "gonks", - "honks", - "scronks", - "sniffs", - "gecks" - ) - attack_verb_simple = list( - "bites", - "claws", - "tears at", - "dabs", - "scratches", - "gnaws", - "chews", - "chomps", - "lunges", - "gecks" - ) - atmos_requirements = list( - "min_oxy" = 5, - "max_oxy" = 0, - "min_tox" = 0, - "max_tox" = 1, - "min_co2" = 0, - "max_co2" = 5, - "min_n2" = 0, - "max_n2" = 0 - ) - a_intent = INTENT_HARM - gold_core_spawnable = HOSTILE_SPAWN - footstep_type = FOOTSTEP_MOB_CLAW + + maxHealth = 30 + health = 30 + + // Mixed combat - fire spit + melee + combat_mode = COMBAT_MODE_MIXED ranged = TRUE + retreat_distance = 3 + minimum_distance = 1 + check_friendly_fire = TRUE + ranged_cooldown_time = 3 SECONDS projectiletype = /obj/item/projectile/geckofire projectilesound = 'sound/magic/fireball.ogg' - - emote_taunt = list("screeches") - emote_taunt_sound = list( - 'sound/f13npc/gecko/gecko_charge1.ogg', - 'sound/f13npc/gecko/gecko_charge2.ogg', - 'sound/f13npc/gecko/gecko_charge3.ogg' - ) - aggrosound = list('sound/f13npc/gecko/gecko_alert.ogg') - death_sound = 'sound/f13npc/gecko/gecko_death.ogg' - can_ghost_into = TRUE // not a bad idea at all - desc_short = "Short, angry, and as confused as they are tasty." - desc_important = "Still in development! Report wierdness on the discord!" - + ranged_message = "spits fire" + variation_list = list( MOB_COLOR_VARIATION(200, 40, 40, 255, 45, 45), MOB_SPEED_LIST(2.6, 3.0, 3.3, 3.7), MOB_SPEED_CHANGE_PER_TURN_CHANCE(50), MOB_HEALTH_LIST(28, 30, 32), - MOB_RETREAT_DISTANCE_LIST(0, 1, 3), - MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(100), - MOB_MINIMUM_DISTANCE_LIST(1, 2, 3), - MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(100), ) /mob/living/simple_animal/hostile/gecko/fire/Initialize() - .=..() + . = ..() resize = 0.8 update_transform() - -/* firey gecko spit - * DAMAGE: 5 - * STAMIN: 5 - * RECOIL: 2 - * WOUNDS: 0 - * WNAKED: 0 - */ -/obj/item/projectile/geckofire - name = "flaming gecko spit" - icon = 'icons/effects/fire.dmi' - icon_state = "3" - range = 4 - light_range = LIGHT_RANGE_FIRE - light_color = LIGHT_COLOR_FIRE - damage = BULLET_DAMAGE_SHOTGUN_PELLET * BULLET_DAMAGE_FIRE - stamina = BULLET_STAMINA_SHOTGUN_PELLET * BULLET_STAMINA_FIRE - spread = BULLET_SPREAD_SURPLUS - recoil = BULLET_RECOIL_SHOTGUN_PELLET - - wound_bonus = BULLET_WOUND_SHOTGUN_PELLET * BULLET_WOUND_FIRE - bare_wound_bonus = BULLET_WOUND_SHOTGUN_PELLET_NAKED_MULT * BULLET_NAKED_WOUND_FIRE - wound_falloff_tile = BULLET_WOUND_FALLOFF_PISTOL_LIGHT - - pixels_per_second = BULLET_SPEED_SHOTGUN_PELLET * 0.35 - damage_falloff = BULLET_FALLOFF_DEFAULT_PISTOL_LIGHT - - sharpness = SHARP_NONE - zone_accuracy_type = ZONE_WEIGHT_SHOTGUN - -/obj/item/projectile/geckofire/on_hit(atom/target) - . = ..() - if(prob(1)) - name = "flaming gecko yartz" - if(iscarbon(target)) - var/mob/living/carbon/M = target - M.adjust_fire_stacks(2) - if(M.fire_stacks > 2) - M.IgniteMob() - - -//Smaller Legacy Geckos// -//Faster and more aggressive than normal geckos, but also easier even squishier. - +// LEGACY GECKO (NEWT) - smaller, faster, aggressive /mob/living/simple_animal/hostile/gecko/legacy name = "newt" - desc = "A large dog sized amphibious biped with an oddly large mouth for its size. Probably related to geckos in some way." + desc = "A large dog-sized amphibious biped with an oddly large mouth for its size. Probably related to geckos in some way." icon = 'icons/fallout/mobs/legacymobs.dmi' icon_state = "legacy_gecko" icon_living = "legacy_gecko" icon_dead = "legacy_gecko_dead" - mob_biotypes = MOB_ORGANIC|MOB_BEAST - speak_chance = 0 - turns_per_move = 5 + sidestep_per_cycle = 2 - guaranteed_butcher_results = list( - /obj/item/reagent_containers/food/snacks/meat/slab/gecko = 2, - /obj/item/stack/sheet/animalhide/gecko = 1) - butcher_results = list(/obj/item/stack/sheet/bone = 1) - butcher_difficulty = 1 - response_help_simple = "pets" - response_disarm_simple = "gently pushes aside" - response_harm_simple = "hits" - taunt_chance = 30 - speed = 0 - maxHealth = 35 - health = 35 - harm_intent_damage = 8 - obj_damage = 20 + dodging = TRUE + melee_damage_lower = 7 melee_damage_upper = 18 - move_to_delay = 1.5 - retreat_distance = 0 - minimum_distance = 0 - aggro_vision_range = 7 - vision_range = 7 + waddle_amount = 5 - waddle_up_time = 1 - waddle_side_time = 1 - pass_flags = PASSTABLE - speak_emote = list( - "squeaks", - "cackles", - "snickers", - "shriek", - "scream", - "skrem", - "scrambles", - "warbles", - "chirps", - "cries", - "kyaas", - "chortles", - "gecks" - ) - emote_see = list( - "screeches", - "licks its eyes", - "twitches", - "scratches its frills", - "gonks", - "honks", - "scronks", - "sniffs", - "gecks" - ) - attack_verb_simple = list( - "bites", - "claws", - "tears at", - "dabs", - "scratches", - "gnaws", - "chews", - "chomps", - "lunges", - "gecks" - ) - atmos_requirements = list( - "min_oxy" = 5, - "max_oxy" = 0, - "min_tox" = 0, - "max_tox" = 1, - "min_co2" = 0, - "max_co2" = 5, - "min_n2" = 0, - "max_n2" = 0 - ) - a_intent = INTENT_HARM - gold_core_spawnable = HOSTILE_SPAWN - footstep_type = FOOTSTEP_MOB_CLAW - sound_pitch = 70 vary_pitches = list(40, 80) - emote_taunt = list("screeches") - emote_taunt_sound = list( - 'sound/f13npc/gecko/gecko_charge1.ogg', - 'sound/f13npc/gecko/gecko_charge2.ogg', - 'sound/f13npc/gecko/gecko_charge3.ogg' - ) - aggrosound = list('sound/f13npc/gecko/gecko_alert.ogg') - death_sound = 'sound/f13npc/gecko/gecko_death.ogg' - can_ghost_into = TRUE // not a bad idea at all - desc_short = "Short, angry, and as confused as they are tasty." - desc_important = "Still in development! Report wierdness on the discord!" - - + +// ALPHA NEWT - stronger variant with stamina damage /mob/living/simple_animal/hostile/gecko/legacy/alpha name = "alpha newt" - desc = "A large dog sized amphibious biped with an oddly large mouth for its size. Probably related to geckos in some way. This one's drooling a lot and looks sort of tired." - icon = 'icons/fallout/mobs/legacymobs.dmi' + desc = "A large dog-sized amphibious biped with an oddly large mouth for its size. This one's drooling a lot and looks sort of tired." icon_state = "legacy_gecko2" icon_living = "legacy_gecko2" - icon_dead = "legacy_gecko_dead" - mob_biotypes = MOB_ORGANIC|MOB_BEAST - speak_chance = 0 - turns_per_move = 5 + + vision_range = 9 + guaranteed_butcher_results = list( /obj/item/reagent_containers/food/snacks/meat/slab/gecko = 3, - /obj/item/stack/sheet/animalhide/gecko = 1) - butcher_results = list(/obj/item/stack/sheet/bone = 1) - butcher_difficulty = 1 - response_help_simple = "pets" - response_disarm_simple = "gently pushes aside" - response_harm_simple = "hits" - taunt_chance = 30 - speed = 0 - maxHealth = 35 - health = 35 - harm_intent_damage = 8 - obj_damage = 20 - melee_damage_lower = 7 - melee_damage_upper = 18 - move_to_delay = 1.5 - retreat_distance = 0 - minimum_distance = 0 - aggro_vision_range = 7 - vision_range = 9 + /obj/item/stack/sheet/animalhide/gecko = 1, + /obj/item/stack/sheet/bone = 1 + ) - faction = list("gecko") - a_intent = INTENT_HARM - gold_core_spawnable = HOSTILE_SPAWN - footstep_type = FOOTSTEP_MOB_CLAW - - emote_taunt = list("screeches") - emote_taunt_sound = list( - 'sound/f13npc/gecko/gecko_charge1.ogg', - 'sound/f13npc/gecko/gecko_charge2.ogg', - 'sound/f13npc/gecko/gecko_charge3.ogg' - ) - aggrosound = list('sound/f13npc/gecko/gecko_alert.ogg') - death_sound = 'sound/f13npc/gecko/gecko_death.ogg' - can_ghost_into = TRUE // not a bad idea at all - desc_short = "Short, angry, and as confused as they are tasty." - desc_important = "Still in development! Report wierdness on the discord!" - variation_list = list( - MOB_COLOR_VARIATION(180, 255, 255, 255, 255, 255), //Rmin, Gmin, Bmin, Rmax, Gmax, Bmax + MOB_COLOR_VARIATION(180, 255, 255, 255, 255, 255), MOB_SPEED_LIST(1.8, 2.0, 2.2), MOB_SPEED_CHANGE_PER_TURN_CHANCE(80), MOB_HEALTH_LIST(30, 35, 40), - MOB_RETREAT_DISTANCE_LIST(0, 1), - MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(50), - MOB_MINIMUM_DISTANCE_LIST(1, 2), - MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(50), - ) + ) /mob/living/simple_animal/hostile/gecko/legacy/alpha/AttackingTarget() . = ..() @@ -460,136 +188,118 @@ var/mob/living/carbon/human/H = target H.reagents.add_reagent(/datum/reagent/toxin/staminatoxin, 1) +// BIG GECKO - slow, heavy hitter, poor vision /mob/living/simple_animal/hostile/gecko/big - name = "big gecko" name = "big gecko" desc = "A large mutated reptile with sharp teeth. This one's pretty big, but its eyes seem clouded and it moves a bit clumsily." - icon = 'icons/fallout/mobs/animals/wasteanimals.dmi' - icon_state = "gekko" - icon_living = "gekko" - icon_dead = "gekko_dead" - mob_biotypes = MOB_ORGANIC|MOB_BEAST - speak_chance = 0 - turns_per_move = 5 - guaranteed_butcher_results = list( - /obj/item/reagent_containers/food/snacks/meat/slab/gecko = 6, - /obj/item/stack/sheet/animalhide/gecko = 2) - butcher_results = list(/obj/item/stack/sheet/bone = 2) - butcher_difficulty = 1 - response_help_simple = "pets" - response_disarm_simple = "gently pushes aside" - response_harm_simple = "hits" - taunt_chance = 30 - speed = 0 - maxHealth = 35 - health = 35 - harm_intent_damage = 8 - obj_damage = 20 + + maxHealth = 110 + health = 110 + melee_damage_lower = 12 melee_damage_upper = 24 - move_to_delay = 1.5 - retreat_distance = 0 - minimum_distance = 0 + aggro_vision_range = 4 vision_range = 4 - - - faction = list("gecko") - a_intent = INTENT_HARM - gold_core_spawnable = HOSTILE_SPAWN + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/gecko = 6, + /obj/item/stack/sheet/animalhide/gecko = 2, + /obj/item/stack/sheet/bone = 2 + ) + footstep_type = FOOTSTEP_MOB_HEAVY - sound_pitch = -75 vary_pitches = list(-100, -80) - emote_taunt = list("screeches") - emote_taunt_sound = list( - 'sound/f13npc/gecko/gecko_charge1.ogg', - 'sound/f13npc/gecko/gecko_charge2.ogg', - 'sound/f13npc/gecko/gecko_charge3.ogg' - ) - aggrosound = list('sound/f13npc/gecko/gecko_alert.ogg') - death_sound = 'sound/f13npc/gecko/gecko_death.ogg' - can_ghost_into = TRUE // not a bad idea at all - desc_short = "Short, angry, and as confused as they are tasty." - desc_important = "Still in development! Report wierdness on the discord!" - + variation_list = list( - MOB_COLOR_VARIATION(120, 80, 80, 250, 100, 100), //Rmin, Gmin, Bmin, Rmax, Gmax, Bmax + MOB_COLOR_VARIATION(120, 80, 80, 250, 100, 100), MOB_SPEED_LIST(2.5, 2.8, 3.0), MOB_SPEED_CHANGE_PER_TURN_CHANCE(80), MOB_HEALTH_LIST(100, 110, 120), - MOB_RETREAT_DISTANCE_LIST(0, 1), - MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(50), - MOB_MINIMUM_DISTANCE_LIST(1, 2), - MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(50), ) /mob/living/simple_animal/hostile/gecko/big/Initialize() - .=..() + . = ..() resize = 1.5 update_transform() +// PLAYABLE GECKO - for ghost roles /mob/living/simple_animal/hostile/gecko/playable - health = 40 maxHealth = 40 - speed = 0 + health = 40 + melee_damage_lower = 8 + melee_damage_upper = 12 + emote_taunt_sound = null emote_taunt = null aggrosound = null idlesound = null see_in_dark = 8 wander = 0 - anchored = FALSE - melee_damage_lower = 8 - melee_damage_upper = 12 - footstep_type = FOOTSTEP_MOB_CLAW -/// Testing its randomness +// DEBUG GECKOS - for testing /mob/living/simple_animal/hostile/gecko/debug sound_pitch = 100 vary_pitches = list(-200, 200) variation_list = list( - MOB_NAME_FROM_GLOBAL_LIST(\ - MOB_RANDOM_NAME(MOB_NAME_RANDOM_MALE, 2),\ - MOB_RANDOM_NAME(MOB_NAME_RANDOM_LIZARD_FEMALE, 1),\ - MOB_RANDOM_NAME(MOB_NAME_RANDOM_ALL_OF_THEM, 5)\ + "varied_global_names" = list( + MOB_RANDOM_NAME(MOB_NAME_RANDOM_MALE, 2), + MOB_RANDOM_NAME(MOB_NAME_RANDOM_LIZARD_FEMALE, 1), + MOB_RANDOM_NAME(MOB_NAME_RANDOM_ALL_OF_THEM, 5) ), MOB_COLOR_VARIATION(20, 190, 0, 255, 2, 0), MOB_SPEED_LIST(1.5, 1.8, 2.0, 2.2, 2.6, 3.0, 3.3, 3.7), MOB_SPEED_CHANGE_PER_TURN_CHANCE(100), - MOB_HEALTH_LIST(2, 3, 5, 7, 30, 35, 37, 38, 40, 45, 48, 49, 49, 49, 49, 2000), - MOB_RETREAT_DISTANCE_LIST(0, 1, 3, 5, 7, 9), - MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(100), - MOB_MINIMUM_DISTANCE_LIST(0, 0, 4, 6), - MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(100), + MOB_HEALTH_LIST(2, 3, 5, 7, 30, 35, 37, 38, 40, 45, 48, 49, 2000), ) -/// Testing its randomness /mob/living/simple_animal/hostile/gecko/debug/stamcrit variation_list = list( MOB_NAME_FROM_GLOBAL_LIST(MOB_RANDOM_NAME(MOB_NAME_RANDOM_LIZARD_FEMALE, 1)), MOB_HEALTH_LIST(50), - MOB_RETREAT_DISTANCE_LIST(4), - MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(100), - MOB_MINIMUM_DISTANCE_LIST(2), - MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(100), ) -/// Testing its randomness /mob/living/simple_animal/hostile/gecko/debug/stamcrit/Initialize() . = ..() new /obj/item/gun/energy/disabler/debug(get_turf(src)) -/mob/living/simple_animal/hostile/gecko/Aggro() +// GECKO FIRE PROJECTILE +/obj/item/projectile/geckofire + name = "flaming gecko spit" + icon = 'icons/effects/fire.dmi' + icon_state = "3" + range = 4 + light_range = LIGHT_RANGE_FIRE + light_color = LIGHT_COLOR_FIRE + + damage = BULLET_DAMAGE_SHOTGUN_PELLET * BULLET_DAMAGE_FIRE + stamina = BULLET_STAMINA_SHOTGUN_PELLET * BULLET_STAMINA_FIRE + spread = BULLET_SPREAD_SURPLUS + recoil = BULLET_RECOIL_SHOTGUN_PELLET + wound_bonus = BULLET_WOUND_SHOTGUN_PELLET * BULLET_WOUND_FIRE + bare_wound_bonus = BULLET_WOUND_SHOTGUN_PELLET_NAKED_MULT * BULLET_NAKED_WOUND_FIRE + wound_falloff_tile = BULLET_WOUND_FALLOFF_PISTOL_LIGHT + pixels_per_second = BULLET_SPEED_SHOTGUN_PELLET * 0.35 + damage_falloff = BULLET_FALLOFF_DEFAULT_PISTOL_LIGHT + sharpness = SHARP_NONE + zone_accuracy_type = ZONE_WEIGHT_SHOTGUN + +/obj/item/projectile/geckofire/on_hit(atom/target) . = ..() - if(.) - return - summon_backup(15) + if(prob(1)) + name = "flaming gecko yartz" + if(iscarbon(target)) + var/mob/living/carbon/M = target + M.adjust_fire_stacks(2) + if(M.fire_stacks > 2) + M.IgniteMob() ////////////////////////// // NIGHTSTALKERS & PELT // ////////////////////////// +// BASE NIGHTSTALKER /mob/living/simple_animal/hostile/stalker name = "nightstalker" desc = "A crazed genetic hybrid of rattlesnake and coyote DNA." @@ -598,80 +308,118 @@ icon_living = "nightstalker" icon_dead = "nightstalker-dead" icon_gib = null + mob_biotypes = MOB_ORGANIC|MOB_BEAST - speak_chance = 0 + + maxHealth = 80 + health = 80 + speed = 1 + move_to_delay = 2.5 turns_per_move = 5 - move_to_delay = 2 - // m2d 3 = standard, less is fast, more is slower. - - retreat_distance = 0 - //how far they pull back - minimum_distance = 0 - // how close you can get before they try to pull back - + melee_damage_lower = 4 + melee_damage_upper = 12 + harm_intent_damage = 8 + obj_damage = 15 + aggro_vision_range = 7 - //tiles within they start attacking, doesn't count the mobs tile - vision_range = 8 - //tiles within they start making noise, does count the mobs tile - + + // Nightstalkers are specifically adapted for nocturnal hunting + has_low_light_vision = TRUE + low_light_bonus = 4 + guaranteed_butcher_results = list( /obj/item/reagent_containers/food/snacks/meat/slab/nightstalker_meat = 2, /obj/item/stack/sheet/sinew = 2, /obj/item/stack/sheet/bone = 2 - ) + ) butcher_results = list( /obj/item/clothing/head/f13/stalkerpelt = 1, /obj/item/reagent_containers/food/snacks/meat/slab/nightstalker_meat = 1 - ) + ) butcher_difficulty = 3 + + waddle_amount = 3 + waddle_up_time = 1 + waddle_side_time = 1 + response_help_simple = "pets" response_disarm_simple = "gently pushes aside" response_harm_simple = "bites" - emote_taunt = list("growls") - taunt_chance = 30 - speed = 1 - maxHealth = 80 - health = 80 - harm_intent_damage = 8 - obj_damage = 15 - melee_damage_lower = 4 - melee_damage_upper = 12 + + speak_emote = list("growls") attack_verb_simple = "bites" attack_sound = 'sound/creatures/nightstalker_bite.ogg' - speak_emote = list("growls") + + emote_taunt = list("growls") + emote_taunt_sound = list('sound/f13npc/nightstalker/taunt1.ogg', 'sound/f13npc/nightstalker/taunt2.ogg') + taunt_chance = 30 + aggrosound = list('sound/f13npc/nightstalker/aggro1.ogg', 'sound/f13npc/nightstalker/aggro2.ogg', 'sound/f13npc/nightstalker/aggro3.ogg') + idlesound = list('sound/f13npc/nightstalker/idle1.ogg') + death_sound = 'sound/f13npc/nightstalker/death.ogg' + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) + faction = list("nightstalkers") - gold_core_spawnable = HOSTILE_SPAWN a_intent = INTENT_HARM + gold_core_spawnable = HOSTILE_SPAWN footstep_type = FOOTSTEP_MOB_CLAW - waddle_amount = 3 - waddle_up_time = 1 - waddle_side_time = 1 + + can_ghost_into = TRUE + pop_required_to_jump_into = MED_MOB_MIN_PLAYERS + + // Z-movement + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 40 + + can_open_doors = FALSE + can_open_airlocks = FALSE + + // Pure melee - venomous bite + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + +/mob/living/simple_animal/hostile/stalker/Aggro() + . = ..() + if(.) + return + summon_backup(10) + +/mob/living/simple_animal/hostile/stalker/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + +/mob/living/simple_animal/hostile/stalker/AttackingTarget() + . = ..() + if(. && ishuman(target)) + var/mob/living/carbon/human/H = target + H.reagents.add_reagent(/datum/reagent/toxin/cazador_venom, 6) +// PLAYABLE NIGHTSTALKER /mob/living/simple_animal/hostile/stalker/playable - health = 80 maxHealth = 80 + health = 80 + melee_damage_lower = 10 + melee_damage_upper = 15 + emote_taunt_sound = null emote_taunt = null aggrosound = null idlesound = null see_in_dark = 8 wander = 0 - anchored = FALSE - melee_damage_lower = 10 - melee_damage_upper = 15 - -/mob/living/simple_animal/hostile/stalker/AttackingTarget() - . = ..() - if(. && ishuman(target)) - var/mob/living/carbon/human/H = target - H.reagents.add_reagent(/datum/reagent/toxin/cazador_venom, 6) -/mob/living/simple_animal/hostile/stalker/playable/legion +/mob/living/simple_animal/hostile/stalker/playable/legion name = "legionstalker" - desc = "A nightstalker bred specifically for the legion under the use of combat and companionship. legionstalkers have the body and loyalty of a canine but the agility and deadlyness of rattlesnake." + desc = "A nightstalker bred specifically for the legion for combat and companionship. Legionstalkers have the body and loyalty of a canine but the agility and deadliness of a rattlesnake." icon_state = "nightstalker-legion" icon_living = "nightstalker-legion" icon_dead = "nightstalker-legion-dead" @@ -684,62 +432,87 @@ icon_state = "nightstalker_cub" icon_living = "nightstalker_cub" icon_dead = "nightstalker_cub_dead" + mob_biotypes = MOB_ORGANIC|MOB_BEAST - speak_chance = 0 - turns_per_move = 5 - retreat_distance = 8 - minimum_distance = 6 - guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/nightstalker_meat = 2, /obj/item/stack/sheet/sinew = 1, /obj/item/stack/sheet/bone = 1) - butcher_results = list(/obj/item/clothing/head/f13/stalkerpelt = 1, /obj/item/reagent_containers/food/snacks/meat/slab/nightstalker_meat = 1) - response_help_simple = "pets" - response_disarm_simple = "pushes aside" - response_harm_simple = "kicks" - taunt_chance = 30 - speed = 1 + maxHealth = 50 health = 50 - harm_intent_damage = 8 - obj_damage = 15 + speed = 1 + turns_per_move = 5 + melee_damage_lower = 5 melee_damage_upper = 10 - attack_verb_simple = "bites" + harm_intent_damage = 8 + obj_damage = 15 + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/nightstalker_meat = 2, + /obj/item/stack/sheet/sinew = 1, + /obj/item/stack/sheet/bone = 1 + ) + butcher_results = list( + /obj/item/clothing/head/f13/stalkerpelt = 1, + /obj/item/reagent_containers/food/snacks/meat/slab/nightstalker_meat = 1 + ) + + waddle_amount = 4 + waddle_up_time = 1 + waddle_side_time = 2 + + response_help_simple = "pets" + response_disarm_simple = "pushes aside" + response_harm_simple = "kicks" + speak_emote = list("howls") + attack_verb_simple = "bites" + attack_sound = 'sound/f13npc/nightstalker/attack1.ogg' + + emote_taunt = list("growls", "snarls") + emote_taunt_sound = list('sound/f13npc/nightstalker/taunt1.ogg', 'sound/f13npc/nightstalker/taunt2.ogg') + taunt_chance = 30 + aggrosound = list('sound/f13npc/nightstalker/aggro1.ogg', 'sound/f13npc/nightstalker/aggro2.ogg', 'sound/f13npc/nightstalker/aggro3.ogg') + idlesound = list('sound/f13npc/nightstalker/idle1.ogg') + death_sound = 'sound/f13npc/nightstalker/death.ogg' + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) + faction = list("nightstalkers", "critter-friend") - gold_core_spawnable = HOSTILE_SPAWN a_intent = INTENT_HARM + gold_core_spawnable = HOSTILE_SPAWN footstep_type = FOOTSTEP_MOB_CLAW + + can_ghost_into = TRUE + pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS + + // Pure melee - runs away and calls for help + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 - emote_taunt_sound = list('sound/f13npc/nightstalker/taunt1.ogg', 'sound/f13npc/nightstalker/taunt2.ogg') - emote_taunt = list("growls", "snarls") - aggrosound = list('sound/f13npc/nightstalker/aggro1.ogg', 'sound/f13npc/nightstalker/aggro2.ogg', 'sound/f13npc/nightstalker/aggro3.ogg') - idlesound = list('sound/f13npc/nightstalker/idle1.ogg') - death_sound = 'sound/f13npc/nightstalker/death.ogg' - attack_sound = 'sound/f13npc/nightstalker/attack1.ogg' - waddle_amount = 4 - waddle_up_time = 1 - waddle_side_time = 2 +/mob/living/simple_animal/hostile/stalkeryoung/Aggro() + ..() + summon_backup(12) + +/mob/living/simple_animal/hostile/stalkeryoung/AttackingTarget() + . = ..() + if(. && ishuman(target)) + var/mob/living/carbon/human/H = target + H.reagents.add_reagent(/datum/reagent/toxin/cazador_venom, 2) /mob/living/simple_animal/hostile/stalkeryoung/playable - health = 80 maxHealth = 80 + health = 80 + melee_damage_lower = 5 + melee_damage_upper = 10 + emote_taunt_sound = null emote_taunt = null aggrosound = null idlesound = null see_in_dark = 8 wander = 0 - anchored = FALSE - melee_damage_lower = 5 - melee_damage_upper = 10 - footstep_type = FOOTSTEP_MOB_CLAW - -/mob/living/simple_animal/hostile/stalker/AttackingTarget() - . = ..() - if(. && ishuman(target)) - var/mob/living/carbon/human/H = target - H.reagents.add_reagent(/datum/reagent/toxin/cazador_venom, 2) +// NIGHTSTALKER ITEMS /obj/item/clothing/head/f13/stalkerpelt name = "nightstalker pelt" desc = "A hat made from nightstalker pelt which makes the wearer feel both comfortable and elegant." @@ -758,19 +531,18 @@ name = "nightstalker meat" desc = "Could taste like rich red meat or flavorful chicken, depending on where the cut comes from." list_reagents = list(/datum/reagent/consumable/nutriment = 6, /datum/reagent/consumable/nutriment/vitamin = 2) - bitesize = 4 //Average animal + bitesize = 4 filling_color = "#FA8072" tastes = list("rich meat" = 3) cooked_type = /obj/item/reagent_containers/food/snacks/meat/steak/nightstalker_meat - slice_path = null foodtype = RAW | MEAT /obj/item/reagent_containers/food/snacks/meat/steak/nightstalker_meat name = "nightstalker steak" - desc = "A surprisingly high quality steak that could come in a variety of textures and may taste of either good chicken or rich beef" + desc = "A surprisingly high quality steak that could come in a variety of textures and may taste of either good chicken or rich beef." ///////////// -// MOLERAT // It's time ~TK +// MOLERAT // ///////////// /mob/living/simple_animal/hostile/molerat @@ -780,208 +552,288 @@ icon_state = "molerat" icon_living = "molerat" icon_dead = "molerat_dead" + mob_biotypes = MOB_ORGANIC|MOB_BEAST - can_ghost_into = TRUE - speak_chance = 0 + + maxHealth = 25 + health = 25 + speed = 2 turns_per_move = 5 - guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/slab/molerat = 2, /obj/item/stack/sheet/sinew = 1,/obj/item/stack/sheet/animalhide/molerat = 1, /obj/item/stack/sheet/bone = 1) - butcher_results = list(/obj/item/stack/sheet/bone = 1) + + melee_damage_lower = 4 + melee_damage_upper = 10 + harm_intent_damage = 8 + obj_damage = 15 + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/molerat = 2, + /obj/item/stack/sheet/sinew = 1, + /obj/item/stack/sheet/animalhide/molerat = 1, + /obj/item/stack/sheet/bone = 1 + ) butcher_difficulty = 1.5 + + waddle_amount = 3 + waddle_up_time = 1 + waddle_side_time = 2 + response_help_simple = "pets" response_disarm_simple = "gently pushes aside" response_harm_simple = "hits" - taunt_chance = 30 - speed = 2 - maxHealth = 25 - health = 25 - harm_intent_damage = 8 - obj_damage = 15 - melee_damage_lower = 4 - melee_damage_upper = 10 + + speak_emote = list("chitters") attack_verb_simple = "bites" attack_sound = 'sound/creatures/molerat_attack.ogg' - speak_emote = list("chitters") - atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) - faction = list("hostile", "gecko") - gold_core_spawnable = HOSTILE_SPAWN - a_intent = INTENT_HARM - - emote_taunt_sound = list('sound/f13npc/molerat/taunt.ogg') + emote_taunt = list("hisses") - aggrosound = list('sound/f13npc/molerat/aggro1.ogg', 'sound/f13npc/molerat/aggro2.ogg',) + emote_taunt_sound = list('sound/f13npc/molerat/taunt.ogg') + taunt_chance = 30 + aggrosound = list('sound/f13npc/molerat/aggro1.ogg', 'sound/f13npc/molerat/aggro2.ogg') idlesound = list('sound/f13npc/molerat/idle.ogg') death_sound = 'sound/f13npc/molerat/death.ogg' - waddle_amount = 3 - waddle_up_time = 1 - waddle_side_time = 2 + + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) + + faction = list("hostile", "gecko") + a_intent = INTENT_HARM + gold_core_spawnable = HOSTILE_SPAWN + + can_ghost_into = TRUE desc_short = "Small, squishy, and numerous." pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS - + + // Z-movement + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 40 + + can_open_doors = FALSE + can_open_airlocks = FALSE + + // Pure melee + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + variation_list = list( MOB_COLOR_VARIATION(50, 50, 50, 255, 255, 255), MOB_SPEED_LIST(2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8), MOB_SPEED_CHANGE_PER_TURN_CHANCE(5), MOB_HEALTH_LIST(15, 20, 25, 26), - MOB_RETREAT_DISTANCE_LIST(0, 1), - MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(100), - MOB_MINIMUM_DISTANCE_LIST(0, 1), - MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(5), ) +/mob/living/simple_animal/hostile/molerat/Aggro() + . = ..() + if(.) + return + summon_backup(8) + +/mob/living/simple_animal/hostile/molerat/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + /mob/living/simple_animal/hostile/molerat/become_the_mob(mob/user) call_backup = /obj/effect/proc_holder/mob_common/summon_backup/small_critter send_mobs = /obj/effect/proc_holder/mob_common/direct_mobs/small_critter make_a_nest = /obj/effect/proc_holder/mob_common/make_nest/molerat . = ..() +////////////////// +// GELATIN CUBE // +////////////////// + /mob/living/simple_animal/hostile/gelcube name = "gelatinous cube" - desc = "A big green radioactive cube creature, it jiggles with menacing wiggles and is making some sort of goofy face at you." + desc = "A big green radioactive cube creature. It jiggles with menacing wiggles and is making some sort of goofy face at you." icon = 'fallout/icons/mob/vatgrowing.dmi' icon_state = "gelatinous" icon_living = "gelatinous" icon_dead = "gelatinous_dead" + mob_biotypes = MOB_ORGANIC|MOB_BEAST - can_ghost_into = TRUE - speak_chance = 0 + + maxHealth = 850 + health = 850 + speed = 8 + move_to_delay = 6 turns_per_move = 10 + + melee_damage_lower = 35 + melee_damage_upper = 45 + harm_intent_damage = 30 + obj_damage = 15 + guaranteed_butcher_results = list(/obj/item/reagent_containers/food/snacks/soup/amanitajelly = 2) butcher_results = list(/obj/item/reagent_containers/food/snacks/soup/amanitajelly = 1) butcher_difficulty = 1.5 + loot = list(/obj/item/stack/f13Cash/random/med) - /// How many things to drop on death? Set to MOB_LOOT_ALL to just drop everything in the list loot_drop_amount = 10 - /// Drop 1 - loot_drop_amount? False always drops loot_drop_amount items loot_amount_random = TRUE - /// slots in a list of trash loot var/random_trash_loot = TRUE + + waddle_amount = 4 + waddle_up_time = 3 + waddle_side_time = 2 + response_help_simple = "jiggles" response_disarm_simple = "wiggles" response_harm_simple = "shakes" - taunt_chance = 30 - speed = 8 - maxHealth = 850 - health = 850 - harm_intent_damage = 30 - obj_damage = 15 - melee_damage_lower = 35 - melee_damage_upper = 45 - move_to_delay = 10 + + speak_emote = list("glorbles") attack_verb_simple = "goops" attack_sound = 'sound/effects/attackblob.ogg' - speak_emote = list("glorbles") - atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) - faction = list("the tungsten cube") //at last, I am at peace ~TK - gold_core_spawnable = HOSTILE_SPAWN - a_intent = INTENT_HARM - - emote_taunt_sound = list('sound/effects/bubbles.ogg') + emote_taunt = list("blorgles") + emote_taunt_sound = list('sound/effects/bubbles.ogg') + taunt_chance = 30 aggrosound = list('sound/misc/splort.ogg') - idlesound = list('sound/vore/prey/squish_01.ogg') //God forgive me for what I must do. Its just a perfect sound. ~TK + idlesound = list('sound/vore/prey/squish_01.ogg') death_sound = 'sound/misc/crack.ogg' - waddle_amount = 4 - waddle_up_time = 3 - waddle_side_time = 2 + + atmos_requirements = list("min_oxy" = 5, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 1, "min_co2" = 0, "max_co2" = 5, "min_n2" = 0, "max_n2" = 0) + + faction = list("the tungsten cube") + a_intent = INTENT_HARM + gold_core_spawnable = HOSTILE_SPAWN + + can_ghost_into = TRUE desc_short = "Big, squishy, and gelatinous." + pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS + + // Z-movement + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 80 // Very slow + + can_open_doors = FALSE + can_open_airlocks = FALSE + + // Pure melee + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 /mob/living/simple_animal/hostile/gelcube/Initialize() . = ..() if(random_trash_loot) loot = GLOB.trash_ammo + GLOB.trash_chem + GLOB.trash_clothing + GLOB.trash_craft + GLOB.trash_gun + GLOB.trash_misc + GLOB.trash_money + GLOB.trash_mob + GLOB.trash_part + GLOB.trash_tool + GLOB.trash_attachment +/mob/living/simple_animal/hostile/gelcube/Aggro() + . = ..() + if(.) + return + summon_backup(8) + +/mob/living/simple_animal/hostile/gelcube/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() //////////// -//T-Birds// -////////// +// T-BIRD // +//////////// /mob/living/simple_animal/hostile/bloodbird - name = "Blood Bird" + name = "blood bird" desc = "A large mutated turkey vulture." icon = 'icons/fallout/mobs/animals/bloodbird.dmi' icon_state = "bloodbird" icon_living = "bloodbird" icon_dead = "bloodbird_dead" + mob_biotypes = MOB_ORGANIC|MOB_BEAST - speak_chance = 0 - turns_per_move = 5 - guaranteed_butcher_results = list( - /obj/item/reagent_containers/food/snacks/meat/slab/chicken = 4, - /obj/item/feather = 3) - butcher_results = list(/obj/item/stack/sheet/bone = 2) - butcher_difficulty = 1 - response_help_simple = "pets" - response_disarm_simple = "gently pushes aside" - response_harm_simple = "hits" - taunt_chance = 30 - speed = 0 + maxHealth = 100 health = 100 - harm_intent_damage = 8 - obj_damage = 20 + speed = 0 + move_to_delay = 2.5 + turns_per_move = 5 + melee_damage_lower = 25 melee_damage_upper = 35 - move_to_delay = 1.5 - retreat_distance = 0 - minimum_distance = 0 + harm_intent_damage = 8 + obj_damage = 20 + aggro_vision_range = 9 vision_range = 8 + + guaranteed_butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/slab/chicken = 4, + /obj/item/feather = 3, + /obj/item/stack/sheet/bone = 2 + ) + butcher_difficulty = 1 + waddle_amount = 5 waddle_up_time = 1 waddle_side_time = 1 pass_flags = PASSTABLE - speak_emote = list( - "cackles", - "squawks", - "clacks", - ) - emote_see = list( - "screeches", - "gonks" - ) - attack_verb_simple = list( - "bites", - "claws", - "rends", - "mutilates" - ) + + response_help_simple = "pets" + response_disarm_simple = "gently pushes aside" + response_harm_simple = "hits" + + speak_emote = list("cackles", "squawks", "clacks") + emote_see = list("screeches", "gonks") + attack_verb_simple = list("bites", "claws", "rends", "mutilates") + + emote_taunt = list("screeches") + emote_taunt_sound = list('sound/creatures/terrorbird/hoot1.ogg', 'sound/creatures/terrorbird/hoot2.ogg', 'sound/creatures/terrorbird/hoot3.ogg', 'sound/creatures/terrorbird/hoot4.ogg') + taunt_chance = 30 + aggrosound = list('sound/creatures/terrorbird/growl1.ogg', 'sound/creatures/terrorbird/growl2.ogg', 'sound/creatures/terrorbird/growl3.ogg') + idlesound = list('sound/creatures/terrorbird/clack1.ogg', 'sound/creatures/terrorbird/clack2.ogg', 'sound/creatures/terrorbird/clack3.ogg') + death_sound = list('sound/creatures/terrorbird/groan1.ogg', 'sound/creatures/terrorbird/groan2.ogg') + faction = list("terror bird") a_intent = INTENT_HARM gold_core_spawnable = HOSTILE_SPAWN footstep_type = FOOTSTEP_MOB_HEAVY - idlesound = list( - 'sound/creatures/terrorbird/clack1.ogg', - 'sound/creatures/terrorbird/clack2.ogg', - 'sound/creatures/terrorbird/clack3.ogg', - ) - - emote_taunt = list("screeches") - emote_taunt_sound = list( - 'sound/creatures/terrorbird/hoot1.ogg', - 'sound/creatures/terrorbird/hoot2.ogg', - 'sound/creatures/terrorbird/hoot3.ogg', - 'sound/creatures/terrorbird/hoot4.ogg', - ) - aggrosound = list( - 'sound/creatures/terrorbird/growl1.ogg', - 'sound/creatures/terrorbird/growl2.ogg', - 'sound/creatures/terrorbird/growl3.ogg', - ) - death_sound = list( - 'sound/creatures/terrorbird/groan1.ogg', - 'sound/creatures/terrorbird/groan2.ogg', - ) - can_ghost_into = FALSE //One day Kotetsu will return to us. ~TK + + can_ghost_into = FALSE desc_short = "What a terrifying bird." + pop_required_to_jump_into = MED_MOB_MIN_PLAYERS + + // Z-movement + can_z_move = TRUE + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 30 // Faster + + can_open_doors = FALSE + can_open_airlocks = FALSE + + // Pure melee + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 - variation_list = list( MOB_COLOR_VARIATION(50, 50, 50, 255, 255, 255), MOB_SPEED_LIST(1.5, 1.8, 2.0, 2.2), MOB_SPEED_CHANGE_PER_TURN_CHANCE(50), MOB_HEALTH_LIST(80, 90, 100, 110), - MOB_RETREAT_DISTANCE_LIST(0, 0, 1), - MOB_RETREAT_DISTANCE_CHANGE_PER_TURN_CHANCE(90), - MOB_MINIMUM_DISTANCE_LIST(0, 0, 0, 1), - MOB_MINIMUM_DISTANCE_CHANGE_PER_TURN_CHANCE(90), ) + +/mob/living/simple_animal/hostile/bloodbird/Aggro() + . = ..() + if(.) + return + summon_backup(10) + +/mob/living/simple_animal/hostile/bloodbird/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() diff --git a/code/modules/mob/living/simple_animal/hostile/f13/wastebots.dm b/code/modules/mob/living/simple_animal/hostile/f13/wastebots.dm index 868bef63b07..45748a45b88 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/wastebots.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/wastebots.dm @@ -1,11 +1,17 @@ -/*IN THIS FILE: --Handy --Gutsy --Protectrons --Robobrains --Assaultrons -*/ +// In this document: Handy, Gutsy, Protectrons, Robobrains, Assaultrons, Liberator +// Shared robot properties as a define for clarity: +// - mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC +// - damage_coeff: immune to TOX, CLONE, STAMINA, OXY +// - atmos_requirements: no atmosphere needed +// - blood_volume = 0, healable = FALSE +// - del_on_death = TRUE (except playable variants) + +/////////////// +// MR. HANDY // +/////////////// + +// BASE HANDY - melee saw bot /mob/living/simple_animal/hostile/handy name = "mr. handy" desc = "A crazed pre-war household assistant robot, armed with a cutting saw." @@ -13,135 +19,175 @@ icon_state = "handy" icon_living = "handy" icon_dead = "robot_dead" - speed = 2 - can_ghost_into = TRUE - gender = NEUTER + mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC - move_resist = MOVE_FORCE_OVERPOWERING // Can't be pulled mob_armor = ARMOR_VALUE_ROBOT_CIVILIAN - maxHealth = 100 + + maxHealth = 100 health = 100 + speed = 2 stamcrit_threshold = SIMPLEMOB_NO_STAMCRIT + + melee_damage_lower = 12 + melee_damage_upper = 24 + + robust_searching = TRUE + stat_attack = CONSCIOUS + + move_resist = MOVE_FORCE_OVERPOWERING + + faction = list("wastebot") + a_intent = INTENT_HARM + gold_core_spawnable = HOSTILE_SPAWN + check_friendly_fire = TRUE + del_on_death = TRUE + healable = FALSE + blood_volume = 0 + + gender = NEUTER + emp_flags = list( MOB_EMP_STUN, MOB_EMP_BERSERK, MOB_EMP_DAMAGE, MOB_EMP_SCRAMBLE - ) - healable = FALSE - stat_attack = CONSCIOUS - auto_fire_delay = GUN_AUTOFIRE_DELAY_SLOWER - melee_damage_lower = 12 - melee_damage_upper = 24 - robust_searching = TRUE - attack_verb_simple = "saws" - faction = list("wastebot") + ) + + damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) minbodytemp = 0 + speak_emote = list("states") - gold_core_spawnable = HOSTILE_SPAWN - del_on_death = TRUE - deathmessage = "blows apart!" - taunt_chance = 30 - blood_volume = 0 - waddle_amount = 3 - waddle_up_time = 2 - waddle_side_time = 1 - send_mobs = null - call_backup = null - + attack_verb_simple = "saws" + attack_sound = 'sound/f13npc/handy/attack.wav' + + emote_taunt = list("raises a saw") emote_taunt_sound = list( 'sound/f13npc/handy/taunt1.ogg', 'sound/f13npc/handy/taunt2.ogg' - ) - emote_taunt = list("raises a saw") - + ) + taunt_chance = 30 aggrosound = list( 'sound/f13npc/handy/aggro1.ogg', 'sound/f13npc/handy/aggro2.ogg' - ) + ) idlesound = list( 'sound/f13npc/handy/idle1.wav', 'sound/f13npc/handy/idle2.ogg', 'sound/f13npc/handy/idle3.ogg' - ) - + ) death_sound = 'sound/f13npc/handy/robo_death.ogg' - attack_sound = 'sound/f13npc/handy/attack.wav' - - damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0) + deathmessage = "blows apart!" + loot = list( /obj/effect/decal/cleanable/robot_debris, /obj/item/stack/crafting/electronicparts/three - ) - pop_required_to_jump_into = MED_MOB_MIN_PLAYERS + ) + + waddle_amount = 3 + waddle_up_time = 2 + waddle_side_time = 1 + + can_ghost_into = TRUE desc_short = "A snooty robot with a circular saw." + pop_required_to_jump_into = MED_MOB_MIN_PLAYERS + + send_mobs = null + call_backup = null // Robots don't call for organic backup + + // Z-movement - flies but slow + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 50 + + can_open_doors = TRUE + can_open_airlocks = TRUE + + // Pure melee - close range saw bot + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + +/mob/living/simple_animal/hostile/handy/Initialize() + . = ..() + add_overlay("eyes-[initial(icon_state)]") +/mob/living/simple_animal/hostile/handy/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() + +// Friendly fire resistance - wastebots are coordinated +/mob/living/simple_animal/hostile/handy/bullet_act(obj/item/projectile/P) + if(P && P.firer && istype(P.firer, /mob/living/simple_animal/hostile/handy)) + var/original_damage = P.damage + P.damage *= 0.2 + . = ..() + P.damage = original_damage + return + return ..() + +// PLAYABLE HANDY /mob/living/simple_animal/hostile/handy/playable - mob_armor = ARMOR_VALUE_ROBOT_CIVILIAN - maxHealth = 300 + maxHealth = 300 health = 300 attack_verb_simple = "shoots a burst of flame at" - emote_taunt_sound = null - emote_taunt = null - aggrosound = null - idlesound = null see_in_dark = 8 wander = FALSE force_threshold = 10 anchored = FALSE del_on_death = FALSE dextrous = TRUE - possible_a_intents = list(INTENT_HELP, INTENT_HARM) ranged = FALSE + emote_taunt_sound = null + emote_taunt = null + aggrosound = null + idlesound = null + possible_a_intents = list(INTENT_HELP, INTENT_HARM) -/mob/living/simple_animal/hostile/handy/Initialize() - . = ..() - add_overlay("eyes-[initial(icon_state)]") - -/mob/living/simple_animal/hostile/handy/nsb //NSB + Raider Bunker specific - name = "mr.handy" +// NSB HANDY - raider bunker variant +/mob/living/simple_animal/hostile/handy/nsb + name = "mr. handy" aggro_vision_range = 15 faction = list("raider") obj_damage = 300 + can_ghost_into = FALSE + +/////////////// +// MR. GUTSY // +/////////////// +// GUTSY - combat variant with plasma + flamer /mob/living/simple_animal/hostile/handy/gutsy name = "mr. gutsy" desc = "A pre-war combat robot based off the Mr. Handy design, armed with plasma weaponry and a deadly close-range flamer." icon_state = "gutsy" icon_living = "gutsy" - icon_dead = "robot_dead" - can_ghost_into = FALSE + mob_armor = ARMOR_VALUE_ROBOT_MILITARY - maxHealth = 100 + + maxHealth = 100 health = 100 stat_attack = UNCONSCIOUS + melee_damage_lower = 18 melee_damage_upper = 64 attack_sound = 'sound/items/welder.ogg' attack_verb_simple = "shoots a burst of flame at" - projectilesound = 'sound/weapons/laser.ogg' - projectiletype = /obj/item/projectile/f13plasma/scatter - extra_projectiles = 1 - ranged = TRUE - retreat_distance = 4 - minimum_distance = 4 - check_friendly_fire = TRUE - loot = list( - /obj/effect/decal/cleanable/robot_debris, - /obj/item/stack/crafting/electronicparts/three, - /obj/item/stock_parts/cell/ammo/mfc - ) - pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS - + + can_ghost_into = FALSE + + emote_taunt = list("raises a flamer") emote_taunt_sound = list( 'sound/f13npc/gutsy/taunt1.ogg', 'sound/f13npc/gutsy/taunt2.ogg', 'sound/f13npc/gutsy/taunt3.ogg', 'sound/f13npc/gutsy/taunt4.ogg' - ) - emote_taunt = list("raises a flamer") - + ) aggrosound = list( 'sound/f13npc/gutsy/aggro1.ogg', 'sound/f13npc/gutsy/aggro2.ogg', @@ -149,12 +195,32 @@ 'sound/f13npc/gutsy/aggro4.ogg', 'sound/f13npc/gutsy/aggro5.ogg', 'sound/f13npc/gutsy/aggro6.ogg' - ) + ) idlesound = list( 'sound/f13npc/gutsy/idle1.ogg', 'sound/f13npc/gutsy/idle2.ogg', 'sound/f13npc/gutsy/idle3.ogg' - ) + ) + + loot = list( + /obj/effect/decal/cleanable/robot_debris, + /obj/item/stack/crafting/electronicparts/three, + /obj/item/stock_parts/cell/ammo/mfc + ) + + desc_short = "A gutsy robot with a plasma gun." + pop_required_to_jump_into = BIG_MOB_MIN_PLAYERS + + // Pure ranged - plasma gun + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 4 + minimum_distance = 1 + + ranged_cooldown_time = 2 SECONDS + extra_projectiles = 1 + projectiletype = /obj/item/projectile/f13plasma/scatter + projectilesound = 'sound/weapons/laser.ogg' projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(PLASMA_VOLUME), @@ -165,67 +231,95 @@ SP_DISTANT_SOUND(PLASMA_DISTANT_SOUND), SP_DISTANT_RANGE(PLASMA_RANGE_DISTANT) ) - desc_short = "A gutsy robot with a plasma gun." +// PLAYABLE GUTSY /mob/living/simple_animal/hostile/handy/gutsy/playable - mob_armor = ARMOR_VALUE_ROBOT_MILITARY - maxHealth = 100 - health = 100 speed = 1 attack_verb_simple = "shoots a burst of flame at" - emote_taunt_sound = null - emote_taunt = null - aggrosound = null - idlesound = null see_in_dark = 8 - environment_smash = 2 //can break lockers, but not walls + environment_smash = ENVIRONMENT_SMASH_STRUCTURES wander = FALSE force_threshold = 10 anchored = FALSE del_on_death = FALSE - possible_a_intents = list(INTENT_HELP, INTENT_HARM) dextrous = TRUE ranged = FALSE + emote_taunt_sound = null + emote_taunt = null + aggrosound = null + idlesound = null + possible_a_intents = list(INTENT_HELP, INTENT_HARM) -/mob/living/simple_animal/hostile/handy/gutsy/nsb //NSB + Raider Bunker specific +// NSB GUTSY +/mob/living/simple_animal/hostile/handy/gutsy/nsb name = "mr. gutsy" aggro_vision_range = 15 faction = list("raider") obj_damage = 300 + can_ghost_into = FALSE +// MR. BURNSY - flamer variant +/mob/living/simple_animal/hostile/handy/gutsy/flamer + name = "Mr. Burnsy" + desc = "A modified mr. gutsy, equipped with a more precise flamer, ditching its plasma weaponry." + color = "#B85C00" + can_ghost_into = FALSE + + projectiletype = /obj/item/projectile/bullet/incendiary/shotgun + projectilesound = 'sound/magic/fireball.ogg' + extra_projectiles = 1 + +/////////////// +// LIBERATOR // +/////////////// + +// LIBERATOR - small Chinese PLA drone /mob/living/simple_animal/hostile/handy/liberator name = "liberator" - desc = "A small pre-War droned used by the People's Liberation Army." + desc = "A small pre-War drone used by the People's Liberation Army." icon = 'icons/fallout/mobs/robots/weirdrobots.dmi' icon_state = "liberator" icon_living = "leberator" icon_dead = "liberator_d" icon_gib = "liberator_g" + mob_armor = ARMOR_VALUE_ROBOT_SECURITY - maxHealth = 50 + + maxHealth = 50 health = 50 + melee_damage_lower = 5 melee_damage_upper = 10 - can_ghost_into = FALSE attack_verb_simple = "slaps" - projectilesound = 'sound/weapons/laser.ogg' - projectiletype = /obj/item/projectile/beam/laser/pistol - extra_projectiles = 1 - ranged = TRUE - retreat_distance = 2 - minimum_distance = 2 - check_friendly_fire = TRUE + + can_ghost_into = FALSE + + emote_taunt = list("levels its laser") + emote_taunt_sound = null + aggrosound = list('sound/f13npc/liberator/chineserobotcarinsurance.ogg') + idlesound = null + attack_sound = null + death_sound = null + loot = list( /obj/effect/decal/cleanable/robot_debris, /obj/item/stack/crafting/electronicparts/three, /obj/item/stock_parts/cell/ammo/mfc - ) - emote_taunt_sound = null - emote_taunt = list("levels its laser") - aggrosound = list("sound/f13npc/liberator/chineserobotcarinsurance.ogg") - idlesound = null - death_sound = null - attack_sound = null + ) + + desc_short = "A robot that shoots lasers." + pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS + + // Pure ranged - laser pistol + combat_mode = COMBAT_MODE_RANGED + ranged = TRUE + retreat_distance = 4 + minimum_distance = 1 + + ranged_cooldown_time = 2 SECONDS + extra_projectiles = 1 + projectiletype = /obj/item/projectile/beam/laser/pistol + projectilesound = 'sound/weapons/laser.ogg' projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(LASER_VOLUME), @@ -236,49 +330,62 @@ SP_DISTANT_SOUND(LASER_DISTANT_SOUND), SP_DISTANT_RANGE(LASER_RANGE_DISTANT) ) - desc_short = "A robot that shoots lasers." +// YELLOW LIBERATOR /mob/living/simple_animal/hostile/handy/liberator/yellow - name = "liberator" - desc = "A small pre-War droned used by the People's Liberation Army." icon_state = "liberator_y" - can_ghost_into = FALSE icon_living = "leberator_y" icon_dead = "liberator_y_d" +/////////////// +// ROBOBRAIN // +/////////////// + +// ROBOBRAIN - cyborg with laser /mob/living/simple_animal/hostile/handy/robobrain name = "robobrain" - desc = "A next-gen cyborg developed by General Atomic International" - icon = 'icons/fallout/mobs/robots/wasterobots.dmi' + desc = "A next-gen cyborg developed by General Atomic International." icon_state = "robobrain" icon_living = "robobrain" icon_dead = "robobrain_d" + mob_armor = ARMOR_VALUE_ROBOT_SECURITY - maxHealth = 110 + + maxHealth = 110 health = 110 stat_attack = UNCONSCIOUS - can_ghost_into = FALSE + melee_damage_lower = 15 melee_damage_upper = 37 attack_verb_simple = "slaps" - projectilesound = 'sound/weapons/laser.ogg' - projectiletype = /obj/item/projectile/beam/laser - extra_projectiles = 1 - ranged = TRUE - retreat_distance = 2 - minimum_distance = 2 - check_friendly_fire = TRUE - loot = list( - /obj/effect/decal/cleanable/robot_debris, - /obj/item/stack/crafting/electronicparts/three, - /obj/item/stock_parts/cell/ammo/mfc - ) - emote_taunt_sound = null + + can_ghost_into = FALSE + emote_taunt = list("levels its laser") + emote_taunt_sound = null aggrosound = null idlesound = null - death_sound = null attack_sound = null + death_sound = null + + loot = list( + /obj/effect/decal/cleanable/robot_debris, + /obj/item/stack/crafting/electronicparts/three, + /obj/item/stock_parts/cell/ammo/mfc + ) + + desc_short = "A brainy robot with lasers." + + // Mixed combat - has melee but prefers ranged + combat_mode = COMBAT_MODE_MIXED + ranged = TRUE + retreat_distance = 4 + minimum_distance = 1 + + ranged_cooldown_time = 2 SECONDS + extra_projectiles = 1 + projectiletype = /obj/item/projectile/beam/laser + projectilesound = 'sound/weapons/laser.ogg' projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(LASER_VOLUME), @@ -289,20 +396,22 @@ SP_DISTANT_SOUND(LASER_DISTANT_SOUND), SP_DISTANT_RANGE(LASER_RANGE_DISTANT) ) - desc_short = "A brainy robot with lasers." - -/mob/living/simple_animal/hostile/handy/robobrain/AttackingTarget() - . = ..() -/mob/living/simple_animal/hostile/handy/robobrain/nsb //NSB + Raider Bunker specific +// NSB ROBOBRAIN +/mob/living/simple_animal/hostile/handy/robobrain/nsb name = "robobrain" aggro_vision_range = 15 faction = list("raider") - can_ghost_into = FALSE obj_damage = 300 - health = 300 maxHealth = 300 + health = 300 + can_ghost_into = FALSE +///////////////// +// PROTECTRON // +///////////////// + +// BASE PROTECTRON - slow, laser-armed security bot /mob/living/simple_animal/hostile/handy/protectron name = "protectron" desc = "A pre-war security robot armed with deadly lasers." @@ -310,86 +419,74 @@ icon_state = "protectron" icon_living = "protectron" icon_dead = "protectron_dead" + mob_armor = ARMOR_VALUE_ROBOT_CIVILIAN - maxHealth = 100 + + maxHealth = 100 health = 100 - stat_attack = UNCONSCIOUS - speed = 4 - can_ghost_into = TRUE - melee_damage_lower = 5 //severely reduced melee damage here because its silly to have a ranged mob also be a cqc master - melee_damage_upper = 10 - extra_projectiles = 0 //removed extra projectiles to make these easier to deal with on super lowpop + speed = 4 // Noticeably slow + move_to_delay = 4 stat_attack = CONSCIOUS - ranged = TRUE - move_to_delay = 9 //WAY slower than average, - // m2d 3 = standard, less is fast, more is slower. - - retreat_distance = 0 // Mob doesn't retreat - //how far they pull back - minimum_distance = 1 - // how close you can get before they try to pull back - + melee_damage_lower = 5 // Weak melee - it's a ranged bot + melee_damage_upper = 10 + aggro_vision_range = 7 - //tiles within they start attacking, doesn't count the mobs tile - vision_range = 8 - //tiles within they start making noise, does count the mobs tile - - attack_verb_simple = list( - "baps", - "bops", - "boops", - "smacks", - "clamps", - "pinches", - "thumps", - "fistos" - ) + + attack_verb_simple = list("baps", "bops", "boops", "smacks", "clamps", "pinches", "thumps", "fistos") attack_sound = 'sound/weapons/punch1.ogg' - projectilesound = 'sound/weapons/laser.ogg' - projectiletype = /obj/item/projectile/beam/laser/pistol - faction = list("wastebot") - check_friendly_fire = TRUE - loot = list( - /obj/effect/decal/cleanable/robot_debris, - /obj/item/stack/crafting/electronicparts/five - ) - attack_phrase = list( - "Howdy pardner!", - "Shoot out at the O.K. Corral!", - "Go back to Oklahoma!", - "Please assume the position.", - "Protect and serve.", - "Antisocial behavior detected.", - "Criminal behavior willbe punished.", - "Please step into the open and identify yourself, law abiding citizens have nothing to fear." - ) + + emote_taunt = list("raises its arm laser", "gets ready to rumble", "assumes the position", "whirls up its servos", "takes aim", "holds its ground") emote_taunt_sound = list( 'sound/f13npc/protectron/taunt1.ogg', 'sound/f13npc/protectron/taunt2.ogg', 'sound/f13npc/protectron/taunt3.ogg' - ) - emote_taunt = list( - "raises its arm laser", - "gets ready to rumble", - "assumes the position", - "whirls up its servos", - "takes aim", - "holds its ground" - ) + ) + taunt_chance = 30 aggrosound = list( 'sound/f13npc/protectron/aggro1.ogg', 'sound/f13npc/protectron/aggro2.ogg', 'sound/f13npc/protectron/aggro3.ogg', 'sound/f13npc/protectron/aggro4.ogg' - ) + ) idlesound = list( 'sound/f13npc/protectron/idle1.ogg', 'sound/f13npc/protectron/idle2.ogg', 'sound/f13npc/protectron/idle3.ogg', - 'sound/f13npc/protectron/idle4.ogg', - ) + 'sound/f13npc/protectron/idle4.ogg' + ) + + attack_phrase = list( + "Howdy pardner!", + "Shoot out at the O.K. Corral!", + "Go back to Oklahoma!", + "Please assume the position.", + "Protect and serve.", + "Antisocial behavior detected.", + "Criminal behavior will be punished.", + "Please step into the open and identify yourself, law abiding citizens have nothing to fear." + ) + + loot = list( + /obj/effect/decal/cleanable/robot_debris, + /obj/item/stack/crafting/electronicparts/five + ) + + can_ghost_into = TRUE + desc_short = "A clunky hunk of junk with a laser." + pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS + + // Mixed combat - laser + melee when cornered. Slow so retreating is pointless. + combat_mode = COMBAT_MODE_MIXED + ranged = TRUE + retreat_distance = 4 + minimum_distance = 1 + + ranged_cooldown_time = 2 SECONDS + extra_projectiles = 0 // One shot at a time on lowpop + projectiletype = /obj/item/projectile/beam/laser/pistol + projectilesound = 'sound/weapons/laser.ogg' projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(LASER_VOLUME), @@ -400,70 +497,57 @@ SP_DISTANT_SOUND(LASER_DISTANT_SOUND), SP_DISTANT_RANGE(LASER_RANGE_DISTANT) ) - pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS - desc_short = "A clunky hunk of junk with a laser." +// PLAYABLE PROTECTRON /mob/living/simple_animal/hostile/handy/protectron/playable - ranged = FALSE melee_damage_lower = 25 melee_damage_upper = 38 - health = 100 - maxHealth = 100 speed = 2 attack_verb_simple = "clamps" - emote_taunt_sound = null - emote_taunt = null - aggrosound = null - idlesound = null see_in_dark = 8 - environment_smash = 1 //can break lockers, but not walls + environment_smash = ENVIRONMENT_SMASH_STRUCTURES wander = FALSE force_threshold = 10 anchored = FALSE del_on_death = FALSE + ranged = FALSE + emote_taunt_sound = null + emote_taunt = null + aggrosound = null + idlesound = null possible_a_intents = list(INTENT_HELP, INTENT_HARM) -/mob/living/simple_animal/hostile/handy/protectron/nsb //NSB + Raider Bunker specific +// NSB PROTECTRON +/mob/living/simple_animal/hostile/handy/protectron/nsb name = "protectron" aggro_vision_range = 15 - can_ghost_into = FALSE faction = list("raider") obj_damage = 300 + can_ghost_into = FALSE -/mob/living/simple_animal/pet/dog/protectron //Not an actual dog +// TRADING PROTECTRON - pet/friendly variant +/mob/living/simple_animal/pet/dog/protectron name = "Trading Protectron" desc = "A standard RobCo RX2 V1.16.4 \"Trade-o-Vend\", loaded with Trade protocols.
Looks like it was kept operational for an indefinite period of time. Its body is covered in cracks and dents of various sizes.
As it has been repaired countless times, it's amazing the machine is still functioning at all." icon = 'icons/fallout/mobs/robots/protectrons.dmi' icon_state = "protectron_trade" icon_living = "protectron_trade" icon_dead = "protectron_trade_dead" + + mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC + maxHealth = 200 health = 200 - can_ghost_into = FALSE speak_chance = 5 - mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC + can_ghost_into = FALSE + blood_volume = 0 + faction = list( - "neutral", - "silicon", - "dog", - "hostile", - "pirate", - "wastebot", - "wolf", - "plants", - "turret", - "enclave", - "ghoul", - "cazador", - "supermutant", - "gecko", - "slime", - "radscorpion", - "skeleton", - "carp", - "bs", - "bighorner" - ) + "neutral", "silicon", "dog", "hostile", "pirate", "wastebot", "wolf", + "plants", "turret", "enclave", "ghoul", "cazador", "supermutant", + "gecko", "slime", "radscorpion", "skeleton", "carp", "bs", "bighorner" + ) + speak = list( "Howdy partner! How about you spend some of them there hard earned caps on some of this fine merchandise.", "Welcome back partner! Hoo-wee it's a good day to buy some personal protection!", @@ -471,104 +555,133 @@ "What a fine day partner. A fine day indeed.", "Reminds me of what my grandpappy used to say, make a snap decision now and never question it. You look like you could use some product there partner.", "Lotta critters out there want to chew you up partner, you could use a little hand with that now couldn't you?" - ) + ) + speak_emote = list() emote_hear = list() emote_see = list() - response_help_simple = "shakes its manipulator" + + response_help_simple = "shakes its manipulator" response_disarm_simple = "pushes" - response_harm_simple = "punches" + response_harm_simple = "punches" attack_sound = 'sound/voice/liveagain.ogg' + butcher_results = list(/obj/effect/gibspawner/robot = 1) - blood_volume = 0 +///////////////// +// ASSAULTRON // +///////////////// + +// BASE ASSAULTRON - fast melee combat robot /mob/living/simple_animal/hostile/handy/assaultron name = "assaultron" desc = "A deadly close combat robot developed by RobCo in a vaguely feminine, yet ominous chassis." icon_state = "assaultron" icon_living = "assaultron" - gender = FEMALE //Pffffffffffffffffffffff icon_dead = "gib7" + + mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC mob_armor = ARMOR_VALUE_ROBOT_MILITARY - maxHealth = 100 + + maxHealth = 100 health = 100 + speed = 1 // Fast for a robot stat_attack = UNCONSCIOUS - can_ghost_into = FALSE - mob_biotypes = MOB_ROBOTIC|MOB_INORGANIC - speed = 1 + gender = FEMALE + melee_damage_lower = 18 melee_damage_upper = 45 - environment_smash = 2 //can smash walls attack_verb_simple = "grinds their claws on" - faction = list("wastebot") + + environment_smash = ENVIRONMENT_SMASH_STRUCTURES + + can_ghost_into = FALSE + desc_short = "A deadly robot." + + emote_taunt = null + emote_taunt_sound = null + aggrosound = null + idlesound = null + loot = list( /obj/effect/decal/cleanable/robot_debris, /obj/item/stack/crafting/electronicparts/three, /obj/item/stock_parts/cell/ammo/mfc - ) - - emote_taunt_sound = FALSE - emote_taunt = FALSE - - aggrosound = FALSE - idlesound = FALSE - desc_short = "A sexy robot." + ) + + // Z-movement - can climb + can_z_move = TRUE + can_climb_ladders = TRUE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 30 + + // Pure melee - fast close combat + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 -/mob/living/simple_animal/hostile/handy/assaultron/nsb //NSB + Raider Bunker specific. +// NSB ASSAULTRON +/mob/living/simple_animal/hostile/handy/assaultron/nsb name = "assaultron" aggro_vision_range = 15 faction = list("raider") obj_damage = 300 can_ghost_into = FALSE -/mob/living/simple_animal/hostile/handy/assaultron/martha //Tipton specific +// MARTHA - unique named assaultron +/mob/living/simple_animal/hostile/handy/assaultron/martha name = "lil martha" - desc = "A deadly close combat robot developed by RobCo and covered in thick, dark blue armor plating, the name lil martha scratched onto it" + desc = "A deadly close combat robot developed by RobCo and covered in thick, dark blue armor plating, the name 'lil martha' scratched onto it." + + mob_armor = ARMOR_VALUE_ROBOT_MILITARY_HEAVY + maxHealth = 500 health = 500 aggro_vision_range = 15 - faction = list("hostile") obj_damage = 300 + + faction = list("hostile") can_ghost_into = FALSE - color = "#3444C8" //dark blue - emp_flags = list() //no emp instakill for you + color = "#3444C8" // Dark blue + + emp_flags = list() // No EMP instakill +// PLAYABLE ASSAULTRON /mob/living/simple_animal/hostile/handy/assaultron/playable see_in_dark = 8 force_threshold = 15 - wander = 0 + wander = FALSE anchored = FALSE del_on_death = FALSE - possible_a_intents = list(INTENT_HELP, INTENT_HARM, INTENT_GRAB, INTENT_DISARM) dextrous = TRUE - deathmessage = "abruptly shuts down, falling to the ground!" can_ghost_into = FALSE + deathmessage = "abruptly shuts down, falling to the ground!" + possible_a_intents = list(INTENT_HELP, INTENT_HARM, INTENT_GRAB, INTENT_DISARM) +// SA-S-E - medical assaultron /mob/living/simple_animal/hostile/handy/assaultron/playable/medical name = "SA-S-E" - desc = "An Assaultron modified for the Medical field, SA-S-E forgoes the weaponry and deadliness of her military countarparts to save lives. Painted white with blue highlights, and a blue cross on the front of her visor, this robot comes equipped with what looks like modified medical gear. Her head has no eye-laser, instead a gently pulsing blue eye that scans people the analyze their health, a defibrilator on her back, and articulated hands to be able to use the myriad medical tools strapped to parts of her body under protective cases all show this model is meant to save lives. She's stockier than other Assaultrons due to all the added gear, and her legs seem much thicker than normal due to reinforced servos and gears." + desc = "An Assaultron modified for the Medical field, SA-S-E forgoes the weaponry and deadliness of her military counterparts to save lives. Painted white with blue highlights, and a blue cross on the front of her visor, this robot comes equipped with what looks like modified medical gear. Her head has no eye-laser, instead a gently pulsing blue eye that scans people to analyze their health, a defibrillator on her back, and articulated hands to be able to use the myriad medical tools strapped to parts of her body under protective cases all show this model is meant to save lives." icon_state = "assaultron_sase" icon_dead = "assaultron_sase_dead" -//Junkers -/mob/living/simple_animal/hostile/handy/gutsy/flamer - name = "Mr. Burnsy" - desc = "A modified mr. gutsy, equipped with a more precise flamer, ditching it's plasma weaponry." - color = "#B85C00" - projectilesound = 'sound/magic/fireball.ogg' - projectiletype = /obj/item/projectile/bullet/incendiary/shotgun - extra_projectiles = 1 - can_ghost_into = FALSE - +// RED EYE ASSAULTRON - laser eye variant /mob/living/simple_animal/hostile/handy/assaultron/laser name = "red eye assaultron" - desc = "A modified assaultron. It's eye has been outfitted with a deadly laser." + desc = "A modified assaultron. Its eye has been outfitted with a deadly laser." color = "#B85C00" + can_ghost_into = FALSE + + // Mixed - laser at range, claws up close + combat_mode = COMBAT_MODE_MIXED ranged = TRUE - retreat_distance = null + retreat_distance = 4 minimum_distance = 1 - projectilesound = 'sound/weapons/laser.ogg' + + ranged_cooldown_time = 3 SECONDS projectiletype = /obj/item/projectile/beam/laser/lasgun + projectilesound = 'sound/weapons/laser.ogg' projectile_sound_properties = list( SP_VARY(FALSE), SP_VOLUME(LASER_VOLUME), @@ -579,5 +692,3 @@ SP_DISTANT_SOUND(LASER_DISTANT_SOUND), SP_DISTANT_RANGE(LASER_RANGE_DISTANT) ) - can_ghost_into = FALSE - diff --git a/code/modules/mob/living/simple_animal/hostile/f13/wolf.dm b/code/modules/mob/living/simple_animal/hostile/f13/wolf.dm index 9aeb9ccaafa..1629403aefe 100644 --- a/code/modules/mob/living/simple_animal/hostile/f13/wolf.dm +++ b/code/modules/mob/living/simple_animal/hostile/f13/wolf.dm @@ -1,6 +1,10 @@ -//Fallout 13 canine directory +// In this document: Feral dogs, Alpha dogs, Wolves, Unique named dogs -// Feral dog - visually some sort of mutt, at some point a coyote style dog can be made from the previous dog sprite, saved as coyote +/////////////// +// FERAL DOG // +/////////////// + +// BASE FERAL DOG /mob/living/simple_animal/hostile/wolf name = "feral dog" desc = "The dogs that survived the Great War are a larger, and tougher breed, size of a wolf.
This one seems to be severely malnourished and its eyes are bloody red." @@ -9,89 +13,183 @@ icon_living = "dog_feral" icon_dead = "dog_feral_dead" icon_gib = "gib" + mob_biotypes = MOB_ORGANIC|MOB_BEAST - turns_per_move = 1 - response_help_simple = "pets" - response_disarm_simple = "pushes aside" - response_harm_simple = "kicks" + maxHealth = 50 health = 50 - faction = list("hostile", "wolf") - environment_smash = 0 - guaranteed_butcher_results = list(/obj/item/stack/sheet/animalhide/wolf = 1, /obj/item/reagent_containers/food/snacks/meat/slab/wolf = 1,/obj/item/stack/sheet/bone = 1) + move_to_delay = 2.5 + turns_per_move = 1 + melee_damage_lower = 8 melee_damage_upper = 16 + aggro_vision_range = 15 + + // Canine predators with natural night vision + has_low_light_vision = TRUE + + guaranteed_butcher_results = list( + /obj/item/stack/sheet/animalhide/wolf = 1, + /obj/item/reagent_containers/food/snacks/meat/slab/wolf = 1, + /obj/item/stack/sheet/bone = 1 + ) + waddle_amount = 2 - waddle_up_time = 0 //Dogs can't look up ~TK + waddle_up_time = 0 // Dogs can't look up waddle_side_time = 1 -// idle_vision_range = 7 + footstep_type = FOOTSTEP_MOB_BAREFOOT + + response_help_simple = "pets" + response_disarm_simple = "pushes aside" + response_harm_simple = "kicks" attack_verb_simple = "bites" attack_sound = 'sound/weapons/bite.ogg' - move_to_delay = 2 - footstep_type = FOOTSTEP_MOB_BAREFOOT - - emote_taunt_sound = list('sound/f13npc/dog/dog_charge1.ogg', 'sound/f13npc/dog/dog_charge2.ogg', 'sound/f13npc/dog/dog_charge3.ogg', 'sound/f13npc/dog/dog_charge4.ogg', 'sound/f13npc/dog/dog_charge5.ogg', 'sound/f13npc/dog/dog_charge6.ogg', 'sound/f13npc/dog/dog_charge7.ogg',) + emote_taunt = list("growls", "barks", "snarls") + emote_taunt_sound = list( + 'sound/f13npc/dog/dog_charge1.ogg', + 'sound/f13npc/dog/dog_charge2.ogg', + 'sound/f13npc/dog/dog_charge3.ogg', + 'sound/f13npc/dog/dog_charge4.ogg', + 'sound/f13npc/dog/dog_charge5.ogg', + 'sound/f13npc/dog/dog_charge6.ogg', + 'sound/f13npc/dog/dog_charge7.ogg' + ) taunt_chance = 30 - aggrosound = list('sound/f13npc/dog/dog_alert1.ogg', 'sound/f13npc/dog/dog_alert2.ogg', 'sound/f13npc/dog/dog_alert3.ogg') - idlesound = list('sound/f13npc/dog/dog_bark1.ogg', 'sound/f13npc/dog/dog_bark2.ogg', 'sound/f13npc/dog/dog_bark3.ogg') + aggrosound = list( + 'sound/f13npc/dog/dog_alert1.ogg', + 'sound/f13npc/dog/dog_alert2.ogg', + 'sound/f13npc/dog/dog_alert3.ogg' + ) + idlesound = list( + 'sound/f13npc/dog/dog_bark1.ogg', + 'sound/f13npc/dog/dog_bark2.ogg', + 'sound/f13npc/dog/dog_bark3.ogg' + ) death_sound = 'sound/f13npc/centaur/centaur_death.ogg' + + faction = list("hostile", "wolf") + a_intent = INTENT_HARM + gold_core_spawnable = HOSTILE_SPAWN + + can_ghost_into = TRUE + pop_required_to_jump_into = SMALL_MOB_MIN_PLAYERS + + // Z-movement + can_z_move = TRUE + can_climb_ladders = FALSE + can_climb_stairs = TRUE + can_jump_down = TRUE + z_move_delay = 20 // Fast runner + + can_open_doors = FALSE + can_open_airlocks = FALSE + + // Pure melee - rush and bite + combat_mode = COMBAT_MODE_MELEE + retreat_distance = null + minimum_distance = 1 + +/mob/living/simple_animal/hostile/wolf/Aggro() + . = ..() + if(.) + return + summon_backup(8) + +/mob/living/simple_animal/hostile/wolf/CanAttack(atom/the_target) + if(isliving(the_target)) + var/mob/living/L = the_target + if(L.stat >= UNCONSCIOUS) + return FALSE + return ..() +// PLAYABLE FERAL DOG /mob/living/simple_animal/hostile/wolf/playable - health = 150 maxHealth = 150 + health = 150 + see_in_dark = 8 + wander = FALSE + anchored = FALSE + del_on_death = FALSE emote_taunt_sound = null emote_taunt = null aggrosound = null idlesound = null - see_in_dark = 8 - wander = 0 - anchored = FALSE possible_a_intents = list(INTENT_HELP, INTENT_HARM) - footstep_type = FOOTSTEP_MOB_BAREFOOT -// Alpha dog +// ALPHA DOG - pack leader /mob/living/simple_animal/hostile/wolf/alpha name = "alpha feral dog" desc = "The dogs that survived the Great War are a larger, and tougher breed, size of a wolf.
Wait... This one's a wolf!" icon_state = "dog_alpha" icon_living = "dog_alpha" icon_dead = "dog_alpha_dead" - guaranteed_butcher_results = list(/obj/item/stack/sheet/animalhide/wolf = 2, /obj/item/reagent_containers/food/snacks/meat/slab/wolf = 3,/obj/item/stack/sheet/bone = 2) + maxHealth = 70 health = 70 + melee_damage_lower = 12 melee_damage_upper = 28 - footstep_type = FOOTSTEP_MOB_BAREFOOT + + guaranteed_butcher_results = list( + /obj/item/stack/sheet/animalhide/wolf = 2, + /obj/item/reagent_containers/food/snacks/meat/slab/wolf = 3, + /obj/item/stack/sheet/bone = 2 + ) + + pop_required_to_jump_into = MED_MOB_MIN_PLAYERS + +/mob/living/simple_animal/hostile/wolf/alpha/Aggro() + . = ..() + if(.) + return + summon_backup(12) // Alpha calls more backup + +// PLAYABLE ALPHA DOG /mob/living/simple_animal/hostile/wolf/alpha/playable - health = 70 - maxHealth = 70 + see_in_dark = 8 + wander = FALSE + anchored = FALSE + del_on_death = FALSE emote_taunt_sound = null emote_taunt = null aggrosound = null idlesound = null - see_in_dark = 8 - wander = 0 - anchored = FALSE possible_a_intents = list(INTENT_HELP, INTENT_HARM) - footstep_type = FOOTSTEP_MOB_BAREFOOT -// The first proper wolf, got to love just relabels without repathing. + +////////// +// WOLF // +////////// + +// WOLF - proper wolf, tougher than a feral dog /mob/living/simple_animal/hostile/wolf/cold name = "wolf" desc = "A mangy wolf." icon_state = "wolf" icon_living = "wolf" icon_dead = "wolf_dead" - guaranteed_butcher_results = list(/obj/item/stack/sheet/animalhide/wolf = 2, /obj/item/reagent_containers/food/snacks/meat/slab/wolf = 3,/obj/item/stack/sheet/bone = 2) + maxHealth = 100 health = 100 + melee_damage_lower = 20 melee_damage_upper = 28 - footstep_type = FOOTSTEP_MOB_BAREFOOT -//Unique Dogs - Guerilla for Khans is a Rottweiler, Brutus and Lupa german shepherds, Sniffs-the-Earth a sheepdog. -//Feel free to move or add code for different behaviours like sleep, some unused sprites prepped for that sort of thing. + guaranteed_butcher_results = list( + /obj/item/stack/sheet/animalhide/wolf = 2, + /obj/item/reagent_containers/food/snacks/meat/slab/wolf = 3, + /obj/item/stack/sheet/bone = 2 + ) + + pop_required_to_jump_into = MED_MOB_MIN_PLAYERS + +///////////////// +// UNIQUE DOGS // +///////////////// + +// Unique named dogs - playable characters. +// Guerilla (Khan) = Rottweiler, Brutus/Lupa = German Shepherds, Sniffs-the-Earth = Sheepdog. /mob/living/simple_animal/hostile/wolf/playable/rottweiler icon_state = "rottweiler" diff --git a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm index 36de5a1238f..641c75edcd5 100644 --- a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm +++ b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm @@ -15,7 +15,7 @@ if(L.reagents) L.reagents.add_reagent(poison_type, poison_per_bite) -//basic spider mob, these generally guard nests +// Basic spider mob, these generally guard nests /mob/living/simple_animal/hostile/poison/giant_spider name = "radspider" desc = "Furry and black, it makes you shudder to look at it. This one has deep red eyes." @@ -52,10 +52,10 @@ see_in_dark = 4 lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE footstep_type = FOOTSTEP_MOB_CLAW - has_field_of_vision = FALSE // 360° vision. + has_field_of_vision = FALSE // 360° vision var/playable_spider = FALSE var/datum/action/innate/spider/lay_web/lay_web - var/directive = "" //Message passed down to children, to relay the creator's orders + var/directive = "" // Message passed down to children to relay the creator's orders /mob/living/simple_animal/hostile/poison/giant_spider/Initialize() . = ..() @@ -63,22 +63,20 @@ lay_web.Grant(src) /mob/living/simple_animal/hostile/poison/giant_spider/Destroy() - // Remove from global list FIRST GLOB.spidermobs -= src - // Clear ranged ability reference without calling remove (which may try to access src) + // Clear ranged ability reference if(ranged_ability) if(ranged_ability.ranged_ability_user == src) ranged_ability.ranged_ability_user = null ranged_ability = null - // Clear ALL abilities with proper cleanup + // Qdel all ability objects with proper reference cleanup if(LAZYLEN(abilities)) var/list/abilities_copy = abilities.Copy() - abilities = null // Clear the list first to break references + abilities = null for(var/obj/effect/proc_holder/ability in abilities_copy) - // Clear cross-references if(ability.ranged_ability_user == src) ability.ranged_ability_user = null if(ability.action) @@ -86,16 +84,14 @@ ability.action.owner = null qdel(ability) - // Clear the lay_web action + // Clear lay_web action (datum/action/innate - not in abilities list) if(lay_web) lay_web.Remove(src) if(lay_web.owner == src) lay_web.owner = null QDEL_NULL(lay_web) - // Clear directive directive = null - return ..() /mob/living/simple_animal/hostile/poison/giant_spider/Topic(href, href_list) @@ -105,7 +101,8 @@ humanize_spider(ghost) /mob/living/simple_animal/hostile/poison/giant_spider/Login() - ..() + . = ..() + GLOB.spidermobs[src] = TRUE if(directive) to_chat(src, span_notice("Your mother left you a directive! Follow it at all costs.")) to_chat(src, "[directive]") @@ -117,7 +114,7 @@ humanize_spider(user) /mob/living/simple_animal/hostile/poison/giant_spider/proc/humanize_spider(mob/user) - if(key || !playable_spider || stat)//Someone is in it, it's dead, or the fun police are shutting it down + if(key || !playable_spider || stat) return FALSE if(isobserver(user)) var/mob/dead/observer/O = user @@ -132,7 +129,27 @@ user.transfer_ckey(src, FALSE) return TRUE -//nursemaids - these create webs and eggs +/mob/living/simple_animal/hostile/poison/giant_spider/handle_automated_action() + if(!..()) // AIStatus is off + return 0 + if(AIStatus == AI_IDLE) + // 1% chance to skitter madly - only when not busy doing something + if(!busy && prob(1)) + stop_automated_movement = 1 + Goto(pick(urange(20, src, 1)), move_to_delay) + addtimer(CALLBACK(src, PROC_REF(stop_skitter)), 5 SECONDS, TIMER_DELETE_ME) + return 1 + +/mob/living/simple_animal/hostile/poison/giant_spider/proc/stop_skitter() + if(!busy) // Don't interrupt if we've started doing something + stop_automated_movement = 0 + walk(src, 0) + +// ============================================================ +// NURSE SPIDER +// Nursemaids create webs, cocoon prey, and lay eggs +// ============================================================ + /mob/living/simple_animal/hostile/poison/giant_spider/nurse desc = "Furry and black, it makes you shudder to look at it. This one has brilliant green eyes." icon_state = "nurse" @@ -150,7 +167,7 @@ var/obj/effect/proc_holder/wrap/wrap var/datum/action/innate/spider/lay_eggs/lay_eggs var/datum/action/innate/spider/set_directive/set_directive - var/static/list/consumed_mobs = list() //the tags of mobs that have been consumed by nurse spiders to lay eggs + var/static/list/consumed_mobs = list() // Tags of mobs consumed to prevent re-feeding /mob/living/simple_animal/hostile/poison/giant_spider/nurse/Initialize() . = ..() @@ -161,16 +178,15 @@ set_directive = new set_directive.Grant(src) - /mob/living/simple_animal/hostile/poison/giant_spider/nurse/Destroy() - // Clear cocoon target reference cocoon_target = null - // CRITICAL FIX: Do NOT qdel the wrap here - let parent handle it - // Just clear the reference to break the cycle + // wrap is in the abilities list - parent giant_spider/Destroy() will qdel it + // Just clear our reference so we don't double-free wrap = null - // Clear actions with proper cleanup + // lay_eggs and set_directive are datum/action/innate - NOT in abilities list + // Must be cleaned up explicitly here if(lay_eggs) lay_eggs.Remove(src) if(lay_eggs.owner == src) @@ -185,7 +201,148 @@ return ..() -//hunters have the most poison and move the fastest, so they can find prey +/mob/living/simple_animal/hostile/poison/giant_spider/nurse/proc/GiveUp(atom/movable/C) + addtimer(CALLBACK(src, PROC_REF(do_give_up), C), 10 SECONDS, TIMER_DELETE_ME) + +/mob/living/simple_animal/hostile/poison/giant_spider/nurse/proc/do_give_up(atom/movable/C) + if(busy == MOVING_TO_TARGET && cocoon_target == C && get_dist(src, cocoon_target) > 1) + cocoon_target = null + busy = SPIDER_IDLE + stop_automated_movement = 0 + +/mob/living/simple_animal/hostile/poison/giant_spider/nurse/handle_automated_action() + if(!..()) + busy = SPIDER_IDLE + stop_automated_movement = FALSE + return 0 + + if(busy == MOVING_TO_TARGET && cocoon_target) + if(QDELETED(cocoon_target)) + cocoon_target = null + busy = SPIDER_IDLE + stop_automated_movement = FALSE + else if(get_dist(src, cocoon_target) <= 1) + cocoon() + return 1 + + if(!busy && prob(30)) + var/list/nearby = view(src, 10) // Renamed from can_see to avoid shadowing proc + + // Priority 1: Cocoon an incapacitated living mob + for(var/mob/living/C in nearby) + if(C.stat && !istype(C, /mob/living/simple_animal/hostile/poison/giant_spider) && !C.anchored) + cocoon_target = C + busy = MOVING_TO_TARGET + Goto(C, move_to_delay) + GiveUp(C) + return 1 + + // Priority 2: Spin a web on this tile if there isn't one + var/obj/structure/spider/stickyweb/W = locate() in get_turf(src) + if(!W) + lay_web.Activate() + return 1 + + // Priority 3: Lay eggs if we're fed + if(fed) + lay_eggs.Activate() + return 1 + + // Priority 4: Cocoon nearby items + for(var/obj/O in nearby) + if(O.anchored) + continue + if(isitem(O) || isstructure(O) || ismachinery(O)) + cocoon_target = O + busy = MOVING_TO_TARGET + stop_automated_movement = 1 + Goto(O, move_to_delay) + GiveUp(O) + return 1 // BUG FIX: break after first target, don't overwrite with last obj in loop + + return 1 + +/mob/living/simple_animal/hostile/poison/giant_spider/nurse/proc/cocoon() + if(stat == DEAD) + return + + if(!cocoon_target || QDELETED(cocoon_target)) + cocoon_target = null + busy = SPIDER_IDLE + stop_automated_movement = FALSE + return + + if(cocoon_target.anchored) + cocoon_target = null + busy = SPIDER_IDLE + stop_automated_movement = FALSE + return + + if(cocoon_target == src) + to_chat(src, span_warning("You can't wrap yourself!")) + cocoon_target = null + busy = SPIDER_IDLE + stop_automated_movement = FALSE + return + + if(istype(cocoon_target, /mob/living/simple_animal/hostile/poison/giant_spider)) + to_chat(src, span_warning("You can't wrap other spiders!")) + cocoon_target = null + busy = SPIDER_IDLE + stop_automated_movement = FALSE + return + + if(!Adjacent(cocoon_target)) + to_chat(src, span_warning("You can't reach [cocoon_target]!")) + cocoon_target = null + busy = SPIDER_IDLE + stop_automated_movement = FALSE + return + + if(busy == SPINNING_COCOON) + return // Already spinning, don't restart + + busy = SPINNING_COCOON + visible_message( + span_notice("[src] begins to secrete a sticky substance around [cocoon_target]."), + span_notice("You begin wrapping [cocoon_target] into a cocoon.") + ) + stop_automated_movement = TRUE + walk(src, 0) + + if(do_after(src, 50, target = cocoon_target)) + // SAFETY: re-check target validity after the do_after sleep + if(busy == SPINNING_COCOON && cocoon_target && !QDELETED(cocoon_target) && !cocoon_target.anchored) + var/obj/structure/spider/cocoon/C = new(cocoon_target.loc) + + if(isliving(cocoon_target)) + var/mob/living/L = cocoon_target + if(L.blood_volume && (L.stat != DEAD || !consumed_mobs[L.tag])) + consumed_mobs[L.tag] = TRUE + fed++ + lay_eggs.UpdateButtonIcon(TRUE) + visible_message( + span_danger("[src] sticks a proboscis into [L] and sucks a viscous substance out."), + span_notice("You suck the nutriment out of [L], feeding you enough to lay a cluster of eggs.") + ) + L.death() + else + to_chat(src, span_warning("[L] cannot sate your hunger!")) + + cocoon_target.forceMove(C) + + if(cocoon_target.density || ismob(cocoon_target)) + C.icon_state = pick("cocoon_large1", "cocoon_large2", "cocoon_large3") + + cocoon_target = null + busy = SPIDER_IDLE + stop_automated_movement = FALSE + +// ============================================================ +// HUNTER SPIDER +// Fastest, most venomous - hunts prey for the nest +// ============================================================ + /mob/living/simple_animal/hostile/poison/giant_spider/hunter desc = "Furry and black, it makes you shudder to look at it. This one has sparkling purple eyes." icon_state = "hunter" @@ -198,7 +355,7 @@ poison_per_bite = 5 move_to_delay = 5 -//vipers are the rare variant of the hunter, no IMMEDIATE damage but so much poison medical care will be needed fast. +// Viper: glass cannon variant - almost no physical damage, lethal poison /mob/living/simple_animal/hostile/poison/giant_spider/hunter/viper name = "radspider viper" desc = "Furry and black, it makes you shudder to look at it. This one has effervescent purple eyes." @@ -211,18 +368,22 @@ melee_damage_upper = 1 poison_per_bite = 12 move_to_delay = 4 - poison_type = /datum/reagent/toxin/venom //all in venom, glass cannon. you bite 5 times and they are DEFINITELY dead, but 40 health and you are extremely obvious. Ambush, maybe? + poison_type = /datum/reagent/toxin/venom speed = 1 gold_core_spawnable = NO_SPAWN -//tarantulas are really tanky, regenerating (maybe), hulky monster but are also extremely slow, so. +// ============================================================ +// TARANTULA +// Extremely tanky, extremely slow +// ============================================================ + /mob/living/simple_animal/hostile/poison/giant_spider/tarantula name = "radtarantula" desc = "Furry and black, it makes you shudder to look at it. This one has abyssal red eyes." icon_state = "tarantula" icon_living = "tarantula" icon_dead = "tarantula_dead" - maxHealth = 300 // woah nelly + maxHealth = 300 health = 300 melee_damage_lower = 35 melee_damage_upper = 40 @@ -236,15 +397,19 @@ /mob/living/simple_animal/hostile/poison/giant_spider/tarantula/Moved(atom/oldloc, dir) . = ..() - if(slowed_by_webs) - if(!(locate(/obj/structure/spider/stickyweb) in loc)) - remove_movespeed_modifier(/datum/movespeed_modifier/tarantula_web) - slowed_by_webs = FALSE - else if(locate(/obj/structure/spider/stickyweb) in loc) + var/on_web = !!(locate(/obj/structure/spider/stickyweb) in loc) + if(slowed_by_webs && !on_web) + remove_movespeed_modifier(/datum/movespeed_modifier/tarantula_web) + slowed_by_webs = FALSE + else if(!slowed_by_webs && on_web) add_movespeed_modifier(/datum/movespeed_modifier/tarantula_web) slowed_by_webs = TRUE -//midwives are the queen of the spiders, can send messages to all them and web faster. That rare round where you get a queen spider and turn your 'for honor' players into 'r6siege' players will be a fun one. +// ============================================================ +// MIDWIFE (QUEEN) +// Alpha of the spider colony - communicates with all spiders +// ============================================================ + /mob/living/simple_animal/hostile/poison/giant_spider/nurse/midwife name = "radspider midwife" desc = "Furry and black, it makes you shudder to look at it. This one has scintillating green eyes." @@ -263,22 +428,25 @@ letmetalkpls.Grant(src) /mob/living/simple_animal/hostile/poison/giant_spider/nurse/midwife/Destroy() - // Clear communication action if(letmetalkpls) letmetalkpls.Remove(src) if(letmetalkpls.owner == src) letmetalkpls.owner = null QDEL_NULL(letmetalkpls) - return ..() -/mob/living/simple_animal/hostile/poison/giant_spider/ice //spiders dont usually like tempatures of 140 kelvin who knew +// ============================================================ +// ICE VARIANTS +// Survive in vacuum/extreme cold, inflict frostoil poison +// ============================================================ + +/mob/living/simple_animal/hostile/poison/giant_spider/ice name = "giant ice radspider" atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) minbodytemp = 0 maxbodytemp = 1500 poison_type = /datum/reagent/consumable/frostoil - color = rgb(114,228,250) + color = rgb(114, 228, 250) gold_core_spawnable = NO_SPAWN /mob/living/simple_animal/hostile/poison/giant_spider/nurse/ice @@ -287,7 +455,7 @@ minbodytemp = 0 maxbodytemp = 1500 poison_type = /datum/reagent/consumable/frostoil - color = rgb(114,228,250) + color = rgb(114, 228, 250) gold_core_spawnable = NO_SPAWN /mob/living/simple_animal/hostile/poison/giant_spider/hunter/ice @@ -296,118 +464,18 @@ minbodytemp = 0 maxbodytemp = 1500 poison_type = /datum/reagent/consumable/frostoil - color = rgb(114,228,250) + color = rgb(114, 228, 250) gold_core_spawnable = NO_SPAWN -/mob/living/simple_animal/hostile/poison/giant_spider/handle_automated_action() - if(!..()) //AIStatus is off - return 0 - if(AIStatus == AI_IDLE) - //1% chance to skitter madly away - if(!busy && prob(1)) - stop_automated_movement = 1 - Goto(pick(urange(20, src, 1)), move_to_delay) - spawn(50) - stop_automated_movement = 0 - walk(src,0) - return 1 - -/mob/living/simple_animal/hostile/poison/giant_spider/nurse/proc/GiveUp(C) - spawn(100) - if(busy == MOVING_TO_TARGET) - if(cocoon_target == C && get_dist(src,cocoon_target) > 1) - cocoon_target = null - busy = FALSE - stop_automated_movement = 0 - -/mob/living/simple_animal/hostile/poison/giant_spider/nurse/handle_automated_action() - if(..()) - var/list/can_see = view(src, 10) - if(!busy && prob(30)) //30% chance to stop wandering and do something - //first, check for potential food nearby to cocoon - for(var/mob/living/C in can_see) - if(C.stat && !istype(C, /mob/living/simple_animal/hostile/poison/giant_spider) && !C.anchored) - cocoon_target = C - busy = MOVING_TO_TARGET - Goto(C, move_to_delay) - //give up if we can't reach them after 10 seconds - GiveUp(C) - return - - //second, spin a sticky spiderweb on this tile - var/obj/structure/spider/stickyweb/W = locate() in get_turf(src) - if(!W) - lay_web.Activate() - else - //third, lay an egg cluster there - if(fed) - lay_eggs.Activate() - else - //fourthly, cocoon any nearby items so those pesky pinkskins can't use them - for(var/obj/O in can_see) - - if(O.anchored) - continue - - if(isitem(O) || isstructure(O) || ismachinery(O)) - cocoon_target = O - busy = MOVING_TO_TARGET - stop_automated_movement = 1 - Goto(O, move_to_delay) - //give up if we can't reach them after 10 seconds - GiveUp(O) - - else if(busy == MOVING_TO_TARGET && cocoon_target) - if(get_dist(src, cocoon_target) <= 1) - cocoon() - - else - busy = SPIDER_IDLE - stop_automated_movement = FALSE - -/mob/living/simple_animal/hostile/poison/giant_spider/nurse/proc/cocoon() - if(stat != DEAD && cocoon_target && !cocoon_target.anchored) - if(cocoon_target == src) - to_chat(src, span_warning("You can't wrap yourself!")) - return - if(istype(cocoon_target, /mob/living/simple_animal/hostile/poison/giant_spider)) - to_chat(src, span_warning("You can't wrap other spiders!")) - return - if(!Adjacent(cocoon_target)) - to_chat(src, span_warning("You can't reach [cocoon_target]!")) - return - if(busy == SPINNING_COCOON) - to_chat(src, span_warning("You're already spinning a cocoon!")) - return //we're already doing this, don't cancel out or anything - busy = SPINNING_COCOON - visible_message(span_notice("[src] begins to secrete a sticky substance around [cocoon_target]."),span_notice("You begin wrapping [cocoon_target] into a cocoon.")) - stop_automated_movement = TRUE - walk(src,0) - if(do_after(src, 50, target = cocoon_target)) - if(busy == SPINNING_COCOON) - var/obj/structure/spider/cocoon/C = new(cocoon_target.loc) - if(isliving(cocoon_target)) - var/mob/living/L = cocoon_target - if(L.blood_volume && (L.stat != DEAD || !consumed_mobs[L.tag])) //if they're not dead, you can consume them anyway - consumed_mobs[L.tag] = TRUE - fed++ - lay_eggs.UpdateButtonIcon(TRUE) - visible_message(span_danger("[src] sticks a proboscis into [L] and sucks a viscous substance out."),span_notice("You suck the nutriment out of [L], feeding you enough to lay a cluster of eggs.")) - L.death() //you just ate them, they're dead. - else - to_chat(src, span_warning("[L] cannot sate your hunger!")) - cocoon_target.forceMove(C) - - if(cocoon_target.density || ismob(cocoon_target)) - C.icon_state = pick("cocoon_large1","cocoon_large2","cocoon_large3") - cocoon_target = null - busy = SPIDER_IDLE - stop_automated_movement = FALSE +// ============================================================ +// SPIDER ACTIONS +// ============================================================ /datum/action/innate/spider icon_icon = 'icons/mob/actions/actions_animal.dmi' background_icon_state = "bg_alien" +// Spin Web action - available to all giant_spiders /datum/action/innate/spider/lay_web name = "Spin Web" desc = "Spin a web to slow down potential prey." @@ -423,23 +491,27 @@ return var/turf/T = get_turf(S) - var/obj/structure/spider/stickyweb/W = locate() in T - if(W) + if(locate(/obj/structure/spider/stickyweb) in T) to_chat(S, span_warning("There's already a web here!")) return - if(S.busy != SPINNING_WEB) - S.busy = SPINNING_WEB - S.visible_message(span_notice("[S] begins to secrete a sticky substance."),span_notice("You begin to lay a web.")) - S.stop_automated_movement = TRUE - if(do_after(S, 40, target = T)) - if(S.busy == SPINNING_WEB && S.loc == T) - new /obj/structure/spider/stickyweb(T) - S.busy = SPIDER_IDLE - S.stop_automated_movement = FALSE - else + if(S.busy == SPINNING_WEB) to_chat(S, span_warning("You're already spinning a web!")) + return + S.busy = SPINNING_WEB + S.visible_message( + span_notice("[S] begins to secrete a sticky substance."), + span_notice("You begin to lay a web.") + ) + S.stop_automated_movement = TRUE + if(do_after(S, 40, target = T)) + if(S.busy == SPINNING_WEB && S.loc == T) + new /obj/structure/spider/stickyweb(T) + S.busy = SPIDER_IDLE + S.stop_automated_movement = FALSE + +// Wrap ability - nurse spider only (obj/effect/proc_holder, added via AddAbility) /obj/effect/proc_holder/wrap name = "Wrap" panel = "Spider" @@ -457,35 +529,11 @@ action = new(src) /obj/effect/proc_holder/wrap/Destroy() - // CRITICAL: Clear ALL references that might keep this alive - - // Clear from user's ranged ability AND click intercept - if(ranged_ability_user) - // Remove from their abilities list - if(ranged_ability_user.abilities) - ranged_ability_user.abilities -= src - // If we're their active ranged ability, clear it - if(ranged_ability_user.ranged_ability == src) - ranged_ability_user.ranged_ability = null - // If we're their click intercept, clear it - if(ranged_ability_user.click_intercept == src) - ranged_ability_user.click_intercept = null - ranged_ability_user = null - - // Clear the action completely + // action was created in Initialize() - qdel it explicitly to prevent leak if(action) - // Remove from owner's action list - if(action.owner) - if(action.owner.actions) - action.owner.actions -= action + if(action.owner == src) action.owner = null - // Delete the action QDEL_NULL(action) - - // Clear any other vars - active = FALSE - has_action = FALSE - return ..() /obj/effect/proc_holder/wrap/update_icon() @@ -499,13 +547,10 @@ return TRUE /obj/effect/proc_holder/wrap/proc/activate(mob/living/user) - var/message if(active) - message = span_notice("You no longer prepare to wrap something in a cocoon.") - remove_ranged_ability(message) + remove_ranged_ability(span_notice("You no longer prepare to wrap something in a cocoon.")) else - message = "You prepare to wrap something in a cocoon. Left-click your target to start wrapping!" - add_ranged_ability(user, message, TRUE) + add_ranged_ability(user, "You prepare to wrap something in a cocoon. Left-click your target to start wrapping!", TRUE) return 1 /obj/effect/proc_holder/wrap/InterceptClickOn(mob/living/caller, params, atom/target) @@ -528,9 +573,9 @@ /obj/effect/proc_holder/wrap/on_lose(mob/living/carbon/user) remove_ranged_ability() - // Don't set ranged_ability_user to null here - let Destroy() handle it ..() +// Lay Eggs action - nurse spider only /datum/action/innate/spider/lay_eggs name = "Lay Eggs" desc = "Lay a cluster of eggs, which will soon grow into more spiders. You must wrap a living being to do this." @@ -542,40 +587,45 @@ if(!istype(owner, /mob/living/simple_animal/hostile/poison/giant_spider/nurse)) return 0 var/mob/living/simple_animal/hostile/poison/giant_spider/nurse/S = owner - if(S.fed) - return 1 - return 0 + return S.fed ? 1 : 0 + return 0 /datum/action/innate/spider/lay_eggs/Activate() if(!istype(owner, /mob/living/simple_animal/hostile/poison/giant_spider/nurse)) return var/mob/living/simple_animal/hostile/poison/giant_spider/nurse/S = owner - var/obj/structure/spider/eggcluster/E = locate() in get_turf(S) - if(E) + if(locate(/obj/structure/spider/eggcluster) in get_turf(S)) to_chat(S, span_warning("There is already a cluster of eggs here!")) - else if(!S.fed) + return + if(!S.fed) to_chat(S, span_warning("You are too hungry to do this!")) - else if(S.busy != LAYING_EGGS) - S.busy = LAYING_EGGS - S.visible_message(span_notice("[S] begins to lay a cluster of eggs."),span_notice("You begin to lay a cluster of eggs.")) - S.stop_automated_movement = TRUE - if(do_after(S, 50, target = get_turf(S))) - if(S.busy == LAYING_EGGS) - E = locate() in get_turf(S) - if(!E || !isturf(S.loc)) - var/obj/structure/spider/eggcluster/C = new /obj/structure/spider/eggcluster(get_turf(S)) - if(S.ckey) - C.player_spiders = TRUE - C.directive = S.directive - C.poison_type = S.poison_type - C.poison_per_bite = S.poison_per_bite - C.faction = S.faction.Copy() - S.fed-- - UpdateButtonIcon(TRUE) - S.busy = SPIDER_IDLE - S.stop_automated_movement = FALSE + return + if(S.busy == LAYING_EGGS) + return + S.busy = LAYING_EGGS + S.visible_message( + span_notice("[S] begins to lay a cluster of eggs."), + span_notice("You begin to lay a cluster of eggs.") + ) + S.stop_automated_movement = TRUE + if(do_after(S, 50, target = get_turf(S))) + if(S.busy == LAYING_EGGS && isturf(S.loc)) + if(!(locate(/obj/structure/spider/eggcluster) in get_turf(S))) + var/obj/structure/spider/eggcluster/C = new /obj/structure/spider/eggcluster(get_turf(S)) + if(S.ckey) + C.player_spiders = TRUE + C.directive = S.directive + C.poison_type = S.poison_type + C.poison_per_bite = S.poison_per_bite + C.faction = S.faction.Copy() + S.fed-- + UpdateButtonIcon(TRUE) + S.busy = SPIDER_IDLE + S.stop_automated_movement = FALSE + +// Set Directive action - nurse spider only /datum/action/innate/spider/set_directive name = "Set Directive" desc = "Set a directive for your children to follow." @@ -588,10 +638,7 @@ var/mob/living/simple_animal/hostile/poison/giant_spider/nurse/S = owner S.directive = stripped_input(S, "Enter the new directive", "Create directive", "[S.directive]") -/mob/living/simple_animal/hostile/poison/giant_spider/Login() - . = ..() - GLOB.spidermobs[src] = TRUE - +// Spider Communication action - midwife only /datum/action/innate/spider/comm name = "Command" desc = "Send a command to all living spiders." @@ -612,15 +659,16 @@ /datum/action/innate/spider/comm/proc/spider_command(mob/living/user, message) if(!message) return - var/my_message - my_message = "Command from [user]: [message]" + var/my_message = "Command from [user]: [message]" for(var/mob/living/simple_animal/hostile/poison/giant_spider/M in GLOB.spidermobs) to_chat(M, my_message) for(var/M in GLOB.dead_mob_list) var/link = FOLLOW_LINK(M, user) to_chat(M, "[link] [my_message]") - usr.log_talk(message, LOG_SAY, tag="spider command") + usr.log_talk(message, LOG_SAY, tag = "spider command") +// Temperature damage override - spiders take brute from extreme temps +// (bypasses the normal fire/burn damage path) /mob/living/simple_animal/hostile/poison/giant_spider/handle_temperature_damage() if(bodytemperature < minbodytemp) adjustBruteLoss(20) diff --git a/code/modules/mob/living/simple_animal/hostile/headcrab.dm b/code/modules/mob/living/simple_animal/hostile/headcrab.dm index ecb394e5a43..cd321da54f5 100644 --- a/code/modules/mob/living/simple_animal/hostile/headcrab.dm +++ b/code/modules/mob/living/simple_animal/hostile/headcrab.dm @@ -49,7 +49,7 @@ return Infect(target) to_chat(src, span_userdanger("With our egg laid, our death approaches rapidly...")) - addtimer(CALLBACK(src, PROC_REF(death)), 100) + addtimer(CALLBACK(src, PROC_REF(death)), 100, TIMER_DELETE_ME) /obj/item/organ/body_egg/changeling_egg name = "changeling egg" diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm index 688a4bc0a38..a02dba6a578 100644 --- a/code/modules/mob/living/simple_animal/hostile/hostile.dm +++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm @@ -1,3 +1,8 @@ +// Combat mode defines +#define COMBAT_MODE_MELEE 1 // Pure melee, always rush in +#define COMBAT_MODE_RANGED 2 // Pure ranged, never voluntarily enter melee +#define COMBAT_MODE_MIXED 3 // Ranged but will melee if target gets close (like reaver) + /mob/living/simple_animal/hostile faction = list("hostile") stop_automated_movement_when_pulled = 0 @@ -77,6 +82,12 @@ var/robust_searching = 0 //By default, mobs have a simple searching method, set this to 1 for the more scrutinous searching (stat_attack, stat_exclusive, etc), should be disabled on most mobs var/vision_range = 9 //How big of an area to search for targets in, a vision of 9 attempts to find targets as soon as they walk into screen view var/aggro_vision_range = 9 //If a mob is aggro, we search in this radius. Defaults to 9 to keep in line with original simple mob aggro radius + + // LOW-LIGHT VISION: Some mobs (mutants, animals, nightkin) see better in darkness + var/has_low_light_vision = FALSE // Set to TRUE for mutants, animals, nightkin, deathclaws, etc. + var/low_light_bonus = 3 // Extra tiles of vision in darkness when has_low_light_vision is TRUE + var/debug_vision = FALSE // Set to TRUE to see cone detection messages in chat + var/search_objects = 0 //If we want to consider objects when searching around, set this to 1. If you want to search for objects while also ignoring mobs until hurt, set it to 2. To completely ignore mobs, even when attacked, set it to 3 var/search_objects_timer_id //Timer for regaining our old search_objects value after being attacked var/search_objects_regain_time = 30 //the delay between being attacked and gaining our old search_objects value back @@ -110,8 +121,14 @@ /// timer for despawning when lonely var/lonely_timer_id - /// Can this mob pursue targets across Z-levels? + /// Can this mob pursue targets across Z-levels? (Master toggle) var/can_z_move = TRUE + /// Can this mob climb ladders specifically? + var/can_climb_ladders = TRUE + /// Can this mob use stairs specifically? + var/can_climb_stairs = TRUE + /// Can this mob jump down through openspace? + var/can_jump_down = TRUE /// How long to wait before attempting Z pursuit (deciseconds) var/z_move_delay = 30 /// Last time we attempted Z-level movement @@ -119,6 +136,260 @@ /// What Z-level was our target last seen on? var/target_last_z = 0 + // Z-PURSUIT STATE TRACKING - prevents AI from getting confused during vertical movement + /// Are we currently in the middle of Z-level pursuit? Used to lock the mob into climbing mode + var/pursuing_z_target = FALSE + /// What structure (ladder/stairs) are we trying to reach for Z pursuit? + var/atom/z_pursuit_structure = null + /// When did we start Z-pursuit? Used for timeout + var/z_pursuit_started = 0 + /// Maximum time to spend in Z-pursuit before giving up (10 seconds) + var/z_pursuit_timeout = 100 + + /// Remembered target during Z-pursuit - won't lose them even if out of sight + var/atom/remembered_target = null + /// When did we last see our target? Used to prevent losing them during brief moments out of sight + var/last_target_sighting = 0 + /// How long to remember target after losing sight (deciseconds) - 15 seconds + var/target_memory_duration = 150 + + /// Are we in active search mode? + var/searching = FALSE + /// Timer ID for search mode + var/search_timer_id = null + /// How long to search after losing sight (deciseconds) - 30 seconds + var/search_duration = 300 + /// Last known location of target + var/turf/last_known_location = null + /// Search radius around last known location + var/search_radius = 7 + /// SMART SEARCH: Track visited search locations to avoid repetition + var/list/searched_turfs = list() + /// SMART SEARCH: Current search expansion level (increases over time) + var/search_expansion = 0 + /// SMART SEARCH: Doors we've already tried to open during search + var/list/searched_doors = list() + /// SMART SEARCH: Containers (closets/crates/lockers) we've searched + var/list/searched_containers = list() + /// SMART SEARCH: Recently opened door (to prioritize exploring beyond it) + var/atom/recently_opened_door = null + /// SMART SEARCH: Container we're currently moving toward to investigate + var/obj/structure/closet/investigating_container = null + /// SMART SEARCH: Timestamp when search started (for timeout) + var/search_start_time = 0 + /// SMART SEARCH: Base search timeout for non-aggressive mobs (2.5 minutes) + var/search_timeout_base = 1500 // 150 seconds = 2.5 minutes + /// SMART SEARCH: Extended search timeout for aggressive mobs (5 minutes) + var/search_timeout_aggressive = 3000 // 300 seconds = 5 minutes + /// SMART SEARCH: Last time we exited search mode + var/last_search_exit_time = 0 + /// SMART SEARCH: Cooldown before re-entering search (prevents spam loop) + var/search_entry_cooldown = 10 // 1 second + + /// How far to call for backup when finding a target (tiles) + var/backup_call_range = 2 + /// Cooldown between backup calls to prevent spam + COOLDOWN_DECLARE(backup_call_cooldown) + /// How long between backup calls (deciseconds) + var/backup_call_delay = 50 // 5 seconds + + /// Are we currently rallying to allies? + var/rallying = FALSE + /// When did we start rallying? + var/rally_start_time = 0 + /// How long to rally before advancing (deciseconds) - 3 seconds + var/rally_duration = 30 + /// Rally point location + var/turf/rally_point = null + + /// Last time we successfully changed Z-levels + var/last_successful_z_move = 0 + /// Cooldown after changing Z before we can pursue again (prevents stair loops) + var/z_move_success_cooldown = 30 // 3 seconds + + /// Can this mob open doors? + var/can_open_doors = FALSE + /// Can this mob open airlocks specifically? + var/can_open_airlocks = FALSE + /// Cooldown between door opening attempts + COOLDOWN_DECLARE(door_open_cooldown) + /// How long between door opening attempts + var/door_open_delay = 20 // 2 seconds + + /// CURIOSITY SYSTEM: Idle mobs occasionally investigate nearby closed doors/rooms + var/curiosity_enabled = TRUE + /// When did we last investigate out of curiosity? + var/last_curiosity_check = 0 + /// How often to consider investigating (deciseconds) - 10-20 seconds + var/curiosity_check_interval = 100 + /// Chance to actually investigate when checking (0-100) + var/curiosity_chance = 35 + + /// What Z-level are we committed to searching? + var/committed_z_level = 0 + /// When did we commit to this Z-level? + var/z_commit_time = 0 + /// How long to stay on a Z-level before switching (deciseconds) - 10 seconds + var/z_commit_duration = 100 + /// How many times have we climbed recently? (prevents yo-yoing) + var/recent_climbs = 0 + /// Last time we reset climb counter + var/last_climb_reset = 0 + + /// Can we hear combat sounds to locate targets? + var/can_hear_combat = TRUE // Work on what mobs can hear or not, leave on TRUE for now + /// Range we can hear combat sounds (tiles) + var/combat_hearing_range = 7 // Reduced to 7 tiles by default + /// Last time we heard combat + var/last_combat_sound = 0 + /// Range we can hear impact sounds (tiles) - same as gunfire (impacts ARE from gunfire) + var/impact_hearing_range = 7 // Increased to 7 tiles to match combat sounds + + // LIGHT DETECTION SYSTEM - Mobs notice player light sources in darkness + /// Can this mob detect light sources? + var/can_detect_light = TRUE + + // DETECTION DELAY SYSTEM - Sneaking players have detection buildup time + /// Detection progress toward player (0 to detection_time_required) + var/detection_progress = 0 + /// How long to fully detect player (deciseconds) - 0 = instant, 30 = 3 seconds for sneaking + var/detection_time_required = 0 + /// Who are we currently building detection progress toward? + var/atom/detecting_target = null + /// Cooldown for detection tick updates + COOLDOWN_DECLARE(detection_tick_cooldown) + /// How far can we detect light sources? (tiles) + var/light_detection_range = 9 + /// Minimum light_range value to be detected (flashlights are ~3-4, torches ~3) + var/light_detection_threshold = 2 + /// Last time we detected a light source + var/last_light_detected = 0 + /// Cooldown between light detections (deciseconds) - prevents spam + var/light_detection_cooldown = 50 // 5 seconds + /// Memory of last detected light location + var/turf/last_light_location = null + /// Should we only detect lights in dark areas? (TRUE = ignore lights in well-lit rooms) + var/only_detect_lights_in_darkness = TRUE + /// Minimum darkness level required to detect lights (0-1, lower = darker) + var/darkness_threshold = 0.5 + + /// SPATIAL AWARENESS & DOOR MEMORY + /// List of door locations we know about and can potentially use + var/list/known_doors = list() + /// Last position we were at + var/turf/last_position = null + /// How long have we been stuck in the same position? + var/stuck_time = 0 + /// Position where we got stuck + var/turf/stuck_position = null + /// How long to wait before considering ourselves stuck (deciseconds) - 10 seconds + var/stuck_timeout = 100 + /// Faster timeout for corner situations where we can see target (deciseconds) - 3 seconds + var/corner_stuck_timeout = 30 + /// How long to wait before smashing (deciseconds) - 15 seconds (stuck_timeout + smash_delay) + var/smash_delay = 150 + + /// DOOR KITING PREVENTION: Track TARGET's door passages to detect exploit + /// Last door the target passed through + var/atom/target_last_door = null + /// How many times has target passed through this door recently? + var/target_door_pass_count = 0 + /// Last time target passed through a door + var/target_last_door_pass_time = 0 + /// Have we committed to one side due to target kiting? + var/committed_to_door_side = FALSE + /// Which side of the door are we committed to? + var/turf/committed_door_location = null + /// Last known position of target (for tracking movement) + var/turf/target_last_position = null + + /// CORNER KITING PREVENTION: Track target peeking around corners + /// How many times has target peeked from same area? + var/corner_peek_count = 0 + /// Last time target peeked + var/last_corner_peek_time = 0 + /// Location where target keeps peeking from + var/turf/corner_peek_location = null + /// Have we committed to rushing a corner? + var/committed_to_corner = FALSE + + /// LIGHT PATROL SYSTEM: Idle mobs patrol to nearby doors when players are nearby + /// Are we on light patrol? + var/light_patrolling = FALSE + /// Where did we start the patrol from? + var/turf/patrol_home = null + /// Which door are we patrolling to? + var/atom/patrol_target_door = null + /// How many times have we visited each door? + var/list/patrol_door_visits = list() + /// Maximum visits before resting at home + var/patrol_max_visits = 2 + /// Are we resting after patrol? + var/patrol_resting = FALSE + /// When did we start resting? + var/patrol_rest_start = 0 + /// How long to rest before patrolling again (30 seconds) + var/patrol_rest_duration = 300 + /// Timer for patrol checks + var/patrol_check_timer = null + + /// THROWN ITEM DETECTION: Track thrown items and investigate spammers + /// Track recent thrown items and their sources + var/list/recent_thrown_items = list() + /// When did we last clear the thrown item list? + var/last_thrown_clear = 0 + /// How many thrown items before we investigate the source? + var/thrown_spam_threshold = 3 + /// Cooldown for investigating thrown items (deciseconds) + var/thrown_investigation_cooldown = 150 // 15 seconds + + /// LIGHT DESTRUCTION DETECTION: Track shot-out lights and investigate + /// Track recent light destructions and their sources + var/list/recent_light_shots = list() + /// When did we last clear the light shot list? + var/last_light_shot_clear = 0 + /// How many lights shot before we investigate the shooter? + var/light_spam_threshold = 3 + + /// Last time we smashed something (prevents spam) + var/last_smash_time = 0 + /// Cooldown between smash attempts (deciseconds) - 5 seconds + var/smash_cooldown = 50 + /// Are we currently stuck and trying alternate routes? + var/is_stuck = FALSE + /// How many alternate path attempts have we made? + var/path_attempts = 0 + /// Maximum path attempts before smashing + var/max_path_attempts = 3 + /// Last time we gave up chasing target due to being stuck + var/last_give_up_time = 0 + /// Cooldown after giving up before we can re-acquire targets (deciseconds) - 15 seconds + var/give_up_cooldown = 150 + + /// How long have we been on stairs? + var/time_on_stairs = 0 + /// Last time we checked if on stairs + var/last_stairs_check = 0 + /// Maximum time allowed on stairs before forced off (deciseconds) - 5 seconds + var/max_stairs_time = 50 + + /// How many consecutive times have we stepped on stairs? + var/consecutive_stair_steps = 0 + /// Maximum consecutive stair steps before forcing off + var/max_consecutive_stair_steps = 5 + + /// What's our primary combat style? + var/combat_mode = COMBAT_MODE_MELEE // COMBAT_MODE_MELEE, COMBAT_MODE_RANGED, or COMBAT_MODE_MIXED + + /// CHAIN REACTION ALERTING - tracks if this mob is the "alpha" that woke up others + var/is_alpha_alerter = FALSE + /// Range for chain reaction LOS alerting (tiles) + var/chain_alert_range = 3 + /// Cooldown for chain alerting to prevent spam + COOLDOWN_DECLARE(chain_alert_cooldown) + /// How long between chain alerts (deciseconds) + var/chain_alert_delay = 30 // 3 seconds + /mob/living/simple_animal/hostile/Initialize() . = ..() @@ -134,6 +405,37 @@ GiveTarget(null) targets_from = null + // Clear Z-pursuit memory references - IMPORTANT FOR GC + remembered_target = null + z_pursuit_structure = null + last_known_location = null + + // Clear Z-commitment + committed_z_level = 0 + + // Clear search mode + if(search_timer_id) + deltimer(search_timer_id) + search_timer_id = null + searching = FALSE + + // Clear smart search memory + if(searched_turfs) + searched_turfs.Cut() + searched_turfs = null + + if(searched_doors) + searched_doors.Cut() + searched_doors = null + + search_expansion = 0 + + // Clear stuck state and door memory + known_doors = null + last_position = null + stuck_position = null + is_stuck = FALSE + // Clear lists if(friends) friends.Cut() @@ -217,17 +519,206 @@ if(AIStatus == AI_OFF) return 0 - var/list/possible_targets = ListTargets() //we look around for potential targets and make it a list for later use. + var/list/possible_targets = ListTargets() + + // Safety check: ensure possible_targets is a proper list, not nested + if(!possible_targets) + possible_targets = list() + else if(!islist(possible_targets)) + possible_targets = list(possible_targets) + else if(islist(possible_targets)) + // Check for nested lists and flatten them + var/has_nested = FALSE + for(var/item in possible_targets) + if(islist(item)) + has_nested = TRUE + break + + if(has_nested) + var/list/flattened = list() + var/original_len = possible_targets.len + for(var/entry in possible_targets) + if(islist(entry)) + for(var/subitem in entry) + flattened += subitem + else + flattened += entry + possible_targets = flattened + + // Debug logging to track down the source + log_runtime("HOSTILE MOB: [src] ([type]) had nested list from ListTargets() at [get_turf(src)]. Original list: [original_len] entries, flattened to [flattened.len] entries.") + + // IMMEDIATE TARGET RESPONSE: Fast acquisition for both search mode and initial detection + // When idle or searching, quickly grab first valid target without waiting for next cycle + if((searching || AIStatus == AI_IDLE) && possible_targets && possible_targets.len > 0) + for(var/atom/possible_target in possible_targets) + // Dead/ghost check + if(isliving(possible_target)) + var/mob/living/L = possible_target + if(L.stat == DEAD || L.ckey && !L.client) + continue + + // CORE FIX: Check distance FIRST using effective range + var/effective_range = get_effective_vision_range(possible_target) + var/actual_distance = get_dist(src, possible_target) + + if(actual_distance > effective_range) + continue // Too far in darkness + + // Now check line of sight + if(!can_see(src, possible_target, effective_range)) + continue + + // Faction check + if(!CanAttack(possible_target)) + continue + + // All checks passed - acquire target + if(AIStatus == AI_IDLE) + toggle_ai(AI_ON) + + if(searching) + exit_search_mode(FALSE, found_target = TRUE) + + last_target_sighting = world.time + remembered_target = possible_target + GiveTarget(possible_target) + call_for_backup(possible_target, "found") + break + + // LIGHT DETECTION: Periodically check for light sources in darkness + // Has its own cooldown to avoid spam + if(!target || searching) + detect_light_source() + + // POSITION TRACKING + var/turf/current_pos = get_turf(src) + if(target && current_pos) + if(last_position == current_pos) + if(!stuck_position) + stuck_position = current_pos + stuck_time = world.time + else + var/stuck_duration = world.time - stuck_time + var/target_dist = get_dist(src, target) + var/can_see_target = can_see(src, target, get_effective_vision_range(target)) + if(can_see_target && target_dist >= 2 && target_dist <= 10) + if(stuck_duration > corner_stuck_timeout && !is_stuck) + is_stuck = TRUE + path_attempts = 0 + visible_message(span_notice("[src] changes approach...")) + else if(stuck_duration > stuck_timeout) + if(!is_stuck) + is_stuck = TRUE + path_attempts = 0 + else + if(is_stuck || stuck_position) + is_stuck = FALSE + stuck_position = null + stuck_time = 0 + path_attempts = 0 + else + is_stuck = FALSE + stuck_position = null + stuck_time = 0 + path_attempts = 0 + + last_position = current_pos + + if(can_open_doors && prob(10)) + scan_for_doors() + + var/on_stairs = FALSE + for(var/obj/structure/stairs/S in loc) + on_stairs = TRUE + break + + if(on_stairs) + if(time_on_stairs == 0) + time_on_stairs = world.time + else if((world.time - time_on_stairs) > max_stairs_time) + commit_to_z_level() + pursuing_z_target = FALSE + z_pursuit_structure = null + time_on_stairs = 0 + var/list/escape_turfs = list() + for(var/turf/T in range(3, src)) + var/has_stairs = FALSE + for(var/obj/structure/stairs/ST in T) + has_stairs = TRUE + break + if(!has_stairs && istype(T, /turf/open)) + escape_turfs += T + if(escape_turfs.len) + Goto(pick(escape_turfs), move_to_delay, 0) + else + time_on_stairs = 0 + + if(can_open_doors && target) + var/turf/T = get_step(src, get_dir(src, target)) + if(T) + for(var/obj/structure/simple_door/SD in T) + if(SD.density) + try_open_door(SD) + for(var/obj/machinery/door/D in T) + if(D.density) + try_open_door(D) if(environment_smash) EscapeConfinement() + // CURIOSITY SYSTEM: Idle mobs occasionally investigate nearby doors/rooms + if(AIStatus == AI_IDLE && curiosity_enabled && can_open_doors && !target && !searching) + if((world.time - last_curiosity_check) > curiosity_check_interval) + last_curiosity_check = world.time + if(prob(curiosity_chance)) + // PERFORMANCE: Use direct object range() instead of turf iteration + // Limit to range 3 instead of vision_range (9) to reduce lag + var/list/nearby_doors = list() + for(var/obj/structure/simple_door/SD in range(3, src)) + if(SD.density) + nearby_doors += SD + for(var/obj/machinery/door/D in range(3, src)) + if(D.density) + nearby_doors += D + + if(nearby_doors.len > 0) + // Prefer doors closer to us + var/atom/door_to_check = null + var/closest_dist = 999 + for(var/atom/door in nearby_doors) + var/dist = get_dist(src, door) + if(dist < closest_dist && prob(70)) // 70% chance to keep closer one + closest_dist = dist + door_to_check = door + + if(!door_to_check) + door_to_check = pick(nearby_doors) + + visible_message(span_notice("[src] investigates a nearby door...")) + Goto(door_to_check, move_to_delay, 0) + // Try to open it when we get adjacent + addtimer(CALLBACK(src, PROC_REF(curiosity_check_door), door_to_check), 3 SECONDS, TIMER_DELETE_ME) + + // LIGHT PATROL SYSTEM: Check for nearby players and patrol + if(AIStatus == AI_IDLE && !target && !searching && !light_patrolling) + // Check every 5 seconds (50 deciseconds) + if(!patrol_check_timer || (world.time - patrol_check_timer) > 50) + patrol_check_timer = world.time + check_light_patrol() + if(AICanContinue(possible_targets)) if(!QDELETED(target) && !targets_from.Adjacent(target)) - DestroyPathToTarget() - if(!MoveToTarget(possible_targets)) //if we lose our target - if(AIShouldSleep(possible_targets)) // we try to acquire a new one - toggle_ai(AI_IDLE) // otherwise we go idle + if(is_stuck) + var/can_see_target_now = target && can_see(src, target, get_effective_vision_range(target)) + // If we can SEE the target but are stuck, smash immediately - don't wait for smash_delay + if(can_see_target_now && (world.time - stuck_time > corner_stuck_timeout)) + DestroyPathToTarget() + else if(world.time - stuck_time > smash_delay) + DestroyPathToTarget() + if(!MoveToTarget(possible_targets)) + if(AIShouldSleep(possible_targets)) + toggle_ai(AI_IDLE) consider_despawning() return 1 @@ -309,17 +800,268 @@ if(stat == CONSCIOUS && !target && AIStatus != AI_OFF && !client && user) FindTarget(list(user), 1) COOLDOWN_RESET(src, sight_shoot_delay) // Let them shoot back immediately when attacked + + // CHAIN REACTION: This mob just woke up, alert nearby allies + trigger_chain_alert(user) return ..() /mob/living/simple_animal/hostile/bullet_act(obj/item/projectile/P) + // ALERT NEARBY ALLIES ABOUT COMBAT - alert at multiple locations + if(P.firer) + // Alert from shooter location (full range, muffled by walls) + alert_allies_of_combat(get_turf(P.firer), P.firer) + + // Alert from impact location (smaller radius, also muffled) + // This represents the sound of bullet hitting wall/target/mob + var/turf/impact_loc = get_turf(src) + if(impact_loc) + // Use a smaller range for impact sounds + alert_allies_of_impact(impact_loc, P.firer, impact_hearing_range) + . = ..() if (peaceful == TRUE) peaceful = FALSE - if(stat == CONSCIOUS && !target && AIStatus != AI_OFF && !client) - if(P.firer && get_dist(src, P.firer) <= aggro_vision_range) - FindTarget(list(P.firer), 1) - COOLDOWN_RESET(src, sight_shoot_delay) // Let them shoot back immediately - Goto(P.starting, move_to_delay, 3) + + // IMPROVED RETARGETING: Switch targets if being shot by someone closer or while searching + if(stat == CONSCIOUS && AIStatus != AI_OFF && !client && P.firer) + if(get_dist(src, P.firer) <= aggro_vision_range) + var/should_retarget = FALSE + + if(!target) + // No target, always acquire the shooter + should_retarget = TRUE + else + var/current_target_dist = get_dist(src, target) + var/shooter_dist = get_dist(src, P.firer) + + // Retarget if: shooter is much closer, OR we're searching (lost current target) + if(shooter_dist < (current_target_dist - 3) || searching) + should_retarget = TRUE + + if(should_retarget) + if(searching) + exit_search_mode(found_target = TRUE) // Found shooter + FindTarget(list(P.firer), 1) + COOLDOWN_RESET(src, sight_shoot_delay) + trigger_chain_alert(P.firer) + visible_message(span_danger("[src] turns its attention to [P.firer]!")) + return + + if(!target) // Only run to projectile source if we have no target after all checks + Goto(P.starting, move_to_delay, 3) + +// GLOBAL HELPER: Alert all hostile mobs about a projectile impact at a location +// This should be called from turf/obj bullet_act implementations or projectile on_range +/proc/alert_hostile_mobs_of_impact(turf/impact_location, atom/firer, impact_range = 5) + if(!impact_location || !firer) + return + + for(var/mob/living/simple_animal/hostile/M in range(7, impact_location)) + if(M.stat == DEAD || M.ckey || !M.can_hear_combat) + continue + + // Only alert mobs actively engaged (has target or searching) + if(!M.target && !M.searching) + continue + + // Check muffled range + var/effective_range = M.calculate_muffled_sound_range(impact_location, impact_range) + var/distance = get_dist(M, impact_location) + var/z_distance = abs(M.z - impact_location.z) + + if(distance <= effective_range && z_distance <= 1) + M.hear_impact_sound(impact_location, firer) + +// CHAIN REACTION ALERTING +// When a mob wakes up, it alerts all allies within LOS (vision range) +// Those allies then check for the original target and also wake up +// This creates a cascading alert system +/mob/living/simple_animal/hostile/proc/trigger_chain_alert(atom/threat) + if(!threat) + return + + // BUG 2 FIX: Only alpha mobs broadcast chain alerts + if(!is_alpha_alerter) + return + + if(!COOLDOWN_FINISHED(src, chain_alert_cooldown)) + return + + COOLDOWN_START(src, chain_alert_cooldown, chain_alert_delay) + + visible_message(span_danger("[src] alerts nearby allies!")) + + var/list/alerted_allies = list() + + for(var/mob/living/simple_animal/hostile/M in range(chain_alert_range, src)) + if(M == src || M.stat == DEAD || M.ckey) + continue + if(!faction_check_mob(M, TRUE)) + continue + if(can_see(src, M, chain_alert_range)) + alerted_allies += M + + var/turf/above_us = get_step_multiz(get_turf(src), UP) + if(above_us) + for(var/mob/living/simple_animal/hostile/M in range(chain_alert_range, above_us)) + if(M == src || M.stat == DEAD || M.ckey) + continue + if(!faction_check_mob(M, TRUE)) + continue + if(can_see(src, M, chain_alert_range)) + alerted_allies += M + + var/turf/below_us = get_step_multiz(get_turf(src), DOWN) + if(below_us) + for(var/mob/living/simple_animal/hostile/M in range(chain_alert_range, below_us)) + if(M == src || M.stat == DEAD || M.ckey) + continue + if(!faction_check_mob(M, TRUE)) + continue + if(can_see(src, M, chain_alert_range)) + alerted_allies += M + + for(var/mob/living/simple_animal/hostile/ally in alerted_allies) + ally.receive_chain_alert(threat, src) + +// Receive a chain alert from an ally +/mob/living/simple_animal/hostile/proc/receive_chain_alert(atom/threat, mob/living/simple_animal/hostile/alerter) + if(!threat || QDELETED(threat)) + return + + if(AIStatus == AI_ON || target) + return + + // Ghost check + if(isliving(threat)) + var/mob/living/L = threat + if(L.stat == DEAD || L.ckey && !L.client) + return + + visible_message(span_danger("[src] perks up from [alerter]'s alert!")) + + if(can_see(src, threat, get_effective_vision_range(threat)) && CanAttack(threat)) + // Direct confirmation - becomes alpha, can propagate + if(AIStatus != AI_ON) + toggle_ai(AI_ON) + FindTarget(list(threat), 1) + remembered_target = threat + last_target_sighting = world.time + last_known_location = get_turf(threat) + // GiveTarget (called by FindTarget) sets is_alpha_alerter = TRUE + // so this mob CAN chain-alert further + trigger_chain_alert(threat) + else + // Can't confirm - search quietly, no further propagation + is_alpha_alerter = FALSE + if(AIStatus != AI_ON) + toggle_ai(AI_ON) + last_known_location = get_turf(threat) + enter_search_mode() // is_alpha_alerter=FALSE → no broadcast + + +// SOUND MUFFLING THROUGH WALLS +// Calculates effective hearing range by counting walls between source and listener +// Each wall reduces range by 1 tile +/mob/living/simple_animal/hostile/proc/calculate_muffled_sound_range(turf/sound_source, base_range) + if(!sound_source) + return 0 + + var/turf/our_turf = get_turf(src) + if(!our_turf) + return 0 + + // Get line between source and us + var/list/line = getline(sound_source, our_turf) + + // Count walls + var/wall_count = 0 + for(var/turf/T in line) + if(iswallturf(T)) + wall_count++ + + // Each wall reduces range by 1 + var/effective_range = base_range - wall_count + + // Can't go below 0 + return max(effective_range, 0) + +// Updated alert_allies_of_combat with sound muffling +/mob/living/simple_animal/hostile/proc/alert_allies_of_combat(turf/combat_location, atom/attacker) + if(!combat_location) + return + + // Find allies in hearing range across Z-levels + var/list/allies_in_range = list() + + // Same Z + for(var/mob/living/simple_animal/hostile/M in range(combat_hearing_range, combat_location)) + if(M == src || M.stat == DEAD || M.ckey) + continue + if(faction_check_mob(M, TRUE)) + // Check muffled range + var/effective_range = M.calculate_muffled_sound_range(combat_location, combat_hearing_range) + var/actual_distance = get_dist(M, combat_location) + + if(actual_distance <= effective_range) + allies_in_range += M + + // Z above + var/turf/above = get_step_multiz(combat_location, UP) + if(above) + for(var/mob/living/simple_animal/hostile/M in range(combat_hearing_range, above)) + if(M == src || M.stat == DEAD || M.ckey) + continue + if(faction_check_mob(M, TRUE)) + // Check muffled range (Z-distance adds 1 wall equivalent) + var/effective_range = M.calculate_muffled_sound_range(combat_location, combat_hearing_range) - 1 + var/actual_distance = get_dist(M, above) + + if(actual_distance <= effective_range) + allies_in_range += M + + // Z below + var/turf/below = get_step_multiz(combat_location, DOWN) + if(below) + for(var/mob/living/simple_animal/hostile/M in range(combat_hearing_range, below)) + if(M == src || M.stat == DEAD || M.ckey) + continue + if(faction_check_mob(M, TRUE)) + // Check muffled range (Z-distance adds 1 wall equivalent) + var/effective_range = M.calculate_muffled_sound_range(combat_location, combat_hearing_range) - 1 + var/actual_distance = get_dist(M, below) + + if(actual_distance <= effective_range) + allies_in_range += M + + // Alert all allies + for(var/mob/living/simple_animal/hostile/ally in allies_in_range) + ally.hear_combat_sound(combat_location, attacker) + +// Alert allies about impact sounds (bullet hitting wall/target) +// Smaller radius than gunfire, but still muffled by walls +/mob/living/simple_animal/hostile/proc/alert_allies_of_impact(turf/impact_location, atom/attacker, impact_range = 5) + if(!impact_location) + return + + var/notified = 0 + var/max_notifications = 10 // CPU OPTIMIZATION: Limit notifications per impact + + // CPU OPTIMIZATION: Reduced range from 20 to 5 tiles, ALL hostile mobs (not just faction) + // Impacts are universal - everyone hears bullets hitting things nearby + for(var/mob/living/simple_animal/hostile/M in range(5, impact_location)) + if(M.stat == DEAD || M.ckey || !M.can_hear_combat) + continue + + // Check muffled range + var/effective_range = M.calculate_muffled_sound_range(impact_location, impact_range) + var/actual_distance = get_dist(M, impact_location) + + if(actual_distance <= effective_range) + M.hear_impact_sound(impact_location, attacker) + notified++ + if(notified >= max_notifications) // Early exit optimization + return /mob/living/simple_animal/hostile/Hear(message, atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, list/spans, message_mode, atom/movable/source) . = ..() @@ -336,22 +1078,423 @@ //////////////HOSTILE MOB TARGETTING AND AGGRESSION//////////// +/// Determine which vision cone the target is in relative to mob's facing direction +/// Returns CONE_FRONT, CONE_PERIPHERAL, or CONE_REAR +/mob/living/simple_animal/hostile/proc/get_vision_cone(atom/target) + if(!target) + return CONE_REAR + + var/turf/my_turf = get_turf(src) + var/turf/target_turf = get_turf(target) + if(!my_turf || !target_turf) + return CONE_REAR + + // Same tile = front cone + if(my_turf == target_turf) + return CONE_FRONT + + // Calculate relative position + var/dx = target_turf.x - my_turf.x + var/dy = target_turf.y - my_turf.y + + // If no direction set, face the target + if(!dir || dir == 0) + setDir(get_dir(src, target)) + return CONE_FRONT + + // ============================================ + // QUADRANT-BASED DETECTION (matches visual sectors exactly) + // ============================================ + // Uses IDENTICAL quadrant logic as get_sector_turfs() for perfect 1:1 match + // + // Sector layout: + // FRONT (RED): -30° to +30° → CONE_FRONT + // RIGHT PERIPHERAL (YELLOW): 30° to 90° → CONE_PERIPHERAL + // RIGHT REAR (CYAN): 90° to 150° → CONE_REAR + // REAR (GRAY): 150° to 210° → CONE_REAR + // LEFT REAR (CYAN): 210° to 270° → CONE_REAR + // LEFT PERIPHERAL (YELLOW): 270° to 330° → CONE_PERIPHERAL + // ============================================ + + // Get base direction angle + var/base_angle = dir_to_angle_detection(dir) + + // Calculate angle to target using SAME quadrant logic as visuals + var/target_angle = calculate_angle_from_offset_detection(dx, dy) + + // Calculate relative angle from facing direction + var/relative_angle = target_angle - base_angle + + // Normalize to -180 to +180 range + while(relative_angle > 180) + relative_angle -= 360 + while(relative_angle < -180) + relative_angle += 360 + + // Classify based on angle (matching visual sectors EXACTLY) + if(relative_angle >= -30 && relative_angle <= 30) + // Front center: -30° to +30° (60° total) + return CONE_FRONT + else if((relative_angle > 30 && relative_angle <= 90) || (relative_angle >= -90 && relative_angle < -30)) + // Peripheral zones: 30° to 90° and -90° to -30° (270° to 330°) + return CONE_PERIPHERAL + else + // Rear zones: 90° to 270° (includes cyan rear peripheral and gray rear center) + return CONE_REAR + +/// Get directional vision multiplier (DISABLED FOR TESTING) +/// Returns 1.0 to disable directional vision while testing lighting +/mob/living/simple_animal/hostile/proc/get_directional_vision_multiplier(atom/target) + // DISABLED: Just return full range for lighting-only testing + return 1.0 + +/// Check if target is within the mob's field of view (direction-based) +/// Returns TRUE if target is in front half (180-degree cone) of mob's facing direction +/// Legacy compatibility wrapper for the multiplier system +/mob/living/simple_animal/hostile/proc/in_field_of_view(atom/target) + if(!target) + return FALSE + + var/multiplier = get_directional_vision_multiplier(target) + return (multiplier >= 0.5) // TRUE if in front or side, FALSE if behind + +/// Calculate effective vision range based on lighting AND directional cone +/// Front cone: Better dark vision (5 tiles in darkness vs 3 in peripheral, 0 in rear) +/mob/living/simple_animal/hostile/proc/get_effective_vision_range(atom/target) + if(!target) + return vision_range + + // Determine which cone target is in + var/cone_type = get_vision_cone(target) + + // No vision behind us ever + if(cone_type == CONE_REAR) + return 0 + + // Check if target has a light source + var/has_light = FALSE + if(isliving(target)) + var/mob/living/L = target + if(L.light_range > 0) + has_light = TRUE + + // If they have a light, they're visible at full range + if(has_light) + // Apply cone multiplier + if(cone_type == CONE_FRONT) + return vision_range + else // CONE_PERIPHERAL + return max(round(vision_range * 0.6), 2) + + // Check ambient lighting at target location + var/turf/target_turf = get_turf(target) + if(!target_turf) + return 2 + + var/light_amount = target_turf.get_lumcount() + var/base_range = vision_range + + // Lighting-based range calculation (same for all cones) + if(light_amount >= 0.5) + base_range = vision_range // Bright: full range + else if(light_amount >= 0.2) + base_range = max(round(vision_range * 0.6), 3) // Dim: 60% range + else + // Darkness: reduced to 40% range + base_range = max(round(vision_range * 0.4), 3) + + // STEALTH BOY CHECK: Drastically reduce detection range for cloaked targets + if(ishuman(target)) + var/mob/living/carbon/human/H = target + if(H.alpha < 100) // Heavily cloaked by stealth boy + // Reduce detection range to 20% - they need to be VERY close + base_range = max(round(base_range * 0.2), 1) + + // Apply cone multipliers + if(cone_type == CONE_FRONT) + return base_range // Full range in front cone + else // CONE_PERIPHERAL + return max(round(base_range * 0.6), 2) // 60% of lighting range in peripheral + +/// LOW-LIGHT VISION: Modified version that adds low-light vision bonus for mutants/animals +/mob/living/simple_animal/hostile/proc/get_effective_vision_range_lowlight(atom/target) + var/base_range = get_effective_vision_range(target) + + if(!has_low_light_vision) + return base_range + + // Check if it's actually dark + var/turf/target_turf = get_turf(target) + if(!target_turf) + return base_range + + var/light_amount = target_turf.get_lumcount() + + // Only apply low-light bonus in darkness + if(light_amount < 0.2) + // Add bonus to dark vision + return base_range + low_light_bonus + + return base_range + +/// Determine which sound cone the target is in (for rear detection) +/mob/living/simple_animal/hostile/proc/get_sound_cone(atom/target) + if(!target) + return null + + var/turf/my_turf = get_turf(src) + var/turf/target_turf = get_turf(target) + if(!my_turf || !target_turf) + return null + + var/dx = target_turf.x - my_turf.x + var/dy = target_turf.y - my_turf.y + + // ============================================ + // QUADRANT-BASED SOUND DETECTION (matches visual sectors exactly) + // ============================================ + // Sound detection only works in REAR zones (90° to 270° from facing) + // + // REAR CENTER (GRAY): 150° to 210° → SOUND_REAR_CENTER (harder to hear) + // REAR PERIPHERAL (CYAN): 90° to 150° and 210° to 270° → SOUND_REAR_PERIPHERAL (easier to hear) + // ============================================ + + // Get base direction angle + var/base_angle = dir_to_angle_detection(dir) + + // Calculate angle to target using SAME quadrant logic + var/target_angle = calculate_angle_from_offset_detection(dx, dy) + + // Calculate relative angle + var/relative_angle = target_angle - base_angle + + // Normalize to -180 to +180 + while(relative_angle > 180) + relative_angle -= 360 + while(relative_angle < -180) + relative_angle += 360 + + // Check if target is in rear zones + if(relative_angle >= -90 && relative_angle <= 90) + return null // Front zones - no sound detection + + // Target is behind - determine if center or peripheral + // Rear center: 150° to 210° (±30° from opposite = relative ±30° from 180°) + // Which is: relative >= 150 OR relative <= -150 + var/abs_rel = abs(relative_angle) + if(abs_rel >= 150) + return SOUND_REAR_CENTER // Directly behind (harder to hear) + else + return SOUND_REAR_PERIPHERAL // Rear diagonal (easier to hear) + +// Helper functions for detection (identical to visual logic) +/mob/living/simple_animal/hostile/proc/dir_to_angle_detection(byond_dir) + switch(byond_dir) + if(NORTH) + return 0 + if(NORTHEAST) + return 45 + if(EAST) + return 90 + if(SOUTHEAST) + return 135 + if(SOUTH) + return 180 + if(SOUTHWEST) + return 225 + if(WEST) + return 270 + if(NORTHWEST) + return 315 + return 0 + +/mob/living/simple_animal/hostile/proc/calculate_angle_from_offset_detection(dx, dy) + var/angle = 0 + + // Cardinals (avoids arctan edge cases) + if(dx == 0) + if(dy > 0) + return 0 // NORTH + else + return 180 // SOUTH + else if(dy == 0) + if(dx > 0) + return 90 // EAST + else + return 270 // WEST + + // Diagonals - use arctan on absolute values + var/acute_angle = arctan(abs(dy), abs(dx)) + + // Map to correct quadrant + if(dx > 0 && dy > 0) + angle = acute_angle // NE + else if(dx < 0 && dy > 0) + angle = 360 - acute_angle // NW + else if(dx > 0 && dy < 0) + angle = 180 - acute_angle // SE + else // dx < 0 && dy < 0 + angle = 180 + acute_angle // SW + + // Normalize + while(angle < 0) + angle += 360 + while(angle >= 360) + angle -= 360 + + return angle + +/// Detect moving targets behind us via sound +/mob/living/simple_animal/hostile/proc/detect_rear_movement(atom/target) + if(!target) + return 0 + + if(!isliving(target)) + return 0 + + var/mob/living/L = target + + // FIX 1: Check if they're BEHIND us first + var/sound_cone = get_sound_cone(target) + if(!sound_cone) + return 0 // Not behind us - no sound detection + + // FIX 2: Check for recent movement + // Must have moved within last 2 seconds (20 deciseconds) + // This catches continuous walking/running between actual movement ticks + if(!L.last_move_time) + return 0 // Never moved + + var/time_since_move = world.time - L.last_move_time + + // 2 second window catches ongoing movement + if(time_since_move > 20) + return 0 // Stopped moving - no sound + + // Get their movement sound level + var/sound_level = 1.0 // Default for non-humans + if(ishuman(L)) + var/mob/living/carbon/human/H = L + sound_level = H.get_movement_sound_level() + + // Calculate detection range based on zone + // REAR CENTER: Directly behind - sound travels straight, easier to hear + // REAR PERIPHERAL: Diagonal/side angles - slightly muffled by head position + var/detection_range = 0 + + if(sound_cone == SOUND_REAR_CENTER) + detection_range = round(sound_level * 3) // Directly behind - sound travels clearly + else // SOUND_REAR_PERIPHERAL + detection_range = round(sound_level * 2.5) // Diagonal - slightly muffled + + return detection_range + /mob/living/simple_animal/hostile/proc/ListTargets()//Step 1, find out what we can see + if(stat == DEAD) // Dead mobs can't detect targets + return list() if(!search_objects) - . = hearers(vision_range, targets_from) - src + // CORE FIX: Filter by effective range BEFORE adding to list + . = list() + + // Get all potential targets in max vision range + var/list/potential_targets = hearers(vision_range, targets_from) - src + + for(var/atom/A in potential_targets) + // Skip non-attackable things early + if(!isliving(A) && !ismecha(A) && !istype(A, /obj/machinery/porta_turret)) + continue + + // VISION-BASED DETECTION + var/effective_range = get_effective_vision_range(A) + + // Apply low-light vision bonus if applicable + if(has_low_light_vision) + effective_range = get_effective_vision_range_lowlight(A) + + var/actual_distance = get_dist(src, A) + + // Check if within visual range + if(actual_distance <= effective_range) + . += A + if(debug_vision && isliving(A)) + var/mob/living/L = A + var/cone = get_vision_cone(L) + var/cone_name = "UNKNOWN" + switch(cone) + if(CONE_FRONT) + cone_name = "FRONT (90°)" + if(CONE_PERIPHERAL) + cone_name = "PERIPHERAL (45°)" + if(CONE_REAR) + cone_name = "REAR (blind)" + to_chat(L, span_notice("[src] detected you via VISION in [cone_name] cone at [actual_distance] tiles (max: [effective_range])")) + continue + + // SOUND-BASED DETECTION (for targets behind us) + var/sound_range = detect_rear_movement(A) + if(sound_range > 0 && actual_distance <= sound_range) + // Heard movement! Turn to face the sound source + var/face_dir = get_dir(src, A) + if(face_dir && face_dir != dir) + setDir(face_dir) + + // Check line-of-sight AND if they're within stealth-adjusted range + // Note: effective_range is already calculated above with stealth penalty + var/can_actually_see = can_see(src, A, effective_range) && (actual_distance <= effective_range) + + if(!can_actually_see) + // Heard something, turned to check, but can't see it - enter search mode + if(!searching && !target) + last_known_location = get_turf(A) + remembered_target = A + last_target_sighting = world.time + + // Wake up and enter search mode immediately + if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF) + toggle_ai(AI_ON) + enter_search_mode() + + if(debug_vision && isliving(A)) + var/mob/living/L = A + to_chat(L, span_warning("[src] heard you but can't see you (stealth: [effective_range] tiles) - searching!")) + var/sound_cone = get_sound_cone(L) + var/cone_name = "UNKNOWN" + switch(sound_cone) + if(SOUND_REAR_CENTER) + cone_name = "REAR CENTER (90°)" + if(SOUND_REAR_PERIPHERAL) + cone_name = "REAR PERIPHERAL (45°)" + to_chat(L, span_warning("[src] detected you via SOUND in [cone_name] cone at [actual_distance] tiles (max: [sound_range])")) + continue + + // Successfully detected via sound - add as target + . += A + if(debug_vision && isliving(A)) + var/mob/living/L = A + var/sound_cone = get_sound_cone(L) + var/cone_name = "UNKNOWN" + switch(sound_cone) + if(SOUND_REAR_CENTER) + cone_name = "REAR CENTER (90°)" + if(SOUND_REAR_PERIPHERAL) + cone_name = "REAR PERIPHERAL (45°)" + to_chat(L, span_notice("[src] detected you via SOUND in [cone_name] cone at [actual_distance] tiles (max: [sound_range])")) - // Check for targets one Z-level ABOVE through openspace above us + // Check for targets one Z-level ABOVE through openspace var/turf/our_turf = get_turf(targets_from) var/turf/above_us = get_step_multiz(our_turf, UP) if(above_us && istype(above_us, /turf/open/transparent/openspace)) - // We're under openspace, we can see up for(var/mob/living/M in range(vision_range, above_us)) - // Make sure they're actually on the openspace (visible to us) var/turf/their_turf = get_turf(M) if(istype(their_turf, /turf/open/transparent/openspace)) - . += M + var/effective_range = get_effective_vision_range(M) + if(has_low_light_vision) + effective_range = get_effective_vision_range_lowlight(M) + var/actual_distance = get_dist(src, M) + if(actual_distance <= effective_range) + . += M - // Check for targets one Z-level BELOW through openspace we're standing on/near + // Check for targets one Z-level BELOW through openspace var/list/openspace_tiles = list() if(istype(our_turf, /turf/open/transparent/openspace)) openspace_tiles += our_turf @@ -366,42 +1509,86 @@ var/turf/their_turf = get_turf(M) var/turf/above_them = get_step_multiz(their_turf, UP) if(above_them && istype(above_them, /turf/open/transparent/openspace)) - . += M + var/effective_range = get_effective_vision_range(M) + if(has_low_light_vision) + effective_range = get_effective_vision_range_lowlight(M) + var/actual_distance = get_dist(src, M) + if(actual_distance <= effective_range) + . += M + + // Add hostile machines that are in effective range + var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha, /obj/structure/destructible/clockwork/ocular_warden, /obj/item/electronic_assembly)) - var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha, /obj/structure/destructible/clockwork/ocular_warden,/obj/item/electronic_assembly)) - for(var/HM in typecache_filter_list(range(vision_range, targets_from), hostile_machines)) CHECK_TICK - if(can_see(targets_from, HM, vision_range)) + var/effective_range = get_effective_vision_range(HM) + var/actual_distance = get_dist(src, HM) + if(actual_distance <= effective_range && can_see(targets_from, HM, effective_range)) . += HM else + // Object search mode - also respect effective range . = list() - for (var/obj/A in oview(vision_range, targets_from)) + for(var/obj/A in oview(vision_range, targets_from)) CHECK_TICK - . += A - for (var/mob/living/A in oview(vision_range, targets_from)) + var/effective_range = get_effective_vision_range(A) + var/actual_distance = get_dist(src, A) + if(actual_distance <= effective_range) + . += A + for(var/mob/living/A in oview(vision_range, targets_from)) CHECK_TICK - . += A + var/effective_range = get_effective_vision_range(A) + var/actual_distance = get_dist(src, A) + if(actual_distance <= effective_range) + if(ishuman(A)) + if(!can_see(src, A, effective_range)) + continue // No detection through walls, regardless of sneak mode + . += A -/mob/living/simple_animal/hostile/proc/FindTarget(list/possible_targets, HasTargetsList = 0)//Step 2, filter down possible targets to things we actually care about +/mob/living/simple_animal/hostile/proc/FindTarget(list/possible_targets, HasTargetsList = 0) . = list() - if (peaceful == FALSE) + + if(peaceful == FALSE) if(!HasTargetsList) possible_targets = ListTargets() + + // SEARCH MODE: If searching and target reappears in LOS, re-acquire + if(searching && target && (target in possible_targets) && can_see(src, target, get_effective_vision_range(target))) + exit_search_mode(found_target = TRUE) + last_target_sighting = world.time + remembered_target = target + GiveTarget(target) + COOLDOWN_START(src, sight_shoot_delay, sight_shoot_delay_time) + call_for_backup(target, "found") + return target + + // Normal target acquisition loop for(var/pos_targ in possible_targets) var/atom/A = pos_targ - if(Found(A))//Just in case people want to override targetting + if(Found(A)) . = list(A) break - if(CanAttack(A))//Can we attack it? + if(CanAttack(A)) . += A continue + var/Target = PickTarget(.) + + // Check detection delay for sneaking targets + if(Target && !check_detection_delay(Target)) + return // Still building detection, don't acquire yet + GiveTarget(Target) COOLDOWN_START(src, sight_shoot_delay, sight_shoot_delay_time) - return Target //We now have a target + if(Target) + remembered_target = Target + last_target_sighting = world.time + last_known_location = get_turf(Target) + + if(!target || target != Target) + call_for_backup(Target, "found") + return Target /mob/living/simple_animal/hostile/proc/PossibleThreats() . = list() @@ -414,8 +1601,6 @@ . += A continue - - /mob/living/simple_animal/hostile/proc/Found(atom/A)//This is here as a potential override to pick a specific target if available return @@ -527,21 +1712,81 @@ return FALSE -/mob/living/simple_animal/hostile/proc/GiveTarget(new_target)//Step 4, give us our selected target +/// Check if enough time has passed to detect a sneaking target +/mob/living/simple_animal/hostile/proc/check_detection_delay(atom/candidate_target) + if(!candidate_target) + return FALSE + + // Check if target is sneaking + var/target_sneaking = FALSE + if(ishuman(candidate_target)) + var/mob/living/carbon/human/H = candidate_target + target_sneaking = H.sneaking + + // Not sneaking = instant detection + if(!target_sneaking) + detection_progress = 0 + detecting_target = null + return TRUE + + // Sneaking = 3 second buildup (30 deciseconds) + var/required_time = 30 + + // Reset if we switched targets + if(detecting_target != candidate_target) + detection_progress = 0 + detecting_target = candidate_target + + // Start detection timer + if(!detection_progress) + detection_progress = world.time // store start time + return FALSE + + var/elapsed = world.time - detection_progress + if(elapsed >= required_time) + // Fully detected! + detection_progress = 0 + detecting_target = null + return TRUE + + // Still building up - not detected yet + return FALSE + +/mob/living/simple_animal/hostile/proc/GiveTarget(new_target) + if(stat == DEAD) // Dead mobs can't acquire targets + return add_target(new_target) LosePatience() if(target != null) + remembered_target = target + last_target_sighting = world.time + is_alpha_alerter = TRUE // We personally have a confirmed target GainPatience() Aggro() return 1 + else + is_alpha_alerter = FALSE // Lost target, no longer primary alerter //What we do after closing in /mob/living/simple_animal/hostile/proc/MeleeAction(patience = TRUE) - if(rapid_melee > 1) - var/datum/callback/cb = CALLBACK(src, PROC_REF(CheckAndAttack)) - var/delay = SSnpcpool.wait / rapid_melee + // Vision check before attempting melee + if(target) + var/effective_range = get_effective_vision_range(target) + if(effective_range == 0) + // Target is behind us - lose target + LoseTarget() + return + var/dist = get_dist(src, target) + if(dist > effective_range) + // Target is beyond our vision range - lose target + LoseTarget() + return + + if(rapid_melee > 1) + var/datum/callback/cb = CALLBACK(src, PROC_REF(CheckAndAttack)) + var/delay = SSnpcpool.wait / rapid_melee for(var/i in 1 to rapid_melee) - addtimer(cb, (i - 1)*delay) + addtimer(cb, (i - 1)*delay, TIMER_DELETE_ME) else AttackingTarget() if(patience) @@ -549,92 +1794,305 @@ /mob/living/simple_animal/hostile/proc/CheckAndAttack() if(target && targets_from && isturf(targets_from.loc) && target.Adjacent(targets_from) && !incapacitated()) + // Vision check before rapid melee attack + var/effective_range = get_effective_vision_range(target) + if(effective_range == 0) + return // Can't see target (behind us) + var/dist = get_dist(src, target) + if(dist > effective_range) + return // Target beyond vision range AttackingTarget() -/mob/living/simple_animal/hostile/proc/MoveToTarget(list/possible_targets)//Step 5, handle movement between us and our target +/mob/living/simple_animal/hostile/proc/MoveToTarget(list/possible_targets) stop_automated_movement = 1 - if (peaceful == TRUE) + + // Safety check: flatten nested lists if somehow we received one + if(possible_targets && islist(possible_targets)) + for(var/item in possible_targets) + if(islist(item)) + // We have a nested list - flatten it + var/list/flattened = list() + for(var/entry in possible_targets) + if(islist(entry)) + // Add all items from the nested list + for(var/subitem in entry) + flattened += subitem + else + flattened += entry + possible_targets = flattened + log_runtime("HOSTILE MOB MoveToTarget: [src] ([type]) had nested list at [get_turf(src)]. Flattened to [flattened.len] entries.") + break + + if(peaceful == TRUE) + LoseTarget() + return 0 + + // DOOR KITING CHECK: Track if target is exploiting door kiting + if(target) + check_target_door_kiting() + + // If target is kiting and we're committed, stay on our side + if(committed_to_door_side && committed_door_location) + var/dist_from_commitment = get_dist(src, committed_door_location) + + if(dist_from_commitment > 2) + // Moved too far from commitment point - return to it + Goto(committed_door_location, move_to_delay, 0) + return 1 + else + // Stay here and wait for target to come to us + walk(src, 0) + + // If target gets far enough away, reset commitment + if(get_dist(src, target) > 7) + committed_to_door_side = FALSE + committed_door_location = null + target_door_pass_count = 0 + target_last_door = null + + return 1 + + // CORNER KITING CHECK: Track if target is exploiting corner peeks + check_corner_kiting() + + // If target is corner peeking, commit to rushing their position + if(committed_to_corner && corner_peek_location) + // Rush toward their last known peek location + Goto(corner_peek_location, move_to_delay, 0) + + // Reset if we reach the corner or target gets far away + if(get_dist(src, corner_peek_location) <= 1 || (target && get_dist(src, target) > 10)) + committed_to_corner = FALSE + corner_peek_location = null + corner_peek_count = 0 + + return 1 + + if(searching) + if(target && (target in possible_targets) && can_see(src, target, get_effective_vision_range(target))) + exit_search_mode(found_target = TRUE) + last_target_sighting = world.time + call_for_backup(target, "found") + else + return 1 + + if(rallying) + if(target && (target in possible_targets) && can_see(src, target, get_effective_vision_range(target))) + rallying = FALSE + rally_point = null + else if((world.time - rally_start_time) > rally_duration) + advance_after_rally() + else + return 1 + + if(!target) + if(remembered_target && !QDELETED(remembered_target) && last_known_location) + if((world.time - last_target_sighting) < target_memory_duration) + if(!searching) + enter_search_mode() + return 1 LoseTarget() return 0 - if(!target || !CanAttack(target)) + + if(!CanAttack(target)) + if(remembered_target && (world.time - last_target_sighting) < target_memory_duration) + if(!searching) + enter_search_mode() + return 1 LoseTarget() return 0 - if(target in possible_targets) - if(target.z != z) + + var/has_los = (target in possible_targets) && can_see(src, target, get_effective_vision_range(target)) + + if(has_los) + last_target_sighting = world.time + remembered_target = target + last_known_location = get_turf(target) + else + var/time_since_seen = world.time - last_target_sighting + if(time_since_seen > 30) + if(!searching) + enter_search_mode() + var/saved_remembered = remembered_target + var/saved_location = last_known_location + var/saved_sighting = last_target_sighting + GiveTarget(null) + remembered_target = saved_remembered + last_known_location = saved_location + last_target_sighting = saved_sighting + return 1 + + // Z-PURSUIT + if(pursuing_z_target) + if(target && target.z == z) + pursuing_z_target = FALSE + z_pursuit_structure = null + else if((world.time - z_pursuit_started) > z_pursuit_timeout) + pursuing_z_target = FALSE + z_pursuit_structure = null + + if(pursuing_z_target && z_pursuit_structure && target && target.z != z) + var/dist = get_dist(src, z_pursuit_structure) + if(dist <= 1 && istype(z_pursuit_structure, /obj/structure/ladder)) + walk(src, 0) + step(src, get_dir(src, z_pursuit_structure)) + else + Goto(z_pursuit_structure, move_to_delay, 0) + return 1 + + // DIFFERENT Z LEVEL + if(target && target.z != z) + var/on_stairs = FALSE + for(var/obj/structure/stairs/S in loc) + on_stairs = TRUE + break + + if((world.time - last_successful_z_move) < z_move_success_cooldown) + if(on_stairs) + Goto(target, move_to_delay, minimum_distance) + return 1 + + if(!on_stairs) if(attempt_z_pursuit()) return 1 - LoseTarget() - return 0 - - var/target_distance = get_dist(targets_from, target) - - // Check if we THINK we're adjacent but target is on different Z - if(targets_from && isturf(targets_from.loc) && target.Adjacent(targets_from)) - // Verify they're actually on the same Z-level - if(target.z != z) - // Target is on different Z but appears adjacent (openspace issue) - // Don't try to melee, just pursue - if(attempt_z_pursuit()) - return 1 - LoseTarget() - return 0 - - if(ranged) - if(!target.Adjacent(targets_from) && ranged_cooldown <= world.time) - OpenFire(target) - - if(!Process_Spacemove()) - walk(src,0) + + if(on_stairs) + Goto(target, move_to_delay, minimum_distance) return 1 - + + LoseTarget() + return 0 + + // DOOR OPENING toward target + if(can_open_doors && target) + var/turf/T = get_step(src, get_dir(src, target)) + if(T) + for(var/obj/structure/simple_door/SD in T) + if(SD.density) + try_open_door(SD) + for(var/obj/machinery/door/D in T) + if(D.density) + try_open_door(D) + + if(!Process_Spacemove()) + walk(src, 0) + return 1 + + var/target_distance = get_dist(targets_from, target) + + // TARGET IN LOS - normal combat path + if(has_los) + // RANGED ATTACK + // Allow shooting even when adjacent if we can't maintain proper distance (tight spaces) + var/can_shoot = FALSE + if(ranged && ranged_cooldown <= world.time) + if(!target.Adjacent(targets_from)) + can_shoot = TRUE // Normal case: not adjacent + else if(retreat_distance != null && target_distance < retreat_distance) + // We want to retreat but target is adjacent - shoot anyway (cornered) + can_shoot = TRUE + else if(target_distance <= 1 && minimum_distance > 0) + // Target is right on us but we want distance - shoot anyway (tight space) + can_shoot = TRUE + + if(can_shoot) + var/fired = OpenFire(target) + if(!fired || is_shot_blocked(target)) + if(retreat_distance != null && abs(target_distance - retreat_distance) <= 2) + var/turf/better_position = find_firing_position(target) + if(better_position) + Move(better_position, get_dir(src, better_position)) + return 1 + + // MOVEMENT if(retreat_distance != null) - if(target_distance <= retreat_distance && CHECK_BITFIELD(mobility_flags, MOBILITY_MOVE)) + if(is_stuck && target_distance >= 2) + if(target_distance > 1) + Goto(target, move_to_delay, 1) + else + is_stuck = FALSE + stuck_position = null + stuck_time = 0 + path_attempts = 0 + else if(target_distance < retreat_distance && CHECK_BITFIELD(mobility_flags, MOBILITY_MOVE)) set_glide_size(DELAY_TO_GLIDE_SIZE(move_to_delay)) - walk_away(src,target,retreat_distance,move_to_delay) + walk_away(src, target, retreat_distance, move_to_delay) + else if(target_distance > retreat_distance) + Goto(target, move_to_delay, retreat_distance) else - Goto(target,move_to_delay,minimum_distance) + walk(src, 0) else - Goto(target,move_to_delay,minimum_distance) - - if(variation_list[MOB_RETREAT_DISTANCE_CHANCE] && LAZYLEN(variation_list[MOB_RETREAT_DISTANCE]) && prob(variation_list[MOB_RETREAT_DISTANCE_CHANCE])) - retreat_distance = vary_from_list(variation_list[MOB_RETREAT_DISTANCE]) - - if(target) - if(COOLDOWN_TIMELEFT(src, melee_cooldown)) - return TRUE - COOLDOWN_START(src, melee_cooldown, melee_attack_cooldown) - - // Only try melee if we're ACTUALLY adjacent (same Z-level) - if(targets_from && isturf(targets_from.loc) && target.Adjacent(targets_from) && target.z == z) - MeleeAction() + if(target_distance > minimum_distance) + Goto(target, move_to_delay, minimum_distance) else - if(rapid_melee > 1 && target_distance <= melee_queue_distance) - MeleeAction(FALSE) - in_melee = FALSE - return 1 - return 0 - - if(environment_smash) + walk(src, 0) + + // MELEE ATTACK + if(COOLDOWN_TIMELEFT(src, melee_cooldown)) + return TRUE + + // Safety check: target might have been cleared during movement/shooting + if(!target) + return 0 + + var/is_adjacent = targets_from && isturf(targets_from.loc) && target.Adjacent(targets_from) && target.z == z + + if(is_adjacent) + var/effective_combat_mode = combat_mode ? combat_mode : COMBAT_MODE_MELEE + var/should_melee = (effective_combat_mode == COMBAT_MODE_MELEE || effective_combat_mode == COMBAT_MODE_MIXED) + + if(should_melee) + COOLDOWN_START(src, melee_cooldown, melee_attack_cooldown) + MeleeAction() + else if(rapid_melee > 1 && target_distance <= melee_queue_distance) + var/effective_combat_mode = combat_mode ? combat_mode : COMBAT_MODE_MELEE + if(effective_combat_mode != COMBAT_MODE_RANGED) + COOLDOWN_START(src, melee_cooldown, melee_attack_cooldown) + MeleeAction(FALSE) + else + in_melee = FALSE + + return 1 + + // TARGET NOT IN LOS but we know roughly where they are (within grace period) + // Move toward last known position - this is the "searching while in combat" path + // ranged_ignores_vision mobs can still shoot + if(target && (environment_smash || can_open_doors || ranged_ignores_vision)) if(target.loc != null && get_dist(targets_from, target.loc) <= vision_range) if(ranged_ignores_vision && ranged_cooldown <= world.time) OpenFire(target) - if((environment_smash & ENVIRONMENT_SMASH_WALLS) || (environment_smash & ENVIRONMENT_SMASH_RWALLS)) - Goto(target,move_to_delay,minimum_distance) - FindHidden() - return 1 - else - if(FindHidden()) + + var/target_dist = get_dist(targets_from, target) + if(target_dist <= 3) + if((environment_smash & ENVIRONMENT_SMASH_WALLS) || (environment_smash & ENVIRONMENT_SMASH_RWALLS)) + Goto(target, move_to_delay, minimum_distance) + FindHidden() return 1 + else + if(FindHidden()) + return 1 + + // If we can open doors and target is nearby but not visible, move toward them + // Don't immediately give up after firing - pursue through the door + if(can_open_doors && target_dist <= vision_range) + Goto(target, move_to_delay, minimum_distance) + return 1 + LoseTarget() return 0 /mob/living/simple_animal/hostile/proc/Goto(target, delay, minimum_distance) if(target == src.target) approaching_target = TRUE + // FIX: Turn to face target while pursuing + var/face_dir = get_dir(src, target) + if(face_dir && face_dir != dir) + setDir(face_dir) else approaching_target = FALSE if(CHECK_BITFIELD(mobility_flags, MOBILITY_MOVE)) set_glide_size(DELAY_TO_GLIDE_SIZE(move_to_delay)) + // Try pathfinding with walk_to - it handles basic pathing walk_to(src, target, minimum_distance, delay) if(variation_list[MOB_MINIMUM_DISTANCE_CHANCE] && LAZYLEN(variation_list[MOB_MINIMUM_DISTANCE]) && prob(variation_list[MOB_MINIMUM_DISTANCE_CHANCE])) minimum_distance = vary_from_list(variation_list[MOB_MINIMUM_DISTANCE]) @@ -643,129 +2101,2133 @@ /mob/living/simple_animal/hostile/adjustHealth(amount, updating_health = TRUE, forced = FALSE) . = ..() - if(!ckey && !stat && search_objects < 3 && . > 0)//Not unconscious, and we don't ignore mobs - if(search_objects)//Turn off item searching and ignore whatever item we were looking at, we're more concerned with fight or flight + if(!ckey && !stat && search_objects < 3 && . > 0) + if(searching) + exit_search_mode(FALSE, found_target = TRUE) + visible_message(span_danger("[src] snaps to attention!")) + var/list/possible_targets = ListTargets() + var/new_target = null + // FIXED: Only acquire target we can actually see + for(var/atom/T in possible_targets) + if(can_see(src, T, get_effective_vision_range(T)) && CanAttack(T)) + new_target = T + break + if(new_target) + GiveTarget(new_target) + remembered_target = new_target + last_target_sighting = world.time + last_known_location = get_turf(new_target) + else + // Was hurt but can't see attacker - keep searching, update memory + remembered_target = remembered_target // unchanged + // search_for_target() will keep running + + else if(rallying) + rallying = FALSE + rally_point = null + visible_message(span_danger("[src] cancels retreat and fights back!")) + var/list/possible_targets = ListTargets() + var/new_target = null + // FIXED: LOS check here too + for(var/atom/T in possible_targets) + if(can_see(src, T, get_effective_vision_range(T)) && CanAttack(T)) + new_target = T + break + if(new_target) + GiveTarget(new_target) + remembered_target = new_target + last_target_sighting = world.time + last_known_location = get_turf(new_target) + else + FindTarget() + + else if(search_objects) LoseTarget() LoseSearchObjects() + if(AIStatus != AI_ON && AIStatus != AI_OFF) toggle_ai(AI_ON) FindTarget() - else if(target != null && prob(40))//No more pulling a mob forever and having a second player attack it, it can switch targets now if it finds a more suitable one + else if(target != null && prob(40)) FindTarget() +/// DOOR KITING PREVENTION: Track if target is passing through same door repeatedly +/mob/living/simple_animal/hostile/proc/check_target_door_kiting() + if(!target || !isliving(target)) + return FALSE + + // Reset if it's been more than 5 seconds + if((world.time - target_last_door_pass_time) > 50) + target_door_pass_count = 0 + target_last_door = null + committed_to_door_side = FALSE + committed_door_location = null + return FALSE + + // Get target's current position + var/turf/target_turf = get_turf(target) + if(!target_turf || !target_last_position) + target_last_position = target_turf + return FALSE + + // Check if target moved + if(target_turf == target_last_position) + return committed_to_door_side + + // Check if target passed through a door between last position and current position + var/atom/door_passed = null + + // Check for doors at old position + for(var/obj/structure/simple_door/SD in target_last_position) + if(!SD.density) // Door is open + door_passed = SD + break + if(!door_passed) + for(var/obj/machinery/door/D in target_last_position) + if(!D.density) // Door is open + door_passed = D + break + + // Check for doors at new position + if(!door_passed) + for(var/obj/structure/simple_door/SD in target_turf) + if(!SD.density) + door_passed = SD + break + if(!door_passed) + for(var/obj/machinery/door/D in target_turf) + if(!D.density) + door_passed = D + break + + // Track door passage + if(door_passed) + if(door_passed == target_last_door) + // Target passed through same door again + if((world.time - target_last_door_pass_time) < 50) // Within 5 seconds + target_door_pass_count++ + if(target_door_pass_count >= 3) + // Target is kiting! Commit to this side + if(!committed_to_door_side) + committed_to_door_side = TRUE + committed_door_location = get_turf(src) + visible_message(span_warning("[src] stops chasing through the doorway!")) + else + target_door_pass_count = 1 // Reset if too much time passed + else + // Different door + target_last_door = door_passed + target_door_pass_count = 1 + + target_last_door_pass_time = world.time + + // Update last position + target_last_position = target_turf + return committed_to_door_side + +/// CORNER KITING PREVENTION: Track if target is peeking around same corner repeatedly +/mob/living/simple_animal/hostile/proc/check_corner_kiting() + if(!target || !isliving(target)) + return FALSE + + // Reset if it's been more than 5 seconds + if((world.time - last_corner_peek_time) > 50) + corner_peek_count = 0 + corner_peek_location = null + committed_to_corner = FALSE + return FALSE + + // Check if we lost sight of target + var/has_los = can_see(src, target, get_effective_vision_range(target)) + + if(!has_los) + // Target disappeared - record as potential peek + var/turf/target_turf = get_turf(target) + if(target_turf) + // Check if this is same area as last peek + if(corner_peek_location && get_dist(corner_peek_location, target_turf) <= 2) + // Same corner area! + if((world.time - last_corner_peek_time) < 50) // Within 5 seconds + corner_peek_count++ + if(corner_peek_count >= 3) + // Target is corner peeking! Commit to rushing + if(!committed_to_corner) + committed_to_corner = TRUE + visible_message(span_warning("[src] stops reacting to the corner peeks!")) + else + corner_peek_count = 1 + else + // New corner location + corner_peek_location = target_turf + corner_peek_count = 1 + + last_corner_peek_time = world.time + + return committed_to_corner + +/// BARRICADE FIX: Check if target is blocked by structure +/mob/living/simple_animal/hostile/proc/is_target_blocked_by_structure(atom/A) + if(!A) + return FALSE + + var/turf/source = get_turf(src) + var/turf/target_turf = get_turf(A) + + if(!source || !target_turf) + return FALSE + + // Get line to target + var/list/line_to_target = getline(source, target_turf) + + // Check for blocking structures (NOT doors - doors can be opened) + for(var/turf/T in line_to_target) + if(T == source || T == target_turf) + continue + + // Check for barricades, tables, windows, etc + for(var/obj/structure/S in T) + if(S.density) + // Skip doors - we handle those separately + if(istype(S, /obj/structure/simple_door)) + continue + if(istype(S, /obj/machinery/door)) + continue + + // Found a blocking structure! + return TRUE + + return FALSE + +/// LIGHT PATROL: Check for nearby players and start patrol +/mob/living/simple_animal/hostile/proc/check_light_patrol() + // Don't patrol if already have target, searching, or currently patrolling + if(target || searching || AIStatus != AI_IDLE || light_patrolling || patrol_resting) + return + + // Check if resting cooldown expired + if(patrol_resting) + if((world.time - patrol_rest_start) > patrol_rest_duration) + patrol_resting = FALSE + patrol_door_visits = list() // Reset visit counts + else + return // Still resting + + // Look for players within 14 tiles + var/found_player = FALSE + for(var/mob/living/carbon/human/H in range(14, src)) + if(H.stat == DEAD || H.ckey && !H.client) + continue + + // Check if we can attack them (faction check) + if(!CanAttack(H)) + continue + + // Found a valid player! + found_player = TRUE + break + + if(!found_player) + return + + // Start light patrol! + start_light_patrol() + +/// LIGHT PATROL: Start patrolling +/mob/living/simple_animal/hostile/proc/start_light_patrol() + light_patrolling = TRUE + patrol_home = get_turf(src) + + // Find doors within 14 tiles + var/list/available_doors = list() + + for(var/obj/structure/simple_door/SD in range(14, src)) + // Skip doors we've visited too many times + var/visits = patrol_door_visits[SD] || 0 + if(visits >= patrol_max_visits) + continue + available_doors += SD + + for(var/obj/machinery/door/D in range(14, src)) + var/visits = patrol_door_visits[D] || 0 + if(visits >= patrol_max_visits) + continue + available_doors += D + + // No doors available? + if(available_doors.len == 0) + // Move to furthest point within 7 tiles + patrol_to_furthest_point() + return + + // Pick a random door + patrol_target_door = pick(available_doors) + + // Record this visit + if(!patrol_door_visits[patrol_target_door]) + patrol_door_visits[patrol_target_door] = 0 + patrol_door_visits[patrol_target_door]++ + + visible_message(span_notice("[src] perks up and starts patrolling...")) + + // Move toward the door (max 7 tiles) + patrol_to_target() + +/// LIGHT PATROL: Move toward patrol target +/mob/living/simple_animal/hostile/proc/patrol_to_target() + if(!patrol_target_door || !patrol_home) + end_light_patrol() + return + + // Calculate how far we can move (max 7 tiles from home) + var/dist_from_home = get_dist(src, patrol_home) + var/dist_to_door = get_dist(src, patrol_target_door) + var/tiles_remaining = 7 - dist_from_home + + if(tiles_remaining <= 0) + // Reached max range - return home + return_to_patrol_home() + return + + if(dist_to_door <= 1) + // Reached the door! + return_to_patrol_home() + return + + // Move toward door with SLOW speed (patrol is slower) + var/patrol_delay = move_to_delay * 1.5 // 50% slower than normal + Goto(patrol_target_door, patrol_delay, 1) + + // Check again in 2 seconds + addtimer(CALLBACK(src, PROC_REF(patrol_to_target)), 2 SECONDS, TIMER_DELETE_ME) + +/// LIGHT PATROL: Return to starting position +/mob/living/simple_animal/hostile/proc/return_to_patrol_home() + if(!patrol_home) + end_light_patrol() + return + + // Move back to home position + var/patrol_delay = move_to_delay * 1.5 + Goto(patrol_home, patrol_delay, 0) + + // Check if we're home + if(get_dist(src, patrol_home) <= 1) + end_light_patrol() + else + // Check again in 2 seconds + addtimer(CALLBACK(src, PROC_REF(return_to_patrol_home)), 2 SECONDS, TIMER_DELETE_ME) + +/// LIGHT PATROL: Move to furthest point when no doors available +/mob/living/simple_animal/hostile/proc/patrol_to_furthest_point() + if(!patrol_home) + end_light_patrol() + return + + // Find furthest open turf within 7 tiles + var/list/candidate_turfs = list() + for(var/turf/open/T in range(7, patrol_home)) + if(!T.density) + candidate_turfs += T + + if(candidate_turfs.len == 0) + end_light_patrol() + return + + // Pick furthest turf + var/turf/furthest = null + var/furthest_dist = 0 + for(var/turf/T in candidate_turfs) + var/dist = get_dist(patrol_home, T) + if(dist > furthest_dist) + furthest_dist = dist + furthest = T + + if(!furthest) + end_light_patrol() + return + + visible_message(span_notice("[src] patrols to a distant point...")) + + // Move to furthest point + var/patrol_delay = move_to_delay * 1.5 + Goto(furthest, patrol_delay, 0) + + // Return home after reaching it + addtimer(CALLBACK(src, PROC_REF(return_to_patrol_home)), 5 SECONDS, TIMER_DELETE_ME) + +/// LIGHT PATROL: End patrol and possibly rest +/mob/living/simple_animal/hostile/proc/end_light_patrol() + light_patrolling = FALSE + patrol_target_door = null + walk(src, 0) // Stop moving + + // Check if we should start resting + var/all_doors_visited = TRUE + for(var/obj/structure/simple_door/SD in range(14, patrol_home)) + var/visits = patrol_door_visits[SD] || 0 + if(visits < patrol_max_visits) + all_doors_visited = FALSE + break + + if(all_doors_visited) + for(var/obj/machinery/door/D in range(14, patrol_home)) + var/visits = patrol_door_visits[D] || 0 + if(visits < patrol_max_visits) + all_doors_visited = FALSE + break + + if(all_doors_visited) + // All doors visited - rest at home + patrol_resting = TRUE + patrol_rest_start = world.time + visible_message(span_notice("[src] returns to its post.")) + +// ======================================== +// THROWN ITEM DETECTION SYSTEM +// ======================================== +// Track who's throwing items, investigate source if spammed + +/// Called when a thrown item lands near the mob +/mob/living/simple_animal/hostile/proc/detect_thrown_item(obj/item/thrown_item, atom/thrower) + if(!thrown_item || !thrower) + return + + // Don't react if we already have a target + if(target) + return + + // Clear old entries (older than 10 seconds) + if((world.time - last_thrown_clear) > 100) + recent_thrown_items = list() + last_thrown_clear = world.time + + // Add this throw to our memory + if(!recent_thrown_items[thrower]) + recent_thrown_items[thrower] = 0 + recent_thrown_items[thrower]++ + + // Check if this thrower is spamming + if(recent_thrown_items[thrower] >= thrown_spam_threshold) + // SPAM DETECTED - investigate the source (thrower) + if(CanAttack(thrower)) + visible_message(span_warning("[src] notices someone throwing things!")) + + // Wake up if idle + if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF) + toggle_ai(AI_ON) + + // Enter search mode targeting the thrower + last_known_location = get_turf(thrower) + remembered_target = thrower + last_target_sighting = world.time + + if(!searching) + enter_search_mode() + + // Reset spam counter for this thrower + recent_thrown_items[thrower] = 0 + else + // First or second throw - investigate the ITEM, not the thrower + var/turf/item_location = get_turf(thrown_item) + + if(item_location && !searching) + visible_message(span_notice("[src] looks toward [thrown_item]...")) + + // End light patrol if active + if(light_patrolling) + end_light_patrol() + + // Wake up + if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF) + toggle_ai(AI_ON) + + // Set investigation state to prevent LoseTarget() from stopping movement + last_known_location = item_location + remembered_target = thrower // Remember who threw it + last_target_sighting = world.time // Mark as recent + + // Move toward the item to investigate (adjacent) + Goto(item_location, move_to_delay, 0) + +// ======================================== +// LIGHT DESTRUCTION DETECTION SYSTEM +// ======================================== +// Investigate shot-out lights, or the shooter if spamming + +/// Called when a light is destroyed nearby +/mob/living/simple_animal/hostile/proc/detect_light_destruction(turf/destruction_location, atom/shooter, silenced = FALSE) + if(!destruction_location) + return + + // Don't react if we already have a target + if(target) + return + + // Clear old entries (older than 10 seconds) + if((world.time - last_light_shot_clear) > 100) + recent_light_shots = list() + last_light_shot_clear = world.time + + // Add this destruction to our memory (if we know the shooter) + if(shooter) + if(!recent_light_shots[shooter]) + recent_light_shots[shooter] = 0 + recent_light_shots[shooter]++ + + // Check if shooter is spamming (shot 3+ lights) + if(shooter && recent_light_shots[shooter] >= light_spam_threshold) + // SPAM DETECTED - investigate the SHOOTER + if(CanAttack(shooter)) + visible_message(span_warning("[src] notices someone shooting out lights!")) + + // Wake up if idle + if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF) + toggle_ai(AI_ON) + + // Enter search mode targeting the shooter + last_known_location = get_turf(shooter) + remembered_target = shooter + last_target_sighting = world.time + + if(!searching) + enter_search_mode() + + // Reset spam counter + recent_light_shots[shooter] = 0 + else + // First, second shot, OR we don't know who shot it + + if(silenced) + // Silenced weapon - only investigate if very close (3 tiles) + // Investigate the LIGHT LOCATION since we can't hear the shooter + if(get_dist(src, destruction_location) > 3) + return + + visible_message(span_notice("[src] notices a light going out...")) + + // End light patrol if active + if(light_patrolling) + end_light_patrol() + + // Wake up if idle + if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF) + toggle_ai(AI_ON) + + // Set investigation state to prevent LoseTarget() from stopping movement + last_known_location = destruction_location + if(shooter) + remembered_target = shooter + last_target_sighting = world.time + + // Move to investigate the light location (adjacent) + if(!searching) + Goto(destruction_location, move_to_delay, 0) + else + // NOT silenced - heard the gunshot! + // Natural response: investigate the SHOOTER, not the light + if(shooter && CanAttack(shooter)) + var/turf/shooter_location = get_turf(shooter) + + visible_message(span_warning("[src] heard that shot!")) + + // End light patrol if active + if(light_patrolling) + end_light_patrol() + + // Wake up if idle + if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF) + toggle_ai(AI_ON) + + // Set investigation state to prevent LoseTarget() from stopping movement + last_known_location = shooter_location + remembered_target = shooter + last_target_sighting = world.time + + // Move directly toward the shooter (adjacent) + if(!searching) + Goto(shooter_location, move_to_delay, 0) + else + // Don't know the shooter - investigate the light location as fallback + visible_message(span_notice("[src] notices a light going out...")) + + // End light patrol if active + if(light_patrolling) + end_light_patrol() + + // Wake up if idle + if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF) + toggle_ai(AI_ON) + + // Set investigation state to prevent LoseTarget() from stopping movement + last_known_location = destruction_location + last_target_sighting = world.time + + // Move to investigate the light location (adjacent) + if(!searching) + Goto(destruction_location, move_to_delay, 0) + /mob/living/simple_animal/hostile/proc/AttackingTarget() + // VISION CHECK: Don't attack if we can't see the target + if(target) + var/effective_range = get_effective_vision_range(target) + // If effective range is 0, target is completely out of vision (behind us) + if(effective_range == 0) + if(isliving(target)) + var/mob/living/L = target + if(L.client) + to_chat(L, span_notice("DEBUG: [src] cannot attack - you are behind them (range=0)")) + return FALSE + + // Check if target is within our effective vision range + var/dist = get_dist(src, target) + if(dist > effective_range) + if(isliving(target)) + var/mob/living/L = target + if(L.client) + to_chat(L, span_notice("DEBUG: [src] cannot attack - beyond vision range (dist=[dist], range=[effective_range])")) + return FALSE + SEND_SIGNAL(src, COMSIG_HOSTILE_ATTACKINGTARGET, target) in_melee = TRUE if(prob(alternate_attack_prob) && AlternateAttackingTarget(target)) return FALSE return target.attack_animal(src) -/// Does an extra *thing* when attacking. Return TRUE to not do the standard attack -/mob/living/simple_animal/hostile/proc/AlternateAttackingTarget(atom/the_target) - return +/// Does an extra *thing* when attacking. Return TRUE to not do the standard attack +/mob/living/simple_animal/hostile/proc/AlternateAttackingTarget(atom/the_target) + return + +/mob/living/simple_animal/hostile/proc/Aggro() + if(ckey) + return TRUE + + // Face the target when we acquire them + if(target && !QDELETED(target)) + var/face_dir = get_dir(src, target) + if(face_dir && face_dir != dir) + setDir(face_dir) + + vision_range = aggro_vision_range + if(target && LAZYLEN(emote_taunt) && prob(taunt_chance)) + INVOKE_ASYNC(src, PROC_REF(emote), "me", EMOTE_VISIBLE, "[pick(emote_taunt)] at [target].") + taunt_chance = max(taunt_chance-7,2) + if(LAZYLEN(emote_taunt_sound)) + var/taunt_choice = pick(emote_taunt_sound) + playsound(loc, taunt_choice, 50, 0, vary = FALSE, frequency = SOUND_FREQ_NORMALIZED(sound_pitch, vary_pitches[1], vary_pitches[2])) + + +/mob/living/simple_animal/hostile/proc/LoseAggro() + stop_automated_movement = 0 + vision_range = initial(vision_range) + taunt_chance = initial(taunt_chance) + +/mob/living/simple_animal/hostile/proc/LoseTarget() + // Check if we should clear memory due to target being crit/dead + if(remembered_target && !QDELETED(remembered_target) && isliving(remembered_target)) + var/mob/living/L = remembered_target + if(L.stat == DEAD || L.stat == UNCONSCIOUS) + // Target went crit/dead - clear memory immediately + remembered_target = null + last_target_sighting = 0 + if(searching) + exit_search_mode(give_up_message = TRUE, found_target = FALSE) + + // Z-pursuit check + if(target && can_z_move && isliving(target)) + var/mob/living/L = target + if(L.z != z) + remembered_target = L + last_target_sighting = world.time + last_known_location = get_turf(L) + + if((world.time - last_z_move_attempt) < 50) + return + + if(attempt_z_pursuit()) + return + + // Memory expired or no memory - truly give up + if(remembered_target && (world.time - last_target_sighting) > target_memory_duration && !searching) + remembered_target = null + + // If memory still valid, enter search instead of fully giving up + if(remembered_target && (world.time - last_target_sighting) < target_memory_duration) + if(!searching) + enter_search_mode() + // Still clear the combat target + GiveTarget(null) + approaching_target = FALSE + in_melee = FALSE + walk(src, 0) + LoseAggro() + return + + // Full give-up + if(searching) + exit_search_mode(give_up_message = FALSE, found_target = FALSE) + + if(rallying) + rallying = FALSE + rally_point = null + + pursuing_z_target = FALSE + z_pursuit_structure = null + remembered_target = null + last_known_location = null + is_stuck = FALSE + stuck_position = null + stuck_time = 0 + path_attempts = 0 + + GiveTarget(null) + approaching_target = FALSE + in_melee = FALSE + walk(src, 0) + LoseAggro() + +// Z-LEVEL PURSUIT SYSTEM +// This proc handles mobs chasing targets up/down stairs and ladders +// Called when target is on a different Z-level than the mob +/mob/living/simple_animal/hostile/proc/attempt_z_pursuit() + if(!target || !can_z_move) // Master toggle + pursuing_z_target = FALSE + z_pursuit_structure = null + return FALSE + + // Check Z-level commitment - don't pursue if committed to current Z + if(!should_change_z_level()) + pursuing_z_target = FALSE + z_pursuit_structure = null + return FALSE + + // Prevent stair loop - don't pursue if we just changed Z-levels + if((world.time - last_successful_z_move) < z_move_success_cooldown) + pursuing_z_target = FALSE + z_pursuit_structure = null + return FALSE + + var/mob/living/L = target + if(!istype(L)) + pursuing_z_target = FALSE + z_pursuit_structure = null + return FALSE + + var/went_up = (L.z > z) + var/went_down = (L.z < z) + + // Don't pursue if target is on same Z (prevents loop when they just climbed) + if(L.z == z) + pursuing_z_target = FALSE + z_pursuit_structure = null + return FALSE + + // STEP 1: Check if we're standing ON a ladder right now + // If yes, climb immediately without pathfinding (if we can climb ladders) + if(can_climb_ladders) + var/obj/structure/ladder/unbreakable/current_ladder = locate() in loc + if(current_ladder) + if(went_up && current_ladder.up) + visible_message(span_warning("[src] climbs up the ladder after [target]!")) + zMove(target = get_turf(current_ladder.up), z_move_flags = ZMOVE_CHECK_PULLEDBY|ZMOVE_ALLOW_BUCKLED|ZMOVE_INCLUDE_PULLED) + last_z_move_attempt = world.time + last_successful_z_move = world.time + pursuing_z_target = FALSE // We climbed! Clear pursuit state + z_pursuit_structure = null + record_z_level_change() + return TRUE + else if(went_down && current_ladder.down) + visible_message(span_warning("[src] climbs down the ladder after [target]!")) + zMove(target = get_turf(current_ladder.down), z_move_flags = ZMOVE_CHECK_PULLEDBY|ZMOVE_ALLOW_BUCKLED|ZMOVE_INCLUDE_PULLED) + last_z_move_attempt = world.time + last_successful_z_move = world.time + pursuing_z_target = FALSE // We climbed! Clear pursuit state + z_pursuit_structure = null + record_z_level_change() + return TRUE + + // STEP 2: Check if we're near stairs (reduces cooldown) + var/near_stairs = FALSE + if(can_climb_stairs && went_up) + for(var/obj/structure/stairs/S in view(vision_range, src)) + if(S.isTerminator()) + var/dist = get_dist(src, S) + if(dist <= 2) // Within 2 tiles of stairs + near_stairs = TRUE + break + + // STEP 3: Cooldown check (can be bypassed if near stairs) + if(world.time < last_z_move_attempt + z_move_delay) + if(near_stairs) + // Very short cooldown when near stairs + if(world.time < last_z_move_attempt + 2) + return pursuing_z_target // Return current state + else + return pursuing_z_target // Return current state + + last_z_move_attempt = world.time + target_last_z = L.z + + if(!went_up && !went_down) + pursuing_z_target = FALSE + z_pursuit_structure = null + return FALSE + + // STEP 4: Find Z-level transition structures (stairs, ladders, openspace) + // Only include structures we're actually allowed to use + var/list/z_structures = list() + + if(went_up) + // Target went UP - look for stairs and ladders going up + if(can_climb_stairs) + for(var/obj/structure/stairs/S in view(vision_range, src)) + if(S.isTerminator()) + z_structures += S + + if(can_climb_ladders) + for(var/obj/structure/ladder/LD in view(vision_range, src)) + if(LD.up) + z_structures += LD + + else if(went_down) + // Target went DOWN - look for openspace and ladders going down + if(can_jump_down) + var/turf/our_turf = get_turf(src) + + if(istype(our_turf, /turf/open/transparent/openspace)) + z_structures += our_turf + + for(var/turf/open/transparent/openspace/OS in view(vision_range, src)) + z_structures += OS + + if(can_climb_ladders) + for(var/obj/structure/ladder/LD in view(vision_range, src)) + if(LD.down) + z_structures += LD + + if(!z_structures.len) + pursuing_z_target = FALSE + z_pursuit_structure = null + return FALSE + + // STEP 5: Pick nearest structure and enter Z-pursuit state + var/atom/nearest = get_closest_atom(/atom, z_structures, src) + + // Enter Z-pursuit state - this locks the mob into climbing mode + pursuing_z_target = TRUE + z_pursuit_structure = nearest + z_pursuit_started = world.time + + visible_message(span_danger("[src] pursues [target] [went_up ? "upward" : "downward"]!")) + + // CALL FOR BACKUP - alert allies about Z-level pursuit + call_for_backup(target, "found") + + LosePatience() + GainPatience() + COOLDOWN_RESET(src, sight_shoot_delay) + + // STEP 6: Handle different structure types + + // STAIRS - special handling for multi-tile climb (only if we can climb stairs) + if(went_up && can_climb_stairs && istype(nearest, /obj/structure/stairs)) + var/obj/structure/stairs/S = nearest + var/turf/our_turf = get_turf(src) + var/turf/stairs_turf = get_turf(S) + var/dist = get_dist(src, S) + + if(our_turf == stairs_turf) + // Already on stairs - climb + var/step_result = step(src, S.dir) + if(!step_result) + // Climb blocked, set micro-cooldown + last_z_move_attempt = world.time - (z_move_delay - 2) + else if(dist <= 1) + // Adjacent - try to step on + walk(src, 0) + var/dir_to_stairs = get_dir(src, S) + var/step_result = step(src, dir_to_stairs) + + if(step_result && get_turf(src) == stairs_turf) + addtimer(CALLBACK(src, PROC_REF(climb_stairs), S), 1, TIMER_DELETE_ME) + else if(!step_result) + last_z_move_attempt = world.time - (z_move_delay - 2) + return TRUE + else + // Use Goto for pathfinding + Goto(S, move_to_delay, 0) + + // LADDERS - aggressive stepping to ensure mob gets ON the ladder tile (only if we can climb ladders) + else if(can_climb_ladders && istype(nearest, /obj/structure/ladder)) + var/obj/structure/ladder/L_target = nearest + var/dist = get_dist(src, L_target) + + // Check if already on ladder tile + var/obj/structure/ladder/on_ladder = locate() in loc + if(on_ladder) + // Already on ladder, next tick will detect and climb + return TRUE + + if(dist == 0) + // Somehow on ladder tile without locate() finding it - next tick will climb + return TRUE + else if(dist == 1) + // Adjacent to ladder - AGGRESSIVELY step onto it + walk(src, 0) // Stop any pathfinding + step(src, get_dir(src, L_target)) // Force step onto ladder + // Next AI tick will detect we're on ladder and climb + else + // Still far away - pathfind toward ladder + Goto(L_target, move_to_delay, 0) + + // OPENSPACE/OTHER - generic pathfinding (only if we can jump down) + else if(can_jump_down) + Goto(nearest, move_to_delay, 0) + else + // We can't use this structure type, give up + pursuing_z_target = FALSE + z_pursuit_structure = null + return FALSE + + return TRUE + +// Helper proc for delayed stair climbing +/mob/living/simple_animal/hostile/proc/climb_stairs(obj/structure/stairs/S) + if(!S || QDELETED(S) || QDELETED(src)) + return + + var/turf/our_turf = get_turf(src) + var/turf/stairs_turf = get_turf(S) + + if(our_turf != stairs_turf) + return + + step(src, S.dir) + + // Mark this as a successful Z-move to prevent immediate loop + record_z_level_change() // Changed from last_successful_z_move = world.time + pursuing_z_target = FALSE + z_pursuit_structure = null + +// SEARCH MODE - actively patrol and look for lost target +/mob/living/simple_animal/hostile/proc/enter_search_mode() + if(searching) + return + + // Don't start searching if the remembered target is dead OR CRIT + if(remembered_target && !QDELETED(remembered_target) && isliving(remembered_target)) + var/mob/living/L = remembered_target + if(L.stat == DEAD || L.stat == UNCONSCIOUS) // FIX: Include UNCONSCIOUS (crit) + // Target is dead/crit, no point searching + remembered_target = null + last_target_sighting = 0 + return + + if(last_search_exit_time > 0 && (world.time - last_search_exit_time) < search_entry_cooldown) + if(!remembered_target || (world.time - last_target_sighting) > target_memory_duration) + return + + searching = TRUE + + if(target) + last_known_location = get_turf(target) + else if(remembered_target && !QDELETED(remembered_target)) + last_known_location = get_turf(remembered_target) + + searched_turfs = list() + searched_doors = list() + searched_containers = list() + search_expansion = 0 + search_start_time = world.time + recently_opened_door = null + investigating_container = null + + visible_message(span_warning("[src] looks around suspiciously...")) + + if(is_alpha_alerter) + if(target) + call_for_backup(target, "lost") + else if(remembered_target && !QDELETED(remembered_target)) + call_for_backup(remembered_target, "lost") + + // Schedule search asynchronously to avoid blocking call in signal handlers + addtimer(CALLBACK(src, PROC_REF(search_for_target)), 0.5 SECONDS, TIMER_DELETE_ME) + +/mob/living/simple_animal/hostile/proc/exit_search_mode(give_up_message = TRUE, found_target = FALSE) + if(!searching) + return + + searching = FALSE + last_search_exit_time = world.time + + searched_turfs = list() + searched_doors = list() + searched_containers = list() + search_expansion = 0 + recently_opened_door = null + investigating_container = null + search_start_time = 0 + + if(!found_target) + // Gave up - no longer tracking this threat + is_alpha_alerter = FALSE + remembered_target = null + last_target_sighting = 0 + if(give_up_message) + visible_message(span_notice("[src] gives up the search.")) + LoseTarget() + else + visible_message(span_danger("[src] resumes the hunt!")) + // is_alpha_alerter stays as-is - if we found the target we may now broadcast + +/mob/living/simple_animal/hostile/proc/search_for_target() + if(!searching || QDELETED(src)) + return + + // Check if remembered_target is dead/invalid - if so, give up search + if(remembered_target) + if(QDELETED(remembered_target)) + // Target deleted entirely + visible_message(span_notice("[src] gives up the search.")) + exit_search_mode(FALSE) + return + + if(isliving(remembered_target)) + var/mob/living/L = remembered_target + if(L.stat == DEAD) + // Target died - no point searching for a corpse + visible_message(span_notice("[src] gives up the search.")) + exit_search_mode(FALSE) + return + + // HARD CHECK: Valid, visible, ATTACKABLE target right now? + var/list/immediate_targets = ListTargets() + if(immediate_targets && immediate_targets.len > 0) + for(var/atom/possible_target in immediate_targets) + // BUG 1 FIX: CanAttack() gates faction member targeting + // BUG 3 FIX: Explicit living+stat check catches ghosts that slip past CanAttack + if(isliving(possible_target)) + var/mob/living/L = possible_target + if(L.stat == DEAD || L.ckey && !L.client) // Dead or ghosted + continue + if(!can_see(src, possible_target, get_effective_vision_range(possible_target))) + continue + if(!CanAttack(possible_target)) + continue + + visible_message(span_danger("[src] spots [possible_target]!")) + exit_search_mode(FALSE, found_target = TRUE) + last_target_sighting = world.time + remembered_target = possible_target + GiveTarget(possible_target) + last_known_location = get_turf(possible_target) + walk(src, 0) + call_for_backup(possible_target, "found") + return + + // TIME-BASED TIMEOUT + if(search_start_time > 0) + var/elapsed_time = world.time - search_start_time + var/timeout_threshold = search_timeout_base + + if(is_low_health || (last_target_sighting > 0 && (world.time - last_target_sighting) < 600)) + timeout_threshold = search_timeout_aggressive + + if(elapsed_time > timeout_threshold) + visible_message(span_notice("[src] gives up the search.")) + exit_search_mode(FALSE) + return + + if(!last_known_location || !CHECK_BITFIELD(mobility_flags, MOBILITY_MOVE)) + addtimer(CALLBACK(src, PROC_REF(search_for_target)), 3 SECONDS, TIMER_DELETE_ME) + return + + var/turf/current_pos = get_turf(src) + var/dist_to_last_known = get_dist(current_pos, last_known_location) + + // PRIORITY 0: Move to last_known_location if we're far from it + // This ensures we search where the player was last seen, not random areas + if(dist_to_last_known > 3) + // Check for doors blocking the path to last_known_location + if(can_open_doors) + var/dir_to_target = get_dir(src, last_known_location) + var/turf/T = get_step(src, dir_to_target) + if(T) + // Check for doors in the next step toward target + for(var/obj/structure/simple_door/SD in T) + if(SD.density && !(SD in searched_doors)) + searched_doors += SD + if(try_open_door(SD)) + visible_message(span_notice("[src] opens [SD] to continue pursuit...")) + + // Check if target is now visible after opening door + sleep(5) + var/list/check_targets = ListTargets() + if(check_targets && check_targets.len > 0) + for(var/atom/possible_target in check_targets) + if(isliving(possible_target)) + var/mob/living/L = possible_target + if(L.stat == DEAD || L.ckey && !L.client) + continue + if(!can_see(src, possible_target, get_effective_vision_range(possible_target))) + continue + if(!CanAttack(possible_target)) + continue + + visible_message(span_danger("[src] spots [possible_target]!")) + exit_search_mode(FALSE, found_target = TRUE) + last_target_sighting = world.time + remembered_target = possible_target + GiveTarget(possible_target) + last_known_location = get_turf(possible_target) + walk(src, 0) + call_for_backup(possible_target, "found") + return + + addtimer(CALLBACK(src, PROC_REF(search_for_target)), 1 SECONDS, TIMER_DELETE_ME) + return + for(var/obj/machinery/door/D in T) + if(D.density && !(D in searched_doors)) + searched_doors += D + if(try_open_door(D)) + visible_message(span_notice("[src] opens [D] to continue pursuit...")) + + // Check if target is now visible after opening door + sleep(5) + var/list/check_targets = ListTargets() + if(check_targets && check_targets.len > 0) + for(var/atom/possible_target in check_targets) + if(isliving(possible_target)) + var/mob/living/L = possible_target + if(L.stat == DEAD || L.ckey && !L.client) + continue + if(!can_see(src, possible_target, get_effective_vision_range(possible_target))) + continue + if(!CanAttack(possible_target)) + continue + + visible_message(span_danger("[src] spots [possible_target]!")) + exit_search_mode(FALSE, found_target = TRUE) + last_target_sighting = world.time + remembered_target = possible_target + GiveTarget(possible_target) + last_known_location = get_turf(possible_target) + walk(src, 0) + call_for_backup(possible_target, "found") + return + + addtimer(CALLBACK(src, PROC_REF(search_for_target)), 1 SECONDS, TIMER_DELETE_ME) + return + + // We're too far from where we last saw them - move closer first + if(!(last_known_location in searched_turfs)) + searched_turfs += last_known_location + visible_message(span_warning("[src] moves to investigate the last known position...")) + Goto(last_known_location, move_to_delay, 0) + addtimer(CALLBACK(src, PROC_REF(search_for_target)), 3 SECONDS, TIMER_DELETE_ME) + return + + // Special case: Very close to last_known_location - do thorough immediate area check + // This catches targets standing right at/near the door or last known position + if(dist_to_last_known <= 1) + var/list/close_range_targets = ListTargets() + if(close_range_targets && close_range_targets.len > 0) + for(var/atom/possible_target in close_range_targets) + if(isliving(possible_target)) + var/mob/living/L = possible_target + if(L.stat == DEAD || L.ckey && !L.client) + continue + + // More lenient check when very close - check actual distance + var/dist_to_target = get_dist(src, possible_target) + if(dist_to_target <= 3) // Within close range + if(CanAttack(possible_target)) + visible_message(span_danger("[src] spots [possible_target] nearby!")) + exit_search_mode(FALSE, found_target = TRUE) + last_target_sighting = world.time + remembered_target = possible_target + GiveTarget(possible_target) + last_known_location = get_turf(possible_target) + walk(src, 0) + call_for_backup(possible_target, "found") + return + + // Now we're close to last_known_location - search systematically around it + var/current_radius = search_radius + (search_expansion * 2) + if(current_radius > 15) + current_radius = 15 + + // PRIORITY 1: Check BOTH doors AND containers, investigate closest one + // This fixes the issue where mobs would check all doors before any containers + if(can_open_doors && last_known_location) + // Ensure last_known_location is a proper turf + if(!isturf(last_known_location)) + last_known_location = get_turf(last_known_location) + + if(last_known_location) + var/list/hiding_spots = list() // Combined list of doors and containers + + // Collect unchecked doors + for(var/obj/structure/simple_door/SD in range(current_radius, last_known_location)) + if(!(SD in searched_doors) && SD.density) + hiding_spots += SD + for(var/obj/machinery/door/D in range(current_radius, last_known_location)) + if(!(D in searched_doors) && D.density) + hiding_spots += D + + // Collect unchecked containers + for(var/obj/structure/closet/C in range(current_radius, last_known_location)) + if(!(C in searched_containers) && !C.opened && !C.welded && !C.locked) + hiding_spots += C + + // Find CLOSEST hiding spot (door or container) + if(hiding_spots.len > 0) + var/atom/closest_spot = null + var/closest_dist = 999 + for(var/atom/spot in hiding_spots) + var/dist = get_dist(src, spot) + if(dist < closest_dist) + closest_dist = dist + closest_spot = spot + + // Handle the closest hiding spot + if(closest_spot) + // Is it a container? + if(istype(closest_spot, /obj/structure/closet)) + var/obj/structure/closet/C = closest_spot + + if(get_dist(src, C) <= 1) // Adjacent - open it NOW + searched_containers += C + visible_message(span_warning("[src] searches [C]...")) + + // Check container contents BEFORE opening + for(var/atom/A in C.contents) + if(isliving(A)) + var/mob/living/L = A + if(L.stat == DEAD || L.ckey && !L.client) + continue + if(CanAttack(L)) + // Found someone hiding! + C.open(src) + sleep(5) + visible_message(span_danger("[src] found [L] hiding in [C]!")) + exit_search_mode(FALSE, found_target = TRUE) + last_target_sighting = world.time + remembered_target = L + GiveTarget(L) + last_known_location = get_turf(L) + walk(src, 0) + call_for_backup(L, "found") + return + + // No one inside - open and check nearby + C.open(src) + sleep(5) + + var/list/check_targets = ListTargets() + if(check_targets && check_targets.len > 0) + for(var/atom/possible_target in check_targets) + if(isliving(possible_target)) + var/mob/living/L = possible_target + if(L.stat == DEAD || L.ckey && !L.client) + continue + if(!CanAttack(possible_target)) + continue + + if(get_dist(possible_target, C) <= 1) + visible_message(span_danger("[src] found [possible_target] near [C]!")) + exit_search_mode(FALSE, found_target = TRUE) + last_target_sighting = world.time + remembered_target = possible_target + GiveTarget(possible_target) + last_known_location = get_turf(possible_target) + walk(src, 0) + call_for_backup(possible_target, "found") + return + + // Container was empty - continue searching + addtimer(CALLBACK(src, PROC_REF(search_for_target)), 1 SECONDS, TIMER_DELETE_ME) + return + else // Not adjacent - move toward it + Goto(C, move_to_delay, 0) + addtimer(CALLBACK(src, PROC_REF(search_for_target)), 1.5 SECONDS, TIMER_DELETE_ME) + return + + // Is it a door? + else + if(get_dist(src, closest_spot) <= 1) // Adjacent - open it + searched_doors += closest_spot + if(try_open_door(closest_spot)) + recently_opened_door = closest_spot + sleep(5) + + var/list/check_targets = ListTargets() + if(check_targets && check_targets.len > 0) + for(var/atom/possible_target in check_targets) + if(isliving(possible_target)) + var/mob/living/L = possible_target + if(L.stat == DEAD || L.ckey && !L.client) + continue + if(!can_see(src, possible_target, get_effective_vision_range(possible_target))) + continue + if(!CanAttack(possible_target)) + continue + + visible_message(span_danger("[src] spots [possible_target] behind the door!")) + exit_search_mode(FALSE, found_target = TRUE) + last_target_sighting = world.time + remembered_target = possible_target + GiveTarget(possible_target) + last_known_location = get_turf(possible_target) + walk(src, 0) + call_for_backup(possible_target, "found") + return + + // No visible target - continue searching + addtimer(CALLBACK(src, PROC_REF(search_for_target)), 1 SECONDS, TIMER_DELETE_ME) + return + else // Not adjacent - move toward it + Goto(closest_spot, move_to_delay, 0) + addtimer(CALLBACK(src, PROC_REF(search_for_target)), 1.5 SECONDS, TIMER_DELETE_ME) + return + + // All hiding spots checked + + // PRIORITY 2: Explore unsearched tiles around last_known_location + var/list/unexplored_turfs = list() + + // Search around last_known_location, not our current position + for(var/turf/T in range(current_radius, last_known_location)) + if(T in searched_turfs) + continue + if(!istype(T, /turf/open)) + continue + + // Prioritize tiles near recently opened doors + if(recently_opened_door) + var/turf/door_turf = get_turf(recently_opened_door) + if(get_dist(T, door_turf) <= 2) + unexplored_turfs.Insert(1, T) + continue + + // PERFORMANCE: Direct door check instead of turf iteration + var/near_unopened_door = FALSE + for(var/obj/structure/simple_door/SD in orange(1, T)) + if(SD.density && !(SD in searched_doors)) + near_unopened_door = TRUE + break + if(!near_unopened_door) + for(var/obj/machinery/door/D in orange(1, T)) + if(D.density && !(D in searched_doors)) + near_unopened_door = TRUE + break + + if(near_unopened_door) + unexplored_turfs.Insert(1, T) + else + unexplored_turfs += T + + // Move to unexplored area + if(unexplored_turfs.len) + var/turf/search_target = null + + // Pick from top candidates, preferring ones closer to last_known_location + var/candidates_count = min(5, unexplored_turfs.len) + var/list/candidate_turfs = unexplored_turfs.Copy(1, candidates_count + 1) + + // Score by distance to last_known_location (closer = better) + var/best_score = 999 + for(var/turf/T in candidate_turfs) + var/score = get_dist(T, last_known_location) + + // Small randomization to avoid perfectly synchronized movement + score += rand(-2, 2) + + if(score < best_score) + best_score = score + search_target = T + + if(!search_target) + search_target = pick(candidate_turfs) + + // Clear recently_opened_door if we've moved past it + if(recently_opened_door) + var/turf/door_turf = get_turf(recently_opened_door) + if(door_turf && get_dist(current_pos, door_turf) > 3) + recently_opened_door = null + + // Move to target + searched_turfs += search_target + Goto(search_target, move_to_delay, 0) + else + // Exhausted current radius - expand search + search_expansion++ + if(search_expansion <= 8) + visible_message(span_notice("[src] expands the search...")) + + addtimer(CALLBACK(src, PROC_REF(search_for_target)), 3 SECONDS, TIMER_DELETE_ME) + +// BACKUP SYSTEM - alert nearby allies about target location +/mob/living/simple_animal/hostile/proc/call_for_backup(atom/found_target, alert_type = "found") + if(!found_target) + return + + // Cooldown check to prevent spam + if(!COOLDOWN_FINISHED(src, backup_call_cooldown)) + return + + COOLDOWN_START(src, backup_call_cooldown, backup_call_delay) + + // Alert message + switch(alert_type) + if("found") + visible_message(span_danger("[src] alerts nearby allies!")) + if("lost") + visible_message(span_warning("[src] signals they've lost sight of the target!")) + + // Find allies in range, INCLUDING ACROSS Z-LEVELS + var/list/allies_to_alert = list() + + // Same Z-level + for(var/mob/living/simple_animal/hostile/M in range(backup_call_range, src)) + if(M == src || M.stat == DEAD || M.ckey) + continue + if(faction_check_mob(M, TRUE)) + allies_to_alert += M + + // Check Z-level above + var/turf/above_us = get_step_multiz(get_turf(src), UP) + if(above_us) + for(var/mob/living/simple_animal/hostile/M in range(backup_call_range, above_us)) + if(M == src || M.stat == DEAD || M.ckey) + continue + if(faction_check_mob(M, TRUE)) + allies_to_alert += M + + // Check Z-level below + var/turf/below_us = get_step_multiz(get_turf(src), DOWN) + if(below_us) + for(var/mob/living/simple_animal/hostile/M in range(backup_call_range, below_us)) + if(M == src || M.stat == DEAD || M.ckey) + continue + if(faction_check_mob(M, TRUE)) + allies_to_alert += M + + // Alert each ally + for(var/mob/living/simple_animal/hostile/ally in allies_to_alert) + switch(alert_type) + if("found") + ally.receive_backup_alert(found_target, get_turf(found_target)) + if("lost") + ally.receive_lost_alert(found_target, get_turf(found_target)) + +// Receive backup alert from ally +/mob/living/simple_animal/hostile/proc/receive_backup_alert(atom/alert_target, turf/target_location) + if(stat == DEAD) // Dead mobs can't respond to alerts + return + if(!alert_target || QDELETED(alert_target)) + return + + if(last_give_up_time && (world.time - last_give_up_time) < give_up_cooldown) + return + + // Ghost/dead check on the alert target itself + if(isliving(alert_target)) + var/mob/living/L = alert_target + if(L.stat == DEAD || L.ckey && !L.client) + return // Don't chase ghosts or corpses + + // Can we see them directly? + if(can_see(src, alert_target, get_effective_vision_range(alert_target)) && CanAttack(alert_target)) + if(searching) + exit_search_mode(found_target = TRUE) + // Direct confirmation → becomes alpha + GiveTarget(alert_target) // GiveTarget sets is_alpha_alerter = TRUE + remembered_target = alert_target + last_target_sighting = world.time + last_known_location = target_location + if(AIStatus != AI_ON) + toggle_ai(AI_ON) + visible_message(span_danger("[src] responds to the alert!")) + return + + // Can't see them - search quietly, no cascade + is_alpha_alerter = FALSE // Explicitly secondary + remembered_target = alert_target + last_target_sighting = world.time + last_known_location = target_location + + if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF) + toggle_ai(AI_ON) + + if(!searching && !target) + // enter_search_mode() will see is_alpha_alerter=FALSE and NOT broadcast + enter_search_mode() + if(target_location && get_dist(src, target_location) > 3) + visible_message(span_danger("[src] responds to the alert and advances!")) + Goto(target_location, move_to_delay, 2) + else + visible_message(span_danger("[src] responds to the alert!")) + else if(target && target != alert_target) + // Already have a target - only update memory if alert target is much closer + if(get_dist(src, alert_target) < get_dist(src, target) - 3) + last_known_location = target_location + +// RETREAT TO ALLIES - move toward nearby allies while calling for backup +/mob/living/simple_animal/hostile/proc/retreat_to_allies() + if(!target) + return + + // Find nearby allies + var/list/nearby_allies = list() + var/turf/my_turf = get_turf(src) + + // Same Z-level + for(var/mob/living/simple_animal/hostile/M in range(backup_call_range, src)) + if(M == src || M.stat == DEAD || M.ckey) + continue + if(faction_check_mob(M, TRUE)) + nearby_allies += M + + // Check Z-level above + var/turf/above_us = get_step_multiz(my_turf, UP) + if(above_us) + for(var/mob/living/simple_animal/hostile/M in range(backup_call_range, above_us)) + if(M == src || M.stat == DEAD || M.ckey) + continue + if(faction_check_mob(M, TRUE)) + nearby_allies += M + + // Check Z-level below + var/turf/below_us = get_step_multiz(my_turf, DOWN) + if(below_us) + for(var/mob/living/simple_animal/hostile/M in range(backup_call_range, below_us)) + if(M == src || M.stat == DEAD || M.ckey) + continue + if(faction_check_mob(M, TRUE)) + nearby_allies += M + + if(nearby_allies.len) + // Found allies! Call for backup and enter rally mode + visible_message(span_danger("[src] retreats toward reinforcements, calling for backup!")) + call_for_backup(target, "found") + + // Enter rally mode + rallying = TRUE + rally_start_time = world.time + rally_point = get_turf(target) // Remember where we saw the target + + // Move toward closest ally + var/mob/living/simple_animal/hostile/closest_ally = null + var/shortest_dist = INFINITY + for(var/mob/living/simple_animal/hostile/ally in nearby_allies) + var/dist = get_dist(src, ally) + if(dist < shortest_dist) + shortest_dist = dist + closest_ally = ally + + if(closest_ally && get_dist(src, closest_ally) > 2) // Only move if not already adjacent + Goto(closest_ally, move_to_delay, 2) // Stop at distance 2 from ally + + // Schedule advance back to target after rally duration + addtimer(CALLBACK(src, PROC_REF(advance_after_rally)), rally_duration, TIMER_DELETE_ME) + else + // No allies nearby, just alert them anyway and keep fighting + call_for_backup(target, "found") + +// Advance back to target after rallying with allies +/mob/living/simple_animal/hostile/proc/advance_after_rally() + if(!rallying || QDELETED(src)) + return + + // Exit rally mode + rallying = FALSE + + // If we still have a target, advance toward them + if(target && !QDELETED(target)) + visible_message(span_danger("[src] advances with reinforcements!")) + + // Move toward target if they're far away + var/dist = get_dist(src, target) + if(dist > 3) + Goto(target, move_to_delay, 1) + + // Fallback: If no target but we have rally point, move there + else if(rally_point && !QDELETED(rally_point)) + visible_message(span_danger("[src] advances toward the last sighting!")) + Goto(rally_point, move_to_delay, 2) + + // Clear rally point + rally_point = null + +// Receive lost target alert from ally +/mob/living/simple_animal/hostile/proc/receive_lost_alert(atom/lost_target, turf/last_location) + // If we're searching for the same target, update our search location + if(searching && remembered_target == lost_target) + last_known_location = last_location + visible_message(span_notice("[src] updates their search pattern...")) + +// DOOR OPENING - smart mobs can open doors instead of destroying them +/mob/living/simple_animal/hostile/proc/try_open_door(atom/D) + if(!can_open_doors) + return FALSE + + if(!D || QDELETED(D)) + return FALSE + + // Check cooldown + if(!COOLDOWN_FINISHED(src, door_open_cooldown)) + return FALSE + + COOLDOWN_START(src, door_open_cooldown, door_open_delay) + + // Handle simple doors (wooden, metal, etc) + if(istype(D, /obj/structure/simple_door)) + var/obj/structure/simple_door/SD = D + + // Check if door has a padlock + if(SD.padlock && SD.padlock.locked) + return FALSE // Can't open locked doors + + // Check if door is already open + if(!SD.density) + return FALSE + + // Check if door is moving + if(SD.moving) + return FALSE + + // Open the door using the proper method + visible_message(span_notice("[src] opens [SD].")) + SD.SwitchState(TRUE) // Use SwitchState with animate = TRUE + return TRUE + + // Handle airlocks + if(istype(D, /obj/machinery/door/airlock)) + if(!can_open_airlocks) + return FALSE + + var/obj/machinery/door/airlock/A = D + if(A.locked || A.welded) + return FALSE // Can't open locked/welded doors + + // Try to open it + if(A.density) // Door is closed + visible_message(span_notice("[src] opens [A].")) + INVOKE_ASYNC(A, TYPE_PROC_REF(/obj/machinery/door, open)) + return TRUE + + return FALSE + + // Handle other machinery doors + if(istype(D, /obj/machinery/door)) + var/obj/machinery/door/MD = D + + // Try to open it + if(MD.density) // Door is closed + visible_message(span_notice("[src] opens [MD].")) + INVOKE_ASYNC(MD, TYPE_PROC_REF(/obj/machinery/door, open)) + return TRUE + + return FALSE + +// CURIOSITY - Check door during idle investigation +/mob/living/simple_animal/hostile/proc/curiosity_check_door(atom/door) + if(!door || QDELETED(door) || stat == DEAD || AIStatus == AI_OFF) + return + + // Only proceed if we're still idle and near the door + if(AIStatus != AI_IDLE || get_dist(src, door) > 2) + return + + // Try to open the door + if(get_dist(src, door) <= 1 && try_open_door(door)) + visible_message(span_notice("[src] peeks through [door]...")) + + // Wait a moment for door to open + sleep(5) + + // Look for targets on the other side + var/list/check_targets = ListTargets() + if(check_targets && check_targets.len > 0) + for(var/atom/possible_target in check_targets) + if(isliving(possible_target)) + var/mob/living/L = possible_target + if(L.stat == DEAD || L.ckey && !L.client) + continue + if(!CanAttack(possible_target)) + continue + + // Found someone! Wake up and aggro properly + visible_message(span_danger("[src] spots [possible_target] through the door!")) + toggle_ai(AI_ON) + last_target_sighting = world.time + remembered_target = possible_target + GiveTarget(possible_target) + last_known_location = get_turf(possible_target) + walk(src, 0) + call_for_backup(possible_target, "found") + COOLDOWN_RESET(src, sight_shoot_delay) + LosePatience() + return + + // Didn't see anyone - check nearby containers (within 5 tiles beyond the door) + var/turf/door_turf = get_turf(door) + if(door_turf) + var/list/nearby_containers = list() + for(var/turf/T in range(5, door_turf)) + for(var/obj/structure/closet/C in T) + if(!C.opened && !C.welded && !C.locked) + nearby_containers += C + + if(nearby_containers.len > 0) + // Pick closest container + var/obj/structure/closet/closest_container = null + var/closest_dist = 999 + for(var/obj/structure/closet/C in nearby_containers) + var/dist = get_dist(src, C) + if(dist < closest_dist) + closest_dist = dist + closest_container = C + + if(closest_container) + visible_message(span_notice("[src] investigates [closest_container]...")) + // Move toward it + Goto(closest_container, move_to_delay, 0) + // Check it after getting close + addtimer(CALLBACK(src, PROC_REF(curiosity_check_container), closest_container), 3 SECONDS, TIMER_DELETE_ME) + return + +// CURIOSITY - Check container during idle investigation +/mob/living/simple_animal/hostile/proc/curiosity_check_container(obj/structure/closet/container) + if(!container || QDELETED(container) || stat == DEAD || AIStatus == AI_OFF) + return + + // Only proceed if we're still idle and near the container + if(AIStatus != AI_IDLE || get_dist(src, container) > 1) + return + + // Check if container is still closed + if(container.opened || container.welded || container.locked) + return + + visible_message(span_warning("[src] searches [container]...")) + + // Check container contents BEFORE opening (to catch hiding mobs) + for(var/atom/A in container.contents) + if(isliving(A)) + var/mob/living/L = A + if(L.stat == DEAD || L.ckey && !L.client) + continue + if(CanAttack(L)) + // Found someone hiding! Open and aggro! + container.open(src) + sleep(5) // Wait for animation + visible_message(span_danger("[src] found [L] hiding in [container]!")) + toggle_ai(AI_ON) + last_target_sighting = world.time + remembered_target = L + GiveTarget(L) + last_known_location = get_turf(L) + walk(src, 0) + call_for_backup(L, "found") + COOLDOWN_RESET(src, sight_shoot_delay) + LosePatience() + return + + // No one inside - just open it + container.open(src) + sleep(5) + + // Check if anyone visible nearby now (they might have stepped out) + var/list/check_targets = ListTargets() + if(check_targets && check_targets.len > 0) + for(var/atom/possible_target in check_targets) + if(isliving(possible_target)) + var/mob/living/L = possible_target + if(L.stat == DEAD || L.ckey && !L.client) + continue + if(!CanAttack(possible_target)) + continue + + // Found someone nearby - aggro! + visible_message(span_danger("[src] spots [possible_target]!")) + toggle_ai(AI_ON) + last_target_sighting = world.time + remembered_target = possible_target + GiveTarget(possible_target) + last_known_location = get_turf(possible_target) + walk(src, 0) + call_for_backup(possible_target, "found") + COOLDOWN_RESET(src, sight_shoot_delay) + LosePatience() + return + +// Z-LEVEL COMMITMENT - prevents constant up/down movement +/mob/living/simple_animal/hostile/proc/commit_to_z_level() + committed_z_level = z + z_commit_time = world.time + recent_climbs = 0 -/mob/living/simple_animal/hostile/proc/Aggro() - if(ckey) - return TRUE - vision_range = aggro_vision_range - if(target && LAZYLEN(emote_taunt) && prob(taunt_chance)) - INVOKE_ASYNC(src, PROC_REF(emote), "me", EMOTE_VISIBLE, "[pick(emote_taunt)] at [target].") - taunt_chance = max(taunt_chance-7,2) - if(LAZYLEN(emote_taunt_sound)) - var/taunt_choice = pick(emote_taunt_sound) - playsound(loc, taunt_choice, 50, 0, vary = FALSE, frequency = SOUND_FREQ_NORMALIZED(sound_pitch, vary_pitches[1], vary_pitches[2])) +/mob/living/simple_animal/hostile/proc/should_change_z_level() + // Reset climb counter every 30 seconds + if((world.time - last_climb_reset) > 300) + recent_climbs = 0 + last_climb_reset = world.time + + // If we've climbed 3+ times recently, commit to current Z + if(recent_climbs >= 3) + commit_to_z_level() + visible_message(span_notice("[src] decides to stay on this level for now.")) + return FALSE + + // If we just committed to a Z-level, don't change + if(committed_z_level == z && (world.time - z_commit_time) < z_commit_duration) + return FALSE + + return TRUE +/mob/living/simple_animal/hostile/proc/record_z_level_change() + recent_climbs++ + last_climb_reset = world.time + last_successful_z_move = world.time -/mob/living/simple_animal/hostile/proc/LoseAggro() - stop_automated_movement = 0 - vision_range = initial(vision_range) - taunt_chance = initial(taunt_chance) +// SOUND DETECTION - hear gunfire and move toward it (WITH MUFFLING) +/mob/living/simple_animal/hostile/proc/hear_combat_sound(turf/sound_location, atom/sound_source) + if(!can_hear_combat) + return + + if(!sound_location || QDELETED(sound_location)) + return + + // Calculate muffled range + var/effective_range = calculate_muffled_sound_range(sound_location, combat_hearing_range) + + // Ignore sounds beyond effective range + var/distance = get_dist(src, sound_location) + var/z_distance = abs(z - sound_location.z) + + if(distance > effective_range) + return + + if(z_distance > 1) // Only hear sounds 1 Z-level away max + return + + last_combat_sound = world.time + + // Don't give exact location - pick a random spot near the sound (simulates hearing direction) + var/list/investigation_turfs = list() + for(var/turf/T in range(3, sound_location)) // Smaller radius for more accurate investigation + if(istype(T, /turf/open)) + investigation_turfs += T + + var/turf/investigation_spot = sound_location + if(investigation_turfs.len) + investigation_spot = pick(investigation_turfs) + + // WAKE UP if idle - sounds should always wake mobs + if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF) + toggle_ai(AI_ON) + visible_message(span_warning("[src] perks up at the sound of combat!")) + + // If we're stuck on stairs or in a loop, this breaks us out + if(pursuing_z_target || (recent_climbs >= 2)) + // Commit to the Z-level the sound came from + if(sound_location.z != z && can_z_move) + committed_z_level = 0 // Clear commitment + recent_climbs = 0 + z_commit_time = 0 + + if(remembered_target) + last_known_location = investigation_spot // Use approximate location + + if(searching) + last_known_location = investigation_spot + else if(sound_location.z == z) + commit_to_z_level() + + if(searching || !target) + last_known_location = investigation_spot + if(!searching) + enter_search_mode() + + // ACTIVELY INVESTIGATE - move toward the sound! + if(!target) // Only investigate if we don't have a target + last_known_location = investigation_spot + + if(!searching) + enter_search_mode() + visible_message(span_notice("[src] starts investigating the commotion...")) + else + // Already searching, update location + visible_message(span_notice("[src] adjusts their search toward the gunfire...")) + else if(searching) + // We have a remembered target but are searching - update search location + last_known_location = investigation_spot + visible_message(span_notice("[src] adjusts their search toward the gunfire...")) -/mob/living/simple_animal/hostile/proc/LoseTarget() - // Check if target just changed Z-levels before giving up - if(target && can_z_move && isliving(target)) - var/mob/living/L = target - if(L.z != z) // Target is on different Z! - if(attempt_z_pursuit()) - return // Don't lose target, we're pursuing +// Hear impact sounds (bullet hitting wall/target) - smaller radius +/mob/living/simple_animal/hostile/proc/hear_impact_sound(turf/impact_location, atom/sound_source) + if(!can_hear_combat) + return - GiveTarget(null) - approaching_target = FALSE - in_melee = FALSE - walk(src, 0) - LoseAggro() + if(!impact_location || QDELETED(impact_location)) + return + + // Calculate muffled range (now 7 tiles to match combat sounds) + var/effective_range = calculate_muffled_sound_range(impact_location, impact_hearing_range) + + // Ignore sounds beyond effective range + var/distance = get_dist(src, impact_location) + var/z_distance = abs(z - impact_location.z) + + if(distance > effective_range) + return + + if(z_distance > 1) // Only hear sounds 1 Z-level away max + return + + last_combat_sound = world.time + + // Use impact location directly for CPU efficiency (no random picking) + var/turf/investigation_spot = impact_location + + // ALWAYS WAKE UP - impacts should alert idle mobs + if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF) + toggle_ai(AI_ON) + visible_message(span_warning("[src] hears an impact nearby!")) + + // AGGRESSIVE DOOR CHECKING - open doors between us and impact (ONLY ADJACENT) + if(can_open_doors) + var/list/path = getline(get_turf(src), impact_location) + for(var/turf/T in path) + // REALISM FIX: Only open doors within melee range (adjacent) + var/door_distance = get_dist(src, T) + if(door_distance > 1) + continue // Too far to open + + // Check for simple doors + for(var/obj/structure/simple_door/SD in T) + if(SD.density && try_open_door(SD)) + visible_message(span_notice("[src] opens [SD] to investigate!")) + break // Only open one door per check for performance + + // Check for machinery doors + for(var/obj/machinery/door/D in T) + if(D.density && try_open_door(D)) + visible_message(span_notice("[src] opens [D] to investigate!")) + break // Only open one door per check for performance + + // INVESTIGATE - move toward the impact + if(!target) + last_known_location = investigation_spot + + if(!searching) + enter_search_mode() + visible_message(span_notice("[src] goes to investigate the noise...")) + else + last_known_location = investigation_spot + else if(searching) + // Update search location to be closer to impact + last_known_location = investigation_spot -/mob/living/simple_animal/hostile/proc/attempt_z_pursuit() - if(!target || !can_z_move) - return FALSE +/mob/living/simple_animal/hostile/proc/hear_gunshot(turf/shot_location, atom/shooter) + if(!can_hear_combat) + return - if(world.time < last_z_move_attempt + z_move_delay) - return FALSE + if(!shot_location || QDELETED(shot_location)) + return - last_z_move_attempt = world.time + // Check if shooter is an enemy + if(shooter && faction_check_mob(shooter)) + return // Don't care about friendly fire - var/mob/living/L = target - if(!istype(L)) - return FALSE + // Calculate muffled range + var/effective_range = calculate_muffled_sound_range(shot_location, combat_hearing_range) - target_last_z = L.z + var/distance = get_dist(src, shot_location) + var/z_distance = abs(z - shot_location.z) - var/went_up = (L.z > z) - var/went_down = (L.z < z) + if(distance > effective_range) + return - if(!went_up && !went_down) - return FALSE + if(z_distance > 1) + return - var/list/z_structures = list() + // Wake up if idle + if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF) + toggle_ai(AI_ON) - if(went_up) - for(var/obj/structure/stairs/S in view(vision_range, src)) - if(S.isTerminator()) - z_structures += S + // If we don't have a target, investigate the sound + if(!target && !searching) + last_known_location = shot_location + enter_search_mode() + visible_message(span_warning("[src] perks up at the sound of gunfire!")) + +// LIGHT DETECTION - detect and investigate light sources in darkness +/mob/living/simple_animal/hostile/proc/detect_light_source() + if(!can_detect_light) + return + + // Check cooldown + if(world.time < last_light_detected + light_detection_cooldown) + return + + // Check if we're in darkness (if required) + if(only_detect_lights_in_darkness) + var/turf/my_turf = get_turf(src) + if(!my_turf) + return - for(var/obj/structure/ladder/L2 in view(vision_range, src)) - if(L2.up) - z_structures += L2 + // Check the ambient light level at our location + var/light_level = my_turf.get_lumcount() + if(light_level > darkness_threshold) + return // Too bright, don't bother detecting lights - else if(went_down) - for(var/turf/open/transparent/openspace/OS in view(vision_range, src)) - z_structures += OS + // ONLY check for living mobs with lights - not objects + var/list/potential_lights = list() + + // FIX: Use the INTEGRATED vision system instead of raw view() + // Check all mobs in range, but filter by effective vision range + for(var/mob/living/M in view(light_detection_range, src)) + if(M == src) + continue + + // Don't detect friendly faction members + if(faction_check_mob(M)) + continue - for(var/obj/structure/ladder/L2 in view(vision_range, src)) - if(L2.down) - z_structures += L2 + // Check if they're emitting enough light + if(M.light_range < light_detection_threshold) + continue + + // FIX: Apply the integrated vision system + // This checks BOTH lighting and direction + var/effective_range = get_effective_vision_range(M) + var/actual_distance = get_dist(src, M) + + // Only detect if within our effective vision range + if(actual_distance <= effective_range) + potential_lights += M - if(!z_structures.len) + // If we found any lights, investigate the closest one + if(potential_lights.len > 0) + var/atom/closest_light = null + var/closest_distance = INFINITY + + for(var/atom/light_source in potential_lights) + var/distance = get_dist(src, light_source) + if(distance < closest_distance) + closest_distance = distance + closest_light = light_source + + if(closest_light) + var/turf/light_location = get_turf(closest_light) + + // Update cooldown + last_light_detected = world.time + last_light_location = light_location + + // Wake up if idle + if(AIStatus == AI_IDLE || AIStatus == AI_Z_OFF) + toggle_ai(AI_ON) + + // If it's a mob and we don't have a target, acquire it + if(ismob(closest_light) && !target) + GiveTarget(closest_light) + visible_message(span_warning("[src] spots a light source in the darkness!")) + return + + // Otherwise investigate the location + if(!target && !searching) + last_known_location = light_location + enter_search_mode() + visible_message(span_warning("[src] notices a light in the darkness...")) + else if(searching && !target) + // Update search location if already searching + last_known_location = light_location + +// CORNER NAVIGATION - try alternate paths when stuck at corners +/mob/living/simple_animal/hostile/proc/navigate_around_corner() + if(!target || !CHECK_BITFIELD(mobility_flags, MOBILITY_MOVE)) return FALSE - var/atom/nearest = get_closest_atom(/atom, z_structures, src) + var/turf/my_turf = get_turf(src) + var/turf/target_turf = get_turf(target) - visible_message(span_danger("[src] pursues [target] [went_up ? "upward" : "downward"]!")) + if(!my_turf || !target_turf) + return FALSE - LosePatience() - GainPatience() - COOLDOWN_RESET(src, sight_shoot_delay) + // Get direction to target + var/target_dir = get_dir(my_turf, target_turf) - if(went_up && istype(nearest, /obj/structure/stairs)) - var/obj/structure/stairs/S = nearest + // Try moving perpendicular to the target direction to navigate around corners + // This helps when stuck at walls/corners + var/list/sidestep_dirs = list() + + // Build list of perpendicular directions based on primary direction + switch(target_dir) + if(NORTH) + sidestep_dirs = list(EAST, WEST) + if(SOUTH) + sidestep_dirs = list(EAST, WEST) + if(EAST) + sidestep_dirs = list(NORTH, SOUTH) + if(WEST) + sidestep_dirs = list(NORTH, SOUTH) + if(NORTHEAST) + sidestep_dirs = list(NORTH, EAST, SOUTHEAST, NORTHWEST) + if(NORTHWEST) + sidestep_dirs = list(NORTH, WEST, SOUTHWEST, NORTHEAST) + if(SOUTHEAST) + sidestep_dirs = list(SOUTH, EAST, NORTHEAST, SOUTHWEST) + if(SOUTHWEST) + sidestep_dirs = list(SOUTH, WEST, NORTHWEST, SOUTHEAST) + + // Shuffle to avoid predictable patterns + sidestep_dirs = shuffle(sidestep_dirs) + + // Try each sidestep direction + for(var/step_dir in sidestep_dirs) + var/turf/test_turf = get_step(my_turf, step_dir) - // Check if we're already on the stairs - var/turf/our_turf = get_turf(src) - var/turf/stairs_turf = get_turf(S) + if(!test_turf) + continue - if(our_turf == stairs_turf) - // We're ON the stairs, just try to step in the stairs' direction - step(src, S.dir) - else - // Path to the stairs themselves, not beyond them - // Use Goto with minimum_distance 0 to actually step onto them - Goto(S, move_to_delay, 0) - else - // For going down or ladders, just path to them - Goto(nearest, move_to_delay, 0) + // Check if this turf is passable + if(test_turf.density) + continue + + // Check for blocking objects + var/blocked = FALSE + for(var/atom/movable/AM in test_turf) + if(AM.density && AM != target) + // Check if we can open it (door) + if(can_open_doors && (istype(AM, /obj/structure/simple_door) || istype(AM, /obj/machinery/door))) + // Try opening the door + try_open_door(AM) + blocked = FALSE + break + else + blocked = TRUE + break + + if(blocked) + continue + + // Check if this position gets us closer or provides better angle + var/current_dist = get_dist(my_turf, target_turf) + var/new_dist = get_dist(test_turf, target_turf) + + // Accept if same distance or closer (allows sidestepping around corners) + if(new_dist <= current_dist + 1) + // Try to move there + if(Move(test_turf, step_dir)) + // Success! Clear stuck state + return TRUE - return TRUE + // Couldn't find alternate path + return FALSE //////////////END HOSTILE MOB TARGETTING AND AGGRESSION//////////// @@ -817,10 +4279,120 @@ return FALSE +// Helper function to find a nearby position with clear line of sight to target +/mob/living/simple_animal/hostile/proc/find_firing_position(atom/target) + if(!target) + return null + + // Get current distance to target + var/current_dist = get_dist(src, target) + + // Try adjacent tiles that maintain roughly the same distance + var/list/candidate_positions = list() + var/turf/my_turf = get_turf(src) + + for(var/direction in GLOB.cardinals + GLOB.diagonals) + var/turf/T = get_step(my_turf, direction) + if(!T || T.density) + continue + + // Check if we can move there + var/blocked = FALSE + for(var/atom/A in T) + if(A.density && A != src) + blocked = TRUE + break + if(blocked) + continue + + // Check if this position has clear shot line (not just vision) + var/has_clear_shot = TRUE + var/turf/target_turf = get_turf(target) + var/list/shot_check = getline(T, target_turf) + + for(var/turf/check_turf in shot_check) + if(check_turf == T || check_turf == target_turf) + continue + + if(check_turf.density) + has_clear_shot = FALSE + break + + for(var/atom/A in check_turf) + if(A.density && !ismob(A)) + has_clear_shot = FALSE + break + + if(!has_clear_shot) + break + + if(!has_clear_shot) + continue + + // Check distance - prefer positions at similar distance + var/new_dist = get_dist(T, target) + var/dist_diff = abs(new_dist - current_dist) + + // Prioritize positions that maintain our distance + if(dist_diff <= 1) + candidate_positions[T] = 1 // High priority + else if(dist_diff <= 2) + candidate_positions[T] = 2 // Medium priority + else + candidate_positions[T] = 3 // Low priority (distance changed too much) + + // Return best position, or null if none found + if(candidate_positions.len > 0) + // Sort by priority and return best + var/turf/best_pos = null + var/best_priority = 999 + for(var/turf/T in candidate_positions) + if(candidate_positions[T] < best_priority) + best_priority = candidate_positions[T] + best_pos = T + return best_pos + + return null + /mob/living/simple_animal/hostile/proc/OpenFire(atom/A) + // CRITICAL: Check if we actually have line of sight before shooting + // Exception: Allow close-range shooting (within 2 tiles) even without perfect LOS + var/dist_to_target = get_dist(src, A) + if(dist_to_target > 2 && !can_see(src, A, get_effective_vision_range(A))) + return FALSE // Can't see target and not close enough, don't shoot + if(COOLDOWN_TIMELEFT(src, sight_shoot_delay)) return FALSE + // BARRICADE FIX: Don't shoot at targets blocked by structures + // Pathfinding will handle getting around obstacles + if(dist_to_target > 2 && is_target_blocked_by_structure(A)) + return FALSE // Target behind barricade/table/window - path around instead + + // TACTICAL: Check shot line for doors - prefer opening over shooting (ONLY ADJACENT) + if(can_open_doors && A.z == z) // Only check same Z-level + var/list/shot_line = getline(src, A) + for(var/turf/T in shot_line) + if(T == get_turf(src)) // Skip our own tile + continue + + // REALISM FIX: Only open doors within melee range (adjacent tiles) + var/door_distance = get_dist(src, T) + if(door_distance > 1) + continue // Too far to open, skip this door + + // Check for simple doors + for(var/obj/structure/simple_door/SD in T) + if(SD.density && try_open_door(SD)) + visible_message(span_notice("[src] opens [SD] to get a clear shot!")) + return FALSE // Don't shoot this turn, door is opening + + // Check for machinery doors + for(var/obj/machinery/door/D in T) + if(D.density && try_open_door(D)) + visible_message(span_notice("[src] opens [D] to get a clear shot!")) + return FALSE // Don't shoot this turn, door is opening + // Allow shooting through openspace if(A.z != z) var/can_shoot = FALSE @@ -851,11 +4423,11 @@ if(rapid > 1) var/datum/callback/cb = CALLBACK(src, PROC_REF(Shoot), A) for(var/i in 1 to rapid) - addtimer(cb, (i - 1)*rapid_fire_delay) + addtimer(cb, (i - 1)*rapid_fire_delay, TIMER_DELETE_ME) else Shoot(A) for(var/i in 1 to extra_projectiles) - addtimer(CALLBACK(src, PROC_REF(Shoot), A), i * auto_fire_delay) + addtimer(CALLBACK(src, PROC_REF(Shoot), A), i * auto_fire_delay, TIMER_DELETE_ME) ranged_cooldown = world.time + ranged_cooldown_time if(sound_after_shooting) addtimer(CALLBACK(GLOBAL_PROC,GLOBAL_PROC_REF(playsound), src, sound_after_shooting, 100, 0, 0), sound_after_shooting_delay, TIMER_STOPPABLE) @@ -915,12 +4487,116 @@ /mob/living/simple_animal/hostile/proc/CanSmashTurfs(turf/T) return iswallturf(T) || ismineralturf(T) +// CHECK SHOT LINE - detect if projectiles will hit walls even with diagonal vision +/mob/living/simple_animal/hostile/proc/is_shot_blocked(atom/target) + if(!target) + return TRUE + + // Get the shot line from us to target + var/turf/source_turf = get_turf(src) + var/turf/target_turf = get_turf(target) + + if(!source_turf || !target_turf) + return TRUE + + // Use getline to get the actual path a projectile would take + var/list/shot_line = getline(source_turf, target_turf) + + // Check each turf in the shot path + for(var/turf/T in shot_line) + if(T == source_turf || T == target_turf) + continue // Skip source and destination + + // Check if turf itself is dense + if(T.density) + return TRUE // Wall or dense turf blocks shot + + // Check for dense atoms (walls, windows, etc) + for(var/atom/A in T) + if(A.density) + // Ignore mobs - we can shoot over/past them + if(ismob(A)) + continue + // Ignore simple animals - can shoot over them + if(istype(A, /mob/living/simple_animal)) + continue + // Dense structure/obj blocks shot + return TRUE + + // Shot line is clear + return FALSE + -/mob/living/simple_animal/hostile/Move(atom/newloc, dir , step_x , step_y) +/mob/living/simple_animal/hostile/Move(atom/newloc, dir, step_x, step_y) + // FIX: Set facing direction when we move (so mobs turn naturally) + if(dir && dir != src.dir && !dodging) + setDir(dir) + if(dodging && approaching_target && prob(dodge_prob) && moving_diagonally == 0 && isturf(loc) && isturf(newloc)) return dodge(newloc,dir) + + // Check if we're stepping onto/off stairs + var/was_on_stairs = FALSE + for(var/obj/structure/stairs/S in loc) + was_on_stairs = TRUE + break + + var/moving_to_stairs = FALSE + for(var/obj/structure/stairs/S in newloc) + moving_to_stairs = TRUE + break + + // ANTI-LOOP: Count consecutive stair steps + if(was_on_stairs || moving_to_stairs) + consecutive_stair_steps++ + + if(consecutive_stair_steps >= max_consecutive_stair_steps) + // Too many stair steps! Force commit and stop pursuing + commit_to_z_level() + pursuing_z_target = FALSE + z_pursuit_structure = null + consecutive_stair_steps = 0 + + visible_message(span_notice("[src] stops at the stairs.")) + return FALSE // Don't move onto stairs else - return ..() + consecutive_stair_steps = 0 + + . = ..() // Do the move + + if(!.) // Move failed + return FALSE + + // IMMEDIATE LADDER CLIMBING + if(can_climb_ladders && pursuing_z_target && target && target.z != z && !client) + if((world.time - last_successful_z_move) < z_move_success_cooldown) + return . + + var/obj/structure/ladder/unbreakable/L = locate() in loc + if(L) + var/went_up = (target.z > z) + var/went_down = (target.z < z) + + if(went_up && L.up) + visible_message(span_warning("[src] immediately climbs up the ladder!")) + zMove(target = get_turf(L.up), z_move_flags = ZMOVE_CHECK_PULLEDBY|ZMOVE_ALLOW_BUCKLED|ZMOVE_INCLUDE_PULLED) + pursuing_z_target = FALSE + z_pursuit_structure = null + last_z_move_attempt = world.time + last_successful_z_move = world.time + record_z_level_change() + consecutive_stair_steps = 0 + return TRUE + else if(went_down && L.down) + visible_message(span_warning("[src] immediately climbs down the ladder!")) + zMove(target = get_turf(L.down), z_move_flags = ZMOVE_CHECK_PULLEDBY|ZMOVE_ALLOW_BUCKLED|ZMOVE_INCLUDE_PULLED) + pursuing_z_target = FALSE + z_pursuit_structure = null + last_z_move_attempt = world.time + last_successful_z_move = world.time + record_z_level_change() + consecutive_stair_steps = 0 + return TRUE /mob/living/simple_animal/hostile/proc/dodge(moving_to,move_direction) //Assuming we move towards the target we want to swerve toward them to get closer @@ -932,6 +4608,70 @@ . = Move(moving_to,move_direction) dodging = TRUE +// SPATIAL AWARENESS - scan for doors we can use +/mob/living/simple_animal/hostile/proc/scan_for_doors() + if(!can_open_doors) + return + + // PERFORMANCE: Reduced scan range from vision_range to fixed 5 to reduce lag + // Direct object iteration instead of turf iteration + var/scan_range = 5 + + for(var/obj/machinery/door/D in range(scan_range, src)) + // Add to known doors if not already there + var/door_loc = get_turf(D) + if(door_loc && !(door_loc in known_doors)) + known_doors += door_loc + + for(var/obj/structure/simple_door/SD in range(scan_range, src)) + // Add to known doors if not already there + var/door_loc = get_turf(SD) + if(door_loc && !(door_loc in known_doors)) + known_doors += door_loc + + // Limit memory size to prevent bloat (keep 20 most recent) + if(known_doors.len > 20) + known_doors.Cut(1, known_doors.len - 20) + +// Try alternate paths using door memory +/mob/living/simple_animal/hostile/proc/try_alternate_path() + if(!target || !can_open_doors) + return FALSE + + if(path_attempts >= max_path_attempts) + return FALSE // Tried enough times + + path_attempts++ + + // Look for known doors that might provide alternate routes + var/list/potential_doors = list() + + for(var/turf/door_loc in known_doors) + if(QDELETED(door_loc)) + known_doors -= door_loc + continue + + // Check if door is in a useful direction + var/door_dist = get_dist(src, door_loc) + var/target_dist = get_dist(src, target) + + // Door should be closer to us than target, but still reasonably close to target + if(door_dist < target_dist && door_dist <= 10) + potential_doors += door_loc + + if(!potential_doors.len) + return FALSE + + // Pick a random door to try + var/turf/chosen_door = pick(potential_doors) + + visible_message(span_notice("[src] looks for another way around...")) + + // Pathfind to the door + Goto(chosen_door, move_to_delay, 0) + + return TRUE + /mob/living/simple_animal/hostile/proc/DestroyObjectsInDirection(direction) var/turf/T = get_step(targets_from, direction) if(T && T.Adjacent(targets_from)) @@ -944,19 +4684,125 @@ /mob/living/simple_animal/hostile/proc/DestroyPathToTarget() - if(environment_smash) - EscapeConfinement() - var/dir_to_target = get_dir(targets_from, target) - var/dir_list = list() - if(dir_to_target in GLOB.diagonals) //it's diagonal, so we need two directions to hit - for(var/direction in GLOB.cardinals) - if(direction & dir_to_target) - dir_list += direction - else - dir_list += dir_to_target - for(var/direction in dir_list) //now we hit all of the directions we got in this fashion, since it's the only directions we should actually need - DestroyObjectsInDirection(direction) + // This should only be called when we're REALLY stuck (after 15 seconds of trying) + + // SPECIAL CASE: If searching, mark current location as unreachable and let search continue + if(searching) + // Mark our stuck position as searched so we don't try to go there again + var/turf/stuck_at = get_turf(src) + if(stuck_at && !(stuck_at in searched_turfs)) + searched_turfs += stuck_at + // Also mark nearby turfs as searched to help pathfinding avoid this area + for(var/turf/T in range(2, stuck_at)) + if(!(T in searched_turfs)) + searched_turfs += T + // Reset stuck state so we can try a different search location + is_stuck = FALSE + stuck_position = null + stuck_time = 0 + path_attempts = 0 + return + + // PRIORITY 1: Try opening doors aggressively first + if(can_open_doors) + var/opened_something = FALSE + + // Try all adjacent turfs for doors + for(var/dir in GLOB.cardinals) + var/turf/T = get_step(src, dir) + if(!T) + continue + + // Try simple doors + for(var/obj/structure/simple_door/SD in T) + if(SD.density && try_open_door(SD)) + opened_something = TRUE + visible_message(span_notice("[src] forces open [SD]!")) + + // Try machinery doors + for(var/obj/machinery/door/D in T) + if(D.density && try_open_door(D)) + opened_something = TRUE + visible_message(span_notice("[src] forces open [D]!")) + + if(opened_something) + // Give the door time to open + return + + // PRIORITY 2: Try alternate paths using door memory + if(can_open_doors && try_alternate_path()) + visible_message(span_notice("[src] tries to find another route...")) + return + + // GIVE UP: If exhausted all path attempts and been stuck for 30+ seconds, give up entirely + if(path_attempts >= max_path_attempts && is_stuck && (world.time - stuck_time > 300)) + visible_message(span_notice("[src] gives up the chase.")) + // Set exhaustion cooldown to prevent immediate re-acquisition + last_give_up_time = world.time + // Force clear ALL target state + searching = FALSE + remembered_target = null + last_target_sighting = 0 + LoseTarget() + return + + // PRIORITY 3: Only smash if we can't open doors AND we're REALLY stuck + if(!environment_smash) + // Can't smash - if we've exhausted options and been stuck 20+ seconds, give up + if(path_attempts >= max_path_attempts && is_stuck && (world.time - stuck_time > 200)) + visible_message(span_notice("[src] gives up the chase.")) + // Set exhaustion cooldown to prevent immediate re-acquisition + last_give_up_time = world.time + // Force clear ALL target state + searching = FALSE + remembered_target = null + last_target_sighting = 0 + LoseTarget() + return + + // Check if we have direct line of sight to target + var/has_direct_los = target && can_see(src, target, get_effective_vision_range(target)) + + // Smash immediately with LOS, otherwise wait for delay + if(!has_direct_los && (!is_stuck || (world.time - stuck_time < smash_delay))) + return // Not stuck long enough yet + + // ANTI-SPAM: Check smash cooldown + if((world.time - last_smash_time) < smash_cooldown) + return // Too soon since last smash + + // ONE frustration message with cooldown + visible_message(span_danger("[src] gets frustrated and starts breaking things!")) + last_smash_time = world.time + + // Now we can smash + EscapeConfinement() + var/dir_to_target = get_dir(targets_from, target) + var/dir_list = list() + if(dir_to_target in GLOB.diagonals) + for(var/direction in GLOB.cardinals) + if(direction & dir_to_target) + dir_list += direction + else + dir_list += dir_to_target + + // Only smash if target is close (prevents smashing through entire base) + var/target_dist = get_dist(targets_from, target) + if(target_dist <= 5) // Only smash if within 5 tiles + for(var/direction in dir_list) + SmashInDirection(direction) +// Pure smashing - no door checking (that's handled above now) +/mob/living/simple_animal/hostile/proc/SmashInDirection(direction) + var/turf/T = get_step(targets_from, direction) + if(T && T.Adjacent(targets_from)) + if(environment_smash) + if(CanSmashTurfs(T)) + T.attack_animal(src) + for(var/obj/O in T) + if(O.density && environment_smash >= ENVIRONMENT_SMASH_STRUCTURES && !O.IsObscured()) + O.attack_animal(src) + return mob/living/simple_animal/hostile/proc/DestroySurroundings() // for use with megafauna destroying everything around them if(environment_smash) @@ -1036,6 +4882,8 @@ mob/living/simple_animal/hostile/proc/DestroySurroundings() // for use with mega search_objects = value /mob/living/simple_animal/hostile/consider_wakeup() + if(stat == DEAD) // Dead mobs don't wake up + return ..() var/list/tlist var/turf/T = get_turf(src) @@ -1143,7 +4991,7 @@ mob/living/simple_animal/hostile/proc/DestroySurroundings() // for use with mega visible_message(span_green("[src] shudders as the EMP overloads its servos!")) LoseTarget() toggle_ai(AI_OFF) - addtimer(CALLBACK(src, PROC_REF(un_emp_stun)), min(intensity, 3 SECONDS)) + addtimer(CALLBACK(src, PROC_REF(un_emp_stun)), min(intensity, 3 SECONDS), TIMER_DELETE_ME) /mob/living/simple_animal/hostile/proc/un_emp_stun() active_emp_flags -= MOB_EMP_STUN @@ -1160,7 +5008,7 @@ mob/living/simple_animal/hostile/proc/DestroySurroundings() // for use with mega visible_message(span_green("[src] lets out a burst of static and whips its gun around wildly!")) var/list/old_faction = faction faction = null - addtimer(CALLBACK(src, PROC_REF(un_emp_berserk), old_faction), intensity SECONDS * 0.5) + addtimer(CALLBACK(src, PROC_REF(un_emp_berserk), old_faction), intensity SECONDS * 0.5, TIMER_DELETE_ME) /mob/living/simple_animal/hostile/proc/un_emp_berserk(list/unberserk) active_emp_flags -= MOB_EMP_BERSERK diff --git a/code/modules/mob/living/simple_animal/hostile/mushroom.dm b/code/modules/mob/living/simple_animal/hostile/mushroom.dm index 235c8530e04..15ba70c97d7 100644 --- a/code/modules/mob/living/simple_animal/hostile/mushroom.dm +++ b/code/modules/mob/living/simple_animal/hostile/mushroom.dm @@ -89,7 +89,7 @@ /mob/living/simple_animal/hostile/mushroom/adjustHealth(amount, updating_health = TRUE, forced = FALSE) //Possibility to flee from a fight just to make it more visually interesting if(!retreat_distance && prob(33)) retreat_distance = 5 - addtimer(CALLBACK(src, PROC_REF(stop_retreat)), 30) + addtimer(CALLBACK(src, PROC_REF(stop_retreat)), 30, TIMER_DELETE_ME) . = ..() /mob/living/simple_animal/hostile/mushroom/proc/stop_retreat() @@ -138,7 +138,7 @@ revive(full_heal = 1) UpdateMushroomCap() recovery_cooldown = 1 - addtimer(CALLBACK(src, PROC_REF(recovery_recharge)), 300) + addtimer(CALLBACK(src, PROC_REF(recovery_recharge)), 300, TIMER_DELETE_ME) /mob/living/simple_animal/hostile/mushroom/proc/recovery_recharge() recovery_cooldown = 0 diff --git a/code/modules/mob/living/simple_animal/hostile/venus_human_trap.dm b/code/modules/mob/living/simple_animal/hostile/venus_human_trap.dm index 76b3d8fba84..4c97999f6a4 100644 --- a/code/modules/mob/living/simple_animal/hostile/venus_human_trap.dm +++ b/code/modules/mob/living/simple_animal/hostile/venus_human_trap.dm @@ -21,7 +21,7 @@ for(var/turf/T in anchors) var/datum/beam/B = Beam(T, "vine", time=INFINITY, maxdistance=5, beam_type=/obj/effect/ebeam/vine) B.sleep_time = 10 //these shouldn't move, so let's slow down updates to 1 second (any slower and the deletion of the vines would be too slow) - addtimer(CALLBACK(src, PROC_REF(bear_fruit)), growth_time) + addtimer(CALLBACK(src, PROC_REF(bear_fruit)), growth_time, TIMER_DELETE_ME) /** * Spawns a venus human trap, then qdels itself. diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index bbd116f2731..f238848c6f5 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -39,6 +39,12 @@ GLOBAL_LIST_EMPTY(playmob_cooldowns) var/wander = 1 ///When set to 1 this stops the animal from moving when someone is pulling it. var/stop_automated_movement_when_pulled = 1 + ///The current direction the mob is facing while idle wandering + var/current_idle_direction = null + ///Last time (world.time) the mob changed its idle wander direction + var/idle_direction_change_time = 0 + ///Minimum time (in deciseconds) before changing idle direction (45 seconds = 450 deciseconds) + var/idle_direction_change_cooldown = 450 ///When someone interacts with the simple animal. ///Help-intent verb in present continuous tense. @@ -352,13 +358,11 @@ GLOBAL_LIST_EMPTY(playmob_cooldowns) abilities = null // Clear the list to break references for(var/obj/effect/proc_holder/ability in abilities_copy) - // Clear cross-references - if(ability.ranged_ability_user == src) - ability.ranged_ability_user = null + // DON'T manually clear ranged_ability_user - let proc_holder's Destroy() handle it via remove_ranged_ability() + // Just clear the action owner to prevent circular refs if(ability.action) if(ability.action.owner == src) ability.action.owner = null - // Don't need to remove from actions list since we're clearing it below qdel(ability) // Clean up ALL actions @@ -473,7 +477,18 @@ GLOBAL_LIST_EMPTY(playmob_cooldowns) return TRUE if(stop_automated_movement_when_pulled && pulledby) //Some animals don't move when pulled return TRUE - var/anydir = pick(GLOB.cardinals) + + // Direction persistence: only change idle direction after cooldown expires + var/anydir + if(!current_idle_direction || (world.time - idle_direction_change_time) >= idle_direction_change_cooldown) + // Cooldown expired or first time - pick a new random direction + anydir = pick(GLOB.cardinals) + current_idle_direction = anydir + idle_direction_change_time = world.time + else + // Use the persistent idle direction + anydir = current_idle_direction + if(Process_Spacemove(anydir)) Move(get_step(src, anydir), anydir) turns_since_move = 0 diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm index 46756dcee40..744f34bf2a0 100644 --- a/code/modules/mob/login.dm +++ b/code/modules/mob/login.dm @@ -53,11 +53,10 @@ mind?.hide_ckey = client?.prefs?.hide_ckey // BYOND 516: Initialize verbs for the client on login/reconnect - // Reconnect shorter: joining longer + // Only refresh verbs on body switch, not entire statbrowser (statbrowser loads once on client connection) if(client) var/delay = client.statbrowser_ready ? 1 : 50 addtimer(CALLBACK(client, TYPE_PROC_REF(/client, init_verbs)), delay) - addtimer(CALLBACK(client, TYPE_PROC_REF(/client, load_statbrowser)), delay) log_message("Client [key_name(src)] has taken ownership of mob [src]([src.type])", LOG_OWNERSHIP) SEND_SIGNAL(src, COMSIG_MOB_CLIENT_LOGIN, client) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 0caad361d77..d49cc0dd908 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -9,8 +9,12 @@ movespeed_modification = null // Clear mind reference to break circular references if(mind) - mind.current = null - mind = null + // Don't clear mind.current if there's a ghost that can still re-enter + // This prevents ghosts from being locked out of revival attempts + var/mob/dead/observer/ghost = mind.get_ghost(even_if_they_cant_reenter = FALSE) + if(!ghost) + mind.current = null + mind = null // Clear held items by destroying them BEFORE parent cleanup // This prevents item cleanup from trying to re-add items during component destruction if(held_items && held_items.len) diff --git a/code/modules/power/lighting.dm b/code/modules/power/lighting.dm index 04592678975..f1f75fcf259 100644 --- a/code/modules/power/lighting.dm +++ b/code/modules/power/lighting.dm @@ -575,7 +575,19 @@ . = ..() if(. && !QDELETED(src)) if(prob(damage_amount * 5)) - break_light_tube() + // Check if attacked_by is a projectile to get the firer + var/atom/shooter = null + var/silenced = FALSE + if(istype(attacked_by, /obj/item/projectile)) + var/obj/item/projectile/P = attacked_by + shooter = P.firer + silenced = P.suppressed + break_light_tube(0, shooter, silenced) + +// Override bullet_act to capture projectile info before damage +/obj/machinery/light/bullet_act(obj/item/projectile/P, def_zone) + . = ..() + // The take_damage proc will handle the rest @@ -720,7 +732,7 @@ // break the light and make sparks if was on -/obj/machinery/light/proc/break_light_tube(skip_sound_and_sparks = 0) +/obj/machinery/light/proc/break_light_tube(skip_sound_and_sparks = 0, atom/shooter = null, silenced = FALSE) if(status == LIGHT_EMPTY || status == LIGHT_BROKEN) return @@ -731,6 +743,18 @@ do_sparks(3, TRUE, src) status = LIGHT_BROKEN update() + + // HOSTILE MOB DETECTION: Alert nearby hostile mobs about light destruction + var/turf/impact_loc = get_turf(src) + if(impact_loc) + for(var/mob/living/simple_animal/hostile/H in range(9, impact_loc)) + if(H.stat == DEAD || H.ckey || !H.can_hear_combat) + continue + // Only alert mobs that can actually SEE the light being destroyed + // Exception: if NOT silenced, sound carries through walls (hearing, not seeing) + if(silenced && !can_see(H, impact_loc, 9)) + continue + H.detect_light_destruction(impact_loc, shooter, silenced) /obj/machinery/light/proc/fix() if(status == LIGHT_OK) diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index 4fd6d5067a4..d9daa76fa39 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -350,12 +350,22 @@ ATTACHMENTS distant_sound = shootprops[CSP_INDEX_DISTANT_SOUND], distant_range = shootprops[CSP_INDEX_DISTANT_RANGE] ) + + // Alert nearby hostile mobs to gunfire + if(!silenced) + for(var/mob/living/simple_animal/hostile/H in range(7, user)) + if(H.stat != DEAD && !H.ckey && H.can_hear_combat) + // Don't alert if shooter is same faction + if(!H.faction_check_mob(user)) + H.hear_gunshot(get_turf(user), user) + if(!silenced && message && COOLDOWN_FINISHED(src, shoot_message_antispam)) COOLDOWN_START(src, shoot_message_antispam, GUN_SHOOT_MESSAGE_ANTISPAM_TIME) if(pointblank) user.visible_message(span_danger("[user] fires [src] point blank at [pbtarget]!"), null, null, COMBAT_MESSAGE_RANGE) else user.visible_message(span_danger("[user] fires [src]!"), null, null, COMBAT_MESSAGE_RANGE) + kickback(user, P) //Adds logging to the attack log whenever anyone draws a gun, adds a pause after drawing a gun before you can do anything based on it's size diff --git a/code/modules/projectiles/guns/energy.dm b/code/modules/projectiles/guns/energy.dm index af210bc8ff4..14599974815 100644 --- a/code/modules/projectiles/guns/energy.dm +++ b/code/modules/projectiles/guns/energy.dm @@ -77,8 +77,21 @@ /obj/item/gun/energy/Destroy() STOP_PROCESSING(SSobj, src) + + // CRITICAL FIX: Energy guns reuse ammo casings from ammo_type list + // Don't let parent delete chambered since it's in ammo_type + // We need to clean up ammo_type ourselves + chambered = null // Clear reference without deleting + + // Clean up the ammo_type list and delete all the ammo casings + if(ammo_type) + for(var/obj/item/ammo_casing/energy/E in ammo_type) + qdel(E) + ammo_type = null + if(cell) QDEL_NULL(cell) + return ..() /obj/item/gun/energy/handle_atom_del(atom/A) diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index 5b0ae58174c..b6f61623de8 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -222,6 +222,10 @@ hit_limb = L.check_limb_hit(def_zone) SEND_SIGNAL(src, COMSIG_PROJECTILE_SELF_ON_HIT, firer, target, Angle, hit_limb) var/turf/target_loca = get_turf(target) + + // Alert hostile mobs about the impact (spawned to avoid blocking, optimized with early exits) + spawn(0) + notify_mobs_of_projectile_impact(target_loca, firer, src) var/hitx var/hity @@ -930,6 +934,35 @@ var/obj/item/projectile/P = A return istype(P) && P.armour_penetration +// GLOBAL PROJECTILE IMPACT HANDLER for hostile.dm +// CPU OPTIMIZED: Early exits, limited notifications, reduced range +/proc/notify_mobs_of_projectile_impact(turf/impact_turf, atom/firer, obj/item/projectile/P) + if(!impact_turf || !firer) + return + + var/notified = 0 + var/max_notifications = 8 // Limit notifications to prevent spam on large mob groups + + // CPU OPTIMIZATION: Reduced range from 20 to 5 tiles (50 tiles vs 400 tiles to check) + for(var/mob/living/simple_animal/hostile/M in range(5, impact_turf)) + if(M.stat == DEAD || M.ckey) + continue + + // Only alert mobs actively engaged (has target or searching) + if(!M.target && !M.searching) + continue + + // Check if mob can hear the impact (with muffling) + var/effective_range = M.calculate_muffled_sound_range(impact_turf, M.impact_hearing_range) + var/distance = get_dist(M, impact_turf) + var/z_distance = abs(M.z - impact_turf.z) + + if(distance <= effective_range && z_distance <= 1) + M.hear_impact_sound(impact_turf, firer) + notified++ + if(notified >= max_notifications) // Early exit after enough notifications + return + /obj/item/projectile/bullet/F13 name = "bullet" //Bullets library diff --git a/code/modules/projectiles/projectile/plasma.dm b/code/modules/projectiles/projectile/plasma.dm index 5df7b6b0b0b..b8fdade1593 100644 --- a/code/modules/projectiles/projectile/plasma.dm +++ b/code/modules/projectiles/projectile/plasma.dm @@ -48,3 +48,10 @@ obj/item/projectile/energy/evebolt irradiate = 25 stamina = 100 icon_state = "plasma3" + +// Radiation projectile used by glowing ghouls +/obj/item/projectile/radiation_thing + name = "radiation" + damage = 0 + irradiate = 20 + icon_state = "declone" diff --git a/code/modules/reagents/reagent_containers/hypospray.dm b/code/modules/reagents/reagent_containers/hypospray.dm index 23d7fd9a83b..813385e1213 100644 --- a/code/modules/reagents/reagent_containers/hypospray.dm +++ b/code/modules/reagents/reagent_containers/hypospray.dm @@ -109,6 +109,11 @@ if(!reagents.total_volume) to_chat(user, span_warning("[src] is empty!")) return + + // Prevent use on simple animals + if(isanimal(M) && !iscarbon(M)) + to_chat(user, span_warning("[src] is not designed for use on animals!")) + return if(M == user) M.visible_message(span_notice("[user] attempts to inject themselves with the [src].")) diff --git a/code/modules/surgery/healing.dm b/code/modules/surgery/healing.dm index 0d3c75ade0b..fb6cb765928 100644 --- a/code/modules/surgery/healing.dm +++ b/code/modules/surgery/healing.dm @@ -56,8 +56,8 @@ break /datum/surgery_step/heal/success(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery) - var/umsg = "You succeed in fixing some of [target]'s wounds" //no period, add initial space to "addons" - var/tmsg = "[user] fixes some of [target]'s wounds" //see above + var/umsg = "You succeed in fixing some of [target]'s wounds" + var/tmsg = "[user] fixes some of [target]'s wounds" var/urhealedamt_brute = brutehealing var/urhealedamt_burn = burnhealing var/urhealedamt_bleed = woundhealing @@ -66,7 +66,7 @@ urhealedamt_brute += round((target.getBruteLoss()/ missinghpbonus),0.1) urhealedamt_burn += round((target.getFireLoss()/ missinghpbonus),0.1) urhealedamt_bleed += round((target.getBleedLoss()/ missinghpbonus),0.1) - else //less healing bonus for the dead since they're expected to have lots of damage to begin with (to make TW into defib not TOO simple) + else urhealedamt_brute += round((target.getBruteLoss()/ (missinghpbonus*5)),0.1) urhealedamt_burn += round((target.getFireLoss()/ (missinghpbonus*5)),0.1) urhealedamt_bleed += round((target.getBleedLoss()/ (missinghpbonus*5)),0.1) @@ -76,6 +76,23 @@ urhealedamt_bleed *= 0.55 umsg += " as best as you can while they have clothing on" tmsg += " as best as they can while [target] has clothing on" + + // Skill 1 (INT >= 6, no surgery training) caps healing to just make the target revivable + // They'll survive but need meds to actually recover + if(ishuman(user)) + var/mob/living/carbon/human/H = user + if(H.get_surgery_skill() == 1) + var/current_total = target.getBruteLoss() + target.getFireLoss() + var/revive_threshold = target.maxHealth * 0.15 // just above crit/death line + var/healable_total = max(0, current_total - revive_threshold) + var/heal_cap = min(1.0, healable_total / max(1, urhealedamt_brute + urhealedamt_burn)) + urhealedamt_brute *= heal_cap + urhealedamt_burn *= heal_cap + urhealedamt_bleed *= heal_cap + if(heal_cap < 1.0) + umsg = "You crudely stabilize [target] — they'll need proper medical care to fully recover" + tmsg = "[user] crudely stabilizes [target]" + target.heal_bodypart_damage(urhealedamt_brute, urhealedamt_burn, bleed = urhealedamt_bleed) display_results(user, target, span_notice("[umsg]."), "[tmsg].", diff --git a/code/modules/tgui_panel/external.dm b/code/modules/tgui_panel/external.dm index 5ffc26aca8e..6a49c14fccf 100644 --- a/code/modules/tgui_panel/external.dm +++ b/code/modules/tgui_panel/external.dm @@ -24,7 +24,7 @@ // Wait for HTML to load, then reinitialize everything spawn(15) - init_verbs() // Restore all verb tabs + init_verbs(force = TRUE) // Force rebuild on manual reload // Restore admin tabs if they're an admin if(holder) diff --git a/fallout/code/modules/mob/living/living.dm b/fallout/code/modules/mob/living/living.dm index 3c863c37a52..3122231c400 100644 --- a/fallout/code/modules/mob/living/living.dm +++ b/fallout/code/modules/mob/living/living.dm @@ -24,6 +24,7 @@ if(.) pseudo_z_axis = newloc.get_fake_z() pixel_z = pseudo_z_axis + last_move_time = world.time // Track movement for sound detection /mob/living/carbon/update_stamina() var/total_health = getStaminaLoss() diff --git a/fallout/obj/stack/f13Cash.dm b/fallout/obj/stack/f13Cash.dm index 6a9ff47c122..61730ac5aba 100644 --- a/fallout/obj/stack/f13Cash.dm +++ b/fallout/obj/stack/f13Cash.dm @@ -37,7 +37,7 @@ max_amount = 15000 throwforce = 0 throw_speed = 2 - throw_range = 2 + throw_range = 7 w_class = WEIGHT_CLASS_TINY full_w_class = WEIGHT_CLASS_TINY resistance_flags = FLAMMABLE