Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Prepare site
run: |
mkdir -p dist
cp -R index.html browser-phone dist/
cp -R index.html browser-phone minecraft dist/
cp CNAME dist/CNAME

- name: Setup Pages
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Subnautica-Inspired Underwater Slice

> **Also in this repo:** [`minecraft/`](minecraft/) — BlockCraft, a
> browser-based Minecraft clone built with three.js (procedural terrain,
> day/night cycle, block building). It is deployed with the GitHub Pages site
> at `/minecraft/`.

This repository is now a **Unity 6.3 LTS** prototype focused on building the look and feel of a
Subnautica-style underwater scene. The goal is a strong visual vertical slice rather than a full
survival sandbox.
Expand Down
59 changes: 59 additions & 0 deletions minecraft/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# BlockCraft

A Minecraft-style voxel sandbox that runs entirely in the browser. No build
step, no asset files — terrain, textures, sounds and the sky are all generated
procedurally at runtime.

Open `index.html` through any static file server (it uses ES modules, so the
`file://` protocol won't work):

```bash
cd minecraft
python3 -m http.server 8000
# then visit http://localhost:8000
```

## Features

- **Infinite procedural terrain** — chunked world streamed around the player,
with rolling hills, mountains, deserts, snow biomes, beaches, lakes and seas
- **Caves and ores** — 3D-noise carved caverns with coal and iron deposits
- **Trees** that generate across chunk borders deterministically
- **Block breaking and placing** with a 9-slot hotbar (keys 1–9, mouse wheel,
or middle-click to pick the block you're looking at)
- **Pixel-art texture atlas** painted onto a canvas at startup — grass, stone,
sand, logs, leaves, glass, water and more
- **Lighting** — baked ambient occlusion, per-face shading and a depth-based
darkness heuristic for caves
- **Day/night cycle** — square sun and moon, stars, drifting blocky clouds,
and dawn/dusk fog tinting
- **Physics** — AABB collision, jumping, swimming with an underwater fog
effect, and a creative-style fly mode (`F`)
- **Persistence** — your block edits are stored in `localStorage` per world
seed; use `?seed=...` in the URL to visit a specific world
- **Procedural audio** — break/place/splash sounds synthesized with WebAudio

## Controls

| Input | Action |
| ---------------- | --------------------- |
| `W A S D` | Move |
| Mouse | Look |
| Left click | Break block |
| Right click | Place block |
| Middle click | Pick block |
| `Space` | Jump / swim / fly up |
| `Shift` | Sprint / fly down |
| `F` | Toggle flying |
| `1`–`9` / wheel | Select hotbar slot |
| `Esc` | Pause menu |

## Tech notes

- [three.js](https://threejs.org) r165, vendored in `vendor/` so the game works
offline and the version is pinned
- One mesh per 16×16×96 chunk (plus a translucent mesh for water/glass), built
with face culling, per-vertex ambient occlusion and a texture atlas
- Terrain from seeded 2D simplex noise (continents, hills, mountain mask,
biomes) plus 3D value noise for caves
- Voxel raycasting uses the Amanatides–Woo DDA traversal
66 changes: 66 additions & 0 deletions minecraft/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BlockCraft — a browser voxel sandbox</title>
<link rel="stylesheet" href="./styles.css" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='6' fill='%237cbd4b'/%3E%3Crect y='6' width='16' height='10' fill='%23866043'/%3E%3C/svg%3E"
/>
<script type="importmap">
{
"imports": {
"three": "./vendor/three.module.min.js"
}
}
</script>
</head>
<body>
<canvas id="game"></canvas>

<div id="hud">
<div id="info"></div>
<div id="crosshair"></div>
<div id="status"></div>
<div id="hotbar"></div>
<div id="underwater"></div>
</div>

<div id="overlay">
<div class="panel">
<h1>BlockCraft</h1>
<p class="tagline">An infinite voxel sandbox that runs in your browser.</p>

<div class="controls">
<div><span class="key">W A S D</span> move</div>
<div><span class="key">Mouse</span> look around</div>
<div><span class="key">Left click</span> break block</div>
<div><span class="key">Right click</span> place block</div>
<div><span class="key">Middle click</span> pick block</div>
<div><span class="key">Space</span> jump / swim up</div>
<div><span class="key">Shift</span> sprint / fly down</div>
<div><span class="key">F</span> toggle flying</div>
<div><span class="key">1–9 / wheel</span> select block</div>
<div><span class="key">Esc</span> pause</div>
</div>

<button id="play-button" type="button">Play</button>

<div class="secondary-actions">
<button id="new-world-button" type="button">New world</button>
<button id="reset-button" type="button">Reset changes</button>
</div>

<p id="seed-label" class="seed"></p>
<p class="footnote">
Your block edits are saved locally per world seed. Built with three.js —
procedural terrain, textures and sounds, no assets required.
</p>
</div>
</div>

<script type="module" src="./js/main.js"></script>
</body>
</html>
81 changes: 81 additions & 0 deletions minecraft/js/audio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Tiny procedural sound effects via WebAudio; no audio assets needed.

let ctx = null;

function audioCtx() {
if (!ctx) {
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return null;
ctx = new AC();
}
if (ctx.state === 'suspended') ctx.resume();
return ctx;
}

function noiseBuffer(ac, seconds) {
const buffer = ac.createBuffer(1, Math.floor(ac.sampleRate * seconds), ac.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
return buffer;
}

export function playBreak() {
const ac = audioCtx();
if (!ac) return;
const t = ac.currentTime;

const src = ac.createBufferSource();
src.buffer = noiseBuffer(ac, 0.14);

const filter = ac.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(900, t);
filter.frequency.exponentialRampToValueAtTime(220, t + 0.13);

const gain = ac.createGain();
gain.gain.setValueAtTime(0.28, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.14);

src.connect(filter).connect(gain).connect(ac.destination);
src.start(t);
}

export function playPlace() {
const ac = audioCtx();
if (!ac) return;
const t = ac.currentTime;

const osc = ac.createOscillator();
osc.type = 'square';
osc.frequency.setValueAtTime(210, t);
osc.frequency.exponentialRampToValueAtTime(150, t + 0.07);

const gain = ac.createGain();
gain.gain.setValueAtTime(0.09, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.08);

osc.connect(gain).connect(ac.destination);
osc.start(t);
osc.stop(t + 0.09);
}

export function playSplash() {
const ac = audioCtx();
if (!ac) return;
const t = ac.currentTime;

const src = ac.createBufferSource();
src.buffer = noiseBuffer(ac, 0.25);

const filter = ac.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(700, t);
filter.Q.value = 0.8;

const gain = ac.createGain();
gain.gain.setValueAtTime(0.12, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.25);

src.connect(filter).connect(gain).connect(ac.destination);
src.start(t);
}
Loading