#include <grrlib.h>
#include "objects.h"
#include "level_loading.h"
#include "stdio.h"
#include "object_includes.h"
#include "ground_line_png.h"
#include "levelCompleteText_png.h"
#include "main.h"
#include "math.h"
#include <math.h>
#include "game.h"
#include "custom_mp3player.h"
#include "player.h"
#include "trail.h"
#include "level.h"
#include "collision.h"
#include "easing.h"

#include <wiiuse/wpad.h>
#include "particles.h"
#include "particle_png.h"
#include <ogc/lwp_watchdog.h>

#include "triggers.h"

#include "groups.h"

AnimationDefinition monster_1_anim;
AnimationDefinition monster_2_anim;
AnimationDefinition monster_3_anim;
AnimationDefinition black_sludge_anim;

FramesDefinition fire1_anim;
FramesDefinition fire2_anim;
FramesDefinition fire3_anim;
FramesDefinition fire4_anim;

FramesDefinition water_1_anim;
FramesDefinition water_2_anim;
FramesDefinition water_3_anim;
FramesDefinition loading_1_anim;
FramesDefinition loading_2_anim;

GRRLIB_texImg *prev_tex = NULL;
int prev_blending = GRRLIB_BLEND_ALPHA;

GRRLIB_texImg *current_coin_texture[4];

const int dual_gamemode_heights[GAMEMODE_COUNT] = {
    9,  // Cube
    10, // Ship
    9,  // Ball
    10, // Ufo
    10, // Wave
    9, // Robot
};

const float jump_heights_table[SPEED_COUNT][JUMP_TYPES_COUNT][GAMEMODE_COUNT][2] = {
    { // SLOW               CUBE                   SHIP                  BALL                    UFO                 WAVE         ROBOT
    /* YELLOW PAD */ {{875.64,   691.2},    {432,      508.248},  {518.4,       414.72002},   {573.48,   458.784},  {0, 0}, {864,      691.2}},
    /* YELLOW ORB */ {{585.12,   458.784},  {573.48,   458.784},  {401.435993,  321.148795},  {573.48,   458.784},  {0, 0}, {516.132,  430.9056}},
    /* BLUE PAD   */ {{-345.6,   -276.48},  {-229.392, -183.519}, {-160.574397, -128.463298}, {-229.392, -183.519}, {0, 0}, {-345.6,   -276.48}},
    /* BLUE ORB   */ {{-229.392, -183.519}, {-229.392, -183.519}, {-160.574397, -128.463298}, {-229.392, -183.519}, {0, 0}, {-229.392, -183.519}},
    /* PINK PAD   */ {{561.6,    449.28},   {302.4,    241.92},   {362.88001,   290.30401},   {345.6,    276.4},    {0, 0}, {561.6,    449.28}},
    /* PINK ORB   */ {{412.884,  330.318},  {212.166,  169.776},  {309.090595,  247.287596},  {240.84,   192.672},  {0, 0}, {412.884,  330.318}},
    },
    { // NORMAL
    /* YELLOW PAD */ {{864,      691.2},    {432,      508.248},  {518.4,       414.72002},   {432,      691.2},    {0, 0}, {864,      691.2}},
    /* YELLOW ORB */ {{603.72,   482.976},  {603.72,   482.976},  {422.60399,   338.08319},   {603.72,   482.976},  {0, 0}, {543.348,  434.6784}},
    /* BLUE PAD   */ {{-345.6,   -276.48},  {-345.6,   -276.48},  {-207.36001,  -165.88801},  {-345.6,   -276.48},  {0, 0}, {-345.6,   -276.48}},
    /* BLUE ORB   */ {{-241.488, -193.185}, {-241.488, -193.18},  {-169.04160,  -135.2295},   {-241.488, -193.185}, {0, 0}, {-241.488, -193.185}},
    /* PINK PAD   */ {{561.6,    449.28},   {302.4,    241.92},   {362.88001,   290.30401},   {345.6,    276.4},    {0, 0}, {561.6,    449.28}},
    /* PINK ORB   */ {{434.7,    347.76},   {223.398,  178.686},  {325.42019,   260.3286},    {258.984,  207.198},  {0, 0}, {434.7,    347.76}},
    },
    { // FAST
    /* YELLOW PAD */ {{864,      691.2},    {432,      508.248},  {518.4,       414.72002},   {432,      691.2},    {0, 0}, {864,      691.2}},
    /* YELLOW ORB */ {{616.68,   481.734},  {616.68,   481.734},  {431.67599,   345.34079},   {616.68,   481.734},  {0, 0}, {555.012,  433.5687}},
    /* BLUE PAD   */ {{-345.6,   -276.48},  {-345.6,   -276.48},  {-207.36001,  -165.88801},  {-345.6,   -276.48},  {0, 0}, {-345.6,   -276.48}},
    /* BLUE ORB   */ {{-246.672, -197.343}, {-246.672, -197.343}, {-172.6704,   -138.1401},   {-246.672, -197.343}, {0, 0}, {-246.672, -197.343}},
    /* PINK PAD   */ {{561.6,    449.28},   {302.4,    241.92},   {362.88001,   290.30401},   {345.6,    276.4},    {0, 0}, {561.6,    449.28}},
    /* PINK ORB   */ {{443.988,  355.212},  {228.15,   182.52},   {332.37539,   265.923},     {258.984,  207.198},  {0, 0}, {443.988,  355.212}},
    },
    { // FASTER
    /* YELLOW PAD */ {{864,      691.2},    {432,      508.248},  {518.4,       414.72002},   {432,      691.2},    {0, 0}, {864,      691.2}},
    /* YELLOW ORB */ {{606.42,   485.136},  {606.42,   485.136},  {424.493993,  339.59519},   {606.42,   485.136},  {0, 0}, {545.778,  436.6224}},
    /* BLUE PAD   */ {{-345.6,   -276.48},  {-345.6,   -276.48},  {-207.36001,  -165.88801},  {-345.6,   -276.48},  {0, 0}, {-345.6,   -276.48}},
    /* BLUE ORB   */ {{-242.568, -194.049}, {-242.568, -194.049}, {-169.7976,   -135.8343},   {-242.568, -194.049}, {0, 0}, {-242.568, -194.049}},
    /* PINK PAD   */ {{561.6,    449.28},   {302.4,    241.92},   {362.88001,   290.30401},   {345.6,    276.4},    {0, 0}, {561.6,    449.28}},
    /* PINK ORB   */ {{436.644,  349.272},  {224.37,   179.496},  {326.85659,   261.5004},    {254.718,  203.742},  {0, 0}, {436.644,  349.272}},
    }
};

struct ColorChannel channels[COL_CHANNEL_COUNT];

float get_mirror_x(float x, float factor) {
    return x + factor * (screenWidth - 2.0f * x);
}

void set_intended_ceiling() {
    float mid_point = (state.ground_y + state.ceiling_y) / 2;
    state.camera_intended_y = mid_point - (SCREEN_HEIGHT_AREA / 2);
}

void set_particle_color(int template_id, int r, int g, int b) {
    particle_templates[template_id].start_color.r = r;
    particle_templates[template_id].start_color.g = g;
    particle_templates[template_id].start_color.b = b;

    particle_templates[template_id].end_color.r = r;
    particle_templates[template_id].end_color.g = g;
    particle_templates[template_id].end_color.b = b;
}

void handle_special_hitbox(Player *player, GameObject *obj, ObjectHitbox *hitbox) {
    switch (*soa_id(obj)) {
        case YELLOW_PAD:
            if (!obj->activated[state.current_player]) {
                MotionTrail_ResumeStroke(&trail);
                player->vel_y = jump_heights_table[state.speed][JUMP_YELLOW_PAD][player->gamemode][player->mini];
                player->on_ground = FALSE;
                player->inverse_rotation = FALSE;
                player->left_ground = TRUE;
                player->ufo_last_y = player->y;
                
                player->robot_anim_timer = 0;
                player->curr_robot_animation_id = ROBOT_JUMP_START;
                
                particle_templates[USE_EFFECT].start_scale = 0;
                particle_templates[USE_EFFECT].end_scale = 60;
                particle_templates[USE_EFFECT].trifading = FALSE;

                set_particle_color(USE_EFFECT, 255, 255, 0);
                particle_templates[USE_EFFECT].start_color.a = 255;
                particle_templates[USE_EFFECT].end_color.a = 0;

                spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);

                obj->activated[state.current_player] = TRUE;
            }
            break;

        case PINK_PAD:
            if (!obj->activated[state.current_player]) {
                MotionTrail_ResumeStroke(&trail);
                player->vel_y = jump_heights_table[state.speed][JUMP_PINK_PAD][player->gamemode][player->mini];
                player->on_ground = FALSE;
                player->inverse_rotation = FALSE;
                player->left_ground = TRUE;
                player->ufo_last_y = player->y;
                
                player->robot_anim_timer = 0;
                player->curr_robot_animation_id = ROBOT_JUMP_START;
                
                particle_templates[USE_EFFECT].start_scale = 0;
                particle_templates[USE_EFFECT].end_scale = 60;
                particle_templates[USE_EFFECT].trifading = FALSE;

                set_particle_color(USE_EFFECT, 255, 31, 255);
                particle_templates[USE_EFFECT].start_color.a = 255;
                particle_templates[USE_EFFECT].end_color.a = 0;

                spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);

                obj->activated[state.current_player] = TRUE;
            }
            break;

        case BLUE_PAD:
            if (obj->activated[state.current_player]) player->gravObj = obj;
            else {
                float rotation = adjust_angle(obj->rotation, obj->flippedV, obj->flippedH);
                if ((rotation < 90 || rotation > 270) && player->upside_down)
                    break;
                    
                if ((rotation > 90 && rotation < 270) && !player->upside_down)
                    break;

                MotionTrail_ResumeStroke(&trail);
                if (player->gamemode == GAMEMODE_WAVE) MotionTrail_AddWavePoint(&wave_trail);

                player->left_ground = TRUE;

                player->gravObj = obj;

                player->vel_y = jump_heights_table[state.speed][JUMP_BLUE_PAD][player->gamemode][player->mini];
                player->upside_down ^= 1;
                flip_other_player(state.current_player);
                player->on_ground = FALSE;
                player->inverse_rotation = FALSE;
                player->ufo_last_y = player->y;
                
                player->robot_anim_timer = 0;
                player->curr_robot_animation_id = ROBOT_JUMP_START;
                
                particle_templates[USE_EFFECT].start_scale = 0;
                particle_templates[USE_EFFECT].end_scale = 60;
                particle_templates[USE_EFFECT].trifading = FALSE;

                set_particle_color(USE_EFFECT, 56, 200, 255);
                particle_templates[USE_EFFECT].start_color.a = 255;
                particle_templates[USE_EFFECT].end_color.a = 0;

                spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);

                obj->activated[state.current_player] = TRUE;
            }
            break;
        
        case YELLOW_ORB:
            if (!obj->activated[state.current_player] && (state.input.holdJump) && player->buffering_state == BUFFER_READY) {    
                MotionTrail_ResumeStroke(&trail);
                
                player->vel_y = jump_heights_table[state.speed][JUMP_YELLOW_ORB][player->gamemode][player->mini];
                
                player->ball_rotation_speed = -1.f;
                
                player->on_ground = FALSE;
                player->on_ceiling = FALSE;
                player->inverse_rotation = FALSE;
                player->left_ground = TRUE;
                player->buffering_state = BUFFER_END;
                player->ufo_last_y = player->y;
                
                player->robot_anim_timer = 0;
                player->curr_robot_animation_id = ROBOT_JUMP_START;

                particle_templates[USE_EFFECT].start_scale = 70;
                particle_templates[USE_EFFECT].end_scale = 0;
                particle_templates[USE_EFFECT].trifading = TRUE;

                set_particle_color(USE_EFFECT, 255, 255, 0);
                particle_templates[USE_EFFECT].start_color.a = 0;
                particle_templates[USE_EFFECT].end_color.a = 255;

                spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);

                obj->activated[state.current_player] = TRUE;
            } 
            if (!obj->collided[state.current_player]) spawn_particle(ORB_HITBOX_EFFECT, *soa_x(obj), *soa_y(obj), obj);
            break;
        
        case PINK_ORB:
            if (!obj->activated[state.current_player] && (state.input.holdJump) && player->buffering_state == BUFFER_READY) {    
                MotionTrail_ResumeStroke(&trail);
                
                player->vel_y = jump_heights_table[state.speed][JUMP_PINK_ORB][player->gamemode][player->mini];
                
                player->ball_rotation_speed = -1.f;
                
                player->on_ground = FALSE;
                player->on_ceiling = FALSE;
                player->inverse_rotation = FALSE;
                player->left_ground = TRUE;
                player->buffering_state = BUFFER_END;
                player->ufo_last_y = player->y;

                player->robot_anim_timer = 0;
                player->curr_robot_animation_id = ROBOT_JUMP_START;

                particle_templates[USE_EFFECT].start_scale = 70;
                particle_templates[USE_EFFECT].end_scale = 0;
                particle_templates[USE_EFFECT].trifading = TRUE;

                set_particle_color(USE_EFFECT, 255, 31, 255);
                particle_templates[USE_EFFECT].start_color.a = 0;
                particle_templates[USE_EFFECT].end_color.a = 255;

                spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);

                obj->activated[state.current_player] = TRUE;
            } 
            if (!obj->collided[state.current_player]) spawn_particle(ORB_HITBOX_EFFECT, *soa_x(obj), *soa_y(obj), obj);
            break;
        
        case BLUE_ORB:
            if (!obj->activated[state.current_player] && (state.input.holdJump) && player->buffering_state == BUFFER_READY) {    
                MotionTrail_ResumeStroke(&trail);
                player->gravObj = obj;
                
                player->vel_y = jump_heights_table[state.speed][JUMP_BLUE_ORB][player->gamemode][player->mini];
                player->upside_down ^= 1;
                if (player->gamemode == GAMEMODE_WAVE) MotionTrail_AddWavePoint(&wave_trail);

                flip_other_player(state.current_player);
                
                player->ball_rotation_speed = -1.f;

                player->robot_anim_timer = 0;
                player->curr_robot_animation_id = ROBOT_JUMP_START;
                
                player->on_ground = FALSE;
                player->on_ceiling = FALSE;
                player->inverse_rotation = FALSE;
                player->left_ground = TRUE;
                player->buffering_state = BUFFER_END;
                player->ufo_last_y = player->y;

                particle_templates[USE_EFFECT].start_scale = 70;
                particle_templates[USE_EFFECT].end_scale = 0;
                particle_templates[USE_EFFECT].trifading = TRUE;

                set_particle_color(USE_EFFECT, 56, 200, 255);
                particle_templates[USE_EFFECT].start_color.a = 0;
                particle_templates[USE_EFFECT].end_color.a = 255;

                spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);

                obj->activated[state.current_player] = TRUE;
            } 
            if (!obj->collided[state.current_player]) spawn_particle(ORB_HITBOX_EFFECT, *soa_x(obj), *soa_y(obj), obj);
            break;
        
        case GREEN_ORB:
            if (!obj->activated[state.current_player] && (state.input.holdJump) && player->buffering_state == BUFFER_READY) {    
                MotionTrail_ResumeStroke(&trail);
                player->gravObj = obj;
                player->ceiling_inv_time = 0.5f;
                
                player->upside_down ^= 1;
                player->vel_y = jump_heights_table[state.speed][JUMP_YELLOW_ORB][player->gamemode][player->mini];
                
                if (player->gamemode == GAMEMODE_SHIP) {
                    player->vel_y *= 0.7f;
                }

                player->ball_rotation_speed = -1.f;
                
                player->robot_anim_timer = 0;
                player->curr_robot_animation_id = ROBOT_JUMP_START;
                
                flip_other_player(state.current_player);
                
                player->on_ground = FALSE;
                player->on_ceiling = FALSE;
                player->inverse_rotation = FALSE;
                player->left_ground = TRUE;
                player->buffering_state = BUFFER_END;
                player->ufo_last_y = player->y;

                particle_templates[USE_EFFECT].start_scale = 70;
                particle_templates[USE_EFFECT].end_scale = 0;
                particle_templates[USE_EFFECT].trifading = TRUE;

                set_particle_color(USE_EFFECT, 0, 255, 0);
                particle_templates[USE_EFFECT].start_color.a = 0;
                particle_templates[USE_EFFECT].end_color.a = 255;

                spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);

                obj->activated[state.current_player] = TRUE;
            } 
            if (!obj->collided[state.current_player]) spawn_particle(ORB_HITBOX_EFFECT, *soa_x(obj), *soa_y(obj), obj);
            break;

        case CUBE_PORTAL: 
            if (!obj->activated[state.current_player]) {
                state.ground_y = 0;
                state.ceiling_y = 999999;
                if (player->gamemode != GAMEMODE_CUBE) {
                    if (player->gamemode != GAMEMODE_BALL && player->gamemode != GAMEMODE_ROBOT) {
                        MotionTrail_StopStroke(&trail);
                        player->vel_y /= 2;
                    }

                    if (player->gamemode == GAMEMODE_WAVE) player->vel_y *= 0.9f;
                    
                    player->ceiling_inv_time = 0.1f;
                    player->snap_rotation = TRUE;
                    player->gamemode = GAMEMODE_CUBE;
                    player->is_cube_or_robot = TRUE;
                    flip_other_player(state.current_player ^ 1);

                    particle_templates[USE_EFFECT].start_scale = 80;
                    particle_templates[USE_EFFECT].end_scale = 0;
                    particle_templates[USE_EFFECT].trifading = TRUE;

                    set_particle_color(USE_EFFECT, 0, 255, 50);
                    particle_templates[USE_EFFECT].start_color.a = 0;
                    particle_templates[USE_EFFECT].end_color.a = 255;

                    spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                }
                if (state.dual) {
                    set_dual_bounds();
                } 
                obj->activated[state.current_player] = TRUE;
            }
            break;
            
        case SHIP_PORTAL: 
            if (!obj->activated[state.current_player]) {
                state.ground_y = maxf(0, ip1_ceilf((*soa_y(obj) - 180) / 30.f)) * 30;
                state.ceiling_y = state.ground_y + 300;
                set_intended_ceiling();

                if (player->gamemode != GAMEMODE_SHIP) {
                    if (player->gamemode == GAMEMODE_WAVE) player->vel_y *= 0.9f;
                    player->vel_y /= (player->gamemode == GAMEMODE_UFO || player->gamemode == GAMEMODE_WAVE) ? 4 : 2;
                    
                    player->gamemode = GAMEMODE_SHIP;
                    player->inverse_rotation = FALSE;
                    player->snap_rotation = TRUE;
                    flip_other_player(state.current_player ^ 1);
                    
                    float min = player->mini ? -406.566f : -345.6f;
                    float max = player->mini ? 508.248f : 432.0f;

                    if (player->vel_y < min) {
                        player->vel_y = min;
                    } else if (player->vel_y > max) {
                        player->vel_y = max;
                    }

                    particle_templates[USE_EFFECT].start_scale = 80;
                    particle_templates[USE_EFFECT].end_scale = 0; 
                    particle_templates[USE_EFFECT].trifading = TRUE;

                    set_particle_color(USE_EFFECT, 255, 31, 255);
                    particle_templates[USE_EFFECT].start_color.a = 0;
                    particle_templates[USE_EFFECT].end_color.a = 255;

                    spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                }
                if (state.dual) {
                    set_dual_bounds();
                } 
                obj->activated[state.current_player] = TRUE;
            }
            break;
        
        case BLUE_GRAVITY_PORTAL:
            player->gravObj = obj;
            if (!obj->activated[state.current_player]) {
                player->ceiling_inv_time = 0.1f;
                if (player->upside_down) {
                    if (player->gamemode != GAMEMODE_BALL) MotionTrail_ResumeStroke(&trail);
                    player->vel_y /= -2;
                    player->upside_down = FALSE;
                    player->inverse_rotation = FALSE;
                    player->snap_rotation = TRUE;
                    if (player->gamemode == GAMEMODE_WAVE) MotionTrail_AddWavePoint(&wave_trail);
                    flip_other_player(state.current_player);
                    player->left_ground = TRUE;
                    player->ufo_last_y = player->y;

                    player->robot_air_time = 1.5f;
                    
                    player->robot_anim_timer = 0;
                    player->curr_robot_animation_id = ROBOT_FALL_START;

                    particle_templates[USE_EFFECT].start_scale = 80;
                    particle_templates[USE_EFFECT].end_scale = 0;
                    particle_templates[USE_EFFECT].trifading = TRUE;

                    set_particle_color(USE_EFFECT, 56, 200, 255);
                    particle_templates[USE_EFFECT].start_color.a = 0;
                    particle_templates[USE_EFFECT].end_color.a = 255;
                    
                    spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                }
                obj->activated[state.current_player] = TRUE;
            }
            break;
        case YELLOW_GRAVITY_PORTAL:
            player->gravObj = obj;
            if (!obj->activated[state.current_player]) {
                player->ceiling_inv_time = 0.1f;
                if (!player->upside_down) {
                    if (player->gamemode != GAMEMODE_BALL) MotionTrail_ResumeStroke(&trail);
                    player->vel_y /= -2;
                    player->upside_down = TRUE;
                    player->inverse_rotation = FALSE;
                    if (player->gamemode == GAMEMODE_WAVE) MotionTrail_AddWavePoint(&wave_trail);
                    flip_other_player(state.current_player);
                    player->left_ground = TRUE;
                    player->ufo_last_y = player->y;
                    
                    player->robot_air_time = 1.5f;
                    
                    player->robot_anim_timer = 0;
                    player->curr_robot_animation_id = ROBOT_FALL_START;

                    particle_templates[USE_EFFECT].start_scale = 80;
                    particle_templates[USE_EFFECT].end_scale = 0;
                    particle_templates[USE_EFFECT].trifading = TRUE;

                    set_particle_color(USE_EFFECT, 255, 255, 0);
                    particle_templates[USE_EFFECT].start_color.a = 0;
                    particle_templates[USE_EFFECT].end_color.a = 255;
                    
                    spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                }
            }
            obj->activated[state.current_player] = TRUE;
            break;
        case ORANGE_MIRROR_PORTAL:
            if (!obj->activated[state.current_player]) {
                particle_templates[USE_EFFECT].start_scale = 80;
                particle_templates[USE_EFFECT].end_scale = 0;
                particle_templates[USE_EFFECT].trifading = TRUE;

                set_particle_color(USE_EFFECT, 255, 94, 0);
                particle_templates[USE_EFFECT].start_color.a = 0;
                particle_templates[USE_EFFECT].end_color.a = 255;
                
                spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);

                state.intended_mirror_factor = 1.f;
                state.intended_mirror_speed_factor = -1.f;
                obj->activated[state.current_player] = TRUE;
            }
            break;
        case BLUE_MIRROR_PORTAL:
            if (!obj->activated[state.current_player]) {
                particle_templates[USE_EFFECT].start_scale = 80;
                particle_templates[USE_EFFECT].end_scale = 0;
                particle_templates[USE_EFFECT].trifading = TRUE;

                set_particle_color(USE_EFFECT, 56, 200, 255);
                particle_templates[USE_EFFECT].start_color.a = 0;
                particle_templates[USE_EFFECT].end_color.a = 255;
                
                spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);

                state.intended_mirror_factor = 0.f;
                state.intended_mirror_speed_factor = 1.f;
                obj->activated[state.current_player] = TRUE;
            }
            break;
        
        case BALL_PORTAL: 
            if (!obj->activated[state.current_player]) {
                state.ground_y = maxf(0, ip1_ceilf((*soa_y(obj) - 150) / 30.f)) * 30;
                state.ceiling_y = state.ground_y + 240;
                set_intended_ceiling();

                if (player->gamemode != GAMEMODE_BALL) {
                    player->ball_rotation_speed = -1.f;

                    switch (player->gamemode) {
                        case GAMEMODE_WAVE:
                            player->vel_y *= 0.9f;
                            player->vel_y /= 2;
                        case GAMEMODE_SHIP:
                        case GAMEMODE_UFO:
                            player->vel_y /= 2;
                            break;
                    }
                    
                    player->gamemode = GAMEMODE_BALL;
                    player->inverse_rotation = FALSE;
                    player->snap_rotation = TRUE;
                    flip_other_player(state.current_player ^ 1);

                    particle_templates[USE_EFFECT].start_scale = 80;
                    particle_templates[USE_EFFECT].end_scale = 0;
                    particle_templates[USE_EFFECT].trifading = TRUE;

                    set_particle_color(USE_EFFECT, 255, 0, 0);
                    particle_templates[USE_EFFECT].start_color.a = 0;
                    particle_templates[USE_EFFECT].end_color.a = 255;

                    spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                }
                if (state.dual) {
                    set_dual_bounds();
                } 
                obj->activated[state.current_player] = TRUE;
            }
            break;

        case BIG_PORTAL:
            if (!obj->activated[state.current_player]) {
                player->mini = FALSE;

                particle_templates[USE_EFFECT].start_scale = 80;
                particle_templates[USE_EFFECT].end_scale = 0;
                particle_templates[USE_EFFECT].trifading = TRUE;

                set_particle_color(USE_EFFECT, 0, 255, 50);
                particle_templates[USE_EFFECT].start_color.a = 0;
                particle_templates[USE_EFFECT].end_color.a = 255;

                spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                
                obj->activated[state.current_player] = TRUE;
            }
            break;        

        case MINI_PORTAL:
            if (!obj->activated[state.current_player]) {
                player->mini = TRUE;

                particle_templates[USE_EFFECT].start_scale = 80;
                particle_templates[USE_EFFECT].end_scale = 0;
                particle_templates[USE_EFFECT].trifading = TRUE;

                set_particle_color(USE_EFFECT, 255, 31, 255);
                particle_templates[USE_EFFECT].start_color.a = 0;
                particle_templates[USE_EFFECT].end_color.a = 255;
                
                spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                
                obj->activated[state.current_player] = TRUE;
            }
            break;
        case UFO_PORTAL:
            if (!obj->activated[state.current_player]) {
                state.ground_y = maxf(0, ip1_ceilf((*soa_y(obj) - 180) / 30.f)) * 30;
                state.ceiling_y = state.ground_y + 300;
                set_intended_ceiling();
                
                if (player->gamemode != GAMEMODE_UFO) {
                    if (player->gamemode == GAMEMODE_WAVE) player->vel_y *= 0.9f;
                    player->vel_y /= (player->gamemode == GAMEMODE_SHIP || player->gamemode == GAMEMODE_WAVE) ? 4 : 2;
                    player->gamemode = GAMEMODE_UFO;
                    player->ufo_last_y = player->y;
                    player->inverse_rotation = FALSE;
                    player->snap_rotation = TRUE;
                    flip_other_player(state.current_player ^ 1);

                    if (state.old_player.gamemode == GAMEMODE_CUBE || state.old_player.gamemode == GAMEMODE_SHIP || state.old_player.gamemode == GAMEMODE_WAVE) {
                        player->buffering_state = BUFFER_READY;
                    }

                    particle_templates[USE_EFFECT].start_scale = 80;
                    particle_templates[USE_EFFECT].end_scale = 0;
                    particle_templates[USE_EFFECT].trifading = TRUE;

                    set_particle_color(USE_EFFECT, 255, 127, 0);
                    particle_templates[USE_EFFECT].start_color.a = 0;
                    particle_templates[USE_EFFECT].end_color.a = 255;

                    spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                }
                if (state.dual) {
                    set_dual_bounds();
                } 
                obj->activated[state.current_player] = TRUE;
            }
            break;
        case SECRET_COIN:
            if (!obj->activated[state.current_player]) {
                // Coin particle
                spawn_particle(COIN_OBJ, *soa_x(obj), *soa_y(obj), NULL);

                // Explode particles
                particle_templates[BREAKABLE_BRICK_PARTICLES].start_color.a = 127;
                for (s32 i = 0; i < 10; i++) {
                    spawn_particle(BREAKABLE_BRICK_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
                }

                // Use particles
                particle_templates[USE_EFFECT].start_scale = 60;
                particle_templates[USE_EFFECT].end_scale = 0;
                particle_templates[USE_EFFECT].trifading = FALSE;

                if (level_info.level_is_custom) {
                    set_particle_color(USE_EFFECT, 255, 255, 255);
                } else {
                    set_particle_color(USE_EFFECT, 255, 255, 0);
                }
                particle_templates[USE_EFFECT].start_color.a = 255;
                particle_templates[USE_EFFECT].end_color.a = 0;

                spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                spawn_particle(ORB_HITBOX_EFFECT, *soa_x(obj), *soa_y(obj), obj);

                obj->hide_sprite = TRUE;
                obj->activated[state.current_player] = TRUE;
            }
            break;
        case SLOW_SPEED_PORTAL:
            if (!obj->activated[state.current_player]) {
                if (state.speed != SPEED_SLOW) {
                    set_particle_color(SPEEDUP, 255, 220, 0);
                    particle_templates[SPEEDUP].speed = -20;
                    particle_templates[SPEEDUP].sourcePosVarY = 20;
                    for (int i = 0; i < 20; i++) {
                        spawn_particle(SPEEDUP, state.camera_x + SCREEN_WIDTH_AREA + 20, state.camera_y + (SCREEN_HEIGHT_AREA / 2), NULL);
                    }
                }
                spawn_particle(ORB_HITBOX_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                state.speed = SPEED_SLOW;
                obj->activated[state.current_player] = TRUE;
            }
            break;
        case NORMAL_SPEED_PORTAL:
            if (!obj->activated[state.current_player]) {
                if (state.speed != SPEED_NORMAL) {
                    set_particle_color(SPEEDUP, 0, 255, 255);
                    particle_templates[SPEEDUP].speed = -60;
                    particle_templates[SPEEDUP].sourcePosVarY = 90;
                    for (int i = 0; i < 20; i++) {
                        spawn_particle(SPEEDUP, state.camera_x + SCREEN_WIDTH_AREA + 90, state.camera_y + (SCREEN_HEIGHT_AREA / 2), NULL);
                    }
                }
                spawn_particle(ORB_HITBOX_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                state.speed = SPEED_NORMAL;
                obj->activated[state.current_player] = TRUE;
            }
            break;
        case FAST_SPEED_PORTAL:
            if (!obj->activated[state.current_player]) {
                if (state.speed != SPEED_FAST) {
                    set_particle_color(SPEEDUP, 64, 255, 64);
                    particle_templates[SPEEDUP].speed = -100;
                    particle_templates[SPEEDUP].sourcePosVarY = 120;
                    for (int i = 0; i < 20; i++) {
                        spawn_particle(SPEEDUP, state.camera_x + SCREEN_WIDTH_AREA + 120, state.camera_y + (SCREEN_HEIGHT_AREA / 2), NULL);
                    }
                }
                spawn_particle(ORB_HITBOX_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                state.speed = SPEED_FAST;
                obj->activated[state.current_player] = TRUE;
            }
            break;
        case FASTER_SPEED_PORTAL:
            if (!obj->activated[state.current_player]) {
                if (state.speed != SPEED_FASTER) {
                    set_particle_color(SPEEDUP, 255, 127, 255);
                    particle_templates[SPEEDUP].speed = -160;
                    particle_templates[SPEEDUP].sourcePosVarY = 200;
                    for (int i = 0; i < 20; i++) {
                        spawn_particle(SPEEDUP, state.camera_x + SCREEN_WIDTH_AREA + 200, state.camera_y + (SCREEN_HEIGHT_AREA / 2), NULL);
                    }
                }
                spawn_particle(ORB_HITBOX_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                state.speed = SPEED_FASTER;
                obj->activated[state.current_player] = TRUE;
            }
            break;
        case DUAL_PORTAL:
            player->gravObj = obj;
            if (!obj->activated[state.current_player] && !state.dual) {
                particle_templates[USE_EFFECT].start_scale = 80;
                particle_templates[USE_EFFECT].end_scale = 0;
                particle_templates[USE_EFFECT].trifading = TRUE;

                set_particle_color(USE_EFFECT, 255, 94, 0);
                particle_templates[USE_EFFECT].start_color.a = 0;
                particle_templates[USE_EFFECT].end_color.a = 255;
                
                spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                
                player->ceiling_inv_time = 0.1f;
                state.dual = TRUE;
                state.dual_portal_y = *soa_y(obj);
                setup_dual();
                
                if (player->gamemode == GAMEMODE_WAVE) {
                    MotionTrail_Init(&wave_trail_p2, 3.f, 3, 10.0f, TRUE, p1, trail_tex);   
                    wave_trail_p2.positionR = (Vec2){state.player2.x, state.player2.y};  
                    wave_trail_p2.startingPositionInitialized = TRUE;
                    MotionTrail_AddWavePoint(&wave_trail_p2);
                }
                MotionTrail_Init(&trail_p2, 0.3f, 3, 10.0f, FALSE, p1, trail_tex);

                obj->activated[state.current_player] = TRUE;
            }
            break;

        case DIVORCE_PORTAL:
            if (!obj->activated[state.current_player]) {
                particle_templates[USE_EFFECT].start_scale = 80;
                particle_templates[USE_EFFECT].end_scale = 0;
                particle_templates[USE_EFFECT].trifading = TRUE;

                set_particle_color(USE_EFFECT, 56, 200, 255);
                particle_templates[USE_EFFECT].start_color.a = 0;
                particle_templates[USE_EFFECT].end_color.a = 255;
                
                spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);

                state.dual = FALSE;
                obj->activated[state.current_player] = TRUE;
                

                if (state.current_player == 1) {
                    memcpy(&state.player, player, sizeof(Player));
                }
                
                switch (state.player.gamemode) {
                    case GAMEMODE_CUBE:
                    case GAMEMODE_ROBOT:
                        state.ground_y = 0;
                        state.ceiling_y = 999999;
                        break;
                    case GAMEMODE_SHIP:
                    case GAMEMODE_UFO:
                        state.ceiling_y = state.ground_y + 300;
                        set_intended_ceiling();
                        break;
                    case GAMEMODE_BALL:
                        state.ceiling_y = state.ground_y + 240;
                        set_intended_ceiling();
                }
            }
            break;
            
        case WAVE_PORTAL:
            if (!obj->activated[state.current_player]) {
                state.ground_y = maxf(0, ip1_ceilf((*soa_y(obj) - 180) / 30.f)) * 30;
                state.ceiling_y = state.ground_y + 300;
                set_intended_ceiling();

                if (player->gamemode != GAMEMODE_WAVE) {
                    player->gamemode = GAMEMODE_WAVE;
                    player->inverse_rotation = FALSE;
                    player->snap_rotation = TRUE;
                    flip_other_player(state.current_player ^ 1);
                    wave_trail.positionR = (Vec2){player->x, player->y};  
                    wave_trail.startingPositionInitialized = TRUE;
                    MotionTrail_AddWavePoint(&wave_trail);

                    particle_templates[USE_EFFECT].start_scale = 80;
                    particle_templates[USE_EFFECT].end_scale = 0;
                    particle_templates[USE_EFFECT].trifading = TRUE;

                    set_particle_color(USE_EFFECT, 56, 200, 255);
                    particle_templates[USE_EFFECT].start_color.a = 0;
                    particle_templates[USE_EFFECT].end_color.a = 255;

                    spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                }
                if (state.dual) {
                    set_dual_bounds();
                } 
                obj->activated[state.current_player] = TRUE;
            }
            break;
        case BLUE_TP_PORTAL:
            if (!obj->activated[state.current_player]) {
                // Teleport
                player->y = *soa_y(obj->object.child_object);
                state.old_player.y = player->y; // delta_y should not be set on blue tp portal

                particle_templates[USE_EFFECT].start_scale = 80;
                particle_templates[USE_EFFECT].end_scale = 0;
                particle_templates[USE_EFFECT].trifading = TRUE;

                particle_templates[USE_EFFECT].start_color.a = 0;
                particle_templates[USE_EFFECT].end_color.a = 255;

                MotionTrail_Clear(&trail);

                set_particle_color(USE_EFFECT, 56, 200, 255);
                spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);

                set_particle_color(USE_EFFECT, 255, 255, 0);
                spawn_particle(USE_EFFECT, *soa_x(obj->object.child_object), *soa_y(obj->object.child_object), obj);
                
                float camera_y = state.camera_y + SCREEN_HEIGHT_AREA / 2;
                if (player->is_cube_or_robot && fabsf(camera_y - *soa_y(obj->object.child_object)) >= SCREEN_HEIGHT_AREA/2 + 60) {
                    state.intermediate_camera_y = state.camera_y = player->y - SCREEN_HEIGHT_AREA / 2;
                }

                player->has_teleported_timer = 0.5f;
                player->just_teleported = TRUE;

                obj->activated[state.current_player] = TRUE;
            }
            break;

        case ROBOT_PORTAL: 
            if (!obj->activated[state.current_player]) {
                state.ground_y = 0;
                state.ceiling_y = 999999;
                if (player->gamemode != GAMEMODE_ROBOT) {
                    if (player->gamemode != GAMEMODE_BALL && player->gamemode != GAMEMODE_CUBE) {
                        MotionTrail_StopStroke(&trail);
                        player->vel_y /= 2;
                    }

                    if (player->gamemode == GAMEMODE_WAVE) player->vel_y *= 0.9f;
                    
                    player->ceiling_inv_time = 0.1f;
                    player->snap_rotation = TRUE;
                    player->gamemode = GAMEMODE_ROBOT;
                    player->is_cube_or_robot = TRUE;
                    flip_other_player(state.current_player ^ 1);

                    particle_templates[USE_EFFECT].start_scale = 80;
                    particle_templates[USE_EFFECT].end_scale = 0;
                    particle_templates[USE_EFFECT].trifading = TRUE;

                    set_particle_color(USE_EFFECT, 255, 255, 255);
                    particle_templates[USE_EFFECT].start_color.a = 0;
                    particle_templates[USE_EFFECT].end_color.a = 255;

                    spawn_particle(USE_EFFECT, *soa_x(obj), *soa_y(obj), obj);
                }
                if (state.dual) {
                    set_dual_bounds();
                } 
                obj->activated[state.current_player] = TRUE;
            }
            break;
        case KEY_OBJ:
            if (!obj->activated[state.current_player]) {
                // Coin particle
                spawn_particle(KEY_OBJ_PART, *soa_x(obj), *soa_y(obj), obj);

                // Explode particles
                particle_templates[BREAKABLE_BRICK_PARTICLES].start_color.a = 127;
                for (s32 i = 0; i < 10; i++) {
                    spawn_particle(BREAKABLE_BRICK_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
                }

                obj->hide_sprite = TRUE;
                obj->activated[state.current_player] = TRUE;
            }
            break;
    }
    if (!obj->collided[state.current_player]) obj->hitbox_counter[state.current_player]++; 
}

float get_rotated_x_hitbox(float x_offset, float y_offset, float rotation) {
    float angle_rad = DegToRad(rotation); // Convert degrees to radians
    float cos_a = cosf(angle_rad);
    float sin_a = sinf(angle_rad);

    return (x_offset * cos_a - y_offset * sin_a);
}

float get_rotated_y_hitbox(float x_offset, float y_offset, float rotation) {
    float angle_rad = DegToRad(rotation); // Convert degrees to radians
    float cos_a = cosf(angle_rad);
    float sin_a = sinf(angle_rad);

    return -(x_offset * sin_a + y_offset * cos_a);
}

void setup_dual() {
    memcpy(&state.player2, &state.player, sizeof(Player));
    state.player2.upside_down = state.player.upside_down ^ 1;
    set_dual_bounds();
}


void set_dual_bounds() {
    int height = MAX(dual_gamemode_heights[state.player.gamemode],
                 dual_gamemode_heights[state.player2.gamemode]);

    float in_block_y = fmodf(state.dual_portal_y, 30);

    int ground_offset = (ceilf(((float) height + 1) / 2) - 1) * 30;
    state.ground_y = maxf(0, floorf((state.dual_portal_y - ground_offset) / 30.f)) * 30;


    // Shift down if odd height and in the top half
    if (height % 2 != 0) {
        if (in_block_y < 15) {
            state.ground_y = maxf(0, state.ground_y - 30);
        }
    }

    state.ceiling_y = state.ground_y + (height * 30.f);
    set_intended_ceiling();
}

void flip_other_player(int current_player) {
    if (state.dual && state.player.gamemode == state.player2.gamemode && state.player.upside_down == state.player2.upside_down) {
        if (current_player == 0) {
            state.player2.upside_down = !state.player.upside_down;
            state.player2.vel_y /= -2;
            state.player2.ceiling_inv_time = 0.1f;
        } else {
            state.player.upside_down = !state.player2.upside_down;
            state.player.vel_y /= -2;
            state.player.ceiling_inv_time = 0.1f;
        }
    }
}

void do_ball_reflection() {
    Player *player_1 = &state.player;
    Player *player_2 = &state.player2;
    if (state.dual && player_1->gamemode == GAMEMODE_BALL && player_2->gamemode == GAMEMODE_BALL) {
        if (player_1->upside_down == player_2->upside_down) {
            bool ballsIntersecting = intersect(
                player_1->x, player_1->y, player_1->width, player_1->height, 0,
                player_2->x, player_2->y, player_2->width, player_2->height, 0
            );

            if (ballsIntersecting) {
                int current_player = state.current_player;
                if (player_1->on_ground || player_1->on_ceiling) {
                    state.current_player = 0;

                    set_particle_color(DUAL_BALL_HITBOX_EFFECT, p1.r, p1.g, p1.b);
                    spawn_particle(DUAL_BALL_HITBOX_EFFECT, player_1->x, player_1->y, NULL);

                    player_2->vel_y = jump_heights_table[state.speed][JUMP_BLUE_PAD][player_2->gamemode][player_2->mini];
                    player_2->upside_down ^= 1;
                } else if (player_2->on_ground || player_2->on_ceiling) {
                    state.current_player = 1;

                    set_particle_color(DUAL_BALL_HITBOX_EFFECT, p2.r, p2.g, p2.b);
                    spawn_particle(DUAL_BALL_HITBOX_EFFECT, player_2->x, player_2->y, NULL);

                    player_1->vel_y = jump_heights_table[state.speed][JUMP_BLUE_PAD][player_1->gamemode][player_1->mini];
                    player_1->upside_down ^= 1;
                }
                state.current_player = current_player;
            }
        }
        
    }
}

// Prepare Graphics
GRRLIB_texImg *bg;
GRRLIB_texImg *ground;
GRRLIB_texImg *ground_l2;
GRRLIB_texImg *ground_line;
GRRLIB_texImg *level_complete_texture;
GRRLIB_texImg *object_images[OBJECT_COUNT][MAX_OBJECT_LAYERS]; 
GRRLIB_texImg *level_font;

int current_fading_effect = FADE_NONE;

bool p1_trail = FALSE;

int find_existing_texture(const unsigned char *texture) {
    for (s32 object = 1; object < OBJECT_COUNT; object++) {
        for (s32 layer = 0; layer < MAX_OBJECT_LAYERS; layer++) {
            const unsigned char *loaded_texture = objects[object].layers[layer].texture;
            if (loaded_texture == texture) {
                return (object * MAX_OBJECT_LAYERS) + layer;
            }
        }
    }
    return -1;
}

int find_existing_previous_texture(int current_object, const unsigned char *texture) {
    for (s32 object = 1; object < current_object; object++) {
        for (s32 layer = 0; layer < MAX_OBJECT_LAYERS; layer++) {
            const unsigned char *loaded_texture = objects[object].layers[layer].texture;
            if (loaded_texture == texture) {
                return (object * MAX_OBJECT_LAYERS) + layer;
            }
        }
    }
    return -1;
}

void load_spritesheet() {
    // Load Textures 
    ground_line = GRRLIB_LoadTexturePNG(ground_line_png);
    level_complete_texture = GRRLIB_LoadTexturePNG(levelCompleteText_png);
    monster_1_anim = prepare_monster_1_animation();
    monster_2_anim = prepare_monster_2_animation();
    monster_3_anim = prepare_monster_3_animation();
    black_sludge_anim = prepare_black_sludge_animation();
    fire1_anim = prepare_fire_1_animation();
    fire2_anim = prepare_fire_2_animation();
    fire3_anim = prepare_fire_3_animation();
    fire4_anim = prepare_fire_4_animation();
    
    water_1_anim = prepare_water_1_animation();
    water_2_anim = prepare_water_2_animation();
    water_3_anim = prepare_water_3_animation();

    loading_1_anim = prepare_loading_1_animation();
    loading_2_anim = prepare_loading_2_animation();

    load_icons();
}

void load_layer_texture(const u8 *texture, int object, int layer) {
    GRRLIB_texImg *image = GRRLIB_LoadTexturePNG((const u8 *) texture);
    if (image == NULL || image->data == NULL) {
        printf("Couldn't load texture of object %d layer %d\n", object, layer);
    } else {
        GRRLIB_SetHandle(image, (image->w/2), (image->h/2));
        object_images[object][layer] = image;
    }
}

void load_obj_textures(int object) {
    if (is_object_unimplemented(object)) return;

    // Skip unused layers
    for (s32 layer = 0; layer < MAX_OBJECT_LAYERS; layer++) {
        const unsigned char *texture = objects[object].layers[layer].texture;
        if (!texture) continue;

        // Skip if already loaded
        if (object_images[object][layer]) continue;
        
        int existing = find_existing_texture(texture);

        if (existing < 0) {
            output_log("Loading texture of object %d layer %d\n", object, layer);
            load_layer_texture((const u8 *) texture, object, layer);
        } else {
            int object_found = existing / MAX_OBJECT_LAYERS;
            int layer_found = existing % MAX_OBJECT_LAYERS;

            if (object_images[object_found][layer_found]) {
                output_log("Found texture of object %d layer %d in object %d layer %d: %p\n", object, layer, object_found, layer_found, object_images[object_found][layer_found]);
            } else {
                const unsigned char *texture = objects[object_found].layers[layer_found].texture;
                output_log("Loading texture of object %d layer %d\n", object_found, layer_found);
                load_layer_texture((const u8 *) texture, object_found, layer_found);
            }
            object_images[object][layer] = object_images[object_found][layer_found];
        }
    }
}

void unload_obj_textures() {
    for (s32 object = 0; object < OBJECT_COUNT; object++) {
        for (s32 layer = 0; layer < MAX_OBJECT_LAYERS; layer++) {
            const unsigned char *texture = objects[object].layers[layer].texture;
            if (!texture) continue;

            int existing = find_existing_previous_texture(object, texture);
            // Dont double free textures
            if (existing < 0) {
                GRRLIB_FreeTexture(object_images[object][layer]);
            }
            object_images[object][layer] = NULL;
        }
    }
}

void unload_spritesheet() {
    // Free all memory used by textures.
    GRRLIB_FreeTexture(ground_line);
    GRRLIB_FreeTexture(big_font_text);
    
    unload_animation_definition(monster_1_anim);
    unload_animation_definition(black_sludge_anim);
    unload_frame_definition(fire1_anim);
    unload_frame_definition(fire2_anim);
    unload_frame_definition(fire3_anim);
    unload_frame_definition(fire4_anim);
    unload_frame_definition(water_1_anim);
    unload_frame_definition(water_2_anim);
    unload_frame_definition(water_3_anim);
    unload_frame_definition(loading_1_anim);
    unload_frame_definition(loading_2_anim);

    unload_icons();
}
void handle_post_draw_object_particles(GameObject *obj, GDObjectLayer *layer) {
    switch (*soa_id(obj)) {
        case SLOW_SPEED_PORTAL:
            particle_templates[SPEED_PORTAL_AMBIENT].sourcePosVarX = obj->width / 2;
            particle_templates[SPEED_PORTAL_AMBIENT].sourcePosVarY = obj->height / 2;
            set_particle_color(SPEED_PORTAL_AMBIENT, 255, 220, 0);
            if ((frameCount & 0b1111) == 0) spawn_particle(SPEED_PORTAL_AMBIENT, *soa_x(obj), *soa_y(obj), obj);
            draw_obj_particles(ORB_HITBOX_EFFECT, obj);
            draw_obj_particles(SPEED_PORTAL_AMBIENT, obj);
            break;
        case NORMAL_SPEED_PORTAL:
            particle_templates[SPEED_PORTAL_AMBIENT].sourcePosVarX = obj->width / 2;
            particle_templates[SPEED_PORTAL_AMBIENT].sourcePosVarY = obj->height / 2;
            set_particle_color(SPEED_PORTAL_AMBIENT, 0, 255, 255);
            if ((frameCount & 0b1111) == 0) spawn_particle(SPEED_PORTAL_AMBIENT, *soa_x(obj), *soa_y(obj), obj);
            draw_obj_particles(ORB_HITBOX_EFFECT, obj);
            draw_obj_particles(SPEED_PORTAL_AMBIENT, obj);
            break;
        case FAST_SPEED_PORTAL:
            particle_templates[SPEED_PORTAL_AMBIENT].sourcePosVarX = obj->width / 2;
            particle_templates[SPEED_PORTAL_AMBIENT].sourcePosVarY = obj->height / 2;
            set_particle_color(SPEED_PORTAL_AMBIENT, 64, 255, 64);
            if ((frameCount & 0b1111) == 0) spawn_particle(SPEED_PORTAL_AMBIENT, *soa_x(obj), *soa_y(obj), obj);
            draw_obj_particles(ORB_HITBOX_EFFECT, obj);
            draw_obj_particles(SPEED_PORTAL_AMBIENT, obj);
            break;
        case FASTER_SPEED_PORTAL:
            particle_templates[SPEED_PORTAL_AMBIENT].sourcePosVarX = obj->width / 2;
            particle_templates[SPEED_PORTAL_AMBIENT].sourcePosVarY = obj->height / 2;
            set_particle_color(SPEED_PORTAL_AMBIENT, 255, 127, 255);
            if ((frameCount & 0b1111) == 0) spawn_particle(SPEED_PORTAL_AMBIENT, *soa_x(obj), *soa_y(obj), obj);
            draw_obj_particles(ORB_HITBOX_EFFECT, obj);
            draw_obj_particles(SPEED_PORTAL_AMBIENT, obj);
            break;
    }

    if (GRRLIB_Settings.blend != prev_blending) {
        GRRLIB_SetBlend(prev_blending);
    }
}

void handle_pre_draw_object_particles(GameObject *obj, GDObjectLayer *layer) {
    switch (*soa_id(obj)) {
        case YELLOW_ORB:
            set_particle_color(ORB_PARTICLES, 255, 255, 0);
            spawn_particle(ORB_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
            draw_obj_particles(ORB_PARTICLES, obj);
            draw_obj_particles(USE_EFFECT, obj);
            draw_obj_particles(ORB_HITBOX_EFFECT, obj);
            break;
        
        case YELLOW_PAD:
            particle_templates[PAD_PARTICLES].angle = 180.f - (adjust_angle_y(obj->rotation, obj->flippedV) + 90.f);

            set_particle_color(PAD_PARTICLES, 255, 255, 0);
            spawn_particle(PAD_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
            draw_obj_particles(PAD_PARTICLES, obj);
            draw_obj_particles(USE_EFFECT, obj);
            break;

        case PINK_ORB:
            set_particle_color(ORB_PARTICLES, 255, 31, 255);
            spawn_particle(ORB_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
            draw_obj_particles(ORB_PARTICLES, obj);
            draw_obj_particles(USE_EFFECT, obj);
            draw_obj_particles(ORB_HITBOX_EFFECT, obj);
            break;

        case PINK_PAD:
            particle_templates[PAD_PARTICLES].angle = 180.f - (adjust_angle_y(obj->rotation, obj->flippedV) + 90.f);

            set_particle_color(PAD_PARTICLES, 255, 31, 255);
            spawn_particle(PAD_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
            draw_obj_particles(PAD_PARTICLES, obj);
            draw_obj_particles(USE_EFFECT, obj);
            break;

        case BLUE_ORB:
            set_particle_color(ORB_PARTICLES, 56, 200, 255);
            spawn_particle(ORB_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
            draw_obj_particles(ORB_PARTICLES, obj);
            draw_obj_particles(USE_EFFECT, obj);
            draw_obj_particles(ORB_HITBOX_EFFECT, obj);
            break;
        
        case BLUE_PAD:
            particle_templates[PAD_PARTICLES].angle = 180.f - (adjust_angle_y(obj->rotation, obj->flippedV) + 90.f);
            
            set_particle_color(PAD_PARTICLES, 56, 200, 255);
            spawn_particle(PAD_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
            draw_obj_particles(PAD_PARTICLES, obj);
            draw_obj_particles(USE_EFFECT, obj);
            break;
        
        case GREEN_ORB:
            set_particle_color(ORB_PARTICLES, 0, 255, 0);
            spawn_particle(ORB_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
            draw_obj_particles(ORB_PARTICLES, obj);
            draw_obj_particles(USE_EFFECT, obj);
            draw_obj_particles(ORB_HITBOX_EFFECT, obj);
            break;
            
        case YELLOW_GRAVITY_PORTAL:
            if (layer->layerNum == 1) {
                particle_templates[PORTAL_PARTICLES].angle = 180.f - adjust_angle_y(obj->rotation, obj->flippedH);

                set_particle_color(PORTAL_PARTICLES, 255, 255, 0);
                particle_templates[PORTAL_PARTICLES].start_color.a = 127;
                particle_templates[PORTAL_PARTICLES].end_color.a = 255;
                spawn_particle(PORTAL_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
                draw_obj_particles(PORTAL_PARTICLES, obj);
                draw_obj_particles(USE_EFFECT, obj);
            }
            break;

        case BLUE_GRAVITY_PORTAL:
        case BLUE_MIRROR_PORTAL:
        case DIVORCE_PORTAL:
        case WAVE_PORTAL:
        case BLUE_TP_PORTAL:
            if (layer->layerNum == 1) {
                particle_templates[PORTAL_PARTICLES].angle = 180.f - adjust_angle_y(obj->rotation, obj->flippedH);

                set_particle_color(PORTAL_PARTICLES, 56, 200, 255);
                particle_templates[PORTAL_PARTICLES].start_color.a = 127;
                particle_templates[PORTAL_PARTICLES].end_color.a = 255;
                spawn_particle(PORTAL_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
                draw_obj_particles(PORTAL_PARTICLES, obj);
                draw_obj_particles(USE_EFFECT, obj);
            }
            break;
        
        case CUBE_PORTAL:
        case BIG_PORTAL:
            if (layer->layerNum == 1) {
                particle_templates[PORTAL_PARTICLES].angle = 180.f - adjust_angle_y(obj->rotation, obj->flippedH);

                set_particle_color(PORTAL_PARTICLES, 0, 255, 50);
                particle_templates[PORTAL_PARTICLES].start_color.a = 127;
                particle_templates[PORTAL_PARTICLES].end_color.a = 255;
                spawn_particle(PORTAL_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
                draw_obj_particles(PORTAL_PARTICLES, obj);
                draw_obj_particles(USE_EFFECT, obj);
            }
            break;

        case SHIP_PORTAL:
        case MINI_PORTAL:
            if (layer->layerNum == 1) {
                particle_templates[PORTAL_PARTICLES].angle = 180.f - adjust_angle_y(obj->rotation, obj->flippedH);

                set_particle_color(PORTAL_PARTICLES, 255, 31, 255);
                particle_templates[PORTAL_PARTICLES].start_color.a = 127;
                particle_templates[PORTAL_PARTICLES].end_color.a = 255;
                spawn_particle(PORTAL_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
                draw_obj_particles(PORTAL_PARTICLES, obj);
                draw_obj_particles(USE_EFFECT, obj);
            }
            break;

        case ORANGE_MIRROR_PORTAL:
        case ORANGE_TP_PORTAL:
        case UFO_PORTAL:
        case DUAL_PORTAL:
            if (layer->layerNum == 1) {
                particle_templates[PORTAL_PARTICLES].angle = 180.f - adjust_angle_y(obj->rotation, obj->flippedH);

                set_particle_color(PORTAL_PARTICLES, 255, 91, 0);
                particle_templates[PORTAL_PARTICLES].start_color.a = 127;
                particle_templates[PORTAL_PARTICLES].end_color.a = 255;
                spawn_particle(PORTAL_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
                draw_obj_particles(PORTAL_PARTICLES, obj);
                draw_obj_particles(USE_EFFECT, obj);
            }
            break;
        
        case BALL_PORTAL:
            if (layer->layerNum == 1) {
                particle_templates[PORTAL_PARTICLES].angle = 180.f - adjust_angle_y(obj->rotation, obj->flippedH);

                set_particle_color(PORTAL_PARTICLES, 255, 0, 0);
                particle_templates[PORTAL_PARTICLES].start_color.a = 127;
                particle_templates[PORTAL_PARTICLES].end_color.a = 255;
                spawn_particle(PORTAL_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
                draw_obj_particles(PORTAL_PARTICLES, obj);
                draw_obj_particles(USE_EFFECT, obj);
            }
            break;
        case BREAKABLE_BLOCK:
            if (layer->layerNum == 1) {
                particle_templates[BREAKABLE_BRICK_PARTICLES].start_color.a = 255;
                draw_obj_particles(BREAKABLE_BRICK_PARTICLES, obj);
            }
            break;
        case SECRET_COIN:
            if (!obj->activated[state.current_player]) {
                if (level_info.level_is_custom) {
                    set_particle_color(COIN_PARTICLES, 255, 255, 255);
                } else {
                    set_particle_color(COIN_PARTICLES, 255, 255, 0);
                }
                
                spawn_particle(COIN_PARTICLES, *soa_x(obj) - 2, *soa_y(obj), obj);
                spawn_particle(COIN_PARTICLES, *soa_x(obj) - 2, *soa_y(obj), obj);
                spawn_particle(COIN_PARTICLES, *soa_x(obj) - 2, *soa_y(obj), obj);
                draw_obj_particles(COIN_PARTICLES, obj);
            } else {
                draw_obj_particles(BREAKABLE_BRICK_PARTICLES, obj);
                draw_obj_particles(USE_EFFECT, obj);
                draw_obj_particles(ORB_HITBOX_EFFECT, obj);
            }
            break;
        case ROBOT_PORTAL:
            if (layer->layerNum == 1) {
                particle_templates[PORTAL_PARTICLES].angle = 180.f - adjust_angle_y(obj->rotation, obj->flippedH);

                set_particle_color(PORTAL_PARTICLES, 255, 255, 255);
                particle_templates[PORTAL_PARTICLES].start_color.a = 127;
                particle_templates[PORTAL_PARTICLES].end_color.a = 255;
                spawn_particle(PORTAL_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
                draw_obj_particles(PORTAL_PARTICLES, obj);
                draw_obj_particles(USE_EFFECT, obj);
            }
            break;
        case KEY_OBJ:
            if (!obj->activated[state.current_player]) {                
                spawn_particle(KEY_PARTICLES, *soa_x(obj), *soa_y(obj), obj);
                draw_obj_particles(KEY_PARTICLES, obj);
            } else {
                draw_obj_particles(BREAKABLE_BRICK_PARTICLES, obj);
                draw_obj_particles(KEY_OBJ_PART, obj);
            }
            break;
    }
}

int get_fade_value(float x, int right_edge) {
    if (x < 0 || x > right_edge)
        return 0;
    else if (x < FADE_WIDTH)
        return (int)(255.0f * (x / FADE_WIDTH));
    else if (x > right_edge - FADE_WIDTH)
        return (int)(255.0f * ((right_edge - x) / FADE_WIDTH));
    else
        return 255;
}

int get_xy_fade_offset(float x, int right_edge) {
    int fade = get_fade_value(x, right_edge);
    return (255 - fade) / 2;
}

float get_in_scale_fade(float x, int right_edge) {
    int fade = get_fade_value(x, right_edge);
    return (fade / 255.f);
}

float get_out_scale_fade(float x, int right_edge) {
    int fade = 255 - get_fade_value(x, right_edge);
    return 1 + ((fade / 255.f) / 2);
}

void get_fade_vars(GameObject *obj, float x, int *fade_x, int *fade_y, float *fade_scale) {
    switch (*soa_id(obj)) {
        case RAINBOW_ARC_SMALL:
        case RAINBOW_ARC_BIG:
            // Those are hardcoded to not fade
            return;
    }
    
    switch (obj->transition_applied) {
        case FADE_NONE:
            break;
        case FADE_UP:
            *fade_y = get_xy_fade_offset(x, screenWidth);
            break;
        case FADE_DOWN:
            *fade_y = -get_xy_fade_offset(x, screenWidth);
            break;
        case FADE_RIGHT:
            *fade_x = get_xy_fade_offset(x, screenWidth) * state.mirror_mult;
            break;
        case FADE_LEFT:
            *fade_x = -get_xy_fade_offset(x, screenWidth) * state.mirror_mult;
            break;
        case FADE_SCALE_IN:
            *fade_scale = get_in_scale_fade(x, screenWidth);
            break;
        case FADE_SCALE_OUT:
            *fade_scale = get_out_scale_fade(x, screenWidth);
            break;
        case FADE_UP_SLOW:
            *fade_x = get_xy_fade_offset(x, screenWidth) * state.mirror_mult * ((current_fading_effect == FADE_CIRCLE_RIGHT) ? 1 : -1);
        case FADE_UP_STATIONARY:
            *fade_y = get_xy_fade_offset(x, screenWidth) / 3;
            break;
        case FADE_DOWN_SLOW:
            *fade_x = get_xy_fade_offset(x, screenWidth) * state.mirror_mult * ((current_fading_effect == FADE_CIRCLE_RIGHT) ? 1 : -1);
        case FADE_DOWN_STATIONARY:
            *fade_y = -get_xy_fade_offset(x, screenWidth) / 3;
            break;
    }
}

int layer_pulses(GameObject *obj, GDObjectLayer *layer) {
    switch (*soa_id(obj)) {
        case YELLOW_ORB:
        case BLUE_ORB:
        case PINK_ORB:
        case GREEN_ORB:
        case PULSING_CIRCLE:
        case PULSING_CIRCUNFERENCE:
        case PULSING_HEART:
        case PULSING_DIAMOND:
        case PULSING_STAR:
        case PULSING_NOTE:
        case PULSING_SQUARE:
        case PULSING_TRIANGLE:
        case PULSING_HEXAGON:
        case D_ARROW:
        case D_EXMARK:
        case D_QMARK:
        case D_CROSS:
        case D_ARROW_2:
        case PULSING_BIG_CIRCUNFERENCE:
        case PULSING_ARROW_3:
        case PULSING_BIG_SQUARE:
        case PULSING_BIG_SQUARE_OUTLINE:
        case PULSING_BIG_CIRCLE:
            return 1;
        case ROD_BIG:
        case ROD_MEDIUM:
        case ROD_SMALL:
            if (layer->layerNum == 1) {
                return 1;
            }
            break;
    }

    return 0;
}

float get_object_pulse(float amplitude, GameObject *obj) {
    switch (*soa_id(obj)) {
        case YELLOW_ORB:
        case BLUE_ORB:
        case PINK_ORB:
        case GREEN_ORB:
            return map_range(amplitude, 0.f, 1.f, 0.3f, 1.2f);
        case PULSING_CIRCLE:
        case PULSING_CIRCUNFERENCE:
        case PULSING_HEART:
        case PULSING_DIAMOND:
        case PULSING_STAR:
        case PULSING_NOTE:
        case PULSING_SQUARE:
        case PULSING_TRIANGLE:
        case PULSING_HEXAGON:
        case ROD_BIG:
        case ROD_MEDIUM:
        case ROD_SMALL:
            return amplitude;
        case D_ARROW:
        case D_EXMARK:
        case D_QMARK:
        case D_CROSS:
        case D_ARROW_2:
        case PULSING_ARROW_3:
        case PULSING_BIG_SQUARE:
        case PULSING_BIG_SQUARE_OUTLINE:
        case PULSING_BIG_CIRCLE:
        case PULSING_BIG_CIRCUNFERENCE:
            return map_range(amplitude, 0.f, 1.f, 0.6f, 1.2f);
    }
    return amplitude;
}

GRRLIB_texImg *get_randomized_texture(GRRLIB_texImg *image, GameObject *obj, GDObjectLayer *layer) {
    switch (*soa_id(obj)) {
        case GROUND_SPIKE:
            return object_images[GROUND_SPIKE][obj->random % 3];
        case ROD_BIG:
        case ROD_MEDIUM:
        case ROD_SMALL:
            if (layer->layerNum == 1) {
                return object_images[ROD_BIG][level_info.pulsing_type + 1]; // balls start at 1
            }
            break;
        case SECRET_COIN:
            int index = (frame_counter & 0b1100000) >> 5;
            return current_coin_texture[index];
        case BUSH_GROUND_SPIKE:
            return object_images[BUSH_GROUND_SPIKE][obj->random & 0b11];
    }

    return image;
}

GRRLIB_texImg *get_coin_particle_texture() {
    int index = (frame_counter & 0b110000) >> 4;
    return current_coin_texture[index];
}

void load_coin_texture() {
    for (s32 i = 0; i < NUM_COIN_FRAMES; i++) {
        if (level_info.level_is_custom) {
            current_coin_texture[i] = GRRLIB_LoadTexturePNG(user_coin_layer[i].texture);
        } else {
            current_coin_texture[i] = GRRLIB_LoadTexturePNG(secret_coin_layer[i].texture);
        }
        GRRLIB_SetHandle(current_coin_texture[i], current_coin_texture[i]->w / 2, current_coin_texture[i]->h / 2);
    }
}

void unload_coin_texture() {
    for (s32 i = 0; i < NUM_COIN_FRAMES; i++) {
        if (current_coin_texture[i]) {
            GRRLIB_FreeTexture(current_coin_texture[i]);
            current_coin_texture[i] = NULL;
        }
    }
}

int get_opacity(GameObject *obj, float x) {
    int opacity = get_fade_value(x, screenWidth);

    if (obj->object.dont_fade) {
        return 255;
    }

    switch (*soa_id(obj)) {
        case BLACK_FULL:
        case BLACK_EDGE:
        case BLACK_CORNER:
        case BLACK_INS_CORNER:
        case BLACK_FILLER:
        case BLACK_PILLAR_END:
        case BLACK_PILLAR:
        case BLACK_SLOPE_45:
        case BLACK_SLOPE_22_66:
            if (obj->transition_applied == FADE_NONE) opacity = 255;
            break;
            
        case COLORED_FULL:
        case COLORED_EDGE:
        case COLORED_CORNER:
        case COLORED_INS_CORNER:
        case COLORED_FILLER:
        case COLORED_PILLAR_END:
        case COLORED_PILLAR:
        case COLORED_SLOPE_45:
        case COLORED_SLOPE_22_66:
        case COLOR_SLOPE_45_NOOUT:
        case COLOR_SLOPE_22_66_NOOUT:
            bool blending = channels[obj->object.detail_col_channel].blending;
            if (!blending && obj->transition_applied == FADE_NONE) opacity = 255;
            break;
        case RAINBOW_ARC_SMALL:
        case RAINBOW_ARC_BIG:
            // Those are hardcoded to not fade
            return 255;
    }

    return opacity;
}

float get_fading_obj_fade(GameObject *obj, float x, float right_edge) {
    float fading_obj_width = FADING_OBJ_WIDTH * screen_factor_x;
    if (x < FADING_OBJ_PADDING || x > right_edge - FADING_OBJ_PADDING)
        return 1.f;
    else if (x < FADING_OBJ_PADDING + fading_obj_width)
        return clampf(1.f - ((x - FADING_OBJ_PADDING) / fading_obj_width), 0.05f, 1.f);
    else if (x > right_edge - fading_obj_width - FADING_OBJ_PADDING)
        return clampf(1.f - ((right_edge - (x + FADING_OBJ_PADDING)) / fading_obj_width), 0.05f, 1.f);
    else
        return 0.05f;
}

float get_rotation_speed(GameObject *obj) {
    switch (*soa_id(obj)) {
        case SAW_BIG: 
        case SAW_MEDIUM:
        case SAW_SMALL:
        case BLADE_BIG:
        case BLADE_MEDIUM:
        case BLADE_SMALL:
        case BLADE_ALT_BIG:
        case BLADE_ALT_MEDIUM:
        case BLADE_ALT_SMALL:
        case DARKBLADE_BIG:
        case DARKBLADE_MEDIUM:
        case DARKBLADE_SMALL:
        case DARKCOGWHEEL_BIG:
        case DARKCOGWHEEL_MEDIUM:
        case DARKCOGWHEEL_SMALL:
        case LIGHTBLADE_BIG:
        case LIGHTBLADE_MEDIUM:
        case LIGHTBLADE_SMALL:
        case FADING_BLADE_BIG:
        case FADING_BLADE_MEDIUM:
        case FADING_BLADE_SMALL:
            return 360.f;
        
        case SAW_DECO_BIG:
        case SAW_DECO_MEDIUM:
        case SAW_DECO_SMALL:
        case SAW_DECO_TINY:
        case WHEEL_BIG:
        case WHEEL_MEDIUM:
        case WHEEL_SMALL:
        case SPIKEWHEEL_BIG:
        case SPIKEWHEEL_MEDIUM:
        case SPIKEWHEEL_SMALL:
        case CARTWHEEL_BIG:
        case CARTWHEEL_MEDIUM:
        case CARTWHEEL_SMALL:
        case ROUND_CLOUD_BIG:
        case ROUND_CLOUD_MEDIUM:
        case ROUND_CLOUD_SMALL:
        case ROTATING_BALL_BIG:
        case ROTATING_BALL_MEDIUM:
        case ROTATING_BALL_SMALL:
        case ROTATING_BALL_TINY:
        case ROTATING_HEXAGON_BIG:
        case ROTATING_HEXAGON_MEDIUM:
        case ROTATING_HEXAGON_TINY:
        case GREEN_ORB:
        case RING_SEG_01:
        case RING_SEG_02:
        case RING_SEG_03:
        case RING_SEG_04:
        case PICKUP_CIRCLE_1:
        case PICKUP_CIRCLE_2:
        case PICKUP_CIRCLE_3:
            return 180.f;
        case FLASH_RING_1:
            return 180.f + map_range(obj->random & 0xff, 0, 255, -10, 10);
        case FLASH_RING_2:
            return 100.f + map_range(obj->random & 0xff, 0, 255, -10, 10);
        case FLASH_RING_3:
            return 80.f + map_range(obj->random & 0xff, 0, 255, -10, 10);
        case SPIRAL_1:
        case SPIRAL_2:
        case SPIRAL_3:
        case SPIRAL_4:
            return 300.f;
    }
    return 0.f;
}

bool is_modifiable(int col_channel, int color_type) {
    switch(col_channel) {
        case OBJ_BLENDING:
        case LBG_NO_LERP:
            return FALSE;
    }
    return TRUE;
}

const HSV lighter_hsv = {
    .h = 0,
    .s = 0.65,
    .v = 1.30,
    .sChecked = FALSE,
    .vChecked = FALSE,
};

u32 get_layer_color(GameObject *obj, int color_type, int col_channel, float opacity, int def_col_channel) {
    Color color;
    color.r = channels[col_channel].color.r;
    color.g = channels[col_channel].color.g;
    color.b = channels[col_channel].color.b;

    // Handle lighter color channel
    if (col_channel == LIGHTER) {
        color = HSV_combine(channels[obj->object.main_col_channel].color, lighter_hsv);

        if (obj->object.main_col_HSV_enabled) {
            color = HSV_combine(color, obj->object.main_col_HSV);
        }
    }

    if (get_main_channel_id(*soa_id(obj)) <= 0 && obj->object.main_col_HSV_enabled) {
        // Detail only objects use the main slot
        color = HSV_combine(color, obj->object.main_col_HSV);
    } else if (color_type == COLOR_MAIN && obj->object.main_col_HSV_enabled) {
        color = HSV_combine(color, obj->object.main_col_HSV);
    } else if (color_type == COLOR_DETAIL && obj->object.detail_col_HSV_enabled) {
        color = HSV_combine(color, obj->object.detail_col_HSV);
    }

    if (obj->object.num_pulses < 2) {
        // Proceed to reset color
        if (color_type == COLOR_MAIN) {
            obj->object.main_color = color;
        } else if (color_type == COLOR_DETAIL) {
            obj->object.detail_color = color;
        }
    }

    if (obj->object.main_being_pulsed && color_type == COLOR_MAIN) {
        color = obj->object.main_col_pulse;
    } else if (obj->object.detail_being_pulsed && color_type == COLOR_DETAIL) {
        color = obj->object.detail_col_pulse;
        
        // Reapply lighter
        if (col_channel == LIGHTER) {
            color = HSV_combine(obj->object.main_col_pulse, lighter_hsv);

            if (obj->object.main_col_HSV_enabled) {
                color = HSV_combine(color, obj->object.main_col_HSV);
            }
        }
    }

    // Force unmodifiable channels to default channel color
    if (color_type == COLOR_UNMOD) {
        color = channels[def_col_channel].color;
    }

    float group_opacity = 1.f;
    for (int i = 0; i < MAX_GROUPS_PER_OBJECT; i++) {
        Node *p = get_group(obj->groups[i]);
        if (p) group_opacity *= p->opacity;
    }
    obj->opacity = group_opacity;

    float new_opacity = opacity * channels[col_channel].alpha * group_opacity;
    float transformed_opacity = new_opacity;

    if (channels[col_channel].blending) transformed_opacity = CLAMP((0.175656971639325 * powf(7.06033051530761, new_opacity / 255.f) - 0.213355914301931), 0, 1) * 255;
    return RGBA(color.r, color.g, color.b, transformed_opacity);
}

GRRLIB_texImg *get_animated_texture(GameObject *obj, int layer_num, float *scale_out, bool *flip_x) {
    switch (*soa_id(obj)) {
        case FIRE_1:
            return get_frame(fire1_anim, layer_num, obj->object.animation_timer, scale_out, flip_x);
        case FIRE_2:
            return get_frame(fire2_anim, layer_num, obj->object.animation_timer, scale_out, flip_x);
        case FIRE_3:
            return get_frame(fire3_anim, layer_num, obj->object.animation_timer, scale_out, flip_x);
        case FIRE_4:
            return get_frame(fire4_anim, layer_num, obj->object.animation_timer, scale_out, flip_x);
        case ANIMATED_WATER_1:
            return get_frame(water_1_anim, layer_num, state.timer, scale_out, flip_x);
        case ANIMATED_WATER_2:
            return get_frame(water_2_anim, layer_num, state.timer, scale_out, flip_x);
        case ANIMATED_WATER_3:
            return get_frame(water_3_anim, layer_num, state.timer, scale_out, flip_x);
        case ANIMATED_LOADING_1:
            return get_frame(loading_1_anim, layer_num, state.timer, scale_out, flip_x);
        case ANIMATED_LOADING_2:
            return get_frame(loading_2_anim, layer_num, state.timer, scale_out, flip_x);
    }
    return NULL;
}

void put_object_layer(GameObject *obj, float x, float y, GDObjectLayer *layer) {
    int obj_id = *soa_id(obj);

    int layer_index = layer->layerNum;

    struct ObjectLayer *objectLayer = layer->layer;

    GRRLIB_texImg *tex = get_randomized_texture(object_images[obj_id][layer_index], obj, layer);
    float default_scale = 1;
    bool flip_x = FALSE;

    if (objects[obj_id].frame_animation) {
        tex = get_animated_texture(obj, layer->layerNum, &default_scale, &flip_x);
    }

    int x_flip_mult = (obj->flippedH ^ flip_x ? -1 : 1);
    int y_flip_mult = (obj->flippedV ? -1 : 1);

    float x_offset = objectLayer->x_offset * x_flip_mult;
    float y_offset = objectLayer->y_offset * y_flip_mult;

    int width = tex->w;
    int height = tex->h;

    int col_channel = layer->col_channel;

    int blending;
    if (channels[col_channel].blending || layer->blending) {
        blending = GRRLIB_BLEND_ADD;
    } else {
        blending = GRRLIB_BLEND_ALPHA;
    }

    int opacity = get_opacity(obj, x);
    int unmodified_opacity = opacity;
    
    if (objects[obj_id].fades) {
        opacity *= get_fading_obj_fade(obj, x, screenWidth);
    }

    u32 color = get_layer_color(obj, objectLayer->color_type, col_channel, opacity, objectLayer->col_channel);

    // If it is invisible because of blending, skip
    if ((blending == GRRLIB_BLEND_ADD && !(color & ~0xff)) || opacity == 0) return;
        
    float angle_rad = DegToRad(obj->rotation); // Convert degrees to radians
    float cos_a = cosf(angle_rad);
    float sin_a = sinf(angle_rad);

    float x_off_rot = (x_offset * cos_a - y_offset * sin_a) * state.mirror_mult;
    float y_off_rot = x_offset * sin_a + y_offset * cos_a;

    int fade_x = 0;
    int fade_y = 0;

    float fade_scale = 1.f;

    if (!obj->object.dont_enter) get_fade_vars(obj, x, &fade_x, &fade_y, &fade_scale);

    // Get scaling because of music
    if (layer_pulses(obj, layer)) {
        if (level_info.custom_song_id <= 0) {
            obj->ampl_scaling = ease_out(obj->ampl_scaling, get_object_pulse(amplitude, obj), 0.25f);
        } else {
            obj->ampl_scaling = get_object_pulse(amplitude, obj);
        }
        fade_scale *= obj->ampl_scaling;
    }

    fade_scale *= default_scale;

    float rotation = adjust_angle(obj->rotation, 0, state.mirror_mult < 0);

    // Handle rod ball spawn
    switch(obj_id) {
        case ROD_BIG:
        case ROD_MEDIUM:
        case ROD_SMALL:
            if (layer->layerNum == 1) {
                rotation = 0.f;
                x_flip_mult = 1.f;
                y_flip_mult = 1.f;
            }
    }

    if (!obj->object.dont_enter) {
        // Handle special fade types
        if (obj->transition_applied == FADE_DOWN_STATIONARY || obj->transition_applied == FADE_UP_STATIONARY) {
            if (unmodified_opacity < 255) {
                if (x > screenWidth / 2) {
                    x = screenWidth - FADE_WIDTH;
                } else {
                    x = FADE_WIDTH;
                }
            }
        }
    }
    
    if (prev_tex != tex) {
        prev_tex = tex;
        set_texture(tex);
    }
    

    if (blending != prev_blending) {
        GRRLIB_SetBlend(blending);
        prev_blending = blending;
    }


    if (*soa_id(obj) == TEXT_OBJ) {
        draw_rotated_text(
            /* Font     */ *font_charsets[level_info.font_used], level_font, 
            /* X        */ get_mirror_x(x, state.mirror_factor),
            /* Y        */ y,
            /* Rotation */ rotation,
            /* Scale X  */ BASE_SCALE * x_flip_mult * fade_scale * state.mirror_mult * obj->scale_x,
            /* Scale Y  */ BASE_SCALE * y_flip_mult * fade_scale * obj->scale_y, 
            /* Color    */ color,
            /* Text     */ obj->object.text
        );
    } else {
        custom_drawImg(
            /* X        */ get_mirror_x(x, state.mirror_factor) + 6 - (width/2) + x_off_rot + fade_x,
            /* Y        */ y + 6 - (height/2) + y_off_rot + fade_y,
            /* Texture  */ tex, 
            /* Rotation */ rotation, 
            /* Scale X  */ BASE_SCALE * x_flip_mult * fade_scale * state.mirror_mult * obj->scale_x, 
            /* Scale Y  */ BASE_SCALE * y_flip_mult * fade_scale * obj->scale_y, 
            /* Color    */ color
        );
    }
}


void draw_background_image(f32 x, f32 y, bool vflip) {
    for (s32 i = 0; i < BG_DIMENSIONS; i++) {
        float calc_x = i*BG_CHUNK - widthAdjust;
        for (s32 j = 0; j < BG_DIMENSIONS; j++) {
            float calc_y = j*BG_CHUNK;
            
            GRRLIB_SetHandle(bg, 512, 512);
            custom_drawPart(
                x + calc_x, 
                y + (vflip ? (BG_CHUNK * 3) - calc_y : calc_y), 
                i * 256, 
                j * 256,
                256, 
                256,
                bg, 0, 1 * BACKGROUND_SCALE, (vflip ? -1 : 1) * BACKGROUND_SCALE, RGBA(channels[BG].color.r, channels[BG].color.g, channels[BG].color.b, 255)
            );
        }
    }
}

void draw_background(f32 x, f32 y) {
    GX_SetTevOp  (GX_TEVSTAGE0, GX_MODULATE);
    GX_SetVtxDesc(GX_VA_TEX0,   GX_DIRECT);
    set_texture(bg);

    float offset = 1024 * BACKGROUND_SCALE;
    int tiles_x = (screenWidth / offset) + 2;
    int tiles_y = (screenHeight / offset) + 2;

    float calc_x = positive_fmod(x, offset) - widthAdjust;
    float calc_y = positive_fmod(y, offset);

    for (int i = -1; i < tiles_x; i++) {
        for (int j = -1; j < tiles_y; j++) {
            // Calculate position for each tile
            float draw_x = -calc_x + i * offset;
            float draw_y = -calc_y + j * offset;
            
            // Flip every other row
            bool vflip = ((j % 2) != 0);
            draw_background_image(draw_x, draw_y, vflip);
        }
    }
}

float complete_text_elapsed = 0;
void draw_end_wall() {  
    if (level_info.wall_y > 0) {
        spawn_particle(END_WALL_PARTICLES, level_info.wall_x, level_info.wall_y, NULL);
        spawn_particle(END_WALL_PARTICLES, level_info.wall_x, level_info.wall_y, NULL);
        draw_particles(END_WALL_PARTICLES);
        draw_particles(END_WALL_COLL_CIRCLE);
        draw_particles(END_WALL_COLL_CIRCUNFERENCE);
        draw_particles(END_WALL_COMPLETE_CIRCLES);
        draw_particles(END_WALL_FIREWORK);
        draw_particles(END_WALL_TEXT_EFFECT);

        // Render rays
        draw_rays();

        if (completion_timer > 2) {
            float mult = 0.5625f * screen_factor_x;

            float offset_x = (level_complete_texture->w / 2);
            float offset_y = (level_complete_texture->h / 2) + 50 * screen_factor_y;

            float text_scale = easeValue(ELASTIC_OUT, 0, mult, complete_text_elapsed, COMPLETE_TEXT_IN_TIME, 0.6f);
            GRRLIB_SetHandle(level_complete_texture, level_complete_texture->w / 2, level_complete_texture->h / 2);
            GRRLIB_DrawImg(screenWidth / 2 - offset_x, screenHeight / 2 - offset_y, level_complete_texture, 0, text_scale, text_scale, 0xffffffff);
            complete_text_elapsed += dt;
        }
    }

    float calc_x = ((level_info.wall_x - state.camera_x) * SCALE) - widthAdjust;
    float calc_y =  positive_fmod(state.camera_y * SCALE, BLOCK_SIZE_PX) + screenHeight;  
    GX_SetTevOp  (GX_TEVSTAGE0, GX_MODULATE);
    GX_SetVtxDesc(GX_VA_TEX0,   GX_DIRECT);
    if (level_info.wall_y > 0) {
        for (s32 j = 0; j < objects[CHECKER_EDGE].num_layers; j++) {
            GRRLIB_texImg *image = object_images[CHECKER_EDGE][j];
            int width = image->w;
            int height = image->h;
            set_texture(image);

            // Draw each wall block
            for (float i = -BLOCK_SIZE_PX; i < screenHeight + BLOCK_SIZE_PX * 2; i += BLOCK_SIZE_PX) {
                custom_drawImg(
                    get_mirror_x(calc_x, state.mirror_factor) - (width/2) + 6, 
                    calc_y + 6 - i - (height/2),    
                    image,
                    adjust_angle(270, 0, state.mirror_mult < 0),
                    BASE_SCALE * state.mirror_mult, BASE_SCALE,
                    RGBA(255, 255, 255, 255) 
                );
            }
        }
        GRRLIB_SetBlend(GRRLIB_BLEND_ADD);
        
        calc_x = ((level_info.wall_x - 25 - state.camera_x) * SCALE) - widthAdjust;

        // Draw glow
        GRRLIB_texImg *image = object_images[GLOW][0];
        int width = image->w;
        int height = image->h;
        set_texture(image);
        for (float i = -BLOCK_SIZE_PX; i < screenHeight + BLOCK_SIZE_PX * 2; i += BLOCK_SIZE_PX) {
            custom_drawImg(
                get_mirror_x(calc_x, state.mirror_factor) - (width/2) + 6, 
                calc_y + 6 - i - (height/2),    
                image,
                adjust_angle(270, 0, state.mirror_mult < 0),
                BASE_SCALE * state.mirror_mult, BASE_SCALE,
                RGBA(p1.r, p1.g, p1.b, 255) 
            );
        }
    }   
    GRRLIB_SetBlend(GRRLIB_BLEND_ALPHA);
    GX_SetTevOp  (GX_TEVSTAGE0, GX_PASSCLR);
    GX_SetVtxDesc(GX_VA_TEX0,   GX_NONE);
}

#define GROUND_SIZE 176 // pixels
#define LINE_SCALE 0.5f

void draw_ground(f32 y, bool is_ceiling) {
    int mult = (is_ceiling ? -1 : 1);

    // First draw the ground
    float calc_x = 0 - positive_fmod(state.ground_x * SCALE, GROUND_SIZE);
    float calc_y = screenHeight - ((y - state.camera_y) * SCALE);

    for (float i = -GROUND_SIZE; i < screenWidth + GROUND_SIZE; i += GROUND_SIZE) {
        GRRLIB_DrawImg(
            calc_x + i + 6, 
            calc_y + 6,    
            ground,
            0, 1.375, 1.375 * mult,
            RGBA(channels[GROUND].color.r, channels[GROUND].color.g, channels[GROUND].color.b, 255) 
        );
        if (ground_l2) {
            GRRLIB_DrawImg(
                calc_x + i + 6, 
                calc_y + 6,    
                ground_l2,
                0, 1.375, 1.375 * mult,
                RGBA(channels[G2].color.r, channels[G2].color.g, channels[G2].color.b, 255) 
            );
        }
    }

    // Then draw the line
    if (channels[LINE].blending) {
        GRRLIB_SetBlend(GRRLIB_BLEND_ADD);
    }

    int line_width = ground_line->w * screen_factor_x;
    
    GRRLIB_DrawImg(
        (screenWidth / 2) - (line_width / (2 / LINE_SCALE)),
        calc_y + (is_ceiling ? 4 : 6),
        ground_line,
        0, LINE_SCALE * screen_factor_x, 0.75,
        RGBA(channels[LINE].color.r, channels[LINE].color.g, channels[LINE].color.b, 255)
    );
    
    if (channels[LINE].blending) {
        GRRLIB_SetBlend(GRRLIB_BLEND_ALPHA);
    }
}

void handle_special_fading(GameObject *obj, float calc_x, float calc_y) {
    switch (current_fading_effect) {
        case FADE_INWARDS:
            if (calc_y > (screenHeight / 2)) {
                obj->transition_applied = FADE_UP;
            } else {
                obj->transition_applied = FADE_DOWN;
            }
            break;
        case FADE_OUTWARDS:
            if (calc_y > (screenHeight / 2)) {
                obj->transition_applied = FADE_DOWN;
            } else {
                obj->transition_applied = FADE_UP;
            }
            break;
        case FADE_CIRCLE_LEFT:
            if (calc_x > (screenWidth / 2)) {
                if (calc_y > (screenHeight / 2)) {
                    obj->transition_applied = FADE_UP_STATIONARY;
                } else {
                    obj->transition_applied = FADE_DOWN_STATIONARY;
                }
            } else {
                if (calc_y > (screenHeight / 2)) {
                    obj->transition_applied = FADE_UP_SLOW;
                } else {
                    obj->transition_applied = FADE_DOWN_SLOW;
                }
            }
            break;
        case FADE_CIRCLE_RIGHT:
            if (calc_x > (screenWidth / 2)) {
                if (calc_y > (screenHeight / 2)) {
                    obj->transition_applied = FADE_UP_SLOW;
                } else {
                    obj->transition_applied = FADE_DOWN_SLOW;
                }
            } else {
                if (calc_y > (screenHeight / 2)) {
                    obj->transition_applied = FADE_UP_STATIONARY;
                } else {
                    obj->transition_applied = FADE_DOWN_STATIONARY;
                }
            }
            break;
        default:
            obj->transition_applied = current_fading_effect;  
    }   
}

int layersDrawn = 0;

int compare_by_layer_index(const void *a, const void *b) {
    GDLayerSortable *la = *(GDLayerSortable **)a;
    GDLayerSortable *lb = *(GDLayerSortable **)b;

    int layerA = la->layer->layerNum;
    int layerB = lb->layer->layerNum;

    if (layerA != layerB) {
        return layerA - layerB;
    } 

    return la->originalIndex - lb->originalIndex;
}

void play_object_animation(GameObject *obj) {
    switch (*soa_id(obj)) {
        case MONSTER_1:
            playObjAnimation(obj, monster_1_anim, obj->object.animation_timer);
            break;
        case MONSTER_2:
            playObjAnimation(obj, monster_2_anim, obj->object.animation_timer);
            break;
        case MONSTER_3:
            playObjAnimation(obj, monster_3_anim, obj->object.animation_timer);
            break;
        case BLACK_SLUDGE:
            playObjAnimation(obj, black_sludge_anim, obj->object.animation_timer);
            break;
    }
    obj->object.animation_timer += dt;
}

static inline uint32_t make_sort_key(int zlayer, int sheet, int zorder, int index) {
    // Normalize ranges
    uint32_t zl  = (uint32_t)(zlayer + 4);              // -4..11 -> 0..5
    uint32_t sh  = (uint32_t)(0x7 - sheet);             // invert for descending
    uint32_t zo  = (uint32_t)(zorder + 100);            // -100..100 -> 0..200
    uint32_t idx = (uint32_t)(index & 0x1FFFF);         // fit into 17 bits

    return (zl  << 28) |   // top 4 bits
           (sh  << 25) |   // next 3 bits
           (zo  << 17) |   // next 8 bits
           (idx);          // bottom 17 bits
}

#define RADIX 256
#define MASK (RADIX-1)

void radix_sort(SortEntry *arr, int n) {
    if (n <= 1) return;

    SortEntry *tmp = (SortEntry *)malloc(n * sizeof(SortEntry));
    if (!tmp) {
        printf("Failed to allocate memory for layer sorting\n");
        return;
    }

    for (int shift = 0; shift < 32; shift += 8) {
        int count[RADIX] = {0};

        // Count
        for (int i = 0; i < n; i++) {
            int digit = (arr[i].key >> shift) & MASK;
            count[digit]++;
        }

        // Prefix sum
        int sum = 0;
        for (int i = 0; i < RADIX; i++) {
            int tmpc = count[i];
            count[i] = sum;
            sum += tmpc;
        }

        // Place into tmp
        for (int i = 0; i < n; i++) {
            int digit = (arr[i].key >> shift) & MASK;
            tmp[count[digit]++] = arr[i];
        }

        // Copy back
        memcpy(arr, tmp, n * sizeof(SortEntry));
    }

    free(tmp);
}

static inline void insertion_sort_by_layer(GDLayerSortable **arr, int n) {
    for (int i = 1; i < n; i++) {
        GDLayerSortable *key = arr[i];
        int j = i - 1;

        // Sort by layer index ascending
        while (j >= 0 && arr[j]->originalIndex > key->originalIndex) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}

#undef RADIX
#undef MASK

void draw_all_object_layers() {
    u64 t0 = gettime();
    if (GRRLIB_Settings.antialias == false) {
        GX_SetCopyFilter(GX_FALSE, rmode->sample_pattern, GX_FALSE, rmode->vfilter);
    } else {
        GX_SetCopyFilter(rmode->aa, rmode->sample_pattern, GX_TRUE, rmode->vfilter);
    }

    int cam_sx = (int)((state.camera_x + SCREEN_WIDTH_AREA / 2) / GFX_SECTION_SIZE);
    int cam_sy = (int)((state.camera_y + SCREEN_HEIGHT_AREA / 2) / GFX_SECTION_SIZE);

    GDLayerSortable *visible_layers[MAX_VISIBLE_LAYERS];
    int visible_count = 1;

    visible_layers[0] = &gfx_player_layer;

    int width = (SCREEN_WIDTH_AREA / 2) / GFX_SECTION_SIZE + 2;
    int height = (SCREEN_HEIGHT_AREA / 2) / GFX_SECTION_SIZE + 2;

    // Gather layers from all visible sections
    for (int dx = -width; dx <= width; dx++) {
        for (int dy = -height; dy <= height; dy++) {
            GFXSection *sec = get_or_create_gfx_section(cam_sx + dx, cam_sy + dy);
            for (int i = 0; i < sec->layer_count; i++) {
                GameObject *obj = sec->layers[i]->layer->obj;
                
                float calc_x = ((*soa_x(obj) - state.camera_x) * SCALE) - widthAdjust;
                float calc_y = screenHeight - ((*soa_y(obj) - state.camera_y) * SCALE);  
                
                int offscreen_area = 90;

                // Those are way bigger
                switch (*soa_id(obj)) {
                    case RAINBOW_ARC_SMALL:
                        offscreen_area = 120;
                        break;
                    case RAINBOW_ARC_BIG:
                        offscreen_area = 240;
                        break;
                }

                if (!obj->toggled && calc_x > -offscreen_area && calc_x < screenWidth + offscreen_area) {        
                    if (calc_y > -offscreen_area && calc_y < screenHeight + offscreen_area) {    
                        if (visible_count < MAX_VISIBLE_LAYERS) {
                            // Add to visible layers, as it can be seen
                            visible_layers[visible_count++] = sec->layers[i];
                        }
                    }
                }
            }
        }
    }

    // Sort layers
    SortEntry *entries = malloc(visible_count * sizeof(SortEntry));

    for (int i = 0; i < visible_count; i++) {
        GDLayerSortable *ls = visible_layers[i];

        GDObjectLayer *GDlayer = ls->layer;

        struct ObjectLayer *layer = GDlayer->layer;

        GameObject *obj = GDlayer->obj;
        
        int zlayer = ls->zlayer + layer->zlayer_offset;
            
        // Player is always index 0
        if (i > 0 && objects[*soa_id(obj)].spritesheet_layer == SHEET_BLOCKS) {
            int col_channel = GDlayer->col_channel;
            bool blending = channels[col_channel].blending || GDlayer->blending;

            // Two color objects must have both channels be blending
            if (obj->has_two_channels) {
                blending = GDlayer->blending || obj->both_channels_blending;
            }

            zlayer -= (blending ^ ((zlayer & 1) == 0));
        }

        int sheet  = objects[*soa_id(obj)].spritesheet_layer;
        int zorder = obj->object.zorder;
        int index  = ls->originalIndex;

        entries[i].key = make_sort_key(zlayer, sheet, zorder, index);
        entries[i].ptr = ls;
    }

    radix_sort(entries, visible_count);


    // Sort same ID objects so instead of layer 0 layer 1 layer 0 layer 1, its layer 0, layer 0, layer 1, layer 1
    int i = 0;
    while (i < visible_count) {
        GDLayerSortable *first = visible_layers[i];
        int obj_id = *soa_id(first->layer->obj);

        int j = i + 1;
        while (j < visible_count &&
            *soa_id(visible_layers[j]->layer->obj) == obj_id) {
            j++;
        }

        int run_length = j - i;
        if (run_length > 1) {
            insertion_sort_by_layer(&visible_layers[i], run_length);
        }

        i = j;
    }

    
    u64 t1 = gettime();
    layer_sorting = ticks_to_microsecs(t1 - t0) / 1000.f;
    
    draw_particles(GLITTER_EFFECT);
    layersDrawn = visible_count;

    prev_blending = -1;

    // Draw in sorted order
    for (int i = 0; i < visible_count; i++) {
        GDObjectLayer *layer = entries[i].ptr->layer;
        GameObject *obj = layer->obj;

        int obj_id = *soa_id(obj);

        if (obj_id == PLAYER_OBJECT) {
            // Draw player related stuff
            u64 t2 = gettime();
            draw_particles(DEATH_CIRCLE);
            draw_particles(DEATH_PARTICLES);
            draw_particles(CUBE_DRAG);
            draw_particles(SHIP_TRAIL);
            draw_particles(HOLDING_SHIP_TRAIL);
            draw_particles(UFO_JUMP);
            draw_particles(UFO_TRAIL);
            draw_particles(ROBOT_JUMP_PARTICLES);
            draw_particles(DUAL_BALL_HITBOX_EFFECT);
            draw_particles(P1_TRAIL);
            
            if (death_timer <= 0) {
                trail = trail_p1;
                wave_trail = wave_trail_p1;
                state.current_player = 0;
                draw_player(&state.player);
                trail_p1 = trail;
                wave_trail_p1 = wave_trail;

                if (state.dual) {
                    Color temp = p1;
                    p1 = p2;
                    p2 = temp;
                    trail = trail_p2;
                    wave_trail = wave_trail_p2;
                    state.current_player = 1;
                    draw_player(&state.player2);
                    trail_p2 = trail;
                    wave_trail_p2 = wave_trail;
                    temp = p1;
                    p1 = p2;
                    p2 = temp;
                }
            }
            draw_particles(SHIP_DRAG);
            draw_particles(COIN_OBJ);
            u64 t3 = gettime();
            player_draw_time = ticks_to_microsecs(t3 - t2) / 1000.f;

            // Restore variables
            set_texture(prev_tex);
            GRRLIB_SetBlend(prev_blending);
        } else if (obj_id < OBJECT_COUNT) {
            u64 t0 = gettime();
            float calc_x = ((*soa_x(obj) - state.camera_x) * SCALE) - widthAdjust;
            float calc_y = screenHeight - ((*soa_y(obj) - state.camera_y) * SCALE);  

            int fade_val = get_fade_value(calc_x, screenWidth);
            bool fade_edge = (fade_val == 255 || fade_val == 0);
            bool is_layer0 = (layer->layerNum == 0);

            if (is_layer0) {
                // Fade objects
                if (fade_edge) handle_special_fading(obj, calc_x, calc_y);
                // If saw, rotate
                if ((objects[obj_id].is_saw || *soa_id(obj) == GREEN_ORB) && !state.paused) {
                    obj->rotation += (((obj->random & 1) ? -get_rotation_speed(obj) : get_rotation_speed(obj))) * dt ;
                }

                if (objects[obj_id].frame_animation) {
                    obj->object.animation_timer += dt;
                }

                if (obj->has_two_channels && channels[obj->object.main_col_channel].blending && channels[obj->object.detail_col_channel].blending) {
                    obj->both_channels_blending = TRUE;
                } else {
                    obj->both_channels_blending = FALSE;
                }
            } 

            handle_pre_draw_object_particles(obj, layer); 
            u64 t1 = gettime();
            obj_particles_time += t1 - t0;

            t0 = gettime();
            if (is_layer0 && objects[*soa_id(obj)].has_movement) {
                play_object_animation(obj);
                set_texture(prev_tex);
                GRRLIB_SetBlend(prev_blending);
            }
            else if (!obj->hide_sprite) put_object_layer(obj, calc_x, calc_y, layer);
            t1 = gettime();
            draw_time += t1 - t0;
            
            t0 = gettime();
            handle_post_draw_object_particles(obj, layer); 
            t1 = gettime();
            obj_particles_time += t1 - t0;
        }
    }

    draw_particles(SPEEDUP);
    
    prev_tex = NULL;
    prev_blending = GRRLIB_BLEND_ALPHA;
    GRRLIB_SetBlend(GRRLIB_BLEND_ALPHA);

    float screen_x_max = screenWidth + 90.0f;
    float screen_y_max = screenHeight + 90.0f;

    if (state.hitbox_display) { 
        GX_LoadPosMtxImm(GXmodelView2D, GX_PNMTX0);
        GX_SetTevOp(GX_TEVSTAGE0, GX_PASSCLR);
        GX_SetVtxDesc(GX_VA_TEX0,   GX_NONE);
        for (int dx = -width; dx <= width; dx++) {
            for (int dy = -height; dy <= height; dy++) {
                Section *sec = get_or_create_section(cam_sx + dx, cam_sy + dy);
                for (int i = 0; i < sec->object_count; i++) {
                    GameObject *obj = sec->objects[i];
                    
                    float calc_x = ((*soa_x(obj) - state.camera_x) * SCALE) - widthAdjust;
                    float calc_y = screenHeight - ((*soa_y(obj) - state.camera_y) * SCALE);  
                    if (calc_x > -90 && calc_x < screen_x_max) {        
                        if (calc_y > -90 && calc_y < screen_y_max) {    
                            draw_hitbox(obj);
                        }
                    }
                }
            }
        }

        if (state.hitbox_display == 2) draw_hitbox_trail(0);
        draw_player_hitbox(&state.player);

        if (state.dual) {
            if (state.hitbox_display == 2) draw_hitbox_trail(1);
            draw_player_hitbox(&state.player2);
        }
        
        GX_SetVtxDesc(GX_VA_TEX0,   GX_DIRECT);
        GX_SetTevOp(GX_TEVSTAGE0, GX_MODULATE);
    }

    draw_time = ticks_to_microsecs(draw_time) / 1000.f;
    obj_particles_time = ticks_to_microsecs(obj_particles_time) / 1000.f;
    
    free(entries);
    
    GX_LoadPosMtxImm(GXmodelView2D, GX_PNMTX0);
}

u64 last_beat_time = 0;
int pulse_frames_left = 0;
int beat_pulse = 0;

void update_beat() {
    const float beat_interval_ms = 60000.f / songs[level_info.song_id].tempo;

    u64 now = gettime();
    u64 elapsed_ms = ticks_to_millisecs(now - last_beat_time);

    // Check if we've hit the next beat
    if (elapsed_ms >= beat_interval_ms) {
        last_beat_time = now;
        pulse_frames_left = 20;
        beat_pulse = true;
    }

    // Countdown pulse frames
    if (pulse_frames_left > 0) {
        pulse_frames_left--;
        beat_pulse = true;
    } else {
        beat_pulse = false;
    }

    if (MP3Player_GetAmplitude() < 0.05f) {
        beat_pulse = false;
    }
}

void handle_objects() {
    int sx = (int)(state.player.x / SECTION_SIZE);
    for (int dx = -1; dx <= 1; dx++) {
        for (int sy = -(400 / SECTION_SIZE); sy <= MAX_LEVEL_HEIGHT / SECTION_SIZE; sy++) {
            Section *sec = get_or_create_section(sx + dx, sy);
            for (int i = 0; i < sec->object_count; i++) {
                GameObject *obj = sec->objects[i];
                handle_triggers(obj);
            }
        }
    }
    calculate_lbg();
    update_triggers();
}

bool is_object_unimplemented(int id) {
    if (id >= OBJECT_COUNT) return TRUE;

    const unsigned char *p = (const unsigned char*) &objects[id];
    for (size_t i = 0; i < sizeof(ObjectDefinition); i++) {
        if (p[i] != 0) {
            return FALSE;
        }
    }
    return TRUE;
}