<!DOCTYPE html>
<html lang="fa">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Computer Smash</title>
<style>
html, body {
margin: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #111;
font-family: Arial, sans-serif;
}
canvas {
display: block;
}
#ui {
position: fixed;
top: 15px;
left: 15px;
z-index: 10;
color: white;
background: rgba(0,0,0,.65);
padding: 12px;
border-radius: 10px;
user-select: none;
}
#reset {
margin-top: 8px;
width: 100%;
padding: 9px;
border: 0;
border-radius: 7px;
cursor: pointer;
font-weight: bold;
}
</style>
</head>
<body>
<div id="ui">
🏏 چوب را بکش و ضربه بزن
<br>
🖱️ برای چرخش دوربین هم بکش
<button id="reset">RESET</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
<script>
////////////////////////////////////////////////////
// SCENE
////////////////////////////////////////////////////
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x15181c);
const camera = new THREE.PerspectiveCamera(
60,
innerWidth / innerHeight,
0.1,
100
);
camera.position.set(8, 6, 10);
const renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
////////////////////////////////////////////////////
// LIGHT
////////////////////////////////////////////////////
scene.add(new THREE.HemisphereLight(
0xffffff,
0x333333,
2
));
const light = new THREE.DirectionalLight(
0xffffff,
3
);
light.position.set(5, 10, 7);
light.castShadow = true;
scene.add(light);
////////////////////////////////////////////////////
// MATERIALS
////////////////////////////////////////////////////
const black = new THREE.MeshStandardMaterial({
color: 0x17191d,
roughness: .55,
metalness: .35
});
const dark = new THREE.MeshStandardMaterial({
color: 0x292d33,
roughness: .45,
metalness: .3
});
const metal = new THREE.MeshStandardMaterial({
color: 0x666d75,
roughness: .3,
metalness: .8
});
const green = new THREE.MeshStandardMaterial({
color: 0x159447,
roughness: .5
});
const blue = new THREE.MeshStandardMaterial({
color: 0x1976d2,
roughness: .4
});
const screenMat = new THREE.MeshStandardMaterial({
color: 0x07151d,
emissive: 0x062536,
emissiveIntensity: 1.5,
roughness: .25
});
const white = new THREE.MeshStandardMaterial({
color: 0xd8d8d8,
roughness: .7
});
const wood = new THREE.MeshStandardMaterial({
color: 0x8b542d,
roughness: .8
});
const rubber = new THREE.MeshStandardMaterial({
color: 0x222222,
roughness: .9
});
////////////////////////////////////////////////////
// COMPUTER
////////////////////////////////////////////////////
const computer = new THREE.Group();
scene.add(computer);
const destructible = [];
function box(
x, y, z,
sx, sy, sz,
material,
name = "",
parent = computer
) {
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(sx, sy, sz),
material
);
mesh.position.set(x, y, z);
mesh.castShadow = true;
mesh.receiveShadow = true;
parent.add(mesh);
if (name) {
addDestructible(mesh, name);
}
return mesh;
}
function addDestructible(mesh, name) {
destructible.push({
mesh: mesh,
name: name,
originalParent: mesh.parent,
originalPosition: mesh.position.clone(),
originalQuaternion: mesh.quaternion.clone(),
velocity: new THREE.Vector3(),
angular: new THREE.Vector3(),
broken: false
});
mesh.userData.destructible = true;
}
////////////////////////////////////////////////////
// PC CASE
////////////////////////////////////////////////////
const tower = new THREE.Group();
tower.position.set(-2.8, 2.2, 0);
computer.add(tower);
// main case
const caseBody = box(
0, 0, 0,
2.4, 4.4, 2.2,
black,
"PC Case",
tower
);
// front
box(
0, 0, 1.13,
2.15, 4.1, .12,
dark,
"Front Panel",
tower
);
// glass side
const glassMat = new THREE.MeshStandardMaterial({
color: 0x18313c,
transparent: true,
opacity: .45,
metalness: .2,
roughness: .1
});
box(
1.21, 0, 0,
.08, 4.0, 2.0,
glassMat,
"Glass Side",
tower
);
// power button
const power = new THREE.Mesh(
new THREE.CylinderGeometry(.16, .16, .08, 24),
green
);
power.rotation.x = Math.PI / 2;
power.position.set(0, 1.65, 1.22);
power.castShadow = true;
tower.add(power);
addDestructible(power, "Power Button");
// USB ports
for (let i = 0; i < 3; i++) {
box(
-.65 + i * .35,
1.05,
1.21,
.22,
.1,
.05,
blue,
"USB Port",
tower
);
}
////////////////////////////////////////////////////
// MOTHERBOARD
////////////////////////////////////////////////////
box(
.2, 0, -.9,
1.5, 3.3, .08,
green,
"Motherboard",
tower
);
// RAM
for (let i = 0; i < 3; i++) {
box(
-.35 + i * .32,
.65,
-.84,
.15,
1.5,
.1,
blue,
"RAM",
tower
);
}
// CPU
box(
.2, .3, -.84,
.65, .65, .12,
metal,
"CPU",
tower
);
// CPU cooler
const cooler = new THREE.Mesh(
new THREE.CylinderGeometry(.38, .38, .3, 20),
metal
);
cooler.position.set(.2, .7, -.84);
cooler.castShadow = true;
tower.add(cooler);
addDestructible(cooler, "CPU Cooler");
// GPU
box(
.15, -.9, -.83,
1.65, .42, .28,
dark,
"Graphics Card",
tower
);
// GPU fans
for (let i = 0; i < 2; i++) {
const fan = new THREE.Mesh(
new THREE.CylinderGeometry(.15, .15, .04, 20),
black
);
fan.rotation.x = Math.PI / 2;
fan.position.set(
-.25 + i * .65,
-.9,
-.67
);
fan.castShadow = true;
tower.add(fan);
addDestructible(fan, "GPU Fan");
}
////////////////////////////////////////////////////
// MONITOR
////////////////////////////////////////////////////
const monitor = new THREE.Group();
monitor.position.set(1.2, 2.35, 0);
computer.add(monitor);
// monitor body
box(
0, 0, 0,
4.7, 3.0, .45,
black,
"Monitor Body",
monitor
);
// screen
box(
0, 0, .27,
4.25, 2.55, .08,
screenMat,
"Screen",
monitor
);
// screen border
box(
0, 1.4, .30,
4.7, .15, .12,
dark,
"Top Bezel",
monitor
);
box(
0, -1.4, .30,
4.7, .15, .12,
dark,
"Bottom Bezel",
monitor
);
// stand
box(
0, -2.0, 0,
.45, 1.0, .45,
metal,
"Monitor Stand",
monitor
);
// base
box(
0, -2.55, .1,
2.0, .18, 1.0,
metal,
"Monitor Base",
monitor
);
////////////////////////////////////////////////////
// KEYBOARD
////////////////////////////////////////////////////
const keyboard = new THREE.Group();
keyboard.position.set(1.2, .35, 3);
keyboard.rotation.x = -.08;
computer.add(keyboard);
box(
0, 0, 0,
4.0, .25, 1.55,
black,
"Keyboard Body",
keyboard
);
for (let row = 0; row < 4; row++) {
const count = row === 3 ? 7 : 10;
for (let col = 0; col < count; col++) {
const key = box(
-1.65 + col * .36,
.18,
-.48 + row * .32,
.27,
.13,
.23,
white,
"Keyboard Key",
keyboard
);
key.rotation.y =
(Math.random() - .5) * .04;
}
}
// spacebar
box(
0, .18, .48,
1.55, .13, .23,
white,
"Spacebar",
keyboard
);
////////////////////////////////////////////////////
// MOUSE
////////////////////////////////////////////////////
const mouse = new THREE.Group();
mouse.position.set(4, .45, 3);
computer.add(mouse);
box(
0, 0, 0,
.85, .35, 1.35,
dark,
"Mouse",
mouse
);
const wheel = new THREE.Mesh(
new THREE.CylinderGeometry(.1, .1, .08, 16),
white
);
wheel.rotation.z = Math.PI / 2;
wheel.position.set(0, .2, -.1);
mouse.add(wheel);
addDestructible(wheel, "Mouse Wheel");
////////////////////////////////////////////////////
// BAT
////////////////////////////////////////////////////
const bat = new THREE.Group();
scene.add(bat);
bat.position.set(
3.8,
2.2,
2.7
);
bat.rotation.z = -.7;
// handle
const handle = new THREE.Mesh(
new THREE.CylinderGeometry(
.13,
.16,
2.0,
16
),
wood
);
handle.position.y = -1;
handle.castShadow = true;
bat.add(handle);
// barrel
const barrel = new THREE.Mesh(
new THREE.CylinderGeometry(
.28,
.15,
2.6,
20
),
wood
);
barrel.position.y = .9;
barrel.castShadow = true;
bat.add(barrel);
// knob
const knob = new THREE.Mesh(
new THREE.SphereGeometry(.23, 16, 12),
wood
);
knob.position.y = -2.0;
bat.add(knob);
////////////////////////////////////////////////////
// DESK
////////////////////////////////////////////////////
box(
0, -.35, 0,
13, .7, 7,
dark,
""
);
// floor
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(40, 40),
new THREE.MeshStandardMaterial({
color: 0x303030,
roughness: 1
})
);
floor.rotation.x = -Math.PI / 2;
floor.position.y = -.72;
floor.receiveShadow = true;
scene.add(floor);
////////////////////////////////////////////////////
// CAMERA
////////////////////////////////////////////////////
let yaw = .7;
let pitch = .35;
let distance = 13;
const target = new THREE.Vector3(
0,
1.2,
0
);
function updateCamera() {
const cp = Math.cos(pitch);
camera.position.x =
target.x +
Math.sin(yaw) * cp * distance;
camera.position.z =
target.z +
Math.cos(yaw) * cp * distance;
camera.position.y =
target.y +
Math.sin(pitch) * distance;
camera.lookAt(target);
}
updateCamera();
////////////////////////////////////////////////////
// CAMERA MOUSE
////////////////////////////////////////////////////
let draggingCamera = false;
let lastX = 0;
let lastY = 0;
renderer.addEventListener("mousedown", e => {
draggingCamera = true;
lastX = e.clientX;
lastY = e.clientY;
});
window.addEventListener("mouseup", () => {
draggingCamera = false;
});
window.addEventListener("mousemove", e => {
if (!draggingCamera) return;
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
yaw -= dx * .008;
pitch += dy * .008;
pitch = Math.max(
-1.35,
Math.min(1.35, pitch)
);
lastX = e.clientX;
lastY = e.clientY;
updateCamera();
});
////////////////////////////////////////////////////
// CAMERA WHEEL
////////////////////////////////////////////////////
window.addEventListener("wheel", e => {
distance += e.deltaY * .01;
distance = Math.max(
5,
Math.min(25, distance)
);
updateCamera();
});
////////////////////////////////////////////////////
// BAT SWING
////////////////////////////////////////////////////
let swinging = false;
let pointerDownX = 0;
let pointerDownY = 0;
let pointerLastX = 0;
let pointerLastY = 0;
let swingVelocity = new THREE.Vector2();
function startSwing(x, y) {
swinging = true;
pointerDownX = x;
pointerDownY = y;
pointerLastX = x;
pointerLastY = y;
swingVelocity.set(0, 0);
}
function moveSwing(x, y) {
if (!swinging) return;
const dx = x - pointerLastX;
const dy = y - pointerLastY;
swingVelocity.set(dx, dy);
pointerLastX = x;
pointerLastY = y;
// Move bat according to pointer
bat.position.x += dx * .025;
bat.position.y -= dy * .025;
bat.rotation.z += dx * .025;
bat.rotation.x += dy * .015;
}
function endSwing() {
if (!swinging) return;
swinging = false;
const speed =
swingVelocity.length();
if (speed > 8) {
smash();
}
}
////////////////////////////////////////////////////
// POINTER EVENTS
////////////////////////////////////////////////////
renderer.addEventListener("mousedown", e => {
// If mouse is close to bat screen area,
// use it as a swing.
const rect =
renderer.domElement.getBoundingClientRect();
const x =
e.clientX - rect.left;
const y =
e.clientY - rect.top;
const batScreen =
bat.position.clone();
batScreen.project(camera);
const bx =
(batScreen.x * .5 + .5) *
rect.width;
const by =
(-batScreen.y * .5 + .5) *
rect.height;
const d =
Math.hypot(x - bx, y - by);
if (d < 220) {
startSwing(
e.clientX,
e.clientY
);
}
});
window.addEventListener("mousemove", e => {
if (swinging) {
moveSwing(
e.clientX,
e.clientY
);
}
});
window.addEventListener("mouseup", () => {
if (swinging) {
endSwing();
}
});
////////////////////////////////////////////////////
// TOUCH
////////////////////////////////////////////////////
renderer.addEventListener(
"touchstart",
e => {
if (e.touches.length !== 1) return;
const t = e.touches[0];
startSwing(
t.clientX,
t.clientY
);
},
{passive:false}
);
renderer.addEventListener(
"touchmove",
e => {
if (!swinging) return;
e.preventDefault();
const t = e.touches[0];
moveSwing(
t.clientX,
t.clientY
);
},
{passive:false}
);
renderer.addEventListener(
"touchend",
() => {
endSwing();
}
);
////////////////////////////////////////////////////
// SMASH
////////////////////////////////////////////////////
const temp = new THREE.Vector3();
function smash() {
// bat world position
const batWorld =
new THREE.Vector3();
bat.getWorldPosition(batWorld);
let hitSomething = false;
for (const part of destructible) {
if (part.broken) continue;
const mesh = part.mesh;
const worldPos =
new THREE.Vector3();
mesh.getWorldPosition(worldPos);
const dist =
worldPos.distanceTo(batWorld);
if (dist < 5.0) {
breakPart(
part,
batWorld
);
hitSomething = true;
}
}
// Stronger hit can break
// a wider group of parts
if (!hitSomething) {
for (const part of destructible) {
if (part.broken) continue;
const worldPos =
new THREE.Vector3();
part.mesh.getWorldPosition(worldPos);
if (worldPos.distanceTo(batWorld) < 6.5) {
breakPart(
part,
batWorld
);
}
}
}
}
////////////////////////////////////////////////////
// BREAK PART
////////////////////////////////////////////////////
function breakPart(part, hitPoint) {
const mesh = part.mesh;
if (part.broken) return;
part.broken = true;
// Preserve world transform
scene.attach(mesh);
const direction =
mesh.position.clone()
.sub(hitPoint);
if (direction.length() < .1) {
direction.set(
Math.random() - .5,
.5,
Math.random() - .5
);
}
direction.normalize();
const force =
5 + Math.random() * 7;
part.velocity.copy(
direction.multiplyScalar(force)
);
part.velocity.y +=
2 + Math.random() * 5;
part.angular.set(
(Math.random() - .5) * 10,
(Math.random() - .5) * 10,
(Math.random() - .5) * 10
);
}
////////////////////////////////////////////////////
// PHYSICS
////////////////////////////////////////////////////
const clock =
new THREE.Clock();
function updatePhysics(dt) {
for (const part of destructible) {
if (!part.broken) continue;
const mesh = part.mesh;
part.velocity.y -=
12 * dt;
mesh.position.addScaledVector(
part.velocity,
dt
);
mesh.rotation.x +=
part.angular.x * dt;
mesh.rotation.y +=
part.angular.y * dt;
mesh.rotation.z +=
part.angular.z * dt;
// floor collision
if (mesh.position.y < -.35) {
mesh.position.y = -.35;
part.velocity.y *= -.35;
part.velocity.x *= .82;
part.velocity.z *= .82;
part.angular.multiplyScalar(.88);
}
}
}
////////////////////////////////////////////////////
// RESET
////////////////////////////////////////////////////
function resetComputer() {
for (const part of destructible) {
const mesh = part.mesh;
part.broken = false;
// Reattach to original parent
part.originalParent.attach(mesh);
mesh.position.copy(
part.originalPosition
);
mesh.quaternion.copy(
part.originalQuaternion
);
part.velocity.set(0, 0, 0);
part.angular.set(0, 0, 0);
}
bat.position.set(
3.8,
2.2,
2.7
);
bat.rotation.set(
0,
0,
-.7
);
}
document.getElementById("reset")
.addEventListener(
"click",
resetComputer
);
window.addEventListener(
"keydown",
e => {
if (
e.key.toLowerCase() === "r"
) {
resetComputer();
}
}
);
////////////////////////////////////////////////////
// RESIZE
////////////////////////////////////////////////////
window.addEventListener(
"resize",
() => {
camera.aspect =
innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(
innerWidth,
innerHeight
);
}
);
////////////////////////////////////////////////////
// LOOP
////////////////////////////////////////////////////
function animate() {
requestAnimationFrame(animate);
const dt =
Math.min(
clock.getDelta(),
.033
);
updatePhysics(dt);
renderer.render(
scene,
camera
);
}
animate();
</script>
</body>
</html>⚠️Content was pasted as plain text and auto-formatted as a code block. Use the Code Block button in the editor for proper formatting.