using System; using UdonSharp; using UnityEngine; using UnityEngine.Animations; using UnityEngine.Rendering; using VRC.SDK3.Components; using VRC.SDKBase; using VRCBilliardsCE.Packages.com.vrcbilliards.vrcbce.Runtime.Scripts.Components; namespace VRCBilliardsCE.Packages.com.vrcbilliards.vrcbce.Runtime.Scripts { /// /// The reasons why the table can be reset /// public enum ResetReason { InstanceOwnerReset, PlayerReset, InvalidState, PlayerLeft } public enum GameMode { EightBall, NineBall, KoreanCarom, JapaneseCarom, ThreeCushionCarom } /// /// This is the base logic that governs the pool table, devoid of almost all physics code. This code is quite /// messy; it includes all setup, teardown, game state and replication logic, and works with all other components /// to allow the player to play pool. /// public partial class PoolStateManager { #region Dependencies public PoolMenu poolMenu; public PoolCue[] poolCues; public ScreenspaceUI pancakeUI; public OscSlogger slogger; public Logger logger; #endregion #region Networked Variables [UdonSynced] private bool turnIsRunning; [UdonSynced] private int timerSecondsPerShot; [UdonSynced] private bool isPlayerAllowedToPlay; [UdonSynced] private bool guideLineEnabled = true; [UdonSynced] private bool[] ballsArePocketed; private bool[] oldBallsArePocketed; [UdonSynced] private bool isTeam2Turn; private bool oldIsTeam2Turn; [UdonSynced] private bool isFoul; [UdonSynced] private bool isGameOver; [UdonSynced] private bool isOpen = true; private bool oldOpen; [UdonSynced] private bool isTeam2Blue; [UdonSynced] private bool isGameInMenus = true; private bool oldIsGameInMenus; [UdonSynced] private bool isTeam2Winner; [UdonSynced] private bool isTableLocked = true; [UdonSynced] private bool isTeams; [UdonSynced] private uint gameID; private uint oldGameID; [UdonSynced] private uint turnID; [UdonSynced] private bool isRepositioningCueBall; /// /// Is the game currently in the first shot? /// [UdonSynced] private bool _isGameBreak; [UdonSynced] private int[] scores = new int[2]; [UdonSynced] private Vector3[] currentBallPositions = new Vector3[NUMBER_OF_SIMULATED_BALLS]; [UdonSynced] private Vector3[] currentBallVelocities = new Vector3[NUMBER_OF_SIMULATED_BALLS]; [UdonSynced] private Vector3[] currentAngularVelocities = new Vector3[NUMBER_OF_SIMULATED_BALLS]; [UdonSynced] private GameMode gameMode; [UdonSynced] private int player1ID; [UdonSynced] private int player2ID; [UdonSynced] private int player3ID; [UdonSynced] private int player4ID; [UdonSynced] private bool gameWasReset; [UdonSynced] private ResetReason latestResetReason; #endregion #region Table Dimensions /// /// These numbers determine the exact size of your table, and will correspond to the blue, yellow and red /// guidance lines you can see in the editor. Theoretically, as long as your table is a cuboid and has six /// pockets, we support it. /// // Note the WIDTH and HEIGHT are halved from their real values - this is due to the reliance of the physics code // working in four quadrants to detect collisions. [Tooltip("How wide is the table? (Meters/2)")] public float tableWidth = 1.1f; [Tooltip("How long is the table? (Meters/2)")] public float tableHeight = 0.64f; [Tooltip("Where are the corner pockets located??")] public Vector3 cornerPocket = new Vector3(1.135f, 0.0f, 0.685f); [Tooltip("Where are the two pockets located?")] public Vector3 middlePocket = new Vector3(0.0f, 0.0f, 0.72f); [Tooltip("What's the radius of the hole that the pocket makes in the cushions? (Meters)")] public float pocketOuterRadius = 0.11f; [Tooltip("What's the radius of the pocket holes? (Meters)")] public float pocketInnerRadius = 0.078f; [Tooltip("How wide are the table's balls? (Meters)")] public float ballDiameter = 0.06f; #endregion // The number of balls we simulate - this const allows us to increase the number we support in the future. private const int NUMBER_OF_SIMULATED_BALLS = 16; // A small fraction designed to slightly move balls around when placed, which helps with making the table // less deterministic. private const float RANDOMIZE_F = 0.0001f; // These four vars dictate some placement of balls when placed. // Half-way between the centre spot and the cushion on the X axis. public float SPOT_POSITION_X = 0.5334f; // Three quarters of the way between the centre spot and the cushion on the X axis. public float SPOT_CAROM_X = 0.8001f; public float BALL_PL_X = 0.03f; public float BALL_PL_Y = 0.05196152422f; #region Desktop private const float DEFAULT_DESKTOP_CUE_ANGLE = 10.0f; // This should never be 90.0f or higher, as it puts the physics simulation into a weird state. private const float MAX_DESKTOP_CUE_ANGLE = 89.0f; private const float MIN_DESKTOP_CUE_ANGLE = 0.0f; #endregion [Tooltip("Use fake shadows? They may clash with your world's lighting.")] public bool fakeBallShadows = true; [Tooltip("Does the table model for this table have rails that guide the ball when the ball sinks?")] public bool tableModelHasRails; public Transform sunkBallsPositionRoot; public GameObject shadows; public ParticleSystem plusOneParticleSystem; public ParticleSystem minusOneParticleSystem; // Where's the surface of the table? public Transform tableSurface; public string uniformTableColour = "_EmissionColor"; public string uniformMarkerColour = "_Color"; public string uniformCueColour = "_EmissionColor"; public string uniformBallColour = "_BallColour"; public string uniformBallFloat = "_CustomColor"; public string ballMaskToggle = "_Turnoff"; [Tooltip( "Change the length of the intro ball-drop animation. If you set this to zero, the animation will not play at all.")] [Range(0f, 5f)] public float introAnimationLength = 2.0f; [ColorUsage(true, true)] public Color tableBlue = new Color(0.0f, 0.5f, 1.5f, 1.0f); [ColorUsage(true, true)] public Color tableOrange = new Color(1.5f, 0.5f, 0.0f, 1.0f); [ColorUsage(true, true)] public Color tableRed = new Color(1.4f, 0.0f, 0.0f, 1.0f); [ColorUsage(true, true)] public Color tableWhite = new Color(1.0f, 1.0f, 1.0f, 1.0f); [ColorUsage(true, true)] public Color tableBlack = new Color(0.01f, 0.01f, 0.01f, 1.0f); [ColorUsage(true, true)] public Color tableYellow = new Color(2.0f, 1.0f, 0.0f, 1.0f); [ColorUsage(true, true)] public Color tableLightBlue = new Color(0.45f, 0.9f, 1.5f, 1.0f); public Color markerOK = new Color(0.0f, 1.0f, 0.0f, 1.0f); public Color markerNotOK = new Color(1.0f, 0.0f, 0.0f, 1.0f); public Color gripColourActive = new Color(0.0f, 0.5f, 1.1f, 1.0f); public Color gripColourInactive = new Color(0.34f, 0.34f, 0.34f, 1.0f); public Color fabricGray = new Color(0.3f, 0.3f, 0.3f, 1.0f); public Color fabricBlue = new Color(0.1f, 0.6f, 1.0f, 1.0f); public Color fabricGreen = new Color(0.15f, 0.75f, 0.3f, 1.0f); [ColorUsage(true, true)] private Color[] ballColors = new Color[NUMBER_OF_SIMULATED_BALLS]; public bool ballCustomColours; public ColorPicker blueTeamSliders; public ColorPicker orangeTeamSliders; private float shaderToggleFloat = 0; private GameObject cueTip; public GameObject[] cueTips; public MeshRenderer[] cueRenderObjs; private Material[] cueMaterials = new Material[2]; /// /// The balls that are used by the table. /// The order of the balls is as follows: cue, black, all blue in ascending order, then all orange in ascending order. /// If the order of the balls is incorrect, gameplay will not proceed correctly. /// /*[Header("Table Objects")]*/ [Tooltip( "The balls that are used by the table." + "\nThe order of the balls is as follows: cue, black, all blue in ascending order, then all orange in ascending order." + "\nIf the order of the balls is incorrect, gameplay will not proceed correctly." )] public Transform[] ballTransforms; private Rigidbody[] ballRigidbodies; [Tooltip("The shadow object for each ball")] public PositionConstraint[] ballShadowPosConstraints; private Transform[] ballShadowPosConstraintTransforms; public ShotGuideController guideline; public GameObject devhit; public GameObject[] playerTotems; public GameObject marker; private Material markerMaterial; public GameObject marker9ball; public GameObject pocketBlockers; public MeshRenderer[] ballRenderers; public MeshRenderer tableRenderer; private Material[] tableMaterials; public Texture[] sets; public Material[] cueGrips; public GameObject audioSourcePoolContainer; public AudioSource cueTipSrc; public AudioClip introSfx; public AudioClip sinkSfx; /// /// The SFX that plays when a good thing happens (sinking a ball correctly, etc) /// public AudioClip successSfx; /// /// The SFX that plays when a foul occurs /// public AudioClip foulSfx; /// /// The SFX that plays when someone wins /// public AudioClip winnerSfx; public AudioClip[] hitsSfx; public AudioClip newTurnSfx; public AudioClip pointMadeSfx; public AudioClip hitBallSfx; public ReflectionProbe tableReflection; public Mesh[] cueballMeshes; public Mesh nineBall; /// /// Player is hitting /// private bool isArmed; private int localPlayerID = -1; public GameObject desktopHitPosition; public GameObject desktopBase; public GameObject[] desktopCueParents; public UnityEngine.UI.Image tiltAmount; /* * Private variables */ private AudioSource[] ballPool; private Transform[] ballPoolTransforms; private AudioSource mainSrc; /// /// We are waiting for our local simulation to finish, before we unpack data /// private bool isUpdateLocked; /// /// The first ball to be hit by cue ball /// private int firstHitBallThisTurn; [Range(10, 50), Tooltip("How many points do your players need to win a carom game? This is the same for all carom variants.")] public int scoreNeededToWinCarom = 10; private int secondBallHitThisTurn; private int thirdBallHitThisTurn; private int cushionsHitThisTurn; /// /// If the simulation was initiated by us, only set from update /// private bool isSimulatedByUs; /// /// Ball dropper timer /// private float introAnimTimer; private float remainingTime; private bool isTimerRunning; private bool isMadePoint; private bool isMadeFoul; private bool IsCarom => gameMode == GameMode.KoreanCarom || gameMode == GameMode.JapaneseCarom || gameMode == GameMode.ThreeCushionCarom; /// /// Game should run in practice mode /// private bool isGameModePractice; private bool isInDesktopTopDownView; /// /// Interpreted value /// private bool playerIsTeam2; /// /// Runtime target colour /// private Color tableSrcColour = new Color(1.0f, 1.0f, 1.0f, 1.0f); /// /// Runtime actual colour /// private Color tableCurrentColour = new Color(1.0f, 1.0f, 1.0f, 1.0f); /// /// Team 0 /// private Color pointerColour0; /// /// Team 1 /// private Color pointerColour1; /// /// No team / open / 9 ball /// private Color pointerColour2; private Color pointerColourErr; private Color pointerClothColour; private Vector3 desktopAimPoint = new Vector3(0.0f, 2.0f, 0.0f); private Vector3 desktopHitPoint = new Vector3(0.0f, 0.0f, 0.0f); private float desktopAngle = DEFAULT_DESKTOP_CUE_ANGLE; public float desktopAngleIncrement = 15f; private bool isDesktopShootingIn; private bool isDesktopSafeRemove = true; private Vector3 desktopShootVector; private Vector3 desktopSafeRemovePoint; private bool isDesktopLocalTurn; private bool isEnteringDesktopModeThisFrame; /// /// Cue input tracking /// private Vector3 localSpacePositionOfCueTip; private Vector3 localSpacePositionOfCueTipLastFrame; private Vector3 cueLocalForwardDirection; private Vector3 cueArmedShotDirection; private float cueFDir; private Vector3 raySphereOutput; private int[] rackOrder8Ball = {9, 2, 10, 11, 1, 3, 4, 12, 5, 13, 14, 6, 15, 7, 8}; private int[] rackOrder9Ball = {2, 3, 4, 5, 9, 6, 7, 8, 1}; private int[] breakRows9ball = {0, 1, 2, 1, 0}; private float ballShadowOffset; private MeshRenderer[] shadowRenders; private bool isPlayerInVR; private VRCPlayerApi localPlayer; private int networkingLocalPlayerID; private Transform markerTransform; private Camera desktopCamera; private TMPro.TextMeshProUGUI timerText; private string timerOutputFormat; private UnityEngine.UI.Image timerCountdown; private UInt32 oldDesktopCue; private UInt32 newDesktopCue; private bool fourBallCueLeftTable; /// /// Have we run a network sync once? Used for situations where we need to specifically catch up a late-joiner. /// private bool hasRunSyncOnce; /// /// For clamping to table or set lower for kitchen /// private float repoMaxX; public CueBallOffTableController cueBallController; private Vector3 desktopCameraInitialPosition; private Quaternion desktopCameraInitialRotation; private float lastLookHorizontal; private float lastLookVertical; private bool lastInputUseDown; private float inputHeldDownTime; private float desktopShootForce; private const float desktopShotPowerMult = 0.15f; public GameObject powerBar; public GameObject topBar; public Vector3 initialPowerBarPos; public void Start() { repoMaxX = tableWidth; localPlayer = Networking.LocalPlayer; networkingLocalPlayerID = localPlayer.playerId; isPlayerInVR = localPlayer.IsUserInVR(); tableMaterials = tableRenderer.materials; ballsArePocketed = new bool[NUMBER_OF_SIMULATED_BALLS]; oldBallsArePocketed = new bool[NUMBER_OF_SIMULATED_BALLS]; ballRigidbodies = new Rigidbody[NUMBER_OF_SIMULATED_BALLS]; for (var i = 0; i < NUMBER_OF_SIMULATED_BALLS; i++) { ballRigidbodies[i] = ballTransforms[i].GetComponent(); } ballShadowPosConstraintTransforms = new Transform[NUMBER_OF_SIMULATED_BALLS]; for (var i = 0; i < NUMBER_OF_SIMULATED_BALLS; i++) { ballShadowPosConstraintTransforms[i] = ballShadowPosConstraints[i].transform; } mainSrc = GetComponent(); if (audioSourcePoolContainer) { ballPool = audioSourcePoolContainer.GetComponentsInChildren(); ballPoolTransforms = new Transform[ballPool.Length]; for (var i = 0; i < ballPool.Length; i++) { ballPoolTransforms[i] = ballPool[i].transform; } } desktopCamera = desktopBase.GetComponentInChildren(); desktopCamera.enabled = false; markerTransform = marker.transform; CopyGameStateToOldState(); markerMaterial = marker.GetComponent().material; cueMaterials[0] = cueRenderObjs[0].material; cueMaterials[1] = cueRenderObjs[1].material; cueMaterials[0].SetColor(uniformCueColour, tableBlack); cueMaterials[1].SetColor(uniformCueColour, tableBlack); cueTip = cueTips[0]; if (tableReflection) { tableReflection.gameObject.SetActive(true); tableReflection.mode = ReflectionProbeMode.Realtime; tableReflection.refreshMode = ReflectionProbeRefreshMode.ViaScripting; tableReflection.timeSlicingMode = ReflectionProbeTimeSlicingMode.IndividualFaces; tableReflection.RenderProbe(); } if (guideline) guideline.gameObject.SetActive(false); if (devhit) devhit.SetActive(false); if (marker) marker.SetActive(false); if (marker9ball) marker9ball.SetActive(false); if (shadows) shadows.SetActive(fakeBallShadows); ballShadowOffset = ballTransforms[0].position.y - ballShadowPosConstraintTransforms[0].position.y; shadowRenders = shadows.GetComponentsInChildren(); timerText = poolMenu.visibleTimerDuringGame; timerOutputFormat = poolMenu.timerOutputFormat; timerCountdown = poolMenu.timerCountdown; desktopCameraInitialPosition = desktopCamera.transform.localPosition; desktopCameraInitialRotation = desktopCamera.transform.localRotation; initialPowerBarPos = powerBar.transform.localPosition; CalculateTableCollisionConstants(); } public void Update() { if (isInDesktopTopDownView) { HandleUpdatingDesktopViewUI(); if (Input.GetKeyDown(KeyCode.E) || Input.GetKeyDown(KeyCode.Escape)) OnDesktopTopDownViewExit(); } else if (canEnterDesktopTopDownView) { if (Input.GetKeyDown(KeyCode.E) || lastInputUseDown) OnDesktopTopDownViewStart(); } if (isGameInMenus) return; // Everything below this line only runs when the game is active. localSpacePositionOfCueTip = tableSurface.transform.InverseTransformPoint(cueTip.transform.position); var copyOfLocalSpacePositionOfCueTip = localSpacePositionOfCueTip; // if shot is prepared for next hit if (isPlayerAllowedToPlay) { if (isRepositioningCueBall) { HandleRepositioningCueBall(); markerMaterial.SetColor(uniformMarkerColour, IsCueContacting() ? markerNotOK : markerOK); } var cueballPosition = currentBallPositions[0]; if (isArmed) copyOfLocalSpacePositionOfCueTip = AimAndHitCueBall(copyOfLocalSpacePositionOfCueTip, cueballPosition); else HandleGuidelinesAndAimMarkers(copyOfLocalSpacePositionOfCueTip, cueballPosition); } localSpacePositionOfCueTipLastFrame = copyOfLocalSpacePositionOfCueTip; // Table outline colour if (isGameInMenus) tableCurrentColour = tableSrcColour * ((Mathf.Sin(Time.timeSinceLevelLoad * 3.0f) * 0.5f) + 1.0f); else tableCurrentColour = Color.Lerp(tableCurrentColour, tableSrcColour, Time.deltaTime * 3.0f); if (isTimerRunning) HandleTimerCountdown(); else { timerText.text = ""; timerCountdown.fillAmount = 0f; } foreach (Material tableMaterial in tableMaterials) { tableMaterial.SetColor(uniformTableColour, tableCurrentColour); } // Run the intro animation. Do not run the animation if this is our first sync! if (hasRunSyncOnce && introAnimTimer > 0.0f) HandleIntroAnimation(); // Run sim only if things are moving if (turnIsRunning) { accumulatedTime += Time.deltaTime; if (accumulatedTime > MAX_SIMULATION_TIME_PER_FRAME) accumulatedTime = MAX_SIMULATION_TIME_PER_FRAME; while (accumulatedTime >= TIME_PER_STEP) { AdvancePhysicsStep(); accumulatedTime -= TIME_PER_STEP; } } if (IsCueInPlay) ballTransforms[0].localPosition = currentBallPositions[0]; for (var i = 1; i < ballsArePocketed.Length; i++) { if (!ballsArePocketed[i]) ballTransforms[i].localPosition = currentBallPositions[i]; } } private void HandleRepositioningCueBall() { Vector3 temp = markerTransform.localPosition; temp.x = Mathf.Clamp(temp.x, -tableWidth, repoMaxX); temp.z = Mathf.Clamp(temp.z, -tableHeight, tableHeight); temp.y = 0.0f; currentBallPositions[0] = temp; ballTransforms[0].localPosition = temp; markerTransform.localPosition = ballTransforms[0].localPosition; markerTransform.localRotation = Quaternion.identity; } public void _ReEnableShadowConstraints() { foreach (var con in ballShadowPosConstraints) { con.constraintActive = true; } } public override void OnDeserialization() { // A somewhat loose way of handling if the pool cues need to run their Update() loops. if (isGameInMenus) { poolCues[0].tableIsActive = false; poolCues[1].tableIsActive = false; } else { poolCues[0].tableIsActive = true; poolCues[1].tableIsActive = true; } // Check if local simulation is in progress, the event will fire off later when physics // are settled by the client. if (turnIsRunning) { isUpdateLocked = true; return; } // We are free to read this update ReadNetworkData(); } // Update loop-scoped handler for introduction animation operations. Non-pure. private void HandleIntroAnimation() { introAnimTimer -= Time.deltaTime; Vector3 temp; float atime; float aitime; if (introAnimTimer < 0.0f) introAnimTimer = 0.0f; // Cueball drops late Transform ball = ballTransforms[0]; temp = ball.localPosition; float height = ball.position.y - ballShadowOffset; atime = Mathf.Clamp(introAnimTimer - 0.33f, 0.0f, 1.0f); aitime = 1.0f - atime; temp.y = Mathf.Abs(Mathf.Cos(atime * 6.29f)) * atime * 0.5f; ball.localPosition = temp; Vector3 scale = new Vector3(aitime, aitime, aitime); ball.localScale = scale; PositionConstraint posCon = ballShadowPosConstraints[0]; posCon.constraintActive = false; Transform posConTrans = ballShadowPosConstraintTransforms[0]; Vector3 position = ball.position; posConTrans.position = new Vector3(position.x, height, position.z); posConTrans.localScale = scale; MeshRenderer r = shadowRenders[0]; Material material = r.material; Color c = material.color; material.color = new Color(c.r, c.g, c.b, aitime); for (int i = 1; i < NUMBER_OF_SIMULATED_BALLS; i++) { ball = ballTransforms[i]; temp = ball.localPosition; height = ball.position.y - ballShadowOffset; atime = Mathf.Clamp(introAnimTimer - 0.84f - (i * 0.03f), 0.0f, 1.0f); aitime = 1.0f - atime; temp.y = Mathf.Abs(Mathf.Cos(atime * 6.29f)) * atime * 0.5f; ball.localPosition = temp; scale = new Vector3(aitime, aitime, aitime); ball.localScale = scale; posCon = ballShadowPosConstraints[i]; posCon.constraintActive = false; posConTrans = ballShadowPosConstraintTransforms[i]; Vector3 position1 = ball.position; posConTrans.position = new Vector3(position1.x, height, position1.z); posConTrans.localScale = scale; r = shadowRenders[i]; var material1 = r.material; c = material1.color; material1.color = new Color(c.r, c.g, c.b, aitime); } } // Update loop-scoped handler of timer countdown functionality. Non-pure. private void HandleTimerCountdown() { remainingTime -= Time.deltaTime; if (remainingTime < 0.0f) { isTimerRunning = false; // We are holding the stick so propogate the change if (Networking.GetOwner(playerTotems[Convert.ToInt32(isTeam2Turn)]) == localPlayer) { Networking.SetOwner(localPlayer, gameObject); OnTurnOverFoul(); } else isPlayerAllowedToPlay = false; } else { if (timerText) timerText.text = timerOutputFormat.Replace("{}", Mathf.Round(remainingTime).ToString()); if (timerCountdown) timerCountdown.fillAmount = remainingTime / timerSecondsPerShot; } } // Update loop-scoped handler for guidelines and aim markers. Non-pure. private void HandleGuidelinesAndAimMarkers(Vector3 copyOfLocalSpacePositionOfCueTip, Vector3 cueballPosition) { cueLocalForwardDirection = tableSurface.transform.InverseTransformVector(cueTip.transform.forward); if (devhit) devhit.SetActive(false); if (guideline) guideline.gameObject.SetActive(false); // Get where the cue will strike the ball if (!IsIntersectingWithSphere(copyOfLocalSpacePositionOfCueTip, cueLocalForwardDirection, cueballPosition)) return; if (guideLineEnabled && guideline) guideline.gameObject.SetActive(true); if (devhit) { devhit.SetActive(true); devhit.transform.localPosition = raySphereOutput; } cueArmedShotDirection = cueLocalForwardDirection; Vector3 shotVector = (cueballPosition - raySphereOutput) * 0.1f; // This is a bit of a hack; we abuse the shot vector and then divide it by 10 which gives us // a mostly-accurate account of where the shot will go. cueArmedShotDirection += shotVector.normalized / 10; cueFDir = Mathf.Atan2(shotVector.z, shotVector.x); // Update the prediction line direction Transform transform1 = guideline.transform; transform1.localPosition = currentBallPositions[0]; transform1.localEulerAngles = new Vector3(0.0f, -cueFDir * Mathf.Rad2Deg, 0.0f); } private void EnableCustomBallColorSlider(bool enabledState) { if (!ballCustomColours) return; if (blueTeamSliders) blueTeamSliders._EnableDisable(enabledState); if (orangeTeamSliders) orangeTeamSliders._EnableDisable(enabledState); } private void RemovePlayerFromGame(int playerID) { Networking.SetOwner(localPlayer, gameObject); switch (playerID) { case 0: player1ID = 0; break; case 1: player2ID = 0; break; case 2: player3ID = 0; break; case 3: player4ID = 0; break; default: return; } RefreshNetworkData(false); } public void _StartHit() { if (logger) logger._Log(name, "StartHit"); // lock aim variables bool isOurTurn = ((localPlayerID >= 0) && (playerIsTeam2 == isTeam2Turn)) || isGameModePractice; if (isOurTurn) { isArmed = true; } } public void _EndHit() { if (logger) logger._Log(name, "EndHit"); isArmed = false; } public void PlaceBall() { if (logger) logger._Log(name, "PlaceBall"); if (IsCueContacting()) return; if (logger) logger._Log(name, "disabling marker because the ball hase been placed"); isRepositioningCueBall = false; if (marker) marker.SetActive(false); isPlayerAllowedToPlay = true; isFoul = false; Networking.SetOwner(localPlayer, gameObject); // Save out position to remote clients RefreshNetworkData(isTeam2Turn); } private void _Reset(ResetReason reason) { isGameInMenus = true; poolCues[0].tableIsActive = false; poolCues[1].tableIsActive = false; isPlayerAllowedToPlay = false; turnIsRunning = false; isTeam2Turn = false; gameWasReset = true; isGameOver = true; latestResetReason = reason; RefreshNetworkData(false); if (logger) logger._Log(name, ToReasonString(reason)); } private void OnDesktopTopDownViewStart() { if (logger) logger._Log(name, "OnDesktopTopDownViewStart"); isInDesktopTopDownView = true; isEnteringDesktopModeThisFrame = true; if (desktopBase) { desktopBase.SetActive(true); desktopCamera.enabled = true; } // Lock player in place localPlayer.Immobilize(true); poolCues[0]._EnteredFlatscreenPlayerCamera(); poolCues[1]._EnteredFlatscreenPlayerCamera(); poolMenu._EnteredFlatscreenPlayerCamera(desktopCamera.transform); pancakeUI._EnterDesktopTopDownView(desktopCamera); } public void _OnPutDownCueLocally() { if (logger) logger._Log(name, "OnPutDownCueLocally"); OnDesktopTopDownViewExit(); } // HandleEndOfTurn assess the table state at the end of the turn, based on what game is playing. private void HandleEndOfTurn() { isSimulatedByUs = false; // We are updating the game state so make sure we are network owner Networking.SetOwner(Networking.LocalPlayer, gameObject); var isCorrectBallSunk = false; var isOpponentColourSunk = false; var winCondition = false; var foulCondition = ballsArePocketed[0]; var deferLossCondition = false; var is8Sink = ballsArePocketed[1]; var numberOfSunkBlues = 0; var numberOfSunkOranges = 0; switch (gameMode) { case GameMode.EightBall: HandleEightBallEndOfTurn( is8Sink, out numberOfSunkBlues, out numberOfSunkOranges, ref isCorrectBallSunk, ref isOpponentColourSunk, ref foulCondition, out deferLossCondition, out winCondition ); break; case GameMode.NineBall: HandleNineBallEndOfTurn(ref foulCondition, ref isCorrectBallSunk, out winCondition); break; case GameMode.KoreanCarom: case GameMode.JapaneseCarom: HandleCaromEndOfTurn(); isCorrectBallSunk = isMadePoint; isOpponentColourSunk = isMadeFoul; winCondition = scores[Convert.ToInt32(isTeam2Turn)] >= scoreNeededToWinCarom; break; case GameMode.ThreeCushionCarom: HandleThreeCushionCaromEndOfTurn(); // There's no action to take when fouling in 3CC (no marker spawn etc) so we just care if the player // scored. isCorrectBallSunk = isMadePoint; winCondition = scores[Convert.ToInt32(isTeam2Turn)] >= scoreNeededToWinCarom; break; default: return; } // Has the player won? if (winCondition) { // Has the player fouled, giving the opponent the win? if (foulCondition) OnTurnOverGameWon(!isTeam2Turn, true); else OnTurnOverGameWon(isTeam2Turn, false); return; } // Has the player fouled catastrophically and lost? if (deferLossCondition) { OnTurnOverGameWon(!isTeam2Turn, true); return; } // Has the player fouled? if (foulCondition) { OnTurnOverFoul(); return; } // Has the player done enough to continue their turn? if (isCorrectBallSunk && !isOpponentColourSunk) { // TODO: not happy with this, revise this based on blackball rules. if (gameMode == GameMode.EightBall && isOpen) { if (numberOfSunkBlues != numberOfSunkOranges) { isTeam2Blue = numberOfSunkBlues > numberOfSunkOranges ? isTeam2Turn : !isTeam2Turn; isOpen = false; ApplyTableColour(isTeam2Turn); } } isPlayerAllowedToPlay = true; _isGameBreak = false; RefreshNetworkData(isTeam2Turn); return; } // End the player's turn and pass the turn on. isPlayerAllowedToPlay = true; _isGameBreak = false; if (IsCarom) { Vector3 temp = currentBallPositions[0]; currentBallPositions[0] = currentBallPositions[9]; currentBallPositions[9] = temp; } turnID++; RefreshNetworkData(!isTeam2Turn); } private void RefreshNetworkData(bool newIsTeam2Playing) { if (logger) { logger._Log(name, "RefreshNetworkData"); } isTeam2Turn = newIsTeam2Playing; Networking.SetOwner(localPlayer, gameObject); RequestSerialization(); ReadNetworkData(); } private void ReadNetworkData() { if (logger) logger._Log(name, "ReadNetworkData"); UpdateScores(); // Assume the marker is off - one of the On... functions might turn it back on again if (marker) marker.SetActive(false); if (gameID > oldGameID && !isGameInMenus) OnNewGameStarted(); else if (isTeam2Turn != oldIsTeam2Turn) OnRemoteTurnChange(); else if (!oldIsGameInMenus && isGameInMenus) OnRemoteGameOver(); if (oldOpen && !isOpen) ApplyTableColour(isTeam2Turn); CopyGameStateToOldState(); if (isTableLocked) poolMenu._EnableUnlockTableButton(); else if (!isGameInMenus) poolMenu._EnableResetButton(); else poolMenu._EnableMainMenu(); poolMenu._UpdateMainMenuView( isTeams, isTeam2Turn, gameMode, timerSecondsPerShot, player1ID, player2ID, player3ID, player4ID, guideLineEnabled ); if (isGameInMenus) { var numberOfPlayers = 0; if (!isTableLocked) { if (player1ID != 0) numberOfPlayers++; if (player2ID != 0) numberOfPlayers++; if (player3ID != 0) numberOfPlayers++; if (player4ID != 0) numberOfPlayers++; } isGameModePractice = localPlayerID == 0 && numberOfPlayers == 1; hasRunSyncOnce = true; return; } switch (gameMode) { case GameMode.KoreanCarom: case GameMode.JapaneseCarom: InitializePocketedStateKoreanJapaneseCarom(); break; case GameMode.ThreeCushionCarom: InitializePocketedStateThreeCushionCarom(); break; case GameMode.EightBall: case GameMode.NineBall: default: break; } // Check this every read // Its basically 'turn start' event if (isPlayerAllowedToPlay) OnLocalTurnStart(); else OnRemoteTurnStart(); // Start of turn so we've hit nothing firstHitBallThisTurn = 0; secondBallHitThisTurn = 0; thirdBallHitThisTurn = 0; cushionsHitThisTurn = 0; hasRunSyncOnce = true; } private void OnRemoteTurnStart() { if (marker9ball) marker9ball.SetActive(false); isTimerRunning = false; isMadePoint = false; isMadeFoul = false; if (devhit) devhit.SetActive(false); if (guideline) guideline.gameObject.SetActive(false); } private void OnLocalTurnStart() { if (((localPlayerID >= 0) && (playerIsTeam2 == isTeam2Turn)) || isGameModePractice) { // Update for desktop isDesktopLocalTurn = true; // Reset hit point desktopHitPoint = Vector3.zero; } else isDesktopLocalTurn = false; if (gameMode == GameMode.NineBall) { int target = GetLowestNumberedBall(ballsArePocketed); if (marker9ball) { marker9ball.SetActive(true); marker9ball.transform.localPosition = currentBallPositions[target]; } ApplyTableColour(isTeam2Turn); } if (!tableModelHasRails || !hasRunSyncOnce) PlaceSunkBallsIntoRestingPlace(); if (timerSecondsPerShot > 0 && !isTimerRunning) ResetTimer(); // sanitize old cue tip location data to prevent stale data from causing unintended effects. localSpacePositionOfCueTipLastFrame = tableSurface.transform.InverseTransformPoint(cueTip.transform.position); } private void OnNewGameStarted() { OnRemoteNewGame(); if (((localPlayerID >= 0) && (playerIsTeam2 == isTeam2Turn)) || isGameModePractice) { if (logger) logger._Log(name, "enabling marker because it is the start of the game and we are breaking"); isRepositioningCueBall = true; repoMaxX = -SPOT_POSITION_X; ballRigidbodies[0].isKinematic = true; if (!marker) return; markerTransform.localPosition = currentBallPositions[0]; if (gameMode == GameMode.EightBall || gameMode == GameMode.NineBall) marker.SetActive(true); ((VRC_Pickup) marker.gameObject.GetComponent(typeof(VRC_Pickup))).pickupable = true; return; } markerTransform.localPosition = currentBallPositions[0]; if (gameMode == GameMode.EightBall || gameMode == GameMode.NineBall) marker.SetActive(true); ((VRC_Pickup) marker.gameObject.GetComponent(typeof(VRC_Pickup))).pickupable = false; } public void OnDisable() { // Disabling the table means we're in an identical state to a late-joiner - the table will have progressed // since we last ran a sync. Ergo, we should tell the table that it needs to handle things as if the player // is a late-joiner. hasRunSyncOnce = false; } private void TeamColors() { ballColors[0] = Color.white; ballColors[1] = Color.black; for (int i = 2; i < 9; i++) { ballColors[i] = tableBlue; } for (int i = 9; i < NUMBER_OF_SIMULATED_BALLS; i++) { ballColors[i] = tableOrange; } } private void UsColors() { // Set colors ballColors[0] = Color.white; ballColors[1] = Color.black; ballColors[2] = Color.yellow; ballColors[3] = Color.blue; ballColors[4] = Color.red; ballColors[5] = Color.magenta; ballColors[6] = new Color(1, 0.6f, 0, 1); ballColors[7] = Color.green; ballColors[8] = new Color(0.59f, 0.29f, 0, 1); ballColors[9] = Color.yellow; ballColors[10] = Color.blue; ballColors[11] = Color.red; ballColors[12] = Color.magenta; ballColors[13] = new Color(1, 0.6f, 0, 1); ballColors[14] = Color.green; ballColors[15] = new Color(0.59f, 0.29f, 0, 1); } private void SetFourBallColours() { ballColors[0] = Color.white; ballColors[9] = Color.yellow; ballColors[2] = Color.red; ballColors[3] = Color.red; } /// /// Updates table colour target to appropriate player colour /// private void ApplyTableColour(bool isTeam2Color) { if (logger) logger._Log(name, "ApplyTableColour"); switch(gameMode) { case GameMode.EightBall: ApplyEightBallTableColour(isTeam2Color); break; case GameMode.NineBall: ApplyNineBallTableColour(isTeam2Color); break; case GameMode.KoreanCarom: case GameMode.JapaneseCarom: case GameMode.ThreeCushionCarom: ApplyCaromTableColour(); break; } cueGrips[Convert.ToInt32(this.isTeam2Turn)].SetColor(uniformMarkerColour, gripColourActive); cueGrips[Convert.ToInt32(!this.isTeam2Turn)].SetColor(uniformMarkerColour, gripColourInactive); if (!ballCustomColours) return; if (gameMode == GameMode.NineBall) { foreach (MeshRenderer meshRenderer in ballRenderers) { meshRenderer.material.SetFloat(ballMaskToggle, 1); } } else { for (var i = 2; i < NUMBER_OF_SIMULATED_BALLS; i++) { if (i < 9) //9 is where it switches to stripes { ballRenderers[i].material.SetFloat(ballMaskToggle, 0); ballRenderers[i].material.SetColor(uniformBallColour, tableBlue); ballRenderers[i].material.SetFloat(uniformBallFloat, shaderToggleFloat); } else { ballRenderers[i].material.SetFloat(ballMaskToggle, 0); ballRenderers[i].material.SetColor(uniformBallColour, tableOrange); ballRenderers[i].material.SetFloat(uniformBallFloat, shaderToggleFloat); } } } } private void ResetTimer() { if (logger) { logger._Log(name, "ResetTimer"); } if (timerSecondsPerShot > 0) { remainingTime = timerSecondsPerShot; isTimerRunning = true; } else { isTimerRunning = false; } } /// /// End of the game. Both with/loss /// private void OnRemoteGameOver() { if (logger) { logger._Log(name, "OnRemoteGameOver"); } ApplyTableColour(isTeam2Winner); if (gameWasReset) { poolMenu._GameWasReset(latestResetReason); foreach (var cue in poolCues) { cue._Respawn(true); } if (slogger) slogger.OscReportGameReset(latestResetReason); } else { poolMenu._TeamWins(isTeam2Winner); PlayAudioClip(winnerSfx); } if (marker9ball) marker9ball.SetActive(false); if (!tableModelHasRails || !hasRunSyncOnce) PlaceSunkBallsIntoRestingPlace(); if (logger) logger._Log(name, "disabling marker because the game is over"); isRepositioningCueBall = false; if (marker) marker.SetActive(false); // Remove any access rights localPlayerID = -1; GrantCueAccess(); player1ID = 0; player2ID = 0; player3ID = 0; player4ID = 0; poolMenu._UpdateMainMenuView( isTeams, isTeam2Turn, gameMode, timerSecondsPerShot, player1ID, player2ID, player3ID, player4ID, guideLineEnabled ); poolMenu._EnableMainMenu(); foreach (var cue in poolCues) { cue._Respawn(true); } EnableCustomBallColorSlider(false); } private void OnRemoteTurnChange() { if (logger) logger._Log(name, nameof(OnRemoteTurnChange)); // Effects ApplyTableColour(isTeam2Turn); mainSrc.PlayOneShot(newTurnSfx, 1.0f); hasFoulBeenPlayedThisTurn = false; isCueOutOfBounds = false; // Register correct cuetip cueTip = cueTips[Convert.ToUInt32(isTeam2Turn)]; if (IsCarom) // 4 ball { if (!isTeam2Turn) { if (logger) logger._Log(name, "0 ball is 0 mesh, 9 ball is 1 mesh"); ballTransforms[0].GetComponent().sharedMesh = cueballMeshes[0]; ballTransforms[9].GetComponent().sharedMesh = cueballMeshes[1]; } else { if (logger) logger._Log(name, "0 ball is 0 mesh, 9 ball is 1 mesh"); ballTransforms[9].GetComponent().sharedMesh = cueballMeshes[0]; ballTransforms[0].GetComponent().sharedMesh = cueballMeshes[1]; } } else { // White was pocketed if (ballsArePocketed[0]) { currentBallPositions[0] = Vector3.zero; currentBallVelocities[0] = Vector3.zero; currentAngularVelocities[0] = Vector3.zero; ballsArePocketed[0] = false; } } if (isFoul && marker) { marker.SetActive(true); markerTransform.localPosition = currentBallPositions[0]; ((VRC_Pickup) marker.gameObject.GetComponent(typeof(VRC_Pickup))).pickupable = false; isRepositioningCueBall = true; ballRigidbodies[0].isKinematic = true; repoMaxX = tableWidth; if (logger) logger._Log(name, "Enabling marker - there was a foul last turn"); if (localPlayerID >= 0 && (playerIsTeam2 == isTeam2Turn || isGameModePractice)) { ((VRC_Pickup) marker.gameObject.GetComponent(typeof(VRC_Pickup))).pickupable = true; // Cope with a marker that might not be synced VRCObjectSync comp = marker.GetComponent(); if (Utilities.IsValid(comp) && Networking.IsOwner(gameObject)) Networking.SetOwner(Networking.LocalPlayer, marker); } } // Force timer reset if (timerSecondsPerShot > 0) ResetTimer(); } /// /// Grant cue access if we are playing /// private void GrantCueAccess() { if (logger) logger._Log(name, "GrantCueAccess"); if (localPlayerID > -1) { if (isGameModePractice) { poolCues[0]._AllowAccess(); poolCues[1]._AllowAccess(); } else if (!playerIsTeam2) // Local player is 1, or 3 { poolCues[0]._AllowAccess(); poolCues[1]._DenyAccess(); } else // Local player is 0, or 2 { poolCues[1]._AllowAccess(); poolCues[0]._DenyAccess(); } } else { poolCues[0]._DenyAccess(); poolCues[1]._DenyAccess(); } } // The generic new game setup that runs for everyone. private void OnRemoteNewGame() { if (logger) logger._Log(name, "OnRemoteNewGame"); poolMenu._EnableResetButton(); // Cue ball currentBallPositions[0] = new Vector3(-SPOT_POSITION_X, 0.0f, 0.0f); currentBallVelocities[0] = Vector3.zero; // Start at spot switch (gameMode) { case GameMode.EightBall: Initialize8Ball(); InitializeEightBallVisuals(); break; case GameMode.NineBall: Initialize9Ball(); InitializeNineBallVisuals(); break; case GameMode.KoreanCarom: case GameMode.JapaneseCarom: Initialize4Ball(); InitializeCaromVisuals(); break; case GameMode.ThreeCushionCarom: InitializeThreeCushionCarom(); InitializeThreeCushionCaromVisuals(); break; } if (localPlayerID >= 0) playerIsTeam2 = localPlayerID % 2 == 1; tableRenderer.material.SetColor(ClothColour, pointerClothColour); if (tableReflection) tableReflection.RenderProbe(); ApplyTableColour(false); GrantCueAccess(); // Effects - don't run this if this is the first network sync, as we may be catching up. if (hasRunSyncOnce) { introAnimTimer = introAnimationLength; if (introAnimTimer > 0.0f) { mainSrc.PlayOneShot(introSfx, 1.0f); SendCustomEventDelayedSeconds(nameof(_ReEnableShadowConstraints), introAnimationLength); } } isTimerRunning = false; poolCues[0]._LeftFlatscreenPlayerCamera(); poolCues[1]._LeftFlatscreenPlayerCamera(); // Make sure that we run a pass on rigidbodies to ensure they are off. PlaceSunkBallsIntoRestingPlace(); OnRemoteTurnChange(); if (slogger) slogger.OscReportGameStarted(localPlayerID >= 0); } /// /// Call at the start of each turn. /// Place sunk inert balls into a specific storage location. /// Sunk ball state is replicated, so this is network-stable. /// private void PlaceSunkBallsIntoRestingPlace() { if (logger) logger._Log(name, "PlaceSunkBallsIntoRestingPlace"); var numberOfSunkBalls = 0; Vector3 localPosition = sunkBallsPositionRoot.localPosition; var posX = localPosition.x; var posY = localPosition.y; var posZ = localPosition.z; for (int i = 0; i < NUMBER_OF_SIMULATED_BALLS; i++) { ballTransforms[i].GetComponent().isKinematic = true; if (!ballsArePocketed[i]) continue; ballTransforms[i].localPosition = new Vector3(posX + numberOfSunkBalls * ballDiameter, posY, posZ); numberOfSunkBalls++; } } private int GetLowestNumberedBall(bool[] balls) { // order in ballsArePocketed is assumed to be [c812345679] for (int i = 2; i < 9; i++) { if (!balls[i]) return i; } if (!balls[1]) return 1; return !balls[9] ? 9 : 0; } private void OnTurnOverGameWon(bool newIsTeam2Winner, bool causedByFoul) { if (logger) logger._Log(name, "OnTurnOverGameWon"); isGameInMenus = true; poolCues[0].tableIsActive = false; poolCues[1].tableIsActive = false; isTeam2Winner = newIsTeam2Winner; isFoul = causedByFoul; isGameOver = true; RefreshNetworkData(isTeam2Turn); } private void OnTurnOverFoul() { if (logger) logger._Log(name, "OnTurnOverFoul"); isFoul = true; isPlayerAllowedToPlay = true; turnID++; RefreshNetworkData(!isTeam2Turn); } /// /// Copy current values to previous values /// private void CopyGameStateToOldState() { oldOpen = isOpen; oldIsGameInMenus = isGameInMenus; oldGameID = gameID; oldIsTeam2Turn = isTeam2Turn; for (int i = 0; i < NUMBER_OF_SIMULATED_BALLS; i++) { oldBallsArePocketed[i] = ballsArePocketed[i]; } } public void _ToggleDesktopUITopDownView() { if (isInDesktopTopDownView) OnDesktopTopDownViewExit(); else if (canEnterDesktopTopDownView) OnDesktopTopDownViewStart(); } private void OnDesktopTopDownViewExit() { if (logger) logger._Log(name, "OnDesktopTopDownViewExit"); isInDesktopTopDownView = false; if (desktopBase) { desktopBase.SetActive(false); if (!desktopCamera) { desktopCamera = desktopBase.GetComponentInChildren(); } desktopCamera.enabled = false; } poolCues[0]._LeftFlatscreenPlayerCamera(); poolCues[1]._LeftFlatscreenPlayerCamera(); Networking.LocalPlayer.Immobilize(false); if (isGameModePractice) { foreach (var cue in poolCues) { cue._Respawn(false); } } poolMenu._LeftFlatscreenPlayerCamera(); pancakeUI._ExitDesktopTopDownView(); } private void HandleCueBallHit() { if (logger) logger._Log(name, nameof(HandleCueBallHit)); isRepositioningCueBall = false; if (marker) marker.SetActive(false); if (devhit) devhit.SetActive(false); if (guideline) guideline.gameObject.SetActive(false); // Remove locks _EndHit(); isPlayerAllowedToPlay = false; isFoul = false; // In case did not drop foul marker cueTipSrc.transform.SetPositionAndRotation(cueTip.transform.position, new Quaternion()); cueTipSrc.PlayOneShot(hitBallSfx, Mathf.Clamp(currentBallVelocities[0].magnitude * 0.1f, 0f, 1f)); // Commit changes turnIsRunning = true; // Make sure we are the network owner Networking.SetOwner(localPlayer, gameObject); RefreshNetworkData(isTeam2Turn); isSimulatedByUs = true; } public override void InputUse(bool value, VRC.Udon.Common.UdonInputEventArgs args) { if (!isInDesktopTopDownView) { if (canEnterDesktopTopDownView) OnDesktopTopDownViewStart(); return; } lastInputUseDown = value; } public override void InputLookHorizontal(float value, VRC.Udon.Common.UdonInputEventArgs args) { if (!isInDesktopTopDownView) return; lastLookHorizontal = value; } public override void InputLookVertical(float value, VRC.Udon.Common.UdonInputEventArgs args) { if (!isInDesktopTopDownView) return; lastLookVertical = value; } // TODO: Single use function, but it short-circuits so cannot be easily put into its using function. private void HandleUpdatingDesktopViewUI() { #if UNITY_ANDROID lastLookHorizontal *= 3; lastLookVertical *= 3; #endif if (isEnteringDesktopModeThisFrame) { isEnteringDesktopModeThisFrame = false; return; } if (Input.GetKey(KeyCode.W)) desktopHitPoint += Vector3.forward * Time.deltaTime; if (Input.GetKey(KeyCode.S)) desktopHitPoint += Vector3.back * Time.deltaTime; if (Input.GetKey(KeyCode.A)) desktopHitPoint += Vector3.left * Time.deltaTime; if (Input.GetKey(KeyCode.D)) desktopHitPoint += Vector3.right * Time.deltaTime; if (Input.GetKey(KeyCode.UpArrow)) lastLookVertical = desktopAngleIncrement * Time.deltaTime; if (Input.GetKey(KeyCode.DownArrow)) lastLookVertical = -desktopAngleIncrement * Time.deltaTime; if (Input.GetKey(KeyCode.LeftArrow)) lastLookHorizontal = -0.25f; if (Input.GetKey(KeyCode.RightArrow)) lastLookHorizontal = 0.25f; var dir = desktopAimPoint - currentBallPositions[0]; dir = Quaternion.Euler(new Vector3(0f, lastLookHorizontal, 0f)) * dir; desktopAimPoint = dir + currentBallPositions[0]; if (!isDesktopLocalTurn) desktopCamera.transform.SetLocalPositionAndRotation(desktopCameraInitialPosition, desktopCameraInitialRotation); else { Vector3 ncursor = desktopAimPoint; ncursor.y = 0.0f; Vector3 delta = ncursor - currentBallPositions[0]; newDesktopCue = Convert.ToUInt32(isTeam2Turn); GameObject cue = desktopCueParents[newDesktopCue]; if (isGameModePractice && newDesktopCue != oldDesktopCue) { poolCues[oldDesktopCue]._Respawn(false); oldDesktopCue = newDesktopCue; } if (lastInputUseDown) { inputHeldDownTime = Mathf.Clamp(inputHeldDownTime, 0.0f, 0.5f); if (!isDesktopShootingIn) { isDesktopShootingIn = true; // Create shooting vector desktopShootVector = delta.normalized; // Create copy of cursor for later desktopSafeRemovePoint = desktopAimPoint; } // Calculate shoot amount; desktopShootForce += Time.deltaTime * desktopShotPowerMult; isDesktopSafeRemove = desktopShootForce < 0.0f; desktopShootForce = Mathf.Clamp(desktopShootForce, 0.0f, 0.5f); // Set delta back to dkShootVector delta = desktopShootVector; } else { inputHeldDownTime = 0; // Trigger shot if (isDesktopShootingIn) { // Shot cancel if (!isDesktopSafeRemove) { isDesktopLocalTurn = false; HitBallWithCue(cueTip.transform.forward, Mathf.Pow(desktopShootForce * 2.0f, 1.4f) * 7.0f); } // Restore cursor position desktopAimPoint = desktopSafeRemovePoint; // 1-frame override to fix rotation delta = desktopShootVector; } isDesktopShootingIn = false; desktopShootForce = 0.0f; } desktopAngle = Mathf.Clamp(desktopAngle + lastLookVertical, MIN_DESKTOP_CUE_ANGLE, MAX_DESKTOP_CUE_ANGLE); if (tiltAmount) tiltAmount.fillAmount = Mathf.InverseLerp(MIN_DESKTOP_CUE_ANGLE, MAX_DESKTOP_CUE_ANGLE, desktopAngle); // Clamp in circle if (desktopHitPoint.magnitude > 0.90f) desktopHitPoint = desktopHitPoint.normalized * 0.9f; desktopHitPosition.transform.localPosition = desktopHitPoint; // Create rotation Quaternion xr = Quaternion.AngleAxis(desktopAngle, Vector3.right); Quaternion r = Quaternion.AngleAxis(Mathf.Atan2(delta.x, delta.z) * Mathf.Rad2Deg, Vector3.up); Vector3 worldHit = new Vector3(desktopHitPoint.x * BALL_PL_X, desktopHitPoint.z * BALL_PL_X, -0.89f - desktopShootForce); cue.transform.localRotation = r * xr; cue.transform.position = tableSurface.transform.TransformPoint(currentBallPositions[0] + (r * xr * worldHit)); desktopCamera.transform.position = cue.transform.position; desktopCamera.transform.localRotation = cue.transform.localRotation; } var powerBarPos = powerBar.transform.localPosition; powerBarPos.x = Mathf.Lerp(initialPowerBarPos.x, topBar.transform.localPosition.x, desktopShootForce * 2.0f); powerBar.transform.localPosition = powerBarPos; } private void UpdateScores() { switch (gameMode) { case GameMode.EightBall: ReportEightBallScore(); return; case GameMode.NineBall: ReportNineBallScore(); return; case GameMode.KoreanCarom: case GameMode.JapaneseCarom: case GameMode.ThreeCushionCarom: ReportFourBallScore(); return; } } private void ResetScores() { if (logger) logger._Log(name, "ResetScores"); poolMenu._SetScore(false, 0); poolMenu._SetScore(true, 0); } public override void OnPlayerLeft(VRCPlayerApi player) { if (!Networking.IsOwner(localPlayer, gameObject) || !player.IsValid()) return; var playerID = player.playerId; if (playerID != player1ID && playerID != player2ID && playerID != player3ID && playerID != player4ID) return; if (isGameInMenus) RemovePlayerFromGame(playerID); else _Reset(ResetReason.PlayerLeft); } /// Is the local player near the table? private bool isNearTable; /// Can the local player enter the desktop top-down view? private bool canEnterDesktopTopDownView; [HideInInspector] public ushort numberOfCuesHeldByLocalPlayer; private int ClothColour = Shader.PropertyToID("_ClothColour"); private int MainTex = Shader.PropertyToID("_MainTex"); public void _LocalPlayerEnteredAreaNearTable() { if (isPlayerInVR) { return; } isNearTable = true; CheckIfCanEnterDesktopTopDownView(); } /// Is the local player near the table? public void _LocalPlayerLeftAreaNearTable() { isNearTable = false; CannotEnterDesktopTopDownView(); } public void _LocalPlayerPickedUpCue() { numberOfCuesHeldByLocalPlayer++; CheckIfCanEnterDesktopTopDownView(); } public void _LocalPlayerDroppedCue() { numberOfCuesHeldByLocalPlayer--; if (numberOfCuesHeldByLocalPlayer == 0) CannotEnterDesktopTopDownView(); } private void CheckIfCanEnterDesktopTopDownView() { if (!isNearTable || numberOfCuesHeldByLocalPlayer <= 0) return; canEnterDesktopTopDownView = true; if (pancakeUI) pancakeUI.gameObject.SetActive(true); } private void CannotEnterDesktopTopDownView() { canEnterDesktopTopDownView = false; if (pancakeUI) pancakeUI.gameObject.SetActive(false); } public static string ToReasonString(ResetReason reason) { switch (reason) { case ResetReason.InvalidState: return "The table was in an invalid state and has been reset"; case ResetReason.PlayerLeft: return "A player left, so the table was reset."; case ResetReason.InstanceOwnerReset: return "The instance owner reset the table."; case ResetReason.PlayerReset: return "A player has reset the table"; default: return "No reason"; } } private void HandleBallSunk(bool isSuccess) { mainSrc.PlayOneShot(sinkSfx, 1.0f); // If good pocket if (isSuccess) HandleSuccessEffects(); else HandleFoulEffects(); } private void HandleSuccessEffects() { // Make a bright flash tableCurrentColour *= 1.9f; PlayAudioClip(successSfx); } private bool hasFoulBeenPlayedThisTurn; private void HandleFoulEffects() { if (hasFoulBeenPlayedThisTurn) return; hasFoulBeenPlayedThisTurn = true; tableCurrentColour = pointerColourErr; PlayAudioClip(foulSfx); } private void HandleBallCollision(int ballID, int otherBallID, Vector3 reflection) { if (logger) logger._Log(name, nameof(HandleBallCollision)); // Prevent sound spam if it happens if (currentBallVelocities[ballID].sqrMagnitude > 0 && currentBallVelocities[otherBallID].sqrMagnitude > 0) { ballPoolTransforms[ballID].position = ballTransforms[ballID].position; ballPool[ballID].PlayOneShot(hitsSfx[UnityEngine.Random.Range(0, hitsSfx.Length - 1)], Mathf.Clamp01(currentBallVelocities[ballID].magnitude * reflection.magnitude)); } if (ballID != 0) return; switch (gameMode) { case GameMode.EightBall: if (firstHitBallThisTurn == 0) { firstHitBallThisTurn = otherBallID; if (IsFirstEightBallHitFoul(firstHitBallThisTurn, GetNumberOfSunkBlues(), GetNumberOfSunkOranges())) HandleFoulEffects(); } break; case GameMode.NineBall: if (firstHitBallThisTurn == 0) { firstHitBallThisTurn = otherBallID; } break; case GameMode.KoreanCarom: HandleKorean4BallScoring(otherBallID); break; case GameMode.JapaneseCarom: HandleJapanese4BallScoring(otherBallID); break; case GameMode.ThreeCushionCarom: HandleThreeCushionCaromScoring(otherBallID); break; } } private void PlayAudioClip(AudioClip clip) { if (!clip) return; mainSrc.PlayOneShot(clip); } } }