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: