diff --git a/code/__defines/machinery.dm b/code/__defines/machinery.dm
index 6f8bec025b..cae68c3a23 100644
--- a/code/__defines/machinery.dm
+++ b/code/__defines/machinery.dm
@@ -3,6 +3,10 @@ var/global/defer_powernet_rebuild = 0 // True if net rebuild will be called
#define CELLRATE 0.002 // Multiplier for watts per tick <> cell storage (e.g., 0.02 means if there is a load of 1000 watts, 20 units will be taken from a cell per second)
// It's a conversion constant. power_used*CELLRATE = charge_provided, or charge_used/CELLRATE = power_provided
+#define KILOWATTS *1000
+#define MEGAWATTS *1000000
+#define GIGAWATTS *1000000000
+
// Doors!
#define DOOR_CRUSH_DAMAGE 20
#define ALIEN_SELECT_AFK_BUFFER 1 // How many minutes that a person can be AFK before not being allowed to be an alien.
diff --git a/code/_helpers/unsorted.dm b/code/_helpers/unsorted.dm
index d244916ec2..4aa05ac244 100644
--- a/code/_helpers/unsorted.dm
+++ b/code/_helpers/unsorted.dm
@@ -1299,3 +1299,16 @@ var/mob/dview/dview_mob = new
tY = max(1, min(world.maxy, origin.y + (text2num(tY) - (world.view + 1))))
return locate(tX, tY, tZ)
+// Displays something as commonly used (non-submultiples) SI units.
+/proc/format_SI(var/number, var/symbol)
+ switch(round(abs(number)))
+ if(0 to 1000-1)
+ return "[number] [symbol]"
+ if(1e3 to 1e6-1)
+ return "[round(number / 1000, 0.1)] k[symbol]" // kilo
+ if(1e6 to 1e9-1)
+ return "[round(number / 1e6, 0.1)] m[symbol]" // mega
+ if(1e9 to 1e12-1) // Probably not needed but why not be complete?
+ return "[round(number / 1e9, 0.1)] g[symbol]" // giga
+ if(1e12 to 1e15-1)
+ return "[round(number / 1e12, 0.1)] t[symbol]" // tera
diff --git a/code/game/gamemodes/meteor/meteors.dm b/code/game/gamemodes/meteor/meteors.dm
index bd056f7c69..ad8b89ec80 100644
--- a/code/game/gamemodes/meteor/meteors.dm
+++ b/code/game/gamemodes/meteor/meteors.dm
@@ -80,6 +80,10 @@
var/turf/T = locate(endx, endy, Z)
return T
+// Override for special behavior when getting hit by meteors, and only meteors. Return one if the meteor hasn't been 'stopped'.
+/atom/proc/handle_meteor_impact(var/obj/effect/meteor/meteor)
+ return TRUE
+
///////////////////////
//The meteor effect
//////////////////////
@@ -101,6 +105,11 @@
var/meteordrop = /obj/item/weapon/ore/iron
var/dropamt = 2
+ // How much damage it does to walls, using take_damage().
+ // Normal walls will die to 150 or more, where as reinforced walls need 800 to penetrate. Durasteel walls need 1200 damage to go through.
+ // Multiply this and the hits var to get a rough idea of how penetrating a meteor is.
+ var/wall_power = 100
+
/obj/effect/meteor/New()
..()
z_original = z
@@ -132,8 +141,11 @@
/obj/effect/meteor/Bump(atom/A)
if(A)
- ram_turf(get_turf(A))
- get_hit()
+ if(A.handle_meteor_impact(src)) // Used for special behaviour when getting hit specifically by a meteor, like a shield.
+ ram_turf(get_turf(A))
+ get_hit()
+ else
+ die(0)
/obj/effect/meteor/CanPass(atom/movable/mover, turf/target, height=0, air_group=0)
return istype(mover, /obj/effect/meteor) ? 1 : ..()
@@ -146,7 +158,11 @@
//then, ram the turf if it still exists
if(T)
- T.ex_act(hitpwr)
+ if(istype(T, /turf/simulated/wall))
+ var/turf/simulated/wall/W = T
+ W.take_damage(wall_power) // Stronger walls can halt asteroids.
+ else
+ T.ex_act(hitpwr) // Floors and other things lack fancy health.
//process getting 'hit' by colliding with a dense object
@@ -154,9 +170,12 @@
/obj/effect/meteor/proc/get_hit()
hits--
if(hits <= 0)
- make_debris()
- meteor_effect()
- qdel(src)
+ die(1)
+
+/obj/effect/meteor/proc/die(var/explode = 1)
+ make_debris()
+ meteor_effect(explode)
+ qdel(src)
/obj/effect/meteor/ex_act()
return
@@ -172,21 +191,24 @@
var/obj/item/O = new meteordrop(get_turf(src))
O.throw_at(dest, 5, 10)
-/obj/effect/meteor/proc/meteor_effect()
+/obj/effect/meteor/proc/shake_players()
+ for(var/mob/M in player_list)
+ var/turf/T = get_turf(M)
+ if(!T || T.z != src.z)
+ continue
+ var/dist = get_dist(M.loc, src.loc)
+ shake_camera(M, dist > 20 ? 3 : 5, dist > 20 ? 1 : 3)
+
+/obj/effect/meteor/proc/meteor_effect(var/explode)
if(heavy)
- for(var/mob/M in player_list)
- var/turf/T = get_turf(M)
- if(!T || T.z != src.z)
- continue
- var/dist = get_dist(M.loc, src.loc)
- shake_camera(M, dist > 20 ? 3 : 5, dist > 20 ? 1 : 3)
+ shake_players()
///////////////////////
//Meteor types
///////////////////////
-//Dust
+// Dust breaks windows and hurts normal walls, generally more of an annoyance than a danger unless two happen to hit the same spot.
/obj/effect/meteor/dust
name = "space dust"
icon_state = "dust"
@@ -194,63 +216,74 @@
hits = 1
hitpwr = 3
meteordrop = /obj/item/weapon/ore/glass
+ wall_power = 50
-//Medium-sized
+// Medium-sized meteors aren't very special and can be stopped easily by r-walls.
/obj/effect/meteor/medium
name = "meteor"
dropamt = 3
+ wall_power = 200
-/obj/effect/meteor/medium/meteor_effect()
+/obj/effect/meteor/medium/meteor_effect(var/explode)
..()
- explosion(src.loc, 0, 1, 2, 3, 0)
+ if(explode)
+ explosion(src.loc, 0, 1, 2, 3, 0)
-//Large-sized
+// Large-sized meteors generally pack the most punch, but are more concentrated towards the epicenter.
/obj/effect/meteor/big
name = "large meteor"
icon_state = "large"
- hits = 6
+ hits = 8
heavy = 1
dropamt = 4
+ wall_power = 400
-/obj/effect/meteor/big/meteor_effect()
+/obj/effect/meteor/big/meteor_effect(var/explode)
..()
- explosion(src.loc, 1, 2, 3, 4, 0)
+ if(explode)
+ explosion(src.loc, devastation_range = 2, heavy_impact_range = 4, light_impact_range = 6, flash_range = 12, adminlog = 0)
-//Flaming meteor
+// 'Flaming' meteors do less overall damage but are spread out more due to a larger but weaker explosion at the end.
/obj/effect/meteor/flaming
name = "flaming meteor"
icon_state = "flaming"
hits = 5
heavy = 1
meteordrop = /obj/item/weapon/ore/phoron
+ wall_power = 100
-/obj/effect/meteor/flaming/meteor_effect()
+/obj/effect/meteor/flaming/meteor_effect(var/explode)
..()
- explosion(src.loc, 1, 2, 3, 4, 0, 0, 5)
+ if(explode)
+ explosion(src.loc, devastation_range = 1, heavy_impact_range = 2, light_impact_range = 8, flash_range = 16, adminlog = 0)
-//Radiation meteor
+// Irradiated meteors do less physical damage but project a ten-tile ranged pulse of radiation upon exploding.
/obj/effect/meteor/irradiated
name = "glowing meteor"
icon_state = "glowing"
heavy = 1
meteordrop = /obj/item/weapon/ore/uranium
+ wall_power = 75
-/obj/effect/meteor/irradiated/meteor_effect()
+/obj/effect/meteor/irradiated/meteor_effect(var/explode)
..()
- explosion(src.loc, 0, 0, 4, 3, 0)
+ if(explode)
+ explosion(src.loc, devastation_range = 0, heavy_impact_range = 0, light_impact_range = 4, flash_range = 6, adminlog = 0)
new /obj/effect/decal/cleanable/greenglow(get_turf(src))
- for(var/mob/living/L in view(5, src))
+ for(var/mob/living/L in view(10, src))
L.apply_effect(40, IRRADIATE)
+// This meteor fries toasters.
/obj/effect/meteor/emp
name = "conducting meteor"
icon_state = "glowing_blue"
desc = "Hide your floppies!"
meteordrop = /obj/item/weapon/ore/osmium
dropamt = 3
+ wall_power = 80
-/obj/effect/meteor/emp/meteor_effect()
+/obj/effect/meteor/emp/meteor_effect(var/explode)
..()
// Best case scenario: Comparable to a low-yield EMP grenade.
// Worst case scenario: Comparable to a standard yield EMP grenade.
@@ -265,10 +298,12 @@
hitpwr = 1
heavy = 1
meteordrop = /obj/item/weapon/ore/phoron
+ wall_power = 150
-/obj/effect/meteor/tunguska/meteor_effect()
+/obj/effect/meteor/tunguska/meteor_effect(var/explode)
..()
- explosion(src.loc, 5, 10, 15, 20, 0)
+ if(explode)
+ explosion(src.loc, 5, 10, 15, 20, 0)
/obj/effect/meteor/tunguska/Bump()
..()
diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm
index 97dd9f5fe0..6aeecfc8e5 100644
--- a/code/game/machinery/doors/airlock.dm
+++ b/code/game/machinery/doors/airlock.dm
@@ -944,7 +944,7 @@ About the new airlock wires panel:
healthcheck()
/obj/effect/energy_field/airlock_crush(var/crush_damage)
- Stress(crush_damage)
+ adjust_strength(crush_damage)
/obj/structure/closet/airlock_crush(var/crush_damage)
..()
diff --git a/code/modules/shieldgen/energy_field.dm b/code/modules/shieldgen/energy_field.dm
index 04b471182b..59f2f02ab6 100644
--- a/code/modules/shieldgen/energy_field.dm
+++ b/code/modules/shieldgen/energy_field.dm
@@ -1,61 +1,100 @@
//---------- actual energy field
+// Each field object has a strength var (mensured in "Renwicks").
+// Melee weapons do 5% of their normal (force var) damage, so a harmbaton would do 0.75 Renwick.
+// Projectiles do 5% of their structural damage, so a normal laser would do 2 Renwick damage.
+// For meteors, one Renwick is about equal to one layer of r-wall.
+// Meteors will be completely halted by the shield if the shield survives the impact.
+// Explosions do 4 Renwick of damage per severity, at a max of 12.
+
/obj/effect/energy_field
- name = "energy field"
+ name = "energy shield field"
desc = "Impenetrable field of energy, capable of blocking anything as long as it's active."
icon = 'icons/obj/machines/shielding.dmi'
- icon_state = "shieldsparkles"
+ icon_state = "shield"
+ alpha = 100
anchored = 1
layer = 4.1 //just above mobs
density = 0
- invisibility = 101
- var/strength = 0
+ var/obj/machinery/shield_gen/my_gen = null
+ var/strength = 0 // in Renwicks
var/ticks_recovering = 10
+ var/max_strength = 10
-/obj/effect/energy_field/New()
- ..()
+/obj/effect/energy_field/New(var/newloc, var/new_gen)
+ ..(newloc)
+ my_gen = new_gen
update_nearby_tiles()
/obj/effect/energy_field/Destroy()
update_nearby_tiles()
+ my_gen.field.Remove(src)
+ my_gen = null
+ var/turf/current_loc = get_turf(src)
+ spawn(1) // Updates neightbors after we're gone.
+ for(var/direction in cardinal)
+ var/turf/T = get_step(current_loc, direction)
+ if(T)
+ for(var/obj/effect/energy_field/F in T)
+ F.update_icon()
..()
/obj/effect/energy_field/ex_act(var/severity)
- Stress(0.5 + severity)
+ adjust_strength(-(4 - severity) * 4)
/obj/effect/energy_field/bullet_act(var/obj/item/projectile/Proj)
- Stress(Proj.get_structure_damage() / 10)
+ adjust_strength(-Proj.get_structure_damage() / 10)
-/obj/effect/energy_field/proc/Stress(var/severity)
- strength -= severity
+/obj/effect/energy_field/attackby(obj/item/W, mob/user)
+ if(W.force)
+ adjust_strength(-W.force / 20)
+ user.do_attack_animation(src)
+ user.setClickCooldown(DEFAULT_ATTACK_COOLDOWN)
+ ..()
- //if we take too much damage, drop out - the generator will bring us back up if we have enough power
- ticks_recovering = min(ticks_recovering + 2, 10)
- if(strength < 1)
- invisibility = 101
- density = 0
- ticks_recovering = 10
- strength = 0
- else if(strength >= 1)
- invisibility = 0
- density = 1
+/obj/effect/energy_field/attack_hand(var/mob/living/user)
+ impact_effect(3) // Harmless, but still produces the 'impact' effect.
+ ..()
-/obj/effect/energy_field/proc/Strengthen(var/severity)
- strength += severity
- if (strength < 0)
- strength = 0
+/obj/effect/energy_field/Bumped(atom/A)
+ ..(A)
+ impact_effect(2)
- //if we take too much damage, drop out - the generator will bring us back up if we have enough power
+/obj/effect/energy_field/handle_meteor_impact(var/obj/effect/meteor/meteor)
+ var/penetrated = TRUE
+ adjust_strength(-max((meteor.wall_power * meteor.hits) / 800, 0)) // One renwick (strength var) equals one r-wall for the purposes of meteor-stopping.
+ sleep(1)
+ if(density) // Check if we're still up.
+ penetrated = FALSE
+ explosion(meteor.loc, 0, 0, 0, 0, 0, 0, 0) // For the sound effect.
+
+ // Returning FALSE will kill the meteor.
+ return penetrated // If the shield's still around, the meteor was successfully stopped, otherwise keep going and plow into the station.
+
+/obj/effect/energy_field/proc/adjust_strength(amount, impact = 1)
var/old_density = density
- if(strength >= 1)
- invisibility = 0
- density = 1
- else if(strength < 1)
- invisibility = 101
- density = 0
-
- if (density != old_density)
+ strength = between(0, strength + amount, max_strength)
+
+ //maptext = "[round(strength, 0.1)]/[max_strength]"
+
+ //if we take too much damage, drop out - the generator will bring us back up if we have enough power
+ if(amount < 0) // Taking damage.
+ if(impact)
+ impact_effect(round(abs(amount * 2)))
+
+ ticks_recovering = min(ticks_recovering + 2, 10)
+ if(strength < 1) // We broke
+ density = 0
+ ticks_recovering = 10
+ strength = 0
+
+ else if(amount > 0) // Healing damage.
+ if(strength >= 1)
+ density = 1
+
+ if(density != old_density)
+ update_icon()
update_nearby_tiles()
/obj/effect/energy_field/CanPass(atom/movable/mover, turf/target, height=1.5, air_group = 0)
@@ -66,3 +105,44 @@
//return (!density || !height || air_group)
return !density
+
+/obj/effect/energy_field/update_icon(var/update_neightbors = 0)
+ overlays.Cut()
+ var/list/adjacent_shields_dir = list()
+ for(var/direction in cardinal)
+ var/turf/T = get_step(src, direction)
+ if(T) // Incase we somehow stepped off the map.
+ for(var/obj/effect/energy_field/F in T)
+ if(update_neightbors)
+ F.update_icon(0)
+ adjacent_shields_dir |= direction
+ break
+ // Icon_state and Glow
+ if(density)
+ icon_state = "shield"
+ set_light(3, 3, "#66FFFF")
+ else
+ icon_state = "shield_broken"
+ set_light(3, 5, "#FF9900")
+
+ // Edge overlays
+ for(var/found_dir in adjacent_shields_dir)
+ overlays += image(src.icon, src, icon_state = "shield_edge", dir = found_dir)
+
+
+// Small visual effect, makes the shield tiles brighten up by becoming more opaque for a moment, and spreads to nearby shields.
+/obj/effect/energy_field/proc/impact_effect(var/i, var/list/affected_shields = list())
+ i = between(1, i, 10)
+ alpha = 200
+ animate(src, alpha = initial(alpha), time = 1 SECOND)
+ affected_shields |= src
+ i--
+ if(i)
+ spawn(2)
+ for(var/direction in cardinal)
+ var/turf/T = get_step(src, direction)
+ if(T) // Incase we somehow stepped off the map.
+ for(var/obj/effect/energy_field/F in T)
+ if(!(F in affected_shields))
+ F.impact_effect(i, affected_shields) // Spread the effect to them.
+
diff --git a/code/modules/shieldgen/handheld_defuser.dm b/code/modules/shieldgen/handheld_defuser.dm
new file mode 100644
index 0000000000..a2909fa7e6
--- /dev/null
+++ b/code/modules/shieldgen/handheld_defuser.dm
@@ -0,0 +1,49 @@
+/obj/item/weapon/shield_diffuser
+ name = "portable shield diffuser"
+ desc = "A small handheld device designed to disrupt energy barriers"
+ description_info = "This device disrupts shields on directly adjacent tiles (in a + shaped pattern), in a similar way the floor mounted variant does. It is, however, portable and run by an internal battery. Can be recharged with a regular recharger."
+ icon = 'icons/obj/machines/shielding.dmi'
+ icon_state = "hdiffuser_off"
+ var/obj/item/weapon/cell/device/cell
+ var/enabled = 0
+
+/obj/item/weapon/shield_diffuser/update_icon()
+ if(enabled)
+ icon_state = "hdiffuser_on"
+ else
+ icon_state = "hdiffuser_off"
+
+/obj/item/weapon/shield_diffuser/New()
+ cell = new(src)
+ ..()
+
+/obj/item/weapon/shield_diffuser/Destroy()
+ qdel(cell)
+ cell = null
+ if(enabled)
+ processing_objects.Remove(src)
+ . = ..()
+
+/obj/item/weapon/shield_diffuser/process()
+ if(!enabled)
+ return
+
+ for(var/direction in cardinal)
+ var/turf/simulated/shielded_tile = get_step(get_turf(src), direction)
+ for(var/obj/effect/energy_field/S in shielded_tile)
+ if(istype(S) && cell.checked_use(10 KILOWATTS * CELLRATE))
+ qdel(S)
+
+/obj/item/weapon/shield_diffuser/attack_self()
+ enabled = !enabled
+ update_icon()
+ if(enabled)
+ processing_objects.Add(src)
+ else
+ processing_objects.Remove(src)
+ to_chat(usr, "You turn \the [src] [enabled ? "on" : "off"].")
+
+/obj/item/weapon/shield_diffuser/examine()
+ . = ..()
+ to_chat(usr, "The charge meter reads [cell ? cell.percent() : 0]%")
+ to_chat(usr, "It is [enabled ? "enabled" : "disabled"].")
\ No newline at end of file
diff --git a/code/modules/shieldgen/shield_capacitor.dm b/code/modules/shieldgen/shield_capacitor.dm
index 45d7e311dd..53ddcec06e 100644
--- a/code/modules/shieldgen/shield_capacitor.dm
+++ b/code/modules/shieldgen/shield_capacitor.dm
@@ -19,14 +19,6 @@
var/charge_rate = 100000 //100 kW
var/obj/machinery/shield_gen/owned_gen
-/obj/machinery/shield_capacitor/New()
- spawn(10)
- for(var/obj/machinery/shield_gen/possible_gen in range(1, src))
- if(get_dir(src, possible_gen) == src.dir)
- possible_gen.owned_capacitor = src
- break
- ..()
-
/obj/machinery/shield_capacitor/emag_act(var/remaining_charges, var/mob/user)
if(prob(75))
src.locked = !src.locked
@@ -54,13 +46,13 @@
if(anchored)
spawn(0)
for(var/obj/machinery/shield_gen/gen in range(1, src))
- if(get_dir(src, gen) == src.dir && !gen.owned_capacitor)
+ if(get_dir(src, gen) == src.dir)
owned_gen = gen
- owned_gen.owned_capacitor = src
+ owned_gen.capacitors |= src
owned_gen.updateDialog()
else
- if(owned_gen && owned_gen.owned_capacitor == src)
- owned_gen.owned_capacitor = null
+ if(owned_gen && src in owned_gen.capacitors)
+ owned_gen.capacitors -= src
owned_gen = null
else
..()
@@ -82,16 +74,16 @@
else
t += "This capacitor is: [active ? "Online" : "Offline" ] [active ? "\[Deactivate\]" : "\[Activate\]"]
"
t += "Capacitor Status: [time_since_fail > 2 ? "OK." : "Discharging!"]
"
- t += "Stored Energy: [round(stored_charge/1000, 0.1)] kJ ([100 * round(stored_charge/max_charge, 0.1)]%)
"
+ t += "Stored Energy: [format_SI(stored_charge, "J")] ([100 * round(stored_charge/max_charge, 0.01)]%)
"
t += "Charge Rate: \
\[----\] \
\[---\] \
\[--\] \
- \[-\][charge_rate] W \
+ \[-\][format_SI(charge_rate, "W")]\
\[+\] \
\[++\] \
\[+++\] \
- \[+++\]
"
+ \[++++\]
"
t += "