minebot buff (#82001)

## About The Pull Request
this pr buffs non-sentient minebots a bit to make them more helpful with
the new arcmining changes.
Minebots now have a better overall AI, they will maintain distance from
enemies and shoot while running. they will also plant landmines while
theyre running away from enemies. these landmines are carefully
programmed by the bot not to trigger when any of its miner friends step
on it. u no longer need to feed minebots an ore to get them to listen to
you, as they now automatically listen to any miners around.
minebots can now repair damaged node drones 

![image](https://github.com/tgstation/tgstation/assets/138636438/12e67eb2-3711-465c-a3ac-54fdadbed5e4)
they also have a new autodefend feature, which makes them automatically
attack any mob that attacks its miner friends or the drone. They also
have some new upgrades!
First is the regenerative shield, this shield allows minebots to tank a
limited amount of hits before breaking. minebots will then need to wait
sometime before the shield re-activates.
Second is the rocket launcher remote control, this allows players to
direct minebots to fire anti-fauna missiles at their target

https://github.com/tgstation/tgstation/assets/138636438/3ec3605e-8e11-4a31-acaa-1382bed98294

Also minebots are now highly customizable, you can rename them, change
their colors, or program their AI through their new user interface

![image](https://github.com/tgstation/tgstation/assets/138636438/d2e1c39d-f9d2-4da7-a5fa-5a41cea31d6e)


## Why It's Good For The Game
Improves minebot AI a bit, and makes it a more viable option for mining
solo players

## Changelog
🆑
balance: minebots have been buffed and have recieved new upgrades
/🆑
This commit is contained in:
Ben10Omintrix
2024-03-30 06:26:35 +02:00
committed by GitHub
parent edc0f54a59
commit 55267e1334
20 changed files with 871 additions and 26 deletions

View File

@@ -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

View File

@@ -1080,6 +1080,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"

View File

@@ -386,6 +386,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,
@@ -401,6 +402,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_SECURITY_HUD" = TRAIT_SECURITY_HUD,

View File

@@ -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 ..()

View File

@@ -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

View File

@@ -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

View File

@@ -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 ..()

View File

@@ -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

View File

@@ -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,

View File

@@ -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.")

View File

@@ -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

View File

@@ -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 ..()

View File

@@ -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)

View File

@@ -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

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

View File

@@ -1153,6 +1153,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"
@@ -1442,6 +1443,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"
@@ -4337,6 +4339,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"
@@ -4717,6 +4720,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"

View File

@@ -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<Data>();
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 (
<Window title="Minebot Settings" width={625} height={328} theme="hackerman">
<Window.Content>
<Stack>
<Stack.Item width="50%">
<Section
textAlign="center"
title={bot_name}
buttons={
<Button.Input
color="transparent"
onCommit={(e, value) =>
act('set_name', {
chosen_name: value,
})
}
>
Rename
</Button.Input>
}
>
<Stack vertical>
<Stack.Item>
<Image
m={1}
src={`data:image/jpeg;base64,${bot_icon}`}
height="160px"
width="160px"
style={{
verticalAlign: 'middle',
borderRadius: '1em',
border: '1px solid green',
}}
/>
</Stack.Item>
<Stack.Item ml="25%">
<Dropdown
width="65%"
displayText={selectedColor?.color_name}
options={possible_colors.map((possible_color) => {
return possible_color.color_name;
})}
onSelected={(selected) =>
setSelectedColor(possibleColorList[selected])
}
/>
</Stack.Item>
<Stack.Item textAlign="center">
<Button
textAlign="center"
width="50%"
style={{ padding: '3px' }}
onClick={() =>
act('set_color', {
chosen_color: selectedColor?.color_value,
})
}
>
Apply Color
</Button>
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
<Stack.Item width="50%" textAlign="center">
<Section title="Configurations">
<LabeledList>
<LabeledList.Item label="Health">
<ProgressBar
value={bot_health}
maxValue={bot_maxhealth}
color="white"
/>
</LabeledList.Item>
<LabeledList.Item label="Mode">
<Button
textAlign="center"
width="50%"
style={{ padding: '3px' }}
onClick={() => act('toggle_mode')}
>
{bot_mode ? 'Combat' : 'Safe'}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Repair Node Drones">
<Button
textAlign="center"
width="50%"
style={{ padding: '3px' }}
onClick={() => act('toggle_repair')}
>
{repair_node_drone ? 'Repair' : 'Ignore'}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Plant Mines">
<Button
textAlign="center"
width="50%"
style={{ padding: '3px' }}
onClick={() => act('toggle_mines')}
>
{plant_mines ? 'On' : 'Off'}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Auto protect">
<Button
textAlign="center"
width="50%"
style={{ padding: '3px' }}
onClick={() => act('toggle_defend')}
>
{auto_defend ? 'On' : 'Off'}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Distance To Maintain">
<NumberInput
width="50%"
value={selectedDistance}
minValue={0}
step={1}
maxValue={5}
onChange={(value) =>
act('change_min_distance', {
distance: value,
})
}
/>
</LabeledList.Item>
</LabeledList>
</Section>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};