diff --git a/source/core/vmexports.cpp b/source/core/vmexports.cpp
index a7aaa2e20..16fe1726a 100644
--- a/source/core/vmexports.cpp
+++ b/source/core/vmexports.cpp
@@ -95,6 +95,26 @@ DEFINE_ACTION_FUNCTION_NATIVE(_Raze, cansee, Raze_cansee)
 	ACTION_RETURN_BOOL(Raze_cansee(x, y, z, s, xe, ye, ze, se));
 }
 
+int Raze_hitscan(double x, double y, double z, sectortype* sec, double xe, double ye, double ze, HitInfoBase* hit, unsigned cliptype, double maxrange)
+{
+	return hitscan(DVector3(x, y, z), sec, DVector3(xe, ye, ze), *hit, cliptype, maxrange);
+}
+
+DEFINE_ACTION_FUNCTION_NATIVE(_Raze, hitscan, Raze_hitscan)
+{
+	PARAM_PROLOGUE;
+	PARAM_FLOAT(x);
+	PARAM_FLOAT(y);
+	PARAM_FLOAT(z);
+	PARAM_POINTER(s, sectortype);
+	PARAM_FLOAT(xe);
+	PARAM_FLOAT(ye);
+	PARAM_FLOAT(ze);
+	PARAM_POINTER(se, HitInfoBase);
+	PARAM_UINT(clip);
+	PARAM_FLOAT(maxrange);
+	ACTION_RETURN_INT(Raze_hitscan(x, y, z, s, xe, ye, ze, se, clip, maxrange));
+}
 
 
 DEFINE_ACTION_FUNCTION_NATIVE(_Raze, SoundEnabled, SoundEnabled)
diff --git a/source/games/duke/src/actors.cpp b/source/games/duke/src/actors.cpp
index 7ad2fcd0d..3b9bacf1b 100644
--- a/source/games/duke/src/actors.cpp
+++ b/source/games/duke/src/actors.cpp
@@ -808,26 +808,6 @@ void reactor(DDukeActor* const actor, int REACTOR, int REACTOR2, int REACTORBURN
 	}
 }
 
-//---------------------------------------------------------------------------
-//
-//
-//
-//---------------------------------------------------------------------------
-
-void bloodsplats(DDukeActor *actor)
-{
-	if (actor->temp_data[0] < 14 * 26)
-	{
-		auto offset = krandf(1);
-		auto lerp = 1. - (double(actor->temp_data[0]) / (14 * 26));
-		auto zadj = (1. / 16.) * lerp;
-		auto sadj = (1. / 12.) * lerp * REPEAT_SCALE;
-		actor->spr.pos.Z += zadj + offset * zadj;
-		actor->spr.scale.Y += sadj + offset * sadj;
-		actor->temp_data[0]++;
-	}
-}
-
 //---------------------------------------------------------------------------
 //
 // 
@@ -1406,7 +1386,7 @@ void handle_se14(DDukeActor* actor, bool checkstat, int RPG, int JIBS6)
 				{
 					auto saved_angle = actor->spr.Angles.Yaw;
 					actor->spr.Angles.Yaw = (actor->spr.pos.XY() - ps[p].GetActor()->spr.pos.XY()).Angle();
-					fi.shoot(actor, RPG);
+					fi.shoot(actor, RPG, nullptr);
 					actor->spr.Angles.Yaw = saved_angle;
 				}
 			}
@@ -1892,7 +1872,7 @@ void handle_se05(DDukeActor* actor, int FIRELASER)
 	{
 		auto ang = actor->spr.Angles.Yaw;
 		actor->spr.Angles.Yaw = (actor->spr.pos.XY() - ps[p].GetActor()->spr.pos.XY()).Angle();
-		fi.shoot(actor, FIRELASER);
+		fi.shoot(actor, FIRELASER, nullptr);
 		actor->spr.Angles.Yaw = ang;
 	}
 
diff --git a/source/games/duke/src/actors_d.cpp b/source/games/duke/src/actors_d.cpp
index 7c2828285..65ad99d3b 100644
--- a/source/games/duke/src/actors_d.cpp
+++ b/source/games/duke/src/actors_d.cpp
@@ -2426,14 +2426,6 @@ void moveexplosions_d(void)  // STATNUM 5
 			else act->spr.shade = 127;
 			continue;
 
-		case BLOODSPLAT1:
-		case BLOODSPLAT2:
-		case BLOODSPLAT3:
-		case BLOODSPLAT4:
-
-			bloodsplats(act);
-			continue;
-
 		case NUKEBUTTON:
 		case NUKEBUTTON + 1:
 		case NUKEBUTTON + 2:
@@ -2824,7 +2816,7 @@ void moveeffectors_d(void)   //STATNUM 3
 			if (act->temp_data[0])
 			{
 				if (act->temp_data[0] == 1)
-					fi.shoot(act, sc->extra);
+					fi.shoot(act, sc->extra, nullptr);
 				else if (act->temp_data[0] == 26 * 5)
 					act->temp_data[0] = 0;
 				act->temp_data[0]++;
diff --git a/source/games/duke/src/actors_r.cpp b/source/games/duke/src/actors_r.cpp
index 410a8ac9a..278d82ae1 100644
--- a/source/games/duke/src/actors_r.cpp
+++ b/source/games/duke/src/actors_r.cpp
@@ -2171,14 +2171,6 @@ void moveexplosions_r(void)  // STATNUM 5
 			else act->spr.shade = 127;
 			continue;
 
-		case BLOODSPLAT1:
-		case BLOODSPLAT2:
-		case BLOODSPLAT3:
-		case BLOODSPLAT4:
-
-			bloodsplats(act);
-			continue;
-
 		case MUD:
 
 			act->temp_data[0]++;
@@ -2559,7 +2551,7 @@ void moveeffectors_r(void)   //STATNUM 3
 			if (act->temp_data[0])
 			{
 				if (act->temp_data[0] == 1)
-					fi.shoot(act, sc->extra);
+					fi.shoot(act, sc->extra, nullptr);
 				else if (act->temp_data[0] == 26 * 5)
 					act->temp_data[0] = 0;
 				act->temp_data[0]++;
diff --git a/source/games/duke/src/animatesprites_d.cpp b/source/games/duke/src/animatesprites_d.cpp
index a015c4567..4bee5a94f 100644
--- a/source/games/duke/src/animatesprites_d.cpp
+++ b/source/games/duke/src/animatesprites_d.cpp
@@ -89,16 +89,6 @@ void animatesprites_d(tspriteArray& tsprites, const DVector2& viewVec, DAngle vi
 			else t->cstat &= ~CSTAT_SPRITE_XFLIP;
 			t->picnum = h->spr.picnum + k;
 			break;
-		case BLOODSPLAT1:
-		case BLOODSPLAT2:
-		case BLOODSPLAT3:
-		case BLOODSPLAT4:
-			if (t->pal == 6)
-			{
-				t->shade = -127;
-				continue;
-			}
-			[[fallthrough]];
 		case BULLETHOLE:
 			t->shade = 16;
 			continue;
diff --git a/source/games/duke/src/animatesprites_r.cpp b/source/games/duke/src/animatesprites_r.cpp
index ce78e4830..fbf6bcd7f 100644
--- a/source/games/duke/src/animatesprites_r.cpp
+++ b/source/games/duke/src/animatesprites_r.cpp
@@ -73,16 +73,6 @@ void animatesprites_r(tspriteArray& tsprites, const DVector2& viewVec, DAngle vi
 			else t->cstat &= ~CSTAT_SPRITE_XFLIP;
 			t->picnum = h->spr.picnum + k;
 			break;
-		case BLOODSPLAT1:
-		case BLOODSPLAT2:
-		case BLOODSPLAT3:
-		case BLOODSPLAT4:
-			if (t->pal == 6)
-			{
-				t->shade = -127;
-				continue;
-			}
-			[[fallthrough]];
 		case BULLETHOLE:
 			t->shade = 16;
 			continue;
diff --git a/source/games/duke/src/constants.h b/source/games/duke/src/constants.h
index aae547119..3fb9805ce 100644
--- a/source/games/duke/src/constants.h
+++ b/source/games/duke/src/constants.h
@@ -392,8 +392,8 @@ enum
 	TFLAG_DOORWALL				= 1 << 5,
 	TFLAG_BLOCKDOOR				= 1 << 6,
 	TFLAG_OUTERSPACE			= 1 << 7,
-
-	TFLAG_NOCIRCLEREFLECT			= 32,
+	TFLAG_NOBLOODSPLAT			= 1 << 8,
+	TFLAG_NOCIRCLEREFLECT		= 1 << 9,
 };
 
 enum
diff --git a/source/games/duke/src/dispatch.cpp b/source/games/duke/src/dispatch.cpp
index c2a72cac2..304d1a532 100644
--- a/source/games/duke/src/dispatch.cpp
+++ b/source/games/duke/src/dispatch.cpp
@@ -74,8 +74,8 @@ void move_d(DDukeActor* i, int g_p, int g_x);
 void move_r(DDukeActor* i, int g_p, int g_x);
 void incur_damage_d(player_struct* p);
 void incur_damage_r(player_struct* p);
-void shoot_d(DDukeActor* i, int atwith);
-void shoot_r(DDukeActor* i, int atwith);
+void shoot_d(DDukeActor* i, int atwith, PClass* cls);
+void shoot_r(DDukeActor* i, int atwith, PClass* cls);
 void selectweapon_d(int snum, int j);
 void selectweapon_r(int snum, int j);
 int doincrements_d(player_struct* p);
diff --git a/source/games/duke/src/duke3d.h b/source/games/duke/src/duke3d.h
index 4ba73ef74..6f03e893a 100644
--- a/source/games/duke/src/duke3d.h
+++ b/source/games/duke/src/duke3d.h
@@ -99,7 +99,7 @@ struct Dispatcher
 
 	// player
 	void (*incur_damage)(player_struct* p);
-	void (*shoot)(DDukeActor*, int);
+	void (*shoot)(DDukeActor*, int, PClass* cls);
 	void (*selectweapon)(int snum, int j);
 	int (*doincrements)(player_struct* p);
 	void (*checkweapons)(player_struct* p);
@@ -125,6 +125,7 @@ bool CallOnUse(DDukeActor* actor, player_struct* user);
 void CallOnMotoSmash(DDukeActor* actor, player_struct* hitter);
 void CallOnRespawn(DDukeActor* actor, int low);
 bool CallAnimate(DDukeActor* actor, tspritetype* hitter);
+bool CallShootThis(DDukeActor* clsdef, DDukeActor* actor, int pn, const DVector3& spos, DAngle sang);
 void CallStaticSetup(DDukeActor* actor);
 
 
diff --git a/source/games/duke/src/funct.h b/source/games/duke/src/funct.h
index f1b377ba5..5750f115a 100644
--- a/source/games/duke/src/funct.h
+++ b/source/games/duke/src/funct.h
@@ -44,7 +44,6 @@ bool respawnmarker(DDukeActor* i, int yellow, int green);
 void forcesphere(DDukeActor* i, int forcesphere);
 void recon(DDukeActor* i, int explosion, int firelaser, int attacksnd, int painsnd, int roamsnd, int shift, int (*getspawn)(DDukeActor* i));
 void reactor(DDukeActor* i, int REACTOR, int REACTOR2, int REACTORBURNT, int REACTOR2BURNT, int REACTORSPARK, int REACTOR2SPARK);
-void bloodsplats(DDukeActor* actor);
 void forcesphereexplode(DDukeActor* i);
 void watersplash2(DDukeActor* i);
 void frameeffect1(DDukeActor* i);
@@ -123,7 +122,6 @@ void quickkill(player_struct* p);
 int setpal(player_struct* p);
 int madenoise(int playerNum);
 int haskey(sectortype* sect, int snum);
-void shootbloodsplat(DDukeActor* i, int p, const DVector3& pos, DAngle ang, int atwith, int BIGFORCE);
 
 void breakwall(int newpn, DDukeActor* spr, walltype* dawallnum);
 int callsound(sectortype* sectnum,DDukeActor* snum, bool endstate = false);
diff --git a/source/games/duke/src/game.cpp b/source/games/duke/src/game.cpp
index b043f1e60..4ad7d46d9 100644
--- a/source/games/duke/src/game.cpp
+++ b/source/games/duke/src/game.cpp
@@ -507,4 +507,16 @@ void CallStaticSetup(DDukeActor* actor)
 	}
 }
 
+bool CallShootThis(DDukeActor* clsdef, DDukeActor* actor, int pn, const DVector3& spos, DAngle sang)
+{
+	int rv = 0;
+	VMReturn ret(&rv);
+	IFVIRTUALPTR(clsdef, DDukeActor, ShootThis)
+	{
+		VMValue val[] = {clsdef, actor, pn >= 0? &ps[pn] : nullptr, spos.X, spos.Y, spos.Z, sang.Degrees()};
+		VMCall(func, val, 7, &ret, 1);
+	}
+	return rv;
+}
+
 END_DUKE_NS
diff --git a/source/games/duke/src/gameexec.cpp b/source/games/duke/src/gameexec.cpp
index e32ed3b0e..bbb3f4651 100644
--- a/source/games/duke/src/gameexec.cpp
+++ b/source/games/duke/src/gameexec.cpp
@@ -1755,7 +1755,7 @@ int ParseState::parse(void)
 		break;
 	case concmd_shoot:
 		insptr++;
-		fi.shoot(g_ac, (short)*insptr);
+		fi.shoot(g_ac, (short)*insptr, nullptr);
 		insptr++;
 		break;
 	case concmd_ifsoundid:
diff --git a/source/games/duke/src/player.cpp b/source/games/duke/src/player.cpp
index 2862300f8..9c6bcd3a8 100644
--- a/source/games/duke/src/player.cpp
+++ b/source/games/duke/src/player.cpp
@@ -993,69 +993,5 @@ int haskey(sectortype* sectp, int snum)
 	return 0;
 }
 
-//---------------------------------------------------------------------------
-//
-//
-//
-//---------------------------------------------------------------------------
-
-void shootbloodsplat(DDukeActor* actor, int p, const DVector3& pos, DAngle ang, int atwith, int BIGFORCE)
-{
-	auto sectp = actor->sector();
-	double zvel;
-	HitInfo hit{};
-
-	if (p >= 0)
-		ang += DAngle22_5 / 2 - randomAngle(22.5);
-	else ang += DAngle180 + DAngle22_5/2 - randomAngle(22.5);
-	
-	zvel = 4 - krandf(8);
-
-
-	hitscan(pos, sectp, DVector3(ang.ToVector() * 1024, zvel * 64), hit, CLIPMASK1);
-
-	// oh my...
-	if ( (pos.XY() - hit.hitpos.XY()).Length() < 64 &&
-		(hit.hitWall != nullptr && hit.hitWall->overpicnum != BIGFORCE) &&
-		((hit.hitWall->twoSided() && hit.hitSector != nullptr &&
-			hit.hitWall->nextSector()->lotag == 0 &&
-			hit.hitSector->lotag == 0 &&
-			(hit.hitSector->floorz - hit.hitWall->nextSector()->floorz) > 16) ||
-			(!hit.hitWall->twoSided() && hit.hitSector->lotag == 0)))
-	{
-		if ((hit.hitWall->cstat & CSTAT_WALL_MASKED) == 0)
-		{
-			if (hit.hitWall->twoSided())
-			{
-				DukeSectIterator it(hit.hitWall->nextSector());
-				while (auto act2 = it.Next())
-				{
-					if (act2->spr.statnum == STAT_EFFECTOR && act2->spr.lotag == SE_13_EXPLOSIVE)
-						return;
-				}
-			}
-
-			if (hit.hitWall->twoSided() &&
-				hit.hitWall->nextWall()->hitag != 0)
-				return;
-
-			if (hit.hitWall->hitag == 0)
-			{
-				auto spawned = spawn(actor, atwith);
-				if (spawned)
-				{
-					spawned->vel.X = -0.75;
-					spawned->spr.Angles.Yaw = hit.hitWall->delta().Angle() - DAngle90;
-					spawned->spr.pos = hit.hitpos;
-					spawned->spr.cstat |= randomXFlip();
-					ssp(spawned, CLIPMASK0);
-					SetActor(spawned, spawned->spr.pos);
-					if (actorflag(actor, SFLAG2_GREENBLOOD))
-						spawned->spr.pal = 6;
-				}
-			}
-		}
-	}
-}
 
 END_DUKE_NS
diff --git a/source/games/duke/src/player_d.cpp b/source/games/duke/src/player_d.cpp
index d75d77bb1..1405348cb 100644
--- a/source/games/duke/src/player_d.cpp
+++ b/source/games/duke/src/player_d.cpp
@@ -1044,7 +1044,7 @@ static void shootshrinker(DDukeActor* actor, int p, const DVector3& pos, DAngle
 //
 //---------------------------------------------------------------------------
 
-void shoot_d(DDukeActor* actor, int atwith)
+void shoot_d(DDukeActor* actor, int atwith, PClass *cls)
 {
 	int p;
 	DVector3 spos;
@@ -1052,35 +1052,17 @@ void shoot_d(DDukeActor* actor, int atwith)
 
 	auto const sect = actor->sector();
 
+	sang = actor->spr.Angles.Yaw;
 	if (actor->isPlayer())
 	{
 		p = actor->PlayerIndex();
+		spos = actor->getPosWithOffsetZ().plusZ(ps[p].pyoff + 4);
+
+		ps[p].crack_time = CRACK_TIME;
 	}
 	else
 	{
 		p = -1;
-	}
-
-	SetGameVarID(g_iAtWithVarID, atwith, actor, p);
-	SetGameVarID(g_iReturnVarID, 0, actor, p);
-	OnEvent(EVENT_SHOOT, p, ps[p].GetActor(), -1);
-	if (GetGameVarID(g_iReturnVarID, actor, p).safeValue() != 0)
-	{
-		return;
-	}
-
-
-	if (actor->isPlayer())
-	{
-		spos = ps[p].GetActor()->getPosWithOffsetZ().plusZ(ps[p].pyoff + 4);
-		sang = ps[p].GetActor()->spr.Angles.Yaw;
-
-		ps[p].crack_time = CRACK_TIME;
-
-	}
-	else
-	{
-		sang = actor->spr.Angles.Yaw;
 		spos = actor->spr.pos.plusZ(-(actor->spr.scale.Y * tileHeight(actor->spr.picnum) * 0.5) + 4);
 
 		if (actor->spr.picnum != ROTATEGUN)
@@ -1094,6 +1076,16 @@ void shoot_d(DDukeActor* actor, int atwith)
 		}
 	}
 
+	if (cls == nullptr)
+	{
+		auto info = spawnMap.CheckKey(atwith);
+		if (info)
+		{
+			cls = static_cast<PClassActor*>(info->Class(atwith));
+		}
+	}
+	if (cls && cls->IsDescendantOf(RUNTIME_CLASS(DDukeActor)) && CallShootThis(static_cast<DDukeActor*>(GetDefaultByType(cls)), actor, p, spos, sang)) return;
+
 	if (isWorldTour()) 
 	{ // Twentieth Anniversary World Tour
 		switch (atwith) 
@@ -1124,13 +1116,6 @@ void shoot_d(DDukeActor* actor, int atwith)
 
 	switch (atwith)
 	{
-	case BLOODSPLAT1:
-	case BLOODSPLAT2:
-	case BLOODSPLAT3:
-	case BLOODSPLAT4:
-		shootbloodsplat(actor, p, spos, sang, atwith, BIGFORCE);
-		break;
-
 	case KNEE:
 		shootknee(actor, p, spos, sang);
 		break;
@@ -1528,7 +1513,7 @@ int doincrements_d(player_struct* p)
 		p->last_quick_kick = p->quick_kick + 1;
 		p->quick_kick--;
 		if (p->quick_kick == 8)
-			fi.shoot(p->GetActor(), KNEE);
+			fi.shoot(p->GetActor(), KNEE, nullptr);
 	}
 	else if (p->last_quick_kick > 0)
 		p->last_quick_kick--;
@@ -2277,7 +2262,7 @@ static void operateweapon(int snum, ESyncBits actions)
 	case PISTOL_WEAPON:	// m-16 in NAM
 		if (p->kickback_pic == 1)
 		{
-			fi.shoot(pact, SHOTSPARK1);
+			fi.shoot(pact, SHOTSPARK1, nullptr);
 			S_PlayActorSound(PISTOL_FIRE, pact);
 			lastvisinc = PlayClock + 32;
 			p->visibility = 0;
@@ -2328,7 +2313,7 @@ static void operateweapon(int snum, ESyncBits actions)
 		if (p->kickback_pic == 4)
 		{
 			for(int ii = 0; ii < 7; ii++)
-				fi.shoot(pact, SHOTGUN);
+				fi.shoot(pact, SHOTGUN, nullptr);
 			p->ammo_amount[SHOTGUN_WEAPON]--;
 
 			S_PlayActorSound(SHOTGUN_FIRE, pact);
@@ -2397,7 +2382,7 @@ static void operateweapon(int snum, ESyncBits actions)
 				}
 
 				S_PlayActorSound(CHAINGUN_FIRE, pact);
-				fi.shoot(pact, CHAINGUN);
+				fi.shoot(pact, CHAINGUN, nullptr);
 				lastvisinc = PlayClock + 32;
 				p->visibility = 0;
 				checkavailweapon(p);
@@ -2441,7 +2426,7 @@ static void operateweapon(int snum, ESyncBits actions)
 			else
 				p->okickback_pic = p->kickback_pic = 0;
 			p->ammo_amount[p->curr_weapon]--;
-			fi.shoot(pact, GROWSPARK);
+			fi.shoot(pact, GROWSPARK, nullptr);
 
 			//#ifdef NAM
 			//#else
@@ -2476,7 +2461,7 @@ static void operateweapon(int snum, ESyncBits actions)
 			else p->okickback_pic = p->kickback_pic = 0;
 
 			p->ammo_amount[SHRINKER_WEAPON]--;
-			fi.shoot(pact, SHRINKER);
+			fi.shoot(pact, SHRINKER, nullptr);
 
 			if (!isNam())
 			{
@@ -2509,7 +2494,7 @@ static void operateweapon(int snum, ESyncBits actions)
 				{
 					p->visibility = 0;
 					lastvisinc = PlayClock + 32;
-					fi.shoot(pact, RPG);
+					fi.shoot(pact, RPG, nullptr);
 					p->ammo_amount[DEVISTATOR_WEAPON]--;
 					checkavailweapon(p);
 				}
@@ -2519,7 +2504,7 @@ static void operateweapon(int snum, ESyncBits actions)
 			{
 				p->visibility = 0;
 				lastvisinc = PlayClock + 32;
-				fi.shoot(pact, RPG);
+				fi.shoot(pact, RPG, nullptr);
 				p->ammo_amount[DEVISTATOR_WEAPON]--;
 				checkavailweapon(p);
 				if (p->ammo_amount[DEVISTATOR_WEAPON] <= 0) p->okickback_pic = p->kickback_pic = 0;
@@ -2539,7 +2524,7 @@ static void operateweapon(int snum, ESyncBits actions)
 
 				p->visibility = 0;
 				lastvisinc = PlayClock + 32;
-				fi.shoot(pact, FREEZEBLAST);
+				fi.shoot(pact, FREEZEBLAST, nullptr);
 				checkavailweapon(p);
 			}
 			if (pact->spr.scale.X < 0.5)
@@ -2567,7 +2552,7 @@ static void operateweapon(int snum, ESyncBits actions)
 			if (p->cursector->lotag != 2) 
 			{
 				p->ammo_amount[FLAMETHROWER_WEAPON]--;
-				fi.shoot(pact, FIREBALL);
+				fi.shoot(pact, FIREBALL, nullptr);
 			}
 			checkavailweapon(p);
 		}
@@ -2589,7 +2574,7 @@ static void operateweapon(int snum, ESyncBits actions)
 			p->GetActor()->restorez();
 			p->vel.Z = 0;
 			if (p->kickback_pic == 3)
-				fi.shoot(pact, HANDHOLDINGLASER);
+				fi.shoot(pact, HANDHOLDINGLASER, nullptr);
 		}
 		if (p->kickback_pic == 16)
 		{
@@ -2602,7 +2587,7 @@ static void operateweapon(int snum, ESyncBits actions)
 	case KNEE_WEAPON:
 		p->kickback_pic++;
 
-		if (p->kickback_pic == 7) fi.shoot(pact, KNEE);
+		if (p->kickback_pic == 7) fi.shoot(pact, KNEE, nullptr);
 		else if (p->kickback_pic == 14)
 		{
 			if (actions & SB_FIRE)
@@ -2621,7 +2606,7 @@ static void operateweapon(int snum, ESyncBits actions)
 			p->ammo_amount[RPG_WEAPON]--;
 			lastvisinc = PlayClock + 32;
 			p->visibility = 0;
-			fi.shoot(pact, RPG);
+			fi.shoot(pact, RPG, nullptr);
 			checkavailweapon(p);
 		}
 		else if (p->kickback_pic == 20)
diff --git a/source/games/duke/src/player_r.cpp b/source/games/duke/src/player_r.cpp
index ba0385c67..60199ad4a 100644
--- a/source/games/duke/src/player_r.cpp
+++ b/source/games/duke/src/player_r.cpp
@@ -810,7 +810,7 @@ static void shootmortar(DDukeActor* actor, int p, const DVector3& pos, DAngle an
 //
 //---------------------------------------------------------------------------
 
-void shoot_r(DDukeActor* actor, int atwith)
+void shoot_r(DDukeActor* actor, int atwith, PClass* cls)
 {
 	int p;
 	DVector3 spos;
@@ -818,18 +818,17 @@ void shoot_r(DDukeActor* actor, int atwith)
 
 	auto const sect = actor->sector();
 
+	sang = actor->spr.Angles.Yaw;
 	if (actor->isPlayer())
 	{
 		p = actor->PlayerIndex();
-		spos = ps[p].GetActor()->getPosWithOffsetZ().plusZ(ps[p].pyoff + 4);
-		sang = ps[p].GetActor()->spr.Angles.Yaw;
+		spos = actor->getPosWithOffsetZ().plusZ(ps[p].pyoff + 4);
 
 		if (isRRRA()) ps[p].crack_time = CRACK_TIME;
 	}
 	else
 	{
 		p = -1;
-		sang = actor->spr.Angles.Yaw;
 		spos = actor->spr.pos.plusZ(-(actor->spr.scale.Y * tileHeight(actor->spr.picnum) * 0.5) - 3);
 
 		if (badguy(actor))
@@ -839,23 +838,18 @@ void shoot_r(DDukeActor* actor, int atwith)
 		}
 	}
 	
-	SetGameVarID(g_iAtWithVarID, atwith, actor, p);
-	SetGameVarID(g_iReturnVarID, 0, actor, p);
-	OnEvent(EVENT_SHOOT, p, ps[p].GetActor(), -1);
-	if (GetGameVarID(g_iReturnVarID, actor, p).safeValue() != 0)
+	if (cls == nullptr)
 	{
-		return;
+		auto info = spawnMap.CheckKey(atwith);
+		if (info)
+		{
+			cls = static_cast<PClassActor*>(info->Class(atwith));
+		}
 	}
+	if (cls && cls->IsDescendantOf(RUNTIME_CLASS(DDukeActor)) && CallShootThis(static_cast<DDukeActor*>(GetDefaultByType(cls)), actor, p, spos, sang)) return;
 
 	switch (atwith)
 	{
-	case BLOODSPLAT1:
-	case BLOODSPLAT2:
-	case BLOODSPLAT3:
-	case BLOODSPLAT4:
-		shootbloodsplat(actor, p, spos, sang, atwith, BIGFORCE);
-		return;
-
 	case SLINGBLADE:
 		if (!isRRRA()) break;
 		[[fallthrough]];
@@ -2726,7 +2720,7 @@ static void operateweapon(int snum, ESyncBits actions, sectortype* psectp)
 	case PISTOL_WEAPON:
 		if (p->kickback_pic == 1)
 		{
-			fi.shoot(pact, SHOTSPARK1);
+			fi.shoot(pact, SHOTSPARK1, nullptr);
 			S_PlayActorSound(PISTOL_FIRE, pact);
 			p->noise_radius = 512;
 			madenoise(snum);
@@ -2791,16 +2785,16 @@ static void operateweapon(int snum, ESyncBits actions, sectortype* psectp)
 
 		if (p->kickback_pic == 4)
 		{
-			fi.shoot(pact, SHOTGUN);
-			fi.shoot(pact, SHOTGUN);
-			fi.shoot(pact, SHOTGUN);
-			fi.shoot(pact, SHOTGUN);
-			fi.shoot(pact, SHOTGUN);
-			fi.shoot(pact, SHOTGUN);
-			fi.shoot(pact, SHOTGUN);
-			fi.shoot(pact, SHOTGUN);
-			fi.shoot(pact, SHOTGUN);
-			fi.shoot(pact, SHOTGUN);
+			fi.shoot(pact, SHOTGUN, nullptr);
+			fi.shoot(pact, SHOTGUN, nullptr);
+			fi.shoot(pact, SHOTGUN, nullptr);
+			fi.shoot(pact, SHOTGUN, nullptr);
+			fi.shoot(pact, SHOTGUN, nullptr);
+			fi.shoot(pact, SHOTGUN, nullptr);
+			fi.shoot(pact, SHOTGUN, nullptr);
+			fi.shoot(pact, SHOTGUN, nullptr);
+			fi.shoot(pact, SHOTGUN, nullptr);
+			fi.shoot(pact, SHOTGUN, nullptr);
 
 			p->ammo_amount[SHOTGUN_WEAPON]--;
 
@@ -2817,16 +2811,16 @@ static void operateweapon(int snum, ESyncBits actions, sectortype* psectp)
 		{
 			if (p->shotgun_state[1])
 			{
-				fi.shoot(pact, SHOTGUN);
-				fi.shoot(pact, SHOTGUN);
-				fi.shoot(pact, SHOTGUN);
-				fi.shoot(pact, SHOTGUN);
-				fi.shoot(pact, SHOTGUN);
-				fi.shoot(pact, SHOTGUN);
-				fi.shoot(pact, SHOTGUN);
-				fi.shoot(pact, SHOTGUN);
-				fi.shoot(pact, SHOTGUN);
-				fi.shoot(pact, SHOTGUN);
+				fi.shoot(pact, SHOTGUN, nullptr);
+				fi.shoot(pact, SHOTGUN, nullptr);
+				fi.shoot(pact, SHOTGUN, nullptr);
+				fi.shoot(pact, SHOTGUN, nullptr);
+				fi.shoot(pact, SHOTGUN, nullptr);
+				fi.shoot(pact, SHOTGUN, nullptr);
+				fi.shoot(pact, SHOTGUN, nullptr);
+				fi.shoot(pact, SHOTGUN, nullptr);
+				fi.shoot(pact, SHOTGUN, nullptr);
+				fi.shoot(pact, SHOTGUN, nullptr);
 
 				p->ammo_amount[SHOTGUN_WEAPON]--;
 
@@ -2917,7 +2911,7 @@ static void operateweapon(int snum, ESyncBits actions, sectortype* psectp)
 				}
 
 				S_PlayActorSound(CHAINGUN_FIRE, pact);
-				fi.shoot(pact, CHAINGUN);
+				fi.shoot(pact, CHAINGUN, nullptr);
 				p->noise_radius = 512;
 				madenoise(snum);
 				lastvisinc = PlayClock + 32;
@@ -2949,7 +2943,7 @@ static void operateweapon(int snum, ESyncBits actions, sectortype* psectp)
 		if (p->kickback_pic > 3)
 		{
 			p->okickback_pic = p->kickback_pic = 0;
-			fi.shoot(pact, GROWSPARK);
+			fi.shoot(pact, GROWSPARK, nullptr);
 			p->noise_radius = 64;
 			madenoise(snum);
 			checkavailweapon(p);
@@ -2962,7 +2956,7 @@ static void operateweapon(int snum, ESyncBits actions, sectortype* psectp)
 		if (p->kickback_pic == 1)
 		{
 			p->ammo_amount[THROWSAW_WEAPON]--;
-			fi.shoot(pact, SHRINKSPARK);
+			fi.shoot(pact, SHRINKSPARK, nullptr);
 			checkavailweapon(p);
 		}
 		p->kickback_pic++;
@@ -2977,7 +2971,7 @@ static void operateweapon(int snum, ESyncBits actions, sectortype* psectp)
 			p->visibility = 0;
 			lastvisinc = PlayClock + 32;
 			S_PlayActorSound(CHAINGUN_FIRE, pact);
-			fi.shoot(pact, SHOTSPARK1);
+			fi.shoot(pact, SHOTSPARK1, nullptr);
 			p->noise_radius = 1024;
 			madenoise(snum);
 			p->ammo_amount[TIT_WEAPON]--;
@@ -3004,7 +2998,7 @@ static void operateweapon(int snum, ESyncBits actions, sectortype* psectp)
 			p->visibility = 0;
 			lastvisinc = PlayClock + 32;
 			S_PlayActorSound(CHAINGUN_FIRE, pact);
-			fi.shoot(pact, CHAINGUN);
+			fi.shoot(pact, CHAINGUN, nullptr);
 			p->noise_radius = 1024;
 			madenoise(snum);
 			p->ammo_amount[MOTORCYCLE_WEAPON]--;
@@ -3031,7 +3025,7 @@ static void operateweapon(int snum, ESyncBits actions, sectortype* psectp)
 		{
 			p->MotoSpeed -= 20;
 			p->ammo_amount[BOAT_WEAPON]--;
-			fi.shoot(pact, BOATGRENADE);
+			fi.shoot(pact, BOATGRENADE, nullptr);
 		}
 		p->kickback_pic++;
 		if (p->kickback_pic > 20)
@@ -3048,7 +3042,7 @@ static void operateweapon(int snum, ESyncBits actions, sectortype* psectp)
 	case ALIENBLASTER_WEAPON:
 		p->kickback_pic++;
 		if (p->kickback_pic >= 7 && p->kickback_pic <= 11)
-			fi.shoot(pact, FIRELASER);
+			fi.shoot(pact, FIRELASER, nullptr);
 
 		if (p->kickback_pic == 5)
 		{
@@ -3106,7 +3100,7 @@ static void operateweapon(int snum, ESyncBits actions, sectortype* psectp)
 		{
 			p->ammo_amount[BOWLING_WEAPON]--;
 			S_PlayActorSound(354, pact);
-			fi.shoot(pact, BOWLINGBALL);
+			fi.shoot(pact, BOWLINGBALL, nullptr);
 			p->noise_radius = 64;
 			madenoise(snum);
 		}
@@ -3129,7 +3123,7 @@ static void operateweapon(int snum, ESyncBits actions, sectortype* psectp)
 			S_PlayActorSound(426, pact);
 		if (p->kickback_pic == 12)
 		{
-			fi.shoot(pact, KNEE);
+			fi.shoot(pact, KNEE, nullptr);
 			p->noise_radius = 64;
 			madenoise(snum);
 		}
@@ -3147,7 +3141,7 @@ static void operateweapon(int snum, ESyncBits actions, sectortype* psectp)
 			S_PlayActorSound(252, pact);
 		if (p->kickback_pic == 8)
 		{
-			fi.shoot(pact, SLINGBLADE);
+			fi.shoot(pact, SLINGBLADE, nullptr);
 			p->noise_radius = 64;
 			madenoise(snum);
 		}
@@ -3167,7 +3161,7 @@ static void operateweapon(int snum, ESyncBits actions, sectortype* psectp)
 				p->ammo_amount[DYNAMITE_WEAPON]--;
 			lastvisinc = PlayClock + 32;
 			p->visibility = 0;
-			fi.shoot(pact, RPG);
+			fi.shoot(pact, RPG, nullptr);
 			p->noise_radius = 2048;
 			madenoise(snum);
 			checkavailweapon(p);
@@ -3185,7 +3179,7 @@ static void operateweapon(int snum, ESyncBits actions, sectortype* psectp)
 			p->ammo_amount[CHICKEN_WEAPON]--;
 			lastvisinc = PlayClock + 32;
 			p->visibility = 0;
-			fi.shoot(pact, RPG2);
+			fi.shoot(pact, RPG2, nullptr);
 			p->noise_radius = 2048;
 			madenoise(snum);
 			checkavailweapon(p);
diff --git a/source/games/duke/src/player_w.cpp b/source/games/duke/src/player_w.cpp
index ebff77310..c21b94927 100644
--- a/source/games/duke/src/player_w.cpp
+++ b/source/games/duke/src/player_w.cpp
@@ -65,10 +65,10 @@ void DoFire(player_struct* p, int snum)
 
 	SetGameVarID(g_iWeaponVarID, p->curr_weapon, p->GetActor(), snum);
 	SetGameVarID(g_iWorksLikeVarID, aplWeaponWorksLike(p->curr_weapon, snum), p->GetActor(), snum);
-	fi.shoot(p->GetActor(), aplWeaponShoots(p->curr_weapon, snum));
+	fi.shoot(p->GetActor(), aplWeaponShoots(p->curr_weapon, snum), nullptr);
 	for (i = 1; i < aplWeaponShotsPerBurst(p->curr_weapon, snum); i++)
 	{
-		fi.shoot(p->GetActor(), aplWeaponShoots(p->curr_weapon, snum));
+		fi.shoot(p->GetActor(), aplWeaponShoots(p->curr_weapon, snum), nullptr);
 		if (aplWeaponFlags(p->curr_weapon, snum) & WEAPON_FLAG_AMMOPERSHOT)
 		{
 			p->ammo_amount[p->curr_weapon]--;
@@ -405,7 +405,7 @@ void operateweapon_ww(int snum, ESyncBits actions)
 				}
 				SetGameVarID(g_iWeaponVarID, p->curr_weapon, p->GetActor(), snum);
 				SetGameVarID(g_iWorksLikeVarID, aplWeaponWorksLike(p->curr_weapon, snum), p->GetActor(), snum);
-				fi.shoot(p->GetActor(), aplWeaponShoots(p->curr_weapon, snum));
+				fi.shoot(p->GetActor(), aplWeaponShoots(p->curr_weapon, snum), nullptr);
 			}
 		}
 
diff --git a/source/games/duke/src/sectors_d.cpp b/source/games/duke/src/sectors_d.cpp
index 4fe24c0f5..038d597f7 100644
--- a/source/games/duke/src/sectors_d.cpp
+++ b/source/games/duke/src/sectors_d.cpp
@@ -969,10 +969,10 @@ void checkhitdefault_d(DDukeActor* targ, DDukeActor* proj)
 			if (Owner && Owner->isPlayer() && targ->spr.picnum != ROTATEGUN && targ->spr.picnum != DRONE)
 				if (ps[Owner->PlayerIndex()].curr_weapon == SHOTGUN_WEAPON)
 				{
-					fi.shoot(targ, BLOODSPLAT3);
-					fi.shoot(targ, BLOODSPLAT1);
-					fi.shoot(targ, BLOODSPLAT2);
-					fi.shoot(targ, BLOODSPLAT4);
+					fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat3"));
+					fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat1"));
+					fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat2"));
+					fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat4"));
 				}
 
 			if (!actorflag(targ, SFLAG2_NODAMAGEPUSH) && !bossguy(targ)) // RR does not have this.
@@ -1172,7 +1172,7 @@ void checkhitsprite_d(DDukeActor* targ, DDukeActor* proj)
 	case FETUSBROKE:
 		for (j = 0; j < 48; j++)
 		{
-			fi.shoot(targ, BLOODSPLAT1);
+			fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat1"));
 			targ->spr.Angles.Yaw += DAngle1 * 58.5; // Was 333, which really makes no sense.
 		}
 		S_PlayActorSound(GLASS_HEAVYBREAK, targ);
@@ -1254,21 +1254,21 @@ void checkhitsprite_d(DDukeActor* targ, DDukeActor* proj)
 		targ->spr.extra -= proj->spr.extra;
 		if (targ->spr.extra > 0) break;
 		targ->spr.Angles.Yaw = randomAngle();
-		fi.shoot(targ, BLOODSPLAT1);
+		fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat1"));
 		targ->spr.Angles.Yaw = randomAngle();
-		fi.shoot(targ, BLOODSPLAT2);
+		fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat2"));
 		targ->spr.Angles.Yaw = randomAngle();
-		fi.shoot(targ, BLOODSPLAT3);
+		fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat3"));
 		targ->spr.Angles.Yaw = randomAngle();
-		fi.shoot(targ, BLOODSPLAT4);
+		fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat4"));
 		targ->spr.Angles.Yaw = randomAngle();
-		fi.shoot(targ, BLOODSPLAT1);
+		fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat1"));
 		targ->spr.Angles.Yaw = randomAngle();
-		fi.shoot(targ, BLOODSPLAT2);
+		fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat2"));
 		targ->spr.Angles.Yaw = randomAngle();
-		fi.shoot(targ, BLOODSPLAT3);
+		fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat3"));
 		targ->spr.Angles.Yaw = randomAngle();
-		fi.shoot(targ, BLOODSPLAT4);
+		fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat4"));
 		spawnguts(targ, PClass::FindActor("DukeJibs1"), 1);
 		spawnguts(targ, PClass::FindActor("DukeJibs2"), 2);
 		spawnguts(targ, PClass::FindActor("DukeJibs3"), 3);
diff --git a/source/games/duke/src/sectors_r.cpp b/source/games/duke/src/sectors_r.cpp
index 6b3b64170..bf92f795e 100644
--- a/source/games/duke/src/sectors_r.cpp
+++ b/source/games/duke/src/sectors_r.cpp
@@ -1333,10 +1333,10 @@ void checkhitdefault_r(DDukeActor* targ, DDukeActor* proj)
 			if (Owner && Owner->isPlayer() && targ->spr.picnum != DRONE)
 				if (ps[Owner->PlayerIndex()].curr_weapon == SHOTGUN_WEAPON)
 				{
-					fi.shoot(targ, BLOODSPLAT3);
-					fi.shoot(targ, BLOODSPLAT1);
-					fi.shoot(targ, BLOODSPLAT2);
-					fi.shoot(targ, BLOODSPLAT4);
+					fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat3"));
+					fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat1"));
+					fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat2"));
+					fi.shoot(targ, -1, PClass::FindActor("DukeBloodSplat4"));
 				}
 
 			if (targ->spr.statnum == STAT_ZOMBIEACTOR)
diff --git a/source/games/duke/src/spawn_d.cpp b/source/games/duke/src/spawn_d.cpp
index 60514c29d..5c7dd5e62 100644
--- a/source/games/duke/src/spawn_d.cpp
+++ b/source/games/duke/src/spawn_d.cpp
@@ -251,20 +251,6 @@ DDukeActor* spawninit_d(DDukeActor* actj, DDukeActor* act, TArray<DDukeActor*>*
 		ChangeActorStat(act, STAT_MISC);
 		break;
 
-	case BLOODSPLAT1:
-	case BLOODSPLAT2:
-	case BLOODSPLAT3:
-	case BLOODSPLAT4:
-		act->spr.cstat |= CSTAT_SPRITE_ALIGNMENT_WALL;
-		act->spr.scale.X = (0.109375 + (krand() & 7) * REPEAT_SCALE);
-		act->spr.scale.Y = (0.109375 + (krand() & 7) * REPEAT_SCALE);
-		act->spr.pos.Z -= 16;
-		if (actj && actj->spr.pal == 6)
-			act->spr.pal = 6;
-		insertspriteq(act);
-		ChangeActorStat(act, STAT_MISC);
-		break;
-
 	case SPACEMARINE:
 		act->spr.extra = 20;
 		act->spr.cstat |= CSTAT_SPRITE_BLOCK_ALL;
diff --git a/source/games/duke/src/spawn_r.cpp b/source/games/duke/src/spawn_r.cpp
index b4dc139e9..26f79cda4 100644
--- a/source/games/duke/src/spawn_r.cpp
+++ b/source/games/duke/src/spawn_r.cpp
@@ -194,20 +194,6 @@ DDukeActor* spawninit_r(DDukeActor* actj, DDukeActor* act, TArray<DDukeActor*>*
 		act->spr.cstat |= CSTAT_SPRITE_ALIGNMENT_FLOOR;
 		[[fallthrough]];
 
-	case BLOODSPLAT1:
-	case BLOODSPLAT2:
-	case BLOODSPLAT3:
-	case BLOODSPLAT4:
-		act->spr.cstat |= CSTAT_SPRITE_ALIGNMENT_WALL;
-		act->spr.scale.X = (0.109375 + (krand() & 7) * REPEAT_SCALE);
-		act->spr.scale.Y = (0.109375 + (krand() & 7) * REPEAT_SCALE);
-		act->spr.pos.Z -= 16;
-		if (actj && actj->spr.pal == 6)
-			act->spr.pal = 6;
-		insertspriteq(act);
-		ChangeActorStat(act, STAT_MISC);
-		break;
-
 	case HYDRENT:
 	case SATELITE:
 	case FUELPOD:
diff --git a/source/games/duke/src/vmexports.cpp b/source/games/duke/src/vmexports.cpp
index bb2a63658..5880b42e5 100644
--- a/source/games/duke/src/vmexports.cpp
+++ b/source/games/duke/src/vmexports.cpp
@@ -619,12 +619,11 @@ void DukeActor_shoot(DDukeActor* act, int intname)
 	{
 		auto cls = PClass::FindActor(FName(ENamedName(intname)));
 		assert(cls);
-		picnum = GetDefaultByType(cls)->spr.picnum;
-		fi.shoot(act, picnum); // for now this crutch must suffice.
+		fi.shoot(act, -1, cls);
 	}
 	else
 	{
-		fi.shoot(act, picnum);
+		fi.shoot(act, picnum, nullptr);
 	}
 }
 
@@ -1138,16 +1137,17 @@ DEFINE_ACTION_FUNCTION_NATIVE(_DukeLevel, floorflags, duke_floorflags)
 	ACTION_RETURN_INT(duke_floorflags(sect));
 }
 
-int duke_wallflags(walltype* wal)
+int duke_wallflags(walltype* wal, int which)
 {
-	return tileflags(wal->picnum);
+	return tileflags(which? wal->overpicnum : wal->picnum);
 }
 
 DEFINE_ACTION_FUNCTION_NATIVE(_DukeLevel, wallflags, duke_wallflags)
 {
 	PARAM_PROLOGUE;
 	PARAM_POINTER(sect, walltype);
-	ACTION_RETURN_INT(duke_wallflags(sect));
+	PARAM_INT(which);
+	ACTION_RETURN_INT(duke_wallflags(sect, which));
 }
 
 int duke_ismirror(walltype* wal)
diff --git a/wadsrc/static/filter/dukeengine/engine/defines.def b/wadsrc/static/filter/dukeengine/engine/defines.def
index c083b905b..288635735 100644
--- a/wadsrc/static/filter/dukeengine/engine/defines.def
+++ b/wadsrc/static/filter/dukeengine/engine/defines.def
@@ -9,3 +9,5 @@ define TFLAG_SLIME			16
 define TFLAG_DOORWALL		32
 define TFLAG_BLOCKDOOR		64
 define TFLAG_OUTERSPACE		128
+define TFLAG_NOBLOODSPLAT	256
+define TFLAG_NOCIRCLEREFLECT 512
diff --git a/wadsrc/static/filter/dukelike/engine/engine.def b/wadsrc/static/filter/dukelike/engine/engine.def
index 7daba65d5..5bf434ba0 100644
--- a/wadsrc/static/filter/dukelike/engine/engine.def
+++ b/wadsrc/static/filter/dukelike/engine/engine.def
@@ -51,6 +51,10 @@ spawnclasses
 	1960 = DukeRecon
 	2300 = DukeOoz
 	2309 = DukeOoz2
+	2296 = DukeBloodSplat1
+	2297 = DukeBloodSplat2
+	2298 = DukeBloodSplat3
+	2299 = DukeBloodSplat4
 
 	1272 = DukeTrash
 	634 = DukeBolt1
@@ -233,3 +237,7 @@ tileflag TFLAG_OUTERSPACE {
 	MOONSKY1
 	BIGORBIT1
 	}
+
+tileflag TFLAG_NOBLOODSPLAT {
+	BIGFORCE
+	}
diff --git a/wadsrc/static/filter/redneck/engine/engine.def b/wadsrc/static/filter/redneck/engine/engine.def
index f52223147..775e669ba 100644
--- a/wadsrc/static/filter/redneck/engine/engine.def
+++ b/wadsrc/static/filter/redneck/engine/engine.def
@@ -52,6 +52,10 @@ spawnclasses
 	1344 = DukeRat
 	1759 = DukeForceSphere
 	1529 = DukeOoz
+	1525 = DukeBloodSplat1
+	1526 = DukeBloodSplat2
+	1527 = DukeBloodSplat3
+	1528 = DukeBloodSplat4
 
 	285 = RedneckChickenSpawner1
 	286 = RedneckChickenSpawner2
diff --git a/wadsrc/static/zscript.txt b/wadsrc/static/zscript.txt
index a1107b5ef..79d435830 100644
--- a/wadsrc/static/zscript.txt
+++ b/wadsrc/static/zscript.txt
@@ -57,6 +57,7 @@ version "4.10"
 #include "zscript/games/duke/actors/soundcontroller.zs"
 #include "zscript/games/duke/actors/respawncontroller.zs"
 #include "zscript/games/duke/actors/respawnmarker.zs"
+#include "zscript/games/duke/actors/bloodsplats.zs"
 
 #include "zscript/games/duke/actors/rat.zs"
 #include "zscript/games/duke/actors/jibs.zs"
diff --git a/wadsrc/static/zscript/games/duke/actors/bloodsplats.zs b/wadsrc/static/zscript/games/duke/actors/bloodsplats.zs
new file mode 100644
index 000000000..b8ea4d7e0
--- /dev/null
+++ b/wadsrc/static/zscript/games/duke/actors/bloodsplats.zs
@@ -0,0 +1,129 @@
+class DukeBloodSplat1 : DukeActor
+{
+	default
+	{
+		statnum STAT_MISC;
+		Pic "BLOODSPLAT1";
+	}
+
+	override void Initialize()
+	{
+		self.cstat |= CSTAT_SPRITE_ALIGNMENT_WALL;
+		self.scale.X = 0.109375 + random(0, 7) * REPEAT_SCALE;
+		self.scale.Y = 0.109375 + random(0, 7) * REPEAT_SCALE;
+		self.pos.Z -= 16;
+		if (self.ownerActor && self.ownerActor.pal == 6)
+			self.pal = 6;
+		self.insertspriteq();
+	}
+
+	override void Tick()
+	{
+		if (self.temp_data[0] < 14 * 26)
+		{
+			let offset = frandom(0, 1);
+			let lerp = 1. - (double(self.temp_data[0]) / (14 * 26));
+			let zadj = (1. / 16.) * lerp;
+			let sadj = (1. / 12.) * lerp * REPEAT_SCALE;
+			self.pos.Z += zadj + offset * zadj;
+			self.scale.Y += sadj + offset * sadj;
+			self.temp_data[0]++;
+		}
+	}
+	
+	override bool Animate(tspritetype t)
+	{
+		if (t.pal == 6)
+			t.shade = -127;
+		else
+			t.shade = 16;
+		return true;
+	}
+	
+	override bool shootthis(DukeActor shooter, DukePlayer p, Vector3 pos, double ang) const
+	{
+		let sectp = shooter.sector;
+		double zvel;
+		HitInfo hit;
+
+		if (p != null) ang += frandom(-11.25, 11.25);
+		else ang += 180 + frandom(-11.25, 11.25);
+		
+		zvel = 4 - frandom(0, 8);
+
+
+		Raze.hitscan(pos, sectp, (ang.ToVector() * 1024, zvel * 64), hit, 1);
+
+		let wal = hit.hitWall;
+		if ( (pos.XY - hit.hitpos.XY).Length() >= 64)
+			return true;
+		if (wal == null)
+			return true;
+		if (wal.hitag != 0)
+			return true;
+		if (dlevel.wallflags(wal, 1) & Duke.TFLAG_NOBLOODSPLAT)
+			return true;
+		if ((wal.cstat & CSTAT_WALL_MASKED) != 0)
+			return true;
+		if (hit.hitSector.lotag != 0)
+			return true;
+		
+		if (wal.twoSided())
+		{
+			if (wal.nextSectorp().lotag != 0 || (hit.hitSector.floorz - wal.nextSectorp().floorz) <= 16)
+				return true;
+			if (hit.hitSector.lotag != 0)
+				return true;
+			if (wal.nextWallp().hitag != 0)
+				return true;
+			
+			DukeSectIterator it;
+			for(let act2 = it.First(wal.nextSectorp()); act2; act2 = it.Next())
+			{
+				if (act2.statnum == STAT_EFFECTOR && act2.lotag == SE_13_EXPLOSIVE)
+				{
+					return true;
+				}
+			}
+		}
+		let spawned = shooter.spawn(self.getclassname());
+		if (spawned)
+		{
+			spawned.vel.X = -0.75;
+			spawned.Angle = wal.delta().Angle() - 90;
+			spawned.pos = hit.hitpos;
+			spawned.cstat |= randomXFlip();
+			spawned.DoMove(CLIPMASK0);
+			spawned.SetPosition(spawned.pos);
+			spawned.cstat2 |= CSTAT2_SPRITE_DECAL;
+
+			if (shooter.actorflag2(SFLAG2_GREENBLOOD))
+				spawned.pal = 6;
+		}
+		return true;
+	}
+}
+
+class DukeBloodSplat2 : DukeBloodSplat1
+{
+	default
+	{
+		Pic "BLOODSPLAT2";
+	}
+}
+
+class DukeBloodSplat3 : DukeBloodSplat1
+{
+	default
+	{
+		Pic "BLOODSPLAT3";
+	}
+}
+
+class DukeBloodSplat4 : DukeBloodSplat1
+{
+	default
+	{
+		Pic "BLOODSPLAT4";
+	}
+}
diff --git a/wadsrc/static/zscript/games/duke/dukeactor.zs b/wadsrc/static/zscript/games/duke/dukeactor.zs
index 5e43f936e..52f775200 100644
--- a/wadsrc/static/zscript/games/duke/dukeactor.zs
+++ b/wadsrc/static/zscript/games/duke/dukeactor.zs
@@ -189,6 +189,10 @@ class DukeActor : CoreActor native
 	virtual void onRespawn(int tag) { }
 	virtual bool animate(tspritetype tspr) { return false; }
 	virtual void RunState() {}	// this is the CON function.
+	virtual bool shootthis(DukeActor actor, DukePlayer p, Vector3 pos, double ang) const // this gets called on the defaults.
+	{
+		return false;
+	}
 	
 	native void RandomScrap();
 	native void hitradius(int r, int hp1, int hp2, int hp3, int hp4);
@@ -231,7 +235,7 @@ struct DukeLevel
 	native static void operatemasterswitches(int lotag);
 	native static void operateactivators(int lotag, DukePlayer p);
 	native static int floorflags(sectortype s);
-	native static int wallflags(walltype s);
+	native static int wallflags(walltype s, int which);
 	native static void AddCycler(sectortype sector, int lotag, int shade, int shade2, int hitag, int state);
 	native static void addtorch(sectortype sector, int shade, int lotag);
 	native static void addlightning(sectortype sector, int shade);
diff --git a/wadsrc/static/zscript/games/duke/dukegame.zs b/wadsrc/static/zscript/games/duke/dukegame.zs
index c0f906701..a86beda0b 100644
--- a/wadsrc/static/zscript/games/duke/dukegame.zs
+++ b/wadsrc/static/zscript/games/duke/dukegame.zs
@@ -70,12 +70,16 @@ struct Duke native
 
 	enum ETextureFlags
 	{
-		TFLAG_WALLSWITCH			= 1,
-		TFLAG_ADULT					= 2,
-		TFLAG_ELECTRIC				= 4,
-		TFLAG_CLEARINVENTORY		= 8,	// really dumb Duke stuff...
-		TFLAG_SLIME					= 16,
-		TFLAG_NOCIRCLEREFLECT			= 32,
+		TFLAG_WALLSWITCH			= 1 << 0,
+		TFLAG_ADULT					= 1 << 1,
+		TFLAG_ELECTRIC				= 1 << 2,
+		TFLAG_CLEARINVENTORY		= 1 << 3,	// really dumb Duke stuff...
+		TFLAG_SLIME					= 1 << 4,
+		TFLAG_DOORWALL				= 1 << 5,
+		TFLAG_BLOCKDOOR				= 1 << 6,
+		TFLAG_OUTERSPACE			= 1 << 7,
+		TFLAG_NOBLOODSPLAT			= 1 << 8,
+		TFLAG_NOCIRCLEREFLECT		= 1 << 9,
 	};
 
 	enum ESoundFlags
diff --git a/wadsrc/static/zscript/razebase.zs b/wadsrc/static/zscript/razebase.zs
index beb3fe444..ba87fa4a5 100644
--- a/wadsrc/static/zscript/razebase.zs
+++ b/wadsrc/static/zscript/razebase.zs
@@ -150,8 +150,16 @@ struct CollisionData
 	native void setWall(walltype w);
 	native void setActor(CoreActor a);
 	native void setVoid();
-
 }
+
+struct HitInfo
+{
+	Vector3 hitpos;
+	sectortype hitSector;
+	walltype hitWall;
+	CoreActor hitActor;
+}
+
 struct Raze
 {
 	const kAngleMask	= 0x7FF;
@@ -173,6 +181,7 @@ struct Raze
 	native static sectortype updatesector(Vector2 pos, sectortype lastsect, double maxdist = 96);
 	native static sectortype, Vector3 clipmove(Vector3 pos, sectortype sect, Vector2 move, double walldist, double ceildist, double flordist, uint cliptype, CollisionData coll, int clipmoveboxtracenum = 3);
 	native static bool cansee(Vector3 start, sectortype startsec, Vector3 end, sectortype endsec);
+	native static int hitscan(Vector3 start, sectortype startsect, Vector3 vect, HitInfo hitinfo, uint cliptype, double maxrange = -1);
 
 	// game check shortcuts