<script>
let scene, camera, renderer, clock;
let currentScore = 0; // Current game score
let highScore = 0; // Highest score achieved
let lastHitScore = 0;
let isCharging = false;
let chargeStartTime = 0;
const MAX_CHARGE_TIME = 1.5;
const ARROW_SPEED_MULTIPLIER = 25; // Reduced from 40 for slower arrows
const GRAVITY = -9.8;
let bow, arrowInBow, target;
let activeArrows = [];
let groundMesh;
let mouseCrosshairElement;
const scoreDisplay = document.getElementById('score');
const highScoreDisplay = document.getElementById('high-score');
const lastHitScoreDisplay = document.getElementById('last-hit-score');
const powerBar = document.getElementById('power-bar');
let shootSound, hitSound, bullseyeSound;
const targetRingsData = [
{ radius: 0.15, score: 100, color: 0xFFFF00 }, { radius: 0.3, score: 50, color: 0xFFA500 },
{ radius: 0.5, score: 30, color: 0xFF0000 }, { radius: 0.75, score: 20, color: 0x0000FF },
{ radius: 1.0, score: 10, color: 0x000000 }
];
const boardThickness = 0.2;
const TARGET_BASE_RADIUS = targetRingsData[targetRingsData.length - 1].radius;
const SCREEN_PADDING_X = 0.1;
const SCREEN_PADDING_Y = 0.1;
init();
animate();
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x60a0d0);
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 100);
scene.fog = new THREE.Fog(0x60a0d0, camera.near + 15, camera.far - 15);
camera.position.set(0, 1.6, 0.2);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.getElementById('game-container').appendChild(renderer.domElement);
clock = new THREE.Clock();
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 15); directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 1024; directionalLight.shadow.mapSize.height = 1024;
scene.add(directionalLight);
const groundGeometry = new THREE.PlaneGeometry(200, 200);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x386641, roughness: 0.8 });
groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
groundMesh.rotation.x = -Math.PI / 2; groundMesh.receiveShadow = true; scene.add(groundMesh);
bow = createBow(); camera.add(bow);
bow.position.set(-0.25, -0.35, -0.7); bow.rotation.y = Math.PI / 20;
arrowInBow = createArrow(); positionArrowInBow(); bow.add(arrowInBow);
target = createTarget();
scene.add(target);
repositionTarget();
loadSounds();
// Load High Score
const storedHighScore = localStorage.getItem('bowArrowHighScore');
if (storedHighScore) {
highScore = parseInt(storedHighScore, 10);
}
updateHighScoreDisplay();
mouseCrosshairElement = document.getElementById('mouse-crosshair');
document.addEventListener('mousemove', onMouseMoveForCrosshair);
const gameContainer = document.getElementById('game-container');
gameContainer.addEventListener('mouseenter', () => {
if (mouseCrosshairElement) mouseCrosshairElement.style.display = 'block';
});
gameContainer.addEventListener('mouseleave', () => {
if (mouseCrosshairElement) mouseCrosshairElement.style.display = 'none';
});
window.addEventListener('resize', onWindowResize);
document.addEventListener('mousedown', onMouseDown);
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('contextmenu', onSetTargetPosition);
}
function updateHighScoreDisplay() {
if (highScoreDisplay) {
highScoreDisplay.textContent = `High Score: ${highScore}`;
}
}
function saveHighScore() {
localStorage.setItem('bowArrowHighScore', highScore.toString());
}
function onMouseMoveForCrosshair(event) {
if (mouseCrosshairElement) {
mouseCrosshairElement.style.left = `${event.clientX - mouseCrosshairElement.offsetWidth / 2}px`;
mouseCrosshairElement.style.top = `${event.clientY - mouseCrosshairElement.offsetHeight / 2}px`;
}
}
function createBow() { const bowGroup = new THREE.Group(); const limbMaterial = new THREE.MeshStandardMaterial({ color: 0x654321, roughness: 0.7, metalness: 0.2 }); const handleMaterial = new THREE.MeshStandardMaterial({ color: 0x4a2e1a, roughness: 0.6 }); const handleGeom = new THREE.CylinderGeometry(0.05, 0.06, 0.4, 12); const handle = new THREE.Mesh(handleGeom, handleMaterial); handle.rotation.z = Math.PI / 2; bowGroup.add(handle); const limbShape = new THREE.Shape(); limbShape.moveTo(0,0); limbShape.quadraticCurveTo(0.1, 0.2, 0.05, 0.4); limbShape.lineTo(-0.05, 0.4); limbShape.quadraticCurveTo(-0.1, 0.2, 0,0); const extrudeSettings = { depth: 0.04, bevelEnabled: false }; const limbGeom = new THREE.ExtrudeGeometry(limbShape, extrudeSettings); const upperLimb = new THREE.Mesh(limbGeom, limbMaterial); upperLimb.position.set(0.2, 0, 0); upperLimb.rotation.z = -Math.PI / 10; bowGroup.add(upperLimb); const lowerLimb = new THREE.Mesh(limbGeom, limbMaterial); lowerLimb.position.set(-0.2, 0, 0); lowerLimb.rotation.z = Math.PI + Math.PI / 10; bowGroup.add(lowerLimb); bowGroup.userData.upperLimbTip = new THREE.Vector3(0.2 + 0.4 * Math.sin(upperLimb.rotation.z), 0.4 * Math.cos(upperLimb.rotation.z), 0.02); bowGroup.userData.lowerLimbTip = new THREE.Vector3(-0.2 - 0.4 * Math.sin(lowerLimb.rotation.z), -0.4 * Math.cos(lowerLimb.rotation.z), 0.02); bowGroup.userData.initialNockingPoint = new THREE.Vector3(0, 0, 0.02); const stringMaterial = new THREE.LineBasicMaterial({ color: 0xcccccc, linewidth: 2 }); const stringPoints = [ bowGroup.userData.upperLimbTip, bowGroup.userData.initialNockingPoint, bowGroup.userData.lowerLimbTip ]; const stringGeometry = new THREE.BufferGeometry().setFromPoints(stringPoints); const bowstring = new THREE.Line(stringGeometry, stringMaterial); bowstring.name = 'bowstring'; bowGroup.add(bowstring); bowGroup.castShadow = true; return bowGroup; }
function positionArrowInBow() { if (!arrowInBow || !bow) return; arrowInBow.rotation.set(0, Math.PI / 2, 0); arrowInBow.position.copy(bow.userData.initialNockingPoint); arrowInBow.position.z += 0.35; arrowInBow.userData.initialLocalZ = arrowInBow.position.z; }
function createArrow() { const arrowGroup = new THREE.Group(); const shaftLength = 0.7; const shaftRadius = 0.008; const shaftMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.5 }); const shaftGeom = new THREE.CylinderGeometry(shaftRadius, shaftRadius, shaftLength, 8); const shaft = new THREE.Mesh(shaftGeom, shaftMaterial); shaft.rotation.z = Math.PI / 2; arrowGroup.add(shaft); const headMaterial = new THREE.MeshStandardMaterial({ color: 0x777777, metalness: 0.8, roughness: 0.4 }); const headGeom = new THREE.ConeGeometry(shaftRadius * 2.5, 0.05, 8); const head = new THREE.Mesh(headGeom, headMaterial); head.position.x = shaftLength / 2; head.rotation.z = -Math.PI / 2; arrowGroup.add(head); arrowGroup.castShadow = true; return arrowGroup; }
function createTarget() { const targetGroup = new THREE.Group(); const boardMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, map: createWoodTexture(), roughness: 0.9 }); const mainBoardRadius = targetRingsData[targetRingsData.length - 1].radius; const mainBoardGeom = new THREE.CylinderGeometry(mainBoardRadius, mainBoardRadius, boardThickness, 32); const mainBoard = new THREE.Mesh(mainBoardGeom, boardMaterial); mainBoard.receiveShadow = true; targetGroup.add(mainBoard); const ringVisualThickness = 0.02; for (let i = 0; i < targetRingsData.length; i++) { const ringData = targetRingsData[i]; const ringGeom = new THREE.CylinderGeometry(ringData.radius, ringData.radius, ringVisualThickness, 32); const ringMaterial = new THREE.MeshStandardMaterial({ color: ringData.color, roughness: 0.6 }); const ring = new THREE.Mesh(ringGeom, ringMaterial); ring.position.y = (boardThickness / 2) + (ringVisualThickness / 2) - (0.001 * i); targetGroup.add(ring); } return targetGroup; }
function createWoodTexture() { const canvas = document.createElement('canvas'); canvas.width = 256; canvas.height = 256; const context = canvas.getContext('2d'); context.fillStyle = '#A0522D'; context.fillRect(0, 0, 256, 256); for (let i = 0; i < 100; i++) { context.fillStyle = `rgba(0,0,0,${Math.random() * 0.15})`; context.fillRect(Math.random() * 256, Math.random() * 256, Math.random() * 100 + 50, Math.random() * 3 + 1); context.beginPath(); context.arc(Math.random() * 256, Math.random() * 256, Math.random() * 10 + 5, 0, Math.PI*2); context.fillStyle = `rgba(0,0,0,${Math.random() * 0.2 + 0.1})`; context.fill(); } const texture = new THREE.CanvasTexture(canvas); texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; texture.repeat.set(2,2); return texture; }
function loadSounds() { shootSound = new Audio('https://cdn.jsdelivr.net/gh/mrdoob/three.js@dev/examples/sounds/358232_j_s_bend_bow_release.mp3'); shootSound.volume = 0.4; hitSound = new Audio('https://cdn.jsdelivr.net/gh/mrdoob/three.js@dev/examples/sounds/hit.mp3'); hitSound.volume = 0.6; bullseyeSound = new Audio('https://actions.google.com/sounds/v1/impacts/crash.ogg'); bullseyeSound.volume = 0.7; }
function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }
function onMouseDown(event) { if (event.button === 0 && !isCharging) { isCharging = true; chargeStartTime = clock.getElapsedTime(); lastHitScoreDisplay.textContent = ""; } }
function onMouseUp(event) { if (event.button === 0 && isCharging) { isCharging = false; fireArrow(event.clientX, event.clientY); powerBar.style.width = '0%'; } }
function onSetTargetPosition(event) { event.preventDefault(); const mouse = new THREE.Vector2(); mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObject(groundMesh); if (intersects.length > 0) { const intersectionPoint = intersects[0].point; target.position.x = intersectionPoint.x; target.position.z = intersectionPoint.z; target.position.y = TARGET_BASE_RADIUS; const cameraXZPos = new THREE.Vector3(camera.position.x, target.position.y, camera.position.z); target.up = new THREE.Vector3(0,1,0); target.lookAt(cameraXZPos); target.rotation.x = Math.PI / 2; } }
function updateBowChargeVisuals(power) { const bowstring = bow.getObjectByName('bowstring'); if (!bowstring) return; const maxPullback = 0.25; const currentPullback = power * maxPullback; const positions = bowstring.geometry.attributes.position; positions.setZ(1, bow.userData.initialNockingPoint.z - currentPullback); positions.needsUpdate = true; if (arrowInBow) { arrowInBow.position.z = arrowInBow.userData.initialLocalZ - currentPullback; } }
function fireArrow(clickX, clickY) {
const chargeDuration = Math.min(clock.getElapsedTime() - chargeStartTime, MAX_CHARGE_TIME);
const power = chargeDuration / MAX_CHARGE_TIME;
const firedArrow = arrowInBow.clone();
arrowInBow.getWorldPosition(firedArrow.position);
scene.add(firedArrow);
const mouseNDC = new THREE.Vector2();
mouseNDC.x = (clickX / window.innerWidth) * 2 - 1;
mouseNDC.y = -(clickY / window.innerHeight) * 2 + 1;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouseNDC, camera);
const aimPoint = new THREE.Vector3();
const targetHeightPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -target.position.y);
if (raycaster.ray.intersectPlane(targetHeightPlane, aimPoint)) {
} else {
aimPoint.copy(raycaster.ray.at(100, new THREE.Vector3()));
}
const direction = new THREE.Vector3().subVectors(aimPoint, firedArrow.position).normalize();
firedArrow.velocity = direction.multiplyScalar(power * ARROW_SPEED_MULTIPLIER + 2); // Reduced base speed from 5 to 2
firedArrow.initialY = firedArrow.position.y;
firedArrow.time = 0;
if (firedArrow.velocity.lengthSq() > 0.001) {
const lookAtTarget = firedArrow.position.clone().add(firedArrow.velocity.clone().normalize());
firedArrow.up.set(0, 1, 0);
firedArrow.lookAt(lookAtTarget);
const fixedPostLookAtRotation = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -Math.PI / 2);
firedArrow.quaternion.multiply(fixedPostLookAtRotation);
}
activeArrows.push(firedArrow);
bow.remove(arrowInBow);
arrowInBow = createArrow();
positionArrowInBow();
bow.add(arrowInBow);
updateBowChargeVisuals(0);
if (shootSound) { shootSound.currentTime = 0; shootSound.play().catch(e => {}); }
}
function animate() { requestAnimationFrame(animate); const delta = clock.getDelta(); if (isCharging) { const chargeDuration = Math.min(clock.getElapsedTime() - chargeStartTime, MAX_CHARGE_TIME); const power = chargeDuration / MAX_CHARGE_TIME; powerBar.style.width = `${power * 100}%`; updateBowChargeVisuals(power); } else { updateBowChargeVisuals(0); } for (let i = activeArrows.length - 1; i >= 0; i--) { const arrow = activeArrows[i]; arrow.time += delta; arrow.velocity.y += GRAVITY * delta; arrow.position.add(arrow.velocity.clone().multiplyScalar(delta)); if (arrow.velocity.lengthSq() > 0.001) { const lookAtTarget = arrow.position.clone().add(arrow.velocity.clone().normalize()); arrow.up.set(0, 1, 0); arrow.lookAt(lookAtTarget); const fixedPostLookAtRotation = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -Math.PI / 2); arrow.quaternion.multiply(fixedPostLookAtRotation); } const arrowTipOffset = new THREE.Vector3(0.35, 0, 0); const arrowTipWorld = arrow.localToWorld(arrowTipOffset.clone()); const hitData = checkCollision(arrowTipWorld, target); if (hitData.hit) { onArrowHit(arrow, target, hitData.score); scene.remove(arrow); activeArrows.splice(i, 1); continue; } if (arrow.position.y < -5 || target.position.distanceTo(arrow.position) > camera.far + 20 ) { scene.remove(arrow); activeArrows.splice(i, 1); } } renderer.render(scene, camera); }
function checkCollision(arrowTipWorld, currentTarget) { if (!currentTarget || !currentTarget.parent) return {hit: false, score: 0}; const arrowTipInTargetLocal = currentTarget.worldToLocal(arrowTipWorld.clone()); if (Math.abs(arrowTipInTargetLocal.y) < boardThickness / 2 + 0.1) { const distSq = arrowTipInTargetLocal.x * arrowTipInTargetLocal.x + arrowTipInTargetLocal.z * arrowTipInTargetLocal.z; for (const ring of targetRingsData) { if (distSq < ring.radius * ring.radius) { return { hit: true, score: ring.score }; } } } return { hit: false, score: 0 }; }
function onArrowHit(arrow, hitTarget, hitScoreValue) {
currentScore += hitScoreValue;
lastHitScore = hitScoreValue;
scoreDisplay.textContent = `Score: ${currentScore}`;
lastHitScoreDisplay.textContent = `+${hitScoreValue} points!`;
if (currentScore > highScore) {
highScore = currentScore;
saveHighScore();
updateHighScoreDisplay();
console.log("New High Score!", highScore);
}
if (hitScoreValue === 100 && bullseyeSound) {
bullseyeSound.currentTime = 0; bullseyeSound.play().catch(e => {});
} else if (hitSound) {
hitSound.currentTime = 0; hitSound.play().catch(e => {});
}
repositionTarget();
}
function repositionTarget() {
if (!target) return;
const ndcX = THREE.MathUtils.randFloat(-1 + SCREEN_PADDING_X * 2, 1 - SCREEN_PADDING_X * 2);
const ndcY = THREE.MathUtils.randFloat(-1 + SCREEN_PADDING_Y * 2, 1 - SCREEN_PADDING_Y * 2);
const ndcZForUnproject = THREE.MathUtils.randFloat(0.3, 0.9);
const newTargetPosNDC = new THREE.Vector3(ndcX, ndcY, ndcZForUnproject);
const newTargetPosWorld = newTargetPosNDC.unproject(camera);
target.position.copy(newTargetPosWorld);
const minY = TARGET_BASE_RADIUS + 0.2;
if (target.position.y < minY) {
target.position.y = minY;
}
const maxY = 10;
if (target.position.y > maxY) {
target.position.y = maxY;
}
const minDistanceToCamera = 7;
if (target.position.distanceTo(camera.position) < minDistanceToCamera) {
const directionFromCamera = new THREE.Vector3().subVectors(target.position, camera.position).normalize();
target.position.copy(camera.position).add(directionFromCamera.multiplyScalar(minDistanceToCamera));
if (target.position.y < minY) target.position.y = minY;
if (target.position.y > maxY) target.position.y = maxY;
}
target.up.set(0, 1, 0);
const lookAtPos = new THREE.Vector3().copy(camera.position);
target.lookAt(lookAtPos);
target.rotation.x = Math.PI / 2;
// console.log("Target repositioned via unproject to (World):", target.position.x.toFixed(1), target.position.y.toFixed(1), target.position.z.toFixed(1));
// console.log("Distance to camera:", target.position.distanceTo(camera.position).toFixed(1));
}
</script>