class_name Shape # FIXME: Potential incompatibility between different game versions that have different shapes. static var REGISTRY: Array[Shape] static func lookup(id: int) -> Shape: return REGISTRY[id] if (id >= 0) and (id < REGISTRY.size()) else EMPTY # Initialized in `Base._static_init`. static var EMPTY : Shape static var FULL : Shape ## Default shape to return when chunk or layer is not found. static var DEFAULT : Shape var id : int var base : Base var mirror : bool var angle : int var shape : Shape2D var points : PackedVector2Array var uvs : PackedVector2Array func _init(base: Base, mirror: bool, angle: int, shape: Shape2D, points: PackedVector2Array) -> void: id = REGISTRY.size() REGISTRY.append(self) self.base = base self.mirror = mirror self.angle = angle self.shape = shape self.points = points # TODO: Define UVs properly. for point in points: uvs.append((Vector2.ONE / 2) + point) ## Represents a base shape, before it has been rotated and mirrored ## (if applicable) to create a number of variants based on that shape. class Base: static var EMPTY := Base.new("empty") static var FULL := Base.new("full" , _rect()) static var HALF := Base.new("half" , [ 0,1, 2,1, 2,2, 0,2 ], true) static var SLOPE := Base.new("slope" , [ 0,1, 1,0, 1,1 ], true) static var HALF_SLOPE := Base.new("half_slope" , [ 0,2, 1,0, 2,2 ], true, true) static var HALF_SLOPE_BIG := Base.new("half_slope_big", [ 0,1, 2,0, 2,2, 0,2 ], true, true) static func _static_init() -> void: Shape.EMPTY = EMPTY.variants[0] Shape.FULL = FULL .variants[0] Shape.DEFAULT = Shape.EMPTY var name : String var variants : Array[Shape] func _init(name: String, shape = null, rotated := false, mirrored := false) -> void: self.name = name if shape == null: variants.append(Shape.new(self, false, 0, null, PackedVector2Array())) elif shape is RectangleShape2D: variants.append(Shape.new(self, false, 0, shape, _points(shape))) if rotated: shape = _rotate(shape) # rotate by 90 degrees (swap x and y components) variants.append(Shape.new(self, false, 0, shape, _points(shape))) elif shape is Array: # Convert points (pairs of `int`s) in `Array` to convex shape. var factor := float(shape.max()) var points_orig := PackedVector2Array() # untransformed points for i in range(0, shape.size(), 2): var point := Vector2(shape[i] / factor, shape[i + 1] / factor) points_orig.append(point - (Vector2.ONE / 2)) # ensure shape is centered # Add variations of the shape (rotated, mirrored), including the standard shape. for mirror in [ false, true ] if mirrored else [ false ]: for angle in [ 0, 90, 180, 270 ] if rotated else [ 0 ]: var points := points_orig.duplicate() # transformed points # Technically we could skip `duplicate()` if not mirrored and not rotated, but eh. if mirror: # Mirror (flip) points along the X axis. for i in points.size(): points[i] *= Transform2D.FLIP_X if angle != 0: # Rotate points around the center. var transform := Transform2D(deg_to_rad(angle), Vector2.ZERO) for i in points.size(): points[i] *= transform var shape_points: PackedVector2Array for point in points: shape_points.append(point * Block.SIZE) shape = ConvexPolygonShape2D.new() shape.points = shape_points variants.append(Shape.new(self, mirror, angle, shape, points)) else: breakpoint static func _rect(w := 1.0, h := 1.0) -> RectangleShape2D: var rect := RectangleShape2D.new() rect.size = Vector2(w, h) * Block.SIZE return rect static func _rotate(rect: RectangleShape2D) -> RectangleShape2D: return _rect(rect.size.y, rect.size.x) static func _points(rect: RectangleShape2D) -> PackedVector2Array: var w := rect.size.x / Block.SIZE / 2 var h := rect.size.y / Block.SIZE / 2 return [ Vector2(-w, -h), Vector2(w, -h), Vector2(w, h), Vector2(-w, h) ] class Layer extends StaticBody2D: var data: PackedByteArray var chunk: Chunk: get: return get_parent() func _init() -> void: data.resize(Chunk.SIZE * Chunk.SIZE) static func load(buffer: StreamBuffer) -> Layer: var result := Layer.new() buffer.read_raw_buffer_into(result.data) return result func save(buffer: StreamBuffer) -> void: buffer.write_raw_buffer(data) func get_at(pos: Vector2i) -> Shape: var index := Chunk.array_index(pos) return Shape.lookup(data[index]) func set_at(pos: Vector2i, value: Shape) -> void: set_id_at(pos, value.id) @rpc("reliable") func set_id_at(pos: Vector2i, id: int) -> void: var index := Chunk.array_index(pos) if data[index] == id: return # TODO: Keep track of how many non-air blocks are present, to allow for cleaning empty chunks. data[index] = id chunk.dirty = true if multiplayer.is_server(): # Send change to every player tracking this chunk. for player in chunk.get_players_tracking(): if player.network.is_local: continue # skip server player set_id_at.rpc_id(player.network.peer_id, pos, id) func _ready() -> void: # TODO: Only update if shapes have been changed. chunk.clean.connect(_update_shapes) _update_shapes() func _update_shapes() -> void: const CHUNK_HALF_SIZE := (Vector2.ONE * Chunk.SIZE * Block.SIZE) / 2 # FIXME: Updating the shape like this is not ideal. for child in get_children(): if child is CollisionShape2D: child.queue_free() for pos in BlockRegion.LOCAL_CHUNK: var shape := get_at(pos) if shape.shape == null: continue var shape2d := CollisionShape2D.new() shape2d.name = "Block %s" % pos shape2d.position = (Vector2(pos) + (Vector2.ONE / 2)) * Block.SIZE - CHUNK_HALF_SIZE shape2d.shape = shape.shape add_child(shape2d)