// A little block world, built entirely from code: terrain, caves, trees,
// pixel textures, sky, and even the tiny font. There are no asset files.
// Start empty-handed, gather wood, make a pick, mine stone and coal, then
// build a cabin and light it. This is a programming sample, not an engine:
// a finite grid, a few plain structs, and one update/draw loop.
//
// goose samples/30_mini_minecraft.goose
// goose samples/30_mini_minecraft.goose -- --seed 123
// goose samples/30_mini_minecraft.goose -- --frames 120
// goose samples/30_mini_minecraft.goose -- --test
//
// WASD walk; Space jump/swim up; Shift run; mouse or arrows look.
// Ctrl sneaks slowly and stops at ledges; jumping releases that protection.
// Hold left mouse to mine; right mouse or E places (hold to build a row).
// 1-8 or wheel selects a slot; middle click picks the aimed-at material.
// C opens crafting; hold Shift with a recipe to make all affordable batches.
// H toggles help. Menus release the mouse cursor.
// Escape pauses/resumes; F5 saves; F9 loads; F12 takes a screenshot.
// Losing focus pauses. Click or Escape resumes when you return.
// Close the window to quit. Saving is explicit, to mini_minecraft.sav in
// the working directory; starting a new game never overwrites a save.
// --frames runs a fresh, deterministic world and saves mini_minecraft.png.
// --test checks gameplay without opening a window or touching save files.
//
// Deliberate limits: 80 x 40 x 80 blocks, no mobs, hunger, damage, dropped
// items, fluid simulation, tool wear, or infinite streaming. Water stays
// where it was generated. Materials go straight into the inventory.
// Lamps are full blocks, with at most sixteen placed lights. Lighting is
// approximate, so it can leak through a wall. Tools are permanent upgrades.
// A day lasts ten minutes; night is peaceful, but lamps help you see.
//
// Collision with this grid is simpler to show directly than to maintain
// a separate physics world full of static boxes.
import std;
import vec;
import math;
import os;
import gfx;
// --- blocks and game state ----------------------------------------------------
let WIDTH = 80;
let HEIGHT = 40;
let DEPTH = 80;
let VOLUME = WIDTH * HEIGHT * DEPTH;
let CHUNK = 16;
let CHUNKS_X = WIDTH / CHUNK;
let CHUNKS_Z = DEPTH / CHUNK;
let SEA = 12;
let SLOTS = 8;
let MAX_LAMPS = 16;
let DAY_LENGTH: f32 = 600.0;
let AIR: u8 = 0;
let GRASS: u8 = 1;
let DIRT: u8 = 2;
let STONE: u8 = 3;
let SAND: u8 = 4;
let WOOD: u8 = 5;
let LEAVES: u8 = 6;
let PLANKS: u8 = 7;
let COBBLE: u8 = 8;
let COAL: u8 = 9;
let WATER: u8 = 10;
let BEDROCK: u8 = 11;
let LAMP: u8 = 12;
// Inventory slots are separate from block IDs: stone drops cobblestone,
// grass drops dirt, and coal is a crafting ingredient rather than a block.
let SLOT_BLOCK: u8[SLOTS] = [DIRT, COBBLE, WOOD, PLANKS, LEAVES, SAND, LAMP, AIR];
let SLOT_NAME: const u8[:][SLOTS] =
["DIRT", "STONE", "WOOD", "PLANKS", "LEAVES", "SAND", "LAMPS", "COAL"];
struct Cell { x: i64, y: i64, z: i64 }
struct Player {
feet: float3,
velocity: f32,
yaw: f32,
pitch: f32,
grounded: bool,
}
// This small, pointer-free record is the save header. The large block grid
// stays in global storage: passing or copying Game never copies the world.
// Transient input and GPU handles are kept outside the saved state.
struct Game {
version: u32,
seed: u32,
player: Player,
bag: i32[SLOTS],
tool: i32, // 0: hands, 1: wooden pick, 2: stone pick
selected: i32,
}
var game: Game;
var blocks: u8[VOLUME];
var day_time: f32 = 90.0; // Morning; zero is sunrise.
// Compute lighting once per frame, not once per block. The meshes contain
// only static sky exposure, so a moving sun never needs to rebuild them.
fn sun_direction() -> float3 {
let angle: f32 = day_time / DAY_LENGTH * TAU;
normalize(float3 { -cos(angle), sin(angle), 0.3 })
}
fn daylight() -> f32 { clamp(sun_direction().y * 3.0 + 0.35, 0.0, 1.0) }
fn horizon() -> float3 {
let day = daylight();
let dusk = 1.0 - abs(day * 2.0 - 1.0);
float3 { 0.045, 0.065, 0.12 } * f3(1.0 - day) +
float3 { 0.70, 0.82, 0.87 } * f3(day) + float3 { 0.30, 0.04, 0.0 } * f3(dusk)
}
fn advance_time(dt: f32) { day_time = (day_time + dt) % DAY_LENGTH; }
struct Mesh {
vertices: gfx::Buffer,
indices: gfx::Buffer,
count: i64,
dirty: bool,
}
var meshes: Mesh[CHUNKS_X * CHUNKS_Z];
var lamps: float4[MAX_LAMPS];
var lamp_count = 0;
var message: u8[..100];
var message_time: f32 = 0.0;
// Cosmetic fragments are a fixed pool: breaking another block reuses the
// oldest slots. They never affect collisions, inventory, or saved games.
let MAX_DEBRIS = 48;
struct Debris { pos: float3, velocity: float3, life: f32, tile: i64 }
var debris: Debris[MAX_DEBRIS];
var next_debris = 0;
fn scatter(c: Cell, b: u8) {
for i in 12 {
let h = hash(c.x + i, c.y, c.z, game.seed);
let velocity = float3 { (h & 255) / 64.0 - 2.0, 1.5 + ((h >> 8) & 255) / 128.0,
((h >> 16) & 255) / 64.0 - 2.0 };
debris[next_debris] = Debris {
position(c) + f3(0.5) + velocity * f3(0.12), velocity, 0.45 + i * 0.012, tile_for(b, 4) };
next_debris = (next_debris + 1) % MAX_DEBRIS;
}
}
fn animate_debris(dt: f32) {
for i in MAX_DEBRIS {
if debris[i].life <= 0.0 { continue; }
debris[i].life -= dt;
debris[i].velocity.y -= 10.0 * dt;
debris[i].pos += debris[i].velocity * f3(dt);
}
}
fn say(text: const u8[:]) {
message.resize(0);
message.append(text);
message_time = 4.0;
}
fn f3(n: f32) -> float3 { float3 { n, n, n } }
fn cell(x: i64, y: i64, z: i64) -> Cell { Cell { x, y, z } }
fn floor_cell(p: float3) -> Cell {
Cell { floor(p.x) as i64, floor(p.y) as i64, floor(p.z) as i64 }
}
fn position(c: Cell) -> float3 { float3 { c.x, c.y, c.z } }
fn inside(c: Cell) -> bool {
c.x >= 0 && c.x < WIDTH && c.y >= 0 && c.y < HEIGHT && c.z >= 0 && c.z < DEPTH
}
fn index(c: Cell) -> i64 { (c.y * DEPTH + c.z) * WIDTH + c.x }
fn block_at(c: Cell) -> u8 {
if !inside(c) { return AIR; }
blocks[index(c)]
}
fn solid(b: u8) -> bool { b != AIR && b != WATER }
fn put(c: Cell, b: u8) {
if inside(c) { blocks[index(c)] = b; }
}
fn dirty_all() {
for i in meshes.len { meshes[i].dirty = true; }
}
fn dirty_at(x: i64, z: i64) {
if x >= 0 && x < WIDTH && z >= 0 && z < DEPTH {
meshes[(z / CHUNK) * CHUNKS_X + x / CHUNK].dirty = true;
}
}
fn find_lamps() {
lamps = [float4_0; MAX_LAMPS];
lamp_count = 0;
for y in HEIGHT {
for z in DEPTH {
for x in WIDTH {
if block_at(cell(x, y, z)) == LAMP && lamp_count < MAX_LAMPS {
lamps[lamp_count] = float4 { x + 0.5, y + 0.5, z + 0.5, 1.0 };
lamp_count++;
}
}
}
}
}
fn edit(c: Cell, b: u8) {
let old = block_at(c);
put(c, b);
// Corner shading samples diagonal neighbours too. Rebuild all chunks
// touched by a one-cell border; most edits still touch only one chunk.
for dz in -1..2 {
for dx in -1..2 { dirty_at(c.x + dx, c.z + dz); }
}
if old == LAMP || b == LAMP { find_lamps(); }
}
// --- deterministic terrain ---------------------------------------------------
// Unsigned arithmetic wraps. A coordinate hash needs no mutable random
// state, so adding a tree does not change the hills or texture patterns.
fn hash(x: i64, y: i64, z: i64, seed: u32) -> u32 {
var h = (x as! u32) * 374761393 + (y as! u32) * 668265263;
h += (z as! u32) * 2246822519 + seed * 3266489917;
h = (h ^ (h >> 13)) * 1274126177;
h ^ (h >> 16)
}
fn noise_value(x: i64, z: i64) -> f32 {
(hash(x, 0, z, game.seed) & 65535) / 65535.0
}
fn noise(x: f32, z: f32) -> f32 {
let ix = floor(x) as i64;
let iz = floor(z) as i64;
let fx = x - ix;
let fz = z - iz;
let u = fx * fx * (3.0 - 2.0 * fx);
let v = fz * fz * (3.0 - 2.0 * fz);
let a = noise_value(ix, iz) * (1.0 - u) + noise_value(ix + 1, iz) * u;
let b = noise_value(ix, iz + 1) * (1.0 - u) + noise_value(ix + 1, iz + 1) * u;
a * (1.0 - v) + b * v
}
fn ground_height(x: i64, z: i64) -> i64 {
let broad = noise(x / 24.0, z / 24.0);
let detail = noise(x / 8.0, z / 8.0);
// A basin guarantees a lake; a broad ridge gives the cave a hillside.
let lake = max(0.0, 1.0 - sqrt((x - 23.0) * (x - 23.0) +
(z - 28.0) * (z - 28.0)) / 18.0);
let ridge = max(0.0, 1.0 - abs(x - 58.0) / 15.0);
let terrain = 13.0 + broad * 7.0 + detail * 3.0 - lake * 11.0 + ridge * 7.0;
let distance = sqrt((x - 40.0) * (x - 40.0) + (z - 40.0) * (z - 40.0));
let clearing = clamp((distance - 8.0) / 5.0, 0.0, 1.0);
floor(19.0 * (1.0 - clearing) + terrain * clearing) as i64
}
fn highest(x: i64, z: i64) -> i64 {
var y = HEIGHT - 1;
while y > 0 && !solid(block_at(cell(x, y, z))) { y--; }
y
}
fn tree(x: i64, z: i64) {
let base = ground_height(x, z) + 1;
if block_at(cell(x, base - 1, z)) != GRASS { return; }
let height = 4 + (hash(x, 1, z, game.seed) % 2) as i64;
for y in base + 3..base + height + 2 {
let radius = if y == base + height + 1 { 1 } else { 2 };
for dz in -radius..radius + 1 {
for dx in -radius..radius + 1 {
if abs(dx) == 2 && abs(dz) == 2 { continue; }
let c = cell(x + dx, y, z + dz);
if block_at(c) == AIR { put(c, LEAVES); }
}
}
}
for y in base..base + height { put(cell(x, y, z), WOOD); }
}
fn respawn() {
// Search dry ground from the centre, without placing the player in a
// trunk, a leaf canopy, a saved building, or the lake.
for radius in 0..WIDTH / 2 {
for z in max(1, 40 - radius)..min(DEPTH - 1, 41 + radius) {
for x in max(1, 40 - radius)..min(WIDTH - 1, 41 + radius) {
let y = highest(x, z);
let b = block_at(cell(x, y, z));
if (b == GRASS || b == SAND || b == STONE) && y >= SEA && y < HEIGHT - 3 {
game.player = Player {
feet: float3 { x + 0.5, y + 1.001, z + 0.5 },
velocity: 0.0, yaw: -0.4, pitch: -0.12, grounded: false };
return;
}
}
}
}
game.player = Player { feet: float3 { 40.5, 41.0, 40.5 }, .. };
}
fn generate(seed: u32) {
game = Game { version: 1, seed: seed, .. };
day_time = 90.0;
debris = [Debris { .. }; MAX_DEBRIS];
message_time = 0.0;
for i in VOLUME { blocks[i] = AIR; }
for z in DEPTH {
for x in WIDTH {
let top = ground_height(x, z);
for y in 0..max(top + 1, SEA + 1) {
var b = STONE;
if y == 0 { b = BEDROCK; }
else if y > top { b = WATER; }
else if y == top { b = if top <= SEA + 1 { SAND } else { GRASS }; }
else if y >= top - 3 { b = if top <= SEA + 1 { SAND } else { DIRT }; }
else {
// Coarse vein clusters with little ragged edges.
let vein = hash(x / 3, y / 3, z / 3, seed) % 13 == 0;
if vein && hash(x, y, z, seed) % 4 != 0 { b = COAL; }
let cave = sin(x * 0.24) + cos(z * 0.21) + sin(y * 0.55 + x * 0.11);
if y > 2 && y < top - 4 && cave > 2.25 { b = AIR; }
}
put(cell(x, y, z), b);
}
}
}
// A walk-in tunnel starts at the clearing and slopes into the ridge.
// Carving overlapping little spheres is easy to read and modify.
let entrance = ground_height(46, 40);
for step in 22 {
let cx: f32 = 45.0 + step * 0.65;
let cy: f32 = entrance + 1.3 - step * 0.22;
let cz: f32 = 40.0 - step * 0.38;
for y in 2..HEIGHT - 1 {
for z in 28..44 {
for x in 43..63 {
let d = float3 { x + 0.5 - cx, y + 0.5 - cy, z + 0.5 - cz };
if length_sq(d) < 3.2 { put(cell(x, y, z), AIR); }
}
}
}
}
for z in 4..DEPTH - 4 {
for x in 4..WIDTH - 4 {
let clearing = abs(x - 40) < 11 && abs(z - 40) < 11;
if !clearing && x % 4 == 0 && z % 4 == 0 && hash(x, 5, z, seed) % 3 == 0 {
tree(x, z);
}
}
}
tree(38, 34); // The first resource is always a few steps away.
respawn();
find_lamps();
dirty_all();
}
// --- walking, swimming, and a grid ray ---------------------------------------
let RADIUS: f32 = 0.29;
let BODY_HEIGHT: f32 = 1.8;
let EYE_HEIGHT: f32 = 1.62;
let STEP: f32 = 1.0 / 120.0;
fn eye() -> float3 { game.player.feet + float3 { 0.0, EYE_HEIGHT, 0.0 } }
fn forward() -> float3 {
let p = game.player;
float3 { sin(p.yaw) * cos(p.pitch), sin(p.pitch), -cos(p.yaw) * cos(p.pitch) }
}
fn overlaps(feet: float3, c: Cell) -> bool {
feet.x + RADIUS > c.x && feet.x - RADIUS < c.x + 1 &&
feet.y + BODY_HEIGHT > c.y && feet.y < c.y + 1 &&
feet.z + RADIUS > c.z && feet.z - RADIUS < c.z + 1
}
fn blocked(feet: float3) -> bool {
// The finite map's edges are walls, while its sky is open.
if feet.x < RADIUS || feet.z < RADIUS || feet.x > WIDTH - RADIUS ||
feet.z > DEPTH - RADIUS || feet.y < 1.0 { return true; }
let lo = floor_cell(feet - float3 { RADIUS, 0.0, RADIUS });
let hi = floor_cell(feet + float3 { RADIUS, BODY_HEIGHT - 0.001, RADIUS });
for y in lo.y..hi.y + 1 {
for z in lo.z..hi.z + 1 {
for x in lo.x..hi.x + 1 {
if solid(block_at(cell(x, y, z))) { return true; }
}
}
}
false
}
fn wet(feet: float3) -> bool {
block_at(floor_cell(feet + float3 { 0.0, 0.05, 0.0 })) == WATER
}
fn supported(feet: float3) -> bool {
// Leave a little overlap with the floor when sneaking onto a ledge.
// Water does not count as support, and only the layer below the feet
// matters: a nearby wall must not let us walk off a roof.
let margin: f32 = RADIUS - 0.02;
let lo = floor_cell(feet - float3 { margin, 0.05, margin });
let hi = floor_cell(feet + float3 { margin, -0.05, margin });
for z in lo.z..hi.z + 1 {
for x in lo.x..hi.x + 1 {
if solid(block_at(cell(x, lo.y, z))) { return true; }
}
}
false
}
fn move_axis(delta: float3, keep_support: bool = false) -> bool {
let start = game.player.feet;
if !blocked(start + delta) && (!keep_support || supported(start + delta)) {
game.player.feet += delta;
return false;
}
// Find the contact point. The fixed timestep bounds each move to less
// than a block, and bisection leaves the player just outside the wall.
var lo: f32 = 0.0;
var hi: f32 = 1.0;
for i in 10 {
let mid = (lo + hi) * 0.5;
let candidate = start + delta * f3(mid);
if blocked(candidate) || (keep_support && !supported(candidate)) { hi = mid; }
else { lo = mid; }
}
game.player.feet += delta * f3(lo);
true
}
fn walk(direction: float2, jump: bool, run: bool, sneak: bool = false) {
let swimming = wet(game.player.feet);
let speed: f32 = if swimming { 2.6 } else if sneak { 1.5 } else if run { 6.0 } else { 4.2 };
let right = float3 { cos(game.player.yaw), 0.0, sin(game.player.yaw) };
let ahead = float3 { sin(game.player.yaw), 0.0, -cos(game.player.yaw) };
var input = direction;
if length_sq(input) > 1.0 { input = normalize(input); }
let delta = (right * f3(input.x) + ahead * f3(input.y)) * f3(speed * STEP);
let keep_support = sneak && game.player.grounded && !jump && !swimming;
move_axis(float3 { delta.x, 0.0, 0.0 }, keep_support);
move_axis(float3 { 0.0, 0.0, delta.z }, keep_support);
if swimming {
game.player.velocity = max(game.player.velocity - 4.0 * STEP, -2.0);
if jump {
game.player.velocity = 4.0;
// A kick near the surface clears a one-block shore. Testing
// the feet for water keeps this active until we actually exit.
if block_at(floor_cell(game.player.feet + float3 { 0.0, 1.0, 0.0 })) != WATER {
game.player.velocity = 8.0;
}
}
} else {
if jump && game.player.grounded { game.player.velocity = 8.0; }
game.player.velocity = max(game.player.velocity - 23.0 * STEP, -30.0);
}
let hit = move_axis(float3 { 0.0, game.player.velocity * STEP, 0.0 });
game.player.grounded = hit && game.player.velocity < 0.0;
if hit { game.player.velocity = 0.0; }
}
struct Hit { found: bool, at: Cell, before: Cell, distance: f32 }
fn ray(origin: float3, direction: float3, reach: f32) -> Hit {
// DDA: cross exactly one grid plane each step. This neither skips thin
// targets nor guesses the placement face from a small sampling step.
var at = floor_cell(origin);
var before = at;
let step = Cell { if direction.x >= 0.0 { 1 } else { -1 },
if direction.y >= 0.0 { 1 } else { -1 },
if direction.z >= 0.0 { 1 } else { -1 } };
let delta = float3 {
if abs(direction.x) < 0.00001 { 1.0e8 } else { abs(1.0 / direction.x) },
if abs(direction.y) < 0.00001 { 1.0e8 } else { abs(1.0 / direction.y) },
if abs(direction.z) < 0.00001 { 1.0e8 } else { abs(1.0 / direction.z) } };
var next = float3 {
(if step.x > 0 { at.x + 1.0 - origin.x } else { origin.x - at.x }) * delta.x,
(if step.y > 0 { at.y + 1.0 - origin.y } else { origin.y - at.y }) * delta.y,
(if step.z > 0 { at.z + 1.0 - origin.z } else { origin.z - at.z }) * delta.z };
var travelled: f32 = 0.0;
while travelled <= reach {
if solid(block_at(at)) { return Hit { true, at, before, travelled }; }
before = at;
if next.x <= next.y && next.x <= next.z {
travelled = next.x;
next.x += delta.x;
at.x += step.x;
} else if next.y <= next.z {
travelled = next.y;
next.y += delta.y;
at.y += step.y;
} else {
travelled = next.z;
next.z += delta.z;
at.z += step.z;
}
}
Hit { found: false, .. }
}
// --- mining, building, and four recipes --------------------------------------
fn drop_slot(b: u8) -> i64 {
if b == GRASS || b == DIRT { return 0; }
if b == STONE || b == COBBLE { return 1; }
if b == COAL { return 7; }
for i in SLOTS { if SLOT_BLOCK[i] == b { return i; } }
-1
}
fn pick_material(hit: Hit) {
if !hit.found { return; }
let slot = drop_slot(block_at(hit.at));
if slot >= 0 && SLOT_BLOCK[slot] != AIR {
game.selected = slot as i32;
if game.bag[slot] == 0 { say("MINE SOME OF THIS MATERIAL FIRST"); }
}
}
fn mining_seconds(b: u8) -> f32 {
if b == BEDROCK { return 0.0; }
if b == STONE || b == COBBLE || b == COAL {
if game.tool == 0 { return 0.0; }
return if game.tool == 1 { 1.15 } else { 0.45 };
}
if b == WOOD { return 1.0; }
if b == PLANKS { return 0.65; }
if b == LEAVES || b == LAMP { return 0.18; }
0.35
}
fn mine(c: Cell) -> bool {
let b = block_at(c);
let slot = drop_slot(b);
if slot < 0 || mining_seconds(b) <= 0.0 { return false; }
if game.bag[slot] >= 999 { say("THAT STACK IS FULL (999)"); return false; }
game.bag[slot]++;
edit(c, AIR);
scatter(c, b);
true
}
fn place(hit: Hit) -> bool {
if !hit.found || hit.at == hit.before || !inside(hit.before) { return false; }
let slot = game.selected;
let b = SLOT_BLOCK[slot];
if b == AIR { say("COAL IS FOR CRAFTING LAMPS"); return false; }
if game.bag[slot] == 0 { say("MINE OR CRAFT SOME FIRST"); return false; }
if solid(block_at(hit.before)) || overlaps(game.player.feet, hit.before) { return false; }
if b == LAMP && lamp_count == MAX_LAMPS {
say("SIXTEEN LIGHTS MAX - PICK ONE UP FIRST");
return false;
}
edit(hit.before, b);
game.bag[slot]--;
true
}
fn can_craft(recipe: i64) -> bool {
if recipe == 0 { return game.bag[2] >= 1 && game.bag[3] <= 995; }
if recipe == 1 { return game.tool == 0 && game.bag[3] >= 5; }
if recipe == 2 { return game.tool == 1 && game.bag[1] >= 3 && game.bag[3] >= 2; }
if recipe == 3 { return game.bag[7] >= 1 && game.bag[3] >= 1 && game.bag[6] <= 995; }
false
}
fn craft(recipe: i64) -> bool {
if !can_craft(recipe) {
say("NOT ENOUGH MATERIALS, OR UPGRADE ALREADY OWNED");
return false;
}
if recipe == 0 {
game.bag[2]--;
game.bag[3] += 4;
say("MADE FOUR PLANKS");
return true;
}
if recipe == 1 {
game.bag[3] -= 5;
game.tool = 1;
say("WOODEN PICK - NOW YOU CAN MINE STONE");
return true;
}
if recipe == 2 {
game.bag[1] -= 3;
game.bag[3] -= 2;
game.tool = 2;
say("STONE PICK - FASTER MINING");
return true;
}
if recipe == 3 {
game.bag[7]--;
game.bag[3]--;
game.bag[6] += 4;
say("MADE FOUR LAMPS - SELECT SLOT 7 TO PLACE");
return true;
}
false
}
fn craft_batches(recipe: i64, all: bool) -> i64 {
if !craft(recipe) { return 0; }
var batches = 1;
// can_craft checks both ingredients and stack capacity. Tools become
// unavailable once owned, so even "craft all" makes only one upgrade.
while all && can_craft(recipe) {
craft(recipe);
batches++;
}
if batches > 1 {
say(str("MADE ", batches * 4, if recipe == 0 { " PLANKS" } else { " LAMPS" }));
}
batches
}
// --- explicit saves ----------------------------------------------------------
fn valid_save(g: const Game&, cells: const u8[:]) -> bool {
if cells.len != VOLUME { return false; }
if g.version != 1 || g.tool < 0 || g.tool > 2 || g.selected < 0 || g.selected >= SLOTS {
return false;
}
for n in g.bag { if n < 0 || n > 999 { return false; } }
var lights = 0;
for b in cells {
if b > LAMP { return false; }
if b == LAMP { lights++; }
}
if lights > MAX_LAMPS { return false; }
let p = g.player;
// Positive tests reject NaNs as well as out-of-range values.
if !(p.feet.x >= RADIUS && p.feet.x <= WIDTH - RADIUS &&
p.feet.z >= RADIUS && p.feet.z <= DEPTH - RADIUS &&
p.feet.y >= 1.0 && p.feet.y <= HEIGHT + 8.0 &&
p.yaw >= -100.0 && p.yaw <= 100.0 && abs(p.pitch) <= 1.5 &&
abs(p.velocity) <= 30.0) { return false; }
for z in DEPTH {
for x in WIDTH { if cells[z * WIDTH + x] != BEDROCK { return false; } }
}
true
}
fn save_image() -> u8[>..] {
var image: u8[>..] = [];
image.append("GSM2");
to_bytes([game], image);
image.append(blocks);
to_bytes([day_time], image);
image
}
fn restore(image: const u8[:]) -> bool {
// The header uses Goose's framed serialization; the grid is just bytes
// of block IDs. Validate both before changing anything in the game.
let header_size = to_bytes([Game { .. }]).len;
let grid_end = 4 + header_size + VOLUME;
let clock_size = to_bytes([day_time]).len;
if image.len < 4 { return false; }
let legacy = image[..4] == "GSM1";
if legacy {
if image.len != grid_end { return false; }
} else if image[..4] != "GSM2" || image.len != grid_end + clock_size { return false; }
var header, ok = from_bytes<Game[>..]>(image[4..4 + header_size]);
if !ok || header.len != 1 { return false; }
let saved_blocks = image[4 + header_size..grid_end];
if !valid_save(header[0], saved_blocks) { return false; }
// The first version had no clock. Old worlds resume in the morning.
var saved_time: f32 = 90.0;
if !legacy {
let clock, clock_ok = from_bytes<f32[>..]>(image[grid_end..]);
if !clock_ok || clock.len != 1 || !(clock[0] >= 0.0 && clock[0] < DAY_LENGTH) { return false; }
saved_time = clock[0];
}
game = header[0];
day_time = saved_time;
debris = [Debris { .. }; MAX_DEBRIS];
for i in VOLUME { blocks[i] = saved_blocks[i]; }
if blocked(game.player.feet) { respawn(); }
find_lamps();
dirty_all();
true
}
fn save_game() {
let image = save_image();
if write_file_atomic("mini_minecraft.sav", image) { say("SAVED MINI_MINECRAFT.SAV"); }
else { say("SAVE FAILED - CHECK THE WORKING DIRECTORY"); }
}
fn load_game() {
var image: u8[>..] = [];
if !read_file("mini_minecraft.sav", image) { say("NO READABLE MINI_MINECRAFT.SAV"); return; }
if !restore(image) { say("SAVE FILE IS DAMAGED OR INCOMPATIBLE"); return; }
say("WORLD LOADED");
}
// --- a 256 x 16 texture atlas, painted one pixel at a time --------------------
let TILE_SIZE = 16;
let TILES = 16;
let WHITE_TILE = 14;
let TILE_COLOR: int3[TILES] = [
int3 { 101, 153, 58 }, int3 { 126, 89, 55 }, int3 { 126, 89, 55 },
int3 { 129, 132, 130 }, int3 { 220, 204, 150 }, int3 { 106, 76, 43 },
int3 { 173, 137, 81 }, int3 { 65, 120, 48 }, int3 { 177, 135, 79 },
int3 { 126, 131, 133 }, int3 { 121, 126, 128 }, int3 { 52, 126, 176 },
int3 { 61, 64, 74 }, int3 { 244, 198, 94 }, int3 { 255, 255, 255 },
int3 { 255, 255, 255 } ];
fn tile_for(b: u8, face: i64) -> i64 {
if b == GRASS { return if face == 2 { 0 } else if face == 3 { 2 } else { 1 }; }
if b == WOOD { return if face == 2 || face == 3 { 6 } else { 5 }; }
if b == DIRT { return 2; }
if b == STONE { return 3; }
if b == SAND { return 4; }
if b == LEAVES { return 7; }
if b == PLANKS { return 8; }
if b == COBBLE { return 9; }
if b == COAL { return 10; }
if b == WATER { return 11; }
if b == BEDROCK { return 12; }
if b == LAMP { return 13; }
WHITE_TILE
}
fn pixel(tile: i64, x: i64, y: i64) -> u8[4] {
var color = TILE_COLOR[tile];
var grain = (hash(x, tile, y, 71) % 27) as i64 - 13;
if tile == 1 && y < 3 + (hash(x / 2, 0, 0, 8) % 3) as i64 {
color = TILE_COLOR[0];
}
if tile == 3 { grain += if (x + y * 3) % 11 == 0 { -20 } else { 0 }; }
if tile == 5 { grain += if (x + (y / 5)) % 4 == 0 { -30 } else { 9 }; }
if tile == 6 {
let ring = max(abs(x - 7), abs(y - 7));
grain += if ring % 3 == 0 { -30 } else { 12 };
}
if tile == 7 {
grain += if hash(x / 2, 0, y / 2, 17) % 3 == 0 { -28 } else { 10 };
}
if tile == 8 {
if y % 4 == 0 || (x + (y / 4) * 7) % 16 == 0 { grain = -42; }
else { grain += if y % 4 == 1 { 12 } else { 0 }; }
}
if tile == 9 {
let row = y / 5;
if y % 5 == 0 || (x + row * 3) % 7 == 0 { grain = -45; }
else if y % 5 == 1 { grain = 18; }
}
if tile == 10 && hash(x / 2, 0, y / 2, 9) % 5 == 0 { color = int3 { 32, 34, 39 }; }
if tile == 11 { grain = if (x + y * 3) % 13 < 2 { 20 } else { 0 }; }
if tile == 12 { grain *= 2; }
if tile == 13 {
if x < 2 || x > 13 || y < 2 || y > 13 { color = int3 { 94, 66, 39 }; }
else { grain = if x == 7 || y == 7 { 25 } else { 0 }; }
}
if tile >= WHITE_TILE { grain = 0; }
[clamp(color.x + grain, 0, 255) as u8, clamp(color.y + grain, 0, 255) as u8,
clamp(color.z + grain, 0, 255) as u8, 255]
}
fn make_atlas() -> gfx::Texture {
var pixels: u8[>..] = [];
for y in TILE_SIZE {
for tile in TILES {
for x in TILE_SIZE { pixels.append(pixel(tile, x, y)); }
}
}
let texture = gfx::texture2d(TILE_SIZE * TILES, TILE_SIZE);
if texture.id == 0 || !gfx::update_texture(texture, pixels) {
abort(str("texture: ", gfx::error()));
}
texture
}
fn atlas_uv(tile: i64, x: f32, y: f32) -> float2 {
// Sample inside the tile, so the nearest sampler never grabs a pixel
// from its neighbour, even right at the edge of a face.
float2 { (tile * TILE_SIZE + 0.5 + x * 15.0) / (TILE_SIZE * TILES),
(0.5 + y * 15.0) / TILE_SIZE }
}
// --- exposed faces, split into 16 x 40 x 16 chunks ----------------------------
struct Vertex {
pos: float3,
normal: float3,
uv: float2,
shade: f32,
material: f32,
}
let NORMAL: Cell[6] = [Cell { 1, 0, 0 }, Cell { -1, 0, 0 },
Cell { 0, 1, 0 }, Cell { 0, -1, 0 },
Cell { 0, 0, 1 }, Cell { 0, 0, -1 }];
let ACROSS: Cell[6] = [Cell { 0, 0, -1 }, Cell { 0, 0, 1 },
Cell { 1, 0, 0 }, Cell { 1, 0, 0 },
Cell { 1, 0, 0 }, Cell { -1, 0, 0 }];
let UP: Cell[6] = [Cell { 0, 1, 0 }, Cell { 0, 1, 0 },
Cell { 0, 0, -1 }, Cell { 0, 0, 1 },
Cell { 0, 1, 0 }, Cell { 0, 1, 0 }];
let CORNERS: float2[4] = [float2 { -1.0, -1.0 }, float2 { 1.0, -1.0 },
float2 { 1.0, 1.0 }, float2 { -1.0, 1.0 }];
var column_top: i64[WIDTH * DEPTH];
fn offset(c: Cell, dir: Cell, amount: i64) -> Cell {
Cell { c.x + dir.x * amount, c.y + dir.y * amount, c.z + dir.z * amount }
}
fn sky_light(c: Cell) -> f32 {
if c.x < 0 || c.x >= WIDTH || c.z < 0 || c.z >= DEPTH { return 1.0; }
if c.y > column_top[c.z * WIDTH + c.x] { return 1.0; }
0.32
}
fn face_visible(b: u8, neighbour: u8) -> bool {
if b == WATER { return neighbour == AIR; }
neighbour == AIR || neighbour == WATER
}
fn add_face(vertices: Vertex[>..]&, indices: u32[>..]&, c: Cell, b: u8, face: i64) {
let n = NORMAL[face];
let u = ACROSS[face];
let v = UP[face];
let outside = c + n;
let base = vertices.len as u32;
for corner in CORNERS {
let side_u = offset(outside, u, corner.x as i64);
let side_v = offset(outside, v, corner.y as i64);
let diagonal = offset(side_u, v, corner.y as i64);
let a = solid(block_at(side_u));
let d = solid(block_at(side_v));
let e = solid(block_at(diagonal));
// Three neighbouring blocks darken a corner; two touching sides
// close it completely, even if the diagonal happens to be air.
var occlusion = 0;
if a { occlusion++; }
if d { occlusion++; }
if e { occlusion++; }
if a && d { occlusion = 3; }
let shade = sky_light(outside) * (1.0 - occlusion * 0.16);
let p = position(c) + f3(0.5) + position(n) * f3(0.5) +
position(u) * f3(corner.x * 0.5) + position(v) * f3(corner.y * 0.5);
let uv = atlas_uv(tile_for(b, face), corner.x * 0.5 + 0.5, 0.5 - corner.y * 0.5);
vertices.push(Vertex { p, position(n), uv, shade, b });
}
indices.append([base, base + 1, base + 2, base, base + 2, base + 3]);
}
fn rebuild_chunk(cx: i64, cz: i64) {
// Scratch lives only until this function returns. The GPU owns a copy.
var vertices: Vertex[>..] = [];
var indices: u32[>..] = [];
for y in HEIGHT {
for z in cz * CHUNK..(cz + 1) * CHUNK {
for x in cx * CHUNK..(cx + 1) * CHUNK {
let c = cell(x, y, z);
let b = block_at(c);
if b == AIR { continue; }
for face in 6 {
if face_visible(b, block_at(c + NORMAL[face])) {
add_face(vertices, indices, c, b, face);
}
}
}
}
}
let i = cz * CHUNKS_X + cx;
if meshes[i].vertices.id != 0 { gfx::release(meshes[i].vertices); }
if meshes[i].indices.id != 0 { gfx::release(meshes[i].indices); }
meshes[i] = Mesh { count: indices.len, .. };
if indices.len > 0 {
meshes[i].vertices = gfx::vertex_buffer(bytes_of(vertices));
meshes[i].indices = gfx::index_buffer(bytes_of(indices));
if meshes[i].vertices.id == 0 || meshes[i].indices.id == 0 {
abort(str("chunk buffers: ", gfx::error()));
}
}
}
fn rebuild_dirty() {
var any = false;
for m in meshes { any = any || m.dirty; }
if !any { return; }
for z in DEPTH {
for x in WIDTH { column_top[z * WIDTH + x] = highest(x, z); }
}
for cz in CHUNKS_Z {
for cx in CHUNKS_X {
if meshes[cz * CHUNKS_X + cx].dirty { rebuild_chunk(cx, cz); }
}
}
}
fn debris_vertices(right: float3, up: float3) -> Vertex[>..] {
// Camera-facing squares reuse the world's texture atlas and lighting.
// Only a small patch of each tile is needed for a chip of that material.
var vertices: Vertex[>..] = [];
let order: i64[6] = [0, 1, 2, 0, 2, 3];
let normal = cross(right, up);
for bit in debris {
if bit.life <= 0.0 { continue; }
let size: f32 = 0.065 * min(1.0, bit.life * 8.0);
for i in order {
let corner = CORNERS[i];
let p = bit.pos + right * f3(corner.x * size) + up * f3(corner.y * size);
let uv = atlas_uv(bit.tile, 0.5 + corner.x * 0.125, 0.5 - corner.y * 0.125);
vertices.push(Vertex { p, normal, uv, sky_light(floor_cell(p)), -1.0 });
}
}
vertices
}
// --- shaders ----------------------------------------------------------------
// Every field begins on a 16-byte boundary, exactly as GLSL std140 wants.
// eye.w is time; target.w is mining progress, or -1 if nothing is targeted.
struct Scene {
view_proj: gfx::mat4,
eye: float4,
target: float4,
lights: float4[MAX_LAMPS],
haze: float4,
sun: float4, // xyz: direction, w: daylight strength
}
let scene_uniform = """
layout(set = 3, binding = 0) uniform Scene {
mat4 view_proj;
vec4 eye;
vec4 target;
vec4 lights[16];
vec4 haze;
vec4 sun;
};
""";
let world_vertex = """
layout(location = 0) in vec3 a_pos;
layout(location = 1) in vec3 a_normal;
layout(location = 2) in vec2 a_uv;
layout(location = 3) in float a_shade;
layout(location = 4) in float a_material;
layout(set = 1, binding = 0) uniform Camera { mat4 view_proj; };
layout(location = 0) out vec3 v_world;
layout(location = 1) out vec3 v_normal;
layout(location = 2) out vec2 v_uv;
layout(location = 3) out float v_shade;
layout(location = 4) flat out float v_material;
void main() {
v_world = a_pos;
v_normal = a_normal;
v_uv = a_uv;
v_shade = a_shade;
v_material = a_material;
gl_Position = view_proj * vec4(a_pos, 1.0);
}
""";
let world_fragment = """
layout(location = 0) in vec3 v_world;
layout(location = 1) in vec3 v_normal;
layout(location = 2) in vec2 v_uv;
layout(location = 3) in float v_shade;
layout(location = 4) flat in float v_material;
layout(set = 2, binding = 0) uniform sampler2D atlas;
layout(location = 0) out vec4 result;
void main() {
vec3 base = texture(atlas, v_uv).rgb;
float direct = max(dot(v_normal, sun.xyz), 0.0) * sun.w;
vec3 light = mix(vec3(0.10, 0.14, 0.23), vec3(0.64, 0.73, 0.82) * 0.48, sun.w);
light += vec3(0.66, 0.59, 0.43) * direct;
light *= v_shade;
for (int i = 0; i < 16; i++) {
float d = length(lights[i].xyz - v_world);
float glow = max(0.0, 1.0 - d / 7.0);
light += vec3(1.0, 0.63, 0.25) * glow * glow * lights[i].w;
}
if (v_material == 12.0) light = max(light, vec3(1.1, 0.96, 0.7));
if (v_material == 10.0) {
float wave = sin(v_world.x * 2.7 + eye.w) * sin(v_world.z * 3.1 - eye.w);
base += vec3(0.04, 0.07, 0.08) * wave;
light = max(light, vec3(mix(0.15, 0.72, sun.w)));
}
vec3 color = base * light;
vec3 cell = floor(v_world - v_normal * 0.002);
bool targeted = all(lessThan(abs(cell - target.xyz), vec3(0.1)));
if (v_material >= 0.0 && target.w >= 0.0 && targeted) {
vec3 local = fract(v_world);
vec3 edge = min(local, 1.0 - local) + abs(v_normal);
float border = min(edge.x, min(edge.y, edge.z));
if (border < 0.018) color = vec3(1.0, 0.91, 0.57);
// A progressively denser cracked pattern while holding mine.
vec3 crack = abs(fract(v_world * 5.0 + sin(v_world.zxy * 13.0) * 0.2) - 0.5);
if (target.w > 0.0 && min(crack.x + crack.y, crack.y + crack.z) < target.w * 0.18)
color *= 0.28;
}
float fog = smoothstep(34.0, 83.0, length(eye.xyz - v_world));
if (haze.w > 0.0) fog = 1.0 - exp(-length(eye.xyz - v_world) * 0.17);
result = vec4(mix(color, haze.rgb, fog), 1.0);
}
""";
let sky_vertex = """
layout(location = 0) out vec2 v_uv;
void main() {
vec2 p = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2));
v_uv = p;
gl_Position = vec4(p * 2.0 - 1.0, 0.9999, 1.0);
}
""";
struct Sky { ahead: float4, right: float4, up: float4, info: float4, sun: float4, haze: float4 }
let sky_fragment = """
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 result;
layout(set = 3, binding = 0) uniform Sky {
vec4 ahead; vec4 right; vec4 up; vec4 info; vec4 sun; vec4 haze;
};
void main() {
vec2 p = v_uv * 2.0 - 1.0;
vec3 ray = normalize(ahead.xyz + right.xyz * p.x * info.x + up.xyz * p.y);
vec3 zenith = mix(vec3(0.008, 0.015, 0.045), vec3(0.29, 0.57, 0.79), sun.w);
vec3 color = mix(haze.rgb, zenith,
clamp(ray.y * 1.4, 0.0, 1.0));
float glow = pow(max(dot(ray, sun.xyz), 0.0), 32.0);
color += vec3(0.18, 0.14, 0.05) * glow * sun.w;
if (ray.y > 0.0 && dot(ray, sun.xyz) > 0.9975) color = vec3(1.0, 0.95, 0.72);
if (ray.y > 0.0 && dot(ray, -sun.xyz) > 0.9985) color = vec3(0.72, 0.79, 0.9);
// A sparse hash of direction cells makes stars stay fixed as we turn.
vec3 star_cell = floor(ray * 220.0);
float star = fract(sin(dot(star_cell, vec3(12.9898, 78.233, 37.719))) * 43758.5453);
if (ray.y > 0.12 && star > 0.9985) color += vec3(0.65) * (1.0 - sun.w);
if (ray.y > 0.08) {
vec2 p = ray.xz * (30.0 / ray.y) + vec2(info.y * 0.22, 0.0);
vec2 grid = floor(p / 5.0);
float n = sin(grid.x * 1.7 + sin(grid.y * 2.3)) + cos(grid.y * 1.2);
vec3 cloud = mix(vec3(0.09, 0.12, 0.20), vec3(0.95, 0.96, 0.92), sun.w);
if (n > 0.8) color = mix(color, cloud, smoothstep(0.08, 0.3, ray.y));
}
if (info.z > 0.0) color = vec3(0.08, 0.28, 0.38);
result = vec4(color, 1.0);
}
""";
// --- a tiny immediate-mode HUD, also drawn with gfx ---------------------------
// A 5 x 7 font is enough for this sample. Each row is a five-bit mask.
// No font file, font rasterizer, or text texture is needed: set bits become
// little rectangles in the same batch as the panels and inventory icons.
fn glyph(c: u8) -> u8[7] {
match c {
'A' => [14, 17, 17, 31, 17, 17, 17], 'B' => [30, 17, 17, 30, 17, 17, 30],
'C' => [14, 17, 16, 16, 16, 17, 14], 'D' => [30, 17, 17, 17, 17, 17, 30],
'E' => [31, 16, 16, 30, 16, 16, 31], 'F' => [31, 16, 16, 30, 16, 16, 16],
'G' => [14, 17, 16, 23, 17, 17, 15], 'H' => [17, 17, 17, 31, 17, 17, 17],
'I' => [14, 4, 4, 4, 4, 4, 14], 'J' => [7, 2, 2, 2, 18, 18, 12],
'K' => [17, 18, 20, 24, 20, 18, 17], 'L' => [16, 16, 16, 16, 16, 16, 31],
'M' => [17, 27, 21, 21, 17, 17, 17], 'N' => [17, 25, 21, 19, 17, 17, 17],
'O' => [14, 17, 17, 17, 17, 17, 14], 'P' => [30, 17, 17, 30, 16, 16, 16],
'Q' => [14, 17, 17, 17, 21, 18, 13], 'R' => [30, 17, 17, 30, 20, 18, 17],
'S' => [15, 16, 16, 14, 1, 1, 30], 'T' => [31, 4, 4, 4, 4, 4, 4],
'U' => [17, 17, 17, 17, 17, 17, 14], 'V' => [17, 17, 17, 17, 17, 10, 4],
'W' => [17, 17, 17, 21, 21, 21, 10], 'X' => [17, 17, 10, 4, 10, 17, 17],
'Y' => [17, 17, 10, 4, 4, 4, 4], 'Z' => [31, 1, 2, 4, 8, 16, 31],
'0' => [14, 17, 19, 21, 25, 17, 14], '1' => [4, 12, 4, 4, 4, 4, 14],
'2' => [14, 17, 1, 2, 4, 8, 31], '3' => [30, 1, 1, 14, 1, 1, 30],
'4' => [2, 6, 10, 18, 31, 2, 2], '5' => [31, 16, 16, 30, 1, 1, 30],
'6' => [14, 16, 16, 30, 17, 17, 14], '7' => [31, 1, 2, 4, 8, 8, 8],
'8' => [14, 17, 17, 14, 17, 17, 14], '9' => [14, 17, 17, 15, 1, 1, 14],
'-' => [0, 0, 0, 31, 0, 0, 0], '+' => [0, 4, 4, 31, 4, 4, 0],
'.' => [0, 0, 0, 0, 0, 12, 12], ':' => [0, 12, 12, 0, 12, 12, 0],
'/' => [1, 2, 2, 4, 8, 8, 16], '(' => [2, 4, 8, 8, 8, 4, 2],
')' => [8, 4, 2, 2, 2, 4, 8], _ => [0; 7],
}
}
struct HudVertex { pos: float2, uv: float2, color: u8[4] }
let INK: u8[4] = [235, 237, 223, 255];
let MUTED: u8[4] = [166, 183, 181, 255];
let GOLD: u8[4] = [250, 209, 116, 255];
let PANEL: u8[4] = [22, 34, 40, 225];
let WHITE: u8[4] = [255, 255, 255, 255];
fn quad(out: HudVertex[>..]&, x: f32, y: f32, w: f32, h: f32, color: u8[4], tile: i64) {
let a = HudVertex { float2 { x, y }, atlas_uv(tile, 0.0, 0.0), color };
let b = HudVertex { float2 { x + w, y }, atlas_uv(tile, 1.0, 0.0), color };
let c = HudVertex { float2 { x + w, y + h }, atlas_uv(tile, 1.0, 1.0), color };
let d = HudVertex { float2 { x, y + h }, atlas_uv(tile, 0.0, 1.0), color };
out.append([a, b, c, a, c, d]);
}
fn rect(out: HudVertex[>..]&, x: f32, y: f32, w: f32, h: f32, color: u8[4]) {
quad(out, x, y, w, h, color, WHITE_TILE);
}
fn text(out: HudVertex[>..]&, x: f32, y: f32, words: const u8[:], color: u8[4], scale: f32) {
var at = x;
for c in words {
let rows = glyph(c);
for row in 7 {
for col in 5 {
if (rows[row] & (1 << (4 - col))) != 0 {
rect(out, at + col * scale, y + row * scale, scale, scale, color);
}
}
}
at += 6.0 * scale;
}
}
fn centred(out: HudVertex[>..]&, middle: f32, y: f32, words: const u8[:], color: u8[4]) {
text(out, middle - words.len * 6.0, y, words, color, 2.0);
}
fn next_goal() -> const u8[:] {
if game.tool == 0 && game.bag[2] < 2 && game.bag[3] < 5 { return "GATHER TWO WOOD BLOCKS"; }
if game.tool == 0 { return "C - MAKE PLANKS AND A WOODEN PICK"; }
if game.tool == 1 { return "MINE THREE STONE - C TO UPGRADE"; }
if game.bag[6] == 0 && lamp_count == 0 { return "FIND COAL - C TO CRAFT LAMPS"; }
"BUILD A CABIN - LIGHT YOUR CAVE"
}
fn block_name(b: u8) -> const u8[:] {
if b == BEDROCK { return "BEDROCK - UNBREAKABLE"; }
if (b == STONE || b == COAL || b == COBBLE) && game.tool == 0 { return "NEEDS A PICK - PRESS C"; }
if b == GRASS { return "GRASS"; }
if b == COAL { return "COAL ORE"; }
let slot = drop_slot(b);
if slot >= 0 { return SLOT_NAME[slot]; }
""
}
fn hud(out: HudVertex[>..]&, size: float2, hit: Hit, progress: f32,
crafting: bool, paused: bool, help: bool) {
rect(out, 18.0, 18.0, 418.0, 99.0, PANEL);
rect(out, 18.0, 18.0, 4.0, 99.0, GOLD);
text(out, 34.0, 31.0, "MINI MINECRAFT", GOLD, 2.0);
text(out, 34.0, 56.0, next_goal(), INK, 1.5);
let tool = if game.tool == 0 { "HANDS" } else if game.tool == 1 { "WOODEN PICK" } else { "STONE PICK" };
text(out, 34.0, 79.0, str(tool, " / SEED ", game.seed), MUTED, 1.0);
let phase = if daylight() < 0.2 { "NIGHT - LIGHT YOUR HOME" }
else if day_time > 450.0 { "DAWN" }
else if day_time > 270.0 { "DUSK - CRAFT SOME LAMPS" }
else if day_time < 150.0 { "MORNING" } else { "AFTERNOON" };
text(out, 34.0, 95.0, phase, MUTED, 1.0);
if help && !crafting && !paused {
let x = size.x - 316.0;
rect(out, x, 18.0, 298.0, 192.0, PANEL);
text(out, x + 14.0, 31.0, "MAKE YOURSELF AT HOME", GOLD, 1.5);
let lines: const u8[:][9] = [
"WASD WALK / SPACE JUMP OR SWIM", "SHIFT RUN / ARROWS LOOK",
"CTRL SNEAK - STOPS AT LEDGES",
"MOUSE LOOK / LEFT HOLD MINE", "RIGHT OR E PLACE - HOLD TO BUILD",
"MIDDLE CLICK PICKS MATERIAL",
"1-8 OR WHEEL SELECT / C CRAFT", "F5 SAVE / F9 LOAD / F12 PHOTO",
"ESC PAUSE / H HIDE HELP" ];
for i in lines.len { text(out, x + 14.0, 58.0 + i * 16.0, lines[i], INK, 1.0); }
} else if !crafting && !paused { text(out, size.x - 132.0, 26.0, "H - HELP", INK, 1.5); }
let bar_x = (size.x - SLOTS * 66.0) * 0.5;
let bar_y = size.y - 82.0;
for i in SLOTS {
let x = bar_x + i * 66.0;
rect(out, x, bar_y, 62.0, 64.0, if game.selected == i { GOLD } else { PANEL });
rect(out, x + 3.0, bar_y + 3.0, 56.0, 58.0, PANEL);
let tile = if i == 7 { 10 } else { tile_for(SLOT_BLOCK[i], 4) };
let tint: u8[4] = if game.bag[i] == 0 { [105, 110, 113, 255] } else { WHITE };
quad(out, x + 17.0, bar_y + 8.0, 28.0, 28.0, tint, tile);
text(out, x + 7.0, bar_y + 8.0, str(i + 1), GOLD, 1.0);
text(out, x + 7.0, bar_y + 40.0, SLOT_NAME[i], MUTED, 1.0);
text(out, x + 7.0, bar_y + 51.0, str(game.bag[i]), INK, 1.0);
}
centred(out, size.x * 0.5, bar_y - 25.0, SLOT_NAME[game.selected], INK);
if !crafting && !paused {
let cx = size.x * 0.5;
let cy = size.y * 0.5;
rect(out, cx - 7.0, cy - 2.0, 15.0, 5.0, PANEL);
rect(out, cx - 2.0, cy - 7.0, 5.0, 15.0, PANEL);
rect(out, cx - 6.0, cy, 13.0, 1.0, INK);
rect(out, cx, cy - 6.0, 1.0, 13.0, INK);
if hit.found {
let label = block_name(block_at(hit.at));
text(out, cx - label.len * 3.0, cy + 22.0, label, INK, 1.0);
if progress > 0.0 {
rect(out, cx - 40.0, cy + 37.0, 80.0, 5.0, PANEL);
rect(out, cx - 40.0, cy + 37.0, 80.0 * progress, 5.0, GOLD);
}
}
}
if message_time > 0.0 {
let width: f32 = message.len * 9.0 + 28.0;
rect(out, (size.x - width) * 0.5, bar_y - 65.0, width, 28.0, PANEL);
text(out, (size.x - width) * 0.5 + 14.0, bar_y - 57.0, message, GOLD, 1.5);
}
if crafting {
let x = (size.x - 504.0) * 0.5;
let y = (size.y - 300.0) * 0.5;
rect(out, x, y, 504.0, 300.0, PANEL);
rect(out, x, y, 504.0, 3.0, GOLD);
text(out, x + 22.0, y + 20.0, "CRAFTING", GOLD, 2.0);
text(out, x + 22.0, y + 48.0, "CLICK A RECIPE OR PRESS ITS NUMBER", MUTED, 1.5);
let titles: const u8[:][4] = ["1 FOUR PLANKS", "2 WOODEN PICK", "3 STONE PICK", "4 FOUR LAMPS"];
let costs: const u8[:][4] = ["1 WOOD", "5 PLANKS", "3 STONE + 2 PLANKS + WOODEN PICK",
"1 COAL + 1 PLANK"];
for i in 4 {
let row_y = y + 78.0 + i * 46.0;
rect(out, x + 16.0, row_y, 472.0, 40.0, [42, 57, 60, 250]);
let ready = can_craft(i);
text(out, x + 28.0, row_y + 7.0, titles[i], if ready { INK } else { MUTED }, 1.5);
let owned = (i == 1 && game.tool >= 1) || (i == 2 && game.tool >= 2);
text(out, x + 424.0, row_y + 8.0, if ready { "READY" } else if owned { "OWNED" } else { "NEED" },
if ready { GOLD } else { MUTED }, 1.0);
text(out, x + 28.0, row_y + 26.0, costs[i], MUTED, 1.0);
}
text(out, x + 22.0, y + 264.0, "HOLD SHIFT WITH A RECIPE TO CRAFT ALL.", GOLD, 1.0);
text(out, x + 22.0, y + 280.0, "TOOLS EQUIP AUTOMATICALLY. C OR ESC CLOSES.", MUTED, 1.0);
} else if paused {
rect(out, (size.x - 400.0) * 0.5, size.y * 0.5 - 45.0, 400.0, 90.0, PANEL);
centred(out, size.x * 0.5, size.y * 0.5 - 26.0, "PAUSED", GOLD);
text(out, size.x * 0.5 - 162.0, size.y * 0.5 + 4.0,
"CLICK OR ESC TO RESUME. F5 SAVES.", INK, 1.5);
}
}
let hud_vertex = """
layout(location = 0) in vec2 a_pos;
layout(location = 1) in vec2 a_uv;
layout(location = 2) in vec4 a_color;
layout(set = 1, binding = 0) uniform Screen { vec4 size; };
layout(location = 0) out vec2 v_uv;
layout(location = 1) out vec4 v_color;
void main() {
v_uv = a_uv; v_color = a_color;
gl_Position = vec4(a_pos.x / size.x * 2.0 - 1.0, 1.0 - a_pos.y / size.y * 2.0, 0.0, 1.0);
}
""";
let hud_fragment = """
layout(location = 0) in vec2 v_uv;
layout(location = 1) in vec4 v_color;
layout(set = 2, binding = 0) uniform sampler2D atlas;
layout(location = 0) out vec4 result;
void main() { result = texture(atlas, v_uv) * v_color; }
""";
// --- gameplay checks, with no window -----------------------------------------
fn world_checksum() -> u32 {
var h: u32 = 2166136261;
for b in blocks { h = (h ^ b) * 16777619; }
h
}
fn gameplay_tests() {
generate(123);
let checksum = world_checksum();
assert(!blocked(game.player.feet));
assert(valid_save(game, blocks));
assert(daylight() == 1.0 && sun_direction().y > 0.0);
day_time = 450.0;
assert(daylight() == 0.0 && sun_direction().y < 0.0);
day_time = DAY_LENGTH - 0.25;
advance_time(0.5);
assert(abs(day_time - 0.25) < 0.001);
generate(123);
assert(checksum == world_checksum());
var counts: i64[13] = [0; 13];
for b in blocks { counts[b]++; }
assert(counts[WOOD] > 20 && counts[WATER] > 20 && counts[COAL] > 20);
generate(321);
assert(checksum != world_checksum());
// A small, controlled arena isolates movement and interaction from the
// terrain generator. All checks call the same functions as the game.
game = Game { version: 1, seed: 7, .. };
for i in VOLUME { blocks[i] = AIR; }
for z in DEPTH {
for x in WIDTH { put(cell(x, 0, z), BEDROCK); }
}
game.player = Player { feet: float3 { 10.5, 1.001, 10.5 }, .. };
put(cell(10, 2, 7), WOOD);
let hit = ray(eye(), float3 { 0.0, 0.0, -1.0 }, 6.0);
assert(hit.found && hit.at == cell(10, 2, 7) && hit.before == cell(10, 2, 8));
assert(!ray(eye(), float3 { 1.0, 0.0, 0.0 }, 6.0).found);
assert(mine(hit.at) && game.bag[2] == 1 && block_at(hit.at) == AIR);
assert(debris[0].life > 0.0);
animate_debris(1.0);
for bit in debris { assert(bit.life <= 0.0); }
assert(!mine(cell(10, 0, 10)));
assert(craft(0) && game.bag[3] == 4 && game.bag[2] == 0);
assert(!craft(1));
game.bag[2] = 1;
assert(craft(0) && craft(1) && game.tool == 1 && game.bag[3] == 3);
for x in 3 { put(cell(15 + x, 1, 10), STONE); assert(mine(cell(15 + x, 1, 10))); }
assert(craft(2) && game.tool == 2 && game.bag[1] == 0 && game.bag[3] == 1);
put(cell(15, 1, 10), COAL);
assert(mine(cell(15, 1, 10)) && craft(3) && game.bag[6] == 4);
game.selected = 6;
assert(place(hit) && block_at(hit.before) == LAMP && lamp_count == 1);
game.selected = 0;
pick_material(Hit { true, hit.before, hit.at, 1.0 });
assert(game.selected == 6 && game.bag[6] == 3);
assert(!place(hit)); // Cannot place twice in the same cell.
let self_hit = Hit { true, cell(10, 1, 9), cell(10, 1, 10), 1.0 };
assert(!place(self_hit)); // Cannot trap the player in a block.
assert(mine(hit.before) && lamp_count == 0);
for i in 120 { walk(float2_0, false, false); }
assert(game.player.grounded && abs(game.player.feet.y - 1.0) < 0.005);
var peak: f32 = 0.0;
for i in 180 {
walk(float2_0, i == 0, false);
peak = max(peak, game.player.feet.y);
}
assert(peak > 2.1 && game.player.grounded);
for y in 1..5 { put(cell(11, y, 10), STONE); }
for i in 120 { walk(float2 { 1.0, 0.0 }, false, true); }
assert(game.player.feet.x < 10.711 && !blocked(game.player.feet));
assert(blocked(float3 { 0.0, 1.0, 2.0 }));
// A low ceiling stops a jump; swimming can climb a one-block shore.
put(cell(10, 3, 10), STONE);
for i in 40 { walk(float2_0, i == 0, false); }
assert(game.player.feet.y < 1.201 && !blocked(game.player.feet));
game.player = Player { feet: float3 { 30.5, 1.0, 30.5 }, .. };
for y in 1..4 {
for x in 29..32 { put(cell(x, y, 30), WATER); }
put(cell(32, y, 30), SAND);
}
put(cell(32, 4, 30), SAND);
for i in 240 { walk(float2 { 1.0, 0.0 }, true, false); }
assert(game.player.feet.x > 32.5 && !blocked(game.player.feet));
game.player = Player { feet: float3 { 30.5, 1.0, 30.5 }, .. };
for i in 240 { walk(float2 { 1.0, 0.0 }, true, false, true); }
assert(game.player.feet.x > 32.5 && !blocked(game.player.feet));
// Sneaking holds every edge and corner of a one-block platform, even
// if run is held too. Jumping or releasing sneak still lets us leave.
put(cell(60, 3, 60), STONE);
let directions: float2[8] = [
float2 { 1.0, 0.0 }, float2 { -1.0, 0.0 },
float2 { 0.0, 1.0 }, float2 { 0.0, -1.0 },
float2 { 1.0, 1.0 }, float2 { -1.0, 1.0 },
float2 { 1.0, -1.0 }, float2 { -1.0, -1.0 } ];
for direction in directions {
game.player = Player { feet: float3 { 60.5, 4.001, 60.5 }, grounded: true, .. };
for i in 240 { walk(direction, false, true, true); }
assert(game.player.feet.y > 3.999 && game.player.grounded && supported(game.player.feet));
}
for i in 120 { walk(float2 { -1.0, -1.0 }, false, false); }
assert(game.player.feet.y < 3.0);
game.player = Player { feet: float3 { 60.5, 4.001, 60.5 }, grounded: true, .. };
for i in 120 { walk(float2 { 1.0, 0.0 }, i == 0, false, true); }
assert(game.player.feet.x > 61.3 && game.player.feet.y < 3.95);
game.player = Player { feet: float3 { 60.5, 4.001, 60.5 }, grounded: true, .. };
put(cell(60, 3, 60), AIR);
for i in 120 { walk(float2_0, false, false, true); }
assert(game.player.feet.y < 1.01); // Mining the floor still makes us fall.
// Bulk crafting consumes only complete recipes, respects stack space,
// and buys a permanent tool once. Ordinary crafting still makes one batch.
let saved_inventory = game.bag;
let saved_tool = game.tool;
game.bag[2] = 3;
game.bag[3] = 0;
assert(craft_batches(0, false) == 1 && game.bag[2] == 2 && game.bag[3] == 4);
assert(craft_batches(0, true) == 2 && game.bag[2] == 0 && game.bag[3] == 12);
game.bag[2] = 3;
game.bag[3] = 994;
assert(craft_batches(0, true) == 1 && game.bag[2] == 2 && game.bag[3] == 998);
assert(craft_batches(0, true) == 0 && game.bag[2] == 2);
game.bag[7] = 3;
game.bag[3] = 2;
game.bag[6] = 0;
assert(craft_batches(3, true) == 2 && game.bag[7] == 1 && game.bag[3] == 0 && game.bag[6] == 8);
game.tool = 0;
game.bag[3] = 10;
assert(craft_batches(1, true) == 1 && game.tool == 1 && game.bag[3] == 5);
assert(craft_batches(1, true) == 0 && game.bag[3] == 5);
game.bag[1] = 6;
assert(craft_batches(2, true) == 1 && game.tool == 2 && game.bag[1] == 3 && game.bag[3] == 3);
game.bag = saved_inventory;
game.tool = saved_tool;
// Out-of-range reach, negative directions, and chunk-border edits.
put(cell(30, 8, 30), STONE);
assert(!ray(float3 { 20.5, 8.5, 30.5 }, float3 { 1.0, 0.0, 0.0 }, 6.0).found);
let back = ray(float3 { 33.5, 8.5, 30.5 }, float3 { -1.0, 0.0, 0.0 }, 6.0);
assert(back.found && back.before == cell(31, 8, 30));
for i in meshes.len { meshes[i].dirty = false; }
edit(cell(15, 8, 15), PLANKS);
assert(meshes[0].dirty && meshes[1].dirty && meshes[CHUNKS_X].dirty && meshes[CHUNKS_X + 1].dirty);
game.selected = 6;
game.bag[6] = 20;
for i in MAX_LAMPS { edit(cell(i + 1, 2, 1), LAMP); }
assert(lamp_count == MAX_LAMPS && !place(back));
assert(game.bag[6] == 20);
game.bag[2] = 999;
put(cell(20, 2, 20), WOOD);
assert(!mine(cell(20, 2, 20)) && block_at(cell(20, 2, 20)) == WOOD);
day_time = 450.0;
var image = save_image();
let saved_checksum = world_checksum();
let saved_bag = game.bag;
blocks[0] = AIR;
game.bag[0] = 123;
day_time = 90.0;
assert(restore(image) && valid_save(game, blocks));
assert(world_checksum() == saved_checksum && game.bag == saved_bag);
assert(day_time == 450.0);
// Non-finite/out-of-range clocks cannot replace a good world.
day_time = -1.0;
let bad_clock = save_image();
day_time = 450.0;
assert(!restore(bad_clock) && day_time == 450.0);
let grid_start = 4 + to_bytes([Game { .. }]).len;
var legacy: u8[>..] = [];
legacy.append(image[..grid_start + VOLUME]);
legacy[3] = '1';
assert(restore(legacy) && day_time == 90.0);
assert(world_checksum() == saved_checksum && game.bag == saved_bag);
assert(!restore(image[..image.len - 1]));
assert(!restore(image[..2]));
image[0] = 'X';
assert(!restore(image) && world_checksum() == saved_checksum);
image[0] = 'G';
image[grid_start] = AIR;
assert(!restore(image));
game.selected = 100;
assert(!valid_save(game, blocks));
print("mini minecraft: terrain, ray, mining, crafting, placement, movement, daylight and saves passed");
}
// --- the program: input, simulation, mesh updates, and drawing ---------------
fn main() {
var seed: u32 = 2026;
var frames = -1;
var test = false;
let argv = args();
var a = 1;
while a < argv.len {
if argv[a] == "--test" { test = true; a++; continue; }
if a + 1 >= argv.len || (argv[a] != "--seed" && argv[a] != "--frames") {
write_stderr("usage: mini_minecraft [--seed N] [--frames N] [--test]\n");
exit(1);
}
let n, ok = parse_int(argv[a + 1]);
if !ok || n < 0 || n > 2147483647 || (argv[a] == "--frames" && n == 0) {
write_stderr("seed must be 0..2147483647; frames must be 1..2147483647\n");
exit(1);
}
if argv[a] == "--seed" { seed = n as u32; } else { frames = n; }
a += 2;
}
if test {
gameplay_tests();
if frames < 0 { return; }
}
generate(seed);
guard gfx::open("Goose: mini Minecraft", 1280, 800, gfx::WINDOW_RESIZABLE) else {
write_stderr(str("gfx: ", gfx::error(), "\n"));
return;
}
let atlas = make_atlas();
let sampler = gfx::sampler(gfx::NEAREST, gfx::CLAMP);
let world_pipeline = gfx::pipeline(embed_shader("vert", world_vertex),
embed_shader("frag", scene_uniform, world_fragment),
gfx::PipelineDesc { cull: gfx::CULL_BACK, depth_test: true, depth_write: true });
let sky_pipeline = gfx::pipeline(embed_shader("vert", sky_vertex), embed_shader("frag", sky_fragment));
var hud_desc = gfx::PipelineDesc { blend: gfx::BLEND_ALPHA };
hud_desc.formats[2] = gfx::VERTEX_UBYTE4_NORM;
let hud_pipeline = gfx::pipeline(embed_shader("vert", hud_vertex),
embed_shader("frag", hud_fragment), hud_desc);
if sampler.id == 0 || world_pipeline.id == 0 || sky_pipeline.id == 0 || hud_pipeline.id == 0 {
abort(str("renderer: ", gfx::error()));
}
rebuild_dirty();
var hud_buffer = gfx::Buffer { 0 };
// This small fixed array sets the buffer's capacity once. Each frame
// uploads only the live fragments; the GPU buffer is reused throughout.
let empty_debris: Vertex[MAX_DEBRIS * 6] = [Vertex { .. }; MAX_DEBRIS * 6];
let debris_buffer = gfx::vertex_buffer(bytes_of(empty_debris));
if debris_buffer.id == 0 { abort(str("fragment buffer: ", gfx::error())); }
var crafting = false;
var paused = !gfx::focused();
var help = true;
var drawn = 0;
var elapsed: f32 = 0.0;
var accumulator: f32 = 0.0;
var progress: f32 = 0.0;
var mining_at = cell(-1, -1, -1);
var place_delay: f32 = 0.0;
var wait_for_buttons = false;
let keys: const u8[:][SLOTS] = ["1", "2", "3", "4", "5", "6", "7", "8"];
if !paused && !gfx::set_mouse_relative(true) { say("MOUSE CAPTURE FAILED - USE ARROW KEYS TO LOOK"); }
while gfx::frame() {
let pixels = gfx::screen_size();
if pixels.x <= 0 || pixels.y <= 0 {
paused = true;
crafting = false;
progress = 0.0;
accumulator = 0.0;
gfx::set_mouse_relative(false);
continue;
}
// A logical canvas keeps the HUD legible on high-density screens.
let scale = max(0.5, min<f32>(pixels.x / 960.0, pixels.y / 600.0));
let screen = float2 { pixels.x / scale, pixels.y / scale };
let mouse = gfx::mouse_pos() / float2 { scale, scale };
let dt: f32 = if frames > 0 { 1.0 / 60.0 } else { min(gfx::delta_time(), 0.05) as f32 };
message_time = max(0.0, message_time - dt);
let was_playing = !paused && !crafting;
let was_crafting = crafting;
// A loss followed by a gain in the same frame must still pause.
// gfx clears held input and releases capture on every focus loss.
var lost_focus = !gfx::focused();
for event in gfx::events() {
if event.kind == gfx::EVENT_FOCUS_LOST { lost_focus = true; }
}
if lost_focus { paused = true; crafting = false; }
else {
if gfx::key_pressed("H") { help = !help; }
if gfx::key_pressed("Escape") {
if crafting { crafting = false; } else { paused = !paused; }
} else if paused && gfx::mouse_pressed(gfx::MOUSE_LEFT) { paused = false; }
else if gfx::key_pressed("C") && !paused { crafting = !crafting; }
if gfx::key_pressed("F5") { save_game(); }
if gfx::key_pressed("F9") { load_game(); progress = 0.0; accumulator = 0.0; }
}
let playing = !paused && !crafting;
if playing != was_playing {
// The default visible cursor returns in menus. Changing capture
// also discards stale motion, so reopening the game cannot jump.
if !gfx::set_mouse_relative(playing) && playing {
say("MOUSE CAPTURE FAILED - USE ARROW KEYS TO LOOK");
}
wait_for_buttons = true;
}
// A resume/menu click must be released before it can mine or build.
if !gfx::mouse_down(gfx::MOUSE_LEFT) && !gfx::mouse_down(gfx::MOUSE_RIGHT) {
wait_for_buttons = false;
}
if crafting && was_crafting {
let x = (screen.x - 504.0) * 0.5;
let y = (screen.y - 300.0) * 0.5 + 78.0;
for i in 4 {
let row_y = y + i * 46.0;
let clicked = gfx::mouse_pressed(gfx::MOUSE_LEFT) && mouse.x >= x + 16.0 &&
mouse.x < x + 488.0 && mouse.y >= row_y && mouse.y < row_y + 40.0;
if clicked || gfx::key_pressed(keys[i]) {
craft_batches(i, gfx::key_down("Left Shift") || gfx::key_down("Right Shift"));
}
}
}
if playing && was_playing {
elapsed += dt;
advance_time(dt);
animate_debris(dt);
for i in SLOTS { if gfx::key_pressed(keys[i]) { game.selected = i as i32; } }
let wheel = gfx::mouse_wheel();
if wheel != 0.0 {
let step = if wheel > 0.0 { -1 } else { 1 };
game.selected = ((game.selected + step + SLOTS) % SLOTS) as i32;
}
if gfx::mouse_relative() {
let delta = gfx::mouse_delta();
game.player.yaw += delta.x * 0.003;
game.player.pitch -= delta.y * 0.003;
}
if gfx::key_down("Left") { game.player.yaw -= dt * 1.8; }
if gfx::key_down("Right") { game.player.yaw += dt * 1.8; }
if gfx::key_down("Up") { game.player.pitch += dt * 1.5; }
if gfx::key_down("Down") { game.player.pitch -= dt * 1.5; }
game.player.pitch = clamp(game.player.pitch, -1.5, 1.5);
game.player.yaw = (game.player.yaw + PI) % TAU - PI;
var direction = float2_0;
if gfx::key_down("W") { direction.y += 1.0; }
if gfx::key_down("S") { direction.y -= 1.0; }
if gfx::key_down("D") { direction.x += 1.0; }
if gfx::key_down("A") { direction.x -= 1.0; }
accumulator += dt;
let sneak = gfx::key_down("Left Ctrl") || gfx::key_down("Right Ctrl");
while accumulator >= STEP {
walk(direction, gfx::key_down("Space"), gfx::key_down("Left Shift"), sneak);
accumulator -= STEP;
}
} else { accumulator = 0.0; }
var hit = ray(eye(), forward(), 6.0);
if playing && was_playing && !wait_for_buttons {
if gfx::mouse_pressed(gfx::MOUSE_MIDDLE) { pick_material(hit); }
if hit.found && gfx::mouse_down(gfx::MOUSE_LEFT) {
if hit.at != mining_at { progress = 0.0; mining_at = hit.at; }
let seconds = mining_seconds(block_at(hit.at));
if seconds > 0.0 { progress += dt / seconds; } else { progress = 0.0; }
if progress >= 1.0 {
mine(hit.at);
progress = 0.0;
hit = ray(eye(), forward(), 6.0);
}
} else { progress = 0.0; mining_at = cell(-1, -1, -1); }
// Edges include a press and release between frames. Held input
// repeats slowly enough to build a wall without emptying a stack.
place_delay = max(0.0, place_delay - dt);
if gfx::key_pressed("E") || gfx::mouse_pressed(gfx::MOUSE_RIGHT) ||
(place_delay == 0.0 && (gfx::key_down("E") || gfx::mouse_down(gfx::MOUSE_RIGHT))) {
place(hit);
place_delay = 0.22;
hit = ray(eye(), forward(), 6.0);
}
} else { progress = 0.0; place_delay = 0.0; }
rebuild_dirty();
// The frame's temporary HUD array is freed at this iteration's end.
// Reuse its GPU buffer until growth needs a larger one.
var hud_vertices: HudVertex[>..] = [];
hud(hud_vertices, screen, hit, progress, crafting, paused, help);
let hud_bytes = bytes_of(hud_vertices);
if hud_buffer.id == 0 || gfx::buffer_size(hud_buffer) < hud_bytes.len {
if hud_buffer.id != 0 { gfx::release(hud_buffer); }
hud_buffer = gfx::buffer(gfx::BUFFER_VERTEX, hud_bytes.len * 2, hud_bytes);
if hud_buffer.id == 0 { abort(str("HUD buffer: ", gfx::error())); }
} else if !gfx::update_buffer(hud_buffer, hud_bytes) { abort(str("HUD upload: ", gfx::error())); }
let ahead = forward();
let right = normalize(cross(ahead, float3 { 0.0, 1.0, 0.0 }));
let up = cross(right, ahead);
let fragments = debris_vertices(right, up);
if fragments.len > 0 && !gfx::update_buffer(debris_buffer, bytes_of(fragments)) {
abort(str("fragment upload: ", gfx::error()));
}
let aspect = pixels.x as f32 / pixels.y;
let view = gfx::look_at(eye(), eye() + ahead, float3 { 0.0, 1.0, 0.0 });
let view_proj = gfx::mul(gfx::perspective(1.15, aspect, 0.05, 150.0), view);
let underwater = block_at(floor_cell(eye())) == WATER;
let sky_color = horizon();
let haze = if underwater { float4 { 0.08, 0.28, 0.38, 1.0 } }
else { float4 { sky_color.x, sky_color.y, sky_color.z, 0.0 } };
let sun_dir = sun_direction();
let sun = float4 { sun_dir.x, sun_dir.y, sun_dir.z, daylight() };
let target = if hit.found && !crafting && !paused {
float4 { hit.at.x, hit.at.y, hit.at.z, progress }
} else { float4 { 0.0, 0.0, 0.0, -1.0 } };
let camera = eye();
let scene = Scene { view_proj, float4 { camera.x, camera.y, camera.z, elapsed },
target, lamps, haze, sun };
let lens = tan(1.15 as f32 * 0.5);
let sky = Sky {
float4 { ahead.x / lens, ahead.y / lens, ahead.z / lens, 0.0 },
float4 { right.x, right.y, right.z, 0.0 },
float4 { up.x, up.y, up.z, 0.0 },
float4 { aspect, elapsed, if underwater { 1.0 } else { 0.0 }, 0.0 }, sun, haze };
gfx::begin_screen(haze);
gfx::bind(sky_pipeline);
gfx::uniforms(gfx::FRAGMENT, 0, sky);
gfx::draw(3);
gfx::bind(world_pipeline);
gfx::bind_texture(gfx::FRAGMENT, 0, atlas, sampler);
gfx::uniforms(gfx::VERTEX, 0, view_proj);
gfx::uniforms(gfx::FRAGMENT, 0, scene);
for m in meshes {
if m.count == 0 { continue; }
gfx::bind_vertex_buffer(m.vertices);
gfx::bind_index_buffer(m.indices, 4);
gfx::draw_indexed(m.count);
}
if fragments.len > 0 {
gfx::bind_vertex_buffer(debris_buffer);
gfx::draw(fragments.len);
}
gfx::bind(hud_pipeline);
gfx::bind_texture(gfx::FRAGMENT, 0, atlas, sampler);
gfx::bind_vertex_buffer(hud_buffer);
gfx::uniforms(gfx::VERTEX, 0, float4 { screen.x, screen.y, 0.0, 0.0 });
gfx::draw(hud_vertices.len);
gfx::end_pass();
if gfx::key_pressed("F12") {
if gfx::screenshot("mini_minecraft.png") { say("SAVED MINI_MINECRAFT.PNG"); }
else { say("SCREENSHOT FAILED"); }
}
drawn++;
if drawn == frames { break; }
}
if frames > 0 {
let saved = gfx::screenshot("mini_minecraft.png");
print("drew ", drawn, " frames; saved mini_minecraft.png: ", saved);
}
// close releases every remaining GPU object, including chunk buffers.
gfx::close();
}