mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-23 05:00:55 +01:00
Merge remote-tracking branch 'tgstation/master' into upstream-6-2-2026
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]!")
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user