code:
/obj/projectile/gravityrepulse
	name = "repulsion bolt"
	icon = 'icons/effects/effects.dmi'
	icon_state = "chronofield"
	hitsound = 'sound/weapons/wave.ogg'
	damage = 0
	damage_type = BRUTE
	nodamage = TRUE
	color = "#33CCFF"
	var/turf/T
	var/power = 4
	var/list/thrown_items = list()

/obj/projectile/gravityrepulse/Initialize(mapload)
	. = ..()
	var/obj/item/ammo_casing/energy/gravity/repulse/C = loc
	if(istype(C)) //Hard-coded maximum power so servers can't be crashed by trying to throw the entire Z level's items
		power = min(C.gun?.power, 15)

/obj/projectile/gravityrepulse/on_hit()
	. = ..()
	T = get_turf(src)
	for(var/atom/movable/A in range(T, power))
		if(A == src || (firer && A == src.firer) || A.anchored || thrown_items[A])
			continue
		if(ismob(A)) //because (ismob(A) && A:mob_negates_gravity()) is a recipe for bugs.
			var/mob/M = A
			if(M.mob_negates_gravity())
				continue
		var/throwtarget = get_edge_target_turf(src, get_dir(src, get_step_away(A, src)))
		A.safe_throw_at(throwtarget,power+1,1, force = MOVE_FORCE_EXTREMELY_STRONG)
		thrown_items[A] = A
	for(var/turf/F in RANGE_TURFS(power, T))
		new /obj/effect/temp_visual/gravpush(F)

/obj/projectile/gravityattract
	name = "attraction bolt"
	icon = 'icons/effects/effects.dmi'
	icon_state = "chronofield"
	hitsound = 'sound/weapons/wave.ogg'
	damage = 0
	damage_type = BRUTE
	nodamage = TRUE
	color = "#FF6600"
	var/turf/T
	var/power = 4
	var/list/thrown_items = list()

/obj/projectile/gravityattract/Initialize(mapload)
	. = ..()
	var/obj/item/ammo_casing/energy/gravity/attract/C = loc
	if(istype(C)) //Hard-coded maximum power so servers can't be crashed by trying to throw the entire Z level's items
		power = min(C.gun?.power, 15)

/obj/projectile/gravityattract/on_hit()
	. = ..()
	T = get_turf(src)
	for(var/atom/movable/A in range(T, power))
		if(A == src || (firer && A == src.firer) || A.anchored || thrown_items[A])
			continue
		if(ismob(A))
			var/mob/M = A
			if(M.mob_negates_gravity())
				continue
		A.safe_throw_at(T, power+1, 1, force = MOVE_FORCE_EXTREMELY_STRONG)
		thrown_items[A] = A
	for(var/turf/F in RANGE_TURFS(power, T))
		new /obj/effect/temp_visual/gravpush(F)

/obj/projectile/gravitychaos
	name = "gravitational blast"
	icon = 'icons/effects/effects.dmi'
	icon_state = "chronofield"
	hitsound = 'sound/weapons/wave.ogg'
	damage = 0
	damage_type = BRUTE
	nodamage = TRUE
	color = "#101010"
	var/turf/T
	var/power = 4
	var/list/thrown_items = list()

/obj/projectile/gravitychaos/Initialize(mapload)
	. = ..()
	var/obj/item/ammo_casing/energy/gravity/chaos/C = loc
	if(istype(C)) //Hard-coded maximum power so servers can't be crashed by trying to throw the entire Z level's items
		power = min(C.gun?.power, 15)

/obj/projectile/gravitychaos/on_hit()
	. = ..()
	T = get_turf(src)
	for(var/atom/movable/A in range(T, power))
		if(A == src|| (firer && A == src.firer) || A.anchored || thrown_items[A])
			continue
		if(ismob(A))
			var/mob/M = A
			if(M.mob_negates_gravity())
				continue
		A.safe_throw_at(get_edge_target_turf(A, pick(GLOB.cardinals)), power+1, 1, force = MOVE_FORCE_EXTREMELY_STRONG)
		thrown_items[A] = A
	for(var/turf/Z as anything in RANGE_TURFS(power,T))
		new /obj/effect/temp_visual/gravpush(Z)


code:
/obj/projectile/hallucination
	name = "bullet"
	icon = null
	icon_state = null
	hitsound = ""
	suppressed = TRUE
	ricochets_max = 0
	ricochet_chance = 0
	damage = 0
	nodamage = TRUE
	projectile_type = /obj/projectile/hallucination
	log_override = TRUE
	var/hal_icon_state
	var/image/fake_icon
	var/mob/living/carbon/hal_target
	var/hal_fire_sound
	var/hal_hitsound
	var/hal_hitsound_wall
	var/hal_impact_effect
	var/hal_impact_effect_wall
	var/hit_duration
	var/hit_duration_wall

/obj/projectile/hallucination/fire()
	..()
	fake_icon = image('icons/obj/guns/projectiles.dmi', src, hal_icon_state, ABOVE_MOB_LAYER)
	if(hal_target.client)
		hal_target.client.images += fake_icon

/obj/projectile/hallucination/Destroy()
	if(hal_target?.client)
		hal_target.client.images -= fake_icon
	QDEL_NULL(fake_icon)
	return ..()

/obj/projectile/hallucination/Bump(atom/A)
	if(!ismob(A))
		if(hal_hitsound_wall)
			hal_target.playsound_local(loc, hal_hitsound_wall, 40, 1)
		if(hal_impact_effect_wall)
			spawn_hit(A, TRUE)
	else if(A == hal_target)
		if(hal_hitsound)
			hal_target.playsound_local(A, hal_hitsound, 100, 1)
		target_on_hit(A)
	qdel(src)
	return TRUE

/obj/projectile/hallucination/proc/target_on_hit(mob/M)
	if(M == hal_target)
		to_chat(hal_target, span_userdanger("[M] is hit by \a [src] in the chest!"))
		hal_apply_effect()
	else if(M in view(hal_target))
		to_chat(hal_target, span_danger("[M] is hit by \a [src] in the chest!!"))
	if(damage_type == BRUTE)
		var/splatter_dir = dir
		if(starting)
			splatter_dir = get_dir(starting, get_turf(M))
		spawn_blood(M, splatter_dir)
	else if(hal_impact_effect)
		spawn_hit(M, FALSE)

/obj/projectile/hallucination/proc/spawn_blood(mob/M, set_dir)
	set waitfor = 0
	if(!hal_target.client)
		return

	var/splatter_icon_state
	if(ISDIAGONALDIR(set_dir))
		splatter_icon_state = "splatter[pick(1, 2, 6)]"
	else
		splatter_icon_state = "splatter[pick(3, 4, 5)]"

	var/image/blood = image('icons/effects/blood.dmi', M, splatter_icon_state, ABOVE_MOB_LAYER)
	var/target_pixel_x = 0
	var/target_pixel_y = 0
	switch(set_dir)
		if(NORTH)
			target_pixel_y = 16
		if(SOUTH)
			target_pixel_y = -16
			layer = ABOVE_MOB_LAYER
		if(EAST)
			target_pixel_x = 16
		if(WEST)
			target_pixel_x = -16
		if(NORTHEAST)
			target_pixel_x = 16
			target_pixel_y = 16
		if(NORTHWEST)
			target_pixel_x = -16
			target_pixel_y = 16
		if(SOUTHEAST)
			target_pixel_x = 16
			target_pixel_y = -16
			layer = ABOVE_MOB_LAYER
		if(SOUTHWEST)
			target_pixel_x = -16
			target_pixel_y = -16
			layer = ABOVE_MOB_LAYER
	hal_target.client.images += blood
	animate(blood, pixel_x = target_pixel_x, pixel_y = target_pixel_y, alpha = 0, time = 5)
	addtimer(CALLBACK(src, .proc/cleanup_blood), 5)

/obj/projectile/hallucination/proc/cleanup_blood(image/blood)
	hal_target.client.images -= blood
	qdel(blood)

/obj/projectile/hallucination/proc/spawn_hit(atom/A, is_wall)
	set waitfor = 0
	if(!hal_target.client)
		return

	var/image/hit_effect = image('icons/effects/blood.dmi', A, is_wall ? hal_impact_effect_wall : hal_impact_effect, ABOVE_MOB_LAYER)
	hit_effect.pixel_x = A.pixel_x + rand(-4,4)
	hit_effect.pixel_y = A.pixel_y + rand(-4,4)
	hal_target.client.images += hit_effect
	sleep(is_wall ? hit_duration_wall : hit_duration)
	hal_target.client.images -= hit_effect
	qdel(hit_effect)

/obj/projectile/hallucination/proc/hal_apply_effect()
	return

/obj/projectile/hallucination/bullet
	name = "bullet"
	hal_icon_state = "bullet"
	hal_fire_sound = "gunshot"
	hal_hitsound = 'sound/weapons/pierce.ogg'
	hal_hitsound_wall = "ricochet"
	hal_impact_effect = "impact_bullet"
	hal_impact_effect_wall = "impact_bullet"
	hit_duration = 5
	hit_duration_wall = 5

/obj/projectile/hallucination/bullet/hal_apply_effect()
	hal_target.adjustStaminaLoss(60)

/obj/projectile/hallucination/laser
	name = "laser"
	damage_type = BURN
	hal_icon_state = "laser"
	hal_fire_sound = 'sound/weapons/laser.ogg'
	hal_hitsound = 'sound/weapons/sear.ogg'
	hal_hitsound_wall = 'sound/weapons/effects/searwall.ogg'
	hal_impact_effect = "impact_laser"
	hal_impact_effect_wall = "impact_laser_wall"
	hit_duration = 4
	hit_duration_wall = 10
	pass_flags = PASSTABLE | PASSGLASS | PASSGRILLE

/obj/projectile/hallucination/laser/hal_apply_effect()
	hal_target.adjustStaminaLoss(20)
	hal_target.blur_eyes(2)

/obj/projectile/hallucination/taser
	name = "electrode"
	damage_type = BURN
	hal_icon_state = "spark"
	color = "#FFFF00"
	hal_fire_sound = 'sound/weapons/taser.ogg'
	hal_hitsound = 'sound/weapons/taserhit.ogg'
	hal_hitsound_wall = null
	hal_impact_effect = null
	hal_impact_effect_wall = null

/obj/projectile/hallucination/taser/hal_apply_effect()
	hal_target.Paralyze(100)
	hal_target.stuttering += 20
	if(hal_target.dna && hal_target.dna.check_mutation(HULK))
		hal_target.say(pick(";RAAAAAAAARGH!", ";HNNNNNNNNNGGGGGGH!", ";GWAAAAAAAARRRHHH!", "NNNNNNNNGGGGGGGGHH!", ";AAAAAAARRRGH!" ), forced = "hulk")
	else if((hal_target.status_flags & CANKNOCKDOWN) && !HAS_TRAIT(hal_target, TRAIT_STUNIMMUNE))
		addtimer(CALLBACK(hal_target, /mob/living/carbon.proc/do_jitter_animation, 20), 5)

/obj/projectile/hallucination/disabler
	name = "disabler beam"
	damage_type = STAMINA
	hal_icon_state = "omnilaser"
	hal_fire_sound = 'sound/weapons/taser2.ogg'
	hal_hitsound = 'sound/weapons/tap.ogg'
	hal_hitsound_wall = 'sound/weapons/effects/searwall.ogg'
	hal_impact_effect = "impact_laser_blue"
	hal_impact_effect_wall = null
	hit_duration = 4
	pass_flags = PASSTABLE | PASSGLASS | PASSGRILLE

/obj/projectile/hallucination/disabler/hal_apply_effect()
	hal_target.adjustStaminaLoss(25)

/obj/projectile/hallucination/ebow
	name = "bolt"
	damage_type = TOX
	hal_icon_state = "cbbolt"
	hal_fire_sound = 'sound/weapons/genhit.ogg'
	hal_hitsound = null
	hal_hitsound_wall = null
	hal_impact_effect = null
	hal_impact_effect_wall = null

/obj/projectile/hallucination/ebow/hal_apply_effect()
	hal_target.Paralyze(100)
	hal_target.stuttering += 5
	hal_target.adjustStaminaLoss(8)

/obj/projectile/hallucination/change
	name = "bolt of change"
	damage_type = BURN
	hal_icon_state = "ice_1"
	hal_fire_sound = 'sound/magic/staff_change.ogg'
	hal_hitsound = null
	hal_hitsound_wall = null
	hal_impact_effect = null
	hal_impact_effect_wall = null

/obj/projectile/hallucination/change/hal_apply_effect()
	new /datum/hallucination/self_delusion(hal_target, TRUE, wabbajack = FALSE)

/obj/projectile/hallucination/death
	name = "bolt of death"
	damage_type = BURN
	hal_icon_state = "pulse1_bl"
	hal_fire_sound = 'sound/magic/wandodeath.ogg'
	hal_hitsound = null
	hal_hitsound_wall = null
	hal_impact_effect = null
	hal_impact_effect_wall = null

/obj/projectile/hallucination/death/hal_apply_effect()
	new /datum/hallucination/death(hal_target, TRUE)

repo_name: AnalWerewolf/sundown

license: mit


code:
/obj/projectile/ion
	name = "ion bolt"
	icon_state = "ion"
	damage = 0
	damage_type = BURN
	nodamage = TRUE
	flag = ENERGY
	impact_effect_type = /obj/effect/temp_visual/impact_effect/ion
	var/emp_radius = 1

/obj/projectile/ion/on_hit(atom/target, blocked = FALSE)
	..()
	empulse(target, emp_radius, emp_radius)
	return BULLET_ACT_HIT

/obj/projectile/ion/weak
	emp_radius = 0


code:
/obj/projectile/meteor
	name = "meteor"
	icon = 'icons/obj/meteor.dmi'
	icon_state = "small1"
	damage = 90
	paralyze = 100
	dismemberment = 90
	armour_penetration = 100
	damage_type = BRUTE
	flag = BULLET

/obj/projectile/meteor/on_hit(atom/target, blocked = FALSE)
	. = ..()
	if(. == BULLET_ACT_HIT && isliving(target))
		explosion(target, devastation_range = -1, light_impact_range = 2, flame_range = 0, flash_range = 1, adminlog = FALSE)
		playsound(target.loc, 'sound/effects/meteorimpact.ogg', 40, TRUE)

/obj/projectile/meteor/Bump(atom/hit_target)
	if(hit_target == firer)
		forceMove(hit_target.loc)
		return
	if(isobj(hit_target))
		SSexplosions.med_mov_atom += hit_target
	if(isturf(hit_target))
		SSexplosions.medturf += hit_target
	playsound(src.loc, 'sound/effects/meteorimpact.ogg', 40, TRUE)
	for(var/mob/onlookers_in_range in urange(10, src))
		if(!onlookers_in_range.stat)
			shake_camera(onlookers_in_range, 3, 1)
	qdel(src)


code:
/obj/projectile/beam/mindflayer
	name = "flayer ray"

/obj/projectile/beam/mindflayer/on_hit(atom/target, blocked = FALSE)
	. = ..()
	if(ishuman(target))
		var/mob/living/carbon/human/M = target
		M.adjustOrganLoss(ORGAN_SLOT_BRAIN, 20)
		M.hallucination += 30


code:
/obj/projectile/neurotoxin
	name = "neurotoxin spit"
	icon_state = "neurotoxin"
	damage = 5
	damage_type = TOX
	nodamage = FALSE
	paralyze = 100
	flag = BIO
	impact_effect_type = /obj/effect/temp_visual/impact_effect/neurotoxin

/obj/projectile/neurotoxin/on_hit(atom/target, blocked = FALSE)
	if(isalien(target))
		paralyze = 0
		nodamage = TRUE
	return ..()


code:
/obj/projectile/plasma
	name = "plasma blast"
	icon_state = "plasmacutter"
	damage_type = BRUTE
	damage = 5
	range = 4
	dismemberment = 20
	impact_effect_type = /obj/effect/temp_visual/impact_effect/purple_laser
	var/mine_range = 3 //mines this many additional tiles of rock
	tracer_type = /obj/effect/projectile/tracer/plasma_cutter
	muzzle_type = /obj/effect/projectile/muzzle/plasma_cutter
	impact_type = /obj/effect/projectile/impact/plasma_cutter

/obj/projectile/plasma/on_hit(atom/target)
	. = ..()
	if(ismineralturf(target))
		var/turf/closed/mineral/M = target
		M.gets_drilled(firer, FALSE)
		if(mine_range)
			mine_range--
			range++
		if(range > 0)
			return BULLET_ACT_FORCE_PIERCE

/obj/projectile/plasma/adv
	damage = 7
	range = 5
	mine_range = 5

/obj/projectile/plasma/adv/mech
	damage = 10
	range = 9
	mine_range = 3

/obj/projectile/plasma/turret
	//Between normal and advanced for damage, made a beam so not the turret does not destroy glass
	name = "plasma beam"
	damage = 24
	range = 7
	pass_flags = PASSTABLE | PASSGLASS | PASSGRILLE


code:
/obj/projectile/bullet/gyro
	name ="explosive bolt"
	icon_state= "bolter"
	damage = 50
	embedding = null
	shrapnel_type = null

/obj/projectile/bullet/gyro/on_hit(atom/target, blocked = FALSE)
	..()
	explosion(target, devastation_range = -1, light_impact_range = 2, explosion_cause = src)
	return BULLET_ACT_HIT

/// PM9 HEDP rocket
/obj/projectile/bullet/a84mm
	name ="\improper HEDP rocket"
	desc = "USE A WEEL GUN"
	icon_state= "84mm-hedp"
	damage = 80
	armour_penetration = 100
	dismemberment = 100
	embedding = null
	shrapnel_type = null
	/// Whether we do extra damage when hitting a mech or silicon
	var/anti_armour_damage = 200

/obj/projectile/bullet/a84mm/on_hit(atom/target, blocked = FALSE)
	if(isliving(target) && prob(1))
		var/mob/living/gibbed_dude = target
		if(gibbed_dude.stat < HARD_CRIT)
			gibbed_dude.say("Is that a fucking ro-", forced = "hit by rocket")
	..()

	do_boom(target)
	if(anti_armour_damage && ismecha(target))
		var/obj/vehicle/sealed/mecha/M = target
		M.take_damage(anti_armour_damage)
	if(issilicon(target))
		var/mob/living/silicon/S = target
		S.take_overall_damage(anti_armour_damage*0.75, anti_armour_damage*0.25)
	return BULLET_ACT_HIT

/// Since some rockets have different booms depending if they hit a living target or not, this is easier than having explosive radius vars
/obj/projectile/bullet/a84mm/proc/do_boom(atom/target)
	explosion(target, devastation_range = -1, heavy_impact_range = 1, light_impact_range = 3, flame_range = 4, flash_range = 1, adminlog = FALSE)

/// PM9 standard rocket
/obj/projectile/bullet/a84mm/he
	name ="\improper HE missile"
	desc = "Boom."
	icon_state = "missile"
	damage = 50
	anti_armour_damage = 0

/obj/projectile/bullet/a84mm/he/do_boom(atom/target, blocked=0)
	if(!isliving(target)) //if the target isn't alive, so is a wall or something
		explosion(target, heavy_impact_range = 1, light_impact_range = 2, flame_range = 3, flash_range = 4, explosion_cause = src)
	else
		explosion(target, light_impact_range = 2, flame_range = 3, flash_range = 4,  explosion_cause = src)

/// PM9 weak rocket
/obj/projectile/bullet/a84mm/weak
	name ="low-yield HE missile"
	desc = "Boom, but less so."
	damage = 30
	anti_armour_damage = 0

/obj/projectile/bullet/a84mm/weak/do_boom(atom/target, blocked=0)
	if(!isliving(target)) //if the target isn't alive, so is a wall or something
		explosion(target, heavy_impact_range = 1, light_impact_range = 2, flame_range = 3, flash_range = 4, explosion_cause = src)
	else
		explosion(target, light_impact_range = 2, flame_range = 3, flash_range = 4, explosion_cause = src)

/// Mech BRM-6 missile
/obj/projectile/bullet/a84mm_br
	name ="\improper HE missile"
	desc = "Boom."
	icon_state = "missile"
	damage = 30
	ricochets_max = 0 //it's a MISSILE
	embedding = null
	shrapnel_type = null
	var/sturdy = list(
	/turf/closed,
	/obj/vehicle/sealed/mecha,
	/obj/machinery/door,
	/obj/structure/window,
	/obj/structure/grille
	)

/obj/item/broken_missile
	name = "\improper broken missile"
	desc = "A missile that did not detonate. The tail has snapped and it is in no way fit to be used again."
	icon = 'icons/obj/guns/projectiles.dmi'
	icon_state = "missile_broken"
	w_class = WEIGHT_CLASS_TINY

/obj/projectile/bullet/a84mm_br/on_hit(atom/target, blocked=0)
	..()
	for(var/i in sturdy)
		if(istype(target, i))
			explosion(target, heavy_impact_range = 1, light_impact_range = 1, flash_range = 2, explosion_cause = src)
			return BULLET_ACT_HIT
	//if(istype(target, /turf/closed) || ismecha(target))
	new /obj/item/broken_missile(get_turf(src), 1)


code:
/obj/projectile/temp
	name = "freeze beam"
	icon_state = "ice_2"
	damage = 0
	damage_type = BURN
	nodamage = FALSE
	flag = ENERGY
	var/temperature = -50 // reduce the body temperature by 50 points

/obj/projectile/temp/on_hit(atom/target, blocked = 0)
	. = ..()
	if(iscarbon(target))
		var/mob/living/carbon/hit_mob = target
		var/thermal_protection = 1 - hit_mob.get_insulation_protection(hit_mob.bodytemperature + temperature)

		// The new body temperature is adjusted by the bullet's effect temperature
		// Reduce the amount of the effect temperature change based on the amount of insulation the mob is wearing
		hit_mob.adjust_bodytemperature((thermal_protection * temperature) + temperature)

	else if(isliving(target))
		var/mob/living/L = target
		// the new body temperature is adjusted by the bullet's effect temperature
		L.adjust_bodytemperature((1 - blocked) * temperature)

/obj/projectile/temp/hot
	name = "heat beam"
	temperature = 100 // Raise the body temp by 100 points

/obj/projectile/temp/cryo
	name = "cryo beam"
	range = 3
	temperature = -240 // Single slow shot reduces temp greatly

/obj/projectile/temp/cryo/on_range()
	var/turf/T = get_turf(src)
	if(isopenturf(T))
		var/turf/open/O = T
		O.freeze_turf()
	return ..()


code:
/obj/projectile/beam/wormhole
	name = "bluespace beam"
	icon_state = "spark"
	hitsound = "sparks"
	damage = 0
	nodamage = TRUE
	pass_flags = PASSGLASS | PASSTABLE | PASSGRILLE | PASSMOB
	//Weakref to the thing that shot us
	var/datum/weakref/gun
	color = "#33CCFF"
	tracer_type = /obj/effect/projectile/tracer/wormhole
	impact_type = /obj/effect/projectile/impact/wormhole
	muzzle_type = /obj/effect/projectile/muzzle/wormhole
	hitscan = TRUE

/obj/projectile/beam/wormhole/orange
	name = "orange bluespace beam"
	color = "#FF6600"

/obj/projectile/beam/wormhole/Initialize(mapload, obj/item/ammo_casing/energy/wormhole/casing)
	. = ..()
	if(casing)
		gun = casing.gun

/obj/projectile/beam/wormhole/on_hit(atom/target)
	var/obj/item/gun/energy/wormhole_projector/projector = gun.resolve()
	if(!projector)
		qdel(src)
		return
	projector.create_portal(src, get_turf(src))


code:
// Replaces chemgrenade stuff, allowing reagent explosions to be called from anywhere.
// It should be called using a location, the range, and a list of reagents involved.

// Threatscale is a multiplier for the 'threat' of the grenade. If you're increasing the affected range drastically, you might want to improve this.
// Extra heat affects the temperature of the mixture, and may cause it to react in different ways.

/proc/chem_splash(turf/epicenter, affected_range = 3, list/datum/reagents/reactants = list(), extra_heat = 0, threatscale = 1, adminlog = 1)
	if(!isturf(epicenter) || !reactants.len || threatscale <= 0)
		return
	var/has_reagents
	var/total_reagents
	for(var/datum/reagents/R in reactants)
		if(R.total_volume)
			has_reagents = 1
			total_reagents += R.total_volume

	if(!has_reagents)
		return

	var/datum/reagents/splash_holder = new/datum/reagents(total_reagents*threatscale)
	splash_holder.my_atom = epicenter // For some reason this is setting my_atom to null, and causing runtime errors.
	var/total_temp = 0

	for(var/datum/reagents/R in reactants)
		R.trans_to(splash_holder, R.total_volume, threatscale, 1, 1)
		total_temp += R.chem_temp
	splash_holder.chem_temp = (total_temp/reactants.len) + extra_heat // Average temperature of reagents + extra heat.
	splash_holder.handle_reactions() // React them now.

	if(splash_holder.total_volume && affected_range >= 0) //The possible reactions didnt use up all reagents, so we spread it around.
		var/datum/effect_system/steam_spread/steam = new /datum/effect_system/steam_spread()
		steam.set_up(10, 0, epicenter)
		steam.attach(epicenter)
		steam.start()

		var/list/viewable = view(affected_range, epicenter)

		var/list/accessible = list(epicenter)
		for(var/i=1; i<=affected_range; i++)
			var/list/turflist = list()
			for(var/turf/T in (orange(i, epicenter) - orange(i-1, epicenter)))
				turflist |= T
			for(var/turf/T in turflist)
				if(!(get_dir(T,epicenter) in GLOB.cardinals) && (abs(T.x - epicenter.x) == abs(T.y - epicenter.y) ))
					turflist.Remove(T)
					turflist.Add(T) // we move the purely diagonal turfs to the end of the list.
			for(var/turf/T in turflist)
				if(accessible[T])
					continue
				for(var/thing in T.get_atmos_adjacent_turfs(alldir = TRUE))
					var/turf/NT = thing
					if(!(NT in accessible))
						continue
					if(!(get_dir(T,NT) in GLOB.cardinals))
						continue
					accessible[T] = 1
					break
		var/list/reactable = accessible
		for(var/turf/T in accessible)
			for(var/atom/A in T.get_all_contents())
				if(!(A in viewable))
					continue
				reactable |= A
			if(extra_heat >= 300)
				T.hotspot_expose(extra_heat*2, 5)
		if(!reactable.len) //Nothing to react with. Probably means we're in nullspace.
			return
		for(var/thing in reactable)
			var/atom/A = thing
			var/distance = max(1,get_dist(A, epicenter))
			var/fraction = 0.5/(2 ** distance) //50/25/12/6... for a 200u splash, 25/12/6/3... for a 100u, 12/6/3/1 for a 50u
			splash_holder.expose(A, TOUCH, fraction)

	//SEPTIC EDIT BEGIN
	epicenter.add_liquid_from_reagents(splash_holder)
	//SEPTIC EDIT END
	qdel(splash_holder)
	return 1


code:
//Generates a wikitable txt file for use with the wiki - does not support productless reactions at the moment
/client/proc/generate_wikichem_list()
	set category = "Debug"
	set name = "Parse Wikichems"

	//If we're a reaction product
	var/prefix_reaction = {"{| class=\"wikitable sortable\" style=\"width:100%; text-align:left; border: 3px solid #FFDD66; cellspacing=0; cellpadding=2; background-color:white;\"
! scope=\"col\" style='width:150px; background-color:#FFDD66;'|Name
! scope=\"col\" class=\"unsortable\" style='background-color:#FFDD66;'|Formula
! scope=\"col\" class=\"unsortable\" style='background-color:#FFDD66; width:170px;'|Reaction conditions
! scope=\"col\" class=\"unsortable\" style='background-color:#FFDD66;'|Description
! scope=\"col\" class=\"unsortable\" style='background-color:#FFDD66;'|Chemical properties
|-
"}

	var/input_text = stripped_input(usr, "Input a name of a reagent, or a series of reagents split with a comma (no spaces) to get it's wiki table entry", "Recipe") //95% of the time, the reagent type is a lowercase, no spaces / underscored version of the name
	if(!input_text)
		to_chat(usr, "Input was blank!")
		return
	text2file(prefix_reaction, "[GLOB.log_directory]/chem_parse.txt")
	var/list/names = splittext("[input_text]", ",")

	for(var/name in names)
		var/datum/reagent/reagent = find_reagent_object_from_type(get_chem_id(name))
		if(!reagent)
			to_chat(usr, "Could not find [name]. Skipping.")
			continue
		//Get reaction
		var/list/reactions = GLOB.chemical_reactions_list_product_index[reagent.type]

		if(!length(reactions))
			to_chat(usr, "Could not find [name] reaction! Continuing anyways.")
			var/single_parse = generate_chemwiki_line(reagent, null)
			text2file(single_parse, "[GLOB.log_directory]/chem_parse.txt")
			continue

		for(var/datum/chemical_reaction/reaction as anything in reactions)
			var/single_parse = generate_chemwiki_line(reagent, reaction)
			text2file(single_parse, "[GLOB.log_directory]/chem_parse.txt")
	text2file("|}", "[GLOB.log_directory]/chem_parse.txt") //Cap off the table
	to_chat(usr, "Done! Saved file to (wherever your root folder is, i.e. where the DME is)/[GLOB.log_directory]/chem_parse.txt OR use the Get Current Logs verb under the Admin tab. (if you click Open, and it does nothing, that's because you've not set a .txt default program! Try downloading it instead, and use that file to set a default program! Have a nice day!")

/// Generate the big list of reagent based reactions.
/proc/generate_chemwiki_line(datum/reagent/reagent, datum/chemical_reaction/reaction)
	//name | Reagent pH | reagents | reaction temp | Overheat temp | pH range | Kinetics | description | OD level | Addiction level | Metabolism rate | impure chem | inverse chem

	//NAME
	//!style='background-color:#FFEE88;'|{{anchor|Synthetic-derived growth factor}}Synthetic-derived growth factor▮
	var/outstring = "!style='background-color:#FFEE88;'|{{anchor|[reagent.name]}}[reagent.name]▮"
	//Impurities
	if(istype(reagent, /datum/reagent/impurity))
		outstring += "\n
Impure reagent"

	if(istype(reagent, /datum/reagent/inverse))
		outstring += "\n
Inverse reagent"

	else
		var/datum/reagent/impure_reagent = GLOB.chemical_reagents_list[reagent.impure_chem]
		if(impure_reagent)
			outstring += "\n
Impurity: \[\[#[impure_reagent.name]|[impure_reagent.name]\]\]"

		var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
		if(inverse_reagent)
			outstring += "\n
Inverse: \[\[#[inverse_reagent.name]|[inverse_reagent.name]\]\] <[reagent.inverse_chem_val*100]%"

		var/datum/reagent/failed_reagent = GLOB.chemical_reagents_list[reagent.failed_chem]
		if(failed_reagent && reaction)
			outstring += "\n
Failed: \[\[#[failed_reagent.name]|[failed_reagent.name]\]\] <[reaction.purity_min*100]%"
	var/ph_color
	CONVERT_PH_TO_COLOR(reagent.ph, ph_color)
	outstring += "\n
pH: [reagent.ph]▮"
	outstring += "\n|"

	//RECIPE
	//|{{RecursiveChem/Oil}}
	if(reaction)
		outstring += "{{RecursiveChem/[reagent.name]}}"
		outstring += "\n|"

		//Reaction conditions
		//min temp
		if(reaction.is_cold_recipe)
			outstring += "Cold reaction\n
"
		outstring += "Min temp: [reaction.required_temp]K\n
Overheat: [reaction.overheat_temp]K\n
Optimal pH: [reaction.optimal_ph_min] to [reaction.optimal_ph_max]"

		//Overly impure levels
		if(reaction.purity_min)
			outstring += "\n
Unstable purity: <[reaction.purity_min*100]%"

		//Kinetics
		var/thermic = reaction.thermic_constant
		if(reaction.reaction_flags & REACTION_HEAT_ARBITARY)
			thermic *= 100 //Because arbitary is a lower scale
		switch(thermic)
			if(-INFINITY to -1500)
				outstring += "\n
Overwhelmingly endothermic"
			if(-1500 to -1000)
				outstring += "\n
Extremely endothermic"
			if(-1000 to -500)
				outstring += "\n
Strongly endothermic"
			if(-500 to -200)
				outstring += "\n
Moderately endothermic"
			if(-200 to -50)
				outstring += "\n
Endothermic"
			if(-50 to 0)
				outstring += "\n
Weakly endothermic"
			if(0)
				outstring += "\n
"
			if(0 to 50)
				outstring += "\n
Weakly Exothermic"
			if(50 to 200)
				outstring += "\n
Exothermic"
			if(200 to 500)
				outstring += "\n
Moderately exothermic"
			if(500 to 1000)
				outstring += "\n
Strongly exothermic"
			if(1000 to 1500)
				outstring += "\n
Extremely exothermic"
			if(1500 to INFINITY)
				outstring += "\n
Overwhelmingly exothermic"
			//if("cheesey")
				//outstring += "
Dangerously Cheesey"

		//pH drift
		if(reaction.results)
			var/start_ph = 0
			var/reactant_vol = 0
			for(var/typepath in reaction.required_reagents)
				var/datum/reagent/req_reagent = GLOB.chemical_reagents_list[typepath]
				start_ph += req_reagent.ph * reaction.required_reagents[typepath]
				reactant_vol += reaction.required_reagents[typepath]

			var/product_vol = 0
			var/end_ph = 0
			for(var/typepath in reaction.results)
				var/datum/reagent/prod_reagent = GLOB.chemical_reagents_list[typepath]
				end_ph += prod_reagent.ph * reaction.results[typepath]
				product_vol += reaction.results[typepath]

			if(reactant_vol || product_vol)
				start_ph = start_ph / reactant_vol
				end_ph = end_ph / product_vol
				var/sum_change = end_ph - start_ph
				sum_change += reaction.H_ion_release

				if(sum_change > 0)
					outstring += "\n
H+ consuming"
				else if (sum_change < 0)
					outstring += "\n
H+ producing"
			else
				to_chat(usr, "[reaction] doesn't have valid product and reagent volumes! Please tell Fermi.")
		else
			if(reaction.H_ion_release > 0)
				outstring += "\n
H+ consuming"
			else if (reaction.H_ion_release < 0)
				outstring += "\n
H+ producing"

		//container
		if(reaction.required_container)
			var/list/names = splittext("[reaction.required_container]", "/")
			var/container_name = "[names[names.len]] [names[names.len-1]]"
			container_name = replacetext(container_name, "_", " ")
			outstring += "\n
[container_name]"

		//Warn if it's dangerous
		if(reaction.reaction_tags & REACTION_TAG_DANGEROUS)
			outstring += "\n
Dangerous"
		outstring += "\n|"

	//Description
	outstring += "[reagent.description]"
	outstring += "\n|"

	//Chemical properties - *2 because 1 tick is every 2s
	outstring += "Rate: [reagent.metabolization_rate*2]u/tick\n
Unreacted purity: [reagent.creation_purity*100]%[(reagent.overdose_threshold ? "\n
OD: [reagent.overdose_threshold]u" : "")]"

	if(length(reagent.addiction_types))
		outstring += "\n
Addictions:"
	for(var/entry in reagent.addiction_types)
		var/datum/addiction/ref = SSaddiction.all_addictions[entry]
		switch(reagent.addiction_types[entry])
			if(-INFINITY to 0)
				continue
			if(0 to 5)
				outstring += "\n
Weak [ref.name]"
			if(5 to 10)
				outstring += "\n
[ref.name]"
			if(10 to 20)
				outstring += "\n
Strong [ref.name]"
			if(20 to INFINITY)
				outstring += "\n
Potent [ref.name]"

	if(reagent.chemical_flags & REAGENT_DEAD_PROCESS)
		outstring += "\n
Works on the dead"

	outstring += "\n|-"
	return outstring


code:
/proc/mix_color_from_reagents(list/reagent_list)
	if(!istype(reagent_list))
		return

	var/mixcolor
	var/vol_counter = 0
	var/vol_temp

	for(var/datum/reagent/R in reagent_list)
		vol_temp = R.volume
		vol_counter += vol_temp

		if(!mixcolor)
			mixcolor = R.color

		else if (length(mixcolor) >= length(R.color))
			mixcolor = BlendRGB(mixcolor, R.color, vol_temp/vol_counter)
		else
			mixcolor = BlendRGB(R.color, mixcolor, vol_temp/vol_counter)

	return mixcolor


code:
/*
* #/datum/equilibrium
*
* A dynamic reaction object that processes the reaction that it is set within it. Relies on a reagents holder to call and operate the functions.
*
* An object/datum to contain the vars for each of the reactions currently ongoing in a holder/reagents datum
* This way all information is kept within one accessable object
* equilibrium is a unique name as reaction is already too close to chemical_reaction
* This is set up this way to reduce holder.dm bloat as well as reduce confusing list overhead
* The crux of the fermimechanics are handled here
* Instant reactions AREN'T handled here. See holder.dm
*/
/datum/equilibrium
	///The chemical reaction that is presently being processed
	var/datum/chemical_reaction/reaction
	///The location/reagents datum the processing is taking place
	var/datum/reagents/holder
	///How much product we can make multiplied by the input recipe's products/required_reagents numerical values
	var/multiplier = INFINITY
	///The sum total of each of the product's numerical's values. This is so the addition/deletion is kept at the right values for multiple product reactions
	var/product_ratio = 0
	///The total possible that this reaction can make presently - used for gui outputs
	var/target_vol = 0
	///The target volume the reaction is headed towards. This is updated every tick, so isn't the total value for the reaction, it's just a way to ensure we can't make more than is possible.
	var/step_target_vol = INFINITY
	///How much of the reaction has been made so far. Mostly used for subprocs, but it keeps track across the whole reaction and is added to every step.
	var/reacted_vol = 0
	///What our last delta_ph was
	var/reaction_quality = 1
	///If we're done with this reaction so that holder can clear it.
	var/to_delete = FALSE
	///Result vars, private - do not edit unless in reaction_step()
	///How much we're adding
	var/delta_t
	///How pure our step is
	var/delta_ph
	///Modifiers from catalysts, do not use negative numbers.
	///I should write a better handiler for modifying these
	///Speed mod
	var/speed_mod = 1
	///pH mod
	var/h_ion_mod = 1
	///Temp mod
	var/thermic_mod = 1
	///Allow us to deal with lag by "charging" up our reactions to react faster over a period - this means that the reaction doesn't suddenly mass react - which can cause explosions
	var/time_deficit
	///Used to store specific data needed for a reaction, usually used to keep track of things between explosion calls. CANNOT be used as a part of chemical_recipe - those vars are static lookup tables.
	var/data = list()

/*
* Creates and sets up a new equlibrium object
*
* Arguments:
* * input_reaction - the chemical_reaction datum that will be processed
* * input_holder - the reagents datum that the output will be put into
*/
/datum/equilibrium/New(datum/chemical_reaction/input_reaction, datum/reagents/input_holder)
	reaction = input_reaction
	holder = input_holder
	if(!holder || !reaction) //sanity check
		stack_trace("A new [type] was set up, with incorrect/null input vars!")
		to_delete = TRUE
		return
	if(!check_inital_conditions()) //If we're outside of the scope of the reaction vars
		to_delete = TRUE
		return
	if(!length(reaction.results)) //Come back to and revise the affected reactions in the next PR, this is a placeholder fix.
		holder.instant_react(reaction) //Even if this check fails, there's a backup - look inside of calculate_yield()
		to_delete = TRUE
		return
	LAZYADD(holder.reaction_list, src)
	SSblackbox.record_feedback("tally", "chemical_reaction", 1, "[reaction.type] attempts")

/datum/equilibrium/Destroy()
	if(reacted_vol < target_vol) //We did NOT finish from reagents - so we can restart this reaction given property changes in the beaker. (i.e. if it stops due to low temp, this will allow it to fast restart when heated up again)
		LAZYADD(holder.failed_but_capable_reactions, reaction) //Consider replacing check with calculate_yield()
	LAZYREMOVE(holder.reaction_list, src)
	holder = null
	reaction = null
	return ..()

/*
* Check to make sure our input vars are sensible - truncated version of check_reagent_properties()
*
* (as the setup in holder.dm checks for that already - this is a way to reduce calculations on New())
* Don't call this unless you know what you're doing, this is an internal proc
*/
/datum/equilibrium/proc/check_inital_conditions()
	//Make sure we have the right multipler for on_reaction()
	for(var/single_reagent in reaction.required_reagents)
		multiplier = min(multiplier, round((holder.get_reagent_amount(single_reagent) / reaction.required_reagents[single_reagent]), CHEMICAL_QUANTISATION_LEVEL))
	if(multiplier == INFINITY)
		return FALSE
	//Consider purity gating too? - probably not, purity is hard to determine
	//To prevent reactions outside of the pH window from starting.
	if(!((holder.ph >= (reaction.optimal_ph_min - reaction.determin_ph_range)) && (holder.ph <= (reaction.optimal_ph_max + reaction.determin_ph_range))))
		return FALSE
	return TRUE

/*
* Check to make sure our input vars are sensible - is the holder overheated? does it have the required reagents? Does it have the required calalysts?
*
* If you're adding more checks for reactions, this is the proc to edit
* otherwise, generally, don't call this directed except internally
*/
/datum/equilibrium/proc/check_reagent_properties()
	//Have we exploded from on_reaction?
	if(!holder.my_atom || holder.reagent_list.len == 0)
		return FALSE
	if(!holder)
		stack_trace("an equilibrium is missing it's holder.")
		return FALSE
	if(!reaction)
		stack_trace("an equilibrium is missing it's reaction.")
		return FALSE

	//set up catalyst checks
	var/total_matching_catalysts = 0
	//Reagents check should be handled in the calculate_yield() from multiplier

	//If the product/reactants are too impure
	for(var/datum/reagent/reagent as anything in holder.reagent_list)
		//this is done this way to reduce processing compared to holder.has_reagent(P)
		for(var/datum/reagent/catalyst as anything in reaction.required_catalysts)
			if(catalyst == reagent.type)
				total_matching_catalysts++
		if(istype(reagent, /datum/reagent/catalyst_agent))
			var/datum/reagent/catalyst_agent/catalyst_agent = reagent
			if(reagent.volume >= catalyst_agent.min_volume)
				catalyst_agent.consider_catalyst(src)

	if(!(total_matching_catalysts == reaction.required_catalysts.len))
		return FALSE

	//All good!
	return TRUE

/*
* Calculates how much we're aiming to create
*
* Specifically calcuates multiplier, product_ratio, step_target_vol
* Also checks to see if these numbers are sane, returns a TRUE/FALSE
* Generally an internal proc
*/
/datum/equilibrium/proc/calculate_yield()
	if(!reaction)
		stack_trace("Tried to calculate an equlibrium for reaction [reaction.type], but there was no reaction set for the datum")
		return FALSE

	multiplier = INFINITY
	for(var/reagent in reaction.required_reagents)
		multiplier = min(multiplier, round((holder.get_reagent_amount(reagent) / reaction.required_reagents[reagent]), CHEMICAL_QUANTISATION_LEVEL))

	if(!length(reaction.results)) //Incase of no reagent product
		product_ratio = 1
		step_target_vol = INFINITY
		for(var/reagent in reaction.required_reagents)
			step_target_vol = min(step_target_vol, multiplier * reaction.required_reagents[reagent])
		if(step_target_vol == 0 || multiplier == 0)
			return FALSE
		//Sanity Check
		if(step_target_vol == INFINITY || multiplier == INFINITY) //I don't see how this can happen, but I'm not bold enough to let infinities roll around for free
			to_delete = TRUE
			CRASH("Tried to calculate target vol for [reaction.type] with no products, but could not find required reagents for the reaction. If it got here, something is really broken with the recipe.")
		return TRUE

	product_ratio = 0
	step_target_vol = 0
	var/true_reacted_vol //Because volumes can be lost mid reactions
	for(var/product in reaction.results)
		step_target_vol += (reaction.results[product]*multiplier)
		product_ratio += reaction.results[product]
		true_reacted_vol += holder.get_reagent_amount(product)
	if(step_target_vol == 0 || multiplier == INFINITY)
		return FALSE
	target_vol = step_target_vol + true_reacted_vol
	reacted_vol = true_reacted_vol
	return TRUE

/*
* Deals with lag - allows a reaction to speed up to 3x from delta_time
* "Charged" time (time_deficit) discharges by incrementing reactions by doubling them
* If delta_time is greater than 1.5, then we save the extra time for the next ticks
*
* Arguments:
* * delta_time - the time between the last proc in world.time
*/
/datum/equilibrium/proc/deal_with_time(delta_time)
	if(delta_time > 1)
		time_deficit += delta_time - 1
		delta_time = 1 //Lets make sure reactions aren't super speedy and blow people up from a big lag spike
	else if (time_deficit)
		if(time_deficit < 0.25)
			delta_time += time_deficit
			time_deficit = 0
		else
			delta_time += 0.25
			time_deficit -= 0.25
	return delta_time

/*
* Main method of checking for explosive - or failed states
* Checks overheated() and overly_impure() of a reaction
* This was moved from the start, to the end - after a reaction, so post reaction temperature changes aren't ignored.
* overheated() is first - so double explosions can't happen (i.e. explosions that blow up the holder)
* step_volume_added is how much product (across all products) was added for this single step
*/
/datum/equilibrium/proc/check_fail_states(step_volume_added)
	//Are we overheated?
	if(reaction.is_cold_recipe)
		if(holder.chem_temp < reaction.overheat_temp && reaction.overheat_temp != NO_OVERHEAT) //This is before the process - this is here so that overly_impure and overheated() share the same code location (and therefore vars) for calls.
			SSblackbox.record_feedback("tally", "chemical_reaction", 1, "[reaction.type] overheated reaction steps")
			reaction.overheated(holder, src, step_volume_added)
	else
		if(holder.chem_temp > reaction.overheat_temp)
			SSblackbox.record_feedback("tally", "chemical_reaction", 1, "[reaction.type] overheated reaction steps")
			reaction.overheated(holder, src, step_volume_added)

	//is our product too impure?
	for(var/product in reaction.results)
		var/datum/reagent/reagent = holder.has_reagent(product)
		if(!reagent) //might be missing from overheat exploding
			continue
		if (reagent.purity < reaction.purity_min)//If purity is below the min, call the proc
			SSblackbox.record_feedback("tally", "chemical_reaction", 1, "[reaction.type] overly impure reaction steps")
			reaction.overly_impure(holder, src, step_volume_added)

	//did we explode?
	if(!holder.my_atom || holder.reagent_list.len == 0)
		return FALSE
	return TRUE

/*
* Main reaction processor - Increments the reaction by a timestep
*
* First checks the holder to make sure it can continue
* Then calculates the purity and volume produced.TRUE
* Then adds/removes reagents
* Then alters the holder pH and temperature, and calls reaction_step
* Arguments:
* * delta_time - the time displacement between the last call and the current, 1 is a standard step
* * purity_modifier - how much to modify the step's purity by (0 - 1)
*/
/datum/equilibrium/proc/react_timestep(delta_time, purity_modifier = 1)
	if(to_delete)
		//This occurs when it explodes
		return FALSE
	if(!check_reagent_properties()) //this is first because it'll call explosions first
		to_delete = TRUE
		return
	if(!calculate_yield())//So that this can detect if we're missing reagents
		to_delete = TRUE
		return
	delta_time = deal_with_time(delta_time)

	delta_t = 0 //how far off optimal temp we care
	delta_ph = 0 //How far off the pH we are
	var/cached_ph = holder.ph
	var/cached_temp = holder.chem_temp
	var/purity = 1 //purity of the current step

	//Begin checks
	//Calculate DeltapH (Deviation of pH from optimal)
	//Within mid range
	if (cached_ph >= reaction.optimal_ph_min  && cached_ph <= reaction.optimal_ph_max)
		delta_ph = 1 //100% purity for this step
	//Lower range
	else if (cached_ph < reaction.optimal_ph_min) //If we're outside of the optimal lower bound
		if (cached_ph < (reaction.optimal_ph_min - reaction.determin_ph_range)) //If we're outside of the deterministic bound
			delta_ph = 0 //0% purity
		else //We're in the deterministic phase
			delta_ph = (((cached_ph - (reaction.optimal_ph_min - reaction.determin_ph_range))**reaction.ph_exponent_factor)/((reaction.determin_ph_range**reaction.ph_exponent_factor))) //main pH calculation
	//Upper range
	else if (cached_ph > reaction.optimal_ph_max) //If we're above of the optimal lower bound
		if (cached_ph > (reaction.optimal_ph_max + reaction.determin_ph_range))  //If we're outside of the deterministic bound
			delta_ph = 0 //0% purity
		else  //We're in the deterministic phase
			delta_ph = (((- cached_ph + (reaction.optimal_ph_max + reaction.determin_ph_range))**reaction.ph_exponent_factor)/(reaction.determin_ph_range**reaction.ph_exponent_factor))//Reverse - to + to prevent math operation failures.

	//This should never proc, but it's a catch incase someone puts in incorrect values
	else
		stack_trace("[holder.my_atom] attempted to determine FermiChem pH for '[reaction.type]' which had an invalid pH of [cached_ph] for set recipie pH vars. It's likely the recipe vars are wrong.")

	//Calculate DeltaT (Deviation of T from optimal)
	if(!reaction.is_cold_recipe)
		if (cached_temp < reaction.optimal_temp && cached_temp >= reaction.required_temp)
			delta_t = (((cached_temp - reaction.required_temp)**reaction.temp_exponent_factor)/((reaction.optimal_temp - reaction.required_temp)**reaction.temp_exponent_factor))
		else if (cached_temp >= reaction.optimal_temp)
			delta_t = 1
		else //too hot
			delta_t = 0
			to_delete = TRUE
			return
	else
		if (cached_temp > reaction.optimal_temp && cached_temp <= reaction.required_temp)
			delta_t = (((reaction.required_temp - cached_temp)**reaction.temp_exponent_factor)/((reaction.required_temp - reaction.optimal_temp)**reaction.temp_exponent_factor))
		else if (cached_temp <= reaction.optimal_temp)
			delta_t = 1
		else //Too cold
			delta_t = 0
			to_delete = TRUE
			return

	//Call any special reaction steps BEFORE addition
	if(reaction.reaction_step(holder, src, delta_t, delta_ph, step_target_vol) == END_REACTION)
		to_delete = TRUE
		return

	//Catalyst modifier
	delta_t *= speed_mod

	purity = delta_ph//set purity equal to pH offset

	//Then adjust purity of result with beaker reagent purity.
	purity *= reactant_purity(reaction)

	//Then adjust it from the input modifier
	purity *= purity_modifier

	//Now we calculate how much to add - this is normalised to the rate up limiter
	var/delta_chem_factor = (reaction.rate_up_lim*delta_t)*delta_time//add/remove factor
	var/total_step_added = 0
	//keep limited
	if(delta_chem_factor > step_target_vol)
		delta_chem_factor = step_target_vol
	else if (delta_chem_factor < CHEMICAL_VOLUME_MINIMUM)
		delta_chem_factor = CHEMICAL_VOLUME_MINIMUM
	//Normalise to multiproducts
	delta_chem_factor /= product_ratio
	//delta_chem_factor = round(delta_chem_factor, CHEMICAL_QUANTISATION_LEVEL) // Might not be needed - left here incase testmerge shows that it does. Remove before full commit.

	//Calculate how much product to make and how much reactant to remove factors..
	for(var/reagent in reaction.required_reagents)
		holder.remove_reagent(reagent, (delta_chem_factor * reaction.required_reagents[reagent]), safety = TRUE)
		//Apply pH changes
		var/pH_adjust
		if(reaction.reaction_flags & REACTION_PH_VOL_CONSTANT)
			pH_adjust = ((delta_chem_factor * reaction.required_reagents[reagent])/target_vol)*(reaction.H_ion_release*h_ion_mod)
		else //Default adds pH independant of volume
			pH_adjust = (delta_chem_factor * reaction.required_reagents[reagent])*(reaction.H_ion_release*h_ion_mod)
		holder.adjust_specific_reagent_ph(reagent, pH_adjust)

	var/step_add
	for(var/product in reaction.results)
		//create the products
		step_add = delta_chem_factor * reaction.results[product]
		//If we make purities in real time
		if(reaction.reaction_flags & REACTION_REAL_TIME_SPLIT && purity < 1)
			var/datum/reagent/product_ref = GLOB.chemical_reagents_list[product]
			if(purity < reaction.purity_min && product_ref.failed_chem) //If we're failed
				holder.add_reagent(product_ref.failed_chem, step_add, null, cached_temp, (1-purity), override_base_ph = TRUE)
			else if(purity < product_ref.inverse_chem_val && product_ref.inverse_chem) //If we're inverse
				holder.add_reagent(product_ref.inverse_chem, step_add, null, cached_temp, (1-purity), override_base_ph = TRUE)
			else if(product_ref.impure_chem && product_ref.impure_chem) //if we're impure
				holder.add_reagent(product*purity, step_add, null, cached_temp, purity, override_base_ph = TRUE)
				holder.add_reagent(product_ref.impure_chem*(1-purity), step_add, null, cached_temp, (1-purity), override_base_ph = TRUE)
			else //We can get here if the flag is set, but there's no associated impure_chem assigned. In some cases this is desired (i.e. multiver only wants to real time split it's inverse chem)
				holder.add_reagent(product, step_add, null, cached_temp, purity, override_base_ph = TRUE)
		//Default handiling
		else
			holder.add_reagent(product, step_add, null, cached_temp, purity, override_base_ph = TRUE)

		//Apply pH changes
		var/pH_adjust
		if(reaction.reaction_flags & REACTION_PH_VOL_CONSTANT)
			pH_adjust = (step_add/target_vol)*(reaction.H_ion_release*h_ion_mod)
		else
			pH_adjust = step_add*(reaction.H_ion_release*h_ion_mod)
		holder.adjust_specific_reagent_ph(product, pH_adjust)
		reacted_vol += step_add
		total_step_added += step_add

	#ifdef REAGENTS_TESTING //Kept in so that people who want to write fermireactions can contact me with this log so I can help them
	if(GLOB.Debug2) //I want my spans for my sanity
		message_admins("Reaction step active for:[reaction.type]</spans>")
		message_admins("|Reaction conditions| Temp: [holder.chem_temp], pH: [holder.ph], reactions: [length(holder.reaction_list)], awaiting reactions: [length(holder.failed_but_capable_reactions)], no. reagents:[length(holder.reagent_list)], no. prev reagents: [length(holder.previous_reagent_list)]<spans>")
		message_admins("Reaction vars: PreReacted:[reacted_vol] of [step_target_vol] of total [target_vol]. delta_t [delta_t], multiplier [multiplier], delta_chem_factor [delta_chem_factor] Pfactor [product_ratio], purity of [purity] from a delta_ph of [delta_ph]. DeltaTime: [delta_time]")
	#endif

	//Apply thermal output of reaction to beaker
	if(reaction.reaction_flags & REACTION_HEAT_ARBITARY)
		holder.chem_temp += clamp((reaction.thermic_constant* total_step_added*thermic_mod), 0, CHEMICAL_MAXIMUM_TEMPERATURE) //old method - for every bit added, the whole temperature is adjusted
	else //Standard mechanics
		var/heat_energy = reaction.thermic_constant * total_step_added * thermic_mod * SPECIFIC_HEAT_DEFAULT
		holder.adjust_thermal_energy(heat_energy, 0, CHEMICAL_MAXIMUM_TEMPERATURE) //heat is relative to the beaker conditions

	//Give a chance of sounds
	if(prob(5))
		holder.my_atom.audible_message(span_notice("[icon2html(holder.my_atom, viewers(DEFAULT_MESSAGE_RANGE, src))] [reaction.mix_message]"))
		if(reaction.mix_sound)
			playsound(get_turf(holder.my_atom), reaction.mix_sound, 80, TRUE)

	//Used for UI output
	reaction_quality = purity

	//post reaction checks
	if(!(check_fail_states(total_step_added)))
		to_delete = TRUE

	//end reactions faster so plumbing is faster
	if((step_add >= step_target_vol) && (length(holder.reaction_list == 1)))//length is so that plumbing is faster - but it doesn't disable competitive reactions. Basically, competitive reactions will likely reach their step target at the start, so this will disable that. We want to avoid that. But equally, we do want to full stop a holder from reacting asap so plumbing isn't waiting an tick to resolve.
		to_delete = TRUE

	holder.update_total()//do NOT recalculate reactions

/*
* Calculates the total sum normalised purity of ALL reagents in a holder
*
* Currently calculates it irrespective of required reagents at the start, but this should be changed if this is powergamed to required reagents
* It's not currently because overly_impure affects all reagents
*/
/datum/equilibrium/proc/reactant_purity(datum/chemical_reaction/C)
	var/list/cached_reagents = holder.reagent_list
	var/i = 0
	var/cached_purity
	for(var/datum/reagent/reagent as anything in holder.reagent_list)
		if (reagent in cached_reagents)
			cached_purity += reagent.purity
			i++
	if(!i)//I've never seen it get here with 0, but in case - it gets here when it blows up from overheat
		stack_trace("No reactants found mid reaction for [C.type]. Beaker: [holder.my_atom]")
		return 0 //we exploded and cleared reagents - but lets not kill the process
	return cached_purity/i

///Panic stop a reaction - cleanup should be handled by the next timestep
/datum/equilibrium/proc/force_clear_reactive_agents()
	for(var/reagent in reaction.required_reagents)
		holder.remove_reagent(reagent, (multiplier * reaction.required_reagents[reagent]), safety = 1)