MODE: NORMAL
ATTEMPT: 1
GEOMETRY RUSH
HOLD SPACE TO JUMP / ORBS TO FLIP
PRESS SPACE TO START
obs.el.remove()); obstacles = []; try { soundPress.currentTime = 0; soundPress.play(); } catch(e){} requestAnimationFrame(gameLoop); } function gameLoop() { if (!isPlaying) return; // --- 1. ジャンプ・重力処理 --- let currentGravity = inverted ? -gravity : gravity; // 床・天井に接している時のジャンプ処理 if (isGrounded && isSpacePressed) { yVelocity = inverted ? -jumpForce : jumpForce; isGrounded = false; try { soundPress.currentTime = 0; soundPress.play(); } catch(e){} } yVelocity += currentGravity; playerY -= yVelocity; // 当たり判定用床・天井リミッター if (!inverted) { if (playerY <= 0) { playerY = 0; yVelocity = 0; isGrounded = true; } } else { if (playerY >= 266) { // 天井の高さ playerY = 266; yVelocity = 0; isGrounded = true; } } // プレイヤーの座標更新 if (!inverted) { player.style.bottom = (54 + playerY) + "px"; } else { player.style.bottom = (54 + playerY) + "px"; } // --- 2. ジオメトリダッシュ風回転アニメーション --- if (!isGrounded) { angle += inverted ? -8 : 8; } else { // 着地時は一番近い90度にピタッとスナップ angle = Math.round(angle / 90) * 90; } player.style.transform = `rotate(${angle}deg)`; // --- 3. スピードアップ (BGM同期風) --- gameSpeed += 0.002; // --- 4. 障害物・ポータルの生成 --- spawnTimer++; if (spawnTimer > 90 + Math.random() * 50) { spawnTimer = 0; // 30%の確率で重力反転ポータル、70%でスパイク(トゲ) if (Math.random() < 0.3) { createPortal(); } else { createSpike(); } } // --- 5. 障害物・ポータルの移動と衝突判定 --- for (let i = obstacles.length - 1; i >= 0; i--) { let obs = obstacles[i]; obs.x -= gameSpeed; obs.el.style.left = obs.x + "px"; // 画面外に出たら削除 if (obs.x < -40) { obs.el.remove(); obstacles.splice(i, 1); continue; } // 衝突判定 (超シビアなボックス判定) let pRect = player.getBoundingClientRect(); let oRect = obs.el.getBoundingClientRect(); if (pRect.left < oRect.right && pRect.right > oRect.left && pRect.top < oRect.bottom && pRect.bottom > oRect.top) { if (obs.type === "portal") { // ポータルに触れたら重力反転! inverted = !inverted; ceiling.style.display = inverted ? "block" : "none"; modeIndicator.innerText = inverted ? "MODE: INVERTED" : "MODE: NORMAL"; modeIndicator.style.color = inverted ? "#ff00ff" : "#00ff00"; obs.el.remove(); obstacles.splice(i, 1); } else { // スパイクに触れたら即死(ゲームオーバー) gameOver(); return; } } } requestAnimationFrame(gameLoop); } function createSpike() { let el = document.createElement("div"); el.className = "obstacle"; el.style.left = "800px"; // 重力反転中なら天井にトゲを生やす if (inverted && Math.random() > 0.3) { el.style.bottom = "auto"; el.style.top = "54px"; el.style.transform = "rotate(180deg)"; } else { el.style.bottom = "54px"; } gameContainer.appendChild(el); obstacles.push({ el: el, x: 800, type: "spike" }); } function createPortal() { let el = document.createElement("div"); el.className = "portal"; el.style.left = "800px"; el.style.top = "140px"; gameContainer.appendChild(el); obstacles.push({ el: el, x: 800, type: "portal" }); } function gameOver() { isPlaying = false; score++; // 死亡時にAttemptをカウントアップ messageBox.style.visibility = "visible"; messageBox.querySelector("h2").innerText = "PRESS SPACE TO RETRY"; try { soundHit.play(); } catch(e){} }