+ if( cell->state & FLAG_EMITTER )
+ {
+ cell->cc = term->runs[0].conditions[0];
+ }
+ }
+
+ // 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];
+
+ // Fade out edges of world so that there isnt an obvious line
+ int dist_x = 16 - VG_MIN( VG_MIN( x, 16 ), 16-VG_MAX( x-16-world.w, 0 ) );
+ int dist_y = 16 - VG_MIN( VG_MIN( y, 16 ), 16-VG_MAX( y-16-world.h, 0 ) );
+ int dist = VG_MAX( dist_x, dist_y ) * 16;
+
+ int value = VG_MAX( 0, 0xFF-0x3F + hash21i( (v2i){x,y}, 0x3F ) - dist );
+
+ px[0] = value;
+ px[1] = 0;
+ px[2] = 0;
+ px[3] = 0;
+ }
+ }
+
+ // Level selection area
+
+ for( int i = 0; i < vg_list_size( career_packs ); i ++ )
+ {
+ struct career_level_pack *grid = &career_packs[ i ];
+
+ int j = 0;
+
+ for( int y = 0; y < grid->dims[1]; y ++ )
+ {
+ for( int x = 0; x < grid->dims[0]; x ++ )
+ {
+ u8 *px = &info_buffer[((y+16+grid->origin[1])*64+16+x+grid->origin[0])*4];
+ px[0] = 0x10;
+
+ if( j < grid->count )
+ {
+ struct cmp_level *lvl = &grid->pack[ j ++ ];
+ v2i_add( grid->origin, (v2i){x,y}, lvl->btn.position );
+ }
+ }
+ }
+ }
+
+ info_buffer[(((16+world.h-3)*64)+world.w+16-1)*4] = 0x30;
+ info_buffer[(((16+world.h-2)*64)+world.w+16-1)*4] = 0x30;
+
+ // 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;
+
+ // Only make breakouts for terminals on the edge
+ if( !(term->pos[1] == 1 || term->pos[1] == world.h-2) )
+ continue;
+
+ 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|FLAG_EMITTER)? 1: -1;
+ original_y = turtle_dir[1];
+
+ info_buffer[((turtle[1]*64)+turtle[0])*4] = 0;
+ v2i_add( turtle_dir, turtle, turtle );
+
+ 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))
+ || (target->state & FLAG_EMITTER) )
+ {
+ 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;
+
+ // Setup world button locations
+ for( int i = 0; i < vg_list_size( world.st.buttons ); i ++ )
+ {
+ struct world_button *btn = &world.st.buttons[i];
+ btn->position[0] = world.w -1;
+ btn->position[1] = world.h -i -2;
+ }
+
+ 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_EMITTER ) 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 int career_load_success = 0;
+
+static void career_serialize(void)
+{
+ if( !career_load_success )
+ return;
+
+ 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_packs ); i ++ )
+ {
+ struct career_level_pack *set = &career_packs[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 )
+ {
+ #ifdef VG_STEAM
+ if( !lvl->is_tutorial && upload )
+ leaderboard_set_score( lvl, score );
+ #endif
+
+ lvl->completed_score = score;
+ }
+
+ if( lvl->unlock ) career_unlock_level( lvl->unlock );
+
+ #ifdef VG_STEAM
+ if( lvl->achievement )
+ {
+ sw_set_achievement( lvl->achievement );
+ }
+
+ // Check ALL maps to trigger master engineer
+ for( int i = 0; i < vg_list_size( career_packs ); i ++ )
+ {
+ struct career_level_pack *set = &career_packs[i];
+
+ for( int j = 0; j < set->count; j ++ )
+ {
+ if( set->pack[j].completed_score == 0 )
+ return;
+ }
+ }
+
+ sw_set_achievement( "MASTER_ENGINEER" );
+ #endif
+ }
+}
+
+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 = 0;
+ 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 );
+ }
+
+ 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_packs ); i ++ )
+ {
+ struct career_level_pack *set = &career_packs[i];
+
+ for( int j = 0; j < set->count; j ++ )
+ career_reset_level( &set->pack[j] );
+ }
+
+ // Header information
+ // =================================
+
+ struct cmp_level *lvl_to_load = &career_packs[0].pack[0];
+
+ // Decode everything from dstate
+ for( int i = 0; i < vg_list_size( career_packs ); i ++ )
+ {
+ struct career_level_pack *set = &career_packs[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 )
+ lvl_to_load = lvl;
+ }
+ }
+
+ if( console_changelevel( 1, &lvl_to_load->map_name ) )
+ {
+ world.pCmpLevel = lvl_to_load;
+ gen_level_text( world.pCmpLevel );
+ }
+
+ career_load_success = 1;
+}
+
+// MAIN GAMEPLAY
+// ===========================================================================================================
+static int is_simulation_running(void)
+{
+ return world.st.buttons[ k_world_button_sim ].state;
+}
+
+static void clear_animation_flags(void)
+{
+ for( int i = 0; i < world.w*world.h; i ++ )
+ world.data[ i ].state &= ~(FLAG_FLIP_FLOP|FLAG_FLIP_ROTATING);
+}
+
+static void simulation_stop(void)
+{
+ world.st.buttons[ k_world_button_sim ].state = 0;
+ world.st.buttons[ k_world_button_pause ].state = 0;
+
+ world.num_fishes = 0;
+ world.sim_frame = 0;
+ world.sim_run = 0;
+ world.frame_lerp = 0.0f;
+
+ io_reset();
+
+ sfx_system_fadeout( &audio_system_balls_rolling, 44100 );
+
+ clear_animation_flags();
+
+ vg_info( "Stopping simulation!\n" );
+}
+
+static void simulation_start(void)
+{
+ 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_run = 0;
+
+ world.sim_delta_speed = world.st.buttons[ k_world_button_speedy ].state? 10.0f: 2.5f;
+ world.sim_delta_ref = vg_time;
+ world.sim_internal_ref = 0.0f;
+ world.sim_internal_time = 0.0f;
+ world.pause_offset_target = 0.0f;
+
+ world.sim_target = 0;
+
+ clear_animation_flags();
+
+ io_reset();
+
+ if( world.pCmpLevel )
+ {
+ world.pCmpLevel->completed_score = 0;
+ }
+}
+
+static int world_check_pos_ok( v2i co, int dist )
+{
+ return (co[0] < dist || co[0] >= world.w-dist || co[1] < dist || co[1] >= world.h-dist)? 0: 1;
+}
+
+static int cell_interactive( v2i co )
+{
+ struct cell *cell;
+
+ // Bounds check
+ if( world_check_pos_ok( co, 1 ) )
+ {
+ cell = pcell( co );
+ if( cell->state & FLAG_EMITTER )
+ return 1;
+ }
+
+ if( !world_check_pos_ok( co, 2 ) )
+ return 0;
+
+ // Flags check
+ if( cell->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|FLAG_EMITTER)) )
+ 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;
+}
+
+static void vg_update(void)
+{
+ // Async events
+ if( world.st.lvl_to_load )
+ {
+ world.st.world_transition = (world.st.lvl_load_time-vg_time) * 4.0f;
+
+ if( vg_time > world.st.lvl_load_time )
+ {
+ if( console_changelevel( 1, &world.st.lvl_to_load->map_name ) )
+ {
+ world.pCmpLevel = world.st.lvl_to_load;
+ gen_level_text( world.pCmpLevel );
+ }
+
+ world.st.lvl_to_load = NULL;
+ }
+ }
+ else
+ {
+ world.st.world_transition = vg_minf( 1.0f, (vg_time-world.st.lvl_load_time) * 4.0f );
+ }
+
+ // Camera
+ // ========================================================================================================
+
+ float r1 = (float)vg_window_y / (float)vg_window_x,
+ r2 = (float)world.h / (float)world.w,
+ size;
+
+ static float size_current = 2.0f;
+ static v3f origin_current = { 0.0f, 0.0f, 0.0f };
+ static v2f drag_offset = { 0.0f, 0.0f };
+ static v2f view_point = { 0.0f, 0.0f };
+ v2f result_view;
+
+ size = ( r2 < r1? (float)(world.w+5) * 0.5f: ((float)(world.h+5) * 0.5f) / r1 ) + 0.5f;
+
+ v2f origin;
+ v2f vt_target;
+
+ origin[0] = floorf( -0.5f * ((float)world.w-4.5f) );
+ origin[1] = floorf( -0.5f * world.h );
+
+ // Create and clamp result view
+ v2_add( view_point, drag_offset, result_view );
+ result_view[0] = vg_clampf( result_view[0], -world.st.zoom, world.st.zoom );
+ result_view[1] = vg_clampf( result_view[1], -world.st.zoom*r1, world.st.zoom*r1 );
+
+ v2_add( origin, result_view, vt_target );
+
+ // Lerp towards target
+ size_current = vg_lerpf( size_current, size - world.st.zoom, vg_time_delta * 6.0f );
+ v2_lerp( origin_current, vt_target, vg_time_delta * 6.0f, origin_current );
+
+ m3x3_projection( m_projection, -size_current, size_current, -size_current*r1, size_current*r1 );
+ m3x3_identity( m_view );
+ m3x3_translate( m_view, origin_current );
+ 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] );
+
+ // Camera dragging
+ {
+ static v2f drag_origin; // x/y pixel
+
+ if( vg_get_button_down( "tertiary" ) )
+ v2_copy( vg_mouse, drag_origin );
+ else if( vg_get_button( "tertiary" ) )
+ {
+ // get offset
+ v2_sub( vg_mouse, drag_origin, drag_offset );
+ v2_div( drag_offset, (v2f){ vg_window_x, vg_window_y }, drag_offset );
+ v2_mul( drag_offset, (v2f){ size_current*2.0f, -size_current*r1*2.0f }, drag_offset );
+ }
+ else
+ {
+ v2_copy( result_view, view_point );
+ v2_copy( (v2f){0.0f,0.0f}, drag_offset );
+ }
+ }
+
+ // Zooming
+ {
+ v2f mview_local;
+ v2f mview_new;
+ v2f mview_cur;
+ v2f mview_delta;
+ float rsize;
+
+ rsize = size-world.st.zoom;
+
+ v2_div( vg_mouse, (v2f){ vg_window_x*0.5f, vg_window_y*0.5f }, mview_local );
+ v2_add( (v2f){ -rsize, -rsize*r1 }, (v2f){ mview_local[0]*rsize, (2.0f-mview_local[1])*rsize*r1 }, mview_cur );
+
+ world.st.zoom = vg_clampf( world.st.zoom + vg_mouse_wheel[1], 0.0f, size - 4.0f );
+
+ // Recalculate new position
+ rsize = size-world.st.zoom;
+ v2_add( (v2f){ -rsize, -rsize*r1 }, (v2f){ mview_local[0]*rsize, (2.0f-mview_local[1])*rsize*r1 }, mview_new );
+
+ // Apply offset
+ v2_sub( mview_new, mview_cur, mview_delta );
+ v2_add( mview_delta, view_point, view_point );
+ }
+
+ // 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( !(cell_ptr->state & FLAG_EMITTER) )
+ {
+ 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->state & FLAG_CANAL) && !(cell_ptr->config == k_cell_type_split) )
+ {
+ world.id_drag_from = world.selected;
+
+ struct cell_description *desc = &cell_descriptions[ world.data[world.id_drag_from].config ];
+ v2_add( desc->trigger_pos, (v2f){ world.tile_x, world.tile_y }, world.drag_from_co );
+ }
+ }
+
+ float local_x = vg_mouse_ws[0] - (float)world.tile_x;
+
+ if( vg_get_button_up("secondary") && world.id_drag_from == world.selected )
+ {
+ u32 link_id = cell_ptr->links[ 0 ]? 0: 1;
+
+ // break existing connection off
+ if( cell_ptr->links[ link_id ] )
+ {
+ struct cell *current_connection = &world.data[ cell_ptr->links[ link_id ]];
+
+ if( !current_connection->links[ link_id ^ 0x1 ] )
+ current_connection->state &= ~FLAG_TARGETED;
+
+ current_connection->links[ link_id ] = 0;
+ cell_ptr->links[ link_id ] = 0;
+ }
+
+ cell_ptr->state &= ~FLAG_IS_TRIGGER;
+ world.id_drag_from = 0;
+ }
+
+ else if( world.id_drag_from && (cell_ptr->state & (FLAG_CANAL|FLAG_EMITTER)) &&
+ ((cell_ptr->config == k_cell_type_split) || (cell_ptr->state & FLAG_EMITTER)) )
+ {
+ 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;
+ }
+
+ for( u32 i = 0; i < 2; i ++ )
+ {
+ if( drag_ptr->links[ i ] )
+ {
+ vg_warn( "Destroying link %u (%hu)\n", i, drag_ptr->links[ i ] );
+
+ struct cell *current_connection = &world.data[ drag_ptr->links[ i ]];
+ if( current_connection->links[ i ^ 0x1 ] == 0 )
+ current_connection->state &= ~FLAG_TARGETED;
+
+ current_connection->links[ i ] = 0;
+ drag_ptr->links[ i ] = 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() )
+ {
+ float old_time = world.sim_internal_time;
+
+ if( !world.st.buttons[ k_world_button_pause ].state )
+ world.sim_internal_time = world.sim_internal_ref + (vg_time-world.sim_delta_ref) * world.sim_delta_speed;
+ else
+ world.sim_internal_time = vg_lerpf( world.sim_internal_time, world.sim_internal_ref + world.pause_offset_target, vg_time_delta*15.0f );
+ world.sim_internal_delta = world.sim_internal_time-old_time;
+
+ world.sim_target = (int)floorf(world.sim_internal_time);
+
+ int success_this_frame = 0;
+ int failure_this_frame = 0;
+
+ while( world.sim_frame < world.sim_target )
+ {
+ sfx_set_playrnd( &audio_random, &audio_system_balls_switching, 0, 8 );
+
+ // 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 ) )
+ {
+ if( fish->payload == run->conditions[ run->recv_count ] )
+ success_this_frame = 1;
+ else
+ failure_this_frame = 1;
+
+ run->recieved[ run->recv_count ++ ] = fish->payload;
+ }
+ else
+ failure_this_frame = 1;
+
+ break;
+ }
+ }
+
+ fish->state = k_fish_state_dead;
+ fish->death_time = -1000.0f;
+ 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;
+ fish->death_time = world.sim_internal_time;
+ }
+ 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;
+ fish->death_time = world.sim_internal_time;
+ }
+ }
+ else
+ fish->flow_reversed = ( fish->dir[0] != -desc->start[0] ||
+ fish->dir[1] != -desc->start[1] )? 1: 0;
+ }
+ }
+ else
+ {
+ if( world_check_pos_ok( fish->pos, 2 ) )
+ fish->state = k_fish_state_bg;
+ else
+ {
+ fish->state = k_fish_state_dead;
+ fish->death_time = world.sim_internal_time;
+ }
+ }
+ }
+
+ //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, 2 ) )
+ {
+ fish->state = k_fish_state_dead;
+ fish->death_time = -1000.0f;
+ }
+ 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 )
+ {
+ #ifdef VG_STEAM
+ sw_set_achievement( "CAN_DO_THAT" );
+ #endif
+
+ 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;
+
+ struct cell *target_peice = &world.data[ cell_current->links[trigger_id] ];
+
+ if( target_peice->state & FLAG_EMITTER )
+ {
+ struct fish *fish = &world.fishes[ world.num_fishes ];
+ lcell( cell_current->links[trigger_id], fish->pos );
+
+ fish->state = k_fish_state_soon_alive;
+ fish->payload = target_peice->cc;
+
+ if( target_peice->config != k_cell_type_stub )
+ {
+ struct cell_description *desc = &cell_descriptions[ target_peice->config ];
+ v2i_copy( desc->start, fish->dir );
+ fish->flow_reversed = 1;
+
+ world.num_fishes ++;
+ alive_count ++;
+ }
+ }
+ else
+ {
+ if( trigger_id )
+ target_peice->state |= FLAG_FLIP_FLOP;
+ else
+ target_peice->state &= ~FLAG_FLIP_FLOP;
+ }
+
+ cell_current->state |= FLAG_TRIGGERED;
+ }
+ }
+ }
+
+ // 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 )
+ {
+ #ifdef VG_STEAM
+ sw_set_achievement( "BANG" );
+ #endif
+
+ // Shatter death (+0.5s)
+ float death_time = world.sim_internal_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 )
+ {
+ char emit = term->runs[ world.sim_run ].conditions[ world.sim_frame ];
+ if( emit == ' ' )
+ continue;
+
+ struct fish *fish = &world.fishes[ world.num_fishes ];
+ v2i_copy( term->pos, fish->pos );
+
+ fish->state = k_fish_state_alive;
+ fish->payload = emit;
+
+ 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 ];
+
+ if( pcell(term->pos)->state & FLAG_OUTPUT )
+ {
+ 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_target = 0;
+ world.num_fishes = 0;
+
+ // Reset timing reference points
+ world.sim_delta_ref = vg_time;
+ world.sim_internal_ref = 0.0f;
+
+ if( world.st.buttons[ k_world_button_pause ].state )
+ world.pause_offset_target = 0.5f;
+ else
+ world.pause_offset_target = 0.0f;
+
+ world.sim_internal_time = 0.0f;
+
+ 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 );
+ }
+
+ sfx_set_play( &audio_tones, &audio_system_balls_extra, 9 );
+ failure_this_frame = 0;
+ success_this_frame = 0;
+ }
+ }
+ else
+ {
+ #ifdef VG_STEAM
+ if( world.sim_run > 0 )
+ sw_set_achievement( "GOOD_ENOUGH" );
+ #endif