diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 29adb478abfffd58a5d564769382c6290c6acefd..ba70546fb366dccddf4740797d15bb7fb5a4ef68 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -15,6 +15,7 @@
 #include "p_local.h"
 #include "p_setup.h" // So we can have P_SetupLevelSky
 #include "p_slopes.h" // P_GetSlopeZAt
+#include "p_haptic.h"
 #include "z_zone.h"
 #include "r_main.h"
 #include "r_draw.h"
@@ -1732,6 +1733,78 @@ static int lib_pPlayerShouldUseSpinHeight(lua_State *L)
 	return 1;
 }
 
+// P_HAPTIC
+///////////
+#define GET_OPTIONAL_PLAYER(arg) \
+	player_t *player = NULL; \
+	if (!lua_isnoneornil(L, arg)) { \
+		player = *((player_t **)luaL_checkudata(L, arg, META_PLAYER)); \
+		if (!player) \
+			return LUA_ErrInvalid(L, "player_t"); \
+	}
+
+static int lib_pDoRumble(lua_State *L)
+{
+	GET_OPTIONAL_PLAYER(1);
+	fixed_t large_magnitude = luaL_checkfixed(L, 2);
+	fixed_t small_magnitude = luaL_optfixed(L, 3, large_magnitude);
+	tic_t duration = luaL_optinteger(L, 4, 0);
+
+#define CHECK_MAGNITUDE(which) \
+	if (which##_magnitude < 0 || which##_magnitude > FRACUNIT) \
+		return luaL_error(L, va(#which " motor frequency %f out of range (minimum is 0.0, maximum is 1.0)", \
+			FixedToFloat(which##_magnitude)))
+
+	CHECK_MAGNITUDE(large);
+	CHECK_MAGNITUDE(small);
+
+#undef CHECK_MAGNITUDE
+
+	lua_pushboolean(L, P_DoRumble(player, large_magnitude, small_magnitude, duration));
+	return 1;
+}
+
+static int lib_pPauseRumble(lua_State *L)
+{
+	GET_OPTIONAL_PLAYER(1);
+	P_PauseRumble(player);
+	return 0;
+}
+
+static int lib_pUnpauseRumble(lua_State *L)
+{
+	GET_OPTIONAL_PLAYER(1);
+	P_UnpauseRumble(player);
+	return 0;
+}
+
+static int lib_pIsRumbleEnabled(lua_State *L)
+{
+	GET_OPTIONAL_PLAYER(1);
+	if (player && P_IsLocalPlayer(player))
+		lua_pushboolean(L, P_IsRumbleEnabled(player));
+	else
+		lua_pushnil(L);
+	return 1;
+}
+
+static int lib_pIsRumblePaused(lua_State *L)
+{
+	GET_OPTIONAL_PLAYER(1);
+	if (player && P_IsLocalPlayer(player))
+		lua_pushboolean(L, P_IsRumblePaused(player));
+	else
+		lua_pushnil(L);
+	return 1;
+}
+
+static int lib_pStopRumble(lua_State *L)
+{
+	GET_OPTIONAL_PLAYER(1);
+	P_StopRumble(player);
+	return 0;
+}
+
 // P_MAP
 ///////////
 
@@ -3368,7 +3441,6 @@ static int lib_sResumeMusic(lua_State *L)
 // G_GAME
 ////////////
 
-// Copypasted from lib_cvRegisterVar :]
 static int lib_gAddGametype(lua_State *L)
 {
 	const char *k;
@@ -4090,6 +4162,14 @@ static luaL_Reg lib[] = {
 	{"P_PlayerCanEnterSpinGaps",lib_pPlayerCanEnterSpinGaps},
 	{"P_PlayerShouldUseSpinHeight",lib_pPlayerShouldUseSpinHeight},
 
+	// p_haptic
+	{"P_DoRumble",lib_pDoRumble},
+	{"P_PauseRumble",lib_pPauseRumble},
+	{"P_UnpauseRumble",lib_pUnpauseRumble},
+	{"P_IsRumbleEnabled",lib_pIsRumbleEnabled},
+	{"P_IsRumblePaused",lib_pIsRumblePaused},
+	{"P_StopRumble",lib_pStopRumble},
+
 	// p_map
 	{"P_CheckPosition",lib_pCheckPosition},
 	{"P_TryMove",lib_pTryMove},
diff --git a/src/lua_script.h b/src/lua_script.h
index e586b04a886d292ec65669d8f831123d0d6b57d1..1c98a32f378ac0084e80d8f0267ae1d4fec8102a 100644
--- a/src/lua_script.h
+++ b/src/lua_script.h
@@ -30,11 +30,13 @@
 // TODO add some distinction between fixed numbers and integer numbers
 // for at least the purpose of printing and maybe math.
 #define luaL_checkfixed(L, i) luaL_checkinteger(L, i)
+#define luaL_optfixed(L, i, o) luaL_optinteger(L, i, o)
 #define lua_pushfixed(L, f) lua_pushinteger(L, f)
 
 // angle_t casting
 // TODO deal with signedness
 #define luaL_checkangle(L, i) ((angle_t)luaL_checkinteger(L, i))
+#define luaL_optangle(L, i, o) ((angle_t)luaL_optinteger(L, i, o))
 #define lua_pushangle(L, a) lua_pushinteger(L, a)
 
 #ifdef _DEBUG