+static void colour_code_v3( char const cc, v3f target )
+{
+ static v3f colour_sets[] =
+ { { 1.0f, 0.9f, 0.3f },
+ { 0.2f, 0.9f, 0.14f },
+ { 0.4f, 0.8f, 1.00f },
+ { 0.882f, 0.204f, 0.922f }
+ };
+
+ if( cc >= 'a' && cc <= 'z' )
+ {
+ int id = cc - 'a';
+
+ if( id < vg_list_size( colour_sets ) )
+ {
+ v3_copy( colour_sets[ id ], target );
+ return;
+ }
+ }
+
+ v3_copy( (v3f){0.0f,0.0f,0.0f}, target );
+}
+
+static int hash21i( v2i p, u32 umod )
+{
+ static const int random_noise[] =
+ {
+ 0x46,0xD5,0xB8,0xD3,0xF2,0xE5,0xCC,0x07,0xD0,0xB3,0x7A,0xA2,0xC3,0xDA,0xDC,0x7F,0xE0,0xB7,0x42,0xA0,0xBF,0x41,0x92,0x32,0x6F,0x0D,0x45,0xC7,0x54,0xDB,0x30,0xC2,
+ 0xD5,0xDA,0x55,0x09,0xDE,0x74,0x48,0x20,0xE1,0x24,0x5C,0x4D,0x6F,0x36,0xD8,0xE9,0x8D,0x8F,0x54,0x99,0x98,0x51,0xFE,0xDB,0x26,0x04,0x65,0x57,0x56,0xF3,0x53,0x30,
+ 0x3D,0x16,0xC0,0xB6,0xF2,0x47,0xCF,0x62,0xB0,0x6C,0x8F,0x4F,0x8C,0x4C,0x17,0xF0,0x19,0x7E,0x2D,0x81,0x8D,0xFB,0x10,0xD3,0x49,0x50,0x60,0xFD,0x38,0x15,0x3B,0xEE,
+ 0x05,0xC1,0xCF,0x62,0x97,0x75,0xDF,0x4E,0x4D,0x89,0x5E,0x88,0x5C,0x30,0x8C,0x54,0x1D,0x39,0x41,0xEA,0xA2,0x63,0x12,0x1B,0x8E,0x35,0x22,0x9B,0x98,0xA3,0x7F,0x80,
+ 0xD6,0x27,0x94,0x66,0xB5,0x1D,0x7E,0xDF,0x96,0x28,0x38,0x3A,0xA0,0xE8,0x71,0x09,0x62,0x5E,0x9D,0x53,0x58,0x1B,0x7D,0x0D,0x2D,0x99,0x77,0x83,0xC3,0x89,0xC2,0xA2,
+ 0xA7,0x1D,0x78,0x80,0x37,0xC1,0x87,0xFF,0x65,0xBF,0x2C,0xF1,0xE5,0xB3,0x09,0xE0,0x25,0x92,0x83,0x0F,0x8A,0x57,0x3C,0x0B,0xC6,0xBC,0x44,0x16,0xE3,0xCE,0xC3,0x0D,
+ 0x69,0xD3,0xC6,0x99,0xB8,0x46,0x44,0xC4,0xF3,0x1E,0xBF,0xF5,0xB4,0xDB,0xFB,0x93,0xA1,0x7B,0xC9,0x08,0x77,0x22,0xE5,0x02,0xEF,0x9E,0x90,0x94,0x8A,0xA6,0x3D,0x7E,
+ 0xA2,0xA0,0x10,0x82,0x47,0x5C,0xAA,0xF8,0x2F,0x0D,0x9F,0x76,0xDA,0x99,0x0F,0xCB,0xE2,0x02,0x0C,0x75,0xCA,0x35,0x29,0xA6,0x49,0x83,0x6D,0x91,0xB4,0xEC,0x31,0x69,
+ 0xBA,0x13,0xF3,0xC7,0x21,0x06,0xC8,0x79,0xEF,0xB1,0x9C,0x6A,0xEE,0x64,0x9A,0xDC,0x1E,0xC6,0x18,0x93,0xA9,0x7E,0x89,0x7D,0x96,0xE5,0x44,0xB8,0x00,0x15,0xAF,0x8C,
+ 0x78,0x8F,0xA8,0x05,0xA7,0x07,0x25,0x9A,0xC8,0x5D,0x90,0x1A,0x41,0x53,0x30,0xD3,0x24,0x33,0x71,0xB4,0x50,0x6E,0xE4,0xEA,0x0D,0x2B,0x6D,0xF5,0x17,0x08,0x74,0x49,
+ 0x71,0xC2,0xAC,0xF7,0xDC,0xB2,0x7E,0xCC,0xB6,0x1B,0xB8,0xA9,0x52,0xCF,0x6B,0x51,0xD2,0x4E,0xC9,0x43,0xEE,0x2E,0x92,0x24,0xBB,0x47,0x4D,0x0C,0x3E,0x21,0x53,0x19,
+ 0xD4,0x82,0xE2,0xC6,0x93,0x85,0x0A,0xF8,0xFA,0x04,0x07,0xD3,0x1D,0xEC,0x03,0x66,0xFD,0xB1,0xFB,0x8F,0xC5,0xDE,0xE8,0x29,0xDF,0x23,0x09,0x9D,0x7C,0x43,0x3D,0x4D,
+ 0x89,0xB9,0x6F,0xB4,0x6B,0x4A,0x51,0xC3,0x94,0xF4,0x7C,0x5E,0x19,0x87,0x79,0xC1,0x80,0x0C,0x45,0x12,0xEC,0x95,0xF3,0x31,0x68,0x42,0xE1,0x06,0x57,0x0E,0xA7,0xFB,
+ 0x78,0x96,0x87,0x23,0xA5,0x20,0x7A,0x09,0x3A,0x45,0xE6,0xD9,0x5E,0x6A,0xD6,0xAA,0x29,0x50,0x92,0x4E,0xD0,0xB5,0x91,0xC2,0x9A,0xCF,0x07,0xFE,0xB2,0x15,0xEB,0xE4,
+ 0x84,0x40,0x14,0x47,0xFA,0x93,0xB9,0x06,0x69,0xDB,0xBD,0x4E,0xEA,0x52,0x9B,0xDE,0x5B,0x50,0x36,0xAB,0xB3,0x1F,0xD2,0xCD,0x9C,0x13,0x07,0x7E,0x8B,0xED,0x72,0x62,
+ 0x74,0x77,0x3B,0x88,0xAC,0x5B,0x6A,0xBC,0xDA,0x99,0xE8,0x24,0x90,0x5A,0xCA,0x8D,0x5C,0x2B,0xF8,0xF1,0xE1,0x1D,0x94,0x11,0xEA,0xCC,0x02,0x09,0x1E,0xA2,0x48,0x67,
+ 0x87,0x5A,0x7E,0xC6,0xCC,0xA3,0xFB,0xC5,0x36,0xEB,0x5C,0xE1,0xAF,0x1E,0xBE,0xE7,0xD8,0x8F,0x70,0xAE,0x42,0x05,0xF5,0xCD,0x2D,0xA2,0xB0,0xFD,0xEF,0x65,0x2C,0x22,
+ 0xCB,0x8C,0x8B,0xAA,0x3D,0x86,0xE2,0xCD,0xBE,0xC3,0x42,0x38,0xE3,0x9C,0x08,0xB5,0xAE,0xBD,0x54,0x73,0x83,0x70,0x24,0x47,0xCA,0x4C,0x04,0xC4,0xE0,0x1D,0x40,0xED,
+ 0xF4,0x2B,0x50,0x8E,0x97,0xB3,0xF0,0xA6,0x76,0xDB,0x49,0x30,0xE5,0xD9,0x71,0x07,0xB2,0xF1,0x0F,0xD6,0x77,0xAA,0x72,0xC0,0xAF,0x66,0xD8,0x40,0xC6,0x08,0x19,0x8C,
+ 0xD9,0x8F,0x5A,0x75,0xAC,0xBE,0xC2,0x40,0x5B,0xBD,0x0D,0x1D,0x00,0xAF,0x26,0x5E,0x78,0x43,0xAA,0xC6,0x4F,0xF3,0xD8,0xE2,0x7F,0x0C,0x1E,0x77,0x4D,0x35,0x96,0x23,
+ 0x32,0x44,0x03,0x8D,0x92,0xE7,0xFD,0x48,0x07,0xD0,0x58,0xFC,0x6D,0xC9,0x91,0x33,0xF0,0x23,0x45,0xA4,0x29,0xB9,0xF5,0xB0,0x68,0x8F,0x7B,0x59,0x15,0x8E,0xA6,0x66,
+ 0x15,0xA0,0x76,0x9B,0x69,0xCB,0x38,0xA5,0xF4,0xB4,0x6B,0xDC,0x1F,0xAB,0xAE,0x12,0x77,0xC0,0x8C,0x4A,0x03,0xB9,0x73,0xD3,0x6D,0x52,0xC5,0xF5,0x6E,0x4E,0x4B,0xA3,
+ 0x24,0x02,0x58,0xEE,0x5F,0xF9,0xD6,0xD0,0x1D,0xBC,0xF4,0xB8,0x4F,0xFD,0x4B,0x2D,0x34,0x77,0x46,0xE5,0xD4,0x33,0x7B,0x9C,0x35,0xCD,0xB0,0x5D,0x06,0x39,0x99,0xEB,
+ 0x0C,0xD0,0x0F,0xF7,0x92,0xB5,0x58,0x5B,0x5E,0x79,0x12,0xF4,0x05,0xF6,0x21,0x07,0x0B,0x49,0x1A,0xFB,0xD4,0x98,0xC4,0xEF,0x7A,0xD6,0xCA,0xA1,0xDA,0xB3,0x51,0x00,
+ 0x76,0xEC,0x08,0x48,0x40,0x35,0xD7,0x94,0xBE,0xF5,0x7B,0xA4,0x20,0x81,0x5F,0x82,0xF3,0x6F,0x96,0x24,0x98,0xB6,0x49,0x18,0xC8,0xC5,0x8C,0xD2,0x38,0x7F,0xC4,0xF6,
+ 0xAA,0x87,0xDC,0x73,0x5B,0xA1,0xAF,0xE5,0x3D,0x37,0x6B,0x85,0xED,0x38,0x62,0x7D,0x57,0xBD,0xCF,0xB5,0x1B,0xA8,0xBB,0x32,0x33,0xD3,0x34,0x5A,0xC1,0x5D,0xFB,0x28,
+ 0x6E,0xE1,0x67,0x51,0xBB,0x31,0x92,0x83,0xAC,0xAA,0x72,0x52,0xFD,0x13,0x4F,0x73,0xD3,0xF0,0x5E,0xFC,0xBA,0xB1,0x3C,0x7B,0x08,0x76,0x03,0x38,0x1E,0xD1,0xCC,0x33,
+ 0xA3,0x1E,0xFC,0xE0,0x82,0x30,0x27,0x93,0x71,0x35,0x75,0x77,0xBA,0x78,0x10,0x33,0xCD,0xAB,0xCF,0x8E,0xAD,0xF9,0x32,0xC9,0x15,0x9F,0xD6,0x6D,0xA8,0xAE,0xB1,0x3F,
+ 0x90,0xEB,0xD4,0xF9,0x31,0x81,0xA3,0x53,0x99,0x4B,0x3C,0x93,0x3B,0xFE,0x55,0xFF,0x25,0x9F,0xCC,0x07,0xC5,0x2C,0x14,0xA7,0xA4,0x1E,0x6C,0xB6,0x91,0x2A,0xE0,0x3E,
+ 0x7F,0x39,0x0A,0xD9,0x24,0x3C,0x01,0xA0,0x30,0x99,0x8E,0xB8,0x1D,0xF9,0xA7,0x78,0x86,0x95,0x35,0x0E,0x21,0xDA,0x7A,0x7B,0xAD,0x9F,0x4E,0xF6,0x63,0x5B,0x96,0xBB,
+ 0x87,0x36,0x3F,0xA7,0x1A,0x66,0x91,0xCD,0xB0,0x3B,0xC0,0x4F,0x54,0xD2,0x5F,0xBB,0x38,0x89,0x1C,0x79,0x7E,0xA2,0x02,0xE4,0x80,0x84,0x1E,0x33,0xAB,0x74,0xFA,0xBE,
+ 0x31,0x46,0x2E,0xC5,0x15,0xB9,0x12,0xE9,0xD3,0x73,0x43,0xEA,0x74,0x11,0xA7,0xC0,0xD5,0xD8,0x39,0x08,0x9F,0x4F,0xC7,0x71,0x25,0x09,0x51,0x65,0xD6,0xA8,0x02,0x1F
+ };
+
+ return random_noise[ (random_noise[p[1] & 1023] + p[0]) & 1023 ] & umod;
+}
+
+// MESH
+// ===========================================================================================================
+
+static void init_mesh( struct mesh *m, float const *tris, u32 length )
+{
+ m->elements = length/3;
+ glGenVertexArrays( 1, &m->vao );
+ glGenBuffers( 1, &m->vbo );
+
+ glBindVertexArray( m->vao );
+ glBindBuffer( GL_ARRAY_BUFFER, m->vbo );
+ glBufferData( GL_ARRAY_BUFFER, length*sizeof(float), tris, GL_STATIC_DRAW );
+
+ glVertexAttribPointer( 0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0 );
+ glEnableVertexAttribArray( 0 );
+
+ VG_CHECK_GL();
+}
+
+static void free_mesh( struct mesh *m )
+{
+ glDeleteVertexArrays( 1, &m->vao );
+ glDeleteBuffers( 1, &m->vbo );
+}
+
+static void draw_mesh( int const start, int const count )
+{
+ glDrawArrays( GL_TRIANGLES, start*3, count*3 );
+}
+
+static void use_mesh( struct mesh *m )
+{
+ glBindVertexArray( m->vao );
+}
+
+
+
+
+
+static struct cell_button *get_wbutton( enum e_world_button btn )
+{
+ return &world.buttons[ btn ];
+}
+
+void leaderboard_set_score( struct cmp_level *cmp_level, u32 score );
+
+// 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') || *c == ' ' )
+ {
+ 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];
+
+ 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) )
+ {
+ 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 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_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 );
+ }
+
+ 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;
+ }
+ }
+ }
+
+ career_load_success = 1;
+}
+
+// MAIN GAMEPLAY
+// ===========================================================================================================
+static int is_simulation_running(void)
+{
+ return world.buttons[ k_world_button_sim ].pressed;
+}
+
+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.buttons[ k_world_button_sim ].pressed = 0;
+ world.buttons[ k_world_button_pause ].pressed = 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 = 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();
+}
+
+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->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 = local_x > 0.5f? 1: 0;
+
+ // 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;
+ }
+
+ if( world.id_drag_from && (cell_ptr->state & FLAG_CANAL) && (cell_ptr->config == k_cell_type_split) )
+ {
+ 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() )
+ {
+ float old_time = world.sim_internal_time;
+
+ if( !world.buttons[ k_world_button_pause ].pressed )
+ 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);
+
+ 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 ) )
+ run->recieved[ run->recv_count ++ ] = fish->payload;
+
+ 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 ) )
+ 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 ) )
+ {
+ 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 )
+ {
+ 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;
+
+ 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 = 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 ];
+ 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_target = 0;
+ world.num_fishes = 0;
+
+ // Reset timing reference points
+ world.sim_delta_ref = vg_time;
+ world.sim_internal_ref = 0.0f;
+
+ if( world.buttons[ k_world_button_pause ].pressed )
+ 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 );
+ }
+ }
+ }
+ else
+ {
+ if( world.sim_run > 0 )
+ sw_set_achievement( "GOOD_ENOUGH" );
+
+ vg_error( "Level failed :(\n" );
+ }
+
+ simulation_stop();
+ break;
+ }
+
+ world.sim_frame ++;
+ }
+
+ // Position update
+ // =====================================================================================================
+
+ world.frame_lerp = world.sim_internal_time - floorf( world.sim_internal_time );
+
+ 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.sim_internal_time > 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;
+ }
+
+ v2_sub( fish->physics_co, fish->physics_v, fish->physics_v );
+ v2_divs( fish->physics_v, world.sim_internal_delta, fish->physics_v );
+ }
+ }
+}
+
+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 )
+{
+ static v3f button_colours[] = {
+ {0.204f, 0.345f, 0.553f},
+ {0.204f, 0.345f, 0.553f},
+ {0.741f, 0.513f, 0.078f},
+ {1.0f, 0.0f, 0.0f}
+ };
+
+ 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
+ if( btn_name == k_world_button_sim )
+ {
+ if( world.buttons[ k_world_button_pause ].pressed )
+ {
+ if( !btn->pressed )
+ {
+ btn->pressed = 1;
+ simulation_start();
+
+ world.pause_offset_target = 0.5f;
+ }
+ else
+ world.pause_offset_target += 1.0f;
+ }
+ else
+ {
+ btn->pressed ^= 0x1;
+
+ if( btn->pressed )
+ simulation_start();
+ else
+ simulation_stop();
+ }
+ }
+ else if( btn_name == k_world_button_pause )
+ {
+ btn->pressed ^= 0x1;
+
+ world.sim_internal_ref = world.sim_internal_time;
+ world.sim_delta_ref = vg_time;
+
+ if( btn->pressed )
+ {
+ float time_frac = world.sim_internal_time-floorf(world.sim_internal_time);
+ world.pause_offset_target = 0.5f - time_frac;
+ }
+ else
+ world.pause_offset_target = 0.0f;
+ }
+ else
+ {
+ btn->pressed ^= 0x1;
+ }
+
+ sfx_set_play( &audio_clicks, &audio_system_ui, btn->pressed?1:0 );
+ }
+
+ // Drawing
+ if( btn->pressed )
+ {
+ if( is_hovering )
+ {
+ btn->light_target = 0.9f;
+ }
+ else
+ {
+ if( btn_name == k_world_button_sim && world.buttons[ k_world_button_pause ].pressed )
+ btn->light_target = fabsf(sinf( vg_time * 2.0f )) * 0.3f + 0.3f;
+ else
+ btn->light_target = 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 );
+
+ // Draw
+
+ v4f final_colour;
+ v3_copy( button_colours[ btn_name ], final_colour );
+ final_colour[3] = btn->light;
+
+ glUniform4f( SHADER_UNIFORM( shader_buttons, "uOffset" ),
+ world.w-1,
+ world.h-btn_name-2,
+ (float)btn_name,
+ 3.0f
+ );
+ glUniform4fv( SHADER_UNIFORM( shader_buttons, "uColour" ), 1, final_colour );
+
+ draw_mesh( 0, 2 );
+}
+
+void vg_render(void)