class_name MapView extends Node2D ## Draws the terrain of the shared world in the style of `art`: every tile to ## fit its neighbours, the cracks of the hits it took, and a short burst of ## chips where a pickaxe lands. The map is drawn in square blocks, so that a ## hit draws only the blocks around its tile again. ## The terrain that rock masses and the edge of the map are made of. Tiles ## beside it are drawn to fit it. const SOLID: Array[int] = [GwSession.TERRAIN_SOFT_ROCK, GwSession.TERRAIN_HARD_ROCK, GwSession.TERRAIN_BEDROCK] ## Tiles along each side of a block. const BLOCK := 16 ## Seconds a burst of chips lasts. const HIT_SECONDS := 0.35 var session: GwSession var art: Art var _width := 0 var _height := 0 var _terrain := PackedByteArray() var _damage := PackedByteArray() var _solid := PackedByteArray() var _masks := PackedInt32Array() var _variants := PackedInt32Array() var _blocks: Array[MapBlock] = [] var _bursts: Array[Dictionary] = [] var _burst_layer := Node2D.new() ## A square part of the map that draws itself. class MapBlock extends Node2D: var view: MapView var tiles: Rect2i func _init(owner_view: MapView, block_tiles: Rect2i) -> void: view = owner_view tiles = block_tiles func _draw() -> void: view.draw_block(self, tiles) func _ready() -> void: _burst_layer.draw.connect(_draw_bursts) add_child(_burst_layer) set_process(false) ## The graphic of a terrain code. static func id_of(terrain: int) -> StringName: match terrain: GwSession.TERRAIN_SOFT_ROCK: return Art.SOFT_ROCK GwSession.TERRAIN_HARD_ROCK: return Art.HARD_ROCK GwSession.TERRAIN_BEDROCK: return Art.BEDROCK return Art.GROUND ## Takes the whole map from `session` anew and draws it, or shows nothing if ## no world runs. Call it when a world starts or was replaced. func reload() -> void: for block in _blocks: remove_child(block) block.free() _blocks.clear() _bursts.clear() _burst_layer.queue_redraw() if session == null or not session.is_running(): _width = 0 _height = 0 return _width = session.map_width() _height = session.map_height() _terrain = session.terrain_codes() _damage = session.damage_codes() _solid = TileShape.flags(_terrain, SOLID) _masks.resize(_width * _height) _variants.resize(_width * _height) for y in _height: for x in _width: var tile := Vector2i(x, y) _masks[y * _width + x] = TileShape.mask(_solid, _width, _height, tile, true) _variants[y * _width + x] = TileShape.variant(tile) for top in range(0, _height, BLOCK): for left in range(0, _width, BLOCK): var block := MapBlock.new(self, Rect2i(left, top, mini(BLOCK, _width - left), mini(BLOCK, _height - top))) _blocks.append(block) add_child(block) move_child(_burst_layer, -1) ## Draws everything again, for example in another style. func redraw() -> void: for block in _blocks: block.queue_redraw() ## Takes `tile` anew from the session after it changed, and draws it and the ## tiles around it again, which may have to fit it differently now. func refresh_tile(tile: Vector2i) -> void: if _blocks.is_empty() or not Rect2i(0, 0, _width, _height).has_point(tile): return _terrain = session.terrain_codes() _damage = session.damage_codes() var index := tile.y * _width + tile.x _solid[index] = 1 if SOLID.has(_terrain[index]) else 0 var touched: Array[MapBlock] = [] for y in range(tile.y - 1, tile.y + 2): for x in range(tile.x - 1, tile.x + 2): var around := Vector2i(x, y) if not Rect2i(0, 0, _width, _height).has_point(around): continue _masks[y * _width + x] = TileShape.mask(_solid, _width, _height, around, true) var block := _block_of(around) if not touched.has(block): touched.append(block) for block in touched: block.queue_redraw() ## The block that draws `tile`. func _block_of(tile: Vector2i) -> MapBlock: var across := ceili(_width / float(BLOCK)) @warning_ignore("integer_division") return _blocks[(tile.y / BLOCK) * across + tile.x / BLOCK] ## Shows a burst of chips on `tile`, where a pickaxe just landed. func hit(tile: Vector2i) -> void: _bursts.append({"tile": tile, "age": 0.0}) set_process(true) ## The number of blocks the map is drawn in. Tests read it. func block_count() -> int: return _blocks.size() ## The number of bursts still shown. Tests read it. func burst_count() -> int: return _bursts.size() ## The neighbour mask a tile is drawn with. Tests read it. func mask_at(tile: Vector2i) -> int: return _masks[tile.y * _width + tile.x] ## Draws the tiles `tiles` onto `block`, while the block draws. func draw_block(block: MapBlock, tiles: Rect2i) -> void: if art == null: return var size := GridMotion.TILE_SIZE for y in range(tiles.position.y, tiles.end.y): for x in range(tiles.position.x, tiles.end.x): var index := y * _width + x var rect := Rect2(x * size, y * size, size, size) var variant := _variants[index] art.draw(block, id_of(_terrain[index]), rect, {"solid": _masks[index], "variant": variant}) if _damage[index] > 0: art.draw(block, Art.CRACKS, rect, {"hits": _damage[index], "variant": variant}) func _process(delta: float) -> void: for burst in _bursts: burst["age"] += delta / HIT_SECONDS _bursts = _bursts.filter(func(burst: Dictionary) -> bool: return burst["age"] < 1.0) if _bursts.is_empty(): set_process(false) _burst_layer.queue_redraw() func _draw_bursts() -> void: if art == null: return var size := GridMotion.TILE_SIZE for burst in _bursts: var tile: Vector2i = burst["tile"] var rect := Rect2(Vector2(tile) * size, Vector2(size, size)) art.draw(_burst_layer, Art.HIT, rect, {"age": burst["age"], "variant": TileShape.variant(tile)})