Adds basic megafauna: Bluespace Horror, Vox Armalis (#31480)

* Adds basic megafauna: Bluespace Horror, Vox Armalis

* Armalis improvements, code review addressed

* Lints

* Update code/modules/mob/living/basic/hostile/megafauna/basic_megafauna.dm

Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>
Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>

* Update code/modules/mob/living/basic/hostile/megafauna/bluespace_horror/bluespace_horror.dm

Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>
Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>

* Update code/modules/mob/living/basic/hostile/megafauna/vox_armalis/vox_armalis.dm

Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>
Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>

---------

Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>
Co-authored-by: DGamerL <108773801+DGamerL@users.noreply.github.com>
This commit is contained in:
PollardTheDragon
2026-02-11 20:23:19 -05:00
committed by GitHub
parent 55c351f4ee
commit f2b81ea285
15 changed files with 1058 additions and 2 deletions
@@ -0,0 +1,210 @@
// Base type for basic mob megafauna
/mob/living/basic/megafauna
name = "megafauna"
desc = "Attack the weak point for massive damage."
health = 1000
maxHealth = 1000
a_intent = INTENT_HARM
sentience_type = SENTIENCE_BOSS
environment_smash = ENVIRONMENT_SMASH_RWALLS
mob_biotypes = MOB_ORGANIC | MOB_EPIC
obj_damage = 400
light_range = 3
faction = list("boss")
weather_immunities = list("lava","ash")
atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
damage_coeff = list(BRUTE = 1, BURN = 0.5, TOX = 1, CLONE = 1, STAMINA = 0, OXY = 1)
minimum_survivable_temperature = 0
maximum_survivable_temperature = INFINITY
speed = 0
move_force = MOVE_FORCE_OVERPOWERING
move_resist = MOVE_FORCE_OVERPOWERING
pull_force = MOVE_FORCE_OVERPOWERING
mob_size = MOB_SIZE_LARGE
layer = LARGE_MOB_LAYER // Looks weird with them slipping under mineral walls and cameras and shit otherwise
flags_2 = IMMUNE_TO_SHUTTLECRUSH_2
initial_traits = list(TRAIT_FLYING)
/// The loot the mob should drop if killed with a crusher
var/list/crusher_loot = list()
/// The type of medal it drops if on hard mode
var/medal_type
/// The type of boss for the medal it drops
var/score_type
/// Is the mob truly dead?
var/elimination = FALSE
/// The mob's internal GPS, if it has one
var/obj/item/gps/internal_gps
/// If this is a megafauna that should grant achievements, or have a gps signal
var/true_spawn = TRUE
/// Has someone enabled hard mode?
var/enraged = FALSE
/// Path of the hardmode loot disk, if applicable.
var/enraged_loot
/// How much ore should killing this give
var/difficulty_ore_modifier = 0
/// Actions to grant on Initialize
var/list/innate_actions = list()
/mob/living/basic/megafauna/Initialize(mapload)
. = ..()
GLOB.alive_megafauna_list |= UID()
if(internal_gps && true_spawn)
internal_gps = new internal_gps(src)
grant_actions_by_list(innate_actions)
generate_random_loot()
RegisterSignal(src, , PROC_REF(hoverboard_deactivation))
/mob/living/basic/megafauna/Destroy()
QDEL_NULL(internal_gps)
UnregisterSignal(src, COMSIG_HOSTILE_FOUND_TARGET)
GLOB.alive_megafauna_list -= UID()
return ..()
/mob/living/basic/megafauna/melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(isliving(target))
var/mob/living/L = target
if(L.stat == DEAD)
devour(L)
/mob/living/basic/megafauna/can_die()
return ..() && health <= 0
/mob/living/basic/megafauna/death(gibbed)
GLOB.alive_megafauna_list -= UID()
// lets normalize the icons a bit
pixel_x = 0
pixel_y = 0
// this happens before the parent call because `del_on_death` may be set
if(can_die() && !admin_spawned)
var/datum/status_effect/crusher_damage/C = has_status_effect(STATUS_EFFECT_CRUSHERDAMAGETRACKING)
if(C && length(crusher_loot) && C.total_damage >= maxHealth * 0.6)
spawn_crusher_loot()
if(enraged && length(loot) && enraged_loot) // Don't drop a disk if the boss drops no loot. Important for legion.
for(var/mob/living/M in urange(20, src)) // Yes big range, but for bubblegum arena
if(M.client)
loot += enraged_loot // Disk for each miner / borg.
if(!elimination) // used so the achievment only occurs for the last legion to die.
SSblackbox.record_feedback("tally", "megafauna_kills", 1, "[initial(name)]")
calc_ore_reward()
return ..()
/// If the megafauna has a pool of random loot items to pick from, override this proc to have it be set on initialization
/mob/living/basic/megafauna/proc/generate_random_loot()
return list()
/// Handling the ore part of the mega reward, the higher the difficulty_ore_modifier the more ore will spawn
/mob/living/basic/megafauna/proc/calc_ore_reward()
var/list/common_ore = list(
/obj/item/stack/ore/uranium,
/obj/item/stack/ore/silver,
/obj/item/stack/ore/gold,
/obj/item/stack/ore/plasma,
/obj/item/stack/ore/titanium)
var/list/rare_ore = list(
/obj/item/stack/ore/diamond,
/obj/item/stack/ore/bluespace_crystal)
for(var/ore in common_ore)
var/obj/item/stack/O = new ore(loc)
O.amount = roll(difficulty_ore_modifier * 2, 4 + difficulty_ore_modifier)
for(var/ore in rare_ore)
var/obj/item/stack/O = new ore(loc)
O.amount = roll(difficulty_ore_modifier, 4 + difficulty_ore_modifier)
/mob/living/basic/megafauna/proc/spawn_crusher_loot()
loot += crusher_loot
/mob/living/basic/megafauna/on_changed_z_level(turf/old_turf, turf/new_turf)
. = ..()
if(!istype(get_area(src), /area/shuttle))
unrage()
/mob/living/basic/megafauna/onShuttleMove(turf/oldT, turf/T1, rotation, mob/caller_mob)
var/turf/oldloc = loc
. = ..()
if(!.)
return
var/turf/newloc = loc
message_admins("Megafauna [src] \
([ADMIN_FLW(src,"FLW")]) \
moved via shuttle from ([oldloc.x], [oldloc.y], [oldloc.z]) to \
([newloc.x], [newloc.y], [newloc.z])[caller_mob ? " called by [ADMIN_LOOKUP(caller_mob)]" : ""]")
/mob/living/basic/megafauna/proc/devour(mob/living/L)
if(!L)
return FALSE
visible_message(
"<span class='danger'>[src] devours [L]!</span>",
"<span class='userdanger'>You feast on [L], restoring your health!</span>")
if(!is_station_level(z) || client) // NPC monsters won't heal while on station
adjustBruteLoss(-L.maxHealth/2)
L.gib()
return TRUE
/mob/living/basic/megafauna/ex_act(severity, target)
switch(severity)
if(EXPLODE_DEVASTATE)
adjustBruteLoss(250)
if(EXPLODE_HEAVY)
adjustBruteLoss(100)
if(EXPLODE_LIGHT)
adjustBruteLoss(50)
/// This proc is called by the HRD-MDE grenade to enrage the megafauna. This should increase the megafaunas attack speed if possible, give it new moves, or disable weak moves. This should be reverseable, and reverses on zlvl change.
/mob/living/basic/megafauna/proc/enrage()
if(enraged || ((health / maxHealth) * 100 <= 80))
return
difficulty_ore_modifier += 4 // Hardmode only helps the station more and gives you bragging rights, no special items for hardmode
enraged = TRUE
/mob/living/basic/megafauna/proc/unrage()
difficulty_ore_modifier -= 4
enraged = FALSE
// MARK: PTL Interaction
/mob/living/basic/megafauna/on_ptl_target(obj/machinery/power/transmission_laser/ptl)
ptl.RegisterSignal(src, COMSIG_MOB_DEATH, TYPE_PROC_REF(/obj/machinery/power/transmission_laser, untarget), ptl)
if(ptl?.firing)
on_ptl_fire(ptl)
return
/mob/living/basic/megafauna/on_ptl_tick(obj/machinery/power/transmission_laser/ptl, output_level)
loot = list() // disable loot drops form the target to prevent cheese
if(10 * output_level * damage_coeff[BURN] / (1 MW) > health) // If we would kill the target dust it.
health = 0 // We need this so can_die() won't prevent dusting
visible_message("<span class='danger'>\The [src] is reduced to dust by the beam!</span>")
dust()
else
adjustFireLoss(10 * output_level / (1 MW))
/mob/living/basic/megafauna/on_ptl_untarget(obj/machinery/power/transmission_laser/ptl)
on_ptl_stop(ptl)
if(ptl)
ptl.UnregisterSignal(src, COMSIG_MOB_DEATH)
/mob/living/basic/megafauna/on_ptl_fire(obj/machinery/power/transmission_laser/ptl)
var/orbital_strike = image(icon, src, "orbital_strike", FLY_LAYER, SOUTH)
add_overlay(orbital_strike)
/mob/living/basic/megafauna/on_ptl_stop(obj/machinery/power/transmission_laser/ptl)
for(var/image/overlay in overlays)
if(overlay.name == "orbital_strike")
cut_overlay(overlay)
/mob/living/basic/megafauna/proc/hoverboard_deactivation(source, target)
SIGNAL_HANDLER // COMSIG_HOSTILE_FOUND_TARGET
if(!isliving(target))
return
var/mob/living/L = target
if(!L.buckled)
return
if(!istype(L.buckled, /obj/tgvehicle/scooter/skateboard/hoverboard))
return
var/obj/tgvehicle/scooter/skateboard/hoverboard/cursed_board = L.buckled
// Not a visible message, as walls or such may be in the way
to_chat(L, "<span class='userdanger'><b>You hear a loud roar in the distance, and the lights on [cursed_board] begin to spark dangerously, as the board rumbles heavily!</b></span>")
playsound(get_turf(src), 'sound/effects/tendril_destroyed.ogg', 200, FALSE, 50, TRUE, TRUE)
cursed_board.necropolis_curse()
@@ -0,0 +1,182 @@
/mob/living/basic/megafauna/bluespace_horror
name = "bluespace horror"
desc = "An indescribable creature, mutated from extended exposure to bluespace energies. There is no telling what it once was."
health = 1500
maxHealth = 1500
icon = 'icons/mob/lavaland/96x96megafauna.dmi'
icon_state = "bluespace_horror"
speak_emote = list("emanates")
melee_attack_cooldown_min = 1 SECONDS
damage_coeff = list(BRUTE = 0.75, BURN = 1.1, TOX = 0, CLONE = 0, STAMINA = 0, OXY = 0)
melee_damage_lower = 35
melee_damage_upper = 45 // It's a megafauna. Don't get hit nerd.
attack_verb_simple = "strike"
attack_verb_continuous = "strikes"
attack_sound = 'sound/magic/ratvar_attack.ogg'
see_in_dark = 20 // I see you
pixel_x = -32
step_type = FOOTSTEP_MOB_HEAVY
is_ranged = TRUE
casing_type = /obj/item/ammo_casing/caseless/bluespace_shards
projectile_sound = 'sound/magic/wand_teleport.ogg'
ranged_burst_count = 2
ranged_burst_interval = 0.4
ranged_cooldown = 1.5 SECONDS
true_spawn = FALSE
ai_controller = /datum/ai_controller/basic_controller/bluespace_horror
innate_actions = list(
/datum/action/cooldown/mob_cooldown/bluespace_horror/summon_mobs = BB_HORROR_SUMMON_MOBS_ACTION,
/datum/action/cooldown/mob_cooldown/bluespace_horror/fireball_fan = BB_HORROR_FIREBALL_FAN_ACTION,
/datum/action/cooldown/mob_cooldown/bluespace_horror/lifesteal_bolt = BB_HORROR_LIFESTEAL_BOLT_ACTION,
/datum/action/cooldown/mob_cooldown/bluespace_horror/magic_missile = BB_HORROR_MAGIC_MISSILE_ACTION,
/datum/action/cooldown/mob_cooldown/bluespace_horror/charge = BB_HORROR_CHARGE_ACTION,
/datum/action/cooldown/mob_cooldown/bluespace_horror/blink = BB_HORROR_BLINK_ACTION)
/// Did we do the big attack?
var/final_burst = FALSE
/mob/living/basic/megafauna/bluespace_horror/Initialize(mapload)
. = ..()
AddElement(/datum/element/ai_retaliate)
/mob/living/basic/megafauna/bluespace_horror/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
return TRUE
/mob/living/basic/megafauna/bluespace_horror/melee_attack(atom/target, list/modifiers, ignore_cooldown)
if(!isliving(target))
return ..()
var/mob/living/L = target
var/datum/status_effect/stacking/unstable_bluespace_threads/G = L.has_status_effect(STATUS_EFFECT_BLUESPACE_THREADS)
if(!G)
L.apply_status_effect(STATUS_EFFECT_BLUESPACE_THREADS, 1, src)
return ..()
if(G.add_stacks(stacks_added = 1, attacker = src))
return ..()
/mob/living/basic/megafauna/bluespace_horror/adjustHealth(amount, updating_health)
if(mind) // For whatever reason, if a player is in control, we want them in charge of their blinks
return ..()
var/datum/action/cooldown/mob_cooldown/bluespace_horror/blink/blink_action = ai_controller.blackboard[BB_HORROR_BLINK_ACTION]
if(blink_action.IsAvailable())
ai_controller.queue_behavior(/datum/ai_behavior/horror_blink_dodge, BB_HORROR_BLINK_ACTION)
return
. = ..()
if(health < maxHealth / 3 && !final_burst)
ai_controller.queue_behavior(/datum/ai_behavior/omega_fireball_fan)
final_burst = TRUE
/// Proc used for various powers and spells
/mob/living/basic/megafauna/bluespace_horror/proc/shoot_projectile(atom/target_atom, obj/projectile/thing_to_shoot, set_angle)
var/turf/startloc = get_turf(src)
var/turf/endloc = get_turf(target_atom)
if(!startloc || !endloc || endloc == loc)
return
var/obj/projectile/P = new thing_to_shoot(startloc)
P.preparePixelProjectile(endloc, startloc)
P.firer = src
P.firer_source_atom = src
if(isnum(set_angle))
P.fire(set_angle)
else
P.fire()
/// WHen hitting a mob five times, does something random
/mob/living/basic/megafauna/bluespace_horror/proc/instability_correction()
switch(rand(1,5))
// ZAP
if(1)
var/list/shock_mobs = list()
for(var/mob/living/C in view(get_turf(src), 5))
if(isliving(C) && !faction_check_mob(C))
shock_mobs += C
if(length(shock_mobs))
for(var/i in 1 to 3)
var/mob/living/sucker = pick_n_take(shock_mobs)
sucker.electrocute_act(rand(5, 25), "instability correction")
playsound(get_turf(sucker), 'sound/effects/eleczap.ogg', 75, TRUE)
Beam(sucker, icon_state = "lightning[rand(1, 12)]", icon = 'icons/effects/effects.dmi', time = 5)
// Vwoop!
if(2)
var/list/desto_turfs = list()
for(var/turf/possible in view(get_turf(src), 3))
if(possible.is_blocked_turf(exclude_mobs = TRUE))
continue
if(isspaceturf(possible))
continue
desto_turfs += possible
for(var/mob/living/C in oview(get_turf(src), 3))
C.forceMove(pick(desto_turfs))
to_chat(C, "<span class='danger'>You are teleported a short distance!</span>")
do_sparks(3, TRUE, C)
// Yoink!
if(3)
for(var/mob/living/C in oview(get_turf(src), 5))
var/dir = get_dir(C.loc, loc)
var/turf/step = get_step(C, dir)
C.forceMove(step)
to_chat(C, "<span class='danger'>You feel a strong force pulling you towards [src]!</span>")
// Shazam!
if(4)
for(var/mob/living/C in oview(get_turf(src), 1))
shoot_projectile(C, /obj/projectile/magic/lifesteal_bolt)
playsound(src, 'sound/magic/invoke_general.ogg', 200, TRUE, 2)
// Kablooey!
if(5)
playsound(src, 'sound/effects/meteorimpact.ogg', 200, TRUE, 2, TRUE)
new /obj/effect/temp_visual/stomp(loc)
for(var/mob/living/L in range(3, src))
if(faction_check(faction, L.faction, FALSE))
continue
L.visible_message("<span class='danger'>[L] was thrown by [src]!</span>",
"<span class='userdanger'>You feel a strong force throwing you!</span>",
"<span class='danger'>You hear a thud.</span>")
var/atom/throw_target = get_edge_target_turf(L, get_dir(src, get_step_away(L, src)))
L.throw_at(throw_target, 4, 4)
var/limb_to_hit = L.get_organ(pick(BODY_ZONE_HEAD, BODY_ZONE_CHEST, BODY_ZONE_R_ARM, BODY_ZONE_L_ARM, BODY_ZONE_R_LEG, BODY_ZONE_L_LEG))
var/armor = L.run_armor_check(def_zone = limb_to_hit, armor_type = MELEE, armor_penetration_percentage = 50)
L.apply_damage(20, BRUTE, limb_to_hit, armor)
/obj/item/ammo_casing/caseless/bluespace_shards
name = "bluespace shards"
projectile_type = /obj/projectile/magic/bluespace_shards
fire_sound = 'sound/magic/wand_teleport.ogg'
pellets = 4
variance = 90
/obj/projectile/magic/bluespace_shards
name = "bluespace shard"
icon_state = "u_laser_alt"
damage = 20
damage_type = BRUTE
nodamage = FALSE
/datum/status_effect/stacking/unstable_bluespace_threads
id = "unstable_bluespace_threads"
tick_interval = 5 SECONDS
stack_threshold = 5
max_stacks = 5
reset_ticks_on_stack = TRUE
var/mob/living/basic/megafauna/bluespace_horror/latest_attacker
/datum/status_effect/stacking/unstable_bluespace_threads/on_creation(mob/living/new_owner, stacks_to_apply, mob/living/attacker)
. = ..()
if(.)
latest_attacker = attacker
/datum/status_effect/stacking/unstable_bluespace_threads/add_stacks(stacks_added, mob/living/attacker)
. = ..()
if(.)
latest_attacker = attacker
if(stacks != stack_threshold)
return TRUE
/datum/status_effect/stacking/unstable_bluespace_threads/stacks_consumed_effect()
addtimer(CALLBACK(latest_attacker, TYPE_PROC_REF(/mob/living/basic/megafauna/bluespace_horror, instability_correction)), 1 SECONDS)
/datum/status_effect/stacking/unstable_bluespace_threads/on_remove()
latest_attacker = null
@@ -0,0 +1,203 @@
/datum/action/cooldown/mob_cooldown/bluespace_horror/summon_mobs
name = "Summon Mobs"
button_icon = 'icons/mob/actions/actions_cult.dmi'
button_icon_state = "telerune"
desc = "Summon a series of servants from bluespace to assist you."
click_to_activate = FALSE
melee_cooldown_time = CLICK_CD_CLICK_ABILITY
cooldown_time = 90 SECONDS
shared_cooldown = NONE
/datum/action/cooldown/mob_cooldown/bluespace_horror/summon_mobs/Activate(atom/target)
var/mob/living/basic/megafauna/bluespace_horror/summoner = owner
if(!istype(summoner))
to_chat(owner, "<span class='warning'>I am unable to summon servants!</span>")
return
var/amount_to_spawn = rand(2, 5)
var/list/all_possible_dirs = GLOB.alldirs
for(var/i in 1 to amount_to_spawn)
var/mob/living/new_mob
var/spawn_loc = get_step(summoner, pick_n_take(all_possible_dirs))
new_mob = create_random_mob(spawn_loc, HOSTILE_SPAWN)
new /obj/effect/temp_visual/emp/pulse/cult(spawn_loc)
new_mob.faction = summoner.faction
StartCooldown()
/datum/action/cooldown/mob_cooldown/bluespace_horror/charge
name = "Violent Charge"
button_icon_state = "OH_YEAAAAH"
desc = "Fully materialize in the material plane and charge forward!"
melee_cooldown_time = CLICK_CD_CLICK_ABILITY
cooldown_time = 20 SECONDS
shared_cooldown = NONE
/datum/action/cooldown/mob_cooldown/bluespace_horror/charge/Activate(atom/target)
. = ..()
var/dir_to_target = get_dir(get_turf(owner), get_turf(target))
var/turf/T = get_step(get_turf(owner), dir_to_target)
for(var/i in 1 to 9)
new /obj/effect/temp_visual/dragon_swoop/bluespace_horror(T)
T = get_step(T, dir_to_target)
owner.visible_message("<span class='danger'>[owner] prepares to charge!</span>")
addtimer(CALLBACK(src, PROC_REF(charge_to), dir_to_target, 0), 4)
StartCooldown()
/datum/action/cooldown/mob_cooldown/bluespace_horror/charge/proc/charge_to(move_dir, times_ran, list/hit_targets = list())
var/mob/living/basic/horror = owner
if(times_ran >= 9)
return
var/turf/T = get_step(get_turf(owner), move_dir)
if(ismineralturf(T))
var/turf/simulated/mineral/M = T
M.gets_drilled()
if(iswallturf(T))
T.dismantle_wall(TRUE)
for(var/obj/structure/window/W in T.contents)
W.take_damage(horror.obj_damage, BRUTE)
for(var/obj/machinery/door/D in T.contents)
D.take_damage(horror.obj_damage, BRUTE)
if(T.x > world.maxx - 2 || T.x < 2) // Keep them from runtiming
return
if(T.y > world.maxy - 2 || T.y < 2)
return
owner.forceMove(T)
playsound(owner, 'sound/effects/bang.ogg', 200, 1)
var/throwtarget = get_edge_target_turf(owner, move_dir)
for(var/mob/living/L in T.contents - owner)
if(owner.faction_check_mob(L))
continue
owner.visible_message("<span class='danger'>[owner] tramples and kicks [L]!</span>")
to_chat(L, "<span class='userdanger'>[owner] tramples you and kicks you away!</span>")
L.throw_at(throwtarget, 10, 1, owner)
L.Weaken(1 SECONDS) // Pain Train has no breaks.
if(L in hit_targets)
L.adjustBruteLoss(15)
else
hit_targets += L
L.adjustBruteLoss(25)
addtimer(CALLBACK(src, PROC_REF(charge_to), move_dir, (times_ran + 1), hit_targets), 0.7)
// The visual effect which appears in front of the horror when it goes to charge.
/obj/effect/temp_visual/dragon_swoop/bluespace_horror
icon_state = "rune_large_distorted"
color = "#000099"
/obj/effect/temp_visual/dragon_swoop/bluespace_horror/Initialize(mapload)
. = ..()
transform *= 0.33
/datum/action/cooldown/mob_cooldown/bluespace_horror/magic_missile
name = "Bluespace Missile"
button_icon_state = "magicm"
desc = "Conjure bluespace projectiles to seek nearby victims!"
melee_cooldown_time = CLICK_CD_CLICK_ABILITY
cooldown_time = 40 SECONDS
shared_cooldown = NONE
/datum/action/cooldown/mob_cooldown/bluespace_horror/magic_missile/Activate(atom/target)
. = ..()
var/mob/living/basic/megafauna/bluespace_horror/horror = owner
if(!istype(horror))
return
for(var/T in RANGE_TURFS(12, horror.loc) - horror.loc)
if(prob(10))
horror.shoot_projectile(T, /obj/projectile/magic/magic_missile/lesser/horror)
playsound(horror, 'sound/magic/magic_missile.ogg', 40, TRUE)
sleep(1)
horror.shoot_projectile(target, /obj/projectile/magic/magic_missile/lesser/horror)
playsound(horror, 'sound/magic/magic_missile.ogg', 40, TRUE)
StartCooldown()
/obj/projectile/magic/magic_missile/lesser/horror
damage = 15
damage_type = BRUTE
nodamage = FALSE
/datum/action/cooldown/mob_cooldown/bluespace_horror/fireball_fan
name = "Fireball Fan"
button_icon_state = "fireball"
desc = "Concentrate bluespace energy into a volatile volley of flame!"
melee_cooldown_time = CLICK_CD_CLICK_ABILITY
cooldown_time = 20 SECONDS
shared_cooldown = NONE
/datum/action/cooldown/mob_cooldown/bluespace_horror/fireball_fan/Activate(atom/target)
. = ..()
var/mob/living/basic/megafauna/bluespace_horror/horror = owner
if(!istype(horror))
return
var/angle_to_target = get_angle(horror, target)
var/variance = -90
for(var/i in 1 to 5)
horror.shoot_projectile(target, /obj/projectile/magic/fireball, angle_to_target + variance)
variance += 45
playsound(horror, 'sound/magic/fireball.ogg', 200, TRUE, 2)
StartCooldown()
/datum/action/cooldown/mob_cooldown/bluespace_horror/lifesteal_bolt
name = "Lifesteal Bolt"
button_icon = 'icons/mob/actions/actions_cult.dmi'
button_icon_state = "revive"
desc = "Fire a fast-moving bolt that drains the vitality of those that live on the material plane."
melee_cooldown_time = CLICK_CD_CLICK_ABILITY
cooldown_time = 10 SECONDS
shared_cooldown = NONE
/datum/action/cooldown/mob_cooldown/bluespace_horror/lifesteal_bolt/Activate(atom/target)
. = ..()
var/mob/living/basic/megafauna/bluespace_horror/horror = owner
if(!istype(horror))
return
horror.shoot_projectile(target, /obj/projectile/magic/lifesteal_bolt)
playsound(horror, 'sound/magic/invoke_general.ogg', 200, TRUE, 2)
StartCooldown()
/obj/projectile/magic/lifesteal_bolt
name = "lifesteal bolt"
icon_state = "arcane_barrage"
damage = 25
damage_type = BRUTE
nodamage = FALSE
/obj/projectile/magic/lifesteal_bolt/on_hit(mob/living/carbon/target)
. = ..()
if(!.)
return .
if(isliving(target) && isliving(firer))
var/mob/living/LF = firer
target.apply_damage(5, CLONE)
LF.adjustBruteLoss(-25)
target.Beam(LF, icon_state = "drain_life", icon = 'icons/effects/effects.dmi', time = 2 SECONDS)
/datum/action/cooldown/mob_cooldown/bluespace_horror/blink
name = "Blink"
button_icon_state = "blink"
desc = "Teleport away from your current location."
click_to_activate = FALSE
melee_cooldown_time = CLICK_CD_CLICK_ABILITY
cooldown_time = 6 SECONDS
shared_cooldown = NONE
/datum/action/cooldown/mob_cooldown/bluespace_horror/blink/Activate(atom/target)
. = ..()
var/list/turfs = list()
for(var/turf/T in range(target, 7))
if(T in range(target, 2))
continue
if(isspaceturf(T))
continue
if(T.density)
continue
turfs += T
var/turf/picked = pick(turfs)
var/datum/effect_system/smoke_spread/smoke
smoke = new /datum/effect_system/smoke_spread/bad()
smoke.set_up(2, FALSE, owner.loc)
smoke.start()
owner.forceMove(picked)
playsound(owner, 'sound/magic/blink.ogg', 150, TRUE, 2)
StartCooldown()
@@ -0,0 +1,77 @@
/datum/ai_controller/basic_controller/bluespace_horror
movement_delay = 1 SECONDS
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_MINIMUM_STAT = UNCONSCIOUS,
BB_AGGRO_RANGE = 7,
)
ai_movement = /datum/ai_movement/jps
idle_behavior = /datum/idle_behavior/idle_random_walk
interesting_dist = AI_MEGAFAUNA_INTERESTING_DIST
max_target_distance = 15
planning_subtrees = list(
/datum/ai_planning_subtree/find_and_hunt_target/corpses/human,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/targeted_mob_ability/horror_offensive_spellcasting,
/datum/ai_planning_subtree/ranged_skirmish,
/datum/ai_planning_subtree/attack_obstacle_in_path/walls,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/find_and_hunt_target/prowl,
/datum/ai_planning_subtree/attack_obstacle_in_path/prowl/walls,
)
/datum/ai_planning_subtree/targeted_mob_ability/horror_offensive_spellcasting
ability_key = BB_HORROR_FIREBALL_FAN_ACTION
/// Minimum time between any spells
var/horror_global_spellcasting_delay = 2 SECONDS
/datum/ai_planning_subtree/targeted_mob_ability/horror_offensive_spellcasting/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
if(controller.blackboard[BB_HORROR_SPELL_COOLDOWN] >= world.time)
return
var/list/ability_keys = list(BB_HORROR_FIREBALL_FAN_ACTION, BB_HORROR_LIFESTEAL_BOLT_ACTION, BB_HORROR_MAGIC_MISSILE_ACTION, BB_HORROR_CHARGE_ACTION, BB_HORROR_SUMMON_MOBS_ACTION)
ability_key = pick_n_take(ability_keys)
var/datum/action/cooldown/mob_cooldown/selected_action = controller.blackboard[ability_key]
while(selected_action && !selected_action.IsAvailable())
if(!ability_keys.len) // All spells are on cooldown
return
ability_key = pick_n_take(ability_keys)
selected_action = controller.blackboard[ability_key]
controller.set_blackboard_key(BB_HORROR_SPELL_COOLDOWN, world.time + horror_global_spellcasting_delay)
return ..()
/// Dodge!
/datum/ai_behavior/horror_blink_dodge
action_cooldown = 6 SECONDS
required_distance = 0
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/horror_blink_dodge/perform(seconds_per_tick, datum/ai_controller/controller, action_key)
. = ..()
var/datum/action/cooldown/mob_cooldown/bluespace_horror/blink/blink_action = controller.blackboard[action_key]
var/result = blink_action.Trigger()
if(result)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
/// Bring them down!
/datum/ai_behavior/omega_fireball_fan
action_cooldown = 10 MINUTES
required_distance = 0
/datum/ai_behavior/omega_fireball_fan/perform(seconds_per_tick, datum/ai_controller/controller, ...)
. = ..()
var/mob/living/basic/megafauna/bluespace_horror/user = controller.pawn
user.say("I WILL NOT BE BANISHED SO EASILY!")
spawn(1 SECONDS)
var/turf/start_turf = get_step(user, pick(GLOB.alldirs))
var/counter = rand(1, 16)
playsound(get_turf(user), 'sound/magic/invoke_general.ogg', 200, TRUE)
for(var/i in 1 to 80)
counter++
if(counter > 16)
counter = 1
var/obj/projectile/proj = pick(/obj/projectile/magic/lifesteal_bolt, /obj/projectile/magic/fireball, /obj/projectile/magic/magic_missile/lesser, /obj/projectile/magic/bluespace_shards)
user.shoot_projectile(start_turf, proj, counter * 22.5)
playsound(get_turf(user), 'sound/magic/staff_chaos.ogg', 50, TRUE)
sleep(1)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
@@ -0,0 +1,231 @@
/mob/living/basic/megafauna/vox_armalis
name = "vox armalis"
desc = "A massive vox, clad in heavy carapace armor and wielding a massive spikethrower. Something this large and heavy should not be moving with such finesse."
health = 300
maxHealth = 300
gender = PLURAL
icon = 'icons/mob/lavaland/32x64fauna.dmi'
icon_state = "armalis"
icon_dead = "armalis_dead"
speak_emote = list("creels")
melee_attack_cooldown_min = 1 SECONDS
damage_coeff = list(BRUTE = 0.2, BURN = 0.2, TOX = 0.2, CLONE = 0, STAMINA = 0, OXY = 0)
melee_damage_lower = 25
melee_damage_upper = 30
attack_verb_simple = "punch"
attack_verb_continuous = "punches"
attack_sound = 'sound/weapons/resonator_blast.ogg'
response_help_continuous = "gestures at"
response_help_simple = "gesture at"
move_force = MOVE_FORCE_NORMAL
see_in_dark = 20 // I see you
step_type = FOOTSTEP_MOB_SHOE
is_ranged = TRUE
casing_type = /obj/item/ammo_casing/caseless/heavy_spike
projectile_sound = 'sound/weapons/bladeslice.ogg'
ranged_burst_count = 2
ranged_cooldown = 0.5 SECONDS // It's player controlled, it can have player fire rates.
initial_traits = list()
blood_color = "#2299FC"
butcher_time = 40 SECONDS
butcher_results = list(
/obj/item/food/fried_vox = 8,
/obj/item/organ/internal/cyberimp/brain/sensory_enhancer = 1,
/obj/item/salvage/loot/vox = 10,
/obj/item/organ/internal/brain/vox = 1,
)
true_spawn = FALSE
innate_actions = list(
/datum/action/cooldown/mob_cooldown/vox_armalis/swap_ammo,
/datum/action/cooldown/mob_cooldown/vox_armalis/activate_qani,
/datum/action/cooldown/mob_cooldown/vox_armalis/ignite_claws,
)
/// Are the claws on
var/plasma_claws = FALSE
/// Do we explode on death?
var/do_death_explosion = TRUE
/mob/living/basic/megafauna/vox_armalis/Initialize(mapload)
. = ..()
add_language("Galactic Common")
add_language("Vox-pidgin")
set_default_language(GLOB.all_languages["Vox-pidgin"])
sight |= SEE_MOBS
var/datum/language/lang = GLOB.all_languages["Vox-pidgin"]
name = lang.get_random_name(gender)
/mob/living/basic/megafauna/vox_armalis/IsAdvancedToolUser()
return TRUE
/mob/living/basic/megafauna/vox_armalis/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
if(isspaceturf(loc))
new /obj/effect/particle_effect/ion_trails(get_turf(src), dir)
return TRUE
/mob/living/basic/megafauna/vox_armalis/update_overlays()
. = ..()
overlays.Cut()
if(plasma_claws)
. += "armalis_plasma_claws"
/mob/living/basic/megafauna/vox_armalis/ex_act(severity)
switch(severity)
if(EXPLODE_DEVASTATE)
adjustBruteLoss(75)
if(EXPLODE_HEAVY)
adjustBruteLoss(25)
if(EXPLODE_LIGHT)
adjustBruteLoss(10)
/mob/living/basic/megafauna/vox_armalis/devour(mob/living/L)
return
/mob/living/basic/megafauna/vox_armalis/death(gibbed)
plasma_claws = FALSE
update_icon(UPDATE_OVERLAYS)
transform = transform.Turn(90)
transform = transform.Translate(0, -16)
if(do_death_explosion)
death_explosion()
return ..()
/mob/living/basic/megafauna/vox_armalis/gib() // Overwrites to make voxblood
if(!death(TRUE) && stat != DEAD)
return FALSE
// hide and freeze for the GC
notransform = TRUE
if(gib_nullifies_icon)
icon = null
invisibility = 101
playsound(src.loc, 'sound/goonstation/effects/gib.ogg', 50, 1)
for(var/i in 1 to 5)
new /obj/effect/gibspawner/vox(loc)
QDEL_IN(src, 0)
return TRUE
/mob/living/basic/megafauna/vox_armalis/proc/death_explosion()
visible_message("<span class='userdanger'>[src] starts beeping ominously!</span>")
for(var/i in 1 to 4)
playsound(loc, 'sound/items/timer.ogg', 30, 0)
sleep(1 SECONDS)
explosion(loc, 5, 11, 20, 26, flame_range = 26, cause = name)
qdel(src)
/mob/living/basic/megafauna/vox_armalis/melee_attack(atom/target, list/modifiers, ignore_cooldown)
if(a_intent == INTENT_HARM)
if(ismob(target) && !plasma_claws)
var/mob/dustlung = target
var/atom/throw_target = get_edge_target_turf(target, get_dir(src, get_step_away(target, src)))
dustlung.throw_at(throw_target, 15, 9) // Like getting hit with a max-spool powerfist
return ..()
if(istype(target, /obj/machinery/door/airlock))
var/obj/machinery/door/airlock/A = target
try_open_airlock(A)
return
if(istype(target, /obj/machinery/door/firedoor))
var/obj/machinery/door/firedoor/A = target
if(A.density)
A.open()
else
A.close()
return
if(iswallturf(target))
return // We're not on kill intent. Don't smash.
if(ismachinery(target)) // We can interface with machines!
var/obj/machinery/machine = target
machine.attack_hand(src)
return
/mob/living/basic/megafauna/vox_armalis/RangedAttack(atom/A, params)
if(a_intent != INTENT_HARM)
return // No shoot on friendly mode
. = ..()
/mob/living/basic/megafauna/vox_armalis/proc/try_open_airlock(obj/machinery/door/airlock/D)
if(D.operating)
return
if(D.welded)
to_chat(src, "<span class='warning'>The door is welded.</span>")
else if(D.locked)
to_chat(src, "<span class='warning'>The door is bolted.</span>")
else if(D.allowed(src))
if(D.density)
D.open(TRUE)
else
D.close(TRUE)
return TRUE
visible_message("<span class='danger'>[src] forces the door!</span>")
playsound(src.loc, "sparks", 100, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
if(D.density)
D.open(TRUE)
else
D.close(TRUE)
/obj/item/ammo_casing/caseless/heavy_spike
name = "heavy alloy spike"
desc = "A large broadhead spike made out of a weird silvery metal."
projectile_type = /obj/projectile/bullet/hspike
muzzle_flash_effect = null
fire_sound = 'sound/weapons/bladeslice.ogg'
/obj/item/ammo_casing/caseless/spike_flechettes
name = "spike flechettes"
desc = "A cluster of loosely-packed alloy spikes."
projectile_type = /obj/projectile/bullet/fspike
muzzle_flash_effect = null
fire_sound = 'sound/weapons/bladeslice.ogg'
pellets = 5
variance = 60
/obj/item/ammo_casing/caseless/spike_penetrator
name = "spike penetrator"
desc = "A dense alloy spike with a reinforced, sharpened tip."
projectile_type = /obj/projectile/bullet/pspike
muzzle_flash_effect = null
fire_sound = 'sound/weapons/bladeslice.ogg'
/obj/projectile/bullet/hspike
name = "heavy alloy spike"
desc = "It's about two feet of weird silvery metal with a wicked point."
damage = 35
knockdown = 3
armor_penetration_flat = 45
icon_state = "magspear"
/obj/projectile/bullet/hspike/on_hit(atom/target, blocked = 0)
if((blocked < 100) && ishuman(target))
var/mob/living/carbon/human/H = target
H.bleed(50)
H.Immobilize(1 SECONDS)
..()
/obj/projectile/bullet/fspike
name = "alloy spike flechette"
desc = "A sharpened dart of silvery metal."
damage = 15
knockdown = 1
armor_penetration_flat = 15
icon_state = "magspear"
/obj/projectile/bullet/fspike/on_hit(atom/target, blocked = 0)
if((blocked < 100) && ishuman(target))
var/mob/living/carbon/human/H = target
H.bleed(25)
..()
/obj/projectile/bullet/pspike
name = "alloy spike penetrator"
desc = "A 2-foot reinforced spike made of a silvery metal and sharpened to a dangerous point."
knockdown = 4
armor_penetration_flat = 100
icon_state = "magspear"
forcedodge = 8
/obj/projectile/bullet/pspike/on_hit(atom/target, blocked = 0)
if((blocked < 100) && ishuman(target))
var/mob/living/carbon/human/H = target
H.bleed(75)
..()
@@ -0,0 +1,132 @@
/datum/action/cooldown/mob_cooldown/vox_armalis/activate_qani
name = "Activate Qani-Laaca"
button_icon = 'icons/obj/surgery.dmi'
button_icon_state = "sandy"
desc = "Activates an advanced Qani-Laaca implant, giving you high speed and the ability to dodge bullets for 20 seconds."
click_to_activate = FALSE
melee_cooldown_time = CLICK_CD_CLICK_ABILITY
cooldown_time = 40 SECONDS
shared_cooldown = NONE
/datum/action/cooldown/mob_cooldown/vox_armalis/activate_qani/Activate(atom/target)
var/mob/living/basic/megafauna/vox_armalis/armalis = owner
if(!istype(armalis))
return
RegisterSignal(armalis, COMSIG_MOVABLE_MOVED, PROC_REF(on_movement))
RegisterSignal(armalis, COMSIG_ATOM_PREHIT, PROC_REF(dodge_bullets))
armalis.next_move_modifier -= 0.5
armalis.speed -= 1
addtimer(CALLBACK(src, PROC_REF(end_qani)), 20 SECONDS)
to_chat(armalis, "<span class='notice'>A flood of suppressants and performance enhancers flood your system, while your augments go into overdrive.</span>")
StartCooldown()
/// Leaves an afterimage behind the mob when they move
/datum/action/cooldown/mob_cooldown/vox_armalis/activate_qani/proc/on_movement(mob/living/L, atom/old_loc)
SIGNAL_HANDLER
if(HAS_TRAIT(L, TRAIT_IMMOBILIZED))
return NONE
new /obj/effect/temp_visual/decoy/mephedrone_afterimage(old_loc, L, 1.25 SECONDS)
/// Tries to dodge incoming bullets if we aren't disabled for any reasons
/datum/action/cooldown/mob_cooldown/vox_armalis/activate_qani/proc/dodge_bullets(mob/living/basic/megafauna/vox_armalis/armalis, obj/projectile/hitting_projectile)
SIGNAL_HANDLER
if(HAS_TRAIT(armalis, TRAIT_IMMOBILIZED))
return NONE
armalis.visible_message(
"<span class='danger'>[armalis] effortlessly dodges [hitting_projectile]!</span>",
"<span class='userdanger'>You effortlessly evade [hitting_projectile]!</span>",
)
playsound(armalis, pick('sound/weapons/bulletflyby.ogg', 'sound/weapons/bulletflyby2.ogg', 'sound/weapons/bulletflyby3.ogg'), 75, TRUE)
armalis.add_filter("mephedrone_overdose_blur", 2, gauss_blur_filter(5))
addtimer(CALLBACK(armalis, TYPE_PROC_REF(/atom, remove_filter), "mephedrone_overdose_blur"), 0.5 SECONDS)
return ATOM_PREHIT_FAILURE
/datum/action/cooldown/mob_cooldown/vox_armalis/activate_qani/proc/end_qani()
var/mob/living/basic/megafauna/vox_armalis/armalis = owner
if(!istype(armalis))
return
UnregisterSignal(armalis, COMSIG_MOVABLE_MOVED)
UnregisterSignal(armalis, COMSIG_ATOM_PREHIT)
armalis.next_move_modifier += 0.5
armalis.speed += 1
to_chat(armalis, "<span class='notice'>Your augments return to standard operation speeds as the suppressants wear off.</span>")
/datum/action/cooldown/mob_cooldown/vox_armalis/swap_ammo
name = "Switch Ammunition"
button_icon = 'icons/obj/guns/energy.dmi'
button_icon_state = "noisecannon"
desc = "Switches the ammunition that your heavy spikethrower fires between spike, flechette, and AP sabot."
click_to_activate = FALSE
melee_cooldown_time = CLICK_CD_CLICK_ABILITY
cooldown_time = 0 SECONDS
shared_cooldown = NONE
/// The type of ammunition
var/ammo_type = 1
/datum/action/cooldown/mob_cooldown/vox_armalis/swap_ammo/Activate(atom/target)
var/mob/living/basic/megafauna/vox_armalis/armalis = owner
if(!istype(armalis))
return
var/datum/component/ranged_attacks/comp = armalis.GetComponent(/datum/component/ranged_attacks)
if(!comp)
return
playsound(armalis, 'sound/weapons/kenetic_reload.ogg', 60, TRUE)
switch(ammo_type)
if(1)
ammo_type++
comp.casing_type = /obj/item/ammo_casing/caseless/spike_flechettes
comp.burst_shots = 1
comp.cooldown_time = 0.5 SECONDS
to_chat(armalis, "<span class='notice'>You switch your heavy spikethrower to flechette shot.</span>")
if(2)
ammo_type++
comp.casing_type = /obj/item/ammo_casing/caseless/spike_penetrator
comp.burst_shots = 1
comp.cooldown_time = 1 SECONDS
to_chat(armalis, "<span class='notice'>You switch your heavy spikethrower to penetrating shot.</span>")
if(3)
ammo_type = 1
comp.casing_type = /obj/item/ammo_casing/caseless/heavy_spike
comp.burst_shots = 2
comp.cooldown_time = 0.5 SECONDS
to_chat(armalis, "<span class='notice'>You switch your heavy spikethrower to heavy spike shot.</span>")
StartCooldown()
/datum/action/cooldown/mob_cooldown/vox_armalis/ignite_claws
name = "Toggle Plasma Talons"
button_icon = 'icons/obj/weapons/energy_melee.dmi'
button_icon_state = "pyro_claws"
desc = "Switches your plasma talons on or off, increasing your damage but preventing you from knocking targets back."
click_to_activate = FALSE
melee_cooldown_time = CLICK_CD_CLICK_ABILITY
cooldown_time = 0 SECONDS
shared_cooldown = NONE
/datum/action/cooldown/mob_cooldown/vox_armalis/ignite_claws/Activate(atom/target)
var/mob/living/basic/megafauna/vox_armalis/armalis = owner
if(!istype(armalis))
return
if(armalis.plasma_claws)
armalis.plasma_claws = FALSE
armalis.melee_damage_lower = initial(armalis.melee_damage_lower)
armalis.melee_damage_upper = initial(armalis.melee_damage_upper)
armalis.attack_sound = initial(armalis.attack_sound)
armalis.attack_verb_simple = initial(armalis.attack_verb_simple)
armalis.attack_verb_continuous = initial(armalis.attack_verb_continuous)
playsound(armalis, 'sound/weapons/saberoff.ogg', 80, TRUE)
to_chat(armalis, "<span class='notice'>You switch off your plasma talons.</span>")
else
armalis.plasma_claws = TRUE
armalis.melee_damage_lower += 10
armalis.melee_damage_upper += 20
armalis.attack_sound = 'sound/weapons/blade1.ogg'
armalis.attack_verb_simple = "slash"
armalis.attack_verb_continuous = "slashes"
playsound(armalis, 'sound/weapons/saberon.ogg', 80, TRUE)
to_chat(armalis, "<span class='warning'>You switch on your plasma talons.</span>")
armalis.update_icon(UPDATE_OVERLAYS)
StartCooldown()