ICT290 / src / scene / theArchanist / theArchanist.cpp
theArchanist.cpp
Raw
#include "theArchanist.h"
#include <random>
#include "gui/gameOver.h"
#include "gui/hud.h"
#include "gui/pause.h"

// Static variables
std::shared_ptr<State> theArchanist::m_thisState{nullptr};
theArchanistSounds theArchanist::m_sounds{};
theArchanistLights theArchanist::m_light{};
gameMap theArchanist::m_map{};
GameWorld theArchanist::m_WorldController{};
GameInfo theArchanist::m_gameInfo{};
Player theArchanist::m_player{};
std::list<std::pair<Object, glm::vec3>> theArchanist::m_collisionObjects{};

std::shared_ptr<State> theArchanist::init(Resources& engineResources,
                                          int seed,
                                          int levels,
                                          int difficulty) {
    m_thisState = std::make_shared<State>(draw,
                                          handleEvents,
                                          destroy,
                                          pause,
                                          update,
                                          engineResources);

    setCamera();

    pause(false);

    m_thisState->audioManager.playSound(m_sounds.music);

    m_gameInfo.seed = seed;
    if (levels > 0) {
        m_gameInfo.lastLevel = levels;
    }
    m_gameInfo.difficulty = (Difficulty)difficulty;
    setDifficultySettings();

    newLevel(10);

    m_thisState->skybox.setModel(
        m_thisState->resources.modelManager.getModel("as2/spaceSkybox.obj"));

    return m_thisState;
}

void theArchanist::destroy() {
    pause(true);
    m_WorldController = GameWorld{};
    m_thisState = nullptr;
    m_gameInfo = GameInfo{};
    m_collisionObjects.clear();
    m_player = Player{};
}

void theArchanist::pause(bool pause) {
    if (pause) {
        Light::disableLighting();
        m_thisState->audioManager.pauseSounds(true);
        ARCH_GUI::resetPauseState();
        m_gameInfo.paused = false;
    } else {
        glClearColor(0.1f, 0.1f, 0.1f, 1.f);

        Light::enableLighting();

        m_thisState->lights[m_light.L1].setPosition(0, 10, 0);
        m_thisState->lights[m_light.L2].setPosition(10, 10, 10);
        m_thisState->lights[m_light.L1].enable();
        m_thisState->lights[m_light.L2].enable();

        m_thisState->camera.sensitivity = m_thisState->resources.settings
                                              .mouseSens;
        m_thisState->audioManager.setMasterVolume(
            m_thisState->resources.settings.volume);
        m_thisState->audioManager.setMute(m_thisState->resources.settings.mute);

        // Set camera perspective
        m_thisState->camera
            .setPerspective(45.f,
                            (float)m_thisState->resources.settings.width
                                / (float)m_thisState->resources.settings.height,
                            0.1f,
                            10000.f);

        m_thisState->audioManager.pauseSounds(false);
    }
}

// testing globals
bool drawRoomDebug{false};
bool drawAIDebug{false};
float testMana{100};

void theArchanist::handleEvents(SDL_Event& event) {
    if (m_gameInfo.paused) {
        m_thisState->camera.lockMouse(false);
        if (ARCH_GUI::pauseState.resume) {
            ARCH_GUI::resetPauseState();
            m_thisState->camera.lockMouse(true);
            m_gameInfo.paused = false;
        }
        if (ARCH_GUI::pauseState.exit) {
            ARCH_GUI::resetPauseState();
            m_thisState->stop = true;
        }
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_KEYDOWN
                && event.key.keysym.sym == SDLK_ESCAPE) {
                ARCH_GUI::resetPauseState();
                m_gameInfo.paused = !m_gameInfo.paused;
                break;
            }
            ImGui_ImplSDL2_ProcessEvent(&event);
        }
    } else {
        while (SDL_PollEvent(&event)) {
            switch (event.type) {
                case SDL_QUIT:
                    exit(0);
                case SDL_KEYDOWN:
                    if (event.key.keysym.sym == SDLK_SPACE) {
                        m_thisState->camera.lockMouse(
                            !m_thisState->camera.isLocked());
                    } else if (event.key.keysym.sym == SDLK_ESCAPE) {
                        m_gameInfo.paused = true;
                        break;
                    } else if (event.key.keysym.sym == SDLK_e) {
                        std::cout << m_thisState->camera.position.x << ' '
                                  << m_thisState->camera.position.y << ' '
                                  << m_thisState->camera.position.z << '\n';
                    } else if (event.key.keysym.sym == SDLK_q) {
                        m_map.m_debug = !m_map.m_debug;
                        drawRoomDebug = !drawRoomDebug;
                        drawAIDebug = !drawAIDebug;
                    } else if (event.key.keysym.sym == SDLK_n) {
                        AITest::spawnDefaultBot();
                    }
                    break;
                case SDL_MOUSEWHEEL: {
                    if (event.wheel.y < 0 && event.wheel.y > -3) {
                        m_player.setZoom(m_player.getZoom()
                                         + 0.25f * (float)-event.wheel.y);
                    } else if (event.wheel.y > 0 && event.wheel.y < 3) {
                        m_player.setZoom(m_player.getZoom()
                                         - 0.25f * (float)event.wheel.y);
                    }
                    break;
                }
                case SDL_MOUSEBUTTONDOWN: {
                    switch (event.button.button) {
                        case SDL_BUTTON_RIGHT: {
                            if (testMana > 20) {
                                testMana -= 20;
                                AITest::shootTarget(clickWorldCoordinates());
                                // play shot sound
                                std::string name{"Player"};
                                auto obj = m_thisState->objects.find(name);
                                m_sounds.magicShot.position = obj->second
                                                                  .location;
                                m_thisState->audioManager.playSound(
                                    m_sounds.magicShot);
                                break;
                            }
                        }
                        default:
                            break;
                    }
                }
                default:
                    break;
            }
        }
        // For input that needs very fast updates
        const Uint8* keyStates = SDL_GetKeyboardState(nullptr);
        if (keyStates[SDL_SCANCODE_W]) {
            m_player.moveUp();
        }
        if (keyStates[SDL_SCANCODE_A]) {
            m_player.moveLeft();
        }
        if (keyStates[SDL_SCANCODE_S]) {
            m_player.moveDown();
        }
        if (keyStates[SDL_SCANCODE_D]) {
            m_player.moveRight();
        }
        if (keyStates[SDL_SCANCODE_LSHIFT]) {
            m_player.setMoveSpeed(5.f);
        } else {
            m_player.setMoveSpeed(2.5f);
        }
    }
}

void theArchanist::update(float deltaTime) {
    if (isGameOver()) {
        if (ARCH_GAME_OVER::mainMenu) {
            m_thisState->stop = true;
        }
        return;
    }

    if (!m_gameInfo.paused) {
        testMana += 12.f * deltaTime;
        if (testMana > 100) {
            testMana = 100;
        }
        m_gameInfo.time += deltaTime;

        trapDoorUpdate();

        auto playerObj = m_thisState->objects.find("Player")->second;
        Camera_ temp = m_thisState->camera;
        m_player.update(playerObj, temp, deltaTime);

        glm::vec2 newPos{playerObj.location.x, playerObj.location.z};

        if (m_WorldController.playerMoveCheck(newPos)) {
            m_WorldController.m_Vehicles.front()->setPosition(
                {playerObj.location.x, playerObj.location.z});
            m_thisState->objects.find("Player")->second = playerObj;
            m_thisState->camera = temp;
            updatePlayerObject(deltaTime);
        }

        m_thisState->audioManager.setListener(m_thisState->camera.position,
                                              m_thisState->camera.direction);

        m_WorldController.Update(deltaTime);

        updateEnemyObjects(deltaTime);
        updateProjectileObjects();
        updateCollisionParticles(deltaTime);
    }
    if (ARCH_GUI::click.occurred) {
        m_thisState->audioManager.playSound(m_sounds.click);
        ARCH_GUI::click.occurred = false;
    }
}

void theArchanist::draw() {
    Light::disableLighting();
    m_thisState->skybox.draw(m_thisState->camera.direction,
                             m_thisState->camera.up);
    Light::enableLighting();
    m_thisState->lights[m_light.L2].enable();
    m_thisState->lights[m_light.L1].enable();

    float drawDistance = 23 + m_player.getZoom();
    m_map.draw(m_thisState->camera.position, drawDistance);

    float camOffset{m_thisState->camera.position.z + 10};
    auto camPos{m_thisState->camera.position};

    for (auto& particle : m_collisionObjects) {
        if (camPos.z < camOffset
            && glm::distance(camPos, particle.first.location) < drawDistance) {
            particle.first.draw();
        }
    }

    if (!drawAIDebug) {
        for (auto& obj : m_thisState->objects) {
            if (!isTrapDoor(obj.first) && camPos.z < camOffset
                && glm::distance(camPos, obj.second.location) < drawDistance) {
                obj.second.draw();
            }
        }
        drawTrapDoor();
    } else {
        AITest::drawDebug();
    }

    m_thisState->lights[m_light.L2].draw();
    m_thisState->lights[m_light.L1].draw();

    if (isGameOver()) {
        ARCH_GAME_OVER::drawGameOver(m_gameInfo.time,
                                     m_gameInfo.currentLevel,
                                     m_gameInfo.lastLevel,
                                     (int)m_gameInfo.difficulty,
                                     m_gameInfo.enemiesKilled,
                                     m_gameInfo.victory,
                                     m_gameInfo.seed);
    } else if (m_gameInfo.paused) {
        ARCH_GUI::pauseMenu(m_thisState->resources);
    } else {
        // Example usage
        ARCH_HUD::drawHud(m_gameInfo.time,
                          (float)m_WorldController.m_Vehicles.front()
                                  ->getHealth()
                              / (float)m_WorldController.m_Vehicles.front()
                                    ->getMaxHealth(),
                          testMana / 100,
                          m_gameInfo.currentLevel,
                          m_gameInfo.lastLevel,
                          m_gameInfo.difficulty,
                          (int)m_WorldController.m_Vehicles.size() - 1);

        if (m_gameInfo.allEnemiesDead) {
            ARCH_HUD::drawTrapdoorMsg();
        }
    }
}

void theArchanist::setCamera() {
    // Set camera perspective
    m_thisState->camera
        .setPerspective(45.f,
                        (float)m_thisState->resources.settings.width
                            / (float)m_thisState->resources.settings.height,
                        0.1f,
                        10000.f);
    m_thisState->camera.pitch += 45;

    // Aligned at centre for new top down camera
    m_thisState->camera.position = glm::vec3{28, 5, 33};

    m_thisState->camera.lockMouse(false);
}

glm::vec3 theArchanist::clickWorldCoordinates() {
    int width, height, mouseX, mouseY;
    glm::vec3 addPos(0.0f);
    SDL_GetMouseState(&mouseX, &mouseY);
    SDL_GetWindowSize(SDL_GL_GetCurrentWindow(), &width, &height);
    float x = (2.0f * (float)mouseX) / (float)width - 1.0f;
    float y = 1.0f - (2.0f * (float)mouseY) / (float)height;
    float z = 1.0f;
    glm::vec3 ray_nds = glm::vec3(x, y, z);
    glm::vec4 ray_clip = glm::vec4(ray_nds.x, ray_nds.y, -1.0, 1.0);
    glm::vec4 ray_eye = glm::inverse(m_thisState->camera.perspectiveMatrix)
                        * ray_clip;
    ray_eye = glm::vec4(ray_eye.x, ray_eye.y, -1.0, 0.0);
    glm::vec4 ray_wor = (glm::inverse(m_thisState->camera.viewMatrix)
                         * ray_eye);
    glm::vec3 ray(ray_wor.x, ray_wor.y, ray_wor.z);
    ray = glm::normalize(ray);
    float distance;
    if (glm::intersectRayPlane(m_thisState->camera.position,
                               ray,
                               glm::vec3(0.f),
                               glm::vec3(0.f, 1.0f, 0.f),
                               distance)) {
        glm::vec3 point(ray.x, ray.y, ray.z);
        point *= glm::vec3(distance);
        addPos = m_thisState->camera.position + point;
    }
    return addPos;
}

void theArchanist::createPlayerObject() {
    auto& player = m_WorldController.m_Vehicles.front();

    std::string name{"Player"};
    glm::vec3 location{player->getPosition().x, 0.7f, player->getPosition().y};
    glm::vec3 scale{0.5f}, rotation{0.f};
    std::shared_ptr<Model> model = m_thisState->resources.modelManager.getModel(
        "as2/robe.obj");
    Object object{name, model, location, rotation, scale};
    m_thisState->objects.emplace(name, object);

    createShadow(player->getID(), 0.25f);
}

// temp animation
bool playerUp{true};

void theArchanist::updatePlayerObject(float deltaTime) {
    auto& player = m_WorldController.m_Vehicles.front();

    auto obj = m_thisState->objects.find("Player");
    glm::vec3 location{player->getPosition().x, 0.7f, player->getPosition().y};

    if (obj != m_thisState->objects.end()) {
        if (obj->second.location.y > 0.8f) {
            playerUp = false;
        } else if (obj->second.location.y < 0.6f) {
            playerUp = true;
        }
        location.y = (playerUp) ? obj->second.location.y + 0.2f * deltaTime
                                : obj->second.location.y - 0.2f * deltaTime;
        obj->second.location = location;
        updateShadow(player->getID(), location);
    }
}

void theArchanist::createShadow(int id, float scale) {
    std::string name = "Shadow_" + std::to_string(id);
    auto model = m_thisState->resources.modelManager.getModel("as2/shadow.obj");
    glm::vec3 location{0}, rotation{0}, shadowScale{scale};
    Object shadow{name, model, location, rotation, shadowScale};
    m_thisState->objects.emplace(name, shadow);
}

void theArchanist::updateShadow(int id, const glm::vec3& location) {
    auto shadow = m_thisState->objects.find("Shadow_" + std::to_string(id));
    if (shadow != m_thisState->objects.end()) {
        shadow->second.location = location;
        shadow->second.location.y = 0.08f;
    }
}

std::shared_ptr<Model> theArchanist::getRandomEnemyModel() {
    std::string modelName[] = {"as2/Bat.obj", "as2/Dragon.obj", "as2/Wasp.obj"};
    std::random_device rd{};
    std::mt19937 gen{rd()};
    std::uniform_int_distribution<> index{0, 2};
    return m_thisState->resources.modelManager.getModel(modelName[index(gen)]);
}

// temp animation vector
std::vector<bool> isUp;

void theArchanist::createEnemyObject(int id,
                                     const glm::vec3& location,
                                     const glm::vec3& rotation,
                                     const glm::vec3& scale) {
    auto model = getRandomEnemyModel();

    std::string name{"Enemy_" + std::to_string(id)};
    Object object{name, model, location, rotation, scale};
    m_thisState->objects.emplace(name, object);

    createShadow(id, 0.25f);

    // Animation bool
    isUp.push_back(true);
}

void theArchanist::updateEnemyObjects(float deltaTime) {
    bool isPlayer{true};
    std::size_t count{0};
    for (auto& enemy : m_WorldController.m_Vehicles) {
        if (isPlayer) {
            isPlayer = false;
            continue;
        }

        glm::vec3 location{enemy->getPosition().x, 1, enemy->getPosition().y};
        float angle = getHeadingAngle(enemy->getHeadingVec());
        auto obj = m_thisState->objects.find("Enemy_"
                                             + std::to_string(enemy->getID()));

        if (obj != m_thisState->objects.end()) {
            if (obj->second.location.y > 1.15f) {
                isUp[count] = false;
            } else if (obj->second.location.y < 0.8f) {
                isUp[count] = true;
            }
            location.y = (isUp[count])
                             ? obj->second.location.y + 0.6f * deltaTime
                             : obj->second.location.y - 0.6f * deltaTime;
            obj->second.location = location;
            obj->second.rotation.y = angle;
            updateShadow(enemy->getID(), location);
            ++count;
        } else {
            createEnemyObject(enemy->getID(),
                              location,
                              glm::vec3{0, angle, 0},
                              glm::vec3{0.2f});
        }
    }
    updateDeadEnemyObjects();
}

void theArchanist::updateDeadEnemyObjects() {
    for (auto& dead : m_WorldController.m_deadVehicles) {
        auto obj = m_thisState->objects.find("Enemy_" + std::to_string(dead));
        obj->second.rotation.x = 90;
        obj->second.rotation.y = 0;
        obj->second.location.y = 0;
        m_sounds.batDeath.position = obj->second.location;
        m_thisState->audioManager.playSound(m_sounds.batDeath);
        m_thisState->objects.erase("Shadow_" + std::to_string(dead));
        ++m_gameInfo.enemiesKilled;
    }
    m_WorldController.m_deadVehicles.clear();
}

float theArchanist::getHeadingAngle(const glm::vec2& heading) {
    float dot = glm::dot(glm::vec2{0, -1}, heading);
    float det = heading.x;

    return -glm::degrees(std::atan2(det, dot));
}

void theArchanist::createProjectileObj(int id,
                                       const glm::vec3& location,
                                       const glm::vec3& rotation,
                                       const glm::vec3& scale) {
    std::shared_ptr<Model> model = m_thisState->resources.modelManager.getModel(
        "as2/"
        "debugSphere.obj");
    std::string name{"Projectile_" + std::to_string(id)};
    Object object{name, model, location, rotation, scale};
    m_thisState->objects.emplace(name, object);
    createShadow(id, 0.15f);
    m_sounds.magicShot.position = location;
    m_thisState->audioManager.playSound(m_sounds.magicShot);
}

void theArchanist::updateProjectileObjects() {
    if (!m_WorldController.m_Projectiles.empty()) {
        for (auto& projectile : m_WorldController.m_Projectiles) {
            float angle = getHeadingAngle(projectile->getHeadingVec());

            glm::vec3 location{projectile->getPosition().x,
                               0.75f,
                               projectile->getPosition().y};

            auto obj = m_thisState->objects.find(
                "Projectile_" + std::to_string(projectile->getID()));

            if (obj != m_thisState->objects.end()) {
                obj->second.location = location;
                obj->second.rotation.y = angle;
                updateShadow(projectile->getID(), location);
            } else {
                createProjectileObj(projectile->getID(),
                                    location,
                                    glm::vec3{0, angle, 0},
                                    glm::vec3{0.1f});
            }
        }
    }
    destroyProjectileObjs();
}

void theArchanist::destroyProjectileObjs() {
    // remove dead projectiles
    float speed{0.5f};
    int amount{10};
    for (auto& deadProj : m_WorldController.m_deadProjectiles) {
        auto projectile = m_thisState->objects
                              .find("Projectile_"
                                    + std::to_string(deadProj.first))
                              ->second;
        createCollisionParticles(deadProj.first,
                                 projectile.location,
                                 projectile.scale,
                                 deadProj.second,
                                 projectile.model,
                                 amount,
                                 speed);
        m_sounds.explosion.position = projectile.location;
        m_thisState->audioManager.playSound(m_sounds.explosion);
        m_thisState->objects.erase("Projectile_"
                                   + std::to_string(deadProj.first));
        m_thisState->objects.erase("Shadow_" + std::to_string(deadProj.first));
    }
    m_WorldController.m_deadProjectiles.clear();
}

void theArchanist::drawTrapDoor() {
    if (m_WorldController.m_Vehicles.size() != 1) {
        m_thisState->objects.find("Trapdoor")->second.draw();
    } else {
        m_thisState->objects.find("Trapdoor_open")->second.draw();
        m_thisState->objects.find("Trapdoor_portal")->second.draw();
    }
}

void theArchanist::loadTrapDoorObj() {
    std::shared_ptr<Model> model = m_thisState->resources.modelManager.getModel(
        "as2/Trapdoor.obj");
    glm::vec3 rotation{0, 0, 0};
    glm::vec3 scale{0.75f};
    glm::vec3 location{28.5f, 0.1f, 28.5f};
    std::string name{"Trapdoor"};
    Object object{name, model, location, rotation, scale};
    m_thisState->objects.emplace(name, object);

    model = m_thisState->resources.modelManager.getModel(
        "as2/Trapdoor_open.obj");
    name = "Trapdoor_open";
    object = Object{name, model, location, rotation, scale};
    m_thisState->objects.emplace(name, object);

    model = m_thisState->resources.modelManager.getModel(
        "as2/Trapdoor_portal.obj");
    name = "Trapdoor_portal";
    object = Object{name, model, location, rotation, scale};
    m_thisState->objects.emplace(name, object);
}

bool theArchanist::isTrapDoor(const std::string& name) {
    return name == "Trapdoor" || name == "Trapdoor_portal"
           || name == "Trapdoor_open";
}

void theArchanist::trapDoorUpdate() {
    if (m_WorldController.m_Vehicles.size() == 1) {
        m_gameInfo.allEnemiesDead = true;
        auto& playerObj = m_thisState->objects.find("Player")->second;

        if (playerObj.location.x < 29 && playerObj.location.x > 28
            && playerObj.location.z < 29 && playerObj.location.z > 28) {
            m_gameInfo.playerHealth -= 50;
            m_gameInfo.spawnRate += 0.01f;
            newLevel(10);
        }
    }
}

void theArchanist::newLevel(int rooms) {
    std::string modelsFile{"../res/preload/roomModels.txt"};
    m_map = gameMap(rooms,
                    m_thisState->resources,
                    modelsFile,
                    m_gameInfo.seed + m_gameInfo.currentLevel * 113);

    ++m_gameInfo.currentLevel;
    m_gameInfo.allEnemiesDead = false;

    m_WorldController = GameWorld{};

    m_thisState->objects.clear();

    m_WorldController.init(m_map);

    AITest::Init(&m_WorldController);
    AITest::loadPlayer(m_gameInfo.playerHealth);
    AITest::loadBots(m_gameInfo.spawnRate, m_gameInfo.seed);

    createPlayerObject();
    loadTrapDoorObj();
}

void theArchanist::setDifficultySettings() {
    switch (m_gameInfo.difficulty) {
        case easy:
            m_gameInfo.spawnRate = 0.01f;
            m_gameInfo.playerHealth = 1000;
            break;
        case normal:
            m_gameInfo.spawnRate = 0.018f;
            m_gameInfo.playerHealth = 750;
            break;
        case hard:
            m_gameInfo.spawnRate = 0.025f;
            m_gameInfo.playerHealth = 500;
            break;
    }
}

bool theArchanist::isGameOver() {
    // TODO check boss is dead for victory, temp is level trans
    if (m_WorldController.m_Vehicles.front()->isDead()) {
        m_gameInfo.victory = false;
        return true;
    } else if (m_gameInfo.currentLevel > m_gameInfo.lastLevel) {
        m_gameInfo.victory = true;
        return true;
    }
    return false;
}

void theArchanist::createCollisionParticles(int id,
                                            const glm::vec3& location,
                                            const glm::vec3& scale,
                                            const glm::vec2& heading,
                                            std::shared_ptr<Model>& model,
                                            int amount,
                                            float speed) {
    std::random_device rd{};
    std::mt19937 gen{rd()};
    std::uniform_int_distribution<> pos{-3, 7};
    std::uniform_int_distribution<> neg{-7, 3};
    std::uniform_int_distribution<> yDist{-7, 4};
    for (int i = 0; i < amount; ++i) {
        int newId = id - 5000 - i;
        std::string name{"Particle_" + std::to_string(newId)};
        glm::vec3 velocity{heading.x, 1, heading.y};
        velocity.x += (heading.x > 0) ? (float)pos(gen) : (float)neg(gen);
        velocity.y += (float)yDist(gen);
        velocity.z += (heading.y > 0) ? (float)pos(gen) : (float)neg(gen);
        glm::normalize(velocity);
        velocity *= speed;
        Object particle{name, model, location, glm::vec3{0, 0, 0}, scale / 4.f};
        m_collisionObjects.emplace_back(particle, velocity);
        createShadow(newId, 0.05f);
    }
}

void theArchanist::updateCollisionParticles(float deltaTime) {
    float gravity = deltaTime * 9.8f;
    glm::vec3 resistance{0.95f, 0, 0.95f};
    auto particleItr = m_collisionObjects.begin();
    while (particleItr != m_collisionObjects.end()) {
        auto& particle = *particleItr;
        if (particle.first.location.y < 0.05f
            || !m_WorldController.currentLevel.isInBoundsTight(
                {particle.first.location.x, particle.first.location.z})) {
            m_thisState->objects.erase(
                "Shadow_"
                + particle.first.name.substr(9,
                                             particle.first.name.size() - 1));
            m_collisionObjects.erase(particleItr++);
        } else {
            particle.first.location += deltaTime * particle.second;
            particle.second.y -= gravity;
            particle.second -= particle.second * resistance * deltaTime;
            updateShadow(std::stoi(
                             particle.first.name
                                 .substr(9, particle.first.name.size() - 1)),
                         particle.first.location);
            ++particleItr;
        }
    }
}