diff --git a/.markdownlint.yaml b/.markdownlint.yaml index fc185c6c385..40033f22b2e 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -57,7 +57,7 @@ MD011: true # MD012/no-multiple-blanks - Multiple consecutive blank lines MD012: -# Consecutive blank lines + # Consecutive blank lines maximum: 2 # MD013/line-length - Line length @@ -235,6 +235,4 @@ MD052: true # MD053/link-image-reference-definitions - Link and image reference definitions should be needed MD053: # Ignored definitions - ignored_definitions: [ - "//" - ] + ignored_definitions: ["//"] diff --git a/examples/assets/images/0x72_DungeonTilesetII_v1.4.png b/examples/assets/images/0x72_DungeonTilesetII_v1.4.png new file mode 100644 index 00000000000..0304491e02f Binary files /dev/null and b/examples/assets/images/0x72_DungeonTilesetII_v1.4.png differ diff --git a/examples/assets/tiles/0x72_DungeonTilesetII_v1.4.tsx b/examples/assets/tiles/0x72_DungeonTilesetII_v1.4.tsx new file mode 100644 index 00000000000..3a0156ac274 --- /dev/null +++ b/examples/assets/tiles/0x72_DungeonTilesetII_v1.4.tsx @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/assets/tiles/dungeon.tmx b/examples/assets/tiles/dungeon.tmx new file mode 100644 index 00000000000..69bf487887e --- /dev/null +++ b/examples/assets/tiles/dungeon.tmx @@ -0,0 +1,19 @@ + + + + + + H4sIAAAAAAAACmNgoAw0Y6FHMfkYAK7z31GQAQAA + + + + + H4sIAAAAAAAACp3Q7QmDMBSF4ZuELFIVF9FNdBQXae0i4scegnYNXzDgKfSH9MJDksPh/og3My+i0Dw4s4ddiqTVjI6HTp10krkfPZ3xZu/uvn97Zeo+OV/o8XbfvQzBTgP3GQsmyaP8XYOV944PNuRkFTIcCfCt35ABAAA= + + + + + H4sIAAAAAAAACmNgoA0oYaKNuamMuOW2IqmBYRgwQ2JfxWHeFKCbw/CYTw8AANEJHwqQAQAA + + + diff --git a/examples/lib/main.dart b/examples/lib/main.dart index 340f41832b9..baceed4dac1 100644 --- a/examples/lib/main.dart +++ b/examples/lib/main.dart @@ -14,6 +14,7 @@ import 'package:examples/stories/rendering/rendering.dart'; import 'package:examples/stories/sprites/sprites.dart'; import 'package:examples/stories/svg/svg.dart'; import 'package:examples/stories/system/system.dart'; +import 'package:examples/stories/tiled/tiled.dart'; import 'package:examples/stories/utils/utils.dart'; import 'package:examples/stories/widgets/widgets.dart'; import 'package:flutter/material.dart'; @@ -38,6 +39,7 @@ void main() { addInputStories(dashbook); addParallaxStories(dashbook); addRenderingStories(dashbook); + addTiledStories(dashbook); addSpritesStories(dashbook); addSvgStories(dashbook); addSystemStories(dashbook); diff --git a/examples/lib/stories/tiled/flame_tiled_animation_example.dart b/examples/lib/stories/tiled/flame_tiled_animation_example.dart new file mode 100644 index 00000000000..31c95d29a33 --- /dev/null +++ b/examples/lib/stories/tiled/flame_tiled_animation_example.dart @@ -0,0 +1,16 @@ +import 'package:flame/game.dart'; +import 'package:flame_tiled/flame_tiled.dart'; + +class FlameTiledAnimationExample extends FlameGame { + static const String description = ''' + Loads and displays an animated Tiled map. + '''; + + late final TiledComponent map; + + @override + Future onLoad() async { + map = await TiledComponent.load('dungeon.tmx', Vector2.all(32)); + add(map); + } +} diff --git a/examples/lib/stories/tiled/tiled.dart b/examples/lib/stories/tiled/tiled.dart new file mode 100644 index 00000000000..5152a6a96b6 --- /dev/null +++ b/examples/lib/stories/tiled/tiled.dart @@ -0,0 +1,14 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/tiled/flame_tiled_animation_example.dart'; + +import 'package:flame/game.dart'; + +void addTiledStories(Dashbook dashbook) { + dashbook.storiesOf('Tiled').add( + 'Flame Tiled Animation', + (_) => GameWidget(game: FlameTiledAnimationExample()), + codeLink: baseLink('effects/flame_tiled_animation_example.dart'), + info: FlameTiledAnimationExample.description, + ); +} diff --git a/examples/pubspec.yaml b/examples/pubspec.yaml index cb374015f66..ba747261c7c 100644 --- a/examples/pubspec.yaml +++ b/examples/pubspec.yaml @@ -1,7 +1,7 @@ name: examples description: A set of small examples showcasing each feature provided by the Flame Engine. homepage: https://github.com/flame-engine/flame/tree/main/examples -publish_to: 'none' +publish_to: "none" version: 0.1.0 @@ -15,6 +15,7 @@ dependencies: flame_audio: ^1.3.1 flame_forge2d: ^0.12.2 flame_svg: ^1.5.0 + flame_tiled: ^1.7.2 flutter: sdk: flutter google_fonts: ^2.3.2 @@ -35,6 +36,8 @@ flutter: - assets/images/tile_maps/ - assets/images/layers/ - assets/images/parallax/ + - assets/images/parallax/ - assets/svgs/ + - assets/tiles/ - assets/audio/music/ - assets/audio/sfx/ diff --git a/packages/flame_tiled/lib/src/mutable_rect.dart b/packages/flame_tiled/lib/src/mutable_rect.dart new file mode 100644 index 00000000000..10a6e6e4fa8 --- /dev/null +++ b/packages/flame_tiled/lib/src/mutable_rect.dart @@ -0,0 +1,39 @@ +import 'dart:ui' show Rect; + +/// A mutable version of [Rect] for tile map animations. +class MutableRect extends Rect { + /// Construct a rectangle from its left, top, right, and bottom edges. + MutableRect.fromLTRB(this.left, this.top, this.right, this.bottom) + : super.fromLTRB(left, top, right, bottom); + + /// Create a new instance from [other]. + factory MutableRect.fromRect(Rect other) => + MutableRect.fromLTRB(other.left, other.top, other.right, other.bottom); + + /// The offset of the left edge of this rectangle from the x axis. + @override + double left; + + /// The offset of the top edge of this rectangle from the y axis. + @override + double top; + + /// The offset of the right edge of this rectangle from the x axis. + @override + double right; + + /// The offset of the bottom edge of this rectangle from the y axis. + @override + double bottom; + + /// Update with [other]'s dimensions. + void copy(Rect other) { + left = other.left; + top = other.top; + right = other.right; + bottom = other.bottom; + } + + /// Convert to immutable rectangle. + Rect toRect() => Rect.fromLTRB(left, top, right, bottom); +} diff --git a/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart index 123fa2660c7..118a6006c93 100644 --- a/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart +++ b/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart @@ -39,4 +39,11 @@ class GroupLayer extends RenderableLayer { child.render(canvas, camera); } } + + @override + void update(double dt) { + for (final child in children) { + child.update(dt); + } + } } diff --git a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart index 29b7b176ea5..01f3d4138e4 100644 --- a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart +++ b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart @@ -80,4 +80,7 @@ class ImageLayer extends RenderableLayer { @override void refreshCache() {} + + @override + void update(double dt) {} } diff --git a/packages/flame_tiled/lib/src/renderable_layers/object_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/object_layer.dart index b5d89c651ee..f0f2ab2f808 100644 --- a/packages/flame_tiled/lib/src/renderable_layers/object_layer.dart +++ b/packages/flame_tiled/lib/src/renderable_layers/object_layer.dart @@ -35,4 +35,7 @@ class ObjectLayer extends RenderableLayer { @override void refreshCache() {} + + @override + void update(double dt) {} } diff --git a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart index b1dc196e434..8f49dde900e 100644 --- a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart +++ b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart @@ -28,6 +28,8 @@ abstract class RenderableLayer { void refreshCache(); + void update(double dt); + double get scaleX => destTileSize.x / map.tileWidth; double get scaleY => destTileSize.y / map.tileHeight; @@ -85,4 +87,7 @@ class UnsupportedLayer extends RenderableLayer { @override void refreshCache() {} + + @override + void update(double dt) {} } diff --git a/packages/flame_tiled/lib/src/renderable_layers/tile_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/tile_layer.dart index 4d781751d17..e49eb8b63ef 100644 --- a/packages/flame_tiled/lib/src/renderable_layers/tile_layer.dart +++ b/packages/flame_tiled/lib/src/renderable_layers/tile_layer.dart @@ -2,19 +2,24 @@ import 'package:flame/extensions.dart'; import 'package:flame/game.dart'; import 'package:flame/sprite.dart'; import 'package:flame_tiled/flame_tiled.dart'; +import 'package:flame_tiled/src/mutable_rect.dart'; import 'package:flame_tiled/src/mutable_transform.dart'; import 'package:flame_tiled/src/renderable_layers/group_layer.dart'; import 'package:flame_tiled/src/renderable_layers/renderable_layer.dart'; +import 'package:flame_tiled/src/tile_animation.dart'; import 'package:flame_tiled/src/tile_transform.dart'; import 'package:flutter/painting.dart'; import 'package:meta/meta.dart'; import 'package:tiled/tiled.dart' as tiled; +import 'package:tiled/tiled.dart'; @internal class TileLayer extends RenderableLayer { late final _layerPaint = Paint(); late final Map _cachedSpriteBatches; late List> indexes; + final animations = []; + final Map animationFrames; TileLayer( super.layer, @@ -22,12 +27,14 @@ class TileLayer extends RenderableLayer { super.map, super.destTileSize, this._cachedSpriteBatches, + this.animationFrames, ) { _layerPaint.color = Color.fromRGBO(255, 255, 255, opacity); } @override void refreshCache() { + animations.clear(); indexes = List.generate( layer.width, (index) => List.filled(layer.height, null), @@ -36,6 +43,13 @@ class TileLayer extends RenderableLayer { _cacheLayerTiles(); } + @override + void update(double dt) { + for (final animation in animations) { + animation.update(dt); + } + } + void _cacheLayerTiles() { for (final batch in _cachedSpriteBatches.values) { batch.clear(); @@ -65,6 +79,7 @@ class TileLayer extends RenderableLayer { final tileData = layer.tileData!; final batchMap = _cachedSpriteBatches; final size = destTileSize; + final halfMapTile = Vector2(map.tileWidth / 2, map.tileHeight / 2); for (var ty = 0; ty < tileData.length; ty++) { final tileRow = tileData[ty]; @@ -88,11 +103,12 @@ class TileLayer extends RenderableLayer { continue; } - final src = tileset.computeDrawRect(tile).toRect(); + final src = + MutableRect.fromRect(tileset.computeDrawRect(tile).toRect()); final flips = SimpleFlips.fromFlips(tileGid.flips); final scale = size.x / src.width; - final anchorX = src.width / 2; - final anchorY = src.height / 2; + final anchorX = src.width - halfMapTile.x; + final anchorY = src.height - halfMapTile.y; late double offsetX; late double offsetY; @@ -116,6 +132,10 @@ class TileLayer extends RenderableLayer { transform: indexes[tx][ty], flip: flips.flip, ); + + if (tile.animation.isNotEmpty) { + _addAnimation(tile, tileset, src); + } } } } @@ -125,7 +145,9 @@ class TileLayer extends RenderableLayer { final batchMap = _cachedSpriteBatches; final halfDestinationTile = destTileSize / 2; final size = destTileSize; - final isometricXShift = map.width * size.x * 0.5; + final isometricXShift = map.height * halfDestinationTile.x; + final isometricYShift = halfDestinationTile.y; + final halfMapTile = Vector2(map.tileWidth / 2, map.tileHeight / 2); for (var ty = 0; ty < tileData.length; ty++) { final tileRow = tileData[ty]; @@ -148,17 +170,18 @@ class TileLayer extends RenderableLayer { continue; } - final src = tileset.computeDrawRect(tile).toRect(); + final src = + MutableRect.fromRect(tileset.computeDrawRect(tile).toRect()); final flips = SimpleFlips.fromFlips(tileGid.flips); final scale = size.x / src.width; - final anchorX = src.width / 2; - final anchorY = src.height / 2; + final anchorX = src.width - halfMapTile.x; + final anchorY = src.height - halfMapTile.y; late double offsetX; late double offsetY; offsetX = halfDestinationTile.x * (tx - ty) + isometricXShift; - offsetY = halfDestinationTile.y * (tx + ty) - size.y; + offsetY = halfDestinationTile.y * (tx + ty) + isometricYShift; final scos = flips.cos * scale; final ssin = flips.sin * scale; @@ -177,6 +200,10 @@ class TileLayer extends RenderableLayer { transform: indexes[tx][ty], flip: flips.flip, ); + + if (tile.animation.isNotEmpty) { + _addAnimation(tile, tileset, src); + } } } } @@ -190,11 +217,11 @@ class TileLayer extends RenderableLayer { var staggerY = 0.0; var staggerX = 0.0; - // Hexagonal Ponity Tiles move down by a fractional amount. + // Isometric staggered tiles move down by a fractional amount. if (map.staggerAxis == tiled.StaggerAxis.y) { staggerY = size.y * 0.5; } else - // Hexagonal Flat Tiles move right by a fractional amount. + // Isometric staggered tiles move right by a fractional amount. if (map.staggerAxis == tiled.StaggerAxis.x) { staggerX = size.x * 0.5; } @@ -202,7 +229,7 @@ class TileLayer extends RenderableLayer { for (var ty = 0; ty < tileData.length; ty++) { final tileRow = tileData[ty]; - // Hexagonal Pointy Tiles shift left and right depending on the row + // Isometric staggered tiles shift left and right depending on the row if (map.staggerAxis == tiled.StaggerAxis.y) { if ((ty.isOdd && map.staggerIndex == tiled.StaggerIndex.odd) || (ty.isEven && map.staggerIndex == tiled.StaggerIndex.even)) { @@ -245,7 +272,8 @@ class TileLayer extends RenderableLayer { } } - final src = tileset.computeDrawRect(tile).toRect(); + final src = + MutableRect.fromRect(tileset.computeDrawRect(tile).toRect()); final flips = SimpleFlips.fromFlips(tileGid.flips); final scale = size.x / src.width; final anchorX = src.width - halfMapTile.x; @@ -291,6 +319,9 @@ class TileLayer extends RenderableLayer { flip: flips.flip, ); } + if (tile.animation.isNotEmpty) { + _addAnimation(tile, tileset, src); + } } for (final tile in xSecondPass) { @@ -367,7 +398,8 @@ class TileLayer extends RenderableLayer { } } - final src = tileset.computeDrawRect(tile).toRect(); + final src = + MutableRect.fromRect(tileset.computeDrawRect(tile).toRect()); final flips = SimpleFlips.fromFlips(tileGid.flips); final scale = size.x / src.width; final anchorX = src.width - halfMapTile.x; @@ -412,6 +444,9 @@ class TileLayer extends RenderableLayer { flip: flips.flip, ); } + if (tile.animation.isNotEmpty) { + _addAnimation(tile, tileset, src); + } } for (final tile in xSecondPass) { @@ -446,6 +481,7 @@ class TileLayer extends RenderableLayer { GroupLayer? parent, tiled.TiledMap map, Vector2 destTileSize, + Map animationFrames, ) async { return TileLayer( layer, @@ -453,6 +489,7 @@ class TileLayer extends RenderableLayer { map, destTileSize, await _loadImages(map), + animationFrames, ); } @@ -473,4 +510,19 @@ class TileLayer extends RenderableLayer { @override void handleResize(Vector2 canvasSize) {} + + void _addAnimation(Tile tile, Tileset tileset, MutableRect source) { + final frames = animationFrames[tile] ??= () { + final rects = []; + final durations = []; + for (final frame in tile.animation) { + final newTile = tileset.tiles[frame.tileId]; + final rect = tileset.computeDrawRect(newTile).toRect(); + rects.add(rect); + durations.add(frame.duration / 1000); + } + return TileFrames(rects, durations); + }(); + animations.add(TileAnimation(source, frames)); + } } diff --git a/packages/flame_tiled/lib/src/renderable_tile_map.dart b/packages/flame_tiled/lib/src/renderable_tile_map.dart index 5187383fae5..f6ba9532242 100644 --- a/packages/flame_tiled/lib/src/renderable_tile_map.dart +++ b/packages/flame_tiled/lib/src/renderable_tile_map.dart @@ -11,6 +11,7 @@ import 'package:flame_tiled/src/renderable_layers/image_layer.dart'; import 'package:flame_tiled/src/renderable_layers/object_layer.dart'; import 'package:flame_tiled/src/renderable_layers/renderable_layer.dart'; import 'package:flame_tiled/src/renderable_layers/tile_layer.dart'; +import 'package:flame_tiled/src/tile_animation.dart'; import 'package:flame_tiled/src/tile_stack.dart'; import 'package:flutter/painting.dart'; import 'package:tiled/tiled.dart' as tiled; @@ -50,12 +51,15 @@ class RenderableTiledMap { /// Paint for the map's background color, if there is one late final ui.Paint? _backgroundPaint; + final Map animationFrames; + /// {@macro _renderable_tiled_map} RenderableTiledMap( this.map, this.renderableLayers, this.destTileSize, { this.camera, + this.animationFrames = const {}, }) { _refreshCache(); @@ -216,14 +220,26 @@ class RenderableTiledMap { Vector2 destTileSize, { Camera? camera, }) async { - final renderableLayers = - await _renderableLayers(map.layers, null, map, destTileSize, camera); + // We're not going to load animation frames that are never referenced; but + // we do supply the common cache for all layers in this map, and maintain + // the update cycle for these in one place. + final animationFrames = {}; + + final renderableLayers = await _renderableLayers( + map.layers, + null, + map, + destTileSize, + camera, + animationFrames, + ); return RenderableTiledMap( map, renderableLayers, destTileSize, camera: camera, + animationFrames: animationFrames, ); } @@ -233,6 +249,7 @@ class RenderableTiledMap { tiled.TiledMap map, Vector2 destTileSize, Camera? camera, + Map animationFrames, ) async { return Future.wait( layers.where((layer) => layer.visible).toList().map((layer) async { @@ -243,6 +260,7 @@ class RenderableTiledMap { parent, map, destTileSize, + animationFrames, ); case tiled.ImageLayer: return ImageLayer.load( @@ -267,6 +285,7 @@ class RenderableTiledMap { map, destTileSize, camera, + animationFrames, ); renderableGroup.children = await children; return renderableGroup; @@ -323,6 +342,18 @@ class RenderableTiledMap { return null; } } + + void update(double dt) { + // First, update animation frames. + for (final frame in animationFrames.values) { + frame.update(dt); + } + + // Then every layer. + for (final layer in renderableLayers) { + layer.update(dt); + } + } } Color? _parseTiledColor(String? tiledColor) { diff --git a/packages/flame_tiled/lib/src/tile_animation.dart b/packages/flame_tiled/lib/src/tile_animation.dart new file mode 100644 index 00000000000..b129caf3375 --- /dev/null +++ b/packages/flame_tiled/lib/src/tile_animation.dart @@ -0,0 +1,60 @@ +import 'dart:ui' show Rect; + +import 'package:flame_tiled/src/mutable_rect.dart'; + +/// Records a single animation for tile on a layer. +/// +/// This works because SpriteBatch holds a list of [Rect]. Those rectangles +/// are usually immutable, but flame_tile uses a mutable rectangle to update +/// the offsets in the image atlas. +class TileAnimation { + /// Frames of the animation loop. + final TileFrames frames; + + /// Rectangle that gets updated for each new frame in the animation. + final MutableRect batchedSource; + + /// Current frame counter. + int frame = 0; + + TileAnimation( + this.batchedSource, + this.frames, + ); + + void update(double dt) { + if (frame != frames.frame) { + frame = frames.frame; + batchedSource.copy(frames.sources[frame]); + } + } +} + +/// Records the list of frames for a tile so that it can be reused. +class TileFrames { + /// Rectangles for each frame in the animation. + final List sources; + + /// Duration, in seconds, for each frame in the animation. + final List durations; + + /// Current frame lifetime. + double frameTime = 0.0; + + /// Current frame counter for all frames sharing this animation. + int frame = 0; + + TileFrames(this.sources, this.durations); + + void update(double dt) { + frameTime += dt; + + // Track really long jank by skipping ahead. + while (durations[frame] <= frameTime) { + final currentFrameTime = durations[frame]; + frame = (frame + 1) % durations.length; + // We still have time to add to this, even if we're late. + frameTime = frameTime - currentFrameTime; + } + } +} diff --git a/packages/flame_tiled/lib/src/tiled_component.dart b/packages/flame_tiled/lib/src/tiled_component.dart index da54adeaefe..5d12a35cc7b 100644 --- a/packages/flame_tiled/lib/src/tiled_component.dart +++ b/packages/flame_tiled/lib/src/tiled_component.dart @@ -4,6 +4,7 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame_tiled/src/renderable_tile_map.dart'; +import 'package:meta/meta.dart'; import 'package:tiled/tiled.dart'; /// {@template _tiled_component} @@ -47,7 +48,17 @@ class TiledComponent extends PositionComponent super.anchor, super.children, super.priority, - }) : super(size: _computeSize(tileMap)); + }) : super( + size: computeSize( + tileMap.map.orientation, + tileMap.destTileSize, + tileMap.map.tileWidth, + tileMap.map.tileHeight, + tileMap.map.width, + tileMap.map.height, + tileMap.map.staggerAxis, + ), + ); @override Future? onLoad() async { @@ -56,6 +67,11 @@ class TiledComponent extends PositionComponent tileMap.camera ??= gameRef.camera; } + @override + void update(double dt) { + tileMap.update(dt); + } + @override void render(Canvas canvas) { tileMap.render(canvas); @@ -79,49 +95,59 @@ class TiledComponent extends PositionComponent ); } - static Vector2 _computeSize(RenderableTiledMap tileMap) { - final tMap = tileMap.map; - - final xScale = tileMap.destTileSize.x / tMap.tileWidth; - final yScale = tileMap.destTileSize.y / tMap.tileHeight; + @visibleForTesting + static Vector2 computeSize( + MapOrientation? orientation, + Vector2 destTileSize, + int tileWidth, + int tileHeight, + int mapWidth, + int mapHeight, + StaggerAxis? staggerAxis, + ) { + if (orientation == null) { + return NotifyingVector2.zero(); + } + final xScale = destTileSize.x / tileWidth; + final yScale = destTileSize.y / tileHeight; final tileScaled = Vector2( - tileMap.map.tileWidth * xScale, - tileMap.map.tileHeight * yScale, + tileWidth * xScale, + tileHeight * yScale, ); - if (tMap.orientation == null) { - return NotifyingVector2.zero(); - } - - switch (tMap.orientation!) { + switch (orientation) { case MapOrientation.staggered: - return tMap.staggerAxis == StaggerAxis.y + return staggerAxis == StaggerAxis.y ? Vector2( - tileScaled.x * tileMap.map.width + tileScaled.x / 2, - tileScaled.y + ((tileMap.map.height - 1) * tileScaled.y / 2), + tileScaled.x * mapWidth + tileScaled.x / 2, + (mapHeight + 1) * (tileScaled.y / 2), ) : Vector2( - tileScaled.x + ((tileMap.map.width - 1) * tileScaled.x / 2), - tileScaled.y * tileMap.map.height + tileScaled.y / 2, + (mapWidth + 1) * (tileScaled.x / 2), + tileScaled.y * mapHeight + tileScaled.y / 2, ); case MapOrientation.hexagonal: - return tMap.staggerAxis == StaggerAxis.y + return staggerAxis == StaggerAxis.y ? Vector2( - tileMap.map.width * tileScaled.x + tileScaled.x / 2, - tileScaled.y + ((tileMap.map.height - 1) * tileScaled.y * 0.75), + mapWidth * tileScaled.x + tileScaled.x / 2, + tileScaled.y + ((mapHeight - 1) * tileScaled.y * 0.75), ) : Vector2( - tileScaled.x + ((tileMap.map.width - 1) * tileScaled.x * 0.75), - (tileMap.map.height * tileScaled.y) + tileScaled.y / 2, + tileScaled.x + ((mapWidth - 1) * tileScaled.x * 0.75), + (mapHeight * tileScaled.y) + tileScaled.y / 2, ); case MapOrientation.isometric: + final halfTile = tileScaled / 2; + final dimensions = mapWidth + mapHeight; + return halfTile..scale(dimensions.toDouble()); + case MapOrientation.orthogonal: return Vector2( - tileMap.map.width * tileScaled.x, - tileMap.map.height * tileScaled.y, + mapWidth * tileScaled.x, + mapHeight * tileScaled.y, ); } } diff --git a/packages/flame_tiled/test/README.md b/packages/flame_tiled/test/README.md index aaf88b88b56..fc6e419b261 100644 --- a/packages/flame_tiled/test/README.md +++ b/packages/flame_tiled/test/README.md @@ -5,3 +5,8 @@ They were released as Public domain. - Tileset_Hexagonal_FlatTop_60x39_60x60.png - Tileset_Hexagonal_PointyTop_60x52_60x80.png + +The following assets were [downloaded here](https://0x72.itch.io/dungeontileset-ii). +They were released as [CC0 1.0 Universal (CC0 1.0) - Public Domain Dedication](https://creativecommons.org/publicdomain/zero/1.0/). + +- 0x72_DungeonTilesetII_v1.4.png diff --git a/packages/flame_tiled/test/assets/0x72_DungeonTilesetII_v1.4.png b/packages/flame_tiled/test/assets/0x72_DungeonTilesetII_v1.4.png new file mode 100644 index 00000000000..0304491e02f Binary files /dev/null and b/packages/flame_tiled/test/assets/0x72_DungeonTilesetII_v1.4.png differ diff --git a/packages/flame_tiled/test/assets/dungeon_animation_hexagonal.tmx b/packages/flame_tiled/test/assets/dungeon_animation_hexagonal.tmx new file mode 100644 index 00000000000..f2d85907b03 --- /dev/null +++ b/packages/flame_tiled/test/assets/dungeon_animation_hexagonal.tmx @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eJxTYmBgUAJiTSA+AsQACCABMg== + + + + + eJxLZWBgSGJkgAMACwMAyQ== + + + + + + eJwrYWJAAQAHbgB3 + + + + diff --git a/packages/flame_tiled/test/assets/dungeon_animation_isometric.png b/packages/flame_tiled/test/assets/dungeon_animation_isometric.png new file mode 100644 index 00000000000..d220467c4d0 Binary files /dev/null and b/packages/flame_tiled/test/assets/dungeon_animation_isometric.png differ diff --git a/packages/flame_tiled/test/assets/dungeon_animation_isometric.tmx b/packages/flame_tiled/test/assets/dungeon_animation_isometric.tmx new file mode 100644 index 00000000000..8503e5f6197 --- /dev/null +++ b/packages/flame_tiled/test/assets/dungeon_animation_isometric.tmx @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eJxTYmBgUAJiTSA+AsQACCABMg== + + + + + eJxLZWBgSGJkgAMACwMAyQ== + + + + + + eJwrYWJAAQAHbgB3 + + + + diff --git a/packages/flame_tiled/test/assets/dungeon_animation_orthogonal.tmx b/packages/flame_tiled/test/assets/dungeon_animation_orthogonal.tmx new file mode 100644 index 00000000000..dd6f83b0175 --- /dev/null +++ b/packages/flame_tiled/test/assets/dungeon_animation_orthogonal.tmx @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eJxTYmBgUAJiTSA+AsQACCABMg== + + + + + eJxLZWBgSGJkgAMACwMAyQ== + + + + + + eJwrYWJAAQAHbgB3 + + + + diff --git a/packages/flame_tiled/test/assets/dungeon_animation_staggered.tmx b/packages/flame_tiled/test/assets/dungeon_animation_staggered.tmx new file mode 100644 index 00000000000..97ecf02154a --- /dev/null +++ b/packages/flame_tiled/test/assets/dungeon_animation_staggered.tmx @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eJxTYmBgUAJiTSA+AsQACCABMg== + + + + + eJxLZWBgSGJkgAMACwMAyQ== + + + + + + eJwrYWJAAQAHbgB3 + + + + diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_hexagonal_0.png b/packages/flame_tiled/test/goldens/dungeon_animation_hexagonal_0.png new file mode 100644 index 00000000000..7d24d31bffa Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_hexagonal_0.png differ diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_hexagonal_1.png b/packages/flame_tiled/test/goldens/dungeon_animation_hexagonal_1.png new file mode 100644 index 00000000000..c21bf2b8526 Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_hexagonal_1.png differ diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_hexagonal_2.png b/packages/flame_tiled/test/goldens/dungeon_animation_hexagonal_2.png new file mode 100644 index 00000000000..faccabff66d Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_hexagonal_2.png differ diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_hexagonal_3.png b/packages/flame_tiled/test/goldens/dungeon_animation_hexagonal_3.png new file mode 100644 index 00000000000..e8b2eb37014 Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_hexagonal_3.png differ diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_isometric_0.png b/packages/flame_tiled/test/goldens/dungeon_animation_isometric_0.png new file mode 100644 index 00000000000..86e7cbaa2ce Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_isometric_0.png differ diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_isometric_1.png b/packages/flame_tiled/test/goldens/dungeon_animation_isometric_1.png new file mode 100644 index 00000000000..56c5253a465 Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_isometric_1.png differ diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_isometric_2.png b/packages/flame_tiled/test/goldens/dungeon_animation_isometric_2.png new file mode 100644 index 00000000000..229f71853ba Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_isometric_2.png differ diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_isometric_3.png b/packages/flame_tiled/test/goldens/dungeon_animation_isometric_3.png new file mode 100644 index 00000000000..a7a2e0175b7 Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_isometric_3.png differ diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_orthogonal_0.png b/packages/flame_tiled/test/goldens/dungeon_animation_orthogonal_0.png new file mode 100644 index 00000000000..b86b2652993 Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_orthogonal_0.png differ diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_orthogonal_1.png b/packages/flame_tiled/test/goldens/dungeon_animation_orthogonal_1.png new file mode 100644 index 00000000000..b5eab7dafce Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_orthogonal_1.png differ diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_orthogonal_2.png b/packages/flame_tiled/test/goldens/dungeon_animation_orthogonal_2.png new file mode 100644 index 00000000000..d02470cf34b Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_orthogonal_2.png differ diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_orthogonal_3.png b/packages/flame_tiled/test/goldens/dungeon_animation_orthogonal_3.png new file mode 100644 index 00000000000..bf675ae1216 Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_orthogonal_3.png differ diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_staggered_0.png b/packages/flame_tiled/test/goldens/dungeon_animation_staggered_0.png new file mode 100644 index 00000000000..7d24d31bffa Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_staggered_0.png differ diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_staggered_1.png b/packages/flame_tiled/test/goldens/dungeon_animation_staggered_1.png new file mode 100644 index 00000000000..c21bf2b8526 Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_staggered_1.png differ diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_staggered_2.png b/packages/flame_tiled/test/goldens/dungeon_animation_staggered_2.png new file mode 100644 index 00000000000..faccabff66d Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_staggered_2.png differ diff --git a/packages/flame_tiled/test/goldens/dungeon_animation_staggered_3.png b/packages/flame_tiled/test/goldens/dungeon_animation_staggered_3.png new file mode 100644 index 00000000000..e8b2eb37014 Binary files /dev/null and b/packages/flame_tiled/test/goldens/dungeon_animation_staggered_3.png differ diff --git a/packages/flame_tiled/test/goldens/flat_hex_even.png b/packages/flame_tiled/test/goldens/flat_hex_even.png index aa17df9e4a9..cdbca424a5c 100644 Binary files a/packages/flame_tiled/test/goldens/flat_hex_even.png and b/packages/flame_tiled/test/goldens/flat_hex_even.png differ diff --git a/packages/flame_tiled/test/goldens/flat_hex_odd.png b/packages/flame_tiled/test/goldens/flat_hex_odd.png index b258765b22d..ac90a245501 100644 Binary files a/packages/flame_tiled/test/goldens/flat_hex_odd.png and b/packages/flame_tiled/test/goldens/flat_hex_odd.png differ diff --git a/packages/flame_tiled/test/goldens/isometric.png b/packages/flame_tiled/test/goldens/isometric.png index 4101bb6e79b..88488c768f0 100644 Binary files a/packages/flame_tiled/test/goldens/isometric.png and b/packages/flame_tiled/test/goldens/isometric.png differ diff --git a/packages/flame_tiled/test/goldens/shifted_scaled_smaller.png b/packages/flame_tiled/test/goldens/shifted_scaled_smaller.png index 7c383cdfc53..ea3345669fc 100644 Binary files a/packages/flame_tiled/test/goldens/shifted_scaled_smaller.png and b/packages/flame_tiled/test/goldens/shifted_scaled_smaller.png differ diff --git a/packages/flame_tiled/test/goldens/tile_stack_all_move.png b/packages/flame_tiled/test/goldens/tile_stack_all_move.png index 9f25b485b99..837d12aaea1 100644 Binary files a/packages/flame_tiled/test/goldens/tile_stack_all_move.png and b/packages/flame_tiled/test/goldens/tile_stack_all_move.png differ diff --git a/packages/flame_tiled/test/tiled_component_sizes_test.dart b/packages/flame_tiled/test/tiled_component_sizes_test.dart new file mode 100644 index 00000000000..ff1bffab8c3 --- /dev/null +++ b/packages/flame_tiled/test/tiled_component_sizes_test.dart @@ -0,0 +1,258 @@ +import 'package:flame/components.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame_tiled/flame_tiled.dart'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:tiled/tiled.dart'; + +void main() { + group('TiledComponent.computeSize', () { + test('orthogonal', () { + expect( + TiledComponent.computeSize( + MapOrientation.orthogonal, + Vector2(16, 16), + 16, + 16, + 5, + 5, + null, + ), + Vector2(80, 80), + reason: 'full sized tiles', + ); + + expect( + TiledComponent.computeSize( + MapOrientation.orthogonal, + Vector2(8, 8), + 16, + 16, + 5, + 4, + null, + ), + Vector2(40, 32), + reason: 'half sized tiles', + ); + expect( + TiledComponent.computeSize( + MapOrientation.orthogonal, + Vector2(8, 32), + 16, + 16, + 4, + 5, + null, + ), + Vector2(32, 160), + reason: 'odd sized tiles', + ); + }); + + test('isometric', () { + expect( + TiledComponent.computeSize( + MapOrientation.isometric, + Vector2(16, 16), + 16, + 16, + 5, + 4, + null, + ), + Vector2(72, 72), + reason: 'full sized tiles', + ); + + expect( + TiledComponent.computeSize( + MapOrientation.isometric, + Vector2(8, 8), + 16, + 16, + 4, + 5, + null, + ), + Vector2(36, 36), + reason: 'full sized tiles', + ); + + expect( + TiledComponent.computeSize( + MapOrientation.isometric, + Vector2(8, 32), + 16, + 16, + 7, + 5, + null, + ), + Vector2(48, 192), + reason: 'odd sized tiles', + ); + }); + + test('isometric', () { + expect( + TiledComponent.computeSize( + MapOrientation.isometric, + Vector2(16, 16), + 16, + 16, + 5, + 4, + null, + ), + Vector2(72, 72), + reason: 'full sized tiles', + ); + + expect( + TiledComponent.computeSize( + MapOrientation.isometric, + Vector2(8, 8), + 16, + 16, + 4, + 5, + null, + ), + Vector2(36, 36), + reason: 'full sized tiles', + ); + + expect( + TiledComponent.computeSize( + MapOrientation.isometric, + Vector2(8, 32), + 16, + 16, + 7, + 5, + null, + ), + Vector2(48, 192), + reason: 'odd sized tiles', + ); + }); + + test('hexagonal', () { + expect( + TiledComponent.computeSize( + MapOrientation.hexagonal, + Vector2(16, 16), + 16, + 16, + 4, + 5, + StaggerAxis.x, + ), + Vector2(52, 88), + reason: 'full sized tiles, stagger x', + ); + + expect( + TiledComponent.computeSize( + MapOrientation.hexagonal, + Vector2(16, 16), + 16, + 16, + 4, + 5, + StaggerAxis.y, + ), + Vector2(72, 64), + reason: 'full sized tiles, stagger y', + ); + + expect( + TiledComponent.computeSize( + MapOrientation.hexagonal, + Vector2(8, 8), + 16, + 16, + 4, + 5, + StaggerAxis.x, + ), + Vector2(26, 44), + reason: 'half sized tiles, stagger x', + ); + + expect( + TiledComponent.computeSize( + MapOrientation.hexagonal, + Vector2(8, 8), + 16, + 16, + 4, + 5, + StaggerAxis.y, + ), + Vector2(36, 32), + reason: 'full sized tiles, stagger y', + ); + }); + + test('staggered', () { + expect( + TiledComponent.computeSize( + MapOrientation.staggered, + Vector2(16, 16), + 16, + 16, + 4, + 5, + StaggerAxis.x, + ), + Vector2(40, 88), + reason: 'full sized tiles, stagger x', + ); + + expect( + TiledComponent.computeSize( + MapOrientation.staggered, + Vector2(16, 16), + 16, + 16, + 4, + 5, + StaggerAxis.y, + ), + Vector2(72, 48), + reason: 'full sized tiles, stagger y', + ); + + expect( + TiledComponent.computeSize( + MapOrientation.staggered, + Vector2(8, 8), + 16, + 16, + 4, + 5, + StaggerAxis.x, + ), + Vector2(20, 44), + reason: 'half sized tiles, stagger x', + ); + + expect( + TiledComponent.computeSize( + MapOrientation.staggered, + Vector2(8, 8), + 16, + 16, + 4, + 5, + StaggerAxis.y, + ), + Vector2(36, 24), + reason: 'half sized tiles, stagger y', + ); + }); + }); +} diff --git a/packages/flame_tiled/test/tiled_test.dart b/packages/flame_tiled/test/tiled_test.dart index 60324b59e60..628a9297ad6 100644 --- a/packages/flame_tiled/test/tiled_test.dart +++ b/packages/flame_tiled/test/tiled_test.dart @@ -7,6 +7,9 @@ import 'package:flame/extensions.dart'; import 'package:flame/flame.dart'; import 'package:flame/game.dart'; import 'package:flame_tiled/flame_tiled.dart'; +import 'package:flame_tiled/src/renderable_layers/tile_layer.dart' + as renderable; + import 'package:flutter/services.dart' show CachingAssetBundle; import 'package:flutter_test/flutter_test.dart'; import 'package:tiled/tiled.dart'; @@ -310,17 +313,16 @@ void main() { Future renderMapToPng( TiledComponent component, - num width, - num height, ) async { final canvasRecorder = PictureRecorder(); final canvas = Canvas(canvasRecorder); component.tileMap.render(canvas); final picture = canvasRecorder.endRecording(); - // Map size is now 320 wide, but it has 1 extra tile of height becusae + final size = component.size; + // Map size is now 320 wide, but it has 1 extra tile of height because // its actually double-height tiles. - final image = await picture.toImageSafe(width.toInt(), height.toInt()); + final image = await picture.toImageSafe(size.x.toInt(), size.y.toInt()); return (await image.toByteData(format: ImageByteFormat.png))! .buffer .asUint8List(); @@ -358,7 +360,7 @@ void main() { }); test('renders', () async { - final pngData = await renderMapToPng(component, 32 * 16, 128 * 16); + final pngData = await renderMapToPng(component); expect(pngData, matchesGoldenFile('goldens/orthogonal.png')); }); @@ -382,14 +384,13 @@ void main() { test('component size', () { expect(component.tileMap.destTileSize, Vector2(64, 32)); - expect(component.size, Vector2(64 * 5, 32 * 5)); + expect(component.size, Vector2(320, 160)); }); test('renders', () async { - // Map size is now 320 wide, but it has 1 extra tile of height becusae + // Map size is now 320 wide, but it has 1 extra tile of height because // its actually double-height tiles. - final pngData = - await renderMapToPng(component, 256 * 5 ~/ 4, (128 * 5 + 128) ~/ 4); + final pngData = await renderMapToPng(component); expect(pngData, matchesGoldenFile('goldens/isometric.png')); }); @@ -424,7 +425,7 @@ void main() { expect(component.size, Vector2(240, 214.5)); - final pngData = await renderMapToPng(component, 240, 215); + final pngData = await renderMapToPng(component); expect(pngData, matchesGoldenFile('goldens/flat_hex_even.png')); }); @@ -438,7 +439,7 @@ void main() { expect(component.size, Vector2(240, 214.5)); - final pngData = await renderMapToPng(component, 240, 215); + final pngData = await renderMapToPng(component); expect(pngData, matchesGoldenFile('goldens/flat_hex_odd.png')); }); @@ -452,7 +453,7 @@ void main() { expect(component.size, Vector2(330, 208)); - final pngData = await renderMapToPng(component, 330, 208); + final pngData = await renderMapToPng(component); expect(pngData, matchesGoldenFile('goldens/pointy_hex_even.png')); }); @@ -466,7 +467,7 @@ void main() { expect(component.size, Vector2(330, 208)); - final pngData = await renderMapToPng(component, 330, 208); + final pngData = await renderMapToPng(component); expect(pngData, matchesGoldenFile('goldens/pointy_hex_odd.png')); }); @@ -501,7 +502,7 @@ void main() { expect(component.size, Vector2(320, 288)); - final pngData = await renderMapToPng(component, 320, 288); + final pngData = await renderMapToPng(component); expect( pngData, @@ -518,7 +519,7 @@ void main() { expect(component.size, Vector2(320 / 2, 288 / 2)); - final pngData = await renderMapToPng(component, 160, 144); + final pngData = await renderMapToPng(component); expect( pngData, @@ -535,7 +536,7 @@ void main() { expect(component.size, Vector2(576 / 2, 160 / 2)); - final pngData = await renderMapToPng(component, 288, 80); + final pngData = await renderMapToPng(component); expect( pngData, @@ -552,7 +553,7 @@ void main() { expect(component.size, Vector2(576, 160)); - final pngData = await renderMapToPng(component, 576, 160); + final pngData = await renderMapToPng(component); expect( pngData, @@ -582,11 +583,7 @@ void main() { test('regular', () async { await setupMap(size); - final pngData = await renderMapToPng( - component, - size.x.toInt() * 5, - size.y.toInt() * 5, - ); + final pngData = await renderMapToPng(component); expect( pngData, @@ -597,11 +594,7 @@ void main() { test('smaller', () async { final smallSize = size / 3; await setupMap(smallSize); - final pngData = await renderMapToPng( - component, - smallSize.x.toInt() * 5, - smallSize.y.toInt() * 5, - ); + final pngData = await renderMapToPng(component); expect( pngData, @@ -612,11 +605,7 @@ void main() { test('larger', () async { final largeSize = size * 2; await setupMap(largeSize); - final pngData = await renderMapToPng( - component, - largeSize.x.toInt() * 5, - largeSize.y.toInt() * 5, - ); + final pngData = await renderMapToPng(component); expect( pngData, @@ -664,8 +653,7 @@ void main() { final stack = component.tileMap.tileStack(0, 0, all: true); stack.position = stack.position + Vector2.all(20); - final pngData = - await renderMapToPng(component, size.x * 5, size.y * 5 + size.y / 2); + final pngData = await renderMapToPng(component); expect( pngData, matchesGoldenFile('goldens/tile_stack_all_move.png'), @@ -676,13 +664,140 @@ void main() { final stack = component.tileMap.tileStack(0, 0, named: {'item'}); stack.position = stack.position + Vector2(-20, 20); - final pngData = await renderMapToPng(component, size.x * 5, size.y * 5); + final pngData = await renderMapToPng(component); expect( pngData, matchesGoldenFile('goldens/tile_stack_single_move.png'), ); }); }); + + group('animated tiles', () { + late TiledComponent component; + late RenderableTiledMap map; + final size = Vector2(16, 16); + + for (final mapType in [ + 'orthogonal', + 'isometric', + 'hexagonal', + 'staggered' + ]) { + group(mapType, () { + setUp(() async { + Flame.bundle = TestAssetBundle( + imageNames: [ + '0x72_DungeonTilesetII_v1.4.png', + ], + mapPath: 'test/assets/dungeon_animation_$mapType.tmx', + ); + component = await TiledComponent.load( + 'dungeon_animation_$mapType.tmx', + size, + ); + map = component.tileMap; + }); + + test('handle single frame animations ($mapType)', () { + expect( + map.renderableLayers.first, + isInstanceOf(), + ); + final layer = map.renderableLayers.first as renderable.TileLayer; + expect( + layer.animations, + hasLength(1), + reason: 'layer has only one animation', + ); + expect( + layer.animationFrames, + hasLength(4), + reason: 'layer only caches frames in use', + ); + expect(layer.animations.first.frames.sources, hasLength(1)); + }); + + test('handle single frame animations ($mapType)', () { + expect(map.renderableLayers[1], isInstanceOf()); + final layer = map.renderableLayers[1] as renderable.TileLayer; + expect( + layer.animations, + hasLength(2), + reason: 'two animations on this layer', + ); + expect( + layer.animationFrames, + hasLength(4), + reason: 'layer only caches frames in use', + ); + + final waterAnimation = layer.animations.first; + final spikeAnimation = layer.animations.last; + expect(waterAnimation.frames.durations, [.18, .17, .15]); + expect(spikeAnimation.frames.durations, [.176, .176, .176, .176]); + + map.update(.177); + expect(waterAnimation.frame, 0); + expect(waterAnimation.frames.frameTime, .177); + expect( + waterAnimation.batchedSource.toRect(), + waterAnimation.frames.sources[0], + ); + + expect(spikeAnimation.frame, 1); + expect(spikeAnimation.frames.frameTime, moreOrLessEquals(.001)); + expect( + spikeAnimation.batchedSource.toRect(), + spikeAnimation.frames.sources[1], + ); + + map.update(.003); + expect(waterAnimation.frame, 1); + expect(waterAnimation.frames.frameTime, moreOrLessEquals(.0)); + expect(spikeAnimation.frame, 1); + expect(spikeAnimation.frames.frameTime, moreOrLessEquals(0.004)); + + map.update(0.17 + 0.15); + expect(waterAnimation.frame, 0, reason: 'wraps around'); + expect( + waterAnimation.batchedSource.toRect(), + waterAnimation.frames.sources[0], + ); + }); + + /// This will not produce a pretty map for non-orthogonal, but that's + /// OK, we're looking for parsing and handling of animations. + test('renders ($mapType)', () async { + var pngData = await renderMapToPng(component); + await expectLater( + pngData, + matchesGoldenFile('goldens/dungeon_animation_${mapType}_0.png'), + ); + + component.update(0.18); + pngData = await renderMapToPng(component); + await expectLater( + pngData, + matchesGoldenFile('goldens/dungeon_animation_${mapType}_1.png'), + ); + + component.update(0.18); + pngData = await renderMapToPng(component); + await expectLater( + pngData, + matchesGoldenFile('goldens/dungeon_animation_${mapType}_2.png'), + ); + + component.update(0.18); + pngData = await renderMapToPng(component); + await expectLater( + pngData, + matchesGoldenFile('goldens/dungeon_animation_${mapType}_3.png'), + ); + }); + }); + } + }); } class TestAssetBundle extends CachingAssetBundle {