From 3315faeb9c65be0968e12f5f46e1e20343ba0b77 Mon Sep 17 00:00:00 2001 From: John Seong Date: Thu, 19 Mar 2026 14:10:35 -0400 Subject: [PATCH 01/27] Added docs and beautified menu --- First Principles/Assets/Scenes/Game.unity | 4 +- .../Scripts/Game/GraphObstacleGenerator.cs | 9 +- .../Assets/Scripts/Game/LevelManager.cs | 222 +++++++++++++++++- .../Scripts/Game/PlayerControllerUI2D.cs | 19 +- README.md | 80 +++++-- docs/Gemfile | 5 + docs/README.md | 32 +++ docs/_config.yml | 32 +++ docs/architecture.md | 75 ++++++ docs/gameplay.md | 51 ++++ docs/index.md | 45 ++++ docs/setup.md | 81 +++++++ docs/troubleshooting.md | 52 ++++ 13 files changed, 670 insertions(+), 37 deletions(-) create mode 100644 docs/Gemfile create mode 100644 docs/README.md create mode 100644 docs/_config.yml create mode 100644 docs/architecture.md create mode 100644 docs/gameplay.md create mode 100644 docs/index.md create mode 100644 docs/setup.md create mode 100644 docs/troubleshooting.md diff --git a/First Principles/Assets/Scenes/Game.unity b/First Principles/Assets/Scenes/Game.unity index bc58abf..e65501d 100644 --- a/First Principles/Assets/Scenes/Game.unity +++ b/First Principles/Assets/Scenes/Game.unity @@ -1143,7 +1143,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 @@ -2482,7 +2482,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/Scripts/Game/GraphObstacleGenerator.cs b/First Principles/Assets/Scripts/Game/GraphObstacleGenerator.cs index bddafea..8354fb7 100644 --- a/First Principles/Assets/Scripts/Game/GraphObstacleGenerator.cs +++ b/First Principles/Assets/Scripts/Game/GraphObstacleGenerator.cs @@ -85,8 +85,8 @@ public GraphWorld GenerateWorld(LevelDefinition def, List curvePoints, 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. @@ -123,7 +123,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,6 +143,8 @@ 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; } diff --git a/First Principles/Assets/Scripts/Game/LevelManager.cs b/First Principles/Assets/Scripts/Game/LevelManager.cs index 2799a36..2ca2acc 100644 --- a/First Principles/Assets/Scripts/Game/LevelManager.cs +++ b/First Principles/Assets/Scripts/Game/LevelManager.cs @@ -28,6 +28,10 @@ public class LevelManager : MonoBehaviour private RectTransform obstaclesRoot; private TextMeshProUGUI storyText; + private TextMeshProUGUI stageHudText; + private TextMeshProUGUI controlsHintText; + private int lastStageHudKey = int.MinValue; + private Sprite cachedHudPanelSprite; private readonly List levels = new List(); private int currentLevelIndex; @@ -87,6 +91,8 @@ private void SetupReferences() CreateObstaclesRootIfNeeded(); CreatePlayerIfNeeded(); CreateStoryTextIfNeeded(); + CreateGameplayHudIfNeeded(); + HideLegacyGraphTuningButtons(); // Wire callbacks. playerController.SetDeathCallback(RestartCurrentLevel); @@ -147,9 +153,6 @@ private void CreateStoryTextIfNeeded() return; } - // Copy font from an existing scene TMP before we add StoryText (so the new label is not the one we sample). - var fontSource = FindAnyObjectByType(); - var storyGo = new GameObject("StoryText"); storyGo.transform.SetParent(canvas.transform, false); @@ -158,16 +161,7 @@ private void CreateStoryTextIfNeeded() tmp.fontSize = 32; tmp.alignment = TextAlignmentOptions.Center; tmp.richText = true; - - if (fontSource != null) - { - tmp.font = fontSource.font; - if (fontSource.fontSharedMaterial != null) - tmp.fontSharedMaterial = fontSource.fontSharedMaterial; - } - - if (tmp.font == null && TMP_Settings.defaultFontAsset != null) - tmp.font = TMP_Settings.defaultFontAsset; + ApplyPrimaryUiTypography(tmp, FindPrimaryEquationTmp(), outlineWidth: 0.06f, outlineAlpha: 0.35f); var rt = tmp.rectTransform; rt.anchorMin = new Vector2(0.5f, 1f); @@ -181,6 +175,204 @@ private void CreateStoryTextIfNeeded() storyText = tmp; } + /// Hides old graph "Trans" / "Scale" tuning buttons so levels control parameters; gameplay uses arrows + jump. + private static void HideLegacyGraphTuningButtons() + { + foreach (var name in new[] { "TransButton", "ScaleButton" }) + { + var go = GameObject.Find(name); + if (go != null) + go.SetActive(false); + } + } + + private void CreateGameplayHudIfNeeded() + { + var canvas = FindAnyObjectByType(); + if (canvas == null) + return; + + var equationStyle = FindPrimaryEquationTmp(); + var panelSprite = GetHudPanelSprite(); + + if (stageHudText == null) + { + var panelGo = new GameObject("StageHudPanel"); + var panelRt = panelGo.AddComponent(); + panelRt.SetParent(canvas.transform, false); + panelRt.anchorMin = new Vector2(0f, 1f); + panelRt.anchorMax = new Vector2(0f, 1f); + panelRt.pivot = new Vector2(0f, 1f); + panelRt.anchoredPosition = new Vector2(22f, -20f); + panelRt.sizeDelta = new Vector2(340f, 76f); + + var panelBg = panelGo.AddComponent(); + panelBg.sprite = panelSprite; + panelBg.color = new Color(0.06f, 0.07f, 0.11f, 0.88f); + panelBg.raycastTarget = false; + panelBg.type = Image.Type.Sliced; + // If sprite isn't 9-slice, Simple still works for a soft tile look. + if (panelSprite != null && panelSprite.border.sqrMagnitude < 0.001f) + panelBg.type = Image.Type.Simple; + + var accentGo = new GameObject("StageHudAccent"); + var accentRt = accentGo.AddComponent(); + accentRt.SetParent(panelGo.transform, false); + accentRt.anchorMin = new Vector2(0f, 1f); + accentRt.anchorMax = new Vector2(1f, 1f); + accentRt.pivot = new Vector2(0.5f, 1f); + accentRt.anchoredPosition = Vector2.zero; + accentRt.sizeDelta = new Vector2(0f, 3f); + var accentImg = accentGo.AddComponent(); + accentImg.sprite = panelSprite; + accentImg.color = new Color(0.95f, 0.72f, 0.25f, 0.95f); + accentImg.raycastTarget = false; + + var textGo = new GameObject("StageHud"); + var textRt = textGo.AddComponent(); + textRt.SetParent(panelGo.transform, false); + textRt.anchorMin = Vector2.zero; + textRt.anchorMax = Vector2.one; + textRt.offsetMin = new Vector2(18f, 12f); + textRt.offsetMax = new Vector2(-16f, -14f); + + var tmp = textGo.AddComponent(); + tmp.richText = true; + tmp.enableWordWrapping = false; + tmp.fontSize = 30; + tmp.alignment = TextAlignmentOptions.MidlineLeft; + tmp.color = new Color(0.94f, 0.95f, 0.98f, 1f); + tmp.characterSpacing = 0.35f; + tmp.lineSpacing = -4f; + ApplyPrimaryUiTypography(tmp, equationStyle, outlineWidth: 0.16f, outlineAlpha: 0.55f); + tmp.text = FormatStageHudLine(1, 1); + + stageHudText = tmp; + } + + if (controlsHintText == null) + { + var barGo = new GameObject("ControlsHintPanel"); + var barRt = barGo.AddComponent(); + barRt.SetParent(canvas.transform, false); + barRt.anchorMin = new Vector2(0.5f, 0f); + barRt.anchorMax = new Vector2(0.5f, 0f); + barRt.pivot = new Vector2(0.5f, 0f); + barRt.anchoredPosition = new Vector2(0f, 18f); + barRt.sizeDelta = new Vector2(620f, 54f); + + var barBg = barGo.AddComponent(); + barBg.sprite = panelSprite; + barBg.color = new Color(0.06f, 0.07f, 0.11f, 0.82f); + barBg.raycastTarget = false; + barBg.type = panelSprite != null && panelSprite.border.sqrMagnitude > 0.001f ? Image.Type.Sliced : Image.Type.Simple; + + var textGo = new GameObject("ControlsHint"); + var textRt = textGo.AddComponent(); + textRt.SetParent(barGo.transform, false); + textRt.anchorMin = Vector2.zero; + textRt.anchorMax = Vector2.one; + textRt.offsetMin = new Vector2(20f, 8f); + textRt.offsetMax = new Vector2(-20f, -8f); + + var tmp = textGo.AddComponent(); + tmp.richText = true; + tmp.enableWordWrapping = false; + tmp.fontSize = 22; + tmp.alignment = TextAlignmentOptions.Midline; + tmp.color = new Color(0.82f, 0.85f, 0.92f, 0.92f); + tmp.characterSpacing = 0.25f; + ApplyPrimaryUiTypography(tmp, equationStyle, outlineWidth: 0.14f, outlineAlpha: 0.5f); + tmp.text = + "Move " + + "\u2190 \u2192 " + + "· " + + "Jump " + + "Space"; + + controlsHintText = tmp; + } + } + + /// The big equation label in Game — used as the typography reference for all gameplay HUD copy. + private static TextMeshProUGUI FindPrimaryEquationTmp() + { + var go = GameObject.Find("Equation"); + if (go != null) + { + var t = go.GetComponent(); + if (t != null && t.font != null) + return t; + } + + foreach (var t in FindObjectsByType()) + { + if (t != null && t.font != null && t.gameObject.CompareTag("EquationText")) + return t; + } + + return null; + } + + private static void ApplyPrimaryUiTypography(TextMeshProUGUI target, TextMeshProUGUI reference, float outlineWidth = 0.14f, float outlineAlpha = 0.5f) + { + if (reference != null) + { + target.font = reference.font; + if (reference.fontSharedMaterial != null) + target.fontSharedMaterial = reference.fontSharedMaterial; + target.fontStyle = reference.fontStyle; + } + else if (TMP_Settings.defaultFontAsset != null) + target.font = TMP_Settings.defaultFontAsset; + + target.outlineWidth = outlineWidth; + target.outlineColor = new Color(0f, 0f, 0f, outlineAlpha); + } + + private static string FormatStageHudLine(int stage, int total) + { + return + "STAGE\n" + + $"{stage} / {total}"; + } + + private void RefreshStageHud() + { + if (stageHudText == null || stageTriggerXGrid == null) + return; + + int total = Mathf.Max(1, stageTriggerXGrid.Count + 1); + int stage = Mathf.Clamp(nextStageIndex + 1, 1, total); + int key = stage | (total << 16); + if (key == lastStageHudKey) + return; + lastStageHudKey = key; + stageHudText.text = FormatStageHudLine(stage, total); + } + + /// Square / UI sprite for flat panels (falls back to a tiny white sprite so Image always draws). + private Sprite GetHudPanelSprite() + { + if (cachedHudPanelSprite != null) + return cachedHudPanelSprite; + + var fromScene = TryGetSquareSprite(); + if (fromScene != null) + { + cachedHudPanelSprite = fromScene; + return cachedHudPanelSprite; + } + + var tex = Texture2D.whiteTexture; + cachedHudPanelSprite = Sprite.Create( + tex, + new Rect(0f, 0f, tex.width, tex.height), + new Vector2(0.5f, 0.5f), + 100f); + return cachedHudPanelSprite; + } + private Sprite TryGetSquareSprite() { // Prefer the existing scene sprite. @@ -323,10 +515,12 @@ private void LoadLevel(int index) currentLevelIndex = Mathf.Clamp(index, 0, levels.Count - 1); nextStageIndex = 0; + lastStageHudKey = int.MinValue; isRestarting = false; var def = levels[currentLevelIndex]; ApplyLevelTheme(def); + RefreshStageHud(); StartCoroutine(LoadWorldAfterThemeChange(def)); } @@ -433,6 +627,8 @@ private void Update() if (playerController == null || stageTriggerXGrid == null) return; + RefreshStageHud(); + // Trigger derivative "pops" at stage boundaries. while (nextStageIndex < stageTriggerXGrid.Count) { diff --git a/First Principles/Assets/Scripts/Game/PlayerControllerUI2D.cs b/First Principles/Assets/Scripts/Game/PlayerControllerUI2D.cs index 2883e93..5a525ad 100644 --- a/First Principles/Assets/Scripts/Game/PlayerControllerUI2D.cs +++ b/First Principles/Assets/Scripts/Game/PlayerControllerUI2D.cs @@ -100,11 +100,24 @@ private void Update() float dt = Time.deltaTime; - // Input -> velocity. - float inputX = Input.GetAxisRaw("Horizontal"); + // Movement: arrow keys / WASD (preferred for keyboard), then legacy Input axes (gamepad, etc.). + float inputX = 0f; + if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A)) + inputX -= 1f; + if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D)) + inputX += 1f; + if (Mathf.Approximately(inputX, 0f)) + inputX = Input.GetAxisRaw("Horizontal"); + velGrid.x = inputX * moveSpeedGridPerSec; - if (grounded && Input.GetButtonDown("Jump")) + bool jumpPressed = + Input.GetKeyDown(KeyCode.Space) || + Input.GetKeyDown(KeyCode.W) || + Input.GetKeyDown(KeyCode.UpArrow) || + Input.GetButtonDown("Jump"); + + if (grounded && jumpPressed) { velGrid.y = jumpVelocityGridPerSec; grounded = false; diff --git a/README.md b/README.md index 0202d7c..996df04 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,70 @@ # First Principles: An Interactive Module -A **Graphing** Calculator. Developed by [Rayan Kaissi](https://github.com/GameGenesis) and [John Seong](https://github.com/wonmor). Powered by **Unity** Game Engine. + +A **graphing calculator** and **derivative-driven platformer** built in **Unity 6**. +Developed by [Rayan Kaissi](https://github.com/GameGenesis) and [John Seong](https://github.com/wonmor). Part of the not-for-profit, open-source **College Math For Toddlers** initiative (**MIT**). --- -## Alpha Build 0.3 +## Alpha build 0.3+ -**Development** is still in **progress**, but will be completed shortly! +The project pairs a **Cartesian graph UI** (functions + numeric derivatives) with **Limbo-style** gameplay: **platforms** follow the curve and **gaps / hazards** follow derivative rules, with **staged progression** (HUD), **level select**, and typography matched to the main equation label. -[Live Demo](https://www.youtube.com/watch?v=yo540yl4Xhs) • [Official Documentation](https://github.com/GameGenesis/First-Principles/wiki/First-Principles-Official-Documentation) • [Watch the Promotional Video](https://www.youtube.com/watch?v=k0soEFAK-CQ) +**Links:** [Demo (YouTube)](https://www.youtube.com/watch?v=yo540yl4Xhs) · [Legacy wiki](https://github.com/GameGenesis/First-Principles/wiki/First-Principles-Official-Documentation) · [Promo video](https://www.youtube.com/watch?v=k0soEFAK-CQ) --- -A puzzle game that visualizes the nature of a curve and its corresponding derivative function, and how various rules (e.g. power rule, product rule, quotient rule, chain rule, squeeze theorem, and L'Hôpital's rule) of differentiation can work in a given equation. It further clarifies the relationship between the rules; for example, proving the mechanism behind power rule using the first principles ```lim h->0 (f(x+h) - f(x)) / h```, or explaining the product rule using the power rule. Same goes for the editions that will be released later, which include topics such as definite/indefinite integration and infinite series. +## Documentation (GitHub Pages) + +Comprehensive docs for setup, gameplay, architecture, and troubleshooting live in **`/docs`** (Jekyll, GitHub Pages–compatible). + +| | | +|--|--| +| **Browse in repo** | [`docs/index.md`](docs/index.md) | +| **Published site** | After you enable **Settings → Pages → `/docs`**: `https://.github.io/First-Principles/` (set `url` / `baseurl` in [`docs/_config.yml`](docs/_config.yml)) | + +Topics covered: **Unity 6000.4.0f1** project path (`First Principles/`), **Menu → Level select → Game**, controls (**arrows / WASD**, **Space** jump), **stages HUD**, package restore script, TextMeshPro, and Pages **404** fixes. --- -### The game will be released in a total of *four* editions: -1. **Pre-Calculus**: The Nature of Functions -2. **The Fundamentals of Calculus**: Limits and Differentiation -3. **Integral Calculus**: The Most Powerful Humanmade Tool -4. **Infinite Series**: Let's Explore Above and Beyond +## Repository layout + +| Path | Purpose | +|------|---------| +| **`First Principles/`** | **Unity project root** — open this folder in Unity Hub (`Assets`, `ProjectSettings`, `Packages`). | +| **`docs/`** | GitHub Pages documentation (Jekyll). | +| **`clean-unity-library.sh`** | Deletes `First Principles/Library` and stray `Packages/com.unity.*` embeds; use when packages are corrupt. | +| **`README.md`** | This file. | + +--- + +## Quick start + +1. Install **Unity 6000.4.0f1** (see `First Principles/ProjectSettings/ProjectVersion.txt`). +2. **Add** the folder **`…/First-Principles/First Principles`** in Unity Hub. +3. Open **Menu** scene, press **Play**. +4. **Play** flow: **Menu** → **Level select** → **Game** (level index is carried by `LevelSelection`). + +--- + +## Gameplay highlights + +- **Level select** — Runtime UI from `LevelSelectController` + `GameLevelCatalog` names. +- **Stages** — Derivative “pop” thresholds; **Stage *k* / *n*** HUD (same font stack as **Equation**). +- **controls** — ←/→ or A/D, **Space** / W / ↑ to jump; legacy Trans/Scale graph buttons are **off** in favour of level definitions. +- **Spawn** — Start column chooses the **lowest** safe platform among opening columns so you don’t begin at the top of the curve. + +--- + +## Planned editions + +1. **Pre-Calculus** — The nature of functions +2. **Fundamentals of Calculus** — Limits and differentiation +3. **Integral calculus** +4. **Infinite series** + +--- + +## Screenshots Screen Shot 2022-03-15 at 2 57 39 PM @@ -29,16 +74,19 @@ A puzzle game that visualizes the nature of a curve and its corresponding deriva ## Dependencies -[**Unity** Game Engine](https://unity.com) - -[TextMeshPro](https://docs.unity3d.com/Manual/com.unity.textmeshpro.html) +- [**Unity**](https://unity.com) **6** (6000.4.0f1) +- **Unity UI (uGUI)** + **TextMesh Pro** (via editor / packages; import TMP Essentials if prompted) --- -## Troubleshooting (Unity / UGUI compile errors) +## Troubleshooting (short) -If you see errors like missing `UnityEngine.UI`, `UIToolkitInteroperabilityBridge`, or broken `UnityEngine.TestTools` types inside package paths, your **`Library`** cache and/or **`Packages/com.unity.*`** folders are usually corrupted (never edit packages under `Library/PackageCache` or embedded registry duplicates). **Quit Unity**, run **`./clean-unity-library.sh`** from the repo root (it removes `First Principles/Library` and stray `Packages/com.unity.ugui` / `com.unity.test-framework` if present), then reopen Unity. Details: [Docs/Fix-Unity-UGUI-PackageErrors.md](First%20Principles/Docs/Fix-Unity-UGUI-PackageErrors.md). +| Issue | Action | +|--------|--------| +| **Wrong project / “Untitled”** | Open **`First Principles/`** in Hub; load `Assets/Scenes/…`. | +| **UGUI / package chaos** | Quit Unity → `./clean-unity-library.sh` → reopen. Details: [`docs/troubleshooting.md`](docs/troubleshooting.md) and `First Principles/Docs/Fix-Unity-UGUI-PackageErrors.md`. | +| **GitHub Pages 404** | Set **`url`** and **`baseurl`** in `docs/_config.yml` to match your user + repo name. | --- -**MIT** License | **First Principles** is a part of a **not-for-profit** and an **open-source** project *College Math For Toddlers*. +**MIT License** · **First Principles** · *College Math For Toddlers* diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 0000000..7b5d42f --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,5 @@ +# Optional: local preview — run: bundle install && bundle exec jekyll serve +source "https://rubygems.org" + +gem "github-pages", group: :jekyll_plugins +gem "webrick", "~> 1.8" diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..3960e71 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,32 @@ +# Documentation (GitHub Pages) + +This folder is the **Jekyll** source for **GitHub Pages**. + +## Enable Pages + +1. GitHub → **Settings** → **Pages** +2. **Source:** Deploy from branch +3. **Branch:** `main` (or default), folder **`/docs`** +4. Save + +Update **`_config.yml`** with your real **`url`** and **`baseurl`** (repo name). + +## Local preview + +```bash +cd docs +bundle install +bundle exec jekyll serve +``` + +Open `http://127.0.0.1:4000/First-Principles/` (adjust for your `baseurl`). + +## Contents + +| File | Purpose | +|------|---------| +| `index.md` | Documentation home | +| `setup.md` | Unity setup & clean restore | +| `gameplay.md` | Controls, stages, flow | +| `architecture.md` | Scenes & scripts | +| `troubleshooting.md` | Packages, TMP, Pages | diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..eb78f06 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,32 @@ +# Jekyll / GitHub Pages (publish from Settings → Pages → Branch: main, folder /docs) +# Fix baseurl to match your repo name if different. +title: "First Principles" +description: "Interactive Unity graphing calculator and derivative-driven platformer — documentation" +url: "https://gamegenesis.github.io" +baseurl: "/First-Principles" + +markdown: kramdown +highlighter: rouge +permalink: pretty + +theme: minima +plugins: + - jekyll-feed + - jekyll-seo-tag + +# Minima 3.x navigation (adds top links on the generated site) +header_pages: + - index.md + - setup.md + - gameplay.md + - architecture.md + - troubleshooting.md + +# Minima skin (optional): dark, classic, solarized, solarized-dark +# minima: +# skin: dark + +exclude: + - Gemfile + - Gemfile.lock + - README.md diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..0043b51 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,75 @@ +--- +layout: page +title: Architecture +permalink: /architecture/ +--- + +# Architecture + +## Scenes (build order) + +Configured in `First Principles/ProjectSettings/EditorBuildSettings.asset`: + +- **Menu** — `SceneFader`, entry to level select or other scenes. +- **LevelSelect** — Runtime UI + `LevelSelectController`. +- **Game** — Graph UI, `GameManager` (function plotter + faders + `LevelManager`). + +## High-level diagram + +``` +Menu LevelSelect Game +──── ─────────── ──── +SceneFader LevelSelectController Canvas + graph renderers + LoadLevelSelect │ FunctionPlotter + └──► LevelSelection (static) + │ + ▼ + LevelManager + ├── Applies LevelDefinition → FunctionPlotter / colors + ├── GraphObstacleGenerator → platforms / hazards + ├── PlayerControllerUI2D + └── DerivativePopAnimator (deriv line) +``` + +## Core scripts (`Assets/Scripts`) + +### Game (`Game/`) + +| Script | Role | +|--------|------| +| `LevelDefinition` | Scriptable-style level config: function params, derivative rules, colors, story (also built at runtime in samples). | +| `LevelManager` | Orchestrates levels, HUD (stage + controls + story), theme, obstacle regen, restart / advance. | +| `GameLevelCatalog` | Display names + level count. | +| `LevelSelection` | Static bridge: selected index from LevelSelect → Game. | +| `LevelSelectController` | Builds list UI; loads **Game**. | +| `GraphObstacleGenerator` | Samples curve/derivative columns → `GraphWorld` (platforms, hazards, finish, spawn). | +| `PlayerControllerUI2D` | Grid-space movement, jump, collisions vs `GridRect` list. | +| `DerivativePopAnimator` | Short “pop” on derivative renderer at stage crossings. | + +### Functions (`Functions/`) + +| Script | Role | +|--------|------| +| `FunctionPlotter` | Samples `FunctionType`, pushes points to line/derivative renderers, equation TMP. | + +### UI (`UI/`) + +| Script | Role | +|--------|------| +| `LineRendererUI` / `DerivRendererUI` | UI-space polylines. | +| `GridRendererUI` | Grid mesh. | +| `LabelManager` / axis labels | Axis numbers. | +| `SceneFader` | Fade + `LoadGame` / `LoadLevelSelect` / `LoadMenu`. | + +## Coordinate spaces + +- **Graph:** points use the plotter’s logical X and grid Y (see grid size on `GridRendererUI`). +- **Platforms:** `GraphObstacleGenerator` maps column index → rectangles in the same grid space; `PlayerControllerUI2D` converts grid ↔ `RectTransform` pixels under **Cartesian Plane**. + +## Persistence + +- **Level index:** in-memory only (`LevelSelection`), cleared after **Consume** on Game load. + +## APIs deprecated in Unity 6 + +Project code prefers **`FindAnyObjectByType`** and parameterless **`FindObjectsByType`** over deprecated `FindObjectOfType` / sort-mode overloads where applicable. diff --git a/docs/gameplay.md b/docs/gameplay.md new file mode 100644 index 0000000..760c80c --- /dev/null +++ b/docs/gameplay.md @@ -0,0 +1,51 @@ +--- +layout: page +title: Gameplay +permalink: /gameplay/ +--- + +# Gameplay + +## Modes + +1. **Graphing calculator** — Functions are plotted on the Cartesian plane; numeric derivatives can be shown as a second curve (`FunctionPlotter`, `LineRendererUI`, `DerivRendererUI`). +2. **Derivative platformer** — On **Game** scene, a small character runs on **platforms** generated from the curve; **gaps / hazards** follow rules based on the **derivative** (`GraphObstacleGenerator`, `PlayerControllerUI2D`). + +Level parameters (colors, function type, transformation coefficients, story text) are defined in code via **`LevelManager`** sample levels / **`LevelDefinition`**. + +## Controls (platformer) + +| Input | Action | +|--------|--------| +| **← / →** or **A / D** | Move | +| **Space**, **W**, or **↑** | Jump | +| **Jump** axis / gamepad | Still supported via Unity **Input Manager** | + +Legacy **Trans** / **Scale** tuning buttons on the Game UI are **disabled**; tuning is driven by **level definitions**, not free sliders. + +## Stages + +- The run is split into **stages** (derivative **“pop”** moments at X thresholds). +- A **stage HUD** (top-left) shows **Stage *k* / *n*** using the same typography family as the equation label. +- Crossing a stage line triggers **`DerivativePopAnimator`** (visual emphasis on the derivative). + +## Level flow + +1. **Menu** — Entry and scene fade. +2. **LevelSelect** — `LevelSelectController` builds a list from **`GameLevelCatalog.DisplayNames`**; choosing a level calls **`LevelSelection.SetSelectedLevel`** and loads **Game**. +3. **Game** — `LevelManager` reads **`LevelSelection.ConsumeSelectedLevel`**, applies the level theme to **`FunctionPlotter`**, regenerates obstacles, and spawns / resets the player. + +## Spawn position + +The generator picks a **spawn column** among the “safe start” columns where the **platform top is lowest**, so the player does not always appear at the visually highest part of the early curve. + +## Win / death + +- **Hazards** or falling below a Y threshold causes **respawn** on the same level. +- Reaching the **finish zone** (far right) **advances** to the next sample level (wraps when last is complete). + +## UI polish + +- **Story** text appears at the top with the **level name** and fades (TMP). +- **Controls** hint bar at the bottom shows move / jump hints. +- **Stage** panel uses a dark “glass” backing and accent strip consistent with the Limbo-like aesthetic. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..e5cf9c6 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,45 @@ +--- +layout: default +title: Home +--- + +# First Principles — documentation + +**First Principles** is an open-source **Unity 6** project that combines a **graphing calculator** (functions and numeric derivatives on a grid) with a **Limbo-inspired 2D platformer**: platforms and hazards are driven by the curve and its derivative, with **staged progression** and per-level themes. + +Developed by [Rayan Kaissi](https://github.com/GameGenesis) and [John Seong](https://github.com/wonmor) as part of *College Math For Toddlers* (MIT). + +## Quick links + +| Guide | Description | +|------|----------------| +| [Setup]({% link setup.md %}) | Unity version, clone, open the correct project folder | +| [Gameplay]({% link gameplay.md %}) | Controls, stages, level select, how the graph affects the world | +| [Architecture]({% link architecture.md %}) | Scenes, scripts, data flow | +| [Troubleshooting]({% link troubleshooting.md %}) | Package cache, TextMeshPro, GitHub Pages / `baseurl` | + +## Repository layout + +``` +First-Principles/ ← git repository root (this site: /docs) +├── docs/ ← GitHub Pages source (you are here) +├── README.md +├── clean-unity-library.sh +└── First Principles/ ← Unity project (note the space) + ├── Assets/ + ├── Packages/ + ├── ProjectSettings/ + └── ... +``` + +Always open the **`First Principles`** folder (the one that contains `Assets` and `ProjectSettings`) in Unity Hub — not the parent git folder alone. + +## External links + +- [YouTube — demo](https://www.youtube.com/watch?v=yo540yl4Xhs) +- [Wiki — official documentation (legacy)](https://github.com/GameGenesis/First-Principles/wiki/First-Principles-Official-Documentation) +- [Repository](https://github.com/GameGenesis/First-Principles) + +--- + +*Documentation version aligned with Unity **6000.4.0f1** and the graph + platformer flow described in this site.* diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 0000000..d5c2493 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,81 @@ +--- +layout: page +title: Setup +permalink: /setup/ +--- + +# Setup + +## Requirements + +| Requirement | Notes | +|-------------|--------| +| **Unity Editor** | **6000.4.0f1** (Unity 6) — see `First Principles/ProjectSettings/ProjectVersion.txt` | +| **Disk** | Large `Library/` folder; safe to delete and regenerate | +| **OS** | Windows / macOS / Linux (editor-supported) | + +Optional: **Git LFS** if you add large assets later (not required for the scripts-focused workflow). + +## Clone the repository + +```bash +git clone https://github.com/GameGenesis/First-Principles.git +cd First-Principles +``` + +If you fork the repo, use your fork URL instead. + +## Open the correct Unity project + +The Unity project lives in a subfolder **with a space** in the name: + +``` +First-Principles/First Principles/ +``` + +In **Unity Hub** → **Add** → choose: + +`.../First-Principles/First Principles` + +You should see `Assets`, `Packages`, and `ProjectSettings` at the root of the added project. + +**Common mistake:** adding only `First-Principles` (parent) will not load the Unity project correctly. + +## First open / packages + +1. Open the project in Unity and wait for **import** and **package restore**. +2. If you use **TextMesh Pro** for the first time, import **TMP Essentials** when prompted (*GameObject → UI → Text - TextMeshPro* often triggers the importer). + +## Clean restore (corrupted Library / packages) + +If you see compile errors about immutable packages, missing UGUI types, or broken test framework assemblies: + +1. Quit Unity. +2. From the repo root run: + + ```bash + ./clean-unity-library.sh + ``` + +3. Reopen the project. + +See [Troubleshooting]({% link troubleshooting.md %}) for details. + +## Run the game + +1. Open scene **`Assets/Scenes/Menu.unity`** (or build settings entry scene). +2. **Play** from the Editor. + +Flow: **Menu** → **Level select** → **Game** (chosen level index is passed via `LevelSelection`). + +## Optional: local documentation site + +From the `docs/` folder (Ruby + Bundler required): + +```bash +cd docs +bundle install +bundle exec jekyll serve +``` + +Browse to `http://localhost:4000/First-Principles/` (include `baseurl` path if configured). diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..dc2dd17 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,52 @@ +--- +layout: page +title: Troubleshooting +permalink: /troubleshooting/ +--- + +# Troubleshooting + +## Unity opens the wrong folder / “Untitled” scene + +- Add **`…/First-Principles/First Principles`** (with **`Assets`**) in Unity Hub — not only the git parent folder. +- If the scene tab says **Untitled**, use **File → Open Scene** and open `Assets/Scenes/Game.unity` (or save your scene under `Assets/Scenes/`). + +## Package / UGUI / test framework compile errors + +**Symptoms:** `UIToolkitInteroperabilityBridge` missing, `UnityEngine.TestTools.Logging` missing, orphan `.meta` under `Packages/com.unity.*`, or errors pointing at **`Library/PackageCache`** and **immutable** packages. + +**Cause:** Corrupt **`Library`** and/or **local embedded** folders under `First Principles/Packages/com.unity.ugui` or `com.unity.test-framework` that override registry packages. + +**Fix:** + +1. Quit Unity. +2. From repo root: `./clean-unity-library.sh` +3. Reopen the project. + +Do **not** hand-edit `Library/PackageCache`. The repo’s `Packages/` in git should normally only contain `manifest.json` and `packages-lock.json`. + +More detail in the Unity project doc: +`First Principles/Docs/Fix-Unity-UGUI-PackageErrors.md` (in the repository). + +## TextMesh Pro — “No Font Asset” / import does nothing + +- Ensure **TMP Essentials** imported (try **GameObject → UI → Text - TextMeshPro** to trigger the wizard). +- **`Window → TextMeshPro → Import TMP Essential Resources`** +- Gameplay HUD copies typography from the **Equation** label; if that object has no font, assign font assets under **TextMesh Pro** resources in the inspector. + +## GitHub Pages — site 404 or wrong paths + +1. **Repository** → **Settings** → **Pages** → Build from **`main`** / **`master`**, folder **`/docs`**. +2. Edit **`docs/_config.yml`**: + - `url`: your GitHub Pages host (e.g. `https://.github.io`). + - `baseurl`: `/` (leading slash, no trailing slash), e.g. `/First-Principles`. + +After changing `_config.yml`, wait for the **Pages build** to finish (Actions tab). + +## `.gitignore` and `Library/` + +The root `.gitignore` ignores **`**/Library/`**, **`**/UserSettings/`**, etc., so the Unity project under **`First Principles/`** is covered. Do not commit `Library/` — teammates regenerate it locally. + +## Input Manager deprecation warning + +Unity may warn that the **Input Manager** is legacy. The platformer uses **`Input.GetKey`** for keyboard and axes for other devices. Migrating to the **Input System** package is optional and not required for current controls. From deb710ad461710c42d841bba4e54fb08a66dfcdb Mon Sep 17 00:00:00 2001 From: John Seong Date: Thu, 19 Mar 2026 14:12:10 -0400 Subject: [PATCH 02/27] Added new level - Series --- .../Scripts/Functions/FunctionPlotter.cs | 116 +++++++++++++++++- .../Assets/Scripts/Game/GameLevelCatalog.cs | 9 +- .../Assets/Scripts/Game/LevelDefinition.cs | 14 ++- .../Assets/Scripts/Game/LevelManager.cs | 5 + 4 files changed, 140 insertions(+), 4 deletions(-) diff --git a/First Principles/Assets/Scripts/Functions/FunctionPlotter.cs b/First Principles/Assets/Scripts/Functions/FunctionPlotter.cs index 1b1d99d..85d46d6 100644 --- a/First Principles/Assets/Scripts/Functions/FunctionPlotter.cs +++ b/First Principles/Assets/Scripts/Functions/FunctionPlotter.cs @@ -172,10 +172,87 @@ 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), + _ => 0f }; } + /// 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 void UpdateEquationText(FunctionType type, float transA, float transK, float transC, float transD, int power, int baseN) @@ -218,6 +295,24 @@ private void UpdateEquationText(FunctionType type, float transA, float transK, f case FunctionType.Tangent: equationText.text = $"f(x) = {a}*(tan({k}*(x - {d})) + ({c}))"; break; + case FunctionType.MaclaurinExpSeries: + equationText.text = $"Maclaurin P_{power}[e^u], u={k}(x-{d}), N={power} terms"; + break; + case FunctionType.MaclaurinSinSeries: + equationText.text = $"Maclaurin P_{power}[sin u], u={k}(x-{d})"; + break; + case FunctionType.MaclaurinCosSeries: + equationText.text = $"Maclaurin P_{power}[cos u], u={k}(x-{d})"; + break; + case FunctionType.GeometricSeriesPartial: + equationText.text = $"Sum u^k, k=0..{power}, u={k}(x-{d})"; + break; + case FunctionType.MultivarParaboloidSlice: + equationText.text = $"z = {a}·( u^2 + y0^2 ), u={k}(x-{d}), y0={c} — paraboloid"; + break; + case FunctionType.MultivarSaddleSlice: + equationText.text = $"z = {a}·( u^2 - y0^2 ), u={k}(x-{d}), y0={c} — saddle"; + break; default: equationText.text = "f(x)"; break; @@ -227,7 +322,26 @@ private void UpdateEquationText(FunctionType type, float transA, float transK, f 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 } /* diff --git a/First Principles/Assets/Scripts/Game/GameLevelCatalog.cs b/First Principles/Assets/Scripts/Game/GameLevelCatalog.cs index 81cb057..6c7546d 100644 --- a/First Principles/Assets/Scripts/Game/GameLevelCatalog.cs +++ b/First Principles/Assets/Scripts/Game/GameLevelCatalog.cs @@ -1,16 +1,21 @@ using UnityEngine; /// -/// 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 { public static readonly string[] DisplayNames = { + "First Principles Primer", "Slope of Parabola", "Waves of Sine", "Shadows of Cosine", - "Absolute Path" + "Absolute Path", + "Maclaurin: e^x", + "Series: geometric tail", + "Saddle slice (multivar)", + "Paraboloid slice (multivar)" }; public static int LevelCount => DisplayNames.Length; diff --git a/First Principles/Assets/Scripts/Game/LevelDefinition.cs b/First Principles/Assets/Scripts/Game/LevelDefinition.cs index 026048c..d78dd80 100644 --- a/First Principles/Assets/Scripts/Game/LevelDefinition.cs +++ b/First Principles/Assets/Scripts/Game/LevelDefinition.cs @@ -10,8 +10,10 @@ 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 +56,16 @@ 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); + 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 2ca2acc..0e361f3 100644 --- a/First Principles/Assets/Scripts/Game/LevelManager.cs +++ b/First Principles/Assets/Scripts/Game/LevelManager.cs @@ -32,6 +32,11 @@ public class LevelManager : MonoBehaviour 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; private readonly List levels = new List(); private int currentLevelIndex; From ae75388826880141eacd2e554a9e7c94bfdcf015 Mon Sep 17 00:00:00 2001 From: John Seong Date: Thu, 19 Mar 2026 14:13:31 -0400 Subject: [PATCH 03/27] Beta Build 1.0 --- .../Assets/Scripts/Game/GameLevelCatalog.cs | 1 + .../Assets/Scripts/Game/LevelManager.cs | 230 ++++++++++++++++-- README.md | 6 +- docs/_config.yml | 2 +- docs/gameplay.md | 13 + docs/index.md | 4 +- 6 files changed, 233 insertions(+), 23 deletions(-) diff --git a/First Principles/Assets/Scripts/Game/GameLevelCatalog.cs b/First Principles/Assets/Scripts/Game/GameLevelCatalog.cs index 6c7546d..9f352b9 100644 --- a/First Principles/Assets/Scripts/Game/GameLevelCatalog.cs +++ b/First Principles/Assets/Scripts/Game/GameLevelCatalog.cs @@ -13,6 +13,7 @@ public static class GameLevelCatalog "Shadows of Cosine", "Absolute Path", "Maclaurin: e^x", + "Maclaurin: sin(x)", "Series: geometric tail", "Saddle slice (multivar)", "Paraboloid slice (multivar)" diff --git a/First Principles/Assets/Scripts/Game/LevelManager.cs b/First Principles/Assets/Scripts/Game/LevelManager.cs index 0e361f3..6972534 100644 --- a/First Principles/Assets/Scripts/Game/LevelManager.cs +++ b/First Principles/Assets/Scripts/Game/LevelManager.cs @@ -404,10 +404,40 @@ private void BuildSampleLevels() { levels.Clear(); - // Stage parameters are tuned to fit within the current UI grid range. + var primerStageColors = new[] + { + new Color(0.78f, 0.62f, 1f, 1f), + new Color(1f, 0.84f, 0.4f, 1f), + new Color(0.5f, 0.86f, 1f, 1f), + new Color(1f, 0.55f, 0.72f, 1f) + }; + levels.Add(MakeLevel( GameLevelCatalog.DisplayNames[0], FunctionType.Power, + curveColor: new Color(0.96f, 0.82f, 0.45f, 1f), + derivativeColor: new Color(0.55f, 0.78f, 1f, 1f), + transA: 0.38f, + transK: 0.42f, + transC: -1.85f, + transD: 0f, + power: 2, + baseN: 2, + story: + "Derivative = slope of the tangent — how fast f(x) rises or falls at each step.\n\n" + + "Gold light traces your path; ice-blue is f'(x) sculpting where the floor exists.\n\n" + + "Where f'(x) clears the rule, platforms hold; where it falls short, the void opens. Each bright pop is another act in the analysis you're walking through.", + derivativePopTriggerCountOverride: 4, + applyGridTheming: true, + gridCenter: new Color(0.55f, 0.45f, 0.92f, 0.4f), + gridOutside: new Color(0.4f, 0.36f, 0.62f, 0.11f), + levelStageColors: primerStageColors, + storyPauseSecondsOverride: 2.95f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[1], + FunctionType.Power, curveColor: new Color(0.9f, 0.3f, 1f, 1f), derivativeColor: new Color(1f, 0.76f, 0.1f, 1f), transA: 1f, @@ -421,7 +451,7 @@ private void BuildSampleLevels() )); levels.Add(MakeLevel( - GameLevelCatalog.DisplayNames[1], + GameLevelCatalog.DisplayNames[2], FunctionType.Sine, curveColor: new Color(0.2f, 1f, 0.7f, 1f), derivativeColor: new Color(0.2f, 0.8f, 1f, 1f), @@ -436,7 +466,7 @@ private void BuildSampleLevels() )); levels.Add(MakeLevel( - GameLevelCatalog.DisplayNames[2], + GameLevelCatalog.DisplayNames[3], FunctionType.Cosine, curveColor: new Color(1f, 0.6f, 0.2f, 1f), derivativeColor: new Color(0.9f, 0.2f, 0.6f, 1f), @@ -451,7 +481,7 @@ private void BuildSampleLevels() )); levels.Add(MakeLevel( - GameLevelCatalog.DisplayNames[3], + GameLevelCatalog.DisplayNames[4], FunctionType.Absolute, curveColor: new Color(0.4f, 0.7f, 1f, 1f), derivativeColor: new Color(1f, 0.15f, 0.15f, 1f), @@ -464,6 +494,111 @@ private void BuildSampleLevels() story: "The absolute curve folds into a single path. Where the traveler crosses the turning point, the derivative flips—and so does the ground." )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[5], + FunctionType.MaclaurinExpSeries, + curveColor: new Color(0.3f, 0.95f, 0.65f, 1f), + derivativeColor: new Color(0.95f, 0.45f, 0.25f, 1f), + transA: 0.48f, + transK: 0.14f, + transC: -2.05f, + transD: 0f, + power: 10, + baseN: 2, + story: + "Taylor polynomials hug a smooth function near a point. Maclaurin is Taylor centered at 0.\n\n" + + "Here the trail is a high-degree partial sum of e^u — polynomials stacking toward the infinite series that rebuilds the exponential.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.25f, 0.55f, 0.42f, 0.38f), + gridOutside: new Color(0.2f, 0.35f, 0.32f, 0.12f), + storyPauseSecondsOverride: 2.35f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[6], + FunctionType.MaclaurinSinSeries, + curveColor: new Color(0.45f, 0.78f, 1f, 1f), + derivativeColor: new Color(1f, 0.5f, 0.85f, 1f), + transA: 0.52f, + transK: 0.52f, + transC: -2f, + transD: 0f, + power: 8, + baseN: 2, + story: + "Odd powers of u alternate signs — that is the Maclaurin DNA of sin(u).\n\n" + + "This graph is a truncated Taylor stack climbing toward the endless sine wave; every extra term is another promise the series keeps near 0.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.28f, 0.42f, 0.65f, 0.38f), + gridOutside: new Color(0.2f, 0.3f, 0.48f, 0.11f), + storyPauseSecondsOverride: 2.3f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[7], + FunctionType.GeometricSeriesPartial, + curveColor: new Color(0.85f, 0.7f, 1f, 1f), + derivativeColor: new Color(1f, 0.35f, 0.55f, 1f), + transA: 0.42f, + transK: 0.038f, + transC: -2.1f, + transD: 0.5f, + power: 16, + baseN: 2, + story: + "A geometric series stacks powers of u. Inside its radius of convergence the tail shrinks — partial sums stabilize toward a limit.\n\n" + + "Feel how the derivative of that finite stack reshapes the terrain as you move along x.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.62f, 0.35f, 0.72f, 0.36f), + gridOutside: new Color(0.42f, 0.28f, 0.5f, 0.11f), + storyPauseSecondsOverride: 2.25f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[8], + FunctionType.MultivarSaddleSlice, + curveColor: new Color(0.5f, 0.85f, 1f, 1f), + derivativeColor: new Color(1f, 0.65f, 0.2f, 1f), + transA: 0.11f, + transK: 0.42f, + transC: 2.15f, + transD: 0f, + power: 2, + baseN: 2, + story: + "Think z = x² − y₀² with y₀ fixed — a slice through a saddle surface.\n\n" + + "The x-derivative still reads the landscape: multivariable ideas, one-variable motion.", + derivativePopTriggerCountOverride: 4, + applyGridTheming: true, + gridCenter: new Color(0.22f, 0.42f, 0.62f, 0.4f), + gridOutside: new Color(0.18f, 0.3f, 0.45f, 0.12f), + storyPauseSecondsOverride: 2.5f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[9], + FunctionType.MultivarParaboloidSlice, + curveColor: new Color(1f, 0.92f, 0.55f, 1f), + derivativeColor: new Color(0.55f, 0.35f, 1f, 1f), + transA: 0.095f, + transK: 0.4f, + transC: 1.75f, + transD: 0f, + power: 2, + baseN: 2, + story: + "Now z = x² + y₀²: an elliptic paraboloid. Freezing y₀ traces a bowl in your plane.\n\n" + + "Gradients in multivar calculus point uphill; here the slice still shows how steeply the bowl climbs as you sprint.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.55f, 0.48f, 0.28f, 0.38f), + gridOutside: new Color(0.4f, 0.35f, 0.22f, 0.11f), + storyPauseSecondsOverride: 2.35f + )); } private LevelDefinition MakeLevel( @@ -477,7 +612,13 @@ private LevelDefinition MakeLevel( float transD, int power, int baseN, - string story) + string story, + int derivativePopTriggerCountOverride = 0, + bool applyGridTheming = false, + Color gridCenter = default, + Color gridOutside = default, + Color[] levelStageColors = null, + float storyPauseSecondsOverride = 0f) { var def = ScriptableObject.CreateInstance(); def.levelName = name; @@ -502,11 +643,31 @@ private LevelDefinition MakeLevel( def.platformThicknessGrid = 0.6f; def.hazardHeightGrid = 0.5f; - def.stageDerivativePopColors = new List(defaultStageCount); - for (int i = 0; i < defaultStageCount; i++) + int popN = derivativePopTriggerCountOverride > 0 ? derivativePopTriggerCountOverride : defaultStageCount; + def.derivativePopTriggerCount = derivativePopTriggerCountOverride; + def.applyGridTheming = applyGridTheming; + if (applyGridTheming) + { + def.gridCenterLineTheming = gridCenter; + def.gridOutsideLineTheming = gridOutside; + } + + if (storyPauseSecondsOverride > 0.01f) + def.storyPauseSeconds = storyPauseSecondsOverride; + + def.stageDerivativePopColors = new List(popN); + if (levelStageColors != null && levelStageColors.Length >= popN) { - float t = defaultStageCount == 1 ? 0f : (float)i / (defaultStageCount - 1); - def.stageDerivativePopColors.Add(Color.Lerp(derivativeColor, Color.white, 0.35f * t)); + for (int i = 0; i < popN; i++) + def.stageDerivativePopColors.Add(levelStageColors[i]); + } + else + { + for (int i = 0; i < popN; i++) + { + float t = popN == 1 ? 0f : (float)i / (popN - 1); + def.stageDerivativePopColors.Add(Color.Lerp(derivativeColor, Color.white, 0.35f * t)); + } } def.storyText = story; @@ -548,20 +709,51 @@ private void ApplyLevelTheme(LevelDefinition def) curveRenderer.color = def.curveColor; derivRenderer.color = def.derivativeColor; - // Stage pop setup. - stagePopColors = def.stageDerivativePopColors ?? new List(); - if (stagePopColors.Count == 0) + int popN = def.derivativePopTriggerCount > 0 ? def.derivativePopTriggerCount : defaultStageCount; + var fromDef = def.stageDerivativePopColors; + if (fromDef != null && fromDef.Count >= popN && fromDef.Count == popN) + stagePopColors = new List(fromDef); + else if (fromDef != null && fromDef.Count >= popN) + stagePopColors = fromDef.GetRange(0, popN); + else { - stagePopColors = new List(defaultStageCount); - for (int i = 0; i < defaultStageCount; i++) - stagePopColors.Add(def.derivativeColor); + stagePopColors = new List(popN); + for (int i = 0; i < popN; i++) + { + float t = popN == 1 ? 0f : (float)i / (popN - 1); + stagePopColors.Add(Color.Lerp(def.derivativeColor, Color.white, 0.35f * t)); + } } - stageTriggerXGrid = new List(defaultStageCount); + stageTriggerXGrid = new List(popN); float width = gridRenderer.gridSize.x; - for (int i = 1; i <= defaultStageCount; i++) + for (int i = 1; i <= popN; i++) + stageTriggerXGrid.Add((i / (float)(popN + 1)) * width); + + storyMiddlePauseSeconds = def.storyPauseSeconds > 0.01f ? def.storyPauseSeconds : 1.65f; + + if (gridRenderer != null) { - stageTriggerXGrid.Add((i / (float)(defaultStageCount + 1)) * width); + if (!gridThemeBaselineCaptured) + { + savedGridCenterLine = gridRenderer.centerLine; + savedGridOutsideLine = gridRenderer.outsideLine; + gridThemeBaselineCaptured = true; + } + + if (def.applyGridTheming) + { + gridRenderer.centerLine = def.gridCenterLineTheming; + gridRenderer.outsideLine = def.gridOutsideLineTheming; + } + else + { + gridRenderer.centerLine = savedGridCenterLine; + gridRenderer.outsideLine = savedGridOutsideLine; + } + + gridRenderer.enabled = false; + gridRenderer.enabled = true; } // Story. @@ -614,7 +806,7 @@ private IEnumerator FadeStoryTextRoutine() yield return null; } - yield return new WaitForSeconds(1.5f); + yield return new WaitForSeconds(storyMiddlePauseSeconds); t = 0f; float fadeOut = 0.35f; diff --git a/README.md b/README.md index 996df04..97096be 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,11 @@ Developed by [Rayan Kaissi](https://github.com/GameGenesis) and [John Seong](htt --- -## Alpha build 0.3+ +## Beta build 1.0 -The project pairs a **Cartesian graph UI** (functions + numeric derivatives) with **Limbo-style** gameplay: **platforms** follow the curve and **gaps / hazards** follow derivative rules, with **staged progression** (HUD), **level select**, and typography matched to the main equation label. +The project pairs a **Cartesian graph UI** (functions + numeric derivatives) with **Limbo-style** gameplay: **platforms** follow the curve and **gaps / hazards** follow derivative rules, with **staged progression** (HUD), **level select**, primer plus **Taylor / Maclaurin / series / multivar** levels, and typography matched to the main equation label. + +**Player build version** (Unity **Project Settings → Player**): **1.0** (`bundleVersion`). **Links:** [Demo (YouTube)](https://www.youtube.com/watch?v=yo540yl4Xhs) · [Legacy wiki](https://github.com/GameGenesis/First-Principles/wiki/First-Principles-Official-Documentation) · [Promo video](https://www.youtube.com/watch?v=k0soEFAK-CQ) diff --git a/docs/_config.yml b/docs/_config.yml index eb78f06..6d9207e 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,7 +1,7 @@ # Jekyll / GitHub Pages (publish from Settings → Pages → Branch: main, folder /docs) # Fix baseurl to match your repo name if different. title: "First Principles" -description: "Interactive Unity graphing calculator and derivative-driven platformer — documentation" +description: "Beta 1.0 — Interactive Unity graphing calculator and derivative-driven platformer — documentation" url: "https://gamegenesis.github.io" baseurl: "/First-Principles" diff --git a/docs/gameplay.md b/docs/gameplay.md index 760c80c..73b097e 100644 --- a/docs/gameplay.md +++ b/docs/gameplay.md @@ -13,6 +13,19 @@ permalink: /gameplay/ Level parameters (colors, function type, transformation coefficients, story text) are defined in code via **`LevelManager`** sample levels / **`LevelDefinition`**. +### Sample level lineup (10) + +| # | Theme | +|---|--------| +| 1 | **First Principles Primer** — rich tutorial copy, aurora grid, four stage pops | +| 2–5 | Classic **Power / Sine / Cosine / Absolute** | +| 6 | **Maclaur partial sum of e^x** | +| 7 | **Maclaurin partial sum of sin(x)** | +| 8 | **Geometric series** partial sum Σu^k | +| 9–10 | **Multivar slices** — saddle z = x² − y₀² and paraboloid z = x² + y₀² | + +Special levels can **tint the grid** and adjust how long story text stays on screen. + ## Controls (platformer) | Input | Action | diff --git a/docs/index.md b/docs/index.md index e5cf9c6..64d1ca8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,9 @@ title: Home # First Principles — documentation -**First Principles** is an open-source **Unity 6** project that combines a **graphing calculator** (functions and numeric derivatives on a grid) with a **Limbo-inspired 2D platformer**: platforms and hazards are driven by the curve and its derivative, with **staged progression** and per-level themes. +**Release: Beta 1.0** + +**First Principles** is an open-source **Unity 6** project that combines a **graphing calculator** (functions and numeric derivatives on a grid) with a **Limbo-inspired 2D platformer**: platforms and hazards are driven by the curve and its derivative, with **staged progression** and per-level themes (including primer, series, and multivariable slices). Developed by [Rayan Kaissi](https://github.com/GameGenesis) and [John Seong](https://github.com/wonmor) as part of *College Math For Toddlers* (MIT). From 2ab9312a0114e4957c127994c457a04de209f4bb Mon Sep 17 00:00:00 2001 From: John Seong Date: Thu, 19 Mar 2026 14:15:00 -0400 Subject: [PATCH 04/27] Added Riemann Sums --- .../Scripts/Functions/FunctionPlotter.cs | 19 +++ .../Scripts/Game/GraphObstacleGenerator.cs | 48 +++++- .../Assets/Scripts/Game/LevelDefinition.cs | 16 ++ .../Assets/Scripts/Game/RiemannRule.cs | 8 + .../Scripts/UI/RiemannStripRendererUI.cs | 159 ++++++++++++++++++ 5 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 First Principles/Assets/Scripts/Game/RiemannRule.cs create mode 100644 First Principles/Assets/Scripts/UI/RiemannStripRendererUI.cs diff --git a/First Principles/Assets/Scripts/Functions/FunctionPlotter.cs b/First Principles/Assets/Scripts/Functions/FunctionPlotter.cs index 85d46d6..7a948b3 100644 --- a/First Principles/Assets/Scripts/Functions/FunctionPlotter.cs +++ b/First Principles/Assets/Scripts/Functions/FunctionPlotter.cs @@ -42,6 +42,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 +93,19 @@ 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 ?? ""; + } + private void PlotFunction(FunctionType type) { lineRenderer = FindAnyObjectByType(); @@ -317,6 +333,9 @@ private void UpdateEquationText(FunctionType type, float transA, float transK, f equationText.text = "f(x)"; break; } + + if (!string.IsNullOrEmpty(equationExtraSuffix)) + equationText.text += $"\n{equationExtraSuffix}"; } } diff --git a/First Principles/Assets/Scripts/Game/GraphObstacleGenerator.cs b/First Principles/Assets/Scripts/Game/GraphObstacleGenerator.cs index 8354fb7..c8834bf 100644 --- a/First Principles/Assets/Scripts/Game/GraphObstacleGenerator.cs +++ b/First Principles/Assets/Scripts/Game/GraphObstacleGenerator.cs @@ -53,7 +53,7 @@ public void SetLayout(RectTransform obstaclesRoot, Vector2Int gridSize, float un obstacleSprite = TryGetSquareSprite(); } - public GraphWorld GenerateWorld(LevelDefinition def, List curvePoints, List derivPoints) + public GraphWorld GenerateWorld(LevelDefinition def, List curvePoints, List derivPoints, FunctionPlotter functionPlotter = null) { if (obstaclesRoot == null) { @@ -80,6 +80,13 @@ 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; @@ -93,9 +100,42 @@ public GraphWorld GenerateWorld(LevelDefinition def, List curvePoints, 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; @@ -149,6 +189,8 @@ public GraphWorld GenerateWorld(LevelDefinition def, List curvePoints, 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) diff --git a/First Principles/Assets/Scripts/Game/LevelDefinition.cs b/First Principles/Assets/Scripts/Game/LevelDefinition.cs index d78dd80..1ef7503 100644 --- a/First Principles/Assets/Scripts/Game/LevelDefinition.cs +++ b/First Principles/Assets/Scripts/Game/LevelDefinition.cs @@ -66,6 +66,22 @@ public class LevelDefinition : ScriptableObject 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/RiemannRule.cs b/First Principles/Assets/Scripts/Game/RiemannRule.cs new file mode 100644 index 0000000..9bef363 --- /dev/null +++ b/First Principles/Assets/Scripts/Game/RiemannRule.cs @@ -0,0 +1,8 @@ +/// Which sample height is used inside each subinterval for Riemann rectangles / stair platforms. +public enum RiemannRule +{ + None = 0, + Left = 1, + Right = 2, + Midpoint = 3 +} diff --git a/First Principles/Assets/Scripts/UI/RiemannStripRendererUI.cs b/First Principles/Assets/Scripts/UI/RiemannStripRendererUI.cs new file mode 100644 index 0000000..fe853ea --- /dev/null +++ b/First Principles/Assets/Scripts/UI/RiemannStripRendererUI.cs @@ -0,0 +1,159 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; + +/// +/// Draws semi-transparent vertical strips for Riemann rectangles (from x-axis to sample height). +/// Grid coordinates match (same parent ). +/// +public class RiemannStripRendererUI : Graphic +{ + public Vector2Int gridSize = new Vector2Int(40, 40); + + private readonly List strips = new List(); // xL, xR, yMin, yMax (grid space) + private GridRendererUI grid; + +#if UNITY_EDITOR + protected override void Reset() + { + base.Reset(); + raycastTarget = false; + if (grid == null) + grid = GetComponentInParent(); + UpdateGridSizeFromParent(); + } +#endif + + protected override void Awake() + { + base.Awake(); + raycastTarget = false; + if (grid == null) + grid = GetComponentInParent(); + UpdateGridSizeFromParent(); + } + + private void Update() + { + UpdateGridSizeFromParent(); + } + + private void UpdateGridSizeFromParent() + { + if (grid != null && grid.gridSize != gridSize) + { + gridSize = grid.gridSize; + SetVerticesDirty(); + } + } + + /// Rebuild strip geometry from level + plotter. + public void Rebuild(LevelDefinition def, FunctionPlotter plotter) + { + strips.Clear(); + color = def.riemannFillColor; + + if (!def.showRiemannVisualization || def.riemannRectCount < 1 || plotter == null) + { + SetVerticesDirty(); + return; + } + + if (grid == null) + grid = GetComponentInParent(); + if (grid != null) + gridSize = grid.gridSize; + + Vector2Int origin = gridSize / 2; + int n = Mathf.Max(1, def.riemannRectCount); + float xStart = def.xStart; + float xEnd = def.xEnd; + float span = xEnd - xStart; + if (span <= 1e-6f) + { + SetVerticesDirty(); + return; + } + + float dx = span / n; + + for (int i = 0; i < n; i++) + { + float xL = xStart + i * dx; + float xR = xStart + (i + 1) * dx; + float xS = SampleX(def.riemannRule, xL, xR); + float yPlot = plotter.SampleCurvePlotterY(xS); + if (!IsFinite(yPlot)) + continue; + + float gxL = xL + origin.x; + float gxR = xR + origin.x; + float gyAxis = origin.y; + float gyTop = yPlot + origin.y; + float ymin = Mathf.Min(gyAxis, gyTop); + float ymax = Mathf.Max(gyAxis, gyTop); + + strips.Add(new Vector4(gxL, gxR, ymin, ymax)); + } + + SetVerticesDirty(); + } + + private static float SampleX(RiemannRule rule, float xL, float xR) + { + return rule switch + { + RiemannRule.Left => xL, + RiemannRule.Right => xR, + RiemannRule.Midpoint => 0.5f * (xL + xR), + _ => 0.5f * (xL + xR) + }; + } + + private static bool IsFinite(float v) => !float.IsNaN(v) && !float.IsInfinity(v); + + protected override void OnPopulateMesh(VertexHelper vh) + { + vh.Clear(); + if (strips.Count == 0) + return; + + float width = rectTransform.rect.width; + float height = rectTransform.rect.height; + if (gridSize.x < 1 || gridSize.y < 1) + return; + + float unitWidth = width / gridSize.x; + float unitHeight = height / gridSize.y; + Color c = color; + + int baseIndex = 0; + for (int i = 0; i < strips.Count; i++) + { + var s = strips[i]; + float xL = s.x; + float xR = s.y; + float ymin = s.z; + float ymax = s.w; + + UIVertex v = UIVertex.simpleVert; + v.color = c; + + v.position = new Vector3(unitWidth * xL, unitHeight * ymin, 0f); + vh.AddVert(v); + + v.position = new Vector3(unitWidth * xR, unitHeight * ymin, 0f); + vh.AddVert(v); + + v.position = new Vector3(unitWidth * xR, unitHeight * ymax, 0f); + vh.AddVert(v); + + v.position = new Vector3(unitWidth * xL, unitHeight * ymax, 0f); + vh.AddVert(v); + + vh.AddTriangle(baseIndex, baseIndex + 1, baseIndex + 2); + vh.AddTriangle(baseIndex, baseIndex + 2, baseIndex + 3); + baseIndex += 4; + } + } +} From 2161ba413bb204351b58652e9aa0f802eb27b50a Mon Sep 17 00:00:00 2001 From: John Seong Date: Thu, 19 Mar 2026 14:32:59 -0400 Subject: [PATCH 05/27] Added TMUA / MAT / AP Calculus BC / AP Physics C Content - including Polar Curves & Coordinates --- CREDITS.md | 26 + First Principles/Assets/Scenes/Game.unity | 17 +- .../Assets/Scenes/LevelSelect.unity | 17 +- First Principles/Assets/Scenes/Menu.unity | 28 +- .../Scripts/Functions/FunctionPlotter.cs | 103 ++- .../Scripts/Game/DerivativePopAnimator.cs | 4 +- .../Assets/Scripts/Game/GameLevelCatalog.cs | 35 +- .../Scripts/Game/GraphObstacleGenerator.cs | 11 + .../Scripts/Game/LearningArticleLibrary.cs | 73 ++ .../Game/LearningArticleLibrary.cs.meta | 11 + .../Assets/Scripts/Game/LevelDefinition.cs | 10 +- .../Assets/Scripts/Game/LevelManager.cs | 800 +++++++++++++++++- .../Scripts/Game/LevelSelectController.cs | 157 +++- .../Scripts/Game/PlayerControllerUI2D.cs | 16 + .../Assets/Scripts/Game/RiemannRule.cs | 6 + .../Assets/Scripts/Game/RiemannRule.cs.meta | 11 + .../Assets/Scripts/Game/SceneCreditsFooter.cs | 42 + .../Scripts/Game/SceneCreditsFooter.cs.meta | 11 + .../Assets/Scripts/UI/AudioManager.cs | 4 + .../Scripts/UI/CanvasSafeAreaBootstrap.cs | 65 ++ .../UI/CanvasSafeAreaBootstrap.cs.meta | 11 + .../Assets/Scripts/UI/DerivRendererUI.cs | 4 + .../Assets/Scripts/UI/DeviceLayout.cs | 105 +++ .../Assets/Scripts/UI/DeviceLayout.cs.meta | 11 + .../Assets/Scripts/UI/GridRendererUI.cs | 4 + .../Assets/Scripts/UI/LabelManager.cs | 4 + .../Assets/Scripts/UI/LineRendererUI.cs | 6 +- .../Assets/Scripts/UI/MathArticlesOverlay.cs | 174 ++++ .../Scripts/UI/MathArticlesOverlay.cs.meta | 11 + .../Assets/Scripts/UI/MobileInputBridge.cs | 22 + .../Scripts/UI/MobileInputBridge.cs.meta | 11 + .../Assets/Scripts/UI/MobileTouchControls.cs | 261 ++++++ .../Scripts/UI/MobileTouchControls.cs.meta | 11 + .../Assets/Scripts/UI/MobileUiRoots.cs | 37 + .../Assets/Scripts/UI/MobileUiRoots.cs.meta | 11 + .../Scripts/UI/RiemannStripRendererUI.cs | 2 + .../Scripts/UI/RiemannStripRendererUI.cs.meta | 11 + .../Assets/Scripts/UI/SceneFader.cs | 10 + .../ProjectSettings/ProjectSettings.asset | 2 +- LICENSE | 48 +- README.md | 13 +- docs/README.md | 4 + docs/_config.yml | 2 + docs/ap-calculus-bc.md | 79 ++ docs/ap-physics-c.md | 65 ++ docs/architecture.md | 9 +- docs/engineering-math.md | 99 +++ docs/gameplay.md | 34 +- docs/index.md | 10 +- docs/mat-calculus.md | 76 ++ docs/math-concepts.md | 141 +++ docs/tmua-calculus.md | 93 ++ 52 files changed, 2717 insertions(+), 111 deletions(-) create mode 100644 CREDITS.md create mode 100644 First Principles/Assets/Scripts/Game/LearningArticleLibrary.cs create mode 100644 First Principles/Assets/Scripts/Game/LearningArticleLibrary.cs.meta create mode 100644 First Principles/Assets/Scripts/Game/RiemannRule.cs.meta create mode 100644 First Principles/Assets/Scripts/Game/SceneCreditsFooter.cs create mode 100644 First Principles/Assets/Scripts/Game/SceneCreditsFooter.cs.meta create mode 100644 First Principles/Assets/Scripts/UI/CanvasSafeAreaBootstrap.cs create mode 100644 First Principles/Assets/Scripts/UI/CanvasSafeAreaBootstrap.cs.meta create mode 100644 First Principles/Assets/Scripts/UI/DeviceLayout.cs create mode 100644 First Principles/Assets/Scripts/UI/DeviceLayout.cs.meta create mode 100644 First Principles/Assets/Scripts/UI/MathArticlesOverlay.cs create mode 100644 First Principles/Assets/Scripts/UI/MathArticlesOverlay.cs.meta create mode 100644 First Principles/Assets/Scripts/UI/MobileInputBridge.cs create mode 100644 First Principles/Assets/Scripts/UI/MobileInputBridge.cs.meta create mode 100644 First Principles/Assets/Scripts/UI/MobileTouchControls.cs create mode 100644 First Principles/Assets/Scripts/UI/MobileTouchControls.cs.meta create mode 100644 First Principles/Assets/Scripts/UI/MobileUiRoots.cs create mode 100644 First Principles/Assets/Scripts/UI/MobileUiRoots.cs.meta create mode 100644 First Principles/Assets/Scripts/UI/RiemannStripRendererUI.cs.meta create mode 100644 docs/ap-calculus-bc.md create mode 100644 docs/ap-physics-c.md create mode 100644 docs/engineering-math.md create mode 100644 docs/mat-calculus.md create mode 100644 docs/math-concepts.md create mode 100644 docs/tmua-calculus.md diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 0000000..880401b --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,26 @@ +# 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** (Rayan Kaissi) × **ORCH AEROSPACE** (John Wonmo Seong) + +## Project + +- **Name:** First Principles +- **License:** [Proprietary](LICENSE) — all rights reserved. +- **Initiative:** *College Math For Toddlers* (not-for-profit) + +## 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. | + +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/Scenes/Game.unity b/First Principles/Assets/Scenes/Game.unity index e65501d..9aaa8a4 100644 --- a/First Principles/Assets/Scenes/Game.unity +++ b/First Principles/Assets/Scenes/Game.unity @@ -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 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..0b4448e 100644 --- a/First Principles/Assets/Scenes/Menu.unity +++ b/First Principles/Assets/Scenes/Menu.unity @@ -591,12 +591,15 @@ MonoBehaviour: m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_text: 'Alpha Build 0.3 + m_text: 'First Principles + Version 1.0 - Powered by Unity. + Credits + GAME GENESIS (Rayan Kaissi) × ORCH AEROSPACE (John Wonmo Seong) - Developed - by Rayan Kaissi and John Seong.' + Proprietary · All rights reserved · College Math For Toddlers + + 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 +1029,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 +1069,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 7a948b3..da82edc 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; @@ -201,10 +204,52 @@ private float EvaluateFunctionY(FunctionType type, float transA, float transK, f 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 * (Mathf.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 * Mathf.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), + _ => 0f }; } + /// 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) { @@ -329,6 +374,36 @@ private void UpdateEquationText(FunctionType type, float transA, float transK, f case FunctionType.MultivarSaddleSlice: equationText.text = $"z = {a}·( u^2 - y0^2 ), u={k}(x-{d}), y0={c} — saddle"; break; + case FunctionType.DampedOscillator: + equationText.text = $"f(x) = {a}·e^(−α|u|)·sin(ωu) + ({c}), u={k}(x-{d})"; + break; + case FunctionType.HyperbolicCosine: + equationText.text = $"f(x) = {a}·(cosh(u) + ({c})), u={k}(x-{d}) — catenary model"; + break; + case FunctionType.FullWaveRectifiedSine: + equationText.text = $"f(x) = {a}·(|sin(u)| + ({c})), u={k}(x-{d})"; + break; + case FunctionType.Arctangent: + equationText.text = $"f(x) = {a}·arctan(u) + ({c}), u={k}(x-{d})"; + break; + case FunctionType.Logistic: + equationText.text = $"Logistic S-curve: L≈{a}, u={k}(x-{d}), steepness∝{baseN}"; + break; + case FunctionType.HyperbolicSine: + equationText.text = $"f(x) = {a}·sinh(u) + ({c}), u={k}(x-{d})"; + break; + case FunctionType.ExponentialDecay: + equationText.text = $"f(x) = {a}·e^(−{k}|u|) + ({c}), u={k}(x-{d})"; + break; + case FunctionType.PolarCardioid: + equationText.text = $"Polar: r ∝ (1+cos θ); θ↔ u={k}(x-{d})"; + break; + case FunctionType.PolarRose: + equationText.text = $"Polar: r ∝ cos({power}·θ); θ↔ u={k}(x-{d})"; + break; + case FunctionType.CircleUpper: + equationText.text = $"Upper arc: u² + (y−{c})² = {a}², u={k}(x−{d}), R=|{a}|"; + break; default: equationText.text = "f(x)"; break; @@ -360,7 +435,23 @@ public enum FunctionType // Multivariable surfaces as 1D slices (fixed y₀ = transC, δ = transD shift) MultivarParaboloidSlice, - MultivarSaddleSlice + 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 } /* 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 9f352b9..084ae84 100644 --- a/First Principles/Assets/Scripts/Game/GameLevelCatalog.cs +++ b/First Principles/Assets/Scripts/Game/GameLevelCatalog.cs @@ -1,10 +1,18 @@ 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 sample levels). /// public static class GameLevelCatalog { + /// Human-readable titles; indices drive LevelSelectController button order. public static readonly string[] DisplayNames = { "First Principles Primer", @@ -16,7 +24,32 @@ public static class GameLevelCatalog "Maclaurin: sin(x)", "Series: geometric tail", "Saddle slice (multivar)", - "Paraboloid 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²" }; public static int LevelCount => DisplayNames.Length; diff --git a/First Principles/Assets/Scripts/Game/GraphObstacleGenerator.cs b/First Principles/Assets/Scripts/Game/GraphObstacleGenerator.cs index c8834bf..b05b3ba 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; @@ -53,6 +63,7 @@ public void SetLayout(RectTransform obstaclesRoot, Vector2Int gridSize, float un obstacleSprite = TryGetSquareSprite(); } + /// 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) diff --git a/First Principles/Assets/Scripts/Game/LearningArticleLibrary.cs b/First Principles/Assets/Scripts/Game/LearningArticleLibrary.cs new file mode 100644 index 0000000..51cc72e --- /dev/null +++ b/First Principles/Assets/Scripts/Game/LearningArticleLibrary.cs @@ -0,0 +1,73 @@ +/// +/// Plain-language math snippets for the in-game reader (TextMeshPro rich text). +/// Kept in one place so GitHub Pages docs can mirror the same ideas in Markdown. +/// When you add a new curriculum theme (new level block), update this string **and** docs/math-concepts.md / exam prep pages as needed. +/// +public static class LearningArticleLibrary +{ + /// Large verbatim TMP block; embedded newlines are meaningful for paragraph breaks. + public static string GetLevelSelectArticleRichText() + { + return @"Math tips & snippets +Short reads tied to the graph + platformer stages. Tap the dark backdrop or Close when done. + +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)²+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⁰+u¹+u²+… 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₀ — 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 ∫ₐᵇ 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*)·Δ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^{-decay}·sin(ω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)² + (y−k)² = R² +A circle is usually written implicitly. Solving for y gives two branches (±√). The game’s circle stage uses the upper semicircle so the path stays a function y(x) over one sweep. Implicit differentiation yields dy/dx = −(x−h)/(y−k); at the ends of the diameter the tangent is vertical (slope blows up). + +──────── Exam prep (separate tracks) ──────── + +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″, ∫ as signed area / FTC mood, domains of ln & √, 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: A = ½∫ r² dθ; arc length √(r²+(dr/dθ)²). 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, τ = dL/dt, L = Iω on fixed axis, angular momentum conservation when τ_ext = 0; E&M uses flux / line-integral setups. Game motifs: exponential decay (τ), projectile parabola, rotation / SHM—visual hooks only. → docs/ap-physics-c.md + +Where to read more +docs/math-concepts.md (index), docs/engineering-math.md (applied circuits/oscillations), and the four prep files above on GitHub Pages. + +— GAME GENESIS × ORCH AEROSPACE · College Math For Toddlers +"; + } +} 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 1ef7503..0ff872e 100644 --- a/First Principles/Assets/Scripts/Game/LevelDefinition.cs +++ b/First Principles/Assets/Scripts/Game/LevelDefinition.cs @@ -1,9 +1,17 @@ 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 diff --git a/First Principles/Assets/Scripts/Game/LevelManager.cs b/First Principles/Assets/Scripts/Game/LevelManager.cs index 6972534..59e005d 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), Riemann overlay helper, touch controls. +// 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,6 +45,7 @@ public class LevelManager : MonoBehaviour private GraphObstacleGenerator obstacleGenerator; private PlayerControllerUI2D playerController; private DerivativePopAnimator popAnimator; + private RiemannStripRendererUI riemannRenderer; private RectTransform obstaclesRoot; @@ -38,9 +60,11 @@ public class LevelManager : MonoBehaviour 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; @@ -62,10 +86,14 @@ private void Start() { SetupReferences(); 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(); @@ -98,6 +126,10 @@ private void SetupReferences() CreateStoryTextIfNeeded(); CreateGameplayHudIfNeeded(); HideLegacyGraphTuningButtons(); + EnsureRiemannRenderer(); + var mainCanvas = FindAnyObjectByType(); + if (mainCanvas != null) + MobileTouchControls.EnsureForGameCanvas(mainCanvas.transform); // Wire callbacks. playerController.SetDeathCallback(RestartCurrentLevel); @@ -159,21 +191,26 @@ private void CreateStoryTextIfNeeded() } var storyGo = new GameObject("StoryText"); - storyGo.transform.SetParent(canvas.transform, false); + var safe = MobileUiRoots.GetSafeContentParent(canvas.transform); + storyGo.transform.SetParent(safe != null ? safe : canvas.transform, false); var tmp = storyGo.AddComponent(); tmp.text = ""; tmp.fontSize = 32; + tmp.enableAutoSizing = true; + tmp.fontSizeMin = 20; + tmp.fontSizeMax = 34; tmp.alignment = TextAlignmentOptions.Center; tmp.richText = true; + tmp.enableWordWrapping = true; ApplyPrimaryUiTypography(tmp, FindPrimaryEquationTmp(), outlineWidth: 0.06f, outlineAlpha: 0.35f); var rt = tmp.rectTransform; - rt.anchorMin = new Vector2(0.5f, 1f); - rt.anchorMax = new Vector2(0.5f, 1f); + rt.anchorMin = new Vector2(0.06f, 1f); + rt.anchorMax = new Vector2(0.94f, 1f); rt.pivot = new Vector2(0.5f, 1f); - rt.anchoredPosition = new Vector2(0, -90f); - rt.sizeDelta = new Vector2(900f, 120f); + rt.anchoredPosition = new Vector2(0, -72f); + rt.sizeDelta = new Vector2(0f, 200f); tmp.color = new Color(1f, 1f, 1f, 0f); @@ -204,12 +241,14 @@ private void CreateGameplayHudIfNeeded() { var panelGo = new GameObject("StageHudPanel"); var panelRt = panelGo.AddComponent(); - panelRt.SetParent(canvas.transform, false); + var safe = MobileUiRoots.GetSafeContentParent(canvas.transform); + panelRt.SetParent(safe != null ? safe : canvas.transform, false); panelRt.anchorMin = new Vector2(0f, 1f); panelRt.anchorMax = new Vector2(0f, 1f); panelRt.pivot = new Vector2(0f, 1f); - panelRt.anchoredPosition = new Vector2(22f, -20f); - panelRt.sizeDelta = new Vector2(340f, 76f); + float topPad = DeviceLayout.PreferOnScreenGameControls ? 12f : 20f; + panelRt.anchoredPosition = new Vector2(18f, -topPad); + panelRt.sizeDelta = new Vector2(380f, 80f); var panelBg = panelGo.AddComponent(); panelBg.sprite = panelSprite; @@ -244,7 +283,10 @@ private void CreateGameplayHudIfNeeded() var tmp = textGo.AddComponent(); tmp.richText = true; tmp.enableWordWrapping = false; - tmp.fontSize = 30; + tmp.fontSize = 28; + tmp.enableAutoSizing = true; + tmp.fontSizeMin = 18; + tmp.fontSizeMax = 30; tmp.alignment = TextAlignmentOptions.MidlineLeft; tmp.color = new Color(0.94f, 0.95f, 0.98f, 1f); tmp.characterSpacing = 0.35f; @@ -257,14 +299,17 @@ private void CreateGameplayHudIfNeeded() if (controlsHintText == null) { + bool tabletUi = DeviceLayout.IsTabletLike(); var barGo = new GameObject("ControlsHintPanel"); var barRt = barGo.AddComponent(); - barRt.SetParent(canvas.transform, false); + var safe = MobileUiRoots.GetSafeContentParent(canvas.transform); + barRt.SetParent(safe != null ? safe : canvas.transform, false); barRt.anchorMin = new Vector2(0.5f, 0f); barRt.anchorMax = new Vector2(0.5f, 0f); barRt.pivot = new Vector2(0.5f, 0f); - barRt.anchoredPosition = new Vector2(0f, 18f); - barRt.sizeDelta = new Vector2(620f, 54f); + float up = DeviceLayout.PreferOnScreenGameControls ? DeviceLayout.TouchHintVerticalOffset : 22f; + barRt.anchoredPosition = new Vector2(0f, up); + barRt.sizeDelta = new Vector2(tabletUi ? 900f : 760f, tabletUi ? 60f : 56f); var barBg = barGo.AddComponent(); barBg.sprite = panelSprite; @@ -283,20 +328,53 @@ private void CreateGameplayHudIfNeeded() var tmp = textGo.AddComponent(); tmp.richText = true; tmp.enableWordWrapping = false; - tmp.fontSize = 22; + tmp.fontSize = 21; + tmp.enableWordWrapping = true; tmp.alignment = TextAlignmentOptions.Midline; tmp.color = new Color(0.82f, 0.85f, 0.92f, 0.92f); tmp.characterSpacing = 0.25f; ApplyPrimaryUiTypography(tmp, equationStyle, outlineWidth: 0.14f, outlineAlpha: 0.5f); - tmp.text = - "Move " + - "\u2190 \u2192 " + - "· " + - "Jump " + - "Space"; + tmp.text = DeviceLayout.PreferOnScreenGameControls + ? "Move \u25C0 \u25B6 · Jump tap (keyboard: arrows / Space)" + : "Move " + + "\u2190 \u2192 " + + "· " + + "Jump " + + "Space"; controlsHintText = tmp; } + + CreateSceneCreditsFooterStrip(canvas, equationStyle, panelSprite); + } + + private void CreateSceneCreditsFooterStrip(Canvas canvas, TextMeshProUGUI equationStyle, Sprite panelSprite) + { + if (canvas == null || GameObject.Find("SceneCreditsFooter") != null) + return; + + bool tabletUi = DeviceLayout.IsTabletLike(); + var footerGo = new GameObject("SceneCreditsFooter"); + var footerRt = footerGo.AddComponent(); + var safe = MobileUiRoots.GetSafeContentParent(canvas.transform); + footerRt.SetParent(safe != null ? safe : canvas.transform, false); + footerRt.anchorMin = new Vector2(0.5f, 0f); + footerRt.anchorMax = new Vector2(0.5f, 0f); + footerRt.pivot = new Vector2(0.5f, 0f); + float controlsUp = DeviceLayout.PreferOnScreenGameControls ? DeviceLayout.TouchHintVerticalOffset : 22f; + float barH = tabletUi ? 60f : 56f; + footerRt.anchoredPosition = new Vector2(0f, controlsUp + barH + 12f); + footerRt.sizeDelta = new Vector2(tabletUi ? 960f : 880f, 74f); + + var ftmp = footerGo.AddComponent(); + ftmp.text = SceneCreditsFooter.BuildCompactRichText(); + ftmp.richText = true; + ftmp.enableWordWrapping = true; + ftmp.fontSize = tabletUi ? 16 : 14; + ftmp.alignment = TextAlignmentOptions.Bottom; + ftmp.color = new Color(0.88f, 0.89f, 0.92f, 0.9f); + ftmp.raycastTarget = false; + ApplyPrimaryUiTypography(ftmp, equationStyle, outlineWidth: 0.1f, outlineAlpha: 0.45f); } /// The big equation label in Game — used as the typography reference for all gameplay HUD copy. @@ -400,6 +478,37 @@ private Sprite TryGetSquareSprite() return null; } + /// + /// Finds or creates under the cartesian plane, sibling-ordered under the main curve line. + /// + private void EnsureRiemannRenderer() + { + if (riemannRenderer == null) + riemannRenderer = FindAnyObjectByType(); + + if (riemannRenderer != null || curveRenderer == null || cartesianPlaneRect == null) + return; + + var go = new GameObject("RiemannStrips"); + go.transform.SetParent(cartesianPlaneRect, false); + var rt = go.AddComponent(); + rt.anchorMin = Vector2.zero; + rt.anchorMax = Vector2.one; + rt.offsetMin = Vector2.zero; + rt.offsetMax = Vector2.zero; + + riemannRenderer = go.AddComponent(); + riemannRenderer.raycastTarget = false; + + int lineIdx = curveRenderer.transform.GetSiblingIndex(); + go.transform.SetSiblingIndex(lineIdx); + } + + /// + /// Constructs all built-in instances. When adding a level: + /// append to with the SAME index, then add a + /// matching levels.Add(MakeLevel(...)) block here using that display name index. + /// private void BuildSampleLevels() { levels.Clear(); @@ -599,8 +708,618 @@ private void BuildSampleLevels() gridOutside: new Color(0.4f, 0.35f, 0.22f, 0.11f), storyPauseSecondsOverride: 2.35f )); + + var integralStageColors = new[] + { + new Color(0.45f, 0.82f, 1f, 1f), + new Color(0.95f, 0.72f, 0.35f, 1f), + new Color(0.75f, 0.55f, 1f, 1f) + }; + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[10], + FunctionType.NaturalExp, + curveColor: new Color(0.98f, 0.88f, 0.48f, 1f), + derivativeColor: new Color(0.3f, 0.78f, 1f, 1f), + transA: 0.34f, + transK: 0.2f, + transC: -1.88f, + transD: 0f, + power: 2, + baseN: 2, + story: + "The definite integral of a nonnegative rate is the accumulated amount — here, the area under the curve between two x-values.\n\n" + + "The blue glass columns are a Riemann sum: chop the interval into equal widths Δx, pick a sample height f(x*) in each slice, and add up f(x*)·Δx. With more rectangles, the sum hugs the true area — ∫ f(x) dx.\n\n" + + "Your trail still follows the smooth graph; the shading only approximates what integration measures.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.22f, 0.4f, 0.58f, 0.38f), + gridOutside: new Color(0.16f, 0.28f, 0.42f, 0.11f), + levelStageColors: integralStageColors, + storyPauseSecondsOverride: 2.75f, + riemannRule: RiemannRule.None, + riemannRectCount: 22, + showRiemannVisualization: true, + useRiemannStairPlatforms: false, + riemannFillColor: new Color(0.25f, 0.52f, 0.92f, 0.3f) + )); + + var riemannLeftColors = new[] + { + new Color(0.98f, 0.45f, 0.42f, 1f), + new Color(1f, 0.78f, 0.35f, 1f), + new Color(0.55f, 0.95f, 0.62f, 1f) + }; + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[11], + FunctionType.Power, + curveColor: new Color(1f, 0.82f, 0.35f, 1f), + derivativeColor: new Color(0.95f, 0.35f, 0.42f, 1f), + transA: 0.44f, + transK: 0.36f, + transC: -1.82f, + transD: 0f, + power: 2, + baseN: 2, + story: + "Left-handed Riemann sum: in each subinterval [xᵢ, xᵢ₊₁], take the rectangle height f(xᵢ) — the left endpoint.\n\n" + + "If f is increasing, left samples are always the shortest side of the strip, so the sum underestimates the area.\n\n" + + "Platforms are flat steps at those left heights — feel the conservative staircase under the parabola.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.52f, 0.28f, 0.22f, 0.36f), + gridOutside: new Color(0.38f, 0.2f, 0.18f, 0.1f), + levelStageColors: riemannLeftColors, + storyPauseSecondsOverride: 2.55f, + riemannRule: RiemannRule.Left, + riemannRectCount: 14, + showRiemannVisualization: true, + useRiemannStairPlatforms: true, + riemannFillColor: new Color(0.95f, 0.35f, 0.4f, 0.32f) + )); + + var riemannRightColors = new[] + { + new Color(0.35f, 0.72f, 1f, 1f), + new Color(0.55f, 0.95f, 0.85f, 1f), + new Color(0.85f, 0.55f, 1f, 1f) + }; + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[12], + FunctionType.Power, + curveColor: new Color(1f, 0.82f, 0.35f, 1f), + derivativeColor: new Color(0.35f, 0.75f, 1f, 1f), + transA: 0.44f, + transK: 0.36f, + transC: -1.82f, + transD: 0f, + power: 2, + baseN: 2, + story: + "Right-handed Riemann sum: height f(xᵢ₊₁) — the right endpoint of each slice.\n\n" + + "For an increasing function, right endpoints are taller, so this rule overestimates the area — the mirror story of the left sum.\n\n" + + "Each step jumps at the right edge; compare mentally with the left-endpoint stage.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.2f, 0.38f, 0.55f, 0.37f), + gridOutside: new Color(0.15f, 0.28f, 0.4f, 0.1f), + levelStageColors: riemannRightColors, + storyPauseSecondsOverride: 2.55f, + riemannRule: RiemannRule.Right, + riemannRectCount: 14, + showRiemannVisualization: true, + useRiemannStairPlatforms: true, + riemannFillColor: new Color(0.25f, 0.55f, 0.95f, 0.3f) + )); + + var riemannMidColors = new[] + { + new Color(0.65f, 1f, 0.55f, 1f), + new Color(0.98f, 0.72f, 0.95f, 1f), + new Color(0.72f, 0.78f, 1f, 1f) + }; + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[13], + FunctionType.Power, + curveColor: new Color(1f, 0.82f, 0.35f, 1f), + derivativeColor: new Color(0.55f, 0.95f, 0.5f, 1f), + transA: 0.44f, + transK: 0.36f, + transC: -1.82f, + transD: 0f, + power: 2, + baseN: 2, + story: + "Midpoint rule: sample at the center (xᵢ + xᵢ₊₁)/2. The rectangle straddles the strip symmetrically.\n\n" + + "On curved graphs, midpoints often cancel over/under-shoot from one side to the other — a practical choice when you want a tight approximation without taking many rectangles.\n\n" + + "Steps sit halfway along each slice; notice how the walk hugs the bowl more evenly than pure left or right rules.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.25f, 0.48f, 0.28f, 0.36f), + gridOutside: new Color(0.18f, 0.34f, 0.22f, 0.1f), + levelStageColors: riemannMidColors, + storyPauseSecondsOverride: 2.55f, + riemannRule: RiemannRule.Midpoint, + riemannRectCount: 14, + showRiemannVisualization: true, + useRiemannStairPlatforms: true, + riemannFillColor: new Color(0.45f, 0.85f, 0.55f, 0.28f) + )); + + var engDampColors = new[] + { + new Color(0.4f, 0.85f, 1f, 1f), + new Color(1f, 0.55f, 0.25f, 1f), + new Color(0.85f, 0.45f, 1f, 1f) + }; + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[14], + FunctionType.DampedOscillator, + curveColor: new Color(0.35f, 0.9f, 1f, 1f), + derivativeColor: new Color(1f, 0.5f, 0.2f, 1f), + transA: 0.52f, + transK: 0.48f, + transC: -1.95f, + transD: 0f, + power: 5, + baseN: 2, + story: + "Damped oscillation shows up everywhere in engineering: springs, circuits, structures.\n\n" + + "Imagine a weight bobbing on a spring with friction: it still wiggles, but the wiggle shrinks over time — that decay is the exponential envelope; the sine part is the vibration.\n\n" + + "Your path is the graph; the derivative still decides safe columns — watch how the slope behaves near the peaks of each ring-down.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.15f, 0.38f, 0.5f, 0.38f), + gridOutside: new Color(0.12f, 0.26f, 0.36f, 0.1f), + levelStageColors: engDampColors, + storyPauseSecondsOverride: 2.6f + )); + + var engCatColors = new[] + { + new Color(0.95f, 0.75f, 0.35f, 1f), + new Color(0.45f, 0.55f, 1f, 1f), + new Color(0.5f, 0.95f, 0.65f, 1f) + }; + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[15], + FunctionType.HyperbolicCosine, + curveColor: new Color(1f, 0.82f, 0.4f, 1f), + derivativeColor: new Color(0.35f, 0.45f, 1f, 1f), + transA: 0.16f, + transK: 0.38f, + transC: -2.15f, + transD: 0f, + power: 2, + baseN: 2, + story: + "A hanging chain or cable suspended at two points forms a catenary. In many idealized setups it is modeled with cosh — the hyperbolic cosine.\n\n" + + "Unlike a parabola (projectile motion in uniform gravity), the catenary comes from balancing tension along a flexible rope under its own weight — a classic intro to hyperbolic functions in statics.\n\n" + + "The graph climbs gently at first then steepens; engineers use these curves for arches and cables.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.48f, 0.36f, 0.2f, 0.36f), + gridOutside: new Color(0.35f, 0.26f, 0.15f, 0.1f), + levelStageColors: engCatColors, + storyPauseSecondsOverride: 2.55f + )); + + var engAcColors = new[] + { + new Color(0.95f, 0.4f, 0.9f, 1f), + new Color(0.45f, 0.9f, 1f, 1f), + new Color(1f, 0.85f, 0.4f, 1f) + }; + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[16], + FunctionType.FullWaveRectifiedSine, + curveColor: new Color(1f, 0.5f, 0.85f, 1f), + derivativeColor: new Color(0.4f, 0.85f, 1f, 1f), + transA: 0.42f, + transK: 0.58f, + transC: -1.95f, + transD: 0f, + power: 2, + baseN: 2, + story: + "|sin(x)| is a full-wave rectified AC sine: flip everything below the axis upward — like a simple model after a rectifier in power electronics.\n\n" + + "The smooth humps touch zero; the corners where sin crosses zero become sharp points, so the derivative jumps (engineering tasks often use averages / RMS values for power calculations).\n\n" + + "Use this stage as a bridge from pure trig to how waveforms look after circuits reshape them.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.45f, 0.22f, 0.42f, 0.36f), + gridOutside: new Color(0.32f, 0.16f, 0.3f, 0.1f), + levelStageColors: engAcColors, + storyPauseSecondsOverride: 2.5f + )); + + // ---- AP Calculus BC + polar + Physics C (indices 17–32) -------------------------------- + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[17], + FunctionType.Arctangent, + curveColor: new Color(0.45f, 0.82f, 1f, 1f), + derivativeColor: new Color(1f, 0.55f, 0.35f, 1f), + transA: 1.05f, + transK: 0.32f, + transC: -2.05f, + transD: 0f, + power: 2, + baseN: 2, + story: + "AP Calculus BC — inverse trig. Arctan is the hero of bounded slopes: d/dx arctan(x) = 1/(1+x²).\n\n" + + "It shows up in integrals that produce arctangent, in related‑rate geometry problems, and whenever an angle is defined from a ratio that grows slowly.\n\n" + + "The graph levels toward horizontal asymptotes — a visual for limits at ±∞.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.22f, 0.4f, 0.55f, 0.36f), + gridOutside: new Color(0.16f, 0.28f, 0.4f, 0.1f), + storyPauseSecondsOverride: 2.45f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[18], + FunctionType.Logistic, + curveColor: new Color(0.4f, 0.95f, 0.65f, 1f), + derivativeColor: new Color(0.95f, 0.35f, 0.55f, 1f), + transA: 1.65f, + transK: 0.28f, + transC: -3.15f, + transD: 0f, + power: 2, + baseN: 4, + story: + "Logistic differential equation (BC staple): dP/dt = kP(1 − P/L). Growth is nearly exponential when P is small, then curves as it nears carrying capacity L.\n\n" + + "Population models, rumor spread, and saturated chemical reactions share this S‑shape. Separation of variables leads here; the inflection point is where growth is fastest.\n\n" + + "Read the rise as “early exponential,” the bend as competition, the top as equilibrium.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.2f, 0.45f, 0.32f, 0.35f), + gridOutside: new Color(0.14f, 0.32f, 0.24f, 0.1f), + storyPauseSecondsOverride: 2.75f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[19], + FunctionType.PolarCardioid, + curveColor: new Color(1f, 0.72f, 0.35f, 1f), + derivativeColor: new Color(0.45f, 0.55f, 1f, 1f), + transA: 0.52f, + transK: 0.34f, + transC: -2.35f, + transD: 0f, + power: 2, + baseN: 2, + story: + "Polar coordinates: describe points by (r, θ) instead of (x, y). A cardioid has the family flavor r ∝ 1 + cos θ — a heartbeat‑shaped loop.\n\n" + + "Here the horizontal axis stands in for θ and the vertical for r(θ) (same trick AP uses when you first graph polar equations before converting to x = r cos θ, y = r sin θ).\n\n" + + "Area in polar uses ½∫ r² dθ; tangent slope needs dr/dθ.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.5f, 0.34f, 0.18f, 0.35f), + gridOutside: new Color(0.36f, 0.24f, 0.14f, 0.1f), + storyPauseSecondsOverride: 2.7f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[20], + FunctionType.PolarRose, + curveColor: new Color(0.95f, 0.45f, 0.9f, 1f), + derivativeColor: new Color(0.45f, 0.9f, 1f, 1f), + transA: 0.78f, + transK: 0.3f, + transC: -2f, + transD: 0f, + power: 5, + baseN: 2, + story: + "Polar rose: r ∝ cos(nθ) traces petals meeting at the origin. Odd n here gives n petals for this cosine form (a classic exam plot).\n\n" + + "Symmetry and period tell you how many times the radius returns to zero — great practice for converting polar area and arc length integrals.\n\n" + + "Watch derivative pops where r changes fastest — those are steep walls on the petal edges.", + derivativePopTriggerCountOverride: 4, + applyGridTheming: true, + gridCenter: new Color(0.42f, 0.22f, 0.48f, 0.36f), + gridOutside: new Color(0.3f, 0.16f, 0.34f, 0.1f), + storyPauseSecondsOverride: 2.65f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[21], + FunctionType.HyperbolicSine, + curveColor: new Color(0.55f, 0.95f, 0.55f, 1f), + derivativeColor: new Color(0.95f, 0.5f, 0.35f, 1f), + transA: 0.072f, + transK: 0.38f, + transC: -2.05f, + transD: 0f, + power: 2, + baseN: 2, + story: + "Hyperbolic sine & cosine (BC): sinh x = (e^x − e^{−x})/2, cosh x = (e^x + e^{−x})/2, and cosh² − sinh² = 1 (a hyperbola identity).\n\n" + + "They solve linear ODEs, describe hanging cables alongside cosh, and mirror trig identities with occasional sign flips.\n\n" + + "You already met the catenary’s cosh; sinh is its odd, rise‑from‑zero partner.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.22f, 0.48f, 0.28f, 0.35f), + gridOutside: new Color(0.16f, 0.34f, 0.2f, 0.1f), + storyPauseSecondsOverride: 2.5f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[22], + FunctionType.ExponentialDecay, + curveColor: new Color(0.5f, 0.78f, 1f, 1f), + derivativeColor: new Color(1f, 0.65f, 0.25f, 1f), + transA: 1.15f, + transK: 0.095f, + transC: -2.25f, + transD: 0f, + power: 2, + baseN: 2, + story: + "AP Physics C (calculus‑based) — exponential decay: charge on a discharging capacitor, current in an RL loop, or any quantity Q(t) with dQ/dt ∝ −Q.\n\n" + + "Solution: Q = Q₀ e^{−t/τ}; τ (time constant) sets how fast the tail relaxes — the same picture as “half‑life” thinking.\n\n" + + "The graph is a one‑sided bump; the derivative carries the sign of “still leaking toward zero.”", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.15f, 0.35f, 0.5f, 0.36f), + gridOutside: new Color(0.12f, 0.25f, 0.36f, 0.1f), + storyPauseSecondsOverride: 2.55f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[23], + FunctionType.Cosine, + curveColor: new Color(0.65f, 0.85f, 1f, 1f), + derivativeColor: new Color(1f, 0.55f, 0.85f, 1f), + transA: 0.92f, + transK: 0.52f, + transC: -2f, + transD: 0f, + power: 2, + baseN: 2, + story: + "Physics C — rotation & angular momentum. For rigid spin about a fixed axis, L = I ω (angular momentum = moment of inertia × angular speed).\n\n" + + "Net torque τ = dL/dt (like F = dp for linear motion). Small oscillations of many rotational systems look sinusoidal — the same graph as linear SHM, now dressed as θ(t) or ω(t).\n\n" + + "Energy sloshes between kinetic ½Iω² and restoring “spring” terms in ϕ — walk the cosine as a rotation story.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.24f, 0.36f, 0.55f, 0.35f), + gridOutside: new Color(0.18f, 0.26f, 0.4f, 0.1f), + storyPauseSecondsOverride: 2.7f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[24], + FunctionType.Power, + curveColor: new Color(0.95f, 0.82f, 0.35f, 1f), + derivativeColor: new Color(0.5f, 0.75f, 1f, 1f), + transA: -0.26f, + transK: 0.36f, + transC: 7.7f, + transD: 0f, + power: 2, + baseN: 2, + story: + "Projectile height vs time (constant g): y(t) = y₀ + v₀ t − ½ g t² — a downward parabola in t for vertical motion.\n\n" + + "Derivatives give vertical velocity, then acceleration −g: the Physics C calculus trilogy x, v, a shows up in every kinematics sprint.\n\n" + + "Peak is where velocity (derivative) crosses zero — a free optimization problem.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.48f, 0.38f, 0.2f, 0.34f), + gridOutside: new Color(0.34f, 0.26f, 0.14f, 0.1f), + storyPauseSecondsOverride: 2.45f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[25], + FunctionType.MaclaurinCosSeries, + curveColor: new Color(0.55f, 0.92f, 1f, 1f), + derivativeColor: new Color(1f, 0.45f, 0.65f, 1f), + transA: 0.5f, + transK: 0.48f, + transC: -2f, + transD: 0f, + power: 8, + baseN: 2, + story: + "Maclaurin for cos(x) uses even powers alternating signs: 1 − x²/2! + x⁴/4! − … — the partner series to sine’s odd powers.\n\n" + + "On the AP exam you estimate errors with Taylor remainders and reason about radius of convergence — here you get to see the polynomial hug the true cosine near 0.\n\n" + + "More terms ⇢ wider trustworthy fit; the derivative polynomials track −sin x in spirit.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.2f, 0.42f, 0.55f, 0.35f), + gridOutside: new Color(0.14f, 0.3f, 0.42f, 0.1f), + storyPauseSecondsOverride: 2.45f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[26], + FunctionType.NaturalLog, + curveColor: new Color(0.65f, 0.95f, 0.55f, 1f), + derivativeColor: new Color(0.85f, 0.45f, 1f, 1f), + transA: 0.48f, + transK: 0.15f, + transC: -2.1f, + transD: -25f, + power: 2, + baseN: 2, + story: + "Natural logarithm is the star of ∫ (1/x) dx = ln|x| + C and shows up in p‑growth comparisons, half‑lives, and ε–δ arguments about slow divergence.\n\n" + + "Domain x > 0 (here ensured by shifting the graph so u stays positive): slopes are always positive but shrink as x grows — classic “diminishing returns.”\n\n" + + "BC links ln x to harmonic series / integral test intuitions.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.28f, 0.45f, 0.24f, 0.35f), + gridOutside: new Color(0.2f, 0.32f, 0.18f, 0.1f), + storyPauseSecondsOverride: 2.35f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[27], + FunctionType.SquareRoot, + curveColor: new Color(0.95f, 0.6f, 0.4f, 1f), + derivativeColor: new Color(0.45f, 0.65f, 1f, 1f), + transA: 0.55f, + transK: 0.14f, + transC: -2.15f, + transD: -20f, + power: 2, + baseN: 2, + story: + "√x — domain restriction hero. d/dx √x = 1/(2√x) blows up approaching 0 from the right: infinite slope at the vertical tangent place (a classic BC “improper behavior” discussion).\n\n" + + "Substitution integrals and arc length formulas love √(1 + (dy/dx)²); the cusp language matches “watch the derivative.”\n\n" + + "Gameplay still samples smooth pieces; the story is the analytic caution at the boundary.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.48f, 0.3f, 0.18f, 0.35f), + gridOutside: new Color(0.34f, 0.22f, 0.12f, 0.1f), + storyPauseSecondsOverride: 2.35f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[28], + FunctionType.Tangent, + curveColor: new Color(0.6f, 0.85f, 1f, 1f), + derivativeColor: new Color(1f, 0.4f, 0.35f, 1f), + transA: 0.42f, + transK: 0.048f, + transC: -2f, + transD: 0f, + power: 2, + baseN: 2, + story: + "Tangent packs vertical asymptotes where cos → 0 — limits sprint material on every AP sheet.\n\n" + + "Here the window is chosen so you explore a single smooth branch between asymptotes: sec²x is the derivative, always ≥ 1 when defined.\n\n" + + "BC parametric/polar work often reduces to chasing trig identities; tan is a spine in those algebra stories.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.2f, 0.38f, 0.52f, 0.35f), + gridOutside: new Color(0.14f, 0.28f, 0.38f, 0.1f), + storyPauseSecondsOverride: 2.3f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[29], + FunctionType.NaturalExp, + curveColor: new Color(0.4f, 1f, 0.75f, 1f), + derivativeColor: new Color(1f, 0.55f, 0.45f, 1f), + transA: 0.2f, + transK: 0.065f, + transC: -2.05f, + transD: 0f, + power: 2, + baseN: 2, + story: + "Exponential growth ODE: if y′ = k y then y = Ce^{kx} — the reason e^x is “its own derivative” up to scaling.\n\n" + + "Separable equations, slope fields, and half‑life problems all orbit this curve before you meet logistic saturation next door.\n\n" + + "Contrast with the ∫ e^x dx level earlier: there we shaded area; here we emphasize rate proportional to amount.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.18f, 0.42f, 0.3f, 0.35f), + gridOutside: new Color(0.13f, 0.3f, 0.22f, 0.1f), + storyPauseSecondsOverride: 2.35f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[30], + FunctionType.Sine, + curveColor: new Color(0.82f, 0.55f, 1f, 1f), + derivativeColor: new Color(0.45f, 0.95f, 0.75f, 1f), + transA: 0.95f, + transK: 0.48f, + transC: -2f, + transD: 0.35f, + power: 2, + baseN: 2, + story: + "Phase & SHM. sin(ωt + ϕ) is the same motion as cosine, just time‑shifted — energy swaps between kinetic and potential in an ideal spring.\n\n" + + "Parametric circles (x = R cos t, y = R sin t) project to these components; BC’s vector‑valued motion unit leans on the same trig backbone.\n\n" + + "Derivative cos tracks velocity up to constants: the platform logic is “who leads, who lags?”", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.38f, 0.22f, 0.5f, 0.35f), + gridOutside: new Color(0.28f, 0.16f, 0.36f, 0.1f), + storyPauseSecondsOverride: 2.4f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[31], + FunctionType.Power, + curveColor: new Color(0.95f, 0.5f, 0.45f, 1f), + derivativeColor: new Color(0.55f, 0.55f, 1f, 1f), + transA: 0.055f, + transK: 0.38f, + transC: -1.88f, + transD: 0f, + power: 3, + baseN: 2, + story: + "Cubic graph sketching — a BC classroom ritual: find critical points, inflection where y″ flips sign, end behavior ±∞.\n\n" + + "Inflection is where curvature changes; the derivative has a local max/min there for smooth cubics.\n\n" + + "Your feet feel one hump + one valley pattern typical of monotone‑derivative pieces between flexes.", + derivativePopTriggerCountOverride: 4, + applyGridTheming: true, + gridCenter: new Color(0.5f, 0.24f, 0.2f, 0.34f), + gridOutside: new Color(0.36f, 0.17f, 0.15f, 0.1f), + storyPauseSecondsOverride: 2.35f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[32], + FunctionType.Exponential, + curveColor: new Color(0.85f, 0.75f, 1f, 1f), + derivativeColor: new Color(1f, 0.6f, 0.35f, 1f), + transA: 0.32f, + transK: 0.088f, + transC: -2.05f, + transD: 0f, + power: 2, + baseN: 2, + story: + "General exponential b^x has derivative proportional to itself: d/dx b^x = (ln b) b^x.\n\n" + + "That constant ln b is the bridge from base‑10 or base‑2 growth to the natural base e where the constant becomes 1.\n\n" + + "Pair mentally with the Maclaurin and logistic levels — three lenses on “growth language.”", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.4f, 0.32f, 0.55f, 0.35f), + gridOutside: new Color(0.28f, 0.22f, 0.4f, 0.1f), + storyPauseSecondsOverride: 2.3f + )); + + levels.Add(MakeLevel( + GameLevelCatalog.DisplayNames[33], + FunctionType.CircleUpper, + curveColor: new Color(0.55f, 0.82f, 1f, 1f), + derivativeColor: new Color(1f, 0.65f, 0.45f, 1f), + transA: 2.65f, + transK: 1f, + transC: -2.15f, + transD: 0f, + power: 2, + baseN: 2, + story: + "Circle equation in standard form: (x − h)² + (y − k)² = R². A full circle is not a single y = f(x) graph — it fails the vertical line test — so we walk the upper semicircle:\n\n" + + "y = k + √(R² − (x − h)²) on |x − h| ≤ R. Implicit differentiation on the circle gives dy/dx = −(x − h)/(y − k) (away from y = k on the full curve).\n\n" + + "Parametric form x = h + R cos t, y = k + R sin t is another AP favorite; this stage keeps you on the top arc.", + derivativePopTriggerCountOverride: 3, + applyGridTheming: true, + gridCenter: new Color(0.22f, 0.36f, 0.52f, 0.36f), + gridOutside: new Color(0.16f, 0.26f, 0.38f, 0.1f), + storyPauseSecondsOverride: 2.65f + )); } + /// + /// Factory for a runtime ScriptableObject instance (not saved as an asset). + /// Optional Riemann args apply integral / stair visualization stages. + /// private LevelDefinition MakeLevel( string name, FunctionType functionType, @@ -618,7 +1337,12 @@ private LevelDefinition MakeLevel( Color gridCenter = default, Color gridOutside = default, Color[] levelStageColors = null, - float storyPauseSecondsOverride = 0f) + float storyPauseSecondsOverride = 0f, + RiemannRule riemannRule = RiemannRule.None, + int riemannRectCount = 18, + bool showRiemannVisualization = false, + bool useRiemannStairPlatforms = false, + Color? riemannFillColor = null) { var def = ScriptableObject.CreateInstance(); def.levelName = name; @@ -671,9 +1395,18 @@ private LevelDefinition MakeLevel( } def.storyText = story; + + def.riemannRule = riemannRule; + def.riemannRectCount = riemannRectCount; + def.showRiemannVisualization = showRiemannVisualization; + def.useRiemannStairPlatforms = useRiemannStairPlatforms; + if (riemannFillColor.HasValue) + def.riemannFillColor = riemannFillColor.Value; + return def; } + /// Loads a level by index, applies theme, fades story, rebuilds collision world. private void LoadLevel(int index) { if (levels.Count == 0 || functionPlotter == null) @@ -690,6 +1423,10 @@ private void LoadLevel(int index) StartCoroutine(LoadWorldAfterThemeChange(def)); } + /// + /// Copies into FunctionPlotter + line colors + grid theme + stage triggers + story text. + /// Does not rebuild physics platforms (see ). + /// private void ApplyLevelTheme(LevelDefinition def) { functionPlotter.functionType = def.functionType; @@ -706,6 +1443,18 @@ private void ApplyLevelTheme(LevelDefinition def) functionPlotter.baseN = def.baseN; functionPlotter.differentiate = true; + if (def.showRiemannVisualization) + { + if (def.riemannRule == RiemannRule.None) + functionPlotter.SetEquationExtraSuffix($"Area ≈ Σ f(x*) Δx, n={def.riemannRectCount} (Δx=(b−a)/n)"); + else + functionPlotter.SetEquationExtraSuffix($"Riemann {def.riemannRule}: n={def.riemannRectCount}"); + } + else if (def.useRiemannStairPlatforms && def.riemannRule != RiemannRule.None) + functionPlotter.SetEquationExtraSuffix($"Stairs: {def.riemannRule} rule, n={def.riemannRectCount}"); + else + functionPlotter.SetEquationExtraSuffix(""); + curveRenderer.color = def.curveColor; derivRenderer.color = def.derivativeColor; @@ -766,6 +1515,10 @@ private void ApplyLevelTheme(LevelDefinition def) } } + /// + /// After theme swap, FunctionPlotter.Update repopulates points; we defer one frame so + /// / derivative lists are current before sampling columns. + /// private IEnumerator LoadWorldAfterThemeChange(LevelDefinition def) { // Wait for the plot to regenerate points with the new parameters. @@ -781,10 +1534,14 @@ private IEnumerator LoadWorldAfterThemeChange(LevelDefinition def) var unitHeight = cartesianPlaneRect.rect.height / (float)gridSize.y; obstacleGenerator.SetLayout(obstaclesRoot, gridSize, unitWidth, unitHeight); + EnsureRiemannRenderer(); + if (riemannRenderer != null) + riemannRenderer.Rebuild(def, functionPlotter); + var curvePoints = curveRenderer.points; var derivPoints = derivRenderer.points; - var world = obstacleGenerator.GenerateWorld(def, curvePoints, derivPoints); + var world = obstacleGenerator.GenerateWorld(def, curvePoints, derivPoints, functionPlotter); playerController.SetWorld(world); playerController.ResetToSpawn(world); } @@ -819,6 +1576,7 @@ private IEnumerator FadeStoryTextRoutine() } } + /// HUD refresh + derivative “pop” triggers based on player X in grid space. private void Update() { if (playerController == null || stageTriggerXGrid == null) diff --git a/First Principles/Assets/Scripts/Game/LevelSelectController.cs b/First Principles/Assets/Scripts/Game/LevelSelectController.cs index e35b2eb..e444f78 100644 --- a/First Principles/Assets/Scripts/Game/LevelSelectController.cs +++ b/First Principles/Assets/Scripts/Game/LevelSelectController.cs @@ -4,8 +4,15 @@ using UnityEngine.UI; using TMPro; +// ----------------------------------------------------------------------------- +// LevelSelectController — all UI for the LevelSelect scene is code-generated +// ----------------------------------------------------------------------------- +// Parents to MobileUiRoots safe rect. Scrolls if GameLevelCatalog.LevelCount grows. +// “Math tips” opens MathArticlesOverlay on the same Canvas. +// ----------------------------------------------------------------------------- + /// -/// Builds a simple Limbo-style level list UI at runtime and loads Game with . +/// Builds Limbo-style level list UI at runtime and loads Game with . /// public class LevelSelectController : MonoBehaviour { @@ -21,9 +28,10 @@ private void Start() return; } + var safeParent = MobileUiRoots.GetSafeContentParent(canvas.transform); var panel = new GameObject("LevelSelectPanel"); var prt = panel.AddComponent(); - prt.SetParent(canvas.transform, false); + prt.SetParent(safeParent != null ? safeParent : canvas.transform, false); prt.anchorMin = Vector2.zero; prt.anchorMax = Vector2.one; prt.offsetMin = Vector2.zero; @@ -36,46 +44,143 @@ private void Start() var titleGo = new GameObject("Title"); var titleRt = titleGo.AddComponent(); titleRt.SetParent(panel.transform, false); - titleRt.anchorMin = new Vector2(0.5f, 0.88f); - titleRt.anchorMax = new Vector2(0.5f, 0.88f); + bool tablet = DeviceLayout.IsTabletLike(); + titleRt.anchorMin = new Vector2(0.5f, tablet ? 0.91f : 0.9f); + titleRt.anchorMax = new Vector2(0.5f, tablet ? 0.91f : 0.9f); titleRt.pivot = new Vector2(0.5f, 0.5f); - titleRt.sizeDelta = new Vector2(900f, 90f); + titleRt.sizeDelta = new Vector2(tablet ? 1020f : 960f, tablet ? 96f : 88f); titleRt.anchoredPosition = Vector2.zero; var titleTmp = titleGo.AddComponent(); titleTmp.text = "Choose a graph stage"; - titleTmp.fontSize = 44; + titleTmp.fontSize = tablet ? 46 : 40; titleTmp.alignment = TextAlignmentOptions.Center; titleTmp.color = Color.white; CopyFontFromAny(titleTmp); - var listGo = new GameObject("LevelList"); - var listRt = listGo.AddComponent(); - listRt.SetParent(panel.transform, false); - listRt.anchorMin = new Vector2(0.5f, 0.5f); - listRt.anchorMax = new Vector2(0.5f, 0.5f); - listRt.pivot = new Vector2(0.5f, 0.5f); - listRt.sizeDelta = new Vector2(560f, 420f); - listRt.anchoredPosition = new Vector2(0f, -20f); + CreateMathArticlesButton(panel.transform, canvas.transform); + + // Scrollable list (many levels + readable on small screens). + var scrollGo = new GameObject("LevelScroll"); + var scrollRt = scrollGo.AddComponent(); + scrollRt.SetParent(panel.transform, false); + scrollRt.anchorMin = DeviceLayout.LevelSelectScrollAnchorMin; + scrollRt.anchorMax = DeviceLayout.LevelSelectScrollAnchorMax; + scrollRt.offsetMin = Vector2.zero; + scrollRt.offsetMax = Vector2.zero; + + var scroll = scrollGo.AddComponent(); + scroll.horizontal = false; + scroll.vertical = true; + scroll.movementType = ScrollRect.MovementType.Clamped; + scroll.scrollSensitivity = DeviceLayout.LevelSelectScrollSensitivity; + + var viewportGo = new GameObject("Viewport"); + var viewportRt = viewportGo.AddComponent(); + viewportRt.SetParent(scrollGo.transform, false); + viewportRt.anchorMin = Vector2.zero; + viewportRt.anchorMax = Vector2.one; + viewportRt.offsetMin = Vector2.zero; + viewportRt.offsetMax = Vector2.zero; + var vImg = viewportGo.AddComponent(); + vImg.color = new Color(0f, 0f, 0f, 0f); + var mask = viewportGo.AddComponent(); + mask.showMaskGraphic = false; + scroll.viewport = viewportRt; + + var contentGo = new GameObject("Content"); + var contentRt = contentGo.AddComponent(); + contentRt.SetParent(viewportGo.transform, false); + contentRt.anchorMin = new Vector2(0f, 1f); + contentRt.anchorMax = new Vector2(1f, 1f); + contentRt.pivot = new Vector2(0.5f, 1f); + contentRt.anchoredPosition = Vector2.zero; + contentRt.sizeDelta = new Vector2(0f, 0f); - var vlg = listGo.AddComponent(); + scroll.content = contentRt; + + var vlg = contentGo.AddComponent(); vlg.childAlignment = TextAnchor.UpperCenter; - vlg.spacing = 18f; - vlg.padding = new RectOffset(8, 8, 8, 8); + vlg.spacing = tablet ? 16f : 14f; + vlg.padding = new RectOffset(tablet ? 14 : 10, tablet ? 14 : 10, tablet ? 12 : 10, tablet ? 12 : 10); vlg.childControlHeight = true; vlg.childControlWidth = true; vlg.childForceExpandHeight = false; vlg.childForceExpandWidth = true; + var fitter = contentGo.AddComponent(); + fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained; + fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + for (int i = 0; i < GameLevelCatalog.LevelCount; i++) { int idx = i; - CreateLevelButton(listRt, GameLevelCatalog.DisplayNames[i], () => StartGameAt(idx)); + CreateLevelButton(contentRt, GameLevelCatalog.DisplayNames[i], () => StartGameAt(idx)); } + CreateSceneCreditsFooter(panel.transform); CreateBackButton(panel.transform); } + private void CreateSceneCreditsFooter(Transform panelRoot) + { + var go = new GameObject("SceneCreditsFooter"); + var rt = go.AddComponent(); + rt.SetParent(panelRoot, false); + bool tablet = DeviceLayout.IsTabletLike(); + rt.anchorMin = new Vector2(0.5f, 0f); + rt.anchorMax = new Vector2(0.5f, 0f); + rt.pivot = new Vector2(0.5f, 0f); + rt.sizeDelta = new Vector2(tablet ? 980f : 920f, tablet ? 76f : 72f); + rt.anchoredPosition = new Vector2(0f, tablet ? 118f : 108f); + + var tmp = go.AddComponent(); + tmp.text = SceneCreditsFooter.BuildCompactRichText(); + tmp.richText = true; + tmp.enableWordWrapping = true; + tmp.fontSize = tablet ? 17 : 15; + tmp.alignment = TextAlignmentOptions.Bottom; + tmp.color = Color.white; + tmp.raycastTarget = false; + CopyFontFromAny(tmp); + } + + private void CreateMathArticlesButton(Transform panelRoot, Transform canvasTransform) + { + var go = new GameObject("MathArticlesButton"); + var rt = go.AddComponent(); + rt.SetParent(panelRoot, false); + bool tablet = DeviceLayout.IsTabletLike(); + rt.anchorMin = new Vector2(0.5f, tablet ? 0.81f : 0.8f); + rt.anchorMax = new Vector2(0.5f, tablet ? 0.81f : 0.8f); + rt.pivot = new Vector2(0.5f, 0.5f); + rt.sizeDelta = new Vector2(tablet ? 580f : 520f, tablet ? 62f : 56f); + rt.anchoredPosition = Vector2.zero; + + var img = go.AddComponent(); + img.color = new Color(0.18f, 0.34f, 0.42f, 1f); + + var btn = go.AddComponent