diff --git a/code/__defines/items_clothing.dm b/code/__defines/items_clothing.dm index 7c80895a81..f857a8f232 100644 --- a/code/__defines/items_clothing.dm +++ b/code/__defines/items_clothing.dm @@ -57,6 +57,7 @@ #define PASSTABLE 0x1 #define PASSGLASS 0x2 #define PASSGRILLE 0x4 +#define PASSBLOB 0x8 // Bitmasks for the flags_inv variable. These determine when a piece of clothing hides another, i.e. a helmet hiding glasses. // WARNING: The following flags apply only to the external suit! diff --git a/code/_helpers/lists.dm b/code/_helpers/lists.dm index 8b9040820f..c4933d5260 100644 --- a/code/_helpers/lists.dm +++ b/code/_helpers/lists.dm @@ -164,6 +164,14 @@ proc/listclearnulls(list/list) L.Swap(i, rand(i,L.len)) return L +//same, but returns nothing and acts on list in place +/proc/shuffle_inplace(list/L) + if(!L) + return + + for(var/i=1, i 8); including areas drastically decreases performance +/proc/urange(dist=0, atom/center=usr, orange=0, areas=0) + if(!dist) + if(!orange) + return list(center) + else + return list() + + var/list/turfs = RANGE_TURFS(dist, center) + if(orange) + turfs -= get_turf(center) + . = list() + for(var/V in turfs) + var/turf/T = V + . += T + . += T.contents + if(areas) + . |= T.loc diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm index b41dd9c57b..68f113e30c 100644 --- a/code/_onclick/item_attack.dm +++ b/code/_onclick/item_attack.dm @@ -23,8 +23,13 @@ avoid code duplication. This includes items that may sometimes act as a standard /obj/item/proc/attack_self(mob/user) return +// Called at the start of resolve_attackby(), before the actual attack. +/obj/item/proc/pre_attack(atom/a, mob/user) + return + //I would prefer to rename this to attack(), but that would involve touching hundreds of files. /obj/item/proc/resolve_attackby(atom/A, mob/user) + pre_attack(A, user) add_fingerprint(user) return A.attackby(src, user) diff --git a/code/datums/ghost_query.dm b/code/datums/ghost_query.dm index c06013105a..dda2565bf1 100644 --- a/code/datums/ghost_query.dm +++ b/code/datums/ghost_query.dm @@ -92,6 +92,13 @@ question = "An Alien has just been created on the facility. Would you like to play as them?" be_special_flag = BE_ALIEN +/datum/ghost_query/blob + role_name = "Blob" + question = "A rapidly expanding Blob has just appeared on the facility. Would you like to play as it?" + be_special_flag = BE_ALIEN + cutoff_number = 1 + wait_time = 10 SECONDS + /datum/ghost_query/syndicate_drone role_name = "Mercenary Drone" question = "A team of dubious mercenaries have purchased a powerful drone, and they are attempting to activate it. Would you like to play as the drone?" diff --git a/code/datums/mutable_appearance.dm b/code/datums/mutable_appearance.dm new file mode 100644 index 0000000000..1cb3a97d9f --- /dev/null +++ b/code/datums/mutable_appearance.dm @@ -0,0 +1,18 @@ +// Mutable appearances are an inbuilt byond datastructure. Read the documentation on them by hitting F1 in DM. +// Basically use them instead of images for overlays/underlays and when changing an object's appearance if you're doing so with any regularity. +// Unless you need the overlay/underlay to have a different direction than the base object. Then you have to use an image due to a bug. + +// Mutable appearances are children of images, just so you know. + +/mutable_appearance/New() + ..() + plane = FLOAT_PLANE // No clue why this is 0 by default yet images are on FLOAT_PLANE + // And yes this does have to be in the constructor, BYOND ignores it if you set it as a normal var + +// Helper similar to image() +/proc/mutable_appearance(icon, icon_state = "", layer = FLOAT_LAYER) + var/mutable_appearance/MA = new() + MA.icon = icon + MA.icon_state = icon_state + MA.layer = layer + return MA \ No newline at end of file diff --git a/code/datums/wires/apc.dm b/code/datums/wires/apc.dm index bdddf048c5..5bb5831d3e 100644 --- a/code/datums/wires/apc.dm +++ b/code/datums/wires/apc.dm @@ -55,12 +55,14 @@ if(APC_WIRE_MAIN_POWER1, APC_WIRE_MAIN_POWER2) if(!mended) - A.shock(usr, 50) + if(istype(usr, /mob/living)) + A.shock(usr, 50) A.shorted = 1 else if(!IsIndexCut(APC_WIRE_MAIN_POWER1) && !IsIndexCut(APC_WIRE_MAIN_POWER2)) A.shorted = 0 - A.shock(usr, 50) + if(istype(usr, /mob/living)) + A.shock(usr, 50) if(APC_WIRE_AI_CONTROL) diff --git a/code/game/atoms.dm b/code/game/atoms.dm index a092ed7b65..12defe06ba 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -84,6 +84,10 @@ P.on_hit(src, 0, def_zone) . = 0 +// Called when a blob expands onto the tile the atom occupies. +/atom/proc/blob_act() + return + /atom/proc/in_contents_of(container)//can take class or object instance as argument if(ispath(container)) if(istype(src.loc, container)) diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index b7210f22a3..9ac0f87198 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -15,7 +15,8 @@ var/moved_recently = 0 var/mob/pulledby = null var/item_state = null // Used to specify the item state for the on-mob overlays. - + var/old_x = 0 + var/old_y = 0 var/auto_init = 1 /atom/movable/New() diff --git a/code/game/machinery/atmoalter/portable_atmospherics.dm b/code/game/machinery/atmoalter/portable_atmospherics.dm index cb0a67cc04..92004f9afe 100644 --- a/code/game/machinery/atmoalter/portable_atmospherics.dm +++ b/code/game/machinery/atmoalter/portable_atmospherics.dm @@ -40,6 +40,9 @@ else update_icon() +/obj/machinery/portable_atmospherics/blob_act() + qdel(src) + /obj/machinery/portable_atmospherics/proc/StandardAirMix() return list( "oxygen" = O2STANDARD * MolesForPressure(), diff --git a/code/game/machinery/camera/camera.dm b/code/game/machinery/camera/camera.dm index 05ef232595..c7ce4ff2d0 100644 --- a/code/game/machinery/camera/camera.dm +++ b/code/game/machinery/camera/camera.dm @@ -106,6 +106,11 @@ ..() //and give it the regular chance of being deleted outright +/obj/machinery/camera/blob_act() + if((stat & BROKEN) || invuln) + return + destroy() + /obj/machinery/camera/hitby(AM as mob|obj) ..() if (istype(AM, /obj)) diff --git a/code/game/machinery/computer/computer.dm b/code/game/machinery/computer/computer.dm index 320e829bea..7155886747 100644 --- a/code/game/machinery/computer/computer.dm +++ b/code/game/machinery/computer/computer.dm @@ -59,6 +59,9 @@ set_broken() ..() +/obj/machinery/computer/blob_act() + ex_act(2) + /obj/machinery/computer/update_icon() overlays.Cut() if(stat & NOPOWER) diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm index 4c3823f11e..f9f6c667c7 100644 --- a/code/game/machinery/doors/door.dm +++ b/code/game/machinery/doors/door.dm @@ -355,6 +355,13 @@ take_damage(150) return +/obj/machinery/door/blob_act() + if(density) // If it's closed. + if(stat & BROKEN) + spawn(0) + open(1) + else + take_damage(100) /obj/machinery/door/update_icon() if(density) diff --git a/code/game/machinery/doors/firedoor.dm b/code/game/machinery/doors/firedoor.dm index ede8c53de2..b22dc27dba 100644 --- a/code/game/machinery/doors/firedoor.dm +++ b/code/game/machinery/doors/firedoor.dm @@ -391,8 +391,9 @@ else use_power(360) else - log_admin("[usr]([usr.ckey]) has forced open an emergency shutter.") - message_admins("[usr]([usr.ckey]) has forced open an emergency shutter.") + if(usr && usr.ckey) + log_admin("[usr]([usr.ckey]) has forced open an emergency shutter.") + message_admins("[usr]([usr.ckey]) has forced open an emergency shutter.") latetoggle() return ..() diff --git a/code/game/objects/effects/chem/water.dm b/code/game/objects/effects/chem/water.dm index 9b15464544..ee1c75980c 100644 --- a/code/game/objects/effects/chem/water.dm +++ b/code/game/objects/effects/chem/water.dm @@ -3,7 +3,7 @@ icon = 'icons/effects/effects.dmi' icon_state = "extinguish" mouse_opacity = 0 - pass_flags = PASSTABLE | PASSGRILLE + pass_flags = PASSTABLE | PASSGRILLE | PASSBLOB /obj/effect/effect/water/New(loc) ..() @@ -27,7 +27,7 @@ var/mob/M for(var/atom/A in T) if(!ismob(A) && A.simulated) // Mobs are handled differently - reagents.touch(A) + reagents.touch(A, reagents.total_volume) else if(ismob(A) && !M) M = A if(M) diff --git a/code/game/objects/effects/explosion_particles.dm b/code/game/objects/effects/explosion_particles.dm index e0750ba1c3..63b2a09c24 100644 --- a/code/game/objects/effects/explosion_particles.dm +++ b/code/game/objects/effects/explosion_particles.dm @@ -67,4 +67,10 @@ spawn(5) var/datum/effect/effect/system/smoke_spread/S = new/datum/effect/effect/system/smoke_spread() S.set_up(5,0,location,null) - S.start() \ No newline at end of file + S.start() + +/datum/effect/system/explosion/smokeless/start() + new/obj/effect/explosion(location) + var/datum/effect/system/expl_particles/P = new/datum/effect/system/expl_particles() + P.set_up(10,location) + P.start() \ No newline at end of file diff --git a/code/game/objects/items/weapons/grenades/flashbang.dm b/code/game/objects/items/weapons/grenades/flashbang.dm index 940029e735..04a7b3e100 100644 --- a/code/game/objects/items/weapons/grenades/flashbang.dm +++ b/code/game/objects/items/weapons/grenades/flashbang.dm @@ -16,10 +16,11 @@ for(var/mob/living/carbon/M in hear(7, get_turf(src))) bang(get_turf(src), M) - for(var/obj/effect/blob/B in hear(8,get_turf(src))) //Blob damage here + for(var/obj/structure/blob/B in hear(8,get_turf(src))) //Blob damage here var/damage = round(30/(get_dist(B,get_turf(src))+1)) - B.health -= damage - B.update_icon() + if(B.overmind) + damage *= B.overmind.blob_type.burn_multiplier + B.adjust_integrity(-damage) new/obj/effect/effect/sparks(src.loc) new/obj/effect/effect/smoke/illumination(src.loc, 5, range=30, power=30, color="#FFFFFF") diff --git a/code/game/objects/items/weapons/material/twohanded.dm b/code/game/objects/items/weapons/material/twohanded.dm index 8f329c6dec..3863e5a517 100644 --- a/code/game/objects/items/weapons/material/twohanded.dm +++ b/code/game/objects/items/weapons/material/twohanded.dm @@ -83,6 +83,7 @@ base_icon = "fireaxe" name = "fire axe" desc = "Truly, the weapon of a madman. Who would think to fight fire with an axe?" + description_info = "This weapon can cleave, striking nearby lesser, hostile enemies close to the primary target. It must be held in both hands to do this." unwielded_force_divisor = 0.25 force_divisor = 0.7 // 10/42 with hardness 60 (steel) and 0.25 unwielded divisor dulled_divisor = 0.75 //Still metal on a stick @@ -123,6 +124,12 @@ var/obj/effect/plant/P = A P.die_off() +// This cannot go into afterattack since some mobs delete themselves upon dying. +/obj/item/weapon/material/twohanded/fireaxe/pre_attack(var/mob/living/target, var/mob/living/user) + if(istype(target)) + cleave(user, target) + ..() + /obj/item/weapon/material/twohanded/fireaxe/scythe icon_state = "scythe0" base_icon = "scythe" diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm index a5d5652408..732540571a 100644 --- a/code/game/objects/structures/crates_lockers/closets.dm +++ b/code/game/objects/structures/crates_lockers/closets.dm @@ -185,6 +185,9 @@ A.forceMove(src.loc) qdel(src) +/obj/structure/closet/blob_act() + damage(100) + /obj/structure/closet/proc/damage(var/damage) health -= damage if(health <= 0) diff --git a/code/game/objects/structures/girders.dm b/code/game/objects/structures/girders.dm index 37aeef7fd9..29b7d89ab7 100644 --- a/code/game/objects/structures/girders.dm +++ b/code/game/objects/structures/girders.dm @@ -43,6 +43,9 @@ return +/obj/structure/girder/blob_act() + dismantle() + /obj/structure/girder/proc/reset_girder() anchored = 1 cover = initial(cover) diff --git a/code/game/objects/structures/inflatable.dm b/code/game/objects/structures/inflatable.dm index 065b637e10..32b1dec788 100644 --- a/code/game/objects/structures/inflatable.dm +++ b/code/game/objects/structures/inflatable.dm @@ -66,6 +66,9 @@ deflate(1) return +/obj/structure/inflatable/blob_act() + deflate(1) + /obj/structure/inflatable/attack_hand(mob/user as mob) add_fingerprint(user) return diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm index 028e0af3dc..4dcd17babc 100644 --- a/code/game/objects/structures/window.dm +++ b/code/game/objects/structures/window.dm @@ -128,6 +128,9 @@ shatter(0) return +/obj/structure/window/blob_act() + take_damage(50) + //TODO: Make full windows a separate type of window. //Once a full window, it will always be a full window, so there's no point //having the same type for both. diff --git a/code/game/objects/weapons.dm b/code/game/objects/weapons.dm index 92d1cc3661..34fcc3d0e5 100644 --- a/code/game/objects/weapons.dm +++ b/code/game/objects/weapons.dm @@ -2,6 +2,7 @@ name = "weapon" icon = 'icons/obj/weapons.dmi' hitsound = "swing_hit" + var/cleaving = FALSE // Used to avoid infinite cleaving. /obj/item/weapon/Bump(mob/M as mob) spawn(0) @@ -12,4 +13,28 @@ item_icons = list( slot_l_hand_str = 'icons/mob/items/lefthand_melee.dmi', slot_r_hand_str = 'icons/mob/items/righthand_melee.dmi', - ) \ No newline at end of file + ) + +// Attacks mobs (atm only simple ones due to friendly fire issues) that are adjacent to the target and user. +/obj/item/weapon/proc/cleave(var/mob/living/user, var/mob/living/target) + if(cleaving) + return // We're busy. + cleaving = TRUE + var/hit_mobs = 0 + for(var/mob/living/simple_animal/SA in orange(get_turf(target), 1)) + if(SA.stat == DEAD) // Don't beat a dead horse. + continue + if(SA == user) // Don't hit ourselves. Simple mobs shouldn't be able to do this but that might change later to be able to hit all mob/living-s. + continue + if(SA == target) // We (presumably) already hit the target before cleave() was called. orange() should prevent this but just to be safe... + continue + if(user.faction == SA.faction) // Avoid friendly fire. + continue + if(!SA.Adjacent(user) || !SA.Adjacent(target)) // Cleaving only hits mobs near the target mob and user. + continue + if(resolve_attackby(SA, user)) // Hit them with the weapon. This won't cause recursive cleaving due to the cleaving variable being set to true. + hit_mobs++ + + if(hit_mobs) + to_chat(user, "You used \the [src] to attack [hit_mobs] other thing\s!") + cleaving = FALSE // We're done now. \ No newline at end of file diff --git a/code/modules/blob/blob.dm b/code/modules/blob/blob.dm index 2352123fb2..32303eed8d 100644 --- a/code/modules/blob/blob.dm +++ b/code/modules/blob/blob.dm @@ -112,6 +112,8 @@ /obj/effect/blob/proc/pulse(var/forceLeft, var/list/dirs) regen() + animate(src, color = "#FF0000", time=1) + animate(color = "#FFFFFF", time=4, easing=ELASTIC_EASING) sleep(5) var/pushDir = pick(dirs) var/turf/T = get_step(src, pushDir) diff --git a/code/modules/blob2/_defines.dm b/code/modules/blob2/_defines.dm new file mode 100644 index 0000000000..1ad0b73893 --- /dev/null +++ b/code/modules/blob2/_defines.dm @@ -0,0 +1,10 @@ +#define BLOB_CORE_PULSE_RANGE 6 +#define BLOB_NODE_PULSE_RANGE 4 + +#define BLOB_CORE_EXPAND_RANGE 8 +#define BLOB_NODE_EXPAND_RANGE 6 + +#define BLOB_DIFFICULTY_EASY 0 +#define BLOB_DIFFICULTY_MEDIUM 1 +#define BLOB_DIFFICULTY_HARD 2 +#define BLOB_DIFFICULTY_SUPERHARD 3 \ No newline at end of file diff --git a/code/modules/blob2/announcement.dm b/code/modules/blob2/announcement.dm new file mode 100644 index 0000000000..6eceeec569 --- /dev/null +++ b/code/modules/blob2/announcement.dm @@ -0,0 +1,18 @@ +/proc/level_seven_blob_announcement(var/obj/structure/blob/core/B) + if(!B || !B.overmind) + return + var/datum/blob_type/blob = B.overmind.blob_type // Shortcut so we don't need to delve into three variables every time. + var/list/lines = list() + + lines += "Confirmed outbreak of level [7 + blob.difficulty] biohazard aboard [station_name()]. All personnel must contain the outbreak." + + if(blob.difficulty >= BLOB_DIFFICULTY_MEDIUM) // Tell them what kind of blob it is if it's tough. + lines += "The biohazard has been identified as a '[blob.name]'." + + if(blob.difficulty >= BLOB_DIFFICULTY_HARD) // If it's really hard then tell them where it is so the response occurs faster. + lines += "It is suspected to have originated from \the [get_area(B)]." + + if(blob.difficulty >= BLOB_DIFFICULTY_SUPERHARD) + lines += "Extreme caution is advised." + + command_announcement.Announce(lines.Join("\n"), "Biohazard Alert", new_sound = 'sound/AI/outbreak7.ogg') \ No newline at end of file diff --git a/code/modules/blob2/blobs/base_blob.dm b/code/modules/blob2/blobs/base_blob.dm new file mode 100644 index 0000000000..01692144cf --- /dev/null +++ b/code/modules/blob2/blobs/base_blob.dm @@ -0,0 +1,298 @@ +var/list/blobs = list() + +/obj/structure/blob + name = "blob" + icon = 'icons/mob/blob.dmi' + desc = "A thick wall of writhing tendrils." + light_range = 2 + density = FALSE // This is false because blob mob AI's walk_to() proc appears to never attempt to move onto dense objects even if allowed by CanPass(). + opacity = FALSE + anchored = TRUE + layer = MOB_LAYER + 0.1 + var/integrity = 0 + var/point_return = 0 //How many points the blob gets back when it removes a blob of that type. If less than 0, blob cannot be removed. + var/max_integrity = 30 + var/health_regen = 2 //how much health this blob regens when pulsed + var/pulse_timestamp = 0 //we got pulsed when? + var/heal_timestamp = 0 //we got healed when? + var/mob/observer/blob/overmind = null + var/base_name = "blob" // The name that gets appended along with the blob_type's name. + +/obj/structure/blob/New(var/newloc, var/new_overmind) + ..(newloc) + if(new_overmind) + overmind = new_overmind + update_icon() + if(!integrity) + integrity = max_integrity + set_dir(pick(cardinal)) + blobs += src + consume_tile() + + +/obj/structure/blob/Destroy() + playsound(src.loc, 'sound/effects/splat.ogg', 50, 1) //Expand() is no longer broken, no check necessary. + blobs -= src + overmind = null + return ..() + +/obj/structure/blob/update_icon() //Updates color based on overmind color if we have an overmind. + if(overmind) + name = "[overmind.blob_type.name] [base_name]" // This is in update_icon() because inert blobs can turn into other blobs with magic if another blob core claims it with pulsing. + color = overmind.blob_type.color + set_light(3, 3, color) + else + name = "inert [base_name]" + color = null + set_light(0) + +/obj/structure/blob/CanPass(atom/movable/mover, turf/target, height=0, air_group=0) + if(air_group || (height==0)) + return TRUE + if(istype(mover) && mover.checkpass(PASSBLOB)) + return TRUE + else + return FALSE +// return ..() + +/obj/structure/blob/examine(mob/user) + ..() + if(!overmind) + to_chat(user, "It seems inert.") // Dead blob. + else + to_chat(user, overmind.blob_type.desc) + +/obj/structure/blob/get_description_info() + if(overmind) + return overmind.blob_type.effect_desc + return ..() + +/obj/structure/blob/emp_act(severity) + if(overmind) + overmind.blob_type.on_emp(src, severity) + +/obj/structure/blob/proc/pulsed() + if(pulse_timestamp <= world.time) + consume_tile() + if(heal_timestamp <= world.time) + adjust_integrity(health_regen) + heal_timestamp = world.time + 2 SECONDS + update_icon() + pulse_timestamp = world.time + 1 SECOND + if(overmind) + overmind.blob_type.on_pulse(src) + return TRUE //we did it, we were pulsed! + return FALSE //oh no we failed + +/obj/structure/blob/proc/pulse_area(pulsing_overmind = overmind, claim_range = 10, pulse_range = 3, expand_range = 2) + src.pulsed() + var/expanded = FALSE + if(prob(70) && expand()) + expanded = TRUE + + var/list/blobs_to_affect = list() + for(var/obj/structure/blob/B in urange(claim_range, src, 1)) + blobs_to_affect += B + + shuffle_inplace(blobs_to_affect) + + for(var/L in blobs_to_affect) + var/obj/structure/blob/B = L + if(!B.overmind && !istype(B, /obj/structure/blob/core) && prob(30)) + B.overmind = pulsing_overmind //reclaim unclaimed, non-core blobs. + B.update_icon() + + var/distance = get_dist(get_turf(src), get_turf(B)) + var/expand_probablity = max(50 / (max(distance, 1)), 1) + if(overmind) + expand_probablity *= overmind.blob_type.spread_modifier + if(overmind.blob_type.slow_spread_with_size) + expand_probablity /= (blobs.len / 10) + + if(distance <= expand_range) + var/can_expand = TRUE + if(blobs_to_affect.len >= 120 && B.heal_timestamp > world.time) + can_expand = FALSE + if(!expanded && can_expand && B.pulse_timestamp <= world.time && prob(expand_probablity)) + var/obj/structure/blob/newB = B.expand(null, null, !expanded) //expansion falls off with range but is faster near the blob causing the expansion + if(newB) + if(expanded) + qdel(newB) + expanded = TRUE + + if(distance <= pulse_range) + B.pulsed() + +/obj/structure/blob/proc/expand(turf/T = null, controller = null, expand_reaction = 1) + if(!T) + var/list/dirs = cardinal.Copy() + for(var/i = 1 to 4) + var/dirn = pick(dirs) + dirs.Remove(dirn) + T = get_step(src, dirn) + if(!(locate(/obj/structure/blob) in T)) + break + else + T = null + if(!T) + return FALSE + + var/make_blob = TRUE //can we make a blob? + + if(istype(T, /turf/space) && !(locate(/obj/structure/lattice) in T) && prob(80)) + make_blob = FALSE + playsound(src.loc, 'sound/effects/splat.ogg', 50, 1) //Let's give some feedback that we DID try to spawn in space, since players are used to it + + consume_tile() //hit the tile we're in, making sure there are no border objects blocking us + + if(!T.CanPass(src, T)) //is the target turf impassable + make_blob = FALSE + T.blob_act(src) //hit the turf if it is + + for(var/atom/A in T) + if(!A.CanPass(src, T)) //is anything in the turf impassable + make_blob = FALSE + A.blob_act(src) //also hit everything in the turf + + if(make_blob) //well, can we? + var/obj/structure/blob/B = new /obj/structure/blob/normal(src.loc) + if(controller) + B.overmind = controller + else + B.overmind = overmind + B.density = TRUE + if(T.Enter(B,src)) //NOW we can attempt to move into the tile + sleep(1) // To have the slide animation work. + B.density = initial(B.density) + B.forceMove(T) + B.update_icon() + if(B.overmind && expand_reaction) + B.overmind.blob_type.on_expand(src, B, T, B.overmind) + return B + + else + blob_attack_animation(T, controller) + T.blob_act(src) //if we can't move in hit the turf again + qdel(B) //we should never get to this point, since we checked before moving in. destroy the blob so we don't have two blobs on one tile + return null + else + blob_attack_animation(T, controller) //if we can't, animate that we attacked + return null + +/obj/structure/blob/proc/consume_tile() + for(var/atom/A in loc) + A.blob_act(src) + if(loc && loc.density) + loc.blob_act(src) //don't ask how a wall got on top of the core, just eat it + +/obj/structure/blob/proc/blob_glow_animation() + flick("[icon_state]_glow", src) + +/obj/structure/blob/proc/blob_attack_animation(atom/A = null, controller) //visually attacks an atom + var/obj/effect/temporary_effect/blob_attack/O = new /obj/effect/temporary_effect/blob_attack(src.loc) + O.set_dir(dir) + if(controller) + var/mob/observer/blob/BO = controller + O.color = BO.blob_type.color + O.alpha = 200 + else if(overmind) + O.color = overmind.blob_type.color + if(A) + O.do_attack_animation(A) //visually attack the whatever + return O //just in case you want to do something to the animation. + +/obj/structure/blob/proc/change_to(type, controller) + if(!ispath(type)) + throw EXCEPTION("change_to(): invalid type for blob") + return + var/obj/structure/blob/B = new type(src.loc, controller) + if(controller) + B.overmind = controller + B.update_icon() + B.set_dir(dir) + qdel(src) + return B + +/obj/structure/blob/attackby(var/obj/item/weapon/W, var/mob/user) + user.setClickCooldown(DEFAULT_ATTACK_COOLDOWN) + playsound(loc, 'sound/effects/attackblob.ogg', 50, 1) + visible_message("\The [src] has been attacked with \the [W][(user ? " by [user]." : ".")]") + var/damage = W.force + switch(W.damtype) + if(BURN) + if(overmind) + damage *= overmind.blob_type.burn_multiplier + else + damage *= 2 + + if(damage > 0) + playsound(src.loc, 'sound/items/welder.ogg', 100, 1) + else + playsound(src, 'sound/weapons/tap.ogg', 50, 1) + if(BRUTE) + if(overmind) + damage *= overmind.blob_type.brute_multiplier + else + damage *= 2 + + if(damage > 0) + playsound(src.loc, 'sound/effects/attackblob.ogg', 50, 1) + else + playsound(src, 'sound/weapons/tap.ogg', 50, 1) + if(overmind) + damage = overmind.blob_type.on_received_damage(src, damage, W.damtype, user) + adjust_integrity(-damage) + return + +/obj/structure/blob/bullet_act(var/obj/item/projectile/P) + if(!P) + return + + var/damage = P.damage + switch(P.damage_type) + if(BRUTE) + if(overmind) + damage *= overmind.blob_type.brute_multiplier + if(BURN) + if(overmind) + damage *= overmind.blob_type.burn_multiplier + + if(overmind) + damage = overmind.blob_type.on_received_damage(src, damage, P.damage_type, P.firer) + + adjust_integrity(-damage) + + return ..() + +/obj/structure/blob/water_act(amount) + if(overmind) + overmind.blob_type.on_water(src, amount) + +/obj/structure/blob/proc/adjust_integrity(amount) + integrity = between(0, integrity + amount, max_integrity) + if(integrity == 0) + playsound(loc, 'sound/effects/splat.ogg', 50, 1) + if(overmind) + overmind.blob_type.on_death(src) + qdel(src) + else + update_icon() + +/obj/effect/temporary_effect/blob_attack + name = "blob" + desc = "The blob lashing out at something." + icon = 'icons/effects/effects.dmi' + icon_state = "blob_attack" + layer = 5.2 + time_to_die = 6 + alpha = 140 + invisibility = 0 + mouse_opacity = 0 + new_light_range = 0 + new_light_power = 0 + +/obj/structure/grille/blob_act() + qdel(src) + +/turf/simulated/wall/blob_act() + take_damage(100) \ No newline at end of file diff --git a/code/modules/blob2/blobs/core.dm b/code/modules/blob2/blobs/core.dm new file mode 100644 index 0000000000..7e2fe741cf --- /dev/null +++ b/code/modules/blob2/blobs/core.dm @@ -0,0 +1,179 @@ +var/list/blob_cores = list() + +/obj/structure/blob/core + name = "blob core" + base_name = "core" + icon = 'icons/mob/blob.dmi' + icon_state = "blank_blob" + desc = "A huge, pulsating yellow mass." + max_integrity = 150 + point_return = -1 + health_regen = 0 //we regen in Life() instead of when pulsed + var/datum/blob_type/desired_blob_type = null // If this is set, the core always creates an overmind possessing this blob type. + var/difficulty_threshold = null // Otherwise if this is set, it picks a random blob_type that is equal or lower in difficulty. + var/core_regen = 2 + var/overmind_get_delay = 0 //we don't want to constantly try to find an overmind, this var tracks when we'll try to get an overmind again + var/resource_delay = 0 + var/point_rate = 2 + var/ai_controlled = TRUE + +// Spawn this if you want a ghost to be able to play as the blob. +/obj/structure/blob/core/player + ai_controlled = FALSE + +// Spawn these if you want a semi-random blob. +/obj/structure/blob/core/random_easy + difficulty_threshold = BLOB_DIFFICULTY_EASY + +/obj/structure/blob/core/random_medium + difficulty_threshold = BLOB_DIFFICULTY_MEDIUM + +/obj/structure/blob/core/random_hard + difficulty_threshold = BLOB_DIFFICULTY_HARD + +// Spawn these if you want a specific blob. +/obj/structure/blob/core/blazing_oil + desired_blob_type = /datum/blob_type/blazing_oil + +/obj/structure/blob/core/grey_goo + desired_blob_type = /datum/blob_type/grey_goo + +/obj/structure/blob/core/electromagnetic_web + desired_blob_type = /datum/blob_type/electromagnetic_web + +/obj/structure/blob/core/fungal_bloom + desired_blob_type = /datum/blob_type/fungal_bloom + +/obj/structure/blob/core/fulminant_organism + desired_blob_type = /datum/blob_type/fulminant_organism + +/obj/structure/blob/core/reactive_spines + desired_blob_type = /datum/blob_type/reactive_spines + +/obj/structure/blob/core/synchronous_mesh + desired_blob_type = /datum/blob_type/synchronous_mesh + +/obj/structure/blob/core/shifting_fragments + desired_blob_type = /datum/blob_type/shifting_fragments + +/obj/structure/blob/core/cryogenic_goo + desired_blob_type = /datum/blob_type/cryogenic_goo + +/obj/structure/blob/core/energized_jelly + desired_blob_type = /datum/blob_type/energized_jelly + +/obj/structure/blob/core/explosive_lattice + desired_blob_type = /datum/blob_type/explosive_lattice + +/obj/structure/blob/core/pressurized_slime + desired_blob_type = /datum/blob_type/pressurized_slime + +/obj/structure/blob/core/radioactive_ooze + desired_blob_type = /datum/blob_type/radioactive_ooze + +/obj/structure/blob/core/classic + desired_blob_type = /datum/blob_type/classic + +/obj/structure/blob/core/New(var/newloc, var/client/new_overmind = null, new_rate = 2, placed = 0) + ..(newloc) + blob_cores += src + processing_objects += src + update_icon() //so it atleast appears + if(!placed && !overmind) + create_overmind(new_overmind) + if(overmind) + update_icon() + point_rate = new_rate + +/obj/structure/blob/core/Destroy() + blob_cores -= src + if(overmind) + overmind.blob_core = null + qdel(overmind) + overmind = null + processing_objects -= src + return ..() + +/obj/structure/blob/core/update_icon() + overlays.Cut() + color = null + var/mutable_appearance/blob_overlay = mutable_appearance('icons/mob/blob.dmi', "blob") + if(overmind) + blob_overlay.color = overmind.blob_type.color + name = "[overmind.blob_type.name] [base_name]" + overlays += blob_overlay + overlays += mutable_appearance('icons/mob/blob.dmi', "blob_core_overlay") + +/obj/structure/blob/core/process() + set waitfor = FALSE + if(QDELETED(src)) + return + if(!overmind) + spawn(0) + create_overmind() + else + if(resource_delay <= world.time) + resource_delay = world.time + 1 SECOND + overmind.add_points(point_rate) + integrity = min(max_integrity, integrity + core_regen) +// if(overmind) +// overmind.update_health_hud() + pulse_area(overmind, 15, BLOB_CORE_PULSE_RANGE, BLOB_CORE_EXPAND_RANGE) + for(var/obj/structure/blob/normal/B in range(1, src)) + if(prob(5)) + B.change_to(/obj/structure/blob/shield/core, overmind) + +/obj/structure/blob/core/proc/create_overmind(client/new_overmind, override_delay) + if(overmind_get_delay > world.time && !override_delay) + return + if(!ai_controlled) // Do we want a bona fide player blob? + overmind_get_delay = world.time + 15 SECONDS //if this fails, we'll try again in 15 seconds + + if(overmind) + qdel(overmind) + + + var/client/C = null + if(!new_overmind) + var/datum/ghost_query/Q = new /datum/ghost_query/blob() + var/list/winner = Q.query() + if(winner.len) + var/mob/observer/dead/D = winner[1] + C = D.client + + else + C = new_overmind + + if(C) + if(!desired_blob_type && !isnull(difficulty_threshold)) + desired_blob_type = get_random_blob_type() + var/mob/observer/blob/B = new(loc, TRUE, 60, desired_blob_type) + B.key = C.key + B.blob_core = src + src.overmind = B + update_icon() + if(B.mind && !B.mind.special_role) + B.mind.special_role = "Blob Overmind" + return TRUE + return FALSE + + else // An AI opponent. + if(!desired_blob_type && !isnull(difficulty_threshold)) + desired_blob_type = get_random_blob_type() + var/mob/observer/blob/B = new(loc, TRUE, 60, desired_blob_type) + overmind = B + B.blob_core = src + B.ai_controlled = TRUE + update_icon() + return TRUE + +/obj/structure/blob/core/proc/get_random_blob_type() + if(!difficulty_threshold) + return + var/list/valid_types = list() + for(var/thing in subtypesof(/datum/blob_type)) + var/datum/blob_type/BT = thing + if(initial(BT.difficulty) > difficulty_threshold) + continue + valid_types += BT + return pick(valid_types) \ No newline at end of file diff --git a/code/modules/blob2/blobs/factory.dm b/code/modules/blob2/blobs/factory.dm new file mode 100644 index 0000000000..6cd8bfa0f2 --- /dev/null +++ b/code/modules/blob2/blobs/factory.dm @@ -0,0 +1,37 @@ +/obj/structure/blob/factory + name = "factory blob" + base_name = "factory" + icon = 'icons/mob/blob.dmi' + icon_state = "blob_factory" + desc = "A thick spire of tendrils." + description_info = "A section of the blob that creates numerous hostile entities to attack enemies of the blob. \ + It requires a 'node' blob be nearby, or it will cease functioning." + max_integrity = 40 + health_regen = 1 + point_return = 25 + var/list/spores = list() + var/max_spores = 3 + var/spore_delay = 0 + var/spore_cooldown = 8 SECONDS + +/obj/structure/blob/factory/Destroy() + for(var/mob/living/simple_animal/hostile/blob/spore/spore in spores) + if(spore.factory == src) + spore.factory = null + spores = null + return ..() + +/obj/structure/blob/factory/pulsed() + . = ..() + if(spores.len >= max_spores) + return + if(spore_delay > world.time) + return + flick("blob_factory_glow", src) + spore_delay = world.time + spore_cooldown + var/mob/living/simple_animal/hostile/blob/spore/S = null + if(overmind) + S = new overmind.blob_type.spore_type(src.loc, src) + S.overmind = overmind + S.update_icons() + overmind.blob_mobs.Add(S) \ No newline at end of file diff --git a/code/modules/blob2/blobs/node.dm b/code/modules/blob2/blobs/node.dm new file mode 100644 index 0000000000..65d47fa1af --- /dev/null +++ b/code/modules/blob2/blobs/node.dm @@ -0,0 +1,36 @@ +var/list/blob_nodes = list() + +/obj/structure/blob/node + name = "blob node" + base_name = "node" + icon_state = "blank_blob" + desc = "A large, pulsating yellow mass." + max_integrity = 50 + health_regen = 3 + point_return = 50 + +/obj/structure/blob/node/New(var/newloc) + ..() + blob_nodes += src + processing_objects += src + update_icon() + +/obj/structure/blob/node/Destroy() + blob_nodes -= src + processing_objects -= src + return ..() + +/obj/structure/blob/node/update_icon() + overlays.Cut() + color = null + var/mutable_appearance/blob_overlay = mutable_appearance('icons/mob/blob.dmi', "blob") + if(overmind) + name = "[overmind.blob_type.name] [base_name]" + blob_overlay.color = overmind.blob_type.color + overlays += blob_overlay + overlays += mutable_appearance('icons/mob/blob.dmi', "blob_node_overlay") + +/obj/structure/blob/node/process() + set waitfor = FALSE + if(overmind) // This check is so that if the core is killed, the nodes stop. + pulse_area(overmind, 10, BLOB_NODE_PULSE_RANGE, BLOB_NODE_EXPAND_RANGE) \ No newline at end of file diff --git a/code/modules/blob2/blobs/normal.dm b/code/modules/blob2/blobs/normal.dm new file mode 100644 index 0000000000..a6e841d4dd --- /dev/null +++ b/code/modules/blob2/blobs/normal.dm @@ -0,0 +1,22 @@ +/obj/structure/blob/normal + name = "normal blob" + base_name = "blob" + icon_state = "blob" + light_range = 0 + integrity = 21 //doesn't start at full health + max_integrity = 25 + health_regen = 1 + +/obj/structure/blob/normal/update_icon() + ..() + if(integrity <= 15) + icon_state = "blob_damaged" + desc = "A thin lattice of slightly twitching tendrils." + else + icon_state = "blob" + desc = "A thick wall of writhing tendrils." + + if(overmind) + name = "[overmind.blob_type.name]" + else + name = "inert [base_name]" \ No newline at end of file diff --git a/code/modules/blob2/blobs/resource.dm b/code/modules/blob2/blobs/resource.dm new file mode 100644 index 0000000000..f8ff4e48e4 --- /dev/null +++ b/code/modules/blob2/blobs/resource.dm @@ -0,0 +1,30 @@ +/obj/structure/blob/resource + name = "resource blob" + base_name = "resource blob" + icon = 'icons/mob/blob.dmi' + icon_state = "blob_resource" + desc = "A thin spire of slightly swaying tendrils." + max_integrity = 40 + point_return = 15 + var/resource_delay = 0 + +/obj/structure/blob/resource/New(var/newloc, var/new_overmind) + ..(newloc, new_overmind) + if(overmind) + overmind.resource_blobs += src + +/obj/structure/blob/resource/Destroy() + if(overmind) + overmind.resource_blobs -= src + return ..() + +/obj/structure/blob/resource/pulsed() + . = ..() + if(resource_delay > world.time) + return + flick("blob_resource_glow", src) + if(overmind) + overmind.add_points(1) + resource_delay = world.time + 4 SECONDS + (overmind.resource_blobs.len * 2.5) //4 seconds plus a quarter second for each resource blob the overmind has + else + resource_delay = world.time + 4 SECONDS \ No newline at end of file diff --git a/code/modules/blob2/blobs/shield.dm b/code/modules/blob2/blobs/shield.dm new file mode 100644 index 0000000000..a519d982a4 --- /dev/null +++ b/code/modules/blob2/blobs/shield.dm @@ -0,0 +1,25 @@ +/obj/structure/blob/shield + name = "thick blob" + base_name = "thick" + icon = 'icons/mob/blob.dmi' + icon_state = "blob_shield" + desc = "A solid wall of slightly twitching tendrils." + max_integrity = 100 + point_return = 4 + +/obj/structure/blob/shield/core + point_return = 0 + +/obj/structure/blob/shield/update_icon() + ..() + if(integrity <= 75) + icon_state = "blob_shield_damaged" + desc = "A wall of twitching tendrils." + else + icon_state = initial(icon_state) + desc = initial(desc) + + if(overmind) + name = "[base_name] [overmind.blob_type.name]" + else + name = "inert [base_name] blob" \ No newline at end of file diff --git a/code/modules/blob2/mobs/blob_mob.dm b/code/modules/blob2/mobs/blob_mob.dm new file mode 100644 index 0000000000..c4e9e28d10 --- /dev/null +++ b/code/modules/blob2/mobs/blob_mob.dm @@ -0,0 +1,57 @@ +//////////////// +// BASE TYPE // +//////////////// + +//Do not spawn +/mob/living/simple_animal/hostile/blob + icon = 'icons/mob/blob.dmi' + pass_flags = PASSBLOB | PASSTABLE + faction = "blob" +// bubble_icon = "blob" +// speak_emote = null //so we use verb_yell/verb_say/etc +// 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) +// minbodytemp = 0 +// maxbodytemp = 360 +// unique_name = 1 +// a_intent = INTENT_HARM + cooperative = TRUE + heat_damage_per_tick = 0 + cold_damage_per_tick = 0 + min_oxy = 0 + max_tox = 0 + max_co2 = 0 + var/mob/observer/blob/overmind = null + var/obj/structure/blob/factory/factory = null + +/mob/living/simple_animal/hostile/blob/speech_bubble_appearance() + return "slime" + +/mob/living/simple_animal/hostile/blob/update_icons() + if(overmind) + color = overmind.blob_type.complementary_color + else + color = null + +/mob/living/simple_animal/hostile/blob/Destroy() + if(overmind) + overmind.blob_mobs -= src + return ..() + +/mob/living/simple_animal/hostile/blob/blob_act(obj/structure/blob/B) + if(!overmind && B.overmind) + overmind = B.overmind + update_icon() + + if(stat != DEAD && health < maxHealth) + adjustBruteLoss(-maxHealth*0.0125) + adjustFireLoss(-maxHealth*0.0125) + +/mob/living/simple_animal/hostile/blob/CanPass(atom/movable/mover, turf/target) + if(istype(mover, /obj/structure/blob)) // Don't block blobs from expanding onto a tile occupied by a blob mob. + return TRUE + return ..() + +/mob/living/simple_animal/hostile/blob/Process_Spacemove() + for(var/obj/structure/blob/B in range(1, src)) + return TRUE + return ..() diff --git a/code/modules/blob2/mobs/spore.dm b/code/modules/blob2/mobs/spore.dm new file mode 100644 index 0000000000..de8d6e2dfb --- /dev/null +++ b/code/modules/blob2/mobs/spore.dm @@ -0,0 +1,110 @@ +//////////////// +// BLOB SPORE // +//////////////// + +/mob/living/simple_animal/hostile/blob/spore + name = "blob spore" + desc = "A floating, fragile spore." + icon_state = "blobpod" + icon_living = "blobpod" + health = 30 + maxHealth = 30 + melee_damage_lower = 2 + melee_damage_upper = 4 + layer = MOB_LAYER + 0.2 // Over the blob. + attacktext = "slams into" + attack_sound = 'sound/effects/slime_squish.ogg' + emote_see = list("sways", "inflates briefly") + var/mob/living/carbon/human/infested = null // The human this thing is totally not making into a zombie. + var/can_infest = FALSE + var/is_infesting = FALSE + +/mob/living/simple_animal/hostile/blob/spore/infesting + name = "infesting blob spore" + can_infest = TRUE + +/mob/living/simple_animal/hostile/blob/spore/weak + name = "fragile blob spore" + health = 15 + maxHealth = 15 + melee_damage_lower = 1 + melee_damage_upper = 2 + +/mob/living/simple_animal/hostile/blob/spore/New(var/newloc, var/obj/structure/blob/factory/my_factory) + if(istype(my_factory)) + factory = my_factory + factory.spores += src + ..(newloc) + +/mob/living/simple_animal/hostile/blob/spore/Destroy() + if(factory) + factory.spores -= src + factory = null + if(infested) + infested.forceMove(get_turf(src)) + visible_message("\The [infested] falls to the ground as the blob spore bursts.") + infested = null + return ..() + +/mob/living/simple_animal/hostile/blob/spore/death(gibbed, deathmessage = "bursts!") + if(overmind) + overmind.blob_type.on_spore_death(src) + ..(gibbed, deathmessage) + qdel(src) + +/mob/living/simple_animal/hostile/blob/spore/update_icons() + if(overmind) + color = overmind.blob_type.complementary_color + set_light(3, 5, color) + else + color = null + set_light(0) + + if(is_infesting) + overlays.Cut() + icon = infested.icon + overlays = infested.overlays + var/mutable_appearance/blob_head_overlay = mutable_appearance('icons/mob/blob.dmi', "blob_head") + if(overmind) + blob_head_overlay.color = overmind.blob_type.complementary_color + color = initial(color)//looks better. + overlays += blob_head_overlay + +/mob/living/simple_animal/hostile/blob/spore/Life() + if(can_infest && !is_infesting && isturf(src.loc)) + for(var/mob/living/carbon/human/H in view(src,1)) + if(H.stat != DEAD) // We want zombies. + continue + if(H.isSynthetic()) // Not philosophical zombies. + continue + infest(H) + break + if(factory && z != factory.z) // This is to prevent spores getting lost in space and making the factory useless. + qdel(src) + ..() + +/mob/living/simple_animal/hostile/blob/spore/proc/infest(mob/living/carbon/human/H) + is_infesting = TRUE + if(H.wear_suit) + var/obj/item/clothing/suit/A = H.wear_suit + if(A.armor && A.armor["melee"]) + maxHealth += A.armor["melee"] //That zombie's got armor, I want armor! + + maxHealth += 40 + health = maxHealth + name = "Infested [H.real_name]" // Not using the Z word. + desc = "A parasitic organism attached to a deceased body, controlling it directly as if it were a puppet." + melee_damage_lower += 8 // 10 total. + melee_damage_upper += 11 // 15 total. + emote_see = list("shambles around", "twitches", "stares") + attacktext = "claws" + + H.forceMove(src) + infested = H + + update_icons() + visible_message("The corpse of [H.name] suddenly rises!") + +/mob/living/simple_animal/hostile/blob/spore/GetIdCard() + if(infested) // If we've infested someone, use their ID. + return infested.GetIdCard() \ No newline at end of file diff --git a/code/modules/blob2/overmind/overmind.dm b/code/modules/blob2/overmind/overmind.dm new file mode 100644 index 0000000000..9b12c4c873 --- /dev/null +++ b/code/modules/blob2/overmind/overmind.dm @@ -0,0 +1,97 @@ +var/list/overminds = list() + +/mob/observer/blob + name = "Blob Overmind" + real_name = "Blob Overmind" + desc = "The overmind. It controls the blob." + icon = 'icons/mob/blob.dmi' + icon_state = "marker" + mouse_opacity = 1 + see_in_dark = 8 + invisibility = INVISIBILITY_OBSERVER + layer = FLY_LAYER + 0.1 + + faction = "blob" + var/obj/structure/blob/core/blob_core = null // The blob overmind's core + var/blob_points = 0 + var/max_blob_points = 200 + var/last_attack = 0 + var/datum/blob_type/blob_type = null + var/list/blob_mobs = list() + var/list/resource_blobs = list() + var/placed = 0 + var/base_point_rate = 2 //for blob core placement + var/ai_controlled = TRUE + var/auto_pilot = FALSE // If true, and if a client is attached, the AI routine will continue running. + +/mob/observer/blob/New(var/newloc, pre_placed = 0, starting_points = 60, desired_blob_type = null) + blob_points = starting_points + if(pre_placed) //we already have a core! + placed = 1 + + overminds += src + var/new_name = "[initial(name)] ([rand(1, 999)])" + name = new_name + real_name = new_name + if(desired_blob_type) + blob_type = new desired_blob_type() + else + var/datum/blob_type/BT = pick(subtypesof(/datum/blob_type)) + blob_type = new BT() + color = blob_type.complementary_color + if(blob_core) + blob_core.update_icon() + level_seven_blob_announcement(blob_core) + + ..(newloc) + +/mob/observer/blob/Destroy() + for(var/BL in blobs) + var/obj/structure/blob/B = BL + if(B && B.overmind == src) + B.overmind = null + B.update_icon() //reset anything that was ours + + for(var/BLO in blob_mobs) + var/mob/living/simple_animal/hostile/blob/BM = BLO + if(BM) + BM.overmind = null + BM.update_icons() + + overminds -= src + return ..() + +/mob/observer/blob/Stat() + ..() + if(statpanel("Status")) + if(blob_core) + stat(null, "Core Health: [blob_core.integrity]") + stat(null, "Power Stored: [blob_points]/[max_blob_points]") + stat(null, "Total Blobs: [blobs.len]") + +/mob/observer/blob/Move(NewLoc, Dir = 0) + if(placed) + var/obj/structure/blob/B = locate() in range("3x3", NewLoc) + if(B) + forceMove(NewLoc) + return TRUE + else + return FALSE + else + var/area/A = get_area(NewLoc) + if(istype(NewLoc, /turf/space) || istype(A, /area/shuttle)) //if unplaced, can't go on shuttles or space tiles + return FALSE + forceMove(NewLoc) + return TRUE + +/mob/observer/blob/proc/add_points(points) + blob_points = between(0, blob_points + points, max_blob_points) + +/mob/observer/blob/Life() + if(ai_controlled && (!client || auto_pilot)) + if(prob(blob_type.ai_aggressiveness)) + auto_attack() + + if(blob_points >= 100) + if(!auto_factory() && !auto_resource()) + auto_node() \ No newline at end of file diff --git a/code/modules/blob2/overmind/powers.dm b/code/modules/blob2/overmind/powers.dm new file mode 100644 index 0000000000..35a6a9e9c7 --- /dev/null +++ b/code/modules/blob2/overmind/powers.dm @@ -0,0 +1,229 @@ +/mob/observer/blob/proc/can_buy(cost = 15) + if(blob_points < cost) + to_chat(src, "You cannot afford this, you need at least [cost] resources!") + return FALSE + add_points(-cost) + return TRUE + +/mob/observer/blob/verb/transport_core() + set category = "Blob" + set name = "Jump to Core" + set desc = "Move your camera to your core." + + if(blob_core) + forceMove(blob_core.loc) + +/mob/observer/blob/proc/createSpecial(price, blobType, nearEquals, needsNode, turf/T) + if(!T) + T = get_turf(src) + var/obj/structure/blob/B = (locate(/obj/structure/blob) in T) + + if(!B) + to_chat(src, "There is no blob here!") + return + + if(!istype(B, /obj/structure/blob/normal)) + to_chat(src, "Unable to use this blob, find a normal one.") + return + + if(nearEquals) + for(var/obj/structure/blob/L in orange(nearEquals, T)) + if(L.type == blobType) + to_chat(src, "There is a similar blob nearby, move more than [nearEquals] tiles away from it!") + return + + if(!can_buy(price)) + return + + var/obj/structure/blob/N = B.change_to(blobType, src) + return N + +/mob/observer/blob/verb/create_shield_power() + set category = "Blob" + set name = "Create Shield Blob (15)" + set desc = "Create a shield blob, which is hard to kill." + create_shield() + +/mob/observer/blob/proc/create_shield(turf/T) + createSpecial(15, /obj/structure/blob/shield, 0, 0, T) + +/mob/observer/blob/verb/create_resource() + set category = "Blob" + set name = "Create Resource Blob (40)" + set desc = "Create a resource tower which will generate resources for you." + + if(!blob_type.can_build_resources) + return FALSE + + createSpecial(40, /obj/structure/blob/resource, 4, 1) + +/mob/observer/blob/verb/auto_resource() + set category = "Blob" + set name = "Auto Resource Blob (40)" + set desc = "Automatically places a resource tower near a node or your core, at a sufficent distance." + + if(!blob_type.can_build_resources) + return FALSE + + var/obj/structure/blob/B = null + var/list/potential_blobs = blobs.Copy() + while(potential_blobs.len) + var/obj/structure/blob/temp = pick(potential_blobs) + if(!(locate(/obj/structure/blob/node) in range(temp, BLOB_NODE_PULSE_RANGE) ) && !(locate(/obj/structure/blob/core) in range(temp, BLOB_CORE_PULSE_RANGE) )) + potential_blobs -= temp // Can't be pulsed. + else if(locate(/obj/structure/blob/resource) in range(temp, 4) ) + potential_blobs -= temp // Too close to another resource blob. + else if(locate(/obj/structure/blob/core) in range(temp, 1) ) + potential_blobs -= temp // Don't take up the core's shield spot. + else if(!istype(temp, /obj/structure/blob/normal)) + potential_blobs -= temp // Not a normal blob. + else + B = temp + break + + CHECK_TICK // Iterating over a list containing hundreds of blobs can get taxing. + + if(B) + forceMove(B.loc) + return createSpecial(40, /obj/structure/blob/resource, 4, 1, B.loc) + + +/mob/observer/blob/verb/create_factory() + set category = "Blob" + set name = "Create Factory Blob (60)" + set desc = "Create a spore tower that will spawn spores to harass your enemies." + + if(!blob_type.can_build_factories) + return FALSE + + createSpecial(60, /obj/structure/blob/factory, 7, 1) + +/mob/observer/blob/verb/auto_factory() + set category = "Blob" + set name = "Auto Factory Blob (60)" + set desc = "Automatically places a resource tower near a node or your core, at a sufficent distance." + + if(!blob_type.can_build_factories) + return FALSE + + var/obj/structure/blob/B = null + var/list/potential_blobs = blobs.Copy() + while(potential_blobs.len) + var/obj/structure/blob/temp = pick(potential_blobs) + if(!(locate(/obj/structure/blob/node) in range(temp, BLOB_NODE_PULSE_RANGE) ) && !(locate(/obj/structure/blob/core) in range(temp, BLOB_CORE_PULSE_RANGE) )) + potential_blobs -= temp // Can't be pulsed. + else if(locate(/obj/structure/blob/factory) in range(temp, 7) ) + potential_blobs -= temp // Too close to another factory blob. + else if(locate(/obj/structure/blob/core) in range(temp, 1) ) + potential_blobs -= temp // Don't take up the core's shield spot. + else if(!istype(temp, /obj/structure/blob/normal)) + potential_blobs -= temp // Not a normal blob. + else + B = temp + break + + CHECK_TICK + + if(B) + forceMove(B.loc) + return createSpecial(60, /obj/structure/blob/factory, 7, 1, B.loc) + + + +/mob/observer/blob/verb/create_node() + set category = "Blob" + set name = "Create Node Blob (100)" + set desc = "Create a node, which will expand blobs around it, and power nearby factory and resource blobs." + + if(!blob_type.can_build_nodes) + return FALSE + + createSpecial(100, /obj/structure/blob/node, 5, 0) + +/mob/observer/blob/verb/auto_node() + set category = "Blob" + set name = "Auto Node Blob (100)" + set desc = "Automatically places a node blob at a sufficent distance." + + if(!blob_type.can_build_nodes) + return FALSE + + var/obj/structure/blob/B = null + var/list/potential_blobs = blobs.Copy() + while(potential_blobs.len) + var/obj/structure/blob/temp = pick(potential_blobs) + if(locate(/obj/structure/blob/node) in range(temp, 5) ) + potential_blobs -= temp + else if(locate(/obj/structure/blob/core) in range(temp, 5) ) + potential_blobs -= temp + else if(!istype(temp, /obj/structure/blob/normal)) + potential_blobs -= temp + else + B = temp + break + + CHECK_TICK + + if(B) + forceMove(B.loc) + return createSpecial(100, /obj/structure/blob/node, 5, 0, B.loc) + + + +/mob/observer/blob/verb/expand_blob_power() + set category = "Blob" + set name = "Expand/Attack Blob (4)" + set desc = "Attempts to create a new blob in this tile. If the tile isn't clear, instead attacks it, damaging mobs and objects." + var/turf/T = get_turf(src) + expand_blob(T) + +/mob/observer/blob/proc/expand_blob(turf/T) + var/obj/structure/blob/B = null + var/turf/other_T = null + for(var/direction in cardinal) + other_T = get_step(T, direction) + if(other_T) + B = locate(/obj/structure/blob) in other_T + if(B) + break + + if(!B) + to_chat(src, "There is no blob cardinally adjacent to the target tile!") + return + + if(!can_buy(4)) + return + + B.expand(T) + +/mob/observer/blob/verb/auto_attack() + set category = "Blob" + set name = "Auto Attack (4)" + set desc = "Automatically tries to kill whatever's attacking you." + + transport_core() // In-case the overmind wandered off somewhere else. + + var/list/potential_targets = list() + for(var/mob/living/L in view(src)) + if(L.stat == DEAD) + continue // Already dying or dead. + if(L.faction == "blob") + continue // No friendly fire. + if(locate(/obj/structure/blob) in L.loc) + continue // Already has a blob over them. + + var/obj/structure/blob/B = null + for(var/direction in cardinal) + var/turf/T = get_step(L, direction) + B = locate(/obj/structure/blob) in T + if(B) + break + if(!B) + continue + + potential_targets += L + + if(potential_targets.len) + var/mob/living/victim = pick(potential_targets) + var/turf/T = get_turf(victim) + expand_blob(T) diff --git a/code/modules/blob2/overmind/types.dm b/code/modules/blob2/overmind/types.dm new file mode 100644 index 0000000000..167fa7d8d1 --- /dev/null +++ b/code/modules/blob2/overmind/types.dm @@ -0,0 +1,545 @@ +// There are different kinds of blobs, with different colors, properties, weaknesses, etc. This datum tells the blob objects what kind they are, without a million typepaths. +/datum/blob_type + var/name = "base blob" + var/desc = "This shouldn't exist." // Shown on examine. + var/effect_desc = "This does nothing special." // For examine panel. + var/ai_desc = "default" // Shown when examining the overmind. + var/difficulty = BLOB_DIFFICULTY_EASY // A rough guess on how hard a blob is to kill. + // When a harder blob spawns by event, the crew is given more information than usual from the announcement. + var/color = "#FFFFFF" // The actual blob's color. + var/complementary_color = "#000000" //a color that's complementary to the normal blob color. Blob mobs are colored in this. + + var/attack_message = "The blob attacks you" // Base message the mob gets when blob_act() gets called on them by the blob. An exclaimation point is added to the end. + var/attack_message_living = null // Appended to attack_message, if the target fails isSynthetic() check. + var/attack_message_synth = null // Ditto, but if they pass isSynthetic(). + var/attack_verb = "attacks" // Used for the visible_message(), as the above is shown to the mob getting hit directly. + // Format is '\The [blob name] [attack_verb] [victim]!' E.g. 'The explosive lattice blasts John Doe!' + + var/damage_type = BRUTE // What kind of damage to do to living mobs via blob_act() + var/armor_check = "melee" // What armor to check for when blob_act()-ing living mobs. + var/armor_pen = 0 // How much armor to penetrate(ignore) when attacking via blob_act(). + var/damage_lower = 30 // Lower bound for amount of damage to do for attacks. + var/damage_upper = 40 // Upper bound. + + var/brute_multiplier = 0.5 // Adjust to make blobs stonger or weaker against brute damage. + var/burn_multiplier = 1.0 // Ditto, for burns. + var/spread_modifier = 0.5 // A multipler on how fast the blob should naturally spread from the core and nodes. + var/slow_spread_with_size = TRUE // Blobs that get really huge will slow down in expansion. + + var/ai_aggressiveness = 10 // Probability of the blob AI attempting to attack someone next to the blob, independant of the attacks from node/core pulsing. + + var/can_build_factories = FALSE // Forbids this blob type from building factories. Set to true to enable. + var/can_build_resources = FALSE // Ditto, for resource blobs. + var/can_build_nodes = TRUE // Ditto, for nodes. + + var/spore_type = /mob/living/simple_animal/hostile/blob/spore + +// Called when a blob receives damage. This needs to return the final damage or blobs will be immortal. +/datum/blob_type/proc/on_received_damage(var/obj/structure/blob/B, damage, damage_type) + return damage + +// Called when a blob dies due to integrity depletion. Not called if deleted by other means. +/datum/blob_type/proc/on_death(var/obj/structure/blob/B) + return + +// Called when a blob expands onto another tile. +/datum/blob_type/proc/on_expand(var/obj/structure/blob/B, var/obj/structure/blob/new_B, var/turf/T, var/mob/observer/blob/O) + return + +// Called when blob_act() is called on a living mob. +/datum/blob_type/proc/on_attack(var/obj/structure/blob/B, var/mob/living/victim, var/def_zone) + return + +// Called when the blob is pulsed by a node or the core. +/datum/blob_type/proc/on_pulse(var/obj/structure/blob/B) + return + +// Called when hit by EMP. +/datum/blob_type/proc/on_emp(obj/structure/blob/B, severity) + return + +// Called when hit by water. +/datum/blob_type/proc/on_water(obj/structure/blob/B, amount) + return + +// Spore things +/datum/blob_type/proc/on_spore_death(mob/living/simple_animal/hostile/blob/spore/S) + return + + +// Subtypes + +// Super fast spreading, but weak to EMP. +/datum/blob_type/grey_goo + name = "grey tide" + desc = "A swarm of self replicating nanomachines. Extremely illegal and dangerous, the EIO was meant to prevent this from showing up a second time." + effect_desc = "Spreads much faster than average, but is harmed greatly by electromagnetic pulses." + ai_desc = "genocidal" + difficulty = BLOB_DIFFICULTY_SUPERHARD // Fastest spread of them all and has snowballing capabilities. + color = "#888888" + complementary_color = "#CCCCCC" + spread_modifier = 1.0 + slow_spread_with_size = FALSE + ai_aggressiveness = 80 + can_build_resources = TRUE + attack_message = "The tide tries to shallow you" + attack_message_living = ", and you feel your skin dissolve" + attack_message_synth = ", and your external plating dissolves" + +/datum/blob_type/grey_goo/on_emp(obj/structure/blob/B, severity) + B.adjust_integrity(-(20 / severity)) + + +// A blob meant to be fought like a fire. +/datum/blob_type/blazing_oil + name = "blazing oil" + desc = "A strange, extremely vicious liquid that seems to burn endlessly." + ai_desc = "aggressive" + effect_desc = "Cannot be harmed by burning weapons, and ignites entities it attacks. It will also gradually heat up the area it is in. Water harms it greatly." + difficulty = BLOB_DIFFICULTY_MEDIUM // Emitters don't work but extinguishers are fairly common. Might need fire/atmos suits. + color = "#B68D00" + complementary_color = "#BE5532" + spread_modifier = 0.5 + ai_aggressiveness = 50 + damage_type = BURN + burn_multiplier = 0 // Fire immunity + attack_message = "The blazing oil splashes you with its burning oil" + attack_message_living = ", and you feel your skin char and melt" + attack_message_synth = ", and your external plating melts" + attack_verb = "splashes" + +/datum/blob_type/blazing_oil/on_attack(obj/structure/blob/B, mob/living/victim) + victim.fire_act() // Burn them. + +/datum/blob_type/blazing_oil/on_water(obj/structure/blob/B, amount) + spawn(1) + B.adjust_integrity(-(amount * 5)) + +/datum/blob_type/blazing_oil/on_pulse(var/obj/structure/blob/B) + var/turf/T = get_turf(B) + if(!T) + return + var/datum/gas_mixture/env = T.return_air() + if(env) + env.add_thermal_energy(10 * 1000) + + +// Mostly a classic blob. No nodes, no other blob types. +/datum/blob_type/classic + name = "lethargic blob" + desc = "A mass that seems bound to its core." + ai_desc = "unambitious" + effect_desc = "Will not create any nodes. Has average strength and resistances." + difficulty = BLOB_DIFFICULTY_EASY // Behaves almost like oldblob, and as such is about as easy as oldblob. + color = "#AAFF00" + complementary_color = "#57787B" + can_build_nodes = FALSE + spread_modifier = 1.0 + ai_aggressiveness = 0 + + +// Makes robots cry. Really weak to brute damage. +/datum/blob_type/electromagnetic_web + name = "electromagnetic web" + desc = "A gooy mesh that generates an electromagnetic field. Electronics will likely be ruined if nearby." + ai_desc = "balanced" + effect_desc = "Causes an EMP on attack, and will EMP upon death. It is also more fragile than average, especially to brute force." + difficulty = BLOB_DIFFICULTY_MEDIUM // Rough for robots but otherwise fragile and can be fought at range like most blobs anyways. + color = "#83ECEC" + complementary_color = "#EC8383" + damage_type = BURN + damage_lower = 10 + damage_upper = 20 + brute_multiplier = 3 + burn_multiplier = 2 + ai_aggressiveness = 60 + attack_message = "The web lashes you" + attack_message_living = ", and you hear a faint buzzing" + attack_message_synth = ", and your electronics get badly damaged" + attack_verb = "lashes" + +/datum/blob_type/electromagnetic_web/on_death(obj/structure/blob/B) + empulse(B.loc, 0, 1, 2) + +/datum/blob_type/electromagnetic_web/on_attack(obj/structure/blob/B, mob/living/victim) + victim.emp_act(2) + + +// Makes spores that spread the blob and infest dead people. +/datum/blob_type/fungal_bloom + name = "fungal bloom" + desc = "A massive network of rapidly expanding mycelium. Large spore-like particles can be seen spreading from it." + ai_desc = "swarming" + effect_desc = "Creates floating spores that attack enemies from specialized blobs, and will spread the blob if killed. The spores can also \ + infest deceased biological humanoids. It is vulnerable to fire." + difficulty = BLOB_DIFFICULTY_MEDIUM // The spores are more of an annoyance but can be difficult to contain. + color = "#AAAAAA" + complementary_color = "#FFFFFF" + damage_type = TOX + damage_lower = 15 + damage_upper = 25 + spread_modifier = 0.3 // Lower, since spores will do a lot of the spreading. + burn_multiplier = 3 + ai_aggressiveness = 40 + can_build_factories = TRUE + spore_type = /mob/living/simple_animal/hostile/blob/spore/infesting + +/datum/blob_type/fungal_bloom/on_spore_death(mob/living/simple_animal/hostile/blob/spore/S) + if(S.is_infesting) + return // Don't make blobs if they were on someone's head. + var/turf/T = get_turf(S) + var/obj/structure/blob/B = locate(/obj/structure/blob) in T + if(B) // Is there already a blob here? If so, just heal it. + B.adjust_integrity(10) + else + B = new /obj/structure/blob/normal(T, S.overmind) // Otherwise spread it. + B.visible_message("\A [B] forms on \the [T] as \the [S] bursts!") + +// Makes tons of weak spores whenever it spreads. +/datum/blob_type/fulminant_organism + name = "fulminant organism" + desc = "A self expanding mass of living biomaterial, that appears to produce entities to defend it, much like a living organism's immune system." + ai_desc = "swarming" + effect_desc = "Creates weak floating spores that attack enemies from specialized blobs, has a chance to also create a spore when \ + it spreads onto a new tile, and has a chance to create a spore when a blob tile is destroyed. It is more fragile than average to all types of damage." + difficulty = BLOB_DIFFICULTY_HARD // Loads of spores that can overwhelm, and spreads quickly. + color = "#FF0000" // Red + complementary_color = "#FFCC00" // Orange-ish + damage_type = TOX + damage_lower = 10 + damage_upper = 20 + spread_modifier = 0.7 + burn_multiplier = 1.5 + brute_multiplier = 1.5 + ai_aggressiveness = 30 // The spores do most of the fighting. + can_build_factories = TRUE + spore_type = /mob/living/simple_animal/hostile/blob/spore/weak + +/datum/blob_type/fulminant_organism/on_expand(var/obj/structure/blob/B, var/obj/structure/blob/new_B, var/turf/T, var/mob/observer/blob/O) + if(prob(10)) // 10% chance to make a weak spore when expanding. + var/mob/living/simple_animal/hostile/blob/S = new spore_type(T) + S.overmind = O + S.update_icons() + O.blob_mobs.Add(S) + +/datum/blob_type/fulminant_organism/on_death(obj/structure/blob/B) + if(prob(33)) // 33% chance to make a spore when dying. + var/mob/living/simple_animal/hostile/blob/S = new spore_type(get_turf(B)) + B.visible_message("A spore floats free from the [name]!") + S.overmind = B.overmind + S.update_icons() + B.overmind.blob_mobs.Add(S) + + +// Auto-retaliates against melee attacks. Weak to projectiles. +/datum/blob_type/reactive_spines + name = "reactive spines" + desc = "An ever-growing lifeform with a large amount of sharp, powerful looking spines. They look like they could pierce most armor." + ai_desc = "defensive" + effect_desc = "When attacked by a melee weapon, it will automatically retaliate, striking the attacker with an armor piercing attack. \ + The blob itself is rather weak to all forms of attacks regardless, and lacks automatic realitation from ranged attacks." + difficulty = BLOB_DIFFICULTY_EASY // Potentially deadly to people not knowing the mechanics, but otherwise fairly tame, due to its slow spread and weakness. + color = "#9ACD32" + complementary_color = "#FFA500" + damage_type = BRUTE + damage_lower = 30 + damage_upper = 40 + armor_pen = 50 // Even with riot armor and tactical jumpsuit, you'd have 90 armor, reduced by 50, totaling 40. Getting hit for around 21 damage is still rough. + burn_multiplier = 2.0 + brute_multiplier = 2.0 + spread_modifier = 0.35 // Ranged projectiles tend to have a higher material cost, so ease up on the spreading. + ai_aggressiveness = 40 + attack_message = "The blob stabs you" + attack_message_living = ", and you feel sharp spines pierce your flesh" + attack_message_synth = ", and your external plating is pierced by sharp spines" + attack_verb = "stabs" + +// Even if the melee attack is enough to one-shot this blob, it gets to retaliate at least once. +/datum/blob_type/reactive_spines/on_received_damage(var/obj/structure/blob/B, damage, damage_type, mob/living/attacker) + if(damage > 0 && attacker && get_dist(B, attacker) <= 1) + B.visible_message("The [name] retaliates, lashing out at \the [attacker]!") + B.blob_attack_animation(attacker, B.overmind) + attacker.blob_act(B) + ..() + + +// Spreads damage to nearby blobs, and attacks with the force of all nearby blobs. +/datum/blob_type/synchronous_mesh + name = "synchronous mesh" + desc = "A mesh that seems strongly interconnected to itself. It moves slowly, but with purpose." + ai_desc = "defensive" + effect_desc = "When damaged, spreads the damage to nearby blobs. When attacking, damage is increased based on how many blobs are near the target. It is resistant to burn damage." + difficulty = BLOB_DIFFICULTY_EASY // Mostly a tank and spank. + color = "#65ADA2" + complementary_color = "#AD6570" + damage_type = BRUTE + damage_lower = 10 + damage_upper = 15 + brute_multiplier = 0.5 + burn_multiplier = 0.2 // Emitters do so much damage that this will likely not matter too much. + spread_modifier = 0.3 // Since the blob spreads damage, it takes awhile to actually kill, so spread is reduced. + ai_aggressiveness = 60 + attack_message = "The mesh synchronously strikes you" + attack_verb = "synchronously strikes" + var/synchronously_attacking = FALSE + +/datum/blob_type/synchronous_mesh/on_attack(obj/structure/blob/B, mob/living/victim) + if(synchronously_attacking) + return + synchronously_attacking = TRUE // To avoid infinite loops. + for(var/obj/structure/blob/C in orange(1, victim)) + if(victim) // Some things delete themselves when dead... + C.blob_attack_animation(victim) + victim.blob_act(C) + synchronously_attacking = FALSE + +/datum/blob_type/synchronous_mesh/on_received_damage(var/obj/structure/blob/B, damage, damage_type) + var/list/blobs_to_hurt = list() // Maximum split is 9, reducing the damage each blob takes to 11.1% but doing that damage to 9 blobs. + for(var/obj/structure/blob/C in range(1, B)) + if(!istype(C, /obj/structure/blob/core) && !istype(C, /obj/structure/blob/node) && C.overmind && (C.overmind == B.overmind) ) //if it doesn't have the same 'ownership' or is a core or node, don't split damage to it + blobs_to_hurt += C + + for(var/thing in blobs_to_hurt) + var/obj/structure/blob/C = thing + if(C == B) + continue // We'll damage this later. + + C.adjust_integrity(-(damage / blobs_to_hurt.len)) + + return damage / max(blobs_to_hurt.len, 1) // To hurt the blob that got hit. + + +/datum/blob_type/shifting_fragments + name = "shifting fragments" + desc = "A collection of fragments that seem to shuffle around constantly." + ai_desc = "evasive" + effect_desc = "Swaps places with nearby blobs when hit or when expanding." + difficulty = BLOB_DIFFICULTY_EASY + color = "#C8963C" + complementary_color = "#3C6EC8" + damage_type = BRUTE + damage_lower = 20 + damage_upper = 30 + brute_multiplier = 0.5 + burn_multiplier = 0.5 + spread_modifier = 0.5 + ai_aggressiveness = 30 + attack_message = "A fragment strikes you" + attack_verb = "strikes" + +/datum/blob_type/shifting_fragments/on_received_damage(var/obj/structure/blob/B, damage, damage_type) + if(damage > 0 && prob(60)) + var/list/available_blobs = list() + for(var/obj/structure/blob/OB in orange(1, B)) + if((istype(OB, /obj/structure/blob/normal) || (istype(OB, /obj/structure/blob/shield) && prob(25))) && OB.overmind && OB.overmind == B.overmind) + available_blobs += OB + if(available_blobs.len) + var/obj/structure/blob/targeted = pick(available_blobs) + var/turf/T = get_turf(targeted) + targeted.forceMove(get_turf(B)) + B.forceMove(T) // Swap places. + return ..() + +/datum/blob_type/shifting_fragments/on_expand(var/obj/structure/blob/B, var/obj/structure/blob/new_B, var/turf/T, var/mob/observer/blob/O) + if(istype(B, /obj/structure/blob/normal) || (istype(B, /obj/structure/blob/shield) && prob(25))) + new_B.forceMove(get_turf(B)) + B.forceMove(T) + +// A very cool blob, literally. +/datum/blob_type/cryogenic_goo + name = "cryogenic goo" + desc = "A mass of goo that freezes anything it touches." + ai_desc = "balanced" + effect_desc = "Lowers the temperature of the room passively, and will also greatly lower the temperature of anything it attacks." + difficulty = BLOB_DIFFICULTY_MEDIUM + color = "#8BA6E9" + complementary_color = "#7D6EB4" + damage_type = BURN + damage_lower = 15 + damage_upper = 25 + brute_multiplier = 0.25 + burn_multiplier = 1.2 + spread_modifier = 0.5 + ai_aggressiveness = 50 + attack_message = "The goo stabs you" + attack_message_living = ", and you feel an intense chill from within" + attack_message_synth = ", and your system reports lower internal temperatures" + attack_verb = "stabs" + +/datum/blob_type/cryogenic_goo/on_attack(obj/structure/blob/B, mob/living/victim) + if(ishuman(victim)) + var/mob/living/carbon/human/H = victim + var/protection = H.get_cold_protection(50) + if(protection < 1) + var/temp_change = 80 // Each hit can reduce temperature by up to 80 kelvin. + var/datum/species/baseline = all_species["Human"] + var/temp_cap = baseline.cold_level_3 - 5 // Can't go lower than this. + + var/cold_factor = abs(protection - 1) + temp_change *= cold_factor // If protection was at 0.5, then they only lose 40 kelvin. + + H.bodytemperature = max(H.bodytemperature - temp_change, temp_cap) + else // Just do some extra burn for mobs who don't process bodytemp + victim.adjustFireLoss(20) + +/datum/blob_type/cryogenic_goo/on_pulse(var/obj/structure/blob/B) + var/turf/simulated/T = get_turf(B) + if(!istype(T)) + return + T.freeze_floor() + var/datum/gas_mixture/env = T.return_air() + if(env) + env.add_thermal_energy(-10 * 1000) + +// Electric blob that stuns. +/datum/blob_type/energized_jelly + name = "energized jelly" + desc = "A substance that seems to generate electricity." + ai_desc = "suppressive" + effect_desc = "When attacking an entity, it will shock them with a strong electric shock. Repeated attacks can stun the target." + difficulty = BLOB_DIFFICULTY_MEDIUM + color = "#EFD65A" + complementary_color = "#00E5B1" + damage_type = BURN + damage_lower = 5 + damage_upper = 10 + brute_multiplier = 0.5 + burn_multiplier = 0.5 + spread_modifier = 0.35 + ai_aggressiveness = 80 + attack_message = "The jelly prods you" + attack_message_living = ", and your flesh burns as electricity arcs into you" + attack_message_synth = ", and your internal circuity is overloaded as electricity arcs into you" + attack_verb = "prods" + +/datum/blob_type/energized_jelly/on_attack(obj/structure/blob/B, mob/living/victim, def_zone) + victim.electrocute_act(10, src, 1, def_zone) + victim.stun_effect_act(0, 40, BP_TORSO, src) + + +// A blob with area of effect attacks. +/datum/blob_type/explosive_lattice + name = "explosive lattice" + desc = "A very unstable lattice that looks quite explosive." + ai_desc = "aggressive" + effect_desc = "When attacking an entity, it will cause a small explosion, hitting things near the target. It is somewhat resilient, but weaker to brute damage." + difficulty = BLOB_DIFFICULTY_MEDIUM + color = "#8B2500" + complementary_color = "#00668B" + damage_type = BURN + damage_lower = 25 + damage_upper = 35 + armor_check = "bomb" + armor_pen = 5 // This is so blob hits still hurt just slightly when wearing a bomb suit (100 bomb resist). + brute_multiplier = 0.75 + burn_multiplier = 0.5 + spread_modifier = 0.4 + ai_aggressiveness = 75 + attack_message = "The lattice blasts you" + attack_message_living = ", and your flesh burns from the blast wave" + attack_message_synth = ", and your plating burns from the blast wave" + attack_verb = "blasts" + var/exploding = FALSE + +/datum/blob_type/explosive_lattice/on_attack(obj/structure/blob/B, mob/living/victim, def_zone) // This doesn't use actual bombs since they're too strong and it would hurt the blob. + if(exploding) // We're busy, don't infinite loop us. + return + + exploding = TRUE + for(var/mob/living/L in range(get_turf(victim), 1)) // We don't use orange(), in case there is more than one mob on the target tile. + if(L == victim) // Already hit. + continue + if(L.faction == "blob") // No friendly fire + continue + L.blob_act() + + // Visual effect. + var/datum/effect/system/explosion/E = new/datum/effect/system/explosion/smokeless() + var/turf/T = get_turf(victim) + E.set_up(T) + E.start() + + // Now for sounds. + playsound(T, "explosion", 75, 1) // Local sound. + + for(var/mob/M in player_list) // For everyone else. + if(M.z == T.z && get_dist(M, T) > world.view && !M.ear_deaf && !istype(M.loc,/turf/space)) + M << 'sound/effects/explosionfar.ogg' + + exploding = FALSE + + +// A blob that slips and drowns you. +/datum/blob_type/pressurized_slime + name = "pressurized slime" + desc = "A large mass that seems to leak slippery fluid everywhere." + ai_desc = "drowning" + effect_desc = "Wets the floor when expanding and when hit. Tries to drown its enemies when attacking. It forces itself past internals. Resistant to burn damage." + difficulty = BLOB_DIFFICULTY_HARD + color = "#AAAABB" + complementary_color = "#BBBBAA" + damage_type = OXY + damage_lower = 5 + damage_upper = 15 + armor_check = null + brute_multiplier = 0.6 + burn_multiplier = 0.2 + spread_modifier = 0.4 + ai_aggressiveness = 75 + attack_message = "The slime splashes into you" + attack_message_living = ", and you gasp for breath" + attack_message_synth = ", and the fluid wears down on your components" + attack_verb = "splashes" + +/datum/blob_type/pressurized_slime/on_attack(obj/structure/blob/B, mob/living/victim, def_zone) + victim.water_act(5) + var/turf/simulated/T = get_turf(victim) + if(T) + T.wet_floor() + +/datum/blob_type/pressurized_slime/on_received_damage(var/obj/structure/blob/B, damage, damage_type) + wet_surroundings(B, damage) + return ..() + +/datum/blob_type/pressurized_slime/on_pulse(var/obj/structure/blob/B) + var/turf/simulated/T = get_turf(B) + if(!istype(T)) + return + T.wet_floor() + +/datum/blob_type/pressurized_slime/on_death(obj/structure/blob/B) + B.visible_message("The blob ruptures, spraying the area with liquid!") + wet_surroundings(B, 50) + +/datum/blob_type/pressurized_slime/proc/wet_surroundings(var/obj/structure/blob/B, var/probability = 50) + for(var/turf/simulated/T in range(1, B)) + if(prob(probability)) + T.wet_floor() + for(var/atom/movable/AM in T) + AM.water_act(2) + + +// A blob that irradiates everything. +/datum/blob_type/radioactive_ooze + name = "radioactive ooze" + desc = "A goopy mess that glows with an unhealthy aura." + ai_desc = "radical" + effect_desc = "Irradiates the surrounding area, and inflicts toxic attacks. Weak to brute damage." + difficulty = BLOB_DIFFICULTY_MEDIUM + color = "#33CC33" + complementary_color = "#99FF66" + damage_type = TOX + damage_lower = 20 + damage_upper = 30 + armor_check = "rad" + brute_multiplier = 0.75 + burn_multiplier = 0.2 + spread_modifier = 0.8 + ai_aggressiveness = 50 + attack_message = "The ooze splashes you" + attack_message_living = ", and you feel warm" + attack_message_synth = ", and your internal systems are bombarded by ionizing radiation" + attack_verb = "splashes" + +/datum/blob_type/radioactive_ooze/on_pulse(var/obj/structure/blob/B) + radiation_repository.radiate(B, 200) \ No newline at end of file diff --git a/code/modules/events/blob.dm b/code/modules/events/blob.dm index bc1888f14d..ee66bbea9e 100644 --- a/code/modules/events/blob.dm +++ b/code/modules/events/blob.dm @@ -2,24 +2,20 @@ announceWhen = 12 endWhen = 120 - var/obj/effect/blob/core/Blob + var/obj/structure/blob/core/Blob -/datum/event/blob/announce() - level_seven_announcement() /datum/event/blob/start() var/turf/T = pick(blobstart) if(!T) kill() return - Blob = new /obj/effect/blob/core(T) - for(var/i = 1; i < rand(3, 4), i++) - Blob.process() + + Blob = new /obj/structure/blob/core/random_medium(T) + /datum/event/blob/tick() if(!Blob || !Blob.loc) Blob = null kill() return - if(IsMultiple(activeFor, 3)) - Blob.process() diff --git a/code/modules/mob/animations.dm b/code/modules/mob/animations.dm index 221777f56a..5ba954106b 100644 --- a/code/modules/mob/animations.dm +++ b/code/modules/mob/animations.dm @@ -140,7 +140,7 @@ note dizziness decrements automatically in the mob's Life() proc. //reset the pixel offsets to zero is_floating = 0 -/mob/proc/do_attack_animation(mob/M) +/atom/movable/proc/do_attack_animation(mob/M) var/pixel_x_diff = 0 var/pixel_y_diff = 0 diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index b87eac5a1d..b93ed0cb5f 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -239,6 +239,7 @@ default behaviour is: amount *= M.incoming_healing_percent bruteloss = min(max(bruteloss + amount, 0),(getMaxHealth()*2)) + updatehealth() /mob/living/proc/getOxyLoss() return oxyloss @@ -258,6 +259,7 @@ default behaviour is: amount *= M.incoming_healing_percent oxyloss = min(max(oxyloss + amount, 0),(getMaxHealth()*2)) + updatehealth() /mob/living/proc/setOxyLoss(var/amount) if(status_flags & GODMODE) return 0 //godmode @@ -281,6 +283,7 @@ default behaviour is: amount *= M.incoming_healing_percent toxloss = min(max(toxloss + amount, 0),(getMaxHealth()*2)) + updatehealth() /mob/living/proc/setToxLoss(var/amount) if(status_flags & GODMODE) return 0 //godmode @@ -309,6 +312,7 @@ default behaviour is: amount *= M.incoming_healing_percent fireloss = min(max(fireloss + amount, 0),(getMaxHealth()*2)) + updatehealth() /mob/living/proc/getCloneLoss() return cloneloss @@ -328,6 +332,7 @@ default behaviour is: amount *= M.incoming_healing_percent cloneloss = min(max(cloneloss + amount, 0),(getMaxHealth()*2)) + updatehealth() /mob/living/proc/setCloneLoss(var/amount) if(status_flags & GODMODE) return 0 //godmode @@ -362,6 +367,7 @@ default behaviour is: if(!isnull(M.incoming_healing_percent)) amount *= M.incoming_healing_percent halloss = min(max(halloss + amount, 0),(getMaxHealth()*2)) + updatehealth() /mob/living/proc/setHalLoss(var/amount) if(status_flags & GODMODE) return 0 //godmode diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index 9ba864a6cb..61ec68550d 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -162,6 +162,43 @@ O.emp_act(severity) ..() +/mob/living/blob_act(var/obj/structure/blob/B) + if(stat == DEAD) + return + + var/damage = rand(30, 40) + var/armor_pen = 0 + var/armor_check = "melee" + var/damage_type = BRUTE + var/attack_message = "The blob attacks you!" + var/attack_verb = "attacks" + var/def_zone = pick(BP_HEAD, BP_TORSO, BP_GROIN, BP_L_ARM, BP_R_ARM, BP_L_LEG, BP_R_LEG) + + if(B && B.overmind) + var/datum/blob_type/blob = B.overmind.blob_type + + damage = rand(blob.damage_lower, blob.damage_upper) + armor_check = blob.armor_check + armor_pen = blob.armor_pen + damage_type = blob.damage_type + + attack_message = "[blob.attack_message][isSynthetic() ? "[blob.attack_message_synth]":"[blob.attack_message_living]"]" + attack_verb = blob.attack_verb + B.overmind.blob_type.on_attack(B, src, def_zone) + + if( (damage_type == TOX || damage_type == OXY) && isSynthetic()) // Borgs and FBPs don't really handle tox/oxy damage the same way other mobs do. + damage_type = BRUTE + damage *= 0.66 // Take 2/3s as much damage. + + visible_message("\The [B] [attack_verb] \the [src]!", "[attack_message]!") + playsound(loc, 'sound/effects/attackblob.ogg', 50, 1) + + //Armor + var/soaked = get_armor_soak(def_zone, armor_check, armor_pen) + var/absorb = run_armor_check(def_zone, armor_check, armor_pen) + + apply_damage(damage, damage_type, def_zone, absorb, soaked) + /mob/living/proc/resolve_item_attack(obj/item/I, mob/living/user, var/target_zone) return target_zone diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index 716a5d62c2..089f629be5 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -104,8 +104,6 @@ var/cpr_time = 1.0//Carbon var/bodytemperature = 310.055 //98.7 F - var/old_x = 0 - var/old_y = 0 var/drowsyness = 0.0//Carbon var/charges = 0.0 var/nutrition = 400.0//Carbon diff --git a/code/modules/power/apc.dm b/code/modules/power/apc.dm index eb142aac1e..1b0c8c7275 100644 --- a/code/modules/power/apc.dm +++ b/code/modules/power/apc.dm @@ -690,6 +690,12 @@ to_chat(user,"The APC interface refused to unlock.") return 1 +/obj/machinery/power/apc/blob_act() + if(!wires.IsAllCut()) + wiresexposed = TRUE + wires.CutAll() + update_icon() + /obj/machinery/power/apc/attack_hand(mob/user) // if (!can_use(user)) This already gets called in interact() and in topic() // return diff --git a/code/modules/power/lighting.dm b/code/modules/power/lighting.dm index 216332cbdd..59d7560a77 100644 --- a/code/modules/power/lighting.dm +++ b/code/modules/power/lighting.dm @@ -329,6 +329,9 @@ broken() return 1 +/obj/machinery/light/blob_act() + broken() + // attempt to set the light's on/off status // will not switch on if broken/burned/empty /obj/machinery/light/proc/seton(var/s) diff --git a/code/modules/power/singularity/emitter.dm b/code/modules/power/singularity/emitter.dm index f0af4f7312..e5dead7ee5 100644 --- a/code/modules/power/singularity/emitter.dm +++ b/code/modules/power/singularity/emitter.dm @@ -250,14 +250,21 @@ if(!P || !P.damage || P.get_structure_damage() <= 0 ) return - integrity = integrity - P.damage - if(integrity <= 0) + adjust_integrity(-P.get_structure_damage()) + +/obj/machinery/power/emitter/blob_act() + adjust_integrity(-1000) // This kills the emitter. + +/obj/machinery/power/emitter/proc/adjust_integrity(amount) + integrity = between(0, integrity + amount, initial(integrity)) + if(integrity == 0) if(powernet && avail(active_power_usage)) // If it's powered, it goes boom if killed. visible_message(src, "\The [src] explodes violently!", "You hear an explosion!") explosion(get_turf(src), 1, 2, 4) else src.visible_message("\The [src] crumples apart!", "You hear metal collapsing.") - qdel(src) + if(src) + qdel(src) /obj/machinery/power/emitter/examine(mob/user) ..() diff --git a/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Core.dm b/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Core.dm index 99972f4708..9fedfc481b 100644 --- a/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Core.dm +++ b/code/modules/reagents/Chemistry-Reagents/Chemistry-Reagents-Core.dm @@ -124,11 +124,13 @@ else if(volume >= 10) T.wet_floor(1) -/datum/reagent/water/touch_obj(var/obj/O) +/datum/reagent/water/touch_obj(var/obj/O, var/amount) if(istype(O, /obj/item/weapon/reagent_containers/food/snacks/monkeycube)) var/obj/item/weapon/reagent_containers/food/snacks/monkeycube/cube = O if(!cube.wrapped) cube.Expand() + else + O.water_act(amount / 5) /datum/reagent/water/touch_mob(var/mob/living/L, var/amount) if(istype(L)) diff --git a/code/modules/reagents/reagent_containers/spray.dm b/code/modules/reagents/reagent_containers/spray.dm index c1c2597daf..136bb34026 100644 --- a/code/modules/reagents/reagent_containers/spray.dm +++ b/code/modules/reagents/reagent_containers/spray.dm @@ -198,12 +198,4 @@ /obj/item/weapon/reagent_containers/spray/plantbgone/New() ..() - reagents.add_reagent("plantbgone", 100) - -/obj/item/weapon/reagent_containers/spray/plantbgone/afterattack(atom/A as mob|obj, mob/user as mob, proximity) - if(!proximity) return - - if(istype(A, /obj/effect/blob)) // blob damage in blob code - return - - ..() \ No newline at end of file + reagents.add_reagent("plantbgone", 100) \ No newline at end of file diff --git a/code/modules/reagents/reagent_dispenser.dm b/code/modules/reagents/reagent_dispenser.dm index 6312d08d84..17b1786449 100644 --- a/code/modules/reagents/reagent_dispenser.dm +++ b/code/modules/reagents/reagent_dispenser.dm @@ -59,7 +59,8 @@ else return - +/obj/structure/reagent_dispensers/blob_act() + qdel(src) @@ -153,6 +154,9 @@ /obj/structure/reagent_dispensers/fueltank/ex_act() explode() +/obj/structure/reagent_dispensers/fueltank/blob_act() + explode() + /obj/structure/reagent_dispensers/fueltank/proc/explode() if (reagents.total_volume > 500) explosion(src.loc,1,2,4) diff --git a/code/modules/tables/tables.dm b/code/modules/tables/tables.dm index 03382bffb2..13fe755751 100644 --- a/code/modules/tables/tables.dm +++ b/code/modules/tables/tables.dm @@ -54,6 +54,9 @@ visible_message("\The [src] breaks down!") return break_to_parts() // if we break and form shards, return them to the caller to do !FUN! things with +/obj/structure/table/blob_act() + take_damage(100) + /obj/structure/table/initialize() ..() diff --git a/icons/effects/effects.dmi b/icons/effects/effects.dmi index e5d15cbfbc..b96d107773 100644 Binary files a/icons/effects/effects.dmi and b/icons/effects/effects.dmi differ diff --git a/icons/mob/blob.dmi b/icons/mob/blob.dmi index 49d49a1088..86d961c4c8 100644 Binary files a/icons/mob/blob.dmi and b/icons/mob/blob.dmi differ diff --git a/polaris.dme b/polaris.dme index 5984d65873..9cafc32d4d 100644 --- a/polaris.dme +++ b/polaris.dme @@ -186,6 +186,7 @@ #include "code\datums\mind.dm" #include "code\datums\mixed.dm" #include "code\datums\modules.dm" +#include "code\datums\mutable_appearance.dm" #include "code\datums\organs.dm" #include "code\datums\progressbar.dm" #include "code\datums\recipe.dm" @@ -1220,7 +1221,20 @@ #include "code\modules\awaymissions\pamphlet.dm" #include "code\modules\awaymissions\trigger.dm" #include "code\modules\awaymissions\zlevel.dm" -#include "code\modules\blob\blob.dm" +#include "code\modules\blob2\_defines.dm" +#include "code\modules\blob2\announcement.dm" +#include "code\modules\blob2\blobs\base_blob.dm" +#include "code\modules\blob2\blobs\core.dm" +#include "code\modules\blob2\blobs\factory.dm" +#include "code\modules\blob2\blobs\node.dm" +#include "code\modules\blob2\blobs\normal.dm" +#include "code\modules\blob2\blobs\resource.dm" +#include "code\modules\blob2\blobs\shield.dm" +#include "code\modules\blob2\mobs\blob_mob.dm" +#include "code\modules\blob2\mobs\spore.dm" +#include "code\modules\blob2\overmind\overmind.dm" +#include "code\modules\blob2\overmind\powers.dm" +#include "code\modules\blob2\overmind\types.dm" #include "code\modules\busy_space\air_traffic.dm" #include "code\modules\busy_space\loremaster.dm" #include "code\modules\busy_space\organizations.dm" diff --git a/sound/weapons/tap.ogg b/sound/weapons/tap.ogg new file mode 100644 index 0000000000..4cb31e9148 Binary files /dev/null and b/sound/weapons/tap.ogg differ