← index
libmx2/libmx/gl_ship/ship.cpp
Source: libmx2/libmx/gl_ship/ship.cpp
#include"mx.hpp"
#include"argz.hpp"

#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#include <GLES3/gl3.h>
#endif

#include"gl.hpp"
#include"loadpng.hpp"
#include"model.hpp"
#include<random>

#ifdef DEBUG_MODE
#define CHECK_GL_ERROR() \
{ GLenum err = glGetError(); \
if (err != GL_NO_ERROR) \
printf("OpenGL Error: %d at %s:%d\n", err, __FILE__, __LINE__); }
#else
#define CHECK_GL_ERROR()
#endif

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

using mx::generateRandomFloat;
using mx::generateRandomInt;

#if defined(__EMSCRIPTEN__) || defined(__ANDOIRD__)

const char *g_vSource = R"(#version 300 es
precision highp float;
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform vec3 objectColor;

out vec3 vertexColor;
out vec2 TexCoords;

void main()
{
    vec4 worldPos = model * vec4(aPos, 1.0);
    gl_Position = projection * view * worldPos;
    vec3 norm = normalize(mat3(transpose(inverse(model)))* aNormal);
    vec3 lightDir = normalize(lightPos - vec3(worldPos));
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;
    float specularStrength = 1.0;    
    float shininess = 64.0;          
    vec3 viewDir = normalize(viewPos - vec3(worldPos));
    vec3 reflectDir = reflect(-lightDir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), float(shininess));
    vec3 specular = specularStrength * spec * lightColor;
    vec3 finalColor = (ambient + diffuse + specular) * objectColor;
    vertexColor = finalColor;
    TexCoords = aTexCoords;
}
)";

const char *g_fSource = R"(#version 300 es
        precision highp float;
        in vec3 vertexColor;
        in vec2 TexCoords;
        uniform sampler2D texture1;  
        out vec4 FragColor;

        uniform float time_f;
        uniform int effect_type;

        vec4 alphaXor(vec4 color) {
            ivec3 source;
            for (int i = 0; i < 3; ++i) {
                source[i] = int(255.0 * clamp(color[i], 0.0, 1.0));
            }
            
            float time_mod = mod(time_f, 10.0); 
            ivec3 time_factors = ivec3(
                int(255.0 * time_mod / 10.0), 
                int(127.0 * time_mod / 10.0), 
                int(63.0 * time_mod / 10.0)
            );
            
            
            ivec3 int_color;
            for (int i = 0; i < 3; ++i) {
                int_color[i] = int(255.0 * clamp(color[i], 0.0, 1.0));
                int_color[i] = int_color[i] ^ (source[i] + time_factors[i % 3]);
                int_color[i] = int_color[i] % 256; 
                color[i] = float(int_color[i]) / 255.0;
            }
            return color;
        }

        vec4 distort(vec2 tc, float time_t, sampler2D tex) {
            vec4 ctx;
            float xDistort = cos(tc.y * 10.0 + time_f) * 0.1;
                float yDistort = sin(tc.x * 10.0 + time_f) * 0.1;
                float tanDistortX = tan(tc.x * 5.0 + time_f) * 0.05;
                float tanDistortY = tan(tc.y * 5.0 + time_f) * 0.05;
                vec2 distortedTC = tc + vec2(xDistort + tanDistortX, yDistort + tanDistortY);
                distortedTC = fract(distortedTC);
                ctx = texture(tex, distortedTC);
                return ctx;
        }


        void main()
        {
            if(effect_type != 3) {
                FragColor = vec4(vertexColor, 1.0) * texture(texture1, TexCoords);
                vec4 ctx = distort(TexCoords, time_f, texture1);
                FragColor = alphaXor(ctx);
                FragColor.a = 1.0;
            } else {
                FragColor = texture(texture1, TexCoords); 
            }
        }
)";
#else
const char *g_vSource = R"(#version 330 core
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec3 aNormal;
    layout (location = 2) in vec2 aTexCoords;
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;

    uniform vec3 lightPos;
    uniform vec3 viewPos;
    uniform vec3 lightColor;
    uniform vec3 objectColor;

    out vec3 vertexColor;
    out vec2 TexCoords;

    void main()
    {
        vec4 worldPos = model * vec4(aPos, 1.0);
        gl_Position = projection * view * worldPos;
        vec3 norm = normalize(mat3(transpose(inverse(model))) * aNormal);
        vec3 lightDir = normalize(lightPos - vec3(worldPos));
        float ambientStrength = 0.1;
        vec3 ambient = ambientStrength * lightColor;
        float diff = max(dot(norm, lightDir), 0.0);
        vec3 diffuse = diff * lightColor;
        float specularStrength = 1.0;    
        float shininess = 64.0;          
        vec3 viewDir = normalize(viewPos - vec3(worldPos));
        vec3 reflectDir = reflect(-lightDir, norm);
        float spec = pow(max(dot(viewDir, reflectDir), 0.0), float(shininess));
        vec3 specular = specularStrength * spec * lightColor;
        vec3 finalColor = (ambient + diffuse + specular) * objectColor;
        vertexColor = finalColor;
        TexCoords = aTexCoords;
    }

)";
const char *g_fSource = R"(#version 330 core
        in vec3 vertexColor;
        in vec2 TexCoords;
        uniform sampler2D texture1;  
        out vec4 FragColor;

        uniform float time_f;
        uniform int effect_type;

        vec4 alphaXor(vec4 color) {
            ivec3 source;
            for (int i = 0; i < 3; ++i) {
                source[i] = int(255.0 * clamp(color[i], 0.0, 1.0));
            }
            
            float time_mod = mod(time_f, 10.0); 
            ivec3 time_factors = ivec3(
                int(255.0 * time_mod / 10.0), 
                int(127.0 * time_mod / 10.0), 
                int(63.0 * time_mod / 10.0)
            );
            
            
            ivec3 int_color;
            for (int i = 0; i < 3; ++i) {
                int_color[i] = int(255.0 * clamp(color[i], 0.0, 1.0));
                int_color[i] = int_color[i] ^ (source[i] + time_factors[i % 3]);
                int_color[i] = int_color[i] % 256; 
                color[i] = float(int_color[i]) / 255.0;
            }
            return color;
        }

        vec4 distort(vec2 tc, float time_t, sampler2D tex) {
            vec4 ctx;
            float xDistort = cos(tc.y * 10.0 + time_f) * 0.1;
                float yDistort = sin(tc.x * 10.0 + time_f) * 0.1;
                float tanDistortX = tan(tc.x * 5.0 + time_f) * 0.05;
                float tanDistortY = tan(tc.y * 5.0 + time_f) * 0.05;
                vec2 distortedTC = tc + vec2(xDistort + tanDistortX, yDistort + tanDistortY);
                distortedTC = fract(distortedTC);
                ctx = texture(tex, distortedTC);
                return ctx;
        }


        void main() {
            if(effect_type != 3) {
                FragColor = vec4(vertexColor, 1.0) * texture(texture1, TexCoords);
                vec4 ctx = distort(TexCoords, time_f, texture1);
                FragColor = alphaXor(ctx);
                FragColor.a = 1.0;
            } else {
                FragColor = texture(texture1, TexCoords); 
            }
        }
)";
#endif
#if !defined(__EMSCRIPTEN__) && !defined(__ANDROID__)
const char* vertSource = R"(#version 330 core

layout (location = 0) in vec3 inPosition; 
layout (location = 1) in float inSize;
layout (location = 2) in vec4 inColor;

uniform mat4 MVP;  

out vec4 fragColor;

void main() {
    gl_Position = MVP * vec4(inPosition, 1.0);
    gl_PointSize = inSize;
    fragColor = inColor;
}
)";

const char* fragSource = R"(#version 330 core

in vec4 fragColor;
out vec4 FragColor;

uniform sampler2D spriteTexture;

void main() {
    float dist = length(gl_PointCoord - vec2(0.5));
    if (dist > 0.5) {
        discard;
    }
    vec4 texColor = texture(spriteTexture, gl_PointCoord);
    FragColor = texColor * fragColor;
}
)";

#else
const char* vertSource = R"(#version 300 es

precision highp float;

layout (location = 0) in vec3 inPosition; 
layout (location = 1) in float inSize;
layout (location = 2) in vec4 inColor;

uniform mat4 MVP;

out vec4 fragColor;

void main() {
    gl_Position = MVP * vec4(inPosition, 1.0);
    gl_PointSize = inSize;
    fragColor = inColor;
}
)";

const char* fragSource = R"(#version 300 es

precision highp float;

in vec4 fragColor;
out vec4 FragColor;

uniform sampler2D spriteTexture;

void main() {
    float dist = length(gl_PointCoord - vec2(0.5));
    if (dist > 0.5) {
        discard;
    }

    vec4 texColor = texture(spriteTexture, gl_PointCoord);
    FragColor = texColor * fragColor;
}
)";
#endif

class Intro : public gl::GLObject {
public:

    Intro() = default;

    virtual ~Intro() {

    }

    virtual void load(gl::GLWindow *win) {

        font.loadFont(win->util.getFilePath("data/font.ttf"), 25);

#if defined(__EMSCRIPTEN__) 
    const char *vSource = R"(#version 300 es
            precision mediump float;
            layout (location = 0) in vec3 aPos;
            layout (location = 1) in vec2 aTexCoord;

            out vec2 TexCoord;

            void main() {
                gl_Position = vec4(aPos, 1.0); 
                TexCoord = aTexCoord;         
            }
    )";
    const char *fSource = R"(#version 300 es
    precision highp float;
    out vec4 FragColor;
    in vec2 TexCoord;
    
    uniform sampler2D textTexture;
    uniform float time_f;
    uniform float alpha;
    
    void main(void) {
        vec2 uv = TexCoord * 2.0 - 1.0;
        float len = length(uv);
        float bubble = smoothstep(0.8, 1.0, 1.0 - len);
        vec2 distort = uv * (1.0 + 0.1 * sin(time_f + len * 20.0));
        vec4 texColor = texture(textTexture, distort * 0.5 + 0.5);
        FragColor = mix(texColor, vec4(1.0, 1.0, 1.0, 1.0), bubble);
        FragColor = FragColor * alpha;
    }
    )";
    
#else
    const char *vSource = R"(#version 330 core
        layout (location = 0) in vec3 aPos;
        layout (location = 1) in vec2 aTexCoord;
        out vec2 TexCoord;
        void main() {
            gl_Position = vec4(aPos, 1.0); 
            TexCoord = aTexCoord;        
        }
    )";
    const char *fSource = R"(#version 330 core
        out vec4 FragColor;
        in vec2 TexCoord;
        uniform sampler2D textTexture;
        uniform float time_f;
        uniform float alpha;
        void main(void) {
            vec2 uv = TexCoord * 2.0 - 1.0;
            float len = length(uv);
            float bubble = smoothstep(0.8, 1.0, 1.0 - len);
            vec2 distort = uv * (1.0 + 0.1 * sin(time_f + len * 20.0));
            vec4 texColor = texture(textTexture, distort * 0.5 + 0.5);
            FragColor = mix(texColor, vec4(1.0, 1.0, 1.0, 1.0), bubble);
            FragColor = FragColor * alpha;
        }
    )";
#endif
        if(!shader.loadProgramFromText(vSource, fSource)) {
            throw mx::Exception("Error loading Intro shader");
        }   

        intro.initSize(win->w, win->h);
        intro.loadTexture(&shader, win->util.getFilePath("data/intro.png"), 0.0f, 0.0f, win->w, win->h);
    }

    virtual void draw(gl::GLWindow *win);
    virtual void event(gl::GLWindow *win, SDL_Event &e) {

    }

    void update(float deltaTime) {
        if (deltaTime > 0.1f) {
            deltaTime = 0.1f;
        }
    }
protected:
    gl::GLSprite intro;
    gl::ShaderProgram shader;
    Uint32 lastUpdateTime = 0;
    float fade = 1.0f;
    mx::Font font;
};


class StarField : public gl::GLObject {
public:
    struct Particle {
        float x, y, z;   
        float vx, vy, vz; 
        float life;
        float twinkle;
    };

    static constexpr int NUM_PARTICLES = 2500;
    gl::ShaderProgram program;
    GLuint VAO, VBO[3];
    GLuint texture;
    std::vector<Particle> particles;
    Uint32 lastUpdateTime = 0;
    float cameraZoom = 3.0f;   
    float cameraRotation = 0.0f; 

    
    glm::vec3 lastCameraPos{0.0f, 0.0f, 0.0f};
    float starFieldRadius = 30.0f; 

    StarField() : particles(NUM_PARTICLES) {}

    ~StarField() override {
        glDeleteVertexArrays(1, &VAO);
        glDeleteBuffers(3, VBO);
        glDeleteTextures(1, &texture);
    }

    void load(gl::GLWindow *win) override {
        if(!program.loadProgramFromText(vertSource, fragSource)) {
            throw mx::Exception("Error loading shader");
        }

        for (auto& p : particles) {
            float radius = generateRandomFloat(10.0f, 30.0f);
            float theta = generateRandomFloat(0.0f, 2.0f * M_PI);
            float phi = generateRandomFloat(0.0f, M_PI);
            p.x = radius * sin(phi) * cos(theta);
            p.y = radius * sin(phi) * sin(theta);
            p.z = radius * cos(phi);
            p.vx = generateRandomFloat(-0.01f, 0.01f);
            p.vy = generateRandomFloat(-0.01f, 0.01f);
            p.vz = generateRandomFloat(-0.01f, 0.01f);
            
            p.life = generateRandomFloat(0.6f, 1.0f);
            p.twinkle = generateRandomFloat(1.0f, 5.0f);
        }
        glGenVertexArrays(1, &VAO);
        glGenBuffers(3, VBO);
        glBindVertexArray(VAO);
        glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
        glBufferData(GL_ARRAY_BUFFER, NUM_PARTICLES * 3 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);\
        glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
        glBufferData(GL_ARRAY_BUFFER, NUM_PARTICLES * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, (void*)0);
        glBindBuffer(GL_ARRAY_BUFFER, VBO[2]);
        glBufferData(GL_ARRAY_BUFFER, NUM_PARTICLES * 4 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, (void*)0);
        texture = gl::loadTexture(win->util.getFilePath("data/star.png"));
        cameraRotation = 356.0f;
        cameraZoom = 0.09f;
        lastUpdateTime = SDL_GetTicks();
    }

    void event(gl::GLWindow *win, SDL_Event &e) override {

    }

    void draw(gl::GLWindow *win) override {
#ifndef __EMSCRIPTEN__
        glEnable(GL_PROGRAM_POINT_SIZE);
#endif
        glDisable(GL_DEPTH_TEST);
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE);

        Uint32 currentTime = SDL_GetTicks();
        float deltaTime = (currentTime - lastUpdateTime) / 1000.0f; // seconds
        lastUpdateTime = currentTime;

        update(deltaTime);

        CHECK_GL_ERROR();

        program.useProgram();

        glm::mat4 MVP = projectionMatrix * viewMatrix * glm::mat4(1.0f);
        program.setUniform("MVP", MVP);
        program.setUniform("spriteTexture", 0);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture);
        glBindVertexArray(VAO);
        glDrawArrays(GL_POINTS, 0, NUM_PARTICLES);
        CHECK_GL_ERROR();

        glDisable(GL_BLEND);
    }

    void update(float deltaTime) {
        if(deltaTime > 0.1f) 
            deltaTime = 0.1f;
            
        
        glm::mat4 invView = glm::inverse(viewMatrix);
        glm::vec3 cameraPos = glm::vec3(invView[3]);
        glm::vec3 cameraForward = -glm::vec3(invView[2]); 
        
        float cameraMoveDistance = glm::length(cameraPos - lastCameraPos);
        if (cameraMoveDistance > 0.5f) {
            lastCameraPos = cameraPos;
        }

        CHECK_GL_ERROR();

        std::vector<float> positions;
        std::vector<float> sizes;
        std::vector<float> colors;
        positions.reserve(NUM_PARTICLES * 3);
        sizes.reserve(NUM_PARTICLES);
        colors.reserve(NUM_PARTICLES * 4);
          
        for (auto& p : particles) {
            
            glm::vec3 starPos(p.x, p.y, p.z);
            glm::vec3 relativePos = starPos - cameraPos;
            float dotProduct = glm::dot(relativePos, cameraForward);
            float distance = glm::length(relativePos);
            bool needsRespawn = distance > starFieldRadius || 
                               (dotProduct < -0.7f && distance > starFieldRadius * 0.5f);
                               
            if (needsRespawn) {
                float forwardDistance = generateRandomFloat(starFieldRadius * 0.5f, starFieldRadius * 0.9f);
                float lateralDistance = generateRandomFloat(0.0f, starFieldRadius * 0.7f);
                float verticalDistance = generateRandomFloat(-starFieldRadius * 0.6f, starFieldRadius * 0.6f);
                glm::vec3 cameraRight = glm::vec3(invView[0]);
                glm::vec3 cameraUp = glm::vec3(invView[1]);
                float angle = generateRandomFloat(0.0f, 2.0f * M_PI);
                
                glm::vec3 newPos = cameraPos + 
                                   cameraForward * forwardDistance +
                                   cameraRight * (lateralDistance * cos(angle)) +
                                   cameraUp * (lateralDistance * sin(angle) + verticalDistance);
                
                p.x = newPos.x;
                p.y = newPos.y;
                p.z = newPos.z;
                
                p.life = generateRandomFloat(0.6f, 1.0f);
                p.twinkle = generateRandomFloat(1.0f, 5.0f);
            }
            
            float twinkleFactor = 0.8f * (1.0f + sin(SDL_GetTicks() * 0.002f * p.twinkle));
            float brightness = 1.3f * p.life * twinkleFactor;
            float distFactor = 1.0f - (distance / starFieldRadius * 0.7f);
            distFactor = glm::clamp(distFactor, 0.3f, 1.0f);
            brightness *= distFactor;
            
            positions.push_back(p.x);
            positions.push_back(p.y);
            positions.push_back(p.z);
            
            float size = 10.0f * p.life * (0.8f + 0.2f * distFactor);
            sizes.push_back(size);
            
            float alpha = p.life * distFactor * 1.2f;
            alpha = glm::clamp(alpha, 0.0f, 1.0f);
            
            float colorVar = 0.15f * sin(SDL_GetTicks() * 0.0015f * p.twinkle + 2.0f);
            colors.push_back(brightness + colorVar);
            colors.push_back(brightness);
            colors.push_back(brightness + colorVar * 0.5f);
            colors.push_back(alpha);
        }

        CHECK_GL_ERROR();

        glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, positions.size() * sizeof(float), positions.data());

        glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizes.size() * sizeof(float), sizes.data());

        CHECK_GL_ERROR();

        glBindBuffer(GL_ARRAY_BUFFER, VBO[2]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, colors.size() * sizeof(float), colors.data());

        CHECK_GL_ERROR();
    }

    void repositionStarsAroundCamera(const glm::vec3& cameraPos) {
        for (auto& p : particles) {
            float radius = generateRandomFloat(10.0f, starFieldRadius);
            float theta = generateRandomFloat(0.0f, 2.0f * M_PI);
            float phi = generateRandomFloat(0.0f, M_PI);
            
            p.x = cameraPos.x + radius * sin(phi) * cos(theta);
            p.y = cameraPos.y + radius * sin(phi) * sin(theta);
            p.z = cameraPos.z + radius * cos(phi);
        }
        
        lastCameraPos = cameraPos;
    }

    void setViewProjectionMatrices(const glm::mat4& view, const glm::mat4& projection) {
        this->viewMatrix = view;
        this->projectionMatrix = projection;
        
        glm::mat3 rotMat(view);
        glm::vec3 d(view[3]);
        glm::vec3 cameraPos = -d * rotMat;
        
        static bool firstTime = true;
        if (firstTime) {
            repositionStarsAroundCamera(cameraPos);
            firstTime = false;
        }
    }
    
private:
    glm::mat4 projectionMatrix{1.0f};
    glm::mat4 viewMatrix{1.0f};
};
    
class ExplodeEmiter {
public:
    static constexpr int MAX_PARTICLES = 10000;

    ExplodeEmiter() = default;
    ~ExplodeEmiter() {
        if (vao != 0) glDeleteVertexArrays(1, &vao);
        if (vbo != 0) glDeleteBuffers(1, &vbo);
    }

    struct Particle {
        float x, y, z;         
        float vx, vy, vz;      
        float r, g, b;         
        float alpha;           
        float size;            
        float life;            
        float maxLife;         
        bool active;           
    };

    
    glm::vec3 position{0.0f, 0.0f, 0.0f};
    
    void load(gl::GLWindow *win) {
        
#ifndef __EMSCRIPTEN__
        const char* particleVS = R"(#version 330 core
        layout (location = 0) in vec3 aPos;
        layout (location = 1) in vec4 aColor;
        layout (location = 2) in float aSize;
        
        out vec4 Color;
        
        uniform mat4 projection;
        uniform mat4 view;
        
        void main() {
            vec4 viewPos = view * vec4(aPos, 1.0);
            gl_Position = projection * viewPos;
            
            float distance = length(viewPos.xyz);
            gl_PointSize = aSize / (distance * 0.1); 
            Color = aColor;
        }
        )";

        const char* particleFS = R"(#version 330 core
        in vec4 Color;
        out vec4 FragColor;
        
        uniform sampler2D particleTexture;
        
        void main() {
            vec2 texCoord = gl_PointCoord;
            vec4 texColor = texture(particleTexture, texCoord);
            if(texColor.r < 0.1 && texColor.g < 0.1 && texColor.b < 0.1) {
                discard;
            }
            vec4 finalColor = texColor * Color;
            float dist = length(texCoord - vec2(0.5));
            float alpha = smoothstep(0.5, 0.4, dist) * finalColor.a;
            
            if (alpha < 0.01)
                discard;
                
            FragColor = vec4(finalColor.rgb, alpha);
        }
        )";
#else
        const char* particleVS = R"(#version 300 es
        precision highp float;
        layout (location = 0) in vec3 aPos;
        layout (location = 1) in vec4 aColor;
        layout (location = 2) in float aSize;
        
        out vec4 Color;
        
        uniform mat4 projection;
        uniform mat4 view;
        
        void main() {
            vec4 viewPos = view * vec4(aPos, 1.0);
            gl_Position = projection * viewPos;
            
            float distance = length(viewPos.xyz);
            gl_PointSize = aSize / (distance * 0.1);
            Color = aColor;
        }
        )";

        const char* particleFS = R"(#version 300 es
        precision highp float;
        in vec4 Color;
        out vec4 FragColor;
        
        uniform sampler2D particleTexture;
        
        void main() {
            vec2 texCoord = gl_PointCoord;
            vec4 texColor = texture(particleTexture, texCoord);
            if(texColor.r < 0.1 && texColor.g < 0.1 && texColor.b < 0.1) {
                discard;
            }
            vec4 finalColor = texColor * Color;
            
            float dist = length(texCoord - vec2(0.5));
            float alpha = smoothstep(0.5, 0.4, dist) * finalColor.a;
            
            if (alpha < 0.01)
                discard;
                
            FragColor = vec4(finalColor.rgb, alpha);
        }
        )";
#endif
        if (!shader.loadProgramFromText(particleVS, particleFS)) {
            throw mx::Exception("Failed to load particle shader program");
        }

        
        particles.resize(MAX_PARTICLES); 
        for (auto& p : particles) {
            p.active = false;
        }

        
        glGenVertexArrays(1, &vao);
        glGenBuffers(1, &vbo);
        
        glBindVertexArray(vao);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        
        glBufferData(GL_ARRAY_BUFFER, MAX_PARTICLES * 8 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
        glEnableVertexAttribArray(1);
        
        glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(7 * sizeof(float)));
        glEnableVertexAttribArray(2);
        
        glBindVertexArray(0);
    }

    void update(float deltaTime) {
        if (activeParticles == 0) return;
        
        int newActiveCount = 0;
        
        for (auto& p : particles) {
            if (!p.active) continue;
            
            p.x += p.vx * deltaTime;
            p.y += p.vy * deltaTime;
            p.z += p.vz * deltaTime;
            
            p.vx *= 0.98f;
            p.vy *= 0.98f;
            p.vz *= 0.98f;
            
            p.vy -= 0.5f * deltaTime;
            
            p.life += deltaTime;
            float lifeRatio = p.life / p.maxLife;
            
            if (lifeRatio < 0.2f) {
                p.alpha = lifeRatio / 0.2f;
            } else if (lifeRatio > 0.8f) {
                p.alpha = (1.0f - lifeRatio) / 0.2f;
            } else {
                p.alpha = 1.0f;
            }
            
            if (lifeRatio < 0.3f) {
                p.size *= 1.01f;
            } else {
                p.size *= 0.99f;
            }
            
            if (lifeRatio > 0.6f) {
                p.r = 1.0f - (lifeRatio - 0.6f) * 2.0f; 
                p.g = 0.6f * (1.0f - (lifeRatio - 0.6f) * 2.5f);
                p.b = 0.0f;
            }
        
            if (p.life >= p.maxLife || p.alpha < 0.01f) {
                p.active = false;
            } else {
                newActiveCount++;
            }
        }
        
        activeParticles = newActiveCount;
    }

    void draw(gl::GLWindow *win) {
        if (activeParticles == 0) return;
        
        
        std::vector<float> particleData;
        particleData.reserve(activeParticles * 8); 
        
        for (const auto& p : particles) {
            if (!p.active) continue;
            
            
            particleData.push_back(p.x);
            particleData.push_back(p.y);
            particleData.push_back(p.z);
            
            
            particleData.push_back(p.r);
            particleData.push_back(p.g);
            particleData.push_back(p.b);
            particleData.push_back(p.alpha);
            
            
            particleData.push_back(p.size);
        }
        
        
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE); 
        glDepthMask(GL_FALSE); 
        
        shader.useProgram();
        shader.setUniform("projection", projectionMatrix);
        shader.setUniform("view", viewMatrix);
        shader.setUniform("particleTexture", 0);
        
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, textureID);
        
        
        glBindVertexArray(vao);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferSubData(GL_ARRAY_BUFFER, 0, particleData.size() * sizeof(float), particleData.data());
        
        
#ifndef __EMSCRIPTEN__
        glEnable(GL_PROGRAM_POINT_SIZE);
#endif
        glDrawArrays(GL_POINTS, 0, activeParticles);
#ifndef __EMSCRIPTEN__
        glDisable(GL_PROGRAM_POINT_SIZE);
#endif
        
        
        glBindVertexArray(0);
        glDepthMask(GL_TRUE);
        glDisable(GL_BLEND);
    }

    void explode() {
        
        reset();
        
        const int WAVE_COUNT = 4;
        int particlesPerWave = MAX_PARTICLES / WAVE_COUNT;
        
        struct WaveParams {
            float minSpeed, maxSpeed;
            float minSize, maxSize;
            float minLife, maxLife;
            glm::vec3 baseColor;
        };
        
        WaveParams waves[WAVE_COUNT] = {
            {40.0f, 60.0f, 25.0f, 40.0f, 1.5f, 2.5f, {1.0f, 0.9f, 0.3f}},
            {30.0f, 45.0f, 20.0f, 30.0f, 2.0f, 3.0f, {1.0f, 0.6f, 0.2f}},
            {20.0f, 35.0f, 15.0f, 25.0f, 2.5f, 3.5f, {1.0f, 0.3f, 0.1f}},
            {10.0f, 25.0f, 5.0f, 15.0f, 3.0f, 4.0f, {0.8f, 0.2f, 0.1f}}
        };
        
        int particleIndex = 0;
        
        for (int wave = 0; wave < WAVE_COUNT; wave++) {
            for (int i = 0; i < particlesPerWave && particleIndex < MAX_PARTICLES; i++) {
                float theta = generateRandomFloat(0.0f, 2.0f * M_PI);
                float phi = generateRandomFloat(0.0f, M_PI);
                float x = sin(phi) * cos(theta);
                float y = sin(phi) * sin(theta);
                float z = cos(phi);        
                auto& p = particles[particleIndex++];
                float offset = 0.8f + 0.2f * static_cast<float>(wave) / WAVE_COUNT;
                p.x = position.x + x * offset;
                p.y = position.y + y * offset;
                p.z = position.z + z * offset;
                float speed = generateRandomFloat(waves[wave].minSpeed, waves[wave].maxSpeed);
                p.vx = x * speed;
                p.vy = y * speed;
                p.vz = z * speed;
                p.vx += generateRandomFloat(-5.0f, 5.0f);
                p.vy += generateRandomFloat(-5.0f, 5.0f);
                p.vz += generateRandomFloat(-5.0f, 5.0f);
                const auto& baseColor = waves[wave].baseColor;
                p.r = baseColor.r * generateRandomFloat(0.9f, 1.1f);
                p.g = baseColor.g * generateRandomFloat(0.9f, 1.1f);
                p.b = baseColor.b * generateRandomFloat(0.9f, 1.1f);
                p.alpha = 0.1f; 
                p.size = generateRandomFloat(waves[wave].minSize, waves[wave].maxSize);
                p.maxLife = generateRandomFloat(waves[wave].minLife, waves[wave].maxLife);
                p.life = 0.0f; 
                p.active = true;
            }
        }
        activeParticles = particleIndex;
    }
    
    void reset() {
        for (auto& p : particles) {
            p.active = false;
        }
        activeParticles = 0;
    }
    
    void setProjectionMatrix(const glm::mat4& proj) { projectionMatrix = proj; }
    void setViewMatrix(const glm::mat4& view) { viewMatrix = view; }
    void setTextureID(GLuint id) { textureID = id; }

protected:
    gl::ShaderProgram shader;
    std::vector<Particle> particles;
    GLuint vao = 0, vbo = 0;
    int activeParticles = 0;
    glm::mat4 projectionMatrix{1.0f};
    glm::mat4 viewMatrix{1.0f};
    GLuint textureID = 0;
};

#ifndef __EMSCRIPTEN__
const char* star_vSource = R"(#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

uniform vec3 lightPos;
uniform vec3 viewPos;

out vec3 Normal;
out vec3 FragPos;
out vec2 TexCoords;

void main() {
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = mat3(transpose(inverse(model))) * aNormal;
    TexCoords = aTexCoords;
    
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}
)";

const char* star_fSource = R"(#version 330 core
in vec3 Normal;
in vec3 FragPos;
in vec2 TexCoords;

out vec4 FragColor;

uniform vec3 lightPos;
uniform vec3 viewPos;
uniform sampler2D starTexture;

void main() {
    float ambientStrength = 0.3;
    vec3 ambient = ambientStrength * vec3(1.0, 1.0, 1.0);

    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * vec3(1.0, 1.0, 1.0);

    float specularStrength = 0.5;
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
    vec3 specular = specularStrength * spec * vec3(1.0, 1.0, 1.0);
    
    vec4 texColor = texture(starTexture, TexCoords);
    vec3 result = (ambient + diffuse + specular) * texColor.rgb;
    FragColor = vec4(result, 1.0);
}
)";
#else
const char* star_vSource = R"(#version 300 es
precision highp float;
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

uniform vec3 lightPos;
uniform vec3 viewPos;

out vec3 Normal;
out vec3 FragPos;
out vec2 TexCoords;

void main() {
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = mat3(transpose(inverse(model))) * aNormal;
    TexCoords = aTexCoords;
    
    gl_Position = projection * view * model * vec4(aPos, 1.0);
})";

const char* star_fSource = R"(#version 300 es
precision highp float;
in vec3 Normal;
in vec3 FragPos;
in vec2 TexCoords;

out vec4 FragColor;

uniform vec3 lightPos;
uniform vec3 viewPos;
uniform sampler2D starTexture;

void main() {
    
    float ambientStrength = 0.3;
    vec3 ambient = ambientStrength * vec3(1.0, 1.0, 1.0);
    
    
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * vec3(1.0, 1.0, 1.0);
    
    float specularStrength = 0.5;
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0); 
    vec3 specular = specularStrength * spec * vec3(1.0, 1.0, 1.0);
    
    vec4 texColor = texture(starTexture, TexCoords);
    vec3 result = (ambient + diffuse + specular) * texColor.rgb;
    FragColor = vec4(result, 1.0);
}
)";
#endif

class Exhaust {
public:
    struct ExhaustParticle {
        glm::vec3 pos;
        glm::vec3 velocity;
        float life;     
        float maxLife;  
    };

    Exhaust() = default;
    ~Exhaust() {
        glDeleteVertexArrays(1, &exhaustVAO);
        glDeleteBuffers(3, exhaustVBO);
    }

    std::vector<ExhaustParticle> exhaustParticles;
    
    const size_t maxExhaustParticles = 50;  

    void spawnExhaustParticle(const glm::vec3& shipPos, const glm::vec3& shipRotation) {
        glm::mat4 rotationMatrix(1.0f);
        rotationMatrix = glm::rotate(rotationMatrix, glm::radians(shipRotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
        rotationMatrix = glm::rotate(rotationMatrix, glm::radians(shipRotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
        rotationMatrix = glm::rotate(rotationMatrix, glm::radians(shipRotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
        
        glm::vec4 leftOffset1(0.29f, -0.05f, 0.25f, 1.0f);   
        glm::vec4 leftOffset2(0.29f, -0.02f, 0.25f, 1.0f);   
        
        glm::vec4 rightOffset1(-0.29f, -0.05f, 0.25f, 1.0f); 
        glm::vec4 rightOffset2(-0.29f, -0.02f, 0.25f, 1.0f);
        glm::vec3 leftPos1 = shipPos + glm::vec3(rotationMatrix * leftOffset1);
        glm::vec3 leftPos2 = shipPos + glm::vec3(rotationMatrix * leftOffset2);
        glm::vec3 rightPos1 = shipPos + glm::vec3(rotationMatrix * rightOffset1);
        glm::vec3 rightPos2 = shipPos + glm::vec3(rotationMatrix * rightOffset2);
        glm::vec4 backVector(0.0f, 0.0f, 1.0f, 0.0f); 
        glm::vec3 backward = glm::normalize(glm::vec3(rotationMatrix * backVector));
        float randomX = generateRandomFloat(-0.05f, 0.05f);
        float randomY = generateRandomFloat(-0.05f, 0.05f);
        float randomZ = generateRandomFloat(-0.05f, 0.05f);
        ExhaustParticle p1;
        p1.pos = leftPos1;
        p1.velocity = backward * 5.0f + glm::vec3(randomX, randomY, randomZ);
        p1.life = 0.0f;
        p1.maxLife = generateRandomFloat(0.8f, 1.2f);
        exhaustParticles.push_back(p1);
        randomX = generateRandomFloat(-0.1f, 0.1f);
        randomY = generateRandomFloat(-0.1f, 0.1f);
        randomZ = generateRandomFloat(-0.1f, 0.1f);
        ExhaustParticle p2;
        p2.pos = leftPos2;
        p2.velocity = backward * 5.0f + glm::vec3(randomX, randomY, randomZ);
        p2.life = 0.0f;
        p2.maxLife = generateRandomFloat(0.8f, 1.2f);
        exhaustParticles.push_back(p2);
        randomX = generateRandomFloat(-0.1f, 0.1f);
        randomY = generateRandomFloat(-0.1f, 0.1f);
        randomZ = generateRandomFloat(-0.1f, 0.1f);
        ExhaustParticle p3;
        p3.pos = rightPos1;
        p3.velocity = backward * 5.0f + glm::vec3(randomX, randomY, randomZ);
        p3.life = 0.0f;
        p3.maxLife = generateRandomFloat(0.8f, 1.2f);
        exhaustParticles.push_back(p3);
        randomX = generateRandomFloat(-0.1f, 0.1f);
        randomY = generateRandomFloat(-0.1f, 0.1f);
        randomZ = generateRandomFloat(-0.1f, 0.1f);
        ExhaustParticle p4;
        p4.pos = rightPos2;
        p4.velocity = backward * 5.0f + glm::vec3(randomX, randomY, randomZ);
        p4.life = 0.0f;
        p4.maxLife = generateRandomFloat(0.8f, 1.2f);
        exhaustParticles.push_back(p4);
        while (exhaustParticles.size() > maxExhaustParticles) {
            exhaustParticles.erase(exhaustParticles.begin());
        }
    }

    void updateExhaustParticles(float deltaTime) {
        for(auto& p : exhaustParticles) {
            p.pos += p.velocity * deltaTime;
            p.life += deltaTime;
        }
        exhaustParticles.erase(
            std::remove_if(exhaustParticles.begin(), exhaustParticles.end(),
                [](const ExhaustParticle& p) { return p.life >= p.maxLife; }),
            exhaustParticles.end()
        );
    }


    GLuint exhaustVAO = 0;
    GLuint exhaustVBO[3];
    gl::ShaderProgram exhaustShader;


    void initializeExhaustBuffers() {
        glGenVertexArrays(1, &exhaustVAO);
        glGenBuffers(3, exhaustVBO);
        glBindVertexArray(exhaustVAO);
        glBindBuffer(GL_ARRAY_BUFFER, exhaustVBO[0]);
        glBufferData(GL_ARRAY_BUFFER, maxExhaustParticles * 3 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);
        glBindBuffer(GL_ARRAY_BUFFER, exhaustVBO[1]);
        glBufferData(GL_ARRAY_BUFFER, maxExhaustParticles * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, (void*)0);
        glBindBuffer(GL_ARRAY_BUFFER, exhaustVBO[2]);
        glBufferData(GL_ARRAY_BUFFER, maxExhaustParticles * 4 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, (void*)0);
        glBindVertexArray(0);
    }

    void drawExhaustParticles(const glm::mat4& MVP, GLuint exhaustTexture) {
        if (exhaustParticles.empty()) return; 
        
        std::vector<float> positions;
        std::vector<float> sizes;
        std::vector<float> colors;
        positions.reserve(exhaustParticles.size() * 3);
        sizes.reserve(exhaustParticles.size());
        colors.reserve(exhaustParticles.size() * 4);
        
        for(auto& p : exhaustParticles) {
            positions.push_back(p.pos.x);
            positions.push_back(p.pos.y);
            positions.push_back(p.pos.z);
            
            float lifeFactor = 1.0f - (p.life / p.maxLife);
            sizes.push_back(20.0f * lifeFactor);  
            
            colors.push_back(1.0f);
            colors.push_back(0.6f + (lifeFactor * 0.3f)); 
            colors.push_back(0.1f);                      
            colors.push_back(lifeFactor * 1.2f);         
        }
        
        
        exhaustShader.useProgram();
        
        glBindBuffer(GL_ARRAY_BUFFER, exhaustVBO[0]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, positions.size() * sizeof(float), positions.data());
        
        glBindBuffer(GL_ARRAY_BUFFER, exhaustVBO[1]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizes.size() * sizeof(float), sizes.data());
        
        glBindBuffer(GL_ARRAY_BUFFER, exhaustVBO[2]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, colors.size() * sizeof(float), colors.data());
        
        
        exhaustShader.setUniform("MVP", MVP);
        exhaustShader.setUniform("spriteTexture", 0);
        
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, exhaustTexture);
        
        
        glBindVertexArray(exhaustVAO);
        glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(exhaustParticles.size()));
    }
};
class Projectiles {
    struct Projectile {
        glm::vec3 position;
        glm::vec3 velocity;
        float life;
        float maxLife;
    };

    std::vector<Projectile> projectiles;
    GLuint projectileVAO = 0;
    GLuint projectileVBO[3];
    gl::ShaderProgram projectileShader;
    glm::mat4 viewMatrix{1.0f};
    glm::mat4 projectionMatrix{1.0f};

public:
    GLuint texture = 0;

    ~Projectiles() {
        if(projectileVAO != 0) glDeleteVertexArrays(1, &projectileVAO);
        if(projectileVBO[0] != 0) glDeleteBuffers(3, projectileVBO);
        if(texture != 0) glDeleteTextures(1, &texture);
    }

    void load(gl::GLWindow *win) {
        const char* fullProjFrag = 
#ifdef __EMSCRIPTEN__
        R"(#version 300 es
        precision highp float;
        in float intensity;
        out vec4 FragColor;
        uniform sampler2D particleTexture;
            
        void main() {
            vec2 texCoord = gl_PointCoord;
            vec4 texColor = texture(particleTexture, texCoord);
            if(texColor.r < 0.7 && texColor.g < 0.7 && texColor.b < 0.7) discard;
            FragColor = vec4(texColor.rgb * intensity, texColor.a * intensity);
        })";
#else
        R"(#version 330 core
        in float intensity;
        out vec4 FragColor;
        uniform sampler2D particleTexture;
            
        void main() {
            vec2 texCoord = gl_PointCoord;
            vec4 texColor = texture(particleTexture, texCoord);
            if(texColor.r < 0.7 && texColor.g < 0.7 && texColor.b < 0.7) discard;
            FragColor = vec4(texColor.rgb * intensity, texColor.a * intensity);
        })";
#endif
const char* fullProjVert =
#ifdef __EMSCRIPTEN__
        R"(#version 300 es
        precision highp float;
        layout (location = 0) in vec3 aPos;
        layout (location = 1) in float aSize;
        layout (location = 2) in float aIntensity;

        uniform mat4 MVP;
        out float intensity;

        void main() {
            gl_Position = MVP * vec4(aPos, 1.0);
            gl_PointSize = aSize * 2.0;
            intensity = aIntensity;
        })";
#else
        R"(#version 330 core
        layout (location = 0) in vec3 aPos;
        layout (location = 1) in float aSize;
        layout (location = 2) in float aIntensity;

        uniform mat4 MVP;
        out float intensity;

        void main() {
            gl_Position = MVP * vec4(aPos, 1.0);
            gl_PointSize = aSize * 2.0;
            intensity = aIntensity;
        })";
#endif

        if (!projectileShader.loadProgramFromText(fullProjVert, fullProjFrag)) {
            throw mx::Exception("Failed to load projectile shader program");
        }

        texture = gl::loadTexture(win->util.getFilePath("data/fire.png"));

        glGenVertexArrays(1, &projectileVAO);
        glGenBuffers(3, projectileVBO);
        glBindVertexArray(projectileVAO);
        glBindBuffer(GL_ARRAY_BUFFER, projectileVBO[0]);
        glBufferData(GL_ARRAY_BUFFER, 100 * 3 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);
        glBindBuffer(GL_ARRAY_BUFFER, projectileVBO[1]);
        glBufferData(GL_ARRAY_BUFFER, 100 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, (void*)0);
        glBindBuffer(GL_ARRAY_BUFFER, projectileVBO[2]);
        glBufferData(GL_ARRAY_BUFFER, 100 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 0, (void*)0);
        glBindVertexArray(0);
    }

    void fire(const glm::vec3& position, const glm::vec3& direction, float speed = 50.0f) {
        Projectile projectile;
        projectile.position = position;
        projectile.velocity = direction * speed;
        projectile.life = 0.0f;
        projectile.maxLife = 1.5f; 
        projectiles.push_back(projectile);
        
        if (projectiles.size() > 100) {
            projectiles.erase(projectiles.begin());
        }
    }

    void setMatrices(const glm::mat4& view, const glm::mat4& projection) {
        viewMatrix = view;
        projectionMatrix = projection;
    }

    void draw(gl::GLWindow *win) {
        if (projectiles.empty()) return;
        
        std::vector<float> positions;
        std::vector<float> sizes;
        std::vector<float> intensities;
        
        positions.reserve(projectiles.size() * 3);
        sizes.reserve(projectiles.size());
        intensities.reserve(projectiles.size());
        
        for (const auto& projectile : projectiles) {
            positions.push_back(projectile.position.x);
            positions.push_back(projectile.position.y);
            positions.push_back(projectile.position.z);
            
        
            float sizeMultiplier = 1.0f + (projectile.life / projectile.maxLife) * 0.5f;
            sizes.push_back(32.0f * sizeMultiplier); 
            
        
            float lifeFactor = 1.0f - (projectile.life / projectile.maxLife);
            intensities.push_back(lifeFactor * lifeFactor); 
        }
        
        
        if (!positions.empty() && !sizes.empty() && !intensities.empty()) {
            glBindBuffer(GL_ARRAY_BUFFER, projectileVBO[0]);
            glBufferSubData(GL_ARRAY_BUFFER, 0, positions.size() * sizeof(float), positions.data());
            
            glBindBuffer(GL_ARRAY_BUFFER, projectileVBO[1]);
            glBufferSubData(GL_ARRAY_BUFFER, 0, sizes.size() * sizeof(float), sizes.data());
            
            glBindBuffer(GL_ARRAY_BUFFER, projectileVBO[2]);
            glBufferSubData(GL_ARRAY_BUFFER, 0, intensities.size() * sizeof(float), intensities.data());
            
            projectileShader.useProgram();
            
            glm::mat4 MVP = projectionMatrix * viewMatrix;
            projectileShader.setUniform("MVP", MVP);
            projectileShader.setUniform("particleTexture", 0);
            
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, texture);

#ifndef __EMSCRIPTEN__
            glEnable(GL_PROGRAM_POINT_SIZE);
#endif
            glEnable(GL_BLEND);
            glBlendFunc(GL_SRC_ALPHA, GL_ONE);
            glDepthMask(GL_FALSE);
            
            glBindVertexArray(projectileVAO);
            glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(projectiles.size()));
        
#ifndef __EMSCRIPTEN__
            glDisable(GL_PROGRAM_POINT_SIZE);
#endif
            glDepthMask(GL_TRUE);
            glDisable(GL_BLEND);
        }
    }

    void update(float deltaTime) {
        for (auto& projectile : projectiles) {
            projectile.position += projectile.velocity * deltaTime;
            projectile.life += deltaTime;
        }
        
        projectiles.erase(
            std::remove_if(projectiles.begin(), projectiles.end(),
                [](const Projectile& p) { return p.life >= p.maxLife; }),
            projectiles.end()
        );
    }
    
    bool checkCollision(const glm::vec3& position, float radius) {
        for (auto it = projectiles.begin(); it != projectiles.end(); ) {
            if (glm::length(it->position - position) < radius) {
                it = projectiles.erase(it);
                return true;
            } else {
                ++it;
            }
        }
        return false;
    }
    
    void clear() {
        projectiles.clear();
    }
};

class StarFighter {
public:
    Exhaust exhaust;
    Projectiles projectiles;
    GLuint exhaustTexture = 0;
    
    float minSpeed = 0.1f;
    float currentSpeed = 1.0f;

    glm::vec3 position{0.0f, 0.0f, 0.0f};
    glm::vec3 rotation{0.0f, 0.0f, 0.0f};
    glm::vec3 velocity{0.0f, 0.0f, 0.0f};
    
    float speed = 10.0f;         
    float turnSpeed = 100.0f;    
    float maxSpeed = 20.0f;      
    float drag = 0.95f;
    
    float cameraDistance = 5.0f;
    float cameraHeight = 1.5f;
    float cameraLag = 0.1f;
    glm::vec3 cameraPosition{0.0f, 0.0f, 0.0f};
    
    void load(gl::GLWindow *win) {
        if(!model.openModel(win->util.getFilePath("data/bird.mxmod.z"))) {
            throw mx::Exception("Failed to load model");
        }
        if(!shader.loadProgramFromText(star_vSource, star_fSource)) {
            throw mx::Exception("Failed to load shader program");
        }
        model.setShaderProgram(&shader, "starTexture");
        model.setTextures(win, win->util.getFilePath("data/bird.tex"), win->util.getFilePath("data"));
        
        position = glm::vec3(0.0f, 0.0f, 0.0f);
        rotation = glm::vec3(0.0f, 0.0f, 0.0f);
        velocity = glm::vec3(0.0f, 0.0f, 0.0f);
        
        exhaust.initializeExhaustBuffers();
        if(!exhaust.exhaustShader.loadProgramFromText(vertSource, fragSource)) {
            throw mx::Exception("Failed to load exhaust shader program");
        }
        
        exhaustTexture = gl::loadTexture(win->util.getFilePath("data/ember.png"));
        if (exhaustTexture == 0) {
            throw mx::Exception("Failed to load exhaust texture");
        }
        projectiles.load(win);
    }

    void moveForward(float deltaTime) {
        glm::mat4 rotationMatrix(1.0f);
        rotationMatrix = glm::rotate(rotationMatrix, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); // Yaw
        rotationMatrix = glm::rotate(rotationMatrix, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); // Pitch
        
        glm::vec3 forward = glm::normalize(glm::vec3(
            -sin(glm::radians(rotation.y)),
            sin(glm::radians(rotation.x)),
            -cos(glm::radians(rotation.y)) * cos(glm::radians(rotation.x))
        ));
        
        velocity += forward * speed * deltaTime;
        float currentSpeed = glm::length(velocity);
        if (currentSpeed > maxSpeed) {
            velocity = velocity * (maxSpeed / currentSpeed);
        }
    }
    
    void moveBackward(float deltaTime) {
        glm::vec3 forward = glm::normalize(glm::vec3(
            -sin(glm::radians(rotation.y)),
            sin(glm::radians(rotation.x)),
            -cos(glm::radians(rotation.y)) * cos(glm::radians(rotation.x))
        ));
        
        velocity -= forward * speed * deltaTime;
        
        float currentSpeed = glm::length(velocity);
        if (currentSpeed > maxSpeed) {
            velocity = velocity * (maxSpeed / currentSpeed);
        }
    }
    
    void yaw(float amount, float deltaTime) {
        rotation.y += amount * turnSpeed * deltaTime;
        
        if (rotation.y > 360.0f) rotation.y -= 360.0f;
        if (rotation.y < 0.0f) rotation.y += 360.0f;
    }
    
    void pitch(float amount, float deltaTime) {
        rotation.x += amount * turnSpeed * deltaTime;
        rotation.x = glm::clamp(rotation.x, -80.0f, 80.0f);
    }
    
    void roll(float amount, float deltaTime) {
        rotation.z += amount * turnSpeed * deltaTime;
        
        if (amount == 0.0f) {
            if (fabs(rotation.z) < 1.0f) {
                rotation.z = 0.0f;
            } else if (rotation.z > 0.0f) {
                rotation.z -= 50.0f * deltaTime;
            } else {
                rotation.z += 50.0f * deltaTime;
            }
        }
        
        rotation.z = glm::clamp(rotation.z, -45.0f, 45.0f);
    }

    void update(float deltaTime) {
        glm::vec3 forward = glm::normalize(glm::vec3(
            -sin(glm::radians(rotation.y)),
            sin(glm::radians(rotation.x)),
            -cos(glm::radians(rotation.y)) * cos(glm::radians(rotation.x))
        ));
        velocity = forward * currentSpeed;
        position += velocity * deltaTime;
    
        updateCamera(deltaTime);
        
        static float exhaustTimer = 0.0f;
        exhaustTimer += deltaTime;
        
        float spawnInterval = 0.05f - (currentSpeed / maxSpeed) * 0.04f; 
        
        if (exhaustTimer >= spawnInterval && currentSpeed > minSpeed * 1.2f) {
            exhaust.spawnExhaustParticle(position, rotation);
            exhaustTimer = 0.0f;
        }
        
        exhaust.updateExhaustParticles(deltaTime);
        projectiles.update(deltaTime);
    }

    void increaseSpeed(float deltaTime) {
        currentSpeed += speed * deltaTime * 2.0f;
        if (currentSpeed > maxSpeed) {
            currentSpeed = maxSpeed;
        }
    }

    void decreaseSpeed(float deltaTime) {
        currentSpeed -= speed * deltaTime * 2.0f;
        if (currentSpeed < minSpeed) {
            currentSpeed = minSpeed;
        }
    }
    
    void updateCamera(float deltaTime) {
        
        glm::vec3 forward = glm::normalize(glm::vec3(
            -sin(glm::radians(rotation.y)),
            sin(glm::radians(rotation.x)),
            -cos(glm::radians(rotation.y)) * cos(glm::radians(rotation.x))
        ));
        
        glm::vec3 idealCameraPos = position - forward * cameraDistance + glm::vec3(0.0f, cameraHeight, 0.0f);
        
        
        cameraPosition = glm::mix(cameraPosition, idealCameraPos, 1.0f - exp(-deltaTime * 10.0f));
    }
    
    glm::mat4 getViewMatrix() {
        return glm::lookAt(
            cameraPosition,                      
            position,                            
            glm::vec3(0.0f, 1.0f, 0.0f)          
        );
    }

    void draw(gl::GLWindow *win, const glm::mat4& projection, const glm::vec3& lightPos) {
        glm::mat4 viewMatrix = getViewMatrix();     
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE);
        glDepthMask(GL_FALSE);
#ifndef __EMSCRIPTEN__
        glEnable(GL_PROGRAM_POINT_SIZE);
#endif
        
        glm::mat4 MVP = projection * viewMatrix;
        exhaust.drawExhaustParticles(MVP, exhaustTexture);
        
#ifndef __EMSCRIPTEN__
        glDisable(GL_PROGRAM_POINT_SIZE);
#endif
        glDepthMask(GL_TRUE);
        glDisable(GL_BLEND);
        
        shader.useProgram();
        
        glm::mat4 modelMatrix = glm::mat4(1.0f);
        modelMatrix = glm::translate(modelMatrix, position);
        
        modelMatrix = glm::rotate(modelMatrix, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
        modelMatrix = glm::rotate(modelMatrix, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
        modelMatrix = glm::rotate(modelMatrix, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
        
        modelMatrix = glm::scale(modelMatrix, glm::vec3(0.5f, 0.5f, 0.5f));
        
        shader.setUniform("model", modelMatrix);
        shader.setUniform("view", viewMatrix);
        shader.setUniform("projection", projection);
        shader.setUniform("lightPos", lightPos);
        shader.setUniform("viewPos", cameraPosition);
        model.drawArrays();
        projectiles.setMatrices(viewMatrix, projection);
        projectiles.draw(win);
    }

    void fireProjectile() {
        glm::vec3 forward = glm::normalize(glm::vec3(
            -sin(glm::radians(rotation.y)),
            sin(glm::radians(rotation.x)),
            -cos(glm::radians(rotation.y)) * cos(glm::radians(rotation.x))
        ));
        
        glm::vec3 projectilePos = position + forward * 0.6f;
        projectiles.fire(projectilePos, forward, 50.0f + currentSpeed);
    }

protected:
    mx::Model model;
    gl::ShaderProgram shader;
};

class Planet {
public:
    Planet() = default;
    
    Planet(const Planet&) = delete;
    Planet& operator=(const Planet&) = delete;
    
    Planet(Planet&& other) noexcept 
        : position(other.position), 
          rotationSpeed(other.rotationSpeed),
          rotationAngle(other.rotationAngle),
          scale(other.scale),
          time_f(other.time_f),
          isDestroyed(other.isDestroyed),
          radius(other.radius) {}
          
    Planet& operator=(Planet&& other) noexcept {
        if (this != &other) {
            position = other.position;
            rotationSpeed = other.rotationSpeed;
            rotationAngle = other.rotationAngle;
            scale = other.scale;
            time_f = other.time_f;
            isDestroyed = other.isDestroyed;
            radius = other.radius;
        }
        return *this;
    }
    
    ~Planet() = default;

    glm::vec3 position{0.0f, 0.0f, -30.0f}; 
    float rotationSpeed = 75.0f;             
    float rotationAngle = 0.0f;
    float scale = 5.0f;                     
    float time_f = 0.0f;  
    int planet_type = 0;
    bool isDestroyed = false;
    float radius = 5.0f;

    float getRadius() const {
         return radius;
    }

    void load(gl::GLWindow *win) {
        static bool loaded = false;
        if(loaded == false) {
            models[0] = std::make_unique<mx::Model>();
            if(!models[0]->openModel(win->util.getFilePath("data/saturn.mxmod.z"))) {
                    throw mx::Exception("Failed to load planet model");
            }
            models[0]->setTextures(win, win->util.getFilePath("data/planet.tex"), win->util.getFilePath("data"));
            models[1] = std::make_unique<mx::Model>();
            if(!models[1]->openModel(win->util.getFilePath("data/star.mxmod.z"))) {
                    throw mx::Exception("Failed to load star planet model");
            }
            models[1]->setTextures(win, win->util.getFilePath("data/star.tex"), win->util.getFilePath("data"));
            models[2] = std::make_unique<mx::Model>();
            if(!models[2]->openModel(win->util.getFilePath("data/torus.mxmod.z"))) {
                    throw mx::Exception("Failed to load star planet model");
            }
            models[2]->setTextures(win, win->util.getFilePath("data/star.tex"), win->util.getFilePath("data"));
            models[3] = std::make_unique<mx::Model>();
            if(!models[3]->openModel(win->util.getFilePath("data/asteroid.mxmod.z"))) {
                    throw mx::Exception("Failed to load star planet model");
            }
            models[3]->setTextures(win, win->util.getFilePath("data/rock.tex"), win->util.getFilePath("data"));
              
            shader = std::make_unique<gl::ShaderProgram>();
            if(!shader->loadProgramFromText(g_vSource, g_fSource)) {
                throw mx::Exception("Failed to load planet shader program");
            }    
            models[0]->setShaderProgram(shader.get(), "texture1");
            models[1]->setShaderProgram(shader.get(), "texture1");
            models[2]->setShaderProgram(shader.get(), "texture1");
            models[3]->setShaderProgram(shader.get(), "texture1");
            loaded = true;
        }
        planet_type = generateRandomInt(0, 3);
        shader->useProgram();
    }

    void update(float deltaTime) {
        rotationAngle += rotationSpeed * deltaTime;
        if(rotationAngle > 360.0f) rotationAngle -= 360.0f;
        
        
        time_f += deltaTime;
        if(time_f > 10000.0f) time_f = 0.0f; 
    }

    void draw(const glm::mat4& view, const glm::mat4& projection, const glm::vec3& lightPos, const glm::vec3& viewPos) {
        if (isDestroyed) return;

        float distance = glm::length(position - viewPos);
        if (distance > 280.0f) return;
        float apparentScale = scale;
        if (distance > 100.0f) {
            apparentScale = scale * (1.0f + (distance - 100.0f) * 0.01f);
        }

        float modelScale = (planet_type == 2) ? (apparentScale / 6.0f) : apparentScale;
        
        shader->useProgram();
        
        glm::mat4 modelMatrix = glm::mat4(1.0f);
        modelMatrix = glm::translate(modelMatrix, position);
        modelMatrix = glm::rotate(modelMatrix, glm::radians(rotationAngle), glm::vec3(0.0f, 1.0f, 0.0f));
        modelMatrix = glm::scale(modelMatrix, glm::vec3(modelScale, modelScale, modelScale));
        
        shader->setUniform("model", modelMatrix);
        shader->setUniform("view", view);
        shader->setUniform("projection", projection);
        shader->setUniform("time_f", time_f);
        shader->setUniform("effect_type", planet_type);
        models[planet_type]->drawArrays();
    }

    void reset() {
        isDestroyed = false;
    }

    static void cleanupResources() {
        for (auto &model : models) {
            if (model) {
                model.reset();
            }
        }
        if (shader) {
            shader.reset();
        }
    }

private:
    static std::unique_ptr<mx::Model> models[4];
    static std::unique_ptr<gl::ShaderProgram> shader;
};

std::unique_ptr<mx::Model> Planet::models[4] = {nullptr, nullptr, nullptr, nullptr};
std::unique_ptr<gl::ShaderProgram> Planet::shader = nullptr;

class Game : public gl::GLObject {
    ExplodeEmiter emiter;
    StarField field;
    StarFighter ship;  
    std::vector<Planet> planets;    
    static const int NUM_PLANETS = 7;
    GLuint texture = 0;
    mx::Controller controller;
public:
    Game() = default;
    virtual ~Game() override {
        planets.clear();
        Planet::cleanupResources();
        if(texture)
            glDeleteTextures(1, &texture);
    }

    void load(gl::GLWindow *win) override {
        font.loadFont(win->util.getFilePath("data/font.ttf"), 18);
        emiter.load(win);
        emiter.setTextureID(gl::loadTexture(win->util.getFilePath("data/star.png")));        
        field.load(win);
        ship.load(win);  
        
        planets.resize(NUM_PLANETS);
        for (auto& planet : planets) {
            planet.load(win);
        }
        
        randomizePlanetPositions();
        
        field.repositionStarsAroundCamera(ship.cameraPosition);
    }
    
    void randomizePlanetPositions() {
        const float minX = -100.0f, maxX = 100.0f;
        const float minY = -50.0f, maxY = 50.0f;
        const float minZ = -100.0f, maxZ = 100.0f;
        const float minDistanceBetweenPlanets = 20.0f;
        
        for (int i = 0; i < static_cast<int>(planets.size()); i++) {
            bool validPosition = false;
            glm::vec3 newPos;
        
            planets[i].planet_type = generateRandomInt(0, 3);
         
            while (!validPosition) {
                newPos = glm::vec3(
                    generateRandomFloat(minX, maxX),
                    generateRandomFloat(minY, maxY),
                    generateRandomFloat(minZ, maxZ)
                );
                
                if (glm::length(newPos - ship.position) < 30.0f) {
                    continue;
                }
                
                validPosition = true;
                for (int j = 0; j < i; j++) {
                    if (glm::length(newPos - planets[j].position) < minDistanceBetweenPlanets) {
                        validPosition = false;
                        break;
                    }
                }
            }
            
            planets[i].position = newPos;
            planets[i].radius = generateRandomFloat(3.0f, 7.0f);
            planets[i].scale = planets[i].radius;
            planets[i].rotationSpeed = generateRandomFloat(1.0f, 5.0f);
            planets[i].rotationAngle = generateRandomFloat(0.0f, 360.0f);
            planets[i].isDestroyed = false;
        }
    }

    void draw(gl::GLWindow *win) override {
        glEnable(GL_DEPTH_TEST);
        glDepthFunc(GL_LESS);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        
        Uint32 currentTime = SDL_GetTicks();
        float deltaTime = (currentTime - lastUpdateTime) / 1000.0f; 
        lastUpdateTime = currentTime;
        
        if (deltaTime > 0.1f) deltaTime = 0.1f;
        
        handleInput(win, deltaTime);
        
        ship.update(deltaTime);
        
        for (auto& planet : planets) {
            planet.update(deltaTime);
        }
        
        float aspectRatio = static_cast<float>(win->w) / static_cast<float>(win->h);
        projectionMatrix = glm::perspective(glm::radians(45.0f), aspectRatio, 0.1f, 300.0f);
        
        viewMatrix = ship.getViewMatrix();
        
        field.setViewProjectionMatrices(viewMatrix, projectionMatrix);  
        emiter.setProjectionMatrix(projectionMatrix);
        emiter.setViewMatrix(viewMatrix);
        
        field.draw(win);
        field.update(deltaTime);
        glEnable(GL_DEPTH_TEST);
        for (auto& planet : planets) {
            planet.draw(viewMatrix, projectionMatrix, lightPos, ship.cameraPosition);
        }
        
        ship.draw(win, projectionMatrix, lightPos);
        
        for (auto& planet : planets) {
            if (!planet.isDestroyed) {
                if (ship.projectiles.checkCollision(planet.position, planet.getRadius())) {
                    emiter.reset();
                    emiter.position = planet.position;
                    emiter.explode();
                    planet.isDestroyed = true;
                    
                }
                
                float distance = glm::length(ship.position - planet.position);
                if (distance < planet.getRadius() + 1.0f) {
                    emiter.reset();
                    emiter.position = planet.position;
                    emiter.explode();
                    planet.isDestroyed = true;
                    
                }
            }
        }

        emiter.update(deltaTime);
        emiter.draw(win);
        if(debug_menu) {
            win->text.setColor({255,255,255,255});
            win->text.printText_Solid(font,25.0f,25.0f, "Ship X,Y,Z: " + std::to_string(ship.position.x) + ", " + std::to_string(ship.position.y) + ", " + std::to_string(ship.position.z));
            win->text.printText_Solid(font,25.0f,50.0f, "Velocity X,Y,Z: " + std::to_string(ship.velocity.x) + ", " + std::to_string(ship.velocity.y) + ", " + std::to_string(ship.velocity.z)); 
            win->text.printText_Solid(font,25.0f,75.0f, "FPS: " + std::to_string(1.0f / deltaTime));
            
            int destroyedCount = 0;
            for (const auto& planet : planets) {
                if (planet.isDestroyed) destroyedCount++;
            }
            win->text.printText_Solid(font,25.0f,100.0f, "Objects destroyed: " + std::to_string(destroyedCount) + "/" + std::to_string(NUM_PLANETS));
            win->text.printText_Solid(font,25.0f,125.0f, "Controls: Arrows to Move, W,S Tilt Up/Down - SPACE to shoot");
            
            if (!planets.empty()) {
                float closestPlanet = 999999.0f;
                float farthestPlanet = 0.0f;
                
                for (const auto& planet : planets) {
                    float dist = glm::length(ship.position - planet.position);
                    closestPlanet = std::min(closestPlanet, dist);
                    farthestPlanet = std::max(farthestPlanet, dist);
                }
                
                win->text.printText_Solid(font,25.0f,150.0f, "Nearest Object: " + std::to_string(closestPlanet));
                win->text.printText_Solid(font,25.0f,175.0f, "Farthest Object: " + std::to_string(farthestPlanet));
            }
            win->text.printText_Solid(font, 25.0f, 200.0f, 
                "Speed: " + std::to_string(ship.currentSpeed) + " / " + std::to_string(ship.maxSpeed));
            std::string con_str = controller.active() ? ("Connected: " + controller.name()) : "Disconnected";
            win->text.printText_Solid(font, 25.0f, 225.0f, "Controller: " + con_str);
            win->text.printText_Solid(font, 25.0f, 250.0f, "Press ENTER to randomize planets");
        }
    }
    
    void handleInput(gl::GLWindow* win, float deltaTime) {
        const Uint8* state = SDL_GetKeyboardState(NULL);
        Uint32 currentTime = SDL_GetTicks();
        if (state[SDL_SCANCODE_SPACE]) {
            if (currentTime - lastFireTime >= FIRE_COOLDOWN) {
                ship.fireProjectile();
                lastFireTime = currentTime;
            }
        }
        if (controller.getButton(SDL_CONTROLLER_BUTTON_A)) {
            if (currentTime - lastFireTime >= FIRE_COOLDOWN) {        
                ship.fireProjectile();
                lastFireTime = currentTime;
            }
        } 
        if(controller.getButton(SDL_CONTROLLER_BUTTON_LEFTSHOULDER)) {
            ship.increaseSpeed(deltaTime); 
        }
        else if(controller.getButton(SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)) {
            ship.decreaseSpeed(deltaTime); 
        }
        if (state[SDL_SCANCODE_UP]) {
            ship.increaseSpeed(deltaTime); 
        }
        else if (state[SDL_SCANCODE_DOWN]) {
            ship.decreaseSpeed(deltaTime); 
        }
        else {   
            if (ship.currentSpeed > 5.0f) {
                ship.decreaseSpeed(deltaTime * 0.5f);
            }
            else if (ship.currentSpeed < 5.0f) {
                ship.increaseSpeed(deltaTime * 0.5f);
            }
        }
        if (state[SDL_SCANCODE_LEFT]) {
            ship.yaw(1.0f, deltaTime);
            ship.roll(-1.0f, deltaTime);  
        }
        else if (state[SDL_SCANCODE_RIGHT]) {
            ship.yaw(-1.0f, deltaTime);
            ship.roll(1.0f, deltaTime);   
        }
        else {
            ship.roll(0.0f, deltaTime);   
        }
        if (state[SDL_SCANCODE_W]) {
            ship.pitch(-1.0f, deltaTime); 
        }
        if (state[SDL_SCANCODE_S]) {
            ship.pitch(1.0f, deltaTime); 
        }
        if (state[SDL_SCANCODE_RETURN]) {
            randomizePlanetPositions();
            emiter.reset();
        }
        if(controller.getAxis(SDL_CONTROLLER_AXIS_RIGHTY) < -0.5f) {
            ship.pitch(-1.0f, deltaTime); 
        }
        else if(controller.getAxis(SDL_CONTROLLER_AXIS_RIGHTY) > 0.5f) {
            ship.pitch(1.0f, deltaTime);  
        }
        if(controller.getAxis(SDL_CONTROLLER_AXIS_LEFTX) > 0.5f) {
            ship.yaw(-1.0f, deltaTime);   
            ship.roll(1.0f, deltaTime);   
        }
        else if(controller.getAxis(SDL_CONTROLLER_AXIS_LEFTX) < -0.5f) {
            ship.yaw(1.0f, deltaTime);    
            ship.roll(-1.0f, deltaTime);  
        }
    }
    
    void event(gl::GLWindow *win, SDL_Event &e) override {
        switch(e.type) {
            case SDL_KEYDOWN:
                if(e.key.keysym.sym == SDLK_RETURN) {
                    randomizePlanetPositions();
                    emiter.reset();
                } else if(e.key.keysym.sym == SDLK_F1) {
                    debug_menu = !debug_menu;
                }
                break;
        }

        if(controller.connectEvent(e)) {
            mx::system_out << "Controller connected...\n";
        }
    }
    
private:
    mx::Font font;
    Uint32 lastUpdateTime = SDL_GetTicks();
    Uint32 lastFireTime = 0;           
    const Uint32 FIRE_COOLDOWN = 200;  
    glm::mat4 viewMatrix{1.0f};
    glm::mat4 projectionMatrix{1.0f};
    glm::vec3 lightPos{10.0f, 10.0f, 10.0f};
    bool debug_menu = true;
};

class MainWindow : public gl::GLWindow {
public:
    MainWindow(std::string path, int tw, int th) : gl::GLWindow("Outer Space", tw, th) {
        setPath(path);
        setObject(new Intro());
        object->load(this);
    }
    
    ~MainWindow() override {}
    
    virtual void event(SDL_Event &e) override {}
    
    virtual void draw() override {
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glEnable(GL_DEPTH_TEST);
        glViewport(0, 0, w, h);
        object->draw(this);
        swap();
        delay();
    }
};

void Intro::draw(gl::GLWindow *win) {
    glDisable(GL_DEPTH_TEST);
    #ifndef __EMSCRIPTEN__
        Uint32 currentTime = SDL_GetTicks();
    #else
        double currentTime = emscripten_get_now();
    #endif
        shader.useProgram();
        shader.setUniform("alpha", fade);
        shader.setUniform("time_f", SDL_GetTicks() / 1000.0f);
        intro.draw();
        if((currentTime - lastUpdateTime) > 35) {
            lastUpdateTime = currentTime;
            fade -= .01;
        }
        if(fade <= 0.0) {
            win->setObject(new Game());
            win->object->load(win);
            return;
        }
        intro.draw();
        if(fade <= 0.1) {
            win->text.setColor({255,255,255,255});
            win->text.printText_Solid(font, 25.0f, 25.0f, "Loading ...");
        }
}

MainWindow *main_w = nullptr;

void eventProc() {
    main_w->proc();
}

int main(int argc, char **argv) {
#ifdef __EMSCRIPTEN__
    try {
        MainWindow main_window("", 1920, 1080);
        main_w = &main_window;
        emscripten_set_main_loop(eventProc, 0, 1);
    } catch(mx::Exception &e) {
        mx::system_err << "mx: Exception: " << e.text() << "\n";
        mx::system_err.flush();
        exit(EXIT_FAILURE);
    }
#else
    Arguments args = proc_args(argc, argv);
    try {
        MainWindow main_window(args.path, args.width, args.height);
        if(args.fullscreen)
		main_window.setFullScreen(true);
        main_window.loop();
    } catch(const mx::Exception &e) {
        mx::system_err << "mx: Exception: " << e.text() << "\n";
        mx::system_err.flush();
        exit(EXIT_FAILURE);
    }
#endif
    return 0;
}