From c1ef9e4dc4a8edf3ac795039797e6b9e51f4dbe1 Mon Sep 17 00:00:00 2001 From: type-two Date: Wed, 29 Jul 2026 12:21:27 +1000 Subject: [PATCH] Sentinel attack runs: duck or die MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From level 3, flybys can become attack runs — klaxon + red 'SENTINEL — DUCK' warning, red eye-light, then a fast sweep across the play plane at the head height it locked at spawn. Hit costs a life; surviving pays +25. Attack chance scales with level (25% + 5%/level, cap 70%). Verified both outcomes via scripted playtest. Co-Authored-By: Claude Fable 5 --- README.md | 3 +++ index.html | 39 +++++++++++++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d0985bc..7217391 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ No build step — single file, Three.js from CDN. - **W/S · A/D · Q/E** — pitch / yaw / roll 90°, works while held and mid-flight - Catch +25 · fit +100 × combo · dodge an agent +10 · 3 lives. Speed and agent frequency ramp per fit; consecutive fits build the combo multiplier. +- From level 3 the **sentinel attacks**: klaxon + red warning, then it sweeps the + play plane at the height your head was at when it locked on. Duck out of the + band or lose a life; surviving pays +25. ## Assets (all generated free & local on MODELBEAST) diff --git a/index.html b/index.html index 7f8e0a6..b8c19ce 100644 --- a/index.html +++ b/index.html @@ -93,6 +93,7 @@ const sfx = { throw: () => tone(190, 0.18, 'sawtooth', 0.13, -140), fit: () => { tone(523, 0.12, 'sine', 0.16); setTimeout(() => tone(784, 0.14, 'sine', 0.16), 90); setTimeout(() => tone(1047, 0.2, 'sine', 0.16), 180); }, dodge: () => tone(880, 0.07, 'sine', 0.1, 220), + klaxon: () => { tone(110, 0.35, 'sawtooth', 0.18, -40); setTimeout(() => tone(110, 0.35, 'sawtooth', 0.18, -40), 450); }, shatter: () => noise(0.35, 0.3, 1400), hit: () => { noise(0.3, 0.35, 500); tone(90, 0.3, 'sawtooth', 0.2, -50); }, }; @@ -154,31 +155,53 @@ texLoader.load('assets/circuit.png', tex => { }); // ---- sentinel flyby (generated image → GLB via the farm; optional) ---- -let sentinel = null; +let sentinel = null, sentinelEye = null; new GLTFLoader().load('assets/sentinel.glb', g => { sentinel = g.scene; const box = new THREE.Box3().setFromObject(sentinel); const size = box.getSize(new THREE.Vector3()).length(); sentinel.scale.setScalar(20 / size); sentinel.visible = false; + sentinelEye = new THREE.PointLight(0xff2233, 0, 40); // lit only during attack runs + sentinel.add(sentinelEye); scene.add(sentinel); }, undefined, () => {}); -let flybyT = 14; +let flybyT = 14, forceAttack = false; function flyby(dt) { if (!sentinel) return; + const s = sentinel.userData; if (!sentinel.visible) { flybyT -= dt; if (flybyT <= 0) { + const playing = state === 'incoming' || state === 'hold' || state === 'thrown'; + s.attack = forceAttack || (playing && level >= 2 && Math.random() < Math.min(0.25 + level*0.05, 0.7)); + forceAttack = false; + s.hit = false; + s.dir = Math.random() < 0.5 ? 1 : -1; sentinel.visible = true; - sentinel.userData.dir = Math.random() < 0.5 ? 1 : -1; - sentinel.position.set(-sentinel.userData.dir * 60, (Math.random()-0.5)*10, -45 - Math.random()*25); - sentinel.rotation.set(0, sentinel.userData.dir > 0 ? Math.PI/2 : -Math.PI/2, 0); + sentinel.rotation.set(0, s.dir > 0 ? Math.PI/2 : -Math.PI/2, 0); + sentinelEye.intensity = s.attack ? 80 : 0; + if (s.attack) { + sentinel.position.set(-s.dir * 70, head.y, -10); // locks onto your head height — duck! + sfx.klaxon(); showMsg('SENTINEL — DUCK', 1200, true); + } else { + sentinel.position.set(-s.dir * 60, (Math.random()-0.5)*10, -45 - Math.random()*25); + } } } else { - sentinel.position.x += sentinel.userData.dir * 14 * dt; + sentinel.position.x += s.dir * (s.attack ? 26 : 14) * dt; sentinel.position.y += Math.sin(performance.now()/400) * 0.02; sentinel.rotation.z = Math.sin(performance.now()/700) * 0.15; - if (Math.abs(sentinel.position.x) > 65) { sentinel.visible = false; flybyT = 12 + Math.random()*16; } + if (s.attack && !s.hit && state !== 'over' && + Math.abs(sentinel.position.x - head.x) < 3.5 && Math.abs(sentinel.position.y - head.y) < 1.5) { + s.hit = true; + damage('SENTINEL STRIKE'); + } + if (Math.abs(sentinel.position.x) > 70) { + if (s.attack && !s.hit && state !== 'over' && state !== 'title') { addScore(25); sfx.dodge(); showMsg('EVADED +25', 700); } + sentinel.visible = false; sentinelEye.intensity = 0; + flybyT = s.attack ? 16 + Math.random()*14 : 12 + Math.random()*16; + } } } @@ -454,7 +477,7 @@ window._auto = () => { // rotate held/flying brick to fit the hole }; Object.defineProperty(window, '_g', { get: () => ({ state, score, health, level, combo, agent: !!(brick && brick.userData.agent), brickZ: brick && +brick.position.z.toFixed(1), brickXY: brick && [+brick.position.x.toFixed(1), +brick.position.y.toFixed(1)], head: [+head.x.toFixed(1), +head.y.toFixed(1)], wallZ: wall && +wall.position.z.toFixed(1) }) }); window._start = () => { if (state === 'title') { $('title').style.display = 'none'; reset(); } }; -window._fly = () => { flybyT = 0.01; return sentinel ? 'loaded' : 'no glb'; }; +window._fly = (attack = false) => { flybyT = 0.01; forceAttack = attack; return sentinel ? 'loaded' : 'no glb'; }; tick();