Files
CabinetOnFire 2ede6af8bc Fix basic mobs triggering click cooldown erroneously. (#95906)
## About The Pull Request

Basic Mobs used to always trigger cooldown on clicks. This resulted in
missed attacks causing you to have a delay before you could attack
again, this makes it really punishing to play as a basic mob.

To resolve this I did a mini-refactor on basic mob's attack chain to
allow for the return values to determine whether we go on cooldown or
not. Preventing attacks that did nothing (due to not passing checks, or
simply not having any behavior) from causing cooldown.

This fixes #95605

## Why It's Good For The Game

being able to perform melee with the same rules as /human is only fair

## Changelog

🆑 DresserOnFire
fix: Fixes a bug where player-controlled basic mobs would get a cooldown
when their attacks miss
refactor: basic mob attack chain can now decided whether an attack
resulted in a cooldown or not.
/🆑
2026-04-29 21:59:01 +01:00

285 lines
10 KiB
Plaintext

/mob/living/basic/mining_drone
name = "\improper Nanotrasen minebot"
desc = "The instructions printed on the side read: This is a small robot used to support miners, can be set to search and collect loose ore, or to help fend off wildlife."
gender = NEUTER
icon = 'icons/mob/silicon/aibots.dmi'
icon_state = "mining_drone"
icon_living = "mining_drone"
basic_mob_flags = DEL_ON_DEATH
status_flags = CANSTUN|CANKNOCKDOWN|CANPUSH
mouse_opacity = MOUSE_OPACITY_ICON
combat_mode = TRUE
habitable_atmos = null
minimum_survivable_temperature = 0
health = 125
maxHealth = 125
melee_damage_lower = 15
melee_damage_upper = 15
obj_damage = 10
attack_verb_continuous = "drills"
attack_verb_simple = "drill"
attack_sound = 'sound/items/weapons/circsawhit.ogg'
sentience_type = SENTIENCE_MINEBOT
speak_emote = list("states")
mob_biotypes = MOB_ROBOTIC
faction = list(FACTION_STATION, FACTION_NEUTRAL)
death_message = "blows apart!"
light_system = OVERLAY_LIGHT
light_range = 6
// I want this to be a bit more dim, for vibes
light_power = 0.6
light_color = "#ff9933"
light_on = FALSE
combat_mode = FALSE
ai_controller = /datum/ai_controller/basic_controller/minebot
damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, STAMINA = 0, OXY = 1)
///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/static/list/pet_commands = list(
/datum/pet_command/idle/minebot,
/datum/pet_command/move,
/datum/pet_command/protect_owner/minebot,
/datum/pet_command/minebot_ability/light,
/datum/pet_command/minebot_ability/dump,
/datum/pet_command/automate_mining,
/datum/pet_command/free/minebot,
/datum/pet_command/follow/start_active,
/datum/pet_command/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)
AddElement(/datum/element/death_drops, /obj/effect/decal/cleanable/blood/gibs/robot_debris/old)
add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_ASHSTORM_IMMUNE, TRAIT_SNOWSTORM_IMMUNE, TRAIT_MINING_AOE_IMMUNE), INNATE_TRAIT)
AddElement(/datum/element/footstep, FOOTSTEP_OBJ_ROBOT, 1, -6, sound_vary = TRUE)
AddComponent(/datum/component/defaceable, \
icon = 'icons/mob/silicon/aibot_faces.dmi', \
icon_states = list("minebot" = FALSE, "minebot_highlight" = TRUE), \
drawing_of = "a face", \
)
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,
)
grant_actions_by_list(innate_actions)
stored_gun = new(src)
var/obj/item/implant/radio/mining/comms = new(src)
comms.implant(src)
assign_access()
/mob/living/basic/mining_drone/set_combat_mode(new_mode, silent = TRUE)
. = ..()
icon_state = combat_mode ? "mining_drone_offense" : "mining_drone"
balloon_alert(src, "now [combat_mode ? "attacking" : "collecting"]")
/mob/living/basic/mining_drone/examine(mob/user)
. = ..()
if(health < maxHealth)
if(health >= maxHealth * 0.5)
. += span_warning("[p_They()] look slightly dented.")
else
. += span_boldwarning("[p_They()] look severely dented!")
if(isnull(stored_gun) || !stored_gun.max_mod_capacity)
return
. += "<b>[stored_gun.get_remaining_mod_capacity()]%</b> mod capacity remaining."
for(var/obj/item/borg/upgrade/modkit/modkit as anything in stored_gun.modkits)
. += span_notice("There is \a [modkit] installed, using <b>[modkit.cost]%</b> capacity.")
if(ai_controller && ai_controller.ai_status == AI_STATUS_IDLE)
. += "The [src] appears to be in <b>sleep mode</b>. You can restore normal functions by <b>tapping</b> it."
/mob/living/basic/mining_drone/welder_act(mob/living/user, obj/item/welder)
if(user.combat_mode)
return FALSE
if(combat_mode)
user.balloon_alert(user, "can't repair in attack mode!")
return TRUE
if(maxHealth == health)
user.balloon_alert(user, "at full integrity!")
return TRUE
if(welder.use_tool(src, user, 0, volume=40))
adjust_brute_loss(-15)
user.balloon_alert(user, "successfully repaired!")
return TRUE
/mob/living/basic/mining_drone/attackby(obj/item/item_used, mob/user, list/modifiers, list/attack_modifiers)
if(item_used.tool_behaviour == TOOL_CROWBAR || istype(item_used, /obj/item/borg/upgrade/modkit))
item_used.melee_attack_chain(user, stored_gun, modifiers)
return
return ..()
/mob/living/basic/mining_drone/attack_hand(mob/living/carbon/human/user, list/modifiers)
if(!user.combat_mode)
if(ai_controller && ai_controller.ai_status == AI_STATUS_IDLE)
ai_controller.set_ai_status(AI_STATUS_ON)
if(LAZYACCESS(modifiers, LEFT_CLICK)) //Lets Right Click be specifically for re-enabling their AI (and avoiding the UI popup), while Left Click simply does both.
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, no_anim = TRUE))
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/click_alt(mob/living/user)
if(user.combat_mode)
return CLICK_ACTION_BLOCKING
set_combat_mode(!combat_mode)
balloon_alert(user, "now [combat_mode ? "attacking wildlife" : "collecting loose ore"]")
return CLICK_ACTION_SUCCESS
/mob/living/basic/mining_drone/RangedAttack(atom/target, list/modifiers)
if(!combat_mode)
return
stored_gun.try_fire_gun(target, src, list2params(modifiers))
/mob/living/basic/mining_drone/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers)
. = ..()
if(!. || !proximity_flag || combat_mode)
return
if(istype(attack_target, /obj/item/stack/ore))
var/obj/item/target_ore = attack_target
target_ore.forceMove(src)
/mob/living/basic/mining_drone/proc/drop_ore()
to_chat(src, span_notice("You dump your stored ore."))
for(var/obj/item/stack/ore/dropped_item in contents)
dropped_item.forceMove(get_turf(src))
/mob/living/basic/mining_drone/death(gibbed)
drop_ore()
if(isnull(stored_gun))
return ..()
for(var/obj/item/borg/upgrade/modkit/modkit as anything in stored_gun.modkits)
modkit.uninstall(stored_gun)
return ..()
/mob/living/basic/mining_drone/Destroy()
QDEL_NULL(stored_gun)
return ..()
/mob/living/basic/mining_drone/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(.)
return
if(!istype(target, /mob/living/basic/node_drone))
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
repair_node_drone(target)
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/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
/mob/living/basic/mining_drone/proc/assign_access()
var/static/list/required_access
if(isnull(required_access))
var/datum/id_trim/access_card = SSid_access.trim_singletons_by_path[/datum/id_trim/job/shaft_miner]
required_access = access_card.access
AddComponent(/datum/component/simple_access, required_access)