#include #include #include #include "player.h" #include "world.h" #include "vec_math.h" // Create a player Player* player_create(float x, float y, float z) { return player_create_with_uid(x, y, z, 0x00000001, "Player"); } // Create a player with specific UID and nickname Player* player_create_with_uid(float x, float y, float z, uint32_t uid, const char* nickname) { Player* player = (Player*)malloc(sizeof(Player)); player->uid = uid; strncpy(player->nickname, nickname, sizeof(player->nickname) - 1); player->nickname[sizeof(player->nickname) - 1] = '\0'; player->position = (Vector3){ x, y, z }; player->prev_position = player->position; player->velocity = (Vector3){ 0, 0, 0 }; player->on_ground = false; player->jump_used = false; player->selected_block = BLOCK_STONE; // Default to stone player->is_flying = false; player->no_clip = false; player->space_press_time = 1.0f; // Initialize to high value so first press isn't a double-tap inventory_init(player); // Initialize inventory return player; } // Free player memory void player_free(Player* player) { if (player) { free(player); } } // Handle player movement input void player_move_input(Player* player, Vector3 forward, Vector3 right, bool flight_enabled) { // Extract horizontal (XZ) component of right vector and normalize it. // Then derive a stable horizontal forward from world up and right to avoid issues when // the camera is looking straight up/down (forward XZ component near zero). Vector3 right_horiz = (Vector3){ right.x, 0, right.z }; float right_len = sqrtf(right_horiz.x * right_horiz.x + right_horiz.y * right_horiz.y + right_horiz.z * right_horiz.z); if (right_len < 1e-6f) { // Fallback: if right is degenerate, use world X axis right_horiz = (Vector3){ 1.0f, 0.0f, 0.0f }; } else { right_horiz.x /= right_len; right_horiz.y /= right_len; right_horiz.z /= right_len; } // Derive forward_horiz as cross(world_up, right_horiz) which gives a horizontal forward // consistent with yaw even when the camera is looking vertically. Vector3 forward_horiz = vec3_cross((Vector3){0, 1, 0}, right_horiz); forward_horiz = vec3_normalize(forward_horiz); // Shifting (sneak) and sprinting support player->shifting = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); bool sprinting = IsKeyDown(KEY_LEFT_CONTROL); float move_speed = PLAYER_SPEED; if (player->shifting && !player->is_flying) { move_speed *= 0.5f; } else if (sprinting && !player->is_flying) { move_speed *= 1.5f; } // Handle flight toggling with double-tap space (when FLIGHT_ENABLED is true) bool space_pressed_now = IsKeyPressed(KEY_SPACE); if (space_pressed_now) { if (flight_enabled && player->space_press_time < DOUBLE_TAP_THRESHOLD) { // Double-tap detected - toggle flying player->is_flying = !player->is_flying; if (player->is_flying) { // Entering flight mode - stop falling velocity player->velocity.y = 0; } } player->space_press_time = 0.0f; } Vector3 move = { 0, 0, 0 }; if (IsKeyDown(KEY_W)) { move = vec3_add(move, vec3_scale(forward_horiz, move_speed)); } if (IsKeyDown(KEY_S)) { move = vec3_add(move, vec3_scale(forward_horiz, -move_speed)); } if (IsKeyDown(KEY_D)) { move = vec3_add(move, vec3_scale(right_horiz, move_speed)); } if (IsKeyDown(KEY_A)) { move = vec3_add(move, vec3_scale(right_horiz, -move_speed)); } // Normalize movement to prevent diagonal speedup float move_len = sqrtf(move.x * move.x + move.z * move.z); if (move_len > move_speed) { float scale = move_speed / move_len; move.x *= scale; move.z *= scale; } if (player->is_flying) { // Flying mode: space goes up, shift goes down, WASD moves horizontally // Apply sprint boost when flying (ctrl key) float fly_speed_mult = 1.0f; if (sprinting) { fly_speed_mult = 1.5f; } // Scale horizontal movement by sprint multiplier player->velocity.x = move.x * fly_speed_mult; player->velocity.z = move.z * fly_speed_mult; // Vertical movement in flight player->velocity.y = 0; if (IsKeyDown(KEY_SPACE)) { player->velocity.y = FLY_SPEED * fly_speed_mult; } if (IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)) { player->velocity.y = -FLY_SPEED * fly_speed_mult; } } else { // Normal mode: jumping with space if (IsKeyDown(KEY_SPACE)) { if (player->on_ground && !player->jump_used) { player->velocity.y = JUMP_FORCE; player->on_ground = false; player->jump_used = true; } } else { player->jump_used = false; } player->velocity.x = move.x; player->velocity.z = move.z; } } // Check collision for a rectangular prism (box) - AABB collision bool world_check_collision_box(World* world, Vector3 center_pos, float width, float height, float depth) { float half_width = width / 2.0f; float half_height = height / 2.0f; float half_depth = depth / 2.0f; float box_min_x = center_pos.x - half_width; float box_max_x = center_pos.x + half_width; float box_min_y = center_pos.y - half_height; float box_max_y = center_pos.y + half_height; float box_min_z = center_pos.z - half_depth; float box_max_z = center_pos.z + half_depth; int min_x = (int)floorf(box_min_x - 1.0f); int max_x = (int)ceilf(box_max_x + 1.0f); int min_y = (int)floorf(box_min_y - 1.0f); int max_y = (int)ceilf(box_max_y + 1.0f); int min_z = (int)floorf(box_min_z - 1.0f); int max_z = (int)ceilf(box_max_z + 1.0f); for (int y = min_y; y <= max_y; y++) { for (int z = min_z; z <= max_z; z++) { for (int x = min_x; x <= max_x; x++) { BlockType block = world_get_block(world, x, y, z); if (block != BLOCK_AIR) { float block_min_x = (float)x; float block_max_x = (float)(x + 1); float block_min_y = (float)y; float block_max_y = (float)(y + 1); float block_min_z = (float)z; float block_max_z = (float)(z + 1); // AABB to AABB collision check if (box_max_x > block_min_x && box_min_x < block_max_x && box_max_y > block_min_y && box_min_y < block_max_y && box_max_z > block_min_z && box_min_z < block_max_z) { return true; } } } } } return false; } // Update player physics void player_update(Player* player, World* world, float dt, bool flight_enabled) { // Update space press timing for double-tap detection if (player->space_press_time < DOUBLE_TAP_THRESHOLD) { player->space_press_time += dt; } // Store previous position for speedometer player->prev_position = player->position; // Only apply gravity if not flying if (!player->is_flying) { player->velocity.y -= GRAVITY * dt; } if (player->velocity.y < -50.0f) { player->velocity.y = -50.0f; } Vector3 new_pos = (Vector3){ player->position.x + player->velocity.x * dt, player->position.y + player->velocity.y * dt, player->position.z + player->velocity.z * dt }; // When no-clipping, bypass collision detection if (player->no_clip) { player->position = new_pos; player->on_ground = false; return; } // Edge safety: if shifting, prevent walking off ledges if (player->shifting && player->on_ground) { // Predict next foot position (center of feet, 0.1 below player) Vector3 foot_pos = new_pos; foot_pos.y -= PLAYER_HEIGHT - 0.1f; // Check a grid under the player's feet (collision box) float half = 0.3f; // half width of collision box float step = 0.08f; // grid step (smaller = smoother) bool any_supported = false; for (float dx = -half; dx <= half; dx += step) { for (float dz = -half; dz <= half; dz += step) { int bx = (int)floorf(foot_pos.x + dx); int by = (int)floorf(foot_pos.y - 0.05f); int bz = (int)floorf(foot_pos.z + dz); if (world_get_block(world, bx, by, bz) != BLOCK_AIR) { any_supported = true; break; } } if (any_supported) break; } if (!any_supported) { // Don't allow movement if the entire area under the box is unsupported new_pos.x = player->position.x; new_pos.z = player->position.z; } } // Use box collision (0.6 blocks wide and deep) if (!world_check_collision_box(world, new_pos, 0.6f, PLAYER_HEIGHT, 0.6f)) { player->position = new_pos; player->on_ground = false; } else { Vector3 slide_pos = player->position; // Slide X Vector3 test_x = (Vector3){ player->position.x + player->velocity.x * dt, player->position.y, player->position.z }; bool allow_x = !world_check_collision_box(world, test_x, 0.6f, PLAYER_HEIGHT, 0.6f); if (allow_x && player->shifting && player->on_ground) { // Edge safety for X: only allow if player would still be supported after moving Vector3 below_test = (Vector3){ test_x.x, test_x.y - 0.1f, test_x.z }; if (!world_check_collision_box(world, below_test, 0.6f, PLAYER_HEIGHT, 0.6f)) { allow_x = false; // Would fall, don't allow } } if (allow_x) { slide_pos.x = test_x.x; } // Slide Y Vector3 test_y = (Vector3){ slide_pos.x, player->position.y + player->velocity.y * dt, player->position.z }; if (!world_check_collision_box(world, test_y, 0.6f, PLAYER_HEIGHT, 0.6f)) { slide_pos.y = test_y.y; } else { if (player->velocity.y < 0) { player->on_ground = true; player->jump_used = false; } player->velocity.y = 0; } // Slide Z Vector3 test_z = (Vector3){ slide_pos.x, slide_pos.y, player->position.z + player->velocity.z * dt }; bool allow_z = !world_check_collision_box(world, test_z, 0.6f, PLAYER_HEIGHT, 0.6f); if (allow_z && player->shifting && player->on_ground) { // Edge safety for Z: only allow if player would still be supported after moving Vector3 below_test = (Vector3){ test_z.x, test_z.y - 0.1f, test_z.z }; if (!world_check_collision_box(world, below_test, 0.6f, PLAYER_HEIGHT, 0.6f)) { allow_z = false; // Would fall, don't allow } } if (allow_z) { slide_pos.z = test_z.z; } player->position = slide_pos; Vector3 below = (Vector3){ player->position.x, player->position.y - 0.1f, player->position.z }; if (world_check_collision_box(world, below, 0.6f, PLAYER_HEIGHT, 0.6f)) { player->on_ground = true; player->velocity.y = 0; player->jump_used = false; } } } // Initialize player inventory void inventory_init(Player* player) { // Initialize hotbar for (int i = 0; i < INVENTORY_SIZE; i++) { player->inventory[i].type = BLOCK_AIR; player->inventory[i].count = 0; } // Initialize big inventory for (int i = 0; i < BIG_INVENTORY_SIZE; i++) { player->big_inventory[i].type = BLOCK_AIR; player->big_inventory[i].count = 0; } player->selected_slot = 0; player->inventory_open = false; player->holding_item = false; player->held_slot.type = BLOCK_AIR; player->held_slot.count = 0; } // Add a block to inventory // Returns true if successful, false if inventory is full bool inventory_add_block(Player* player, BlockType block_type) { if (block_type == BLOCK_AIR || block_type == BLOCK_BEDROCK) { return false; // Can't collect air or bedrock } // First, try to add to existing stack in hotbar for (int i = 0; i < INVENTORY_SIZE; i++) { if (player->inventory[i].type == block_type && player->inventory[i].count < INVENTORY_MAX_STACK) { player->inventory[i].count++; return true; } } // Second, try to add to existing stack in big inventory for (int i = 0; i < BIG_INVENTORY_SIZE; i++) { if (player->big_inventory[i].type == block_type && player->big_inventory[i].count < INVENTORY_MAX_STACK) { player->big_inventory[i].count++; return true; } } // Find empty slot in hotbar first for (int i = 0; i < INVENTORY_SIZE; i++) { if (player->inventory[i].type == BLOCK_AIR) { player->inventory[i].type = block_type; player->inventory[i].count = 1; return true; } } // Then find empty slot in big inventory for (int i = 0; i < BIG_INVENTORY_SIZE; i++) { if (player->big_inventory[i].type == BLOCK_AIR) { player->big_inventory[i].type = block_type; player->big_inventory[i].count = 1; return true; } } return false; // Inventory full } // Remove a block from inventory (for placing) // Returns true if successful, false if block not available bool inventory_remove_block(Player* player, BlockType block_type) { // First try hotbar for (int i = 0; i < INVENTORY_SIZE; i++) { if (player->inventory[i].type == block_type && player->inventory[i].count > 0) { player->inventory[i].count--; if (player->inventory[i].count <= 0) { player->inventory[i].type = BLOCK_AIR; } return true; } } // Then try big inventory for (int i = 0; i < BIG_INVENTORY_SIZE; i++) { if (player->big_inventory[i].type == block_type && player->big_inventory[i].count > 0) { player->big_inventory[i].count--; if (player->big_inventory[i].count <= 0) { player->big_inventory[i].type = BLOCK_AIR; } return true; } } return false; } // Get count of a specific block type in inventory int inventory_get_count(Player* player, BlockType block_type) { int total = 0; for (int i = 0; i < INVENTORY_SIZE; i++) { if (player->inventory[i].type == block_type) { total += player->inventory[i].count; } } return total; } // Get the currently selected block type based on selected slot // Only returns block if selected slot has items - otherwise returns BLOCK_AIR (no placement) BlockType inventory_get_selected_block(Player* player) { InventorySlot* slot = &player->inventory[player->selected_slot]; if (slot->count > 0) { return slot->type; } // Selected slot is empty - don't place anything return BLOCK_AIR; } // Toggle big inventory open/closed void inventory_toggle_big(Player* player) { player->inventory_open = !player->inventory_open; } // Check if big inventory is open bool inventory_is_big_open(Player* player) { return player->inventory_open; } // Helper: compute total free capacity for a block type across hotbar+big inventory static int inventory_total_free_capacity(Player* player, BlockType block_type) { int total = 0; // existing stacks of same type for (int i = 0; i < INVENTORY_SIZE; i++) { if (player->inventory[i].type == block_type) { total += (INVENTORY_MAX_STACK - player->inventory[i].count); } } for (int i = 0; i < BIG_INVENTORY_SIZE; i++) { if (player->big_inventory[i].type == block_type) { total += (INVENTORY_MAX_STACK - player->big_inventory[i].count); } } // empty slots for (int i = 0; i < INVENTORY_SIZE; i++) { if (player->inventory[i].type == BLOCK_AIR) total += INVENTORY_MAX_STACK; } for (int i = 0; i < BIG_INVENTORY_SIZE; i++) { if (player->big_inventory[i].type == BLOCK_AIR) total += INVENTORY_MAX_STACK; } return total; } // Give items to player: returns true on success bool inventory_give(Player* player, BlockType block_type, int count) { if (count <= 0) return false; if (block_type == BLOCK_AIR || block_type == BLOCK_BEDROCK) return false; int capacity = inventory_total_free_capacity(player, block_type); if (capacity < count) return false; // not enough room int remaining = count; // Preferred: selected hotbar slot int pref = player->selected_slot; // If same type, fill into it if (player->inventory[pref].type == block_type) { int can = INVENTORY_MAX_STACK - player->inventory[pref].count; int take = (remaining <= can) ? remaining : can; player->inventory[pref].count += take; remaining -= take; } else if (player->inventory[pref].type == BLOCK_AIR) { int take = (remaining <= INVENTORY_MAX_STACK) ? remaining : INVENTORY_MAX_STACK; player->inventory[pref].type = block_type; player->inventory[pref].count = take; remaining -= take; } // Next: try to fill other existing stacks of same type in hotbar for (int i = 0; remaining > 0 && i < INVENTORY_SIZE; i++) { if (i == pref) continue; if (player->inventory[i].type == block_type && player->inventory[i].count < INVENTORY_MAX_STACK) { int can = INVENTORY_MAX_STACK - player->inventory[i].count; int take = (remaining <= can) ? remaining : can; player->inventory[i].count += take; remaining -= take; } } // Then: existing stacks in big inventory for (int i = 0; remaining > 0 && i < BIG_INVENTORY_SIZE; i++) { if (player->big_inventory[i].type == block_type && player->big_inventory[i].count < INVENTORY_MAX_STACK) { int can = INVENTORY_MAX_STACK - player->big_inventory[i].count; int take = (remaining <= can) ? remaining : can; player->big_inventory[i].count += take; remaining -= take; } } // Then: empty hotbar slots (left-to-right) for (int i = 0; remaining > 0 && i < INVENTORY_SIZE; i++) { if (player->inventory[i].type == BLOCK_AIR) { int take = (remaining <= INVENTORY_MAX_STACK) ? remaining : INVENTORY_MAX_STACK; player->inventory[i].type = block_type; player->inventory[i].count = take; remaining -= take; } } // Finally: empty big inventory slots for (int i = 0; remaining > 0 && i < BIG_INVENTORY_SIZE; i++) { if (player->big_inventory[i].type == BLOCK_AIR) { int take = (remaining <= INVENTORY_MAX_STACK) ? remaining : INVENTORY_MAX_STACK; player->big_inventory[i].type = block_type; player->big_inventory[i].count = take; remaining -= take; } } return (remaining == 0); }