Commit d4ba32ba authored by Yousef Sameh's avatar Yousef Sameh

New QuestionBank Api and schema

parent ada9ac1c
...@@ -38,33 +38,38 @@ public class AppRouter : MonoBehaviour ...@@ -38,33 +38,38 @@ public class AppRouter : MonoBehaviour
private async UniTask Boot() private async UniTask Boot()
{ {
// 1. Init Supabase // 1. Try cache first — instant
// var cached = UserService.Instance.LoadFromCache();
// 2. Init Supabase
bool ready = await SupabaseManager.Instance.Initialize(); bool ready = await SupabaseManager.Instance.Initialize();
if (!ready) if (!ready)
{ {
ShowError("Failed to connect to server"); // Offline but have cache — go to home
// if (cached != null) { GoToHome(); return; }
ShowError("Failed to connect");
return; return;
} }
// 2. Listen for unexpected sign-outs
SupabaseManager.Instance.AddAuthStateListener(OnAuthStateChanged);
// 3. Ensure session // 3. Ensure session
var authResult = await SupabaseAuthentication.Instance.EnsureSession(); var authResult = await SupabaseAuthentication.Instance.EnsureSession();
if (authResult.IsT1) if (authResult.IsT1)
{ {
// if (cached != null) { GoToHome(); return; }
GoToLogin(); GoToLogin();
return; return;
} }
// 4. Try load profile // 4. Refresh from network
var profileResult = await UserService.Instance.GetCurrentUser(); var profileResult = await UserService.Instance.GetCurrentUser();
profileResult.Switch( profileResult.Switch(
success => GoToHome(), success => GoToHome(),
error => GoToLogin() error =>
{
// if (cached != null) GoToHome(); // stale cache, still usable
GoToLogin();
}
); );
_booted = true;
} }
// ─── Auth State Listener (Safety Net Only) ─────────────────────── // ─── Auth State Listener (Safety Net Only) ───────────────────────
...@@ -101,6 +106,7 @@ public class AppRouter : MonoBehaviour ...@@ -101,6 +106,7 @@ public class AppRouter : MonoBehaviour
{ {
if (SceneManager.GetActiveScene().name != "MainMenu") if (SceneManager.GetActiveScene().name != "MainMenu")
await SceneManager.LoadSceneAsync("MainMenu"); await SceneManager.LoadSceneAsync("MainMenu");
UserService.Instance.StartListening();
HideSplash(); HideSplash();
} }
......
using System; using System;
using System.Threading;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
using OneOf; using OneOf;
using Supabase.Realtime;
using Supabase.Realtime.PostgresChanges;
using UnityEngine; using UnityEngine;
public class UserService public class UserService
...@@ -8,11 +13,98 @@ public class UserService ...@@ -8,11 +13,98 @@ public class UserService
private static UserService _instance; private static UserService _instance;
public static UserService Instance => _instance ??= new UserService(); public static UserService Instance => _instance ??= new UserService();
private const string CACHE_KEY = "cached_user_profile";
public User CurrentUser { get; private set; } public User CurrentUser { get; private set; }
public bool HasProfile => CurrentUser != null; public bool HasProfile => CurrentUser != null;
public bool IsFromCache { get; private set; }
public event Action<User> OnUserChanged; public event Action<User> OnUserChanged;
public event Action<string, string> OnRankChanged; // oldRank, newRank
public event Action<int, int> OnPointsChanged; // oldPoints, newPoints
private RealtimeChannel _channel;
// ─── Realtime ────────────────────────────────────────────────────
public async Task StartListening()
{
var client = SupabaseManager.Instance.Supabase();
var authUser = client?.Auth.CurrentUser;
if (authUser == null) return;
if (_channel != null) return;
private UserService() { } await client.Realtime.ConnectAsync();
Debug.Log("Current UserID" + authUser.Id);
_channel = client.Realtime.Channel("user_profile");
_channel.Register(new PostgresChangesOptions("public", "users", PostgresChangesOptions.ListenType.All, filter: $"id=eq.{authUser.Id}"));
_channel.AddPostgresChangeHandler(
PostgresChangesOptions.ListenType.All,
(sender, change) =>
{
// Run on main thread to avoid Unity threading issues
UniTask.Post(() =>
{
var newUser = change.Model<User>();
CurrentUser = newUser;
IsFromCache = false;
OnUserChanged?.Invoke(CurrentUser);
SaveToCache(CurrentUser);
});
}
);
await _channel.Subscribe();
}
public void StopListening()
{
if (_channel == null) return;
_channel.Unsubscribe();
_channel = null;
}
// ─── Cache ───────────────────────────────────────────────────────
private void SaveToCache(User user)
{
if (user == null)
{
PlayerPrefs.DeleteKey(CACHE_KEY);
PlayerPrefs.Save();
return;
}
var json = JsonConvert.SerializeObject(user);
PlayerPrefs.SetString(CACHE_KEY, json);
PlayerPrefs.Save();
}
public User LoadFromCache()
{
var json = PlayerPrefs.GetString(CACHE_KEY, null);
if (string.IsNullOrEmpty(json)) return null;
try
{
var user = JsonConvert.DeserializeObject<User>(json);
CurrentUser = user;
IsFromCache = true;
OnUserChanged?.Invoke(CurrentUser);
return user;
}
catch
{
PlayerPrefs.DeleteKey(CACHE_KEY);
return null;
}
}
// ─── Network ─────────────────────────────────────────────────────
public async UniTask<OneOf<UserResult, ErrorResult>> GetCurrentUser() public async UniTask<OneOf<UserResult, ErrorResult>> GetCurrentUser()
{ {
...@@ -32,7 +124,17 @@ public class UserService ...@@ -32,7 +124,17 @@ public class UserService
if (response?.Models == null || response.Models.Count == 0) if (response?.Models == null || response.Models.Count == 0)
return new ErrorResult("Profile not found"); return new ErrorResult("Profile not found");
var oldUser = CurrentUser;
CurrentUser = response.Models[0]; CurrentUser = response.Models[0];
IsFromCache = false;
SaveToCache(CurrentUser);
if (oldUser != null && oldUser.Rank != CurrentUser.Rank)
OnRankChanged?.Invoke(oldUser.Rank, CurrentUser.Rank);
if (oldUser != null && oldUser.Points != CurrentUser.Points)
OnPointsChanged?.Invoke(oldUser.Points, CurrentUser.Points);
OnUserChanged?.Invoke(CurrentUser); OnUserChanged?.Invoke(CurrentUser);
return new UserResult(CurrentUser); return new UserResult(CurrentUser);
} }
...@@ -45,10 +147,10 @@ public class UserService ...@@ -45,10 +147,10 @@ public class UserService
public async UniTask<OneOf<UserResult, ErrorResult>> CreateProfile( public async UniTask<OneOf<UserResult, ErrorResult>> CreateProfile(
string username, string username,
string school = null,
string grade = null, string grade = null,
string sex = null, string sex = null,
string age = null) string curriculum = null,
string term = null)
{ {
try try
{ {
...@@ -59,21 +161,16 @@ public class UserService ...@@ -59,21 +161,16 @@ public class UserService
var user = new User var user = new User
{ {
Username = username, Username = username,
School = school,
Grade = grade, Grade = grade,
Sex = sex, Sex = sex,
Age = age, Curriculum = curriculum,
Term = term,
Rank = "normal", Rank = "normal",
Points = 0, Points = 0
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
}; };
await client.From<User>().Insert(user); await client.From<User>().Insert(user);
return await GetCurrentUser();
// Re-fetch to get server-set fields (id, timestamps)
var fetchResult = await GetCurrentUser();
return fetchResult;
} }
catch (Exception ex) catch (Exception ex)
{ {
...@@ -84,10 +181,10 @@ public class UserService ...@@ -84,10 +181,10 @@ public class UserService
public async UniTask<OneOf<UserResult, ErrorResult>> UpdateProfile( public async UniTask<OneOf<UserResult, ErrorResult>> UpdateProfile(
string username = null, string username = null,
string school = null,
string grade = null, string grade = null,
string sex = null, string sex = null,
string age = null) string curriculum = null,
string term = null)
{ {
try try
{ {
...@@ -98,17 +195,16 @@ public class UserService ...@@ -98,17 +195,16 @@ public class UserService
var update = new User(); var update = new User();
if (username != null) update.Username = username; if (username != null) update.Username = username;
if (school != null) update.School = school;
if (grade != null) update.Grade = grade; if (grade != null) update.Grade = grade;
if (sex != null) update.Sex = sex; if (sex != null) update.Sex = sex;
if (age != null) update.Age = age; if (curriculum != null) update.Curriculum = curriculum;
if (term != null) update.Term = term;
await client await client
.From<User>() .From<User>()
.Where(x => x.Id == authUser.Id) .Where(x => x.Id == authUser.Id)
.Update(update); .Update(update);
// Re-fetch
return await GetCurrentUser(); return await GetCurrentUser();
} }
catch (Exception ex) catch (Exception ex)
...@@ -118,9 +214,15 @@ public class UserService ...@@ -118,9 +214,15 @@ public class UserService
} }
} }
// ─── Clear ───────────────────────────────────────────────────────
public void ClearUser() public void ClearUser()
{ {
StopListening();
CurrentUser = null; CurrentUser = null;
IsFromCache = false;
PlayerPrefs.DeleteKey(CACHE_KEY);
PlayerPrefs.Save();
OnUserChanged?.Invoke(null); OnUserChanged?.Invoke(null);
} }
} }
\ No newline at end of file
...@@ -2,50 +2,36 @@ using System; ...@@ -2,50 +2,36 @@ using System;
using Supabase.Postgrest.Attributes; using Supabase.Postgrest.Attributes;
using Supabase.Postgrest.Models; using Supabase.Postgrest.Models;
using Newtonsoft.Json;
[Table("users")] [Table("users")]
public class User : BaseModel public class User : BaseModel
{ {
[PrimaryKey("id")] [PrimaryKey("id")]
[JsonProperty("id")]
public string Id { get; set; } public string Id { get; set; }
[Column("user_name")] [Column("user_name")]
[JsonProperty("user_name")]
public string Username { get; set; } public string Username { get; set; }
[Column("rank")] [Column("rank")]
[JsonProperty("rank")]
public string Rank { get; set; } public string Rank { get; set; }
[Column("points")] [Column("points")]
[JsonProperty("points")]
public int Points { get; set; } = 0; public int Points { get; set; } = 0;
[Column("created_at")] [Column("created_at")]
[JsonProperty("created_at")]
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
[Column("updated_at")] [Column("updated_at")]
[JsonProperty("updated_at")]
public DateTime UpdatedAt { get; set; } public DateTime UpdatedAt { get; set; }
[Column("school")]
[JsonProperty("school")]
public string School { get; set; }
[Column("grade")] [Column("grade")]
[JsonProperty("grade")]
public string Grade { get; set; } public string Grade { get; set; }
[Column("sex")] [Column("sex")]
[JsonProperty("sex")]
public string Sex { get; set; } public string Sex { get; set; }
[Column("age")] [Column("curriculum")]
[JsonProperty("age")] public string Curriculum { get; set; }
public string Age { get; set; }
}
[Column("term")]
public string Term { get; set; }
}
\ No newline at end of file
...@@ -47,6 +47,8 @@ public class HomeController : MonoBehaviour ...@@ -47,6 +47,8 @@ public class HomeController : MonoBehaviour
private void OnUserChange(User user) private void OnUserChange(User user)
{ {
print($"[HomeController] Updating user info: {user?.Username}, Rank: {user?.Rank}, Points: {user?.Points}");
username.text = user.Username; username.text = user.Username;
xp.text = user.Points.ToString(); xp.text = user.Points.ToString();
rank.text = AppUtils.RankToArabic(user.Rank); rank.text = AppUtils.RankToArabic(user.Rank);
......
...@@ -14,6 +14,7 @@ public class LeaderboardController : MonoBehaviour ...@@ -14,6 +14,7 @@ public class LeaderboardController : MonoBehaviour
var root = leaderboardDocument.rootVisualElement; var root = leaderboardDocument.rootVisualElement;
leaderboardScrollView = root.Q<ScrollView>("Leaderboard"); leaderboardScrollView = root.Q<ScrollView>("Leaderboard");
UserService.Instance.OnUserChanged += _ => LoadLeaderboard();
LoadLeaderboard().Forget(); LoadLeaderboard().Forget();
} }
......
...@@ -7,9 +7,9 @@ public class LoginController : MonoBehaviour ...@@ -7,9 +7,9 @@ public class LoginController : MonoBehaviour
private TextField username; private TextField username;
private DropdownField grade; private DropdownField grade;
private DropdownField school;
private DropdownField sex; private DropdownField sex;
private TextField age; private DropdownField term;
private DropdownField curriculum;
private Button register; private Button register;
...@@ -18,12 +18,10 @@ public class LoginController : MonoBehaviour ...@@ -18,12 +18,10 @@ public class LoginController : MonoBehaviour
var root = uIDocument.rootVisualElement; var root = uIDocument.rootVisualElement;
username = root.Q<TextField>("Username"); username = root.Q<TextField>("Username");
username.languageDirection = LanguageDirection.RTL;
grade = root.Q<DropdownField>("Grade"); grade = root.Q<DropdownField>("Grade");
school = root.Q<DropdownField>("School");
sex = root.Q<DropdownField>("Sex"); sex = root.Q<DropdownField>("Sex");
age = root.Q<TextField>("Age"); term = root.Q<DropdownField>("Term");
curriculum = root.Q<DropdownField>("Curriculum");
register = root.Q<Button>("Register"); register = root.Q<Button>("Register");
register.clicked += RegisterAnon; register.clicked += RegisterAnon;
...@@ -44,7 +42,7 @@ public class LoginController : MonoBehaviour ...@@ -44,7 +42,7 @@ public class LoginController : MonoBehaviour
return; return;
} }
var signUp = await UserService.Instance.CreateProfile(username.text, grade.value, school.value, sex.value, age.text); var signUp = await UserService.Instance.CreateProfile(username.text, curriculum.value, grade.value, sex.value, term.value);
signUp.Switch(user => signUp.Switch(user =>
{ {
AppRouter.GoToHome(); AppRouter.GoToHome();
......
...@@ -23,4 +23,9 @@ public class ProfileController : MonoBehaviour ...@@ -23,4 +23,9 @@ public class ProfileController : MonoBehaviour
{ {
name.text = user.Username; name.text = user.Username;
} }
private void OnDestroy()
{
UserService.Instance.OnUserChanged -= OnUserChange;
}
} }
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<ui:Label text="👤" name="TextFieldLabel" language-direction="RTL" class="emoji" style="color: rgb(117, 117, 117); margin-bottom: 20px; font-size: 40px; -unity-text-align: middle-right; margin-left: 5px;"/> <ui:Label text="👤" name="TextFieldLabel" language-direction="RTL" class="emoji" style="color: rgb(117, 117, 117); margin-bottom: 20px; font-size: 40px; -unity-text-align: middle-right; margin-left: 5px;"/>
<ui:Label text="اسم المستخدم" name="TextFieldLabel" language-direction="RTL" class="base-text-light" style="color: rgb(117, 117, 117); margin-bottom: 20px; font-size: 35px; -unity-font-definition: url(&quot;project://database/Assets/ALArcade/Hakwaty%20Font/TSHakwaty-DemiBold.otf?fileID=12800000&amp;guid=566b773a07b3d064aa1f4c6ef7b6f6fa&amp;type=3#TSHakwaty-DemiBold&quot;);"/> <ui:Label text="اسم المستخدم" name="TextFieldLabel" language-direction="RTL" class="base-text-light" style="color: rgb(117, 117, 117); margin-bottom: 20px; font-size: 35px; -unity-font-definition: url(&quot;project://database/Assets/ALArcade/Hakwaty%20Font/TSHakwaty-DemiBold.otf?fileID=12800000&amp;guid=566b773a07b3d064aa1f4c6ef7b6f6fa&amp;type=3#TSHakwaty-DemiBold&quot;);"/>
</ui:VisualElement> </ui:VisualElement>
<ui:TextField label="" placeholder-text="" name="Username" value="" language-direction="RTL" class="textField" style="flex-direction: row-reverse; color: rgb(0, 0, 0); -unity-font-definition: url(&quot;project://database/Assets/UI%20Toolkit/UnityThemes/UnityDefaultRuntimeTheme.tss?fileID=2230732570650464555&amp;guid=901fb73b2529c134f9cf372789759383&amp;type=3#NotInter-Regular&quot;); -unity-text-align: upper-right; -unity-text-generator: advanced; justify-content: space-between; align-content: flex-start;"> <ui:TextField label="" placeholder-text="" name="Username" value="" language-direction="RTL" class="textField" style="flex-direction: row-reverse; color: rgb(0, 0, 0); -unity-font-definition: url(&quot;project://database/Assets/UI%20Toolkit/UnityThemes/UnityDefaultRuntimeTheme.tss?fileID=2230732570650464555&amp;guid=901fb73b2529c134f9cf372789759383&amp;type=3#NotInter-Regular&quot;); -unity-text-align: upper-right; -unity-text-generator: advanced; justify-content: space-between; align-content: flex-start; white-space: nowrap; text-overflow: clip;">
<ui:Image source="project://database/Assets/Art/export/mail@3x.png?fileID=2800000&amp;guid=0d76662a81af3a7408ca3c2975f08b8f&amp;type=3#mail@3x" tint-color="rgb(158, 158, 158)" style="margin-left: 25px; width: 51px; display: none;"/> <ui:Image source="project://database/Assets/Art/export/mail@3x.png?fileID=2800000&amp;guid=0d76662a81af3a7408ca3c2975f08b8f&amp;type=3#mail@3x" tint-color="rgb(158, 158, 158)" style="margin-left: 25px; width: 51px; display: none;"/>
</ui:TextField> </ui:TextField>
</ui:VisualElement> </ui:VisualElement>
...@@ -34,8 +34,8 @@ ...@@ -34,8 +34,8 @@
<ui:Label text="النظام التعليمي" name="TextFieldLabel" language-direction="RTL" class="base-text-light" style="color: rgb(117, 117, 117); margin-bottom: 20px; font-size: 35px; -unity-font-definition: url(&quot;project://database/Assets/ALArcade/Hakwaty%20Font/TSHakwaty-DemiBold.otf?fileID=12800000&amp;guid=566b773a07b3d064aa1f4c6ef7b6f6fa&amp;type=3#TSHakwaty-DemiBold&quot;);"/> <ui:Label text="النظام التعليمي" name="TextFieldLabel" language-direction="RTL" class="base-text-light" style="color: rgb(117, 117, 117); margin-bottom: 20px; font-size: 35px; -unity-font-definition: url(&quot;project://database/Assets/ALArcade/Hakwaty%20Font/TSHakwaty-DemiBold.otf?fileID=12800000&amp;guid=566b773a07b3d064aa1f4c6ef7b6f6fa&amp;type=3#TSHakwaty-DemiBold&quot;);"/>
</ui:VisualElement> </ui:VisualElement>
<ui:VisualElement name="Menu" class="padding" style="flex-grow: 0; background-color: rgb(255, 255, 255); height: auto; flex-shrink: 10; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; border-top-left-radius: 50px; border-top-right-radius: 50px; border-bottom-right-radius: 50px; border-bottom-left-radius: 50px; padding-top: 25px; padding-right: 25px; padding-bottom: 25px; padding-left: 25px; justify-content: space-between; border-left-color: rgba(0, 0, 0, 0.25); border-right-color: rgba(0, 0, 0, 0.25); border-top-color: rgba(0, 0, 0, 0.25); border-bottom-color: rgba(0, 0, 0, 0.25); display: flex; margin-bottom: 0;"> <ui:VisualElement name="Menu" class="padding" style="flex-grow: 0; background-color: rgb(255, 255, 255); height: auto; flex-shrink: 10; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; border-top-left-radius: 50px; border-top-right-radius: 50px; border-bottom-right-radius: 50px; border-bottom-left-radius: 50px; padding-top: 25px; padding-right: 25px; padding-bottom: 25px; padding-left: 25px; justify-content: space-between; border-left-color: rgba(0, 0, 0, 0.25); border-right-color: rgba(0, 0, 0, 0.25); border-top-color: rgba(0, 0, 0, 0.25); border-bottom-color: rgba(0, 0, 0, 0.25); display: flex; margin-bottom: 0;">
<ui:Button text="" name="Grade" class="row-btn" style="height: 80px; -unity-text-align: middle-right;"> <ui:Button text="" name="" class="row-btn" style="height: 80px; -unity-text-align: middle-right;">
<ui:DropdownField label="" choices="اختر المدرسة...,مصري عربي,مصري لغات" index="0" language-direction="RTL" name="School" style="margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; position: absolute; flex-grow: 0; font-size: 35px; -unity-font-definition: url(&quot;project://database/Assets/ALArcade/Hakwaty%20Font/TSHakwaty-DemiBold.otf?fileID=12800000&amp;guid=566b773a07b3d064aa1f4c6ef7b6f6fa&amp;type=3#TSHakwaty-DemiBold&quot;); -unity-text-generator: advanced; color: rgb(66, 66, 66); -unity-text-align: upper-right; flex-direction: row;"/> <ui:DropdownField label="" choices="اختر المدرسة...,مصري عربي,مصري لغات" index="0" language-direction="RTL" name="Curriculum" style="margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; position: absolute; flex-grow: 0; font-size: 35px; -unity-font-definition: url(&quot;project://database/Assets/ALArcade/Hakwaty%20Font/TSHakwaty-DemiBold.otf?fileID=12800000&amp;guid=566b773a07b3d064aa1f4c6ef7b6f6fa&amp;type=3#TSHakwaty-DemiBold&quot;); -unity-text-generator: advanced; color: rgb(66, 66, 66); -unity-text-align: upper-right; flex-direction: row;"/>
</ui:Button> </ui:Button>
</ui:VisualElement> </ui:VisualElement>
</ui:VisualElement> </ui:VisualElement>
...@@ -59,7 +59,7 @@ ...@@ -59,7 +59,7 @@
</ui:VisualElement> </ui:VisualElement>
<ui:VisualElement name="Menu" class="padding" style="flex-grow: 0; background-color: rgb(255, 255, 255); height: auto; flex-shrink: 10; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; border-top-left-radius: 50px; border-top-right-radius: 50px; border-bottom-right-radius: 50px; border-bottom-left-radius: 50px; padding-top: 25px; padding-right: 25px; padding-bottom: 25px; padding-left: 25px; justify-content: space-between; border-left-color: rgba(0, 0, 0, 0.25); border-right-color: rgba(0, 0, 0, 0.25); border-top-color: rgba(0, 0, 0, 0.25); border-bottom-color: rgba(0, 0, 0, 0.25); display: flex; margin-bottom: 0;"> <ui:VisualElement name="Menu" class="padding" style="flex-grow: 0; background-color: rgb(255, 255, 255); height: auto; flex-shrink: 10; border-top-width: 0; border-right-width: 0; border-bottom-width: 0; border-left-width: 0; border-top-left-radius: 50px; border-top-right-radius: 50px; border-bottom-right-radius: 50px; border-bottom-left-radius: 50px; padding-top: 25px; padding-right: 25px; padding-bottom: 25px; padding-left: 25px; justify-content: space-between; border-left-color: rgba(0, 0, 0, 0.25); border-right-color: rgba(0, 0, 0, 0.25); border-top-color: rgba(0, 0, 0, 0.25); border-bottom-color: rgba(0, 0, 0, 0.25); display: flex; margin-bottom: 0;">
<ui:Button text="" name="Level" class="row-btn" style="height: 80px; -unity-text-align: middle-right;"> <ui:Button text="" name="Level" class="row-btn" style="height: 80px; -unity-text-align: middle-right;">
<ui:DropdownField label="" choices="اختر فصل دراسي...,الفصل الدراسي الاول,الفصل الدراسي الثاني" index="0" language-direction="RTL" name="Grade" style="margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; position: absolute; flex-grow: 0; font-size: 35px; -unity-font-definition: url(&quot;project://database/Assets/ALArcade/Hakwaty%20Font/TSHakwaty-DemiBold.otf?fileID=12800000&amp;guid=566b773a07b3d064aa1f4c6ef7b6f6fa&amp;type=3#TSHakwaty-DemiBold&quot;); -unity-text-generator: advanced; color: rgb(66, 66, 66); -unity-text-align: upper-right; flex-direction: row;"/> <ui:DropdownField label="" choices="اختر فصل دراسي...,الفصل الدراسي الاول,الفصل الدراسي الثاني" index="0" language-direction="RTL" name="Term" style="margin-top: 0; margin-right: 0; margin-bottom: 0; margin-left: 0; position: absolute; flex-grow: 0; font-size: 35px; -unity-font-definition: url(&quot;project://database/Assets/ALArcade/Hakwaty%20Font/TSHakwaty-DemiBold.otf?fileID=12800000&amp;guid=566b773a07b3d064aa1f4c6ef7b6f6fa&amp;type=3#TSHakwaty-DemiBold&quot;); -unity-text-generator: advanced; color: rgb(66, 66, 66); -unity-text-align: upper-right; flex-direction: row;"/>
</ui:Button> </ui:Button>
</ui:VisualElement> </ui:VisualElement>
</ui:VisualElement> </ui:VisualElement>
......
...@@ -1116,10 +1116,10 @@ RectTransform: ...@@ -1116,10 +1116,10 @@ RectTransform:
m_Children: [] m_Children: []
m_Father: {fileID: 5665338920870028329} m_Father: {fileID: 5665338920870028329}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0} m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 22.572838, y: 0} m_AnchoredPosition: {x: 22.572838, y: -25.96685}
m_SizeDelta: {x: 51.9337, y: 0} m_SizeDelta: {x: 0, y: 51.9337}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &1367268893384981932 --- !u!222 &1367268893384981932
CanvasRenderer: CanvasRenderer:
...@@ -3222,10 +3222,10 @@ RectTransform: ...@@ -3222,10 +3222,10 @@ RectTransform:
m_Children: [] m_Children: []
m_Father: {fileID: 5665338920870028329} m_Father: {fileID: 5665338920870028329}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0} m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 83.71851, y: 0} m_AnchoredPosition: {x: 83.71851, y: -25.96685}
m_SizeDelta: {x: 51.9337, y: 0} m_SizeDelta: {x: 0, y: 51.9337}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6044436298841298018 --- !u!222 &6044436298841298018
CanvasRenderer: CanvasRenderer:
...@@ -3406,10 +3406,10 @@ RectTransform: ...@@ -3406,10 +3406,10 @@ RectTransform:
m_Children: [] m_Children: []
m_Father: {fileID: 5665338920870028329} m_Father: {fileID: 5665338920870028329}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0} m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 267.15555, y: 0} m_AnchoredPosition: {x: 267.15555, y: -25.96685}
m_SizeDelta: {x: 51.9337, y: 0} m_SizeDelta: {x: 0, y: 51.9337}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7681831561278972478 --- !u!222 &7681831561278972478
CanvasRenderer: CanvasRenderer:
...@@ -3793,10 +3793,10 @@ RectTransform: ...@@ -3793,10 +3793,10 @@ RectTransform:
m_Children: [] m_Children: []
m_Father: {fileID: 5665338920870028329} m_Father: {fileID: 5665338920870028329}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0} m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 206.00986, y: 0} m_AnchoredPosition: {x: 206.00986, y: -25.96685}
m_SizeDelta: {x: 51.9337, y: 0} m_SizeDelta: {x: 0, y: 51.9337}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8749602556167845090 --- !u!222 &8749602556167845090
CanvasRenderer: CanvasRenderer:
...@@ -4052,10 +4052,10 @@ RectTransform: ...@@ -4052,10 +4052,10 @@ RectTransform:
m_Children: [] m_Children: []
m_Father: {fileID: 5665338920870028329} m_Father: {fileID: 5665338920870028329}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0} m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 144.8642, y: 0} m_AnchoredPosition: {x: 144.8642, y: -25.96685}
m_SizeDelta: {x: 51.9337, y: 0} m_SizeDelta: {x: 0, y: 51.9337}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8187631058268110602 --- !u!222 &8187631058268110602
CanvasRenderer: CanvasRenderer:
......
...@@ -2374,8 +2374,8 @@ MonoBehaviour: ...@@ -2374,8 +2374,8 @@ MonoBehaviour:
m_OnCullStateChanged: m_OnCullStateChanged:
m_PersistentCalls: m_PersistentCalls:
m_Calls: [] m_Calls: []
text: "\u0646\u062C\u062D\u062A \u0627\u0644\u0645\u0647\u0645\u0629" text: "\u0646\u0642\u0627\u0637\u0643"
fontStack: {fileID: 11400000, guid: 0029e5efb4c7a12f1ac9136de794e6dc, type: 2} fontStack: {fileID: 11400000, guid: 657d8db1dabff4325ae70686887b629b, type: 2}
appearance: {fileID: 11400000, guid: 3a559cf5d653f05ea807e1be5655df92, type: 2} appearance: {fileID: 11400000, guid: 3a559cf5d653f05ea807e1be5655df92, type: 2}
fontSize: 72 fontSize: 72
baseDirection: 2 baseDirection: 2
......
...@@ -296,108 +296,7 @@ namespace com.al_arcade.cs ...@@ -296,108 +296,7 @@ namespace com.al_arcade.cs
{ {
return new CsQuestion[] return new CsQuestion[]
{ {
new()
{
id = "1", source = "علوم - الوحدة 3",
wrong_word = "القمر", correct_answer = "الشمس",
words = new CsWord[]
{
new() { word_text = "القمر", position = 0, is_wrong = true },
new() { word_text = "مصدر", position = 1, is_wrong = false },
new() { word_text = "الضوء", position = 2, is_wrong = false },
new() { word_text = "والحرارة", position = 3, is_wrong = false },
new() { word_text = "للأرض", position = 4, is_wrong = false },
},
options = new CsOption[]
{
new() { option_text = "الشمس", is_correct = true },
new() { option_text = "النجوم", is_correct = false },
new() { option_text = "الكواكب", is_correct = false },
new() { option_text = "المذنبات", is_correct = false },
}
},
new()
{
id = "2", source = "كيمياء",
wrong_word = "الأكسجين", correct_answer = "النيتروجين",
words = new CsWord[]
{
new() { word_text = "يتكون", position = 0, is_wrong = false },
new() { word_text = "معظم", position = 1, is_wrong = false },
new() { word_text = "الغلاف", position = 2, is_wrong = false },
new() { word_text = "الجوي", position = 3, is_wrong = false },
new() { word_text = "من", position = 4, is_wrong = false },
new() { word_text = "الأكسجين", position = 5, is_wrong = true },
},
options = new CsOption[]
{
new() { option_text = "النيتروجين", is_correct = true },
new() { option_text = "ثاني أكسيد الكربون", is_correct = false },
new() { option_text = "الهيليوم", is_correct = false },
}
},
new()
{
id = "3", source = "أحياء",
wrong_word = "الجذر", correct_answer = "الورقة",
words = new CsWord[]
{
new() { word_text = "الجذر", position = 0, is_wrong = true },
new() { word_text = "هي", position = 1, is_wrong = false },
new() { word_text = "الجزء", position = 2, is_wrong = false },
new() { word_text = "المسؤول", position = 3, is_wrong = false },
new() { word_text = "عن", position = 4, is_wrong = false },
new() { word_text = "البناء", position = 5, is_wrong = false },
new() { word_text = "الضوئي", position = 6, is_wrong = false },
},
options = new CsOption[]
{
new() { option_text = "الورقة", is_correct = true },
new() { option_text = "الساق", is_correct = false },
new() { option_text = "الزهرة", is_correct = false },
new() { option_text = "البذرة", is_correct = false },
}
},
new()
{
id = "4", source = "فيزياء",
wrong_word = "تنخفض", correct_answer = "ترتفع",
words = new CsWord[]
{
new() { word_text = "تنخفض", position = 0, is_wrong = true },
new() { word_text = "درجة", position = 1, is_wrong = false },
new() { word_text = "غليان", position = 2, is_wrong = false },
new() { word_text = "الماء", position = 3, is_wrong = false },
new() { word_text = "عند", position = 4, is_wrong = false },
new() { word_text = "مئة", position = 5, is_wrong = false },
new() { word_text = "سلسيوس", position = 6, is_wrong = false },
},
options = new CsOption[]
{
new() { option_text = "ترتفع", is_correct = true },
new() { option_text = "تثبت", is_correct = false },
new() { option_text = "تتذبذب", is_correct = false },
}
},
new()
{
id = "5", source = "جغرافيا",
wrong_word = "خمس", correct_answer = "سبع",
words = new CsWord[]
{
new() { word_text = "يتكون", position = 0, is_wrong = false },
new() { word_text = "العالم", position = 1, is_wrong = false },
new() { word_text = "من", position = 2, is_wrong = false },
new() { word_text = "خمس", position = 3, is_wrong = true },
new() { word_text = "قارات", position = 4, is_wrong = false },
},
options = new CsOption[]
{
new() { option_text = "ست", is_correct = false },
new() { option_text = "سبع", is_correct = true },
new() { option_text = "ثمان", is_correct = false },
}
},
}; };
} }
......
...@@ -83,9 +83,15 @@ namespace com.al_arcade.cs ...@@ -83,9 +83,15 @@ namespace com.al_arcade.cs
if (uiManager != null && uiManager.isMusicOn) if (uiManager != null && uiManager.isMusicOn)
SSAudioManager.EnsureInstance().PlayMusic(); SSAudioManager.EnsureInstance().PlayMusic();
var filter = new QuestionFilter()
.CurriculumId(0)
.SubjectId(0)
.GradeId(session.gradeId)
.Count(session.questionCount)
.Shuffle(true);
yield return api.FetchCs( yield return api.FetchCs(
session.buildType, session.classCode, filter,
session.questionCount, session.gradeId,
qs => _questions = qs, qs => _questions = qs,
err => onError(err) err => onError(err)
); );
...@@ -445,7 +451,7 @@ namespace com.al_arcade.cs ...@@ -445,7 +451,7 @@ namespace com.al_arcade.cs
_targetGroup.RemoveMember(_targetGroup.Targets[1].Object); _targetGroup.RemoveMember(_targetGroup.Targets[1].Object);
} }
_groupFraming.CenterOffset = new Vector2(0f, -0.7f); _groupFraming.CenterOffset = new Vector2(0f, -0.8f);
} }
private IEnumerator UnlockAfterCooldown() private IEnumerator UnlockAfterCooldown()
......
...@@ -308,44 +308,7 @@ namespace com.al_arcade.cs ...@@ -308,44 +308,7 @@ namespace com.al_arcade.cs
{ {
return new CsQuestion[] return new CsQuestion[]
{ {
new()
{
id = "1", source = "علوم",
wrong_word = "القمر", correct_answer = "الشمس",
words = new CsWord[]
{
new() { word_text = "القمر", position = 0, is_wrong = true },
new() { word_text = "مصدر", position = 1, is_wrong = false },
new() { word_text = "الضوء", position = 2, is_wrong = false },
new() { word_text = "والحرارة", position = 3, is_wrong = false },
new() { word_text = "للأرض", position = 4, is_wrong = false },
},
options = new CsOption[]
{
new() { option_text = "الشمس", is_correct = true },
new() { option_text = "النجوم", is_correct = false },
new() { option_text = "الكواكب", is_correct = false },
}
},
new()
{
id = "2", source = "جغرافيا",
wrong_word = "خمس", correct_answer = "سبع",
words = new CsWord[]
{
new() { word_text = "يتكون", position = 0, is_wrong = false },
new() { word_text = "العالم", position = 1, is_wrong = false },
new() { word_text = "من", position = 2, is_wrong = false },
new() { word_text = "خمس", position = 3, is_wrong = true },
new() { word_text = "قارات", position = 4, is_wrong = false },
},
options = new CsOption[]
{
new() { option_text = "ست", is_correct = false },
new() { option_text = "سبع", is_correct = true },
new() { option_text = "ثمان", is_correct = false },
}
},
}; };
} }
} }
......
...@@ -207,7 +207,7 @@ public class CsSentence : MonoBehaviour ...@@ -207,7 +207,7 @@ public class CsSentence : MonoBehaviour
{ {
csWord = rect.gameObject.AddComponent<CsWordButton>(); csWord = rect.gameObject.AddComponent<CsWordButton>();
} }
csWord.Setup(_question.words[i].word_text, _question.words[i].is_wrong, i, elementWidth, 1f); csWord.Setup(_question.words[i].word_text, _question.words[i].is_distractor, i, elementWidth, 1f);
// Animate to the locally offset position // Animate to the locally offset position
rect.DOLocalMove(targetPos, 0.3f).SetEase(Ease.OutCubic).OnComplete(() => rect.DOLocalMove(targetPos, 0.3f).SetEase(Ease.OutCubic).OnComplete(() =>
...@@ -224,32 +224,6 @@ public class CsSentence : MonoBehaviour ...@@ -224,32 +224,6 @@ public class CsSentence : MonoBehaviour
} }
} }
_targetGroup.RemoveMember(_targetGroup.Targets[1].Object);
// Update Camera Group (top + left + right)
// Top
_targetGroup.AddMember(_wordTexts[0].transform.parent, 1f, _wordTexts[0].transform.parent.GetComponent<RectTransform>().rect.height * 0.02f);
var widestRect = widestWord.transform.parent.GetComponent<RectTransform>();
var offsetFromCenter = Vector3.right * (widestRect.sizeDelta.x / 2);
var right = new GameObject("Right");
var rightRect = right.AddComponent<RectTransform>();
rightRect.SetParent(widestRect);
rightRect.localPosition = offsetFromCenter;
rightRect.SetParent(widestRect.parent);
var left = new GameObject("left");
var leftRect = left.AddComponent<RectTransform>();
leftRect.SetParent(widestRect);
leftRect.localPosition = -offsetFromCenter;
leftRect.SetParent(widestRect.parent);
_targetGroup.AddMember(rightRect, 1f, _wordTexts[0].transform.parent.GetComponent<RectTransform>().rect.height * 0.02f);
_targetGroup.AddMember(leftRect, 1f, _wordTexts[0].transform.parent.GetComponent<RectTransform>().rect.height * 0.02f);
if (_background != null) Destroy(_background); if (_background != null) Destroy(_background);
} }
...@@ -365,8 +339,6 @@ public class CsSentence : MonoBehaviour ...@@ -365,8 +339,6 @@ public class CsSentence : MonoBehaviour
cnvRt.offsetMax = Vector2.zero; cnvRt.offsetMax = Vector2.zero;
cnvRt.offsetMin = Vector2.zero; cnvRt.offsetMin = Vector2.zero;
_targetGroup.AddMember(cnvRt, 1f, 2f);
canvasObj.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity); canvasObj.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
var _backgroundImage = backGO.AddComponent<Image>(); var _backgroundImage = backGO.AddComponent<Image>();
_backgroundImage.type = Image.Type.Sliced; _backgroundImage.type = Image.Type.Sliced;
...@@ -378,6 +350,31 @@ public class CsSentence : MonoBehaviour ...@@ -378,6 +350,31 @@ public class CsSentence : MonoBehaviour
cnvRt.DOLocalMoveY(0f, 0.3f).SetEase(Ease.OutCubic); cnvRt.DOLocalMoveY(0f, 0.3f).SetEase(Ease.OutCubic);
_backgroundImage.DOFade(1f, 0.3f).SetEase(Ease.OutCubic).From(0f); _backgroundImage.DOFade(1f, 0.3f).SetEase(Ease.OutCubic).From(0f);
StartCoroutine(AnimateTextIn()); StartCoroutine(AnimateTextIn());
// Update Camera Group (top + left + right)
// Top
_targetGroup.AddMember(_wordTexts[0].transform.parent, 1f, _wordTexts[0].transform.parent.GetComponent<RectTransform>().rect.height * 0.02f);
var widestRect = widestWord.transform.parent.GetComponent<RectTransform>();
var offsetFromCenter = Vector3.right * (widestRect.sizeDelta.x / 2);
var right = new GameObject("Right");
var rightRect = right.AddComponent<RectTransform>();
rightRect.SetParent(widestRect);
rightRect.localPosition = offsetFromCenter;
rightRect.SetParent(widestRect.parent);
var left = new GameObject("left");
var leftRect = left.AddComponent<RectTransform>();
leftRect.SetParent(widestRect);
leftRect.localPosition = -offsetFromCenter;
leftRect.SetParent(widestRect.parent);
_targetGroup.AddMember(rightRect, 1f, _wordTexts[0].transform.parent.GetComponent<RectTransform>().rect.height * 0.02f);
_targetGroup.AddMember(leftRect, 1f, _wordTexts[0].transform.parent.GetComponent<RectTransform>().rect.height * 0.02f);
} }
} }
...@@ -274,21 +274,7 @@ namespace com.al_arcade.mcq ...@@ -274,21 +274,7 @@ namespace com.al_arcade.mcq
{ {
return new McqQuestion[] return new McqQuestion[]
{ {
new() { id="1", question_text="ما هي عاصمة مصر؟",
answer1="القاهرة", answer2="الإسكندرية",
answer3="أسوان", answer4="المنصورة", source="جغرافيا" },
new() { id="2", question_text="كم عدد كواكب المجموعة الشمسية؟",
answer1="8", answer2="9",
answer3="7", answer4="10", source="علوم" },
new() { id="3", question_text="ما هو أكبر محيط في العالم؟",
answer1="المحيط الهادي", answer2="المحيط الأطلسي",
answer3="المحيط الهندي", answer4="المحيط المتجمد", source="جغرافيا" },
new() { id="4", question_text="ما هو الغاز الأكثر وفرة في الغلاف الجوي؟",
answer1="النيتروجين", answer2="الأكسجين",
answer3="ثاني أكسيد الكربون", answer4="الهيليوم", source="كيمياء" },
new() { id="5", question_text="من اخترع المصباح الكهربائي؟",
answer1="توماس إديسون", answer2="نيكولا تسلا",
answer3="ألبرت آينشتاين", answer4="إسحاق نيوتن", source="تاريخ العلوم" },
}; };
} }
......
...@@ -73,9 +73,15 @@ namespace com.al_arcade.mcq ...@@ -73,9 +73,15 @@ namespace com.al_arcade.mcq
var session = SSGameSession.EnsureInstance(); var session = SSGameSession.EnsureInstance();
var api = SSApiManager.EnsureInstance(); var api = SSApiManager.EnsureInstance();
var filter = new QuestionFilter()
.CurriculumId(0)
.SubjectId(0)
.GradeId(session.gradeId)
.Count(session.questionCount)
.Shuffle(true);
yield return api.FetchMcq( yield return api.FetchMcq(
session.buildType, session.classCode, filter,
session.questionCount, session.gradeId,
qs => _questions = qs, qs => _questions = qs,
err => onError(err) err => onError(err)
); );
...@@ -153,7 +159,7 @@ namespace com.al_arcade.mcq ...@@ -153,7 +159,7 @@ namespace com.al_arcade.mcq
BeginGameplay(); BeginGameplay();
} }
public void ResetGame() public override void ResetGame()
{ {
ResetBaseState(); ResetBaseState();
_state = McqGameState.Idle; _state = McqGameState.Idle;
...@@ -311,8 +317,9 @@ namespace com.al_arcade.mcq ...@@ -311,8 +317,9 @@ namespace com.al_arcade.mcq
// ─── Gate Spawning ─────────────────────────────────────────────────── // ─── Gate Spawning ───────────────────────────────────────────────────
private void SpawnGates(McqQuestion question) private void SpawnGates(McqQuestion question)
{ {
string[] answers = question.GetShuffledAnswers(out int correctIdx); var shuffled = McqHelper.Shuffle(question);
_correctGateIndex = correctIdx; string[] answers = shuffled.answers;
_correctGateIndex = shuffled.correctIndex;
_activeGates.Clear(); _activeGates.Clear();
if (questionDisplay != null) if (questionDisplay != null)
...@@ -327,7 +334,7 @@ namespace com.al_arcade.mcq ...@@ -327,7 +334,7 @@ namespace com.al_arcade.mcq
for (int i = 0; i < answers.Length; i++) for (int i = 0; i < answers.Length; i++)
{ {
Vector3 gatePos = basePos + Vector3.right * (startX + i * gateSpacing); Vector3 gatePos = basePos + Vector3.right * (startX + i * gateSpacing);
var gate = CreateGate(gatePos, i, answers[i], i == correctIdx); var gate = CreateGate(gatePos, i, answers[i], i == _correctGateIndex);
_activeGates.Add(gate); _activeGates.Add(gate);
gate.transform.DOScale(Vector3.one, 0.5f) gate.transform.DOScale(Vector3.one, 0.5f)
......
...@@ -328,15 +328,7 @@ namespace com.al_arcade.mcq ...@@ -328,15 +328,7 @@ namespace com.al_arcade.mcq
{ {
return new McqQuestion[] return new McqQuestion[]
{ {
new() { id="1", question_text="ما هي عاصمة مصر؟",
answer1="القاهرة", answer2="الإسكندرية",
answer3="أسوان", answer4="المنصورة", source="جغرافيا" },
new() { id="2", question_text="كم عدد كواكب المجموعة الشمسية؟",
answer1="8", answer2="9",
answer3="7", answer4="10", source="علوم" },
new() { id="3", question_text="ما هو أكبر محيط في العالم؟",
answer1="المحيط الهادي", answer2="المحيط الأطلسي",
answer3="المحيط الهندي", answer4="المحيط المتجمد", source="جغرافيا" },
}; };
} }
} }
......
fileFormatVersion: 2
guid: 20ba488acbcf34d438c13bfb24840820
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System;
using LightSide;
using DG.Tweening;
public class AnswerButtonUI : MonoBehaviour
{
[SerializeField] private Button button;
[SerializeField] private Image answerImageComponent;
[SerializeField] private UniText answerTextComponent;
[SerializeField] private LayoutGroup answerLayout;
[SerializeField] private Image buttonBackgroundImage;
[SerializeField] private Color normalColor = Color.white;
[SerializeField] private Color normalTextColor = Color.black;
[SerializeField] private Color selectedCorrectColor = Color.green;
[SerializeField] private Color selectedIncorrectColor = Color.red;
[SerializeField] private Color correctAnswerColor = new Color(0.2f, 0.8f, 0.2f, 1f);
[SerializeField] private Color disabledColor = new Color(0.7f, 0.7f, 0.7f, 1f);
private int answerIndex;
private Action<int> onClicked;
private bool isInteractable = true;
private CanvasGroup canvasGroup;
private RectTransform rectTransform;
private void OnEnable()
{
if (button != null)
{
button.onClick.RemoveAllListeners();
}
}
private void OnDisable()
{
DOTween.Kill(this);
}
/// <summary>
/// Setup answer button with text and optional image
/// </summary>
public void Setup(string answerText, Sprite answerImage, int index, Action<int> callback)
{
if (button != null)
button.Select();
answerIndex = index;
onClicked = callback;
isInteractable = true;
// Cache components
if (rectTransform == null)
rectTransform = GetComponent<RectTransform>();
if (canvasGroup == null)
canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup == null)
canvasGroup = gameObject.AddComponent<CanvasGroup>();
// Setup text
if (answerTextComponent != null)
{
answerTextComponent.Text = answerText;
answerTextComponent.color = normalTextColor;
}
// Setup image
if (answerImageComponent != null)
{
if (answerImage != null)
{
answerImageComponent.sprite = answerImage;
answerImageComponent.gameObject.SetActive(true);
}
else
{
answerImageComponent.gameObject.SetActive(false);
}
}
// Setup button
if (button != null)
{
button.onClick.RemoveAllListeners();
button.onClick.AddListener(() => HandleClick());
button.interactable = true;
}
// Reset background color
if (buttonBackgroundImage != null)
{
buttonBackgroundImage.color = normalColor;
}
// Rebuild layout
if (answerLayout != null)
{
LayoutRebuilder.ForceRebuildLayoutImmediate(answerLayout.transform as RectTransform);
}
}
/// <summary>
/// Handle button click
/// </summary>
private void HandleClick()
{
if (!isInteractable || button == null || !button.interactable)
return;
onClicked?.Invoke(answerIndex);
}
/// <summary>
/// Mark this answer as selected and correct
/// </summary>
public void SetSelected(bool isCorrect)
{
isInteractable = false;
if (buttonBackgroundImage != null)
{
DOTween.Kill(buttonBackgroundImage, true);
buttonBackgroundImage.DOColor(
isCorrect ? selectedCorrectColor : selectedIncorrectColor,
0.3f
).SetEase(Ease.OutQuad);
}
if (button != null)
{
button.interactable = false;
}
}
/// <summary>
/// Show this as the correct answer (when user selected wrong)
/// </summary>
public void SetCorrect()
{
isInteractable = false;
if (buttonBackgroundImage != null)
{
DOTween.Kill(buttonBackgroundImage, true);
buttonBackgroundImage.DOColor(correctAnswerColor, 0.3f)
.SetEase(Ease.OutQuad);
}
if (button != null)
{
button.interactable = false;
}
}
/// <summary>
/// Disable this button without highlighting
/// </summary>
public void SetDisabled()
{
isInteractable = false;
if (buttonBackgroundImage != null)
{
DOTween.Kill(buttonBackgroundImage, true);
buttonBackgroundImage.DOColor(disabledColor, 0.3f)
.SetEase(Ease.OutQuad);
}
if (button != null)
{
button.interactable = false;
}
}
/// <summary>
/// Reset button to normal state
/// </summary>
public void Reset()
{
isInteractable = true;
if (buttonBackgroundImage != null)
{
DOTween.Kill(buttonBackgroundImage, true);
buttonBackgroundImage.color = normalColor;
}
if (answerTextComponent != null)
{
answerTextComponent.color = normalTextColor;
}
if (canvasGroup != null)
{
canvasGroup.alpha = 1f;
}
if (rectTransform != null)
{
rectTransform.localScale = Vector3.one;
}
if (button != null)
{
button.interactable = true;
}
}
/// <summary>
/// Get answer index
/// </summary>
public int GetAnswerIndex()
{
return answerIndex;
}
/// <summary>
/// Check if button is interactable
/// </summary>
public bool IsInteractable()
{
return isInteractable;
}
}
\ No newline at end of file
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.Events;
using DG.Tweening;
using com.al_arcade.shared;
using com.al_arcade.mcq;
using System;
namespace com.al_arcade.mcq
{
public class NewMCQGameManger : MonoBehaviour
{
[SerializeField] private int totalLives = 5;
[SerializeField] private float questionTime = 5;
[SerializeField] private float streakBonusThreshold = 3;
[Header("Audio — SFX Clips")]
[SerializeField] private AudioClip sfxCorrect;
[SerializeField] private AudioClip sfxWrong;
[SerializeField] private AudioClip sfxClick;
[SerializeField] private AudioClip sfxVictory;
[SerializeField] private AudioClip sfxDefeat;
[SerializeField] private AudioClip sfxWhoosh;
[SerializeField] private AudioClip sfxPop;
[SerializeField] private AudioClip sfxCheer;
[SerializeField] private AudioClip sfxCountdown;
private DateTime gameStartTime;
private McqQuestion[] _questions;
private McqQuestion currentQuestion;
private int currentQuestionIndex;
private int _score, _streak, _bestStreak, _lives;
private int _correctCount, _wrongCount;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
SetupAudioManager();
_lives = totalLives;
if (NewMCQUIManager.Instance != null)
{
NewMCQUIManager.Instance.SetLives(totalLives, totalLives);
NewMCQUIManager.Instance.SetScore(0);
NewMCQUIManager.Instance.ShowGameUI();
}
StartCoroutine(StartGame());
}
private IEnumerator StartGame()
{
NewMCQUIManager.Instance.ShowLoading("جاري تحميل الأسئلة...");
var session = SSGameSession.EnsureInstance();
var api = SSApiManager.EnsureInstance();
string error = null;
yield return api.FetchMcq(
session.buildType, session.classCode,
session.questionCount, session.gradeId,
qs => _questions = qs,
err => error = err
);
if (error != null || _questions == null || _questions.Length == 0)
{
NewMCQUIManager.Instance.ShowError(error ?? "لا توجد أسئلة متاحة");
yield break;
}
NewMCQUIManager.Instance.HideLoading();
gameStartTime = DateTime.Now;
ShowNextQuestion();
}
private void ShowNextQuestion()
{
NewMCQUIManager.Instance.SetProgress(currentQuestionIndex, _questions.Length);
if (currentQuestionIndex >= _questions.Length)
{
StartCoroutine(VictorySequence());
return;
}
McqQuestion question = _questions[currentQuestionIndex];
QuestionUi.Instance.DisplayQuestion(question, OnAnswerSubmitted);
}
private void OnAnswerSubmitted(int selectedIndex, bool isCorrect)
{
Debug.Log($"Answer Index: {selectedIndex}, Correct:{isCorrect}");
if (isCorrect)
{
_correctCount++;
_streak++;
if (_bestStreak < _streak)
{
_bestStreak = _streak;
}
int points = 100;
if (_streak >= streakBonusThreshold)
points += (_streak - (int)streakBonusThreshold + 1) * 25;
_score += points;
NewMCQUIManager.Instance.SetScore(_score);
ShowCorrectFeedback(points);
var audio = SSAudioManager.Instance;
if (audio != null)
{
if (audio.sfxCorrect != null) audio.PlayCorrect();
else audio.PlayCorrectBeep();
}
}
else
{
_streak = 0;
_wrongCount++;
_lives--;
ShowWrongFeedback();
NewMCQUIManager.Instance.SetLives(_lives, totalLives);
if (_lives <= 0)
{
StartCoroutine(GameOverSequence());
return;
}
var audio = SSAudioManager.Instance;
if (audio != null)
{
if (audio.sfxWrong != null) audio.PlayWrong();
else audio.PlayWrongBeep();
}
}
Invoke(nameof(GoToNextQuestion), 1f);
}
private void GoToNextQuestion()
{
currentQuestionIndex++;
QuestionUi.Instance.ResetForNextQuestion();
ShowNextQuestion();
}
private IEnumerator VictorySequence()
{
var audio = SSAudioManager.Instance;
if (audio != null)
{ if (audio.sfxVictory != null) audio.PlayVictory(); else audio.PlaySuccessJingle(); }
var particles = SSParticleManager.Instance;
if (particles != null) particles.PlayScreenConfetti();
yield return new WaitForSeconds(0.5f);
NewMCQUIManager.Instance.ShowResults(_score, _correctCount, _wrongCount,
_bestStreak, _questions.Length, true);
GameHistoryService.Instance.AddGame("mcq", _score, gameStartTime, DateTime.Now);
}
private IEnumerator GameOverSequence()
{
var audio = SSAudioManager.Instance;
if (audio != null)
{ if (audio.sfxDefeat != null) audio.PlayDefeat(); else audio.PlayFailBuzz(); }
yield return new WaitForSeconds(0.5f);
NewMCQUIManager.Instance.ShowResults(_score, _correctCount, _wrongCount,
_bestStreak, _questions.Length, false);
}
private void ShowCorrectFeedback(int points)
{
if (NewMCQUIManager.Instance != null)
{
string msg = _streak >= streakBonusThreshold
? $"ممتاز! +{points} (سلسلة {_streak}×)"
: $"صحيح! +{points}";
NewMCQUIManager.Instance.ShowFeedback(msg, true);
}
}
private void ShowWrongFeedback()
{
if (NewMCQUIManager.Instance != null) NewMCQUIManager.Instance.ShowFeedback("خطأ!", false);
if (Camera.main != null)
{
Debug.Log("shake");
DOTween.Kill(Camera.main.transform, "camShake");
Camera.main.transform.DOShakePosition(0.4f, 0.3f, 15, 90f, false, true)
.SetEase(Ease.OutQuad).SetId("camShake");
}
}
private void SetupAudioManager()
{
var a = SSAudioManager.EnsureInstance();
if (sfxCorrect != null) a.sfxCorrect = sfxCorrect;
if (sfxWrong != null) a.sfxWrong = sfxWrong;
if (sfxClick != null) a.sfxClick = sfxClick;
if (sfxVictory != null) a.sfxVictory = sfxVictory;
if (sfxDefeat != null) a.sfxDefeat = sfxDefeat;
if (sfxWhoosh != null) a.sfxWhoosh = sfxWhoosh;
if (sfxPop != null) a.sfxPop = sfxPop;
if (sfxCheer != null) a.sfxCheer = sfxCheer;
if (sfxCountdown != null) a.sfxCountdown = sfxCountdown;
}
}
}
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
// using ALArcade.ArabicTMP;
namespace com.al_arcade.mcq
{
using DG.Tweening;
using LightSide;
using shared;
using UnityEngine.SceneManagement;
public class NewMCQUIManager : MonoBehaviour
{
public static NewMCQUIManager Instance;
[Header("CanvasGroups")]
[SerializeField] private CanvasGroup _gameUI;
[SerializeField] private CanvasGroup _loadingUI;
[SerializeField] private CanvasGroup _errorUI;
[SerializeField] private CanvasGroup _resultsUI;
[SerializeField] private CanvasGroup _feedbackUI;
[Header("Game UI")]
[SerializeField] private UniText _scoreText;
[SerializeField] private UniText _streakText;
[SerializeField] private UniText _progressText;
[SerializeField] private UniText _loadingText;
[SerializeField] private UniText _errorText;
[SerializeField] private UniText _feedbackText;
[SerializeField] private Image _feedbackBg;
[SerializeField] private Image[] _heartIcons;
[Header("Results UI")]
[SerializeField] private UniText _resultTitle;
[SerializeField] private UniText _resultScore;
[SerializeField] private UniText _resultCorrect;
[SerializeField] private UniText _resultWrong;
[SerializeField] private UniText _resultStreak;
[SerializeField] private Button _resultRestartBtn;
[SerializeField] private Slider _progressSlider;
[SerializeField] private GameObject _winIcon;
[SerializeField] private GameObject _loseIcon;
private Image _progressFill;
[Header("Events")]
public UnityEvent onRestartClicked;
private void Awake()
{
if (_gameUI != null)
{
_gameUI.alpha = 0; _gameUI.gameObject.SetActive(false);
}
if (_loadingUI != null)
{
_loadingUI.alpha = 0; _loadingUI.gameObject.SetActive(false);
}
if (_errorUI != null)
{
_errorUI.alpha = 0; _errorUI.gameObject.SetActive(false);
}
if (_resultsUI != null)
{
_resultsUI.alpha = 0; _resultsUI.gameObject.SetActive(false);
}
if (_feedbackUI != null)
{
_feedbackUI.alpha = 0; _feedbackUI.gameObject.SetActive(true);
}
if (_winIcon != null) _winIcon.SetActive(false);
if (_loseIcon != null) _loseIcon.SetActive(false);
if(Instance != null)
{
Debug.LogError("there is two ui manager");
}
else
{
Instance = this;
}
}
private void Start()
{
_resultRestartBtn.onClick.AddListener(() => { SceneManager.LoadScene(SceneManager.GetActiveScene().name); });
}
public void ShowGameUI()
{
_gameUI.gameObject.SetActive(true);
_gameUI.DOFade(1f, 0.5f);
}
public void SetScore(int score)
{
if (_scoreText == null) return;
_scoreText.Text = score.ToString("N0");
DOTween.Kill(_scoreText.transform, "scorePunch");
_scoreText.transform.DOPunchScale(Vector3.one * 0.2f, 0.3f, 6, 0.3f)
.SetId("scorePunch");
}
public void SetStreak(int streak)
{
if (_streakText == null) return;
_streakText.Text = streak > 1 ? $"🔥 ×{streak}" : "";
if (streak > 1)
{
DOTween.Kill(_streakText.transform, "streakPop");
_streakText.transform.localScale = Vector3.one * 1.3f;
_streakText.transform.DOScale(Vector3.one, 0.4f)
.SetEase(Ease.OutElastic).SetId("streakPop");
}
}
public void SetLives(int lives, int maxLives)
{
for (int i = 0; i < _heartIcons.Length; i++)
{
bool active = i < maxLives;
_heartIcons[i].gameObject.SetActive(active);
if (active)
{
bool alive = i < lives;
_heartIcons[i].color = alive
? SSColorPalette.Danger
: SSColorPalette.WithAlpha(SSColorPalette.Danger, 0.2f);
if (!alive && i == lives)
{
DOTween.Kill(_heartIcons[i].transform);
_heartIcons[i].transform.DOPunchScale(Vector3.one * 0.5f, 0.3f);
}
}
}
}
public void SetProgress(int current, int total)
{
if (_progressText != null) _progressText.Text = $"{current} / {total}";
if (_progressFill != null && total > 0)
{
float t = (float)current / total;
DOTween.Kill(_progressFill.rectTransform, "progFill");
_progressFill.rectTransform.DOAnchorMax(new Vector2(t, 1f), 0.5f)
.SetEase(Ease.OutQuad).SetId("progFill");
}
if (_progressSlider != null)
{
DOTween.Kill("progSlider");
DOVirtual.Float(_progressSlider.value, (float)current, 0.5f, score =>
{
_progressSlider.value = score;
}).SetEase(Ease.OutQuad).SetId("progSlider");
}
}
public void ShowFeedback(string message, bool isCorrect)
{
if (_feedbackText != null) _feedbackText.Text = message;
if (_feedbackBg != null)
_feedbackBg.color = SSColorPalette.WithAlpha(
isCorrect ? SSColorPalette.Success : SSColorPalette.Danger, 0.92f);
DOTween.Kill(_feedbackUI);
_feedbackUI.alpha = 0;
var bgRect = _feedbackBg?.rectTransform;
if (bgRect != null)
{
print("Showing Feedback");
bgRect.localScale = new Vector3(0.5f, 0f, 1f);
var seq = DOTween.Sequence();
seq.Append(_feedbackUI.DOFade(1f, 0.15f));
seq.Join(bgRect.DOScaleX(1f, 0.25f).SetEase(Ease.OutBack));
seq.Join(bgRect.DOScaleY(1f, 0.2f).SetEase(Ease.OutBack).SetDelay(0.05f));
seq.AppendInterval(1.2f);
seq.Append(_feedbackUI.DOFade(0f, 0.3f));
}
}
public void ShowLoading(string msg)
{
_loadingUI.gameObject.SetActive(true);
if (_loadingText != null) _loadingText.Text = msg;
_loadingUI.DOFade(1f, 0.3f);
}
public void HideLoading()
{
_loadingUI.DOFade(0f, 0.3f)
.OnComplete(() => _loadingUI.gameObject.SetActive(false));
}
public void ShowError(string msg)
{
_errorUI.gameObject.SetActive(true);
if (_errorText != null) _errorText.Text = msg;
_errorUI.DOFade(1f, 0.3f);
}
public void ShowResults(int score, int correct, int wrong,
int bestStreak, int total, bool won)
{
_resultsUI.gameObject.SetActive(true);
_resultsUI.alpha = 0;
if (_resultTitle != null) _resultTitle.Text = won ? "أحسنت!" : "حظ أوفر!";
if (_resultScore != null) _resultScore.Text = score.ToString("N0");
if (_resultCorrect != null) _resultCorrect.Text = $"صحيح: {correct}";
if (_resultWrong != null) _resultWrong.Text = $"خطأ: {wrong}";
if (_resultStreak != null) _resultStreak.Text = $"أعلى سلسلة: {bestStreak}";
if (_winIcon != null) _winIcon.SetActive(won);
if (_loseIcon != null) _loseIcon.SetActive(!won);
var seq = DOTween.Sequence();
seq.Append(_resultsUI.DOFade(1f, 0.5f));
if (_resultTitle != null)
{
_resultTitle.transform.localScale = Vector3.zero;
seq.Append(_resultTitle.transform.DOScale(1f, 0.5f).SetEase(Ease.OutBack));
}
if (_winIcon != null)
{
_winIcon.transform.localScale = Vector3.zero;
seq.Append(_winIcon.transform.DOScale(1f, 0.5f).SetEase(Ease.OutBack));
}
if (_loseIcon != null)
{
_loseIcon.transform.localScale = Vector3.zero;
seq.Append(_loseIcon.transform.DOScale(1f, 0.5f).SetEase(Ease.OutBack));
}
if (_resultScore != null)
{
_resultScore.transform.localScale = Vector3.zero;
seq.Append(_resultScore.transform.DOScale(1f, 0.4f).SetEase(Ease.OutBack));
}
}
public void HideResults()
{
_resultsUI.DOFade(0f, 0.3f)
.OnComplete(() => _resultsUI.gameObject.SetActive(false));
}
public void RestartButtonFunction()
{
HideResults();
onRestartClicked?.Invoke();
McqGameManager.Instance.ResetGame();
McqGameManager.Instance.StartGame();
}
public void ResetUI()
{
_gameUI.gameObject.SetActive(false);
_loadingUI.gameObject.SetActive(false);
_errorUI.gameObject.SetActive(false);
_resultsUI.gameObject.SetActive(false);
_feedbackUI.alpha = 0;
}
}
}
fileFormatVersion: 2
guid: 168f088242b9231408ef684c9094c66e
\ No newline at end of file
using System;
using Newtonsoft.Json;
namespace com.al_arcade.shared
{
/// <summary>
/// Data object for reporting a single question attempt to the server.
/// </summary>
[Serializable]
public class AttemptData
{
// Required
[JsonProperty("question_type")] public string questionType;
[JsonProperty("question_id")] public int questionId;
[JsonProperty("session_id")] public string sessionId;
[JsonProperty("is_correct")] public int isCorrect;
[JsonProperty("time_taken_ms")] public int timeTakenMs;
// Optional
[JsonProperty("is_skipped")] public int isSkipped;
[JsonProperty("is_timeout")] public int isTimeout;
[JsonProperty("selected_answer")] public string selectedAnswer;
[JsonProperty("correct_answer")] public string correctAnswer;
[JsonProperty("room_id")] public string roomId;
[JsonProperty("player_id")] public string playerId;
[JsonProperty("round_in_game")] public int? roundInGame;
[JsonProperty("total_rounds")] public int? totalRounds;
[JsonProperty("player_streak")] public int playerStreak;
[JsonProperty("player_score_at_time")] public int playerScoreAtTime;
[JsonProperty("players_in_room")] public int? playersInRoom;
[JsonProperty("answer_rank")] public int? answerRank;
[JsonProperty("hint_used")] public int hintUsed;
[JsonProperty("power_up_used")] public string powerUpUsed;
[JsonProperty("device_type")] public string deviceType = "unknown";
[JsonProperty("os")] public string os;
[JsonProperty("client_version")] public string clientVersion;
[JsonProperty("screen_width")] public int? screenWidth;
[JsonProperty("screen_height")] public int? screenHeight;
public AttemptData() { }
public AttemptData(string type, int qId, string session,
bool correct, int timeMs)
{
questionType = type;
questionId = qId;
sessionId = session;
isCorrect = correct ? 1 : 0;
timeTakenMs = timeMs;
}
}
[Serializable]
public class BatchAttemptPayload
{
public AttemptData[] attempts;
}
}
\ No newline at end of file
fileFormatVersion: 2 fileFormatVersion: 2
guid: 68aee830395e108478a9643b20de1689 guid: 8b3bc4d6f06a2d94baff2c7e3214a202
\ No newline at end of file \ No newline at end of file
using System.Collections.Generic;
using UnityEngine;
namespace com.al_arcade.shared
{
/// <summary>
/// Shuffled MCQ answers container.
/// answer1 from the API is ALWAYS correct — this shuffles
/// them for display and tracks which index is correct.
/// </summary>
public struct ShuffledMcq
{
/// <summary>The 4 answers in shuffled order.</summary>
public string[] answers;
/// <summary>Index (0-3) of the correct answer in the shuffled array.</summary>
public int correctIndex;
/// <summary>Check if player's chosen index is correct.</summary>
public bool IsCorrect(int chosenIndex) => chosenIndex == correctIndex;
/// <summary>The correct answer text.</summary>
public string CorrectText => answers[correctIndex];
}
public static class McqHelper
{
/// <summary>
/// Shuffles the 4 answers and returns which index is now correct.
/// </summary>
public static ShuffledMcq Shuffle(McqQuestion q)
{
// Build list with original indices
var list = new List<(string text, bool isCorrect)>
{
(q.answer1, true), // answer1 = ALWAYS correct
(q.answer2, false),
(q.answer3, false),
(q.answer4, false),
};
// Fisher-Yates shuffle
for (int i = list.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
(list[i], list[j]) = (list[j], list[i]);
}
var result = new ShuffledMcq
{
answers = new string[4],
correctIndex = -1
};
for (int i = 0; i < 4; i++)
{
result.answers[i] = list[i].text;
if (list[i].isCorrect)
result.correctIndex = i;
}
return result;
}
}
}
\ No newline at end of file
fileFormatVersion: 2 fileFormatVersion: 2
guid: 8b4109c5dc3e965418f041d490b09d96 guid: 1bacf1883f436a74caa55a658a8746e3
\ No newline at end of file \ No newline at end of file
using System.Collections.Generic;
namespace com.al_arcade.shared
{
/// <summary>
/// Fluent builder for question filter parameters.
/// Used by GetMcq, GetTf, GetCs, GetMixed.
/// </summary>
public class QuestionFilter
{
private readonly Dictionary<string, string> _params = new();
public QuestionFilter CurriculumId(int id)
{
_params["curriculum_id"] = id.ToString();
return this;
}
public QuestionFilter SubjectId(int id)
{
_params["subject_id"] = id.ToString();
return this;
}
public QuestionFilter GradeId(int id)
{
_params["grade_id"] = id.ToString();
return this;
}
public QuestionFilter TermId(int id)
{
_params["term_id"] = id.ToString();
return this;
}
public QuestionFilter ChapterId(int id)
{
_params["chapter_id"] = id.ToString();
return this;
}
public QuestionFilter Difficulty(string diff)
{
if (!string.IsNullOrEmpty(diff)) _params["difficulty"] = diff;
return this;
}
public QuestionFilter BloomLevel(string bloom)
{
if (!string.IsNullOrEmpty(bloom)) _params["bloom_level"] = bloom;
return this;
}
public QuestionFilter VerifiedOnly(bool v = true)
{
if (v) _params["verified_only"] = "1";
return this;
}
public QuestionFilter Count(int n)
{
if (n > 0) _params["count"] = n.ToString();
return this;
}
public QuestionFilter Shuffle(bool s = true)
{
_params["shuffle"] = s ? "1" : "0";
return this;
}
// Mixed-specific counts
public QuestionFilter McqCount(int n)
{
if (n >= 0) _params["mcq_count"] = n.ToString();
return this;
}
public QuestionFilter TfCount(int n)
{
if (n >= 0) _params["tf_count"] = n.ToString();
return this;
}
public QuestionFilter CsCount(int n)
{
if (n >= 0) _params["cs_count"] = n.ToString();
return this;
}
public Dictionary<string, string> Build() => new(_params);
/// <summary>Quick shortcut: filter by chapter only.</summary>
public static QuestionFilter ForChapter(int chapterId, int count = 10)
{
return new QuestionFilter().ChapterId(chapterId).Count(count);
}
/// <summary>Quick shortcut: filter by subject + grade + term.</summary>
public static QuestionFilter ForSubject(int subjectId, int gradeId, int termId, int count = 10)
{
return new QuestionFilter()
.SubjectId(subjectId)
.GradeId(gradeId)
.TermId(termId)
.Count(count);
}
}
}
\ No newline at end of file
fileFormatVersion: 2 fileFormatVersion: 2
guid: cb8a50d45c5491e45a8c75803f8df92c guid: 52e062c4425a8ca4ea8b284d19376b38
\ No newline at end of file \ No newline at end of file
...@@ -59,30 +59,31 @@ namespace com.al_arcade.shared ...@@ -59,30 +59,31 @@ namespace com.al_arcade.shared
{ {
var api = SSApiManager.EnsureInstance(); var api = SSApiManager.EnsureInstance();
yield return api.ValidateClassCode(code, // yield return api.ValidateClassCode(code,
cls => // cls =>
{ // {
classCode = cls.class_code; // classCode = cls.class_code;
className = cls.class_name; // className = cls.class_name;
teacherName = cls.teacher_name; // teacherName = cls.teacher_name;
gradeName = cls.grade_name; // gradeName = cls.grade_name;
buildType = "teacher"; // buildType = "teacher";
isClassValidated = true; // isClassValidated = true;
if (int.TryParse(cls.grade_id, out int gid)) // if (int.TryParse(cls.grade_id, out int gid))
gradeId = gid; // gradeId = gid;
Debug.Log($"[SSSession] Teacher session started: {className} by {teacherName}"); // Debug.Log($"[SSSession] Teacher session started: {className} by {teacherName}");
onSuccess?.Invoke(); // onSuccess?.Invoke();
onSessionReady?.Invoke(); // onSessionReady?.Invoke();
}, // },
err => // err =>
{ // {
isClassValidated = false; // isClassValidated = false;
Debug.LogWarning($"[SSSession] Validation failed: {err}"); // Debug.LogWarning($"[SSSession] Validation failed: {err}");
onError?.Invoke(err); // onError?.Invoke(err);
onSessionError?.Invoke(err); // onSessionError?.Invoke(err);
}); // });
yield break;
} }
......
...@@ -396,22 +396,7 @@ namespace com.al_arcade.tf ...@@ -396,22 +396,7 @@ namespace com.al_arcade.tf
{ {
return new TfQuestion[] return new TfQuestion[]
{ {
new() { id="1", question_text="الشمس تدور حول الأرض",
is_true=false, source="علوم" },
new() { id="2", question_text="الماء يتكون من الهيدروجين والأكسجين",
is_true=true, source="كيمياء" },
new() { id="3", question_text="القمر ينير بذاته",
is_true=false, source="فلك" },
new() { id="4", question_text="النباتات تحتاج ضوء الشمس لعملية البناء الضوئي",
is_true=true, source="أحياء" },
new() { id="5", question_text="عدد قارات العالم خمس قارات",
is_true=false, source="جغرافيا" },
new() { id="6", question_text="الحديد يتمدد بالحرارة",
is_true=true, source="فيزياء" },
new() { id="7", question_text="المحيط الأطلسي هو أكبر محيطات العالم",
is_true=false, source="جغرافيا" },
new() { id="8", question_text="جسم الإنسان يحتوي على 206 عظمة",
is_true=true, source="أحياء" },
}; };
} }
......
...@@ -54,9 +54,15 @@ namespace com.al_arcade.tf ...@@ -54,9 +54,15 @@ namespace com.al_arcade.tf
{ {
var session = SSGameSession.EnsureInstance(); var session = SSGameSession.EnsureInstance();
var api = SSApiManager.EnsureInstance(); var api = SSApiManager.EnsureInstance();
var filter = new QuestionFilter()
.CurriculumId(0)
.SubjectId(0)
.GradeId(session.gradeId)
.Count(session.questionCount);
yield return api.FetchTf( yield return api.FetchTf(
session.buildType, session.classCode, filter,
session.questionCount, session.gradeId,
qs => _questions = qs, qs => _questions = qs,
err => onError(err) err => onError(err)
); );
......
...@@ -320,16 +320,7 @@ namespace com.al_arcade.tf ...@@ -320,16 +320,7 @@ namespace com.al_arcade.tf
{ {
return new TfQuestion[] return new TfQuestion[]
{ {
new() { id="1", question_text="الشمس تدور حول الأرض",
is_true=false, source="علوم" },
new() { id="2", question_text="الماء يتكون من الهيدروجين والأكسجين",
is_true=true, source="كيمياء" },
new() { id="3", question_text="القمر ينير بذاته",
is_true=false, source="فلك" },
new() { id="4", question_text="النباتات تحتاج ضوء الشمس لعملية البناء الضوئي",
is_true=true, source="أحياء" },
new() { id="5", question_text="الحديد يتمدد بالحرارة",
is_true=true, source="فيزياء" },
}; };
} }
} }
......
...@@ -13,7 +13,7 @@ PlayerSettings: ...@@ -13,7 +13,7 @@ PlayerSettings:
useOnDemandResources: 0 useOnDemandResources: 0
accelerometerFrequency: 60 accelerometerFrequency: 60
companyName: DefaultCompany companyName: DefaultCompany
productName: Correct The Sentence productName: SSBookMinigames
defaultCursor: {fileID: 0} defaultCursor: {fileID: 0}
cursorHotspot: {x: 0, y: 0} cursorHotspot: {x: 0, y: 0}
m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1} m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1}
...@@ -170,7 +170,7 @@ PlayerSettings: ...@@ -170,7 +170,7 @@ PlayerSettings:
androidMaxAspectRatio: 2.4 androidMaxAspectRatio: 2.4
androidMinAspectRatio: 1 androidMinAspectRatio: 1
applicationIdentifier: applicationIdentifier:
Android: com.DefaultCompany.CorrectTheSentence Android: com.DefaultCompany.SSBookMinigames
Standalone: com.Unity-Technologies.com.unity.template.urp-blank Standalone: com.Unity-Technologies.com.unity.template.urp-blank
iPhone: com.Unity-Technologies.com.unity.template.urp-blank iPhone: com.Unity-Technologies.com.unity.template.urp-blank
buildNumber: buildNumber:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment