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(); + if (mainCanvas != null && !graphCalculatorMode) + MobileTouchControls.EnsureForGameCanvas(mainCanvas.transform); // Wire callbacks. playerController.SetDeathCallback(RestartCurrentLevel); playerController.SetFinishCallback(AdvanceLevel); } + /// + /// Free graphing workspace (graphic calculator mode). + /// Legacy TransButton / ScaleButton are shown; pinch zoom applies on the graph window. + /// + private void EnterGraphCalculatorMode() + { + levels.Clear(); + currentLevelIndex = 0; + stageTriggerXGrid = new List(); + stagePopColors = new List(); + nextStageIndex = 0; + + if (obstaclesRoot != null) + obstaclesRoot.gameObject.SetActive(false); + + if (playerController != null) + playerController.gameObject.SetActive(false); + + if (gridRenderer != null && !gridThemeBaselineCaptured) + { + savedGridCenterLine = gridRenderer.centerLine; + savedGridOutsideLine = gridRenderer.outsideLine; + gridThemeBaselineCaptured = true; + } + + if (gridRenderer != null) + { + gridRenderer.centerLine = savedGridCenterLine; + gridRenderer.outsideLine = savedGridOutsideLine; + gridRenderer.enabled = false; + gridRenderer.enabled = true; + } + + if (storyFadeRoutine != null) + { + StopCoroutine(storyFadeRoutine); + storyFadeRoutine = null; + } + + if (storyText != null) + { + storyText.gameObject.SetActive(true); + RefreshStoryBannerForCurrentMode(null); + storyText.color = new Color(1f, 1f, 1f, 0.94f); + } + + if (stageHudText != null && stageHudText.transform.parent != null) + stageHudText.transform.parent.gameObject.SetActive(false); + + functionPlotter.transA = 1f; + functionPlotter.transK = 1f; + functionPlotter.transC = 0f; + functionPlotter.transD = 0f; + functionPlotter.power = 2; + functionPlotter.baseN = 2; + functionPlotter.differentiate = false; + functionPlotter.xStart = -12f; + functionPlotter.xEnd = 12f; + functionPlotter.step = 0.06f; + functionPlotter.SetEquationExtraSuffix(""); + functionPlotter.SetCustomExpression("x^2"); + + if (curveRenderer != null) + { + curveRenderer.color = new Color(0.95f, 0.8f, 0.38f, 1f); + curveRenderer.enabled = false; + curveRenderer.enabled = true; + } + + if (derivRenderer != null) + { + derivRenderer.color = new Color(0.55f, 0.78f, 1f, 1f); + derivRenderer.enabled = false; + } + + if (riemannRenderer != null) + riemannRenderer.ClearStrips(); + + var canvas = FindAnyObjectByType(); + var safe = canvas != null ? MobileUiRoots.GetSafeContentParent(canvas.transform) as RectTransform : null; + var hintParent = safe != null ? safe : canvas?.transform as RectTransform; + float bridgeControls = DeviceLayout.PreferOnScreenGameControls ? DeviceLayout.TouchHintVerticalOffset : 22f; + float transRowBottom = bridgeControls + 74f; + + GraphCalculatorEquationPanel.Ensure(hintParent, functionPlotter, FindPrimaryEquationTmp(), transRowBottom + 110f, 108f); + + var transGo = GameObject.Find("TransButton"); + var scaleGo = GameObject.Find("ScaleButton"); + LayoutCalculatorToolButtons(transGo, scaleGo, transRowBottom); + + TextMeshProUGUI paramHint = null; + var legacyHint = GameObject.Find("FaxasGraphParamHint"); + if (legacyHint != null) + legacyHint.name = "GraphicCalculatorParamHint"; + + if (hintParent != null && GameObject.Find("GraphicCalculatorParamHint") == null) + { + var hintGo = new GameObject("GraphicCalculatorParamHint"); + var hrt = hintGo.AddComponent(); + hrt.SetParent(hintParent, false); + hrt.anchorMin = new Vector2(0.5f, 0f); + hrt.anchorMax = new Vector2(0.5f, 0f); + hrt.pivot = new Vector2(0.5f, 0f); + bool tablet = DeviceLayout.IsTabletLike(); + float up = DeviceLayout.PreferOnScreenGameControls ? DeviceLayout.TouchHintVerticalOffset + 312f : 318f; + hrt.anchoredPosition = new Vector2(0f, up); + hrt.sizeDelta = new Vector2(tablet ? 1000f : 920f, tablet ? 128f : 118f); + + paramHint = hintGo.AddComponent(); + paramHint.richText = true; + paramHint.textWrappingMode = TextWrappingModes.Normal; + paramHint.fontSize = tablet ? 26 : 23; + paramHint.alignment = TextAlignmentOptions.Top; + paramHint.color = new Color(0.9f, 0.93f, 0.98f, 0.96f); + ApplyPrimaryUiTypography(paramHint, FindPrimaryEquationTmp(), outlineWidth: 0.12f, outlineAlpha: 0.45f); + } + else if (GameObject.Find("GraphicCalculatorParamHint") != null) + paramHint = GameObject.Find("GraphicCalculatorParamHint").GetComponent(); + + foreach (var oldT in GetComponents()) + Destroy(oldT); + foreach (var oldP in GetComponents()) + Destroy(oldP); + + var toolbar = gameObject.AddComponent(); + toolbar.Configure(functionPlotter, + transGo != null ? transGo.GetComponent