summaryrefslogtreecommitdiff
path: root/sourcemod/scripting/gokz-core/timer
diff options
context:
space:
mode:
Diffstat (limited to 'sourcemod/scripting/gokz-core/timer')
-rw-r--r--sourcemod/scripting/gokz-core/timer/pause.sp257
-rw-r--r--sourcemod/scripting/gokz-core/timer/timer.sp368
-rw-r--r--sourcemod/scripting/gokz-core/timer/virtual_buttons.sp322
3 files changed, 947 insertions, 0 deletions
diff --git a/sourcemod/scripting/gokz-core/timer/pause.sp b/sourcemod/scripting/gokz-core/timer/pause.sp
new file mode 100644
index 0000000..92ab1fb
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/timer/pause.sp
@@ -0,0 +1,257 @@
+static bool paused[MAXPLAYERS + 1];
+static bool pausedOnLadder[MAXPLAYERS + 1];
+static float lastPauseTime[MAXPLAYERS + 1];
+static bool hasPausedInThisRun[MAXPLAYERS + 1];
+static float lastResumeTime[MAXPLAYERS + 1];
+static bool hasResumedInThisRun[MAXPLAYERS + 1];
+static float lastDuckValue[MAXPLAYERS + 1];
+static float lastStaminaValue[MAXPLAYERS + 1];
+
+
+
+// =====[ PUBLIC ]=====
+
+bool GetPaused(int client)
+{
+ return paused[client];
+}
+
+void SetPausedOnLadder(int client, bool onLadder)
+{
+ pausedOnLadder[client] = onLadder;
+}
+
+void Pause(int client)
+{
+ if (!CanPause(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnPause(client, result);
+ if (result != Plugin_Continue)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Generic)");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+
+ // Pause
+ paused[client] = true;
+ pausedOnLadder[client] = Movement_GetMovetype(client) == MOVETYPE_LADDER;
+ lastDuckValue[client] = Movement_GetDuckSpeed(client);
+ lastStaminaValue[client] = GetEntPropFloat(client, Prop_Send, "m_flStamina");
+ Movement_SetVelocity(client, view_as<float>( { 0.0, 0.0, 0.0 } ));
+ Movement_SetMovetype(client, MOVETYPE_NONE);
+ if (GetTimerRunning(client))
+ {
+ hasPausedInThisRun[client] = true;
+ lastPauseTime[client] = GetEngineTime();
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnPause_Post(client);
+}
+
+bool CanPause(int client, bool showError = false)
+{
+ if (paused[client])
+ {
+ return false;
+ }
+
+ if (GetTimerRunning(client))
+ {
+ if (hasResumedInThisRun[client]
+ && GetEngineTime() - lastResumeTime[client] < GOKZ_PAUSE_COOLDOWN)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Just Resumed)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ else if (!Movement_GetOnGround(client)
+ && !(Movement_GetSpeed(client) == 0 && Movement_GetVerticalVelocity(client) == 0))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Midair)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ else if (BhopTriggersJustTouched(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Just Landed)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ else if (AntiPauseTriggerIsTouched(client))
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Pause (Anti Pause Area)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void Resume(int client, bool force = false)
+{
+ if (!paused[client])
+ {
+ return;
+ }
+ if (!force && !CanResume(client, true))
+ {
+ return;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnResume(client, result);
+ if (result != Plugin_Continue)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Resume (Generic)");
+ GOKZ_PlayErrorSound(client);
+ return;
+ }
+
+ // Resume
+ if (pausedOnLadder[client])
+ {
+ Movement_SetMovetype(client, MOVETYPE_LADDER);
+ }
+ else
+ {
+ Movement_SetMovetype(client, MOVETYPE_WALK);
+ }
+
+ // Prevent noclip exploit
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
+ paused[client] = false;
+ if (GetTimerRunning(client))
+ {
+ hasResumedInThisRun[client] = true;
+ lastResumeTime[client] = GetEngineTime();
+ }
+ Movement_SetDuckSpeed(client, lastDuckValue[client]);
+ SetEntPropFloat(client, Prop_Send, "m_flStamina", lastStaminaValue[client]);
+
+ // Call Post Forward
+ Call_GOKZ_OnResume_Post(client);
+}
+
+bool CanResume(int client, bool showError = false)
+{
+ if (GetTimerRunning(client) && hasPausedInThisRun[client]
+ && GetEngineTime() - lastPauseTime[client] < GOKZ_PAUSE_COOLDOWN)
+ {
+ if (showError)
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Can't Resume (Just Paused)");
+ GOKZ_PlayErrorSound(client);
+ }
+ return false;
+ }
+ return true;
+}
+
+void TogglePause(int client)
+{
+ if (paused[client])
+ {
+ Resume(client);
+ }
+ else
+ {
+ Pause(client);
+ }
+}
+
+
+
+// =====[ EVENTS ]=====
+
+void OnClientPutInServer_Pause(int client)
+{
+ paused[client] = false;
+}
+
+void OnTimerStart_Pause(int client)
+{
+ hasPausedInThisRun[client] = false;
+ hasResumedInThisRun[client] = false;
+ Resume(client, true);
+}
+
+void OnChangeMovetype_Pause(int client, MoveType newMovetype)
+{
+ // Check if player has escaped MOVETYPE_NONE
+ if (!paused[client] || newMovetype == MOVETYPE_NONE)
+ {
+ return;
+ }
+
+ // Player has escaped MOVETYPE_NONE, so resume
+ paused[client] = false;
+ if (GetTimerRunning(client))
+ {
+ hasResumedInThisRun[client] = true;
+ lastResumeTime[client] = GetEngineTime();
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnResume_Post(client);
+}
+
+void OnPlayerSpawn_Pause(int client)
+{
+ if (!paused[client])
+ {
+ return;
+ }
+
+ // Player has left paused state by spawning in, so resume
+ paused[client] = false;
+ if (GetTimerRunning(client))
+ {
+ hasResumedInThisRun[client] = true;
+ lastResumeTime[client] = GetEngineTime();
+ }
+
+ Movement_SetDuckSpeed(client, lastDuckValue[client]);
+ SetEntPropFloat(client, Prop_Send, "m_flStamina", lastStaminaValue[client]);
+
+ // Call Post Forward
+ Call_GOKZ_OnResume_Post(client);
+}
+
+void OnJoinTeam_Pause(int client, int team)
+{
+ // Only handle joining spectators. Joining other teams is handled by OnPlayerSpawn.
+ if (team == CS_TEAM_SPECTATOR)
+ {
+ paused[client] = true;
+
+ if (GetTimerRunning(client))
+ {
+ hasPausedInThisRun[client] = true;
+ lastPauseTime[client] = GetEngineTime();
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnPause_Post(client);
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/timer/timer.sp b/sourcemod/scripting/gokz-core/timer/timer.sp
new file mode 100644
index 0000000..f6696ac
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/timer/timer.sp
@@ -0,0 +1,368 @@
+static bool timerRunning[MAXPLAYERS + 1];
+static float currentTime[MAXPLAYERS + 1];
+static int currentCourse[MAXPLAYERS + 1];
+static float lastEndTime[MAXPLAYERS + 1];
+static float lastFalseEndTime[MAXPLAYERS + 1];
+static float lastStartSoundTime[MAXPLAYERS + 1];
+static int lastStartMode[MAXPLAYERS + 1];
+static bool validTime[MAXPLAYERS + 1];
+
+
+// =====[ PUBLIC ]=====
+
+bool GetTimerRunning(int client)
+{
+ return timerRunning[client];
+}
+
+bool GetValidTimer(int client)
+{
+ return validTime[client];
+}
+
+float GetCurrentTime(int client)
+{
+ return currentTime[client];
+}
+
+void SetCurrentTime(int client, float time)
+{
+ currentTime[client] = time;
+ // The timer should be running if time is not negative.
+ timerRunning[client] = time >= 0.0;
+}
+
+int GetCurrentCourse(int client)
+{
+ return currentCourse[client];
+}
+
+void SetCurrentCourse(int client, int course)
+{
+ currentCourse[client] = course;
+}
+
+int GetCurrentTimeType(int client)
+{
+ if (GetTeleportCount(client) == 0)
+ {
+ return TimeType_Pro;
+ }
+ return TimeType_Nub;
+}
+
+bool TimerStart(int client, int course, bool allowMidair = false, bool playSound = true)
+{
+ if (!IsPlayerAlive(client)
+ || JustStartedTimer(client)
+ || JustTeleported(client)
+ || JustNoclipped(client)
+ || !IsPlayerValidMoveType(client)
+ || !allowMidair && (!Movement_GetOnGround(client) || JustLanded(client))
+ || allowMidair && !Movement_GetOnGround(client) && (!GOKZ_GetValidJump(client) || GOKZ_GetHitPerf(client))
+ || (GOKZ_GetTimerRunning(client) && GOKZ_GetCourse(client) != course))
+ {
+ return false;
+ }
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTimerStart(client, course, result);
+ if (result != Plugin_Continue)
+ {
+ return false;
+ }
+
+ // Prevent noclip exploit
+ SetEntProp(client, Prop_Send, "m_CollisionGroup", GOKZ_COLLISION_GROUP_STANDARD);
+
+ // Start Timer
+ currentTime[client] = 0.0;
+ timerRunning[client] = true;
+ currentCourse[client] = course;
+ lastStartMode[client] = GOKZ_GetCoreOption(client, Option_Mode);
+ validTime[client] = true;
+ if (playSound)
+ {
+ PlayTimerStartSound(client);
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnTimerStart_Post(client, course);
+
+ return true;
+}
+
+bool TimerEnd(int client, int course)
+{
+ if (!IsPlayerAlive(client))
+ {
+ return false;
+ }
+
+ if (!timerRunning[client] || course != currentCourse[client])
+ {
+ PlayTimerFalseEndSound(client);
+ lastFalseEndTime[client] = GetGameTime();
+ return false;
+ }
+
+ float time = GetCurrentTime(client);
+ int teleportsUsed = GetTeleportCount(client);
+
+ // Call Pre Forward
+ Action result;
+ Call_GOKZ_OnTimerEnd(client, course, time, teleportsUsed, result);
+ if (result != Plugin_Continue)
+ {
+ return false;
+ }
+
+ if (!validTime[client])
+ {
+ PlayTimerFalseEndSound(client);
+ lastFalseEndTime[client] = GetGameTime();
+ TimerStop(client, false);
+ return false;
+ }
+ // End Timer
+ timerRunning[client] = false;
+ lastEndTime[client] = GetGameTime();
+ PlayTimerEndSound(client);
+
+ if (!IsFakeClient(client))
+ {
+ // Print end timer message
+ Call_GOKZ_OnTimerEndMessage(client, course, time, teleportsUsed, result);
+ if (result == Plugin_Continue)
+ {
+ PrintEndTimeString(client);
+ }
+ }
+
+ // Call Post Forward
+ Call_GOKZ_OnTimerEnd_Post(client, course, time, teleportsUsed);
+
+ return true;
+}
+
+bool TimerStop(int client, bool playSound = true)
+{
+ if (!timerRunning[client])
+ {
+ return false;
+ }
+
+ timerRunning[client] = false;
+ if (playSound)
+ {
+ PlayTimerStopSound(client);
+ }
+
+ Call_GOKZ_OnTimerStopped(client);
+
+ return true;
+}
+
+void TimerStopAll(bool playSound = true)
+{
+ for (int client = 1; client <= MaxClients; client++)
+ {
+ if (IsValidClient(client))
+ {
+ TimerStop(client, playSound);
+ }
+ }
+}
+
+void PlayTimerStartSound(int client)
+{
+ if (GetGameTime() - lastStartSoundTime[client] > GOKZ_TIMER_SOUND_COOLDOWN)
+ {
+ GOKZ_EmitSoundToClient(client, gC_ModeStartSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer Start");
+ GOKZ_EmitSoundToClientSpectators(client, gC_ModeStartSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer Start");
+ lastStartSoundTime[client] = GetGameTime();
+ }
+}
+
+void InvalidateRun(int client)
+{
+ if (validTime[client])
+ {
+ validTime[client] = false;
+ Call_GOKZ_OnRunInvalidated(client);
+ }
+}
+
+// =====[ EVENTS ]=====
+
+void OnClientPutInServer_Timer(int client)
+{
+ timerRunning[client] = false;
+ currentTime[client] = 0.0;
+ currentCourse[client] = 0;
+ lastEndTime[client] = 0.0;
+ lastFalseEndTime[client] = 0.0;
+ lastStartSoundTime[client] = 0.0;
+ lastStartMode[client] = MODE_COUNT; // So it won't equal any mode
+}
+
+void OnPlayerRunCmdPost_Timer(int client)
+{
+ if (IsPlayerAlive(client) && GetTimerRunning(client) && !GetPaused(client))
+ {
+ currentTime[client] += GetTickInterval();
+ }
+}
+
+void OnChangeMovetype_Timer(int client, MoveType newMovetype)
+{
+ if (!IsValidMovetype(newMovetype))
+ {
+ if (TimerStop(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Timer Stopped (Noclipped)");
+ }
+ }
+}
+
+void OnTeleportToStart_Timer(int client)
+{
+ if (GetCurrentMapPrefix() == MapPrefix_KZPro)
+ {
+ TimerStop(client, false);
+ }
+}
+
+void OnClientDisconnect_Timer(int client)
+{
+ TimerStop(client);
+}
+
+void OnPlayerDeath_Timer(int client)
+{
+ TimerStop(client);
+}
+
+void OnOptionChanged_Timer(int client, Option option)
+{
+ if (option == Option_Mode)
+ {
+ if (TimerStop(client))
+ {
+ GOKZ_PrintToChat(client, true, "%t", "Timer Stopped (Changed Mode)");
+ }
+ }
+}
+
+void OnRoundStart_Timer()
+{
+ TimerStopAll();
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static bool IsPlayerValidMoveType(int client)
+{
+ return IsValidMovetype(Movement_GetMovetype(client));
+}
+
+static bool IsValidMovetype(MoveType movetype)
+{
+ return movetype == MOVETYPE_WALK
+ || movetype == MOVETYPE_LADDER
+ || movetype == MOVETYPE_NONE
+ || movetype == MOVETYPE_OBSERVER;
+}
+
+static bool JustTeleported(int client)
+{
+ return gB_OriginTeleported[client] || gB_VelocityTeleported[client]
+ || gI_CmdNum[client] - gI_TeleportCmdNum[client] <= GOKZ_TIMER_START_GROUND_TICKS;
+}
+
+static bool JustLanded(int client)
+{
+ return !gB_OldOnGround[client]
+ || gI_CmdNum[client] - Movement_GetLandingCmdNum(client) <= GOKZ_TIMER_START_NO_TELEPORT_TICKS;
+}
+
+static bool JustStartedTimer(int client)
+{
+ return timerRunning[client] && GetCurrentTime(client) < EPSILON;
+}
+
+static bool JustEndedTimer(int client)
+{
+ return GetGameTime() - lastEndTime[client] < 1.0;
+}
+
+static void PlayTimerEndSound(int client)
+{
+ GOKZ_EmitSoundToClient(client, gC_ModeEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer End");
+ GOKZ_EmitSoundToClientSpectators(client, gC_ModeEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer End");
+}
+
+static void PlayTimerFalseEndSound(int client)
+{
+ if (!JustEndedTimer(client)
+ && (GetGameTime() - lastFalseEndTime[client]) > GOKZ_TIMER_SOUND_COOLDOWN)
+ {
+ GOKZ_EmitSoundToClient(client, gC_ModeFalseEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer False End");
+ GOKZ_EmitSoundToClientSpectators(client, gC_ModeFalseEndSounds[GOKZ_GetCoreOption(client, Option_Mode)], _, "Timer False End");
+ }
+}
+
+static void PlayTimerStopSound(int client)
+{
+ GOKZ_EmitSoundToClient(client, GOKZ_SOUND_TIMER_STOP, _, "Timer Stop");
+ GOKZ_EmitSoundToClientSpectators(client, GOKZ_SOUND_TIMER_STOP, _, "Timer Stop");
+}
+
+static void PrintEndTimeString(int client)
+{
+ if (GetCurrentCourse(client) == 0)
+ {
+ switch (GetCurrentTimeType(client))
+ {
+ case TimeType_Nub:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "Beat Map (NUB)",
+ client,
+ GOKZ_FormatTime(GetCurrentTime(client)),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ case TimeType_Pro:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "Beat Map (PRO)",
+ client,
+ GOKZ_FormatTime(GetCurrentTime(client)),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ }
+ }
+ else
+ {
+ switch (GetCurrentTimeType(client))
+ {
+ case TimeType_Nub:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "Beat Bonus (NUB)",
+ client,
+ currentCourse[client],
+ GOKZ_FormatTime(GetCurrentTime(client)),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ case TimeType_Pro:
+ {
+ GOKZ_PrintToChatAll(true, "%t", "Beat Bonus (PRO)",
+ client,
+ currentCourse[client],
+ GOKZ_FormatTime(GetCurrentTime(client)),
+ gC_ModeNamesShort[GOKZ_GetCoreOption(client, Option_Mode)]);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/sourcemod/scripting/gokz-core/timer/virtual_buttons.sp b/sourcemod/scripting/gokz-core/timer/virtual_buttons.sp
new file mode 100644
index 0000000..aa88a9d
--- /dev/null
+++ b/sourcemod/scripting/gokz-core/timer/virtual_buttons.sp
@@ -0,0 +1,322 @@
+/*
+ Most commonly referred to in the KZ community as timer tech.
+ Lets players press 'virtual' start and end buttons without looking.
+*/
+
+
+
+static int beamSprite;
+static int haloSprite;
+static float lastUsePressTime[MAXPLAYERS + 1];
+static int lastTeleportTick[MAXPLAYERS + 1];
+static bool startedTimerLastTick[MAXPLAYERS + 1];
+static bool onlyNaturalButtonPressed[MAXPLAYERS + 1];
+static int startTimerButtonPressTick[MAXPLAYERS + 1];
+static bool hasEndedTimerSincePressingUse[MAXPLAYERS + 1];
+static bool hasTeleportedSincePressingUse[MAXPLAYERS + 1];
+static bool hasVirtualStartButton[MAXPLAYERS + 1];
+static bool hasVirtualEndButton[MAXPLAYERS + 1];
+static bool wasInEndZone[MAXPLAYERS + 1];
+static float virtualStartOrigin[MAXPLAYERS + 1][3];
+static float virtualEndOrigin[MAXPLAYERS + 1][3];
+static int virtualStartCourse[MAXPLAYERS + 1];
+static int virtualEndCourse[MAXPLAYERS + 1];
+static bool virtualButtonsLocked[MAXPLAYERS + 1];
+
+
+
+// =====[ PUBLIC ]=====
+
+bool GetHasVirtualStartButton(int client)
+{
+ return hasVirtualStartButton[client];
+}
+
+bool GetHasVirtualEndButton(int client)
+{
+ return hasVirtualEndButton[client];
+}
+
+bool ToggleVirtualButtonsLock(int client)
+{
+ virtualButtonsLocked[client] = !virtualButtonsLocked[client];
+ return virtualButtonsLocked[client];
+}
+
+void LockVirtualButtons(int client)
+{
+ virtualButtonsLocked[client] = true;
+}
+
+int GetVirtualButtonPosition(int client, float position[3], bool isStart)
+{
+ if (isStart && hasVirtualStartButton[client])
+ {
+ position = virtualStartOrigin[client];
+ return virtualStartCourse[client];
+ }
+ else if (!isStart && hasVirtualEndButton[client])
+ {
+ position = virtualEndOrigin[client];
+ return virtualEndCourse[client];
+ }
+
+ return -1;
+}
+
+void SetVirtualButtonPosition(int client, float position[3], int course, bool isStart)
+{
+ if (isStart)
+ {
+ virtualStartCourse[client] = course;
+ virtualStartOrigin[client] = position;
+ hasVirtualStartButton[client] = true;
+ }
+ else
+ {
+ virtualEndCourse[client] = course;
+ virtualEndOrigin[client] = position;
+ hasVirtualEndButton[client] = true;
+ }
+}
+
+void ResetVirtualButtonPosition(int client, bool isStart)
+{
+ if (isStart)
+ {
+ virtualStartCourse[client] = -1;
+ virtualStartOrigin[client] = {0.0, 0.0, 0.0};
+ hasVirtualStartButton[client] = false;
+ }
+ else
+ {
+ virtualEndCourse[client] = -1;
+ virtualEndOrigin[client] = {0.0, 0.0, 0.0};
+ hasVirtualEndButton[client] = false;
+ }
+}
+
+// =====[ EVENTS ]=====
+
+void OnMapStart_VirtualButtons()
+{
+ beamSprite = PrecacheModel("materials/sprites/laserbeam.vmt");
+ haloSprite = PrecacheModel("materials/sprites/glow01.vmt");
+}
+
+void OnClientPutInServer_VirtualButtons(int client)
+{
+ startedTimerLastTick[client] = false;
+ hasVirtualEndButton[client] = false;
+ hasVirtualStartButton[client] = false;
+ virtualButtonsLocked[client] = false;
+ onlyNaturalButtonPressed[client] = false;
+ wasInEndZone[client] = false;
+ startTimerButtonPressTick[client] = 0;
+}
+
+void OnStartButtonPress_VirtualButtons(int client, int course)
+{
+ if (!virtualButtonsLocked[client] &&
+ lastTeleportTick[client] + GOKZ_TIMER_START_NO_TELEPORT_TICKS < GetGameTickCount())
+ {
+ Movement_GetOrigin(client, virtualStartOrigin[client]);
+ virtualStartCourse[client] = course;
+ hasVirtualStartButton[client] = true;
+ startTimerButtonPressTick[client] = GetGameTickCount();
+ }
+}
+
+void OnEndButtonPress_VirtualButtons(int client, int course)
+{
+ // Prevent setting end virtual button to where it would usually be unreachable
+ if (IsPlayerStuck(client))
+ {
+ return;
+ }
+
+ if (!virtualButtonsLocked[client] &&
+ lastTeleportTick[client] + GOKZ_TIMER_START_NO_TELEPORT_TICKS < GetGameTickCount())
+ {
+ Movement_GetOrigin(client, virtualEndOrigin[client]);
+ virtualEndCourse[client] = course;
+ hasVirtualEndButton[client] = true;
+ }
+}
+
+void OnPlayerRunCmdPost_VirtualButtons(int client, int buttons, int cmdnum)
+{
+ CheckForAndHandleUsage(client, buttons);
+ UpdateIndicators(client, cmdnum);
+}
+
+void OnCountedTeleport_VirtualButtons(int client)
+{
+ hasTeleportedSincePressingUse[client] = true;
+}
+
+void OnTeleport_DelayVirtualButtons(int client)
+{
+ lastTeleportTick[client] = GetGameTickCount();
+}
+
+
+
+// =====[ PRIVATE ]=====
+
+static void CheckForAndHandleUsage(int client, int buttons)
+{
+ if (buttons & IN_USE && !(gI_OldButtons[client] & IN_USE))
+ {
+ lastUsePressTime[client] = GetGameTime();
+ hasEndedTimerSincePressingUse[client] = false;
+ hasTeleportedSincePressingUse[client] = false;
+ onlyNaturalButtonPressed[client] = startTimerButtonPressTick[client] == GetGameTickCount();
+ }
+
+ bool useCheck = PassesUseCheck(client);
+
+ // Start button
+ if ((useCheck || GOKZ_GetCoreOption(client, Option_TimerButtonZoneType) == TimerButtonZoneType_BothZones)
+ && GetHasVirtualStartButton(client) && InRangeOfVirtualStart(client) && CanReachVirtualStart(client))
+ {
+ if (TimerStart(client, virtualStartCourse[client], .playSound = false))
+ {
+ startedTimerLastTick[client] = true;
+ OnVirtualStartButtonPress_Teleports(client);
+ }
+ }
+ else if (startedTimerLastTick[client])
+ {
+ // Without that check you get two sounds when pressing the natural timer button
+ if (!onlyNaturalButtonPressed[client])
+ {
+ PlayTimerStartSound(client);
+ }
+ onlyNaturalButtonPressed[client] = false;
+ startedTimerLastTick[client] = false;
+ }
+
+ // End button
+ if ((useCheck || GOKZ_GetCoreOption(client, Option_TimerButtonZoneType) != TimerButtonZoneType_BothButtons)
+ && GetHasVirtualEndButton(client) && InRangeOfVirtualEnd(client) && CanReachVirtualEnd(client))
+ {
+ if (!wasInEndZone[client])
+ {
+ TimerEnd(client, virtualEndCourse[client]);
+ hasEndedTimerSincePressingUse[client] = true; // False end counts as well
+ wasInEndZone[client] = true;
+ }
+ }
+ else
+ {
+ wasInEndZone[client] = false;
+ }
+}
+
+static bool PassesUseCheck(int client)
+{
+ if (GetGameTime() - lastUsePressTime[client] < GOKZ_VIRTUAL_BUTTON_USE_DETECTION_TIME + EPSILON
+ && !hasEndedTimerSincePressingUse[client]
+ && !hasTeleportedSincePressingUse[client])
+ {
+ return true;
+ }
+
+ return false;
+}
+
+bool InRangeOfVirtualStart(int client)
+{
+ return InRangeOfButton(client, virtualStartOrigin[client]);
+}
+
+static bool InRangeOfVirtualEnd(int client)
+{
+ return InRangeOfButton(client, virtualEndOrigin[client]);
+}
+
+static bool InRangeOfButton(int client, const float buttonOrigin[3])
+{
+ float origin[3];
+ Movement_GetOrigin(client, origin);
+ float distanceToButton = GetVectorDistance(origin, buttonOrigin);
+ return distanceToButton <= gF_ModeVirtualButtonRanges[GOKZ_GetCoreOption(client, Option_Mode)];
+}
+
+bool CanReachVirtualStart(int client)
+{
+ return CanReachButton(client, virtualStartOrigin[client]);
+}
+
+static bool CanReachVirtualEnd(int client)
+{
+ return CanReachButton(client, virtualEndOrigin[client]);
+}
+
+static bool CanReachButton(int client, const float buttonOrigin[3])
+{
+ float origin[3];
+ Movement_GetOrigin(client, origin);
+ Handle trace = TR_TraceRayFilterEx(origin, buttonOrigin, MASK_PLAYERSOLID, RayType_EndPoint, TraceEntityFilterPlayers);
+ bool didHit = TR_DidHit(trace);
+ delete trace;
+ return !didHit;
+}
+
+
+
+// ===== [ INDICATOR ] =====
+
+static void UpdateIndicators(int client, int cmdnum)
+{
+ if (cmdnum % 128 != 0 || !IsPlayerAlive(client)
+ || GOKZ_GetCoreOption(client, Option_VirtualButtonIndicators) == VirtualButtonIndicators_Disabled)
+ {
+ return;
+ }
+
+ if (hasVirtualStartButton[client])
+ {
+ DrawIndicator(client, virtualStartOrigin[client], { 0, 255, 0, 255 } );
+ }
+
+ if (hasVirtualEndButton[client])
+ {
+ DrawIndicator(client, virtualEndOrigin[client], { 255, 0, 0, 255 } );
+ }
+}
+
+static void DrawIndicator(int client, const float origin[3], const int colour[4])
+{
+ float radius = gF_ModeVirtualButtonRanges[GOKZ_GetCoreOption(client, Option_Mode)];
+ if (radius <= EPSILON) // Don't draw circle of radius 0
+ {
+ return;
+ }
+
+ float x, y, start[3], end[3];
+
+ // Create the start position for the first part of the beam
+ start[0] = origin[0] + radius;
+ start[1] = origin[1];
+ start[2] = origin[2];
+
+ for (int i = 1; i <= 31; i++) // Circle is broken into 31 segments
+ {
+ float angle = 2 * PI / 31 * i;
+ x = radius * Cosine(angle);
+ y = radius * Sine(angle);
+
+ end[0] = origin[0] + x;
+ end[1] = origin[1] + y;
+ end[2] = origin[2];
+
+ TE_SetupBeamPoints(start, end, beamSprite, haloSprite, 0, 0, 0.97, 0.2, 0.2, 0, 0.0, colour, 0);
+ TE_SendToClient(client);
+
+ start[0] = end[0];
+ start[1] = end[1];
+ start[2] = end[2];
+ }
+}