Merge remote-tracking branch 'tgstation/master' into upstream-6-2-2026

This commit is contained in:
shayoki
2026-06-03 01:23:54 -05:00
1259 changed files with 50763 additions and 28726 deletions
+3 -3
View File
@@ -19,15 +19,15 @@
///Handles the maid attacking other players, cancelling the attack to clean up instead.
/mob/living/basic/alien/maid/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(!.)
return FALSE
if(.)
return
target.wash(CLEAN_SCRUB)
if(istype(target, /obj/effect/decal/cleanable))
visible_message(span_notice("[src] cleans up \the [target]."))
else
visible_message(span_notice("[src] polishes \the [target]."))
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/**
* Barmaid special type
+8 -18
View File
@@ -232,21 +232,22 @@
. += span_deadsay("Upon closer examination, [p_they()] appear[p_s()] to be [HAS_MIND_TRAIT(user, TRAIT_NAIVE) ? "asleep" : "dead"].")
/mob/living/basic/proc/melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE)
if(!early_melee_attack(target, modifiers, ignore_cooldown))
var/early_melee_result = early_melee_attack(target, modifiers, ignore_cooldown)
if(early_melee_result) //Truthy value means we want to end the chain
if(!ignore_cooldown && early_melee_result == BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN)
changeNext_move(melee_attack_cooldown)
return FALSE
var/result = target.attack_basic_mob(src, modifiers)
SEND_SIGNAL(src, COMSIG_HOSTILE_POST_ATTACKINGTARGET, target, result)
if(!ignore_cooldown)
changeNext_move(melee_attack_cooldown) // Set it again because objects like to fuck with it in attack_basic_mob
if(result && !ignore_cooldown) //Only set cooldown if the attack achieved something, which is the case when the value is true-ey. This could definitely be done better but is probably easier once living/simple is gone and we no longer use attack_animal.
changeNext_move(melee_attack_cooldown)
return result
/mob/living/basic/proc/early_melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE)
face_atom(target)
if(!ignore_cooldown)
changeNext_move(melee_attack_cooldown) // Set cooldown early in case it is cancelled
if(SEND_SIGNAL(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, target, Adjacent(target), modifiers) & COMPONENT_HOSTILE_NO_ATTACK)
return FALSE //but more importantly return before attack_animal called
return TRUE
return BASIC_MOB_END_ATTACK_CHAIN //but more importantly return before attack_animal called
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
/mob/living/basic/resolve_unarmed_attack(atom/attack_target, list/modifiers)
melee_attack(attack_target, modifiers)
@@ -307,17 +308,6 @@
if (.)
update_held_items()
/mob/living/basic/update_held_items()
. = ..()
if(isnull(client) || isnull(hud_used) || hud_used.hud_version == HUD_STYLE_NOHUD)
return
var/turf/our_turf = get_turf(src)
for(var/obj/item/held in held_items)
var/index = get_held_index_of_item(held)
SET_PLANE(held, ABOVE_HUD_PLANE, our_turf)
held.screen_loc = ui_hand_position(index)
client.screen |= held
/mob/living/basic/get_body_temp_heat_damage_limit()
return maximum_survivable_temperature
@@ -21,6 +21,7 @@
initial_language_holder = /datum/language_holder/empty
can_buckle_to = FALSE
damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, STAMINA = 0, OXY = 1)
pull_force = 0
/// Size of cloud produced from a dying spore
var/death_cloud_size = BLOBMOB_CLOUD_NONE
var/loot = /obj/item/food/spore_sack
+2 -1
View File
@@ -77,10 +77,11 @@
/mob/living/basic/boss/early_melee_attack(mob/living/target, list/modifiers, ignore_cooldown)
. = ..()
if(!. || !istype(target))
if(. || !istype(target))
return
if(should_devour(target))
devour(target)
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/// Determines if this mob is worth devouring
/mob/living/basic/boss/proc/should_devour(mob/living/victim)
@@ -63,6 +63,8 @@
source.clear_path_hud(remove_hud = FALSE)
/datum/ai_controller/basic_controller/bot/proc/add_to_blacklist(atom/target, duration)
if(QDELETED(target))
return
var/final_duration = duration || blackboard[BB_UNREACHABLE_LIST_COOLDOWN]
set_blackboard_key_assoc_lazylist(BB_TEMPORARY_IGNORE_LIST, target, TRUE)
addtimer(CALLBACK(src, PROC_REF(remove_from_blacklist), target), final_duration)
@@ -107,11 +109,15 @@
///set the target if we can reach them
/datum/ai_controller/basic_controller/bot/proc/set_if_can_reach(key, target, duration, distance = 10, bypass_add_to_blacklist = FALSE)
if(can_reach_target(target, distance))
EVLOG_MAPTEXT(src, EVLOG_CATEGORY_AI_TARGETING, "[pawn] has selected [target] as a target for blackboard key [key]!", get_turf(target), "Target: [target]")
EVLOG_LINES(src, EVLOG_CATEGORY_AI_TARGETING, "Line to target", get_turf(pawn), get_turf(target))
set_blackboard_key(key, target)
return TRUE
if(bypass_add_to_blacklist)
return FALSE
var/final_duration = duration || blackboard[BB_UNREACHABLE_LIST_COOLDOWN]
EVLOG_MAPTEXT(src, EVLOG_CATEGORY_AI_TARGETING, "[pawn] has added [target] to its targetting blacklist!", get_turf(target), "Target: [target]")
EVLOG_LINES(src, EVLOG_CATEGORY_AI_TARGETING, "Line to target", get_turf(pawn), get_turf(target))
add_to_blacklist(target, final_duration)
return FALSE
@@ -0,0 +1,176 @@
/mob/living/basic/bot/secbot/ed209
name = "\improper ED-209 Security Robot"
desc = "A security robot. He looks less than thrilled."
icon_state = "ed209"
base_icon_state = "ed209"
light_color = "#f84e4e"
density = TRUE
health = 100
maxHealth = 100
obj_damage = 60
environment_smash = ENVIRONMENT_SMASH_WALLS //Walls can't stop THE LAW
mob_size = MOB_SIZE_LARGE
ai_controller = /datum/ai_controller/basic_controller/bot/ed209
bot_type = ADVANCED_SEC_BOT
hackables = "combat inhibitors"
custom_materials = list(
/datum/material/iron = SHEET_MATERIAL_AMOUNT * 2.8,
/datum/material/glass = SMALL_MATERIAL_AMOUNT * 2.1,
)
///sound of the projectiles we shoot
var/projectile_sound = 'sound/items/weapons/laser.ogg'
///what projectiles we shoot
var/projectile_type = /obj/projectile/beam/disabler
///what projectiles we shoot when emagged
var/emagged_projectile_type = /obj/projectile/beam
///sound of emagged projectile
var/emagged_projectile_sound = 'sound/items/weapons/laser.ogg'
///special hats that change our personality :mistake:
var/static/list/sherrif_hats = typecacheof(list(
/obj/item/clothing/head/cowboy,
))
var/datum/action/cooldown/mob_cooldown/ed209_charge/bot_charge
///our riding component
var/ride_component = /datum/component/riding/creature/ed_bot
///have we become a sheriff
var/sheriffized = FALSE
///timer till we yell out our war cry again
COOLDOWN_DECLARE(shoot_cry)
/mob/living/basic/bot/secbot/ed209/Initialize(mapload)
. = ..()
set_weapon()
bot_charge = new(src)
var/static/list/hat_offset = list(2, 0)
AddElement(/datum/element/hat_wearer,\
offsets = hat_offset,\
)
AddComponent(/datum/component/defaceable, \
icon = 'icons/mob/silicon/aibot_faces.dmi', \
icon_states = list("ed209" = FALSE, "ed209_highlight" = TRUE), \
drawing_of = "a face", \
)
AddComponent(/datum/component/stun_n_cuff,\
stun_sound = 'sound/items/weapons/egloves.ogg',\
handcuff_type = /obj/item/restraints/handcuffs/cable/zipties,\
)
AddElement(/datum/element/ridable, ride_component)
RegisterSignal(src, COMSIG_BASICMOB_POST_ATTACK_RANGED, PROC_REF(post_ranged_attack))
/mob/living/basic/bot/secbot/ed209/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs)
. = ..()
sheriffized = (is_type_in_typecache(arrived, sherrif_hats)) //yeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeehawwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww
/mob/living/basic/bot/secbot/ed209/proc/post_ranged_attack()
SIGNAL_HANDLER
if(!sheriffized || !COOLDOWN_FINISHED(src, shoot_cry))
return
COOLDOWN_START(src, shoot_cry, 30 SECONDS)
INVOKE_ASYNC(src, TYPE_PROC_REF(/atom/movable, say), "YIPPIE-KI-YAY!")
/mob/living/basic/bot/secbot/ed209/Exited(atom/movable/gone, direction)
. = ..()
sheriffized = (is_type_in_typecache(gone, sherrif_hats))
/mob/living/basic/bot/secbot/ed209/examine(mob/user)
. = ..()
if(sheriffized)
. += span_notice("Fastest hand in the west.")
/mob/living/basic/bot/secbot/ed209/bot_reset(bypass_ai_reset = FALSE)
. = ..()
if(bot_access_flags & BOT_COVER_EMAGGED && isnull(bot_charge.owner))
bot_charge.Grant(src)
if(!(bot_access_flags & BOT_COVER_EMAGGED) && !isnull(bot_charge.owner))
bot_charge.Remove(src)
set_weapon()
/mob/living/basic/bot/secbot/ed209/emag_act(mob/user, obj/item/card/emag/emag_card)
. = ..()
icon_state = "ed209[bot_mode_flags & BOT_MODE_ON]"
set_weapon()
balloon_alert(user, "safeties disabled")
audible_message(span_bolddanger("[src] buzzes menacingly!"))
return TRUE
/mob/living/basic/bot/secbot/ed209/proc/set_weapon()
qdel(GetComponent(/datum/component/ranged_attacks))
var/projectile = (bot_access_flags & BOT_COVER_EMAGGED) ? emagged_projectile_type : projectile_type
var/final_projectile_sound = (bot_access_flags & BOT_COVER_EMAGGED) ? emagged_projectile_sound : projectile_sound
AddComponent(\
/datum/component/ranged_attacks,\
projectile_type = projectile,\
projectile_sound = final_projectile_sound,\
)
/mob/living/basic/bot/secbot/ed209/ui_data(mob/user)
var/list/data = ..()
if(!(bot_access_flags & BOT_COVER_LOCKED) || HAS_SILICON_ACCESS(user))
data["custom_controls"]["handcuff"] = security_mode_flags & SECBOT_HANDCUFF_TARGET
data["custom_controls"]["check_ids"] = security_mode_flags & SECBOT_CHECK_IDS
data["custom_controls"]["check_records"] = security_mode_flags & SECBOT_CHECK_RECORDS
return data
/mob/living/basic/bot/secbot/ed209/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
var/mob/user = ui.user
if(. || !isliving(user) || (bot_access_flags & BOT_COVER_LOCKED) && !HAS_SILICON_ACCESS(user))
return
switch(action)
if("handcuff")
security_mode_flags ^= SECBOT_HANDCUFF_TARGET
if("check_ids")
security_mode_flags ^= SECBOT_CHECK_IDS
if("check_records")
security_mode_flags ^= SECBOT_CHECK_RECORDS
/mob/living/basic/bot/secbot/ed209/retrieve_secbot_drops(atom/drop_location)
var/obj/item/bot_assembly/ed209/ed_assembly = new(drop_location)
ed_assembly.build_step = ASSEMBLY_FIRST_STEP
ed_assembly.add_overlay("hs_hole")
ed_assembly.created_name = name
new /obj/item/assembly/prox_sensor(drop_location)
var/obj/item/gun/energy/disabler/disabler_gun = new(drop_location)
disabler_gun.cell.charge = 0
disabler_gun.update_appearance()
if(prob(50))
new /obj/item/bodypart/leg/left/robot(drop_location)
if(prob(25))
new /obj/item/bodypart/leg/right/robot(drop_location)
if(prob(75))
return
if(prob(50)) // either helmet or vest
new /obj/item/clothing/head/helmet(drop_location)
else
new /obj/item/clothing/suit/armor/vest(drop_location)
/mob/living/basic/bot/secbot/ed209/Destroy()
. = ..()
QDEL_NULL(bot_charge)
/mob/living/basic/bot/secbot/ed209/nukie
name = "\improper ED-209(+1) Syndicate Robot"
desc = "Wait this one's red? This cannot be good... right??"
icon_state = "red209"
light_color = "#5c0909"
faction = list(ROLE_SYNDICATE)
health = 250
maxHealth = 250
obj_damage = 60
req_one_access = list(ACCESS_SYNDICATE)
bot_mode_flags = parent_type::bot_mode_flags & ~BOT_MODE_REMOTE_ENABLED
radio_key = /obj/item/encryptionkey/syndicate
additional_access = /datum/id_trim/syndicom/crew
radio_channel = RADIO_CHANNEL_SYNDICATE
ai_controller = /datum/ai_controller/basic_controller/bot/ed209/syndicate
bot_type = ADVANCED_SEC_BOT
hackables = "combat inhibitors"
projectile_sound = 'sound/items/weapons/gun/l6/shot.ogg'
projectile_type = /obj/projectile/bullet/a7mm
emagged_projectile_sound = 'sound/items/weapons/minebot_rocket.ogg'
emagged_projectile_type = /obj/projectile/bullet/rocket/weak //lord have mercy
ride_component = /datum/component/riding/creature/ed_bot/nukie //ride at ur own risk. especially if its emagged. warranty void
@@ -0,0 +1,40 @@
/datum/action/cooldown/mob_cooldown/ed209_charge
name = "Bot Tackle"
desc = "Not even God's mightiest Quarterback can withstand this."
cooldown_time = 10 SECONDS
background_icon_state = "bg_revenant"
overlay_icon_state = "bg_revenant_border"
shared_cooldown = NONE
///duration of telegraph
var/telegraph_duration = 1.25 SECONDS
///damage we apply on tackle
var/tackle_damage = 25
/datum/action/cooldown/mob_cooldown/ed209_charge/Activate(atom/target)
var/turf/target_turf = get_turf(target)
if(isclosedturf(target_turf) || isspaceturf(target_turf))
owner.balloon_alert(owner, "base not suitable!")
return FALSE
addtimer(CALLBACK(src, PROC_REF(commence_launch), target), telegraph_duration)
owner.Shake(duration = telegraph_duration)
StartCooldown()
return TRUE
/datum/action/cooldown/mob_cooldown/ed209_charge/proc/commence_launch(atom/target)
var/turf/target_turf = get_turf(target)
owner.throw_at(target = target_turf, range = 7, speed = 1, spin = FALSE, callback = CALLBACK(src, PROC_REF(on_tackle), target_turf))
new /obj/effect/temp_visual/mook_dust(owner.loc)
/datum/action/cooldown/mob_cooldown/ed209_charge/proc/on_tackle(turf/target, original_pixel_y)
playsound(get_turf(owner), 'sound/effects/meteorimpact.ogg', 100, TRUE)
new /obj/effect/temp_visual/mook_dust(owner.loc)
for(var/mob/living/victim in oview(1, owner))
if(victim in owner.buckled_mobs)
continue
victim.apply_damage(tackle_damage)
if(QDELETED(victim))
continue
var/throw_dir = victim.loc == owner.loc ? get_dir(owner, victim) : pick(GLOB.alldirs)
var/throwtarget = get_edge_target_turf(victim, throw_dir)
victim.throw_at(target = throwtarget, range = 3, speed = 1)
victim.visible_message(span_warning("[victim] eats steel!"))
@@ -0,0 +1,65 @@
#define DEFAULT_LINES "default_lines"
#define SPECIAL_LINES "special_lines"
/datum/ai_controller/basic_controller/bot/ed209
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/secbot,
BB_UNREACHABLE_LIST_COOLDOWN = 1 MINUTES,
BB_ALWAYS_IGNORE_FACTION = TRUE,
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/ranged_skirmish,
/datum/ai_planning_subtree/arrest_target/ed209,
/datum/ai_planning_subtree/find_patrol_beacon,
)
reset_keys = list(
BB_BEACON_TARGET,
BB_PREVIOUS_BEACON_TARGET,
BB_BOT_SUMMON_TARGET,
)
/datum/ai_controller/basic_controller/bot/ed209/TryPossessPawn(atom/new_pawn)
. = ..()
if(. & AI_CONTROLLER_INCOMPATIBLE)
return
RegisterSignal(new_pawn, COMSIG_AI_BLACKBOARD_KEY_SET(BB_BASIC_MOB_CURRENT_TARGET), PROC_REF(on_target_set))
/datum/ai_controller/basic_controller/bot/ed209/proc/on_target_set()
SIGNAL_HANDLER
var/datum/action/cooldown/bot_announcement/announcement = blackboard[BB_ANNOUNCE_ABILITY]
var/static/list/lines_to_pick = list(
DEFAULT_LINES = list(
"Scumbag alert!",
"Threat detected!"
),
SPECIAL_LINES = list(
"Why y'all causin trouble in my town?",
"Fill your hands, you son of a bitch.",
"Aint nobody causin trouble in MY jurisdiction."
)
)
var/mob/living/basic/bot/secbot/ed209/my_bot = pawn
var/list/final_list = my_bot.sheriffized ? lines_to_pick[SPECIAL_LINES] : lines_to_pick[DEFAULT_LINES]
INVOKE_ASYNC(announcement, TYPE_PROC_REF(/datum/action/cooldown/bot_announcement, announce), pick(final_list))
/datum/ai_planning_subtree/arrest_target/ed209
arrest_behavior = /datum/ai_behavior/basic_melee_attack/interact_once/bot/ed209
/datum/ai_behavior/basic_melee_attack/interact_once/bot/ed209
action_cooldown = 0.5 SECONDS
/datum/ai_behavior/basic_melee_attack/interact_once/bot/ed209/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
if(!(. & AI_BEHAVIOR_DELAY))
return AI_BEHAVIOR_DELAY //this kinda sucks but we have to do this cause we need to shoot while moving to stun
#undef DEFAULT_LINES
#undef SPECIAL_LINES
@@ -0,0 +1,16 @@
/datum/ai_controller/basic_controller/bot/ed209/syndicate
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_UNREACHABLE_LIST_COOLDOWN = 1 MINUTES,
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/basic_ranged_attack_subtree,
/datum/ai_planning_subtree/find_patrol_beacon,
)
reset_keys = list(
BB_BEACON_TARGET,
BB_PREVIOUS_BEACON_TARGET,
BB_BOT_SUMMON_TARGET,
)
@@ -0,0 +1,19 @@
/obj/item/antag_spawner/nuke_ops/ed209_nukie
name = "Syndicate ED209 Robot"
desc = "A single-use beacon designed to quickly launch reinforcement operatives into the field."
/obj/item/antag_spawner/nuke_ops/ed209_nukie/attack_self(mob/user)
if(!(check_usability(user)))
return
to_chat(user, span_notice("You activate [src] and wait for confirmation."))
drop_bot()
do_sparks(4, TRUE, src)
qdel(src)
/obj/item/antag_spawner/nuke_ops/ed209_nukie/proc/drop_bot()
var/mob/living/basic/bot/secbot/ed209/nukie/nuclear_bot = new()
var/obj/structure/closet/supplypod/pod = setup_pod()
nuclear_bot.forceMove(pod)
new /obj/effect/pod_landingzone(spawn_location ? spawn_location : get_turf(src), pod)
@@ -1,4 +1,4 @@
/mob/living/basic/bot/honkbot
/mob/living/basic/bot/secbot/honkbot
name = "\improper Honkbot"
desc = "A little robot. It looks happy with its bike horn."
icon_state = "honkbot"
@@ -16,15 +16,17 @@
data_hud_type = TRAIT_SECURITY_HUD_ID_ONLY
additional_access = /datum/id_trim/job/clown
possessed_message = "You are a honkbot! Make sure the crew are having a great time!"
security_mode_flags = parent_type::security_mode_flags | HONKBOT_MODE_SLIP
///our voicelines
var/static/list/honkbot_sounds = list(
HONKBOT_VOICED_HONK_HAPPY = 'sound/items/bikehorn.ogg',
HONKBOT_VOICED_HONK_SAD = 'sound/misc/sadtrombone.ogg',
)
///Honkbot's flags
var/honkbot_flags = HONKBOT_CHECK_RECORDS | HONKBOT_HANDCUFF_TARGET | HONKBOT_MODE_SLIP
stun_sound = 'sound/items/airhorn/AirHorn.ogg'
baton_type = /obj/item/bikehorn/airhorn
cuff_type = /obj/item/restraints/handcuffs/cable/zipties/fake
/mob/living/basic/bot/honkbot/Initialize(mapload)
/mob/living/basic/bot/secbot/honkbot/Initialize(mapload)
. = ..()
var/static/list/clown_friends = typecacheof(list(
/mob/living/carbon/human,
@@ -48,33 +50,34 @@
on_slip_callback = CALLBACK(src, PROC_REF(post_slip)),\
can_slip_callback = CALLBACK(src, PROC_REF(pre_slip)),\
)
AddComponent(/datum/component/stun_n_cuff,\
stun_sound = 'sound/items/airhorn/AirHorn.ogg',\
post_stun_callback = CALLBACK(src, PROC_REF(post_stun)),\
post_arrest_callback = CALLBACK(src, PROC_REF(post_arrest)),\
handcuff_type = /obj/item/restraints/handcuffs/cable/zipties/fake,\
)
/mob/living/basic/bot/honkbot/generate_speak_list()
/mob/living/basic/bot/secbot/honkbot/generate_speak_list()
return honkbot_sounds
/mob/living/basic/bot/honkbot/proc/pre_slip()
/mob/living/basic/bot/secbot/honkbot/proc/pre_slip()
return (prob(70) && ai_controller?.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
/mob/living/basic/bot/honkbot/proc/post_slip()
/mob/living/basic/bot/secbot/honkbot/proc/post_slip()
INVOKE_ASYNC(src, TYPE_PROC_REF(/mob/living/basic/bot, speak), HONKBOT_VOICED_HONK_SAD)
set_attacking_state()
/mob/living/basic/bot/honkbot/proc/set_attacking_state()
/mob/living/basic/bot/secbot/honkbot/proc/set_attacking_state()
icon_state = "[base_icon_state]-c"
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, update_appearance)), 0.2 SECONDS)
/mob/living/basic/bot/honkbot/proc/post_arrest(mob/living/carbon/current_target)
/mob/living/basic/bot/secbot/honkbot/post_arrest(mob/living/carbon/current_target)
playsound(src, (bot_access_flags & BOT_COVER_EMAGGED ? SFX_HONKBOT_E : 'sound/items/bikehorn.ogg'), 50, FALSE)
icon_state = bot_access_flags & BOT_COVER_EMAGGED ? "[base_icon_state]-e" : "[base_icon_state]-c"
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, update_appearance)), 3 SECONDS, TIMER_OVERRIDE|TIMER_UNIQUE)
/mob/living/basic/bot/honkbot/proc/post_stun(mob/living/carbon/current_target)
audible_message(span_danger("[src] gives out an evil laugh!"))
playsound(src, 'sound/mobs/non-humanoids/honkbot/honkbot_evil_laugh.ogg', 75, TRUE, -1) // evil laughter
/mob/living/basic/bot/secbot/honkbot/retrieve_emag_message()
audible_message(span_danger("[src] gives out an evil laugh!"))
playsound(src, 'sound/mobs/non-humanoids/honkbot/honkbot_evil_laugh.ogg', 75, TRUE, -1) // evil laughter
/mob/living/basic/bot/secbot/honkbot/post_stun(mob/living/carbon/current_target)
if(!istype(current_target))
return
@@ -86,26 +89,36 @@
sound_damage(deafen = 10 SECONDS)
/mob/living/basic/bot/honkbot/ui_data(mob/user)
/mob/living/basic/bot/secbot/honkbot/ui_data(mob/user)
var/list/data = ..()
if(!(bot_access_flags & BOT_COVER_LOCKED) || HAS_SILICON_ACCESS(user))
data["custom_controls"]["slip_people"] = honkbot_flags & HONKBOT_MODE_SLIP
data["custom_controls"]["fake_cuff"] = honkbot_flags & HONKBOT_HANDCUFF_TARGET
data["custom_controls"]["check_ids"] = honkbot_flags & HONKBOT_CHECK_IDS
data["custom_controls"]["check_records"] = honkbot_flags & HONKBOT_CHECK_RECORDS
data["custom_controls"]["slip_people"] = security_mode_flags & HONKBOT_MODE_SLIP
data["custom_controls"]["fake_cuff"] = security_mode_flags & SECBOT_HANDCUFF_TARGET
data["custom_controls"]["check_ids"] = security_mode_flags & SECBOT_CHECK_IDS
data["custom_controls"]["check_records"] = security_mode_flags & SECBOT_CHECK_RECORDS
return data
/mob/living/basic/bot/honkbot/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
/mob/living/basic/bot/secbot/honkbot/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
var/mob/user = ui.user
if(. || !isliving(user) || (bot_access_flags & BOT_COVER_LOCKED) && !HAS_SILICON_ACCESS(user))
return
switch(action)
if("slip_people")
honkbot_flags ^= HONKBOT_MODE_SLIP
security_mode_flags ^= HONKBOT_MODE_SLIP
if("fake_cuff")
honkbot_flags ^= HONKBOT_HANDCUFF_TARGET
security_mode_flags ^= SECBOT_HANDCUFF_TARGET
if("check_ids")
honkbot_flags ^= HONKBOT_CHECK_IDS
security_mode_flags ^= SECBOT_CHECK_IDS
if("check_records")
honkbot_flags ^= HONKBOT_CHECK_RECORDS
security_mode_flags ^= SECBOT_CHECK_RECORDS
/mob/living/basic/bot/secbot/honkbot/retrieve_secbot_drops(atom/drop_location)
var/obj/item/bot_assembly/honkbot/honkbot_assembly = new(drop_location)
honkbot_assembly.build_step = ASSEMBLY_FIRST_STEP
honkbot_assembly.created_name = name
new /obj/item/assembly/prox_sensor(drop_location)
drop_part(baton_type, drop_location)
/mob/living/basic/bot/secbot/honkbot/nopatrol
bot_mode_flags = parent_type::bot_mode_flags & ~BOT_MODE_AUTOPATROL
@@ -1,6 +1,6 @@
/datum/ai_controller/basic_controller/bot/honkbot
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/secbot,
BB_UNREACHABLE_LIST_COOLDOWN = 1 MINUTES,
BB_ALWAYS_IGNORE_FACTION = TRUE,
)
@@ -8,8 +8,8 @@
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/use_mob_ability/random_honk,
/datum/ai_planning_subtree/find_wanted_targets,
/datum/ai_planning_subtree/troll_target,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/arrest_target,
/datum/ai_planning_subtree/slip_victims,
/datum/ai_planning_subtree/play_with_clowns,
/datum/ai_planning_subtree/find_patrol_beacon,
@@ -43,60 +43,6 @@
add_to_blacklist(slip_target)
clear_blackboard_key(BB_SLIP_TARGET)
/datum/ai_planning_subtree/find_wanted_targets
/datum/ai_planning_subtree/find_wanted_targets/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/static/list/can_arrest = typecacheof(list(/mob/living/carbon/human))
if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
controller.queue_behavior(/datum/ai_behavior/bot_search/wanted_targets, BB_BASIC_MOB_CURRENT_TARGET, can_arrest)
/datum/ai_behavior/bot_search/wanted_targets
/datum/ai_behavior/bot_search/wanted_targets/valid_target(datum/ai_controller/basic_controller/bot/controller, mob/living/my_target)
if(!ishuman(my_target))
return FALSE
var/mob/living/carbon/human/human_target = my_target
if(human_target.handcuffed || human_target.stat != CONSCIOUS)
return FALSE
if(locate(human_target) in controller.blackboard[BB_BASIC_MOB_RETALIATE_LIST])
return TRUE
var/mob/living/basic/bot/honkbot/my_bot = controller.pawn
var/honkbot_flags = my_bot.honkbot_flags
var/assess_flags = NONE
if(human_target.IsParalyzed() && !(honkbot_flags & HONKBOT_HANDCUFF_TARGET))
return FALSE
if(my_bot.bot_access_flags & BOT_COVER_EMAGGED)
assess_flags |= JUDGE_EMAGGED
if(honkbot_flags & HONKBOT_CHECK_IDS)
assess_flags |= JUDGE_IDCHECK
if(honkbot_flags & HONKBOT_CHECK_RECORDS)
assess_flags |= JUDGE_RECORDCHECK
return (human_target.assess_threat(assess_flags) > 0)
/datum/ai_planning_subtree/troll_target
/datum/ai_planning_subtree/troll_target/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/mob/living/carbon/my_target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(QDELETED(my_target) || !istype(my_target) || my_target.handcuffed)
controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET)
return
var/mob/living/basic/bot/honkbot/my_bot = controller.pawn
if(my_target.IsParalyzed() && !(my_bot.honkbot_flags & HONKBOT_HANDCUFF_TARGET))
controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET)
return
controller.queue_behavior(/datum/ai_behavior/basic_melee_attack/interact_once/honkbot, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/basic_melee_attack/interact_once/honkbot
/datum/ai_behavior/basic_melee_attack/interact_once/honkbot/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
var/mob/living/carbon/human/human_target = controller.blackboard[target_key]
if(!isnull(human_target))
controller.remove_from_blackboard_lazylist_key(BB_BASIC_MOB_RETALIATE_LIST, human_target)
return ..()
/datum/ai_planning_subtree/play_with_clowns/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/mob/living/clown_target = controller.blackboard[BB_CLOWN_FRIEND]
if(QDELETED(clown_target))
@@ -0,0 +1,263 @@
/mob/living/basic/bot/secbot
name = "\improper Securitron"
desc = "A little security robot. He looks less than thrilled."
icon = 'icons/mob/silicon/aibots.dmi'
icon_state = "secbot"
base_icon_state = "secbot"
light_color = "#f56275"
light_power = 0.8
gender = MALE
density = FALSE
anchored = FALSE
health = 25
maxHealth = 25
damage_coeff = list(BRUTE = 0.5, BURN = 0.7, TOX = 0, STAMINA = 0, OXY = 0)
pass_flags = PASSMOB | PASSFLAPS
combat_mode = TRUE
can_buckle_to = FALSE
req_one_access = list(ACCESS_SECURITY)
radio_key = /obj/item/encryptionkey/secbot //AI Priv + Security
radio_channel = RADIO_CHANNEL_SECURITY //Security channel
bot_type = SEC_BOT
bot_mode_flags = ~BOT_MODE_CAN_BE_SAPIENT
data_hud_type = TRAIT_SECURITY_HUD
hackables = "target identification systems"
path_image_color = COLOR_RED
possessed_message = "You are a securitron! Guard the station to the best of your ability!"
additional_access = /datum/id_trim/job/detective
custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 1.2, /datum/material/glass = SMALL_MATERIAL_AMOUNT * 3.2)
ai_controller = /datum/ai_controller/basic_controller/bot/secbot
///Whether this secbot is considered 'commissioned' and given the trait on Initialize.
var/commissioned = FALSE
///The type of baton this Secbot will use
var/baton_type = /obj/item/melee/baton/security
///The weapon (from baton_type) that will be used to make arrests.
var/obj/item/weapon
///The threat level of the BOT, will arrest anyone at threatlevel 4 or above
var/threatlevel = 0
///Flags SecBOTs use on what to check on targets when arresting, and whether they should announce it to security/handcuff their target
/// Look at the security_mode_flags bitfield for more information on what's togglable here.
var/security_mode_flags = SECBOT_DECLARE_ARRESTS | SECBOT_CHECK_RECORDS | SECBOT_HANDCUFF_TARGET
/// On arrest, charges the violator this much.
/// If they don't have that much in their account, they will get beaten instead
var/price_arrest = 0
/// Charged each time the violator is stunned on detain
var/price_detain = 0
///The department the secbot will deposit collected money into
var/payment_department = ACCOUNT_SEC
///what sound we play when stunning
var/stun_sound = 'sound/items/weapons/egloves.ogg'
///The type of cuffs we use on criminals after making arrests
var/cuff_type = /obj/item/restraints/handcuffs/cable/zipties/used
/mob/living/basic/bot/secbot/Initialize(mapload)
. = ..()
weapon = new baton_type(src)
update_appearance(UPDATE_ICON)
if(commissioned)
ADD_TRAIT(src, TRAIT_COMMISSIONED, INNATE_TRAIT)
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
)
AddElement(/datum/element/connect_loc, loc_connections)
AddComponent(/datum/component/security_vision, judgement_criteria = NONE, update_judgement_criteria = CALLBACK(src, PROC_REF(judgement_criteria)))
add_arrest_component()
/mob/living/basic/bot/secbot/Destroy()
QDEL_NULL(weapon)
return ..()
/mob/living/basic/bot/secbot/update_icon_state()
if(mode == BOT_HUNT)
icon_state = "[base_icon_state]-c"
return ..()
/mob/living/basic/bot/secbot/turn_off()
..()
update_bot_mode(new_mode = BOT_IDLE)
/mob/living/basic/bot/secbot/on_saboteur(datum/source, disrupt_duration)
. = ..()
if(!(security_mode_flags & SECBOT_SABOTEUR_AFFECTED))
security_mode_flags |= SECBOT_SABOTEUR_AFFECTED
addtimer(CALLBACK(src, PROC_REF(remove_saboteur_effect)), disrupt_duration)
return TRUE
/mob/living/basic/bot/secbot/proc/remove_saboteur_effect()
security_mode_flags &= ~SECBOT_SABOTEUR_AFFECTED
/mob/living/basic/bot/secbot/electrocute_act(shock_damage, source, siemens_coeff = 1, flags = NONE)//shocks only make him angry
if(speed >= initial(speed) + 3)
return
speed += 3
addtimer(VARSET_CALLBACK(src, speed, speed - 3), 6 SECONDS)
playsound(src, 'sound/machines/defib/defib_zap.ogg', 50)
visible_message(span_warning("[src] shakes and speeds up!"))
/mob/living/basic/bot/secbot/Exited(atom/movable/gone, direction)
. = ..()
if(gone == weapon)
weapon = null
update_appearance()
// Variables sent to TGUI
/mob/living/basic/bot/secbot/ui_data(mob/user)
var/list/data = ..()
if(!(bot_access_flags & BOT_COVER_LOCKED) || HAS_SILICON_ACCESS(user))
data["custom_controls"]["check_id"] = security_mode_flags & SECBOT_CHECK_IDS
data["custom_controls"]["check_weapons"] = security_mode_flags & SECBOT_CHECK_WEAPONS
data["custom_controls"]["check_warrants"] = security_mode_flags & SECBOT_CHECK_RECORDS
data["custom_controls"]["handcuff_targets"] = security_mode_flags & SECBOT_HANDCUFF_TARGET
data["custom_controls"]["arrest_alert"] = security_mode_flags & SECBOT_DECLARE_ARRESTS
return data
// Actions received from TGUI
/mob/living/basic/bot/secbot/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
var/mob/user = ui.user
if(. || (bot_access_flags & BOT_COVER_LOCKED && !HAS_SILICON_ACCESS(user)))
return
switch(action)
if("check_id")
security_mode_flags ^= SECBOT_CHECK_IDS
return TRUE
if("check_weapons")
security_mode_flags ^= SECBOT_CHECK_WEAPONS
return TRUE
if("check_warrants")
security_mode_flags ^= SECBOT_CHECK_RECORDS
return TRUE
if("handcuff_targets")
security_mode_flags ^= SECBOT_HANDCUFF_TARGET
return TRUE
if("arrest_alert")
security_mode_flags ^= SECBOT_DECLARE_ARRESTS
return TRUE
/mob/living/basic/bot/secbot/attack_hand(mob/living/carbon/human/user, list/modifiers)
// Turns an oversight into a feature. Beepsky will now announce when pacifists taunt him over sec comms.
if(HAS_TRAIT(user, TRAIT_PACIFISM))
user.visible_message(span_notice("[user] taunts [src], daring [p_them()] to give chase!"), \
span_notice("You taunt [src], daring [p_them()] to chase you!"), span_hear("You hear someone shout a daring taunt!"), DEFAULT_MESSAGE_RANGE, user)
speak("Taunted by pacifist scumbag [RUNECHAT_BOLD("[user]")] in [get_area(src)].", radio_channel)
// Interrupt the attack chain. We've already handled this scenario for pacifists.
return
return ..()
/mob/living/basic/bot/secbot/proc/retrieve_emag_message()
audible_message(span_danger("[src] buzzes oddly!"))
/mob/living/basic/bot/secbot/emag_act(mob/user, obj/item/card/emag/emag_card)
. = ..()
if(!(bot_access_flags & BOT_COVER_EMAGGED))
return
if(user)
balloon_alert(user, "target assessment circuits shorted")
retrieve_emag_message()
security_mode_flags &= ~SECBOT_DECLARE_ARRESTS
update_appearance()
return TRUE
/mob/living/basic/bot/secbot/proc/post_arrest(mob/living/carbon/current_target)
playsound(src, SFX_LAW, 50, FALSE)
/mob/living/basic/bot/secbot/proc/post_stun(mob/living/carbon/current_target, harm = FALSE)
flick("[base_icon_state]-c", src)
var/threat = 5 || ai_controller.blackboard[BB_CURRENT_CRIMINAL_ASSESSMENT]
if(security_mode_flags & SECBOT_DECLARE_ARRESTS)
var/area/location = get_area(src)
speak("[security_mode_flags & SECBOT_HANDCUFF_TARGET ? "Arresting" : "Detaining"] level [threat] scumbag [RUNECHAT_BOLD("[current_target]")] in [location].", radio_channel)
payment_check(current_target)
update_bot_mode(new_mode = BOT_PREP_ARREST)
/mob/living/basic/bot/secbot/explode()
var/atom/drop_location = drop_location()
retrieve_secbot_drops(drop_location)
new /obj/effect/decal/cleanable/blood/oil(loc)
return ..()
/mob/living/basic/bot/secbot/proc/retrieve_secbot_drops(atom/drop_location)
var/obj/item/bot_assembly/secbot/secbot_assembly = new(drop_location)
secbot_assembly.build_step = ASSEMBLY_FIRST_STEP
secbot_assembly.add_overlay("hs_hole")
secbot_assembly.created_name = name
new /obj/item/assembly/prox_sensor(drop_location)
drop_part(weapon, drop_location)
/mob/living/basic/bot/secbot/proc/on_entered(datum/source, atom/movable/to_be_tripped)
SIGNAL_HANDLER
var/mob/living/possible_target = ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(!has_gravity() || !ismob(to_be_tripped) || !possible_target)
return
var/mob/living/carbon/tripped_mob = to_be_tripped
if(istype(tripped_mob) && !in_range(src, possible_target))
knockOver(tripped_mob)
/// Returns true if the current target is unable to pay to be detained/arrested
/mob/living/basic/bot/secbot/proc/payment_check(mob/living/carbon/human/human_target)
var/fair_market_price = (security_mode_flags & SECBOT_HANDCUFF_TARGET) ? price_arrest : price_detain
if(fair_market_price <= 0)
return FALSE
if(!ishuman(human_target))
return FALSE
var/obj/item/card/id/target_id = human_target.get_idcard()
if(!target_id)
say("Unable to pay fine: No ID card found.")
return TRUE
var/datum/bank_account/insurance = target_id.registered_account
if(!insurance)
say("Unable to pay fine: No bank account found.")
return TRUE
if(!insurance.adjust_money(-fair_market_price, "Securitron fine"))
say("Unable to pay fine: Not enough funds in account.")
return TRUE
SSeconomy.get_dep_account(payment_department)?.adjust_money(fair_market_price)
say("Fine paid: Thank you for your compliance. Your account been charged [fair_market_price] [MONEY_NAME].")
return FALSE
/mob/living/basic/bot/secbot/generate_speak_list()
var/static/list/secbot_lines = list(
BEEPSKY_VOICED_CRIMINAL_DETECTED = 'sound/mobs/non-humanoids/beepsky/criminal.ogg',
BEEPSKY_VOICED_FREEZE = 'sound/mobs/non-humanoids/beepsky/freeze.ogg',
BEEPSKY_VOICED_JUSTICE = 'sound/mobs/non-humanoids/beepsky/justice.ogg',
BEEPSKY_VOICED_YOUR_MOVE = 'sound/mobs/non-humanoids/beepsky/creep.ogg',
BEEPSKY_VOICED_I_AM_THE_LAW = 'sound/mobs/non-humanoids/beepsky/iamthelaw.ogg',
BEEPSKY_VOICED_SECURE_DAY = 'sound/mobs/non-humanoids/beepsky/secureday.ogg',
)
return secbot_lines
/mob/living/basic/bot/secbot/proc/judgement_criteria()
var/final = FALSE
if(bot_access_flags & BOT_COVER_EMAGGED)
final |= JUDGE_EMAGGED
if(security_mode_flags & SECBOT_CHECK_IDS)
final |= JUDGE_IDCHECK
if(security_mode_flags & SECBOT_CHECK_RECORDS)
final |= JUDGE_RECORDCHECK
if(security_mode_flags & SECBOT_CHECK_WEAPONS)
final |= JUDGE_WEAPONCHECK
if(security_mode_flags & SECBOT_SABOTEUR_AFFECTED)
final |= JUDGE_CHILLOUT
return final
/mob/living/basic/bot/secbot/proc/add_arrest_component()
AddComponent(/datum/component/stun_n_cuff,\
stun_sound = stun_sound,\
post_stun_callback = CALLBACK(src, PROC_REF(post_stun)),\
post_arrest_callback = CALLBACK(src, PROC_REF(post_arrest)),\
handcuff_type = cuff_type,\
)
@@ -0,0 +1,12 @@
/datum/action/cooldown/mob_cooldown/bot/sword
name = "Energy Sword"
desc = "Turn your sword off/on!"
button_icon = 'icons/obj/weapons/transforming_energy.dmi'
button_icon_state = "e_sword_on"
cooldown_time = 0 SECONDS
click_to_activate = FALSE
/datum/action/cooldown/mob_cooldown/bot/sword/Activate(mob/living/firer, atom/target)
var/obj/item/melee/energy/sword/saber/my_sword = locate() in owner
INVOKE_ASYNC(my_sword, TYPE_PROC_REF(/obj/item, attack_self), owner)
return TRUE
@@ -0,0 +1,91 @@
/datum/ai_controller/basic_controller/bot/secbot
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/secbot,
BB_UNREACHABLE_LIST_COOLDOWN = 1 MINUTES,
BB_ALWAYS_IGNORE_FACTION = TRUE,
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/arrest_target,
/datum/ai_planning_subtree/find_patrol_beacon,
)
reset_keys = list(
BB_BEACON_TARGET,
BB_PREVIOUS_BEACON_TARGET,
BB_BOT_SUMMON_TARGET,
)
/datum/targeting_strategy/basic/secbot/can_attack(mob/living/living_mob, atom/the_target, vision_range)
var/datum/ai_controller/basic_controller/bot/my_controller = living_mob.ai_controller
if(isnull(my_controller))
return FALSE
if(!ishuman(the_target) || LAZYACCESS(my_controller.blackboard[BB_TEMPORARY_IGNORE_LIST], the_target))
return FALSE
var/mob/living/carbon/human/human_target = the_target
if(human_target.handcuffed || human_target.stat != CONSCIOUS)
return FALSE
if(locate(human_target) in my_controller.blackboard[BB_BASIC_MOB_RETALIATE_LIST])
return TRUE
var/mob/living/basic/bot/secbot/my_bot = living_mob
if(human_target.IsParalyzed() && !(my_bot.security_mode_flags & SECBOT_HANDCUFF_TARGET))
return FALSE
var/assess_flags = my_bot.judgement_criteria()
var/assessed_threat = human_target.assess_threat(assess_flags)
if(assessed_threat > THREAT_ASSESS_DANGEROUS)
my_controller.set_blackboard_key(BB_CURRENT_CRIMINAL_ASSESSMENT, assessed_threat)
return (assessed_threat > THREAT_ASSESS_DANGEROUS)
/datum/ai_controller/basic_controller/bot/secbot/TryPossessPawn(atom/new_pawn)
. = ..()
if(. & AI_CONTROLLER_INCOMPATIBLE)
return
RegisterSignal(new_pawn, COMSIG_AI_BLACKBOARD_KEY_SET(BB_BASIC_MOB_CURRENT_TARGET), PROC_REF(on_target_set))
RegisterSignal(new_pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(BB_BASIC_MOB_CURRENT_TARGET), PROC_REF(on_clear_target))
/datum/ai_controller/basic_controller/bot/secbot/proc/on_target_set()
SIGNAL_HANDLER
var/datum/action/cooldown/bot_announcement/announcement = blackboard[BB_ANNOUNCE_ABILITY]
var/threat_level = 5 || blackboard[BB_CURRENT_CRIMINAL_ASSESSMENT]
INVOKE_ASYNC(announcement, TYPE_PROC_REF(/datum/action/cooldown/bot_announcement, announce), "Level [threat_level] infraction alert!")
playsound(pawn, pick(
'sound/mobs/non-humanoids/beepsky/criminal.ogg',
'sound/mobs/non-humanoids/beepsky/justice.ogg',
'sound/mobs/non-humanoids/beepsky/freeze.ogg',
), 50, FALSE)
var/mob/living/basic/bot/secbot/my_bot = pawn
my_bot.update_bot_mode(new_mode = BOT_HUNT)
/datum/ai_controller/basic_controller/bot/secbot/proc/on_clear_target()
SIGNAL_HANDLER
clear_blackboard_key(BB_CURRENT_CRIMINAL_ASSESSMENT)
/datum/ai_planning_subtree/arrest_target
///what behavior do we use when arresting?
var/datum/ai_behavior/arrest_behavior = /datum/ai_behavior/basic_melee_attack/interact_once/bot
/datum/ai_planning_subtree/arrest_target/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/mob/living/carbon/my_target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(QDELETED(my_target) || !istype(my_target) || my_target.handcuffed)
controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET)
return
var/mob/living/basic/bot/secbot/my_bot = controller.pawn
var/bot_flags = my_bot.security_mode_flags
if(my_target.IsParalyzed() && !(bot_flags & SECBOT_HANDCUFF_TARGET))
controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET)
return
controller.queue_behavior(arrest_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/basic_melee_attack/interact_once/bot
movement_behavior = /datum/ai_movement/basic_avoidance
/datum/ai_behavior/basic_melee_attack/interact_once/bot/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
var/mob/living/carbon/human/human_target = controller.blackboard[target_key]
if(!isnull(human_target) && human_target.handcuffed)
controller.remove_from_blackboard_lazylist_key(BB_BASIC_MOB_RETALIATE_LIST, human_target)
return ..()
@@ -0,0 +1,65 @@
/mob/living/basic/bot/secbot/beepsky
name = "Commander Beep O'sky"
desc = "It's Commander Beep O'sky! Officially the superior officer of all bots on station, Beepsky remains as humble and dedicated to the law as the day he was first fabricated."
bot_mode_flags = BOT_MODE_ON | BOT_MODE_AUTOPATROL | BOT_MODE_REMOTE_ENABLED
commissioned = TRUE
/mob/living/basic/bot/secbot/beepsky/officer
name = "Officer Beepsky"
desc = "It's Officer Beepsky! Powered by a potato and a shot of whiskey, and with a sturdier reinforced chassis, too."
health = 45
/mob/living/basic/bot/secbot/beepsky/officer/Initialize(mapload)
. = ..()
// Beepsky hates people scanning them
RegisterSignal(src, COMSIG_MOVABLE_SPY_STEALING, PROC_REF(on_spy_scan))
/mob/living/basic/bot/secbot/beepsky/officer/proc/on_spy_scan(datum/source, mob/user)
SIGNAL_HANDLER
ai_controller?.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, user)
/mob/living/basic/bot/secbot/beepsky/ofitser
name = "Prison Ofitser"
desc = "Powered by the tears and sweat of laborers."
bot_mode_flags = ~(BOT_MODE_CAN_BE_SAPIENT|BOT_MODE_AUTOPATROL)
/mob/living/basic/bot/secbot/beepsky/armsky
name = "Sergeant-At-Armsky"
desc = "It's Sergeant-At-Armsky! He's a disgruntled assistant to the warden that would probably shoot you if he had hands."
health = 45
bot_mode_flags = ~(BOT_MODE_CAN_BE_SAPIENT|BOT_MODE_AUTOPATROL)
security_mode_flags = SECBOT_DECLARE_ARRESTS | SECBOT_CHECK_IDS | SECBOT_CHECK_RECORDS | SECBOT_CHECK_WEAPONS
/mob/living/basic/bot/secbot/beepsky/jr
name = "Officer Pipsqueak"
desc = "It's Commander Beep O'sky's smaller, just-as aggressive cousin, Pipsqueak."
commissioned = FALSE
/mob/living/basic/bot/secbot/beepsky/jr/Initialize(mapload)
. = ..()
update_transform(0.8)
/mob/living/basic/bot/secbot/pingsky
name = "Officer Pingsky"
desc = "It's Officer Pingsky! Delegated to satellite guard duty for harbouring anti-human sentiment."
light_color = "#62baf5"
radio_channel = RADIO_CHANNEL_AI_PRIVATE
bot_mode_flags = ~(BOT_MODE_CAN_BE_SAPIENT|BOT_MODE_AUTOPATROL)
security_mode_flags = SECBOT_DECLARE_ARRESTS | SECBOT_CHECK_IDS | SECBOT_CHECK_RECORDS
/mob/living/basic/bot/secbot/genesky
name = "Officer Genesky"
desc = "A beefy variant of the standard securitron model."
health = 50
faction = list(FACTION_NANOTRASEN_PRIVATE)
bot_mode_flags = BOT_MODE_ON
bot_access_flags = BOT_COVER_LOCKED | BOT_COVER_EMAGGED
/mob/living/basic/bot/secbot/beepsky/explode()
var/atom/current_location = drop_location()
new /obj/item/stock_parts/power_store/cell/potato(current_location)
var/obj/item/reagent_containers/cup/glass/drinkingglass/shotglass/drinking_oil = new(current_location)
drinking_oil.reagents.add_reagent(/datum/reagent/consumable/ethanol/whiskey, 15)
return ..()
@@ -0,0 +1,99 @@
/mob/living/basic/bot/secbot/grievous //This bot is powerful. If you managed to get 4 eswords somehow, you deserve this horror. Emag him for best results.
name = "General Beepsky"
desc = "Is that a secbot with four eswords in its arms...?"
icon = 'icons/mob/silicon/aibots.dmi'
icon_state = "grievous"
base_icon_state = "grievous"
health = 150
maxHealth = 150
ai_controller = /datum/ai_controller/basic_controller/bot/secbot/super_beepsky
baton_type = /obj/item/melee/energy/sword/saber
speed = 4 //he's a fast fucker
///chance we block bullets
var/block_chance = 50
///is our sword currently active?
var/sword_active = FALSE
/mob/living/basic/bot/secbot/grievous/Initialize(mapload)
. = ..()
RegisterSignal(weapon, COMSIG_TRANSFORMING_ON_TRANSFORM, PROC_REF(on_weapon_transform))
var/static/list/abilities = list(
/datum/action/cooldown/mob_cooldown/bot/sword = null,
)
grant_actions_by_list(abilities)
INVOKE_ASYNC(weapon, TYPE_PROC_REF(/obj/item, attack_self), src)
RegisterSignal(src, COMSIG_ATOM_PRE_BULLET_ACT, PROC_REF(block_bullets))
/mob/living/basic/bot/secbot/grievous/check_block(atom/hit_by, damage, attack_text = "the attack", attack_type = MELEE_ATTACK, armour_penetration = 0, damage_type = BRUTE)
. = ..()
if(. & FAILED_BLOCK)
return .
return (sword_active && prob(block_chance) ? SUCCESSFUL_BLOCK : FAILED_BLOCK)
/mob/living/basic/bot/secbot/grievous/proc/on_weapon_transform(obj/item/source, mob/user, active)
SIGNAL_HANDLER
if(active)
visible_message(span_warning("[src] ignites his energy swords!"))
sword_active = active
update_icon_state()
/mob/living/basic/bot/secbot/grievous/add_arrest_component() //i dont think we'll be arresting people...
return
/mob/living/basic/bot/secbot/grievous/proc/block_bullets(datum/source, obj/projectile/hitting_projectile)
SIGNAL_HANDLER
if(stat != CONSCIOUS )
return NONE
if(!sword_active || !prob(block_chance))
return NONE
visible_message(span_warning("[source] deflects [hitting_projectile] with its energy swords!"))
playsound(source, 'sound/items/weapons/blade1.ogg', 50, TRUE)
return COMPONENT_BULLET_BLOCKED
/mob/living/basic/bot/secbot/grievous/on_entered(datum/source, atom/movable/movable_target)
. = ..()
if(!ismob(movable_target) || !ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] == movable_target)
return
visible_message(span_warning("[src] flails his swords and cuts [movable_target]!"))
playsound(src, 'sound/mobs/non-humanoids/beepsky/beepskyspinsabre.ogg' , 100, TRUE, -1)
INVOKE_ASYNC(src, TYPE_PROC_REF(/mob, ClickOn), movable_target)
/mob/living/basic/bot/secbot/grievous/update_icon_state()
. = ..()
icon_state = "[base_icon_state][ sword_active ? "-c" : ""]"
/mob/living/basic/bot/secbot/grievous/Destroy()
QDEL_NULL(weapon)
return ..()
/mob/living/basic/bot/secbot/grievous/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(.)
return
INVOKE_ASYNC(weapon, TYPE_PROC_REF(/obj/item, melee_attack_chain), src, target)
playsound(src, 'sound/items/weapons/blade1.ogg', 50, TRUE, -1)
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/mob/living/basic/bot/secbot/grievous/explode()
var/atom/drop_location = drop_location()
//Parent is dropping the weapon, so let's drop 3 more to make up for it.
for(var/i in 0 to 3)
drop_part(baton_type, drop_location)
return ..()
/mob/living/basic/bot/secbot/grievous/toy //A toy version of general beepsky!
name = "Genewul Bweepskee"
desc = "An adorable looking secbot with four toy swords taped to its arms"
health = 50
maxHealth = 50
block_chance = 0
baton_type = /obj/item/toy/sword
@@ -0,0 +1,16 @@
/datum/ai_controller/basic_controller/bot/secbot/super_beepsky
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/find_patrol_beacon,
)
/datum/ai_controller/basic_controller/bot/secbot/super_beepsky/on_target_set()
. = ..()
var/mob/living/basic/bot/secbot/grievous/super_beeps = pawn
if(!super_beeps.sword_active)
INVOKE_ASYNC(super_beeps.weapon, TYPE_PROC_REF(/obj/item, attack_self), super_beeps)
super_beeps.visible_message("<b>[super_beeps]</b> points at [blackboard[BB_BASIC_MOB_CURRENT_TARGET]]!")
+1 -1
View File
@@ -57,7 +57,7 @@
if (!istype(attacker))
return
for (var/mob/living/basic/clown/harbringer in oview(src, 7))
harbringer.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker)
harbringer.ai_controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time)
/mob/living/basic/clown/melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE)
if(!istype(target, /obj/item/food/grown/banana/bunch))
@@ -58,7 +58,7 @@
if(equipping.pulledby)
equipping.pulledby.stop_pulling()
equipping.screen_loc = null // will get moved if inventory is visible
hud_used?.update_inventory_slot(slot)
equipping.forceMove(src) //This has to come before has_equipped is called.
SET_PLANE_EXPLICIT(equipping, ABOVE_HUD_PLANE, src)
@@ -22,18 +22,13 @@
update_inv_internal_storage()
/mob/living/basic/drone/proc/update_inv_internal_storage()
if(internal_storage && client && hud_used?.hud_shown)
internal_storage.screen_loc = ui_drone_storage
client.screen += internal_storage
hud_used?.update_inventory_slot(ITEM_SLOT_DEX_STORAGE)
/mob/living/basic/drone/update_worn_head()
remove_overlay(DRONE_HEAD_LAYER)
hud_used?.update_inventory_slot(ITEM_SLOT_HEAD)
if(head)
if(client && hud_used?.hud_shown)
head.screen_loc = ui_drone_head
client.screen += head
var/used_head_icon = 'icons/mob/clothing/head/utility.dmi'
if(istype(head, /obj/item/clothing/mask))
used_head_icon = 'icons/mob/clothing/mask.dmi'
@@ -111,18 +111,18 @@
/mob/living/basic/bee/early_melee_attack(atom/target, list/modifiers)
. = ..()
if(!.)
return FALSE
if(.)
return
if(istype(target, /obj/machinery/hydroponics))
var/obj/machinery/hydroponics/hydro = target
pollinate(hydro)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
if(istype(target, /obj/structure/beebox))
var/obj/structure/beebox/hive = target
handle_habitation(hive)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/mob/living/basic/bee/proc/handle_habitation(obj/structure/beebox/hive)
if(hive == beehome) //if its our home, we enter or exit it
@@ -119,6 +119,10 @@
whinny_angrily()
/mob/living/basic/pony/get_shove_flags(mob/living/shover, obj/item/weapon)
. = ..()
. |= SHOVE_CAN_STAGGER
/datum/ai_controller/basic_controller/pony
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
@@ -25,6 +25,10 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial())
var/use_message = span_holoparasite("You shuffle the deck...")
/// Message sent when it's already used.
var/used_message = span_holoparasite("All the cards seem to be blank now.")
/// Examine description if the creator is unused
var/unused_description = span_holoparasite("You feel beckoned to draw one...")
/// Examine description if the creator is used.
var/used_description = span_holoparasite("They seem rather uninteresting.")
/// Failure message if no ghost picks the holopara.
var/failure_message = span_boldholoparasite("..And draw a card! It's... blank? Maybe you should try again later.")
/// Failure message if we don't allow lings.
@@ -113,6 +117,13 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial())
to_chat(user, failure_message)
used = FALSE
/obj/item/guardian_creator/examine(mob/user)
. = ..()
if(used)
. += span_holoparasite(used_description)
else
. += span_holoparasite(unused_description)
/obj/item/guardian_creator/proc/on_reimbursed(datum/source)
SIGNAL_HANDLER
was_refunded = TRUE
@@ -185,6 +196,8 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial())
allow_changeling = FALSE
use_message = span_holoparasite("You start to power on the injector...")
used_message = span_holoparasite("The injector has already been used.")
unused_description = span_holoparasite("Its vial is filled with a violent storm of color.")
used_description = span_holoparasite("Nothing seems to be loaded in the injector.")
failure_message = span_boldholoparasite("...ERROR. BOOT SEQUENCE ABORTED. AI FAILED TO INTIALIZE. PLEASE CONTACT SUPPORT OR TRY AGAIN LATER.")
ling_failure = span_boldholoparasite("The holoparasites recoil in horror. They want nothing to do with a creature like you.")
success_message = span_holoparasite("<b>%GUARDIAN</b> is now online!")
@@ -198,6 +211,8 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial())
theme = GUARDIAN_THEME_CARP
use_message = span_holoparasite("You put the fishsticks in your mouth...")
used_message = span_holoparasite("Someone's already taken a bite out of these fishsticks! Ew.")
unused_description = span_holoparasite("They look hot and ready to eat!")
used_description = span_holoparasite("They look soggy and old...")
failure_message = span_boldholoparasite("You couldn't catch any carp spirits from the seas of Lake Carp. Maybe there are none, maybe you fucked up.")
ling_failure = span_boldholoparasite("Carp'sie seems to not have taken you as the chosen one. Maybe it's because of your horrifying origin.")
success_message = span_holoparasite("<b>%GUARDIAN</b> has been caught!")
@@ -212,6 +227,8 @@ GLOBAL_LIST_INIT(guardian_radial_images, setup_guardian_radial())
theme = GUARDIAN_THEME_MINER
use_message = span_holoparasite("You pierce your skin with the shard...")
used_message = span_holoparasite("This shard seems to have lost all its power...")
unused_description = span_holoparasite("It glows with an otherwordly power...")
used_description = span_holoparasite("It looks dull, with dried blood on the tip.")
failure_message = span_boldholoparasite("The shard hasn't reacted at all. Maybe try again later...")
ling_failure = span_boldholoparasite("The power of the shard seems to not react with your horrifying, mutated body.")
success_message = span_holoparasite("<b>%GUARDIAN</b> has appeared!")
@@ -92,7 +92,7 @@
if(equipping.pulledby)
equipping.pulledby.stop_pulling()
equipping.screen_loc = null // will get moved if inventory is visible
hud_used?.update_inventory_slot(slot)
equipping.forceMove(src)
SET_PLANE_EXPLICIT(equipping, ABOVE_HUD_PLANE, src)
@@ -106,10 +106,7 @@
return ITEM_SLOT_DEX_STORAGE
/mob/living/basic/guardian/dextrous/proc/update_inv_internal_storage()
if(isnull(internal_storage) || isnull(client) || !hud_used?.hud_shown)
return
internal_storage.screen_loc = ui_back
client.screen += internal_storage
hud_used?.update_inventory_slot(ITEM_SLOT_DEX_STORAGE)
/mob/living/basic/guardian/dextrous/regenerate_icons()
update_inv_internal_storage()
@@ -33,20 +33,15 @@
. = ..()
if (QDELETED(src))
return
RegisterSignal(summoner, COMSIG_LIVING_IGNITED, PROC_REF(on_summoner_ignited))
ADD_TRAIT(summoner, TRAIT_NOFIRE, REF(src))
RegisterSignal(summoner, COMSIG_LIVING_LIFE, PROC_REF(on_summoner_life))
/mob/living/basic/guardian/gaseous/cut_summoner(different_person)
if (!isnull(summoner))
UnregisterSignal(summoner, list(COMSIG_LIVING_IGNITED, COMSIG_LIVING_LIFE))
REMOVE_TRAIT(summoner, TRAIT_NOFIRE, REF(src))
UnregisterSignal(summoner, COMSIG_LIVING_LIFE)
return ..()
/// Prevent our summoner from being on fire
/mob/living/basic/guardian/gaseous/proc/on_summoner_ignited(mob/living/source)
SIGNAL_HANDLER
source.extinguish_mob()
source.set_fire_stacks(0, remove_wet_stacks = FALSE)
/// Maintain our summoner at a stable body temperature
/mob/living/basic/guardian/gaseous/proc/on_summoner_life(mob/living/source, seconds_per_tick)
SIGNAL_HANDLER
@@ -112,6 +112,8 @@
/// Validate whether we can teleport this object
/datum/action/cooldown/mob_cooldown/guardian_bluespace_beacon/proc/can_teleport(mob/living/source, atom/movable/target)
if(!istype(target)) // Turfs
return FALSE
if (isnull(beacon))
source.balloon_alert(source, "no beacon!")
return FALSE
@@ -126,7 +128,7 @@
if (target.anchored)
target.balloon_alert(source, "it won't budge!")
return FALSE
if(beacon.z != target.z)
if((beacon.z != target.z) && !(target.z in SSmapping.get_connected_levels(beacon.z)))
target.balloon_alert(source, "too far from beacon!")
return FALSE
return TRUE
@@ -51,22 +51,22 @@
/mob/living/basic/mining/ice_whelp/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(!.)
return FALSE
if(.)
return
if(istype(target, /obj/structure/flora/rock/icy))
create_sculpture(target)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
if(!istype(target, type))
return TRUE
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
var/mob/living/victim = target
if(victim.stat != DEAD)
return TRUE
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
cannibalize_victim(victim)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/// Carve a stone into a beautiful self-portrait
/mob/living/basic/mining/ice_whelp/proc/create_sculpture(atom/target)
@@ -72,12 +72,16 @@
appearance = original.appearance
setDir(original.dir)
maxHealth = hp
updatehealth() // re-cap health to new value
if (isnull(hp))
maxHealth = hp
updatehealth() // re-cap health to new value
melee_damage_lower = damage
melee_damage_upper = damage
multiply_chance = replicate
if (isnull(damage))
melee_damage_lower = damage
melee_damage_upper = damage
if (isnull(replicate))
multiply_chance = replicate
remove_faction(FACTION_NEUTRAL)
transform = initial(transform)
@@ -85,19 +85,19 @@
/mob/living/basic/seedling/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(!.)
return FALSE
if(.)
return
if(istype(target, /obj/machinery/hydroponics))
treat_hydro_tray(target)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
if(isnull(held_can))
return TRUE
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 FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
///seedlings can water trays, remove weeds, or remove dead plants
@@ -6,8 +6,8 @@
icon_living = "bileworm"
icon_dead = "bileworm_dead"
mob_biotypes = MOB_ORGANIC|MOB_BUG|MOB_MINING
maxHealth = 100
health = 100
maxHealth = 110
health = 110
verb_say = "spittles"
verb_ask = "spittles questioningly"
verb_exclaim = "splutters and gurgles"
@@ -30,10 +30,16 @@
ai_controller = /datum/ai_controller/basic_controller/bileworm
///which action this mob will be given, subtypes have different attacks
var/attack_action_path = /datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm
///which, if at all, mob this evolves into at the 30 min mark
/// Which action this mob will be given, subtypes have different attacks
var/attack_action_path = /datum/action/cooldown/mob_cooldown/bileworm_spew
/// Which, if at all, mob this evolves into at the 30 min mark
var/evolve_path = /mob/living/basic/mining/bileworm/vileworm
/// Emissive icon_state
var/emissive_state = "bileworm_e"
/// Icon file used for the jumping animation
var/icon_jump = 'icons/mob/simple/lavaland/bileworm_jump.dmi'
/// Are we currently in the middle of a jump?
var/jumping = FALSE
/mob/living/basic/mining/bileworm/Initialize(mapload)
. = ..()
@@ -42,11 +48,7 @@
if(ispath(evolve_path))
AddComponent(/datum/component/evolutionary_leap, 30 MINUTES, evolve_path)
AddElement(/datum/element/content_barfer)
//setup mob abilities
//well, one of them has to start on infinite cooldown
var/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/spew_bile = new(src)
var/datum/action/cooldown/mob_cooldown/spew_bile = new attack_action_path(src)
spew_bile.Grant(src)
spew_bile.StartCooldownSelf(INFINITY)
ai_controller?.set_blackboard_key(BB_BILEWORM_SPEW_BILE, spew_bile)
@@ -61,5 +63,10 @@
/mob/living/basic/mining/bileworm/update_overlays()
. = ..()
if (stat != DEAD)
. += emissive_appearance(icon, "[icon_living]_e", src)
if (stat != DEAD && !jumping)
. += emissive_appearance(icon, emissive_state, src)
/mob/living/basic/mining/bileworm/on_attacked(datum/source, atom/attacker, attack_flags)
. = ..()
if (!(attack_flags & (ATTACKER_STAMINA_ATTACK | ATTACKER_SHOVING)))
ai_controller?.set_blackboard_key(BB_BILEWORM_SCARED, TRUE)
@@ -2,21 +2,70 @@
name = "Resurface"
desc = "Burrow underground, and then move to a new location near your target. Must spew bile to refresh."
shared_cooldown = MOB_SHARED_COOLDOWN_1 | MOB_SHARED_COOLDOWN_2
/// Damage tracker var for bileworms
var/jump_damaged = FALSE
/// How long does the jump take to perform?
var/jump_length = 1 SECONDS
/// How long do we get stunned for if we fail a jump?
var/jump_stun = 2.4 SECONDS
/datum/action/cooldown/mob_cooldown/resurface/Grant(mob/granted_to)
. = ..()
owner?.AddElement(/datum/element/relay_attackers)
/datum/action/cooldown/mob_cooldown/resurface/Activate(atom/target_atom)
StartCooldownSelf(INFINITY)
StartCooldownOthers(INFINITY)
burrow(owner, target_atom)
//spew now off cooldown shortly
StartCooldownOthers(1.5 SECONDS)
/datum/action/cooldown/mob_cooldown/resurface/proc/burrow(mob/living/burrower, atom/target)
/// Amount of frames in the jump animation
#define BILEWORM_JUMP_FRAMES 14
/datum/action/cooldown/mob_cooldown/resurface/proc/burrow(mob/living/burrower, atom/target, force = FALSE)
var/turf/unburrow_turf = get_unburrow_turf(burrower, target)
if(!unburrow_turf) // means all the turfs nearby are station turfs or something, not lavaland
if (!unburrow_turf) // means all the turfs nearby are station turfs or something, not lavaland
to_chat(burrower, span_warning("Couldn't burrow anywhere near the target!"))
if(burrower.ai_controller?.ai_status == AI_STATUS_ON)
//this is a valid reason to give up on a target
burrower.ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET)
return
if (istype(burrower, /mob/living/basic/mining/bileworm) && !force)
var/mob/living/basic/mining/bileworm/worm = burrower
new /obj/effect/temp_visual/mook_dust(get_turf(worm))
var/old_icon_state = worm.icon_state
worm.icon_state = null
// Get rid of the base emissive
worm.jumping = TRUE
worm.update_appearance(UPDATE_OVERLAYS)
var/atom/movable/visual = worm.flick_overlay_view(mutable_appearance(worm.icon_jump, "[worm.icon_living]_jump_1"), jump_length)
visual.vis_flags |= VIS_INHERIT_ID // So you can attack it
var/atom/movable/emissive_visual = worm.flick_overlay_view(emissive_appearance(worm.icon_jump, "[worm.emissive_state]_jump_1", worm), jump_length)
// We have to manually animate these to get around BYOND icon_state animations happening in world time and not per-object
for (var/i in 2 to BILEWORM_JUMP_FRAMES)
animate(visual, time = jump_length / BILEWORM_JUMP_FRAMES, icon_state = "[worm.icon_living]_jump_[i]", flags = ANIMATION_CONTINUE)
animate(emissive_visual, time = jump_length / BILEWORM_JUMP_FRAMES, icon_state = "[worm.emissive_state]_jump_[i]", flags = ANIMATION_CONTINUE)
jump_damaged = FALSE
RegisterSignal(worm, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked))
// Not in an if check direclty to reduce duplicate code
var/jump_result = do_after(worm, jump_length, worm, hidden = TRUE, extra_checks = CALLBACK(src, PROC_REF(damage_check)))
UnregisterSignal(worm, COMSIG_ATOM_WAS_ATTACKED)
if (worm.icon_state == null)
worm.icon_state = old_icon_state
worm.jumping = FALSE
worm.update_appearance(UPDATE_OVERLAYS)
if (!jump_result && jump_stun)
if (!QDELETED(visual))
QDEL_NULL(visual)
QDEL_NULL(emissive_visual)
worm.flick_overlay_view(mutable_appearance('icons/effects/effects.dmi', "dazed"), jump_stun)
worm.Stun(jump_stun)
addtimer(CALLBACK(src, PROC_REF(burrow_again), burrower, target), jump_stun)
return
burrower.ai_controller?.set_blackboard_key(BB_BILEWORM_SCARED, FALSE)
playsound(burrower, 'sound/effects/break_stone.ogg', 50, TRUE)
new /obj/effect/temp_visual/mook_dust(get_turf(burrower))
ADD_TRAIT(burrower, TRAIT_GODMODE, REF(src))
@@ -29,59 +78,189 @@
REMOVE_TRAIT(burrower, TRAIT_GODMODE, REF(src))
burrower.RemoveInvisibility(type)
#undef BILEWORM_JUMP_FRAMES
/datum/action/cooldown/mob_cooldown/resurface/proc/burrow_again(mob/living/burrower, atom/target)
if (!QDELETED(burrower) && !burrower.stat)
// Burrow immediatelly after being stunned out of the first jump to avoid chainstuns
burrow(burrower, target, force = TRUE)
/datum/action/cooldown/mob_cooldown/resurface/proc/on_attacked(datum/source, atom/attacker, attack_flags)
SIGNAL_HANDLER
if (!(attack_flags & (ATTACKER_STAMINA_ATTACK | ATTACKER_SHOVING)) && jump_stun)
jump_damaged = TRUE
/datum/action/cooldown/mob_cooldown/resurface/proc/damage_check()
return !jump_damaged
/datum/action/cooldown/mob_cooldown/resurface/proc/get_unburrow_turf(mob/living/burrower, atom/target)
//we want the worm to try guaranteeing a hit on a living target if it thinks it can
var/cardinal_only = FALSE
if(isliving(target))
var/mob/living/living_target = target
if(living_target.stat >= UNCONSCIOUS)
cardinal_only = TRUE
var/list/potential_turfs = shuffle(oview(5, target))//get in view, shuffle
var/list/fallback_turfs = list()
for(var/turf/open/misc/chosen_one in potential_turfs)//first turf that counts as ground
if(cardinal_only && !(get_dir(chosen_one, target) in GLOB.cardinals))
fallback_turfs.Add(chosen_one)
continue
var/list/potential_turfs = shuffle(oview(5, target)) // get in view, shuffle
for(var/turf/open/misc/chosen_one in potential_turfs) // first turf that counts as ground
return chosen_one
//even if a worm can't execute someone in crit, it should not fail if it has SOMETHING to move to.
if(fallback_turfs.len)
return pick(fallback_turfs)
/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm
/datum/action/cooldown/mob_cooldown/bileworm_spew
name = "Spew Bile"
desc = "Spews bile everywhere. Must resurface after use to refresh."
projectile_type = /obj/projectile/bileworm_acid
projectile_sound = 'sound/mobs/non-humanoids/bileworm/bileworm_spit.ogg'
desc = "Spew a barrage of bile globs."
shared_cooldown = MOB_SHARED_COOLDOWN_1 | MOB_SHARED_COOLDOWN_2
cooldown_time = 3 SECONDS
/// Sound played when firing a projectile
var/projectile_sound = 'sound/mobs/non-humanoids/bileworm/bileworm_spit.ogg'
/// How many additional projectiles to shoot around the target?
var/additional_shots = 4
/// Delay between each shot
var/shot_delay = 0.15 SECONDS
/// Effect to spawn
var/obj/effect/bileworm_acid/acid_type = /obj/effect/bileworm_acid
/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/Activate(atom/target_atom)
/datum/action/cooldown/mob_cooldown/bileworm_spew/Activate(atom/target_atom)
StartCooldownSelf(INFINITY)
attack_sequence(owner, target_atom)
//resurface now off cooldown shortly
StartCooldownOthers(2.5 SECONDS)
StartCooldownSelf()
// Resurface now off cooldown shortly
StartCooldownOthers(2 SECONDS) // Enough time for a mark + detonation combo, or 3 shots with a PKA
/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/attack_sequence(mob/living/firer, atom/target)
fire_in_directions(firer, target, GLOB.cardinals)
SLEEP_CHECK_DEATH(0.5 SECONDS, firer)
fire_in_directions(firer, target, GLOB.diagonals)
/datum/action/cooldown/mob_cooldown/bileworm_spew/proc/attack_sequence(mob/living/firer, atom/target)
new acid_type(firer, target)
playsound(firer, projectile_sound, 70)
var/list/all_dirs = GLOB.alldirs.Copy()
var/turf/hit_turf = get_turf(target)
// One guaranteed to hit target's turf, the rest hit randomly around them
for (var/i in 1 to additional_shots)
if (!length(all_dirs))
return
/obj/projectile/bileworm_acid
SLEEP_CHECK_DEATH(shot_delay, firer)
if (hit_turf != target.loc)
all_dirs = list(NONE) + GLOB.alldirs // Refresh potential target turfs if they've moved
var/turf/target_turf = null
// NODE drones get a much more focused barrage sent at them
if (istype(target, /mob/living/basic/node_drone) && prob(30))
target_turf = get_turf(target)
else
var/target_dir = pick_n_take(all_dirs)
target_turf = target_dir ? get_step(target, target_dir) : get_turf(target)
if (target_turf == firer.loc) // Don't hit ourselves
var/target_dir = pick_n_take(all_dirs)
target_turf = target_dir ? get_step(target, target_dir) : get_turf(target)
new acid_type(firer, target_turf)
if (i % 2 == 0)
playsound(firer, projectile_sound, 70)
/obj/effect/bileworm_acid
name = "acidic bile"
icon_state = "neurotoxin"
hitsound = 'sound/items/weapons/sear.ogg'
damage = 20
speed = 0.5
range = 20
jitter = 3 SECONDS
stutter = 3 SECONDS
damage_type = BURN
pass_flags = PASSTABLE
icon = 'icons/obj/weapons/guns/projectiles.dmi'
icon_state = "bile_glob"
layer = ABOVE_ALL_MOB_LAYER
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
/// Damage dealt to all mobs in the impact turf
var/damage = 20
/// Tiles travelled per second at maximum range
var/speed = 5
/// Maximum range for our speed calculations
var/max_range = 10
/// Warning sign on the turf we're about to hit
var/obj/effect/bileworm_target/target_sign = null
/obj/projectile/bileworm_acid/Initialize(mapload)
/obj/effect/bileworm_acid/Initialize(mapload, atom/new_target)
. = ..()
AddComponent(/datum/component/parriable_projectile)
update_appearance(UPDATE_OVERLAYS)
if (new_target)
fire_at(get_turf(new_target))
/obj/effect/bileworm_acid/Destroy(force)
QDEL_NULL(target_sign)
return ..()
/obj/effect/bileworm_acid/update_overlays()
. = ..()
. += emissive_appearance(icon, icon_state, src, alpha = 20, effect_type = EMISSIVE_BLOOM)
/obj/effect/bileworm_acid/proc/fire_at(turf/target)
if (!istype(target))
target = get_turf(target)
var/turf/start = get_turf(src)
forceMove(target) // Immediately move to the target turf so we can guarantee they can see us
pixel_x = (start.x - target.x) * ICON_SIZE_X
pixel_y = (start.y - target.y) * ICON_SIZE_Y
// Because BYOND's get_dist is taxicab distance
var/travel_dist = sqrt((start.x - target.x) ** 2 + (start.y - target.y) ** 2)
// Slightly scale the speed with distance for fairer gameplay
if (travel_dist < max_range)
speed *= 0.5 + travel_dist / max_range * 0.5
var/travel_time = travel_dist / speed * 1 SECONDS
animate(src, pixel_x = 0, time = travel_time, flags = ANIMATION_PARALLEL, easing = SINE_EASING | EASE_OUT) // Not a perfect arc but looks better
animate(src, pixel_y = 160, time = travel_time / 2, flags = ANIMATION_PARALLEL, easing = QUAD_EASING | EASE_OUT)
animate(pixel_y = 0, time = travel_time / 2, easing = QUAD_EASING | EASE_IN)
animate(src, transform = matrix().Turn(start.x > target.x ? -91 : 91), time = travel_time / 2, flags = ANIMATION_PARALLEL, easing = SINE_EASING | EASE_IN)
animate(transform = matrix().Turn(start.x > target.x ? -180 : 180), time = travel_time / 2, easing = SINE_EASING | EASE_OUT)
target_sign = new(target)
addtimer(CALLBACK(src, PROC_REF(impact), target), travel_time)
/obj/effect/bileworm_acid/proc/impact(turf/target)
var/obj/effect/abstract/particle_holder/impact_particles = new(target, /particles/bile)
QDEL_IN(impact_particles, /particles/bile::lifespan)
var/hit_something = FALSE
for (var/mob/living/victim in target)
if(HAS_TRAIT(target, TRAIT_UNHITTABLE_BY_PROJECTILES))
if(!HAS_TRAIT(target, TRAIT_BLOCKING_PROJECTILES) && isliving(target))
var/mob/living/living_target = target
living_target.block_projectile_effects()
continue
// Doesn't make much sense to use melee for mobs, but its a mining mob so eeeeeh
// Can't use acid either as its mostly for atom armor only
var/blocked = victim.run_armor_check(null, MELEE, armour_penetration = 40, silent = TRUE)
if (blocked >= 100)
continue
hit_something = TRUE
victim.apply_damage(damage, BURN, null, blocked, wound_bonus = CANT_WOUND)
to_chat(victim, span_userdanger("You're hit by [src]!"))
for (var/obj/thing in target)
if (!thing.uses_integrity || !thing.density)
continue
hit_something = TRUE
thing.take_damage(damage, BURN, ACID, sound_effect = FALSE)
if (hit_something)
playsound(target, 'sound/items/weapons/sear.ogg', 50, -1)
qdel(src)
/obj/effect/bileworm_target
icon = 'icons/mob/telegraphing/telegraph.dmi'
icon_state = "projectile_circle"
layer = BELOW_MOB_LAYER
plane = GAME_PLANE
/obj/effect/bileworm_target/Initialize(mapload)
. = ..()
update_appearance(UPDATE_OVERLAYS)
/obj/effect/bileworm_target/update_overlays()
. = ..()
. += emissive_appearance(icon, icon_state, src, alpha = 90, effect_type = EMISSIVE_BLOOM)
/particles/bile
icon = 'icons/effects/particles/goop.dmi'
icon_state = list("goop_1" = 6, "goop_2" = 2, "goop_3" = 1)
width = 100
height = 100
count = 10
spawning = 10
color = "#00ea2b80" //to get 96 alpha
lifespan = 1.5 SECONDS
fade = 1 SECONDS
grow = -0.025
gravity = list(0, 0.15)
position = generator(GEN_SPHERE, 0, 8, NORMAL_RAND)
spin = generator(GEN_NUM, -15, 15, NORMAL_RAND)
velocity = generator(GEN_BOX, list(-2, 2), list(2, 4), NORMAL_RAND)
/datum/action/cooldown/mob_cooldown/devour
name = "Devour"
@@ -1,10 +1,13 @@
/datum/ai_controller/basic_controller/bileworm
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/bileworm,
BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining,
BB_BILEWORM_FLEE_DISTANCE = 3,
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/call_reinforcements/mining,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/bileworm_attack,
/datum/ai_planning_subtree/bileworm_execute,
@@ -20,16 +23,12 @@
return
var/datum/action/cooldown/mob_cooldown/resurface = controller.blackboard[BB_BILEWORM_RESURFACE]
var/datum/action/cooldown/mob_cooldown/bile = controller.blackboard[BB_BILEWORM_SPEW_BILE]
//because one ability is always INFINITY cooldown, this actually works to check which ability should be used
//sometimes it will try to spew bile on infinity cooldown, but that's okay because as soon as resurface is ready it will attempt that
if(resurface?.IsAvailable())
if(resurface?.IsAvailable() && (controller.blackboard[BB_BILEWORM_SCARED] || get_dist(controller.pawn, controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) <= controller.blackboard[BB_BILEWORM_FLEE_DISTANCE]))
controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/and_plan_execute, BB_BILEWORM_RESURFACE, BB_BASIC_MOB_CURRENT_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
var/datum/action/cooldown/mob_cooldown/bile = controller.blackboard[BB_BILEWORM_SPEW_BILE]
if(bile?.IsAvailable())
controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/and_plan_execute, BB_BILEWORM_SPEW_BILE, BB_BASIC_MOB_CURRENT_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING //focus on the fight
@@ -2,7 +2,7 @@
name = "bilehorn"
desc = "Bits of bileworm anatomy rearranged to produce wonderful music, not bile. Keeps the name though, because for an instrument, it is quite vile."
force = 5
icon = 'icons/mob/simple/lavaland/bileworm.dmi'
icon = 'icons/obj/mining_zones/artefacts.dmi'
icon_state = "bilehorn"
allowed_instrument_ids = "bilehorn"
inhand_icon_state = null
@@ -5,7 +5,7 @@
name = "bileworm skin"
desc = "The slushy, squishy and slightly shiny skin of a postmortem bileworm."
singular_name = "bileworm skin piece"
icon = 'icons/mob/simple/lavaland/bileworm.dmi'
icon = 'icons/obj/stack_objects.dmi'
icon_state = "sheet-bileworm"
inhand_icon_state = null
merge_type = /obj/item/stack/sheet/animalhide/bileworm
@@ -4,34 +4,20 @@
icon_state = "vileworm"
icon_living = "vileworm"
icon_dead = "vileworm_dead"
maxHealth = 150
health = 150
maxHealth = 175
health = 175
attack_action_path = /datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/vileworm
attack_action_path = /datum/action/cooldown/mob_cooldown/bileworm_spew/corrupt
evolve_path = null
/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/vileworm
/datum/action/cooldown/mob_cooldown/bileworm_spew/corrupt
name = "Spew Corrupted Bile"
desc = "Spews corrupted bile everywhere. Must resurface after use to refresh."
projectile_type = /obj/projectile/bileworm_acid/vile
desc = "Spew a barrage of corrupted bile globs."
cooldown_time = 2.5 SECONDS
acid_type = /obj/effect/bileworm_acid/corrupt
additional_shots = 6
shot_delay = 0.1 SECONDS
/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/vileworm/Activate(atom/target_atom)
StartCooldownSelf(INFINITY)
attack_sequence(owner, target_atom)
//faster than unevolved
StartCooldownOthers(1.5 SECONDS)
/datum/action/cooldown/mob_cooldown/projectile_attack/dir_shots/bileworm/vileworm/attack_sequence(mob/living/firer, atom/target)
fire_in_directions(firer, target, GLOB.cardinals)
SLEEP_CHECK_DEATH(0.25 SECONDS, firer)
fire_in_directions(firer, target, GLOB.diagonals)
SLEEP_CHECK_DEATH(0.25 SECONDS, firer)
fire_in_directions(firer, target, GLOB.cardinals)
// surprise!
if(prob(25))
SLEEP_CHECK_DEATH(0.25 SECONDS, firer)
fire_in_directions(firer, target, GLOB.diagonals)
/obj/projectile/bileworm_acid/vile
name = "corrupted bile"
icon_state = "vileworm"
/obj/effect/bileworm_acid/corrupt
icon_state = "corrupt_bile_glob"
damage = 27
@@ -15,9 +15,16 @@
var/beam_duration = 2 SECONDS
/// How long do we wind up before firing?
var/charge_duration = 1 SECONDS
/// Have we been hit and have to abort the blast?
var/abort_blast = FALSE
/// A list of all the beam parts.
var/list/beam_parts = list()
/datum/action/cooldown/mob_cooldown/brimbeam/Grant(mob/granted_to)
. = ..()
if(owner)
owner.AddElement(/datum/element/relay_attackers)
/datum/action/cooldown/mob_cooldown/brimbeam/Destroy()
extinguish_laser()
return ..()
@@ -25,6 +32,7 @@
/datum/action/cooldown/mob_cooldown/brimbeam/Activate(atom/target)
StartCooldown(360 SECONDS)
abort_blast = FALSE
owner.face_atom(target)
owner.move_resist = MOVE_FORCE_VERY_STRONG
owner.balloon_alert_to_viewers("charging...")
@@ -32,17 +40,20 @@
var/mutable_appearance/direction_emissive = emissive_appearance('icons/mob/simple/lavaland/lavaland_monsters.dmi', "brimdemon_telegraph_dir", owner, alpha = 150, effect_type = EMISSIVE_NO_BLOOM)
owner.add_overlay(direction_overlay)
owner.add_overlay(direction_emissive)
RegisterSignal(owner, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_owner_attacked))
var/fully_charged = do_after(owner, delay = charge_duration, target = owner)
var/fully_charged = do_after(owner, delay = charge_duration, target = owner, extra_checks = CALLBACK(src, PROC_REF(beam_charge_check)))
owner.cut_overlay(direction_overlay)
owner.cut_overlay(direction_emissive)
if (!fully_charged)
UnregisterSignal(owner, COMSIG_ATOM_WAS_ATTACKED)
StartCooldown()
return TRUE
if (!fire_laser())
var/static/list/fail_emotes = list("coughs.", "wheezes.", "belches out a puff of black smoke.")
owner.manual_emote(pick(fail_emotes))
UnregisterSignal(owner, COMSIG_ATOM_WAS_ATTACKED)
StartCooldown()
return TRUE
@@ -51,11 +62,20 @@
demon.icon_state = demon.firing_icon_state
demon.update_appearance(UPDATE_OVERLAYS)
do_after(owner, delay = beam_duration, target = owner, hidden = TRUE)
do_after(owner, delay = beam_duration, target = owner, hidden = TRUE, extra_checks = CALLBACK(src, PROC_REF(beam_charge_check)))
UnregisterSignal(owner, COMSIG_ATOM_WAS_ATTACKED)
extinguish_laser()
StartCooldown()
return TRUE
/datum/action/cooldown/mob_cooldown/brimbeam/proc/on_owner_attacked(datum/source, atom/attacker, attack_flags, direction)
SIGNAL_HANDLER
if (!(attack_flags & ATTACK_RANGED) && !(direction & owner.dir))
abort_blast = TRUE
/datum/action/cooldown/mob_cooldown/brimbeam/proc/beam_charge_check()
return !abort_blast
/// Create a laser in the direction we are facing
/datum/action/cooldown/mob_cooldown/brimbeam/proc/fire_laser()
owner.visible_message(span_danger("[owner] fires a brimbeam!"))
@@ -67,7 +87,7 @@
if(affected_turf.opacity)
break
var/blocked = FALSE
for(var/obj/potential_block in affected_turf.contents)
for(var/obj/potential_block in affected_turf)
if(potential_block.opacity)
blocked = TRUE
break
@@ -77,8 +97,8 @@
new_brimbeam.dir = owner.dir
beam_parts += new_brimbeam
new_brimbeam.assign_creator(owner)
for(var/mob/living/hit_mob in affected_turf.contents)
hit_mob.apply_damage(damage = 25, damagetype = BURN)
for(var/mob/living/hit_mob in affected_turf)
hit_mob.apply_damage(25, BURN, blocked = hit_mob.run_armor_check(null, LASER, silent = TRUE), wound_bonus = CANT_WOUND)
to_chat(hit_mob, span_userdanger("You're blasted by [owner]'s brimbeam!"))
RegisterSignal(new_brimbeam, COMSIG_QDELETING, PROC_REF(extinguish_laser)) // In case idk a singularity eats it or something
if(!length(beam_parts))
@@ -126,16 +146,10 @@
return ..()
/obj/effect/brimbeam/process()
var/atom/ignore = creator?.resolve()
var/ignore = creator?.resolve()
for(var/mob/living/hit_mob in get_turf(src))
if(hit_mob == ignore)
continue
damage(hit_mob)
/// Hurt the passed mob
/obj/effect/brimbeam/proc/damage(mob/living/hit_mob)
hit_mob.apply_damage(damage = 5, damagetype = BURN)
to_chat(hit_mob, span_danger("You're damaged by [src]!"))
if(hit_mob != ignore)
hit_mob.apply_damage(7, BURN, blocked = hit_mob.run_armor_check(null, LASER, silent = TRUE), wound_bonus = CANT_WOUND)
/// Ignore damage dealt to this mob
/obj/effect/brimbeam/proc/assign_creator(mob/living/maker)
@@ -4,6 +4,7 @@
/datum/ai_controller/basic_controller/brimdemon
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining/low_node_priority,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
@@ -11,6 +12,7 @@
idle_behavior = /datum/idle_behavior/idle_random_walk/no_target
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/call_reinforcements/mining,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic,
/datum/ai_planning_subtree/move_to_cardinal/brimdemon,
@@ -13,8 +13,8 @@
mob_biotypes = MOB_ORGANIC|MOB_BUG|MOB_MINING
friendly_verb_continuous = "harmlessly rolls into"
friendly_verb_simple = "harmlessly roll into"
maxHealth = 45
health = 45
maxHealth = 175
health = 175
melee_damage_lower = 0
melee_damage_upper = 0
attack_verb_continuous = "barrels into"
@@ -49,16 +49,26 @@
else
can_lay_eggs = FALSE
var/list/food_types = string_list(list(/obj/item/stack/ore))
// Rarer ores heal us a bit depending on the rarity
var/static/list/food_types = list(
/obj/item/stack/ore = 0,
/obj/item/stack/ore/gold = 5,
/obj/item/stack/ore/uranium = 5,
/obj/item/stack/ore/diamond = 15,
/obj/item/stack/ore/bluespace_crystal = 20,
/obj/item/stack/ore/bananium = 30,
)
var/static/list/innate_actions = list(
/datum/action/cooldown/mob_cooldown/spit_ore = BB_SPIT_ABILITY,
/datum/action/cooldown/mob_cooldown/burrow = BB_BURROW_ABILITY,
)
grant_actions_by_list(innate_actions)
AddElement(/datum/element/ore_collecting)
AddElement(/datum/element/basic_eating, food_types = food_types, add_to_contents = TRUE)
AddElement(/datum/element/wall_tearer, allow_reinforced = FALSE)
AddComponent(/datum/component/proficient_miner, 0.05, TRUE) // Speedy boi
AddComponent(/datum/component/ai_listen_to_weather)
AddComponent(\
/datum/component/appearance_on_aggro,\
@@ -81,10 +91,10 @@
SIGNAL_HANDLER
if(stat != CONSCIOUS)
return COMPONENT_BULLET_PIERCED
return NONE
/// High penetration bullets should still go through. No goldgrub can save you from the colossus' death bolts.
if(prob(hitting_projectile.armour_penetration))
/// Reflects PKA/PKC shots and plasma cutter beams, unless they have high armor penetration
if(prob(hitting_projectile.armour_penetration) || (hitting_projectile.armor_flag != BOMB && hitting_projectile.armor_flag != ENERGY))
return NONE
visible_message(span_danger("[hitting_projectile] is repelled by [source]'s girth!"))
@@ -44,9 +44,9 @@
/datum/action/cooldown/mob_cooldown/burrow/Activate()
var/obj/effect/dummy/phased_mob/grub_burrow/holder = null
var/turf/current_loc = get_turf(owner)
var/mob/living/grub = owner
if(!do_after(owner, 3 SECONDS, target = current_loc))
owner.balloon_alert(owner, "need to stay still!")
if(!do_after(owner, 2.5 SECONDS, target = current_loc, extra_checks = CALLBACK(src, PROC_REF(health_check), grub.health)))
return
if(get_turf(owner) != current_loc)
@@ -72,6 +72,10 @@
StartCooldown()
return TRUE
/datum/action/cooldown/mob_cooldown/burrow/proc/health_check(health)
var/mob/living/grub = owner
return grub.health >= health
/obj/effect/dummy/phased_mob/grub_burrow
/obj/effect/dummy/phased_mob/grub_burrow/phased_check(mob/living/user, direction)
@@ -9,7 +9,7 @@
pixel_x = -12
base_pixel_x = -12
gender = MALE // Female ones are the bipedal elites
speed = 30
speed = 12
basic_mob_flags = IMMUNE_TO_FISTS
maxHealth = 300
health = 300
@@ -197,7 +197,7 @@
This one is clearly ancient, and its tentacles constantly churn the earth around it."
maxHealth = 400
health = 400
crusher_drop_chance = 30 // Wow a whole 5% more likely, how generous
crusher_drop_chance = 100
/// Don't re-check nearby turfs for this long
COOLDOWN_DECLARE(retarget_turfs_cooldown)
/// List of places we might spawn a tentacle, if we're alive
@@ -214,7 +214,7 @@
tentacle_target_turfs -= target_turf
continue
if (prob(10))
new /obj/effect/goliath_tentacle(target_turf)
new /obj/effect/goliath_tentacle(target_turf, src)
/mob/living/basic/mining/goliath/ancient/immortal/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
. = ..()
@@ -224,7 +224,7 @@
/// Store nearby turfs in our list so we can pop them out later
/mob/living/basic/mining/goliath/ancient/immortal/proc/cache_nearby_turfs()
COOLDOWN_START(src, retarget_turfs_cooldown, 10 SECONDS)
COOLDOWN_START(src, retarget_turfs_cooldown, 5 SECONDS)
LAZYCLEARLIST(tentacle_target_turfs)
for(var/turf/open/floor in orange(4, loc))
LAZYADD(tentacle_target_turfs, floor)
@@ -20,12 +20,12 @@
return ..()
/datum/action/cooldown/mob_cooldown/goliath_tentacles/Activate(atom/target)
new /obj/effect/goliath_tentacle(target)
var/list/directions = GLOB.cardinals.Copy()
for(var/i in 1 to 3)
var/spawndir = pick_n_take(directions)
new /obj/effect/goliath_tentacle(target, owner)
for(var/spawndir in GLOB.cardinals)
var/turf/adjacent_target = get_step(target, spawndir)
if(adjacent_target)
if(isgroundlessturf(adjacent_target) && !islava(adjacent_target))
continue
if(isopenturf(adjacent_target) || ismineralturf(adjacent_target))
new /obj/effect/goliath_tentacle(adjacent_target)
if (isliving(target))
@@ -51,7 +51,7 @@
for (var/dir in directions)
var/turf/adjacent_target = get_step(target, dir)
if(adjacent_target)
new /obj/effect/goliath_tentacle(adjacent_target)
new /obj/effect/goliath_tentacle/drag(adjacent_target, owner)
owner.visible_message(span_warning("[owner] unleashes tentacles from the ground around it!"))
StartCooldown()
return TRUE
@@ -70,7 +70,7 @@
shared_cooldown = NONE
/datum/action/cooldown/mob_cooldown/tentacle_grasp/Activate(atom/target)
new /obj/effect/temp_visual/effect_trail/burrowed_tentacle(owner.loc, target)
new /obj/effect/temp_visual/effect_trail/burrowed_tentacle(owner.loc, target, owner)
if (isliving(target))
owner.visible_message(span_warning("[owner] reaches for [target] with its tentacles!"))
StartCooldown()
@@ -83,4 +83,20 @@
move_speed = 2
homing = FALSE
spawn_interval = 0.1 SECONDS
spawned_effect = /obj/effect/goliath_tentacle
spawned_effect = /obj/effect/goliath_tentacle/drag
/// Mob who fired us
var/mob/living/owner = null
/obj/effect/temp_visual/effect_trail/burrowed_tentacle/Initialize(mapload, atom/target, mob/living/owner)
. = ..()
src.owner = owner
/obj/effect/temp_visual/effect_trail/burrowed_tentacle/Destroy()
. = ..()
owner = null
/obj/effect/temp_visual/effect_trail/burrowed_tentacle/add_spawner()
AddComponent(/datum/component/spawner, spawn_types = list(spawned_effect), max_spawned = max_spawned, spawn_time = spawn_interval, spawn_callback = CALLBACK(src, PROC_REF(on_tentacle_spawned)))
/obj/effect/temp_visual/effect_trail/burrowed_tentacle/proc/on_tentacle_spawned(obj/effect/goliath_tentacle/tentacle)
tentacle.set_owner(owner)
@@ -4,6 +4,7 @@
/datum/ai_controller/basic_controller/goliath
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
@@ -11,6 +12,7 @@
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/call_reinforcements/mining,
/datum/ai_planning_subtree/target_retaliate/check_faction,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/find_food,
@@ -34,7 +36,7 @@
return ..()
var/mob/living/target = controller.blackboard[target_key]
// Interrupt attack chain to use tentacles, unless the target is already tentacled
if (ismecha(target) || (isliving(target) && !target.has_status_effect(/datum/status_effect/incapacitating/stun/goliath_tentacled)))
if (ismecha(target) || (isliving(target) && !target.get_item_by_slot(ITEM_SLOT_LEGCUFFED)))
var/datum/action/cooldown/using_action = controller.blackboard[BB_GOLIATH_TENTACLES]
if (using_action?.IsAvailable())
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
@@ -46,7 +48,7 @@
/datum/ai_planning_subtree/targeted_mob_ability/goliath_tentacles/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/target = controller.blackboard[target_key]
if (!(isliving(target) || ismecha(target)) || (isliving(target) && target.has_status_effect(/datum/status_effect/incapacitating/stun/goliath_tentacled)))
if (!(isliving(target) || ismecha(target)) || (isliving(target) && target.get_item_by_slot(ITEM_SLOT_LEGCUFFED)))
return // Target can be an item or already grabbed, we don't want to tentacle those
var/time_on_target = controller.blackboard[BB_BASIC_MOB_HAS_TARGET_TIME] || 0
if (time_on_target < MIN_TIME_TO_TENTACLE)
@@ -13,9 +13,13 @@
/// Lower bound of damage to inflict
var/min_damage = 10
/// Upper bound of damage to inflict
var/max_damage = 15
var/max_damage = 15
/// Type of legcuff we spawn
var/trap_type = /obj/item/restraints/legcuffs/goliath_tentacle
/// Mob who fired the tentacle
var/mob/living/owner = null
/obj/effect/goliath_tentacle/Initialize(mapload)
/obj/effect/goliath_tentacle/Initialize(mapload, mob/living/goliath)
. = ..()
if (ismineralturf(loc))
var/turf/closed/mineral/floor = loc
@@ -28,11 +32,24 @@
deltimer(action_timer)
action_timer = addtimer(CALLBACK(src, PROC_REF(animate_grab)), 0.7 SECONDS, TIMER_STOPPABLE)
update_appearance(UPDATE_OVERLAYS)
set_owner(goliath)
/obj/effect/goliath_tentacle/Destroy()
deltimer(action_timer)
return ..()
/obj/effect/goliath_tentacle/proc/set_owner(mob/living/goliath)
if (owner)
UnregisterSignal(owner, COMSIG_QDELETING)
owner = goliath
if (owner)
RegisterSignal(owner, COMSIG_QDELETING, PROC_REF(on_owner_del))
/obj/effect/goliath_tentacle/proc/on_owner_del(datum/source)
SIGNAL_HANDLER
owner = null
retract()
/// Change to next icon state and set up grapple
/obj/effect/goliath_tentacle/proc/animate_grab()
icon_state = "goliath_tentacle_wiggle"
@@ -42,20 +59,31 @@
/// Grab everyone we share space with. If it's nobody, go home.
/obj/effect/goliath_tentacle/proc/grab()
var/trapped_mobs = FALSE
for (var/mob/living/victim in loc)
if (victim.stat == DEAD || HAS_TRAIT(victim, TRAIT_TENTACLE_IMMUNE))
if (victim.stat == DEAD || owner && victim.faction_check_atom(owner))
continue
if (HAS_TRAIT(victim, TRAIT_TENTACLE_IMMUNE) || SEND_SIGNAL(victim, COMSIG_GOLIATH_TENTACLED_GRABBED) & COMPONENT_GOLIATH_CANCEL_TENTACLE_GRAB)
continue
var/obj/item/restraints/legcuffs/goliath_tentacle/trap = new trap_type(loc, victim, src)
if (QDELETED(trap))
continue
balloon_alert(victim, "grabbed")
visible_message(span_danger("[src] grabs hold of [victim]!"))
victim.adjust_brute_loss(rand(min_damage, max_damage))
if (victim.apply_status_effect(/datum/status_effect/incapacitating/stun/goliath_tentacled, grapple_time, src))
buckle_mob(victim, TRUE)
SEND_SIGNAL(victim, COMSIG_GOLIATH_TENTACLED_GRABBED)
victim.apply_damage(rand(min_damage, max_damage), BRUTE, pick(BODY_ZONE_L_LEG, BODY_ZONE_R_LEG), wound_bonus = CANT_WOUND)
trapped_mobs = TRUE
for (var/obj/vehicle/sealed/mecha/mech in loc)
mech.take_damage(rand(min_damage, max_damage), damage_type = BRUTE, damage_flag = MELEE, sound_effect = TRUE)
if (!has_buckled_mobs())
// Already retracting
if (icon_state == "goliath_tentacle_retract")
return
if (!trapped_mobs)
retract()
return
deltimer(action_timer)
action_timer = addtimer(CALLBACK(src, PROC_REF(retract)), grapple_time, TIMER_STOPPABLE)
@@ -64,7 +92,6 @@
if (icon_state == "goliath_tentacle_retract")
return // Already retracting
SEND_SIGNAL(src, COMSIG_GOLIATH_TENTACLE_RETRACTING)
unbuckle_all_mobs(force = TRUE)
icon_state = "goliath_tentacle_retract"
update_appearance(UPDATE_OVERLAYS)
deltimer(action_timer)
@@ -77,54 +104,136 @@
/obj/effect/goliath_tentacle/attack_hand(mob/living/user, list/modifiers)
. = ..()
if (. || !has_buckled_mobs())
return
retract()
return TRUE
if(!.)
retract()
return TRUE
/// Goliath tentacle stun with special removal conditions
/datum/status_effect/incapacitating/stun/goliath_tentacled
id = "goliath_tentacled"
duration = 10 SECONDS
/// The tentacle that is tenderly holding us close
/// An alternative variant which drags the mob in towards the goliath
/obj/effect/goliath_tentacle/drag
trap_type = /obj/item/restraints/legcuffs/goliath_tentacle/drag
/obj/item/restraints/legcuffs/goliath_tentacle
name = "tentacle mass"
desc = "A writhing tentacle constricting one of your limbs."
icon_state = "goliath_tentacle"
slowdown = 4
breakouttime = 6 SECONDS
resistance_flags = LAVA_PROOF | FIRE_PROOF
obj_flags = CONDUCTS_ELECTRICITY | CAN_BE_HIT
item_flags = DROPDEL
legcuff_state = "goliath_tentacle"
uses_integrity = TRUE
max_integrity = 75 // 3 PKC swings, 5 bayonet swings
/// Tentacle we're attached to
var/obj/effect/goliath_tentacle/tentacle
/// Leash component linking the victim to the turf
var/datum/component/leash/leash
/// Beam between the victim and the tentacle tile
var/datum/beam/beam_effect
/datum/status_effect/incapacitating/stun/goliath_tentacled/on_creation(mob/living/new_owner, set_duration, obj/effect/goliath_tentacle/tentacle)
/obj/item/restraints/legcuffs/goliath_tentacle/Initialize(mapload, mob/living/target, obj/effect/goliath_tentacle/tentacle)
. = ..()
if (!.)
return
ADD_TRAIT(src, TRAIT_IGNORE_DEMOLITION, INNATE_TRAIT)
src.tentacle = tentacle
if (!target?.equip_to_slot_if_possible(src, ITEM_SLOT_LEGCUFFED, disable_warning = TRUE, bypass_equip_delay_self = TRUE))
return INITIALIZE_HINT_QDEL
/datum/status_effect/incapacitating/stun/goliath_tentacled/on_apply()
/obj/item/restraints/legcuffs/goliath_tentacle/Destroy(force)
. = ..()
RegisterSignal(owner, COMSIG_CARBON_PRE_MISC_HELP, PROC_REF(on_helped))
RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_TENTACLE_IMMUNE), COMSIG_BRIMDUST_EXPLOSION), PROC_REF(release))
RegisterSignals(tentacle, list(COMSIG_QDELETING, COMSIG_GOLIATH_TENTACLE_RETRACTING), PROC_REF(on_tentacle_left))
if (tentacle)
tentacle.retract()
tentacle = null
if (leash)
UnregisterSignal(leash, COMSIG_QDELETING)
QDEL_NULL(leash)
QDEL_NULL(beam_effect)
/datum/status_effect/incapacitating/stun/goliath_tentacled/on_remove()
/obj/item/restraints/legcuffs/goliath_tentacle/equipped(mob/living/user, slot, initial)
. = ..()
UnregisterSignal(owner, list(COMSIG_CARBON_PRE_MISC_HELP, SIGNAL_ADDTRAIT(TRAIT_TENTACLE_IMMUNE), COMSIG_BRIMDUST_EXPLOSION))
if (isnull(tentacle))
if (slot != ITEM_SLOT_LEGCUFFED || leash)
return
UnregisterSignal(tentacle, list(COMSIG_QDELETING, COMSIG_GOLIATH_TENTACLE_RETRACTING))
leash_target(user)
/obj/item/restraints/legcuffs/goliath_tentacle/proc/release(datum/source)
SIGNAL_HANDLER
qdel(src)
/obj/item/restraints/legcuffs/goliath_tentacle/proc/on_tentacle_left(datum/source)
SIGNAL_HANDLER
if (tentacle)
UnregisterSignal(tentacle, list(COMSIG_QDELETING, COMSIG_GOLIATH_TENTACLE_RETRACTING))
tentacle = null
release()
/obj/item/restraints/legcuffs/goliath_tentacle/proc/leash_target(mob/living/user)
leash = user.AddComponent(/datum/component/leash, owner = get_turf(user), distance = 1, silent = TRUE)
beam_effect = user.Beam(get_turf(user), "goliath_tentacle", beam_type = /obj/effect/ebeam/goliath, emissive = FALSE)
RegisterSignal(beam_effect.visuals, COMSIG_CLICK, PROC_REF(on_beam_click))
RegisterSignals(user, list(SIGNAL_ADDTRAIT(TRAIT_TENTACLE_IMMUNE), COMSIG_BRIMDUST_EXPLOSION), PROC_REF(release))
RegisterSignals(tentacle, list(COMSIG_QDELETING, COMSIG_GOLIATH_TENTACLE_RETRACTING), PROC_REF(on_tentacle_left))
RegisterSignal(leash, COMSIG_QDELETING, PROC_REF(release))
/obj/item/restraints/legcuffs/goliath_tentacle/proc/on_beam_click(atom/source, atom/location, control, params, mob/user)
SIGNAL_HANDLER
INVOKE_ASYNC(src, PROC_REF(process_beam_click), source, location, params, user)
/obj/item/restraints/legcuffs/goliath_tentacle/proc/process_beam_click(atom/source, atom/location, params, mob/user)
if(world.time <= user.next_move)
return
var/obj/item/held_thing = user.get_active_held_item()
if (!istype(held_thing))
return
var/turf/nearest_turf = null
for (var/turf/line_turf in get_line(get_turf(src), get_turf(beam_effect.target)))
if (line_turf.IsReachableBy(user))
nearest_turf = line_turf
break
if (isnull(nearest_turf))
return
if (!user.can_perform_action(nearest_turf))
nearest_turf.balloon_alert(user, "cannot reach!")
return
held_thing.melee_attack_chain(user, src, params2list(params))
user.changeNext_move(held_thing.attack_speed)
playsound(user, held_thing.hitsound || 'sound/effects/blob/attackblob.ogg', held_thing.get_clamped_volume(), TRUE, extrarange = held_thing.stealthy_audio ? SILENCED_SOUND_EXTRARANGE : -1, falloff_distance = 0)
/obj/item/restraints/legcuffs/goliath_tentacle/play_attack_sound(damage_amount, damage_type, damage_flag)
return
/obj/item/restraints/legcuffs/goliath_tentacle/drag
/obj/item/restraints/legcuffs/goliath_tentacle/drag/Initialize(mapload, mob/living/target, obj/effect/goliath_tentacle/tentacle)
. = ..()
QDEL_IN(src, 15 SECONDS)
if (. != INITIALIZE_HINT_QDEL)
START_PROCESSING(SSprocessing, src)
/obj/item/restraints/legcuffs/goliath_tentacle/drag/Destroy(force)
. = ..()
STOP_PROCESSING(SSprocessing, src)
/obj/item/restraints/legcuffs/goliath_tentacle/drag/process(seconds_per_tick)
if (leash.distance <= 1)
return PROCESS_KILL
leash.set_distance(leash.distance - 1)
/obj/item/restraints/legcuffs/goliath_tentacle/drag/leash_target(mob/living/user)
if (!tentacle?.owner)
return ..()
leash = user.AddComponent(/datum/component/leash, owner = tentacle.owner, distance = get_dist(user, tentacle.owner), silent = TRUE)
beam_effect = user.Beam(tentacle.owner, "goliath_tentacle", beam_type = /obj/effect/ebeam/goliath, emissive = FALSE)
RegisterSignal(beam_effect.visuals, COMSIG_CLICK, PROC_REF(on_beam_click))
RegisterSignals(user, list(SIGNAL_ADDTRAIT(TRAIT_TENTACLE_IMMUNE), COMSIG_BRIMDUST_EXPLOSION), PROC_REF(release))
RegisterSignals(tentacle.owner, COMSIG_QDELETING, PROC_REF(on_tentacle_left))
RegisterSignal(leash, COMSIG_QDELETING, PROC_REF(release))
tentacle.retract()
tentacle = null
/// Some kind soul has rescued us
/datum/status_effect/incapacitating/stun/goliath_tentacled/proc/on_helped(mob/source, mob/helping)
SIGNAL_HANDLER
release()
source.visible_message(span_notice("[helping] rips [source] from the tentacle's grasp!"))
return COMPONENT_BLOCK_MISC_HELP
/// Something happened to make the tentacle let go
/datum/status_effect/incapacitating/stun/goliath_tentacled/proc/release()
SIGNAL_HANDLER
owner.remove_status_effect(/datum/status_effect/incapacitating/stun/goliath_tentacled)
/// Something happened to our associated tentacle
/datum/status_effect/incapacitating/stun/goliath_tentacled/proc/on_tentacle_left()
SIGNAL_HANDLER
UnregisterSignal(tentacle, list(COMSIG_QDELETING, COMSIG_GOLIATH_TENTACLE_RETRACTING)) // No endless loops for us please
tentacle = null
release()
/obj/effect/ebeam/goliath
name = "goliath tentacle"
mouse_opacity = MOUSE_OPACITY_ICON
@@ -46,16 +46,16 @@
/mob/living/basic/mining/gutlunch/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(!.)
if(.)
return
if(!istype(target, /obj/structure/ore_container/food_trough/gutlunch_trough))
return TRUE
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
var/obj/ore_food = locate(/obj/item/stack/ore) in target
if(isnull(ore_food))
balloon_alert(src, "no food!")
else
UnarmedAttack(ore_food, TRUE, modifiers)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/mob/living/basic/mining/gutlunch/proc/after_birth(mob/living/basic/mining/gutlunch/grub/baby, mob/living/partner)
var/our_color = LAZYACCESS(atom_colours, FIXED_COLOUR_PRIORITY) || COLOR_GRAY
@@ -75,7 +75,7 @@
/mob/living/basic/mining/legion/update_overlays()
. = ..()
if (stat != DEAD && has_emissive) // Shouldn't really happen but just in case
. += emissive_appearance(icon, "[icon_living]_e", src, effect_type = EMISSIVE_NO_BLOOM)
. += emissive_appearance(icon, "[icon_living]_e", src, effect_type = EMISSIVE_BLOOM)
/// Put a corpse in this guy
/mob/living/basic/mining/legion/proc/consume(mob/living/carbon/human/consumed)
@@ -2,25 +2,29 @@
/datum/ai_controller/basic_controller/legion
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/legion,
BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
BB_AGGRO_RANGE = 5, // Unobservant
BB_BASIC_MOB_FLEE_DISTANCE = 6,
BB_RANGED_SKIRMISH_MIN_DISTANCE = 4,
BB_RANGED_SKIRMISH_MAX_DISTANCE = 6,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/call_reinforcements/mining,
/datum/ai_planning_subtree/random_speech/legion,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/maintain_distance,
/datum/ai_planning_subtree/targeted_mob_ability,
/datum/ai_planning_subtree/flee_target/legion,
)
/// Chase and attack whatever we are targeting, if it's friendly we will heal them
/datum/ai_controller/basic_controller/legion_brood
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/legion,
BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining/low_node_priority,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
@@ -103,8 +103,17 @@
else
add_ally(creator)
created_by = WEAKREF(creator)
ai_controller?.set_blackboard_key(BB_LEGION_BROOD_CREATOR, creator)
RegisterSignal(creator, COMSIG_QDELETING, PROC_REF(creator_destroyed))
if (!ai_controller)
return
ai_controller.set_blackboard_key(BB_LEGION_BROOD_CREATOR, creator)
if (!creator.ai_controller)
return
// Inherit our creator's target and reinforcement requests
ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, creator.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
ai_controller.set_blackboard_key(BB_MINING_MOB_REINFORCEMENTS_REQUESTS, creator.ai_controller.blackboard[BB_MINING_MOB_REINFORCEMENTS_REQUESTS])
/// Reference handling
/mob/living/basic/mining/legion_brood/proc/creator_destroyed()
@@ -8,12 +8,13 @@
/datum/ai_controller/basic_controller/lobstrosity
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining,
BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
BB_LOBSTROSITY_EXPLOIT_TRAITS = list(TRAIT_INCAPACITATED, TRAIT_FLOORED, TRAIT_IMMOBILIZED, TRAIT_KNOCKEDOUT),
BB_LOBSTROSITY_FINGER_LUST = 0,
BB_LOBSTROSITY_NAIVE_HUNTER = FALSE,
BB_BASIC_MOB_FLEE_DISTANCE = 8,
BB_BASIC_MOB_FLEE_DISTANCE = 6,
BB_EAT_FOOD_COOLDOWN = 3 MINUTES,
BB_ONLY_FISH_WHILE_HUNGRY = TRUE,
BB_TARGET_PRIORITY_TRAIT = TRAIT_SCARY_FISHERMAN,
@@ -24,6 +25,7 @@
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/call_reinforcements/mining,
/datum/ai_planning_subtree/random_speech/insect,
/datum/ai_planning_subtree/hoard_fingers,
/datum/ai_planning_subtree/pet_planning,
@@ -40,7 +40,7 @@
drop_mod = crusher_drop_chance,\
drop_immediately = basic_mob_flags & DEL_ON_DEATH,\
)
RegisterSignal(src, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(check_ashwalker_peace_violation))
RegisterSignal(src, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked))
// We add this to ensure that mobs will actually receive the above signal, as some will lack AI
// handling for retaliation and attack special cases
AddElement(/datum/element/relay_attackers)
@@ -55,9 +55,9 @@
throw_blocked_message = throw_blocked_message,\
)
/mob/living/basic/mining/proc/check_ashwalker_peace_violation(datum/source, mob/living/carbon/human/possible_ashwalker)
/mob/living/basic/mining/proc/on_attacked(datum/source, atom/attacker, attack_flags)
SIGNAL_HANDLER
if(!isashwalker(possible_ashwalker) || !has_faction(FACTION_ASHWALKER))
if(!isashwalker(attacker) || !has_faction(FACTION_ASHWALKER))
return
remove_faction(FACTION_ASHWALKER)
@@ -97,26 +97,26 @@
/mob/living/basic/mining/mook/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(!.)
return FALSE
if(.)
return
return attack_sequence(target)
/mob/living/basic/mining/mook/proc/attack_sequence(atom/target)
if(istype(target, /obj/item/stack/ore) && isnull(held_ore))
var/obj/item/ore_target = target
ore_target.forceMove(src)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
if(istype(target, /obj/structure/ore_container/material_stand))
if(held_ore)
held_ore.forceMove(target)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
if(istype(target, /obj/structure/bonfire))
var/obj/structure/bonfire/fire_target = target
if(!fire_target.burning)
fire_target.start_burning()
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/mob/living/basic/mining/mook/proc/change_combatant_state(state)
attack_state = state
@@ -187,7 +187,7 @@
if(istype(intruder, /mob/living/basic/mining/mook))
return
for(var/mob/living/basic/mining/mook/villager in oview(src, 9))
villager.ai_controller?.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, intruder)
villager.ai_controller?.set_blackboard_key_assoc(BB_BASIC_MOB_RETALIATE_LIST, intruder, world.time)
/mob/living/basic/mining/mook/worker
@@ -71,7 +71,7 @@ GLOBAL_LIST_EMPTY(raptor_population)
/obj/item/food/meat/slab/xeno = -15,
/obj/item/food/meat/steak = 50,
/obj/item/food/grown/ash_flora = 10,
/obj/item/fish = 15,
/obj/item/fish = 25,
/obj/item/organ = 25,
)
/// Inheritance datum we store our genetic data in
@@ -92,7 +92,7 @@ GLOBAL_LIST_EMPTY(raptor_population)
else
change_growth_stage(growth_stage, RAPTOR_ADULT)
add_traits(list(TRAIT_ASHSTORM_IMMUNE, TRAIT_SNOWSTORM_IMMUNE, TRAIT_MINING_AOE_IMMUNE), INNATE_TRAIT)
add_traits(list(TRAIT_ASHSTORM_IMMUNE, TRAIT_SNOWSTORM_IMMUNE, TRAIT_MINING_AOE_IMMUNE, TRAIT_NO_SLIP_ICE, TRAIT_NO_SLIP_SLIDE), INNATE_TRAIT)
AddElement(\
/datum/element/crusher_loot,\
trophy_type = /obj/item/crusher_trophy/raptor_feather,\
@@ -169,16 +169,16 @@ GLOBAL_LIST_EMPTY(raptor_population)
/mob/living/basic/raptor/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(!.)
return FALSE
if(.)
return
if(!istype(target, /obj/structure/ore_container/food_trough/raptor_trough))
return TRUE
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
var/obj/ore_food = locate(/obj/item/stack/ore) in target
if(isnull(ore_food))
balloon_alert(src, "no food!")
else
UnarmedAttack(ore_food, TRUE, modifiers)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/mob/living/basic/raptor/melee_attack(mob/living/target, list/modifiers, ignore_cooldown)
if (!combat_mode && istype(target, /mob/living/basic/raptor))
@@ -96,6 +96,10 @@ GLOBAL_LIST_INIT(raptor_colors, init_raptor_colors())
// Doesn't care for your excuses for friendly fire
ai_controller = /datum/ai_controller/basic_controller/raptor/aggressive
/datum/raptor_color/red/setup_raptor(mob/living/basic/raptor/raptor)
. = ..()
ADD_TRAIT(raptor, TRAIT_MINING_AGGRO, INNATE_TRAIT)
/datum/raptor_color/purple
color = "purple"
description = "A small, nimble breed, these raptors have been bred as travel companions rather than mounts, capable of storing the owner's possessions and helping them escape from danger unscathed."
@@ -440,7 +444,7 @@ GLOBAL_LIST_INIT(raptor_colors, init_raptor_colors())
/datum/raptor_color/black/setup_raptor(mob/living/basic/raptor/raptor)
. = ..()
raptor.add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_NOFIRE_SPREAD), INNATE_TRAIT)
raptor.add_traits(list(TRAIT_LAVA_IMMUNE, TRAIT_NOFIRE_SPREAD, TRAIT_MINING_AGGRO), INNATE_TRAIT)
/datum/raptor_color/black/setup_adult(mob/living/basic/raptor/raptor)
. = ..()
@@ -0,0 +1,155 @@
#define HEARTBEAT_NORMAL (1.8 SECONDS)
#define HEARTBEAT_FAST (1 SECONDS)
#define HEARTBEAT_FRANTIC (0.4 SECONDS)
GLOBAL_LIST_INIT(tendrils, list())
/mob/living/basic/mining/tendril
name = "necropolis tendril"
desc = "A vile tendril of corruption, originating deep underground."
icon = 'icons/mob/simple/lavaland/tendril.dmi'
icon_state = "tendril"
icon_living = "tendril"
pixel_w = -8
base_pixel_w = -8
status_flags = NONE
mob_biotypes = MOB_ORGANIC | MOB_SKELETAL | MOB_MINING
basic_mob_flags = DEL_ON_DEATH | IMMUNE_TO_FISTS
mob_size = MOB_SIZE_HUGE
maxHealth = 1200
health = 1200
friendly_verb_continuous = "flails at"
friendly_verb_simple = "flail at"
speak_emote = list("resonates")
obj_damage = 100
melee_damage_lower = 20
melee_damage_upper = 20
sharpness = SHARP_POINTY
wound_bonus = CANT_WOUND
attack_sound = 'sound/items/weapons/pierce.ogg'
attack_verb_continuous = "pierces through"
attack_verb_simple = "pierce through"
throw_blocked_message = "does nothing to the thick shell of"
move_resist = INFINITY
light_range = 3
light_power = 2
light_color = LIGHT_COLOR_LAVA
ai_controller = /datum/ai_controller/basic_controller/tendril
/// Looping heartbeat sound
var/datum/looping_sound/heartbeat/soundloop
/// Melee attack ability to used in retaliation to melee strikes and whenever it manages to grab someone
var/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_melee/tendril_melee
/mob/living/basic/mining/tendril/Initialize(mapload)
. = ..()
GLOB.tendrils += src
AddElement(/datum/element/ai_retaliate)
AddElement(/datum/element/death_drops, /obj/structure/closet/crate/necropolis/tendril)
AddComponent(/datum/component/ai_target_timer)
AddComponent(/datum/component/gps, "Eerie Signal")
AddComponent(/datum/component/basic_mob_attack_telegraph, display_telegraph_overlay = FALSE, telegraph_duration = 0.4 SECONDS)
AddComponent(/datum/component/regenerator, regeneration_delay = 30 SECONDS, brute_per_second = 20)
add_traits(list(TRAIT_BACKSTAB_IMMUNE, TRAIT_IMMOBILIZED), INNATE_TRAIT)
var/static/list/abilities = list(
/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_lash = BB_TENDRIL_LASH,
/datum/action/cooldown/mob_cooldown/tendril_chaser = BB_TENDRIL_CHASER,
/datum/action/cooldown/mob_cooldown/tendril_cross_spikes = BB_TENDRIL_SPIKES,
)
grant_actions_by_list(abilities)
tendril_melee = new(src)
tendril_melee.Grant(src)
AddComponent(/datum/component/revenge_ability, tendril_melee, targeting = GET_TARGETING_STRATEGY(ai_controller.blackboard[BB_TARGETING_STRATEGY]), max_range = 2, target_self = TRUE)
soundloop = new(src, start_immediately = FALSE)
soundloop.mid_length = HEARTBEAT_NORMAL
soundloop.pressure_affected = FALSE
soundloop.start()
update_appearance(UPDATE_OVERLAYS)
var/turf/our_turf = get_turf(src)
for (var/turf/rock in range(4, src))
var/dist = sqrt((rock.x - our_turf.x) ** 2 + (rock.y - our_turf.y) ** 2)
if (dist > 4.5)
continue
if (ismineralturf(rock))
rock.ScrapeAway(null, CHANGETURF_IGNORE_AIR)
if (istype(rock, /turf/open/misc/asteroid) && prob(100 / sqrt(max(1, dist))))
rock.ChangeTurf(/turf/open/indestructible/necropolis, null, CHANGETURF_IGNORE_AIR)
/mob/living/basic/mining/tendril/Destroy()
GLOB.tendrils -= src
QDEL_NULL(soundloop)
if(!SSachievements.achievements_enabled || (flags_1 & ADMIN_SPAWNED_1))
return ..()
for(var/mob/living/killer in view(7, src))
if(killer.stat || !killer.client)
continue
killer.client.give_award(/datum/award/score/tendril_score, killer)
if (!length(GLOB.tendrils))
killer.client.give_award(/datum/award/achievement/boss/tendril_exterminator, killer)
return ..()
/mob/living/basic/mining/tendril/update_overlays()
. = ..()
. += emissive_appearance(icon, "[icon_state]_e", src, effect_type = EMISSIVE_NO_BLOOM)
. += emissive_appearance(icon, "[icon_state]_e_bloom", src, effect_type = EMISSIVE_BLOOM)
/mob/living/basic/mining/tendril/Life(seconds_per_tick)
. = ..()
update_heartbeat() // Just a single math op unless we need updating so its fine to put it here
/mob/living/basic/mining/tendril/updatehealth()
. = ..()
update_heartbeat()
/mob/living/basic/mining/tendril/proc/update_heartbeat()
if (!soundloop)
return
var/beat_rate = HEARTBEAT_NORMAL
if (ai_controller?.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
beat_rate = round(HEARTBEAT_FRANTIC + health / maxHealth * (HEARTBEAT_FAST - HEARTBEAT_FRANTIC), 0.05 SECONDS)
if (beat_rate != soundloop.mid_length)
soundloop.set_mid_length(beat_rate)
/mob/living/basic/mining/tendril/do_attack_animation(atom/attacked_atom, visual_effect_icon, obj/item/used_item, no_effect, fov_effect = TRUE, item_animation_override = null)
if(!no_effect && (visual_effect_icon || used_item))
do_item_attack_animation(attacked_atom, visual_effect_icon, used_item, animation_type = item_animation_override)
// Stabby visuals
var/obj/effect/temp_visual/spike_stab/stab = new(get_turf(src))
var/target_dir = get_dir(src, attacked_atom)
stab.transform = matrix().Turn(dir2angle(target_dir) + rand(-7, 7))
if (target_dir & NORTH)
stab.pixel_z = 24
else if (target_dir & SOUTH)
stab.pixel_z = -24
if (target_dir & EAST)
stab.pixel_w = 24
else if (target_dir & WEST)
stab.pixel_w = -24
/obj/effect/temp_visual/spike_stab
icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
icon_state = "spike_small"
duration = 0.4 SECONDS
/mob/living/basic/mining/tendril/proc/snatch_react()
if (tendril_melee.IsAvailable())
tendril_melee.Activate(warning = FALSE)
#undef HEARTBEAT_NORMAL
#undef HEARTBEAT_FAST
#undef HEARTBEAT_FRANTIC
@@ -0,0 +1,358 @@
/// Fires out two cross patterns of damaging tentacles which reel in anything they hit, then causes a followup attack
/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_lash
name = "Tentacle Lash"
desc = "Lash out with your tentacles in 8 directions, reeling in whatever you hit and unleashing a deadly followup attack afterwards."
button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
button_icon_state = "goliath_tentacle_wiggle"
background_icon_state = "bg_demon"
overlay_icon_state = "bg_demon_border"
click_to_activate = FALSE
cooldown_time = 8 SECONDS
melee_cooldown_time = 0
shared_cooldown = NONE
projectile_type = /obj/projectile/tentacle_lash
/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_lash/Activate(atom/target)
disable_cooldown_actions()
for (var/swipe_dir in GLOB.cardinals)
var/turf/open/line_turf = get_step(owner, swipe_dir)
for (var/i in 1 to projectile_type::range)
if (!istype(line_turf) || line_turf.is_blocked_turf(exclude_mobs = TRUE))
break
var/obj/effect/temp_visual/telegraphing/line/telegraph = new(line_turf)
telegraph.dir = swipe_dir
line_turf = get_step(line_turf, swipe_dir)
SLEEP_CHECK_DEATH(0.8 SECONDS, owner)
for (var/swipe_dir in GLOB.cardinals)
shoot_projectile(get_turf(owner), get_step(owner, swipe_dir), dir2angle(swipe_dir), owner)
SLEEP_CHECK_DEATH(1.6 SECONDS, owner)
for (var/swipe_dir in GLOB.diagonals)
var/turf/open/line_turf = get_step(owner, swipe_dir)
for (var/i in 1 to projectile_type::range)
if (!istype(line_turf) || line_turf.is_blocked_turf(exclude_mobs = TRUE))
break
var/obj/effect/temp_visual/telegraphing/line/telegraph = new(line_turf)
telegraph.dir = swipe_dir
line_turf = get_step(line_turf, swipe_dir)
SLEEP_CHECK_DEATH(0.8 SECONDS, owner)
for (var/swipe_dir in GLOB.diagonals)
shoot_projectile(get_turf(owner), get_step(owner, swipe_dir), dir2angle(swipe_dir), owner)
SLEEP_CHECK_DEATH(1.2 SECONDS, owner)
StartCooldown()
SLEEP_CHECK_DEATH(0.6 SECONDS, owner)
enable_cooldown_actions()
return TRUE
/obj/projectile/tentacle_lash
name = "tentacle spike"
icon_state = "tentacle_spike"
pass_flags = PASSTABLE
damage = 5 // +10 from the grab
armor_flag = MELEE
range = 7
hit_prone_targets = TRUE
hitsound = 'sound/effects/wounds/pierce1.ogg'
sharpness = SHARP_POINTY
/// Beam connecting us and the firer
var/datum/beam/tentacle_beam = null
/// Does this projectile persist and reel in targets?
var/reel_in = TRUE
/// How long does the projectile persist?
var/duration = 1.2 SECONDS
/// Damage dealt to targets who get snatched from entering the beam or being hit directly
var/snatch_damage = 10
/obj/projectile/tentacle_lash/stab
damage = 15
range = 2
duration = 0.2 SECONDS // Just for visual flair
reel_in = FALSE
/obj/projectile/tentacle_lash/fire(fire_angle, atom/direct_target)
. = ..()
if (!firer)
return
tentacle_beam = Beam(firer, "goliath_tentacle", beam_type = (reel_in ? /obj/effect/ebeam/reacting : /obj/effect/ebeam), emissive = FALSE)
if (reel_in)
RegisterSignal(tentacle_beam, COMSIG_BEAM_ENTERED, PROC_REF(on_beam_entered))
/obj/projectile/tentacle_lash/Destroy()
QDEL_NULL(tentacle_beam)
return ..()
// Don't range out, stop and persist until we're done
/obj/projectile/tentacle_lash/on_range()
STOP_PROCESSING(SSprojectiles, src)
// Reset to tile center
pixel_x = 0
pixel_y = 0
QDEL_IN(src, duration)
/obj/projectile/tentacle_lash/prehit_pierce(atom/target)
// Ye 'ole colossus cheese
if (astype(target, /mob/living)?.stat == DEAD)
return PROJECTILE_PIERCE_PHASE
return ..()
/obj/projectile/tentacle_lash/on_hit(atom/target, blocked, pierce_hit)
. = ..()
if (!reel_in || !isliving(target) || blocked >= 100 || pierce_hit || . != BULLET_ACT_HIT)
return
snatch_target(target)
/obj/projectile/tentacle_lash/proc/on_beam_entered(datum/beam/source, obj/effect/ebeam/hit, atom/movable/entered)
SIGNAL_HANDLER
if (!reel_in || entered == firer || !isliving(entered))
return
var/mob/living/victim = entered
if ((!firer || !firer.faction_check_atom(victim)) && victim.stat != DEAD)
INVOKE_ASYNC(src, PROC_REF(snatch_target), entered)
/obj/projectile/tentacle_lash/proc/snatch_target(mob/living/victim)
if (HAS_TRAIT(victim, TRAIT_TENTACLE_IMMUNE) || SEND_SIGNAL(victim, COMSIG_TENDRIL_TENTACLED_GRABBED) & COMPONENT_TENDRIL_CANCEL_TENTACLE_GRAB)
return
to_chat(victim, span_userdanger("You're snatched by [firer]'s tentacles!"))
victim.apply_damage(snatch_damage, BRUTE, BODY_ZONE_CHEST, wound_bonus = CANT_WOUND)
if (QDELETED(victim))
qdel(src)
return
var/snatch_callback = null
if (istype(firer, /mob/living/basic/mining/tendril))
var/mob/living/basic/mining/tendril/tendril = firer
snatch_callback = CALLBACK(tendril, TYPE_PROC_REF(/mob/living/basic/mining/tendril, snatch_react))
victim.throw_at(firer, initial(range), 1, firer, FALSE, gentle = TRUE, callback = snatch_callback)
playsound(victim, hitsound, 50, -3, pressure_affected = FALSE)
qdel(src)
/// An ability which makes spikes come out of the ground towards your target
/datum/action/cooldown/mob_cooldown/tendril_chaser
name = "Impaling Spikes"
desc = "Send a spiked subterranean tendril chasing after your target."
button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
button_icon_state = "spikes_stabbing"
background_icon_state = "bg_demon"
overlay_icon_state = "bg_demon_border"
cooldown_time = 8 SECONDS
click_to_activate = TRUE
shared_cooldown = NONE
/// Lazy list of references to spike trails
var/list/active_chasers
/// Health percentage threshold at which we send out wide charsers after the main target
var/wide_chaser_threshold = 0.7
/datum/action/cooldown/mob_cooldown/tendril_chaser/Grant(mob/granted_to)
. = ..()
RegisterSignal(granted_to, COMSIG_MOB_ABILITY_STARTED, PROC_REF(on_ability_started))
RegisterSignal(granted_to, COMSIG_MOB_ABILITY_FINISHED, PROC_REF(on_ability_finished))
// Clean up after ourselves
/datum/action/cooldown/mob_cooldown/tendril_chaser/Remove(mob/removed_from)
UnregisterSignal(removed_from, list(COMSIG_MOB_ABILITY_STARTED, COMSIG_MOB_ABILITY_FINISHED))
QDEL_LIST(active_chasers)
return ..()
/datum/action/cooldown/mob_cooldown/tendril_chaser/proc/on_ability_started(mob/living/owner, datum/action/cooldown/activated)
SIGNAL_HANDLER
// Delete all of our chasers when our owner triggers cross spikes as to not cause guaranteed damage
if (istype(activated, /datum/action/cooldown/mob_cooldown/tendril_cross_spikes))
QDEL_LIST(active_chasers)
/datum/action/cooldown/mob_cooldown/tendril_chaser/proc/on_ability_finished(mob/living/owner, datum/action/cooldown/activated)
SIGNAL_HANDLER
if (istype(activated, /datum/action/cooldown/mob_cooldown/tendril_cross_spikes))
ResetCooldown()
/datum/action/cooldown/mob_cooldown/tendril_chaser/Activate(atom/target)
. = ..()
var/primary_type = /obj/effect/temp_visual/effect_trail/tendril_chaser
if (isliving(owner))
var/mob/living/as_living = owner
if (as_living.health / as_living.maxHealth <= wide_chaser_threshold)
primary_type = /obj/effect/temp_visual/effect_trail/tendril_chaser/wide_area
var/obj/effect/temp_visual/effect_trail/tendril_chaser/chaser = new primary_type(get_turf(owner), target)
LAZYADD(active_chasers, WEAKREF(chaser))
RegisterSignal(chaser, COMSIG_QDELETING, PROC_REF(on_chaser_destroyed))
playsound(owner, 'sound/effects/magic/demon_attack1.ogg', vol = 100, vary = TRUE, pressure_affected = FALSE)
/// Remove a spike trail from our list of active trails
/datum/action/cooldown/mob_cooldown/tendril_chaser/proc/on_chaser_destroyed(atom/chaser)
SIGNAL_HANDLER
LAZYREMOVE(active_chasers, WEAKREF(chaser))
/obj/effect/temp_visual/effect_trail/tendril_chaser
duration = 10 SECONDS
move_speed = 4
spawned_effect = /obj/effect/temp_visual/emerging_ground_spike/tendril
/// Do we spawn spikes around ourselves as well or only on our own turf?
var/area_spawn = FALSE
/obj/effect/temp_visual/effect_trail/tendril_chaser/wide_area
area_spawn = TRUE
/obj/effect/temp_visual/effect_trail/tendril_chaser/add_spawner()
return
/obj/effect/temp_visual/effect_trail/tendril_chaser/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
. = ..()
if (!area_spawn)
var/turf/spawn_turf = get_turf(src)
if (!(locate(/obj/effect/temp_visual/emerging_ground_spike/tendril) in spawn_turf) && isopenturf(spawn_turf))
new spawned_effect(spawn_turf)
return
for (var/spawn_dir in GLOB.cardinals)
if (spawn_dir & REVERSE_DIR(movement_dir))
continue
var/turf/spawn_loc = get_step(src, spawn_dir)
if (!(locate(/obj/effect/temp_visual/emerging_ground_spike/tendril) in spawn_loc) && isopenturf(spawn_loc))
new spawned_effect(spawn_loc)
/obj/effect/temp_visual/emerging_ground_spike/tendril
icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
icon_state = "spikes_stabbing"
duration = 0.7 SECONDS
position_variance = 3
impale_damage = 10
damage_blacklist_typecache = list(
/mob/living/basic/mining/tendril,
)
impale_wound_bonus = CANT_WOUND
// Have we hit someone yet?
var/hit_loser = FALSE
/obj/effect/temp_visual/emerging_ground_spike/tendril/single
icon_state = "spike"
duration = 1 SECONDS
harm_delay = 0.25 SECONDS
position_variance = 5
/obj/effect/temp_visual/emerging_ground_spike/tendril/impale()
. = ..()
hit_loser |= .
RegisterSignal(loc, COMSIG_ATOM_ENTERED, PROC_REF(on_entered))
/obj/effect/temp_visual/emerging_ground_spike/tendril/proc/on_entered(atom/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
SIGNAL_HANDLER
if (!isliving(arrived))
return
if (harm_mob(arrived) && !hit_loser)
playsound(src, 'sound/items/weapons/slice.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE)
hit_loser = TRUE
/datum/action/cooldown/mob_cooldown/tendril_cross_spikes
name = "Cross Spikes"
desc = "Create a wave of spikes around yourself, impaling anyone caught in it."
button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
button_icon_state = "spike"
background_icon_state = "bg_demon"
overlay_icon_state = "bg_demon_border"
click_to_activate = FALSE
cooldown_time = 10 SECONDS
melee_cooldown_time = 0
shared_cooldown = NONE
/// Health threshold at which we reduce the amount of empty spots on the ground
var/health_threshold = 0.3
/datum/action/cooldown/mob_cooldown/tendril_cross_spikes/Activate(atom/target)
disable_cooldown_actions()
spawn_spikes()
SLEEP_CHECK_DEATH(0.4 SECONDS, owner)
spawn_spikes(inverse = TRUE)
StartCooldown()
enable_cooldown_actions()
return TRUE
/datum/action/cooldown/mob_cooldown/tendril_cross_spikes/proc/spawn_spikes(inverse = FALSE)
var/list/turf/spike_turfs = list()
var/turf/owner_turf = get_turf(owner)
var/reduced_spawns = FALSE
if (isliving(owner))
var/mob/living/as_living = owner
if (as_living.health / as_living.maxHealth <= health_threshold)
reduced_spawns = TRUE
for (var/turf/open/target_turf in oview(7, owner_turf))
if (sqrt((target_turf.x - owner_turf.x) ** 2 + (target_turf.y - owner_turf.y) ** 2) > 9.5) // big circle is a lie
continue
if (reduced_spawns)
if (abs(target_turf.x - owner_turf.x) % 2 == abs(target_turf.y - owner_turf.y + inverse) % 2)
new /obj/effect/temp_visual/telegraphing/circle/short(target_turf)
spike_turfs += target_turf
continue
var/row_diff = inverse ? abs(target_turf.x - owner_turf.x) : abs(target_turf.y - owner_turf.y)
var/column_diff = inverse ? abs(target_turf.y - owner_turf.y) : abs(target_turf.x - owner_turf.x)
var/row = floor((row_diff + 1) / 3)
if (row % 2 == 0)
if (row_diff - row * 3 == 0)
if (column_diff % 4 == 2)
continue
else
if (column_diff % 4 != 0)
continue
else
if (row_diff - row * 3 == 0)
if (column_diff % 4 == 0)
continue
else
if (column_diff % 4 != 2)
continue
new /obj/effect/temp_visual/telegraphing/circle/short(target_turf)
spike_turfs += target_turf
SLEEP_CHECK_DEATH(1 SECONDS, owner)
for (var/turf/open/to_spawn in spike_turfs)
new /obj/effect/temp_visual/emerging_ground_spike/tendril/single(to_spawn)
SLEEP_CHECK_DEATH(0.8 SECONDS, owner)
/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_melee
name = "Tentacle Stab"
desc = "Stab nearby hostiles with long tentacles."
button_icon = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
button_icon_state = "goliath_tentacle_wiggle"
background_icon_state = "bg_demon"
overlay_icon_state = "bg_demon_border"
click_to_activate = FALSE
cooldown_time = 4 SECONDS
melee_cooldown_time = 0
shared_cooldown = NONE
projectile_type = /obj/projectile/tentacle_lash/stab
/datum/action/cooldown/mob_cooldown/projectile_attack/tendril_melee/Activate(atom/target_atom, warning = TRUE)
if (warning)
for (var/stab_dir in GLOB.alldirs)
var/turf/open/stab_turf = get_step(owner, stab_dir)
if (!istype(stab_turf))
continue
var/obj/effect/temp_visual/telegraphing/line/short/telegraph = new(stab_turf)
telegraph.dir = stab_dir
SLEEP_CHECK_DEATH(0.5 SECONDS, owner)
for (var/stab_dir in GLOB.alldirs)
shoot_projectile(get_turf(owner), get_step(owner, stab_dir), firer = owner)
SLEEP_CHECK_DEATH(0.5 SECONDS, owner)
StartCooldown()
return TRUE
@@ -0,0 +1,35 @@
/datum/ai_controller/basic_controller/tendril
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
planning_subtrees = list(
/datum/ai_planning_subtree/call_reinforcements/mining,
/datum/ai_planning_subtree/target_retaliate/check_faction,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/targeted_mob_ability/tendril_chaser,
/datum/ai_planning_subtree/use_mob_ability/tendril_spikes,
/datum/ai_planning_subtree/use_mob_ability/tendril_lash,
)
/datum/ai_planning_subtree/targeted_mob_ability/tendril_chaser
ability_key = BB_TENDRIL_CHASER
operational_datums = list(/datum/component/ai_target_timer)
finish_planning = FALSE
/datum/ai_planning_subtree/use_mob_ability/tendril_lash
ability_key = BB_TENDRIL_LASH
/datum/ai_planning_subtree/use_mob_ability/tendril_lash/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
if (!controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
return FALSE
return ..()
/datum/ai_planning_subtree/use_mob_ability/tendril_spikes
ability_key = BB_TENDRIL_SPIKES
/datum/ai_planning_subtree/use_mob_ability/tendril_spikes/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
if (!controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
return FALSE
return ..()
@@ -1,12 +1,16 @@
/datum/ai_controller/basic_controller/watcher
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining,
BB_RANGED_SKIRMISH_MIN_DISTANCE = 3,
BB_RANGED_SKIRMISH_MAX_DISTANCE = 5,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/call_reinforcements/mining,
/datum/ai_planning_subtree/target_retaliate/check_faction,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/maintain_distance,
@@ -255,13 +255,13 @@
/mob/living/basic/mining_drone/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(!.)
return FALSE
if(.)
return
if(!istype(target, /mob/living/basic/node_drone))
return TRUE
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
repair_node_drone(target)
return FALSE
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)
@@ -103,21 +103,21 @@
/mob/living/basic/pet/cat/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(!.)
return FALSE
if(.)
return
if(istype(target, /obj/machinery/oven/range) && can_interact_with_stove)
target.attack_hand(src)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
if(!can_hold_item)
return TRUE
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
if(!is_type_in_list(target, huntable_items) || held_food)
return TRUE
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
var/atom/movable/movable_target = target
movable_target.forceMove(src)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/mob/living/basic/pet/cat/Exited(atom/movable/gone, direction)
. = ..()
@@ -35,6 +35,8 @@
attack_sound = 'sound/items/weapons/bite.ogg'
attack_vis_effect = ATTACK_EFFECT_BITE
melee_attack_cooldown = 0.8 SECONDS
melee_damage_lower = 5
melee_damage_upper = 10
/// Instructions you can give to dogs
var/static/list/pet_commands = list(
/datum/pet_command/idle,
@@ -63,14 +63,14 @@
/mob/living/basic/orbie/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(!.)
return FALSE
if(.)
return
if(src == target || happy_state || !istype(target))
return TRUE
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
toggle_happy_state()
addtimer(CALLBACK(src, PROC_REF(toggle_happy_state)), 30 SECONDS)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/mob/living/basic/orbie/proc/on_lights(datum/source)
SIGNAL_HANDLER
@@ -282,14 +282,14 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
/// This is pretty much meant for players, AI will use the task-specific procs instead.
/mob/living/basic/parrot/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(!.)
return FALSE
if(.)
return
if(isitem(target) && steal_from_ground(target))
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
if(iscarbon(target) && steal_from_mob(target))
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/// Picks up an item from the ground and puts it in our claws. Returns TRUE if we picked it up, FALSE otherwise.
/mob/living/basic/parrot/proc/steal_from_ground(obj/item/target)
@@ -7,7 +7,7 @@
/// Poly has only just survived a round, and is doing a consecutive one.
#define POLY_CONSECUTIVE_ROUND "consecutive_round"
/// haunt filter we apply to who we possess
#define POLY_POSSESS_FILTER
#define POLY_POSSESS_FILTER "poly_possess_filter"
/// haunt filter color we apply to who we possess
#define POLY_POSSESS_GLOW "#522059"
@@ -5,5 +5,7 @@
mob_biotypes = MOB_ORGANIC|MOB_BEAST
default_blood_volume = BLOOD_VOLUME_NORMAL
basic_mob_flags = SENDS_DEATH_MOODLETS
melee_damage_lower = 5
melee_damage_upper = 5
/// if the mob is protected from being renamed by collars.
var/unique_pet = FALSE
@@ -138,7 +138,7 @@ GLOBAL_LIST_INIT(animatable_blacklist, typecacheof(list(
/mob/living/basic/mimic/crate/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
if(target == src)
toggle_open()
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
return ..()
/mob/living/basic/mimic/crate/CanAllowThrough(atom/movable/mover, border_dir)
@@ -411,10 +411,13 @@ GLOBAL_LIST_INIT(animatable_blacklist, typecacheof(list(
return TRUE
/mob/living/basic/mimic/copy/ranged/CopyObject(obj/item/gun/original, mob/living/creator, destroy_original = 0)
if(..())
obj_damage = 0
melee_damage_upper = original.force
melee_damage_lower = original.force - max(0, (original.force / 2))
. = ..()
if(!.)
return
obj_damage = 0
melee_damage_upper = original.force
melee_damage_lower = original.force - max(0, (original.force / 2))
RegisterSignal(original, COMSIG_GUN_REPLENISHED_CHARGE, PROC_REF(on_regained_charge))
/mob/living/basic/mimic/copy/ranged/CopyObjectVisuals(obj/original)
name = original.name
@@ -424,4 +427,9 @@ GLOBAL_LIST_INIT(animatable_blacklist, typecacheof(list(
/mob/living/basic/mimic/copy/ranged/can_use_guns(obj/item/gun)
return TRUE
/// If our gun ran out of ammo but then regenerated it then we should go back to shooting
/mob/living/basic/mimic/copy/ranged/proc/on_regained_charge()
SIGNAL_HANDLER
ai_controller?.set_blackboard_key(BB_GUNMIMIC_GUN_EMPTY, FALSE)
#undef CANT_INSERT_FULL
@@ -87,3 +87,30 @@
return
controller.queue_behavior(/datum/ai_behavior/basic_ranged_attack/avoid_friendly_fire, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
return SUBTREE_RETURN_FINISH_PLANNING //we are going into battle...no distractions.
/// Special subtree for living wands/staffs of animation which will focus on animating more things
/datum/ai_controller/basic_controller/mimic_copy/gun/animator
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_HUNT_TARGETING_STRATEGY = /datum/targeting_strategy/anything,
BB_GUNMIMIC_GUN_EMPTY = FALSE,
)
planning_subtrees = list(
/datum/ai_planning_subtree/shoot_animatable_objects,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/random_speech/when_has_target/mimic/gun,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
/// Try to find objects and then shoot them
/datum/ai_planning_subtree/shoot_animatable_objects
/datum/ai_planning_subtree/shoot_animatable_objects/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
if(controller.blackboard[BB_GUNMIMIC_GUN_EMPTY])
return // No charge in our gun
if(!controller.blackboard_key_exists(BB_CURRENT_HUNTING_TARGET))
controller.queue_behavior(/datum/ai_behavior/find_and_set/animatable, BB_CURRENT_HUNTING_TARGET)
return
controller.queue_behavior(/datum/ai_behavior/ranged_skirmish, BB_CURRENT_HUNTING_TARGET, BB_HUNT_TARGETING_STRATEGY, null, 9, 0)
controller.queue_behavior(/datum/ai_behavior/clear_key, BB_CURRENT_HUNTING_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
@@ -320,4 +320,4 @@
/// If someone slaps one of the school, scatter
/mob/living/basic/carp/passive/proc/on_attacked(mob/living/attacker)
for(var/mob/living/basic/carp/passive/schoolmate in oview(src, 9))
schoolmate.ai_controller?.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker)
schoolmate.ai_controller?.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time)
@@ -2,38 +2,47 @@
GLOBAL_LIST_INIT(magicarp_spell_types, list(
/obj/projectile/magic/animate = "dancing",
/obj/projectile/magic/arcane_barrage = "arcane",
/obj/projectile/magic/babel = "babbling",
/obj/projectile/magic/change = "transforming",
/obj/projectile/magic/death = "grim",
/obj/projectile/magic/door = "unbarred",
/obj/projectile/magic/fireball = "blazing",
/obj/projectile/magic/freeze = "frigid",
/obj/projectile/magic/levitate = "weightless",
/obj/projectile/magic/resurrection = "vital",
/obj/projectile/magic/spellblade = "vorpal",
/obj/projectile/magic/swap = "quantum",
/obj/projectile/magic/teleport = "warping",
/obj/projectile/magic/babel = "babbling",
))
/// A reduced list of spells for magicarp spawned in xenobiology, less disruptive
GLOBAL_LIST_INIT(xenobiology_magicarp_spell_types, list(
/obj/projectile/magic/animate = "dancing",
/obj/projectile/magic/teleport = "warping",
/obj/projectile/magic/arcane_barrage = "arcane",
/obj/projectile/magic/door = "unbarred",
/obj/projectile/magic/fireball = "blazing",
/obj/projectile/magic/freeze = "frigid",
/obj/projectile/magic/levitate = "weightless",
/obj/projectile/magic/spellblade = "vorpal",
/obj/projectile/magic/arcane_barrage = "arcane",
/obj/projectile/magic/swap = "quantum",
/obj/projectile/magic/teleport = "warping",
))
/// Associative list of magicarp spells to colours, expand this list if you expand the other lists
GLOBAL_LIST_INIT(magicarp_spell_colours, list(
/obj/projectile/magic/animate = COLOR_CARP_RUSTY,
/obj/projectile/magic/arcane_barrage = COLOR_CARP_PURPLE,
/obj/projectile/magic/babel = COLOR_CARP_BROWN,
/obj/projectile/magic/change = COLOR_CARP_PINK,
/obj/projectile/magic/death = COLOR_CARP_DARK_BLUE,
/obj/projectile/magic/door = COLOR_CARP_GREEN,
/obj/projectile/magic/fireball = COLOR_CARP_RED,
/obj/projectile/magic/freeze = COLOR_CARP_TURQUOISE,
/obj/projectile/magic/levitate = COLOR_CARP_DARK_PURPLE,
/obj/projectile/magic/resurrection = COLOR_CARP_PALE_GREEN,
/obj/projectile/magic/spellblade = COLOR_CARP_SILVER,
/obj/projectile/magic/swap = COLOR_CARP_LIGHT_BLUE,
/obj/projectile/magic/teleport = COLOR_CARP_GRAPE,
/obj/projectile/magic/babel = COLOR_CARP_BROWN,
))
/**
@@ -19,7 +19,7 @@
damage_coeff = list(BRUTE = 1, BURN = 1.25, TOX = 4, STAMINA = 1, OXY = 1)
basic_mob_flags = FLAMMABLE_MOB
status_flags = NONE
speed = -0.1
speed = 0.2
maxHealth = 90
health = 90
melee_damage_lower = 15
@@ -61,7 +61,7 @@
)
AddComponent(\
/datum/component/regenerator,\
regeneration_delay = 4 SECONDS,\
regeneration_delay = 3 SECONDS,\
brute_per_second = maxHealth / 6,\
outline_colour = COLOR_PINK,\
)
@@ -96,16 +96,16 @@
/mob/living/basic/eyeball/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(!.)
return FALSE
if(.)
return
if(!ishuman(target))
return TRUE
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
var/mob/living/carbon/human_target = target
var/obj/item/organ/eyes/eyes = human_target.get_organ_slot(ORGAN_SLOT_EYES)
if(isnull(eyes) || eyes.damage < 10)
return TRUE
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
heal_eye_damage(human_target, eyes)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/mob/living/basic/eyeball/proc/heal_eye_damage(mob/living/target, obj/item/organ/eyes/eyes)
if(!COOLDOWN_FINISHED(src, eye_healing))
@@ -121,7 +121,7 @@
if (!istype(attacker))
return
for (var/mob/living/basic/garden_gnome/potential_gnome in oview(src, 7))
potential_gnome.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker)
potential_gnome.ai_controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time)
/datum/ai_controller/basic_controller/garden_gnome
blackboard = list(
@@ -99,16 +99,16 @@
/mob/living/basic/hivebot/mechanic/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(!.)
return FALSE
if(.)
return
if(ismachinery(target))
repair_machine(target)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
if(istype(target, /mob/living/basic/hivebot))
repair_hivebot(target)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/mob/living/basic/hivebot/mechanic/proc/repair_machine(obj/machinery/fixable)
if(fixable.get_integrity() >= fixable.max_integrity)
@@ -41,18 +41,14 @@
minimum_survivable_temperature = 0
maximum_survivable_temperature = 1500
obj_damage = 0
pull_force = 0
environment_smash = ENVIRONMENT_SMASH_NONE
ai_controller = /datum/ai_controller/basic_controller/lightgeist
/mob/living/basic/lightgeist/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
ADD_TRAIT(src, TRAIT_MEDICAL_HUD, INNATE_TRAIT)
remove_verb(src, /mob/living/verb/pulled)
remove_verb(src, /mob/verb/me_verb)
add_traits(list(TRAIT_VENTCRAWLER_ALWAYS, TRAIT_MEDICAL_HUD, TRAIT_EMOTEMUTE), INNATE_TRAIT)
AddElement(/datum/element/simple_flying)
AddComponent(\
/datum/component/healing_touch,\
@@ -45,6 +45,8 @@
var/position_variance = 8
/// Damage to deal on impale
var/impale_damage = 15
/// Wound bonus on impale
var/impale_wound_bonus = 0
/// Typecache of types of mobs not to damage
var/list/damage_blacklist_typecache = list(
/mob/living/basic/meteor_heart,
@@ -67,14 +69,22 @@
/obj/effect/temp_visual/emerging_ground_spike/proc/impale()
if (!isturf(loc))
return
var/hit_someone = FALSE
for(var/mob/living/victim in loc)
if (is_type_in_typecache(victim, damage_blacklist_typecache))
continue
hit_someone = TRUE
var/target_zone = victim.resting ? BODY_ZONE_CHEST : pick_weight(standing_damage_zones)
victim.apply_damage(impale_damage, damagetype = BRUTE, def_zone = target_zone, sharpness = SHARP_POINTY)
if (harm_mob(victim))
hit_someone = TRUE
if (hit_someone)
playsound(src, 'sound/items/weapons/slice.ogg', vol = 50, vary = TRUE, pressure_affected = FALSE)
else
playsound(src, 'sound/misc/splort.ogg', vol = 25, vary = TRUE, pressure_affected = FALSE)
return hit_someone
/obj/effect/temp_visual/emerging_ground_spike/proc/harm_mob(mob/living/victim)
if (is_type_in_typecache(victim, damage_blacklist_typecache))
return FALSE
var/target_zone = victim.resting ? BODY_ZONE_CHEST : pick_weight(standing_damage_zones)
victim.apply_damage(impale_damage, damagetype = BRUTE, def_zone = target_zone, wound_bonus = impale_wound_bonus, sharpness = SHARP_POINTY)
return TRUE
@@ -149,29 +149,29 @@
/// Handles the logic for attacking anything.
/mob/living/basic/morph/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(!.)
return FALSE
if(.)
return
if(HAS_TRAIT(src, TRAIT_DISGUISED) && (melee_damage_disguised <= 0))
balloon_alert(src, "can't attack while disguised!")
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
if(isliving(target)) //Eat Corpses to regen health
var/mob/living/living_target = target
if(living_target.stat != DEAD)
return TRUE
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
eat(eatable = living_target, delay = 3 SECONDS, update_health = -50)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
if(!isitem(target)) //Eat items just to be annoying
return TRUE
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
var/obj/item/item_target = target
if(item_target.anchored)
return TRUE
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
eat(eatable = item_target, delay = 2 SECONDS)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/// Eat stuff. Delicious. Return TRUE if we ate something, FALSE otherwise.
/// Required: `eatable` is the thing (item or mob) that we are going to eat.
@@ -171,22 +171,22 @@
/// Checks if we are able to attack this object, as well as send out the signal to see if we get any special regal rat interactions.
/mob/living/basic/regal_rat/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(!.)
return FALSE
if(.)
return
if(DOING_INTERACTION(src, REGALRAT_INTERACTION) || !allowed_to_attack(target))
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN
if(SEND_SIGNAL(target, COMSIG_RAT_INTERACT, src) & COMPONENT_RAT_INTERACTED)
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
if(isnull(mind) || combat_mode)
return TRUE
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
if(poison_target(target))
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
return TRUE
return BASIC_MOB_CONTINUE_ATTACK_CHAIN
/// Checks if we are allowed to attack this mob. Will return TRUE if we are potentially allowed to attack, but if we end up in a case where we should NOT attack, return FALSE.
/mob/living/basic/regal_rat/proc/allowed_to_attack(atom/the_target)
@@ -62,6 +62,8 @@
var/draining = FALSE
/// Have we already given this revenant abilities?
var/generated_objectives_and_spells = FALSE
/// ckey of the player who controlled this mob when it was killed
var/old_ckey = ""
/// Lazylist of drained mobs to ensure that we don't steal a soul from someone twice
var/list/drained_mobs = null
@@ -348,10 +350,7 @@
visible_message(span_danger("[src]'s body breaks apart into a fine pile of blue dust."))
var/obj/item/ectoplasm/revenant/goop = new(get_turf(src)) // the ectoplasm will handle moving us out of dormancy
goop.old_ckey = client.ckey
goop.revenant = src
forceMove(goop)
new /obj/item/ectoplasm/revenant(get_turf(src), src) // the ectoplasm will handle moving us out of dormancy
/mob/living/basic/revenant/proc/on_move(datum/source, atom/entering_loc)
SIGNAL_HANDLER
@@ -524,4 +523,30 @@
if(!HAS_TRAIT(src, TRAIT_REVENANT_REVEALED) && !istype(reflecting_in, /obj/structure/mirror/magic))
apply_wibbly_filters(reflection)
/mob/living/basic/revenant/proc/get_new_user()
message_admins("A poll for the reforming revenant was created.")
var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to be [span_notice(name)] (reforming)?", check_jobban = ROLE_REVENANT, role = ROLE_REVENANT, poll_time = 5 SECONDS, checked_target = src, alert_pic = src, role_name_text = "reforming revenant", chat_text_border_icon = src)
if(!chosen_one)
message_admins("No candidates were found for the new revenant.")
visible_message(span_revenwarning("A blue dust appears from thin air and settles down."))
new /obj/item/ectoplasm/revenant(get_turf(src)) // inert
qdel(src)
return
PossessByPlayer(chosen_one.key)
message_admins("[chosen_one.key] has been made into the reformed revenant via poll.")
qdel(chosen_one)
/mob/living/basic/revenant/proc/reform(cause)
if(QDELETED(src))
return FALSE
death_reset()
if(isnull(client))
INVOKE_ASYNC(src, PROC_REF(get_new_user))
return TRUE
message_admins("[client.ckey] has been remade into a revenant.")
log_message("was remade as a revenant.", LOG_GAME)
return TRUE
#undef REVENANT_STUNNED_TRAIT
@@ -1,7 +1,7 @@
#define REVENANT_DEFILE_MIN_DAMAGE 30
#define REVENANT_DEFILE_MAX_DAMAGE 50
//Transmit: the revemant's only direct way to communicate. Sends a single message silently to a single mob
//Transmit: the revenant's only direct way to communicate. Sends a single message silently to a single mob
/datum/action/cooldown/spell/list_target/telepathy/revenant
name = "Revenant Transmit"
background_icon_state = "bg_revenant"
@@ -12,6 +12,14 @@
antimagic_flags = MAGIC_RESISTANCE_HOLY|MAGIC_RESISTANCE_MIND
/datum/action/cooldown/spell/list_target/telepathy/revenant/get_list_targets(atom/center, target_radius = 7)
if(!istype(center, /mob/living/basic/revenant))
return ..()
var/mob/living/basic/revenant/revenant = center
if(!revenant.dormant)
return ..()
return ..(get_turf(revenant), 2)
/datum/action/cooldown/spell/aoe/revenant
background_icon_state = "bg_revenant"
overlay_icon_state = "bg_revenant_border"
@@ -5,31 +5,36 @@
icon = 'icons/effects/effects.dmi'
icon_state = "revenantEctoplasm"
w_class = WEIGHT_CLASS_SMALL
/// Are we currently reforming?
var/reforming = TRUE
/// Are we inert (aka distorted such that we can't reform)?
// Can the revenant reform?
var/inert = FALSE
/// The key of the revenant that we started the reform as
var/old_ckey
/// The revenant we're currently storing
var/mob/living/basic/revenant/revenant
/obj/item/ectoplasm/revenant/Initialize(mapload)
/obj/item/ectoplasm/revenant/Initialize(mapload, revenant)
. = ..()
addtimer(CALLBACK(src, PROC_REF(try_reform)), 1 MINUTES)
inert = !revenant
if(revenant)
AddComponent(/datum/component/revenant_prison, revenant = revenant)
addtimer(CALLBACK(src, PROC_REF(reform)), 1 MINUTES)
/obj/item/ectoplasm/revenant/Destroy()
if(!QDELETED(revenant))
qdel(revenant)
return ..()
/obj/item/ectoplasm/revenant/proc/check_for_mirrors(turf/location, radius)
PRIVATE_PROC(TRUE)
for(var/obj/structure/mirror/mirror in view(radius, location))
if(mirror.cursable && !mirror.GetComponent(/datum/component/revenant_prison))
return mirror
return null
/obj/item/ectoplasm/revenant/attack_self(mob/user)
if(!reforming || inert)
if(inert)
return ..()
user.visible_message(
span_notice("[user] scatters [src] in all directions."),
span_notice("You scatter [src] across the area. The particles slowly fade away."),
span_notice("You scatter [src] across the area."),
)
var/obj/structure/mirror/nearby_mirror = check_for_mirrors(drop_location(), 5)
if(nearby_mirror)
transfer_to_mirror(nearby_mirror)
user.dropItemToGround(src)
qdel(src)
@@ -37,14 +42,25 @@
. = ..()
if(inert)
return
visible_message(span_notice("[src] breaks into particles upon impact, which fade away to nothingness."))
var/obj/structure/mirror/nearby_mirror = check_for_mirrors(get_turf(hit_atom), 3)
if(!nearby_mirror)
visible_message(span_notice("[src] breaks into particles upon impact, which fade away to nothingness."))
else
transfer_to_mirror(nearby_mirror)
qdel(src)
/obj/item/ectoplasm/revenant/proc/transfer_to_mirror(obj/structure/mirror/nearby_mirror)
PRIVATE_PROC(TRUE)
nearby_mirror.TakeComponent(GetComponent(/datum/component/revenant_prison))
nearby_mirror.visible_message(span_revenwarning("A dismal moan echoes as particles of [src] fall onto [nearby_mirror]!"))
log_game("A revenant was trapped inside [nearby_mirror]")
message_admins("A revenant was trapped inside [nearby_mirror] [ADMIN_JMP(nearby_mirror)]")
/obj/item/ectoplasm/revenant/examine(mob/user)
. = ..()
if(inert)
. += span_revennotice("It seems inert.")
else if(reforming)
else
. += span_revenwarning("It is shifting and distorted. It would be wise to destroy this.")
/obj/item/ectoplasm/revenant/suicide_act(mob/living/user)
@@ -52,45 +68,13 @@
qdel(src)
return OXYLOSS
/obj/item/ectoplasm/revenant/proc/try_reform()
if(reforming)
reforming = FALSE
reform()
else
inert = TRUE
visible_message(span_warning("[src] settles down and seems lifeless."))
/// Actually moves the revenant out of ourself
/obj/item/ectoplasm/revenant/proc/reform()
if(QDELETED(src) || QDELETED(revenant) || inert)
if(QDELETED(src) || inert)
return
if(!GetComponent(/datum/component/revenant_prison))
return
message_admins("Revenant ectoplasm was left undestroyed for 1 minute and is reforming into a new revenant.")
forceMove(drop_location()) //In case it's in a backpack or someone's hand
var/user_name = old_ckey
if(isnull(revenant.client))
var/mob/potential_user = get_new_user()
revenant.PossessByPlayer(potential_user.key)
user_name = potential_user.ckey
qdel(potential_user)
message_admins("[user_name] has been [old_ckey == user_name ? "re":""]made into a revenant by reforming ectoplasm.")
revenant.log_message("was [old_ckey == user_name ? "re":""]made as a revenant by reforming ectoplasm.", LOG_GAME)
SEND_SIGNAL(src, COMSIG_REVENANT_RELEASE, cause = "ectoplasm reforming")
visible_message(span_revenboldnotice("[src] suddenly rises into the air before fading away."))
revenant.death_reset()
revenant = null
qdel(src)
/// Handles giving the revenant a new client to control it
/obj/item/ectoplasm/revenant/proc/get_new_user()
message_admins("The new revenant's old client either could not be found or is in a new, living mob - grabbing a random candidate instead...")
var/mob/chosen_one = SSpolling.poll_ghosts_for_target("Do you want to be [span_notice(revenant.name)] (reforming)?", check_jobban = ROLE_REVENANT, role = ROLE_REVENANT, poll_time = 5 SECONDS, checked_target = revenant, alert_pic = revenant, role_name_text = "reforming revenant", chat_text_border_icon = revenant)
if(isnull(chosen_one))
message_admins("No candidates were found for the new revenant.")
inert = TRUE
visible_message(span_revenwarning("[src] settles down and seems lifeless."))
qdel(revenant)
return null
return chosen_one
@@ -565,7 +565,6 @@
unsuitable_cold_damage = 1
unsuitable_heat_damage = 1
menu_description = "Stronger assassin spider variant with an unmatched speed, high amount of health and very deadly poison, but deals very low amount of damage. It also has ability to ventcrawl."
apply_spider_antag = FALSE
innate_actions = list(
/datum/action/cooldown/mob_cooldown/lay_web/sticky_web,
/datum/action/cooldown/mob_cooldown/lay_web/web_spikes,
@@ -591,7 +590,6 @@
melee_damage_lower = 15
melee_damage_upper = 20
ai_controller = /datum/ai_controller/basic_controller/giant_spider/retaliate
apply_spider_antag = FALSE
/mob/living/basic/spider/giant/sgt_araneus/Initialize(mapload)
. = ..()
@@ -51,8 +51,6 @@
var/directive = ""
/// Short description of what this mob is capable of, for radial menu uses
var/menu_description = "Tanky and strong for the defense of the nest and other spiders."
/// If true then you shouldn't be told that you're a spider antagonist as soon as you are placed into this mob
var/apply_spider_antag = TRUE
/// Commands you can give this spider once it is tamed
var/static/list/tamed_commands = list(
/datum/pet_command/idle,
@@ -221,7 +219,6 @@
response_harm_continuous = "splats"
response_harm_simple = "splat"
ai_controller = /datum/ai_controller/basic_controller/giant_spider/pest
apply_spider_antag = FALSE
///list of pet commands we follow
var/static/list/pet_commands = list(
/datum/pet_command/idle,
@@ -49,8 +49,8 @@
/// Proc that we call on attacking something to dust 'em.
/mob/living/basic/supermatter_spider/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(!.)
return FALSE
if(.)
return
if(isliving(target))
var/mob/living/victim = target
@@ -59,14 +59,14 @@
victim.dust()
if(single_use)
death()
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
if(!isturf(target))
dust_feedback(target)
qdel(target)
if(single_use)
death()
return FALSE
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
/// Simple proc that plays the supermatter dusting sound and sends a visible message.
/mob/living/basic/supermatter_spider/proc/dust_feedback(atom/target)
+3 -3
View File
@@ -68,11 +68,11 @@
var/turf/open/our_turf = src.loc
if(!our_turf.air || !our_turf.air.gases[/datum/gas/carbon_dioxide])
return
var/co2 = our_turf.air.gases[/datum/gas/carbon_dioxide][MOLES]
var/datum/gas_mixture/our_air = our_turf.air
var/co2 = our_air.gases[/datum/gas/carbon_dioxide][MOLES]
if(co2 > 0 && SPT_PROB(13, seconds_per_tick))
var/amt = min(co2, 9)
our_turf.air.gases[/datum/gas/carbon_dioxide][MOLES] -= amt
our_air.adjust_gas(/datum/gas/carbon_dioxide, -amt)
our_turf.atmos_spawn_air("[GAS_O2]=[amt]")
/mob/living/basic/tree/melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE)
@@ -98,4 +98,4 @@
if (!istype(attacker))
return
for (var/mob/living/basic/trooper/nanotrasen/potential_trooper in oview(src, 7))
potential_trooper.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker)
potential_trooper.ai_controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time)