Upload index.html
Browse files- index.html +137 -5
index.html
CHANGED
|
@@ -84,6 +84,8 @@ function genKoiPatches(type) {
|
|
| 84 |
return arr;
|
| 85 |
}
|
| 86 |
let fishes = [], bubbles = [], seaweeds = [], motes = [], rays = [], rocks = [], grass = [], fgGrass = [];
|
|
|
|
|
|
|
| 87 |
let curNight = 0; // v7: shared night factor (dims the top-light + sheen)
|
| 88 |
let plantPoints = [], _ppUsed = 0; // collision points — a pooled set, reused in place
|
| 89 |
let sandHeights = [];
|
|
@@ -452,9 +454,34 @@ class Fish {
|
|
| 452 |
if (this.roll > 1.15) { this.roll = 1.15; this.rollVel *= -0.25; }
|
| 453 |
else if (this.roll < -1.15) { this.roll = -1.15; this.rollVel *= -0.25; }
|
| 454 |
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
this
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
|
| 459 |
// NOTE: fish do NOT collide with plants — they plough straight through.
|
| 460 |
// The seaweed bends out of the way instead (see updatePlants).
|
|
@@ -788,6 +815,20 @@ class Fish {
|
|
| 788 |
c.restore();
|
| 789 |
}
|
| 790 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 791 |
}
|
| 792 |
}
|
| 793 |
|
|
@@ -1269,8 +1310,9 @@ function drawRays(t, light) {
|
|
| 1269 |
const [rr, rg, rb] = light.ray;
|
| 1270 |
const yTop0 = -10, ySpan = H * 0.95 + 10;
|
| 1271 |
|
| 1272 |
-
// PERF: only rebuild tinted sprites when the light color shifts significantly (delta >
|
| 1273 |
-
|
|
|
|
| 1274 |
rayTintRGB = [rr, rg, rb];
|
| 1275 |
buildRaySprites(); // rebuild with the new tint
|
| 1276 |
}
|
|
@@ -1810,10 +1852,100 @@ function doResize() {
|
|
| 1810 |
buildStaticLayer();
|
| 1811 |
if (!grainPattern) buildGrainTile(); // v8: grain tile is size-independent, build once
|
| 1812 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1813 |
window.addEventListener("resize", doResize);
|
| 1814 |
doResize();
|
| 1815 |
requestAnimationFrame(frame);
|
| 1816 |
})();
|
| 1817 |
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1818 |
</body>
|
| 1819 |
</html>
|
|
|
|
| 84 |
return arr;
|
| 85 |
}
|
| 86 |
let fishes = [], bubbles = [], seaweeds = [], motes = [], rays = [], rocks = [], grass = [], fgGrass = [];
|
| 87 |
+
let playerFish = null; // the fish controlled by gamepad (spawned with F key)
|
| 88 |
+
const GAMEPAD_DEADZONE = 0.15; // ignore small stick movements
|
| 89 |
let curNight = 0; // v7: shared night factor (dims the top-light + sheen)
|
| 90 |
let plantPoints = [], _ppUsed = 0; // collision points — a pooled set, reused in place
|
| 91 |
let sandHeights = [];
|
|
|
|
| 454 |
if (this.roll > 1.15) { this.roll = 1.15; this.rollVel *= -0.25; }
|
| 455 |
else if (this.roll < -1.15) { this.roll = -1.15; this.rollVel *= -0.25; }
|
| 456 |
|
| 457 |
+
// Player fish: input-driven movement (keyboard WASD + gamepad sticks),
|
| 458 |
+
// overrides the AI cruising so the player is fully in control.
|
| 459 |
+
if (this === playerFish) {
|
| 460 |
+
const inp = getPlayerInput();
|
| 461 |
+
// Turn toward the combined input direction (WASD or left stick).
|
| 462 |
+
if (Math.hypot(inp.dx, inp.dy) > GAMEPAD_DEADZONE) {
|
| 463 |
+
const targetAngle = Math.atan2(inp.dy, inp.dx);
|
| 464 |
+
let d = targetAngle - this.angle;
|
| 465 |
+
while (d > Math.PI) d -= 2 * Math.PI;
|
| 466 |
+
while (d < -Math.PI) d += 2 * Math.PI;
|
| 467 |
+
const turnSpeed = 4.0; // rad/s — responsive turning
|
| 468 |
+
this.angle += Math.max(-turnSpeed * dt, Math.min(turnSpeed * dt, d));
|
| 469 |
+
}
|
| 470 |
+
// Speed: cruise baseline, boosted by Shift / R-stick down,
|
| 471 |
+
// slowed by Space / R-stick up.
|
| 472 |
+
let speedMult = 1;
|
| 473 |
+
if (inp.boost > 0) speedMult += inp.boost * 2.0; // speed up
|
| 474 |
+
if (inp.slow > 0) speedMult -= inp.slow * 0.85; // slow down
|
| 475 |
+
speedMult = Math.max(0.15, Math.min(3.0, speedMult));
|
| 476 |
+
const playerSpeed = this.speed * this.cruiseSpeed * speedMult * 60 * dt;
|
| 477 |
+
this.x += Math.cos(this.angle) * playerSpeed;
|
| 478 |
+
this.y += Math.sin(this.angle) * playerSpeed;
|
| 479 |
+
} else {
|
| 480 |
+
// Normal AI fish movement.
|
| 481 |
+
const sp = this.speed * this.cruiseSpeed * this.speedFactor * 60 * dt;
|
| 482 |
+
this.x += Math.cos(this.angle) * sp;
|
| 483 |
+
this.y += Math.sin(this.angle) * sp;
|
| 484 |
+
}
|
| 485 |
|
| 486 |
// NOTE: fish do NOT collide with plants — they plough straight through.
|
| 487 |
// The seaweed bends out of the way instead (see updatePlants).
|
|
|
|
| 815 |
c.restore();
|
| 816 |
}
|
| 817 |
|
| 818 |
+
// Player fish indicator: glowing outline to make it easy to spot
|
| 819 |
+
if (this === playerFish) {
|
| 820 |
+
c.save();
|
| 821 |
+
c.globalAlpha = 0.6 + Math.sin(performance.now() * 0.005) * 0.3; // pulsing glow
|
| 822 |
+
c.strokeStyle = '#FFD700'; // gold color
|
| 823 |
+
c.lineWidth = 2;
|
| 824 |
+
c.shadowColor = '#FFD700';
|
| 825 |
+
c.shadowBlur = 8;
|
| 826 |
+
c.beginPath();
|
| 827 |
+
c.ellipse(0, 0, s * 0.75, s * 0.35, 0, 0, Math.PI * 2);
|
| 828 |
+
c.stroke();
|
| 829 |
+
c.restore();
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
}
|
| 833 |
}
|
| 834 |
|
|
|
|
| 1310 |
const [rr, rg, rb] = light.ray;
|
| 1311 |
const yTop0 = -10, ySpan = H * 0.95 + 10;
|
| 1312 |
|
| 1313 |
+
// PERF: only rebuild tinted sprites when the light color shifts significantly (delta > 35)
|
| 1314 |
+
// Higher threshold = fewer expensive canvas rebuilds during slow dusk/dawn transitions
|
| 1315 |
+
if (!rayTintRGB || Math.abs(rayTintRGB[0] - rr) + Math.abs(rayTintRGB[1] - rg) + Math.abs(rayTintRGB[2] - rb) > 35) {
|
| 1316 |
rayTintRGB = [rr, rg, rb];
|
| 1317 |
buildRaySprites(); // rebuild with the new tint
|
| 1318 |
}
|
|
|
|
| 1852 |
buildStaticLayer();
|
| 1853 |
if (!grainPattern) buildGrainTile(); // v8: grain tile is size-independent, build once
|
| 1854 |
}
|
| 1855 |
+
// ============ Player controls: keyboard + gamepad ============
|
| 1856 |
+
const keys = {}; // currently-held keyboard keys (lowercased)
|
| 1857 |
+
let prevStartPressed = false; // edge-detect the gamepad Start button
|
| 1858 |
+
|
| 1859 |
+
// Spawn a fresh player-controlled fish at the centre of the tank.
|
| 1860 |
+
function spawnPlayerFish() {
|
| 1861 |
+
if (playerFish) { // replace any existing one
|
| 1862 |
+
const idx = fishes.indexOf(playerFish);
|
| 1863 |
+
if (idx !== -1) fishes.splice(idx, 1);
|
| 1864 |
+
}
|
| 1865 |
+
playerFish = new Fish();
|
| 1866 |
+
playerFish.x = W / 2;
|
| 1867 |
+
playerFish.y = H / 2;
|
| 1868 |
+
playerFish.size = rand(20, 35); // medium size for visibility
|
| 1869 |
+
playerFish.speed *= 1.5; // a touch faster than average for fun
|
| 1870 |
+
playerFish.hue = (Math.random() * 360) | 0; // unique colour so it stands out
|
| 1871 |
+
playerFish.saturation = 90;
|
| 1872 |
+
playerFish.lightness = 55;
|
| 1873 |
+
fishes.push(playerFish);
|
| 1874 |
+
console.log('Player fish spawned! Steer with WASD or the left stick.');
|
| 1875 |
+
}
|
| 1876 |
+
|
| 1877 |
+
// Keyboard: WASD steer, Shift speed up, Space slow down, F spawn.
|
| 1878 |
+
document.addEventListener('keydown', (e) => {
|
| 1879 |
+
const k = e.key.toLowerCase();
|
| 1880 |
+
keys[k] = true;
|
| 1881 |
+
if (k === ' ') e.preventDefault(); // stop Space from scrolling the page
|
| 1882 |
+
if (k === 'f' && !e.repeat) spawnPlayerFish();
|
| 1883 |
+
});
|
| 1884 |
+
document.addEventListener('keyup', (e) => {
|
| 1885 |
+
keys[e.key.toLowerCase()] = false;
|
| 1886 |
+
});
|
| 1887 |
+
|
| 1888 |
+
// Poll gamepad state each frame (Gamepad API requires polling).
|
| 1889 |
+
function pollGamepad() {
|
| 1890 |
+
const pads = navigator.getGamepads ? navigator.getGamepads() : null;
|
| 1891 |
+
let startNow = false;
|
| 1892 |
+
if (pads) {
|
| 1893 |
+
for (const pad of pads) {
|
| 1894 |
+
if (!pad || !pad.connected) continue;
|
| 1895 |
+
// Xbox 360 layout: Start button is index [7].
|
| 1896 |
+
if (pad.buttons[7] && pad.buttons[7].pressed) startNow = true;
|
| 1897 |
+
break; // only the first connected pad
|
| 1898 |
+
}
|
| 1899 |
+
}
|
| 1900 |
+
if (startNow && !prevStartPressed) spawnPlayerFish(); // on-press only
|
| 1901 |
+
prevStartPressed = startNow;
|
| 1902 |
+
}
|
| 1903 |
+
setInterval(pollGamepad, 10);
|
| 1904 |
+
|
| 1905 |
+
// Combine keyboard + gamepad into one input vector for the player fish.
|
| 1906 |
+
function getPlayerInput() {
|
| 1907 |
+
let dx = 0, dy = 0, boost = 0, slow = 0;
|
| 1908 |
+
// Keyboard: WASD steer, Shift speed up, Space slow down.
|
| 1909 |
+
if (keys['a']) dx -= 1;
|
| 1910 |
+
if (keys['d']) dx += 1;
|
| 1911 |
+
if (keys['w']) dy -= 1;
|
| 1912 |
+
if (keys['s']) dy += 1;
|
| 1913 |
+
if (keys['shift']) boost = 1;
|
| 1914 |
+
if (keys[' ']) slow = 1;
|
| 1915 |
+
|
| 1916 |
+
// Gamepad: left stick steers, right stick down speeds up / up slows down.
|
| 1917 |
+
const pads = navigator.getGamepads ? navigator.getGamepads() : null;
|
| 1918 |
+
if (pads) {
|
| 1919 |
+
for (const pad of pads) {
|
| 1920 |
+
if (!pad || !pad.connected) continue;
|
| 1921 |
+
const lx = Math.abs(pad.axes[0]) > GAMEPAD_DEADZONE ? pad.axes[0] : 0;
|
| 1922 |
+
const ly = Math.abs(pad.axes[1]) > GAMEPAD_DEADZONE ? pad.axes[1] : 0;
|
| 1923 |
+
dx += lx;
|
| 1924 |
+
dy += ly;
|
| 1925 |
+
if (pad.axes[3] > GAMEPAD_DEADZONE) boost = Math.max(boost, pad.axes[3]); // down → faster
|
| 1926 |
+
if (pad.axes[3] < -GAMEPAD_DEADZONE) slow = Math.max(slow, -pad.axes[3]); // up → slower
|
| 1927 |
+
break; // only the first connected pad
|
| 1928 |
+
}
|
| 1929 |
+
}
|
| 1930 |
+
return { dx, dy, boost, slow };
|
| 1931 |
+
}
|
| 1932 |
+
|
| 1933 |
+
// Fade out the controls help text 10s after load/refresh.
|
| 1934 |
+
setTimeout(() => {
|
| 1935 |
+
const el = document.getElementById('controls-help');
|
| 1936 |
+
if (el) el.style.opacity = '0';
|
| 1937 |
+
}, 10000);
|
| 1938 |
+
|
| 1939 |
window.addEventListener("resize", doResize);
|
| 1940 |
doResize();
|
| 1941 |
requestAnimationFrame(frame);
|
| 1942 |
})();
|
| 1943 |
</script>
|
| 1944 |
+
|
| 1945 |
+
<div id="controls-help" style="position: fixed; bottom: 15px; left: 15px; color: rgba(255,255,255,0.7); font-family: monospace; font-size: 12px; pointer-events: none; z-index: 1000; line-height: 1.7; transition: opacity 1.5s ease;">
|
| 1946 |
+
<strong>Keyboard:</strong> F spawn · WASD steer · Shift speed up · Space slow down<br>
|
| 1947 |
+
<strong>Controller:</strong> Start spawn · Left stick steer · Right stick ↓ speed up · Right stick ↑ slow down
|
| 1948 |
+
</div>
|
| 1949 |
+
|
| 1950 |
</body>
|
| 1951 |
</html>
|