import Timer from "../util/Timer.js"; import GameSettings from "./GameSettings.js"; import GameWindow from "./GameWindow.js"; import WorldRenderer from "./render/WorldRenderer.js"; import ScreenRenderer from "./render/gui/ScreenRenderer.js"; import ItemRenderer from "./render/gui/ItemRenderer.js"; import IngameOverlay from "./gui/overlay/IngameOverlay.js"; import PlayerEntity from "./entity/PlayerEntity.js"; import SoundManager from "./sound/SoundManager.js"; import Block from "./world/block/Block.js"; import BoundingBox from "../util/BoundingBox.js"; import {BlockRegistry} from "./world/block/BlockRegistry.js"; import FontRenderer from "./render/gui/FontRenderer.js"; import GrassColorizer from "./render/GrassColorizer.js"; import GuiMainMenu from "./gui/screens/GuiMainMenu.js"; import GuiLoadingScreen from "./gui/screens/GuiLoadingScreen.js"; import * as THREE from "../../../../../libraries/three.module.js"; import ParticleRenderer from "./render/particle/ParticleRenderer.js"; import GuiChat from "./gui/screens/GuiChat.js"; import CommandHandler from "./command/CommandHandler.js"; import GuiContainerCreative from "./gui/screens/container/GuiContainerCreative.js"; export default class Minecraft { static VERSION = "1.0.4" static URL_GITHUB = "https://github.com/labystudio/js-minecraft"; /** * Create Minecraft instance and render it on a canvas */ constructor(canvasWrapperId, resources) { this.resources = resources; this.currentScreen = null; this.loadingScreen = null; this.world = null; this.player = null; this.fps = 0; // Tick timer this.timer = new Timer(20); this.settings = new GameSettings(); this.settings.load(); // Create window and world renderer this.window = new GameWindow(this, canvasWrapperId); // Create renderers this.worldRenderer = new WorldRenderer(this, this.window); this.screenRenderer = new ScreenRenderer(this, this.window); this.itemRenderer = new ItemRenderer(this, this.window); // Create current screen and overlay this.ingameOverlay = new IngameOverlay(this, this.window); // Command handler this.commandHandler = new CommandHandler(this); this.frames = 0; this.lastTime = Date.now(); // Create all blocks BlockRegistry.create(); this.itemRenderer.initialize(); // Create font renderer this.fontRenderer = new FontRenderer(this); // Grass colorizer this.grassColorizer = new GrassColorizer(this); this.particleRenderer = new ParticleRenderer(this); // Update window size this.window.updateWindowSize(); // Create sound manager this.soundManager = new SoundManager(); this.displayScreen(new GuiMainMenu()); // Initialize this.init(); } init() { // Start render loop this.running = true; this.requestNextFrame(); } loadWorld(world) { if (world === null) { this.worldRenderer.reset(); this.itemRenderer.reset(); this.world.chunks.clear(); this.world = null; this.player = null; this.loadingScreen = null; this.displayScreen(new GuiMainMenu()); } else { // Display loading screen this.loadingScreen = new GuiLoadingScreen(); this.loadingScreen.setTitle("Building terrain..."); this.displayScreen(this.loadingScreen); // Create world this.world = world; this.worldRenderer.scene.add(this.world.group); // Create player this.player = new PlayerEntity(this, this.world); this.player.username = "Player" + Math.floor(Math.random() * 100); this.world.addEntity(this.player); // Load spawn chunks and respawn player this.world.findSpawn(); this.world.loadSpawnChunks(); this.player.respawn(); } } hasInGameFocus() { return this.window.mouseLocked && this.currentScreen === null; } isInGame() { return this.world !== null && this.worldRenderer !== null && this.player !== null; } addMessageToChat(message) { this.ingameOverlay.chatOverlay.addMessage(message); } requestNextFrame() { requestAnimationFrame(() => { if (this.running) { this.requestNextFrame(); this.onLoop(); } }); } onLoop() { // Update the timer this.timer.advanceTime(); // Call the tick to reach updates 20 per seconds for (let i = 0; i < this.timer.ticks; i++) { this.onTick(); } // Render the game this.onRender(this.timer.partialTicks); // Increase rendered frame this.frames++; // Loop if a second passed while (Date.now() >= this.lastTime + 1000) { this.fps = this.frames; this.lastTime += 1000; this.frames = 0; } } onRender(partialTicks) { if (this.isInGame()) { // Player rotation if (!this.isPaused()) { this.player.turn(this.window.mouseMotionX, this.window.mouseMotionY); this.window.mouseMotionX = 0; this.window.mouseMotionY = 0; } // Update lights while (this.world.updateLights()) { // Empty } // Render the game if (this.hasInGameFocus()) { this.worldRenderer.render(partialTicks); } } // Render current screen this.screenRenderer.render(partialTicks); this.itemRenderer.render(partialTicks); } displayScreen(screen) { if (typeof screen === "undefined") { console.error("Tried to display an undefined screen"); return; } // Close previous screen if (this.currentScreen !== null) { this.currentScreen.onClose(); } // Switch screen this.currentScreen = screen; // Update window size this.window.updateWindowSize(); // Initialize new screen if (screen === null) { this.window.requestFocus(); } else { this.window.exitFocus(); screen.setup(this, this.window.width, this.window.height); } // Update items this.itemRenderer.rebuildAllItems(); } onTick() { if (this.isInGame() && !this.isPaused()) { // Tick world this.world.onTick(); // Tick renderer this.worldRenderer.onTick(); // Tick the player this.player.onUpdate(); // Tick particle renderer this.particleRenderer.onTick(); // Tick overlay this.ingameOverlay.onTick(); } // Tick the screen if (this.currentScreen !== null) { this.currentScreen.updateScreen(); } // Update loading progress if (this.loadingScreen !== null && this.isInGame()) { let cameraChunkX = Math.floor(this.player.x) >> 4; let cameraChunkZ = Math.floor(this.player.z) >> 4; let renderDistance = this.settings.viewDistance; let requiredChunks = Math.pow(renderDistance * 2 - 1, 2); let loadedChunks = this.world.chunks.size; // Load chunks and count setTimeout(() => { for (let x = -renderDistance + 1; x < renderDistance; x++) { for (let z = -renderDistance + 1; z < renderDistance; z++) { this.world.getChunkAt(cameraChunkX + x, cameraChunkZ + z); } } }, 0); // Update progress let progress = 1 / requiredChunks * Math.max(0, loadedChunks - this.world.lightUpdateQueue.length / 1000); this.loadingScreen.setProgress(progress); // Finish loading if (progress >= 0.99) { this.loadingScreen = null; this.displayScreen(null); } } } onKeyPressed(button) { // Select slot for (let i = 1; i <= 9; i++) { if (button === 'Digit' + i) { this.player.inventory.selectedSlotIndex = i - 1; } } // Toggle perspective if (button === this.settings.keyTogglePerspective) { this.settings.thirdPersonView = (this.settings.thirdPersonView + 1) % 3; this.settings.save(); } // Open chat if (button === this.settings.keyOpenChat) { this.displayScreen(new GuiChat()); } // Toggle debug overlay if (button === "F3") { this.settings.debugOverlay = !this.settings.debugOverlay; } // Open inventory if (button === this.settings.keyOpenInventory) { this.displayScreen(new GuiContainerCreative(this.player)); } } onMouseClicked(button) { if (this.window.mouseLocked) { let hitResult = this.player.rayTrace(5, this.timer.partialTicks); // Destroy block if (button === 0) { if (hitResult != null) { // Get previous block let typeId = this.world.getBlockAt(hitResult.x, hitResult.y, hitResult.z); let block = Block.getById(typeId); if (typeId !== 0) { let soundName = block.getSound().getBreakSound(); // Play sound this.soundManager.playSound( soundName, hitResult.x + 0.5, hitResult.y + 0.5, hitResult.z + 0.5, 1.0, 1.0 ); // Spawn particle this.particleRenderer.spawnBlockBreakParticle(this.world, hitResult.x, hitResult.y, hitResult.z); // Destroy block this.world.setBlockAt(hitResult.x, hitResult.y, hitResult.z, 0); } } this.player.swingArm(); } // Pick block if (button === 1) { if (hitResult != null) { let typeId = this.world.getBlockAt(hitResult.x, hitResult.y, hitResult.z); if (typeId !== 0) { // Switch to slot if item is already in hotbar for (const item of this.player.inventory.items) { const index = this.player.inventory.items.indexOf(item); if (item === typeId && index <= 8) { this.player.inventory.selectedSlotIndex = index; return; } } // Set item in hotbar this.player.inventory.setItemInSelectedSlot(typeId); } } } // Place block if (button === 2) { if (hitResult != null) { let x = hitResult.x + hitResult.face.x; let y = hitResult.y + hitResult.face.y; let z = hitResult.z + hitResult.face.z; let placedBoundingBox = new BoundingBox(x, y, z, x + 1, y + 1, z + 1); // Don't place blocks if the player is standing there if (!placedBoundingBox.intersects(this.player.boundingBox)) { let typeId = this.player.inventory.getItemInSelectedSlot(); // Get previous block let prevTypeId = this.world.getBlockAt(x, y, z); if (typeId !== 0 && prevTypeId !== typeId) { // Place block this.world.setBlockAt(x, y, z, typeId); // Swing player arm this.player.swingArm(); // Handle block abilities let block = Block.getById(typeId); block.onBlockPlaced(this.world, x, y, z, hitResult.face); // Play sound let sound = block.getSound(); let soundName = sound.getStepSound(); this.soundManager.playSound( soundName, hitResult.x + 0.5, hitResult.y + 0.5, hitResult.z + 0.5, 1.0, sound.getPitch() * 0.8 ); } } } } // Rebuild multiple chunk sections this.worldRenderer.flushRebuild = true; } } onMouseScroll(delta) { if (this.isInGame()) { this.player.inventory.shiftSelectedSlot(delta); } } isPaused() { return !this.hasInGameFocus() && this.loadingScreen === null; } stop() { if (this.currentScreen !== null) { this.currentScreen.onClose(); } this.running = false; this.worldRenderer.reset(); this.itemRenderer.reset(); this.screenRenderer.reset(); this.window.close(); } getThreeTexture(id) { if (!(id in this.resources)) { console.error("Texture not found: " + id); return; } let image = this.resources[id]; let canvas = document.createElement('canvas'); let context = canvas.getContext("2d"); canvas.width = image.width; canvas.height = image.height; context.imageSmoothingEnabled = false; context.drawImage(image, 0, 0, image.width, image.height); return new THREE.CanvasTexture(canvas); } }