diff --git a/code/__defines/mobs.dm b/code/__defines/mobs.dm index f610c0c2a3..881ba74c27 100644 --- a/code/__defines/mobs.dm +++ b/code/__defines/mobs.dm @@ -288,6 +288,19 @@ #define SA_ROBOTIC 3 #define SA_HUMANOID 4 +// More refined version of SA_* ""intelligence"" seperators. +// Now includes bitflags, so to target two classes you just do 'MOB_CLASS_ANIMAL|MOB_CLASS_HUMANOID' +#define MOB_CLASS_ANIMAL 1 // Simple mobs like saviks and bears. +#define MOB_CLASS_HUMANOID 2 // Non-robotic humanoids. +#define MOB_CLASS_CONSTRUCT 4 // Silicons, mechanical simple mobs, and FBPs. +#define MOB_CLASS_SLIME 8 // Everyone's favorite xenobiology specimen. +#define MOB_CLASS_ABERRATION 16 // Weird shit. +#define MOB_CLASS_DEMONIC 32 // Cult stuff. +#define MOB_CLASS_BOSS 64 // Future megafauna hopefully someday. +#define MOB_CLASS_ILLUSION 128 // Fake mobs, e.g. Technomancer illusions. + +#define MOB_CLASS_ALL (MOB_CLASS_ANIMAL|MOB_CLASS_HUMANOID|MOB_CLASS_CONSTRUCT|MOB_CLASS_SLIME|MOB_CLASS_ABERRATION|MOB_CLASS_DEMONIC|MOB_CLASS_BOSS|MOB_CLASS_ILLUSION) + // For slime commanding. Higher numbers allow for more actions. #define SLIME_COMMAND_OBEY 1 // When disciplined. #define SLIME_COMMAND_FACTION 2 // When in the same 'faction'. diff --git a/code/game/objects/effects/spiders.dm b/code/game/objects/effects/spiders.dm index d3b9853e06..5f0a1e7f45 100644 --- a/code/game/objects/effects/spiders.dm +++ b/code/game/objects/effects/spiders.dm @@ -56,9 +56,11 @@ /obj/effect/spider/stickyweb icon_state = "stickyweb1" - New() - if(prob(50)) - icon_state = "stickyweb2" + +/obj/effect/spider/stickyweb/initialize() + if(prob(50)) + icon_state = "stickyweb2" + return ..() /obj/effect/spider/stickyweb/CanPass(atom/movable/mover, turf/target, height=0, air_group=0) if(air_group || (height==0)) return 1 @@ -80,10 +82,12 @@ var/spiders_min = 6 var/spiders_max = 24 var/spider_type = /obj/effect/spider/spiderling - New() - pixel_x = rand(3,-3) - pixel_y = rand(3,-3) - processing_objects |= src + +/obj/effect/spider/eggcluster/initialize() + pixel_x = rand(3,-3) + pixel_y = rand(3,-3) + processing_objects |= src + return ..() /obj/effect/spider/eggcluster/New(var/location, var/atom/parent) get_light_and_color(parent) diff --git a/code/modules/ai/_defines.dm b/code/modules/ai/_defines.dm index 354f428e88..c72158134e 100644 --- a/code/modules/ai/_defines.dm +++ b/code/modules/ai/_defines.dm @@ -14,6 +14,12 @@ #define AI_LOG_DEBUG 4 // More detailed information about the flow of execution. #define AI_LOG_TRACE 5 // Even more detailed than the last. +// Results of pre-movement checks. +// Todo: Move outside AI code? +#define MOVEMENT_ON_COOLDOWN -1 // Recently moved and needs to try again soon. +#define MOVEMENT_FAILED 0 // Move() returned false for whatever reason and the mob didn't move. +#define MOVEMENT_SUCCESSFUL 1 // Move() returned true and the mob hopefully moved. + // Reasons for targets to not be valid. Based on why, the AI responds differently. #define AI_TARGET_VALID 0 // We can fight them. #define AI_TARGET_INVIS 1 // They were in field of view but became invisible. Switch to STANCE_BLINDFIGHT if no other viable targets exist. diff --git a/code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm b/code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm new file mode 100644 index 0000000000..206ce2c705 --- /dev/null +++ b/code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm @@ -0,0 +1,144 @@ +// AIs for simple mobs. + +/datum/ai_holder/simple_mob + hostile = TRUE // The majority of simplemobs are hostile. + cooperative = TRUE + returns_home = TRUE + can_flee = FALSE + speak_chance = 1 // If the mob's saylist is empty, nothing will happen. + +// For animals. +/datum/ai_holder/simple_mob/passive + hostile = FALSE + wander = TRUE + can_flee = TRUE + +// Ranged mobs. + +/datum/ai_holder/simple_mob/ranged + ranged = TRUE + +// Tries to not waste ammo. +/datum/ai_holder/simple_mob/ranged/careful + conserve_ammo = TRUE + +// Runs away from its target if within a certain distance. +/datum/ai_holder/simple_mob/ranged/kiting + pointblank = TRUE // So we don't need to copypaste post_melee_attack(). + var/run_if_this_close = 4 // If anything gets within this range, it'll try to move away. + +/datum/ai_holder/simple_mob/ranged/kiting/threatening + threaten = TRUE + threaten_delay = 1 SECOND // Less of a threat and more of pre-attack notice. + threaten_timeout = 30 SECONDS + +/datum/ai_holder/simple_mob/ranged/kiting/post_ranged_attack(atom/A) + if(get_dist(holder, A) < run_if_this_close) + holder.IMove(get_step_away(holder, A, run_if_this_close)) + holder.face_atom(A) + + +// Melee mobs. + +/datum/ai_holder/simple_mob/melee + +// Dances around the enemy its fighting, making it harder to fight back. +/datum/ai_holder/simple_mob/melee/evasive + +/datum/ai_holder/simple_mob/melee/evasive/post_melee_attack(atom/A) + if(holder.Adjacent(A)) + holder.IMove(get_step(holder, pick(alldirs))) + holder.face_atom(A) + +// The AI for hooligan crabs. Follows people for awhile. +/datum/ai_holder/simple_mob/melee/hooligan + hostile = FALSE + retaliate = TRUE + max_home_distance = 12 + var/random_follow = TRUE // Turn off if you want to bus with crabs. + +/datum/ai_holder/simple_mob/melee/hooligan/handle_stance_strategical() + ..() + if(random_follow && stance == STANCE_IDLE && !leader) + if(prob(10)) + for(var/mob/living/L in hearers(holder)) + if(!istype(L, holder)) // Don't follow other hooligan crabs. + holder.visible_message("\The [holder] starts to follow \the [L].") + set_follow(L, rand(20 SECONDS, 40 SECONDS)) + + +// The AI for nurse spiders. Wraps things in webs by 'attacking' them. +/datum/ai_holder/simple_mob/melee/nurse_spider + wander = TRUE + base_wander_delay = 8 + +// Get us unachored objects as an option as well. +/datum/ai_holder/simple_mob/melee/nurse_spider/list_targets() + . = ..() + + var/static/alternative_targets = typecacheof(list(/obj/item, /obj/structure)) + + for(var/AT in typecache_filter_list(range(vision_range, holder), alternative_targets)) + var/obj/O = AT + if(can_see(holder, O, vision_range) && !O.anchored) + . += O + +// Select an obj if no mobs are around. +/datum/ai_holder/melee/nurse_spider/pick_target(list/targets) + var/mobs_only = locate(/mob/living) in targets // If a mob is in the list of targets, then ignore objects. + if(mobs_only) + for(var/A in targets) + if(!isliving(A)) + targets -= A + + return ..(targets) + +/datum/ai_holder/simple_mob/melee/nurse_spider/can_attack(atom/movable/the_target) + . = ..() + if(!.) // Parent returned FALSE. + if(istype(the_target, /obj)) + var/obj/O = the_target + if(!O.anchored) + return TRUE + +/* + + + +/datum/ai_holder/simple_mob/melee/nurse_spider/list_targets() + var/list/targets = ..() + + if(targets.len) // Do regular targeting if there's actual enemies. + world << "Returned early." + world << "targets was [english_list(targets)]." + return targets + + // Otherwise lets target objects to web them. + var/static/webbable_types = typecacheof(list(/obj/machinery, /obj/item/, /obj/structure)) + for(var/WT in typecache_filter_list(range(vision_range, holder), webbable_types)) + var/obj/O = WT + if(!O.anchored && can_see(holder, O, vision_range)) + targets += WT + + world << "targets was [english_list(targets)]." + return targets +*/ +/* +/datum/ai_holder/simple_mob/melee/nurse_spider/can_attack(atom/movable/the_target) + . = ..() + if(!.) // Parent returned FALSE. + if(istype(the_target, /obj)) + var/obj/O = the_target + if(!O.anchored) + return TRUE +*/ + +/* + . = hearers(vision_range, holder) - src // Remove ourselves to prevent suicidal decisions. + + var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha)) + + for(var/HM in typecache_filter_list(range(vision_range, holder), hostile_machines)) + if(can_see(holder, HM, vision_range)) + . += HM +*/ \ No newline at end of file diff --git a/code/modules/ai/aI_holder_subtypes/slime_ai.dm b/code/modules/ai/aI_holder_subtypes/slime_ai.dm new file mode 100644 index 0000000000..905bb4b109 --- /dev/null +++ b/code/modules/ai/aI_holder_subtypes/slime_ai.dm @@ -0,0 +1,102 @@ +// Specialized AI for slime simplemobs. +// Unlike the parent AI code, this will probably break a lot of things if you put it on something that isn't /mob/living/simple_animal/slime + +/datum/ai_holder/normal/slime + +/datum/ai_holder/normal/slime/New() + ..() + ASSERT(istype(holder, /mob/living/simple_animal/slime)) + +/datum/ai_holder/normal/slime/find_target(var/list/possible_targets, var/has_targets_list = FALSE) + var/mob/living/simple_animal/slime/me = holder + if(me.victim) // Don't worry about finding another target if we're eatting someone. + return + if(leader && me.can_command(leader)) // If following someone, don't attack until the leader says so, something hits you, or the leader is no longer worthy. + return + ..() + +/datum/ai_holder/normal/slime/found(mob/living/L) + var/mob/living/simple_animal/slime/me = holder + if(isliving(L)) + if(can_attack(L)) + if(L.faction == me.faction && !attack_same_faction) + if(ishuman(L)) + var/mob/living/carbon/human/H = L + if(istype(H.species, /datum/species/monkey)) // istype() is so they'll eat the alien monkeys too. + return H // Monkeys are always food. + else + return + + // if(L in friends) + // return + + if(istype(L, /mob/living/simple_animal/slime)) + var/mob/living/simple_animal/slime/buddy = L + if(buddy.slime_color == me.slime_color || me.discipline || me.unity || buddy.unity) + return // Don't hurt same colored slimes. + else + return buddy //do hurt others + + if(ishuman(L)) + var/mob/living/carbon/human/H = L + if(istype(H.species, /datum/species/monkey)) // istype() is so they'll eat the alien monkeys too. + return H // Monkeys are always food. + + if(issilicon(L) || isbot(L)) + if(me.discipline && !me.rabid) + return // We're a good slime. For now at least. + return + return + +/datum/ai_holder/normal/slime/closest_distance() + if(isliving(target)) + var/mob/living/L = target + if(L.stat) + return 1 // Melee (eat) the target if dying, don't shoot it. + return ..() + + +/datum/ai_holder/normal/slime/help_requested(var/mob/living/simple_animal/slime/buddy) + var/mob/living/simple_animal/slime/me = holder + if(istype(buddy)) + if(buddy.slime_color != me.slime_color && (!me.unity || !buddy.unity)) // We only help slimes of the same color, if it's another slime calling for help. + // ai_log("HelpRequested() by [buddy] but they are a [buddy.slime_color] while we are a [src.slime_color].",2) + ai_log("help_requested() : Help was requested by [buddy] but they are a [buddy.slime_color] while we are a [me.slime_color].", AI_LOG_INFO) + return + ..() + + +/datum/ai_holder/normal/slime/handle_resist() + var/mob/living/simple_animal/slime/me = holder + if(me.buckled && me.victim && isliving(me.buckled) && me.victim == me.buckled) // If it's buckled to a living thing it's probably eating it. + return + else + ..() + + +/datum/ai_holder/normal/slime/melee_attack(atom/A) + var/mob/living/simple_animal/slime/me = holder + if(isliving(A)) + var/mob/living/L = A + if( (!L.lying && prob(60 + (me.power_charge * 4) ) || (!L.lying && me.optimal_combat) )) // "Smart" slimes always stun first. + me.a_intent = I_DISARM // Stun them first. + else if(me.can_consume(L) && L.lying) + me.a_intent = I_GRAB // Then eat them. + else + me.a_intent = I_HURT // Otherwise robust them. + ..(A) + +/* +/mob/living/simple_animal/slime/PunchTarget() + if(victim) + return // Already eatting someone. + if(!client) // AI controlled. + if( (!target_mob.lying && prob(60 + (power_charge * 4) ) || (!target_mob.lying && optimal_combat) )) // "Smart" slimes always stun first. + a_intent = I_DISARM // Stun them first. + else if(can_consume(target_mob) && target_mob.lying) + a_intent = I_GRAB // Then eat them. + else + a_intent = I_HURT // Otherwise robust them. + ai_log("PunchTarget() will [a_intent] [target_mob]",2) + ..() +*/ \ No newline at end of file diff --git a/code/modules/ai/ai_holder.dm b/code/modules/ai/ai_holder.dm index 41c5210833..f41627fd4f 100644 --- a/code/modules/ai/ai_holder.dm +++ b/code/modules/ai/ai_holder.dm @@ -19,6 +19,10 @@ var/mob/living/holder = null // The mob this datum is going to control. var/stance = STANCE_IDLE // Determines if the mob should be doing a specific thing, e.g. attacking, following, standing around, etc. var/intelligence_level = AI_NORMAL // Adjust to make the AI be intentionally dumber, or make it more robust (e.g. dodging grenades). + var/autopilot = FALSE // If true, the AI won't be deactivated if a client gets attached to the AI's mob. + var/busy = FALSE // If true, the ticker will skip processing this mob until this is false. Good for if you need the + // mob to stay still (e.g. delayed attacking). If you need the mob to be inactive for an extended period of time, + // consider sleeping the AI instead. @@ -73,11 +77,14 @@ // 'Tactical' processes such as moving a step, meleeing an enemy, firing a projectile, and other fairly cheap actions that need to happen quickly. /datum/ai_holder/proc/handle_tactics() + if(busy) + return handle_stance_tactical() // 'Strategical' processes that are more expensive on the CPU and so don't get run as often as the above proc, such as A* pathfinding or robust targeting. /datum/ai_holder/proc/handle_strategicals() -// world << "[holder.name] Strategicals!" + if(busy) + return handle_stance_strategical() /* @@ -301,3 +308,23 @@ return FALSE return TRUE + +// Helper proc to turn AI 'busy' mode on or off without having to check if there is an AI, to simplify writing code. +/mob/living/proc/set_AI_busy(value) + if(ai_holder) + ai_holder.busy = value + +/mob/living/proc/is_AI_busy() + if(!ai_holder) + return FALSE + return ai_holder.busy + +// Helper proc to check for the AI's stance. +// Returns null if there's no AI holder, or the mob has a player and autopilot is not on. +// Otherwise returns the stance. +/mob/living/proc/get_AI_stance() + if(!ai_holder) + return null + if(client && !ai_holder.autopilot) + return null + return ai_holder.stance \ No newline at end of file diff --git a/code/modules/ai/ai_holder_combat.dm b/code/modules/ai/ai_holder_combat.dm index d749761d1e..1f904c1f24 100644 --- a/code/modules/ai/ai_holder_combat.dm +++ b/code/modules/ai/ai_holder_combat.dm @@ -86,12 +86,16 @@ set_stance(STANCE_APPROACH) // We're not entirely sure how holder will do melee attacks since any /mob/living could be holder, but we don't have to care because Interfaces. -/datum/ai_holder/proc/melee_attack(atom/movable/AM) - return holder.IAttack(AM) +/datum/ai_holder/proc/melee_attack(atom/A) + . = holder.IAttack(A) + if(.) + post_melee_attack(A) // Ditto. -/datum/ai_holder/proc/ranged_attack(atom/movable/AM) - return holder.IRangedAttack(AM) +/datum/ai_holder/proc/ranged_attack(atom/A) + . = holder.IRangedAttack(A) + if(.) + post_ranged_attack(A) // Most mobs probably won't have this defined but we don't care. /datum/ai_holder/proc/special_attack(atom/movable/AM) @@ -100,7 +104,13 @@ // Called when within striking/shooting distance, however cooldown is not considered. // Override to do things like move in a random step for evasiveness. // Note that this is called BEFORE the attack. -/datum/ai_holder/proc/on_engagement(atom/movable/AM) +/datum/ai_holder/proc/on_engagement(atom/A) + +// Called after a successful (IE not on cooldown) ranged attack. +/datum/ai_holder/proc/post_ranged_attack(atom/A) + +// Ditto but for melee. +/datum/ai_holder/proc/post_melee_attack(atom/A) // Used to make sure projectiles will probably hit the target and not the wall or a friend. /datum/ai_holder/proc/test_projectile_safety(atom/movable/AM) diff --git a/code/modules/ai/ai_holder_communication.dm b/code/modules/ai/ai_holder_communication.dm index ba49fa29e6..9ed3d53728 100644 --- a/code/modules/ai/ai_holder_communication.dm +++ b/code/modules/ai/ai_holder_communication.dm @@ -7,17 +7,9 @@ var/threaten_timeout = 1 MINUTE // If the mob threatens someone, they leave, and then come back before this timeout period, the mob escalates to fighting immediately. var/last_conflict_time = null // Last occurance of threatening or fighting being used, in world.time. - var/threaten_sound = null // Sound file played when the mob calls threaten_target() for the first time. - var/stand_down_sound = null // Sound file played when the mob loses sight of the threatened target. - var/speak_chance = 0 // Probability that the mob talks (this is 'X in 200' chance since even 1/100 is pretty noisy) var/reacts = 0 // Reacts to some things being said. - var/datum/say_list/say_list = null // Datum containing all of our lines. - var/say_list_type = /datum/say_list // Type to give us on initialization. Default has empty lists, so the mob will be silent. -/datum/ai_holder/New() - ..() - say_list = new say_list_type() /datum/ai_holder/proc/should_threaten() if(!threaten) @@ -37,25 +29,28 @@ threatening = TRUE last_conflict_time = world.time - holder.say(safepick(say_list.say_threaten)) - playsound(holder.loc, threaten_sound, 75, 1) // We do this twice to make the sound -very- noticable to the target. - playsound(target.loc, threaten_sound, 75, 1) // Actual aim-mode also does that so at least it's consistant. + if(holder.say_list) + holder.say(safepick(holder.say_list.say_threaten)) + playsound(holder.loc, holder.say_list.threaten_sound, 50, 1) // We do this twice to make the sound -very- noticable to the target. + playsound(target.loc, holder.say_list.threaten_sound, 50, 1) // Actual aim-mode also does that so at least it's consistant. else // Otherwise we are waiting for them to go away or to wait long enough for escalate. if(target in list_targets()) // Are they still visible? if(threaten_delay && last_conflict_time + threaten_delay < world.time) // Waited too long. threatening = FALSE set_stance(STANCE_APPROACH) - holder.say(safepick(say_list.say_escalate)) + if(holder.say_list) + holder.say(safepick(holder.say_list.say_escalate)) else return // Wait a bit. else // They left, or so we think. threatening = FALSE set_stance(STANCE_IDLE) - holder.say(safepick(say_list.say_stand_down)) - playsound(holder.loc, stand_down_sound, 50, 1) // We do this twice to make the sound -very- noticable to the target. - playsound(target.loc, stand_down_sound, 50, 1) // Actual aim-mode also does that so at least it's consistant. + if(holder.say_list) + holder.say(safepick(holder.say_list.say_stand_down)) + playsound(holder.loc, holder.say_list.stand_down_sound, 50, 1) // We do this twice to make the sound -very- noticable to the target. + playsound(target.loc, holder.say_list.stand_down_sound, 50, 1) // Actual aim-mode also does that so at least it's consistant. // Determines what is deserving of a warning when STANCE_ALERT is active. /datum/ai_holder/proc/will_threaten(mob/living/the_target) @@ -75,13 +70,25 @@ /datum/ai_holder/proc/handle_idle_speaking() if(rand(0,200) < speak_chance) - var/list/comm_types = list() // What kinds of things can we do? + // Check if anyone is around to 'appreciate' what we say. + var/alone = TRUE + for(var/m in viewers(holder)) + var/mob/M = m + if(M.client) + alone = FALSE + break + if(alone) // Forever alone. No point doing anything else. + return - if(say_list.speak.len) + var/list/comm_types = list() // What kinds of things can we do? + if(!holder.say_list) + return + + if(holder.say_list.speak.len) comm_types += COMM_SAY - if(say_list.emote_hear.len) + if(holder.say_list.emote_hear.len) comm_types += COMM_AUDIBLE_EMOTE - if(say_list.emote_see.len) + if(holder.say_list.emote_see.len) comm_types += COMM_VISUAL_EMOTE if(!comm_types.len) @@ -89,11 +96,11 @@ switch(pick(comm_types)) if(COMM_SAY) - holder.say(safepick(say_list.speak)) + holder.say(safepick(holder.say_list.speak)) if(COMM_AUDIBLE_EMOTE) - holder.audible_emote(safepick(say_list.emote_hear)) + holder.audible_emote(safepick(holder.say_list.emote_hear)) if(COMM_VISUAL_EMOTE) - holder.visible_emote(safepick(say_list.emote_see)) + holder.visible_emote(safepick(holder.say_list.emote_see)) #undef COMM_SAY #undef COMM_AUDIBLE_EMOTE diff --git a/code/modules/ai/ai_holder_debug.dm b/code/modules/ai/ai_holder_debug.dm index 538d74e895..c4b2f990a0 100644 --- a/code/modules/ai/ai_holder_debug.dm +++ b/code/modules/ai/ai_holder_debug.dm @@ -102,9 +102,6 @@ threaten = TRUE wander = TRUE - threaten_sound = 'sound/weapons/TargetOn.ogg' - stand_down_sound = 'sound/weapons/TargetOff.ogg' - /datum/ai_holder/hostile/ranged/debug wander = FALSE conserve_ammo = FALSE @@ -115,8 +112,6 @@ last_turf_display = TRUE debug_ai = AI_LOG_INFO - say_list_type = /datum/say_list/pirate - /mob/living/simple_animal/hostile/pirate hostile = FALSE ai_inactive = TRUE diff --git a/code/modules/ai/ai_holder_fleeing.dm b/code/modules/ai/ai_holder_fleeing.dm index bde630ef57..b9c45ace93 100644 --- a/code/modules/ai/ai_holder_fleeing.dm +++ b/code/modules/ai/ai_holder_fleeing.dm @@ -16,7 +16,7 @@ return TRUE if(can_flee) - if(!hostile) + if(!hostile && !retaliate) return TRUE // We're not hostile and someone attacked us first. if(flee_when_dying && (holder.health / holder.getMaxHealth()) <= dying_threshold) return TRUE // We're gonna die! diff --git a/code/modules/ai/ai_holder_follow.dm b/code/modules/ai/ai_holder_follow.dm index a9f1433c6b..9329fe53d7 100644 --- a/code/modules/ai/ai_holder_follow.dm +++ b/code/modules/ai/ai_holder_follow.dm @@ -57,4 +57,12 @@ ai_log("lose_follow() : Exited.", AI_LOG_DEBUG) /datum/ai_holder/proc/should_follow_leader() - return leader && get_dist(holder, leader) > follow_distance \ No newline at end of file + if(!leader) + return FALSE + if(follow_until_time && world.time > follow_until_time) + lose_follow() + set_stance(STANCE_IDLE) + return FALSE + if(get_dist(holder, leader) > follow_distance) + return TRUE + return FALSE \ No newline at end of file diff --git a/code/modules/ai/ai_holder_movement.dm b/code/modules/ai/ai_holder_movement.dm index 3a15fa5dc1..55b2e2ce34 100644 --- a/code/modules/ai/ai_holder_movement.dm +++ b/code/modules/ai/ai_holder_movement.dm @@ -6,7 +6,9 @@ // Home. var/turf/home_turf = null // The mob's 'home' turf. It will try to stay near it if told to do so. var/returns_home = FALSE // If true, makes the mob go to its 'home' if it strays too far. + var/home_low_priority = FALSE // If true, the mob will not go home unless it has nothing better to do, e.g. its following someone. var/max_home_distance = 3 // How far the mob can go away from its home before being told to go_home(). + // Note that there is a 'BYOND cap' of 14 due to limitations of get_/step_to(). // Wandering. var/wander = FALSE // If true, the mob will randomly move in the four cardinal directions when idle. @@ -15,7 +17,6 @@ var/wander_when_pulled = FALSE // If the mob will refrain from wandering if someone is pulling it. - /datum/ai_holder/proc/walk_to_destination() ai_log("walk_to_destination() : Entering.",AI_LOG_DEBUG) if(!destination) @@ -41,7 +42,15 @@ ai_log("walk_to_destination() : Exiting.",AI_LOG_DEBUG) /datum/ai_holder/proc/should_go_home() - return (returns_home && home_turf) && (get_dist(holder, home_turf) > max_home_distance) + if(!returns_home || !home_turf) + return FALSE + if(get_dist(holder, home_turf) > max_home_distance) + if(!home_low_priority) + return TRUE + else if(!leader && !target) + return TRUE + return FALSE +// return (returns_home && home_turf) && (get_dist(holder, home_turf) > max_home_distance) /datum/ai_holder/proc/go_home() if(home_turf) @@ -70,7 +79,6 @@ // Walk towards whatever. /datum/ai_holder/proc/walk_path(atom/A, get_to = 1) ai_log("walk_path() : Entered.", AI_LOG_DEBUG) - var/turf/pre_step_turf = get_turf(holder) if(use_astar) if(!path.len) // If we're missing a path, make a new one. @@ -78,13 +86,14 @@ calculate_path(A, get_to) if(!path.len) // If we still don't have one, then the target's probably somewhere inaccessible to us. Get as close as we can. - ai_log("walk_path() : Failed to obtain path to target. Using step_to() instead.", AI_LOG_INFO) - step_to(holder, A) - if(get_turf(holder) == pre_step_turf) + ai_log("walk_path() : Failed to obtain path to target. Using get_step_to() instead.", AI_LOG_INFO) + // step_to(holder, A) + if(holder.IMove(get_step_to(holder, A)) == MOVEMENT_FAILED) + ai_log("walk_path() : Failed to move, attempting breakthrough.", AI_LOG_TRACE) breakthrough(A) // We failed to move, time to smash things. return - if(!move_once()) // Start walking the path. + if(move_once() == FALSE) // Start walking the path. ai_log("walk_path() : Failed to step.", AI_LOG_TRACE) ++failed_steps if(failed_steps > 3) // We're probably stuck. @@ -93,8 +102,10 @@ failed_steps = 0 else - step_to(holder, A) - if(get_turf(holder) == pre_step_turf) + // step_to(holder, A) + ai_log("walk_path() : Going to IMove().", AI_LOG_TRACE) + if(holder.IMove(get_step_to(holder, A)) == MOVEMENT_FAILED ) + ai_log("walk_path() : Failed to move, attempting breakthrough.", AI_LOG_TRACE) breakthrough(A) // We failed to move, time to smash things. ai_log("walk_path() : Exited.", AI_LOG_DEBUG) @@ -110,14 +121,17 @@ var/turf/T = src.path[1] T.overlays -= path_overlay - step_towards(holder, src.path[1]) - if(holder.loc != src.path[1]) - ai_log("move_once() : Failed step. Exiting.", AI_LOG_TRACE) - return FALSE - else - path -= src.path[1] - ai_log("move_once() : Successful step. Exiting.", AI_LOG_TRACE) - return TRUE +// step_towards(holder, src.path[1]) + if(holder.IMove(get_step_towards(holder, src.path[1])) != MOVEMENT_ON_COOLDOWN) + if(holder.loc != src.path[1]) + ai_log("move_once() : Failed step. Exiting.", AI_LOG_TRACE) + return MOVEMENT_FAILED + else + path -= src.path[1] + ai_log("move_once() : Successful step. Exiting.", AI_LOG_TRACE) + return MOVEMENT_SUCCESSFUL + ai_log("move_once() : Mob movement on cooldown. Exiting.", AI_LOG_TRACE) + return MOVEMENT_ON_COOLDOWN /datum/ai_holder/proc/should_wander() return wander && !leader @@ -135,6 +149,6 @@ var/moving_to = 0 // Apparently this is required or it always picks 4, according to the previous developer for simplemob AI. moving_to = pick(cardinal) holder.set_dir(moving_to) - holder.Move(get_step(holder,moving_to)) + holder.IMove(get_step(holder,moving_to)) wander_delay = base_wander_delay ai_log("handle_wander_movement() : Exited.", AI_LOG_DEBUG) diff --git a/code/modules/ai/ai_holder_targeting.dm b/code/modules/ai/ai_holder_targeting.dm index 5de7b2890c..3eaa33f449 100644 --- a/code/modules/ai/ai_holder_targeting.dm +++ b/code/modules/ai/ai_holder_targeting.dm @@ -1,8 +1,8 @@ // Used for assigning a target for attacking. /datum/ai_holder - var/hostile = FALSE // Do we try to hurt others? Setting to false disables most combat processing. - var/retaliate = FALSE // Attacks whatever struck it first. + var/hostile = FALSE // Do we try to hurt others? + var/retaliate = FALSE // Attacks whatever struck it first. Mobs will still attack back if this is false but hostile is true. var/atom/movable/target = null // The thing (mob or object) we're trying to kill. var/turf/target_last_seen_turf = null // Where the mob last observed the target being, used if the target disappears but the mob wants to keep fighting. @@ -26,6 +26,8 @@ // Step 2, filter down possible targets to things we actually care about. /datum/ai_holder/proc/find_target(var/list/possible_targets, var/has_targets_list = FALSE) + if(!hostile) // So retaliating mobs only attack the thing that hit it. + return null . = list() if(!has_targets_list) possible_targets = list_targets() @@ -45,12 +47,13 @@ // Step 3, pick among the possible, attackable targets. /datum/ai_holder/proc/pick_target(list/targets) if(target != null) // If we already have a target, but are told to pick again, calculate the lowest distance between all possible, and pick from the lowest distance targets. - for(var/possible_target in targets) - var/atom/A = possible_target - var/target_dist = get_dist(holder, target) - var/possible_target_distance = get_dist(holder, A) - if(target_dist < possible_target_distance) - targets -= A + targets = target_filter_distance(targets) +// for(var/possible_target in targets) +// var/atom/A = possible_target +// var/target_dist = get_dist(holder, target) +// var/possible_target_distance = get_dist(holder, A) +// if(target_dist < possible_target_distance) +// targets -= A if(!targets.len) // We found nothing. return var/chosen_target = pick(targets) @@ -66,6 +69,18 @@ set_stance(STANCE_APPROACH) return TRUE +// Filters return one or more 'preferred' targets. + +// This one is for closest targets. +/datum/ai_holder/proc/target_filter_distance(list/targets) + for(var/possible_target in targets) + var/atom/A = possible_target + var/target_dist = get_dist(holder, target) + var/possible_target_distance = get_dist(holder, A) + if(target_dist < possible_target_distance) + targets -= A + return targets + /datum/ai_holder/proc/can_attack(atom/movable/the_target) if(!can_see_target(the_target)) return FALSE @@ -156,4 +171,31 @@ if(last_turf_display && target_last_seen_turf) target_last_seen_turf.overlays -= last_turf_overlay ai_log("lose_target_position() : Last position is being reset.", AI_LOG_INFO) - target_last_seen_turf = null \ No newline at end of file + target_last_seen_turf = null + +// Responds to a hostile action against its mob. +/datum/ai_holder/proc/react_to_attack(atom/movable/attacker) + if(holder.stat) // We're dead. + ai_log("react_to_attack() : Was attacked by [attacker], but we are dead/unconscious.", AI_LOG_TRACE) + return FALSE + if(!hostile && !retaliate) // Not allowed to defend ourselves. + ai_log("react_to_attack() : Was attacked by [attacker], but we are not allowed to attack back.", AI_LOG_TRACE) + return FALSE + if(holder.IIsAlly(attacker)) // I'll overlook it THIS time... + ai_log("react_to_attack() : Was attacked by [attacker], but they were an ally.", AI_LOG_TRACE) + return FALSE + if(target) // Already fighting someone. Switching every time we get hit would impact our combat performance. + ai_log("react_to_attack() : Was attacked by [attacker], but we already have a target.", AI_LOG_TRACE) + return FALSE + + if(stance == STANCE_SLEEP) // If we're asleep, try waking up if someone's wailing on us. + ai_log("react_to_attack() : AI is asleep. Waking up.", AI_LOG_TRACE) + go_wake() + + ai_log("react_to_attack() : Was attacked by [attacker].", AI_LOG_INFO) + return give_target(attacker) // Also handles setting the appropiate stance. + +/* + if(ai_inactive || stat || M == target_mob) return //Not if we're dead or already hitting them + if(M in friends || M.faction == faction) return //I'll overlook it THIS time... +*/ \ No newline at end of file diff --git a/code/modules/ai/interfaces.dm b/code/modules/ai/interfaces.dm index b38b1ce7a3..9ef507ceeb 100644 --- a/code/modules/ai/interfaces.dm +++ b/code/modules/ai/interfaces.dm @@ -6,20 +6,20 @@ /mob/living/proc/IAttack(atom/A) return FALSE -/mob/living/simple_animal/IAttack(atom/A) +/mob/living/simple_mob/IAttack(atom/A) return attack_target(A) /mob/living/proc/IRangedAttack(atom/A) return FALSE -/mob/living/simple_animal/IRangedAttack(atom/A) +/mob/living/simple_mob/IRangedAttack(atom/A) return shoot_target(A) /mob/living/proc/ISpecialAttack(atom/A) return FALSE -/mob/living/simple_animal/ISpecialAttack(atom/A) - return special_attack_target() +/mob/living/simple_mob/ISpecialAttack(atom/A) + return special_attack_target(A) /mob/living/proc/ISay(message) @@ -31,8 +31,32 @@ if(!.) // Outside the faction, try to see if they're friends. return L in friends +/mob/living/simple_mob/IIsAlly(mob/living/L) + . = ..() + if(!.) // Outside the faction, try to see if they're friends. + return L in friends + /mob/living/proc/IGetID() /mob/living/simple_animal/IGetID() if(myid) - return myid.GetID() \ No newline at end of file + return myid.GetID() + +// Respects move cooldowns as if it had a client. +/mob/living/proc/IMove(newloc) + if(check_move_cooldown()) +// if(!newdir) +// newdir = get_dir(get_turf(src), newloc) + + // Move()ing to another tile successfully returns 32 because BYOND. Would rather deal with TRUE/FALSE-esque terms. + // Note that moving to the same tile will be 'successful'. + var/turf/old_T = get_turf(src) + . = SelfMove(newloc) ? MOVEMENT_SUCCESSFUL : MOVEMENT_FAILED + if(. == MOVEMENT_SUCCESSFUL) + set_dir(get_dir(old_T, newloc)) + // Apply movement delay. + // Player movement has more factors but its all in the client and fixing that would be its own project. + setMoveCooldown(movement_delay()) + return + + . = MOVEMENT_ON_COOLDOWN // To avoid superfast mobs that aren't meant to be superfast. Is actually -1. diff --git a/code/modules/ai/say_list.dm b/code/modules/ai/say_list.dm index 29cd476eee..37f14823e5 100644 --- a/code/modules/ai/say_list.dm +++ b/code/modules/ai/say_list.dm @@ -1,13 +1,29 @@ -// A simple datum that just holds many lists of lines for AI mobs to pick from. +// A simple datum that just holds many lists of lines for mobs to pick from. // This is its own datum in order to be able to have different types of mobs be able to use the same lines if desired, // even when inheritence wouldn't be able to do so. // Also note this also contains emotes, despite its name. +// and now sounds because its probably better that way. + +/mob/living + var/datum/say_list/say_list = null + var/say_list_type = /datum/say_list // Type to give us on initialization. Default has empty lists, so the mob will be silent. + +/mob/living/initialize() + if(say_list_type) + say_list = new say_list_type(src) + return ..() + +/mob/living/Destroy() + qdel_null(say_list) + return ..() + /datum/say_list var/list/speak = list() // Things the mob might say if it talks while idle. var/list/emote_hear = list() // Hearable emotes it might perform var/list/emote_see = list() // Unlike speak_emote, the list of things in this variable only show by themselves with no spoken text. IE: Ian barks, Ian yaps + var/list/say_understood = list() // When accepting an order. var/list/say_cannot = list() // When they cannot comply. var/list/say_maybe_target = list() // When they briefly see something. @@ -16,6 +32,11 @@ var/list/say_stand_down = list() // When the threatened thing goes away. var/list/say_escalate = list() // When the threatened thing doesn't go away. + var/threaten_sound = null // Sound file played when the mob's AI calls threaten_target() for the first time. + var/stand_down_sound = null // Sound file played when the mob's AI loses sight of the threatened target. + + + @@ -25,10 +46,38 @@ // This one's pretty dumb, but pirates are dumb anyways and it makes for a good test. /datum/say_list/pirate speak = list("Yarr!") + say_understood = list("Alright, matey.") say_cannot = list("No, matey.") say_maybe_target = list("Eh?") say_got_target = list("Yarrrr!") say_threaten = list("You best leave, this booty is mine.", "No plank to walk on, just walk away.") say_stand_down = list("Good.") - say_escalate = list("Yarr! The booty is mine!") \ No newline at end of file + say_escalate = list("Yarr! The booty is mine!") + +/datum/say_list/malf_drone + speak = list("ALERT.","Hostile-ile-ile entities dee-twhoooo-wected.","Threat parameterszzzz- szzet.","Bring sub-sub-sub-systems uuuup to combat alert alpha-a-a.") + emote_see = list("beeps menacingly","whirrs threateningly","scans its immediate vicinity") + + say_understood = list("Affirmative.", "Positive.") + say_cannot = list("Denied.", "Negative.") + say_maybe_target = list("Possible threat detected. Investigating.", "Motion detected.", "Investigating.") + say_got_target = list("Threat detected.", "New task: Remove threat.", "Threat removal engaged.", "Engaging target.") + say_threaten = list("Motion detected, judging target...") + say_stand_down = list("Visual lost.", "Error: Target not found.") + say_escalate = list("Viable target found. Removing.", "Engaging target.", "Target judgement complete. Removal required.") + + threaten_sound = 'sound/effects/turret/move1.wav' + stand_down_sound = 'sound/effects/turret/move2.wav' + +/datum/say_list/mercenary + threaten_sound = 'sound/weapons/TargetOn.ogg' + stand_down_sound = 'sound/weapons/TargetOff.ogg' + + +/datum/say_list/crab + emote_hear = list("clicks") + emote_see = list("clacks") + +/datum/say_list/spider + emote_hear = list("chitters") \ No newline at end of file diff --git a/code/modules/mob/living/death.dm b/code/modules/mob/living/death.dm index d6c2bc0405..6812323a4e 100644 --- a/code/modules/mob/living/death.dm +++ b/code/modules/mob/living/death.dm @@ -1,3 +1,7 @@ /mob/living/death() clear_fullscreens() + + if(ai_holder) + ai_holder.go_sleep() + . = ..() \ No newline at end of file diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index 3d9140dd46..e0e85edbcd 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -92,6 +92,13 @@ /mob/living/proc/getsoak(var/def_zone, var/type) return 0 +// Clicking with an empty hand +/mob/living/attack_hand(mob/living/L) + ..() + if(istype(L) && L.a_intent != I_HELP) + if(ai_holder) // Using disarm, grab, or harm intent is considered a hostile action to the mob's AI. + ai_holder.react_to_attack(L) + /mob/living/bullet_act(var/obj/item/projectile/P, var/def_zone) //Being hit while using a deadman switch @@ -102,6 +109,9 @@ src.visible_message("[src] triggers their deadman's switch!") signaler.signal() + if(ai_holder && P.firer) + ai_holder.react_to_attack(P.firer) + //Armor var/soaked = get_armor_soak(def_zone, P.check_armour, P.armor_penetration) var/absorb = run_armor_check(def_zone, P.check_armour, P.armor_penetration) @@ -272,6 +282,8 @@ var/client/assailant = M.client if(assailant) add_attack_logs(M,src,"Hit by thrown [O.name]") + if(ai_holder) + ai_holder.react_to_attack(O.thrower) // Begin BS12 momentum-transfer code. var/mass = 1.5 @@ -335,6 +347,8 @@ adjustBruteLoss(damage) add_attack_logs(user,src,"Generic attack (probably animal)", admin_notify = FALSE) //Usually due to simple_animal attacks + if(ai_holder) + ai_holder.react_to_attack(user) src.visible_message("[user] has [attack_message] [src]!") user.do_attack_animation(src) spawn(1) updatehealth() diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index e9faaa0769..30c5580d14 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -5,6 +5,8 @@ var/maxHealth = 100 //Maximum health that should be possible. Avoid adjusting this if you can, and instead use modifiers datums. var/health = 100 //A mob's health + var/mob_class = MOB_CLASS_ANIMAL // A mob's "class", e.g. human, mechanical, animal, etc. Used for certain projectile effects. See __defines/mob.dm for available classes. + var/hud_updateflag = 0 //Damage related vars, NOTE: THESE SHOULD ONLY BE MODIFIED BY PROCS diff --git a/code/modules/mob/living/login.dm b/code/modules/mob/living/login.dm index 275e88be08..96d59dc330 100644 --- a/code/modules/mob/living/login.dm +++ b/code/modules/mob/living/login.dm @@ -8,4 +8,9 @@ update_antag_icons(mind) client.screen |= global_hud.darksight client.images |= dsoverlay + + if(ai_holder && !ai_holder.autopilot) + ai_holder.go_sleep() + to_chat(src,"Mob AI disabled while you are controlling the mob.") + return . diff --git a/code/modules/mob/living/logout.dm b/code/modules/mob/living/logout.dm index 577221a03e..fa04b0b317 100644 --- a/code/modules/mob/living/logout.dm +++ b/code/modules/mob/living/logout.dm @@ -1,6 +1,11 @@ /mob/living/Logout() ..() - if (mind) + if (mind) //Per BYOND docs key remains set if the player DCs, becomes null if switching bodies. - if(!key) //key and mind have become seperated. + if(!key) //key and mind have become seperated. mind.active = 0 //This is to stop say, a mind.transfer_to call on a corpse causing a ghost to re-enter its body. + + spawn(15 SECONDS) //15 seconds to get back into the mob before it goes wild + if(src && !src.client) + if(ai_holder) + ai_holder.go_wake() diff --git a/code/modules/mob/living/simple_animal/animals/mouse.dm b/code/modules/mob/living/simple_animal/animals/mouse.dm index 123e82a611..42d6ef6423 100644 --- a/code/modules/mob/living/simple_animal/animals/mouse.dm +++ b/code/modules/mob/living/simple_animal/animals/mouse.dm @@ -16,10 +16,10 @@ see_in_dark = 6 universal_understand = 1 - mob_size = MOB_MINISCULE + mob_size = MOB_SMALL pass_flags = PASSTABLE - can_pull_size = ITEMSIZE_TINY - can_pull_mobs = MOB_PULL_NONE +// can_pull_size = ITEMSIZE_TINY +// can_pull_mobs = MOB_PULL_NONE layer = MOB_LAYER density = 0 diff --git a/code/modules/mob/living/simple_mob/appearance.dm b/code/modules/mob/living/simple_mob/appearance.dm new file mode 100644 index 0000000000..2c8bc5efd8 --- /dev/null +++ b/code/modules/mob/living/simple_mob/appearance.dm @@ -0,0 +1,69 @@ +/mob/living/simple_mob/update_icon() + . = ..() + var/mutable_appearance/ma = new(src) + ma.layer = layer + ma.plane = plane + + ma.overlays = list(modifier_overlay) + + //Awake and normal + if((stat == CONSCIOUS) && (!icon_rest || !resting || !incapacitated(INCAPACITATION_DISABLED) )) + ma.icon_state = icon_living + + //Dead + else if(stat >= DEAD) + ma.icon_state = icon_dead + + //Resting or KO'd + else if(((stat == UNCONSCIOUS) || resting || incapacitated(INCAPACITATION_DISABLED) ) && icon_rest) + ma.icon_state = icon_rest + + //Backup + else + ma.icon_state = initial(icon_state) + + if(has_hands) + if(r_hand_sprite) + ma.overlays += r_hand_sprite + if(l_hand_sprite) + ma.overlays += l_hand_sprite + + if(has_eye_glow) + add_eyes() + + appearance = ma + + +// If your simple mob's update_icon() call calls overlays.Cut(), this needs to be called after this, or manually apply modifier_overly to overlays. +/mob/living/simple_mob/update_modifier_visuals() + var/image/effects = null + if(modifier_overlay) + overlays -= modifier_overlay + modifier_overlay.overlays.Cut() + effects = modifier_overlay + else + effects = new() + + for(var/datum/modifier/M in modifiers) + if(M.mob_overlay_state) + var/image/I = image("icon" = 'icons/mob/modifier_effects.dmi', "icon_state" = M.mob_overlay_state) + I.appearance_flags = RESET_COLOR // So colored mobs don't affect the overlay. + effects.overlays += I + + modifier_overlay = effects + overlays += modifier_overlay + + +/mob/living/simple_mob/proc/add_eyes() + if(!eye_layer) + eye_layer = image(icon, "[icon_state]-eyes") + eye_layer.plane = PLANE_LIGHTING_ABOVE + + overlays += eye_layer + +/mob/living/simple_mob/proc/remove_eyes() + overlays -= eye_layer + + +/mob/living/simple_mob/gib() + ..(icon_gib,1,icon) // we need to specify where the gib animation is stored \ No newline at end of file diff --git a/code/modules/mob/living/simple_animal/simple_animal2.dm b/code/modules/mob/living/simple_mob/combat.dm similarity index 68% rename from code/modules/mob/living/simple_animal/simple_animal2.dm rename to code/modules/mob/living/simple_mob/combat.dm index 04522eb101..d7f784c2be 100644 --- a/code/modules/mob/living/simple_animal/simple_animal2.dm +++ b/code/modules/mob/living/simple_mob/combat.dm @@ -1,8 +1,5 @@ -// Reorganized and somewhat cleaned up. -// AI code has been made into a datum, inside the AI module folder. - // Does a melee attack. -/mob/living/simple_animal/proc/attack_target(atom/A) +/mob/living/simple_mob/proc/attack_target(atom/A) set waitfor = FALSE // For attack animations, if they're ever added. Don't want the AI processor to get held up. if(!A.Adjacent(src)) @@ -13,8 +10,10 @@ return do_attack(A) + // This does the actual attack. -/mob/living/simple_animal/proc/do_attack(atom/A) +// This is a seperate proc for the purposes of attack animations. +/mob/living/simple_mob/proc/do_attack(atom/A) if(!A.Adjacent(src)) // They could've moved in the meantime. return FALSE @@ -27,10 +26,8 @@ if(isliving(A)) // Check defenses. var/mob/living/L = A - if(prob(melee_miss_chance)) // This is stupid (The logging part). - src.attack_log += text("\[[time_stamp()]\] attacked [L.name] ([L.ckey])") - L.attack_log += text("\[[time_stamp()]\] was attacked by [src.name] ([src.ckey])") - visible_message("[src] misses [L]!") + if(prob(melee_miss_chance)) + add_attack_logs(src, L, "Animal-attacked (miss)", admin_notify = FALSE) do_attack_animation(src) return FALSE // We missed. @@ -40,12 +37,16 @@ return FALSE // We were blocked. if(A.attack_generic(src, damage_to_do, pick(attacktext)) && attack_sound) + apply_melee_effects(A) playsound(src, attack_sound, 75, 1) return TRUE +// Override for special effects after a successful attack. +/mob/living/simple_mob/proc/apply_melee_effects(atom/A) + //The actual top-level ranged attack proc -/mob/living/simple_animal/proc/shoot_target(atom/A) +/mob/living/simple_mob/proc/shoot_target(atom/A) if(!canClick()) return FALSE @@ -58,8 +59,9 @@ return TRUE + //Shoot a bullet at someone (idk why user is an argument when src would fit???) -/mob/living/simple_animal/proc/shoot(atom/A, turf/start, mob/living/user, bullet = 0) +/mob/living/simple_mob/proc/shoot(atom/A, turf/start, mob/living/user, bullet = 0) if(A == start) return @@ -69,6 +71,7 @@ return P.launch(A) + //Special attacks, like grenades or blinding spit or whatever -/mob/living/simple_animal/proc/special_attack_target(atom/A) +/mob/living/simple_mob/proc/special_attack_target(atom/A) return FALSE \ No newline at end of file diff --git a/code/modules/mob/living/simple_mob/defense.dm b/code/modules/mob/living/simple_mob/defense.dm new file mode 100644 index 0000000000..dac6bfeafc --- /dev/null +++ b/code/modules/mob/living/simple_mob/defense.dm @@ -0,0 +1,192 @@ +// Hit by a projectile. +/mob/living/simple_mob/bullet_act(var/obj/item/projectile/P) + //Projectiles with bonus SA damage + if(!P.nodamage) + // if(!P.SA_vulnerability || P.SA_vulnerability == intelligence_level) + if(P.SA_vulnerability & mob_class) + P.damage += P.SA_bonus_damage + + . = ..() + + +// When someone clicks us with an empty hand +/mob/living/simple_mob/attack_hand(mob/living/L) + ..() + + switch(L.a_intent) + if(I_HELP) + if(health > 0) + L.visible_message("\The [L] [response_help] \the [src].") + + if(I_DISARM) + L.visible_message("\The [L] [response_disarm] \the [src].") + L.do_attack_animation(src) + //TODO: Push the mob away or something + + if(I_GRAB) + if (L == src) + return + if (!(status_flags & CANPUSH)) + return + if(!incapacitated(INCAPACITATION_ALL) && prob(grab_resist)) + L.visible_message("\The [L] tries to grab \the [src] but fails!") + return + + var/obj/item/weapon/grab/G = new /obj/item/weapon/grab(L, src) + + L.put_in_active_hand(G) + + G.synch() + G.affecting = src + LAssailant = L + + L.visible_message("\The [L] has grabbed [src] passively!") + L.do_attack_animation(src) + + if(I_HURT) + var/armor = run_armor_check(def_zone = null, attack_flag = "melee") + apply_damage(damage = harm_intent_damage, damagetype = BURN, def_zone = null, blocked = armor, blocked = resistance, used_weapon = null, sharp = FALSE, edge = FALSE) + L.visible_message("\The [L] [response_harm] \the [src]!") + L.do_attack_animation(src) + + return + + +// When somoene clicks us with an item in hand +/mob/living/simple_mob/attackby(var/obj/item/O, var/mob/user) + if(istype(O, /obj/item/stack/medical)) + if(stat != DEAD) + // This could be done better. + var/obj/item/stack/medical/MED = O + if(health < getMaxHealth()) + if(MED.amount >= 1) + adjustBruteLoss(-MED.heal_brute) + MED.amount -= 1 + if(MED.amount <= 0) + qdel(MED) + visible_message("\The [user] applies the [MED] on [src].") + else + var/datum/gender/T = gender_datums[src.get_visible_gender()] + to_chat(user, "\The [src] is dead, medical items won't bring [T.him] back to life.") // the gender lookup is somewhat overkill, but it functions identically to the obsolete gender macros and future-proofs this code + if(meat_type && (stat == DEAD)) //if the animal has a meat, and if it is dead. + if(istype(O, /obj/item/weapon/material/knife) || istype(O, /obj/item/weapon/material/knife/butch)) + harvest(user) + + return ..() + + +// Handles the actual harming by a melee weapon. +/mob/living/simple_mob/hit_with_weapon(obj/item/O, mob/living/user, var/effective_force, var/hit_zone) + effective_force = O.force + + //Animals can't be stunned(?) + if(O.damtype == HALLOSS) + effective_force = 0 + if(supernatural && istype(O,/obj/item/weapon/nullrod)) + effective_force *= 2 + purge = 3 + if(O.force <= resistance) + to_chat(user,"This weapon is ineffective, it does no damage.") + return 2 //??? + +// react_to_attack(user) + + . = ..() + + +// Exploding. +/mob/living/simple_mob/ex_act(severity) + if(!blinded) + flash_eyes() + var/armor = run_armor_check(def_zone = null, attack_flag = "bomb") + var/bombdam = 500 + switch (severity) + if (1.0) + bombdam = 500 + if (2.0) + bombdam = 60 + if (3.0) + bombdam = 30 + + apply_damage(damage = bombdam, damagetype = BRUTE, def_zone = null, blocked = armor, blocked = resistance, used_weapon = null, sharp = FALSE, edge = FALSE) + + if(bombdam > maxHealth) + gib() + + +// Fire stuff. Not really exciting at the moment. +/mob/living/simple_mob/handle_fire() + return +/mob/living/simple_mob/update_fire() + return +/mob/living/simple_mob/IgniteMob() + return +/mob/living/simple_mob/ExtinguishMob() + return + + +// Electricity +/mob/living/simple_mob/electrocute_act(var/shock_damage, var/obj/source, var/siemens_coeff = 1.0, var/def_zone = null) + shock_damage *= siemens_coeff + if(shock_damage < 1) + return 0 + + apply_damage(damage = shock_damage, damagetype = BURN, def_zone = null, blocked = null, blocked = resistance, used_weapon = null, sharp = FALSE, edge = FALSE) + playsound(loc, "sparks", 50, 1, -1) + + var/datum/effect/effect/system/spark_spread/s = new /datum/effect/effect/system/spark_spread + s.set_up(5, 1, loc) + s.start() + + +// Shot with taser/stunvolver +/mob/living/simple_mob/stun_effect_act(var/stun_amount, var/agony_amount, var/def_zone, var/used_weapon=null) + if(taser_kill) + var/stunDam = 0 + var/agonyDam = 0 + var/armor = run_armor_check(def_zone = null, attack_flag = "energy") + + if(stun_amount) + stunDam += stun_amount * 0.5 + apply_damage(damage = stunDam, damagetype = BURN, def_zone = null, blocked = armor, blocked = resistance, used_weapon = used_weapon, sharp = FALSE, edge = FALSE) + + if(agony_amount) + agonyDam += agony_amount * 0.5 + apply_damage(damage = agonyDam, damagetype = BURN, def_zone = null, blocked = armor, blocked = resistance, used_weapon = used_weapon, sharp = FALSE, edge = FALSE) + + +// Electromagnetism +/mob/living/simple_mob/emp_act(severity) + ..() // To emp_act() its contents. + if(!isSynthetic()) + return + switch(severity) + if(1) + // adjustFireLoss(rand(15, 25)) + adjustFireLoss(min(60, getMaxHealth()*0.5)) // Weak mobs will always take two direct EMP hits to kill. Stronger ones might take more. + if(2) + adjustFireLoss(min(30, getMaxHealth()*0.25)) + // adjustFireLoss(rand(10, 18)) + if(3) + adjustFireLoss(min(15, getMaxHealth()*0.125)) + // adjustFireLoss(rand(5, 12)) + if(4) + adjustFireLoss(min(7, getMaxHealth()*0.0625)) + // adjustFireLoss(rand(1, 6)) + + +// Armor +/mob/living/simple_mob/getarmor(def_zone, attack_flag) + var/armorval = armor[attack_flag] + if(!armorval) + return 0 + else + return armorval + +/mob/living/simple_mob/getsoak(def_zone, attack_flag) + var/armorval = armor_soak[attack_flag] + if(!armorval) + return 0 + else + return armorval + diff --git a/code/modules/mob/living/simple_mob/hands.dm b/code/modules/mob/living/simple_mob/hands.dm new file mode 100644 index 0000000000..52da3b75d7 --- /dev/null +++ b/code/modules/mob/living/simple_mob/hands.dm @@ -0,0 +1,123 @@ +// Hand procs for player-controlled SA's +/mob/living/simple_mob/swap_hand() + src.hand = !( src.hand ) + if(hud_used.l_hand_hud_object && hud_used.r_hand_hud_object) + if(hand) //This being 1 means the left hand is in use + hud_used.l_hand_hud_object.icon_state = "l_hand_active" + hud_used.r_hand_hud_object.icon_state = "r_hand_inactive" + else + hud_used.l_hand_hud_object.icon_state = "l_hand_inactive" + hud_used.r_hand_hud_object.icon_state = "r_hand_active" + return + +/mob/living/simple_mob/put_in_active_hand(var/obj/item/I) + if(!has_hands || !istype(I)) + return + +//Puts the item into our active hand if possible. returns 1 on success. +/mob/living/simple_mob/put_in_active_hand(var/obj/item/W) + if(!has_hands) + return FALSE + return (hand ? put_in_l_hand(W) : put_in_r_hand(W)) + +/mob/living/simple_mob/put_in_l_hand(var/obj/item/W) + if(!..() || l_hand) + return 0 + W.forceMove(src) + l_hand = W + W.equipped(src,slot_l_hand) + W.add_fingerprint(src) + update_inv_l_hand() + return TRUE + +/mob/living/simple_mob/put_in_r_hand(var/obj/item/W) + if(!..() || r_hand) + return 0 + W.forceMove(src) + r_hand = W + W.equipped(src,slot_r_hand) + W.add_fingerprint(src) + update_inv_r_hand() + return TRUE + +/mob/living/simple_mob/update_inv_r_hand() + if(QDESTROYING(src)) + return + + if(r_hand) + r_hand.screen_loc = ui_rhand //TODO + + //determine icon state to use + var/t_state + if(r_hand.item_state_slots && r_hand.item_state_slots[slot_r_hand_str]) + t_state = r_hand.item_state_slots[slot_r_hand_str] + else if(r_hand.item_state) + t_state = r_hand.item_state + else + t_state = r_hand.icon_state + + //determine icon to use + var/icon/t_icon + if(r_hand.item_icons && (slot_r_hand_str in r_hand.item_icons)) + t_icon = r_hand.item_icons[slot_r_hand_str] + else if(r_hand.icon_override) + t_state += "_r" + t_icon = r_hand.icon_override + else + t_icon = INV_R_HAND_DEF_ICON + + //apply color + var/image/standing = image(icon = t_icon, icon_state = t_state) + standing.color = r_hand.color + + r_hand_sprite = standing + + else + r_hand_sprite = null + + update_icon() + +/mob/living/simple_mob/update_inv_l_hand() + if(QDESTROYING(src)) + return + + if(l_hand) + l_hand.screen_loc = ui_lhand //TODO + + //determine icon state to use + var/t_state + if(l_hand.item_state_slots && l_hand.item_state_slots[slot_l_hand_str]) + t_state = l_hand.item_state_slots[slot_l_hand_str] + else if(l_hand.item_state) + t_state = l_hand.item_state + else + t_state = l_hand.icon_state + + //determine icon to use + var/icon/t_icon + if(l_hand.item_icons && (slot_l_hand_str in l_hand.item_icons)) + t_icon = l_hand.item_icons[slot_l_hand_str] + else if(l_hand.icon_override) + t_state += "_l" + t_icon = l_hand.icon_override + else + t_icon = INV_L_HAND_DEF_ICON + + //apply color + var/image/standing = image(icon = t_icon, icon_state = t_state) + standing.color = l_hand.color + + l_hand_sprite = standing + + else + l_hand_sprite = null + + update_icon() + +//Can insert extra huds into the hud holder here. +/mob/living/simple_mob/proc/extra_huds(var/datum/hud/hud,var/icon/ui_style,var/list/hud_elements) + return + +//If they can or cannot use tools/machines/etc +/mob/living/simple_mob/IsAdvancedToolUser() + return has_hands \ No newline at end of file diff --git a/code/modules/mob/living/simple_mob/life.dm b/code/modules/mob/living/simple_mob/life.dm new file mode 100644 index 0000000000..2b3fbf50ed --- /dev/null +++ b/code/modules/mob/living/simple_mob/life.dm @@ -0,0 +1,159 @@ +/mob/living/simple_mob/Life() + ..() + + //Health + updatehealth() + if(stat >= DEAD) + return FALSE + + handle_stunned() + handle_weakened() + handle_paralysed() + handle_supernatural() + handle_atmos() + + handle_special() + + return TRUE + + +//Should we be dead? +/mob/living/simple_mob/updatehealth() + health = getMaxHealth() - getToxLoss() - getFireLoss() - getBruteLoss() + + //Alive, becoming dead + if((stat < DEAD) && (health <= 0)) + death() + + //Overhealth + if(health > getMaxHealth()) + health = getMaxHealth() + + //Update our hud if we have one + if(healths) + if(stat != DEAD) + var/heal_per = (health / getMaxHealth()) * 100 + switch(heal_per) + if(100 to INFINITY) + healths.icon_state = "health0" + if(80 to 100) + healths.icon_state = "health1" + if(60 to 80) + healths.icon_state = "health2" + if(40 to 60) + healths.icon_state = "health3" + if(20 to 40) + healths.icon_state = "health4" + if(0 to 20) + healths.icon_state = "health5" + else + healths.icon_state = "health6" + else + healths.icon_state = "health7" + + //Updates the nutrition while we're here + if(nutrition_icon) + var/food_per = (nutrition / initial(nutrition)) * 100 + switch(food_per) + if(90 to INFINITY) + nutrition_icon.icon_state = "nutrition0" + if(75 to 90) + nutrition_icon.icon_state = "nutrition1" + if(50 to 75) + nutrition_icon.icon_state = "nutrition2" + if(25 to 50) + nutrition_icon.icon_state = "nutrition3" + if(0 to 25) + nutrition_icon.icon_state = "nutrition4" + +// Override for special bullshit. +/mob/living/simple_mob/proc/handle_special() + return + + +// Handle interacting with and taking damage from atmos +// TODO - Refactor this to use handle_environment() like a good /mob/living +/mob/living/simple_mob/proc/handle_atmos() + var/atmos_unsuitable = 0 + + var/atom/A = src.loc + + if(istype(A,/turf)) + var/turf/T = A + + var/datum/gas_mixture/Environment = T.return_air() + + if(Environment) + + if( abs(Environment.temperature - bodytemperature) > 40 ) + bodytemperature += ((Environment.temperature - bodytemperature) / 5) + + if(min_oxy) + if(Environment.gas["oxygen"] < min_oxy) + atmos_unsuitable = 1 + if(max_oxy) + if(Environment.gas["oxygen"] > max_oxy) + atmos_unsuitable = 1 + if(min_tox) + if(Environment.gas["phoron"] < min_tox) + atmos_unsuitable = 2 + if(max_tox) + if(Environment.gas["phoron"] > max_tox) + atmos_unsuitable = 2 + if(min_n2) + if(Environment.gas["nitrogen"] < min_n2) + atmos_unsuitable = 1 + if(max_n2) + if(Environment.gas["nitrogen"] > max_n2) + atmos_unsuitable = 1 + if(min_co2) + if(Environment.gas["carbon_dioxide"] < min_co2) + atmos_unsuitable = 1 + if(max_co2) + if(Environment.gas["carbon_dioxide"] > max_co2) + atmos_unsuitable = 1 + + //Atmos effect + if(bodytemperature < minbodytemp) + fire_alert = 2 + adjustBruteLoss(cold_damage_per_tick) + if(fire) + fire.icon_state = "fire1" + else if(bodytemperature > maxbodytemp) + fire_alert = 1 + adjustBruteLoss(heat_damage_per_tick) + if(fire) + fire.icon_state = "fire2" + else + fire_alert = 0 + if(fire) + fire.icon_state = "fire0" + + if(atmos_unsuitable) + adjustBruteLoss(unsuitable_atoms_damage) + if(oxygen) + oxygen.icon_state = "oxy1" + else if(oxygen) + if(oxygen) + oxygen.icon_state = "oxy0" + + +/mob/living/simple_mob/proc/handle_supernatural() + if(purge) + purge -= 1 + +/mob/living/simple_mob/death(gibbed, deathmessage = "dies!") + density = 0 //We don't block even if we did before + + if(has_eye_glow) + remove_eyes() + + if(loot_list.len) //Drop any loot + for(var/path in loot_list) + if(prob(loot_list[path])) + new path(get_turf(src)) + + spawn(3) //We'll update our icon in a sec + update_icon() + + return ..(gibbed,deathmessage) \ No newline at end of file diff --git a/code/modules/mob/living/simple_mob/on_click.dm b/code/modules/mob/living/simple_mob/on_click.dm new file mode 100644 index 0000000000..be162bc230 --- /dev/null +++ b/code/modules/mob/living/simple_mob/on_click.dm @@ -0,0 +1,47 @@ +/* + Animals +*/ +/mob/living/simple_mob/UnarmedAttack(var/atom/A, var/proximity) + if(!(. = ..())) + return + +// setClickCooldown(get_attack_speed()) + + if(has_hands && istype(A,/obj) && a_intent != I_HURT) + var/obj/O = A + return O.attack_hand(src) + + switch(a_intent) + if(I_HELP) + if(isliving(A)) + custom_emote(1,"[pick(friendly)] [A]!") + + if(I_HURT) + if(prob(special_attack_prob)) + if(special_attack_min_range <= 1) + special_attack_target(A) + + else if(melee_damage_upper == 0 && istype(A,/mob/living)) + custom_emote(1,"[pick(friendly)] [A]!") + + else + attack_target(A) + + if(I_GRAB) + if(has_hands) + A.attack_hand(src) + + if(I_DISARM) + if(has_hands) + A.attack_hand(src) + +/mob/living/simple_mob/RangedAttack(var/atom/A) +// setClickCooldown(get_attack_speed()) + var/distance = get_dist(src, A) + + if(prob(special_attack_prob) && (distance >= special_attack_min_range) && (distance <= special_attack_max_range)) + special_attack_target() + return + + if(projectiletype) + shoot_target(A) \ No newline at end of file diff --git a/code/modules/mob/living/simple_mob/simple_hud.dm b/code/modules/mob/living/simple_mob/simple_hud.dm new file mode 100644 index 0000000000..fe851648b4 --- /dev/null +++ b/code/modules/mob/living/simple_mob/simple_hud.dm @@ -0,0 +1,311 @@ +/mob/living/simple_mob/instantiate_hud(var/datum/hud/hud) + if(!client) + return //Why bother. + + var/ui_style = 'icons/mob/screen1_animal.dmi' + if(ui_icons) + ui_style = ui_icons + + var/ui_color = "#ffffff" + var/ui_alpha = 255 + + var/list/adding = list() + var/list/other = list() + var/list/hotkeybuttons = list() + var/list/slot_info = list() + + hud.adding = adding + hud.other = other + hud.hotkeybuttons = hotkeybuttons + + var/list/hud_elements = list() + var/obj/screen/using + var/obj/screen/inventory/inv_box + + var/has_hidden_gear + if(LAZYLEN(hud_gears)) + for(var/gear_slot in hud_gears) + inv_box = new /obj/screen/inventory() + inv_box.icon = ui_style + inv_box.color = ui_color + inv_box.alpha = ui_alpha + + var/list/slot_data = hud_gears[gear_slot] + inv_box.name = gear_slot + inv_box.screen_loc = slot_data["loc"] + inv_box.slot_id = slot_data["slot"] + inv_box.icon_state = slot_data["state"] + slot_info["[inv_box.slot_id]"] = inv_box.screen_loc + + if(slot_data["dir"]) + inv_box.set_dir(slot_data["dir"]) + + if(slot_data["toggle"]) + other += inv_box + has_hidden_gear = 1 + else + adding += inv_box + + if(has_hidden_gear) + using = new /obj/screen() + using.name = "toggle" + using.icon = ui_style + using.icon_state = "other" + using.screen_loc = ui_inventory + using.hud_layerise() + using.color = ui_color + using.alpha = ui_alpha + adding += using + + //Intent Backdrop + using = new /obj/screen() + using.name = "act_intent" + using.icon = ui_style + using.icon_state = "intent_"+a_intent + using.screen_loc = ui_acti + using.color = ui_color + using.alpha = ui_alpha + hud.adding += using + hud.action_intent = using + + hud_elements |= using + + //Small intent quarters + var/icon/ico + + ico = new(ui_style, "black") + ico.MapColors(0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, -1,-1,-1,-1) + ico.DrawBox(rgb(255,255,255,1),1,ico.Height()/2,ico.Width()/2,ico.Height()) + using = new /obj/screen() + using.name = I_HELP + using.icon = ico + using.screen_loc = ui_acti + using.alpha = ui_alpha + using.layer = LAYER_HUD_ITEM //These sit on the intent box + hud.adding += using + hud.help_intent = using + + ico = new(ui_style, "black") + ico.MapColors(0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, -1,-1,-1,-1) + ico.DrawBox(rgb(255,255,255,1),ico.Width()/2,ico.Height()/2,ico.Width(),ico.Height()) + using = new /obj/screen() + using.name = I_DISARM + using.icon = ico + using.screen_loc = ui_acti + using.alpha = ui_alpha + using.layer = LAYER_HUD_ITEM + hud.adding += using + hud.disarm_intent = using + + ico = new(ui_style, "black") + ico.MapColors(0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, -1,-1,-1,-1) + ico.DrawBox(rgb(255,255,255,1),ico.Width()/2,1,ico.Width(),ico.Height()/2) + using = new /obj/screen() + using.name = I_GRAB + using.icon = ico + using.screen_loc = ui_acti + using.alpha = ui_alpha + using.layer = LAYER_HUD_ITEM + hud.adding += using + hud.grab_intent = using + + ico = new(ui_style, "black") + ico.MapColors(0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, -1,-1,-1,-1) + ico.DrawBox(rgb(255,255,255,1),1,1,ico.Width()/2,ico.Height()/2) + using = new /obj/screen() + using.name = I_HURT + using.icon = ico + using.screen_loc = ui_acti + using.alpha = ui_alpha + using.layer = LAYER_HUD_ITEM + hud.adding += using + hud.hurt_intent = using + + //Move intent (walk/run) + using = new /obj/screen() + using.name = "mov_intent" + using.icon = ui_style + using.icon_state = (m_intent == "run" ? "running" : "walking") + using.screen_loc = ui_movi + using.color = ui_color + using.alpha = ui_alpha + hud.adding += using + hud.move_intent = using + + //Resist button + using = new /obj/screen() + using.name = "resist" + using.icon = ui_style + using.icon_state = "act_resist" + using.screen_loc = ui_pull_resist + using.color = ui_color + using.alpha = ui_alpha + hud.hotkeybuttons += using + + //Pull button + pullin = new /obj/screen() + pullin.icon = ui_style + pullin.icon_state = "pull0" + pullin.name = "pull" + pullin.screen_loc = ui_pull_resist + hud.hotkeybuttons += pullin + hud_elements |= pullin + + //Health status + healths = new /obj/screen() + healths.icon = ui_style + healths.icon_state = "health0" + healths.name = "health" + healths.screen_loc = ui_health + hud_elements |= healths + + //Oxygen dep icon + oxygen = new /obj/screen() + oxygen.icon = ui_style + oxygen.icon_state = "oxy0" + oxygen.name = "oxygen" + oxygen.screen_loc = ui_oxygen + hud_elements |= oxygen + + //Toxins present icon + toxin = new /obj/screen() + toxin.icon = ui_style + toxin.icon_state = "tox0" + toxin.name = "toxin" + toxin.screen_loc = ui_toxin + hud_elements |= toxin + + //Fire warning + fire = new /obj/screen() + fire.icon = ui_style + fire.icon_state = "fire0" + fire.name = "fire" + fire.screen_loc = ui_fire + hud_elements |= fire + + //Pressure warning + pressure = new /obj/screen() + pressure.icon = ui_style + pressure.icon_state = "pressure0" + pressure.name = "pressure" + pressure.screen_loc = ui_pressure + hud_elements |= pressure + + //Body temp warning + bodytemp = new /obj/screen() + bodytemp.icon = ui_style + bodytemp.icon_state = "temp0" + bodytemp.name = "body temperature" + bodytemp.screen_loc = ui_temp + hud_elements |= bodytemp + + //Nutrition status + nutrition_icon = new /obj/screen() + nutrition_icon.icon = ui_style + nutrition_icon.icon_state = "nutrition0" + nutrition_icon.name = "nutrition" + nutrition_icon.screen_loc = ui_nutrition + hud_elements |= nutrition_icon + + pain = new /obj/screen( null ) + + zone_sel = new /obj/screen/zone_sel( null ) + zone_sel.icon = ui_style + zone_sel.color = ui_color + zone_sel.alpha = ui_alpha + zone_sel.overlays.Cut() + zone_sel.overlays += image('icons/mob/zone_sel.dmi', "[zone_sel.selecting]") + hud_elements |= zone_sel + + //Hand things + if(has_hands) + //Drop button + using = new /obj/screen() + using.name = "drop" + using.icon = ui_style + using.icon_state = "act_drop" + using.screen_loc = ui_drop_throw + using.color = ui_color + using.alpha = ui_alpha + hud.hotkeybuttons += using + + //Equip detail + using = new /obj/screen() + using.name = "equip" + using.icon = ui_style + using.icon_state = "act_equip" + using.screen_loc = ui_equip + using.color = ui_color + using.alpha = ui_alpha + hud.adding += using + + //Hand slots themselves + inv_box = new /obj/screen/inventory/hand() + inv_box.hud = src + inv_box.name = "r_hand" + inv_box.icon = ui_style + inv_box.icon_state = "r_hand_inactive" + if(!hand) //This being 0 or null means the right hand is in use + inv_box.icon_state = "r_hand_active" + inv_box.screen_loc = ui_rhand + inv_box.slot_id = slot_r_hand + inv_box.color = ui_color + inv_box.alpha = ui_alpha + hud.r_hand_hud_object = inv_box + hud.adding += inv_box + slot_info["[slot_r_hand]"] = inv_box.screen_loc + + inv_box = new /obj/screen/inventory/hand() + inv_box.hud = src + inv_box.name = "l_hand" + inv_box.icon = ui_style + inv_box.icon_state = "l_hand_inactive" + if(hand) //This being 1 means the left hand is in use + inv_box.icon_state = "l_hand_active" + inv_box.screen_loc = ui_lhand + inv_box.slot_id = slot_l_hand + inv_box.color = ui_color + inv_box.alpha = ui_alpha + hud.l_hand_hud_object = inv_box + hud.adding += inv_box + slot_info["[slot_l_hand]"] = inv_box.screen_loc + + //Swaphand titlebar + using = new /obj/screen/inventory() + using.name = "hand" + using.icon = ui_style + using.icon_state = "hand1" + using.screen_loc = ui_swaphand1 + using.color = ui_color + using.alpha = ui_alpha + hud.adding += using + + using = new /obj/screen/inventory() + using.name = "hand" + using.icon = ui_style + using.icon_state = "hand2" + using.screen_loc = ui_swaphand2 + using.color = ui_color + using.alpha = ui_alpha + hud.adding += using + + //Throw button + throw_icon = new /obj/screen() + throw_icon.icon = ui_style + throw_icon.icon_state = "act_throw_off" + throw_icon.name = "throw" + throw_icon.screen_loc = ui_drop_throw + throw_icon.color = ui_color + throw_icon.alpha = ui_alpha + hud.hotkeybuttons += throw_icon + hud_elements |= throw_icon + + extra_huds(hud,ui_style,hud_elements) + + client.screen = list() + + client.screen += hud_elements + client.screen += adding + hotkeybuttons + client.screen += client.void + + return diff --git a/code/modules/mob/living/simple_mob/simple_mob.dm b/code/modules/mob/living/simple_mob/simple_mob.dm new file mode 100644 index 0000000000..cc38d0f6b4 --- /dev/null +++ b/code/modules/mob/living/simple_mob/simple_mob.dm @@ -0,0 +1,252 @@ +// Reorganized and somewhat cleaned up. +// AI code has been made into a datum, inside the AI module folder. + +/mob/living/simple_mob + name = "animal" + desc = "" + icon = 'icons/mob/animal.dmi' + health = 20 + maxHealth = 20 + + mob_bump_flag = SIMPLE_ANIMAL + mob_swap_flags = MONKEY|SLIME|HUMAN + mob_push_flags = MONKEY|SLIME|HUMAN + + var/tt_desc = "Uncataloged Life Form" //Tooltip description + + //Settings for played mobs + var/show_stat_health = 1 // Does the percentage health show in the stat panel for the mob + var/has_hands = 0 // Set to 1 to enable the use of hands and the hands hud + var/list/hud_gears // Slots to show on the hud (typically none) + var/ui_icons // Icon file path to use for the HUD, otherwise generic icons are used + var/r_hand_sprite // If they have hands, + var/l_hand_sprite // they could use some icons. + var/player_msg // Message to print to players about 'how' to play this mob on login. + + //Mob icon/appearance settings + var/icon_living = "" // The iconstate if we're alive, required + var/icon_dead = "" // The iconstate if we're dead, required + var/icon_gib = "generic_gib" // The iconstate for being gibbed, optional. Defaults to a generic gib animation. + var/icon_rest = null // The iconstate for resting, optional + var/image/modifier_overlay = null // Holds overlays from modifiers. + var/image/eye_layer = null // Holds the eye overlay. + var/has_eye_glow = FALSE // If true, adds an overlay over the lighting plane for [icon_state]-eyes. + attack_icon = 'icons/effects/effects.dmi' //Just the default, played like the weapon attack anim + attack_icon_state = "slash" //Just the default + + //Mob talking settings + universal_speak = 0 // Can all mobs in the entire universe understand this one? + var/has_langs = list(LANGUAGE_GALCOM)// Text name of their language if they speak something other than galcom. They speak the first one. + + //Movement things. + var/movement_cooldown = 5 // Lower is faster. + var/movement_sound = null // If set, will play this sound when it moves on its own will. + + //Mob interaction + var/response_help = "tries to help" // If clicked on help intent + var/response_disarm = "tries to disarm" // If clicked on disarm intent + var/response_harm = "tries to hurt" // If clicked on harm intent + var/list/friends = list() // Mobs on this list wont get attacked regardless of faction status. + var/harm_intent_damage = 3 // How much an unarmed harm click does to this mob. + var/meat_amount = 0 // How much meat to drop from this mob when butchered + var/obj/meat_type // The meat object to drop + var/list/loot_list = list() // The list of lootable objects to drop, with "/path = prob%" structure + var/obj/item/weapon/card/id/myid// An ID card if they have one to give them access to stuff. + + //Mob environment settings + var/minbodytemp = 250 // Minimum "okay" temperature in kelvin + var/maxbodytemp = 350 // Maximum of above + var/heat_damage_per_tick = 3 // Amount of damage applied if animal's body temperature is higher than maxbodytemp + var/cold_damage_per_tick = 2 // Same as heat_damage_per_tick, only if the bodytemperature it's lower than minbodytemp + var/fire_alert = 0 // 0 = fine, 1 = hot, 2 = cold + + var/min_oxy = 5 // Oxygen in moles, minimum, 0 is 'no minimum' + var/max_oxy = 0 // Oxygen in moles, maximum, 0 is 'no maximum' + var/min_tox = 0 // Phoron min + var/max_tox = 1 // Phoron max + var/min_co2 = 0 // CO2 min + var/max_co2 = 5 // CO2 max + var/min_n2 = 0 // N2 min + var/max_n2 = 0 // N2 max + var/unsuitable_atoms_damage = 2 // This damage is taken when atmos doesn't fit all the requirements above + + //Hostility settings + var/taser_kill = 1 // Is the mob weak to tasers + + //Attack ranged settings + var/projectiletype // The projectiles I shoot + var/projectilesound // The sound I make when I do it + var/casingtype // What to make the hugely laggy casings pile out of + + //Mob melee settings + var/melee_damage_lower = 2 // Lower bound of randomized melee damage + var/melee_damage_upper = 6 // Upper bound of randomized melee damage + var/list/attacktext = list("attacked") // "You are [attacktext] by the mob!" + var/list/friendly = list("nuzzles") // "The mob [friendly] the person." + var/attack_sound = null // Sound to play when I attack + var/melee_miss_chance = 15 // percent chance to miss a melee attack. + var/attack_armor_type = "melee" // What armor does this check? + var/attack_armor_pen = 0 // How much armor pen this attack has. + var/attack_sharp = 0 // Is the attack sharp? + var/attack_edge = 0 // Does the attack have an edge? + + //Special attacks + var/special_attack_prob = 0 // Chance of the mob doing a special attack (0 for never) + var/special_attack_min_range = 0 // Min range to perform the special attacks from + var/special_attack_max_range = 0 // Max range to perform special attacks from + + //Damage resistances + var/grab_resist = 0 // Chance for a grab attempt to fail. Note that this is not a true resist and is just a prob() of failure. + var/resistance = 0 // Damage reduction for all types + var/list/armor = list( // Values for normal getarmor() checks + "melee" = 0, + "bullet" = 0, + "laser" = 0, + "energy" = 0, + "bomb" = 0, + "bio" = 100, + "rad" = 100 + ) + var/list/armor_soak = list( // Values for getsoak() checks. + "melee" = 0, + "bullet" = 0, + "laser" = 0, + "energy" = 0, + "bomb" = 0, + "bio" = 0, + "rad" = 0 + ) + var/purge = 0 // Cult stuff. + var/supernatural = FALSE // Ditto. + + +/mob/living/simple_mob/initialize() + verbs -= /mob/verb/observe + maxHealth = health + + for(var/L in has_langs) + languages |= all_languages[L] + if(languages.len) + default_language = languages[1] + + if(has_eye_glow) + add_eyes() + return ..() + + +/mob/living/simple_mob/Destroy() + default_language = null + if(myid) + qdel(myid) + myid = null + + friends.Cut() + languages.Cut() + + if(has_eye_glow) + remove_eyes() + return ..() + + +//Client attached +/mob/living/simple_mob/Login() + . = ..() + to_chat(src,"You are \the [src]. [player_msg]") + + +/mob/living/simple_mob/emote(var/act, var/type, var/desc) + if(act) + ..(act, type, desc) + + +/mob/living/simple_mob/SelfMove() + . = ..() + if(movement_sound) + playsound(src, movement_sound, 50, 1) + +/mob/living/simple_mob/movement_delay() + var/tally = 0 //Incase I need to add stuff other than "speed" later + + tally = movement_cooldown + + if(force_max_speed) + return -3 + + for(var/datum/modifier/M in modifiers) + if(!isnull(M.haste) && M.haste == TRUE) + return -3 + if(!isnull(M.slowdown)) + tally += M.slowdown + + if(purge)//Purged creatures will move more slowly. The more time before their purge stops, the slower they'll move. + if(tally <= 0) + tally = 1 + tally *= purge + + if(m_intent == "walk") + tally *= 1.5 + + return tally+config.animal_delay + + +/mob/living/simple_mob/Stat() + ..() + if(statpanel("Status") && show_stat_health) + stat(null, "Health: [round((health / getMaxHealth()) * 100)]%") + +/mob/living/simple_mob/lay_down() + ..() + if(resting && icon_rest) + icon_state = icon_rest + else + icon_state = icon_living + update_icon() + + +/mob/living/simple_mob/say(var/message,var/datum/language/language) + var/verb = "says" + if(speak_emote.len) + verb = pick(speak_emote) + + message = sanitize(message) + + ..(message, null, verb) + +/mob/living/simple_mob/get_speech_ending(verb, var/ending) + return verb + + +/mob/living/simple_mob/put_in_hands(var/obj/item/W) // No hands. + W.forceMove(get_turf(src)) + return 1 + +// Harvest an animal's delicious byproducts +/mob/living/simple_mob/proc/harvest(var/mob/user) + var/actual_meat_amount = max(1,(meat_amount/2)) + if(meat_type && actual_meat_amount>0 && (stat == DEAD)) + for(var/i=0;i[user] chops up \the [src]!") + new/obj/effect/decal/cleanable/blood/splatter(get_turf(src)) + qdel(src) + else + user.visible_message("[user] butchers \the [src] messily!") + gib() + + +/mob/living/simple_mob/is_sentient() + return mob_class & MOB_CLASS_HUMANOID|MOB_CLASS_ANIMAL // Update this if needed. +// return intelligence_level != SA_PLANT && intelligence_level != SA_ROBOTIC + +//Just some subpaths for easy searching +/mob/living/simple_mob/hostile + faction = "not yours" +// ai_holder_type = /datum/ai_holder/regular/hostile + +/mob/living/simple_mob/retaliate +// ai_holder_type = /datum/ai_holder/regular/retaliate + +/mob/living/simple_mob/get_nametag_desc(mob/user) + return "[tt_desc]" \ No newline at end of file diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/animal.dm b/code/modules/mob/living/simple_mob/subtypes/animal/animal.dm new file mode 100644 index 0000000000..864ee02ace --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/animal/animal.dm @@ -0,0 +1,2 @@ +/mob/living/simple_mob/animal + mob_class = MOB_CLASS_ANIMAL \ No newline at end of file diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider.dm b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider.dm new file mode 100644 index 0000000000..efa0dc77bc --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider.dm @@ -0,0 +1,64 @@ +/* + Spiders come in various types, and are a fairly common enemy both inside and outside the station. + Their attacks can inject reagents, which can cause harm long after the spider is killed. + Thick material will prevent injections, similar to other means of injections. +*/ + +// The base spider, in the 'walking tank' family. +/mob/living/simple_mob/animal/giant_spider + name = "giant spider" + desc = "Furry and brown, it makes you shudder to look at it. This one has deep red eyes." + tt_desc = "Atrax robustus gigantus" + icon_state = "guard" + icon_living = "guard" + icon_dead = "guard_dead" + has_eye_glow = TRUE + + faction = "spiders" + maxHealth = 200 + health = 200 + pass_flags = PASSTABLE + movement_cooldown = 10 + + see_in_dark = 10 + + response_help = "pets" + response_disarm = "gently pushes aside" + response_harm = "punches" + + melee_damage_lower = 18 + melee_damage_upper = 30 + attack_sharp = 1 + attack_edge = 1 + + heat_damage_per_tick = 20 + cold_damage_per_tick = 20 + + speak_emote = list("chitters") + + meat_type = /obj/item/weapon/reagent_containers/food/snacks/xenomeat/spidermeat + + say_list_type = /datum/say_list/spider + ai_holder_type = /datum/ai_holder/simple_mob/melee + + var/poison_type = "spidertoxin" // The reagent that gets injected when it attacks. + var/poison_chance = 10 // Chance for injection to occur. + var/poison_per_bite = 5 // Amount added per injection. + +/mob/living/simple_mob/animal/giant_spider/apply_melee_effects(var/atom/A) + if(isliving(A)) + var/mob/living/L = A + if(L.reagents) + var/target_zone = pick(BP_TORSO,BP_TORSO,BP_TORSO,BP_L_LEG,BP_R_LEG,BP_L_ARM,BP_R_ARM,BP_HEAD) + if(L.can_inject(src, null, target_zone)) + inject_poison(L, target_zone) + +// Does actual poison injection, after all checks passed. +/mob/living/simple_mob/animal/giant_spider/proc/inject_poison(mob/living/L, target_zone) + if(prob(poison_chance)) + to_chat(L, "You feel a tiny prick.") + L.reagents.add_reagent(poison_type, poison_per_bite) + +// Subtype + + diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/nurse.dm b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/nurse.dm new file mode 100644 index 0000000000..8d7532fbf5 --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/animal/giant_spider/nurse.dm @@ -0,0 +1,188 @@ +// Nurses, they create webs and eggs. +// They're fragile but their attacks can cause horrifying consequences. +/mob/living/simple_mob/animal/giant_spider/nurse + desc = "Furry and beige, it makes you shudder to look at it. This one has brilliant green eyes." + icon_state = "nurse" + icon_living = "nurse" + icon_dead = "nurse_dead" + + maxHealth = 40 + health = 40 + + movement_cooldown = 5 // A bit faster so that they can inject the eggs easier. + + melee_damage_lower = 5 // Doesn't do a lot of damage, since the goal is to make more spiders with egg attacks. + melee_damage_upper = 10 + poison_per_bite = 5 + poison_type = "stoxin" + + player_msg = "You can spin webs on an adjacent tile, or cocoon an object by clicking on it.
\ + You can also cocoon a dying or dead entity by clicking on them, and you will gain charges for egg-laying.
\ + To lay eggs, click a nearby tile. Laying eggs will deplete a charge." + ai_holder_type = /datum/ai_holder/simple_mob/melee/nurse_spider + + var/fed = 0 // Counter for how many egg laying 'charges' the spider has. + var/egg_inject_chance = 25 // One in four chance to get eggs. + var/egg_type = /obj/effect/spider/eggcluster/small + var/web_type = /obj/effect/spider/stickyweb/dark + + +/mob/living/simple_mob/animal/giant_spider/nurse/inject_poison(mob/living/L, target_zone) + ..() // Inject the stoxin here. + if(ishuman(L) && prob(egg_inject_chance)) + var/mob/living/carbon/human/H = L + var/obj/item/organ/external/O = H.get_organ(target_zone) + if(O) + var/eggcount = 0 + for(var/obj/effect/spider/eggcluster/E in O.implants) + eggcount++ + if(!eggcount) + var/eggs = new egg_type(O, src) + O.implants += eggs + to_chat(H, span("critical", "\The [src] injects something into your [O.name]!") ) // Oh god its laying eggs in me! + +// Webs target in a web if able to. +/mob/living/simple_mob/animal/giant_spider/nurse/attack_target(atom/A) + if(isturf(A)) + if(fed) + return lay_eggs(A) + return web_tile(A) + + if(isliving(A)) + var/mob/living/L = A + if(!L.stat) + return ..() + + if(!istype(A, /atom/movable)) + return + var/atom/movable/AM = A + + if(AM.anchored) + return ..() + + return spin_cocoon(AM) + +/mob/living/simple_mob/animal/giant_spider/nurse/proc/spin_cocoon(atom/movable/AM) + if(!istype(AM)) + return FALSE // We can't cocoon walls sadly. + visible_message(span("notice", "\The [src] begins to secrete a sticky substance around \the [AM].") ) + + // Get our AI to stay still. + set_AI_busy(TRUE) + + if(!do_mob(src, AM, 5 SECONDS)) + set_AI_busy(FALSE) + to_chat(src, span("warning", "You need to stay still to spin a web around \the [AM].")) + return FALSE + + set_AI_busy(FALSE) + + if(!AM) // Make sure it didn't get deleted for whatever reason. + to_chat(src, span("warning", "Whatever you were spinning a web for, its no longer there...")) + return FALSE + + if(!isturf(AM.loc)) + to_chat(src, span("warning", "You can't spin \the [AM] in a web while it is inside \the [AM.loc].")) + return FALSE + + if(!Adjacent(AM)) + to_chat(src, span("warning", "You need to be next to \the [AM] to spin it into a web.")) + return FALSE + + // Finally done with the checks. + var/obj/effect/spider/cocoon/C = new(AM.loc) + var/large_cocoon = FALSE + for(var/mob/living/L in C.loc) + if(istype(L, /mob/living/simple_mob/animal/giant_spider)) // Cannibalism is bad. + continue + fed++ + visible_message(span("warning","\The [src] sticks a proboscis into \the [L], and sucks a viscous substance out.")) + to_chat(src, span("notice", "You've fed upon \the [L], and can now lay [fed] cluster\s of eggs.")) + L.forceMove(C) + large_cocoon = TRUE + break + + // This part's pretty stupid. + for(var/obj/O in C.loc) + if(!O.anchored) + O.forceMove(C) + + // Todo: Put this code on the cocoon object itself? + if(large_cocoon) + C.icon_state = pick("cocoon_large1","cocoon_large2","cocoon_large3") + + return TRUE + +/mob/living/simple_mob/animal/giant_spider/nurse/handle_special() + set waitfor = FALSE + if(get_AI_stance() == STANCE_IDLE && !is_AI_busy() && isturf(loc)) + if(fed) + lay_eggs(loc) + else + web_tile(loc) + +/mob/living/simple_mob/animal/giant_spider/nurse/proc/web_tile(turf/T) + if(!istype(T)) + return FALSE + + var/obj/effect/spider/stickyweb/W = locate() in T + if(W) + return FALSE // Already got webs here. + + visible_message(span("notice", "\The [src] begins to secrete a sticky substance.") ) + // Get our AI to stay still. + set_AI_busy(TRUE) + + if(!do_mob(src, T, 5 SECONDS)) + set_AI_busy(FALSE) + to_chat(src, span("warning", "You need to stay still to spin a web on \the [T].")) + return FALSE + + W = locate() in T + if(W) + return FALSE // Spamclick protection. + + set_AI_busy(FALSE) + new web_type(T) + return TRUE + + +/mob/living/simple_mob/animal/giant_spider/nurse/proc/lay_eggs(turf/T) + if(!istype(T)) + return FALSE + + if(!fed) + return FALSE + + var/obj/effect/spider/eggcluster/E = locate() in T + if(E) + return FALSE // Already got eggs here. + + visible_message(span("notice", "\The [src] begins to lay a cluster of eggs.") ) + // Get our AI to stay still. + set_AI_busy(TRUE) + + if(!do_mob(src, T, 5 SECONDS)) + set_AI_busy(FALSE) + to_chat(src, span("warning", "You need to stay still to lay eggs on \the [T].")) + return FALSE + + E = locate() in T + if(E) + return FALSE // Spamclick protection. + + set_AI_busy(FALSE) + new egg_type(T) + fed-- + return TRUE + + +// Variant that 'blocks' light (by being a negative light source). +// This is done to make webbed rooms scary and allow for spiders on the other side of webs to see prey. +/obj/effect/spider/stickyweb/dark + name = "dense web" + desc = "It's sticky, and blocks a lot of light." + light_color = "#FFFFFF" + light_range = 2 + light_power = -3 + diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/sif/hooligan_crab.dm b/code/modules/mob/living/simple_mob/subtypes/animal/sif/hooligan_crab.dm new file mode 100644 index 0000000000..fd0f5a8fbc --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/animal/sif/hooligan_crab.dm @@ -0,0 +1,59 @@ +/* + Hooligan Crabs are called so because they are rather curious and tend to follow people, + whether the people want them to or not, and sometimes causing vandalism by accident. + They're pretty strong and have strong melee armor, but won't attack first. + They unknowingly play a role in keeping the shoreline fairly safe, by killing whatever would attack other people. +*/ + +/mob/living/simple_mob/animal/sif/hooligan_crab + name = "hooligan crab" + desc = "A large, hard-shelled crustacean. This one is mostly grey." + icon_state = "sif_crab" + icon_living = "sif_crab" + icon_dead = "sif_crab_dead" + icon_scale = 1.5 + + faction = "crabs" + + maxHealth = 200 + health = 200 + movement_cooldown = 10 + movement_sound = 'sound/weapons/heavysmash.ogg' + armor = list( + "melee" = 40, + "bullet" = 20, + "laser" = 10, + "energy" = 0, + "bomb" = 0, + "bio" = 0, + "rad" = 0 + ) + armor_soak = list( + "melee" = 10, + "bullet" = 5, + "laser" = 0, + "energy" = 0, + "bomb" = 0, + "bio" = 0, + "rad" = 0 + ) + + mob_size = MOB_LARGE + + melee_damage_lower = 22 + melee_damage_upper = 35 + attack_armor_pen = 35 + attack_sharp = 1 + attack_edge = 1 + + meat_type = /obj/item/weapon/reagent_containers/food/snacks/meat + response_help = "pets" + response_disarm = "gently pushes aside" + response_harm = "kicks" + friendly = "pinches" + attacktext = list("clawed", "pinched", "crushed") + speak_emote = list("clicks") + + ai_holder_type = /datum/ai_holder/simple_mob/melee/hooligan + say_list_type = /datum/say_list/crab + diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/sif/sif.dm b/code/modules/mob/living/simple_mob/subtypes/animal/sif/sif.dm new file mode 100644 index 0000000000..b38949c237 --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/animal/sif/sif.dm @@ -0,0 +1,3 @@ +// Mobs intended to be on Sif. As such, they won't die to the cold. +/mob/living/simple_mob/animal/sif + minbodytemp = 175 \ No newline at end of file diff --git a/code/modules/mob/living/simple_mob/subtypes/mechanical/combat_drone.dm b/code/modules/mob/living/simple_mob/subtypes/mechanical/combat_drone.dm new file mode 100644 index 0000000000..bf425954a0 --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/mechanical/combat_drone.dm @@ -0,0 +1,71 @@ +/* + Combat drones have a rapid ranged attack, and have a projectile shield. + They are rather slow, but attempt to 'kite' its target. + A solid hit with an EMP grenade will kill the shield instantly. +*/ + +/mob/living/simple_mob/mechanical/combat_drone + name = "combat drone" + desc = "An automated combat drone armed with state of the art weaponry and shielding." + icon_state = "drone" + icon_living = "drone" + icon_dead = "drone_dead" + has_eye_glow = TRUE + + faction = "malf_drone" + + maxHealth = 50 // Shield has 150 for total of 200. + health = 50 + movement_cooldown = 5 + hovering = TRUE + + base_attack_cooldown = 5 + projectiletype = /obj/item/projectile/beam/drone + projectilesound = 'sound/weapons/laser3.ogg' + + response_help = "pokes" + response_disarm = "gently pushes aside" + response_harm = "hits" + + ai_holder_type = /datum/ai_holder/simple_mob/ranged/kiting/threatening + say_list_type = /datum/say_list/malf_drone + + var/datum/effect/effect/system/ion_trail_follow/ion_trail = null + var/obj/item/shield_projector/shields = null + +/mob/living/simple_mob/mechanical/combat_drone/initialize() + ion_trail = new + ion_trail.set_up(src) + ion_trail.start() + + shields = new /obj/item/shield_projector/rectangle/automatic/drone(src) + return ..() + +/mob/living/simple_mob/mechanical/combat_drone/Destroy() + qdel_null(ion_trail) + qdel_null(shields) + return ..() + +/mob/living/simple_mob/mechanical/combat_drone/death() + ..(null,"suddenly breaks apart.") + qdel(src) + +/mob/living/simple_mob/mechanical/combat_drone/Process_Spacemove(var/check_drift = 0) + return TRUE + +/obj/item/projectile/beam/drone + damage = 10 + +/obj/item/shield_projector/rectangle/automatic/drone + shield_health = 150 + max_shield_health = 150 + shield_regen_delay = 10 SECONDS + shield_regen_amount = 10 + size_x = 1 + size_y = 1 + +// A slightly easier drone, for POIs. +// Difference is that it should not be faster than you. +/mob/living/simple_mob/mechanical/combat_drone/lesser + desc = "An automated combat drone with an aged apperance." + movement_cooldown = 10 \ No newline at end of file diff --git a/code/modules/mob/living/simple_mob/subtypes/mechanical/mechanical.dm b/code/modules/mob/living/simple_mob/subtypes/mechanical/mechanical.dm new file mode 100644 index 0000000000..7fd21b43c7 --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/mechanical/mechanical.dm @@ -0,0 +1,20 @@ +/mob/living/simple_mob/mechanical + mob_class = MOB_CLASS_CONSTRUCT + + 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 + + taser_kill = FALSE + +/mob/living/simple_mob/mechanical/isSynthetic() + return TRUE + +/mob/living/simple_mob/mechanical/speech_bubble_appearance() + return faction != "neutral" ? "synthetic_evil" : "machine" \ No newline at end of file diff --git a/code/modules/mob/living/simple_mob/subtypes/mechanical/viscerator.dm b/code/modules/mob/living/simple_mob/subtypes/mechanical/viscerator.dm new file mode 100644 index 0000000000..e718cd880f --- /dev/null +++ b/code/modules/mob/living/simple_mob/subtypes/mechanical/viscerator.dm @@ -0,0 +1,36 @@ +/* + Viscerators are fragile and don't hit very hard, but fast, evasive, and rarely come alone. + They also tend to dodge while in melee range. + A weapon that can cleave is very effective against them. +*/ + +/mob/living/simple_mob/mechanical/viscerator + name = "viscerator" + desc = "A small, twin-bladed machine capable of inflicting very deadly lacerations." + icon = 'icons/mob/critter.dmi' + icon_state = "viscerator_attack" + icon_living = "viscerator_attack" + hovering = TRUE // Won't trigger landmines. + + faction = "syndicate" + maxHealth = 15 + health = 15 + movement_cooldown = 0 + + pass_flags = PASSTABLE + mob_swap_flags = 0 + mob_push_flags = 0 + + melee_damage_lower = 4 // Approx 8 DPS. + melee_damage_upper = 4 + base_attack_cooldown = 5 // Two attacks a second or so. + attack_sharp = 1 + attack_edge = 1 + attack_sound = 'sound/weapons/bladeslice.ogg' + attacktext = list("cut", "sliced") + + ai_holder_type = /datum/ai_holder/simple_mob/melee/evasive + +/mob/living/simple_mob/mechanical/viscerator/death() + ..(null,"is smashed into pieces!") + qdel(src) \ No newline at end of file diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm index 2841003152..80fc03bae1 100644 --- a/code/modules/mob/mob_movement.dm +++ b/code/modules/mob/mob_movement.dm @@ -13,6 +13,11 @@ /mob/proc/setMoveCooldown(var/timeout) move_delay = max(world.time + timeout, move_delay) +/mob/proc/check_move_cooldown() + if(world.time < src.move_delay) + return FALSE // Need to wait more. + return TRUE + /client/North() ..() @@ -198,7 +203,8 @@ if(moving) return 0 - if(world.time < mob.move_delay) return + if(!mob.check_move_cooldown()) + return if(locate(/obj/effect/stop/, mob.loc)) for(var/obj/effect/stop/S in mob.loc) diff --git a/code/modules/shieldgen/directional_shield.dm b/code/modules/shieldgen/directional_shield.dm index 1892a3f3ad..91d3a008b5 100644 --- a/code/modules/shieldgen/directional_shield.dm +++ b/code/modules/shieldgen/directional_shield.dm @@ -88,6 +88,8 @@ but allow those projectiles to leave the shield from the inside. Blocking too many damaging projectiles will cause the shield to fail." icon = 'icons/obj/device.dmi' icon_state = "signmaker_sec" + light_range = 4 + light_power = 4 var/active = FALSE // If it's on. var/shield_health = 400 // How much damage the shield blocks before breaking. This is a shared health pool for all shields attached to this projector. var/max_shield_health = 400 // Ditto. This is fairly high, but shields are really big, you can't miss them, and laser carbines pump out so much hurt. @@ -170,6 +172,8 @@ var/new_color = rgb(new_r, new_g, new_b) + set_light(light_range, light_power, new_color) + // Now deploy the new color to all the shields. for(var/obj/effect/directional_shield/S in active_shields) S.update_color(new_color) diff --git a/icons/mob/animal.dmi b/icons/mob/animal.dmi index 90cc743bcf..a88deb1b5d 100644 Binary files a/icons/mob/animal.dmi and b/icons/mob/animal.dmi differ diff --git a/polaris.dme b/polaris.dme index efd6fe7878..2064763b89 100644 --- a/polaris.dme +++ b/polaris.dme @@ -1266,6 +1266,7 @@ #include "code\modules\ai\ai_holder_targeting.dm" #include "code\modules\ai\interfaces.dm" #include "code\modules\ai\say_list.dm" +#include "code\modules\ai\aI_holder_subtypes\simple_mob_ai.dm" #include "code\modules\alarm\alarm.dm" #include "code\modules\alarm\alarm_handler.dm" #include "code\modules\alarm\atmosphere_alarm.dm" @@ -1887,7 +1888,6 @@ #include "code\modules\mob\living\silicon\robot\subtypes\syndicate.dm" #include "code\modules\mob\living\simple_animal\corpse.dm" #include "code\modules\mob\living\simple_animal\simple_animal.dm" -#include "code\modules\mob\living\simple_animal\simple_animal2.dm" #include "code\modules\mob\living\simple_animal\simple_hud.dm" #include "code\modules\mob\living\simple_animal\aliens\alien.dm" #include "code\modules\mob\living\simple_animal\aliens\creature.dm" @@ -1937,6 +1937,22 @@ #include "code\modules\mob\living\simple_animal\slime\life.dm" #include "code\modules\mob\living\simple_animal\slime\slime.dm" #include "code\modules\mob\living\simple_animal\slime\subtypes.dm" +#include "code\modules\mob\living\simple_mob\appearance.dm" +#include "code\modules\mob\living\simple_mob\combat.dm" +#include "code\modules\mob\living\simple_mob\defense.dm" +#include "code\modules\mob\living\simple_mob\hands.dm" +#include "code\modules\mob\living\simple_mob\life.dm" +#include "code\modules\mob\living\simple_mob\on_click.dm" +#include "code\modules\mob\living\simple_mob\simple_hud.dm" +#include "code\modules\mob\living\simple_mob\simple_mob.dm" +#include "code\modules\mob\living\simple_mob\subtypes\animal\animal.dm" +#include "code\modules\mob\living\simple_mob\subtypes\animal\giant_spider.dm" +#include "code\modules\mob\living\simple_mob\subtypes\animal\giant_spider\nurse.dm" +#include "code\modules\mob\living\simple_mob\subtypes\animal\sif\hooligan_crab.dm" +#include "code\modules\mob\living\simple_mob\subtypes\animal\sif\sif.dm" +#include "code\modules\mob\living\simple_mob\subtypes\mechanical\combat_drone.dm" +#include "code\modules\mob\living\simple_mob\subtypes\mechanical\mechanical.dm" +#include "code\modules\mob\living\simple_mob\subtypes\mechanical\viscerator.dm" #include "code\modules\mob\living\voice\voice.dm" #include "code\modules\mob\new_player\login.dm" #include "code\modules\mob\new_player\logout.dm" @@ -2448,7 +2464,7 @@ #include "code\ZAS\Zone.dm" #include "interface\interface.dm" #include "interface\skin.dmf" -#include "maps\plane\plane.dm" +#include "maps\example\example.dm" #include "maps\submaps\_readme.dm" #include "maps\submaps\space_submaps\space.dm" #include "maps\submaps\surface_submaps\mountains\mountains.dm"