diff --git a/code/__DEFINES/ai/monsters.dm b/code/__DEFINES/ai/monsters.dm index d4f9af401cf..f80eb85735d 100644 --- a/code/__DEFINES/ai/monsters.dm +++ b/code/__DEFINES/ai/monsters.dm @@ -128,6 +128,10 @@ #define BB_MINEBOT_DUMP_ABILITY "minebot_dump_ability" /// key that stores our target turf #define BB_TARGET_MINERAL_TURF "target_mineral_turf" +///key that holds our missile ability +#define BB_MINEBOT_MISSILE_ABILITY "minebot_missile_ability" +///key that holds our landmine ability +#define BB_MINEBOT_LANDMINE_ABILITY "minebot_landmine_ability" /// key that stores list of the turfs we ignore #define BB_BLACKLIST_MINERAL_TURFS "blacklist_mineral_turfs" /// key that stores the previous blocked wall @@ -136,6 +140,20 @@ #define BB_AUTOMATED_MINING "automated_mining" /// key that stores the nearest dead human #define BB_NEARBY_DEAD_MINER "nearby_dead_miner" +///key that holds the drone we defend +#define BB_DRONE_DEFEND "defend_drone" +///key that holds the minimum distance before we flee +#define BB_MINIMUM_SHOOTING_DISTANCE "minimum_shooting_distance" +///key that holds the miner we must befriend +#define BB_MINER_FRIEND "miner_friend" +///key that holds the missile target +#define BB_MINEBOT_MISSILE_TARGET "minebot_missile_target" +///should we auto protect? +#define BB_MINEBOT_AUTO_DEFEND "minebot_auto_defend" +///should we repair drones? +#define BB_MINEBOT_REPAIR_DRONE "minebot_repair_drone" +///should we plant mines? +#define BB_MINEBOT_PLANT_MINES "minebot_plant_mines" //seedling keys /// the water can we will pick up diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index 40d49732efe..c4e32814a77 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -1083,6 +1083,12 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai ///Trait granted by janitor skillchip, allows communication with cleanbots #define TRAIT_CLEANBOT_WHISPERER "cleanbot_whisperer" +///Trait granted by the miner skillchip, allows communication with minebots +#define TRAIT_ROCK_STONER "rock_stoner" + +///Trait given by the regenerative shield component +#define TRAIT_REGEN_SHIELD "regen_shield" + /// Trait given when a mob is currently in invisimin mode #define TRAIT_INVISIMIN "invisimin" diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index e38b69a95ba..df52bea6a26 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -388,6 +388,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_RDS_SUPPRESSED" = TRAIT_RDS_SUPPRESSED, "TRAIT_REAGENT_SCANNER" = TRAIT_REAGENT_SCANNER, "TRAIT_RECENTLY_BLOCKED_MAGIC" = TRAIT_RECENTLY_BLOCKED_MAGIC, + "TRAIT_REGEN_SHIELD" = TRAIT_REGEN_SHIELD, "TRAIT_RELAYING_ATTACKER" = TRAIT_RELAYING_ATTACKER, "TRAIT_REMOTE_TASTING" = TRAIT_REMOTE_TASTING, "TRAIT_RESEARCH_SCANNER" = TRAIT_RESEARCH_SCANNER, @@ -403,6 +404,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_RIFT_FAILURE" = TRAIT_RIFT_FAILURE, "TRAIT_ROCK_EATER" = TRAIT_ROCK_EATER, "TRAIT_ROCK_METAMORPHIC" = TRAIT_ROCK_METAMORPHIC, + "TRAIT_ROCK_STONER" = TRAIT_ROCK_STONER, "TRAIT_ROD_SUPLEX" = TRAIT_ROD_SUPLEX, "TRAIT_SABRAGE_PRO" = TRAIT_SABRAGE_PRO, "TRAIT_SACRIFICED" = TRAIT_SACRIFICED, diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm index 07d2a17cc8d..172a31d4793 100644 --- a/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm @@ -64,3 +64,15 @@ . = ..() if (clear_failed_targets) controller.clear_blackboard_key(target_key) + +/datum/ai_behavior/run_away_from_target/run_and_shoot + clear_failed_targets = FALSE + +/datum/ai_behavior/run_away_from_target/run_and_shoot/perform(seconds_per_tick, datum/ai_controller/controller, target_key, hiding_location_key) + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + finish_action(controller, succeeded = FALSE, target_key = target_key, hiding_location_key = hiding_location_key) + var/mob/living/living_pawn = controller.pawn + living_pawn.RangedAttack(target) + return ..() + diff --git a/code/datums/components/pet_commands/pet_commands_basic.dm b/code/datums/components/pet_commands/pet_commands_basic.dm index 9941f83e8b1..5ff476a85b9 100644 --- a/code/datums/components/pet_commands/pet_commands_basic.dm +++ b/code/datums/components/pet_commands/pet_commands_basic.dm @@ -247,6 +247,8 @@ set_command_target(parent, victim) /datum/pet_command/protect_owner/proc/set_attacking_target(atom/source, mob/living/attacker) + SIGNAL_HANDLER + var/mob/living/basic/owner = weak_parent.resolve() if(isnull(owner)) return diff --git a/code/datums/components/regenerative_shield.dm b/code/datums/components/regenerative_shield.dm new file mode 100644 index 00000000000..5ecf6708203 --- /dev/null +++ b/code/datums/components/regenerative_shield.dm @@ -0,0 +1,91 @@ +#define SHIELD_FILTER "shield filter" + +/// gives the mobs a regenerative shield, it will tank hits for them and then need to recharge for a bit +/datum/component/regenerative_shield + ///number of hits we can tank + var/number_of_hits = 15 + ///the limit of the damage we can tank + var/damage_threshold + ///the overlay of the shield + var/list/shield_overlays = list() + ///how long before the shield can regenerate + var/regeneration_time + +/datum/component/regenerative_shield/Initialize(number_of_hits = 15, damage_threshold = 50, regeneration_time = 2 MINUTES, list/shield_overlays) + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + src.number_of_hits = number_of_hits + src.damage_threshold = damage_threshold + src.regeneration_time = regeneration_time + + var/atom/movable/living_parent = parent + for(var/type_path as anything in shield_overlays) + if(!ispath(type_path)) + continue + var/obj/effect/overlay/new_effect = new type_path() + living_parent.vis_contents += new_effect + apply_filter_effects(new_effect) + src.shield_overlays += new_effect + +/datum/component/regenerative_shield/RegisterWithParent() + . = ..() + ADD_TRAIT(parent, TRAIT_REGEN_SHIELD, REF(src)) + RegisterSignal(parent, COMSIG_LIVING_CHECK_BLOCK, PROC_REF(block_attack)) + +/datum/component/regenerative_shield/UnregisterFromParent() + var/atom/movable/living_parent = parent + for(var/obj/effect/overlay as anything in shield_overlays) + living_parent.vis_contents -= overlay + QDEL_LIST(shield_overlays) + UnregisterSignal(parent, COMSIG_LIVING_CHECK_BLOCK) + REMOVE_TRAIT(parent, TRAIT_REGEN_SHIELD, REF(src)) + return ..() + +/datum/component/regenerative_shield/proc/block_attack( + mob/living/source, + atom/hitby, + damage, + attack_text, + attack_type, + armour_penetration, + damage_type, + attack_flag, +) + SIGNAL_HANDLER + + if(damage <= 0 ||damage_type == STAMINA) + return NONE + + if(damage >= damage_threshold || number_of_hits <= 0) + return NONE + + playsound(get_turf(parent), 'sound/weapons/tap.ogg', 20) + new /obj/effect/temp_visual/guardian/phase/out(get_turf(parent)) + number_of_hits = max(0, number_of_hits - 1) + if(number_of_hits <= 0) + disable_shield() + return SUCCESSFUL_BLOCK + +/datum/component/regenerative_shield/proc/disable_shield() + addtimer(CALLBACK(src, PROC_REF(enable_shield)), regeneration_time) + for(var/obj/effect/my_effect as anything in shield_overlays) + animate(my_effect, alpha = 0, time = 3 SECONDS) + my_effect.remove_filter(SHIELD_FILTER) + playsound(parent, 'sound/mecha/mech_shield_drop.ogg', 20) + +/datum/component/regenerative_shield/proc/enable_shield() + number_of_hits = initial(number_of_hits) + for(var/obj/effect/my_effect as anything in shield_overlays) + animate(my_effect, alpha = 255, time = 3 SECONDS) + addtimer(CALLBACK(src, PROC_REF(apply_filter_effects), my_effect), 5 SECONDS) + playsound(parent, 'sound/mecha/mech_shield_raise.ogg', 20) + +/datum/component/regenerative_shield/proc/apply_filter_effects(obj/effect/new_effect) + if(isnull(new_effect)) + return + new_effect.add_filter(SHIELD_FILTER, 1, list("type" = "outline", "color" = "#b6e6f3", "alpha" = 0, "size" = 1)) + var/filter = new_effect.get_filter(SHIELD_FILTER) + animate(filter, alpha = 200, time = 0.5 SECONDS, loop = -1) + animate(alpha = 0, time = 0.5 SECONDS) + +#undef SHIELD_FILTER diff --git a/code/datums/elements/mob_access.dm b/code/datums/elements/mob_access.dm new file mode 100644 index 00000000000..04ef6ac9d59 --- /dev/null +++ b/code/datums/elements/mob_access.dm @@ -0,0 +1,29 @@ +///element given to mobs that have levels of access +/datum/element/mob_access + element_flags = ELEMENT_BESPOKE + argument_hash_start_idx = 2 + /// What can this mob access? + var/list/my_access + +/datum/element/mob_access/Attach(datum/target, list/accesses) + . = ..() + if(!isliving(target)) + return ELEMENT_INCOMPATIBLE + for(var/access_path in accesses) + if(!ispath(access_path)) + continue + var/datum/id_trim/job/trim = SSid_access.trim_singletons_by_path[access_path] + if(isnull(trim)) + continue + my_access += trim.access + + RegisterSignal(target, COMSIG_MOB_TRIED_ACCESS, PROC_REF(attempt_access)) + +/datum/element/mob_access/proc/attempt_access(datum/source, obj/door_attempt) + SIGNAL_HANDLER + + return (door_attempt.check_access_list(my_access)) ? ACCESS_ALLOWED : ACCESS_DISALLOWED + +/datum/element/mob_access/Detach(datum/source, ...) + UnregisterSignal(source, COMSIG_MOB_TRIED_ACCESS) + return ..() diff --git a/code/game/machinery/computer/orders/order_items/mining/order_toys.dm b/code/game/machinery/computer/orders/order_items/mining/order_toys.dm index 5ce92f7b84f..7ee4c6cfba8 100644 --- a/code/game/machinery/computer/orders/order_items/mining/order_toys.dm +++ b/code/game/machinery/computer/orders/order_items/mining/order_toys.dm @@ -21,6 +21,14 @@ item_path = /obj/item/mine_bot_upgrade/health cost_per_order = 350 +/datum/orderable_item/toys_drones/drone_shield + item_path = /obj/item/mine_bot_upgrade/regnerative_shield + cost_per_order = 500 + +/datum/orderable_item/toys_drones/drone_shield + item_path = /obj/item/minebot_remote_control + cost_per_order = 500 + /datum/orderable_item/toys_drones/drone_pka item_path = /obj/item/borg/upgrade/modkit/cooldown/minebot cost_per_order = 525 diff --git a/code/modules/jobs/job_types/shaft_miner.dm b/code/modules/jobs/job_types/shaft_miner.dm index ca4ed9c8580..8a9e4a284bb 100644 --- a/code/modules/jobs/job_types/shaft_miner.dm +++ b/code/modules/jobs/job_types/shaft_miner.dm @@ -35,6 +35,7 @@ id_trim = /datum/id_trim/job/shaft_miner uniform = /obj/item/clothing/under/rank/cargo/miner/lavaland + skillchips = list(/obj/item/skillchip/job/miner) backpack_contents = list( /obj/item/flashlight/seclite = 1, /obj/item/knife/combat/survival = 1, diff --git a/code/modules/library/skill_learning/job_skillchips/miner.dm b/code/modules/library/skill_learning/job_skillchips/miner.dm new file mode 100644 index 00000000000..2970e5b6085 --- /dev/null +++ b/code/modules/library/skill_learning/job_skillchips/miner.dm @@ -0,0 +1,8 @@ +/obj/item/skillchip/job/miner + name = "TUNN3L_R4T skillchip" + desc = "Gain control of minebots." + auto_traits = list(TRAIT_ROCK_STONER) + skill_name = "Battlebot enthusiast" + skill_description = "Lead minebots into war." + activate_message = span_notice("A newfound obsession with battlebots fosters within you.") + deactivate_message = span_notice("You finally get over your battlebots phase, now go get a job.") diff --git a/code/modules/mob/living/basic/minebots/minebot.dm b/code/modules/mob/living/basic/minebots/minebot.dm index 30f486e9416..d57097adcf4 100644 --- a/code/modules/mob/living/basic/minebots/minebot.dm +++ b/code/modules/mob/living/basic/minebots/minebot.dm @@ -31,13 +31,18 @@ light_on = FALSE combat_mode = FALSE ai_controller = /datum/ai_controller/basic_controller/minebot - ///the access card we use to access mining - var/obj/item/card/id/access_card ///the gun we use to kill var/obj/item/gun/energy/recharge/kinetic_accelerator/minebot/stored_gun + ///our normal overlay + var/mutable_appearance/neutral_overlay + ///our combat mode overlay + var/mutable_appearance/combat_overlay + ///our current color, if any + var/selected_color ///the commands our owner can give us - var/list/pet_commands = list( + var/static/list/pet_commands = list( /datum/pet_command/idle/minebot, + /datum/pet_command/protect_owner/minebot, /datum/pet_command/minebot_ability/light, /datum/pet_command/minebot_ability/dump, /datum/pet_command/automate_mining, @@ -45,17 +50,27 @@ /datum/pet_command/follow, /datum/pet_command/point_targeting/attack/minebot, ) + ///possible colors the bot can have + var/static/list/possible_colors= list( + "Default" = null, //default color state + "Blue" = "#70d5e7", + "Red" = "#ee7fb9", + "Green" = "#5fea94", + ) /mob/living/basic/mining_drone/Initialize(mapload) . = ..() - + neutral_overlay = mutable_appearance(icon = 'icons/mob/silicon/aibots.dmi', icon_state = "mining_drone_grey") + combat_overlay = mutable_appearance(icon = 'icons/mob/silicon/aibots.dmi', icon_state = "mining_drone_offense_grey") + AddComponent(/datum/component/obeys_commands, pet_commands) var/static/list/death_drops = list(/obj/effect/decal/cleanable/robot_debris/old) AddElement(/datum/element/death_drops, death_drops) add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_ASHSTORM_IMMUNE), INNATE_TRAIT) AddElement(/datum/element/footstep, FOOTSTEP_OBJ_ROBOT, 1, -6, sound_vary = TRUE) - AddComponent(/datum/component/tameable, food_types = list(/obj/item/stack/ore), tame_chance = 100, bonus_tame_chance = 5) var/static/list/innate_actions = list( + /datum/action/cooldown/mob_cooldown/missile_launcher = BB_MINEBOT_MISSILE_ABILITY, + /datum/action/cooldown/mob_cooldown/drop_landmine = BB_MINEBOT_LANDMINE_ABILITY, /datum/action/cooldown/mob_cooldown/minedrone/toggle_light = BB_MINEBOT_LIGHT_ABILITY, /datum/action/cooldown/mob_cooldown/minedrone/toggle_meson_vision = null, /datum/action/cooldown/mob_cooldown/minedrone/dump_ore = BB_MINEBOT_DUMP_ABILITY, @@ -66,10 +81,11 @@ stored_gun = new(src) var/obj/item/implant/radio/mining/comms = new(src) comms.implant(src) - access_card = new /obj/item/card/id/advanced/gold(src) - SSid_access.apply_trim_to_card(access_card, /datum/id_trim/job/shaft_miner) - - RegisterSignal(src, COMSIG_MOB_TRIED_ACCESS, PROC_REF(attempt_access)) + var/static/list/accesses = list( + /datum/id_trim/job/shaft_miner, + ) + AddElement(/datum/element/mob_access, accesses) + RegisterSignal(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(pre_attack)) /mob/living/basic/mining_drone/set_combat_mode(new_mode, silent = TRUE) . = ..() @@ -115,8 +131,82 @@ return ..() /mob/living/basic/mining_drone/attack_hand(mob/living/carbon/human/user, list/modifiers) + if(!user.combat_mode) + ui_interact(user) + return + return ..() + +/mob/living/basic/mining_drone/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "MineBot", name) + ui.open() + +/mob/living/basic/mining_drone/ui_data(mob/user) + var/list/data = list() + data["auto_defend"] = ai_controller.blackboard[BB_MINEBOT_AUTO_DEFEND] + data["repair_node_drone"] = ai_controller.blackboard[BB_MINEBOT_REPAIR_DRONE] + data["plant_mines"] = ai_controller.blackboard[BB_MINEBOT_PLANT_MINES] + data["bot_maintain_distance"] = ai_controller.blackboard[BB_MINIMUM_SHOOTING_DISTANCE] + data["bot_name"] = name + data["bot_mode"] = combat_mode + data["bot_health"] = health + data["bot_maxhealth"] = maxHealth + data["bot_color"] = "" + var/color_value = neutral_overlay.color + for(var/index in possible_colors) + if(possible_colors[index] == color_value) + data["bot_color"] = index + break + return data + +/mob/living/basic/mining_drone/ui_static_data(mob/user) + var/list/data = list() + data["bot_icon"] = icon2base64(getFlatIcon(src)) + data["possible_colors"] = list() + for(var/color in possible_colors) + data["possible_colors"] += list(list( + "color_name" = color, + "color_value" = possible_colors[color], + )) + return data + +/mob/living/basic/mining_drone/ui_act(action, params, datum/tgui/ui) + . = ..() + switch(action) + if("change_min_distance") + var/new_distance = clamp(params["distance"], 0, 5) + ai_controller.set_blackboard_key(BB_MINIMUM_SHOOTING_DISTANCE, new_distance) + if("toggle_defend") + var/new_toggle = !ai_controller.blackboard[BB_MINEBOT_AUTO_DEFEND] + ai_controller.set_blackboard_key(BB_MINEBOT_AUTO_DEFEND, new_toggle) + if("toggle_repair") + var/new_defend = !ai_controller.blackboard[BB_MINEBOT_REPAIR_DRONE] + ai_controller.set_blackboard_key(BB_MINEBOT_REPAIR_DRONE, new_defend) + if("toggle_mines") + var/new_mines = !ai_controller.blackboard[BB_MINEBOT_PLANT_MINES] + ai_controller.set_blackboard_key(BB_MINEBOT_PLANT_MINES, new_mines) + if("set_name") + var/input_name = sanitize_name(params["chosen_name"], allow_numbers = TRUE) + name = (input_name ? input_name : initial(name)) + if("toggle_mode") + set_combat_mode(!combat_mode) + if("set_color") + change_color(params["chosen_color"]) + update_static_data(ui.user, ui) + return TRUE + +/mob/living/basic/mining_drone/proc/change_color(new_color) + selected_color = new_color + if(!isnull(selected_color)) + neutral_overlay.color = selected_color + combat_overlay.color = selected_color + update_appearance() + +/mob/living/basic/mining_drone/AltClick(mob/living/user) + . = ..() if(user.combat_mode) - return ..() + return set_combat_mode(!combat_mode) balloon_alert(user, "now [combat_mode ? "attacking wildlife" : "collecting loose ore"]") @@ -125,7 +215,6 @@ return stored_gun.afterattack(target, src) - /mob/living/basic/mining_drone/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers) . = ..() @@ -141,16 +230,6 @@ for(var/obj/item/stack/ore/dropped_item in contents) dropped_item.forceMove(get_turf(src)) -/mob/living/basic/mining_drone/proc/attempt_access(mob/drone, obj/door_attempt) - SIGNAL_HANDLER - - if(door_attempt.check_access(access_card)) - return ACCESS_ALLOWED - return ACCESS_DISALLOWED - -/mob/living/basic/mining_drone/tamed(mob/living/tamer, atom/food) - AddComponent(/datum/component/obeys_commands, pet_commands) - /mob/living/basic/mining_drone/death(gibbed) drop_ore() @@ -164,6 +243,25 @@ /mob/living/basic/mining_drone/Destroy() QDEL_NULL(stored_gun) - QDEL_NULL(access_card) return ..() +/mob/living/basic/mining_drone/proc/pre_attack(datum/source, atom/target) + SIGNAL_HANDLER + + if(!istype(target, /mob/living/basic/node_drone)) + return NONE + INVOKE_ASYNC(src, PROC_REF(repair_node_drone), target) + return COMPONENT_HOSTILE_NO_ATTACK + +/mob/living/basic/mining_drone/proc/repair_node_drone(mob/living/my_target) + do_sparks(5, FALSE, source = my_target) + if(!do_after(src, 6 SECONDS, my_target)) + return + my_target.heal_overall_damage(brute = 50) + +/mob/living/basic/mining_drone/update_overlays() + . = ..() + if(stat == DEAD || isnull(selected_color)) + return + + . += combat_mode ? combat_overlay : neutral_overlay diff --git a/code/modules/mob/living/basic/minebots/minebot_abilities.dm b/code/modules/mob/living/basic/minebots/minebot_abilities.dm index 4f119fd9b66..88cd156d1b1 100644 --- a/code/modules/mob/living/basic/minebots/minebot_abilities.dm +++ b/code/modules/mob/living/basic/minebots/minebot_abilities.dm @@ -49,3 +49,131 @@ to_chat(owner, span_notice("You toggle your meson vision [(owner.sight & SEE_TURFS) ? "on" : "off"].")) +/datum/action/cooldown/mob_cooldown/missile_launcher + name = "Launch Missile" + button_icon = 'icons/obj/weapons/guns/projectiles.dmi' + button_icon_state = "84mm-heap" + background_icon_state = "bg_default" + overlay_icon_state = "bg_default_border" + desc = "Launch a missile towards the target!" + cooldown_time = 10 SECONDS + shared_cooldown = NONE + melee_cooldown_time = 0 SECONDS + ///how long before we launch said missile + var/wind_up_timer = 1 SECONDS + +/datum/action/cooldown/mob_cooldown/missile_launcher/IsAvailable(feedback = TRUE) + if(is_mining_level(owner.z)) + return TRUE + if(feedback) + owner.balloon_alert(owner, "cant be used here!") + return FALSE + +/datum/action/cooldown/mob_cooldown/missile_launcher/Activate(atom/target) + var/turf/target_turf = get_turf(target) + if(isnull(target_turf) || !can_see(owner, target_turf, 7)) + return FALSE + owner.Shake(duration = wind_up_timer) + addtimer(CALLBACK(src, PROC_REF(launch_missile), target_turf), wind_up_timer) + StartCooldown() + return TRUE + +/datum/action/cooldown/mob_cooldown/missile_launcher/proc/launch_missile(turf/target_turf) + new /obj/effect/temp_visual/mook_dust(get_turf(owner)) + var/obj/effect/temp_visual/rising_rocket/new_rocket = new(get_turf(owner)) + addtimer(CALLBACK(src, PROC_REF(drop_missile), target_turf), new_rocket.duration) + +/datum/action/cooldown/mob_cooldown/missile_launcher/proc/drop_missile(turf/target_turf) + new /obj/effect/temp_visual/falling_rocket(target_turf) + var/obj/effect/temp_visual/falling_shadow = new /obj/effect/temp_visual/shadow_telegraph(target_turf) + animate(falling_shadow, transform = matrix().Scale(0.1, 0.1), time = falling_shadow.duration) + +/datum/action/cooldown/mob_cooldown/drop_landmine + name = "Landmine" + desc = "Drop a landmine!" + button_icon = 'icons/obj/weapons/grenade.dmi' + button_icon_state = "landmine" + background_icon_state = "bg_default" + overlay_icon_state = "bg_default_border" + cooldown_time = 10 SECONDS + shared_cooldown = NONE + melee_cooldown_time = 0 SECONDS + click_to_activate = FALSE + +/datum/action/cooldown/mob_cooldown/drop_landmine/IsAvailable(feedback = TRUE) + if(is_mining_level(owner.z)) + return TRUE + if(feedback) + owner.balloon_alert(owner, "cant be used here!") + return FALSE + +/datum/action/cooldown/mob_cooldown/drop_landmine/Activate(atom/target) + var/turf/my_turf = get_turf(owner) + if(isgroundlessturf(my_turf)) + return FALSE + var/obj/effect/mine/minebot/my_mine = new(my_turf) + my_mine.ignore_list = owner.faction.Copy() + playsound(my_turf, 'sound/weapons/armbomb.ogg', 20) + StartCooldown() + return TRUE + +/obj/effect/temp_visual/rising_rocket + name = "Missile" + icon = 'icons/obj/weapons/guns/projectiles.dmi' + icon_state = "84mm-heap" + layer = ABOVE_ALL_MOB_LAYER + duration = 2 SECONDS + +/obj/effect/temp_visual/rising_rocket/Initialize(mapload) + . = ..() + playsound(src, 'sound/weapons/minebot_rocket.ogg', 100, FALSE) + animate(src, pixel_y = base_pixel_y + 500, time = duration, easing = EASE_IN) + +/obj/effect/temp_visual/falling_rocket + name = "Missile" + icon = 'icons/obj/weapons/guns/projectiles.dmi' + icon_state = "84mm-heap" + layer = ABOVE_ALL_MOB_LAYER + duration = 0.7 SECONDS + pixel_y = 60 + ///the radius of our explosion + var/explosion_radius = 2 + ///damage of our explosion + var/explosion_damage = 100 + +/obj/effect/temp_visual/falling_rocket/Initialize(mapload) + . = ..() + transform = transform.Turn(180) + addtimer(CALLBACK(src, PROC_REF(create_explosion)), duration) + animate(src, pixel_y = 0, time = duration) + +/obj/effect/temp_visual/falling_rocket/proc/create_explosion() + playsound(src, 'sound/weapons/minebot_rocket.ogg', 100, FALSE) + var/datum/effect_system/fluid_spread/smoke/smoke = new + smoke.set_up(1, holder = src) + smoke.start() + for(var/mob/living/living_target in oview(explosion_radius, src)) + if(living_target.incorporeal_move) + continue + living_target.apply_damage(explosion_damage) + +/obj/effect/mine/minebot + name = "Landmine" + ///we dont detonate if any of these people step on us + var/list/ignore_list = list() + ///the damage we apply to whoever steps on us + var/damage_to_apply = 50 + +/obj/effect/mine/minebot/mineEffect(mob/living/victim) + if(!istype(victim)) + return + var/datum/effect_system/fluid_spread/smoke/smoke = new + smoke.set_up(0, holder = src) + smoke.start() + playsound(src, 'sound/effects/explosion3.ogg', 100) + victim.apply_damage(damage_to_apply) + +/obj/effect/mine/minebot/can_trigger(atom/movable/on_who) + if(REF(on_who) in ignore_list) + return FALSE + return ..() diff --git a/code/modules/mob/living/basic/minebots/minebot_ai.dm b/code/modules/mob/living/basic/minebots/minebot_ai.dm index 4aba93c4265..2e585085a10 100644 --- a/code/modules/mob/living/basic/minebots/minebot_ai.dm +++ b/code/modules/mob/living/basic/minebots/minebot_ai.dm @@ -2,6 +2,11 @@ blackboard = list( BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends, + BB_BASIC_MOB_FLEE_DISTANCE = 3, + BB_MINIMUM_SHOOTING_DISTANCE = 3, + BB_MINEBOT_PLANT_MINES = TRUE, + BB_MINEBOT_REPAIR_DRONE = TRUE, + BB_MINEBOT_AUTO_DEFEND = TRUE, BB_BLACKLIST_MINERAL_TURFS = list(/turf/closed/mineral/gibtonite), BB_AUTOMATED_MINING = FALSE, ) @@ -11,11 +16,90 @@ planning_subtrees = list( /datum/ai_planning_subtree/simple_find_target, /datum/ai_planning_subtree/pet_planning, + /datum/ai_planning_subtree/befriend_miners, + /datum/ai_planning_subtree/defend_node, + /datum/ai_planning_subtree/launch_missiles, + /datum/ai_planning_subtree/minebot_maintain_distance, /datum/ai_planning_subtree/basic_ranged_attack_subtree/minebot, /datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/minebot, /datum/ai_planning_subtree/minebot_mining, /datum/ai_planning_subtree/locate_dead_humans, ) + ai_traits = PAUSE_DURING_DO_AFTER + +/datum/ai_planning_subtree/launch_missiles + +/datum/ai_planning_subtree/launch_missiles/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/datum/action/cooldown/missile_ability = controller.blackboard[BB_MINEBOT_MISSILE_ABILITY] + if(!missile_ability?.IsAvailable()) + return + if(!controller.blackboard_key_exists(BB_MINEBOT_MISSILE_TARGET)) + controller.queue_behavior(/datum/ai_behavior/find_and_set/clear_bombing_zone, BB_MINEBOT_MISSILE_TARGET, /obj/effect/temp_visual/minebot_target, 7) + return + controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/and_clear_target, BB_MINEBOT_MISSILE_ABILITY, BB_MINEBOT_MISSILE_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/find_and_set/clear_bombing_zone + +/datum/ai_behavior/find_and_set/clear_bombing_zone/search_tactic(datum/ai_controller/controller, locate_path, search_range) + for(var/obj/effect/temp_visual/minebot_target/target in oview(search_range, controller.pawn)) + if(isclosedturf(get_turf(target))) + continue + return target + return null + +/datum/ai_planning_subtree/befriend_miners/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + if(!controller.blackboard_key_exists(BB_MINER_FRIEND)) + controller.queue_behavior(/datum/ai_behavior/find_and_set/miner_to_befriend, BB_MINER_FRIEND) + return + controller.queue_behavior(/datum/ai_behavior/befriend_target, BB_MINER_FRIEND) + +/datum/ai_behavior/find_and_set/miner_to_befriend + +/datum/ai_behavior/find_and_set/miner_to_befriend/search_tactic(datum/ai_controller/controller, locate_path, search_range) + for(var/mob/living/carbon/human/target in oview(search_range, controller.pawn)) + if(HAS_TRAIT(target, TRAIT_ROCK_STONER)) + return target + return null + +/datum/ai_planning_subtree/defend_node/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/target = controller.blackboard[BB_DRONE_DEFEND] + if(QDELETED(target)) + controller.queue_behavior(/datum/ai_behavior/find_and_set, BB_DRONE_DEFEND, /mob/living/basic/node_drone) + return + var/mob/living/living_pawn = controller.pawn + if(!living_pawn.faction.Find(REF(target))) + controller.queue_behavior(/datum/ai_behavior/befriend_target, BB_DRONE_DEFEND) + return + if(target.health < (target.maxHealth * 0.75) && controller.blackboard[BB_MINEBOT_REPAIR_DRONE]) + controller.queue_behavior(/datum/ai_behavior/repair_drone, BB_DRONE_DEFEND) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/repair_drone + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION | AI_BEHAVIOR_REQUIRE_REACH + +/datum/ai_behavior/repair_drone/setup(datum/ai_controller/controller, target_key) + . = ..() + var/turf/target = controller.blackboard[target_key] + if(isnull(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/repair_drone/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + finish_action(controller, FALSE, target_key) + return + var/mob/living/living_pawn = controller.pawn + living_pawn.say("REPAIRING [target]!") + living_pawn.UnarmedAttack(target) + finish_action(controller, TRUE, target_key) + +/datum/ai_behavior/repair_drone/finish_action(datum/ai_controller/controller, success, target_key) + . = ..() + if(!success) + controller.clear_blackboard_key(target_key) ///find dead humans and report their location on the radio /datum/ai_planning_subtree/locate_dead_humans/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) @@ -28,6 +112,7 @@ for(var/mob/living/carbon/human/target in oview(search_range, controller.pawn)) if(target.stat >= UNCONSCIOUS && target.mind) return target + return null /datum/ai_behavior/send_sos_message behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION @@ -58,16 +143,51 @@ operational_datums = null ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack/minebot -/datum/ai_behavior/basic_ranged_attack/minebot - behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT - avoid_friendly_fire = TRUE - /datum/ai_planning_subtree/basic_ranged_attack_subtree/minebot/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(QDELETED(target)) + return var/mob/living/living_pawn = controller.pawn if(!living_pawn.combat_mode) //we are not on attack mode return + controller.queue_behavior(ranged_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_planning_subtree/minebot_maintain_distance/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(QDELETED(target)) + return + var/mob/living/living_pawn = controller.pawn + if(get_dist(living_pawn, target) <= controller.blackboard[BB_MINIMUM_SHOOTING_DISTANCE]) + controller.queue_behavior(/datum/ai_behavior/run_away_from_target/run_and_shoot/minebot, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/run_away_from_target/run_and_shoot/minebot + +/datum/ai_behavior/run_away_from_target/run_and_shoot/minebot/perform(seconds_per_tick, datum/ai_controller/controller, target_key, hiding_location_key) + if(!controller.blackboard[BB_MINEBOT_PLANT_MINES]) + return ..() + var/datum/action/cooldown/mine_ability = controller.blackboard[BB_MINEBOT_LANDMINE_ABILITY] + mine_ability?.Trigger() return ..() +/datum/ai_behavior/basic_ranged_attack/minebot + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + avoid_friendly_fire = TRUE + ///if our target is closer than this distance, finish action + var/minimum_distance = 3 + +/datum/ai_behavior/basic_ranged_attack/minebot/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) + . = ..() + minimum_distance = controller.blackboard[BB_MINIMUM_SHOOTING_DISTANCE] ? controller.blackboard[BB_MINIMUM_SHOOTING_DISTANCE] : initial(minimum_distance) + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + finish_action(controller, target_key, FALSE) + return + var/mob/living/living_pawn = controller.pawn + if(get_dist(living_pawn, target) <= minimum_distance) + finish_action(controller, target_key, TRUE) + ///mine walls if we are on automated mining mode /datum/ai_planning_subtree/minebot_mining/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) if(!controller.blackboard[BB_AUTOMATED_MINING]) @@ -216,3 +336,19 @@ /datum/pet_command/idle/minebot/execute_action(datum/ai_controller/controller) controller.set_blackboard_key(BB_AUTOMATED_MINING, FALSE) return ..() + +/datum/pet_command/protect_owner/minebot + +/datum/pet_command/protect_owner/minebot/set_command_target(mob/living/parent, atom/target) + if(!parent.ai_controller.blackboard[BB_MINEBOT_AUTO_DEFEND]) + return + if(!parent.ai_controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET) && !QDELETED(target)) //we are already dealing with something, + parent.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, target) + +/datum/pet_command/protect_owner/minebot/execute_action(datum/ai_controller/controller) + if(controller.blackboard[BB_MINEBOT_AUTO_DEFEND]) + var/mob/living/living_pawn = controller.pawn + living_pawn.set_combat_mode(TRUE) + controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND) + + diff --git a/code/modules/mob/living/basic/minebots/minebot_remote_control.dm b/code/modules/mob/living/basic/minebots/minebot_remote_control.dm new file mode 100644 index 00000000000..618edc3d0b6 --- /dev/null +++ b/code/modules/mob/living/basic/minebots/minebot_remote_control.dm @@ -0,0 +1,72 @@ +#define BOMB_COOLDOWN 20 SECONDS +/obj/item/minebot_remote_control + name = "Remote Control" + desc = "Requesting stratagem!" + icon = 'icons/obj/mining.dmi' + icon_state = "minebot_bomb_control" + item_flags = NOBLUDGEON + ///are we currently primed to drop a bomb? + var/primed = FALSE + ///our last user + var/datum/weakref/last_user + ///cooldown till we can drop the next bomb + COOLDOWN_DECLARE(bomb_timer) + +/obj/item/minebot_remote_control/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE) + . = ..() + clear_priming() + +/obj/item/minebot_remote_control/proc/clear_priming() + var/mob/living/living_user = last_user?.resolve() + last_user = null + primed = FALSE + if(isnull(living_user)) + return + living_user.client?.mouse_override_icon = initial(living_user.client?.mouse_override_icon) + living_user.update_mouse_pointer() + +/obj/item/minebot_remote_control/attack_self(mob/user) + . = ..() + if(.) + return . + + if(!COOLDOWN_FINISHED(src, bomb_timer)) + balloon_alert(user, "on cooldown!") + return TRUE + + prime_bomb(user) + return TRUE + +/obj/item/minebot_remote_control/proc/prime_bomb(mob/user) + primed = TRUE + last_user = WEAKREF(user) + user.client?.mouse_override_icon = 'icons/effects/mouse_pointers/weapon_pointer.dmi' + user.update_mouse_pointer() + +/obj/item/minebot_remote_control/afterattack(atom/attacked_atom, mob/living/user, proximity) + . = ..() + + . |= AFTERATTACK_PROCESSED_ITEM + + if(!primed) + user.balloon_alert(user, "not primed!") + return + var/turf/target_turf = get_turf(attacked_atom) + if(isnull(target_turf) || isclosedturf(target_turf) || isgroundlessturf(target_turf)) + user.balloon_alert(user, "invalid target!") + return + playsound(src, 'sound/machines/beep.ogg', 30) + clear_priming() + new /obj/effect/temp_visual/minebot_target(target_turf) + COOLDOWN_START(src, bomb_timer, BOMB_COOLDOWN) + +/obj/effect/temp_visual/minebot_target + name = "Rocket Target" + icon = 'icons/mob/actions/actions_items.dmi' + icon_state = "sniper_zoom" + layer = BELOW_MOB_LAYER + plane = GAME_PLANE + light_range = 2 + duration = 5 SECONDS + +#undef BOMB_COOLDOWN diff --git a/code/modules/mob/living/basic/minebots/minebot_upgrades.dm b/code/modules/mob/living/basic/minebots/minebot_upgrades.dm index d68e3a85081..d1800072b5d 100644 --- a/code/modules/mob/living/basic/minebots/minebot_upgrades.dm +++ b/code/modules/mob/living/basic/minebots/minebot_upgrades.dm @@ -3,6 +3,7 @@ desc = "A minebot upgrade." icon_state = "door_electronics" icon = 'icons/obj/devices/circuitry_n_data.dmi' + item_flags = NOBLUDGEON /obj/item/mine_bot_upgrade/afterattack(mob/living/basic/mining_drone/minebot, mob/user, proximity) . = ..() @@ -58,3 +59,35 @@ minebot.melee_damage_lower = initial(minebot.melee_damage_lower) + base_damage_add minebot.melee_damage_upper = initial(minebot.melee_damage_upper) + base_damage_add minebot.stored_gun?.recharge_time += base_cooldown_add + +/obj/item/mine_bot_upgrade/regnerative_shield + name = "regenerative shield" + desc = "Allows your minebot to tank many hits before going down!" + +/obj/item/mine_bot_upgrade/regnerative_shield/upgrade_bot(mob/living/basic/mining_drone/minebot, mob/user) + if(HAS_TRAIT(minebot, TRAIT_REGEN_SHIELD)) + user.balloon_alert(minebot, "already has it!") + return + var/static/list/shield_layers = list( + /obj/effect/overlay/minebot_top_shield, + /obj/effect/overlay/minebot_bottom_shield + ) + minebot.AddComponent(/datum/component/regenerative_shield, shield_overlays = shield_layers) + qdel(src) + +/obj/effect/overlay/minebot_top_shield + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + anchored = TRUE + vis_flags = VIS_INHERIT_DIR + icon = 'icons/mob/silicon/aibots.dmi' + icon_state = "minebot_shield_top_layer" + layer = ABOVE_ALL_MOB_LAYER + +/obj/effect/overlay/minebot_bottom_shield + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + anchored = TRUE + vis_flags = VIS_INHERIT_DIR + icon = 'icons/mob/silicon/aibots.dmi' + icon_state = "minebot_shield_bottom_layer" + layer = BELOW_MOB_LAYER + diff --git a/icons/mob/silicon/aibots.dmi b/icons/mob/silicon/aibots.dmi index 8949caa0299..85f4e350589 100644 Binary files a/icons/mob/silicon/aibots.dmi and b/icons/mob/silicon/aibots.dmi differ diff --git a/icons/obj/mining.dmi b/icons/obj/mining.dmi index cdc886d85ac..d81a6d966c3 100644 Binary files a/icons/obj/mining.dmi and b/icons/obj/mining.dmi differ diff --git a/sound/weapons/minebot_rocket.ogg b/sound/weapons/minebot_rocket.ogg new file mode 100644 index 00000000000..f8749a15e18 Binary files /dev/null and b/sound/weapons/minebot_rocket.ogg differ diff --git a/tgstation.dme b/tgstation.dme index 32d92632a65..69ec30eb9be 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1243,6 +1243,7 @@ #include "code\datums\components\recharging_attacks.dm" #include "code\datums\components\redirect_attack_hand_from_turf.dm" #include "code\datums\components\reflection.dm" +#include "code\datums\components\regenerative_shield.dm" #include "code\datums\components\regenerator.dm" #include "code\datums\components\religious_tool.dm" #include "code\datums\components\rename.dm" @@ -1531,6 +1532,7 @@ #include "code\datums\elements\light_eater.dm" #include "code\datums\elements\loomable.dm" #include "code\datums\elements\mirage_border.dm" +#include "code\datums\elements\mob_access.dm" #include "code\datums\elements\mob_grabber.dm" #include "code\datums\elements\mob_killed_tally.dm" #include "code\datums\elements\move_force_on_death.dm" @@ -4425,6 +4427,7 @@ #include "code\modules\library\skill_learning\job_skillchips\_job.dm" #include "code\modules\library\skill_learning\job_skillchips\chef.dm" #include "code\modules\library\skill_learning\job_skillchips\janitor.dm" +#include "code\modules\library\skill_learning\job_skillchips\miner.dm" #include "code\modules\library\skill_learning\job_skillchips\psychologist.dm" #include "code\modules\library\skill_learning\job_skillchips\research_director.dm" #include "code\modules\library\skill_learning\job_skillchips\roboticist.dm" @@ -4800,6 +4803,7 @@ #include "code\modules\mob\living\basic\minebots\minebot.dm" #include "code\modules\mob\living\basic\minebots\minebot_abilities.dm" #include "code\modules\mob\living\basic\minebots\minebot_ai.dm" +#include "code\modules\mob\living\basic\minebots\minebot_remote_control.dm" #include "code\modules\mob\living\basic\minebots\minebot_upgrades.dm" #include "code\modules\mob\living\basic\pets\fox.dm" #include "code\modules\mob\living\basic\pets\penguin.dm" diff --git a/tgui/packages/tgui/interfaces/MineBot.tsx b/tgui/packages/tgui/interfaces/MineBot.tsx new file mode 100644 index 00000000000..4a3087cf8d6 --- /dev/null +++ b/tgui/packages/tgui/interfaces/MineBot.tsx @@ -0,0 +1,197 @@ +import { BooleanLike } from 'common/react'; +import { useState } from 'react'; + +import { useBackend } from '../backend'; +import { + Button, + Dropdown, + Image, + LabeledList, + NumberInput, + ProgressBar, + Section, + Stack, +} from '../components'; +import { Window } from '../layouts'; + +type Data = { + auto_defend: BooleanLike; + repair_node_drone: BooleanLike; + plant_mines: BooleanLike; + bot_mode: BooleanLike; + bot_name: string; + bot_health: number; + bot_maintain_distance: number; + bot_maxhealth: number; + bot_icon: string; + bot_color: string; + possible_colors: Possible_Colors[]; +}; + +type Possible_Colors = { + color_name: string; + color_value: string; +}; + +export const MineBot = (props) => { + const { act, data } = useBackend(); + const { + auto_defend, + repair_node_drone, + plant_mines, + bot_name, + bot_health, + bot_mode, + bot_maxhealth, + possible_colors, + bot_maintain_distance, + bot_color, + bot_icon, + } = data; + const possibleColorList = {}; + for (const index in possible_colors) { + const color = possible_colors[index]; + possibleColorList[color.color_name] = color; + } + const [selectedDistance, setSelectedDistance] = useState( + bot_maintain_distance, + ); + const [selectedColor, setSelectedColor] = useState( + possibleColorList[bot_color], + ); + return ( + + + + +
+ act('set_name', { + chosen_name: value, + }) + } + > + Rename + + } + > + + + + + + { + return possible_color.color_name; + })} + onSelected={(selected) => + setSelectedColor(possibleColorList[selected]) + } + /> + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + act('change_min_distance', { + distance: value, + }) + } + /> + + +
+
+
+
+
+ ); +};