# Codebase: `Assets`

> Generated on 2026-02-21 15:20:10

---

## Directory Structure

```
.
    ├── ALArcade
    ├── ALArcade.meta
        ├── ArabicTMP
        ├── ArabicTMP.meta
            ├── ArabicCharsets.cs
            ├── ArabicCharsets.cs.meta
            ├── ArabicSupport.cs
            ├── ArabicSupport.cs.meta
            ├── ArabicTMPInputField.cs
            ├── ArabicTMPInputField.cs.meta
            ├── ArabicTextMeshPro.cs
            ├── ArabicTextMeshPro.cs.meta
            ├── ArabicTextMeshProUGUI.cs
            ├── ArabicTextMeshProUGUI.cs.meta
            ├── Editor
            ├── Editor.meta
                ├── ArabicFontAssetCreator.cs
                ├── ArabicFontAssetCreator.cs.meta
                ├── ArabicTMPInputFieldEditor.cs
                ├── ArabicTMPInputFieldEditor.cs.meta
                ├── ArabicTMPMenu.cs
                ├── ArabicTMPMenu.cs.meta
                ├── ArabicTextMeshProEditor.cs
                ├── ArabicTextMeshProEditor.cs.meta
                ├── ArabicTextMeshProUGUIEditor.cs
                ├── ArabicTextMeshProUGUIEditor.cs.meta
    ├── CODEBASE.md
    ├── CODEBASE.md.meta
    ├── Hakawaty.asset
    ├── Hakawaty.asset.meta
    ├── InputSystem_Actions.inputactions
    ├── InputSystem_Actions.inputactions.meta
    ├── Plugins
    ├── Plugins.meta
        ├── Demigiant
        ├── Demigiant.meta
            ├── DOTween
            ├── DOTween.meta
                ├── DOTween.XML
                ├── DOTween.XML.meta
                ├── DOTween.dll
                ├── DOTween.dll.mdb
                ├── DOTween.dll.mdb.meta
                ├── DOTween.dll.meta
                ├── Editor
                ├── Editor.meta
                    ├── DOTweenEditor.XML
                    ├── DOTweenEditor.XML.meta
                    ├── DOTweenEditor.dll
                    ├── DOTweenEditor.dll.mdb
                    ├── DOTweenEditor.dll.mdb.meta
                    ├── DOTweenEditor.dll.meta
                    ├── Imgs
                    ├── Imgs.meta
                        ├── DOTweenIcon.png
                        ├── DOTweenIcon.png.meta
                        ├── DOTweenMiniIcon.png
                        ├── DOTweenMiniIcon.png.meta
                        ├── Footer.png
                        ├── Footer.png.meta
                        ├── Footer_dark.png
                        ├── Footer_dark.png.meta
                        ├── Header.jpg
                        ├── Header.jpg.meta
                ├── Modules
                ├── Modules.meta
                    ├── DOTweenModuleAudio.cs
                    ├── DOTweenModuleAudio.cs.meta
                    ├── DOTweenModuleEPOOutline.cs
                    ├── DOTweenModuleEPOOutline.cs.meta
                    ├── DOTweenModulePhysics.cs
                    ├── DOTweenModulePhysics.cs.meta
                    ├── DOTweenModulePhysics2D.cs
                    ├── DOTweenModulePhysics2D.cs.meta
                    ├── DOTweenModuleSprite.cs
                    ├── DOTweenModuleSprite.cs.meta
                    ├── DOTweenModuleUI.cs
                    ├── DOTweenModuleUI.cs.meta
                    ├── DOTweenModuleUIToolkit.cs
                    ├── DOTweenModuleUIToolkit.cs.meta
                    ├── DOTweenModuleUnityVersion.cs
                    ├── DOTweenModuleUnityVersion.cs.meta
                    ├── DOTweenModuleUtils.cs
                    ├── DOTweenModuleUtils.cs.meta
                ├── readme.txt
                ├── readme.txt.meta
    ├── Readme.asset
    ├── Readme.asset.meta
    ├── Resources
    ├── Resources.meta
        ├── DOTweenSettings.asset
        ├── DOTweenSettings.asset.meta
    ├── Scenes
    ├── Scenes.meta
        ├── SampleScene.unity
        ├── SampleScene.unity.meta
    ├── ScienceStreet
    ├── ScienceStreet.meta
        ├── CS
        ├── CS.meta
            ├── Resources
            ├── Resources.meta
            ├── Scripts
            ├── Scripts.meta
                ├── CsBotController.cs
                ├── CsBotController.cs.meta
                ├── CsDemoBuilder.cs
                ├── CsDemoBuilder.cs.meta
                ├── CsGameManager.cs
                ├── CsGameManager.cs.meta
                ├── CsOptionDraggable.cs
                ├── CsOptionDraggable.cs.meta
                ├── CsUIManager.cs
                ├── CsUIManager.cs.meta
                ├── CsWordButton.cs
                ├── CsWordButton.cs.meta
        ├── Fonts
        ├── Fonts.meta
            ├── TSHakwaty-Regular.otf
            ├── TSHakwaty-Regular.otf.meta
        ├── MCQ
        ├── MCQ.meta
            ├── Resources
            ├── Resources.meta
            ├── Scripts
            ├── Scripts.meta
                ├── McqDemoBuilder.cs
                ├── McqDemoBuilder.cs.meta
                ├── McqGameManager.cs
                ├── McqGameManager.cs.meta
                ├── McqGateController.cs
                ├── McqGateController.cs.meta
                ├── McqPlayerRunner.cs
                ├── McqPlayerRunner.cs.meta
                ├── McqQuestionDisplay.cs
                ├── McqQuestionDisplay.cs.meta
                ├── McqUIManager.cs
                ├── McqUIManager.cs.meta
        ├── Shared
        ├── Shared.meta
            ├── Resources
            ├── Resources.meta
            ├── Scripts
            ├── Scripts.meta
                ├── SSApiManager.cs
                ├── SSApiManager.cs.meta
                ├── SSAudioManager.cs
                ├── SSAudioManager.cs.meta
                ├── SSColorPalette.cs
                ├── SSColorPalette.cs.meta
                ├── SSDataModels.cs
                ├── SSDataModels.cs.meta
                ├── SSFontManager.cs
                ├── SSFontManager.cs.meta
                ├── SSGameSession.cs
                ├── SSGameSession.cs.meta
                ├── SSParticleManager.cs
                ├── SSParticleManager.cs.meta
        ├── TF
        ├── TF.meta
            ├── Resources
            ├── Resources.meta
            ├── Scripts
            ├── Scripts.meta
                ├── TfDemoBuilder.cs
                ├── TfDemoBuilder.cs.meta
                ├── TfGameManager.cs
                ├── TfGameManager.cs.meta
                ├── TfHandController.cs
                ├── TfHandController.cs.meta
                ├── TfProductionLine.cs
                ├── TfProductionLine.cs.meta
                ├── TfQuestionScreen.cs
                ├── TfQuestionScreen.cs.meta
                ├── TfUIManager.cs
                ├── TfUIManager.cs.meta
    ├── Settings
    ├── Settings.meta
        ├── DefaultVolumeProfile.asset
        ├── DefaultVolumeProfile.asset.meta
        ├── Mobile_RPAsset.asset
        ├── Mobile_RPAsset.asset.meta
        ├── Mobile_Renderer.asset
        ├── Mobile_Renderer.asset.meta
        ├── PC_RPAsset.asset
        ├── PC_RPAsset.asset.meta
        ├── PC_Renderer.asset
        ├── PC_Renderer.asset.meta
        ├── SampleSceneProfile.asset
        ├── SampleSceneProfile.asset.meta
        ├── UniversalRenderPipelineGlobalSettings.asset
        ├── UniversalRenderPipelineGlobalSettings.asset.meta
    ├── TextMesh Pro
    ├── TextMesh Pro.meta
        ├── Fonts
        ├── Fonts.meta
            ├── LiberationSans - OFL.txt
            ├── LiberationSans - OFL.txt.meta
            ├── LiberationSans.ttf
            ├── LiberationSans.ttf.meta
        ├── Resources
        ├── Resources.meta
            ├── Fonts & Materials
            ├── Fonts & Materials.meta
                ├── LiberationSans SDF - Drop Shadow.mat
                ├── LiberationSans SDF - Drop Shadow.mat.meta
                ├── LiberationSans SDF - Fallback.asset
                ├── LiberationSans SDF - Fallback.asset.meta
                ├── LiberationSans SDF - Outline.mat
                ├── LiberationSans SDF - Outline.mat.meta
                ├── LiberationSans SDF.asset
                ├── LiberationSans SDF.asset.meta
            ├── LineBreaking Following Characters.txt
            ├── LineBreaking Following Characters.txt.meta
            ├── LineBreaking Leading Characters.txt
            ├── LineBreaking Leading Characters.txt.meta
            ├── Sprite Assets
            ├── Sprite Assets.meta
                ├── EmojiOne.asset
                ├── EmojiOne.asset.meta
            ├── Style Sheets
            ├── Style Sheets.meta
                ├── Default Style Sheet.asset
                ├── Default Style Sheet.asset.meta
            ├── TMP Settings.asset
            ├── TMP Settings.asset.meta
        ├── Shaders
        ├── Shaders.meta
            ├── SDFFunctions.hlsl
            ├── SDFFunctions.hlsl.meta
            ├── TMP_Bitmap-Custom-Atlas.shader
            ├── TMP_Bitmap-Custom-Atlas.shader.meta
            ├── TMP_Bitmap-Mobile.shader
            ├── TMP_Bitmap-Mobile.shader.meta
            ├── TMP_Bitmap.shader
            ├── TMP_Bitmap.shader.meta
            ├── TMP_SDF Overlay.shader
            ├── TMP_SDF Overlay.shader.meta
            ├── TMP_SDF SSD.shader
            ├── TMP_SDF SSD.shader.meta
            ├── TMP_SDF-HDRP LIT.shadergraph
            ├── TMP_SDF-HDRP LIT.shadergraph.meta
            ├── TMP_SDF-HDRP UNLIT.shadergraph
            ├── TMP_SDF-HDRP UNLIT.shadergraph.meta
            ├── TMP_SDF-Mobile Masking.shader
            ├── TMP_SDF-Mobile Masking.shader.meta
            ├── TMP_SDF-Mobile Overlay.shader
            ├── TMP_SDF-Mobile Overlay.shader.meta
            ├── TMP_SDF-Mobile SSD.shader
            ├── TMP_SDF-Mobile SSD.shader.meta
            ├── TMP_SDF-Mobile-2-Pass.shader
            ├── TMP_SDF-Mobile-2-Pass.shader.meta
            ├── TMP_SDF-Mobile.shader
            ├── TMP_SDF-Mobile.shader.meta
            ├── TMP_SDF-Surface-Mobile.shader
            ├── TMP_SDF-Surface-Mobile.shader.meta
            ├── TMP_SDF-Surface.shader
            ├── TMP_SDF-Surface.shader.meta
            ├── TMP_SDF-URP Lit.shadergraph
            ├── TMP_SDF-URP Lit.shadergraph.meta
            ├── TMP_SDF-URP Unlit.shadergraph
            ├── TMP_SDF-URP Unlit.shadergraph.meta
            ├── TMP_SDF.shader
            ├── TMP_SDF.shader.meta
            ├── TMP_Sprite.shader
            ├── TMP_Sprite.shader.meta
            ├── TMPro.cginc
            ├── TMPro.cginc.meta
            ├── TMPro_Mobile.cginc
            ├── TMPro_Mobile.cginc.meta
            ├── TMPro_Properties.cginc
            ├── TMPro_Properties.cginc.meta
            ├── TMPro_Surface.cginc
            ├── TMPro_Surface.cginc.meta
        ├── Sprites
        ├── Sprites.meta
            ├── EmojiOne Attribution.txt
            ├── EmojiOne Attribution.txt.meta
            ├── EmojiOne.json
            ├── EmojiOne.json.meta
            ├── EmojiOne.png
            ├── EmojiOne.png.meta
    ├── TutorialInfo
    ├── TutorialInfo.meta
        ├── Icons
        ├── Icons.meta
            ├── URP.png
            ├── URP.png.meta
        ├── Layout.wlt
        ├── Layout.wlt.meta
        ├── Scripts
        ├── Scripts.meta
            ├── Editor
            ├── Editor.meta
                ├── ReadmeEditor.cs
                ├── ReadmeEditor.cs.meta
            ├── Readme.cs
            ├── Readme.cs.meta
    ├── collectcodebase.sh
    ├── collectcodebase.sh.meta
```

---

## File Contents


---

### `ALArcade/ArabicTMP/ArabicCharsets.cs`

> Lines: 42 | Size: 1.4KB

```csharp
using UnityEngine;

namespace ALArcade.ArabicTMP
{
    /// <summary>
    /// Static class providing character sets for Arabic font creation
    /// </summary>
    public static class ArabicCharsets
    {
        /// <summary>
        /// Returns the complete Arabic character set for font asset creation
        /// </summary>
        public static string GetCompleteArabicCharset()
        {
            return
                "0600-06FF\n" +  // Arabic Basic
                "0750-077F\n" +  // Arabic Supplement
                "08A0-08FF\n" +  // Arabic Extended-A
                "FB50-FDFF\n" +  // Arabic Presentation Forms-A
                "FE70-FEFF\n" +  // Arabic Presentation Forms-B
                "0020-007F\n";   // Basic Latin for mixed text
        }

        /// <summary>
        /// Returns the basic Arabic character set for font asset creation (less comprehensive)
        /// </summary>
        public static string GetBasicArabicCharset()
        {
            return
                "0600-06FF\n" +  // Arabic Basic
                "FE70-FEFF\n" +  // Arabic Presentation Forms-B
                "0020-007F\n";   // Basic Latin for mixed text
        }

        /// <summary>
        /// Returns the essential Persian characters (چ پ ژ گ) to add to an Arabic font
        /// </summary>
        public static string GetPersianCharset()
        {
            return "067E, 0686, 0698, 06AF\n"; // Persian letters پ چ ژ گ
        }
    }
}
```

---

### `ALArcade/ArabicTMP/ArabicSupport.cs`

> Lines: 684 | Size: 23.5KB

```csharp
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;

namespace ALArcade.ArabicTMP
{
    /// <summary>
    /// AL-Arcade Ultimate Arabic Support for Unity TextMeshPro
    /// FINAL VERSION - All issues fixed
    /// </summary>
    public static class ArabicSupport
    {

        #region Unicode Ranges

        // Arabic Unicode blocks
        private const int ARABIC_BASIC_START = 0x0600;
        private const int ARABIC_BASIC_END = 0x06FF;
        private const int ARABIC_SUPPLEMENT_START = 0x0750;
        private const int ARABIC_SUPPLEMENT_END = 0x077F;
        private const int ARABIC_EXTENDED_A_START = 0x08A0;
        private const int ARABIC_EXTENDED_A_END = 0x08FF;
        private const int ARABIC_PRESENTATION_A_START = 0xFB50;
        private const int ARABIC_PRESENTATION_A_END = 0xFDFF;
        private const int ARABIC_PRESENTATION_B_START = 0xFE70;
        private const int ARABIC_PRESENTATION_B_END = 0xFEFF;

        // Special control characters
        private const char ZWJ = (char)0x200D;
        private const char ZWNJ = (char)0x200C;
        private const char RLM = (char)0x200F;
        private const char LRM = (char)0x200E;
        private const char ALM = (char)0x061C;

        #endregion

        #region Enums

        private enum LetterForm
        {
            Isolated = 0,
            Initial = 1,
            Medial = 2,
            Final = 3
        }

        private enum ConnectionType
        {
            None,
            Right,
            Dual,
            Transparent
        }

        private enum TextDirection
        {
            LTR,
            RTL,
            Neutral
        }

        #endregion

        #region Character Mappings

        private static readonly Dictionary<char, char[]> ArabicGlyphMap = new Dictionary<char, char[]>() {
            // Basic Arabic Letters
            { 'ا', new char[] { 'ا', 'ا', '\uFE8E', '\uFE8E' } },
            { 'ب', new char[] { 'ب', '\uFE91', '\uFE92', '\uFE90' } },
            { 'ت', new char[] { 'ت', '\uFE97', '\uFE98', '\uFE96' } },
            { 'ث', new char[] { 'ث', '\uFE9B', '\uFE9C', '\uFE9A' } },
            { 'ج', new char[] { 'ج', '\uFE9F', '\uFEA0', '\uFE9E' } },
            { 'ح', new char[] { 'ح', '\uFEA3', '\uFEA4', '\uFEA2' } },
            { 'خ', new char[] { 'خ', '\uFEA7', '\uFEA8', '\uFEA6' } },
            { 'د', new char[] { 'د', 'د', '\uFEAA', '\uFEAA' } },
            { 'ذ', new char[] { 'ذ', 'ذ', '\uFEAC', '\uFEAC' } },
            { 'ر', new char[] { 'ر', 'ر', '\uFEAE', '\uFEAE' } },
            { 'ز', new char[] { 'ز', 'ز', '\uFEB0', '\uFEB0' } },
            { 'س', new char[] { 'س', '\uFEB3', '\uFEB4', '\uFEB2' } },
            { 'ش', new char[] { 'ش', '\uFEB7', '\uFEB8', '\uFEB6' } },
            { 'ص', new char[] { 'ص', '\uFEBB', '\uFEBC', '\uFEBA' } },
            { 'ض', new char[] { 'ض', '\uFEBF', '\uFEC0', '\uFEBE' } },
            { 'ط', new char[] { 'ط', '\uFEC3', '\uFEC4', '\uFEC2' } },
            { 'ظ', new char[] { 'ظ', '\uFEC7', '\uFEC8', '\uFEC6' } },
            { 'ع', new char[] { 'ع', '\uFECB', '\uFECC', '\uFECA' } },
            { 'غ', new char[] { 'غ', '\uFECF', '\uFED0', '\uFECE' } },
            { 'ف', new char[] { 'ف', '\uFED3', '\uFED4', '\uFED2' } },
            { 'ق', new char[] { 'ق', '\uFED7', '\uFED8', '\uFED6' } },
            { 'ك', new char[] { 'ك', '\uFEDB', '\uFEDC', '\uFEDA' } },
            { 'ل', new char[] { 'ل', '\uFEDF', '\uFEE0', '\uFEDE' } },
            { 'م', new char[] { 'م', '\uFEE3', '\uFEE4', '\uFEE2' } },
            { 'ن', new char[] { 'ن', '\uFEE7', '\uFEE8', '\uFEE6' } },
            { 'ه', new char[] { 'ه', '\uFEEB', '\uFEEC', '\uFEEA' } },
            { 'و', new char[] { 'و', 'و', '\uFEEE', '\uFEEE' } },
            { 'ي', new char[] { 'ي', '\uFEF3', '\uFEF4', '\uFEF2' } },
            { 'ى', new char[] { 'ى', 'ى', '\uFEF0', '\uFEF0' } },
            { 'ة', new char[] { 'ة', 'ة', '\uFE94', '\uFE94' } },
            { 'ء', new char[] { 'ء', 'ء', 'ء', 'ء' } },
            
            // Alef variants
            { 'آ', new char[] { 'آ', 'آ', '\uFE82', '\uFE82' } },
            { 'أ', new char[] { 'أ', 'أ', '\uFE84', '\uFE84' } },
            { 'إ', new char[] { 'إ', 'إ', '\uFE88', '\uFE88' } },
            { 'ؤ', new char[] { 'ؤ', 'ؤ', '\uFE86', '\uFE86' } },
            { 'ئ', new char[] { 'ئ', '\uFE8B', '\uFE8C', '\uFE8A' } },
            
            // Persian/Urdu letters
            { 'پ', new char[] { 'پ', '\uFB58', '\uFB59', '\uFB57' } },
            { 'چ', new char[] { 'چ', '\uFB7C', '\uFB7D', '\uFB7B' } },
            { 'ژ', new char[] { 'ژ', 'ژ', '\uFB8B', '\uFB8B' } },
            { 'گ', new char[] { 'گ', '\uFB94', '\uFB95', '\uFB93' } },
            { 'ک', new char[] { 'ک', '\uFB90', '\uFB91', '\uFB8F' } },
            { 'ی', new char[] { 'ی', '\uFBFE', '\uFBFF', '\uFBFD' } }
        };

        private static readonly Dictionary<string, char[]> LamAlefLigatures = new Dictionary<string, char[]>() {
            { "لا", new char[] { '\uFEFB', '\uFEFC' } },
            { "لأ", new char[] { '\uFEF7', '\uFEF8' } },
            { "لإ", new char[] { '\uFEF9', '\uFEFA' } },
            { "لآ", new char[] { '\uFEF5', '\uFEF6' } }
        };

        // FIXED: Lam-Alef ligatures also don't connect forward
        private static readonly HashSet<char> RightConnectingOnly = new HashSet<char> {
            'ا', 'آ', 'أ', 'إ', 'ى',    // Alef family
            'د', 'ذ',                    // Dal family
            'ر', 'ز', 'ژ',              // Reh family
            'و', 'ؤ',                    // Waw family
            'ة',                         // Teh Marbuta
            'ء',                         // Hamza
            // CRITICAL FIX: Add Lam-Alef ligatures
            '\uFEFB', '\uFEFC',         // لا ligatures
            '\uFEF7', '\uFEF8',         // لأ ligatures
            '\uFEF9', '\uFEFA',         // لإ ligatures
            '\uFEF5', '\uFEF6'          // لآ ligatures
        };

        private static readonly HashSet<char> Diacritics = new HashSet<char> {
            '\u064B', '\u064C', '\u064D', '\u064E', '\u064F', '\u0650', '\u0651', '\u0652',
            '\u0653', '\u0654', '\u0655', '\u0656', '\u0657', '\u0658', '\u0659', '\u065A',
            '\u065B', '\u065C', '\u065D', '\u065E', '\u065F', '\u0670'
        };

        #endregion

        #region Public API

        public static string Fix(string originalText, bool showTashkeel = true, bool preserveNumbers = true,
                                bool fixTags = true, bool forceRTL = true)
        {

            if (string.IsNullOrEmpty(originalText))
            {
                return originalText;
            }

            try
            {
                List<TagInfo> tags = new List<TagInfo>();
                string textWithoutTags = originalText;

                if (fixTags)
                {
                    textWithoutTags = ExtractTMPTags(originalText, tags);
                }

                bool containsArabic = ContainsArabicCharacters(textWithoutTags);

                if (!containsArabic && !forceRTL)
                {
                    return originalText;
                }

                string processedText = ProcessArabicText(textWithoutTags, showTashkeel, preserveNumbers, forceRTL);

                if (fixTags && tags.Count > 0)
                {
                    processedText = ReinsertTMPTags(processedText, tags);
                }

                return processedText;
            }
            catch (Exception ex)
            {
                Debug.LogError($"[AL-Arcade Arabic] Error processing text: {ex.Message}");
                return originalText;
            }
        }

        public static void LogFontRecommendations()
        {
            Debug.Log("[AL-Arcade Arabic] 🎯 RECOMMENDED ARABIC FONTS FOR UNITY:\n" +
                     "✅ Noto Sans Arabic (Google Fonts - Free & Excellent)\n" +
                     "✅ Amiri (Free - Perfect for traditional Arabic)\n" +
                     "✅ Cairo (Google Fonts - Modern & Clean)\n" +
                     "✅ Almarai (Google Fonts - Great for UI)\n" +
                     "Download from: fonts.google.com");
        }

        #endregion

        #region Core Processing

        private static string ProcessArabicText(string text, bool showTashkeel, bool preserveNumbers, bool forceRTL)
        {
            if (string.IsNullOrEmpty(text)) return text;

            List<CharInfo> chars = AnalyzeCharacters(text);

            if (!showTashkeel)
            {
                chars.RemoveAll(c => c.IsDiacritic);
            }

            if (!preserveNumbers)
            {
                ConvertToArabicNumerals(chars);
            }

            ProcessLamAlefLigatures(chars);
            ApplyArabicShaping(chars);
            string result = HandleBidirectionalText(chars, forceRTL);

            return result;
        }

        private static List<CharInfo> AnalyzeCharacters(string text)
        {
            List<CharInfo> chars = new List<CharInfo>(text.Length);

            for (int i = 0; i < text.Length; i++)
            {
                char c = text[i];
                CharInfo charInfo = new CharInfo
                {
                    OriginalChar = c,
                    ProcessedChar = c,
                    IsArabic = IsArabicChar(c),
                    IsDiacritic = IsDiacritic(c),
                    IsLatin = IsLatinChar(c),
                    IsDigit = char.IsDigit(c),
                    IsWhitespace = char.IsWhiteSpace(c),
                    IsPunctuation = char.IsPunctuation(c),
                    ConnectionType = GetConnectionType(c),
                    Direction = GetCharDirection(c)
                };
                chars.Add(charInfo);
            }

            return chars;
        }

        private static void ProcessLamAlefLigatures(List<CharInfo> chars)
        {
            for (int i = 0; i < chars.Count - 1; i++)
            {
                if (chars[i].OriginalChar == 'ل')
                {
                    int alefIndex = FindNextNonDiacriticIndex(chars, i);
                    if (alefIndex != -1)
                    {
                        char alefChar = chars[alefIndex].OriginalChar;
                        string lamAlefKey = "ل" + alefChar;

                        if (LamAlefLigatures.ContainsKey(lamAlefKey))
                        {
                            bool isConnectedToPrevious = CanConnectToPrevious(chars, i);
                            char ligature = LamAlefLigatures[lamAlefKey][isConnectedToPrevious ? 1 : 0];

                            chars[i].ProcessedChar = ligature;
                            chars[i].IsLigature = true;
                            // CRITICAL FIX: Mark ligature as right-connecting only
                            chars[i].ConnectionType = ConnectionType.Right;

                            chars[alefIndex].ShouldRemove = true;
                        }
                    }
                }
            }

            chars.RemoveAll(c => c.ShouldRemove);
        }

        private static void ApplyArabicShaping(List<CharInfo> chars)
        {
            for (int i = 0; i < chars.Count; i++)
            {
                CharInfo charInfo = chars[i];

                if (!charInfo.IsArabic || charInfo.IsDiacritic || charInfo.IsLigature)
                {
                    continue;
                }

                char baseChar = charInfo.OriginalChar;
                if (ArabicGlyphMap.ContainsKey(baseChar))
                {
                    LetterForm form = DetermineLetterForm(chars, i);
                    charInfo.ProcessedChar = ArabicGlyphMap[baseChar][(int)form];
                }
            }
        }

        // COMPLETELY FIXED: Bidirectional text handling
        private static string HandleBidirectionalText(List<CharInfo> chars, bool forceRTL)
        {
            if (chars.Count == 0) return "";

            List<TextRun> runs = CreateDirectionalRuns(chars);
            bool isOverallRTL = forceRTL || IsPrimarilyArabic(chars);

            StringBuilder result = new StringBuilder();

            if (isOverallRTL)
            {
                // RTL Context: Reverse run order, but NEVER reverse English internally
                for (int i = runs.Count - 1; i >= 0; i--)
                {
                    TextRun run = runs[i];

                    if (run.IsRTL)
                    {
                        // Arabic: Keep in logical order (already shaped correctly)
                        foreach (var charInfo in run.Characters)
                        {
                            result.Append(charInfo.ProcessedChar);
                        }
                    }
                    else
                    {
                        // CRITICAL FIX: English in RTL context - NEVER reverse
                        foreach (var charInfo in run.Characters)
                        {
                            result.Append(charInfo.ProcessedChar);
                        }
                    }
                }
            }
            else
            {
                // LTR Context
                foreach (var run in runs)
                {
                    if (run.IsRTL)
                    {
                        // Arabic in LTR context - reverse
                        for (int i = run.Characters.Count - 1; i >= 0; i--)
                        {
                            result.Append(run.Characters[i].ProcessedChar);
                        }
                    }
                    else
                    {
                        // English in LTR context - keep normal
                        foreach (var charInfo in run.Characters)
                        {
                            result.Append(charInfo.ProcessedChar);
                        }
                    }
                }
            }

            return result.ToString();
        }

        // IMPROVED: Better run creation
        private static List<TextRun> CreateDirectionalRuns(List<CharInfo> chars)
        {
            List<TextRun> runs = new List<TextRun>();
            if (chars.Count == 0) return runs;

            TextRun currentRun = null;

            for (int i = 0; i < chars.Count; i++)
            {
                CharInfo charInfo = chars[i];

                bool isRTL = charInfo.Direction == TextDirection.RTL;
                bool isLTR = charInfo.Direction == TextDirection.LTR;
                bool isNeutral = charInfo.Direction == TextDirection.Neutral;

                if (currentRun == null)
                {
                    currentRun = new TextRun
                    {
                        IsRTL = isRTL,
                        Characters = new List<CharInfo> { charInfo }
                    };
                }
                else if (isNeutral)
                {
                    // Neutral characters join current run
                    currentRun.Characters.Add(charInfo);
                }
                else if (isRTL == currentRun.IsRTL)
                {
                    // Same direction
                    currentRun.Characters.Add(charInfo);
                }
                else
                {
                    // Direction change
                    runs.Add(currentRun);
                    currentRun = new TextRun
                    {
                        IsRTL = isRTL,
                        Characters = new List<CharInfo> { charInfo }
                    };
                }
            }

            if (currentRun != null)
            {
                runs.Add(currentRun);
            }

            return runs;
        }

        #endregion

        #region Helper Methods

        private static LetterForm DetermineLetterForm(List<CharInfo> chars, int index)
        {
            bool canConnectToPrevious = CanConnectToPrevious(chars, index);
            bool canConnectToNext = CanConnectToNext(chars, index);

            if (canConnectToPrevious && canConnectToNext)
            {
                return LetterForm.Medial;
            }
            else if (canConnectToPrevious)
            {
                return LetterForm.Final;
            }
            else if (canConnectToNext)
            {
                return LetterForm.Initial;
            }
            else
            {
                return LetterForm.Isolated;
            }
        }

        private static bool CanConnectToPrevious(List<CharInfo> chars, int index)
        {
            if (index <= 0) return false;

            for (int i = index - 1; i >= 0; i--)
            {
                if (chars[i].IsDiacritic) continue;

                // Check original character AND processed character for right-connecting-only
                bool isRightConnectingOnly = RightConnectingOnly.Contains(chars[i].OriginalChar) ||
                                           RightConnectingOnly.Contains(chars[i].ProcessedChar);

                return chars[i].IsArabic &&
                       chars[i].ConnectionType != ConnectionType.None &&
                       !isRightConnectingOnly;
            }
            return false;
        }

        private static bool CanConnectToNext(List<CharInfo> chars, int index)
        {
            if (index >= chars.Count - 1) return false;

            // CRITICAL FIX: Check both original and processed character
            bool isRightConnectingOnly = RightConnectingOnly.Contains(chars[index].OriginalChar) ||
                                       RightConnectingOnly.Contains(chars[index].ProcessedChar);

            if (isRightConnectingOnly)
            {
                return false;
            }

            for (int i = index + 1; i < chars.Count; i++)
            {
                if (chars[i].IsDiacritic) continue;

                return chars[i].IsArabic && chars[i].ConnectionType != ConnectionType.None;
            }
            return false;
        }

        private static int FindNextNonDiacriticIndex(List<CharInfo> chars, int startIndex)
        {
            for (int i = startIndex + 1; i < chars.Count; i++)
            {
                if (!chars[i].IsDiacritic)
                {
                    return i;
                }
            }
            return -1;
        }

        private static ConnectionType GetConnectionType(char c)
        {
            if (IsDiacritic(c)) return ConnectionType.Transparent;
            if (!IsArabicChar(c)) return ConnectionType.None;
            if (c == 'ء') return ConnectionType.None;
            if (RightConnectingOnly.Contains(c)) return ConnectionType.Right;
            return ConnectionType.Dual;
        }

        private static TextDirection GetCharDirection(char c)
        {
            if (IsArabicChar(c)) return TextDirection.RTL;
            if (IsLatinChar(c)) return TextDirection.LTR;
            return TextDirection.Neutral;
        }

        private static bool IsPrimarilyArabic(List<CharInfo> chars)
        {
            int arabicCount = 0;
            int totalCount = 0;

            foreach (var c in chars)
            {
                if (!c.IsWhitespace && !c.IsDiacritic)
                {
                    totalCount++;
                    if (c.IsArabic) arabicCount++;
                }
            }

            return totalCount > 0 && (arabicCount * 2 > totalCount);
        }

        private static void ConvertToArabicNumerals(List<CharInfo> chars)
        {
            char[] westernNumerals = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
            char[] arabicNumerals = { '٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩' };

            for (int i = 0; i < chars.Count; i++)
            {
                if (chars[i].IsDigit)
                {
                    for (int j = 0; j < westernNumerals.Length; j++)
                    {
                        if (chars[i].OriginalChar == westernNumerals[j])
                        {
                            chars[i].ProcessedChar = arabicNumerals[j];
                            break;
                        }
                    }
                }
            }
        }

        #endregion

        #region Character Classification

        private static bool ContainsArabicCharacters(string text)
        {
            foreach (char c in text)
            {
                if (IsArabicChar(c)) return true;
            }
            return false;
        }

        private static bool IsArabicChar(char c)
        {
            int code = (int)c;
            return (code >= ARABIC_BASIC_START && code <= ARABIC_BASIC_END) ||
                   (code >= ARABIC_SUPPLEMENT_START && code <= ARABIC_SUPPLEMENT_END) ||
                   (code >= ARABIC_EXTENDED_A_START && code <= ARABIC_EXTENDED_A_END) ||
                   (code >= ARABIC_PRESENTATION_A_START && code <= ARABIC_PRESENTATION_A_END) ||
                   (code >= ARABIC_PRESENTATION_B_START && code <= ARABIC_PRESENTATION_B_END);
        }

        private static bool IsLatinChar(char c)
        {
            return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
        }

        private static bool IsDiacritic(char c)
        {
            return Diacritics.Contains(c);
        }

        #endregion

        #region TextMeshPro Tag Handling

        private static string ExtractTMPTags(string text, List<TagInfo> tags)
        {
            StringBuilder result = new StringBuilder();
            bool inTag = false;
            StringBuilder tagBuilder = new StringBuilder();

            for (int i = 0; i < text.Length; i++)
            {
                char c = text[i];

                if (c == '<' && !inTag)
                {
                    inTag = true;
                    tagBuilder.Clear();
                    tagBuilder.Append(c);
                    continue;
                }

                if (inTag)
                {
                    tagBuilder.Append(c);
                    if (c == '>')
                    {
                        inTag = false;
                        tags.Add(new TagInfo
                        {
                            Tag = tagBuilder.ToString(),
                            Position = result.Length
                        });
                    }
                    continue;
                }

                result.Append(c);
            }

            if (inTag)
            {
                result.Append(tagBuilder);
            }

            return result.ToString();
        }

        private static string ReinsertTMPTags(string text, List<TagInfo> tags)
        {
            if (tags.Count == 0) return text;

            StringBuilder result = new StringBuilder(text);

            for (int i = tags.Count - 1; i >= 0; i--)
            {
                int pos = Math.Min(tags[i].Position, result.Length);
                result.Insert(pos, tags[i].Tag);
            }

            return result.ToString();
        }

        #endregion

        #region Data Classes

        private class CharInfo
        {
            public char OriginalChar;
            public char ProcessedChar;
            public bool IsArabic;
            public bool IsDiacritic;
            public bool IsLatin;
            public bool IsDigit;
            public bool IsWhitespace;
            public bool IsPunctuation;
            public bool IsLigature;
            public bool ShouldRemove;
            public ConnectionType ConnectionType;
            public TextDirection Direction;
        }

        private class TextRun
        {
            public bool IsRTL;
            public List<CharInfo> Characters = new List<CharInfo>();
        }

        private class TagInfo
        {
            public string Tag;
            public int Position;
        }

        #endregion
    }
}
```

---

### `ALArcade/ArabicTMP/ArabicTMPInputField.cs`

> Lines: 129 | Size: 4.3KB

```csharp
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;

namespace ALArcade.ArabicTMP
{
    [AddComponentMenu("UI/Arabic TextMeshPro - Input Field", 12)]
    public class ArabicTMPInputField : TMP_InputField
    {
        [SerializeField] private bool m_ShowTashkeel = true;
        [SerializeField] private bool m_PreserveNumbers = true;
        [SerializeField] private bool m_FixTags = true;
        [SerializeField] private bool m_ForceRTL = true; // FIXED: Default to true for better Arabic support

        private string m_OriginalText = "";
        private bool m_ProcessingInternalChange = false;

        // Shadow the base .text property
        public new string text
        {
            get => m_OriginalText;
            set
            {
                if (m_OriginalText == value)
                    return;

                m_OriginalText = value;
                m_ProcessingInternalChange = true;
                var processed = ArabicSupport.Fix(value, m_ShowTashkeel, m_PreserveNumbers, m_FixTags, m_ForceRTL);
                base.SetTextWithoutNotify(processed);
                m_ProcessingInternalChange = false;
            }
        }

        protected override void Awake()
        {
            base.Awake();

            if (textComponent != null)
            {
                // FIXED: Ensure text component is properly set up for RTL
                textComponent.isRightToLeftText = true;
                textComponent.alignment = TextAlignmentOptions.Right;
            }

            // Ensure text viewport has correct layout for RTL
            if (textViewport != null)
            {
                // Adjust padding to make sure text appears correctly aligned
                textComponent.margin = new Vector4(0, 0, 5, 0); // Add right margin for RTL text
            }
        }

        protected override void Start()
        {
            base.Start();
            onValueChanged.AddListener(OnValueChangedHandler);

            if (!string.IsNullOrEmpty(m_OriginalText))
                text = m_OriginalText;
        }

        private void OnValueChangedHandler(string value)
        {
            // Only update our stored original if this wasn't triggered by our own setText
            if (!m_ProcessingInternalChange)
            {
                m_OriginalText = value;
            }
        }

        public override void OnSelect(BaseEventData eventData)
        {
            base.OnSelect(eventData);

            // When selected, ensure caretPosition is at the proper end for RTL
            if (caretPosition == 0 && m_OriginalText.Length > 0)
            {
                caretPosition = 0; // For RTL, visual position 0 is at the right side
            }
        }

        public override void OnDeselect(BaseEventData eventData)
        {
            base.OnDeselect(eventData);

            // Ensure text is properly formatted on deselection
            text = m_OriginalText;
        }

        // Override LateUpdate to re-process text before each frame render
        protected override void LateUpdate()
        {
            base.LateUpdate();

            // Apply Arabic shaping if the text changed and we're not in the middle of our own change
            if (!m_ProcessingInternalChange)
            {
                var processed = ArabicSupport.Fix(m_OriginalText, m_ShowTashkeel, m_PreserveNumbers, m_FixTags, m_ForceRTL);
                if (base.text != processed)
                {
                    m_ProcessingInternalChange = true;
                    base.SetTextWithoutNotify(processed);
                    m_ProcessingInternalChange = false;
                }
            }
        }

#if UNITY_EDITOR
        protected override void OnValidate()
        {
            base.OnValidate();

            if (!string.IsNullOrEmpty(m_OriginalText))
            {
                m_ProcessingInternalChange = true;
                var processed = ArabicSupport.Fix(m_OriginalText, m_ShowTashkeel, m_PreserveNumbers, m_FixTags, m_ForceRTL);
                base.SetTextWithoutNotify(processed);
                m_ProcessingInternalChange = false;
            }

            if (textComponent != null)
            {
                textComponent.isRightToLeftText = true;
                textComponent.alignment = TextAlignmentOptions.Right;
            }
        }
#endif
    }
}
```

---

### `ALArcade/ArabicTMP/ArabicTextMeshPro.cs`

> Lines: 164 | Size: 5.3KB

```csharp
using TMPro;
using UnityEngine;

namespace ALArcade.ArabicTMP
{
    /// <summary>
    /// Arabic-enabled TextMeshPro component for 3D/World space
    /// </summary>
    [AddComponentMenu("Text/Arabic TextMeshPro - Text", 10)]
    public class ArabicTextMeshPro : TextMeshPro
    {
        // Original (editable) Arabic text
        [SerializeField] private string m_ArabicText;

        // Plugin options
        [Tooltip("Show Arabic diacritics (tashkeel)")]
        [SerializeField] private bool m_ShowTashkeel = true;

        [Tooltip("Keep Western digits (123) as-is instead of converting to Eastern Arabic")]
        [SerializeField] private bool m_PreserveNumbers = true;

        [Tooltip("Process rich text tags (color, bold, etc.)")]
        [SerializeField] private bool m_FixTags = true;

        [Tooltip("Force RTL even if text starts with Latin letters")]
        [SerializeField] private bool m_ForceRTL = true; // FIXED: Default to true for better Arabic support

        /// <summary>
        /// The original Arabic text (not fixed for rendering)
        /// </summary>
        public string arabicText
        {
            get { return m_ArabicText; }
            set
            {
                if (m_ArabicText == value)
                    return;

                m_ArabicText = value;

                // Apply Arabic text processing and update TMP text
                text = ArabicSupport.Fix(m_ArabicText, m_ShowTashkeel, m_PreserveNumbers, m_FixTags, m_ForceRTL);
            }
        }

        /// <summary>
        /// Show or hide Arabic diacritics (tashkeel)
        /// </summary>
        public bool showTashkeel
        {
            get { return m_ShowTashkeel; }
            set
            {
                if (m_ShowTashkeel == value)
                    return;

                m_ShowTashkeel = value;

                // Re-process text with new setting
                if (!string.IsNullOrEmpty(m_ArabicText))
                    text = ArabicSupport.Fix(m_ArabicText, m_ShowTashkeel, m_PreserveNumbers, m_FixTags, m_ForceRTL);
            }
        }

        /// <summary>
        /// Preserve Western digits (123) as-is
        /// </summary>
        public bool preserveNumbers
        {
            get { return m_PreserveNumbers; }
            set
            {
                if (m_PreserveNumbers == value)
                    return;

                m_PreserveNumbers = value;

                // Re-process text with new setting
                if (!string.IsNullOrEmpty(m_ArabicText))
                    text = ArabicSupport.Fix(m_ArabicText, m_ShowTashkeel, m_PreserveNumbers, m_FixTags, m_ForceRTL);
            }
        }

        /// <summary>
        /// Process rich text tags (color, bold, etc.)
        /// </summary>
        public bool fixTags
        {
            get { return m_FixTags; }
            set
            {
                if (m_FixTags == value)
                    return;

                m_FixTags = value;

                // Re-process text with new setting
                if (!string.IsNullOrEmpty(m_ArabicText))
                    text = ArabicSupport.Fix(m_ArabicText, m_ShowTashkeel, m_PreserveNumbers, m_FixTags, m_ForceRTL);
            }
        }

        /// <summary>
        /// Force RTL even if text starts with Latin letters
        /// </summary>
        public bool forceRTL
        {
            get { return m_ForceRTL; }
            set
            {
                if (m_ForceRTL == value)
                    return;

                m_ForceRTL = value;

                // Re-process text with new setting
                if (!string.IsNullOrEmpty(m_ArabicText))
                    text = ArabicSupport.Fix(m_ArabicText, m_ShowTashkeel, m_PreserveNumbers, m_FixTags, m_ForceRTL);
            }
        }

#if UNITY_EDITOR
        protected override void OnValidate()
        {
            base.OnValidate();

            // Update displayed text whenever properties change in the editor
            if (!string.IsNullOrEmpty(m_ArabicText))
            {
                text = ArabicSupport.Fix(m_ArabicText, m_ShowTashkeel, m_PreserveNumbers, m_FixTags, m_ForceRTL);
            }

            // Set alignment to right by default for Arabic text
            if (alignment == TextAlignmentOptions.TopLeft ||
                alignment == TextAlignmentOptions.Left ||
                alignment == TextAlignmentOptions.BottomLeft)
            {
                alignment = TextAlignmentOptions.Right;
            }
        }
#endif

        protected override void Awake()
        {
            base.Awake();

            // Initialize with proper alignment for RTL text
            if (alignment == TextAlignmentOptions.TopLeft)
                alignment = TextAlignmentOptions.TopRight;
            else if (alignment == TextAlignmentOptions.Left)
                alignment = TextAlignmentOptions.Right;
            else if (alignment == TextAlignmentOptions.BottomLeft)
                alignment = TextAlignmentOptions.BottomRight;

            // Set RTL flag (for TMP's own RTL awareness)
            isRightToLeftText = true;

            // Initial text processing
            if (!string.IsNullOrEmpty(m_ArabicText))
            {
                text = ArabicSupport.Fix(m_ArabicText, m_ShowTashkeel, m_PreserveNumbers, m_FixTags, m_ForceRTL);
            }
        }
    }
}
```

---

### `ALArcade/ArabicTMP/ArabicTextMeshProUGUI.cs`

> Lines: 104 | Size: 2.8KB

```csharp
using TMPro;
using UnityEngine;

namespace ALArcade.ArabicTMP
{
    [AddComponentMenu("UI/Arabic TextMeshPro - Text (UI)", 11)]
    public class ArabicTextMeshProUGUI : TextMeshProUGUI
    {
        [Header("Arabic Settings")]
        [SerializeField] private string m_ArabicText = "";
        [SerializeField] private bool m_ShowTashkeel = true;
        [SerializeField] private bool m_PreserveNumbers = true;
        [SerializeField] private bool m_FixTags = true;
        [SerializeField] private bool m_ForceRTL = true;

        public string arabicText
        {
            get => m_ArabicText;
            set
            {
                if (m_ArabicText == value) return;
                m_ArabicText = value;
                UpdateDisplayText();
            }
        }

        public bool showTashkeel
        {
            get => m_ShowTashkeel;
            set
            {
                if (m_ShowTashkeel == value) return;
                m_ShowTashkeel = value;
                UpdateDisplayText();
            }
        }

        public bool preserveNumbers
        {
            get => m_PreserveNumbers;
            set
            {
                if (m_PreserveNumbers == value) return;
                m_PreserveNumbers = value;
                UpdateDisplayText();
            }
        }

        public bool fixTags
        {
            get => m_FixTags;
            set
            {
                if (m_FixTags == value) return;
                m_FixTags = value;
                UpdateDisplayText();
            }
        }

        public bool forceRTL
        {
            get => m_ForceRTL;
            set
            {
                if (m_ForceRTL == value) return;
                m_ForceRTL = value;
                UpdateDisplayText();
            }
        }

        private void UpdateDisplayText()
        {
            if (!string.IsNullOrEmpty(m_ArabicText))
            {
                text = ArabicSupport.Fix(m_ArabicText, m_ShowTashkeel, m_PreserveNumbers, m_FixTags, m_ForceRTL);
            }
        }

        protected override void Awake()
        {
            base.Awake();

            // Set RTL properties
            isRightToLeftText = true;

            // Set default alignment for Arabic
            if (alignment == TextAlignmentOptions.TopLeft)
                alignment = TextAlignmentOptions.TopRight;
            else if (alignment == TextAlignmentOptions.Left)
                alignment = TextAlignmentOptions.Right;
            else if (alignment == TextAlignmentOptions.BottomLeft)
                alignment = TextAlignmentOptions.BottomRight;

            UpdateDisplayText();
        }

#if UNITY_EDITOR
        protected override void OnValidate()
        {
            base.OnValidate();
            UpdateDisplayText();
        }
#endif
    }
}
```

---

### `ALArcade/ArabicTMP/Editor/ArabicFontAssetCreator.cs`

> Lines: 208 | Size: 8.4KB

```csharp
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using TMPro;
using TMPro.EditorUtilities;
using System.IO;
using System.Text;

namespace ALArcade.ArabicTMP.Editor
{
    public class ArabicFontAssetCreator : EditorWindow
    {
        private Font sourceFont;
        private string savePath = "Assets/";
        private string saveFileName = "New Arabic Font Asset";
        private int samplingPointSize = 90;
        private int atlasPadding = 9;
        private int atlasWidth = 1024;
        private int atlasHeight = 1024;
        private bool includeArabicBasic = true;
        private bool includeArabicSupplement = true;
        private bool includeArabicExtendedA = true;
        private bool includeArabicPresentationA = true;
        private bool includeArabicPresentationB = true;
        private bool includePersian = true;

        private TMP_FontAsset createdFontAsset;
        private readonly string[] arabicRanges = new string[] {
            // Basic Arabic
            "0600-06FF",
            // Arabic Supplement
            "0750-077F",
            // Arabic Extended-A
            "08A0-08FF",
            // Arabic Presentation Forms-A
            "FB50-FDFF",
            // Arabic Presentation Forms-B
            "FE70-FEFF",
            // Persian Characters
            "067E, 0686, 0698, 06AF"
        };

        [MenuItem("Window/AL-Arcade/Arabic TMP/Font Asset Creator")]
        public static void ShowWindow()
        {
            EditorWindow.GetWindow<ArabicFontAssetCreator>("Arabic Font Asset Creator");
        }

        private void OnGUI()
        {
            GUILayout.Label("Create Arabic TextMeshPro Font Asset", EditorStyles.boldLabel);
            EditorGUILayout.Space();

            // Source font selection
            sourceFont = (Font)EditorGUILayout.ObjectField("Source Font File", sourceFont, typeof(Font), false);

            // Atlas settings
            EditorGUILayout.Space();
            EditorGUILayout.LabelField("Atlas Settings", EditorStyles.boldLabel);
            samplingPointSize = EditorGUILayout.IntField("Sampling Point Size", samplingPointSize);
            atlasPadding = EditorGUILayout.IntField("Atlas Padding", atlasPadding);
            atlasWidth = EditorGUILayout.IntField("Atlas Width", atlasWidth);
            atlasHeight = EditorGUILayout.IntField("Atlas Height", atlasHeight);

            // Character sets
            EditorGUILayout.Space();
            EditorGUILayout.LabelField("Character Sets to Include", EditorStyles.boldLabel);
            includeArabicBasic = EditorGUILayout.Toggle("Arabic Basic (0600-06FF)", includeArabicBasic);
            includeArabicSupplement = EditorGUILayout.Toggle("Arabic Supplement (0750-077F)", includeArabicSupplement);
            includeArabicExtendedA = EditorGUILayout.Toggle("Arabic Extended-A (08A0-08FF)", includeArabicExtendedA);
            includeArabicPresentationA = EditorGUILayout.Toggle("Arabic Presentation Forms-A (FB50-FDFF)", includeArabicPresentationA);
            includeArabicPresentationB = EditorGUILayout.Toggle("Arabic Presentation Forms-B (FE70-FEFF)", includeArabicPresentationB);
            includePersian = EditorGUILayout.Toggle("Persian Letters (پ چ ژ گ)", includePersian);

            // Save settings
            EditorGUILayout.Space();
            EditorGUILayout.LabelField("Save Settings", EditorStyles.boldLabel);
            savePath = EditorGUILayout.TextField("Save Path", savePath);
            saveFileName = EditorGUILayout.TextField("Asset Name", saveFileName);

            // Create button
            EditorGUILayout.Space();

            if (sourceFont == null)
            {
                EditorGUILayout.HelpBox("A Source Font File is required to create a font asset.", MessageType.Error);
            }

            EditorGUI.BeginDisabledGroup(sourceFont == null);
            if (GUILayout.Button("Create Font Asset"))
            {
                GenerateFontAsset();
            }
            EditorGUI.EndDisabledGroup();

            // Show the created font asset
            EditorGUILayout.Space();
            if (createdFontAsset != null)
            {
                EditorGUILayout.ObjectField("Created Font Asset", createdFontAsset, typeof(TMP_FontAsset), false);
            }

            // Help box
            EditorGUILayout.Space();
            EditorGUILayout.HelpBox(
                "This utility creates a TextMeshPro Font Asset with all necessary Arabic Unicode ranges.\n\n" +
                "1. Select a source font that contains Arabic glyphs.\n" +
                "2. Choose which character sets to include.\n" +
                "3. Set the save location and asset name.\n" +
                "4. Click 'Create Font Asset'.\n\n" +
                "The created asset will be ready for use with the Arabic TMP components.",
                MessageType.Info);
        }

        private void GenerateFontAsset()
        {
            if (sourceFont == null)
            {
                EditorUtility.DisplayDialog("Error", "A Source Font File is required to create a font asset.", "OK");
                return;
            }

            // Generate the character set string for TMP Font Asset Creator
            StringBuilder charSetBuilder = new StringBuilder();

            // Add selected character ranges
            if (includeArabicBasic) charSetBuilder.Append(arabicRanges[0] + "\n");
            if (includeArabicSupplement) charSetBuilder.Append(arabicRanges[1] + "\n");
            if (includeArabicExtendedA) charSetBuilder.Append(arabicRanges[2] + "\n");
            if (includeArabicPresentationA) charSetBuilder.Append(arabicRanges[3] + "\n");
            if (includeArabicPresentationB) charSetBuilder.Append(arabicRanges[4] + "\n");
            if (includePersian) charSetBuilder.Append(arabicRanges[5] + "\n");

            // Add basic Latin range for mixed text
            charSetBuilder.Append("0020-007F\n"); // Basic Latin

            string characterSet = charSetBuilder.ToString();

            // Create temp file with character set
            string tempFilePath = Path.Combine(Path.GetTempPath(), "ArabicCharSet.txt");
            File.WriteAllText(tempFilePath, characterSet);

            // Ensure the save path exists
            if (!Directory.Exists(savePath))
            {
                Directory.CreateDirectory(savePath);
            }

            // Generate the font asset using TMP_FontAsset.CreateFontAsset
            string fullPath = Path.Combine(savePath, saveFileName + ".asset");

            // Create the font asset
            TMP_FontAsset fontAsset = TMP_FontAsset.CreateFontAsset(
                sourceFont,
                samplingPointSize,
                atlasPadding,
                UnityEngine.TextCore.LowLevel.GlyphRenderMode.SDFAA,
                atlasWidth,
                atlasHeight,
                TMPro.AtlasPopulationMode.Dynamic,
                true);

            // Initialize character table with our character set
            fontAsset.ReadFontAssetDefinition();

            // Save the created font asset
            AssetDatabase.CreateAsset(fontAsset, fullPath);

            // Save the material as a sub-asset
            fontAsset.material.name = saveFileName + " Material";
            AssetDatabase.AddObjectToAsset(fontAsset.material, fontAsset);

            // Update atlas texture and add it as sub-asset
            if (fontAsset.atlasTexture != null)
            {
                fontAsset.atlasTexture.name = saveFileName + " Atlas";
                AssetDatabase.AddObjectToAsset(fontAsset.atlasTexture, fontAsset);
            }

            // The following is a workaround to "load" character set into the font
            // In practice, this will set these characters to be loaded when needed
            string sampleText = "السلام عليكم مرحبا بكم في يونيتي";
            fontAsset.TryAddCharacters(sampleText);

            // Save all assets
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();

            // Store the created font asset reference
            createdFontAsset = fontAsset;

            EditorUtility.DisplayDialog("Success", "Arabic font asset created successfully at:\n" + fullPath, "OK");

            // Clean up temp file
            try
            {
                File.Delete(tempFilePath);
            }
            catch
            {
                // Ignore cleanup errors
            }

            // Select the created asset
            EditorGUIUtility.PingObject(fontAsset);
        }
    }
}
#endif
```

---

### `ALArcade/ArabicTMP/Editor/ArabicTMPInputFieldEditor.cs`

> Lines: 49 | Size: 2.3KB

```csharp
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using TMPro;
using ALArcade.ArabicTMP;

namespace ALArcade.ArabicTMP.Editor {
    [CustomEditor(typeof(ArabicTMPInputField)), CanEditMultipleObjects]
    public class ArabicTMPInputFieldEditor : TMPro.EditorUtilities.TMP_InputFieldEditor {
        // SerializedProperties for Arabic input field options
        SerializedProperty m_ShowTashkeelProp;
        SerializedProperty m_PreserveNumbersProp;
        SerializedProperty m_FixTagsProp;
        SerializedProperty m_ForceRTLProp;

        protected override void OnEnable() {
            base.OnEnable();
            
            // Get references to serialized properties
            m_ShowTashkeelProp = serializedObject.FindProperty("m_ShowTashkeel");
            m_PreserveNumbersProp = serializedObject.FindProperty("m_PreserveNumbers");
            m_FixTagsProp = serializedObject.FindProperty("m_FixTags");
            m_ForceRTLProp = serializedObject.FindProperty("m_ForceRTL");
        }

        public override void OnInspectorGUI() {
            serializedObject.Update();
            
            // Draw the Arabic options before the base inspector
            EditorGUILayout.LabelField("Arabic Input Options", EditorStyles.boldLabel);
            
            EditorGUILayout.PropertyField(m_ShowTashkeelProp, new GUIContent("Show Diacritics (Tashkeel)", "Display Arabic diacritic marks"));
            EditorGUILayout.PropertyField(m_PreserveNumbersProp, new GUIContent("Preserve Numbers", "Keep Western numerals (123) as-is"));
            EditorGUILayout.PropertyField(m_FixTagsProp, new GUIContent("Fix Rich Text Tags", "Process rich text tags like <color> properly"));
            EditorGUILayout.PropertyField(m_ForceRTLProp, new GUIContent("Force RTL", "Force right-to-left rendering even if text starts with Latin characters"));
            
            EditorGUILayout.Space();
            
            // Call the base class's OnInspectorGUI
            base.OnInspectorGUI();
            
            serializedObject.ApplyModifiedProperties();
            
            // Show helpful notes for first-time users
            EditorGUILayout.Space();
            EditorGUILayout.HelpBox("This component works best with an ArabicTextMeshProUGUI as the text component.", MessageType.Info);
        }
    }
}
#endif
```

---

### `ALArcade/ArabicTMP/Editor/ArabicTMPMenu.cs`

> Lines: 287 | Size: 11.6KB

```csharp
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using TMPro;
using UnityEngine.UI;
using UnityEngine.EventSystems;

namespace ALArcade.ArabicTMP.Editor
{
    public static class ArabicTMPMenu
    {
        [MenuItem("GameObject/UI/Arabic Text - TextMeshPro", false, 2001)]
        static void CreateArabicTextUGUI(MenuCommand menuCommand)
        {
            // Create a new GameObject with ArabicTextMeshProUGUI
            GameObject go = new GameObject("Arabic Text (TMP)");
            go.AddComponent<RectTransform>();
            go.AddComponent<ArabicTextMeshProUGUI>().text = "أهلا بالعالم";  // Hello World in Arabic

            // Set alignment to Right
            go.GetComponent<ArabicTextMeshProUGUI>().alignment = TextAlignmentOptions.Right;

            // Assign font asset if default Arabic font exists
            TMP_FontAsset arabicFont = GetDefaultArabicFont();
            if (arabicFont != null)
            {
                go.GetComponent<ArabicTextMeshProUGUI>().font = arabicFont;
            }

            // Register the creation in the Undo system
            Undo.RegisterCreatedObjectUndo(go, "Create " + go.name);

            // Set parent
            PlaceUIElementRoot(go, menuCommand);

            // Select the newly created object
            Selection.activeObject = go;
        }

        [MenuItem("GameObject/3D Object/Arabic Text - TextMeshPro", false, 2001)]
        static void CreateArabicTextWorld(MenuCommand menuCommand)
        {
            // Create a new GameObject with ArabicTextMeshPro
            GameObject go = new GameObject("Arabic Text (TMP)");
            go.AddComponent<ArabicTextMeshPro>().text = "أهلا بالعالم";  // Hello World in Arabic

            // Set alignment to Right
            go.GetComponent<ArabicTextMeshPro>().alignment = TextAlignmentOptions.Right;

            // Set default size for 3D text
            go.GetComponent<ArabicTextMeshPro>().fontSize = 36;

            // Assign font asset if default Arabic font exists
            TMP_FontAsset arabicFont = GetDefaultArabicFont();
            if (arabicFont != null)
            {
                go.GetComponent<ArabicTextMeshPro>().font = arabicFont;
            }

            // Register the creation in the Undo system
            Undo.RegisterCreatedObjectUndo(go, "Create " + go.name);

            // Set parent
            PlaceGameObjectInScene(go, menuCommand);

            // Select the newly created object
            Selection.activeObject = go;
        }

        [MenuItem("GameObject/UI/Arabic Input Field - TextMeshPro", false, 2051)]
        static void CreateArabicInputField(MenuCommand menuCommand)
        {
            // Create a GameObject with InputField with built-in UI components
            GameObject go = CreateInputFieldWithRTL();

            // Register the creation in the Undo system
            Undo.RegisterCreatedObjectUndo(go, "Create " + go.name);

            // Set parent
            PlaceUIElementRoot(go, menuCommand);

            // Select the newly created object
            Selection.activeObject = go;
        }

        private static GameObject CreateInputFieldWithRTL()
        {
            // Create a new GameObject with ArabicTMPInputField
            GameObject go = new GameObject("Arabic Input Field (TMP)");
            RectTransform rt = go.AddComponent<RectTransform>();
            rt.sizeDelta = new Vector2(160, 30);

            // Add the Input field component which in turn adds the needed sub components
            ArabicTMPInputField inputField = go.AddComponent<ArabicTMPInputField>();

            // Get reference to the TMP_InputField.textComponent
            GameObject textArea = new GameObject("Text Area");
            RectTransform textAreaRectTransform = textArea.AddComponent<RectTransform>();
            textAreaRectTransform.anchorMin = Vector2.zero;
            textAreaRectTransform.anchorMax = Vector2.one;
            textAreaRectTransform.sizeDelta = Vector2.zero;
            textAreaRectTransform.offsetMin = new Vector2(10, 6);
            textAreaRectTransform.offsetMax = new Vector2(-10, -7);
            textArea.transform.SetParent(go.transform, false);

            // Create text component
            GameObject textComponent = new GameObject("Text");
            RectTransform textRectTransform = textComponent.AddComponent<RectTransform>();
            textRectTransform.anchorMin = new Vector2(0, 0);
            textRectTransform.anchorMax = new Vector2(1, 1);
            textRectTransform.sizeDelta = Vector2.zero;
            textRectTransform.offsetMin = new Vector2(0, 0);
            textRectTransform.offsetMax = new Vector2(0, 0);
            textComponent.transform.SetParent(textArea.transform, false);

            // Instead of regular TMP_Text, use our ArabicTextMeshProUGUI
            ArabicTextMeshProUGUI text = textComponent.AddComponent<ArabicTextMeshProUGUI>();
            text.alignment = TextAlignmentOptions.Right;
            text.color = new Color(50f / 255f, 50f / 255f, 50f / 255f, 1f);
            text.text = "";

            // Create placeholder for the text input
            GameObject placeholder = new GameObject("Placeholder");
            RectTransform placeholderRectTransform = placeholder.AddComponent<RectTransform>();
            placeholderRectTransform.anchorMin = Vector2.zero;
            placeholderRectTransform.anchorMax = Vector2.one;
            placeholderRectTransform.sizeDelta = Vector2.zero;
            placeholderRectTransform.offsetMin = Vector2.zero;
            placeholderRectTransform.offsetMax = Vector2.zero;
            placeholder.transform.SetParent(textArea.transform, false);

            // Again use ArabicTextMeshProUGUI for the placeholder
            ArabicTextMeshProUGUI placeholderText = placeholder.AddComponent<ArabicTextMeshProUGUI>();
            placeholderText.alignment = TextAlignmentOptions.Right;
            placeholderText.text = "أدخل النص...";  // "Enter text..." in Arabic
            placeholderText.enableWordWrapping = false;
            placeholderText.extraPadding = true;

            Color placeholderColor = new Color(0f, 0f, 0f, 0.5f);
            placeholderText.color = placeholderColor;

            // Make the placeholder text slightly smaller
            placeholderText.fontSize = text.fontSize - 0.75f;

            // Set references on the InputField
            inputField.textComponent = text;
            inputField.placeholder = placeholderText;
            inputField.fontAsset = text.font;

            // Assign Arabic font if available
            TMP_FontAsset arabicFont = GetDefaultArabicFont();
            if (arabicFont != null)
            {
                text.font = arabicFont;
                placeholderText.font = arabicFont;
                inputField.fontAsset = arabicFont;
            }

            return go;
        }

        // Helper function to get default Arabic font, if available
        private static TMP_FontAsset GetDefaultArabicFont()
        {
            // Look for common Arabic fonts in project
            string[] guids = AssetDatabase.FindAssets("t:TMP_FontAsset");
            foreach (string guid in guids)
            {
                string assetPath = AssetDatabase.GUIDToAssetPath(guid);
                TMP_FontAsset font = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(assetPath);

                // Look for fonts with common Arabic font names
                if (font != null)
                {
                    string fontName = font.name.ToLowerInvariant();
                    if (fontName.Contains("arabic") || fontName.Contains("amiri") ||
                        fontName.Contains("cairo") || fontName.Contains("scheherazade") ||
                        fontName.Contains("dubai") || fontName.Contains("noto") && fontName.Contains("arabic"))
                    {
                        return font;
                    }
                }
            }
            return null;
        }

        // Common code for placing UI elements in hierarchy
        private static void PlaceUIElementRoot(GameObject element, MenuCommand menuCommand)
        {
            GameObject parent = menuCommand.context as GameObject;

            if (parent == null || parent.GetComponentInParent<Canvas>() == null)
            {
                parent = GetOrCreateCanvasGameObject();
            }

            string uniqueName = GameObjectUtility.GetUniqueNameForSibling(parent.transform, element.name);
            element.name = uniqueName;
            Undo.RegisterCreatedObjectUndo(element, "Create " + element.name);
            Undo.SetTransformParent(element.transform, parent.transform, "Parent " + element.name);
            GameObjectUtility.SetParentAndAlign(element, parent);

            Selection.activeGameObject = element;
        }

        // Get or create canvas for UI elements
        private static GameObject GetOrCreateCanvasGameObject()
        {
            GameObject selectedGo = Selection.activeGameObject;

            // Try to find canvas in selected hierarchy
            Canvas canvas = (selectedGo != null) ? selectedGo.GetComponentInParent<Canvas>() : null;
            if (canvas != null && canvas.gameObject.activeInHierarchy)
            {
                return canvas.gameObject;
            }

            // No canvas in hierarchy, try to find any canvas
            canvas = Object.FindObjectOfType<Canvas>();
            if (canvas != null && canvas.gameObject.activeInHierarchy)
            {
                return canvas.gameObject;
            }

            // No canvas exists, create one
            return CreateNewUI();
        }

        // Create a new UI with EventSystem
        private static GameObject CreateNewUI()
        {
            // Root canvas
            GameObject root = new GameObject("Canvas");
            root.layer = LayerMask.NameToLayer("UI");
            Canvas canvas = root.AddComponent<Canvas>();
            canvas.renderMode = RenderMode.ScreenSpaceOverlay;
            root.AddComponent<CanvasScaler>();
            root.AddComponent<GraphicRaycaster>();

            // Create EventSystem if needed
            CreateEventSystemIfNeeded();

            return root;
        }

        // Create EventSystem if none exists
        private static void CreateEventSystemIfNeeded()
        {
            if (Object.FindObjectOfType<EventSystem>() == null)
            {
                GameObject eventSystem = new GameObject("EventSystem");
                eventSystem.AddComponent<EventSystem>();
                eventSystem.AddComponent<StandaloneInputModule>();

                Undo.RegisterCreatedObjectUndo(eventSystem, "Create EventSystem");
            }
        }

        // Place 3D objects in the scene
        private static void PlaceGameObjectInScene(GameObject element, MenuCommand menuCommand)
        {
            GameObject parent = menuCommand.context as GameObject;

            if (parent == null)
            {
                // No context, place at scene root
                parent = null;
            }

            string uniqueName = GameObjectUtility.GetUniqueNameForSibling(parent != null ? parent.transform : null, element.name);
            element.name = uniqueName;

            // Register undo
            Undo.RegisterCreatedObjectUndo(element, "Create " + element.name);

            // Set parent
            if (parent != null)
            {
                Undo.SetTransformParent(element.transform, parent.transform, "Parent " + element.name);
                GameObjectUtility.SetParentAndAlign(element, parent);
            }

            // Set selection to new object
            Selection.activeGameObject = element;
        }
    }
}
#endif
```

---

### `ALArcade/ArabicTMP/Editor/ArabicTextMeshProEditor.cs`

> Lines: 83 | Size: 3.8KB

```csharp
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using TMPro;
using ALArcade.ArabicTMP;

namespace ALArcade.ArabicTMP.Editor {
    [CustomEditor(typeof(ArabicTextMeshPro)), CanEditMultipleObjects]
    public class ArabicTextMeshProEditor : TMPro.EditorUtilities.TMP_EditorPanelUI {
        // SerializedProperties for Arabic text component
        SerializedProperty m_ArabicTextProp;
        SerializedProperty m_ShowTashkeelProp;
        SerializedProperty m_PreserveNumbersProp;
        SerializedProperty m_FixTagsProp;
        SerializedProperty m_ForceRTLProp;

        protected override void OnEnable() {
            base.OnEnable();
            
            // Get references to serialized properties
            m_ArabicTextProp = serializedObject.FindProperty("m_ArabicText");
            m_ShowTashkeelProp = serializedObject.FindProperty("m_ShowTashkeel");
            m_PreserveNumbersProp = serializedObject.FindProperty("m_PreserveNumbers");
            m_FixTagsProp = serializedObject.FindProperty("m_FixTags");
            m_ForceRTLProp = serializedObject.FindProperty("m_ForceRTL");
        }

        public override void OnInspectorGUI() {
            if (target == null)
                return;
                
            serializedObject.Update();

            // Draw Arabic text input field
            EditorGUILayout.LabelField("Arabic Text", EditorStyles.boldLabel);
            EditorGUI.BeginChangeCheck();
            
            string newText = EditorGUILayout.TextArea(m_ArabicTextProp.stringValue, GUILayout.MinHeight(80));
            if (EditorGUI.EndChangeCheck()) {
                m_ArabicTextProp.stringValue = newText;
                
                // Ensure immediate update in editor
                foreach (var obj in targets) {
                    if (obj is ArabicTextMeshPro arabicText) {
                        arabicText.arabicText = newText;
                    }
                }
            }
            
            // Draw options section
            EditorGUILayout.Space();
            EditorGUILayout.LabelField("Arabic Options", EditorStyles.boldLabel);
            
            EditorGUILayout.PropertyField(m_ShowTashkeelProp, new GUIContent("Show Diacritics (Tashkeel)", "Display Arabic diacritic marks"));
            EditorGUILayout.PropertyField(m_PreserveNumbersProp, new GUIContent("Preserve Numbers", "Keep Western numerals (123) as-is"));
            EditorGUILayout.PropertyField(m_FixTagsProp, new GUIContent("Fix Rich Text Tags", "Process rich text tags like <color> properly"));
            EditorGUILayout.PropertyField(m_ForceRTLProp, new GUIContent("Force RTL", "Force right-to-left rendering even if text starts with Latin characters"));
            
            EditorGUILayout.Space();
            
            // Draw default TMP properties
            EditorGUILayout.LabelField("TextMeshPro Settings", EditorStyles.boldLabel);
            
            // Hide the original text property since we're using our custom one
            SerializedProperty prop = serializedObject.FindProperty("m_text");
            if (prop != null) {
                // Make the TMP text field read-only to prevent editing it directly
                GUI.enabled = false;
                EditorGUILayout.PropertyField(prop, new GUIContent("Processed Text (Read Only)"));
                GUI.enabled = true;
            }
            
            DrawMainSettings();
            DrawExtraSettings();
            
            serializedObject.ApplyModifiedProperties();
            
            // Show helpful notes for first-time users
            EditorGUILayout.Space();
            EditorGUILayout.HelpBox("Enter Arabic text in the 'Arabic Text' field above. It will be automatically shaped and displayed correctly.", MessageType.Info);
        }
    }
}
#endif
```

---

### `ALArcade/ArabicTMP/Editor/ArabicTextMeshProUGUIEditor.cs`

> Lines: 83 | Size: 3.8KB

```csharp
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using TMPro;
using ALArcade.ArabicTMP;

namespace ALArcade.ArabicTMP.Editor {
    [CustomEditor(typeof(ArabicTextMeshProUGUI)), CanEditMultipleObjects]
    public class ArabicTextMeshProUGUIEditor : TMPro.EditorUtilities.TMP_EditorPanelUI {
        // SerializedProperties for Arabic text component
        SerializedProperty m_ArabicTextProp;
        SerializedProperty m_ShowTashkeelProp;
        SerializedProperty m_PreserveNumbersProp;
        SerializedProperty m_FixTagsProp;
        SerializedProperty m_ForceRTLProp;

        protected override void OnEnable() {
            base.OnEnable();
            
            // Get references to serialized properties
            m_ArabicTextProp = serializedObject.FindProperty("m_ArabicText");
            m_ShowTashkeelProp = serializedObject.FindProperty("m_ShowTashkeel");
            m_PreserveNumbersProp = serializedObject.FindProperty("m_PreserveNumbers");
            m_FixTagsProp = serializedObject.FindProperty("m_FixTags");
            m_ForceRTLProp = serializedObject.FindProperty("m_ForceRTL");
        }

        public override void OnInspectorGUI() {
            if (target == null)
                return;
                
            serializedObject.Update();

            // Draw Arabic text input field
            EditorGUILayout.LabelField("Arabic Text", EditorStyles.boldLabel);
            EditorGUI.BeginChangeCheck();
            
            string newText = EditorGUILayout.TextArea(m_ArabicTextProp.stringValue, GUILayout.MinHeight(80));
            if (EditorGUI.EndChangeCheck()) {
                m_ArabicTextProp.stringValue = newText;
                
                // Ensure immediate update in editor
                foreach (var obj in targets) {
                    if (obj is ArabicTextMeshProUGUI arabicText) {
                        arabicText.arabicText = newText;
                    }
                }
            }
            
            // Draw options section
            EditorGUILayout.Space();
            EditorGUILayout.LabelField("Arabic Options", EditorStyles.boldLabel);
            
            EditorGUILayout.PropertyField(m_ShowTashkeelProp, new GUIContent("Show Diacritics (Tashkeel)", "Display Arabic diacritic marks"));
            EditorGUILayout.PropertyField(m_PreserveNumbersProp, new GUIContent("Preserve Numbers", "Keep Western numerals (123) as-is"));
            EditorGUILayout.PropertyField(m_FixTagsProp, new GUIContent("Fix Rich Text Tags", "Process rich text tags like <color> properly"));
            EditorGUILayout.PropertyField(m_ForceRTLProp, new GUIContent("Force RTL", "Force right-to-left rendering even if text starts with Latin characters"));
            
            EditorGUILayout.Space();
            
            // Draw default TMP properties
            EditorGUILayout.LabelField("TextMeshPro Settings", EditorStyles.boldLabel);
            
            // Hide the original text property since we're using our custom one
            SerializedProperty prop = serializedObject.FindProperty("m_text");
            if (prop != null) {
                // Make the TMP text field read-only to prevent editing it directly
                GUI.enabled = false;
                EditorGUILayout.PropertyField(prop, new GUIContent("Processed Text (Read Only)"));
                GUI.enabled = true;
            }
            
            DrawMainSettings();
            DrawExtraSettings();
            
            serializedObject.ApplyModifiedProperties();
            
            // Show helpful notes for first-time users
            EditorGUILayout.Space();
            EditorGUILayout.HelpBox("Enter Arabic text in the 'Arabic Text' field above. It will be automatically shaped and displayed correctly.", MessageType.Info);
        }
    }
}
#endif
```

---

### `Plugins/Demigiant/DOTween/Modules/DOTweenModuleAudio.cs`

> Lines: 198 | Size: 8.7KB

```csharp
﻿// Author: Daniele Giardini - http://www.demigiant.com
// Created: 2018/07/13

#if !DOTWEEN_NOAUDIO // MODULE_MARKER
using System;
using DG.Tweening.Core;
using DG.Tweening.Plugins.Options;
using UnityEngine;
using UnityEngine.Audio; // Required for AudioMixer

#pragma warning disable 1591
namespace DG.Tweening
{
	public static class DOTweenModuleAudio
    {
        #region Shortcuts

        #region Audio

        /// <summary>Tweens an AudioSource's volume to the given value.
        /// Also stores the AudioSource as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach (0 to 1)</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<float, float, FloatOptions> DOFade(this AudioSource target, float endValue, float duration)
        {
            if (endValue < 0) endValue = 0;
            else if (endValue > 1) endValue = 1;
            TweenerCore<float, float, FloatOptions> t = DOTween.To(() => target.volume, x => target.volume = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        /// <summary>Tweens an AudioSource's pitch to the given value.
        /// Also stores the AudioSource as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<float, float, FloatOptions> DOPitch(this AudioSource target, float endValue, float duration)
        {
            TweenerCore<float, float, FloatOptions> t = DOTween.To(() => target.pitch, x => target.pitch = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        #endregion

        #region AudioMixer

        /// <summary>Tweens an AudioMixer's exposed float to the given value.
        /// Also stores the AudioMixer as the tween's target so it can be used for filtered operations.
        /// Note that you need to manually expose a float in an AudioMixerGroup in order to be able to tween it from an AudioMixer.</summary>
        /// <param name="floatName">Name given to the exposed float to set</param>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<float, float, FloatOptions> DOSetFloat(this AudioMixer target, string floatName, float endValue, float duration)
        {
            TweenerCore<float, float, FloatOptions> t = DOTween.To(()=> {
                    float currVal;
                    target.GetFloat(floatName, out currVal);
                    return currVal;
                }, x=> target.SetFloat(floatName, x), endValue, duration);
            t.SetTarget(target);
            return t;
        }

        #region Operation Shortcuts

        /// <summary>
        /// Completes all tweens that have this target as a reference
        /// (meaning tweens that were started from this target, or that had this target added as an Id)
        /// and returns the total number of tweens completed
        /// (meaning the tweens that don't have infinite loops and were not already complete)
        /// </summary>
        /// <param name="withCallbacks">For Sequences only: if TRUE also internal Sequence callbacks will be fired,
        /// otherwise they will be ignored</param>
        public static int DOComplete(this AudioMixer target, bool withCallbacks = false)
        {
            return DOTween.Complete(target, withCallbacks);
        }

        /// <summary>
        /// Kills all tweens that have this target as a reference
        /// (meaning tweens that were started from this target, or that had this target added as an Id)
        /// and returns the total number of tweens killed.
        /// </summary>
        /// <param name="complete">If TRUE completes the tween before killing it</param>
        public static int DOKill(this AudioMixer target, bool complete = false)
        {
            return DOTween.Kill(target, complete);
        }

        /// <summary>
        /// Flips the direction (backwards if it was going forward or viceversa) of all tweens that have this target as a reference
        /// (meaning tweens that were started from this target, or that had this target added as an Id)
        /// and returns the total number of tweens flipped.
        /// </summary>
        public static int DOFlip(this AudioMixer target)
        {
            return DOTween.Flip(target);
        }

        /// <summary>
        /// Sends to the given position all tweens that have this target as a reference
        /// (meaning tweens that were started from this target, or that had this target added as an Id)
        /// and returns the total number of tweens involved.
        /// </summary>
        /// <param name="to">Time position to reach
        /// (if higher than the whole tween duration the tween will simply reach its end)</param>
        /// <param name="andPlay">If TRUE will play the tween after reaching the given position, otherwise it will pause it</param>
        public static int DOGoto(this AudioMixer target, float to, bool andPlay = false)
        {
            return DOTween.Goto(target, to, andPlay);
        }

        /// <summary>
        /// Pauses all tweens that have this target as a reference
        /// (meaning tweens that were started from this target, or that had this target added as an Id)
        /// and returns the total number of tweens paused.
        /// </summary>
        public static int DOPause(this AudioMixer target)
        {
            return DOTween.Pause(target);
        }

        /// <summary>
        /// Plays all tweens that have this target as a reference
        /// (meaning tweens that were started from this target, or that had this target added as an Id)
        /// and returns the total number of tweens played.
        /// </summary>
        public static int DOPlay(this AudioMixer target)
        {
            return DOTween.Play(target);
        }

        /// <summary>
        /// Plays backwards all tweens that have this target as a reference
        /// (meaning tweens that were started from this target, or that had this target added as an Id)
        /// and returns the total number of tweens played.
        /// </summary>
        public static int DOPlayBackwards(this AudioMixer target)
        {
            return DOTween.PlayBackwards(target);
        }

        /// <summary>
        /// Plays forward all tweens that have this target as a reference
        /// (meaning tweens that were started from this target, or that had this target added as an Id)
        /// and returns the total number of tweens played.
        /// </summary>
        public static int DOPlayForward(this AudioMixer target)
        {
            return DOTween.PlayForward(target);
        }

        /// <summary>
        /// Restarts all tweens that have this target as a reference
        /// (meaning tweens that were started from this target, or that had this target added as an Id)
        /// and returns the total number of tweens restarted.
        /// </summary>
        public static int DORestart(this AudioMixer target)
        {
            return DOTween.Restart(target);
        }

        /// <summary>
        /// Rewinds all tweens that have this target as a reference
        /// (meaning tweens that were started from this target, or that had this target added as an Id)
        /// and returns the total number of tweens rewinded.
        /// </summary>
        public static int DORewind(this AudioMixer target)
        {
            return DOTween.Rewind(target);
        }

        /// <summary>
        /// Smoothly rewinds all tweens that have this target as a reference
        /// (meaning tweens that were started from this target, or that had this target added as an Id)
        /// and returns the total number of tweens rewinded.
        /// </summary>
        public static int DOSmoothRewind(this AudioMixer target)
        {
            return DOTween.SmoothRewind(target);
        }

        /// <summary>
        /// Toggles the paused state (plays if it was paused, pauses if it was playing) of all tweens that have this target as a reference
        /// (meaning tweens that were started from this target, or that had this target added as an Id)
        /// and returns the total number of tweens involved.
        /// </summary>
        public static int DOTogglePause(this AudioMixer target)
        {
            return DOTween.TogglePause(target);
        }

        #endregion

        #endregion

        #endregion
    }
}
#endif

```

---

### `Plugins/Demigiant/DOTween/Modules/DOTweenModuleEPOOutline.cs`

> Lines: 146 | Size: 6.7KB

```csharp
﻿using UnityEngine;

#if DOTWEEN_EPO || EPO_DOTWEEN // MODULE_MARKER

using EPOOutline;
using DG.Tweening.Plugins.Options;
using DG.Tweening;
using DG.Tweening.Core;

namespace DG.Tweening
{
    public static class DOTweenModuleEPOOutline
    {
        public static int DOKill(this SerializedPass target, bool complete)
        {
            return DOTween.Kill(target, complete);
        }

        public static TweenerCore<float, float, FloatOptions> DOFloat(this SerializedPass target, string propertyName, float endValue, float duration)
        {
            var tweener = DOTween.To(() => target.GetFloat(propertyName), x => target.SetFloat(propertyName, x), endValue, duration);
            tweener.SetOptions(true).SetTarget(target);
            return tweener;
        }

        public static TweenerCore<Color, Color, ColorOptions> DOFade(this SerializedPass target, string propertyName, float endValue, float duration)
        {
            var tweener = DOTween.ToAlpha(() => target.GetColor(propertyName), x => target.SetColor(propertyName, x), endValue, duration);
            tweener.SetOptions(true).SetTarget(target);
            return tweener;
        }

        public static TweenerCore<Color, Color, ColorOptions> DOColor(this SerializedPass target, string propertyName, Color endValue, float duration)
        {
            var tweener = DOTween.To(() => target.GetColor(propertyName), x => target.SetColor(propertyName, x), endValue, duration);
            tweener.SetOptions(false).SetTarget(target);
            return tweener;
        }

        public static TweenerCore<Vector4, Vector4, VectorOptions> DOVector(this SerializedPass target, string propertyName, Vector4 endValue, float duration)
        {
            var tweener = DOTween.To(() => target.GetVector(propertyName), x => target.SetVector(propertyName, x), endValue, duration);
            tweener.SetOptions(false).SetTarget(target);
            return tweener;
        }

        public static TweenerCore<float, float, FloatOptions> DOFloat(this SerializedPass target, int propertyId, float endValue, float duration)
        {
            var tweener = DOTween.To(() => target.GetFloat(propertyId), x => target.SetFloat(propertyId, x), endValue, duration);
            tweener.SetOptions(true).SetTarget(target);
            return tweener;
        }

        public static TweenerCore<Color, Color, ColorOptions> DOFade(this SerializedPass target, int propertyId, float endValue, float duration)
        {
            var tweener = DOTween.ToAlpha(() => target.GetColor(propertyId), x => target.SetColor(propertyId, x), endValue, duration);
            tweener.SetOptions(true).SetTarget(target);
            return tweener;
        }

        public static TweenerCore<Color, Color, ColorOptions> DOColor(this SerializedPass target, int propertyId, Color endValue, float duration)
        {
            var tweener = DOTween.To(() => target.GetColor(propertyId), x => target.SetColor(propertyId, x), endValue, duration);
            tweener.SetOptions(false).SetTarget(target);
            return tweener;
        }

        public static TweenerCore<Vector4, Vector4, VectorOptions> DOVector(this SerializedPass target, int propertyId, Vector4 endValue, float duration)
        {
            var tweener = DOTween.To(() => target.GetVector(propertyId), x => target.SetVector(propertyId, x), endValue, duration);
            tweener.SetOptions(false).SetTarget(target);
            return tweener;
        }

        public static int DOKill(this Outlinable.OutlineProperties target, bool complete = false)
        {
            return DOTween.Kill(target, complete);
        }

        public static int DOKill(this Outliner target, bool complete = false)
        {
            return DOTween.Kill(target, complete);
        }

        /// <summary>
        /// Controls the alpha (transparency) of the outline
        /// </summary>
        public static TweenerCore<Color, Color, ColorOptions> DOFade(this Outlinable.OutlineProperties target, float endValue, float duration)
        {
            var tweener = DOTween.ToAlpha(() => target.Color, x => target.Color = x, endValue, duration);
            tweener.SetOptions(true).SetTarget(target);
            return tweener;
        }

        /// <summary>
        /// Controls the color of the outline
        /// </summary>
        public static TweenerCore<Color, Color, ColorOptions> DOColor(this Outlinable.OutlineProperties target, Color endValue, float duration)
        {
            var tweener = DOTween.To(() => target.Color, x => target.Color = x, endValue, duration);
            tweener.SetOptions(false).SetTarget(target);
            return tweener;
        }

        /// <summary>
        /// Controls the amount of blur applied to the outline
        /// </summary>
        public static TweenerCore<float, float, FloatOptions> DOBlurShift(this Outlinable.OutlineProperties target, float endValue, float duration, bool snapping = false)
        {
            var tweener = DOTween.To(() => target.BlurShift, x => target.BlurShift = x, endValue, duration);
            tweener.SetOptions(snapping).SetTarget(target);
            return tweener;
        }

        /// <summary>
        /// Controls the amount of blur applied to the outline
        /// </summary>
        public static TweenerCore<float, float, FloatOptions> DOBlurShift(this Outliner target, float endValue, float duration, bool snapping = false)
        {
            var tweener = DOTween.To(() => target.BlurShift, x => target.BlurShift = x, endValue, duration);
            tweener.SetOptions(snapping).SetTarget(target);
            return tweener;
        }

        /// <summary>
        /// Controls the amount of dilation applied to the outline
        /// </summary>
        public static TweenerCore<float, float, FloatOptions> DODilateShift(this Outlinable.OutlineProperties target, float endValue, float duration, bool snapping = false)
        {
            var tweener = DOTween.To(() => target.DilateShift, x => target.DilateShift = x, endValue, duration);
            tweener.SetOptions(snapping).SetTarget(target);
            return tweener;
        }

        /// <summary>
        /// Controls the amount of dilation applied to the outline
        /// </summary>
        public static TweenerCore<float, float, FloatOptions> DODilateShift(this Outliner target, float endValue, float duration, bool snapping = false)
        {
            var tweener = DOTween.To(() => target.DilateShift, x => target.DilateShift = x, endValue, duration);
            tweener.SetOptions(snapping).SetTarget(target);
            return tweener;
        }
    }
}
#endif

```

---

### `Plugins/Demigiant/DOTween/Modules/DOTweenModulePhysics.cs`

> Lines: 216 | Size: 13.7KB

```csharp
﻿// Author: Daniele Giardini - http://www.demigiant.com
// Created: 2018/07/13

#if !DOTWEEN_NOPHYSICS // MODULE_MARKER
using System;
using DG.Tweening.Core;
using DG.Tweening.Core.Enums;
using DG.Tweening.Plugins;
using DG.Tweening.Plugins.Core.PathCore;
using DG.Tweening.Plugins.Options;
using UnityEngine;

#pragma warning disable 1591
namespace DG.Tweening
{
	public static class DOTweenModulePhysics
    {
        #region Shortcuts

        #region Rigidbody

        /// <summary>Tweens a Rigidbody's position to the given value.
        /// Also stores the rigidbody as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector3, Vector3, VectorOptions> DOMove(this Rigidbody target, Vector3 endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector3, Vector3, VectorOptions> t = DOTween.To(() => target.position, target.MovePosition, endValue, duration);
            t.SetOptions(snapping).SetTarget(target);
            return t;
        }

        /// <summary>Tweens a Rigidbody's X position to the given value.
        /// Also stores the rigidbody as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector3, Vector3, VectorOptions> DOMoveX(this Rigidbody target, float endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector3, Vector3, VectorOptions> t = DOTween.To(() => target.position, target.MovePosition, new Vector3(endValue, 0, 0), duration);
            t.SetOptions(AxisConstraint.X, snapping).SetTarget(target);
            return t;
        }

        /// <summary>Tweens a Rigidbody's Y position to the given value.
        /// Also stores the rigidbody as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector3, Vector3, VectorOptions> DOMoveY(this Rigidbody target, float endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector3, Vector3, VectorOptions> t = DOTween.To(() => target.position, target.MovePosition, new Vector3(0, endValue, 0), duration);
            t.SetOptions(AxisConstraint.Y, snapping).SetTarget(target);
            return t;
        }

        /// <summary>Tweens a Rigidbody's Z position to the given value.
        /// Also stores the rigidbody as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector3, Vector3, VectorOptions> DOMoveZ(this Rigidbody target, float endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector3, Vector3, VectorOptions> t = DOTween.To(() => target.position, target.MovePosition, new Vector3(0, 0, endValue), duration);
            t.SetOptions(AxisConstraint.Z, snapping).SetTarget(target);
            return t;
        }

        /// <summary>Tweens a Rigidbody's rotation to the given value.
        /// Also stores the rigidbody as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="mode">Rotation mode</param>
        public static TweenerCore<Quaternion, Vector3, QuaternionOptions> DORotate(this Rigidbody target, Vector3 endValue, float duration, RotateMode mode = RotateMode.Fast)
        {
            TweenerCore<Quaternion, Vector3, QuaternionOptions> t = DOTween.To(() => target.rotation, target.MoveRotation, endValue, duration);
            t.SetTarget(target);
            t.plugOptions.rotateMode = mode;
            return t;
        }

        /// <summary>Tweens a Rigidbody's rotation so that it will look towards the given position.
        /// Also stores the rigidbody as the tween's target so it can be used for filtered operations</summary>
        /// <param name="towards">The position to look at</param><param name="duration">The duration of the tween</param>
        /// <param name="axisConstraint">Eventual axis constraint for the rotation</param>
        /// <param name="up">The vector that defines in which direction up is (default: Vector3.up)</param>
        public static TweenerCore<Quaternion, Vector3, QuaternionOptions> DOLookAt(this Rigidbody target, Vector3 towards, float duration, AxisConstraint axisConstraint = AxisConstraint.None, Vector3? up = null)
        {
            TweenerCore<Quaternion, Vector3, QuaternionOptions> t = DOTween.To(() => target.rotation, target.MoveRotation, towards, duration)
                .SetTarget(target).SetSpecialStartupMode(SpecialStartupMode.SetLookAt);
            t.plugOptions.axisConstraint = axisConstraint;
            t.plugOptions.up = (up == null) ? Vector3.up : (Vector3)up;
            return t;
        }

        #region Special

        /// <summary>Tweens a Rigidbody's position to the given value, while also applying a jump effect along the Y axis.
        /// Returns a Sequence instead of a Tweener.
        /// Also stores the Rigidbody as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param>
        /// <param name="jumpPower">Power of the jump (the max height of the jump is represented by this plus the final Y offset)</param>
        /// <param name="numJumps">Total number of jumps</param>
        /// <param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static Sequence DOJump(this Rigidbody target, Vector3 endValue, float jumpPower, int numJumps, float duration, bool snapping = false)
        {
            if (numJumps < 1) numJumps = 1;
            float startPosY = 0;
            float offsetY = -1;
            bool offsetYSet = false;
            Sequence s = DOTween.Sequence();
            Tween yTween = DOTween.To(() => target.position, target.MovePosition, new Vector3(0, jumpPower, 0), duration / (numJumps * 2))
                .SetOptions(AxisConstraint.Y, snapping).SetEase(Ease.OutQuad).SetRelative()
                .SetLoops(numJumps * 2, LoopType.Yoyo)
                .OnStart(() => startPosY = target.position.y);
            s.Append(DOTween.To(() => target.position, target.MovePosition, new Vector3(endValue.x, 0, 0), duration)
                    .SetOptions(AxisConstraint.X, snapping).SetEase(Ease.Linear)
                ).Join(DOTween.To(() => target.position, target.MovePosition, new Vector3(0, 0, endValue.z), duration)
                    .SetOptions(AxisConstraint.Z, snapping).SetEase(Ease.Linear)
                ).Join(yTween)
                .SetTarget(target).SetEase(DOTween.defaultEaseType);
            yTween.OnUpdate(() => {
                if (!offsetYSet) {
                    offsetYSet = true;
                    offsetY = s.isRelative ? endValue.y : endValue.y - startPosY;
                }
                Vector3 pos = target.position;
                pos.y += DOVirtual.EasedValue(0, offsetY, yTween.ElapsedPercentage(), Ease.OutQuad);
                target.MovePosition(pos);
            });
            return s;
        }

        /// <summary>Tweens a Rigidbody's position through the given path waypoints, using the chosen path algorithm.
        /// Also stores the Rigidbody as the tween's target so it can be used for filtered operations.
        /// <para>NOTE: to tween a rigidbody correctly it should be set to kinematic at least while being tweened.</para>
        /// <para>BEWARE: doesn't work on Windows Phone store (waiting for Unity to fix their own bug).
        /// If you plan to publish there you should use a regular transform.DOPath.</para></summary>
        /// <param name="path">The waypoints to go through</param>
        /// <param name="duration">The duration of the tween</param>
        /// <param name="pathType">The type of path: Linear (straight path), CatmullRom (curved CatmullRom path) or CubicBezier (curved with control points)</param>
        /// <param name="pathMode">The path mode: 3D, side-scroller 2D, top-down 2D</param>
        /// <param name="resolution">The resolution of the path (useless in case of Linear paths): higher resolutions make for more detailed curved paths but are more expensive.
        /// Defaults to 10, but a value of 5 is usually enough if you don't have dramatic long curves between waypoints</param>
        /// <param name="gizmoColor">The color of the path (shown when gizmos are active in the Play panel and the tween is running)</param>
        public static TweenerCore<Vector3, Path, PathOptions> DOPath(
            this Rigidbody target, Vector3[] path, float duration, PathType pathType = PathType.Linear,
            PathMode pathMode = PathMode.Full3D, int resolution = 10, Color? gizmoColor = null
        )
        {
            if (resolution < 1) resolution = 1;
            TweenerCore<Vector3, Path, PathOptions> t = DOTween.To(PathPlugin.Get(), () => target.position, target.MovePosition, new Path(pathType, path, resolution, gizmoColor), duration)
                .SetTarget(target).SetUpdate(UpdateType.Fixed);

            t.plugOptions.isRigidbody = true;
            t.plugOptions.mode = pathMode;
            return t;
        }
        /// <summary>Tweens a Rigidbody's localPosition through the given path waypoints, using the chosen path algorithm.
        /// Also stores the Rigidbody as the tween's target so it can be used for filtered operations
        /// <para>NOTE: to tween a rigidbody correctly it should be set to kinematic at least while being tweened.</para>
        /// <para>BEWARE: doesn't work on Windows Phone store (waiting for Unity to fix their own bug).
        /// If you plan to publish there you should use a regular transform.DOLocalPath.</para></summary>
        /// <param name="path">The waypoint to go through</param>
        /// <param name="duration">The duration of the tween</param>
        /// <param name="pathType">The type of path: Linear (straight path), CatmullRom (curved CatmullRom path) or CubicBezier (curved with control points)</param>
        /// <param name="pathMode">The path mode: 3D, side-scroller 2D, top-down 2D</param>
        /// <param name="resolution">The resolution of the path: higher resolutions make for more detailed curved paths but are more expensive.
        /// Defaults to 10, but a value of 5 is usually enough if you don't have dramatic long curves between waypoints</param>
        /// <param name="gizmoColor">The color of the path (shown when gizmos are active in the Play panel and the tween is running)</param>
        public static TweenerCore<Vector3, Path, PathOptions> DOLocalPath(
            this Rigidbody target, Vector3[] path, float duration, PathType pathType = PathType.Linear,
            PathMode pathMode = PathMode.Full3D, int resolution = 10, Color? gizmoColor = null
        )
        {
            if (resolution < 1) resolution = 1;
            Transform trans = target.transform;
            TweenerCore<Vector3, Path, PathOptions> t = DOTween.To(PathPlugin.Get(), () => trans.localPosition, x => target.MovePosition(trans.parent == null ? x : trans.parent.TransformPoint(x)), new Path(pathType, path, resolution, gizmoColor), duration)
                .SetTarget(target).SetUpdate(UpdateType.Fixed);

            t.plugOptions.isRigidbody = true;
            t.plugOptions.mode = pathMode;
            t.plugOptions.useLocalPosition = true;
            return t;
        }
        // Used by path editor when creating the actual tween, so it can pass a pre-compiled path
        internal static TweenerCore<Vector3, Path, PathOptions> DOPath(
            this Rigidbody target, Path path, float duration, PathMode pathMode = PathMode.Full3D
        )
        {
            TweenerCore<Vector3, Path, PathOptions> t = DOTween.To(PathPlugin.Get(), () => target.position, target.MovePosition, path, duration)
                .SetTarget(target);

            t.plugOptions.isRigidbody = true;
            t.plugOptions.mode = pathMode;
            return t;
        }
        internal static TweenerCore<Vector3, Path, PathOptions> DOLocalPath(
            this Rigidbody target, Path path, float duration, PathMode pathMode = PathMode.Full3D
        )
        {
            Transform trans = target.transform;
            TweenerCore<Vector3, Path, PathOptions> t = DOTween.To(PathPlugin.Get(), () => trans.localPosition, x => target.MovePosition(trans.parent == null ? x : trans.parent.TransformPoint(x)), path, duration)
                .SetTarget(target);

            t.plugOptions.isRigidbody = true;
            t.plugOptions.mode = pathMode;
            t.plugOptions.useLocalPosition = true;
            return t;
        }

        #endregion

        #endregion

        #endregion
	}
}
#endif

```

---

### `Plugins/Demigiant/DOTween/Modules/DOTweenModulePhysics2D.cs`

> Lines: 193 | Size: 11.9KB

```csharp
﻿// Author: Daniele Giardini - http://www.demigiant.com
// Created: 2018/07/13

#if !DOTWEEN_NOPHYSICS2D // MODULE_MARKER
using System;
using DG.Tweening.Core;
using DG.Tweening.Plugins;
using DG.Tweening.Plugins.Core.PathCore;
using DG.Tweening.Plugins.Options;
using UnityEngine;

#pragma warning disable 1591
namespace DG.Tweening
{
	public static class DOTweenModulePhysics2D
    {
        #region Shortcuts

        #region Rigidbody2D Shortcuts

        /// <summary>Tweens a Rigidbody2D's position to the given value.
        /// Also stores the Rigidbody2D as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOMove(this Rigidbody2D target, Vector2 endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => target.position, target.MovePosition, endValue, duration);
            t.SetOptions(snapping).SetTarget(target);
            return t;
        }

        /// <summary>Tweens a Rigidbody2D's X position to the given value.
        /// Also stores the Rigidbody2D as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOMoveX(this Rigidbody2D target, float endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => target.position, target.MovePosition, new Vector2(endValue, 0), duration);
            t.SetOptions(AxisConstraint.X, snapping).SetTarget(target);
            return t;
        }

        /// <summary>Tweens a Rigidbody2D's Y position to the given value.
        /// Also stores the Rigidbody2D as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOMoveY(this Rigidbody2D target, float endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => target.position, target.MovePosition, new Vector2(0, endValue), duration);
            t.SetOptions(AxisConstraint.Y, snapping).SetTarget(target);
            return t;
        }

        /// <summary>Tweens a Rigidbody2D's rotation to the given value.
        /// Also stores the Rigidbody2D as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<float, float, FloatOptions> DORotate(this Rigidbody2D target, float endValue, float duration)
        {
            TweenerCore<float, float, FloatOptions> t = DOTween.To(() => target.rotation, target.MoveRotation, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        #region Special

        /// <summary>Tweens a Rigidbody2D's position to the given value, while also applying a jump effect along the Y axis.
        /// Returns a Sequence instead of a Tweener.
        /// Also stores the Rigidbody2D as the tween's target so it can be used for filtered operations.
        /// <para>IMPORTANT: a rigidbody2D can't be animated in a jump arc using MovePosition, so the tween will directly set the position</para></summary>
        /// <param name="endValue">The end value to reach</param>
        /// <param name="jumpPower">Power of the jump (the max height of the jump is represented by this plus the final Y offset)</param>
        /// <param name="numJumps">Total number of jumps</param>
        /// <param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static Sequence DOJump(this Rigidbody2D target, Vector2 endValue, float jumpPower, int numJumps, float duration, bool snapping = false)
        {
            if (numJumps < 1) numJumps = 1;
            float startPosY = 0;
            float offsetY = -1;
            bool offsetYSet = false;
            Sequence s = DOTween.Sequence();
            Tween yTween = DOTween.To(() => target.position, x => target.position = x, new Vector2(0, jumpPower), duration / (numJumps * 2))
                .SetOptions(AxisConstraint.Y, snapping).SetEase(Ease.OutQuad).SetRelative()
                .SetLoops(numJumps * 2, LoopType.Yoyo)
                .OnStart(() => startPosY = target.position.y);
            s.Append(DOTween.To(() => target.position, x => target.position = x, new Vector2(endValue.x, 0), duration)
                    .SetOptions(AxisConstraint.X, snapping).SetEase(Ease.Linear)
                ).Join(yTween)
                .SetTarget(target).SetEase(DOTween.defaultEaseType);
            yTween.OnUpdate(() => {
                if (!offsetYSet) {
                    offsetYSet = true;
                    offsetY = s.isRelative ? endValue.y : endValue.y - startPosY;
                }
                Vector3 pos = target.position;
                pos.y += DOVirtual.EasedValue(0, offsetY, yTween.ElapsedPercentage(), Ease.OutQuad);
                target.MovePosition(pos);
            });
            return s;
        }

        /// <summary>Tweens a Rigidbody2D's position through the given path waypoints, using the chosen path algorithm.
        /// Also stores the Rigidbody2D as the tween's target so it can be used for filtered operations.
        /// <para>NOTE: to tween a Rigidbody2D correctly it should be set to kinematic at least while being tweened.</para>
        /// <para>BEWARE: doesn't work on Windows Phone store (waiting for Unity to fix their own bug).
        /// If you plan to publish there you should use a regular transform.DOPath.</para></summary>
        /// <param name="path">The waypoints to go through</param>
        /// <param name="duration">The duration of the tween</param>
        /// <param name="pathType">The type of path: Linear (straight path), CatmullRom (curved CatmullRom path) or CubicBezier (curved with control points)</param>
        /// <param name="pathMode">The path mode: 3D, side-scroller 2D, top-down 2D</param>
        /// <param name="resolution">The resolution of the path (useless in case of Linear paths): higher resolutions make for more detailed curved paths but are more expensive.
        /// Defaults to 10, but a value of 5 is usually enough if you don't have dramatic long curves between waypoints</param>
        /// <param name="gizmoColor">The color of the path (shown when gizmos are active in the Play panel and the tween is running)</param>
        public static TweenerCore<Vector3, Path, PathOptions> DOPath(
            this Rigidbody2D target, Vector2[] path, float duration, PathType pathType = PathType.Linear,
            PathMode pathMode = PathMode.Full3D, int resolution = 10, Color? gizmoColor = null
        )
        {
            if (resolution < 1) resolution = 1;
            int len = path.Length;
            Vector3[] path3D = new Vector3[len];
            for (int i = 0; i < len; ++i) path3D[i] = path[i];
            TweenerCore<Vector3, Path, PathOptions> t = DOTween.To(PathPlugin.Get(), () => target.position, x => target.MovePosition(x), new Path(pathType, path3D, resolution, gizmoColor), duration)
                .SetTarget(target).SetUpdate(UpdateType.Fixed);

            t.plugOptions.isRigidbody2D = true;
            t.plugOptions.mode = pathMode;
            return t;
        }
        /// <summary>Tweens a Rigidbody2D's localPosition through the given path waypoints, using the chosen path algorithm.
        /// Also stores the Rigidbody2D as the tween's target so it can be used for filtered operations
        /// <para>NOTE: to tween a Rigidbody2D correctly it should be set to kinematic at least while being tweened.</para>
        /// <para>BEWARE: doesn't work on Windows Phone store (waiting for Unity to fix their own bug).
        /// If you plan to publish there you should use a regular transform.DOLocalPath.</para></summary>
        /// <param name="path">The waypoint to go through</param>
        /// <param name="duration">The duration of the tween</param>
        /// <param name="pathType">The type of path: Linear (straight path), CatmullRom (curved CatmullRom path) or CubicBezier (curved with control points)</param>
        /// <param name="pathMode">The path mode: 3D, side-scroller 2D, top-down 2D</param>
        /// <param name="resolution">The resolution of the path: higher resolutions make for more detailed curved paths but are more expensive.
        /// Defaults to 10, but a value of 5 is usually enough if you don't have dramatic long curves between waypoints</param>
        /// <param name="gizmoColor">The color of the path (shown when gizmos are active in the Play panel and the tween is running)</param>
        public static TweenerCore<Vector3, Path, PathOptions> DOLocalPath(
            this Rigidbody2D target, Vector2[] path, float duration, PathType pathType = PathType.Linear,
            PathMode pathMode = PathMode.Full3D, int resolution = 10, Color? gizmoColor = null
        )
        {
            if (resolution < 1) resolution = 1;
            int len = path.Length;
            Vector3[] path3D = new Vector3[len];
            for (int i = 0; i < len; ++i) path3D[i] = path[i];
            Transform trans = target.transform;
            TweenerCore<Vector3, Path, PathOptions> t = DOTween.To(PathPlugin.Get(), () => trans.localPosition, x => target.MovePosition(trans.parent == null ? x : trans.parent.TransformPoint(x)), new Path(pathType, path3D, resolution, gizmoColor), duration)
                .SetTarget(target).SetUpdate(UpdateType.Fixed);

            t.plugOptions.isRigidbody2D = true;
            t.plugOptions.mode = pathMode;
            t.plugOptions.useLocalPosition = true;
            return t;
        }
        // Used by path editor when creating the actual tween, so it can pass a pre-compiled path
        internal static TweenerCore<Vector3, Path, PathOptions> DOPath(
            this Rigidbody2D target, Path path, float duration, PathMode pathMode = PathMode.Full3D
        )
        {
            TweenerCore<Vector3, Path, PathOptions> t = DOTween.To(PathPlugin.Get(), () => target.position, x => target.MovePosition(x), path, duration)
                .SetTarget(target);

            t.plugOptions.isRigidbody2D = true;
            t.plugOptions.mode = pathMode;
            return t;
        }
        internal static TweenerCore<Vector3, Path, PathOptions> DOLocalPath(
            this Rigidbody2D target, Path path, float duration, PathMode pathMode = PathMode.Full3D
        )
        {
            Transform trans = target.transform;
            TweenerCore<Vector3, Path, PathOptions> t = DOTween.To(PathPlugin.Get(), () => trans.localPosition, x => target.MovePosition(trans.parent == null ? x : trans.parent.TransformPoint(x)), path, duration)
                .SetTarget(target);

            t.plugOptions.isRigidbody2D = true;
            t.plugOptions.mode = pathMode;
            t.plugOptions.useLocalPosition = true;
            return t;
        }

        #endregion

        #endregion

        #endregion
	}
}
#endif

```

---

### `Plugins/Demigiant/DOTween/Modules/DOTweenModuleSprite.cs`

> Lines: 93 | Size: 4.1KB

```csharp
﻿// Author: Daniele Giardini - http://www.demigiant.com
// Created: 2018/07/13

#if !DOTWEEN_NOSPRITES // MODULE_MARKER
using System;
using UnityEngine;
using DG.Tweening.Core;
using DG.Tweening.Plugins.Options;

#pragma warning disable 1591
namespace DG.Tweening
{
	public static class DOTweenModuleSprite
    {
        #region Shortcuts

        #region SpriteRenderer

        /// <summary>Tweens a SpriteRenderer's color to the given value.
        /// Also stores the spriteRenderer as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Color, Color, ColorOptions> DOColor(this SpriteRenderer target, Color endValue, float duration)
        {
            TweenerCore<Color, Color, ColorOptions> t = DOTween.To(() => target.color, x => target.color = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        /// <summary>Tweens a Material's alpha color to the given value.
        /// Also stores the spriteRenderer as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Color, Color, ColorOptions> DOFade(this SpriteRenderer target, float endValue, float duration)
        {
            TweenerCore<Color, Color, ColorOptions> t = DOTween.ToAlpha(() => target.color, x => target.color = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        /// <summary>Tweens a SpriteRenderer's color using the given gradient
        /// (NOTE 1: only uses the colors of the gradient, not the alphas - NOTE 2: creates a Sequence, not a Tweener).
        /// Also stores the image as the tween's target so it can be used for filtered operations</summary>
        /// <param name="gradient">The gradient to use</param><param name="duration">The duration of the tween</param>
        public static Sequence DOGradientColor(this SpriteRenderer target, Gradient gradient, float duration)
        {
            Sequence s = DOTween.Sequence();
            GradientColorKey[] colors = gradient.colorKeys;
            int len = colors.Length;
            for (int i = 0; i < len; ++i) {
                GradientColorKey c = colors[i];
                if (i == 0 && c.time <= 0) {
                    target.color = c.color;
                    continue;
                }
                float colorDuration = i == len - 1
                    ? duration - s.Duration(false) // Verifies that total duration is correct
                    : duration * (i == 0 ? c.time : c.time - colors[i - 1].time);
                s.Append(target.DOColor(c.color, colorDuration).SetEase(Ease.Linear));
            }
            s.SetTarget(target);
            return s;
        }

        #endregion

        #region Blendables

        #region SpriteRenderer

        /// <summary>Tweens a SpriteRenderer's color to the given value,
        /// in a way that allows other DOBlendableColor tweens to work together on the same target,
        /// instead than fight each other as multiple DOColor would do.
        /// Also stores the SpriteRenderer as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The value to tween to</param><param name="duration">The duration of the tween</param>
        public static Tweener DOBlendableColor(this SpriteRenderer target, Color endValue, float duration)
        {
            endValue = endValue - target.color;
            Color to = new Color(0, 0, 0, 0);
            return DOTween.To(() => to, x => {
                    Color diff = x - to;
                    to = x;
                    target.color += diff;
                }, endValue, duration)
                .Blendable().SetTarget(target);
        }

        #endregion

        #endregion

        #endregion
	}
}
#endif

```

---

### `Plugins/Demigiant/DOTween/Modules/DOTweenModuleUI.cs`

> Lines: 662 | Size: 42.2KB

```csharp
﻿// Author: Daniele Giardini - http://www.demigiant.com
// Created: 2018/07/13

#if !DOTWEEN_NOUI // MODULE_MARKER

using System;
using System.Globalization;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening.Core;
using DG.Tweening.Core.Enums;
using DG.Tweening.Plugins;
using DG.Tweening.Plugins.Options;
using Outline = UnityEngine.UI.Outline;
using Text = UnityEngine.UI.Text;

#pragma warning disable 1591
namespace DG.Tweening
{
	public static class DOTweenModuleUI
    {
        #region Shortcuts

        #region CanvasGroup

        /// <summary>Tweens a CanvasGroup's alpha color to the given value.
        /// Also stores the canvasGroup as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<float, float, FloatOptions> DOFade(this CanvasGroup target, float endValue, float duration)
        {
            TweenerCore<float, float, FloatOptions> t = DOTween.To(() => target.alpha, x => target.alpha = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        #endregion

        #region Graphic

        /// <summary>Tweens an Graphic's color to the given value.
        /// Also stores the image as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Color, Color, ColorOptions> DOColor(this Graphic target, Color endValue, float duration)
        {
            TweenerCore<Color, Color, ColorOptions> t = DOTween.To(() => target.color, x => target.color = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        /// <summary>Tweens an Graphic's alpha color to the given value.
        /// Also stores the image as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Color, Color, ColorOptions> DOFade(this Graphic target, float endValue, float duration)
        {
            TweenerCore<Color, Color, ColorOptions> t = DOTween.ToAlpha(() => target.color, x => target.color = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        #endregion

        #region Image

        /// <summary>Tweens an Image's color to the given value.
        /// Also stores the image as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Color, Color, ColorOptions> DOColor(this Image target, Color endValue, float duration)
        {
            TweenerCore<Color, Color, ColorOptions> t = DOTween.To(() => target.color, x => target.color = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        /// <summary>Tweens an Image's alpha color to the given value.
        /// Also stores the image as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Color, Color, ColorOptions> DOFade(this Image target, float endValue, float duration)
        {
            TweenerCore<Color, Color, ColorOptions> t = DOTween.ToAlpha(() => target.color, x => target.color = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        /// <summary>Tweens an Image's fillAmount to the given value.
        /// Also stores the image as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach (0 to 1)</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<float, float, FloatOptions> DOFillAmount(this Image target, float endValue, float duration)
        {
            if (endValue > 1) endValue = 1;
            else if (endValue < 0) endValue = 0;
            TweenerCore<float, float, FloatOptions> t = DOTween.To(() => target.fillAmount, x => target.fillAmount = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        /// <summary>Tweens an Image's colors using the given gradient
        /// (NOTE 1: only uses the colors of the gradient, not the alphas - NOTE 2: creates a Sequence, not a Tweener).
        /// Also stores the image as the tween's target so it can be used for filtered operations</summary>
        /// <param name="gradient">The gradient to use</param><param name="duration">The duration of the tween</param>
        public static Sequence DOGradientColor(this Image target, Gradient gradient, float duration)
        {
            Sequence s = DOTween.Sequence();
            GradientColorKey[] colors = gradient.colorKeys;
            int len = colors.Length;
            for (int i = 0; i < len; ++i) {
                GradientColorKey c = colors[i];
                if (i == 0 && c.time <= 0) {
                    target.color = c.color;
                    continue;
                }
                float colorDuration = i == len - 1
                    ? duration - s.Duration(false) // Verifies that total duration is correct
                    : duration * (i == 0 ? c.time : c.time - colors[i - 1].time);
                s.Append(target.DOColor(c.color, colorDuration).SetEase(Ease.Linear));
            }
            s.SetTarget(target);
            return s;
        }

        #endregion

        #region LayoutElement

        /// <summary>Tweens an LayoutElement's flexibleWidth/Height to the given value.
        /// Also stores the LayoutElement as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOFlexibleSize(this LayoutElement target, Vector2 endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => new Vector2(target.flexibleWidth, target.flexibleHeight), x => {
                    target.flexibleWidth = x.x;
                    target.flexibleHeight = x.y;
                }, endValue, duration);
            t.SetOptions(snapping).SetTarget(target);
            return t;
        }

        /// <summary>Tweens an LayoutElement's minWidth/Height to the given value.
        /// Also stores the LayoutElement as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOMinSize(this LayoutElement target, Vector2 endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => new Vector2(target.minWidth, target.minHeight), x => {
                target.minWidth = x.x;
                target.minHeight = x.y;
            }, endValue, duration);
            t.SetOptions(snapping).SetTarget(target);
            return t;
        }

        /// <summary>Tweens an LayoutElement's preferredWidth/Height to the given value.
        /// Also stores the LayoutElement as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOPreferredSize(this LayoutElement target, Vector2 endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => new Vector2(target.preferredWidth, target.preferredHeight), x => {
                target.preferredWidth = x.x;
                target.preferredHeight = x.y;
            }, endValue, duration);
            t.SetOptions(snapping).SetTarget(target);
            return t;
        }

        #endregion

        #region Outline

        /// <summary>Tweens a Outline's effectColor to the given value.
        /// Also stores the Outline as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Color, Color, ColorOptions> DOColor(this Outline target, Color endValue, float duration)
        {
            TweenerCore<Color, Color, ColorOptions> t = DOTween.To(() => target.effectColor, x => target.effectColor = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        /// <summary>Tweens a Outline's effectColor alpha to the given value.
        /// Also stores the Outline as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Color, Color, ColorOptions> DOFade(this Outline target, float endValue, float duration)
        {
            TweenerCore<Color, Color, ColorOptions> t = DOTween.ToAlpha(() => target.effectColor, x => target.effectColor = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        /// <summary>Tweens a Outline's effectDistance to the given value.
        /// Also stores the Outline as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOScale(this Outline target, Vector2 endValue, float duration)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => target.effectDistance, x => target.effectDistance = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        #endregion

        #region RectTransform

        /// <summary>Tweens a RectTransform's anchoredPosition to the given value.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOAnchorPos(this RectTransform target, Vector2 endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => target.anchoredPosition, x => target.anchoredPosition = x, endValue, duration);
            t.SetOptions(snapping).SetTarget(target);
            return t;
        }
        /// <summary>Tweens a RectTransform's anchoredPosition X to the given value.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOAnchorPosX(this RectTransform target, float endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => target.anchoredPosition, x => target.anchoredPosition = x, new Vector2(endValue, 0), duration);
            t.SetOptions(AxisConstraint.X, snapping).SetTarget(target);
            return t;
        }
        /// <summary>Tweens a RectTransform's anchoredPosition Y to the given value.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOAnchorPosY(this RectTransform target, float endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => target.anchoredPosition, x => target.anchoredPosition = x, new Vector2(0, endValue), duration);
            t.SetOptions(AxisConstraint.Y, snapping).SetTarget(target);
            return t;
        }

        /// <summary>Tweens a RectTransform's anchoredPosition3D to the given value.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector3, Vector3, VectorOptions> DOAnchorPos3D(this RectTransform target, Vector3 endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector3, Vector3, VectorOptions> t = DOTween.To(() => target.anchoredPosition3D, x => target.anchoredPosition3D = x, endValue, duration);
            t.SetOptions(snapping).SetTarget(target);
            return t;
        }
        /// <summary>Tweens a RectTransform's anchoredPosition3D X to the given value.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector3, Vector3, VectorOptions> DOAnchorPos3DX(this RectTransform target, float endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector3, Vector3, VectorOptions> t = DOTween.To(() => target.anchoredPosition3D, x => target.anchoredPosition3D = x, new Vector3(endValue, 0, 0), duration);
            t.SetOptions(AxisConstraint.X, snapping).SetTarget(target);
            return t;
        }
        /// <summary>Tweens a RectTransform's anchoredPosition3D Y to the given value.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector3, Vector3, VectorOptions> DOAnchorPos3DY(this RectTransform target, float endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector3, Vector3, VectorOptions> t = DOTween.To(() => target.anchoredPosition3D, x => target.anchoredPosition3D = x, new Vector3(0, endValue, 0), duration);
            t.SetOptions(AxisConstraint.Y, snapping).SetTarget(target);
            return t;
        }
        /// <summary>Tweens a RectTransform's anchoredPosition3D Z to the given value.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector3, Vector3, VectorOptions> DOAnchorPos3DZ(this RectTransform target, float endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector3, Vector3, VectorOptions> t = DOTween.To(() => target.anchoredPosition3D, x => target.anchoredPosition3D = x, new Vector3(0, 0, endValue), duration);
            t.SetOptions(AxisConstraint.Z, snapping).SetTarget(target);
            return t;
        }

        /// <summary>Tweens a RectTransform's anchorMax to the given value.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOAnchorMax(this RectTransform target, Vector2 endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => target.anchorMax, x => target.anchorMax = x, endValue, duration);
            t.SetOptions(snapping).SetTarget(target);
            return t;
        }

        /// <summary>Tweens a RectTransform's anchorMin to the given value.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOAnchorMin(this RectTransform target, Vector2 endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => target.anchorMin, x => target.anchorMin = x, endValue, duration);
            t.SetOptions(snapping).SetTarget(target);
            return t;
        }

        /// <summary>Tweens a RectTransform's pivot to the given value.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOPivot(this RectTransform target, Vector2 endValue, float duration)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => target.pivot, x => target.pivot = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }
        /// <summary>Tweens a RectTransform's pivot X to the given value.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOPivotX(this RectTransform target, float endValue, float duration)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => target.pivot, x => target.pivot = x, new Vector2(endValue, 0), duration);
            t.SetOptions(AxisConstraint.X).SetTarget(target);
            return t;
        }
        /// <summary>Tweens a RectTransform's pivot Y to the given value.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOPivotY(this RectTransform target, float endValue, float duration)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => target.pivot, x => target.pivot = x, new Vector2(0, endValue), duration);
            t.SetOptions(AxisConstraint.Y).SetTarget(target);
            return t;
        }

        /// <summary>Tweens a RectTransform's sizeDelta to the given value.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOSizeDelta(this RectTransform target, Vector2 endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => target.sizeDelta, x => target.sizeDelta = x, endValue, duration);
            t.SetOptions(snapping).SetTarget(target);
            return t;
        }

        /// <summary>Punches a RectTransform's anchoredPosition towards the given direction and then back to the starting one
        /// as if it was connected to the starting position via an elastic.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="punch">The direction and strength of the punch (added to the RectTransform's current position)</param>
        /// <param name="duration">The duration of the tween</param>
        /// <param name="vibrato">Indicates how much will the punch vibrate</param>
        /// <param name="elasticity">Represents how much (0 to 1) the vector will go beyond the starting position when bouncing backwards.
        /// 1 creates a full oscillation between the punch direction and the opposite direction,
        /// while 0 oscillates only between the punch and the start position</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static Tweener DOPunchAnchorPos(this RectTransform target, Vector2 punch, float duration, int vibrato = 10, float elasticity = 1, bool snapping = false)
        {
            return DOTween.Punch(() => target.anchoredPosition, x => target.anchoredPosition = x, punch, duration, vibrato, elasticity)
                .SetTarget(target).SetOptions(snapping);
        }

        /// <summary>Shakes a RectTransform's anchoredPosition with the given values.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="duration">The duration of the tween</param>
        /// <param name="strength">The shake strength</param>
        /// <param name="vibrato">Indicates how much will the shake vibrate</param>
        /// <param name="randomness">Indicates how much the shake will be random (0 to 180 - values higher than 90 kind of suck, so beware). 
        /// Setting it to 0 will shake along a single direction.</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        /// <param name="fadeOut">If TRUE the shake will automatically fadeOut smoothly within the tween's duration, otherwise it will not</param>
        /// <param name="randomnessMode">Randomness mode</param>
        public static Tweener DOShakeAnchorPos(this RectTransform target, float duration, float strength = 100, int vibrato = 10, float randomness = 90, bool snapping = false, bool fadeOut = true, ShakeRandomnessMode randomnessMode = ShakeRandomnessMode.Full)
        {
            return DOTween.Shake(() => target.anchoredPosition, x => target.anchoredPosition = x, duration, strength, vibrato, randomness, true, fadeOut, randomnessMode)
                .SetTarget(target).SetSpecialStartupMode(SpecialStartupMode.SetShake).SetOptions(snapping);
        }
        /// <summary>Shakes a RectTransform's anchoredPosition with the given values.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="duration">The duration of the tween</param>
        /// <param name="strength">The shake strength on each axis</param>
        /// <param name="vibrato">Indicates how much will the shake vibrate</param>
        /// <param name="randomness">Indicates how much the shake will be random (0 to 180 - values higher than 90 kind of suck, so beware). 
        /// Setting it to 0 will shake along a single direction.</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        /// <param name="fadeOut">If TRUE the shake will automatically fadeOut smoothly within the tween's duration, otherwise it will not</param>
        /// <param name="randomnessMode">Randomness mode</param>
        public static Tweener DOShakeAnchorPos(this RectTransform target, float duration, Vector2 strength, int vibrato = 10, float randomness = 90, bool snapping = false, bool fadeOut = true, ShakeRandomnessMode randomnessMode = ShakeRandomnessMode.Full)
        {
            return DOTween.Shake(() => target.anchoredPosition, x => target.anchoredPosition = x, duration, strength, vibrato, randomness, fadeOut, randomnessMode)
                .SetTarget(target).SetSpecialStartupMode(SpecialStartupMode.SetShake).SetOptions(snapping);
        }

        #region Special

        /// <summary>Tweens a RectTransform's anchoredPosition to the given value, while also applying a jump effect along the Y axis.
        /// Returns a Sequence instead of a Tweener.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param>
        /// <param name="jumpPower">Power of the jump (the max height of the jump is represented by this plus the final Y offset)</param>
        /// <param name="numJumps">Total number of jumps</param>
        /// <param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static Sequence DOJumpAnchorPos(this RectTransform target, Vector2 endValue, float jumpPower, int numJumps, float duration, bool snapping = false)
        {
            if (numJumps < 1) numJumps = 1;
            float startPosY = 0;
            float offsetY = -1;
            bool offsetYSet = false;

            // Separate Y Tween so we can elaborate elapsedPercentage on that insted of on the Sequence
            // (in case users add a delay or other elements to the Sequence)
            Sequence s = DOTween.Sequence();
            Tween yTween = DOTween.To(() => target.anchoredPosition, x => target.anchoredPosition = x, new Vector2(0, jumpPower), duration / (numJumps * 2))
                .SetOptions(AxisConstraint.Y, snapping).SetEase(Ease.OutQuad).SetRelative()
                .SetLoops(numJumps * 2, LoopType.Yoyo)
                .OnStart(()=> startPosY = target.anchoredPosition.y);
            s.Append(DOTween.To(() => target.anchoredPosition, x => target.anchoredPosition = x, new Vector2(endValue.x, 0), duration)
                    .SetOptions(AxisConstraint.X, snapping).SetEase(Ease.Linear)
                ).Join(yTween)
                .SetTarget(target).SetEase(DOTween.defaultEaseType);
            s.OnUpdate(() => {
                if (!offsetYSet) {
                    offsetYSet = true;
                    offsetY = s.isRelative ? endValue.y : endValue.y - startPosY;
                }
                Vector2 pos = target.anchoredPosition;
                pos.y += DOVirtual.EasedValue(0, offsetY, s.ElapsedDirectionalPercentage(), Ease.OutQuad);
                target.anchoredPosition = pos;
            });
            return s;
        }

        #endregion

        #endregion

        #region ScrollRect

        /// <summary>Tweens a ScrollRect's horizontal/verticalNormalizedPosition to the given value.
        /// Also stores the ScrollRect as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static Tweener DONormalizedPos(this ScrollRect target, Vector2 endValue, float duration, bool snapping = false)
        {
            return DOTween.To(() => new Vector2(target.horizontalNormalizedPosition, target.verticalNormalizedPosition),
                x => {
                    target.horizontalNormalizedPosition = x.x;
                    target.verticalNormalizedPosition = x.y;
                }, endValue, duration)
                .SetOptions(snapping).SetTarget(target);
        }
        /// <summary>Tweens a ScrollRect's horizontalNormalizedPosition to the given value.
        /// Also stores the ScrollRect as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static Tweener DOHorizontalNormalizedPos(this ScrollRect target, float endValue, float duration, bool snapping = false)
        {
            return DOTween.To(() => target.horizontalNormalizedPosition, x => target.horizontalNormalizedPosition = x, endValue, duration)
                .SetOptions(snapping).SetTarget(target);
        }
        /// <summary>Tweens a ScrollRect's verticalNormalizedPosition to the given value.
        /// Also stores the ScrollRect as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static Tweener DOVerticalNormalizedPos(this ScrollRect target, float endValue, float duration, bool snapping = false)
        {
            return DOTween.To(() => target.verticalNormalizedPosition, x => target.verticalNormalizedPosition = x, endValue, duration)
                .SetOptions(snapping).SetTarget(target);
        }

        #endregion

        #region Slider

        /// <summary>Tweens a Slider's value to the given value.
        /// Also stores the Slider as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<float, float, FloatOptions> DOValue(this Slider target, float endValue, float duration, bool snapping = false)
        {
            TweenerCore<float, float, FloatOptions> t = DOTween.To(() => target.value, x => target.value = x, endValue, duration);
            t.SetOptions(snapping).SetTarget(target);
            return t;
        }

        #endregion

        #region Text

        /// <summary>Tweens a Text's color to the given value.
        /// Also stores the Text as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Color, Color, ColorOptions> DOColor(this Text target, Color endValue, float duration)
        {
            TweenerCore<Color, Color, ColorOptions> t = DOTween.To(() => target.color, x => target.color = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        /// <summary>
        /// Tweens a Text's text from one integer to another, with options for thousands separators
        /// </summary>
        /// <param name="fromValue">The value to start from</param>
        /// <param name="endValue">The end value to reach</param>
        /// <param name="duration">The duration of the tween</param>
        /// <param name="addThousandsSeparator">If TRUE (default) also adds thousands separators</param>
        /// <param name="culture">The <see cref="CultureInfo"/> to use (InvariantCulture if NULL)</param>
        public static TweenerCore<int, int, NoOptions> DOCounter(
            this Text target, int fromValue, int endValue, float duration, bool addThousandsSeparator = true, CultureInfo culture = null
        ){
            int v = fromValue;
            CultureInfo cInfo = !addThousandsSeparator ? null : culture ?? CultureInfo.InvariantCulture;
            TweenerCore<int, int, NoOptions> t = DOTween.To(() => v, x => {
                v = x;
                target.text = addThousandsSeparator
                    ? v.ToString("N0", cInfo)
                    : v.ToString();
            }, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        /// <summary>Tweens a Text's alpha color to the given value.
        /// Also stores the Text as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Color, Color, ColorOptions> DOFade(this Text target, float endValue, float duration)
        {
            TweenerCore<Color, Color, ColorOptions> t = DOTween.ToAlpha(() => target.color, x => target.color = x, endValue, duration);
            t.SetTarget(target);
            return t;
        }

        /// <summary>Tweens a Text's text to the given value.
        /// Also stores the Text as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end string to tween to</param><param name="duration">The duration of the tween</param>
        /// <param name="richTextEnabled">If TRUE (default), rich text will be interpreted correctly while animated,
        /// otherwise all tags will be considered as normal text</param>
        /// <param name="scrambleMode">The type of scramble mode to use, if any</param>
        /// <param name="scrambleChars">A string containing the characters to use for scrambling.
        /// Use as many characters as possible (minimum 10) because DOTween uses a fast scramble mode which gives better results with more characters.
        /// Leave it to NULL (default) to use default ones</param>
        public static TweenerCore<string, string, StringOptions> DOText(this Text target, string endValue, float duration, bool richTextEnabled = true, ScrambleMode scrambleMode = ScrambleMode.None, string scrambleChars = null)
        {
            if (endValue == null) {
                if (Debugger.logPriority > 0) Debugger.LogWarning("You can't pass a NULL string to DOText: an empty string will be used instead to avoid errors");
                endValue = "";
            }
            TweenerCore<string, string, StringOptions> t = DOTween.To(() => target.text, x => target.text = x, endValue, duration);
            t.SetOptions(richTextEnabled, scrambleMode, scrambleChars)
                .SetTarget(target);
            return t;
        }

        #endregion

        #region Blendables

        #region Graphic

        /// <summary>Tweens a Graphic's color to the given value,
        /// in a way that allows other DOBlendableColor tweens to work together on the same target,
        /// instead than fight each other as multiple DOColor would do.
        /// Also stores the Graphic as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The value to tween to</param><param name="duration">The duration of the tween</param>
        public static Tweener DOBlendableColor(this Graphic target, Color endValue, float duration)
        {
            endValue = endValue - target.color;
            Color to = new Color(0, 0, 0, 0);
            return DOTween.To(() => to, x => {
                Color diff = x - to;
                to = x;
                target.color += diff;
            }, endValue, duration)
                .Blendable().SetTarget(target);
        }

        #endregion

        #region Image

        /// <summary>Tweens a Image's color to the given value,
        /// in a way that allows other DOBlendableColor tweens to work together on the same target,
        /// instead than fight each other as multiple DOColor would do.
        /// Also stores the Image as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The value to tween to</param><param name="duration">The duration of the tween</param>
        public static Tweener DOBlendableColor(this Image target, Color endValue, float duration)
        {
            endValue = endValue - target.color;
            Color to = new Color(0, 0, 0, 0);
            return DOTween.To(() => to, x => {
                Color diff = x - to;
                to = x;
                target.color += diff;
            }, endValue, duration)
                .Blendable().SetTarget(target);
        }

        #endregion

        #region Text

        /// <summary>Tweens a Text's color BY the given value,
        /// in a way that allows other DOBlendableColor tweens to work together on the same target,
        /// instead than fight each other as multiple DOColor would do.
        /// Also stores the Text as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The value to tween to</param><param name="duration">The duration of the tween</param>
        public static Tweener DOBlendableColor(this Text target, Color endValue, float duration)
        {
            endValue = endValue - target.color;
            Color to = new Color(0, 0, 0, 0);
            return DOTween.To(() => to, x => {
                Color diff = x - to;
                to = x;
                target.color += diff;
            }, endValue, duration)
                .Blendable().SetTarget(target);
        }

        #endregion

        #endregion

        #region Shapes

        /// <summary>Tweens a RectTransform's anchoredPosition so that it draws a circle around the given center.
        /// Also stores the RectTransform as the tween's target so it can be used for filtered operations.<para/>
        /// IMPORTANT: SetFrom(value) requires a <see cref="Vector2"/> instead of a float, where the X property represents the "from degrees value"</summary>
        /// <param name="center">Circle-center/pivot around which to rotate (in UI anchoredPosition coordinates)</param>
        /// <param name="endValueDegrees">The end value degrees to reach (to rotate counter-clockwise pass a negative value)</param>
        /// <param name="duration">The duration of the tween</param>
        /// <param name="relativeCenter">If TRUE the <see cref="center"/> coordinates will be considered as relative to the target's current anchoredPosition</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector2, Vector2, CircleOptions> DOShapeCircle(
            this RectTransform target, Vector2 center, float endValueDegrees, float duration, bool relativeCenter = false, bool snapping = false
        )
        {
            TweenerCore<Vector2, Vector2, CircleOptions> t = DOTween.To(
                CirclePlugin.Get(), () => target.anchoredPosition, x => target.anchoredPosition = x, center, duration
            );
            t.SetOptions(endValueDegrees, relativeCenter, snapping).SetTarget(target);
            return t;
        }

        #endregion

        #endregion

        // █████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
        // ███ INTERNAL CLASSES ████████████████████████████████████████████████████████████████████████████████████████████████
        // █████████████████████████████████████████████████████████████████████████████████████████████████████████████████████

        public static class Utils
        {
            /// <summary>
            /// Converts the anchoredPosition of the first RectTransform to the second RectTransform,
            /// taking into consideration offset, anchors and pivot, and returns the new anchoredPosition
            /// </summary>
            public static Vector2 SwitchToRectTransform(RectTransform from, RectTransform to)
            {
                Vector2 localPoint;
                Vector2 fromPivotDerivedOffset = new Vector2(from.rect.width * 0.5f + from.rect.xMin, from.rect.height * 0.5f + from.rect.yMin);
                Vector2 screenP = RectTransformUtility.WorldToScreenPoint(null, from.position);
                screenP += fromPivotDerivedOffset;
                RectTransformUtility.ScreenPointToLocalPointInRectangle(to, screenP, null, out localPoint);
                Vector2 pivotDerivedOffset = new Vector2(to.rect.width * 0.5f + to.rect.xMin, to.rect.height * 0.5f + to.rect.yMin);
                return to.anchoredPosition + localPoint - pivotDerivedOffset;
            }
        }
	}
}
#endif

```

---

### `Plugins/Demigiant/DOTween/Modules/DOTweenModuleUIToolkit.cs`

> Lines: 161 | Size: 11.7KB

```csharp
﻿// Author: Daniele Giardini - http://www.demigiant.com
// Created: 2026/01/08

#if DOTWEEN_UITOOLKIT && UNITY_2021_3_OR_NEWER // MODULE_MARKER

using UnityEngine;
using UnityEngine.UIElements;
using DG.Tweening.Core;
using DG.Tweening.Core.Enums;
using DG.Tweening.Plugins.Options;

#pragma warning disable 1591
namespace DG.Tweening
{
	public static class DOTweenModuleUIToolkit
    {
        #region Shortcuts

        #region VisualElement

        /// <summary>Tweens a VisualElement's position (via style.translate) to the given value.
        /// Also stores the VisualElement as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector3, Vector3, VectorOptions> DOMove(this VisualElement target, Vector3 endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector3, Vector3, VectorOptions> t
                = DOTween.To(() => target.resolvedStyle.translate, x => target.style.translate = new Translate(x.x, x.y, x.z), endValue, duration);
            t.SetOptions(snapping).SetTarget(target);
            return t;
        }
        /// <summary>Tweens a VisualElement's position (via style.translate) to the given value.
        /// Also stores the VisualElement as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOMove(this VisualElement target, Vector2 endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t
                = DOTween.To(() => (Vector2)target.resolvedStyle.translate, x => target.style.translate = new Translate(x.x, x.y, 0), endValue, duration);
            t.SetOptions(snapping).SetTarget(target);
            return t;
        }
        /// <summary>Tweens a VisualElement's X position (via style.translate) to the given value.
        /// Also stores the VisualElement as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector3, Vector3, VectorOptions> DOMoveX(this VisualElement target, float endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector3, Vector3, VectorOptions> t
                = DOTween.To(() => target.resolvedStyle.translate, x => target.style.translate = new Translate(x.x, x.y, x.z), new Vector3(endValue, 0, 0), duration);
            t.SetOptions(AxisConstraint.X, snapping).SetTarget(target);
            return t;
        }
        /// <summary>Tweens a VisualElement's Y position (via style.translate) to the given value.
        /// Also stores the VisualElement as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector3, Vector3, VectorOptions> DOMoveY(this VisualElement target, float endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector3, Vector3, VectorOptions> t
                = DOTween.To(() => target.resolvedStyle.translate, x => target.style.translate = new Translate(x.x, x.y, x.z), new Vector3(0, endValue, 0), duration);
            t.SetOptions(AxisConstraint.Y, snapping).SetTarget(target);
            return t;
        }
        /// <summary>Tweens a VisualElement's Z position (via style.translate) to the given value.
        /// Also stores the VisualElement as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static TweenerCore<Vector3, Vector3, VectorOptions> DOMoveZ(this VisualElement target, float endValue, float duration, bool snapping = false)
        {
            TweenerCore<Vector3, Vector3, VectorOptions> t
                = DOTween.To(() => target.resolvedStyle.translate, x => target.style.translate = new Translate(x.x, x.y, x.z), new Vector3(0, 0, endValue), duration);
            t.SetOptions(AxisConstraint.Z, snapping).SetTarget(target);
            return t;
        }
        
        /// <summary>Tweens a VisualElement's scale (via style.scale) to the given value.
        /// Also stores the VisualElement as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOScale(this VisualElement target, Vector2 endValue, float duration)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t
                = DOTween.To(() => (Vector2)target.resolvedStyle.scale.value, x => target.style.scale = new Scale(x), endValue, duration);
            t.SetTarget(target);
            return t;
        }
        /// <summary>Tweens a VisualElement's scale (via style.scale) to the given value.
        /// Also stores the VisualElement as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOScale(this VisualElement target, float endValue, float duration)
        {
            TweenerCore<Vector2, Vector2, VectorOptions> t
                = DOTween.To(() => (Vector2)target.resolvedStyle.scale.value, x => target.style.scale = new Scale(x), new Vector2(endValue, endValue), duration);
            t.SetTarget(target);
            return t;
        }
        
        /// <summary>Tweens a VisualElement's rotation (via style.rotate) to the given value.
        /// Also stores the VisualElement as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param><param name="duration">The duration of the tween</param>
        public static TweenerCore<float, float, FloatOptions> DORotate(this VisualElement target, float endValue, float duration)
        {
            TweenerCore<float, float, FloatOptions> t
                = DOTween.To(() => target.resolvedStyle.rotate.angle.value, x => target.style.rotate = new Rotate(x), endValue, duration);
            t.SetTarget(target);
            return t;
        }
        
        /// <summary>Punches a VisualElement's position towards the given direction and then back to the starting one
        /// as if it was connected to the starting position via an elastic.
        /// Also stores the VisualElement as the tween's target so it can be used for filtered operations</summary>
        /// <param name="punch">The direction and strength of the punch (added to the VisualElement's current position)</param>
        /// <param name="duration">The duration of the tween</param>
        /// <param name="vibrato">Indicates how much will the punch vibrate</param>
        /// <param name="elasticity">Represents how much (0 to 1) the vector will go beyond the starting position when bouncing backwards.
        /// 1 creates a full oscillation between the punch direction and the opposite direction,
        /// while 0 oscillates only between the punch and the start position</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        public static Tweener DOPunch(this VisualElement target, Vector3 punch, float duration, int vibrato = 10, float elasticity = 1, bool snapping = false)
        {
            return DOTween.Punch(() => target.resolvedStyle.translate, x => target.style.translate = new Translate(x.x, x.y, x.z), punch, duration, vibrato, elasticity)
                .SetTarget(target).SetOptions(snapping);
        }
        
        /// <summary>Shakes a VisualElement's position with the given values.
        /// Also stores the VisualElement as the tween's target so it can be used for filtered operations</summary>
        /// <param name="duration">The duration of the tween</param>
        /// <param name="strength">The shake strength</param>
        /// <param name="vibrato">Indicates how much will the shake vibrate</param>
        /// <param name="randomness">Indicates how much the shake will be random (0 to 180 - values higher than 90 kind of suck, so beware). 
        /// Setting it to 0 will shake along a single direction.</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        /// <param name="fadeOut">If TRUE the shake will automatically fadeOut smoothly within the tween's duration, otherwise it will not</param>
        /// <param name="randomnessMode">Randomness mode</param>
        public static Tweener DOShake(this VisualElement target, float duration, float strength = 100, int vibrato = 10, float randomness = 90, bool snapping = false, bool fadeOut = true, ShakeRandomnessMode randomnessMode = ShakeRandomnessMode.Full)
        {
            return DOTween.Shake(() => target.resolvedStyle.translate, x => target.style.translate = new Translate(x.x, x.y, x.z), duration, strength, vibrato, randomness, true, fadeOut, randomnessMode)
                .SetTarget(target).SetSpecialStartupMode(SpecialStartupMode.SetShake).SetOptions(snapping);
        }
        /// <summary>Shakes a VisualElement's position with the given values.
        /// Also stores the VisualElement as the tween's target so it can be used for filtered operations</summary>
        /// <param name="duration">The duration of the tween</param>
        /// <param name="strength">The shake strength on each axis</param>
        /// <param name="vibrato">Indicates how much will the shake vibrate</param>
        /// <param name="randomness">Indicates how much the shake will be random (0 to 180 - values higher than 90 kind of suck, so beware). 
        /// Setting it to 0 will shake along a single direction.</param>
        /// <param name="snapping">If TRUE the tween will smoothly snap all values to integers</param>
        /// <param name="fadeOut">If TRUE the shake will automatically fadeOut smoothly within the tween's duration, otherwise it will not</param>
        /// <param name="randomnessMode">Randomness mode</param>
        public static Tweener DOShake(this VisualElement target, float duration, Vector2 strength, int vibrato = 10, float randomness = 90, bool snapping = false, bool fadeOut = true, ShakeRandomnessMode randomnessMode = ShakeRandomnessMode.Full)
        {
            return DOTween.Shake(() => target.resolvedStyle.translate, x => target.style.translate = new Translate(x.x, x.y, x.z), duration, strength, vibrato, randomness, fadeOut, randomnessMode)
                .SetTarget(target).SetSpecialStartupMode(SpecialStartupMode.SetShake).SetOptions(snapping);
        }

        #endregion

        #endregion
	}
}
#endif

```

---

### `Plugins/Demigiant/DOTween/Modules/DOTweenModuleUnityVersion.cs`

> Lines: 389 | Size: 17.9KB

```csharp
﻿// Author: Daniele Giardini - http://www.demigiant.com
// Created: 2018/07/13

using System;
using UnityEngine;
using DG.Tweening.Core;
using DG.Tweening.Plugins.Options;
//#if UNITY_2018_1_OR_NEWER && (NET_4_6 || NET_STANDARD_2_0)
//using Task = System.Threading.Tasks.Task;
//#endif

#pragma warning disable 1591
namespace DG.Tweening
{
    /// <summary>
    /// Shortcuts/functions that are not strictly related to specific Modules
    /// but are available only on some Unity versions
    /// </summary>
	public static class DOTweenModuleUnityVersion
    {
        #region Material

        /// <summary>Tweens a Material's color using the given gradient
        /// (NOTE 1: only uses the colors of the gradient, not the alphas - NOTE 2: creates a Sequence, not a Tweener).
        /// Also stores the image as the tween's target so it can be used for filtered operations</summary>
        /// <param name="gradient">The gradient to use</param><param name="duration">The duration of the tween</param>
        public static Sequence DOGradientColor(this Material target, Gradient gradient, float duration)
        {
            Sequence s = DOTween.Sequence();
            GradientColorKey[] colors = gradient.colorKeys;
            int len = colors.Length;
            for (int i = 0; i < len; ++i) {
                GradientColorKey c = colors[i];
                if (i == 0 && c.time <= 0) {
                    target.color = c.color;
                    continue;
                }
                float colorDuration = i == len - 1
                    ? duration - s.Duration(false) // Verifies that total duration is correct
                    : duration * (i == 0 ? c.time : c.time - colors[i - 1].time);
                s.Append(target.DOColor(c.color, colorDuration).SetEase(Ease.Linear));
            }
            s.SetTarget(target);
            return s;
        }
        /// <summary>Tweens a Material's named color property using the given gradient
        /// (NOTE 1: only uses the colors of the gradient, not the alphas - NOTE 2: creates a Sequence, not a Tweener).
        /// Also stores the image as the tween's target so it can be used for filtered operations</summary>
        /// <param name="gradient">The gradient to use</param>
        /// <param name="property">The name of the material property to tween (like _Tint or _SpecColor)</param>
        /// <param name="duration">The duration of the tween</param>
        public static Sequence DOGradientColor(this Material target, Gradient gradient, string property, float duration)
        {
            Sequence s = DOTween.Sequence();
            GradientColorKey[] colors = gradient.colorKeys;
            int len = colors.Length;
            for (int i = 0; i < len; ++i) {
                GradientColorKey c = colors[i];
                if (i == 0 && c.time <= 0) {
                    target.SetColor(property, c.color);
                    continue;
                }
                float colorDuration = i == len - 1
                    ? duration - s.Duration(false) // Verifies that total duration is correct
                    : duration * (i == 0 ? c.time : c.time - colors[i - 1].time);
                s.Append(target.DOColor(c.color, property, colorDuration).SetEase(Ease.Linear));
            }
            s.SetTarget(target);
            return s;
        }

        #endregion

        #region CustomYieldInstructions

        /// <summary>
        /// Returns a <see cref="CustomYieldInstruction"/> that waits until the tween is killed or complete.
        /// It can be used inside a coroutine as a yield.
        /// <para>Example usage:</para><code>yield return myTween.WaitForCompletion(true);</code>
        /// </summary>
        public static CustomYieldInstruction WaitForCompletion(this Tween t, bool returnCustomYieldInstruction)
        {
            if (!t.active) {
                if (Debugger.logPriority > 0) Debugger.LogInvalidTween(t);
                return null;
            }
            return new DOTweenCYInstruction.WaitForCompletion(t);
        }

        /// <summary>
        /// Returns a <see cref="CustomYieldInstruction"/> that waits until the tween is killed or rewinded.
        /// It can be used inside a coroutine as a yield.
        /// <para>Example usage:</para><code>yield return myTween.WaitForRewind();</code>
        /// </summary>
        public static CustomYieldInstruction WaitForRewind(this Tween t, bool returnCustomYieldInstruction)
        {
            if (!t.active) {
                if (Debugger.logPriority > 0) Debugger.LogInvalidTween(t);
                return null;
            }
            return new DOTweenCYInstruction.WaitForRewind(t);
        }

        /// <summary>
        /// Returns a <see cref="CustomYieldInstruction"/> that waits until the tween is killed.
        /// It can be used inside a coroutine as a yield.
        /// <para>Example usage:</para><code>yield return myTween.WaitForKill();</code>
        /// </summary>
        public static CustomYieldInstruction WaitForKill(this Tween t, bool returnCustomYieldInstruction)
        {
            if (!t.active) {
                if (Debugger.logPriority > 0) Debugger.LogInvalidTween(t);
                return null;
            }
            return new DOTweenCYInstruction.WaitForKill(t);
        }

        /// <summary>
        /// Returns a <see cref="CustomYieldInstruction"/> that waits until the tween is killed or has gone through the given amount of loops.
        /// It can be used inside a coroutine as a yield.
        /// <para>Example usage:</para><code>yield return myTween.WaitForElapsedLoops(2);</code>
        /// </summary>
        /// <param name="elapsedLoops">Elapsed loops to wait for</param>
        public static CustomYieldInstruction WaitForElapsedLoops(this Tween t, int elapsedLoops, bool returnCustomYieldInstruction)
        {
            if (!t.active) {
                if (Debugger.logPriority > 0) Debugger.LogInvalidTween(t);
                return null;
            }
            return new DOTweenCYInstruction.WaitForElapsedLoops(t, elapsedLoops);
        }

        /// <summary>
        /// Returns a <see cref="CustomYieldInstruction"/> that waits until the tween is killed
        /// or has reached the given time position (loops included, delays excluded).
        /// It can be used inside a coroutine as a yield.
        /// <para>Example usage:</para><code>yield return myTween.WaitForPosition(2.5f);</code>
        /// </summary>
        /// <param name="position">Position (loops included, delays excluded) to wait for</param>
        public static CustomYieldInstruction WaitForPosition(this Tween t, float position, bool returnCustomYieldInstruction)
        {
            if (!t.active) {
                if (Debugger.logPriority > 0) Debugger.LogInvalidTween(t);
                return null;
            }
            return new DOTweenCYInstruction.WaitForPosition(t, position);
        }

        /// <summary>
        /// Returns a <see cref="CustomYieldInstruction"/> that waits until the tween is killed or started
        /// (meaning when the tween is set in a playing state the first time, after any eventual delay).
        /// It can be used inside a coroutine as a yield.
        /// <para>Example usage:</para><code>yield return myTween.WaitForStart();</code>
        /// </summary>
        public static CustomYieldInstruction WaitForStart(this Tween t, bool returnCustomYieldInstruction)
        {
            if (!t.active) {
                if (Debugger.logPriority > 0) Debugger.LogInvalidTween(t);
                return null;
            }
            return new DOTweenCYInstruction.WaitForStart(t);
        }

        #endregion

#if UNITY_2018_1_OR_NEWER
        #region Unity 2018.1 or Newer

        #region Material

        /// <summary>Tweens a Material's named texture offset property with the given ID to the given value.
        /// Also stores the material as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param>
        /// <param name="propertyID">The ID of the material property to tween (also called nameID in Unity's manual)</param>
        /// <param name="duration">The duration of the tween</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOOffset(this Material target, Vector2 endValue, int propertyID, float duration)
        {
            if (!target.HasProperty(propertyID)) {
                if (Debugger.logPriority > 0) Debugger.LogMissingMaterialProperty(propertyID);
                return null;
            }
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => target.GetTextureOffset(propertyID), x => target.SetTextureOffset(propertyID, x), endValue, duration);
            t.SetTarget(target);
            return t;
        }

        /// <summary>Tweens a Material's named texture scale property with the given ID to the given value.
        /// Also stores the material as the tween's target so it can be used for filtered operations</summary>
        /// <param name="endValue">The end value to reach</param>
        /// <param name="propertyID">The ID of the material property to tween (also called nameID in Unity's manual)</param>
        /// <param name="duration">The duration of the tween</param>
        public static TweenerCore<Vector2, Vector2, VectorOptions> DOTiling(this Material target, Vector2 endValue, int propertyID, float duration)
        {
            if (!target.HasProperty(propertyID)) {
                if (Debugger.logPriority > 0) Debugger.LogMissingMaterialProperty(propertyID);
                return null;
            }
            TweenerCore<Vector2, Vector2, VectorOptions> t = DOTween.To(() => target.GetTextureScale(propertyID), x => target.SetTextureScale(propertyID, x), endValue, duration);
            t.SetTarget(target);
            return t;
        }

        #endregion

        #region .NET 4.6 or Newer

#if UNITY_2018_1_OR_NEWER && (NET_4_6 || NET_STANDARD_2_0)

        #region Async Instructions

        /// <summary>
        /// Returns an async <see cref="System.Threading.Tasks.Task"/> that waits until the tween is killed or complete.
        /// It can be used inside an async operation.
        /// <para>Example usage:</para><code>await myTween.WaitForCompletion();</code>
        /// </summary>
        public static async System.Threading.Tasks.Task AsyncWaitForCompletion(this Tween t)
        {
            if (!t.active) {
                if (Debugger.logPriority > 0) Debugger.LogInvalidTween(t);
                return;
            }
            while (t.active && !t.IsComplete()) await System.Threading.Tasks.Task.Yield();
        }

        /// <summary>
        /// Returns an async <see cref="System.Threading.Tasks.Task"/> that waits until the tween is killed or rewinded.
        /// It can be used inside an async operation.
        /// <para>Example usage:</para><code>await myTween.AsyncWaitForRewind();</code>
        /// </summary>
        public static async System.Threading.Tasks.Task AsyncWaitForRewind(this Tween t)
        {
            if (!t.active) {
                if (Debugger.logPriority > 0) Debugger.LogInvalidTween(t);
                return;
            }
            while (t.active && (!t.playedOnce || t.position * (t.CompletedLoops() + 1) > 0)) await System.Threading.Tasks.Task.Yield();
        }

        /// <summary>
        /// Returns an async <see cref="System.Threading.Tasks.Task"/> that waits until the tween is killed.
        /// It can be used inside an async operation.
        /// <para>Example usage:</para><code>await myTween.AsyncWaitForKill();</code>
        /// </summary>
        public static async System.Threading.Tasks.Task AsyncWaitForKill(this Tween t)
        {
            if (!t.active) {
                if (Debugger.logPriority > 0) Debugger.LogInvalidTween(t);
                return;
            }
            while (t.active) await System.Threading.Tasks.Task.Yield();
        }

        /// <summary>
        /// Returns an async <see cref="System.Threading.Tasks.Task"/> that waits until the tween is killed or has gone through the given amount of loops.
        /// It can be used inside an async operation.
        /// <para>Example usage:</para><code>await myTween.AsyncWaitForElapsedLoops();</code>
        /// </summary>
        /// <param name="elapsedLoops">Elapsed loops to wait for</param>
        public static async System.Threading.Tasks.Task AsyncWaitForElapsedLoops(this Tween t, int elapsedLoops)
        {
            if (!t.active) {
                if (Debugger.logPriority > 0) Debugger.LogInvalidTween(t);
                return;
            }
            while (t.active && t.CompletedLoops() < elapsedLoops) await System.Threading.Tasks.Task.Yield();
        }

        /// <summary>
        /// Returns an async <see cref="System.Threading.Tasks.Task"/> that waits until the tween is killed or started
        /// (meaning when the tween is set in a playing state the first time, after any eventual delay).
        /// It can be used inside an async operation.
        /// <para>Example usage:</para><code>await myTween.AsyncWaitForPosition();</code>
        /// </summary>
        /// <param name="position">Position (loops included, delays excluded) to wait for</param>
        public static async System.Threading.Tasks.Task AsyncWaitForPosition(this Tween t, float position)
        {
            if (!t.active) {
                if (Debugger.logPriority > 0) Debugger.LogInvalidTween(t);
                return;
            }
            while (t.active && t.position * (t.CompletedLoops() + 1) < position) await System.Threading.Tasks.Task.Yield();
        }

        /// <summary>
        /// Returns an async <see cref="System.Threading.Tasks.Task"/> that waits until the tween is killed.
        /// It can be used inside an async operation.
        /// <para>Example usage:</para><code>await myTween.AsyncWaitForKill();</code>
        /// </summary>
        public static async System.Threading.Tasks.Task AsyncWaitForStart(this Tween t)
        {
            if (!t.active) {
                if (Debugger.logPriority > 0) Debugger.LogInvalidTween(t);
                return;
            }
            while (t.active && !t.playedOnce) await System.Threading.Tasks.Task.Yield();
        }

        #endregion
#endif

        #endregion

        #endregion
#endif
    }

    // █████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
    // ███ CLASSES █████████████████████████████████████████████████████████████████████████████████████████████████████████
    // █████████████████████████████████████████████████████████████████████████████████████████████████████████████████████

    public static class DOTweenCYInstruction
    {
        public class WaitForCompletion : CustomYieldInstruction
        {
            public override bool keepWaiting { get {
                return t.active && !t.IsComplete();
            }}
            readonly Tween t;
            public WaitForCompletion(Tween tween)
            {
                t = tween;
            }
        }

        public class WaitForRewind : CustomYieldInstruction
        {
            public override bool keepWaiting { get {
                return t.active && (!t.playedOnce || t.position * (t.CompletedLoops() + 1) > 0);
            }}
            readonly Tween t;
            public WaitForRewind(Tween tween)
            {
                t = tween;
            }
        }

        public class WaitForKill : CustomYieldInstruction
        {
            public override bool keepWaiting { get {
                return t.active;
            }}
            readonly Tween t;
            public WaitForKill(Tween tween)
            {
                t = tween;
            }
        }

        public class WaitForElapsedLoops : CustomYieldInstruction
        {
            public override bool keepWaiting { get {
                return t.active && t.CompletedLoops() < elapsedLoops;
            }}
            readonly Tween t;
            readonly int elapsedLoops;
            public WaitForElapsedLoops(Tween tween, int elapsedLoops)
            {
                t = tween;
                this.elapsedLoops = elapsedLoops;
            }
        }

        public class WaitForPosition : CustomYieldInstruction
        {
            public override bool keepWaiting { get {
                return t.active && t.position * (t.CompletedLoops() + 1) < position;
            }}
            readonly Tween t;
            readonly float position;
            public WaitForPosition(Tween tween, float position)
            {
                t = tween;
                this.position = position;
            }
        }

        public class WaitForStart : CustomYieldInstruction
        {
            public override bool keepWaiting { get {
                return t.active && !t.playedOnce;
            }}
            readonly Tween t;
            public WaitForStart(Tween tween)
            {
                t = tween;
            }
        }
    }
}

```

---

### `Plugins/Demigiant/DOTween/Modules/DOTweenModuleUtils.cs`

> Lines: 167 | Size: 6.7KB

```csharp
﻿// Author: Daniele Giardini - http://www.demigiant.com
// Created: 2018/07/13

using System;
using System.Reflection;
using UnityEngine;
using DG.Tweening.Core;
using DG.Tweening.Plugins.Core.PathCore;
using DG.Tweening.Plugins.Options;

#pragma warning disable 1591
namespace DG.Tweening
{
    /// <summary>
    /// Utility functions that deal with available Modules and rewrite them.
    /// Modules defines:
    /// - DOTAUDIO
    /// - DOTPHYSICS
    /// - DOTPHYSICS2D
    /// - DOTSPRITE
    /// - DOTUI
    /// Extra defines set and used for implementation of external assets:
    /// - DOTWEEN_TMP ► TextMesh Pro
    /// - DOTWEEN_TK2D ► 2D Toolkit
    /// </summary>
	public static class DOTweenModuleUtils
    {
        static bool _initialized;

        #region Reflection

        /// <summary>
        /// Called via Reflection by DOTweenComponent on Awake
        /// </summary>
#if UNITY_2018_1_OR_NEWER
        [UnityEngine.Scripting.Preserve]
#endif
        public static void Init()
        {
            if (_initialized) return;

            _initialized = true;
            DOTweenExternalCommand.SetOrientationOnPath += Physics.SetOrientationOnPath;

#if UNITY_EDITOR
#if UNITY_4_3 || UNITY_4_4 || UNITY_4_5 || UNITY_4_6 || UNITY_5 || UNITY_2017_1
            UnityEditor.EditorApplication.playmodeStateChanged += PlaymodeStateChanged;
#else
            UnityEditor.EditorApplication.playModeStateChanged += PlaymodeStateChanged;
#endif
#endif
        }

#if UNITY_2018_1_OR_NEWER
#pragma warning disable
        [UnityEngine.Scripting.Preserve]
        // Just used to preserve methods when building, never called
        static void Preserver()
        {
            Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
            MethodInfo mi = typeof(MonoBehaviour).GetMethod("Stub");
        }
#pragma warning restore
#endif

        #endregion

#if UNITY_EDITOR
        // Fires OnApplicationPause in DOTweenComponent even when Editor is paused (otherwise it's only fired at runtime)
#if UNITY_4_3 || UNITY_4_4 || UNITY_4_5 || UNITY_4_6 || UNITY_5 || UNITY_2017_1
        static void PlaymodeStateChanged()
        #else
        static void PlaymodeStateChanged(UnityEditor.PlayModeStateChange state)
#endif
        {
            if (DOTween.instance == null) return;
            DOTween.instance.OnApplicationPause(UnityEditor.EditorApplication.isPaused);
        }
#endif

        // █████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
        // ███ INTERNAL CLASSES ████████████████████████████████████████████████████████████████████████████████████████████████
        // █████████████████████████████████████████████████████████████████████████████████████████████████████████████████████

        public static class Physics
        {
            // Called via DOTweenExternalCommand callback
            public static void SetOrientationOnPath(PathOptions options, Tween t, Quaternion newRot, Transform trans)
            {
#if !DOTWEEN_NOPHYSICS // PHYSICS_MARKER
                if (options.isRigidbody) ((Rigidbody)t.target).rotation = newRot;
                else trans.rotation = newRot;
#else
                trans.rotation = newRot;
#endif
            }

            // Returns FALSE if the DOTween's Physics2D Module is disabled, or if there's no Rigidbody2D attached
            public static bool HasRigidbody2D(Component target)
            {
#if !DOTWEEN_NOPHYSICS2D // PHYSICS2D_MARKER
                return target.GetComponent<Rigidbody2D>() != null;
#else
                return false;
#endif
            }

            #region Called via Reflection


            // Called via Reflection by DOTweenPathInspector
            // Returns FALSE if the DOTween's Physics Module is disabled, or if there's no rigidbody attached
#if UNITY_2018_1_OR_NEWER
            [UnityEngine.Scripting.Preserve]
#endif
            public static bool HasRigidbody(Component target)
            {
#if !DOTWEEN_NOPHYSICS // PHYSICS_MARKER
                return target.GetComponent<Rigidbody>() != null;
#else
                return false;
#endif
            }

            // Called via Reflection by DOTweenPath
#if UNITY_2018_1_OR_NEWER
            [UnityEngine.Scripting.Preserve]
#endif
            public static TweenerCore<Vector3, Path, PathOptions> CreateDOTweenPathTween(
                MonoBehaviour target, bool tweenRigidbody, bool isLocal, Path path, float duration, PathMode pathMode
            ){
                TweenerCore<Vector3, Path, PathOptions> t = null;
                bool rBodyFoundAndTweened = false;
#if !DOTWEEN_NOPHYSICS // PHYSICS_MARKER
                if (tweenRigidbody) {
                    Rigidbody rBody = target.GetComponent<Rigidbody>();
                    if (rBody != null) {
                        rBodyFoundAndTweened = true;
                        t = isLocal
                            ? rBody.DOLocalPath(path, duration, pathMode)
                            : rBody.DOPath(path, duration, pathMode);
                    }
                }
#endif
#if !DOTWEEN_NOPHYSICS2D // PHYSICS2D_MARKER
                if (!rBodyFoundAndTweened && tweenRigidbody) {
                    Rigidbody2D rBody2D = target.GetComponent<Rigidbody2D>();
                    if (rBody2D != null) {
                        rBodyFoundAndTweened = true;
                        t = isLocal
                            ? rBody2D.DOLocalPath(path, duration, pathMode)
                            : rBody2D.DOPath(path, duration, pathMode);
                    }
                }
#endif
                if (!rBodyFoundAndTweened) {
                    t = isLocal
                        ? target.transform.DOLocalPath(path, duration, pathMode)
                        : target.transform.DOPath(path, duration, pathMode);
                }
                return t;
            }

            #endregion
        }
    }
}

```

---

### `Plugins/Demigiant/DOTween/readme.txt`

> Lines: 28 | Size: 2.1KB

```text
DOTween and DOTween Pro are copyright (c) 2014-2018 Daniele Giardini - Demigiant

// IMPORTANT!!! /////////////////////////////////////////////
// Upgrading DOTween from versions older than 1.2.000 ///////
// (or DOTween Pro older than 1.0.000) //////////////////////
-------------------------------------------------------------
If you're upgrading your project from a version of DOTween older than 1.2.000 (or DOTween Pro older than 1.0.000) please follow these instructions carefully.
1) Import the new version in the same folder as the previous one, overwriting old files. A lot of errors will appear but don't worry
2) Close and reopen Unity (and your project). This is fundamental: skipping this step will cause a bloodbath
3) Open DOTween's Utility Panel (Tools > Demigiant > DOTween Utility Panel) if it doesn't open automatically, then press "Setup DOTween...": this will run the upgrade setup
4) From the Add/Remove Modules panel that opens, activate/deactivate Modules for Unity systems and for external assets (Pro version only)

// GET STARTED //////////////////////////////////////////////

- After importing a new DOTween update, select DOTween's Utility Panel from the "Tools/Demigiant" menu (if it doesn't open automatically) and press the "Setup DOTween..." button to activate/deactivate Modules. You can also access a Preferences Tab from there to choose default settings for DOTween.
- In your code, add "using DG.Tweening" to each class where you want to use DOTween.
- You're ready to tween. Check out the links below for full documentation and license info.


// LINKS ///////////////////////////////////////////////////////

DOTween website (documentation, examples, etc): http://dotween.demigiant.com
DOTween license: http://dotween.demigiant.com/license.php
DOTween repository (Google Code): https://code.google.com/p/dotween/
Demigiant website (documentation, examples, etc): http://www.demigiant.com

// NOTES //////////////////////////////////////////////////////

- DOTween's Utility Panel can be found under "Tools > Demigiant > DOTween Utility Panel" and also contains other useful options, plus a tab to set DOTween's preferences
```

---

### `ScienceStreet/CS/Scripts/CsBotController.cs`

> Lines: 250 | Size: 11.3KB

```csharp
using UnityEngine;
using DG.Tweening;

namespace com.al_arcade.cs
{
    using shared;

    public class CsBotController : MonoBehaviour
    {
        private GameObject _body, _head, _leftEye, _rightEye, _mouth;
        private Material _bodyMat, _headMat, _mouthMat;
        private Sequence _idleAnim;
        private Sequence _headTiltAnim;
        private Coroutine _blinkCoroutine;
        private float _baseY;

        public void Build()
        {
            _baseY = transform.position.y;
            var shader = Shader.Find("Universal Render Pipeline/Lit")
                ?? Shader.Find("Standard") ?? Shader.Find("Unlit/Color");

            // ── Body ─────────────────────────────────────────────────
            _body = GameObject.CreatePrimitive(PrimitiveType.Capsule);
            _body.name = "Body";
            _body.transform.SetParent(transform);
            _body.transform.localPosition = new Vector3(0, 1f, 0);
            _body.transform.localScale = new Vector3(1.2f, 1.2f, 1.2f);
            Destroy(_body.GetComponent<Collider>());
            _bodyMat = new Material(shader) { color = SSColorPalette.Primary };
            _body.GetComponent<Renderer>().material = _bodyMat;

            // ── Head ─────────────────────────────────────────────────
            _head = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            _head.name = "Head";
            _head.transform.SetParent(transform);
            _head.transform.localPosition = new Vector3(0, 2.8f, 0);
            _head.transform.localScale = Vector3.one * 1.1f;
            Destroy(_head.GetComponent<Collider>());
            _headMat = new Material(shader) { color = SSColorPalette.Accent };
            _head.GetComponent<Renderer>().material = _headMat;

            // ── Eyes ─────────────────────────────────────────────────
            var eyeMat = new Material(shader) { color = Color.white };
            var pupilMat = new Material(shader) { color = SSColorPalette.TextDark };

            for (int side = -1; side <= 1; side += 2)
            {
                var eye = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                eye.name = side < 0 ? "LeftEye" : "RightEye";
                eye.transform.SetParent(_head.transform);
                eye.transform.localPosition = new Vector3(side * 0.22f, 0.1f, -0.42f);
                eye.transform.localScale = Vector3.one * 0.22f;
                Destroy(eye.GetComponent<Collider>());
                eye.GetComponent<Renderer>().material = new Material(eyeMat);

                if (side < 0) _leftEye = eye; else _rightEye = eye;

                var pupil = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                pupil.name = "Pupil";
                pupil.transform.SetParent(eye.transform);
                pupil.transform.localPosition = new Vector3(0, 0, -0.35f);
                pupil.transform.localScale = Vector3.one * 0.55f;
                Destroy(pupil.GetComponent<Collider>());
                pupil.GetComponent<Renderer>().material = new Material(pupilMat);
            }

            // ── Mouth ────────────────────────────────────────────────
            _mouth = GameObject.CreatePrimitive(PrimitiveType.Cube);
            _mouth.name = "Mouth";
            _mouth.transform.SetParent(_head.transform);
            _mouth.transform.localPosition = new Vector3(0, -0.2f, -0.45f);
            _mouth.transform.localScale = new Vector3(0.25f, 0.06f, 0.05f);
            Destroy(_mouth.GetComponent<Collider>());
            _mouthMat = new Material(shader) { color = SSColorPalette.Danger };
            _mouth.GetComponent<Renderer>().material = _mouthMat;

            // ── Arms ─────────────────────────────────────────────────
            for (int side = -1; side <= 1; side += 2)
            {
                var arm = GameObject.CreatePrimitive(PrimitiveType.Capsule);
                arm.name = side < 0 ? "LeftArm" : "RightArm";
                arm.transform.SetParent(_body.transform);
                arm.transform.localPosition = new Vector3(side * 0.55f, 0.1f, 0);
                arm.transform.localScale = new Vector3(0.2f, 0.45f, 0.2f);
                arm.transform.localRotation = Quaternion.Euler(0, 0, side * -15f);
                Destroy(arm.GetComponent<Collider>());
                arm.GetComponent<Renderer>().material =
                    new Material(shader) { color = SSColorPalette.PrimaryDark };
            }

            // ── Feet ─────────────────────────────────────────────────
            for (int side = -1; side <= 1; side += 2)
            {
                var foot = GameObject.CreatePrimitive(PrimitiveType.Cube);
                foot.name = "Foot";
                foot.transform.SetParent(transform);
                foot.transform.localPosition = new Vector3(side * 0.3f, 0.1f, -0.1f);
                foot.transform.localScale = new Vector3(0.3f, 0.2f, 0.5f);
                Destroy(foot.GetComponent<Collider>());
                foot.GetComponent<Renderer>().material =
                    new Material(shader) { color = SSColorPalette.PrimaryDark };
            }

            StartIdleAnimation();
        }

        private void StartIdleAnimation()
        {
            KillAllTweens();
            float y = _baseY;

            _idleAnim = DOTween.Sequence();
            _idleAnim.Append(transform.DOMoveY(y + 0.1f, 1.5f).SetEase(Ease.InOutSine));
            _idleAnim.Append(transform.DOMoveY(y, 1.5f).SetEase(Ease.InOutSine));
            _idleAnim.SetLoops(-1, LoopType.Restart);

            if (_head != null)
            {
                _headTiltAnim = DOTween.Sequence();
                _headTiltAnim.Append(_head.transform.DOLocalRotate(new Vector3(0, 5, 3), 2f)
                    .SetEase(Ease.InOutSine));
                _headTiltAnim.Append(_head.transform.DOLocalRotate(new Vector3(0, -5, -3), 2f)
                    .SetEase(Ease.InOutSine));
                _headTiltAnim.SetLoops(-1, LoopType.Restart);
            }

            _blinkCoroutine = StartCoroutine(BlinkLoop());
        }

        private void KillAllTweens()
        {
            _idleAnim?.Kill(); _idleAnim = null;
            _headTiltAnim?.Kill(); _headTiltAnim = null;
            if (_blinkCoroutine != null) { StopCoroutine(_blinkCoroutine); _blinkCoroutine = null; }
            DOTween.Kill(transform);
            if (_head != null) DOTween.Kill(_head.transform);
            if (_mouth != null) DOTween.Kill(_mouth.transform);
            if (_headMat != null) DOTween.Kill(_headMat);
            if (_mouthMat != null) DOTween.Kill(_mouthMat);
        }

        private System.Collections.IEnumerator BlinkLoop()
        {
            while (true)
            {
                yield return new WaitForSeconds(Random.Range(2f, 5f));
                if (_leftEye != null)
                {
                    DOTween.Kill(_leftEye.transform, "blink");
                    _leftEye.transform.DOScaleY(0.01f, 0.08f).SetId("blink")
                        .OnComplete(() => _leftEye.transform.DOScaleY(0.22f, 0.08f));
                }
                if (_rightEye != null)
                {
                    DOTween.Kill(_rightEye.transform, "blink");
                    _rightEye.transform.DOScaleY(0.01f, 0.08f).SetId("blink")
                        .OnComplete(() => _rightEye.transform.DOScaleY(0.22f, 0.08f));
                }
            }
        }

        // ── Reactions ────────────────────────────────────────────────

        public void PlayHappy()
        {
            if (_mouth != null)
            {
                DOTween.Kill(_mouth.transform, "mouthAnim");
                _mouth.transform.DOScaleX(0.4f, 0.2f).SetEase(Ease.OutBack).SetId("mouthAnim");
                _mouth.transform.DOScaleY(0.12f, 0.2f);
                DOTween.Kill(_mouthMat);
                _mouthMat.DOColor(SSColorPalette.Success, 0.2f);
            }

            DOTween.Kill(transform, "botPunch");
            transform.DOPunchScale(Vector3.one * 0.15f, 0.5f, 6, 0.3f).SetId("botPunch");

            if (_headMat != null)
            {
                DOTween.Kill(_headMat, "headFlash");
                _headMat.DOColor(SSColorPalette.Success, 0.2f).SetId("headFlash")
                    .OnComplete(() => _headMat.DOColor(SSColorPalette.Accent, 0.4f));
            }

            // Reset mouth after delay
            DOTween.Sequence().SetDelay(1f).OnComplete(() =>
            {
                if (_mouth == null) return;
                DOTween.Kill(_mouth.transform, "mouthAnim");
                _mouth.transform.DOScaleX(0.25f, 0.2f).SetId("mouthAnim");
                _mouth.transform.DOScaleY(0.06f, 0.2f);
                if (_mouthMat != null) { DOTween.Kill(_mouthMat); _mouthMat.DOColor(SSColorPalette.Danger, 0.3f); }
            });
        }

        public void PlaySad()
        {
            if (_mouth != null)
            {
                DOTween.Kill(_mouth.transform, "mouthAnim");
                _mouth.transform.DOScaleX(0.15f, 0.15f).SetId("mouthAnim");
            }
            if (_head != null)
            {
                DOTween.Kill(_head.transform, "headShake");
                _head.transform.DOShakeRotation(0.4f, 10f, 10).SetId("headShake");
            }
            if (_headMat != null)
            {
                DOTween.Kill(_headMat, "headFlash");
                _headMat.DOColor(SSColorPalette.Danger, 0.15f).SetId("headFlash")
                    .OnComplete(() => _headMat.DOColor(SSColorPalette.Accent, 0.4f));
            }

            DOTween.Sequence().SetDelay(1f).OnComplete(() =>
            {
                if (_mouth == null) return;
                DOTween.Kill(_mouth.transform, "mouthAnim");
                _mouth.transform.DOScaleX(0.25f, 0.2f).SetId("mouthAnim");
            });
        }

        public void PlayVictoryDance()
        {
            KillAllTweens();
            var seq = DOTween.Sequence();
            seq.Append(transform.DOJump(transform.position + Vector3.up * 0.3f, 0.5f, 3, 1.5f));
            seq.Join(transform.DORotate(new Vector3(0, 360, 0), 1.5f, RotateMode.FastBeyond360));
            seq.OnComplete(() => StartIdleAnimation());
        }

        public void ShowSpeechBubble()
        {
            DOTween.Kill(transform, "botPunch");
            transform.DOPunchScale(Vector3.one * 0.1f, 0.3f, 4, 0.2f).SetId("botPunch");
        }

        public void ResetBot()
        {
            KillAllTweens();
            transform.localScale = Vector3.one;
            transform.localRotation = Quaternion.identity;
            transform.position = new Vector3(transform.position.x, _baseY, transform.position.z);
            StartIdleAnimation();
        }

        private void OnDestroy() => KillAllTweens();
    }
}

```

---

### `ScienceStreet/CS/Scripts/CsDemoBuilder.cs`

> Lines: 420 | Size: 20.6KB

```csharp
using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Events;
using DG.Tweening;
using TMPro;
using ALArcade.ArabicTMP;

namespace com.al_arcade.cs
{
    using shared;

    [AddComponentMenu("Science Street/CS Demo Builder")]
    public class CsDemoBuilder : MonoBehaviour
    {
        [Header("Build Configuration")]
        [SerializeField] private string buildType = "scistreet";
        [SerializeField] private int gradeId = 0;
        [SerializeField] private int questionCount = 8;
        [SerializeField] private string classCode = "";

        [Header("Arabic Font")]
        [Tooltip("Drag your Arabic TMP_FontAsset here")]
        [SerializeField] private TMP_FontAsset arabicFont;

        [Header("Debug")]
        [SerializeField] private bool useOfflineTestData = false;

        private CsGameManager _gm;
        private CsBotController _bot;
        private CsUIManager _uiManager;

        private void Start()
        {
            DOTween.Init();
            if (arabicFont != null) SSFontManager.Font = arabicFont;
            StartCoroutine(BuildEverything());
        }

        private IEnumerator BuildEverything()
        {
            Debug.Log("═══════════════════════════════════════════");
            Debug.Log("  Science Street — CS Demo Builder");
            Debug.Log("  Correct-the-Sentence Game");
            Debug.Log("═══════════════════════════════════════════");

            // ── EventSystem ──────────────────────────────────────────
            if (FindObjectOfType<EventSystem>() == null)
            {
                var es = new GameObject("EventSystem");
                es.AddComponent<EventSystem>();
                es.AddComponent<StandaloneInputModule>();
            }

            // ── Managers ─────────────────────────────────────────────
            SSApiManager.EnsureInstance();
            SSAudioManager.EnsureInstance();
            SSParticleManager.EnsureInstance();

            var session = SSGameSession.EnsureInstance();
            session.buildType = buildType;
            session.gradeId = gradeId;
            session.questionCount = questionCount;
            session.classCode = classCode;
            yield return null;

            // ── Scene ────────────────────────────────────────────────
            BuildScene();
            yield return null;

            // ── Camera ───────────────────────────────────────────────
            SetupCamera();
            yield return null;

            // ── Bot ──────────────────────────────────────────────────
            var botObj = new GameObject("Bot");
            botObj.transform.position = new Vector3(0, 0, 0);
            _bot = botObj.AddComponent<CsBotController>();
            _bot.Build();
            yield return null;

            // ── Word Container ───────────────────────────────────────
            var wordContainer = new GameObject("WordContainer");
            wordContainer.transform.position = Vector3.zero;
            yield return null;

            // ── UI ───────────────────────────────────────────────────
            var uiObj = new GameObject("CsUI");
            _uiManager = uiObj.AddComponent<CsUIManager>();
            _uiManager.BuildUI();

            // ★ FIX: Initialize the UnityEvent if null
            if (_uiManager.onRestartClicked == null)
                _uiManager.onRestartClicked = new UnityEvent();

            yield return null;

            // ── Game Manager ─────────────────────────────────────────
            var gmObj = new GameObject("CsGameManager");
            _gm = gmObj.AddComponent<CsGameManager>();
            _gm.bot = _bot;
            _gm.uiManager = _uiManager;
            _gm.wordContainer = wordContainer.transform;

            // ★ FIX: Safe listener via named method
            _uiManager.onRestartClicked.AddListener(OnRestartClicked);
            yield return null;

            // ── Lighting ─────────────────────────────────────────────
            SetupLighting();
            yield return null;

            Debug.Log("═══════════════════════════════════════════");
            Debug.Log("  CS Game built! Starting...");
            Debug.Log("  Click words to find the wrong one,");
            Debug.Log("  then drag the correct replacement.");
            Debug.Log("═══════════════════════════════════════════");

            yield return new WaitForSeconds(0.5f);

            // ── Start ────────────────────────────────────────────────
            if (useOfflineTestData)
                _gm.StartWithQuestions(GetTestQuestions());
            else
                _gm.StartGame();
        }

        // ════════════════════════════════════════════════════════════
        // RESTART HANDLER
        // ════════════════════════════════════════════════════════════

        private void OnRestartClicked()
        {
            if (_gm != null) _gm.ResetGame();
            if (_gm != null) _gm.StartGame();
        }

        // ════════════════════════════════════════════════════════════
        // SCENE
        // ════════════════════════════════════════════════════════════

        private void BuildScene()
        {
            Camera.main.clearFlags = CameraClearFlags.SolidColor;
            Camera.main.backgroundColor = new Color32(30, 30, 60, 255);

            var shader = GetShader();

            // Ground
            var ground = GameObject.CreatePrimitive(PrimitiveType.Cube);
            ground.name = "Ground";
            ground.transform.position = new Vector3(0, -0.05f, 0);
            ground.transform.localScale = new Vector3(20, 0.1f, 15);
            ground.GetComponent<Renderer>().material =
                new Material(shader) { color = new Color32(40, 40, 65, 255) };

            // Circular platform
            var platform = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
            platform.name = "Platform";
            platform.transform.position = new Vector3(0, 0.05f, 0);
            platform.transform.localScale = new Vector3(3f, 0.1f, 3f);
            Destroy(platform.GetComponent<Collider>());
            platform.GetComponent<Renderer>().material =
                new Material(shader) { color = SSColorPalette.WithAlpha(SSColorPalette.Primary, 0.6f) };

            // Outer ring with pulse
            var ring = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
            ring.name = "Ring";
            ring.transform.position = new Vector3(0, 0.04f, 0);
            ring.transform.localScale = new Vector3(3.5f, 0.08f, 3.5f);
            Destroy(ring.GetComponent<Collider>());
            var ringMat = new Material(shader)
                { color = SSColorPalette.WithAlpha(SSColorPalette.Accent, 0.3f) };
            ring.GetComponent<Renderer>().material = ringMat;
            ringMat.DOFade(0.1f, 1.5f).SetEase(Ease.InOutSine)
                .SetLoops(-1, LoopType.Yoyo);

            // Background decorations
            for (int i = 0; i < 20; i++)
            {
                var deco = GameObject.CreatePrimitive(
                    Random.value > 0.5f ? PrimitiveType.Cube : PrimitiveType.Sphere);
                deco.name = $"BgDeco_{i}";
                float x = Random.Range(-9f, 9f);
                float y = Random.Range(1f, 7f);
                float z = Random.Range(3f, 10f);
                deco.transform.position = new Vector3(x, y, z);
                float scale = Random.Range(0.2f, 0.8f);
                deco.transform.localScale = Vector3.one * scale;
                deco.transform.rotation = Random.rotation;
                Destroy(deco.GetComponent<Collider>());

                Color[] colors = {
                    SSColorPalette.Primary, SSColorPalette.Accent,
                    SSColorPalette.Success, SSColorPalette.Info
                };
                var dMat = new Material(shader)
                {
                    color = SSColorPalette.WithAlpha(
                        colors[Random.Range(0, colors.Length)],
                        Random.Range(0.05f, 0.2f))
                };
                deco.GetComponent<Renderer>().material = dMat;

                float spd = Random.Range(1.5f, 3f);
                float amt = Random.Range(0.3f, 0.8f);
                deco.transform.DOMoveY(y + amt, spd)
                    .SetEase(Ease.InOutSine).SetLoops(-1, LoopType.Yoyo);
                deco.transform.DORotate(
                    new Vector3(Random.Range(-30, 30), Random.Range(-30, 30),
                        Random.Range(-30, 30)),
                    Random.Range(5f, 10f), RotateMode.FastBeyond360)
                    .SetLoops(-1, LoopType.Incremental).SetEase(Ease.Linear);
            }

            // Grid lines on ground
            for (float x = -9; x <= 9; x += 3)
            {
                var line = GameObject.CreatePrimitive(PrimitiveType.Cube);
                line.name = "GridX";
                line.transform.position = new Vector3(x, 0.01f, 3);
                line.transform.localScale = new Vector3(0.02f, 0.01f, 15);
                Destroy(line.GetComponent<Collider>());
                line.GetComponent<Renderer>().material = new Material(shader)
                    { color = SSColorPalette.WithAlpha(SSColorPalette.Primary, 0.08f) };
            }
            for (float z = -2; z <= 12; z += 3)
            {
                var line = GameObject.CreatePrimitive(PrimitiveType.Cube);
                line.name = "GridZ";
                line.transform.position = new Vector3(0, 0.01f, z);
                line.transform.localScale = new Vector3(20, 0.01f, 0.02f);
                Destroy(line.GetComponent<Collider>());
                line.GetComponent<Renderer>().material = new Material(shader)
                    { color = SSColorPalette.WithAlpha(SSColorPalette.Primary, 0.08f) };
            }

            // Floor label
            var floorText = new GameObject("FloorLabel");
            floorText.transform.position = new Vector3(0, 0.02f, -3);
            floorText.transform.rotation = Quaternion.Euler(90, 0, 0);
            var ft = floorText.AddComponent<ArabicTextMeshPro>();
            ft.arabicText = "SCIENCE STREET";
            ft.fontSize = 6;
            ft.alignment = TMPro.TextAlignmentOptions.Center;
            ft.color = SSColorPalette.WithAlpha(SSColorPalette.Accent, 0.08f);
            ft.fontStyle = TMPro.FontStyles.Bold;
            ft.rectTransform.sizeDelta = new Vector2(15, 2);
            SSFontManager.Apply(ft);
        }

        private void SetupCamera()
        {
            var cam = Camera.main;
            if (cam == null) return;
            cam.transform.position = new Vector3(0, 4f, -6f);
            cam.transform.rotation = Quaternion.Euler(25, 0, 0);
            cam.fieldOfView = 60;
            cam.nearClipPlane = 0.1f;
        }

        private void SetupLighting()
        {
            var existing = FindObjectOfType<Light>();
            Light dirLight;
            if (existing != null)
                dirLight = existing;
            else
            {
                var lo = new GameObject("DirLight");
                dirLight = lo.AddComponent<Light>();
            }
            dirLight.type = LightType.Directional;
            dirLight.transform.rotation = Quaternion.Euler(50, -30, 0);
            dirLight.color = new Color32(180, 190, 220, 255);
            dirLight.intensity = 0.9f;
            dirLight.shadows = LightShadows.Soft;

            // Accent light on bot
            var accent = new GameObject("AccentLight");
            var aLight = accent.AddComponent<Light>();
            aLight.type = LightType.Point;
            aLight.transform.position = new Vector3(0, 5, -2);
            aLight.range = 10;
            aLight.intensity = 0.8f;
            aLight.color = SSColorPalette.Accent;

            // Blue back light
            var back = new GameObject("BackLight");
            var bLight = back.AddComponent<Light>();
            bLight.type = LightType.Point;
            bLight.transform.position = new Vector3(0, 3, 5);
            bLight.range = 12;
            bLight.intensity = 0.4f;
            bLight.color = SSColorPalette.Primary;

            RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Flat;
            RenderSettings.ambientLight = new Color32(40, 42, 60, 255);
        }

        // ════════════════════════════════════════════════════════════
        // TEST DATA
        // ════════════════════════════════════════════════════════════

        private CsQuestion[] GetTestQuestions()
        {
            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 },
                    }
                },
            };
        }

        private Shader GetShader()
        {
            return Shader.Find("Universal Render Pipeline/Lit")
                ?? Shader.Find("Standard")
                ?? Shader.Find("Unlit/Color");
        }
    }
}
```

---

### `ScienceStreet/CS/Scripts/CsGameManager.cs`

> Lines: 405 | Size: 14.0KB

```csharp
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using DG.Tweening;

namespace com.al_arcade.cs
{
    using shared;

    public enum CsGameState
    {
        Idle, Loading, ShowingSentence, WaitingForWordClick,
        WaitingForDrop, Feedback, Complete
    }

    public class CsGameManager : MonoBehaviour
    {
        public static CsGameManager Instance { get; private set; }

        [Header("Settings")]
        [SerializeField] private float sentenceShowDelay = 0.5f;
        [SerializeField] private float feedbackDuration = 1.5f;
        [SerializeField] private float wordClickCooldown = 0.3f;

        [Header("References")]
        public CsBotController bot;
        public CsUIManager uiManager;
        public Transform wordContainer;

        // State
        private CsGameState _state = CsGameState.Idle;
        private CsQuestion[] _questions;
        private int _currentIndex;
        private int _score, _streak, _correctCount, _wrongCount, _wrongClicks;

        private List<CsWordButton> _wordButtons = new();
        private CsWordButton _wrongWordButton;
        private bool _wordClickLocked;

        [Header("Events")]
        public UnityEvent onGameStart;
        public UnityEvent<bool> onAnswerGiven;
        public UnityEvent<int> onGameComplete;

        private Action<bool> _onOptionDropped;

        private void Awake()
        {
            if (Instance != null && Instance != this) { Destroy(gameObject); return; }
            Instance = this;
        }

        // ═══════════════════════════════════════════════════════════
        // PUBLIC API
        // ═══════════════════════════════════════════════════════════

        public void StartGame() => StartCoroutine(StartGameRoutine());

        public void StartWithQuestions(CsQuestion[] questions)
        {
            _questions = questions;
            BeginGameplay();
        }

        public void ResetGame()
        {
            StopAllCoroutines();
            _state = CsGameState.Idle;
            _currentIndex = _score = _streak = _correctCount = _wrongCount = 0;
            ClearWordButtons();
            if (uiManager != null) uiManager.ResetUI();
            if (bot != null) bot.ResetBot();
        }

        public void OnWordClicked(CsWordButton wordButton)
        {
            if (_state != CsGameState.WaitingForWordClick) return;
            if (_wordClickLocked) return;

            if (wordButton.IsWrong)
            {
                _wrongWordButton = wordButton;
                wordButton.Highlight(true);

                var audio = SSAudioManager.Instance;
                if (audio != null) audio.PlayClickBeep();

                var particles = SSParticleManager.Instance;
                if (particles != null)
                    particles.PlaySparks(wordButton.transform.position, SSColorPalette.Accent);

                _state = CsGameState.WaitingForDrop;
                ShowOptions();
            }
            else
            {
                _wrongClicks++;
                _wordClickLocked = true;
                wordButton.ShakeWrong();

                var audio = SSAudioManager.Instance;
                if (audio != null) audio.PlayWrongBeep();

                StartCoroutine(UnlockAfterCooldown());
            }
        }

        public void OnOptionDropped(bool isCorrectOption)
        {
            _onOptionDropped?.Invoke(isCorrectOption);
        }

        // ═══════════════════════════════════════════════════════════
        // GAME FLOW
        // ═══════════════════════════════════════════════════════════

        private IEnumerator StartGameRoutine()
        {
            _state = CsGameState.Loading;
            if (uiManager != null) uiManager.ShowLoading("جاري تحميل الأسئلة...");

            var session = SSGameSession.EnsureInstance();
            var api = SSApiManager.EnsureInstance();
            string error = null;

            yield return api.FetchCs(
                session.buildType, session.classCode,
                session.questionCount, session.gradeId,
                qs => _questions = qs,
                err => error = err);

            if (error != null || _questions == null || _questions.Length == 0)
            {
                if (uiManager != null) uiManager.ShowError(error ?? "لا توجد أسئلة");
                _state = CsGameState.Idle;
                yield break;
            }

            if (uiManager != null) uiManager.HideLoading();
            BeginGameplay();
        }

        private void BeginGameplay()
        {
            _currentIndex = _score = _streak = _correctCount = _wrongCount = 0;

            if (uiManager != null)
            {
                uiManager.ShowGameUI();
                uiManager.SetScore(0);
                uiManager.SetProgress(0, _questions.Length);
            }

            onGameStart?.Invoke();
            StartCoroutine(QuestionLoop());
        }

        private IEnumerator QuestionLoop()
        {
            while (_currentIndex < _questions.Length)
            {
                yield return PresentQuestion(_questions[_currentIndex]);
                _currentIndex++;
                if (uiManager != null)
                    uiManager.SetProgress(_currentIndex, _questions.Length);
            }

            _state = CsGameState.Complete;
            yield return VictorySequence();
        }

        private IEnumerator PresentQuestion(CsQuestion question)
        {
            _state = CsGameState.ShowingSentence;
            _wrongClicks = 0;
            _wrongWordButton = null;

            if (bot != null) bot.ShowSpeechBubble();

            ClearWordButtons();
            yield return new WaitForSeconds(sentenceShowDelay);

            SpawnWords(question);
            yield return new WaitForSeconds(0.3f);

            _state = CsGameState.WaitingForWordClick;
            if (uiManager != null)
                uiManager.ShowHint("انقر على الكلمة الخاطئة في الجملة");

            bool questionComplete = false;
            _onOptionDropped = null;

            while (!questionComplete)
            {
                if (_state == CsGameState.WaitingForDrop)
                {
                    bool? result = null;
                    _onOptionDropped = correct => result = correct;

                    while (result == null) yield return null;

                    if (result == true)
                    {
                        questionComplete = true;
                        yield return CorrectAnswerFeedback(question);
                    }
                    else
                    {
                        yield return WrongOptionFeedback();
                    }
                }
                else
                {
                    yield return null;
                }
            }

            if (uiManager != null) uiManager.HideOptions();
            yield return new WaitForSeconds(0.5f);
        }

        private IEnumerator CorrectAnswerFeedback(CsQuestion question)
        {
            _state = CsGameState.Feedback;
            _correctCount++;
            _streak++;

            int points = Mathf.Max(100 - _wrongClicks * 15, 25);
            if (_streak >= 3) points += (_streak - 2) * 25;
            _score += points;

            if (_wrongWordButton != null)
                _wrongWordButton.ReplaceText(question.correct_answer);

            if (uiManager != null)
            {
                uiManager.ShowFeedback($"ممتاز! +{points} ✓", true);
                uiManager.SetScore(_score);
                uiManager.SetStreak(_streak);
            }

            if (bot != null) bot.PlayHappy();

            var audio = SSAudioManager.Instance;
            if (audio != null)
            { if (audio.sfxCorrect != null) audio.PlayCorrect(); else audio.PlayCorrectBeep(); }

            var particles = SSParticleManager.Instance;
            if (particles != null && _wrongWordButton != null)
                particles.PlayCorrectBurst(_wrongWordButton.transform.position);

            onAnswerGiven?.Invoke(true);
            yield return new WaitForSeconds(feedbackDuration);
        }

        private IEnumerator WrongOptionFeedback()
        {
            _wrongClicks++;

            if (uiManager != null)
                uiManager.ShowFeedback("حاول مرة أخرى ✗", false);

            if (bot != null) bot.PlaySad();

            var audio = SSAudioManager.Instance;
            if (audio != null) audio.PlayWrongBeep();

            if (Camera.main != null)
            {
                DOTween.Kill(Camera.main.transform, "csShake");
                Camera.main.transform.DOShakePosition(0.3f, 0.15f, 10)
                    .SetId("csShake");
            }

            yield return new WaitForSeconds(0.8f);
        }

        // ═══════════════════════════════════════════════════════════
        // WORD SPAWNING — ★ Dynamic sizing, no overlaps
        // ═══════════════════════════════════════════════════════════

        private void SpawnWords(CsQuestion question)
        {
            if (question.words == null || question.words.Length == 0) return;
            ClearWordButtons();

            int wordCount = question.words.Length;

            // ★ Calculate total width needed based on actual text lengths
            float padding = 0.6f;       // extra padding per word card
            float gapBetween = 0.25f;   // gap between cards
            float charWidth = 0.32f;    // approx width per character

            float[] wordWidths = new float[wordCount];
            float totalWidth = 0;
            for (int i = 0; i < wordCount; i++)
            {
                float w = question.words[i].word_text.Length * charWidth + padding;
                w = Mathf.Max(w, 1.0f); // minimum width
                wordWidths[i] = w;
                totalWidth += w;
            }
            totalWidth += (wordCount - 1) * gapBetween;

            // ★ If total width exceeds max, scale everything down
            float maxAllowedWidth = 14f;
            float scaleFactor = 1f;
            if (totalWidth > maxAllowedWidth)
            {
                scaleFactor = maxAllowedWidth / totalWidth;
                totalWidth = maxAllowedWidth;
                for (int i = 0; i < wordCount; i++)
                    wordWidths[i] *= scaleFactor;
            }

            // Base position above bot
            Vector3 basePos = bot != null
                ? bot.transform.position + Vector3.up * 3.5f
                : Vector3.up * 4f;

            // ★ RTL layout: start from right edge
            float startX = totalWidth / 2f;
            float currentX = startX;

            for (int i = 0; i < wordCount; i++)
            {
                var word = question.words[i];
                float halfW = wordWidths[i] / 2f;
                float posX = currentX - halfW;

                var wordObj = new GameObject($"Word_{i}_{word.word_text}");
                if (wordContainer != null)
                    wordObj.transform.SetParent(wordContainer);
                wordObj.transform.position = basePos + Vector3.right * posX;

                var wb = wordObj.AddComponent<CsWordButton>();
                wb.Setup(word.word_text, word.is_wrong, i, wordWidths[i], scaleFactor);
                _wordButtons.Add(wb);

                // Entrance animation
                wordObj.transform.localScale = Vector3.zero;
                wordObj.transform.DOScale(Vector3.one, 0.4f)
                    .SetDelay(i * 0.08f)
                    .SetEase(Ease.OutBack);

                currentX -= wordWidths[i] + gapBetween;
            }
        }

        private void ShowOptions()
        {
            var question = _questions[_currentIndex];
            if (uiManager != null)
            {
                uiManager.ShowOptions(question.options, _wrongWordButton);
                uiManager.ShowHint("اسحب الإجابة الصحيحة إلى الكلمة الخاطئة");
            }
        }

        private void ClearWordButtons()
        {
            foreach (var wb in _wordButtons)
            {
                if (wb != null && wb.gameObject != null)
                {
                    DOTween.Kill(wb.transform);
                    Destroy(wb.gameObject);
                }
            }
            _wordButtons.Clear();
        }

        private IEnumerator UnlockAfterCooldown()
        {
            yield return new WaitForSeconds(wordClickCooldown);
            _wordClickLocked = false;
        }

        // ── Victory ──────────────────────────────────────────────────

        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();

            if (bot != null) bot.PlayVictoryDance();

            yield return new WaitForSeconds(1f);

            if (uiManager != null)
                uiManager.ShowResults(_score, _correctCount, _wrongCount,
                    _questions.Length, _streak);

            onGameComplete?.Invoke(_score);
        }

        public int Score => _score;
    }
}

```

---

### `ScienceStreet/CS/Scripts/CsOptionDraggable.cs`

> Lines: 218 | Size: 8.8KB

```csharp
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using DG.Tweening;
using ALArcade.ArabicTMP;

namespace com.al_arcade.cs
{
    using shared;

    public class CsOptionDraggable : MonoBehaviour,
        IBeginDragHandler, IDragHandler, IEndDragHandler,
        IPointerEnterHandler, IPointerExitHandler
    {
        public string OptionText { get; private set; }
        public bool IsCorrect { get; private set; }

        private RectTransform _rect;
        private Image _bg;
        private ArabicTextMeshProUGUI _text;
        private Vector2 _originalAnchoredPos;
        private Canvas _canvas;
        private CsWordButton _targetWord;
        private CanvasGroup _canvasGroup;

        // ★ Drag offset: distance from pointer to rect center at grab time
        private Vector2 _dragOffset;
        private bool _originalPosCaptured;
        private Camera _canvasCamera;

        public void Setup(string text, bool isCorrect, Canvas canvas, CsWordButton targetWord)
        {
            OptionText = text;
            IsCorrect = isCorrect;
            _canvas = canvas;
            _targetWord = targetWord;

            _rect = GetComponent<RectTransform>();
            if (_rect == null) _rect = gameObject.AddComponent<RectTransform>();

            // ★ CanvasGroup so we can make it not block raycasts while dragging
            _canvasGroup = gameObject.AddComponent<CanvasGroup>();

            // Determine canvas camera (null for ScreenSpaceOverlay)
            _canvasCamera = null;
            if (_canvas != null && _canvas.renderMode != RenderMode.ScreenSpaceOverlay)
                _canvasCamera = _canvas.worldCamera;

            // Background
            _bg = gameObject.AddComponent<Image>();
            _bg.color = SSColorPalette.Card;
            _bg.raycastTarget = true;

            // Text
            var textObj = new GameObject("Text");
            textObj.transform.SetParent(transform, false);
            var textRect = textObj.AddComponent<RectTransform>();
            textRect.anchorMin = Vector2.zero;
            textRect.anchorMax = Vector2.one;
            textRect.offsetMin = new Vector2(10, 5);
            textRect.offsetMax = new Vector2(-10, -5);

            _text = textObj.AddComponent<ArabicTextMeshProUGUI>();
            _text.arabicText = text;
            _text.fontSize = 22;
            _text.alignment = TMPro.TextAlignmentOptions.Center;
            _text.color = SSColorPalette.TextDark;
            _text.fontStyle = TMPro.FontStyles.Bold;
            _text.raycastTarget = false;
            _text.enableAutoSizing = true;
            _text.fontSizeMin = 14;
            _text.fontSizeMax = 22;
            SSFontManager.Apply(_text);

            // Entrance animation
            transform.localScale = Vector3.one * 0.5f;
            transform.DOScale(Vector3.one, 0.3f).SetEase(Ease.OutBack);

            _originalPosCaptured = false;
        }

        // ════════════════════════════════════════════════════════════
        // DRAG HANDLERS — ★ Cursor-accurate positioning
        // ════════════════════════════════════════════════════════════

        public void OnBeginDrag(PointerEventData eventData)
        {
            // ★ Capture the original anchored position for return-to-origin
            _originalAnchoredPos = _rect.anchoredPosition;
            _originalPosCaptured = true;

            // ★ Calculate offset: where the pointer is relative to the rect's center
            //   so the card doesn't jump — it stays exactly where you grabbed it
            Vector2 localPointerPos;
            RectTransformUtility.ScreenPointToLocalPointInRectangle(
                _rect.parent as RectTransform,   // parent rect
                eventData.position,               // screen pointer
                _canvasCamera,                    // camera (null for overlay)
                out localPointerPos);

            _dragOffset = _rect.anchoredPosition - localPointerPos;

            // Visual feedback
            _bg.color = SSColorPalette.WithAlpha(SSColorPalette.Primary, 0.3f);
            DOTween.Kill(transform, "optScale");
            transform.DOScale(Vector3.one * 1.08f, 0.12f).SetId("optScale");
            transform.SetAsLastSibling();

            // ★ Let raycasts pass through while dragging (optional, helps with drop detection)
            _canvasGroup.blocksRaycasts = false;

            var audio = SSAudioManager.Instance;
            if (audio != null) audio.PlayClickBeep();
        }

        public void OnDrag(PointerEventData eventData)
        {
            if (_rect == null || _canvas == null) return;

            // ★ Convert screen pointer to local position in parent
            Vector2 localPointerPos;
            RectTransformUtility.ScreenPointToLocalPointInRectangle(
                _rect.parent as RectTransform,
                eventData.position,
                _canvasCamera,
                out localPointerPos);

            // ★ Apply with the initial offset so the card stays under the finger
            _rect.anchoredPosition = localPointerPos + _dragOffset;
        }

        public void OnEndDrag(PointerEventData eventData)
        {
            // Re-enable raycasts
            _canvasGroup.blocksRaycasts = true;

            // ★ Check if dropped near the target word
            bool droppedOnTarget = false;
            if (_targetWord != null && Camera.main != null)
            {
                Vector3 worldPos = _targetWord.transform.position;
                Vector3 screenPos = Camera.main.WorldToScreenPoint(worldPos);
                float distance = Vector2.Distance(eventData.position, (Vector2)screenPos);

                if (distance < 120f)
                {
                    droppedOnTarget = true;

                    if (CsGameManager.Instance != null)
                        CsGameManager.Instance.OnOptionDropped(IsCorrect);

                    if (IsCorrect)
                    {
                        // Snap to target and disappear
                        DOTween.Kill(transform);
                        transform.DOScale(Vector3.zero, 0.3f)
                            .SetEase(Ease.InBack)
                            .OnComplete(() =>
                            {
                                if (gameObject != null) Destroy(gameObject);
                            });
                        return;
                    }
                    else
                    {
                        // Wrong — shake then return
                        DOTween.Kill(transform, "optShake");
                        transform.DOShakePosition(0.3f, 10f, 15).SetId("optShake");
                    }
                }
            }

            // ★ Return to original position (always, unless correct + destroyed)
            ReturnToOrigin();
        }

        private void ReturnToOrigin()
        {
            _bg.color = SSColorPalette.Card;
            DOTween.Kill(transform, "optScale");
            transform.DOScale(Vector3.one, 0.2f).SetId("optScale");

            if (_originalPosCaptured)
            {
                DOTween.Kill(_rect, "optReturn");
                _rect.DOAnchorPos(_originalAnchoredPos, 0.35f)
                    .SetEase(Ease.OutBack).SetId("optReturn");
            }
        }

        // ════════════════════════════════════════════════════════════
        // HOVER
        // ════════════════════════════════════════════════════════════

        public void OnPointerEnter(PointerEventData eventData)
        {
            DOTween.Kill(_bg, "optHover");
            _bg.DOColor(SSColorPalette.WithAlpha(SSColorPalette.Primary, 0.1f), 0.15f)
                .SetId("optHover");
        }

        public void OnPointerExit(PointerEventData eventData)
        {
            DOTween.Kill(_bg, "optHover");
            _bg.DOColor(SSColorPalette.Card, 0.15f).SetId("optHover");
        }

        // ════════════════════════════════════════════════════════════
        // CLEANUP
        // ════════════════════════════════════════════════════════════

        private void OnDestroy()
        {
            DOTween.Kill(transform);
            if (_rect != null) DOTween.Kill(_rect);
            if (_bg != null) DOTween.Kill(_bg);
        }
    }
}
```

---

### `ScienceStreet/CS/Scripts/CsUIManager.cs`

> Lines: 470 | Size: 22.0KB

```csharp
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using DG.Tweening;
using ALArcade.ArabicTMP;

namespace com.al_arcade.cs
{
    using shared;

    public class CsUIManager : MonoBehaviour
    {
        private Canvas _canvas;
        private CanvasGroup _gameUI, _loadingUI, _errorUI, _resultsUI;
        private CanvasGroup _optionsPanel, _feedbackGroup;

        private ArabicTextMeshProUGUI _scoreText, _streakText, _progressText, _hintText;
        private ArabicTextMeshProUGUI _feedbackText;
        private Image _feedbackBg, _progressFill;

        private ArabicTextMeshProUGUI _loadingText, _errorText;
        private ArabicTextMeshProUGUI _resultTitle, _resultScore, _resultStats;

        private Transform _optionsContainer;
        private List<CsOptionDraggable> _activeOptions = new();

        [Header("Events")]
        public UnityEvent onRestartClicked;

        // ════════════════════════════════════════════════════════════
        // BUILD
        // ════════════════════════════════════════════════════════════

        public void BuildUI()
        {
            var co = new GameObject("CsCanvas");
            co.transform.SetParent(transform);
            _canvas = co.AddComponent<Canvas>();
            _canvas.renderMode = RenderMode.ScreenSpaceOverlay;
            _canvas.sortingOrder = 100;

            var sc = co.AddComponent<CanvasScaler>();
            sc.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
            sc.referenceResolution = new Vector2(1920, 1080);
            sc.matchWidthOrHeight = 0.5f;
            co.AddComponent<GraphicRaycaster>();

            BuildGameHUD(co.transform);
            BuildOptionsPanel(co.transform);
            BuildFeedbackOverlay(co.transform);
            BuildLoadingPanel(co.transform);
            BuildErrorPanel(co.transform);
            BuildResultsPanel(co.transform);

            _gameUI.alpha = 0; _gameUI.gameObject.SetActive(false);
            _loadingUI.alpha = 0; _loadingUI.gameObject.SetActive(false);
            _errorUI.alpha = 0; _errorUI.gameObject.SetActive(false);
            _resultsUI.alpha = 0; _resultsUI.gameObject.SetActive(false);
            _optionsPanel.alpha = 0; _optionsPanel.gameObject.SetActive(false);
            _feedbackGroup.alpha = 0;
        }

        // ── Game HUD ─────────────────────────────────────────────────

        private void BuildGameHUD(Transform parent)
        {
            var go = MkFull(parent, "GameUI");
            _gameUI = go.AddComponent<CanvasGroup>();

            // Top bar
            var topBar = new GameObject("TopBar");
            topBar.transform.SetParent(go.transform, false);
            var tbr = topBar.AddComponent<RectTransform>();
            tbr.anchorMin = new Vector2(0, 1); tbr.anchorMax = new Vector2(1, 1);
            tbr.pivot = new Vector2(0.5f, 1);
            tbr.anchoredPosition = Vector2.zero; tbr.sizeDelta = new Vector2(0, 90);
            topBar.AddComponent<Image>().color =
                SSColorPalette.WithAlpha(SSColorPalette.PrimaryDark, 0.8f);

            _scoreText = MkTxt(topBar.transform, "Score", "0", 36,
                new Vector2(0, 1), new Vector2(40, -12), new Vector2(200, 50));
            _scoreText.color = SSColorPalette.Accent;
            _scoreText.fontStyle = TMPro.FontStyles.Bold;
            _scoreText.alignment = TMPro.TextAlignmentOptions.TopLeft;

            MkTxt(topBar.transform, "ScoreLbl", "النقاط", 14,
                new Vector2(0, 1), new Vector2(40, -55), new Vector2(100, 25))
                .color = SSColorPalette.WithAlpha(Color.white, 0.5f);

            _streakText = MkTxt(topBar.transform, "Streak", "", 26,
                new Vector2(0.5f, 1), new Vector2(0, -15), new Vector2(200, 50));
            _streakText.color = SSColorPalette.Warning;
            _streakText.alignment = TMPro.TextAlignmentOptions.Center;

            // Progress bar
            var pbg = MkFull(go.transform, "ProgressBg");
            var pr = pbg.GetComponent<RectTransform>();
            pr.anchorMin = new Vector2(0.25f, 1); pr.anchorMax = new Vector2(0.75f, 1);
            pr.pivot = new Vector2(0.5f, 1);
            pr.anchoredPosition = new Vector2(0, -95); pr.sizeDelta = new Vector2(0, 10);
            pbg.AddComponent<Image>().color = SSColorPalette.WithAlpha(Color.white, 0.08f);

            var fill = new GameObject("Fill");
            fill.transform.SetParent(pbg.transform, false);
            var fr = fill.AddComponent<RectTransform>();
            fr.anchorMin = Vector2.zero; fr.anchorMax = new Vector2(0, 1);
            fr.pivot = new Vector2(0, 0.5f);
            fr.offsetMin = fr.offsetMax = Vector2.zero;
            _progressFill = fill.AddComponent<Image>();
            _progressFill.color = SSColorPalette.Accent;

            _progressText = MkTxt(go.transform, "ProgTxt", "0 / 0", 14,
                new Vector2(0.5f, 1), new Vector2(0, -112), new Vector2(200, 25));
            _progressText.alignment = TMPro.TextAlignmentOptions.Center;
            _progressText.color = SSColorPalette.WithAlpha(Color.white, 0.5f);

            // Hint
            _hintText = MkTxt(go.transform, "Hint", "", 18,
                new Vector2(0.5f, 0), new Vector2(0, 200), new Vector2(600, 40));
            _hintText.alignment = TMPro.TextAlignmentOptions.Center;
            _hintText.color = SSColorPalette.WithAlpha(Color.white, 0.5f);
            _hintText.fontStyle = TMPro.FontStyles.Italic;
        }

        // ── Options Panel ────────────────────────────────────────────

        private void BuildOptionsPanel(Transform parent)
        {
            var go = new GameObject("OptionsPanel");
            go.transform.SetParent(parent, false);
            var r = go.AddComponent<RectTransform>();
            r.anchorMin = new Vector2(0, 0); r.anchorMax = new Vector2(1, 0);
            r.pivot = new Vector2(0.5f, 0);
            r.anchoredPosition = new Vector2(0, 20);
            r.sizeDelta = new Vector2(-80, 160);
            _optionsPanel = go.AddComponent<CanvasGroup>();
            go.AddComponent<Image>().color =
                SSColorPalette.WithAlpha(SSColorPalette.PrimaryDark, 0.85f);

            var layout = go.AddComponent<HorizontalLayoutGroup>();
            layout.spacing = 16;
            layout.padding = new RectOffset(30, 30, 20, 20);
            layout.childAlignment = TextAnchor.MiddleCenter;
            layout.childForceExpandWidth = true;
            layout.childForceExpandHeight = true;
            layout.childControlWidth = true;
            layout.childControlHeight = true;

            _optionsContainer = go.transform;

            MkTxt(parent, "OptionsLabel",
                "اسحب الإجابة الصحيحة إلى الكلمة الخاطئة ↑", 14,
                new Vector2(0.5f, 0), new Vector2(0, 185), new Vector2(500, 25))
                .color = SSColorPalette.WithAlpha(SSColorPalette.Accent, 0.6f);
        }

        // ── Feedback Overlay ─────────────────────────────────────────

        private void BuildFeedbackOverlay(Transform parent)
        {
            var go = new GameObject("FeedbackOverlay");
            go.transform.SetParent(parent, false);
            var r = go.AddComponent<RectTransform>();
            r.anchorMin = new Vector2(0.2f, 0.4f);
            r.anchorMax = new Vector2(0.8f, 0.6f);
            r.sizeDelta = Vector2.zero;

            _feedbackGroup = go.AddComponent<CanvasGroup>();
            _feedbackGroup.blocksRaycasts = false;
            _feedbackGroup.interactable = false;

            _feedbackBg = go.AddComponent<Image>();
            _feedbackBg.color = SSColorPalette.WithAlpha(SSColorPalette.Success, 0.92f);

            _feedbackText = MkTxt(go.transform, "FeedbackTxt", "", 34,
                Vector2.zero, Vector2.zero, Vector2.zero);
            _feedbackText.rectTransform.anchorMin = Vector2.zero;
            _feedbackText.rectTransform.anchorMax = Vector2.one;
            _feedbackText.rectTransform.offsetMin = new Vector2(10, 5);
            _feedbackText.rectTransform.offsetMax = new Vector2(-10, -5);
            _feedbackText.alignment = TMPro.TextAlignmentOptions.Center;
            _feedbackText.color = Color.white;
            _feedbackText.fontStyle = TMPro.FontStyles.Bold;
            _feedbackText.enableWordWrapping = true;
        }

        // ── Loading ──────────────────────────────────────────────────

        private void BuildLoadingPanel(Transform parent)
        {
            var go = MkFull(parent, "LoadingUI");
            _loadingUI = go.AddComponent<CanvasGroup>();
            go.AddComponent<Image>().color =
                SSColorPalette.WithAlpha(SSColorPalette.PrimaryDark, 0.92f);
            _loadingText = MkTxt(go.transform, "Txt", "جاري التحميل...", 28,
                new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(500, 60));
            _loadingText.alignment = TMPro.TextAlignmentOptions.Center;
            _loadingText.color = Color.white;
        }

        // ── Error ────────────────────────────────────────────────────

        private void BuildErrorPanel(Transform parent)
        {
            var go = MkFull(parent, "ErrorUI");
            _errorUI = go.AddComponent<CanvasGroup>();
            go.AddComponent<Image>().color =
                SSColorPalette.WithAlpha(SSColorPalette.Danger, 0.92f);
            _errorText = MkTxt(go.transform, "Txt", "خطأ", 24,
                new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(600, 100));
            _errorText.alignment = TMPro.TextAlignmentOptions.Center;
            _errorText.color = Color.white;
            _errorText.enableWordWrapping = true;
        }

        // ── Results ──────────────────────────────────────────────────

        private void BuildResultsPanel(Transform parent)
        {
            var go = MkFull(parent, "ResultsUI");
            _resultsUI = go.AddComponent<CanvasGroup>();
            go.AddComponent<Image>().color =
                SSColorPalette.WithAlpha(SSColorPalette.PrimaryDark, 0.95f);

            _resultTitle = MkTxt(go.transform, "Title", "أحسنت! 🎉", 48,
                new Vector2(0.5f, 0.5f), new Vector2(0, 180), new Vector2(500, 70));
            _resultTitle.alignment = TMPro.TextAlignmentOptions.Center;
            _resultTitle.color = SSColorPalette.Accent;
            _resultTitle.fontStyle = TMPro.FontStyles.Bold;

            _resultScore = MkTxt(go.transform, "Score", "0", 72,
                new Vector2(0.5f, 0.5f), new Vector2(0, 90), new Vector2(300, 90));
            _resultScore.alignment = TMPro.TextAlignmentOptions.Center;
            _resultScore.color = Color.white;
            _resultScore.fontStyle = TMPro.FontStyles.Bold;

            _resultStats = MkTxt(go.transform, "Stats", "", 22,
                new Vector2(0.5f, 0.5f), new Vector2(0, -20), new Vector2(500, 80));
            _resultStats.alignment = TMPro.TextAlignmentOptions.Center;
            _resultStats.color = SSColorPalette.WithAlpha(Color.white, 0.7f);
            _resultStats.enableWordWrapping = true;

            // Restart button
            var btn = new GameObject("RestartBtn");
            btn.transform.SetParent(go.transform, false);
            var br = btn.AddComponent<RectTransform>();
            br.anchorMin = br.anchorMax = new Vector2(0.5f, 0.5f);
            br.anchoredPosition = new Vector2(0, -110); br.sizeDelta = new Vector2(300, 60);
            btn.AddComponent<Image>().color = SSColorPalette.Accent;
            btn.AddComponent<Button>().onClick.AddListener(() =>
            {
                _resultsUI.DOFade(0, 0.3f).OnComplete(() =>
                    _resultsUI.gameObject.SetActive(false));
                onRestartClicked?.Invoke();
            });
            var bt = MkTxt(btn.transform, "BtnTxt", "إعادة اللعب 🔄", 24,
                Vector2.zero, Vector2.zero, Vector2.zero);
            bt.rectTransform.anchorMin = Vector2.zero;
            bt.rectTransform.anchorMax = Vector2.one;
            bt.rectTransform.offsetMin = bt.rectTransform.offsetMax = Vector2.zero;
            bt.alignment = TMPro.TextAlignmentOptions.Center;
            bt.color = SSColorPalette.TextDark;
            bt.fontStyle = TMPro.FontStyles.Bold;
        }

        // ════════════════════════════════════════════════════════════
        // PUBLIC METHODS
        // ════════════════════════════════════════════════════════════

        public void ShowGameUI()
        { _gameUI.gameObject.SetActive(true); _gameUI.DOFade(1f, 0.3f); }

        public void SetScore(int score)
        {
            if (_scoreText == null) return;
            _scoreText.arabicText = score.ToString("N0");
            DOTween.Kill(_scoreText.transform, "sPunch");
            _scoreText.transform.DOPunchScale(Vector3.one * 0.2f, 0.3f, 6, 0.3f).SetId("sPunch");
        }

        public void SetStreak(int streak)
        {
            if (_streakText == null) return;
            _streakText.arabicText = streak > 1 ? $"🔥 ×{streak}" : "";
            if (streak > 1)
            {
                DOTween.Kill(_streakText.transform, "skPop");
                _streakText.transform.localScale = Vector3.one * 1.5f;
                _streakText.transform.DOScale(1f, 0.4f).SetEase(Ease.OutElastic).SetId("skPop");
            }
        }

        public void SetProgress(int current, int total)
        {
            if (_progressText != null) _progressText.arabicText = $"{current} / {total}";
            if (_progressFill != null && total > 0)
            {
                DOTween.Kill(_progressFill.rectTransform, "pFill");
                _progressFill.rectTransform.DOAnchorMax(new Vector2((float)current / total, 1f), 0.5f)
                    .SetEase(Ease.OutQuad).SetId("pFill");
            }
        }

        public void ShowHint(string hint)
        {
            if (_hintText == null) return;
            _hintText.arabicText = hint;
            DOTween.Kill(_hintText, "hintFade");
            _hintText.alpha = 0;
            _hintText.DOFade(1, 0.4f).SetId("hintFade");
        }

        public void ShowFeedback(string message, bool isCorrect)
        {
            if (_feedbackText != null) _feedbackText.arabicText = message;
            if (_feedbackBg != null)
                _feedbackBg.color = SSColorPalette.WithAlpha(
                    isCorrect ? SSColorPalette.Success : SSColorPalette.Danger, 0.92f);

            DOTween.Kill(_feedbackGroup, "fbAnim");
            _feedbackGroup.alpha = 0;
            var bgRect = _feedbackBg?.rectTransform;
            if (bgRect != null)
            {
                bgRect.localScale = new Vector3(0.3f, 0f, 1f);
                var seq = DOTween.Sequence().SetId("fbAnim");
                seq.Append(_feedbackGroup.DOFade(1f, 0.12f));
                seq.Join(bgRect.DOScaleX(1f, 0.25f).SetEase(Ease.OutBack));
                seq.Join(bgRect.DOScaleY(1f, 0.2f).SetEase(Ease.OutBack).SetDelay(0.04f));
                seq.AppendInterval(1f);
                seq.Append(_feedbackGroup.DOFade(0f, 0.3f));
            }
        }

        // ── Options ──────────────────────────────────────────────────

        public void ShowOptions(CsOption[] options, CsWordButton targetWord)
        {
            ClearOptions();
            _optionsPanel.gameObject.SetActive(true);
            DOTween.Kill(_optionsPanel, "optFade");
            _optionsPanel.alpha = 0;
            _optionsPanel.DOFade(1f, 0.3f).SetId("optFade");

            var rect = _optionsPanel.GetComponent<RectTransform>();
            DOTween.Kill(rect, "optSlide");
            rect.anchoredPosition = new Vector2(rect.anchoredPosition.x, -100);
            rect.DOAnchorPosY(20, 0.4f).SetEase(Ease.OutBack).SetId("optSlide");

            if (options == null) return;
            for (int i = 0; i < options.Length; i++)
            {
                var opt = options[i];
                var optObj = new GameObject($"Option_{i}");
                optObj.transform.SetParent(_optionsContainer, false);
                optObj.AddComponent<RectTransform>().sizeDelta = new Vector2(200, 120);
                var draggable = optObj.AddComponent<CsOptionDraggable>();
                draggable.Setup(opt.option_text, opt.is_correct, _canvas, targetWord);
                _activeOptions.Add(draggable);
            }
        }

        public void HideOptions()
        {
            if (_optionsPanel == null) return;
            DOTween.Kill(_optionsPanel, "optFade");
            var rect = _optionsPanel.GetComponent<RectTransform>();
            DOTween.Kill(rect, "optSlide");
            var seq = DOTween.Sequence();
            seq.Append(_optionsPanel.DOFade(0f, 0.25f));
            seq.Join(rect.DOAnchorPosY(-100, 0.3f).SetEase(Ease.InBack));
            seq.OnComplete(() =>
            {
                ClearOptions();
                _optionsPanel.gameObject.SetActive(false);
            });
        }

        private void ClearOptions()
        {
            foreach (var opt in _activeOptions)
            {
                if (opt != null && opt.gameObject != null)
                { DOTween.Kill(opt.transform); Destroy(opt.gameObject); }
            }
            _activeOptions.Clear();
        }

        // ── Loading / Error / Results ────────────────────────────────

        public void ShowLoading(string msg)
        { _loadingUI.gameObject.SetActive(true); if (_loadingText != null) _loadingText.arabicText = 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.arabicText = msg; _errorUI.DOFade(1f, 0.3f); }

        public void ShowResults(int score, int correct, int wrong, int total, int bestStreak)
        {
            _resultsUI.gameObject.SetActive(true);
            _resultsUI.alpha = 0;

            float pct = total > 0 ? (float)correct / total * 100f : 0;
            if (_resultTitle != null)
                _resultTitle.arabicText = pct >= 70 ? "أحسنت! 🎉" : "عمل جيد! 👏";
            if (_resultScore != null) _resultScore.arabicText = score.ToString("N0");
            if (_resultStats != null)
                _resultStats.arabicText = $"✓ صحيح: {correct} / {total}  |  " +
                                    $"✗ خطأ: {wrong}  |  " +
                                    $"🔥 أعلى سلسلة: {bestStreak}";

            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 (_resultScore != null)
            {
                _resultScore.transform.localScale = Vector3.zero;
                seq.Append(_resultScore.transform.DOScale(1f, 0.4f).SetEase(Ease.OutBack));
                int ds = 0;
                DOTween.To(() => ds, x => { ds = x; if (_resultScore != null) _resultScore.arabicText = x.ToString("N0"); },
                    score, 1.2f).SetEase(Ease.OutQuad).SetDelay(0.8f);
            }
        }

        public void ResetUI()
        {
            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 (_optionsPanel != null) { _optionsPanel.alpha = 0; _optionsPanel.gameObject.SetActive(false); }
            if (_feedbackGroup != null) _feedbackGroup.alpha = 0;
            ClearOptions();
        }

        // ── Helpers ──────────────────────────────────────────────────

        private GameObject MkFull(Transform p, string n)
        {
            var go = new GameObject(n);
            go.transform.SetParent(p, false);
            var r = go.AddComponent<RectTransform>();
            r.anchorMin = Vector2.zero; r.anchorMax = Vector2.one;
            r.sizeDelta = Vector2.zero; r.anchoredPosition = Vector2.zero;
            return go;
        }

        private ArabicTextMeshProUGUI MkTxt(Transform p, string n, string text,
            float sz, Vector2 anchor, Vector2 pos, Vector2 size)
        {
            var go = new GameObject(n);
            go.transform.SetParent(p, false);
            var r = go.AddComponent<RectTransform>();
            r.anchorMin = r.anchorMax = anchor;
            r.pivot = anchor;
            r.anchoredPosition = pos; r.sizeDelta = size;
            var t = go.AddComponent<ArabicTextMeshProUGUI>();
            t.arabicText = text; t.fontSize = sz; t.enableWordWrapping = true;
            SSFontManager.Apply(t);
            return t;
        }
    }
}

```

---

### `ScienceStreet/CS/Scripts/CsWordButton.cs`

> Lines: 215 | Size: 8.9KB

```csharp
using UnityEngine;
using DG.Tweening;
using ALArcade.ArabicTMP;

namespace com.al_arcade.cs
{
    using shared;

    [RequireComponent(typeof(BoxCollider))]
    public class CsWordButton : MonoBehaviour
    {
        public string WordText { get; private set; }
        public bool IsWrong { get; private set; }
        public int Index { get; private set; }

        private ArabicTextMeshPro _text;
        private GameObject _background;
        private Material _bgMaterial;
        private BoxCollider _collider;
        private bool _isHighlighted;
        private Sequence _idleAnim;
        private float _cardWidth;
        private float _cardHeight = 0.8f;

        /// <summary>
        /// Setup with explicit width and optional scale factor to prevent overlaps.
        /// </summary>
        public void Setup(string text, bool isWrong, int index,
            float cardWidth = -1f, float scaleFactor = 1f)
        {
            WordText = text;
            IsWrong = isWrong;
            Index = index;

            // ★ If width not provided, calculate from text
            if (cardWidth <= 0)
                _cardWidth = text.Length * 0.32f + 0.6f;
            else
                _cardWidth = cardWidth;

            _cardWidth = Mathf.Max(_cardWidth, 0.8f);

            BuildVisuals();
            StartIdleAnimation();
        }

        private void BuildVisuals()
        {
            var shader = Shader.Find("Universal Render Pipeline/Lit")
                ?? Shader.Find("Standard") ?? Shader.Find("Unlit/Color");

            // ── Background card ──────────────────────────────────────
            _background = GameObject.CreatePrimitive(PrimitiveType.Cube);
            _background.name = "WordBg";
            _background.transform.SetParent(transform);
            _background.transform.localPosition = Vector3.zero;
            _background.transform.localScale = new Vector3(_cardWidth, _cardHeight, 0.08f);
            Destroy(_background.GetComponent<Collider>());

            _bgMaterial = new Material(shader) { color = SSColorPalette.NeutralWord };
            _background.GetComponent<Renderer>().material = _bgMaterial;

            // ── Text ─────────────────────────────────────────────────
            var textObj = new GameObject("WordText");
            textObj.transform.SetParent(transform);
            textObj.transform.localPosition = new Vector3(0, 0, -0.05f);
            // ★ Face camera (identity rotation — parent handles billboard in LateUpdate if needed)
            textObj.transform.localRotation = Quaternion.identity;

            _text = textObj.AddComponent<ArabicTextMeshPro>();
            _text.arabicText = WordText;
            _text.fontSize = 4.5f;
            _text.alignment = TMPro.TextAlignmentOptions.Center;
            _text.color = SSColorPalette.TextDark;
            _text.fontStyle = TMPro.FontStyles.Bold;
            _text.enableAutoSizing = true;
            _text.fontSizeMin = 2f;
            _text.fontSizeMax = 5f;
            _text.rectTransform.sizeDelta = new Vector2(_cardWidth - 0.2f, _cardHeight - 0.1f);
            _text.enableWordWrapping = false;
            _text.overflowMode = TMPro.TextOverflowModes.Ellipsis;
            SSFontManager.Apply(_text);

            // ── Collider (matches card exactly) ──────────────────────
            _collider = GetComponent<BoxCollider>();
            _collider.size = new Vector3(_cardWidth, _cardHeight, 0.2f);

            // ── Shadow ───────────────────────────────────────────────
            var shadow = GameObject.CreatePrimitive(PrimitiveType.Cube);
            shadow.name = "Shadow";
            shadow.transform.SetParent(transform);
            shadow.transform.localPosition = new Vector3(0.04f, -0.04f, 0.02f);
            shadow.transform.localScale = new Vector3(_cardWidth, _cardHeight, 0.06f);
            Destroy(shadow.GetComponent<Collider>());
            shadow.GetComponent<Renderer>().material =
                new Material(shader) { color = new Color(0, 0, 0, 0.12f) };
        }

        private void StartIdleAnimation()
        {
            KillTweens();
            float baseY = transform.position.y;
            float offset = Index * 0.15f;
            _idleAnim = DOTween.Sequence();
            _idleAnim.Append(
                transform.DOMoveY(baseY + 0.05f, 1.5f + offset * 0.1f)
                    .SetEase(Ease.InOutSine));
            _idleAnim.Append(
                transform.DOMoveY(baseY, 1.5f + offset * 0.1f)
                    .SetEase(Ease.InOutSine));
            _idleAnim.SetLoops(-1, LoopType.Restart);
            _idleAnim.SetDelay(offset);
        }

        private void KillTweens()
        {
            _idleAnim?.Kill();
            _idleAnim = null;
            DOTween.Kill(transform);
            if (_bgMaterial != null) DOTween.Kill(_bgMaterial);
        }

        // ── Billboard towards camera ─────────────────────────────────
        private void LateUpdate()
        {
            if (Camera.main == null) return;
            Vector3 awayFromCam = transform.position - Camera.main.transform.position;
            awayFromCam.y = 0;
            if (awayFromCam.sqrMagnitude > 0.001f)
                transform.rotation = Quaternion.LookRotation(awayFromCam);
        }

        // ── Mouse Interaction ────────────────────────────────────────

        private void OnMouseEnter()
        {
            if (_isHighlighted) return;
            DOTween.Kill(_bgMaterial, "hover");
            _bgMaterial.DOColor(SSColorPalette.WithAlpha(SSColorPalette.Primary, 0.3f), 0.2f)
                .SetId("hover");
            DOTween.Kill(transform, "hoverScale");
            transform.DOScale(Vector3.one * 1.1f, 0.2f).SetEase(Ease.OutQuad)
                .SetId("hoverScale");
        }

        private void OnMouseExit()
        {
            if (_isHighlighted) return;
            DOTween.Kill(_bgMaterial, "hover");
            _bgMaterial.DOColor(SSColorPalette.NeutralWord, 0.2f).SetId("hover");
            DOTween.Kill(transform, "hoverScale");
            transform.DOScale(Vector3.one, 0.2f).SetEase(Ease.OutQuad).SetId("hoverScale");
        }

        private void OnMouseDown()
        {
            if (CsGameManager.Instance != null)
                CsGameManager.Instance.OnWordClicked(this);
        }

        // ── Public Methods ───────────────────────────────────────────

        public void Highlight(bool isWrongWord)
        {
            _isHighlighted = true;
            KillTweens();

            Color targetColor = isWrongWord
                ? SSColorPalette.WrongWord
                : SSColorPalette.CorrectWord;

            _bgMaterial.DOColor(targetColor, 0.3f);
            if (_text != null) _text.color = Color.white;
            transform.DOPunchScale(Vector3.one * 0.2f, 0.4f, 8, 0.3f);

            // Pulsing glow
            DOTween.Sequence()
                .Append(_bgMaterial.DOColor(Color.Lerp(targetColor, Color.white, 0.3f), 0.5f))
                .Append(_bgMaterial.DOColor(targetColor, 0.5f))
                .SetLoops(-1, LoopType.Restart);
        }

        public void ShakeWrong()
        {
            DOTween.Kill(transform, "shakeWrong");
            transform.DOShakePosition(0.3f, 0.15f, 15, 90, false, true).SetId("shakeWrong");
            transform.DOShakeRotation(0.3f, 5f, 15);

            Color orig = _bgMaterial.color;
            DOTween.Kill(_bgMaterial, "shakeColor");
            _bgMaterial.DOColor(SSColorPalette.WithAlpha(SSColorPalette.Danger, 0.3f), 0.1f)
                .SetId("shakeColor")
                .OnComplete(() => _bgMaterial.DOColor(orig, 0.3f));
        }

        public void ReplaceText(string newText)
        {
            DOTween.Kill(transform, "replace");
            var seq = DOTween.Sequence().SetId("replace");
            seq.Append(transform.DOScale(Vector3.one * 0.5f, 0.2f).SetEase(Ease.InBack));
            seq.AppendCallback(() =>
            {
                if (_text != null) _text.arabicText = newText;
                WordText = newText;
                DOTween.Kill(_bgMaterial);
                _bgMaterial.color = SSColorPalette.CorrectWord;
                if (_text != null) _text.color = Color.white;
            });
            seq.Append(transform.DOScale(Vector3.one * 1.2f, 0.3f).SetEase(Ease.OutBack));
            seq.Append(transform.DOScale(Vector3.one, 0.15f));
        }

        private void OnDestroy() => KillTweens();
    }
}

```

---

### `ScienceStreet/MCQ/Scripts/McqDemoBuilder.cs`

> Lines: 330 | Size: 14.5KB

```csharp

using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Events;
using DG.Tweening;
using TMPro;

namespace com.al_arcade.mcq
{
    using shared;

    [AddComponentMenu("Science Street/MCQ Demo Builder")]
    public class McqDemoBuilder : MonoBehaviour
    {
        [Header("Build Configuration")]
        [SerializeField] private string buildType = "scistreet";
        [SerializeField] private int gradeId = 0;
        [SerializeField] private int questionCount = 10;
        [SerializeField] private string classCode = "";

        [Header("Game Settings")]
        [SerializeField] private float runSpeed = 12f;
        [SerializeField] private int lives = 3;

        [Header("Arabic Font")]
        [Tooltip("Drag your Arabic TMP Font Asset here")]
        [SerializeField] private TMP_FontAsset arabicFont;

        [Header("Debug")]
        [SerializeField] private bool useOfflineTestData = false;

        private McqGameManager _gm;
        private McqPlayerRunner _player;
        private McqQuestionDisplay _questionDisplay;
        private McqUIManager _uiManager;

        private void Start()
        {
            DOTween.Init();
            if (arabicFont != null) SSFontManager.Font = arabicFont;
            StartCoroutine(BuildEverything());
        }

        private IEnumerator BuildEverything()
        {
            Debug.Log("══════ MCQ Runner Demo Builder ══════");

            // ── EventSystem ──────────────────────────────────────────
            if (FindObjectOfType<EventSystem>() == null)
            {
                var es = new GameObject("EventSystem");
                es.AddComponent<EventSystem>();
                es.AddComponent<StandaloneInputModule>();
            }

            // ── Managers ─────────────────────────────────────────────
            SSApiManager.EnsureInstance();
            SSAudioManager.EnsureInstance();
            SSParticleManager.EnsureInstance();

            var session = SSGameSession.EnsureInstance();
            session.buildType = buildType;
            session.gradeId = gradeId;
            session.questionCount = questionCount;
            session.classCode = classCode;
            yield return null;

            // ── Scene ────────────────────────────────────────────────
            BuildScene();
            yield return null;

            // ── Player ───────────────────────────────────────────────
            BuildPlayer();
            yield return null;

            // ── Question Display ─────────────────────────────────────
            BuildQuestionDisplay();
            yield return null;

            // ── UI ───────────────────────────────────────────────────
            BuildUI();
            yield return null;

            // ── Game Manager ─────────────────────────────────────────
            BuildGameManager();
            yield return null;

            // ── Camera ───────────────────────────────────────────────
            SetupCamera();
            yield return null;

            // ── Lighting ─────────────────────────────────────────────
            SetupLighting();
            yield return null;

            Debug.Log("══════ MCQ Ready — A/D or ←/→ to steer ══════");

            yield return new WaitForSeconds(0.3f);

            // ── Start ────────────────────────────────────────────────
            if (useOfflineTestData)
                _gm.StartWithQuestions(GetTestQuestions());
            else
                _gm.StartGame();
        }

        // ════════════════════════════════════════════════════════════
        // BUILD METHODS
        // ════════════════════════════════════════════════════════════

        private void BuildScene()
        {
            Camera.main.clearFlags = CameraClearFlags.SolidColor;
            Camera.main.backgroundColor = new Color32(180, 200, 240, 255);

            var shader = GetLitShader();

            // Road (centered at x=0, extends along +Z)
            var road = GameObject.CreatePrimitive(PrimitiveType.Cube);
            road.name = "Road";
            road.transform.position = new Vector3(0, -0.05f, 200);
            road.transform.localScale = new Vector3(16, 0.1f, 500);
            road.GetComponent<Renderer>().material = new Material(shader)
                { color = new Color32(60, 60, 80, 255) };

            // Lane dividers
            for (int i = -1; i <= 1; i += 2)
            {
                var line = GameObject.CreatePrimitive(PrimitiveType.Cube);
                line.name = $"LaneLine_{i}";
                line.transform.position = new Vector3(i * 2f, 0.01f, 200);
                line.transform.localScale = new Vector3(0.08f, 0.01f, 500);
                Destroy(line.GetComponent<Collider>());
                line.GetComponent<Renderer>().material = new Material(shader)
                    { color = SSColorPalette.WithAlpha(SSColorPalette.Accent, 0.4f) };
            }

            // Center dashes
            for (float z = 0; z < 400; z += 4)
            {
                var dash = GameObject.CreatePrimitive(PrimitiveType.Cube);
                dash.name = "Dash";
                dash.transform.position = new Vector3(0, 0.01f, z);
                dash.transform.localScale = new Vector3(0.05f, 0.01f, 2f);
                Destroy(dash.GetComponent<Collider>());
                dash.GetComponent<Renderer>().material = new Material(shader)
                    { color = SSColorPalette.WithAlpha(Color.white, 0.2f) };
            }

            // Side decorations
            for (int side = -1; side <= 1; side += 2)
            {
                for (float z = 0; z < 400; z += 20)
                {
                    var block = GameObject.CreatePrimitive(PrimitiveType.Cube);
                    block.name = "SideBlock";
                    float h = Random.Range(2f, 5f);
                    block.transform.position = new Vector3(side * 12f, h / 2f, z);
                    block.transform.localScale = new Vector3(
                        Random.Range(2f, 4f), h, Random.Range(3f, 8f));
                    Destroy(block.GetComponent<Collider>());
                    var c = Color.Lerp(SSColorPalette.Primary,
                        SSColorPalette.PrimaryLight, Random.value);
                    block.GetComponent<Renderer>().material = new Material(shader)
                        { color = SSColorPalette.WithAlpha(c, 0.4f) };
                }
            }

            // Horizon
            var horizon = GameObject.CreatePrimitive(PrimitiveType.Cube);
            horizon.name = "Horizon";
            horizon.transform.position = new Vector3(0, 2f, 500);
            horizon.transform.localScale = new Vector3(200, 40, 1);
            Destroy(horizon.GetComponent<Collider>());
            horizon.GetComponent<Renderer>().material = new Material(shader)
                { color = new Color32(100, 120, 180, 255) };
        }

        private void BuildPlayer()
        {
            var obj = new GameObject("Player");
            obj.tag = "Player";
            obj.transform.position = Vector3.zero;

            _player = obj.AddComponent<McqPlayerRunner>();
            _player.BuildDefaultVisuals();

            var rb = obj.GetComponent<Rigidbody>();
            if (rb == null) rb = obj.AddComponent<Rigidbody>();
            rb.isKinematic = true;
            rb.useGravity = false;
        }

        private void BuildQuestionDisplay()
        {
            var obj = new GameObject("QuestionDisplay");
            obj.transform.position = new Vector3(0, 8, 10);

            _questionDisplay = obj.AddComponent<McqQuestionDisplay>();
            _questionDisplay.Build(_player.transform);
        }

        private void BuildUI()
        {
            var obj = new GameObject("McqUI");
            _uiManager = obj.AddComponent<McqUIManager>();
            _uiManager.BuildUI();

            // ★ FIX: Initialize the UnityEvent if null
            if (_uiManager.onRestartClicked == null)
                _uiManager.onRestartClicked = new UnityEvent();
        }

        private void BuildGameManager()
        {
            var obj = new GameObject("McqGameManager");
            _gm = obj.AddComponent<McqGameManager>();
            _gm.player = _player;
            _gm.questionDisplay = _questionDisplay;
            _gm.uiManager = _uiManager;
            _gm.gateParent = new GameObject("Gates").transform;

            // ★ FIX: Safe listener — all refs captured after they exist
            _uiManager.onRestartClicked.AddListener(OnRestartClicked);
        }

        private void OnRestartClicked()
        {
            if (_gm != null) _gm.ResetGame();
            if (_player != null) _player.ResetToStart();
            SetupCamera();
            if (_gm != null) _gm.StartGame();
        }

        private void SetupCamera()
        {
            var cam = Camera.main;
            if (cam == null) return;

            var follower = cam.GetComponent<McqCameraFollow>();
            if (follower == null)
                follower = cam.gameObject.AddComponent<McqCameraFollow>();

            follower.target = _player != null ? _player.transform : null;
            follower.offset = new Vector3(0, 5f, -8f);
            follower.lookOffset = new Vector3(0, 1.5f, 12f);
            follower.smoothSpeed = 6f;
        }

        private void SetupLighting()
        {
            var existing = FindObjectOfType<Light>();
            Light dirLight;
            if (existing != null)
            {
                dirLight = existing;
            }
            else
            {
                var lo = new GameObject("DirLight");
                dirLight = lo.AddComponent<Light>();
            }

            dirLight.type = LightType.Directional;
            dirLight.transform.rotation = Quaternion.Euler(45, -30, 0);
            dirLight.color = new Color32(255, 245, 230, 255);
            dirLight.intensity = 1.2f;
            dirLight.shadows = LightShadows.Soft;

            RenderSettings.ambientLight = new Color32(80, 85, 110, 255);
            RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Flat;
        }

        // ════════════════════════════════════════════════════════════
        // TEST DATA
        // ════════════════════════════════════════════════════════════

        private McqQuestion[] GetTestQuestions()
        {
            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="تاريخ العلوم" },
            };
        }

        private Shader GetLitShader()
        {
            return Shader.Find("Universal Render Pipeline/Lit")
                ?? Shader.Find("Standard")
                ?? Shader.Find("Unlit/Color");
        }
    }

    // ════════════════════════════════════════════════════════════════
    // Camera follow — 3rd-person runner (behind + above)
    // ════════════════════════════════════════════════════════════════
    public class McqCameraFollow : MonoBehaviour
    {
        public Transform target;
        public Vector3 offset = new(0, 5, -8);
        public Vector3 lookOffset = new(0, 1.5f, 12);
        public float smoothSpeed = 6f;

        private void LateUpdate()
        {
            if (target == null) return;
            Vector3 desired = target.position + offset;
            transform.position = Vector3.Lerp(transform.position, desired,
                Time.deltaTime * smoothSpeed);
            Vector3 lookTarget = target.position + lookOffset;
            transform.rotation = Quaternion.Slerp(transform.rotation,
                Quaternion.LookRotation(lookTarget - transform.position),
                Time.deltaTime * smoothSpeed);
        }
    }
}
```

---

### `ScienceStreet/MCQ/Scripts/McqGameManager.cs`

> Lines: 391 | Size: 14.0KB

```csharp
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using DG.Tweening;
using ALArcade.ArabicTMP;

namespace com.al_arcade.mcq
{
    using shared;

    public enum McqGameState
    {
        Idle, Loading, ShowingQuestion, WaitingForAnswer,
        AnswerFeedback, BetweenQuestions, GameComplete, GameOver
    }

    public class McqGameManager : MonoBehaviour
    {
        public static McqGameManager Instance { get; private set; }

        [Header("Game Settings")]
        [SerializeField] private float gateSpawnDistance = 40f;
        [SerializeField] private float questionDisplayTime = 1.5f;
        [SerializeField] private float feedbackDisplayTime = 2f;
        [SerializeField] private float playerRunSpeed = 12f;
        [SerializeField] private float gateSpacing = 4f;
        [SerializeField] private float streakBonusThreshold = 3;
        [SerializeField] private int totalLives = 3;

        [Header("References")]
        public McqPlayerRunner player;
        public McqQuestionDisplay questionDisplay;
        public McqUIManager uiManager;
        public Transform gateParent;

        [Header("Runtime State (Read Only)")]
        [SerializeField] private McqGameState _state = McqGameState.Idle;
        public McqGameState State => _state;

        private McqQuestion[] _questions;
        private int _currentIndex;
        private int _score, _streak, _bestStreak, _lives;
        private int _correctCount, _wrongCount;

        private List<McqGateController> _activeGates = new();
        private int _correctGateIndex = -1;

        [Header("Events")]
        public UnityEvent onGameStart;
        public UnityEvent<McqQuestion> onQuestionPresented;
        public UnityEvent<bool> onAnswerGiven;
        public UnityEvent<int, int> onScoreChanged;
        public UnityEvent<int> onLifeLost;
        public UnityEvent<int> onGameComplete;
        public UnityEvent onGameOver;

        private void Awake()
        {
            if (Instance != null && Instance != this) { Destroy(gameObject); return; }
            Instance = this;
        }

        // ── Public API ───────────────────────────────────────────────

        public void StartGame() => StartCoroutine(StartGameRoutine());

        public void StartWithQuestions(McqQuestion[] questions)
        {
            _questions = questions;
            BeginGameplay();
        }

        public void ResetGame()
        {
            StopAllCoroutines();
            _state = McqGameState.Idle;
            _currentIndex = 0;
            _score = _streak = _bestStreak = _correctCount = _wrongCount = 0;
            _lives = totalLives;
            _questions = null;

            foreach (var g in _activeGates)
                if (g != null) { DOTween.Kill(g.transform); Destroy(g.gameObject); }
            _activeGates.Clear();

            if (player != null) player.ResetToStart();
            if (uiManager != null) uiManager.ResetUI();
            if (questionDisplay != null) questionDisplay.Hide();
        }

        // ── Game Flow ────────────────────────────────────────────────

        private IEnumerator StartGameRoutine()
        {
            _state = McqGameState.Loading;
            if (uiManager != null) uiManager.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)
            {
                if (uiManager != null) uiManager.ShowError(error ?? "لا توجد أسئلة متاحة");
                _state = McqGameState.Idle;
                yield break;
            }

            if (uiManager != null) uiManager.HideLoading();
            BeginGameplay();
        }

        private void BeginGameplay()
        {
            _currentIndex = 0;
            _score = _streak = _bestStreak = _correctCount = _wrongCount = 0;
            _lives = totalLives;

            if (uiManager != null)
            {
                uiManager.SetLives(totalLives, totalLives);
                uiManager.SetScore(0);
                uiManager.SetProgress(0, _questions.Length);
                uiManager.ShowGameUI();
            }

            onGameStart?.Invoke();
            StartCoroutine(GameLoop());
        }

        private IEnumerator GameLoop()
        {
            while (_currentIndex < _questions.Length && _lives > 0)
            {
                yield return PresentQuestion(_questions[_currentIndex]);
                _currentIndex++;
                if (uiManager != null)
                    uiManager.SetProgress(_currentIndex, _questions.Length);
            }

            if (_lives <= 0)
            {
                _state = McqGameState.GameOver;
                yield return GameOverSequence();
            }
            else
            {
                _state = McqGameState.GameComplete;
                yield return VictorySequence();
            }
        }

        private IEnumerator PresentQuestion(McqQuestion question)
        {
            _state = McqGameState.ShowingQuestion;

            if (questionDisplay != null)
                questionDisplay.Show(question.question_text, question.source);

            onQuestionPresented?.Invoke(question);
            yield return new WaitForSeconds(questionDisplayTime);

            _state = McqGameState.WaitingForAnswer;
            SpawnGates(question);

            bool answered = false;
            bool wasCorrect = false;

            void OnGateHit(int idx)
            {
                answered = true;
                wasCorrect = idx == _correctGateIndex;
            }

            foreach (var gate in _activeGates)
                gate.onPlayerEnter += OnGateHit;

            if (player != null) player.RunForward(playerRunSpeed);

            float timeout = gateSpawnDistance / playerRunSpeed + 5f;
            float timer = 0;
            while (!answered && timer < timeout)
            {
                timer += Time.deltaTime;
                yield return null;
            }

            if (player != null) player.Stop();

            foreach (var gate in _activeGates)
                gate.onPlayerEnter -= OnGateHit;

            _state = McqGameState.AnswerFeedback;
            yield return ProcessAnswer(answered && wasCorrect);

            // Cleanup gates
            foreach (var gate in _activeGates)
            {
                if (gate != null)
                {
                    DOTween.Kill(gate.transform);
                    gate.transform.DOScale(Vector3.zero, 0.3f)
                        .SetEase(Ease.InBack)
                        .OnComplete(() => { if (gate != null) Destroy(gate.gameObject); });
                }
            }
            _activeGates.Clear();

            yield return new WaitForSeconds(0.5f);
            _state = McqGameState.BetweenQuestions;
            if (player != null) yield return player.MoveToNextLane(0.4f);
            if (questionDisplay != null) questionDisplay.Hide();
        }

        private IEnumerator ProcessAnswer(bool correct)
        {
            if (correct)
            {
                _correctCount++;
                _streak++;
                if (_streak > _bestStreak) _bestStreak = _streak;
                int points = 100;
                if (_streak >= streakBonusThreshold)
                    points += (_streak - (int)streakBonusThreshold + 1) * 25;
                _score += points;

                ShowCorrectFeedback(points);

                var audio = SSAudioManager.Instance;
                if (audio != null)
                {
                    if (audio.sfxCorrect != null) audio.PlayCorrect();
                    else audio.PlayCorrectBeep();
                }
                var particles = SSParticleManager.Instance;
                if (particles != null && player != null)
                    particles.PlayCorrectBurst(player.transform.position + Vector3.up * 2f);
            }
            else
            {
                _wrongCount++;
                _streak = 0;
                _lives--;
                ShowWrongFeedback();

                var audio = SSAudioManager.Instance;
                if (audio != null)
                {
                    if (audio.sfxWrong != null) audio.PlayWrong();
                    else audio.PlayWrongBeep();
                }
                var particles = SSParticleManager.Instance;
                if (particles != null && player != null)
                    particles.PlayWrongBurst(player.transform.position + Vector3.up * 2f);

                if (uiManager != null) uiManager.SetLives(_lives, totalLives);
                onLifeLost?.Invoke(_lives);
            }

            if (uiManager != null) { uiManager.SetScore(_score); uiManager.SetStreak(_streak); }
            onAnswerGiven?.Invoke(correct);
            onScoreChanged?.Invoke(_score, _streak);
            yield return new WaitForSeconds(feedbackDisplayTime);
        }

        // ══════════════════════════════════════════════════════════
        // GATE SPAWNING — always road-center X=0
        // ══════════════════════════════════════════════════════════

        private void SpawnGates(McqQuestion question)
        {
            string[] answers = question.GetShuffledAnswers(out int correctIdx);
            _correctGateIndex = correctIdx;
            _activeGates.Clear();

            float playerZ = player != null ? player.transform.position.z : 0f;

            // ★ Gates spawn relative to ROAD center (x=0), NOT player x
            Vector3 basePos = new Vector3(0f, 0f, playerZ + gateSpawnDistance);

            float totalWidth = (answers.Length - 1) * gateSpacing;
            float startX = -totalWidth / 2f;

            for (int i = 0; i < answers.Length; i++)
            {
                Vector3 gatePos = basePos + Vector3.right * (startX + i * gateSpacing);
                var gate = CreateGate(gatePos, i, answers[i], i == correctIdx);
                _activeGates.Add(gate);

                gate.transform.localScale = Vector3.zero;
                gate.transform.DOScale(Vector3.one, 0.5f)
                    .SetDelay(i * 0.1f)
                    .SetEase(Ease.OutBack);
            }
        }

        private McqGateController CreateGate(Vector3 position, int index,
            string answerText, bool isCorrect)
        {
            var go = new GameObject($"Gate_{index}");
            go.transform.position = position;
            if (gateParent != null) go.transform.SetParent(gateParent);

            var gate = go.AddComponent<McqGateController>();
            gate.Setup(index, answerText, isCorrect);
            return gate;
        }

        // ── Feedback ─────────────────────────────────────────────────

        private void ShowCorrectFeedback(int points)
        {
            if (uiManager != null)
            {
                string msg = _streak >= streakBonusThreshold
                    ? $"ممتاز! +{points} (سلسلة {_streak}×) ✨"
                    : $"صحيح! +{points} ✓";
                uiManager.ShowFeedback(msg, true);
            }
            foreach (var g in _activeGates)
            {
                if (g == null) continue;
                if (g.GateIndex == _correctGateIndex) g.PlayCorrectAnimation();
                else g.PlayFadeOut();
            }
        }

        private void ShowWrongFeedback()
        {
            if (uiManager != null) uiManager.ShowFeedback("خطأ! ✗", false);

            foreach (var g in _activeGates)
            {
                if (g == null) continue;
                if (g.GateIndex == _correctGateIndex) g.PlayRevealCorrect();
                else if (g.WasEntered) g.PlayWrongAnimation();
            }

            if (Camera.main != null)
            {
                DOTween.Kill(Camera.main.transform, "camShake");
                Camera.main.transform.DOShakePosition(0.4f, 0.3f, 15, 90f, false, true)
                    .SetEase(Ease.OutQuad).SetId("camShake");
            }
        }

        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);
            if (uiManager != null)
                uiManager.ShowResults(_score, _correctCount, _wrongCount,
                    _bestStreak, _questions.Length, true);
            onGameComplete?.Invoke(_score);
        }

        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);
            if (uiManager != null)
                uiManager.ShowResults(_score, _correctCount, _wrongCount,
                    _bestStreak, _questions.Length, false);
            onGameOver?.Invoke();
        }

        public int Score => _score;
        public int Lives => _lives;
        public int Streak => _streak;
        public int CurrentQuestionIndex => _currentIndex;
        public int TotalQuestions => _questions?.Length ?? 0;
    }
}

```

---

### `ScienceStreet/MCQ/Scripts/McqGateController.cs`

> Lines: 207 | Size: 9.2KB

```csharp
using System;
using UnityEngine;
using DG.Tweening;
using ALArcade.ArabicTMP;

namespace com.al_arcade.mcq
{
    using shared;

    public class McqGateController : MonoBehaviour
    {
        public int GateIndex { get; private set; }
        public string AnswerText { get; private set; }
        public bool IsCorrect { get; private set; }
        public bool WasEntered { get; private set; }

        public event Action<int> onPlayerEnter;

        private GameObject _backPanel;
        private ArabicTextMeshPro _label;
        private BoxCollider _collider;
        private Material _panelMaterial;
        private Sequence _idleAnim;
        private Sequence _glowAnim;

        private const float GateWidth = 2.5f;
        private const float GateHeight = 3.5f;
        private const float GateDepth = 0.8f;
        private const float FrameThickness = 0.15f;

        public void Setup(int index, string text, bool correct)
        {
            GateIndex = index;
            AnswerText = text;
            IsCorrect = correct;
            WasEntered = false;
            BuildVisuals();
            StartIdleAnimation();
        }

        private void BuildVisuals()
        {
            var shader = GetShader();

            // ── Pillars ──────────────────────────────────────────────
            CreatePillar(Vector3.left * (GateWidth / 2f), "LeftPillar", shader);
            CreatePillar(Vector3.right * (GateWidth / 2f), "RightPillar", shader);

            // ── Top bar ──────────────────────────────────────────────
            var topBar = GameObject.CreatePrimitive(PrimitiveType.Cube);
            topBar.name = "TopBar";
            topBar.transform.SetParent(transform);
            topBar.transform.localPosition = Vector3.up * GateHeight;
            topBar.transform.localScale = new Vector3(GateWidth + FrameThickness * 2, FrameThickness, GateDepth);
            Destroy(topBar.GetComponent<Collider>());
            var topMat = new Material(shader) { color = SSColorPalette.Primary };
            topBar.GetComponent<Renderer>().material = topMat;

            // ── Answer panel — on the +Z side (back wall of gate archway) ──
            _backPanel = GameObject.CreatePrimitive(PrimitiveType.Cube);
            _backPanel.name = "AnswerPanel";
            _backPanel.transform.SetParent(transform);
            _backPanel.transform.localPosition = new Vector3(0, GateHeight / 2f, GateDepth / 2f - 0.05f);
            _backPanel.transform.localScale = new Vector3(GateWidth - 0.1f, GateHeight * 0.5f, 0.05f);
            Destroy(_backPanel.GetComponent<Collider>());
            _panelMaterial = new Material(shader) { color = SSColorPalette.GateDefault };
            _backPanel.GetComponent<Renderer>().material = _panelMaterial;

            // ── Text label — identity rotation, readable from -Z (camera side) ──
            var textObj = new GameObject("AnswerLabel");
            textObj.transform.SetParent(transform);
            textObj.transform.localPosition = new Vector3(0, GateHeight / 2f, GateDepth / 2f - 0.08f);
            // ★ NO Euler(0,180,0) — TMP 3D text is readable from -Z by default,
            //   camera is behind player at -Z. Identity rotation = correct.
            textObj.transform.localRotation = Quaternion.identity;

            _label = textObj.AddComponent<ArabicTextMeshPro>();
            _label.arabicText = AnswerText;
            _label.fontSize = 4f;
            _label.alignment = TMPro.TextAlignmentOptions.Center;
            _label.color = Color.white;
            _label.fontStyle = TMPro.FontStyles.Bold;
            _label.enableAutoSizing = true;
            _label.fontSizeMin = 2f;
            _label.fontSizeMax = 5f;
            _label.rectTransform.sizeDelta = new Vector2(GateWidth - 0.4f, GateHeight * 0.45f);
            _label.enableWordWrapping = true;
            _label.overflowMode = TMPro.TextOverflowModes.Ellipsis;
            SSFontManager.Apply(_label);

            // ── Trigger collider ─────────────────────────────────────
            _collider = gameObject.AddComponent<BoxCollider>();
            _collider.isTrigger = true;
            _collider.center = new Vector3(0, GateHeight / 2f, 0);
            _collider.size = new Vector3(GateWidth, GateHeight, GateDepth);

            // ── Floor highlight ──────────────────────────────────────
            var floor = GameObject.CreatePrimitive(PrimitiveType.Cube);
            floor.name = "FloorHighlight";
            floor.transform.SetParent(transform);
            floor.transform.localPosition = new Vector3(0, 0.01f, 0);
            floor.transform.localScale = new Vector3(GateWidth, 0.02f, GateDepth * 1.5f);
            Destroy(floor.GetComponent<Collider>());
            var floorMat = new Material(shader)
            { color = SSColorPalette.WithAlpha(SSColorPalette.Primary, 0.15f) };
            floor.GetComponent<Renderer>().material = floorMat;
        }

        private void CreatePillar(Vector3 offset, string name, Shader shader)
        {
            var pillar = GameObject.CreatePrimitive(PrimitiveType.Cube);
            pillar.name = name;
            pillar.transform.SetParent(transform);
            pillar.transform.localPosition = offset + Vector3.up * (GateHeight / 2f);
            pillar.transform.localScale = new Vector3(FrameThickness, GateHeight, GateDepth);
            Destroy(pillar.GetComponent<Collider>());
            var mat = new Material(shader) { color = SSColorPalette.PrimaryDark };
            pillar.GetComponent<Renderer>().material = mat;
        }

        private void StartIdleAnimation()
        {
            KillAllTweens();
            float baseY = transform.position.y;
            _idleAnim = DOTween.Sequence();
            _idleAnim.Append(transform.DOMoveY(baseY + 0.15f, 1.2f).SetEase(Ease.InOutSine));
            _idleAnim.Append(transform.DOMoveY(baseY, 1.2f).SetEase(Ease.InOutSine));
            _idleAnim.SetLoops(-1, LoopType.Restart);

            if (_panelMaterial != null)
            {
                _glowAnim = DOTween.Sequence();
                _glowAnim.Append(_panelMaterial.DOColor(SSColorPalette.PrimaryLight, 1.5f));
                _glowAnim.Append(_panelMaterial.DOColor(SSColorPalette.GateDefault, 1.5f));
                _glowAnim.SetLoops(-1, LoopType.Restart);
            }
        }

        private void KillAllTweens()
        {
            _idleAnim?.Kill();
            _glowAnim?.Kill();
            _idleAnim = null;
            _glowAnim = null;
            DOTween.Kill(transform);
            if (_panelMaterial != null) DOTween.Kill(_panelMaterial);
            if (_label != null) DOTween.Kill(_label.transform);
        }

        // ── Collision ────────────────────────────────────────────────

        private void OnTriggerEnter(Collider other)
        {
            if (WasEntered) return;
            if (!other.CompareTag("Player") && other.GetComponent<McqPlayerRunner>() == null) return;
            WasEntered = true;
            onPlayerEnter?.Invoke(GateIndex);
        }

        // ── Feedback Animations ──────────────────────────────────────

        public void PlayCorrectAnimation()
        {
            KillAllTweens();
            if (_panelMaterial != null) _panelMaterial.DOColor(SSColorPalette.GateCorrect, 0.3f);
            transform.DOPunchScale(Vector3.one * 0.2f, 0.5f, 8, 0.5f);
            if (_label != null) _label.transform.DOPunchScale(Vector3.one * 0.3f, 0.4f, 6, 0.3f);
        }

        public void PlayWrongAnimation()
        {
            KillAllTweens();
            if (_panelMaterial != null) _panelMaterial.DOColor(SSColorPalette.GateWrong, 0.3f);
            transform.DOShakePosition(0.5f, 0.4f, 20, 90f, false, true);
            transform.DOShakeRotation(0.5f, 10f, 20, 90f, true);
        }

        public void PlayRevealCorrect()
        {
            KillAllTweens();
            if (_panelMaterial != null)
            {
                var seq = DOTween.Sequence();
                seq.Append(_panelMaterial.DOColor(SSColorPalette.GateCorrect, 0.2f));
                seq.Append(_panelMaterial.DOColor(SSColorPalette.Accent, 0.3f));
                seq.Append(_panelMaterial.DOColor(SSColorPalette.GateCorrect, 0.3f));
                seq.SetLoops(3, LoopType.Restart);
            }
            transform.DOPunchScale(Vector3.one * 0.15f, 0.8f, 4, 0.2f);
        }

        public void PlayFadeOut()
        {
            KillAllTweens();
            if (_panelMaterial != null) _panelMaterial.DOFade(0.3f, 0.5f);
            transform.DOScale(Vector3.one * 0.8f, 0.5f).SetEase(Ease.InBack);
        }

        private void OnDestroy() => KillAllTweens();

        private Shader GetShader()
        {
            return Shader.Find("Universal Render Pipeline/Lit") ??
                   Shader.Find("Standard") ?? Shader.Find("Unlit/Color");
        }
    }
}

```

---

### `ScienceStreet/MCQ/Scripts/McqPlayerRunner.cs`

> Lines: 189 | Size: 6.9KB

```csharp
using System.Collections;
using UnityEngine;
using DG.Tweening;

namespace com.al_arcade.mcq
{
    using shared;

    [RequireComponent(typeof(Rigidbody))]
    public class McqPlayerRunner : MonoBehaviour
    {
        [Header("Movement")]
        [SerializeField] private float laneSwitchSpeed = 8f;
        [SerializeField] private float laneSwitchDuration = 0.25f;

        [Header("Visual")]
        [SerializeField] private float bobAmplitude = 0.08f;
        [SerializeField] private float bobFrequency = 6f;

        private Rigidbody _rb;
        private float _currentSpeed;
        private bool _isRunning;
        private Vector3 _startPosition;
        private float _bobTimer;
        private float _baseY;
        private Renderer _renderer;
        private Material _material;

        private int _currentLane = -1;
        private float[] _lanePositions;

        private void Awake()
        {
            _rb = GetComponent<Rigidbody>();
            _rb.isKinematic = true;
            _rb.useGravity = false;
            _startPosition = transform.position;
            _baseY = transform.position.y;
            _renderer = GetComponentInChildren<Renderer>();
            if (_renderer != null) _material = _renderer.material;
            gameObject.tag = "Player";
        }

        public void SetLanePositions(float[] positions) => _lanePositions = positions;

        public void RunForward(float speed)
        {
            _currentSpeed = speed;
            _isRunning = true;
        }

        public void Stop()
        {
            _currentSpeed = 0;
            _isRunning = false;
        }

        public void ResetToStart()
        {
            Stop();
            DOTween.Kill(transform);
            transform.position = _startPosition;
            transform.rotation = Quaternion.identity;
            transform.localScale = Vector3.one;
            _currentLane = -1;
        }

        public IEnumerator MoveToNextLane(float duration)
        {
            Vector3 target = transform.position + Vector3.forward * 2f;
            DOTween.Kill(transform, "moveNext");
            yield return transform.DOMove(target, duration)
                .SetEase(Ease.InOutQuad).SetId("moveNext")
                .WaitForCompletion();
        }

        private void Update()
        {
            if (!_isRunning) return;

            transform.position += Vector3.forward * (_currentSpeed * Time.deltaTime);

            _bobTimer += Time.deltaTime;
            float bobY = _baseY + Mathf.Sin(_bobTimer * bobFrequency) * bobAmplitude;
            transform.position = new Vector3(transform.position.x, bobY, transform.position.z);

            if (_lanePositions != null && _lanePositions.Length > 0)
                HandleLaneInput();
            else
                HandleFreeMovement();
        }

        private void HandleFreeMovement()
        {
            float moveX = 0f;
            if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A))
                moveX = -laneSwitchSpeed * Time.deltaTime;
            if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D))
                moveX = laneSwitchSpeed * Time.deltaTime;

            if (moveX != 0)
            {
                Vector3 pos = transform.position;
                pos.x = Mathf.Clamp(pos.x + moveX, -6f, 6f);
                transform.position = pos;

                float targetTilt = Mathf.Clamp(-moveX * 300f, -15f, 15f);
                transform.rotation = Quaternion.Slerp(transform.rotation,
                    Quaternion.Euler(0, 0, targetTilt), Time.deltaTime * 10f);
            }
            else
            {
                transform.rotation = Quaternion.Slerp(transform.rotation,
                    Quaternion.identity, Time.deltaTime * 8f);
            }
        }

        private void HandleLaneInput()
        {
            if (Input.GetKeyDown(KeyCode.LeftArrow) || Input.GetKeyDown(KeyCode.A))
                if (_currentLane > 0) MoveToLane(--_currentLane);
            if (Input.GetKeyDown(KeyCode.RightArrow) || Input.GetKeyDown(KeyCode.D))
                if (_currentLane < _lanePositions.Length - 1) MoveToLane(++_currentLane);
        }

        private void MoveToLane(int lane)
        {
            if (_lanePositions == null || lane < 0 || lane >= _lanePositions.Length) return;
            DOTween.Kill(transform, "laneSwitch");
            transform.DOMoveX(_lanePositions[lane], laneSwitchDuration)
                .SetEase(Ease.OutQuad).SetId("laneSwitch");

            var audio = SSAudioManager.Instance;
            if (audio != null) audio.PlayClickBeep();
        }

        public void FlashColor(Color color, float duration = 0.3f)
        {
            if (_material == null) return;
            Color orig = _material.color;
            DOTween.Kill(_material);
            _material.DOColor(color, duration * 0.3f)
                .OnComplete(() => _material.DOColor(orig, duration * 0.7f));
        }

        public void BuildDefaultVisuals()
        {
            var shader = Shader.Find("Universal Render Pipeline/Lit") ??
                         Shader.Find("Standard") ?? Shader.Find("Unlit/Color");

            var body = GameObject.CreatePrimitive(PrimitiveType.Capsule);
            body.name = "Body";
            body.transform.SetParent(transform);
            body.transform.localPosition = Vector3.up * 1f;
            body.transform.localScale = new Vector3(0.6f, 1f, 0.6f);
            Destroy(body.GetComponent<Collider>());
            var bodyMat = new Material(shader) { color = SSColorPalette.Accent };
            body.GetComponent<Renderer>().material = bodyMat;
            _renderer = body.GetComponent<Renderer>();
            _material = bodyMat;

            var head = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            head.name = "Head";
            head.transform.SetParent(transform);
            head.transform.localPosition = Vector3.up * 2.2f;
            head.transform.localScale = Vector3.one * 0.5f;
            Destroy(head.GetComponent<Collider>());
            var headMat = new Material(shader) { color = new Color32(255, 220, 170, 255) };
            head.GetComponent<Renderer>().material = headMat;

            for (int i = -1; i <= 1; i += 2)
            {
                var eye = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                eye.name = "Eye";
                eye.transform.SetParent(head.transform);
                eye.transform.localPosition = new Vector3(i * 0.15f, 0.05f, -0.4f);
                eye.transform.localScale = Vector3.one * 0.2f;
                Destroy(eye.GetComponent<Collider>());
                eye.GetComponent<Renderer>().material = new Material(shader) { color = Color.black };
            }

            var col = gameObject.AddComponent<CapsuleCollider>();
            col.center = Vector3.up * 1.2f;
            col.radius = 0.4f;
            col.height = 2.8f;
            col.isTrigger = true;
        }
    }
}

```

---

### `ScienceStreet/MCQ/Scripts/McqQuestionDisplay.cs`

> Lines: 124 | Size: 5.1KB

```csharp
using UnityEngine;
using DG.Tweening;
using ALArcade.ArabicTMP;

namespace com.al_arcade.mcq
{
    using shared;

    public class McqQuestionDisplay : MonoBehaviour
    {
        [Header("Settings")]
        [SerializeField] private float floatHeight = 6f;
        [SerializeField] private float followSpeed = 5f;
        [SerializeField] private float forwardOffset = 5f;

        private ArabicTextMeshPro _questionText;
        private ArabicTextMeshPro _sourceText;
        private GameObject _panel;
        private Transform _playerTransform;
        private bool _visible;

        public void Build(Transform playerTarget = null)
        {
            _playerTransform = playerTarget;
            var shader = Shader.Find("Universal Render Pipeline/Lit") ??
                         Shader.Find("Standard") ?? Shader.Find("Unlit/Color");

            // Background panel (Cube)
            _panel = GameObject.CreatePrimitive(PrimitiveType.Cube);
            _panel.name = "QuestionPanel";
            _panel.transform.SetParent(transform);
            _panel.transform.localPosition = Vector3.zero;
            _panel.transform.localScale = new Vector3(12f, 2.5f, 0.1f);
            Destroy(_panel.GetComponent<Collider>());
            var panelMat = new Material(shader)
            { color = new Color(0.12f, 0.12f, 0.5f, 0.95f) };
            _panel.GetComponent<Renderer>().material = panelMat;

            // Accent bar
            var bar = GameObject.CreatePrimitive(PrimitiveType.Cube);
            bar.name = "AccentBar";
            bar.transform.SetParent(_panel.transform);
            bar.transform.localPosition = new Vector3(0, -0.52f, -0.01f);
            bar.transform.localScale = new Vector3(1.01f, 0.04f, 1.2f);
            Destroy(bar.GetComponent<Collider>());
            bar.GetComponent<Renderer>().material = new Material(shader)
            { color = SSColorPalette.Accent };

            // ★ Question text — identity local rotation
            //   Parent will billboard to face camera
            var textObj = new GameObject("QuestionText");
            textObj.transform.SetParent(transform);
            textObj.transform.localPosition = new Vector3(0, 0.2f, -0.06f);
            textObj.transform.localRotation = Quaternion.identity;

            _questionText = textObj.AddComponent<ArabicTextMeshPro>();
            _questionText.alignment = TMPro.TextAlignmentOptions.Center;
            _questionText.color = Color.white;
            _questionText.fontStyle = TMPro.FontStyles.Bold;
            _questionText.enableAutoSizing = true;
            _questionText.fontSizeMin = 2f;
            _questionText.fontSizeMax = 5f;
            _questionText.rectTransform.sizeDelta = new Vector2(11f, 1.8f);
            _questionText.enableWordWrapping = true;
            _questionText.overflowMode = TMPro.TextOverflowModes.Ellipsis;
            SSFontManager.Apply(_questionText);

            // Source text
            var srcObj = new GameObject("SourceText");
            srcObj.transform.SetParent(transform);
            srcObj.transform.localPosition = new Vector3(0, -0.85f, -0.06f);
            srcObj.transform.localRotation = Quaternion.identity;

            _sourceText = srcObj.AddComponent<ArabicTextMeshPro>();
            _sourceText.alignment = TMPro.TextAlignmentOptions.Center;
            _sourceText.color = SSColorPalette.WithAlpha(Color.white, 0.5f);
            _sourceText.fontSize = 2f;
            _sourceText.rectTransform.sizeDelta = new Vector2(11f, 0.5f);
            SSFontManager.Apply(_sourceText);

            transform.localScale = Vector3.zero;
            _visible = false;
        }

        public void Show(string question, string source = "")
        {
            if (_questionText != null) _questionText.arabicText = question ?? "";
            if (_sourceText != null)
                _sourceText.arabicText = !string.IsNullOrEmpty(source) ? $"📖 {source}" : "";

            _visible = true;
            DOTween.Kill(transform, "displayScale");
            transform.DOScale(Vector3.one, 0.5f).SetEase(Ease.OutBack).SetId("displayScale");
        }

        public void Hide()
        {
            _visible = false;
            DOTween.Kill(transform, "displayScale");
            transform.DOScale(Vector3.zero, 0.3f).SetEase(Ease.InBack).SetId("displayScale");
        }

        private void LateUpdate()
        {
            if (_playerTransform == null) return;

            // ★ Follow player, stay between player and gates
            Vector3 targetPos = _playerTransform.position
                                + Vector3.up * floatHeight
                                + Vector3.forward * forwardOffset;
            transform.position = Vector3.Lerp(transform.position, targetPos,
                Time.deltaTime * followSpeed);

            // ★ Billboard: face away from camera so text (-Z face) faces camera
            if (Camera.main != null)
            {
                Vector3 awayFromCam = transform.position - Camera.main.transform.position;
                awayFromCam.y = 0;
                if (awayFromCam.sqrMagnitude > 0.001f)
                    transform.rotation = Quaternion.LookRotation(awayFromCam);
            }
        }
    }
}

```

---

### `ScienceStreet/MCQ/Scripts/McqUIManager.cs`

> Lines: 425 | Size: 18.4KB

```csharp
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using DG.Tweening;
using ALArcade.ArabicTMP;

namespace com.al_arcade.mcq
{
    using shared;

    public class McqUIManager : MonoBehaviour
    {
        private Canvas _canvas;
        private CanvasGroup _gameUI, _loadingUI, _errorUI, _resultsUI, _feedbackUI;

        private ArabicTextMeshProUGUI _scoreText, _streakText, _progressText;
        private ArabicTextMeshProUGUI _loadingText, _errorText, _feedbackText;
        private Image _feedbackBg;
        private Image[] _heartIcons;

        private ArabicTextMeshProUGUI _resultTitle, _resultScore;
        private ArabicTextMeshProUGUI _resultCorrect, _resultWrong, _resultStreak;
        private Button _resultRestartBtn;
        private Image _progressFill;

        [Header("Events")]
        public UnityEvent onRestartClicked;

        // ════════════════════════════════════════════════════════════
        // BUILD
        // ════════════════════════════════════════════════════════════

        public void BuildUI()
        {
            var canvasObj = new GameObject("McqCanvas");
            canvasObj.transform.SetParent(transform);
            _canvas = canvasObj.AddComponent<Canvas>();
            _canvas.renderMode = RenderMode.ScreenSpaceOverlay;
            _canvas.sortingOrder = 100;

            var scaler = canvasObj.AddComponent<CanvasScaler>();
            scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
            scaler.referenceResolution = new Vector2(1920, 1080);
            scaler.matchWidthOrHeight = 0.5f;
            canvasObj.AddComponent<GraphicRaycaster>();

            BuildGameUI(canvasObj.transform);
            BuildFeedbackUI(canvasObj.transform);
            BuildLoadingUI(canvasObj.transform);
            BuildErrorUI(canvasObj.transform);
            BuildResultsUI(canvasObj.transform);

            _gameUI.alpha = 0; _gameUI.gameObject.SetActive(false);
            _loadingUI.alpha = 0; _loadingUI.gameObject.SetActive(false);
            _errorUI.alpha = 0; _errorUI.gameObject.SetActive(false);
            _resultsUI.alpha = 0; _resultsUI.gameObject.SetActive(false);
            _feedbackUI.alpha = 0;
        }

        private void BuildGameUI(Transform parent)
        {
            var go = MakePanel(parent, "GameUI", Vector2.zero, Vector2.one);
            _gameUI = go.AddComponent<CanvasGroup>();

            var topBar = MakeImg(go.transform, "TopBar",
                new Vector2(0, 1), new Vector2(1, 1),
                SSColorPalette.WithAlpha(SSColorPalette.PrimaryDark, 0.85f));
            topBar.rectTransform.pivot = new Vector2(0.5f, 1);
            topBar.rectTransform.sizeDelta = new Vector2(0, 80);

            _scoreText = MakeTxt(topBar.transform, "Score", "0",
                new Vector2(30, -15), 36, TMPro.TextAlignmentOptions.TopLeft);
            _scoreText.color = SSColorPalette.Accent;
            _scoreText.fontStyle = TMPro.FontStyles.Bold;

            MakeTxt(topBar.transform, "ScoreLabel", "النقاط",
                new Vector2(30, -50), 16, TMPro.TextAlignmentOptions.TopLeft)
                .color = SSColorPalette.WithAlpha(Color.white, 0.6f);

            _streakText = MakeTxt(topBar.transform, "Streak", "",
                new Vector2(200, -15), 24, TMPro.TextAlignmentOptions.TopLeft);
            _streakText.color = SSColorPalette.Warning;

            // Hearts
            _heartIcons = new Image[5];
            for (int i = 0; i < 5; i++)
            {
                var h = new GameObject($"Heart_{i}");
                h.transform.SetParent(topBar.transform, false);
                var r = h.AddComponent<RectTransform>();
                r.anchorMin = r.anchorMax = new Vector2(1, 0.5f);
                r.pivot = new Vector2(1, 0.5f);
                r.anchoredPosition = new Vector2(-30 - i * 45, 0);
                r.sizeDelta = new Vector2(36, 36);
                _heartIcons[i] = h.AddComponent<Image>();
                _heartIcons[i].color = SSColorPalette.Danger;
                h.SetActive(false);
            }

            // Progress bar
            var progBg = MakeImg(go.transform, "ProgressBg",
                new Vector2(0, 0), new Vector2(1, 0),
                SSColorPalette.WithAlpha(Color.white, 0.1f));
            progBg.rectTransform.anchoredPosition = new Vector2(0, 15);
            progBg.rectTransform.sizeDelta = new Vector2(-80, 8);
            progBg.rectTransform.pivot = new Vector2(0.5f, 0);

            var fillObj = new GameObject("ProgressFill");
            fillObj.transform.SetParent(progBg.transform, false);
            var fr = fillObj.AddComponent<RectTransform>();
            fr.anchorMin = Vector2.zero;
            fr.anchorMax = new Vector2(0, 1);
            fr.pivot = new Vector2(0, 0.5f);
            fr.offsetMin = fr.offsetMax = Vector2.zero;
            _progressFill = fillObj.AddComponent<Image>();
            _progressFill.color = SSColorPalette.Accent;

            _progressText = MakeTxt(go.transform, "ProgressText", "0 / 0",
                new Vector2(0, 35), 16, TMPro.TextAlignmentOptions.Bottom);
            _progressText.rectTransform.anchorMin = _progressText.rectTransform.anchorMax =
                new Vector2(0.5f, 0);
            _progressText.color = SSColorPalette.WithAlpha(Color.white, 0.5f);
        }

        private void BuildFeedbackUI(Transform parent)
        {
            var go = MakePanel(parent, "FeedbackUI", Vector2.zero, Vector2.one);
            _feedbackUI = go.AddComponent<CanvasGroup>();
            _feedbackUI.blocksRaycasts = false;
            _feedbackUI.interactable = false;

            var bgObj = new GameObject("FeedbackBg");
            bgObj.transform.SetParent(go.transform, false);
            var bgRect = bgObj.AddComponent<RectTransform>();
            bgRect.anchorMin = new Vector2(0.2f, 0.4f);
            bgRect.anchorMax = new Vector2(0.8f, 0.6f);
            bgRect.offsetMin = bgRect.offsetMax = Vector2.zero;
            _feedbackBg = bgObj.AddComponent<Image>();
            _feedbackBg.color = SSColorPalette.WithAlpha(SSColorPalette.Success, 0.9f);

            _feedbackText = MakeTxt(bgObj.transform, "FeedbackText", "",
                Vector2.zero, 36, TMPro.TextAlignmentOptions.Center);
            _feedbackText.rectTransform.anchorMin = Vector2.zero;
            _feedbackText.rectTransform.anchorMax = Vector2.one;
            _feedbackText.rectTransform.sizeDelta = Vector2.zero;
            _feedbackText.color = Color.white;
            _feedbackText.fontStyle = TMPro.FontStyles.Bold;
        }

        private void BuildLoadingUI(Transform parent)
        {
            var go = MakePanel(parent, "LoadingUI", Vector2.zero, Vector2.one);
            _loadingUI = go.AddComponent<CanvasGroup>();
            go.AddComponent<Image>().color =
                SSColorPalette.WithAlpha(SSColorPalette.PrimaryDark, 0.9f);

            _loadingText = MakeTxt(go.transform, "LoadingText", "جاري التحميل...",
                Vector2.zero, 28, TMPro.TextAlignmentOptions.Center);
            _loadingText.rectTransform.anchorMin = new Vector2(0.2f, 0.4f);
            _loadingText.rectTransform.anchorMax = new Vector2(0.8f, 0.6f);
            _loadingText.rectTransform.sizeDelta = Vector2.zero;
            _loadingText.color = Color.white;
        }

        private void BuildErrorUI(Transform parent)
        {
            var go = MakePanel(parent, "ErrorUI", Vector2.zero, Vector2.one);
            _errorUI = go.AddComponent<CanvasGroup>();
            go.AddComponent<Image>().color =
                SSColorPalette.WithAlpha(SSColorPalette.Danger, 0.92f);

            _errorText = MakeTxt(go.transform, "ErrorText", "خطأ",
                Vector2.zero, 24, TMPro.TextAlignmentOptions.Center);
            _errorText.rectTransform.anchorMin = new Vector2(0.15f, 0.35f);
            _errorText.rectTransform.anchorMax = new Vector2(0.85f, 0.65f);
            _errorText.rectTransform.sizeDelta = Vector2.zero;
            _errorText.color = Color.white;
            _errorText.enableWordWrapping = true;
        }

        private void BuildResultsUI(Transform parent)
        {
            var go = MakePanel(parent, "ResultsUI", Vector2.zero, Vector2.one);
            _resultsUI = go.AddComponent<CanvasGroup>();
            go.AddComponent<Image>().color =
                SSColorPalette.WithAlpha(SSColorPalette.PrimaryDark, 0.95f);

            _resultTitle = MakeAnchoredTxt(go.transform, "Title", "أحسنت! 🎉",
                new Vector2(0, 200), 48);
            _resultTitle.color = SSColorPalette.Accent;
            _resultTitle.fontStyle = TMPro.FontStyles.Bold;

            _resultScore = MakeAnchoredTxt(go.transform, "Score", "0",
                new Vector2(0, 120), 64);
            _resultScore.color = Color.white;
            _resultScore.fontStyle = TMPro.FontStyles.Bold;

            _resultCorrect = MakeAnchoredTxt(go.transform, "Correct", "",
                new Vector2(-200, 0), 24);
            _resultCorrect.color = SSColorPalette.Success;

            _resultWrong = MakeAnchoredTxt(go.transform, "Wrong", "",
                new Vector2(0, 0), 24);
            _resultWrong.color = SSColorPalette.Danger;

            _resultStreak = MakeAnchoredTxt(go.transform, "Streak", "",
                new Vector2(200, 0), 24);
            _resultStreak.color = SSColorPalette.Accent;

            // Restart button
            var btnObj = new GameObject("RestartBtn");
            btnObj.transform.SetParent(go.transform, false);
            var br = btnObj.AddComponent<RectTransform>();
            br.anchorMin = br.anchorMax = new Vector2(0.5f, 0.5f);
            br.anchoredPosition = new Vector2(0, -120);
            br.sizeDelta = new Vector2(300, 60);
            btnObj.AddComponent<Image>().color = SSColorPalette.Accent;
            _resultRestartBtn = btnObj.AddComponent<Button>();
            _resultRestartBtn.onClick.AddListener(() =>
            { HideResults(); onRestartClicked?.Invoke(); });

            var btnTxt = MakeTxt(btnObj.transform, "BtnText", "إعادة اللعب 🔄",
                Vector2.zero, 24, TMPro.TextAlignmentOptions.Center);
            btnTxt.rectTransform.anchorMin = Vector2.zero;
            btnTxt.rectTransform.anchorMax = Vector2.one;
            btnTxt.rectTransform.sizeDelta = Vector2.zero;
            btnTxt.color = SSColorPalette.TextDark;
            btnTxt.fontStyle = TMPro.FontStyles.Bold;
        }

        // ══════════════════════════════════════════════════════════
        // PUBLIC
        // ══════════════════════════════════════════════════════════

        public void ShowGameUI()
        {
            _gameUI.gameObject.SetActive(true);
            _gameUI.DOFade(1f, 0.3f);
        }

        public void SetScore(int score)
        {
            if (_scoreText == null) return;
            _scoreText.arabicText = 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.arabicText = 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.arabicText = $"{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");
            }
        }

        public void ShowFeedback(string message, bool isCorrect)
        {
            if (_feedbackText != null) _feedbackText.arabicText = 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)
            {
                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.arabicText = 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.arabicText = 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.arabicText = won ? "أحسنت! 🎉" : "حظ أوفر! 😔";
            if (_resultScore != null) _resultScore.arabicText = score.ToString("N0");
            if (_resultCorrect != null) _resultCorrect.arabicText = $"✓ صحيح: {correct}";
            if (_resultWrong != null) _resultWrong.arabicText = $"✗ خطأ: {wrong}";
            if (_resultStreak != null) _resultStreak.arabicText = $"🔥 أعلى سلسلة: {bestStreak}";

            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 (_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 ResetUI()
        {
            _gameUI.gameObject.SetActive(false);
            _loadingUI.gameObject.SetActive(false);
            _errorUI.gameObject.SetActive(false);
            _resultsUI.gameObject.SetActive(false);
            _feedbackUI.alpha = 0;
        }

        // ── Helpers ──────────────────────────────────────────────────

        private GameObject MakePanel(Transform p, string n, Vector2 amin, Vector2 amax)
        {
            var go = new GameObject(n);
            go.transform.SetParent(p, false);
            var r = go.AddComponent<RectTransform>();
            r.anchorMin = amin; r.anchorMax = amax;
            r.sizeDelta = Vector2.zero; r.anchoredPosition = Vector2.zero;
            return go;
        }

        private Image MakeImg(Transform p, string n, Vector2 amin, Vector2 amax, Color c)
        {
            var go = new GameObject(n);
            go.transform.SetParent(p, false);
            var r = go.AddComponent<RectTransform>();
            r.anchorMin = amin; r.anchorMax = amax; r.sizeDelta = Vector2.zero;
            var img = go.AddComponent<Image>();
            img.color = c;
            return img;
        }

        private ArabicTextMeshProUGUI MakeTxt(Transform p, string n, string text,
            Vector2 pos, float size, TMPro.TextAlignmentOptions align)
        {
            var go = new GameObject(n);
            go.transform.SetParent(p, false);
            var r = go.AddComponent<RectTransform>();
            r.anchoredPosition = pos; r.sizeDelta = new Vector2(400, 60);
            var tmp = go.AddComponent<ArabicTextMeshProUGUI>();
            tmp.arabicText = text; tmp.fontSize = size; tmp.alignment = align;
            tmp.enableWordWrapping = true;
            SSFontManager.Apply(tmp);
            return tmp;
        }

        private ArabicTextMeshProUGUI MakeAnchoredTxt(Transform p, string n, string text,
            Vector2 pos, float size)
        {
            var t = MakeTxt(p, n, text, pos, size, TMPro.TextAlignmentOptions.Center);
            t.rectTransform.anchorMin = t.rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
            return t;
        }
    }
}

```

---

### `ScienceStreet/Shared/Scripts/SSApiManager.cs`

> Lines: 260 | Size: 11.2KB

```csharp
// ============================================================================
//  Science Street — API Manager (Singleton)
//  Handles all communication with the PHP backend
// ============================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using Newtonsoft.Json;

namespace com.al_arcade.shared
{
    public class SSApiManager : MonoBehaviour
    {
        public static SSApiManager Instance { get; private set; }

        [Header("API Configuration")]
        [SerializeField] private string baseUrl =
            "https://phpserver.caprover.al-arcade.com/ssbook/api.php";

        [SerializeField] private int timeoutSeconds = 15;

        // ── Singleton Setup ──────────────────────────────────────────
        private void Awake()
        {
            if (Instance != null && Instance != this)
            {
                Destroy(gameObject);
                return;
            }
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }

        // ── Ensure Instance Exists ───────────────────────────────────
        public static SSApiManager EnsureInstance()
        {
            if (Instance != null) return Instance;
            var go = new GameObject("[SSApiManager]");
            Instance = go.AddComponent<SSApiManager>();
            DontDestroyOnLoad(go);
            return Instance;
        }

        // ════════════════════════════════════════════════════════════
        // GENERIC REQUEST
        // ════════════════════════════════════════════════════════════

        public IEnumerator GetRequest(string action,
            Dictionary<string, string> parameters,
            Action<string> onSuccess,
            Action<string> onError)
        {
            string url = baseUrl + "?action=" + UnityWebRequest.EscapeURL(action);
            if (parameters != null)
            {
                foreach (var kv in parameters)
                {
                    url += "&" + UnityWebRequest.EscapeURL(kv.Key) +
                           "=" + UnityWebRequest.EscapeURL(kv.Value);
                }
            }

            using var req = UnityWebRequest.Get(url);
            req.timeout = timeoutSeconds;

            yield return req.SendWebRequest();

            if (req.result != UnityWebRequest.Result.Success)
            {
                string errorMsg = req.error ?? "Network error";
                Debug.LogWarning($"[SSApi] Request failed: {errorMsg}");
                onError?.Invoke(errorMsg);
                yield break;
            }

            string json = req.downloadHandler.text;
            onSuccess?.Invoke(json);
        }

        // ════════════════════════════════════════════════════════════
        // PING
        // ════════════════════════════════════════════════════════════

        public IEnumerator Ping(Action<bool> onResult)
        {
            yield return GetRequest("ping", null,
                json => onResult?.Invoke(true),
                err => onResult?.Invoke(false));
        }

        // ════════════════════════════════════════════════════════════
        // VALIDATE CLASS CODE
        // ════════════════════════════════════════════════════════════

        public IEnumerator ValidateClassCode(string code,
            Action<ClassInfo> onSuccess,
            Action<string> onError)
        {
            var p = new Dictionary<string, string> { { "class_code", code } };

            yield return GetRequest("validate_class", p,
                json =>
                {
                    try
                    {
                        var resp = JsonConvert.DeserializeObject<ValidateClassResponse>(json);
                        if (resp.success && resp.classInfo != null)
                            onSuccess?.Invoke(resp.classInfo);
                        else
                            onError?.Invoke(resp.error ?? "Validation failed");
                    }
                    catch (Exception e)
                    {
                        Debug.LogError($"[SSApi] Parse error: {e.Message}");
                        onError?.Invoke("Failed to parse server response");
                    }
                },
                onError);
        }

        // ════════════════════════════════════════════════════════════
        // GET GRADES
        // ════════════════════════════════════════════════════════════

        public IEnumerator GetGrades(Action<GradeInfo[]> onSuccess,
            Action<string> onError)
        {
            yield return GetRequest("get_grades", null,
                json =>
                {
                    try
                    {
                        var resp = JsonConvert.DeserializeObject<GradesResponse>(json);
                        if (resp.success)
                            onSuccess?.Invoke(resp.grades ?? Array.Empty<GradeInfo>());
                        else
                            onError?.Invoke(resp.error ?? "Failed to load grades");
                    }
                    catch (Exception e)
                    {
                        onError?.Invoke("Parse error: " + e.Message);
                    }
                },
                onError);
        }

        // ════════════════════════════════════════════════════════════
        // FETCH MCQ QUESTIONS
        // ════════════════════════════════════════════════════════════

        public IEnumerator FetchMcq(string buildType, string classCode,
            int count, int gradeId,
            Action<McqQuestion[]> onSuccess,
            Action<string> onError)
        {
            var p = new Dictionary<string, string>
            {
                { "build_type", buildType },
                { "count", count.ToString() }
            };
            if (gradeId > 0) p["grade_id"] = gradeId.ToString();
            if (buildType == "teacher" && !string.IsNullOrEmpty(classCode))
                p["class_code"] = classCode;

            yield return GetRequest("get_mcq", p,
                json =>
                {
                    try
                    {
                        var resp = JsonConvert.DeserializeObject<ApiResponse<McqQuestion>>(json);
                        if (resp.success && resp.questions != null)
                            onSuccess?.Invoke(resp.questions);
                        else
                            onError?.Invoke(resp.error ?? "No questions returned");
                    }
                    catch (Exception e)
                    {
                        onError?.Invoke("Parse error: " + e.Message);
                    }
                },
                onError);
        }

        // ════════════════════════════════════════════════════════════
        // FETCH T/F QUESTIONS
        // ════════════════════════════════════════════════════════════

        public IEnumerator FetchTf(string buildType, string classCode,
            int count, int gradeId,
            Action<TfQuestion[]> onSuccess,
            Action<string> onError)
        {
            var p = new Dictionary<string, string>
            {
                { "build_type", buildType },
                { "count", count.ToString() }
            };
            if (gradeId > 0) p["grade_id"] = gradeId.ToString();
            if (buildType == "teacher" && !string.IsNullOrEmpty(classCode))
                p["class_code"] = classCode;

            yield return GetRequest("get_tf", p,
                json =>
                {
                    try
                    {
                        var resp = JsonConvert.DeserializeObject<ApiResponse<TfQuestion>>(json);
                        if (resp.success && resp.questions != null)
                            onSuccess?.Invoke(resp.questions);
                        else
                            onError?.Invoke(resp.error ?? "No questions returned");
                    }
                    catch (Exception e)
                    {
                        onError?.Invoke("Parse error: " + e.Message);
                    }
                },
                onError);
        }

        // ════════════════════════════════════════════════════════════
        // FETCH CS QUESTIONS
        // ════════════════════════════════════════════════════════════

        public IEnumerator FetchCs(string buildType, string classCode,
            int count, int gradeId,
            Action<CsQuestion[]> onSuccess,
            Action<string> onError)
        {
            var p = new Dictionary<string, string>
            {
                { "build_type", buildType },
                { "count", count.ToString() }
            };
            if (gradeId > 0) p["grade_id"] = gradeId.ToString();
            if (buildType == "teacher" && !string.IsNullOrEmpty(classCode))
                p["class_code"] = classCode;

            yield return GetRequest("get_cs", p,
                json =>
                {
                    try
                    {
                        var resp = JsonConvert.DeserializeObject<ApiResponse<CsQuestion>>(json);
                        if (resp.success && resp.questions != null)
                            onSuccess?.Invoke(resp.questions);
                        else
                            onError?.Invoke(resp.error ?? "No questions returned");
                    }
                    catch (Exception e)
                    {
                        onError?.Invoke("Parse error: " + e.Message);
                    }
                },
                onError);
        }
    }
}

```

---

### `ScienceStreet/Shared/Scripts/SSAudioManager.cs`

> Lines: 155 | Size: 5.8KB

```csharp
// ============================================================================
//  Science Street — Audio Manager
//  Plays SFX with optional AudioClip references.
//  Works silently if no clips are assigned.
// ============================================================================
using UnityEngine;
using System.Collections.Generic;

namespace com.al_arcade.shared
{
    public class SSAudioManager : MonoBehaviour
    {
        public static SSAudioManager Instance { get; private set; }

        [Header("Sound Effects (all optional)")]
        public AudioClip sfxCorrect;
        public AudioClip sfxWrong;
        public AudioClip sfxClick;
        public AudioClip sfxWhoosh;
        public AudioClip sfxPop;
        public AudioClip sfxVictory;
        public AudioClip sfxDefeat;
        public AudioClip sfxTick;
        public AudioClip sfxSlam;
        public AudioClip sfxDing;
        public AudioClip sfxBuzz;
        public AudioClip sfxSwipe;
        public AudioClip sfxExplosion;
        public AudioClip sfxCheer;
        public AudioClip sfxCountdown;

        [Header("Settings")]
        [Range(0f, 1f)] public float masterVolume = 1f;
        public int audioSourcePoolSize = 8;

        private List<AudioSource> _pool = new List<AudioSource>();
        private int _poolIndex = 0;

        private void Awake()
        {
            if (Instance != null && Instance != this)
            {
                Destroy(gameObject);
                return;
            }
            Instance = this;
            DontDestroyOnLoad(gameObject);
            InitPool();
        }

        public static SSAudioManager EnsureInstance()
        {
            if (Instance != null) return Instance;
            var go = new GameObject("[SSAudioManager]");
            Instance = go.AddComponent<SSAudioManager>();
            DontDestroyOnLoad(go);
            Instance.InitPool();
            return Instance;
        }

        private void InitPool()
        {
            if (_pool.Count > 0) return;
            for (int i = 0; i < audioSourcePoolSize; i++)
            {
                var src = gameObject.AddComponent<AudioSource>();
                src.playOnAwake = false;
                _pool.Add(src);
            }
        }

        private AudioSource GetSource()
        {
            if (_pool.Count == 0) InitPool();
            var src = _pool[_poolIndex % _pool.Count];
            _poolIndex++;
            return src;
        }

        // ── Play Methods ─────────────────────────────────────────────

        public void Play(AudioClip clip, float volumeScale = 1f, float pitch = 1f)
        {
            if (clip == null) return;
            var src = GetSource();
            src.pitch = pitch;
            src.volume = masterVolume * volumeScale;
            src.PlayOneShot(clip);
        }

        public void PlayCorrect(float vol = 1f)    => Play(sfxCorrect, vol);
        public void PlayWrong(float vol = 1f)       => Play(sfxWrong, vol);
        public void PlayClick(float vol = 0.7f)     => Play(sfxClick, vol);
        public void PlayWhoosh(float vol = 0.8f)    => Play(sfxWhoosh, vol);
        public void PlayPop(float vol = 0.8f)       => Play(sfxPop, vol);
        public void PlayVictory(float vol = 1f)     => Play(sfxVictory, vol);
        public void PlayDefeat(float vol = 1f)      => Play(sfxDefeat, vol);
        public void PlayTick(float vol = 0.5f)      => Play(sfxTick, vol);
        public void PlaySlam(float vol = 1f)        => Play(sfxSlam, vol);
        public void PlayDing(float vol = 0.8f)      => Play(sfxDing, vol);
        public void PlayBuzz(float vol = 0.8f)      => Play(sfxBuzz, vol);
        public void PlaySwipe(float vol = 0.7f)     => Play(sfxSwipe, vol);
        public void PlayExplosion(float vol = 1f)   => Play(sfxExplosion, vol);
        public void PlayCheer(float vol = 1f)       => Play(sfxCheer, vol);
        public void PlayCountdown(float vol = 0.8f) => Play(sfxCountdown, vol);

        /// <summary>
        /// Plays a generated procedural beep (no AudioClip needed).
        /// Useful for demo/prototype mode.
        /// </summary>
        public void PlayProceduralBeep(float frequency = 440f, float duration = 0.1f,
            float volume = 0.3f)
        {
            int sampleRate = AudioSettings.outputSampleRate;
            int sampleCount = Mathf.CeilToInt(sampleRate * duration);
            var clip = AudioClip.Create("beep", sampleCount, 1, sampleRate, false);
            float[] data = new float[sampleCount];

            for (int i = 0; i < sampleCount; i++)
            {
                float t = (float)i / sampleRate;
                float envelope = 1f - ((float)i / sampleCount); // fade out
                data[i] = Mathf.Sin(2f * Mathf.PI * frequency * t) * envelope * volume;
            }

            clip.SetData(data, 0);
            var src = GetSource();
            src.pitch = 1f;
            src.volume = masterVolume;
            src.PlayOneShot(clip);
        }

        public void PlayCorrectBeep()  => PlayProceduralBeep(880f, 0.15f, 0.25f);
        public void PlayWrongBeep()     => PlayProceduralBeep(220f, 0.3f, 0.25f);
        public void PlayClickBeep()     => PlayProceduralBeep(660f, 0.05f, 0.15f);
        public void PlaySuccessJingle()
        {
            // Three ascending tones
            PlayProceduralBeep(523f, 0.12f, 0.2f); // C5
            StartCoroutine(DelayedBeep(660f, 0.12f, 0.1f));  // E5
            StartCoroutine(DelayedBeep(784f, 0.2f, 0.2f));   // G5
        }

        public void PlayFailBuzz()
        {
            PlayProceduralBeep(150f, 0.4f, 0.3f);
        }

        private System.Collections.IEnumerator DelayedBeep(float freq, float dur, float delay)
        {
            yield return new WaitForSeconds(delay);
            PlayProceduralBeep(freq, dur, 0.2f);
        }
    }
}

```

---

### `ScienceStreet/Shared/Scripts/SSColorPalette.cs`

> Lines: 58 | Size: 3.7KB

```csharp
// ============================================================================
//  Science Street — Shared Color Palette
//  AL-Arcade Game Dev Team
// ============================================================================
using UnityEngine;

namespace com.al_arcade.shared
{
    /// <summary>
    /// Central color definitions matching the Science Street brand.
    /// </summary>
    public static class SSColorPalette
    {
        // ── Brand Colors ─────────────────────────────────────────────
        public static readonly Color Primary      = new Color32(48, 48, 208, 255);    // #3030D0
        public static readonly Color PrimaryDark  = new Color32(32, 32, 168, 255);    // #2020A8
        public static readonly Color PrimaryLight = new Color32(80, 80, 220, 255);    // #5050DC
        public static readonly Color Accent       = new Color32(254, 215, 0, 255);    // #FED700
        public static readonly Color AccentDark   = new Color32(212, 180, 0, 255);    // #D4B400

        // ── Feedback Colors ──────────────────────────────────────────
        public static readonly Color Success      = new Color32(16, 185, 129, 255);   // #10B981
        public static readonly Color SuccessLight = new Color32(209, 250, 229, 255);  // #D1FAE5
        public static readonly Color Danger       = new Color32(239, 68, 68, 255);    // #EF4444
        public static readonly Color DangerLight  = new Color32(254, 226, 226, 255);  // #FEE2E2
        public static readonly Color Warning      = new Color32(245, 158, 11, 255);   // #F59E0B
        public static readonly Color Info         = new Color32(59, 130, 246, 255);   // #3B82F6

        // ── UI Colors ────────────────────────────────────────────────
        public static readonly Color Background   = new Color32(238, 240, 248, 255);  // #EEF0F8
        public static readonly Color Card         = Color.white;
        public static readonly Color TextDark     = new Color32(26, 26, 46, 255);     // #1A1A2E
        public static readonly Color TextMuted    = new Color32(107, 114, 128, 255);  // #6B7280
        public static readonly Color Border       = new Color32(229, 231, 235, 255);  // #E5E7EB

        // ── Game-specific ────────────────────────────────────────────
        public static readonly Color GateCorrect  = new Color32(16, 185, 129, 255);
        public static readonly Color GateWrong    = new Color32(239, 68, 68, 255);
        public static readonly Color GateDefault  = new Color32(80, 80, 220, 255);
        public static readonly Color TrueGreen    = new Color32(34, 197, 94, 255);    // #22C55E
        public static readonly Color FalseRed     = new Color32(239, 68, 68, 255);    // #EF4444
        public static readonly Color WrongWord    = new Color32(239, 68, 68, 255);
        public static readonly Color CorrectWord  = new Color32(16, 185, 129, 255);
        public static readonly Color NeutralWord  = new Color32(248, 249, 252, 255);

        // ── Helpers ──────────────────────────────────────────────────
        public static Color WithAlpha(Color c, float a)
        {
            return new Color(c.r, c.g, c.b, a);
        }

        public static Color Lerp3(Color a, Color b, Color c, float t)
        {
            if (t < 0.5f) return Color.Lerp(a, b, t * 2f);
            return Color.Lerp(b, c, (t - 0.5f) * 2f);
        }
    }
}

```

---

### `ScienceStreet/Shared/Scripts/SSDataModels.cs`

> Lines: 136 | Size: 3.8KB

```csharp
// ============================================================================
//  Science Street — Shared Data Models
//  JSON deserialization targets for API responses
// ============================================================================
using System;
using UnityEngine;
using Newtonsoft.Json;

namespace com.al_arcade.shared
{
    // ── API Wrapper Responses ────────────────────────────────────────

    [Serializable]
    public class ApiResponse<T>
    {
        public bool success;
        public string error;
        public int count;
        public T[] questions;
    }

    [Serializable]
    public class GradesResponse
    {
        public bool success;
        public string error;
        public GradeInfo[] grades;
    }

    [Serializable]
    public class ValidateClassResponse
    {
        public bool success;
        public string error;

        [JsonProperty("class")]
        public ClassInfo classInfo;
    }

    // ── Core Data Types ──────────────────────────────────────────────

    [Serializable]
    public class GradeInfo
    {
        public string id;
        public string name;
    }

    [Serializable]
    public class ClassInfo
    {
        public string id;
        public string class_name;
        public string class_code;
        public string is_active;
        public string grade_id;
        public string grade_name;
        public string teacher_name;
    }

    // ── MCQ ──────────────────────────────────────────────────────────

    [Serializable]
    public class McqQuestion
    {
        public string id;
        public string question_text;
        public string answer1; // always correct
        public string answer2;
        public string answer3;
        public string answer4;
        public string source;
        public string grade_name;

        /// <summary>
        /// Returns all 4 answers shuffled. Sets correctIndex to the
        /// position of the correct answer in the shuffled array.
        /// </summary>
        public string[] GetShuffledAnswers(out int correctIndex)
        {
            string[] arr = { answer1, answer2, answer3, answer4 };
            string correct = answer1;

            // Fisher-Yates shuffle
            for (int i = arr.Length - 1; i > 0; i--)
            {
                int j = UnityEngine.Random.Range(0, i + 1);
                (arr[i], arr[j]) = (arr[j], arr[i]);
            }

            correctIndex = Array.IndexOf(arr, correct);
            return arr;
        }
    }

    // ── True/False ───────────────────────────────────────────────────

    [Serializable]
    public class TfQuestion
    {
        public string id;
        public string question_text;
        public bool is_true;
        public string source;
        public string grade_name;
    }

    // ── Correct Sentence ─────────────────────────────────────────────

    [Serializable]
    public class CsQuestion
    {
        public string id;
        public string source;
        public string grade_name;
        public string wrong_word;
        public string correct_answer;
        public CsWord[] words;
        public CsOption[] options;
    }

    [Serializable]
    public class CsWord
    {
        public string word_text;
        public int position;
        public bool is_wrong;
    }

    [Serializable]
    public class CsOption
    {
        public string option_text;
        public bool is_correct;
    }
}

```

---

### `ScienceStreet/Shared/Scripts/SSFontManager.cs`

> Lines: 28 | Size: 776B

```csharp
using UnityEngine;

namespace com.al_arcade.shared
{
    /// <summary>
    /// Global Arabic font manager. Set the font once from any DemoBuilder,
    /// then all text components pick it up via Apply().
    /// </summary>
    public static class SSFontManager
    {
        private static TMPro.TMP_FontAsset _font;

        public static TMPro.TMP_FontAsset Font
        {
            get => _font;
            set => _font = value;
        }

        public static void Apply(ALArcade.ArabicTMP.ArabicTextMeshPro tmp)
        {
            if (_font != null && tmp != null) tmp.font = _font;
        }

        public static void Apply(ALArcade.ArabicTMP.ArabicTextMeshProUGUI tmp)
        {
            if (_font != null && tmp != null) tmp.font = _font;
        }
    }
}
```

---

### `ScienceStreet/Shared/Scripts/SSGameSession.cs`

> Lines: 119 | Size: 4.1KB

```csharp
// ============================================================================
//  Science Street — Game Session Manager
//  Holds current game session state (build type, class code, etc.)
// ============================================================================
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Events;

namespace com.al_arcade.shared
{
    public class SSGameSession : MonoBehaviour
    {
        public static SSGameSession Instance { get; private set; }

        [Header("Build Configuration")]
        [Tooltip("scistreet = public, teacher = requires class code")]
        public string buildType = "scistreet";

        [Tooltip("Grade ID filter. 0 = all grades")]
        public int gradeId = 0;

        [Tooltip("Number of questions to fetch")]
        public int questionCount = 10;

        // ── Runtime State (Teacher Build) ────────────────────────────
        [HideInInspector] public string classCode = "";
        [HideInInspector] public string className = "";
        [HideInInspector] public string teacherName = "";
        [HideInInspector] public string gradeName = "";
        [HideInInspector] public bool isClassValidated = false;

        // ── Events ───────────────────────────────────────────────────
        [Header("Events")]
        public UnityEvent onSessionReady;
        public UnityEvent<string> onSessionError;

        private void Awake()
        {
            if (Instance != null && Instance != this)
            {
                Destroy(gameObject);
                return;
            }
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }

        public static SSGameSession EnsureInstance()
        {
            if (Instance != null) return Instance;
            var go = new GameObject("[SSGameSession]");
            Instance = go.AddComponent<SSGameSession>();
            DontDestroyOnLoad(go);
            return Instance;
        }

        /// <summary>
        /// Validates a teacher class code. On success, stores all class info.
        /// </summary>
        public IEnumerator ValidateAndStartTeacherSession(string code,
            Action onSuccess, Action<string> onError)
        {
            var api = SSApiManager.EnsureInstance();

            yield return api.ValidateClassCode(code,
                cls =>
                {
                    classCode = cls.class_code;
                    className = cls.class_name;
                    teacherName = cls.teacher_name;
                    gradeName = cls.grade_name;
                    buildType = "teacher";
                    isClassValidated = true;

                    if (int.TryParse(cls.grade_id, out int gid))
                        gradeId = gid;

                    Debug.Log($"[SSSession] Teacher session started: {className} by {teacherName}");
                    onSuccess?.Invoke();
                    onSessionReady?.Invoke();
                },
                err =>
                {
                    isClassValidated = false;
                    Debug.LogWarning($"[SSSession] Validation failed: {err}");
                    onError?.Invoke(err);
                    onSessionError?.Invoke(err);
                });
        }

        /// <summary>
        /// Starts a Science Street (public) session.
        /// </summary>
        public void StartSciStreetSession(int grade = 0)
        {
            buildType = "scistreet";
            gradeId = grade;
            classCode = "";
            isClassValidated = true;
            Debug.Log("[SSSession] SciStreet session started");
            onSessionReady?.Invoke();
        }

        /// <summary>
        /// Resets the session to defaults.
        /// </summary>
        public void ResetSession()
        {
            buildType = "scistreet";
            gradeId = 0;
            classCode = "";
            className = "";
            teacherName = "";
            gradeName = "";
            isClassValidated = false;
        }
    }
}

```

---

### `ScienceStreet/Shared/Scripts/SSParticleManager.cs`

> Lines: 240 | Size: 9.1KB

```csharp
// ============================================================================
//  Science Street — Particle Effect Manager
//  Creates particle effects programmatically. Works without any prefabs.
// ============================================================================
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

namespace com.al_arcade.shared
{
    public class SSParticleManager : MonoBehaviour
    {
        public static SSParticleManager Instance { get; private set; }

        [Header("Optional Prefabs (leave null for procedural)")]
        public ParticleSystem correctBurstPrefab;
        public ParticleSystem wrongBurstPrefab;
        public ParticleSystem confettiPrefab;
        public ParticleSystem sparksPrefab;
        public ParticleSystem starBurstPrefab;

        private Dictionary<string, ParticleSystem> _cache = new();

        private void Awake()
        {
            if (Instance != null && Instance != this)
            {
                Destroy(gameObject);
                return;
            }
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }

        public static SSParticleManager EnsureInstance()
        {
            if (Instance != null) return Instance;
            var go = new GameObject("[SSParticleManager]");
            Instance = go.AddComponent<SSParticleManager>();
            DontDestroyOnLoad(go);
            return Instance;
        }

        // ── Public Play Methods ──────────────────────────────────────

        public void PlayCorrectBurst(Vector3 position)
        {
            if (correctBurstPrefab != null)
            {
                SpawnPrefab(correctBurstPrefab, position);
                return;
            }
            PlayProceduralBurst(position, SSColorPalette.Success,
                SSColorPalette.Accent, 30, 0.6f);
        }

        public void PlayWrongBurst(Vector3 position)
        {
            if (wrongBurstPrefab != null)
            {
                SpawnPrefab(wrongBurstPrefab, position);
                return;
            }
            PlayProceduralBurst(position, SSColorPalette.Danger,
                SSColorPalette.Warning, 20, 0.4f);
        }

        public void PlayConfetti(Vector3 position)
        {
            if (confettiPrefab != null)
            {
                SpawnPrefab(confettiPrefab, position);
                return;
            }
            PlayProceduralConfetti(position, 80);
        }

        public void PlaySparks(Vector3 position, Color color)
        {
            if (sparksPrefab != null)
            {
                SpawnPrefab(sparksPrefab, position);
                return;
            }
            PlayProceduralBurst(position, color, Color.white, 15, 0.3f);
        }

        public void PlayStarBurst(Vector3 position)
        {
            if (starBurstPrefab != null)
            {
                SpawnPrefab(starBurstPrefab, position);
                return;
            }
            PlayProceduralBurst(position, SSColorPalette.Accent,
                Color.white, 25, 0.8f);
        }

        public void PlayScreenConfetti()
        {
            if (Camera.main == null) return;
            Vector3 pos = Camera.main.transform.position +
                          Camera.main.transform.forward * 5f +
                          Vector3.up * 3f;
            PlayConfetti(pos);
        }

        // ── Procedural Particle Creation ─────────────────────────────

        private void PlayProceduralBurst(Vector3 pos, Color startColor,
            Color endColor, int count, float lifetime)
        {
            var go = new GameObject("SSParticle_Burst");
            go.transform.position = pos;
            var ps = go.AddComponent<ParticleSystem>();

            var main = ps.main;
            main.startLifetime = lifetime;
            main.startSpeed = new ParticleSystem.MinMaxCurve(2f, 6f);
            main.startSize = new ParticleSystem.MinMaxCurve(0.05f, 0.15f);
            main.startColor = new ParticleSystem.MinMaxGradient(startColor, endColor);
            main.gravityModifier = 1.5f;
            main.maxParticles = count;
            main.simulationSpace = ParticleSystemSimulationSpace.World;
            main.loop = false;
            main.playOnAwake = true;
            main.duration = 0.1f;

            var emission = ps.emission;
            emission.rateOverTime = 0;
            emission.SetBursts(new[] { new ParticleSystem.Burst(0f, (short)count) });

            var shape = ps.shape;
            shape.shapeType = ParticleSystemShapeType.Sphere;
            shape.radius = 0.2f;

            var sizeOverLife = ps.sizeOverLifetime;
            sizeOverLife.enabled = true;
            sizeOverLife.size = new ParticleSystem.MinMaxCurve(1f,
                new AnimationCurve(
                    new Keyframe(0f, 1f),
                    new Keyframe(1f, 0f)));

            // Use default material
            var renderer = go.GetComponent<ParticleSystemRenderer>();
            renderer.material = GetParticleMaterial();

            ps.Play();
            Destroy(go, lifetime + 0.5f);
        }

        private void PlayProceduralConfetti(Vector3 pos, int count)
        {
            var go = new GameObject("SSParticle_Confetti");
            go.transform.position = pos;
            var ps = go.AddComponent<ParticleSystem>();

            var main = ps.main;
            main.startLifetime = new ParticleSystem.MinMaxCurve(2f, 4f);
            main.startSpeed = new ParticleSystem.MinMaxCurve(3f, 8f);
            main.startSize = new ParticleSystem.MinMaxCurve(0.08f, 0.2f);
            main.startRotation3D = true;
            main.startRotationX = new ParticleSystem.MinMaxCurve(0f, 360f * Mathf.Deg2Rad);
            main.startRotationY = new ParticleSystem.MinMaxCurve(0f, 360f * Mathf.Deg2Rad);
            main.startRotationZ = new ParticleSystem.MinMaxCurve(0f, 360f * Mathf.Deg2Rad);
            main.gravityModifier = 1.2f;
            main.maxParticles = count;
            main.simulationSpace = ParticleSystemSimulationSpace.World;
            main.loop = false;
            main.duration = 0.2f;

            // Random color from our palette
            var gradient = new Gradient();
            gradient.SetKeys(
                new[] {
                    new GradientColorKey(SSColorPalette.Accent, 0f),
                    new GradientColorKey(SSColorPalette.Primary, 0.25f),
                    new GradientColorKey(SSColorPalette.Success, 0.5f),
                    new GradientColorKey(SSColorPalette.Danger, 0.75f),
                    new GradientColorKey(SSColorPalette.Info, 1f)
                },
                new[] {
                    new GradientAlphaKey(1f, 0f),
                    new GradientAlphaKey(1f, 0.8f),
                    new GradientAlphaKey(0f, 1f)
                });
            main.startColor = new ParticleSystem.MinMaxGradient(gradient);

            var emission = ps.emission;
            emission.rateOverTime = 0;
            emission.SetBursts(new[] { new ParticleSystem.Burst(0f, (short)count) });

            var shape = ps.shape;
            shape.shapeType = ParticleSystemShapeType.Cone;
            shape.angle = 40f;
            shape.radius = 0.5f;

            var rotOverLife = ps.rotationOverLifetime;
            rotOverLife.enabled = true;
            rotOverLife.z = new ParticleSystem.MinMaxCurve(-180f * Mathf.Deg2Rad,
                180f * Mathf.Deg2Rad);

            var renderer = go.GetComponent<ParticleSystemRenderer>();
            renderer.material = GetParticleMaterial();
            renderer.renderMode = ParticleSystemRenderMode.Billboard;

            ps.Play();
            Destroy(go, 5f);
        }

        // ── Helpers ──────────────────────────────────────────────────

        private void SpawnPrefab(ParticleSystem prefab, Vector3 pos)
        {
            var instance = Instantiate(prefab, pos, Quaternion.identity);
            var main = instance.main;
            float lifetime = main.startLifetime.constantMax;
            if (main.startLifetime.mode == ParticleSystemCurveMode.TwoConstants)
                lifetime = main.startLifetime.constantMax;
            Destroy(instance.gameObject, lifetime + main.duration + 1f);
        }

        private Material _cachedMat;
        private Material GetParticleMaterial()
        {
            if (_cachedMat != null) return _cachedMat;

            // Try to find the default particle shader
            Shader shader = Shader.Find("Particles/Standard Unlit");
            if (shader == null) shader = Shader.Find("Universal Render Pipeline/Particles/Unlit");
            if (shader == null) shader = Shader.Find("Mobile/Particles/Alpha Blended");
            if (shader == null) shader = Shader.Find("Sprites/Default");
            if (shader == null) shader = Shader.Find("Unlit/Color");

            _cachedMat = new Material(shader);
            _cachedMat.SetFloat("_Mode", 2); // Fade
            return _cachedMat;
        }
    }
}

```

---

### `ScienceStreet/TF/Scripts/TfDemoBuilder.cs`

> Lines: 433 | Size: 20.1KB

```csharp
using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Events;
using DG.Tweening;
using TMPro;

namespace com.al_arcade.tf
{
    using shared;

    [AddComponentMenu("Science Street/TF Demo Builder")]
    public class TfDemoBuilder : MonoBehaviour
    {
        [Header("Build Configuration")]
        [SerializeField] private string buildType = "scistreet";
        [SerializeField] private int gradeId = 0;
        [SerializeField] private int questionCount = 15;
        [SerializeField] private string classCode = "";

        [Header("Game Settings")]
        [SerializeField] private int stepsToWin = 5;
        [SerializeField] private float stepDistance = 3f;

        [Header("Arabic Font")]
        [Tooltip("Drag your Arabic TMP_FontAsset here")]
        [SerializeField] private TMP_FontAsset arabicFont;

        [Header("Debug")]
        [SerializeField] private bool useOfflineTestData = false;

        private TfGameManager _gm;
        private TfHandController _handController;
        private TfProductionLine _productionLine;
        private TfQuestionScreen _questionScreen;
        private TfUIManager _uiManager;

        private Vector3 _camPos;
        private Vector3 _camLookAt;
        private float _lineCenterZ;

        private void Start()
        {
            DOTween.Init();
            if (arabicFont != null) SSFontManager.Font = arabicFont;
            StartCoroutine(BuildEverything());
        }

        private IEnumerator BuildEverything()
        {
            Debug.Log("═══════════════════════════════════════════");
            Debug.Log("  Science Street — T/F Factory (SIDE VIEW)");
            Debug.Log("═══════════════════════════════════════════");

            if (FindObjectOfType<EventSystem>() == null)
            {
                var es = new GameObject("EventSystem");
                es.AddComponent<EventSystem>();
                es.AddComponent<StandaloneInputModule>();
            }

            SSApiManager.EnsureInstance();
            SSAudioManager.EnsureInstance();
            SSParticleManager.EnsureInstance();

            var session = SSGameSession.EnsureInstance();
            session.buildType = buildType;
            session.gradeId = gradeId;
            session.questionCount = questionCount;
            session.classCode = classCode;
            yield return null;

            // Layout computation
            float totalLength = stepsToWin * stepDistance + 10f;
            _lineCenterZ = 2f + totalLength / 2f;
            _camPos = new Vector3(-11f, 3.5f, _lineCenterZ);
            _camLookAt = new Vector3(0f, 1.5f, _lineCenterZ);

            // ── Build everything ─────────────────────────────────────
            BuildScene(totalLength, _lineCenterZ);
            yield return null;

            SetupCamera();
            yield return null;

            // Production line
            var lineObj = new GameObject("ProductionLine");
            lineObj.transform.position = new Vector3(0, 0, 2);
            _productionLine = lineObj.AddComponent<TfProductionLine>();
            _productionLine.Build(stepsToWin, stepDistance, _camPos);
            yield return null;

            // Question screen
            var screenObj = new GameObject("QuestionScreen");
            _questionScreen = screenObj.AddComponent<TfQuestionScreen>();
            Vector3 screenPos = new Vector3(0, 5f, _lineCenterZ);
            _questionScreen.Build(screenPos, _camPos);
            yield return null;

            // Hands
            var handsObj = new GameObject("Hands");
            _handController = handsObj.AddComponent<TfHandController>();
            _handController.Build(Camera.main);
            yield return null;

            // UI
            var uiObj = new GameObject("TfUI");
            _uiManager = uiObj.AddComponent<TfUIManager>();
            _uiManager.BuildUI();
            if (_uiManager.onRestartClicked == null)
                _uiManager.onRestartClicked = new UnityEvent();
            yield return null;

            // Game Manager
            var gmObj = new GameObject("TfGameManager");
            _gm = gmObj.AddComponent<TfGameManager>();
            _gm.handController = _handController;
            _gm.productionLine = _productionLine;
            _gm.questionScreen = _questionScreen;
            _gm.uiManager = _uiManager;
            _uiManager.onRestartClicked.AddListener(OnRestartClicked);
            yield return null;

            SetupLighting();
            yield return null;

            Debug.Log("═══════════════════════════════════════════");
            Debug.Log("  T/F Game Ready! A/← = True  D/→ = False");
            Debug.Log("═══════════════════════════════════════════");

            yield return new WaitForSeconds(0.5f);

            if (useOfflineTestData)
                _gm.StartWithQuestions(GetTestQuestions());
            else
                _gm.StartGame();
        }

        private void OnRestartClicked()
        {
            if (_gm != null) _gm.ResetGame();
            SetupCamera();
            if (_gm != null) _gm.StartGame();
        }

        private void SetupCamera()
        {
            var cam = Camera.main;
            if (cam == null) return;
            cam.transform.position = _camPos;
            cam.transform.LookAt(_camLookAt);
            cam.fieldOfView = 55;
            cam.nearClipPlane = 0.1f;
        }

        // ════════════════════════════════════════════════════════════
        // ★ BRIGHT FACTORY SCENE
        // ════════════════════════════════════════════════════════════

        private void BuildScene(float totalLength, float lineCenterZ)
        {
            // ★ Bright blue-gray sky instead of dark
            Camera.main.clearFlags = CameraClearFlags.SolidColor;
            Camera.main.backgroundColor = new Color32(70, 80, 110, 255);

            var shader = GetShader();
            float sceneDepth = totalLength + 12f;
            float halfDepth = sceneDepth / 2f;

            // ── Floor — lighter ──────────────────────────────────────
            var floor = GameObject.CreatePrimitive(PrimitiveType.Cube);
            floor.name = "Floor";
            floor.transform.position = new Vector3(0, -0.05f, lineCenterZ);
            floor.transform.localScale = new Vector3(30, 0.1f, sceneDepth);
            floor.GetComponent<Renderer>().material =
                new Material(shader) { color = new Color32(65, 65, 80, 255) };

            // ── Floor tile pattern ───────────────────────────────────
            for (float x = -12; x <= 12; x += 6f)
            {
                for (float z = lineCenterZ - halfDepth; z < lineCenterZ + halfDepth; z += 6f)
                {
                    var tile = GameObject.CreatePrimitive(PrimitiveType.Cube);
                    tile.name = "Tile";
                    tile.transform.position = new Vector3(x, 0.001f, z);
                    tile.transform.localScale = new Vector3(5.8f, 0.001f, 5.8f);
                    Destroy(tile.GetComponent<Collider>());
                    bool checker = ((int)(x / 6) + (int)(z / 6)) % 2 == 0;
                    tile.GetComponent<Renderer>().material = new Material(shader)
                    {
                        color = checker
                            ? new Color32(60, 60, 75, 255)
                            : new Color32(70, 70, 85, 255)
                    };
                }
            }

            // ── Walls — brighter, with color accent ──────────────────
            for (int side = -1; side <= 1; side += 2)
            {
                var wall = GameObject.CreatePrimitive(PrimitiveType.Cube);
                wall.name = $"Wall_{(side < 0 ? "L" : "R")}";
                wall.transform.position = new Vector3(side * 15f, 5f, lineCenterZ);
                wall.transform.localScale = new Vector3(0.5f, 10f, sceneDepth);
                Destroy(wall.GetComponent<Collider>());
                wall.GetComponent<Renderer>().material =
                    new Material(shader) { color = new Color32(75, 75, 95, 255) };

                // ★ Accent stripe on walls
                var stripe = GameObject.CreatePrimitive(PrimitiveType.Cube);
                stripe.name = "WallStripe";
                stripe.transform.position = new Vector3(side * 14.7f, 1.5f, lineCenterZ);
                stripe.transform.localScale = new Vector3(0.1f, 0.15f, sceneDepth);
                Destroy(stripe.GetComponent<Collider>());
                stripe.GetComponent<Renderer>().material = new Material(shader)
                {
                    color = SSColorPalette.WithAlpha(SSColorPalette.Accent, 0.3f)
                };
            }

            // ── End walls ────────────────────────────────────────────
            foreach (float z in new[] { lineCenterZ + halfDepth, lineCenterZ - halfDepth })
            {
                var bw = GameObject.CreatePrimitive(PrimitiveType.Cube);
                bw.name = "EndWall";
                bw.transform.position = new Vector3(0, 5, z);
                bw.transform.localScale = new Vector3(30, 10, 0.5f);
                Destroy(bw.GetComponent<Collider>());
                bw.GetComponent<Renderer>().material =
                    new Material(shader) { color = new Color32(70, 70, 88, 255) };
            }

            // ── Ceiling — slightly reflective look ───────────────────
            var ceiling = GameObject.CreatePrimitive(PrimitiveType.Cube);
            ceiling.name = "Ceiling";
            ceiling.transform.position = new Vector3(0, 10, lineCenterZ);
            ceiling.transform.localScale = new Vector3(30, 0.3f, sceneDepth);
            Destroy(ceiling.GetComponent<Collider>());
            ceiling.GetComponent<Renderer>().material =
                new Material(shader) { color = new Color32(55, 58, 72, 255) };

            // ── Industrial pipes — more colorful ─────────────────────
            Color[] pipeColors =
            {
                new Color32(90, 90, 110, 255),
                new Color32(100, 85, 75, 255),
                SSColorPalette.WithAlpha(SSColorPalette.Primary, 0.5f),
            };
            for (int i = 0; i < 12; i++)
            {
                int side = Random.value > 0.5f ? 1 : -1;
                var pipe = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
                pipe.name = $"Pipe_{i}";
                pipe.transform.position = new Vector3(
                    side * Random.Range(12f, 14.5f),
                    Random.Range(4f, 8.5f),
                    lineCenterZ + Random.Range(-halfDepth * 0.7f, halfDepth * 0.7f));
                pipe.transform.localScale = new Vector3(
                    Random.Range(0.12f, 0.25f),
                    Random.Range(2f, 6f),
                    Random.Range(0.12f, 0.25f));
                pipe.transform.rotation = Quaternion.Euler(0, 0, 90);
                Destroy(pipe.GetComponent<Collider>());
                pipe.GetComponent<Renderer>().material =
                    new Material(shader)
                    {
                        color = pipeColors[Random.Range(0, pipeColors.Length)]
                    };
            }

            // ── Warning stripes — brighter accent ────────────────────
            for (float z = lineCenterZ - halfDepth; z < lineCenterZ + halfDepth; z += 5f)
            {
                var ws = GameObject.CreatePrimitive(PrimitiveType.Cube);
                ws.name = "WarningStripe";
                ws.transform.position = new Vector3(0, 0.01f, z);
                ws.transform.localScale = new Vector3(18, 0.01f, 0.2f);
                Destroy(ws.GetComponent<Collider>());
                ws.GetComponent<Renderer>().material = new Material(shader)
                {
                    color = SSColorPalette.WithAlpha(SSColorPalette.Accent, 0.08f)
                };
            }

            // ── Hanging lamps — ★ brighter, more of them ─────────────
            for (float z = lineCenterZ - halfDepth + 3; z < lineCenterZ + halfDepth; z += 5f)
            {
                // Pole
                var pole = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
                pole.name = "LampPole";
                pole.transform.position = new Vector3(0, 9.3f, z);
                pole.transform.localScale = new Vector3(0.03f, 0.6f, 0.03f);
                Destroy(pole.GetComponent<Collider>());
                pole.GetComponent<Renderer>().material =
                    new Material(shader) { color = new Color32(90, 90, 100, 255) };

                // Lamp head
                var head = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
                head.name = "LampHead";
                head.transform.position = new Vector3(0, 8.7f, z);
                head.transform.localScale = new Vector3(0.5f, 0.06f, 0.5f);
                Destroy(head.GetComponent<Collider>());
                head.GetComponent<Renderer>().material = new Material(shader)
                {
                    color = SSColorPalette.WithAlpha(SSColorPalette.Accent, 0.6f)
                };

                // ★ Actual point light per lamp
                var lampLight = new GameObject($"LampLight_{z:F0}");
                var lt = lampLight.AddComponent<Light>();
                lt.type = LightType.Point;
                lt.range = 8;
                lt.intensity = 0.4f;
                lt.color = new Color32(255, 240, 210, 255); // warm white
                lampLight.transform.position = new Vector3(0, 8.5f, z);
            }

            // ── Equipment boxes (decorative) ─────────────────────────
            for (int i = 0; i < 6; i++)
            {
                float side = (i % 2 == 0) ? -1 : 1;
                var equip = GameObject.CreatePrimitive(PrimitiveType.Cube);
                equip.name = "Equipment";
                float h = Random.Range(1f, 2.5f);
                equip.transform.position = new Vector3(
                    side * Random.Range(5f, 10f),
                    h / 2f,
                    lineCenterZ + Random.Range(-halfDepth * 0.6f, halfDepth * 0.6f));
                equip.transform.localScale = new Vector3(
                    Random.Range(1f, 2f), h, Random.Range(1f, 2f));
                Destroy(equip.GetComponent<Collider>());
                Color[] equipColors =
                {
                    SSColorPalette.WithAlpha(SSColorPalette.Primary, 0.4f),
                    new Color32(80, 80, 100, 255),
                    SSColorPalette.WithAlpha(SSColorPalette.Info, 0.3f),
                };
                equip.GetComponent<Renderer>().material = new Material(shader)
                {
                    color = equipColors[Random.Range(0, equipColors.Length)]
                };
            }
        }

        // ════════════════════════════════════════════════════════════
        // ★ BRIGHTER LIGHTING
        // ════════════════════════════════════════════════════════════

        private void SetupLighting()
        {
            // Main directional — brighter, warmer
            var existing = FindObjectOfType<Light>();
            Light dirLight;
            if (existing != null)
                dirLight = existing;
            else
            {
                var lo = new GameObject("DirectionalLight");
                dirLight = lo.AddComponent<Light>();
            }
            dirLight.type = LightType.Directional;
            dirLight.transform.rotation = Quaternion.Euler(45, -30, 0);
            // ★ Brighter, warmer color
            dirLight.color = new Color32(240, 235, 220, 255);
            dirLight.intensity = 1.1f;
            dirLight.shadows = LightShadows.Soft;

            // ★ Strong fill light from camera side
            var fillObj = new GameObject("FillLight");
            var fill = fillObj.AddComponent<Light>();
            fill.type = LightType.Point;
            fill.range = 35;
            fill.intensity = 0.6f;
            fill.color = new Color32(210, 220, 240, 255);
            fillObj.transform.position = new Vector3(-10, 5, _lineCenterZ);

            // ★ Accent colored lights along line
            Color[] accentColors = {
                SSColorPalette.Accent, SSColorPalette.Info,
                SSColorPalette.Primary, SSColorPalette.Success
            };
            for (int i = 0; i < 4; i++)
            {
                var pl = new GameObject($"AccentLight_{i}");
                var lt = pl.AddComponent<Light>();
                lt.type = LightType.Point;
                lt.range = 10;
                lt.intensity = 0.3f;
                lt.color = accentColors[i % accentColors.Length];
                float z = _lineCenterZ + Mathf.Lerp(-6, 6, (float)i / 3);
                pl.transform.position = new Vector3(
                    (i % 2 == 0 ? -1 : 1) * 6f, 6f, z);
            }

            // ★ Brighter ambient
            RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Flat;
            RenderSettings.ambientLight = new Color32(75, 78, 95, 255);
        }

        // ════════════════════════════════════════════════════════════
        // TEST DATA
        // ════════════════════════════════════════════════════════════

        private TfQuestion[] GetTestQuestions()
        {
            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="أحياء" },
            };
        }

        private Shader GetShader()
        {
            return Shader.Find("Universal Render Pipeline/Lit")
                ?? Shader.Find("Standard")
                ?? Shader.Find("Unlit/Color");
        }
    }
}
```

---

### `ScienceStreet/TF/Scripts/TfGameManager.cs`

> Lines: 235 | Size: 8.4KB

```csharp
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
using DG.Tweening;

namespace com.al_arcade.tf
{
    using shared;

    public enum TfGameState
    { Idle, Loading, Playing, Feedback, Complete, GameOver }

    public class TfGameManager : MonoBehaviour
    {
        public static TfGameManager Instance { get; private set; }

        [Header("Game Settings")]
        [SerializeField] private int stepsToWin = 5;
        [SerializeField] private float feedbackDuration = 1.2f;
        [SerializeField] private float stepDistance = 3f;

        [Header("References")]
        public TfHandController handController;
        public TfProductionLine productionLine;
        public TfQuestionScreen questionScreen;
        public TfUIManager uiManager;

        private TfGameState _state = TfGameState.Idle;
        private TfQuestion[] _questions;
        private int _currentIndex, _progress, _score, _streak;
        private int _correctCount, _wrongCount, _totalAsked;

        private bool _waitingForAnswer;
        private int _pendingAnswer = -1;

        [Header("Events")]
        public UnityEvent onGameStart;
        public UnityEvent<bool> onAnswerGiven;
        public UnityEvent<int> onProgressChanged;
        public UnityEvent onGameComplete;
        public UnityEvent onGameOver;

        private void Awake()
        {
            if (Instance != null && Instance != this) { Destroy(gameObject); return; }
            Instance = this;
        }

        public void StartGame() => StartCoroutine(StartGameRoutine());

        public void StartWithQuestions(TfQuestion[] questions)
        { _questions = questions; BeginGameplay(); }

        public void SubmitAnswer(bool answer)
        {
            if (!_waitingForAnswer) return;
            _waitingForAnswer = false;
            _pendingAnswer = answer ? 1 : 0;
        }

        public void ResetGame()
        {
            StopAllCoroutines();
            _state = TfGameState.Idle;
            _currentIndex = _progress = _score = _streak = 0;
            _correctCount = _wrongCount = _totalAsked = 0;
            _questions = null;
            _waitingForAnswer = false;
            _pendingAnswer = -1;

            if (productionLine != null) productionLine.ResetLine();
            if (questionScreen != null) questionScreen.Clear();
            if (handController != null) handController.Reset();
            if (uiManager != null) uiManager.ResetUI();
        }

        private IEnumerator StartGameRoutine()
        {
            _state = TfGameState.Loading;
            if (uiManager != null) uiManager.ShowLoading("جاري تحميل الأسئلة...");

            var session = SSGameSession.EnsureInstance();
            var api = SSApiManager.EnsureInstance();
            string error = null;

            yield return api.FetchTf(
                session.buildType, session.classCode,
                session.questionCount, session.gradeId,
                qs => _questions = qs,
                err => error = err
            );

            if (error != null || _questions == null || _questions.Length == 0)
            {
                if (uiManager != null) uiManager.ShowError(error ?? "لا توجد أسئلة");
                _state = TfGameState.Idle;
                yield break;
            }

            if (uiManager != null) uiManager.HideLoading();
            BeginGameplay();
        }

        private void BeginGameplay()
        {
            _currentIndex = _progress = _score = _streak = 0;
            _correctCount = _wrongCount = _totalAsked = 0;
            _state = TfGameState.Playing;

            if (uiManager != null)
            {
                uiManager.ShowGameUI();
                uiManager.SetProgress(0, stepsToWin);
                uiManager.SetScore(0);
            }

            onGameStart?.Invoke();
            StartCoroutine(GameLoop());
        }

        private IEnumerator GameLoop()
        {
            while (_progress < stepsToWin && _currentIndex < _questions.Length)
            {
                var q = _questions[_currentIndex];
                if (questionScreen != null)
                    questionScreen.ShowQuestion(q.question_text, q.source);

                _waitingForAnswer = true;
                _pendingAnswer = -1;
                _state = TfGameState.Playing;
                if (handController != null) handController.SetReady(true);

                while (_pendingAnswer < 0)
                {
                    if (Input.GetKeyDown(KeyCode.LeftArrow) || Input.GetKeyDown(KeyCode.A))
                        SubmitAnswer(true);
                    if (Input.GetKeyDown(KeyCode.RightArrow) || Input.GetKeyDown(KeyCode.D))
                        SubmitAnswer(false);
                    yield return null;
                }

                if (handController != null) handController.SetReady(false);

                _state = TfGameState.Feedback;
                _totalAsked++;
                bool playerSaidTrue = _pendingAnswer == 1;
                bool isCorrect = playerSaidTrue == q.is_true;

                if (isCorrect)
                {
                    _correctCount++;
                    _streak++;
                    _progress++;
                    _score += 100 + (_streak > 2 ? (_streak - 2) * 25 : 0);

                    if (handController != null) handController.PlayCorrectFeedback(playerSaidTrue);
                    if (productionLine != null) yield return productionLine.MoveForward(stepDistance);
                    if (questionScreen != null) questionScreen.ShowFeedback(true, _streak);

                    var audio = SSAudioManager.Instance;
                    if (audio != null)
                    { if (audio.sfxCorrect != null) audio.PlayCorrect(); else audio.PlayCorrectBeep(); }

                    var particles = SSParticleManager.Instance;
                    if (particles != null && Camera.main != null)
                        particles.PlayCorrectBurst(Camera.main.transform.position +
                            Camera.main.transform.forward * 3f);
                }
                else
                {
                    _wrongCount++;
                    _streak = 0;
                    _progress = Mathf.Max(0, _progress - 1);

                    if (handController != null) handController.PlayWrongFeedback(playerSaidTrue);
                    if (productionLine != null) yield return productionLine.MoveBackward(stepDistance);
                    if (questionScreen != null) questionScreen.ShowFeedback(false, 0);

                    var audio = SSAudioManager.Instance;
                    if (audio != null)
                    { if (audio.sfxWrong != null) audio.PlayWrong(); else audio.PlayWrongBeep(); }

                    if (Camera.main != null)
                    {
                        DOTween.Kill(Camera.main.transform, "camShake");
                        Camera.main.transform.DOShakePosition(0.4f, 0.3f, 15)
                            .SetEase(Ease.OutQuad).SetId("camShake");
                    }
                }

                onAnswerGiven?.Invoke(isCorrect);
                onProgressChanged?.Invoke(_progress);
                if (uiManager != null)
                {
                    uiManager.SetProgress(_progress, stepsToWin);
                    uiManager.SetScore(_score);
                    uiManager.SetStreak(_streak);
                }

                yield return new WaitForSeconds(feedbackDuration);
                _currentIndex++;
                if (_currentIndex >= _questions.Length) _currentIndex = 0;
            }

            if (_progress >= stepsToWin)
            {
                _state = TfGameState.Complete;
                yield return VictorySequence();
            }
        }

        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();

            if (productionLine != null) yield return productionLine.ProductComplete();
            yield return new WaitForSeconds(0.5f);

            if (uiManager != null)
                uiManager.ShowResults(_score, _correctCount, _wrongCount, true);
            onGameComplete?.Invoke();
        }

        public int Progress => _progress;
        public int StepsToWin => stepsToWin;
        public int Score => _score;
    }
}

```

---

### `ScienceStreet/TF/Scripts/TfHandController.cs`

> Lines: 272 | Size: 11.3KB

```csharp
using UnityEngine;
using DG.Tweening;
using ALArcade.ArabicTMP;

namespace com.al_arcade.tf
{
    using shared;

    public class TfHandController : MonoBehaviour
    {
        private GameObject _leftHand, _rightHand;
        private GameObject _leftBtn, _rightBtn;
        private ArabicTextMeshPro _leftLabel, _rightLabel;
        private Material _leftMat, _rightMat;

        private Vector3 _leftRestPos, _rightRestPos;
        private Vector3 _leftBtnBaseScale, _rightBtnBaseScale;
        private bool _isReady;
        private Sequence _idleSeq;
        private Camera _cam;

        public void Build(Camera cam)
        {
            _cam = cam;
            transform.SetParent(cam.transform);
            transform.localPosition = Vector3.zero;
            transform.localRotation = Quaternion.identity;

            BuildHand(true, out _leftHand, out _leftBtn, out _leftLabel, out _leftMat);
            BuildHand(false, out _rightHand, out _rightBtn, out _rightLabel, out _rightMat);

            _leftRestPos = _leftHand.transform.localPosition;
            _rightRestPos = _rightHand.transform.localPosition;
            _leftBtnBaseScale = _leftBtn.transform.localScale;
            _rightBtnBaseScale = _rightBtn.transform.localScale;

            StartIdleAnimation();
        }

        private void BuildHand(bool isLeft, out GameObject hand, out GameObject btn,
            out ArabicTextMeshPro label, out Material mat)
        {
            float side = isLeft ? -1f : 1f;
            var shader = GetShader();

            // ── Hand root ────────────────────────────────────────────
            hand = new GameObject(isLeft ? "LeftHand" : "RightHand");
            hand.transform.SetParent(transform);
            hand.transform.localPosition = new Vector3(side * 0.35f, -0.3f, 0.55f);
            hand.transform.localRotation = Quaternion.Euler(12f, -side * 8f, side * 4f);

            // ── Arm (smaller, more proportional) ─────────────────────
            var arm = GameObject.CreatePrimitive(PrimitiveType.Capsule);
            arm.name = "Arm";
            arm.transform.SetParent(hand.transform);
            arm.transform.localPosition = new Vector3(0, -0.08f, 0);
            arm.transform.localScale = new Vector3(0.08f, 0.14f, 0.08f);
            Destroy(arm.GetComponent<Collider>());
            arm.GetComponent<Renderer>().material =
                new Material(shader) { color = new Color32(255, 220, 180, 255) };

            // ── Button (sized for close-up FPS view) ─────────────────
            btn = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
            btn.name = isLeft ? "TrueButton" : "FalseButton";
            btn.transform.SetParent(hand.transform);
            btn.transform.localPosition = new Vector3(0, 0.1f, 0.06f);
            btn.transform.localScale = new Vector3(0.16f, 0.025f, 0.16f);
            Destroy(btn.GetComponent<Collider>());

            mat = new Material(shader)
            {
                color = isLeft ? SSColorPalette.TrueGreen : SSColorPalette.FalseRed
            };
            btn.GetComponent<Renderer>().material = mat;

            // ── Label — ★ small font, tight rect, on top of button ───
            var labelObj = new GameObject("Label");
            labelObj.transform.SetParent(btn.transform);
            labelObj.transform.localPosition = new Vector3(0, 0.8f, 0);
            labelObj.transform.localRotation = Quaternion.Euler(90, 0, 0);
            // ★ Scale the label container down so the text is proportional
            labelObj.transform.localScale = new Vector3(0.6f, 0.6f, 0.6f);

            label = labelObj.AddComponent<ArabicTextMeshPro>();
            label.arabicText = isLeft ? "صح ✓" : "خطأ ✗";
            // ★ fontSize 2 in a small world-space rect = readable not giant
            label.fontSize = 2.2f;
            label.fontSizeMin = 1.5f;
            label.fontSizeMax = 2.5f;
            label.enableAutoSizing = true;
            label.alignment = TMPro.TextAlignmentOptions.Center;
            label.color = Color.white;
            label.fontStyle = TMPro.FontStyles.Bold;
            // ★ Tight rect so text doesn't overflow
            label.rectTransform.sizeDelta = new Vector2(1.2f, 0.5f);
            label.overflowMode = TMPro.TextOverflowModes.Overflow;
            SSFontManager.Apply(label);

            // ── Glow ring ────────────────────────────────────────────
            var ring = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
            ring.name = "GlowRing";
            ring.transform.SetParent(btn.transform);
            ring.transform.localPosition = Vector3.zero;
            ring.transform.localScale = new Vector3(1.15f, 0.4f, 1.15f);
            Destroy(ring.GetComponent<Collider>());
            ring.GetComponent<Renderer>().material = new Material(shader)
            {
                color = SSColorPalette.WithAlpha(
                    isLeft ? SSColorPalette.TrueGreen : SSColorPalette.FalseRed, 0.25f)
            };
        }

        // ── Idle Animation ───────────────────────────────────────────

        private void StartIdleAnimation()
        {
            KillIdleTween();
            if (_leftHand == null || _rightHand == null) return;

            _idleSeq = DOTween.Sequence();
            _idleSeq.Append(_leftHand.transform
                .DOLocalMoveY(_leftRestPos.y + 0.015f, 1.8f)
                .SetEase(Ease.InOutSine));
            _idleSeq.Join(_rightHand.transform
                .DOLocalMoveY(_rightRestPos.y - 0.015f, 1.8f)
                .SetEase(Ease.InOutSine));
            _idleSeq.Append(_leftHand.transform
                .DOLocalMoveY(_leftRestPos.y, 1.8f)
                .SetEase(Ease.InOutSine));
            _idleSeq.Join(_rightHand.transform
                .DOLocalMoveY(_rightRestPos.y, 1.8f)
                .SetEase(Ease.InOutSine));
            _idleSeq.SetLoops(-1, LoopType.Restart);
        }

        private void KillIdleTween()
        {
            _idleSeq?.Kill();
            _idleSeq = null;
        }

        // ── Input ────────────────────────────────────────────────────

        public void SetReady(bool ready) => _isReady = ready;

        private void Update()
        {
            if (!_isReady) return;
            if (Input.GetMouseButtonDown(0))
            {
                PressButton(Input.mousePosition.x < Screen.width / 2f);
            }
        }

        private void PressButton(bool isTrue)
        {
            if (!_isReady) return;

            var hand = isTrue ? _leftHand : _rightHand;
            var btn = isTrue ? _leftBtn : _rightBtn;
            var restPos = isTrue ? _leftRestPos : _rightRestPos;
            var btnBaseScale = isTrue ? _leftBtnBaseScale : _rightBtnBaseScale;

            // ★ Kill ALL tweens, reset to known state
            KillIdleTween();
            DOTween.Kill(hand.transform);
            DOTween.Kill(btn.transform);
            hand.transform.localPosition = restPos;
            btn.transform.localScale = btnBaseScale;

            // ★ Press animation — always returns to restPos
            var pressSeq = DOTween.Sequence();
            pressSeq.Append(hand.transform
                .DOLocalMoveY(restPos.y + 0.06f, 0.07f)
                .SetEase(Ease.OutQuad));
            pressSeq.Append(hand.transform
                .DOLocalMoveY(restPos.y - 0.04f, 0.05f)
                .SetEase(Ease.InQuad));
            pressSeq.Append(hand.transform
                .DOLocalMoveY(restPos.y, 0.2f)
                .SetEase(Ease.OutBounce));
            pressSeq.OnComplete(() => StartIdleAnimation());

            // ★ Button squash (Y axis is thickness for cylinder)
            btn.transform.DOScaleY(btnBaseScale.y * 0.3f, 0.05f)
                .SetEase(Ease.InQuad)
                .OnComplete(() =>
                    btn.transform.DOScaleY(btnBaseScale.y, 0.15f)
                        .SetEase(Ease.OutBounce));

            var audio = SSAudioManager.Instance;
            if (audio != null) audio.PlayClickBeep();

            if (TfGameManager.Instance != null)
                TfGameManager.Instance.SubmitAnswer(isTrue);
        }

        // ── Feedback ─────────────────────────────────────────────────

        public void PlayCorrectFeedback(bool wasTrue)
        {
            var hand = wasTrue ? _leftHand : _rightHand;
            var mat = wasTrue ? _leftMat : _rightMat;
            var restPos = wasTrue ? _leftRestPos : _rightRestPos;
            if (hand == null || mat == null) return;

            DOTween.Kill(hand.transform);
            DOTween.Kill(mat);
            var orig = mat.color;

            var seq = DOTween.Sequence();
            seq.Append(hand.transform
                .DOLocalMoveY(restPos.y + 0.12f, 0.2f)
                .SetEase(Ease.OutBack));
            seq.Join(mat.DOColor(Color.white, 0.1f));
            seq.Append(mat.DOColor(orig, 0.3f));
            seq.Append(hand.transform.DOLocalMove(restPos, 0.3f));
            seq.OnComplete(() => StartIdleAnimation());
        }

        public void PlayWrongFeedback(bool wasTrue)
        {
            var hand = wasTrue ? _leftHand : _rightHand;
            var restPos = wasTrue ? _leftRestPos : _rightRestPos;
            if (hand == null) return;

            DOTween.Kill(hand.transform);
            var seq = DOTween.Sequence();
            seq.Append(hand.transform.DOShakePosition(0.35f, 0.03f, 18));
            seq.Append(hand.transform.DOLocalMove(restPos, 0.25f));
            seq.OnComplete(() => StartIdleAnimation());
        }

        public void Reset()
        {
            KillIdleTween();
            _isReady = false;
            if (_leftHand != null)
            {
                DOTween.Kill(_leftHand.transform);
                _leftHand.transform.localPosition = _leftRestPos;
            }
            if (_rightHand != null)
            {
                DOTween.Kill(_rightHand.transform);
                _rightHand.transform.localPosition = _rightRestPos;
            }
            if (_leftBtn != null)
            {
                DOTween.Kill(_leftBtn.transform);
                _leftBtn.transform.localScale = _leftBtnBaseScale;
            }
            if (_rightBtn != null)
            {
                DOTween.Kill(_rightBtn.transform);
                _rightBtn.transform.localScale = _rightBtnBaseScale;
            }
        }

        private void OnDestroy()
        {
            KillIdleTween();
            if (_leftHand != null) DOTween.Kill(_leftHand.transform);
            if (_rightHand != null) DOTween.Kill(_rightHand.transform);
            if (_leftBtn != null) DOTween.Kill(_leftBtn.transform);
            if (_rightBtn != null) DOTween.Kill(_rightBtn.transform);
        }

        private Shader GetShader() =>
            Shader.Find("Universal Render Pipeline/Lit")
            ?? Shader.Find("Standard") ?? Shader.Find("Unlit/Color");
    }
}
```

---

### `ScienceStreet/TF/Scripts/TfProductionLine.cs`

> Lines: 267 | Size: 12.4KB

```csharp
using System.Collections;
using UnityEngine;
using DG.Tweening;
using ALArcade.ArabicTMP;

namespace com.al_arcade.tf
{
    using shared;

    public class TfProductionLine : MonoBehaviour
    {
        private GameObject _product;
        private GameObject[] _rollers;
        private Material _productMat;
        private float _currentZ;
        private int _stepsToWin;

        public void Build(int stepsToWin, float stepDistance, Vector3 cameraPos)
        {
            _stepsToWin = stepsToWin;
            float totalLength = stepsToWin * stepDistance + 10f;
            var shader = GetShader();

            // ── Belt surface ─────────────────────────────────────────
            var belt = GameObject.CreatePrimitive(PrimitiveType.Cube);
            belt.name = "Belt";
            belt.transform.SetParent(transform);
            belt.transform.localPosition = new Vector3(0, 0.3f, totalLength / 2f);
            belt.transform.localScale = new Vector3(2.5f, 0.1f, totalLength);
            Destroy(belt.GetComponent<Collider>());
            belt.GetComponent<Renderer>().material =
                new Material(shader) { color = new Color32(55, 55, 65, 255) };

            // ── Belt top highlight ───────────────────────────────────
            var beltTop = GameObject.CreatePrimitive(PrimitiveType.Cube);
            beltTop.name = "BeltTop";
            beltTop.transform.SetParent(transform);
            beltTop.transform.localPosition = new Vector3(0, 0.36f, totalLength / 2f);
            beltTop.transform.localScale = new Vector3(2.4f, 0.005f, totalLength);
            Destroy(beltTop.GetComponent<Collider>());
            beltTop.GetComponent<Renderer>().material =
                new Material(shader) { color = new Color32(70, 70, 80, 255) };

            // ── Rollers ──────────────────────────────────────────────
            int rc = Mathf.CeilToInt(totalLength / 2f);
            _rollers = new GameObject[rc];
            for (int i = 0; i < rc; i++)
            {
                var r = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
                r.name = $"Roller_{i}";
                r.transform.SetParent(transform);
                r.transform.localPosition = new Vector3(0, 0.15f, i * 2f);
                r.transform.localRotation = Quaternion.Euler(0, 0, 90);
                r.transform.localScale = new Vector3(0.1f, 1.3f, 0.1f);
                Destroy(r.GetComponent<Collider>());
                r.GetComponent<Renderer>().material =
                    new Material(shader) { color = new Color32(130, 130, 140, 255) };
                _rollers[i] = r;
            }

            // ── Side rails ───────────────────────────────────────────
            for (int s = -1; s <= 1; s += 2)
            {
                var rail = GameObject.CreatePrimitive(PrimitiveType.Cube);
                rail.name = "Rail";
                rail.transform.SetParent(transform);
                rail.transform.localPosition = new Vector3(s * 1.35f, 0.5f, totalLength / 2f);
                rail.transform.localScale = new Vector3(0.06f, 0.35f, totalLength);
                Destroy(rail.GetComponent<Collider>());
                rail.GetComponent<Renderer>().material =
                    new Material(shader) { color = SSColorPalette.Primary };
            }

            // ── Legs ─────────────────────────────────────────────────
            for (float z = 1; z < totalLength; z += 4f)
            {
                for (int s = -1; s <= 1; s += 2)
                {
                    var leg = GameObject.CreatePrimitive(PrimitiveType.Cube);
                    leg.name = "Leg";
                    leg.transform.SetParent(transform);
                    leg.transform.localPosition = new Vector3(s * 1f, 0.1f, z);
                    leg.transform.localScale = new Vector3(0.08f, 0.3f, 0.08f);
                    Destroy(leg.GetComponent<Collider>());
                    leg.GetComponent<Renderer>().material =
                        new Material(shader) { color = new Color32(85, 85, 95, 255) };
                }
            }

            // ── Progress markers ─────────────────────────────────────
            for (int i = 0; i < stepsToWin; i++)
            {
                float mz = (i + 1) * stepDistance;

                // Line marker
                var marker = GameObject.CreatePrimitive(PrimitiveType.Cube);
                marker.name = $"Marker_{i}";
                marker.transform.SetParent(transform);
                marker.transform.localPosition = new Vector3(0, 0.37f, mz);
                marker.transform.localScale = new Vector3(2.5f, 0.015f, 0.1f);
                Destroy(marker.GetComponent<Collider>());
                marker.GetComponent<Renderer>().material =
                    new Material(shader)
                    {
                        color = SSColorPalette.WithAlpha(SSColorPalette.Accent, 0.5f)
                    };

                // Step label — facing camera
                var labelObj = new GameObject("StepLabel");
                labelObj.transform.SetParent(marker.transform);
                labelObj.transform.position =
                    marker.transform.position + new Vector3(1.6f, 0.5f, 0);

                // Billboard toward camera
                Vector3 away = labelObj.transform.position - cameraPos;
                away.y = 0;
                if (away.sqrMagnitude > 0.01f)
                    labelObj.transform.rotation = Quaternion.LookRotation(away);

                var lbl = labelObj.AddComponent<ArabicTextMeshPro>();
                lbl.arabicText = $"✦ {i + 1}";
                // ★ Small readable size
                lbl.fontSize = 2f;
                lbl.alignment = TMPro.TextAlignmentOptions.Center;
                lbl.color = SSColorPalette.WithAlpha(SSColorPalette.Accent, 0.7f);
                lbl.rectTransform.sizeDelta = new Vector2(1.5f, 0.6f);
                SSFontManager.Apply(lbl);
            }

            // ── End zone ─────────────────────────────────────────────
            float endZ = stepsToWin * stepDistance + 2f;
            var ez = GameObject.CreatePrimitive(PrimitiveType.Cube);
            ez.name = "EndZone";
            ez.transform.SetParent(transform);
            ez.transform.localPosition = new Vector3(0, 0.37f, endZ);
            ez.transform.localScale = new Vector3(3f, 0.015f, 2f);
            Destroy(ez.GetComponent<Collider>());
            ez.GetComponent<Renderer>().material =
                new Material(shader) { color = SSColorPalette.WithAlpha(SSColorPalette.Success, 0.35f) };

            // ── Product ──────────────────────────────────────────────
            _product = new GameObject("Product");
            _product.transform.SetParent(transform);
            _product.transform.localPosition = new Vector3(0, 0.7f, 0);

            var box = GameObject.CreatePrimitive(PrimitiveType.Cube);
            box.name = "Box";
            box.transform.SetParent(_product.transform);
            box.transform.localPosition = Vector3.zero;
            box.transform.localScale = new Vector3(1.2f, 1f, 1.2f);
            Destroy(box.GetComponent<Collider>());
            _productMat = new Material(shader) { color = SSColorPalette.Accent };
            box.GetComponent<Renderer>().material = _productMat;

            // Stripe on box
            var stripe = GameObject.CreatePrimitive(PrimitiveType.Cube);
            stripe.name = "Stripe";
            stripe.transform.SetParent(_product.transform);
            stripe.transform.localPosition = new Vector3(0, 0, -0.61f);
            stripe.transform.localScale = new Vector3(1.0f, 0.25f, 0.02f);
            Destroy(stripe.GetComponent<Collider>());
            stripe.GetComponent<Renderer>().material =
                new Material(shader) { color = SSColorPalette.Primary };

            // Star on top
            var star = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            star.name = "Star";
            star.transform.SetParent(_product.transform);
            star.transform.localPosition = new Vector3(0, 0.55f, 0);
            star.transform.localScale = Vector3.one * 0.25f;
            Destroy(star.GetComponent<Collider>());
            star.GetComponent<Renderer>().material =
                new Material(shader) { color = Color.white };

            _currentZ = 0;
        }

        // ── Movement ─────────────────────────────────────────────────

        public IEnumerator MoveForward(float dist)
        {
            _currentZ += dist;
            SpinRollers(true);
            DOTween.Kill(_product.transform, "prodMove");
            yield return _product.transform
                .DOLocalMoveZ(_currentZ, 0.6f)
                .SetEase(Ease.OutBack).SetId("prodMove")
                .WaitForCompletion();

            _product.transform.DOPunchScale(Vector3.one * 0.12f, 0.3f, 6, 0.3f);
            if (_productMat != null)
            {
                DOTween.Kill(_productMat, "prodColor");
                _productMat.DOColor(SSColorPalette.Success, 0.12f).SetId("prodColor")
                    .OnComplete(() => _productMat.DOColor(SSColorPalette.Accent, 0.3f));
            }
        }

        public IEnumerator MoveBackward(float dist)
        {
            _currentZ = Mathf.Max(0, _currentZ - dist);
            SpinRollers(false);

            DOTween.Kill(_product.transform, "prodMove");
            var seq = DOTween.Sequence().SetId("prodMove");
            seq.Append(_product.transform.DOShakePosition(0.3f, 0.08f, 15));
            seq.Append(_product.transform
                .DOLocalMoveZ(_currentZ, 0.5f).SetEase(Ease.OutQuad));

            if (_productMat != null)
            {
                DOTween.Kill(_productMat, "prodColor");
                _productMat.DOColor(SSColorPalette.Danger, 0.12f).SetId("prodColor")
                    .OnComplete(() => _productMat.DOColor(SSColorPalette.Accent, 0.3f));
            }
            yield return seq.WaitForCompletion();
        }

        public IEnumerator ProductComplete()
        {
            DOTween.Kill(_product.transform);
            var seq = DOTween.Sequence();
            seq.Append(_product.transform
                .DOLocalMoveY(3f, 0.6f).SetEase(Ease.OutQuad));
            seq.Join(_product.transform
                .DOScale(1.5f, 0.4f).SetEase(Ease.OutBack));
            seq.Join(_product.transform
                .DOLocalRotate(new Vector3(0, 360, 0), 0.8f, RotateMode.FastBeyond360)
                .SetEase(Ease.InOutQuad));
            yield return seq.WaitForCompletion();

            var particles = SSParticleManager.Instance;
            if (particles != null)
                particles.PlayStarBurst(_product.transform.position);
        }

        public void ResetLine()
        {
            _currentZ = 0;
            if (_product != null)
            {
                DOTween.Kill(_product.transform);
                _product.transform.localPosition = new Vector3(0, 0.7f, 0);
                _product.transform.localScale = Vector3.one;
                _product.transform.localRotation = Quaternion.identity;
            }
        }

        private void SpinRollers(bool forward)
        {
            if (_rollers == null) return;
            float angle = forward ? 360 : -360;
            foreach (var r in _rollers)
            {
                if (r == null) continue;
                DOTween.Kill(r.transform, "rollerSpin");
                r.transform.DOLocalRotate(
                    r.transform.localEulerAngles + new Vector3(angle, 0, 0),
                    0.5f, RotateMode.FastBeyond360)
                    .SetEase(Ease.OutQuad).SetId("rollerSpin");
            }
        }

        private Shader GetShader() =>
            Shader.Find("Universal Render Pipeline/Lit")
            ?? Shader.Find("Standard") ?? Shader.Find("Unlit/Color");
    }
}
```

---

### `ScienceStreet/TF/Scripts/TfQuestionScreen.cs`

> Lines: 266 | Size: 13.3KB

```csharp
using UnityEngine;
using DG.Tweening;
using ALArcade.ArabicTMP;

namespace com.al_arcade.tf
{
    using shared;

    public class TfQuestionScreen : MonoBehaviour
    {
        private ArabicTextMeshPro _questionText;
        private ArabicTextMeshPro _sourceText;
        private ArabicTextMeshPro _feedbackText;
        private Material _screenMat;
        private Material _feedbackMat;
        private GameObject _feedbackPanel;

        /// <summary>
        /// Build screen at position, rotated to face cameraPos.
        /// </summary>
        public void Build(Vector3 position, Vector3 cameraPos)
        {
            transform.position = position;
            var shader = GetShader();

            // ── Frame ────────────────────────────────────────────────
            var frame = GameObject.CreatePrimitive(PrimitiveType.Cube);
            frame.name = "Frame";
            frame.transform.SetParent(transform);
            frame.transform.localPosition = Vector3.zero;
            frame.transform.localScale = new Vector3(7f, 3.5f, 0.2f);
            Destroy(frame.GetComponent<Collider>());
            frame.GetComponent<Renderer>().material =
                new Material(shader) { color = new Color32(25, 25, 35, 255) };

            // ── Screen border highlight ──────────────────────────────
            var borderGlow = GameObject.CreatePrimitive(PrimitiveType.Cube);
            borderGlow.name = "BorderGlow";
            borderGlow.transform.SetParent(transform);
            borderGlow.transform.localPosition = new Vector3(0, 0, 0.01f);
            borderGlow.transform.localScale = new Vector3(7.15f, 3.65f, 0.18f);
            Destroy(borderGlow.GetComponent<Collider>());
            borderGlow.GetComponent<Renderer>().material =
                new Material(shader) { color = SSColorPalette.WithAlpha(SSColorPalette.Primary, 0.3f) };

            // ── Screen surface ───────────────────────────────────────
            var surface = GameObject.CreatePrimitive(PrimitiveType.Cube);
            surface.name = "Surface";
            surface.transform.SetParent(transform);
            surface.transform.localPosition = new Vector3(0, 0, -0.12f);
            surface.transform.localScale = new Vector3(6.6f, 3.1f, 0.02f);
            Destroy(surface.GetComponent<Collider>());
            _screenMat = new Material(shader) { color = new Color32(15, 15, 50, 255) };
            surface.GetComponent<Renderer>().material = _screenMat;

            // ── Pole ─────────────────────────────────────────────────
            var pole = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
            pole.name = "Pole";
            pole.transform.SetParent(transform);
            pole.transform.localPosition = new Vector3(0, -3f, 0);
            pole.transform.localScale = new Vector3(0.12f, 1.8f, 0.12f);
            Destroy(pole.GetComponent<Collider>());
            pole.GetComponent<Renderer>().material =
                new Material(shader) { color = new Color32(70, 70, 85, 255) };

            // ── Pole base ────────────────────────────────────────────
            var poleBase = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
            poleBase.name = "PoleBase";
            poleBase.transform.SetParent(transform);
            poleBase.transform.localPosition = new Vector3(0, -4.7f, 0);
            poleBase.transform.localScale = new Vector3(0.5f, 0.05f, 0.5f);
            Destroy(poleBase.GetComponent<Collider>());
            poleBase.GetComponent<Renderer>().material =
                new Material(shader) { color = new Color32(60, 60, 75, 255) };

            // ── Question text ★ z = -0.25 (pushed out toward camera) ─
            var textObj = new GameObject("QuestionText");
            textObj.transform.SetParent(transform);
            textObj.transform.localPosition = new Vector3(0, 0.25f, -0.25f);
            textObj.transform.localRotation = Quaternion.identity;

            _questionText = textObj.AddComponent<ArabicTextMeshPro>();
            _questionText.alignment = TMPro.TextAlignmentOptions.Center;
            _questionText.color = Color.white;
            _questionText.fontStyle = TMPro.FontStyles.Bold;
            _questionText.enableAutoSizing = true;
            // ★ Sensible font range for a 7-unit-wide screen at ~12m distance
            _questionText.fontSizeMin = 1.8f;
            _questionText.fontSizeMax = 3.8f;
            _questionText.rectTransform.sizeDelta = new Vector2(6.2f, 2.2f);
            _questionText.enableWordWrapping = true;
            _questionText.overflowMode = TMPro.TextOverflowModes.Ellipsis;
            SSFontManager.Apply(_questionText);

            // ── Source text ──────────────────────────────────────────
            var srcObj = new GameObject("SourceText");
            srcObj.transform.SetParent(transform);
            srcObj.transform.localPosition = new Vector3(0, -1.15f, -0.25f);
            srcObj.transform.localRotation = Quaternion.identity;

            _sourceText = srcObj.AddComponent<ArabicTextMeshPro>();
            _sourceText.alignment = TMPro.TextAlignmentOptions.Center;
            _sourceText.color = SSColorPalette.WithAlpha(Color.white, 0.45f);
            _sourceText.fontSize = 1.5f;
            _sourceText.rectTransform.sizeDelta = new Vector2(6f, 0.5f);
            SSFontManager.Apply(_sourceText);

            // ── Feedback panel (overlays question) ───────────────────
            _feedbackPanel = GameObject.CreatePrimitive(PrimitiveType.Cube);
            _feedbackPanel.name = "FeedbackPanel";
            _feedbackPanel.transform.SetParent(transform);
            _feedbackPanel.transform.localPosition = new Vector3(0, 0, -0.2f);
            _feedbackPanel.transform.localScale = new Vector3(6.6f, 3.1f, 0.02f);
            Destroy(_feedbackPanel.GetComponent<Collider>());
            _feedbackMat = new Material(shader)
            {
                color = SSColorPalette.WithAlpha(SSColorPalette.Success, 0f)
            };
            _feedbackPanel.GetComponent<Renderer>().material = _feedbackMat;

            var fbObj = new GameObject("FeedbackText");
            fbObj.transform.SetParent(_feedbackPanel.transform);
            fbObj.transform.localPosition = new Vector3(0, 0, -0.15f);
            fbObj.transform.localRotation = Quaternion.identity;
            // ★ Scale container so feedback text isn't giant
            // FeedbackPanel is 6.6 wide but we want text around 3 units
            fbObj.transform.localScale = new Vector3(0.5f, 0.5f, 1f);

            _feedbackText = fbObj.AddComponent<ArabicTextMeshPro>();
            _feedbackText.alignment = TMPro.TextAlignmentOptions.Center;
            _feedbackText.color = Color.white;
            // ★ Moderate feedback font size
            _feedbackText.fontSize = 4f;
            _feedbackText.fontSizeMin = 2.5f;
            _feedbackText.fontSizeMax = 5f;
            _feedbackText.enableAutoSizing = true;
            _feedbackText.fontStyle = TMPro.FontStyles.Bold;
            _feedbackText.rectTransform.sizeDelta = new Vector2(12f, 5f);
            _feedbackText.enableWordWrapping = true;
            _feedbackText.arabicText = "";
            SSFontManager.Apply(_feedbackText);

            // ── Accent strips (top + bottom) ─────────────────────────
            var topStrip = GameObject.CreatePrimitive(PrimitiveType.Cube);
            topStrip.name = "TopStrip";
            topStrip.transform.SetParent(transform);
            topStrip.transform.localPosition = new Vector3(0, 1.8f, -0.12f);
            topStrip.transform.localScale = new Vector3(6.6f, 0.06f, 0.05f);
            Destroy(topStrip.GetComponent<Collider>());
            topStrip.GetComponent<Renderer>().material =
                new Material(shader) { color = SSColorPalette.Accent };

            var botStrip = GameObject.CreatePrimitive(PrimitiveType.Cube);
            botStrip.name = "BotStrip";
            botStrip.transform.SetParent(transform);
            botStrip.transform.localPosition = new Vector3(0, -1.8f, -0.12f);
            botStrip.transform.localScale = new Vector3(6.6f, 0.04f, 0.05f);
            Destroy(botStrip.GetComponent<Collider>());
            botStrip.GetComponent<Renderer>().material =
                new Material(shader) { color = SSColorPalette.WithAlpha(SSColorPalette.Accent, 0.5f) };

            // ── Status indicator lights ──────────────────────────────
            for (int i = -1; i <= 1; i += 2)
            {
                var light = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                light.name = "StatusLight";
                light.transform.SetParent(frame.transform);
                light.transform.localPosition = new Vector3(i * 0.42f, -0.55f, -0.6f);
                light.transform.localScale = Vector3.one * 0.06f;
                Destroy(light.GetComponent<Collider>());
                light.GetComponent<Renderer>().material = new Material(shader)
                {
                    color = i < 0 ? SSColorPalette.TrueGreen : SSColorPalette.FalseRed
                };
            }

            // ★ Rotate entire screen so -Z faces camera
            Vector3 awayFromCamera = transform.position - cameraPos;
            awayFromCamera.y = 0;
            if (awayFromCamera.sqrMagnitude > 0.01f)
                transform.rotation = Quaternion.LookRotation(awayFromCamera);
        }

        // ── Show Question ────────────────────────────────────────────

        public void ShowQuestion(string text, string source = "")
        {
            // Hide feedback
            if (_feedbackMat != null)
            {
                DOTween.Kill(_feedbackMat, "fbMat");
                _feedbackMat.color = new Color(0, 0, 0, 0);
            }
            if (_feedbackText != null) _feedbackText.arabicText = "";

            if (_questionText != null)
            {
                _questionText.arabicText = text;
                DOTween.Kill(_questionText.transform, "qScale");
                _questionText.transform.localScale = Vector3.one * 0.6f;
                _questionText.DOFade(0, 0);
                _questionText.DOFade(1, 0.25f);
                _questionText.transform.DOScale(1f, 0.4f)
                    .SetEase(Ease.OutBack).SetId("qScale");
            }

            if (_sourceText != null)
                _sourceText.arabicText = !string.IsNullOrEmpty(source) ? $"📖 {source}" : "";

            // Screen flash effect
            if (_screenMat != null)
            {
                DOTween.Kill(_screenMat, "screenFlash");
                _screenMat
                    .DOColor(SSColorPalette.WithAlpha(SSColorPalette.Primary, 0.7f), 0.12f)
                    .SetId("screenFlash")
                    .OnComplete(() =>
                        _screenMat.DOColor(new Color32(15, 15, 50, 255), 0.35f));
            }
        }

        // ── Show Feedback ────────────────────────────────────────────

        public void ShowFeedback(bool correct, int streak)
        {
            if (_feedbackText != null)
            {
                _feedbackText.arabicText = correct
                    ? (streak >= 3 ? $"🔥 ×{streak} ممتاز!" : "صحيح! ✓")
                    : "خطأ! ✗";

                DOTween.Kill(_feedbackText.transform, "fbScale");
                _feedbackText.transform.localScale = new Vector3(0.3f, 0.3f, 1f);
                _feedbackText.transform
                    .DOScale(new Vector3(0.5f, 0.5f, 1f), 0.3f)
                    .SetEase(Ease.OutBack).SetId("fbScale");
            }

            if (_feedbackMat != null)
            {
                DOTween.Kill(_feedbackMat, "fbMat");
                _feedbackMat.color = new Color(0, 0, 0, 0);
                Color target = SSColorPalette.WithAlpha(
                    correct ? SSColorPalette.Success : SSColorPalette.Danger, 0.82f);
                _feedbackMat.DOColor(target, 0.15f).SetId("fbMat");
            }
        }

        // ── Clear ────────────────────────────────────────────────────

        public void Clear()
        {
            if (_questionText != null) _questionText.arabicText = "";
            if (_sourceText != null) _sourceText.arabicText = "";
            if (_feedbackText != null) _feedbackText.arabicText = "";
            if (_feedbackMat != null)
            {
                DOTween.Kill(_feedbackMat, "fbMat");
                _feedbackMat.color = new Color(0, 0, 0, 0);
            }
        }

        private Shader GetShader() =>
            Shader.Find("Universal Render Pipeline/Lit")
            ?? Shader.Find("Standard") ?? Shader.Find("Unlit/Color");
    }
}
```

---

### `ScienceStreet/TF/Scripts/TfUIManager.cs`

> Lines: 393 | Size: 17.8KB

```csharp
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using DG.Tweening;
using ALArcade.ArabicTMP;

namespace com.al_arcade.tf
{
    using shared;

    public class TfUIManager : MonoBehaviour
    {
        private Canvas _canvas;
        private CanvasGroup _gameUI, _loadingUI, _errorUI, _resultsUI;
        private ArabicTextMeshProUGUI _scoreText, _streakText;
        private ArabicTextMeshProUGUI _loadingText, _errorText;
        private Image _progressFill;
        private ArabicTextMeshProUGUI _progressLabel;
        private ArabicTextMeshProUGUI _resultTitle, _resultScore, _resultStats;

        [Header("Events")]
        public UnityEvent onRestartClicked;

        // ════════════════════════════════════════════════════════════
        // BUILD — all sizes tuned for 1920×1080 reference
        // ════════════════════════════════════════════════════════════

        public void BuildUI()
        {
            var co = new GameObject("TfCanvas");
            co.transform.SetParent(transform);
            _canvas = co.AddComponent<Canvas>();
            _canvas.renderMode = RenderMode.ScreenSpaceOverlay;
            _canvas.sortingOrder = 100;

            var sc = co.AddComponent<CanvasScaler>();
            sc.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
            sc.referenceResolution = new Vector2(1920, 1080);
            sc.matchWidthOrHeight = 0.5f;
            co.AddComponent<GraphicRaycaster>();

            BuildGameHUD(co.transform);
            BuildLoadingPanel(co.transform);
            BuildErrorPanel(co.transform);
            BuildResultsPanel(co.transform);

            _gameUI.alpha = 0; _gameUI.gameObject.SetActive(false);
            _loadingUI.alpha = 0; _loadingUI.gameObject.SetActive(false);
            _errorUI.alpha = 0; _errorUI.gameObject.SetActive(false);
            _resultsUI.alpha = 0; _resultsUI.gameObject.SetActive(false);
        }

        // ── Game HUD ─────────────────────────────────────────────────

        private void BuildGameHUD(Transform parent)
        {
            var go = MkPanel(parent, "GameUI");
            _gameUI = go.AddComponent<CanvasGroup>();

            // ── Top bar ──────────────────────────────────────────────
            var topBarObj = new GameObject("TopBar");
            topBarObj.transform.SetParent(go.transform, false);
            var tbr = topBarObj.AddComponent<RectTransform>();
            tbr.anchorMin = new Vector2(0, 1);
            tbr.anchorMax = new Vector2(1, 1);
            tbr.pivot = new Vector2(0.5f, 1);
            tbr.anchoredPosition = Vector2.zero;
            tbr.sizeDelta = new Vector2(0, 72);
            topBarObj.AddComponent<Image>().color =
                SSColorPalette.WithAlpha(SSColorPalette.PrimaryDark, 0.82f);

            // Score — top-left
            _scoreText = MkTxt(topBarObj.transform, "Score", "0",
                32, new Vector2(0, 0.5f), new Vector2(36, 6), new Vector2(180, 44));
            _scoreText.color = SSColorPalette.Accent;
            _scoreText.fontStyle = TMPro.FontStyles.Bold;
            _scoreText.alignment = TMPro.TextAlignmentOptions.MidlineLeft;

            // Score label
            var scoreLbl = MkTxt(topBarObj.transform, "ScoreLbl", "النقاط",
                14, new Vector2(0, 0.5f), new Vector2(36, -20), new Vector2(100, 22));
            scoreLbl.color = SSColorPalette.WithAlpha(Color.white, 0.5f);
            scoreLbl.alignment = TMPro.TextAlignmentOptions.MidlineLeft;

            // Streak — center
            _streakText = MkTxt(topBarObj.transform, "Streak", "",
                26, new Vector2(0.5f, 0.5f), new Vector2(0, 0), new Vector2(200, 44));
            _streakText.color = SSColorPalette.Warning;
            _streakText.alignment = TMPro.TextAlignmentOptions.Center;

            // ── Progress bar ─────────────────────────────────────────
            var progBg = new GameObject("ProgressBg");
            progBg.transform.SetParent(go.transform, false);
            var pr = progBg.AddComponent<RectTransform>();
            pr.anchorMin = new Vector2(0.28f, 1);
            pr.anchorMax = new Vector2(0.72f, 1);
            pr.pivot = new Vector2(0.5f, 1);
            pr.anchoredPosition = new Vector2(0, -78);
            pr.sizeDelta = new Vector2(0, 14);
            var progBgImg = progBg.AddComponent<Image>();
            progBgImg.color = SSColorPalette.WithAlpha(Color.white, 0.08f);

            var fill = new GameObject("Fill");
            fill.transform.SetParent(progBg.transform, false);
            var fr = fill.AddComponent<RectTransform>();
            fr.anchorMin = Vector2.zero;
            fr.anchorMax = new Vector2(0, 1);
            fr.pivot = new Vector2(0, 0.5f);
            fr.offsetMin = Vector2.zero;
            fr.offsetMax = Vector2.zero;
            _progressFill = fill.AddComponent<Image>();
            _progressFill.color = SSColorPalette.Accent;

            // Progress label
            _progressLabel = MkTxt(go.transform, "ProgLbl", "0 / 5",
                16, new Vector2(0.5f, 1), new Vector2(0, -98), new Vector2(200, 22));
            _progressLabel.alignment = TMPro.TextAlignmentOptions.Center;
            _progressLabel.color = SSColorPalette.WithAlpha(Color.white, 0.55f);

            // ── Bottom hint ──────────────────────────────────────────
            var hintObj = new GameObject("HintBg");
            hintObj.transform.SetParent(go.transform, false);
            var hbr = hintObj.AddComponent<RectTransform>();
            hbr.anchorMin = new Vector2(0.25f, 0);
            hbr.anchorMax = new Vector2(0.75f, 0);
            hbr.pivot = new Vector2(0.5f, 0);
            hbr.anchoredPosition = new Vector2(0, 20);
            hbr.sizeDelta = new Vector2(0, 44);
            var hintBgImg = hintObj.AddComponent<Image>();
            hintBgImg.color = SSColorPalette.WithAlpha(SSColorPalette.PrimaryDark, 0.6f);

            var hint = MkTxt(hintObj.transform, "Hint",
                "← صح (يسار)  |  خطأ (يمين) →",
                18, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(480, 36));
            hint.alignment = TMPro.TextAlignmentOptions.Center;
            hint.color = SSColorPalette.WithAlpha(Color.white, 0.5f);
        }

        // ── Loading ──────────────────────────────────────────────────

        private void BuildLoadingPanel(Transform parent)
        {
            var go = MkPanel(parent, "LoadingUI");
            _loadingUI = go.AddComponent<CanvasGroup>();
            go.AddComponent<Image>().color =
                SSColorPalette.WithAlpha(SSColorPalette.PrimaryDark, 0.9f);

            _loadingText = MkTxt(go.transform, "Txt", "جاري التحميل...",
                28, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(500, 50));
            _loadingText.alignment = TMPro.TextAlignmentOptions.Center;
            _loadingText.color = Color.white;

            // Spinner dots
            var dots = MkTxt(go.transform, "Dots", "● ● ●",
                18, new Vector2(0.5f, 0.5f), new Vector2(0, -36), new Vector2(200, 30));
            dots.alignment = TMPro.TextAlignmentOptions.Center;
            dots.color = SSColorPalette.Accent;
        }

        // ── Error ────────────────────────────────────────────────────

        private void BuildErrorPanel(Transform parent)
        {
            var go = MkPanel(parent, "ErrorUI");
            _errorUI = go.AddComponent<CanvasGroup>();
            go.AddComponent<Image>().color =
                SSColorPalette.WithAlpha(SSColorPalette.Danger, 0.9f);

            _errorText = MkTxt(go.transform, "Txt", "خطأ",
                24, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(600, 90));
            _errorText.alignment = TMPro.TextAlignmentOptions.Center;
            _errorText.color = Color.white;
            _errorText.enableWordWrapping = true;
        }

        // ── Results ──────────────────────────────────────────────────

        private void BuildResultsPanel(Transform parent)
        {
            var go = MkPanel(parent, "ResultsUI");
            _resultsUI = go.AddComponent<CanvasGroup>();

            // Semi-transparent background
            var bg = go.AddComponent<Image>();
            bg.color = SSColorPalette.WithAlpha(SSColorPalette.PrimaryDark, 0.94f);

            // Card panel (centered box)
            var card = new GameObject("Card");
            card.transform.SetParent(go.transform, false);
            var cr = card.AddComponent<RectTransform>();
            cr.anchorMin = new Vector2(0.25f, 0.15f);
            cr.anchorMax = new Vector2(0.75f, 0.85f);
            cr.offsetMin = cr.offsetMax = Vector2.zero;
            var cardImg = card.AddComponent<Image>();
            cardImg.color = SSColorPalette.WithAlpha(new Color32(30, 30, 55, 255), 0.95f);

            // Title
            _resultTitle = MkTxt(card.transform, "Title", "تم التصنيع! 🎉",
                42, new Vector2(0.5f, 1), new Vector2(0, -50), new Vector2(500, 55));
            _resultTitle.alignment = TMPro.TextAlignmentOptions.Center;
            _resultTitle.color = SSColorPalette.Accent;
            _resultTitle.fontStyle = TMPro.FontStyles.Bold;

            // Score
            _resultScore = MkTxt(card.transform, "Score", "0",
                56, new Vector2(0.5f, 1), new Vector2(0, -130), new Vector2(300, 70));
            _resultScore.alignment = TMPro.TextAlignmentOptions.Center;
            _resultScore.color = Color.white;
            _resultScore.fontStyle = TMPro.FontStyles.Bold;

            // Score label
            MkTxt(card.transform, "ScoreLbl", "النقاط",
                16, new Vector2(0.5f, 1), new Vector2(0, -185), new Vector2(100, 25))
                .color = SSColorPalette.WithAlpha(Color.white, 0.4f);

            // Stats
            _resultStats = MkTxt(card.transform, "Stats", "",
                20, new Vector2(0.5f, 0.5f), new Vector2(0, 10), new Vector2(450, 60));
            _resultStats.alignment = TMPro.TextAlignmentOptions.Center;
            _resultStats.color = SSColorPalette.WithAlpha(Color.white, 0.65f);
            _resultStats.enableWordWrapping = true;

            // Restart button
            var btnObj = new GameObject("RestartBtn");
            btnObj.transform.SetParent(card.transform, false);
            var br = btnObj.AddComponent<RectTransform>();
            br.anchorMin = new Vector2(0.5f, 0);
            br.anchorMax = new Vector2(0.5f, 0);
            br.pivot = new Vector2(0.5f, 0);
            br.anchoredPosition = new Vector2(0, 40);
            br.sizeDelta = new Vector2(260, 52);

            var btnImg = btnObj.AddComponent<Image>();
            btnImg.color = SSColorPalette.Accent;

            var btn = btnObj.AddComponent<Button>();
            btn.onClick.AddListener(() =>
            {
                _resultsUI.DOFade(0, 0.3f)
                    .OnComplete(() => _resultsUI.gameObject.SetActive(false));
                onRestartClicked?.Invoke();
            });

            var btnTxt = MkTxt(btnObj.transform, "BtnTxt", "إعادة اللعب 🔄",
                22, Vector2.zero, Vector2.zero, Vector2.zero);
            btnTxt.rectTransform.anchorMin = Vector2.zero;
            btnTxt.rectTransform.anchorMax = Vector2.one;
            btnTxt.rectTransform.offsetMin = btnTxt.rectTransform.offsetMax = Vector2.zero;
            btnTxt.alignment = TMPro.TextAlignmentOptions.Center;
            btnTxt.color = SSColorPalette.TextDark;
            btnTxt.fontStyle = TMPro.FontStyles.Bold;
        }

        // ════════════════════════════════════════════════════════════
        // PUBLIC METHODS
        // ════════════════════════════════════════════════════════════

        public void ShowGameUI()
        {
            _gameUI.gameObject.SetActive(true);
            _gameUI.DOFade(1, 0.3f);
        }

        public void SetScore(int s)
        {
            if (_scoreText == null) return;
            _scoreText.arabicText = s.ToString("N0");
            DOTween.Kill(_scoreText.transform, "sp");
            _scoreText.transform.DOPunchScale(Vector3.one * 0.12f, 0.2f, 4, 0.2f).SetId("sp");
        }

        public void SetStreak(int s)
        {
            if (_streakText == null) return;
            _streakText.arabicText = s > 1 ? $"🔥 ×{s}" : "";
            if (s > 1)
            {
                DOTween.Kill(_streakText.transform, "sk");
                _streakText.transform.localScale = Vector3.one * 1.4f;
                _streakText.transform.DOScale(1f, 0.35f).SetEase(Ease.OutElastic).SetId("sk");
            }
        }

        public void SetProgress(int cur, int total)
        {
            if (_progressLabel != null) _progressLabel.arabicText = $"{cur} / {total}";
            if (_progressFill != null && total > 0)
            {
                DOTween.Kill(_progressFill.rectTransform, "pf");
                _progressFill.rectTransform
                    .DOAnchorMax(new Vector2((float)cur / total, 1), 0.4f)
                    .SetEase(Ease.OutQuad).SetId("pf");
            }
        }

        public void ShowLoading(string m)
        {
            _loadingUI.gameObject.SetActive(true);
            if (_loadingText != null) _loadingText.arabicText = m;
            _loadingUI.DOFade(1, 0.3f);
        }

        public void HideLoading()
        {
            _loadingUI.DOFade(0, 0.3f)
                .OnComplete(() => _loadingUI.gameObject.SetActive(false));
        }

        public void ShowError(string m)
        {
            _errorUI.gameObject.SetActive(true);
            if (_errorText != null) _errorText.arabicText = m;
            _errorUI.DOFade(1, 0.3f);
        }

        public void ShowResults(int score, int correct, int wrong, bool won)
        {
            _resultsUI.gameObject.SetActive(true);
            _resultsUI.alpha = 0;

            if (_resultTitle != null)
                _resultTitle.arabicText = won ? "تم التصنيع بنجاح! 🎉" : "حظ أوفر! 😔";
            if (_resultScore != null)
                _resultScore.arabicText = score.ToString("N0");
            if (_resultStats != null)
                _resultStats.arabicText = $"✓ صحيح: {correct}  |  ✗ خطأ: {wrong}";

            // Animated entrance
            var seq = DOTween.Sequence();
            seq.Append(_resultsUI.DOFade(1, 0.4f));
            if (_resultTitle != null)
            {
                _resultTitle.transform.localScale = Vector3.zero;
                seq.Append(_resultTitle.transform
                    .DOScale(1, 0.45f).SetEase(Ease.OutBack));
            }
            if (_resultScore != null)
            {
                _resultScore.transform.localScale = Vector3.zero;
                seq.Append(_resultScore.transform
                    .DOScale(1, 0.35f).SetEase(Ease.OutBack));

                // Count-up
                int ds = 0;
                DOTween.To(() => ds, x =>
                {
                    ds = x;
                    if (_resultScore != null) _resultScore.arabicText = x.ToString("N0");
                }, score, 1f).SetEase(Ease.OutQuad).SetDelay(0.7f);
            }
        }

        public void ResetUI()
        {
            if (_gameUI != null) _gameUI.gameObject.SetActive(false);
            if (_loadingUI != null) _loadingUI.gameObject.SetActive(false);
            if (_errorUI != null) _errorUI.gameObject.SetActive(false);
            if (_resultsUI != null) _resultsUI.gameObject.SetActive(false);
        }

        // ════════════════════════════════════════════════════════════
        // HELPERS
        // ════════════════════════════════════════════════════════════

        private GameObject MkPanel(Transform p, string n)
        {
            var go = new GameObject(n);
            go.transform.SetParent(p, false);
            var r = go.AddComponent<RectTransform>();
            r.anchorMin = Vector2.zero;
            r.anchorMax = Vector2.one;
            r.sizeDelta = Vector2.zero;
            return go;
        }

        private ArabicTextMeshProUGUI MkTxt(Transform p, string n, string text,
            float size, Vector2 anchor, Vector2 pos, Vector2 sz)
        {
            var go = new GameObject(n);
            go.transform.SetParent(p, false);
            var r = go.AddComponent<RectTransform>();
            r.anchorMin = r.anchorMax = anchor;
            r.pivot = anchor;
            r.anchoredPosition = pos;
            r.sizeDelta = sz;
            var t = go.AddComponent<ArabicTextMeshProUGUI>();
            t.arabicText = text;
            t.fontSize = size;
            t.enableWordWrapping = true;
            SSFontManager.Apply(t);
            return t;
        }
    }
}
```

---

### `TextMesh Pro/Fonts/LiberationSans - OFL.txt`

> Lines: 45 | Size: 4.4KB

```text
Digitized data copyright (c) 2010 Google Corporation
	with Reserved Font Arimo, Tinos and Cousine.
Copyright (c) 2012 Red Hat, Inc.
	with Reserved Font Name Liberation.

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL

-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.

The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the copyright statement(s).

"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.

"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.

5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
```

---

### `TextMesh Pro/Resources/LineBreaking Following Characters.txt`

> Lines: 0 | Size: 269B

```text
﻿)]｝〕〉》」』】〙〗〟’”｠»ヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻‐゠–〜?!‼⁇⁈⁉・、%,.:;。！？］）：；＝}¢°"†‡℃〆％，．
```

---

### `TextMesh Pro/Resources/LineBreaking Leading Characters.txt`

> Lines: 0 | Size: 95B

```text
﻿([｛〔〈《「『【〘〖〝‘“｟«$—…‥〳〴〵\［（{£¥"々〇＄￥￦ #
```

---

### `TextMesh Pro/Sprites/EmojiOne Attribution.txt`

> Lines: 2 | Size: 185B

```text
This sample of beautiful emojis are provided by EmojiOne https://www.emojione.com/

Please visit their website to view the complete set of their emojis and review their licensing terms.
```

---

### `TextMesh Pro/Sprites/EmojiOne.json`

> Lines: 156 | Size: 3.9KB

```json
{"frames": [

{
	"filename": "1f60a.png",
	"frame": {"x":0,"y":0,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "1f60b.png",
	"frame": {"x":128,"y":0,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "1f60d.png",
	"frame": {"x":256,"y":0,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "1f60e.png",
	"frame": {"x":384,"y":0,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "1f600.png",
	"frame": {"x":0,"y":128,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "1f601.png",
	"frame": {"x":128,"y":128,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "1f602.png",
	"frame": {"x":256,"y":128,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "1f603.png",
	"frame": {"x":384,"y":128,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "1f604.png",
	"frame": {"x":0,"y":256,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "1f605.png",
	"frame": {"x":128,"y":256,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "1f606.png",
	"frame": {"x":256,"y":256,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "1f609.png",
	"frame": {"x":384,"y":256,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "1f618.png",
	"frame": {"x":0,"y":384,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "1f923.png",
	"frame": {"x":128,"y":384,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "263a.png",
	"frame": {"x":256,"y":384,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
},
{
	"filename": "2639.png",
	"frame": {"x":384,"y":384,"w":128,"h":128},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":128,"h":128},
	"sourceSize": {"w":128,"h":128},
	"pivot": {"x":0.5,"y":0.5}
}],
"meta": {
	"app": "http://www.codeandweb.com/texturepacker",
	"version": "1.0",
	"image": "EmojiOne.png",
	"format": "RGBA8888",
	"size": {"w":512,"h":512},
	"scale": "1",
	"smartupdate": "$TexturePacker:SmartUpdate:196a26a2e149d875b91ffc8fa3581e76:fc928c7e275404b7e0649307410475cb:424723c3774975ddb2053fd5c4b85f6e$"
}
}

```

---

### `TutorialInfo/Scripts/Editor/ReadmeEditor.cs`

> Lines: 242 | Size: 6.5KB

```csharp
﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Reflection;

[CustomEditor(typeof(Readme))]
[InitializeOnLoad]
public class ReadmeEditor : Editor
{
    static string s_ShowedReadmeSessionStateName = "ReadmeEditor.showedReadme";
    
    static string s_ReadmeSourceDirectory = "Assets/TutorialInfo";

    const float k_Space = 16f;

    static ReadmeEditor()
    {
        EditorApplication.delayCall += SelectReadmeAutomatically;
    }

    static void RemoveTutorial()
    {
        if (EditorUtility.DisplayDialog("Remove Readme Assets",
            
            $"All contents under {s_ReadmeSourceDirectory} will be removed, are you sure you want to proceed?",
            "Proceed",
            "Cancel"))
        {
            if (Directory.Exists(s_ReadmeSourceDirectory))
            {
                FileUtil.DeleteFileOrDirectory(s_ReadmeSourceDirectory);
                FileUtil.DeleteFileOrDirectory(s_ReadmeSourceDirectory + ".meta");
            }
            else
            {
                Debug.Log($"Could not find the Readme folder at {s_ReadmeSourceDirectory}");
            }

            var readmeAsset = SelectReadme();
            if (readmeAsset != null)
            {
                var path = AssetDatabase.GetAssetPath(readmeAsset);
                FileUtil.DeleteFileOrDirectory(path + ".meta");
                FileUtil.DeleteFileOrDirectory(path);
            }

            AssetDatabase.Refresh();
        }
    }

    static void SelectReadmeAutomatically()
    {
        if (!SessionState.GetBool(s_ShowedReadmeSessionStateName, false))
        {
            var readme = SelectReadme();
            SessionState.SetBool(s_ShowedReadmeSessionStateName, true);

            if (readme && !readme.loadedLayout)
            {
                LoadLayout();
                readme.loadedLayout = true;
            }
        }
    }

    static void LoadLayout()
    {
        var assembly = typeof(EditorApplication).Assembly;
        var windowLayoutType = assembly.GetType("UnityEditor.WindowLayout", true);
        var method = windowLayoutType.GetMethod("LoadWindowLayout", BindingFlags.Public | BindingFlags.Static);
        method.Invoke(null, new object[] { Path.Combine(Application.dataPath, "TutorialInfo/Layout.wlt"), false });
    }

    static Readme SelectReadme()
    {
        var ids = AssetDatabase.FindAssets("Readme t:Readme");
        if (ids.Length == 1)
        {
            var readmeObject = AssetDatabase.LoadMainAssetAtPath(AssetDatabase.GUIDToAssetPath(ids[0]));

            Selection.objects = new UnityEngine.Object[] { readmeObject };

            return (Readme)readmeObject;
        }
        else
        {
            Debug.Log("Couldn't find a readme");
            return null;
        }
    }

    protected override void OnHeaderGUI()
    {
        var readme = (Readme)target;
        Init();

        var iconWidth = Mathf.Min(EditorGUIUtility.currentViewWidth / 3f - 20f, 128f);

        GUILayout.BeginHorizontal("In BigTitle");
        {
            if (readme.icon != null)
            {
                GUILayout.Space(k_Space);
                GUILayout.Label(readme.icon, GUILayout.Width(iconWidth), GUILayout.Height(iconWidth));
            }
            GUILayout.Space(k_Space);
            GUILayout.BeginVertical();
            {

                GUILayout.FlexibleSpace();
                GUILayout.Label(readme.title, TitleStyle);
                GUILayout.FlexibleSpace();
            }
            GUILayout.EndVertical();
            GUILayout.FlexibleSpace();
        }
        GUILayout.EndHorizontal();
    }

    public override void OnInspectorGUI()
    {
        var readme = (Readme)target;
        Init();

        foreach (var section in readme.sections)
        {
            if (!string.IsNullOrEmpty(section.heading))
            {
                GUILayout.Label(section.heading, HeadingStyle);
            }

            if (!string.IsNullOrEmpty(section.text))
            {
                GUILayout.Label(section.text, BodyStyle);
            }

            if (!string.IsNullOrEmpty(section.linkText))
            {
                if (LinkLabel(new GUIContent(section.linkText)))
                {
                    Application.OpenURL(section.url);
                }
            }

            GUILayout.Space(k_Space);
        }

        if (GUILayout.Button("Remove Readme Assets", ButtonStyle))
        {
            RemoveTutorial();
        }
    }

    bool m_Initialized;

    GUIStyle LinkStyle
    {
        get { return m_LinkStyle; }
    }

    [SerializeField]
    GUIStyle m_LinkStyle;

    GUIStyle TitleStyle
    {
        get { return m_TitleStyle; }
    }

    [SerializeField]
    GUIStyle m_TitleStyle;

    GUIStyle HeadingStyle
    {
        get { return m_HeadingStyle; }
    }

    [SerializeField]
    GUIStyle m_HeadingStyle;

    GUIStyle BodyStyle
    {
        get { return m_BodyStyle; }
    }

    [SerializeField]
    GUIStyle m_BodyStyle;

    GUIStyle ButtonStyle
    {
        get { return m_ButtonStyle; }
    }

    [SerializeField]
    GUIStyle m_ButtonStyle;

    void Init()
    {
        if (m_Initialized)
            return;
        m_BodyStyle = new GUIStyle(EditorStyles.label);
        m_BodyStyle.wordWrap = true;
        m_BodyStyle.fontSize = 14;
        m_BodyStyle.richText = true;

        m_TitleStyle = new GUIStyle(m_BodyStyle);
        m_TitleStyle.fontSize = 26;

        m_HeadingStyle = new GUIStyle(m_BodyStyle);
        m_HeadingStyle.fontStyle = FontStyle.Bold;
        m_HeadingStyle.fontSize = 18;

        m_LinkStyle = new GUIStyle(m_BodyStyle);
        m_LinkStyle.wordWrap = false;

        // Match selection color which works nicely for both light and dark skins
        m_LinkStyle.normal.textColor = new Color(0x00 / 255f, 0x78 / 255f, 0xDA / 255f, 1f);
        m_LinkStyle.stretchWidth = false;

        m_ButtonStyle = new GUIStyle(EditorStyles.miniButton);
        m_ButtonStyle.fontStyle = FontStyle.Bold;

        m_Initialized = true;
    }

    bool LinkLabel(GUIContent label, params GUILayoutOption[] options)
    {
        var position = GUILayoutUtility.GetRect(label, LinkStyle, options);

        Handles.BeginGUI();
        Handles.color = LinkStyle.normal.textColor;
        Handles.DrawLine(new Vector3(position.xMin, position.yMax), new Vector3(position.xMax, position.yMax));
        Handles.color = Color.white;
        Handles.EndGUI();

        EditorGUIUtility.AddCursorRect(position, MouseCursor.Link);

        return GUI.Button(position, label, LinkStyle);
    }
}

```

---

### `TutorialInfo/Scripts/Readme.cs`

> Lines: 16 | Size: 302B

```csharp
﻿using System;
using UnityEngine;

public class Readme : ScriptableObject
{
    public Texture2D icon;
    public string title;
    public Section[] sections;
    public bool loadedLayout;

    [Serializable]
    public class Section
    {
        public string heading, text, linkText, url;
    }
}

```

---

### `collectcodebase.sh`

> Lines: 529 | Size: 16.6KB

```bash
#!/bin/bash

# ============================================================================
#  CODEBASE COLLECTOR v4 — macOS M1/ARM Safe
#  Drop into any project root, then run:   bash collect_codebase.sh
# ============================================================================

if [ -z "$BASH_VERSION" ]; then
    echo "ERROR: Run with:  bash $0"
    exit 1
fi

set -uo pipefail

# ── Config ──────────────────────────────────────────────────────────────────

OUTFILE="CODEBASE.md"
PROJECT="$(basename "$(pwd)")"
NOW="$(date "+%Y-%m-%d %H:%M:%S")"
MAX_KB=500

# Directories to skip (all levels)
SKIP_DIRS=(
    .git .svn .hg
    node_modules vendor
    venv .venv env .env
    __pycache__ .pytest_cache .mypy_cache .tox
    dist build .build DerivedData
    .next .nuxt .output .cache .parcel-cache
    coverage .nyc_output .terraform
    target Pods .cocoapods Carthage
    .gradle .idea .vscode .vs
    obj .sass-cache tmp .tmp logs
    xcuserdata .swiftpm .expo
    bower_components .serverless .amplify
    cdk.out .turbo .angular
)

# File name patterns to skip
SKIP_NAMES=(
    ".DS_Store"
    "Thumbs.db"
    "package-lock.json"
    "yarn.lock"
    "pnpm-lock.yaml"
    "Podfile.lock"
    "Gemfile.lock"
    "composer.lock"
    "Cargo.lock"
)

# Extensions to skip (binary / generated)
SKIP_EXT=(
    min.js min.css map
    pyc pyo class o a so dylib dll exe
    bin dat db sqlite sqlite3
    ico icns png jpg jpeg gif bmp svg webp avif
    mp3 mp4 wav avi mov mkv flac ogg
    pdf zip tar gz rar 7z bz2 xz
    woff woff2 ttf eot otf
    pb tfstate
    lock
)

# Extensions to include as code
CODE_EXT=(
    sh bash zsh fish
    html htm css scss sass less styl
    js jsx ts tsx mjs cjs
    vue svelte astro
    py pyi pyw pyx
    rb erb rake gemspec
    php phtml
    java kt kts scala groovy gradle
    c h cpp hpp cc cxx hxx hh
    m mm
    cs csx fs fsx vb
    swift rs go dart lua
    pl pm t
    r R Rmd
    hs lhs
    ex exs erl hrl
    clj cljs cljc edn
    zig nim v
    json jsonc json5
    yaml yml toml
    xml xsl xsd plist
    ini cfg conf properties
    md mdx rst txt adoc
    sql graphql gql proto
    tf tfvars hcl nix
    cmake mk csv
    gitignore dockerignore editorconfig
    env
)

# Exact filenames to always include
EXACT_NAMES=(
    Makefile makefile GNUmakefile CMakeLists.txt
    Dockerfile Containerfile Vagrantfile
    Rakefile Gemfile Procfile Brewfile
    Podfile Fastfile Dangerfile Jenkinsfile
    Justfile Taskfile Earthfile Caddyfile Tiltfile
    .gitignore .gitattributes .editorconfig
    .eslintrc .prettierrc .babelrc .nvmrc
    .ruby-version .python-version .tool-versions
    .env.example .env.sample .flake8 .pylintrc
    .swiftlint.yml .rubocop.yml
    setup.cfg setup.py pyproject.toml
    requirements.txt constraints.txt
    docker-compose.yml docker-compose.yaml
    tsconfig.json jsconfig.json
    webpack.config.js vite.config.ts vite.config.js
    rollup.config.js esbuild.config.js
    tailwind.config.js tailwind.config.ts
    postcss.config.js postcss.config.cjs
    next.config.js next.config.mjs next.config.ts
    nuxt.config.ts svelte.config.js astro.config.mjs
    angular.json nx.json turbo.json lerna.json
    jest.config.js jest.config.ts vitest.config.ts
    playwright.config.ts cypress.config.ts
    Cargo.toml go.mod go.sum
    Package.swift Package.resolved pubspec.yaml
    build.gradle settings.gradle pom.xml
    mix.exs rebar.config stack.yaml
    flake.nix shell.nix default.nix
    deno.json deno.jsonc bun.lockb
    nginx.conf project.pbxproj
)

# ── Temp files ──────────────────────────────────────────────────────────────

TMP_FILES="$(mktemp /tmp/cbc_files.XXXXXX)"
TMP_OUT="$(mktemp /tmp/cbc_out.XXXXXX)"
cleanup() { rm -f "$TMP_FILES" "$TMP_OUT"; }
trap cleanup EXIT

# ── Helpers ─────────────────────────────────────────────────────────────────

get_ext() {
    local name="$1"
    case "$name" in
        *.*) echo "${name##*.}" ;;
        *)   echo "" ;;
    esac
}

in_array() {
    local needle="$1"
    shift
    local item
    for item in "$@"; do
        if [ "$needle" = "$item" ]; then
            return 0
        fi
    done
    return 1
}

is_binary_file() {
    local mime
    mime="$(file --mime-encoding -- "$1" 2>/dev/null || echo "binary")"
    case "$mime" in
        *binary*) return 0 ;;
        *)        return 1 ;;
    esac
}

file_bytes() {
    wc -c < "$1" 2>/dev/null | tr -d '[:space:]'
}

file_lines() {
    wc -l < "$1" 2>/dev/null | tr -d '[:space:]'
}

human_size() {
    echo "$1" | awk '{
        if ($1 > 1048576)     printf "%.1fMB", $1/1048576
        else if ($1 > 1024)   printf "%.1fKB", $1/1024
        else                  printf "%dB", $1
    }'
}

get_lang() {
    local base ext
    base="$(basename "$1")"
    ext="$(get_ext "$base")"

    # Special filenames first
    case "$base" in
        Makefile|makefile|GNUmakefile)     echo "makefile"    ; return ;;
        Dockerfile*|Containerfile)         echo "dockerfile"  ; return ;;
        Vagrantfile|Gemfile|Rakefile)      echo "ruby"        ; return ;;
        Podfile|Fastfile|Dangerfile)       echo "ruby"        ; return ;;
        Jenkinsfile)                       echo "groovy"      ; return ;;
        Justfile)                          echo "just"        ; return ;;
        Tiltfile|Earthfile)                echo "python"      ; return ;;
        .gitignore|.dockerignore)          echo "gitignore"   ; return ;;
        .editorconfig)                     echo "ini"         ; return ;;
        .env*)                             echo "bash"        ; return ;;
        nginx.conf)                        echo "nginx"       ; return ;;
        *.config.js|*.config.ts)           echo "javascript"  ; return ;;
    esac

    # By extension
    case "$ext" in
        sh|bash|zsh|fish)            echo "bash" ;;
        py|pyi|pyw|pyx)              echo "python" ;;
        js|mjs|cjs)                  echo "javascript" ;;
        jsx)                         echo "jsx" ;;
        ts)                          echo "typescript" ;;
        tsx)                         echo "tsx" ;;
        rb|rake|gemspec|erb)         echo "ruby" ;;
        php|phtml)                   echo "php" ;;
        java)                        echo "java" ;;
        kt|kts)                      echo "kotlin" ;;
        scala)                       echo "scala" ;;
        groovy|gradle)               echo "groovy" ;;
        c|h)                         echo "c" ;;
        cpp|hpp|cc|cxx|hxx|hh)       echo "cpp" ;;
        m|mm)                        echo "objectivec" ;;
        cs|csx)                      echo "csharp" ;;
        fs|fsx)                      echo "fsharp" ;;
        vb)                          echo "vb" ;;
        swift)                       echo "swift" ;;
        rs)                          echo "rust" ;;
        go)                          echo "go" ;;
        dart)                        echo "dart" ;;
        lua)                         echo "lua" ;;
        pl|pm|t)                     echo "perl" ;;
        r|R)                         echo "r" ;;
        Rmd)                         echo "rmarkdown" ;;
        hs|lhs)                      echo "haskell" ;;
        ex|exs)                      echo "elixir" ;;
        erl|hrl)                     echo "erlang" ;;
        clj|cljs|cljc|edn)          echo "clojure" ;;
        zig)                         echo "zig" ;;
        nim)                         echo "nim" ;;
        v)                           echo "v" ;;
        html|htm)                    echo "html" ;;
        css)                         echo "css" ;;
        scss)                        echo "scss" ;;
        sass)                        echo "sass" ;;
        less)                        echo "less" ;;
        styl)                        echo "stylus" ;;
        vue)                         echo "vue" ;;
        svelte)                      echo "svelte" ;;
        astro)                       echo "astro" ;;
        json|jsonc|json5)            echo "json" ;;
        yaml|yml)                    echo "yaml" ;;
        toml)                        echo "toml" ;;
        xml|xsl|xsd|plist)          echo "xml" ;;
        ini|cfg|conf|properties)     echo "ini" ;;
        env)                         echo "bash" ;;
        md|mdx)                      echo "markdown" ;;
        rst)                         echo "rst" ;;
        txt|adoc)                    echo "text" ;;
        sql)                         echo "sql" ;;
        graphql|gql)                 echo "graphql" ;;
        proto)                       echo "protobuf" ;;
        tf|tfvars|hcl)               echo "hcl" ;;
        nix)                         echo "nix" ;;
        cmake)                       echo "cmake" ;;
        *)                           echo "" ;;
    esac
}

# ── Check: should we process this file? ─────────────────────────────────────

check_file() {
    local filepath="$1"
    local filename ext bytes kb

    filename="$(basename "$filepath")"
    ext="$(get_ext "$filename")"

    # Skip self and output
    [ "$filename" = "$OUTFILE" ] && return 1
    [ "$filename" = "collect_codebase.sh" ] && return 1

    # Skip by exact name
    if in_array "$filename" "${SKIP_NAMES[@]}"; then
        return 1
    fi

    # Skip by extension
    if [ -n "$ext" ] && in_array "$ext" "${SKIP_EXT[@]}"; then
        return 1
    fi

    # Check size
    bytes="$(file_bytes "$filepath")"
    bytes="${bytes:-0}"
    kb=$((bytes / 1024))
    if [ "$kb" -gt "$MAX_KB" ]; then
        return 1
    fi

    # Must match code extension OR exact filename
    local match=false
    if in_array "$filename" "${EXACT_NAMES[@]}"; then
        match=true
    fi
    if [ "$match" = false ] && [ -n "$ext" ] && in_array "$ext" "${CODE_EXT[@]}"; then
        match=true
    fi
    if [ "$match" = false ]; then
        return 1
    fi

    # Skip binary
    if is_binary_file "$filepath"; then
        return 2
    fi

    return 0
}

# ── Build find command to get all files ─────────────────────────────────────

find_all_files() {
    # Build prune expression for directories
    local prune_args=""
    local d
    for d in "${SKIP_DIRS[@]}"; do
        if [ -n "$prune_args" ]; then
            prune_args="$prune_args -o -name $d"
        else
            prune_args="-name $d"
        fi
    done

    # Find all regular files, pruning skip dirs, sorted
    eval "find . \\( $prune_args \\) -prune -o -type f -print" 2>/dev/null | sort
}

# ── Build directory tree ────────────────────────────────────────────────────

build_tree() {
    if command -v tree >/dev/null 2>&1; then
        local ignore
        ignore="$(IFS='|'; echo "${SKIP_DIRS[*]}")"
        tree -a -I "$ignore" --charset utf-8 --dirsfirst -F 2>/dev/null
    else
        echo "."
        # Simple fallback using find
        local prune_args=""
        local d
        for d in "${SKIP_DIRS[@]}"; do
            if [ -n "$prune_args" ]; then
                prune_args="$prune_args -o -name $d"
            else
                prune_args="-name $d"
            fi
        done
        eval "find . \\( $prune_args \\) -prune -o -print" 2>/dev/null \
            | sed 1d \
            | sort \
            | awk '{
                # Count depth by slashes
                n = gsub(/\//, "/")
                indent = ""
                for (i = 0; i < n; i++) indent = indent "    "
                # Get basename
                split($0, parts, "/")
                name = parts[length(parts)]
                print indent "├── " name
            }' \
            | head -800
    fi
}

# ── Banner ──────────────────────────────────────────────────────────────────

echo ""
echo "╔══════════════════════════════════════════════════════╗"
echo "║           CODEBASE COLLECTOR v4                      ║"
echo "╚══════════════════════════════════════════════════════════╝"
echo ""
echo "  Project:  $PROJECT"
echo "  Output:   $OUTFILE"
echo ""
echo "  Scanning all folders and subfolders..."
echo ""

# ── Collect all file paths ──────────────────────────────────────────────────

find_all_files > "$TMP_FILES"

total_found="$(wc -l < "$TMP_FILES" | tr -d '[:space:]')"
echo "  Found $total_found files total. Filtering..."

# ── Write markdown ──────────────────────────────────────────────────────────

file_count=0
skipped_count=0
binary_count=0
total_lines=0

# --- Header ---
cat > "$TMP_OUT" <<HEADER
# Codebase: \`${PROJECT}\`

> Generated on ${NOW}

---

## Directory Structure

\`\`\`
HEADER

build_tree >> "$TMP_OUT"

echo '```' >> "$TMP_OUT"
echo "" >> "$TMP_OUT"
echo "---" >> "$TMP_OUT"
echo "" >> "$TMP_OUT"
echo "## File Contents" >> "$TMP_OUT"
echo "" >> "$TMP_OUT"

# --- Each file ---
while IFS= read -r fpath; do

    # Strip leading ./
    fpath="${fpath#./}"

    # Skip empty lines
    [ -z "$fpath" ] && continue

    # Check file
    check_file "$fpath"
    result=$?

    if [ $result -eq 1 ]; then
        skipped_count=$((skipped_count + 1))
        continue
    fi
    if [ $result -eq 2 ]; then
        binary_count=$((binary_count + 1))
        continue
    fi

    # Get metadata
    lang="$(get_lang "$fpath")"
    lc="$(file_lines "$fpath")"
    lc="${lc:-0}"
    total_lines=$((total_lines + lc))
    bytes="$(file_bytes "$fpath")"
    bytes="${bytes:-0}"
    sz="$(human_size "$bytes")"

    # Write separator and header
    echo "" >> "$TMP_OUT"
    echo "---" >> "$TMP_OUT"
    echo "" >> "$TMP_OUT"
    echo "### \`$fpath\`" >> "$TMP_OUT"
    echo "" >> "$TMP_OUT"
    echo "> Lines: $lc | Size: $sz" >> "$TMP_OUT"
    echo "" >> "$TMP_OUT"

    # Opening code fence
    echo "\`\`\`$lang" >> "$TMP_OUT"

    # Dump file contents safely with cat
    cat -- "$fpath" >> "$TMP_OUT" 2>/dev/null

    # Ensure trailing newline before closing fence
    echo "" >> "$TMP_OUT"

    # Closing code fence
    echo '```' >> "$TMP_OUT"

    file_count=$((file_count + 1))

    # Progress indicator
    if [ $((file_count % 10)) -eq 0 ]; then
        echo "  ... processed $file_count files" >&2
    fi

done < "$TMP_FILES"

# --- Footer ---
echo "" >> "$TMP_OUT"
echo "---" >> "$TMP_OUT"
echo "" >> "$TMP_OUT"
echo "## Summary" >> "$TMP_OUT"
echo "" >> "$TMP_OUT"
echo "| Metric | Value |" >> "$TMP_OUT"
echo "|--------|-------|" >> "$TMP_OUT"
echo "| Files included | $file_count |" >> "$TMP_OUT"
echo "| Files skipped | $skipped_count |" >> "$TMP_OUT"
echo "| Binary skipped | $binary_count |" >> "$TMP_OUT"
echo "| Total lines | $total_lines |" >> "$TMP_OUT"
echo "| Generated | $NOW |" >> "$TMP_OUT"
echo "" >> "$TMP_OUT"
echo "---" >> "$TMP_OUT"

# ── Move output ─────────────────────────────────────────────────────────────

mv -f "$TMP_OUT" "$OUTFILE"

# ── Results ─────────────────────────────────────────────────────────────────

out_bytes="$(file_bytes "$OUTFILE")"
out_bytes="${out_bytes:-0}"
out_sz="$(human_size "$out_bytes")"

echo ""
echo "  ================================================"
echo "    DONE"
echo "  ================================================"
echo ""
echo "    Output:          $OUTFILE"
echo "    Size:            $out_sz"
echo "    Files collected: $file_count"
echo "    Files skipped:   $skipped_count"
echo "    Binary skipped:  $binary_count"
echo "    Total lines:     $total_lines"
echo ""
echo "  ================================================"
echo ""

if [ "$out_bytes" -gt 10485760 ]; then
    echo "  WARNING: Output exceeds 10MB."
    echo "  Add more dirs to SKIP_DIRS or lower MAX_KB."
    echo ""
fi

```

---

## Summary

| Metric | Value |
|--------|-------|
| Files included | 53 |
| Files skipped | 209 |
| Binary skipped | 0 |
| Total lines | 11582 |
| Generated | 2026-02-21 15:20:10 |

---
