Drop parachuted parcels behind the plane as it crosses

Adds a sprite-based parcel system. The plane spawns a new
parcel-on-parachute (assets/parcel.png, 236 KB transparent) every
~1.4 s while it's visible (progress 0.04-0.92). Each parcel:

- Spawns at the plane's current position with a tiny random offset.
- Falls at 1.4 world units/s, with a small horizontal drift and a
  parachute sway sinusoid for character.
- Scales 0.35 → 2.6 over its lifetime, simulating the perspective
  of falling toward the camera.
- Fades in over 6% of life, fades out over the last 15%.
- Cleaned up (removed from scene + material disposed) when its
  lifetime expires or it drops below y = -10.

Implemented as THREE.Sprite so it always faces the camera, no need
to track per-parcel orientation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MVA Global Fret 2026-05-05 12:46:27 +02:00
parent 6fcc772bf0
commit 3f773380c4
2 changed files with 74 additions and 0 deletions

BIN
assets/parcel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

View File

@ -65,6 +65,72 @@ loader.load(
(err) => console.error('Failed to load airplane.glb:', err)
);
/* Colis parachutés
À intervalle régulier (et tant que l'avion est dans le cadre), on
spawn un sprite « parachute + colis » à la position de l'avion.
Chaque colis tombe lentement et grossit (effet de zoom à la
descente vers la caméra). Il s'efface en fin de course.
*/
const parcelTex = new THREE.TextureLoader().load('assets/parcel.png');
parcelTex.colorSpace = THREE.SRGBColorSpace;
const PARCEL_SPAWN_EVERY = 1.4; // un colis toutes les ~1.4s
const PARCEL_FALL_SPEED = 1.4; // unités monde/s
const PARCEL_DRIFT = 0.45; // dérive horizontale (sortie de soute)
const PARCEL_INITIAL_SCALE = 0.35;
const PARCEL_FINAL_SCALE = 2.6;
const PARCEL_LIFETIME = 5.5; // s avant de disparaître
const parcels = [];
let lastSpawn = 0;
function spawnParcel(x, y) {
const mat = new THREE.SpriteMaterial({
map: parcelTex, transparent: true, opacity: 0
});
const sprite = new THREE.Sprite(mat);
sprite.position.set(x + (Math.random() - 0.5) * 0.4, y - 0.2, 0.2);
sprite.scale.setScalar(PARCEL_INITIAL_SCALE);
scene.add(sprite);
parcels.push({
sprite, age: 0,
vx: (Math.random() - 0.5) * PARCEL_DRIFT, // dérive latérale légère
sway: 0.5 + Math.random() * 0.6, // freq de balancement
swayAmp: 0.08 + Math.random() * 0.08
});
}
function updateParcels(dt, t) {
for (let i = parcels.length - 1; i >= 0; i--) {
const p = parcels[i];
p.age += dt;
const life = p.age / PARCEL_LIFETIME;
/* Trajectoire : chute douce + dérive + balancement parachute */
p.sprite.position.y -= PARCEL_FALL_SPEED * dt;
p.sprite.position.x += p.vx * dt;
p.sprite.position.x += Math.sin(t * p.sway) * p.swayAmp * dt;
/* Grandit en tombant — perspective */
const k = Math.min(1, life * 1.1);
p.sprite.scale.setScalar(
PARCEL_INITIAL_SCALE + (PARCEL_FINAL_SCALE - PARCEL_INITIAL_SCALE) * k
);
/* Fade-in rapide au spawn, fade-out lent à la fin */
let opacity = 1;
if (life < 0.06) opacity = life / 0.06;
else if (life > 0.85) opacity = (1 - life) / 0.15;
p.sprite.material.opacity = Math.max(0, Math.min(1, opacity));
if (life >= 1 || p.sprite.position.y < -10) {
scene.remove(p.sprite);
p.sprite.material.dispose();
parcels.splice(i, 1);
}
}
}
/* Souris
L'avion avance automatiquement à vitesse de croisière (BASE_SPEED).
Bouger la souris ajoute un boost qui le pousse plus vite vers la fin.
@ -137,6 +203,14 @@ function tick() {
planeHolder.position.set(px, py + bob, 0);
/* Spawn d'un colis parachuté à intervalle régulier uniquement
pendant que l'avion est dans (ou proche de) la zone visible. */
if (p > 0.04 && p < 0.92 && t - lastSpawn > PARCEL_SPAWN_EVERY) {
spawnParcel(px, py + bob);
lastSpawn = t;
}
updateParcels(dt, t);
/* Avec nez à -X et up = +Y :
- rotation.z = PITCH (négatif = nez en l'air)
- rotation.x = ROLL