// Basketball-net style hit test: a forgiving cylinder "behind" the ring. // Assumes ring is oriented to face camera (lookAt), so ring local +Z = "behind" (away from camera). AFRAME.registerSystem("tetherGame", { init() { this.orbEl = document.querySelector("#orb"); this.ringEl = document.querySelector("#ring"); this.scoreText = document.querySelector("#hudScore"); this.score = 0; this.cooldown = 0; this.prevOrbWorld = new THREE.Vector3(); this.prevValid = false; // tuning knobs for the "net" this.netDepth = 0.75; // meters behind ring plane this.netInset = 0.02; // start a tiny bit behind the ring plane (avoid counting in front) this.netRadiusMul = 1.18; // forgiving radius multiplier // temps this.tmpPrevL = new THREE.Vector3(); this.tmpCurL = new THREE.Vector3(); this.tmpP = new THREE.Vector3(); setTimeout(() => this.ringEl?.components["target-ring"]?.randomize?.(), 150); this.render(); }, render() { this.scoreText?.setAttribute("value", `Score: ${this.score}`); }, onHit() { this.score++; this.cooldown = 0.25; this.render(); // keep your SFX if you have it: if (window.SFX?.beep) { SFX.beep(980, 0.07); setTimeout(() => SFX.beep(1310, 0.05), 60); } // shrink hole slightly and move ring (optional, keep if you like) const rc = this.ringEl.components["target-ring"]; if (rc?.data?.radiusHole != null) { rc.data.radiusHole = Math.max(0.15, rc.data.radiusHole - 0.01); } rc?.randomize?.(); }, tick(_, dtMs) { const dt = Math.min(dtMs / 1000, 0.033); if (this.cooldown > 0) this.cooldown -= dt; const orbObj = this.orbEl?.object3D; const ringObj = this.ringEl?.object3D; const ringComp = this.ringEl?.components["target-ring"]; if (!orbObj || !ringObj || !ringComp) return; // Only score when ball is free-flying (not held) const orbComp = this.orbEl.components["orb-ballistics"]; const held = !!orbComp?.heldBy; const curWorld = orbObj.position; if (!this.prevValid) { this.prevOrbWorld.copy(curWorld); this.prevValid = true; return; } if (held || this.cooldown > 0) { this.prevOrbWorld.copy(curWorld); return; } // Cylinder behind the ring, in ring-local coords: // - ring plane is z=0 // - "behind" is +z const holeR = ringComp.data.radiusHole; const netR = holeR * this.netRadiusMul; const z0 = this.netInset; const z1 = this.netInset + this.netDepth; const netR2 = netR * netR; // Convert prev and current world positions into ring local space this.tmpPrevL.copy(this.prevOrbWorld); this.tmpCurL.copy(curWorld); ringObj.worldToLocal(this.tmpPrevL); ringObj.worldToLocal(this.tmpCurL); // Sample along segment so fast balls don't skip through const segLen = this.tmpPrevL.distanceTo(this.tmpCurL); const steps = Math.min(6, Math.max(1, Math.ceil(segLen / (netR * 0.5)))); let scored = false; for (let i = 1; i <= steps; i++) { const t = i / steps; this.tmpP.copy(this.tmpPrevL).lerp(this.tmpCurL, t); // inside cylinder volume? if (this.tmpP.z >= z0 && this.tmpP.z <= z1) { const r2 = this.tmpP.x * this.tmpP.x + this.tmpP.y * this.tmpP.y; if (r2 <= netR2) { this.onHit(); scored = true; break; } } } this.prevOrbWorld.copy(curWorld); } });