diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 3b0904b2abc..02027ec957e 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -102,6 +102,8 @@
#define COMSIG_ATOM_USED_IN_CRAFT "atom_used_in_craft"
///from base of atom/blob_act(): (/obj/structure/blob)
#define COMSIG_ATOM_BLOB_ACT "atom_blob_act"
+ /// if returned, forces nothing to happen when the atom is attacked by a blob
+ #define COMPONENT_CANCEL_BLOB_ACT (1<<0)
///from base of atom/acid_act(): (acidpwr, acid_volume)
#define COMSIG_ATOM_ACID_ACT "atom_acid_act"
///from base of atom/emag_act(): (/mob/user)
@@ -112,7 +114,7 @@
#define COMSIG_ATOM_NARSIE_ACT "atom_narsie_act"
///from base of atom/rcd_act(): (/mob, /obj/item/construction/rcd, passed_mode)
#define COMSIG_ATOM_RCD_ACT "atom_rcd_act"
-///from base of atom/singularity_pull(): (S, current_size)
+///from base of atom/singularity_pull(): (/datum/component/singularity, current_size)
#define COMSIG_ATOM_SING_PULL "atom_sing_pull"
///from obj/machinery/bsa/full/proc/fire(): ()
#define COMSIG_ATOM_BSA_BEAM "atom_bsa_beam_pass"
@@ -193,6 +195,12 @@
///called on /living when someone starts pulling it (atom/movable/puller, state, force)
#define COMSIG_LIVING_START_PULL "living_start_pull"
+/// from /datum/component/singularity/proc/can_move(), as well as /obj/energy_ball/proc/can_move()
+/// if a callback returns `SINGULARITY_TRY_MOVE_BLOCK`, then the singularity will not move to that turf
+#define COMSIG_ATOM_SINGULARITY_TRY_MOVE "atom_singularity_try_move"
+ /// When returned from `COMSIG_ATOM_SINGULARITY_TRY_MOVE`, the singularity will move to that turf
+ #define SINGULARITY_TRY_MOVE_BLOCK (1 << 0)
+
/////////////////
///from base of area/Entered(): (/area)
diff --git a/code/__DEFINES/flags.dm b/code/__DEFINES/flags.dm
index dbc805084fe..04b67226664 100644
--- a/code/__DEFINES/flags.dm
+++ b/code/__DEFINES/flags.dm
@@ -48,6 +48,8 @@ GLOBAL_LIST_INIT(bitflags, list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 204
#define UNPAINTABLE_1 (1 << 20)
/// Is the thing currently spinning?
#define IS_SPINNING_1 (1 << 21)
+/// Does the supermatter skip over this atom?
+#define SUPERMATTER_IGNORES_1 (1 << 22)
/// If the thing can reflect light (lasers/energy)
#define RICOCHET_SHINY (1<<0)
diff --git a/code/_globalvars/game_modes.dm b/code/_globalvars/game_modes.dm
index 301a3bfacf8..79d4a6f47b1 100644
--- a/code/_globalvars/game_modes.dm
+++ b/code/_globalvars/game_modes.dm
@@ -9,7 +9,7 @@ GLOBAL_DATUM(start_state, /datum/station_state) // Used in round-end report
//TODO clear this one up too
-GLOBAL_DATUM(cult_narsie, /obj/singularity/narsie/large/cult)
+GLOBAL_DATUM(cult_narsie, /obj/narsie)
///We want reality_smash_tracker to exist only once and be accesable from anywhere.
GLOBAL_DATUM_INIT(reality_smash_track, /datum/reality_smash_tracker, new)
diff --git a/code/_globalvars/lists/objects.dm b/code/_globalvars/lists/objects.dm
index 7d8cbc17293..cf291490aa5 100644
--- a/code/_globalvars/lists/objects.dm
+++ b/code/_globalvars/lists/objects.dm
@@ -13,7 +13,7 @@ GLOBAL_LIST_EMPTY(deliverybeacontags) //list of all tags associated with d
GLOBAL_LIST_EMPTY(wayfindingbeacons) //list of all navigation beacons used by wayfinding pinpointers
GLOBAL_LIST_EMPTY(nuke_list)
GLOBAL_LIST_EMPTY(alarmdisplay) //list of all machines or programs that can display station alerts
-GLOBAL_LIST_EMPTY(singularities) //list of all singularities on the station (actually technically all engines)
+GLOBAL_LIST_EMPTY_TYPED(singularities, /datum/component/singularity) //list of all singularities on the station
GLOBAL_LIST_EMPTY(mechpad_list) //list of all /obj/machinery/mechpad
GLOBAL_LIST(chemical_reactions_list) //list of all /datum/chemical_reaction datums. Used during chemical reactions
diff --git a/code/controllers/subsystem/traumas.dm b/code/controllers/subsystem/traumas.dm
index 33d4831fe73..d35ff4b2822 100644
--- a/code/controllers/subsystem/traumas.dm
+++ b/code/controllers/subsystem/traumas.dm
@@ -130,7 +130,7 @@ SUBSYSTEM_DEF(traumas)
/obj/item/melee/cultblade, /obj/item/cult_bastard,
/obj/item/restraints/legcuffs/bola/cult, /obj/item/clothing/suit/space/hardsuit/cult,
/obj/item/clothing/suit/hooded/cultrobes, /obj/item/clothing/head/hooded/cult_hoodie, /obj/effect/rune,
- /obj/item/stack/sheet/runed_metal, /obj/machinery/door/airlock/cult, /obj/singularity/narsie,
+ /obj/item/stack/sheet/runed_metal, /obj/machinery/door/airlock/cult, /obj/narsie,
/obj/item/soulstone,
/obj/item/clothing/suit/wizrobe, /obj/item/clothing/head/wizard, /obj/item/spellbook, /obj/item/staff,
/obj/item/clothing/suit/space/hardsuit/shielded/wizard, /obj/item/clothing/suit/space/hardsuit/wizard,
diff --git a/code/datums/components/chasm.dm b/code/datums/components/chasm.dm
index 9d7b180b813..e0a96bc6217 100644
--- a/code/datums/components/chasm.dm
+++ b/code/datums/components/chasm.dm
@@ -7,6 +7,8 @@
var/static/list/falling_atoms = list() // Atoms currently falling into chasms
var/static/list/forbidden_types = typecacheof(list(
/obj/singularity,
+ /obj/energy_ball,
+ /obj/narsie,
/obj/docking_port,
/obj/structure/lattice,
/obj/structure/stone_tile,
diff --git a/code/datums/components/singularity.dm b/code/datums/components/singularity.dm
new file mode 100644
index 00000000000..80f32f295c0
--- /dev/null
+++ b/code/datums/components/singularity.dm
@@ -0,0 +1,294 @@
+/// The range at which a singularity is considered "contained" to admins
+#define FIELD_CONTAINMENT_DISTANCE 30
+
+/// What's the chance that, when a singularity moves, it'll go to its target?
+#define CHANCE_TO_MOVE_TO_TARGET 60
+
+/// Things that maybe move around and does stuff to things around them
+/// Used for the singularity (duh) and Nar'Sie
+/datum/component/singularity
+ /// Callback for consuming objects (for example, Nar'Sie replaces this to call narsie_act)
+ var/datum/callback/consume_callback
+
+ /// The range to pull in stuff around it
+ var/consume_range
+
+ /// Does this singularity move?
+ var/roaming
+
+ /// The chosen direction to drift in
+ var/drifting_dir
+
+ /// How many tiles out to pull in
+ var/grav_pull
+
+ /// The last direction we failed to move in (for example: if we are contained)
+ var/last_failed_movement
+
+ /// How big is the singularity?
+ var/singularity_size
+
+ /// Should we disregard the possibility of failed movements? Used by stage five singularities
+ var/disregard_failed_movements
+
+ /// Can this singularity be BSA'd?
+ var/bsa_targetable
+
+ /// Should the admins be alerted when this is created?
+ var/notify_admins
+
+ /// If specified, the singularity will slowly move to this target
+ var/atom/target
+
+/datum/component/singularity/Initialize(
+ bsa_targetable = TRUE,
+ consume_range = 0,
+ consume_callback = CALLBACK(src, .proc/default_singularity_act),
+ disregard_failed_movements = FALSE,
+ grav_pull = 4,
+ notify_admins = TRUE,
+ singularity_size = STAGE_ONE,
+ roaming = TRUE,
+)
+ if (!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.bsa_targetable = bsa_targetable
+ src.consume_callback = consume_callback
+ src.consume_range = consume_range
+ src.disregard_failed_movements = disregard_failed_movements
+ src.grav_pull = grav_pull
+ src.notify_admins = notify_admins
+ src.roaming = roaming
+ src.singularity_size = singularity_size
+
+/datum/component/singularity/RegisterWithParent()
+ START_PROCESSING(SSdcs, src)
+
+ // The singularity stops drifting for no man!
+ parent.AddElement(/datum/element/forced_gravity, FALSE)
+
+ parent.AddElement(/datum/element/bsa_blocker)
+ RegisterSignal(parent, COMSIG_ATOM_BSA_BEAM, .proc/bluespace_reaction)
+
+ RegisterSignal(parent, COMSIG_ATOM_BLOB_ACT, .proc/block_blob)
+
+ RegisterSignal(parent, list(
+ COMSIG_ATOM_ATTACK_ANIMAL,
+ COMSIG_ATOM_ATTACK_HAND,
+ COMSIG_ATOM_ATTACK_PAW,
+ ), .proc/consume_attack)
+ RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/consume_attackby)
+
+ RegisterSignal(parent, COMSIG_MOVABLE_PRE_MOVE, .proc/moved)
+ RegisterSignal(parent, list(COMSIG_ATOM_BUMPED, COMSIG_MOVABLE_CROSSED), .proc/consume)
+
+ RegisterSignal(parent, COMSIG_ATOM_BULLET_ACT, .proc/consume_bullets)
+
+ if (notify_admins)
+ admin_investigate_setup()
+
+ GLOB.singularities |= src
+
+/datum/component/singularity/Destroy(force, silent)
+ GLOB.singularities -= src
+ QDEL_NULL(consume_callback)
+ target = null
+
+ return ..()
+
+/datum/component/singularity/UnregisterFromParent()
+ STOP_PROCESSING(SSdcs, src)
+
+ parent.RemoveElement(/datum/element/bsa_blocker)
+ parent.RemoveElement(/datum/element/forced_gravity)
+
+ UnregisterSignal(parent, list(
+ COMSIG_ATOM_ATTACK_ANIMAL,
+ COMSIG_ATOM_ATTACK_HAND,
+ COMSIG_ATOM_ATTACK_PAW,
+ COMSIG_ATOM_BLOB_ACT,
+ COMSIG_ATOM_BSA_BEAM,
+ COMSIG_ATOM_BULLET_ACT,
+ COMSIG_ATOM_BUMPED,
+ COMSIG_MOVABLE_CROSSED,
+ COMSIG_MOVABLE_PRE_MOVE,
+ COMSIG_PARENT_ATTACKBY,
+ ))
+
+/datum/component/singularity/process(delta_time)
+ if (roaming)
+ move()
+ eat()
+
+/datum/component/singularity/proc/block_blob()
+ SIGNAL_HANDLER
+
+ return COMPONENT_CANCEL_BLOB_ACT
+
+/datum/component/singularity/proc/consume(datum/source, atom/thing)
+ consume_callback?.Invoke(thing, src)
+
+/datum/component/singularity/proc/consume_attack(datum/source, mob/user)
+ SIGNAL_HANDLER
+
+ consume(source, user)
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/datum/component/singularity/proc/consume_attackby(datum/source, obj/item/item, mob/user)
+ SIGNAL_HANDLER
+
+ consume(source, user)
+
+// Will there be an impact? Who knows. Will we see it? No.
+/datum/component/singularity/proc/consume_bullets(obj/projectile/projectile)
+ SIGNAL_HANDLER
+
+ qdel(projectile)
+
+/// Calls singularity_act on the thing passed, usually destroying the object
+/datum/component/singularity/proc/default_singularity_act(atom/thing)
+ thing.singularity_act(singularity_size, parent)
+
+/datum/component/singularity/proc/eat()
+ var/atom/atom_parent = parent
+
+ for (var/_tile in spiral_range_turfs(grav_pull, parent))
+ var/turf/tile = _tile
+ if (!tile || !isturf(atom_parent.loc))
+ continue
+ if (get_dist(tile, parent) > consume_range)
+ tile.singularity_pull(src, singularity_size)
+ else
+ consume(src, tile)
+
+ for (var/_thing in tile)
+ var/atom/thing = _thing
+
+ // Because we can possibly yield in the middle of iteration, let's make sure what were looking at is still there
+ // Without this, you get "Qdeleted thing being thrown around"
+ if (QDELETED(thing))
+ continue
+
+ if (isturf(atom_parent.loc) && thing != parent)
+ var/atom/movable/movable_thing = thing
+ if (get_dist(movable_thing, parent) > consume_range)
+ movable_thing.singularity_pull(parent, singularity_size)
+ else
+ consume(src, movable_thing)
+
+ CHECK_TICK
+
+/datum/component/singularity/proc/move()
+ var/drifting_dir = pick(GLOB.alldirs - last_failed_movement)
+
+ if (!QDELETED(target) && prob(CHANCE_TO_MOVE_TO_TARGET))
+ drifting_dir = get_dir(parent, target)
+
+ step(parent, drifting_dir)
+
+/datum/component/singularity/proc/moved(datum/source, atom/new_location)
+ SIGNAL_HANDLER
+
+ var/atom/atom_parent = parent
+ var/current_direction = atom_parent.dir
+ var/turf/current_turf = get_turf(parent)
+
+ for(var/dir in GLOB.cardinals)
+ if(current_direction & dir)
+ current_turf = get_step(current_turf, dir)
+ if(!current_turf)
+ break
+ // eat the stuff if we're going to move into it so it doesn't mess up our movement
+ for(var/atom/thing_on_turf in current_turf.contents)
+ consume(src, thing_on_turf)
+ consume(src, current_turf)
+
+ if(disregard_failed_movements || check_turfs_in(current_direction))
+ last_failed_movement = null
+ else
+ last_failed_movement = current_direction
+ return COMPONENT_MOVABLE_BLOCK_PRE_MOVE
+
+/datum/component/singularity/proc/can_move(turf/to_move)
+ if (!to_move)
+ return FALSE
+
+ for (var/_thing in to_move)
+ var/atom/thing = _thing
+ if (SEND_SIGNAL(thing, COMSIG_ATOM_SINGULARITY_TRY_MOVE) & SINGULARITY_TRY_MOVE_BLOCK)
+ return FALSE
+
+ return TRUE
+
+/// Makes sure we don't move out of the z-level by checking the turfs around us.
+/// Takes in the direction we're going, and optionally how many steps forward to look.
+/// If steps are not provided, it will be inferred by singularity_size.
+/datum/component/singularity/proc/check_turfs_in(direction, steps)
+ if (!direction)
+ return FALSE
+ var/atom/atom_parent = parent
+ if (!steps)
+ switch (singularity_size)
+ if (STAGE_ONE)
+ steps = 1
+ if (STAGE_TWO)
+ steps = 3//Yes this is right
+ if (STAGE_THREE)
+ steps = 3
+ if (STAGE_FOUR)
+ steps = 4
+ if (STAGE_FIVE)
+ steps = 5
+ var/list/turfs = list()
+ var/turf/farthest_turf = atom_parent.loc
+ for (var/_ = 1 to steps)
+ farthest_turf = get_step(farthest_turf, direction)
+ if (!isturf(farthest_turf))
+ return FALSE
+ turfs.Add(farthest_turf)
+ var/dir2
+ var/dir3
+ switch (direction)
+ if (NORTH || SOUTH)
+ dir2 = EAST
+ dir3 = WEST
+ if (EAST || WEST)
+ dir2 = NORTH
+ dir3 = SOUTH
+ var/turf/farthest_perpendicular_turf = farthest_turf
+ for (var/_ = 1 to steps - 1)
+ farthest_perpendicular_turf = get_step(farthest_perpendicular_turf, dir2)
+ if (!isturf(farthest_perpendicular_turf))
+ return FALSE
+ turfs.Add(farthest_perpendicular_turf)
+ for (var/_ = 1 to steps - 1)
+ farthest_turf = get_step(farthest_turf, dir3)
+ if (!isturf(farthest_turf))
+ return FALSE
+ turfs.Add(farthest_turf)
+ for (var/turf_in_range in turfs)
+ if (isnull(turf_in_range))
+ continue
+ if (!can_move(turf_in_range))
+ return FALSE
+ return TRUE
+
+/// Logs to admins that a singularity was created
+/datum/component/singularity/proc/admin_investigate_setup()
+ var/turf/spawned_turf = get_turf(parent)
+ message_admins("A singulo has been created at [ADMIN_VERBOSEJMP(spawned_turf)].")
+ var/atom/atom_parent = parent
+ atom_parent.investigate_log("was made a singularity at [AREACOORD(spawned_turf)].", INVESTIGATE_SINGULO)
+
+/// Fired when the singularity is fired at with the BSA and deletes it
+/datum/component/singularity/proc/bluespace_reaction()
+ if (!bsa_targetable)
+ return
+
+ var/atom/atom_parent = parent
+ atom_parent.investigate_log("has been shot by bluespace artillery and destroyed.", INVESTIGATE_SINGULO)
+ qdel(parent)
+
+#undef CHANCE_TO_MOVE_TO_TARGET
+#undef FIELD_CONTAINMENT_DISTANCE
diff --git a/code/datums/components/storage/concrete/bag_of_holding.dm b/code/datums/components/storage/concrete/bag_of_holding.dm
index 9edbe00e9ea..b91fc726689 100644
--- a/code/datums/components/storage/concrete/bag_of_holding.dm
+++ b/code/datums/components/storage/concrete/bag_of_holding.dm
@@ -23,5 +23,5 @@
log_game("[key_name(user)] detonated a bag of holding at [loc_name(loccheck)].")
user.gib(TRUE, TRUE, TRUE)
- new/obj/singularity/boh_tear(loccheck)
+ new/obj/boh_tear(loccheck)
qdel(A)
diff --git a/code/datums/radiation_wave.dm b/code/datums/radiation_wave.dm
index 87b2e2e14b5..74319aa3dd1 100644
--- a/code/datums/radiation_wave.dm
+++ b/code/datums/radiation_wave.dm
@@ -120,7 +120,9 @@
/obj/machinery/atmospherics,
/obj/item/ammo_casing,
/obj/item/implant,
- /obj/singularity
+ /obj/singularity,
+ /obj/energy_ball,
+ /obj/narsie,
))
if(!can_contaminate || !can_contam || blacklisted[thing.type])
continue
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 90aa43059fc..4c4320fc1c7 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -682,8 +682,10 @@
* default behaviour is to send the [COMSIG_ATOM_BLOB_ACT] signal
*/
/atom/proc/blob_act(obj/structure/blob/B)
- SEND_SIGNAL(src, COMSIG_ATOM_BLOB_ACT, B)
- return
+ var/blob_act_result = SEND_SIGNAL(src, COMSIG_ATOM_BLOB_ACT, B)
+ if (blob_act_result & COMPONENT_CANCEL_BLOB_ACT)
+ return FALSE
+ return TRUE
/atom/proc/fire_act(exposed_temperature, exposed_volume)
SEND_SIGNAL(src, COMSIG_ATOM_FIRE_ACT, exposed_temperature, exposed_volume)
diff --git a/code/game/machinery/shieldgen.dm b/code/game/machinery/shieldgen.dm
index 25c77fa4e40..ed25027a062 100644
--- a/code/game/machinery/shieldgen.dm
+++ b/code/game/machinery/shieldgen.dm
@@ -278,6 +278,7 @@
. = ..()
if(anchored)
connect_to_network()
+ RegisterSignal(src, COMSIG_ATOM_SINGULARITY_TRY_MOVE, .proc/block_singularity_if_active)
/obj/machinery/power/shieldwallgen/Destroy()
for(var/d in GLOB.cardinals)
@@ -364,6 +365,12 @@
if(F && (F.gen_primary == src || F.gen_secondary == src)) //it's ours, kill it.
qdel(F)
+/obj/machinery/power/shieldwallgen/proc/block_singularity_if_active()
+ SIGNAL_HANDLER
+
+ if (active)
+ return SINGULARITY_TRY_MOVE_BLOCK
+
/obj/machinery/power/shieldwallgen/can_be_unfasten_wrench(mob/user, silent)
if(active)
if(!silent)
@@ -455,6 +462,7 @@
for(var/mob/living/L in get_turf(src))
visible_message("\The [src] is suddenly occupying the same space as \the [L]!")
L.gib()
+ RegisterSignal(src, COMSIG_ATOM_SINGULARITY_TRY_MOVE, .proc/block_singularity)
/obj/machinery/shieldwall/Destroy()
gen_primary = null
@@ -489,6 +497,11 @@
if(gen_secondary) //using power may cause us to be destroyed
gen_secondary.add_load(drain_amount * 0.5)
+/obj/machinery/shieldwall/proc/block_singularity()
+ SIGNAL_HANDLER
+
+ return SINGULARITY_TRY_MOVE_BLOCK
+
/obj/machinery/shieldwall/CanAllowThrough(atom/movable/mover, turf/target)
. = ..()
if(istype(mover) && (mover.pass_flags & PASSGLASS))
diff --git a/code/game/machinery/syndicatebeacon.dm b/code/game/machinery/syndicatebeacon.dm
index 78c52b44a4f..51653beb30f 100644
--- a/code/game/machinery/syndicatebeacon.dm
+++ b/code/game/machinery/syndicatebeacon.dm
@@ -22,8 +22,10 @@
if(user)
to_chat(user, "The connected wire doesn't have enough current.")
return
- for(var/obj/singularity/singulo in GLOB.singularities)
- if(singulo.z == z)
+ for (var/_singulo in GLOB.singularities)
+ var/datum/component/singularity/singulo = _singulo
+ var/atom/singulo_atom = singulo.parent
+ if(singulo_atom.z == z)
singulo.target = src
icon_state = "[icontype]1"
active = TRUE
@@ -32,7 +34,8 @@
/obj/machinery/power/singularity_beacon/proc/Deactivate(mob/user = null)
- for(var/obj/singularity/singulo in GLOB.singularities)
+ for(var/_singulo in GLOB.singularities)
+ var/datum/component/singularity/singulo = _singulo
if(singulo.target == src)
singulo.target = null
icon_state = "[icontype]0"
@@ -93,7 +96,9 @@
add_load(1500)
if(cooldown <= world.time)
cooldown = world.time + 80
- for(var/obj/singularity/singulo in GLOB.singularities)
+ for(var/_singulo_component in GLOB.singularities)
+ var/datum/component/singularity/singulo_component = _singulo_component
+ var/atom/singulo = singulo_component.parent
if(singulo.z == z)
say("[singulo] is now [get_dist(src,singulo)] standard lengths away to the [dir2text(get_dir(src,singulo))]")
else
diff --git a/code/game/objects/items/melee/misc.dm b/code/game/objects/items/melee/misc.dm
index e19044b973a..6365ef7a56a 100644
--- a/code/game/objects/items/melee/misc.dm
+++ b/code/game/objects/items/melee/misc.dm
@@ -570,7 +570,7 @@
/obj/item/melee/roastingstick/Initialize()
. = ..()
if (!ovens)
- ovens = typecacheof(list(/obj/singularity, /obj/machinery/power/supermatter_crystal, /obj/structure/bonfire))
+ ovens = typecacheof(list(/obj/singularity, /obj/energy_ball, /obj/machinery/power/supermatter_crystal, /obj/structure/bonfire))
/obj/item/melee/roastingstick/attack_self(mob/user)
on = !on
diff --git a/code/game/objects/obj_defense.dm b/code/game/objects/obj_defense.dm
index d1242c05505..1dc1a752733 100644
--- a/code/game/objects/obj_defense.dm
+++ b/code/game/objects/obj_defense.dm
@@ -94,6 +94,8 @@
return TRUE
/obj/blob_act(obj/structure/blob/B)
+ if (!..())
+ return
if(isturf(loc))
var/turf/T = loc
if(T.intact && HAS_TRAIT(src, TRAIT_T_RAY_VISIBLE))
diff --git a/code/modules/antagonists/cult/cult_items.dm b/code/modules/antagonists/cult/cult_items.dm
index e59339437bf..7147c920049 100644
--- a/code/modules/antagonists/cult/cult_items.dm
+++ b/code/modules/antagonists/cult/cult_items.dm
@@ -477,7 +477,7 @@
to_chat(user, "You try to shatter the orb, but it remains as solid as a rock!")
to_chat(user, "It seems that the blood cult has exhausted its ability to curse the emergency escape shuttle. It would be unwise to create more cursed orbs or to continue to try to shatter this one.")
return
- if(locate(/obj/singularity/narsie) in GLOB.poi_list)
+ if(locate(/obj/narsie) in GLOB.poi_list)
to_chat(user, "Nar'Sie is already on this plane, there is no delaying the end of all things.")
return
diff --git a/code/modules/antagonists/cult/runes.dm b/code/modules/antagonists/cult/runes.dm
index 7a23a312d2a..a486a8d6bab 100644
--- a/code/modules/antagonists/cult/runes.dm
+++ b/code/modules/antagonists/cult/runes.dm
@@ -503,7 +503,7 @@ structure_check() searches for nearby cultist structures required for the invoca
if(!(place in summon_objective.summon_spots))
to_chat(user, "The Geometer can only be summoned where the veil is weak - in [english_list(summon_objective.summon_spots)]!")
return
- if(locate(/obj/singularity/narsie) in GLOB.poi_list)
+ if(locate(/obj/narsie) in GLOB.poi_list)
for(var/M in invokers)
to_chat(M, "Nar'Sie is already on this plane!")
log_game("Nar'Sie rune failed - already summoned")
@@ -516,7 +516,7 @@ structure_check() searches for nearby cultist structures required for the invoca
sleep(40)
if(src)
color = RUNE_COLOR_RED
- new /obj/singularity/narsie/large/cult(T) //Causes Nar'Sie to spawn even if the rune has been removed
+ new /obj/narsie(T) //Causes Nar'Sie to spawn even if the rune has been removed
//Rite of Resurrection: Requires a dead or inactive cultist. When reviving the dead, you can only perform one revival for every three sacrifices your cult has carried out.
/obj/effect/rune/raise_dead
diff --git a/code/modules/antagonists/morph/morph.dm b/code/modules/antagonists/morph/morph.dm
index e3bb1402e5f..ef92118d75e 100644
--- a/code/modules/antagonists/morph/morph.dm
+++ b/code/modules/antagonists/morph/morph.dm
@@ -38,6 +38,8 @@
var/static/list/blacklist_typecache = typecacheof(list(
/atom/movable/screen,
/obj/singularity,
+ /obj/energy_ball,
+ /obj/narsie,
/mob/living/simple_animal/hostile/morph,
/obj/effect))
diff --git a/code/modules/antagonists/wizard/equipment/artefact.dm b/code/modules/antagonists/wizard/equipment/artefact.dm
index c709fe05e0b..9222f3087ca 100644
--- a/code/modules/antagonists/wizard/equipment/artefact.dm
+++ b/code/modules/antagonists/wizard/equipment/artefact.dm
@@ -16,7 +16,7 @@
w_class = WEIGHT_CLASS_NORMAL
hitsound = 'sound/weapons/bladeslice.ogg'
var/charges = 1
- var/spawn_type = /obj/singularity/wizard
+ var/spawn_type = /obj/tear_in_reality
var/spawn_amt = 1
var/activate_descriptor = "reality"
var/rend_desc = "You should run now."
@@ -98,29 +98,38 @@
rend_desc = "Gently wafting with the sounds of mirthful grunting."
icon_state = "clownrender"
-////TEAR IN REALITY
+#define TEAR_IN_REALITY_CONSUME_RANGE 3
+#define TEAR_IN_REALITY_SINGULARITY_SIZE STAGE_FOUR
-/obj/singularity/wizard
+/// Tear in reality, spawned by the veil render
+/obj/tear_in_reality
name = "tear in the fabric of reality"
desc = "This isn't right."
icon = 'icons/effects/224x224.dmi'
icon_state = "reality"
pixel_x = -96
pixel_y = -96
- dissipate = 0
- move_self = 0
- consume_range = 3
- grav_pull = 4
- current_size = STAGE_FOUR
- allowed_size = STAGE_FOUR
+ anchored = TRUE
+ density = TRUE
+ move_resist = INFINITY
+ layer = MASSIVE_OBJ_LAYER
+ light_range = 6
+ appearance_flags = LONG_GLIDE
+ resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | FREEZE_PROOF
+ obj_flags = CAN_BE_HIT | DANGEROUS_POSSESSION
-/obj/singularity/wizard/process()
- move()
- eat()
- return
+/obj/tear_in_reality/Initialize(mapload)
+ . = ..()
+ AddComponent(
+ /datum/component/singularity, \
+ consume_range = TEAR_IN_REALITY_CONSUME_RANGE, \
+ notify_admins = !mapload, \
+ roaming = FALSE, \
+ singularity_size = TEAR_IN_REALITY_SINGULARITY_SIZE, \
+ )
-/obj/singularity/wizard/attack_tk(mob/user)
+/obj/tear_in_reality/attack_tk(mob/user)
if(!iscarbon(user))
return
. = COMPONENT_CANCEL_ATTACK_CHAIN
@@ -132,18 +141,17 @@
insaneinthemembrane.sanity = 0
for(var/lore in typesof(/datum/brain_trauma/severe))
jedi.gain_trauma(lore)
- addtimer(CALLBACK(src, /obj/singularity/wizard.proc/deranged, jedi), 10 SECONDS)
+ addtimer(CALLBACK(src, .proc/deranged, jedi), 10 SECONDS)
-
-/obj/singularity/wizard/proc/deranged(mob/living/carbon/C)
+/obj/tear_in_reality/proc/deranged(mob/living/carbon/C)
if(!C || C.stat == DEAD)
return
C.vomit(0, TRUE, TRUE, 3, TRUE)
C.spew_organ(3, 2)
C.death()
-/obj/singularity/wizard/mapped/admin_investigate_setup()
- return
+#undef TEAR_IN_REALITY_CONSUME_RANGE
+#undef TEAR_IN_REALITY_SINGULARITY_SIZE
/////////////////////////////////////////Scrying///////////////////
diff --git a/code/modules/awaymissions/mission_code/Academy.dm b/code/modules/awaymissions/mission_code/Academy.dm
index 8c87735758f..9790946d87f 100644
--- a/code/modules/awaymissions/mission_code/Academy.dm
+++ b/code/modules/awaymissions/mission_code/Academy.dm
@@ -60,21 +60,20 @@
name = "Pyromancy Evaluation"
info = "Current Grade: F. Educator's Notes: No improvement shown despite multiple private lessons. Suggest additional tutelage."
-
+/// The immobile, close pulling singularity seen in the academy away mission
/obj/singularity/academy
- dissipate = FALSE
move_self = FALSE
- grav_pull = TRUE
-/obj/singularity/academy/admin_investigate_setup()
- return
+/obj/singularity/academy/Initialize()
+ . = ..()
+
+ var/datum/component/singularity/singularity = singularity_component.resolve()
+ singularity?.grav_pull = 1
/obj/singularity/academy/process(delta_time)
- eat()
if(DT_PROB(0.5, delta_time))
mezzer()
-
/obj/item/clothing/glasses/meson/truesight
name = "The Lens of Truesight"
desc = "I can see forever!"
diff --git a/code/modules/awaymissions/mission_code/stationCollision.dm b/code/modules/awaymissions/mission_code/stationCollision.dm
index 4ed9f5d79f6..859654c0cb7 100644
--- a/code/modules/awaymissions/mission_code/stationCollision.dm
+++ b/code/modules/awaymissions/mission_code/stationCollision.dm
@@ -135,22 +135,3 @@ GLOBAL_VAR_INIT(sc_safecode5, "[rand(0,9)]")
new /obj/item/clothing/suit/space/hardsuit/cult(src)
//new /obj/item/teleportation_scroll(src)
new /obj/item/stack/ore/diamond(src)
-
-/*
- * Modified Nar'Sie
- */
-/obj/singularity/narsie/mini
- desc = "Your body becomes weak and your feel your mind slipping away as you try to comprehend what you know can't be possible."
- move_self = 0 //Contianed narsie does not move!
- grav_pull = 0 //Contained narsie does not pull stuff in!
-//Override this to prevent no adminlog runtimes and admin warnings about a singularity without containment
-/obj/singularity/narsie/mini/admin_investigate_setup()
- return
-
-/obj/singularity/narsie/mini/process(delta_time)
- eat()
- if(DT_PROB(13, delta_time))
- mezzer()
-
-/obj/singularity/narsie/mini/ex_act()
- return
diff --git a/code/modules/power/singularity/boh_tear.dm b/code/modules/power/singularity/boh_tear.dm
index 80de8d9c17a..44d562f2768 100644
--- a/code/modules/power/singularity/boh_tear.dm
+++ b/code/modules/power/singularity/boh_tear.dm
@@ -1,51 +1,47 @@
/// BoH tear
/// The BoH tear is a stationary singularity with a really high gravitational pull, which collapses briefly after being created
/// The BoH isn't deleted for 10 minutes (only moved to nullspace) so that admins may retrieve the things back in case of a grief
-/obj/singularity/boh_tear
+#define BOH_TEAR_CONSUME_RANGE 1
+#define BOH_TEAR_GRAV_PULL 25
+
+/obj/boh_tear
name = "tear in the fabric of reality"
desc = "Your own comprehension of reality starts bending as you stare this."
+ anchored = TRUE
+ appearance_flags = LONG_GLIDE
+ density = TRUE
icon = 'icons/effects/96x96.dmi'
icon_state = "boh_tear"
+ layer = MASSIVE_OBJ_LAYER
+ light_range = 6
+ move_resist = INFINITY
+ obj_flags = CAN_BE_HIT | DANGEROUS_POSSESSION
pixel_x = -32
pixel_y = -32
- dissipate = 0
- move_self = 0
- consume_range = 1
- grav_pull = 25
- current_size = STAGE_SIX
- allowed_size = STAGE_SIX
- var/ghosts = list()
- var/old_loc
- var/start_time = 0
+ resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | FREEZE_PROOF
+ flags_1 = SUPERMATTER_IGNORES_1
-/obj/singularity/boh_tear/Initialize()
+/obj/boh_tear/Initialize()
. = ..()
- old_loc = loc
- start_time = world.time
QDEL_IN(src, 5 SECONDS) // vanishes after 5 seconds
-/obj/singularity/boh_tear/process()
- //Backup to catch timerss errors
- if(start_time + (10 SECONDS) < world.time)
- stack_trace("The timer subsytem isn't firing properly, yell at your local coders posthaste")
- qdel(src)
- eat()
+ AddComponent(
+ /datum/component/singularity, \
+ consume_range = BOH_TEAR_CONSUME_RANGE, \
+ grav_pull = BOH_TEAR_GRAV_PULL, \
+ roaming = FALSE, \
+ singularity_size = STAGE_SIX, \
+ )
-/obj/singularity/boh_tear/consume(atom/A)
- A.singularity_act(current_size, src)
-
-/obj/singularity/boh_tear/admin_investigate_setup()
- var/turf/T = get_turf(src)
- message_admins("A BoH tear has been created at [ADMIN_VERBOSEJMP(T)].")
- investigate_log("was created at [AREACOORD(T)].", INVESTIGATE_SINGULO)
-
-
-/obj/singularity/boh_tear/attack_tk(mob/user)
+/obj/boh_tear/attack_tk(mob/user)
if(!isliving(user))
return
var/mob/living/jedi = user
to_chat(jedi, "You don't feel like you are real anymore.")
jedi.dust_animation()
jedi.spawn_dust()
- addtimer(CALLBACK(src, .proc/consume, jedi), 0.5 SECONDS)
+ addtimer(CALLBACK(src, /atom/proc/attack_hand, jedi), 0.5 SECONDS)
return COMPONENT_CANCEL_ATTACK_CHAIN
+
+#undef BOH_TEAR_CONSUME_RANGE
+#undef BOH_TEAR_GRAV_PULL
diff --git a/code/modules/power/singularity/containment_field.dm b/code/modules/power/singularity/containment_field.dm
index f7e901a56dd..13ca77a0f38 100644
--- a/code/modules/power/singularity/containment_field.dm
+++ b/code/modules/power/singularity/containment_field.dm
@@ -20,6 +20,7 @@
/obj/machinery/field/containment/Initialize()
. = ..()
air_update_turf(TRUE)
+ RegisterSignal(src, COMSIG_ATOM_SINGULARITY_TRY_MOVE, .proc/block_singularity)
/obj/machinery/field/containment/Destroy()
FG1.fields -= src
@@ -79,6 +80,11 @@
FG2 = master2
return TRUE
+/obj/machinery/field/containment/proc/block_singularity()
+ SIGNAL_HANDLER
+
+ return SINGULARITY_TRY_MOVE_BLOCK
+
/obj/machinery/field/containment/shock(mob/living/user)
if(!FG1 || !FG2)
qdel(src)
diff --git a/code/modules/power/singularity/field_generator.dm b/code/modules/power/singularity/field_generator.dm
index 136f5a7b30b..5f212abf7f9 100644
--- a/code/modules/power/singularity/field_generator.dm
+++ b/code/modules/power/singularity/field_generator.dm
@@ -58,6 +58,7 @@ no power level overlay is currently in the overlays list.
. = ..()
fields = list()
connected_gens = list()
+ RegisterSignal(src, COMSIG_ATOM_SINGULARITY_TRY_MOVE, .proc/block_singularity_if_active)
/obj/machinery/field/generator/anchored/Initialize()
. = ..()
@@ -340,11 +341,6 @@ no power level overlay is currently in the overlays list.
clean_up = 0
update_icon()
- //This is here to help fight the "hurr durr, release singulo cos nobody will notice before the
- //singulo eats the evidence". It's not fool-proof but better than nothing.
- //I want to avoid using global variables.
- INVOKE_ASYNC(src, .proc/notify_admins)
-
move_resist = initial(move_resist)
/obj/machinery/field/generator/proc/shield_floor(create)
@@ -386,17 +382,11 @@ no power level overlay is currently in the overlays list.
if(S)
qdel(S)
-/obj/machinery/field/generator/proc/notify_admins()
- var/temp = TRUE //stops spam
- for(var/obj/singularity/O in GLOB.singularities)
- if(O.last_warning && temp)
- if((world.time - O.last_warning) > 50) //to stop message-spam
- temp = FALSE
- var/turf/T = get_turf(src)
- message_admins("A singulo exists and a containment field has failed at [ADMIN_VERBOSEJMP(T)].")
- investigate_log("has failed whilst a singulo exists at [AREACOORD(T)].", INVESTIGATE_SINGULO)
- notify_ghosts("IT'S LOOSE", source = src, action = NOTIFY_ORBIT, flashwindow = FALSE, ghost_sound = 'sound/machines/warning-buzzer.ogg', header = "IT'S LOOSE", notify_volume = 75)
- O.last_warning = world.time
+/obj/machinery/field/generator/proc/block_singularity_if_active()
+ SIGNAL_HANDLER
+
+ if (active)
+ return SINGULARITY_TRY_MOVE_BLOCK
/obj/machinery/field/generator/shock(mob/living/user)
if(fields.len)
diff --git a/code/modules/power/singularity/narsie.dm b/code/modules/power/singularity/narsie.dm
index e4afd627571..8db08530d76 100644
--- a/code/modules/power/singularity/narsie.dm
+++ b/code/modules/power/singularity/narsie.dm
@@ -1,74 +1,204 @@
-/obj/singularity/narsie //Moving narsie to a child object of the singularity so it can be made to function differently. --NEO
- name = "Nar'Sie's Avatar"
+#define NARSIE_CHANCE_TO_PICK_NEW_TARGET 5
+#define NARSIE_CONSUME_RANGE 12
+#define NARSIE_GRAV_PULL 10
+#define NARSIE_MESMERIZE_CHANCE 25
+#define NARSIE_MESMERIZE_EFFECT 60
+#define NARSIE_SINGULARITY_SIZE 12
+
+/// Nar'Sie, the God of the blood cultists
+/obj/narsie
+ name = "Nar'Sie"
desc = "Your mind begins to bubble and ooze as it tries to comprehend what it sees."
- icon = 'icons/obj/narsie_small.dmi'
+ icon = 'icons/obj/narsie.dmi'
icon_state = "narsie"
- pixel_x = -89
- pixel_y = -85
+ anchored = TRUE
+ appearance_flags = LONG_GLIDE
density = FALSE
- current_size = 9 //It moves/eats like a max-size singulo, aside from range. --NEO
- contained = 0 //Are we going to move around?
- dissipate = 0 //Do we lose energy over time?
- move_self = 1 //Do we move on our own?
- grav_pull = 5 //How many tiles out do we pull?
- consume_range = 6 //How many tiles out do we eat
+ gender = FEMALE
+ layer = MASSIVE_OBJ_LAYER
+ light_color = COLOR_RED
light_power = 0.7
light_range = 15
- light_color = COLOR_RED
- gender = FEMALE
-
-/obj/singularity/narsie/large
- name = "Nar'Sie"
- icon = 'icons/obj/narsie.dmi'
- // Pixel stuff centers Narsie.
+ light_range = 6
+ move_resist = INFINITY
+ obj_flags = CAN_BE_HIT | DANGEROUS_POSSESSION
pixel_x = -236
pixel_y = -256
- current_size = 12
- grav_pull = 10
- consume_range = 12 //How many tiles out do we eat
+ resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | FREEZE_PROOF
+ flags_1 = SUPERMATTER_IGNORES_1
-/obj/singularity/narsie/large/Initialize()
- . = ..()
- send_to_playing_players("NAR'SIE HAS RISEN")
- sound_to_playing_players('sound/creatures/narsie_rises.ogg')
+ /// The singularity component to move around Nar'Sie.
+ /// A weak ref in case an admin removes the component to preserve the functionality.
+ var/datum/weakref/singularity
- var/area/A = get_area(src)
- if(A)
- var/mutable_appearance/alert_overlay = mutable_appearance('icons/effects/cult_effects.dmi', "ghostalertsie")
- notify_ghosts("Nar'Sie has risen in \the [A.name]. Reach out to the Geometer to be given a new shell for your soul.", source = src, alert_overlay = alert_overlay, action=NOTIFY_ATTACK)
- narsie_spawn_animation()
- UnregisterSignal(src, COMSIG_ATOM_BSA_BEAM) //set up in /singularity/Initialize()
-
-/obj/singularity/narsie/large/cult // For the new cult ending, guaranteed to end the round within 3 minutes
var/list/souls_needed = list()
var/soul_goal = 0
var/souls = 0
var/resolved = FALSE
-/obj/singularity/narsie/large/cult/Initialize()
+/obj/narsie/Initialize()
. = ..()
+
+ singularity = WEAKREF(AddComponent(
+ /datum/component/singularity, \
+ bsa_targetable = FALSE, \
+ consume_callback = CALLBACK(src, .proc/consume), \
+ consume_range = NARSIE_CONSUME_RANGE, \
+ disregard_failed_movements = TRUE, \
+ grav_pull = NARSIE_GRAV_PULL, \
+ roaming = FALSE, /* This is set once the animation finishes */ \
+ singularity_size = NARSIE_SINGULARITY_SIZE, \
+ ))
+
+ send_to_playing_players("NAR'SIE HAS RISEN")
+ sound_to_playing_players('sound/creatures/narsie_rises.ogg')
+
+ var/area/area = get_area(src)
+ if(area)
+ var/mutable_appearance/alert_overlay = mutable_appearance('icons/effects/cult_effects.dmi', "ghostalertsie")
+ notify_ghosts("Nar'Sie has risen in [area]. Reach out to the Geometer to be given a new shell for your soul.", source = src, alert_overlay = alert_overlay, action = NOTIFY_ATTACK)
+ narsie_spawn_animation()
+
GLOB.cult_narsie = src
var/list/all_cults = list()
- for(var/datum/antagonist/cult/C in GLOB.antagonists)
- if(!C.owner)
+
+ for (var/datum/antagonist/cult/cultist in GLOB.antagonists)
+ if (!cultist.owner)
continue
- all_cults |= C.cult_team
- for(var/datum/team/cult/T in all_cults)
- deltimer(T.blood_target_reset_timer)
- T.blood_target = src
- var/datum/objective/eldergod/summon_objective = locate() in T.objectives
+ all_cults |= cultist.cult_team
+
+ for (var/_cult_team in all_cults)
+ var/datum/team/cult/cult_team = _cult_team
+ deltimer(cult_team.blood_target_reset_timer)
+ cult_team.blood_target = src
+ var/datum/objective/eldergod/summon_objective = locate() in cult_team.objectives
if(summon_objective)
summon_objective.summoned = TRUE
- for(var/datum/mind/cult_mind in SSticker.mode.cult)
- if(isliving(cult_mind.current))
+
+ for (var/_cult_mind in SSticker.mode.cult)
+ var/datum/mind/cult_mind = _cult_mind
+ if (isliving(cult_mind.current))
var/mob/living/L = cult_mind.current
L.narsie_act()
- for(var/mob/living/player in GLOB.player_list)
- if(player.stat != DEAD && player.loc && is_station_level(player.loc.z) && !iscultist(player) && !isanimal(player))
+
+ for (var/mob/living/carbon/player in GLOB.player_list)
+ if (player.stat != DEAD && is_station_level(player.loc?.z) && !iscultist(player))
souls_needed[player] = TRUE
+
soul_goal = round(1 + LAZYLEN(souls_needed) * 0.75)
INVOKE_ASYNC(GLOBAL_PROC, .proc/begin_the_end)
+/obj/narsie/Destroy()
+ send_to_playing_players("\"[pick("Nooooo...", "Not die. How-", "Die. Mort-", "Sas tyen re-")]\"")
+ sound_to_playing_players('sound/magic/demon_dies.ogg', 50)
+
+ var/list/all_cults = list()
+
+ for (var/datum/antagonist/cult/cultist in GLOB.antagonists)
+ if (!cultist.owner)
+ continue
+ all_cults |= cultist.cult_team
+
+ for(var/_cult_team in all_cults)
+ var/datum/team/cult/cult_team = _cult_team
+ var/datum/objective/eldergod/summon_objective = locate() in cult_team.objectives
+ if (summon_objective)
+ summon_objective.summoned = FALSE
+ summon_objective.killed = TRUE
+
+ return ..()
+
+/obj/narsie/attack_ghost(mob/user)
+ makeNewConstruct(/mob/living/simple_animal/hostile/construct/harvester, user, cultoverride = TRUE, loc_override = loc)
+
+/obj/narsie/process()
+ var/datum/component/singularity/singularity_component = singularity.resolve()
+
+ if (!isnull(singularity_component) && (!singularity_component?.target || prob(NARSIE_CHANCE_TO_PICK_NEW_TARGET)))
+ pickcultist()
+
+ if (prob(NARSIE_MESMERIZE_CHANCE))
+ mesmerize()
+
+/obj/narsie/Bump(atom/target)
+ var/turf/target_turf = get_turf(target)
+ if (target_turf == loc)
+ target_turf = get_step(target, target.dir) //please don't slam into a window like a bird, Nar'Sie
+ forceMove(target_turf)
+
+/// Stun people around Nar'Sie that aren't cultists
+/obj/narsie/proc/mesmerize()
+ for (var/mob/living/carbon/victim in viewers(NARSIE_CONSUME_RANGE, src))
+ if (victim.stat == CONSCIOUS)
+ if (!iscultist(victim))
+ to_chat(victim, "You feel conscious thought crumble away in an instant as you gaze upon [src]...")
+ victim.apply_effect(NARSIE_MESMERIZE_EFFECT, EFFECT_STUN)
+
+/// Narsie rewards her cultists with being devoured first, then picks a ghost to follow.
+/obj/narsie/proc/pickcultist()
+ var/list/cultists = list()
+ var/list/noncultists = list()
+
+ for (var/mob/living/carbon/food in GLOB.alive_mob_list) //we don't care about constructs or cult-Ians or whatever. cult-monkeys are fair game i guess
+ var/turf/pos = get_turf(food)
+ if (!pos || (pos.z != z))
+ continue
+
+ if (iscultist(food))
+ cultists += food
+ else
+ noncultists += food
+
+ if (cultists.len) //cultists get higher priority
+ acquire(pick(cultists))
+ return
+
+ if (noncultists.len)
+ acquire(pick(noncultists))
+ return
+
+ //no living humans, follow a ghost instead.
+ for (var/mob/dead/observer/ghost in GLOB.player_list)
+ var/turf/pos = get_turf(ghost)
+ if (!pos || (pos.z != z))
+ continue
+ cultists += ghost
+ if (cultists.len)
+ acquire(pick(cultists))
+ return
+
+/// Nar'Sie gets a taste of something, and will start to gravitate towards it
+/obj/narsie/proc/acquire(atom/food)
+ var/datum/component/singularity/singularity_component = singularity.resolve()
+
+ if (isnull(singularity_component))
+ return
+
+ var/old_target = singularity_component.target
+ if (food == old_target)
+ return
+
+ to_chat(old_target, "NAR'SIE HAS LOST INTEREST IN YOU.")
+ singularity_component.target = food
+ if(ishuman(food))
+ to_chat(food, "NAR'SIE HUNGERS FOR YOUR SOUL.")
+ else
+ to_chat(food, "NAR'SIE HAS CHOSEN YOU TO LEAD HER TO HER NEXT MEAL.")
+
+/// Called to make Nar'Sie convert objects to cult stuff, or to eat
+/obj/narsie/proc/consume(atom/target)
+ if (isturf(target))
+ target.narsie_act()
+
+/obj/narsie/proc/narsie_spawn_animation()
+ setDir(SOUTH)
+ flick("narsie_spawn_anim", src)
+ addtimer(CALLBACK(src, .proc/narsie_spawn_animation_end), 3.5 SECONDS)
+
+/obj/narsie/proc/narsie_spawn_animation_end()
+ var/datum/component/singularity/singularity_component = singularity.resolve()
+ singularity_component?.roaming = TRUE
+
/proc/begin_the_end()
sleep(50)
if(QDELETED(GLOB.cult_narsie)) // uno
@@ -105,21 +235,6 @@
sound_to_playing_players('sound/machines/alarm.ogg')
addtimer(CALLBACK(GLOBAL_PROC, .proc/cult_ending_helper), 120)
-/obj/singularity/narsie/large/cult/Destroy()
- send_to_playing_players("\"[pick("Nooooo...", "Not die. How-", "Die. Mort-", "Sas tyen re-")]\"")
- sound_to_playing_players('sound/magic/demon_dies.ogg', 50)
- var/list/all_cults = list()
- for(var/datum/antagonist/cult/C in GLOB.antagonists)
- if(!C.owner)
- continue
- all_cults |= C.cult_team
- for(var/datum/team/cult/T in all_cults)
- var/datum/objective/eldergod/summon_objective = locate() in T.objectives
- if(summon_objective)
- summon_objective.summoned = FALSE
- summon_objective.killed = TRUE
- return ..()
-
/proc/ending_helper()
SSticker.force_ending = 1
@@ -131,106 +246,9 @@
else // explosion
Cinematic(CINEMATIC_CULT_NUKE,world,CALLBACK(GLOBAL_PROC,/proc/ending_helper))
-//ATTACK GHOST IGNORING PARENT RETURN VALUE
-/obj/singularity/narsie/large/attack_ghost(mob/dead/observer/user as mob)
- makeNewConstruct(/mob/living/simple_animal/hostile/construct/harvester, user, cultoverride = TRUE, loc_override = src.loc)
-
-/obj/singularity/narsie/process()
- eat()
- if(!target || prob(5))
- pickcultist()
- move()
- if(prob(25))
- mezzer()
-
-
-/obj/singularity/narsie/Bump(atom/A)
- var/turf/T = get_turf(A)
- if(T == loc)
- T = get_step(A, A.dir) //please don't slam into a window like a bird, Nar'Sie
- forceMove(T)
-
-
-/obj/singularity/narsie/mezzer()
- for(var/mob/living/carbon/M in viewers(consume_range, src))
- if(M.stat == CONSCIOUS)
- if(!iscultist(M))
- to_chat(M, "You feel conscious thought crumble away in an instant as you gaze upon [src.name]...")
- M.apply_effect(60, EFFECT_STUN)
-
-
-/obj/singularity/narsie/consume(atom/A)
- if(isturf(A))
- A.narsie_act()
-
-
-/obj/singularity/narsie/ex_act() //No throwing bombs at her either.
- return
-
-
-/obj/singularity/narsie/proc/pickcultist() //Narsie rewards her cultists with being devoured first, then picks a ghost to follow.
- var/list/cultists = list()
- var/list/noncultists = list()
-
- for(var/mob/living/carbon/food in GLOB.alive_mob_list) //we don't care about constructs or cult-Ians or whatever. cult-monkeys are fair game i guess
- var/turf/pos = get_turf(food)
- if(!pos || (pos.z != z))
- continue
-
- if(iscultist(food))
- cultists += food
- else
- noncultists += food
-
- if(cultists.len) //cultists get higher priority
- acquire(pick(cultists))
- return
-
- if(noncultists.len)
- acquire(pick(noncultists))
- return
-
- //no living humans, follow a ghost instead.
- for(var/mob/dead/observer/ghost in GLOB.player_list)
- var/turf/pos = get_turf(ghost)
- if(!pos || (pos.z != z))
- continue
- cultists += ghost
- if(cultists.len)
- acquire(pick(cultists))
- return
-
-
-/obj/singularity/narsie/proc/acquire(atom/food)
- if(food == target)
- return
- to_chat(target, "NAR'SIE HAS LOST INTEREST IN YOU.")
- target = food
- if(ishuman(target))
- to_chat(target, "NAR'SIE HUNGERS FOR YOUR SOUL.")
- else
- to_chat(target, "NAR'SIE HAS CHOSEN YOU TO LEAD HER TO HER NEXT MEAL.")
-
-//Wizard narsie
-/obj/singularity/narsie/wizard
- grav_pull = 0
-
-/obj/singularity/narsie/wizard/eat()
-// if(defer_powernet_rebuild != 2)
-// defer_powernet_rebuild = 1
- for(var/atom/X in urange(consume_range,src,1))
- if(isturf(X) || ismovable(X))
- consume(X)
-// if(defer_powernet_rebuild != 2)
-// defer_powernet_rebuild = 0
- return
-
-
-/obj/singularity/narsie/proc/narsie_spawn_animation()
- setDir(SOUTH)
- move_self = FALSE
- flick("narsie_spawn_anim",src)
- addtimer(CALLBACK(src, .proc/narsie_spawn_animation_end), 3.5 SECONDS)
-
-/obj/singularity/narsie/proc/narsie_spawn_animation_end()
- move_self = TRUE
+#undef NARSIE_CHANCE_TO_PICK_NEW_TARGET
+#undef NARSIE_CONSUME_RANGE
+#undef NARSIE_GRAV_PULL
+#undef NARSIE_MESMERIZE_CHANCE
+#undef NARSIE_MESMERIZE_EFFECT
+#undef NARSIE_SINGULARITY_SIZE
diff --git a/code/modules/power/singularity/singularity.dm b/code/modules/power/singularity/singularity.dm
index 78a843aab7d..e4b25e78d4d 100644
--- a/code/modules/power/singularity/singularity.dm
+++ b/code/modules/power/singularity/singularity.dm
@@ -1,5 +1,4 @@
-
-
+/// The gravitational singularity
/obj/singularity
name = "gravitational singularity"
desc = "A gravitational singularity."
@@ -11,41 +10,51 @@
layer = MASSIVE_OBJ_LAYER
light_range = 6
appearance_flags = LONG_GLIDE
+
+ /// The singularity component itself.
+ /// A weak ref in case an admin removes the component to preserve the functionality.
+ var/datum/weakref/singularity_component
+
var/current_size = 1
var/allowed_size = 1
- var/contained = 1 //Are we going to move around?
var/energy = 100 //How strong are we?
var/dissipate = TRUE //Do we lose energy over time?
var/dissipate_delay = 10
var/dissipate_track = 0
var/dissipate_strength = 1 //How much energy do we lose?
- var/move_self = 1 //Do we move on our own?
- var/grav_pull = 4 //How many tiles out do we pull?
- var/consume_range = 0 //How many tiles out do we eat
var/event_chance = 10 //Prob for event each tick
- var/target = null //its target. moves towards the target if it has one
- var/last_failed_movement = 0//Will not move in the same dir if it couldnt before, will help with the getting stuck on fields thing
- var/last_warning
- var/consumedSupermatter = 0 //If the singularity has eaten a supermatter shard and can go to stage six
- var/drifting_dir = 0 // Chosen direction to drift in
+ var/move_self = TRUE
+ var/consumed_supermatter = FALSE //If the singularity has eaten a supermatter shard and can go to stage six
+
+ flags_1 = SUPERMATTER_IGNORES_1
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | FREEZE_PROOF
obj_flags = CAN_BE_HIT | DANGEROUS_POSSESSION
/obj/singularity/Initialize(mapload, starting_energy = 50)
- //CARN: admin-alert for chuckle-fuckery.
- admin_investigate_setup()
-
- src.energy = starting_energy
. = ..()
+
+ energy = starting_energy
+
START_PROCESSING(SSobj, src)
GLOB.poi_list |= src
GLOB.singularities |= src
- for(var/obj/machinery/power/singularity_beacon/singubeacon in GLOB.machines)
- if(singubeacon.active)
- target = singubeacon
+
+ var/datum/component/singularity/new_component = AddComponent(
+ /datum/component/singularity, \
+ consume_callback = CALLBACK(src, .proc/consume), \
+ )
+
+ singularity_component = WEAKREF(new_component)
+
+ expand(current_size)
+
+ for (var/obj/machinery/power/singularity_beacon/singubeacon in GLOB.machines)
+ if (singubeacon.active)
+ new_component.target = singubeacon
break
- AddElement(/datum/element/bsa_blocker)
- RegisterSignal(src, COMSIG_ATOM_BSA_BEAM, .proc/bluespace_reaction)
+
+ if (!mapload)
+ notify_ghosts("IT'S LOOSE", source = src, action = NOTIFY_ORBIT, flashwindow = FALSE, ghost_sound = 'sound/machines/warning-buzzer.ogg', header = "IT'S LOOSE", notify_volume = 75)
/obj/singularity/Destroy()
STOP_PROCESSING(SSobj, src)
@@ -53,49 +62,6 @@
GLOB.singularities.Remove(src)
return ..()
-/obj/singularity/Move(atom/newloc, direct)
- var/turf/T = get_turf(src)
- for(var/dir in GLOB.cardinals)
- if(direct & dir)
- T = get_step(T, dir)
- if(!T)
- break
- // eat the stuff if we're going to move into it so it doesn't mess up our movement
- for(var/atom/A in T.contents)
- consume(A)
- consume(T)
-
- if(current_size >= STAGE_FIVE || check_turfs_in(direct))
- last_failed_movement = 0//Reset this because we moved
- return ..()
- else
- last_failed_movement = direct
- return FALSE
-
-/obj/singularity/attack_hand(mob/user)
- consume(user)
- return TRUE
-
-/obj/singularity/attack_paw(mob/user)
- consume(user)
-
-/obj/singularity/attack_alien(mob/user)
- consume(user)
-
-/obj/singularity/attack_animal(mob/user)
- consume(user)
-
-/obj/singularity/attackby(obj/item/W, mob/user, params)
- consume(user)
- return TRUE
-
-/obj/singularity/Process_Spacemove() //The singularity stops drifting for no man!
- return FALSE
-
-/obj/singularity/blob_act(obj/structure/blob/B)
- return
-
-
/obj/singularity/attack_tk(mob/user)
if(!iscarbon(user))
return
@@ -150,7 +116,6 @@
rip_u.dismember(BURN)
qdel(rip_u)
-
/obj/singularity/ex_act(severity, target)
switch(severity)
if(1)
@@ -164,52 +129,14 @@
energy -= round(((energy+1)/3),1)
if(3)
energy -= round(((energy+1)/4),1)
- return
-
-
-/obj/singularity/bullet_act(obj/projectile/P)
- qdel(P)
- return BULLET_ACT_HIT //Will there be an impact? Who knows. Will we see it? No.
-
-
-/obj/singularity/Bump(atom/A)
- consume(A)
- if(QDELETED(A)) // don't keep moving into objects that weren't destroyed infinitely
- step(src, drifting_dir)
- return
-
-/obj/singularity/Crossed(atom/A)
- ..()
- consume(A)
-
-/obj/singularity/Bumped(atom/movable/AM)
- consume(AM)
-
/obj/singularity/process()
if(current_size >= STAGE_TWO)
- move()
if(prob(event_chance))//Chance for it to run a special event TODO:Come up with one or two more that fit
event()
- eat()
dissipate()
check_energy()
- return
-
-
-/obj/singularity/attack_ai() //to prevent ais from gibbing themselves when they click on one.
- return
-
-
-/obj/singularity/proc/admin_investigate_setup()
- var/turf/T = get_turf(src)
- last_warning = world.time
- var/count = locate(/obj/machinery/field/containment) in urange(30, src, 1)
- if(!count)
- message_admins("A singulo has been created without containment fields active at [ADMIN_VERBOSEJMP(T)].")
- investigate_log("was created at [AREACOORD(T)]. [count?"":"No containment fields were active"]", INVESTIGATE_SINGULO)
-
/obj/singularity/proc/dissipate()
if(!dissipate)
return
@@ -219,13 +146,18 @@
else
dissipate_track++
-
-/obj/singularity/proc/expand(force_size = 0)
+/obj/singularity/proc/expand(force_size)
var/temp_allowed_size = src.allowed_size
+
if(force_size)
temp_allowed_size = force_size
- if(temp_allowed_size >= STAGE_SIX && !consumedSupermatter)
+
+ if(temp_allowed_size >= STAGE_SIX && !consumed_supermatter)
temp_allowed_size = STAGE_FIVE
+
+ var/new_grav_pull
+ var/new_consume_range
+
switch(temp_allowed_size)
if(STAGE_ONE)
current_size = STAGE_ONE
@@ -233,8 +165,8 @@
icon_state = "singularity_s1"
pixel_x = 0
pixel_y = 0
- grav_pull = 4
- consume_range = 0
+ new_grav_pull = 4
+ new_consume_range = 0
dissipate_delay = 10
dissipate_track = 0
dissipate_strength = 1
@@ -245,8 +177,8 @@
icon_state = "singularity_s3"
pixel_x = -32
pixel_y = -32
- grav_pull = 6
- consume_range = 1
+ new_grav_pull = 6
+ new_consume_range = 1
dissipate_delay = 5
dissipate_track = 0
dissipate_strength = 5
@@ -257,8 +189,8 @@
icon_state = "singularity_s5"
pixel_x = -64
pixel_y = -64
- grav_pull = 8
- consume_range = 2
+ new_grav_pull = 8
+ new_consume_range = 2
dissipate_delay = 4
dissipate_track = 0
dissipate_strength = 20
@@ -269,8 +201,8 @@
icon_state = "singularity_s7"
pixel_x = -96
pixel_y = -96
- grav_pull = 10
- consume_range = 3
+ new_grav_pull = 10
+ new_consume_range = 3
dissipate_delay = 10
dissipate_track = 0
dissipate_strength = 10
@@ -280,18 +212,27 @@
icon_state = "singularity_s9"
pixel_x = -128
pixel_y = -128
- grav_pull = 10
- consume_range = 4
- dissipate = 0 //It cant go smaller due to e loss
+ new_grav_pull = 10
+ new_consume_range = 4
+ dissipate = FALSE //It cant go smaller due to e loss
if(STAGE_SIX) //This only happens if a stage 5 singulo consumes a supermatter shard.
current_size = STAGE_SIX
icon = 'icons/effects/352x352.dmi'
icon_state = "singularity_s11"
pixel_x = -160
pixel_y = -160
- grav_pull = 15
- consume_range = 5
- dissipate = 0
+ new_grav_pull = 15
+ new_consume_range = 5
+ dissipate = FALSE
+
+ var/datum/component/singularity/resolved_singularity = singularity_component.resolve()
+ if (!isnull(resolved_singularity))
+ resolved_singularity.consume_range = new_consume_range
+ resolved_singularity.grav_pull = new_grav_pull
+ resolved_singularity.disregard_failed_movements = current_size >= STAGE_FIVE
+ resolved_singularity.roaming = move_self && current_size >= STAGE_TWO
+ resolved_singularity.singularity_size = current_size
+
if(current_size == allowed_size)
investigate_log("grew to size [current_size]", INVESTIGATE_SINGULO)
return TRUE
@@ -316,7 +257,7 @@
if(1000 to 1999)
allowed_size = STAGE_FOUR
if(2000 to INFINITY)
- if(energy >= 3000 && consumedSupermatter)
+ if(energy >= 3000 && consumed_supermatter)
allowed_size = STAGE_SIX
else
allowed_size = STAGE_FIVE
@@ -324,51 +265,14 @@
expand()
return TRUE
-
-/obj/singularity/proc/eat()
- for(var/tile in spiral_range_turfs(grav_pull, src))
- var/turf/T = tile
- if(!T || !isturf(loc))
- continue
- if(get_dist(T, src) > consume_range)
- T.singularity_pull(src, current_size)
- else
- consume(T)
- for(var/thing in T)
- if(isturf(loc) && thing != src)
- var/atom/movable/X = thing
- if(get_dist(X, src) > consume_range)
- X.singularity_pull(src, current_size)
- else
- consume(X)
- CHECK_TICK
- return
-
-
-/obj/singularity/proc/consume(atom/A)
- var/gain = A.singularity_act(current_size, src)
- src.energy += gain
- if(istype(A, /obj/machinery/power/supermatter_crystal) && !consumedSupermatter)
+/obj/singularity/proc/consume(atom/thing)
+ var/gain = thing.singularity_act(current_size, src)
+ energy += gain
+ if(istype(thing, /obj/machinery/power/supermatter_crystal) && !consumed_supermatter)
desc = "[initial(desc)] It glows fiercely with inner fire."
name = "supermatter-charged [initial(name)]"
- consumedSupermatter = 1
+ consumed_supermatter = TRUE
set_light(10)
- return
-
-
-/obj/singularity/proc/move(force_move = 0)
- if(!move_self)
- return FALSE
-
- var/drifting_dir = pick(GLOB.alldirs - last_failed_movement)
-
- if(force_move)
- drifting_dir = force_move
-
- if(target && prob(60))
- drifting_dir = get_dir(src,target) //moves to a singulo beacon, if there is one
-
- step(src, drifting_dir)
/obj/singularity/proc/check_cardinals_range(steps, retry_with_move = FALSE)
. = length(GLOB.cardinals) //Should be 4.
@@ -497,18 +401,13 @@
/obj/singularity/proc/emp_area()
empulse(src, 8, 10)
- return
/obj/singularity/singularity_act()
var/gain = (energy/2)
var/dist = max((current_size - 2),1)
explosion(src.loc,(dist),(dist*2),(dist*4))
qdel(src)
- return(gain)
-
-/obj/singularity/proc/bluespace_reaction()
- investigate_log("has been shot by bluespace artillery and destroyed.", INVESTIGATE_SINGULO)
- qdel(src)
+ return gain
/obj/singularity/deadchat_controlled
move_self = FALSE
diff --git a/code/modules/power/supermatter/supermatter.dm b/code/modules/power/supermatter/supermatter.dm
index 107e2942175..044603e896b 100644
--- a/code/modules/power/supermatter/supermatter.dm
+++ b/code/modules/power/supermatter/supermatter.dm
@@ -467,7 +467,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
else if(power > POWER_PENALTY_THRESHOLD)
investigate_log("has spawned additional energy balls.", INVESTIGATE_SUPERMATTER)
if(T)
- var/obj/singularity/energy_ball/E = new(T)
+ var/obj/energy_ball/E = new(T)
E.energy = 200 //Gets us about 9 balls
else if(power > EVENT_POWER_PENALTY_THRESHOLD && prob(power/50) && !istype(src, /obj/machinery/power/supermatter_crystal/shard))
var/datum/round_event_control/crystal_invasion/crystals = new/datum/round_event_control/crystal_invasion
@@ -1008,7 +1008,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
user.dust(force = TRUE)
if(power_changes)
matter_power += 200
- else if(istype(AM, /obj/singularity))
+ else if(AM.flags_1 & SUPERMATTER_IGNORES_1)
return
else if(isobj(AM))
if(!iseffect(AM))
diff --git a/code/modules/power/tesla/energy_ball.dm b/code/modules/power/tesla/energy_ball.dm
index 9b3ce5801a4..a8977131a5a 100644
--- a/code/modules/power/tesla/energy_ball.dm
+++ b/code/modules/power/tesla/energy_ball.dm
@@ -10,22 +10,26 @@
#define BLOB (STRUCTURE + 1)
#define STRUCTURE (1)
-/obj/singularity/energy_ball
+/// The Tesla engine
+/obj/energy_ball
name = "energy ball"
desc = "An energy ball."
icon = 'icons/obj/tesla_engine/energy_ball.dmi'
icon_state = "energy_ball"
+ anchored = TRUE
+ appearance_flags = LONG_GLIDE
+ density = TRUE
+ layer = MASSIVE_OBJ_LAYER
+ light_range = 6
+ move_resist = INFINITY
+ obj_flags = CAN_BE_HIT | DANGEROUS_POSSESSION
pixel_x = -32
pixel_y = -32
- current_size = STAGE_TWO
- move_self = 1
- grav_pull = 0
- contained = 0
- density = TRUE
- energy = 0
- dissipate = FALSE
- dissipate_delay = 5
- dissipate_strength = 1
+ resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | FREEZE_PROOF
+ flags_1 = SUPERMATTER_IGNORES_1
+
+ var/energy
+ var/target
var/list/orbiting_balls = list()
var/miniball = FALSE
var/produced_power
@@ -33,39 +37,37 @@
var/energy_to_lower = -20
var/list/shocked_things = list()
-/obj/singularity/energy_ball/Initialize(mapload, starting_energy = 50, is_miniball = FALSE)
- miniball = is_miniball
+/obj/energy_ball/Initialize(mapload, starting_energy = 50, is_miniball = FALSE)
. = ..()
- if(!is_miniball)
+
+ energy = starting_energy
+ miniball = is_miniball
+ START_PROCESSING(SSobj, src)
+
+ if (!is_miniball)
set_light(10, 7, "#5e5edd")
-/obj/singularity/energy_ball/ex_act(severity, target)
- return
+ var/turf/spawned_turf = get_turf(src)
+ message_admins("A tesla has been created at [ADMIN_VERBOSEJMP(spawned_turf)].")
+ investigate_log("(tesla) was created at [AREACOORD(spawned_turf)].", INVESTIGATE_SINGULO)
-/obj/singularity/energy_ball/consume(severity, target)
- return
+/obj/energy_ball/Destroy()
+ if(orbiting && istype(orbiting.parent, /obj/energy_ball))
+ var/obj/energy_ball/parent_energy_ball = orbiting.parent
+ parent_energy_ball.orbiting_balls -= src
-/obj/singularity/energy_ball/Destroy()
- if(orbiting && istype(orbiting.parent, /obj/singularity/energy_ball))
- var/obj/singularity/energy_ball/EB = orbiting.parent
- EB.orbiting_balls -= src
+ QDEL_LIST(orbiting_balls)
+ STOP_PROCESSING(SSobj, src)
- for(var/ball in orbiting_balls)
- var/obj/singularity/energy_ball/EB = ball
- QDEL_NULL(EB)
- . = ..()
+ return ..()
-/obj/singularity/energy_ball/admin_investigate_setup()
- if(miniball)
- return //don't annnounce miniballs
- ..()
-
-
-/obj/singularity/energy_ball/process()
- if(!orbiting)
+/obj/energy_ball/process()
+ if(orbiting)
+ energy = 0 // ensure we dont have miniballs of miniballs
+ else
handle_energy()
- move_the_basket_ball(4 + orbiting_balls.len * 1.5)
+ move(4 + orbiting_balls.len * 1.5)
playsound(src.loc, 'sound/magic/lightningbolt.ogg', 100, TRUE, extrarange = 30)
@@ -81,40 +83,46 @@
var/list/temp_shock = list()
tesla_zap(ball, range, TESLA_MINI_POWER/7*range, shocked_targets = temp_shock)
shocked_things += temp_shock
- else
- energy = 0 // ensure we dont have miniballs of miniballs //But it'll be cool broooooooooooooooo
-/obj/singularity/energy_ball/examine(mob/user)
+/obj/energy_ball/examine(mob/user)
. = ..()
if(orbiting_balls.len)
. += "There are [orbiting_balls.len] mini-balls orbiting it."
-
-/obj/singularity/energy_ball/proc/move_the_basket_ball(move_amount)
+/obj/energy_ball/proc/move(move_amount)
var/list/dirs = GLOB.alldirs.Copy()
- for(var/I in 1 to 30)
+ for (var/i in 1 to 30)
var/atom/real_thing = pick(shocked_things)
dirs += get_dir(src, real_thing) //Carry some momentum yeah? Just a bit tho
- for(var/i in 0 to move_amount)
+ for (var/i in 0 to move_amount)
var/move_dir = pick(dirs) //ensures teslas don't just sit around
- if(target && prob(10))
- move_dir = get_dir(src,target)
- var/turf/T = get_step(src, move_dir)
- if(can_move(T))
- forceMove(T)
+ if (target && prob(10))
+ move_dir = get_dir(src, target)
+ var/turf/turf_to_move = get_step(src, move_dir)
+ if (can_move(turf_to_move))
+ forceMove(turf_to_move)
setDir(move_dir)
- for(var/mob/living/carbon/C in loc)
- dust_mobs(C)
+ for (var/mob/living/carbon/mob_to_dust in loc)
+ dust_mobs(mob_to_dust)
+/obj/energy_ball/proc/can_move(turf/to_move)
+ if (!to_move)
+ return FALSE
-/obj/singularity/energy_ball/proc/handle_energy()
+ for (var/_thing in to_move)
+ var/atom/thing = _thing
+ if (SEND_SIGNAL(thing, COMSIG_ATOM_SINGULARITY_TRY_MOVE) & SINGULARITY_TRY_MOVE_BLOCK)
+ return FALSE
+
+ return TRUE
+
+/obj/energy_ball/proc/handle_energy()
if(energy >= energy_to_raise)
energy_to_lower = energy_to_raise - 20
energy_to_raise = energy_to_raise * 1.25
playsound(src.loc, 'sound/magic/lightning_chargeup.ogg', 100, TRUE, extrarange = 30)
addtimer(CALLBACK(src, .proc/new_mini_ball), 100)
-
else if(energy < energy_to_lower && orbiting_balls.len)
energy_to_raise = energy_to_raise / 1.25
energy_to_lower = (energy_to_raise / 1.25) - 20
@@ -122,31 +130,31 @@
var/Orchiectomy_target = pick(orbiting_balls)
qdel(Orchiectomy_target)
- else if(orbiting_balls.len)
- dissipate() //sing code has a much better system.
-
-/obj/singularity/energy_ball/proc/new_mini_ball()
+/obj/energy_ball/proc/new_mini_ball()
if(!loc)
return
- var/obj/singularity/energy_ball/EB = new(loc, 0, TRUE)
- EB.transform *= pick(0.3, 0.4, 0.5, 0.6, 0.7)
- var/icon/I = icon(icon,icon_state,dir)
+ var/obj/energy_ball/miniball = new /obj/energy_ball(
+ loc,
+ /* starting_energy = */ 0,
+ /* is_miniball = */ TRUE
+ )
+
+ miniball.transform *= pick(0.3, 0.4, 0.5, 0.6, 0.7)
+ var/icon/I = icon(icon, icon_state,dir)
var/orbitsize = (I.Width() + I.Height()) * pick(0.4, 0.5, 0.6, 0.7, 0.8)
orbitsize -= (orbitsize / world.icon_size) * (world.icon_size * 0.25)
- EB.orbit(src, orbitsize, pick(FALSE, TRUE), rand(10, 25), pick(3, 4, 5, 6, 36))
+ miniball.orbit(src, orbitsize, pick(FALSE, TRUE), rand(10, 25), pick(3, 4, 5, 6, 36))
-
-/obj/singularity/energy_ball/Bump(atom/A)
+/obj/energy_ball/Bump(atom/A)
dust_mobs(A)
-/obj/singularity/energy_ball/Bumped(atom/movable/AM)
+/obj/energy_ball/Bumped(atom/movable/AM)
dust_mobs(AM)
-
-/obj/singularity/energy_ball/attack_tk(mob/user)
+/obj/energy_ball/attack_tk(mob/user)
if(!iscarbon(user))
return
var/mob/living/carbon/jedi = user
@@ -158,25 +166,22 @@
jedi.death()
return COMPONENT_CANCEL_ATTACK_CHAIN
-
-/obj/singularity/energy_ball/orbit(obj/singularity/energy_ball/target)
+/obj/energy_ball/orbit(obj/energy_ball/target)
if (istype(target))
target.orbiting_balls += src
GLOB.poi_list -= src
- target.dissipate_strength = target.orbiting_balls.len
. = ..()
-/obj/singularity/energy_ball/stop_orbit()
- if (orbiting && istype(orbiting.parent, /obj/singularity/energy_ball))
- var/obj/singularity/energy_ball/orbitingball = orbiting.parent
+/obj/energy_ball/stop_orbit()
+ if (orbiting && istype(orbiting.parent, /obj/energy_ball))
+ var/obj/energy_ball/orbitingball = orbiting.parent
orbitingball.orbiting_balls -= src
- orbitingball.dissipate_strength = orbitingball.orbiting_balls.len
. = ..()
if (!QDELETED(src))
qdel(src)
-/obj/singularity/energy_ball/proc/dust_mobs(atom/A)
+/obj/energy_ball/proc/dust_mobs(atom/A)
if(isliving(A))
var/mob/living/L = A
if(L.incorporeal_move || L.status_flags & GODMODE)
diff --git a/code/modules/shuttle/on_move.dm b/code/modules/shuttle/on_move.dm
index a55c153ab82..ff80e9a925d 100644
--- a/code/modules/shuttle/on_move.dm
+++ b/code/modules/shuttle/on_move.dm
@@ -38,7 +38,7 @@ All ShuttleMove procs go here
else //non-living mobs shouldn't be affected by shuttles, which is why this is an else
- if(istype(thing, /obj/singularity) && !istype(thing, /obj/singularity/narsie)) //it's a singularity but not a god, ignore it.
+ if(istype(thing, /obj/singularity) || istype(thing, /obj/energy_ball))
continue
if(!thing.anchored)
step(thing, shuttle_dir)
diff --git a/code/modules/shuttle/supply.dm b/code/modules/shuttle/supply.dm
index f1773c166ae..502d0a9a222 100644
--- a/code/modules/shuttle/supply.dm
+++ b/code/modules/shuttle/supply.dm
@@ -6,8 +6,8 @@ GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list(
/obj/item/disk/nuclear,
/obj/machinery/nuclearbomb,
/obj/item/beacon,
- /obj/singularity/narsie,
- /obj/singularity/wizard,
+ /obj/narsie,
+ /obj/tear_in_reality,
/obj/machinery/teleport/station,
/obj/machinery/teleport/hub,
/obj/machinery/quantumpad,
diff --git a/tgstation.dme b/tgstation.dme
index df8c7fc0865..fa7dbfadb83 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -480,6 +480,7 @@
#include "code\datums\components\rot.dm"
#include "code\datums\components\rotation.dm"
#include "code\datums\components\shrink.dm"
+#include "code\datums\components\singularity.dm"
#include "code\datums\components\sitcomlaughter.dm"
#include "code\datums\components\sizzle.dm"
#include "code\datums\components\slippery.dm"