summaryrefslogtreecommitdiff
path: root/examples/games/zen_craft
diff options
context:
space:
mode:
authorZuhaitz Méndez Fernández de Aránguiz <zuhaitz@debian>2026-01-21 01:02:10 +0000
committerZuhaitz Méndez Fernández de Aránguiz <zuhaitz@debian>2026-01-21 01:02:10 +0000
commit8144aef45d5db22ab2895b41448cd76bf01e05cc (patch)
tree338daa041de0f211f50a4ad9529e090795137177 /examples/games/zen_craft
parentfd84225099175d54ea20f0d39ddf10ccd2daa381 (diff)
Steve
Diffstat (limited to 'examples/games/zen_craft')
-rw-r--r--examples/games/zen_craft/assets/dirt.pngbin0 -> 3831 bytes
-rw-r--r--examples/games/zen_craft/assets/grass.pngbin0 -> 4052 bytes
-rw-r--r--examples/games/zen_craft/assets/grass_side.pngbin0 -> 4052 bytes
-rw-r--r--examples/games/zen_craft/assets/moon.pngbin0 -> 313 bytes
-rw-r--r--examples/games/zen_craft/assets/stone.pngbin0 -> 3254 bytes
-rw-r--r--examples/games/zen_craft/assets/sun.pngbin0 -> 266 bytes
-rw-r--r--examples/games/zen_craft/main.zc523
7 files changed, 523 insertions, 0 deletions
diff --git a/examples/games/zen_craft/assets/dirt.png b/examples/games/zen_craft/assets/dirt.png
new file mode 100644
index 0000000..1e94b29
--- /dev/null
+++ b/examples/games/zen_craft/assets/dirt.png
Binary files differ
diff --git a/examples/games/zen_craft/assets/grass.png b/examples/games/zen_craft/assets/grass.png
new file mode 100644
index 0000000..178ae60
--- /dev/null
+++ b/examples/games/zen_craft/assets/grass.png
Binary files differ
diff --git a/examples/games/zen_craft/assets/grass_side.png b/examples/games/zen_craft/assets/grass_side.png
new file mode 100644
index 0000000..178ae60
--- /dev/null
+++ b/examples/games/zen_craft/assets/grass_side.png
Binary files differ
diff --git a/examples/games/zen_craft/assets/moon.png b/examples/games/zen_craft/assets/moon.png
new file mode 100644
index 0000000..e884c4e
--- /dev/null
+++ b/examples/games/zen_craft/assets/moon.png
Binary files differ
diff --git a/examples/games/zen_craft/assets/stone.png b/examples/games/zen_craft/assets/stone.png
new file mode 100644
index 0000000..4db9a2d
--- /dev/null
+++ b/examples/games/zen_craft/assets/stone.png
Binary files differ
diff --git a/examples/games/zen_craft/assets/sun.png b/examples/games/zen_craft/assets/sun.png
new file mode 100644
index 0000000..288f766
--- /dev/null
+++ b/examples/games/zen_craft/assets/sun.png
Binary files differ
diff --git a/examples/games/zen_craft/main.zc b/examples/games/zen_craft/main.zc
new file mode 100644
index 0000000..7f86c19
--- /dev/null
+++ b/examples/games/zen_craft/main.zc
@@ -0,0 +1,523 @@
+//> 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 {
+ var c: Chunk;
+ c.x = x;
+ c.z = z;
+
+ // Generate Heightmap
+ var 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 {
+ var 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 {
+ var btype = self.blocks[x][y][z];
+ if btype != BLOCK_AIR {
+ var _world_x = (float)(self.x * 16 + x);
+ var _world_y = (float)y;
+ var _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
+ var cx = x / 16;
+ var cz = z / 16;
+
+ // Determine local block coordinates
+ var lx = x % 16;
+ var 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; }
+
+ var cx = x / 16;
+ var cz = z / 16;
+ var lx = x % 16;
+ var 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 {
+ var r: RaycastHit;
+ r.hit = 0;
+
+ var x = ox;
+ var y = oy;
+ var z = oz;
+
+ var step = 0.05;
+ var dist = 0.0;
+
+ var last_x = (int)floor(x);
+ var last_y = (int)floor(y);
+ var last_z = (int)floor(z);
+
+ while dist < max_dist {
+ x += dx * step;
+ y += dy * step;
+ z += dz * step;
+ dist += step;
+
+ var ix = (int)floor(x);
+ var iy = (int)floor(y);
+ var 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
+var model_dirt: rl::Model;
+var model_grass: rl::Model;
+var model_stone: rl::Model;
+var tex_sun: rl::Texture2D;
+var tex_moon: rl::Texture2D;
+
+fn load_texture_from_memory(_data: char*, _len: int) -> rl::Texture2D {
+ var tex: rl::Texture2D;
+ raw {
+ Image img = LoadImageFromMemory(".png", (unsigned char*)_data, _len);
+ tex = LoadTextureFromImage(img);
+ UnloadImage(img);
+ }
+ return tex;
+}
+
+fn init_textures() {
+ var dirt_data = embed "examples/games/zen_craft/assets/dirt.png";
+ var grass_data = embed "examples/games/zen_craft/assets/grass_side.png";
+ var stone_data = embed "examples/games/zen_craft/assets/stone.png";
+ var sun_data = embed "examples/games/zen_craft/assets/sun.png";
+ var moon_data = embed "examples/games/zen_craft/assets/moon.png";
+
+ var _tex_dirt = load_texture_from_memory(dirt_data.data, dirt_data.len);
+ var _tex_grass = load_texture_from_memory(grass_data.data, grass_data.len);
+ var _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
+ var 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
+ var 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 {
+ var min_x = (int)floor(x - w/2.0);
+ var max_x = (int)floor(x + w/2.0);
+ var min_y = (int)floor(y);
+ var max_y = (int)floor(y + h);
+ var min_z = (int)floor(z - w/2.0);
+ var 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 {
+ var 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
+ var 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
+ var dx = 0.0;
+ var 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; }
+
+ var fx = cos(self.yaw);
+ var fz = sin(self.yaw);
+ var rx = -sin(self.yaw);
+ var rz = cos(self.yaw);
+
+ var move_x = fx * dx + rx * dz;
+ var move_z = fz * dx + rz * dz;
+
+ var 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)
+ var box_w = 0.6;
+ var 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
+ var view_x = cos(self.yaw) * cos(self.pitch);
+ var view_z = sin(self.yaw) * cos(self.pitch);
+ var 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
+ var p = Player::new(10.0, 10.0, 10.0);
+ var selected_block = BLOCK_STONE;
+
+ // Generate Chunks
+ var 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() {
+ var 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 {
+ var c: Chunk* = &chunks[i];
+ c.draw();
+ }
+
+ // Block interaction
+ var screen_center = rl::Vector2{
+ x: (float)rl::GetScreenWidth() / 2.0,
+ y: (float)rl::GetScreenHeight() / 2.0
+ };
+ var pick_ray = rl::GetScreenToWorldRay(screen_center, p.camera);
+
+ var 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
+ var hx = (float)hit.x;
+ var hy = (float)hit.y;
+ var 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) {
+ var nx = hit.x + hit.face_x;
+ var ny = hit.y + hit.face_y;
+ var nz = hit.z + hit.face_z;
+
+ // Don't place inside player (Simple check)
+ var dist_x = p.x - ((float)nx + 0.5);
+ var dist_y = p.y + 1.6 - ((float)ny + 0.5); // Eye pos vs block center
+ var 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
+ var sw = rl::GetScreenWidth();
+ var 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();
+ }
+} \ No newline at end of file