<!DOCTYPE html>
<html lang="ja" >
<head>
<meta charset="UTF-8" >
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" >
<title>3 D Physics Gem Puzzle</title>
<style>
body { margin: 0 ; overflow: hidden; background-color: #111; font-family: 'Segoe UI', sans-serif; }
#ui { position: absolute; top: 20px; left: 20px; color: white; pointer-events: none; z-index: 10; text-shadow: 0 0 5px #000; }
h1 { margin: 0 ; font-size: 24 px; color: #FFD700; }
p { margin: 5 px 0 ; font-size: 14 px; color: #ccc; }
#score-box { font-size: 32px; font-weight: bold; margin-top: 10px; }
#combo-msg { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 40px; color: #fff; font-weight: bold; opacity: 0; transition: opacity 0.5s; pointer-events: none; text-shadow: 0 0 20px #FF00FF; white-space: nowrap; }
#loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 20px; }
</style>
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js" ></script>
<script type="importmap" >
{
"imports" : {
"three" : "https://unpkg.com/three@0.160.0/build/three.module.js" ,
"three/addons/" : "https://unpkg.com/three@0.160.0/examples/jsm/" ,
"cannon-es" : "https://unpkg.com/cannon-es@0.20.0/dist/cannon-es.js"
}
}
</script>
</head>
<body>
<div id="loading" >Loading 3 D Engine...</div>
<div id="ui" >
<h1>Gem Physics 3 D</h1>
<p>左クリック: 同色を消す (3 つ以上)</p>
<p>右クリック: 宝石をかき混ぜる (回転)</p>
<div id="score-box" >SCORE: <span id="score" >0 </span></div>
</div>
<div id="combo-msg" >COMBO!</div>
<script type="module" >
import * as THREE from 'three' ;
import * as CANNON from 'cannon-es' ;
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js' ;
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js' ;
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js' ;
const GEM_RADIUS = 0.8 ;
const BOARD_WIDTH = 12 ;
const BOARD_HEIGHT = 16 ;
const GEM_COUNT = 100 ;
const COLORS = [0xFF0055 , 0x00FF99 , 0x00CCFF , 0xFFFF00 , 0xAA00FF ];
const ROTATE_RADIUS = 3.5 ;
let score = 0 ;
let gems = [];
let world, scene, camera, renderer, composer;
let raycaster, mouse;
function init ( ) {
document.getElementById('loading' ).style.display = 'none' ;
world = new CANNON.World();
world.gravity.set (0 , -20 , 0 );
world.broadphase = new CANNON.NaiveBroadphase();
world.solver.iterations = 10 ;
const wallMaterial = new CANNON.Material();
const gemMaterial = new CANNON.Material();
const contactMat = new CANNON.ContactMaterial(wallMaterial, gemMaterial, {
friction: 0.1 ,
restitution: 0.3
});
world.addContactMaterial(contactMat);
scene = new THREE.Scene();
scene.background = new THREE.Color(0x111122 );
scene.fog = new THREE.FogExp2(0x111122 , 0.02 );
camera = new THREE.PerspectiveCamera(45 , window.innerWidth / window.innerHeight, 0.1 , 100 );
camera.position.set (0 , 20 , 25 );
camera.lookAt(0 , 5 , 0 );
const ambientLight = new THREE.AmbientLight(0x404040 );
scene.add (ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff , 2 );
dirLight.position.set (10 , 20 , 10 );
dirLight.castShadow = true ;
scene.add (dirLight);
const pointLight = new THREE.PointLight(0xffffff , 5 , 50 );
pointLight.position.set (0 , 10 , 5 );
scene.add (pointLight);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true ;
document.body.appendChild(renderer.domElement);
const renderScene = new RenderPass(scene, camera);
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5 , 0.4 , 0.85 );
bloomPass.threshold = 0.2 ;
bloomPass.strength = 0.8 ;
bloomPass.radius = 0.5 ;
composer = new EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
createContainer(wallMaterial);
for (let i = 0 ; i < GEM_COUNT; i++) {
setTimeout(() => spawnGem(gemMaterial), i * 50 );
}
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2();
window.addEventListener('resize' , onWindowResize);
window.addEventListener('mousedown' , onMouseDown);
window.addEventListener('contextmenu' , e => e.preventDefault());
animate();
}
function createContainer (material ) {
const floorShape = new CANNON.Box(new CANNON.Vec3(BOARD_WIDTH/2 , 1 , BOARD_WIDTH/2 ));
const floorBody = new CANNON.Body({ mass: 0 , material: material });
floorBody.addShape(floorShape);
floorBody.position.set (0 , -1 , 0 );
world.addBody(floorBody);
const floorMesh = new THREE.Mesh(
new THREE.BoxGeometry(BOARD_WIDTH, 2 , BOARD_WIDTH),
new THREE.MeshStandardMaterial({ color: 0x333344 , roughness: 0.8 })
);
floorMesh.position.copy(floorBody.position);
floorMesh.receiveShadow = true ;
scene.add (floorMesh);
const wallThickness = 1 ;
const h = BOARD_HEIGHT;
const positions = [
{ x: 0 , z: -BOARD_WIDTH/2 - wallThickness/2 , w: BOARD_WIDTH + wallThickness*2 , d: wallThickness },
{ x: 0 , z: BOARD_WIDTH/2 + wallThickness/2 , w: BOARD_WIDTH + wallThickness*2 , d: wallThickness },
{ x: -BOARD_WIDTH/2 - wallThickness/2 , z: 0 , w: wallThickness, d: BOARD_WIDTH },
{ x: BOARD_WIDTH/2 + wallThickness/2 , z: 0 , w: wallThickness, d: BOARD_WIDTH }
];
positions.forEach(pos => {
const shape = new CANNON.Box(new CANNON.Vec3(pos.w/2 , h/2 , pos.d/2 ));
const body = new CANNON.Body({ mass: 0 , material: material });
body.addShape(shape);
body.position.set (pos.x, h/2 , pos.z);
world.addBody(body);
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(pos.w, h, pos.d),
new THREE.MeshPhysicalMaterial({
color: 0x88ccff ,
transmission: 0.9 ,
opacity: 0.3 ,
transparent: true ,
roughness: 0.1
})
);
mesh.position.copy(body.position);
scene.add (mesh);
});
}
function spawnGem (material ) {
const colorIdx = Math.floor(Math.random() * COLORS.length);
const color = COLORS[colorIdx];
const shape = new CANNON.Sphere(GEM_RADIUS);
const body = new CANNON.Body({ mass: 5 , material: material });
body.addShape(shape);
body.position.set (
(Math.random() - 0.5 ) * (BOARD_WIDTH - 2 ),
BOARD_HEIGHT + Math.random() * 5 ,
(Math.random() - 0.5 ) * (BOARD_WIDTH - 2 )
);
body.linearDamping = 0.5 ;
body.angularDamping = 0.5 ;
world.addBody(body);
const geometry = new THREE.IcosahedronGeometry(GEM_RADIUS, 0 );
const mesh = new THREE.Mesh(geometry, new THREE.MeshPhysicalMaterial({
color: color,
metalness: 0.1 ,
roughness: 0.1 ,
transmission: 0.2 ,
thickness: 1.0 ,
emissive: color,
emissiveIntensity: 0.2
}));
mesh.castShadow = true ;
mesh.receiveShadow = true ;
scene.add (mesh);
gems.push({ mesh, body, colorIdx, id: body.id });
}
function onMouseDown (event ) {
event .preventDefault();
mouse.x = (event .clientX / window.innerWidth) * 2 - 1 ;
mouse.y = -(event .clientY / window.innerHeight) * 2 + 1 ;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(gems.map(g => g.mesh));
if (intersects.length > 0 ) {
const targetMesh = intersects[0 ].object ;
const targetGem = gems.find(g => g.mesh === targetMesh);
if (event .button === 2 ) {
rotateArea(targetGem.body.position);
} else {
tryMatch(targetGem);
}
}
}
function tryMatch (startGem ) {
const matchGroup = [startGem];
const checkQueue = [startGem];
const checkedIds = new Set([startGem.id]);
const MAX_DIST = GEM_RADIUS * 2.5 ;
while (checkQueue.length > 0 ) {
const current = checkQueue.shift();
for (let other of gems) {
if (checkedIds.has(other.id)) continue ;
if (other.colorIdx !== current.colorIdx) continue ;
const dist = current.body.position.distanceTo(other.body.position);
if (dist < MAX_DIST) {
matchGroup.push(other);
checkQueue.push(other);
checkedIds.add (other.id);
}
}
}
if (matchGroup.length >= 3 ) {
removeGems(matchGroup);
}
}
function removeGems (group ) {
const points = group .length * group .length * 100 ;
score += points;
updateUI(points, group .length);
group .forEach(gem => {
scene.remove (gem.mesh);
world.removeBody(gem.body);
const idx = gems.indexOf(gem);
if (idx > -1 ) gems.splice(idx, 1 );
});
setTimeout(() => {
for (let i=0 ; i<group .length; i++) {
spawnGem(gems[0 ]?.body.material || new CANNON.Material());
}
}, 500 );
}
function rotateArea (centerPos ) {
gems.forEach(gem => {
const pos = gem.body.position;
const dist = Math.sqrt(Math.pow(pos.x - centerPos.x, 2 ) + Math.pow(pos.z - centerPos.z, 2 ));
if (dist < ROTATE_RADIUS) {
gem.body.velocity.y += 5 ;
const angle = Math.atan2(pos.z - centerPos.z, pos.x - centerPos.x);
const force = 8 ;
gem.body.velocity.x += -Math.sin(angle) * force;
gem.body.velocity.z += Math.cos(angle) * force;
gem.body.velocity.x += (centerPos.x - pos.x) * 1 ;
gem.body.velocity.z += (centerPos.z - pos.z) * 1 ;
gem.body.angularVelocity.set (Math.random()*10 , Math.random()*10 , Math.random()*10 );
}
});
}
function updateUI (addedScore, count ) {
const scoreEl = document.getElementById('score' );
let current = parseInt(scoreEl.innerText);
const step = Math.ceil((score - current) / 10 );
const timer = setInterval(() => {
current += step;
if (current >= score) {
current = score;
clearInterval(timer);
}
scoreEl.innerText = current;
}, 30 );
if (count > 0 ) {
const msg = document.getElementById('combo-msg' );
msg.innerText = count + " CHAIN!\n+" + addedScore;
msg.style.opacity = 1 ;
msg.style.transform = "translate(-50%, -50%) scale(1.5)" ;
setTimeout(() => {
msg.style.opacity = 0 ;
msg.style.transform = "translate(-50%, -50%) scale(1)" ;
}, 1000 );
}
}
function onWindowResize ( ) {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}
function animate ( ) {
requestAnimationFrame(animate);
world.step(1 / 60 );
gems.forEach(gem => {
gem.mesh.position.copy(gem.body.position);
gem.mesh.quaternion.copy(gem.body.quaternion);
if (gem.body.position.y < -10 ) {
gem.body.position.set (0 , 20 , 0 );
gem.body.velocity.set (0 ,0 ,0 );
}
});
composer.render();
}
init();
</script>
</body>
</html>
copy
コメント