M1d: Stilproben #12
+36
-3
@@ -1,7 +1,8 @@
|
||||
class_name Avatar
|
||||
extends Node2D
|
||||
## The local player's figure. It moves outside the lockstep simulation; only
|
||||
## its pickaxe hits become commands.
|
||||
## its pickaxe hits become commands. `art` draws it, stepping while it walks
|
||||
## and facing the way it went last.
|
||||
|
||||
## Walking speed: five tiles per second.
|
||||
const SPEED := 5.0 * GridMotion.TILE_SIZE
|
||||
@@ -9,9 +10,30 @@ const RADIUS := 5.0
|
||||
## Longest frame that moves the figure in one go. Longer frames are cut, so
|
||||
## that the figure cannot pass through a wall after a stall.
|
||||
const MAX_STEP_SECONDS := 0.1
|
||||
## How far the step of a walking figure turns per pixel, in radians: about
|
||||
## two steps per second at walking speed.
|
||||
const STEP_PER_PIXEL := 0.16
|
||||
|
||||
## Callable(x: int, y: int) -> bool that says whether a tile can be walked on.
|
||||
var is_walkable: Callable
|
||||
var art: Art
|
||||
## The number of the player the figure belongs to, for its colour, or -1
|
||||
## before the player is in the world.
|
||||
var player := -1
|
||||
## 1.0 while the figure faces right, -1.0 while it faces left.
|
||||
var facing := 1.0
|
||||
## Grows while the figure walks; the styles swing the steps by it.
|
||||
var step := 0.0
|
||||
## True while the figure walked in the last frame.
|
||||
var moving := false
|
||||
|
||||
|
||||
## Where a figure standing at `at` is drawn: a tile wide and one and a half
|
||||
## tiles tall, with its feet just below its body, so that it stands upright
|
||||
## in its tile.
|
||||
static func figure_rect(at: Vector2) -> Rect2:
|
||||
var size := GridMotion.TILE_SIZE
|
||||
return Rect2(at.x - size / 2.0, at.y + RADIUS + 1.0 - size * 1.5, size, size * 1.5)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
@@ -20,12 +42,23 @@ func _process(delta: float) -> void:
|
||||
|
||||
## Moves the figure in `direction` for `delta` seconds, sliding along walls.
|
||||
func walk(direction: Vector2, delta: float) -> void:
|
||||
var was_moving := moving
|
||||
moving = false
|
||||
if direction == Vector2.ZERO or not is_walkable.is_valid():
|
||||
if was_moving:
|
||||
queue_redraw()
|
||||
return
|
||||
var motion := direction * SPEED * minf(delta, MAX_STEP_SECONDS)
|
||||
var before := position
|
||||
position = GridMotion.move(position, motion, RADIUS, is_walkable)
|
||||
var walked := position.distance_to(before)
|
||||
moving = walked > 0.0
|
||||
step += walked * STEP_PER_PIXEL
|
||||
if direction.x != 0.0:
|
||||
facing = signf(direction.x)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
draw_circle(Vector2.ZERO, RADIUS + 1.0, Color(0.15, 0.1, 0.05))
|
||||
draw_circle(Vector2.ZERO, RADIUS, Color(0.95, 0.85, 0.6))
|
||||
if art != null:
|
||||
art.draw(self, Art.FIGURE, figure_rect(Vector2.ZERO), {"player": player, "facing": facing, "step": step, "moving": moving})
|
||||
|
||||
+43
-14
@@ -1,19 +1,27 @@
|
||||
class_name Figures
|
||||
extends Node2D
|
||||
## The figures of the other players, with their names. A new position comes
|
||||
## over the network about ten times a second; in between, each figure glides
|
||||
## toward the latest one.
|
||||
## The figures of the other players, with their names, drawn by `art`. A new
|
||||
## position comes over the network about ten times a second; in between,
|
||||
## each figure glides toward the latest one, stepping and facing the way it
|
||||
## goes.
|
||||
|
||||
## How quickly a figure closes the gap to its latest position, per second.
|
||||
const GLIDE := 15.0
|
||||
const COLOR := Color(0.55, 0.8, 0.95)
|
||||
const OUTLINE := Color(0.05, 0.1, 0.15)
|
||||
## Pixels per frame below which a gliding figure counts as standing.
|
||||
const STANDING := 0.05
|
||||
## Size and width of the names, in pixels of the playing field. The names
|
||||
## are drawn at the size they have on screen, so that they stay sharp.
|
||||
const NAME_SIZE := 8
|
||||
const NAME_WIDTH := 80.0
|
||||
const NAME_OUTLINE := Color(0.05, 0.05, 0.06)
|
||||
|
||||
var session: GwSession
|
||||
var art: Art
|
||||
var _shown: Dictionary[int, Vector2] = {}
|
||||
var _latest: Dictionary[int, Vector2] = {}
|
||||
var _facing: Dictionary[int, float] = {}
|
||||
var _step: Dictionary[int, float] = {}
|
||||
var _moving: Dictionary[int, bool] = {}
|
||||
|
||||
|
||||
## Moves the figure of `player` toward `position`. A figure that was not
|
||||
@@ -27,14 +35,14 @@ func place(player: int, position: Vector2) -> void:
|
||||
|
||||
## Removes the figure of `player`.
|
||||
func forget(player: int) -> void:
|
||||
_shown.erase(player)
|
||||
_latest.erase(player)
|
||||
for figures: Dictionary in [_shown, _latest, _facing, _step, _moving]:
|
||||
figures.erase(player)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
_shown.clear()
|
||||
_latest.clear()
|
||||
for figures: Dictionary in [_shown, _latest, _facing, _step, _moving]:
|
||||
figures.clear()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
@@ -43,21 +51,42 @@ func shown_at(player: int) -> Vector2:
|
||||
return _shown.get(player, Vector2.INF)
|
||||
|
||||
|
||||
## 1.0 if the figure of `player` faces right, -1.0 if it faces left.
|
||||
func facing_of(player: int) -> float:
|
||||
return _facing.get(player, 1.0)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _shown.is_empty():
|
||||
return
|
||||
var weight := minf(1.0, GLIDE * delta)
|
||||
for player in _shown:
|
||||
_shown[player] = _shown[player].lerp(_latest[player], weight)
|
||||
var before := _shown[player]
|
||||
_shown[player] = before.lerp(_latest[player], weight)
|
||||
var moved := _shown[player] - before
|
||||
if absf(moved.x) > STANDING:
|
||||
_facing[player] = signf(moved.x)
|
||||
_step[player] = _step.get(player, 0.0) + moved.length() * Avatar.STEP_PER_PIXEL
|
||||
_moving[player] = moved.length() > STANDING
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if art == null:
|
||||
return
|
||||
var font := ThemeDB.fallback_font
|
||||
# The camera enlarges the field. Text drawn at its size on screen and then
|
||||
# shrunk stays sharp; text drawn small and enlarged would blur.
|
||||
var zoom := maxf(1.0, get_global_transform_with_canvas().get_scale().x)
|
||||
for player in _shown:
|
||||
var at := _shown[player]
|
||||
draw_circle(at, Avatar.RADIUS + 1.0, OUTLINE)
|
||||
draw_circle(at, Avatar.RADIUS, COLOR)
|
||||
var state := {"player": player, "facing": facing_of(player), "step": _step.get(player, 0.0), "moving": _moving.get(player, false)}
|
||||
art.draw(self, Art.FIGURE, Avatar.figure_rect(at), state)
|
||||
if session != null:
|
||||
var label := Vector2(at.x - NAME_WIDTH / 2.0, at.y - Avatar.RADIUS - 3.0)
|
||||
draw_string(font, label, session.player_name(player), HORIZONTAL_ALIGNMENT_CENTER, NAME_WIDTH, NAME_SIZE)
|
||||
var label := Vector2(at.x - NAME_WIDTH / 2.0, Avatar.figure_rect(at).position.y - 2.0)
|
||||
var text := session.player_name(player)
|
||||
var size := roundi(NAME_SIZE * zoom)
|
||||
draw_set_transform(label, 0.0, Vector2.ONE / zoom)
|
||||
draw_string_outline(font, Vector2.ZERO, text, HORIZONTAL_ALIGNMENT_CENTER, NAME_WIDTH * zoom, size, maxi(2, roundi(zoom * 1.5)), NAME_OUTLINE)
|
||||
draw_string(font, Vector2.ZERO, text, HORIZONTAL_ALIGNMENT_CENTER, NAME_WIDTH * zoom, size)
|
||||
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
|
||||
|
||||
+30
-6
@@ -40,6 +40,8 @@ var session: GwSession
|
||||
## The network connection of an open world or a guest, or null.
|
||||
var link: SessionLink
|
||||
var clock: TickClock
|
||||
## The style the playing field is drawn in.
|
||||
var art: Art
|
||||
var menu: Menu
|
||||
var map_view: MapView
|
||||
var marker: TileMarker
|
||||
@@ -75,13 +77,19 @@ func _ready() -> void:
|
||||
if _player_key.is_empty():
|
||||
push_warning("the player key cannot be read: %s" % error_string(store.last_read_error))
|
||||
clock = TickClock.new(GwSession.TICKS_PER_SECOND)
|
||||
art = Art.new()
|
||||
art.load_overrides(Art.OVERRIDE_DIR)
|
||||
map_view = MapView.new()
|
||||
map_view.art = art
|
||||
add_child(map_view)
|
||||
marker = TileMarker.new()
|
||||
marker.art = art
|
||||
add_child(marker)
|
||||
figures = Figures.new()
|
||||
figures.art = art
|
||||
add_child(figures)
|
||||
avatar = Avatar.new()
|
||||
avatar.art = art
|
||||
add_child(avatar)
|
||||
var camera := Camera2D.new()
|
||||
camera.zoom = CAMERA_ZOOM
|
||||
@@ -200,8 +208,9 @@ func _show_world() -> void:
|
||||
for node: Node2D in [avatar, marker, figures]:
|
||||
node.show()
|
||||
map_view.session = session
|
||||
map_view.queue_redraw()
|
||||
map_view.reload()
|
||||
figures.session = session
|
||||
avatar.player = session.local_player()
|
||||
figures.clear()
|
||||
avatar.is_walkable = session.is_walkable
|
||||
avatar.position = GridMotion.tile_center(session.spawn_point())
|
||||
@@ -230,7 +239,7 @@ func _leave(message: String) -> void:
|
||||
link = null
|
||||
session = null
|
||||
map_view.session = null
|
||||
map_view.queue_redraw()
|
||||
map_view.reload()
|
||||
avatar.is_walkable = Callable()
|
||||
figures.clear()
|
||||
_show_menu(message)
|
||||
@@ -295,7 +304,7 @@ func _on_notice(notice: Dictionary) -> void:
|
||||
if notice["resync"]:
|
||||
resyncs += 1
|
||||
_keep_diverged_recording()
|
||||
map_view.queue_redraw()
|
||||
map_view.reload()
|
||||
hud.show_stone(session.item_count(GwSession.ITEM_STONE))
|
||||
hud.flash("RESYNCED")
|
||||
else:
|
||||
@@ -390,8 +399,16 @@ func _apply(events: Array[Dictionary]) -> void:
|
||||
var local := session.local_player()
|
||||
for event in events:
|
||||
match event["type"]:
|
||||
"tile_hit", "tile_changed":
|
||||
map_view.queue_redraw()
|
||||
"tile_hit":
|
||||
var tile := Vector2i(event["x"], event["y"])
|
||||
map_view.refresh_tile(tile)
|
||||
# The own hits showed their burst when they were made.
|
||||
if event["player"] != local:
|
||||
map_view.hit(tile)
|
||||
"tile_changed":
|
||||
var tile := Vector2i(event["x"], event["y"])
|
||||
map_view.refresh_tile(tile)
|
||||
map_view.hit(tile)
|
||||
"item_gained":
|
||||
if event["player"] == local:
|
||||
hud.show_stone(session.item_count(GwSession.ITEM_STONE))
|
||||
@@ -401,6 +418,9 @@ func _apply(events: Array[Dictionary]) -> void:
|
||||
"player_joined":
|
||||
if event["player"] != local:
|
||||
hud.flash("PLAYER_JOINED", [session.player_name(event["player"])])
|
||||
else:
|
||||
avatar.player = local
|
||||
avatar.queue_redraw()
|
||||
_update_status()
|
||||
"player_left":
|
||||
figures.forget(event["player"])
|
||||
@@ -422,7 +442,9 @@ func _try_dig() -> void:
|
||||
dig_at(marker.tile)
|
||||
|
||||
|
||||
## Queues a pickaxe hit on `tile`, at most once per cooldown. The caller must
|
||||
## Queues a pickaxe hit on `tile`, at most once per cooldown, and shows its
|
||||
## burst of chips at once: the hit itself lands with a later tick, which for a
|
||||
## guest comes back from the host only after the round trip. The caller must
|
||||
## check the reach: figures move outside the lockstep, so the core does not
|
||||
## know where the figure is. The core checks the cooldown, the tile, its
|
||||
## terrain, the tool tier and the free space in the inventory.
|
||||
@@ -431,6 +453,8 @@ func dig_at(tile: Vector2i) -> void:
|
||||
return
|
||||
session.dig(tile.x, tile.y)
|
||||
_next_dig_tick = session.tick() + GwSession.DIG_COOLDOWN_TICKS
|
||||
if session.terrain_at(tile.x, tile.y) != GwSession.TERRAIN_GROUND:
|
||||
map_view.hit(tile)
|
||||
|
||||
|
||||
## Writes the running world as the save. Returns false if nothing was
|
||||
|
||||
+167
-33
@@ -1,48 +1,182 @@
|
||||
class_name MapView
|
||||
extends Node2D
|
||||
## Draws the terrain of the shared world. The colours are placeholders until
|
||||
## a style is chosen from the samples of plan M1d.
|
||||
## 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.
|
||||
|
||||
const CRACK_COLOR := Color(0.1, 0.08, 0.06, 0.8)
|
||||
## 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()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
## 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 width := session.map_width()
|
||||
var height := session.map_height()
|
||||
var terrain := session.terrain_codes()
|
||||
var damage := session.damage_codes()
|
||||
var size := GridMotion.TILE_SIZE
|
||||
for y in height:
|
||||
for x in width:
|
||||
var index := y * width + x
|
||||
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)
|
||||
draw_rect(rect, color_of(terrain[index]))
|
||||
if damage[index] > 0:
|
||||
_draw_cracks(rect, damage[index])
|
||||
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})
|
||||
|
||||
|
||||
## The placeholder colour of a terrain code.
|
||||
static func color_of(terrain: int) -> Color:
|
||||
if terrain == GwSession.TERRAIN_GROUND:
|
||||
return Color(0.42, 0.36, 0.28)
|
||||
if terrain == GwSession.TERRAIN_SOFT_ROCK:
|
||||
return Color(0.56, 0.55, 0.52)
|
||||
if terrain == GwSession.TERRAIN_HARD_ROCK:
|
||||
return Color(0.33, 0.33, 0.37)
|
||||
if terrain == GwSession.TERRAIN_BEDROCK:
|
||||
return Color(0.12, 0.12, 0.14)
|
||||
return Color.MAGENTA
|
||||
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()
|
||||
|
||||
|
||||
## One crack line for every hit the tile has taken.
|
||||
func _draw_cracks(rect: Rect2, hits: int) -> void:
|
||||
var inset := rect.grow(-3.0)
|
||||
for line in hits:
|
||||
var share := (line + 1.0) / (hits + 1.0)
|
||||
var from := Vector2(inset.position.x, inset.position.y + inset.size.y * share)
|
||||
var to := Vector2(inset.end.x, inset.position.y + inset.size.y * (1.0 - share))
|
||||
draw_line(from, to, CRACK_COLOR, 1.0)
|
||||
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)})
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
class_name TileMarker
|
||||
extends Node2D
|
||||
## Outlines the tile under the mouse: green when the figure can reach it,
|
||||
## red when it is too far away.
|
||||
|
||||
const REACHABLE_COLOR := Color(0.4, 1.0, 0.4)
|
||||
const UNREACHABLE_COLOR := Color(1.0, 0.4, 0.4)
|
||||
## Marks the tile under the mouse, drawn by `art`: one way when the figure
|
||||
## can reach it, another when it is too far away.
|
||||
|
||||
var tile := Vector2i(-1, -1)
|
||||
var in_reach := false
|
||||
var art: Art
|
||||
|
||||
|
||||
## Moves the outline to `new_tile`. Redraws only when something changed.
|
||||
## Moves the mark to `new_tile`. Redraws only when something changed.
|
||||
func show_tile(new_tile: Vector2i, reachable: bool) -> void:
|
||||
if new_tile == tile and reachable == in_reach:
|
||||
return
|
||||
@@ -20,6 +18,7 @@ func show_tile(new_tile: Vector2i, reachable: bool) -> void:
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if art == null:
|
||||
return
|
||||
var size := GridMotion.TILE_SIZE
|
||||
var color := REACHABLE_COLOR if in_reach else UNREACHABLE_COLOR
|
||||
draw_rect(Rect2(Vector2(tile) * size, Vector2(size, size)), color, false, 1.0)
|
||||
art.draw(self, Art.MARKER, Rect2(Vector2(tile) * size, Vector2(size, size)), {"reachable": in_reach})
|
||||
|
||||
@@ -31,3 +31,26 @@ func test_without_a_map_the_figure_stays() -> void:
|
||||
avatar.walk(Vector2.LEFT, 0.1)
|
||||
check_eq(avatar.position, Vector2.ZERO, "no walkability callable")
|
||||
avatar.free()
|
||||
|
||||
|
||||
func test_a_walking_figure_steps_and_faces_the_way_it_goes() -> void:
|
||||
var avatar := Avatar.new()
|
||||
avatar.is_walkable = _open_field
|
||||
avatar.position = GridMotion.tile_center(Vector2i(50, 50))
|
||||
check(not avatar.moving, "standing at first")
|
||||
avatar.walk(Vector2.LEFT, 0.1)
|
||||
check(avatar.moving, "walking")
|
||||
check_eq(avatar.facing, -1.0, "facing left")
|
||||
check(avatar.step > 0.0, "the step turns")
|
||||
avatar.walk(Vector2.UP, 0.1)
|
||||
check_eq(avatar.facing, -1.0, "walking up keeps the way it faces")
|
||||
avatar.walk(Vector2.ZERO, 0.1)
|
||||
check(not avatar.moving, "standing again")
|
||||
avatar.free()
|
||||
|
||||
|
||||
func test_a_figure_stands_upright_in_its_tile() -> void:
|
||||
var rect := Avatar.figure_rect(Vector2(100, 100))
|
||||
check_eq(rect.size, Vector2(GridMotion.TILE_SIZE, GridMotion.TILE_SIZE * 1.5), "a tile wide, one and a half tall")
|
||||
check_eq(rect.get_center().x, 100.0, "centred on the figure")
|
||||
check_eq(rect.end.y, 100.0 + Avatar.RADIUS + 1.0, "its feet just below its body")
|
||||
|
||||
@@ -32,3 +32,13 @@ func test_figures_can_be_forgotten() -> void:
|
||||
figures.clear()
|
||||
check_eq(figures.shown_at(3), Vector2.INF, "all are gone")
|
||||
figures.free()
|
||||
|
||||
|
||||
func test_a_figure_faces_the_way_it_glides() -> void:
|
||||
var figures := Figures.new()
|
||||
figures.place(1, Vector2(100.0, 0.0))
|
||||
check_eq(figures.facing_of(1), 1.0, "facing right at first")
|
||||
figures.place(1, Vector2.ZERO)
|
||||
figures._process(0.02)
|
||||
check_eq(figures.facing_of(1), -1.0, "facing left while it glides to the left")
|
||||
figures.free()
|
||||
|
||||
@@ -381,3 +381,14 @@ func test_a_mistyped_address_is_reported() -> void:
|
||||
check(main.session == null, "nothing starts")
|
||||
check_eq(main.menu.status(), tr("ADDRESS_INVALID") % "host:port", "the menu says why")
|
||||
_clean_up([directory])
|
||||
|
||||
|
||||
func test_an_own_hit_shows_its_burst_at_once() -> void:
|
||||
var directory := TestFiles.temporary_dir("burst")
|
||||
var main := _start_main(directory)
|
||||
main.play(false)
|
||||
var rock: Vector2i = main.session.spawn_point() + Vector2i(7, -2)
|
||||
check_eq(main.session.terrain_at(rock.x, rock.y), GwSession.TERRAIN_SOFT_ROCK, "rock beside the clearing")
|
||||
main.dig_at(rock)
|
||||
check_eq(main.map_view.burst_count(), 1, "the burst shows before the hit lands in a tick")
|
||||
_clean_up([directory])
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
extends TestCase
|
||||
## The map view over a real world of the core.
|
||||
|
||||
var key := PackedByteArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])
|
||||
|
||||
|
||||
func _session() -> GwSession:
|
||||
var session := GwSession.new()
|
||||
session.start_new(7)
|
||||
session.join(key, "Ada")
|
||||
session.step()
|
||||
return session
|
||||
|
||||
|
||||
func _view(session: GwSession) -> MapView:
|
||||
var view := MapView.new()
|
||||
view.art = Art.new()
|
||||
view.session = session
|
||||
tree.root.add_child(view)
|
||||
view.reload()
|
||||
return view
|
||||
|
||||
|
||||
func _free(view: MapView) -> void:
|
||||
tree.root.remove_child(view)
|
||||
view.free()
|
||||
|
||||
|
||||
## The first tile of the soft rock beside the spawn clearing.
|
||||
func _rock(session: GwSession) -> Vector2i:
|
||||
return session.spawn_point() + Vector2i(7, -2)
|
||||
|
||||
|
||||
func test_the_map_is_drawn_in_blocks_that_cover_it() -> void:
|
||||
var session := _session()
|
||||
var view := _view(session)
|
||||
var across := ceili(session.map_width() / float(MapView.BLOCK))
|
||||
var down := ceili(session.map_height() / float(MapView.BLOCK))
|
||||
check_eq(view.block_count(), across * down, "blocks over the whole map")
|
||||
for style in Art.style_keys():
|
||||
view.art.select(style)
|
||||
view.redraw()
|
||||
await tree.process_frame
|
||||
_free(view)
|
||||
|
||||
|
||||
func test_each_tile_is_drawn_to_fit_the_rock_around_it() -> void:
|
||||
var session := _session()
|
||||
var view := _view(session)
|
||||
var solid := TileShape.flags(session.terrain_codes(), MapView.SOLID)
|
||||
for tile: Vector2i in [Vector2i(0, 0), session.spawn_point(), _rock(session)]:
|
||||
var expected := TileShape.mask(solid, session.map_width(), session.map_height(), tile, true)
|
||||
check_eq(view.mask_at(tile), expected, "the neighbours of %s" % tile)
|
||||
_free(view)
|
||||
|
||||
|
||||
func test_a_broken_rock_no_longer_shapes_its_neighbours() -> void:
|
||||
var session := _session()
|
||||
var view := _view(session)
|
||||
var rock := _rock(session)
|
||||
var west := rock + Vector2i(-1, 0)
|
||||
check(view.mask_at(west) & TileShape.E != 0, "the rock lies east of its neighbour")
|
||||
for hit in 3:
|
||||
session.dig(rock.x, rock.y)
|
||||
session.step()
|
||||
for wait in GwSession.DIG_COOLDOWN_TICKS:
|
||||
session.step()
|
||||
check_eq(session.terrain_at(rock.x, rock.y), GwSession.TERRAIN_GROUND, "three hits break it")
|
||||
view.refresh_tile(rock)
|
||||
check(view.mask_at(west) & TileShape.E == 0, "now there is ground east of the neighbour")
|
||||
await tree.process_frame
|
||||
_free(view)
|
||||
|
||||
|
||||
func test_a_burst_of_chips_shows_for_a_moment() -> void:
|
||||
var view := _view(_session())
|
||||
view.hit(Vector2i(3, 3))
|
||||
check_eq(view.burst_count(), 1, "a burst")
|
||||
view._process(MapView.HIT_SECONDS * 0.5)
|
||||
check_eq(view.burst_count(), 1, "still in the air")
|
||||
await tree.process_frame
|
||||
view._process(MapView.HIT_SECONDS * 0.6)
|
||||
check_eq(view.burst_count(), 0, "gone after its time")
|
||||
_free(view)
|
||||
|
||||
|
||||
func test_without_a_world_nothing_is_drawn() -> void:
|
||||
var view := _view(_session())
|
||||
view.session = null
|
||||
view.reload()
|
||||
check_eq(view.block_count(), 0, "no blocks")
|
||||
view.refresh_tile(Vector2i(1, 1))
|
||||
await tree.process_frame
|
||||
_free(view)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bp0vlwfwtlh7h
|
||||
Reference in New Issue
Block a user