Mobile-emulator / cubeX.html
ngiactcp
Add several new playable games to the application
61bdef8
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Rubik's 3D - Ultimate Edition</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { margin: 0; overflow: hidden; background: #050505; touch-action: none; font-family: 'Segoe UI', Roboto, sans-serif; color: white; }
canvas { display: block; }
.glass { background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); }
.btn-interact { transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); cursor: pointer; }
.btn-interact:active { transform: scale(0.92); }
.ui-fade { transition: opacity 0.4s ease, visibility 0.4s; }
.hidden-ui { opacity: 0; visibility: hidden; pointer-events: none; }
.timer-race { color: #f87171; text-shadow: 0 0 10px rgba(248, 113, 113, 0.5); }
.timer-zen { color: #60a5fa; }
</style>
</head>
<body>
<!-- HUD -->
<div id="hud" class="absolute inset-0 pointer-events-none p-6 flex flex-col justify-between hidden-ui z-10">
<div class="flex justify-between items-start">
<div class="glass p-4 rounded-2xl flex flex-col items-center min-w-[100px] border-blue-500/30">
<span id="mode-label" class="text-[10px] uppercase tracking-widest font-bold">Zen</span>
<span id="timer-val" class="text-2xl font-mono font-black">00:00</span>
</div>
<div class="flex flex-col gap-3 pointer-events-auto">
<button onclick="togglePause()" class="glass w-12 h-12 rounded-full flex items-center justify-center btn-interact hover:bg-white/10">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</button>
</div>
</div>
</div>
<!-- MAIN HOME MENU -->
<div id="home-menu" class="absolute inset-0 z-50 flex items-center justify-center p-6 bg-black">
<div class="w-full max-w-sm text-center">
<h1 class="text-6xl font-black italic tracking-tighter mb-10 text-transparent bg-clip-text bg-gradient-to-br from-white to-gray-600">CUBE.X</h1>
<div class="flex flex-col gap-4">
<button id="resume-btn" onclick="resumeGameAction()" class="glass p-5 rounded-2xl hidden flex items-center justify-between group btn-interact border-emerald-500/50 bg-emerald-500/5">
<div class="text-left">
<div class="text-lg font-bold text-emerald-400">Continue</div>
<div id="resume-info" class="text-xs text-gray-400">Resume saved state</div>
</div>
<div class="text-emerald-400"><svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"></path></svg></div>
</button>
<button onclick="setupGame('zen')" class="glass p-5 rounded-2xl flex items-center justify-between group btn-interact hover:bg-white/10 border-blue-500/20">
<div class="text-left">
<div class="text-lg font-bold">Zen Mode</div>
<div class="text-xs text-gray-400">Relax, learn, and practice</div>
</div>
<div class="text-blue-400 font-bold text-xs uppercase tracking-tighter">Unlimited</div>
</button>
<button onclick="setupGame('timed')" class="glass p-5 rounded-2xl flex items-center justify-between group btn-interact hover:bg-white/10 border-red-500/20">
<div class="text-left">
<div class="text-lg font-bold">Timed Race</div>
<div class="text-xs text-gray-400">Solve as fast as you can</div>
</div>
<div class="text-red-400 font-bold text-xs uppercase tracking-tighter">Speedrun</div>
</button>
<button onclick="resetGlobal()" class="mt-6 text-[10px] text-gray-600 hover:text-red-400 uppercase tracking-widest font-bold transition-colors">Reset Progress</button>
</div>
</div>
</div>
<!-- PAUSE OVERLAY -->
<div id="pause-overlay" class="absolute inset-0 z-40 flex items-center justify-center bg-black/80 backdrop-blur-md hidden-ui ui-fade">
<div class="text-center glass p-10 rounded-3xl max-w-xs w-full mx-6">
<h2 class="text-3xl font-black mb-6">PAUSED</h2>
<div class="flex flex-col gap-4">
<button onclick="togglePause()" class="bg-white text-black font-bold py-4 rounded-xl btn-interact">Resume</button>
<button onclick="saveAndGoHome()" class="glass font-bold py-4 rounded-xl btn-interact hover:bg-white/10">Save & Quit</button>
</div>
</div>
</div>
<script>
// --- IMPROVED AUDIO ENGINE ---
const AudioEngine = {
ctx: null,
init() {
if (!this.ctx) this.ctx = new (window.AudioContext || window.webkitAudioContext)();
if (this.ctx.state === 'suspended') this.ctx.resume();
},
playTurn() {
this.init();
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(180, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(60, this.ctx.currentTime + 0.15);
gain.gain.setValueAtTime(0.1, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.15);
osc.connect(gain); gain.connect(this.ctx.destination);
osc.start(); osc.stop(this.ctx.currentTime + 0.15);
},
playClick() {
this.init();
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(400, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(10, this.ctx.currentTime + 0.05);
gain.gain.setValueAtTime(0.05, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.05);
osc.connect(gain); gain.connect(this.ctx.destination);
osc.start(); osc.stop(this.ctx.currentTime + 0.05);
}
};
// --- GAME ENGINE ---
let scene, camera, renderer, cubeParent;
const pieces = [];
let isRotating = false, isPaused = false, gameActive = false;
let lon = -45, lat = 30, startLon = 0, startLat = 0;
const raycaster = new THREE.Raycaster(), mouse = new THREE.Vector2(), touchStart = new THREE.Vector2();
let intersected = null;
let gameTime = 0, timerInterval, currentMode = 'zen';
const step = 1.03;
const COLORS = { front:0xef4444, back:0xf97316, up:0xffffff, down:0xffd700, left:0x22c55e, right:0x2563eb, internal:0x111111 };
init();
animate();
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0xffffff, 0.8));
const light = new THREE.DirectionalLight(0xffffff, 0.4);
light.position.set(10, 20, 10);
scene.add(light);
createCube();
updateCamera();
checkSaves();
window.addEventListener('touchstart', onTouchStart, { passive: false });
window.addEventListener('touchmove', onTouchMove, { passive: false });
window.addEventListener('touchend', () => intersected = null);
window.addEventListener('resize', onResize);
}
function createCube() {
if (cubeParent) scene.remove(cubeParent);
cubeParent = new THREE.Group(); scene.add(cubeParent);
pieces.length = 0;
const geo = new THREE.BoxGeometry(1, 1, 1);
for (let x = -1; x <= 1; x++) {
for (let y = -1; y <= 1; y++) {
for (let z = -1; z <= 1; z++) {
const mats = [
new THREE.MeshLambertMaterial({ color: x === 1 ? COLORS.right : COLORS.internal }),
new THREE.MeshLambertMaterial({ color: x === -1 ? COLORS.left : COLORS.internal }),
new THREE.MeshLambertMaterial({ color: y === 1 ? COLORS.up : COLORS.internal }),
new THREE.MeshLambertMaterial({ color: y === -1 ? COLORS.down : COLORS.internal }),
new THREE.MeshLambertMaterial({ color: z === 1 ? COLORS.front : COLORS.internal }),
new THREE.MeshLambertMaterial({ color: z === -1 ? COLORS.back : COLORS.internal })
];
const cubie = new THREE.Mesh(geo, mats);
cubie.position.set(x * step, y * step, z * step);
const edges = new THREE.EdgesGeometry(geo);
cubie.add(new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x000000 })));
cubeParent.add(cubie); pieces.push(cubie);
}
}
}
}
function setupGame(mode) {
AudioEngine.playClick();
currentMode = mode;
gameTime = 0;
const timerEl = document.getElementById('timer-val');
const labelEl = document.getElementById('mode-label');
labelEl.textContent = mode;
timerEl.className = 'text-2xl font-mono font-black ' + (mode === 'timed' ? 'timer-race' : 'timer-zen');
createCube();
scramble(15);
document.getElementById('home-menu').classList.add('hidden-ui');
document.getElementById('hud').classList.remove('hidden-ui');
gameActive = true; isPaused = false;
startTimer();
}
function togglePause() {
AudioEngine.playClick();
isPaused = !isPaused;
const overlay = document.getElementById('pause-overlay');
if (isPaused) {
overlay.classList.remove('hidden-ui');
clearInterval(timerInterval);
} else {
overlay.classList.add('hidden-ui');
startTimer();
}
}
function saveAndGoHome() {
AudioEngine.playClick();
saveState();
location.reload();
}
function saveState() {
const data = {
mode: currentMode,
time: gameTime,
pieces: pieces.map(p => ({
pos: p.position.toArray(),
rot: [p.rotation.x, p.rotation.y, p.rotation.z]
}))
};
localStorage.setItem('cube_ultimate_save', JSON.stringify(data));
}
function checkSaves() {
const save = localStorage.getItem('cube_ultimate_save');
if (save) {
document.getElementById('resume-btn').classList.remove('hidden');
}
}
function resumeGameAction() {
AudioEngine.playClick();
const save = JSON.parse(localStorage.getItem('cube_ultimate_save'));
if (!save) return;
currentMode = save.mode;
gameTime = save.time;
document.getElementById('mode-label').textContent = currentMode;
save.pieces.forEach((pData, i) => {
pieces[i].position.fromArray(pData.pos);
pieces[i].rotation.set(pData.rot[0], pData.rot[1], pData.rot[2]);
});
document.getElementById('home-menu').classList.add('hidden-ui');
document.getElementById('hud').classList.remove('hidden-ui');
gameActive = true; isPaused = false;
startTimer();
}
function startTimer() {
if (timerInterval) clearInterval(timerInterval);
timerInterval = setInterval(() => {
gameTime++;
const mins = Math.floor(gameTime / 60).toString().padStart(2, '0');
const secs = (gameTime % 60).toString().padStart(2, '0');
document.getElementById('timer-val').textContent = `${mins}:${secs}`;
}, 1000);
}
function onTouchStart(e) {
if (isRotating || isPaused || !gameActive) return;
const t = e.touches[0];
touchStart.set(t.clientX, t.clientY);
startLon = lon; startLat = lat;
mouse.x = (t.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(t.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(pieces);
intersected = hits.length > 0 ? hits[0] : null;
}
function onTouchMove(e) {
if (isRotating || isPaused || !gameActive) return;
const t = e.touches[0];
const dx = t.clientX - touchStart.x;
const dy = t.clientY - touchStart.y;
const dist = Math.hypot(dx, dy);
if (!intersected || dist < 15) {
lon = startLon + dx * 0.3; lat = startLat + dy * 0.3;
updateCamera();
} else if (dist > 30) {
handleSwipe(dx, dy);
intersected = null;
}
}
function handleSwipe(dx, dy) {
const hit = intersected;
const normal = hit.face.normal.clone().applyQuaternion(hit.object.getWorldQuaternion(new THREE.Quaternion())).normalize();
const camForward = new THREE.Vector3(); camera.getWorldDirection(camForward);
const camRight = new THREE.Vector3().crossVectors(camera.up, camForward).normalize();
const camUp = camera.up.clone().normalize();
const swipeWorld = camRight.clone().multiplyScalar(dx).add(camUp.clone().multiplyScalar(-dy)).normalize();
const axisVec = new THREE.Vector3().crossVectors(normal, swipeWorld).normalize();
const axes = ["x", "y", "z"];
let axis = "x", maxVal = 0;
axes.forEach(a => { if (Math.abs(axisVec[a]) > maxVal) { maxVal = Math.abs(axisVec[a]); axis = a; } });
rotateLayer(axis, hit.object.position[axis], (Math.PI / 2) * (axisVec[axis] > 0 ? 1 : -1));
}
function rotateLayer(axis, layer, angle) {
if (isRotating) return;
isRotating = true;
AudioEngine.playTurn();
const group = new THREE.Group(); scene.add(group);
const activePieces = pieces.filter(p => Math.abs(p.position[axis] - layer) < 0.1);
activePieces.forEach(p => group.add(p));
const start = performance.now();
function anim(now) {
const t = Math.min((now - start) / 240, 1);
group.rotation[axis] = angle * (t * (2 - t));
if (t < 1) requestAnimationFrame(anim);
else {
group.rotation[axis] = angle; group.updateMatrixWorld();
activePieces.forEach(p => {
p.applyMatrix4(group.matrixWorld);
p.position.x = Math.round(p.position.x / step) * step;
p.position.y = Math.round(p.position.y / step) * step;
p.position.z = Math.round(p.position.z / step) * step;
p.rotation.set(Math.round(p.rotation.x/(Math.PI/2))*(Math.PI/2), Math.round(p.rotation.y/(Math.PI/2))*(Math.PI/2), Math.round(p.rotation.z/(Math.PI/2))*(Math.PI/2));
cubeParent.add(p);
});
scene.remove(group); isRotating = false;
}
} requestAnimationFrame(anim);
}
function scramble(moves = 10) {
let count = 0;
const interval = setInterval(() => {
const axis = ['x', 'y', 'z'][Math.floor(Math.random() * 3)];
const layers = [-step, 0, step];
rotateLayer(axis, layers[Math.floor(Math.random() * 3)], Math.random() > 0.5 ? Math.PI/2 : -Math.PI/2);
if (++count >= moves) clearInterval(interval);
}, 260);
}
function updateCamera() {
lat = Math.max(-85, Math.min(85, lat));
const phi = THREE.MathUtils.degToRad(90 - lat);
const theta = THREE.MathUtils.degToRad(lon);
camera.position.set(12 * Math.sin(phi) * Math.cos(theta), 12 * Math.cos(phi), 12 * Math.sin(phi) * Math.sin(theta));
camera.lookAt(0, 0, 0);
}
function resetGlobal() { localStorage.removeItem('cube_ultimate_save'); location.reload(); }
function onResize() { camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }
function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); }
</script>
</body>
</html>