Sentinel attack runs: duck or die
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 <noreply@anthropic.com>
This commit is contained in:
parent
d759365b20
commit
c1ef9e4dc4
@ -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)
|
||||
|
||||
|
||||
39
index.html
39
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();
|
||||
</script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user