🔬 AI発明ギャラリー tag RPGマップ生成ツール
🔒 サンドボックス内で実行中 ⛶ 全画面で遊ぶ
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>スタイリッシュ・アドベンチャーマップ生成器</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        body {
            background-color: #0f172a;
            color: #f8fafc;
            font-family: 'Helvetica Neue', Arial, 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', Meiryo, sans-serif;
            margin: 0;
            overflow: hidden;
            display: flex;
            flex-direction: column;
            height: 100vh;
        }
        #controls {
            background-color: rgba(15, 23, 42, 0.8);
            backdrop-filter: blur(10px);
            border-bottom: 1px solid #334155;
            padding: 1rem;
            display: flex;
            flex-wrap: wrap;
            gap: 1.5rem;
            align-items: center;
            z-index: 10;
        }
        .control-group {
            display: flex;
            flex-direction: column;
            gap: 0.4rem;
        }
        label {
            font-size: 0.875rem;
            color: #cbd5e1;
            font-weight: 500;
        }
        input[type="range"] {
            accent-color: #3b82f6;
        }
        select, button {
            background-color: #1e293b;
            color: #f8fafc;
            border: 1px solid #475569;
            border-radius: 0.375rem;
            padding: 0.5rem 1rem;
            font-size: 0.875rem;
            outline: none;
            transition: all 0.2s;
        }
        select:hover, button:hover {
            border-color: #64748b;
            background-color: #334155;
        }
        button {
            cursor: pointer;
            background-color: #2563eb;
            border-color: #2563eb;
            font-weight: bold;
        }
        button:hover {
            background-color: #1d4ed8;
            border-color: #1d4ed8;
        }
        #canvas-container {
            flex-grow: 1;
            position: relative;
            overflow: hidden;
            display: flex;
            justify-content: center;
            align-items: center;
            background: radial-gradient(circle at center, #1e293b 0%, #020617 100%);
        }
        canvas {
            box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
            image-rendering: -moz-crisp-edges;
            image-rendering: -webkit-crisp-edges;
            image-rendering: pixelated;
            image-rendering: crisp-edges;
        }
        #loading {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 1.5rem;
            font-weight: bold;
            color: #38bdf8;
            display: none;
            z-index: 20;
            text-shadow: 0 0 10px rgba(56, 189, 248, 0.5);
        }
    </style>
</head>
<body>

    <div id="controls">
        <div class="control-group">
            <label for="mapTypeSelect">マップの種類</label>
            <select id="mapTypeSelect">
                <option value="overworld">フィールド (自然・大陸)</option>
                <option value="town">市街地 (街・城壁)</option>
                <option value="dungeon">ダンジョン (洞窟)</option>
            </select>
        </div>
        <div class="control-group">
            <label for="themeSelect">環境テーマ</label>
            <select id="themeSelect">
                <option value="modern_nature">モダン・ネイチャー (鮮やか)</option>
                <option value="deep_exploration">ディープ・エクスプロレーション (重厚)</option>
                <option value="frost_peaks">フロスト・ピークス (寒冷・高山)</option>
                <option value="cyber_topography">サイバー・トポグラフィ (ネオン)</option>
            </select>
        </div>
        <div class="control-group">
            <label for="sizeSlider">マップサイズ (<span id="sizeValue">100</span>x<span id="sizeValue2">100</span>)</label>
            <input type="range" id="sizeSlider" min="50" max="400" value="100" step="10">
        </div>
        <div class="control-group">
            <label for="zoomSlider">ズーム倍率 (<span id="zoomValue">1.5</span>x)</label>
            <input type="range" id="zoomSlider" min="0.5" max="20" value="1.5" step="0.5">
        </div>
        <button id="generateBtn">世界を構築する</button>
        <div style="margin-left: auto; color: #94a3b8; font-size: 0.875rem; display: flex; align-items: center; gap: 0.5rem;">
            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8L22 12L18 16"/><path d="M2 12H22"/><path d="M6 16L2 12L6 8"/></svg>
            <span>矢印キー (または WASD) で移動</span>
        </div>
    </div>

    <div id="canvas-container">
        <div id="loading">Generating World...</div>
        <canvas id="mapCanvas"></canvas>
    </div>

    <script>
        const canvas = document.getElementById('mapCanvas');
        const ctx = canvas.getContext('2d', { alpha: false });
        
        const mapTypeSelect = document.getElementById('mapTypeSelect');
        const themeSelect = document.getElementById('themeSelect');
        const sizeSlider = document.getElementById('sizeSlider');
        const sizeValue = document.getElementById('sizeValue');
        const sizeValue2 = document.getElementById('sizeValue2');
        const zoomSlider = document.getElementById('zoomSlider');
        const zoomValue = document.getElementById('zoomValue');
        const generateBtn = document.getElementById('generateBtn');
        const loadingIndicator = document.getElementById('loading');

        let gridSize = parseInt(sizeSlider.value);
        let zoomLevel = parseFloat(zoomSlider.value);
        let mapData = []; 
        let player = { x: 0, y: 0 };

        // --- Simplex Noise Generator ---
        const ClassicalNoise = function(r) {
            if (r == undefined) r = Math;
            this.grad3 = [[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0], [1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1], [0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]]; 
            this.p = [];
            for (let i=0; i<256; i++) { this.p[i] = Math.floor(r.random()*256); }
            this.perm = []; 
            for(let i=0; i<512; i++) { this.perm[i] = this.p[i & 255]; }
        };
        ClassicalNoise.prototype.dot = function(g, x, y) { return g[0]*x + g[1]*y; };
        ClassicalNoise.prototype.noise = function(xin, yin) { 
            let n0 = 0, n1 = 0, n2 = 0; 
            const F2 = 0.5*(Math.sqrt(3.0)-1.0); 
            const s = (xin+yin)*F2; 
            const i = Math.floor(xin+s); 
            const j = Math.floor(yin+s); 
            const G2 = (3.0-Math.sqrt(3.0))/6.0; 
            const t = (i+j)*G2; 
            const X0 = i-t; 
            const Y0 = j-t; 
            const x0 = xin-X0; 
            const y0 = yin-Y0; 
            let i1, j1; 
            if(x0>y0) {i1=1; j1=0;} else {i1=0; j1=1;}      
            const x1 = x0 - i1 + G2; 
            const y1 = y0 - j1 + G2; 
            const x2 = x0 - 1.0 + 2.0 * G2; 
            const y2 = y0 - 1.0 + 2.0 * G2; 
            const ii = i & 255; 
            const jj = j & 255; 
            const gi0 = this.perm[ii+this.perm[jj]] % 12; 
            const gi1 = this.perm[ii+i1+this.perm[jj+j1]] % 12; 
            const gi2 = this.perm[ii+1+this.perm[jj+1]] % 12; 
            let t0 = 0.5 - x0*x0-y0*y0; 
            if(t0<0) { n0 = 0.0; } else { t0 *= t0; n0 = t0 * t0 * this.dot(this.grad3[gi0], x0, y0); }
            let t1 = 0.5 - x1*x1-y1*y1; 
            if(t1<0) { n1 = 0.0; } else { t1 *= t1; n1 = t1 * t1 * this.dot(this.grad3[gi1], x1, y1); }
            let t2 = 0.5 - x2*x2-y2*y2; 
            if(t2<0) { n2 = 0.0; } else { t2 *= t2; n2 = t2 * t2 * this.dot(this.grad3[gi2], x2, y2); }
            return 70.0 * (n0 + n1 + n2); 
        };
        const noiseGen = new ClassicalNoise();

        // --- Utils ---
        function adjustBrightness(hex, percent) {
            if (!hex) return '#000000';
            let r = parseInt(hex.substring(1,3), 16);
            let g = parseInt(hex.substring(3,5), 16);
            let b = parseInt(hex.substring(5,7), 16);
            r = Math.min(255, Math.max(0, parseInt(r * (100 + percent) / 100)));
            g = Math.min(255, Math.max(0, parseInt(g * (100 + percent) / 100)));
            b = Math.min(255, Math.max(0, parseInt(b * (100 + percent) / 100)));
            return "#" + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0');
        }

        function simpleHash(x, y) {
            return Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
        }

        // --- Unified Theme Definitions ---
        const themes = {
            'modern_nature': {
                ocean:     '#0ea5e9', shallows: '#38bdf8', sand: '#fde047', plains: '#4ade80', forest: '#16a34a', mountain: '#94a3b8', peak: '#f8fafc',
                road:      '#e2e8f0', town_wall: '#94a3b8', grass: '#4ade80',
                castle: '#cbd5e1', shop: '#fb923c', house: '#d6d3d1', plaza: '#f8fafc', water_feature: '#38bdf8',
                dungeon_floor: '#64748b', dungeon_wall: '#1e293b' 
            },
            'deep_exploration': {
                ocean:     '#0f172a', shallows: '#1e3a8a', sand: '#b45309', plains: '#064e3b', forest: '#022c22', mountain: '#3f3f46', peak: '#71717a',
                road:      '#3f3f46', town_wall: '#27272a', grass: '#064e3b',
                castle: '#52525b', shop: '#b45309', house: '#78716c', plaza: '#3f3f46', water_feature: '#1e3a8a',
                dungeon_floor: '#18181b', dungeon_wall: '#09090b'
            },
            'frost_peaks': {
                ocean:     '#1e1b4b', shallows: '#3b82f6', sand: '#e0f2fe', plains: '#f1f5f9', forest: '#94a3b8', mountain: '#475569', peak: '#0ea5e9',
                road:      '#cbd5e1', town_wall: '#334155', grass: '#f1f5f9',
                castle: '#e2e8f0', shop: '#60a5fa', house: '#94a3b8', plaza: '#cbd5e1', water_feature: '#3b82f6',
                dungeon_floor: '#334155', dungeon_wall: '#0f172a'
            },
            'cyber_topography': {
                ocean:     '#000000', shallows: '#0f172a', sand: '#06b6d4', plains: '#059669', forest: '#10b981', mountain: '#db2777', peak: '#f43f5e',
                road:      '#1e293b', town_wall: '#db2777', grass: '#020617',
                castle: '#f1f5f9', shop: '#d946ef', house: '#8b5cf6', plaza: '#334155', water_feature: '#0ea5e9',
                dungeon_floor: '#000000', dungeon_wall: '#10b981'
            }
        };

        // --- Generators ---
        function generateOverworld() {
            const frequency = 0.03;
            const seaLevel = 0.35;
            const offsetX = Math.random() * 10000;
            const offsetY = Math.random() * 10000;

            const tiers = [
                { type: 'ocean', limit: 0.35, steps: 1, baseLevel: 0, isWalkable: false, isWater: true },
                { type: 'shallows', limit: 0.45, steps: 2, baseLevel: 1, isWalkable: false, isWater: true },
                { type: 'sand', limit: 0.50, steps: 2, baseLevel: 3, isWalkable: true, isWater: false },
                { type: 'plains', limit: 0.65, steps: 3, baseLevel: 5, isWalkable: true, isWater: false },
                { type: 'forest', limit: 0.78, steps: 3, baseLevel: 8, isWalkable: true, isWater: false },
                { type: 'mountain', limit: 0.90, steps: 4, baseLevel: 11, isWalkable: true, isWater: false },
                { type: 'peak', limit: 1.00, steps: 3, baseLevel: 15, isWalkable: false, isWater: false }
            ];

            for (let x = 0; x < gridSize; x++) {
                mapData[x] = [];
                for (let y = 0; y < gridSize; y++) {
                    let nx = (x + offsetX) * frequency;
                    let ny = (y + offsetY) * frequency;
                    let elevation = 1 * noiseGen.noise(nx, ny) + 0.5 * noiseGen.noise(2 * nx, 2 * ny) + 0.25 * noiseGen.noise(4 * nx, 4 * ny);
                    elevation = Math.max(0, Math.min(1, (elevation + 1.75) / 3.5));
                    
                    let adjElev;
                    const intSea = 0.45;
                    if (elevation < seaLevel) adjElev = (elevation / seaLevel) * intSea;
                    else adjElev = intSea + ((elevation - seaLevel) / (1 - seaLevel)) * (1 - intSea);

                    let tierIdx = 6;
                    for (let i = 0; i < tiers.length; i++) {
                        if (adjElev <= tiers[i].limit) { tierIdx = i; break; }
                    }
                    const tier = tiers[tierIdx];
                    const prevLimit = tierIdx === 0 ? 0 : tiers[tierIdx - 1].limit;
                    const range = tier.limit - prevLimit;
                    const relative = Math.max(0, Math.min(1, range > 0 ? (adjElev - prevLimit) / range : 1));
                    const stepIndex = Math.min(tier.steps - 1, Math.floor(relative * tier.steps));
                    
                    mapData[x][y] = {
                        type: tier.type,
                        level: tier.baseLevel + stepIndex,
                        isWalkable: tier.isWalkable,
                        isWater: tier.isWater,
                        brightnessBoost: tier.isWater ? (adjElev / 0.45)*15 : stepIndex * 5
                    };
                }
            }
        }

        function generateTown() {
            // ベースは草地で初期化
            for (let x = 0; x < gridSize; x++) {
                mapData[x] = [];
                for (let y = 0; y < gridSize; y++) {
                    mapData[x][y] = { type: 'grass', level: 1, isWalkable: true, isWater: false, brightnessBoost: 0 };
                }
            }

            const mid = Math.floor(gridSize / 2);
            
            // 外壁と堀 (十字方向には門・道を作る)
            for (let x = 0; x < gridSize; x++) {
                for (let y = 0; y < gridSize; y++) {
                    if (x < 2 || x > gridSize - 3 || y < 2 || y > gridSize - 3) {
                        if (Math.abs(x - mid) <= 2 || Math.abs(y - mid) <= 2) {
                            mapData[x][y] = { type: 'road', level: 1, isWalkable: true, isWater: false, brightnessBoost: 0 };
                        } else {
                            mapData[x][y] = { type: 'town_wall', level: 12, isWalkable: false, isWater: false, brightnessBoost: 0 };
                        }
                        continue;
                    }
                    if (x === 2 || x === gridSize - 3 || y === 2 || y === gridSize - 3) {
                        if (Math.abs(x - mid) <= 2 || Math.abs(y - mid) <= 2) {
                            mapData[x][y] = { type: 'road', level: 1, isWalkable: true, isWater: false, brightnessBoost: 0 };
                        } else {
                            mapData[x][y] = { type: 'ocean', level: 0, isWalkable: false, isWater: true, brightnessBoost: 10 };
                        }
                        continue;
                    }
                }
            }

            // BSP (二分空間分割) を用いて区画を生成する
            const blocks = [];
            function partition(x, y, w, h, iterations) {
                const minBlockSize = 20; // ブロックが細かくなりすぎるのを防ぐ
                if (iterations === 0 || w < minBlockSize * 2 || h < minBlockSize * 2) {
                    blocks.push({
                        x1: x, y1: y, 
                        x2: x + w, y2: y + h, 
                        cx: x + w / 2, cy: y + h / 2, 
                        w: w, h: h
                    });
                    return;
                }

                let splitH = Math.random() > 0.5;
                if (w > h * 1.3) splitH = false;
                else if (h > w * 1.3) splitH = true;

                if (splitH) {
                    let splitRatio = 0.35 + Math.random() * 0.3;
                    let splitSize = Math.floor(h * splitRatio);
                    
                    if (splitSize < minBlockSize || (h - splitSize) < minBlockSize) {
                        blocks.push({x1: x, y1: y, x2: x + w, y2: y + h, cx: x + w / 2, cy: y + h / 2, w: w, h: h});
                        return;
                    }
                    partition(x, y, w, splitSize, iterations - 1);
                    partition(x, y + splitSize, w, h - splitSize, iterations - 1);
                } else {
                    let splitRatio = 0.35 + Math.random() * 0.3;
                    let splitSize = Math.floor(w * splitRatio);
                    
                    if (splitSize < minBlockSize || (w - splitSize) < minBlockSize) {
                        blocks.push({x1: x, y1: y, x2: x + w, y2: y + h, cx: x + w / 2, cy: y + h / 2, w: w, h: h});
                        return;
                    }
                    partition(x, y, splitSize, h, iterations - 1);
                    partition(x + splitSize, y, w - splitSize, h, iterations - 1);
                }
            }

            partition(3, 3, gridSize - 6, gridSize - 6, 12);

            const centerGridX = gridSize / 2;
            const centerGridY = gridSize / 2;

            blocks.sort((a, b) => {
                const da = Math.hypot(a.cx - centerGridX, a.cy - centerGridY);
                const db = Math.hypot(b.cx - centerGridX, b.cy - centerGridY);
                return da - db;
            });

            // ゾーン分け
            let castleCount = 0;
            blocks.forEach((blk) => {
                const distToCenter = Math.hypot(blk.cx - centerGridX, blk.cy - centerGridY);
                const normalizedDist = distToCenter / (gridSize / 2);

                if (castleCount === 0 && blk.w >= 18 && blk.h >= 18) {
                    blk.zone = 'castle';
                    castleCount++;
                } else if (normalizedDist < 0.35) {
                    blk.zone = Math.random() > 0.3 ? 'shop' : 'plaza';
                } else if (normalizedDist < 0.7) {
                    blk.zone = Math.random() > 0.4 ? 'house' : 'park';
                } else {
                    blk.zone = Math.random() > 0.5 ? 'house' : 'park';
                }
            });

            if (castleCount === 0 && blocks.length > 0) blocks[0].zone = 'castle';

            // 各ブロック内の描画
            blocks.forEach(blk => {
                const isEdgeX = blk.x2 >= gridSize - 4;
                const isEdgeY = blk.y2 >= gridSize - 4;
                
                const isMainRoadX = Math.abs(blk.cx - centerGridX) < 15 && blk.w > 20;
                const isMainRoadY = Math.abs(blk.cy - centerGridY) < 15 && blk.h > 20;
                const rWidthX = isMainRoadX ? 4 : 2;
                const rWidthY = isMainRoadY ? 4 : 2;

                const drawX2 = isEdgeX ? blk.x2 - 1 : blk.x2 - rWidthX;
                const drawY2 = isEdgeY ? blk.y2 - 1 : blk.y2 - rWidthY;

                // 大通り
                for(let x=blk.x1; x<blk.x2; x++) {
                    for(let y=blk.y1; y<blk.y2; y++) {
                        if (x > drawX2 || y > drawY2) {
                            mapData[x][y] = { type: 'road', level: 1, isWalkable: true, isWater: false, brightnessBoost: 0 };
                        }
                    }
                }

                const bw = drawX2 - blk.x1 + 1;
                const bh = drawY2 - blk.y1 + 1;
                if (bw <= 0 || bh <= 0) return;

                const drawBlkX1 = blk.x1;
                const drawBlkY1 = blk.y1;

                // --- 曲がりくねった裏路地 ---
                if (blk.zone === 'shop' || blk.zone === 'house') {
                    const numAlleys = Math.random() > 0.2 ? 2 : 1; 
                    for (let a = 0; a < numAlleys; a++) {
                        const isVertical = Math.random() > 0.5;
                        let cx = isVertical ? drawBlkX1 + 1 + Math.floor(Math.random() * (bw - 2)) : drawBlkX1;
                        let cy = isVertical ? drawBlkY1 : drawBlkY1 + 1 + Math.floor(Math.random() * (bh - 2));
                        let steps = 0;
                        while (steps < bw * bh) {
                            if (cx >= drawBlkX1 && cx <= drawX2 && cy >= drawBlkY1 && cy <= drawY2) {
                                mapData[cx][cy] = { type: 'road', level: 1, isWalkable: true, isWater: false, brightnessBoost: -8 };
                                if (Math.random() < 0.2 && cx + 1 <= drawX2) {
                                    mapData[cx+1][cy] = { type: 'road', level: 1, isWalkable: true, isWater: false, brightnessBoost: -8 };
                                }
                            }
                            if (isVertical) {
                                cy++;
                                if (cy > drawY2) break;
                                if (Math.random() < 0.4) cx += (Math.random() > 0.5 ? 1 : -1);
                                cx = Math.max(drawBlkX1, Math.min(drawX2, cx));
                            } else {
                                cx++;
                                if (cx > drawX2) break;
                                if (Math.random() < 0.4) cy += (Math.random() > 0.5 ? 1 : -1);
                                cy = Math.max(drawBlkY1, Math.min(drawY2, cy));
                            }
                            steps++;
                        }
                    }
                }

                // --- ゾーンごとの建築 ---
                if (blk.zone === 'castle') {
                    // 【城】橋を架けてアクセス可能にする
                    const cx = Math.floor(drawBlkX1 + bw/2);
                    const cy = Math.floor(drawBlkY1 + bh/2);
                    const maxCastleRadius = Math.min(10, Math.floor(Math.min(bw, bh) / 2) - 2); 
                    
                    const hashVal = simpleHash(cx, cy);
                    const hasMoat = (hashVal - Math.floor(hashVal)) > 0.4; // 60%でお堀あり

                    for(let x=drawBlkX1; x<=drawX2; x++) {
                        for(let y=drawBlkY1; y<=drawY2; y++) {
                            const dx = Math.abs(x - cx);
                            const dy = Math.abs(y - cy);
                            const dist = Math.max(dx, dy); 
                            
                            if (dist <= maxCastleRadius - 3) {
                                let level = 20 - Math.floor(dist * 1.5); 
                                if (dist > maxCastleRadius - 5 && dist <= maxCastleRadius - 3 && Math.abs(dx - dy) < 2) {
                                    level += 8; 
                                }
                                mapData[x][y] = { type: 'castle', level: Math.max(4, level), isWalkable: false, isWater: false, brightnessBoost: level*2 };
                            } else if (dist <= maxCastleRadius) {
                                // 堀または城壁+必ず十字の橋を架ける
                                const isBridge = (dx <= 1 || dy <= 1);
                                
                                if (isBridge) {
                                    mapData[x][y] = { type: 'road', level: 1, isWalkable: true, isWater: false, brightnessBoost: 0 };
                                } else {
                                    if (hasMoat) {
                                        mapData[x][y] = { type: 'water_feature', level: 0, isWalkable: false, isWater: true, brightnessBoost: 10 };
                                    } else {
                                        if (dist === maxCastleRadius) {
                                            mapData[x][y] = { type: 'town_wall', level: 8, isWalkable: false, isWater: false, brightnessBoost: 0 };
                                        } else {
                                            mapData[x][y] = { type: 'plaza', level: 1, isWalkable: true, isWater: false, brightnessBoost: 0 };
                                        }
                                    }
                                }
                            } else {
                                mapData[x][y] = { type: 'grass', level: 1, isWalkable: true, isWater: false, brightnessBoost: 0 };
                            }
                        }
                    }
                } else if (blk.zone === 'shop') {
                    for(let x=drawBlkX1; x<=drawX2; x++) {
                        for(let y=drawBlkY1; y<=drawY2; y++) {
                            if (mapData[x][y].type === 'road') continue;
                            const sx = Math.floor(x / 2);
                            const sy = Math.floor(y / 2);
                            const hash = simpleHash(sx*5, sy*3);
                            
                            if (hash - Math.floor(hash) > 0.15) {
                                const level = 4 + Math.floor((hash * 10) % 5); 
                                const colorVar = Math.floor((hash - 0.5) * 50);
                                mapData[x][y] = { type: 'shop', level: level, isWalkable: false, isWater: false, brightnessBoost: colorVar };
                            } else {
                                mapData[x][y] = { type: 'plaza', level: 1, isWalkable: true, isWater: false, brightnessBoost: 0 };
                            }
                        }
                    }
                } else if (blk.zone === 'house') {
                    for(let x=drawBlkX1; x<=drawX2; x++) {
                        for(let y=drawBlkY1; y<=drawY2; y++) {
                            if (mapData[x][y].type === 'road') continue;
                            const sx = Math.floor(x / 3);
                            const sy = Math.floor(y / 3);
                            const hash = simpleHash(sx*7, sy*11);
                            const inX = x % 3;
                            const inY = y % 3;
                            
                            if (hash - Math.floor(hash) > 0.25 && inX < 2 && inY < 2) {
                                const level = 3 + Math.floor((hash * 10) % 3);
                                mapData[x][y] = { type: 'house', level: level, isWalkable: false, isWater: false, brightnessBoost: Math.floor((hash - 0.5) * 20) };
                            } else {
                                mapData[x][y] = { type: 'grass', level: 1, isWalkable: true, isWater: false, brightnessBoost: -5 };
                            }
                        }
                    }
                } else if (blk.zone === 'plaza') {
                    const cx = Math.floor(drawBlkX1 + bw/2);
                    const cy = Math.floor(drawBlkY1 + bh/2);
                    for(let x=drawBlkX1; x<=drawX2; x++) {
                        for(let y=drawBlkY1; y<=drawY2; y++) {
                            const dist = Math.hypot(x - cx, y - cy);
                            if (dist < 3.5) { 
                                mapData[x][y] = { type: 'water_feature', level: 1, isWalkable: false, isWater: true, brightnessBoost: 20 };
                            } else if (dist < 4.5) { 
                                mapData[x][y] = { type: 'castle', level: 3, isWalkable: false, isWater: false, brightnessBoost: 0 }; 
                            } else {
                                mapData[x][y] = { type: 'plaza', level: 1, isWalkable: true, isWater: false, brightnessBoost: 0 };
                            }
                        }
                    }
                } else if (blk.zone === 'park') {
                    const isPondPark = bw > 15 && bh > 15 && Math.random() > 0.5;
                    const cx = Math.floor(drawBlkX1 + bw/2);
                    const cy = Math.floor(drawBlkY1 + bh/2);
                    const pondRadius = Math.min(bw, bh) / 3;

                    for(let x=drawBlkX1; x<=drawX2; x++) {
                        for(let y=drawBlkY1; y<=drawY2; y++) {
                            const dist = Math.hypot(x - cx, y - cy);
                            
                            if (isPondPark && dist < pondRadius) {
                                mapData[x][y] = { type: 'water_feature', level: 0, isWalkable: false, isWater: true, brightnessBoost: 10 };
                            } else if (isPondPark && dist < pondRadius + 1) {
                                mapData[x][y] = { type: 'sand', level: 1, isWalkable: true, isWater: false, brightnessBoost: 0 };
                            } else {
                                const hash = simpleHash(Math.floor(x/2), Math.floor(y/2)); 
                                const fineHash = simpleHash(x, y); 
                                
                                if (hash - Math.floor(hash) > 0.4 && fineHash - Math.floor(fineHash) > 0.3) {
                                    const treeHeight = 4 + Math.floor((fineHash*10)%4);
                                    mapData[x][y] = { type: 'forest', level: treeHeight, isWalkable: false, isWater: false, brightnessBoost: 0 };
                                } else if (fineHash - Math.floor(fineHash) > 0.8) {
                                    mapData[x][y] = { type: 'road', level: 1, isWalkable: true, isWater: false, brightnessBoost: -10 }; 
                                } else {
                                    mapData[x][y] = { type: 'grass', level: 1, isWalkable: true, isWater: false, brightnessBoost: 0 };
                                }
                            }
                        }
                    }
                }
            });
        }

        function generateDungeon() {
            let grid = [];
            for(let x=0; x<gridSize; x++) {
                grid[x] = [];
                for(let y=0; y<gridSize; y++) {
                    grid[x][y] = Math.random() < 0.45; 
                }
            }

            for(let step=0; step<4; step++) {
                let next = [];
                for(let x=0; x<gridSize; x++) {
                    next[x] = [];
                    for(let y=0; y<gridSize; y++) {
                        let walls = 0;
                        for(let dx=-1; dx<=1; dx++) {
                            for(let dy=-1; dy<=1; dy++) {
                                let nx = x+dx, ny = y+dy;
                                if (nx<0 || nx>=gridSize || ny<0 || ny>=gridSize) walls++;
                                else if(grid[nx][ny]) walls++;
                            }
                        }
                        next[x][y] = walls >= 5;
                    }
                }
                grid = next;
            }

            for(let x=0; x<gridSize; x++) {
                mapData[x] = [];
                for(let y=0; y<gridSize; y++) {
                    if (x === 0 || x === gridSize-1 || y === 0 || y === gridSize-1) grid[x][y] = true;
                    
                    const isWall = grid[x][y];
                    const wallHeight = 8 + Math.floor((simpleHash(x, y) - Math.floor(simpleHash(x,y))) * 3);

                    mapData[x][y] = {
                        type: isWall ? 'dungeon_wall' : 'dungeon_floor',
                        level: isWall ? wallHeight : 1,
                        isWalkable: !isWall,
                        isWater: false,
                        brightnessBoost: isWall ? (wallHeight - 8) * 5 : 0
                    };
                }
            }
        }

        // --- Core Application Logic ---
        function generateWorld() {
            loadingIndicator.style.display = 'block';
            
            setTimeout(() => {
                gridSize = parseInt(sizeSlider.value);
                const mapType = mapTypeSelect.value;
                mapData = [];

                if (mapType === 'overworld') generateOverworld();
                else if (mapType === 'town') generateTown();
                else if (mapType === 'dungeon') generateDungeon();

                applyColorsToMap();
                initPlayerPosition();
                
                loadingIndicator.style.display = 'none';
                resizeAndRender();
            }, 50);
        }

        function applyColorsToMap() {
            const theme = themes[themeSelect.value];
            for (let x = 0; x < gridSize; x++) {
                for (let y = 0; y < gridSize; y++) {
                    let cell = mapData[x][y];
                    const baseColor = theme[cell.type] || '#ff00ff';
                    
                    const hashVal = simpleHash(x, y);
                    const noiseVal = hashVal - Math.floor(hashVal);
                    const variation = (noiseVal - 0.5) * 10;
                    
                    cell.color = adjustBrightness(baseColor, variation + cell.brightnessBoost);
                }
            }
        }

        function initPlayerPosition() {
            for (let i = 0; i < 5000; i++) {
                const rx = Math.floor(Math.random() * gridSize);
                const ry = Math.floor(Math.random() * gridSize);
                if (mapData[rx] && mapData[rx][ry] && mapData[rx][ry].isWalkable) {
                    player.x = rx;
                    player.y = ry;
                    return;
                }
            }
            player.x = Math.floor(gridSize / 2);
            player.y = Math.floor(gridSize / 2);
        }

        // --- Rendering ---
        function resizeAndRender() {
            if (mapData.length === 0) return;
            const container = document.getElementById('canvas-container');
            canvas.width = container.clientWidth;
            canvas.height = container.clientHeight;
            
            const maxCellSizeW = canvas.width / 50; 
            const maxCellSizeH = canvas.height / 50;
            const baseCellSize = Math.min(maxCellSizeW, maxCellSizeH);

            renderCanvas(baseCellSize);
        }

        function renderCanvas(baseCellSize) {
            const theme = themes[themeSelect.value];
            
            ctx.fillStyle = mapTypeSelect.value === 'overworld' ? theme.ocean : '#000000';
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            const cellSize = baseCellSize * zoomLevel;
            const actualSize = Math.max(0.1, cellSize) + 1; 

            const mapW = gridSize * cellSize;
            const mapH = gridSize * cellSize;

            let camX = Math.floor((player.x + 0.5) * cellSize - canvas.width / 2);
            let camY = Math.floor((player.y + 0.5) * cellSize - canvas.height / 2);

            if (mapW > canvas.width) camX = Math.max(0, Math.min(camX, mapW - canvas.width));
            else camX = (mapW - canvas.width) / 2;
            
            if (mapH > canvas.height) camY = Math.max(0, Math.min(camY, mapH - canvas.height));
            else camY = (mapH - canvas.height) / 2;

            ctx.save();
            ctx.translate(-camX, -camY);

            const startX = Math.max(0, Math.floor(camX / cellSize) - 1);
            const endX = Math.min(gridSize - 1, Math.floor((camX + canvas.width) / cellSize) + 1);
            const startY = Math.max(0, Math.floor(camY / cellSize) - 1);
            const endY = Math.min(gridSize - 1, Math.floor((camY + canvas.height) / cellSize) + 1);

            const getLevelSafe = (tx, ty) => {
                if (tx >= 0 && tx < gridSize && ty >= 0 && ty < gridSize && mapData[tx] && mapData[tx][ty]) {
                    return mapData[tx][ty].level;
                }
                return -1; 
            };

            for (let x = startX; x <= endX; x++) {
                if (!mapData[x]) continue;
                for (let y = startY; y <= endY; y++) {
                    const cell = mapData[x][y];
                    if (!cell) continue;

                    const px = Math.floor(x * cellSize);
                    const py = Math.floor(y * cellSize);

                    ctx.fillStyle = cell.color;
                    ctx.fillRect(px, py, actualSize, actualSize);

                    if (!cell.isWater) {
                        const bevelSize = Math.max(1, actualSize * 0.15);
                        const myLevel = cell.level;
                        
                        const nL = getLevelSafe(x, y - 1);
                        const sL = getLevelSafe(x, y + 1);
                        const wL = getLevelSafe(x - 1, y);
                        const eL = getLevelSafe(x + 1, y);

                        if (nL !== -1) {
                            if (myLevel > nL) {
                                ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
                                ctx.fillRect(px, py, actualSize, bevelSize);
                            } else if (myLevel < nL) {
                                ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
                                ctx.fillRect(px, py, actualSize, bevelSize);
                            }
                        }

                        if (wL !== -1) {
                            if (myLevel > wL) {
                                ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
                                ctx.fillRect(px, py, bevelSize, actualSize);
                            } else if (myLevel < wL) {
                                ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
                                ctx.fillRect(px, py, bevelSize, actualSize);
                            }
                        }

                        if (sL !== -1 && myLevel > sL) {
                            ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
                            ctx.fillRect(px, py + actualSize - bevelSize, actualSize, bevelSize);
                        }

                        if (eL !== -1 && myLevel > eL) {
                            ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
                            ctx.fillRect(px + actualSize - bevelSize, py, bevelSize, actualSize);
                        }
                    }
                }
            }
            
            drawPlayer(cellSize);
            ctx.restore();
        }

        function drawPlayer(cellSize) {
            if (!player || player.x === undefined) return;
            
            const actualSize = Math.max(0.1, cellSize);
            const px = player.x * cellSize;
            const py = player.y * cellSize;
            const cx = px + actualSize / 2;
            const cy = py + actualSize / 2;
            const radius = Math.max(0.1, actualSize * 0.35);

            ctx.fillStyle = '#ffffff';
            ctx.shadowColor = '#ffffff';
            ctx.shadowBlur = 15;
            ctx.beginPath();
            ctx.arc(cx, cy, radius, 0, Math.PI * 2);
            ctx.fill();

            ctx.fillStyle = '#38bdf8'; 
            ctx.shadowBlur = 0; 
            ctx.beginPath();
            ctx.arc(cx, cy, radius * 0.5, 0, Math.PI * 2);
            ctx.fill();
        }

        // --- Event Listeners ---
        function updateUI() {
            sizeValue.textContent = sizeSlider.value;
            sizeValue2.textContent = sizeSlider.value;
            zoomValue.textContent = parseFloat(zoomSlider.value).toFixed(1);
        }

        sizeSlider.addEventListener('input', updateUI);
        zoomSlider.addEventListener('input', () => {
            updateUI();
            zoomLevel = parseFloat(zoomSlider.value);
            resizeAndRender();
        });
        
        mapTypeSelect.addEventListener('change', generateWorld);
        themeSelect.addEventListener('change', () => {
            if(mapData.length > 0) {
                applyColorsToMap();
                resizeAndRender();
            }
        });

        generateBtn.addEventListener('click', generateWorld);

        let resizeTimeout;
        window.addEventListener('resize', () => {
            clearTimeout(resizeTimeout);
            resizeTimeout = setTimeout(resizeAndRender, 100); 
        });

        window.addEventListener('keydown', (e) => {
            if (mapData.length === 0) return;

            let dx = 0, dy = 0;
            if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') dy = -1;
            if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') dy = 1;
            if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') dx = -1;
            if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') dx = 1;

            if (dx !== 0 || dy !== 0) {
                const newX = player.x + dx;
                const newY = player.y + dy;

                if (newX < 0 || newX >= gridSize || newY < 0 || newY >= gridSize) return;

                const cell = mapData[newX][newY];
                if (!cell || !cell.isWalkable) return;

                player.x = newX;
                player.y = newY;
                resizeAndRender();
            }
        });

        // Initialize
        updateUI();
        generateWorld(); 

    </script>
</body>
</html>

RPGマップ生成ツール

by tag

自動で世界をつくってくれます!

👁 33 回閲覧
📅 投稿:2026年6月20日 🔄 更新:2026年8月13日
応援スタンプを押してみよう!