<!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>