diff --git a/package-lock.json b/package-lock.json index 850c3fd..b0cc84f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1400,6 +1400,7 @@ "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", @@ -1612,6 +1613,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1875,6 +1877,7 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2929,6 +2932,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3264,6 +3268,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3283,10 +3288,11 @@ } }, "node_modules/vite": { - "version": "7.1.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", - "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/src/entities/Player.ts b/src/entities/Player.ts index d5b2ee1..4a46cfa 100644 --- a/src/entities/Player.ts +++ b/src/entities/Player.ts @@ -48,12 +48,12 @@ export class Player implements IPlayerForSettings { private input: PlayerInput; private interactor = new BlockInteractor(); + private initPos: THREE.Vector3 = new THREE.Vector3(); + constructor(scene: THREE.Scene) { this.cameraHelper.visible = false; this.controls.pointerSpeed = 2; - this.position.set(32, 32, 32); - scene.add(this.camera); scene.add(this.cameraHelper); scene.add(this.selectionHelper); @@ -61,6 +61,10 @@ export class Player implements IPlayerForSettings { this.input = new PlayerInput(); } + public getInput(): PlayerInput { + return this.input; + } + get position() { return this.camera.position; } @@ -73,6 +77,16 @@ export class Player implements IPlayerForSettings { return this.worldVelocityVector; } + set position(pos: THREE.Vector3) { + this.position = pos; + } + + // it should be called only once when world is generated or player respawned + set initPosition(pos: THREE.Vector3) { + this.initPos.copy(pos); + this.position.copy(this.initPos); + } + public applySettings(settings: PlayerSettings) { this.maxSpeed = settings.maxSpeed; this.cameraHelper.visible = settings.showCameraHelper; @@ -102,7 +116,7 @@ export class Player implements IPlayerForSettings { const cmd = this.input.read(this.maxSpeed); if (cmd.resetPressed) { - this.position.set(32, 16, 32); + this.position.copy(this.initPos); this.velocity.set(0, 0, 0); } diff --git a/src/entities/PlayerInput.ts b/src/entities/PlayerInput.ts index 79783ff..7eeb758 100644 --- a/src/entities/PlayerInput.ts +++ b/src/entities/PlayerInput.ts @@ -8,16 +8,34 @@ export class PlayerInput { KeyD: false, Space: false, KeyR: false, + F5: false, + F9: false, }; private jumpQueued = false; private resetQueued = false; + private onSave?: () => void; + private onLoad?: () => void; + private onReset?: () => void; + constructor() { document.addEventListener('keydown', this.onKeyDown); document.addEventListener('keyup', this.onKeyUp); } + public setOnSaveCallback(callback: () => void) { + this.onSave = callback; + } + + public setOnLoadCallback(callback: () => void) { + this.onLoad = callback; + } + + public setOnResetCallback(callback: () => void) { + this.onReset = callback; + } + public read(maxSpeed: number): PlayerCommand { let moveZ = 0; let moveX = 0; @@ -51,6 +69,23 @@ export class PlayerInput { if (event.code === 'KeyR') { this.resetQueued = true; + if (this.onReset) { + this.onReset(); + } + } + + if (event.code === 'F5') { + event.preventDefault(); + if(this.onSave){ + this.onSave(); + } + } + + if (event.code === 'F9') { + event.preventDefault(); + if(this.onLoad){ + this.onLoad(); + } } return; diff --git a/src/game/Game.ts b/src/game/Game.ts index 075a336..92a02e1 100644 --- a/src/game/Game.ts +++ b/src/game/Game.ts @@ -51,6 +51,8 @@ export class Game { height: window.innerHeight, }; + private playerInitPos: THREE.Vector3; + private dropSystem: DropSystem; private onResizeBound = () => this.onResize(); @@ -77,13 +79,17 @@ export class Game { this.settingsApplier = new SettingsApplier( this.fog, this.world, - this.player + this.player, + this.world ); this.gui = new GameGUI(this.settingsStore); this.gui.build(); this.settingsStore.subscribe((settings) => this.settingsApplier.apply(settings) ); + this.world.setOnRegenerate(() => { + this.settingsApplier.respawnPlayerOnHighestBlock(); + }); this.hotbarUI = new HotbarUI(this.player.inventory, { inputBlocked: () => @@ -123,6 +129,12 @@ export class Game { ); this.setupWorld(); + + const spawnX = this.settingsStore.settings.player.spawnPoint.x; + const spawnZ = this.settingsStore.settings.player.spawnPoint.z; + this.playerInitPos = this.world.getSpawnPoint(spawnX, spawnZ); + this.player.initPosition = this.playerInitPos; + this.setupStats(); this.sun.setup(); this.fog.setup(); @@ -131,12 +143,76 @@ export class Game { window.addEventListener('resize', this.onResizeBound); document.addEventListener('mousedown', this.onMouseDownBound); document.addEventListener('mouseup', this.onMouseUpBound); + + this.player.getInput().setOnSaveCallback(() => this.saveGame()); + this.player.getInput().setOnLoadCallback(() => this.loadGame()); + this.player.getInput().setOnResetCallback(() => this.resetGame()); } public start() { this.renderLoop(); } + private saveGame() { + const gameState = { + playerPosition: { + x: this.player.position.x, + y: this.player.position.y, + z: this.player.position.z, + }, + playerVelocity: { + x: this.player.velocity.x, + y: this.player.velocity.y, + z: this.player.velocity.z, + }, + inventory: this.player.inventory.toJSON(), + worldData: this.world.getDataStore().toJSON(), + timestamp: Date.now(), + }; + + localStorage.setItem('minecraftGameSave', JSON.stringify(gameState)); + console.log('Game saved!'); + } + + private loadGame() { + const saved = localStorage.getItem('minecraftGameSave'); + if (!saved) { + console.log('No save found!'); + return; + } + + try { + const gameState = JSON.parse(saved); + + this.player.position.set( + gameState.playerPosition.x, + gameState.playerPosition.y, + gameState.playerPosition.z + ); + + this.player.velocity.set( + gameState.playerVelocity.x, + gameState.playerVelocity.y, + gameState.playerVelocity.z + ); + + this.player.inventory.fromJSON(gameState.inventory); + this.world.getDataStore().fromJSON(gameState.worldData); + this.world.regenerateChunks(); + + console.log('Game loaded!'); + } catch (error) { + console.error('Failed to load game:', error); + } + } + + private resetGame() { + localStorage.removeItem('minecraftGameSave'); + this.world.getDataStore().clear(); + this.world.regenerateChunks(); + console.log('Game reset! World regenerated.'); + } + private setupWorld() { this.world.generate(); this.scene.add(this.world); diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 29def00..9c5cefc 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -2,7 +2,7 @@ import type { GameSettings } from '../types/settings.types'; export const createDefaultSettings = (): GameSettings => ({ fog: { near: 50, far: 100, enabled: true }, - player: { maxSpeed: 10, showCameraHelper: false }, + player: { spawnPoint: { x: 32, y: 0, z: 32 }, maxSpeed: 10, showCameraHelper: false }, world: { renderDistance: 2, seed: 0, diff --git a/src/settings/SettingsApplier.ts b/src/settings/SettingsApplier.ts index 54677c3..39f801c 100644 --- a/src/settings/SettingsApplier.ts +++ b/src/settings/SettingsApplier.ts @@ -1,23 +1,27 @@ import type { IFogForSettings, IWorldForSettings, - IPlayerForSettings, GameSettings, } from '../types/settings.types'; +import type { Player } from '../entities/Player'; +import type { World } from '../world/World'; export class SettingsApplier { private fog: IFogForSettings; private world: IWorldForSettings; - private player: IPlayerForSettings; + private player: Player; + private worldInstance: World; constructor( fog: IFogForSettings, world: IWorldForSettings, - player: IPlayerForSettings + player: Player, + worldInstance: World ) { this.fog = fog; this.world = world; this.player = player; + this.worldInstance = worldInstance; } public apply(settings: GameSettings) { @@ -25,4 +29,13 @@ export class SettingsApplier { this.player.applySettings(settings.player); this.world.applySettings(settings.world); } + + public respawnPlayerOnHighestBlock() { + const currentX = this.player.position.x; + const currentZ = this.player.position.z; + const spawnPoint = this.worldInstance.getSpawnPoint(currentX, currentZ); + this.player.position.copy(spawnPoint); + this.player.initPosition = spawnPoint; + this.player.velocity.set(0, 0, 0); + } } diff --git a/src/types/settings.types.ts b/src/types/settings.types.ts index 9da9280..0295862 100644 --- a/src/types/settings.types.ts +++ b/src/types/settings.types.ts @@ -10,6 +10,7 @@ export interface IFogForSettings { } export type PlayerSettings = { + spawnPoint: { x: number; y: number; z: number }; maxSpeed: number; showCameraHelper: boolean; }; diff --git a/src/ui/Inventory.ts b/src/ui/Inventory.ts index 8513bb3..790da59 100644 --- a/src/ui/Inventory.ts +++ b/src/ui/Inventory.ts @@ -72,4 +72,23 @@ export class Inventory { stack.count = remaining; return false; } + + public toJSON(): Slot[] { + return this.slots.map(slot => slot ? { ...slot } : null); + } + + public fromJSON(data: Slot[]) { + for (let i = 0; i < Math.min(data.length, this.size); i++) { + const slot = data[i]; + if (slot && slot.itemId !== undefined && slot.count !== undefined) { + this.slots[i] = { + itemId: slot.itemId, + count: slot.count, + durability: slot.durability, + }; + } else { + this.slots[i] = null; + } + } + } } diff --git a/src/world/DataStore.ts b/src/world/DataStore.ts index ea06687..64a2d27 100644 --- a/src/world/DataStore.ts +++ b/src/world/DataStore.ts @@ -9,6 +9,21 @@ export class DataStore { this.data.clear(); } + public toJSON(): Record { + const obj: Record = {}; + this.data.forEach((value, key) => { + obj[key] = value; + }); + return obj; + } + + public fromJSON(obj: Record) { + this.data.clear(); + Object.entries(obj).forEach(([key, value]) => { + this.data.set(key, value); + }); + } + public set( chunkX: number, chunkZ: number, diff --git a/src/world/World.ts b/src/world/World.ts index 9fa57b2..34aec9b 100644 --- a/src/world/World.ts +++ b/src/world/World.ts @@ -38,6 +38,7 @@ export class World private dataStore = new DataStore(); private chunks = new Map(); + private onRegenerateCallback?: () => void; applySettings(settings: WorldSettings) { this.renderDistance = settings.renderDistance; @@ -58,6 +59,32 @@ export class World this.params.clouds.density = settings.clouds.density; this.generate(); + + if (this.onRegenerateCallback) { + this.onRegenerateCallback(); + } + } + + public setOnRegenerate(callback: () => void) { + this.onRegenerateCallback = callback; + } + + public getDataStore(): DataStore { + return this.dataStore; + } + + public regenerateChunks() { + for (const chunk of this.chunks.values()) { + chunk.disposeInstances(); + this.remove(chunk); + } + this.chunks.clear(); + + for (let cx = -this.renderDistance; cx <= this.renderDistance; cx++) { + for (let cz = -this.renderDistance; cz <= this.renderDistance; cz++) { + this.addChunk(cx, cz, true); + } + } } public generate() { @@ -266,6 +293,32 @@ export class World }; } + public getSpawnPoint(x: number, z: number): THREE.Vector3 { + const { chunk, local } = this.getCurrentChunkAndLocal(x, z); + const worldChunk = this.getChunk(chunk.x, chunk.z); + if (worldChunk && worldChunk.loaded) { + const y = worldChunk.chunkData.getMaxYCoord(local.x, local.z); + const playerHeight = 2; + if (y !== null) { + return new THREE.Vector3(x, y + playerHeight, z); + } + } + return new THREE.Vector3(32, 32, 32); + } + + private getCurrentChunkAndLocal(x: number, z: number) { + const worldX = Math.floor(x); + const worldZ = Math.floor(z); + const chunkX = Math.floor(worldX / this.chunkSize.width); + const chunkZ = Math.floor(worldZ / this.chunkSize.width); + const lx = worldX - chunkX * this.chunkSize.width; + const lz = worldZ - chunkZ * this.chunkSize.width; + return { + chunk: { x: chunkX, z: chunkZ }, + local: { x: lx, z: lz }, + }; + } + private revealIfVisible(pos: BlockPosition) { const { chunk, local } = this.toChunkAndLocal(pos.x, pos.y, pos.z); const worldChunk = this.getChunk(chunk.x, chunk.z); diff --git a/src/world/WorldChunk.ts b/src/world/WorldChunk.ts index 66fe064..f1e0d1c 100644 --- a/src/world/WorldChunk.ts +++ b/src/world/WorldChunk.ts @@ -67,6 +67,24 @@ class ChunkData { const inst = this.getInstanceId(x, y, z); return { id, instanceId: inst >= 0 ? inst : null }; } + + /* + get the highest non-empty block Y at (x, z) + The purpose of this function is to spawn a player on top of the terrain + instead of hardcoding a position + this.position.set(32, 32, 32); <----- hell nah + */ + getMaxYCoord(x: number, z: number): number { + let maxY = -1; + for (let y = this.size.height - 1; y >= 0; y--) { + const id = this.getId(x, y, z); + if (id !== ITEM_ID.EMPTY && id !== ITEM_ID.WATER && id !== ITEM_ID.CLOUD) { + maxY = y; + break; + } + } + return maxY; + } } export class WorldChunk extends THREE.Group { @@ -91,6 +109,11 @@ export class WorldChunk extends THREE.Group { this.data = new ChunkData(chunkSize); } + public get chunkData() + { + return this.data; + } + public generate() { this.loaded = false; @@ -419,6 +442,7 @@ export class WorldChunk extends THREE.Group { this.clear(); this.meshes.clear(); + // lekki overflow tutaj chyba leci d-_-b const maxBlocksCount = this.chunkSize.width * this.chunkSize.width * this.chunkSize.height; @@ -435,11 +459,12 @@ export class WorldChunk extends THREE.Group { continue; } - const mesh = new THREE.InstancedMesh( - geometry, - block.material, - maxBlocksCount - ); + try{ + const mesh = new THREE.InstancedMesh( + geometry, + block.material, + maxBlocksCount + ); mesh.name = block.id.toString(); mesh.castShadow = block.id !== ITEM_ID.CLOUD && block.id !== ITEM_ID.WATER; @@ -448,6 +473,9 @@ export class WorldChunk extends THREE.Group { mesh.count = 0; this.meshes.set(block.id, mesh); + } catch (e) { + console.error('Failed to create instanced mesh for block ID:', block.id, e); + } } const matrix = new THREE.Matrix4();