<!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>
自動で世界をつくってくれます!