diff --git a/Assets/Scenes/Demo.unity b/Assets/Scenes/Demo.unity
index eed7979..9ad132c 100644
--- a/Assets/Scenes/Demo.unity
+++ b/Assets/Scenes/Demo.unity
@@ -22160,6 +22160,50 @@ Transform:
m_CorrespondingSourceObject: {fileID: 4270791331071568, guid: f0bfb58546547264682d6066f7947e72, type: 3}
m_PrefabInstance: {fileID: 71862592}
m_PrefabAsset: {fileID: 0}
+--- !u!1 &292487261
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 292487263}
+ - component: {fileID: 292487262}
+ m_Layer: 0
+ m_Name: SupabaseTestInsert
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &292487262
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 292487261}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 619706b2e2fd4fe4eb36567a686c7da0, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+--- !u!4 &292487263
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 292487261}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: -0.0981338, y: 2.3009996, z: 3.8460662}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!4 &292591182 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 4940683387464080, guid: 1941338505201ed4b86d0fc32a45bdcc, type: 3}
@@ -154096,6 +154140,65 @@ Transform:
m_CorrespondingSourceObject: {fileID: 4829453017703304, guid: 07751b6a4092629438c147af65e877b3, type: 3}
m_PrefabInstance: {fileID: 1574431602}
m_PrefabAsset: {fileID: 0}
+--- !u!1 &2143540272
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2143540275}
+ - component: {fileID: 2143540274}
+ - component: {fileID: 2143540273}
+ m_Layer: 0
+ m_Name: SupabaseManager
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &2143540273
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2143540272}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 50b27839ecef84443a10112beb860a7a, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+--- !u!114 &2143540274
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2143540272}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: ef01b27a4a6f3724a981d178a963c567, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
+ supabaseUrl: https://vihjspljbslozbjzxutl.supabase.co
+ supabaseKey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZpaGpzcGxqYnNsb3pianp4dXRsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDk1NDc4OTMsImV4cCI6MjA2NTEyMzg5M30.IYPvShgu5j3NnE5PHn-aFLCBJl1QQaVQvAjzxFt8tlA
+--- !u!4 &2143540275
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2143540272}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: -0.0981338, y: 2.3009996, z: 3.8460662}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1001 &2143858061
PrefabInstance:
m_ObjectHideFlags: 0
@@ -160336,3 +160439,5 @@ SceneRoots:
- {fileID: 401603027}
- {fileID: 72663021}
- {fileID: 1568526664}
+ - {fileID: 2143540275}
+ - {fileID: 292487263}
diff --git a/Assets/Scripts/BodyLinkHandler.cs b/Assets/Scripts/BodyLinkHandler.cs
index bd1bbfd..4291344 100644
--- a/Assets/Scripts/BodyLinkHandler.cs
+++ b/Assets/Scripts/BodyLinkHandler.cs
@@ -53,6 +53,8 @@ public class BodyLinkHandler : MonoBehaviour, IPointerClickHandler, IPointerExit
string arabicLog = $"تم الضغط على الرابط '{linkID}' في البريد من '{email.senderName}'";
UserActionLogger.Instance?.Log(englishLog, arabicLog);
+ bool isOptimal = !email.isPhishing;
+ SupabaseEventLogger.Instance?.LogDecisionEvent(isOptimal);
}
else
diff --git a/Assets/Scripts/CharacterMovement.cs b/Assets/Scripts/CharacterMovement.cs
index fd20c8f..e5afed2 100644
--- a/Assets/Scripts/CharacterMovement.cs
+++ b/Assets/Scripts/CharacterMovement.cs
@@ -54,6 +54,7 @@ public class CharacterMovement : MonoBehaviour
{
animator.SetTrigger("StartWalking");
isStarted = true;
+ SupabaseEventLogger.Instance?.StartSession();
InstructionManager.Instance?.ShowScreenInstruction("mission_intro");
}
void Update()
diff --git a/Assets/Scripts/EmailOpenPanel.cs b/Assets/Scripts/EmailOpenPanel.cs
index f372000..6b9b4aa 100644
--- a/Assets/Scripts/EmailOpenPanel.cs
+++ b/Assets/Scripts/EmailOpenPanel.cs
@@ -129,6 +129,14 @@ public class EmailOpenPanel : MonoBehaviour
SceneOutcomeManager.Instance.Ignored(emailData);
break;
}
+
+ SupabaseEventLogger.Instance?.CompleteSessionAndSubmitResult(
+ userId: "user123", // replace with real user ID if available
+ passed: isCorrect,
+ optimal: isCorrect ? 1 : 0,
+ suboptimal: isCorrect ? 0 : 1,
+ scenarioId: "scene_1"
+);
}
void LocalizeTMP(TextMeshProUGUI tmp, string english, string arabic)
diff --git a/Assets/Scripts/MiniQuizManager.cs b/Assets/Scripts/MiniQuizManager.cs
index 3c6f333..46a280f 100644
--- a/Assets/Scripts/MiniQuizManager.cs
+++ b/Assets/Scripts/MiniQuizManager.cs
@@ -61,6 +61,9 @@ public class MiniQuizManager : MonoBehaviour
// Prepend a ✅ to the selected label
answerLabels[selectedIndex].text = "✅ " + answerLabels[selectedIndex].text;
+
+ bool isCorrect = (selectedIndex == correctIndex);
+ SupabaseEventLogger.Instance?.LogDecisionEvent(isCorrect);
}
}
diff --git a/Assets/SupabaseEventLogger.cs b/Assets/SupabaseEventLogger.cs
new file mode 100644
index 0000000..02b4d97
--- /dev/null
+++ b/Assets/SupabaseEventLogger.cs
@@ -0,0 +1,180 @@
+using UnityEngine;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Supabase; // Make sure SupabaseManager initializes this correctly
+using Postgrest.Models;
+using Postgrest.Attributes;
+
+public class SupabaseEventLogger : MonoBehaviour
+{
+ public static SupabaseEventLogger Instance;
+
+ private DateTime sessionStartTime;
+
+ private void Awake()
+ {
+ if (Instance == null)
+ Instance = this;
+ else
+ Destroy(gameObject);
+ }
+
+ ///
+ /// Call this at the start of the game session.
+ ///
+ public async void StartSession()
+ {
+ sessionStartTime = DateTime.UtcNow;
+
+ var gameEvent = new GameEvent
+ {
+ Id = Guid.NewGuid(), // <== Ensure this is explicitly set
+ EventKey = "game_session_started",
+ Timestamp = sessionStartTime,
+ UserId = "user123"
+ };
+ await Client.Instance.From().Insert(gameEvent);
+ Debug.Log("✅ Supabase Event: game_session_started");
+ }
+
+ ///
+ /// Logs optimal/suboptimal decisions at runtime.
+ ///
+ public async void LogDecisionEvent(bool isOptimal)
+ {
+ string eventKey = isOptimal ? "game_optimal_decision_made" : "game_suboptimal_decision_made";
+
+ var gameEvent = new GameEvent
+ {
+ Id = Guid.NewGuid(),
+ EventKey = eventKey,
+ Timestamp = DateTime.UtcNow,
+ UserId = "user123"
+ };
+
+ await Client.Instance.From().Insert(gameEvent);
+ Debug.Log($"✅ Supabase Event: {eventKey}");
+ }
+
+ ///
+ /// Completes the session and submits full results to phishing_game_attempts table.
+ ///
+ public async void CompleteSessionAndSubmitResult(string userId, bool passed, int optimal, int suboptimal, string scenarioId, List decisionLog = null)
+ {
+ var endTime = DateTime.UtcNow;
+ int duration = (int)(endTime - sessionStartTime).TotalSeconds;
+
+ // Log completion events
+ await Client.Instance.From().Insert(new GameEvent
+ {
+ Id = Guid.NewGuid(),
+ EventKey = "game_session_completed",
+ Timestamp = endTime,
+ UserId = userId
+ });
+
+ await Client.Instance.From().Insert(new GameEvent
+ {Id = Guid.NewGuid(),
+ EventKey = "game_score_recorded",
+ Timestamp = endTime,
+ UserId = userId
+ });
+
+ // Insert session result
+ var gameAttempt = new GameAttempt
+ {
+ GameId = "phishing-awareness-1",
+ ScenarioId = scenarioId,
+ UserId = userId,
+ AttemptNumber = 1,
+ StartTime = sessionStartTime,
+ EndTime = endTime,
+ DurationSeconds = duration,
+ Score = passed ? 100 : 50,
+ Passed = passed,
+ Optimal = optimal,
+ Suboptimal = suboptimal,
+ KeyDecisionsLogJson = decisionLog != null ? JsonUtility.ToJson(new DecisionLogWrapper { decisions = decisionLog }) : "[]"
+ };
+
+ await Client.Instance.From().Insert(gameAttempt);
+ Debug.Log("✅ Supabase Game Result Submitted");
+ }
+
+ [Serializable]
+ public class Decision
+ {
+ public string decisionId;
+ public string timestamp;
+ public bool optimal;
+ }
+
+ [Serializable]
+ public class DecisionLogWrapper
+ {
+ public List decisions;
+ }
+}
+
+// Existing SupabaseEventLogger class here...
+// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+
+[Table("game_events")]
+public class GameEvent : BaseModel
+{
+ [PrimaryKey("id", false)]
+ public Guid Id { get; set; }
+
+ [Column("event_key")]
+ public string EventKey { get; set; }
+
+ [Column("timestamp")]
+ public DateTime Timestamp { get; set; }
+
+ [Column("user_id")]
+ public string UserId { get; set; }
+}
+
+[Table("phishing_game_attempts")]
+public class GameAttempt : BaseModel
+{
+ [PrimaryKey("id", false)]
+ public Guid Id { get; set; } = Guid.NewGuid();
+
+ [Column("game_id")]
+ public string GameId { get; set; }
+
+ [Column("scenario_id")]
+ public string ScenarioId { get; set; }
+
+ [Column("user_id")]
+ public string UserId { get; set; }
+
+ [Column("attempt_number")]
+ public int AttemptNumber { get; set; }
+
+ [Column("start_timestamp")]
+ public DateTime StartTime { get; set; }
+
+ [Column("end_timestamp")]
+ public DateTime EndTime { get; set; }
+
+ [Column("duration_seconds")]
+ public int DurationSeconds { get; set; }
+
+ [Column("final_score_percentage")]
+ public float Score { get; set; }
+
+ [Column("pass_fail_status")]
+ public bool Passed { get; set; }
+
+ [Column("optimal_decisions_count")]
+ public int Optimal { get; set; }
+
+ [Column("suboptimal_decisions_count")]
+ public int Suboptimal { get; set; }
+
+ [Column("key_decisions_log")]
+ public string KeyDecisionsLogJson { get; set; }
+}
diff --git a/Assets/SupabaseEventLogger.cs.meta b/Assets/SupabaseEventLogger.cs.meta
new file mode 100644
index 0000000..f07d482
--- /dev/null
+++ b/Assets/SupabaseEventLogger.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 50b27839ecef84443a10112beb860a7a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/SupabaseManager.cs b/Assets/SupabaseManager.cs
new file mode 100644
index 0000000..05593cc
--- /dev/null
+++ b/Assets/SupabaseManager.cs
@@ -0,0 +1,31 @@
+using Supabase;
+using UnityEngine;
+using System.Threading.Tasks;
+
+public class SupabaseManager : MonoBehaviour
+{
+ public static Supabase.Client Client => Supabase.Client.Instance;
+
+ [Header("Supabase Settings")]
+ public string supabaseUrl = "https://vihjspljbslozbjzxutl.supabase.co";
+ public string supabaseKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZpaGpzcGxqYnNsb3pianp4dXRsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDk1NDc4OTMsImV4cCI6MjA2NTEyMzg5M30.IYPvShgu5j3NnE5PHn-aFLCBJl1QQaVQvAjzxFt8tlA";
+
+ private async void Awake()
+ {
+ if (Supabase.Client.Instance != null)
+ {
+ Debug.Log("✅ Supabase already initialized.");
+ return;
+ }
+
+ var options = new SupabaseOptions
+ {
+ AutoConnectRealtime = false,
+ ShouldInitializeRealtime = false
+ };
+
+ await Supabase.Client.InitializeAsync(supabaseUrl, supabaseKey, options);
+
+ Debug.Log("✅ Supabase Initialized via static method");
+ }
+}
diff --git a/Assets/SupabaseManager.cs.meta b/Assets/SupabaseManager.cs.meta
new file mode 100644
index 0000000..63ed0f4
--- /dev/null
+++ b/Assets/SupabaseManager.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ef01b27a4a6f3724a981d178a963c567
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/SupabaseTestInsert.cs b/Assets/SupabaseTestInsert.cs
new file mode 100644
index 0000000..55ee95a
--- /dev/null
+++ b/Assets/SupabaseTestInsert.cs
@@ -0,0 +1,39 @@
+using UnityEngine;
+using Supabase;
+using System;
+using Postgrest.Models;
+using Postgrest.Attributes;
+
+public class SupabaseTestInsert : MonoBehaviour
+{
+ async void Start()
+ {
+ var gameEvent = new GameEvent
+ {
+ Id = Guid.NewGuid(),
+ EventKey = "manual_test_event",
+ Timestamp = DateTime.UtcNow,
+ UserId = "test_user_1"
+ };
+
+ Debug.Log("🔍 Trying test insert...");
+ await Client.Instance.From().Insert(gameEvent);
+ Debug.Log("✅ Test insert complete");
+ }
+
+ [Table("game_events")]
+ public class GameEvent : BaseModel
+ {
+ [PrimaryKey("id", false)]
+ public Guid Id { get; set; }
+
+ [Column("event_key")]
+ public string EventKey { get; set; }
+
+ [Column("timestamp")]
+ public DateTime Timestamp { get; set; }
+
+ [Column("user_id")]
+ public string UserId { get; set; }
+ }
+}
diff --git a/Assets/SupabaseTestInsert.cs.meta b/Assets/SupabaseTestInsert.cs.meta
new file mode 100644
index 0000000..fc0f79d
--- /dev/null
+++ b/Assets/SupabaseTestInsert.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 619706b2e2fd4fe4eb36567a686c7da0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: