added basic leaderboard fetching
[fishladder.git] / fishladder.c
index da57230e80267f4264bc9d238ba2a3101d080907..7f47fdb54a49c96a1a5e9cfe8a913f88ef590861 100644 (file)
@@ -1,9 +1,39 @@
 // Copyright (C) 2021 Harry Godden (hgn) - All Rights Reserved
 
-//#define VG_STEAM
+#define VG_STEAM
+#define VG_STEAM_APPID 1218140U
 #include "vg/vg.h"
 #include "fishladder_resources.h"
 
+/*
+       Todo for release:
+               Tutorial levels:
+                       1. Transport
+                       2. Split
+                       3. Merge (and explode)
+                       4. Principle 1 (divide colours)
+                       5. Principle 2 (combine colours)
+                       
+               Trainee levels:
+                       Simple maths                    (x3)
+                       Colour ordering                 (x2)
+                       Routing problems                (x2)
+                       
+               Medium levels:
+                       Reverse order
+               
+               New things to program:
+                       UI text element renderer (SDF)                          DONE(sorta)
+                       Particle system thing for ball collision        
+                       Level descriptions / titles                                     HALF
+                       Row Gridlines for I/O
+                       Play button / Speed controller
+                       
+                       
+       After release:
+               
+*/
+
 const char *level_pack_1[] = { 
        "level0", 
        "level1", 
@@ -121,16 +151,68 @@ m3x3f m_mdl;
                |          |    |     |    |     |    |
 */
 
+struct cell_description
+{
+       v2i start;
+       v2i end;
+       
+       int is_special;
+       int is_linear;
+}
+cell_descriptions[] =
+{
+       // 0-3
+       {},
+       { .start = {  1,  0 }, .end = { -1,  0 } },
+       { .start = {  0,  1 }, .end = {  0, -1 } },
+       { .start = {  0,  1 }, .end = {  1,  0 } },
+       // 4-7
+       { .start = { -1,  0 }, .end = {  1,  0 } },
+       { .start = { -1,  0 }, .end = {  1,  0 }, .is_linear = 1 },
+       { .start = {  0,  1 }, .end = { -1,  0 } },
+       { .start = {  0,  1 }, .is_special = 1 },
+       // 8-11
+       { .start = {  0, -1 }, .end = {  0,  1 } },
+       { .start = {  1,  0 }, .end = {  0, -1 } },
+       { .start = {  0,  1 }, .end = {  0, -1 }, .is_linear = 1 },
+       { },
+       // 12-15
+       { .start = { -1,  0 }, .end = {  0, -1 } },
+       { .end = { 0, -1 }, .is_special = 1 },
+       { },
+       { }
+};
+
 enum cell_type
 {
+       k_cell_type_stub = 0,
        k_cell_type_ramp_right = 3,
        k_cell_type_ramp_left = 6,
        k_cell_type_split = 7,
-       k_cell_type_merge = 13
+       k_cell_type_merge = 13,
+       k_cell_type_con_r = 1,
+       k_cell_type_con_u = 2,
+       k_cell_type_con_l = 4,
+       k_cell_type_con_d = 8
 };
 
+v2f const curve_3[] = {{0.5f,1.0f},{0.5f,0.625f},{0.625f,0.5f},{1.0f,0.5f}};
+v2f const curve_6[] = {{0.5f,1.0f},{0.5f,0.625f},{0.375f,0.5f},{0.0f,0.5f}};
+v2f const curve_9[] = {{1.0f,0.5f},{0.625f,0.5f},{0.5f,0.375f},{0.5f,0.0f}};
+v2f const curve_12[]= {{0.0f,0.5f},{0.375f,0.5f},{0.5f,0.375f},{0.5f,0.0f}};
+
+v2f const curve_1[] = {{1.0f,0.5f},{0.8f,0.5f},{0.3f,0.5f},{0.2f,0.5f}};
+v2f const curve_4[] = {{0.0f,0.5f},{0.3f,0.5f},{0.5f,0.5f},{0.8f,0.5f}};
+v2f const curve_2[] = {{0.5f,1.0f},{0.5f,0.8f},{0.5f,0.3f},{0.5f,0.2f}};
+v2f const curve_8[] = {{0.5f,0.0f},{0.5f,0.3f},{0.5f,0.5f},{0.5f,0.8f}};
+
+v2f const curve_7[] = {{0.5f,0.8438f},{0.875f,0.8438f},{0.625f,0.5f},{1.0f,0.5f}};
+v2f const curve_7_1[] = {{0.5f,0.8438f},{1.0f-0.875f,0.8438f},{1.0-0.625f,0.5f},{0.0f,0.5f}};
+
+float const curve_7_linear_section = 0.1562f;
+
 v3f colour_sets[] =
-{ { 0.9f, 0.6f, 0.20f },
+{ { 1.0f, 0.9f, 0.3f },
   { 0.2f, 0.9f, 0.14f },
   { 0.4f, 0.8f, 1.00f } };
 
@@ -188,6 +270,15 @@ static void use_mesh( struct mesh *m )
        glBindVertexArray( m->vao );
 }
 
+enum e_fish_state
+{
+       k_fish_state_soon_dead = -1,
+       k_fish_state_dead = 0,
+       k_fish_state_alive,
+       k_fish_state_bg,
+       k_fish_state_soon_alive
+};
+
 struct world
 {
 #pragma pack(push,1)
@@ -210,6 +301,7 @@ struct world
        int simulating;
        int sim_run, max_runs;
        
+       float sim_speed;
        float frame_lerp;
        
        struct cell_terminal
@@ -252,8 +344,9 @@ struct world
        {
                v2i pos;
                v2i dir;
-               int alive;
+               enum e_fish_state state;
                char payload;
+               int flow_reversed;
                float death_time;
                v2f physics_v;
                v2f physics_co;
@@ -262,13 +355,12 @@ struct world
        
        int num_fishes;
        
-       char map_name[128];
+       char map_name[64];
        struct career_level *ptr_career_level;
        
        u32 score;
        u32 completed;
        u32 time;
-       
 } world = {};
 
 static void map_free(void)
@@ -310,9 +402,9 @@ static int map_load( const char *str, const char *name )
        // Scan for width
        for(;; world.w ++)
        {
-               if( str[world.w] == ';' )
+               if( c[world.w] == ';' )
                        break;
-               else if( !str[world.w] )
+               else if( !c[world.w] )
                {
                        vg_error( "Unexpected EOF when parsing level\n" );
                        return 0;
@@ -645,6 +737,7 @@ static void map_serialize( FILE *stream )
 int main( int argc, char *argv[] )
 {
        vg_init( argc, argv, "Marble Computing | SPACE: Test | LeftClick: Toggle tile | RightClick: Drag wire" );
+       return 0;
 }
 
 static int console_credits( int argc, char const *argv[] )
@@ -752,6 +845,7 @@ static void simulation_stop(void)
        world.simulating = 0;
        world.num_fishes = 0;
        world.sim_frame = 0;
+       world.sim_run = 0;
        
        io_reset();
        
@@ -780,8 +874,12 @@ static int console_changelevel( int argc, char const *argv[] )
        return 0;
 }
 
+void leaderboard_found( LeaderboardFindResult_t *pCallback );
 void vg_start(void)
 {
+       // Steamworks callbacks
+       sw_leaderboard_found = &leaderboard_found;
+
        vg_function_push( (struct vg_cmd){
                .name = "_map_write",
                .function = console_save_map
@@ -959,10 +1057,15 @@ void vg_free(void)
        map_free();
 }
 
+static int world_check_pos_ok( v2i co )
+{
+       return (co[0] < 2 || co[0] >= world.w-2 || co[1] < 2 || co[1] >= world.h-2)? 0: 1;
+}
+
 static int cell_interactive( v2i co )
 {
        // Bounds check
-       if( co[0] < 2 || co[0] >= world.w-2 || co[1] < 2 || co[1] >= world.h-2 )
+       if( !world_check_pos_ok( co ) )
                return 0;
 
        // Flags check
@@ -1135,22 +1238,6 @@ static void map_reclassify( v2i start, v2i end, int update_texbuffer )
        }
 }
 
-
-v2f const curve_3[] = {{0.5f,1.0f},{0.5f,0.625f},{0.625f,0.5f},{1.0f,0.5f}};
-v2f const curve_6[] = {{0.5f,1.0f},{0.5f,0.625f},{0.375f,0.5f},{0.0f,0.5f}};
-v2f const curve_9[] = {{1.0f,0.5f},{0.625f,0.5f},{0.5f,0.375f},{0.5f,0.0f}};
-v2f const curve_12[]= {{0.0f,0.5f},{0.375f,0.5f},{0.5f,0.375f},{0.5f,0.0f}};
-
-v2f const curve_1[] = {{1.0f,0.5f},{0.8f,0.5f},{0.3f,0.5f},{0.2f,0.5f}};
-v2f const curve_4[] = {{0.0f,0.5f},{0.3f,0.5f},{0.5f,0.5f},{0.8f,0.5f}};
-v2f const curve_2[] = {{0.5f,1.0f},{0.5f,0.8f},{0.5f,0.3f},{0.5f,0.2f}};
-v2f const curve_8[] = {{0.5f,0.8f},{0.5f,0.5f},{0.5f,0.3f},{0.5f,0.0f}};
-
-v2f const curve_7[] = {{0.5f,0.8438f},{0.875f,0.8438f},{0.625f,0.5f},{1.0f,0.5f}};
-v2f const curve_7_1[] = {{0.5f,0.8438f},{1.0f-0.875f,0.8438f},{1.0-0.625f,0.5f},{0.0f,0.5f}};
-
-float const curve_7_linear_section = 0.1562f;
-
 u16 id_drag_from = 0;
 v2f drag_from_co;
 v2f drag_to_co;
@@ -1183,7 +1270,7 @@ void vg_update(void)
        world.tile_y = floorf( world.tile_pos[1] );
 
        // Tilemap editing
-       if( !world.simulating )
+       if( !world.simulating && !gui_want_mouse() )
        {
                v2_copy( vg_mouse_ws, drag_to_co );
        
@@ -1305,6 +1392,7 @@ void vg_update(void)
                        world.sim_frame = 0;
                        world.sim_start = vg_time;
                        world.sim_run = 0;
+                       world.sim_speed = 2.5f;
                        
                        for( int i = 0; i < world.w*world.h; i ++ )
                                world.data[ i ].state &= ~FLAG_FLIP_FLOP;
@@ -1316,7 +1404,7 @@ void vg_update(void)
        // Fish ticks
        if( world.simulating )
        {       
-               while( world.sim_frame < (int)((vg_time-world.sim_start)*2.0f) )
+               while( world.sim_frame < (int)((vg_time-world.sim_start)*world.sim_speed) )
                {
                        //vg_info( "frame: %u\n", world.sim_frame );
                        sfx_set_playrnd( &audio_random, &audio_system_balls_switching, 0, 9 );
@@ -1340,106 +1428,145 @@ void vg_update(void)
                        {
                                struct fish *fish = &world.fishes[i];
                                
-                               if( fish->alive == -1 )
-                                       fish->alive = 0;
+                               if( fish->state == k_fish_state_soon_dead )
+                                       fish->state = k_fish_state_dead;
                                
-                               if( fish->alive != 1 )
+                               if( fish->state == k_fish_state_soon_alive )
+                                       fish->state = k_fish_state_alive;
+                               
+                               if( fish->state < k_fish_state_alive )
                                        continue;
                                
                                struct cell *cell_current = pcell( fish->pos );
 
-                               // Apply to output
-                               if( cell_current->state & FLAG_OUTPUT )
+                               if( fish->state == k_fish_state_alive )
                                {
-                                       for( int j = 0; j < arrlen( world.io ); j ++ )
+                                       // Apply to output
+                                       if( cell_current->state & FLAG_OUTPUT )
                                        {
-                                               struct cell_terminal *term = &world.io[j];
-                                               
-                                               if( term->id == fish->pos[1]*world.w + fish->pos[0] )
+                                               for( int j = 0; j < arrlen( world.io ); j ++ )
                                                {
-                                                       struct terminal_run *run = &term->runs[ world.sim_run ];
-                                                       if( run->recv_count < vg_list_size( run->recieved ) )
-                                                               run->recieved[ run->recv_count ++ ] = fish->payload;
+                                                       struct cell_terminal *term = &world.io[j];
                                                        
-                                                       break;
+                                                       if( term->id == fish->pos[1]*world.w + fish->pos[0] )
+                                                       {
+                                                               struct terminal_run *run = &term->runs[ world.sim_run ];
+                                                               if( run->recv_count < vg_list_size( run->recieved ) )
+                                                                       run->recieved[ run->recv_count ++ ] = fish->payload;
+                                                               
+                                                               break;
+                                                       }
                                                }
+                                               
+                                               fish->state = k_fish_state_dead;
+                                               continue;
                                        }
                                        
-                                       fish->alive = 0;
-                                       continue;
-                               }
-                               
-                               if( cell_current->config == k_cell_type_split )
-                               {
-                                       // Flip flop L/R
-                                       fish->dir[0] = cell_current->state&FLAG_FLIP_FLOP?1:-1;
-                                       fish->dir[1] = 0;
-                                       
-                                       if( !(cell_current->state & FLAG_TARGETED) )
-                                               cell_current->state ^= FLAG_FLIP_FLOP;
-                               }
-                               else if( cell_current->config == k_cell_type_merge )
-                               {
-                                       // Can only move up
-                                       fish->dir[0] = 0;
-                                       fish->dir[1] = -1;
-                               }
-                               else
-                               {
-                                       struct cell *cell_next = pcell( (v2i){ fish->pos[0]+fish->dir[0], fish->pos[1]+fish->dir[1] } );
-                                       if( !(cell_next->state & (FLAG_CANAL|FLAG_OUTPUT)) )
+
+                                       if( cell_current->config == k_cell_type_merge )
                                        {
-                                               // Try other directions for valid, so down, left, right..
-                                               v2i dirs[] = {{1,0},{-1,0},{0,-1}};
-                                               //vg_info( "Trying some other directions...\n" );
-                                               
-                                               for( int j = 0; j < vg_list_size(dirs); j ++ )
+                                               // Can only move up
+                                               fish->dir[0] = 0;
+                                               fish->dir[1] = -1;
+                                               fish->flow_reversed = 0;
+                                       }
+                                       else
+                                       {
+                                               if( cell_current->config == k_cell_type_split )
+                                               {
+                                                       // Flip flop L/R
+                                                       fish->dir[0] = cell_current->state&FLAG_FLIP_FLOP?1:-1;
+                                                       fish->dir[1] = 0;
+                                                       
+                                                       if( !(cell_current->state & FLAG_TARGETED) )
+                                                               cell_current->state ^= FLAG_FLIP_FLOP;
+                                               }
+                                               else
                                                {
-                                                       if( (dirs[j][0] == -fish->dir[0]) && (dirs[j][1] == -fish->dir[1]) )
-                                                               continue;
+                                                       // Apply cell out-flow
+                                                       struct cell_description *desc = &cell_descriptions[ cell_current->config ];
+                                                       
+                                                       v2i_copy( fish->flow_reversed? desc->start: desc->end, fish->dir );
+                                               }
+                                               
+                                               v2i pos_next;
+                                               v2i_add( fish->pos, fish->dir, pos_next );
+                                               
+                                               struct cell *cell_next = pcell( pos_next );
                                                
-                                                       if( pcell( (v2i){ fish->pos[0]+dirs[j][0], fish->pos[1]+dirs[j][1] } )->state & (FLAG_CANAL|FLAG_OUTPUT) )
+                                               if( cell_next->state & (FLAG_CANAL|FLAG_OUTPUT) )
+                                               {
+                                                       struct cell_description *desc = &cell_descriptions[ cell_next->config ];
+                                                       
+                                                       if( cell_next->config == k_cell_type_merge )
                                                        {
-                                                               fish->dir[0] = dirs[j][0];
-                                                               fish->dir[1] = dirs[j][1];
+                                                               if( fish->dir[0] == 0 )
+                                                                       fish->state = k_fish_state_dead;
+                                                               else
+                                                                       fish->flow_reversed = 0;
+                                                       }
+                                                       else
+                                                       {
+                                                               if( cell_next->config == k_cell_type_split )
+                                                               {
+                                                                       if( fish->dir[0] == 0 )
+                                                                       {
+                                                                               sfx_set_playrnd( &audio_splitter, &audio_system_balls_important, 0, 1 );
+                                                                               cell_next->state |= FLAG_FLIP_ROTATING;
+                                                                               
+                                                                               fish->flow_reversed = 0;
+                                                                       }
+                                                                       else
+                                                                               fish->state = k_fish_state_dead;
+                                                               }
+                                                               else
+                                                                       fish->flow_reversed = ( fish->dir[0] != -desc->start[0] || 
+                                                                                                                                       fish->dir[1] != -desc->start[1] )? 1: 0;
                                                        }
                                                }
+                                               else
+                                                       fish->state = world_check_pos_ok( fish->pos )? k_fish_state_bg: k_fish_state_dead;
                                        }
+                                       
+                                       //v2i_add( fish->pos, fish->dir, fish->pos );
                                }
-                               
-                               fish->pos[0] += fish->dir[0];
-                               fish->pos[1] += fish->dir[1];
-                               
-                               struct cell *cell_entry = pcell( fish->pos );
-                               
-                               if( !(cell_entry->state & (FLAG_INPUT|FLAG_CANAL|FLAG_OUTPUT) ))
-                                       fish->alive = 0;
-                               else
+                               else if( fish->state == k_fish_state_bg )
                                {
-                                       if( fish->dir[0] )
+                                       v2i_add( fish->pos, fish->dir, fish->pos );
+                                       
+                                       if( !world_check_pos_ok( fish->pos ) )
+                                               fish->state = k_fish_state_dead;
+                                       else
                                        {
-                                               if( cell_entry->config == k_cell_type_split ||
-                                                        cell_entry->config == k_cell_type_ramp_right ||
-                                                        cell_entry->config == k_cell_type_ramp_left )
+                                               struct cell *cell_entry = pcell( fish->pos );
+                                               
+                                               if( cell_entry->state & FLAG_CANAL )
                                                {
-                                                       // Special death (FALL)                                                 
-                                                       v2_sub( fish->physics_co, fish->physics_v, fish->physics_v );
-                                                       v2_divs( fish->physics_v, vg_time_delta, fish->physics_v );
-
-                                                       fish->alive = -2;
-                                                       vg_warn( "Special death (fall)\n" );
-                                                       continue;
+                                                       if( cell_entry->config == k_cell_type_con_r || cell_entry->config == k_cell_type_con_u 
+                                                               || cell_entry->config == k_cell_type_con_l || cell_entry->config == k_cell_type_con_d )
+                                                       {
+                                                               sw_set_achievement( "CAN_DO_THAT" );
+                                                       
+                                                               fish->state = k_fish_state_soon_alive;
+                                                               
+                                                               fish->dir[0] = 0;
+                                                               fish->dir[1] = 0;
+                                                               fish->flow_reversed = 1;
+                                                               
+                                                               switch( cell_entry->config )
+                                                               {
+                                                                       case k_cell_type_con_r: fish->dir[0] =  1; break;
+                                                                       case k_cell_type_con_l: fish->dir[0] = -1; break;
+                                                                       case k_cell_type_con_u: fish->dir[1] =  1; break;
+                                                                       case k_cell_type_con_d: fish->dir[1] = -1; break;
+                                                               }
+                                                       }
                                                }
                                        }
-                               
-                                       if( cell_entry->config == k_cell_type_split )
-                                       {
-                                               sfx_set_playrnd( &audio_splitter, &audio_system_balls_important, 0, 1 );
-                                               cell_entry->state |= FLAG_FLIP_ROTATING;
-                                       }
                                }
+                               else { vg_error( "fish behaviour unimplemented for behaviour type (%d)\n" ); }
                                
-                               if( fish->alive )
+                               if( fish->state >= k_fish_state_alive )
                                        alive_count ++;
                        }
                        
@@ -1448,8 +1575,9 @@ void vg_update(void)
                        {
                                struct fish *fish = &world.fishes[i];
                                
-                               if( fish->alive == 1 )
+                               if( fish->state == k_fish_state_alive )
                                {
+                                       v2i_add( fish->pos, fish->dir, fish->pos );
                                        struct cell *cell_current = pcell( fish->pos );
 
                                        if( cell_current->state & FLAG_IS_TRIGGER )
@@ -1474,22 +1602,58 @@ void vg_update(void)
                        }
                        
                        // Third pass (collisions)
+                       struct fish *fi, *fj;
+                       
                        for( int i = 0; i < world.num_fishes; i ++ )
                        {
-                               if( world.fishes[i].alive == 1 )
+                               fi = &world.fishes[i];
+                               
+                               if( fi->state == k_fish_state_alive )
                                {
+                                       int continue_again = 0;
+                               
                                        for( int j = i+1; j < world.num_fishes; j ++ )
                                        {
-                                               if( (world.fishes[j].alive == 1) && (world.fishes[i].pos[0] == world.fishes[j].pos[0]) &&
-                                                        (world.fishes[i].pos[1] == world.fishes[j].pos[1]) )
+                                               fj = &world.fishes[j];
+                                               
+                                               if( (fj->state == k_fish_state_alive) )
                                                {
-                                                       // Shatter death (+0.5s)
-                                                       world.fishes[i].alive = -1;
-                                                       world.fishes[j].alive = -1;
-                                                       world.fishes[i].death_time = 0.5f;
-                                                       world.fishes[j].death_time = 0.5f;
+                                                       v2i fi_prev;
+                                                       v2i fj_prev;
+                                                       
+                                                       v2i_sub( fi->pos, fi->dir, fi_prev );
+                                                       v2i_sub( fj->pos, fj->dir, fj_prev );
+                                               
+                                                       int 
+                                                       collide_next_frame = ( 
+                                                                (fi->pos[0] == fj->pos[0]) &&
+                                                                (fi->pos[1] == fj->pos[1]))? 1: 0,
+                                                       collide_this_frame = (
+                                                                (fi_prev[0] == fj->pos[0]) &&
+                                                                (fi_prev[1] == fj->pos[1]) &&
+                                                                (fj_prev[0] == fi->pos[0]) &&
+                                                                (fj_prev[1] == fi->pos[1])
+                                                               )? 1: 0;
+                                                       
+                                                       if( collide_next_frame || collide_this_frame )
+                                                       {
+                                                               sw_set_achievement( "BANG" );
+                                                       
+                                                               // Shatter death (+0.5s)
+                                                               float death_time = collide_this_frame? 0.0f: 0.5f;
+                                                               
+                                                               fi->state = k_fish_state_soon_dead;
+                                                               fj->state = k_fish_state_soon_dead;
+                                                               fi->death_time = death_time;
+                                                               fj->death_time = death_time;
+                                                               
+                                                               continue_again = 1;
+                                                               break;
+                                                       }
                                                }
                                        }
+                                       if( continue_again )
+                                               continue;
                                }
                        }
                        
@@ -1505,28 +1669,24 @@ void vg_update(void)
                                {
                                        if( world.sim_frame < term->runs[ world.sim_run ].condition_count )
                                        {
-                                               struct fish *fish = &world.fishes[world.num_fishes++];
+                                               struct fish *fish = &world.fishes[ world.num_fishes ];
                                                fish->pos[0] = posx;
                                                fish->pos[1] = posy;
-                                               fish->alive = 1;
+                                               fish->state = k_fish_state_alive;
                                                fish->payload = term->runs[ world.sim_run ].conditions[ world.sim_frame ];
                                                
-                                               int can_spawn = 0;
+                                               struct cell *cell_ptr = pcell( fish->pos );
                                                
-                                               v2i dirs[] = {{1,0},{-1,0},{0,-1}};
-                                               for( int j = 0; j < vg_list_size(dirs); j ++ )
-                                                       if( pcell( (v2i){ posx+dirs[j][0], posy+dirs[j][1] } )->state & FLAG_CANAL )
-                                                       {
-                                                               fish->dir[0] = dirs[j][0];
-                                                               fish->dir[1] = dirs[j][1];
-                                                               can_spawn = 1;
-                                                               break;
-                                                       }
-                                               
-                                               if( !can_spawn )
-                                                       world.num_fishes--;
-                                               else
+                                               if( cell_ptr->config != k_cell_type_stub )
+                                               {
+                                                       struct cell_description *desc = &cell_descriptions[ cell_ptr->config ];
+                                                       
+                                                       v2i_copy( desc->start, fish->dir );
+                                                       fish->flow_reversed = 1;
+                                                       
+                                                       world.num_fishes ++;
                                                        alive_count ++;
+                                               }
                                        }
                                }
                        }
@@ -1572,6 +1732,10 @@ void vg_update(void)
                                                world.sim_frame = 0;
                                                world.sim_start = vg_time;
                                                world.num_fishes = 0;
+                                               
+                                               for( int i = 0; i < world.w*world.h; i ++ )
+                                                       world.data[ i ].state &= ~FLAG_FLIP_FLOP;
+                                               
                                                continue;
                                        }
                                        else
@@ -1589,6 +1753,9 @@ void vg_update(void)
                                }
                                else
                                {
+                                       if( world.sim_run > 0 )
+                                               sw_set_achievement( "GOOD_ENOUGH" );
+                                       
                                        vg_error( "Level failed :(\n" );
                                }
                                
@@ -1608,7 +1775,7 @@ void vg_update(void)
                }
                
                float scaled_time = 0.0f;
-               scaled_time = (vg_time-world.sim_start)*2.0f;
+               scaled_time = (vg_time-world.sim_start)*world.sim_speed;
                world.frame_lerp = scaled_time - (float)world.sim_frame;
                
                // Update positions
@@ -1616,78 +1783,74 @@ void vg_update(void)
                {
                        struct fish *fish = &world.fishes[i];
                        
-                       if( fish->alive == 0 )
+                       if( fish->state == k_fish_state_dead )
                                continue;
                        
-                       if( fish->alive == -1 && (world.frame_lerp > fish->death_time) )
+                       if( fish->state == k_fish_state_soon_dead && (world.frame_lerp > fish->death_time) )
                                continue; // Todo: particle thing?
+                               
+                       struct cell *cell = pcell(fish->pos);
+                       struct cell_description *desc = &cell_descriptions[ cell->config ];
+                       
+                       v2f const *curve;
                        
-                       if( fish->alive == -2 )
+                       float t = world.frame_lerp;
+                       if( fish->flow_reversed && !desc->is_linear )
+                               t = 1.0f-t;
+
+                       v2_copy( fish->physics_co, fish->physics_v );
+                       
+                       switch( cell->config )
                        {
-                               v2_muladds( fish->physics_v, (v2f){ 0.0, -9.8f }, vg_time_delta, fish->physics_v );
-                               v2_muladds( fish->physics_co, fish->physics_v, vg_time_delta, fish->physics_co );
+                               case k_cell_type_merge:
+                                       if( fish->dir[0] == 1 )
+                                               curve = curve_12;
+                                       else
+                                               curve = curve_9;
+                               break;
+                               case k_cell_type_con_r: curve = curve_1; break;
+                               case k_cell_type_con_l: curve = curve_4; break;
+                               case k_cell_type_con_u: curve = curve_2; break;
+                               case k_cell_type_con_d: curve = curve_8; break;
+                               case 3: curve = curve_3; break;
+                               case 6: curve = curve_6; break;
+                               case 9: curve = curve_9; break;
+                               case 12: curve = curve_12; break;
+                               case 7:
+                                       if( t > curve_7_linear_section )
+                                       {
+                                               t -= curve_7_linear_section;
+                                               t *= (1.0f/(1.0f-curve_7_linear_section));
+                                               
+                                               curve = cell->state & FLAG_FLIP_FLOP? curve_7: curve_7_1;
+                                       }
+                                       else curve = NULL;
+                               break;
+                               default: curve = NULL; break;
                        }
-                       else
-                       {               
-                               struct cell *cell = pcell(fish->pos);
-                               v2f const *curve;
-                               
-                               float t = world.frame_lerp;
-                               
-                               v2_copy( fish->physics_co, fish->physics_v );
-                               
-                               switch( cell->config )
-                               {
-                                       case 13:
-                                               if( fish->dir[0] == 1 )
-                                                       curve = curve_12;
-                                               else
-                                                       curve = curve_9;
-                                       break;
-                                       case 1: curve = curve_1; break;
-                                       case 4: curve = curve_4; break;
-                                       case 2: curve = curve_2; break;
-                                       case 8: curve = curve_8; break;
-                                       case 3: curve = curve_3; break;
-                                       case 6: curve = curve_6; break;
-                                       case 9: curve = curve_9; break;
-                                       case 12: curve = curve_12; break;
-                                       case 7:
-                                               if( t > curve_7_linear_section )
-                                               {
-                                                       t -= curve_7_linear_section;
-                                                       t *= (1.0f/(1.0f-curve_7_linear_section));
-                                                       
-                                                       curve = cell->state & FLAG_FLIP_FLOP? curve_7: curve_7_1;
-                                               }
-                                               else curve = NULL;
-                                       break;
-                                       default: curve = NULL; break;
-                               }
 
-                               if( curve )
-                               {
-                                       float t2 = t * t;
-                                       float t3 = t * t * t;
+                       if( curve )
+                       {
+                               float t2 = t * t;
+                               float t3 = t * t * t;
 
-                                       float cA = 3.0f*t2 - 3.0f*t3;
-                                       float cB = 3.0f*t3 - 6.0f*t2 + 3.0f*t;
-                                       float cC = 3.0f*t2 - t3 - 3.0f*t + 1.0f;
+                               float cA = 3.0f*t2 - 3.0f*t3;
+                               float cB = 3.0f*t3 - 6.0f*t2 + 3.0f*t;
+                               float cC = 3.0f*t2 - t3 - 3.0f*t + 1.0f;
 
-                                       fish->physics_co[0] = t3*curve[3][0] + cA*curve[2][0] + cB*curve[1][0] + cC*curve[0][0];
-                                       fish->physics_co[1] = t3*curve[3][1] + cA*curve[2][1] + cB*curve[1][1] + cC*curve[0][1];
-                                       fish->physics_co[0] += (float)fish->pos[0];
-                                       fish->physics_co[1] += (float)fish->pos[1];
-                               }
-                               else
-                               {
-                                       v2f origin;
-                                       origin[0] = (float)fish->pos[0] + (float)fish->dir[0]*-0.5f + 0.5f;
-                                       origin[1] = (float)fish->pos[1] + (float)fish->dir[1]*-0.5f + 0.5f;
-                                       
-                                       fish->physics_co[0] = origin[0] + (float)fish->dir[0]*t;
-                                       fish->physics_co[1] = origin[1] + (float)fish->dir[1]*t;
-                               }
+                               fish->physics_co[0] = t3*curve[3][0] + cA*curve[2][0] + cB*curve[1][0] + cC*curve[0][0];
+                               fish->physics_co[1] = t3*curve[3][1] + cA*curve[2][1] + cB*curve[1][1] + cC*curve[0][1];
+                               fish->physics_co[0] += (float)fish->pos[0];
+                               fish->physics_co[1] += (float)fish->pos[1];
+                       }
+                       else
+                       {
+                               v2f origin;
+                               origin[0] = (float)fish->pos[0] + (float)fish->dir[0]*-0.5f + 0.5f;
+                               origin[1] = (float)fish->pos[1] + (float)fish->dir[1]*-0.5f + 0.5f;
+                               
+                               fish->physics_co[0] = origin[0] + (float)fish->dir[0]*t;
+                               fish->physics_co[1] = origin[1] + (float)fish->dir[1]*t;
                        }
                }
        }
@@ -1806,6 +1969,27 @@ void vg_render(void)
        }
        
        
+       // Level title
+       ui_begin( &ui_global_ctx, 512, 256 );
+       
+       ui_global_ctx.override_colour = 0xff9a8a89;
+       //ui_text( &ui_global_ctx, world.map_title, 6, 0 );
+       ui_global_ctx.override_colour = 0xffffffff;
+       
+       ui_resolve( &ui_global_ctx );
+       
+       m3x3f world_text;
+       m3x3_copy( vg_pv, world_text );
+       m3x3_translate( world_text, (v3f){ 1.55f, 1.9f, 0.0f } );
+       m3x3_rotate( world_text, VG_PIf*0.5f );
+       m3x3_scale( world_text, (v3f){0.01f,-0.01f,0.01f} );
+       
+       ui_draw( &ui_global_ctx, world_text );
+       
+       // Main
+       // =========================================================================================
+       
+       use_mesh( &world.tile );
        SHADER_USE( shader_tile_main );
 
        m2x2f subtransform;
@@ -1833,7 +2017,7 @@ void vg_render(void)
        SHADER_USE( shader_ball );
        glUniformMatrix3fv( SHADER_UNIFORM( shader_ball, "uPv" ), 1, GL_FALSE, (float *)vg_pv );
        
-       vg_tex2d_bind( &tex_ball, 0 );
+       vg_tex2d_bind( &tex_ball_noise, 0 );
        glUniform1i( SHADER_UNIFORM( shader_ball, "uTexMain" ), 0 );
        
        // Draw 'fish'
@@ -1843,10 +2027,10 @@ void vg_render(void)
                {
                        struct fish *fish = &world.fishes[i];
                        
-                       if( fish->alive == 0 )
+                       if( fish->state == k_fish_state_dead || fish->state == k_fish_state_bg )
                                continue;
                        
-                       if( fish->alive == -1 && (world.frame_lerp > fish->death_time) )
+                       if( fish->state == k_fish_state_soon_dead && (world.frame_lerp > fish->death_time) )
                                continue;
                        
                        v4f dot_colour = { 0.0f, 0.0f, 0.0f, 1.0f };
@@ -1854,7 +2038,8 @@ void vg_render(void)
                        
                        glUniform3fv( SHADER_UNIFORM( shader_ball, "uColour" ), 1, dot_colour );
                        glUniform2fv( SHADER_UNIFORM( shader_ball, "uOffset" ), 1, fish->physics_co );
-                       draw_mesh( 0, 32 );
+                       glUniform2f( SHADER_UNIFORM( shader_ball, "uTexOffset" ), (float)i * 1.2334, (float)i * -0.3579f );
+                       draw_mesh( 0, 2 );
                }
        }
        
@@ -2020,7 +2205,7 @@ void vg_render(void)
                                        glUniform4fv( SHADER_UNIFORM( shader_tile_colour, "uColour" ), 1, dot_colour );
                                
                                        // Draw filled if tick not passed, draw empty if empty
-                                       if( world.sim_frame > j && world.sim_run >= k )
+                                       if( (world.sim_frame > j && world.sim_run >= k) || world.sim_run > k )
                                                draw_mesh( empty_start, empty_count );
                                        else
                                                draw_mesh( filled_start, filled_count );
@@ -2059,104 +2244,308 @@ void vg_render(void)
                
        use_mesh( &world.numbers );
        draw_numbers( (v3f){ 2.0f, (float)world.h-1.875f, 0.3333f }, world.score );
-               
-       // Level selection UI
-       use_mesh( &world.circle );
-       float ratio = ((float)vg_window_x/(float)vg_window_y);
-       
-       m3x3f ui_view = M3X3_IDENTITY;
-       m3x3_scale( ui_view, (v3f){ 1.0f, ratio, 1.0f } );
-       glUniformMatrix3fv( SHADER_UNIFORM( shader_tile_colour, "uPv" ), 1, GL_FALSE, (float *)ui_view );
+}
 
-       // Calculate mouse in UIsp
-       v3f mouse_ui_space = { ((float)vg_mouse[0] / (float)(vg_window_x)) * 2.0f - 1.0f,
-                                                                 (((float)vg_mouse[1] / (float)(vg_window_y)) * 2.0f - 1.0f)*(-1.0f/ratio), 0.0125f };
+static ui_colourset flcol_list_a = {
+       .main = 0xff877979,
+       .hover = 0xffa09393,
+       .active = 0xffbfb1b0
+};
+static ui_colourset flcol_list_b = {
+       .main = 0xff7c6e6e,
+       .hover = 0xffa09393,
+       .active = 0xffbfb1b0
+};
+
+static ui_colourset flcol_list_complete_a = {
+       .main = 0xff62a064,
+       .hover = 0xff8dc18f,
+       .active = 0xffb2ddb3
+};
 
-       // Get selected level
-       const float selection_scale = 0.05f;
-       int const level_count = vg_list_size( level_pack_1 );
-       int level_select = -1;
+static ui_colourset flcol_list_complete_b = {
+       .main = 0xff79b37b,
+       .hover = 0xff8dc18f,
+       .active = 0xffb2ddb3
+};
+
+static ui_colourset flcol_list_locked = {
+       .main = 0xff655959,
+       .hover = 0xff655959,
+       .active = 0xff655959
+};
+
+static struct
+{
+       SteamLeaderboard_t steam_leaderboard;
+       int leaderboard_matches;
        
-       if( mouse_ui_space[0] <= -0.8f )
+       struct cmp_level *level_selected;
+} 
+ui_data;
+
+void vg_ui(void)
+{
+       // UI memory
+       static int pack_selection = 0;
+       static struct pack_info
+       {
+               struct cmp_level *levels;
+               u32 level_count;
+               const char *name;
+       }
+       pack_infos[] = 
        {
-               float levels_range = (float)level_count*selection_scale*0.6f;
-               float level_offset = ((-mouse_ui_space[1] + levels_range) / levels_range) * 0.5f * (float)level_count;
-               level_select = ceilf( level_offset );
+               {
+                       .levels = cmp_levels_tutorials,
+                       .level_count = vg_list_size(cmp_levels_tutorials),
+                       .name = "Training"
+               },
+               {
+                       .levels = cmp_levels_basic,
+                       .level_count = vg_list_size(cmp_levels_basic),
+                       .name = "Main"
+               },
+               {
+                       .levels = cmp_levels_grad,
+                       .level_count = vg_list_size(cmp_levels_tutorials),
+                       .name = "Expert"
+               }
+       };
+       
+       // UI Code
+       ui_global_ctx.cursor[0] = 0;
+       ui_global_ctx.cursor[1] = 0;
+       ui_global_ctx.cursor[2] = 256;
 
-               // Draw selector
-               if( level_select >= 0 && level_select < vg_list_size( level_pack_1 ) )
+       gui_fill_y();
+       
+       ui_global_ctx.id_base = 4 << 16;
+       
+       gui_new_node();
+       {
+               gui_capture_mouse( 9999 );
+               gui_fill_rect( ui_global_ctx.cursor, 0xff5577ff );
+               
+               gui_text( "ASSIGNMENTS", 8, 0 );
+               
+               ui_global_ctx.cursor[1] += 30;
+               ui_global_ctx.cursor[3] = 25;
+               
+               gui_new_node();
                {
-                       glUniform4f( SHADER_UNIFORM( shader_tile_colour, "uColour" ), 0.369768f, 0.3654f, 0.42f, 1.0f );
+                       ui_rect_pad( ui_global_ctx.cursor, 2 );
+                       ui_global_ctx.cursor[2] = 84;
                        
-                       use_mesh( &world.tile );
-                       glUniform3f( SHADER_UNIFORM( shader_tile_colour, "uOffset" ), 
-                               -1.0f, 
-                               ((float)level_count - (float)level_select * 2.0f ) * selection_scale * 0.6f,
-                               selection_scale
-                       );
-                       draw_mesh( 2, 2 );
-                       
-                       use_mesh( &world.circle );
-                       
-                       if( vg_get_button_down( "primary" ) )
+                       for( int i = 0; i < 3; i ++ )
                        {
-                               console_changelevel( 1, level_pack_1 + level_select );
-                       }
-               }
-       }
-       else mouse_ui_space[1] = INFINITY;
-
-       glUniform4f( SHADER_UNIFORM( shader_tile_colour, "uColour" ), 0.4f, 0.39f, 0.45f, 1.0f );
+                               if( i == pack_selection )
+                                       gui_override_colours( &flcol_list_locked );
 
-       // Draw levels
-       for( int i = 0; i < level_count; i ++ )
-       {
-               struct career_level *clevel = &career.levels[i];
-       
-               v3f level_ui_space = { 
-                       -0.97f, 
-                       ((float)level_count - (float)i * 2.0f ) * selection_scale * 0.6f + selection_scale * 0.5f,
-                       selection_scale * 0.5f
-               };
+                               if( gui_button( 2000 + i ) == k_button_click )
+                                       pack_selection = i;
+                               
+                               ui_global_ctx.cursor[1] += 2;
+                               gui_text( pack_infos[i].name, 4, 0 );
+                               gui_end_right();
+                               
+                               gui_reset_colours();
+                       } 
+               }
+               gui_end_down();
                
-               float scale = vg_clampf( 1.0f - fabsf(level_ui_space[1] - mouse_ui_space[1]) * 2.0f, 0.9f, 1.0f );
-               level_ui_space[2] *= scale;
+               ui_global_ctx.cursor[3] = 500;
                
-               glUniform3fv( SHADER_UNIFORM( shader_tile_colour, "uOffset" ), 1, level_ui_space );
+               // DRAW LEVEL SELECTION LIST
+               {
+                       struct cmp_level *levels = pack_infos[ pack_selection ].levels;
+                       int count = pack_infos[ pack_selection ].level_count;
+                       int unlocked = 3000;
                
-               if( clevel->completed )
-                       draw_mesh( filled_start, filled_count );
-               else
-                       draw_mesh( empty_start, empty_count );
+                       static struct ui_scrollbar sb = {
+                               .bar_height = 400
+                       };
+                       
+                       ui_px view_height = ui_global_ctx.cursor[3];
+                       ui_px level_height = 50;
+
+                       // Level scroll view
+                       gui_new_node();
+                       {
+                               gui_fill_rect( ui_global_ctx.cursor, 0xff5a4e4d );
+                               gui_set_clip( ui_global_ctx.cursor );
+                               
+                               ui_global_ctx.cursor[2] = 14;
+                               gui_align_right();
+                               
+                               ui_px content_height = count*level_height;
+                               if( content_height > view_height )
+                               {
+                                       ui_scrollbar( &ui_global_ctx, &sb, 1 );
+                                       ui_global_ctx.cursor[1] -= ui_calculate_content_scroll( &sb, content_height );
+                               }
+                               else
+                               {
+                                       gui_fill_rect( ui_global_ctx.cursor, 0xff807373 );
+                               }
+                               
+                               ui_global_ctx.cursor[2] = 240;
+                               ui_global_ctx.cursor[3] = level_height;
+                               gui_align_left();
+                               
+                               for( int i = 0; i < count; i ++ )
+                               {
+                                       struct cmp_level *lvl_info = &levels[i];
+                               
+                                       if( i < unlocked )
+                                       {
+                                               if( lvl_info->completed_score != 0 )
+                                                       gui_override_colours( i&0x1? &flcol_list_complete_a: &flcol_list_complete_b );
+                                               else
+                                                       gui_override_colours( i&0x1? &flcol_list_a: &flcol_list_b );
+                                       }
+                                       else
+                                               gui_override_colours( &flcol_list_locked );
+                                       
+                                       if( i < unlocked )
+                                       {
+                                               if( gui_button( 2 + i ) == k_button_click )
+                                               {
+                                                       ui_data.level_selected = &levels[i];
+                                                       ui_data.leaderboard_matches = 0;
+                                                       sw_find_leaderboard( ui_data.level_selected->map_name );
+                                               }
+                                               
+                                               ui_global_ctx.override_colour = 0xffffffff;
+                                               gui_text( lvl_info->title, 6, 0 );
+                                               ui_global_ctx.cursor[1] += 18;
+                                               gui_text( "incomplete", 4, 0 );
+                                       }
+                                       else
+                                       {
+                                               gui_button( 2 + i );
+                                               
+                                               ui_global_ctx.override_colour = 0xff786f6f;
+                                               gui_text( "???", 6, 0 );
+                                               ui_global_ctx.cursor[1] += 18;
+                                               gui_text( "locked", 4, 0 );
+                                       }
+                                       
+                                       gui_end_down();
+                               }
+                               
+                               gui_reset_colours();            
+                               gui_release_clip();
+                       }
+                       gui_end_down();
+               }
        }
+       gui_end_right();
        
-       // Level scores
-       glUniform4f( SHADER_UNIFORM( shader_tile_colour, "uColour" ), 0.4f*1.25f, 0.39f*1.25f, 0.45f*1.25f, 1.0f );
+       // Selected level UI
+       // ============================================================
        
-       use_mesh( &world.numbers );
-       for( int i = 0; i < level_count; i ++ )
+       if( ui_data.level_selected )
        {
-               struct career_level *clevel = &career.levels[i];
-       
-               v3f level_ui_space = { 
-                       -0.94f, 
-                       ((float)level_count - (float)i * 2.0f ) * selection_scale * 0.6f + selection_scale * 0.5f,
-                       0.02f
-               };
+               ui_global_ctx.cursor[0] += 16;
+               ui_global_ctx.cursor[1] += 16;
+               ui_global_ctx.cursor[2] = 512-40;
+               ui_global_ctx.cursor[3] = 560-16;
                
-               if( clevel->completed )
-                       draw_numbers( level_ui_space, clevel->score );
+               gui_new_node();
+               {
+                       gui_capture_mouse( 9999 );
+                       
+                       gui_fill_rect( ui_global_ctx.cursor, 0xff5a4e4d );
+                       ui_global_ctx.cursor[1] += 4;
+                       gui_text( ui_data.level_selected->title, 6, 0 );
+                       
+                       ui_global_ctx.cursor[1] += 30;
+                       ui_rect_pad( ui_global_ctx.cursor, 8 );
+                       ui_global_ctx.cursor[3] = 300;
+                       
+                       gui_new_node();
+                       {
+                               gui_fill_rect( ui_global_ctx.cursor, 0xff655959 );
+                       }
+                       gui_end_down();
+                       
+                       ui_text_use_paragraph( &ui_global_ctx );
+                       ui_global_ctx.cursor[1] += 2;
+                       
+                       gui_text( ui_data.level_selected->description, 5, 0 );
+                       ui_text_use_title( &ui_global_ctx );
                        
-               level_ui_space[0] = -0.975f;
-               level_ui_space[1] -= 0.01f;
-               draw_numbers( level_ui_space, i );
+                       // Buttons at the bottom
+                       ui_global_ctx.cursor[3] = 30;
+                       ui_global_ctx.cursor[2] = 80;
+                       
+                       gui_align_bottom();
+                       ui_global_ctx.cursor[1] -= 8;
+                       
+                       if( gui_button( 3000 ) == k_button_click )
+                       {
+                               ui_data.level_selected = NULL;
+                       }
+                       gui_text( "Back", 6, 0 );
+                       gui_end();
+                       
+                       gui_align_right();
+                       ui_global_ctx.cursor[2] = 170;
+                       ui_global_ctx.cursor[0] -= 8 + 170 + 2;
+                       
+                       {
+                               gui_override_colours( &flcol_list_locked );
+                               if( gui_button( 3001 ) == k_button_click )
+                                       vg_error( "UNIMPLEMENTED\n" );
+                                       
+                               gui_text( "Restore Solution", 6, 0 );
+                               gui_end_right();
+                       }
+                       
+                       ui_global_ctx.cursor[0] += 2;
+                       ui_global_ctx.cursor[2] = 80;
+                       
+                       {
+                               gui_override_colours( &flcol_list_complete_a );
+                               if( gui_button( 3002 ) == k_button_click )
+                               {
+                                       console_changelevel( 1, &ui_data.level_selected->map_name );
+                                       ui_data.level_selected = NULL;
+                                       ui_data.leaderboard_matches = 0;
+                               }
+                               gui_text( "Play", 6, 0 );
+                               gui_end();
+                       }
+                       
+                       gui_reset_colours();
+               }
+               gui_end_right();
+               
+               if( ui_data.leaderboard_matches )
+               {
+                       ui_global_ctx.cursor[0] += 16;
+                       ui_global_ctx.cursor[3] = 250;
+                       
+                       // If has results
+                       gui_new_node();
+                       {
+                               gui_fill_rect( ui_global_ctx.cursor, 0xff5a4e4d );
+                       }
+                       gui_end();
+               }
        }
-       
-       //use_mesh( &world.numbers );
-       //draw_numbers( (v3f){ 0.0f, -0.5f, 0.1f }, 128765 );
 }
 
-void vg_ui(void)
+void leaderboard_found( LeaderboardFindResult_t *pCallback )
 {
-       ui_test();
+       if( !pCallback->m_bLeaderboardFound )
+               vg_error( "Leaderboard could not be found\n" );
+       
+       ui_data.steam_leaderboard = pCallback->m_hSteamLeaderboard;
+       ui_data.leaderboard_matches = 0;
+       
+       if( ui_data.level_selected )
+               if( !strcmp( sw_get_leaderboard_name( ui_data.steam_leaderboard ), ui_data.level_selected->map_name ) )
+                       ui_data.leaderboard_matches = 1;
 }