diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..6678e1d
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,42 @@
+# Build Jekyll documentation (GitHub Pages source under /docs).
+# No secrets required.
+name: Documentation
+
+on:
+ push:
+ branches: [main, master]
+ paths:
+ - 'docs/**'
+ - '.github/workflows/docs.yml'
+ pull_request:
+ paths:
+ - 'docs/**'
+ - '.github/workflows/docs.yml'
+
+concurrency:
+ group: docs-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ jekyll-build:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: docs
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: '3.3'
+ bundler-cache: true
+ working-directory: docs
+
+ - name: Jekyll build
+ run: bundle exec jekyll build --destination ./_site
diff --git a/.github/workflows/unity.yml b/.github/workflows/unity.yml
new file mode 100644
index 0000000..02cc6b8
--- /dev/null
+++ b/.github/workflows/unity.yml
@@ -0,0 +1,39 @@
+# Unity project: compile + run Edit Mode tests (if any) via GameCI.
+# Requires repo secret UNITY_LICENSE — see docs/ci.md
+name: Unity
+
+on:
+ push:
+ branches: [main, master]
+ paths:
+ - 'First Principles/**'
+ - '.github/workflows/unity.yml'
+ pull_request:
+ paths:
+ - 'First Principles/**'
+ - '.github/workflows/unity.yml'
+ workflow_dispatch:
+
+concurrency:
+ group: unity-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ test-editmode:
+ name: Edit Mode tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Run tests
+ uses: game-ci/unity-test-runner@v4
+ env:
+ UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
+ with:
+ projectPath: "First Principles"
+ unityVersion: 6000.4.0f1
+ testMode: editmode
diff --git a/CREDITS.md b/CREDITS.md
new file mode 100644
index 0000000..7f3c685
--- /dev/null
+++ b/CREDITS.md
@@ -0,0 +1,36 @@
+# Credits — *First Principles*
+
+Use this file for **in-app parity**, **App Store Connect** (description / promotional text), and **support pages**. Keep wording accurate; avoid implying endorsement by Apple, Unity, or other third parties.
+
+## Developers
+
+**GAME GENESIS** ([itch.io](https://game-genesis.itch.io) · [Rayan Kaissi](https://github.com/rkaissi/)) × **ORCH AEROSPACE** ([orchaerospace.com](https://orchaerospace.com) · [John Wonmo Seong](https://github.com/wonmor))
+
+**Proud graduates of Garth Webb Secondary School, Oakville.**
+
+**Copyright © 2022-2026** Game Genesis (Rayan Kaissi), Orch Aerospace (John Wonmo Seong). All rights reserved.
+
+## Development & support
+
+**First Principles** has been built over **about four years** of design, engineering, and iteration. If the project matters to you, **please support** the team however you can — purchasing or wishlisting on your platform, sharing the game, or following updates — so we can keep improving it. **Thank you.**
+
+*(Use your real App Store / Steam / itch.io / support links on store pages and in promotional copy; they are not duplicated here to avoid stale URLs.)*
+
+## Project
+
+- **Name:** First Principles
+- **License:** [Proprietary](LICENSE) — all rights reserved.
+
+## Third-party software (summary)
+
+| Component | Rights / notes |
+|-----------|----------------|
+| **Unity Engine** | © Unity Technologies. Use of Unity is subject to [Unity’s terms and policies](https://unity.com/legal). “Unity” is a trademark of Unity Technologies. |
+| **TextMesh Pro** | Included with Unity; bundled resources may include fonts under OFL or other licenses—see asset folders (e.g. `Assets/TextMesh Pro/`) for license files. |
+| **Outfit (font)** | Copyright © The Outfit Project Authors. Licensed under the **SIL Open Font License 1.1** — see `First Principles/Assets/Fonts/Outfit-OFL.txt`. Bundled variable font `Outfit-VariableFont_wght.ttf`; used project-wide via TextMesh Pro after running **First Principles → Fonts → Apply Outfit for all TextMesh Pro** in the editor. |
+
+For other assets under `Assets/`, check any `LICENSE`, `*.txt`, or `Third Party Notices` files next to those assets.
+
+## Contact / support
+
+Link your **support URL** and **privacy policy URL** (if any) in App Store Connect when you submit. Repository and issue-tracker links belong in the store listing, not inside the app, unless you add a dedicated screen.
diff --git a/First Principles/Assets/Editor.meta b/First Principles/Assets/Editor.meta
new file mode 100644
index 0000000..c6c81aa
--- /dev/null
+++ b/First Principles/Assets/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5fafca59c07d54245b80af13bcb16ea6
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Editor/OutfitFontProjectSetup.cs b/First Principles/Assets/Editor/OutfitFontProjectSetup.cs
new file mode 100644
index 0000000..268eeb6
--- /dev/null
+++ b/First Principles/Assets/Editor/OutfitFontProjectSetup.cs
@@ -0,0 +1,209 @@
+#if UNITY_EDITOR
+using System.Collections.Generic;
+using TMPro;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using UnityEngine.TextCore.LowLevel;
+
+///
+/// Generates a TextMeshPro SDF asset from Outfit (Google Fonts / OFL), sets it as the project default,
+/// and assigns it to all / in scenes & prefabs.
+///
+public static class OutfitFontProjectSetup
+{
+ public const string TtfPath = "Assets/Fonts/Outfit-VariableFont_wght.ttf";
+ public const string SdfPath = "Assets/Fonts/Outfit SDF.asset";
+ const string TmpSettingsPath = "Assets/TextMesh Pro/Resources/TMP Settings.asset";
+ const string LiberationFallbackPath = "Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset";
+
+ [MenuItem("First Principles/Fonts/Apply Outfit for all TextMesh Pro")]
+ public static void GenerateAndApplyFromMenu()
+ {
+ if (!GenerateAndApplyAll())
+ EditorUtility.DisplayDialog("Outfit font", "Setup failed — see Console.", "OK");
+ else
+ EditorUtility.DisplayDialog("Outfit font", "Outfit is now the default TMP font and applied across scenes & prefabs.", "OK");
+ }
+
+ /// Unity Batchmode: -executeMethod OutfitFontProjectSetup.GenerateAndApplyAllBatch (close the editor if it has this project open).
+ public static void GenerateAndApplyAllBatch()
+ {
+ if (!GenerateAndApplyAll())
+ EditorApplication.Exit(1);
+ else
+ EditorApplication.Exit(0);
+ }
+
+ static bool GenerateAndApplyAll()
+ {
+ AssetDatabase.Refresh();
+
+ var outfit = GetOrCreateOutfitSdfAsset();
+ if (outfit == null)
+ {
+ Debug.LogError("[Outfit] Could not create TMP font asset. Is the TTF at " + TtfPath + "?");
+ return false;
+ }
+
+ if (!AssignTmpSettingsDefault(outfit))
+ return false;
+
+ RetargetAllTmpComponents(outfit);
+
+ AssetDatabase.SaveAssets();
+ AssetDatabase.Refresh();
+ Debug.Log("[Outfit] Font setup complete.");
+ return true;
+ }
+
+ public static TMP_FontAsset GetOrCreateOutfitSdfAsset()
+ {
+ var existing = AssetDatabase.LoadAssetAtPath(SdfPath);
+ if (existing != null)
+ return existing;
+
+ var font = AssetDatabase.LoadAssetAtPath(TtfPath);
+ if (font == null)
+ {
+ Debug.LogError("[Outfit] Missing font file: " + TtfPath);
+ return null;
+ }
+
+ // Dynamic atlas — fills glyphs as needed; includeFontData must be on the TTF import.
+ var asset = TMP_FontAsset.CreateFontAsset(
+ font,
+ 90,
+ 9,
+ GlyphRenderMode.SDFAA,
+ 1024,
+ 1024,
+ AtlasPopulationMode.Dynamic,
+ true);
+
+ if (asset == null)
+ {
+ Debug.LogError("[Outfit] TMP_FontAsset.CreateFontAsset returned null.");
+ return null;
+ }
+
+ asset.name = "Outfit SDF";
+ AssetDatabase.CreateAsset(asset, SdfPath);
+
+ var liberation = AssetDatabase.LoadAssetAtPath(LiberationFallbackPath);
+ if (liberation != null)
+ {
+ if (asset.fallbackFontAssetTable == null)
+ asset.fallbackFontAssetTable = new List();
+ if (!asset.fallbackFontAssetTable.Contains(liberation))
+ asset.fallbackFontAssetTable.Add(liberation);
+ EditorUtility.SetDirty(asset);
+ }
+
+ AssetDatabase.SaveAssets();
+ return asset;
+ }
+
+ static bool AssignTmpSettingsDefault(TMP_FontAsset outfit)
+ {
+ var settings = AssetDatabase.LoadAssetAtPath(TmpSettingsPath);
+ if (settings == null)
+ {
+ Debug.LogError("[Outfit] TMP Settings not found at " + TmpSettingsPath);
+ return false;
+ }
+
+ var so = new SerializedObject(settings);
+ so.FindProperty("m_defaultFontAsset").objectReferenceValue = outfit;
+
+ var fallback = so.FindProperty("m_fallbackFontAssets");
+ if (fallback != null && fallback.isArray)
+ {
+ var liberation = AssetDatabase.LoadAssetAtPath(LiberationFallbackPath);
+ if (liberation != null && liberation != outfit)
+ {
+ bool hasLib = false;
+ for (int i = 0; i < fallback.arraySize; i++)
+ {
+ if (fallback.GetArrayElementAtIndex(i).objectReferenceValue == liberation)
+ hasLib = true;
+ }
+
+ if (!hasLib)
+ {
+ int i = fallback.arraySize;
+ fallback.InsertArrayElementAtIndex(i);
+ fallback.GetArrayElementAtIndex(i).objectReferenceValue = liberation;
+ }
+ }
+ }
+
+ so.ApplyModifiedPropertiesWithoutUndo();
+ EditorUtility.SetDirty(settings);
+ return true;
+ }
+
+ static void RetargetAllTmpComponents(TMP_FontAsset outfit)
+ {
+ foreach (var guid in AssetDatabase.FindAssets("t:Scene", new[] { "Assets/Scenes" }))
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guid);
+ var scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Single);
+ bool dirty = false;
+
+ foreach (var root in scene.GetRootGameObjects())
+ {
+ foreach (var tmp in root.GetComponentsInChildren(true))
+ {
+ Undo.RecordObject(tmp, "Outfit font");
+ tmp.font = outfit;
+ EditorUtility.SetDirty(tmp);
+ dirty = true;
+ }
+
+ foreach (var tmp3d in root.GetComponentsInChildren(true))
+ {
+ Undo.RecordObject(tmp3d, "Outfit font");
+ tmp3d.font = outfit;
+ EditorUtility.SetDirty(tmp3d);
+ dirty = true;
+ }
+ }
+
+ if (dirty)
+ EditorSceneManager.MarkSceneDirty(scene);
+
+ EditorSceneManager.SaveScene(scene);
+ }
+
+ foreach (var guid in AssetDatabase.FindAssets("t:Prefab", new[] { "Assets" }))
+ {
+ string path = AssetDatabase.GUIDToAssetPath(guid);
+ if (string.IsNullOrEmpty(path) || path.Contains("PackageCache"))
+ continue;
+
+ var root = PrefabUtility.LoadPrefabContents(path);
+ try
+ {
+ foreach (var tmp in root.GetComponentsInChildren(true))
+ {
+ tmp.font = outfit;
+ EditorUtility.SetDirty(tmp);
+ }
+
+ foreach (var tmp3d in root.GetComponentsInChildren(true))
+ {
+ tmp3d.font = outfit;
+ EditorUtility.SetDirty(tmp3d);
+ }
+
+ PrefabUtility.SaveAsPrefabAsset(root, path);
+ }
+ finally
+ {
+ PrefabUtility.UnloadPrefabContents(root);
+ }
+ }
+ }
+}
+#endif
diff --git a/First Principles/Assets/Editor/OutfitFontProjectSetup.cs.meta b/First Principles/Assets/Editor/OutfitFontProjectSetup.cs.meta
new file mode 100644
index 0000000..ead04d6
--- /dev/null
+++ b/First Principles/Assets/Editor/OutfitFontProjectSetup.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e53c0d6f71824ab79a1b2c3d4e5f6078
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Fonts/Outfit-OFL.txt b/First Principles/Assets/Fonts/Outfit-OFL.txt
new file mode 100644
index 0000000..723cd44
--- /dev/null
+++ b/First Principles/Assets/Fonts/Outfit-OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2021 The Outfit Project Authors (https://github.com/Outfitio/Outfit-Fonts)
+
+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:
+https://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.
diff --git a/First Principles/Assets/Fonts/Outfit-OFL.txt.meta b/First Principles/Assets/Fonts/Outfit-OFL.txt.meta
new file mode 100644
index 0000000..15c5686
--- /dev/null
+++ b/First Principles/Assets/Fonts/Outfit-OFL.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: d42b9c5f61754e30b9283f9a1bc2def0
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Fonts/Outfit-VariableFont_wght.ttf b/First Principles/Assets/Fonts/Outfit-VariableFont_wght.ttf
new file mode 100644
index 0000000..466d624
Binary files /dev/null and b/First Principles/Assets/Fonts/Outfit-VariableFont_wght.ttf differ
diff --git a/First Principles/Assets/Fonts/Outfit-VariableFont_wght.ttf.meta b/First Principles/Assets/Fonts/Outfit-VariableFont_wght.ttf.meta
new file mode 100644
index 0000000..0c2b29b
--- /dev/null
+++ b/First Principles/Assets/Fonts/Outfit-VariableFont_wght.ttf.meta
@@ -0,0 +1,21 @@
+fileFormatVersion: 2
+guid: c31a8b4e5f6044d2a9172e8f90ab1cde
+TrueTypeFontImporter:
+ externalObjects: {}
+ serializedVersion: 4
+ fontSize: 16
+ forceTextureCase: -2
+ characterSpacing: 0
+ characterPadding: 1
+ includeFontData: 1
+ fontNames:
+ - Outfit
+ fallbackFontReferences: []
+ customCharacters:
+ fontRenderingMode: 0
+ ascentCalculationMode: 1
+ useLegacyBoundsCalculation: 0
+ shouldRoundAdvanceValue: 1
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Resources.meta b/First Principles/Assets/Resources.meta
new file mode 100644
index 0000000..fd4b0f0
--- /dev/null
+++ b/First Principles/Assets/Resources.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 1cbc6ff2863a247d085948108c777c49
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Resources/Localization.meta b/First Principles/Assets/Resources/Localization.meta
new file mode 100644
index 0000000..de73e9f
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 1bac7ea06468f462d9438fc1b6cfa810
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Resources/Localization/README.md b/First Principles/Assets/Resources/Localization/README.md
new file mode 100644
index 0000000..baa3b43
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/README.md
@@ -0,0 +1,12 @@
+# Localization
+
+- **Files**: `en.txt`, `ar.txt`, `fr.txt`, `zh.txt`, `ko.txt`, `ja.txt`, `de.txt`, `es.txt` (UTF-8).
+- **Keys**: Copy from `en.txt`. Optional `story.0` … `story.42` override long level narratives; if omitted, the built-in English body from `LevelManager` is used.
+- **Fonts (TMP)**: The default / Outfit asset may not include Arabic or CJK glyphs. To avoid tofu:
+ 1. Import **Noto Sans** (Arabic, SC, JP, KR) or **Source Han** as TMP font assets.
+ 2. Add them to **Fallback Font Assets** on your primary TMP font, or swap the default font per language in code.
+
+## Player preference
+
+- Stored in `PlayerPrefs` under `fp_language` (`en`, `ar`, `fr`, `zh`, `ko`, `ja`, `de`, `es`).
+- Menu and Level Select include a **Language** chip (tap cycles languages).
diff --git a/First Principles/Assets/Resources/Localization/README.md.meta b/First Principles/Assets/Resources/Localization/README.md.meta
new file mode 100644
index 0000000..784953a
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/README.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: debf82893f0d44c07a8418d694028b61
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Resources/Localization/ar.txt b/First Principles/Assets/Resources/Localization/ar.txt
new file mode 100644
index 0000000..73659a3
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/ar.txt
@@ -0,0 +1,78 @@
+# العربية (RTL)
+ui.choose_stage=اختر مرحلة الرسم
+ui.math_tips=نصائح وقصاصات رياضية
+ui.back_menu=العودة للقائمة
+ui.graphic_calculator_mode=وضع آلة الرسوم البيانية
+ui.loading=جارٍ التحميل…
+ui.language=اللغة
+ui.tap_change_language=اضغط للتبديل
+ui.close=إغلاق
+ui.math_concepts=المفاهيم الرياضية
+ui.jump=قفز
+ui.move=تحرك
+ui.keyboard_hint_mobile=(لوحة المفاتيح: الأسهم / المسافة)
+hud.stage=المرحلة
+controls.mobile=تحرك ◀ ▶ · قفز مس (لوحة المفاتيح: الأسهم / المسافة)
+controls.desktop=تحرك ← → · قفز مسافة
+controls.calculator=آلة رسومية اكتب f(u) · Trans · Scale · قرصة · رجوع
+graph.label_fu=f(u) =
+graph.placeholder=x^2 + sin(x) · ln(x) عندما x>0 · min(x,3)...
+graph.status_enter=Enter / اضغط خارج الحقل للرسم
+graph.status_graphed=تم الرسم
+graph.line1=وضع آلة الرسوم البيانية
+graph.line2=اكتب f(u) أدناه (المتغير x في المربع = u الداخلي). ثم:
+graph.line3=Trans ← {0} · نقرتان + · اضغط مطولًا − · Scale تكبير باللمس / اضغط مطولًا للتصغير · قرصة بإصبعين
+graph.line4=A={0} k={1} C={2} D={3} x في [{4},{5}]
+graph.param_a=A (مقياس عمودي)
+graph.param_k=k (مقياس أفقي)
+graph.param_c=C (إزاحة عمودية)
+graph.param_d=D (إزاحة أفقية)
+graph.calculator_intro=وضع آلة الرسوم البيانية\nاكتب تقريبًا أي f(u) (المتغير x في صيغتك)؛ Trans يضبط A و k و C و D؛ Scale والقرصة تكبّران النافذة.
+footer.proprietary=© 2022-2026 · ملكية خاصة · جميع الحقوق محفوظة · First Principles
+footer.attribution=GAME GENESIS (Rayan Kaissi) × ORCH AEROSPACE (John Wonmo Seong)
+footer.school=خريجو Garth Webb Secondary School في أوكفيل، نفتخر بذلك.
+footer.support=أربع سنوات من التطوير — إن أعجبك العمل، يُرجى دعم المشروع. شكرًا.
+footer.unity=صُنع باستخدام Unity. Unity علامة تجارية لشركة Unity Technologies.
+level.0=مقدمة المبادئ الأولى
+level.1=ميل قطع مكافئ
+level.2=أمواج الجيب
+level.3=ظلال جيب التمام
+level.4=مسار القيمة المطلقة
+level.5=ماكلورين: e^x
+level.6=ماكلورين: sin(x)
+level.7=متسلسلة: ذيل هندسي
+level.8=شريحة سرج (متعدد المتغيرات)
+level.9=شريحة قطع مكافئ ثلاثي (متعدد المتغيرات)
+level.10=مساحة تحت المنحنى
+level.11=ريمان: نقاط يسارية
+level.12=ريمان: نقاط يمينية
+level.13=ريمان: قاعدة النقطة الوسطى
+level.14=هندسة: اهتزاز مُخمَّد
+level.15=هندسة: سلسلة معلّقة (cosh)
+level.16=هندسة: تيار متردد مُقَوَّم (|sin|)
+level.17=BC: قوس ظل والمقلوبات المثلثية
+level.18=BC: نمو لوجستي (dP/dt = kP(1−P/L))
+level.19=قطبي: قلب r ~ 1+cos θ
+level.20=قطبي: وردة r ~ cos(nθ)
+level.21=BC: sinh x والدوال الزائدية
+level.22=فيزياء C: اضمحلال أسي (τ, RC)
+level.23=فيزياء C: عزم زاوي و L = Iω
+level.24=فيزياء C: ارتفاع قذيفة y(t)
+level.25=BC: ماكلورين cos(x)
+level.26=BC: ln x و ∫ dx/x
+level.27=BC: √x والمجال / النتوء
+level.28=BC: tan x بين خطوط مقاربة
+level.29=BC: e^{kx} و y′ = ky
+level.30=BC: طور وحركة توافقية بسيطة (طاقة)
+level.31=BC: تكعيب ونقطة انعطاف (رسم)
+level.32=BC: b^x و d/dx b^x
+level.33=دائرة: (x−h)² + (y−k)² = R²
+level.34=طيران: رفع C_L(α) خطي + انفصال
+level.35=طيران: قطب مقاومة قطع مكافئ C_D(C_L)
+level.36=طيران: جو متساوي الحرارة ρ(h)
+level.37=طيران: حركة مجنحة / اهتزاز مُخمَّد
+level.38=طيران: نيوتوني Cp ~ sin²α
+level.39=طيران: Strouhal / نبرة انبعاث دوامات
+level.40=طيران: غلاف دخول غلاف جوي (تسخين ρV)
+level.41=مسابقات: ln وتقعر وحدود
+level.42=الزعيم: شريحة هروب ماندلبروت (كسيري)
diff --git a/First Principles/Assets/Resources/Localization/ar.txt.meta b/First Principles/Assets/Resources/Localization/ar.txt.meta
new file mode 100644
index 0000000..33cacf7
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/ar.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: d64b1830d93ba4a71bede12cb90ac750
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Resources/Localization/de.txt b/First Principles/Assets/Resources/Localization/de.txt
new file mode 100644
index 0000000..98a1d5f
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/de.txt
@@ -0,0 +1,78 @@
+# Deutsch
+ui.choose_stage=Graph-Phase wählen
+ui.math_tips=Mathe-Tipps & Kurzinfos
+ui.back_menu=Zum Menü
+ui.graphic_calculator_mode=Grafikrechner-Modus
+ui.loading=Laden…
+ui.language=Sprache
+ui.tap_change_language=Tippen zum Wechseln
+ui.close=Schließen
+ui.math_concepts=Mathe-Konzepte
+ui.jump=Sprung
+ui.move=Bewegen
+ui.keyboard_hint_mobile=(Tastatur: Pfeile / Leertaste)
+hud.stage=PHASE
+controls.mobile=Bewegen ◀ ▶ · Sprung Tipp (Tastatur: Pfeile / Leertaste)
+controls.desktop=Bewegen ← → · Sprung Leertaste
+controls.calculator=Grafikrechner f(u) eingeben · Trans · Maßstab · Pinch · Zurück
+graph.label_fu=f(u) =
+graph.placeholder=x^2 + sin(x) · ln(x) für x>0 · min(x,3)...
+graph.status_enter=Eingabe / außerhalb tippen zum Zeichnen
+graph.status_graphed=Gezeichnet
+graph.line1=Grafikrechner-Modus
+graph.line2=Geben Sie unten f(u) ein (Variable x im Feld = inneres u). Dann:
+graph.line3=Trans → {0} · Doppeltipp + · halten − · Maßstab hereinzoomen / halten herauszoomen · Zweifinger-Pinch
+graph.line4=A={0} k={1} C={2} D={3} x in [{4},{5}]
+graph.param_a=A (vertikaler Maßstab)
+graph.param_k=k (horizontaler Maßstab)
+graph.param_c=C (vertikale Verschiebung)
+graph.param_d=D (horizontale Verschiebung)
+graph.calculator_intro=Grafikrechner-Modus\nGeben Sie fast beliebiges f(u) ein (Variable x in Ihrer Formel); Trans stellt A, k, C, D ein; Maßstab und Pinch zoomen das Fenster.
+footer.proprietary=© 2022-2026 · Proprietär · Alle Rechte vorbehalten · First Principles
+footer.attribution=GAME GENESIS (Rayan Kaissi) × ORCH AEROSPACE (John Wonmo Seong)
+footer.school=Stolze Absolventen der Garth Webb Secondary School, Oakville.
+footer.support=Vier Jahre Entwicklung — wenn Ihnen diese Arbeit etwas bedeutet, unterstützen Sie das Projekt. Danke.
+footer.unity=Erstellt mit Unity. Unity ist eine Marke der Unity Technologies.
+level.0=Einstieg: Erste Prinzipien
+level.1=Steigung der Parabel
+level.2=Sinuswellen
+level.3=Kosinusschatten
+level.4=Betragspfad
+level.5=Maclaurin: e^x
+level.6=Maclaurin: sin(x)
+level.7=Reihe: geometrischer Rest
+level.8=Sattelschnitt (mehrere Var.)
+level.9=Paraboloidschnitt (mehrere Var.)
+level.10=Fläche unter der Kurve
+level.11=Riemann: linke Endpunkte
+level.12=Riemann: rechte Endpunkte
+level.13=Riemann: Mittelpunktsregel
+level.14=Technik: gedämpfte Schwingung
+level.15=Technik: Kettenlinie (cosh)
+level.16=Technik: gleichgerichtete AC (|sin|)
+level.17=BC: arctan & inverse Trig.
+level.18=BC: logistisches Wachstum (dP/dt = kP(1−P/L))
+level.19=Polar: Kardioide r ~ 1+cos θ
+level.20=Polar: Rose r ~ cos(nθ)
+level.21=BC: sinh x & Hyperbelfunktionen
+level.22=Physik C: exponentieller Zerfall (τ, RC)
+level.23=Physik C: Drehimpuls & L = Iω
+level.24=Physik C: Wurfhöhe y(t)
+level.25=BC: Maclaurin cos(x)
+level.26=BC: ln x & ∫ dx/x
+level.27=BC: √x & Definitionsbereich / Spitze
+level.28=BC: tan x zwischen Asymptoten
+level.29=BC: e^{kx} & y′ = ky
+level.30=BC: Phase & SHM (Energie)
+level.31=BC: Kubik & Wendepunkt (Skizze)
+level.32=BC: b^x & d/dx b^x
+level.33=Kreis: (x−h)² + (y−k)² = R²
+level.34=Luftfahrt: Auftrieb C_L(α) linear + Strömungsabriss
+level.35=Luftfahrt: parabolische Widerstandspolare C_D(C_L)
+level.36=Luftfahrt: isotherme Atmosphäre ρ(h)
+level.37=Luftfahrt: Phugoid / gedämpfte Nick-Hubbewegung
+level.38=Luftfahrt: newtonsch Cp ~ sin²α
+level.39=Luftfahrt: Strouhal / Wirbelabwurf-Ton
+level.40=Luftfahrt: Wiedereintritts-Hülle (ρV Erwärmung)
+level.41=Wettkampf: ln, Krümmung & Schranken
+level.42=BOSS: Mandelbrot-Escapeschnitt (Fraktal)
diff --git a/First Principles/Assets/Resources/Localization/de.txt.meta b/First Principles/Assets/Resources/Localization/de.txt.meta
new file mode 100644
index 0000000..b7e48da
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/de.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 1d0d43692951142a88ad814bc5890ae9
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Resources/Localization/en.txt b/First Principles/Assets/Resources/Localization/en.txt
new file mode 100644
index 0000000..14c1111
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/en.txt
@@ -0,0 +1,80 @@
+# English (default) — First Principles UI strings
+# Stories: optional keys story.N override embedded English when present in other locale files.
+
+ui.choose_stage=Choose a graph stage
+ui.math_tips=Math tips & snippets
+ui.back_menu=Back to Menu
+ui.graphic_calculator_mode=Graphic calculator mode
+ui.loading=Loading…
+ui.language=Language
+ui.tap_change_language=Tap to change
+ui.close=Close
+ui.math_concepts=Math concepts
+ui.jump=Jump
+ui.move=Move
+ui.keyboard_hint_mobile=(keyboard: arrows / Space)
+hud.stage=STAGE
+controls.mobile=Move ◀ ▶ · Jump tap (keyboard: arrows / Space)
+controls.desktop=Move ← → · Jump Space
+controls.calculator=Graphic calculator Type f(u) · Trans · Scale · Pinch · Back
+graph.label_fu=f(u) =
+graph.placeholder=x^2 + sin(x) · ln(x) for x>0 · min(x,3)...
+graph.status_enter=Enter / tap away to graph
+graph.status_graphed=Graphed
+graph.line1=Graphic calculator mode
+graph.line2=Type f(u) below (variable x in the box = inner u). Then:
+graph.line3=Trans → {0} · double-tap + · hold − · Scale zoom in / hold zoom out · two-finger pinch
+graph.line4=A={0} k={1} C={2} D={3} x in [{4},{5}]
+graph.param_a=A (vertical scale)
+graph.param_k=k (horizontal scale)
+graph.param_c=C (vertical shift)
+graph.param_d=D (horizontal shift)
+graph.calculator_intro=Graphic calculator mode\nType almost any f(u) in the field (variable x in your formula); Trans adjusts A, k, C, D; Scale & pinch zoom the window.
+footer.proprietary=© 2022-2026 · Proprietary · All rights reserved · First Principles
+footer.attribution=GAME GENESIS (Rayan Kaissi) × ORCH AEROSPACE (John Wonmo Seong)
+footer.school=Proud graduates of Garth Webb Secondary School, Oakville.
+footer.support=Four years of development — if you value this work, please support the project. Thank you.
+footer.unity=Made with Unity. Unity is a trademark of Unity Technologies.
+level.0=First Principles Primer
+level.1=Slope of Parabola
+level.2=Waves of Sine
+level.3=Shadows of Cosine
+level.4=Absolute Path
+level.5=Maclaurin: e^x
+level.6=Maclaurin: sin(x)
+level.7=Series: geometric tail
+level.8=Saddle slice (multivar)
+level.9=Paraboloid slice (multivar)
+level.10=Area under the curve
+level.11=Riemann: left endpoints
+level.12=Riemann: right endpoints
+level.13=Riemann: midpoint rule
+level.14=Engineering: damped oscillation
+level.15=Engineering: catenary (cosh)
+level.16=Engineering: rectified AC (|sin|)
+level.17=BC: arctan & inverse trig
+level.18=BC: logistic growth (dP/dt = kP(1−P/L))
+level.19=Polar: cardioid r ~ 1+cos θ
+level.20=Polar: rose r ~ cos(nθ)
+level.21=BC: sinh x & hyperbolic functions
+level.22=Physics C: exponential decay (τ, RC)
+level.23=Physics C: angular momentum & L = Iω
+level.24=Physics C: projectile height y(t)
+level.25=BC: Maclaurin cos(x)
+level.26=BC: ln x & ∫ dx/x
+level.27=BC: √x & domain / cusp craft
+level.28=BC: tan x between asymptotes
+level.29=BC: e^{kx} & y′ = ky
+level.30=BC: phase & SHM (energy swaps)
+level.31=BC: cubic & inflection (sketching)
+level.32=BC: b^x & d/dx b^x
+level.33=Circle: (x−h)² + (y−k)² = R²
+level.34=Aerospace: lift C_L(α) linear + stall
+level.35=Aerospace: parabolic drag polar C_D(C_L)
+level.36=Aerospace: isothermal atmosphere ρ(h)
+level.37=Aerospace: phugoid / damped pitch–heave mood
+level.38=Aerospace: Newtonian Cp ~ sin²α
+level.39=Aerospace: Strouhal / vortex shedding tone
+level.40=Aerospace: re-entry decay envelope (ρV heating mood)
+level.41=Competition math: ln, concavity & bound tricks
+level.42=BOSS: Mandelbrot escape slice (fractal boundary mood)
diff --git a/First Principles/Assets/Resources/Localization/en.txt.meta b/First Principles/Assets/Resources/Localization/en.txt.meta
new file mode 100644
index 0000000..e732315
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/en.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 79cd056ab9ddb4160a99f6467de38935
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Resources/Localization/es.txt b/First Principles/Assets/Resources/Localization/es.txt
new file mode 100644
index 0000000..ae0c408
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/es.txt
@@ -0,0 +1,78 @@
+# Español
+ui.choose_stage=Elige una fase del gráfico
+ui.math_tips=Consejos y fórmulas
+ui.back_menu=Volver al menú
+ui.graphic_calculator_mode=Modo calculadora gráfica
+ui.loading=Cargando…
+ui.language=Idioma
+ui.tap_change_language=Toca para cambiar
+ui.close=Cerrar
+ui.math_concepts=Conceptos matemáticos
+ui.jump=Saltar
+ui.move=Mover
+ui.keyboard_hint_mobile=(teclado: flechas / Espacio)
+hud.stage=FASE
+controls.mobile=Mover ◀ ▶ · Saltar tocar (teclado: flechas / Espacio)
+controls.desktop=Mover ← → · Saltar Espacio
+controls.calculator=Calculadora gráfica Escribir f(u) · Trans · Escala · Pellizco · Atrás
+graph.label_fu=f(u) =
+graph.placeholder=x^2 + sin(x) · ln(x) para x>0 · min(x,3)...
+graph.status_enter=Intro / tocar fuera para graficar
+graph.status_graphed=Graficado
+graph.line1=Modo calculadora gráfica
+graph.line2=Escribe f(u) abajo (la variable x en el cuadro = u interior). Luego:
+graph.line3=Trans → {0} · doble toque + · mantén − · Escala acercar / mantén alejar · pellizco con dos dedos
+graph.line4=A={0} k={1} C={2} D={3} x en [{4},{5}]
+graph.param_a=A (escala vertical)
+graph.param_k=k (escala horizontal)
+graph.param_c=C (desplazamiento vertical)
+graph.param_d=D (desplazamiento horizontal)
+graph.calculator_intro=Modo calculadora gráfica\nEscribe casi cualquier f(u) (variable x en tu fórmula); Trans ajusta A, k, C, D; Escala y pellizco hacen zoom en la ventana.
+footer.proprietary=© 2022-2026 · Propietario · Todos los derechos reservados · First Principles
+footer.attribution=GAME GENESIS (Rayan Kaissi) × ORCH AEROSPACE (John Wonmo Seong)
+footer.school=Orgullosos graduados de Garth Webb Secondary School, Oakville.
+footer.support=Cuatro años de desarrollo — si valoras este trabajo, apoya el proyecto. Gracias.
+footer.unity=Hecho con Unity. Unity es una marca registrada de Unity Technologies.
+level.0=Introducción: primeros principios
+level.1=Pendiente de la parábola
+level.2=Ondas del seno
+level.3=Sombras del coseno
+level.4=Camino en valor absoluto
+level.5=Maclaurin: e^x
+level.6=Maclaurin: sin(x)
+level.7=Serie: cola geométrica
+level.8=Rebanada silla (multivar.)
+level.9=Rebanada paraboloide (multivar.)
+level.10=Área bajo la curva
+level.11=Riemann: extremos izquierdos
+level.12=Riemann: extremos derechos
+level.13=Riemann: punto medio
+level.14=Ingeniería: oscilación amortiguada
+level.15=Ingeniería: catenaria (cosh)
+level.16=Ingeniería: CA rectificada (|sin|)
+level.17=BC: arctan y trig. inversa
+level.18=BC: crecimiento logístico (dP/dt = kP(1−P/L))
+level.19=Polar: cardioide r ~ 1+cos θ
+level.20=Polar: rosa r ~ cos(nθ)
+level.21=BC: sinh x e hiperbólicas
+level.22=Física C: decaimiento exponencial (τ, RC)
+level.23=Física C: momento angular y L = Iω
+level.24=Física C: altura del proyectil y(t)
+level.25=BC: Maclaurin cos(x)
+level.26=BC: ln x y ∫ dx/x
+level.27=BC: √x y dominio / punta
+level.28=BC: tan x entre asíntotas
+level.29=BC: e^{kx} y y′ = ky
+level.30=BC: fase y MAS (energía)
+level.31=BC: cúbica e inflexión (boceto)
+level.32=BC: b^x y d/dx b^x
+level.33=Círculo: (x−h)² + (y−k)² = R²
+level.34=Aero: sustentación C_L(α) lineal + pérdida
+level.35=Aero: polar parabólica C_D(C_L)
+level.36=Aero: atmósfera isoterma ρ(h)
+level.37=Aero: fugoide / cabeceo–heave amortiguado
+level.38=Aero: newtoniano Cp ~ sin²α
+level.39=Aero: Strouhal / tono de vórtices
+level.40=Aero: envolvente de reentrada (calor ρV)
+level.41=Competición: ln, concavidad y cotas
+level.42=JEFE: corte de escape Mandelbrot (fractal)
diff --git a/First Principles/Assets/Resources/Localization/es.txt.meta b/First Principles/Assets/Resources/Localization/es.txt.meta
new file mode 100644
index 0000000..f465292
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/es.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 97c13adf3e502476096e3e359b46260b
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Resources/Localization/fr.txt b/First Principles/Assets/Resources/Localization/fr.txt
new file mode 100644
index 0000000..6c01770
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/fr.txt
@@ -0,0 +1,78 @@
+# Français
+ui.choose_stage=Choisissez un niveau graphique
+ui.math_tips=Astuces & formules
+ui.back_menu=Retour au menu
+ui.graphic_calculator_mode=Mode calculatrice graphique
+ui.loading=Chargement…
+ui.language=Langue
+ui.tap_change_language=Appuyez pour changer
+ui.close=Fermer
+ui.math_concepts=Concepts maths
+ui.jump=Saut
+ui.move=Déplacer
+ui.keyboard_hint_mobile=(clavier : flèches / Espace)
+hud.stage=NIVEAU
+controls.mobile=Déplacer ◀ ▶ · Saut tapoter (clavier : flèches / Espace)
+controls.desktop=Déplacer ← → · Saut Espace
+controls.calculator=Calculatrice graphique Saisir f(u) · Trans · Échelle · Pincer · Retour
+graph.label_fu=f(u) =
+graph.placeholder=x^2 + sin(x) · ln(x) si x>0 · min(x,3)...
+graph.status_enter=Entrée / toucher ailleurs pour tracer
+graph.status_graphed=Tracé
+graph.line1=Mode calculatrice graphique
+graph.line2=Saisissez f(u) ci-dessous (la variable x dans la zone = u interne). Ensuite :
+graph.line3=Trans → {0} · double appui + · maintenir − · Échelle zoom avant / maintenir zoom arrière · pincer deux doigts
+graph.line4=A={0} k={1} C={2} D={3} x ∈ [{4},{5}]
+graph.param_a=A (échelle verticale)
+graph.param_k=k (échelle horizontale)
+graph.param_c=C (translation verticale)
+graph.param_d=D (translation horizontale)
+graph.calculator_intro=Mode calculatrice graphique\nSaisissez presque n’importe quelle f(u) (variable x dans la formule) ; Trans règle A, k, C, D ; Échelle et pincement zooment la fenêtre.
+footer.proprietary=© 2022-2026 · Propriétaire · Tous droits réservés · First Principles
+footer.attribution=GAME GENESIS (Rayan Kaissi) × ORCH AEROSPACE (John Wonmo Seong)
+footer.school=Diplômés fiers du Garth Webb Secondary School, Oakville.
+footer.support=Quatre ans de développement — si ce travail vous parle, soutenez le projet. Merci.
+footer.unity=Développé avec Unity. Unity est une marque d’Unity Technologies.
+level.0=Introduction : premiers principes
+level.1=Pente d’une parabole
+level.2=Vague sinusoïdale
+level.3=L’ombre du cosinus
+level.4=Chemin en valeur absolue
+level.5=Maclaurin : e^x
+level.6=Maclaurin : sin(x)
+level.7=Série : queue géométrique
+level.8=Tranche selle (plusieurs variables)
+level.9=Tranche paraboloïde (plusieurs variables)
+level.10=Aire sous la courbe
+level.11=Riemann : extrémités gauches
+level.12=Riemann : extrémités droites
+level.13=Riemann : point milieu
+level.14=Ingénierie : oscillation amortie
+level.15=Ingénierie : chaînette (cosh)
+level.16=Ingénierie : AC redressée (|sin|)
+level.17=BC : arctan & trig réciproques
+level.18=BC : croissance logistique (dP/dt = kP(1−P/L))
+level.19=Polaire : cardioïde r ~ 1+cos θ
+level.20=Polaire : rose r ~ cos(nθ)
+level.21=BC : sinh x & fonctions hyperboliques
+level.22=Physique C : décroissance exponentielle (τ, RC)
+level.23=Physique C : moment angulaire & L = Iω
+level.24=Physique C : hauteur de projectile y(t)
+level.25=BC : Maclaurin cos(x)
+level.26=BC : ln x & ∫ dx/x
+level.27=BC : √x & domaine / point anguleux
+level.28=BC : tan x entre asymptotes
+level.29=BC : e^{kx} & y′ = ky
+level.30=BC : phase & MHS (échanges d’énergie)
+level.31=BC : cubique & inflexion (esquisse)
+level.32=BC : b^x & d/dx b^x
+level.33=Cercle : (x−h)² + (y−k)² = R²
+level.34=Aéro : portance C_L(α) linéaire + décrochage
+level.35=Aéro : polaire de traînée parabolique C_D(C_L)
+level.36=Aéro : atmosphère isotherme ρ(h)
+level.37=Aéro : phugoïde / oscillation amortie tangage–pilonnement
+level.38=Aéro : newtonien Cp ~ sin²α
+level.39=Aéro : Strouhal / tonalité tourbillonnaire
+level.40=Aéro : enveloppe rentrée (échauffement ρV)
+level.41=Concours : ln, concavité & bornes
+level.42=BOSS : tranche d’échappement Mandelbrot (fractale)
diff --git a/First Principles/Assets/Resources/Localization/fr.txt.meta b/First Principles/Assets/Resources/Localization/fr.txt.meta
new file mode 100644
index 0000000..5b173e5
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/fr.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 68d29e01b5fea4d429082a2e0dfc8751
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Resources/Localization/ja.txt b/First Principles/Assets/Resources/Localization/ja.txt
new file mode 100644
index 0000000..ccc65a3
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/ja.txt
@@ -0,0 +1,78 @@
+# 日本語
+ui.choose_stage=グラフのステージを選ぶ
+ui.math_tips=数学のヒント
+ui.back_menu=メニューへ戻る
+ui.graphic_calculator_mode=グラフ電卓モード
+ui.loading=読み込み中…
+ui.language=言語
+ui.tap_change_language=タップで切替
+ui.close=閉じる
+ui.math_concepts=数学コンセプト
+ui.jump=ジャンプ
+ui.move=移動
+ui.keyboard_hint_mobile=(キーボード:矢印/スペース)
+hud.stage=ステージ
+controls.mobile=移動 ◀ ▶ · ジャンプ タップ (キーボード:矢印/スペース)
+controls.desktop=移動 ← → · ジャンプ スペース
+controls.calculator=グラフ電卓 f(u) を入力 · Trans · Scale · ピンチ · 戻る
+graph.label_fu=f(u) =
+graph.placeholder=x^2 + sin(x) · ln(x) (x>0)· min(x,3)...
+graph.status_enter=Enter/外側タップでグラフ描画
+graph.status_graphed=描画済み
+graph.line1=グラフ電卓モード
+graph.line2=下にf(u)を入力(欄内の変数xは内側の u)。次に:
+graph.line3=Trans → {0} · ダブルタップ + · 長押し − · Scale はタップで拡大/長押しで縮小 · 2本指ピンチ
+graph.line4=A={0} k={1} C={2} D={3} x は [{4},{5}]
+graph.param_a=A(縦スケール)
+graph.param_k=k(横スケール)
+graph.param_c=C(縦シフト)
+graph.param_d=D(横シフト)
+graph.calculator_intro=グラフ電卓モード\nほぼ任意のf(u)を入力(式の変数x);Transで A,k,C,D を調整;Scaleとピンチでウィンドウをズーム。
+footer.proprietary=© 2022-2026 · 無断複製禁止 · First Principles
+footer.attribution=GAME GENESIS (Rayan Kaissi) × ORCH AEROSPACE (John Wonmo Seong)
+footer.school=Garth Webb Secondary School(オークビル)の誇り高き卒業生。
+footer.support=4年の開発 — よろしければプロジェクトを支援してください。ありがとうございます。
+footer.unity=Unityで制作。Unity は Unity Technologies の商標です。
+level.0=ファーストプリンシプル入門
+level.1=放物線の傾き
+level.2=サイン波
+level.3=コサインの陰
+level.4=絶対値の道
+level.5=マクローリン:e^x
+level.6=マクローリン:sin(x)
+level.7=級数:幾何級数の尾部
+level.8=サドル断面(多変数)
+level.9=放物面断面(多変数)
+level.10=曲線下の面積
+level.11=リーマン:左端点
+level.12=リーマン:右端点
+level.13=リーマン:中点公式
+level.14=工学:減衰振動
+level.15=工学:カテナリー(cosh)
+level.16=工学:整流AC(|sin|)
+level.17=BC:arctanと逆三角関数
+level.18=BC:ロジスティック成長(dP/dt = kP(1−P/L))
+level.19=極座標:カージオイド r ~ 1+cos θ
+level.20=極座標:バラ曲線 r ~ cos(nθ)
+level.21=BC:sinh xと双曲線関数
+level.22=物理C:指数減衰(τ, RC)
+level.23=物理C:角運動量と L = Iω
+level.24=物理C:投射体の高さ y(t)
+level.25=BC:マクローリン cos(x)
+level.26=BC:ln x と ∫ dx/x
+level.27=BC:√x と定義域/カスプ
+level.28=BC:漸近線間の tan x
+level.29=BC:e^{kx} と y′ = ky
+level.30=BC:位相と単振動(エネルギー)
+level.31=BC:三次関数と変曲点(スケッチ)
+level.32=BC:b^x と d/dx b^x
+level.33=円:(x−h)² + (y−k)² = R²
+level.34=航空:揚力C_L(α) 線形+失速
+level.35=航空:放物型抗力極曲線 C_D(C_L)
+level.36=航空:等温大気 ρ(h)
+level.37=航空:フゴイド/減衰ピッチ・上下動
+level.38=航空:ニュートン流 Cp ~ sin²α
+level.39=航空:ストルーハル/渦放出音
+level.40=航空:再突入減衰包絡線(ρV加熱)
+level.41=競技数学:ln、凹性と境界
+level.42=BOSS:マンデルブロ脱出断面(フラクタル)
diff --git a/First Principles/Assets/Resources/Localization/ja.txt.meta b/First Principles/Assets/Resources/Localization/ja.txt.meta
new file mode 100644
index 0000000..90c7960
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/ja.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 0a1e11ac9711d4ba49ea338615dabe2d
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Resources/Localization/ko.txt b/First Principles/Assets/Resources/Localization/ko.txt
new file mode 100644
index 0000000..86d57ee
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/ko.txt
@@ -0,0 +1,78 @@
+# 한국어
+ui.choose_stage=그래프 스테이지 선택
+ui.math_tips=수학 팁 모음
+ui.back_menu=메뉴로
+ui.graphic_calculator_mode=그래픽 계산기 모드
+ui.loading=불러오는 중…
+ui.language=언어
+ui.tap_change_language=탭하여 변경
+ui.close=닫기
+ui.math_concepts=수학 개념
+ui.jump=점프
+ui.move=이동
+ui.keyboard_hint_mobile=(키보드: 방향키 / 스페이스)
+hud.stage=스테이지
+controls.mobile=이동 ◀ ▶ · 점프 탭 (키보드: 방향키 / 스페이스)
+controls.desktop=이동 ← → · 점프 스페이스
+controls.calculator=그래픽 계산기 f(u) 입력 · Trans · Scale · 핀치 · 뒤로
+graph.label_fu=f(u) =
+graph.placeholder=x^2 + sin(x) · ln(x) (x>0) · min(x,3)...
+graph.status_enter=Enter / 바깥 탭으로 그래프
+graph.status_graphed=그래프 완료
+graph.line1=그래픽 계산기 모드
+graph.line2=아래에 f(u) 입력(상자 안 변수 x = 내부 u). 그다음:
+graph.line3=Trans → {0} · 더블탭 + · 길게 − · Scale 탭 확대 / 길게 축소 · 두 손가락 핀치
+graph.line4=A={0} k={1} C={2} D={3} x ∈ [{4},{5}]
+graph.param_a=A (세로 스케일)
+graph.param_k=k (가로 스케일)
+graph.param_c=C (세로 이동)
+graph.param_d=D (가로 이동)
+graph.calculator_intro=그래픽 계산기 모드\n거의 모든 f(u) 입력(식의 변수 x); Trans로 A,k,C,D 조절; Scale·핀치로 창 확대.
+footer.proprietary=© 2022-2026 · 무단 복제 금지 · First Principles
+footer.attribution=GAME GENESIS (Rayan Kaissi) × ORCH AEROSPACE (John Wonmo Seong)
+footer.school=Garth Webb Secondary School, Oakville 졸업생들이 자랑스럽습니다.
+footer.support=4년간의 개발 — 이 작품이 가치 있다면 프로젝트를 응원해 주세요. 감사합니다.
+footer.unity=Unity로 제작. Unity는 Unity Technologies의 상표입니다.
+level.0=첫 원리 입문
+level.1=포물선의 기울기
+level.2=사인 물결
+level.3=코사인의 그림자
+level.4=절대값 경로
+level.5=매클로린: e^x
+level.6=매클로린: sin(x)
+level.7=급수: 기하 꼬리
+level.8=안장 단면 (다변수)
+level.9=포물면 단면 (다변수)
+level.10=곡선 아래 면적
+level.11=리만: 왼쪽 끝점
+level.12=리만: 오른쪽 끝점
+level.13=리만: 중점 공식
+level.14=공학: 감쇠 진동
+level.15=공학: 현수선 (cosh)
+level.16=공학: 정류 AC (|sin|)
+level.17=BC: 아크탄젠트와 역삼각
+level.18=BC: 로지스틱 성장 (dP/dt = kP(1−P/L))
+level.19=극좌표: 카디오이드 r ~ 1+cos θ
+level.20=극좌표: 장미 r ~ cos(nθ)
+level.21=BC: sinh x와 쌍곡선 함수
+level.22=물리C: 지수 감쇠 (τ, RC)
+level.23=물리C: 각운동량과 L = Iω
+level.24=물리C: 포물선 높이 y(t)
+level.25=BC: 매클로린 cos(x)
+level.26=BC: ln x와 ∫ dx/x
+level.27=BC: √x와 정의역/뾰족점
+level.28=BC: 점근선 사이 tan x
+level.29=BC: e^{kx}와 y′ = ky
+level.30=BC: 위상과 단진동 (에너지)
+level.31=BC: 삼차함수와 변곡점 (스케치)
+level.32=BC: b^x와 d/dx b^x
+level.33=원: (x−h)² + (y−k)² = R²
+level.34=항공: 양력 C_L(α) 선형 및 실속
+level.35=항공: 포물형 항극곡선 C_D(C_L)
+level.36=항공: 등온 대기 ρ(h)
+level.37=항공: 퓨고이드 / 감쇠 피치-헤이브
+level.38=항공: 뉴턴 Cp ~ sin²α
+level.39=항공: 스트루할 / 와류 방출 음
+level.40=항공: 재진입 감쇠 포락선 (ρV 가열)
+level.41=경시: ln, 오목성과 경계
+level.42=보스: 만델브로 탈출 단면 (프랙탈)
diff --git a/First Principles/Assets/Resources/Localization/ko.txt.meta b/First Principles/Assets/Resources/Localization/ko.txt.meta
new file mode 100644
index 0000000..c11c961
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/ko.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 8e74202c5d24b4988b1f9287620f208f
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Resources/Localization/zh.txt b/First Principles/Assets/Resources/Localization/zh.txt
new file mode 100644
index 0000000..3765114
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/zh.txt
@@ -0,0 +1,78 @@
+# 简体中文
+ui.choose_stage=选择图象关卡
+ui.math_tips=数学提示摘录
+ui.back_menu=返回菜单
+ui.graphic_calculator_mode=图形计算器模式
+ui.loading=加载中…
+ui.language=语言
+ui.tap_change_language=点击切换
+ui.close=关闭
+ui.math_concepts=数学概念
+ui.jump=跳跃
+ui.move=移动
+ui.keyboard_hint_mobile=(键盘:方向键 / 空格)
+hud.stage=阶段
+controls.mobile=移动 ◀ ▶ · 跳跃 点按 (键盘:方向键 / 空格)
+controls.desktop=移动 ← → · 跳跃 空格
+controls.calculator=图形计算器 输入 f(u) · Trans · Scale · 双指缩放 · 返回
+graph.label_fu=f(u) =
+graph.placeholder=x^2 + sin(x) · ln(x)(x>0)· min(x,3)...
+graph.status_enter=回车 / 点击外部绘制图象
+graph.status_graphed=已绘制
+graph.line1=图形计算器模式
+graph.line2=在下方输入 f(u)(框内变量 x 为内层 u)。然后:
+graph.line3=Trans → {0} · 双击 + · 长按 − · Scale 点按放大 / 长按缩小 · 双指 捏合
+graph.line4=A={0} k={1} C={2} D={3} x 在 [{4},{5}] 内
+graph.param_a=A(纵向缩放)
+graph.param_k=k(横向缩放)
+graph.param_c=C(纵向平移)
+graph.param_d=D(横向平移)
+graph.calculator_intro=图形计算器模式\n可输入几乎任意 f(u)(式中变量 x);Trans 调整 A、k、C、D;Scale 与 捏合 缩放窗口。
+footer.proprietary=© 2022-2026 · 专有软件 · 保留所有权利 · First Principles
+footer.attribution=GAME GENESIS (Rayan Kaissi) × ORCH AEROSPACE (John Wonmo Seong)
+footer.school=毕业于 Garth Webb Secondary School(奥克维尔),引以为豪。
+footer.support=历时四年的开发——若您觉得有价值,请支持本项目。谢谢。
+footer.unity=使用 Unity 制作。Unity 为 Unity Technologies 的商标。
+level.0=第一性原理入门
+level.1=抛物线斜率
+level.2=正弦波动
+level.3=余弦掠影
+level.4=绝对值路径
+level.5=麦克劳林:e^x
+level.6=麦克劳林:sin(x)
+level.7=级数:几何尾部
+level.8=鞍点切片(多元)
+level.9=抛物面切片(多元)
+level.10=曲线下面积
+level.11=黎曼:左端点
+level.12=黎曼:右端点
+level.13=黎曼:中点法则
+level.14=工程:阻尼振动
+level.15=工程:悬链线(cosh)
+level.16=工程:整流交流(|sin|)
+level.17=BC:反正切与反三角
+level.18=BC:逻辑斯蒂增长(dP/dt = kP(1−P/L))
+level.19=极坐标:心形线 r ~ 1+cos θ
+level.20=极坐标:玫瑰线 r ~ cos(nθ)
+level.21=BC:sinh x 与双曲函数
+level.22=物理C:指数衰减(τ, RC)
+level.23=物理C:角动量与 L = Iω
+level.24=物理C:抛体高度 y(t)
+level.25=BC:麦克劳林 cos(x)
+level.26=BC:ln x 与 ∫ dx/x
+level.27=BC:√x 与定义域/尖点
+level.28=BC:渐近线之间的 tan x
+level.29=BC:e^{kx} 与 y′ = ky
+level.30=BC:相位与简谐振动(能量)
+level.31=BC:三次与拐点(作图)
+level.32=BC:b^x 与 d/dx b^x
+level.33=圆:(x−h)² + (y−k)² = R²
+level.34=航空:升力 C_L(α) 线性+失速
+level.35=航空:抛物型阻力极曲线 C_D(C_L)
+level.36=航空:等温大气 ρ(h)
+level.37=航空:起伏运动 / 阻尼俯仰-沉浮
+level.38=航空:牛顿流 Cp ~ sin²α
+level.39=航空:斯特劳哈尔 / 涡脱落音
+level.40=航空:再入衰减包络(ρV 加热)
+level.41=竞赛数学:ln、凹性与界
+level.42=BOSS:曼德博逃逸截面(分形)
diff --git a/First Principles/Assets/Resources/Localization/zh.txt.meta b/First Principles/Assets/Resources/Localization/zh.txt.meta
new file mode 100644
index 0000000..94ad402
--- /dev/null
+++ b/First Principles/Assets/Resources/Localization/zh.txt.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 7fd139c87eaa741f69140e75c4ccbe8c
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Scenes/Game.unity b/First Principles/Assets/Scenes/Game.unity
index bc58abf..f629fc4 100644
--- a/First Principles/Assets/Scenes/Game.unity
+++ b/First Principles/Assets/Scenes/Game.unity
@@ -283,7 +283,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_text: f(x) = (0.4(x - 2))^3 + (-2)
+ m_text: f(x) = 0.38*((0.42*(x - 0))^2 + (-1.85))
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 79fe93baac0634b6a88831eb18f73b04, type: 2}
m_sharedMaterial: {fileID: -5611889265188688309, guid: 79fe93baac0634b6a88831eb18f73b04, type: 2}
@@ -372,6 +372,7 @@ GameObject:
- component: {fileID: 212810172}
- component: {fileID: 212810171}
- component: {fileID: 212810170}
+ - component: {fileID: 2134999001}
m_Layer: 5
m_Name: Canvas
m_TagString: Untagged
@@ -411,14 +412,26 @@ MonoBehaviour:
m_UiScaleMode: 1
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
- m_ReferenceResolution: {x: 1920, y: 1080}
+ m_ReferenceResolution: {x: 1080, y: 1920}
m_ScreenMatchMode: 0
- m_MatchWidthOrHeight: 0
+ m_MatchWidthOrHeight: 0.45
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
+--- !u!114 &2134999001
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 212810169}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: c4d5e6f708192a3b4c5d6e7f8090a1b2, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
--- !u!223 &212810172
Canvas:
m_ObjectHideFlags: 0
@@ -1143,7 +1156,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
- m_IsActive: 1
+ m_IsActive: 0
--- !u!224 &848927268
RectTransform:
m_ObjectHideFlags: 0
@@ -2411,11 +2424,11 @@ MonoBehaviour:
xStart: -20
xEnd: 20
step: 0.1
- transA: 1
- transK: 0.4
- transC: -2
- transD: 2
- power: 3
+ transA: 0.38
+ transK: 0.42
+ transC: -1.85
+ transD: 0
+ power: 2
functionType: 0
baseN: 2
differentiate: 1
@@ -2482,7 +2495,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
- m_IsActive: 1
+ m_IsActive: 0
--- !u!224 &1471649600
RectTransform:
m_ObjectHideFlags: 0
diff --git a/First Principles/Assets/Scenes/LevelSelect.unity b/First Principles/Assets/Scenes/LevelSelect.unity
index 4ffa4ca..1b0744a 100644
--- a/First Principles/Assets/Scenes/LevelSelect.unity
+++ b/First Principles/Assets/Scenes/LevelSelect.unity
@@ -284,6 +284,7 @@ GameObject:
- component: {fileID: 500013}
- component: {fileID: 500012}
- component: {fileID: 500011}
+ - component: {fileID: 2134999003}
m_Layer: 5
m_Name: Canvas
m_TagString: Untagged
@@ -342,14 +343,26 @@ MonoBehaviour:
m_UiScaleMode: 1
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
- m_ReferenceResolution: {x: 1920, y: 1080}
+ m_ReferenceResolution: {x: 1080, y: 1920}
m_ScreenMatchMode: 0
- m_MatchWidthOrHeight: 0
+ m_MatchWidthOrHeight: 0.45
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
+--- !u!114 &2134999003
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 500009}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: c4d5e6f708192a3b4c5d6e7f8090a1b2, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
--- !u!223 &500013
Canvas:
m_ObjectHideFlags: 0
diff --git a/First Principles/Assets/Scenes/Menu.unity b/First Principles/Assets/Scenes/Menu.unity
index 2aeffbc..2f35b6c 100644
--- a/First Principles/Assets/Scenes/Menu.unity
+++ b/First Principles/Assets/Scenes/Menu.unity
@@ -591,12 +591,23 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_text: 'Alpha Build 0.3
+ m_text: 'First Principles
+ Version 1.0
- Powered by Unity.
+ Calculus reader: Level select → Math tips · Game → Math concepts (top-right)
- Developed
- by Rayan Kaissi and John Seong.'
+ Credits
+ GAME GENESIS (Rayan Kaissi) × ORCH AEROSPACE (John Wonmo Seong)
+
+ Proud graduates of Garth Webb Secondary School, Oakville.
+
+ Four years of development — if you value this work, please support the project. Thank you.
+
+ © 2022-2026 · Proprietary · All rights reserved · First Principles
+
+ Stages include primer, integrals, AP BC, Physics C, Aerospace, and more — see Level select list.
+
+ Made with Unity. Unity is a trademark of Unity Technologies.'
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 79fe93baac0634b6a88831eb18f73b04, type: 2}
m_sharedMaterial: {fileID: -5611889265188688309, guid: 79fe93baac0634b6a88831eb18f73b04, type: 2}
@@ -1026,6 +1037,7 @@ GameObject:
- component: {fileID: 1372280671}
- component: {fileID: 1372280670}
- component: {fileID: 1372280669}
+ - component: {fileID: 2134999002}
m_Layer: 5
m_Name: Canvas
m_TagString: Untagged
@@ -1065,14 +1077,26 @@ MonoBehaviour:
m_UiScaleMode: 1
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
- m_ReferenceResolution: {x: 1920, y: 1080}
+ m_ReferenceResolution: {x: 1080, y: 1920}
m_ScreenMatchMode: 0
- m_MatchWidthOrHeight: 0
+ m_MatchWidthOrHeight: 0.45
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 0
+--- !u!114 &2134999002
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1372280668}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: c4d5e6f708192a3b4c5d6e7f8090a1b2, type: 3}
+ m_Name:
+ m_EditorClassIdentifier:
--- !u!223 &1372280671
Canvas:
m_ObjectHideFlags: 0
diff --git a/First Principles/Assets/Scripts/Functions/FunctionPlotter.cs b/First Principles/Assets/Scripts/Functions/FunctionPlotter.cs
index 1b1d99d..4f934fc 100644
--- a/First Principles/Assets/Scripts/Functions/FunctionPlotter.cs
+++ b/First Principles/Assets/Scripts/Functions/FunctionPlotter.cs
@@ -1,9 +1,12 @@
/*
- * FunctionPlotter.cs Written by John Seong
- * An Open-Source Project
- * Main Features:
- * 1. Plot Functions
- * 2. Plot Their Corresponding First Derivatives
+ * FunctionPlotter.cs — John Seong / First Principles
+ *
+ * Maintenance overview:
+ * • Each Update() calls InitPlotFunction → samples f and numeric f' over [xStart,xEnd].
+ * • Points are in “grid space”: (xPlot + gridOrigin.x, yPlot + gridOrigin.y).
+ * • To add a new curve: extend FunctionType, EvaluateFunctionY, and UpdateEquationText.
+ * • LevelManager sets public fields to match LevelDefinition; differentiate=true feeds DerivRendererUI.
+ * • SampleCurvePlotterY / SetEquationExtraSuffix support Riemann overlay & TMP sub-lines.
*/
using System.Collections.Generic;
@@ -34,6 +37,10 @@ public class FunctionPlotter : MonoBehaviour
public int baseN = 2;
+ /// When is , evaluates this as f(u) with u = transK·(x−transD); plotted y = transA·f(u)+transC.
+ [TextArea(1, 3)]
+ public string customExpression = "x^2";
+
public bool differentiate = false;
// Local points list
@@ -42,6 +49,9 @@ public class FunctionPlotter : MonoBehaviour
[SerializeField] TextMeshProUGUI equationText;
+ /// Optional second line under the main equation (e.g. Riemann / integral note).
+ private string equationExtraSuffix = "";
+
private LineRendererUI lineRenderer;
private DerivRendererUI derivRenderer;
@@ -90,6 +100,26 @@ public void RefreshGrid()
grid.enabled = true;
}
+ ///
+ /// f(x) in plotter coordinates (before adding grid origin), using current transforms.
+ ///
+ public float SampleCurvePlotterY(float xPlotter)
+ {
+ return EvaluateFunctionY(functionType, transA, transK, transC, transD, power, baseN, xPlotter);
+ }
+
+ public void SetEquationExtraSuffix(string suffix)
+ {
+ equationExtraSuffix = suffix ?? "";
+ }
+
+ /// Switches to typed expression mode (graphic calculator).
+ public void SetCustomExpression(string expression)
+ {
+ customExpression = string.IsNullOrWhiteSpace(expression) ? "0" : expression.Trim();
+ functionType = FunctionType.CustomExpression;
+ }
+
private void PlotFunction(FunctionType type)
{
lineRenderer = FindAnyObjectByType();
@@ -162,6 +192,9 @@ private float EvaluateFunctionY(FunctionType type, float transA, float transK, f
return type switch
{
+ FunctionType.CustomExpression =>
+ EvaluateCustomExpression(transA, transC, u),
+
FunctionType.Power => transA * (Mathf.Pow(u, power) + transC),
FunctionType.Absolute => transA * (Mathf.Abs(u) + transC),
FunctionType.Exponential => transA * (Mathf.Pow(baseN, u) + transC),
@@ -172,12 +205,208 @@ private float EvaluateFunctionY(FunctionType type, float transA, float transK, f
FunctionType.Sine => transA * (Mathf.Sin(u) + transC),
FunctionType.Cosine => transA * (Mathf.Cos(u) + transC),
FunctionType.Tangent => transA * (Mathf.Tan(u) + transC),
+
+ // Maclaurin (Taylor at 0) partial sums — `power` = number of nonzero terms beyond constant where applicable.
+ FunctionType.MaclaurinExpSeries => transA * (MaclaurinExpPartialSum(u, power) + transC),
+ FunctionType.MaclaurinSinSeries => transA * (MaclaurinSinPartialSum(u, power) + transC),
+ FunctionType.MaclaurinCosSeries => transA * (MaclaurinCosPartialSum(u, power) + transC),
+
+ // Geometric series partial sum Σ u^k, k=0..power — avoid |u|≥1 for stability in view.
+ FunctionType.GeometricSeriesPartial => transA * (GeometricPartialSum(u, power) + transC),
+
+ // Multivariable "slices": fix y0 = transC, plot z along x — u = transK*(x - transD). (transD is x-phase only.)
+ FunctionType.MultivarParaboloidSlice => transA * (u * u + transC * transC),
+ FunctionType.MultivarSaddleSlice => transA * (u * u - transC * transC),
+
+ // Engineering / applied: u = transK*(x - transD); `power` ↔ oscillation index; `baseN` ↔ decay strength.
+ FunctionType.DampedOscillator => DampedOscillatorY(u, transA, transC, power, baseN),
+ FunctionType.HyperbolicCosine => transA * ((float)System.Math.Cosh(Mathf.Clamp(u, -8f, 8f)) + transC),
+ FunctionType.FullWaveRectifiedSine => transA * (Mathf.Abs(Mathf.Sin(u)) + transC),
+
+ // AP Calculus BC & polar: u = transK*(x - transD) plays the role of θ in polar captions.
+ FunctionType.Arctangent => transA * Mathf.Atan(u) + transC,
+ FunctionType.Logistic => LogisticY(u, transA, transC, baseN),
+ FunctionType.HyperbolicSine => transA * (float)System.Math.Sinh(Mathf.Clamp(u, -4f, 4f)) + transC,
+ FunctionType.ExponentialDecay => transA * Mathf.Exp(-Mathf.Max(0.02f, transK) * Mathf.Abs(u)) + transC,
+ FunctionType.PolarCardioid => transA * (1f + Mathf.Cos(u)) + transC,
+ FunctionType.PolarRose => transA * Mathf.Cos(Mathf.Max(1, power) * u) + transC,
+
+ // Upper half of (u)² + (y−k)² = R² with u = transK·(x−h), R = |transA|, k = transC, h = transD.
+ FunctionType.CircleUpper => CircleUpperY(u, transA, transC),
+
+ // Aerospace / aerodynamics teaching curves (u = transK·(x−transD)).
+ FunctionType.AeroLiftVsAlpha => AeroLiftVsAlphaY(u, transA, transC),
+ FunctionType.AeroIsothermalDensity => AeroIsothermalDensityY(u, transA, transC, baseN),
+ FunctionType.AeroNewtonianSinSquared => AeroNewtonianSinSquaredY(u, transA, transC),
+
+ // Mandelbrot: escape-time vs Im(c) with Re(c)=transA; use |Im| inside iteration (same count as c̄) — cheap symmetry about the real axis.
+ FunctionType.MandelbrotEscapeImSlice => MandelbrotEscapeImSliceY(u, transA, transC, power, baseN),
+
_ => 0f
};
}
+ private float EvaluateCustomExpression(float transA, float transC, float u)
+ {
+ if (MathExpressionEvaluator.TryEvaluate(customExpression, u, out float fy, out _))
+ return transA * fy + transC;
+ return float.NaN;
+ }
+
+ ///
+ /// 1D slice through the Mandelbrot diag: c = cr + i·u, y ≈ normalized escape iterations (cheap preview, not deep zoom).
+ /// Uses = |u| when computing z²+c since n(c) = n(c̄). Keep modest (e.g. 24–32) for CPU.
+ ///
+ private static float MandelbrotEscapeImSliceY(float u, float cr, float yOffset, int maxIter, int heightScaleFromBaseN)
+ {
+ maxIter = Mathf.Clamp(maxIter, 10, 34);
+ float amp = 0.14f * Mathf.Max(1, heightScaleFromBaseN);
+ int it = MandelbrotEscapeIterations(cr, Mathf.Abs(u), maxIter);
+ float norm = it / (float)maxIter;
+ return yOffset + amp * norm;
+ }
+
+ private static int MandelbrotEscapeIterations(float cr, float ci, int maxIter)
+ {
+ float zr = 0f, zi = 0f;
+ for (int n = 0; n < maxIter; n++)
+ {
+ float zr2 = zr * zr - zi * zi + cr;
+ float zi2 = 2f * zr * zi + ci;
+ zr = zr2;
+ zi = zi2;
+ if (zr * zr + zi * zi > 4f)
+ return n;
+ }
+ return maxIter;
+ }
+
+ /// Crude CL(α): linear to ±α_stall then exponential decay (stall).
+ private static float AeroLiftVsAlphaY(float u, float liftSlope, float c)
+ {
+ float stall = 0.58f;
+ if (Mathf.Abs(u) <= stall)
+ return liftSlope * u + c;
+ float sign = Mathf.Sign(u);
+ float peak = liftSlope * stall * sign;
+ return peak * Mathf.Exp(-(Mathf.Abs(u) - stall) * 1.12f) + c;
+ }
+
+ /// ρ/ρ₀ ~ e^{−h/H} for h≥0; u as scaled altitude. baseN scales 1/H.
+ private static float AeroIsothermalDensityY(float u, float rhoScale, float c, int scaleHeightInv)
+ {
+ float h = Mathf.Max(0f, u);
+ float invH = 0.055f * Mathf.Max(1, scaleHeightInv > 0 ? scaleHeightInv : 1);
+ return rhoScale * Mathf.Exp(-invH * h) + c;
+ }
+
+ /// Newtonian impact theory mood: Cp ~ sin²α for α∈[0,π/2].
+ private static float AeroNewtonianSinSquaredY(float u, float a, float c)
+ {
+ float rad = Mathf.Clamp(u, 0f, 1.48f);
+ float s = Mathf.Sin(rad);
+ return a * s * s + c;
+ }
+
+ /// y = k + √(R² − u²) for |u|≤R; outside domain uses k so samples stay finite (flat shoulder).
+ private static float CircleUpperY(float u, float radiusSigned, float k)
+ {
+ float r = Mathf.Max(0.02f, Mathf.Abs(radiusSigned));
+ float s = r * r - u * u;
+ if (s < 0f)
+ return float.NaN;
+ return k + Mathf.Sqrt(s);
+ }
+
+ /// S-curve L/(1+e^{-s u}) + C; scales steepness (larger → sharper transition).
+ private static float LogisticY(float u, float carryingCapacity, float c, int steepnessFromBaseN)
+ {
+ float s = 0.05f * Mathf.Max(1, steepnessFromBaseN > 0 ? steepnessFromBaseN : 1);
+ float z = Mathf.Clamp(-s * u, -30f, 30f);
+ return carryingCapacity / (1f + Mathf.Exp(z)) + c;
+ }
+
+ /// Underdamped-style envelope A·e^(-α|u|)·sin(ωu) + C (u = scaled time/position).
+ private static float DampedOscillatorY(float u, float a, float c, int power, int baseN)
+ {
+ float omega = 0.28f * Mathf.Max(1, power);
+ float decay = 0.042f * Mathf.Max(1, baseN);
+ return a * Mathf.Exp(-decay * Mathf.Abs(u)) * Mathf.Sin(omega * u) + c;
+ }
+
+ /// e^u ≈ Σ_{k=0}^{N} u^k/k!
+ private static float MaclaurinExpPartialSum(float u, int maxDegree)
+ {
+ maxDegree = Mathf.Clamp(maxDegree, 0, 18);
+ float sum = 0f;
+ float term = 1f;
+ sum += term;
+ for (int k = 1; k <= maxDegree; k++)
+ {
+ term *= u / k;
+ sum += term;
+ if (!IsFinite(sum)) break;
+ }
+ return sum;
+ }
+
+ /// sin u ≈ Σ (-1)^n u^{2n+1}/(2n+1)! up to n = maxN.
+ private static float MaclaurinSinPartialSum(float u, int maxN)
+ {
+ maxN = Mathf.Clamp(maxN, 0, 12);
+ float sum = 0f;
+ float term = u;
+ sum += term;
+ for (int n = 1; n <= maxN; n++)
+ {
+ term *= -u * u / ((2f * n) * (2f * n + 1f));
+ sum += term;
+ if (!IsFinite(sum)) break;
+ }
+ return sum;
+ }
+
+ /// cos u ≈ Σ (-1)^n u^{2n}/(2n)! up to n = maxN.
+ private static float MaclaurinCosPartialSum(float u, int maxN)
+ {
+ maxN = Mathf.Clamp(maxN, 0, 12);
+ float sum = 0f;
+ float term = 1f;
+ sum += term;
+ for (int n = 1; n <= maxN; n++)
+ {
+ term *= -u * u / ((2f * n - 1f) * (2f * n));
+ sum += term;
+ if (!IsFinite(sum)) break;
+ }
+ return sum;
+ }
+
+ /// Σ_{k=0}^{N} u^k for N = maxPower (geometric partial sum).
+ private static float GeometricPartialSum(float u, int maxPower)
+ {
+ maxPower = Mathf.Clamp(maxPower, 0, 24);
+ float sum = 0f;
+ float uPow = 1f;
+ sum += uPow;
+ for (int k = 1; k <= maxPower; k++)
+ {
+ uPow *= u;
+ sum += uPow;
+ if (!IsFinite(sum)) break;
+ }
+ return sum;
+ }
+
private static bool IsFinite(float f) => !(float.IsNaN(f) || float.IsInfinity(f));
+ private static string EscapeTmpRichText(string raw)
+ {
+ if (string.IsNullOrEmpty(raw))
+ return "";
+ return raw.Replace("&", "&").Replace("<", "<").Replace(">", ">");
+ }
+
private void UpdateEquationText(FunctionType type, float transA, float transK, float transC, float transD, int power, int baseN)
{
// Keep equation text simple and consistent; domain errors (e.g. log/sqrt of non-positive) are handled by skipping non-finite points.
@@ -189,45 +418,170 @@ private void UpdateEquationText(FunctionType type, float transA, float transK, f
switch (type)
{
case FunctionType.Power:
- equationText.text = $"f(x) = {a}*(({k}*(x - {d}))^{power} + ({c}))";
+ equationText.text = $@"\(f(x) = {a}\cdot\left({k}(x-{d})\right)^{{{power}}} + {c}\)";
break;
case FunctionType.Absolute:
- equationText.text = $"f(x) = {a}*(|{k}*(x - {d})| + ({c}))";
+ equationText.text = $@"\(f(x) = {a}\left(\left|{k}(x-{d})\right| + {c}\right)\)";
break;
case FunctionType.Exponential:
- equationText.text = $"f(x) = {a}*({baseN}^({k}*(x - {d})) + ({c}))";
+ // transA* (baseN^(k(x-d)) + c)
+ equationText.text = $@"\(f(x) = {a}\left({baseN}^{{{k}(x-{d})}} + {c}\right)\)";
break;
case FunctionType.NaturalExp:
- equationText.text = $"f(x) = {a}*(e^({k}*(x - {d})) + ({c}))";
+ equationText.text = $@"\(f(x) = {a}\left(e^{{{k}(x-{d})}} + {c}\right)\)";
break;
case FunctionType.Log:
- equationText.text = $"f(x) = {a}*(log10({k}*(x - {d})) + ({c}))";
+ equationText.text = $@"\(f(x) = {a}\left(\log_{10}\!\left({k}(x-{d})\right) + {c}\right)\)";
break;
case FunctionType.NaturalLog:
- equationText.text = $"f(x) = {a}*(ln({k}*(x - {d})) + ({c}))";
+ equationText.text = $@"\(f(x) = {a}\left(\ln\left({k}(x-{d})\right) + {c}\right)\)";
break;
case FunctionType.SquareRoot:
- equationText.text = $"f(x) = {a}*(sqrt({k}*(x - {d})) + ({c}))";
+ equationText.text = $@"\(f(x) = {a}\left(\sqrt{{{k}(x-{d})}} + {c}\right)\)";
break;
case FunctionType.Sine:
- equationText.text = $"f(x) = {a}*(sin({k}*(x - {d})) + ({c}))";
+ equationText.text = $@"\(f(x) = {a}\left(\sin\left({k}(x-{d})\right) + {c}\right)\)";
break;
case FunctionType.Cosine:
- equationText.text = $"f(x) = {a}*(cos({k}*(x - {d})) + ({c}))";
+ equationText.text = $@"\(f(x) = {a}\left(\cos\left({k}(x-{d})\right) + {c}\right)\)";
break;
case FunctionType.Tangent:
- equationText.text = $"f(x) = {a}*(tan({k}*(x - {d})) + ({c}))";
+ equationText.text = $@"\(f(x) = {a}\left(\tan\left({k}(x-{d})\right) + {c}\right)\)";
+ break;
+ case FunctionType.MaclaurinExpSeries:
+ equationText.text = $@"\(P_{{{power}}}[e^{{u}}],\; u={k}(x-{d})\)";
break;
+ case FunctionType.MaclaurinSinSeries:
+ equationText.text = $@"\(P_{{{power}}}[\sin u],\; u={k}(x-{d})\)";
+ break;
+ case FunctionType.MaclaurinCosSeries:
+ equationText.text = $@"\(P_{{{power}}}[\cos u],\; u={k}(x-{d})\)";
+ break;
+ case FunctionType.GeometricSeriesPartial:
+ equationText.text = $@"\(\sum_{{j=0}}^{{{power}}} u^{{j}},\; u={k}(x-{d})\)";
+ break;
+ case FunctionType.MultivarParaboloidSlice:
+ equationText.text = $@"\(z={a}\left(u^{2}+y_{{0}}^{2}\right),\; u={k}(x-{d}),\; y_{{0}}={c}\)";
+ break;
+ case FunctionType.MultivarSaddleSlice:
+ equationText.text = $@"\(z={a}\left(u^{2}-y_{{0}}^{2}\right),\; u={k}(x-{d}),\; y_{{0}}={c}\)";
+ break;
+ case FunctionType.DampedOscillator:
+ equationText.text = $@"\(f={a}\, e^{{-\alpha|u|}}\sin(\omega u)+{c},\; u={k}(x-{d})\)";
+ break;
+ case FunctionType.HyperbolicCosine:
+ equationText.text = $@"\(f={a}\left(\cosh(u)+{c}\right),\; u={k}(x-{d})\)";
+ break;
+ case FunctionType.FullWaveRectifiedSine:
+ equationText.text = $@"\(f={a}\left(\left|\sin u\right|+{c}\right),\; u={k}(x-{d})\)";
+ break;
+ case FunctionType.Arctangent:
+ equationText.text = $@"\(f={a}\arctan(u)+{c},\; u={k}(x-{d})\)";
+ break;
+ case FunctionType.Logistic:
+ equationText.text = $@"\(\text{{Logistic}},\; L\approx{a},\; u={k}(x-{d})\)";
+ break;
+ case FunctionType.HyperbolicSine:
+ equationText.text = $@"\(f={a}\sinh(u)+{c},\; u={k}(x-{d})\)";
+ break;
+ case FunctionType.ExponentialDecay:
+ equationText.text = $@"\(f={a}\, e^{{-{k}|u|}}+{c},\; u={k}(x-{d})\)";
+ break;
+ case FunctionType.PolarCardioid:
+ equationText.text = $@"\(r \propto 1+\cos\theta,\; \theta\leftrightarrow u={k}(x-{d})\)";
+ break;
+ case FunctionType.PolarRose:
+ equationText.text = $@"\(r \propto \cos({power}\theta),\; \theta\leftrightarrow u={k}(x-{d})\)";
+ break;
+ case FunctionType.CircleUpper:
+ equationText.text = $@"\(u^{2}+(y-{c})^{2}={a}^{2}\ \text{{(upper)}},\; u={k}(x-{d})\)";
+ break;
+ case FunctionType.AeroLiftVsAlpha:
+ equationText.text = $@"\(C_L(\alpha)\ \text{{stall model}},\; \text{{slope}}\approx {a},\; u={k}(x-{d})\)";
+ break;
+ case FunctionType.AeroIsothermalDensity:
+ equationText.text = $@"\(\rho/\rho_0 \propto e^{{-h/H}},\; u={k}(x-{d}),\; H^{{-1}}\propto {baseN}\)";
+ break;
+ case FunctionType.AeroNewtonianSinSquared:
+ equationText.text = $@"\(C_p \propto \sin^{{2}}\alpha,\; u={k}(x-{d})\)";
+ break;
+ case FunctionType.MandelbrotEscapeImSlice:
+ equationText.text =
+ $@"\(\text{{Mandelbrot slice}}\) \(h\propto\text{{escape-time}},\; c=({a})+\mathrm{{i}}u,\; u={k}(x-{d}),\; N={power}\)";
+ break;
+ case FunctionType.CustomExpression:
+ {
+ string esc = EscapeTmpRichText(customExpression);
+ equationText.text =
+ $"Typed f(u)\n" +
+ $"{esc}\n" +
+ $"Plotted: y = A·f(u)+C, u = k·(x−D). Edit below. Use sin, cos, tan, sqrt, ln, log, exp, ^, pi, e …";
+ if (!string.IsNullOrEmpty(equationExtraSuffix))
+ equationText.text += $"\n{equationExtraSuffix}";
+ return;
+ }
default:
- equationText.text = "f(x)";
+ equationText.text = @"\(f(x)\)";
break;
}
+
+ if (!string.IsNullOrEmpty(equationExtraSuffix))
+ equationText.text += $"\n{equationExtraSuffix}";
+
+ if (equationText != null)
+ equationText.text = TmpLatex.Process(equationText.text);
}
}
public enum FunctionType
{
- Power, Absolute, Exponential, NaturalExp, Log, NaturalLog, SquareRoot, Sine, Cosine, Tangent
+ Power,
+ Absolute,
+ Exponential,
+ NaturalExp,
+ Log,
+ NaturalLog,
+ SquareRoot,
+ Sine,
+ Cosine,
+ Tangent,
+
+ // Infinite series / Taylor–Maclaurin (partial sums)
+ MaclaurinExpSeries,
+ MaclaurinSinSeries,
+ MaclaurinCosSeries,
+ GeometricSeriesPartial,
+
+ // Multivariable surfaces as 1D slices (fixed y₀ = transC, δ = transD shift)
+ MultivarParaboloidSlice,
+ MultivarSaddleSlice,
+
+ // Engineering / applied classical shapes
+ DampedOscillator,
+ HyperbolicCosine,
+ FullWaveRectifiedSine,
+
+ // AP Calculus BC extras, polar (r vs θ plotted with θ on the horizontal axis), Physics C hooks
+ Arctangent,
+ Logistic,
+ HyperbolicSine,
+ ExponentialDecay,
+ PolarCardioid,
+ PolarRose,
+
+ // Upper semicircle: y = k + √(R²−u²), u = transK·(x−transD), R = |transA|, center (transD, transC) when transK = 1.
+ CircleUpper,
+
+ // Aerospace / aerodynamics (toy models for instruction — not a CFD solver)
+ AeroLiftVsAlpha,
+ AeroIsothermalDensity,
+ AeroNewtonianSinSquared,
+
+ /// Escape iteration count vs Im(c) with fixed Re(c) = transA (boss slice); uses |Im| in iteration for conjugate symmetry.
+ MandelbrotEscapeImSlice,
+
+ /// User-typed f(u) via (graphic calculator).
+ CustomExpression
}
/*
diff --git a/First Principles/Assets/Scripts/Functions/MathExpressionEvaluator.cs b/First Principles/Assets/Scripts/Functions/MathExpressionEvaluator.cs
new file mode 100644
index 0000000..36aef58
--- /dev/null
+++ b/First Principles/Assets/Scripts/Functions/MathExpressionEvaluator.cs
@@ -0,0 +1,391 @@
+using System;
+using UnityEngine;
+
+///
+/// Small math parser for typed f(x) in graphic calculator mode (no eval(), no code execution).
+/// Supports + − * / ^, parentheses, unary −, sin cos tan asin acos atan sqrt abs,
+/// log (base 10), ln, exp, min max (two args), constants pi e, implicit multiply (e.g. 2x, 2(, )x).
+///
+public static class MathExpressionEvaluator
+{
+ public static bool TryEvaluate(string expression, float x, out float y, out string error)
+ {
+ y = float.NaN;
+ error = null;
+ if (string.IsNullOrWhiteSpace(expression))
+ {
+ error = "Empty expression";
+ return false;
+ }
+
+ try
+ {
+ var p = new Parser(expression.Trim(), x);
+ y = p.ParseExpression();
+ p.SkipWs();
+ if (!p.Eof())
+ {
+ error = $"Unexpected text: \"{p.PeekRest()}\"";
+ return false;
+ }
+
+ return true;
+ }
+ catch (ParseException ex)
+ {
+ error = ex.Message;
+ return false;
+ }
+ }
+
+ /// Quick sanity check at a few sample points (may still be undefined elsewhere).
+ public static bool TryValidateRough(string expression, out string error)
+ {
+ error = null;
+ if (string.IsNullOrWhiteSpace(expression))
+ {
+ error = "Empty expression";
+ return false;
+ }
+
+ foreach (float sx in new[] { -1f, 0f, 0.5f, 1f, 2f })
+ {
+ if (!TryEvaluate(expression, sx, out float y, out string err))
+ {
+ error = err;
+ return false;
+ }
+
+ // NaN at a sample point is OK (domain hole); reject only non-finite overflows.
+ if (float.IsInfinity(y))
+ {
+ error = "Value overflow";
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private class ParseException : Exception
+ {
+ public ParseException(string message) : base(message) { }
+ }
+
+ private class Parser
+ {
+ private readonly string s;
+ private readonly float xVal;
+ private int i;
+
+ public Parser(string src, float x)
+ {
+ s = src;
+ xVal = x;
+ i = 0;
+ }
+
+ public bool Eof() => i >= s.Length;
+
+ public string PeekRest()
+ {
+ SkipWs();
+ return i < s.Length ? s.Substring(i, Mathf.Min(24, s.Length - i)) : "";
+ }
+
+ public void SkipWs()
+ {
+ while (i < s.Length && char.IsWhiteSpace(s[i]))
+ i++;
+ }
+
+ private bool Match(char c)
+ {
+ SkipWs();
+ if (i < s.Length && s[i] == c)
+ {
+ i++;
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool MatchInsensitive(string word)
+ {
+ SkipWs();
+ if (i + word.Length > s.Length)
+ return false;
+ for (int k = 0; k < word.Length; k++)
+ {
+ if (char.ToLowerInvariant(s[i + k]) != word[k])
+ return false;
+ }
+
+ // avoid matching "sin" inside "xsine"
+ if (i + word.Length < s.Length && char.IsLetterOrDigit(s[i + word.Length]))
+ return false;
+
+ i += word.Length;
+ return true;
+ }
+
+ public float ParseExpression() => ParseAddSub();
+
+ private float ParseAddSub()
+ {
+ float v = ParseMulDiv();
+ while (true)
+ {
+ SkipWs();
+ if (Match('+'))
+ v += ParseMulDiv();
+ else if (Match('-'))
+ v -= ParseMulDiv();
+ else
+ break;
+ }
+
+ return v;
+ }
+
+ private float ParseMulDiv()
+ {
+ float v = ParseUnary();
+ while (true)
+ {
+ SkipWs();
+ if (Match('*'))
+ {
+ v *= ParseUnary();
+ }
+ else if (Match('/'))
+ {
+ float d = ParseUnary();
+ if (Mathf.Abs(d) < 1e-12f)
+ return float.NaN;
+ v /= d;
+ }
+ else if (ImplicitMultiplyFollows())
+ {
+ v *= ParseUnary();
+ }
+ else
+ break;
+ }
+
+ return v;
+ }
+
+ private bool ImplicitMultiplyFollows()
+ {
+ if (Eof())
+ return false;
+ SkipWs();
+ if (i >= s.Length)
+ return false;
+ char c = s[i];
+ // After a value, "2x", "2(", ")(" , "2 sin"
+ if (char.IsDigit(c) || c == '.' || c == '(')
+ return true;
+ if (char.IsLetter(c))
+ {
+ // unary minus after * is handled elsewhere; here "3x" "3sin"
+ return true;
+ }
+
+ return false;
+ }
+
+ private float ParseUnary()
+ {
+ SkipWs();
+ if (Match('+'))
+ return ParseUnary();
+ if (Match('-'))
+ return -ParseUnary();
+ return ParsePow();
+ }
+
+ private float ParsePow()
+ {
+ float b = ParsePrimary();
+ SkipWs();
+ if (Match('^') || MatchTwo('*'))
+ {
+ float e = ParsePow(); // right-associative
+ try
+ {
+ // Avoid Mathf.Pow domain noise for simple integers
+ if (Mathf.Abs(e - Mathf.Round(e)) < 1e-5f && e > -30f && e < 30f)
+ return IntPowSafe(b, Mathf.RoundToInt(e));
+ return Mathf.Pow(b, e);
+ }
+ catch
+ {
+ return float.NaN;
+ }
+ }
+
+ return b;
+ }
+
+ private bool MatchTwo(char c)
+ {
+ SkipWs();
+ if (i + 1 < s.Length && s[i] == c && s[i + 1] == c)
+ {
+ i += 2;
+ return true;
+ }
+
+ return false;
+ }
+
+ private static float IntPowSafe(float b, int exp)
+ {
+ if (exp == 0)
+ return 1f;
+ float r = 1f;
+ int ae = Mathf.Abs(exp);
+ for (int k = 0; k < ae; k++)
+ r *= b;
+ return exp < 0 ? 1f / r : r;
+ }
+
+ private float ParsePrimary()
+ {
+ SkipWs();
+ if (i >= s.Length)
+ throw new ParseException("Unexpected end of expression");
+
+ if (Match('('))
+ {
+ float v = ParseExpression();
+ if (!Match(')'))
+ throw new ParseException("Missing ')'");
+ return v;
+ }
+
+ if (char.IsDigit(s[i]) || (s[i] == '.' && i + 1 < s.Length && char.IsDigit(s[i + 1])))
+ return ReadNumber();
+
+ return ParseIdentOrCall();
+ }
+
+ private float ReadNumber()
+ {
+ int start = i;
+ bool dot = false;
+ while (i < s.Length)
+ {
+ char c = s[i];
+ if (char.IsDigit(c))
+ i++;
+ else if (c == '.' && !dot)
+ {
+ dot = true;
+ i++;
+ }
+ else
+ break;
+ }
+
+ if (start == i)
+ throw new ParseException("Expected number");
+ string slice = s.Substring(start, i - start);
+ if (!float.TryParse(slice, System.Globalization.NumberStyles.Float,
+ System.Globalization.CultureInfo.InvariantCulture, out float v))
+ throw new ParseException("Bad number");
+ return v;
+ }
+
+ private float ParseIdentOrCall()
+ {
+ // identifiers: x, pi, e, functions
+ if (MatchInsensitive("sinh"))
+ return (float)System.Math.Sinh(ClampArg(ExpectFn1Arg(), 4f));
+ if (MatchInsensitive("cosh"))
+ return (float)System.Math.Cosh(ClampArg(ExpectFn1Arg(), 8f));
+ if (MatchInsensitive("tanh"))
+ return (float)System.Math.Tanh(ClampArg(ExpectFn1Arg(), 8f));
+ if (MatchInsensitive("asin"))
+ return Mathf.Asin(ExpectFn1Arg());
+ if (MatchInsensitive("acos"))
+ return Mathf.Acos(ExpectFn1Arg());
+ if (MatchInsensitive("atan"))
+ return Mathf.Atan(ExpectFn1Arg());
+ if (MatchInsensitive("sin"))
+ return Mathf.Sin(ExpectFn1Arg());
+ if (MatchInsensitive("cos"))
+ return Mathf.Cos(ExpectFn1Arg());
+ if (MatchInsensitive("tan"))
+ return Mathf.Tan(ExpectFn1Arg());
+ if (MatchInsensitive("sqrt"))
+ return Mathf.Sqrt(ExpectFn1Arg());
+ if (MatchInsensitive("abs"))
+ return Mathf.Abs(ExpectFn1Arg());
+ if (MatchInsensitive("log"))
+ {
+ // log10
+ float a = ExpectFn1Arg();
+ if (a <= 0f)
+ return float.NaN;
+ return Mathf.Log10(a);
+ }
+
+ if (MatchInsensitive("ln"))
+ {
+ float a = ExpectFn1Arg();
+ if (a <= 0f)
+ return float.NaN;
+ return Mathf.Log(a);
+ }
+
+ if (MatchInsensitive("exp"))
+ return Mathf.Exp(ClampArg(ExpectFn1Arg(), 20f));
+ if (MatchInsensitive("min"))
+ return ExpectFn2Args(min: true);
+ if (MatchInsensitive("max"))
+ return ExpectFn2Args(min: false);
+
+ if (MatchInsensitive("pi"))
+ return Mathf.PI;
+ if (MatchInsensitive("e"))
+ return Mathf.Exp(1f);
+ if (MatchInsensitive("x"))
+ return xVal;
+
+ throw new ParseException($"Unknown symbol near \"{PeekRest()}\"");
+ }
+
+ private static float ClampArg(float v, float lim)
+ {
+ return Mathf.Clamp(v, -lim, lim);
+ }
+
+ private float ExpectFn1Arg()
+ {
+ if (!Match('('))
+ throw new ParseException("Expected '(' after function");
+ float v = ParseExpression();
+ if (!Match(')'))
+ throw new ParseException("Missing ')'");
+ return v;
+ }
+
+ private float ExpectFn2Args(bool min)
+ {
+ if (!Match('('))
+ throw new ParseException("Expected '(' after function");
+ float a = ParseExpression();
+ SkipWs();
+ if (!Match(','))
+ throw new ParseException("Expected ','");
+ float b = ParseExpression();
+ if (!Match(')'))
+ throw new ParseException("Missing ')'");
+ return min ? Mathf.Min(a, b) : Mathf.Max(a, b);
+ }
+ }
+}
diff --git a/First Principles/Assets/Scripts/Functions/MathExpressionEvaluator.cs.meta b/First Principles/Assets/Scripts/Functions/MathExpressionEvaluator.cs.meta
new file mode 100644
index 0000000..89a89dc
--- /dev/null
+++ b/First Principles/Assets/Scripts/Functions/MathExpressionEvaluator.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6c5470cb1309a4cdda64fe2bf3c67173
\ No newline at end of file
diff --git a/First Principles/Assets/Scripts/Game/DerivativePopAnimator.cs b/First Principles/Assets/Scripts/Game/DerivativePopAnimator.cs
index 72f9bac..e42e80e 100644
--- a/First Principles/Assets/Scripts/Game/DerivativePopAnimator.cs
+++ b/First Principles/Assets/Scripts/Game/DerivativePopAnimator.cs
@@ -2,7 +2,9 @@
using UnityEngine;
///
-/// Briefly scales derivative line thickness and tint for a "pop" when the player crosses stage thresholds.
+/// Briefly scales thickness and swaps tint when the player
+/// crosses horizontal stage thresholds (see LevelManager.stageTriggerXGrid).
+/// Coroutine-based; starting a new stops the previous animation.
///
public class DerivativePopAnimator : MonoBehaviour
{
diff --git a/First Principles/Assets/Scripts/Game/GameLevelCatalog.cs b/First Principles/Assets/Scripts/Game/GameLevelCatalog.cs
index 81cb057..4f39c70 100644
--- a/First Principles/Assets/Scripts/Game/GameLevelCatalog.cs
+++ b/First Principles/Assets/Scripts/Game/GameLevelCatalog.cs
@@ -1,19 +1,77 @@
using UnityEngine;
+// -----------------------------------------------------------------------------
+// GameLevelCatalog + LevelSelection — strings & handoff from LevelSelect → Game
+// -----------------------------------------------------------------------------
+// DisplayNames[i] MUST correspond to levels[i] from LevelManager.BuildSampleLevels.
+// LevelSelection is static session state cleared after ConsumeSelectedLevel.
+// -----------------------------------------------------------------------------
+
///
-/// Shared level titles (must match the order built in LevelManager sample levels).
+/// Shared level titles (must match the order built in sample levels).
///
public static class GameLevelCatalog
{
+ /// Human-readable titles; indices drive LevelSelectController button order.
public static readonly string[] DisplayNames =
{
+ "First Principles Primer",
"Slope of Parabola",
"Waves of Sine",
"Shadows of Cosine",
- "Absolute Path"
+ "Absolute Path",
+ "Maclaurin: e^x",
+ "Maclaurin: sin(x)",
+ "Series: geometric tail",
+ "Saddle slice (multivar)",
+ "Paraboloid slice (multivar)",
+ "Area under the curve",
+ "Riemann: left endpoints",
+ "Riemann: right endpoints",
+ "Riemann: midpoint rule",
+ "Engineering: damped oscillation",
+ "Engineering: catenary (cosh)",
+ "Engineering: rectified AC (|sin|)",
+ // --- AP Calculus BC + Physics C extension (order must match LevelManager.BuildSampleLevels) ---
+ "BC: arctan & inverse trig",
+ "BC: logistic growth (dP/dt = kP(1−P/L))",
+ "Polar: cardioid r ~ 1+cos θ",
+ "Polar: rose r ~ cos(nθ)",
+ "BC: sinh x & hyperbolic functions",
+ "Physics C: exponential decay (τ, RC)",
+ "Physics C: angular momentum & L = Iω",
+ "Physics C: projectile height y(t)",
+ "BC: Maclaurin cos(x)",
+ "BC: ln x & ∫ dx/x",
+ "BC: √x & domain / cusp craft",
+ "BC: tan x between asymptotes",
+ "BC: e^{kx} & y′ = ky",
+ "BC: phase & SHM (energy swaps)",
+ "BC: cubic & inflection (sketching)",
+ "BC: b^x & d/dx b^x",
+ "Circle: (x−h)² + (y−k)² = R²",
+ // --- Aerospace engineering & aerodynamics (order must match LevelManager.BuildSampleLevels) ---
+ "Aerospace: lift C_L(α) linear + stall",
+ "Aerospace: parabolic drag polar C_D(C_L)",
+ "Aerospace: isothermal atmosphere ρ(h)",
+ "Aerospace: phugoid / damped pitch–heave mood",
+ "Aerospace: Newtonian Cp ~ sin²α",
+ "Aerospace: Strouhal / vortex shedding tone",
+ "Aerospace: re-entry decay envelope (ρV heating mood)",
+ "Competition math: ln, concavity & bound tricks",
+ "BOSS: Mandelbrot escape slice (fractal boundary mood)"
};
public static int LevelCount => DisplayNames.Length;
+
+ /// Localized title for level ; falls back to .
+ public static string GetLocalizedDisplayName(int index)
+ {
+ if (index < 0 || index >= DisplayNames.Length)
+ return "";
+ string key = $"level.{index}";
+ return LocalizationManager.Get(key, DisplayNames[index]);
+ }
}
///
diff --git a/First Principles/Assets/Scripts/Game/GraphCalculatorEquationPanel.cs b/First Principles/Assets/Scripts/Game/GraphCalculatorEquationPanel.cs
new file mode 100644
index 0000000..922bcb2
--- /dev/null
+++ b/First Principles/Assets/Scripts/Game/GraphCalculatorEquationPanel.cs
@@ -0,0 +1,203 @@
+using System;
+using TMPro;
+using UnityEngine;
+using UnityEngine.UI;
+
+///
+/// Runtime for graphic calculator mode: user types f(u) (variable x in the formula = inner u after Trans).
+///
+public static class GraphCalculatorEquationPanel
+{
+ private const string RootName = "GraphicCalculatorEquationRoot";
+ private const string LegacyRootName = "FaxasEquationInputRoot";
+
+ private static TextMeshProUGUI labelTmp;
+ private static TextMeshProUGUI placeholderTmp;
+ private static TextMeshProUGUI statusTmp;
+
+ public static void Ensure(RectTransform parent, FunctionPlotter plotter, TextMeshProUGUI typographyReference, float bottomY, float panelHeight)
+ {
+ if (parent == null || plotter == null)
+ return;
+ if (GameObject.Find(RootName) != null)
+ return;
+
+ var legacyRoot = GameObject.Find(LegacyRootName);
+ if (legacyRoot != null)
+ {
+ legacyRoot.name = RootName;
+ return;
+ }
+
+ bool tablet = DeviceLayout.IsTabletLike();
+ float w = tablet ? 980f : 900f;
+
+ var root = new GameObject(RootName);
+ var rt = root.AddComponent();
+ rt.SetParent(parent, false);
+ rt.anchorMin = new Vector2(0.5f, 0f);
+ rt.anchorMax = new Vector2(0.5f, 0f);
+ rt.pivot = new Vector2(0.5f, 0f);
+ rt.anchoredPosition = new Vector2(0f, bottomY);
+ rt.sizeDelta = new Vector2(w, panelHeight);
+
+ var bg = root.AddComponent();
+ RuntimeUiPolish.UseRoundedSliced(bg);
+ bg.color = new Color(0.1f, 0.11f, 0.15f, 0.92f);
+ bg.raycastTarget = true;
+
+ var labelGo = new GameObject("Label");
+ var lrt = labelGo.AddComponent();
+ lrt.SetParent(rt, false);
+ lrt.anchorMin = new Vector2(0f, 1f);
+ lrt.anchorMax = new Vector2(0f, 1f);
+ lrt.pivot = new Vector2(0f, 1f);
+ lrt.anchoredPosition = new Vector2(tablet ? 16f : 12f, -6f);
+ lrt.sizeDelta = new Vector2(220f, 28f);
+ var label = labelGo.AddComponent();
+ label.text = LocalizationManager.Get("graph.label_fu", "f(u) =");
+ label.fontSize = tablet ? 26 : 24;
+ label.alignment = TextAlignmentOptions.MidlineLeft;
+ label.color = new Color(0.85f, 0.88f, 0.95f, 0.9f);
+ CopyFont(label, typographyReference);
+
+ var inputShell = new GameObject("TMP_InputField");
+ var irt = inputShell.AddComponent();
+ irt.SetParent(rt, false);
+ irt.anchorMin = new Vector2(0f, 0f);
+ irt.anchorMax = new Vector2(1f, 1f);
+ irt.offsetMin = new Vector2(tablet ? 96f : 88f, 10f);
+ irt.offsetMax = new Vector2(-14f, -36f);
+
+ var inputBg = inputShell.AddComponent();
+ RuntimeUiPolish.UseRoundedSliced(inputBg);
+ inputBg.color = new Color(0.15f, 0.16f, 0.21f, 0.95f);
+
+ var textArea = new GameObject("Text Area", typeof(RectTransform), typeof(RectMask2D));
+ textArea.transform.SetParent(inputShell.transform, false);
+ var tArt = textArea.GetComponent();
+ tArt.anchorMin = Vector2.zero;
+ tArt.anchorMax = Vector2.one;
+ tArt.offsetMin = new Vector2(10f, 8f);
+ tArt.offsetMax = new Vector2(-10f, -8f);
+
+ var textGo = new GameObject("Text");
+ var textRt = textGo.AddComponent();
+ textRt.SetParent(textArea.transform, false);
+ textRt.anchorMin = Vector2.zero;
+ textRt.anchorMax = Vector2.one;
+ textRt.offsetMin = Vector2.zero;
+ textRt.offsetMax = Vector2.zero;
+ var textMesh = textGo.AddComponent();
+ textMesh.text = plotter.customExpression;
+ textMesh.fontSize = tablet ? 24 : 21;
+ textMesh.color = Color.white;
+ textMesh.alignment = TextAlignmentOptions.MidlineLeft;
+ CopyFont(textMesh, typographyReference);
+
+ var phGo = new GameObject("Placeholder");
+ var phRt = phGo.AddComponent();
+ phRt.SetParent(textArea.transform, false);
+ phRt.anchorMin = Vector2.zero;
+ phRt.anchorMax = Vector2.one;
+ phRt.offsetMin = Vector2.zero;
+ phRt.offsetMax = Vector2.zero;
+ var ph = phGo.AddComponent();
+ ph.text = LocalizationManager.Get("graph.placeholder", "x^2 + sin(x) · ln(x) for x>0 · min(x,3)...");
+ ph.fontSize = tablet ? 24 : 21;
+ ph.color = new Color(1f, 1f, 1f, 0.32f);
+ ph.fontStyle = FontStyles.Italic;
+ ph.alignment = TextAlignmentOptions.MidlineLeft;
+ CopyFont(ph, typographyReference);
+
+ var input = inputShell.AddComponent();
+ input.textComponent = textMesh;
+ input.placeholder = ph;
+ input.textViewport = tArt;
+ input.lineType = TMP_InputField.LineType.SingleLine;
+ input.characterValidation = TMP_InputField.CharacterValidation.None;
+ input.text = plotter.customExpression;
+
+ var statusGo = new GameObject("Status");
+ var srt = statusGo.AddComponent();
+ srt.SetParent(rt, false);
+ srt.anchorMin = new Vector2(0f, 0f);
+ srt.anchorMax = new Vector2(1f, 0f);
+ srt.pivot = new Vector2(0.5f, 0f);
+ srt.anchoredPosition = new Vector2(0f, 4f);
+ srt.sizeDelta = new Vector2(-24f, 22f);
+ var status = statusGo.AddComponent();
+ status.fontSize = tablet ? 16 : 14;
+ status.alignment = TextAlignmentOptions.Midline;
+ status.color = new Color(0.75f, 0.92f, 0.8f, 0.95f);
+ status.richText = true;
+ status.text = LocalizationManager.Get("graph.status_enter", "Enter / tap away to graph");
+ CopyFont(status, typographyReference);
+
+ labelTmp = label;
+ placeholderTmp = ph;
+ statusTmp = status;
+ LocalizationManager.ApplyTextDirection(labelTmp);
+ LocalizationManager.ApplyTextDirection(placeholderTmp);
+ LocalizationManager.ApplyTextDirection(statusTmp);
+ LocalizationManager.LanguageChanged -= RefreshEquationPanelStaticCopy;
+ LocalizationManager.LanguageChanged += RefreshEquationPanelStaticCopy;
+
+ void Apply(string s)
+ {
+ string t = string.IsNullOrWhiteSpace(s) ? "" : s.Trim();
+ if (string.IsNullOrEmpty(t))
+ return;
+
+ if (!MathExpressionEvaluator.TryValidateRough(t, out string err))
+ {
+ status.text = $"{TmpEscape(err)}";
+ return;
+ }
+
+ status.text = LocalizationManager.Get("graph.status_graphed", "Graphed");
+ plotter.SetCustomExpression(t);
+ }
+
+ input.onSubmit.AddListener(Apply);
+ input.onEndEdit.AddListener(Apply);
+ }
+
+ private static void RefreshEquationPanelStaticCopy()
+ {
+ if (labelTmp != null)
+ {
+ labelTmp.text = LocalizationManager.Get("graph.label_fu", "f(u) =");
+ LocalizationManager.ApplyTextDirection(labelTmp);
+ }
+ if (placeholderTmp != null)
+ {
+ placeholderTmp.text = LocalizationManager.Get("graph.placeholder", "x^2 + sin(x) · ln(x) for x>0 · min(x,3)...");
+ LocalizationManager.ApplyTextDirection(placeholderTmp);
+ }
+ if (statusTmp != null && statusTmp.text.IndexOf("#ff9a9a", StringComparison.Ordinal) < 0)
+ {
+ statusTmp.text = LocalizationManager.Get("graph.status_enter", "Enter / tap away to graph");
+ LocalizationManager.ApplyTextDirection(statusTmp);
+ }
+ }
+
+ private static string TmpEscape(string s)
+ {
+ if (string.IsNullOrEmpty(s))
+ return "";
+ return s.Replace("&", "&").Replace("<", "<").Replace(">", ">");
+ }
+
+ private static void CopyFont(TextMeshProUGUI target, TextMeshProUGUI reference)
+ {
+ if (reference != null && reference.font != null)
+ {
+ target.font = reference.font;
+ if (reference.fontSharedMaterial != null)
+ target.fontSharedMaterial = reference.fontSharedMaterial;
+ }
+ else if (TMP_Settings.defaultFontAsset != null)
+ target.font = TMP_Settings.defaultFontAsset;
+ }
+}
diff --git a/First Principles/Assets/Scripts/Game/GraphCalculatorEquationPanel.cs.meta b/First Principles/Assets/Scripts/Game/GraphCalculatorEquationPanel.cs.meta
new file mode 100644
index 0000000..6e468c6
--- /dev/null
+++ b/First Principles/Assets/Scripts/Game/GraphCalculatorEquationPanel.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 24e071c27da724d1b8b832e03cd1b9a1
\ No newline at end of file
diff --git a/First Principles/Assets/Scripts/Game/GraphCalculatorSession.cs b/First Principles/Assets/Scripts/Game/GraphCalculatorSession.cs
new file mode 100644
index 0000000..c9a7d70
--- /dev/null
+++ b/First Principles/Assets/Scripts/Game/GraphCalculatorSession.cs
@@ -0,0 +1,22 @@
+///
+/// When set from Menu or Level select, the next Game scene load opens graphic calculator mode
+/// (transforms + zoom + pinch) instead of the derivative platformer.
+///
+public static class GraphCalculatorSession
+{
+ private static bool pendingEnter;
+
+ public static void RequestEnterFromMenu()
+ {
+ pendingEnter = true;
+ }
+
+ /// True once at Game scene start; clears the request.
+ public static bool ConsumeEnterRequest()
+ {
+ if (!pendingEnter)
+ return false;
+ pendingEnter = false;
+ return true;
+ }
+}
diff --git a/First Principles/Assets/Scripts/Game/GraphCalculatorSession.cs.meta b/First Principles/Assets/Scripts/Game/GraphCalculatorSession.cs.meta
new file mode 100644
index 0000000..ebfc816
--- /dev/null
+++ b/First Principles/Assets/Scripts/Game/GraphCalculatorSession.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e3bffe35bc19d48eb89aedc7dd63bc3e
\ No newline at end of file
diff --git a/First Principles/Assets/Scripts/Game/GraphCalculatorToolbar.cs b/First Principles/Assets/Scripts/Game/GraphCalculatorToolbar.cs
new file mode 100644
index 0000000..295d70d
--- /dev/null
+++ b/First Principles/Assets/Scripts/Game/GraphCalculatorToolbar.cs
@@ -0,0 +1,195 @@
+using TMPro;
+using UnityEngine;
+using UnityEngine.EventSystems;
+using UnityEngine.UI;
+
+///
+/// Graphic calculator mode: Trans edits transformation parameters; Scale zooms the window in/out.
+/// Trans: short tap cycles A → k → C → D; double-tap nudges +; hold (~½s) nudges −.
+/// Scale: short tap zoom in; hold zoom out. Pair with for pinch.
+///
+public class GraphCalculatorToolbar : MonoBehaviour
+{
+ [SerializeField] private float longPressSeconds = 0.48f;
+ [SerializeField] private float doubleTapWindow = 0.38f;
+ [SerializeField] private float paramStep = 0.1f;
+ [SerializeField] private float kStep = 0.06f;
+ [SerializeField] private float zoomTapFactor = 0.86f;
+
+ private FunctionPlotter plot;
+ private TextMeshProUGUI hint;
+ private int paramIndex;
+ private float transLastShortTime = -1f;
+
+ private void OnEnable()
+ {
+ LocalizationManager.LanguageChanged += OnLocChanged;
+ }
+
+ private void OnDisable()
+ {
+ LocalizationManager.LanguageChanged -= OnLocChanged;
+ }
+
+ private void OnLocChanged() => RefreshHint();
+
+ public void Configure(FunctionPlotter functionPlotter, Button transBtn, Button scaleBtn, TextMeshProUGUI parameterHint)
+ {
+ plot = functionPlotter;
+ hint = parameterHint;
+
+ if (transBtn != null)
+ {
+ transBtn.onClick.RemoveAllListeners();
+ AttachPressSplit(transBtn.gameObject, OnTransShort, OnTransLong);
+ }
+
+ if (scaleBtn != null)
+ {
+ scaleBtn.onClick.RemoveAllListeners();
+ AttachPressSplit(scaleBtn.gameObject, OnScaleShort, OnScaleLong);
+ }
+
+ RefreshHint();
+ }
+
+ private void AttachPressSplit(GameObject go, System.Action shortRel, System.Action longRel)
+ {
+ var h = go.GetComponent();
+ if (h == null)
+ h = go.AddComponent();
+ h.longPressThreshold = longPressSeconds;
+ h.onShortRelease = shortRel;
+ h.onLongRelease = longRel;
+ }
+
+ private void OnTransShort()
+ {
+ if (plot == null)
+ return;
+
+ if (transLastShortTime > 0f && Time.time - transLastShortTime < doubleTapWindow)
+ {
+ NudgeParam(+1);
+ transLastShortTime = -1f;
+ return;
+ }
+
+ transLastShortTime = Time.time;
+ paramIndex = (paramIndex + 1) % 4;
+ RefreshHint();
+ }
+
+ private void OnTransLong()
+ {
+ if (plot == null)
+ return;
+ NudgeParam(-1);
+ }
+
+ private void NudgeParam(int sign)
+ {
+ float s = sign > 0 ? 1f : -1f;
+ switch (paramIndex)
+ {
+ case 0:
+ plot.transA = ClampGeneric(plot.transA + s * paramStep);
+ break;
+ case 1:
+ plot.transK = Mathf.Clamp(plot.transK + s * kStep, 0.02f, 6f);
+ break;
+ case 2:
+ plot.transC = ClampGeneric(plot.transC + s * paramStep);
+ break;
+ case 3:
+ plot.transD = ClampGeneric(plot.transD + s * paramStep);
+ break;
+ }
+
+ RefreshHint();
+ }
+
+ private static float ClampGeneric(float v)
+ {
+ return Mathf.Clamp(v, -12f, 12f);
+ }
+
+ private void OnScaleShort() => ApplyZoomWindow(zoomTapFactor);
+
+ private void OnScaleLong() => ApplyZoomWindow(1f / zoomTapFactor);
+
+ private void ApplyZoomWindow(float halfWidthMultiplier)
+ {
+ if (plot == null)
+ return;
+ float mid = (plot.xStart + plot.xEnd) * 0.5f;
+ float half = (plot.xEnd - plot.xStart) * 0.5f * halfWidthMultiplier;
+ half = Mathf.Clamp(half, 0.35f, 160f);
+ plot.xStart = mid - half;
+ plot.xEnd = mid + half;
+ plot.step = Mathf.Clamp((plot.xEnd - plot.xStart) / 480f, 0.004f, 0.42f);
+ }
+
+ private void RefreshHint()
+ {
+ if (hint == null || plot == null)
+ return;
+
+ string[] names =
+ {
+ LocalizationManager.Get("graph.param_a", "A (vertical scale)"),
+ LocalizationManager.Get("graph.param_k", "k (horizontal scale)"),
+ LocalizationManager.Get("graph.param_c", "C (vertical shift)"),
+ LocalizationManager.Get("graph.param_d", "D (horizontal shift)")
+ };
+ string line1 = LocalizationManager.Get("graph.line1", "Graphic calculator mode");
+ string line2 = LocalizationManager.Get("graph.line2",
+ "Type f(u) below (variable x in the box = inner u). Then:");
+ string line3Fmt = LocalizationManager.Get("graph.line3",
+ "Trans → {0} · double-tap + · hold − · Scale zoom in / hold zoom out · two-finger pinch");
+ string line3 = string.Format(line3Fmt, names[paramIndex]);
+ string line4Fmt = LocalizationManager.Get("graph.line4",
+ "A={0} k={1} C={2} D={3} x∈[{4},{5}]");
+ string line4 = string.Format(line4Fmt,
+ plot.transA.ToString("0.##"),
+ plot.transK.ToString("0.##"),
+ plot.transC.ToString("0.##"),
+ plot.transD.ToString("0.##"),
+ plot.xStart.ToString("0.##"),
+ plot.xEnd.ToString("0.##"));
+
+ hint.richText = true;
+ hint.alignment = TextAlignmentOptions.Center;
+ hint.text = line1 + "\n" + line2 + "\n" + line3 + "\n" + line4;
+ LocalizationManager.ApplyTextDirection(hint);
+ }
+}
+
+/// Short release vs long release on the same without using onClick.
+public class UiShortLongPress : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
+{
+ public float longPressThreshold = 0.48f;
+ public System.Action onShortRelease;
+ public System.Action onLongRelease;
+
+ private float downTime;
+ private bool down;
+
+ public void OnPointerDown(PointerEventData eventData)
+ {
+ downTime = Time.time;
+ down = true;
+ }
+
+ public void OnPointerUp(PointerEventData eventData)
+ {
+ if (!down)
+ return;
+ down = false;
+ float dt = Time.time - downTime;
+ if (dt >= longPressThreshold)
+ onLongRelease?.Invoke();
+ else
+ onShortRelease?.Invoke();
+ }
+}
\ No newline at end of file
diff --git a/First Principles/Assets/Scripts/Game/GraphCalculatorToolbar.cs.meta b/First Principles/Assets/Scripts/Game/GraphCalculatorToolbar.cs.meta
new file mode 100644
index 0000000..58169c2
--- /dev/null
+++ b/First Principles/Assets/Scripts/Game/GraphCalculatorToolbar.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f549cd855c6db4e82ad07b3a1e7b4b9d
\ No newline at end of file
diff --git a/First Principles/Assets/Scripts/Game/GraphObstacleGenerator.cs b/First Principles/Assets/Scripts/Game/GraphObstacleGenerator.cs
index bddafea..2289c0c 100644
--- a/First Principles/Assets/Scripts/Game/GraphObstacleGenerator.cs
+++ b/First Principles/Assets/Scripts/Game/GraphObstacleGenerator.cs
@@ -2,6 +2,16 @@
using UnityEngine;
using UnityEngine.UI;
+// =============================================================================
+// GraphObstacleGenerator — curve & derivative → platform / hazard columns
+// =============================================================================
+// For each integer column of the graph grid, samples f and f' (or Rieman stair rule)
+// to decide SAFE (solid platform) vs hazard gap. Visuals are child UI Images under
+// obstaclesRoot; logical rects live in GraphWorld for PlayerControllerUI2D.
+// Coordinate space: same as LineRendererUI points (grid cells, origin at grid center).
+// =============================================================================
+
+/// Axis-aligned rectangle in graph grid units (column slices, finish band, etc.).
public struct GridRect
{
public float xMin;
@@ -50,10 +60,11 @@ public void SetLayout(RectTransform obstaclesRoot, Vector2Int gridSize, float un
this.unitWidth = unitWidth;
this.unitHeight = unitHeight;
- obstacleSprite = TryGetSquareSprite();
+ obstacleSprite = RuntimeUiPolish.Rounded9Slice != null ? RuntimeUiPolish.Rounded9Slice : TryGetSquareSprite();
}
- public GraphWorld GenerateWorld(LevelDefinition def, List curvePoints, List derivPoints)
+ /// Required for Riemann stair mode; used to evaluate exact f at sample x.
+ public GraphWorld GenerateWorld(LevelDefinition def, List curvePoints, List derivPoints, FunctionPlotter functionPlotter = null)
{
if (obstaclesRoot == null)
{
@@ -80,22 +91,62 @@ public GraphWorld GenerateWorld(LevelDefinition def, List curvePoints,
float originY = gridSize.y / 2f;
float width = gridSize.x;
+ var gridOrigin = new Vector2Int(gridSize.x / 2, gridSize.y / 2);
+
+ bool useRiemannStairs = def.useRiemannStairPlatforms
+ && def.riemannRule != RiemannRule.None
+ && def.riemannRectCount > 0
+ && functionPlotter != null
+ && (def.xEnd - def.xStart) > 1e-6f;
// Finish zone at the far right.
float finishWidth = 1f;
world.finish = new GridRect(width - finishWidth, width, 0f, gridSize.y);
- int spawnCol = Mathf.Clamp(def.forcePlatformsAtStartColumns, 1, gridSize.x) - 1;
- float spawnYTop = 0f;
+ int spawnCol = 0;
+ float spawnYTop = float.PositiveInfinity;
bool spawnChosen = false;
// Generate columns.
for (int col = 0; col < gridSize.x; col++)
{
float xSample = col + 0.5f;
+ float xPlotCol = xSample - gridOrigin.x;
+ float xPlotForF = xPlotCol;
+ float xDerivSample = xSample;
- bool hasCurve = TrySampleNearestY(curvePoints, xSample, out float yCurve);
- bool hasDeriv = TrySampleNearestY(derivPoints, xSample, out float yDeriv);
+ if (useRiemannStairs)
+ {
+ int n = Mathf.Max(1, def.riemannRectCount);
+ float dx = (def.xEnd - def.xStart) / n;
+ float t = (xPlotCol - def.xStart) / dx;
+ int idx = Mathf.Clamp(Mathf.FloorToInt(t), 0, n - 1);
+ float xL = def.xStart + idx * dx;
+ float xR = def.xStart + (idx + 1) * dx;
+ xPlotForF = def.riemannRule switch
+ {
+ RiemannRule.Left => xL,
+ RiemannRule.Right => xR,
+ RiemannRule.Midpoint => 0.5f * (xL + xR),
+ _ => xPlotCol
+ };
+ xDerivSample = xPlotForF + gridOrigin.x;
+ }
+
+ bool hasCurve;
+ float yCurve;
+ if (useRiemannStairs)
+ {
+ float yPlot = functionPlotter.SampleCurvePlotterY(xPlotForF);
+ hasCurve = IsFiniteFloat(yPlot);
+ yCurve = yPlot + gridOrigin.y;
+ }
+ else
+ {
+ hasCurve = TrySampleNearestY(curvePoints, xSample, out yCurve);
+ }
+
+ bool hasDeriv = TrySampleNearestY(derivPoints, xDerivSample, out float yDeriv);
float dyValue = hasDeriv ? (yDeriv - originY) : float.NegativeInfinity;
bool safeByDerivative = hasDeriv && dyValue > def.derivativeSafeThreshold;
@@ -123,7 +174,8 @@ public GraphWorld GenerateWorld(LevelDefinition def, List curvePoints,
world.platforms.Add(platform);
CreateRectVisual($"Platform_{col}", platform, def.curveColor);
- if (!spawnChosen && forcedSafeStart)
+ // Pick the lowest platform among the starting columns so the player doesn't spawn at the top of the graph.
+ if (forcedSafeStart && safe && (!spawnChosen || platformTop < spawnYTop))
{
spawnCol = col;
spawnYTop = platformTop;
@@ -142,10 +194,14 @@ public GraphWorld GenerateWorld(LevelDefinition def, List curvePoints,
world.spawnXGrid = spawnCol + 0.5f;
}
+ if (float.IsPositiveInfinity(spawnYTop) && world.platforms.Count > 0)
+ spawnYTop = world.platforms[0].yMax;
world.spawnYTopGrid = spawnYTop;
return world;
}
+ private static bool IsFiniteFloat(float v) => !float.IsNaN(v) && !float.IsInfinity(v);
+
private void CreateRectVisual(string name, GridRect rect, Color color)
{
if (obstaclesRoot == null)
@@ -169,7 +225,14 @@ private void CreateRectVisual(string name, GridRect rect, Color color)
var img = go.AddComponent();
img.sprite = obstacleSprite;
- img.color = new Color(color.r, color.g, color.b, 0.9f);
+ bool isHazard = name.StartsWith("Hazard", System.StringComparison.OrdinalIgnoreCase);
+ var c = isHazard ? color : Color.Lerp(color, Color.white, 0.12f);
+ img.color = new Color(c.r, c.g, c.b, isHazard ? 0.88f : 0.93f);
+ if (obstacleSprite != null && obstacleSprite.border.sqrMagnitude > 0.001f &&
+ RuntimeUiPolish.ShouldUseSlicedForSize(pxW, pxH))
+ img.type = Image.Type.Sliced;
+ else
+ img.type = Image.Type.Simple;
img.raycastTarget = false;
}
diff --git a/First Principles/Assets/Scripts/Game/GraphPinchZoom.cs b/First Principles/Assets/Scripts/Game/GraphPinchZoom.cs
new file mode 100644
index 0000000..453f9ff
--- /dev/null
+++ b/First Principles/Assets/Scripts/Game/GraphPinchZoom.cs
@@ -0,0 +1,56 @@
+using UnityEngine;
+using UnityEngine.InputSystem.EnhancedTouch;
+using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch;
+
+///
+/// Two-finger pinch zoom on the math window ( / xEnd).
+/// Only used in graphic calculator mode.
+///
+public class GraphPinchZoom : MonoBehaviour
+{
+ private FunctionPlotter plot;
+ private float lastDist;
+ private bool pinching;
+
+ public void Setup(FunctionPlotter plotter)
+ {
+ plot = plotter;
+ enabled = plotter != null;
+ }
+
+ private void Update()
+ {
+ if (plot == null)
+ return;
+
+ if (Touch.activeTouches.Count == 2)
+ {
+ var t0 = Touch.activeTouches[0];
+ var t1 = Touch.activeTouches[1];
+ float d = Vector2.Distance(t0.screenPosition, t1.screenPosition);
+ if (pinching && lastDist > 2f)
+ {
+ // Fingers closer => smaller d => ratio < 1 => narrower half-width => zoom in.
+ float ratio = d / lastDist;
+ ApplyHalfWidthScale(ratio);
+ }
+ lastDist = d;
+ pinching = true;
+ }
+ else
+ {
+ pinching = false;
+ lastDist = 0f;
+ }
+ }
+
+ private void ApplyHalfWidthScale(float ratio)
+ {
+ float mid = (plot.xStart + plot.xEnd) * 0.5f;
+ float half = (plot.xEnd - plot.xStart) * 0.5f * ratio;
+ half = Mathf.Clamp(half, 0.32f, 160f);
+ plot.xStart = mid - half;
+ plot.xEnd = mid + half;
+ plot.step = Mathf.Clamp((plot.xEnd - plot.xStart) / 520f, 0.004f, 0.42f);
+ }
+}
diff --git a/First Principles/Assets/Scripts/Game/GraphPinchZoom.cs.meta b/First Principles/Assets/Scripts/Game/GraphPinchZoom.cs.meta
new file mode 100644
index 0000000..3e1ec9b
--- /dev/null
+++ b/First Principles/Assets/Scripts/Game/GraphPinchZoom.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3db8e03cd1f8743d69304a26a3ab490c
\ No newline at end of file
diff --git a/First Principles/Assets/Scripts/Game/LearningArticleLibrary.cs b/First Principles/Assets/Scripts/Game/LearningArticleLibrary.cs
new file mode 100644
index 0000000..7fe670e
--- /dev/null
+++ b/First Principles/Assets/Scripts/Game/LearningArticleLibrary.cs
@@ -0,0 +1,117 @@
+///
+/// Plain-language math snippets for the in-game reader (TextMesh Pro rich text).
+/// Mathematical parts use LaTeX delimiters \( … \) / \[ … \]; converts them at load time.
+/// Mirror ideas in docs (MathJax on GitHub Pages).
+///
+public static class LearningArticleLibrary
+{
+ /// Large verbatim TMP block; embedded newlines are meaningful for paragraph breaks.
+ public static string GetLevelSelectArticleRichText()
+ {
+ return TmpLatex.Process(@"Math concepts — in-game reader
+The game First Principles takes its name from public remarks by Elon Musk on a first-principles approach to business and to solving problems in life and work—then teaches calculus-first ideas on the graph as a matching metaphor.
+Use Math concepts while playing, or Math tips & snippets on Level select — same scrollable notes. Tap the dark backdrop or Close to dismiss.
+
+How this game teaches calculus (read the graph while you play)
+
+• Main curve — the thick path on the Cartesian grid is \(y = f(x)\) for this stage. Your character tries to stand on platforms that follow that shape. The Equation label (when visible) names the rule being plotted.
+
+• Derivative curve \(f'(x)\) — drawn as a second graph; values come from a numerical derivative (sampled slopes). On many levels, where \(f'\) is large enough you get solid ground; where it falls short you can fall through — the platformer is tied to slope logic.
+
+• Stages & “pops” — the Stage HUD counts story beats. As you move right, the derivative visualization can pop or recolor at set \(x\)-values (like curtain lifts between ideas in a lesson).
+
+• Riemann sums / area — some levels shade rectangles or build stair platforms from left-, right-, or midpoint-rule samples. That pictures \(\displaystyle \int_a^b f(x)\,dx\) as the limit of \(\sum f(x^\ast)\,\Delta x\).
+
+• Story text — the banner at the top ties each level to the math (series, polar, physics metaphors, etc.). Pause and read; it fades so the run stays readable. The First Principles Primer also nudges first-principles business thinking (reason from fundamentals vs analogy).
+
+Everything below is a topic glossary you can skim between deaths or after finishing a graph — plus a First principles thinking (business) section before exam prep.
+
+
+1. Derivatives = slope & rate
+The derivative \(f'(x)\) measures how steeply \(f(x)\) rises or falls. In physics it is often a rate: how fast position changes (velocity), how fast temperature changes along a bar, etc. In this game, the derivative helps decide where the ground exists.
+
+2. Parabola (power / quadratic)
+A quadratic \(y = a(x-h)^2+k\) is the shape of projectile motion in ideal textbook setups and many optimization problems (min/max). One smooth hump; the slope switches sign at the vertex.
+
+3. Sine & cosine = waves & rotation
+Sines and cosines describe vibrations, AC signals, sound, and anything that repeats. Cosine is sine shifted: same wave, different starting phase. Complex numbers (below) make these waves easier to solve in circuits and vibrations.
+
+4. Absolute value & kinks
+\(|x|\) bends the graph so there is a corner on the axis. The derivative jumps there in ideal math — in real programs we plot smooth samples, but the idea matters: nonsmooth points need special care in analysis and simulation.
+
+5. Taylor & Maclaurin series
+Smooth functions can be approximated near a point by polynomials with matching derivatives. Maclaurin means “expand around \(0\).” More terms usually improve the fit nearby; the full infinite sum is the series (where it converges).
+
+6. Geometric series
+Sums \(u^0+u^1+u^2+\cdots\) appear in probability, signal processing, and digital math. When \(|u|<1\) the tail shrinks and the infinite sum has a clean closed form; that is the same mood as stability and “things settle.”
+
+7. Multivariable slices
+Surfaces \(z = f(x,y)\) can be cut by fixing \(y=y_0\) — you get a 1D curve in \(x\). That is how higher-dimensional calculus is often reasoned about in engineering: fix all but one variable, take partial derivatives, gradients, directional slopes.
+
+8. Integrals & area (Riemann sums)
+The definite integral \(\displaystyle \int_a^b f(x)\,dx\) is the signed area under the curve. Riemann sums chop \([a,b]\) into thin rectangles: pick sample heights (left, right, midpoint), add \(f(x^\ast)\,\Delta x\). More rectangles → closer to the true integral.
+
+9. Engineering math — modeling mindset
+Engineering math picks tools that match the world: linear algebra for structures and networks, complex numbers / phasors for steady AC, differential equations for motion and heat, transforms (Laplace/Fourier) for signals and control. The goal is a usable model, then check it against reality.
+
+10. Damped oscillation
+Many systems lose energy while oscillating: \(e^{-\alpha t}\sin(\omega t)\) style decay is the cartoon of that idea — envelope shrinks, oscillation persists briefly. Mechanical damping, resistor–capacitor–inductor circuits, and control systems all share this language.
+
+11. Catenary & cosh
+A hanging cable under its own weight forms a catenary; \(\cosh\) is the hyperbolic cosine that models that ideal shape (and shows up in hyperbolic PDEs and relativity too). Different from a parabola even if both look “like arches.”
+
+12. Rectified sine \(|\sin|\)
+Full-wave rectification flips negative lobes upward — a first step in turning AC into something closer to DC for power supplies. Corners at zeros mean derivatives jump — a reminder that idealized circuits still start from calculus intuitions.
+
+13. Circle \((x-h)^2 + (y-k)^2 = R^2\)
+A circle is usually written implicitly. Solving for \(y\) gives two branches (\(\pm\sqrt{\cdot}\)). The game’s circle stage uses the upper semicircle so the path stays a function \(y(x)\) over one sweep. Implicit differentiation yields \(\frac{dy}{dx} = -\frac{x-h}{y-k}\); at the ends of the diameter the tangent is vertical (slope blows up).
+
+──────── Aerospace engineering & aerodynamics ────────
+Levels prefixed Aerospace: turn textbook flight‑vehicle math into paths you run. They are toy models for pedagogy — not CFD, flight test, or ITAR‑grade simulations.
+
+Lift, drag, atmosphere
+\(C_L(\alpha)\) grows ~linearly before stall (flow separation), then drops — slopes & breakpoints drive your platforms. Drag polar \(C_D = C_{D0} + K C_L^2\) is a parabola in \(C_L\): min‑drag sweet spots matter for glide & endurance. Isothermal atmosphere uses \(\rho(h) \propto e^{-h/H}\) (scale height \(H\)): density drives \(q = \tfrac{1}{2}\rho V^2\), Reynolds, thrust lapse.
+
+Stability, unsteady aero, entry
+Phugoid / short‑period moods use damped sinusoids from linearized dynamics (eigenvalues in state space). Strouhal shedding ties frequency to \(U/D\) with sine‑like traces. Newtonian \(\sin^2\alpha\) sketches hypersonic windward pressure teaching curves. Re‑entry decay uses an exponential envelope mood for “how fast heating threat relaxes” in simplified stories.
+
+See docs/engineering-math.md § Aerospace for a longer map.
+
+──────── First principles thinking (business) ────────
+Startup culture often cites Elon Musk for reviving first principles at places like Tesla / SpaceX: stop trusting “the market always prices it this way,” and instead unpack assumptions until you hit bedrock facts (materials, energy, physics, true unit costs), then reason upward into a new design. The idea is older than any one founder — but the habit matches this game’s visuals.
+
+Map the metaphor
+• Your mental “business curve” is like f(x) — the story you tell about how output moves with a lever (price, latency, quality, throughput).
+• Where it breaks or soars is like f'(x) — sensitivity. Same as here: derivative magnitude decides whether the “floor” (your plan) actually holds.
+• Stages & pops — slice the bet into acts; each reveal is a new hypothesis. Riemann / area levels — many small choices sum; refine the grid before you scale spend.
+• Multivariable slices — fix hidden variables on purpose; one clear 1D walk beats a fuzzy 4D slide deck.
+
+This is not legal, tax, or investing advice — a thinking drill you can pair with real advisors and data. Full write-up: docs/first-principles-business.md on GitHub Pages.
+
+──────── Exam prep (separate tracks) ────────
+
+Competition mathematics (contest lens)
+Problems from contests like AMC / AIME often reward bounding, symmetry, and knowing when a function is concave or convex. Natural log is a classic hub: \(\ln\) is concave on its domain — tangents/secants give linear estimates used in inequalities. In-game stage: Competition math: ln, concavity & bound tricks (before the Mandelbrot boss). Not affiliated with MAA or any contest body. → docs/competition-math.md
+
+AMC 10 & AMC 12 (MAA, US)
+Both are middle/early high-school multiple-choice sprints; calculus is not required. Load-bearing ideas: smart algebra, functions (including logs), coordinate geometry, counting & probability, modular arithmetic. Graph fluency from this game helps with shape intuition, domains (e.g. \(\ln\), \(\sqrt{\cdot}\)), and reading options even when you solve by algebra. Not affiliated with MAA — use official AMC materials for real problems. → docs/amc-10-12.md
+
+TMUA — Test of Mathematics for University Admission (UK)
+Two-paper multiple choice. Calculus shows up as fast chain/product/quotient fluency, sketch reasoning from \(f'\) and \(f''\), \(\int\) as signed area / FTC mood, domains of \(\ln\) & \(\sqrt{\cdot}\), exp/log inequalities, limits & asymptotes. Typical traps: \(|x|\) kinks, endpoint maxima, “exactly one option is true” elimination. No reproduced questions—use official TMUA materials. → docs/tmua-calculus.md
+
+MAT — Mathematics Admissions Test (Oxford / UK)
+Emphasis on multi-step reasoning and exact algebra—not the same pacing as TMUA. Calculus supports graph sense, implicit curves, inequalities, and “how many solutions?” via monotonicity. Format evolves; check Oxford’s official MAT page. No reproduced questions—use official MAT papers. → docs/mat-calculus.md
+
+AP Calculus BC (College Board, US)
+BC stacks series & Taylor, parametric / polar / vector-valued motion, DEs (logistic, separable), and richer integration (incl. improper) beside AB. Polar recap: \(\displaystyle A = \tfrac{1}{2}\int r^2\,d\theta\); arc length \(\displaystyle \int \sqrt{r^2+(dr/d\theta)^2}\,d\theta\). In-game: stages BC:, Polar:, Circle, plus Maclaurin & Riemann levels. Pair with CB Course Description & FRQs. → docs/ap-calculus-bc.md
+
+AP Physics C (College Board, US)
+Calculus-first mechanics & E&M: \(v = dr/dt\), \(a = dv/dt\), work integrals, \(\tau = dL/dt\), \(L = I\omega\) on a fixed axis, angular momentum conservation when \(\tau_{\mathrm{ext}} = 0\); E&M uses flux / line-integral setups. Game motifs: exponential decay \((\tau)\), projectile parabola, rotation / SHM—visual hooks only. → docs/ap-physics-c.md
+
+Where to read more
+docs/math-concepts.md (index), docs/first-principles-business.md (Musk-popularized builder lens ↔ game), docs/competition-math.md & docs/amc-10-12.md (contest / MAA map), docs/engineering-math.md (applied circuits/oscillations), and the exam-prep files above on GitHub Pages.
+
+— © 2022-2026 · GAME GENESIS × ORCH AEROSPACE · First Principles
+");
+ }
+}
diff --git a/First Principles/Assets/Scripts/Game/LearningArticleLibrary.cs.meta b/First Principles/Assets/Scripts/Game/LearningArticleLibrary.cs.meta
new file mode 100644
index 0000000..a1aa667
--- /dev/null
+++ b/First Principles/Assets/Scripts/Game/LearningArticleLibrary.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a3c91f7e2d4b5e6f708192a3b4c5d6e7
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/First Principles/Assets/Scripts/Game/LevelDefinition.cs b/First Principles/Assets/Scripts/Game/LevelDefinition.cs
index 026048c..0ff872e 100644
--- a/First Principles/Assets/Scripts/Game/LevelDefinition.cs
+++ b/First Principles/Assets/Scripts/Game/LevelDefinition.cs
@@ -1,17 +1,27 @@
using System.Collections.Generic;
using UnityEngine;
+// -----------------------------------------------------------------------------
+// LevelDefinition — Data container for one playable graph stage
+// -----------------------------------------------------------------------------
+// Used in two ways: (1) CreateAssetMenu .asset files in the editor, (2) runtime
+// ScriptableObject.CreateInstance in LevelManager.MakeLevel. Keep field semantics in
+// sync with LevelManager.ApplyLevelTheme and GraphObstacleGenerator.GenerateWorld.
+// -----------------------------------------------------------------------------
+
///
/// Defines a single stage: which curve to plot, what colors to use, and how the derivative
-/// should influence gameplay (safe vs hazard/gaps), plus the story text.
+/// should influence gameplay (safe vs hazard/gaps), plus story text and optional Riemann UX.
///
[CreateAssetMenu(menuName = "FirstPrinciples/Level Definition", fileName = "LevelDefinition")]
public class LevelDefinition : ScriptableObject
{
[Header("Identity")]
public string levelName = "Stage";
- [TextArea(2, 6)]
+ [TextArea(4, 12)]
public string storyText = "Follow the curve. Watch the derivative.";
+ [Tooltip("Extra seconds the story stays readable after fading in (0 = use LevelManager default).")]
+ public float storyPauseSeconds = 0f;
[Header("Graph Parameters (FunctionPlotter)")]
public FunctionType functionType = FunctionType.Power;
@@ -54,6 +64,32 @@ public class LevelDefinition : ScriptableObject
[Tooltip("X trigger positions (grid units, relative to left edge of the graph) where we pop the derivative.")]
public List stageTriggerX = new List();
+ [Header("Level flow (optional)")]
+ [Tooltip("How many derivative-pop boundaries to use (0 = LevelManager default).")]
+ public int derivativePopTriggerCount = 0;
+
+ [Tooltip("Tint the background grid to match this level’s mood.")]
+ public bool applyGridTheming = false;
+
+ public Color gridCenterLineTheming = new Color(1f, 1f, 1f, 0.39f);
+ public Color gridOutsideLineTheming = new Color(1f, 1f, 1f, 0.14f);
+
+ [Header("Riemann sums & area under the curve")]
+ [Tooltip("Left / right / midpoint sample for rectangles and optional stair platforms.")]
+ public RiemannRule riemannRule = RiemannRule.None;
+
+ [Tooltip("Number of subintervals n (rectangles). Larger n → closer to ∫ f dx.")]
+ [Min(1)]
+ public int riemannRectCount = 16;
+
+ [Tooltip("Fill rectangles from the x-axis to f(x*) in the graph plane.")]
+ public bool showRiemannVisualization = false;
+
+ [Tooltip("Platforms are flat per subinterval at the Riemann sample height (step terrain under the curve).")]
+ public bool useRiemannStairPlatforms = false;
+
+ public Color riemannFillColor = new Color(0.25f, 0.55f, 0.95f, 0.32f);
+
public void EnsureDefaultStagePopData(int stageCount)
{
if (stageCount < 1)
diff --git a/First Principles/Assets/Scripts/Game/LevelManager.cs b/First Principles/Assets/Scripts/Game/LevelManager.cs
index 2799a36..ad26bc3 100644
--- a/First Principles/Assets/Scripts/Game/LevelManager.cs
+++ b/First Principles/Assets/Scripts/Game/LevelManager.cs
@@ -6,8 +6,29 @@
using UnityEngine.SceneManagement;
using UnityEngine.UI;
+// =============================================================================
+// LevelManager — Game scene: curriculum levels, graph theme, platformer world
+// =============================================================================
+// Flow (single-scene Game):
+// 1. Start() → SetupReferences() finds FunctionPlotter / renderers / plane, adds
+// GraphObstacleGenerator & DerivativePopAnimator if missing, creates runtime UI
+// (ObstaclesRoot, player, story TMP, HUD, Math concepts overlay button), Riemann helper, touch.
+// 2. BuildSampleLevels() populates `levels`. CRITICAL: index order must match
+// GameLevelCatalog.DisplayNames (level select uses the same indices).
+// 3. LoadLevel(i) → ApplyLevelTheme(def) pushes params into FunctionPlotter and HUD
+// state, then LoadWorldAfterThemeChange waits a frame so LineRenderer points exist,
+// then regenerates obstacles + Riemann mesh and resets the player.
+// 4. Update() advances nextStageIndex when PlayerCenterGrid.x crosses
+// stageTriggerXGrid[k], firing DerivativePopAnimator.
+// Dependencies: Cartesian plane RectTransform, Canvas with CanvasSafeAreaBootstrap for mobile.
+// =============================================================================
+
+///
+/// Central coordinator for the playable “graph as level” mode on the Game scene.
+///
public class LevelManager : MonoBehaviour
{
+ // --- Serialized tuning ---
[Header("Stage Pops")]
[SerializeField] private int defaultStageCount = 3;
@@ -24,14 +45,26 @@ public class LevelManager : MonoBehaviour
private GraphObstacleGenerator obstacleGenerator;
private PlayerControllerUI2D playerController;
private DerivativePopAnimator popAnimator;
+ private RiemannStripRendererUI riemannRenderer;
private RectTransform obstaclesRoot;
private TextMeshProUGUI storyText;
+ private TextMeshProUGUI stageHudText;
+ private TextMeshProUGUI controlsHintText;
+ private int lastStageHudKey = int.MinValue;
+ private Sprite cachedHudPanelSprite;
+ private float storyMiddlePauseSeconds = 1.65f;
+ private bool gridThemeBaselineCaptured;
+ private Color savedGridCenterLine;
+ private Color savedGridOutsideLine;
+
+ /// Runtime-built list of stages (also representable as LevelDefinition assets).
private readonly List levels = new List();
private int currentLevelIndex;
+ /// How many stage boundary thresholds the player has already crossed (derivative pops).
private int nextStageIndex;
private List stageTriggerXGrid;
private List stagePopColors;
@@ -39,6 +72,9 @@ public class LevelManager : MonoBehaviour
private bool isRestarting;
private Coroutine storyFadeRoutine;
+ /// Graphic calculator mode: transforms, scale zoom, pinch — no platformer.
+ private bool graphCalculatorMode;
+
private void Awake()
{
// Keep things single-scene: if another instance somehow appears, destroy it.
@@ -49,14 +85,123 @@ private void Awake()
}
}
+ private void OnEnable()
+ {
+ LocalizationManager.LanguageChanged += OnLocalizationChanged;
+ }
+
+ private void OnDisable()
+ {
+ LocalizationManager.LanguageChanged -= OnLocalizationChanged;
+ }
+
+ private void OnLocalizationChanged()
+ {
+ RefreshControlsHintLocalized();
+ RefreshMathConceptsLabelLocalized();
+ RefreshSceneFooterLocalized();
+ RefreshStageHudLocalizedForce();
+
+ if (graphCalculatorMode)
+ RefreshStoryBannerForCurrentMode(null);
+ else if (levels.Count > 0 && currentLevelIndex >= 0 && currentLevelIndex < levels.Count)
+ RefreshStoryBannerForCurrentMode(levels[currentLevelIndex]);
+ }
+
+ private void RefreshControlsHintLocalized()
+ {
+ if (controlsHintText == null)
+ return;
+
+ if (graphCalculatorMode)
+ {
+ controlsHintText.text = LocalizationManager.Get("controls.calculator",
+ "Graphic calculator Type f(u) · Trans · Scale · Pinch · Back");
+ }
+ else if (DeviceLayout.PreferOnScreenGameControls)
+ {
+ controlsHintText.text = LocalizationManager.Get("controls.mobile",
+ "Move \u25C0 \u25B6 \u00b7 Jump tap (keyboard: arrows / Space)");
+ }
+ else
+ {
+ controlsHintText.text = LocalizationManager.Get("controls.desktop",
+ "Move \u2190 \u2192 \u00b7 Jump Space");
+ }
+
+ LocalizationManager.ApplyTextDirection(controlsHintText);
+ }
+
+ private void RefreshMathConceptsLabelLocalized()
+ {
+ var go = GameObject.Find("MathConceptsButton");
+ if (go == null)
+ return;
+ var tmp = go.GetComponentInChildren();
+ if (tmp == null)
+ return;
+ tmp.text = LocalizationManager.Get("ui.math_concepts", "Math concepts");
+ LocalizationManager.ApplyTextDirection(tmp);
+ }
+
+ private void RefreshSceneFooterLocalized()
+ {
+ var go = GameObject.Find("SceneCreditsFooter");
+ if (go == null)
+ return;
+ var tmp = go.GetComponent();
+ if (tmp == null)
+ return;
+ tmp.text = SceneCreditsFooter.BuildCompactRichText();
+ LocalizationManager.ApplyTextDirection(tmp);
+ }
+
+ private void RefreshStageHudLocalizedForce()
+ {
+ lastStageHudKey = int.MinValue;
+ RefreshStageHud();
+ }
+
+ private void RefreshStoryBannerForCurrentMode(LevelDefinition def)
+ {
+ if (storyText == null)
+ return;
+
+ if (graphCalculatorMode || def == null)
+ {
+ storyText.text = TmpLatex.Process(LocalizationManager.Get("graph.calculator_intro",
+ "Graphic calculator mode\n" +
+ "Type almost any f(u) in the field (variable x in your formula); Trans adjusts A, k, C, D; Scale & pinch zoom the window."));
+ storyText.isRightToLeftText = false;
+ return;
+ }
+
+ string title = LocalizationManager.GetWithFallback($"level.{currentLevelIndex}", def.levelName);
+ string story = LocalizationManager.GetWithFallback($"story.{currentLevelIndex}", def.storyText);
+ storyText.text = TmpLatex.Process($"{title}\n{story}");
+ // Long level copy is often mixed Latin/math; keep LTR unless you add full `story.N` translations in RTL locales.
+ storyText.isRightToLeftText = false;
+ }
+
private void Start()
{
+ graphCalculatorMode = GraphCalculatorSession.ConsumeEnterRequest();
SetupReferences();
+ if (graphCalculatorMode)
+ {
+ EnterGraphCalculatorMode();
+ return;
+ }
+
BuildSampleLevels();
+ // LevelSelect sets LevelSelection; opening Game directly falls back to index 0.
int startIndex = LevelSelection.ConsumeSelectedLevel(levels.Count);
LoadLevel(startIndex);
}
+ ///
+ /// One-time wiring: graph components, obstacle generator, player, HUD, Riemann UI hook, touch bar.
+ ///
private void SetupReferences()
{
functionPlotter = FindAnyObjectByType();
@@ -87,12 +232,196 @@ private void SetupReferences()
CreateObstaclesRootIfNeeded();
CreatePlayerIfNeeded();
CreateStoryTextIfNeeded();
+ CreateGameplayHudIfNeeded();
+ if (!graphCalculatorMode)
+ HideLegacyGraphTuningButtons();
+ EnsureRiemannRenderer();
+ var mainCanvas = FindAnyObjectByType