diff --git a/source/common/2d/v_draw.cpp b/source/common/2d/v_draw.cpp
index 70bf53042..7250a11c9 100644
--- a/source/common/2d/v_draw.cpp
+++ b/source/common/2d/v_draw.cpp
@@ -324,6 +324,14 @@ DEFINE_ACTION_FUNCTION(_Screen, ClearClipRect)
 	return 0;
 }
 
+DEFINE_ACTION_FUNCTION(_Screen, ClearScreen)
+{
+	PARAM_PROLOGUE;
+	twod->ClearScreen();
+	return 0;
+}
+
+
 void F2DDrawer::GetClipRect(int *x, int *y, int *w, int *h)
 {
 	if (x) *x = clipleft;
diff --git a/source/common/scripting/interface/vmnatives.cpp b/source/common/scripting/interface/vmnatives.cpp
index 502d78de3..c196c7d93 100644
--- a/source/common/scripting/interface/vmnatives.cpp
+++ b/source/common/scripting/interface/vmnatives.cpp
@@ -49,6 +49,7 @@
 #include "s_music.h"
 #include "i_interface.h"
 #include "base_sbar.h"
+#include "image.h"
 
 //==========================================================================
 //
@@ -477,6 +478,20 @@ DEFINE_ACTION_FUNCTION_NATIVE(_TexMan, OkForLocalization, OkForLocalization_)
 	ACTION_RETURN_INT(OkForLocalization_(name, subst));
 }
 
+static int UseGamePalette(int index)
+{
+	auto tex = TexMan.GameByIndex(index, false);
+	if (!tex) return false;
+	auto image = tex->GetTexture()->GetImage();
+	return image ? image->UseGamePalette() : false;
+}
+
+DEFINE_ACTION_FUNCTION_NATIVE(_TexMan, UseGamePalette, UseGamePalette)
+{
+	PARAM_PROLOGUE;
+	PARAM_INT(texid);
+	ACTION_RETURN_INT(UseGamePalette(texid));
+}
 
 //=====================================================================================
 //
diff --git a/source/core/screenjob.cpp b/source/core/screenjob.cpp
index 8e8b83bd8..b80f22a39 100644
--- a/source/core/screenjob.cpp
+++ b/source/core/screenjob.cpp
@@ -51,11 +51,107 @@
 #include <vpx/vpx_decoder.h>
 #include <vpx/vp8dx.h>
 #include "raze_music.h"
+#include "vm.h"
 
 
 IMPLEMENT_CLASS(DScreenJob, true, false)
+IMPLEMENT_CLASS(DSkippableScreenJob, true, false)
+IMPLEMENT_CLASS(DBlackScreen, true, false)
 IMPLEMENT_CLASS(DImageScreen, true, false)
 
+DEFINE_FIELD(DScreenJob, flags)
+DEFINE_FIELD(DScreenJob, fadetime)
+DEFINE_FIELD_NAMED(DScreenJob, state, jobstate)
+DEFINE_FIELD(DScreenJob, fadestate)
+DEFINE_FIELD(DScreenJob, ticks)
+DEFINE_FIELD(DScreenJob, pausable)
+
+DEFINE_FIELD(DBlackScreen, wait)
+DEFINE_FIELD(DBlackScreen, cleared)
+
+DEFINE_FIELD(DImageScreen, tilenum)
+DEFINE_FIELD(DImageScreen, trans)
+DEFINE_FIELD(DImageScreen, waittime)
+DEFINE_FIELD(DImageScreen, cleared)
+DEFINE_FIELD(DImageScreen, texid)
+
+DEFINE_ACTION_FUNCTION(DScreenJob, Init)
+{
+	// todo
+	return 0;
+}
+
+DEFINE_ACTION_FUNCTION(DScreenJob, ProcessInput)
+{
+	PARAM_SELF_PROLOGUE(DScreenJob);
+	ACTION_RETURN_BOOL(self->ProcessInput());
+}
+
+DEFINE_ACTION_FUNCTION(DScreenJob, Start)
+{
+	PARAM_SELF_PROLOGUE(DScreenJob);
+	self->Start();
+	return 0;
+}
+
+DEFINE_ACTION_FUNCTION(DScreenJob, OnEvent)
+{
+	PARAM_SELF_PROLOGUE(DScreenJob);
+	PARAM_POINTER(evt, FInputEvent);
+	if (evt->Type != EV_KeyDown)
+	{
+		// not needed in the transition phase
+		ACTION_RETURN_BOOL(false);
+	}
+	event_t ev = {};
+	ev.type = EV_KeyDown;
+	ev.data1 = evt->KeyScan;
+	ACTION_RETURN_BOOL(self->OnEvent(&ev));
+}
+
+DEFINE_ACTION_FUNCTION(DScreenJob, OnTick)
+{
+	PARAM_SELF_PROLOGUE(DScreenJob);
+	self->OnTick();
+	return 0;
+}
+
+DEFINE_ACTION_FUNCTION(DScreenJob, Draw)
+{
+	PARAM_SELF_PROLOGUE(DScreenJob);
+	PARAM_FLOAT(smooth);
+	self->Draw(smooth);
+	return 0;
+}
+
+DEFINE_ACTION_FUNCTION(DSkippableScreenJob, Init)
+{
+	// todo
+	return 0;
+}
+
+DEFINE_ACTION_FUNCTION(DSkippableScreenJob, Skipped)
+{
+	PARAM_SELF_PROLOGUE(DSkippableScreenJob);
+	self->Skipped();
+	return 0;
+}
+
+DEFINE_ACTION_FUNCTION(DBlackScreen, Init)
+{
+	// todo
+	return 0;
+}
+
+DEFINE_ACTION_FUNCTION(DImageScreen, Init)
+{
+	// todo
+	return 0;
+}
+
+
+
+
 void DScreenJob::OnDestroy()
 {
 	if (flags & stopmusic) Mus_Stop();
diff --git a/source/core/screenjob.h b/source/core/screenjob.h
index aa02c075e..3916a3373 100644
--- a/source/core/screenjob.h
+++ b/source/core/screenjob.h
@@ -12,12 +12,13 @@ class ScreenJobRunner;
 class DScreenJob : public DObject
 {
 	DECLARE_CLASS(DScreenJob, DObject)
+public:
 	const int flags;
 	const float fadetime;	// in milliseconds
 	int fadestate = fadein;
 
 	friend class ScreenJobRunner;
-protected:
+//protected:
 	int ticks = 0;
 	int state = running;
 	bool pausable = true;
@@ -74,7 +75,8 @@ public:
 
 class DSkippableScreenJob : public DScreenJob
 {
-protected:
+	DECLARE_CLASS(DSkippableScreenJob, DScreenJob)
+public:
 	DSkippableScreenJob(int fade = 0, float fadet = 250.f) : DScreenJob(fade, fadet)
 	{}
 
@@ -90,6 +92,8 @@ protected:
 
 class DBlackScreen : public DScreenJob
 {
+	DECLARE_CLASS(DBlackScreen, DScreenJob)
+public:
 	int wait;
 	bool cleared = false;
 
@@ -109,10 +113,12 @@ class DImageScreen : public DSkippableScreenJob
 {
 	DECLARE_CLASS(DImageScreen, DScreenJob)
 
+public:
 	int tilenum = -1;
 	int trans;
 	int waittime; // in ms.
 	bool cleared = false;
+	FTextureID texid;
 	FGameTexture* tex = nullptr;
 
 public:
diff --git a/source/games/duke/src/sounds.cpp b/source/games/duke/src/sounds.cpp
index c9214b65b..8a4abf5ff 100644
--- a/source/games/duke/src/sounds.cpp
+++ b/source/games/duke/src/sounds.cpp
@@ -46,6 +46,7 @@ source as it is released.
 #include "gamestate.h"
 #include "names_d.h"
 #include "i_music.h"
+#include "vm.h"
 
 CVAR(Bool, wt_forcemidi, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) // quick hack to disable the oggs, which are of lower quality than playing the MIDIs with a good synth and sound font.
 CVAR(Bool, wt_forcevoc, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG) // The same for sound effects. The re-recordings are rather poor and disliked
@@ -719,6 +720,14 @@ void S_PlaySpecialMusic(unsigned int m)
 	}
 }
 
+DEFINE_ACTION_FUNCTION_NATIVE(_Duke, PlaySpecialMusic, S_PlaySpecialMusic)
+{
+	PARAM_PROLOGUE;
+	PARAM_INT(song);
+	S_PlaySpecialMusic(song);
+	return 0;
+}
+
 //---------------------------------------------------------------------------
 //
 //
diff --git a/wadsrc/static/zscript.txt b/wadsrc/static/zscript.txt
index da000f758..44b9bc8ee 100644
--- a/wadsrc/static/zscript.txt
+++ b/wadsrc/static/zscript.txt
@@ -28,6 +28,9 @@ version "4.3"
 
 #include "zscript/constants.zs"
 #include "zscript/razebase.zs"
+#include "zscript/screenjob.zs"
+#include "zscript/games/duke/dukegame.zs"
+#include "zscript/games/duke/ui/screens.zs"
 #include "zscript/games/duke/ui/menu.zs"
 #include "zscript/games/blood/ui/menu.zs"
 #include "zscript/games/sw/ui/menu.zs"
diff --git a/wadsrc/static/zscript/engine/base.zs b/wadsrc/static/zscript/engine/base.zs
index a0b2a0c83..3a3a4fb3b 100644
--- a/wadsrc/static/zscript/engine/base.zs
+++ b/wadsrc/static/zscript/engine/base.zs
@@ -239,6 +239,7 @@ struct TexMan
 	native static Vector2 GetScaledOffset(TextureID tex);
 	native static int CheckRealHeight(TextureID tex);
 	native static bool OkForLocalization(TextureID patch, String textSubstitute);
+	native static bool UseGamePalette(TextureID tex);
 }
 
 enum EScaleMode
@@ -402,6 +403,7 @@ struct Screen native
 	native static int, int, int, int GetViewWindow();
 	native static double, double, double, double GetFullscreenRect(double vwidth, double vheight, int fsmode);
 	native static Vector2 SetOffset(double x, double y);
+	native static void ClearScreen(color col = 0);
 }
 
 struct Font native
diff --git a/wadsrc/static/zscript/games/duke/dukegame.zs b/wadsrc/static/zscript/games/duke/dukegame.zs
new file mode 100644
index 000000000..7b47cff15
--- /dev/null
+++ b/wadsrc/static/zscript/games/duke/dukegame.zs
@@ -0,0 +1,26 @@
+// contains all global Duke definitions
+struct Duke native
+{
+	enum ESpecialMusic
+	{
+		MUS_INTRO = 0,
+		MUS_BRIEFING = 1,
+		MUS_LOADING = 2,
+	};
+	
+	enum EPalette
+	{
+		BASEPAL = 0,
+		WATERPAL,
+		SLIMEPAL,
+		TITLEPAL,
+		DREALMSPAL,
+		ENDINGPAL,  // 5
+		ANIMPAL,    // not used anymore. The anim code now generates true color textures.
+		DRUGPAL,
+		BASEPALCOUNT
+	};
+	
+
+	native static void PlaySpecialMusic(int which);
+}
diff --git a/wadsrc/static/zscript/games/duke/ui/screens.zs b/wadsrc/static/zscript/games/duke/ui/screens.zs
new file mode 100644
index 000000000..324e0a03e
--- /dev/null
+++ b/wadsrc/static/zscript/games/duke/ui/screens.zs
@@ -0,0 +1,35 @@
+
+
+//---------------------------------------------------------------------------
+//
+//
+//
+//---------------------------------------------------------------------------
+
+class DRealmsScreen : SkippableScreenJob
+{
+	void Init()
+	{
+		Super.Init(fadein | fadeout);
+	}
+
+	override void Start()
+	{
+		Duke.PlaySpecialMusic(Duke.MUS_INTRO);
+	}
+
+	override void OnTick()
+	{
+		if (ticks >= 7 * GameTicRate) jobstate = finished;
+	}
+
+	override void Draw(double smoothratio)
+	{
+		let tex = TexMan.CheckForTexture("DREALMS"); 
+		int translation = TexMan.UseGamePalette(tex)? Translation.MakeID(Translation_BasePalette, Duke.DREALMSPAL) : 0;
+
+		screen.ClearScreen();
+		screen.DrawTexture(tex, true, 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit43, DTA_TranslationIndex, translation, DTA_LegacyRenderStyle, STYLE_Normal);
+	}
+}
+
diff --git a/wadsrc/static/zscript/screenjob.zs b/wadsrc/static/zscript/screenjob.zs
new file mode 100644
index 000000000..5c8f7f25a
--- /dev/null
+++ b/wadsrc/static/zscript/screenjob.zs
@@ -0,0 +1,87 @@
+
+class ScreenJob native
+{
+	native int flags;
+	native float fadetime;	// in milliseconds
+	native int fadestate;
+
+	native int ticks;
+	native int jobstate;
+	native bool pausable;
+
+	enum EJobState
+	{
+		running = 1,	// normal operation
+		skipped = 2,	// finished by user skipping
+		finished = 3,	// finished by completing its sequence
+		stopping = 4,	// running ending animations / fadeout, etc. Will not accept more input.
+		stopped = 5,	// we're done here.
+	};
+	enum EJobFlags
+	{
+		visible = 0,
+		fadein = 1,
+		fadeout = 2,
+		stopmusic = 4,
+		stopsound = 8,
+	};
+
+	native void Init(int flags = 0, float fadet = 250.f);
+	native virtual bool ProcessInput();
+	native virtual void Start();
+	native virtual bool OnEvent(InputEvent evt);
+	native virtual void OnTick();
+	native virtual void Draw(double smoothratio);
+
+	//native int DrawFrame(double smoothratio);
+	//native int GetFadeState();
+	//native override void OnDestroy();
+}
+
+//---------------------------------------------------------------------------
+//
+//
+//
+//---------------------------------------------------------------------------
+
+class SkippableScreenJob : ScreenJob native
+{
+	native void Init(int flags = 0, float fadet = 250.f);
+	//native override bool OnEvent(InputEvent evt);
+	virtual void Skipped() {}
+}
+
+//---------------------------------------------------------------------------
+//
+//
+//
+//---------------------------------------------------------------------------
+
+class BlackScreen : ScreenJob native
+{
+	native int wait;
+	native bool cleared;
+
+	native void Init(int w, int flags = 0);
+	//override void OnTick();
+	//override void Draw(double smooth);
+}
+
+//---------------------------------------------------------------------------
+//
+//
+//
+//---------------------------------------------------------------------------
+
+class ImageScreen : SkippableScreenJob native
+{
+	native int tilenum;
+	native int trans;
+	native int waittime; // in ms.
+	native bool cleared;
+	native TextureID texid;
+
+	native void Init(TextureID tex, int fade = fadein | fadeout, int wait = 3000, int translation = 0);
+	//override void OnTick();
+	//override void Draw(double smooth);
+}