Spaces:
Running
Running
| <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 | <b>Space</b> - Jump | <b>Shift</b> - Sneak<br> | |
| <b>Mouse</b> - Look around | <b>Left Click</b> - Break block<br> | |
| <b>Right Click</b> - Place block / Use item | <b>E</b> - Inventory<br> | |
| <b>1-9</b> - Hotbar select | <b>Esc</b> - Pause | <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> |