terainia/web/index.html
Maximus Gorog b52c1927cf Render + UI polish since pre-alpha-0.0.1
- Greedy meshing now bakes per-vertex AO with 4-corner sampling and an
  anisotropic-diagonal split when corner AO disagrees.
- WGSL: extracted sky_dome() for hemisphere ambient sampling so vertical
  faces match the sun-side sky tint at day; ambient_strength mixed by
  day strength instead of a flat constant.
- Step-1 post pipeline: render scene into an offscreen color texture,
  pass-through to the surface. Foundation for FXAA/shafts that will
  follow.
- Input bug: merge_held() now recomputes per tick from sticky keyboard +
  live touch bridge, so releasing the joystick actually stops the
  player (previous OR-into-self bug ate playtests).
- Touch UI hit-zones reordered (menu/hotbar above the joystick z-index);
  hotbar widened to 10 slots with tap-to-select on mobile.
- find_safe_spawn anchors on natural_surface_y so spawn is deterministic
  from noise — towers built at spawn no longer climb the spawn point.
- move_axis is sub-stepped (0.45-block max) so high-velocity falls can't
  teleport the player inside terrain.
2026-05-23 18:44:56 -06:00

637 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Voxel Game</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
<style>
:root {
--ui-bg: rgba(0,0,0,0.45);
--ui-fg: #f0f0f0;
}
html, body {
margin: 0;
height: 100%;
background: #1a1a1a;
color: var(--ui-fg);
font-family: system-ui, sans-serif;
overflow: hidden;
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
touch-action: none;
overscroll-behavior: none;
}
#game-canvas {
display: block;
width: 100vw;
height: 100dvh;
cursor: crosshair;
image-rendering: pixelated;
touch-action: none;
}
#hud {
position: fixed;
top: 12px;
left: 12px;
background: var(--ui-bg);
padding: 10px 14px;
border-radius: 6px;
font-size: 13px;
line-height: 1.5;
pointer-events: none;
}
#hud kbd {
background: #333;
border-radius: 3px;
padding: 1px 5px;
border: 1px solid #555;
font-size: 11px;
}
#crosshair {
position: fixed;
left: 50%; top: 50%;
width: 14px; height: 14px;
margin-left: -7px; margin-top: -7px;
pointer-events: none;
}
#crosshair::before, #crosshair::after {
content: "";
position: absolute;
background: #fff;
mix-blend-mode: difference;
}
#crosshair::before { left: 6px; top: 0; width: 2px; height: 14px; }
#crosshair::after { top: 6px; left: 0; height: 2px; width: 14px; }
/* Connection status / name */
#net-status {
position: fixed;
top: 12px; right: 12px;
background: var(--ui-bg);
padding: 6px 10px;
border-radius: 6px;
font-size: 12px;
display: flex;
gap: 8px;
align-items: center;
}
#net-status .dot {
width: 9px; height: 9px; border-radius: 50%;
background: #d33; transition: background 0.3s;
}
#net-status.connected .dot { background: #6c6; }
#gp-indicator {
display: none;
font-size: 14px;
line-height: 1;
filter: grayscale(40%);
opacity: 0.8;
}
#gp-indicator.active { display: inline; filter: none; opacity: 1; }
#net-status input {
background: rgba(0,0,0,0.3);
border: 1px solid #555;
border-radius: 3px;
color: #eee;
font-size: 12px;
padding: 2px 6px;
width: 110px;
font-family: inherit;
}
/* HP bar */
#hp {
position: fixed;
bottom: 70px;
left: 50%;
transform: translateX(-50%);
width: 210px;
height: 18px;
background: rgba(0,0,0,0.5);
border: 2px solid rgba(255,255,255,0.25);
border-radius: 4px;
overflow: hidden;
}
#hp-fill {
height: 100%;
width: 100%;
background: linear-gradient(90deg, #e64141, #f0a040);
transition: width 0.2s;
}
#hp-label {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
text-shadow: 0 1px 2px #000;
}
/* Hotbar — must sit above the touch overlay (z 5) so taps land on slots
instead of being intercepted by the look pad. */
#hotbar {
position: fixed;
bottom: max(14px, env(safe-area-inset-bottom, 0));
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 5px;
background: var(--ui-bg);
padding: 5px;
border-radius: 8px;
pointer-events: auto;
z-index: 30;
touch-action: manipulation;
}
.slot {
width: 44px;
height: 44px;
border: 2px solid #555;
border-radius: 6px;
display: flex;
align-items: end;
justify-content: center;
padding-bottom: 3px;
font-size: 10px;
text-align: center;
color: #fff;
text-shadow: 0 1px 2px rgba(0,0,0,0.85);
cursor: pointer;
user-select: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4) inset;
}
.slot.active {
border-color: #fff;
box-shadow: 0 0 0 2px rgba(255,255,255,0.30) inset, 0 0 8px rgba(255,255,255,0.35);
}
/* Narrow viewports: shrink slots so all 10 fit in one row. */
@media (max-width: 620px) {
.slot { width: 34px; height: 34px; font-size: 8px; padding-bottom: 2px; }
#hotbar { gap: 3px; padding: 4px; }
}
@media (max-width: 400px) {
.slot { width: 30px; height: 30px; font-size: 7px; padding-bottom: 2px; }
#hotbar { gap: 2px; padding: 3px; }
}
/* Menu */
#menu {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(3px);
display: none;
align-items: center;
justify-content: center;
z-index: 60;
}
body.menu-open #menu { display: flex; }
#menu-card {
background: rgba(28, 28, 32, 0.95);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 10px;
padding: 28px 32px;
width: 360px;
max-width: 92vw;
box-shadow: 0 12px 40px rgba(0,0,0,0.55);
color: #f0f0f0;
font-size: 14px;
}
#menu-card h2 {
margin: 0 0 18px;
font-size: 22px;
letter-spacing: 1px;
text-align: center;
}
.menu-row {
display: flex;
align-items: center;
justify-content: space-between;
margin: 10px 0;
gap: 12px;
}
.menu-row label { flex: 1; }
.menu-row .value { width: 56px; text-align: right; opacity: 0.85; }
.menu-row input[type="range"] {
flex: 2;
accent-color: #8aa;
}
.menu-row input[type="text"] {
flex: 2;
background: rgba(0,0,0,0.3);
border: 1px solid #444;
border-radius: 4px;
color: inherit;
padding: 4px 8px;
font-family: inherit;
font-size: 13px;
}
.seg-toggle {
flex: 2;
display: flex;
gap: 0;
border: 1px solid #444;
border-radius: 5px;
overflow: hidden;
}
.seg-toggle button {
flex: 1;
background: rgba(0,0,0,0.3);
color: #ccc;
border: none;
padding: 6px 10px;
font-family: inherit;
font-size: 13px;
cursor: pointer;
}
.seg-toggle button + button { border-left: 1px solid #444; }
.seg-toggle button.active {
background: #6a9;
color: #001;
font-weight: 700;
}
.menu-actions {
display: flex;
gap: 10px;
margin-top: 18px;
}
.menu-actions button {
flex: 1;
padding: 10px;
background: #6a9;
color: #001;
border: none;
border-radius: 5px;
font-weight: 600;
font-size: 14px;
cursor: pointer;
}
.menu-actions button.secondary {
background: #444;
color: #ddd;
}
.menu-actions button:active { transform: translateY(1px); }
#menu-tip {
margin-top: 14px;
font-size: 11px;
opacity: 0.55;
text-align: center;
}
#menu-btn {
/* Must outrank #touch-overlay (z 5) — otherwise on mobile the look pad
sits on top of the button and eats the tap. */
position: fixed;
top: 12px;
right: 280px;
background: var(--ui-bg);
border: 1px solid rgba(255,255,255,0.35);
color: #fff;
width: 44px;
height: 44px;
border-radius: 6px;
font-size: 22px;
line-height: 1;
cursor: pointer;
display: none;
z-index: 30;
touch-action: manipulation;
-webkit-user-select: none;
user-select: none;
}
#menu-btn:active { background: rgba(255,255,255,0.25); }
body.touch #menu-btn {
display: flex;
align-items: center;
justify-content: center;
right: 12px;
top: 60px;
}
/* Gamepad debug overlay — must outrank the settings menu (z 60) since
"Test gamepad…" is opened from inside the menu and we need to actually
see the overlay land on top of it. */
#gptest {
position: fixed;
right: 12px;
bottom: 80px;
z-index: 70;
width: 320px;
max-width: 92vw;
}
#gptest-card {
background: rgba(20, 20, 24, 0.94);
border: 1px solid rgba(255,255,255,0.18);
border-radius: 8px;
padding: 12px 14px;
color: #e7e7e7;
font-size: 13px;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
}
#gptest button {
background: #555; color: #eee; border: none;
padding: 4px 10px; border-radius: 4px;
font-family: inherit; font-size: 11px; cursor: pointer;
}
.gp-btn {
width: 22px; height: 22px;
border-radius: 4px;
background: rgba(80,80,80,0.5);
border: 1px solid rgba(255,255,255,0.15);
display: flex; align-items: center; justify-content: center;
font-size: 9px; color: #999;
}
.gp-btn.on {
background: #6c6; color: #001; font-weight: 700;
}
.gp-stick {
width: 60px; height: 60px;
border-radius: 50%;
background: rgba(0,0,0,0.4);
border: 1px solid rgba(255,255,255,0.25);
position: relative;
}
.gp-stick-dot {
position: absolute;
width: 12px; height: 12px;
border-radius: 50%;
background: #ddd;
left: 50%; top: 50%;
transform: translate(-50%, -50%);
transition: transform 0.05s linear;
}
.gp-stick-label {
font-size: 9px; opacity: 0.55; text-align: center; margin-top: 3px;
}
/* Death screen */
#death {
position: fixed;
inset: 0;
background: rgba(120, 0, 0, 0.55);
backdrop-filter: blur(2px);
display: none;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 24px;
z-index: 100;
}
body.dead #death { display: flex; }
#death h1 {
font-size: 64px;
margin: 0;
color: #fff;
text-shadow: 0 4px 12px #000;
letter-spacing: 4px;
}
#death button {
padding: 12px 36px;
font-size: 18px;
background: #fff;
color: #200;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
}
#death button:active { transform: translateY(1px); }
/* Touch UI — MCPE-style: joystick left, action stack right */
body.touch #hud { display: none; }
#touch-overlay {
position: fixed;
inset: 0;
pointer-events: none; /* children opt in */
z-index: 5;
display: none;
}
body.touch #touch-overlay { display: block; }
body.touch #crosshair { display: block; }
/* Look pad sits underneath everything else, full screen, transparent.
pointer events on stick/buttons land on them first because of z-index. */
#look-pad {
position: absolute;
inset: 0;
background: transparent;
pointer-events: auto;
touch-action: none;
z-index: 1;
}
#stick {
position: absolute;
left: max(28px, env(safe-area-inset-left, 0));
bottom: max(28px, env(safe-area-inset-bottom, 0));
width: 160px; height: 160px;
border-radius: 50%;
background: rgba(0,0,0,0.22);
border: 2px solid rgba(255,255,255,0.28);
pointer-events: auto;
touch-action: none;
z-index: 2;
box-shadow: 0 4px 18px rgba(0,0,0,0.35);
}
#stick-knob {
position: absolute;
width: 64px; height: 64px;
border-radius: 50%;
background: rgba(255,255,255,0.55);
left: 50%; top: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
transition: background 0.15s;
box-shadow: 0 2px 6px rgba(0,0,0,0.3) inset;
}
#stick.engaged #stick-knob { background: rgba(255,255,255,0.85); }
#touch-actions {
position: absolute;
right: max(20px, env(safe-area-inset-right, 0));
bottom: max(28px, env(safe-area-inset-bottom, 0));
display: flex;
flex-direction: column-reverse; /* jump on the bottom (thumb), break/place above */
gap: 14px;
pointer-events: none;
z-index: 2;
}
.tbtn {
width: 72px; height: 72px;
border-radius: 50%;
background: rgba(0,0,0,0.32);
border: 2px solid rgba(255,255,255,0.32);
color: #fff;
font-size: 22px;
font-weight: 700;
display: flex;
align-items: center; justify-content: center;
pointer-events: auto;
touch-action: none;
user-select: none;
box-shadow: 0 3px 12px rgba(0,0,0,0.3);
}
.tbtn:active, .tbtn.pressed {
background: rgba(255,255,255,0.32);
transform: translateY(1px);
}
#btn-jump { width: 90px; height: 90px; font-size: 28px; }
#btn-break { background: rgba(180,40,40,0.40); }
#btn-place { background: rgba(40,140,40,0.40); }
#btn-sprint { background: rgba(80,80,140,0.40); font-size: 18px; }
/* Hide the top-right name input on touch — name lives in the menu. */
body.touch #net-status #player-name { display: none; }
</style>
</head>
<body>
<canvas id="game-canvas" width="1280" height="720" tabindex="0"></canvas>
<div id="crosshair"></div>
<div id="hud">
<strong>Voxel Game</strong><br/>
<kbd>WASD</kbd> move &nbsp; <kbd>Space</kbd> jump &nbsp; <kbd>Ctrl</kbd> sprint<br/>
Click canvas to lock mouse · <kbd>LMB</kbd> break · <kbd>RMB</kbd> place<br/>
<kbd>1</kbd><kbd>6</kbd> pick block
</div>
<div id="net-status">
<span class="dot"></span>
<span id="net-text">offline</span>
<span id="gp-indicator" title="Gamepad detected">🎮</span>
<input id="player-name" placeholder="name" maxlength="24" />
</div>
<div id="hp">
<div id="hp-fill"></div>
<div id="hp-label">20 / 20</div>
</div>
<div id="hotbar">
<div class="slot" data-b="1" style="background:#5fbb52">grass</div>
<div class="slot" data-b="2" style="background:#8c5c33">dirt</div>
<div class="slot active" data-b="3" style="background:#808284">stone</div>
<div class="slot" data-b="4" style="background:#e0d18c">sand</div>
<div class="slot" data-b="5" style="background:#6b4d2d">wood</div>
<div class="slot" data-b="6" style="background:#338b39">leaves</div>
<div class="slot" data-b="7" style="background:#6b6b73">cobble</div>
<div class="slot" data-b="8" style="background:#a64d38">brick</div>
<div class="slot" data-b="9" style="background:#f2f4f7;color:#222;text-shadow:none">snow</div>
<div class="slot" data-b="10" style="background:#9ed1f0;color:#1a3344;text-shadow:none">ice</div>
</div>
<div id="touch-overlay" aria-hidden="true">
<div id="look-pad"></div>
<div id="stick"><div id="stick-knob"></div></div>
<div id="touch-actions">
<button id="btn-jump" class="tbtn" type="button"></button>
<button id="btn-place" class="tbtn" type="button"></button>
<button id="btn-break" class="tbtn" type="button"></button>
<button id="btn-sprint" class="tbtn" type="button">»</button>
</div>
</div>
<div id="death">
<h1>YOU DIED</h1>
<button id="respawn-btn">RESPAWN</button>
</div>
<button id="menu-btn" title="Menu (Esc)"></button>
<div id="gptest" style="display:none;">
<div id="gptest-card">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
<strong>Gamepad debug</strong>
<button id="gptest-close" type="button">close</button>
</div>
<div id="gptest-info" style="font-size:11px; opacity:0.7; margin-bottom:8px;">no controller detected — press any button or move a stick</div>
<div style="display:flex; gap:14px; align-items:center; margin-bottom:8px;">
<div>
<div class="gp-stick"><div class="gp-stick-dot" id="gp-stick-l"></div></div>
<div class="gp-stick-label" id="gp-stick-l-label">L stick</div>
</div>
<div>
<div class="gp-stick"><div class="gp-stick-dot" id="gp-stick-r"></div></div>
<div class="gp-stick-label" id="gp-stick-r-label">R stick</div>
</div>
<div style="flex:1; font-family:monospace; font-size:10px; line-height:1.4;" id="gptest-axes"></div>
</div>
<div id="gptest-buttons" style="display:flex; flex-wrap:wrap; gap:4px;"></div>
<div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.1); font-size: 11px;">
<div style="opacity:0.7; margin-bottom: 5px;">
Stick calibration — push stick fully in the named direction, then click.
</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 4px;">
<button id="gp-cal-lx" type="button">L stick ←→</button>
<button id="gp-cal-ly" type="button">L stick ↑↓</button>
<button id="gp-cal-rx" type="button">R stick ←→</button>
<button id="gp-cal-ry" type="button">R stick ↑↓</button>
</div>
<div id="gp-mapping" style="margin-top: 6px; font-family:monospace; opacity: 0.75;"></div>
<button id="gp-cal-reset" type="button" style="margin-top: 6px;">reset to defaults (0,1,2,3)</button>
<div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.1);">
<div style="opacity:0.7; margin-bottom: 4px;">
Path verification — these bypass the gamepad and send input straight to the engine.
If clicking these moves you, the game itself is fine; the problem is upstream (Steam Input, etc).
</div>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 4px;">
<button id="gp-test-fwd" type="button">▲ walk fwd 1s</button>
<button id="gp-test-jump" type="button">▼ jump once</button>
</div>
</div>
<div id="gp-kbd-state" style="margin-top: 8px; font-family:monospace; opacity:0.7;">kbd: (waiting)</div>
</div>
</div>
</div>
<div id="menu">
<div id="menu-card">
<h2>SETTINGS</h2>
<div class="menu-row">
<label>Input mode</label>
<div class="seg-toggle">
<button id="mode-pc" type="button">PC</button>
<button id="mode-mobile" type="button">Mobile</button>
</div>
</div>
<div class="menu-row">
<label for="set-name">Player name</label>
<input id="set-name" type="text" maxlength="24" />
</div>
<div class="menu-row">
<label for="set-sens">Mouse sensitivity</label>
<input id="set-sens" type="range" min="0.001" max="0.02" step="0.0005" />
<span class="value" id="set-sens-val"></span>
</div>
<div class="menu-row">
<label for="set-fov">FOV</label>
<input id="set-fov" type="range" min="40" max="110" step="1" />
<span class="value" id="set-fov-val"></span>
</div>
<div class="menu-row">
<label for="set-dist">Render distance</label>
<input id="set-dist" type="range" min="64" max="800" step="16" />
<span class="value" id="set-dist-val"></span>
</div>
<div class="menu-row">
<label for="set-tscale">Time of day speed</label>
<input id="set-tscale" type="range" min="0" max="8" step="0.25" />
<span class="value" id="set-tscale-val"></span>
</div>
<div class="menu-actions">
<button id="menu-resume">RESUME</button>
<button id="menu-respawn" class="secondary">Respawn</button>
</div>
<div class="menu-actions" style="margin-top: 8px;">
<button id="menu-gptest" class="secondary">Test gamepad…</button>
</div>
<div id="menu-tip">Esc to toggle · settings save automatically</div>
</div>
</div>
<script type="module" src="./main.js"></script>
</body>
</html>