migration

This commit is contained in:
2026-07-26 12:31:59 +05:30
parent 4b2129b944
commit c2f4c2f808
12 changed files with 1818 additions and 981 deletions
+64 -82
View File
@@ -1,83 +1,65 @@
Logger initiated at 7/4/2026 12:31:19 AM
Logger initiated at 7/19/2026 12:08:23 PM
[7/4/2026 12:31:19 AM] Cupid settings updated
[7/4/2026 12:31:20 AM] Cupid settings updated
[7/4/2026 12:31:21 AM] Configured dedicated match reporting for match id 0 with secret 38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328 and internal api base https://kickkingsapi.playpoolstudios.com
[7/4/2026 12:31:27 AM] Invalid Room port
[7/4/2026 12:31:27 AM] Mirror server exception logging enabled
[7/4/2026 12:31:27 AM] Game started On localhost, tutorial mode enabled
[7/4/2026 12:31:27 AM] Client connected
[7/4/2026 12:31:27 AM] Selected team changed from Red to Blue
[7/4/2026 12:31:30 AM] force = 70 * 0.9955804 = 69.69063
[7/4/2026 12:31:30 AM] launching puck at (0.33, 4.99) with (-23.24, -347.68) force
[7/4/2026 12:31:33 AM] Selected team changed from Blue to Red
[7/4/2026 12:31:36 AM] AI best puck Puck (4): total=2.27 (prox=0.93, between=0.00, traj=0.29, push=1.00)
[7/4/2026 12:31:36 AM] force = 70 * 0.9010412 = 63.07288
[7/4/2026 12:31:36 AM] launching puck at (-3.93, -0.74) with (247.90, 46.87) force
[7/4/2026 12:31:39 AM] Selected team changed from Red to Blue
[7/4/2026 12:31:41 AM] force = 70 * 0.9955804 = 69.69063
[7/4/2026 12:31:41 AM] launching puck at (-4.85, 1.20) with (338.34, -83.34) force
[7/4/2026 12:31:44 AM] Selected team changed from Blue to Red
[7/4/2026 12:31:47 AM] AI best puck Puck: total=1.60 (prox=0.85, between=0.00, traj=0.00, push=1.00)
[7/4/2026 12:31:47 AM] force = 70 * 0.9010412 = 63.07288
[7/4/2026 12:31:47 AM] launching puck at (4.00, 0.10) with (-252.21, -6.33) force
[7/4/2026 12:31:50 AM] Selected team changed from Red to Blue
[7/4/2026 12:31:50 AM] force = 70 * 0.9955804 = 69.69063
[7/4/2026 12:31:50 AM] launching puck at (4.64, -1.86) with (-323.34, 129.88) force
[7/4/2026 12:31:54 AM] Selected team changed from Blue to Red
[7/4/2026 12:31:56 AM] AI best puck Puck (3): total=1.52 (prox=0.77, between=0.00, traj=0.00, push=1.00)
[7/4/2026 12:31:56 AM] force = 70 * 0.9010412 = 63.07288
[7/4/2026 12:31:56 AM] launching puck at (3.88, 0.98) with (-244.53, -62.11) force
[7/4/2026 12:31:59 AM] Selected team changed from Red to Blue
[7/4/2026 12:32:00 AM] force = 70 * 0.9955804 = 69.69063
[7/4/2026 12:32:00 AM] launching puck at (4.19, 2.72) with (-292.21, -189.82) force
[7/4/2026 12:32:03 AM] Selected team changed from Blue to Red
[7/4/2026 12:32:06 AM] AI best puck Puck (3): total=1.60 (prox=0.85, between=0.00, traj=0.00, push=1.00)
[7/4/2026 12:32:06 AM] force = 70 * 0.9010412 = 63.07288
[7/4/2026 12:32:06 AM] launching puck at (-3.51, 1.92) with (221.34, -121.08) force
[7/4/2026 12:32:06 AM] Score changed from 0 to 1
[7/4/2026 12:32:06 AM] Blue goal scored, blue score is now 1
[7/4/2026 12:32:06 AM] Selected team changed from Red to Blue
[7/4/2026 12:32:09 AM] Selected team changed from Blue to Red
[7/4/2026 12:32:12 AM] AI best puck Puck: total=3.05 (prox=0.85, between=0.03, traj=0.71, push=1.00)
[7/4/2026 12:32:12 AM] force = 70 * 0.9010412 = 63.07288
[7/4/2026 12:32:12 AM] launching puck at (0.00, -4.00) with (0.00, 252.29) force
[7/4/2026 12:32:16 AM] Selected team changed from Red to Blue
[7/4/2026 12:32:17 AM] force = 70 * 0.9955804 = 69.69063
[7/4/2026 12:32:17 AM] launching puck at (4.31, -2.54) with (-300.13, 177.04) force
[7/4/2026 12:32:20 AM] Selected team changed from Blue to Red
[7/4/2026 12:32:23 AM] AI best puck Puck: total=3.55 (prox=0.93, between=0.03, traj=0.91, push=1.00)
[7/4/2026 12:32:23 AM] force = 70 * 0.9010412 = 63.07288
[7/4/2026 12:32:23 AM] launching puck at (0.07, -4.00) with (-4.27, 252.26) force
[7/4/2026 12:32:23 AM] Score changed from 0 to 1
[7/4/2026 12:32:23 AM] Red goal scored, red score is now 1
[7/4/2026 12:32:26 AM] Selected team changed from Red to Blue
[7/4/2026 12:32:28 AM] force = 70 * 0.7183406 = 50.28384
[7/4/2026 12:32:28 AM] launching puck at (1.75, 1.97) with (-88.20, -98.90) force
[7/4/2026 12:32:31 AM] Selected team changed from Blue to Red
[7/4/2026 12:32:34 AM] AI best puck Puck: total=3.19 (prox=0.85, between=0.03, traj=0.78, push=1.00)
[7/4/2026 12:32:34 AM] force = 70 * 0.9010412 = 63.07288
[7/4/2026 12:32:34 AM] launching puck at (0.00, -4.00) with (0.00, 252.29) force
[7/4/2026 12:32:34 AM] Score changed from 1 to 2
[7/4/2026 12:32:34 AM] Red goal scored, red score is now 2
[7/4/2026 12:32:37 AM] Selected team changed from Red to Blue
[7/4/2026 12:32:38 AM] force = 70 * 0.9955804 = 69.69063
[7/4/2026 12:32:38 AM] launching puck at (3.10, 3.93) with (-215.79, -273.60) force
[7/4/2026 12:32:41 AM] Selected team changed from Blue to Red
[7/4/2026 12:32:44 AM] AI best puck Puck: total=3.19 (prox=0.85, between=0.03, traj=0.78, push=1.00)
[7/4/2026 12:32:44 AM] force = 70 * 0.9010412 = 63.07288
[7/4/2026 12:32:44 AM] launching puck at (0.00, -4.00) with (-0.02, 252.29) force
[7/4/2026 12:32:44 AM] Score changed from 2 to 3
[7/4/2026 12:32:44 AM] Red goal scored, red score is now 3
[7/4/2026 12:32:57 AM] Mirror server: client disconnected (connId=0)
[7/4/2026 12:32:57 AM] Client disconnected
[7/4/2026 12:33:02 AM] Cupid settings updated
[7/4/2026 12:33:03 AM] Cupid settings updated
[7/4/2026 12:33:04 AM] Configured dedicated match reporting for match id 0 with secret 38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328 and internal api base https://kickkingsapi.playpoolstudios.com
[7/4/2026 12:33:10 AM] Invalid Room port
[7/4/2026 12:33:10 AM] Mirror server exception logging enabled
[7/4/2026 12:33:10 AM] Game started On localhost, tutorial mode enabled
[7/4/2026 12:33:10 AM] Client connected
[7/4/2026 12:33:10 AM] Selected team changed from Red to Blue
[7/4/2026 12:33:14 AM] Mirror server: client disconnected (connId=0)
[7/4/2026 12:33:14 AM] Client disconnected
[7/19/2026 12:08:23 PM] Cupid settings updated
[7/19/2026 12:08:23 PM] Cupid settings updated
[7/19/2026 12:08:25 PM] Starting matchmake as warlock2
[7/19/2026 12:08:53 PM] Still waiting: {"ok":true,"entry_fee":21,"rc_prize":34,"for_red":{"Players":[{"Name":"warlock2","LastSeen":1784443133384,"UserId":3,"l10_wins":4,"l10_losses":4}],"GameName":"soccar","Port":26143,"InitTime":1784443133384,"entry_fee":21,"match_id":450,"rc_prize":34,"your_team":"red"},"for_blue":null}
[7/19/2026 12:08:54 PM] Got into a room
[7/19/2026 12:08:54 PM] {"ok":true,"entry_fee":21,"rc_prize":34,"for_red":{"Players":[{"Name":"warlock2","LastSeen":1784443134684,"UserId":3,"l10_wins":4,"l10_losses":4},{"Name":"Malik gaming","LastSeen":1784443134377,"UserId":96,"l10_wins":0,"l10_losses":5}],"GameName":"soccar","Port":26143,"InitTime":1784443133384,"entry_fee":21,"match_id":450,"instance_started":true,"rc_prize":34,"your_team":"red"},"for_blue":{"Players":[{"Name":"warlock2","LastSeen":1784443134684,"UserId":3,"l10_wins":4,"l10_losses":4},{"Name":"Malik gaming","LastSeen":1784443134377,"UserId":96,"l10_wins":0,"l10_losses":5}],"GameName":"soccar","Port":26143,"InitTime":1784443133384,"entry_fee":21,"match_id":450,"instance_started":true,"rc_prize":34,"your_team":"blue"}}
[7/19/2026 12:08:54 PM] Configured dedicated match reporting for match id 450 with secret 38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328 and internal api base https://kkapi.playpoolstudios.com
[7/19/2026 12:08:54 PM] Setting cupid to load game scene
[7/19/2026 12:08:54 PM] Loading game scene
[7/19/2026 12:08:59 PM] Starting client at $kkapi.playpoolstudios.com:26143
[7/19/2026 12:08:59 PM] Client connected
[7/19/2026 12:09:06 PM] Selected team changed from Red to Blue
[7/19/2026 12:09:24 PM] Selected team changed from Blue to Red
[7/19/2026 12:09:35 PM] Score changed from 0 to 1
[7/19/2026 12:09:38 PM] Selected team changed from Red to Blue
[7/19/2026 12:09:51 PM] Selected team changed from Blue to Red
[7/19/2026 12:09:56 PM] Selected team changed from Red to Blue
[7/19/2026 12:10:11 PM] Selected team changed from Blue to Red
[7/19/2026 12:10:17 PM] Selected team changed from Red to Blue
[7/19/2026 12:10:27 PM] Selected team changed from Blue to Red
[7/19/2026 12:10:42 PM] Selected team changed from Red to Blue
[7/19/2026 12:10:48 PM] Score changed from 0 to 1
[7/19/2026 12:10:51 PM] Selected team changed from Blue to Red
[7/19/2026 12:11:06 PM] Selected team changed from Red to Blue
[7/19/2026 12:11:12 PM] Selected team changed from Blue to Red
[7/19/2026 12:11:19 PM] Selected team changed from Red to Blue
[7/19/2026 12:11:27 PM] Selected team changed from Blue to Red
[7/19/2026 12:11:42 PM] Selected team changed from Red to Blue
[7/19/2026 12:11:48 PM] Selected team changed from Blue to Red
[7/19/2026 12:11:59 PM] Selected team changed from Red to Blue
[7/19/2026 12:12:12 PM] Selected team changed from Blue to Red
[7/19/2026 12:12:27 PM] Selected team changed from Red to Blue
[7/19/2026 12:12:31 PM] Selected team changed from Blue to Red
[7/19/2026 12:12:46 PM] Selected team changed from Red to Blue
[7/19/2026 12:12:56 PM] Selected team changed from Blue to Red
[7/19/2026 12:13:05 PM] Selected team changed from Red to Blue
[7/19/2026 12:13:12 PM] Selected team changed from Blue to Red
[7/19/2026 12:13:27 PM] Selected team changed from Red to Blue
[7/19/2026 12:13:36 PM] Selected team changed from Blue to Red
[7/19/2026 12:13:38 PM] Score changed from 1 to 2
[7/19/2026 12:13:41 PM] Selected team changed from Red to Blue
[7/19/2026 12:13:47 PM] Selected team changed from Blue to Red
[7/19/2026 12:14:02 PM] Selected team changed from Red to Blue
[7/19/2026 12:14:11 PM] Score changed from 1 to 2
[7/19/2026 12:14:14 PM] Selected team changed from Blue to Red
[7/19/2026 12:14:29 PM] Selected team changed from Red to Blue
[7/19/2026 12:14:42 PM] Selected team changed from Blue to Red
[7/19/2026 12:14:47 PM] Selected team changed from Red to Blue
[7/19/2026 12:14:53 PM] Selected team changed from Blue to Red
[7/19/2026 12:15:00 PM] Selected team changed from Red to Blue
[7/19/2026 12:15:15 PM] Selected team changed from Blue to Red
[7/19/2026 12:15:25 PM] Selected team changed from Red to Blue
[7/19/2026 12:15:40 PM] Selected team changed from Blue to Red
[7/19/2026 12:15:45 PM] Selected team changed from Red to Blue
[7/19/2026 12:16:00 PM] Selected team changed from Blue to Red
[7/19/2026 12:16:05 PM] Selected team changed from Red to Blue
[7/19/2026 12:16:20 PM] Selected team changed from Blue to Red
[7/19/2026 12:16:27 PM] Selected team changed from Red to Blue
[7/19/2026 12:16:42 PM] Selected team changed from Blue to Red
[7/19/2026 12:16:43 PM] Score changed from 2 to 3
[7/19/2026 12:16:50 PM] Client disconnected
[7/19/2026 12:16:55 PM] Cupid settings updated
[7/19/2026 12:16:55 PM] Cupid settings updated
+5
View File
@@ -411,3 +411,8 @@
[5/1/2026 1:13:06 AM] Active player connections: 0
[5/1/2026 1:13:12 AM] Server will be closed due to no players in, 40
[5/1/2026 1:13:22 AM] Server will be closed due to no players in, 30
[7/19/2026 12:42:42 AM] Starting server at port 7777
[7/19/2026 12:42:42 AM] Mirror server exception logging enabled
[7/19/2026 12:42:42 AM] Failed to fetch red and blue names, isServer?
[7/19/2026 12:42:42 AM] Starting auto close watchdog
[7/19/2026 12:42:42 AM] Active player connections: 0
+6 -6
View File
@@ -4421,7 +4421,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 7de8d561c0baced43a6296139e1d7b46, type: 3}
m_Sprite: {fileID: 21300000, guid: 4297fad7619e3e8459accb93d1731a2c, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
@@ -4636,7 +4636,7 @@ MonoBehaviour:
preview: 0
isPlaying: 1
loop: 1
currentFrame: 177
currentFrame: 57
--- !u!1 &239916595
GameObject:
m_ObjectHideFlags: 0
@@ -12735,7 +12735,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &925282217
RectTransform:
m_ObjectHideFlags: 0
@@ -15675,7 +15675,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &1197981874
RectTransform:
m_ObjectHideFlags: 0
@@ -19498,7 +19498,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0}
m_AnchorMax: {x: 0.5, y: 0}
m_AnchoredPosition: {x: -32.9, y: 562.4}
m_AnchoredPosition: {x: -32.9, y: 527}
m_SizeDelta: {x: 1080, y: 66.900024}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1506964490
@@ -22656,7 +22656,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0}
m_AnchorMax: {x: 0.5, y: 0}
m_AnchoredPosition: {x: 0, y: 585.17004}
m_AnchoredPosition: {x: 0, y: 594}
m_SizeDelta: {x: 1080, y: 66.900024}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1744675600
+69
View File
@@ -898,6 +898,7 @@ GameObject:
- component: {fileID: 189358081}
- component: {fileID: 189358080}
- component: {fileID: 189358079}
- component: {fileID: 189358082}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
@@ -979,6 +980,50 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &189358082
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 189358078}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_RenderShadows: 1
m_RequiresDepthTextureOption: 2
m_RequiresOpaqueTextureOption: 2
m_CameraType: 0
m_Cameras: []
m_RendererIndex: -1
m_VolumeLayerMask:
serializedVersion: 2
m_Bits: 1
m_VolumeTrigger: {fileID: 0}
m_VolumeFrameworkUpdateModeOption: 2
m_RenderPostProcessing: 0
m_Antialiasing: 0
m_AntialiasingQuality: 2
m_StopNaN: 0
m_Dithering: 0
m_ClearDepth: 1
m_AllowXRRendering: 1
m_AllowHDROutput: 1
m_UseScreenCoordOverride: 0
m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0}
m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
m_RequiresDepthTexture: 0
m_RequiresColorTexture: 0
m_Version: 2
m_TaaSettings:
m_Quality: 3
m_FrameInfluence: 0.1
m_JitterScale: 1
m_MipBias: 0
m_VarianceClampScale: 0.9
m_ContrastAdaptiveSharpening: 0
--- !u!1 &209223422
GameObject:
m_ObjectHideFlags: 0
@@ -7864,6 +7909,18 @@ PrefabInstance:
propertyPath: m_IsActive
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3465182817620600145, guid: 169bd8967120a614da3d7b0e9c71557c, type: 3}
propertyPath: m_AnchorMax.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 3465182817620600145, guid: 169bd8967120a614da3d7b0e9c71557c, type: 3}
propertyPath: m_AnchorMin.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 3465182817620600145, guid: 169bd8967120a614da3d7b0e9c71557c, type: 3}
propertyPath: m_AnchoredPosition.y
value: 714.47
objectReference: {fileID: 0}
- target: {fileID: 3749771552368919126, guid: 169bd8967120a614da3d7b0e9c71557c, type: 3}
propertyPath: m_Sprite
value:
@@ -7968,6 +8025,18 @@ PrefabInstance:
propertyPath: m_IsActive
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8202105634251418107, guid: 169bd8967120a614da3d7b0e9c71557c, type: 3}
propertyPath: m_AnchorMax.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 8202105634251418107, guid: 169bd8967120a614da3d7b0e9c71557c, type: 3}
propertyPath: m_AnchorMin.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 8202105634251418107, guid: 169bd8967120a614da3d7b0e9c71557c, type: 3}
propertyPath: m_AnchoredPosition.y
value: 714.47
objectReference: {fileID: 0}
- target: {fileID: 8269240558470408597, guid: 169bd8967120a614da3d7b0e9c71557c, type: 3}
propertyPath: m_Sprite
value:
+1182 -887
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -846,6 +846,10 @@ public class GameManager : NetworkBehaviour
StartCoroutine(CoLeaveIfThirdPlayer());
}
public void Leave(){
LevelLoadManager.instance.Leave();
}
IEnumerator CoLeaveIfThirdPlayer()
{
const int maxFrames = 30;
+1 -1
View File
@@ -14,7 +14,7 @@ using UnityEngine.UI;
public class LoginManager : MonoBehaviour
{
public const string AuthBaseUrl = "https://kickkingsapi.playpoolstudios.com";
public const string AuthBaseUrl = "https://kkapi.playpoolstudios.com";
const string PlayerPrefsAuthKey = "WalkInvest.AuthToken";
const int MIN_USERNAME_LENGTH = 3;
+25 -4
View File
@@ -162,7 +162,10 @@ public class NetManager : NetworkManager
{
base.OnClientDisconnect();
Logger.Log("Client disconnected");
if (ShouldReconnect())
// Mirror raises OnDisconnectedEvent before clearing ConnectState, so
// NetworkClient.isConnected is still true here on involuntary drops.
// Gate on policy flags only; teardown wait happens in the reconnect loop.
if (CanAttemptReconnect())
{
if (reconnectCoroutine == null)
reconnectCoroutine = StartCoroutine(ReconnectLoopCoroutine());
@@ -206,6 +209,12 @@ public class NetManager : NetworkManager
SetReconnectCanvasVisible(true);
reopenRequestedThisDisconnect = false;
reopenRequestInFlight = false;
// Wait until Mirror finishes OnClientDisconnectInternal (clears Connected
// then Shutdown). Without this, ShouldReconnect() fails immediately.
while (NetworkClient.isConnected || NetworkClient.active)
yield return null;
float reconnectStartedAt = Time.realtimeSinceStartup;
float initialWaitEnd = Time.realtimeSinceStartup + InitialReconnectDelaySeconds;
while (ShouldReconnect() && Time.realtimeSinceStartup < initialWaitEnd)
@@ -242,20 +251,32 @@ public class NetManager : NetworkManager
SetReconnectCanvasVisible(false);
}
bool ShouldReconnect()
/// <summary>
/// Policy checks that are safe during Mirror's disconnect callback
/// (when <see cref="NetworkClient.isConnected"/> may still be true).
/// </summary>
bool CanAttemptReconnect()
{
if (s_IntentionalClientDisconnect)
return false;
if (NetworkServer.active)
return false;
if (NetworkClient.isConnected)
return false;
if (GameManager.instance != null && GameManager.instance.isGameEnded)
return false;
return true;
}
bool ShouldReconnect()
{
if (!CanAttemptReconnect())
return false;
if (NetworkClient.isConnected)
return false;
return true;
}
IEnumerator CoRequestReopenForCurrentPort()
{
reopenRequestInFlight = true;
@@ -32,6 +32,7 @@ public class TutorialManagerMainMenu : MonoBehaviour
[Header("Screen 6")]
public GameObject screen6;
public Button btnNext6;
public CardCarousel gameModeCarousel;
[Header("Screen 7")]
public GameObject screen7;
@@ -85,6 +86,8 @@ public class TutorialManagerMainMenu : MonoBehaviour
void OnNext6(){
screen6.SetActive(false);
screen7.SetActive(true);
gameModeCarousel.SetSelectedIndex(1);
curScreen = 7;
}
+456
View File
@@ -0,0 +1,456 @@
using System;
using System.Collections.Generic;
using DG.Tweening;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
[RequireComponent(typeof(EventTrigger))]
public class CardCarousel : MonoBehaviour
{
[SerializeField] RectTransform[] items;
[SerializeField] float spacing = 40f;
[SerializeField] float swipeThreshold = 60f;
[SerializeField] float snapDuration = 0.25f;
[SerializeField] Ease snapEase = Ease.OutCubic;
[Tooltip("Horizontal half-width of the center tap zone, in local units of this RectTransform.")]
[SerializeField] float centerTapHalfWidth = 260f;
[SerializeField] bool looped = true;
[SerializeField] int selectedIndex;
[Header("Focus Animation")]
[SerializeField] float activeScale = 1.05f;
[SerializeField] float inactiveScale = 0.88f;
[SerializeField] float activePullDown = 35f;
public UnityEvent<int> onSelected = new UnityEvent<int>();
EventTrigger eventTrigger;
RectTransform rectTransform;
RectTransform dragSpace;
RectTransform duplicateClone;
readonly Dictionary<RectTransform, Vector3> baseScales = new();
float slotStep;
float focusX;
float focusY;
float focusPosition;
float dragStartPosition;
float pointerDownLocalX;
bool isDragging;
bool isAnimating;
Tween snapTween;
public int SelectedIndex => selectedIndex;
void Awake()
{
rectTransform = (RectTransform)transform;
eventTrigger = GetComponent<EventTrigger>();
RegisterTrigger(EventTriggerType.PointerDown, OnPointerDown);
RegisterTrigger(EventTriggerType.Drag, OnDrag);
RegisterTrigger(EventTriggerType.PointerUp, OnPointerUp);
}
void Start()
{
if (items == null || items.Length < 2)
{
Debug.LogWarning($"{nameof(CardCarousel)} on {name} needs at least 2 items.", this);
return;
}
selectedIndex = Mathf.Clamp(selectedIndex, 0, items.Length - 1);
dragSpace = items[0].parent as RectTransform;
slotStep = items[0].rect.width + spacing;
focusX = items[0].anchoredPosition.x;
focusY = items[0].anchoredPosition.y;
duplicateClone = CreateClone(items[0], "_carouselClone_Dup");
foreach (var item in items)
CacheBaseScale(item);
focusPosition = selectedIndex;
ApplyLayout();
}
void OnDestroy()
{
snapTween?.Kill();
}
void RegisterTrigger(EventTriggerType type, Action<BaseEventData> handler)
{
var entry = new EventTrigger.Entry { eventID = type };
entry.callback.AddListener(handler.Invoke);
eventTrigger.triggers.Add(entry);
}
RectTransform CreateClone(RectTransform source, string suffix)
{
var clone = Instantiate(source, source.parent);
clone.name = source.name + suffix;
DisableInteractions(clone);
return clone;
}
static void DisableInteractions(RectTransform target)
{
foreach (var graphic in target.GetComponentsInChildren<Graphic>(true))
graphic.raycastTarget = false;
foreach (var button in target.GetComponentsInChildren<Button>(true))
button.interactable = false;
}
void OnPointerDown(BaseEventData eventData)
{
if (!TryGetPointer(eventData, out var pointer) || isAnimating || items == null || items.Length < 2)
return;
snapTween?.Kill();
isAnimating = false;
isDragging = false;
dragStartPosition = focusPosition;
pointerDownLocalX = GetLocalDragX(pointer);
}
void OnDrag(BaseEventData eventData)
{
if (!TryGetPointer(eventData, out var pointer) || items == null || items.Length < 2)
return;
isDragging = true;
float delta = GetLocalDragX(pointer) - pointerDownLocalX;
focusPosition = dragStartPosition - delta / slotStep;
if (!looped)
focusPosition = Mathf.Clamp(focusPosition, 0f, items.Length - 1);
ApplyLayout();
}
void OnPointerUp(BaseEventData eventData)
{
if (!TryGetPointer(eventData, out var pointer) || isAnimating || items == null || items.Length < 2)
return;
float pointerDelta = GetLocalDragX(pointer) - pointerDownLocalX;
float dragDelta = focusPosition - dragStartPosition;
bool passedSwipeThreshold = Mathf.Abs(pointerDelta) >= swipeThreshold
|| Mathf.Abs(dragDelta * slotStep) >= swipeThreshold;
if (!passedSwipeThreshold)
{
if (TryGetTapZone(pointer, out int tapDirection))
{
if (tapDirection == 0)
SelectCurrent();
else if (CanMoveBy(tapDirection))
AnimateToDirection(tapDirection);
else
SnapTo(selectedIndex, null);
}
else
SnapTo(selectedIndex, null);
isDragging = false;
return;
}
isDragging = false;
int direction = dragDelta > 0f ? 1 : -1;
if (Mathf.Abs(focusPosition - selectedIndex) > 0.5f)
{
int nearest = Mathf.RoundToInt(focusPosition);
if (!looped)
nearest = Mathf.Clamp(nearest, 0, items.Length - 1);
if (nearest != selectedIndex && CanMoveBy(nearest > selectedIndex ? 1 : -1))
{
AnimateTo(nearest);
return;
}
}
if (!CanMoveBy(direction))
{
SnapTo(selectedIndex, null);
return;
}
AnimateToDirection(direction);
}
float GetLocalDragX(PointerEventData pointer)
{
if (dragSpace == null)
return pointer.position.x;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
dragSpace, pointer.position, pointer.pressEventCamera, out var local);
return local.x;
}
bool TryGetTapZone(PointerEventData pointer, out int direction)
{
direction = 0;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
rectTransform, pointer.position, pointer.pressEventCamera, out var local))
return false;
if (Mathf.Abs(local.x) <= centerTapHalfWidth)
return true;
direction = local.x < 0f ? -1 : 1;
return true;
}
void SelectCurrent()
{
if (Mathf.Approximately(focusPosition, selectedIndex))
onSelected.Invoke(selectedIndex);
else
SnapTo(selectedIndex, () => onSelected.Invoke(selectedIndex));
}
void AnimateToDirection(int direction)
{
int newIndex = GetAdjacentIndex(direction, items.Length);
if (newIndex < 0)
return;
AnimateTo(newIndex);
}
void AnimateTo(int newIndex)
{
isAnimating = true;
snapTween?.Kill();
snapTween = DOTween.To(
() => focusPosition,
value =>
{
focusPosition = value;
ApplyLayout();
},
newIndex,
snapDuration)
.SetEase(snapEase)
.OnComplete(() => CompleteSlide(newIndex));
}
void CompleteSlide(int newIndex)
{
selectedIndex = newIndex;
focusPosition = newIndex;
isAnimating = false;
ApplyLayout();
}
void SnapTo(float targetPosition, Action onComplete)
{
isAnimating = true;
snapTween?.Kill();
snapTween = DOTween.To(
() => focusPosition,
value =>
{
focusPosition = value;
ApplyLayout();
},
targetPosition,
snapDuration)
.SetEase(snapEase)
.OnComplete(() =>
{
isAnimating = false;
onComplete?.Invoke();
});
}
float GetItemOffset(int itemIndex)
{
return (itemIndex - focusPosition) * slotStep;
}
bool IsItemVisible(float offset)
{
return Mathf.Abs(offset) <= slotStep * 1.5f;
}
void ApplyLayout()
{
int count = items.Length;
bool useDuplicate = looped && count == 2;
for (int i = 0; i < count; i++)
{
float offset = GetItemOffset(i);
if (IsItemVisible(offset))
{
SetItemTransform(items[i], offset);
items[i].gameObject.SetActive(true);
}
else
items[i].gameObject.SetActive(false);
}
if (useDuplicate && Mathf.Approximately(focusPosition, Mathf.Round(focusPosition)))
{
int restIndex = Mathf.RoundToInt(focusPosition);
int otherIndex = 1 - restIndex;
float otherOffset = GetItemOffset(otherIndex);
float duplicateOffset = -otherOffset;
if (IsItemVisible(duplicateOffset))
{
SyncDuplicateClone(items[otherIndex]);
SetItemTransform(duplicateClone, duplicateOffset);
duplicateClone.gameObject.SetActive(true);
}
else if (duplicateClone != null)
duplicateClone.gameObject.SetActive(false);
}
else if (duplicateClone != null)
{
duplicateClone.gameObject.SetActive(false);
}
int topIndex = 0;
float bestFocus = float.MaxValue;
for (int i = 0; i < count; i++)
{
if (!items[i].gameObject.activeSelf)
continue;
float focus = GetFocusT(GetItemOffset(i));
if (focus < bestFocus)
{
bestFocus = focus;
topIndex = i;
}
}
items[topIndex].SetAsLastSibling();
}
void SyncDuplicateClone(RectTransform source)
{
string expectedName = source.name + "_carouselClone_Dup";
if (duplicateClone == null || duplicateClone.name != expectedName)
{
var parent = source.parent;
int sibling = duplicateClone != null ? duplicateClone.GetSiblingIndex() : source.GetSiblingIndex();
if (duplicateClone != null)
{
baseScales.Remove(duplicateClone);
Destroy(duplicateClone.gameObject);
}
duplicateClone = CreateClone(source, "_carouselClone_Dup");
duplicateClone.SetParent(parent, false);
duplicateClone.SetSiblingIndex(sibling);
}
CacheBaseScale(duplicateClone, source);
}
void CacheBaseScale(RectTransform item, RectTransform source = null)
{
if (baseScales.ContainsKey(item))
return;
baseScales[item] = source != null ? source.localScale : item.localScale;
}
void SetItemTransform(RectTransform item, float xOffset)
{
CacheBaseScale(item);
float focusT = GetFocusT(xOffset);
float scale = Mathf.Lerp(activeScale, inactiveScale, focusT);
item.localScale = baseScales[item] * scale;
var pos = item.anchoredPosition;
pos.x = focusX + xOffset;
pos.y = focusY - Mathf.Lerp(activePullDown, 0f, focusT);
item.anchoredPosition = pos;
}
float GetFocusT(float xOffset)
{
if (slotStep <= 0f)
return 0f;
float t = Mathf.Clamp01(Mathf.Abs(xOffset) / slotStep);
return t * t * (3f - 2f * t);
}
static int WrapIndex(int index, int count)
{
if (count <= 0)
return 0;
return ((index % count) + count) % count;
}
int GetAdjacentIndex(int direction, int count)
{
int index = selectedIndex + direction;
if (looped)
return WrapIndex(index, count);
return index >= 0 && index < count ? index : -1;
}
bool CanMoveBy(int direction)
{
if (items == null || items.Length == 0)
return false;
return GetAdjacentIndex(direction, items.Length) >= 0;
}
int NormalizeIndex(int index)
{
if (items == null || items.Length == 0)
return 0;
return looped ? WrapIndex(index, items.Length) : Mathf.Clamp(index, 0, items.Length - 1);
}
static bool TryGetPointer(BaseEventData eventData, out PointerEventData pointer)
{
pointer = eventData as PointerEventData;
return pointer != null;
}
public void SetSelectedIndex(int index, bool animate = false)
{
if (items == null || items.Length == 0)
return;
int target = NormalizeIndex(index);
if (target == selectedIndex)
return;
if (!animate || !isActiveAndEnabled)
{
snapTween?.Kill();
isAnimating = false;
selectedIndex = target;
focusPosition = target;
ApplyLayout();
return;
}
int direction = target > selectedIndex ? 1 : -1;
if (!CanMoveBy(direction) || Mathf.Abs(target - selectedIndex) != 1)
{
selectedIndex = target;
focusPosition = target;
ApplyLayout();
return;
}
AnimateToDirection(direction);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a4951ec6b41c5d14cb8f5a8803dfa0c2