Jamie Greunbaum 8eaef49f2e - Added game room, including pool and skee-ball.
- Moved video screen into its own separate movie tent.
- Adjusted stable post-processing volume.
- Chickens are now at full volume.
- Added button to toggle chickens off and on.
2026-02-09 03:49:54 -05:00

2201 lines
72 KiB
C#

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
{
/// <summary>
/// The reasons why the table can be reset
/// </summary>
public enum ResetReason
{
InstanceOwnerReset,
PlayerReset,
InvalidState,
PlayerLeft
}
public enum GameMode {
EightBall,
NineBall,
KoreanCarom,
JapaneseCarom,
ThreeCushionCarom
}
/// <summary>
/// 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.
/// </summary>
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;
/// <summary>
/// Is the game currently in the first shot?
/// </summary>
[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
/// <summary>
/// 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.
/// </summary>
// 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];
/// <summary>
/// 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.
/// </summary>
/*[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;
/// <summary>
/// The SFX that plays when a good thing happens (sinking a ball correctly, etc)
/// </summary>
public AudioClip successSfx;
/// <summary>
/// The SFX that plays when a foul occurs
/// </summary>
public AudioClip foulSfx;
/// <summary>
/// The SFX that plays when someone wins
/// </summary>
public AudioClip winnerSfx;
public AudioClip[] hitsSfx;
public AudioClip newTurnSfx;
public AudioClip pointMadeSfx;
public AudioClip hitBallSfx;
public ReflectionProbe tableReflection;
public Mesh[] cueballMeshes;
public Mesh nineBall;
/// <summary>
/// Player is hitting
/// </summary>
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;
/// <summary>
/// We are waiting for our local simulation to finish, before we unpack data
/// </summary>
private bool isUpdateLocked;
/// <summary>
/// The first ball to be hit by cue ball
/// </summary>
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;
/// <summary>
/// If the simulation was initiated by us, only set from update
/// </summary>
private bool isSimulatedByUs;
/// <summary>
/// Ball dropper timer
/// </summary>
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;
/// <summary>
/// Game should run in practice mode
/// </summary>
private bool isGameModePractice;
private bool isInDesktopTopDownView;
/// <summary>
/// Interpreted value
/// </summary>
private bool playerIsTeam2;
/// <summary>
/// Runtime target colour
/// </summary>
private Color tableSrcColour = new Color(1.0f, 1.0f, 1.0f, 1.0f);
/// <summary>
/// Runtime actual colour
/// </summary>
private Color tableCurrentColour = new Color(1.0f, 1.0f, 1.0f, 1.0f);
/// <summary>
/// Team 0
/// </summary>
private Color pointerColour0;
/// <summary>
/// Team 1
/// </summary>
private Color pointerColour1;
/// <summary>
/// No team / open / 9 ball
/// </summary>
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;
/// <summary>
/// Cue input tracking
/// </summary>
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;
/// <summary>
/// Have we run a network sync once? Used for situations where we need to specifically catch up a late-joiner.
/// </summary>
private bool hasRunSyncOnce;
/// <summary>
/// For clamping to table or set lower for kitchen
/// </summary>
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<Rigidbody>();
}
ballShadowPosConstraintTransforms = new Transform[NUMBER_OF_SIMULATED_BALLS];
for (var i = 0; i < NUMBER_OF_SIMULATED_BALLS; i++)
{
ballShadowPosConstraintTransforms[i] = ballShadowPosConstraints[i].transform;
}
mainSrc = GetComponent<AudioSource>();
if (audioSourcePoolContainer)
{
ballPool = audioSourcePoolContainer.GetComponentsInChildren<AudioSource>();
ballPoolTransforms = new Transform[ballPool.Length];
for (var i = 0; i < ballPool.Length; i++)
{
ballPoolTransforms[i] = ballPool[i].transform;
}
}
desktopCamera = desktopBase.GetComponentInChildren<Camera>();
desktopCamera.enabled = false;
markerTransform = marker.transform;
CopyGameStateToOldState();
markerMaterial = marker.GetComponent<MeshRenderer>().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<MeshRenderer>();
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;
}
/// <summary>
/// Updates table colour target to appropriate player colour
/// </summary>
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;
}
}
/// <summary>
/// End of the game. Both with/loss
/// </summary>
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<MeshFilter>().sharedMesh = cueballMeshes[0];
ballTransforms[9].GetComponent<MeshFilter>().sharedMesh = cueballMeshes[1];
}
else
{
if (logger)
logger._Log(name, "0 ball is 0 mesh, 9 ball is 1 mesh");
ballTransforms[9].GetComponent<MeshFilter>().sharedMesh = cueballMeshes[0];
ballTransforms[0].GetComponent<MeshFilter>().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<VRCObjectSync>();
if (Utilities.IsValid(comp) && Networking.IsOwner(gameObject))
Networking.SetOwner(Networking.LocalPlayer, marker);
}
}
// Force timer reset
if (timerSecondsPerShot > 0)
ResetTimer();
}
/// <summary>
/// Grant cue access if we are playing
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
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<Rigidbody>().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);
}
/// <summary>
/// Copy current values to previous values
/// </summary>
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<Camera>();
}
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);
}
/// <Summary> Is the local player near the table? </Summary>
private bool isNearTable;
/// <Summary> Can the local player enter the desktop top-down view? </Summary>
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();
}
/// <Summary> Is the local player near the table? </Summary>
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);
}
}
}