Files
flame/packages/flame_tiled/test/tiled_test.dart
John McDole f4e8af2c90 TileStack (#1910)
Support searching for, and collecting stacks of tiles.
2022-09-20 08:25:34 +02:00

716 lines
20 KiB
Dart

import 'dart:io';
import 'dart:typed_data';
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/flame.dart';
import 'package:flame/game.dart';
import 'package:flame_tiled/flame_tiled.dart';
import 'package:flutter/services.dart' show CachingAssetBundle;
import 'package:flutter_test/flutter_test.dart';
import 'package:tiled/tiled.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('TiledComponent', () {
late TiledComponent tiled;
setUp(() async {
Flame.bundle = TestAssetBundle(
imageNames: ['map-level1.png', 'image1.png'],
mapPath: 'test/assets/map.tmx',
);
tiled = await TiledComponent.load('x', Vector2.all(16));
});
test('correct loads the file', () async {
expect(tiled.tileMap.renderableLayers.length, equals(3));
});
group('is positionable', () {
test('size, width, and height are readable - not writable', () async {
expect(tiled.size, Vector2(512.0, 2048.0));
expect(tiled.width, 512);
expect(tiled.height, 2048);
tiled.size = Vector2(256, 1024);
expect(tiled.size, Vector2(512.0, 2048.0));
tiled.width = 2;
expect(tiled.size, Vector2(512.0, 2048.0));
tiled.height = 2;
expect(tiled.size, Vector2(512.0, 2048.0));
});
test('from constructor', () async {
final map = TiledComponent(
tiled.tileMap,
position: Vector2(10, 20),
anchor: Anchor.bottomCenter,
children: [tiled],
angle: 1.4,
priority: 2,
scale: Vector2(1.5, 2.0),
);
expect(tiled.parent, map);
expect(map.anchor, Anchor.bottomCenter);
expect(map.angle, 1.4);
expect(map.priority, 2);
expect(map.position, Vector2(10, 20));
expect(map.scale, Vector2(1.5, 2.0));
});
});
});
test('correctly loads external tileset', () async {
final tsxProvider = await FlameTsxProvider.parse('external_tileset_1.tsx');
expect(tsxProvider.getCachedSource() != null, true);
expect(
tsxProvider
.getCachedSource()!
.getSingleChild('tileset')
.getString('name') ==
'level1',
true,
);
expect(
tsxProvider.filename == 'external_tileset_1.tsx',
true,
);
});
group('Layered tiles render correctly with layered sprite batch', () {
late Uint8List canvasPixelData;
late RenderableTiledMap overlapMap;
setUp(() async {
Flame.bundle = TestAssetBundle(
imageNames: [
'green_sprite.png',
'red_sprite.png',
],
mapPath: 'test/assets/2_tiles-green_on_red.tmx',
);
overlapMap = await RenderableTiledMap.fromFile(
'2_tiles-green_on_red.tmx',
Vector2.all(16),
);
final canvasRecorder = PictureRecorder();
final canvas = Canvas(canvasRecorder);
overlapMap.render(canvas);
final picture = canvasRecorder.endRecording();
final image = await picture.toImageSafe(32, 16);
final bytes = await image.toByteData();
canvasPixelData = bytes!.buffer.asUint8List();
});
test(
'Correctly loads batches list',
() => expect(overlapMap.renderableLayers.length == 2, true),
);
test(
'Canvas pixel dimensions match',
() => expect(
canvasPixelData.length == 16 * 32 * 4,
true,
),
);
test('Base test - right tile pixel is red', () {
expect(
canvasPixelData[16 * 4] == 255 &&
canvasPixelData[(16 * 4) + 1] == 0 &&
canvasPixelData[(16 * 4) + 2] == 0 &&
canvasPixelData[(16 * 4) + 3] == 255,
true,
);
final rightTilePixels = <int>[];
for (var i = 16 * 4; i < 16 * 32 * 4; i += 32 * 4) {
rightTilePixels.addAll(canvasPixelData.getRange(i, i + (16 * 4)));
}
var allRed = true;
for (var i = 0; i < rightTilePixels.length; i += 4) {
allRed &= rightTilePixels[i] == 255 &&
rightTilePixels[i + 1] == 0 &&
rightTilePixels[i + 2] == 0 &&
rightTilePixels[i + 3] == 255;
}
expect(allRed, true);
});
test('Left tile pixel is green', () {
expect(
canvasPixelData[15 * 4] == 0 &&
canvasPixelData[(15 * 4) + 1] == 255 &&
canvasPixelData[(15 * 4) + 2] == 0 &&
canvasPixelData[(15 * 4) + 3] == 255,
true,
);
final leftTilePixels = <int>[];
for (var i = 0; i < 15 * 32 * 4; i += 32 * 4) {
leftTilePixels.addAll(canvasPixelData.getRange(i, i + (16 * 4)));
}
var allGreen = true;
for (var i = 0; i < leftTilePixels.length; i += 4) {
allGreen &= leftTilePixels[i] == 0 &&
leftTilePixels[i + 1] == 255 &&
leftTilePixels[i + 2] == 0 &&
leftTilePixels[i + 3] == 255;
}
expect(allGreen, true);
});
});
group('Flipped and rotated tiles render correctly with sprite batch:', () {
late Uint8List canvasPixelData, canvasPixelDataAtlas;
late RenderableTiledMap overlapMap;
setUp(() async {
Flame.bundle = TestAssetBundle(
imageNames: [
'4_color_sprite.png',
],
mapPath: 'test/assets/8_tiles-flips.tmx',
);
overlapMap = await RenderableTiledMap.fromFile(
'8_tiles-flips.tmx',
Vector2.all(16),
);
final canvasRecorder = PictureRecorder();
final canvas = Canvas(canvasRecorder);
overlapMap.render(canvas);
final picture = canvasRecorder.endRecording();
final image = await picture.toImageSafe(64, 48);
final bytes = await image.toByteData();
canvasPixelData = bytes!.buffer.asUint8List();
await Flame.images.ready();
final canvasRecorderAtlas = PictureRecorder();
final canvasAtlas = Canvas(canvasRecorderAtlas);
overlapMap.render(canvasAtlas);
final pictureAtlas = canvasRecorderAtlas.endRecording();
final imageAtlas = await pictureAtlas.toImageSafe(64, 48);
final bytesAtlas = await imageAtlas.toByteData();
canvasPixelDataAtlas = bytesAtlas!.buffer.asUint8List();
});
test('[useAtlas = true] Green tile pixels are in correct spots', () {
final leftTilePixels = <int>[];
for (var i = 65 * 8 * 4; i < ((64 * 23) + (8 * 3)) * 4; i += 64 * 4) {
leftTilePixels.addAll(canvasPixelDataAtlas.getRange(i, i + (16 * 4)));
}
var allGreen = true;
for (var i = 0; i < leftTilePixels.length; i += 4) {
allGreen &= leftTilePixels[i] == 0 &&
leftTilePixels[i + 1] == 255 &&
leftTilePixels[i + 2] == 0 &&
leftTilePixels[i + 3] == 255;
}
expect(allGreen, true);
final rightTilePixels = <int>[];
for (var i = 69 * 8 * 4; i < ((64 * 23) + (8 * 7)) * 4; i += 64 * 4) {
rightTilePixels.addAll(canvasPixelDataAtlas.getRange(i, i + (16 * 4)));
}
for (var i = 0; i < rightTilePixels.length; i += 4) {
allGreen &= rightTilePixels[i] == 0 &&
rightTilePixels[i + 1] == 255 &&
rightTilePixels[i + 2] == 0 &&
rightTilePixels[i + 3] == 255;
}
expect(allGreen, true);
});
test('[useAtlas = false] Green tile pixels are in correct spots', () {
final leftTilePixels = <int>[];
for (var i = 65 * 8 * 4; i < ((64 * 23) + (8 * 3)) * 4; i += 64 * 4) {
leftTilePixels.addAll(canvasPixelData.getRange(i, i + (16 * 4)));
}
var allGreen = true;
for (var i = 0; i < leftTilePixels.length; i += 4) {
allGreen &= leftTilePixels[i] == 0 &&
leftTilePixels[i + 1] == 255 &&
leftTilePixels[i + 2] == 0 &&
leftTilePixels[i + 3] == 255;
}
expect(allGreen, true);
final rightTilePixels = <int>[];
for (var i = 69 * 8 * 4; i < ((64 * 23) + (8 * 7)) * 4; i += 64 * 4) {
rightTilePixels.addAll(canvasPixelData.getRange(i, i + (16 * 4)));
}
for (var i = 0; i < rightTilePixels.length; i += 4) {
allGreen &= rightTilePixels[i] == 0 &&
rightTilePixels[i + 1] == 255 &&
rightTilePixels[i + 2] == 0 &&
rightTilePixels[i + 3] == 255;
}
expect(allGreen, true);
});
});
group('Test getLayer:', () {
late RenderableTiledMap _renderableTiledMap;
setUp(() async {
Flame.bundle = TestAssetBundle(
imageNames: ['map-level1.png'],
mapPath: 'test/assets/layers_test.tmx',
);
_renderableTiledMap =
await RenderableTiledMap.fromFile('layers_test.tmx', Vector2.all(32));
});
test('Get Tile Layer', () {
expect(
_renderableTiledMap.getLayer<TileLayer>('MyTileLayer'),
isNotNull,
);
});
test('Get Object Layer', () {
expect(
_renderableTiledMap.getLayer<ObjectGroup>('MyObjectLayer'),
isNotNull,
);
});
test('Get Image Layer', () {
expect(
_renderableTiledMap.getLayer<ImageLayer>('MyImageLayer'),
isNotNull,
);
});
test('Get Group Layer', () {
expect(
_renderableTiledMap.getLayer<Group>('MyGroupLayer'),
isNotNull,
);
});
test('Get no layer', () {
expect(
_renderableTiledMap.getLayer<TileLayer>('Nonexistent layer'),
isNull,
);
});
});
Future<Uint8List> 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
// its actually double-height tiles.
final image = await picture.toImageSafe(width.toInt(), height.toInt());
return (await image.toByteData(format: ImageByteFormat.png))!
.buffer
.asUint8List();
}
group('orthogonal with groups, offsets, opacity and parallax', () {
late TiledComponent component;
final mapSizePx = Vector2(32 * 16, 128 * 16);
setUp(() async {
Flame.bundle = TestAssetBundle(
imageNames: [
'image1.png',
'map-level1.png',
],
mapPath: 'test/assets/map.tmx',
);
component = await TiledComponent.load(
'map.tmx',
Vector2(16, 16),
);
// Need to initialize a game and call `onLoad` and `onGameResize` to
// get the camera and canvas sizes all initialized
final game = FlameGame(children: [component]);
component.onLoad();
component.onGameResize(mapSizePx);
game.onGameResize(mapSizePx);
game.camera.snapTo(Vector2(150, 20));
});
test('component size', () {
expect(component.tileMap.destTileSize, Vector2(16, 16));
expect(component.size, mapSizePx);
});
test('renders', () async {
final pngData = await renderMapToPng(component, 32 * 16, 128 * 16);
expect(pngData, matchesGoldenFile('goldens/orthogonal.png'));
});
});
group('isometric', () {
late TiledComponent component;
setUp(() async {
Flame.bundle = TestAssetBundle(
imageNames: [
'isometric_spritesheet.png',
],
mapPath: 'test/assets/test_isometric.tmx',
);
component = await TiledComponent.load(
'test_isometric.tmx',
Vector2(256 / 4, 128 / 4),
);
});
test('component size', () {
expect(component.tileMap.destTileSize, Vector2(64, 32));
expect(component.size, Vector2(64 * 5, 32 * 5));
});
test('renders', () async {
// Map size is now 320 wide, but it has 1 extra tile of height becusae
// its actually double-height tiles.
final pngData =
await renderMapToPng(component, 256 * 5 ~/ 4, (128 * 5 + 128) ~/ 4);
expect(pngData, matchesGoldenFile('goldens/isometric.png'));
});
});
group('hexagonal', () {
late TiledComponent component;
Future<TiledComponent> setupMap(
String tmxFile,
String imageFile,
Vector2 destTileSize,
) async {
Flame.bundle = TestAssetBundle(
imageNames: [
imageFile,
],
mapPath: 'test/assets/$tmxFile',
);
return component = await TiledComponent.load(
tmxFile,
destTileSize,
);
}
test('flat + even staggerd', () async {
await setupMap(
'flat_hex_even.tmx',
'Tileset_Hexagonal_FlatTop_60x39_60x60.png',
Vector2(60, 39),
);
expect(component.size, Vector2(240, 214.5));
final pngData = await renderMapToPng(component, 240, 215);
expect(pngData, matchesGoldenFile('goldens/flat_hex_even.png'));
});
test('flat + odd staggerd', () async {
await setupMap(
'flat_hex_odd.tmx',
'Tileset_Hexagonal_FlatTop_60x39_60x60.png',
Vector2(60, 39),
);
expect(component.size, Vector2(240, 214.5));
final pngData = await renderMapToPng(component, 240, 215);
expect(pngData, matchesGoldenFile('goldens/flat_hex_odd.png'));
});
test('pointy + even staggerd', () async {
await setupMap(
'pointy_hex_even.tmx',
'Tileset_Hexagonal_PointyTop_60x52_60x80.png',
Vector2(60, 52),
);
expect(component.size, Vector2(330, 208));
final pngData = await renderMapToPng(component, 330, 208);
expect(pngData, matchesGoldenFile('goldens/pointy_hex_even.png'));
});
test('pointy + odd staggerd', () async {
await setupMap(
'pointy_hex_odd.tmx',
'Tileset_Hexagonal_PointyTop_60x52_60x80.png',
Vector2(60, 52),
);
expect(component.size, Vector2(330, 208));
final pngData = await renderMapToPng(component, 330, 208);
expect(pngData, matchesGoldenFile('goldens/pointy_hex_odd.png'));
});
});
group('isometric staggered', () {
late TiledComponent component;
Future<TiledComponent> setupMap(
String tmxFile,
String imageFile,
Vector2 destTileSize,
) async {
Flame.bundle = TestAssetBundle(
imageNames: [
imageFile,
],
mapPath: 'test/assets/$tmxFile',
);
return component = await TiledComponent.load(
tmxFile,
destTileSize,
);
}
test('x + odd', () async {
await setupMap(
'iso_staggered_overlap_x_odd.tmx',
'dirt_atlas.png',
Vector2(128, 64),
);
expect(component.size, Vector2(320, 288));
final pngData = await renderMapToPng(component, 320, 288);
expect(
pngData,
matchesGoldenFile('goldens/iso_staggered_overlap_x_odd.png'),
);
});
test('x + even + half sized', () async {
await setupMap(
'iso_staggered_overlap_x_even.tmx',
'dirt_atlas.png',
Vector2(128 / 2, 64 / 2),
);
expect(component.size, Vector2(320 / 2, 288 / 2));
final pngData = await renderMapToPng(component, 160, 144);
expect(
pngData,
matchesGoldenFile('goldens/iso_staggered_overlap_x_even.png'),
);
});
test('y + odd + half', () async {
await setupMap(
'iso_staggered_overlap_y_odd.tmx',
'dirt_atlas.png',
Vector2(128 / 2, 64 / 2),
);
expect(component.size, Vector2(576 / 2, 160 / 2));
final pngData = await renderMapToPng(component, 288, 80);
expect(
pngData,
matchesGoldenFile('goldens/iso_staggered_overlap_y_odd.png'),
);
});
test('y + even', () async {
await setupMap(
'iso_staggered_overlap_y_even.tmx',
'dirt_atlas.png',
Vector2(128, 64),
);
expect(component.size, Vector2(576, 160));
final pngData = await renderMapToPng(component, 576, 160);
expect(
pngData,
matchesGoldenFile('goldens/iso_staggered_overlap_y_even.png'),
);
});
});
group('shifted and scaled', () {
late TiledComponent component;
final size = Vector2(256, 128);
Future<void> setupMap(
Vector2 destTileSize,
) async {
Flame.bundle = TestAssetBundle(
imageNames: [
'isometric_spritesheet.png',
],
mapPath: 'test/assets/test_shifted.tmx',
);
component = await TiledComponent.load(
'test_isometric.tmx',
destTileSize,
);
}
test('regular', () async {
await setupMap(size);
final pngData = await renderMapToPng(
component,
size.x.toInt() * 5,
size.y.toInt() * 5,
);
expect(
pngData,
matchesGoldenFile('goldens/shifted_scaled_regular.png'),
);
});
test('smaller', () async {
final smallSize = size / 3;
await setupMap(smallSize);
final pngData = await renderMapToPng(
component,
smallSize.x.toInt() * 5,
smallSize.y.toInt() * 5,
);
expect(
pngData,
matchesGoldenFile('goldens/shifted_scaled_smaller.png'),
);
});
test('larger', () async {
final largeSize = size * 2;
await setupMap(largeSize);
final pngData = await renderMapToPng(
component,
largeSize.x.toInt() * 5,
largeSize.y.toInt() * 5,
);
expect(
pngData,
matchesGoldenFile('goldens/shifted_scaled_larger.png'),
);
});
});
group('TileStack', () {
late TiledComponent component;
final size = Vector2(256 / 2, 128 / 2);
setUp(() async {
Flame.bundle = TestAssetBundle(
imageNames: [
'isometric_spritesheet.png',
],
mapPath: 'test/assets/test_isometric.tmx',
);
component = await TiledComponent.load('test_isometric.tmx', size);
});
test('from all layers', () {
var stack = component.tileMap.tileStack(0, 0, all: true);
expect(stack.length, 2);
stack = component.tileMap.tileStack(1, 0, all: true);
expect(stack.length, 1);
});
test('from some layers', () {
var stack = component.tileMap.tileStack(0, 0, named: {'empty'});
expect(stack.length, 0);
stack = component.tileMap.tileStack(0, 0, named: {'item'});
expect(stack.length, 1);
stack = component.tileMap.tileStack(0, 0, ids: {1});
expect(stack.length, 1);
stack = component.tileMap.tileStack(0, 0, ids: {1, 2});
expect(stack.length, 2);
});
test('can be positioned together', () async {
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);
expect(
pngData,
matchesGoldenFile('goldens/tile_stack_all_move.png'),
);
});
test('can be positioned singularly', () async {
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);
expect(
pngData,
matchesGoldenFile('goldens/tile_stack_single_move.png'),
);
});
});
}
class TestAssetBundle extends CachingAssetBundle {
TestAssetBundle({
required this.imageNames,
required this.mapPath,
});
final List<String> imageNames;
final String mapPath;
@override
Future<ByteData> load(String key) async {
final split = key.split('/');
final imgName = split.isNotEmpty ? split.last : key;
var toLoadName = key.split('/').last;
if (!imageNames.contains(imgName) && imageNames.isNotEmpty) {
toLoadName = imageNames.first;
}
return File('test/assets/$toLoadName')
.readAsBytes()
.then((bytes) => ByteData.view(Uint8List.fromList(bytes).buffer));
}
@override
Future<String> loadString(String key, {bool cache = true}) {
return File(mapPath).readAsString();
}
}