Files
Aurora.3/code/modules/blob/blob.dm
T
hazelratandGitHub f56a3f6714 Blobs can now spawn up to three cores per event (#21377)
Blobs are too trivial, this should make them exciting. 

Requirements for this event to proc are two engineers, so in the
worst-case you still have two people with an emitter each who can each
destroy one blob and circle to the third after.

Could use scrutiny from someone more familiar with event code than
myself, I feel I've stepped on something here.

<img width="980" height="895" alt="image"
src="https://github.com/user-attachments/assets/38e15dfb-70a1-475e-b9b0-120765b507d5"
/>
2025-09-30 15:55:56 +00:00

440 lines
12 KiB
Plaintext

/obj/effect/blob
name = "pulsating mass"
desc = "A pulsating mass of interwoven tendrils."
icon = 'icons/mob/npc/blob.dmi'
icon_state = "blob"
light_range = 3
light_power = 4
light_color = BLOB_COLOR_PULS
density = TRUE
anchored = TRUE
mouse_opacity = MOUSE_OPACITY_OPAQUE
layer = BLOB_SHIELD_LAYER
var/maxHealth = 30
var/health
var/regen_rate = 5
/// Damage gets divided by these modifiers, based on damage type
var/brute_resist = 4.3
var/fire_resist = 0.8
/// Special resist for laser based weapons - Emitters or handheld energy weaponry. Damage is divided by this and THEN by fire_resist.
var/laser_resist = 2
var/expandType = /obj/effect/blob
/// % chance to grow a secondary blob core instead of whatever was supposed to grow. Secondary cores are considerably weaker, but still nasty.
var/secondary_core_growth_chance = 5
var/damage_min = 15
var/damage_max = 25
var/pruned = FALSE
var/product = /obj/item/blob_tendril
var/attack_time = 0
/// Time in deciseconds before next attack will occur
var/attack_cooldown = 60
/// The core this blob piece belongs to
var/obj/effect/blob/parent_core
/// Determines how to pass core inheritance
var/is_core = FALSE
/obj/effect/blob/Initialize()
. = ..()
health = maxHealth
update_icon()
START_PROCESSING(SSprocessing, src)
/obj/effect/blob/CanPass(atom/movable/mover, turf/target, height, air_group)
if(mover?.movement_type & PHASING)
return TRUE
if(air_group || height == 0)
return TRUE
return FALSE
/obj/effect/blob/ex_act(var/severity)
switch(severity)
if(1)
take_damage(rand(100, 120) / brute_resist)
if(2)
take_damage(rand(60, 100) / brute_resist)
if(3)
take_damage(rand(20, 60) / brute_resist)
/obj/effect/blob/update_icon()
if(health > maxHealth / 2)
icon_state = "blob"
else
icon_state = "blob_damaged"
/obj/effect/blob/process()
if(!parent_core || QDELETED(parent_core))
take_damage(maxHealth / 4) // four processes to die if main core is deddo
return
regen()
if(world.time < (attack_time + attack_cooldown))
return
attempt_attack()
/obj/effect/blob/proc/take_damage(var/damage)
health -= damage
if(health < 0)
playsound(get_turf(src), 'sound/effects/splat.ogg', 50, TRUE)
qdel(src)
else
update_icon()
/obj/effect/blob/proc/regen()
health = min(health + regen_rate, maxHealth)
update_icon()
/obj/effect/blob/proc/expand(var/turf/T)
if(istype(T, /turf/space) || (isopenturf(T)) || (istype(T, /turf/simulated/mineral) && T.density))
return
if(istype(T, /turf/simulated/wall))
var/turf/simulated/wall/SW = T
SW.take_damage(80)
return
var/obj/structure/girder/G = locate() in T
if(G)
G.take_damage(rand(40, 80))
return
var/obj/structure/window/W = locate() in T
if(W)
W.shatter()
return
var/obj/structure/grille/GR = locate() in T
if(GR)
qdel(GR)
return
var/obj/structure/tank_wall/TW = locate() in T
if(TW)
TW.take_damage(rand(5,20))
return
for(var/obj/machinery/door/D in T) // There can be several - and some of them can be open, locate() is not suitable
if(D.density)
attack_door(D)
if(D.health <= 0)
if(!D.open(TRUE))
D.visible_message(SPAN_WARNING("\The [src] bashes through \the [D], demolishing it!"))
qdel(D)
return
var/obj/structure/foamedmetal/F = locate() in T
if(F)
F.visible_message(SPAN_WARNING("\The [src] lashes into \the [F], tearing it apart!"))
qdel(F)
return
var/obj/structure/reagent_dispensers/RT = locate() in T
if(RT)
RT.visible_message(SPAN_WARNING("\The [src] pierces into \the [RT], blowing it apart!"))
RT.ex_act(2)
return
var/obj/structure/inflatable/I = locate() in T
if(I)
I.visible_message(SPAN_WARNING("\The [src] rips into \the [F], tearing a hole into it!"))
I.deflate(TRUE)
return
var/obj/vehicle/V = locate() in T
if(V)
V.ex_act(2)
return
var/obj/machinery/camera/CA = locate() in T
if(CA && !(CA.stat & BROKEN))
CA.take_damage(30)
return
// Above things, we destroy completely and thus can use locate. Mobs are different.
for(var/mob/living/L in T)
if(L.stat == DEAD)
continue
attack_living(L)
var/inherited_core = parent_core
if(is_core)
inherited_core = src
if(!(locate(/obj/effect/blob/core) in range(2, T)) && prob(secondary_core_growth_chance))
var/obj/effect/blob/core/secondary/S = new /obj/effect/blob/core/secondary(T)
S.parent_core = inherited_core
else
var/obj/effect/blob/B = new expandType(T, min(health, 30))
B.parent_core = inherited_core
/obj/effect/blob/proc/pulse(var/forceLeft, var/list/dirs, var/bad_dir)
sleep(4)
var/pushDir = pick(dirs - bad_dir)
var/turf/T = get_step(src, pushDir)
var/obj/effect/blob/B = locate() in T
if(!B)
if(prob(health))
var/retry = expand(T)
if(retry)
pulse(forceLeft - 1, dirs, pushDir)
else if(forceLeft)
B.pulse(forceLeft - 1, dirs - get_dir(B, src))
/obj/effect/blob/proc/attack_msg(atom/source)
source.visible_message(SPAN_WARNING("A tendril flies out from \the [src] and smashes into \the [source]!"), SPAN_DANGER("A tendril flies out from \the [src] and smashes into you!"))
playsound(get_turf(src), 'sound/effects/attackblob.ogg', 50, TRUE)
/obj/effect/blob/proc/attack_door(var/obj/machinery/door/D)
if(!D)
return
attack_msg(D)
D.take_damage(rand(damage_min, damage_max))
/obj/effect/blob/proc/attack_living(var/mob/living/L)
if(!L)
return
var/blob_damage = pick(DAMAGE_BRUTE, DAMAGE_BURN)
attack_msg(L)
L.apply_damage(rand(damage_min, damage_max), blob_damage, used_weapon = "blob tendril")
/obj/effect/blob/proc/attempt_attack()
var/mob/living/victim = locate() in view(1, src)
if(victim)
if(victim.stat == DEAD)
return
attack_living(victim)
attack_time = world.time
/obj/effect/blob/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit)
. = ..()
if(. != BULLET_ACT_HIT)
return .
if(!hitting_projectile)
return
switch(hitting_projectile.damage_type)
if(DAMAGE_BRUTE)
take_damage(hitting_projectile.damage / brute_resist)
if(DAMAGE_BURN)
take_damage((hitting_projectile.damage / laser_resist) / fire_resist)
/obj/effect/blob/attackby(obj/item/attacking_item, mob/user)
user.setClickCooldown(DEFAULT_ATTACK_COOLDOWN)
user.do_attack_animation(src)
playsound(get_turf(src), 'sound/effects/attackblob.ogg', 50, TRUE)
if(attacking_item.iswirecutter())
if(!pruned)
to_chat(user, SPAN_NOTICE("You collect a sample from \the [src]."))
var/obj/P = new product(get_turf(user))
user.put_in_hands(P)
pruned = TRUE
return
else
to_chat(user, SPAN_WARNING("\The [src] has already been pruned."))
return
var/damage = 0
switch(attacking_item.damtype)
if(DAMAGE_BURN)
damage = (attacking_item.force / fire_resist)
if(attacking_item.iswelder())
playsound(get_turf(src), 'sound/items/Welder.ogg', 100, TRUE)
if(DAMAGE_BRUTE)
damage = (attacking_item.force / brute_resist)
take_damage(damage)
/obj/effect/blob/fire_act(exposed_temperature, exposed_volume)
. = ..()
take_damage(rand(5, 20) / fire_resist)
#define CORE_SHIELD_HIGH "high"
#define CORE_SHIELD_LOW "low"
/obj/effect/blob/core
name = "master nucleus"
desc = "A massive, fragile nucleus guarded by a shield of thick tendrils."
icon_state = "blob_core"
maxHealth = 450
damage_min = 25
damage_max = 35
expandType = /obj/effect/blob/shield
product = /obj/item/blob_core
is_core = TRUE
light_color = BLOB_COLOR_CORE
layer = BLOB_CORE_LAYER
var/blob_may_process = TRUE
var/reported_low_damage = FALSE
var/pulse_power = 50 // How far the core can potentially grow in size
var/times_to_pulse = 4
/obj/effect/blob/core/proc/get_health_percent()
return ((health / maxHealth) * 100)
/obj/effect/blob/core/proc/process_core_health()
var/health_percent = get_health_percent()
if(health_percent > 75)
if(reported_low_damage)
report_shield_status(CORE_SHIELD_HIGH)
else if(health_percent < 33)
if(!reported_low_damage)
report_shield_status(CORE_SHIELD_LOW)
/obj/effect/blob/core/proc/report_shield_status(var/status)
if(status == CORE_SHIELD_LOW)
visible_message(SPAN_DANGER("\The [src]'s internal tendril shield fails, leaving the nucleus vulnerable!"), 3)
reported_low_damage = TRUE
if(status == CORE_SHIELD_HIGH)
visible_message(SPAN_DANGER("\The [src]'s internal tendril shield seems to have fully reformed."), 3)
reported_low_damage = FALSE
// Rough icon state changes that reflect the core's health
/obj/effect/blob/core/update_icon()
switch(get_health_percent())
if(66 to INFINITY)
icon_state = "blob_core"
if(33 to 66)
icon_state = "blob_node"
if(-INFINITY to 33)
icon_state = "blob_factory"
/obj/effect/blob/core/process()
set waitfor = 0
if(!blob_may_process)
return
blob_may_process = FALSE
process_core_health()
regen()
for(var/i = 1 to times_to_pulse)
pulse(pulse_power, GLOB.cardinals.Copy())
blob_may_process = TRUE
if(world.time < (attack_time + attack_cooldown))
return
attempt_attack()
// Blob has a very small probability of growing these when spreading. These will spread the blob further.
/obj/effect/blob/core/secondary
name = "auxiliary nucleus"
desc = "An interwoven mass of tendrils. A glowing nucleus pulses at its center."
icon_state = "blob_node"
maxHealth = 125
regen_rate = 1
damage_min = 15
damage_max = 20
layer = BLOB_NODE_LAYER
product = /obj/item/blob_core/aux
pulse_power = 20 // Not as strong as big daddy core
times_to_pulse = 4
/obj/effect/blob/core/secondary/process()
if(!parent_core || QDELETED(parent_core))
take_damage(maxHealth / 4) // four processes to die if main core is deddo
return
..()
/obj/effect/blob/core/secondary/process_core_health()
return
/obj/effect/blob/core/secondary/update_icon()
icon_state = (health / maxHealth >= 0.5) ? "blob_node" : "blob_factory"
/obj/effect/blob/shield
name = "shielding mass"
desc = "A pulsating mass of interwoven tendrils. These seem particularly robust, but not quite as active."
icon_state = "blob_idle"
maxHealth = 120
damage_min = 15
damage_max = 25
attack_cooldown = 45
regen_rate = 4
opacity = TRUE
expandType = /obj/effect/blob/ravaging
light_color = BLOB_COLOR_SHIELD
/obj/effect/blob/shield/Initialize()
. = ..()
update_nearby_tiles()
/obj/effect/blob/shield/Destroy()
density = FALSE
update_nearby_tiles()
return ..()
/obj/effect/blob/shield/update_icon()
if(health > maxHealth * 2 / 3)
icon_state = "blob_idle"
else if(health > maxHealth / 3)
icon_state = "blob"
else
icon_state = "blob_damaged"
/obj/effect/blob/shield/CanPass(var/atom/movable/mover, var/turf/target, var/height = 0, var/air_group = 0)
if(mover?.movement_type & PHASING)
return TRUE
return !density
/obj/effect/blob/ravaging
name = "ravaging mass"
desc = "A mass of interwoven tendrils. They thrash around haphazardly at anything in reach."
maxHealth = 20
damage_min = 20
damage_max = 25
attack_cooldown = 30
light_color = BLOB_COLOR_RAV
color = "#ffd400" //Temporary, for until they get a new sprite.
#define TENDRIL_SOLID "solid"
#define TENDRIL_FIRE "fire"
//produce
/obj/item/blob_tendril
name = "asteroclast tendril"
desc = "A tendril removed from an asteroclast. It's entirely lifeless."
icon = 'icons/mob/npc/blob.dmi'
icon_state = "tendril"
item_state = "blob_tendril"
w_class = WEIGHT_CLASS_BULKY
reach = 2 // long range tentacle whips - geeves
attack_verb = list("smacked", "smashed", "whipped")
var/types_of_tendril = list(TENDRIL_SOLID, TENDRIL_FIRE)
/obj/item/blob_tendril/Initialize()
. = ..()
var/tendril_type = pick(types_of_tendril)
switch(tendril_type)
if(TENDRIL_SOLID)
desc = "An incredibly dense, yet flexible, tendril, removed from an asteroclast."
force = 15
color = COLOR_BRONZE
origin_tech = list(TECH_MATERIAL = 2, TECH_BIO = 2)
if(TENDRIL_FIRE)
desc = "A tendril removed from an asteroclast. It's hot to the touch."
damtype = DAMAGE_BURN
force = 22
color = COLOR_AMBER
origin_tech = list(TECH_POWER = 2, TECH_BIO = 2)
/obj/item/blob_tendril/afterattack(obj/O, mob/user)
if(prob(50))
force--
if(force <= 0)
visible_message(SPAN_NOTICE("\The [src] crumbles apart!"))
user.drop_from_inventory(src)
new /obj/effect/decal/cleanable/ash(get_turf(src))
qdel(src)
/obj/item/blob_core
name = "asteroclast nucleus sample"
desc = "A sample taken from an asteroclast's nucleus. It pulses with energy."
icon_state = "core_sample"
item_state = "blob_core"
w_class = WEIGHT_CLASS_NORMAL
origin_tech = list(TECH_MATERIAL = 4, TECH_BLUESPACE = 5, TECH_BIO = 7)
/obj/item/blob_core/aux
name = "asteroclast auxiliary nucleus sample"
desc = "A sample taken from an asteroclast's auxiliary nucleus."
icon_state = "core_sample_2"
origin_tech = list(TECH_MATERIAL = 2, TECH_BLUESPACE = 3, TECH_BIO = 4)
#undef CORE_SHIELD_HIGH
#undef CORE_SHIELD_LOW
#undef TENDRIL_SOLID
#undef TENDRIL_FIRE