myminecraft / index.html
izuemon's picture
Update index.html
b0d20bf verified
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Minecraft Browser</title>
<style>
:root {
--bg: #1a1a1a;
--panel: #2a2a2a;
--border: #3d3d3d;
--text: #ffffff;
--gold: #c8a44e;
--red: #c0392b;
--green: #4a8c3f;
--blue: #3a6ea5;
--hover: #3a3a3a;
--slot-bg: #8b8b8b;
--slot-border: #373737;
--slot-highlight: #ffffff;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #000;
font-family: 'Courier New', 'MS Gothic', monospace;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
image-rendering: pixelated;
-webkit-image-rendering: pixelated;
cursor: default;
width: 100vw;
height: 100vh;
}
#game-canvas {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
/* ホーム画面 */
#home-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.85);
background-image: linear-gradient(180deg, rgba(30, 20, 60, 0.9) 0%, rgba(10, 8, 20, 0.95) 100%);
transition: opacity 0.5s;
}
#home-screen.hidden {
opacity: 0;
pointer-events: none;
}
#home-screen .title {
font-size: clamp(32px, 6vw, 72px);
color: #fff;
letter-spacing: 6px;
text-shadow: 3px 3px 0 #000, 0 0 40px rgba(180, 150, 100, 0.5);
margin-bottom: 8px;
font-weight: bold;
}
#home-screen .subtitle {
font-size: 14px;
color: #aaa;
margin-bottom: 50px;
letter-spacing: 3px;
}
#home-screen .btn {
display: block;
width: 260px;
padding: 14px 20px;
margin: 8px;
font-size: 16px;
font-family: inherit;
cursor: pointer;
background: var(--panel);
color: var(--text);
border: 3px solid var(--border);
letter-spacing: 2px;
transition: all 0.15s;
text-align: center;
}
#home-screen .btn:hover {
background: var(--hover);
border-color: #666;
transform: scale(1.03);
}
#home-screen .btn.gold {
border-color: var(--gold);
color: var(--gold);
font-weight: bold;
}
#home-screen .btn.gold:hover {
background: #3d3020;
border-color: #e0c060;
}
.btn-row {
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: center;
}
/* HUD */
#hud {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
z-index: 50;
pointer-events: none;
display: none;
}
#hud.active {
display: block;
}
#crosshair {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 51;
pointer-events: none;
display: none;
}
#crosshair.active {
display: block;
}
#crosshair::before,
#crosshair::after {
content: '';
position: absolute;
background: rgba(255, 255, 255, 0.8);
}
#crosshair::before {
width: 20px;
height: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#crosshair::after {
width: 2px;
height: 20px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#hotbar {
position: fixed;
bottom: 8px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 2px;
z-index: 52;
pointer-events: auto;
}
.hotbar-slot {
width: 50px;
height: 50px;
background: var(--slot-bg);
border: 3px solid var(--slot-border);
position: relative;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #fff;
transition: border-color 0.1s;
}
.hotbar-slot.selected {
border-color: #fff;
box-shadow: 0 0 8px rgba(255, 255, 255, 0.4);
background: #a0a0a0;
}
.hotbar-slot .count {
position: absolute;
bottom: 2px;
right: 4px;
font-size: 11px;
color: #fff;
text-shadow: 1px 1px 0 #000;
}
.hotbar-slot img {
width: 32px;
height: 32px;
image-rendering: pixelated;
}
#status-bars {
position: fixed;
bottom: 70px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 20px;
z-index: 52;
pointer-events: none;
}
.status-bar {
display: flex;
gap: 2px;
}
.heart {
width: 14px;
height: 14px;
background: #e03030;
border-radius: 2px;
}
.heart.empty {
background: #3a1a1a;
}
.hunger-icon {
width: 14px;
height: 14px;
background: #c08040;
border-radius: 2px;
}
.hunger-icon.empty {
background: #2a1a0a;
}
/* インベントリ */
#inventory-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 200;
background: rgba(0, 0, 0, 0.7);
display: none;
align-items: center;
justify-content: center;
flex-direction: column;
}
#inventory-overlay.active {
display: flex;
}
#inventory-panel {
background: var(--panel);
border: 4px solid var(--border);
padding: 20px;
display: grid;
grid-template-columns: repeat(9, 48px);
gap: 3px;
max-height: 80vh;
overflow-y: auto;
}
.inv-slot {
width: 48px;
height: 48px;
background: var(--slot-bg);
border: 2px solid var(--slot-border);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #fff;
position: relative;
transition: background 0.1s;
}
.inv-slot:hover {
background: #a0a0a0;
border-color: #fff;
}
.inv-slot .count {
position: absolute;
bottom: 1px;
right: 3px;
font-size: 10px;
}
#inventory-close {
margin-top: 12px;
padding: 8px 30px;
font-family: inherit;
cursor: pointer;
background: var(--panel);
color: var(--text);
border: 2px solid var(--border);
font-size: 14px;
}
/* ポーズメニュー */
#pause-menu {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 180;
background: rgba(0, 0, 0, 0.6);
display: none;
align-items: center;
justify-content: center;
flex-direction: column;
}
#pause-menu.active {
display: flex;
}
#pause-menu .btn {
width: 220px;
padding: 12px;
margin: 6px;
font-family: inherit;
cursor: pointer;
background: var(--panel);
color: var(--text);
border: 2px solid var(--border);
font-size: 14px;
letter-spacing: 1px;
text-align: center;
}
#pause-menu .btn:hover {
background: var(--hover);
}
/* メッセージ表示 */
#game-message {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 55;
color: #fff;
font-size: 16px;
text-shadow: 2px 2px 0 #000;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
}
#game-message.show {
opacity: 1;
}
/* ブロック選択ハイライト */
#block-highlight {
position: fixed;
pointer-events: none;
z-index: 49;
border: 2px solid rgba(0, 0, 0, 0.5);
display: none;
}
@media (max-width: 600px) {
.hotbar-slot {
width: 36px;
height: 36px;
font-size: 8px;
}
.hotbar-slot img {
width: 24px;
height: 24px;
}
#inventory-panel {
grid-template-columns: repeat(9, 36px);
}
.inv-slot {
width: 36px;
height: 36px;
}
}
</style>
</head>
<body>
<!-- ホーム画面 -->
<div id="home-screen">
<div class="title">MINECRAFT</div>
<div class="subtitle">Browser Edition</div>
<button class="btn gold" id="btn-survival">▶ Survival Mode</button>
<button class="btn" id="btn-creative">✦ Creative Mode</button>
<button class="btn" id="btn-controls">🖱 Controls</button>
<button class="btn" id="btn-options">⚙ Options</button>
<div id="controls-info" style="display:none;color:#ccc;margin-top:20px;text-align:center;line-height:1.8;font-size:13px;">
<b>W A S D</b> - Move &nbsp;|&nbsp; <b>Space</b> - Jump &nbsp;|&nbsp; <b>Shift</b> - Sneak<br>
<b>Mouse</b> - Look around &nbsp;|&nbsp; <b>Left Click</b> - Break block<br>
<b>Right Click</b> - Place block / Use item &nbsp;|&nbsp; <b>E</b> - Inventory<br>
<b>1-9</b> - Hotbar select &nbsp;|&nbsp; <b>Esc</b> - Pause &nbsp;|&nbsp; <b>F</b> - Toggle flight (Creative)
</div>
</div>
<!-- クロスヘア -->
<div id="crosshair"></div>
<!-- HUD -->
<div id="hud">
<div id="status-bars">
<div class="status-bar" id="hearts-container"></div>
<div class="status-bar" id="hunger-container"></div>
</div>
<div id="hotbar"></div>
</div>
<!-- ブロック選択 -->
<div id="block-highlight"></div>
<!-- ゲームメッセージ -->
<div id="game-message"></div>
<!-- ポーズメニュー -->
<div id="pause-menu">
<div style="font-size:28px;color:#fff;margin-bottom:20px;letter-spacing:3px;">Game Paused</div>
<button class="btn" id="btn-resume">▶ Resume</button>
<button class="btn" id="btn-save">💾 Save (WIP)</button>
<button class="btn" id="btn-quit">🚪 Quit to Menu</button>
</div>
<!-- インベントリ -->
<div id="inventory-overlay">
<div id="inventory-panel"></div>
<button id="inventory-close">Close (E)</button>
</div>
<!-- Three.js CDN -->
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<!-- JSZip CDN -->
<script src="https://unpkg.com/jszip@3.10.1/dist/jszip.min.js">
</script>
<script type="module">
import * as THREE from 'three';
// ============================================================
// 設定・定数
// ============================================================
const CHUNK_SIZE = 16;
const CHUNK_HEIGHT = 80;
const RENDER_DISTANCE = 6;
const GRAVITY = 20;
const JUMP_VELOCITY = 9;
const PLAYER_HEIGHT = 1.7;
const PLAYER_SPEED = 6;
const CREATIVE_SPEED = 12;
const REACH_DISTANCE = 6;
const BLOCK_SIZE = 1;
// ブロックID定義
const BLOCK_AIR = 0;
const BLOCK_STONE = 1;
const BLOCK_GRASS = 2;
const BLOCK_DIRT = 3;
const BLOCK_COBBLESTONE = 4;
const BLOCK_WOOD = 5;
const BLOCK_LEAVES = 6;
const BLOCK_SAND = 7;
const BLOCK_WATER = 8;
const BLOCK_PLANKS = 9;
const BLOCK_GLASS = 10;
const BLOCK_TORCH = 11;
const BLOCK_CHEST = 12;
const BLOCK_CRAFTING_TABLE = 13;
const BLOCK_BED = 14;
const BLOCK_BRICK = 15;
const BLOCK_IRON_ORE = 16;
const BLOCK_GOLD_ORE = 17;
const BLOCK_DIAMOND_ORE = 18;
const BLOCK_DOOR = 19;
// ブロック情報
const BLOCK_INFO = {
[BLOCK_AIR]: { name: 'air', solid: false, transparent: true, hardness: 0, drop: null,
topColor: '#000000', sideColor: '#000000', bottomColor: '#000000' },
[BLOCK_STONE]: { name: 'stone', solid: true, transparent: false, hardness: 3, drop: BLOCK_COBBLESTONE,
topColor: '#7a7a7a', sideColor: '#7a7a7a', bottomColor: '#7a7a7a', texTop: 'stone', texSide: 'stone',
texBottom: 'stone' },
[BLOCK_GRASS]: { name: 'grass_block', solid: true, transparent: false, hardness: 0.8, drop: BLOCK_DIRT,
topColor: '#6db04a', sideColor: '#8b6b4a', bottomColor: '#6b4423', texTop: 'grass_block_top',
texSide: 'grass_block_side', texBottom: 'dirt' },
[BLOCK_DIRT]: { name: 'dirt', solid: true, transparent: false, hardness: 0.5, drop: BLOCK_DIRT,
topColor: '#6b4423', sideColor: '#6b4423', bottomColor: '#6b4423', texTop: 'dirt', texSide: 'dirt',
texBottom: 'dirt' },
[BLOCK_COBBLESTONE]: { name: 'cobblestone', solid: true, transparent: false, hardness: 2, drop: BLOCK_COBBLESTONE,
topColor: '#6e6e6e', sideColor: '#6e6e6e', bottomColor: '#6e6e6e', texTop: 'cobblestone',
texSide: 'cobblestone', texBottom: 'cobblestone' },
[BLOCK_WOOD]: { name: 'oak_log', solid: true, transparent: false, hardness: 1, drop: BLOCK_WOOD,
topColor: '#b89a6e', sideColor: '#8b6b3e', bottomColor: '#b89a6e', texTop: 'oak_log_top',
texSide: 'oak_log', texBottom: 'oak_log_top' },
[BLOCK_LEAVES]: { name: 'oak_leaves', solid: true, transparent: true, hardness: 0.2, drop: BLOCK_LEAVES,
topColor: '#3a8030', sideColor: '#3a8030', bottomColor: '#3a8030', texTop: 'oak_leaves',
texSide: 'oak_leaves', texBottom: 'oak_leaves' },
[BLOCK_SAND]: { name: 'sand', solid: true, transparent: false, hardness: 0.5, drop: BLOCK_SAND,
topColor: '#dbc07c', sideColor: '#dbc07c', bottomColor: '#dbc07c', texTop: 'sand', texSide: 'sand',
texBottom: 'sand' },
[BLOCK_WATER]: { name: 'water', solid: false, transparent: true, hardness: 100, drop: null, topColor: '#3355cc',
sideColor: '#3355cc', bottomColor: '#3355cc', texTop: 'water_still', texSide: 'water_still',
texBottom: 'water_still' },
[BLOCK_PLANKS]: { name: 'oak_planks', solid: true, transparent: false, hardness: 0.8, drop: BLOCK_PLANKS,
topColor: '#b8945c', sideColor: '#b8945c', bottomColor: '#b8945c', texTop: 'oak_planks',
texSide: 'oak_planks', texBottom: 'oak_planks' },
[BLOCK_GLASS]: { name: 'glass', solid: true, transparent: true, hardness: 0.3, drop: null, topColor: '#c8e8ff',
sideColor: '#c8e8ff', bottomColor: '#c8e8ff', texTop: 'glass', texSide: 'glass', texBottom: 'glass' },
[BLOCK_TORCH]: { name: 'torch', solid: false, transparent: true, hardness: 0, drop: BLOCK_TORCH,
topColor: '#ffcc00', sideColor: '#ffaa00', bottomColor: '#885500' },
[BLOCK_CHEST]: { name: 'chest', solid: true, transparent: false, hardness: 1, drop: BLOCK_CHEST,
topColor: '#c49a40', sideColor: '#b08030', bottomColor: '#8a6020', texTop: 'chest_top',
texSide: 'chest_side', texBottom: 'chest_bottom' },
[BLOCK_CRAFTING_TABLE]: { name: 'crafting_table', solid: true, transparent: false, hardness: 1,
drop: BLOCK_CRAFTING_TABLE, topColor: '#9a7a4a', sideColor: '#8a6a3a', bottomColor: '#6a4a2a',
texTop: 'crafting_table_top', texSide: 'crafting_table_side', texBottom: 'oak_planks' },
[BLOCK_BED]: { name: 'bed', solid: true, transparent: true, hardness: 0.5, drop: BLOCK_BED, topColor: '#cc3333',
sideColor: '#aa2222', bottomColor: '#882222' },
[BLOCK_BRICK]: { name: 'bricks', solid: true, transparent: false, hardness: 2, drop: BLOCK_BRICK,
topColor: '#b05040', sideColor: '#b05040', bottomColor: '#b05040', texTop: 'bricks', texSide: 'bricks',
texBottom: 'bricks' },
[BLOCK_IRON_ORE]: { name: 'iron_ore', solid: true, transparent: false, hardness: 3, drop: BLOCK_IRON_ORE,
topColor: '#c8b090', sideColor: '#c8b090', bottomColor: '#c8b090', texTop: 'iron_ore', texSide: 'iron_ore',
texBottom: 'iron_ore' },
[BLOCK_GOLD_ORE]: { name: 'gold_ore', solid: true, transparent: false, hardness: 3, drop: BLOCK_GOLD_ORE,
topColor: '#d4b830', sideColor: '#d4b830', bottomColor: '#d4b830', texTop: 'gold_ore', texSide: 'gold_ore',
texBottom: 'gold_ore' },
[BLOCK_DIAMOND_ORE]: { name: 'diamond_ore', solid: true, transparent: false, hardness: 3,
drop: BLOCK_DIAMOND_ORE, topColor: '#70d0e0', sideColor: '#70d0e0', bottomColor: '#70d0e0',
texTop: 'diamond_ore', texSide: 'diamond_ore', texBottom: 'diamond_ore' },
[BLOCK_DOOR]: { name: 'door', solid: true, transparent: true, hardness: 1, drop: BLOCK_DOOR, topColor: '#6b4423',
sideColor: '#6b4423', bottomColor: '#6b4423' },
};
// アイテム定義
const ITEM_INFO = {
'wooden_sword': { name: 'Wooden Sword', stackable: false, durability: 60, damage: 4, type: 'weapon' },
'stone_pickaxe': { name: 'Stone Pickaxe', stackable: false, durability: 132, damage: 2, type: 'tool',
speed: 1.5 },
'torch_item': { name: 'Torch', stackable: true, maxStack: 64, type: 'placeable', placesBlock: BLOCK_TORCH },
'apple': { name: 'Apple', stackable: true, maxStack: 64, type: 'food', hungerRestore: 4, healAmount: 2 },
'bread': { name: 'Bread', stackable: true, maxStack: 64, type: 'food', hungerRestore: 5, healAmount: 3 },
'compass': { name: 'Compass', stackable: false, type: 'tool', special: 'compass' },
'clock': { name: 'Clock', stackable: false, type: 'tool', special: 'clock' },
'bucket': { name: 'Bucket', stackable: true, maxStack: 16, type: 'tool', special: 'bucket' },
'chest_item': { name: 'Chest', stackable: true, maxStack: 64, type: 'placeable', placesBlock: BLOCK_CHEST },
'crafting_table_item': { name: 'Crafting Table', stackable: true, maxStack: 64, type: 'placeable',
placesBlock: BLOCK_CRAFTING_TABLE },
'bed_item': { name: 'Bed', stackable: true, maxStack: 1, type: 'placeable', placesBlock: BLOCK_BED },
'door_item': { name: 'Door', stackable: true, maxStack: 64, type: 'placeable', placesBlock: BLOCK_DOOR },
'leather_helmet': { name: 'Leather Helmet', stackable: false, durability: 55, type: 'armor', armorSlot: 'head',
protection: 1 },
'leather_chestplate': { name: 'Leather Chestplate', stackable: false, durability: 80, type: 'armor',
armorSlot: 'chest', protection: 2 },
};
// ============================================================
// シンプレックスノイズ(地形生成用)
// ============================================================
class SimplexNoise {
constructor(seed = 0) {
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.perm = new Uint8Array(512);
this._seed(seed);
}
_seed(seed) {
const p = new Uint8Array(256);
for (let i = 0; i < 256; i++) p[i] = i;
let s = seed;
for (let i = 255; i > 0; i--) {
s = (s * 16807 + 0) % 2147483647;
const j = s % (i + 1);
[p[i], p[j]] = [p[j], p[i]];
}
for (let i = 0; i < 512; i++) this.perm[i] = p[i & 255];
}
dot(g, x, y, z) { return g[0] * x + g[1] * y + g[2] * z; }
noise3D(x, y, z) {
const F3 = 1 / 3;
const G3 = 1 / 6;
const floor = Math.floor;
const s = (x + y + z) * F3;
const i = floor(x + s),
j = floor(y + s),
k = floor(z + s);
const t = (i + j + k) * G3;
const X0 = i - t,
Y0 = j - t,
Z0 = k - t;
const x0 = x - X0,
y0 = y - Y0,
z0 = z - Z0;
let i1, j1, k1, i2, j2, k2;
if (x0 >= y0) {
if (y0 >= z0) { i1 = 1;
j1 = 0;
k1 = 0;
i2 = 1;
j2 = 1;
k2 = 0; } else if (x0 >= z0) { i1 = 1;
j1 = 0;
k1 = 0;
i2 = 1;
j2 = 0;
k2 = 1; } else { i1 = 0;
j1 = 0;
k1 = 1;
i2 = 1;
j2 = 0;
k2 = 1; }
} else {
if (y0 < z0) { i1 = 0;
j1 = 0;
k1 = 1;
i2 = 0;
j2 = 1;
k2 = 1; } else if (x0 < z0) { i1 = 0;
j1 = 1;
k1 = 0;
i2 = 0;
j2 = 1;
k2 = 1; } else { i1 = 0;
j1 = 1;
k1 = 0;
i2 = 1;
j2 = 1;
k2 = 0; }
}
const x1 = x0 - i1 + G3,
y1 = y0 - j1 + G3,
z1 = z0 - k1 + G3;
const x2 = x0 - i2 + 2 * G3,
y2 = y0 - j2 + 2 * G3,
z2 = z0 - k2 + 2 * G3;
const x3 = x0 - 1 + 3 * G3,
y3 = y0 - 1 + 3 * G3,
z3 = z0 - 1 + 3 * G3;
const ii = i & 255,
jj = j & 255,
kk = k & 255;
const p = this.perm;
const gi0 = p[ii + p[jj + p[kk]]] % 12;
const gi1 = p[ii + i1 + p[jj + j1 + p[kk + k1]]] % 12;
const gi2 = p[ii + i2 + p[jj + j2 + p[kk + k2]]] % 12;
const gi3 = p[ii + 1 + p[jj + 1 + p[kk + 1]]] % 12;
let n0 = 0,
n1 = 0,
n2 = 0,
n3 = 0;
const t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0;
if (t0 > 0) { const d = t0 * t0;
n0 = d * d * this.dot(this.grad3[gi0], x0, y0, z0); }
const t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1;
if (t1 > 0) { const d = t1 * t1;
n1 = d * d * this.dot(this.grad3[gi1], x1, y1, z1); }
const t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2;
if (t2 > 0) { const d = t2 * t2;
n2 = d * d * this.dot(this.grad3[gi2], x2, y2, z2); }
const t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3;
if (t3 > 0) { const d = t3 * t3;
n3 = d * d * this.dot(this.grad3[gi3], x3, y3, z3); }
return 32 * (n0 + n1 + n2 + n3);
}
noise2D(x, y) { return this.noise3D(x, y, 0); }
fbm(x, y, octaves = 4, lacunarity = 2, gain = 0.5) {
let value = 0,
amplitude = 1,
frequency = 1,
maxVal = 0;
for (let i = 0; i < octaves; i++) {
value += amplitude * this.noise2D(x * frequency, y * frequency);
maxVal += amplitude;
amplitude *= gain;
frequency *= lacunarity;
}
return value / maxVal;
}
fbm3D(x, y, z, octaves = 3, lacunarity = 2, gain = 0.5) {
let value = 0,
amplitude = 1,
frequency = 1,
maxVal = 0;
for (let i = 0; i < octaves; i++) {
value += amplitude * this.noise3D(x * frequency, y * frequency, z * frequency);
maxVal += amplitude;
amplitude *= gain;
frequency *= lacunarity;
}
return value / maxVal;
}
}
// ============================================================
// テクスチャ管理
// ============================================================
class TextureManager {
constructor() {
this.textures = {};
this.atlasCanvas = null;
this.atlasTexture = null;
this.atlasSize = 256;
this.tileSize = 16;
this.tilesPerRow = this.atlasSize / this.tileSize;
this.tileMap = {};
this.nextTileIndex = 0;
this.loadedFromZip = false;
}
async init(zip = null) {
this.atlasCanvas = document.createElement('canvas');
this.atlasCanvas.width = this.atlasSize;
this.atlasCanvas.height = this.atlasSize;
const ctx = this.atlasCanvas.getContext('2d');
ctx.fillStyle = '#ff00ff';
ctx.fillRect(0, 0, this.atlasSize, this.atlasSize);
if (zip) {
try {
await this._loadFromZip(zip);
this.loadedFromZip = true;
console.log('Textures loaded from ZIP');
} catch (e) {
console.warn('Failed to load textures from ZIP, using procedural:', e.message);
}
}
if (!this.loadedFromZip) {
this._generateProceduralTextures();
}
this.atlasTexture = new THREE.CanvasTexture(this.atlasCanvas);
this.atlasTexture.magFilter = THREE.NearestFilter;
this.atlasTexture.minFilter = THREE.NearestFilter;
this.atlasTexture.wrapS = THREE.ClampToEdgeWrapping;
this.atlasTexture.wrapT = THREE.ClampToEdgeWrapping;
this.atlasTexture.colorSpace = THREE.SRGBColorSpace;
return this;
}
async _loadFromZip(zip) {
const texturePaths = [
'assets/minecraft/textures/block/stone.png',
'assets/minecraft/textures/block/dirt.png',
'assets/minecraft/textures/block/grass_block_top.png',
'assets/minecraft/textures/block/grass_block_side.png',
'assets/minecraft/textures/block/cobblestone.png',
'assets/minecraft/textures/block/oak_log.png',
'assets/minecraft/textures/block/oak_log_top.png',
'assets/minecraft/textures/block/oak_leaves.png',
'assets/minecraft/textures/block/sand.png',
'assets/minecraft/textures/block/water_still.png',
'assets/minecraft/textures/block/oak_planks.png',
'assets/minecraft/textures/block/glass.png',
'assets/minecraft/textures/block/chest_top.png',
'assets/minecraft/textures/block/chest_side.png',
'assets/minecraft/textures/block/chest_bottom.png',
'assets/minecraft/textures/block/crafting_table_top.png',
'assets/minecraft/textures/block/crafting_table_side.png',
'assets/minecraft/textures/block/bricks.png',
'assets/minecraft/textures/block/iron_ore.png',
'assets/minecraft/textures/block/gold_ore.png',
'assets/minecraft/textures/block/diamond_ore.png',
];
const loadedImages = {};
for (const path of texturePaths) {
const file = zip.file(path);
if (file) {
try {
const blob = await file.async('blob');
const dataUrl = await new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
const img = await new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = reject;
image.src = dataUrl;
});
const key = path.replace('assets/minecraft/textures/block/', '').replace('.png', '');
loadedImages[key] = img;
} catch (e) {
console.warn(`Failed to load texture: ${path}`, e);
}
}
}
if (Object.keys(loadedImages).length < 3) throw new Error('Not enough textures loaded');
const ctx = this.atlasCanvas.getContext('2d');
ctx.fillStyle = '#ff00ff';
ctx.fillRect(0, 0, this.atlasSize, this.atlasSize);
for (const [key, img] of Object.entries(loadedImages)) {
const idx = this.nextTileIndex++;
const col = idx % this.tilesPerRow;
const row = Math.floor(idx / this.tilesPerRow);
ctx.drawImage(img, col * this.tileSize, row * this.tileSize, this.tileSize, this.tileSize);
this.tileMap[key] = { col, row, index: idx };
}
this._loadedImages = loadedImages;
}
_generateProceduralTextures() {
const ctx = this.atlasCanvas.getContext('2d');
ctx.fillStyle = '#ff00ff';
ctx.fillRect(0, 0, this.atlasSize, this.atlasSize);
const blockTypes = [
{ key: 'stone', color: '#7a7a7a' },
{ key: 'dirt', color: '#6b4423' },
{ key: 'grass_block_top', color: '#6db04a' },
{ key: 'grass_block_side', color: '#8b6b4a' },
{ key: 'cobblestone', color: '#6e6e6e' },
{ key: 'oak_log', color: '#8b6b3e' },
{ key: 'oak_log_top', color: '#b89a6e' },
{ key: 'oak_leaves', color: '#3a8030' },
{ key: 'sand', color: '#dbc07c' },
{ key: 'water_still', color: '#3355cc' },
{ key: 'oak_planks', color: '#b8945c' },
{ key: 'glass', color: '#c8e8ff' },
{ key: 'chest_top', color: '#c49a40' },
{ key: 'chest_side', color: '#b08030' },
{ key: 'chest_bottom', color: '#8a6020' },
{ key: 'crafting_table_top', color: '#9a7a4a' },
{ key: 'crafting_table_side', color: '#8a6a3a' },
{ key: 'bricks', color: '#b05040' },
{ key: 'iron_ore', color: '#c8b090' },
{ key: 'gold_ore', color: '#d4b830' },
{ key: 'diamond_ore', color: '#70d0e0' },
];
for (const bt of blockTypes) {
const idx = this.nextTileIndex++;
const col = idx % this.tilesPerRow;
const row = Math.floor(idx / this.tilesPerRow);
ctx.fillStyle = bt.color;
ctx.fillRect(col * this.tileSize, row * this.tileSize, this.tileSize, this.tileSize);
ctx.fillStyle = 'rgba(255,255,255,0.08)';
for (let i = 0; i < 3; i++) {
ctx.fillRect(col * this.tileSize + Math.floor(Math.random() * 14), row * this.tileSize + Math
.floor(Math.random() * 14), 2, 2);
}
ctx.strokeStyle = 'rgba(0,0,0,0.25)';
ctx.strokeRect(col * this.tileSize, row * this.tileSize, this.tileSize, this.tileSize);
this.tileMap[bt.key] = { col, row, index: idx };
}
}
getUV(key) {
const t = this.tileMap[key] || this.tileMap['stone'] || { col: 0, row: 0 };
const u0 = t.col / this.tilesPerRow;
const v0 = 1 - (t.row + 1) / this.tilesPerRow;
const u1 = (t.col + 1) / this.tilesPerRow;
const v1 = 1 - t.row / this.tilesPerRow;
return { u0, v0, u1, v1 };
}
getTexture() { return this.atlasTexture; }
}
// ============================================================
// サウンド管理
// ============================================================
class SoundManager {
constructor() {
this.audioCtx = null;
this.sounds = {};
this.enabled = true;
this.volume = 0.5;
try { this.audioCtx = new(window.AudioContext || window.webkitAudioContext)(); } catch (e) { this.enabled =
false; }
}
async init(zip = null) {
if (!this.enabled || !zip) return;
try {
const soundFiles = [
'assets/minecraft/sounds/dig/stone1.ogg',
'assets/minecraft/sounds/dig/grass1.ogg',
'assets/minecraft/sounds/dig/wood1.ogg',
'assets/minecraft/sounds/step/stone1.ogg',
'assets/minecraft/sounds/step/grass1.ogg',
'assets/minecraft/sounds/random/click.ogg',
'assets/minecraft/sounds/random/eat.ogg',
'assets/minecraft/sounds/mob/villager/yes1.ogg',
];
for (const path of soundFiles) {
const file = zip.file(path);
if (file) {
const blob = await file.async('blob');
const arrayBuffer = await blob.arrayBuffer();
const audioBuffer = await this.audioCtx.decodeAudioData(arrayBuffer);
const key = path.replace('assets/minecraft/sounds/', '').replace('.ogg', '');
this.sounds[key] = audioBuffer;
}
}
console.log(`Loaded ${Object.keys(this.sounds).length} sounds from ZIP`);
} catch (e) {
console.warn('Failed to load sounds from ZIP:', e.message);
}
}
play(key, volume = 1) {
if (!this.enabled || !this.audioCtx) return;
if (this.audioCtx.state === 'suspended') this.audioCtx.resume();
const buffer = this.sounds[key];
if (buffer) {
const source = this.audioCtx.createBufferSource();
source.buffer = buffer;
const gain = this.audioCtx.createGain();
gain.gain.value = volume * this.volume;
source.connect(gain);
gain.connect(this.audioCtx.destination);
source.start(0);
} else {
this._playProcedural(key, volume);
}
}
_playProcedural(key, volume) {
if (!this.audioCtx) return;
const osc = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain();
osc.connect(gain);
gain.connect(this.audioCtx.destination);
gain.gain.value = volume * this.volume * 0.3;
if (key.includes('dig') || key.includes('step')) {
osc.type = 'square';
osc.frequency.value = key.includes('stone') ? 200 : key.includes('wood') ? 300 : 250;
} else if (key.includes('click')) {
osc.type = 'sine';
osc.frequency.value = 600;
} else {
osc.type = 'triangle';
osc.frequency.value = 400;
}
osc.start(0);
gain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 0.15);
osc.stop(this.audioCtx.currentTime + 0.15);
}
}
// ============================================================
// パーティクル管理
// ============================================================
class ParticleSystem {
constructor(scene) {
this.scene = scene;
this.particles = [];
}
emit(position, color = '#ffffff', count = 8) {
for (let i = 0; i < count; i++) {
const geo = new THREE.SphereGeometry(0.03, 4, 4);
const mat = new THREE.MeshBasicMaterial({ color });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(position);
mesh.position.x += (Math.random() - 0.5) * 0.4;
mesh.position.z += (Math.random() - 0.5) * 0.4;
mesh.position.y += Math.random() * 0.3;
mesh.userData = {
velocity: new THREE.Vector3(
(Math.random() - 0.5) * 3,
Math.random() * 3 + 2,
(Math.random() - 0.5) * 3
),
life: 0.6 + Math.random() * 0.4,
age: 0,
};
this.scene.add(mesh);
this.particles.push(mesh);
}
}
update(dt) {
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i];
p.userData.age += dt;
if (p.userData.age >= p.userData.life) {
this.scene.remove(p);
p.geometry.dispose();
p.material.dispose();
this.particles.splice(i, 1);
} else {
const vel = p.userData.velocity;
p.position.x += vel.x * dt;
p.position.y += vel.y * dt;
p.position.z += vel.z * dt;
vel.y -= 9.8 * dt;
const alpha = 1 - p.userData.age / p.userData.life;
p.material.opacity = alpha;
p.material.transparent = true;
p.scale.setScalar(alpha);
}
}
}
clear() {
for (const p of this.particles) {
this.scene.remove(p);
p.geometry.dispose();
p.material.dispose();
}
this.particles.length = 0;
}
}
// ============================================================
// ワールド(チャンク管理)
// ============================================================
class World {
constructor(scene, textureManager, seed = 42) {
this.scene = scene;
this.textureManager = textureManager;
this.noise = new SimplexNoise(seed);
this.noiseCave = new SimplexNoise(seed + 1000);
this.noiseBiome = new SimplexNoise(seed + 2000);
this.chunks = {};
this.chunkMeshes = {};
this.blockData = {}; // 追加データ(チェストの開閉状態など)
this.seaLevel = 32;
}
getChunkKey(cx, cz) { return `${cx},${cz}`; }
getBlock(wx, wy, wz) {
const cx = Math.floor(wx / CHUNK_SIZE);
const cz = Math.floor(wz / CHUNK_SIZE);
const key = this.getChunkKey(cx, cz);
const chunk = this.chunks[key];
if (!chunk) return BLOCK_AIR;
const lx = ((wx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const lz = ((wz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
if (wy < 0 || wy >= CHUNK_HEIGHT) return wy < 0 ? BLOCK_STONE : BLOCK_AIR;
return chunk[lx + lz * CHUNK_SIZE + wy * CHUNK_SIZE * CHUNK_SIZE] || BLOCK_AIR;
}
setBlock(wx, wy, wz, blockId) {
const cx = Math.floor(wx / CHUNK_SIZE);
const cz = Math.floor(wz / CHUNK_SIZE);
const key = this.getChunkKey(cx, cz);
if (!this.chunks[key]) this.generateChunk(cx, cz);
const chunk = this.chunks[key];
const lx = ((wx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const lz = ((wz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
if (wy < 0 || wy >= CHUNK_HEIGHT) return;
const idx = lx + lz * CHUNK_SIZE + wy * CHUNK_SIZE * CHUNK_SIZE;
chunk[idx] = blockId;
this.rebuildChunkMesh(cx, cz);
if (lx === 0) this.rebuildChunkMesh(cx - 1, cz);
if (lx === CHUNK_SIZE - 1) this.rebuildChunkMesh(cx + 1, cz);
if (lz === 0) this.rebuildChunkMesh(cx, cz - 1);
if (lz === CHUNK_SIZE - 1) this.rebuildChunkMesh(cx, cz + 1);
if (wy === 0 && cx === 0 && cz === 0) {} // 隣接チャンクの上下も必要に応じて
}
generateChunk(cx, cz) {
const key = this.getChunkKey(cx, cz);
if (this.chunks[key]) return;
const size = CHUNK_SIZE * CHUNK_SIZE * CHUNK_HEIGHT;
const chunk = new Uint8Array(size);
const worldX = cx * CHUNK_SIZE;
const worldZ = cz * CHUNK_SIZE;
for (let lx = 0; lx < CHUNK_SIZE; lx++) {
for (let lz = 0; lz < CHUNK_SIZE; lz++) {
const wx = worldX + lx;
const wz = worldZ + lz;
const biomeVal = this.noiseBiome.fbm(wx * 0.003, wz * 0.003, 3);
const baseHeight = this.noise.fbm(wx * 0.01, wz * 0.01, 4) * 25 + 28;
const detailHeight = this.noise.fbm(wx * 0.05, wz * 0.05, 3) * 8;
const mountainExtra = Math.max(0, this.noise.fbm(wx * 0.015, wz * 0.015, 3)) * 20;
const isMountain = biomeVal > 0.4;
const isDesert = biomeVal < -0.35;
const isOcean = biomeVal < -0.55;
let terrainHeight = Math.floor(baseHeight + detailHeight + (isMountain ? mountainExtra : 0));
if (isOcean) terrainHeight = Math.floor(this.seaLevel - 4 + Math.abs(this.noise.fbm(wx * 0.02, wz * 0.02,
2)) * 3);
if (isDesert) terrainHeight = Math.floor(baseHeight * 0.8 + detailHeight * 0.5 + 2);
terrainHeight = Math.max(1, Math.min(CHUNK_HEIGHT - 5, terrainHeight));
for (let wy = 0; wy < CHUNK_HEIGHT; wy++) {
const idx = lx + lz * CHUNK_SIZE + wy * CHUNK_SIZE * CHUNK_SIZE;
if (wy > terrainHeight) {
if (wy <= this.seaLevel && !isDesert && !isMountain) {
chunk[idx] = BLOCK_WATER;
} else {
chunk[idx] = BLOCK_AIR;
}
} else if (wy === terrainHeight) {
if (wy >= this.seaLevel - 1 && !isDesert && !isMountain && biomeVal > -0.3) {
chunk[idx] = BLOCK_GRASS;
} else if (isDesert) {
chunk[idx] = BLOCK_SAND;
} else if (wy < this.seaLevel && biomeVal < -0.3) {
chunk[idx] = BLOCK_SAND;
} else {
chunk[idx] = BLOCK_GRASS;
}
} else if (wy >= terrainHeight - 4) {
chunk[idx] = isDesert ? BLOCK_SAND : BLOCK_DIRT;
} else {
const caveNoise = this.noiseCave.fbm3D(wx * 0.08, wy * 0.08, wz * 0.08, 3);
const caveThreshold = wy < 20 ? 0.15 : 0.25;
if (caveNoise > caveThreshold && wy > 2) {
chunk[idx] = BLOCK_AIR;
} else {
if (wy < 10 && Math.random() < 0.003) chunk[idx] = BLOCK_DIAMOND_ORE;
else if (wy < 18 && Math.random() < 0.006) chunk[idx] = BLOCK_GOLD_ORE;
else if (wy < 30 && Math.random() < 0.01) chunk[idx] = BLOCK_IRON_ORE;
else chunk[idx] = BLOCK_STONE;
}
}
}
if (isDesert && terrainHeight > 2 && Math.random() < 0.04 && terrainHeight < CHUNK_HEIGHT - 4) {
const idxCactus = lx + lz * CHUNK_SIZE + (terrainHeight + 1) * CHUNK_SIZE * CHUNK_SIZE;
if (chunk[idxCactus] === BLOCK_AIR) chunk[idxCactus] = BLOCK_WOOD;
}
if (!isDesert && !isOcean && biomeVal > -0.1 && terrainHeight > this.seaLevel && Math.random() <
0.06) {
for (let t = 1; t <= 4; t++) {
const treeIdx = lx + lz * CHUNK_SIZE + (terrainHeight + t) * CHUNK_SIZE * CHUNK_SIZE;
if (terrainHeight + t < CHUNK_HEIGHT && chunk[treeIdx] === BLOCK_AIR) {
chunk[treeIdx] = t <= 3 ? BLOCK_WOOD : BLOCK_LEAVES;
}
}
for (let dx = -1; dx <= 1; dx++)
for (let dz = -1; dz <= 1; dz++) {
const leafIdx = (lx + dx) + (lz + dz) * CHUNK_SIZE + (terrainHeight + 3) * CHUNK_SIZE *
CHUNK_SIZE;
if (lx + dx >= 0 && lx + dx < CHUNK_SIZE && lz + dz >= 0 && lz + dz < CHUNK_SIZE && terrainHeight +
3 < CHUNK_HEIGHT) {
if (chunk[leafIdx] === BLOCK_AIR && !(dx === 0 && dz === 0)) chunk[leafIdx] =
BLOCK_LEAVES;
}
}
}
}
}
this.chunks[key] = chunk;
this.rebuildChunkMesh(cx, cz);
}
rebuildChunkMesh(cx, cz) {
const key = this.getChunkKey(cx, cz);
const chunk = this.chunks[key];
if (!chunk) return;
if (this.chunkMeshes[key]) {
this.scene.remove(this.chunkMeshes[key]);
if (this.chunkMeshes[key].geometry) this.chunkMeshes[key].geometry.dispose();
if (Array.isArray(this.chunkMeshes[key].material)) {
this.chunkMeshes[key].material.forEach(m => m.dispose());
} else if (this.chunkMeshes[key].material) {
this.chunkMeshes[key].material.dispose();
}
delete this.chunkMeshes[key];
}
const worldX = cx * CHUNK_SIZE;
const worldZ = cz * CHUNK_SIZE;
const positions = [];
const normals = [];
const uvs = [];
const indices = [];
const transparentPositions = [];
const transparentNormals = [];
const transparentUvs = [];
const transparentIndices = [];
let vi = 0,
tvi = 0;
const faceData = [
{ dir: [0, 1, 0], nx: 0, ny: 1, nz: 0, corners: [
[0, 1, 1],
[1, 1, 1],
[1, 1, 0],
[0, 1, 0]
], uvKey: 'texTop', faceName: 'up' },
{ dir: [0, -1, 0], nx: 0, ny: -1, nz: 0, corners: [
[0, 0, 0],
[1, 0, 0],
[1, 0, 1],
[0, 0, 1]
], uvKey: 'texBottom', faceName: 'down' },
{ dir: [0, 0, 1], nx: 0, ny: 0, nz: 1, corners: [
[0, 1, 1],
[0, 0, 1],
[1, 0, 1],
[1, 1, 1]
], uvKey: 'texSide', faceName: 'south' },
{ dir: [0, 0, -1], nx: 0, ny: 0, nz: -1, corners: [
[1, 1, 0],
[1, 0, 0],
[0, 0, 0],
[0, 1, 0]
], uvKey: 'texSide', faceName: 'north' },
{ dir: [1, 0, 0], nx: 1, ny: 0, nz: 0, corners: [
[1, 1, 1],
[1, 0, 1],
[1, 0, 0],
[1, 1, 0]
], uvKey: 'texSide', faceName: 'east' },
{ dir: [-1, 0, 0], nx: -1, ny: 0, nz: 0, corners: [
[0, 1, 0],
[0, 0, 0],
[0, 0, 1],
[0, 1, 1]
], uvKey: 'texSide', faceName: 'west' },
];
for (let lx = 0; lx < CHUNK_SIZE; lx++) {
for (let lz = 0; lz < CHUNK_SIZE; lz++) {
for (let ly = 0; ly < CHUNK_HEIGHT; ly++) {
const idx = lx + lz * CHUNK_SIZE + ly * CHUNK_SIZE * CHUNK_SIZE;
const blockId = chunk[idx];
if (blockId === BLOCK_AIR) continue;
const info = BLOCK_INFO[blockId];
if (!info || !info.solid) continue;
const wx = worldX + lx;
const wy = ly;
const wz = worldZ + lz;
const isTransparent = info.transparent;
const targetArrays = isTransparent ?
{ pos: transparentPositions, norm: transparentNormals, uv: transparentUvs,
idx: transparentIndices, vi: tvi } :
{ pos: positions, norm: normals, uv: uvs, idx: indices, vi: vi };
for (const face of faceData) {
const nx = wx + face.dir[0];
const ny = wy + face.dir[1];
const nz = wz + face.dir[2];
const neighborId = this.getBlock(nx, ny, nz);
const neighborInfo = BLOCK_INFO[neighborId];
const neighborSolid = neighborInfo && neighborInfo.solid && !neighborInfo.transparent;
const isSameTransparent = neighborInfo && neighborInfo.transparent && isTransparent;
if (!neighborSolid && !isSameTransparent) {
let uvKey = face.uvKey;
if (face.faceName === 'up') uvKey = 'texTop';
else if (face.faceName === 'down') uvKey = 'texBottom';
else uvKey = 'texSide';
const texName = info[uvKey] || info['texSide'] || 'stone';
const uv = this.textureManager.getUV(texName);
const baseIdx = targetArrays.vi;
for (const corner of face.corners) {
targetArrays.pos.push(
wx + corner[0] * BLOCK_SIZE,
wy + corner[1] * BLOCK_SIZE,
wz + corner[2] * BLOCK_SIZE
);
targetArrays.norm.push(face.nx, face.ny, face.nz);
}
targetArrays.uv.push(uv.u0, uv.v0, uv.u1, uv.v0, uv.u1, uv.v1, uv.u0, uv.v1);
targetArrays.idx.push(baseIdx, baseIdx + 1, baseIdx + 2, baseIdx, baseIdx + 2, baseIdx +
3);
targetArrays.vi += 4;
}
}
if (isTransparent) tvi = targetArrays.vi;
else vi = targetArrays.vi;
}
}
}
const meshes = [];
if (positions.length > 0) {
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geo.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
geo.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
geo.setIndex(indices);
geo.computeBoundingSphere();
const mat = new THREE.MeshLambertMaterial({
map: this.textureManager.getTexture(),
side: THREE.FrontSide,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(0, 0, 0);
mesh.castShadow = true;
mesh.receiveShadow = true;
this.scene.add(mesh);
meshes.push(mesh);
}
if (transparentPositions.length > 0) {
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(transparentPositions, 3));
geo.setAttribute('normal', new THREE.Float32BufferAttribute(transparentNormals, 3));
geo.setAttribute('uv', new THREE.Float32BufferAttribute(transparentUvs, 2));
geo.setIndex(transparentIndices);
geo.computeBoundingSphere();
const mat = new THREE.MeshLambertMaterial({
map: this.textureManager.getTexture(),
side: THREE.FrontSide,
transparent: true,
opacity: 0.7,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(0, 0, 0);
mesh.renderOrder = 1;
mesh.material.depthWrite = false;
this.scene.add(mesh);
meshes.push(mesh);
}
if (meshes.length > 0) {
const group = new THREE.Group();
meshes.forEach(m => group.add(m));
group.position.set(worldX, 0, worldZ);
this.chunkMeshes[key] = group;
this.scene.add(group);
}
}
getLoadedChunks() { return Object.keys(this.chunks).map(k => k.split(',').map(Number)); }
ensureChunksAround(px, pz) {
const pcx = Math.floor(px / CHUNK_SIZE);
const pcz = Math.floor(pz / CHUNK_SIZE);
for (let dx = -RENDER_DISTANCE; dx <= RENDER_DISTANCE; dx++) {
for (let dz = -RENDER_DISTANCE; dz <= RENDER_DISTANCE; dz++) {
const cx = pcx + dx;
const cz = pcz + dz;
const key = this.getChunkKey(cx, cz);
if (!this.chunks[key]) this.generateChunk(cx, cz);
}
}
const allKeys = Object.keys(this.chunks);
for (const key of allKeys) {
const [cx, cz] = key.split(',').map(Number);
if (Math.abs(cx - pcx) > RENDER_DISTANCE + 1 || Math.abs(cz - pcz) > RENDER_DISTANCE + 1) {
if (this.chunkMeshes[key]) {
this.scene.remove(this.chunkMeshes[key]);
if (this.chunkMeshes[key].children) {
this.chunkMeshes[key].children.forEach(c => {
if (c.geometry) c.geometry.dispose();
if (c.material) c.material.dispose();
});
}
delete this.chunkMeshes[key];
}
delete this.chunks[key];
}
}
}
getSpawnPoint() {
const sx = 0,
sz = 0;
for (let y = CHUNK_HEIGHT - 1; y >= 0; y--) {
const b = this.getBlock(sx, y, sz);
if (b !== BLOCK_AIR && b !== BLOCK_WATER && BLOCK_INFO[b] && BLOCK_INFO[b].solid) {
return { x: sx + 0.5, y: y + 2, z: sz + 0.5 };
}
}
return { x: 0.5, y: 40, z: 0.5 };
}
}
// ============================================================
// ゲームメイン
// ============================================================
class MinecraftGame {
constructor() {
this.mode = 'survival'; // 'survival' | 'creative'
this.isRunning = false;
this.isPaused = false;
this.inventoryOpen = false;
this.playerHealth = 20;
this.playerHunger = 20;
this.playerPosition = new THREE.Vector3(0, 40, 0);
this.playerVelocity = new THREE.Vector3(0, 0, 0);
this.playerRotation = { yaw: 0, pitch: 0 };
this.selectedSlot = 0;
this.hotbarItems = new Array(9).fill(null);
this.inventoryItems = new Array(36).fill(null);
this.breakingBlock = null;
this.breakingProgress = 0;
this.breakingTargetPos = null;
this.isFlying = false;
this.flyUp = false;
this.flyDown = false;
this.keys = {};
this.mouseDown = { left: false, right: false };
this.lastInteractionTime = 0;
this.dayTime = 0;
this.dayLength = 600;
this.chestStorage = {};
this.armorSlots = { head: null, chest: null, legs: null, feet: null };
this.bedSpawnPoint = null;
this.compassTarget = null;
this.gameTime = 0;
this.spawnPoint = null;
this.renderer = null;
this.scene = null;
this.camera = null;
this.world = null;
this.textureManager = null;
this.soundManager = null;
this.particleSystem = null;
this.sunLight = null;
this.ambientLight = null;
this.raycaster = new THREE.Raycaster();
this.raycaster.far = REACH_DISTANCE;
this.blockHighlightMesh = null;
this.skyMesh = null;
this.clock = new THREE.Clock();
}
async init() {
this._setupRenderer();
this._setupScene();
this._setupLights();
this._setupSky();
this.textureManager = new TextureManager();
let zip = null;
try {
const response = await fetch('InventivetalentDev-minecraft-assets-26.1.2-0-g44dd81e.zip');
if (response.ok) {
const blob = await response.blob();
zip = await JSZip.loadAsync(blob);
console.log('ZIP loaded successfully');
}
} catch (e) {
console.warn('Could not load ZIP file:', e.message);
}
await this.textureManager.init(zip);
this.soundManager = new SoundManager();
await this.soundManager.init(zip);
this.world = new World(this.scene, this.textureManager, Math.floor(Math.random() * 100000));
this.particleSystem = new ParticleSystem(this.scene);
this._setupBlockHighlight();
this._setupEventListeners();
this._initInventory();
this._updateHotbarUI();
this._updateHeartsUI();
this._updateHungerUI();
this.renderer.setAnimationLoop((time) => this._gameLoop(time));
console.log('Game initialized');
}
_setupRenderer() {
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 1.0;
document.getElementById('game-canvas')?.remove();
const canvas = document.createElement('canvas');
canvas.id = 'game-canvas';
document.body.prepend(canvas);
this.renderer.domElement = canvas;
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('game-canvas').replaceWith(this.renderer.domElement);
this.renderer.domElement.id = 'game-canvas';
}
_setupScene() {
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color('#87CEEB');
this.scene.fog = new THREE.Fog('#c8d8f0', 60, 140);
this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 200);
this.camera.position.set(0, 40, 0);
}
_setupLights() {
this.ambientLight = new THREE.AmbientLight('#8899bb', 0.6);
this.scene.add(this.ambientLight);
this.sunLight = new THREE.DirectionalLight('#ffeedd', 1.2);
this.sunLight.position.set(100, 120, 50);
this.sunLight.castShadow = true;
this.sunLight.shadow.mapSize.width = 1024;
this.sunLight.shadow.mapSize.height = 1024;
this.sunLight.shadow.camera.near = 0.5;
this.sunLight.shadow.camera.far = 300;
this.sunLight.shadow.camera.left = -80;
this.sunLight.shadow.camera.right = 80;
this.sunLight.shadow.camera.top = 80;
this.sunLight.shadow.camera.bottom = -80;
this.scene.add(this.sunLight);
const hemi = new THREE.HemisphereLight('#8899cc', '#554433', 0.3);
this.scene.add(hemi);
}
_setupSky() {
const skyGeo = new THREE.SphereGeometry(90, 32, 32);
const skyMat = new THREE.ShaderMaterial({
uniforms: {
topColor: { value: new THREE.Color('#4488cc') },
bottomColor: { value: new THREE.Color('#c8d8f0') },
offset: { value: 20 },
exponent: { value: 0.4 },
},
vertexShader: `
varying vec3 vWorldPosition;
void main() {
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPos.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`,
fragmentShader: `
uniform vec3 topColor;
uniform vec3 bottomColor;
uniform float offset;
uniform float exponent;
varying vec3 vWorldPosition;
void main() {
float h = normalize(vWorldPosition + offset).y;
gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
}`,
side: THREE.BackSide,
depthWrite: false,
});
this.skyMesh = new THREE.Mesh(skyGeo, skyMat);
this.scene.add(this.skyMesh);
}
_setupBlockHighlight() {
const geo = new THREE.BoxGeometry(BLOCK_SIZE + 0.02, BLOCK_SIZE + 0.02, BLOCK_SIZE + 0.02);
const edges = new THREE.EdgesGeometry(geo);
const mat = new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 1, transparent: true, opacity: 0.5,
depthTest: true });
this.blockHighlightMesh = new THREE.LineSegments(edges, mat);
this.blockHighlightMesh.visible = false;
this.scene.add(this.blockHighlightMesh);
}
_setupEventListeners() {
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
});
document.addEventListener('keydown', (e) => {
this.keys[e.code] = true;
if (e.code === 'KeyE' && this.isRunning && !this.isPaused) {
this._toggleInventory();
}
if (e.code === 'Escape' && this.isRunning) {
if (this.inventoryOpen) {
this._toggleInventory();
} else {
this._togglePause();
}
}
if (e.code === 'KeyF' && this.isRunning && this.mode === 'creative') {
this.isFlying = !this.isFlying;
this.playerVelocity.y = 0;
this._showMessage(this.isFlying ? 'Flight: ON' : 'Flight: OFF');
}
if (e.code >= 'Digit1' && e.code <= 'Digit9' && this.isRunning && !this.inventoryOpen) {
this.selectedSlot = parseInt(e.code.replace('Digit', '')) - 1;
this._updateHotbarUI();
}
if (e.code === 'Space' && this.isRunning && this.mode === 'creative' && this.isFlying) {
this.flyUp = true;
}
if (e.code === 'ShiftLeft' && this.isRunning && this.mode === 'creative' && this.isFlying) {
this.flyDown = true;
}
});
document.addEventListener('keyup', (e) => {
this.keys[e.code] = false;
if (e.code === 'Space') this.flyUp = false;
if (e.code === 'ShiftLeft') this.flyDown = false;
});
const canvas = this.renderer.domElement;
canvas.addEventListener('mousedown', (e) => {
if (!this.isRunning || this.isPaused || this.inventoryOpen) return;
if (e.button === 0) this.mouseDown.left = true;
if (e.button === 2) this.mouseDown.right = true;
if (e.button === 2) this._useItem();
});
canvas.addEventListener('mouseup', (e) => {
if (e.button === 0) {
this.mouseDown.left = false;
this.breakingProgress = 0;
this.breakingBlock = null;
this.breakingTargetPos = null;
}
if (e.button === 2) this.mouseDown.right = false;
});
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
canvas.addEventListener('mousemove', (e) => {
if (!this.isRunning || this.isPaused || this.inventoryOpen) return;
if (document.pointerLockElement === canvas) {
this.playerRotation.yaw -= e.movementX * 0.002;
this.playerRotation.pitch -= e.movementY * 0.002;
this.playerRotation.pitch = Math.max(-Math.PI / 2.3, Math.min(Math.PI / 2.3, this
.playerRotation.pitch));
}
});
canvas.addEventListener('click', () => {
if (this.isRunning && !this.isPaused && !this.inventoryOpen && document.pointerLockElement !==
canvas) {
canvas.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
if (document.pointerLockElement !== canvas && this.isRunning && !this.isPaused && !this
.inventoryOpen) {
// ポーズはしないがフォーカス喪失を通知
}
});
document.getElementById('btn-survival')?.addEventListener('click', () => this.startGame('survival'));
document.getElementById('btn-creative')?.addEventListener('click', () => this.startGame('creative'));
document.getElementById('btn-controls')?.addEventListener('click', () => {
const info = document.getElementById('controls-info');
info.style.display = info.style.display === 'none' ? 'block' : 'none';
});
document.getElementById('btn-options')?.addEventListener('click', () => {
this.soundManager.volume = this.soundManager.volume === 0.5 ? 0 : 0.5;
this._showMessage('Sound: ' + (this.soundManager.volume > 0 ? 'ON' : 'OFF'));
});
document.getElementById('btn-resume')?.addEventListener('click', () => this._togglePause());
document.getElementById('btn-quit')?.addEventListener('click', () => this._quitToMenu());
document.getElementById('inventory-close')?.addEventListener('click', () => this._toggleInventory());
document.getElementById('btn-save')?.addEventListener('click', () => this._showMessage(
'Save feature coming soon!'));
}
_initInventory() {
this.hotbarItems[0] = { id: 'stone_pickaxe', count: 1 };
this.hotbarItems[1] = { id: 'torch_item', count: 16 };
this.hotbarItems[2] = { id: 'apple', count: 3 };
this.hotbarItems[3] = { id: 'bread', count: 2 };
this.hotbarItems[4] = { id: 'wooden_sword', count: 1 };
this.hotbarItems[5] = { id: 'crafting_table_item', count: 1 };
this.hotbarItems[6] = { id: 'chest_item', count: 2 };
this.hotbarItems[7] = { id: 'compass', count: 1 };
this.hotbarItems[8] = { id: 'clock', count: 1 };
}
startGame(mode) {
this.mode = mode;
this.isRunning = true;
this.isPaused = false;
this.inventoryOpen = false;
this.playerHealth = 20;
this.playerHunger = 20;
this.playerVelocity.set(0, 0, 0);
this.isFlying = false;
this.flyUp = false;
this.flyDown = false;
this.breakingProgress = 0;
this.breakingBlock = null;
this.breakingTargetPos = null;
this.chestStorage = {};
this.gameTime = 0;
this.world.ensureChunksAround(0, 0);
this.spawnPoint = this.world.getSpawnPoint();
this.playerPosition.set(this.spawnPoint.x, this.spawnPoint.y, this.spawnPoint.z);
this.playerRotation.yaw = 0;
this.playerRotation.pitch = 0;
this.camera.position.copy(this.playerPosition);
this.camera.position.y += PLAYER_HEIGHT * 0.85;
document.getElementById('home-screen').classList.add('hidden');
document.getElementById('hud').classList.add('active');
document.getElementById('crosshair').classList.add('active');
this._updateHotbarUI();
this._updateHeartsUI();
this._updateHungerUI();
this.renderer.domElement.requestPointerLock();
this._showMessage(mode === 'survival' ? 'Survival Mode - Survive and thrive!' :
'Creative Mode - Build freely!');
}
_quitToMenu() {
this.isRunning = false;
this.isPaused = false;
this.inventoryOpen = false;
document.getElementById('home-screen').classList.remove('hidden');
document.getElementById('hud').classList.remove('active');
document.getElementById('crosshair').classList.remove('active');
document.getElementById('pause-menu').classList.remove('active');
document.getElementById('inventory-overlay').classList.remove('active');
document.exitPointerLock();
this.particleSystem.clear();
this._showMessage('');
}
_togglePause() {
this.isPaused = !this.isPaused;
document.getElementById('pause-menu').classList.toggle('active', this.isPaused);
if (this.isPaused) {
document.exitPointerLock();
} else {
this.renderer.domElement.requestPointerLock();
}
}
_toggleInventory() {
this.inventoryOpen = !this.inventoryOpen;
document.getElementById('inventory-overlay').classList.toggle('active', this.inventoryOpen);
if (this.inventoryOpen) {
document.exitPointerLock();
this._renderInventoryUI();
} else {
this.renderer.domElement.requestPointerLock();
}
}
_renderInventoryUI() {
const panel = document.getElementById('inventory-panel');
panel.innerHTML = '';
const allSlots = [...this.hotbarItems, ...this.inventoryItems];
for (let i = 0; i < allSlots.length; i++) {
const slot = document.createElement('div');
slot.className = 'inv-slot';
if (i < 9) slot.style.borderColor = '#fff';
const item = allSlots[i];
if (item) {
slot.textContent = item.id.substring(0, 6);
slot.title = ITEM_INFO[item.id]?.name || item.id;
const countEl = document.createElement('span');
countEl.className = 'count';
countEl.textContent = item.count > 1 ? item.count : '';
slot.appendChild(countEl);
}
const idx = i;
slot.addEventListener('click', () => {
if (idx < 9) {
this.selectedSlot = idx;
this._updateHotbarUI();
}
});
panel.appendChild(slot);
}
}
_updateHotbarUI() {
const hotbar = document.getElementById('hotbar');
hotbar.innerHTML = '';
for (let i = 0; i < 9; i++) {
const slot = document.createElement('div');
slot.className = 'hotbar-slot' + (i === this.selectedSlot ? ' selected' : '');
const item = this.hotbarItems[i];
if (item) {
const shortName = item.id.replace(/_/g, ' ').substring(0, 8);
slot.textContent = shortName;
slot.title = ITEM_INFO[item.id]?.name || item.id;
if (item.count > 1) {
const countEl = document.createElement('span');
countEl.className = 'count';
countEl.textContent = item.count;
slot.appendChild(countEl);
}
}
const idx = i;
slot.addEventListener('click', () => {
this.selectedSlot = idx;
this._updateHotbarUI();
});
hotbar.appendChild(slot);
}
}
_updateHeartsUI() {
const container = document.getElementById('hearts-container');
container.innerHTML = '';
const hearts = Math.ceil(this.playerHealth / 2);
for (let i = 0; i < 10; i++) {
const heart = document.createElement('div');
heart.className = 'heart' + (i >= hearts ? ' empty' : '');
container.appendChild(heart);
}
}
_updateHungerUI() {
const container = document.getElementById('hunger-container');
container.innerHTML = '';
const hunger = Math.ceil(this.playerHunger / 2);
for (let i = 0; i < 10; i++) {
const icon = document.createElement('div');
icon.className = 'hunger-icon' + (i >= hunger ? ' empty' : '');
container.appendChild(icon);
}
}
_showMessage(msg) {
const el = document.getElementById('game-message');
el.textContent = msg;
el.classList.toggle('show', msg.length > 0);
clearTimeout(this._msgTimeout);
if (msg.length > 0) {
this._msgTimeout = setTimeout(() => el.classList.remove('show'), 2500);
}
}
_getSelectedItem() {
return this.hotbarItems[this.selectedSlot];
}
_useItem() {
if (!this.isRunning || this.isPaused || this.inventoryOpen) return;
const item = this._getSelectedItem();
if (!item) return;
const info = ITEM_INFO[item.id];
if (!info) return;
if (info.type === 'food') {
if (this.playerHunger < 20 || this.playerHealth < 20) {
this.playerHunger = Math.min(20, this.playerHunger + (info.hungerRestore || 2));
this.playerHealth = Math.min(20, this.playerHealth + (info.healAmount || 1));
item.count--;
if (item.count <= 0) this.hotbarItems[this.selectedSlot] = null;
this.soundManager.play('random/eat', 0.8);
this._updateHotbarUI();
this._updateHeartsUI();
this._updateHungerUI();
}
} else if (info.special === 'bucket') {
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(0, 0), this.camera);
raycaster.far = REACH_DISTANCE;
const intersects = raycaster.intersectObjects(this.scene.children, true);
if (intersects.length > 0) {
const point = intersects[0].point;
const bx = Math.floor(point.x);
const by = Math.floor(point.y);
const bz = Math.floor(point.z);
const block = this.world.getBlock(bx, by, bz);
if (block === BLOCK_WATER && this.mode === 'survival') {
this.world.setBlock(bx, by, bz, BLOCK_AIR);
this._showMessage('Water collected!');
this.soundManager.play('random/click', 0.6);
}
}
} else if (info.type === 'placeable' && info.placesBlock) {
this._placeBlock(info.placesBlock);
}
}
_placeBlock(blockId) {
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(0, 0), this.camera);
raycaster.far = REACH_DISTANCE;
const intersects = raycaster.intersectObjects(this.scene.children, true);
if (intersects.length > 0) {
const point = intersects[0].point;
const normal = intersects[0].face?.normal || new THREE.Vector3(0, 1, 0);
const placeX = Math.floor(point.x + normal.x * 0.5);
const placeY = Math.floor(point.y + normal.y * 0.5);
const placeZ = Math.floor(point.z + normal.z * 0.5);
const existingBlock = this.world.getBlock(placeX, placeY, placeZ);
if (existingBlock === BLOCK_AIR || (BLOCK_INFO[existingBlock] && !BLOCK_INFO[existingBlock].solid)) {
const playerFootX = Math.floor(this.playerPosition.x);
const playerFootY = Math.floor(this.playerPosition.y);
const playerFootZ = Math.floor(this.playerPosition.z);
const playerHeadY = Math.floor(this.playerPosition.y + PLAYER_HEIGHT);
if (!(placeX === playerFootX && placeY === playerFootY && placeZ === playerFootZ) &&
!(placeX === playerFootX && placeY === playerHeadY && placeZ === playerFootZ)) {
this.world.setBlock(placeX, placeY, placeZ, blockId);
if (this.mode === 'survival') {
const item = this._getSelectedItem();
if (item) {
item.count--;
if (item.count <= 0) this.hotbarItems[this.selectedSlot] = null;
this._updateHotbarUI();
}
}
this.soundManager.play('dig/stone1', 0.6);
const bInfo = BLOCK_INFO[blockId];
if (bInfo) {
this.particleSystem.emit(new THREE.Vector3(placeX + 0.5, placeY + 0.5, placeZ + 0.5),
bInfo.topColor, 5);
}
if (blockId === BLOCK_CHEST) {
const chestKey = `${placeX},${placeY},${placeZ}`;
this.chestStorage[chestKey] = [];
this._showMessage('Chest placed! Right-click to open (WIP)');
}
if (blockId === BLOCK_CRAFTING_TABLE) {
this._showMessage('Crafting Table placed! (Crafting UI WIP)');
}
if (blockId === BLOCK_BED) {
this.bedSpawnPoint = { x: placeX + 0.5, y: placeY + 0.5, z: placeZ + 0.5 };
this._showMessage('Bed placed! Spawn point set.');
}
}
}
}
}
_gameLoop(time) {
requestAnimationFrame(() => {});
const dt = Math.min(this.clock.getDelta(), 0.2);
if (this.isRunning && !this.isPaused && !this.inventoryOpen) {
this._updatePlayer(dt);
this._updateBreaking(dt);
this._updateWorld(dt);
this._updateDayCycle(dt);
this._updateHighlight();
this.particleSystem.update(dt);
this.gameTime += dt;
if (this.mode === 'survival') {
this._updateSurvival(dt);
}
}
this._updateCamera();
this.renderer.render(this.scene, this.camera);
}
_updatePlayer(dt) {
const speed = this.mode === 'creative' ? CREATIVE_SPEED : PLAYER_SPEED;
const forward = new THREE.Vector3(-Math.sin(this.playerRotation.yaw), 0, -Math.cos(this.playerRotation
.yaw)).normalize();
const right = new THREE.Vector3(Math.cos(this.playerRotation.yaw), 0, -Math.sin(this.playerRotation.yaw))
.normalize();
let moveX = 0,
moveZ = 0;
if (this.keys['KeyW']) { moveX += forward.x;
moveZ += forward.z; }
if (this.keys['KeyS']) { moveX -= forward.x;
moveZ -= forward.z; }
if (this.keys['KeyA']) { moveX -= right.x;
moveZ -= right.z; }
if (this.keys['KeyD']) { moveX += right.x;
moveZ += right.z; }
const len = Math.sqrt(moveX * moveX + moveZ * moveZ);
if (len > 1) { moveX /= len;
moveZ /= len; }
const isSneaking = this.keys['ShiftLeft'] && !this.isFlying;
const moveSpeed = speed * (isSneaking ? 0.4 : 1);
if (this.isFlying && this.mode === 'creative') {
this.playerVelocity.x = moveX * moveSpeed;
this.playerVelocity.z = moveZ * moveSpeed;
this.playerVelocity.y = 0;
if (this.flyUp) this.playerVelocity.y = moveSpeed;
if (this.flyDown) this.playerVelocity.y = -moveSpeed;
this.playerPosition.x += this.playerVelocity.x * dt;
this.playerPosition.y += this.playerVelocity.y * dt;
this.playerPosition.z += this.playerVelocity.z * dt;
} else {
this.playerVelocity.x = moveX * moveSpeed;
this.playerVelocity.z = moveZ * moveSpeed;
if (!this.isFlying) {
this.playerVelocity.y -= GRAVITY * dt;
}
const newX = this.playerPosition.x + this.playerVelocity.x * dt;
const newY = this.playerPosition.y + this.playerVelocity.y * dt;
const newZ = this.playerPosition.z + this.playerVelocity.z * dt;
const halfW = 0.3;
const footY = newY;
const headY = newY + PLAYER_HEIGHT;
const checkPositions = [
{ x: newX - halfW, y: footY, z: newZ - halfW },
{ x: newX + halfW, y: footY, z: newZ - halfW },
{ x: newX - halfW, y: footY, z: newZ + halfW },
{ x: newX + halfW, y: footY, z: newZ + halfW },
{ x: newX - halfW, y: footY + 0.6, z: newZ - halfW },
{ x: newX + halfW, y: footY + 0.6, z: newZ - halfW },
{ x: newX - halfW, y: footY + 0.6, z: newZ + halfW },
{ x: newX + halfW, y: footY + 0.6, z: newZ + halfW },
];
let collidesX = false,
collidesZ = false,
collidesY = false;
for (const cp of checkPositions) {
const bx = Math.floor(cp.x);
const by = Math.floor(cp.y);
const bz = Math.floor(cp.z);
const block = this.world.getBlock(bx, by, bz);
if (block !== BLOCK_AIR && BLOCK_INFO[block] && BLOCK_INFO[block].solid) {
if (Math.abs(cp.x - this.playerPosition.x) > 0.01) collidesX = true;
if (Math.abs(cp.z - this.playerPosition.z) > 0.01) collidesZ = true;
if (Math.abs(cp.y - this.playerPosition.y) > 0.01) collidesY = true;
}
}
if (!collidesX) this.playerPosition.x = newX;
if (!collidesZ) this.playerPosition.z = newZ;
if (!collidesY) {
this.playerPosition.y = newY;
} else {
if (this.playerVelocity.y < 0) {
this.playerPosition.y = Math.floor(this.playerPosition.y) + 0.01;
}
this.playerVelocity.y = 0;
}
const groundCheckY = this.playerPosition.y - 0.05;
const onGround = this._isBlockSolid(
Math.floor(this.playerPosition.x),
Math.floor(groundCheckY),
Math.floor(this.playerPosition.z)
);
if (onGround && this.keys['Space'] && !isSneaking) {
this.playerVelocity.y = JUMP_VELOCITY;
this.soundManager.play('step/grass1', 0.3);
}
}
if (this.playerPosition.y < -10) {
this.playerPosition.set(this.spawnPoint?.x || 0, this.spawnPoint?.y || 40, this.spawnPoint?.z || 0);
this.playerVelocity.set(0, 0, 0);
if (this.mode === 'survival') {
this.playerHealth = Math.max(1, this.playerHealth - 4);
this._updateHeartsUI();
}
}
this.world.ensureChunksAround(this.playerPosition.x, this.playerPosition.z);
}
_isBlockSolid(bx, by, bz) {
const block = this.world.getBlock(bx, by, bz);
return block !== BLOCK_AIR && BLOCK_INFO[block] && BLOCK_INFO[block].solid;
}
_updateBreaking(dt) {
if (!this.mouseDown.left || this.mode === 'creative') {
if (this.mode === 'creative' && this.mouseDown.left) {
this._breakBlockCreative();
this.mouseDown.left = false;
}
return;
}
const hit = this._getTargetBlock();
if (!hit) {
this.breakingProgress = 0;
this.breakingBlock = null;
this.breakingTargetPos = null;
return;
}
const { bx, by, bz, blockId } = hit;
const posKey = `${bx},${by},${bz}`;
if (posKey !== this.breakingTargetPos) {
this.breakingProgress = 0;
this.breakingBlock = blockId;
this.breakingTargetPos = posKey;
}
const hardness = BLOCK_INFO[blockId]?.hardness || 1;
const toolSpeed = 1;
this.breakingProgress += (dt / hardness) * toolSpeed;
if (this.breakingProgress >= 1) {
this._breakBlock(bx, by, bz, blockId);
this.breakingProgress = 0;
this.breakingBlock = null;
this.breakingTargetPos = null;
}
}
_breakBlockCreative() {
const hit = this._getTargetBlock();
if (hit) {
this.world.setBlock(hit.bx, hit.by, hit.bz, BLOCK_AIR);
this.soundManager.play('dig/stone1', 0.5);
this.particleSystem.emit(new THREE.Vector3(hit.bx + 0.5, hit.by + 0.5, hit.bz + 0.5),
BLOCK_INFO[hit.blockId]?.topColor || '#ffffff', 8);
}
}
_breakBlock(bx, by, bz, blockId) {
const info = BLOCK_INFO[blockId];
this.world.setBlock(bx, by, bz, BLOCK_AIR);
this.soundManager.play(info?.texSide?.includes('wood') ? 'dig/wood1' : 'dig/stone1', 0.6);
this.particleSystem.emit(new THREE.Vector3(bx + 0.5, by + 0.5, bz + 0.5), info?.topColor || '#ffffff', 10);
if (info?.drop) {
this._addItemToInventory({ id: Object.keys(BLOCK_INFO).find(k => BLOCK_INFO[k].name === BLOCK_INFO[
info.drop]?.name) || 'stone', count: 1 });
}
if (blockId === BLOCK_CHEST) {
const chestKey = `${bx},${by},${bz}`;
if (this.chestStorage[chestKey]) {
for (const item of this.chestStorage[chestKey]) {
this._addItemToInventory(item);
}
delete this.chestStorage[chestKey];
}
}
}
_getTargetBlock() {
this.raycaster.setFromCamera(new THREE.Vector2(0, 0), this.camera);
this.raycaster.far = REACH_DISTANCE;
const intersects = this.raycaster.intersectObjects(this.scene.children, true);
if (intersects.length > 0) {
const point = intersects[0].point;
const normal = intersects[0].face?.normal || new THREE.Vector3(0, 0, 1);
const bx = Math.floor(point.x - normal.x * 0.01);
const by = Math.floor(point.y - normal.y * 0.01);
const bz = Math.floor(point.z - normal.z * 0.01);
const blockId = this.world.getBlock(bx, by, bz);
if (blockId !== BLOCK_AIR && BLOCK_INFO[blockId]?.solid) {
return { bx, by, bz, blockId };
}
}
return null;
}
_addItemToInventory(item) {
if (!item || item.count <= 0) return;
const info = ITEM_INFO[item.id];
const maxStack = info?.maxStack || 64;
for (let i = 0; i < 9; i++) {
if (this.hotbarItems[i] && this.hotbarItems[i].id === item.id && this.hotbarItems[i].count < maxStack) {
const space = maxStack - this.hotbarItems[i].count;
const add = Math.min(space, item.count);
this.hotbarItems[i].count += add;
item.count -= add;
if (item.count <= 0) { this._updateHotbarUI(); return; }
}
}
for (let i = 0; i < 9; i++) {
if (!this.hotbarItems[i]) {
this.hotbarItems[i] = { id: item.id, count: item.count };
this._updateHotbarUI();
return;
}
}
for (let i = 0; i < this.inventoryItems.length; i++) {
if (this.inventoryItems[i] && this.inventoryItems[i].id === item.id && this.inventoryItems[i].count <
maxStack) {
const space = maxStack - this.inventoryItems[i].count;
const add = Math.min(space, item.count);
this.inventoryItems[i].count += add;
item.count -= add;
if (item.count <= 0) return;
}
}
for (let i = 0; i < this.inventoryItems.length; i++) {
if (!this.inventoryItems[i]) {
this.inventoryItems[i] = { id: item.id, count: item.count };
return;
}
}
}
_updateWorld(dt) {
// ワールドの更新(水のアニメーションなど将来的に追加可能)
}
_updateDayCycle(dt) {
this.dayTime += dt;
const cycleProgress = (this.dayTime % this.dayLength) / this.dayLength;
const sunAngle = cycleProgress * Math.PI * 2;
const sunX = Math.cos(sunAngle) * 100;
const sunY = Math.sin(sunAngle) * 100;
this.sunLight.position.set(sunX, sunY, 50);
const isNight = sunY < -10;
const duskDawn = Math.abs(sunY) < 20;
const brightness = isNight ? 0.15 : (duskDawn ? 0.5 : 1.2);
this.sunLight.intensity = brightness;
this.ambientLight.intensity = isNight ? 0.15 : (duskDawn ? 0.35 : 0.6);
const skyColor = isNight ? new THREE.Color('#1a1a3a') : (duskDawn ? new THREE.Color('#cc8844') : new THREE
.Color('#87CEEB'));
this.scene.background = skyColor;
this.scene.fog = new THREE.Fog(isNight ? '#1a1a3a' : '#c8d8f0', 60, 140);
if (this.skyMesh && this.skyMesh.material.uniforms) {
this.skyMesh.material.uniforms.topColor.value = isNight ? new THREE.Color('#0a0a20') : new THREE.Color(
'#4488cc');
this.skyMesh.material.uniforms.bottomColor.value = isNight ? new THREE.Color('#1a1a3a') : new THREE.Color(
'#c8d8f0');
}
}
_updateHighlight() {
const hit = this._getTargetBlock();
if (hit && this.blockHighlightMesh) {
this.blockHighlightMesh.visible = true;
this.blockHighlightMesh.position.set(hit.bx + 0.5, hit.by + 0.5, hit.bz + 0.5);
} else if (this.blockHighlightMesh) {
this.blockHighlightMesh.visible = false;
}
}
_updateSurvival(dt) {
if (this.gameTime - (this._lastHungerTick || 0) > 40) {
this._lastHungerTick = this.gameTime;
if (this.playerHunger > 0) {
this.playerHunger = Math.max(0, this.playerHunger - 0.5);
this._updateHungerUI();
}
if (this.playerHunger <= 0 && this.playerHealth > 1) {
this.playerHealth = Math.max(1, this.playerHealth - 1);
this._updateHeartsUI();
}
}
if (this.playerHealth <= 0) {
this._showMessage('You died! Respawning...');
this.playerHealth = 20;
this.playerHunger = 20;
const sp = this.bedSpawnPoint || this.spawnPoint || { x: 0, y: 40, z: 0 };
this.playerPosition.set(sp.x, sp.y, sp.z);
this.playerVelocity.set(0, 0, 0);
this._updateHeartsUI();
this._updateHungerUI();
}
}
_updateCamera() {
const yaw = this.playerRotation.yaw;
const pitch = this.playerRotation.pitch;
const camX = this.playerPosition.x;
const camY = this.playerPosition.y + PLAYER_HEIGHT * 0.85;
const camZ = this.playerPosition.z;
this.camera.position.set(camX, camY, camZ);
const lookX = camX - Math.sin(yaw) * Math.cos(pitch);
const lookY = camY + Math.sin(pitch);
const lookZ = camZ - Math.cos(yaw) * Math.cos(pitch);
this.camera.lookAt(lookX, lookY, lookZ);
}
}
// ============================================================
// 起動
// ============================================================
const game = new MinecraftGame();
game.init().then(() => {
console.log('Minecraft Browser Edition ready!');
console.log('Controls: WASD=Move, Space=Jump, Mouse=Look, LeftClick=Break, RightClick=Place/Use, E=Inventory, 1-9=Hotbar, Esc=Pause');
console.log('Special items: Torch (places light), Apple/Bread (food), Compass, Clock, Bucket, Weapons, Armor');
console.log('Special blocks: Chest (storage), Crafting Table, Bed (spawn point), Door');
}).catch(err => {
console.error('Failed to initialize game:', err);
document.body.innerHTML +=
'<div style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#300;padding:20px;z-index:999;">Failed to load game. Please check console.</div>';
});
console.log('Minecraft Browser Edition - Script loaded. Waiting for ZIP and initialization...');
</script>
</body>
</html>