メインコンテンツへスキップ
見出し画像

自作ゲームに”敗北”を取り入れてみた。


    前回の記事↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓


    当時のルールでは、負ける要素も、詰む要素も無かったので、

    つまんないんですよねぇ。

    そこで!ビームを撃たせてみました!


    画像
    発射まで3…


    画像
    発射まで2…


    画像
    発射まで1…


    画像
    BANG!
    次の発射まで3…

    みたいな感じです。

    画像

    当たると死にます。(?)

    ここまで見てくれてありがとう!

    コード載せときます。↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

    <!DOCTYPE html>
    <html lang="ja">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>立方体の展開図パズル (Flashy Victory)</title>
        <style>
            body {
                margin: 0;
                overflow: hidden;
                background-color: #ffffff ;
                font-family: sans-serif;
            }
    
            canvas {
                display: block;
            }
    
            #victory  {
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                font-size: 100px;
                font-weight: 900;
                color: gold;
                text-shadow:
                    4px 4px 0px #b8860b ,
                    8px 8px 0px rgba(0, 0, 0, 0.2);
                display: none;
                pointer-events: none;
                z-index: 10;
                animation: popIn 0.5s ease-out forwards;
                white-space: nowrap;
            }
    
            #gameover  {
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                font-size: 100px;
                font-weight: 900;
                color: #ff0000 ;
                text-shadow: 4px 4px 0px #8b0000 ;
                display: none;
                pointer-events: none;
                z-index: 11;
                white-space: nowrap;
            }
    
            @keyframes popIn {
                0% {
                    transform: translate(-50%, -50%) scale(0) rotate(-30deg);
                    opacity: 0;
                }
    
                80% {
                    transform: translate(-50%, -50%) scale(1.2) rotate(5deg);
                    opacity: 1;
                }
    
                100% {
                    transform: translate(-50%, -50%) scale(1) rotate(0deg);
                    opacity: 1;
                }
            }
        </style>
    </head>
    
    <body>
        <div id="victory">EXCELLENT!</div>
    
        <div id="gameover">GAME OVER</div>
        <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/"
                }
            }
        </script>
        <script type="module">
            import * as THREE from 'three';
    
            // --- シーン設定 ---
            const scene = new THREE.Scene();
            scene.background = new THREE.Color(0xffffff);
    
            const aspect = window.innerWidth / window.innerHeight;
            const camera = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000);
            camera.position.set(0, 0, 35);
            camera.lookAt(0, 0, 0);
    
            const renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            document.body.appendChild(renderer.domElement);
    
            const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
            scene.add(ambientLight);
            const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
            dirLight.position.set(5, 10, 10);
            scene.add(dirLight);
    
            // --- グリッド設定 ---
            const SIZE = 2;
            const GRID_COLS = 10;
            const GRID_ROWS = 10;
            const TOTAL_WIDTH = SIZE * GRID_COLS;
            const TOTAL_HEIGHT = SIZE * GRID_ROWS;
            const GRID_OFFSET_X = -TOTAL_WIDTH / 2;
            const GRID_OFFSET_Y = TOTAL_HEIGHT / 2;
    
            const faceMaterial = new THREE.MeshPhongMaterial({
                color: 0x88ccff, side: THREE.DoubleSide,
                polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1
            });
            const faceMaterialVictory = new THREE.MeshPhongMaterial({
                color: 0xffd700, side: THREE.DoubleSide,
                shininess: 100,
                emissive: 0x442200,
                polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1
            });
            const yellowMaterial = new THREE.MeshPhongMaterial({
                color: 0xffff00, side: THREE.DoubleSide,
                polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1
            });
            const sparkleMaterial = new THREE.MeshBasicMaterial({
                color: 0x0088ff, transparent: true, opacity: 0.5, side: THREE.DoubleSide, depthTest: false
            });
            const teleportMaterial = new THREE.MeshBasicMaterial({
                color: 0xffaa00, transparent: true, opacity: 0.5, side: THREE.DoubleSide, depthTest: false
            });
    
            const lineMaterial = new THREE.LineBasicMaterial({ color: 0x000000 });
            const gridLineMaterial = new THREE.LineBasicMaterial({ color: 0xcccccc });
    
            function getCellPosition(col, row) {
                const x = GRID_OFFSET_X + (col * SIZE) + (SIZE / 2);
                const y = GRID_OFFSET_Y - (row * SIZE) - (SIZE / 2);
                return { x, y };
            }
    
            // --- 1. グリッド描画 ---
            const gridGroup = new THREE.Group();
            scene.add(gridGroup);
            for (let i = 0; i <= GRID_COLS; i++) {
                const x = GRID_OFFSET_X + (i * SIZE);
                const pts = [new THREE.Vector3(x, GRID_OFFSET_Y, 0), new THREE.Vector3(x, GRID_OFFSET_Y - TOTAL_HEIGHT, 0)];
                gridGroup.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), gridLineMaterial));
            }
            for (let j = 0; j <= GRID_ROWS; j++) {
                const y = GRID_OFFSET_Y - (j * SIZE);
                const pts = [new THREE.Vector3(GRID_OFFSET_X, y, 0), new THREE.Vector3(GRID_OFFSET_X + TOTAL_WIDTH, y, 0)];
                gridGroup.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), gridLineMaterial));
            }
    
            // --- 2. 展開図定義 (11種類) ---
            const PATTERNS = [
                // Group A
                [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 1, y: 2 }],
                [{ x: 2, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 1, y: 2 }],
                [{ x: 3, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 1, y: 2 }],
                [{ x: 3, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 0, y: 2 }],
                [{ x: 0, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 0, y: 2 }],
                [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 0, y: 2 }],
                // Group B
                [{ x: 0, y: 2 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 2, y: 0 }, { x: 3, y: 0 }],
                [{ x: 1, y: 2 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 2, y: 0 }, { x: 3, y: 0 }],
                [{ x: 2, y: 2 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 2, y: 0 }, { x: 3, y: 0 }],
                // Group C
                [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 2, y: 2 }, { x: 3, y: 2 }],
                // Group D
                [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 2, y: 0 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 4, y: 1 }]
            ];
    
            const validSignatures = new Set();
            function normalizeNet(cells) {
                let minX = Infinity, minY = Infinity;
                cells.forEach(c => { minX = Math.min(minX, c.x); minY = Math.min(minY, c.y); });
                const shifted = cells.map(c => ({ x: c.x - minX, y: c.y - minY }));
                shifted.sort((a, b) => (a.y - b.y) || (a.x - b.x));
                return shifted.map(p => `${p.x},${p.y}`).join('|');
            }
            PATTERNS.forEach(base => {
                let current = base.map(p => ({ ...p }));
                for (let f = 0; f < 2; f++) {
                    for (let r = 0; r < 4; r++) {
                        validSignatures.add(normalizeNet(current));
                        current = current.map(p => ({ x: -p.y, y: p.x }));
                    }
                    current = current.map(p => ({ x: -p.x, y: p.y }));
                }
            });
    
            // --- 4. 生成ロジック ---
            // MOVED DECLARATIONS UP TO AVOID TEMPORAL DEAD ZONE
            const occupiedCells = new Set();
            let netMeshes = [];
            let yellowList = [];
            let confettiActive = false;
            let particles = [];
            // Beam Vars
            let turnCount = 0;
            let beamTarget = null; // {type: 'col'|'row', index: number}
            let beamWarningMesh = null;
            let gameLocked = false;
    
            function initGame() {
                netMeshes.forEach(o => { scene.remove(o.mesh); scene.remove(o.line); });
                netMeshes = [];
                yellowList.forEach(o => { scene.remove(o.mesh); scene.remove(o.line); });
                yellowList = [];
                occupiedCells.clear();
                document.getElementById('victory').style.display = 'none';
                document.getElementById('gameover').style.display = 'none';
                turnCount = 0;
                beamTarget = null;
                if (beamWarningMesh) { scene.remove(beamWarningMesh); beamWarningMesh = null; }
                gameLocked = false;
                // Stop confetti
                confettiActive = false;
                particles.forEach(p => scene.remove(p));
                particles = [];
    
                // Net
                let attempts = 0;
                let success = false;
                let netCells = [];
    
                while (!success && attempts < 100) {
                    occupiedCells.clear();
                    netCells = [];
                    attempts++;
    
                    const patternBase = PATTERNS[Math.floor(Math.random() * PATTERNS.length)];
                    const rot = Math.floor(Math.random() * 4);
                    const flip = Math.random() < 0.5;
    
                    const pattern = patternBase.map(p => {
                        let x = p.x;
                        let y = p.y;
                        if (flip) { x = -x; }
                        for (let r = 0; r < rot; r++) { [x, y] = [-y, x]; }
                        return { x, y };
                    });
    
                    const minX = Math.min(...pattern.map(p => p.x));
                    const minY = Math.min(...pattern.map(p => p.y));
                    const normPattern = pattern.map(p => ({ x: p.x - minX, y: p.y - minY }));
                    const width = Math.max(...normPattern.map(p => p.x)) + 1;
                    const height = Math.max(...normPattern.map(p => p.y)) + 1;
                    const maxCol = GRID_COLS - width;
                    const maxRow = GRID_ROWS - height;
                    if (maxCol < 0 || maxRow < 0) continue;
    
                    const startCol = Math.floor(Math.random() * (maxCol + 1));
                    const startRow = Math.floor(Math.random() * (maxRow + 1));
                    for (let p of normPattern) {
                        const c = startCol + p.x;
                        const r = startRow + p.y;
                        netCells.push({ c, r });
                        occupiedCells.add(`${c},${r}`);
                    }
                    success = true;
                }
    
                netCells.forEach(cell => {
                    const pos = getCellPosition(cell.c, cell.r);
                    const mesh = new THREE.Mesh(new THREE.PlaneGeometry(SIZE, SIZE), faceMaterial.clone());
                    mesh.position.set(pos.x, pos.y, 0);
                    mesh.userData = { col: cell.c, row: cell.r, isFace: true };
                    scene.add(mesh);
                    const line = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.PlaneGeometry(SIZE, SIZE)), lineMaterial.clone());
                    line.position.set(pos.x, pos.y, 0);
                    scene.add(line);
                    netMeshes.push({ mesh, line, col: cell.c, row: cell.r });
                });
    
                // Yellow Cells
                const OFFSETS_RULES = [
                    { v: 1, h: 0 }, { v: 2, h: 0 }, { v: 3, h: 0 },
                    { v: 0, h: 1 }, { v: 0, h: 2 }, { v: 0, h: 3 },
                    { v: 2, h: 2 }, { v: 3, h: 3 },
                ];
                let yellowPlaced = false;
                attempts = 0;
                while (!yellowPlaced && attempts < 200) {
                    attempts++;
                    const c1 = 3 + Math.floor(Math.random() * 4);
                    const r1 = 3 + Math.floor(Math.random() * 4);
                    if (occupiedCells.has(`${c1},${r1}`)) continue;
    
                    const rule = OFFSETS_RULES[Math.floor(Math.random() * OFFSETS_RULES.length)];
                    const dirH = Math.random() < 0.5 ? 1 : -1;
                    const dirV = Math.random() < 0.5 ? 1 : -1;
                    const c2 = c1 + (rule.h * dirH);
                    const r2 = r1 + (rule.v * dirV);
    
                    if (c2 < 0 || c2 >= GRID_COLS || r2 < 0 || r2 >= GRID_ROWS) continue;
                    if (occupiedCells.has(`${c2},${r2}`)) continue;
                    if (c1 === c2 && r1 === r2) continue;
    
                    [{ c: c1, r: r1 }, { c: c2, r: r2 }].forEach(t => {
                        const pos = getCellPosition(t.c, t.r);
                        const mesh = new THREE.Mesh(new THREE.PlaneGeometry(SIZE, SIZE), yellowMaterial);
                        mesh.position.set(pos.x, pos.y, 0);
                        scene.add(mesh);
                        const line = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.PlaneGeometry(SIZE, SIZE)), lineMaterial);
                        line.position.set(pos.x, pos.y, 0);
                        scene.add(line);
                        yellowList.push({ mesh, line, col: t.c, row: t.r });
                    });
                    yellowPlaced = true;
                }
                document.body.style.backgroundColor = ''; // Clear debug
                // Initial Warning
                pickBeamTarget();
                drawBeamWarning();
            }
            initGame();
    
            // --- 5. インタラクション ---
            const raycaster = new THREE.Raycaster();
            const mouse = new THREE.Vector2();
            let selectedFace = null;
            let highlightMeshes = [];
    
            window.addEventListener('mousemove', (event) => {
                mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
                mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
            });
    
            window.addEventListener('contextmenu', (event) => {
                if (gameLocked) return;
                event.preventDefault();
                raycaster.setFromCamera(mouse, camera);
                const intersects = raycaster.intersectObjects(netMeshes.map(n => n.mesh));
    
                if (intersects.length > 0) {
                    const clickedMesh = intersects[0].object;
                    const target = netMeshes.find(n => n.mesh === clickedMesh);
                    if (selectedFace === target) {
                        deselectFace();
                    } else {
                        if (selectedFace) deselectFace();
                        selectFace(target);
                    }
                } else {
                    if (selectedFace) deselectFace();
                }
            });
    
            window.addEventListener('click', (event) => {
                if (gameLocked || !selectedFace) return;
                raycaster.setFromCamera(mouse, camera);
    
                const intersects = raycaster.intersectObjects(highlightMeshes);
                if (intersects.length > 0) {
                    const hit = intersects[0].object;
                    const targetCol = hit.userData.col;
                    const targetRow = hit.userData.row;
                    const isTeleport = hit.userData.isTeleport;
                    moveFace(selectedFace, targetCol, targetRow, isTeleport);
                }
            });
    
            function selectFace(faceObj) {
                selectedFace = faceObj;
                animateScale(faceObj.mesh, 1.2);
                animateScale(faceObj.line, 1.2);
                faceObj.mesh.renderOrder = 1;
                showValidMoves(faceObj);
            }
    
            function deselectFace() {
                if (!selectedFace) return;
                animateScale(selectedFace.mesh, 1.0);
                animateScale(selectedFace.line, 1.0);
                selectedFace.mesh.renderOrder = 0;
                selectedFace = null;
                clearHighlights();
            }
    
            function clearHighlights() {
                highlightMeshes.forEach(m => scene.remove(m));
                highlightMeshes = [];
            }
    
            function showValidMoves(faceObj) {
                clearHighlights();
                const otherFaces = netMeshes.filter(f => f !== faceObj);
                const candidates = [];
                const isValidPos = (c, r) => c >= 0 && c < GRID_COLS && r >= 0 && r < GRID_ROWS;
    
                otherFaces.forEach(f => {
                    [[0, 1], [0, -1], [1, 0], [-1, 0]].forEach(d => {
                        const nc = f.col + d[0];
                        const nr = f.row + d[1];
                        if (isValidPos(nc, nr)) {
                            const occupiedByNet = netMeshes.some(n => n.col === nc && n.row === nr);
                            const isYellow = yellowList.find(y => y.col === nc && y.row === nr);
    
                            if (!occupiedByNet && isYellow) {
                                const otherYellow = yellowList.find(y => y !== isYellow);
                                if (otherYellow) {
                                    const tc = otherYellow.col;
                                    const tr = otherYellow.row;
                                    if (!netMeshes.some(n => n.col === tc && n.row === tr)) {
                                        candidates.push({ c: nc, r: nr, isTeleport: true, destC: tc, destR: tr });
                                    }
                                }
                            } else if (!occupiedByNet && !isYellow) {
                                if (!candidates.some(cand => cand.c === nc && cand.r === nr)) {
                                    candidates.push({ c: nc, r: nr, isTeleport: false });
                                }
                            }
                        }
                    });
                });
    
                candidates.forEach(cand => {
                    let isValidCandidate = false;
    
                    // Check 1: Does moving to the target cell (cand.c, cand.r) form a valid net?
                    // This applies to BOTH normal moves and the "entry" into a teleport.
                    let entryCells = otherFaces.map(f => ({ x: f.col, y: f.row }));
                    entryCells.push({ x: cand.c, y: cand.r });
                    const entrySig = normalizeNet(entryCells);
    
                    if (validSignatures.has(entrySig)) {
                        if (cand.isTeleport) {
                            // Check 2: If teleport, is the destination ALSO valid?
                            let exitCells = otherFaces.map(f => ({ x: f.col, y: f.row }));
                            exitCells.push({ x: cand.destC, y: cand.destR });
                            const exitSig = normalizeNet(exitCells);
    
                            if (validSignatures.has(exitSig)) {
                                isValidCandidate = true;
                            }
                        } else {
                            // Normal move, already checked entrySig
                            isValidCandidate = true;
                        }
                    }
    
                    if (isValidCandidate) {
                        const pos = getCellPosition(cand.c, cand.r);
                        const mat = cand.isTeleport ? teleportMaterial : sparkleMaterial;
                        const hl = new THREE.Mesh(new THREE.PlaneGeometry(SIZE, SIZE), mat);
                        hl.position.set(pos.x, pos.y, 0.1);
                        hl.userData = { col: cand.c, row: cand.r, isTeleport: cand.isTeleport };
                        scene.add(hl);
                        highlightMeshes.push(hl);
                    }
                });
            }
    
            function moveFace(faceObj, targetCol, targetRow, isTeleport) {
                occupiedCells.delete(`${faceObj.col},${faceObj.row}`);
    
                let finalC = targetCol;
                let finalR = targetRow;
                const yellowEntry = yellowList.find(y => y.col === targetCol && y.row === targetRow);
                const yellowExit = yellowList.find(y => y !== yellowEntry);
    
                if (isTeleport && yellowExit) {
                    finalC = yellowExit.col;
                    finalR = yellowExit.row;
                }
    
                faceObj.col = finalC;
                faceObj.row = finalR;
                occupiedCells.add(`${finalC},${finalR}`);
                faceObj.mesh.userData.col = finalC;
                faceObj.mesh.userData.row = finalR;
    
                deselectFace();
    
                const targetPos = getCellPosition(targetCol, targetRow);
    
                if (isTeleport) {
                    // Phase 1: Spiral Suck In
                    animateComplex(faceObj.mesh, {
                        type: 'teleportIn',
                        targetPos: targetPos,
                        duration: 500,
                        onComplete: () => {
                            // Phase 2: Instant Move & Spiral Spit Out
                            const exitPos = getCellPosition(finalC, finalR);
                            faceObj.mesh.position.set(exitPos.x, exitPos.y, faceObj.mesh.position.z);
                            faceObj.line.position.set(exitPos.x, exitPos.y, faceObj.line.position.z);
    
                            animateComplex(faceObj.mesh, {
                                type: 'teleportOut',
                                targetPos: exitPos,
                                duration: 500,
                                onComplete: triggerVictory
                            });
                            animateComplex(faceObj.line, {
                                type: 'teleportOut',
                                targetPos: exitPos,
                                duration: 500
                            });
                        }
                    });
                    animateComplex(faceObj.line, {
                        type: 'teleportIn',
                        targetPos: targetPos,
                        duration: 500
                    });
    
                } else {
                    const finalPos = getCellPosition(finalC, finalR);
                    animateMove(faceObj.mesh, finalPos);
                    animateMove(faceObj.line, finalPos, () => {
                        advanceTurn();
                    });
                }
            }
    
            // --- Beam Hazard ---
            function advanceTurn() {
                turnCount++;
                if (turnCount % 3 === 0) {
                    fireBeam();
                }
            }
    
            function pickBeamTarget() {
                const isCol = Math.random() < 0.5;
                const idx = Math.floor(Math.random() * 10);
                beamTarget = { type: isCol ? 'col' : 'row', index: idx };
            }
    
            function drawBeamWarning() {
                if (beamWarningMesh) scene.remove(beamWarningMesh);
    
                const w = beamTarget.type === 'col' ? SIZE : TOTAL_WIDTH;
                const h = beamTarget.type === 'col' ? TOTAL_HEIGHT : SIZE;
                const geom = new THREE.PlaneGeometry(w, h);
                const mat = new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.2, side: THREE.DoubleSide });
                beamWarningMesh = new THREE.Mesh(geom, mat);
    
                let x, y;
                if (beamTarget.type === 'col') {
                    const pos = getCellPosition(beamTarget.index, 0); // row irrelevant for getCellPosition X
                    x = pos.x;
                    y = GRID_OFFSET_Y - TOTAL_HEIGHT / 2;
                } else {
                    const pos = getCellPosition(0, beamTarget.index);
                    x = GRID_OFFSET_X + TOTAL_WIDTH / 2;
                    y = pos.y;
                }
                beamWarningMesh.position.set(x, y, 0.05); // slightly above grid
                scene.add(beamWarningMesh);
            }
    
            function fireBeam() {
                if (!beamTarget) return; // Should not happen
    
                // Visual: Bright Beam
                const w = beamTarget.type === 'col' ? SIZE : TOTAL_WIDTH;
                const h = beamTarget.type === 'col' ? TOTAL_HEIGHT : SIZE;
                const geom = new THREE.PlaneGeometry(w, h);
                const mat = new THREE.MeshBasicMaterial({ color: 0xff0000, side: THREE.DoubleSide, opacity: 0.8, transparent: true });
                const beamMesh = new THREE.Mesh(geom, mat);
                beamMesh.position.copy(beamWarningMesh.position);
                beamMesh.position.z = 0.1;
                scene.add(beamMesh);
    
                // Remove warning
                scene.remove(beamWarningMesh);
                beamWarningMesh = null;
    
                // Check Collision
                let hit = false;
                netMeshes.forEach(n => {
                    if (beamTarget.type === 'col') {
                        if (n.col === beamTarget.index) hit = true;
                    } else {
                        if (n.row === beamTarget.index) hit = true;
                    }
                });
    
                // Fade out Beam
                let op = 0.8;
                const fadeId = setInterval(() => {
                    op -= 0.05;
                    beamMesh.material.opacity = op;
                    if (op <= 0) {
                        clearInterval(fadeId);
                        scene.remove(beamMesh);
                    }
                }, 50);
    
                if (hit) {
                    triggerGameOver();
                } else {
                    // Immediate next warning
                    pickBeamTarget();
                    drawBeamWarning();
                }
            }
    
            function triggerGameOver() {
                gameLocked = true;
                document.getElementById('gameover').style.display = 'block';
                setTimeout(() => {
                    initGame();
                }, 3000);
            }
    
            // --- Victory ---
            function triggerVictory() {
                gameLocked = true;
                document.getElementById('victory').style.display = 'block';
                yellowList.forEach(y => { scene.remove(y.mesh); scene.remove(y.line); });
                yellowList = [];
                netMeshes.forEach(n => { n.mesh.material = faceMaterialVictory; });
    
                // Start Confetti
                confettiActive = true;
                for (let i = 0; i < 100; i++) {
                    const geom = new THREE.PlaneGeometry(0.5, 0.5);
                    const mat = new THREE.MeshBasicMaterial({ color: Math.random() * 0xffffff, side: THREE.DoubleSide });
                    const p = new THREE.Mesh(geom, mat);
                    p.position.set(0, 0, 5); // Start center
                    // Random velocity
                    p.userData = {
                        vx: (Math.random() - 0.5) * 1,
                        vy: (Math.random() - 0.5) * 1 + 0.5, // slightly up
                        vz: (Math.random() - 0.5) * 1,
                        rotx: Math.random() * 0.2,
                        roty: Math.random() * 0.2
                    };
                    scene.add(p);
                    particles.push(p);
                }
            }
    
            // --- 6. アニメーション ---
            const animations = [];
    
            function animateScale(obj, targetScale) {
                animations.push({
                    obj: obj,
                    type: 'scale',
                    target: targetScale,
                    start: obj.scale.x,
                    startTime: Date.now(),
                    duration: 200
                });
            }
    
            function animateMove(obj, targetPos, onComplete) {
                animations.push({
                    obj: obj,
                    type: 'move',
                    target: targetPos,
                    startX: obj.position.x,
                    startY: obj.position.y,
                    startTime: Date.now(),
                    duration: 300,
                    onComplete: onComplete
                });
            }
    
            function animateComplex(obj, params) {
                animations.push({
                    obj: obj,
                    type: params.type,
                    target: params.targetPos,
                    startParams: {
                        x: obj.position.x, y: obj.position.y,
                        sx: obj.scale.x, rot: obj.rotation.z
                    },
                    startTime: Date.now(),
                    duration: params.duration,
                    onComplete: params.onComplete
                });
            }
    
            function updateAnimations() {
                const now = Date.now();
                for (let i = animations.length - 1; i >= 0; i--) {
                    const anim = animations[i];
                    const elapsed = now - anim.startTime;
                    const progress = Math.min(elapsed / anim.duration, 1);
    
                    const ease = 1 - Math.pow(1 - progress, 3);
    
                    if (anim.type === 'scale') {
                        const s = anim.start + (anim.target - anim.start) * ease;
                        anim.obj.scale.set(s, s, 1);
                    } else if (anim.type === 'move') {
                        const x = anim.startX + (anim.target.x - anim.startX) * ease;
                        const y = anim.startY + (anim.target.y - anim.startY) * ease;
                        anim.obj.position.set(x, y, anim.obj.position.z);
                    } else if (anim.type === 'teleportIn') {
                        const x = anim.startParams.x + (anim.target.x - anim.startParams.x) * ease;
                        const y = anim.startParams.y + (anim.target.y - anim.startParams.y) * ease;
                        anim.obj.position.set(x, y, anim.obj.position.z);
                        const s = Math.max(0, anim.startParams.sx * (1 - ease));
                        anim.obj.scale.set(s, s, 1);
                        anim.obj.rotation.z = anim.startParams.rot + (4 * Math.PI * ease);
                    } else if (anim.type === 'teleportOut') {
                        const s = ease;
                        anim.obj.scale.set(s, s, 1);
                        anim.obj.rotation.z = 4 * Math.PI * (1 - ease);
                    }
    
                    if (progress >= 1) {
                        if (anim.type === 'teleportOut') anim.obj.rotation.z = 0;
                        if (anim.onComplete) anim.onComplete();
                        animations.splice(i, 1);
                    }
                }
            }
    
            // --- メインループ ---
            let time = 0;
            function animate() {
                time += 0.05;
                highlightMeshes.forEach((mesh, idx) => {
                    mesh.material.opacity = 0.5 + Math.sin(time + idx) * 0.3;
                });
                updateAnimations();
    
                if (confettiActive) {
                    particles.forEach(p => {
                        p.position.x += p.userData.vx;
                        p.position.y += p.userData.vy;
                        p.position.z += p.userData.vz;
                        p.rotation.x += p.userData.rotx;
                        p.rotation.y += p.userData.roty;
                        // Gravity
                        p.userData.vy -= 0.02;
                        // Reset if too low
                        if (p.position.y < -20) {
                            p.position.set(0, 0, 5);
                            p.userData.vy = Math.random() * 1 + 0.5;
                        }
                    });
    
                    // Pulse Net
                    const scale = 1 + Math.sin(time * 0.2) * 0.05;
                    netMeshes.forEach(n => {
                        n.mesh.scale.set(scale, scale, 1);
                        n.line.scale.set(scale, scale, 1);
                    });
                }
    
                renderer.render(scene, camera);
            }
            renderer.setAnimationLoop(animate);
    
            window.addEventListener('resize', () => {
                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(window.innerWidth, window.innerHeight);
                renderer.render(scene, camera);
            });
        </script>
    </body>
    
    </html>
     
     
     
    フォロバ100%です。暇な時間を折り紙でつぶす人。たまに自分でも何を言っているのか分からなくなる。でもマイクラもフォトナもやる。(?)実は折り紙より得意。 …………………………

    あなたへのおすすめ