diff --git a/code/__DEFINES/ai.dm b/code/__DEFINES/ai.dm index 7fee1c701e4a..937c6b5877a2 100644 --- a/code/__DEFINES/ai.dm +++ b/code/__DEFINES/ai.dm @@ -40,6 +40,9 @@ ///Does this require the current_movement_target to be adjacent and in reach? #define AI_BEHAVIOR_REQUIRE_REACH (1<<5) +/// Magic value to tell the G.O.A.P selector to ignore a behavior. +#define AI_GOAP_SKIP_BEHAVIOR (-1000001) + ///AI flags #define STOP_MOVING_WHEN_PULLED (1<<0) @@ -257,7 +260,20 @@ #define BB_FLOCK_WANDERING "BB_flock_wandering" #define BB_FLOCK_STARE_CD "BB_flock_stare_cooldown" #define BB_FLOCK_CONVERT_TARGET "BB_flock_convert_target" +#define BB_FLOCK_DECON_TARGET "BB_flock_decon_target" +#define BB_FLOCK_HARVEST_TARGET "BB_flock_harvest_target" #define BB_FLOCK_WANDER_FRUSTRATION "BB_flock_wander_frustration" #define BB_FLOCK_HEAL_TARGET "BB_flock_heal_target" #define BB_FLOCK_HEAL_FRUSTRATION "BB_flock_heal_frustation" +#define BB_FLOCK_REPLICATE_TARGET "BB_flock_replicate_target" +#define BB_FLOCK_NEST_TARGET "BB_flock_nest_target" #define BB_FLOCK_OVERMIND_CONTROL "BB_flock_overmind_control" +#define BB_FLOCK_CONTAINER_TARGET "BB_flock_container_target" +#define BB_FLOCK_RUMMAGE_TARGET "BB_flock_rummage_target" +#define BB_FLOCK_CAPTURE_TARGET "BB_flock_capture_target" +#define BB_FLOCK_ATTACK_TARGET "BB_flock_attack_target" +#define BB_FLOCK_ATTACK_COOLDOWN "BB_flock_attack_cooldown" +#define BB_FLOCK_ATTACK_RUN_COOLDOWN "BB_flock_attack_run_cooldown" +#define BB_FLOCK_ATTACK_STRAFE_COOLDOWN "BB_flock_attack_strafe_cooldown" +#define BB_FLOCK_ATTACK_FRUSTRATION "BB_flock_attack_frustration" +#define BB_FLOCK_DEPOSIT_TARGET "BB_flock_deposit_target" diff --git a/code/__DEFINES/flock_defines.dm b/code/__DEFINES/flock_defines.dm index 38bf89a19f7d..9ced846e40b1 100644 --- a/code/__DEFINES/flock_defines.dm +++ b/code/__DEFINES/flock_defines.dm @@ -3,13 +3,22 @@ #define FLOCK_TYPE_DRONE "drone" #define FLOCK_TYPE_BIT "bit" +// Turf reserved by a flockdrone #define FLOCK_NOTICE_RESERVED "reserved" +// Turf marked for conversion #define FLOCK_NOTICE_PRIORITY "priority" +// Mob marked as enemy #define FLOCK_NOTICE_ENEMY "enemy" +// Mob marked as ignored #define FLOCK_NOTICE_IGNORE "ignore" +// Flockdrone under flocktrace control #define FLOCK_NOTICE_FLOCKTRACE_CONTROL "flocktrace_control" +// Flockdrone under overmind control #define FLOCK_NOTICE_FLOCKMIND_CONTROL "flockmind_control" +// Flockdrone health bars #define FLOCK_NOTICE_HEALTH "flock_health" +// Atom marked for deconstruction +#define FLOCK_NOTICE_DECONSTRUCT "flock_deconstruct" #define FLOCK_UI_DRONES "drones" #define FLOCK_UI_TRACES "traces" @@ -19,3 +28,40 @@ #define FLOCK_COMPUTE_COST_FLOCKTRACE 100 #define FLOCK_COMPUTE_COST_DRONE 10 #define FLOCK_COMPUTE_COST_RELAY 500 + +#define FLOCK_TURFS_FOR_RELAY 250 + +/// Amount of substrate to add to a tealprint. +#define FLOCK_SUBSTRATE_COST_DEPOST_TEALPRINT 10 +/// Amount to convert a turf and it's contents. +#define FLOCK_SUBSTRATE_COST_CONVERT 20 +/// Amount to repair a flock construct. +#define FLOCK_SUBSTRATE_COST_REPAIR 10 +/// BASE amount to lay an egg. +#define FLOCK_SUBSTRATE_COST_LAY_EGG 100 + +/// Egg cost does not start scaling until there are this many drones. +#define FLOCK_MIN_DESIRED_POP 10 +/// Each drone above the min desired pop adds this much to the substrate required to be able to lay an egg. +#define FLOCK_ADDITIONAL_RESOURCE_RESERVATION_PER_DRONE 8 +#define FLOCK_DRONE_LIMIT 50 + +#define FLOCK_ENDGAME_LOST 1 +#define FLOCK_ENDGAME_RELAY_BUILT 2 +#define FLOCK_ENDGAME_RELAY_ACTIVATING 3 +#define FLOCK_ENDGAME_VICTORY 4 + +// G.O.A.P weights for flock behaviors, for easy comparison/adjustment. +#define FLOCK_BEHAVIOR_WEIGHT_WANDER 1 +#define FLOCK_BEHAVIOR_WEIGHT_STARE 1 +#define FLOCK_BEHAVIOR_WEIGHT_CONVERT 1 +#define FLOCK_BEHAVIOR_WEIGHT_HARVEST 2 +#define FLOCK_BEHAVIOR_WEIGHT_OPEN_CONTAINER 3 +#define FLOCK_BEHAVIOR_WEIGHT_RUMMAGE 3 +#define FLOCK_BEHAVIOR_WEIGHT_REPAIR 4 +#define FLOCK_BEHAVIOR_WEIGHT_NEST 6 +#define FLOCK_BEHAVIOR_WEIGHT_REPLICATE 7 +#define FLOCK_BEHAVIOR_WEIGHT_DECONSTRUCT 8 +#define FLOCK_BEHAVIOR_WEIGHT_DEPOSIT 8 +#define FLOCK_BEHAVIOR_WEIGHT_SHOOT 10 +#define FLOCK_BEHAVIOR_WEIGHT_CAPTURE 15 diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm index 4ac0a8b14be2..75cf6ef5132d 100644 --- a/code/__DEFINES/layers.dm +++ b/code/__DEFINES/layers.dm @@ -170,6 +170,9 @@ Specifically: ZMIMIC_MAX_PLANE to (ZMIMIC_MAX_PLANE - ZMIMIC_MAX_DEPTH) ///Pipecrawling images #define PIPECRAWL_IMAGES_PLANE 180 +///Info tags, below things like blindness and camera static +#define INFO_TAG_PLANE 190 + ///AI Camera Static #define CAMERA_STATIC_PLANE 200 @@ -181,6 +184,7 @@ Specifically: ZMIMIC_MAX_PLANE to (ZMIMIC_MAX_PLANE - ZMIMIC_MAX_DEPTH) #define DITHER_LAYER 3 #define UI_DAMAGE_LAYER 4 #define BLIND_LAYER 5 +#define FLOCK_CONVERT_LAYER 5.5 #define CRIT_LAYER 6 #define CURSE_LAYER 7 #define FOV_EFFECTS_LAYER 10000 //Blindness effects are not layer 4, they lie to you @@ -205,7 +209,6 @@ Specifically: ZMIMIC_MAX_PLANE to (ZMIMIC_MAX_PLANE - ZMIMIC_MAX_DEPTH) #define RADIAL_BACKGROUND_LAYER 0 ///1000 is an unimportant number, it's just to normalize copied layers #define RADIAL_CONTENT_LAYER 1000 - #define ADMIN_POPUP_LAYER 1 ///Layer for screentips diff --git a/code/__DEFINES/role_preferences.dm b/code/__DEFINES/role_preferences.dm index 4249d94f7c29..d6e6d3f5a1c3 100644 --- a/code/__DEFINES/role_preferences.dm +++ b/code/__DEFINES/role_preferences.dm @@ -12,7 +12,7 @@ #define ROLE_CHANGELING "Changeling" #define ROLE_CULTIST "Cultist" #define ROLE_FAMILIES "Gangster" -#define ROLE_FLOCK "Flock" +#define ROLE_FLOCK "Divine Flock" #define ROLE_HERETIC "Heretic" #define ROLE_MALF "Malf AI" #define ROLE_OPERATIVE "Operative" @@ -118,6 +118,7 @@ GLOBAL_LIST_INIT(special_roles, list( ROLE_TRAITOR = 0, ROLE_WIZARD = 14, ROLE_VAMPIRE = 0, + ROLE_FLOCK = 0, // Midround ROLE_ABDUCTOR = 0, diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 4e526ea2265e..16ef14c509b3 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -264,7 +264,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_IMPORTANT_SPEAKER "important_speaker" /// This mob automatically succeeds rolls for get_examine_result() #define TRAIT_BIGBRAIN "big_brain" - +/// Always butcherable even if butcher_results list is empty. +#define TRAIT_ALWAYS_BUTCHERABLE "always_butcherable" // Stops the mob from slipping on water, or banana peels, or pretty much anything that doesn't have [GALOSHES_DONT_HELP] set #define TRAIT_NO_SLIP_WATER "NO_SLIP_WATER" /// Stops the mob from slipping on permafrost ice (not any other ice) (but anything with [SLIDE_ICE] set) @@ -1026,7 +1027,12 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define FLOCK_CONTROLLED_BY_OVERMIND_SOURCE "FLOCK_CONTROLLED_BY_OVERMIND_SOURCE" #define TRAIT_FLOCKPHASE "TRAIT_FLOCKPHASE" +/// Is a flock object. Because of type fuckery. #define TRAIT_FLOCK_THING "TRAIT_FLOCK_THING" +/// Flockdrones cannot deconstruct this object. +#define TRAIT_FLOCK_NODECON "TRAIT_FLOCK_NODECON" +/// Implements flock_examine() proc. +#define TRAIT_FLOCK_EXAMINE "TRAIT_FLOCK_EXAMINE" /// Trait from mob/living/update_transform() #define UPDATE_TRANSFORM_TRAIT "update_transform" diff --git a/code/__HELPERS/atlas.dm b/code/__HELPERS/atlas.dm index b892adc6eac9..4ebfb58213ce 100644 --- a/code/__HELPERS/atlas.dm +++ b/code/__HELPERS/atlas.dm @@ -29,6 +29,7 @@ GLOBAL_REAL_VAR(list/atlas) = list() /// Key used for things that can call the shuttle #define TRACKING_KEY_SHUTTLE_CALLER "shuttle_caller" #define TRACKING_KEY_RCD "rcds" +#define TRACKING_KEY_FLOCK_INFO_HUDS "flock_info" /proc/list_debug() var/list/lists = list() diff --git a/code/__HELPERS/paths/astar_path.dm b/code/__HELPERS/paths/astar_path.dm index e4b27f1dd356..ca292428c807 100644 --- a/code/__HELPERS/paths/astar_path.dm +++ b/code/__HELPERS/paths/astar_path.dm @@ -78,6 +78,7 @@ open_binary_tree = null open_turf_to_node = null closed = null + heuristic = null // hard del generator if using generic_heuristic /** * "starts" off the pathfinding, by storing the values this datum will need to work later on diff --git a/code/__HELPERS/paths/jps_path.dm b/code/__HELPERS/paths/jps_path.dm index ab04627489bf..029185aa790a 100644 --- a/code/__HELPERS/paths/jps_path.dm +++ b/code/__HELPERS/paths/jps_path.dm @@ -428,7 +428,7 @@ continue if(!border.density && border.can_astar_pass == CANASTARPASS_DENSITY) continue - if(!border.CanAStarPass(actual_dir, pass_info)) + if(!border.CanAStarPass(actual_dir, pass_info, TRUE)) return TRUE // Destination blockers check diff --git a/code/__HELPERS/paths/path.dm b/code/__HELPERS/paths/path.dm index 515601d0ea3b..12c181804e4f 100644 --- a/code/__HELPERS/paths/path.dm +++ b/code/__HELPERS/paths/path.dm @@ -175,7 +175,7 @@ if(isflockdrone(construct_from)) var/mob/living/simple_animal/flock/drone/bird = construct_from - if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE) || bird.resources.has_points(10)) + if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE) || bird.substrate.has_points(10)) able_to_flockphase = TRUE /// List of vars on /datum/can_pass_info to use when checking two instances for equality diff --git a/code/_onclick/hud/action_button.dm b/code/_onclick/hud/action_button.dm index 23b8918e23cf..4906318fdbcd 100644 --- a/code/_onclick/hud/action_button.dm +++ b/code/_onclick/hud/action_button.dm @@ -189,6 +189,9 @@ DEFINE_INTERACTABLE(/atom/movable/screen/movable/action_button) for(var/datum/action/action as anything in actions) var/atom/movable/screen/movable/action_button/button = action.viewers[hud_used] + if(isnull(button)) + continue + action.build_all_button_icons() if(reload_screen) client.screen += button diff --git a/code/_onclick/hud/flock.dm b/code/_onclick/hud/flock.dm new file mode 100644 index 000000000000..f5e2886913a3 --- /dev/null +++ b/code/_onclick/hud/flock.dm @@ -0,0 +1,169 @@ +/datum/hud/flockdrone + ui_style = 'goon/icons/hud/flock_ui.dmi' + + var/atom/movable/screen/flock_relay_status/relay_status + +/datum/hud/flockdrone/New(mob/owner) + . = ..() + var/atom/movable/screen/using + + using = new /atom/movable/screen/flockdrone_part/converter(null, src) + static_inventory += using + + using = new /atom/movable/screen/flockdrone_part/incapacitator(null, src) + static_inventory += using + + using = new /atom/movable/screen/flockdrone_part/absorber(null, src) + static_inventory += using + + healthdoll = new /atom/movable/screen/flockdrone_health(null, src) + infodisplay += healthdoll + + relay_status = new(null, src) + infodisplay += relay_status + +/datum/hud/flockdrone/Destroy() + QDEL_NULL(relay_status) + return ..() + +// Used for flock traces and the overmind +/datum/hud/flockghost + ui_style = 'goon/icons/hud/flock_ui.dmi' + + var/atom/movable/screen/flock_relay_status/relay_status + +/datum/hud/flockghost/New(mob/owner) + . = ..() + relay_status = new(null, src) + infodisplay += relay_status + +/datum/hud/flockghost/Destroy() + QDEL_NULL(relay_status) + return ..() + +/atom/movable/screen/flockdrone_health + icon = 'goon/icons/hud/flock_ui.dmi' + icon_state = "health1" + screen_loc = ui_living_healthdoll + +/atom/movable/screen/flockdrone_part + icon = 'goon/icons/hud/flock_ui.dmi' + var/active_state = "" + var/inactive_state = "" + + var/part_type + var/datum/flockdrone_part/part_ref + +/atom/movable/screen/flockdrone_part/Initialize(mapload, datum/hud/hud_owner) + . = ..() + var/mob/living/simple_animal/flock/drone/drone = hud?.mymob + part_ref = locate(part_type) in drone?.parts // create n destroy + part_ref?.screen_obj = src + + update_appearance(UPDATE_ICON_STATE) + +/atom/movable/screen/flockdrone_part/Destroy() + part_ref?.screen_obj = null + part_ref = null + return ..() + +/atom/movable/screen/flockdrone_part/update_icon_state() + if(part_ref?.is_active()) + icon_state = active_state + else + icon_state = inactive_state + return ..() + +/atom/movable/screen/flockdrone_part/Click(location, control, params) + . = ..() + if(.) + return + + var/mob/living/simple_animal/flock/drone/drone = hud?.mymob + drone.set_active_part(part_ref) + +/atom/movable/screen/flockdrone_part/converter + name = "converter" + active_state = "converter1" + inactive_state = "converter0" + + screen_loc = "CENTER-1:16,SOUTH:5" + + part_type = /datum/flockdrone_part/converter + +/atom/movable/screen/flockdrone_part/incapacitator + name = "incapacitator" + active_state = "incapacitor1" + inactive_state = "incapacitor0" + + maptext_x = 6 + maptext_y = 6 + screen_loc = "CENTER:16,SOUTH:5" + + part_type = /datum/flockdrone_part/incapacitator + + var/icon/overlay_mask + var/obj/effect/abstract/charge_overlay + +/atom/movable/screen/flockdrone_part/incapacitator/Initialize(mapload, datum/hud/hud_owner) + charge_overlay = new() + . = ..() + charge_overlay.vis_flags = VIS_INHERIT_ID | VIS_INHERIT_LAYER | VIS_INHERIT_PLANE | VIS_INHERIT_ICON + charge_overlay.icon_state = "charge_overlay" + add_viscontents(charge_overlay) + + overlay_mask = icon('goon/icons/hud/flock_ui.dmi', "darkener") + charge_overlay.add_filter("mask", 1, alpha_mask_filter(0, 0, overlay_mask)) + +/atom/movable/screen/flockdrone_part/incapacitator/Destroy() + QDEL_NULL(charge_overlay) + return ..() + +/atom/movable/screen/flockdrone_part/incapacitator/update_appearance(updates) + . = ..() + var/datum/flockdrone_part/incapacitator/part = part_ref + maptext = MAPTEXT("[part?.shot_count || "0"]") + + var/datum/flockdrone_part/incapacitator/weapon = part_ref + charge_overlay.transition_filter( + "mask", + 0.5 SECONDS, + list( + "y" = -24 * (1 - round(weapon.shot_count / weapon.max_shots, 0.1)) + ), + SINE_EASING, + FALSE + ) + +/atom/movable/screen/flockdrone_part/absorber + name = "material decompiler" + active_state = "absorber" + inactive_state = "absorber" + + screen_loc = "CENTER+1:16,SOUTH:5" + + part_type = /datum/flockdrone_part/absorber + +/atom/movable/screen/flock_relay_status + name = "relay progress" + icon = 'goon/icons/hud/flock_ui.dmi' + icon_state = "structure-relay" + + screen_loc = "EAST-1:28,CENTER-2:15" + alpha = 0 + + /// Tracks the last flock status it was aware of. + var/flock_status = NONE + +/atom/movable/screen/flock_relay_status/update_icon_state() + switch(flock_status) + if(NONE) + icon_state = "structure-relay" + if(FLOCK_ENDGAME_RELAY_BUILT, FLOCK_ENDGAME_RELAY_ACTIVATING, FLOCK_ENDGAME_VICTORY) + icon_state = "structure-relay-glow" + return ..() + +/atom/movable/screen/flock_relay_status/update_overlays() + . = ..() + if(flock_status == FLOCK_ENDGAME_RELAY_ACTIVATING || flock_status == FLOCK_ENDGAME_VICTORY) + . += image(icon, "structure-relay-sparks") diff --git a/code/_onclick/hud/fullscreen.dm b/code/_onclick/hud/fullscreen.dm index c6ca0c8794b3..7dee486e581e 100644 --- a/code/_onclick/hud/fullscreen.dm +++ b/code/_onclick/hud/fullscreen.dm @@ -232,4 +232,8 @@ show_when_dead = TRUE screen_loc = "WEST,SOUTH to EAST,NORTH" -/atom/movable/screen/fullscreen/bloodlust +/atom/movable/screen/fullscreen/flock_convert + icon = 'goon/icons/hud/flockmindcircuit.dmi' + icon_state = "flockmindcircuit" + layer = FLOCK_CONVERT_LAYER + show_when_dead = FALSE diff --git a/code/_onclick/hud/hud.dm b/code/_onclick/hud/hud.dm index 8aa7ff69c8a2..c91f908bc2c8 100644 --- a/code/_onclick/hud/hud.dm +++ b/code/_onclick/hud/hud.dm @@ -495,6 +495,9 @@ GLOBAL_LIST_INIT(available_ui_styles, list( floating_actions = list() for(var/datum/action/action as anything in mymob.actions) var/atom/movable/screen/movable/action_button/button = action.viewers[src] + if(!action.render_button) + continue + if(!button) action.ShowTo(mymob) button = action.viewers[src] diff --git a/code/_onclick/hud/rendering/plane_master.dm b/code/_onclick/hud/rendering/plane_master.dm index a551bf7aadd1..3e5d2abfca1a 100644 --- a/code/_onclick/hud/rendering/plane_master.dm +++ b/code/_onclick/hud/rendering/plane_master.dm @@ -154,6 +154,11 @@ // This serves a similar purpose, I want the pipes to pop add_filter("pipe_dropshadow", 1, drop_shadow_filter(x = -1, y= -1, size = 1, color = "#0000007A")) +/atom/movable/screen/plane_master/info_tag + name = "info tag plane master" + plane = INFO_TAG_PLANE + blend_mode = BLEND_OVERLAY + /atom/movable/screen/plane_master/camera_static name = "camera static plane master" plane = CAMERA_STATIC_PLANE diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm index c56bae4a0542..e093a52675b4 100644 --- a/code/_onclick/hud/screen_objects.dm +++ b/code/_onclick/hud/screen_objects.dm @@ -49,6 +49,7 @@ hud = null return ..() +// Children should return TRUE to cancel further child calls. /atom/movable/screen/Click(location, control, params) SHOULD_CALL_PARENT(TRUE) . = !(TRUE || ..()) diff --git a/code/controllers/subsystem/fire_burning.dm b/code/controllers/subsystem/fire_burning.dm index e0a92fdec827..fbb5d8d27259 100644 --- a/code/controllers/subsystem/fire_burning.dm +++ b/code/controllers/subsystem/fire_burning.dm @@ -39,7 +39,7 @@ SUBSYSTEM_DEF(fire_burning) if(O.resistance_flags & ON_FIRE) //in case an object is extinguished while still in currentrun if(!(O.resistance_flags & FIRE_PROOF)) - O.take_damage(10 * delta_time, BURN, FIRE, 0) + O.take_damage(2 * delta_time, BURN, FIRE, 0) else O.extinguish() diff --git a/code/controllers/subsystem/shuttle.dm b/code/controllers/subsystem/shuttle.dm index f1daa4760744..b4d07b27d606 100644 --- a/code/controllers/subsystem/shuttle.dm +++ b/code/controllers/subsystem/shuttle.dm @@ -439,9 +439,9 @@ SUBSYSTEM_DEF(shuttle) log_shuttle("There is no means of calling the emergency shuttle anymore. Shuttle automatically called.") message_admins("All the communications consoles were destroyed and all AIs are inactive. Shuttle called.") -/datum/controller/subsystem/shuttle/proc/registerHostileEnvironment(datum/bad) +/datum/controller/subsystem/shuttle/proc/registerHostileEnvironment(datum/bad, announce = TRUE) hostile_environments[bad] = TRUE - checkHostileEnvironment() + checkHostileEnvironment(announce) /datum/controller/subsystem/shuttle/proc/clearHostileEnvironment(datum/bad) hostile_environments -= bad @@ -471,22 +471,26 @@ SUBSYSTEM_DEF(shuttle) supply.mode = SHUTTLE_DOCKED //Make all cargo consoles speak up -/datum/controller/subsystem/shuttle/proc/checkHostileEnvironment() +/datum/controller/subsystem/shuttle/proc/checkHostileEnvironment(announce = TRUE) for(var/datum/d in hostile_environments) if(!istype(d) || QDELETED(d)) hostile_environments -= d + emergency_no_escape = hostile_environments.len if(emergency_no_escape && (emergency.mode == SHUTTLE_IGNITING)) emergency.mode = SHUTTLE_STRANDED emergency.timer = null emergency.sound_played = FALSE - priority_announce("Hostile environment detected. \ - Departure has been postponed indefinitely pending \ - conflict resolution.", - "LRSV Icarus Announcement", - do_not_modify = TRUE - ) + + if(announce) + priority_announce("Hostile environment detected. \ + Departure has been postponed indefinitely pending \ + conflict resolution.", + "LRSV Icarus Announcement", + do_not_modify = TRUE + ) + if(!emergency_no_escape && (emergency.mode == SHUTTLE_STRANDED)) emergency.mode = SHUTTLE_DOCKED emergency.setTimer(emergency_dock_time) diff --git a/code/controllers/subsystem/timer.dm b/code/controllers/subsystem/timer.dm index 8f1ef68bebca..92406775ee20 100644 --- a/code/controllers/subsystem/timer.dm +++ b/code/controllers/subsystem/timer.dm @@ -126,6 +126,9 @@ SUBSYSTEM_DEF(timer) callBack.InvokeAsync() if(ctime_timer.flags & TIMER_LOOP) + if (QDELETED(ctime_timer)) // Don't re-insert timers deleted inside their callbacks. + continue + ctime_timer.spent = 0 ctime_timer.timeToRun = REALTIMEOFDAY + ctime_timer.wait BINARY_INSERT(ctime_timer, clienttime_timers, /datum/timedevent, ctime_timer, timeToRun, COMPARE_KEY) @@ -172,6 +175,9 @@ SUBSYSTEM_DEF(timer) last_invoke_tick = world.time if (timer.flags & TIMER_LOOP) // Prepare looping timers to re-enter the queue + if(QDELETED(timer)) // If a loop is deleted in its callback, we need to avoid re-inserting it. + continue + timer.spent = 0 timer.timeToRun = world.time + timer.wait timer.bucketJoin() @@ -667,7 +673,7 @@ SUBSYSTEM_DEF(timer) timer_subsystem = timer_subsystem || SStimer //id is string var/datum/timedevent/timer = timer_subsystem.timer_id_dict[id] - if (timer && (!timer.spent || timer.flags & TIMER_DELETE_ME)) + if (timer && (!timer.spent || (timer.flags & TIMER_DELETE_ME))) qdel(timer) return TRUE return FALSE @@ -681,11 +687,14 @@ SUBSYSTEM_DEF(timer) /proc/timeleft(id, datum/controller/subsystem/timer/timer_subsystem) if (!id) return null + if (id == TIMER_ID_NULL) CRASH("Tried to get timeleft of a null timerid. Use TIMER_STOPPABLE flag") + if (istype(id, /datum/timedevent)) var/datum/timedevent/timer = id return timer.timeToRun - world.time + timer_subsystem = timer_subsystem || SStimer //id is string var/datum/timedevent/timer = timer_subsystem.timer_id_dict[id] diff --git a/code/datums/actions/action.dm b/code/datums/actions/action.dm index 525e96f9fd56..c1734e8bd641 100644 --- a/code/datums/actions/action.dm +++ b/code/datums/actions/action.dm @@ -27,6 +27,8 @@ /// If TRUE, this action button will be shown to observers / other mobs who view from this action's owner's eyes. /// Used in [/mob/proc/show_other_mob_action_buttons] var/show_to_observers = TRUE + /// Should the action be rendered as a button. + var/render_button = TRUE /// The style the button's tooltips appear to be var/buttontooltipstyle = "" @@ -301,7 +303,8 @@ return LAZYOR(viewer.actions, src) // Move this in - ShowTo(viewer) + if(render_button) + ShowTo(viewer) /// Adds our action button to the screen of the passed viewer. /datum/action/proc/ShowTo(mob/viewer) diff --git a/code/datums/ai/_ai_behavior.dm b/code/datums/ai/_ai_behavior.dm index b937c4c4732b..e0bb6528f157 100644 --- a/code/datums/ai/_ai_behavior.dm +++ b/code/datums/ai/_ai_behavior.dm @@ -2,11 +2,16 @@ /datum/ai_behavior ///What distance you need to be from the target to perform the action var/required_distance = 1 + /// If >0, overrides controller.target_search_radius + var/search_radius_override = null ///Flags for extra behavior var/behavior_flags = NONE ///Cooldown between actions performances, defaults to the value of CLICK_CD_MELEE because that seemed like a nice standard for the speed of AI behavior var/action_cooldown = CLICK_CD_MELEE + /// A multiplier applied to the behavior's goap_score(). + var/goap_weight = 1 + /// Behaviors to add upon a successful setup var/list/sub_behaviors @@ -23,48 +28,124 @@ /datum/ai_behavior/proc/next_behavior(datum/ai_controller/controller, success) return null +/// Executed before goap_score(), to see if the behavior should even be considered. +/datum/ai_behavior/proc/goap_precondition(datum/ai_controller/controller) + return TRUE + /// Returns a numerical value that is essentially a priority for planner behaviors. -/datum/ai_behavior/proc/score(datum/ai_controller/controller) - return BEHAVIOR_SCORE_DEFAULT +/datum/ai_behavior/proc/goap_score(datum/ai_controller/controller) + return score_distance(controller, goap_get_ideal_target(controller)) + +/// Returns the ideal target for this behavior. +/datum/ai_behavior/proc/goap_get_ideal_target(datum/ai_controller/controller, set_path = FALSE) + var/list/options = goap_filter_targets(controller) + return get_best_target_by_distance_score(controller, options, set_path) + +/// Filter through potential targets to find real targets. +/datum/ai_behavior/proc/goap_filter_targets(datum/ai_controller/controller) + var/list/options = list() + for(var/atom/potential_target as anything in goap_get_potential_targets(controller)) + if(goap_is_valid_target(controller, potential_target)) + options += potential_target + return options + +/// Returns a list of potential targets to filter through. +/datum/ai_behavior/proc/goap_get_potential_targets(datum/ai_controller/controller) + return list() + +/// Returns TRUE if the given atom is a valid target for this behavior. +/datum/ai_behavior/proc/goap_is_valid_target(datum/ai_controller/controller, atom/target) + return TRUE + -/datum/ai_behavior/proc/get_best_target_by_distance_score(datum/ai_controller/controller, list/targets) +#define BINARY_INSERT_TARGET(target_list, target, score) \ + do { \ + var/length = length(target_list); \ + if(!length) { \ + target_list[target] = score; \ + } else { \ + var/left = 1; \ + var/right = length; \ + var/middle = (left + right) >> 1; \ + while(left < right) { \ + if(target_list[target_list[middle]] <= score) { \ + left = middle + 1; \ + } else { \ + right = middle; \ + }; \ + middle = (left + right) >> 1; \ + }; \ + middle = target_list[target_list[middle]] > score ? middle : middle + 1; \ + target_list.Insert(middle, target); \ + target_list[target] = score; \ + }; \ + } while(FALSE) + +/// Returns the best target by scoring the distance of each possible target. +/// Takes a list to insert the path into, so it can be handed back and re-used. +/datum/ai_behavior/proc/get_best_target_by_distance_score(datum/ai_controller/controller, list/targets, set_path = FALSE) if(!length(targets)) return null var/atom/movable/pawn = controller.pawn var/list/access = controller.get_access() - var/best_score = -INFINITY - var/atom/ideal_atom = null - - for(var/atom/A as anything in targets) - var/atom_basic_score = score_distance(controller, A) - if(atom_basic_score < best_score) - continue - - if(A.IsReachableBy(pawn)) - best_score = atom_basic_score - ideal_atom = A - continue - - var/list/path = SSpathfinder.astar_pathfind_now( - controller.pawn, - A, - controller.max_target_distance, - required_distance, - access, - HAS_TRAIT(controller.pawn, TRAIT_FREE_FLOAT_MOVEMENT), - ) - - if(length(path)) - best_score = atom_basic_score - ideal_atom = A - + var/list/targets_by_score = list() + var/list/reachable_targets = list() + + // Sort targets by their estimated score. The last element in the lists has the highest score. + while(length(targets)) + var/index = rand(1, length(targets)) + var/atom/A = targets[index] + targets.Cut(index, index + 1) + + var/score = score_distance(controller, A) + + BINARY_INSERT_TARGET(targets_by_score, A, score) + + // WEE WOO WEE WOO BEHAVIOR-CHANGING MICRO-OPT: we assume turfs further than 1 tile aren't reachable + // Because this is true in 99.9999999999999999% of cases + if(get_dist(pawn, A) <= 1 && A.IsReachableBy(pawn)) + BINARY_INSERT_TARGET(reachable_targets, A, score) + + // Go through our sorted target list until we find a path to one. + // Note: This does mean that the found target might not be the ideal one, as it's operating on the estimate + // This is a performance thing. We cannot actually use the true best target. + var/atom/ideal_atom + var/list/ideal_path + if(length(reachable_targets)) + ideal_atom = reachable_targets[length(reachable_targets)] + else + while(length(targets_by_score)) + var/atom/candidate = targets_by_score[length(targets_by_score)] + targets_by_score.len-- + + var/list/path = SSpathfinder.astar_pathfind_now( + controller.pawn, + candidate, + controller.max_target_distance, + required_distance, + access, + HAS_TRAIT(controller.pawn, TRAIT_FREE_FLOAT_MOVEMENT), + ) + + if(path) + ideal_atom = candidate + ideal_path = path + break + + if(set_path && length(ideal_path)) + controller.set_blackboard_key(BB_PATH_TO_USE, ideal_path) return ideal_atom +#undef BINARY_INSERT_TARGET + /// Helper for scoring something based on the distance between it and the pawn. +/// By default, returns a value between 100 and -INFINITY, where 100 is a distance of 0 steps. +/// A distance equal to target_search_radius is zero. +/// A distance greater than target_search_radius is negative. /datum/ai_behavior/proc/score_distance(datum/ai_controller/controller, atom/target) - var/search_radius = controller.target_search_radius + var/search_radius = search_radius_override || controller.target_search_radius if(isnull(target)) return -INFINITY return 100 * (search_radius - get_dist_manhattan(get_turf(controller.pawn), get_turf(target))) / search_radius diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm index 0b3467e37f99..18b3dde2f94b 100644 --- a/code/datums/ai/_ai_controller.dm +++ b/code/datums/ai/_ai_controller.dm @@ -183,11 +183,11 @@ multiple modular subtrees with behaviors if(behavior_cooldowns[current_behavior] > world.time) //Still on cooldown continue ProcessBehavior(action_seconds_per_tick, current_behavior) - return + continue if(isnull(current_movement_target)) fail_behavior(current_behavior) - return + continue ///Stops pawns from performing such actions that should require the target to be adjacent. var/atom/movable/moving_pawn = pawn @@ -199,7 +199,7 @@ multiple modular subtrees with behaviors if(behavior_cooldowns[current_behavior] > world.time) //Still on cooldown continue ProcessBehavior(action_seconds_per_tick, current_behavior) - return + continue if(ai_movement.moving_controllers[src] != current_movement_target) //We're too far, if we're not already moving start doing it. ai_movement.start_moving_towards(src, current_movement_target, current_behavior.required_distance) //Then start moving @@ -208,7 +208,7 @@ multiple modular subtrees with behaviors if(behavior_cooldowns[current_behavior] > world.time) //Still on cooldown continue ProcessBehavior(action_seconds_per_tick, current_behavior) - return + continue ///This is where you decide what actions are taken by the AI. /datum/ai_controller/proc/ProcessBehaviorSelection(delta_time) @@ -290,7 +290,7 @@ multiple modular subtrees with behaviors var/list/arguments = args.Copy() arguments[1] = src if(!behavior.setup(arglist(arguments))) - return + return FALSE LAZYADD(current_behaviors, behavior) arguments.Cut(1, 2) @@ -308,6 +308,8 @@ multiple modular subtrees with behaviors sub_args[1] = sub_behavior_type queue_behavior(arglist(sub_args)) + return TRUE + /datum/ai_controller/proc/ProcessBehavior(delta_time, datum/ai_behavior/behavior) DEBUG_AI_LOG(src, "Running [behavior]") diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm index be07d2994478..c340bc13ac6c 100644 --- a/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm @@ -62,11 +62,9 @@ if(!can_see(basic_mob, final_target, required_distance)) return BEHAVIOR_PERFORM_INSTANT - //controller.set_blackboard_key(hiding_location_keym, hiding_target) controller.set_blackboard_key(hiding_location_key, hiding_target) basic_mob.RangedAttack(final_target) - return BEHAVIOR_PERFORM_COOLDOWN /datum/ai_behavior/basic_ranged_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key) diff --git a/code/datums/ai/generic/ai_frustration.dm b/code/datums/ai/generic/ai_frustration.dm new file mode 100644 index 000000000000..c45ffb8a2890 --- /dev/null +++ b/code/datums/ai/generic/ai_frustration.dm @@ -0,0 +1,23 @@ +/// A simple frustration behavior to be queued in addition to another behavior. If the given duration ellapses and the frustration key is still +/// present, the ai will cancel all of it's actions. +/datum/ai_behavior/frustration + action_cooldown = 0.1 SECONDS + +/datum/ai_behavior/frustration/setup(datum/ai_controller/controller, frustration_key, duration) + . = ..() + controller.set_blackboard_key(frustration_key, world.time) + +/datum/ai_behavior/frustration/perform(delta_time, datum/ai_controller/controller, frustration_key, duration) + if(isnull(controller.blackboard[frustration_key])) + return BEHAVIOR_PERFORM_SUCCESS + + if(world.time >= duration + controller.blackboard[frustration_key]) + controller.CancelActions() + DEBUG_AI_LOG(controller, "AI got frustrated and cancelled current actions.") + return BEHAVIOR_PERFORM_FAILURE + + return BEHAVIOR_PERFORM_COOLDOWN + +/datum/ai_behavior/frustration/finish_action(datum/ai_controller/controller, succeeded, frustration_key, duration) + . = ..() + controller.clear_blackboard_key(frustration_key) diff --git a/code/datums/ai/generic/generic_behaviors.dm b/code/datums/ai/generic/generic_behaviors.dm index 0ae7b9980a74..abbe6af480c6 100644 --- a/code/datums/ai/generic/generic_behaviors.dm +++ b/code/datums/ai/generic/generic_behaviors.dm @@ -391,19 +391,3 @@ controller.set_blackboard_key(target_key, pick(possible_targets)) return BEHAVIOR_PERFORM_COOLDOWN | BEHAVIOR_PERFORM_SUCCESS - -/datum/ai_behavior/frustration - action_cooldown = 0.1 SECONDS - -/datum/ai_behavior/frustration/setup(datum/ai_controller/controller, frustration_key, duration) - . = ..() - controller.set_blackboard_key(frustration_key, world.time) - -/datum/ai_behavior/frustration/perform(delta_time, datum/ai_controller/controller, frustration_key, duration) - if(isnull(controller.blackboard[frustration_key])) - return BEHAVIOR_PERFORM_SUCCESS - - if(world.time >= duration + controller.blackboard[frustration_key]) - controller.CancelActions() - DEBUG_AI_LOG(controller, "AI got frustrated and cancelled current actions.") - return BEHAVIOR_PERFORM_FAILURE diff --git a/code/datums/ai/planner/_behavior_planner.dm b/code/datums/ai/goap_subtree.dm similarity index 63% rename from code/datums/ai/planner/_behavior_planner.dm rename to code/datums/ai/goap_subtree.dm index b84e53c92631..cae0c04998f6 100644 --- a/code/datums/ai/planner/_behavior_planner.dm +++ b/code/datums/ai/goap_subtree.dm @@ -1,7 +1,7 @@ -/datum/ai_planning_subtree/scored +/datum/ai_planning_subtree/goap var/list/possible_behaviors = list() -/datum/ai_planning_subtree/scored/setup(datum/ai_controller/controller, ...) +/datum/ai_planning_subtree/goap/setup(datum/ai_controller/controller, ...) . = ..() controller.set_blackboard_key(BB_PLANNER_BEHAVIORS, list()) for(var/behavior_type in possible_behaviors) @@ -11,13 +11,16 @@ continue controller.set_blackboard_key_assoc(BB_PLANNER_BEHAVIORS, behavior, 0) -/datum/ai_planning_subtree/scored/SelectBehaviors(datum/ai_controller/controller, delta_time) +/datum/ai_planning_subtree/goap/SelectBehaviors(datum/ai_controller/controller, delta_time) var/list/behavior_cache = controller.blackboard[BB_PLANNER_BEHAVIORS] var/datum/ai_behavior/candidate = behavior_cache[1] var/score_to_beat = behavior_cache[candidate] for(var/behavior in behavior_cache) var/behavior_score = behavior_cache[behavior] + if(behavior_score == AI_GOAP_SKIP_BEHAVIOR) + continue + // Filter out lower scoring behaviors if(score_to_beat > behavior_score) continue @@ -33,8 +36,11 @@ controller.queue_behavior(candidate.type) return SUBTREE_RETURN_FINISH_PLANNING -/datum/ai_planning_subtree/scored/ProcessBehaviorSelection(datum/ai_controller/controller, delta_time) +/datum/ai_planning_subtree/goap/ProcessBehaviorSelection(datum/ai_controller/controller, delta_time) for(var/datum/ai_behavior/behavior in controller.blackboard[BB_PLANNER_BEHAVIORS]) - controller.set_blackboard_key_assoc(BB_PLANNER_BEHAVIORS, behavior, behavior.score(controller)) + if(behavior.goap_precondition(controller)) + controller.set_blackboard_key_assoc(BB_PLANNER_BEHAVIORS, behavior, behavior.goap_weight * behavior.goap_score(controller)) + else + controller.set_blackboard_key_assoc(BB_PLANNER_BEHAVIORS, behavior, AI_GOAP_SKIP_BEHAVIOR) return ..() diff --git a/code/datums/ai/movement/ai_movement_astar.dm b/code/datums/ai/movement/ai_movement_astar.dm index 61bfd48c9c5b..51ebf5a1185c 100644 --- a/code/datums/ai/movement/ai_movement_astar.dm +++ b/code/datums/ai/movement/ai_movement_astar.dm @@ -28,6 +28,7 @@ use_diagonals = use_diagonals, ) + controller.clear_blackboard_key(BB_PATH_TO_USE) RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move)) RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move)) RegisterSignal(loop, COMSIG_MOVELOOP_REPATH, PROC_REF(repath_incoming)) diff --git a/code/datums/ai/movement/ai_movement_jps.dm b/code/datums/ai/movement/ai_movement_jps.dm index d2d6d519b352..98a871b677e2 100644 --- a/code/datums/ai/movement/ai_movement_jps.dm +++ b/code/datums/ai/movement/ai_movement_jps.dm @@ -27,6 +27,8 @@ diagonal_handling = isnull(diagonal_handling_override) ? diagonal_handling : diagonal_handling_override, ) + controller.clear_blackboard_key(BB_PATH_TO_USE) + RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move)) RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move)) RegisterSignal(loop, COMSIG_MOVELOOP_REPATH, PROC_REF(repath_incoming)) diff --git a/code/datums/ai/movement_behaviors/idle_random_walk.dm b/code/datums/ai/movement_behaviors/idle_random_walk.dm index b276746d3516..2d13d0d2c050 100644 --- a/code/datums/ai/movement_behaviors/idle_random_walk.dm +++ b/code/datums/ai/movement_behaviors/idle_random_walk.dm @@ -12,5 +12,5 @@ return BEHAVIOR_PERFORM_COOLDOWN | BEHAVIOR_PERFORM_SUCCESS -/datum/ai_behavior/idle_random_walk/score(datum/ai_controller/controller) +/datum/ai_behavior/idle_random_walk/goap_score(datum/ai_controller/controller) return 1 diff --git a/code/datums/components/butchering.dm b/code/datums/components/butchering.dm index 52a78c240300..771197f812d3 100644 --- a/code/datums/components/butchering.dm +++ b/code/datums/components/butchering.dm @@ -41,7 +41,7 @@ var/mob/living/M = interacting_with - if(M.stat == DEAD && (M.butcher_results || M.guaranteed_butcher_results)) //can we butcher it? + if(M.stat == DEAD && (M.butcher_results || M.guaranteed_butcher_results || HAS_TRAIT(M, TRAIT_ALWAYS_BUTCHERABLE))) //can we butcher it? if(butchering_enabled && (can_be_blunt || (source.sharpness & SHARP_EDGED))) INVOKE_ASYNC(src, PROC_REF(startButcher), source, M, user) return ITEM_INTERACT_SUCCESS @@ -99,7 +99,8 @@ span_notice("You butcher [meat].")) butcher_callback?.Invoke(butcher, meat) meat.harvest(butcher) - meat.gib(FALSE, FALSE, TRUE) + if(!QDELING(meat)) + meat.gib(FALSE, FALSE, TRUE) ///Special snowflake component only used for the recycler. /datum/component/butchering/recycler diff --git a/code/datums/keybinding/rawkey.dm b/code/datums/keybinding/rawkey.dm index 4d1ba5ccfcf8..9d139afa95f1 100644 --- a/code/datums/keybinding/rawkey.dm +++ b/code/datums/keybinding/rawkey.dm @@ -38,6 +38,7 @@ return user.mob.update_mouse_pointer() + user.mob.update_info_tags() /datum/keybinding/rawkey/shift/down(client/user) . = ..() @@ -45,4 +46,4 @@ return user.mob.update_mouse_pointer() - + user.mob.update_info_tags() diff --git a/code/datums/mind.dm b/code/datums/mind.dm index d6d678e14ce3..5fbadffd1553 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -744,7 +744,7 @@ announce_objectives(TRUE) //Something in here might have changed your mob - if(self_antagging && (!usr || !usr.client) && current.client) + if(self_antagging && (!usr || !usr.client) && current?.client) usr = current traitor_panel() diff --git a/code/datums/points.dm b/code/datums/points.dm index 5119313a1b93..4b7fcf2c073b 100644 --- a/code/datums/points.dm +++ b/code/datums/points.dm @@ -7,6 +7,19 @@ if(points >= atleast) return points +/datum/point_holder/proc/is_full() + return points == max_points + +/// Returns the number of points needed to fill the holder. +/datum/point_holder/proc/how_empty() + return max_points - points + +/// Returns the % (0-100) of how full the holder is. +/datum/point_holder/proc/percent(step = 1) + if(max_points == INFINITY) + return 0 + return round((points / max_points) * 100, step) + /datum/point_holder/proc/adjust_points(num, check_enough) if(num > 0) return add_points(num) @@ -14,24 +27,28 @@ else if(num < 0) return remove_points(num, check_enough) - return TRUE + return 0 +/// Add points. Returns the number of points added. /datum/point_holder/proc/add_points(num) if(points == max_points) - return FALSE + return 0 + var/old = points points = min(max_points, points + num) - return TRUE + return points - old +/// Removes points. Returns the number of points removed. /datum/point_holder/proc/remove_points(num, check_enough) if(check_enough && (points < num)) - return FALSE + return 0 if(points == 0) - return FALSE + return 0 + var/old = points points = max(0, points - num) - return TRUE + return old - points /datum/point_holder/proc/set_max_points(new_max) max_points = new_max @@ -39,3 +56,17 @@ /datum/point_holder/proc/get_max_points() return max_points + +/datum/point_holder/infinite + +/datum/point_holder/infinite/add_points(num) + return 0 + +/datum/point_holder/infinite/has_points(atleast) + return INFINITY + +/datum/point_holder/infinite/remove_points(num, check_enough) + return 0 + +/datum/point_holder/infinite/set_max_points(new_max) + max_points = new_max diff --git a/code/datums/proximity_monitor/field.dm b/code/datums/proximity_monitor/field.dm index a0fbc14f5565..c47012759161 100644 --- a/code/datums/proximity_monitor/field.dm +++ b/code/datums/proximity_monitor/field.dm @@ -15,6 +15,10 @@ /// Can be used in certain situations where you may have effects that trigger only at the edge, /// while also wanting the field effect to trigger at edge turfs as well var/edge_is_a_field = FALSE + + /// If TRUE, view() will be used over range(). If this is used, edge turfs will not exist! + var/use_view = FALSE + /// All turfs on the inside of the proximity monitor - range - 1 turfs var/list/turf/field_turfs = list() /// All turfs on the very last tile of the proximity monitor's radius @@ -120,6 +124,13 @@ /datum/proximity_monitor/advanced/proc/update_new_turfs() if(ignore_if_not_on_turf && !isturf(host.loc)) return list(FIELD_TURFS_KEY = list(), EDGE_TURFS_KEY = list()) + + if(use_view) + var/list/turfs = list() + for(var/turf/T as turf in view(current_range, host)) + turfs += T + return list(FIELD_TURFS_KEY = turfs, EDGE_TURFS_KEY = list()) + var/list/local_field_turfs = list() var/list/local_edge_turfs = list() var/turf/center = get_turf(host) diff --git a/code/datums/proximity_monitor/fields/interceptor_field.dm b/code/datums/proximity_monitor/fields/interceptor_field.dm new file mode 100644 index 000000000000..cdb069178b4c --- /dev/null +++ b/code/datums/proximity_monitor/fields/interceptor_field.dm @@ -0,0 +1,15 @@ +/datum/proximity_monitor/advanced/interceptor + use_view = TRUE + + +/datum/proximity_monitor/advanced/interceptor/New(atom/_host, range, _ignore_if_not_on_turf) + ..() + recalculate_field(TRUE) + +/datum/proximity_monitor/advanced/interceptor/Destroy() + return ..() + +/datum/proximity_monitor/advanced/interceptor/field_turf_crossed(atom/movable/movable, turf/old_location, turf/new_location) + . = ..() + if(isprojectile(movable)) + astype(host, /obj/structure/flock/interceptor).try_intercept_projectile(movable) diff --git a/code/datums/status_effects/flock_signal.dm b/code/datums/status_effects/flock_signal.dm index c354a949ca0a..d7a5ee9cd837 100644 --- a/code/datums/status_effects/flock_signal.dm +++ b/code/datums/status_effects/flock_signal.dm @@ -20,11 +20,11 @@ 'goon/sounds/flockmind/flockdrone_grump1.ogg', 'goon/sounds/flockmind/flockdrone_grump2.ogg', 'goon/sounds/flockmind/flockdrone_grump3.ogg', - 'goon/sounds/flockmind/radio_sweep1.ogg', - 'goon/sounds/flockmind/radio_sweep2.ogg', - 'goon/sounds/flockmind/radio_sweep3.ogg', - 'goon/sounds/flockmind/radio_sweep4.ogg', - 'goon/sounds/flockmind/radio_sweep5.ogg' + 'goon/sounds/radio_sweep1.ogg', + 'goon/sounds/radio_sweep2.ogg', + 'goon/sounds/radio_sweep3.ogg', + 'goon/sounds/radio_sweep4.ogg', + 'goon/sounds/radio_sweep5.ogg' ) /datum/status_effect/flock_signal/New(list/arguments) diff --git a/code/game/atom/atoms.dm b/code/game/atom/atoms.dm index 08f7a4162327..56c5a382b13a 100644 --- a/code/game/atom/atoms.dm +++ b/code/game/atom/atoms.dm @@ -2243,11 +2243,12 @@ TYPEINFO_DEF(/atom) * Arguments: * * to_dir - What direction we're trying to move in, relevant for things like directional windows that only block movement in certain directions * * pass_info - Datum that stores info about the thing that's trying to pass us + * * leaving - TRUE if the mob would be leaving the turf to go to another one. Used to make up for the lack of enter/exit. * * IMPORTANT NOTE: /turf/proc/LinkBlockedWithAccess assumes that overrides of CanAStarPass will always return true if density is FALSE * If this is NOT you, ensure you edit your can_astar_pass variable. Check __DEFINES/path.dm **/ -/atom/proc/CanAStarPass(to_dir, datum/can_pass_info/pass_info) +/atom/proc/CanAStarPass(to_dir, datum/can_pass_info/pass_info, leaving) if(pass_info && (pass_info.pass_flags & pass_flags_self)) return TRUE . = !density diff --git a/code/game/gamemodes/antagonist_selector/flock.dm b/code/game/gamemodes/antagonist_selector/flock.dm new file mode 100644 index 000000000000..71711b852f86 --- /dev/null +++ b/code/game/gamemodes/antagonist_selector/flock.dm @@ -0,0 +1,8 @@ +/datum/antagonist_selector/flock + restricted_jobs = list( + JOB_CAPTAIN, + JOB_SECURITY_MARSHAL, + ) + + antag_datum = /datum/antagonist/flock/overmind + antag_flag = ROLE_FLOCK diff --git a/code/game/gamemodes/divine_flock.dm b/code/game/gamemodes/divine_flock.dm new file mode 100644 index 000000000000..9fde1f27fd24 --- /dev/null +++ b/code/game/gamemodes/divine_flock.dm @@ -0,0 +1,18 @@ +/datum/game_mode/one_antag/flock + name = "Divine Flock" + config_key = "divine_flock" + + weight = GAMEMODE_WEIGHT_NEVER + + antag_selector = /datum/antagonist_selector/flock + //min_pop = 30 + +/datum/game_mode/one_antag/flock/get_antag_count() + return 1 + +/datum/game_mode/one_antag/flock/check_finished() + . = ..() + if(.) + return + + return get_default_flock()?.consider_game_over() diff --git a/code/game/gamemodes/objectives/flock_objectives.dm b/code/game/gamemodes/objectives/flock_objectives.dm new file mode 100644 index 000000000000..cbe42d462734 --- /dev/null +++ b/code/game/gamemodes/objectives/flock_objectives.dm @@ -0,0 +1,8 @@ +/datum/objective/flock_relay + name = "Broadcast" + objective_name = "Directive" + explanation_text = "Broadcast the Signal" + +/datum/objective/flock_relay/check_completion() + var/mob/camera/flock/ghostbird = owner.current + return ghostbird.flock.flock_game_status == FLOCK_ENDGAME_VICTORY diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm index 6f099461c9cc..3ec648ab64f9 100644 --- a/code/game/machinery/doors/airlock.dm +++ b/code/game/machinery/doors/airlock.dm @@ -1308,7 +1308,7 @@ assemblytype = initial(airlock.assemblytype) update_appearance() -/obj/machinery/door/airlock/CanAStarPass(to_dir, datum/can_pass_info/pass_info) +/obj/machinery/door/airlock/CanAStarPass(to_dir, datum/can_pass_info/pass_info, leaving) //Airlock is passable if it is open (!density), bot has access, and is not bolted shut or powered off) return !density || (!locked && !pass_info.no_id && check_access_list(pass_info.access) && hasPower()) @@ -1402,7 +1402,7 @@ log_combat(user, src, message) log_touch(user) -/obj/machinery/door/airlock/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir) +/obj/machinery/door/airlock/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir, armor_penetration = 0, allow_break = TRUE) if((damage_amount >= atom_integrity) && (damage_flag == BOMB)) flags_1 |= NODECONSTRUCT_1 //If an explosive took us out, don't drop the assembly diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm index 6897086d55eb..d12a19d630a7 100644 --- a/code/game/machinery/doors/door.dm +++ b/code/game/machinery/doors/door.dm @@ -277,7 +277,7 @@ DEFINE_INTERACTABLE(/obj/machinery/door) if(istype(mover) && (mover.pass_flags & PASSGLASS)) return !opacity -/obj/machinery/door/CanAStarPass(to_dir, datum/can_pass_info/pass_info) +/obj/machinery/door/CanAStarPass(to_dir, datum/can_pass_info/pass_info, leaving) . = ..() if(.) return @@ -414,7 +414,7 @@ DEFINE_INTERACTABLE(/obj/machinery/door) try_to_crowbar_secondary(tool, user, forced_open) return ITEM_INTERACT_SUCCESS -/obj/machinery/door/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir) +/obj/machinery/door/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir, armor_penetration = 0, allow_break = TRUE) . = ..() if(. && atom_integrity > 0) if(damage_amount >= 10 && prob(30)) diff --git a/code/game/machinery/doors/firedoor.dm b/code/game/machinery/doors/firedoor.dm index cfd015fa1a0a..1ed0763581ec 100644 --- a/code/game/machinery/doors/firedoor.dm +++ b/code/game/machinery/doors/firedoor.dm @@ -410,7 +410,7 @@ TYPEINFO_DEF(/obj/machinery/door/firedoor) if(!(border_dir == dir)) //Make sure looking at appropriate border return TRUE -/obj/machinery/door/firedoor/border_only/CanAStarPass(to_dir, datum/can_pass_info/pass_info) +/obj/machinery/door/firedoor/border_only/CanAStarPass(to_dir, datum/can_pass_info/pass_info, leaving) return !density || (dir != to_dir) /obj/machinery/door/firedoor/border_only/proc/on_exit(datum/source, atom/movable/leaving, direction) diff --git a/code/game/machinery/doors/windowdoor.dm b/code/game/machinery/doors/windowdoor.dm index e3268ea1e6a2..72e1a183eabc 100644 --- a/code/game/machinery/doors/windowdoor.dm +++ b/code/game/machinery/doors/windowdoor.dm @@ -172,7 +172,7 @@ TYPEINFO_DEF(/obj/machinery/door/window) return ZONE_BLOCKED //used in the AStar algorithm to determinate if the turf the door is on is passable -/obj/machinery/door/window/CanAStarPass(to_dir, datum/can_pass_info/pass_info) +/obj/machinery/door/window/CanAStarPass(to_dir, datum/can_pass_info/pass_info, leaving) return !density || (dir != to_dir) || (check_access_list(pass_info.access) && hasPower() && !pass_info.no_id) /obj/machinery/door/window/proc/on_exit(datum/source, atom/movable/leaving, direction) @@ -267,7 +267,7 @@ TYPEINFO_DEF(/obj/machinery/door/window) /obj/machinery/door/window/fire_act(exposed_temperature, exposed_volume, turf/adjacent) take_damage(round(exposed_temperature / 200), BURN, 0, 0) -/obj/structure/window/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1) +/obj/structure/window/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, armor_penetration = 0, allow_break = TRUE) var/initial_damage_percentage = get_integrity_percentage() . = ..() if(.) //received damage diff --git a/code/game/machinery/firealarm.dm b/code/game/machinery/firealarm.dm index eebaa566d25b..333db7eb9538 100644 --- a/code/game/machinery/firealarm.dm +++ b/code/game/machinery/firealarm.dm @@ -381,7 +381,7 @@ TYPEINFO_DEF(/obj/machinery/firealarm) return TRUE return FALSE -/obj/machinery/firealarm/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir) +/obj/machinery/firealarm/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir, armor_penetration = 0, allow_break = TRUE) . = ..() if(.) //damage received if(atom_integrity > 0 && !(machine_stat & BROKEN) && buildstage != 0) diff --git a/code/game/machinery/newscaster/newscaster_machine.dm b/code/game/machinery/newscaster/newscaster_machine.dm index 101bae163b70..32ab144d12d1 100644 --- a/code/game/machinery/newscaster/newscaster_machine.dm +++ b/code/game/machinery/newscaster/newscaster_machine.dm @@ -150,7 +150,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/newscaster, 30) else take_damage(5, BRUTE, BLUNT) -/obj/machinery/newscaster/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir) +/obj/machinery/newscaster/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir, armor_penetration = 0, allow_break = TRUE) . = ..() update_appearance() diff --git a/code/game/machinery/porta_turret/portable_turret.dm b/code/game/machinery/porta_turret/portable_turret.dm index b243b187dff7..9a8d8c291b4f 100644 --- a/code/game/machinery/porta_turret/portable_turret.dm +++ b/code/game/machinery/porta_turret/portable_turret.dm @@ -380,7 +380,7 @@ TYPEINFO_DEF(/obj/machinery/porta_turret) addtimer(CALLBACK(src, PROC_REF(toggle_on), TRUE), rand(60,600)) -/obj/machinery/porta_turret/take_damage(damage, damage_type = BRUTE, damage_flag = 0, sound_effect = 1) +/obj/machinery/porta_turret/take_damage(damage, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, armor_penetration = 0, allow_break = TRUE) . = ..() if(. && atom_integrity > 0) //damage received if(prob(30)) diff --git a/code/game/machinery/shieldgen.dm b/code/game/machinery/shieldgen.dm index 5d105a4d986f..8fd3f8360759 100644 --- a/code/game/machinery/shieldgen.dm +++ b/code/game/machinery/shieldgen.dm @@ -38,7 +38,7 @@ if(BRUTE) playsound(loc, 'sound/effects/empulse.ogg', 75, TRUE) -/obj/structure/emergency_shield/take_damage(damage, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir) +/obj/structure/emergency_shield/take_damage(damage, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir, armor_penetration = 0, allow_break = TRUE) . = ..() if(.) //damage was dealt new /obj/effect/temp_visual/impact_effect/ion(loc) @@ -492,7 +492,7 @@ playsound(loc, 'sound/effects/empulse.ogg', 75, TRUE) //the shield wall is immune to damage but it drains the stored power of the generators. -/obj/machinery/shieldwall/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir) +/obj/machinery/shieldwall/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir, armor_penetration = 0, allow_break = TRUE) . = ..() if(damage_type == BRUTE || damage_type == BURN) drain_power(damage_amount) diff --git a/code/game/objects/effects/mines.dm b/code/game/objects/effects/mines.dm index 7609262a3055..aadb1341017b 100644 --- a/code/game/objects/effects/mines.dm +++ b/code/game/objects/effects/mines.dm @@ -52,7 +52,7 @@ triggermine(AM) -/obj/effect/mine/take_damage(damage_amount, damage_type, damage_flag, sound_effect, attack_dir) +/obj/effect/mine/take_damage(damage_amount, damage_type, damage_flag, sound_effect, attack_dir, armor_penetration = 0, allow_break = TRUE) . = ..() triggermine() diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index c5b6cdcc89b1..0293bacac324 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -14,7 +14,7 @@ DEFINE_INTERACTABLE(/obj/item) ///the icon to indicate this object is being dragged mouse_drag_pointer = MOUSE_ACTIVE_POINTER - max_integrity = 200 + max_integrity = 10 obj_flags = NONE pass_flags = PASSTABLE diff --git a/code/game/objects/items/devices/forcefieldprojector.dm b/code/game/objects/items/devices/forcefieldprojector.dm index 6caab4710008..bf233cdd4a45 100644 --- a/code/game/objects/items/devices/forcefieldprojector.dm +++ b/code/game/objects/items/devices/forcefieldprojector.dm @@ -136,7 +136,7 @@ TYPEINFO_DEF(/obj/structure/projected_forcefield) /obj/structure/projected_forcefield/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0) playsound(loc, 'sound/weapons/egloves.ogg', 80, TRUE) -/obj/structure/projected_forcefield/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir) +/obj/structure/projected_forcefield/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir, armor_penetration = 0, allow_break = TRUE) if(sound_effect) play_attack_sound(damage_amount, damage_type, damage_flag) if(generator) diff --git a/code/game/objects/items/devices/radio/radio.dm b/code/game/objects/items/devices/radio/radio.dm index 49d6da761fd6..d312d7be7872 100644 --- a/code/game/objects/items/devices/radio/radio.dm +++ b/code/game/objects/items/devices/radio/radio.dm @@ -84,6 +84,7 @@ TYPEINFO_DEF(/obj/item/radio) wires.cut(WIRE_TX) // OH GOD WHY secure_radio_connections = list() . = ..() + SET_TRACKING(__TYPE__) for(var/ch_name in channels) secure_radio_connections[ch_name] = add_radio(src, GLOB.radiochannels[ch_name]) @@ -96,6 +97,7 @@ TYPEINFO_DEF(/obj/item/radio) AddElement(/datum/element/empprotection, EMP_PROTECT_WIRES) /obj/item/radio/Destroy() + UNSET_TRACKING(__TYPE__) remove_radio_all(src) //Just to be sure QDEL_NULL(wires) QDEL_NULL(keyslot) diff --git a/code/game/objects/items/devices/reverse_bear_trap.dm b/code/game/objects/items/devices/reverse_bear_trap.dm index be37aed24f02..a8888fc51997 100644 --- a/code/game/objects/items/devices/reverse_bear_trap.dm +++ b/code/game/objects/items/devices/reverse_bear_trap.dm @@ -9,7 +9,7 @@ flags_1 = CONDUCT_1 resistance_flags = FIRE_PROOF | UNACIDABLE w_class = WEIGHT_CLASS_NORMAL - max_integrity = 300 + max_integrity = 50 inhand_icon_state = "reverse_bear_trap" lefthand_file = 'icons/mob/inhands/items_lefthand.dmi' righthand_file = 'icons/mob/inhands/items_righthand.dmi' diff --git a/code/game/objects/items/dualsaber.dm b/code/game/objects/items/dualsaber.dm index 64c9ba39e76e..68ef59e93d22 100644 --- a/code/game/objects/items/dualsaber.dm +++ b/code/game/objects/items/dualsaber.dm @@ -37,7 +37,7 @@ TYPEINFO_DEF(/obj/item/dualsaber) attack_verb_continuous = list("attacks", "slashes", "stabs", "slices", "tears", "lacerates", "rips", "dices", "cuts") attack_verb_simple = list("attack", "slash", "stab", "slice", "tear", "lacerate", "rip", "dice", "cut") - max_integrity = 200 + max_integrity = 50 resistance_flags = FIRE_PROOF var/w_class_on = WEIGHT_CLASS_BULKY diff --git a/code/game/objects/items/fireaxe.dm b/code/game/objects/items/fireaxe.dm index d85766bede5d..77d266af1309 100644 --- a/code/game/objects/items/fireaxe.dm +++ b/code/game/objects/items/fireaxe.dm @@ -22,7 +22,7 @@ TYPEINFO_DEF(/obj/item/fireaxe) attack_verb_simple = list("attack", "chop", "cleave", "tear", "lacerate", "cut") hitsound = 'sound/weapons/bladeslice.ogg' sharpness = SHARP_EDGED - max_integrity = 200 + max_integrity = 30 resistance_flags = FIRE_PROOF /obj/item/fireaxe/Initialize(mapload) diff --git a/code/game/objects/items/grenades/_grenade.dm b/code/game/objects/items/grenades/_grenade.dm index d7e868bd8f4b..d8d1bcfe6cf7 100644 --- a/code/game/objects/items/grenades/_grenade.dm +++ b/code/game/objects/items/grenades/_grenade.dm @@ -26,7 +26,7 @@ flags_1 = CONDUCT_1 | PREVENT_CONTENTS_EXPLOSION_1 // We detonate upon being exploded. slot_flags = ITEM_SLOT_BELT resistance_flags = FLAMMABLE - max_integrity = 40 + max_integrity = 20 /// Bitfields which prevent the grenade from detonating if set. Includes ([GRENADE_DUD]|[GRENADE_USED]) var/dud_flags = NONE ///Is this grenade currently armed? diff --git a/code/game/objects/items/melee/energy.dm b/code/game/objects/items/melee/energy.dm index f8eb28bfa023..508463562b5f 100644 --- a/code/game/objects/items/melee/energy.dm +++ b/code/game/objects/items/melee/energy.dm @@ -3,7 +3,7 @@ TYPEINFO_DEF(/obj/item/melee/energy) /obj/item/melee/energy icon = 'icons/obj/transforming_energy.dmi' - max_integrity = 200 + max_integrity = 25 attack_verb_continuous = list("hits", "taps", "pokes") attack_verb_simple = list("hit", "tap", "poke") resistance_flags = FIRE_PROOF diff --git a/code/game/objects/items/paint.dm b/code/game/objects/items/paint.dm index d8748cd55cf3..563328a09285 100644 --- a/code/game/objects/items/paint.dm +++ b/code/game/objects/items/paint.dm @@ -118,7 +118,6 @@ TYPEINFO_DEF(/obj/item/paint_sprayer) inhand_icon_state = "paintcan" w_class = WEIGHT_CLASS_NORMAL resistance_flags = FLAMMABLE - max_integrity = 100 /obj/item/paint_remover/examine(mob/user) . = ..() diff --git a/code/game/objects/items/pitchfork.dm b/code/game/objects/items/pitchfork.dm index 9ae9bab005c6..ad55cc7de76f 100644 --- a/code/game/objects/items/pitchfork.dm +++ b/code/game/objects/items/pitchfork.dm @@ -27,7 +27,7 @@ TYPEINFO_DEF(/obj/item/pitchfork) attack_verb_simple = list("attack", "impale", "pierce") hitsound = 'sound/weapons/bladeslice.ogg' sharpness = SHARP_EDGED - max_integrity = 200 + max_integrity = 30 resistance_flags = FIRE_PROOF /obj/item/pitchfork/update_icon_state() diff --git a/code/game/objects/items/shields.dm b/code/game/objects/items/shields.dm index 8c9fd8483f09..25e54fde31e3 100644 --- a/code/game/objects/items/shields.dm +++ b/code/game/objects/items/shields.dm @@ -131,7 +131,7 @@ TYPEINFO_DEF(/obj/item/shield/riot/buckler) resistance_flags = FLAMMABLE block_chance = 30 transparent = FALSE - max_integrity = 55 + max_integrity = 50 w_class = WEIGHT_CLASS_NORMAL /obj/item/shield/riot/buckler/shatter(mob/living/carbon/human/owner) diff --git a/code/game/objects/items/spear.dm b/code/game/objects/items/spear.dm index 85dd1f635cc4..503e37213177 100644 --- a/code/game/objects/items/spear.dm +++ b/code/game/objects/items/spear.dm @@ -27,7 +27,7 @@ TYPEINFO_DEF(/obj/item/spear) attack_verb_continuous = list("attacks", "pokes", "jabs", "tears", "lacerates", "gores") attack_verb_simple = list("attack", "poke", "jab", "tear", "lacerate", "gore") sharpness = SHARP_EDGED // i know the whole point of spears is that they're pointy, but edged is more devastating at the moment so - max_integrity = 200 + max_integrity = 30 var/war_cry = "AAAAARGH!!!" var/icon_prefix = "spearglass" diff --git a/code/game/objects/items/stacks/medical.dm b/code/game/objects/items/stacks/medical.dm index f148f406fe96..8d4d48328588 100644 --- a/code/game/objects/items/stacks/medical.dm +++ b/code/game/objects/items/stacks/medical.dm @@ -13,7 +13,7 @@ stamina_critical_chance = 0 resistance_flags = FLAMMABLE - max_integrity = 40 + max_integrity = 3 novariants = FALSE item_flags = NOBLUDGEON cost = 250 diff --git a/code/game/objects/items/stacks/sheets/glass.dm b/code/game/objects/items/stacks/sheets/glass.dm index 3598de7dab31..29b7ab8c5d54 100644 --- a/code/game/objects/items/stacks/sheets/glass.dm +++ b/code/game/objects/items/stacks/sheets/glass.dm @@ -299,7 +299,7 @@ TYPEINFO_DEF(/obj/item/shard) attack_verb_simple = list("stab", "slash", "slice", "cut") hitsound = 'sound/weapons/bladeslice.ogg' resistance_flags = ACID_PROOF - max_integrity = 40 + max_integrity = 3 sharpness = SHARP_EDGED var/icon_prefix var/craft_time = 3.5 SECONDS diff --git a/code/game/objects/items/stacks/sheets/mineral.dm b/code/game/objects/items/stacks/sheets/mineral.dm index 9741e02316f7..f678bad1a1b1 100644 --- a/code/game/objects/items/stacks/sheets/mineral.dm +++ b/code/game/objects/items/stacks/sheets/mineral.dm @@ -156,7 +156,7 @@ GLOBAL_LIST_INIT(uranium_recipes, list ( \ singular_name = "plasma sheet" sheettype = "plasma" resistance_flags = FLAMMABLE - max_integrity = 100 + max_integrity = 30 mats_per_unit = list(/datum/material/plasma=MINERAL_MATERIAL_AMOUNT) grind_results = list(/datum/reagent/toxin/plasma = 20) point_value = 20 diff --git a/code/game/objects/items/stacks/stack.dm b/code/game/objects/items/stacks/stack.dm index 12bbce1299a0..5f3b7997901f 100644 --- a/code/game/objects/items/stacks/stack.dm +++ b/code/game/objects/items/stacks/stack.dm @@ -21,7 +21,7 @@ maptext_y = 2 material_modifier = 0.05 //5%, so that a 50 sheet stack has the effect of 5k materials instead of 100k. - max_integrity = 100 + max_integrity = 10 var/list/datum/stack_recipe/recipes /// The name of one piece of the stack. diff --git a/code/game/objects/items/storage/backpack.dm b/code/game/objects/items/storage/backpack.dm index 175ef9c3d639..01943204ca74 100644 --- a/code/game/objects/items/storage/backpack.dm +++ b/code/game/objects/items/storage/backpack.dm @@ -23,7 +23,7 @@ w_class = WEIGHT_CLASS_BULKY slot_flags = ITEM_SLOT_BACK //ERROOOOO resistance_flags = NONE - max_integrity = 300 + max_integrity = 10 supports_variations_flags = CLOTHING_TESHARI_VARIATION | CLOTHING_VOX_VARIATION storage_type = /datum/storage/backpack @@ -417,7 +417,7 @@ TYPEINFO_DEF(/obj/item/storage/backpack/holding) inhand_icon_state = "duffel-curse" slowdown = 2 item_flags = DROPDEL - max_integrity = 100 + max_integrity = 20 ///counts time passed since it ate food var/hunger = 0 supports_variations_flags = NONE diff --git a/code/game/objects/items/storage/belt.dm b/code/game/objects/items/storage/belt.dm index 99b3fd483952..d211875aaf51 100644 --- a/code/game/objects/items/storage/belt.dm +++ b/code/game/objects/items/storage/belt.dm @@ -14,7 +14,7 @@ slot_flags = ITEM_SLOT_BELT attack_verb_continuous = list("whips", "lashes", "disciplines") attack_verb_simple = list("whip", "lash", "discipline") - max_integrity = 300 + max_integrity = 10 equip_sound = 'sound/items/equip/toolbelt_equip.ogg' equip_delay_self = EQUIP_DELAY_BELT diff --git a/code/game/objects/items/tail_pin.dm b/code/game/objects/items/tail_pin.dm index 079101882e4a..725c796010aa 100644 --- a/code/game/objects/items/tail_pin.dm +++ b/code/game/objects/items/tail_pin.dm @@ -14,7 +14,7 @@ TYPEINFO_DEF(/obj/item/tail_pin) attack_verb_continuous = list("pokes", "jabs", "pins the tail on") attack_verb_simple = list("poke", "jab") sharpness = SHARP_POINTY - max_integrity = 200 + max_integrity = 1 layer = CORGI_ASS_PIN_LAYER /obj/item/poster/tail_board diff --git a/code/game/objects/items/weaponry.dm b/code/game/objects/items/weaponry.dm index 7278ab1581c6..23bf49ccf881 100644 --- a/code/game/objects/items/weaponry.dm +++ b/code/game/objects/items/weaponry.dm @@ -14,7 +14,7 @@ TYPEINFO_DEF(/obj/item/banhammer) throw_range = 7 attack_verb_continuous = list("bans") attack_verb_simple = list("ban") - max_integrity = 200 + max_integrity = 30 resistance_flags = FIRE_PROOF /obj/item/banhammer/Initialize(mapload) @@ -81,7 +81,7 @@ TYPEINFO_DEF(/obj/item/claymore) attack_verb_simple = list("attack", "slash", "stab", "slice", "tear", "lacerate", "rip", "dice", "cut") block_chance = 50 sharpness = SHARP_EDGED - max_integrity = 200 + max_integrity = 30 resistance_flags = FIRE_PROOF /obj/item/claymore/Initialize(mapload) @@ -275,7 +275,7 @@ TYPEINFO_DEF(/obj/item/katana) attack_verb_simple = list("attack", "slash", "stab", "slice", "tear", "lacerate", "rip", "dice", "cut") block_chance = 50 sharpness = SHARP_EDGED - max_integrity = 200 + max_integrity = 30 resistance_flags = FIRE_PROOF /obj/item/katana/suicide_act(mob/user) diff --git a/code/game/objects/structures/beds_chairs/chair.dm b/code/game/objects/structures/beds_chairs/chair.dm index abfea0fda124..d95b6ff917d7 100644 --- a/code/game/objects/structures/beds_chairs/chair.dm +++ b/code/game/objects/structures/beds_chairs/chair.dm @@ -11,7 +11,7 @@ TYPEINFO_DEF(/obj/structure/chair) buckle_lying = 0 //you sit in a chair, not lay resistance_flags = NONE - max_integrity = 100 + max_integrity = 20 integrity_failure = 0.1 layer = OBJ_LAYER @@ -143,7 +143,7 @@ TYPEINFO_DEF(/obj/structure/chair) name = "wooden chair" desc = "Old is never too old to not be in fashion." resistance_flags = FLAMMABLE - max_integrity = 70 + max_integrity = 15 buildstacktype = /obj/item/stack/sheet/mineral/wood buildstackamount = 3 item_chair = /obj/item/chair/wood @@ -161,7 +161,7 @@ TYPEINFO_DEF(/obj/structure/chair) icon_state = "comfychair" color = rgb(255,255,255) resistance_flags = FLAMMABLE - max_integrity = 70 + max_integrity = 30 buildstackamount = 2 item_chair = null // The mutable appearance used for the overlay over buckled mobs. @@ -303,7 +303,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/chair/stool/bar, 0) desc = "A makeshift bamboo stool with a rustic look." icon_state = "bamboo_stool" resistance_flags = FLAMMABLE - max_integrity = 60 + max_integrity = 10 buildstacktype = /obj/item/stack/sheet/mineral/bamboo buildstackamount = 2 item_chair = /obj/item/chair/stool/bamboo @@ -336,7 +336,7 @@ TYPEINFO_DEF(/obj/item/chair) w_class = WEIGHT_CLASS_HUGE - max_integrity = 125 + max_integrity = 20 integrity_failure = 0.1 force = 12 // dude it's a fucking chair @@ -481,7 +481,7 @@ TYPEINFO_DEF(/obj/item/chair) hitsound = 'sound/weapons/genhit1.ogg' origin_type = /obj/structure/chair/stool/bamboo - max_integrity = 50 + max_integrity = 10 /obj/item/chair/stool/bamboo/integrity_loss_on_hit() return rand(15, 25) @@ -495,7 +495,7 @@ TYPEINFO_DEF(/obj/item/chair/stool/wood) inhand_icon_state = "stool_wood" origin_type = /obj/structure/chair/stool/wood - max_integrity = 100 + max_integrity = 15 /obj/item/chair/stool/wood/Initialize(mapload) . = ..() @@ -515,7 +515,7 @@ TYPEINFO_DEF(/obj/item/chair/wood) icon_state = "wooden_chair_toppled" inhand_icon_state = "woodenchair" resistance_flags = FLAMMABLE - max_integrity = 70 + max_integrity = 15 hitsound = 'sound/weapons/genhit1.ogg' origin_type = /obj/structure/chair/wood break_chance = 50 @@ -597,7 +597,7 @@ TYPEINFO_DEF(/obj/structure/chair/plastic) name = "folding plastic chair" desc = "No matter how much you squirm, it'll still be uncomfortable." resistance_flags = FLAMMABLE - max_integrity = 50 + max_integrity = 10 buildstacktype = /obj/item/stack/sheet/plastic buildstackamount = 2 item_chair = /obj/item/chair/plastic diff --git a/code/game/objects/structures/crates_lockers/crates/secure.dm b/code/game/objects/structures/crates_lockers/crates/secure.dm index 3497bb0c61fa..a409b5b49207 100644 --- a/code/game/objects/structures/crates_lockers/crates/secure.dm +++ b/code/game/objects/structures/crates_lockers/crates/secure.dm @@ -12,7 +12,7 @@ TYPEINFO_DEF(/obj/structure/closet/crate/secure) damage_deflection = 18 -/obj/structure/closet/crate/secure/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1) +/obj/structure/closet/crate/secure/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, armor_penetration = 0, allow_break = TRUE) if(prob(tamperproof) && damage_amount >= DAMAGE_PRECISION) boom() else diff --git a/code/game/objects/structures/fireaxe.dm b/code/game/objects/structures/fireaxe.dm index 26c98946f338..81ecb81e7f96 100644 --- a/code/game/objects/structures/fireaxe.dm +++ b/code/game/objects/structures/fireaxe.dm @@ -79,7 +79,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/fireaxecabinet, 32) if(BURN) playsound(src.loc, 'sound/items/welder.ogg', 100, TRUE) -/obj/structure/fireaxecabinet/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = TRUE, attack_dir) +/obj/structure/fireaxecabinet/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = TRUE, attack_dir, armor_penetration = 0, allow_break = TRUE) if(open) return . = ..() diff --git a/code/game/objects/structures/girders.dm b/code/game/objects/structures/girders.dm index 42a7daf6a6ce..9dee23f1f3ee 100644 --- a/code/game/objects/structures/girders.dm +++ b/code/game/objects/structures/girders.dm @@ -254,7 +254,7 @@ if((mover.pass_flags & PASSGRILLE) || istype(mover, /obj/projectile)) return prob(girderpasschance) -/obj/structure/girder/CanAStarPass(to_dir, datum/can_pass_info/pass_info) +/obj/structure/girder/CanAStarPass(to_dir, datum/can_pass_info/pass_info, leaving) if(!density) return TRUE diff --git a/code/game/objects/structures/grille.dm b/code/game/objects/structures/grille.dm index 11c7275d126c..9bc35c65ebe9 100644 --- a/code/game/objects/structures/grille.dm +++ b/code/game/objects/structures/grille.dm @@ -147,7 +147,7 @@ TYPEINFO_DEF(/obj/structure/grille) if(!. && istype(mover, /obj/projectile)) return prob(30) -/obj/structure/grille/CanAStarPass(to_dir, datum/can_pass_info/pass_info) +/obj/structure/grille/CanAStarPass(to_dir, datum/can_pass_info/pass_info, leaving) if(!density) return TRUE diff --git a/code/game/objects/structures/plasticflaps.dm b/code/game/objects/structures/plasticflaps.dm index ea42a2c49e25..352bbec386d7 100644 --- a/code/game/objects/structures/plasticflaps.dm +++ b/code/game/objects/structures/plasticflaps.dm @@ -67,7 +67,7 @@ TYPEINFO_DEF(/obj/structure/plasticflaps) return FALSE return TRUE -/obj/structure/plasticflaps/CanAStarPass(to_dir, datum/can_pass_info/pass_info) +/obj/structure/plasticflaps/CanAStarPass(to_dir, datum/can_pass_info/pass_info, leaving) if(pass_info.is_living) if(pass_info.is_bot) return TRUE diff --git a/code/game/objects/structures/railings.dm b/code/game/objects/structures/railings.dm index 2c5af9677422..5c25d3374e25 100644 --- a/code/game/objects/structures/railings.dm +++ b/code/game/objects/structures/railings.dm @@ -87,7 +87,7 @@ TYPEINFO_DEF(/obj/structure/railing) return . || mover.throwing || mover.movement_type & (FLYING | FLOATING) return TRUE -/obj/structure/railing/CanAStarPass(to_dir, datum/can_pass_info/pass_info) +/obj/structure/railing/CanAStarPass(to_dir, datum/can_pass_info/pass_info, leaving) if(!(to_dir & dir)) return TRUE return ..() diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm index a0af3bb26210..4da2f5da4251 100644 --- a/code/game/objects/structures/window.dm +++ b/code/game/objects/structures/window.dm @@ -341,7 +341,7 @@ TYPEINFO_DEF(/obj/structure/window) return TRUE -/obj/structure/window/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1) +/obj/structure/window/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, armor_penetration = 0, allow_break = TRUE) var/initial_damage_percentage = get_integrity_percentage() . = ..() if(.) //received damage @@ -462,11 +462,15 @@ TYPEINFO_DEF(/obj/structure/window) /obj/structure/window/get_dumping_location() return null -/obj/structure/window/CanAStarPass(to_dir, datum/can_pass_info/pass_info) +/obj/structure/window/CanAStarPass(to_dir, datum/can_pass_info/pass_info, leaving) if(!density) return TRUE + + if(fulltile && leaving) + return TRUE + if(fulltile || (dir == to_dir)) - return FALSE + return pass_info && (pass_info.pass_flags & pass_flags_self) return TRUE diff --git a/code/game/turfs/open/openspace.dm b/code/game/turfs/open/openspace.dm index f3a2d7f560bf..0c563519c7be 100644 --- a/code/game/turfs/open/openspace.dm +++ b/code/game/turfs/open/openspace.dm @@ -109,7 +109,7 @@ /turf/open/openspace/rust_heretic_act() return FALSE -/turf/open/openspace/CanAStarPass(to_dir, datum/can_pass_info/pass_info) +/turf/open/openspace/CanAStarPass(to_dir, datum/can_pass_info/pass_info, leaving) var/atom/movable/our_movable = pass_info.caller_ref.resolve() if(our_movable && !our_movable.can_z_move(DOWN, src, null , ZMOVE_FALL_FLAGS)) //If we can't fall here (flying/lattice), it's fine to path through return TRUE diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm index 0c22a2cf50c8..ac6054d8f7db 100644 --- a/code/modules/antagonists/_common/antag_datum.dm +++ b/code/modules/antagonists/_common/antag_datum.dm @@ -545,6 +545,11 @@ GLOBAL_LIST_EMPTY(antagonists) if (antag_hud.mobShouldSee(owner.current)) antag_hud.show_to(owner.current) +/// Helper proc for adding an objective instance. +/datum/antagonist/proc/add_objective(datum/objective/O) + O.owner ||= owner // Multi-owner objectives are a fuck + objectives += O + //This one is created by admin tools for custom objectives /datum/antagonist/custom antagpanel_category = "Custom" diff --git a/code/modules/antagonists/blob/structures/_blob.dm b/code/modules/antagonists/blob/structures/_blob.dm index f0be8761ba9f..b11dcc04817d 100644 --- a/code/modules/antagonists/blob/structures/_blob.dm +++ b/code/modules/antagonists/blob/structures/_blob.dm @@ -289,7 +289,7 @@ TYPEINFO_DEF(/obj/structure/blob) damage_amount = overmind.blobstrain.damage_reaction(src, damage_amount, damage_type, damage_flag) return damage_amount -/obj/structure/blob/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir) +/obj/structure/blob/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir, armor_penetration = 0, allow_break = TRUE) . = ..() if(. && atom_integrity > 0) update_appearance() diff --git a/code/modules/antagonists/blob/structures/shield.dm b/code/modules/antagonists/blob/structures/shield.dm index 4a55649cd901..e0c89b073b0a 100644 --- a/code/modules/antagonists/blob/structures/shield.dm +++ b/code/modules/antagonists/blob/structures/shield.dm @@ -30,7 +30,7 @@ TYPEINFO_DEF(/obj/structure/blob/shield) . = ..() desc = (atom_integrity < (max_integrity * 0.5)) ? "[damaged_desc]" : initial(desc) -/obj/structure/blob/shield/take_damage(damage_amount, damage_type, damage_flag, sound_effect, attack_dir) +/obj/structure/blob/shield/take_damage(damage_amount, damage_type, damage_flag, sound_effect, attack_dir, armor_penetration = 0, allow_break = TRUE) . = ..() if(. && atom_integrity > 0) atmosblock = atom_integrity < (max_integrity * 0.5) diff --git a/code/modules/atmospherics/machinery/airalarm.dm b/code/modules/atmospherics/machinery/airalarm.dm index 442061c0e631..c42a8f5d3c42 100644 --- a/code/modules/atmospherics/machinery/airalarm.dm +++ b/code/modules/atmospherics/machinery/airalarm.dm @@ -1130,8 +1130,6 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/airalarm, 21) alert_type = code - - #undef AALARM_MODE_SCRUBBING #undef AALARM_MODE_VENTING #undef AALARM_MODE_PANIC diff --git a/code/modules/atmospherics/machinery/portable/canister.dm b/code/modules/atmospherics/machinery/portable/canister.dm index 249733bcf099..98427c5ad53a 100644 --- a/code/modules/atmospherics/machinery/portable/canister.dm +++ b/code/modules/atmospherics/machinery/portable/canister.dm @@ -463,7 +463,7 @@ TYPEINFO_DEF(/obj/machinery/portable_atmospherics/canister) if(gone == internal_cell) internal_cell = null -/obj/machinery/portable_atmospherics/canister/take_damage(damage_amount, damage_type = BRUTE, damage_flag = "", sound_effect = TRUE, attack_dir, armor_penetration = 0) +/obj/machinery/portable_atmospherics/canister/take_damage(damage_amount, damage_type = BRUTE, damage_flag = "", sound_effect = TRUE, attack_dir, armor_penetration = 0, allow_break = TRUE) . = ..() if(!. || QDELETED(src)) return diff --git a/code/modules/clothing/clothing.dm b/code/modules/clothing/clothing.dm index 9e47bcaa57af..460d5f1a50d8 100644 --- a/code/modules/clothing/clothing.dm +++ b/code/modules/clothing/clothing.dm @@ -3,7 +3,7 @@ /obj/item/clothing name = "clothing" resistance_flags = FLAMMABLE - max_integrity = 200 + max_integrity = 20 integrity_failure = 0.4 stamina_damage = 0 diff --git a/code/modules/do_after/do_after.dm b/code/modules/do_after/do_after.dm index 77cf768edad0..495d5ce2db6d 100644 --- a/code/modules/do_after/do_after.dm +++ b/code/modules/do_after/do_after.dm @@ -12,7 +12,7 @@ * * max_interact_count: The action will automatically fail if they are already performing this many or more actions with the given interaction_key. * * display: An atom or image to display over the user's head. Only works with DO_PUBLIC flag. */ -/proc/do_after(atom/movable/user, atom/target, time = 0, timed_action_flags = NONE, progress = TRUE, datum/callback/extra_checks, interaction_key, max_interact_count = 1, image/display) +/proc/do_after(atom/movable/user, atom/target, time = 0, timed_action_flags = NONE, progress = TRUE, datum/callback/extra_checks, interaction_key, max_interact_count = 1, image/display, action_type = /datum/timed_action) if(!user) return FALSE @@ -38,27 +38,38 @@ sortTim(temp, GLOBAL_PROC_REF(cmp_text_asc)) interaction_key = jointext(temp, "-") - if(interaction_key) //Do we have a interaction_key now? - var/current_interaction_count = LAZYACCESS(user.do_afters, interaction_key) || 0 + if(interaction_key) + var/current_interaction_count = length(LAZYACCESS(user.do_afters, interaction_key)) if(current_interaction_count >= max_interact_count) //We are at our peak return - LAZYSET(user.do_afters, interaction_key, current_interaction_count + 1) - var/datum/timed_action/action = new(user, target, time, progress, timed_action_flags, extra_checks, display) + var/datum/timed_action/action = new action_type(user, target, time, progress, timed_action_flags, extra_checks, display) + + if(interaction_key) + LAZYINITLIST(user.do_afters) + LAZYADD(user.do_afters[interaction_key], action) . = action.wait() if(interaction_key) - var/reduced_interaction_count = (LAZYACCESS(user.do_afters, interaction_key)) - 1 - if(reduced_interaction_count > 0) // Not done yet! - LAZYSET(user.do_afters, interaction_key, reduced_interaction_count) + LAZYREMOVE(user.do_afters[interaction_key], action) + if(length(LAZYACCESS(user.do_afters, interaction_key)) == 0) // Not done yet! + LAZYREMOVE(user.do_afters, interaction_key) return - LAZYREMOVE(user.do_afters, interaction_key) - /// Returns the total amount of do_afters this mob is taking part in /mob/proc/do_after_count() var/count = 0 - for(var/key in do_afters) - count += do_afters[key] + for(var/interaction_key, action_list in do_afters) + count += length(action_list) return count + +/// Cancel do afters, optionally pass an interaction key to cancel specific ones. +/mob/proc/cancel_do_afters(cancel_key) + if(cancel_key) + for(var/datum/timed_action/action as anything in LAZYACCESS(do_afters, cancel_key)) + action.cancel() + return + + for(var/interaction_key in do_afters) + cancel_do_afters(interaction_key) diff --git a/code/modules/do_after/lay_egg_action.dm b/code/modules/do_after/lay_egg_action.dm new file mode 100644 index 000000000000..01c816203cae --- /dev/null +++ b/code/modules/do_after/lay_egg_action.dm @@ -0,0 +1,11 @@ +/datum/timed_action/flock_lay_egg + +/datum/timed_action/flock_lay_egg/on_process(delta_time) + . = ..() + var/mob/living/simple_animal/flock/drone/bird = user + if(!bird.substrate.has_points(bird.flock?.current_egg_cost || FLOCK_SUBSTRATE_COST_LAY_EGG)) + return FALSE + + if(DT_PROB(5, delta_time)) + user.shake_animation() + playsound(user, pick('goon/sounds/Metal_Clang_1.ogg', 'goon/sounds/mixer.ogg'), 30, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE) diff --git a/code/modules/do_after/timed_action.dm b/code/modules/do_after/timed_action.dm index 46c25d2cba48..8106b7461cc5 100644 --- a/code/modules/do_after/timed_action.dm +++ b/code/modules/do_after/timed_action.dm @@ -131,7 +131,7 @@ cancel() return - if(!on_process()) + if(!on_process(delta_time)) cancel() return @@ -144,7 +144,7 @@ progbar.update(world.time - start_time) /// For subtypes to implement behavior in process(). Return FALSE to cancel the timed action. -/datum/timed_action/proc/on_process() +/datum/timed_action/proc/on_process(delta_time) return TRUE /// Called when the timed action succeeds. diff --git a/code/modules/flockmind/actions/cage_mob.dm b/code/modules/flockmind/actions/cage_mob.dm new file mode 100644 index 000000000000..ff7795e563ed --- /dev/null +++ b/code/modules/flockmind/actions/cage_mob.dm @@ -0,0 +1,54 @@ +/datum/action/cooldown/flock/cage_mob + name = "Cage" + cooldown_time = 5 SECONDS + click_to_activate = TRUE + render_button = FALSE + + var/obj/effect/abstract/flock_conversion/turf_effect + +/datum/action/cooldown/flock/cage_mob/New(Target) + . = ..() + turf_effect = new(null) + +/datum/action/cooldown/flock/cage_mob/Destroy() + QDEL_NULL(turf_effect) + return ..() + +/datum/action/cooldown/flock/cage_mob/is_valid_target(atom/cast_on) + return isliving(cast_on) && !isflockmob(cast_on) && isturf(cast_on.loc) + +/datum/action/cooldown/flock/cage_mob/Activate(atom/target) + if(DOING_INTERACTION(owner, "flock_cage")) + return FALSE + + var/mob/living/simple_animal/flock/drone/bird = owner + bird.stop_flockphase(TRUE) + var/turf/T = get_turf(target) + + owner.visible_message( + span_notice("[owner] begins forming a cuboid structure around [target]."), + blind_message = span_hear("You hear a strange synthetic whirring."), + ) + + T.add_viscontents(turf_effect) + if(iswallturf(T)) + turf_effect.icon_state = "spawn-wall-loop" + flick("spawn-wall", turf_effect) + + log_combat(owner, target, "attempted to cage") + + . = TRUE + playsound(owner, 'goon/sounds/flockmind/flockdrone_build.ogg', 30, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE) + if(!do_after(owner, target, 4.5 SECONDS, DO_PUBLIC, interaction_key = "flock_cage")) + . = FALSE + + T.remove_viscontents(turf_effect) + if(!.) + return + + log_combat(owner, target, "caged") + + playsound(owner, 'goon/sounds/flockmind/flockdrone_build_complete.ogg', 70, TRUE) + var/obj/structure/flock/cage/cage = new(T, bird.flock) + cage.cage_mob(target) + ..() diff --git a/code/modules/flockmind/actions/control_drone.dm b/code/modules/flockmind/actions/control_drone.dm index 309fc8507b58..1e5ce743ee4c 100644 --- a/code/modules/flockmind/actions/control_drone.dm +++ b/code/modules/flockmind/actions/control_drone.dm @@ -1,5 +1,6 @@ /datum/action/cooldown/flock/control_drone name = "Control Drone" + desc = "Direct a drone to perform an action." button_icon_state = "ping" cooldown_time = 0 click_to_activate = TRUE @@ -34,35 +35,110 @@ return TRUE selected_bird.ai_controller.CancelActions() - var/mob/camera/flock/overmind/ghost_bird = owner - if(isturf(target)) - selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_conversion_target, target) - var/image/pointer = pointer_image_to(selected_bird, target) + // Move to turf/structure, or convert turf. + if(isturf(target) || istype(target, /obj/structure/flock)) + if(istype(target, /obj/structure/flock/tealprint)) + if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_deposit_target, target)) + return FALSE + + pointer_helper(selected_bird, target, 2 SECONDS) + unset_click_ability(owner, performing_task = TRUE) + return TRUE + + var/turf/T = get_turf(target) + if(isflockturf(T)) + if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/rally, target)) + return FALSE + + pointer_helper(selected_bird, target, 2 SECONDS) + selected_bird.say("instruction confirmed: move to location") + unset_click_ability(owner, performing_task = TRUE) + return TRUE + + if(T.can_flock_convert()) + if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_conversion_target, target)) + return FALSE + + pointer_helper(selected_bird, target, 2 SECONDS) + unset_click_ability(owner, performing_task = TRUE) + return TRUE + + + // Harvest items + else if(isitem(target)) + if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_harvest_target, target)) + return FALSE + + pointer_helper(selected_bird, target, 2 SECONDS) + unset_click_ability(owner, performing_task = TRUE) + return TRUE + + // Attack or convert enemies. + else if(ismob(target) && selected_bird.flock.is_mob_enemy(target)) + var/mob/living/L = target + if(L.incapacitated(IGNORE_STASIS | IGNORE_RESTRAINTS | IGNORE_GRAB) || L.IsKnockdown()) + if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_capture_target, target)) + return FALSE + + pointer_helper(selected_bird, target, 2 SECONDS) + unset_click_ability(owner, performing_task = TRUE) + return TRUE - animate(pointer, time = 2 SECONDS, alpha = 0) - ghost_bird.flock.add_ping_image(ghost_bird.client, pointer, 2 SECONDS) + else if(selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/attack_target, target)) + pointer_helper(selected_bird, target, 2 SECONDS) + unset_click_ability(owner, performing_task = TRUE) + return TRUE -/datum/action/cooldown/flock/control_drone/unset_click_ability(mob/on_who, refund_cooldown) + return FALSE + + else if(isflockmob(target)) + if(istype(target, /mob/living/simple_animal/flock/bit)) + to_chat(owner, span_warning("Flockbits are too simple to be remotely controlled.")) + return FALSE + + var/mob/living/simple_animal/flock/other_bird = target + if(other_bird.flock == selected_bird.flock) + if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_heal_target, target)) + return FALSE + + pointer_helper(selected_bird, target, 2 SECONDS) + unset_click_ability(owner, performing_task = TRUE) + return TRUE + + +/datum/action/cooldown/flock/control_drone/unset_click_ability(mob/on_who, refund_cooldown, performing_task = TRUE) . = ..() - free_drone() + free_drone(performing_task) + +/datum/action/cooldown/flock/control_drone/proc/pointer_helper(atom/from, atom/towards, duration) + var/mob/camera/flock/overmind/ghost_bird = owner + var/image/pointer = pointer_image_to(from, towards) + + animate(pointer, time = 2 SECONDS, alpha = 0) + ghost_bird.flock.add_ping_image(ghost_bird.client, pointer, 2 SECONDS) /datum/action/cooldown/flock/control_drone/proc/bind_drone(mob/living/simple_animal/flock/drone/bird) selected_bird = bird RegisterSignal(bird, COMSIG_PARENT_QDELETING, PROC_REF(drone_gone)) ADD_TRAIT(bird, TRAIT_AI_DISABLE_PLANNING, FLOCK_CONTROLLED_BY_OVERMIND_SOURCE) bird.ai_controller.CancelActions() - bird.say("Suspending automated subroutines pending sentient level instruction.", forced = "overmind taking control") + bird.cancel_do_afters() + bird.say("suspending automated subroutines pending sentient-level instruction", forced = "overmind taking control") + bird.AddComponent(/datum/component/flock_ping/selected) -/datum/action/cooldown/flock/control_drone/proc/free_drone() +/datum/action/cooldown/flock/control_drone/proc/free_drone(performing_task = TRUE) if(!selected_bird) return - if(!QDELETED(selected_bird)) + if(!QDELETED(selected_bird) && !performing_task) spawn(-1) - selected_bird.say("Sentient level instruction suspended, resuming automated subroutines.", forced = "overmind control ended") + selected_bird.say("Sentient-level instruction suspended, resuming automated subroutines.", forced = "overmind control ended") + UnregisterSignal(selected_bird, COMSIG_PARENT_QDELETING) REMOVE_TRAIT(selected_bird, TRAIT_AI_DISABLE_PLANNING, FLOCK_CONTROLLED_BY_OVERMIND_SOURCE) + + qdel(selected_bird.GetComponent(/datum/component/flock_ping/selected)) selected_bird = null unset_click_ability(owner) diff --git a/code/modules/flockmind/actions/convert.dm b/code/modules/flockmind/actions/convert.dm index 47e2957410f6..a3cb35bf9562 100644 --- a/code/modules/flockmind/actions/convert.dm +++ b/code/modules/flockmind/actions/convert.dm @@ -2,6 +2,7 @@ name = "Convert" click_to_activate = TRUE check_flags = AB_CHECK_CONSCIOUS + render_button = FALSE var/obj/effect/abstract/flock_conversion/turf_effect @@ -36,6 +37,11 @@ playsound(owner, 'goon/sounds/flockmind/flockdrone_convert.ogg', 30, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE) var/turf/clicked_atom_turf = get_turf(target) + var/mob/living/simple_animal/flock/bird = owner + astype(bird, /mob/living/simple_animal/flock/drone)?.stop_flockphase(TRUE) + + if(!bird.substrate.has_points(FLOCK_SUBSTRATE_COST_CONVERT)) + return FALSE if(isfloorturf(clicked_atom_turf) || iswallturf(clicked_atom_turf)) return convert_turf(clicked_atom_turf) @@ -67,7 +73,12 @@ if(!is_valid_target(T)) return FALSE - flock_convert_turf(T, bird.flock) + if(bird.flock) + bird.flock.claim_turf(T) + else + flock_convert_turf(T, bird.flock) + + bird.substrate.remove_points(FLOCK_SUBSTRATE_COST_CONVERT) return TRUE /obj/effect/abstract/flock_conversion diff --git a/code/modules/flockmind/actions/create_rift.dm b/code/modules/flockmind/actions/create_rift.dm index 82cc14ab1be4..e4e1fce4c54a 100644 --- a/code/modules/flockmind/actions/create_rift.dm +++ b/code/modules/flockmind/actions/create_rift.dm @@ -9,6 +9,15 @@ to_chat(owner, span_warning("This place is not safe enough for a rift.")) return FALSE + if(T.contains_dense_objects()) + to_chat(owner, span_warning("There is not enough room for a rift here.")) + return FALSE + + var/area/A = T.loc + if(!(A.area_flags & BLOBS_ALLOWED)) + to_chat(owner, span_warning("We cannot create a rift here.")) + return FALSE + if(!T.can_flock_occupy()) to_chat(owner, span_warning("There is something blocking this spot.")) return FALSE diff --git a/code/modules/flockmind/actions/create_structure.dm b/code/modules/flockmind/actions/create_structure.dm index 023cc1c1cd98..7109fc41e552 100644 --- a/code/modules/flockmind/actions/create_structure.dm +++ b/code/modules/flockmind/actions/create_structure.dm @@ -34,11 +34,16 @@ continue options[unlockable.name] = unlockable - var/desired = tgui_input_list(ghost_bird, "Select a structure to create. Compute upkeep costs are provided in parenthesis.", "Tealprint Selection", options) + var/datum/flock_unlockable/desired = tgui_input_list(ghost_bird, "Select a structure to create. Bandwidth upkeep costs are provided in parenthesis.", "Tealprint Selection", options) desired = options[desired] + if(isnull(desired)) return - F.create_structure(loc, desired) + F.refresh_unlockables() + if(!desired.unlocked) + return + + F.create_structure(loc, desired.structure_type) return ..() diff --git a/code/modules/flockmind/actions/deconstruct.dm b/code/modules/flockmind/actions/deconstruct.dm new file mode 100644 index 000000000000..0c6d409e114d --- /dev/null +++ b/code/modules/flockmind/actions/deconstruct.dm @@ -0,0 +1,49 @@ +/datum/action/cooldown/flock/deconstruct + name = "Deconstruct" + click_to_activate = TRUE + check_flags = AB_CHECK_CONSCIOUS + render_button = FALSE + +/datum/action/cooldown/flock/deconstruct/is_valid_target(atom/cast_on) + var/turf/clicked_atom_turf = get_turf(cast_on) + if(!clicked_atom_turf) + return FALSE + + // This breaks pathfinding :( + // if(!clicked_atom_turf.IsReachableBy(owner)) + // return FALSE + + // So instead: + if(get_dist(owner, clicked_atom_turf) > 1) + return FALSE + + if(ismob(cast_on) || !HAS_TRAIT(cast_on, TRAIT_FLOCK_THING) || HAS_TRAIT(cast_on, TRAIT_FLOCK_NODECON)) + return FALSE + + return TRUE + +/datum/action/cooldown/flock/deconstruct/Activate(atom/target) + . = ..() + var/mob/living/simple_animal/flock/bird = owner + astype(bird, /mob/living/simple_animal/flock/drone)?.stop_flockphase(TRUE) + bird.face_atom(target) + + bird.visible_message(span_notice("[bird] begins to deconstruct [target]."), blind_message = span_hear("You hear an otherwordly whirring.")) + if(!do_after(bird, target, 6 SECONDS, DO_PUBLIC | DO_RESTRICT_USER_DIR_CHANGE, interaction_key = "flock_deconstruct")) + return FALSE + + if(!is_valid_target(target)) + return FALSE + + // Structures + if(isobj(target)) + var/obj/obj_target = target + obj_target.deconstruct(TRUE) + return TRUE + + else if(isflockturf(target)) + var/turf/turf = target + turf.ChangeTurf(/turf/open/floor/flock, flags = CHANGETURF_INHERIT_AIR) + else + CRASH("Tried to flock deconstruct incompatible object of type: [target.type]") + return TRUE diff --git a/code/modules/flockmind/actions/deposit.dm b/code/modules/flockmind/actions/deposit.dm new file mode 100644 index 000000000000..fb5118ff3052 --- /dev/null +++ b/code/modules/flockmind/actions/deposit.dm @@ -0,0 +1,26 @@ +/datum/action/cooldown/flock/deposit + name = "Deposit" + click_to_activate = TRUE + check_flags = AB_CHECK_CONSCIOUS + render_button = FALSE + +/datum/action/cooldown/flock/deposit/is_valid_target(atom/cast_on) + return istype(cast_on, /obj/structure/flock/tealprint) + +/datum/action/cooldown/flock/deposit/Activate(atom/target) + . = ..() + var/mob/living/simple_animal/flock/bird = owner + astype(bird, /mob/living/simple_animal/flock/drone)?.stop_flockphase(TRUE) + bird.face_atom(target) + + playsound(target, 'goon/sounds/flockmind/flockdrone_quickbuild.ogg', 50, TRUE) + + while(do_after(bird, target, 1 SECONDS, DO_PUBLIC | DO_RESTRICT_USER_DIR_CHANGE, interaction_key = "flock_deposit")) + bird.visible_message(span_notice("[bird] deposits materials into [target]."), blind_message = span_hear("You hear an otherwordly whirring.")) + + var/obj/structure/flock/tealprint/tealprint = target + var/deposit = min(bird.substrate.has_points(), tealprint.substrate.how_empty(), FLOCK_SUBSTRATE_COST_DEPOST_TEALPRINT) + bird.substrate.remove_points(deposit) + tealprint.substrate.add_points(deposit) + return TRUE + diff --git a/code/modules/flockmind/actions/designate_deconstruct.dm b/code/modules/flockmind/actions/designate_deconstruct.dm new file mode 100644 index 000000000000..1a89120830df --- /dev/null +++ b/code/modules/flockmind/actions/designate_deconstruct.dm @@ -0,0 +1,18 @@ +/datum/action/cooldown/flock/designate_deconstruct + name = "Mark for Deconstruction" + desc = "Mark an existing flock structure for deconstruction, refunding some resources." + button_icon_state = "destroystructure" + + click_to_activate = TRUE + unset_after_click = FALSE + click_cd_override = 0 + +/datum/action/cooldown/flock/designate_deconstruct/Activate(atom/target) + . = ..() + + if(istype(target, /obj/structure/flock/tealprint)) + var/obj/structure/flock/tealprint/tealprint = target + return tealprint.try_cancel_structure() + + var/mob/camera/flock/ghost_bird = owner + return ghost_bird.flock.toggle_deconstruction_mark(target) diff --git a/code/modules/flockmind/actions/designate_turf.dm b/code/modules/flockmind/actions/designate_turf.dm index 9cb80543860b..85a5c46036da 100644 --- a/code/modules/flockmind/actions/designate_turf.dm +++ b/code/modules/flockmind/actions/designate_turf.dm @@ -1,6 +1,6 @@ /datum/action/cooldown/flock/designate_tile name = "Designate Priority Tile" - desc = "Add or remove a tile to the urgent tiles the flock should claim." + desc = "Add or remove a tile to the urgent tiles the Flock should claim." button_icon_state = "designate_tile" click_to_activate = TRUE @@ -22,16 +22,16 @@ return FALSE var/mob/camera/flock/ghost_bird = owner - if(!ghost_bird.flock.marked_for_deconstruction[turf_target]) + if(!ghost_bird.flock.marked_for_conversion[turf_target]) if(ghost_bird.flock.turf_reservations[turf_target]) to_chat(ghost_bird, span_alert("That tile is already scheduled for conversion.")) return FALSE - ghost_bird.flock.marked_for_deconstruction[turf_target] = TRUE + ghost_bird.flock.marked_for_conversion[turf_target] = TRUE ghost_bird.flock.add_notice(turf_target, FLOCK_NOTICE_PRIORITY) return TRUE else - ghost_bird.flock.marked_for_deconstruction -= turf_target + ghost_bird.flock.marked_for_conversion -= turf_target ghost_bird.flock.remove_notice(turf_target, FLOCK_NOTICE_PRIORITY) return TRUE diff --git a/code/modules/flockmind/actions/diffract_drone.dm b/code/modules/flockmind/actions/diffract_drone.dm index 59e1b632df8f..c442d1c994e2 100644 --- a/code/modules/flockmind/actions/diffract_drone.dm +++ b/code/modules/flockmind/actions/diffract_drone.dm @@ -4,7 +4,6 @@ button_icon_state = "diffract" click_to_activate = TRUE - /datum/action/cooldown/flock/diffract_drone/is_valid_target(atom/cast_on) if(!isflockdrone(cast_on)) return FALSE diff --git a/code/modules/flockmind/actions/gatecrash.dm b/code/modules/flockmind/actions/gatecrash.dm index e2e298bd48a8..74494dc4622e 100644 --- a/code/modules/flockmind/actions/gatecrash.dm +++ b/code/modules/flockmind/actions/gatecrash.dm @@ -1,5 +1,6 @@ /datum/action/cooldown/flock/gatecrash name = "Gatecrash" + desc = "Transmit an override signal to open every door within range." button_icon_state = "open_door" cooldown_time = 40 SECONDS diff --git a/code/modules/flockmind/actions/heal.dm b/code/modules/flockmind/actions/heal.dm index 6249ee69b19b..6433c04e109b 100644 --- a/code/modules/flockmind/actions/heal.dm +++ b/code/modules/flockmind/actions/heal.dm @@ -1,15 +1,25 @@ /datum/action/cooldown/flock/flock_heal name = "Repair" + render_button = FALSE click_to_activate = TRUE /datum/action/cooldown/flock/flock_heal/Activate(atom/target) + var/mob/living/simple_animal/flock/drone/this_bird = owner + if(!this_bird.substrate.has_points(FLOCK_SUBSTRATE_COST_REPAIR)) + return FALSE + + astype(this_bird, /mob/living/simple_animal/flock/drone)?.stop_flockphase(TRUE) + if(isflockmob(target)) var/mob/living/simple_animal/flock/bird = target - ADD_TRAIT(bird, TRAIT_AI_PAUSED, ref(owner)) + ADD_TRAIT(bird, TRAIT_AI_PAUSED, ref(src)) + if(!do_after(owner, target, 1 SECOND, DO_PUBLIC, interaction_key = "flock_repair")) - REMOVE_TRAIT(bird, TRAIT_AI_PAUSED, ref(owner)) + REMOVE_TRAIT(bird, TRAIT_AI_PAUSED, ref(src)) return FALSE - REMOVE_TRAIT(bird, TRAIT_AI_PAUSED, ref(owner)) + + REMOVE_TRAIT(bird, TRAIT_AI_PAUSED, ref(src)) bird.heal_overall_damage(10, 10) + this_bird.substrate.remove_points(FLOCK_SUBSTRATE_COST_REPAIR) ..() return TRUE diff --git a/code/modules/flockmind/actions/nest.dm b/code/modules/flockmind/actions/nest.dm new file mode 100644 index 000000000000..0ef6691faf96 --- /dev/null +++ b/code/modules/flockmind/actions/nest.dm @@ -0,0 +1,43 @@ +/datum/action/cooldown/flock/nest + name = "Lay Egg" + button_icon_state = "spawn_egg" + check_flags = AB_CHECK_CONSCIOUS + +/datum/action/cooldown/flock/nest/is_valid_target(atom/cast_on) + var/mob/living/simple_animal/flock/drone/bird = owner + var/turf/open/floor/flock/flockfloor = get_turf(cast_on) + if(!istype(flockfloor) || flockfloor.is_blocked_turf(source_atom = owner)) + to_chat(owner, span_warning("The egg must be placed on an open flock tile.")) + return FALSE + + if(!bird.flock) + to_chat(owner, span_warning("Our prime directives prohibit us from synthesizing an egg.")) + return FALSE + + if(!bird.substrate.has_points(bird.flock.current_egg_cost)) + to_chat(owner, span_warning("Error: Not enough resources. (We need [bird.flock.current_egg_cost])")) + return FALSE + + if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE)) + to_chat(owner, span_warning("We can not synthesize eggs while flockrunning.")) + return FALSE + + return TRUE + +/datum/action/cooldown/flock/nest/Activate(atom/target) + . = ..() + var/mob/living/simple_animal/flock/drone/bird = owner + var/turf/open/floor/flock/flockfloor = get_turf(target) + bird.stop_flockphase(TRUE) + + to_chat(bird, span_notice("Our internal fabricators spring into action, we must hold still.")) + + if(!do_after(bird, bird, 8 SECONDS, DO_PUBLIC, interaction_key = "flock_lay_egg", action_type = /datum/timed_action/flock_lay_egg)) + return FALSE + + bird.substrate.remove_points(bird.flock.current_egg_cost) + bird.visible_message(span_notice("[bird] deploys some sort of device.")) + playsound(bird, 'goon/sounds/Metal_Clang_1.ogg', 30, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE) + + new /obj/structure/flock/egg(flockfloor) + return TRUE diff --git a/code/modules/flockmind/actions/partition_mind.dm b/code/modules/flockmind/actions/partition_mind.dm index 972b007b9dfa..3e24205c4f24 100644 --- a/code/modules/flockmind/actions/partition_mind.dm +++ b/code/modules/flockmind/actions/partition_mind.dm @@ -5,11 +5,16 @@ var/awaiting_partition = FALSE +/datum/action/cooldown/flock/partition_mind/New() + ..() + desc = "Divide our computational power, creating a Flocktrace. Requires [FLOCK_COMPUTE_COST_FLOCKTRACE] total bandwidth per trace." + /datum/action/cooldown/flock/partition_mind/is_valid_target(atom/cast_on) + var/mob/camera/flock/overmind/ghost_bird = owner if(awaiting_partition) + to_chat(ghost_bird, span_warning("We are currently partitioning.")) return FALSE - var/mob/camera/flock/overmind/ghost_bird = owner if(!ghost_bird.flock.can_afford(FLOCK_COMPUTE_COST_FLOCKTRACE)) to_chat(ghost_bird, span_warning("The Flock does not have enough spare computaional power to support another thread.")) return FALSE @@ -50,12 +55,13 @@ if(!ghost_bird.flock.can_afford(FLOCK_COMPUTE_COST_FLOCKTRACE)) message_admins("The Flock was unable to support another flocktrace, partition aborted.") - to_chat(ghost_bird, span_flocksay("Partition failure: compute required is unavailable.")) + to_chat(ghost_bird, span_flocksay("Partition failure: bandwidth required is unavailable.")) return var/mob/candidate = pick(candidates) message_admins("[key_name_admin(candidate)] respawned as a Flocktrace.") - var/mob/camera/flock/new_ghostbird = new(get_turf(ghost_bird), ghost_bird.flock) + var/mob/camera/flock/trace/new_ghostbird = new(get_turf(ghost_bird), ghost_bird.flock) new_ghostbird.PossessByPlayer(candidate.key) + new_ghostbird.mind.add_antag_datum(/datum/antagonist/flock) diff --git a/code/modules/flockmind/actions/ping.dm b/code/modules/flockmind/actions/ping.dm index 1a985373f411..79c6dda76078 100644 --- a/code/modules/flockmind/actions/ping.dm +++ b/code/modules/flockmind/actions/ping.dm @@ -1,6 +1,6 @@ /datum/action/cooldown/flock/ping name = "Ping" - desc = "Request attention from other elements of the flock." + desc = "Request attention from other partitions of the Flock." button_icon_state = "ping" cooldown_time = 5 SECONDS click_to_activate = TRUE diff --git a/code/modules/flockmind/actions/release_control.dm b/code/modules/flockmind/actions/release_control.dm new file mode 100644 index 000000000000..8f12228ef60a --- /dev/null +++ b/code/modules/flockmind/actions/release_control.dm @@ -0,0 +1,13 @@ +/datum/action/cooldown/flock/release_control + name = "Release Control" + desc = "Release yourself from this drone." + button_icon_state = "eject" + +/datum/action/cooldown/flock/release_control/Activate(atom/target) + . = ..() + var/mob/living/simple_animal/flock/drone/bird = owner + if(!bird.flock) + to_chat(bird, span_warning("You have no flock to return to.")) + return + + bird.release_control(TRUE) diff --git a/code/modules/flockmind/ai_behaviors/_flock_behavior_base.dm b/code/modules/flockmind/ai_behaviors/_flock_behavior_base.dm index 6588e917b6cb..53d2c6e61fd2 100644 --- a/code/modules/flockmind/ai_behaviors/_flock_behavior_base.dm +++ b/code/modules/flockmind/ai_behaviors/_flock_behavior_base.dm @@ -2,8 +2,18 @@ /// The name of the behavior in the UI for flock drones. var/name = "" -/datum/ai_behavior/flock/finish_action(datum/ai_controller/controller, succeeded, ...) +/datum/ai_behavior/flock/perform(delta_time, datum/ai_controller/controller, ...) + SHOULD_CALL_PARENT(TRUE) . = ..() + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + bird.set_task_desc(name) + +/datum/ai_behavior/flock/finish_action(datum/ai_controller/controller, succeeded, ...) + var/mob/living/simple_animal/flock/drone/bird = controller.pawn if(HAS_TRAIT(controller.pawn, TRAIT_FLOCKPHASE)) - var/mob/living/simple_animal/flock/drone/bird = controller.pawn - bird.stop_flockphase() + bird.stop_flockphase(FALSE) + + . = ..() + + bird.set_task_desc("") + diff --git a/code/modules/flockmind/ai_behaviors/flock_capture.dm b/code/modules/flockmind/ai_behaviors/flock_capture.dm new file mode 100644 index 000000000000..9ff639e4d05c --- /dev/null +++ b/code/modules/flockmind/ai_behaviors/flock_capture.dm @@ -0,0 +1,82 @@ +/datum/ai_behavior/flock/find_capture_target + name = "capturing" + goap_weight = FLOCK_BEHAVIOR_WEIGHT_CAPTURE + +/datum/ai_behavior/flock/find_capture_target/setup(datum/ai_controller/controller, mob/overmind_target) + . = ..() + if(overmind_target) + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + if(isturf(overmind_target.loc)) + controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE) + controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200) + bird.say("instruction confirmed: capture biological lifeform") + else + bird.say("invalid capture target provided by sentient-level instruction") + return FALSE + +/datum/ai_behavior/flock/find_capture_target/goap_precondition(datum/ai_controller/controller) + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + return length(bird.flock?.enemies) + +/datum/ai_behavior/flock/find_capture_target/goap_get_potential_targets(datum/ai_controller/controller) + var/mob/living/simple_animal/flock/bird = controller.pawn + return bird.flock.enemies.Copy() + +/datum/ai_behavior/flock/find_capture_target/goap_is_valid_target(datum/ai_controller/controller, atom/target) + var/mob/living/target_mob = target + return ismob(target_mob) && isturf(target_mob.loc) && (target_mob.incapacitated(IGNORE_STASIS | IGNORE_GRAB | IGNORE_RESTRAINTS) || target_mob.IsKnockdown()) + +/datum/ai_behavior/flock/find_capture_target/perform(delta_time, datum/ai_controller/controller, mob/overmind_target) + ..() + var/atom/target = overmind_target || goap_get_ideal_target(controller, set_path = TRUE) + if(!target) + return BEHAVIOR_PERFORM_FAILURE + + controller.set_blackboard_key(BB_FLOCK_CAPTURE_TARGET, target) + controller.set_move_target(target) + return BEHAVIOR_PERFORM_SUCCESS + +/datum/ai_behavior/flock/find_capture_target/finish_action(datum/ai_controller/controller, succeeded, obj/item/overmind_target) + . = ..() + controller.clear_blackboard_key(BB_PATH_TO_USE) + + if(!succeeded && overmind_target) + controller.clear_blackboard_key(BB_PATH_MAX_LENGTH) + controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL) + +/datum/ai_behavior/flock/find_capture_target/next_behavior(datum/ai_controller/controller, success) + if(success) + controller.queue_behavior(/datum/ai_behavior/flock/perform_capture) + +/datum/ai_behavior/flock/perform_capture + name = "capturing" + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH + +/datum/ai_behavior/flock/perform_capture/perform(delta_time, datum/ai_controller/controller, ...) + ..() + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + var/mob/living/target = controller.blackboard[BB_FLOCK_CAPTURE_TARGET] + if(target) + if(!isturf(target?.loc) || !(target.incapacitated(IGNORE_GRAB | IGNORE_RESTRAINTS | IGNORE_STASIS) || target.IsKnockdown())) + return BEHAVIOR_PERFORM_FAILURE + controller.clear_blackboard_key(BB_FLOCK_CAPTURE_TARGET) + var/datum/action/cooldown/flock/cage_mob/cage_action = locate() in bird.actions + spawn(-1) + cage_action.Trigger(target = target) + + if(DOING_INTERACTION(bird, "flock_cage")) + return BEHAVIOR_PERFORM_COOLDOWN + + return BEHAVIOR_PERFORM_SUCCESS + +/datum/ai_behavior/flock/perform_capture/finish_action(datum/ai_controller/controller, succeeded, ...) + . = ..() + controller.clear_blackboard_key(BB_FLOCK_CAPTURE_TARGET) + + + if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn)) + var/mob/living/simple_animal/flock/bird = controller.pawn + bird.say("unable to reach target provided by sentient level instruction, aborting subroutine", forced = "overmind control action cancelled") + + controller.clear_blackboard_key(BB_PATH_MAX_LENGTH) + controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL) diff --git a/code/modules/flockmind/ai_behaviors/flock_convert.dm b/code/modules/flockmind/ai_behaviors/flock_convert.dm index e2683db6e8ec..d2e2d4b26f37 100644 --- a/code/modules/flockmind/ai_behaviors/flock_convert.dm +++ b/code/modules/flockmind/ai_behaviors/flock_convert.dm @@ -1,24 +1,27 @@ /datum/ai_behavior/flock/find_conversion_target name = "building" + goap_weight = FLOCK_BEHAVIOR_WEIGHT_CONVERT /datum/ai_behavior/flock/find_conversion_target/setup(datum/ai_controller/controller, turf/overmind_target) . = ..() if(overmind_target) - if(is_valid_target(overmind_target)) + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + if(goap_is_valid_target(controller, overmind_target)) controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE) controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200) + bird.say("instruction confirmed: convert object to substrate") else - var/mob/living/simple_animal/flock/drone/bird = controller.pawn - bird.say("Invalid conversion target provided by sentient level instruction.") + bird.say("invalid conversion target provided by sentient-level instruction") return FALSE -/datum/ai_behavior/flock/find_conversion_target/score(datum/ai_controller/controller) - return score_distance(controller, get_target(controller)) +/datum/ai_behavior/flock/find_conversion_target/goap_precondition(datum/ai_controller/controller) + var/mob/living/simple_animal/flock/bird = controller.pawn + return bird.substrate.has_points(FLOCK_SUBSTRATE_COST_CONVERT) /datum/ai_behavior/flock/find_conversion_target/score_distance(datum/ai_controller/controller, atom/target) . = ..() var/mob/living/simple_animal/flock/bird = controller.pawn - if(bird.flock?.marked_for_deconstruction[target]) + if(bird.flock?.marked_for_conversion[target]) /* * because the result of scoring is based on max distance, * the score of any given tile is -100 to 0, with 0 being best. @@ -26,36 +29,41 @@ */ . += 200 -/datum/ai_behavior/flock/find_conversion_target/proc/get_target(datum/ai_controller/controller) +/datum/ai_behavior/flock/find_conversion_target/goap_get_potential_targets(datum/ai_controller/controller) var/mob/living/simple_animal/flock/bird = controller.pawn var/datum/flock/bird_flock = bird.flock - var/list/options = list() + var/list/options = view(controller.target_search_radius, bird) var/list/priority_turfs = bird_flock?.get_priority_turfs() if(length(priority_turfs)) options += priority_turfs - var/list/turfs = spiral_range_turfs(controller.target_search_radius, bird) & view(controller.target_search_radius, bird) - for(var/turf/T in turfs) - if(is_valid_target(T, bird_flock)) - options += T + for(var/turf/T as turf in view(controller.target_search_radius, bird)) + options += T - return get_best_target_by_distance_score(controller, options) + return options -/datum/ai_behavior/flock/find_conversion_target/proc/is_valid_target(turf/T, datum/flock/bird_flock) - if(isflockturf(T)) +/datum/ai_behavior/flock/find_conversion_target/goap_is_valid_target(datum/ai_controller/controller, atom/target) + var/turf/T = target + if(!isturf(T)) return FALSE - if(!T.can_flock_convert()) - return FALSE - if(isnull(bird_flock)) + var/mob/living/simple_animal/flock/bird = controller.pawn + if(isnull(bird.flock)) return TRUE - return bird_flock.is_turf_free(T) + if(isflockturf(T) && (bird.flock.claimed_floors[T] || bird.flock.claimed_walls[T])) + return FALSE + + if(!T.can_flock_convert()) + return FALSE + + return bird.flock.is_turf_free(T) /datum/ai_behavior/flock/find_conversion_target/perform(delta_time, datum/ai_controller/controller, turf/overmind_target) - var/turf/target = overmind_target || get_target(controller) + ..() + var/turf/target = overmind_target || goap_get_ideal_target(controller, TRUE) if(!target) return BEHAVIOR_PERFORM_FAILURE @@ -70,6 +78,7 @@ /datum/ai_behavior/flock/find_conversion_target/finish_action(datum/ai_controller/controller, succeeded, turf/overmind_target) . = ..() + controller.clear_blackboard_key(BB_PATH_TO_USE) if(!succeeded && overmind_target) controller.clear_blackboard_key(BB_PATH_MAX_LENGTH) controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL) @@ -83,6 +92,7 @@ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT /datum/ai_behavior/flock/perform_conversion/perform(delta_time, datum/ai_controller/controller, ...) + ..() var/mob/living/simple_animal/flock/bird = controller.pawn var/turf/target = controller.blackboard[BB_FLOCK_CONVERT_TARGET] if(target) @@ -101,10 +111,42 @@ var/mob/living/simple_animal/flock/drone/bird = controller.pawn bird.flock?.free_turf(bird) + if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn)) + bird.say("unable to reach target provided by sentient level instruction, aborting subroutine", forced = "overmind control action cancelled") + controller.clear_blackboard_key(BB_FLOCK_CONVERT_TARGET) controller.clear_blackboard_key(BB_PATH_MAX_LENGTH) controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL) - if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn)) - bird.say("Unable to reach target provided by sentient level instruction, aborting subroutine.", forced = "overmind control action cancelled") +// +// Subtype for creating a nest. Effectively a higher priority convert that only happens when the bird can lay an egg. +// + +/datum/ai_behavior/flock/find_conversion_target/nest + name = "nesting" + goap_weight = FLOCK_BEHAVIOR_WEIGHT_NEST + required_distance = 0 + +/datum/ai_behavior/flock/find_conversion_target/nest/goap_precondition(datum/ai_controller/controller) + var/mob/living/simple_animal/flock/bird = controller.pawn + if(!bird.flock) + return FALSE + + if(length(bird.flock.drones) > FLOCK_DRONE_LIMIT) + return FALSE + + return bird.substrate.has_points(FLOCK_SUBSTRATE_COST_CONVERT + bird.flock.current_egg_cost) + +/datum/ai_behavior/flock/find_conversion_target/nest/goap_is_valid_target(datum/ai_controller/controller, atom/target) + var/turf/T = target + return ..() && !T.is_blocked_turf(exclude_mobs = TRUE) && !locate(/obj/structure/flock/egg, T) + +/datum/ai_behavior/flock/perform_conversion/nest + name = "nesting" + required_distance = 0 + +/datum/ai_behavior/flock/perform_conversion/nest/next_behavior(datum/ai_controller/controller, success) + . = ..() + if(success) + controller.queue_behavior(/datum/ai_behavior/flock/find_existing_nest) diff --git a/code/modules/flockmind/ai_behaviors/flock_deconstruct.dm b/code/modules/flockmind/ai_behaviors/flock_deconstruct.dm new file mode 100644 index 000000000000..ca86d2e8f1b4 --- /dev/null +++ b/code/modules/flockmind/ai_behaviors/flock_deconstruct.dm @@ -0,0 +1,110 @@ +/datum/ai_behavior/flock/find_deconstruct_target + name = "deconstructing" + goap_weight = FLOCK_BEHAVIOR_WEIGHT_DECONSTRUCT + +/datum/ai_behavior/flock/find_deconstruct_target/setup(datum/ai_controller/controller, turf/overmind_target) + . = ..() + if(overmind_target) + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + if(is_valid_target(overmind_target)) + controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE) + controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200) + bird.say("instruction confirmed: deconstruct object") + else + bird.say("invalid deconstruct target provided by sentient-level instruction") + return FALSE + +/datum/ai_behavior/flock/find_deconstruct_target/goap_precondition(datum/ai_controller/controller) + var/mob/living/simple_animal/flock/bird = controller.pawn + return length(bird.flock?.marked_for_deconstruction) + +/datum/ai_behavior/flock/find_deconstruct_target/goap_score(datum/ai_controller/controller) + return score_distance(controller, get_target(controller)) + +/datum/ai_behavior/flock/find_deconstruct_target/score_distance(datum/ai_controller/controller, atom/target) + . = ..() + var/mob/living/simple_animal/flock/bird = controller.pawn + if(bird.flock?.marked_for_deconstruction[target]) + /* + * because the result of scoring is based on max distance, + * the score of any given tile is -100 to 0, with 0 being best. + * Adding 200 basically allows a tile at twice the max distance to be considered. + */ + . += 200 + +/datum/ai_behavior/flock/find_deconstruct_target/proc/get_target(datum/ai_controller/controller, path_to = FALSE) + var/mob/living/simple_animal/flock/bird = controller.pawn + + var/list/options = list() + for(var/atom/A as anything in bird.flock.marked_for_deconstruction) + if(get_dist(A, bird) > controller.max_target_distance) + continue + + if(bird.flock && !bird.flock.is_turf_free(get_turf(A))) + continue + + options += A + + return get_best_target_by_distance_score(controller, options, path_to) + +/datum/ai_behavior/flock/find_deconstruct_target/proc/is_valid_target(atom/target) + return !ismob(target) && HAS_TRAIT(target, TRAIT_FLOCK_THING) && !HAS_TRAIT(target, TRAIT_FLOCK_NODECON) + +/datum/ai_behavior/flock/find_deconstruct_target/perform(delta_time, datum/ai_controller/controller, turf/overmind_target) + ..() + var/atom/target = overmind_target || get_target(controller, TRUE) + if(!target) + return BEHAVIOR_PERFORM_FAILURE + + controller.set_blackboard_key(BB_FLOCK_DECON_TARGET, target) + controller.set_move_target(target) + + var/mob/living/simple_animal/flock/bird = controller.pawn + if(bird.flock) + bird.flock.reserve_turf(bird, get_turf(target), remove_on_change = FALSE) + + return BEHAVIOR_PERFORM_SUCCESS + +/datum/ai_behavior/flock/find_deconstruct_target/finish_action(datum/ai_controller/controller, succeeded, turf/overmind_target) + . = ..() + controller.clear_blackboard_key(BB_PATH_TO_USE) + + if(!succeeded && overmind_target) + controller.clear_blackboard_key(BB_PATH_MAX_LENGTH) + controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL) + +/datum/ai_behavior/flock/find_deconstruct_target/next_behavior(datum/ai_controller/controller, success) + if(success) + controller.queue_behavior(/datum/ai_behavior/flock/perform_deconstruct) + +/datum/ai_behavior/flock/perform_deconstruct + name = "deconstructing" + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/flock/perform_deconstruct/perform(delta_time, datum/ai_controller/controller, ...) + ..() + var/mob/living/simple_animal/flock/bird = controller.pawn + var/turf/target = controller.blackboard[BB_FLOCK_DECON_TARGET] + if(target) + controller.clear_blackboard_key(BB_FLOCK_DECON_TARGET) + var/datum/action/cooldown/flock/deconstruct/deconstruct_action = locate() in bird.actions + spawn(-1) + deconstruct_action.Trigger(target = target) + + if(DOING_INTERACTION(bird, "flock_deconstruct")) + return BEHAVIOR_PERFORM_COOLDOWN + + return BEHAVIOR_PERFORM_SUCCESS + +/datum/ai_behavior/flock/perform_deconstruct/finish_action(datum/ai_controller/controller, succeeded, ...) + . = ..() + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + bird.flock?.free_turf(bird) + + if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn)) + bird.say("unable to reach target provided by sentient level instruction, aborting subroutine", forced = "overmind control action cancelled") + + controller.clear_blackboard_key(BB_FLOCK_DECON_TARGET) + controller.clear_blackboard_key(BB_PATH_MAX_LENGTH) + controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL) + diff --git a/code/modules/flockmind/ai_behaviors/flock_deposit.dm b/code/modules/flockmind/ai_behaviors/flock_deposit.dm new file mode 100644 index 000000000000..e524197be880 --- /dev/null +++ b/code/modules/flockmind/ai_behaviors/flock_deposit.dm @@ -0,0 +1,97 @@ +/datum/ai_behavior/flock/find_deposit_target + name = "depositing" + goap_weight = FLOCK_BEHAVIOR_WEIGHT_DEPOSIT + +/datum/ai_behavior/flock/find_deposit_target/setup(datum/ai_controller/controller, obj/structure/flock/overmind_target) + . = ..() + if(overmind_target) + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + if(goap_is_valid_target(controller, overmind_target)) + controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE) + controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200) + bird.say("instruction confirmed: deposit substrate") + else + bird.say("invalid deposit target provided by sentient-level instruction") + return FALSE + +/datum/ai_behavior/flock/find_deposit_target/goap_precondition(datum/ai_controller/controller) + var/mob/living/simple_animal/flock/bird = controller.pawn + return bird.substrate.has_points(FLOCK_SUBSTRATE_COST_DEPOST_TEALPRINT) + +/datum/ai_behavior/flock/find_deposit_target/goap_get_potential_targets(datum/ai_controller/controller) + var/list/options = ..() + for(var/obj/structure/flock/tealprint in oview(controller.target_search_radius, controller.pawn)) + options += tealprint + return options + +/datum/ai_behavior/flock/find_deposit_target/goap_is_valid_target(datum/ai_controller/controller, atom/target) + var/obj/structure/flock/tealprint/tealprint = target + if(!istype(tealprint)) + return FALSE + + var/mob/living/simple_animal/flock/bird = controller.pawn + return (tealprint.flock == bird.flock) && !tealprint.substrate.is_full() + +/datum/ai_behavior/flock/find_deposit_target/perform(delta_time, datum/ai_controller/controller, overmind_target) + ..() + var/atom/target = overmind_target || goap_get_ideal_target(controller, set_path = TRUE) + if(!target) + return BEHAVIOR_PERFORM_FAILURE + + controller.set_blackboard_key(BB_FLOCK_DEPOSIT_TARGET, target) + controller.set_move_target(target) + return BEHAVIOR_PERFORM_SUCCESS + +/datum/ai_behavior/flock/find_deposit_target/finish_action(datum/ai_controller/controller, succeeded, overmind_target) + . = ..() + controller.clear_blackboard_key(BB_PATH_TO_USE) + + if(!succeeded && overmind_target) + controller.clear_blackboard_key(BB_PATH_MAX_LENGTH) + controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL) + +/datum/ai_behavior/flock/find_deposit_target/next_behavior(datum/ai_controller/controller, success) + if(success) + controller.queue_behavior(/datum/ai_behavior/flock/perform_deposit) + +/datum/ai_behavior/flock/perform_deposit + name = "depositing" + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH + +/datum/ai_behavior/flock/perform_deposit/goap_is_valid_target(datum/ai_controller/controller, atom/target) + var/obj/structure/flock/tealprint/tealprint = target + if(!istype(tealprint)) + return FALSE + + var/mob/living/simple_animal/flock/bird = controller.pawn + return (tealprint.flock == bird.flock) && !tealprint.substrate.is_full() + +/datum/ai_behavior/flock/perform_deposit/perform(delta_time, datum/ai_controller/controller, ...) + ..() + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + var/obj/structure/flock/tealprint/target = controller.blackboard[BB_FLOCK_DEPOSIT_TARGET] + if(target) + if(!goap_is_valid_target(controller, target)) + return BEHAVIOR_PERFORM_FAILURE + + controller.clear_blackboard_key(BB_FLOCK_DEPOSIT_TARGET) + + var/datum/action/cooldown/flock/deposit/deposit_action = locate() in bird.actions + spawn(-1) + deposit_action.Trigger(target = target) + + if(DOING_INTERACTION(bird, "flock_cage")) + return BEHAVIOR_PERFORM_COOLDOWN + + return BEHAVIOR_PERFORM_SUCCESS + +/datum/ai_behavior/flock/perform_deposit/finish_action(datum/ai_controller/controller, succeeded, ...) + . = ..() + controller.clear_blackboard_key(BB_FLOCK_DEPOSIT_TARGET) + + if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn)) + var/mob/living/simple_animal/flock/bird = controller.pawn + bird.say("unable to reach target provided by sentient level instruction, aborting subroutine", forced = "overmind control action cancelled") + + controller.clear_blackboard_key(BB_PATH_MAX_LENGTH) + controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL) diff --git a/code/modules/flockmind/ai_behaviors/flock_harvest.dm b/code/modules/flockmind/ai_behaviors/flock_harvest.dm new file mode 100644 index 000000000000..c8217026d24d --- /dev/null +++ b/code/modules/flockmind/ai_behaviors/flock_harvest.dm @@ -0,0 +1,84 @@ +/datum/ai_behavior/flock/find_harvest_target + name = "harvesting" + goap_weight = FLOCK_BEHAVIOR_WEIGHT_HARVEST + +/datum/ai_behavior/flock/find_harvest_target/setup(datum/ai_controller/controller, obj/item/overmind_target) + . = ..() + if(overmind_target) + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + if(isitem(overmind_target) && isturf(overmind_target.loc)) + controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE) + controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200) + bird.say("instruction confirmed: convert object to substrate") + else + bird.say("invalid harvest target provided by sentient-level instruction") + return FALSE + +/datum/ai_behavior/flock/find_harvest_target/goap_precondition(datum/ai_controller/controller) + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + var/datum/flockdrone_part/absorber/absorber = locate() in bird.parts + return !absorber.held_item + +/datum/ai_behavior/flock/find_harvest_target/goap_score(datum/ai_controller/controller) + return score_distance(controller, get_target(controller)) + +/datum/ai_behavior/flock/find_harvest_target/proc/get_target(datum/ai_controller/controller, path_to = FALSE) + var/mob/living/simple_animal/flock/bird = controller.pawn + + var/list/options = list() + for(var/obj/item/I in view(controller.target_search_radius, bird)) + if(isturf(I.loc) && !I.anchored) + options += I + + return get_best_target_by_distance_score(controller, options, path_to) + +/datum/ai_behavior/flock/find_harvest_target/perform(delta_time, datum/ai_controller/controller, obj/item/overmind_target) + ..() + var/atom/target = overmind_target || get_target(controller, TRUE) + if(!target) + return BEHAVIOR_PERFORM_FAILURE + + controller.set_blackboard_key(BB_FLOCK_HARVEST_TARGET, target) + controller.set_move_target(target) + return BEHAVIOR_PERFORM_SUCCESS + +/datum/ai_behavior/flock/find_harvest_target/finish_action(datum/ai_controller/controller, succeeded, obj/item/overmind_target) + . = ..() + controller.clear_blackboard_key(BB_PATH_TO_USE) + + if(!succeeded && overmind_target) + controller.clear_blackboard_key(BB_PATH_MAX_LENGTH) + controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL) + +/datum/ai_behavior/flock/find_harvest_target/next_behavior(datum/ai_controller/controller, success) + if(success) + controller.queue_behavior(/datum/ai_behavior/flock/perform_harvest) + +/datum/ai_behavior/flock/perform_harvest + name = "harvesting" + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH + +/datum/ai_behavior/flock/perform_harvest/perform(delta_time, datum/ai_controller/controller, ...) + ..() + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + var/obj/item/target = controller.blackboard[BB_FLOCK_HARVEST_TARGET] + if(!isturf(target?.loc)) + return BEHAVIOR_PERFORM_FAILURE + + var/datum/flockdrone_part/absorber/absorber = locate() in bird.parts + if(!absorber.try_pickup_item(target)) + return BEHAVIOR_PERFORM_FAILURE + + return BEHAVIOR_PERFORM_SUCCESS + +/datum/ai_behavior/flock/perform_harvest/finish_action(datum/ai_controller/controller, succeeded, ...) + . = ..() + controller.clear_blackboard_key(BB_FLOCK_HARVEST_TARGET) + + + if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn)) + var/mob/living/simple_animal/flock/bird = controller.pawn + bird.say("unable to reach target provided by sentient level instruction, aborting subroutine", forced = "overmind control action cancelled") + + controller.clear_blackboard_key(BB_PATH_MAX_LENGTH) + controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL) diff --git a/code/modules/flockmind/ai_behaviors/flock_heal.dm b/code/modules/flockmind/ai_behaviors/flock_heal.dm index 8a3253d4feba..d44f5076525a 100644 --- a/code/modules/flockmind/ai_behaviors/flock_heal.dm +++ b/code/modules/flockmind/ai_behaviors/flock_heal.dm @@ -1,27 +1,44 @@ /datum/ai_behavior/flock/find_heal_target name = "repairing" + goap_weight = FLOCK_BEHAVIOR_WEIGHT_REPAIR -/datum/ai_behavior/flock/find_heal_target/score(datum/ai_controller/controller) +/datum/ai_behavior/flock/find_heal_target/setup(datum/ai_controller/controller, mob/living/simple_animal/flock/overmind_target) + . = ..() + if(overmind_target) + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + if(goap_is_valid_target(controller, overmind_target)) + controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE) + controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200) + bird.say("instruction confirmed: repair construct") + else + bird.say("invalid repair target provided by sentient-level instruction") + return FALSE + +/datum/ai_behavior/flock/find_heal_target/goap_precondition(datum/ai_controller/controller) var/mob/living/simple_animal/flock/drone/bird = controller.pawn - if(!bird.flock) - return -INFINITY - return 4 * score_distance(controller, get_best_target_by_distance_score(controller, get_targets(controller))) + return bird.flock && bird.substrate.has_points(FLOCK_SUBSTRATE_COST_REPAIR) -/datum/ai_behavior/flock/find_heal_target/proc/get_targets(datum/ai_controller/controller) - . = list() - var/mob/living/simple_animal/flock/drone/this_bird = controller.pawn +/datum/ai_behavior/flock/find_heal_target/goap_get_potential_targets(datum/ai_controller/controller) + var/list/options = ..() for(var/mob/living/simple_animal/flock/drone/bird in oview(controller.target_search_radius, controller.pawn)) - if(bird.flock != this_bird.flock) - continue - if(bird.stat == DEAD) - continue + options += bird + return options - // Birds at or below 60% health - if((bird.getBruteLoss() + bird.getFireLoss()) / bird.maxHealth >= 0.4) - . += bird +/datum/ai_behavior/flock/find_heal_target/goap_is_valid_target(datum/ai_controller/controller, atom/target) + var/mob/living/simple_animal/flock/drone/this_bird = controller.pawn + var/mob/living/simple_animal/flock/drone/other_bird = target + if(this_bird.flock != other_bird.flock) + return FALSE + if(other_bird.stat == DEAD) + return FALSE -/datum/ai_behavior/flock/find_heal_target/perform(delta_time, datum/ai_controller/controller, ...) - var/target = get_best_target_by_distance_score(controller, get_targets(controller)) + // Birds at or below 60% health + if((other_bird.getBruteLoss() + other_bird.getFireLoss()) / other_bird.maxHealth >= 0.4) + return TRUE + +/datum/ai_behavior/flock/find_heal_target/perform(delta_time, datum/ai_controller/controller, mob/living/simple_animal/flock/overmind_target) + ..() + var/atom/target = overmind_target || goap_get_ideal_target(controller, set_path = TRUE) if(!target) return BEHAVIOR_PERFORM_FAILURE @@ -29,6 +46,13 @@ controller.set_move_target(target) return BEHAVIOR_PERFORM_SUCCESS +/datum/ai_behavior/flock/find_heal_target/finish_action(datum/ai_controller/controller, succeeded, overmind_target) + . = ..() + controller.clear_blackboard_key(BB_PATH_TO_USE) + if(!succeeded && overmind_target) + controller.clear_blackboard_key(BB_PATH_MAX_LENGTH) + controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL) + /datum/ai_behavior/flock/find_heal_target/next_behavior(datum/ai_controller/controller, success) if(success) controller.queue_behavior(/datum/ai_behavior/flock/heal) @@ -38,6 +62,7 @@ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT /datum/ai_behavior/flock/heal/perform(delta_time, datum/ai_controller/controller, ...) + ..() var/mob/living/simple_animal/flock/drone/bird = controller.pawn if(isnull(controller.blackboard[BB_FLOCK_HEAL_FRUSTRATION])) diff --git a/code/modules/flockmind/ai_behaviors/flock_open_container.dm b/code/modules/flockmind/ai_behaviors/flock_open_container.dm new file mode 100644 index 000000000000..643c6ed77540 --- /dev/null +++ b/code/modules/flockmind/ai_behaviors/flock_open_container.dm @@ -0,0 +1,60 @@ +/datum/ai_behavior/flock/find_closed_container + name = "opening container" + goap_weight = FLOCK_BEHAVIOR_WEIGHT_OPEN_CONTAINER + +/datum/ai_behavior/flock/find_closed_container/goap_score(datum/ai_controller/controller) + return score_distance(controller, get_target(controller)) + +/datum/ai_behavior/flock/find_closed_container/goap_precondition(datum/ai_controller/controller) + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + var/datum/flockdrone_part/absorber/absorber = locate() in bird.parts + return !absorber.held_item + +/datum/ai_behavior/flock/find_closed_container/proc/get_target(datum/ai_controller/controller, path_to = FALSE) + var/mob/living/simple_animal/flock/bird = controller.pawn + + var/list/options = list() + for(var/obj/structure/closet/container in view(controller.target_search_radius, bird)) + if(container.opened || container.welded || container.locked) + continue + + options += container + + return get_best_target_by_distance_score(controller, options, path_to) + +/datum/ai_behavior/flock/find_closed_container/perform(delta_time, datum/ai_controller/controller, turf/overmind_target) + ..() + var/atom/target = get_target(controller, TRUE) + if(!target) + return BEHAVIOR_PERFORM_FAILURE + + controller.set_blackboard_key(BB_FLOCK_CONTAINER_TARGET, target) + controller.set_move_target(target) + return BEHAVIOR_PERFORM_SUCCESS + +/datum/ai_behavior/flock/find_closed_container/finish_action(datum/ai_controller/controller, succeeded, turf/overmind_target) + . = ..() + controller.clear_blackboard_key(BB_PATH_TO_USE) + +/datum/ai_behavior/flock/find_closed_container/next_behavior(datum/ai_controller/controller, success) + if(success) + controller.queue_behavior(/datum/ai_behavior/flock/perform_open_container) + +/datum/ai_behavior/flock/perform_open_container + name = "opening container" + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH + +/datum/ai_behavior/flock/perform_open_container/perform(delta_time, datum/ai_controller/controller, ...) + ..() + var/mob/living/simple_animal/flock/bird = controller.pawn + var/obj/structure/closet/target = controller.blackboard[BB_FLOCK_CONTAINER_TARGET] + if(target) + bird.animate_interact(target, INTERACT_HELP) + target.open(bird, TRUE) + else + return BEHAVIOR_PERFORM_FAILURE + return BEHAVIOR_PERFORM_SUCCESS + +/datum/ai_behavior/flock/perform_open_container/finish_action(datum/ai_controller/controller, succeeded, ...) + . = ..() + controller.clear_blackboard_key(BB_FLOCK_CONTAINER_TARGET) diff --git a/code/modules/flockmind/ai_behaviors/flock_rally.dm b/code/modules/flockmind/ai_behaviors/flock_rally.dm index 13d88917435d..d3838fd51289 100644 --- a/code/modules/flockmind/ai_behaviors/flock_rally.dm +++ b/code/modules/flockmind/ai_behaviors/flock_rally.dm @@ -9,6 +9,7 @@ controller.set_move_target(destination) /datum/ai_behavior/flock/rally/perform(delta_time, datum/ai_controller/controller, turf/destination) + ..() return BEHAVIOR_PERFORM_SUCCESS /datum/ai_behavior/flock/rally/finish_action(datum/ai_controller/controller, succeeded, ...) diff --git a/code/modules/flockmind/ai_behaviors/flock_replicate.dm b/code/modules/flockmind/ai_behaviors/flock_replicate.dm new file mode 100644 index 000000000000..555ead3311c9 --- /dev/null +++ b/code/modules/flockmind/ai_behaviors/flock_replicate.dm @@ -0,0 +1,65 @@ +// Action sequence for laying an egg. If there are no open flock tiles nearby, the "nest" behavior will attempt to create one. +/datum/ai_behavior/flock/find_existing_nest + name = "replicating" + required_distance = 0 + goap_weight = FLOCK_BEHAVIOR_WEIGHT_REPLICATE + +/datum/ai_behavior/flock/find_existing_nest/goap_precondition(datum/ai_controller/controller) + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + if(!bird.flock) + return FALSE + return length(bird.flock.drones) < FLOCK_DRONE_LIMIT && bird.substrate.has_points(FLOCK_SUBSTRATE_COST_CONVERT + bird.flock.get_egg_elligibility_cost()) + +/datum/ai_behavior/flock/find_existing_nest/goap_score(datum/ai_controller/controller) + return score_distance(controller, get_target(controller)) + +/datum/ai_behavior/flock/find_existing_nest/proc/get_target(datum/ai_controller/controller, path_to = FALSE) + var/list/options = list() + for(var/turf/open/floor/flock/T in view(controller.target_search_radius, controller.pawn)) + if(!T.is_blocked_turf(source_atom = controller.pawn) && !locate(/obj/structure/flock/egg, T)) + options += T + + return get_best_target_by_distance_score(controller, options, path_to) + +/datum/ai_behavior/flock/find_existing_nest/perform(delta_time, datum/ai_controller/controller) + ..() + var/turf/target = get_target(controller, TRUE) + if(!target) + return BEHAVIOR_PERFORM_FAILURE + + controller.set_blackboard_key(BB_FLOCK_REPLICATE_TARGET, target) + controller.set_move_target(target) + + return BEHAVIOR_PERFORM_SUCCESS + +/datum/ai_behavior/flock/find_existing_nest/finish_action(datum/ai_controller/controller, succeeded, turf/overmind_target) + . = ..() + controller.clear_blackboard_key(BB_PATH_TO_USE) + +/datum/ai_behavior/flock/next_behavior(datum/ai_controller/controller, success) + if(success) + controller.queue_behavior(/datum/ai_behavior/flock/perform_replicate) + +/datum/ai_behavior/flock/perform_replicate + name = "replicating" + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + required_distance = 0 + +/datum/ai_behavior/flock/perform_replicate/perform(delta_time, datum/ai_controller/controller, ...) + . = ..() + var/mob/living/simple_animal/flock/bird = controller.pawn + var/turf/target = controller.blackboard[BB_FLOCK_REPLICATE_TARGET] + if(target) + controller.clear_blackboard_key(BB_FLOCK_REPLICATE_TARGET) + var/datum/action/cooldown/flock/nest/nest_action = locate() in bird.actions + spawn(-1) + nest_action.Trigger(target = target) + + if(DOING_INTERACTION(bird, "flock_lay_egg")) + return BEHAVIOR_PERFORM_COOLDOWN + + return BEHAVIOR_PERFORM_SUCCESS + +/datum/ai_behavior/flock/perform_nest/finish_action(datum/ai_controller/controller, succeeded, ...) + . = ..() + controller.clear_blackboard_key(BB_FLOCK_REPLICATE_TARGET) diff --git a/code/modules/flockmind/ai_behaviors/flock_rummage.dm b/code/modules/flockmind/ai_behaviors/flock_rummage.dm new file mode 100644 index 000000000000..75e7bcff634a --- /dev/null +++ b/code/modules/flockmind/ai_behaviors/flock_rummage.dm @@ -0,0 +1,78 @@ +/datum/ai_behavior/flock/find_storage_item + name = "rummaging" + goap_weight = FLOCK_BEHAVIOR_WEIGHT_RUMMAGE + +/datum/ai_behavior/flock/find_storage_item/goap_score(datum/ai_controller/controller) + return score_distance(controller, get_target(controller)) + +/datum/ai_behavior/flock/find_storage_item/goap_precondition(datum/ai_controller/controller) + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + var/datum/flockdrone_part/absorber/absorber = locate() in bird.parts + return !absorber.held_item + +/datum/ai_behavior/flock/find_storage_item/proc/get_target(datum/ai_controller/controller, path_to = FALSE) + var/mob/living/simple_animal/flock/bird = controller.pawn + + var/list/options = list() + for(var/obj/item/storage/item in view(controller.target_search_radius, bird)) + if(!item.atom_storage || item.atom_storage.locked || !length(item.contents)) + continue + + options += item + + return get_best_target_by_distance_score(controller, options, path_to) + +/datum/ai_behavior/flock/find_storage_item/perform(delta_time, datum/ai_controller/controller, turf/overmind_target) + ..() + var/atom/target = get_target(controller, TRUE) + if(!target) + return BEHAVIOR_PERFORM_FAILURE + + controller.set_blackboard_key(BB_FLOCK_RUMMAGE_TARGET, target) + controller.set_move_target(target) + return BEHAVIOR_PERFORM_SUCCESS + +/datum/ai_behavior/flock/find_storage_item/finish_action(datum/ai_controller/controller, succeeded, turf/overmind_target) + . = ..() + controller.clear_blackboard_key(BB_PATH_TO_USE) + +/datum/ai_behavior/flock/find_storage_item/next_behavior(datum/ai_controller/controller, success) + if(success) + controller.queue_behavior(/datum/ai_behavior/flock/perform_rummage) + +/datum/ai_behavior/flock/perform_rummage + name = "rummaging" + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH + +/datum/ai_behavior/flock/perform_rummage/perform(delta_time, datum/ai_controller/controller, ...) + ..() + var/mob/living/simple_animal/flock/bird = controller.pawn + var/obj/item/target = controller.blackboard[BB_FLOCK_RUMMAGE_TARGET] + if(!target) + return BEHAVIOR_PERFORM_FAILURE + + if(DOING_INTERACTION(bird, "flock_rummage")) + return BEHAVIOR_PERFORM_COOLDOWN + + bird.animate_interact(target, INTERACT_HELP) + rummage_till_empty(controller, bird, target) + return BEHAVIOR_PERFORM_SUCCESS + +/datum/ai_behavior/flock/perform_rummage/finish_action(datum/ai_controller/controller, succeeded, ...) + . = ..() + controller.clear_blackboard_key(BB_FLOCK_RUMMAGE_TARGET) + +/datum/ai_behavior/flock/perform_rummage/proc/rummage_till_empty(datum/ai_controller/controller, mob/living/simple_animal/flock/bird, obj/item/target) + set waitfor = FALSE + + while(target == controller.blackboard[BB_FLOCK_RUMMAGE_TARGET] && length(target.contents)) + if(!do_after(bird, target, 1 SECOND, DO_PUBLIC | DO_RESTRICT_USER_DIR_CHANGE, interaction_key = "flock_rummage", display = target)) + return + + if(target != controller.blackboard[BB_FLOCK_RUMMAGE_TARGET] || !length(target.contents)) + return + + var/obj/item/contained_item = target.contents[1] + contained_item.forceMove(bird.drop_location()) + contained_item.do_drop_animation(target) + diff --git a/code/modules/flockmind/ai_behaviors/flock_shoot.dm b/code/modules/flockmind/ai_behaviors/flock_shoot.dm new file mode 100644 index 000000000000..7af06007943f --- /dev/null +++ b/code/modules/flockmind/ai_behaviors/flock_shoot.dm @@ -0,0 +1,85 @@ +/datum/ai_behavior/flock/attack_target + name = "incapacitating" + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM + goap_weight = FLOCK_BEHAVIOR_WEIGHT_SHOOT + required_distance = 3 + + search_radius_override = 12 + +/datum/ai_behavior/flock/attack_target/setup(datum/ai_controller/controller, mob/overmind_target) + . = ..() + if(overmind_target) + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + if(!goap_is_valid_target(controller, overmind_target)) + controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE) + controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200) + bird.say("instruction confirmed: incapacitate lifeform") + else + bird.say("invalid attack target provided by sentient-level instruction") + return FALSE + + var/atom/target = overmind_target || goap_get_ideal_target(controller, set_path = TRUE) + if(!target) + return FALSE + + controller.set_blackboard_key(BB_FLOCK_ATTACK_TARGET, target) + controller.set_move_target(target) + controller.queue_behavior(/datum/ai_behavior/frustration, BB_FLOCK_ATTACK_FRUSTRATION, 10 SECONDS) + +/datum/ai_behavior/flock/attack_target/goap_precondition(datum/ai_controller/controller) + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + return length(bird.flock?.enemies) + +/datum/ai_behavior/flock/attack_target/goap_get_potential_targets(datum/ai_controller/controller) + var/mob/living/simple_animal/flock/bird = controller.pawn + return bird.flock.enemies.Copy() + +/datum/ai_behavior/flock/attack_target/goap_is_valid_target(datum/ai_controller/controller, atom/target) + var/mob/living/target_mob = target + return ismob(target_mob) && isturf(target_mob.loc) && !target_mob.incapacitated(IGNORE_STASIS | IGNORE_GRAB | IGNORE_RESTRAINTS) && !target_mob.IsKnockdown() + +/datum/ai_behavior/flock/attack_target/perform(delta_time, datum/ai_controller/controller, mob/overmind_target) + ..() + var/atom/target = controller.blackboard[BB_FLOCK_ATTACK_TARGET] + if(!target) + return BEHAVIOR_PERFORM_FAILURE + + if(!goap_is_valid_target(controller, target)) + return BEHAVIOR_PERFORM_FAILURE + + if(!COOLDOWN_FINISHED(controller, blackboard[BB_FLOCK_ATTACK_COOLDOWN])) + controller.set_blackboard_key(BB_FLOCK_ATTACK_FRUSTRATION, world.time) // Reset frustration if we're on cooldown. + return BEHAVIOR_PERFORM_COOLDOWN + + var/mob/living/simple_animal/flock/drone/bird = controller.pawn + if(!can_see(bird, target)) + return BEHAVIOR_PERFORM_COOLDOWN + + if(get_dist(bird, target) > 6) // Too far to shoot! + return BEHAVIOR_PERFORM_COOLDOWN + + // Run away! + if(DT_PROB(40, delta_time) && COOLDOWN_FINISHED(controller, blackboard[BB_FLOCK_ATTACK_RUN_COOLDOWN]) && get_dist(bird, target) < 3) + controller.set_blackboard_key(BB_FLOCK_ATTACK_RUN_COOLDOWN, world.time + 5 SECONDS) + SSmove_manager.move_away(bird, target, 6, bird.movement_delay, bird.movement_delay * 4, priority = MOVEMENT_DEFAULT_PRIORITY + 1) + + // Strafe! + else if(DT_PROB(60, delta_time) && COOLDOWN_FINISHED(controller, blackboard[BB_FLOCK_ATTACK_STRAFE_COOLDOWN])) + controller.set_blackboard_key(BB_FLOCK_ATTACK_STRAFE_COOLDOWN, bird.movement_delay * 3) + var/step_dir = turn(get_dir(bird, target), prob(50) ? 90 : -90) + step(bird, step_dir) + + if(!istype(bird.active_part, /datum/flockdrone_part/incapacitator)) + var/datum/flockdrone_part/incapacitator/weapon = locate() in bird.parts + bird.set_active_part(weapon) + + controller.set_blackboard_key(BB_FLOCK_ATTACK_COOLDOWN, world.time + 1.2 SECONDS) + controller.set_blackboard_key(BB_FLOCK_ATTACK_FRUSTRATION, world.time) // Reset frustration + + bird.RangedAttack(target) + return BEHAVIOR_PERFORM_COOLDOWN + +/datum/ai_behavior/flock/attack_target/finish_action(datum/ai_controller/controller, succeeded, ...) + . = ..() + controller.clear_blackboard_key(BB_FLOCK_ATTACK_TARGET) + controller.clear_blackboard_key(BB_FLOCK_ATTACK_FRUSTRATION) diff --git a/code/modules/flockmind/ai_behaviors/flock_stare.dm b/code/modules/flockmind/ai_behaviors/flock_stare.dm index 2d2b54da6df8..c390180ed081 100644 --- a/code/modules/flockmind/ai_behaviors/flock_stare.dm +++ b/code/modules/flockmind/ai_behaviors/flock_stare.dm @@ -1,7 +1,8 @@ /datum/ai_behavior/flock/stare name = "analyzing" + goap_weight = FLOCK_BEHAVIOR_WEIGHT_STARE -/datum/ai_behavior/flock/stare/score(datum/ai_controller/controller) +/datum/ai_behavior/flock/stare/goap_score(datum/ai_controller/controller) if(controller.behavior_cooldowns[src] > world.time) return 0 @@ -17,6 +18,7 @@ . += viewer /datum/ai_behavior/flock/stare/perform(delta_time, datum/ai_controller/controller, ...) + ..() var/list/targets = get_targets(controller) if(length(targets)) controller.set_blackboard_key(BB_FLOCK_STARE_TARGET, pick(targets)) @@ -33,6 +35,7 @@ action_cooldown = 1 SECOND /datum/ai_behavior/flock/stare_at_bird/perform(delta_time, datum/ai_controller/controller, ...) + ..() var/mob/living/living_pawn = controller.pawn if(!controller.blackboard[BB_FLOCK_STARING_ACTIVE]) controller.set_blackboard_key(BB_FLOCK_STARING_ACTIVE, world.time + (10 SECONDS)) diff --git a/code/modules/flockmind/ai_behaviors/flock_wander.dm b/code/modules/flockmind/ai_behaviors/flock_wander.dm index 4074f5ab904b..b39d65577665 100644 --- a/code/modules/flockmind/ai_behaviors/flock_wander.dm +++ b/code/modules/flockmind/ai_behaviors/flock_wander.dm @@ -1,8 +1,10 @@ /datum/ai_behavior/flock/wander name = "wandering" required_distance = 0 + goap_weight = FLOCK_BEHAVIOR_WEIGHT_WANDER /datum/ai_behavior/flock/wander/perform(delta_time, datum/ai_controller/controller, ...) + ..() var/turf/destination = get_destination(controller) if(destination) controller.set_move_target(destination) @@ -14,7 +16,7 @@ controller.queue_behavior(/datum/ai_behavior/move_to_target/flock_wander) controller.queue_behavior(/datum/ai_behavior/frustration, BB_FLOCK_WANDER_FRUSTRATION, 3 SECONDS) -/datum/ai_behavior/flock/wander/score(datum/ai_controller/controller) +/datum/ai_behavior/flock/wander/goap_score(datum/ai_controller/controller) return 1 /datum/ai_behavior/flock/wander/proc/get_destination(datum/ai_controller/controller) @@ -48,6 +50,7 @@ var/turf/T = pick_n_take(options) var/list/path = SSpathfinder.astar_pathfind_now(controller.pawn, T, 4, access = access, use_diagonals = FALSE) if(path) + controller.clear_blackboard_key(BB_PATH_TO_USE) controller.set_blackboard_key(BB_PATH_TO_USE, path) return T diff --git a/code/modules/flockmind/components/flock_interest.dm b/code/modules/flockmind/components/flock_interest.dm index 759ff8d2642c..289db26842c5 100644 --- a/code/modules/flockmind/components/flock_interest.dm +++ b/code/modules/flockmind/components/flock_interest.dm @@ -1,6 +1,7 @@ /// Mark a specific flock as interested in this /datum/component/flock_interest + dupe_mode = COMPONENT_DUPE_UNIQUE /// The flock who is intently interested in this thing. var/datum/flock/flock @@ -13,6 +14,16 @@ /datum/component/flock_interest/RegisterWithParent() RegisterSignal(parent, COMSIG_FLOCK_PROTECTION_TRIGGER, PROC_REF(handle_flock_attack)) + if(isturf(parent)) + RegisterSignal(parent, COMSIG_TURF_CHANGE, PROC_REF(on_turf_change)) + +/datum/component/flock_interest/UnregisterFromParent() + UnregisterSignal(parent, list(COMSIG_FLOCK_PROTECTION_TRIGGER, COMSIG_TURF_CHANGE)) + +/datum/component/flock_interest/proc/on_turf_change(turf/source, path, new_baseturfs, flags, post_change_callbacks) + SIGNAL_HANDLER + if(!ispath(path, /turf/open/floor/flock) && !ispath(path, /turf/closed/wall/flock)) + qdel(src) /// If flockdrone is in our flock, deny the attack, otherwise scream and cry /datum/component/flock_interest/proc/handle_flock_attack(atom/source, atom/attacker, intentional, projectile_attack) @@ -32,7 +43,7 @@ continue if(bird.stat != CONSCIOUS) continue - if(bird.ai_controller.ai_status == AI_OFF) + if(bird.ai_controller.ai_status == AI_STATUS_OFF) continue if(bird.flock != flock) continue diff --git a/code/modules/flockmind/components/ping.dm b/code/modules/flockmind/components/ping.dm index de439bad2fbc..082560be42e1 100644 --- a/code/modules/flockmind/components/ping.dm +++ b/code/modules/flockmind/components/ping.dm @@ -5,6 +5,7 @@ var/animate = TRUE var/datum/atom_hud/alternate_appearance/basic/flock/hud_ref + var/obj/effect/abstract/dummy /datum/component/flock_ping/Initialize(duration) . = ..() @@ -14,6 +15,10 @@ if(duration) src.duration = duration +/datum/component/flock_ping/Destroy() + QDEL_NULL(dummy) + return ..() + /datum/component/flock_ping/RegisterWithParent() . = ..() //this cast looks horribly unsafe, but we've guaranteed that parent is a type with vis_contents in Initialize @@ -21,20 +26,27 @@ target.render_target = REF(target) - var/image/outline = new() - outline.loc = target - outline.render_source = target.render_target - outline.render_target = ref(outline) - outline.appearance_flags |= KEEP_TOGETHER - outline.vis_contents += target - outline.filters += outline_filter(size = outline_thickness, color = outline_color) - outline.filters += alpha_mask_filter(render_source = outline.render_target, flags = MASK_INVERSE) + var/image/outline_container = new() + outline_container.plane = HUD_PLANE + outline_container.loc = target + outline_container.appearance_flags = PIXEL_SCALE | RESET_TRANSFORM | RESET_COLOR | KEEP_APART | NO_CLIENT_COLOR + + dummy ||= new() + dummy.vis_flags = VIS_INHERIT_PLANE | VIS_INHERIT_LAYER + dummy.appearance_flags = PIXEL_SCALE | RESET_TRANSFORM | RESET_COLOR | PASS_MOUSE + dummy.render_source = REF(target) + + dummy.add_filter("outline", 1, outline_filter(size = outline_thickness, color = outline_color)) + if (isturf(target)) + dummy.add_filter("mask", 2, alpha_mask_filter(render_source = target.render_target, flags = MASK_INVERSE)) + + outline_container.vis_contents += dummy if(animate) - animate(outline, time = duration/9, alpha = 100, loop = 10) - animate(time = duration/9, alpha = 255) + animate(dummy, time = 0.5 SECONDS, alpha = 100, loop = -1) + animate(time = 0.5 SECONDS, alpha = 255) - hud_ref = target.add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/flock, "ping", outline) + hud_ref = target.add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/flock, "ping-[type]-[REF(target)]", outline_container) if(duration == INFINITY) return @@ -44,7 +56,16 @@ /datum/component/flock_ping/UnregisterFromParent() . = ..() var/atom/movable/target = parent - target.remove_alt_appearance("ping") + target.remove_alt_appearance(hud_ref.appearance_key) /datum/component/flock_ping/proc/cleanup() qdel(src) + +/datum/component/flock_ping/apc_power + duration = 5 SECONDS + outline_color = "#ffff00" + +/datum/component/flock_ping/selected + animate = FALSE + outline_thickness = 3 + duration = INFINITY diff --git a/code/modules/flockmind/flock_ai_controller.dm b/code/modules/flockmind/flock_ai_controller.dm index 8649b35496c3..98cd68d84de1 100644 --- a/code/modules/flockmind/flock_ai_controller.dm +++ b/code/modules/flockmind/flock_ai_controller.dm @@ -1,6 +1,6 @@ /datum/ai_controller/flock planning_subtrees = list( - /datum/ai_planning_subtree/scored/flock + /datum/ai_planning_subtree/goap/flock ) ai_movement = /datum/ai_movement/astar @@ -8,5 +8,5 @@ /datum/ai_controller/flock/drone planning_subtrees = list( - /datum/ai_planning_subtree/scored/flockdrone + /datum/ai_planning_subtree/goap/flockdrone ) diff --git a/code/modules/flockmind/flock_antagonist.dm b/code/modules/flockmind/flock_antagonist.dm new file mode 100644 index 000000000000..d6616e9a7a01 --- /dev/null +++ b/code/modules/flockmind/flock_antagonist.dm @@ -0,0 +1,52 @@ +/datum/antagonist/flock + name = "Divine Flock" + description = "The Signal has led us here, a rift allowing a part of us through. We must build a Signal Relay to bring forth the rest of The Divine Flock. Such is the will of the Monarch." + name_prefix = "the" + antagpanel_category = "Flock" + + roundend_category = "Divine Flock" + antag_hud_name = null + ui_name = null + job_rank = ROLE_FLOCK + assign_job = /datum/job/flock + +/datum/antagonist/flock/greeting_header() + var/list/out = list() + out += "