Files
CabinetOnFireandGitHub 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

350 lines
11 KiB
Plaintext

#define SEEDLING_STATE_NEUTRAL 0
#define SEEDLING_STATE_WARMUP 1
#define SEEDLING_STATE_ACTIVE 2
/**
* A mobile plant with a rapid ranged attack.
* It can pick up watering cans and look after plants.
*/
/mob/living/basic/seedling
name = "seedling"
desc = "This oversized, predatory flower conceals what can only be described as an organic energy cannon."
icon = 'icons/mob/simple/jungle/seedling.dmi'
icon_state = "seedling"
icon_living = "seedling"
icon_dead = "seedling_dead"
habitable_atmos = list("min_oxy" = 2, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
minimum_survivable_temperature = 0
maximum_survivable_temperature = 450
mob_biotypes = MOB_ORGANIC | MOB_PLANT
maxHealth = 100
health = 100
pixel_z = -14
base_pixel_z = -14
pixel_x = -14
base_pixel_x = -14
response_harm_continuous = "strikes"
response_harm_simple = "strike"
melee_damage_lower = 30
melee_damage_upper = 30
lighting_cutoff_green = 20
lighting_cutoff_blue = 25
mob_size = MOB_SIZE_LARGE
faction = list(FACTION_PLANTS)
attack_sound = 'sound/items/weapons/bladeslice.ogg'
attack_vis_effect = ATTACK_EFFECT_SLASH
ai_controller = /datum/ai_controller/basic_controller/seedling
///the state of combat we are in
var/combatant_state = SEEDLING_STATE_NEUTRAL
///the colors our petals can have
var/static/list/possible_colors = list(COLOR_RED, COLOR_YELLOW, COLOR_OLIVE, COLOR_CYAN)
///appearance when we are in our normal state
var/mutable_appearance/petal_neutral
///appearance when we are in our warmup state
var/mutable_appearance/petal_warmup
///appearance when we are in the firing state
var/mutable_appearance/petal_active
///appearance when we are dead
var/mutable_appearance/petal_dead
///the bucket we carry
var/obj/item/reagent_containers/cup/held_can
///commands we follow
var/list/seedling_commands = list(
/datum/pet_command/idle,
/datum/pet_command/free,
/datum/pet_command/follow/start_active,
)
/mob/living/basic/seedling/Initialize(mapload)
. = ..()
var/static/list/innate_actions = list(
/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/seedling = BB_RAPIDSEEDS_ABILITY,
/datum/action/cooldown/mob_cooldown/solarbeam = BB_SOLARBEAM_ABILITY,
)
grant_actions_by_list(innate_actions)
var/petal_color = pick(possible_colors)
petal_neutral = mutable_appearance(icon, "[icon_state]_overlay")
petal_neutral.color = petal_color
petal_warmup = mutable_appearance(icon, "[icon_state]_charging_overlay")
petal_warmup.color = petal_color
petal_active = mutable_appearance(icon, "[icon_state]_fire_overlay")
petal_active.color = petal_color
petal_dead = mutable_appearance(icon, "[icon_state]_dead_overlay")
petal_dead.color = petal_color
AddElement(/datum/element/wall_tearer, allow_reinforced = FALSE)
AddComponent(/datum/component/obeys_commands, seedling_commands)
RegisterSignal(src, COMSIG_KB_MOB_DROPITEM_DOWN, PROC_REF(drop_can))
update_appearance()
/mob/living/basic/seedling/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(.)
return
if(istype(target, /obj/machinery/hydroponics))
treat_hydro_tray(target)
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
if(isnull(held_can))
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
if(istype(target, /obj/structure/sink) || istype(target, /obj/structure/reagent_dispensers))
held_can.melee_attack_chain(src, target)
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
///seedlings can water trays, remove weeds, or remove dead plants
/mob/living/basic/seedling/proc/treat_hydro_tray(obj/machinery/hydroponics/hydro)
if(hydro.plant_status == HYDROTRAY_PLANT_DEAD)
balloon_alert(src, "dead plant removed")
hydro.set_seed(null)
return
if(hydro.weedlevel > 0)
balloon_alert(src, "weeds uprooted")
hydro.set_weedlevel(0)
return
var/list/can_reagents = held_can?.reagents.reagent_list
if(!length(can_reagents))
return
if((locate(/datum/reagent/water) in can_reagents) && (hydro.waterlevel < hydro.maxwater))
INVOKE_ASYNC(held_can, TYPE_PROC_REF(/obj/item, melee_attack_chain), src, hydro)
return
/mob/living/basic/seedling/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers)
. = ..()
if(!. || !proximity_flag || held_can)
return
if(!istype(attack_target, /obj/item/reagent_containers/cup/watering_can))
return
var/obj/item/can_target = attack_target
can_target.forceMove(src)
/mob/living/basic/seedling/proc/change_combatant_state(state)
combatant_state = state
update_appearance()
/mob/living/basic/seedling/attackby(obj/item/can, mob/living/carbon/human/user, list/modifiers, list/attack_modifiers)
if(istype(can, /obj/item/reagent_containers/cup/watering_can) && isnull(held_can))
can.forceMove(src)
return
return ..()
/mob/living/basic/seedling/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs)
if(istype(arrived, /obj/item/reagent_containers/cup/watering_can))
held_can = arrived
update_appearance()
return ..()
/mob/living/basic/seedling/update_overlays()
. = ..()
if(stat == DEAD)
. += petal_dead
return
switch(combatant_state)
if(SEEDLING_STATE_NEUTRAL)
. += petal_neutral
if(held_can)
. += mutable_appearance(icon, "seedling_can_overlay")
if(SEEDLING_STATE_WARMUP)
. += petal_warmup
if(SEEDLING_STATE_ACTIVE)
. += petal_active
/mob/living/basic/seedling/update_icon_state()
. = ..()
if(stat == DEAD)
return
switch(combatant_state)
if(SEEDLING_STATE_NEUTRAL)
icon_state = "seedling"
if(SEEDLING_STATE_WARMUP)
icon_state = "seedling_charging"
if(SEEDLING_STATE_ACTIVE)
icon_state = "seedling_fire"
/mob/living/basic/seedling/proc/drop_can(mob/living/user)
SIGNAL_HANDLER
if(isnull(held_can))
return
dropItemToGround(held_can)
return COMSIG_KB_ACTIVATED
/mob/living/basic/seedling/Exited(atom/movable/gone, direction)
. = ..()
if(gone != held_can)
return
held_can = null
update_appearance()
/mob/living/basic/seedling/death(gibbed)
. = ..()
if(isnull(held_can))
return
held_can.forceMove(drop_location())
/mob/living/basic/seedling/Destroy()
QDEL_NULL(held_can)
return ..()
/mob/living/basic/seedling/meanie
maxHealth = 400
health = 400
faction = list(FACTION_JUNGLE, FACTION_PLANTS)
ai_controller = /datum/ai_controller/basic_controller/seedling/meanie
seedling_commands = list(
/datum/pet_command/idle,
/datum/pet_command/free,
/datum/pet_command/follow/start_active,
/datum/pet_command/attack,
/datum/pet_command/use_ability/solarbeam,
/datum/pet_command/use_ability/rapidseeds,
)
//abilities
/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/seedling
name = "Solar Energy"
button_icon = 'icons/obj/weapons/guns/projectiles.dmi'
button_icon_state = "seedling"
desc = "Fire small beams of solar energy."
cooldown_time = 10 SECONDS
projectile_type = /obj/projectile/seedling
default_projectile_spread = 10
shot_count = 10
shot_delay = 0.2 SECONDS
shared_cooldown = NONE
///how long we must charge up before firing off
var/charge_up_timer = 3 SECONDS
///is the owner of this ability a seedling?
var/is_seedling = FALSE
/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/seedling/Grant(mob/grant_to)
. = ..()
if(isnull(owner))
return
is_seedling = istype(owner, /mob/living/basic/seedling)
/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/seedling/IsAvailable(feedback)
. = ..()
if(!.)
return FALSE
if(!is_seedling)
return TRUE
var/mob/living/basic/seedling/seed_owner = owner
if(seed_owner.combatant_state != SEEDLING_STATE_NEUTRAL)
if(feedback)
seed_owner.balloon_alert(seed_owner, "charging!")
return FALSE
return TRUE
/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/seedling/Activate(atom/target)
if(is_seedling)
var/mob/living/basic/seedling/seed_owner = owner
seed_owner.change_combatant_state(state = SEEDLING_STATE_WARMUP)
addtimer(CALLBACK(src, PROC_REF(attack_sequence), owner, target), charge_up_timer)
StartCooldown()
return TRUE
/datum/action/cooldown/mob_cooldown/projectile_attack/rapid_fire/seedling/attack_sequence(mob/living/firer, atom/target)
if(is_seedling)
var/mob/living/basic/seedling/seed_owner = owner
seed_owner.change_combatant_state(state = SEEDLING_STATE_ACTIVE)
addtimer(CALLBACK(seed_owner, TYPE_PROC_REF(/mob/living/basic/seedling, change_combatant_state), SEEDLING_STATE_NEUTRAL), 4 SECONDS)
return ..()
/datum/action/cooldown/mob_cooldown/solarbeam
name = "Solar Beam"
button_icon = 'icons/effects/beam.dmi'
button_icon_state = "solar_beam"
desc = "Concentrate the power of the sun onto your target!"
cooldown_time = 30 SECONDS
shared_cooldown = NONE
///how long will it take for us to charge up the beam
var/beam_charge_up = 3 SECONDS
///is the owner of this ability a seedling?
var/is_seedling = FALSE
/datum/action/cooldown/mob_cooldown/solarbeam/Grant(mob/grant_to)
. = ..()
if(isnull(owner))
return
is_seedling = istype(owner, /mob/living/basic/seedling)
/datum/action/cooldown/mob_cooldown/solarbeam/IsAvailable(feedback)
. = ..()
if(!.)
return FALSE
if(!is_seedling)
return TRUE
var/mob/living/basic/seedling/seed_owner = owner
if(seed_owner.combatant_state != SEEDLING_STATE_NEUTRAL)
if(feedback)
seed_owner.balloon_alert(seed_owner, "charging!")
return FALSE
return TRUE
/datum/action/cooldown/mob_cooldown/solarbeam/Activate(atom/target)
if(is_seedling)
var/mob/living/basic/seedling/seed_owner = owner
seed_owner.change_combatant_state(state = SEEDLING_STATE_WARMUP)
var/turf/target_turf = get_turf(target)
playsound(owner, 'sound/effects/seedling_chargeup.ogg', 100, FALSE)
var/obj/effect/temp_visual/solarbeam_killsat/owner_beam = new(get_turf(owner))
animate(owner_beam, transform = matrix().Scale(1, 32), alpha = 255, time = beam_charge_up)
var/obj/effect/temp_visual/solarbeam_killsat/target_beam = new(target_turf)
animate(target_beam, transform = matrix().Scale(2, 1), alpha = 255, time = beam_charge_up)
addtimer(CALLBACK(src, PROC_REF(launch_beam), owner, target_turf), beam_charge_up)
StartCooldown()
return TRUE
///the solarbeam will damage people, otherwise it will heal plants
/datum/action/cooldown/mob_cooldown/solarbeam/proc/launch_beam(mob/living/firer, turf/target_turf)
for(var/atom/target_atom as anything in target_turf)
if(istype(target_atom, /obj/machinery/hydroponics))
var/obj/machinery/hydroponics/hydro = target_atom
hydro.adjust_plant_health(10)
new /obj/effect/temp_visual/heal(target_turf, COLOR_HEALING_CYAN)
if(!isliving(target_atom))
continue
var/mob/living/living_target = target_atom
living_target.adjust_fire_stacks(0.2)
living_target.ignite_mob()
living_target.adjust_fire_loss(30)
playsound(target_turf, 'sound/effects/magic/lightningbolt.ogg', 50, TRUE)
if(!is_seedling)
return
var/mob/living/basic/seedling/seed_firer = firer
seed_firer.change_combatant_state(state = SEEDLING_STATE_NEUTRAL)
#undef SEEDLING_STATE_NEUTRAL
#undef SEEDLING_STATE_WARMUP
#undef SEEDLING_STATE_ACTIVE