Spielfeld über Art zeichnen
Karte, Figuren und Zielmarke zeichnet jetzt Art. Die Karte zeichnet in Blöcken von 16 mal 16 Kacheln und nach einem Treffer nur die Blöcke um die Kachel neu; jede Kachel kennt ihre Nachbarn. Figuren stehen aufrecht in ihrer Kachel, schreiten und schauen in die Richtung, in die sie gehen. Der eigene Schlag zeigt seine Splitter sofort, auch als Gast, und die Namen über fremden Figuren werden in Bildschirmgröße gezeichnet und bleiben scharf. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
+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