| #include <cinttypes> | |
| #include "scrolly-tile.hpp" | |
| #include "graphics/color.hpp" | |
| using namespace blit; | |
| #define __AUDIO__ | |
| #define SCREEN_W 160 | |
| #define SCREEN_H 120 | |
| #define TILE_W 10 | |
| #define TILE_H 10 | |
| #define TILE_SOLID 0b1 << 7 | |
| #define TILE_WATER 0b1 << 6 | |
| // Bitmask for keeping track of adjacent tiles | |
| // in a single uint8_t | |
| #define TILE_LEFT 1 << 7 | |
| #define TILE_RIGHT 1 << 6 | |
| #define TILE_BELOW 1 << 5 | |
| #define TILE_ABOVE 1 << 4 | |
| #define TILE_ABOVE_LEFT 1 << 3 | |
| #define TILE_ABOVE_RIGHT 1 << 2 | |
| #define TILE_BELOW_LEFT 1 << 1 | |
| #define TILE_BELOW_RIGHT 1 << 0 | |
| #define PLAYER_W 2 | |
| #define PLAYER_H 4 | |
| #define TILES_X 16 | |
| #define TILES_Y 15 | |
| #define PLAYER_TOP (player_position.y) | |
| #define PLAYER_BOTTOM (player_position.y + PLAYER_H) | |
| #define PLAYER_RIGHT (player_position.x + PLAYER_W) | |
| #define PLAYER_LEFT (player_position.x) | |
| #define RANDOM_TYPE_HRNG 0 | |
| #define RANDOM_TYPE_PRNG 1 | |
| #define PASSAGE_COUNT 5 | |
| // Number of times a player can jump sequentially | |
| // including mid-air jumps and the initial ground | |
| // or wall jump | |
| #define MAX_JUMP 3 | |
| uint8_t current_random_source = RANDOM_TYPE_PRNG; | |
| uint32_t current_random_seed = 0x64063701; | |
| // All the art is rainbow fun time, so we don't need | |
| // much data about each tile. | |
| // Rounded corners are also procedural depending upon | |
| // tile proximity. | |
| // The screen allows for 16x12 10x10 tiles, but we | |
| // use an extra 3 vertically: | |
| // +1 - because an offset row means we can see 13 rows total | |
| // +2 - because tile features need an adjacent row to generate from | |
| // IE: when our screen is shifted down 5px you can see 13 | |
| // rows and both the top and bottom visible row are next to the | |
| // additional two invisible rows which govern how corners are rounded. | |
| uint8_t tiles[16 * 15] = { 0 }; | |
| uint32_t current_row = 0; | |
| Timer state_update; | |
| Point tile_offset(0, 0); | |
| struct SaveData { | |
| uint32_t highscore; | |
| } save_data; | |
| Vec2 player_position(80.0f, SCREEN_H - PLAYER_H); | |
| Vec2 player_velocity(0.0f, 0.0f); | |
| Vec2 jump_velocity(0.0f, -2.0f); | |
| uint8_t player_jump_count = 0; | |
| uint32_t player_progress = 0; | |
| bool player_on_floor = false; | |
| enum enum_player_state { | |
| ground = 0, | |
| near_wall_left = 1, | |
| wall_left = 2, | |
| near_wall_right = 3, | |
| wall_right = 4, | |
| air = 5 | |
| }; | |
| enum_player_state player_state = ground; | |
| uint8_t last_passage_width = 0; | |
| float water_level = 0; | |
| // Used for tracking where the engine has linked a finished passage | |
| // back to those still active. | |
| uint16_t linked_passage_mask = 0; | |
| uint8_t passages[PASSAGE_COUNT] = {0}; | |
| // Keep track of game state | |
| enum enum_state { | |
| menu = 0, | |
| play = 1, | |
| dead = 2 | |
| }; | |
| enum_state game_state = enum_state::menu; | |
| using tile_callback = uint8_t (*)(uint8_t tile, uint8_t x, uint8_t y, void *args); | |
| uint32_t prng_lfsr = 0; | |
| const uint16_t prng_tap = 0x74b8; | |
| uint32_t get_random_number() { | |
| switch(current_random_source) { | |
| default: | |
| return 0; | |
| case RANDOM_TYPE_HRNG: | |
| return blit::random(); | |
| case RANDOM_TYPE_PRNG: | |
| uint8_t lsb = prng_lfsr & 1; | |
| prng_lfsr >>= 1; | |
| if (lsb) { | |
| prng_lfsr ^= prng_tap; | |
| } | |
| return prng_lfsr; | |
| } | |
| } | |
| void for_each_tile(tile_callback callback, void *args) { | |
| for (auto y = 0; y < TILES_Y; y++) { | |
| for(auto x = 0; x < TILES_X; x++) { | |
| uint16_t index = (y * TILES_X) + x; | |
| tiles[index] = callback(tiles[index], x, y, args); | |
| } | |
| } | |
| } | |
| uint8_t get_tile_at(uint8_t x, uint8_t y) { | |
| // Get the tile at a given x/y grid coordinate | |
| if (x < 0) return TILE_SOLID; | |
| if (x > 15) return TILE_SOLID; | |
| if (y > TILES_Y) return 0; | |
| if(y < 0) return TILE_SOLID; | |
| uint16_t index = (y * TILES_X) + x; | |
| return tiles[index]; | |
| } | |
| uint8_t get_adjacent_tile_solid_flags(uint8_t x, uint8_t y) { | |
| // TODO: avoid calls to get_tile_at and use offsets to find | |
| // adjacent tiles more efficiently. | |
| uint8_t feature_map = 0; | |
| feature_map |= (get_tile_at(x - 1, y) & TILE_SOLID) ? TILE_LEFT : 0; | |
| feature_map |= (get_tile_at(x + 1, y) & TILE_SOLID) ? TILE_RIGHT : 0; | |
| feature_map |= (get_tile_at(x, y - 1) & TILE_SOLID) ? TILE_ABOVE : 0; | |
| feature_map |= (get_tile_at(x, y + 1) & TILE_SOLID) ? TILE_BELOW : 0; | |
| feature_map |= (get_tile_at(x - 1, y - 1) & TILE_SOLID) ? TILE_ABOVE_LEFT : 0; | |
| feature_map |= (get_tile_at(x + 1, y - 1) & TILE_SOLID) ? TILE_ABOVE_RIGHT : 0; | |
| feature_map |= (get_tile_at(x - 1, y + 1) & TILE_SOLID) ? TILE_BELOW_LEFT : 0; | |
| feature_map |= (get_tile_at(x + 1, y + 1) & TILE_SOLID) ? TILE_BELOW_RIGHT : 0; | |
| return feature_map; | |
| } | |
| uint8_t render_tile(uint8_t tile, uint8_t x, uint8_t y, void *args) { | |
| // Rendering tiles is pretty simple and involves drawing rectangles | |
| // in the right places. | |
| // But a large amount of this function is given over to rounding | |
| // corners depending upon the content of neighbouring tiles. | |
| // This could probably be rewritten to use a lookup table? | |
| Point offset = *(Point *)args; | |
| auto tile_x = (x * TILE_W) + offset.x; | |
| auto tile_y = (y * TILE_H) + offset.y; | |
| uint8_t feature_map = get_adjacent_tile_solid_flags(x, y); | |
| bool round_tl = (feature_map & (TILE_ABOVE_LEFT | TILE_ABOVE | TILE_LEFT)) == 0; | |
| bool round_tr = (feature_map & (TILE_ABOVE_RIGHT | TILE_ABOVE | TILE_RIGHT)) == 0; | |
| bool round_bl = (feature_map & (TILE_BELOW_LEFT | TILE_BELOW | TILE_LEFT)) == 0; | |
| bool round_br = (feature_map & (TILE_BELOW_RIGHT | TILE_BELOW | TILE_RIGHT)) == 0; | |
| Pen color_base = hsv_to_rgba(((120 - tile_y) + 110.0f) / 120.0f, 0.5f, 0.8f); | |
| if(tile & TILE_SOLID) { | |
| // Draw tiles without anti-aliasing to save code bloat | |
| // Uses the rounded corner flags to miss a pixel for a | |
| // basic rounded corner effect. | |
| for(auto py = 0; py < TILE_H; py++){ | |
| for(auto px = 0; px < TILE_W; px++){ | |
| // Skip drawing the pixels for each rounded corner | |
| if(round_tl && px == 0 && py == 0) continue; | |
| if(round_tr && px == TILE_W - 1 && py == 0) continue; | |
| if(round_bl && px == 0 && py == TILE_H - 1) continue; | |
| if(round_br && px == TILE_H - 1 && py == TILE_H - 1) continue; | |
| screen.pen = color_base; | |
| screen.pixel(Point(tile_x + px, tile_y + py)); | |
| } | |
| } | |
| } else { | |
| if(feature_map & TILE_ABOVE) { | |
| // Draw the top left/right rounded inside corners | |
| // for an empty tile. | |
| if (feature_map & TILE_LEFT) { | |
| screen.pen = color_base; | |
| screen.pixel(Point(tile_x, tile_y)); | |
| } | |
| if (feature_map & TILE_RIGHT) { | |
| screen.pen = color_base; | |
| screen.pixel(Point(tile_x + TILE_W - 1, tile_y)); | |
| } | |
| } | |
| if(feature_map & TILE_BELOW) { | |
| // If we have a tile directly to the left and right | |
| // of this one then it's a little pocket we can fill with water! | |
| // TODO: Make this not look rubbish | |
| if(feature_map & TILE_LEFT && feature_map & TILE_RIGHT) { | |
| screen.pen = Pen(200, 200, 255, 128); | |
| screen.rectangle(Rect(tile_x, tile_y + (TILE_H / 2), TILE_W, TILE_H / 2)); | |
| } | |
| // Draw the bottom left/right rounded inside corners | |
| // for an empty tile. | |
| if(feature_map & TILE_LEFT) { | |
| screen.pen = color_base; | |
| screen.pixel(Point(tile_x, tile_y + TILE_H - 1)); | |
| } | |
| if(feature_map & TILE_RIGHT) { | |
| screen.pen = color_base; | |
| screen.pixel(Point(tile_x + TILE_W - 1, tile_y + TILE_H - 1)); | |
| } | |
| } | |
| } | |
| return tile; | |
| } | |
| uint16_t generate_new_row_mask() { | |
| uint16_t new_row_mask = 0x0000; | |
| uint8_t passage_width = floorf(((sinf(current_row / 10.0f) + 1.0f) / 2.0f) * PASSAGE_COUNT); | |
| // Cut our consistent winding passage through the level | |
| // by tracking the x coord of our passage we can ensure | |
| // that it's always navigable without having to reject | |
| // procedurally generated segments | |
| for(auto p = 0; p < PASSAGE_COUNT; p++){ | |
| if(p > passage_width) { | |
| continue; | |
| } | |
| // Controls how far a passage can snake left/right | |
| uint8_t turning_size = get_random_number() % 7; | |
| new_row_mask |= (0x8000 >> passages[p]); | |
| // At every new generation we choose to branch a passage | |
| // either left or right, or let it continue upwards. | |
| switch(get_random_number() % 3){ | |
| case 0: // Passage goes right | |
| while(turning_size--){ | |
| if(passages[p] < TILES_X - 2){ | |
| passages[p] += 1; | |
| } | |
| new_row_mask |= (0x8000 >> passages[p]); | |
| } | |
| break; | |
| case 1: // Passage goes left | |
| while(turning_size--){ | |
| if(passages[p] > 1){ | |
| passages[p] -= 1; | |
| } | |
| new_row_mask |= (0x8000 >> passages[p]); | |
| } | |
| break; | |
| } | |
| } | |
| // Whenever we have a narrowing of our passage we must check | |
| // for orphaned passages and link them back to the ones still | |
| // available, to avoid the player going up a tunnel that ends | |
| // abruptly :( | |
| // This routine picks a random passage from the ones remaining | |
| // and routes every orphaned passage to it. | |
| if(passage_width < last_passage_width) { | |
| uint8_t target_passage = get_random_number() % (passage_width + 1); | |
| uint8_t target_p_x = passages[target_passage]; | |
| for(auto i = passage_width; i < last_passage_width + 1; i++){ | |
| new_row_mask |= (0x8000 >> passages[i]); | |
| int8_t direction = (passages[i] < target_p_x) ? 1 : -1; | |
| while(passages[i] != target_p_x) { | |
| passages[i] += direction; | |
| new_row_mask |= (0x8000 >> passages[i]); | |
| } | |
| } | |
| } | |
| last_passage_width = passage_width; | |
| current_row++; | |
| return ~new_row_mask; | |
| } | |
| void update_tiles() { | |
| // Shift all of our tile rows down by 1 starting | |
| // with the second-to-bottom tile which replaces | |
| // the bottom-most tile. | |
| for(auto y = TILES_Y - 2; y >= 0; y--){ | |
| for(auto x = 0; x < TILES_X; x++){ | |
| uint16_t tgt = ((y + 1) * TILES_X) + x; | |
| uint16_t src = (y * TILES_X) + x; | |
| tiles[tgt] = tiles[src]; | |
| } | |
| } | |
| uint16_t row_mask = generate_new_row_mask(); | |
| // Replace the very top row of tiles with our newly | |
| // generated row mask. | |
| for(auto x = 0; x < TILES_X; x++) { | |
| if(row_mask & (1 << x)) { | |
| tiles[x] = TILE_SOLID; | |
| } | |
| else { | |
| tiles[x] = 0; | |
| } | |
| } | |
| } | |
| void update_state(Timer &timer) { | |
| if (game_state == enum_state::menu) { | |
| tile_offset.y += 1; | |
| } | |
| if (game_state == enum_state::play && (player_position.y < 70)) { | |
| tile_offset.y += 1; | |
| if(water_level > 10){ | |
| water_level -= 1; | |
| } | |
| player_position.y += 1; | |
| player_progress += 1; | |
| } | |
| if(tile_offset.y >= 0) { | |
| tile_offset.y = -10; | |
| update_tiles(); | |
| } | |
| } | |
| bool place_player() { | |
| // Try to find a suitable place to drop the player | |
| // where they will be standing on solid ground | |
| for(auto y = 10; y > 0; y--){ | |
| for(auto x = 0; x < TILES_X; x++){ | |
| uint16_t here = get_tile_at(x, y); | |
| uint16_t below = get_tile_at(x, y + 1); | |
| if(below & TILE_SOLID && (here & TILE_SOLID) == 0) { | |
| player_position.x = (x * TILE_W) + 4; | |
| player_position.y = (y * TILE_H) + tile_offset.y; | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| void new_level() { | |
| prng_lfsr = current_random_seed; | |
| player_position.x = 80.0f; | |
| player_position.y = float(SCREEN_H - PLAYER_H); | |
| player_velocity.x = 0.0f; | |
| player_velocity.y = 0.0f; | |
| player_progress = 0; | |
| tile_offset.y = -10; | |
| tile_offset.x = 0; | |
| water_level = 0; | |
| last_passage_width = 0; | |
| current_row = 0; | |
| for(auto x = 0; x < 5; x++) { | |
| passages[x] = (get_random_number() % 14) + 1; | |
| } | |
| // Use update_tiles to create the initial game state | |
| // instead of having a separate loop that breaks in weird ways | |
| for(auto y = 0; y < TILES_Y; y++) { | |
| update_tiles(); | |
| } | |
| bool placed_successfully = place_player(); | |
| } | |
| void new_game() { | |
| new_level(); | |
| game_state = enum_state::play; | |
| } | |
| void init() { | |
| set_screen_mode(lores); | |
| // Attempt to load the first save slot. | |
| if (read_save(save_data)) { | |
| // Loaded successfully! | |
| } | |
| else { | |
| // No save file or it failed to load. | |
| save_data.highscore = 0; | |
| } | |
| #ifdef __AUDIO__ | |
| channels[0].waveforms = Waveform::NOISE; | |
| channels[0].frequency = 4200; | |
| channels[0].attack_ms = 1; | |
| channels[0].decay_ms = 1; | |
| channels[0].sustain = 0xffff; | |
| channels[0].release_ms = 1; | |
| channels[0].trigger_attack(); | |
| channels[1].waveforms = Waveform::SQUARE; | |
| channels[1].frequency = 0; | |
| channels[1].attack_ms = 30; | |
| channels[1].decay_ms = 100; | |
| channels[1].sustain = 0; | |
| channels[1].release_ms = 0; | |
| #endif | |
| state_update.init(update_state, 10, -1); | |
| state_update.start(); | |
| new_level(); | |
| } | |
| uint8_t collide_player_lr(uint8_t tile, uint8_t x, uint8_t y, void *args) { | |
| Point offset = *(Point *)args; | |
| auto tile_x = (x * TILE_W) + offset.x; | |
| auto tile_y = (y * TILE_H) + offset.y; | |
| auto tile_top = tile_y; | |
| auto tile_bottom = tile_y + TILE_H; | |
| auto tile_left = tile_x; | |
| auto tile_right = tile_x + TILE_W; | |
| if(tile & TILE_SOLID) { | |
| uint8_t near_wall_distance = 2; | |
| if(((PLAYER_BOTTOM > tile_top) && (PLAYER_BOTTOM < tile_bottom)) | |
| || ((PLAYER_TOP > tile_top) && PLAYER_TOP < tile_bottom)){ | |
| // Collide the left-hand side of the tile right of player | |
| if(PLAYER_RIGHT > tile_left && (PLAYER_LEFT < tile_left)){ | |
| // screen.pen = Pen(255, 255, 255, 100); | |
| // screen.rectangle(rect(tile_x, tile_y, TILE_W, TILE_H)); | |
| player_position.x = float(tile_left - PLAYER_W); | |
| player_velocity.x = 0.0f; | |
| player_state = wall_right; | |
| } | |
| else if(((PLAYER_RIGHT + near_wall_distance) > tile_left) && (PLAYER_LEFT < tile_left)) { | |
| player_state = near_wall_right; | |
| } | |
| // Collide the right-hand side of the tile left of player | |
| if((PLAYER_LEFT < tile_right) && (PLAYER_RIGHT > tile_right)) { | |
| // screen.pen = Pen(255, 255, 255, 100); | |
| // screen.rectangle(rect(tile_x, tile_y, TILE_W, TILE_H)); | |
| player_position.x = float(tile_right); | |
| player_velocity.x = 0.0f; | |
| player_state = wall_left; | |
| } | |
| else if(((PLAYER_LEFT - near_wall_distance) < tile_right) && (PLAYER_RIGHT > tile_right)) { | |
| player_state = near_wall_left; | |
| } | |
| } | |
| } | |
| return tile; | |
| } | |
| uint8_t collide_player_ud(uint8_t tile, uint8_t x, uint8_t y, void *args) { | |
| Point offset = *(Point *)args; | |
| auto tile_x = (x * TILE_W) + offset.x; | |
| auto tile_y = (y * TILE_H) + offset.y; | |
| auto tile_top = tile_y; | |
| auto tile_bottom = tile_y + TILE_H; | |
| auto tile_left = tile_x; | |
| auto tile_right = tile_x + TILE_W; | |
| if(tile & TILE_SOLID) { | |
| if((PLAYER_RIGHT > tile_left) && (PLAYER_LEFT < tile_right)){ | |
| // Collide the bottom side of the tile above player | |
| if(PLAYER_TOP < tile_bottom && PLAYER_BOTTOM > tile_bottom){ | |
| player_position.y = float(tile_bottom); | |
| player_velocity.y = 0; | |
| } | |
| // Collide the top side of the tile below player | |
| if((PLAYER_BOTTOM > tile_top) && (PLAYER_TOP < tile_top)){ | |
| player_position.y = float(tile_top - PLAYER_H); | |
| player_velocity.y = 0; | |
| player_jump_count = MAX_JUMP; | |
| player_state = ground; | |
| } | |
| } | |
| } | |
| return tile; | |
| } | |
| void update(uint32_t time_ms) { | |
| int32_t water_dist = player_position.y - (SCREEN_H - water_level); | |
| if (water_dist < 0) { | |
| water_dist = 0; | |
| } | |
| #ifdef __AUDIO__ | |
| channels[0].volume = 4000 + (sinf(float(time_ms) / 1000.0f) * 3000); | |
| #endif | |
| if (game_state == enum_state::menu) { | |
| if(buttons.pressed & Button::B) { | |
| new_game(); | |
| } | |
| else if(buttons.pressed & Button::DPAD_UP) { | |
| current_random_source = RANDOM_TYPE_PRNG; | |
| new_level(); | |
| } | |
| else if(buttons.pressed & Button::DPAD_DOWN) { | |
| current_random_source = RANDOM_TYPE_HRNG; | |
| new_level(); | |
| } | |
| else if(buttons.pressed & Button::DPAD_RIGHT) { | |
| if(current_random_source == RANDOM_TYPE_PRNG) { | |
| current_random_seed++; | |
| new_level(); | |
| } | |
| } | |
| else if(buttons.pressed & Button::DPAD_LEFT) { | |
| if(current_random_source == RANDOM_TYPE_PRNG) { | |
| current_random_seed--; | |
| new_level(); | |
| } | |
| } | |
| return; | |
| } | |
| if(game_state == enum_state::dead){ | |
| if(buttons.pressed & Button::B) { | |
| game_state = enum_state::menu; | |
| } | |
| return; | |
| } | |
| if(game_state == enum_state::play){ | |
| static enum_player_state last_wall_jump = enum_player_state::ground; | |
| #ifdef __AUDIO__ | |
| static float jump_sweep = 0.0; | |
| #endif | |
| Vec2 movement(0, 0); | |
| water_level += 0.05f; | |
| jump_velocity.x = 0.0f; | |
| // Apply Gravity | |
| player_velocity.y += 0.098f; | |
| if(buttons & Button::DPAD_LEFT) { | |
| player_velocity.x -= 0.1f; | |
| movement.x = -1; | |
| if(buttons & Button::DPAD_UP) { | |
| if(player_state == wall_left | |
| || player_state == near_wall_left) { | |
| player_velocity.y -= 0.12f; | |
| } | |
| movement.y = -1; | |
| } | |
| } | |
| if(buttons & Button::DPAD_RIGHT) { | |
| player_velocity.x += 0.1f; | |
| movement.x = 1; | |
| if(buttons & Button::DPAD_UP) { | |
| if(player_state == wall_right | |
| || player_state == near_wall_right) { | |
| player_velocity.y -= 0.12f; | |
| } | |
| movement.y = -1; | |
| } | |
| } | |
| if(buttons & Button::DPAD_DOWN) { | |
| movement.y = 1; | |
| } | |
| if(player_jump_count){ | |
| if(buttons.pressed & Button::A) { | |
| if(player_state == wall_left | |
| || player_state == wall_right | |
| || player_state == near_wall_left | |
| || player_state == near_wall_right) { | |
| enum_player_state wall_jump_state = (player_state == wall_left || player_state == near_wall_left) ? wall_left : wall_right; | |
| jump_velocity.x = (wall_jump_state == wall_left) ? 1.2f : -1.2f; | |
| if(last_wall_jump != wall_jump_state) { | |
| player_jump_count = MAX_JUMP; | |
| } | |
| last_wall_jump = wall_jump_state; | |
| } | |
| player_velocity = jump_velocity; | |
| player_state = air; | |
| player_jump_count--; | |
| #ifdef __AUDIO__ | |
| channels[1].trigger_attack(); | |
| jump_sweep = 1.0f; | |
| #endif | |
| } | |
| } | |
| #ifdef __AUDIO__ | |
| if(jump_sweep > 0) { | |
| channels[1].frequency = 880 - (880.0f * jump_sweep); | |
| jump_sweep -= 0.05f; | |
| } | |
| #endif | |
| switch(player_state) { | |
| case wall_left: | |
| case wall_right: | |
| case near_wall_left: | |
| case near_wall_right: | |
| if ((buttons & Button::DPAD_LEFT) && (player_state == wall_left || player_state == near_wall_left)){ | |
| player_velocity.y *= 0.5f; | |
| break; | |
| } | |
| if ((buttons & Button::DPAD_RIGHT) && (player_state == wall_right || player_state == near_wall_right)){ | |
| player_velocity.y *= 0.5f; | |
| break; | |
| } | |
| // Fall through to air friction | |
| case air: | |
| // Air friction | |
| player_velocity.y *= 0.98f; | |
| player_velocity.x *= 0.91f; | |
| break; | |
| case ground: | |
| // Ground friction | |
| player_velocity *= 0.8f; | |
| break; | |
| } | |
| // Default state is in the air unless we collide | |
| // with a wall or the ground | |
| player_state = enum_player_state::air; | |
| player_position.x += player_velocity.x; | |
| // Useful for debug since you can position the player directly | |
| //player_position.x += movement.x; | |
| if(player_position.x <= 0){ | |
| player_position.x = 0; | |
| player_velocity.x = 0; | |
| player_state = wall_left; | |
| } | |
| else if(player_position.x + PLAYER_W >= SCREEN_W) { | |
| player_position.x = float(SCREEN_W - PLAYER_W); | |
| player_velocity.x = 0; | |
| player_state = wall_right; | |
| } | |
| for_each_tile(collide_player_lr, (void *)&tile_offset); | |
| player_position.y += player_velocity.y; | |
| // Useful for debug since you can position the player directly | |
| //player_position.y += movement.y; | |
| if(player_position.y + PLAYER_H > SCREEN_H || player_position.y > SCREEN_H - water_level) { | |
| game_state = enum_state::dead; | |
| if (player_progress > save_data.highscore) { | |
| save_data.highscore = player_progress; | |
| write_save(save_data); // save save_data | |
| } | |
| } | |
| for_each_tile(collide_player_ud, (void *)&tile_offset); | |
| } | |
| } | |
| void render_summary() { | |
| std::string text = "Game mode: "; | |
| if(current_random_source == RANDOM_TYPE_PRNG) { | |
| text.append("Competitive"); | |
| } else { | |
| text.append("Random Practice"); | |
| } | |
| screen.text(text, minimal_font, Point(10, (SCREEN_H / 2) + 20)); | |
| if(current_random_source == RANDOM_TYPE_PRNG) { | |
| char buf[9]; | |
| snprintf(buf, 9, "%08" PRIX32, current_random_seed); | |
| text = "Level seed: "; | |
| text.append(buf); | |
| screen.text(text, minimal_font, Point(10, (SCREEN_H / 2) + 30)); | |
| } | |
| text = "Press B"; | |
| screen.text(text, minimal_font, Point(10, (SCREEN_H / 2) + 50)); | |
| text = "Highscore: "; | |
| text.append(std::to_string(save_data.highscore)); | |
| text.append("cm"); | |
| screen.text(text, minimal_font, Point(10, (SCREEN_H / 2) + 40)); | |
| } | |
| void render(uint32_t time_ms) { | |
| screen.pen = Pen(0, 0, 0); | |
| screen.clear(); | |
| std::string text = "RAINBOW ASCENT"; | |
| if (game_state == enum_state::menu) { | |
| for_each_tile(render_tile, (void *)&tile_offset); | |
| // Draw the player | |
| screen.pen = Pen(255, 255, 255); | |
| screen.rectangle(Rect(player_position.x, player_position.y, PLAYER_W, PLAYER_H)); | |
| screen.pen = Pen(255, 50, 50); | |
| screen.rectangle(Rect(player_position.x, player_position.y, PLAYER_W, 1)); | |
| screen.pen = Pen(0, 0, 0, 200); | |
| screen.clear(); | |
| uint8_t x = 10; | |
| for(auto c : text) { | |
| uint8_t y = 20 + (5.0f * sinf((time_ms / 250.0f) + (float(x) / text.length() * 2.0f * pi))); | |
| Pen color_letter = hsv_to_rgba((x - 10) / 140.0f, 0.5f, 0.8f); | |
| screen.pen = color_letter; | |
| char buf[2]; | |
| buf[0] = c; | |
| buf[1] = '\0'; | |
| screen.text(buf, minimal_font, Point(x, y)); | |
| x += 10; | |
| } | |
| screen.pen = Pen(255, 255, 255, 150); | |
| render_summary(); | |
| return; | |
| } | |
| Pen color_water = hsv_to_rgba(((120 - 120) + 110.0f) / 120.0f, 1.0f, 0.5f); | |
| color_water.a = 255; | |
| if(water_level > 0){ | |
| screen.pen = color_water; | |
| screen.rectangle(Rect(0, SCREEN_H - water_level, SCREEN_W, water_level + 1)); | |
| for(auto x = 0; x < SCREEN_W; x++){ | |
| uint16_t offset = x + uint16_t(sinf(time_ms / 500.0f) * 5.0f); | |
| if((offset % 5) > 0){ | |
| screen.pixel(Point(x, SCREEN_H - water_level - 1)); | |
| } | |
| if(((offset + 2) % 5) == 0){ | |
| screen.pixel(Point(x, SCREEN_H - water_level - 2)); | |
| } | |
| if(((offset + 3) % 5) == 0){ | |
| screen.pixel(Point(x, SCREEN_H - water_level - 2)); | |
| } | |
| } | |
| } | |
| for_each_tile(render_tile, (void *)&tile_offset); | |
| // Draw the player | |
| screen.pen = Pen(255, 255, 255); | |
| screen.rectangle(Rect(player_position.x, player_position.y, PLAYER_W, PLAYER_H)); | |
| screen.pen = Pen(255, 50, 50); | |
| screen.rectangle(Rect(player_position.x, player_position.y, PLAYER_W, 1)); | |
| /* | |
| // Show number of active passages | |
| p = std::to_string(passage_width + 1); | |
| p.append(" passages"); | |
| screen.text(p, minimal_font, point(2, 10)); | |
| */ | |
| if(water_level > 0){ | |
| color_water.a = 100; | |
| screen.pen = color_water; | |
| screen.rectangle(Rect(0, SCREEN_H - water_level, SCREEN_W, water_level + 1)); | |
| for(auto x = 0; x < SCREEN_W; x++){ | |
| uint16_t offset = x + uint16_t(sinf(time_ms / 500.0f) * 5.0f); | |
| if((offset % 5) > 0){ | |
| screen.pixel(Point(x, SCREEN_H - water_level - 1)); | |
| } | |
| if(((offset + 2) % 5) == 0){ | |
| screen.pixel(Point(x, SCREEN_H - water_level - 2)); | |
| } | |
| if(((offset + 3) % 5) == 0){ | |
| screen.pixel(Point(x, SCREEN_H - water_level - 2)); | |
| } | |
| } | |
| } | |
| if(game_state == enum_state::dead) { | |
| screen.pen = Pen(128, 0, 0, 200); | |
| screen.rectangle(Rect(0, 0, SCREEN_W, SCREEN_H)); | |
| screen.pen = Pen(255, 0, 0, 255); | |
| screen.text("YOU DIED!", minimal_font, Point((SCREEN_W / 2) - 20, (SCREEN_H / 2) - 4)); | |
| // Round stats | |
| screen.pen = Pen(255, 255, 255); | |
| std::string text = ""; | |
| text = "You climbed: "; | |
| text.append(std::to_string(player_progress)); | |
| text.append("cm"); | |
| screen.text(text, minimal_font, Point(10, (SCREEN_H / 2) + 10)); | |
| render_summary(); | |
| } | |
| else | |
| { | |
| // Draw the HUD | |
| screen.pen = Pen(255, 255, 255); | |
| text = std::to_string(player_progress); | |
| text.append("cm"); | |
| screen.text(text, minimal_font, Point(2, 2)); | |
| /* | |
| // State debug info | |
| text = "Jumps: "; | |
| text.append(std::to_string(player_jump_count)); | |
| screen.text(text, minimal_font, point(2, 12)); | |
| text = "State: "; | |
| switch(player_state){ | |
| case enum_player_state::ground: | |
| text.append("GROUND"); | |
| break; | |
| case enum_player_state::air: | |
| text.append("AIR"); | |
| break; | |
| case enum_player_state::near_wall_left: | |
| text.append("NEAR L"); | |
| break; | |
| case enum_player_state::wall_left: | |
| text.append("WALL L"); | |
| break; | |
| case enum_player_state::near_wall_right: | |
| text.append("NEAR R"); | |
| break; | |
| case enum_player_state::wall_right: | |
| text.append("WALL R"); | |
| break; | |
| } | |
| screen.text(text, minimal_font, point(2, 22)); | |
| */ | |
| } | |
| } |