import { Component } from "$/common/ecs/mod.ts"; import { AIR_ID, BlockChange } from "$/common/protocol.ts"; import { BlockRegistry, EverythingRegistry } from "$/common/everything_registry.ts"; import { AIR, CHUNK_AREA, CHUNK_HEIGHT, CHUNK_SIZE, Faces, ID_MASK, VOID } from "../../common/constants.ts"; import { AssetManager } from "../assets.ts"; import { ClientWorld } from "../client_world.ts"; import { generate_chunk } from "../generation.ts"; import { ItemStack } from "../inventory.ts"; import { PlayerComponent } from "../player.ts"; import { gl, Texture } from "../renderer/mod.ts"; import { Camera } from "./camera.ts"; export interface BlockData { id: string; x: number; y: number; z: number; data: T; } export interface Block { id: string; x: number; y: number; z: number; } export { CHUNK_AREA, CHUNK_HEIGHT, CHUNK_SIZE }; export interface Chunk { x: number; z: number; blocks: Uint32Array; blocks_data: BlockData[]; generated: boolean; dirty: boolean; opaque_vertex_buffer?: WebGLBuffer; opaque_vertex_count?: number; transparent_vertex_buffer?: WebGLBuffer; transparent_vertex_count?: number; } export class Dimension extends Component { world: ClientWorld; image: Texture = AssetManager.instance.get("bworld:textures"); chunks: Chunk[] = []; second_timer = 0; tick_timer = 0; seed: string; // blocks players changed from the generated terrain, per chunk, so they survive reloading chunks changes = new Map>(); constructor(world: ClientWorld, seed = "seed") { super(); this.world = world; this.seed = seed; } add_chunk(x: number, z: number) { const chunk = { x, z, blocks: new Uint32Array(CHUNK_AREA * CHUNK_HEIGHT), blocks_data: [], dirty: true, generated: false, }; this.chunks.push(chunk); return chunk; } get_chunk(x: number, z: number) { return this.chunks.find((chunk) => chunk.x === x && chunk.z === z); } add_block(block: Block) { const block_chunk_x = Math.floor(block.x / CHUNK_SIZE); const block_chunk_z = Math.floor(block.z / CHUNK_SIZE); let chunk = this.get_chunk(block_chunk_x, block_chunk_z); if (!chunk) { chunk = this.add_chunk(block_chunk_x, block_chunk_z); } const [nid, block_info] = EverythingRegistry.get_full("blocks", block.id)!; const lx = block.x - block_chunk_x * CHUNK_SIZE; const lz = block.z - block_chunk_z * CHUNK_SIZE; const ly = block.y; const index = ly * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx; chunk.blocks[index] = nid; chunk.dirty = true; if (block_info?.on_create) { block_info?.on_create(this, block); } } get_block(x: number, y: number, z: number) { const chunk_x = Math.floor(x / CHUNK_SIZE); const chunk_z = Math.floor(z / CHUNK_SIZE); const chunk = this.get_chunk(chunk_x, chunk_z); if (!chunk) { return VOID; } const lx = x - chunk_x * CHUNK_SIZE; const lz = z - chunk_z * CHUNK_SIZE; const ly = y; const index = ly * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx; return chunk.blocks[index] & ID_MASK; } add_block_data(block_data: BlockData) { const block_chunk_x = Math.floor(block_data.x / CHUNK_SIZE); const block_chunk_z = Math.floor(block_data.z / CHUNK_SIZE); let chunk = this.get_chunk(block_chunk_x, block_chunk_z); if (!chunk) { chunk = this.add_chunk(block_chunk_x, block_chunk_z); } chunk.blocks_data.push(block_data); } get_block_data(x: number, y: number, z: number) { const chunk_x = Math.floor(x / CHUNK_SIZE); const chunk_z = Math.floor(z / CHUNK_SIZE); const chunk = this.get_chunk(chunk_x, chunk_z); if (!chunk) { return undefined; } return chunk.blocks_data.find((block_data) => block_data.x === x && block_data.y === y && block_data.z === z ) as BlockData; } break_block(x: number, y: number, z: number, drop_item: boolean = true) { const block_chunk_x = Math.floor(x / CHUNK_SIZE); const block_chunk_z = Math.floor(z / CHUNK_SIZE); const chunk = this.get_chunk(block_chunk_x, block_chunk_z); if (!chunk) { return; } const block_info = EverythingRegistry.get_by_id("blocks", this.get_block(x, y, z))!; if (drop_item && block_info.drop_table) { const [player] = this.world.get_tag("player")!; const player_component = player.get(PlayerComponent)!; player_component.player_inventory.container.add_item(new ItemStack(block_info.drop_table)); } const lx = x - block_chunk_x * CHUNK_SIZE; const lz = z - block_chunk_z * CHUNK_SIZE; const ly = y; const index = ly * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx; chunk.blocks[index] = AIR; chunk.dirty = true; if (lx === 0) { const n = this.get_chunk(block_chunk_x - 1, block_chunk_z); if (n) { n.dirty = true; } } else if (lx === CHUNK_SIZE - 1) { const n = this.get_chunk(block_chunk_x + 1, block_chunk_z); if (n) { n.dirty = true; } } if (lz === 0) { const n = this.get_chunk(block_chunk_x, block_chunk_z - 1); if (n) { n.dirty = true; } } else if (lz === CHUNK_SIZE - 1) { const n = this.get_chunk(block_chunk_x, block_chunk_z + 1); if (n) { n.dirty = true; } } if (block_info?.on_break) { block_info.on_break(this, { x, y, z, id: block_info.id }); } } index_to_xyz(index: number) { const y = Math.floor(index / CHUNK_AREA); const rem = index % CHUNK_AREA; const z = Math.floor(rem / CHUNK_SIZE); const x = rem % CHUNK_SIZE; return [x, y, z]; } record_change(x: number, y: number, z: number, id: string) { const chunk_key = `${Math.floor(x / CHUNK_SIZE)},${Math.floor(z / CHUNK_SIZE)}`; let chunk_changes = this.changes.get(chunk_key); if (!chunk_changes) { chunk_changes = new Map(); this.changes.set(chunk_key, chunk_changes); } chunk_changes.set(`${x},${y},${z}`, [x, y, z, id]); } // call after the player changes a block so it gets saved and sent to the server sync_block(x: number, y: number, z: number) { const numeric_id = this.get_block(x, y, z); const id = numeric_id === AIR ? AIR_ID : EverythingRegistry.get_by_id("blocks", numeric_id)?.id; if (!id) { return; } this.record_change(x, y, z, id); this.world.connection?.send({ type: "set_block", x, y, z, id }); } // set a block without drops or syncing, for changes that came from somewhere else apply_change(x: number, y: number, z: number, id: string) { const chunk = this.get_chunk(Math.floor(x / CHUNK_SIZE), Math.floor(z / CHUNK_SIZE)); if (!chunk || !chunk.generated) { return; } const current = this.get_block(x, y, z); if (id === AIR_ID) { if (current !== AIR) { this.break_block(x, y, z, false); } return; } const nid = EverythingRegistry.get_id("blocks", id); if (nid === undefined || nid === current) { return; } this.add_block({ x, y, z, id }); } apply_chunk_changes(cx: number, cz: number) { for (const [x, y, z, id] of this.changes.get(`${cx},${cz}`)?.values() ?? []) { this.apply_change(x, y, z, id); } } load_chunk(cx: number, cz: number) { generate_chunk(this, cx, cz, this.seed); const chunk = this.get_chunk(cx, cz); if (chunk) { chunk.generated = true; chunk.dirty = true; } const neighbors = [ [cx - 1, cz], [cx + 1, cz], [cx, cz - 1], [cx, cz + 1], ]; for (const [nx, nz] of neighbors) { const neighbor = this.chunks.find((c) => c.x === nx && c.z === nz); if (neighbor && neighbor.generated) { neighbor.dirty = true; } } // trees spill into neighboring chunks, so their changes need reapplying too for (let dx = -1; dx <= 1; dx++) { for (let dz = -1; dz <= 1; dz++) { this.apply_chunk_changes(cx + dx, cz + dz); } } } unload_chunk(cx: number, cz: number) { const chunk_i = this.chunks.findIndex((c) => c.x === cx && c.z === cz); if (chunk_i === -1) { console.warn("Tried unloading a chunk that doesn't exist"); return; } this.delete_chunk_mesh(this.chunks[chunk_i]); this.chunks.splice(chunk_i, 1); } delete_chunk_mesh(chunk: Chunk) { if (chunk.opaque_vertex_buffer) { gl.deleteBuffer(chunk.opaque_vertex_buffer); } if (chunk.transparent_vertex_buffer) { gl.deleteBuffer(chunk.transparent_vertex_buffer); } } get_looked_block( dimension: Dimension, camera: Camera, max_distance = 6, step = 0.05, ): { x: number; y: number; z: number; block: number; face: Faces } | undefined { const yaw = camera.yaw; const pitch = camera.pitch; const cos_pitch = Math.cos(pitch); const dx = -Math.sin(yaw) * cos_pitch; const dy = Math.sin(pitch); const dz = -Math.cos(yaw) * cos_pitch; let x = camera.x; let y = camera.y; let z = camera.z; let prev_bx = Math.floor(x); let prev_by = Math.floor(y); let prev_bz = Math.floor(z); let dist = 0; while (dist <= max_distance) { x += dx * step; y += dy * step; z += dz * step; dist += step; const bx = Math.floor(x); const by = Math.floor(y); const bz = Math.floor(z); if (bx === prev_bx && by === prev_by && bz === prev_bz) { continue; } const block = dimension.get_block(bx, by, bz); if (block && block !== AIR && block !== VOID) { let face: Faces; if (bx > prev_bx) { face = "west"; } else if (bx < prev_bx) { face = "east"; } else if (by > prev_by) { face = "bottom"; } else if (by < prev_by) { face = "top"; } else if (bz > prev_bz) { face = "north"; } else { face = "south"; } return { x: bx, y: by, z: bz, face, block }; } prev_bx = bx; prev_by = by; prev_bz = bz; } return undefined; } create_padded_chunk(chunk: Chunk) { const cx = chunk.x; const cz = chunk.z; const size = CHUNK_SIZE + 2; const padded = new Uint32Array(size * size * CHUNK_HEIGHT); for (let y = 0; y < CHUNK_HEIGHT; y++) { for (let z = -1; z <= CHUNK_SIZE; z++) { for (let x = -1; x <= CHUNK_SIZE; x++) { let block: number; if (x >= 0 && x < CHUNK_SIZE && z >= 0 && z < CHUNK_SIZE) { const index = y * CHUNK_SIZE * CHUNK_SIZE + z * CHUNK_SIZE + x; block = chunk.blocks[index] & ID_MASK; } else { const wx = cx * CHUNK_SIZE + x; const wz = cz * CHUNK_SIZE + z; block = this.get_block(wx, y, wz) ?? AIR; } const px = x + 1; const pz = z + 1; const pindex = y * size * size + pz * size + px; padded[pindex] = block; } } } return padded; } }