//> link: -lraylib -lm import "raylib.h" as rl; import "math.h"; const SCREEN_WIDTH = 800; const SCREEN_HEIGHT = 450; const TITLE = "Zen Craft"; // Block Types (Enum) const BLOCK_AIR = 0; const BLOCK_DIRT = 1; const BLOCK_GRASS = 2; const BLOCK_STONE = 3; // Physics Constants const GRAVITY = 18.0; const JUMP_FORCE = 8.0; const MOVE_SPEED = 5.0; // ** World / Chunk ** struct Chunk { blocks: int[16][16][16]; // x, y, z x: int; z: int; } extern fn GenImagePerlinNoise(width: int, height: int, offsetX: int, offsetY: int, scale: float) -> rl::Image; impl Chunk { static fn new(x: int, z: int) -> Chunk { let c: Chunk; c.x = x; c.z = z; // Generate Heightmap let heights: int[16][16]; raw { Image noise = GenImagePerlinNoise(16, 16, x * 16, z * 16, 0.5f); // Use LoadImageColors to safely get pixel data regardless of format Color* pixels = LoadImageColors(noise); for (int i = 0; i < 16; i++) { for (int j = 0; j < 16; j++) { // pixels is row-major: y * width + x // we want x=i, z=j. z is y in image. Color col = pixels[j * 16 + i]; int val = col.r; // Grayscale, so r=g=b // Map 0..255 to height 3..15 int h = 3 + (val / 20); if (h > 15) h = 15; heights[i][j] = h; } } UnloadImageColors(pixels); UnloadImage(noise); } for cx in 0..16 { for cz in 0..16 { let h = heights[cx][cz]; for cy in 0..16 { if cy < h - 1 { c.blocks[cx][cy][cz] = BLOCK_STONE; // Stone base } else if cy < h { c.blocks[cx][cy][cz] = BLOCK_DIRT; // Dirt layer } else if cy == h { c.blocks[cx][cy][cz] = BLOCK_GRASS; // Grass top } else { c.blocks[cx][cy][cz] = BLOCK_AIR; } } } } return c; } fn draw(self) { for x in 0..16 { for y in 0..16 { for z in 0..16 { let btype = self.blocks[x][y][z]; if btype != BLOCK_AIR { let _world_x = (float)(self.x * 16 + x); let _world_y = (float)y; let _world_z = (float)(self.z * 16 + z); match btype { BLOCK_DIRT => rl::DrawModel(model_dirt, rl::Vector3{x: _world_x + 0.5, y: _world_y + 0.5, z: _world_z + 0.5}, 1.0, rl::WHITE), BLOCK_STONE => rl::DrawModel(model_stone, rl::Vector3{x: _world_x + 0.5, y: _world_y + 0.5, z: _world_z + 0.5}, 1.0, rl::WHITE), BLOCK_GRASS => rl::DrawModel(model_grass, rl::Vector3{x: _world_x + 0.5, y: _world_y + 0.5, z: _world_z + 0.5}, 1.0, rl::WHITE), _ => rl::DrawCube(rl::Vector3{x: _world_x + 0.5, y: _world_y + 0.5, z: _world_z + 0.5}, 1.0, 1.0, 1.0, rl::PINK) } } } } } } } // Global helpers for multi-chunk operations fn get_block(chunks: Chunk*, num_chunks: int, x: int, y: int, z: int) -> int { // Check bounds (0 to 32 for x and z, 0 to 16 for y) if y < 0 || y >= 16 { return BLOCK_AIR; } if x < 0 || x >= 32 { return BLOCK_AIR; } if z < 0 || z >= 32 { return BLOCK_AIR; } // Determine chunk coordinates let cx = x / 16; let cz = z / 16; // Determine local block coordinates let lx = x % 16; let lz = z % 16; // Find the right chunk for i in 0..num_chunks { if chunks[i].x == cx && chunks[i].z == cz { return chunks[i].blocks[lx][y][lz]; } } return BLOCK_AIR; } fn set_block(chunks: Chunk*, num_chunks: int, x: int, y: int, z: int, type: int) { if y < 0 || y >= 16 { return; } let cx = x / 16; let cz = z / 16; let lx = x % 16; let lz = z % 16; for i in 0..num_chunks { if chunks[i].x == cx && chunks[i].z == cz { chunks[i].blocks[lx][y][lz] = type; return; } } } struct RaycastHit { hit: int; x: int; y: int; z: int; face_x: int; face_y: int; face_z: int; } fn raycast(chunks: Chunk*, num_chunks: int, ox: float, oy: float, oz: float, dx: float, dy: float, dz: float, max_dist: float) -> RaycastHit { let r: RaycastHit; r.hit = 0; let x = ox; let y = oy; let z = oz; let step = 0.05; let dist = 0.0; let last_x = (int)floor(x); let last_y = (int)floor(y); let last_z = (int)floor(z); while dist < max_dist { x += dx * step; y += dy * step; z += dz * step; dist += step; let ix = (int)floor(x); let iy = (int)floor(y); let iz = (int)floor(z); if get_block(chunks, num_chunks, ix, iy, iz) != BLOCK_AIR { r.hit = 1; r.x = ix; r.y = iy; r.z = iz; // Determine face r.face_x = 0; r.face_y = 0; r.face_z = 0; if ix > last_x { r.face_x = -1; } else if ix < last_x { r.face_x = 1; } else if iy > last_y { r.face_y = -1; } else if iy < last_y { r.face_y = 1; } else if iz > last_z { r.face_z = -1; } else if iz < last_z { r.face_z = 1; } return r; } last_x = ix; last_y = iy; last_z = iz; } return r; } // Raylib Externs extern fn DrawCubeTexture(texture: rl::Texture2D, position: rl::Vector3, width: float, height: float, length: float, color: rl::Color); // Textures & Models let model_dirt: rl::Model; let model_grass: rl::Model; let model_stone: rl::Model; let tex_sun: rl::Texture2D; let tex_moon: rl::Texture2D; fn load_texture_from_memory(_data: char*, _len: int) -> rl::Texture2D { let tex: rl::Texture2D; raw { Image img = LoadImageFromMemory(".png", (unsigned char*)_data, _len); tex = LoadTextureFromImage(img); UnloadImage(img); } return tex; } fn init_textures() { let dirt_data = embed "examples/games/zen_craft/assets/dirt.png"; let grass_data = embed "examples/games/zen_craft/assets/grass_side.png"; let stone_data = embed "examples/games/zen_craft/assets/stone.png"; let sun_data = embed "examples/games/zen_craft/assets/sun.png"; let moon_data = embed "examples/games/zen_craft/assets/moon.png"; let _tex_dirt = load_texture_from_memory(dirt_data.data, dirt_data.len); let _tex_grass = load_texture_from_memory(grass_data.data, grass_data.len); let _tex_stone = load_texture_from_memory(stone_data.data, stone_data.len); tex_sun = load_texture_from_memory(sun_data.data, sun_data.len); tex_moon = load_texture_from_memory(moon_data.data, moon_data.len); // Create Models let mesh = rl::GenMeshCube(1.0, 1.0, 1.0); model_dirt = rl::LoadModelFromMesh(mesh); model_grass = rl::LoadModelFromMesh(mesh); model_stone = rl::LoadModelFromMesh(mesh); raw { model_dirt.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = _tex_dirt; model_grass.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = _tex_grass; model_stone.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = _tex_stone; } } fn draw_sky(camera: rl::Camera3D) { // Disable Depth Mask to draw sky behind everything rl::BeginMode3D(camera); // Simple Fixed Sun Position or billboard let sun_pos = rl::Vector3{x: camera.position.x + 20.0, y: camera.position.y + 40.0, z: camera.position.z + 20.0}; raw { DrawBillboard(camera, tex_sun, sun_pos, 10.0f, WHITE); } rl::EndMode3D(); } struct Player { x: float; y: float; z: float; vx: float; vy: float; vz: float; yaw: float; pitch: float; camera: rl::Camera3D; } fn check_collision(chunks: Chunk*, num_chunks: int, x: float, y: float, z: float, w: float, h: float) -> int { let min_x = (int)floor(x - w/2.0); let max_x = (int)floor(x + w/2.0); let min_y = (int)floor(y); let max_y = (int)floor(y + h); let min_z = (int)floor(z - w/2.0); let max_z = (int)floor(z + w/2.0); for ix in min_x..max_x+1 { for iy in min_y..max_y+1 { for iz in min_z..max_z+1 { if get_block(chunks, num_chunks, ix, iy, iz) != BLOCK_AIR { return 1; } } } } return 0; } impl Player { static fn new(x: float, y: float, z: float) -> Player { let p: Player; p.x = x; p.y = y; p.z = z; p.vx = 0.0; p.vy = 0.0; p.vz = 0.0; p.yaw = 0.0; p.pitch = 0.0; p.camera = rl::Camera3D{ position: rl::Vector3{x: x, y: y, z: z}, target: rl::Vector3{x: x+1.0, y: y, z: z}, up: rl::Vector3{x: 0.0, y: 1.0, z: 0.0}, fovy: 60.0, projection: rl::CAMERA_PERSPECTIVE }; return p; } fn update(self, chunks: Chunk*, num_chunks: int, dt: float) { // Mouse look let mouse_delta = rl::GetMouseDelta(); self.yaw += mouse_delta.x * 0.003; self.pitch += mouse_delta.y * 0.003; if self.pitch > 1.5 { self.pitch = 1.5; } if self.pitch < -1.5 { self.pitch = -1.5; } // Movement input let dx = 0.0; let dz = 0.0; if rl::IsKeyDown(rl::KEY_W) { dx += 1.0; } if rl::IsKeyDown(rl::KEY_S) { dx -= 1.0; } if rl::IsKeyDown(rl::KEY_A) { dz -= 1.0; } if rl::IsKeyDown(rl::KEY_D) { dz += 1.0; } let fx = cos(self.yaw); let fz = sin(self.yaw); let rx = -sin(self.yaw); let rz = cos(self.yaw); let move_x = fx * dx + rx * dz; let move_z = fz * dx + rz * dz; let mlen = sqrt(move_x*move_x + move_z*move_z); if mlen > 0.001 { move_x /= mlen; move_z /= mlen; self.vx = move_x * MOVE_SPEED; self.vz = move_z * MOVE_SPEED; } else { self.vx = 0.0; self.vz = 0.0; } // Gravity and jump self.vy -= (float)GRAVITY * dt; if rl::IsKeyPressed(rl::KEY_SPACE) { // Only jump if on ground (check slightly below) if check_collision(chunks, num_chunks, self.x, self.y - 0.1, self.z, 0.6, 1.8) { self.vy = JUMP_FORCE; } } // Physics integration (Separate Axes) let box_w = 0.6; let box_h = 1.8; // X axis self.x += self.vx * dt; if check_collision(chunks, num_chunks, self.x, self.y, self.z, box_w, box_h) { self.x -= self.vx * dt; // Revert self.vx = 0.0; } // Z axis self.z += self.vz * dt; if check_collision(chunks, num_chunks, self.x, self.y, self.z, box_w, box_h) { self.z -= self.vz * dt; // Revert self.vz = 0.0; } // Y axis self.y += self.vy * dt; if check_collision(chunks, num_chunks, self.x, self.y, self.z, box_w, box_h) { self.y -= self.vy * dt; // Revert self.vy = 0.0; } // Check World Floor (Void Safety) if self.y < -10.0 { self.y = 20.0; self.vy = 0.0; } // Sync Camera let view_x = cos(self.yaw) * cos(self.pitch); let view_z = sin(self.yaw) * cos(self.pitch); let view_y = -sin(self.pitch); self.camera.position.x = self.x; self.camera.position.y = self.y + 1.6; self.camera.position.z = self.z; self.camera.target.x = self.camera.position.x + view_x; self.camera.target.y = self.camera.position.y + view_y; self.camera.target.z = self.camera.position.z + view_z; } } fn main() { rl::InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, TITLE); rl::SetWindowState(rl::FLAG_WINDOW_RESIZABLE); defer rl::CloseWindow(); rl::SetTargetFPS(60); // Load Assets init_textures(); // Initialize Player let p = Player::new(10.0, 10.0, 10.0); let selected_block = BLOCK_STONE; // Generate Chunks let chunks: Chunk[4]; chunks[0] = Chunk::new(0, 0); chunks[1] = Chunk::new(1, 0); chunks[2] = Chunk::new(0, 1); chunks[3] = Chunk::new(1, 1); rl::DisableCursor(); // Capture mouse while !rl::WindowShouldClose() { let dt: float = rl::GetFrameTime(); (&p).update(&chunks[0], 4, dt); // Block selection input if rl::IsKeyPressed(rl::KEY_ONE) { selected_block = BLOCK_DIRT; } if rl::IsKeyPressed(rl::KEY_TWO) { selected_block = BLOCK_GRASS; } if rl::IsKeyPressed(rl::KEY_THREE) { selected_block = BLOCK_STONE; } rl::BeginDrawing(); // Sky blue background rl::ClearBackground(rl::Color{r: 135, g: 206, b: 235, a: 255}); draw_sky(p.camera); rl::BeginMode3D(p.camera); // Render chunks for i in 0..4 { let c: Chunk* = &chunks[i]; c.draw(); } // Block interaction let screen_center = rl::Vector2{ x: (float)rl::GetScreenWidth() / 2.0, y: (float)rl::GetScreenHeight() / 2.0 }; let pick_ray = rl::GetScreenToWorldRay(screen_center, p.camera); let hit = raycast(&chunks[0], 4, pick_ray.position.x, pick_ray.position.y, pick_ray.position.z, pick_ray.direction.x, pick_ray.direction.y, pick_ray.direction.z, 5.0); if hit.hit != 0 { // Draw selection let hx = (float)hit.x; let hy = (float)hit.y; let hz = (float)hit.z; // Draw slight offset wireframe rl::DrawCubeWires(rl::Vector3{x: hx + 0.5, y: hy + 0.5, z: hz + 0.5}, 1.01, 1.01, 1.01, rl::BLACK); // Input handling if rl::IsMouseButtonPressed(rl::MOUSE_BUTTON_LEFT) { set_block(&chunks[0], 4, hit.x, hit.y, hit.z, BLOCK_AIR); } else if rl::IsMouseButtonPressed(rl::MOUSE_BUTTON_RIGHT) { let nx = hit.x + hit.face_x; let ny = hit.y + hit.face_y; let nz = hit.z + hit.face_z; // Don't place inside player (Simple check) let dist_x = p.x - ((float)nx + 0.5); let dist_y = p.y + 1.6 - ((float)ny + 0.5); // Eye pos vs block center let dist_z = p.z - ((float)nz + 0.5); if (dist_x*dist_x + dist_y*dist_y + dist_z*dist_z) > 2.0 { set_block(&chunks[0], 4, nx, ny, nz, selected_block); } } } rl::EndMode3D(); // Draw HUD let sw = rl::GetScreenWidth(); let sh = rl::GetScreenHeight(); rl::DrawRectangle(sw/2 - 5, sh/2 - 1, 10, 2, rl::RED); // Horizontal rl::DrawRectangle(sw/2 - 1, sh/2 - 5, 2, 10, rl::RED); // Vertical rl::DrawText("Chicken Jockey", 10, 10, 20, rl::DARKGRAY); rl::DrawText("WASD+Mouse: Move | Click: Break/Place", 10, 30, 20, rl::DARKGRAY); rl::DrawText("1: Dirt | 2: Grass | 3: Stone", 10, 50, 20, rl::DARKGRAY); // Show selected rl::DrawText("Selected:", 10, 70, 20, rl::BLACK); match selected_block { BLOCK_DIRT => rl::DrawText("DIRT", 110, 70, 20, rl::BROWN), BLOCK_GRASS => rl::DrawText("GRASS", 110, 70, 20, rl::GREEN), BLOCK_STONE => rl::DrawText("STONE", 110, 70, 20, rl::GRAY), _ => {} } rl::DrawFPS(10, 100); rl::EndDrawing(); } }