The features added in Rounds A–D were correct but expensive. The hot
path per frame was sky_color() called from apply_fog for EVERY distant
pixel — 4-octave cloud fbm + star hash + sun/moon disc per fragment,
hundreds of thousands of pixels per frame. Profile-driven cuts that
keep all features but stop paying for them in the wrong places:
1. apply_fog now mixes terrain toward sky_dome (cheap gradient) not
sky_color (gradient + clouds + sun + moon + stars). Distant terrain
still fades to the right-direction sky color at every time of day;
the per-pixel cost drops by ~80%. Full sky_color still runs for
the SKY BACKGROUND pass where it's actually paid for.
2. Sky pipeline draws AFTER terrain with depth_compare = LessEqual.
The full-screen sky was previously written first then over-painted
by terrain — sky's expensive fragment shader ran on every screen
pixel. Now it only runs on pixels with no terrain in front of them
(depth = 1.0 cleared), which on most views is 30–60% of the screen
instead of 100%.
3. fbm2 reduced from 4 → 3 octaves. Negligible visual change at the
scales we sample, ~25% cheaper per cloud-pixel.
4. Cloud branch skips entirely when day_strength < 0.05 (full night).
Clouds invisible at night anyway, fbm + smoothstep + mix skipped.
5. In-game FPS HUD (top-right corner):
- Telemetry struct gains frame_dt_ms (EMA-smoothed in app.rs
with coefficient 0.85 so the number is readable, not flickery).
- wasm bridge: get_frame_dt_ms().
- main.js setupFpsHud() polls it at 5Hz, color-coded:
green ≤ 18ms (≥55fps), amber 18-33ms, red beyond.
- Reads what THE GAME measures, not the browser's
requestAnimationFrame which gets throttled to 1 Hz on
unfocused windows.
No features removed. God rays, FXAA, ACES tonemap, bounce baking,
specular materials, leaf translucency — all still there. Tests:
63 passing. Wasm release clean.
664 lines
20 KiB
HTML
664 lines
20 KiB
HTML
<!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;
|
||
}
|
||
/* Frame-time HUD — top-right, always-on, fed from
|
||
window.voxel_game.get_frame_dt_ms() so the number reflects what
|
||
the game itself is measuring (not the browser's throttled rAF).
|
||
Colors: green ≤ 18ms, amber ≤ 33ms, red beyond. */
|
||
#fps {
|
||
position: fixed;
|
||
top: 12px;
|
||
right: 12px;
|
||
background: var(--ui-bg);
|
||
padding: 6px 10px;
|
||
border-radius: 6px;
|
||
font: 12px/1.2 ui-monospace, monospace;
|
||
pointer-events: none;
|
||
z-index: 20;
|
||
min-width: 60px;
|
||
text-align: right;
|
||
color: #cfc;
|
||
}
|
||
#fps.warn { color: #ffd86a; }
|
||
#fps.bad { color: #ff7a6a; }
|
||
#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; }
|
||
/* Settings menu always wins over the death overlay — when the player
|
||
pops the menu they need to interact with sliders / respawn /
|
||
settings, not stare at a red death curtain. The z-index alone
|
||
wouldn't be enough because the death overlay also has a
|
||
backdrop-filter that visibly tints anything underneath. */
|
||
body.menu-open #death { display: none; }
|
||
#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 <kbd>Space</kbd> jump <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="fps">— fps</div>
|
||
|
||
<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>
|