+// WORLD/MAP
+// ===========================================================================================================
+
+static void map_free(void)
+{
+ arrfree( world.data );
+ arrfree( world.io );
+
+ world.w = 0;
+ world.h = 0;
+ world.data = NULL;
+ world.io = NULL;
+ world.score = 0;
+ world.time = 0;
+ world.completed = 0;
+ world.max_runs = 0;
+ world.initialzed = 0;
+}
+
+static void io_reset(void)
+{
+ for( int i = 0; i < arrlen( world.io ); i ++ )
+ {
+ struct cell_terminal *term = &world.io[i];
+
+ for( int j = 0; j < term->run_count; j ++ )
+ term->runs[j].recv_count = 0;
+ }
+}
+
+static struct cell *pcell( v2i pos )
+{
+ return &world.data[ pos[1]*world.w + pos[0] ];
+}
+
+static void map_reclassify( v2i start, v2i end, int update_texbuffer )
+{
+ v2i full_start = { 1,1 };
+ v2i full_end = { world.w-1, world.h-1 };
+
+ if( !start || !end )
+ {
+ start = full_start;
+ end = full_end;
+ }
+
+ // Texture data
+ u8 info_buffer[64*64*4];
+ u32 pixel_id = 0;
+
+ int px0 = vg_max( start[0], full_start[0] ),
+ px1 = vg_min( end[0], full_end[0] ),
+ py0 = vg_max( start[1], full_start[1] ),
+ py1 = vg_min( end[1], full_end[1] );
+
+ for( int y = py0; y < py1; y ++ )
+ {
+ for( int x = px0; x < px1; x ++ )
+ {
+ struct cell *cell = pcell((v2i){x,y});
+
+ v2i dirs[] = {{1,0},{0,1},{-1,0},{0,-1}};
+
+ u8 height = 0;
+ u8 config = 0x00;
+
+ if( cell->state & (FLAG_CANAL|FLAG_INPUT|FLAG_OUTPUT) )
+ {
+ for( int i = 0; i < vg_list_size( dirs ); i ++ )
+ {
+ struct cell *neighbour = pcell((v2i){x+dirs[i][0], y+dirs[i][1]});
+ if( neighbour->state & (FLAG_CANAL|FLAG_INPUT|FLAG_OUTPUT) )
+ config |= 0x1 << i;
+ }
+
+ height = 128;
+ }
+ else
+ {
+ if( cell->state & FLAG_WALL )
+ height = 0xFF-0x3F + hash21i( (v2i){x,y}, 0x3F );
+
+ config = 0xF;
+ }
+
+ pcell((v2i){x,y})->config = config;
+
+ u8 *info_px = &info_buffer[ (pixel_id ++)*4 ];
+ info_px[0] = height;
+ info_px[1] = cell->state & FLAG_WALL? 0: 255;
+ info_px[2] = 0;
+ info_px[3] = 0;
+
+ if(
+ (
+ ((cell->state & FLAG_IS_TRIGGER) && (cell->config == 0xF || cell->config == k_cell_type_split)) ||
+ ((cell->state & FLAG_TARGETED) && (cell->config != k_cell_type_split))
+ ) && update_texbuffer
+ ){
+ cell->state &= ~(FLAG_TARGETED|FLAG_IS_TRIGGER);
+ for( u32 i = 0; i < 2; i ++ )
+ {
+ if( cell->links[i] )
+ {
+ struct cell *other_ptr = &world.data[ cell->links[i] ];
+ other_ptr->links[ i ] = 0;
+ other_ptr->state &= ~FLAG_IS_TRIGGER;
+
+ if( other_ptr->links[ i ^ 0x1 ] == 0 )
+ other_ptr->state &= ~FLAG_TARGETED;
+ }
+ }
+
+ cell->links[0] = 0;
+ cell->links[1] = 0;
+ }
+ }
+ }
+
+ if( update_texbuffer )
+ {
+ glBindTexture( GL_TEXTURE_2D, world.background_data );
+ glTexSubImage2D( GL_TEXTURE_2D, 0, px0 + 16, py0 + 16, px1-px0, py1-py0, GL_RGBA, GL_UNSIGNED_BYTE, info_buffer );
+ }
+}
+
+static int map_load( const char *str, const char *name )
+{
+ //TODO: It may be worthwhile, at this point, to switch to binary encoding for save data
+
+ map_free();
+
+ char const *c = str;
+
+ // Scan for width
+ for(;; world.w ++)
+ {
+ if( c[world.w] == ';' )
+ break;
+ else if( !c[world.w] )
+ {
+ vg_error( "Unexpected EOF when parsing level\n" );
+ return 0;
+ }
+ }
+
+ struct cell *row = arraddnptr( world.data, world.w );
+ int cx = 0;
+ int reg_start = 0, reg_end = 0;
+
+ u32 *links_to_make = NULL;
+ int links_satisfied = 0;
+
+ char link_id_buffer[32];
+ int link_id_n = 0;
+
+ for(;;)
+ {
+ if( !*c )
+ break;
+
+ if( *c == '\r' ) { c ++; continue; } // fuck off windows
+
+ if( *c == ';' )
+ {
+ c ++;
+
+ if( *c == '\r' ) c ++;
+
+ // Parse attribs
+ if( *c != '\n' )
+ {
+ while( *c )
+ {
+ if( *c == '\r' ) { c ++; continue; }
+
+ if( reg_start < reg_end )
+ {
+ struct cell_terminal *terminal = &world.io[ reg_start ];
+ struct terminal_run *run = &terminal->runs[ terminal->run_count-1 ];
+
+ if( *c >= 'a' && *c <= 'z' )
+ {
+ run->conditions[ run->condition_count ++ ] = *c;
+ }
+ else
+ {
+ if( *c == ',' || *c == '\n' )
+ {
+ reg_start ++;
+
+ if( *c == '\n' )
+ break;
+ }
+ else if( *c == ':' )
+ {
+ terminal->runs[ terminal->run_count ].condition_count = 0;
+ terminal->run_count ++;
+ world.max_runs = vg_max( world.max_runs, terminal->run_count );
+ }
+ else
+ {
+ vg_error( "Unkown attribute '%c' (row: %u)\n", *c, world.h );
+ goto IL_REG_ERROR;
+ }
+ }
+ }
+ else
+ {
+ if( links_satisfied < arrlen( links_to_make ) )
+ {
+ struct cell *target = &world.data[ links_to_make[ links_satisfied ] ];
+
+ if( (((u32)*c >= (u32)'0') && ((u32)*c <= (u32)'9')) || *c == '-' )
+ {
+ if( link_id_n >= vg_list_size( link_id_buffer )-1 )
+ {
+ vg_error( "Number was way too long to be parsed (row: %u)\n", world.h );
+ goto IL_REG_ERROR;
+ }
+
+ link_id_buffer[ link_id_n ++ ] = *c;
+ }
+ else if( *c == ',' || *c == '\n' )
+ {
+ link_id_buffer[ link_id_n ] = 0x00;
+ int value = atoi( link_id_buffer );
+
+ target->links[value >= 0? 1:0] = abs(value);
+ links_satisfied ++;
+ link_id_n = 0;
+
+ if( *c == '\n' )
+ break;
+ }
+ else
+ {
+ vg_error( "Invalid character '%c' (row: %u)\n", *c, world.h );
+ goto IL_REG_ERROR;
+ }
+ }
+ else
+ {
+ vg_error( "Too many values to assign (row: %u)\n", world.h );
+ goto IL_REG_ERROR;
+ }
+ }
+
+ c ++;
+ }
+ }
+
+ // Registry length-error checks
+ if( reg_start != reg_end )
+ {
+ vg_error( "Not enough spawn values assigned (row: %u, %u of %u)\n", world.h, reg_start, reg_end );
+ goto IL_REG_ERROR;
+ }
+
+ if( links_satisfied != arrlen( links_to_make ) )
+ {
+ vg_error( "Not enough link values assigned (row: %u, %u of %u)\n", world.h, links_satisfied, arrlen( links_to_make ) );
+ goto IL_REG_ERROR;
+ }
+
+ if( cx != world.w )
+ {
+ vg_error( "Not enough cells to match previous row definition (row: %u, %u<%u)\n", world.h, cx, world.w );
+ goto IL_REG_ERROR;
+ }
+
+ row = arraddnptr( world.data, world.w );
+ cx = 0;
+ world.h ++;
+ reg_end = reg_start = arrlen( world.io );
+
+ arrsetlen( links_to_make, 0 );
+ links_satisfied = 0;
+ }
+ else
+ {
+ if( cx == world.w )
+ {
+ vg_error( "Too many cells to match previous row definition (row: %u, %u>%u)\n", world.h, cx, world.w );
+ goto IL_REG_ERROR;
+ }
+
+ // Tile initialization
+ // row[ cx ] .. etc
+ struct cell *cell = &row[ cx ];
+ cell->config = 0xF;
+
+ if( *c == '+' || *c == '-' )
+ {
+ struct cell_terminal *term = arraddnptr( world.io, 1 );
+ term->pos[0] = cx;
+ term->pos[1] = world.h;
+
+ term->run_count = 1;
+ term->runs[0].condition_count = 0;
+
+ cell->state = *c == '+'? FLAG_INPUT: FLAG_OUTPUT;
+ reg_end ++;
+ }
+ else if( *c == '#' ) cell->state = FLAG_WALL;
+ else if( ((u32)*c >= (u32)'A') && ((u32)*c <= (u32)'A'+0xf) )
+ {
+ // Canal flag bits (4bit/16 value):
+ // 0: Canal present
+ // 1: Is trigger
+ // 2: Reserved
+ // 3: Reserved
+
+ cell->state = ((u32)*c - (u32)'A') & (FLAG_CANAL|FLAG_IS_TRIGGER);
+
+ if( cell->state & FLAG_IS_TRIGGER )
+ arrpush( links_to_make, cx + world.h*world.w );
+
+ cell->links[0] = 0;
+ cell->links[1] = 0;
+ world.score ++;
+ }
+ else cell->state = 0x00;
+
+ cx ++;
+ }
+
+ c ++;
+ }
+
+ // Update data texture to fill out the background
+ {
+ u8 info_buffer[64*64*4];
+ for( int x = 0; x < 64; x ++ )
+ {
+ for( int y = 0; y < 64; y ++ )
+ {
+ u8 *px = &info_buffer[((x*64)+y)*4];
+
+ px[0] = 0xFF-0x3F + hash21i( (v2i){x,y}, 0x3F );
+ px[1] = 0;
+ px[2] = 0;
+ px[3] = 0;
+ }
+ }
+
+ // Random walks.. kinda
+ for( int i = 0; i < arrlen(world.io); i ++ )
+ {
+ struct cell_terminal *term = &world.io[ i ];
+
+ v2i turtle;
+ v2i turtle_dir;
+ int original_y;
+
+ turtle[0] = 16+term->pos[0];
+ turtle[1] = 16+term->pos[1];
+
+ turtle_dir[0] = 0;
+ turtle_dir[1] = pcell(term->pos)->state & FLAG_INPUT? 1: -1;
+ original_y = turtle_dir[1];
+
+ for( int i = 0; i < 100; i ++ )
+ {
+ info_buffer[((turtle[1]*64)+turtle[0])*4] = 0;
+
+ v2i_add( turtle_dir, turtle, turtle );
+
+ if( turtle[0] == 0 ) break;
+ if( turtle[0] == 63 ) break;
+ if( turtle[1] == 0 ) break;
+ if( turtle[1] == 63 ) break;
+
+ int random_value = hash21i( turtle, 0xFF );
+ if( random_value > 255-40 && !turtle_dir[0] )
+ {
+ turtle_dir[0] = -1;
+ turtle_dir[1] = 0;
+ }
+ else if( random_value > 255-80 && !turtle_dir[0] )
+ {
+ turtle_dir[0] = 1;
+ turtle_dir[1] = 0;
+ }
+ else if( random_value > 255-100 )
+ {
+ turtle_dir[0] = 0;
+ turtle_dir[1] = original_y;
+ }
+ }
+ }
+
+ glBindTexture( GL_TEXTURE_2D, world.background_data );
+ glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, 64, 64, GL_RGBA, GL_UNSIGNED_BYTE, info_buffer );
+ }
+
+ arrfree( links_to_make );
+
+ map_reclassify( NULL, NULL, 1 );
+
+ // Validate links
+ for( int i = 0; i < world.h*world.w; i ++ )
+ {
+ struct cell *src = &world.data[i];
+ if( src->state & FLAG_IS_TRIGGER )
+ {
+ int link_id = src->links[0]?0:1;
+ if( src->links[link_id] <= world.h*world.w )
+ {
+ struct cell *target = &world.data[ src->links[link_id] ];
+ if( (target->state & FLAG_CANAL) && (target->config == k_cell_type_split) )
+ {
+ if( target->links[ link_id ] )
+ {
+ vg_error( "Link target was already targeted\n" );
+ goto IL_REG_ERROR;
+ }
+ else
+ {
+ // Valid link
+ target->links[ link_id ] = i;
+ target->state |= FLAG_TARGETED;
+ }
+ }
+ else
+ {
+ vg_error( "Link target was invalid\n" );
+ goto IL_REG_ERROR;
+ }
+ }
+ else
+ {
+ vg_error( "Link target out of bounds\n" );
+ goto IL_REG_ERROR;
+ }
+ }
+ }
+
+ vg_success( "Map '%s' loaded! (%u:%u)\n", name, world.w, world.h );
+
+ io_reset();
+
+ strncpy( world.map_name, name, vg_list_size( world.map_name )-1 );
+ world.initialzed = 1;
+ return 1;
+
+IL_REG_ERROR:
+ arrfree( links_to_make );
+ map_free();
+ return 0;
+}
+
+static void map_serialize( FILE *stream )
+{
+ for( int y = 0; y < world.h; y ++ )
+ {
+ for( int x = 0; x < world.w; x ++ )
+ {
+ struct cell *cell = pcell( (v2i){ x, y } );
+
+ if( cell->state & FLAG_WALL ) fputc( '#', stream );
+ else if( cell->state & FLAG_INPUT ) fputc( '+', stream );
+ else if( cell->state & FLAG_OUTPUT ) fputc( '-', stream );
+ else if( cell->state & (FLAG_CANAL|FLAG_IS_TRIGGER|FLAG_RESERVED0|FLAG_RESERVED1) )
+ {
+ fputc( (cell->state & (FLAG_CANAL|FLAG_IS_TRIGGER|FLAG_RESERVED0|FLAG_RESERVED1)) + (u32)'A', stream );
+ }
+ else fputc( ' ', stream );
+ }
+
+ fputc( ';', stream );
+
+ int terminal_write_count = 0;
+
+ for( int x = 0; x < world.w; x ++ )
+ {
+ for( int i = 0; i < arrlen( world.io ); i ++ )
+ {
+ struct cell_terminal *term = &world.io[i];
+ if( v2i_eq( term->pos, (v2i){x,y} ) )
+ {
+ if( terminal_write_count )
+ fputc( ',', stream );
+ terminal_write_count ++;
+
+ for( int j = 0; j < term->run_count; j ++ )
+ {
+ struct terminal_run *run = &term->runs[j];
+
+ for( int k = 0; k < run->condition_count; k ++ )
+ fputc( run->conditions[k], stream );
+
+ if( j < term->run_count-1 )
+ fputc( ':', stream );
+ }
+ }
+ }
+ }
+
+ for( int x = 0; x < world.w; x ++ )
+ {
+ struct cell *cell = pcell( (v2i){ x,y } );
+ if( cell->state & FLAG_IS_TRIGGER )
+ {
+ if( terminal_write_count )
+ fputc( ',', stream );
+ terminal_write_count ++;
+
+ fprintf( stream, "%d", cell->links[0]? -cell->links[0]: cell->links[1] );
+ }
+ }
+
+ fputc( '\n', stream );
+ }
+}
+
+// CAREER STATE
+// ===========================================================================================================
+
+#pragma pack(push,1)
+struct dcareer_state
+{
+ u32 version;
+ i32 in_map;
+
+ u32 reserved[14];
+
+ struct dlevel_state
+ {
+ i32 score;
+ i32 unlocked;
+ i32 reserved[2];
+ }
+ levels[ NUM_CAMPAIGN_LEVELS ];
+};
+#pragma pack(pop)
+
+static void career_serialize(void)
+{
+ struct dcareer_state encoded;
+ encoded.version = 2;
+ encoded.in_map = world.pCmpLevel? world.pCmpLevel->serial_id: -1;
+
+ memset( encoded.reserved, 0, sizeof( encoded.reserved ) );
+
+ for( int i = 0; i < vg_list_size( career_serializable ); i ++ )
+ {
+ struct serializable_set *set = &career_serializable[i];
+
+ for( int j = 0; j < set->count; j ++ )
+ {
+ struct cmp_level *lvl = &set->pack[j];
+ struct dlevel_state *dest = &encoded.levels[lvl->serial_id];
+
+ dest->score = lvl->completed_score;
+ dest->unlocked = lvl->unlocked;
+ dest->reserved[0] = 0;
+ dest->reserved[1] = 0;
+ }
+ }
+
+ vg_asset_write( "sav/game.sv2", &encoded, sizeof( struct dcareer_state ) );
+}
+
+static void career_unlock_level( struct cmp_level *lvl );
+static void career_unlock_level( struct cmp_level *lvl )
+{
+ lvl->unlocked = 1;
+
+ if( lvl->linked )
+ career_unlock_level( lvl->linked );
+}
+
+static void career_pass_level( struct cmp_level *lvl, int score, int upload )
+{
+ if( score > 0 )
+ {
+ if( score < lvl->completed_score || lvl->completed_score == 0 )
+ {
+ if( !lvl->is_tutorial && upload )
+ leaderboard_set_score( lvl, score );
+
+ lvl->completed_score = score;
+ }
+
+ if( lvl->unlock ) career_unlock_level( lvl->unlock );
+ }
+}
+
+static void career_reset_level( struct cmp_level *lvl )
+{
+ lvl->unlocked = 0;
+ lvl->completed_score = 0;
+}
+
+static void career_load(void)
+{
+ i64 sz;
+ struct dcareer_state encoded;
+
+ // Blank save state
+ memset( (void*)&encoded, 0, sizeof( struct dcareer_state ) );
+ encoded.in_map = -1;
+ encoded.levels[0].unlocked = 1;
+
+ // Load and copy data into encoded
+ void *cr = vg_asset_read_s( "sav/game.sv2", &sz );
+
+ if( cr )
+ {
+ if( sz > sizeof( struct dcareer_state ) )
+ vg_warn( "This save file is too big! Some levels will be lost\n" );
+
+ if( sz <= offsetof( struct dcareer_state, levels ) )
+ {
+ vg_warn( "This save file is too small to have a header. Creating a blank one\n" );
+ free( cr );
+ return;
+ }
+
+ memcpy( (void*)&encoded, cr, VG_MIN( sizeof( struct dcareer_state ), sz ) );
+ free( cr );
+ }
+ else
+ vg_info( "No save file... Using blank one\n" );
+
+ // Reset memory
+ for( int i = 0; i < vg_list_size( career_serializable ); i ++ )
+ {
+ struct serializable_set *set = &career_serializable[i];
+
+ for( int j = 0; j < set->count; j ++ )
+ career_reset_level( &set->pack[j] );
+ }
+
+ // Header information
+ // =================================
+
+ // Decode everything from dstate
+ for( int i = 0; i < vg_list_size( career_serializable ); i ++ )
+ {
+ struct serializable_set *set = &career_serializable[i];
+
+ for( int j = 0; j < set->count; j ++ )
+ {
+ struct cmp_level *lvl = &set->pack[j];
+ struct dlevel_state *src = &encoded.levels[lvl->serial_id];
+
+ if( src->unlocked ) career_unlock_level( lvl );
+ if( src->score ) lvl->completed_score = src->score;
+
+ // ...
+ if( lvl->serial_id == encoded.in_map )
+ {
+ if( console_changelevel( 1, &lvl->map_name ) )
+ world.pCmpLevel = lvl;
+ }
+ }
+ }
+}
+
+// MAIN GAMEPLAY
+// ===========================================================================================================
+static int is_simulation_running(void)
+{
+ return world.buttons[ k_world_button_sim ].pressed;
+}
+
+static void simulation_stop(void)
+{
+ world.buttons[ k_world_button_sim ].pressed = 0;
+
+ world.num_fishes = 0;
+ world.sim_frame = 0;
+ world.sim_run = 0;
+
+ io_reset();
+
+ sfx_system_fadeout( &audio_system_balls_rolling, 44100 );
+
+ vg_info( "Stopping simulation!\n" );
+}
+
+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( !world_check_pos_ok( co ) )
+ return 0;
+
+ // Flags check
+ if( world.data[ world.w*co[1] + co[0] ].state & (FLAG_WALL|FLAG_INPUT|FLAG_OUTPUT) )
+ return 0;
+
+ // List of 3x3 configurations that we do not allow
+ static u32 invalid_src[][9] =
+ {
+ { 0,1,0,
+ 1,1,1,
+ 0,1,0
+ },
+ { 0,0,0,
+ 0,1,1,
+ 0,1,1
+ },
+ { 0,0,0,
+ 1,1,0,
+ 1,1,0
+ },
+ { 0,1,1,
+ 0,1,1,
+ 0,0,0
+ },
+ { 1,1,0,
+ 1,1,0,
+ 0,0,0
+ },
+ { 0,1,0,
+ 0,1,1,
+ 0,1,0
+ },
+ { 0,1,0,
+ 1,1,0,
+ 0,1,0
+ }
+ };
+
+ // Statically compile invalid configurations into bitmasks
+ static u32 invalid[ vg_list_size(invalid_src) ];
+
+ for( int i = 0; i < vg_list_size(invalid_src); i ++ )
+ {
+ u32 comped = 0x00;
+
+ for( int j = 0; j < 3; j ++ )
+ for( int k = 0; k < 3; k ++ )
+ comped |= invalid_src[i][ j*3+k ] << ((j*5)+k);
+
+ invalid[i] = comped;
+ }
+
+ // Extract 5x5 grid surrounding tile
+ u32 blob = 0x1000;
+ for( int y = co[1]-2; y < co[1]+3; y ++ )
+ for( int x = co[0]-2; x < co[0]+3; x ++ )
+ {
+ struct cell *cell = pcell((v2i){x,y});
+
+ if( cell && (cell->state & (FLAG_CANAL|FLAG_INPUT|FLAG_OUTPUT)) )
+ blob |= 0x1 << ((y-(co[1]-2))*5 + x-(co[0]-2));
+ }
+
+ // Run filter over center 3x3 grid to check for invalid configurations
+ int kernel[] = { 0, 1, 2, 5, 6, 7, 10, 11, 12 };
+ for( int i = 0; i < vg_list_size(kernel); i ++ )
+ {
+ if( blob & (0x1 << (6+kernel[i])) )
+ {
+ u32 window = blob >> kernel[i];
+
+ for( int j = 0; j < vg_list_size(invalid); j ++ )
+ if((window & invalid[j]) == invalid[j])
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
+void vg_update(void)
+{
+ // Camera
+ // ========================================================================================================
+
+ float r1 = (float)vg_window_y / (float)vg_window_x,
+ r2 = (float)world.h / (float)world.w,
+ size;
+
+ size = ( r2 < r1? (float)world.w * 0.5f: ((float)world.h * 0.5f) / r1 ) + 2.5f;
+ m3x3_projection( m_projection, -size, size, -size*r1, size*r1 );
+
+ v3f origin;
+ origin[0] = floorf( -0.5f * world.w );
+ origin[1] = floorf( -0.5f * world.h );
+ origin[2] = 0.0f;
+
+ m3x3_identity( m_view );
+ m3x3_translate( m_view, origin );
+ m3x3_mul( m_projection, m_view, vg_pv );
+ vg_projection_update();
+
+ // Mouse input
+ // ========================================================================================================
+ v2_copy( vg_mouse_ws, world.tile_pos );
+
+ world.tile_x = floorf( world.tile_pos[0] );
+ world.tile_y = floorf( world.tile_pos[1] );
+
+ // Tilemap
+ // ========================================================================================================
+ if( !is_simulation_running() && !gui_want_mouse() )
+ {
+ v2_copy( vg_mouse_ws, world.drag_to_co );
+
+ if( cell_interactive( (v2i){ world.tile_x, world.tile_y } ))
+ {
+ world.selected = world.tile_y * world.w + world.tile_x;
+
+ static u32 modify_state = 0;
+
+ struct cell *cell_ptr = &world.data[world.selected];
+
+ if( vg_get_button_down("primary") )
+ modify_state = (cell_ptr->state & FLAG_CANAL) ^ FLAG_CANAL;
+
+ if( vg_get_button("primary") && ((cell_ptr->state & FLAG_CANAL) != modify_state) )
+ {
+ cell_ptr->state &= ~FLAG_CANAL;
+ cell_ptr->state |= modify_state;
+
+ if( cell_ptr->state & FLAG_CANAL )
+ {
+ cell_ptr->links[0] = 0;
+ cell_ptr->links[1] = 0;
+
+ sfx_set_playrnd( &audio_tile_mod, &audio_system_sfx, 3, 6 );
+ world.score ++;
+ }
+ else
+ {
+ sfx_set_playrnd( &audio_tile_mod, &audio_system_sfx, 0, 3 );
+ world.score --;
+ }
+
+ map_reclassify( (v2i){ world.tile_x -2, world.tile_y -2 },
+ (v2i){ world.tile_x +2, world.tile_y +2 }, 1 );
+ }
+
+ if( vg_get_button_down("secondary") && !(cell_ptr->config == k_cell_type_split) )
+ {
+ world.id_drag_from = world.selected;
+ world.drag_from_co[0] = world.tile_x + 0.5f;
+ world.drag_from_co[1] = world.tile_y + 0.5f;
+ }
+
+ if( world.id_drag_from && (cell_ptr->config == k_cell_type_split) )
+ {
+ float local_x = vg_mouse_ws[0] - (float)world.tile_x;
+ world.drag_to_co[0] = (float)world.tile_x + (local_x > 0.5f? 0.75f: 0.25f);
+ world.drag_to_co[1] = (float)world.tile_y + 0.25f;
+
+ if( vg_get_button_up("secondary") )
+ {
+ struct cell *drag_ptr = &world.data[world.id_drag_from];
+ u32 link_id = local_x > 0.5f? 1: 0;
+
+ // Cleanup existing connections
+ if( cell_ptr->links[ link_id ] )
+ {
+ vg_warn( "Destroying existing connection on link %u (%hu)\n", link_id, cell_ptr->links[ link_id ] );
+
+ struct cell *current_connection = &world.data[ cell_ptr->links[ link_id ]];
+ current_connection->state &= ~FLAG_IS_TRIGGER;
+ current_connection->links[ link_id ] = 0;
+ }
+
+ if( drag_ptr->links[ link_id ^ 0x1 ] )
+ {
+ vg_warn( "Destroying alternate link %u (%hu)\n", link_id ^ 0x1, drag_ptr->links[ link_id ^ 0x1 ] );
+
+ struct cell *current_connection = &world.data[ drag_ptr->links[ link_id ^ 0x1 ]];
+ if( !current_connection->links[ link_id ] )
+ current_connection->state &= ~FLAG_TARGETED;
+
+ current_connection->links[ link_id ^ 0x1 ] = 0;
+ drag_ptr->links[ link_id ^ 0x1 ] = 0;
+ }
+
+ // Create the new connection
+ vg_success( "Creating connection on link %u (%hu)\n", link_id, world.id_drag_from );
+
+ cell_ptr->links[ link_id ] = world.id_drag_from;
+ drag_ptr->links[ link_id ] = world.selected;
+
+ cell_ptr->state |= FLAG_TARGETED;
+ drag_ptr->state |= FLAG_IS_TRIGGER;
+ world.id_drag_from = 0;
+ }
+ }
+ }
+ else
+ {
+ world.selected = -1;
+ }
+
+ if( !(vg_get_button("secondary") && world.id_drag_from) )
+ world.id_drag_from = 0;
+ }
+ else
+ {
+ world.selected = -1;
+ world.id_drag_from = 0;
+ }
+
+ // Marble state updates
+ // ========================================================================================================
+ if( is_simulation_running() )
+ {
+ while( world.sim_frame < (int)((vg_time-world.sim_start)*world.sim_speed) )
+ {
+ sfx_set_playrnd( &audio_random, &audio_system_balls_switching, 0, 9 );
+
+ // Update splitter deltas
+ for( int i = 0; i < world.h*world.w; i ++ )
+ {
+ struct cell *cell = &world.data[i];
+ if( cell->config == k_cell_type_split )
+ {
+ cell->state &= ~FLAG_FLIP_ROTATING;
+ }
+ if( cell->state & FLAG_IS_TRIGGER )
+ cell->state &= ~FLAG_TRIGGERED;
+ }
+
+ int alive_count = 0;
+
+ // Update fish positions
+ for( int i = 0; i < world.num_fishes; i ++ )
+ {
+ struct fish *fish = &world.fishes[i];
+
+ if( fish->state == k_fish_state_soon_dead )
+ fish->state = k_fish_state_dead;
+
+ 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 );
+
+ if( fish->state == k_fish_state_alive )
+ {
+ // Apply to output
+ if( cell_current->state & FLAG_OUTPUT )
+ {
+ for( int j = 0; j < arrlen( world.io ); j ++ )
+ {
+ struct cell_terminal *term = &world.io[j];
+
+ if( v2i_eq( term->pos, fish->pos ) )
+ {
+ 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;
+ }
+
+
+ if( cell_current->config == k_cell_type_merge )
+ {
+ // 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
+ {
+ // 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( cell_next->state & (FLAG_CANAL|FLAG_OUTPUT) )
+ {
+ struct cell_description *desc = &cell_descriptions[ cell_next->config ];
+
+ if( cell_next->config == k_cell_type_merge )
+ {
+ 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 );
+ }
+ else if( fish->state == k_fish_state_bg )
+ {
+ v2i_add( fish->pos, fish->dir, fish->pos );
+
+ if( !world_check_pos_ok( fish->pos ) )
+ fish->state = k_fish_state_dead;
+ else
+ {
+ struct cell *cell_entry = pcell( fish->pos );
+
+ if( cell_entry->state & FLAG_CANAL )
+ {
+ 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;
+ }
+ }
+ }
+ }
+ }
+ else { vg_error( "fish behaviour unimplemented for behaviour type (%d)\n" ); }
+
+ if( fish->state >= k_fish_state_alive )
+ alive_count ++;
+ }
+
+ // Second pass (triggers)
+ for( int i = 0; i < world.num_fishes; i ++ )
+ {
+ struct fish *fish = &world.fishes[i];
+
+ 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 )
+ {
+ int trigger_id = cell_current->links[0]?0:1;
+ int connection_id = cell_current->links[trigger_id];
+ int target_px = connection_id % world.w;
+ int target_py = (connection_id - target_px)/world.w;
+
+ vg_line2( (v2f){ fish->pos[0], fish->pos[1] }, (v2f){ target_px, target_py }, 0xffffffff, 0xffffffff );
+
+ struct cell *target_peice = &world.data[ cell_current->links[trigger_id] ];
+
+ cell_current->state |= FLAG_TRIGGERED;
+
+ if( trigger_id )
+ target_peice->state |= FLAG_FLIP_FLOP;
+ else
+ target_peice->state &= ~FLAG_FLIP_FLOP;
+ }
+ }
+ }
+
+ // Third pass (collisions)
+ struct fish *fi, *fj;
+
+ for( int i = 0; i < world.num_fishes; i ++ )
+ {
+ 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 ++ )
+ {
+ fj = &world.fishes[j];
+
+ if( (fj->state == k_fish_state_alive) )
+ {
+ 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;
+ }
+ }
+
+ // Spawn fishes
+ for( int i = 0; i < arrlen( world.io ); i ++ )
+ {
+ struct cell_terminal *term = &world.io[ i ];
+ int is_input = pcell(term->pos)->state & FLAG_INPUT;
+
+ if( is_input )
+ {
+ if( world.sim_frame < term->runs[ world.sim_run ].condition_count )
+ {
+ struct fish *fish = &world.fishes[ world.num_fishes ];
+ v2i_copy( term->pos, fish->pos );
+
+ fish->state = k_fish_state_alive;
+ fish->payload = term->runs[ world.sim_run ].conditions[ world.sim_frame ];
+
+ struct cell *cell_ptr = pcell( fish->pos );
+
+ 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 ++;
+ }
+ }
+ }
+ }
+
+ if( alive_count == 0 )
+ {
+ world.completed = 1;
+
+ for( int i = 0; i < arrlen( world.io ); i ++ )
+ {
+ struct cell_terminal *term = &world.io[ i ];
+ int is_input = pcell(term->pos)->state & FLAG_INPUT;
+
+ if( !is_input )
+ {
+ struct terminal_run *run = &term->runs[ world.sim_run ];
+
+ if( run->recv_count == run->condition_count )
+ {
+ for( int j = 0; j < run->condition_count; j ++ )
+ {
+ if( run->recieved[j] != run->conditions[j] )
+ {
+ world.completed = 0;
+ break;
+ }
+ }
+ }
+ else
+ {
+ world.completed = 0;
+ break;
+ }
+ }
+ }
+
+ if( world.completed )
+ {
+ if( world.sim_run < world.max_runs-1 )
+ {
+ vg_success( "Run passed, starting next\n" );
+ world.sim_run ++;
+ 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
+ {
+ vg_success( "Level passed!\n" );
+
+ u32 score = 0;
+ for( int i = 0; i < world.w*world.h; i ++ )
+ if( world.data[ i ].state & FLAG_CANAL )
+ score ++;
+
+ world.score = score;
+ world.time = world.sim_frame;
+
+ // Copy into career data
+ if( world.pCmpLevel )
+ {
+ career_pass_level( world.pCmpLevel, world.score, 1 );
+ }
+ }
+ }
+ else
+ {
+ if( world.sim_run > 0 )
+ sw_set_achievement( "GOOD_ENOUGH" );
+
+ vg_error( "Level failed :(\n" );
+ }
+
+ simulation_stop();
+ break;
+ }
+
+ world.sim_frame ++;
+ }
+
+ // Position update
+ // =====================================================================================================
+
+ float scaled_time = 0.0f;
+ scaled_time = (vg_time-world.sim_start)*world.sim_speed;
+ world.frame_lerp = scaled_time - (float)world.sim_frame;
+
+ for( int i = 0; i < world.num_fishes; i ++ )
+ {
+ struct fish *fish = &world.fishes[i];
+
+ if( fish->state == k_fish_state_dead )
+ continue;
+
+ 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;
+
+ 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 )
+ {
+ 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;
+ }
+
+ 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;
+
+ 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;
+ }
+ }
+ }
+}
+
+void leaderboard_found( LeaderboardFindResult_t *pCallback );
+void leaderboard_downloaded( LeaderboardScoresDownloaded_t *pCallback );
+
+static void render_tiles( v2i start, v2i end, v4f const regular_colour, v4f const selected_colour )
+{
+ v2i full_start = { 0,0 };
+ v2i full_end = { world.w, world.h };
+
+ if( !start || !end )
+ {
+ start = full_start;
+ end = full_end;
+ }
+
+ glUniform4fv( SHADER_UNIFORM( shader_tile_main, "uColour" ), 1, regular_colour );
+
+ for( int y = start[1]; y < end[1]; y ++ )
+ {
+ for( int x = start[0]; x < end[0]; x ++ )
+ {
+ struct cell *cell = pcell((v2i){x,y});
+ int selected = world.selected == y*world.w + x;
+
+ int tile_offsets[][2] =
+ {
+ {2, 0}, {0, 3}, {0, 2}, {2, 2},
+ {1, 0}, {2, 3}, {3, 2}, {1, 3},
+ {3, 1}, {0, 1}, {1, 2}, {2, 1},
+ {1, 1}, {3, 3}, {2, 1}, {2, 1}
+ };
+
+ int uv[2] = { 3, 0 };
+
+ if( cell->state & (FLAG_CANAL|FLAG_INPUT|FLAG_OUTPUT) )
+ {
+ uv[0] = tile_offsets[ cell->config ][0];
+ uv[1] = tile_offsets[ cell->config ][1];
+ } else continue;
+
+ glUniform4f( SHADER_UNIFORM( shader_tile_main, "uOffset" ), (float)x, (float)y, uv[0], uv[1] );
+ if( selected )
+ {
+ glUniform4fv( SHADER_UNIFORM( shader_tile_main, "uColour" ), 1, selected_colour );
+ draw_mesh( 0, 2 );
+ glUniform4fv( SHADER_UNIFORM( shader_tile_main, "uColour" ), 1, regular_colour );
+ }
+ else
+ draw_mesh( 0, 2 );
+ }
+ }
+}
+
+/*
+static void draw_numbers( v3f coord, int number )
+{
+ v3f pos;
+ v3_copy( coord, pos );
+ int digits[8]; int i = 0;
+
+ while( number > 0 && i < 8 )
+ {
+ digits[i ++] = number % 10;
+ number = number / 10;
+ }
+
+ for( int j = 0; j < i; j ++ )
+ {
+ glUniform3fv( SHADER_UNIFORM( shader_tile_colour, "uOffset" ), 1, pos );
+ draw_mesh( MESH_NUMBERS_OFFSETS[digits[i-j-1]][0], MESH_NUMBERS_OFFSETS[digits[i-j-1]][1] );
+ pos[0] += pos[2] * 0.75f;
+ }
+}*/
+
+static void wbutton_run( enum e_world_button btn_name )
+{
+ struct cell_button *btn = &world.buttons[btn_name];
+
+ // Interaction
+
+ int is_hovering = (world.tile_x == world.w-1 && world.tile_y == world.h-btn_name-2)?1:0;
+ if( vg_get_button_up( "primary" ) && is_hovering )
+ {
+ // Click event
+ btn->pressed ^= 0x1;
+
+ if( btn_name == k_world_button_sim )
+ {
+ if( btn->pressed )
+ {
+ vg_success( "Starting simulation!\n" );
+
+ sfx_set_playrnd( &audio_rolls, &audio_system_balls_rolling, 0, 1 );
+
+ world.num_fishes = 0;
+ 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;
+
+ io_reset();
+ }
+ else
+ {
+ simulation_stop();
+ }
+ }
+ }
+
+ // Drawing
+ if( btn->pressed ) btn->light_target = is_hovering? 0.9f: 0.8f;
+ else btn->light_target = is_hovering? 0.2f: 0.0f;
+
+ if( vg_get_button( "primary" ) && is_hovering )
+ btn->light_target = 1.0f;
+
+ btn->light = vg_lerpf( btn->light, btn->light_target, vg_time_delta*26.0f );
+
+ glUniform4f( SHADER_UNIFORM( shader_buttons, "uOffset" ),
+ world.w-1,
+ world.h-btn_name-2,
+ (float)btn_name,
+ 3.0f
+ );
+ glUniform4f( SHADER_UNIFORM( shader_buttons, "uColour" ), 0.204f, 0.345f, 0.553f, btn->light );
+
+ draw_mesh( 0, 2 );
+}