diff --git a/code/__defines/_planes+layers.dm b/code/__defines/_planes+layers.dm index c54fbe93cc..01d890b152 100644 --- a/code/__defines/_planes+layers.dm +++ b/code/__defines/_planes+layers.dm @@ -130,6 +130,8 @@ What is the naming convention for planes or layers? #define PLANE_ADMIN2 33 //Purely for shenanigans (above lighting) +#define PLANE_BUILDMODE 39 //Things that only show up when you have buildmode on + //Fullscreen overlays under inventory #define PLANE_FULLSCREEN 90 //Blindness, mesons, druggy, etc #define OBFUSCATION_LAYER 5 //Where images covering the view for eyes are put diff --git a/code/__defines/mobs.dm b/code/__defines/mobs.dm index 5278b8c526..cfbcfcd1dd 100644 --- a/code/__defines/mobs.dm +++ b/code/__defines/mobs.dm @@ -394,7 +394,9 @@ #define VIS_OBJS 20 #define VIS_MOBS 21 -#define VIS_COUNT 21 //Must be highest number from above. +#define VIS_BUILDMODE 22 + +#define VIS_COUNT 22 //Must be highest number from above. //Some mob icon layering defines #define BODY_LAYER -100 diff --git a/code/global.dm b/code/global.dm index e9bcbb49f4..111a56c116 100644 --- a/code/global.dm +++ b/code/global.dm @@ -178,6 +178,7 @@ var/list/station_departments = list("Command", "Medical", "Engineering", "Scienc //Icons for in-game HUD glasses. Why don't we just share these a little bit? var/static/icon/ingame_hud = icon('icons/mob/hud.dmi') var/static/icon/ingame_hud_med = icon('icons/mob/hud_med.dmi') +var/static/icon/buildmode_hud = icon('icons/misc/buildmode.dmi') //Keyed list for caching icons so you don't need to make them for records, IDs, etc all separately. //Could be useful for AI impersonation or something at some point? diff --git a/code/modules/admin/verbs/buildmode.dm b/code/modules/admin/verbs/buildmode.dm index 26575fd41a..a5e9c15029 100644 --- a/code/modules/admin/verbs/buildmode.dm +++ b/code/modules/admin/verbs/buildmode.dm @@ -1,3 +1,15 @@ +#define BUILDMODE_BASIC 1 +#define BUILDMODE_ADVANCED 2 +#define BUILDMODE_EDIT 3 +#define BUILDMODE_THROW 4 +#define BUILDMODE_ROOM 5 +#define BUILDMODE_LADDER 6 +#define BUILDMODE_CONTENTS 7 +#define BUILDMODE_LIGHTS 8 +#define BUILDMODE_AI 9 + +#define LAST_BUILDMODE 9 + /proc/togglebuildmode(mob/M as mob in player_list) set name = "Toggle Build Mode" set category = "Special Verbs" @@ -6,6 +18,7 @@ log_admin("[key_name(usr)] has left build mode.") M.client.buildmode = 0 M.client.show_popup_menus = 1 + M.plane_holder.set_vis(VIS_BUILDMODE, FALSE) for(var/obj/effect/bmode/buildholder/H) if(H.cl == M.client) qdel(H) @@ -13,6 +26,7 @@ log_admin("[key_name(usr)] has entered build mode.") M.client.buildmode = 1 M.client.show_popup_menus = 0 + M.plane_holder.set_vis(VIS_BUILDMODE, TRUE) var/obj/effect/bmode/buildholder/H = new/obj/effect/bmode/buildholder() var/obj/effect/bmode/builddir/A = new/obj/effect/bmode/builddir(H) @@ -72,7 +86,8 @@ screen_loc = "NORTH,WEST+1" Click() switch(master.cl.buildmode) - if(1) // Basic Build + + if(BUILDMODE_BASIC) to_chat(usr, "***********************************************************") to_chat(usr, "Left Mouse Button = Construct / Upgrade") to_chat(usr, "Right Mouse Button = Deconstruct / Delete / Downgrade") @@ -82,7 +97,8 @@ to_chat(usr, "Use the button in the upper left corner to") to_chat(usr, "change the direction of built objects.") to_chat(usr, "***********************************************************") - if(2) // Adv. Build + + if(BUILDMODE_ADVANCED) to_chat(usr, "***********************************************************") to_chat(usr, "Right Mouse Button on buildmode button = Set object type") to_chat(usr, "Middle Mouse Button on buildmode button= On/Off object type saying") @@ -94,44 +110,56 @@ to_chat(usr, "Use the button in the upper left corner to") to_chat(usr, "change the direction of built objects.") to_chat(usr, "***********************************************************") - if(3) // Edit + + if(BUILDMODE_EDIT) to_chat(usr, "***********************************************************") to_chat(usr, "Right Mouse Button on buildmode button = Select var(type) & value") to_chat(usr, "Left Mouse Button on turf/obj/mob = Set var(type) & value") to_chat(usr, "Right Mouse Button on turf/obj/mob = Reset var's value") to_chat(usr, "***********************************************************") - if(4) // Throw + + if(BUILDMODE_THROW) to_chat(usr, "***********************************************************") to_chat(usr, "Left Mouse Button on turf/obj/mob = Select") to_chat(usr, "Right Mouse Button on turf/obj/mob = Throw") to_chat(usr, "***********************************************************") - if(5) // Room Build + + if(BUILDMODE_ROOM) to_chat(usr, "***********************************************************") to_chat(usr, "Left Mouse Button on turf = Select as point A") to_chat(usr, "Right Mouse Button on turf = Select as point B") to_chat(usr, "Right Mouse Button on buildmode button = Change floor/wall type") to_chat(usr, "***********************************************************") - if(6) // Make Ladders + + if(BUILDMODE_LADDER) to_chat(usr, "***********************************************************") to_chat(usr, "Left Mouse Button on turf = Set as upper ladder loc") to_chat(usr, "Right Mouse Button on turf = Set as lower ladder loc") to_chat(usr, "***********************************************************") - if(7) // Move Into Contents + + if(BUILDMODE_CONTENTS) to_chat(usr, "***********************************************************") to_chat(usr, "Left Mouse Button on turf/obj/mob = Select") to_chat(usr, "Right Mouse Button on turf/obj/mob = Move into selection") to_chat(usr, "***********************************************************") - if(8) // Make Lights + + if(BUILDMODE_LIGHTS) to_chat(usr, "***********************************************************") to_chat(usr, "Left Mouse Button on turf/obj/mob = Make it glow") to_chat(usr, "Right Mouse Button on turf/obj/mob = Reset glowing") to_chat(usr, "Right Mouse Button on buildmode button = Change glow properties") to_chat(usr, "***********************************************************") - if(9) // Control mobs with ai_holders. + + if(BUILDMODE_AI) to_chat(usr, "***********************************************************") + to_chat(usr, "Left Mouse Button drag box = Select only mobs in box") + to_chat(usr, "Left Mouse Button drag box + shift = Select additional mobs in area") + to_chat(usr, "Left Mouse Button on non-mob = Deselect all mobs") to_chat(usr, "Left Mouse Button on AI mob = Select/Deselect mob") to_chat(usr, "Left Mouse Button + alt on AI mob = Toggle hostility on mob") - to_chat(usr, "Left Mouse Button + ctrl on AI mob = Reset target/following/movement") + to_chat(usr, "Left Mouse Button + shift on AI mob = Toggle AI (also resets)") + to_chat(usr, "Left Mouse Button + ctrl on AI mob = Copy mob faction") + to_chat(usr, "Right Mouse Button + ctrl on any mob = Paste mob faction copied with Left Mouse Button + shift") to_chat(usr, "Right Mouse Button on enemy mob = Command selected mobs to attack mob") to_chat(usr, "Right Mouse Button on allied mob = Command selected mobs to follow mob") to_chat(usr, "Right Mouse Button + shift on any mob = Command selected mobs to follow mob regardless of faction") @@ -159,6 +187,7 @@ var/obj/effect/bmode/buildquit/buildquit = null var/atom/movable/throw_atom = null var/list/selected_mobs = list() + var/copied_faction = null /obj/effect/bmode/buildholder/Destroy() qdel(builddir) @@ -184,7 +213,6 @@ selected_mobs -= unit C.images -= unit.selected_image - /obj/effect/bmode/buildmode icon_state = "buildmode1" screen_loc = "NORTH,WEST+2" @@ -207,48 +235,25 @@ if(pa.Find("middle")) switch(master.cl.buildmode) - if(2) + if(BUILDMODE_ADVANCED) objsay=!objsay - if(pa.Find("left")) - switch(master.cl.buildmode) - if(1) - master.cl.buildmode = 2 - src.icon_state = "buildmode2" - if(2) - master.cl.buildmode = 3 - src.icon_state = "buildmode3" - if(3) - master.cl.buildmode = 4 - src.icon_state = "buildmode4" - if(4) - master.cl.buildmode = 5 - src.icon_state = "buildmode5" - if(5) - master.cl.buildmode = 6 - src.icon_state = "buildmode6" - if(6) - master.cl.buildmode = 7 - src.icon_state = "buildmode7" - if(7) - master.cl.buildmode = 8 - src.icon_state = "buildmode8" - if(8) - master.cl.buildmode = 9 - src.icon_state = "buildmode9" - if(9) - master.cl.buildmode = 1 - src.icon_state = "buildmode1" + if(master.cl.buildmode == LAST_BUILDMODE) + master.cl.buildmode = 1 + else + master.cl.buildmode++ + src.icon_state = "buildmode[master.cl.buildmode]" else if(pa.Find("right")) switch(master.cl.buildmode) - if(1) // Basic Build + if(BUILDMODE_BASIC) + return 1 - if(2) // Adv. Build + if(BUILDMODE_ADVANCED) objholder = get_path_from_partial_text(/obj/structure/closet) - if(3) // Edit + if(BUILDMODE_EDIT) var/list/locked = list("vars", "key", "ckey", "client", "firemut", "ishulk", "telekinesis", "xray", "virus", "viruses", "cuffed", "ka", "last_eaten", "urine") master.buildmode.varholder = input(usr,"Enter variable name:" ,"Name", "name") @@ -267,14 +272,16 @@ master.buildmode.valueholder = input(usr,"Enter variable value:" ,"Value") as obj in world if("turf-reference") master.buildmode.valueholder = input(usr,"Enter variable value:" ,"Value") as turf in world - if(5) // Room build + + if(BUILDMODE_ROOM) var/choice = alert("Would you like to change the floor or wall holders?","Room Builder", "Floor", "Wall") switch(choice) if("Floor") floor_holder = get_path_from_partial_text(/turf/simulated/floor/plating) if("Wall") wall_holder = get_path_from_partial_text(/turf/simulated/wall) - if(8) // Lights + + if(BUILDMODE_LIGHTS) var/choice = alert("Change the new light range, power, or color?", "Light Maker", "Range", "Power", "Color") switch(choice) if("Range") @@ -301,7 +308,7 @@ var/list/pa = params2list(params) switch(buildmode) - if(1) // Basic Build + if(BUILDMODE_BASIC) if(istype(object,/turf) && pa.Find("left") && !pa.Find("alt") && !pa.Find("ctrl") ) if(istype(object,/turf/space)) var/turf/T = object @@ -350,7 +357,8 @@ if(NORTHWEST) var/obj/structure/window/reinforced/WIN = new/obj/structure/window/reinforced(get_turf(object)) WIN.set_dir(NORTHWEST) - if(2) // Adv. Build + + if(BUILDMODE_ADVANCED) if(pa.Find("left") && !pa.Find("ctrl")) if(ispath(holder.buildmode.objholder,/turf)) var/turf/T = get_turf(object) @@ -369,8 +377,7 @@ if(holder.buildmode.objsay) to_chat(usr, "[object.type]") - - if(3) // Edit + if(BUILDMODE_EDIT) if(pa.Find("left")) //I cant believe this shit actually compiles. if(object.vars.Find(holder.buildmode.varholder)) log_admin("[key_name(usr)] modified [object.name]'s [holder.buildmode.varholder] to [holder.buildmode.valueholder]") @@ -384,7 +391,7 @@ else to_chat(user, "[initial(object.name)] does not have a var called '[holder.buildmode.varholder]'") - if(4) // Throw + if(BUILDMODE_THROW) if(pa.Find("left")) if(istype(object, /atom/movable)) holder.throw_atom = object @@ -392,7 +399,8 @@ if(holder.throw_atom) holder.throw_atom.throw_at(object, 10, 1) log_admin("[key_name(usr)] threw [holder.throw_atom] at [object]") - if(5) // Room build + + if(BUILDMODE_ROOM) if(pa.Find("left")) holder.buildmode.coordA = get_turf(object) to_chat(user, "Defined [object] ([object.type]) as point A.") @@ -411,7 +419,8 @@ ) holder.buildmode.coordA = null holder.buildmode.coordB = null - if(6) // Ladders + + if(BUILDMODE_LADDER) if(pa.Find("left")) holder.buildmode.coordA = get_turf(object) to_chat(user, "Defined [object] ([object.type]) as upper ladder location.") @@ -430,7 +439,8 @@ B.update_icon() holder.buildmode.coordA = null holder.buildmode.coordB = null - if(7) // Move into contents + + if(BUILDMODE_CONTENTS) if(pa.Find("left")) if(istype(object, /atom)) holder.throw_atom = object @@ -438,23 +448,32 @@ if(holder.throw_atom && istype(object, /atom/movable)) object.forceMove(holder.throw_atom) log_admin("[key_name(usr)] moved [object] into [holder.throw_atom].") - if(8) // Lights + + if(BUILDMODE_LIGHTS) if(pa.Find("left")) if(object) object.set_light(holder.buildmode.new_light_range, holder.buildmode.new_light_intensity, holder.buildmode.new_light_color) if(pa.Find("right")) if(object) object.set_light(0, 0, "#FFFFFF") - if(9) // AI control + + if(BUILDMODE_AI) if(pa.Find("left")) if(isliving(object)) var/mob/living/L = object - // Reset processes. - if(pa.Find("ctrl")) - if(!isnull(L.get_AI_stance())) // Null means there's no AI datum or it has one but is player controlled w/o autopilot on. + + // Pause/unpause AI + if(pa.Find("shift")) + var/stance = L.get_AI_stance() + if(!isnull(stance)) // Null means there's no AI datum or it has one but is player controlled w/o autopilot on. var/datum/ai_holder/AI = L.ai_holder - AI.forget_everything() - to_chat(user, span("notice", "\The [L]'s AI has forgotten its target/movement destination/leader.")) + if(stance == STANCE_SLEEP) + AI.go_wake() + to_chat(user, span("notice", "\The [L]'s AI has been enabled.")) + else + AI.go_sleep() + to_chat(user, span("notice", "\The [L]'s AI has been disabled.")) + return else to_chat(user, span("warning", "\The [L] is not AI controlled.")) return @@ -469,6 +488,12 @@ to_chat(user, span("warning", "\The [L] is not AI controlled.")) return + // Copy faction + if(pa.Find("ctrl")) + holder.copied_faction = L.faction + to_chat(user, span("notice", "Copied faction '[holder.copied_faction]'.")) + return + // Select/Deselect if(!isnull(L.get_AI_stance())) if(L in holder.selected_mobs) @@ -477,10 +502,27 @@ else holder.select_AI_mob(user.client, L) to_chat(user, span("notice", "Selected \the [L].")) + return else to_chat(user, span("warning", "\The [L] is not AI controlled.")) + return + else //Not living + for(var/mob/living/unit in holder.selected_mobs) + holder.deselect_AI_mob(user.client, unit) + if(pa.Find("right")) + // Paste faction + if(pa.Find("ctrl") && isliving(object)) + if(!holder.copied_faction) + to_chat(user, span("warning", "LMB+Shift a mob to copy their faction before pasting.")) + return + else + var/mob/living/L = object + L.faction = holder.copied_faction + to_chat(user, span("notice", "Pasted faction '[holder.copied_faction]'.")) + return + if(istype(object, /atom)) // Force attack. var/atom/A = object @@ -491,6 +533,9 @@ AI.give_target(A) i++ to_chat(user, span("notice", "Commanded [i] mob\s to attack \the [A].")) + var/image/orderimage = image(buildmode_hud,A,"ai_targetorder") + orderimage.plane = PLANE_BUILDMODE + flick_overlay(orderimage, list(user.client), 8, TRUE) return if(isliving(object)) // Follow or attack. @@ -515,16 +560,67 @@ if(j) message += "[j] mob\s to follow \the [L]." to_chat(user, span("notice", message)) + var/image/orderimage = image(buildmode_hud,L,"ai_targetorder") + orderimage.plane = PLANE_BUILDMODE + flick_overlay(orderimage, list(user.client), 8, TRUE) + return if(isturf(object)) // Move or reposition. var/turf/T = object - var/i = 0 + var/forced = 0 + var/told = 0 for(var/mob/living/unit in holder.selected_mobs) var/datum/ai_holder/AI = unit.ai_holder - AI.give_destination(T, 1, pa.Find("shift")) // If shift is held, the mobs will not stop moving to attack a visible enemy. - i++ - to_chat(user, span("notice", "Commanded [i] mob\s to move to \the [T].")) + if(unit.get_AI_stance() == STANCE_SLEEP) + unit.forceMove(T) + forced++ + else + AI.give_destination(T, 1, pa.Find("shift")) // If shift is held, the mobs will not stop moving to attack a visible enemy. + told++ + to_chat(user, span("notice", "Commanded [told] mob\s to move to \the [T], and manually placed [forced] of them.")) + var/image/orderimage = image(buildmode_hud,T,"ai_turforder") + orderimage.plane = PLANE_BUILDMODE + flick_overlay(orderimage, list(user.client), 8, TRUE) + return +/proc/build_drag(var/client/user, buildmode, var/atom/fromatom, var/atom/toatom, var/atom/fromloc, var/atom/toloc, var/fromcontrol, var/tocontrol, params) + var/obj/effect/bmode/buildholder/holder = null + for(var/obj/effect/bmode/buildholder/H) + if(H.cl == user) + holder = H + break + if(!holder) return + var/list/pa = params2list(params) + + switch(buildmode) + if(BUILDMODE_AI) + + //Holding shift prevents the deselection of existing + if(!pa.Find("shift")) + for(var/mob/living/unit in holder.selected_mobs) + holder.deselect_AI_mob(user, unit) + + var/turf/c1 = get_turf(fromatom) + var/turf/c2 = get_turf(toatom) + if(!c1 || !c2) + return //Dragged outside window or something + + var/low_x = min(c1.x,c2.x) + var/low_y = min(c1.y,c2.y) + var/hi_x = max(c1.x,c2.x) + var/hi_y = max(c1.y,c2.y) + var/z = c1.z //Eh + + var/i = 0 + for(var/mob/living/L in living_mob_list) + if(L.z != z || L.client) + continue + if(L.x >= low_x && L.x <= hi_x && L.y >= low_y && L.y <= hi_y) + holder.select_AI_mob(user, L) + i++ + + to_chat(user, span("notice", "Band-selected [i] mobs.")) + return /obj/effect/bmode/buildmode/proc/get_path_from_partial_text(default_path) var/desired_path = input("Enter full or partial typepath.","Typepath","[default_path]") @@ -596,4 +692,15 @@ if(isturf(floor_type)) T.ChangeTurf(floor_type) else - new floor_type(T) \ No newline at end of file + new floor_type(T) + +#undef BUILDMODE_BASIC +#undef BUILDMODE_ADVANCED +#undef BUILDMODE_EDIT +#undef BUILDMODE_THROW +#undef BUILDMODE_ROOM +#undef BUILDMODE_LADDER +#undef BUILDMODE_CONTENTS +#undef BUILDMODE_LIGHTS +#undef BUILDMODE_AI +#undef LAST_BUILDMODE \ No newline at end of file diff --git a/code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm b/code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm index 45a3067a1b..a53906a7a3 100644 --- a/code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm +++ b/code/modules/ai/aI_holder_subtypes/simple_mob_ai.dm @@ -150,3 +150,40 @@ // Simple mobs that retaliate and support others in their faction who get attacked. /datum/ai_holder/simple_mob/retaliate/cooperative cooperative = TRUE + +// With all the bells and whistles +/datum/ai_holder/simple_mob/humanoid + intelligence_level = AI_SMART //Purportedly + retaliate = TRUE //If attacked, attack back + threaten = TRUE //Verbal threats + firing_lanes = TRUE //Avoid shooting allies + conserve_ammo = TRUE //Don't shoot when it can't hit target + can_breakthrough = TRUE //Can break through doors + violent_breakthrough = FALSE //Won't try to break through walls (humans can, but usually don't) + speak_chance = 2 //Babble chance + cooperative = TRUE //Assist each other + wander = TRUE //Wander around + returns_home = TRUE //But not too far + use_astar = TRUE //Path smartly + home_low_priority = TRUE //Following/helping is more important + +// The hostile subtype is implied to be trained combatants who use ""tactics"" +/datum/ai_holder/simple_mob/humanoid/hostile + var/run_if_this_close = 4 // If anything gets within this range, it'll try to move away. + hostile = TRUE //Attack! + +// Juke +/datum/ai_holder/simple_mob/humanoid/hostile/post_melee_attack(atom/A) + holder.IMove(get_step(holder, pick(alldirs))) + holder.face_atom(A) + +/datum/ai_holder/simple_mob/humanoid/hostile/post_ranged_attack(atom/A) + //Pick a random turf to step into + var/turf/T = get_step(holder, pick(alldirs)) + if(check_trajectory(A, T)) // Can we even hit them from there? + holder.IMove(T) + holder.face_atom(A) + + if(get_dist(holder, A) < run_if_this_close) + holder.IMove(get_step_away(holder, A)) + holder.face_atom(A) diff --git a/code/modules/ai/aI_holder_subtypes/slime_xenobio_ai.dm b/code/modules/ai/aI_holder_subtypes/slime_xenobio_ai.dm index 16609cc961..540a9e5397 100644 --- a/code/modules/ai/aI_holder_subtypes/slime_xenobio_ai.dm +++ b/code/modules/ai/aI_holder_subtypes/slime_xenobio_ai.dm @@ -77,7 +77,7 @@ if(rabid) return var/justified = my_slime.is_justified_to_discipline() // This will also consider the AI-side of that proc. - lost_target() // Stop attacking. + remove_target() // Stop attacking. if(justified) obedience++ @@ -137,7 +137,7 @@ // Called when using a pacification agent (or it's Kendrick being initalized). /datum/ai_holder/simple_mob/xenobio_slime/proc/pacify() - lost_target() // So it stops trying to kill them. + remove_target() // So it stops trying to kill them. rabid = FALSE hostile = FALSE retaliate = FALSE @@ -247,7 +247,7 @@ else delayed_say("Fine...", speaker) adjust_discipline(1, TRUE) // This must come before losing the target or it will be unjustified. - lost_target() + remove_target() if(leader) // We're being asked to stop following someone. diff --git a/code/modules/ai/ai_holder.dm b/code/modules/ai/ai_holder.dm index 4ef5507d5b..e22abf8ee0 100644 --- a/code/modules/ai/ai_holder.dm +++ b/code/modules/ai/ai_holder.dm @@ -46,8 +46,25 @@ home_turf = null return ..() +/datum/ai_holder/proc/update_stance_hud() + var/image/stanceimage = holder.grab_hud(LIFE_HUD) + stanceimage.icon_state = "ais_[stance]" + holder.apply_hud(LIFE_HUD, stanceimage) + +/datum/ai_holder/proc/update_paused_hud() + var/image/sleepingimage = holder.grab_hud(STATUS_HUD) + var/asleep = 0 + if(busy) + asleep = 2 + else if (stance == STANCE_SLEEP) + asleep = 1 + sleepingimage.icon_state = "ai_[asleep]" + holder.apply_hud(STATUS_HUD, sleepingimage) // Now for the actual AI stuff. +/datum/ai_holder/proc/set_busy(var/value = 0) + busy = value + update_paused_hud() // Makes this ai holder not get processed. // Called automatically when the host mob is killed. @@ -58,6 +75,7 @@ forget_everything() // If we ever wake up, its really unlikely that our current memory will be of use. set_stance(STANCE_SLEEP) SSai.processing -= src + update_paused_hud() // Reverses the above proc. // Revived mobs will wake their AI if they have one. @@ -68,6 +86,7 @@ return set_stance(STANCE_IDLE) SSai.processing += src + update_paused_hud() /datum/ai_holder/proc/should_wake() if(holder.client && !autopilot) @@ -80,9 +99,7 @@ /datum/ai_holder/proc/forget_everything() // Some of these might be redundant, but hopefully this prevents future bugs if that changes. lose_follow() - lose_target() - lose_target_position() - give_up_movement() + remove_target() // '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() @@ -103,39 +120,13 @@ /datum/ai_holder/proc/handle_special_strategical() -/* - //AI Actions - if(!ai_inactive) - //Stanceyness - handle_stance() - - //Movement - if(!stop_automated_movement && wander && !anchored) //Allowed to move? - handle_wander_movement() - - //Speaking - if(speak_chance && stance == STANCE_IDLE) // Allowed to chatter? - handle_idle_speaking() - - //Resisting out buckles - if(stance != STANCE_IDLE && incapacitated(INCAPACITATION_BUCKLED_PARTIALLY)) - handle_resist() - - //Resisting out of closets - if(istype(loc,/obj/structure/closet)) - var/obj/structure/closet/C = loc - if(C.welded) - resist() - else - C.open() -*/ - // For setting the stance WITHOUT processing it /datum/ai_holder/proc/set_stance(var/new_stance) ai_log("set_stance() : Setting stance from [stance] to [new_stance].", AI_LOG_INFO) stance = new_stance if(stance_coloring) // For debugging or really weird mobs. stance_color() + update_stance_hud() // This is called every half a second. /datum/ai_holder/proc/handle_stance_tactical() @@ -207,7 +198,8 @@ if(STANCE_REPOSITION) // This is the same as above but doesn't stop if an enemy is visible since its an 'in-combat' move order. ai_log("handle_stance_tactical() : STANCE_REPOSITION, going to walk_to_destination().", AI_LOG_TRACE) - walk_to_destination() + if(!target && !find_target()) + walk_to_destination() if(STANCE_FOLLOW) ai_log("handle_stance_tactical() : STANCE_FOLLOW, going to walk_to_leader().", AI_LOG_TRACE) @@ -233,12 +225,18 @@ ai_log("++++++++++ Slow Process Beginning ++++++++++", AI_LOG_TRACE) ai_log("handle_stance_strategical() : Called.", AI_LOG_TRACE) + ai_log("handle_stance_strategical() : LTT=[lose_target_time]", AI_LOG_TRACE) + if(lose_target_time && (lose_target_time + lose_target_timeout < world.time)) // We were tracking an enemy but they are gone. + ai_log("handle_stance_strategical() : Giving up a chase.", AI_LOG_DEBUG) + remove_target() + + if(stance in STANCES_COMBAT) + request_help() // Call our allies. + switch(stance) if(STANCE_IDLE) - if(speak_chance) // In the long loop since otherwise it wont shut up. handle_idle_speaking() - if(hostile) ai_log("handle_stance_strategical() : STANCE_IDLE, going to find_target().", AI_LOG_TRACE) find_target() @@ -246,6 +244,7 @@ if(target) ai_log("handle_stance_strategical() : STANCE_APPROACH, going to calculate_path([target]).", AI_LOG_TRACE) calculate_path(target) + walk_to_target() if(STANCE_MOVE) if(hostile && find_target()) // This will switch its stance. ai_log("handle_stance_strategical() : STANCE_MOVE, found target and was inturrupted.", AI_LOG_TRACE) @@ -255,6 +254,7 @@ else if(leader) ai_log("handle_stance_strategical() : STANCE_FOLLOW, going to calculate_path([leader]).", AI_LOG_TRACE) calculate_path(leader) + walk_to_leader() ai_log("handle_stance_strategical() : Exiting.", AI_LOG_TRACE) ai_log("++++++++++ Slow Process Ending ++++++++++", AI_LOG_TRACE) @@ -263,7 +263,7 @@ // 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 + ai_holder.set_busy(value) /mob/living/proc/is_AI_busy() if(!ai_holder) diff --git a/code/modules/ai/ai_holder_combat.dm b/code/modules/ai/ai_holder_combat.dm index 63154e4fe4..65fd1785b1 100644 --- a/code/modules/ai/ai_holder_combat.dm +++ b/code/modules/ai/ai_holder_combat.dm @@ -9,47 +9,23 @@ var/violent_breakthrough = TRUE // If false, the AI is not allowed to destroy things like windows or other structures in the way. Requires above var to be true. var/stand_ground = FALSE // If true, the AI won't try to get closer to an enemy if out of range. - - + // This does the actual attacking. /datum/ai_holder/proc/engage_target() ai_log("engage_target() : Entering.", AI_LOG_DEBUG) // Can we still see them? -// if(!target || !can_attack(target) || (!(target in list_targets())) ) if(!target || !can_attack(target)) ai_log("engage_target() : Lost sight of target.", AI_LOG_TRACE) - lose_target() // We lost them. - - if(!find_target()) // If we can't get a new one, then wait for a bit and then time out. - set_stance(STANCE_IDLE) - lost_target() - ai_log("engage_target() : No more targets. Exiting.", AI_LOG_DEBUG) + if(lose_target()) // We lost them (returns TRUE if we found something else to do) + ai_log("engage_target() : Pursuing other options (last seen, or a new target).", AI_LOG_TRACE) return - // if(lose_target_time + lose_target_timeout < world.time) - // ai_log("engage_target() : Unseen enemy timed out.", AI_LOG_TRACE) - // set_stance(STANCE_IDLE) // It must've been the wind. - // lost_target() - // ai_log("engage_target() : Exiting.", AI_LOG_DEBUG) - // return - - // // But maybe we do one last ditch effort. - // if(!target_last_seen_turf || intelligence_level < AI_SMART) - // ai_log("engage_target() : No last known position or is too dumb to fight unseen enemies.", AI_LOG_TRACE) - // set_stance(STANCE_IDLE) - // else - // ai_log("engage_target() : Fighting unseen enemy.", AI_LOG_TRACE) - // engage_unseen_enemy() - else - ai_log("engage_target() : Got new target ([target]).", AI_LOG_TRACE) var/distance = get_dist(holder, target) ai_log("engage_target() : Distance to target ([target]) is [distance].", AI_LOG_TRACE) holder.face_atom(target) last_conflict_time = world.time - request_help() // Call our allies. - // Do a 'special' attack, if one is allowed. // if(prob(special_attack_prob) && (distance >= special_attack_min_range) && (distance <= special_attack_max_range)) if(holder.ICheckSpecialAttack(target)) @@ -198,12 +174,8 @@ // Make sure we can still chase/attack them. if(!target || !can_attack(target)) ai_log("walk_to_target() : Lost target.", AI_LOG_INFO) - if(!find_target()) - lost_target() - ai_log("walk_to_target() : Exiting.", AI_LOG_DEBUG) - return - else - ai_log("walk_to_target() : Found new target ([target]).", AI_LOG_INFO) + lose_target() + return // Find out where we're going. var/get_to = closest_distance(target) @@ -220,7 +192,6 @@ ai_log("walk_to_target() : Exiting.", AI_LOG_DEBUG) return - // Otherwise keep walking. if(!stand_ground) walk_path(target, get_to) diff --git a/code/modules/ai/ai_holder_combat_unseen.dm b/code/modules/ai/ai_holder_combat_unseen.dm index 0cb518f08c..c854377c89 100644 --- a/code/modules/ai/ai_holder_combat_unseen.dm +++ b/code/modules/ai/ai_holder_combat_unseen.dm @@ -2,21 +2,21 @@ // Used when a target is out of sight or invisible. /datum/ai_holder/proc/engage_unseen_enemy() + ai_log("engage_unseen_enemy() : Entering.", AI_LOG_TRACE) // Lets do some last things before giving up. - if(!ranged) - if(get_dist(holder, target_last_seen_turf > 1)) // We last saw them over there. + if(conserve_ammo || !holder.ICheckRangedAttack(target_last_seen_turf)) + if(get_dist(holder, target_last_seen_turf) > 1) // We last saw them over there. // Go to where you last saw the enemy. - give_destination(target_last_seen_turf, 1, TRUE) // This will set it to STANCE_REPOSITION. - else // We last saw them next to us, so do a blind attack on that tile. + give_destination(target_last_seen_turf, 1, TRUE) // Sets stance as well + else if(lose_target_time == world.time) // We last saw them next to us, so do a blind attack on that tile. melee_on_tile(target_last_seen_turf) - - else if(!conserve_ammo) + else + find_target() + else shoot_near_turf(target_last_seen_turf) // This shoots semi-randomly near a specific turf. /datum/ai_holder/proc/shoot_near_turf(turf/targeted_turf) - if(!ranged) - return // Can't shoot. if(get_dist(holder, targeted_turf) > max_range(targeted_turf)) return // Too far to shoot. @@ -32,6 +32,7 @@ // Attempts to attack something on a specific tile. // TODO: Put on mob/living? /datum/ai_holder/proc/melee_on_tile(turf/T) + ai_log("melee_on_tile() : Entering.", AI_LOG_TRACE) var/mob/living/L = locate() in T if(!L) T.visible_message("\The [holder] attacks nothing around \the [T].") diff --git a/code/modules/ai/ai_holder_communication.dm b/code/modules/ai/ai_holder_communication.dm index 4cfec6f7af..ef8fcf253d 100644 --- a/code/modules/ai/ai_holder_communication.dm +++ b/code/modules/ai/ai_holder_communication.dm @@ -15,7 +15,7 @@ /datum/ai_holder/proc/should_threaten() if(!threaten) return FALSE // We don't negotiate. - if(target in attackers) + if(check_attacker(target)) return FALSE // They (or someone like them) attacked us before, escalate immediately. if(!will_threaten(target)) return FALSE // Pointless to threaten an animal, a mindless drone, or an object. diff --git a/code/modules/ai/ai_holder_cooperation.dm b/code/modules/ai/ai_holder_cooperation.dm index 0f6b0bcfa2..dd6228f460 100644 --- a/code/modules/ai/ai_holder_cooperation.dm +++ b/code/modules/ai/ai_holder_cooperation.dm @@ -74,7 +74,7 @@ ai_log("request_help() : Exiting.", AI_LOG_DEBUG) -// What allies receive when someone else is calling for help. +// What allies receive when someone else is calling for help.1 /datum/ai_holder/proc/help_requested(mob/living/friend) ai_log("help_requested() : Entering.", AI_LOG_DEBUG) if(stance == STANCE_SLEEP) @@ -92,24 +92,26 @@ if(!holder.IIsAlly(friend)) // Extra sanity. ai_log("help_requested() : Help requested by [friend] but we hate them.", AI_LOG_INFO) return - if(friend.ai_holder && friend.ai_holder.target && !can_attack(friend.ai_holder.target)) - ai_log("help_requested() : Help requested by [friend] but we don't want to fight their target.", AI_LOG_INFO) - return - if(get_dist(holder, friend) <= follow_distance) - ai_log("help_requested() : Help requested by [friend] but we're already here.", AI_LOG_INFO) - return - if(get_dist(holder, friend) <= vision_range) // Within our sight. - ai_log("help_requested() : Help requested by [friend], and within target sharing range.", AI_LOG_INFO) - if(friend.ai_holder) // AI calling for help. - if(friend.ai_holder.target && can_attack(friend.ai_holder.target)) // Friend wants us to attack their target. - last_conflict_time = world.time // So we attack immediately and not threaten. - give_target(friend.ai_holder.target) // This will set us to the appropiate stance. - ai_log("help_requested() : Given target [target] by [friend]. Exiting", AI_LOG_DEBUG) - return + var/their_target = friend?.ai_holder?.target + if(their_target) // They have a target and aren't just shouting for no reason + if(!can_attack(their_target, vision_required = FALSE)) + ai_log("help_requested() : Help requested by [friend] but we don't want to fight their target.", AI_LOG_INFO) + return + if(get_dist(holder, friend) <= follow_distance) + ai_log("help_requested() : Help requested by [friend] but we're already here.", AI_LOG_INFO) + return + if(get_dist(holder, friend) <= vision_range) // Within our sight. + ai_log("help_requested() : Help requested by [friend], and within target sharing range.", AI_LOG_INFO) + last_conflict_time = world.time // So we attack immediately and not threaten. + give_target(their_target, urgent = TRUE) // This will set us to the appropiate stance. + ai_log("help_requested() : Given target [target] by [friend]. Exiting", AI_LOG_DEBUG) + return // Otherwise they're outside our sight, lack a target, or aren't AI controlled, but within call range. // So assuming we're AI controlled, we'll go to them and see whats wrong. ai_log("help_requested() : Help requested by [friend], going to go to friend.", AI_LOG_INFO) + if(their_target) + add_attacker(their_target) // We won't wait and 'warn' them while they're stabbing our ally set_follow(friend, 10 SECONDS) ai_log("help_requested() : Exiting.", AI_LOG_DEBUG) diff --git a/code/modules/ai/ai_holder_targeting.dm b/code/modules/ai/ai_holder_targeting.dm index e70e991d6e..40f2c5752f 100644 --- a/code/modules/ai/ai_holder_targeting.dm +++ b/code/modules/ai/ai_holder_targeting.dm @@ -26,6 +26,7 @@ // Step 1, find out what we can see. /datum/ai_holder/proc/list_targets() . = hearers(vision_range, holder) - holder // Remove ourselves to prevent suicidal decisions. ~ SRC is the ai_holder. + . -= dview_mob // Not the dview mob either, nerd. var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha)) @@ -35,6 +36,7 @@ // 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) + ai_log("find_target() : Entered.", AI_LOG_TRACE) if(!hostile) // So retaliating mobs only attack the thing that hit it. return null . = list() @@ -70,13 +72,17 @@ return chosen_target // Step 4, give us our selected target. -/datum/ai_holder/proc/give_target(new_target) +/datum/ai_holder/proc/give_target(new_target, urgent = FALSE) + ai_log("give_target() : Given '[new_target]', urgent=[urgent].", AI_LOG_TRACE) target = new_target + if(target != null) - if(should_threaten()) + lose_target_time = 0 + track_target_position() + if(should_threaten() && !urgent) set_stance(STANCE_ALERT) else - set_stance(STANCE_APPROACH) + set_stance(STANCE_FIGHT) last_target_time = world.time return TRUE @@ -109,8 +115,8 @@ sorted_targets += A return sorted_targets -/datum/ai_holder/proc/can_attack(atom/movable/the_target) - if(!can_see_target(the_target)) +/datum/ai_holder/proc/can_attack(atom/movable/the_target, vision_required = TRUE) + if(!can_see_target(the_target) && vision_required) return FALSE if(istype(the_target, /mob/zshadow)) @@ -157,21 +163,38 @@ /datum/ai_holder/proc/found(atom/movable/the_target) return FALSE -//We can't see the target, go look or attack where they were last seen. +// 'Soft' loss of target. They may still exist, we still have some info about them maybe. /datum/ai_holder/proc/lose_target() + ai_log("lose_target() : Entering.", AI_LOG_TRACE) if(target) + ai_log("lose_target() : Had a target, setting to null and LTT.", AI_LOG_DEBUG) target = null lose_target_time = world.time give_up_movement() + if(target_last_seen_turf && intelligence_level >= AI_SMART) + ai_log("lose_target() : Going into 'engage unseen enemy' mode.", AI_LOG_INFO) + engage_unseen_enemy() + return TRUE //We're still working on it + else + ai_log("lose_target() : Can't chase target, so giving up.", AI_LOG_INFO) + remove_target() + return find_target() //Returns if we found anything else to do -//Target is no longer valid (?) -/datum/ai_holder/proc/lost_target() - set_stance(STANCE_IDLE) + return FALSE //Nothing new to do + +// 'Hard' loss of target. Clean things up and return to idle. +/datum/ai_holder/proc/remove_target() + ai_log("remove_target() : Entering.", AI_LOG_TRACE) + if(target) + target = null + + lose_target_time = 0 + give_up_movement() lose_target_position() - lose_target() - + set_stance(STANCE_IDLE) + // Check if target is visible to us. /datum/ai_holder/proc/can_see_target(atom/movable/the_target, view_range = vision_range) ai_log("can_see_target() : Entering.", AI_LOG_TRACE) @@ -235,7 +258,7 @@ ai_log("react_to_attack() : Was attacked by [attacker], but we already have a target.", AI_LOG_TRACE) on_attacked(attacker) // So we attack immediately and not threaten. return FALSE - else if(attacker in attackers && world.time > last_target_time + 3 SECONDS) // Otherwise, let 'er rip + else if(check_attacker(attacker) && world.time > last_target_time + 3 SECONDS) // Otherwise, let 'er rip ai_log("react_to_attack() : Was attacked by [attacker]. Can retaliate, waited 3 seconds.", AI_LOG_INFO) on_attacked(attacker) // So we attack immediately and not threaten. return give_target(attacker) // Also handles setting the appropiate stance. @@ -246,16 +269,25 @@ ai_log("react_to_attack() : Was attacked by [attacker].", AI_LOG_INFO) on_attacked(attacker) // So we attack immediately and not threaten. - return give_target(attacker) // Also handles setting the appropiate stance. + return give_target(attacker, urgent = TRUE) // Also handles setting the appropiate stance. // Sets a few vars so mobs that threaten will react faster to an attacker or someone who attacked them before. /datum/ai_holder/proc/on_attacked(atom/movable/AM) - if(isliving(AM)) - var/mob/living/L = AM - if(!(L.name in attackers)) - attackers |= L.name - last_conflict_time = world.time + last_conflict_time = world.time + add_attacker(AM) +// Checks to see if an atom attacked us lately +/datum/ai_holder/proc/check_attacker(var/atom/movable/A) + return (A in attackers) + +// We were attacked by this thing recently +/datum/ai_holder/proc/add_attacker(var/atom/movable/A) + attackers |= A.name + +// Forgive this attacker +/datum/ai_holder/proc/remove_attacker(var/atom/movable/A) + attackers -= A.name + // Causes targeting to prefer targeting the taunter if possible. // This generally occurs if more than one option is within striking distance, including the taunter. // Otherwise the default filter will prefer the closest target. diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index c4331ab669..97c425f958 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -340,6 +340,14 @@ if(inactivity > duration) return inactivity return 0 +//Called when the client performs a drag-and-drop operation. +/client/MouseDrop(start_object,end_object,start_location,end_location,start_control,end_control,params) + if(buildmode && start_control == "mapwindow.map" && start_control == end_control) + build_drag(src,buildmode,start_object,end_object,start_location,end_location,start_control,end_control,params) + else + . = ..() + + // Byond seemingly calls stat, each tick. // Calling things each tick can get expensive real quick. // So we slow this down a little. diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 977f6eb88e..cc7ec62da4 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -16,7 +16,7 @@ dsma.blend_mode = BLEND_ADD dsoverlay.appearance = dsma - selected_image = image(icon = 'icons/mob/screen1.dmi', loc = src, icon_state = "centermarker") + selected_image = image(icon = buildmode_hud, loc = src, icon_state = "ai_sel") /mob/living/Destroy() dsoverlay.loc = null //I'll take my coat with me diff --git a/code/modules/mob/living/simple_mob/simple_mob.dm b/code/modules/mob/living/simple_mob/simple_mob.dm index c80a6b7329..c9030b0860 100644 --- a/code/modules/mob/living/simple_mob/simple_mob.dm +++ b/code/modules/mob/living/simple_mob/simple_mob.dm @@ -14,6 +14,8 @@ mob_swap_flags = ~HEAVY mob_push_flags = ~HEAVY + has_huds = TRUE // We do show AI status huds for buildmode players + var/tt_desc = null //Tooltip description //Settings for played mobs @@ -295,4 +297,9 @@ return mob_class & MOB_CLASS_HUMANOID|MOB_CLASS_ANIMAL|MOB_CLASS_SLIME // Update this if needed. /mob/living/simple_mob/get_nametag_desc(mob/user) - return "[tt_desc]" \ No newline at end of file + return "[tt_desc]" + +/mob/living/simple_mob/make_hud_overlays() + hud_list[STATUS_HUD] = gen_hud_image(buildmode_hud, src, "ai_0", plane = PLANE_BUILDMODE) + hud_list[LIFE_HUD] = gen_hud_image(buildmode_hud, src, "ais_1", plane = PLANE_BUILDMODE) + add_overlay(hud_list) diff --git a/code/modules/mob/mob_planes.dm b/code/modules/mob/mob_planes.dm index 2ba47421a4..86173ee7fe 100644 --- a/code/modules/mob/mob_planes.dm +++ b/code/modules/mob/mob_planes.dm @@ -35,6 +35,8 @@ plane_masters[VIS_MESONS] = new /obj/screen/plane_master{plane = PLANE_MESONS} //Meson-specific things like open ceilings. + plane_masters[VIS_BUILDMODE] = new /obj/screen/plane_master{plane = PLANE_BUILDMODE} //Things that only show up while in build mode + // Real tangible stuff planes plane_masters[VIS_TURFS] = new /obj/screen/plane_master/main{plane = TURF_PLANE} plane_masters[VIS_OBJS] = new /obj/screen/plane_master/main{plane = OBJ_PLANE} diff --git a/code/modules/xenobio/items/slimepotions.dm b/code/modules/xenobio/items/slimepotions.dm index 944853bec7..2a600fb801 100644 --- a/code/modules/xenobio/items/slimepotions.dm +++ b/code/modules/xenobio/items/slimepotions.dm @@ -109,7 +109,7 @@ to_chat(user, "You feed \the [SM] the agent, calming it.") playsound(src, 'sound/effects/bubbles.ogg', 50, 1) - AI.lost_target() // So hostile things stop attacking people even if not hostile anymore. + AI.remove_target() // So hostile things stop attacking people even if not hostile anymore. var/newname = copytext(sanitize(input(user, "Would you like to give \the [M] a name?", "Name your new pet", M.name) as null|text),1,MAX_NAME_LEN) if(newname) @@ -202,7 +202,7 @@ to_chat(user, "You feed \the [M] the agent. It will now try to murder things that want to murder you instead.") to_chat(M, "\The [user] feeds you \the [src], and feel that the others will regard you as an outsider now.") M.faction = user.faction - AI.lost_target() // So hostile things stop attacking people even if not hostile anymore. + AI.remove_target() // So hostile things stop attacking people even if not hostile anymore. playsound(src, 'sound/effects/bubbles.ogg', 50, 1) qdel(src) @@ -237,7 +237,7 @@ to_chat(user, "You feed \the [M] the agent. It will now be your best friend.") to_chat(M, "\The [user] feeds you \the [src], and feel that \the [user] wants to be best friends with you.") M.friends.Add(user) - AI.lost_target() // So hostile things stop attacking people even if not hostile anymore. + AI.remove_target() // So hostile things stop attacking people even if not hostile anymore. playsound(src, 'sound/effects/bubbles.ogg', 50, 1) qdel(src) diff --git a/icons/misc/buildmode.dmi b/icons/misc/buildmode.dmi index dc8de0b6d0..82214656eb 100644 Binary files a/icons/misc/buildmode.dmi and b/icons/misc/buildmode.dmi differ diff --git a/vorestation.dme b/vorestation.dme index a5172db312..fa92ba7c10 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -1551,6 +1551,7 @@ #include "code\modules\ai\_defines.dm" #include "code\modules\ai\ai_holder.dm" #include "code\modules\ai\ai_holder_combat.dm" +#include "code\modules\ai\ai_holder_combat_unseen.dm" #include "code\modules\ai\ai_holder_communication.dm" #include "code\modules\ai\ai_holder_cooperation.dm" #include "code\modules\ai\ai_holder_debug.dm"