Spaces:
Running
Running
Codex CLI
commited on
Commit
·
2c6b4f6
1
Parent(s):
1f43352
feat(pickups): enhance health orb mechanics with magnetism and attraction animations
Browse files- src/pickups.js +35 -8
src/pickups.js
CHANGED
|
@@ -44,19 +44,23 @@ export function spawnHealthOrbs(center, count) {
|
|
| 44 |
mesh: group,
|
| 45 |
light: null,
|
| 46 |
pos: group.position,
|
| 47 |
-
radius: 0.7, // pickup
|
| 48 |
heal: 1,
|
| 49 |
bobT: G.random() * Math.PI * 2,
|
| 50 |
vel,
|
| 51 |
state: 'air', // 'air' | 'settled'
|
| 52 |
settleTimer: 0,
|
| 53 |
-
baseY: 0.2
|
|
|
|
| 54 |
};
|
| 55 |
G.orbs.push(orb);
|
| 56 |
}
|
| 57 |
}
|
| 58 |
|
| 59 |
export function updatePickups(delta) {
|
|
|
|
|
|
|
|
|
|
| 60 |
for (let i = G.orbs.length - 1; i >= 0; i--) {
|
| 61 |
const o = G.orbs[i];
|
| 62 |
|
|
@@ -104,20 +108,43 @@ export function updatePickups(delta) {
|
|
| 104 |
|
| 105 |
// Visuals: rotation and glow pulse
|
| 106 |
o.bobT += delta * 2.0;
|
| 107 |
-
|
|
|
|
|
|
|
| 108 |
// No dynamic light; keep unlit glow cheap
|
| 109 |
|
| 110 |
-
// If settled, apply gentle bob around baseY
|
| 111 |
-
if (o.state === 'settled') {
|
| 112 |
o.pos.y = o.baseY + Math.sin(o.bobT) * 0.06;
|
| 113 |
}
|
| 114 |
|
| 115 |
-
//
|
| 116 |
const dx = o.pos.x - G.player.pos.x;
|
| 117 |
const dz = o.pos.z - G.player.pos.z;
|
| 118 |
const dist = Math.hypot(dx, dz);
|
| 119 |
-
|
| 120 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
G.player.health = Math.min(CFG.player.health, G.player.health + o.heal);
|
| 122 |
// Pulse green heal overlay
|
| 123 |
G.healFlash = Math.min(1, G.healFlash + CFG.hud.healPulsePerPickup + o.heal * CFG.hud.healPulsePerHP);
|
|
|
|
| 44 |
mesh: group,
|
| 45 |
light: null,
|
| 46 |
pos: group.position,
|
| 47 |
+
radius: 0.7, // legacy; pickup now uses absolute distance
|
| 48 |
heal: 1,
|
| 49 |
bobT: G.random() * Math.PI * 2,
|
| 50 |
vel,
|
| 51 |
state: 'air', // 'air' | 'settled'
|
| 52 |
settleTimer: 0,
|
| 53 |
+
baseY: 0.2,
|
| 54 |
+
magnet: false
|
| 55 |
};
|
| 56 |
G.orbs.push(orb);
|
| 57 |
}
|
| 58 |
}
|
| 59 |
|
| 60 |
export function updatePickups(delta) {
|
| 61 |
+
// Attraction/pickup thresholds (meters)
|
| 62 |
+
const ATTRACT_RADIUS = 5.0; // start pulling from farther away
|
| 63 |
+
const PICKUP_DIST = 3.0; // auto-collect distance
|
| 64 |
for (let i = G.orbs.length - 1; i >= 0; i--) {
|
| 65 |
const o = G.orbs[i];
|
| 66 |
|
|
|
|
| 108 |
|
| 109 |
// Visuals: rotation and glow pulse
|
| 110 |
o.bobT += delta * 2.0;
|
| 111 |
+
// Speed up spin slightly when magnetized
|
| 112 |
+
const spin = o.magnet ? 4.0 : 1.5;
|
| 113 |
+
o.mesh.rotation.y += delta * spin;
|
| 114 |
// No dynamic light; keep unlit glow cheap
|
| 115 |
|
| 116 |
+
// If settled and not magnetized, apply gentle bob around baseY
|
| 117 |
+
if (o.state === 'settled' && !o.magnet) {
|
| 118 |
o.pos.y = o.baseY + Math.sin(o.bobT) * 0.06;
|
| 119 |
}
|
| 120 |
|
| 121 |
+
// Distance on ground plane (for feel); magnet + pickup thresholds
|
| 122 |
const dx = o.pos.x - G.player.pos.x;
|
| 123 |
const dz = o.pos.z - G.player.pos.z;
|
| 124 |
const dist = Math.hypot(dx, dz);
|
| 125 |
+
// Begin attraction when close enough
|
| 126 |
+
if (!o.magnet && dist <= ATTRACT_RADIUS) {
|
| 127 |
+
o.magnet = true;
|
| 128 |
+
}
|
| 129 |
+
// Attraction animation: pull toward player smoothly
|
| 130 |
+
if (o.magnet) {
|
| 131 |
+
// Disable physics while magnetized for a clean pull
|
| 132 |
+
if (o.vel) { o.vel.set(0, 0, 0); }
|
| 133 |
+
const t = Math.max(0, Math.min(1, 1 - dist / ATTRACT_RADIUS));
|
| 134 |
+
// Non-linear catch-up factor for a snappy feel
|
| 135 |
+
const alpha = 1 - Math.pow(1 - Math.min(0.95, 0.15 + t * 0.8), Math.max(1, delta * 60));
|
| 136 |
+
// Target toward player's position (eye-level), looks good with vertical glide
|
| 137 |
+
o.pos.lerp(G.player.pos, alpha);
|
| 138 |
+
// Scale up slightly as it gets closer
|
| 139 |
+
const s = 1 + 0.35 * t;
|
| 140 |
+
o.mesh.scale.setScalar(s);
|
| 141 |
+
} else {
|
| 142 |
+
// Reset scale when not magnetized
|
| 143 |
+
o.mesh.scale.setScalar(1);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// Pickup check using absolute distance threshold (meters)
|
| 147 |
+
if (dist <= PICKUP_DIST && G.player.alive && G.state === 'playing') {
|
| 148 |
G.player.health = Math.min(CFG.player.health, G.player.health + o.heal);
|
| 149 |
// Pulse green heal overlay
|
| 150 |
G.healFlash = Math.min(1, G.healFlash + CFG.hud.healPulsePerPickup + o.heal * CFG.hud.healPulsePerHP);
|