🔬 AI発明ギャラリー o-yuto 街作りゲーム
🔒 サンドボックス内で実行中 ⛶ 全画面で遊ぶ
HTML / CSS / JS
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Realistic 3D City Builder - High Fidelity Update</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <style>
        body { margin: 0; overflow: hidden; background-color: #a5f3fc; font-family: 'Inter', system-ui, sans-serif; }
        canvas { display: block; }
        .ui-panel {
            background: rgba(255, 255, 255, 0.7);
            backdrop-filter: blur(20px);
            -webkit-backdrop-filter: blur(20px);
            border: 1px solid rgba(255, 255, 255, 0.3);
            color: #0f172a;
            border-radius: 24px;
            pointer-events: auto;
            box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
        }
        .btn-build {
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            cursor: pointer;
            position: relative;
            overflow: hidden;
        }
        .btn-build:hover { background: rgba(255, 255, 255, 0.5); transform: translateY(-2px); }
        .btn-build.active {
            background: #0284c7;
            color: white;
            box-shadow: 0 0 20px rgba(2, 132, 199, 0.4);
        }
        #ui-layer {
            position: absolute;
            inset: 0;
            pointer-events: none;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            padding: 32px;
        }
        .stat-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.6; }
        .stat-value { font-size: 1.5rem; font-weight: 900; letter-spacing: -0.02em; }
    </style>
</head>
<body>

<div id="ui-layer">
    <div class="flex justify-center">
        <div class="ui-panel px-10 py-5 flex gap-12 items-center">
            <div class="text-center">
                <div class="stat-label text-amber-700">Budget</div>
                <div id="stat-money" class="stat-value text-amber-600">¥30,000</div>
            </div>
            <div class="h-12 w-px bg-slate-300/50"></div>
            <div class="text-center">
                <div class="stat-label text-blue-700">Population</div>
                <div id="stat-pop" class="stat-value text-blue-600">0</div>
            </div>
            <div class="h-12 w-px bg-slate-300/50"></div>
            <div class="text-center">
                <div class="stat-label text-rose-700">Happiness</div>
                <div id="stat-happiness" class="stat-value text-rose-500">100%</div>
            </div>
        </div>
    </div>

    <div class="flex justify-between items-end">
        <div class="ui-panel p-6 max-w-sm border-l-8 border-sky-500">
            <h2 class="text-xl font-black text-sky-900 mb-2">High Fidelity City</h2>
            <p id="msg" class="text-sm text-slate-600 leading-relaxed font-medium">
                リアルな質感を追求した新しい街づくり。
                夕暮れのライティングと洗練されたグリッドで理想の都市を。
            </p>
        </div>

        <div class="ui-panel p-3 flex flex-col gap-2 overflow-y-auto max-h-[70vh]">
            <button class="btn-build active p-3 rounded-2xl flex items-center gap-4" data-type="road">
                <div class="w-10 h-10 bg-slate-800 rounded-lg flex items-center justify-center text-xl shadow-inner">🛣️</div>
                <div class="text-left"><div class="text-sm font-bold">道路</div><div class="text-[10px] opacity-60">¥100</div></div>
            </button>
            <button class="btn-build p-3 rounded-2xl flex items-center gap-4" data-type="residential">
                <div class="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center text-xl shadow-inner">🏠</div>
                <div class="text-left"><div class="text-sm font-bold">住宅</div><div class="text-[10px] opacity-60">¥500</div></div>
            </button>
            <button class="btn-build p-3 rounded-2xl flex items-center gap-4" data-type="commercial">
                <div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center text-xl shadow-inner">🏢</div>
                <div class="text-left"><div class="text-sm font-bold">商業</div><div class="text-[10px] opacity-60">¥800</div></div>
            </button>
            <button class="btn-build p-3 rounded-2xl flex items-center gap-4" data-type="industrial">
                <div class="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center text-xl shadow-inner">🏭</div>
                <div class="text-left"><div class="text-sm font-bold">工業</div><div class="text-[10px] opacity-60">¥1,200</div></div>
            </button>
            <div class="h-px bg-slate-300/50 my-1"></div>
            <button class="btn-build p-3 rounded-2xl flex items-center gap-4" data-type="port">
                <div class="w-10 h-10 bg-cyan-100 rounded-lg flex items-center justify-center text-xl shadow-inner">🚢</div>
                <div class="text-left"><div class="text-sm font-bold text-cyan-800">港湾</div><div class="text-[10px] opacity-60">¥6,000</div></div>
            </button>
            <button class="btn-build p-3 rounded-2xl flex items-center gap-4 hover:bg-rose-100 mt-2" data-type="bulldoze">
                <div class="w-10 h-10 bg-rose-200 rounded-lg flex items-center justify-center text-xl shadow-inner">🚜</div>
                <div class="text-left"><div class="text-sm font-bold text-rose-700">撤去</div><div class="text-[10px] opacity-60">¥50</div></div>
            </button>
        </div>
    </div>
</div>

<script>
    const WORLD_SIZE = 64;
    const GRID_DIVISIONS = 64;
    
    let scene, camera, renderer, raycaster, mouse;
    let grid = {};
    let money = 30000;
    let currentType = 'road';
    let stats = { population: 0, jobs: 0, happiness: 100, portBonus: 1.0 };
    
    // Camera state
    let isRotating = false;
    let isDragging = false;
    let dragStartPos = new THREE.Vector2();
    let dragCurrentPos = new THREE.Vector2();
    let hasMoved = false;

    let theta = Math.PI / 4;
    let phi = Math.PI / 3.5;
    let targetTheta = theta;
    let targetPhi = phi;
    let radius = 45;
    let targetRadius = radius;
    
    let cameraTarget = new THREE.Vector3(0, 0, 0);
    let targetCameraTarget = new THREE.Vector3(0, 0, 0);

    const COLORS = {
        ground: 0x3a4d39,
        ocean: 0x075985,
        sky: 0xbae6fd,
        grid: 0x2d3a2d,
        window: 0xe0f2fe,
        asphalt: 0x1e293b
    };

    window.onload = init;

    function init() {
        scene = new THREE.Scene();
        scene.background = new THREE.Color(COLORS.sky);
        scene.fog = new THREE.FogExp2(COLORS.sky, 0.008);

        camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 0.1, 1000);
        
        renderer = new THREE.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        renderer.outputEncoding = THREE.sRGBEncoding;
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.toneMappingExposure = 1.2;
        document.body.appendChild(renderer.domElement);

        // Lights
        const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6);
        scene.add(hemiLight);

        const sun = new THREE.DirectionalLight(0xffdfba, 1.4);
        sun.position.set(60, 100, 40);
        sun.castShadow = true;
        sun.shadow.camera.left = -40;
        sun.shadow.camera.right = 40;
        sun.shadow.camera.top = 40;
        sun.shadow.camera.bottom = -40;
        sun.shadow.mapSize.width = 4096;
        sun.shadow.mapSize.height = 4096;
        sun.shadow.radius = 4;
        scene.add(sun);

        // Ground Mesh
        const groundGeo = new THREE.BoxGeometry(WORLD_SIZE, 1.5, WORLD_SIZE);
        const groundMat = new THREE.MeshStandardMaterial({ 
            color: COLORS.ground, 
            roughness: 0.8,
            metalness: 0.1
        });
        const ground = new THREE.Mesh(groundGeo, groundMat);
        ground.position.y = -0.75;
        ground.receiveShadow = true;
        scene.add(ground);

        // Grid Helper (Subtle)
        const gridHelper = new THREE.GridHelper(WORLD_SIZE, GRID_DIVISIONS, COLORS.grid, COLORS.grid);
        gridHelper.position.y = 0.01;
        gridHelper.material.transparent = true;
        gridHelper.material.opacity = 0.2;
        scene.add(gridHelper);

        // Water
        const waterGeo = new THREE.PlaneGeometry(3000, 3000);
        const waterMat = new THREE.MeshStandardMaterial({ 
            color: COLORS.ocean, 
            transparent: true, 
            opacity: 0.8,
            roughness: 0.1,
            metalness: 0.5
        });
        const water = new THREE.Mesh(waterGeo, waterMat);
        water.rotation.x = -Math.PI / 2;
        water.position.y = -1.2;
        scene.add(water);

        raycaster = new THREE.Raycaster();
        mouse = new THREE.Vector2();

        // Hover Marker
        const markerGeo = new THREE.PlaneGeometry(0.95, 0.95);
        const markerMat = new THREE.MeshBasicMaterial({ 
            color: 0xffffff, 
            transparent: true, 
            opacity: 0.2, 
            side: THREE.DoubleSide 
        });
        window.hoverMarker = new THREE.Mesh(markerGeo, markerMat);
        window.hoverMarker.rotation.x = -Math.PI/2;
        window.hoverMarker.position.y = 0.05;
        scene.add(window.hoverMarker);

        window.addEventListener('resize', onResize);
        window.addEventListener('mousemove', onMouseMove);
        window.addEventListener('mousedown', onMouseDown);
        window.addEventListener('mouseup', onMouseUp);
        window.addEventListener('wheel', onWheel, { passive: false });
        window.oncontextmenu = () => false;

        document.querySelectorAll('.btn-build').forEach(btn => {
            btn.addEventListener('click', (e) => {
                document.querySelectorAll('.btn-build').forEach(b => b.classList.remove('active'));
                btn.classList.add('active');
                currentType = btn.dataset.type;
                e.stopPropagation();
            });
        });

        setInterval(tickEconomy, 2000);
        animate();
    }

    // --- High Quality Assets ---
    function createBuildingGroup(type) {
        const g = new THREE.Group();
        const baseSize = 0.75 + Math.random() * 0.1;
        
        switch(type) {
            case 'road':
                const roadBase = new THREE.Mesh(
                    new THREE.PlaneGeometry(1, 1),
                    new THREE.MeshStandardMaterial({ color: COLORS.asphalt, roughness: 0.9 })
                );
                roadBase.rotation.x = -Math.PI/2;
                roadBase.position.y = 0.02;
                roadBase.receiveShadow = true;
                g.add(roadBase);
                
                const line = new THREE.Mesh(
                    new THREE.PlaneGeometry(0.08, 0.5),
                    new THREE.MeshBasicMaterial({ color: 0xffffff, opacity: 0.8, transparent: true })
                );
                line.rotation.x = -Math.PI/2;
                line.position.y = 0.025;
                g.add(line);
                break;

            case 'residential':
                const height = 0.6 + Math.random() * 0.6;
                const wallColor = Math.random() > 0.5 ? 0xf8fafc : 0xf1f5f9;
                const body = new THREE.Mesh(
                    new THREE.BoxGeometry(baseSize, height, baseSize),
                    new THREE.MeshStandardMaterial({ color: wallColor, roughness: 0.5 })
                );
                body.position.y = height/2;
                body.castShadow = true;
                body.receiveShadow = true;
                g.add(body);
                
                // Roof
                const roof = new THREE.Mesh(
                    new THREE.BoxGeometry(baseSize + 0.1, 0.1, baseSize + 0.1),
                    new THREE.MeshStandardMaterial({ color: 0x334155 })
                );
                roof.position.y = height + 0.05;
                g.add(roof);

                // Small windows
                for(let i=0; i<4; i++) {
                    const win = new THREE.Mesh(new THREE.PlaneGeometry(0.15, 0.15), new THREE.MeshBasicMaterial({ color: COLORS.window }));
                    win.position.set(0.38 * (i < 2 ? 1 : -1), height * 0.6, 0.2 * (i % 2 === 0 ? 1 : -1));
                    win.rotation.y = (i < 2 ? 1 : -1) * Math.PI/2;
                    g.add(win);
                }
                break;

            case 'commercial':
                const commH = 1.5 + Math.random() * 2.5;
                const commBody = new THREE.Mesh(
                    new THREE.BoxGeometry(baseSize, commH, baseSize),
                    new THREE.MeshStandardMaterial({ color: 0xcbd5e1, metalness: 0.3, roughness: 0.2 })
                );
                commBody.position.y = commH/2;
                commBody.castShadow = true;
                g.add(commBody);
                
                // Glass facade look
                const glass = new THREE.Mesh(
                    new THREE.BoxGeometry(baseSize + 0.02, commH * 0.8, baseSize - 0.2),
                    new THREE.MeshStandardMaterial({ color: 0x38bdf8, transparent: true, opacity: 0.4 })
                );
                glass.position.y = commH * 0.5;
                g.add(glass);
                break;

            case 'industrial':
                const indH = 0.8;
                const fac = new THREE.Mesh(
                    new THREE.BoxGeometry(0.9, indH, 0.8),
                    new THREE.MeshStandardMaterial({ color: 0x475569, metalness: 0.4 })
                );
                fac.position.y = indH/2;
                fac.castShadow = true;
                g.add(fac);
                
                const chimney = new THREE.Mesh(
                    new THREE.CylinderGeometry(0.12, 0.12, 1.8, 12),
                    new THREE.MeshStandardMaterial({ color: 0x1e293b })
                );
                chimney.position.set(0.25, 0.9, 0.2);
                chimney.castShadow = true;
                g.add(chimney);
                break;

            case 'port':
                const pier = new THREE.Mesh(
                    new THREE.BoxGeometry(1.8, 0.3, 3),
                    new THREE.MeshStandardMaterial({ color: 0x334155, roughness: 0.7 })
                );
                pier.position.y = 0.15;
                pier.receiveShadow = true;
                g.add(pier);
                
                const craneBase = new THREE.Mesh(
                    new THREE.BoxGeometry(0.4, 1.8, 0.4),
                    new THREE.MeshStandardMaterial({ color: 0xeab308 })
                );
                craneBase.position.set(-0.5, 0.9, -0.8);
                craneBase.castShadow = true;
                g.add(craneBase);
                break;
        }
        return g;
    }

    // --- Gameplay Core ---
    function build(x, z) {
        const gx = Math.round(x);
        const gz = Math.round(z);
        const key = `${gx},${gz}`;
        const limit = WORLD_SIZE / 2;
        
        if (Math.abs(gx) >= limit || Math.abs(gz) >= limit) return;

        if (currentType === 'bulldoze') {
            if (grid[key]) {
                if (money >= 50) {
                    money -= 50;
                    scene.remove(grid[key].mesh);
                    delete grid[key];
                    updateStats();
                }
            }
            return;
        }

        if (grid[key]) return;

        let cost = 0, pop = 0, jobs = 0, rev = 0, maint = 0;
        switch(currentType) {
            case 'road': cost = 100; break;
            case 'residential': cost = 500; pop = 24; rev = 180; maint = 40; break;
            case 'commercial': cost = 800; jobs = 30; rev = 450; maint = 100; break;
            case 'industrial': cost = 1200; jobs = 60; rev = 800; maint = 200; break;
            case 'port': cost = 6000; jobs = 120; rev = 2500; maint = 800; break;
        }

        if (money >= cost) {
            money -= cost;
            const mesh = createBuildingGroup(currentType);
            mesh.position.set(gx, 0, gz);
            scene.add(mesh);
            grid[key] = { type: currentType, mesh, pop, jobs, rev, maint };
            updateStats();
        }
    }

    function updateStats() {
        let pop = 0, jobs = 0, hasPort = false;
        Object.values(grid).forEach(item => {
            pop += item.pop || 0;
            jobs += item.jobs || 0;
            if (item.type === 'port') hasPort = true;
        });
        stats.population = pop;
        stats.jobs = jobs;
        stats.portBonus = hasPort ? 1.7 : 1.0;
        const ratio = pop > 0 ? Math.min(1.3, (jobs + 10) / (pop + 1)) : 1;
        stats.happiness = Math.min(100, Math.floor(ratio * 90));
        updateUI();
    }

    function tickEconomy() {
        let net = 0;
        Object.values(grid).forEach(item => {
            if (item.rev) {
                let m = stats.happiness / 100;
                if (item.type === 'industrial') m *= stats.portBonus;
                net += item.rev * m;
            }
            if (item.maint) net -= item.maint;
        });
        money += Math.floor(net);
        updateUI();
    }

    function updateUI() {
        document.getElementById('stat-money').textContent = `¥${money.toLocaleString()}`;
        document.getElementById('stat-pop').textContent = stats.population.toLocaleString();
        document.getElementById('stat-happiness').textContent = `${stats.happiness}%`;
    }

    // --- Controls ---
    function onMouseMove(e) {
        mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
        mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;

        if (isRotating) {
            targetTheta -= e.movementX * 0.005;
            targetPhi = THREE.MathUtils.clamp(targetPhi - e.movementY * 0.005, 0.2, Math.PI / 2.1);
        } else if (isDragging) {
            const forward = new THREE.Vector3();
            camera.getWorldDirection(forward);
            forward.y = 0; forward.normalize();
            const right = new THREE.Vector3().crossVectors(camera.up, forward).normalize();
            
            const factor = radius * 0.001;
            targetCameraTarget.addScaledVector(right, (e.clientX - dragCurrentPos.x) * factor);
            targetCameraTarget.addScaledVector(forward, (e.clientY - dragCurrentPos.y) * factor);
            
            const limit = WORLD_SIZE / 2;
            targetCameraTarget.x = THREE.MathUtils.clamp(targetCameraTarget.x, -limit, limit);
            targetCameraTarget.z = THREE.MathUtils.clamp(targetCameraTarget.z, -limit, limit);
            
            dragCurrentPos.set(e.clientX, e.clientY);
            if (dragStartPos.distanceTo(dragCurrentPos) > 5) hasMoved = true;
        }
    }

    function onMouseDown(e) {
        if (e.button === 2) isRotating = true;
        else if (e.button === 0) { isDragging = true; hasMoved = false; dragStartPos.set(e.clientX, e.clientY); dragCurrentPos.set(e.clientX, e.clientY); }
    }

    function onMouseUp(e) {
        if (e.button === 0 && isDragging && !hasMoved) {
            raycaster.setFromCamera(mouse, camera);
            const hits = raycaster.intersectObjects(scene.children);
            const ground = hits.find(h => h.object.geometry.type === "BoxGeometry");
            if (ground) build(ground.point.x, ground.point.z);
        }
        isRotating = false; isDragging = false;
    }

    function onWheel(e) {
        targetRadius = THREE.MathUtils.clamp(targetRadius + e.deltaY * 0.05, 10, 180);
    }

    function onResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    }

    function animate() {
        requestAnimationFrame(animate);
        
        theta += (targetTheta - theta) * 0.1;
        phi += (targetPhi - phi) * 0.1;
        radius += (targetRadius - radius) * 0.1;
        cameraTarget.lerp(targetCameraTarget, 0.1);
        
        camera.position.x = cameraTarget.x + radius * Math.sin(phi) * Math.cos(theta);
        camera.position.y = cameraTarget.y + radius * Math.cos(phi);
        camera.position.z = cameraTarget.z + radius * Math.sin(phi) * Math.sin(theta);
        camera.lookAt(cameraTarget);
        
        // Marker behavior
        if (!isDragging) {
            raycaster.setFromCamera(mouse, camera);
            const hits = raycaster.intersectObjects(scene.children);
            const ground = hits.find(h => h.object.geometry.type === "BoxGeometry");
            if (ground) {
                const gx = Math.round(ground.point.x);
                const gz = Math.round(ground.point.z);
                const limit = WORLD_SIZE / 2;
                if (Math.abs(gx) < limit && Math.abs(gz) < limit) {
                    window.hoverMarker.position.set(gx, 0.06, gz);
                    window.hoverMarker.visible = true;
                } else { window.hoverMarker.visible = false; }
            } else { window.hoverMarker.visible = false; }
        } else { window.hoverMarker.visible = false; }
        
        renderer.render(scene, camera);
    }
</script>

</body>
</html>

街作りゲーム

by o-yuto
🤖 使用したAI
Gemini
💡 工夫したこと・プロンプトのポイント
なるべく現代の街に近づけた
👁 11 回閲覧
📅 投稿:2026年3月21日 🔄 更新:2026年4月12日
応援スタンプを押してみよう!