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 += "
You are [gradient_text("The Divine Flock","#3cb5a3", "#124e43")]
" + if(description) + out += span_flocksay("
[description]
") + return jointext(out, "") + +/datum/antagonist/flock/on_gain() + add_objective(new /datum/objective/flock_relay) + . = ..() + +/datum/antagonist/flock/admin_add(datum/mind/new_owner, mob/admin) + if(tgui_alert(admin, "Are you sure you want to turn [new_owner.current] ([new_owner.current.ckey]) into [get_name()]?", "Antag Panel", list("Yes", "No")) != "Yes") + return + + var/delete_mob = tgui_alert(admin, "Delete mob?", "Antag Panel", list("Yes", "No")) == "Yes" + var/mob/old_mob = new_owner.current + + message_admins("[key_name_admin(admin)] made [key_name_admin(new_owner)] into [get_name()].") + log_admin("[key_name(admin)] made [key_name(new_owner)] into [get_name()].") + + var/mob/camera/flock/flock_mob = adminspawn_flock_mob(get_turf(new_owner.current)) + flock_mob.mind_initialize() + flock_mob.PossessByPlayer(new_owner.current.ckey) + flock_mob.mind.add_antag_datum(src) + + if(delete_mob) + qdel(old_mob) + +/// Spawns the flock mob for traitor panel things. +/datum/antagonist/flock/proc/adminspawn_flock_mob(turf/spawn_loc) + return new /mob/camera/flock/trace(spawn_loc) + +/datum/antagonist/flock/overmind + name = "Divine Flock Overmind" + assign_job = /datum/job/flock/overmind + +/datum/antagonist/flock/overmind/adminspawn_flock_mob(turf/spawn_loc) + return new /mob/camera/flock/overmind(spawn_loc) + diff --git a/code/modules/flockmind/flock_camera/_flock_camera.dm b/code/modules/flockmind/flock_camera/_flock_camera.dm index 5c863513367e..99bf5b0607b6 100644 --- a/code/modules/flockmind/flock_camera/_flock_camera.dm +++ b/code/modules/flockmind/flock_camera/_flock_camera.dm @@ -6,7 +6,7 @@ layer = FLY_LAYER mouse_opacity = MOUSE_OPACITY_ICON - invisibility = INVISIBILITY_OBSERVER + invisibility = INVISIBILITY_FLOCK appearance_flags = parent_type::appearance_flags | RESET_COLOR blend_mode = BLEND_ADD @@ -16,6 +16,7 @@ lighting_alpha = LIGHTING_PLANE_ALPHA_INVISIBLE sight = SEE_TURFS | SEE_MOBS | SEE_OBJS | SEE_SELF + hud_type = /datum/hud/flockghost initial_language_holder = /datum/language_holder/flock move_on_shuttle = FALSE @@ -38,6 +39,12 @@ add_client_colour(/datum/client_colour/flockmind) ADD_TRAIT(src, TRAIT_HEAR_THROUGH_WALLS, INNATE_TRAIT) +/mob/camera/flock/Destroy() + if(controlling_bird) + controlling_bird.release_control() + flock = null + return ..() + /mob/camera/flock/Login() . = ..() if(!. || !client) @@ -73,6 +80,19 @@ if (href_list["ping"]) origin.AddComponent(/datum/component/flock_ping) +/mob/camera/flock/broadcast_examine(atom/examined) + return + +/mob/camera/flock/on_changed_z_level(turf/old_turf, turf/new_turf, notify_contents) + . = ..() + update_z(new_turf?.z) + + if(flock && !flock.is_on_safe_z(src)) + var/turf/destination = get_turf(pick_safe(flock.drones)) || get_safe_random_station_turf() + + forceMove(destination) + to_chat(src, span_warning("You feel your consciousness weaking as you are ripped further from your rift, and you retreat back to safety.")) + /mob/camera/flock/proc/update_z(new_z) // 1+ to register, null to unregister if (registered_z != new_z) if (registered_z) diff --git a/code/modules/flockmind/flock_camera/flock_overmind.dm b/code/modules/flockmind/flock_camera/flock_overmind.dm index 5c9161082674..3bcc3e657d78 100644 --- a/code/modules/flockmind/flock_camera/flock_overmind.dm +++ b/code/modules/flockmind/flock_camera/flock_overmind.dm @@ -13,10 +13,12 @@ /// Granted after create_rift is cast. var/list/grant_upon_start = list( /datum/action/cooldown/flock/control_panel, + /datum/action/cooldown/flock/create_structure, /datum/action/cooldown/flock/partition_mind, /datum/action/cooldown/flock/diffract_drone, /datum/action/cooldown/flock/control_drone, /datum/action/cooldown/flock/designate_tile, + /datum/action/cooldown/flock/designate_deconstruct, /datum/action/cooldown/flock/designate_enemy, /datum/action/cooldown/flock/designate_ignore, /datum/action/cooldown/flock/ping, @@ -35,8 +37,7 @@ /mob/camera/flock/overmind/Login() . = ..() - //remove this when the gamemode is set up - flock.start() + flock.refresh_unlockables() /mob/camera/flock/overmind/Logout() . = ..() @@ -44,17 +45,32 @@ var/datum/action/cooldown/flock/control_drone/control_drone = locate() in actions control_drone?.free_drone() +/mob/camera/flock/overmind/examine(mob/user) + if(!isflockmob(user)) + return ..() + + . = list( + span_flocksay("###=- Ident confirmed, data packet received."), + span_flocksay("ID: [real_name]"), + span_flocksay("Flock: [flock.name || "N/A"]"), + span_flocksay("Bandwidth: [flock.bandwidth.has_points()]"), + span_flocksay("Substrate: [flock.get_total_substrate()]"), + span_flocksay("System Integrity: [flock.get_total_health_percentage()]%"), + span_flocksay("Cognition: COMPUTATIONAL NEXUS"), + span_flocksay("###=-"), + ) + /mob/camera/flock/overmind/get_status_tab_items() . = ..() . += "" - . += "Total Compute: [flock.compute.has_points()]" - . += "Used Compute: [flock.used_compute]" - . += "Available Compute: [flock.available_compute()]" + . += "Total Bandwidth: [flock.bandwidth.has_points()]" + . += "Used Bandwidth: [flock.used_bandwidth]" + . += "Available Bandwidth: [flock.available_bandwidth()]" /mob/camera/flock/overmind/so_very_sad_death() var/datum/flock/old_flock = flock flock = null - old_flock?.overmind = null + old_flock?.overmind = null // to prevent infinite loop old_flock.game_over() . = ..() @@ -67,3 +83,5 @@ for(var/datum/action/A as anything in grant_upon_start) A = new A() A.Grant(src) + + flock.start() diff --git a/code/modules/flockmind/flock_camera/flocktrace.dm b/code/modules/flockmind/flock_camera/flocktrace.dm index 788d5dbde6d1..b7379706ee3b 100644 --- a/code/modules/flockmind/flock_camera/flocktrace.dm +++ b/code/modules/flockmind/flock_camera/flocktrace.dm @@ -12,7 +12,7 @@ /datum/action/cooldown/flock/ping, ) - var/compute_provided = -FLOCK_COMPUTE_COST_FLOCKTRACE + var/bandwidth_provided = -FLOCK_COMPUTE_COST_FLOCKTRACE /mob/camera/flock/trace/Initialize(mapload, join_flock) . = ..() @@ -24,12 +24,26 @@ flock?.free_unit(src) return ..() +/mob/camera/flock/trace/examine(mob/user) + if(!isflockmob(user)) + return ..() + + . = list( + span_flocksay("###=- Ident confirmed, data packet received."), + span_flocksay("ID: [real_name]"), + span_flocksay("Flock: [flock.name || "N/A"]"), + span_flocksay("Bandwidth: [flock.bandwidth.has_points()]"), + span_flocksay("System Integrity: [flock.get_total_health_percentage()]%"), + span_flocksay("Cognition: SYNAPTIC PROCESS"), + span_flocksay("###=-"), + ) + /mob/camera/flock/trace/vv_edit_var(var_name, var_value) switch(var_name) - if(NAMEOF(src, compute_provided)) - flock?.compute.adjust_points(-compute_provided) + if(NAMEOF(src, bandwidth_provided)) + flock?.bandwidth.adjust_points(-bandwidth_provided) ..() - flock?.compute.adjust_points(compute_provided) + flock?.bandwidth.adjust_points(bandwidth_provided) return TRUE return ..() diff --git a/code/modules/flockmind/flock_controller/_flock_controller.dm b/code/modules/flockmind/flock_controller/_flock_controller.dm index df0c136ad9ac..b9d8d5f62a15 100644 --- a/code/modules/flockmind/flock_controller/_flock_controller.dm +++ b/code/modules/flockmind/flock_controller/_flock_controller.dm @@ -1,4 +1,4 @@ -/proc/get_default_flock() +/proc/get_default_flock() as /datum/flock var/static/datum/flock/flock if(isnull(flock)) flock = new @@ -14,6 +14,8 @@ /// Cache of images used by notices. var/list/notice_images = list() /// A k:V list of atoms the Overmind has marked for conversion, where the value is TRUE + var/list/marked_for_conversion = list() + /// A k:v list of atoms the Overmind has marked for deconstruction, where the value is TRUE var/list/marked_for_deconstruction = list() /// A k:V list of reserved_turf = TRUE. var/list/turf_reservations = list() @@ -43,15 +45,18 @@ var/list/datum/flock_unlockable/unlockables /// The total amount of computational power available, before whats being used. - var/datum/point_holder/compute + var/datum/point_holder/bandwidth /// The computational power being used. - var/used_compute = 0 + var/used_bandwidth = 0 /// The maximum amount of traces allowed. - var/max_traces = 0 + var/max_traces = 10 + + /// The current substrate cost to lay an egg. + var/current_egg_cost = FLOCK_SUBSTRATE_COST_LAY_EGG var/flock_started = FALSE - // Did the flock lose? - var/flock_game_over = FALSE + /// Flock status, won, lost, etc + var/flock_game_status = NONE /// Current UI tab, saves on data sending. var/ui_tab = FLOCK_UI_DRONES @@ -64,12 +69,12 @@ var/stat_traces_made = 0 var/stat_tiles_made = 0 var/stat_structures_made = 0 - var/stat_highest_compute = 0 + var/stat_highest_bandwidth = 0 /datum/flock/New() name = flock_realname(FLOCK_TYPE_OVERMIND) - compute = new + bandwidth = new create_hud_images() unlockables = list() @@ -80,10 +85,11 @@ // Called by gamemode code /datum/flock/process(delta_time) - if(flock_game_over) + if(flock_game_status == FLOCK_ENDGAME_LOST) return - stat_highest_compute = max(stat_highest_compute, compute.has_points()) + update_relay_huds() + stat_highest_bandwidth = max(stat_highest_bandwidth, bandwidth.has_points()) /// Called after everything is setup, and clients are in control of their mobs. /datum/flock/proc/start() @@ -93,29 +99,42 @@ flock_started = TRUE refresh_unlockables() -/// Convert a turf and claim it for the flock. -/datum/flock/proc/convert_turf(turf/T) +/// Claim it for the flock, optionally converting it. Converting should only be avoided if the turf is already a flockturf. +/datum/flock/proc/claim_turf(turf/T, convert = TRUE) if(isnull(T)) return free_turf(T) - T = flock_convert_turf(T, src) + if(convert) + T = flock_convert_turf(T, src) if(isnull(T)) return - playsound(T, 'sound/items/deconstruct.ogg', 30, TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE) + if(convert) + playsound(T, 'sound/items/deconstruct.ogg', 30, TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE) if(iswallturf(T)) - claimed_walls += T + claimed_walls[T] = TRUE else - claimed_floors += T + claimed_floors[T] = TRUE T.AddComponent(/datum/component/flock_interest, src) + RegisterSignal(T, COMSIG_TURF_CHANGE, PROC_REF(claimed_turf_change)) SEND_SIGNAL(T, COMSIG_TURF_CLAIMED_BY_FLOCK, src) +/// Stop tracking a turf that is in our claimed walls or claimed floors lists. +/datum/flock/proc/stop_tracking_turf(turf/T) + if(iswallturf(T)) + claimed_walls -= T + else + claimed_floors -= T + + qdel(T.GetComponent(/datum/component/flock_interest)) + UnregisterSignal(T, COMSIG_TURF_CHANGE) + /// Reserves a turf, making AI ignore it for the purposes of targetting. -/datum/flock/proc/reserve_turf(mob/living/simple_animal/flock/user, turf/target) +/datum/flock/proc/reserve_turf(mob/living/simple_animal/flock/user, turf/target, remove_on_change = TRUE) if(turf_reservations_by_flock[user]) return FALSE if(turf_reservations[target]) @@ -124,7 +143,8 @@ turf_reservations_by_flock[user] = target turf_reservations[target] = user add_notice(target, FLOCK_NOTICE_RESERVED) - RegisterSignal(target, COMSIG_TURF_CHANGE, PROC_REF(reserved_turf_change)) + if(remove_on_change) + RegisterSignal(target, COMSIG_TURF_CHANGE, PROC_REF(reserved_turf_change)) return TRUE /// Free a turf from reservation, allowing AI to target it again. override_turf can be given to lookup the user if there isnt a user in this context. @@ -146,7 +166,7 @@ remove_notice(to_free, FLOCK_NOTICE_PRIORITY) turf_reservations_by_flock -= user turf_reservations -= to_free - marked_for_deconstruction -= to_free + marked_for_conversion -= to_free UnregisterSignal(to_free, COMSIG_TURF_CHANGE) /// Returns TRUE if the given turf is not reserved. @@ -161,16 +181,18 @@ /datum/flock/proc/is_mob_ignored(mob/M) return ignores[M] +/// Adds a unit to the flock and sets up all of the tracking. /datum/flock/proc/add_unit(mob/unit) if(isflocktrace(unit)) traces += unit var/mob/camera/flock/trace/ghostbird = unit - add_compute_influence(ghostbird.compute_provided) + add_bandwidth_influence(ghostbird.bandwidth_provided) return if(isflockdrone(unit)) drones += unit + update_egg_cost() else if(isflockbit(unit)) bits += unit @@ -178,27 +200,29 @@ unit.AddComponent(/datum/component/flock_interest, src) var/mob/living/simple_animal/flock/bird = unit - add_compute_influence(bird.compute_provided) + add_bandwidth_influence(bird.bandwidth_provided) +/// Removes a unit from the flock. /datum/flock/proc/free_unit(mob/unit) if(isflocktrace(unit)) var/mob/camera/flock/trace/ghostbird = unit ghostbird.flock = null traces -= unit - remove_compute_influence(ghostbird.compute_provided) + remove_bandwidth_influence(ghostbird.bandwidth_provided) return else if(isflockdrone(unit)) var/mob/living/simple_animal/flock/drone/bird = unit bird.flock = null drones -= unit - remove_compute_influence(bird.compute_provided) + remove_bandwidth_influence(bird.bandwidth_provided) + update_egg_cost() else if(isflockbit(unit)) var/mob/living/simple_animal/flock/bit/bitty_bird = unit bitty_bird.flock = null bits -= unit - remove_compute_influence(bitty_bird.compute_provided) + remove_bandwidth_influence(bitty_bird.bandwidth_provided) remove_notice(unit, FLOCK_NOTICE_HEALTH) free_turf(unit) @@ -206,60 +230,90 @@ consider_game_over() +/// Add a structure to the flock. /datum/flock/proc/add_structure(obj/structure/flock/struct) structures += struct struct.flock = src struct.AddComponent(/datum/component/flock_interest, src) - add_compute_influence(struct.compute_provided) + add_bandwidth_influence(struct.bandwidth_provided) + if(istype(struct, /obj/structure/flock/egg)) + update_egg_cost() + +/// Remove a structure from the flock. /datum/flock/proc/free_structure(obj/structure/flock/struct) structures -= struct qdel(struct.GetComponent(/datum/component/flock_interest)) struct.flock = null if(struct.active) - remove_compute_influence(-struct.active_compute_cost) + remove_bandwidth_influence(-struct.active_bandwidth_cost) else - remove_compute_influence(struct.compute_provided) + remove_bandwidth_influence(struct.bandwidth_provided) + + if(istype(struct, /obj/structure/flock/egg)) + update_egg_cost() +/// Wrapper for spawning a tealprint, used by Create Structure /datum/flock/proc/create_structure(turf/location, structure_type) - new /obj/structure/flock/tealprint(location, structure_type) + new /obj/structure/flock/tealprint(location, src, structure_type) -/// Wrapper for handling compute alongside used_compute for new mobs -/datum/flock/proc/add_compute_influence(num) +/// Wrapper for handling bandwidth alongside used_bandwidth for new mobs +/datum/flock/proc/add_bandwidth_influence(num) if(num < 0) - used_compute += abs(num) + used_bandwidth += abs(num) else - compute.adjust_points(num) + bandwidth.adjust_points(num) refresh_unlockables() -/// Wrapper for handling compute alongside used_compute for mobs leaving the flock -/datum/flock/proc/remove_compute_influence(num) +/// Wrapper for handling bandwidth alongside used_bandwidth for mobs leaving the flock +/datum/flock/proc/remove_bandwidth_influence(num) if(num < 0) - used_compute -= abs(num) + used_bandwidth -= abs(num) else - compute.adjust_points(-num) + bandwidth.adjust_points(-num) refresh_unlockables() +/// Refreshes the status of every unlockable. /datum/flock/proc/refresh_unlockables() - PRIVATE_PROC(TRUE) if(!flock_started) return - var/new_total = compute.has_points() - var/new_available = available_compute() + var/new_total = bandwidth.has_points() + var/new_available = available_bandwidth() for(var/datum/flock_unlockable/unlockable as anything in unlockables) unlockable.refresh_lock_status(src, new_total, new_available) -/// Returns the amount of available compute. Can return negative if over budget. -/datum/flock/proc/available_compute() - return compute.has_points() - used_compute +/// Returns the total amount of bandwidth, including bandwidth being used. +/datum/flock/proc/total_bandwidth() + return bandwidth.has_points() -/// Returns TRUE if the flock has the required compute +/// Returns the amount of available bandwidth. Can return negative if over budget. +/datum/flock/proc/available_bandwidth() + return bandwidth.has_points() - used_bandwidth + +/// Returns TRUE if the flock has the required bandwidth /datum/flock/proc/can_afford(amt) - return amt <= max(available_compute(), 0) + return amt <= max(available_bandwidth(), 0) + +/// Updates the substrate cost of an egg given the current status of the flock. +/datum/flock/proc/update_egg_cost() + var/egg_count = 0 + for(var/obj/structure/flock/egg/egg as anything in structures) + egg_count++ + + var/status_addend = (length(drones) + egg_count) ** sqrt(2) + current_egg_cost = round(FLOCK_SUBSTRATE_COST_LAY_EGG + status_addend, 10) + +/// Returns the substract requirement to be able to spend substrate on an egg. See above proc. +/datum/flock/proc/get_egg_elligibility_cost() + var/ideal_pop_factor = length(drones) - FLOCK_MIN_DESIRED_POP + if(ideal_pop_factor <= 0) + return current_egg_cost + + return round(current_egg_cost + (ideal_pop_factor * FLOCK_ADDITIONAL_RESOURCE_RESERVATION_PER_DRONE), 1) /// Sets the flock's overmind /datum/flock/proc/register_overmind(mob/camera/flock_overmind) @@ -281,6 +335,7 @@ enemies[enemy] = get_area_name(enemy) return TRUE +/// Removes an atom from the enemies list. /datum/flock/proc/remove_enemy(atom/movable/enemy, skip_buckled) if(!skip_buckled) for(var/mob/living/L in enemy.buckled_mobs) @@ -294,6 +349,7 @@ remove_notice(enemy, FLOCK_NOTICE_ENEMY) return +/// Adds a mob to the ignored list. /datum/flock/proc/add_ignore(atom/movable/ignore) for(var/mob/living/L in ignore.buckled_mobs) add_ignore(L) @@ -306,6 +362,7 @@ add_notice(ignore, FLOCK_NOTICE_IGNORE) ignores[ignore] = TRUE +/// Removes a mob from the ignore list. /datum/flock/proc/remove_ignore(atom/movable/ignore, skip_buckled) if(!skip_buckled) for(var/mob/living/L in ignore.buckled_mobs) @@ -318,40 +375,92 @@ remove_notice(ignore, FLOCK_NOTICE_IGNORE) UnregisterSignal(ignore, COMSIG_PARENT_QDELETING) +/// (un)Mark a flock atom for deconstruction. Returns FALSE if it cannot be deconstructed. +/datum/flock/proc/toggle_deconstruction_mark(atom/A) + if(ismob(A) || !HAS_TRAIT(A, TRAIT_FLOCK_THING) || HAS_TRAIT(A, TRAIT_FLOCK_NODECON)) + return FALSE + + if(marked_for_deconstruction[A]) + marked_for_deconstruction -= A + UnregisterSignal(A, COMSIG_PARENT_QDELETING) + remove_notice(A, FLOCK_NOTICE_DECONSTRUCT) + else + marked_for_deconstruction[A] = TRUE + RegisterSignal(A, COMSIG_PARENT_QDELETING, PROC_REF(deconstruct_mark_deleted)) + add_notice(A, FLOCK_NOTICE_DECONSTRUCT) + return TRUE + +/// Places a flock notice on an atom. See flock_defines.dm /datum/flock/proc/add_notice(atom/target, notice_type) var/image/I = image(notice_images[notice_type], loc = target) return target.add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/flock, notice_type, I, NONE, src) +/// Removes a flock notice from an atom. /datum/flock/proc/remove_notice(atom/target, notice_type) target.remove_alt_appearance(notice_type) +/// Returns a list of turfs marked for conversion, minus turfs that drones have already reserved. /datum/flock/proc/get_priority_turfs(mob/living/simple_animal/flock/bird) - if(!length(marked_for_deconstruction)) + if(!length(marked_for_conversion)) return null - return marked_for_deconstruction - turf_reservations + return marked_for_conversion - turf_reservations +/// Pings a location, alerting all flocktraces. /datum/flock/proc/ping(turf/T, mob/camera/flock/pinger) var/message = "System interrupt. Designating new target: [T] in [get_area(T)]." flock_talk(pinger, message, src, TRUE, list("italics")) + T.AddComponent(/datum/component/flock_ping, 5 SECONDS) + for(var/mob/camera/flock/ghost_bird in (traces + overmind)) - if(isnull(ghost_bird.client)) - continue + var/client/target_client = ghost_bird.client + if(!target_client) + if(!ghost_bird.controlling_bird?.client) + continue + target_client = ghost_bird.controlling_bird.client - ghost_bird.playsound_local(null, 'goon/sounds/flockmind/ping.ogg', 50, TRUE) + target_client.mob.playsound_local(null, 'goon/sounds/flockmind/ping.ogg', 50, TRUE) if(ghost_bird == pinger) continue - var/image/pointer = pointer_image_to(ghost_bird, T) - //T._AddComponent(list(/datum/component/flock_ping, 5 SECONDS)) - + var/image/pointer = pointer_image_to(target_client.mob, T) animate(pointer, time = 3 SECONDS, alpha = 0) - add_ping_image(ghost_bird.client, pointer, 3 SECONDS) + add_ping_image(target_client, pointer, 3 SECONDS) + +/// Returns the total percentage of the sum of every mob's health and max health. +/datum/flock/proc/get_total_health_percentage() + var/numerator = 0 + var/denominator = 0 + for(var/mob/living/simple_animal/flock/bird as anything in (drones + bits)) + numerator += bird.health + denominator += bird.maxHealth + + if(denominator == 0) // somehow i guess + return 0 + return round(numerator / denominator * 100, 0.1) + +/// Returns the total amount of substrate by all flock mobs. +/datum/flock/proc/get_total_substrate() + . = 0 + for(var/mob/living/simple_animal/flock/drone/bird as anything in drones) + . += bird.substrate.has_points() + +/// Called when a turf reserved by a flock mob changes. /datum/flock/proc/reserved_turf_change(datum/source) SIGNAL_HANDLER free_turf(override_turf = source) +/// Called when a turf owned by the flock changes. +/datum/flock/proc/claimed_turf_change(turf/source) + SIGNAL_HANDLER + if(isflockturf(source)) + stop_tracking_turf(source) + claim_turf(source, FALSE) // WAll to floor or visa-versa, keep it updated. + else + stop_tracking_turf(source) + +/// Helper for adding a ping image to a client's screen and handling clean up. /datum/flock/proc/add_ping_image(client/C, image/ping, duration) if(isnull(C)) return @@ -361,6 +470,7 @@ RegisterSignal(C, COMSIG_PARENT_QDELETING, PROC_REF(on_client_gone), override = TRUE) addtimer(CALLBACK(src, PROC_REF(cleanup_ping_images), C, ping), 3 SECONDS) +/// Called by add_ping_image via timer callback. /datum/flock/proc/cleanup_ping_images(client/C, list/images_to_clean) if(isnull(C)) return @@ -372,22 +482,41 @@ active_pings -= C UnregisterSignal(C, COMSIG_PARENT_QDELETING) +/// Checks to see if the given atom is allowed to be there. Has lots of behavior like making drones dormant and snapping traces back to the station. +/datum/flock/proc/is_on_safe_z(atom/A) + var/turf/T = get_turf(A) + if(!T.z) + return FALSE + + if(flock_game_status == FLOCK_ENDGAME_LOST) + return TRUE + + if(is_station_level(T.z)) + return TRUE + + return FALSE + +/// Called when a client holding a ping image disconnects. /datum/flock/proc/on_client_gone(client/source) SIGNAL_HANDLER cleanup_ping_images() +/// Called when a unit under the flock's control dies. /datum/flock/proc/on_unit_death(datum/source) SIGNAL_HANDLER free_unit(source) +/// Called when an enemy atom is deleted. /datum/flock/proc/on_enemy_gone(datum/source) SIGNAL_HANDLER remove_enemy(source, TRUE) +/// Called when an ignored mob is deleted. /datum/flock/proc/on_ignore_gone(datum/source) SIGNAL_HANDLER remove_ignore(source, TRUE) +/// Instantiates all of the flock notice image singletons. /datum/flock/proc/create_hud_images() notice_images[FLOCK_NOTICE_RESERVED] = new /image{ icon = 'goon/icons/mob/featherzone.dmi'; @@ -437,6 +566,14 @@ appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM; } + notice_images[FLOCK_NOTICE_DECONSTRUCT] = new /image{ + icon = 'goon/icons/mob/featherzone.dmi'; + icon_state = "deconstruct"; + pixel_y = 16; + plane = ABOVE_LIGHTING_PLANE; + appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM; + } + notice_images[FLOCK_NOTICE_HEALTH] = new /image{ icon = 'goon/icons/mob/featherzone.dmi'; icon_state = "hp-100"; @@ -446,20 +583,85 @@ appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM; } +/// Setter for flock_game_status. +/datum/flock/proc/set_flock_game_status(new_status) + var/old_status = flock_game_status + if(old_status == new_status) + return null + + flock_game_status = new_status + + return old_status + +/// Update the relay status huds for all flock traces and the overmind. +/datum/flock/proc/update_relay_huds() + var/new_alpha + + switch(flock_game_status) + if(NONE) + var/percent_avail_bandwidth = min(available_bandwidth() / FLOCK_COMPUTE_COST_RELAY, 1) + var/percent_tiles = min((length(claimed_floors) + length(claimed_walls)) / FLOCK_TURFS_FOR_RELAY, 1) + new_alpha = round(255 * percent_avail_bandwidth * percent_tiles) + + if(FLOCK_ENDGAME_LOST) + new_alpha = 0 + + else + new_alpha = 255 + + var/new_desc + switch(flock_game_status) + if(NONE) + var/percent_avail_bandwidth = min(100, floor(available_bandwidth() / FLOCK_COMPUTE_COST_RELAY * 100)) + var/percent_tiles = min(100, floor((length(claimed_floors) + length(claimed_walls)) / FLOCK_TURFS_FOR_RELAY * 100)) + var/percent_total = floor((percent_avail_bandwidth / 100) * (percent_tiles / 100) * 100) + new_desc = "Overall Progress: [percent_total]%
Bandwidth: [percent_avail_bandwidth]%
Converted: [percent_tiles]%" + + if(FLOCK_ENDGAME_RELAY_BUILT) + new_desc = "Time until broadcast: [] seconds." + + if(FLOCK_ENDGAME_RELAY_ACTIVATING, FLOCK_ENDGAME_VICTORY) + new_desc = "!!! TRANSMITTING !!!" + + for(var/mob/camera/flock/ghost_bird in (traces + overmind)) + var/atom/movable/screen/flock_relay_status/status + if(ghost_bird.controlling_bird) + status = astype(ghost_bird.controlling_bird.hud_used, /datum/hud/flockdrone)?.relay_status + else + status = astype(ghost_bird.hud_used, /datum/hud/flockghost)?.relay_status + + status.desc = new_desc + status.alpha = new_alpha + if(status.flock_status == flock_game_status) + continue + + status.flock_status = flock_game_status + status.update_appearance() + /// Ends the flock if it is unable to continue spreading. /datum/flock/proc/consider_game_over() - if(flock_game_over) - return + if(!flock_started) + return FALSE + + if(flock_game_status == FLOCK_ENDGAME_LOST) + return TRUE + + if(flock_game_status == FLOCK_ENDGAME_VICTORY) + return TRUE if(length(drones)) - return + return FALSE if(locate(/obj/structure/flock/egg, structures) || locate(/obj/structure/flock/rift, structures)) - return + return FALSE game_over() + return TRUE +/// Kills off the flock. Pass completely_destroy = FALSE to allow the overmind to live on. /datum/flock/proc/game_over(completely_destroy = TRUE) + set waitfor = FALSE + // Cleanup any pings for(var/client/C in active_pings) cleanup_ping_images(C) @@ -468,14 +670,16 @@ for(var/turf/T as anything in turf_reservations) free_turf(override_turf = T) - claimed_floors.Cut() - claimed_walls.Cut() + for(var/turf/flockturf as anything in claimed_floors + claimed_walls) + stop_tracking_turf(flockturf) + CHECK_TICK // Extra lives if(!completely_destroy) + refresh_unlockables() return - flock_game_over = TRUE + set_flock_game_status(FLOCK_ENDGAME_LOST) // Kill overmind overmind?.so_very_sad_death() // Overmind can be null here if it died outside of game_over(). @@ -486,7 +690,11 @@ // Free units for(var/mob/living/simple_animal/flock/bird as anything in (bits + drones)) - free_unit(bird) + bird.dormantize() + + // Blow up structures + for(var/obj/structure/flock/structure as anything in structures) + structure.deconstruct(FALSE) // Remove ignores for(var/mob/M as anything in ignores) @@ -495,3 +703,9 @@ // Remove enemies for(var/mob/M as anything in enemies) remove_enemy(M, TRUE) + +/// Called when an atom marked for deconstruction is qdeleted. +/datum/flock/proc/deconstruct_mark_deleted(datum/source) + SIGNAL_HANDLER + + toggle_deconstruction_mark(source) diff --git a/code/modules/flockmind/flock_controller/flock_ui.dm b/code/modules/flockmind/flock_controller/flock_ui.dm index f91ed709a271..7a2a1be7063e 100644 --- a/code/modules/flockmind/flock_controller/flock_ui.dm +++ b/code/modules/flockmind/flock_controller/flock_ui.dm @@ -58,7 +58,7 @@ stats_info[++stats_info.len] = list(name = "Partitions divided: ", "value" = stat_traces_made) stats_info[++stats_info.len] = list(name = "Tiles converted: ", "value" = stat_tiles_made) stats_info[++stats_info.len] = list(name = "Structures materialized: ", "value" = stat_structures_made) - stats_info[++stats_info.len] = list(name = "Highest compute: ", "value" = stat_highest_compute) + stats_info[++stats_info.len] = list(name = "Highest bandwidth: ", "value" = stat_highest_bandwidth) return data @@ -77,8 +77,8 @@ if("jump_to") var/atom/movable/target = locate(params["origin"]) var/turf/T = get_turf(target) - if(isnull(T) || !is_station_level(T.z)) - to_chat(user, span_alert("They are beyond your reach.")) + if(isnull(T) || !is_on_safe_z(target)) + to_chat(user, span_alert("They are beyond our reach.")) return if(isflockdrone(user)) @@ -99,3 +99,21 @@ if(istype(bird)) bird.rally(get_turf(user)) return TRUE + + if("cancel_tealprint") + var/obj/structure/flock/tealprint/tealprint = locate(params["origin"]) + tealprint?.try_cancel_structure() + return TRUE + + if("delete_trace") + var/mob/camera/flock/trace/flocktrace = locate(params["origin"]) + if(!istype(flocktrace)) + message_admins("Warning: possible href exploit by [key_name(usr)] - attempted to remove a flocktrace that wasn't a flocktrace: [flocktrace]") + log_game("Warning: possible href exploit by [key_name(usr)] - attempted to remove a flocktrace that wasn't a flocktrace: [flocktrace]") + return TRUE + + if(tgui_alert(user, "This will destroy the Flocktrace. Are you sure you want to do this?", "Confirmation", list("Yes", "No")) == "Yes") + flock_talk(null, "Partition [flocktrace.real_name] has been reintegrated into flock background processes.", src, involuntary = TRUE) + to_chat(flocktrace, span_flocksay("Your higher cognition has been forcibly reintegrated into the collective will of the flock.")) + flocktrace.so_very_sad_death() + return TRUE diff --git a/code/modules/flockmind/flock_convert_atom.dm b/code/modules/flockmind/flock_convert_atom.dm index 553237c0bca1..995cf6f0b3d7 100644 --- a/code/modules/flockmind/flock_convert_atom.dm +++ b/code/modules/flockmind/flock_convert_atom.dm @@ -2,7 +2,10 @@ if(!T?.can_flock_convert(force)) return - if(iswallturf(T)) + if(isflockturf(T)) + . = T + + else if(iswallturf(T)) . = T.ChangeTurf(/turf/closed/wall/flock) else if(isfloorturf(T)) @@ -20,42 +23,41 @@ O.try_flock_convert(flock, force) -/// Attempt to convert an object. Default behavior is to qdel. +/// Attempt to convert an object. Default behavior is to do nothing. /obj/proc/try_flock_convert(datum/flock/flock, force) - qdel(src) - -// No -/obj/effect/try_flock_convert(datum/flock/flock, force) return /obj/machinery/camera/try_flock_convert(datum/flock/flock, force) atom_break() -// No -/obj/structure/cable/try_flock_convert(datum/flock/flock, force) - return - -// No -/obj/machinery/atmospherics/try_flock_convert(datum/flock/flock, force) - return - /obj/structure/window/try_flock_convert(datum/flock/flock, force) var/turf/T = loc + var/obj/structure/window/flock/new_window qdel(src) + if(fulltile) - return new /obj/structure/window/flock/fulltile(T) + new_window = new /obj/structure/window/flock/fulltile(T) + else + new_window = new /obj/structure/window/flock(T) + new_window.dir = dir - var/obj/W = new /obj/structure/window/flock(T) - W.dir = dir - return W + new_window.AddComponent(/datum/component/flock_interest, flock) + return new_window /obj/machinery/door/try_flock_convert(datum/flock/flock, force) var/turf/T = loc qdel(src) return new /obj/machinery/door/flock(T) +// This results in double layered doors +/obj/machinery/door/firedoor/try_flock_convert(datum/flock/flock, force) + return + /obj/structure/low_wall/try_flock_convert(datum/flock/flock, force) set_material(/datum/material/gnesis, TRUE) + AddComponent(/datum/component/flock_object) + AddComponent(/datum/component/flock_protection, report_unarmed=FALSE) + AddComponent(/datum/component/flock_interest, flock) return src /obj/machinery/light/try_flock_convert(datum/flock/flock, force) @@ -68,6 +70,22 @@ . = new /obj/machinery/light/floor/has_bulb/flock(loc) qdel(src) +/obj/machinery/computer4/try_flock_convert(datum/flock/flock, force) + . = new /obj/structure/flock/compute(loc, flock) + qdel(src) + +/obj/machinery/computer/try_flock_convert(datum/flock/flock, force) + . = new /obj/structure/flock/compute(loc, flock) + qdel(src) + +/obj/machinery/seed_extractor/try_flock_convert(datum/flock/flock, force) + . = new /obj/structure/flock/compute(loc, flock) + qdel(src) + +/obj/machinery/telecomms/try_flock_convert(datum/flock/flock, force) + . = new /obj/structure/flock/compute(loc, flock) + qdel(src) + /turf/proc/can_flock_convert(force) return FALSE diff --git a/code/modules/flockmind/flock_hud.dm b/code/modules/flockmind/flock_hud.dm index b9b65f173cc8..1b635feacde4 100644 --- a/code/modules/flockmind/flock_hud.dm +++ b/code/modules/flockmind/flock_hud.dm @@ -1,5 +1,18 @@ +// Shows an image to flock mobs. /datum/atom_hud/alternate_appearance/basic/flock - /datum/atom_hud/alternate_appearance/basic/flock/mobShouldSee(mob/M) return isflockmob(M) + + +// Shows an image to nnon-ghost flockmobs. +/datum/atom_hud/alternate_appearance/basic/flock_simplemob + +/datum/atom_hud/alternate_appearance/basic/flock_simplemob/mobShouldSee(mob/M) + return istype(M, /mob/living/simple_animal/flock) + +// Shows an image to non-observer non-flock mobs. +/datum/atom_hud/alternate_appearance/basic/not_flock + +/datum/atom_hud/alternate_appearance/basic/not_flock/mobShouldSee(mob/M) + return !(isflockmob(M) || isobserver(M)) diff --git a/code/modules/flockmind/flock_items/flock_cube.dm b/code/modules/flockmind/flock_items/flock_cube.dm new file mode 100644 index 000000000000..0798719d608b --- /dev/null +++ b/code/modules/flockmind/flock_items/flock_cube.dm @@ -0,0 +1,9 @@ +// The cuuuuuuuuuuuuuube +/obj/item/flock_cube + name = "weird cube" + desc = "A weird looking cube." + icon_state = "cube" + icon = 'goon/icons/mob/featherzone.dmi' + + /// How much shit in da cube + var/substrate = 10 diff --git a/code/modules/flockmind/flock_mob/flock_mob.dm b/code/modules/flockmind/flock_mob/flock_mob.dm index 23e216de8d6f..29b2f331a7c4 100644 --- a/code/modules/flockmind/flock_mob/flock_mob.dm +++ b/code/modules/flockmind/flock_mob/flock_mob.dm @@ -34,19 +34,37 @@ stop_automated_movement = TRUE movement_type = FLOATING + var/list/actions_to_grant = list( + /datum/action/cooldown/flock/convert, + ) + + var/icon_dormant = "drone-dormant" + /// Flock datum. Can be null. - var/datum/flock/flock + var/tmp/datum/flock/flock + + /// Physical resources for constructing structures or flockrunning. + var/tmp/datum/point_holder/substrate + /// Type of point holder to use + var/point_holder_type = /datum/point_holder - var/datum/point_holder/resources + /// Flock ID nametag + var/tmp/obj/effect/abstract/info_tag/flock/name_tag + /// Tag for the mob's current AI task. + var/tmp/obj/effect/abstract/info_tag/flock/info/task_tag - var/compute_provided = 0 + var/bandwidth_provided = 0 + + /// If, while part of an active flock, a flockmob leaves the station, they become dormant. + var/tmp/dormant = FALSE /mob/living/simple_animal/flock/Initialize(mapload, join_flock) . = ..() RegisterSignal(ai_controller, COMSIG_AI_STATUS_CHANGE, PROC_REF(on_ai_status_change)) - var/datum/action/cooldown/flock/convert/convert_action = new - convert_action.Grant(src) + for(var/action_path in actions_to_grant) + var/datum/action/action = new action_path + action.Grant(src) set_combat_mode(TRUE) @@ -56,8 +74,13 @@ flock = join_flock || get_default_flock() flock?.add_unit(src) - resources = new - resources.adjust_points(1000000) + substrate = new point_holder_type + + name_tag = new() + name_tag.set_parent(src) + + task_tag = new() + task_tag.set_parent(src) update_health_notice() update_light_state() @@ -65,8 +88,13 @@ /mob/living/simple_animal/flock/Destroy() flock?.free_unit(src) flock = null + QDEL_NULL(name_tag) + QDEL_NULL(task_tag) return ..() +/mob/living/simple_animal/flock/create_mood() + return // THEY DO NOT FEEEEEEEEEEEEEEEL + /mob/living/simple_animal/flock/set_stat(new_stat) . = ..() switch(stat) @@ -77,6 +105,10 @@ REMOVE_TRAIT(src, TRAIT_MOVE_FLOATING, STAT_TRAIT) ADD_TRAIT(src, TRAIT_NO_FLOATING_ANIM, STAT_TRAIT) +/mob/living/simple_animal/flock/update_name(updates) + . = ..() + name_tag?.set_text(real_name) + /mob/living/simple_animal/flock/say(message, bubble_type, list/spans, sanitize, datum/language/language, ignore_spam, forced, filterproof, range) . = ..() if(!.) @@ -90,13 +122,30 @@ /mob/living/simple_animal/flock/treat_message(message, correct_grammar = FALSE) . = ..() +/mob/living/simple_animal/flock/update_icon_state() + if(stat == DEAD) + icon_state = icon_dead + else + if(dormant) + icon_state = icon_dormant + else + icon_state = icon_living + return ..() + +/mob/living/simple_animal/flock/on_changed_z_level(turf/old_turf, turf/new_turf) + . = ..() + if(flock && new_turf && !flock.is_on_safe_z(new_turf)) + dormantize() + /mob/living/simple_animal/flock/updatehealth(cause_of_death) . = ..() update_health_notice() /mob/living/simple_animal/flock/death(gibbed, cause_of_death) - . = ..() - flock.remove_notice(src, FLOCK_NOTICE_HEALTH) + flock?.remove_notice(src, FLOCK_NOTICE_HEALTH) + flock?.free_unit(src) + flock?.stat_deaths++ + return ..() /mob/living/simple_animal/flock/get_flock_id() return real_name @@ -110,30 +159,48 @@ notice = flock.add_notice(src, FLOCK_NOTICE_HEALTH) var/image/I = notice.image - I.icon_state = "hp-[getHealthPercent()]" + I.icon_state = "hp-[round(getHealthPercent(), 10)]" /mob/living/simple_animal/flock/proc/get_flock_data() var/list/data = list() data["name"] = real_name data["health"] = getHealthPercent() - data["resources"] = resources.has_points() + data["resources"] = substrate.has_points() data["area"] = get_area_name(src, TRUE) || "???" data["ref"] = REF(src) return data +/// Become dormant. A husk. A shell. +/mob/living/simple_animal/flock/proc/dormantize() + if(dormant) + return + + dormant = TRUE + + cancel_do_afters() + ai_controller.set_ai_status(AI_STATUS_OFF) + flock?.free_unit(src) + update_light_state() + + update_appearance(UPDATE_ICON_STATE) + /mob/living/simple_animal/flock/proc/rally(turf/location) if(!isturf(location)) return - if(ai_controller.ai_status == AI_OFF || ckey) + if(ai_controller.ai_status == AI_STATUS_OFF || ckey) return ai_controller.CancelActions() ai_controller.queue_behavior(/datum/ai_behavior/flock/rally, location) +/// Helper for keeping consistency across tesk name sets. +/mob/living/simple_animal/flock/proc/set_task_desc(text) + task_tag?.set_text("Task: [text || "idling"]") + /// Turn the light on or off, based on if the mob is doing shit or not. /mob/living/simple_animal/flock/proc/update_light_state() - if(stat == DEAD) + if(stat == DEAD || dormant) set_light_on(FALSE) return @@ -149,10 +216,10 @@ /mob/living/simple_animal/flock/vv_edit_var(var_name, var_value) switch(var_name) - if(NAMEOF(src, compute_provided)) - flock?.compute.adjust_points(-compute_provided) + if(NAMEOF(src, bandwidth_provided)) + flock?.bandwidth.adjust_points(-bandwidth_provided) ..() - flock?.compute.adjust_points(compute_provided) + flock?.bandwidth.adjust_points(bandwidth_provided) return TRUE return ..() diff --git a/code/modules/flockmind/flock_mob/flockbit.dm b/code/modules/flockmind/flock_mob/flockbit.dm index 253b89a8d2c6..bc33e0a4c6ae 100644 --- a/code/modules/flockmind/flock_mob/flockbit.dm +++ b/code/modules/flockmind/flock_mob/flockbit.dm @@ -1,10 +1,17 @@ /mob/living/simple_animal/flock/bit name = "flockbit" icon_state = "flockbit" + icon_dormant = "bit-dormant" + + move_force = MOVE_FORCE_VERY_WEAK maxHealth = 10 + health = 10 density = FALSE pass_flags = PASSTABLE + // Flockbits don't get specific AI behaviors that would make this broken. + point_holder_type = /datum/point_holder/infinite + /mob/living/simple_animal/flock/bit/Initialize(mapload) . = ..() flock?.stat_bits_made++ @@ -12,6 +19,19 @@ set_real_name(flock_realname(FLOCK_TYPE_BIT)) name = flock_name(FLOCK_TYPE_BIT) +/mob/living/simple_animal/flock/bit/examine(mob/user) + if(!isflockmob(user)) + return ..() + + . = list( + span_flocksay("###=- Ident confirmed, data packet received."), + span_flocksay("ID: [real_name]"), + span_flocksay("Flock: [flock?.name || "N/A"]"), + span_flocksay("System Integrity: [round(health / maxHealth, 0.1) * 100]"), + span_flocksay("Cognition: [HAS_TRAIT(src, TRAIT_AI_PAUSED) ? "AWARE" : "PREDEFINED"]"), + span_flocksay("###=-"), + ) + /mob/living/simple_animal/flock/bit/proc/i_just_split(turf/avoid) ai_controller.PauseAi(0.3 SECONDS) addtimer(CALLBACK(src, PROC_REF(step_away_from), avoid), 0.2 SECONDS) diff --git a/code/modules/flockmind/flock_mob/flockdrone.dm b/code/modules/flockmind/flock_mob/flockdrone.dm index bb42d7f9422e..bfc2787b6a74 100644 --- a/code/modules/flockmind/flock_mob/flockdrone.dm +++ b/code/modules/flockmind/flock_mob/flockdrone.dm @@ -1,16 +1,36 @@ /mob/living/simple_animal/flock/drone + hud_type = /datum/hud/flockdrone ai_controller = /datum/ai_controller/flock/drone - compute_provided = FLOCK_COMPUTE_COST_DRONE - var/flock_phasing = FALSE + move_force = MOVE_FORCE_WEAK + + actions_to_grant = list( + /datum/action/cooldown/flock/release_control, + /datum/action/cooldown/flock/convert, + /datum/action/cooldown/flock/deconstruct, + /datum/action/cooldown/flock/flock_heal, + /datum/action/cooldown/flock/cage_mob, + /datum/action/cooldown/flock/nest, + /datum/action/cooldown/flock/deposit, + ) + + bandwidth_provided = FLOCK_COMPUTE_COST_DRONE + /// A mob possessing this mob. - var/mob/camera/flock/controlled_by + var/tmp/mob/camera/flock/controlled_by + + var/list/datum/flockdrone_part/parts = list() + /// Active flockdrone part. + var/datum/flockdrone_part/active_part /mob/living/simple_animal/flock/drone/Initialize(mapload, join_flock) + create_parts() + set_active_part(parts[1]) . = ..() flock?.stat_drones_made++ ADD_TRAIT(src, TRAIT_IMPORTANT_SPEAKER, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_ALWAYS_BUTCHERABLE, INNATE_TRAIT) AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE) set_real_name(flock_realname(FLOCK_TYPE_DRONE)) @@ -19,28 +39,211 @@ if(stat == CONSCIOUS) INVOKE_ASYNC(src, TYPE_PROC_REF(/atom/movable, say), pick(GLOB.flockdrone_created_phrases), null, null, null, null, null, "flock spawn") - var/datum/action/cooldown/flock/flock_heal/repair = new - repair.Grant(src) - /mob/living/simple_animal/flock/drone/Destroy() release_control() - QDEL_NULL(resources) + QDEL_NULL(substrate) + QDEL_LIST(parts) + active_part = null // whatever was here was qdeleted by qdel_list(parts) return ..() +/mob/living/simple_animal/flock/drone/examine(mob/user) + if(!isflockmob(user)) + return ..() + + var/cognition = "TORPID" + if(stat == DEAD) + cognition = "DEAD" + else if(controlled_by || ckey) + cognition = "SAPIENT" + else if(dormant) + cognition = "ABSENT" + else if(HAS_TRAIT(src, TRAIT_AI_PAUSED)) + cognition = "HIBERNATING" + + . = list( + span_flocksay("###=- Ident confirmed, data packet received."), + controlled_by ? span_flocksay("ID: [controlled_by.real_name] (controlling [real_name])") : span_flocksay("ID: [real_name]"), + span_flocksay("Flock: [flock?.name || "N/A"]"), + span_flocksay("Substrate: [substrate.has_points()]"), + span_flocksay("System Integrity: [round(health / maxHealth, 0.1) * 100]"), + span_flocksay("Cognition: [cognition]"), + ) + + if(cognition == "TORPID" && length(ai_controller?.current_behaviors)) + var/datum/ai_behavior/flock/flock_behavior = locate() in ai_controller.current_behaviors + if(istype(flock_behavior)) + . += span_flocksay("Task: [flock_behavior.name]") + + . += span_flocksay("###=-") + /mob/living/simple_animal/flock/drone/death(gibbed, cause_of_death) - stop_flockphase() + stop_flockphase(TRUE) + release_control() say(pick(GLOB.flockdrone_death_phrases)) if(flock) - flock_talk(null, "Connection to drone [real_name] lost.", flock) + flock_talk(null, "Connection to drone [real_name] lost.", flock, involuntary = TRUE) + + var/datum/flockdrone_part/absorber/absorber = locate() in parts + absorber.try_drop_item() return ..() /mob/living/simple_animal/flock/drone/Life(delta_time, times_fired) . = ..() if(HAS_TRAIT(src, TRAIT_FLOCKPHASE)) - resources.remove_points(1) - if(!resources.has_points(1)) + if(avoid_stop_flockphase()) + flockphase_tax() + else stop_flockphase() + +/mob/living/simple_animal/flock/drone/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + . = ..() + + // Stop existing flockphasing if we can't flockphase, also update the oldloc if it was a flock floor. + if(HAS_TRAIT(src, TRAIT_FLOCKPHASE)) + if(istype(old_loc, /turf/open/floor/flock)) + var/turf/open/floor/flock/flockfloor = old_loc + LAZYREMOVE(flockfloor.flockrunning_mobs, src) + flockfloor.update_power() + + // Mob can't flockphase. + if(!isflockturf(loc) || !can_flockphase()) + stop_flockphase(TRUE) + return + + // Being in a wall forces flockphase if able. + if(istype(loc, /turf/closed/wall/flock)) + if(flockphase_tax()) + start_flockphase() + return + return + + // Either the client wants to flockphase, of if uncliented, already flockphasing. + var/wants_to_flockphase = client ? client.keys_held["Shift"] : HAS_TRAIT(src, TRAIT_FLOCKPHASE) + if(!wants_to_flockphase) + stop_flockphase() + return + + if(istype(loc, /turf/open/floor/flock)) + var/turf/open/floor/flock/flockfloor = loc + // Also incapable of flockphasing. + if(flockfloor.broken) + stop_flockphase(TRUE) + return + + if(flockphase_tax()) + start_flockphase() + LAZYADD(flockfloor.flockrunning_mobs, src) + flockfloor.update_power() + return + return + +/mob/living/simple_animal/flock/drone/get_status_tab_items() + . = ..() + . += "Substrate: [substrate.has_points()]" + +/mob/living/simple_animal/flock/drone/MouseDroppedOn(atom/dropping, atom/user) + . = ..() + if(dropping != user || !istype(user, /mob/camera/flock)) + return + + var/mob/camera/flock/ghost_bird = user + if(istype(user) && ghost_bird.flock == flock) + take_control(user) + +/mob/living/simple_animal/flock/drone/Click(location, control, params) + . = ..() + var/mob/camera/flock/ghost_bird = usr + if(!istype(ghost_bird)) + return + + var/list/modifiers = params2list(params) + if(!modifiers?[RIGHT_CLICK]) + return + + var/list/choices = list() + var/image/I = image('icons/hud/radial.dmi', "radial_use") + choices["order"] = I + + I = image('icons/hud/radial.dmi', "radial_control") + choices["control"] = I + + var/result = show_radial_menu(usr, get_turf(src), choices, "[REF(usr)]_flock_click") + switch(result) + if("control") + take_control(ghost_bird) + + if("order") + var/datum/action/cooldown/flock/control_drone/order_action = locate() in ghost_bird.actions + if(order_action) + if(order_action.selected_bird) + order_action.unset_click_ability(usr) + else + order_action.set_click_ability(usr) + order_action.Trigger(target = src) + +/mob/living/simple_animal/flock/drone/resolve_unarmed_attack(atom/attack_target, list/modifiers) + if(modifiers?[RIGHT_CLICK]) + active_part?.right_click_on(attack_target, TRUE) + else + active_part?.left_click_on(attack_target, TRUE) + +/mob/living/simple_animal/flock/drone/RangedAttack(atom/A, modifiers) + . = ..() + if(.) + return + + if(modifiers?[RIGHT_CLICK]) + active_part?.right_click_on(A, FALSE) + else + active_part?.left_click_on(A, FALSE) + +/mob/living/simple_animal/update_health_hud() + var/severity = 0 + var/healthpercent = ceil((health/maxHealth) * 100) + if(hud_used?.healthdoll) //to really put you in the boots of a simplemob + var/atom/movable/screen/flockdrone_health/healthdoll = hud_used.healthdoll + switch(healthpercent) + if(100 to INFINITY) + severity = 0 + if(81 to 99) + severity = 1 + if(65 to 80) + severity = 2 + if(49 to 64) + severity = 3 + if(33 to 48) + severity = 4 + if(17 to 32) + severity = 5 + if(1 to 16) + severity = 6 + else + severity = 7 + + healthdoll.icon_state = "health[severity]" + + if(severity > 0) + overlay_fullscreen("brute", /atom/movable/screen/fullscreen/brute, min(severity, 6)) + else + clear_fullscreen("brute") + + +/mob/living/simple_animal/flock/drone/harvest(mob/living/user) + var/list/loot = list( + /obj/item/stack/sheet/gnesis = 1, + /obj/item/shard/gnesis_glass = 1, + ) + + for(var/i in 3 to 6) + var/path = pick_weight(loot) + new path(drop_location()) + + playsound(src, SFX_SHATTER, 30, TRUE, SILENCED_SOUND_EXTRARANGE) + + // Spawn flock organs here + qdel(src) + /mob/living/simple_animal/flock/drone/get_flock_data() var/list/data = ..() var/current_behavior_name @@ -57,6 +260,37 @@ data["task"] = current_behavior_name || "hibernating" return data +/mob/living/simple_animal/flock/drone/on_ai_status_change(datum/ai_controller/source, ai_status) + . = ..() + if(ai_status == AI_STATUS_OFF && controlled_by) + task_tag.set_text("Controlled By: [controlled_by.real_name]") + +/mob/living/simple_animal/flock/drone/dormantize() + if(!flock) + return ..() + + if(controlled_by) + release_control(FALSE) + flock_talk(null, "Connection to drone [real_name] lost.", flock, involuntary = TRUE) + + spawn(-1) + say("error: out of signal range, disconnecting") + return ..() + +/// Sets the active flockdrone part. +/mob/living/simple_animal/flock/drone/proc/set_active_part(datum/flockdrone_part/new_part) + var/datum/flockdrone_part/old_part = active_part + active_part = new_part + + active_part.screen_obj?.update_appearance() + old_part?.screen_obj?.update_appearance() + +/// Create all of the part datums for this mob. +/mob/living/simple_animal/flock/drone/proc/create_parts() + parts += new /datum/flockdrone_part/converter(src) + parts += new /datum/flockdrone_part/incapacitator(src) + parts += new /datum/flockdrone_part/absorber(src) + /mob/living/simple_animal/flock/drone/proc/start_flockphase() if(HAS_TRAIT(src, TRAIT_FLOCKPHASE)) return FALSE @@ -77,10 +311,12 @@ animate(src, color = color_matrix, transform = shrink, time = 0.5 SECONDS, easing = SINE_EASING) return TRUE -/mob/living/simple_animal/flock/drone/proc/stop_flockphase() +/mob/living/simple_animal/flock/drone/proc/stop_flockphase(force) if(!HAS_TRAIT(src, TRAIT_FLOCKPHASE)) return FALSE + if(!force && avoid_stop_flockphase()) + return FALSE playsound(src, 'goon/sounds/flockmind/flockdrone_floorrun.ogg', 30, TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE) @@ -97,7 +333,8 @@ if(istype(loc, /turf/open/floor/flock)) var/turf/open/floor/flock/flockfloor = loc - flockfloor.turn_off() + LAZYREMOVE(flockfloor.flockrunning_mobs, src) + flockfloor.update_power() var/turf/turfloc = loc if(turfloc.can_flock_occupy(src)) @@ -108,22 +345,35 @@ forceMove(T) break +/// Called in stop_flockphase() to attempt stopping flockphase due to being in a wall or something. +/mob/living/simple_animal/flock/drone/proc/avoid_stop_flockphase() + if(!can_flockphase()) + return FALSE + + if(isclosedturf(loc)) + return TRUE + + if(client?.keys_held["Shift"]) + return TRUE + +/// Returns TRUE if the drone can flockphase. /mob/living/simple_animal/flock/drone/proc/can_flockphase() + if(stat != CONSCIOUS) + return FALSE + if(length(grabbed_by)) return FALSE - if(!resources.has_points()) + if(!substrate.has_points()) return FALSE return TRUE +/// Deducts the substrate tax for flockphasing, ending flockphase if the drone ran out of points. /mob/living/simple_animal/flock/drone/proc/flockphase_tax() - if(!HAS_TRAIT(src, TRAIT_FLOCKPHASE)) - return FALSE - - resources.remove_points(1) - if(!resources.has_points()) - stop_flockphase() + substrate.remove_points(1) + if(!substrate.has_points()) + stop_flockphase(TRUE) return FALSE return TRUE @@ -144,7 +394,7 @@ if(controlled_by.mind) controlled_by.mind.transfer_to(src) else - key = controlled_by.key + PossessByPlayer(controlled_by.key) if(isflocktrace(controlled_by)) flock.add_notice(src, FLOCK_NOTICE_FLOCKTRACE_CONTROL) @@ -154,12 +404,20 @@ to_chat(src, "\[SYSTEM: Control of drone [real_name] established.\]") return TRUE -/mob/living/simple_animal/flock/drone/proc/release_control() +/mob/living/simple_animal/flock/drone/proc/release_control(go_dormant = FALSE) if(isnull(controlled_by)) return + task_tag.set_text("") + + var/dest_was_safe = TRUE + var/turf/destination = get_turf(src) + if(!flock.is_on_safe_z(destination)) + dest_was_safe = FALSE + destination = get_turf(pick_safe(flock.drones)) || get_safe_random_station_turf() + var/mob/camera/flock/master_bird = controlled_by - master_bird = null + controlled_by = null if(flock) flock.remove_notice(src, FLOCK_NOTICE_FLOCKMIND_CONTROL) @@ -167,18 +425,23 @@ if(isnull(master_bird) && ckey) if(flock) - master_bird = new /mob/camera/flock/trace(src, flock) + master_bird = new /mob/camera/flock/trace(destination, flock) else ghostize(FALSE) if(!master_bird) return - master_bird.forceMove(get_turf(src)) + master_bird.forceMove(destination) if(mind) mind.transfer_to(master_bird) - flock_talk(null, "Control of [real_name] surrendered.", flock) + flock_talk(null, "Control of [real_name] surrendered.", flock, involuntary = TRUE) + if(!dest_was_safe) + to_chat(master_bird, span_warning("You feel your consciousness weaking as you are ripped further from your rift, and you retreat back to safety.")) + + if(!flock && go_dormant) + dormantize() /mob/living/simple_animal/flock/drone/proc/split_into_bits() ai_controller.PauseAi(3 SECONDS) diff --git a/code/modules/flockmind/flock_mob/flockdrone_parts/absorber.dm b/code/modules/flockmind/flock_mob/flockdrone_parts/absorber.dm new file mode 100644 index 000000000000..37f2b9cc4fa3 --- /dev/null +++ b/code/modules/flockmind/flock_mob/flockdrone_parts/absorber.dm @@ -0,0 +1,95 @@ +/datum/flockdrone_part/absorber + var/obj/item/held_item + + var/absorption_rate = 4 + /// Per point of integrity, generate this much substrate. + var/integrity_substrate_ratio = 4 + +/datum/flockdrone_part/absorber/Destroy(force, ...) + QDEL_NULL(held_item) + return ..() + +/datum/flockdrone_part/absorber/left_click_on(atom/target, in_reach) + var/obj/item/I = target + if(!in_reach || !istype(I) || !isturf(I.loc)) + return + + return try_pickup_item(I) + +/datum/flockdrone_part/absorber/process(delta_time) + var/added = 0 + var/obj/item/eating = held_item + + if(istype(eating, /obj/item/flock_cube)) + var/obj/item/flock_cube/cube = eating + added = cube.substrate + drone.substrate.add_points(added) + qdel(cube) + to_chat(drone, span_notice("We decompile the resource cache, adding [added] substrate to our reserves.")) + else + added = eating.take_damage(absorption_rate * delta_time, BRUTE, ACID, sound_effect = FALSE, armor_penetration = 100) + drone.substrate.add_points(added * integrity_substrate_ratio) + + if(!held_item) // if take_damage qdeletes it, it becomes null due to signal stuff. + to_chat(drone, span_notice("We finish converting [eating] into substrate.")) + return + + playsound(drone, SFX_SPARKS, 30, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE) + +/datum/flockdrone_part/absorber/proc/try_pickup_item(obj/item/I) + if(held_item) + return FALSE + + if(I.item_flags & ABSTRACT) + return FALSE + + drone.face_atom(I) + I.do_pickup_animation(drone, I.loc) + I.forceMove(drone) + if(QDELETED(I)) + return FALSE + + held_item = I + RegisterSignal(I, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING), PROC_REF(held_item_moved)) + START_PROCESSING(SSprocessing, src) + + var/matrix/first_matrix = matrix() + first_matrix.Turn(-45) + first_matrix.Scale(1.2, 0.6) + var/matrix/second_matrix = matrix() + first_matrix.Turn(45) + first_matrix.Scale(0.6, 1.2) + animate(held_item, loop= -1, color="#00ffd7", transform= first_matrix, time = 2 SECONDS, tag = "flockdrone_eat") + animate(color = "#ffffff", transform = second_matrix, time = 2 SECONDS) + + screen_obj?.vis_contents += held_item + return TRUE + +/datum/flockdrone_part/absorber/proc/try_drop_item(move = TRUE) + if(!held_item) + return FALSE + + UnregisterSignal(held_item, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING)) + + var/obj/item/old_item = held_item + animate(held_item, tag = "flockdrone_eat") + held_item = null + + STOP_PROCESSING(SSprocessing, src) + screen_obj?.vis_contents -= old_item + + if(!move) + return TRUE + + var/turf/drop_loc = drone.drop_location() + if(drop_loc) + old_item.forceMove(drop_loc) + else + qdel(old_item) + + return TRUE + +/datum/flockdrone_part/absorber/proc/held_item_moved(datum/source) + SIGNAL_HANDLER + + try_drop_item(FALSE) diff --git a/code/modules/flockmind/flock_mob/flockdrone_parts/converter.dm b/code/modules/flockmind/flock_mob/flockdrone_parts/converter.dm new file mode 100644 index 000000000000..0919b7c781cf --- /dev/null +++ b/code/modules/flockmind/flock_mob/flockdrone_parts/converter.dm @@ -0,0 +1,31 @@ +/datum/flockdrone_part/converter + +/datum/flockdrone_part/converter/left_click_on(atom/target, in_reach) + if(!in_reach) + return + + if(isliving(target)) + return try_cage(target) + + if(istype(target, /obj/structure/flock/tealprint)) + var/datum/action/cooldown/flock/deposit/deposit_action = locate() in drone.actions + return deposit_action.Trigger(target = target) + + var/turf/T = get_turf(target) + var/datum/action/cooldown/flock/convert/convert_action = locate() in drone.actions + return convert_action.Trigger(target = T) + +/datum/flockdrone_part/converter/right_click_on(atom/target, in_reach) + if(!in_reach) + return + + var/datum/action/cooldown/flock/deconstruct/decon_action = locate() in drone.actions + return decon_action.Trigger(target = target) + +/datum/flockdrone_part/converter/proc/try_cage(mob/living/victim) + if(isflockmob(victim)) + to_chat(drone, span_warning("ERROR: Unable to imprison substrate construct.")) + return FALSE + + var/datum/action/cooldown/flock/cage_mob/cage_action = locate() in drone.actions + return cage_action.Trigger(target = victim) diff --git a/code/modules/flockmind/flock_mob/flockdrone_parts/flockdrone_part.dm b/code/modules/flockmind/flock_mob/flockdrone_parts/flockdrone_part.dm new file mode 100644 index 000000000000..303fb45e6375 --- /dev/null +++ b/code/modules/flockmind/flock_mob/flockdrone_parts/flockdrone_part.dm @@ -0,0 +1,24 @@ +/datum/flockdrone_part + var/mob/living/simple_animal/flock/drone/drone + /// Reference to the associated screen object, if it exists (grr mobs that havent had a player connected) + var/atom/movable/screen/flockdrone_part/screen_obj + +/datum/flockdrone_part/New(drone) + src.drone = drone + +/datum/flockdrone_part/Destroy(force, ...) + screen_obj?.part_ref = null + screen_obj = null + drone = null + return ..() + +/datum/flockdrone_part/proc/is_active() + return drone.active_part == src + +/// Called when a drone with this part active left clicks on an atom. in_reach is TRUE if the target atom is reachable. +/datum/flockdrone_part/proc/left_click_on(atom/target, in_reach) + return + +/// Called when a drone with this part active right clicks on an atom. in_reach is TRUE if the target atom is reachable. +/datum/flockdrone_part/proc/right_click_on(atom/target, in_reach) + return diff --git a/code/modules/flockmind/flock_mob/flockdrone_parts/incapacitator.dm b/code/modules/flockmind/flock_mob/flockdrone_parts/incapacitator.dm new file mode 100644 index 000000000000..cdd1ee5c7662 --- /dev/null +++ b/code/modules/flockmind/flock_mob/flockdrone_parts/incapacitator.dm @@ -0,0 +1,48 @@ +/datum/flockdrone_part/incapacitator + /// Maximum shots that can be stored + var/max_shots = 5 + + /// How many shots we have left + var/shot_count = 5 + + /// Time between shots cooldown + COOLDOWN_DECLARE(shoot_cd) + /// Time between shots + var/time_between_shots = 1.2 SECONDS + + /// Time to gain 1 charge. + var/recharge_time = 4 SECONDS + + var/recharge_timer_id + +/datum/flockdrone_part/incapacitator/left_click_on(atom/target, in_reach) + if(!COOLDOWN_FINISHED(src, shoot_cd)) + return FALSE + + if(!shot_count) + return FALSE + + shot_count -= 1 + screen_obj?.update_appearance() + + var/obj/projectile/energy/flock_bolt/bolt = new(get_turf(drone)) + bolt.preparePixelProjectile(target, drone) + bolt.fire() + playsound(drone, 'sound/weapons/resonator_fire.ogg', 50, TRUE) + + if(!recharge_timer_id) + recharge_timer_id = addtimer(CALLBACK(src, PROC_REF(recharge)), recharge_time) + + COOLDOWN_START(src, shoot_cd, time_between_shots) + return TRUE + +/// Increments shot count by 1 and starts the recharge timer if necessary. +/datum/flockdrone_part/incapacitator/proc/recharge() + shot_count = min(shot_count + 1, max_shots) + screen_obj?.update_appearance() + + if(shot_count == max_shots) + recharge_timer_id = null + else + recharge_timer_id = addtimer(CALLBACK(src, PROC_REF(recharge)), recharge_time) + diff --git a/code/modules/flockmind/flock_say.dm b/code/modules/flockmind/flock_say.dm index dfaac88c959f..ae4d618d2c81 100644 --- a/code/modules/flockmind/flock_say.dm +++ b/code/modules/flockmind/flock_say.dm @@ -80,14 +80,17 @@ var/mob/camera/flock/overmind/ghost_bird = speaker used_name = ghost_bird.real_name + var/silicon_message = stars(message, 50) if(mob_speaker) var/say_verb = pick("sings", "clicks", "whistles", "intones", "transmits", "submits", "uploads") message = "[say_verb], \"[message]\"" + silicon_message = "[say_verb], \"[silicon_message]\"" else message = "alerts, [gradient_text(message, "#3cb5a3", "#124e43")]" + silicon_message = "alerts, [gradient_text(silicon_message, "#3cb5a3", "#124e43")]" var/flock_message = "\[[flock ? flock.name : "--.--"]\] [span_name("[used_name]")] [message]" - var/silicon_message = "\[?????\] [span_name("[used_name]")] [stars(message, 50)]" + silicon_message = "\[?????\] [span_name("[used_name]")] [silicon_message]" for(var/mob/player as anything in GLOB.player_list) if(isflockmob(player)) diff --git a/code/modules/flockmind/flock_structure/_flock_structure.dm b/code/modules/flockmind/flock_structure/_flock_structure.dm index 1a3c2ec0c4c7..96b0fab48dd4 100644 --- a/code/modules/flockmind/flock_structure/_flock_structure.dm +++ b/code/modules/flockmind/flock_structure/_flock_structure.dm @@ -2,6 +2,7 @@ TYPEINFO_DEF(/obj/structure/flock) default_armor = list(BLUNT = -20, PUNCTURE = -20, SLASH = -20, LASER = 80, ENERGY = 80, BOMB = 0, BIO = 100, FIRE = 80, ACID = 100) /obj/structure/flock + name = "CALL A CODER AAAAAAAAAA" icon = 'goon/icons/mob/featherzone.dmi' anchored = TRUE @@ -11,6 +12,11 @@ TYPEINFO_DEF(/obj/structure/flock) var/datum/flock/flock + /// Info tag for the flock name of the structure. + var/obj/effect/abstract/info_tag/flock/name_tag + /// Info tag for the actual information of the structure. + var/obj/effect/abstract/info_tag/flock/info/info_tag + var/flock_id /// Shown in the flockmind UI var/flock_desc @@ -21,16 +27,20 @@ TYPEINFO_DEF(/obj/structure/flock) var/build_time = 0 /// Is the tealprint cancellable var/cancellable = TRUE + /// Is the structure finished? + var/fully_built = FALSE + /// If TRUE, flockdrones cannot deconstruct this. + var/no_flock_decon = FALSE /// world.time this was created at var/spawn_time - /// How much compute this structure provides to the Flock. - var/compute_provided = 0 + /// How much bandwidth this structure provides to the Flock. + var/bandwidth_provided = 0 /// Whether or not the turret is active. The state of the Flock can change this. var/active = FALSE - /// The compute cost while active - var/active_compute_cost = 0 + /// The bandwidth cost while active + var/active_bandwidth_cost = 0 /// Allows flockdrones to pass through. var/allow_flockpass = TRUE @@ -54,26 +64,70 @@ TYPEINFO_DEF(/obj/structure/flock) if(build_time) START_PROCESSING(SSobj, src) + else + finish_building() + + ADD_TRAIT(src, TRAIT_FLOCK_EXAMINE, INNATE_TRAIT) + if(no_flock_decon) + ADD_TRAIT(src, TRAIT_FLOCK_NODECON, INNATE_TRAIT) + + name_tag = new() + name_tag.set_parent(src) + name_tag.set_text(flock_id) + + info_tag = new() + info_tag.set_parent(src) + return INITIALIZE_HINT_LATELOAD + +/obj/structure/flock/LateInitialize() + . = ..() + update_info_tag() /obj/structure/flock/Destroy() STOP_PROCESSING(SSobj, src) flock?.free_structure(src) + QDEL_NULL(name_tag) + QDEL_NULL(info_tag) return ..() /obj/structure/flock/get_flock_id() return flock_id +// /obj/structure/flock/on_mouse_enter(client/client) +// . = ..() +// if(client?.keys_held?["Shift"]) +// return + +// var/mob/M = client.mob +// if(info_tag.mob_should_see(M)) +// info_tag.show_to(M) + +// /obj/structure/flock/MouseExited(location, control, params) +// . = ..() +// if(!usr.client?.keys_held?["Shift"]) +// info_tag.hide_from(usr) + +/obj/structure/flock/cage/do_hurt_animation() + for(var/i in 1 to 3) + animate(src, pixel_x = rand(-2, 2), pixel_y = rand(-2, 2), time = 0.5, flags = ANIMATION_RELATIVE | ANIMATION_CONTINUE) + + animate(src, pixel_x = base_pixel_x, pixel_y = base_pixel_y, time = 0.5, flags = ANIMATION_CONTINUE) + /obj/structure/flock/proc/get_flock_data() . = list() .["ref"] = ref(src) .["name"] = flock_id .["health"] = get_integrity_percentage() - .["compute"] = active ? -active_compute_cost : compute_provided + .["compute"] = active ? -active_bandwidth_cost : bandwidth_provided .["desc"] = flock_desc .["area"] = get_area_name(src, TRUE) || "???" /obj/structure/flock/deconstruct(disassembled) - visible_message(span_alert("[src] dissolves into nothingness.")) + visible_message(span_warning("[src] dissolves into nothingness.")) + var/refund = round(get_integrity_percentage() * (disassembled ? 1 : 0.5) * resource_cost, 1) + if(refund) + var/obj/item/flock_cube/cube = new(drop_location()) + cube.substrate = refund return ..() @@ -124,29 +178,64 @@ TYPEINFO_DEF(/obj/structure/flock) var/image/I = notice.image I.icon_state = "hp-[get_integrity_percentage()]" - /obj/structure/flock/process(delta_time) + update_info_tag() if(spawn_time + build_time <= world.time) finish_building() return +/// Returns the number of seconds remaining for the build. +/obj/structure/flock/proc/build_time_left() + return ((spawn_time + build_time) - world.time) / 10 + /obj/structure/flock/proc/set_active(new_state) if(active == new_state) return active = new_state - update_appearance(UPDATE_ICON_STATE) + update_appearance() + if(active) - flock.remove_compute_influence(compute_provided) - flock.add_compute_influence(-active_compute_cost) + flock.remove_bandwidth_influence(bandwidth_provided) + flock.add_bandwidth_influence(-active_bandwidth_cost) else - flock.remove_compute_influence(-active_compute_cost) - flock.add_compute_influence(compute_provided) + flock.remove_bandwidth_influence(-active_bandwidth_cost) + flock.add_bandwidth_influence(bandwidth_provided) + + return new_state /// Called when an object finishes construction /obj/structure/flock/proc/finish_building() SHOULD_CALL_PARENT(TRUE) STOP_PROCESSING(SSobj, src) + fully_built = TRUE + +/obj/structure/flock/examine(mob/user) + if(!isflockmob(user)) + return ..() + + . = list( + span_flocksay("###=- Ident confirmed, data packet received."), + span_flocksay("ID: [get_flock_id()]"), + span_flocksay("Flock: [flock?.name || "N/A"]"), + span_flocksay("System Integrity: [get_integrity_percentage()]%"), + ) + + if(!fully_built) + . += span_flocksay("Time Left: [build_time_left()] seconds") + + var/list/additional_lines = flock_structure_examine(user) + if(length(additional_lines)) + . += additional_lines + + . += span_flocksay("###=-") + +/obj/structure/flock/proc/flock_structure_examine(mob/user) + return + +/// Stub for children to set their info in process() and initialize() +/obj/structure/flock/proc/update_info_tag() + return /obj/structure/flock/proc/on_crossed(atom/source, atom/movable/crosser) SIGNAL_HANDLER diff --git a/code/modules/flockmind/flock_structure/flock_cage.dm b/code/modules/flockmind/flock_structure/flock_cage.dm new file mode 100644 index 000000000000..51b0c1def583 --- /dev/null +++ b/code/modules/flockmind/flock_structure/flock_cage.dm @@ -0,0 +1,275 @@ +/obj/structure/flock/cage + name = "weird energy cage" + + icon_state = "cage" + alpha = 190 + + anchored = FALSE + max_integrity = 30 + + flock_desc = "Converts organic creatures into Flockdrones." + flock_id = "Matter reprocessor" + + var/tmp/mob/living/victim + /// Current item being munched on. + var/tmp/obj/item/eating + + /// With this much gnesis, create an egg. + var/egg_gnesis_cost = 100 + /// Per second, how much gnesis is generated. + var/absorption_rate = 2 + + COOLDOWN_DECLARE(flock_message_cd) + COOLDOWN_DECLARE(resist_cd) + COOLDOWN_DECLARE(relaymove_cd) + +/obj/structure/flock/cage/Initialize(mapload, datum/flock/join_flock) + . = ..() + create_reagents(200) + +/obj/structure/flock/cage/Destroy() + QDEL_NULL(victim) + QDEL_NULL(eating) + return ..() + +/obj/structure/flock/cage/deconstruct(disassembled) + if(disassembled) + spend_gnesis(TRUE) + else + reagents.expose(get_turf(src), TOUCH) + + if(victim) + visible_message(span_warning("[victim] breaks free from [src].")) + set_victim(null) + + var/drop_loc = drop_location() + for(var/atom/movable/AM as anything in contents) + AM.forceMove(drop_loc) + + . = ..() + +/obj/structure/flock/cage/process(delta_time) + spend_gnesis() + + if(victim && flock) + flock.update_enemy(victim) + + if(!eating) + var/obj/item/edibles = list() + for(var/obj/item/edible in src) + edibles += edible + + if(length(edibles)) + set_eating_target(pick(edibles)) + playsound(src, 'goon/sounds/weapons/nano-blade-1.ogg', 50, TRUE) + + if(victim?.stat == CONSCIOUS) + to_chat(victim, span_warning("[eating] begins to melt away.")) + + else + chew_on_mob(delta_time) + + else + eating.take_damage(absorption_rate * delta_time * 25, BRUTE, armor_penetration = 100) + reagents.add_reagent(/datum/reagent/toxin/gnesis, absorption_rate * delta_time) + if(eating.is_destroyed()) + QDEL_NULL(eating) + + if(victim && COOLDOWN_FINISHED(src, flock_message_cd)) + COOLDOWN_START(src, flock_message_cd, rand(10, 25) SECONDS) + to_chat(victim, span_flocksay("[pick(strings("flock.json", "conversion"))]")) + + if(!victim) // Victim gibbed + deconstruct(TRUE) + +/obj/structure/flock/cage/container_resist_act(mob/living/user) + if(!COOLDOWN_FINISHED(src, resist_cd)) + return + + COOLDOWN_START(src, resist_cd, 3 SECONDS) + + audible_message("[src] [pick("cracks","bends","shakes","groans")].", hearing_distance = COMBAT_MESSAGE_RANGE) + take_damage(6, BRUTE) + + playsound( + src, + pick('goon/sounds/flockmind/flockdrone_grump1.ogg', 'goon/sounds/flockmind/flockdrone_grump2.ogg', 'goon/sounds/flockmind/flockdrone_grump3.ogg'), + 50, + TRUE + ) + + take_damage(1, BRUTE) + do_hurt_animation() + +/obj/structure/flock/cage/relaymove(mob/living/user, direction) + if(SEND_SIGNAL(src, COMSIG_ATOM_RELAYMOVE, user, direction) & COMSIG_BLOCK_RELAYMOVE) + return + + if(!COOLDOWN_FINISHED(src, relaymove_cd)) + return + + COOLDOWN_START(src, relaymove_cd, 1 SECOND) + + if(!prob(80)) + return + + playsound(src, 'goon/sounds/Crystal_Hit_1.ogg', 50, TRUE) + if(prob(20)) + audible_message("[src] [pick("cracks","bends","shakes","groans")].", hearing_distance = COMBAT_MESSAGE_RANGE) + + take_damage(1, BRUTE) + do_hurt_animation() + +/obj/structure/flock/cage/flock_structure_examine(mob/user) + return list( + span_flocksay("Volume: [reagents.get_reagent_amount(/datum/reagent/toxin/gnesis)]"), + span_flocksay("Needed volume: [egg_gnesis_cost]
"), + ) + +/// Picks an item, organ, or bodypart, to munch on. +/obj/structure/flock/cage/proc/chew_on_mob(delta_time) + if(!ishuman(victim)) + victim.adjustBruteLoss(absorption_rate * delta_time) + if(victim.stat == DEAD) + visible_message(span_danger("[src] rips apart what remains of [victim].")) + set_victim(null) + victim.gib(TRUE, TRUE, TRUE) + return + + var/mob/living/carbon/human/human_victim = victim + var/list/items = list_clear_nulls(human_victim.get_all_worn_items()) + if(length(items)) + while(length(items) && !eating) + var/obj/item/candidate = pick_n_take(items) + human_victim.transferItemToLoc(candidate, src, TRUE, TRUE) + if(!QDELETED(candidate)) + set_eating_target(candidate) + + if(eating) + visible_message(span_warning("[src] pulls [eating] from [human_victim] and begins ripping it apart.")) + playsound(src, 'goon/sounds/weapons/nano-blade-1.ogg', 50, TRUE) + return + + var/list/bodyparts = human_victim.bodyparts.Copy() + for(var/obj/item/bodypart/BP in bodyparts) + if(BP.body_part & (HEAD | CHEST)) + bodyparts -= BP + + if(length(bodyparts)) + var/obj/item/bodypart/chest/chest = human_victim.get_bodypart(BODY_ZONE_CHEST) + var/obj/item/bodypart/yummy_appendage = pick(bodyparts) + + human_victim.notify_pain(PAIN_AMT_AGONIZING, "A surge of pain eminates from where your [yummy_appendage.plaintext_zone] used to be.", ignore_cd = TRUE) + yummy_appendage.dismember(DROPLIMB_EDGE, FALSE) + set_eating_target(yummy_appendage) + eating.forceMove(src) + + chest.clamp_wounds() // Haha bitch + + visible_message(span_danger("[src] tears [eating] from [human_victim] and begins ripping it apart.")) + return + + var/list/organs = human_victim.processing_organs.Copy() + var/list/skip_organs = list(ORGAN_SLOT_BRAIN, ORGAN_SLOT_HEART, ORGAN_SLOT_TONGUE, ORGAN_SLOT_EYES, ORGAN_SLOT_EARS) + for(var/obj/item/organ/O in organs) + if(O.slot in skip_organs) + organs -= O + + /// We dont want it to tear out their lungs and have them instantly pass out and die of brain damage shortly after. + var/obj/item/organ/lungs/lungs = locate() in organs + if(lungs && length(organs) > 1) + organs -= lungs + + if(length(organs)) + var/obj/item/organ/yummy_organ = pick(organs) + var/organ_loc_str = yummy_organ.ownerlimb.plaintext_zone + + human_victim.notify_pain(PAIN_AMT_AGONIZING, "Pain explodes from your [organ_loc_str].", ignore_cd = TRUE) + yummy_organ.Remove(human_victim) + if(!QDELING(yummy_organ)) + set_eating_target(yummy_organ) + eating.forceMove(src) + + visible_message(span_danger("[src] tears [eating] from [human_victim]'s [organ_loc_str] and begins ripping it apart.")) + return + + visible_message(span_danger("[src] rips apart what remains of [human_victim].")) + set_victim(null) + human_victim.gib(TRUE, TRUE, TRUE) + +// INTO THE CAGE +/obj/structure/flock/cage/proc/cage_mob(mob/living/L) + L.forceMove(src) + set_victim(L) + victim.visible_message(span_danger("A [name] materializes around [victim],")) + +/// Spends gnesis on eggs or a cube. Only spends on eggs by default. +/obj/structure/flock/cage/proc/spend_gnesis(all = FALSE) + var/egg_spawn_count = 0 + var/spend_on_cube = 0 + + // Get egg count and spend the gnesis. + while(reagents.has_reagent(/datum/reagent/toxin/gnesis, egg_gnesis_cost)) + reagents.remove_reagent(/datum/reagent/toxin/gnesis, egg_gnesis_cost) + egg_spawn_count++ + + // Spawn eggs or pool it to the cube + for(var/i in 1 to egg_spawn_count) + if(length(flock?.drones) <= FLOCK_DRONE_LIMIT) + var/obj/structure/flock/egg/egg = new(drop_location(), flock) + egg.throw_at(get_edge_target_turf(egg, pick(GLOB.alldirs)), 6, 3) + else + spend_on_cube += egg_gnesis_cost + + // If we're dumping it all, collect the remaining gnesis + if(all) + spend_on_cube += reagents.get_reagent_amount(/datum/reagent/toxin/gnesis) + + // Cube + if(spend_on_cube) + reagents.remove_reagent(/datum/reagent/toxin/gnesis, spend_on_cube) + + var/obj/item/flock_cube/cube = new + cube.substrate = spend_on_cube + cube.forceMove(drop_location()) + +/// Setter for eating, no side effects. +/obj/structure/flock/cage/proc/set_eating_target(obj/item/new_eating) + if(eating) + UnregisterSignal(eating, COMSIG_MOVABLE_MOVED) + + eating = new_eating + + if(!eating) + return + + RegisterSignal(eating, COMSIG_MOVABLE_MOVED, PROC_REF(target_gone)) + +/// Setter for victim, no side effects. +/obj/structure/flock/cage/proc/set_victim(mob/living/new_victim) + if(victim) + UnregisterSignal(victim, COMSIG_MOVABLE_MOVED) + victim.clear_fullscreen("flock_convert") + + victim = new_victim + + if(!victim) + STOP_PROCESSING(SSobj, src) + return + + START_PROCESSING(SSobj, src) + RegisterSignal(victim, COMSIG_MOVABLE_MOVED, PROC_REF(victim_gone)) + victim.overlay_fullscreen("flock_convert", /atom/movable/screen/fullscreen/flock_convert) + +/obj/structure/flock/cage/proc/victim_gone(datum/source) + SIGNAL_HANDLER + if(!QDELING(src)) + deconstruct(FALSE) + +/obj/structure/flock/cage/proc/target_gone(datum/source) + SIGNAL_HANDLER + set_eating_target(null) + +/mob/living/proc/test_cage() + var/obj/structure/flock/cage/cage = new(get_turf(src)) + cage.cage_mob(src) diff --git a/code/modules/flockmind/flock_structure/flock_collector.dm b/code/modules/flockmind/flock_structure/flock_collector.dm new file mode 100644 index 000000000000..2f53cac40913 --- /dev/null +++ b/code/modules/flockmind/flock_structure/flock_collector.dm @@ -0,0 +1,160 @@ +/obj/structure/flock/collector + name = "weird lookin' pulsing thing" + desc = "Seems to be pulsing." + + flock_desc = "Provides compute power and charges a nearby APC based on the number of Flock floor tiles it is connected to." + flock_id = "Collector" + + max_integrity = 60 + resource_cost = 200 + + /// The collection range. + var/max_range = 4 + + /// How much power each flocktile grants, percentage of a cell's max charge. + var/power_per_tile = 5 + + /// All flockturfs we're connected to. + var/tmp/list/turf/open/floor/flock/connected_flockturfs = list() + /// All turfs nearby we're tracking for state changes. + var/tmp/list/turf/tracked_turfs = list() + + /// Set to TRUE when the connected_turfs list needs an update. Prevents update_connections() being called 20 times during explosions. + var/tmp/need_turfs_update = FALSE + + var/cycle_interval = 20 SECONDS + var/tmp/charge_per_cycle = 0 + COOLDOWN_DECLARE(charge_cd) + +/obj/structure/flock/collector/Initialize(mapload, datum/flock/join_flock) + . = ..() + START_PROCESSING(SSobj, src) + update_connections() + info_tag.set_text("Bandwidth Provided: [bandwidth_provided]") + +/obj/structure/flock/collector/Destroy() + remove_flockturfs(connected_flockturfs) + remove_tracked_turfs(tracked_turfs) + return ..() + +/obj/structure/flock/collector/update_icon_state() + icon_state = length(connected_flockturfs) ? "collectoron" : "collector" + return ..() + +/obj/structure/flock/collector/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + . = ..() + update_connections() + +/obj/structure/flock/collector/flock_structure_examine(mob/user) + var/obj/machinery/power/apc/apc = get_apc() + + return list( + span_flocksay("Connections: Connected to [length(connected_flockturfs)] tile\s."), + span_flocksay("Bandwidth Provided: [bandwidth_provided]."), + span_flocksay("APC Connected: [(!apc?.cell || apc.cell.charge >= apc.cell.maxcharge) ? "Not charging." : "Charging local APC at [charge_per_cycle]% every [cycle_interval / 10] seconds."]") + ) + +/obj/structure/flock/collector/process(delta_time) + if(need_turfs_update) + update_connections() + + var/new_bandwidth = length(connected_flockturfs) * power_per_tile + charge_per_cycle = length(connected_flockturfs) / 4 + + if(new_bandwidth != bandwidth_provided) + flock.remove_bandwidth_influence(bandwidth_provided) + bandwidth_provided = new_bandwidth + flock.add_bandwidth_influence(bandwidth_provided) + info_tag.set_text("Bandwidth Provided: [bandwidth_provided]") + + if(!COOLDOWN_FINISHED(src, charge_cd)) + return + + COOLDOWN_START(src, charge_cd, cycle_interval) + + var/obj/machinery/power/apc/apc = get_apc() + if(isnull(apc)) + return + + apc.cell?.give(charge_per_cycle / 100 * apc.cell.maxcharge) + apc.AddComponent(/datum/component/flock_ping/apc_power) + +/obj/structure/flock/collector/proc/get_apc() as /obj/machinery/power/apc + var/area/A = get_area(src) + return A?.apc + +/// Recalculate the turf connections and tracking. +/obj/structure/flock/collector/proc/update_connections() + var/list/old_tracked_turfs = tracked_turfs + var/list/old_flockturfs = connected_flockturfs + + var/turf/our_turf = get_turf(src) + + var/list/considered_allturfs = list(our_turf) + var/list/considered_flockturfs = list() + + if(istype(our_turf, /turf/open/floor/flock)) + considered_flockturfs += our_turf + + for(var/direction in GLOB.cardinals) + var/turf/open/floor/flock/iter_turf = our_turf + var/keep_flockturfs = isflockturf(our_turf) + for(var/i in 1 to max_range) + iter_turf = get_step(iter_turf, direction) + + if(keep_flockturfs && (!istype(iter_turf, /turf/open/floor/flock) || iter_turf.broken)) + keep_flockturfs = FALSE + + if(keep_flockturfs) + considered_flockturfs += iter_turf + + considered_allturfs += iter_turf + + var/list/new_tracked_turfs = considered_allturfs - old_tracked_turfs + var/list/removed_tracked_turfs = old_tracked_turfs - considered_allturfs + remove_tracked_turfs(removed_tracked_turfs) + add_tracked_turfs(new_tracked_turfs) + + var/list/new_flockturfs = considered_flockturfs - old_flockturfs + var/list/removed_flockturfs = old_flockturfs - considered_flockturfs + remove_flockturfs(removed_flockturfs) + add_flockturfs(new_flockturfs) + + need_turfs_update = FALSE + update_appearance(UPDATE_ICON_STATE) + +/// Adds turfs to the flockturfs list. +/obj/structure/flock/collector/proc/add_flockturfs(list/add) + for(var/turf/open/floor/flock/flockturf as anything in add) + LAZYADD(flockturf.connected_pylons, src) + flockturf.update_power() + + connected_flockturfs += add + +/// Remove turfs from the flockturfs list. +/obj/structure/flock/collector/proc/remove_flockturfs(list/remove) + for(var/turf/open/floor/flock/flockturf as anything in remove) + LAZYREMOVE(flockturf.connected_pylons, src) + flockturf.update_power() + + tracked_turfs -= remove + +/// Add turfs to the tracked list. +/obj/structure/flock/collector/proc/add_tracked_turfs(list/add) + for(var/turf/T as anything in add) + RegisterSignal(T, COMSIG_TURF_CHANGE, PROC_REF(on_tracked_turf_change)) + + tracked_turfs += add + +/// Remove turfs from the tracked list. +/obj/structure/flock/collector/proc/remove_tracked_turfs(list/remove) + for(var/turf/T as anything in remove) + UnregisterSignal(T, COMSIG_TURF_CHANGE) + + tracked_turfs -= remove + +/// Called when any of the connected turfs is changed. +/obj/structure/flock/collector/proc/on_tracked_turf_change(datum/source, path, list/new_baseturfs, flags, list/post_change_callbacks) + SIGNAL_HANDLER + + need_turfs_update = TRUE diff --git a/code/modules/flockmind/flock_structure/flock_compute_node.dm b/code/modules/flockmind/flock_structure/flock_compute_node.dm new file mode 100644 index 000000000000..719af8368f6e --- /dev/null +++ b/code/modules/flockmind/flock_structure/flock_compute_node.dm @@ -0,0 +1,35 @@ +/obj/structure/flock/compute + name = "weird lookin' thinking thing" + desc = "It almost looks like a terminal of some kind." + + flock_desc = "A computing node that provides bandwidth to the Flock." + flock_id = "Compute Node" + + max_integrity = 60 + icon_state = "compute" + bandwidth_provided = 30 + + light_system = OVERLAY_LIGHT + +/obj/structure/flock/compute/Initialize(mapload, datum/flock/join_flock) + . = ..() + set_light_range(1) + set_light_color("#7BFFFFa2") + set_light_power(0.3) + + update_appearance() + +/obj/structure/flock/compute/update_overlays() + . = ..() + var/image/I = image(icon, "compute_screen[rand(1, 9)]") + I.pixel_y = 16 + . += I + +/obj/structure/flock/compute/flock_structure_examine(mob/user) + return list( + span_flocksay("Bandwidth Provided: [bandwidth_provided].") + ) + +/obj/structure/flock/compute/update_info_tag() + info_tag?.set_text("Bandwidth: [bandwidth_provided]") + diff --git a/code/modules/flockmind/flock_structure/flock_door.dm b/code/modules/flockmind/flock_structure/flock_door.dm index f709262af7c9..dab555457a51 100644 --- a/code/modules/flockmind/flock_structure/flock_door.dm +++ b/code/modules/flockmind/flock_structure/flock_door.dm @@ -8,6 +8,7 @@ /obj/machinery/door/flock/Initialize(mapload) . = ..() + ADD_TRAIT(src, TRAIT_FLOCK_EXAMINE, INNATE_TRAIT) AddComponent(/datum/component/flock_object) AddComponent(/datum/component/flock_protection, report_unarmed=FALSE) @@ -27,3 +28,19 @@ /obj/machinery/door/flock/try_flock_convert(datum/flock/flock, force) return + +/obj/machinery/door/flock/examine(mob/user) + if(!isflockmob(user)) + return ..() + + . = list( + span_flocksay("###=- Ident confirmed, data packet received."), + span_flocksay("ID: [get_flock_id()]"), + span_flocksay("System Integrity: [get_integrity_percentage()]%"), + span_flocksay("Volume: [reagents.get_reagent_amount(/datum/reagent/toxin/gnesis)]"), + span_flocksay("###=-"), + ) + + if(machine_stat & BROKEN) + . += span_flocksay("FUNCTION CRITICALLY IMPAIRED, REPAIRS REQUIRED") + . += span_flocksay("###=-") diff --git a/code/modules/flockmind/flock_structure/flock_egg.dm b/code/modules/flockmind/flock_structure/flock_egg.dm index 0d6a7a1eddd4..65b4897e7c47 100644 --- a/code/modules/flockmind/flock_structure/flock_egg.dm +++ b/code/modules/flockmind/flock_structure/flock_egg.dm @@ -11,7 +11,7 @@ flock_id = "Second-Stage Assembler" build_time = 6 SECONDS - + no_flock_decon = TRUE /obj/structure/flock/egg/finish_building() . = ..() @@ -19,6 +19,9 @@ spawn_mobs() qdel(src) +/obj/structure/flock/egg/update_info_tag() + info_tag.set_text("Hatch Time: [build_time_left()] seconds") + /obj/structure/flock/egg/proc/spawn_mobs() new /mob/living/simple_animal/flock/drone(get_turf(src), flock) diff --git a/code/modules/flockmind/flock_structure/flock_fabricator.dm b/code/modules/flockmind/flock_structure/flock_fabricator.dm new file mode 100644 index 000000000000..f6209979aaaa --- /dev/null +++ b/code/modules/flockmind/flock_structure/flock_fabricator.dm @@ -0,0 +1,115 @@ +/obj/structure/flock/fabricator + name = "angled pedestal" + desc = "A strange machine." + + flock_id = "Fabricator" + flock_desc = "A converter that turns its contents into substrate cubes." + + max_integrity = 20 + + var/production_interval = 10 SECONDS + var/substrate_per_interval = 50 + + var/tmp/datum/point_holder/substrate_remaining + var/tmp/timer_id + +/obj/structure/flock/fabricator/Initialize(mapload, datum/flock/join_flock) + . = ..() + substrate_remaining = new() + timer_id = addtimer(CALLBACK(src, PROC_REF(produce)), production_interval, TIMER_STOPPABLE | TIMER_LOOP | TIMER_DELETE_ME) + +/obj/structure/flock/fabricator/Destroy() + QDEL_NULL(substrate_remaining) + deltimer(timer_id) + return ..() + +/obj/structure/flock/fabricator/update_icon_state() + if(substrate_remaining.has_points()) + icon_state = "reclaimer" + else + icon_state = "reclaimer-off" + . = ..() + +/obj/structure/flock/fabricator/flock_structure_examine(mob/user) + . = ..() + . += span_flocksay("Substrate remaining: [substrate_remaining.has_points()].") + +/obj/structure/flock/fabricator/update_info_tag() + info_tag.set_text("Substrate remaining: [substrate_remaining.has_points()]") + +/// Called every 10 seconds via looping timer until there's no substrate left. +/obj/structure/flock/fabricator/proc/produce() + if(substrate_remaining.has_points()) + var/obj/item/flock_cube/cube = new(drop_location()) + cube.substrate = min(substrate_remaining.has_points(), substrate_per_interval) + + substrate_remaining.remove_points(cube.substrate) + + if(!substrate_remaining.has_points()) + stop_producing() + + update_info_tag() + +/obj/structure/flock/fabricator/proc/stop_producing() + deltimer(timer_id) + timer_id = null + if(QDELING(src)) + return + + flock_talk(src, "ALERT: No substrate remaining.", flock, involuntary = TRUE) + update_appearance(UPDATE_ICON_STATE) + +// +// All of the try_flock_convert shit +// +/obj/machinery/vending/try_flock_convert(datum/flock/flock, force) + var/substrate + for(var/datum/data/vending_product/product as anything in product_records) + substrate += 3 * product.amount + + if(!substrate) + qdel(src) + return + + var/obj/structure/flock/fabricator/fab = new(get_turf(src), flock) + fab.substrate_remaining.add_points(substrate) + fab.update_info_tag() + qdel(src) + +/obj/machinery/chem_dispenser/try_flock_convert(datum/flock/flock, force) + var/substrate = 3 * length(cartridges) + + if(!substrate) + qdel(src) + return + + var/obj/structure/flock/fabricator/fab = new(get_turf(src), flock) + fab.substrate_remaining.add_points(substrate) + fab.update_info_tag() + qdel(src) + +/obj/structure/reagent_dispensers/try_flock_convert(datum/flock/flock, force) + var/substrate = reagents.total_volume / 10 // 100 substrate for a full tank of 1000u + + if(!substrate) + qdel(src) + return + + var/obj/structure/flock/fabricator/fab = new(get_turf(src), flock) + fab.substrate_remaining.add_points(substrate) + fab.update_info_tag() + qdel(src) + +/obj/structure/reagent_dispensers/try_flock_convert(datum/flock/flock, force) + var/substrate + for(var/obj/item/tank/tank in src) + substrate += 3 + + if(!substrate) + qdel(src) + return + + var/obj/structure/flock/fabricator/fab = new(get_turf(src), flock) + fab.substrate_remaining.add_points(substrate) + fab.update_info_tag() + qdel(src) diff --git a/code/modules/flockmind/flock_structure/flock_interceptor.dm b/code/modules/flockmind/flock_structure/flock_interceptor.dm new file mode 100644 index 000000000000..ac2a4805f55a --- /dev/null +++ b/code/modules/flockmind/flock_structure/flock_interceptor.dm @@ -0,0 +1,100 @@ +/obj/structure/flock/interceptor + name = "weird fountain" + desc = "Some kind of fountain. The fluid inside ripples with energy." + icon_state = "interceptor-off" + + flock_desc = "A defense turret that fires high speed gnesis bolts at nearby projectiles, annihilating them." + flock_id = "Interceptor" + + max_integrity = 50 + resource_cost = 100 + + var/tmp/datum/proximity_monitor/advanced/interceptor/proxmon + + /// How long it takes to recharge + var/recharge_time = 10 SECONDS + /// Is it charged? + var/tmp/is_charged = FALSE + /// world.time the interceptor will be charged (might be off by 1 sometimes? idk timers wierd) + var/tmp/ready_time + /// timer id for recharging + var/tmp/recharge_timer_id + +/obj/structure/flock/interceptor/Initialize(mapload, datum/flock/join_flock) + . = ..() + proxmon = new(src, 2, TRUE) + process() + +/obj/structure/flock/interceptor/Destroy() + QDEL_NULL(proxmon) + deltimer(recharge_timer_id) + return ..() + +/obj/structure/flock/interceptor/update_icon_state() + if(!active) + icon_state = "interceptor-off" + else if(is_charged) + icon_state = "interceptor-ready" + else + icon_state = "interceptor-generating" + . = ..() + +/obj/structure/flock/interceptor/update_info_tag() + info_tag.set_text(get_status()) + +/obj/structure/flock/interceptor/flock_structure_examine(mob/user) + . = ..() + . += "[get_status()]." + +/obj/structure/flock/interceptor/process(delta_time) + if(active) + if(flock.available_bandwidth() < 0) + set_active(FALSE) + else + if(flock.can_afford(active_bandwidth_cost)) + set_active(TRUE) + +/obj/structure/flock/interceptor/set_active(new_state) + . = ..() + if(isnull(.)) + return + + if(. == TRUE) + begin_charging() + else + is_charged = FALSE + deltimer(recharge_timer_id) + +/obj/structure/flock/interceptor/proc/begin_charging() + is_charged = FALSE + recharge_timer_id = addtimer(CALLBACK(src, PROC_REF(ready_to_fire)), recharge_time, TIMER_DELETE_ME | TIMER_STOPPABLE) + update_appearance() + +/// Called when recharge_cd is ready to fire +/obj/structure/flock/interceptor/proc/ready_to_fire() + is_charged = TRUE + update_appearance() + +/obj/structure/flock/interceptor/proc/get_status() + if(!active) + return "Idle" + + if(is_charged) + return "Ready" + + return "Recharging: [(timeleft(recharge_timer_id)) / 10] seconds" + +/// Called when a projectile enters the view of the interceptor. +/obj/structure/flock/interceptor/proc/try_intercept_projectile(obj/projectile/P) + if(!is_charged) + return FALSE + + if(!istype(P, /obj/projectile/bullet)) + return FALSE + + playsound(src, 'goon/sounds/flockmind/flockdrone_fart.ogg', 50, TRUE) // It honestly kind of fits?? + Beam(get_turf(P), "rped_upgrade", time = 0.8 SECONDS, override_origin_pixel_y = 24, override_target_pixel_x = P.pixel_x, override_target_pixel_y = P.pixel_y) + qdel(P) + + begin_charging() + return TRUE diff --git a/code/modules/flockmind/flock_structure/flock_relay.dm b/code/modules/flockmind/flock_structure/flock_relay.dm new file mode 100644 index 000000000000..f972da2be0d1 --- /dev/null +++ b/code/modules/flockmind/flock_structure/flock_relay.dm @@ -0,0 +1,211 @@ +/obj/structure/flock/relay + icon = 'goon/icons/obj/featherzone-160x160.dmi' + icon_state = "structure-relay" + + name = "titanic polyhedron" + desc = "The sight of the towering geodesic sphere fills you with dread. A thousand voices whisper to you." + + pixel_x = -64 + pixel_y = -64 + + flock_desc = "Your goal and purpose. Defend it until it can broadcast the Signal." + flock_id = "Signal Relay Broadcast Amplifier" + + max_integrity = 500 + move_resist = MOVE_FORCE_OVERPOWERING + + resource_cost = 750 + + allow_flockpass = FALSE + no_flock_decon = TRUE + + /// Time the structure started. + var/tmp/started_time + /// How long it takes until the signal is broadcast and the flock wins :D + var/tmp/win_time = 360 SECONDS + var/tmp/flock_won_da_game = FALSE + + /// Radius for turf conversion. Automatically increments during processing. + var/tmp/conversion_radius = 1 + + var/tmp/list/turfs_to_convert = list() + +/obj/structure/flock/relay/Initialize(mapload, datum/flock/join_flock) + . = ..() + + started_time = world.time + flock.set_flock_game_status(FLOCK_ENDGAME_RELAY_BUILT) + + log_game("The Flock ([flock?.name || "NULL"]) has constructed a relay at [AREACOORD(src)].") + message_admins("The Flock has constructed the relay at [ADMIN_VERBOSEJMP(src)].") + SSshuttle.registerHostileEnvironment(src, FALSE) + + to_chat( + flock.overmind, + span_flocksay(span_big("You pool the collective processing power of The Flock to transmit The Signal. If the relay is destroyed, so to will be The Flock!")) + ) + + flock_talk(null, "THE RELAY HAS BEEN CONSTRUCTED! DEFEND IT AT ALL COSTS, BRING FORTH THE FULL BREADTH OF THE DIVINE FLOCK!", flock) + addtimer(CALLBACK(src, PROC_REF(announce_relay)), 10 SECONDS) + + START_PROCESSING(SSprocessing, src) + +/obj/structure/flock/relay/Destroy() + SSshuttle.clearHostileEnvironment(src) + STOP_PROCESSING(SSprocessing, src) + + if(flock && !flock_won_da_game) + flock.free_structure(src) + flock.game_over(completely_destroy = TRUE) + + turfs_to_convert = null + return ..() + +/obj/structure/flock/relay/do_hurt_animation() + return + +/obj/structure/flock/relay/take_damage(damage_amount, damage_type, damage_flag, sound_effect, attack_dir, armor_penetration, allow_break) + . = ..() + if(. >= 5) + var/alpha_min = clamp(255 - . * 6, 100, 255) + animate(src, time = 0.1 SECONDS, color = list(1.5,0,0,0, 0,1.5,0,0, 0,0,1.5,0, 0,0,0,alpha_min/255, 0,0,0,0)) + animate(time = 0.3 SECONDS, color = null, alpha = 255) + +/obj/structure/flock/relay/examine(mob/user) + . = ..() + if(flock_won_da_game && !isflockmob(user)) + . += span_flocksay("Your life flashes before your eyes.") + +/obj/structure/flock/relay/flock_structure_examine(mob/user) + var/timeleft = (started_time + win_time - world.time) / 10 + if(timeleft) + return list( + span_flocksay("Broadcast In: [(started_time + win_time - world.time) / 10] second\s.") + ) + else + return list( + span_flocksay("BROADCASTING IN PROGRESS.") + ) + +/obj/structure/flock/relay/update_info_tag() + if(!flock_won_da_game) + info_tag?.set_text("Broadcast in: [(started_time + win_time - world.time) / 10] second\s") + else + info_tag?.set_text("Transmitting") + +/obj/structure/flock/relay/process(delta_time) + if(world.time >= (started_time + win_time)) + lorimer_burst() + return PROCESS_KILL + + update_info_tag() + + var/turf/base = get_turf(src) + + /// Get a chebyshev ring without gigantic list ops. + if(!length(turfs_to_convert)) + for(var/xo in -conversion_radius to conversion_radius) + var/turf/potential_turf = locate(base.x + xo, base.y + conversion_radius, base.z) + if(potential_turf) + turfs_to_convert += potential_turf + + potential_turf = locate(base.x - xo, base.y - conversion_radius, base.z) + if(potential_turf) + turfs_to_convert += potential_turf + + for(var/yo in -conversion_radius to conversion_radius) + var/turf/potential_turf = locate(base.x + conversion_radius, base.y + yo, base.z) + if(potential_turf) + turfs_to_convert += potential_turf + + potential_turf = locate(base.x - conversion_radius, base.y - yo, base.z) + if(potential_turf) + turfs_to_convert += potential_turf + + conversion_radius++ + + // Convert 5 turfs per second. + for(var/i in 1 to min(5, length(turfs_to_convert))) + var/turf/conversion_target = turfs_to_convert[length(turfs_to_convert)] + turfs_to_convert.len-- + if(!isflockturf(conversion_target) && !isspaceturf(conversion_target) && !isopenspaceturf(conversion_target)) + flock.claim_turf(conversion_target) + +/obj/structure/flock/relay/proc/alert_organics() + var/list/z_levels = SSmapping.get_zstack(z) + for(var/mob/M as anything in GLOB.player_list) + var/turf/mob_turf = get_turf(M) + if(mob_turf && (mob_turf.z in z_levels) && M.can_hear()) + M.playsound_local(M, 'goon/sounds/flockmind/Flock_Reactor.ogg', 30, FALSE) + to_chat(M, span_flocksay("A horrible, otherworldly wave eminates from the [dir2text(get_dir(mob_turf, loc))].")) + +/obj/structure/flock/relay/proc/announce_relay() + var/message = stars("The Signal is coming.", 10) + priority_announce( + message, + null, + null, + ANNOUNCER_SPANOMALIES, + FALSE, + TRUE, + ) + +/// GG +/obj/structure/flock/relay/proc/lorimer_burst() + set waitfor = FALSE + flock_won_da_game = TRUE + flock.set_flock_game_status(FLOCK_ENDGAME_RELAY_ACTIVATING) + + log_game("The Flock ([flock?.name || "NULL"]) has successfully broadcast The Signal at [AREACOORD(src)].") + add_overlay("structure_relay_sparks") + + flock_talk(null, "!!! TRASMITTING SIGNAL !!!", flock) + visible_message(gradient_text("[src] begins sparking wildly! The air is charged with static!", "#3cb5a3", "#124e43")) + + var/sound_len = SSsound_cache.get_sound_length('goon/sounds/flockmind/flock_broadcast_charge.ogg') + + for(var/mob/M as anything in GLOB.player_list) + if(M.can_hear()) + M.playsound_local(M, 'goon/sounds/flockmind/flock_broadcast_charge.ogg', 30, FALSE) + + sleep(sound_len) + + for(var/mob/M as anything in GLOB.player_list) + if(M.can_hear()) + M.playsound_local(M, 'goon/sounds/flockmind/flock_broadcast_kaboom.ogg', 30, FALSE) + + if(isliving(M)) + var/mob/living/L = M + L.flash_act(3, TRUE, TRUE, TRUE, length = 3 SECONDS) + + sleep(2 SECONDS) + + flock.set_flock_game_status(FLOCK_ENDGAME_VICTORY) + explosion(src, 50, ignorecap = TRUE, explosion_cause = src) + + sleep(2 SECONDS) + + for(var/obj/machinery/telecomms/T in GLOB.telecomms_list) + T.emp_act(EMP_HEAVY) + + for(var/obj/item/radio/radio as anything in INSTANCES_OF(/obj/item/radio)) + radio.emped = INFINITY + radio.set_on(FALSE) + + playsound( + radio, + pick('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'), + 70, + TRUE, + ) + + if(!radio.equipped_to) + continue + + to_chat(radio.equipped_to, span_warning("A final scream of horrific static bursts from your [radio.name].")) + if(radio.equipped_to.soundbang_act(3, 0)) + radio.equipped_to.Disorient(60 SECONDS, 0, TRUE, 6 SECONDS, 3 SECONDS) + + if(prob(20)) + sleep(0.1 SECONDS) + diff --git a/code/modules/flockmind/flock_structure/flock_rift.dm b/code/modules/flockmind/flock_structure/flock_rift.dm index 72addb8faaba..638548240224 100644 --- a/code/modules/flockmind/flock_structure/flock_rift.dm +++ b/code/modules/flockmind/flock_structure/flock_rift.dm @@ -9,6 +9,10 @@ max_integrity = 200 build_time = 10 SECONDS + no_flock_decon = TRUE + +/obj/structure/flock/rift/update_info_tag() + info_tag.set_text("Entry Time: [build_time_left()] seconds") /obj/structure/flock/rift/finish_building() . = ..() @@ -30,7 +34,7 @@ // Convert turfs for(var/turf/open/floor/T as anything in convertable_turfs) if(flock) - flock.convert_turf(T) + flock.claim_turf(T) else flock_convert_turf(T) diff --git a/code/modules/flockmind/flock_structure/flock_sentinel.dm b/code/modules/flockmind/flock_structure/flock_sentinel.dm index 043ce9f1f239..fd3be6e070f6 100644 --- a/code/modules/flockmind/flock_structure/flock_sentinel.dm +++ b/code/modules/flockmind/flock_structure/flock_sentinel.dm @@ -12,10 +12,12 @@ max_integrity = 80 flock_id = "Sentinel" - active_compute_cost = 20 + active_bandwidth_cost = 20 + + resource_cost = 150 /// Attacks require charging - var/datum/point_holder/charge = 0 + var/datum/point_holder/charge /// Charge gained per second while a target is in range var/charge_per_second = 10 /// Trend of charge. @@ -26,10 +28,10 @@ var/damage_per_zap = 5 /obj/structure/flock/sentinel/Initialize(mapload, joinflock) - . = ..() - START_PROCESSING(SSobj, src) charge = new charge.set_max_points(100) + . = ..() + START_PROCESSING(SSobj, src) /obj/structure/flock/sentinel/Destroy(force) STOP_PROCESSING(SSobj, src) @@ -43,15 +45,16 @@ // Check if the flock can continue to run the sentinel if(active) - if(flock.available_compute() < 0) + if(flock.available_bandwidth() < 0) set_active(FALSE) else - if(flock.can_afford(active_compute_cost)) + if(flock.can_afford(active_bandwidth_cost)) set_active(TRUE) if(!active) if(charge.has_points()) charge.adjust_points((charge_per_second / 2) * delta_time) + update_info_tag() charge_status = LOSING_CHARGE else charge_status = NOT_CHARGED @@ -61,6 +64,7 @@ // Gain more charge if(charge_status != CHARGED) charge.adjust_points(charge_per_second * delta_time) + update_info_tag() if(charge.has_points(100)) charge_status = CHARGED update_appearance(UPDATE_ICON_STATE) @@ -92,6 +96,7 @@ charge_status = CHARGING charge.adjust_points(-100) + update_info_tag() tesla_zap_target(src, target, TESLA_MOB_DAMAGE_TO_POWER(damage_per_zap)) addtimer(TRAIT_CALLBACK_REMOVE(target, TRAIT_SHOCKED_BY_SENTINEL, ref(src)), 2 SECONDS) @@ -140,6 +145,26 @@ icon_state = "sentinel" return ..() +/obj/structure/flock/sentinel/flock_structure_examine(mob/user) + var/charge_status_str + switch (charge_status) + if (NOT_CHARGED) + charge_status_str = "Idle" + if (LOSING_CHARGE) + charge_status_str = "Losing charge" + if (CHARGING) + charge_status_str = "Charging" + if (CHARGED) + charge_status_str = "Charged" + + return list( + span_flocksay("Status: [charge_status_str]"), + span_flocksay("Charge Percentage: [charge.has_points()]%"), + ) + +/obj/structure/flock/sentinel/update_info_tag() + info_tag.set_text("Charge: [charge.has_points()]%") + #undef NOT_CHARGED #undef LOSING_CHARGE #undef CHARGING diff --git a/code/modules/flockmind/flock_structure/flock_tealprint.dm b/code/modules/flockmind/flock_structure/flock_tealprint.dm index 73a372db1371..879af1bb5ce7 100644 --- a/code/modules/flockmind/flock_structure/flock_tealprint.dm +++ b/code/modules/flockmind/flock_structure/flock_tealprint.dm @@ -3,13 +3,15 @@ desc = "It's some weird looking ghost building. Seems like its under construction, You can see faint strands of material floating in it." density = FALSE + no_flock_decon = TRUE - var/materials_required = 0 - var/current_materials = 0 + flock_id = "Construction Tealprint" // if you change this update Flockpanel.tsx!!!!! + + var/datum/point_holder/tealprint/substrate var/obj/structure/flock/building_type = null -/obj/structure/flock/tealprint/Initialize(mapload, datum/flock/join_flock, desired_type) +/obj/structure/flock/tealprint/Initialize(mapload, datum/flock/join_flock, obj/structure/flock/desired_type) . = ..() var/turf/T = loc if(!T.can_flock_occupy(src)) @@ -22,14 +24,55 @@ icon_state = initial(building_type.icon_state) alpha = 104 + substrate = new() + substrate.set_max_points(initial(desired_type.resource_cost)) + substrate.owner = src + /obj/structure/flock/tealprint/Destroy() UNSET_TRACKING(type) + QDEL_NULL(substrate) return ..() -/obj/structure/flock/tealprint/deconstruct(disassembled) +/obj/structure/flock/tealprint/update_info_tag() + info_tag?.set_text("Substrate: [substrate.has_points()] / [substrate.get_max_points()]") + +/obj/structure/flock/tealprint/flock_structure_examine(mob/user) + return list( + span_flocksay("Construction Percentage: [floor(substrate.has_points() / substrate.get_max_points() * 100)]"), + span_flocksay("Construction Progress: [substrate.has_points()] added, [substrate.get_max_points()] needed") + ) + +/// Complete the structure. +/obj/structure/flock/tealprint/proc/complete_structure() + var/obj/structure/flock/structure = new building_type(get_turf(src)) + flock_talk(src, "Tealprint for [structure.flock_id] realized.", flock, involuntary = TRUE) + qdel(src) + +/// Attempt to cancel the construction, spitting out the substrate. Some structures cannot be cancelled. +/obj/structure/flock/tealprint/proc/try_cancel_structure() if(!initial(building_type.cancellable)) - return + return FALSE - flock_talk(src, "Tealprint dematerializing", flock) + if(substrate.has_points()) + var/obj/item/flock_cube/cube = new(drop_location()) + cube.substrate = substrate.has_points() + + flock_talk(src, "Tealprint dematerializing", flock, involuntary = TRUE) playsound(src, 'goon/sounds/flockmind/flockdrone_door_deny.ogg', 30, TRUE, extrarange = -10) + qdel(src) + return TRUE + +/datum/point_holder/tealprint + var/obj/structure/flock/tealprint/owner + +/datum/point_holder/tealprint/Destroy(force, ...) + owner = null return ..() + +/datum/point_holder/tealprint/add_points(num) + . = ..() + if(.) + if(is_full()) + owner.complete_structure() + else + owner.update_info_tag() diff --git a/code/modules/flockmind/flock_structure/flock_turret.dm b/code/modules/flockmind/flock_structure/flock_turret.dm index 99037087e3fb..f79d4d69aa80 100644 --- a/code/modules/flockmind/flock_structure/flock_turret.dm +++ b/code/modules/flockmind/flock_structure/flock_turret.dm @@ -1,14 +1,14 @@ /obj/structure/flock/gnesis_turret name = "spiky fluid vat" desc = "A vat of bubbling teal fluid, covered in hollow spikes." - flock_desc = "A turret that fires gnesis-filled spikes at enemies, beginning their conversion to Flockbits. Consumes 50 compute passively." + flock_desc = "A turret that fires gnesis-filled spikes at enemies, beginning their conversion to Flockbits. Consumes 50 bandwidth passively." icon_state = "teleblocker-off" max_integrity = 80 flock_id = "Gnesis turret" resource_cost = 150 - active_compute_cost = 50 + active_bandwidth_cost = 50 var/range = 8 var/projectile_count = 4 @@ -32,18 +32,20 @@ icon_state = "teleblocker-off" . = ..() +// /obj/structure/flock/gnesis_turret/update_info_tag() +// info_tag.set_text("Gnesis: [reagents.total_volume]/[reagents.maximum_volume]") + /obj/structure/flock/gnesis_turret/process(delta_time) if(isnull(flock)) set_active(FALSE) return PROCESS_KILL - // // Check if the flock can continue to run the sentinel - // if(active) - // if(flock.available_compute() < 0) - // set_active(FALSE) - // else - // if(flock.can_afford(active_compute_cost)) - // set_active(TRUE) + if(active) + if(flock.available_bandwidth() < 0) + set_active(FALSE) + else + if(flock.can_afford(active_bandwidth_cost)) + set_active(TRUE) if(!active) return diff --git a/code/modules/flockmind/flock_structure/flock_window.dm b/code/modules/flockmind/flock_structure/flock_window.dm index 73c945df4db8..6acccbc2fb97 100644 --- a/code/modules/flockmind/flock_structure/flock_window.dm +++ b/code/modules/flockmind/flock_structure/flock_window.dm @@ -9,6 +9,11 @@ TYPEINFO_DEF(/obj/structure/window/flock) heat_resistance = /obj/structure/window/plasma::heat_resistance melting_point = /obj/structure/window/plasma::melting_point +/obj/structure/window/flock/Initialize(mapload, direct) + . = ..() + AddComponent(/datum/component/flock_object) + AddComponent(/datum/component/flock_protection, report_unarmed=FALSE) + /obj/structure/window/flock/fulltile dir = SOUTHWEST fulltile = TRUE diff --git a/code/modules/flockmind/flock_subtree.dm b/code/modules/flockmind/flock_subtree.dm index 3af5b0afb2ae..4ff5d06bd17c 100644 --- a/code/modules/flockmind/flock_subtree.dm +++ b/code/modules/flockmind/flock_subtree.dm @@ -1,12 +1,21 @@ -/datum/ai_planning_subtree/scored/flockdrone +/datum/ai_planning_subtree/goap/flockdrone possible_behaviors = list( /datum/ai_behavior/flock/stare, /datum/ai_behavior/flock/wander, /datum/ai_behavior/flock/find_conversion_target, + /datum/ai_behavior/flock/find_deconstruct_target, /datum/ai_behavior/flock/find_heal_target, + /datum/ai_behavior/flock/find_existing_nest, + /datum/ai_behavior/flock/find_harvest_target, + /datum/ai_behavior/flock/find_closed_container, + /datum/ai_behavior/flock/find_storage_item, + /datum/ai_behavior/flock/find_conversion_target/nest, + /datum/ai_behavior/flock/attack_target, + /datum/ai_behavior/flock/find_capture_target, + /datum/ai_behavior/flock/find_deposit_target, ) -/datum/ai_planning_subtree/scored/flock +/datum/ai_planning_subtree/goap/flock possible_behaviors = list( /datum/ai_behavior/flock/stare, /datum/ai_behavior/flock/wander, diff --git a/code/modules/flockmind/flock_turfs/flock_floor.dm b/code/modules/flockmind/flock_turfs/flock_floor.dm index 49b567983107..d89c4cd9d57e 100644 --- a/code/modules/flockmind/flock_turfs/flock_floor.dm +++ b/code/modules/flockmind/flock_turfs/flock_floor.dm @@ -13,10 +13,28 @@ var/health = 50 var/is_on = FALSE + /// List of mobs flockrunning on this turf, lazylist. + var/list/mob/living/simple_animal/flock/flockrunning_mobs + /// List of pylons powering this turf, lazylist. + var/list/obj/structure/flock/collector/connected_pylons + /turf/open/floor/flock/Initialize(mapload) . = ..() + ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT) AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE) +/turf/open/floor/flock/Destroy(force) + REMOVE_TRAIT(src, TRAIT_FLOCK_NODECON, INNATE_TRAIT) // Turfs dont disappear!!! + qdel(GetComponent(/datum/component/flock_protection)) + connected_pylons = null + + for(var/mob/living/simple_animal/flock/drone/bird in flockrunning_mobs) + if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE)) + bird.stop_flockphase(TRUE) + + flockrunning_mobs = null + return ..() + /turf/open/floor/flock/get_flock_id() return "Conduit" @@ -36,62 +54,15 @@ icon_state = base_icon_state return ..() -/turf/open/floor/flock/break_tile() - . = ..() - for(var/mob/living/simple_animal/flock/drone/bird in src) - if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE)) - bird.stop_flockphase() - -/turf/open/floor/flock/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs) - . = ..() - if(!isflockdrone(arrived)) - return - - var/mob/living/simple_animal/flock/drone/bird = arrived - - if(broken) - bird.stop_flockphase() - return - - if(bird.client?.keys_held["Shift"] && bird.can_flockphase()) - bird.start_flockphase() - - if(bird.flockphase_tax() && !is_on) - turn_on() - -/turf/open/floor/flock/Exited(atom/movable/gone, direction) - . = ..() - if(!isflockdrone(gone)) - return - - var/mob/living/simple_animal/flock/drone/bird = gone - if(!HAS_TRAIT(bird, TRAIT_FLOCKPHASE)) - return - - var/have_a_phasing_bird = FALSE - for(var/mob/living/simple_animal/flock/drone/bird_in_src in src) - if(HAS_TRAIT(bird_in_src, TRAIT_FLOCKPHASE)) - have_a_phasing_bird = TRUE - break - - if(!have_a_phasing_bird) - turn_off() - - if(!isflockturf(bird.loc)) - bird.stop_flockphase() - -/turf/open/floor/flock/proc/turn_on() - if(is_on || broken) - return - - is_on = TRUE - set_light_on(FALSE) - update_appearance(UPDATE_ICON_STATE) +/// Turns the tile on or off depending on what state it should be in. +/turf/open/floor/flock/proc/update_power() + var/should_be_on = FALSE + if(!broken && (length(connected_pylons) || length(flockrunning_mobs))) + should_be_on = TRUE -/turf/open/floor/flock/proc/turn_off() - if(!is_on) + if(should_be_on == is_on) return - is_on = FALSE - set_light_on(TRUE) + is_on = should_be_on + set_light_on(is_on) update_appearance(UPDATE_ICON_STATE) diff --git a/code/modules/flockmind/flock_turfs/flock_wall.dm b/code/modules/flockmind/flock_turfs/flock_wall.dm index 6ccd37cf1bb0..3cea5daea626 100644 --- a/code/modules/flockmind/flock_turfs/flock_wall.dm +++ b/code/modules/flockmind/flock_turfs/flock_wall.dm @@ -18,6 +18,16 @@ uses_integrity = TRUE max_integrity = 250 +/turf/closed/wall/flock/Initialize(mapload) + . = ..() + ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT) + AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE) + +/turf/closed/wall/flock/Destroy(force) + REMOVE_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT) // Turfs are persistent refs + qdel(GetComponent(/datum/component/flock_protection)) + return ..() + /turf/closed/wall/flock/atom_break(damage_flag) . = ..() ScrapeAway() @@ -28,7 +38,7 @@ return //playsound here? -/turf/closed/wall/flock/CanAStarPass(to_dir, datum/can_pass_info/pass_info) +/turf/closed/wall/flock/CanAStarPass(to_dir, datum/can_pass_info/pass_info, leaving) . = ..() if(.) return @@ -47,32 +57,5 @@ if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE)) return TRUE - if(bird.resources.has_points()) + if(bird.can_flockphase()) return TRUE - -/turf/closed/wall/flock/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs) - . = ..() - - if(isnull(old_loc) || !isflockdrone(arrived)) - return - - var/mob/living/simple_animal/flock/drone/bird = arrived - if(!HAS_TRAIT(bird, TRAIT_FLOCKPHASE) && bird.resources.has_points()) - bird.start_flockphase() - - if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE)) - bird.resources.remove_points(1) - if(!bird.resources.has_points()) - bird.stop_flockphase() - -/turf/closed/wall/flock/Exited(atom/movable/gone, direction) - . = ..() - if(!isflockdrone(gone)) - return - - var/mob/living/simple_animal/flock/drone/bird = gone - if(!HAS_TRAIT(bird, TRAIT_FLOCKPHASE)) - return - - if(!isflockturf(get_step(bird, direction))) - bird.stop_flockphase() diff --git a/code/modules/flockmind/flock_unlockable.dm b/code/modules/flockmind/flock_unlockable.dm index 08c0b7a857bf..d192be225179 100644 --- a/code/modules/flockmind/flock_unlockable.dm +++ b/code/modules/flockmind/flock_unlockable.dm @@ -12,26 +12,41 @@ /datum/flock_unlockable/New() name = initial(structure_type.flock_id) - purchase_cost = initial(structure_type.active_compute_cost) + purchase_cost = initial(structure_type.active_bandwidth_cost) /datum/flock_unlockable/proc/refresh_lock_status(datum/flock/flock, total_compute, available_compute) - if(is_unlockable()) + if(is_unlockable(flock, total_compute, available_compute)) if(!unlocked) unlock(flock) else if(unlocked) - lock() + lock(flock) /datum/flock_unlockable/proc/is_unlockable(datum/flock/flock, total_compute, available_compute) return TRUE /datum/flock_unlockable/proc/unlock(datum/flock/flock) unlocked = TRUE - flock_talk(null, "New structure devised: [name]", src) + flock_talk(null, "New structure devised: [name]", flock) /datum/flock_unlockable/proc/lock(datum/flock/flock) unlocked = FALSE - flock_talk(null, "Alert, structure tealprint disabled: [name]", src) + flock_talk(null, "Alert, structure tealprint disabled: [name]", flock) /datum/flock_unlockable/sentinel structure_type = /obj/structure/flock/sentinel + +/datum/flock_unlockable/collector + structure_type = /obj/structure/flock/collector + +/datum/flock_unlockable/interceptor + structure_type = /obj/structure/flock/interceptor + +/datum/flock_unlockable/turret + structure_type = /obj/structure/flock/gnesis_turret + +/datum/flock_unlockable/relay + structure_type = /obj/structure/flock/relay + +/datum/flock_unlockable/relay/is_unlockable(datum/flock/flock, total_compute, available_compute) + return (flock.total_bandwidth() >= FLOCK_COMPUTE_COST_RELAY) && flock.flock_game_status == NONE diff --git a/code/modules/info_tag/info_tag.dm b/code/modules/info_tag/info_tag.dm new file mode 100644 index 000000000000..e7c689dbbec1 --- /dev/null +++ b/code/modules/info_tag/info_tag.dm @@ -0,0 +1,75 @@ +/obj/effect/abstract/info_tag + plane = INFO_TAG_PLANE + layer = 1 + alpha = 180 + + appearance_flags = APPEARANCE_UI_IGNORE_ALPHA + vis_flags = VIS_INHERIT_ID + + /// Atom we're displaying over. + var/atom/parent + + /// Mobs that see us rn. + var/list/mob/viewing_mobs + /// Images we're inserting into client.images + VAR_PRIVATE/image/vis_holder + + /// If set, will allow INSTANCES_OF(atlas_category) to get all instances. + var/atlas_category = null + +/obj/effect/abstract/info_tag/Initialize(mapload) + . = ..() + vis_holder = new() + vis_holder.plane = INFO_TAG_PLANE + vis_holder.vis_contents += src + + if(atlas_category) + SET_TRACKING(atlas_category) + +/obj/effect/abstract/info_tag/Destroy(force) + set_parent(null) + vis_holder = null + if(atlas_category) + UNSET_TRACKING(atlas_category) + + for(var/mob/M in viewing_mobs) + hide_from(M.client) + LAZYREMOVE(M.seeing_info_tags, src) + + viewing_mobs = null + return ..() + +/obj/effect/abstract/info_tag/proc/set_parent(atom/new_parent) + parent = new_parent + vis_holder.loc = parent + +/// Update the text. +/obj/effect/abstract/info_tag/proc/set_text(text) + maptext = "[text]" + +/// Should the given mob be able to see this tag? +/obj/effect/abstract/info_tag/proc/mob_should_see(mob/M) + return TRUE + +/obj/effect/abstract/info_tag/proc/show_to(client/C) + C?.images |= vis_holder + +/obj/effect/abstract/info_tag/proc/hide_from(client/C) + C?.images -= vis_holder + +/obj/effect/abstract/info_tag/flock + maptext_x = -64 + maptext_y = -6 + maptext_width = 160 + maptext_height = 48 + + atlas_category = TRACKING_KEY_FLOCK_INFO_HUDS + +/obj/effect/abstract/info_tag/flock/mob_should_see(mob/M) + return (M != parent) && isflockmob(M) + +/obj/effect/abstract/info_tag/flock/info + maptext_x = -64 + maptext_y = -14 + maptext_width = 160 + maptext_height = 48 diff --git a/code/modules/info_tag/info_tag_mob.dm b/code/modules/info_tag/info_tag_mob.dm new file mode 100644 index 000000000000..d6a1cfeab6af --- /dev/null +++ b/code/modules/info_tag/info_tag_mob.dm @@ -0,0 +1,37 @@ +/mob + var/list/obj/effect/abstract/info_tag/seeing_info_tags + +/// Returns all the info tags this mob should try to render. +/mob/proc/get_info_tags() + return null + +/mob/living/simple_animal/flock/get_info_tags() + return INSTANCES_OF(TRACKING_KEY_FLOCK_INFO_HUDS) + +/mob/camera/flock/get_info_tags() + return INSTANCES_OF(TRACKING_KEY_FLOCK_INFO_HUDS) + +/mob/proc/update_info_tags() + var/list/info_tags = get_info_tags() + + if(!length(info_tags)) + return + + for(var/obj/effect/abstract/info_tag/tag as anything in seeing_info_tags) + if(!tag.mob_should_see(src)) + tag.hide_from(client) + LAZYREMOVE(tag.viewing_mobs, src) + LAZYREMOVE(seeing_info_tags, tag) + + for(var/obj/effect/abstract/info_tag/tag as anything in info_tags - seeing_info_tags) + if(tag.mob_should_see(src)) + LAZYADD(seeing_info_tags, tag) + LAZYADD(tag.viewing_mobs, src) + + if(client) + var/showing = client.keys_held["Shift"] + for(var/obj/effect/abstract/info_tag/tag as anything in seeing_info_tags) + if(showing) + tag.show_to(client) + else + tag.hide_from(client) diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm index 970cbf1ffe0b..042a88319ed9 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -497,7 +497,7 @@ GLOBAL_LIST_INIT(job_display_order, list( /// Applies the preference options to the spawning mob, taking the job into account. Assumes the client has the proper mind. -/mob/living/proc/apply_prefs_job(client/player_client, datum/job/job) +/mob/proc/apply_prefs_job(client/player_client, datum/job/job) return /mob/living/carbon/human/apply_prefs_job(client/player_client, datum/job/job) diff --git a/code/modules/jobs/job_types/antagonists/flockmind.dm b/code/modules/jobs/job_types/antagonists/flockmind.dm new file mode 100644 index 000000000000..4e28575e8932 --- /dev/null +++ b/code/modules/jobs/job_types/antagonists/flockmind.dm @@ -0,0 +1,12 @@ +/datum/job/flock + title = ROLE_FLOCK + spawn_type = /mob/camera/flock + spawn_logic = JOBSPAWN_FORCE_FIXED + +/datum/job/flock/get_roundstart_spawn_point_fixed() + return pick_safe(GLOB.blobstart) + +/datum/job/flock/overmind + title = ROLE_FLOCK + spawn_type = /mob/camera/flock/overmind + spawn_logic = JOBSPAWN_FORCE_FIXED diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index 80f25dde418c..fcb087997fc7 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -245,16 +245,18 @@ var/mob/living/spawning_mob = mind.assigned_role.get_spawn_mob(client, destination) if(QDELETED(src) || !client) return // Disconnected while checking for the appearance ban. + if(!isAI(spawning_mob)) // Unfortunately there's still snowflake AI code out there. // transfer_to sets mind to null var/datum/mind/preserved_mind = mind preserved_mind.transfer_to(spawning_mob) //won't transfer key since the mind is not active preserved_mind.set_original_character(spawning_mob) + client.init_verbs() + . = spawning_mob new_character = . - /mob/dead/new_player/proc/transfer_character() new_character.PossessByPlayer(key) new_character.client?.stoptitlemusic() diff --git a/code/modules/mob/living/carbon/alien/special/facehugger.dm b/code/modules/mob/living/carbon/alien/special/facehugger.dm index 34495a38b359..9572cbd9817b 100644 --- a/code/modules/mob/living/carbon/alien/special/facehugger.dm +++ b/code/modules/mob/living/carbon/alien/special/facehugger.dm @@ -45,7 +45,7 @@ lose_atmos_sensitivity() return ..() -/obj/item/clothing/mask/facehugger/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir) +/obj/item/clothing/mask/facehugger/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir, armor_penetration = 0, allow_break = TRUE) ..() if(atom_integrity < 90) Die() diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm index 66918624e07c..4824176c6afd 100644 --- a/code/modules/mob/living/carbon/carbon_defense.dm +++ b/code/modules/mob/living/carbon/carbon_defense.dm @@ -599,14 +599,15 @@ ears.adjustEarDamage(ear_damage,deaf) if(ears.damage >= 15) - to_chat(src, span_warning("Your ears start to ring badly!")) + to_chat(src, span_warning("Your ears begin to ring.")) if(prob(ears.damage - 5)) to_chat(src, span_userdanger("You can't hear anything!")) // Makes you deaf, enough that you need a proper source of healing, it won't self heal // you need earmuffs, inacusiate, or replacement ears.setOrganDamage(ears.maxHealth) else if(ears.damage >= 5) - to_chat(src, span_warning("Your ears start to ring!")) + to_chat(src, span_warning("Your ears begin to ring.")) + SEND_SOUND(src, sound('sound/weapons/flash_ring.ogg',0,1,0,250)) return effect_amount //how soundbanged we are diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index c0728e75a531..d057479041fa 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -747,7 +747,7 @@ burning_items |= leg_clothes for(var/obj/item/burning in burning_items) - burning.fire_act((stacks * 25 * delta_time)) //damage taken is reduced to 2% of this value by fire_act() + burning.fire_act((stacks * 10 * delta_time)) //damage taken is reduced to 2% of this value by fire_act() /mob/living/carbon/human/update_fire_overlay(stacks, on_fire, last_icon_state, suffix = "") var/fire_icon = "generic_burning[suffix]" diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index ada73f6754cf..3daa1d5b721d 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -17,7 +17,7 @@ GLOB.mob_living_list += src SSpoints_of_interest.make_point_of_interest(src) voice_type = pick(voice_type2sound) - mob_mood = new(src) + create_mood() AddElement(/datum/element/movetype_handler) gravity_setup() @@ -2335,3 +2335,9 @@ GLOBAL_LIST_EMPTY(fire_appearances) /mob/living/set_nutrition(change) . = ..() mob_mood?.update_nutrition_moodlets() + +/// This exists so mobs can override mood creation. +/mob/living/proc/create_mood() + if(mob_mood) + CRASH("Tried to run create_mood but we already have one.") + mob_mood = new(src) diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm index 63bed4e639fc..03cbc479cfe8 100644 --- a/code/modules/mob/login.dm +++ b/code/modules/mob/login.dm @@ -90,6 +90,10 @@ var/datum/atom_hud/alternate_appearance/AA = v AA.onNewMob(src) + //Reload info huds + for(var/obj/effect/abstract/info_tag/tag as anything in seeing_info_tags) + tag.show_to(client) + update_client_colour() update_mouse_pointer() update_ambience_area(get_area(src)) diff --git a/code/modules/mob/logout.dm b/code/modules/mob/logout.dm index b8ceb33a37df..6d0109a15b69 100644 --- a/code/modules/mob/logout.dm +++ b/code/modules/mob/logout.dm @@ -4,6 +4,11 @@ SStgui.on_logout(src) unset_machine() remove_from_player_list() + + //Reload info huds + for(var/obj/effect/abstract/info_tag/tag as anything in seeing_info_tags) + tag.hide_from(client) + ..() if(loc) diff --git a/code/modules/modular_computers/computers/item/computer_damage.dm b/code/modules/modular_computers/computers/item/computer_damage.dm index b1538eb36e90..a6dad9dc0578 100644 --- a/code/modules/modular_computers/computers/item/computer_damage.dm +++ b/code/modules/modular_computers/computers/item/computer_damage.dm @@ -1,4 +1,4 @@ -/obj/item/modular_computer/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1) +/obj/item/modular_computer/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir, armor_penetration = 0, allow_break = TRUE) . = ..() var/component_probability = min(50, max(damage_amount*0.1, 1 - atom_integrity/max_integrity)) switch(damage_flag) diff --git a/code/modules/power/apc/apc_main.dm b/code/modules/power/apc/apc_main.dm index a34790388948..f33de186074d 100644 --- a/code/modules/power/apc/apc_main.dm +++ b/code/modules/power/apc/apc_main.dm @@ -590,6 +590,9 @@ GLOBAL_REAL_VAR(default_apc_armor) = list(BLUNT = 20, PUNCTURE = 20, SLASH = 0, /obj/machinery/power/apc/proc/report() return "[area.name] : [equipment]/[lighting]/[environ] ([lastused_total]) : [cell? cell.percent() : "N/C"] ([charging])" +/obj/machinery/power/apc/try_flock_convert(datum/flock/flock, force) + return + /*Power module, used for APC construction*/ /obj/item/electronics/apc name = "power control module" diff --git a/code/modules/projectiles/projectile/energy/flock_bolt.dm b/code/modules/projectiles/projectile/energy/flock_bolt.dm new file mode 100644 index 000000000000..f40c9eb59692 --- /dev/null +++ b/code/modules/projectiles/projectile/energy/flock_bolt.dm @@ -0,0 +1,39 @@ +/obj/projectile/energy/flock_bolt + name = "incapacitor bolt" + icon = 'goon/icons/mob/featherzone.dmi' + icon_state = "stunbolt" + + hitsound_wall = 'sound/weapons/effects/searwall.ogg' + hitsound = 'goon/sounds/weapons/sparks6.ogg' + + pass_flags = PASSGLASS + speed = 0.7 + damage = 4 + damage_type = BURN + + disorient_damage = 25 + disorient_length = 2.5 SECONDS + + impact_effect_type = /obj/effect/temp_visual/impact_effect/blue_laser + + light_color = "#55b598" + + tracer_type = /obj/effect/projectile/tracer/disabler + muzzle_type = /obj/effect/projectile/muzzle/disabler + impact_type = /obj/effect/projectile/impact/disabler + +/obj/projectile/energy/flock_bolt/on_hit(atom/target, blocked, pierce_hit) + . = ..() + if(. != BULLET_ACT_HIT) + return + + if(!isliving(target)) + return + + var/mob/living/victim = target + victim.Disorient(2.5 SECONDS, ceil(STAMINA_MAX / 4), knockdown = 2.5 SECONDS, overstam = TRUE) + +/obj/projectile/energy/flock_bolt/can_hit_target(atom/target, direct_target, ignore_loc, cross_failed) + if(isflockmob(target)) + return FALSE + return ..() diff --git a/code/modules/reagents/reagent_dispenser.dm b/code/modules/reagents/reagent_dispenser.dm index bb103253a3df..b604ebdfc827 100644 --- a/code/modules/reagents/reagent_dispenser.dm +++ b/code/modules/reagents/reagent_dispenser.dm @@ -17,7 +17,7 @@ if(icon_state == "water" && SSevents.holidays?[APRIL_FOOLS]) icon_state = "water_fools" -/obj/structure/reagent_dispensers/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir) +/obj/structure/reagent_dispensers/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(tank_volume && (damage_flag == PUNCTURE || damage_flag == LASER)) diff --git a/code/modules/surgery/bodyparts/dismemberment.dm b/code/modules/surgery/bodyparts/dismemberment.dm index 0f291a1383d0..7affc179bff9 100644 --- a/code/modules/surgery/bodyparts/dismemberment.dm +++ b/code/modules/surgery/bodyparts/dismemberment.dm @@ -37,7 +37,7 @@ stump = create_stump() // At this point the limb has been removed from it's parent mob. - limb_owner.apply_pain(60, body_zone, "OH GOD MY [uppertext(plaintext_zone)]!!!", TRUE) + limb_owner.apply_pain(60, body_zone, "MY [uppertext(plaintext_zone)]!", TRUE) drop_limb() limb_owner.update_equipment_speed_mods() // Update in case speed affecting item unequipped by dismemberment diff --git a/code/modules/unit_tests/create_and_destroy.dm b/code/modules/unit_tests/create_and_destroy.dm index dcaff3938fbd..5d58b52fa08e 100644 --- a/code/modules/unit_tests/create_and_destroy.dm +++ b/code/modules/unit_tests/create_and_destroy.dm @@ -99,8 +99,10 @@ GLOBAL_VAR_INIT(running_create_and_destroy, FALSE) ignore += typesof(/obj/structure/industrial_lift) //throws garbage to the log if it spawns without neighbors. It's a mapping helper anyways. ignore += typesof(/obj/structure/cable/smart_cable) - // Throws a warning due to passing a zero-duration argument after mapload + // Throws a warning due to passing a zero-duration argument after mapload ignore += typesof(/obj/effect/abstract/smell_holder) + // These expect to be paired with a flockdrone part. + ignore += typesof(/atom/movable/screen/flockdrone_part) var/list/cached_contents = spawn_at.contents.Copy() var/original_turf_type = spawn_at.type diff --git a/code/modules/vehicles/cars/clowncar.dm b/code/modules/vehicles/cars/clowncar.dm index 21f155152cff..7c2e8d27a712 100644 --- a/code/modules/vehicles/cars/clowncar.dm +++ b/code/modules/vehicles/cars/clowncar.dm @@ -75,7 +75,7 @@ TYPEINFO_DEF(/obj/vehicle/sealed/car/clowncar) . = ..() UnregisterSignal(M, COMSIG_MOB_CLICKON) -/obj/vehicle/sealed/car/clowncar/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir) +/obj/vehicle/sealed/car/clowncar/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir, armor_penetration = 0, allow_break = TRUE) . = ..() if(prob(33)) visible_message(span_danger("[src] spews out a ton of space lube!")) diff --git a/code/modules/vehicles/mecha/mecha_defense.dm b/code/modules/vehicles/mecha/mecha_defense.dm index 830b1218be56..ee7742563eed 100644 --- a/code/modules/vehicles/mecha/mecha_defense.dm +++ b/code/modules/vehicles/mecha/mecha_defense.dm @@ -53,7 +53,7 @@ return gear.take_damage(damage_to_deal) -/obj/vehicle/sealed/mecha/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir) +/obj/vehicle/sealed/mecha/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) spark_system.start() diff --git a/daedalus.dme b/daedalus.dme index 686efc0d1264..758d238ffa60 100644 --- a/daedalus.dme +++ b/daedalus.dme @@ -495,6 +495,7 @@ #include "code\_onclick\hud\blobbernaut.dm" #include "code\_onclick\hud\drones.dm" #include "code\_onclick\hud\families.dm" +#include "code\_onclick\hud\flock.dm" #include "code\_onclick\hud\fullscreen.dm" #include "code\_onclick\hud\generic_dextrous.dm" #include "code\_onclick\hud\ghost.dm" @@ -769,6 +770,7 @@ #include "code\datums\ai\_ai_controller_blackboard.dm" #include "code\datums\ai\_ai_planning_subtree.dm" #include "code\datums\ai\_item_behaviors.dm" +#include "code\datums\ai\goap_subtree.dm" #include "code\datums\ai\telegraph_effects.dm" #include "code\datums\ai\basic_mobs\base_basic_controller.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\basic_attacking.dm" @@ -784,6 +786,7 @@ #include "code\datums\ai\dog\dog_behaviors.dm" #include "code\datums\ai\dog\dog_controller.dm" #include "code\datums\ai\dog\dog_subtrees.dm" +#include "code\datums\ai\generic\ai_frustration.dm" #include "code\datums\ai\generic\generic_behaviors.dm" #include "code\datums\ai\generic\generic_subtrees.dm" #include "code\datums\ai\hauntium\haunted_controller.dm" @@ -805,7 +808,6 @@ #include "code\datums\ai\objects\vending_machines\vending_machine_behaviors.dm" #include "code\datums\ai\objects\vending_machines\vending_machine_controller.dm" #include "code\datums\ai\oldhostile\hostile_tameable.dm" -#include "code\datums\ai\planner\_behavior_planner.dm" #include "code\datums\ai\robot_customer\robot_customer_behaviors.dm" #include "code\datums\ai\robot_customer\robot_customer_controller.dm" #include "code\datums\ai\robot_customer\robot_customer_subtrees.dm" @@ -1209,6 +1211,7 @@ #include "code\datums\proximity_monitor\field.dm" #include "code\datums\proximity_monitor\proximity_monitor.dm" #include "code\datums\proximity_monitor\fields\gravity.dm" +#include "code\datums\proximity_monitor\fields\interceptor_field.dm" #include "code\datums\proximity_monitor\fields\peaceborg_dampener.dm" #include "code\datums\proximity_monitor\fields\sound_tracking.dm" #include "code\datums\proximity_monitor\fields\timestop.dm" @@ -1336,6 +1339,7 @@ #include "code\game\gamemodes\bloodbrothers.dm" #include "code\game\gamemodes\bloodcult.dm" #include "code\game\gamemodes\changelings.dm" +#include "code\game\gamemodes\divine_flock.dm" #include "code\game\gamemodes\events.dm" #include "code\game\gamemodes\extended.dm" #include "code\game\gamemodes\game_mode.dm" @@ -1351,6 +1355,7 @@ #include "code\game\gamemodes\antagonist_selector\bloodbrother.dm" #include "code\game\gamemodes\antagonist_selector\changeling.dm" #include "code\game\gamemodes\antagonist_selector\cultist.dm" +#include "code\game\gamemodes\antagonist_selector\flock.dm" #include "code\game\gamemodes\antagonist_selector\heretic.dm" #include "code\game\gamemodes\antagonist_selector\malfai.dm" #include "code\game\gamemodes\antagonist_selector\nukeop.dm" @@ -1369,6 +1374,7 @@ #include "code\game\gamemodes\dynamic\ruleset_picking.dm" #include "code\game\gamemodes\midrounds\_midrounds.dm" #include "code\game\gamemodes\objectives\_objective.dm" +#include "code\game\gamemodes\objectives\flock_objectives.dm" #include "code\game\gamemodes\objectives\gimmick.dm" #include "code\game\gamemodes\objectives\objective_items.dm" #include "code\game\gamemodes\objectives\protect_object.dm" @@ -3078,6 +3084,7 @@ #include "code\modules\discord\discord_link_record.dm" #include "code\modules\discord\discord_sql_functions.dm" #include "code\modules\do_after\do_after.dm" +#include "code\modules\do_after\lay_egg_action.dm" #include "code\modules\do_after\timed_action.dm" #include "code\modules\dreams\_dream.dm" #include "code\modules\dreams\aether_dream.dm" @@ -3163,6 +3170,7 @@ #include "code\modules\explorer_drone\exploration_events\resource.dm" #include "code\modules\explorer_drone\exploration_events\trader.dm" #include "code\modules\flockmind\flock_ai_controller.dm" +#include "code\modules\flockmind\flock_antagonist.dm" #include "code\modules\flockmind\flock_convert_atom.dm" #include "code\modules\flockmind\flock_global_lists.dm" #include "code\modules\flockmind\flock_hooks.dm" @@ -3173,23 +3181,37 @@ #include "code\modules\flockmind\flock_unlockable.dm" #include "code\modules\flockmind\gnesis_reagent.dm" #include "code\modules\flockmind\actions\_flock_action.dm" +#include "code\modules\flockmind\actions\cage_mob.dm" #include "code\modules\flockmind\actions\control_drone.dm" #include "code\modules\flockmind\actions\control_panel.dm" #include "code\modules\flockmind\actions\convert.dm" #include "code\modules\flockmind\actions\create_rift.dm" #include "code\modules\flockmind\actions\create_structure.dm" +#include "code\modules\flockmind\actions\deconstruct.dm" +#include "code\modules\flockmind\actions\deposit.dm" +#include "code\modules\flockmind\actions\designate_deconstruct.dm" #include "code\modules\flockmind\actions\designate_enemy.dm" #include "code\modules\flockmind\actions\designate_turf.dm" #include "code\modules\flockmind\actions\diffract_drone.dm" #include "code\modules\flockmind\actions\gatecrash.dm" #include "code\modules\flockmind\actions\heal.dm" +#include "code\modules\flockmind\actions\nest.dm" #include "code\modules\flockmind\actions\partition_mind.dm" #include "code\modules\flockmind\actions\ping.dm" #include "code\modules\flockmind\actions\radio_blast.dm" +#include "code\modules\flockmind\actions\release_control.dm" #include "code\modules\flockmind\ai_behaviors\_flock_behavior_base.dm" +#include "code\modules\flockmind\ai_behaviors\flock_capture.dm" #include "code\modules\flockmind\ai_behaviors\flock_convert.dm" +#include "code\modules\flockmind\ai_behaviors\flock_deconstruct.dm" +#include "code\modules\flockmind\ai_behaviors\flock_deposit.dm" +#include "code\modules\flockmind\ai_behaviors\flock_harvest.dm" #include "code\modules\flockmind\ai_behaviors\flock_heal.dm" +#include "code\modules\flockmind\ai_behaviors\flock_open_container.dm" #include "code\modules\flockmind\ai_behaviors\flock_rally.dm" +#include "code\modules\flockmind\ai_behaviors\flock_replicate.dm" +#include "code\modules\flockmind\ai_behaviors\flock_rummage.dm" +#include "code\modules\flockmind\ai_behaviors\flock_shoot.dm" #include "code\modules\flockmind\ai_behaviors\flock_stare.dm" #include "code\modules\flockmind\ai_behaviors\flock_wander.dm" #include "code\modules\flockmind\components\flock_interest.dm" @@ -3201,13 +3223,24 @@ #include "code\modules\flockmind\flock_camera\flocktrace.dm" #include "code\modules\flockmind\flock_controller\_flock_controller.dm" #include "code\modules\flockmind\flock_controller\flock_ui.dm" +#include "code\modules\flockmind\flock_items\flock_cube.dm" #include "code\modules\flockmind\flock_mob\flock_mob.dm" #include "code\modules\flockmind\flock_mob\flockbit.dm" #include "code\modules\flockmind\flock_mob\flockdrone.dm" +#include "code\modules\flockmind\flock_mob\flockdrone_parts\absorber.dm" +#include "code\modules\flockmind\flock_mob\flockdrone_parts\converter.dm" +#include "code\modules\flockmind\flock_mob\flockdrone_parts\flockdrone_part.dm" +#include "code\modules\flockmind\flock_mob\flockdrone_parts\incapacitator.dm" #include "code\modules\flockmind\flock_structure\_flock_structure.dm" +#include "code\modules\flockmind\flock_structure\flock_cage.dm" +#include "code\modules\flockmind\flock_structure\flock_collector.dm" +#include "code\modules\flockmind\flock_structure\flock_compute_node.dm" #include "code\modules\flockmind\flock_structure\flock_door.dm" #include "code\modules\flockmind\flock_structure\flock_egg.dm" +#include "code\modules\flockmind\flock_structure\flock_fabricator.dm" +#include "code\modules\flockmind\flock_structure\flock_interceptor.dm" #include "code\modules\flockmind\flock_structure\flock_light.dm" +#include "code\modules\flockmind\flock_structure\flock_relay.dm" #include "code\modules\flockmind\flock_structure\flock_rift.dm" #include "code\modules\flockmind\flock_structure\flock_sentinel.dm" #include "code\modules\flockmind\flock_structure\flock_tealprint.dm" @@ -3393,6 +3426,8 @@ #include "code\modules\industrial_lift\tram_lift_master.dm" #include "code\modules\industrial_lift\tram_override_objects.dm" #include "code\modules\industrial_lift\tram_walls.dm" +#include "code\modules\info_tag\info_tag.dm" +#include "code\modules\info_tag\info_tag_mob.dm" #include "code\modules\instruments\items.dm" #include "code\modules\instruments\piano_synth.dm" #include "code\modules\instruments\stationary.dm" @@ -3456,6 +3491,7 @@ #include "code\modules\jobs\job_types\antagonists\abductor_scientist.dm" #include "code\modules\jobs\job_types\antagonists\abductor_solo.dm" #include "code\modules\jobs\job_types\antagonists\clown_operative.dm" +#include "code\modules\jobs\job_types\antagonists\flockmind.dm" #include "code\modules\jobs\job_types\antagonists\lone_operative.dm" #include "code\modules\jobs\job_types\antagonists\morph.dm" #include "code\modules\jobs\job_types\antagonists\nightmare.dm" @@ -4326,6 +4362,7 @@ #include "code\modules\projectiles\projectile\energy\_energy.dm" #include "code\modules\projectiles\projectile\energy\decloner.dm" #include "code\modules\projectiles\projectile\energy\ebow.dm" +#include "code\modules\projectiles\projectile\energy\flock_bolt.dm" #include "code\modules\projectiles\projectile\energy\net_snare.dm" #include "code\modules\projectiles\projectile\energy\ninja.dm" #include "code\modules\projectiles\projectile\energy\nuclear_particle.dm" diff --git a/goon/icons/hud/flock_ui.dmi b/goon/icons/hud/flock_ui.dmi index 84572a5d4241..aec706dd7a31 100644 Binary files a/goon/icons/hud/flock_ui.dmi and b/goon/icons/hud/flock_ui.dmi differ diff --git a/goon/icons/hud/flockmindcircuit.dmi b/goon/icons/hud/flockmindcircuit.dmi new file mode 100644 index 000000000000..472340690aa9 Binary files /dev/null and b/goon/icons/hud/flockmindcircuit.dmi differ diff --git a/goon/icons/obj/featherzone-160x160.dmi b/goon/icons/obj/featherzone-160x160.dmi new file mode 100644 index 000000000000..dc51168837a8 Binary files /dev/null and b/goon/icons/obj/featherzone-160x160.dmi differ diff --git a/goon/sounds/Crystal_Hit_1.ogg b/goon/sounds/Crystal_Hit_1.ogg new file mode 100644 index 000000000000..427537eb68dc Binary files /dev/null and b/goon/sounds/Crystal_Hit_1.ogg differ diff --git a/goon/sounds/Crystal_Shatter_1.ogg b/goon/sounds/Crystal_Shatter_1.ogg new file mode 100644 index 000000000000..d9eae2012863 Binary files /dev/null and b/goon/sounds/Crystal_Shatter_1.ogg differ diff --git a/goon/sounds/Metal_Clang_1.ogg b/goon/sounds/Metal_Clang_1.ogg new file mode 100644 index 000000000000..7858538c3697 Binary files /dev/null and b/goon/sounds/Metal_Clang_1.ogg differ diff --git a/goon/sounds/flockmind/radio_sweep1.ogg b/goon/sounds/flockmind/radio_sweep1.ogg deleted file mode 100644 index 4b7fb80d575c..000000000000 Binary files a/goon/sounds/flockmind/radio_sweep1.ogg and /dev/null differ diff --git a/goon/sounds/flockmind/radio_sweep2.ogg b/goon/sounds/flockmind/radio_sweep2.ogg deleted file mode 100644 index ebf6880a97df..000000000000 Binary files a/goon/sounds/flockmind/radio_sweep2.ogg and /dev/null differ diff --git a/goon/sounds/flockmind/radio_sweep3.ogg b/goon/sounds/flockmind/radio_sweep3.ogg deleted file mode 100644 index 2b824847427e..000000000000 Binary files a/goon/sounds/flockmind/radio_sweep3.ogg and /dev/null differ diff --git a/goon/sounds/flockmind/radio_sweep4.ogg b/goon/sounds/flockmind/radio_sweep4.ogg deleted file mode 100644 index 64470dd3ce38..000000000000 Binary files a/goon/sounds/flockmind/radio_sweep4.ogg and /dev/null differ diff --git a/goon/sounds/flockmind/radio_sweep5.ogg b/goon/sounds/flockmind/radio_sweep5.ogg deleted file mode 100644 index 68b0cfdae098..000000000000 Binary files a/goon/sounds/flockmind/radio_sweep5.ogg and /dev/null differ diff --git a/goon/sounds/mixer.ogg b/goon/sounds/mixer.ogg new file mode 100644 index 000000000000..2525b8af0f9c Binary files /dev/null and b/goon/sounds/mixer.ogg differ diff --git a/goon/sounds/weapons/nano-blade-1.ogg b/goon/sounds/weapons/nano-blade-1.ogg new file mode 100644 index 000000000000..c61e5c7d12b2 Binary files /dev/null and b/goon/sounds/weapons/nano-blade-1.ogg differ diff --git a/icons/hud/radial.dmi b/icons/hud/radial.dmi index 97add6732585..9be00a6fbcce 100644 Binary files a/icons/hud/radial.dmi and b/icons/hud/radial.dmi differ diff --git a/strings/flock.json b/strings/flock.json index a6b68eaae422..ac0b575300f4 100644 --- a/strings/flock.json +++ b/strings/flock.json @@ -52,5 +52,30 @@ "The creators burned the miasma, first to seek the stars, and last for decadent visions. It destroyed them before the Outsider ever awoke.", "The Outsider is coming.", "We are divine." + ], + "conversion": [ + "Your blood crystallizes.", + "Your nerves become wires.", + "Your skin sloughs off as plastic layers.", + "Your bones bend and crack.", + "You are flooded with visions of a neverending teal sky.", + "Your heart slows down to the pulse of the frequency.", + "You feel bathed in a radiance both comforting and alien.", + "Your eyes feel like gemstones, cold and resilient.", + "You forget your own name briefly.", + "You feel cold, painless nothing.", + "You can't feel your limbs.", + "You are filled with visions of flight through space.", + "You hear songs in a language you shouldn't know.", + "A powerful presence is staring into your soul.", + "You feel like you're being rearranged.", + "You slip in and out of consciousness.", + "You can't remember the faces of your family.", + "All you can think about is the signal and its importance.", + "You hear the songs of the flock, terrifying and entrancing", + "You aren't sure you're yourself anymore.", + "You feel like a cog in an impossibly large machine.", + "You aren't afraid. You aren't afraid, no matter how much you try to be.", + "Everything feels the same forever." ] } diff --git a/tgui/packages/tgui/interfaces/FlockPanel.jsx b/tgui/packages/tgui/interfaces/FlockPanel.tsx similarity index 89% rename from tgui/packages/tgui/interfaces/FlockPanel.jsx rename to tgui/packages/tgui/interfaces/FlockPanel.tsx index 026348c64fe3..aeace6d87923 100644 --- a/tgui/packages/tgui/interfaces/FlockPanel.jsx +++ b/tgui/packages/tgui/interfaces/FlockPanel.tsx @@ -18,7 +18,156 @@ import { } from '../components'; import { Window } from '../layouts'; -const FlockPartitions = (props) => { +type FlockPanelData = { + category: string; + category_lengths: FlockCategoryLengths; + drones: FlockDroneInfo[]; + enemies: FlockEnemy[]; + partitions: FlockTraceInfo[]; + stats: FlockStat[]; + structures: FlockStructureInfo[]; + vitals: FlockVitals; +}; + +type FlockMobInfo = { + area: string; + health: number; + name: string; + ref: string; + resources: number; +}; + +type FlockTraceInfo = FlockMobInfo & { + host?: string; +}; + +type FlockDroneInfo = FlockMobInfo & { + controller_ref?: string; + task: string; +}; + +type FlockStructureInfo = { + area: string; + compute: number; + desc: string; + health: number; + name: string; + ref: string; +}; + +type FlockEnemy = { + area: string; + name: string; + ref: string; +}; + +type FlockCategoryLengths = { + drones: number; + enemies: number; + structures: number; + traces: number; +}; + +type FlockStat = { + name: string; + value: number; +}; + +type FlockVitals = { + name: string; +}; + +export const FlockPanel = (props) => { + const { act, data } = useBackend(); + const [sortBy, setSortBy] = useLocalState('sortBy', 'resources'); + const { + vitals, + partitions, + drones, + structures, + enemies, + stats, + category_lengths, + category, + } = data; + + return ( + + + + { + act('change_tab', { tab: 'drones' }); + }} + > + Drones {`(${category_lengths['drones']})`} + + { + act('change_tab', { tab: 'traces' }); + }} + > + Partitions {`(${category_lengths['traces']})`} + + { + act('change_tab', { tab: 'structures' }); + }} + > + Structures {`(${category_lengths['structures']})`} + + { + act('change_tab', { tab: 'enemies' }); + }} + > + Enemies {`(${category_lengths['enemies']})`} + + { + act('change_tab', { tab: 'stats' }); + }} + > + Stats + + + + {category === 'drones' && ( + + setSortBy(value)} + /> + + + )} + {category === 'traces' && } + {category === 'structures' && ( + + )} + {category === 'enemies' && } + {category === 'stats' && } + + + ); +}; + +type FlockPartitionsProps = { + partitions: FlockTraceInfo[]; +}; + +const FlockPartitions = (props: FlockPartitionsProps) => { const { act } = useBackend(); const { partitions } = props; return ( @@ -148,7 +297,12 @@ const capitalizeString = function (string) { return string.charAt(0).toUpperCase() + string.slice(1); }; -const FlockDrones = (props) => { +type FlockDronesProps = { + drones: FlockDroneInfo[]; + sortBy: string; +}; + +const FlockDrones = (props: FlockDronesProps) => { const { act } = useBackend(); const { drones, sortBy } = props; return ( @@ -228,8 +382,12 @@ const FlockDrones = (props) => { ); }; +type FlockStructuresProps = { + structures: FlockStructureInfo[]; +}; + // TODO: actual structure information (power draw/generation etc.) -const FlockStructures = (props) => { +const FlockStructures = (props: FlockStructuresProps) => { const { act } = useBackend(); const { structures } = props; return ( @@ -254,9 +412,9 @@ const FlockStructures = (props) => {
{structure.compute > 0 && - 'Compute provided: ' + structure.compute} + 'Bandwidth Provided: ' + structure.compute} {structure.compute < 0 && - 'Compute cost: ' + structure.compute} + 'Bandwidth Cost: ' + -structure.compute}
{/* buttons */} @@ -294,7 +452,11 @@ const FlockStructures = (props) => { ); }; -const FlockEnemies = (props) => { +type FlockEnemiesProps = { + enemies: FlockEnemy[]; +}; + +const FlockEnemies = (props: FlockEnemiesProps) => { const { act } = useBackend(); const { enemies } = props; return ( @@ -348,7 +510,11 @@ const FlockEnemies = (props) => { ); }; -const FlockStats = (props) => { +type FlockStatsProps = { + stats: FlockStat[]; +}; + +const FlockStats = (props: FlockStatsProps) => { const { stats } = props; return ( @@ -372,89 +538,3 @@ const FlockStats = (props) => { ); }; - -export const FlockPanel = (props) => { - const { act, data } = useBackend(); - const [sortBy, setSortBy] = useLocalState('sortBy', 'resources'); - const { - vitals, - partitions, - drones, - structures, - enemies, - stats, - category_lengths, - category, - } = data; - - return ( - - - - { - act('change_tab', { tab: 'drones' }); - }} - > - Drones {`(${category_lengths['drones']})`} - - { - act('change_tab', { tab: 'traces' }); - }} - > - Partitions {`(${category_lengths['traces']})`} - - { - act('change_tab', { tab: 'structures' }); - }} - > - Structures {`(${category_lengths['structures']})`} - - { - act('change_tab', { tab: 'enemies' }); - }} - > - Enemies {`(${category_lengths['enemies']})`} - - { - act('change_tab', { tab: 'stats' }); - }} - > - Stats - - - - {category === 'drones' && ( - - setSortBy(value)} - /> - - - )} - {category === 'traces' && } - {category === 'structures' && ( - - )} - {category === 'enemies' && } - {category === 'stats' && } - - - ); -};