Code Sketch


YADNESH
By: Mhalsakant School
Category: Art
// --- ISRO ULTIMATE 3D ANIMATED MISSION: SHARP ROCKET & 3D PLANETS ---
cleari()
originBottomLeft()

// ==================== FULL JANA GANA MANA TUNE SETUP ====================
setNoteInstrument(Instrument.PIANO)
val fullJanaGanaTune = Array(
    (53, 200), (55, 200), (57, 200), (57, 200), (57, 200), (57, 200), (57, 200), 
    (57, 200), (57, 200), (55, 200), (57, 200), (58, 200),
    (57, 200), (57, 200), (57, 200), (55, 200), (55, 200), (55, 200), (53, 200), (55, 200), (53, 300),
    (55, 200), (57, 200), (55, 200), (53, 300),
    (53, 200), (60, 200), (60, 200), (60, 200), (60, 200), (60, 200), (60, 200),
    (60, 200), (58, 200), (60, 200), (58, 200), (57, 200), (55, 200), (57, 200), (58, 300),
    (57, 200), (57, 200), (57, 200), (57, 200), (55, 200), (58, 200), (57, 200),
    (55, 200), (55, 200), (53, 200), (55, 200), (53, 300),
    (53, 200), (55, 200), (57, 200), (57, 200), (57, 200), (55, 200), (57, 200), (58, 300),
    (57, 200), (55, 200), (57, 200), (53, 200), (55, 200), (53, 300),
    (53, 200), (55, 200), (57, 200), (57, 200), (57, 200), (57, 200), (57, 200),
    (55, 200), (57, 200), (58, 200), (57, 200), (55, 200), (53, 300),
    (60, 250), (58, 250), (60, 300),
    (58, 250), (57, 250), (58, 300),
    (57, 250), (55, 250), (57, 300),
    (53, 200), (53, 200), (55, 200), (55, 200), (57, 200), (57, 200), (58, 400)
)
var tuneIndex = 0
var anthemFinished = false
var finalCreditsTimer = 0.0

val patrioticInfoText = 
    "?? BHARAT MATA KI JAI! ??\n" +
    "Jana Gana Mana Adhinayaka Jaya He...\n" +
    "Bharata Bhagya Vidhata!"
var charIndex = 0

def playJanaGanaStep() {
    if (tuneIndex < fullJanaGanaTune.length) {
        val (note, duration) = fullJanaGanaTune(tuneIndex)
        playNote(note, duration)
        tuneIndex += 1
        if (tuneIndex >= fullJanaGanaTune.length) {
            anthemFinished = true
        }
    }
}

// ==================== PLANET DETAILS & 3D COLORS ====================
case class PlanetDetail(name: String, surfaceCol: Color, darkSurfCol: Color, accent3D: Color, infoLine1: String, dayNightStatus: String, gravityVal: String, alienHumanStatus: String, atmosphere: String, coreTemp: String, funFact: String, quizQuestion: String, quizOptions: Array[String], correctOpt: Int)

val planetDetailsMap = Array(
    PlanetDetail("Sun", cm.rgb(255, 140, 0), cm.rgb(120, 60, 0), cm.rgb(255, 220, 100), "Solar Plasma Surface", "Day: Perpetual Bright", "274.0 m/s� (27.9g)", "Aliens: No | Humans: No", "Hydrogen & Helium", "15 Million �C", "Generates energy via nuclear fusion.", "What is the core temperature of Sun?", Array("A) 1 million �C", "B) 15 million �C", "C) 100,000 �C"), 1),
    PlanetDetail("Mercury", cm.rgb(120, 120, 120), cm.rgb(60, 60, 60), cm.rgb(200, 200, 200), "Rocky Cratered Surface", "Day/Night: 58 Earth Days", "3.7 m/s� (0.38g)", "Aliens: No | Humans: No", "Minimal (Oxygen, Sodium)", "430�C / -180�C", "Closest planet to the Sun.", "Does Mercury have any moons?", Array("A) Yes (2 moons)", "B) No moons", "C) Yes (1 moon)"), 1),
    PlanetDetail("Venus", cm.rgb(210, 150, 80), cm.rgb(100, 70, 30), cm.rgb(255, 200, 130), "Volcanic Sulfuric Plains", "Day/Night: 243 Earth Days", "8.87 m/s� (0.90g)", "Aliens: No | Humans: No", "Carbon Dioxide", "462�C (Hottest)", "Hottest planet in solar system.", "Which gas makes Venus extremely hot?", Array("A) Oxygen", "B) Carbon Dioxide", "C) Nitrogen"), 1),
    PlanetDetail("Earth", cm.rgb(40, 130, 240), cm.rgb(20, 60, 120), cm.rgb(100, 200, 255), "Blue Terrestrial Crust", "Day & Night (24 Hours)", "9.8 m/s� (1.0g)", "Aliens: Unproven | Humans: Yes", "Nitrogen, Oxygen", "5,500�C", "Only planet supporting life.", "What percentage of Earth's surface is water?", Array("A) 50%", "B) 71%", "C) 90%"), 1),
    PlanetDetail("Mars", cm.rgb(200, 70, 40), cm.rgb(100, 30, 15), cm.rgb(255, 130, 100), "Red Dusty Iron Oxide Desert", "Day/Night: 24h 39m (Sol)", "3.72 m/s� (0.38g)", "Aliens: Microbial (Suspected)", "Carbon Dioxide", "-60�C Average", "Home to Olympus Mons volcano.", "Why does Mars appear red?", Array("A) Iron Oxide (Rust)", "B) Red Sandstorms", "C) Lava oceans"), 0),
    PlanetDetail("Jupiter", cm.rgb(210, 140, 90), cm.rgb(100, 60, 40), cm.rgb(255, 190, 140), "Gaseous Stormy Surface", "Day/Night: 10 Hours", "24.79 m/s� (2.53g)", "Aliens: No | Humans: No", "Hydrogen & Helium", "24,000�C", "Largest planet with Great Red Spot.", "What is Jupiter's Great Red Spot?", Array("A) A giant volcano", "B) A massive storm", "C) A crater"), 1),
    PlanetDetail("Saturn", cm.rgb(190, 170, 110), cm.rgb(90, 80, 50), cm.rgb(240, 230, 160), "Icy Ringed Gas Plains", "Day/Night: 10.7 Hours", "10.44 m/s� (1.07g)", "Aliens: No | Humans: No", "Hydrogen & Helium", "11,700�C", "Famous for complex ring system.", "What are Saturn's rings primarily made of?", Array("A) Solid rock slabs", "B) Ice and dust particles", "C) Burning gas"), 1),
    PlanetDetail("Uranus", cm.rgb(100, 200, 220), cm.rgb(40, 90, 100), cm.rgb(180, 240, 255), "Cold Methane Ice Crust", "Day/Night: Extreme Tilt", "8.69 m/s� (0.89g)", "Aliens: No | Humans: No", "Hydrogen, Helium, Methane", "-197�C", "Rotates completely on its side.", "How does Uranus rotate on its axis?", Array("A) Normally", "B) Completely on its side", "C) Backwards"), 1),
    PlanetDetail("Neptune", cm.rgb(50, 90, 220), cm.rgb(20, 40, 100), cm.rgb(130, 170, 255), "Supersonic Stormy Ice Crust", "Day/Night: 16 Hours", "11.15 m/s� (1.14g)", "Aliens: No | Humans: No", "Hydrogen, Helium, Methane", "-201�C", "Windiest planet with supersonic winds.", "What gives Neptune its blue appearance?", Array("A) Deep oceans", "B) Methane in atmosphere", "C) Blue rocks"), 1),
    PlanetDetail("Maha-Prithvi", cm.rgb(30, 190, 130), cm.rgb(15, 90, 60), cm.rgb(120, 255, 190), "Super-Earth Habitable Crust", "Day/Night: ~290 Days Orbit", "14.2 m/s� (1.45g)", "Aliens: Unknown | Humans: Future", "Oxygen rich", "Unknown", "A promising exoplanet candidate.", "Is Maha-Prithvi a real discovery?", Array("A) Yes (Kepler-22b)", "B) No, purely fictional", "C) Mars moon"), 0)
)

// ==================== STARS & SOLAR SYSTEM SETUP ====================
val starCount = 300
val starX = Array.fill(starCount)(randomDouble(0, cwidth))
val starY = Array.fill(starCount)(randomDouble(0, cheight))
val starSize = Array.fill(starCount)(randomDouble(1.0, 3.5))
var waveAngle = 0.0
var chakraRotation = 0.0
var neonGlowPhase = 0.0
var globalTime = 0.0

val planetList = Array("Sun", "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Maha-Prithvi")
var selectedPlanet = "Mars"
var earthNotificationTimer = 0.0

// GAME & MISSION STATES
var isTimerRunning = false
var countdownTimer = 10.0
var isLaunched = false
var isLanded = false

// TOP SURFACE LANDING & INFO STATES
var rocketSurfaceY = 160.0 
var doorOpeningProgress = 0.0
var astronautOut = false
var infoPanelShown = false
var infoPageNumber = 1

// QUIZ STATES
var quizActive = false
var quizFeedback = ""

// MINI GAME STATES
var activeMiniGameType = 0
var isMiniGameActive = false
var readyToReturnEarth = false
var gameCountdownTimer = 15.0
var isGameTimerRunning = false

// THANK YOU & RETURN STATES
var thankYouShown = false
var thankYouTimer = 4.0

var isReturnTimerRunning = false
var returnCountdownTimer = 5.0
var isReturnLaunched = false
var isTeleportedToEarth = false
var startAnthemPhase = false

// MINI-GAME PLAYER & OBJECT PHYSICS
var miniPlayerX = (cwidth / 2.0) - 100.0
var miniPlayerY = 220.0
var miniPlayerVy = 0.0
var modifierGrounded = true
var isKeyPotionCollected = false
var isDoorUnlocked = false

var starsCaughtCount = 0
var targetStarsToCatch = 5
var fallingStarX = Array.fill(5)(randomDouble(120, cwidth - 120))
var fallingStarY = Array.fill(5)(randomDouble(150, cheight - 80))

var asteroidX = Array.fill(4)(cwidth + randomDouble(50, 150))
var asteroidY = Array.fill(4)(randomDouble(150, cheight - 80))

val tricolorPalette = Array(cm.rgb(255, 153, 51), cm.rgb(255, 255, 255), cm.rgb(18, 136, 7))

// ==================== FIREWORKS SYSTEM ====================
class Particle(x0: Double, y0: Double, col: Color, seed: Boolean) {
    var location = Vector2D(x0, y0)
    private var velocity = if (seed) Vector2D(0, randomDouble(6, 12)) else randomExplosionVector
    private var acceleration = Vector2D(0, 0)
    var lifespan = 255.0
    private var exploded = false

    def randomExplosionVector: Vector2D = {
        val rv1 = Vector2D(randomNormalDouble, randomNormalDouble)
        val r1 = randomDouble(2, 5)
        rv1 * r1
    }

    def applyForce(force: Vector2D) { acceleration += force }
    def shouldExplode: Boolean = !exploded && lifespan <= 0
    def isDead: Boolean = if (seed) exploded else lifespan <= 0

    def explode() {
        exploded = true
        if (startAnthemPhase && !anthemFinished) {
            playJanaGanaStep()
            if (charIndex < patrioticInfoText.length) {
                charIndex = math.min(charIndex + 4, patrioticInfoText.length)
            }
        }
    }

    def checkExplode() { if (seed && velocity.y < 0) lifespan = 0 }

    def step() {
        velocity += acceleration
        location += velocity
        if (!seed) {
            lifespan -= 5
            velocity *= 0.95
        }
        acceleration *= 0
        checkExplode()
    }

    def view(canvas: CanvasDraw) {
        canvas.stroke(col)
        if (seed) canvas.strokeWeight(5) else canvas.strokeWeight(2.5)
        canvas.point(location.x, location.y)
    }
}

class Firework() {
    private val fireworkColor = tricolorPalette(random(tricolorPalette.length))
    private val firework = new Particle(randomDouble(cwidth), cheight, fireworkColor, true)
    private val particles = ArrayBuffer.empty[Particle]

    def done: Boolean = firework.isDead && particles.isEmpty

    def step() {
        particles.filterInPlace(p => !p.isDead)
        if (!firework.isDead) {
            firework.applyForce(Vector2D(0, 0.08))
            firework.step()
            if (firework.shouldExplode) {
                repeat(90) {
                    particles.append(new Particle(firework.location.x, firework.location.y, fireworkColor, false))
                }
                firework.explode()
            }
        }
        repeatFor(particles) { p =>
            p.applyForce(Vector2D(0, 0.08))
            p.step()
        }
    }

    def view(canvas: CanvasDraw) {
        firework.view(canvas)
        repeatFor(particles) { p => p.view(canvas) }
    }
}

val fireworks = ArrayBuffer.empty[Firework]

// ==================== UPDATE LOGIC ====================
def updateState() {
    fireworks.filterInPlace(f => !f.done)
    if (startAnthemPhase && !anthemFinished) {
        if (randomDouble(1) < 0.1) fireworks.append(new Firework())
    } else if (anthemFinished) {
        finalCreditsTimer += 0.04
    }
    repeatFor(fireworks) { f => f.step() }

    waveAngle += 0.1
    chakraRotation += 0.08
    neonGlowPhase += 0.08
    globalTime += 0.025

    if (earthNotificationTimer > 0) earthNotificationTimer -= 0.04
    if (thankYouTimer > 0 && thankYouShown) {
        thankYouTimer -= 0.04
        if (thankYouTimer <= 0) {
            thankYouShown = false
            astronautOut = false 
            readyToReturnEarth = true 
        }
    }

    if (isTimerRunning && countdownTimer > 0) {
        countdownTimer -= 0.04
        if (countdownTimer <= 0) {
            countdownTimer = 0.0
            isTimerRunning = false
            quizActive = true 
        }
    }

    if (isLaunched && !isLanded) {
        rocketSurfaceY += 4.5
        val targetLandingY = 150.0
        if (rocketSurfaceY >= targetLandingY) {
            rocketSurfaceY = targetLandingY
            isLanded = true
        }
    }

    if (isLanded && !isReturnLaunched) {
        if (!thankYouShown && !readyToReturnEarth && !infoPanelShown) {
            if (doorOpeningProgress < 1.0) {
                doorOpeningProgress += 0.02
            } else {
                astronautOut = true
                infoPanelShown = true
            }
        }
    }

    if (isGameTimerRunning && gameCountdownTimer > 0 && !thankYouShown && !readyToReturnEarth) {
        gameCountdownTimer -= 0.04
        if (gameCountdownTimer <= 0) {
            gameCountdownTimer = 15.0
            isGameTimerRunning = false
            isMiniGameActive = false
            infoPanelShown = true
            isKeyPotionCollected = false
            isDoorUnlocked = false
            starsCaughtCount = 0
        }
    }

    // MINI-GAME 1
    if (isMiniGameActive && activeMiniGameType == 0 && !thankYouShown) {
        val gx = cwidth / 2.0 - 220.0
        val gy = cheight / 2.0 - 100.0
        val speed = 4.0
        
        if (isKeyPressed(Kc.VK_RIGHT) && miniPlayerX < gx + 410) miniPlayerX += speed
        if (isKeyPressed(Kc.VK_LEFT) && miniPlayerX > gx + 15) miniPlayerX -= speed
        if (isKeyPressed(Kc.VK_UP) && modifierGrounded) {
            miniPlayerVy = -8.0
            modifierGrounded = false
        }

        miniPlayerY -= miniPlayerVy
        if (!modifierGrounded) miniPlayerVy += 0.4 

        val floorLevel = gy + 175.0
        if (miniPlayerY >= floorLevel) {
            miniPlayerY = floorLevel
            miniPlayerVy = 0.0
            modifierGrounded = true
        } else {
            if (miniPlayerY < floorLevel) modifierGrounded = false
        }

        if (!isKeyPotionCollected && math.abs(miniPlayerX - (gx + 350)) < 25 && math.abs(miniPlayerY - (gy + 110)) < 25) {
            isKeyPotionCollected = true
            isDoorUnlocked = true
        }

        if (isDoorUnlocked && math.abs(miniPlayerX - (gx + 50)) < 30 && math.abs(miniPlayerY - (gy + 120)) < 30) {
            isMiniGameActive = false
            isGameTimerRunning = false
            thankYouShown = true
            thankYouTimer = 4.0
        }
    }

    // MINI-GAME 2
    if (isMiniGameActive && activeMiniGameType == 1 && !thankYouShown) {
        val gx = cwidth / 2.0 - 220.0
        val gy = cheight / 2.0 - 100.0
        val speed = 4.5

        if (isKeyPressed(Kc.VK_RIGHT) && miniPlayerX < gx + 410) miniPlayerX += speed
        if (isKeyPressed(Kc.VK_LEFT) && miniPlayerX > gx + 15) miniPlayerX -= speed
        if (isKeyPressed(Kc.VK_UP) && miniPlayerY > gy + 30) miniPlayerY -= speed
        if (isKeyPressed(Kc.VK_DOWN) && miniPlayerY < gy + 175) miniPlayerY += speed

        for (i <- 0 until fallingStarX.length) {
            fallingStarY(i) += 1.5
            if (fallingStarY(i) > gy + 185) {
                fallingStarY(i) = gy + 30.0
                fallingStarX(i) = gx + randomDouble(30, 380)
            }
            if (math.abs(miniPlayerX - fallingStarX(i)) < 25 && math.abs(miniPlayerY - fallingStarY(i)) < 25) {
                starsCaughtCount += 1
                fallingStarY(i) = gy + 30.0
                fallingStarX(i) = gx + randomDouble(30, 380)
            }
        }

        if (starsCaughtCount >= targetStarsToCatch) {
            isMiniGameActive = false
            isGameTimerRunning = false
            thankYouShown = true
            thankYouTimer = 4.0
        }
    }

    // MINI-GAME 3
    if (isMiniGameActive && activeMiniGameType == 2 && !thankYouShown) {
        val gx = cwidth / 2.0 - 220.0
        val gy = cheight / 2.0 - 100.0
        val speed = 4.5

        if (isKeyPressed(Kc.VK_UP) && miniPlayerY > gy + 30) miniPlayerY -= speed
        if (isKeyPressed(Kc.VK_DOWN) && miniPlayerY < gy + 175) miniPlayerY += speed

        for (i <- 0 until asteroidX.length) {
            asteroidX(i) -= 3.5
            if (asteroidX(i) < gx) {
                asteroidX(i) = gx + 430.0
                asteroidY(i) = gy + randomDouble(30, 170)
            }
        }

        if (randomDouble(1) < 0.003) {
            isMiniGameActive = false
            isGameTimerRunning = false
            thankYouShown = true
            thankYouTimer = 4.0
        }
    }

    if (isReturnTimerRunning && returnCountdownTimer > 0) {
        returnCountdownTimer -= 0.04
        if (returnCountdownTimer <= 0) {
            returnCountdownTimer = 0.0
            isReturnTimerRunning = false
            isReturnLaunched = true
        }
    }

    if (isReturnLaunched && !isTeleportedToEarth) {
        rocketSurfaceY -= 6.0
        if (rocketSurfaceY < -100.0) {
            isTeleportedToEarth = true
            startAnthemPhase = true
        }
    }
}

// MOUSE CLICK INPUT
onMouseClick { (x, y) =>
    if (x >= 15 && x <= 125 && y >= 15 && y <= 55) {
        if (!isTimerRunning && !isLaunched && !quizActive) {
            isTimerRunning = true
            countdownTimer = 10.0
        }
    } 
    else if (quizActive) {
        var qCorrect = 1
        repeatFor(planetDetailsMap) { pd =>
            if (pd.name == selectedPlanet) qCorrect = pd.correctOpt
        }
        
        val boxY = cheight / 2.0 - 10.0
        if (x >= cwidth / 2.0 - 190 && x <= cwidth / 2.0 + 190 && y >= boxY && y <= boxY + 25) {
            if (qCorrect == 0) { quizActive = false; isLaunched = true; rocketSurfaceY = -60.0 }
            else { quizFeedback = "? Wrong Answer! Try again." }
        }
        else if (x >= cwidth / 2.0 - 190 && x <= cwidth / 2.0 + 190 && y >= boxY + 30 && y <= boxY + 55) {
            if (qCorrect == 1) { quizActive = false; isLaunched = true; rocketSurfaceY = -60.0 }
            else { quizFeedback = "? Wrong Answer! Try again." }
        }
        else if (x >= cwidth / 2.0 - 190 && x <= cwidth / 2.0 + 190 && y >= boxY + 60 && y <= boxY + 85) {
            if (qCorrect == 2) { quizActive = false; isLaunched = true; rocketSurfaceY = -60.0 }
            else { quizFeedback = "? Wrong Answer! Try again." }
        }
    }
    else if (infoPanelShown && !isMiniGameActive) {
        if (x >= cwidth / 2.0 + 80 && x <= cwidth / 2.0 + 210 && y >= cheight / 2.0 + 45 && y <= cheight / 2.0 + 80) {
            if (infoPageNumber < 3) {
                infoPageNumber += 1
            } else {
                infoPanelShown = false
                astronautOut = false
                isMiniGameActive = true
                isGameTimerRunning = true
                gameCountdownTimer = 15.0
                activeMiniGameType = random(3) 
                starsCaughtCount = 0
                miniPlayerX = cwidth / 2.0 - 150.0
                miniPlayerY = cheight / 2.0 + 40.0
            }
        }
        else if (x >= cwidth / 2.0 - 210 && x <= cwidth / 2.0 - 80 && y >= cheight / 2.0 + 45 && y <= cheight / 2.0 + 80) {
            if (infoPageNumber > 1) {
                infoPageNumber -= 1
            }
        }
    }
    else if (readyToReturnEarth && !isReturnLaunched && x >= cwidth / 2.0 - 120 && x <= cwidth / 2.0 + 120 && y >= cheight - 80 && y <= cheight - 30) {
        if (!isReturnTimerRunning) {
            isReturnTimerRunning = true
            returnCountdownTimer = 5.0
        }
    }
    else if (x <= 145 && y >= cheight - 400 && y <= cheight - 20 && !isLaunched && !quizActive) {
        val index = ((cheight - 30 - y) / 25).toInt
        if (index >= 0 && index < planetList.length) {
            val chosen = planetList(index)
            if (chosen == "Earth") {
                earthNotificationTimer = 4.0
            } else {
                selectedPlanet = chosen
                infoPageNumber = 1
            }
        }
    }
}

// ==================== RENDERING UI & GRAPHICS ====================
def drawPlanetList(canvas: CanvasDraw) {
    val glowWidth = (math.sin(neonGlowPhase) * 2 + 3).toFloat

    canvas.stroke(0, 255, 255, 200)
    canvas.strokeWeight(glowWidth)
    canvas.fill(10, 15, 30, 245)
    canvas.rect(10, cheight - 405, 140, 375)

    canvas.fill(0, 255, 255)
    canvas.text("SELECT TARGET", 18, cheight - 42)

    var yPos = cheight - 65.0
    var pBaseCol = cm.rgb(255, 255, 255)
    var pAccentCol = cm.rgb(200, 200, 200)

    repeatFor(planetList) { pName =>
        if (pName == selectedPlanet) {
            if (pName == "Sun") canvas.fill(255, 50, 0) else canvas.fill(18, 136, 7)
            canvas.stroke(0, 255, 200)
            canvas.strokeWeight(1.5)
            canvas.rect(12, yPos - 12, 136, 22)
            canvas.fill(255, 255, 255)
        } else {
            if (pName == "Sun") canvas.fill(255, 100, 100) else canvas.fill(180, 220, 255)
        }
        
        val label = if (pName == "Sun") "? Sun" else if (pName == "Maha-Prithvi") "? Maha-Prithvi" else s"? $pName"
        canvas.text(label, 16, yPos)

        if (pName == selectedPlanet) {
            repeatFor(planetDetailsMap) { pd =>
                if (pd.name == pName) {
                    pBaseCol = pd.surfaceCol
                    pAccentCol = pd.accent3D
                }
            }
        }
        yPos -= 25.0
    }

    // DRAW 3D ANIMATED SPHERE LIVE PREVIEW NEXT TO MENU
    canvas.fill(10, 15, 30, 245)
    canvas.stroke(255, 215, 0)
    canvas.strokeWeight(2)
    canvas.rect(155, cheight - 210, 120, 120)

    canvas.fill(255, 255, 255)
    canvas.text("3D LIVE VIEW:", 162, cheight - 185)

    // 3D Animated Shading Sphere with rotating highlight ring
    val sphereX = 215.0
    val sphereY = cheight - 130.0
    val radius = 35.0

    canvas.fill(pBaseCol)
    canvas.stroke(pAccentCol)
    canvas.strokeWeight(2)
    canvas.ellipse(sphereX, sphereY, radius * 2, radius * 2)

    // 3D Curved Highlights (Simulating Rotation)
    canvas.noFill()
    canvas.stroke(255, 255, 255, 180)
    canvas.strokeWeight(1.5)
    val animOffset = math.sin(globalTime * 3) * 15.0
    canvas.ellipse(sphereX + (animOffset * 0.2), sphereY, radius * 0.8, radius * 1.9)
    canvas.ellipse(sphereX, sphereY + (animOffset * 0.2), radius * 1.9, radius * 0.8)

    // Glowing core specular reflection
    canvas.fill(255, 255, 255, 220)
    canvas.noStroke()
    canvas.ellipse(sphereX - 10, sphereY - 10, 8, 8)

    if (earthNotificationTimer > 0) {
        canvas.fill(255, 50, 50, 240)
        canvas.stroke(255, 255, 255)
        canvas.strokeWeight(2)
        canvas.rect(155, cheight - 265, 260, 42)
        canvas.fill(255, 255, 255)
        canvas.text("? Already on Earth! Choose another.", 165, cheight - 239)
    }
}

// ZOOMED OUT SOLAR SYSTEM WITH DIFFERENT CUSTOM SPEEDS
def drawFullSolarSystem(canvas: CanvasDraw) {
    val centerX = cwidth / 2.0 + 80.0
    val centerY = cheight / 2.0

    canvas.fill(255, 140, 0)
    canvas.stroke(255, 200, 0)
    canvas.strokeWeight(2)
    canvas.ellipse(centerX, centerY, 45, 45)

    val orbits = Array(50.0, 80.0, 110.0, 145.0, 185.0, 225.0, 265.0, 305.0)
    val orbitalSpeeds = Array(2.2, 1.6, 1.2, 1.0, 0.8, 0.5, 0.35, 0.25)
    val planetColors = Array(
        cm.rgb(150, 150, 150), 
        cm.rgb(220, 180, 100), 
        cm.rgb(40, 130, 240), 
        cm.rgb(200, 70, 40),  
        cm.rgb(210, 140, 90), 
        cm.rgb(190, 170, 110),
        cm.rgb(100, 200, 220),
        cm.rgb(50, 90, 220)   
    )

    for (i <- orbits.indices) {
        canvas.noFill()
        canvas.stroke(255, 255, 255, 35)
        canvas.strokeWeight(1)
        canvas.ellipse(centerX, centerY, orbits(i) * 2, orbits(i) * 2)

        val angle = globalTime * orbitalSpeeds(i) + (i * 1.5)
        val px = centerX + math.cos(angle) * orbits(i)
        val py = centerY + math.sin(angle) * orbits(i)

        canvas.fill(planetColors(i))
        canvas.noStroke()
        canvas.ellipse(px, py, 10, 10)
    }
}

def drawPlanetSurfaceBackground(canvas: CanvasDraw) {
    var surfColor = cm.rgb(50, 50, 80)
    var darkSurfColor = cm.rgb(25, 25, 40)
    repeatFor(planetDetailsMap) { pd =>
        if (pd.name == selectedPlanet) {
            surfColor = pd.surfaceCol
            darkSurfColor = pd.darkSurfCol
        }
    }

    canvas.fill(surfColor)
    canvas.noStroke()
    canvas.rect(0, 0, cwidth, 150)

    canvas.fill(darkSurfColor)
    canvas.rect(0, 115, cwidth, 35)
}

def drawWavingTricolorFlag(canvas: CanvasDraw) {
    val fw = 160.0
    val fh = 28.0
    val fx = (cwidth - fw) / 2.0 + 110.0
    val fy = cheight / 2.0 - 10.0

    canvas.stroke(220, 220, 220)
    canvas.strokeWeight(6)
    canvas.line(fx, fy + fh * 3 + 15, fx, fy - 160)

    val stepSize = 4.0
    var x = 0.0
    while (x < fw) {
        val yOffset = math.sin(waveAngle + (x * 0.04)) * 9.0
        
        canvas.stroke(255, 153, 51)
        canvas.strokeWeight(stepSize + 1)
        canvas.line(fx + x, fy + fh * 2 + yOffset, fx + x, fy + fh * 3 + yOffset)

        canvas.stroke(255, 255, 255)
        canvas.line(fx + x, fy + fh + yOffset, fx + x, fy + fh * 2 + yOffset)

        canvas.stroke(18, 136, 7)
        canvas.line(fx + x, fy + yOffset, fx + x, fy + fh + yOffset)

        x += stepSize
    }

    val chakraX = fx + fw / 2.0
    val chakraY = fy + fh + (fh / 2.0) + (math.sin(waveAngle + ((fw / 2.0) * 0.04)) * 9.0)
    val radius = 11.0

    canvas.stroke(0, 0, 128)
    canvas.strokeWeight(2)
    canvas.noFill()
    canvas.ellipse(chakraX, chakraY, radius * 2, radius * 2)

    canvas.strokeWeight(1)
    for (i <- 0 until 24) {
        val angle = chakraRotation + (i * (2 * math.Pi / 24))
        val xEnd = chakraX + math.cos(angle) * radius
        val yEnd = chakraY + math.sin(angle) * radius
        canvas.line(chakraX, chakraY, xEnd, yEnd)
    }
}

def drawSingleSoldier(canvas: CanvasDraw, sx: Double, sy: Double) {
    canvas.fill(45, 80, 45)
    canvas.stroke(20, 50, 20)
    canvas.strokeWeight(1)
    canvas.rect(sx - 12, sy, 24, 38)

    canvas.fill(240, 195, 150)
    canvas.rect(sx - 4, sy + 38, 8, 6)
    canvas.ellipse(sx, sy + 52, 18, 18)

    canvas.fill(30, 60, 30)
    canvas.ellipse(sx, sy + 57, 22, 14)

    canvas.stroke(45, 80, 45)
    canvas.strokeWeight(5)
    canvas.line(sx - 12, sy + 34, sx - 18, sy + 10)
    canvas.line(sx + 12, sy + 34, sx + 22, sy + 48)
    canvas.line(sx + 22, sy + 48, sx + 8, sy + 57)

    canvas.stroke(240, 195, 150)
    canvas.strokeWeight(3)
    canvas.line(sx + 8, sy + 57, sx + 4, sy + 58)

    canvas.stroke(30, 50, 30)
    canvas.strokeWeight(6)
    canvas.line(sx - 6, sy, sx - 6, sy - 25)
    canvas.line(sx + 6, sy, sx + 6, sy - 25)
}

def drawSalutingAstronaut(canvas: CanvasDraw, ax: Double, ay: Double) {
    canvas.pushMatrix()
    canvas.translate(ax, ay)

    canvas.fill(240, 240, 245)
    canvas.stroke(80, 80, 90)
    canvas.strokeWeight(1)
    canvas.rect(-12, -4, 24, 38)

    canvas.fill(255, 153, 51); canvas.rect(-5, 14, 10, 3)
    canvas.fill(18, 136, 7); canvas.rect(-5, 10, 10, 3)

    canvas.fill(80, 80, 90)
    canvas.rect(-9, -25, 7, 20); canvas.rect(2, -25, 7, 20)

    canvas.stroke(200, 200, 210)
    canvas.strokeWeight(5)
    canvas.line(-12, 28, -18, 10)
    canvas.line(12, 28, 22, 42)
    canvas.line(22, 42, 8, 50)

    canvas.fill(255, 255, 255)
    canvas.stroke(100, 100, 120)
    canvas.ellipse(0, 48, 22, 22)

    canvas.fill(20, 140, 220)
    canvas.noStroke()
    canvas.ellipse(1, 48, 14, 12)

    canvas.popMatrix()
}

def drawSoldiersSquad(canvas: CanvasDraw) {
    val baseX = (cwidth / 2.0) + 40.0
    val baseY = (cheight / 2.0) - 130.0

    drawSingleSoldier(canvas, baseX, baseY)        
    drawSingleSoldier(canvas, baseX + 35, baseY)   
    drawSingleSoldier(canvas, baseX + 70, baseY)
    drawSalutingAstronaut(canvas, baseX - 40, baseY)
}

def drawLaunchAndUI(canvas: CanvasDraw) {
    if (!isLaunched && !isTimerRunning && !quizActive) {
        canvas.fill(255, 50, 0)
        canvas.stroke(255, 200, 0)
        canvas.strokeWeight((math.sin(neonGlowPhase * 3) * 2 + 3).toFloat)
        canvas.rect(15, 15, 110, 40)

        canvas.fill(255, 255, 255)
        canvas.text("? LAUNCH", 25, 30)
    }

    if (readyToReturnEarth && !isReturnLaunched) {
        canvas.fill(18, 136, 7)
        canvas.stroke(0, 255, 150)
        canvas.strokeWeight((math.sin(neonGlowPhase * 3) * 2 + 4).toFloat)
        canvas.rect(cwidth / 2.0 - 120, cheight - 75, 240, 45)

        canvas.fill(255, 255, 255)
        canvas.text("? RETURN TO EARTH", cwidth / 2.0 - 95, cheight - 48)
    }

    if (isTimerRunning) {
        val sec = countdownTimer.toInt
        canvas.fill(255, 215, 0)
        canvas.text(s"COUNTDOWN: $sec s", 135, 35)
    } else if (isReturnTimerRunning) {
        val sec = returnCountdownTimer.toInt
        canvas.fill(255, 215, 0)
        canvas.text(s"RETURN COUNTDOWN: $sec s", cwidth / 2.0 - 110, cheight - 90)
    }
}

// ==================== ULTRA-SHARP DETAILED ISRO ROCKET ====================
def drawISRORocketTopSurface(canvas: CanvasDraw) {
    val rx = cwidth / 2.0
    val ry = rocketSurfaceY

    canvas.pushMatrix()
    canvas.translate(rx, ry)

    // Powerful Rocket Exhaust Fire & Thruster Glow
    if ((isLaunched && !isLanded) || (isReturnLaunched && !isTeleportedToEarth)) {
        canvas.fill(255, 120, 0)
        canvas.noStroke()
        canvas.ellipse(0, -55, randomDouble(26, 38), randomDouble(35, 50))
        canvas.fill(255, 255, 220)
        canvas.ellipse(0, -45, 14, 25)
    }

    // Rocket Body - Main Fuselage (High Aerodynamic Details)
    canvas.fill(245, 245, 250)
    canvas.stroke(0, 255, 255)
    canvas.strokeWeight(2)
    canvas.rect(-12, -45, 24, 45)

    // Ultra-Sharp Nose Cone (Using lines instead of triangle)
    canvas.fill(255, 60, 60)
    canvas.stroke(255, 200, 200)
    canvas.strokeWeight(1.5)
    canvas.beginShape()
    canvas.vertex(0, -68)
    canvas.vertex(-12, -45)
    canvas.vertex(12, -45)
    canvas.endShape()

    // Side Boosters / Strap-on Motors (Detailed ISRO design)
    canvas.fill(220, 220, 230)
    canvas.stroke(100, 100, 120)
    canvas.rect(-20, -35, 7, 35)
    canvas.rect(13, -35, 7, 35)
    // Booster pointy tips
    canvas.beginShape(); canvas.vertex(-16.5, -45); canvas.vertex(-20, -35); canvas.vertex(-13, -35); canvas.endShape()
    canvas.beginShape(); canvas.vertex(16.5, -45); canvas.vertex(-13, -35); canvas.vertex(20, -35); canvas.endShape()

    // Aerodynamic Fins at bottom
    canvas.fill(255, 153, 51)
    canvas.beginShape(); canvas.vertex(-12, 0); canvas.vertex(-22, 10); canvas.vertex(-12, 5); canvas.endShape()
    canvas.beginShape(); canvas.vertex(12, 0); canvas.vertex(22, 10); canvas.vertex(12, 5); canvas.endShape()

    // Door / Airlock compartment
    if (isLanded && !thankYouShown) {
        canvas.fill(20, 20, 40)
        val doorWidth = 14.0 * doorOpeningProgress
        canvas.rect(-7, -25, doorWidth, 22)
    } else {
        canvas.fill(160, 160, 175)
        canvas.rect(-7, -25, 14, 22)
    }

    // ISRO Branding Stamp
    canvas.fill(0, 51, 102)
    canvas.text("ISRO", -10, -12)

    if (astronautOut && !isMiniGameActive && !thankYouShown) {
        canvas.fill(240, 240, 245)
        canvas.stroke(50, 50, 60)
        canvas.strokeWeight(1)
        canvas.rect(22, -22, 16, 22)
        canvas.fill(255, 255, 255)
        canvas.stroke(80, 80, 100)
        canvas.ellipse(30, -17, 14, 14)
        canvas.fill(0, 180, 240)
        canvas.noStroke()
        canvas.ellipse(31, -17, 9, 7)
    }

    canvas.popMatrix()
}

def drawQuizPopup(canvas: CanvasDraw) {
    if (quizActive) {
        var qText = ""
        var opts = Array("", "", "")
        repeatFor(planetDetailsMap) { pd =>
            if (pd.name == selectedPlanet) {
                qText = pd.quizQuestion
                opts = pd.quizOptions
            }
        }

        canvas.fill(10, 15, 30, 250)
        canvas.stroke(255, 215, 0)
        canvas.strokeWeight(3)
        canvas.rect(cwidth / 2.0 - 210, cheight / 2.0 - 90, 420, 180)

        canvas.fill(255, 255, 255)
        canvas.text(s"? MISSION QUESTION FOR $selectedPlanet:", cwidth / 2.0 - 195, cheight / 2.0 - 65)
        canvas.text(qText, cwidth / 2.0 - 195, cheight / 2.0 - 40)

        val boxY = cheight / 2.0 - 10.0
        canvas.fill(30, 40, 70)
        canvas.stroke(0, 255, 255)
        canvas.strokeWeight(1.5)
        canvas.rect(cwidth / 2.0 - 190, boxY, 380, 25)
        canvas.rect(cwidth / 2.0 - 190, boxY + 30, 380, 25)
        canvas.rect(cwidth / 2.0 - 190, boxY + 60, 380, 25)

        canvas.fill(255, 255, 255)
        canvas.text(opts(0), cwidth / 2.0 - 180, boxY + 17)
        canvas.text(opts(1), cwidth / 2.0 - 180, boxY + 47)
        canvas.text(opts(2), cwidth / 2.0 - 180, boxY + 77)

        if (quizFeedback != "") {
            canvas.fill(255, 50, 50)
            canvas.text(quizFeedback, cwidth / 2.0 - 100, boxY + 105)
        }
    }
}

def drawPlanetInfoPopup(canvas: CanvasDraw) {
    if (infoPanelShown && !isMiniGameActive) {
        var dLine = ""
        var dnInfo = ""
        var gInfo = ""
        var alienInfo = ""
        var atmInfo = ""
        var coreInfo = ""
        var factInfo = ""

        repeatFor(planetDetailsMap) { pd =>
            if (pd.name == selectedPlanet) {
                dLine = pd.infoLine1
                dnInfo = pd.dayNightStatus
                gInfo = pd.gravityVal
                alienInfo = pd.alienHumanStatus
                atmInfo = pd.atmosphere
                coreInfo = pd.coreTemp
                factInfo = pd.funFact
            }
        }

        canvas.fill(10, 15, 30, 250)
        canvas.stroke(0, 255, 255)
        canvas.strokeWeight(3)
        canvas.rect(cwidth / 2.0 - 230, cheight / 2.0 - 110, 460, 215)

        canvas.fill(255, 255, 255)
        canvas.text(s"? ASTRO-REPORT ($selectedPlanet) - Page $infoPageNumber/3", cwidth / 2.0 - 215, cheight / 2.0 - 85)

        if (infoPageNumber == 1) {
            canvas.text(s"? Surface: $dLine", cwidth / 2.0 - 215, cheight / 2.0 - 50)
            canvas.text(s"? $dnInfo", cwidth / 2.0 - 215, cheight / 2.0 - 20)
            canvas.text(s"? Gravity: $gInfo", cwidth / 2.0 - 215, cheight / 2.0 + 10)
        } else if (infoPageNumber == 2) {
            canvas.text(s"? Atmosphere: $atmInfo", cwidth / 2.0 - 215, cheight / 2.0 - 50)
            canvas.text(s"? Core Temp: $coreInfo", cwidth / 2.0 - 215, cheight / 2.0 - 20)
            canvas.text(s"? $alienInfo", cwidth / 2.0 - 215, cheight / 2.0 + 10)
        } else {
            canvas.text(s"? Fun Fact: $factInfo", cwidth / 2.0 - 215, cheight / 2.0 - 35)
            canvas.text("? Mission Ready: Proceed to Game!", cwidth / 2.0 - 215, cheight / 2.0 + 5)
        }

        if (infoPageNumber > 1) {
            canvas.fill(100, 100, 150)
            canvas.stroke(255, 255, 255)
            canvas.strokeWeight(1.5)
            canvas.rect(cwidth / 2.0 - 210, cheight / 2.0 + 45, 120, 32)
            canvas.fill(255, 255, 255)
            canvas.text("? PREV PAGE", cwidth / 2.0 - 200, cheight / 2.0 + 66)
        }

        canvas.fill(255, 153, 51)
        canvas.stroke(255, 255, 255)
        canvas.strokeWeight(2)
        canvas.rect(cwidth / 2.0 + 80, cheight / 2.0 + 45, 130, 32)
        canvas.fill(255, 255, 255)
        val nextBtnText = if (infoPageNumber < 3) "NEXT PAGE ?" else "PLAY GAME ?"
        canvas.text(nextBtnText, cwidth / 2.0 + 90, cheight / 2.0 + 66)
    }

    if (thankYouShown) {
        canvas.fill(10, 15, 30, 245)
        canvas.stroke(0, 255, 100)
        canvas.strokeWeight(3)
        canvas.rect(cwidth / 2.0 - 210, cheight / 2.0 - 60, 420, 120)

        canvas.fill(50, 255, 50)
        canvas.text("? FANTASTIC! GAME COMPLETED!", cwidth / 2.0 - 165, cheight / 2.0 - 25)
        canvas.fill(255, 255, 255)
        canvas.text("??? Astronaut: 'Thank you! Re-entering spaceship...'", cwidth / 2.0 - 195, cheight / 2.0 + 15)
    }
}

def drawAstronautPlayer(canvas: CanvasDraw, ax: Double, ay: Double) {
    canvas.pushMatrix()
    canvas.translate(ax, ay)

    canvas.fill(240, 240, 245)
    canvas.stroke(50, 50, 60)
    canvas.strokeWeight(1)
    canvas.rect(-8, -10, 16, 22)

    canvas.fill(255, 153, 51); canvas.rect(-4, 2, 8, 2)
    canvas.fill(18, 136, 7); canvas.rect(-4, -2, 8, 2)

    canvas.fill(255, 255, 255)
    canvas.stroke(80, 80, 100)
    canvas.ellipse(0, 18, 18, 18)

    canvas.fill(0, 180, 240)
    canvas.noStroke()
    canvas.ellipse(1, 18, 12, 9)

    canvas.popMatrix()
}

def drawMiniGames(canvas: CanvasDraw) {
    if (isMiniGameActive) {
        val gx = cwidth / 2.0 - 220.0
        val gy = cheight / 2.0 - 100.0

        canvas.fill(10, 10, 25, 240)
        canvas.stroke(255, 0, 200)
        canvas.strokeWeight(3)
        canvas.rect(gx, gy, 440, 210)

        val gSec = gameCountdownTimer.toInt
        canvas.fill(255, 50, 50)
        canvas.text(s"? Time Left: ${gSec}s", gx + 310, gy + 20)

        if (activeMiniGameType == 0) {
            canvas.fill(255, 255, 255)
            canvas.text("? Mini-Game 1: Platform Potion Hunt!", gx + 15, gy + 190)

            canvas.fill(255, 153, 51); canvas.rect(gx + 20, gy + 175, 400, 12)
            canvas.fill(255, 255, 255); canvas.rect(gx + 140, gy + 120, 120, 10)
            canvas.fill(18, 136, 7); canvas.rect(gx + 280, gy + 75, 100, 10)

            if (!isKeyPotionCollected) {
                canvas.fill(255, 50, 50)
                canvas.ellipse(gx + 350, gy + 110, 16, 16)
            }

            if (isDoorUnlocked) {
                canvas.fill(0, 200, 255, 200)
                canvas.ellipse(gx + 50, gy + 120, 25, 40)
            }
            drawAstronautPlayer(canvas, miniPlayerX, miniPlayerY)

        } else if (activeMiniGameType == 1) {
            canvas.fill(255, 255, 255)
            canvas.text(s"? Mini-Game 2: Catch Stars ($starsCaughtCount/$targetStarsToCatch)", gx + 15, gy + 190)

            for (i <- 0 until fallingStarX.length) {
                canvas.fill(255, 255, 0)
                canvas.ellipse(fallingStarX(i), fallingStarY(i), 14, 14)
            }
            drawAstronautPlayer(canvas, miniPlayerX, miniPlayerY)

        } else {
            canvas.fill(255, 255, 255)
            canvas.text("?? Mini-Game 3: Asteroid Dodge (Up/Down)", gx + 15, gy + 190)

            for (i <- 0 until asteroidX.length) {
                canvas.fill(200, 50, 50)
                canvas.ellipse(asteroidX(i), asteroidY(i), 20, 20)
            }
            drawAstronautPlayer(canvas, miniPlayerX, miniPlayerY)
        }
    }
}

def drawPatrioticInfoText(canvas: CanvasDraw) {
    if (charIndex > 0) {
        val visibleSubstring = patrioticInfoText.substring(0, charIndex)
        val lines = visibleSubstring.split("\n")
        val startX = 20.0
        var startY = cheight - 60.0

        for (i <- lines.indices) {
            canvas.fill(255, 255, 255) 
            canvas.text(lines(i), startX, startY)
            startY -= 22.0
        }
    }
}

def viewState(canvas: CanvasDraw) {
    if (anthemFinished) {
        canvas.fill(0, 0, 0, 255)
        canvas.noStroke()
        canvas.rect(0, 0, cwidth, cheight)

        canvas.fill(255, 255, 255)
        canvas.text("THE END", cwidth / 2.0 - 50.0, cheight / 2.0 - 20.0)
        
        canvas.fill(255, 153, 51)
        canvas.text("Made by Yadnesh Shahare and Bhavesh Shahare", cwidth / 2.0 - 180.0, cheight / 2.0 + 20.0)
        return
    }

    canvas.fill(0, 0, 0, 255)
    canvas.noStroke()
    canvas.rect(0, 0, cwidth, cheight)

    for (i <- 0 until starCount) {
        canvas.stroke(255, 255, 255)
        canvas.strokeWeight(starSize(i))
        canvas.point(starX(i), starY(i))
    }
    
    repeatFor(fireworks) { f => f.view(canvas) }

    if (isTeleportedToEarth) {
        drawWavingTricolorFlag(canvas)
        drawSoldiersSquad(canvas)
        drawPatrioticInfoText(canvas)
    } else {
        if (isLaunched) {
            drawPlanetSurfaceBackground(canvas)
        } else {
            drawFullSolarSystem(canvas)
            drawPlanetList(canvas)
        }
        drawLaunchAndUI(canvas)
        drawISRORocketTopSurface(canvas)
    }

    drawQuizPopup(canvas)
    drawPlanetInfoPopup(canvas)
    drawMiniGames(canvas)
}

animateWithCanvasDraw { canvas =>
    updateState()
    viewState(canvas)
}