feat: bubbles, food pellets & fish feeding behavior

- Bubbles.ts: rising bubble particles from 2 air stones (near plants),
  wobble + growth + fade near surface, 50-mesh pool
- Food.ts: auto-dropping food pellets (every 6-14s, 3-6 pieces) that
  float down and settle on the floor; manual drop via click
- GLTFish.ts: fish now notice food within 1.2 units, seek at 1.4x speed
  with faster turn rate, eat pellets within 0.15 units; faster tail beat
  while chasing
- main.ts: wires food + bubbles into render loop; click drops 4 pellets
This commit is contained in:
2026-10-07 11:49:10 +00:00
parent 4893ee2f8c
commit c7427f9c61
4 changed files with 465 additions and 19 deletions
+17 -5
View File
@@ -5,6 +5,8 @@ import { setupLighting } from "./scene/Lighting";
import { createGLTFishSchool, type FishSpec } from "./scene/GLTFish";
import { createDecor, type DecorSpec } from "./scene/Decor";
import { createParallaxCamera } from "./scene/Camera";
import { createBubbles } from "./scene/Bubbles";
import { createFood } from "./scene/Food";
const canvas = document.querySelector<HTMLCanvasElement>("#aquarium");
if (!canvas) {
@@ -34,10 +36,10 @@ setupLighting(scene, renderer);
createTank(scene);
const water = createWater(scene);
// Fish — GLB models with swim animations + wandering AI.
// Maya's tropical fish already face local +X (no orient fix). The Quaternius
// pack is built with its long axis on +Z (nose at +Z), so it is rotated +90°
// about Y to bring the nose onto +X.
// Food — pellets that drop from above; fish seek and eat them.
const food = createFood(scene);
// Fish — GLB models with swim animations + wandering AI + food seeking.
const fishSpecs: FishSpec[] = [
// Tropical (maya's-aquarium-3d)
{ url: "models/angelfish.glb", targetLength: 0.55, speed: [0.25, 0.45] },
@@ -52,7 +54,7 @@ const fishSpecs: FishSpec[] = [
{ url: "models/quaternius/glb/Fish2.glb", targetLength: 0.6, speed: [0.3, 0.55], orient: { y: Math.PI / 2 } },
{ url: "models/quaternius/glb/Fish3.glb", targetLength: 0.55, speed: [0.35, 0.6], orient: { y: Math.PI / 2 } },
];
const fishSchool = createGLTFishSchool(scene, fishSpecs);
const fishSchool = createGLTFishSchool(scene, fishSpecs, food);
// Decor — static plants and rocks on the gravel floor (y ~ 0.05).
const decorSpecs: DecorSpec[] = [
@@ -64,6 +66,9 @@ const decorSpecs: DecorSpec[] = [
];
createDecor(scene, decorSpecs);
// Bubbles — rising from air stones near the plants.
const bubbles = createBubbles(scene);
const parallax = createParallaxCamera(camera);
// Mouse tracking (normalized to -1..1)
@@ -73,6 +78,11 @@ window.addEventListener("mousemove", (e) => {
mouseY = (e.clientY / window.innerHeight) * 2 - 1;
});
// Click to drop food.
window.addEventListener("click", () => {
food.dropFood(4);
});
// Resize handling
function resize(): void {
const width = window.innerWidth;
@@ -95,7 +105,9 @@ function animate(): void {
const dt = Math.min((now - lastFrameTime) / 1000, 0.1);
lastFrameTime = now;
water.update(time);
food.update(dt, time);
fishSchool.update(dt, time);
bubbles.update(dt, time);
parallax.update(mouseX, mouseY, time, dt);
renderer.render(scene, camera);
}