<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>One on One: Dr. J vs. Larry Bird - Three.js</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #111;
font-family: 'Courier New', Courier, monospace;
user-select: none;
}
#canvas-container {
width: 100vw;
height: 100vh;
display: block;
}
#ui {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 40px;
color: #fff;
font-size: 20px;
font-weight: bold;
text-shadow: 2px 2px #000;
pointer-events: none;
}
.score-card {
background: rgba(0, 0, 0, 0.6);
padding: 10px 20px;
border: 2px solid #555;
border-radius: 4px;
}
#controls-hint {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
color: #eee;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
pointer-events: none;
}
</style>
<!-- Three.js CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
</head>
<body>
<div id="ui">
<div class="score-card" style="color: #33cc33;">BIRD: <span id="bird-score">0</span></div>
<div class="score-card" style="color: #3388ff;">DR. J: <span id="drj-score">0</span></div>
</div>
<div id="controls-hint">Press <b>SPACE</b> to Shoot (Larry Bird) / Trigger Shatter & Janitor</div>
<div id="canvas-container"></div>
<script>
// --- Retro Audio Synthesizer (Web Audio API) ---
const AudioEngine = {
ctx: null,
init() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
},
playDribble() {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(140, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(40, this.ctx.currentTime + 0.08);
gain.gain.setValueAtTime(0.6, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.08);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.08);
},
playSwish() {
if (!this.ctx) return;
const bufferSize = this.ctx.sampleRate * 0.15;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(1200, this.ctx.currentTime);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.5, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.15);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
noise.start();
},
playGlassShatter() {
if (!this.ctx) return;
// White noise burst + high-pass metallic ring
const bufferSize = this.ctx.sampleRate * 0.6;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const noise = this.ctx.createBufferSource();
noise.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'highpass';
filter.frequency.setValueAtTime(2500, this.ctx.currentTime);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(1.0, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.6);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.ctx.destination);
noise.start();
},
playMutter() {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(80 + Math.random() * 60, this.ctx.currentTime);
gain.gain.setValueAtTime(0.2, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(0.01, this.ctx.currentTime + 0.1);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.1);
}
};
// --- Scene Setup ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a1a);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 10, 22);
camera.lookAt(0, 4, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.getElementById('canvas-container').appendChild(renderer.domElement);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const spotLight = new THREE.DirectionalLight(0xffffff, 0.8);
spotLight.position.set(5, 20, 15);
spotLight.castShadow = true;
scene.add(spotLight);
// --- Court Floor ---
const courtGeo = new THREE.PlaneGeometry(30, 20);
const courtMat = new THREE.MeshStandardMaterial({ color: 0xb58042, roughness: 0.6 });
const court = new THREE.Mesh(courtGeo, courtMat);
court.rotation.x = -Math.PI / 2;
court.receiveShadow = true;
scene.add(court);
// Key / Paint Lines
const lineMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
const keyGeo = new THREE.RingGeometry(2.5, 2.6, 32, 1, 0, Math.PI);
const keyLine = new THREE.Mesh(keyGeo, lineMat);
keyLine.rotation.x = -Math.PI / 2;
keyLine.rotation.z = Math.PI / 2;
keyLine.position.set(0, 0.01, -2);
scene.add(keyLine);
// --- Hoop & Backboard ---
const hoopGroup = new THREE.Group();
hoopGroup.position.set(0, 0, -8);
const poleGeo = new THREE.CylinderGeometry(0.15, 0.15, 8);
const poleMat = new THREE.MeshStandardMaterial({ color: 0x333333 });
const pole = new THREE.Mesh(poleGeo, poleMat);
pole.position.set(0, 4, -1);
hoopGroup.add(pole);
const armGeo = new THREE.CylinderGeometry(0.1, 0.1, 1.5);
const arm = new THREE.Mesh(armGeo, poleMat);
arm.rotation.x = Math.PI / 3;
arm.position.set(0, 7.2, -0.4);
hoopGroup.add(arm);
// Glass Backboard
const backboardGeo = new THREE.BoxGeometry(4, 2.8, 0.1);
const backboardMat = new THREE.MeshStandardMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.7,
roughness: 0.1
});
const backboard = new THREE.Mesh(backboardGeo, backboardMat);
backboard.position.set(0, 7.5, 0);
hoopGroup.add(backboard);
// Rim
const rimGeo = new THREE.TorusGeometry(0.65, 0.05, 8, 24);
const rimMat = new THREE.MeshStandardMaterial({ color: 0xd63031 });
const rim = new THREE.Mesh(rimGeo, rimMat);
rim.rotation.x = Math.PI / 2;
rim.position.set(0, 6.7, 0.8);
hoopGroup.add(rim);
scene.add(hoopGroup);
// --- Shattered Glass Particles ---
let glassShards = [];
function shatterBackboard() {
backboard.visible = false;
AudioEngine.playGlassShatter();
const shardCount = 70;
const shardGeo = new THREE.BoxGeometry(0.2, 0.2, 0.05);
const shardMat = new THREE.MeshStandardMaterial({
color: 0xccffff,
transparent: true,
opacity: 0.8
});
for (let i = 0; i < shardCount; i++) {
const shard = new THREE.Mesh(shardGeo, shardMat);
shard.position.set(
(Math.random() - 0.5) * 3.5,
7.5 + (Math.random() - 0.5) * 2,
-8 + (Math.random() - 0.5) * 0.5
);
shard.userData = {
velocity: new THREE.Vector3(
(Math.random() - 0.5) * 0.15,
Math.random() * 0.1,
Math.random() * 0.2
),
rotSpeed: new THREE.Vector3(Math.random() * 0.2, Math.random() * 0.2, Math.random() * 0.2)
};
scene.add(shard);
glassShards.push(shard);
}
}
// --- Blocky Character Builder ---
function createPlayer(bodyColor, skinColor, hairColor) {
const group = new THREE.Group();
// Torso / Jersey
const torso = new THREE.Mesh(
new THREE.BoxGeometry(0.9, 1.2, 0.5),
new THREE.MeshStandardMaterial({ color: bodyColor })
);
torso.position.y = 2.4;
group.add(torso);
// Head
const head = new THREE.Mesh(
new THREE.BoxGeometry(0.5, 0.55, 0.5),
new THREE.MeshStandardMaterial({ color: skinColor })
);
head.position.y = 3.3;
group.add(head);
// Hair
const hair = new THREE.Mesh(
new THREE.BoxGeometry(0.55, 0.25, 0.55),
new THREE.MeshStandardMaterial({ color: hairColor })
);
hair.position.y = 3.6;
group.add(hair);
// Limbs
const limbMat = new THREE.MeshStandardMaterial({ color: skinColor });
const legGeo = new THREE.BoxGeometry(0.3, 1.2, 0.3);
const leftLeg = new THREE.Mesh(legGeo, limbMat);
leftLeg.position.set(-0.25, 1.2, 0);
group.add(leftLeg);
const rightLeg = new THREE.Mesh(legGeo, limbMat);
rightLeg.position.set(0.25, 1.2, 0);
group.add(rightLeg);
return group;
}
// Larry Bird: Green jersey, light skin, blond hair
const bird = createPlayer(0x00843d, 0xffdbac, 0xdec166);
bird.position.set(-3, 0, 0);
scene.add(bird);
// Dr. J: Blue/Red Sixers vibe, darker skin, dark hair / afro shape
const drJ = createPlayer(0x0046b8, 0x8d5524, 0x111111);
drJ.position.set(2, 0, -2);
scene.add(drJ);
// Basketball
const ballGeo = new THREE.SphereGeometry(0.32, 16, 16);
const ballMat = new THREE.MeshStandardMaterial({ color: 0xee6711, roughness: 0.4 });
const ball = new THREE.Mesh(ballGeo, ballMat);
ball.position.set(-2.5, 1.5, 0.8);
scene.add(ball);
// --- Janitor Builder ---
function createJanitor() {
const group = new THREE.Group();
// Overalls
const torso = new THREE.Mesh(
new THREE.BoxGeometry(0.8, 1.1, 0.5),
new THREE.MeshStandardMaterial({ color: 0x4a6572 })
);
torso.position.y = 2.2;
group.add(torso);
// Head + Cap
const head = new THREE.Mesh(
new THREE.BoxGeometry(0.45, 0.45, 0.45),
new THREE.MeshStandardMaterial({ color: 0xf5c6a5 })
);
head.position.y = 3.0;
group.add(head);
const cap = new THREE.Mesh(
new THREE.BoxGeometry(0.55, 0.15, 0.65),
new THREE.MeshStandardMaterial({ color: 0x333333 })
);
cap.position.set(0, 3.25, 0.05);
group.add(cap);
// Broom
const handle = new THREE.Mesh(
new THREE.CylinderGeometry(0.04, 0.04, 3),
new THREE.MeshStandardMaterial({ color: 0x8b5a2b })
);
handle.position.set(0.4, 1.8, 0.5);
handle.rotation.x = 0.2;
group.add(handle);
const bristles = new THREE.Mesh(
new THREE.BoxGeometry(0.6, 0.4, 0.2),
new THREE.MeshStandardMaterial({ color: 0xd2b48c })
);
bristles.position.set(0.4, 0.2, 0.8);
group.add(bristles);
group.position.set(-15, 0, -7); // Start off-court
group.visible = false;
return group;
}
const janitor = createJanitor();
scene.add(janitor);
// --- Game Logic & Animation States ---
let state = 'DRIBBLE'; // DRIBBLE, SHOOTING, SHATTERED, CLEANING
let shotProgress = 0;
let ballStartPos = new THREE.Vector3();
let scores = { bird: 0, drj: 0 };
let janitorState = { active: false, targetX: 0, timer: 0 };
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
AudioEngine.init();
if (state === 'DRIBBLE') {
state = 'SHOOTING';
shotProgress = 0;
ballStartPos.copy(ball.position);
}
}
});
// --- Animation Loop ---
let clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
const elapsed = clock.getElapsedTime();
// Idle Dribbling
if (state === 'DRIBBLE') {
const dribbleY = Math.abs(Math.sin(elapsed * 8)) * 1.2 + 0.3;
ball.position.set(bird.position.x + 0.5, dribbleY, bird.position.z + 0.6);
bird.position.y = Math.sin(elapsed * 4) * 0.05;
drJ.position.y = Math.cos(elapsed * 4) * 0.05;
if (dribbleY < 0.35 && Math.sin(elapsed * 8) < 0) {
AudioEngine.playDribble();
}
}
// Shooting Animation
if (state === 'SHOOTING') {
shotProgress += delta * 1.2;
const t = shotProgress;
// Quadratic arc to hoop rim
const target = new THREE.Vector3(0, 6.7, -7.2);
ball.position.x = THREE.MathUtils.lerp(ballStartPos.x, target.x, t);
ball.position.z = THREE.MathUtils.lerp(ballStartPos.z, target.z, t);
ball.position.y = THREE.MathUtils.lerp(ballStartPos.y, target.y, t) + Math.sin(t * Math.PI) * 4;
bird.position.y = Math.sin(Math.min(t * 2, 1) * Math.PI) * 0.8;
if (t >= 1) {
state = 'SHATTERED';
scores.bird += 2;
document.getElementById('bird-score').innerText = scores.bird;
shatterBackboard();
ball.position.set(0, 0.35, -7.2);
// Summon Janitor after a short delay
setTimeout(() => {
janitor.visible = true;
janitorState.active = true;
}, 800);
}
}
// Shard Physics Update
if (glassShards.length > 0) {
glassShards.forEach((shard) => {
if (shard.position.y > 0.05) {
shard.userData.velocity.y -= 9.8 * delta * 0.1;
shard.position.add(shard.userData.velocity);
shard.rotation.x += shard.userData.rotSpeed.x;
shard.rotation.y += shard.userData.rotSpeed.y;
} else {
shard.position.y = 0.02; // Settle on ground
}
});
}
// Janitor Clean-Up Logic
if (janitorState.active) {
if (janitor.position.x < 0) {
// Walking to hoop
janitor.position.x += delta * 3;
janitor.position.y = Math.abs(Math.sin(elapsed * 8)) * 0.15;
} else {
// Sweeping animation
janitorState.timer += delta;
janitor.rotation.y = Math.sin(janitorState.timer * 4) * 0.4;
if (Math.random() < 0.03) AudioEngine.playMutter();
// Gradually clean up shards
if (glassShards.length > 0 && Math.random() < 0.1) {
const removed = glassShards.pop();
scene.remove(removed);
}
// Finish and reset
if (janitorState.timer > 4) {
janitor.position.x -= delta * 3;
if (janitor.position.x < -14) {
janitorState.active = false;
janitor.visible = false;
janitor.rotation.y = 0;
janitorState.timer = 0;
// Restore backboard
backboard.visible = true;
state = 'DRIBBLE';
}
}
}
}
renderer.render(scene, camera);
}
// Window Resize Support
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();
</script>
</body>
</html>