diff --git a/flixel/tile/FlxBaseTilemap.hx b/flixel/tile/FlxBaseTilemap.hx index c622c71695..f3b473e729 100644 --- a/flixel/tile/FlxBaseTilemap.hx +++ b/flixel/tile/FlxBaseTilemap.hx @@ -9,14 +9,17 @@ import flixel.system.FlxAssets; import flixel.util.FlxArrayUtil; import flixel.util.FlxCollision; import flixel.util.FlxColor; +import flixel.util.FlxDestroyUtil; +import flixel.util.FlxDirection; import flixel.util.FlxDirectionFlags; import flixel.util.FlxStringUtil; import openfl.display.BitmapData; using StringTools; +using flixel.tile.FlxBaseTilemap.AmbiIntIterator; @:autoBuild(flixel.system.macros.FlxMacroUtil.deprecateOverride("overlapsWithCallback", "overlapsWithCallback is deprecated, use objectOverlapsTiles")) -class FlxBaseTilemap extends FlxObject +abstract class FlxBaseTilemap extends FlxObject { /** * Set this flag to use one of the 16-tile binary auto-tile algorithms (OFF, AUTO, or ALT). @@ -145,7 +148,12 @@ class FlxBaseTilemap extends FlxObject */ public function getColumnAt(worldX:Float, bind = false):Int { - throw "getColumnAt must be implemented"; + final result = Math.floor((worldX - x) / getTileWidth()); + + if (bind) + return result < 0 ? 0 : (result >= widthInTiles ? widthInTiles - 1 : result); + + return result; } /** @@ -158,7 +166,12 @@ class FlxBaseTilemap extends FlxObject */ public function getRowAt(worldY:Float, bind = false):Int { - throw "getRowAt must be implemented"; + final result = Math.floor((worldY - y) / getTileWidth()); + + if (bind) + return result < 0 ? 0 : (result >= heightInTiles ? heightInTiles - 1 : result); + + return result; } /** @@ -168,11 +181,11 @@ class FlxBaseTilemap extends FlxObject * @param midpoint Whether to use the tile's midpoint, or upper left corner * @since 5.9.0 */ - public function getColumnPos(column:Float, midPoint = false):Float + public function getColumnPos(column:Int, midpoint = false):Float { - throw "getColumnPos must be implemented"; + return x + column * getTileWidth() + (midpoint ? getTileWidth() * 0.5 : 0); } - + /** * Get the world position of the specified row * @@ -180,11 +193,49 @@ class FlxBaseTilemap extends FlxObject * @param midpoint Whether to use the tile's midpoint, or upper left corner * @since 5.9.0 */ - public function getRowPos(row:Int, midPoint = false):Float + public function getRowPos(row:Int, midpoint = false):Float + { + return y + row * getTileHeight() + (midpoint ? getTileHeight() * 0.5 : 0); + } + + /** + * Get the world position of the column at the specified location + * + * @param worldX An X coordinate in the world + * @param midpoint Whether to use the tile's midpoint, or left edge + * @since 6.2.0 + */ + public function getColumnPosAt(worldX:Float, midpoint = false):Float + { + return getColumnPos(getColumnAt(worldX), midpoint); + } + + /** + * Get the world position of the row at the specified location + * + * @param worldY An X coordinate in the world + * @param midpoint Whether to use the tile's midpoint, or upper edge + * @since 6.2.0 + */ + public function getRowPosAt(worldY:Float, midpoint = false):Float { - throw "getRowPos must be implemented"; + return getRowPos(getRowAt(worldY), midpoint); } + /** + * Get the width of a column, in world coordinates + * + * @since 6.2.0 + */ + abstract public function getTileWidth():Float; + + /** + * Get the height of a row, in world coordinates + * + * @since 6.2.0 + */ + abstract public function getTileHeight():Float; + /** * **Note:** This method name is misleading! It does not return a `tileIndex`, it returns a `mapIndex` * @@ -202,25 +253,421 @@ class FlxBaseTilemap extends FlxObject return getTilePos(mapIndex, midpoint); } + // ============================================================================= + //{ region Ray + Helpers + // ============================================================================= + /** - * Shoots a ray from the start point to the end point. - * If/when it passes through a tile, it stores that point and returns false. + * Determines whether the ray can travel from `start` to `end` without hitting a solid wall. * * **Note:** In flixel 5.0.0, this was redone, the old method is now `rayStep` - * - * @param start The world coordinates of the start of the ray. - * @param end The world coordinates of the end of the ray. - * @param result Optional result vector, to avoid creating a new instance to be returned. - * Only returned if the line enters the rect. - * @return Returns true if the ray made it from Start to End without hitting anything. - * Returns false and fills Result if a tile was hit. + * + * @param start The world coordinates of the start of the ray + * @param end The world coordinates of the end of the ray + * @param result Optional result vector, indicating where the ray hit the first wall + * @return Whether the ray can travel from `start` to `end` without hitting a wall */ public function ray(start:FlxPoint, end:FlxPoint, ?result:FlxPoint):Bool { - throw "ray must be implemented"; - return false; + return switch rayAdvanced(start, end, false) + { + case END: + return true; + + case STOPPED(index, x, y, entry): + if (result != null) + result.set(x, y); + + return false; + } } - + + /** + * Determines whether the ray can travel from `start` to `end` without hitting a solid wall. + * + * @param start The world coordinates of the start of the ray + * @param end The world coordinates of the end of the ray + * @param checkDir If `true`, tiles' `allowCollision` directions are used, otherwise just checks `solid` + * @return Whether the ray can travel from `start` to `end` without hitting a wall + */ + public function rayAdvanced(start:FlxPoint, end:FlxPoint, checkDir = true) + { + final func = checkRayDirHelper(start, end, checkDir); + return findInRay(start, end, func); + } + + /** + * Ray func helper, checks tiles' directions according to the ray's direction + */ + function checkRayDirHelper(start:FlxPoint, end:FlxPoint, checkDir:Bool) + { + if (!checkDir) + return (_, t:Tile, _)->t != null && t.solid; + + return function (i:Int, tile:Null, entry:FlxRayEntry) + { + return tile != null && switch entry + { + case EDGE(dir): + tile.allowCollisions.has(dir); + + case START: tile.allowCollisions == ANY + || (tile.allowCollisions.left && start.x < end.x) + || (tile.allowCollisions.right && start.x > end.x) + || (tile.allowCollisions.up && start.y < end.y) + || (tile.allowCollisions.down && start.y > end.y); + } + }; + } + + /** + * Calls `func` on all tiles overlapping a ray from `start` to `end` + * + * @param start The world coordinates of the start of the ray + * @param end The world coordinates of the end of the ray + * @param func The function, where `index` is the tile's map index, `tile` is the tile data + * at that location, if one exists and `entry` is how the ray entered the tile + * @since 6.2.0 + */ + inline public function forEachInRay(start, end, func:(index:Int, tile:Null, entry:FlxRayEntry)->Void) + { + findInRay(start, end, (i, t, e)->{ func(i, t, e); return false; }); + } + + /** + * Checks all tile indices overlapping a ray from `start` to `end`, + * finds the first tile that satisfies to condition of `func` and returns its index + * + * @param start The world coordinates of the start of the ray + * @param end The world coordinates of the end of the ray + * @param func The stopping condition, where `index` is the tile's map index, `tile` is the + * tile data at that location, if one exists and `entry` is how the ray entered the + * tile. If `true` is returned, the search ends and that tile's index is the result + * @return The index of the found tile + * @since 6.2.0 + */ + public function findIndexInRay(start, end, func:(index:Int, tile:Null, entry:FlxRayEntry)->Bool):Int + { + switch findInRay(start, end, func) + { + case END: + return -1; + case STOPPED(mapIndex, _, _, _): + return mapIndex; + } + } + + /** + * Checks all tile indices overlapping a ray from `start` to `end`, + * finds the first tile that satisfies to condition of `func` + * + * @param start The world coordinates of the start of the ray + * @param end The world coordinates of the end of the ray + * @param func The stopping condition, where `index` is the tile's map index, `tile` is the + * tile data at that location, if one exists, `entry` is how the ray entered the + * tile. If `true` is returned, the search ends and that tile is the result + * @return The result of the ray, whether it reached the end or was stopped, and where + * @since 6.2.0 + */ + public function findInRay + (start:FlxPoint, end:FlxPoint, func:(index:Int, tile:Null, entry:FlxRayEntry)->Bool):FlxRayResult + { + // trim the line to the parts inside the map + final trimmedStart = calcRayEntry(start, end); + final trimmedEnd = calcRayExit(start, end); + + start.putWeak(); + end.putWeak(); + + if (trimmedStart == null && trimmedEnd == null) + return END; + + // Cache x/y in floats so we can put() them now + final wasStartTrimmed = trimmedStart.x != start.x || trimmedStart.y != start.y; + final startX = trimmedStart.x; + final startY = trimmedStart.y; + final endX = trimmedEnd.x; + final endY = trimmedEnd.y; + trimmedStart.put(); + trimmedEnd.put(); + + final startIndex = getMapIndexAt(startX, startY); + final endIndex = getMapIndexAt(endX, endY); + final startTileX = getColumn(startIndex); + final startTileY = getRow(startIndex); + final endTileX = getColumn(endIndex); + final endTileY = getRow(endIndex); + final dirY = start.y < end.y ? FlxDirection.UP : FlxDirection.DOWN; + + // handle vertical line (infinite slope), first + if (start.x == end.x) + { + final entry = wasStartTrimmed ? EDGE(dirY) : START; + final resultIndex = findIndexInColumnWithEntry(startTileX, startTileY, endTileY, func, entry); + if (resultIndex != -1) + { + final resultY = getRowPos(getRow(resultIndex) + (start.y > end.y ? 1 : 0)); + final colEntry = getRow(resultIndex) == startTileY ? entry : EDGE(dirY); + return STOPPED(resultIndex, start.x, resultY, colEntry); + } + + return END; + } + + // Use y = mx + b formula + final m = (start.y - end.y) / (start.x - end.x); + // y - mx = b + final b = start.y - m * start.x; + + final movesRight = start.x < end.x; + final inc = movesRight ? 1 : -1; + final offset = movesRight ? 1 : 0; + var colEntry = wasStartTrimmed ? EDGE(movesRight ? LEFT : RIGHT) : START; + var lastTileY = startTileY; + + for (tileX in startTileX.iter(endTileX)) + { + final xPos = getColumnPos(tileX + offset); + final yPos = ambiClamp(m * getColumnPos(tileX + offset) + b, startY, endY); + final tileY = getRowAt(yPos); + final resultIndex = findIndexInColumnWithEntry(tileX, lastTileY, tileY, func, colEntry); + if (resultIndex != -1) + { + final endY = getRow(resultIndex); + final tileEntry = endY == lastTileY ? colEntry : EDGE(dirY); + return calcRayResult(resultIndex, tileEntry, m, b, start); + } + + colEntry = EDGE(movesRight ? LEFT : RIGHT); + lastTileY = tileY; + } + + return END; + } + + /** + * Helper to clamp between to values, regardless of which is smaller + */ + function ambiClamp(value:Float, a:Float, b:Float):Float + { + if (a > b) + return ambiClamp(value, b, a); + + if (value < a) + return a; + + if (value > b) + return b; + + return value; + } + + /** + * Helper to add an `entry` to `findIndexInColumn` callbacks + * + * @param column The column to check + * @param startRow The row to check from + * @param endRow The row to check to + * @param func The stopping condition, where `index` is the tile's map index, `tile` is the + * tile data at that location, if one exists, `entry` is how the ray entered the + * tile. If `true` is returned, the search ends and that tile is the result + * @param entry How the ray entered this column + */ + function findIndexInColumnWithEntry + (column, startRow, endRow, func:(index:Int, tile:Null, entry:FlxRayEntry) -> Bool, entry:FlxRayEntry) + { + final startI = getMapIndex(column, startRow); + final edge = EDGE(startRow < endRow ? UP : DOWN); + + return findIndexInColumn(column, startRow, endRow, function(i, t) + { + return func(i, t, i == startI ? entry : edge); + }); + } + + function calcRayResult(index:Int, entry:FlxRayEntry, m:Float, b:Float, start:FlxPoint):FlxRayResult + { + return switch entry + { + case START: + STOPPED(index, start.x, start.y, entry); + case EDGE(LEFT): + final x = getColumnPos(getColumn(index)); + final y = m * x + b; + STOPPED(index, x, y, entry); + case EDGE(RIGHT): + final x = getColumnPos(getColumn(index) + 1); + final y = m * x + b; + STOPPED(index, x, y, entry); + case EDGE(UP): + final y = getRowPos(getRow(index)); + final x = (y - b) / m; + STOPPED(index, x, y, entry); + case EDGE(DOWN): + final y = getRowPos(getRow(index) + 1); + final x = (y - b) / m; + STOPPED(index, x, y, entry); + } + } + + /** + * Calls `func` on all tiles in the `column` between the specified `startRow` and `endRow` + * + * @param column The column to check + * @param startRow The row to check from + * @param endRow The row to check to + * @param func The function, where `index` is the tile's map index, and + * `tile` is the tile data at that location, if one exists + * @since 6.2.0 + */ + public function forEachInColumn(column, startRow, endRow, func:(index:Int, tile:Null)->Void) + { + findIndexInColumn(column, startRow, endRow, (i, t)->{ func(i, t); return false; }); + } + + /** + * Calls `func` on all tiles in the `column` between the specified `startColumn` and `endColumn` + * + * @param row The row to check + * @param startColumn The column to check from + * @param endColumn The column to check to + * @param func The function, where `index` is the tile's map index, and + * `tile` is the tile data at that location, if one exists + * @since 6.2.0 + */ + overload public inline extern function forEachInRow(row, startColumn, endColumn, func:(index:Int, tile:Null)->Void) + { + findIndexInRow(row, startColumn, endColumn, (i, t)->{ func(i, t); return false; }); + } + + /** + * Checks all tiles in the `column` between the specified `startRow` and `endRow`, + * Retrieves the first tile that satisfies to condition of `func` and returns it + * + * @param column The column to check + * @param startRow The row to check from + * @param endRow The row to check to + * @param func The stopping condition, where `index` is the tile's map index, `tile` is + * the tile data at that location, if one exists. If `true` is returned, + * the search ends and that tile is the result + * @return The found tile + * @since 6.2.0 + */ + public function findInColumn(column:Int, startRow:Int, endRow:Int, func):Null + { + final index = findIndexInColumn(column, startRow, endRow, func); + if (index < 0) + return null; + + return getTileData(index); + } + + /** + * Checks all tile indices in the `column` between the specified `startRow` and `endRow`, + * finds the first tile that satisfies to condition of `func` and returns its index + * + * @param column The column to check + * @param startRow The row to check from + * @param endRow The row to check to + * @param func The stopping condition, where `index` is the tile's map index, `tile` is + * the tile data at that location, if one exists. If `true` is returned, + * the search ends and that tile is the result + * @return The index of the found tile + * @since 6.2.0 + */ + public function findIndexInColumn(column:Int, startRow:Int, endRow:Int, func:(index:Int, tile:Null)->Bool):Int + { + if (!columnExists(column)) + throw 'Invalid column: $column, total column: $widthInTiles'; + + if (startRow < 0) + startRow = 0; + else if (startRow > heightInTiles - 1) + startRow = heightInTiles - 1; + + if (endRow < 0) + endRow = 0; + else if (endRow > heightInTiles - 1) + endRow = heightInTiles - 1; + + for (row in startRow.iter(endRow)) + { + final index = getMapIndex(column, row); + if (index == -1) + throw 'Unexpected -1 map index for column: $column row: $row'; + + final tile = getTileData(index, true); + if (func(index, tile)) + return index; + } + + return -1; + } + + /** + * Checks all tile indices in the `row` between the specified start and end column, + * Retrieves the first tile that satisfies to condition of `func` and returns it + * + * @param row The row to check + * @param startColumn The column to check from + * @param endColumn The column to check to + * @param func The stopping condition, where `index` is the tile's map index, `tile` is + * the tile data at that location, if one exists. If `true` is returned, + * the search ends and that tile is the result + * @return The found tile + * @since 6.2.0 + */ + public function findInRow(row:Int, startColumn:Int, endColumn:Int, func) + { + final index = findIndexInRow(row, startColumn, endColumn, func); + if (index < 0) + return null; + + return getTileData(index, true); + } + + /** + * Checks all tile indices in the `row` between the specified start and end column, + * finds the first tile that satisfies to condition of `func` and returns its index + * + * @param row The row to check + * @param startColumn The column to check from + * @param endColumn The column to check to + * @param func The stopping condition, where `index` is the tile's map index, and + * `tile` is the tile data at that location, if one exists. If `true` is + * returned, the search ends and that tile's index is the result + * @return The index of the found tile + * @since 6.2.0 + */ + public function findIndexInRow(row:Int, startColumn:Int, endColumn:Int, func:(index:Int, tile:Null)->Bool):Int + { + if (!rowExists(row)) + throw 'Invalid row: $row, total rows: $heightInTiles'; + + if (startColumn < 0) + startColumn = 0; + else if (startColumn > widthInTiles - 1) + startColumn = widthInTiles - 1; + + if (endColumn < 0) + endColumn = 0; + else if (endColumn > widthInTiles - 1) + endColumn = widthInTiles - 1; + + for (column in startColumn.iter(endColumn)) + { + final index = getMapIndex(column, row); + if (index == -1) + throw 'Unexpected -1 map index for column: $column row: $row'; + + final tile = getTileData(index, true); + if (func(index, tile)) + return index; + } + + return -1; + } + /** * Shoots a ray from the start point to the end point. * If/when it passes through a tile, it stores that point and returns false. @@ -239,7 +686,6 @@ class FlxBaseTilemap extends FlxObject public function rayStep(start:FlxPoint, end:FlxPoint, ?result:FlxPoint, resolution:Float = 1):Bool { throw "rayStep must be implemented?"; - return false; } /** @@ -287,6 +733,9 @@ class FlxBaseTilemap extends FlxObject return calcRayEntry(end, start, result); } + //} endregion Ray + Helpers + // ============================================================================= + /** * Searches all tiles near the object for any that satisfy the given filter. Stops searching * when the first overlapping tile that satisfies the condition is found @@ -300,7 +749,7 @@ class FlxBaseTilemap extends FlxObject */ public function isOverlappingTile(object:FlxObject, ?filter:(tile:Tile)->Bool, ?position:FlxPoint):Bool { - throw "overlapsWithCallback must be implemented"; + throw "isOverlappingTile must be implemented"; } /** @@ -315,7 +764,7 @@ class FlxBaseTilemap extends FlxObject */ public function forEachOverlappingTile(object:FlxObject, func:(tile:Tile)->Void, ?position:FlxPoint):Bool { - throw "overlapsWithCallback must be implemented"; + throw "forEachOverlappingTile must be implemented"; } @:deprecated("overlapsWithCallback is deprecated, use objectOverlapsTiles(object, callback, pos), instead") // 5.9.0 @@ -783,7 +1232,7 @@ class FlxBaseTilemap extends FlxObject * @param row The grid Y location, in tiles * @since 5.9.0 */ - public overload extern inline function getMapIndex(column:Int, row:Int):Int + overload public inline extern function getMapIndex(column:Int, row:Int):Int { return tileExists(column, row) ? (row * widthInTiles + column) : -1; } @@ -797,7 +1246,7 @@ class FlxBaseTilemap extends FlxObject * @param worldPos A location in the world * @since 5.9.0 */ - public overload extern inline function getMapIndex(worldPos:FlxPoint):Int + overload public inline extern function getMapIndex(worldPos:FlxPoint):Int { return getMapIndexAt(worldPos.x, worldPos.y); } @@ -817,7 +1266,10 @@ class FlxBaseTilemap extends FlxObject return getMapIndex(getColumnAt(worldX), getRowAt(worldY)); } /** - * Calculates the column from a map location + * Calculates the column from a map index + * + * **Note:** The index is not checked against the total tiles, to ensure a + * valid tile, use `if (tileExists(mapIndex))`, first * * @param mapIndex The location in the map where `mapIndex = row * widthInTiles + column` * @since 5.9.0 @@ -828,7 +1280,10 @@ class FlxBaseTilemap extends FlxObject } /** - * Calculates the column from a map location + * Calculates the row from a map index + * + * **Note:** The index is not checked against the total tiles, to ensure a + * valid tile, use `if (tileExists(mapIndex))`, first * * @param mapIndex The location in the map where `mapIndex = row * widthInTiles + column` * @since 5.9.0 @@ -845,7 +1300,7 @@ class FlxBaseTilemap extends FlxObject * @param row The grid Y location, in tiles * @since 5.9.0 */ - public overload extern inline function tileExists(column:Int, row:Int):Bool + overload public inline extern function tileExists(column:Int, row:Int):Bool { return columnExists(column) && rowExists(row); } @@ -858,7 +1313,7 @@ class FlxBaseTilemap extends FlxObject * @param mapIndex The desired location in the map * @since 5.9.0 */ - public overload extern inline function tileExists(mapIndex:Int):Bool + overload public inline extern function tileExists(mapIndex:Int):Bool { return mapIndex >= 0 && mapIndex < _data.length; } @@ -869,7 +1324,7 @@ class FlxBaseTilemap extends FlxObject * @param worldPos A location in the map * @since 5.9.0 */ - public overload extern inline function tileExists(worldPos:FlxPoint):Bool + overload public inline extern function tileExists(worldPos:FlxPoint):Bool { return tileExistsAt(worldPos.x, worldPos.y); } @@ -892,7 +1347,7 @@ class FlxBaseTilemap extends FlxObject * @param column The grid X location, in tiles * @since 5.9.0 */ - public overload extern inline function columnExists(column:Int):Bool + overload public inline extern function columnExists(column:Int):Bool { return column >= 0 && column < widthInTiles; } @@ -914,7 +1369,7 @@ class FlxBaseTilemap extends FlxObject * @param row The grid Y location, in tiles * @since 5.9.0 */ - public overload extern inline function rowExists(row:Int):Bool + overload public inline extern function rowExists(row:Int):Bool { return row >= 0 && row < heightInTiles; } @@ -936,11 +1391,12 @@ class FlxBaseTilemap extends FlxObject * * @param column The grid X location, in tiles * @param row The grid Y location, in tiles + * @param orient If `true`, positions the tile in the world, useful for collision * @since 5.9.0 */ - public overload extern inline function getTileData(column:Int, row:Int):Null + overload public inline extern function getTileData(column:Int, row:Int, orient = false) { - return getTileData(getMapIndex(column, row)); + return getTileData(getMapIndex(column, row), orient); } /** @@ -948,47 +1404,47 @@ class FlxBaseTilemap extends FlxObject * if the `mapIndex` is invalid, the result is `null` * * **Note:** A tile's `mapIndex` can be calculated via `row * widthInTiles + column` - * - * **Note:** The reulting tile's `x`, `y`, `width` and `height` will not be accurate. - * You can call `tile.orient` or similar methods * * @param mapIndex The desired location in the map + * @param orient If `true`, positions the tile in the world, useful for collision * @since 5.9.0 */ - public overload extern inline function getTileData(mapIndex:Int):Null + overload public inline extern function getTileData(mapIndex:Int, orient = false):Null { - return _tileObjects[getTileIndex(mapIndex)]; + final tile = _tileObjects[getTileIndex(mapIndex)]; + if (orient) + orientTile(tile, mapIndex); + + return tile; } + abstract function orientTile(tile:Null, mapIndex:Int):Null; + /** * Finds the tile instance with the given world location, if the * coordinate does not overlap the tilemap, the result is `null` * - * **Note:** The reulting tile's `x`, `y`, `width` and `height` will not be accurate. - * You can call `tile.orient` or similar methods - * * @param worldPos A location in the world + * @param orient If `true`, positions the tile in the world, useful for collision * @since 5.9.0 */ - public overload extern inline function getTileData(worldPos:FlxPoint):Null + overload public inline extern function getTileData(worldPos:FlxPoint, orient = false):Null { - return getTileDataAt(worldPos.x, worldPos.y); + return getTileDataAt(worldPos.x, worldPos.y, orient); } /** * Finds the tile instance with the given world location, if the * coordinate does not overlap the tilemap, the result is `null` * - * **Note:** The reulting tile's `x`, `y`, `width` and `height` will not be accurate. - * You can call `tile.orient` or similar methods - * * @param worldX An X coordinate in the world - * @param worldY A Y coordinate in the world + * @param worldY An Y coordinate in the world + * @param orient If `true`, positions the tile in the world, useful for collision * @since 5.9.0 */ - public overload extern inline function getTileDataAt(worldX:Float, worldY:Float):Null + overload public inline extern function getTileDataAt(worldX:Float, worldY:Float, orient = false):Null { - return _tileObjects[getTileIndexAt(worldX, worldY)]; + return getTileData(getMapIndexAt(worldX, worldY), orient); } /** @@ -1000,7 +1456,7 @@ class FlxBaseTilemap extends FlxObject * @return The tile index of the tile at this location * @since 5.9.0 */ - public overload extern inline function getTileIndex(column:Int, row:Int):Int + overload public inline extern function getTileIndex(column:Int, row:Int):Int { return getTileIndex(getMapIndex(column, row)); } @@ -1015,7 +1471,7 @@ class FlxBaseTilemap extends FlxObject * @return The tileIndex of the tile with this `mapIndex` * @since 5.9.0 */ - public overload extern inline function getTileIndex(mapIndex:Int):Int + overload public inline extern function getTileIndex(mapIndex:Int):Int { return tileExists(mapIndex) ? _data[mapIndex] : -1; } @@ -1028,7 +1484,7 @@ class FlxBaseTilemap extends FlxObject * @return The tileIndex of the tile at this location * @since 5.9.0 */ - public overload extern inline function getTileIndex(worldPos:FlxPoint):Int + overload public inline extern function getTileIndex(worldPos:FlxPoint):Int { return getTileIndexAt(worldPos.x, worldPos.y); } @@ -1049,18 +1505,19 @@ class FlxBaseTilemap extends FlxObject /** * Get the world position of the specified tile, if the `mapIndex` is invalid, - * the result is `null` + * `null` is returned and `result` is unchanged * * **Note:** A tile's `mapIndex` can be calculated via `row * widthInTiles + column` * * @param mapIndex The desired location in the map * @param midpoint Whether to use the tile's midpoint, or upper left corner + * @param result The point used to set the position, if the mapIndex is valid * @return The world position of the matching tile * @since 5.9.0 */ - public overload extern inline function getTilePos(mapIndex:Int, midpoint = false):Null + overload public inline extern function getTilePos(mapIndex:Int, midpoint = false, ?result:FlxPoint):Null { - return tileExists(mapIndex) ? getTilePos(getColumn(mapIndex), getRow(mapIndex), midpoint) : null; + return tileExists(mapIndex) ? getTilePos(getColumn(mapIndex), getRow(mapIndex), midpoint, result) : null; } /** @@ -1072,12 +1529,15 @@ class FlxBaseTilemap extends FlxObject * @param column The grid X location, in tiles * @param row The grid Y location, in tiles * @param midpoint Whether to use the tile's midpoint, or upper left corner + * @param result The point used to set the resulting position * @return The world position of the matching tile * @since 5.9.0 */ - public overload extern inline function getTilePos(column:Int, row:Int, midpoint = false):FlxPoint + overload public inline extern function getTilePos(column:Int, row:Int, midpoint = false, ?result:FlxPoint):FlxPoint { - return FlxPoint.get(getColumnPos(column, midpoint), getRowPos(row, midpoint)); + if (result == null) + result = FlxPoint.get(); + return result.set(getColumnPos(column, midpoint), getRowPos(row, midpoint)); } /** @@ -1088,12 +1548,13 @@ class FlxBaseTilemap extends FlxObject * * @param worldPos A location in the world * @param midpoint Whether to use the tile's midpoint, or upper left corner + * @param result The point used to set the resulting position * @return The world position of the overlapping tile * @since 5.9.0 */ - public overload extern inline function getTilePos(worldPos:FlxPoint, midpoint = false):FlxPoint + overload public inline extern function getTilePos(worldPos:FlxPoint, midpoint = false, ?result:FlxPoint):FlxPoint { - return getTilePosAt(worldPos.x, worldPos.y, midpoint); + return getTilePosAt(worldPos.x, worldPos.y, midpoint, result); } /** @@ -1105,12 +1566,13 @@ class FlxBaseTilemap extends FlxObject * @param worldX An X coordinate in the world * @param worldY A Y coordinate in the world * @param midpoint Whether to use the tile's midpoint, or upper left corner + * @param result The point used to set the resulting position * @return The world position of the overlapping tile * @since 5.9.0 */ - public inline function getTilePosAt(worldX:Float, worldY:Float, midpoint = false):FlxPoint + public inline function getTilePosAt(worldX:Float, worldY:Float, midpoint = false, ?result:FlxPoint):FlxPoint { - return getTilePos(getColumnAt(worldX), getRowAt(worldY), midpoint); + return getTilePos(getColumnAt(worldX), getRowAt(worldY), midpoint, result); } /** @@ -1247,7 +1709,7 @@ class FlxBaseTilemap extends FlxObject * @return Whether or not the tile was actually changed. * @since 5.9.0 */ - public overload extern inline function setTileIndex(mapIndex:Int, tileIndex:Int, redraw = true):Bool + overload public inline extern function setTileIndex(mapIndex:Int, tileIndex:Int, redraw = true):Bool { return setTileHelper(mapIndex, tileIndex, redraw); } @@ -1262,7 +1724,7 @@ class FlxBaseTilemap extends FlxObject * @return Whether or not the tile was actually changed. * @since 5.9.0 */ - public overload extern inline function setTileIndex(column:Int, row:Int, tileIndex:Int, redraw = true):Bool + overload public inline extern function setTileIndex(column:Int, row:Int, tileIndex:Int, redraw = true):Bool { return setTileHelper(getMapIndex(column, row), tileIndex, redraw); } @@ -1276,7 +1738,7 @@ class FlxBaseTilemap extends FlxObject * @return Whether or not the tile was actually changed. * @since 5.9.0 */ - public overload extern inline function setTileIndex(worldPos:FlxPoint, tileIndex:Int, redraw = true):Bool + overload public inline extern function setTileIndex(worldPos:FlxPoint, tileIndex:Int, redraw = true):Bool { return setTileIndexAt(worldPos.x, worldPos.y, tileIndex, redraw); } @@ -1652,3 +2114,68 @@ enum FlxTilemapAutoTiling */ FULL; } + +// ============================================================================= +//{ region Ray Helpers +// ============================================================================= + +/** + * How a ray entered a given tile. It either came in through an edge, or started inside + */ +enum FlxRayEntry +{ + /** The ray entered the tile on the given edge */ + EDGE(dir:FlxDirection); + + /** The ray started in the tile */ + START; +} + +/** + * The end result of sending a ray through a tilemap. + * It will either reach it's end or be stopped by a tile + */ +enum FlxRayResult +{ + /** The ray reached a stopping tile */ + STOPPED(mapIndex:Int, x:Float, y:Float, entry:FlxRayEntry); + + /** The ray reached the end without being stopped */ + END; +} + +/** + * Internal helper used to iterate between 2 numbers, in either direction + */ +private class AmbiIntIterator +{ + final start:Int; + final iterator:IntIterator; + final step:Int; + + inline public function new(start:Int, end:Int, inclusive = true) + { + this.start = start; + this.step = start < end ? 1 : -1; + final dis = (end - start) * step + (inclusive ? 1 : 0); + iterator = (0... dis); + } + + inline public function hasNext() + { + return iterator.hasNext(); + } + + inline public function next() + { + return start + iterator.next() * step; + } + + inline static public function iter(start:Int, end:Int, inclusive = true) + { + return new AmbiIntIterator(start, end, inclusive); + } +} + +//} endregion Ray Helpers +// ============================================================================= \ No newline at end of file diff --git a/flixel/tile/FlxTilemap.hx b/flixel/tile/FlxTilemap.hx index 85d3478b32..fbeb5385b1 100644 --- a/flixel/tile/FlxTilemap.hx +++ b/flixel/tile/FlxTilemap.hx @@ -809,36 +809,16 @@ class FlxTypedTilemap extends FlxBaseTilemap return results; } - override function getColumnAt(worldX:Float, bind = false):Int + function getTileWidth() { - final result = Math.floor((worldX - x) / scaledTileWidth); - - if (bind) - return result < 0 ? 0 : (result >= widthInTiles ? widthInTiles - 1 : result); - - return result; + return scaledTileWidth; } - override function getRowAt(worldY:Float, bind = false):Int + function getTileHeight() { - final result = Math.floor((worldY - y) / scaledTileHeight); - - if (bind) - return result < 0 ? 0 : (result >= heightInTiles ? heightInTiles -1 : result); - - return result; + return scaledTileHeight; } - override function getColumnPos(column:Float, midpoint = false):Float - { - return x + column * scaledTileWidth + (midpoint ? scaledTileWidth * 0.5 : 0); - } - - override function getRowPos(row:Int, midpoint = false):Float - { - return y + row * scaledTileHeight + (midpoint ? scaledTileHeight * 0.5 : 0); - } - /** * Returns a new array full of every coordinate of the requested tile type. * @@ -874,167 +854,7 @@ class FlxTypedTilemap extends FlxBaseTilemap updateWorld ); } - - /** - * Shoots a ray from the start point to the end point. - * If/when it passes through a tile, it stores that point and returns false. - * Note: In flixel 5.0.0, this was redone, the old method is now `rayStep` - * - * @param start The world coordinates of the start of the ray. - * @param end The world coordinates of the end of the ray. - * @param result Optional result vector, to avoid creating a new instance to be returned. - * Only returned if the line enters the rect. - * @return Returns true if the ray made it from Start to End without hitting anything. - * Returns false and fills Result if a tile was hit. - */ - override function ray(start:FlxPoint, end:FlxPoint, ?result:FlxPoint):Bool - { - // trim the line to the parts inside the map - final trimmedStart = calcRayEntry(start, end); - final trimmedEnd = calcRayExit(start, end); - - start.putWeak(); - end.putWeak(); - - if (trimmedStart == null || trimmedEnd == null) - { - FlxDestroyUtil.put(trimmedStart); - FlxDestroyUtil.put(trimmedEnd); - return true; - } - - start = trimmedStart; - end = trimmedEnd; - - inline function clearRefs() - { - trimmedStart.put(); - trimmedEnd.put(); - } - - final startIndex = getMapIndex(start); - final endIndex = getMapIndex(end); - - // If the starting tile is solid, return the starting position - final tile = getTileData(startIndex); - if (tile != null && tile.solid) - { - if (result != null) - result.copyFrom(start); - - clearRefs(); - return false; - } - - final startTileX = getColumn(startIndex); - final startTileY = getRow(startIndex); - final endTileX = getColumn(endIndex); - final endTileY = getRow(endIndex); - var hitIndex = -1; - - if (start.x == end.x) - { - hitIndex = checkColumn(startTileX, startTileY, endTileY); - if (hitIndex != -1 && result != null) - { - // check the bottom - result.copyFrom(getTilePos(hitIndex)); - result.x = start.x; - if (start.y > end.y) - result.y += scaledTileHeight; - } - } - else - { - // Use y = mx + b formula - final m = (start.y - end.y) / (start.x - end.x); - // y - mx = b - final b = start.y - m * start.x; - - final movesRight = start.x < end.x; - final inc = movesRight ? 1 : -1; - final offset = movesRight ? 1 : 0; - var tileX = startTileX; - var lastTileY = startTileY; - - while (tileX != endTileX) - { - final xPos = getColumnPos(tileX + offset); - final yPos = m * getColumnPos(tileX + offset) + b; - final tileY = getRowAt(yPos); - hitIndex = checkColumn(tileX, lastTileY, tileY); - if (hitIndex != -1) - break; - lastTileY = tileY; - tileX += inc; - } - - if (hitIndex == -1) - hitIndex = checkColumn(endTileX, lastTileY, endTileY); - - if (hitIndex != -1 && result != null) - { - result.copyFrom(getTilePos(hitIndex)); - if (Std.int(hitIndex / widthInTiles) == lastTileY) - { - if (start.x > end.x) - result.x += scaledTileWidth; - - // set result to left side - result.y = m * result.x + b;//mx + b - } - else - { - // if ascending - if (start.y > end.y) - { - // change result to bottom - result.y += scaledTileHeight; - } - // otherwise result is top - - // x = (y - b)/m - result.x = (result.y - b) / m; - } - } - } - - clearRefs(); - return hitIndex == -1; - } - - function checkColumn(x:Int, startY:Int, endY:Int):Int - { - if (startY < 0) - startY = 0; - - if (endY < 0) - endY = 0; - - if (startY > heightInTiles - 1) - startY = heightInTiles - 1; - - if (endY > heightInTiles - 1) - endY = heightInTiles - 1; - - var y = startY; - final step = startY <= endY ? 1 : -1; - while (true) - { - final index = getMapIndex(x, y); - final tile = getTileData(index); - if (tile != null && tile.solid) - return index; - - if (y == endY) - break; - - y += step; - } - - return -1; - } - + /** * Shoots a ray from the start point to the end point. * If/when it passes through a tile, it stores that point and returns false. @@ -1375,6 +1195,15 @@ class FlxTypedTilemap extends FlxBaseTilemap } #end + + function orientTile(tile:Null, mapIndex:Int):Null + { + if (tile != null) + tile.orientByIndex(mapIndex); + + return tile; + } + /** * Internal function used in setTileIndex() and the constructor to update the map. * diff --git a/tests/unit/src/flixel/tile/FlxTilemapTest.hx b/tests/unit/src/flixel/tile/FlxTilemapTest.hx index ff9038ecf3..68bd528a18 100644 --- a/tests/unit/src/flixel/tile/FlxTilemapTest.hx +++ b/tests/unit/src/flixel/tile/FlxTilemapTest.hx @@ -2,7 +2,9 @@ package flixel.tile; import flixel.FlxObject; import flixel.math.FlxPoint; +import flixel.tile.FlxBaseTilemap; import flixel.util.FlxColor; +import flixel.util.FlxDirection; import flixel.util.FlxDirectionFlags; import haxe.PosInfos; import massive.munit.Assert; @@ -11,11 +13,13 @@ import openfl.errors.ArgumentError; using StringTools; +// null safety breaks on 4.2 +#if (haxe >= version("4.3.0")) @:nullSafety(Strict) #end class FlxTilemapTest extends FlxTest { - var tilemap:FlxTilemap; - var sampleMapString:String; - var sampleMapArray:Array; + var tilemap:FlxTilemap = new FlxTilemap(); + var sampleMapString:String = ""; + var sampleMapArray:Array = []; @Before function before() @@ -243,6 +247,147 @@ class FlxTilemapTest extends FlxTest Assert.isFalse(tilemap.ray(new FlxPoint(0, 0), new FlxPoint(tilemap.width, tilemap.height))); } + @Test // #1617 + function testRayAdvanced() + { + var mapData = [ + 0, 0, 0, + 0, 1, 0, + 0, 0, 0 + ]; + tilemap.loadMapFromArray(mapData, 3, 3, getBitmapData(), 8, 8); + + final startP = FlxPoint.get(); + final endP = FlxPoint.get(); + + function assertRay(expected:FlxRayResult, start:FlxPoint, end:FlxPoint, checkDir = true, ?msg:String, ?pos:PosInfos) + { + final actual = tilemap.rayAdvanced(start, end, checkDir); + if (actual.equals(expected)) + Assert.assertionCount++; + else if (msg != null) + Assert.fail(msg, pos); + else + Assert.fail('Expected rayAdvanced($start, $end, $checkDir)to be ${resultToString(expected)}, got ${resultToString(actual)}', pos); + } + + inline function getPos(index:Int, result:FlxPoint) + { + if (tilemap.getTilePos(index, true, result) == null) + throw 'Expected valid tile at index $index'; + + return result; + } + inline function setStart(index:Int) return getPos(index, startP); + inline function setEnd (index:Int) return getPos(index, endP ); + + function assertRayStopped(start:FlxPoint, end:FlxPoint, checkDir = true, ?msg:String, ?pos:PosInfos) + { + // Test start and end verbatim, then offset x and test again to ensure both pure-vertical and sloped tests + final result = tilemap.rayAdvanced(start, end, checkDir); + if (result.match(STOPPED(_, _, _, _))) + Assert.assertionCount++; + else if (msg != null) + Assert.fail(msg, pos); + else + Assert.fail('Expected rayAdvanced($start, $end, $checkDir) to be STOPPED', pos); + } + + function assertRayNotStopped(start:FlxPoint, end:FlxPoint, checkDir = true, ?msg:String, ?pos:PosInfos) + { + assertRay(END, start, end, checkDir, 'Expected rayAdvanced($start, $end, $checkDir) to be NOT STOPPED', pos); + } + + tilemap.setTileProperties(1, ANY); + + final tl = 0; final tc = 1; final tr = 2; + final lc = 3; final c = 4; final rc = 5; + final bl = 6; final bc = 7; final br = 8; + + assertRayNotStopped(setStart(tl), setEnd(tr)); + assertRayNotStopped(setStart(tr), setEnd(tl)); + assertRayNotStopped(setStart(tl), setEnd(bl)); + assertRayNotStopped(setStart(bl), setEnd(tl)); + assertRayNotStopped(setStart(bl), setEnd(br)); + assertRayNotStopped(setStart(br), setEnd(bl)); + assertRayNotStopped(setStart(tr), setEnd(br)); + assertRayNotStopped(setStart(br), setEnd(tr)); + + // For all purely vertical tests, also check at a slight angle, since the code is different + assertRayNotStopped(setStart(tl).subtract(1, 0), setEnd(bl)); + assertRayNotStopped(setStart(bl).subtract(1, 0), setEnd(tl)); + assertRayNotStopped(setStart(tr).subtract(1, 0), setEnd(br)); + assertRayNotStopped(setStart(br).subtract(1, 0), setEnd(tr)); + + function testDirections(allowCollisions:FlxDirectionFlags, ?pos) + { + tilemap.setTileProperties(1, allowCollisions); + function assertRaySimplified(dir:FlxDirection, x, y, start, end, label:String, ?pos) + { + final expected = allowCollisions.has(dir) ? STOPPED(c, x, y, EDGE(dir)) : END; + final actual = tilemap.rayAdvanced(setStart(start), setEnd(end), true); + if (actual.equals(expected)) + Assert.assertionCount++; + else + Assert.fail('Expected rayAdvanced[$label]($startP, $endP) ' + + 'to be ${resultToString(expected)}, got ${resultToString(actual)}', pos); + } + + // check pure vertical and horizontal + assertRaySimplified(LEFT , 8, 12, lc, rc, "L->R", pos); + assertRaySimplified(RIGHT, 16, 12, rc, lc, "R->L", pos); + assertRaySimplified(UP , 12, 8, tc, bc, "T->B", pos); + assertRaySimplified(DOWN, 12, 16, bc, tc, "B->T", pos); + + // then check at various slopes + assertRaySimplified(UP , 14, 8, tc, br, "T->BR", pos); + assertRaySimplified(UP , 10, 8, tc, bl, "T->BL", pos); + assertRaySimplified(DOWN, 14, 16, bc, tr, "B->TR", pos); + assertRaySimplified(DOWN, 10, 16, bc, tl, "B->TL", pos); + } + + testDirections(ANY); + testDirections(ANY.without(LEFT )); + testDirections(ANY.without(RIGHT)); + testDirections(ANY.without(UP )); + testDirections(ANY.without(DOWN )); + testDirections(LEFT .with(RIGHT)); + testDirections(LEFT .with(UP )); + testDirections(LEFT .with(DOWN )); + testDirections(RIGHT.with(UP )); + testDirections(RIGHT.with(DOWN )); + testDirections(DOWN .with(UP )); + testDirections(LEFT ); + testDirections(RIGHT); + testDirections(UP ); + testDirections(DOWN ); + testDirections(NONE); + + // checkDir:false (always a hit if the tile allows any conditions) + tilemap.setTileProperties(1, UP); + + // check pure vertical and horizontal + assertRay(STOPPED(c, 8, 12, EDGE(LEFT )), setStart(lc), setEnd(rc), false); + assertRay(STOPPED(c, 16, 12, EDGE(RIGHT)), setStart(rc), setEnd(lc), false); + assertRay(STOPPED(c, 12, 8, EDGE(UP )), setStart(tc), setEnd(bc), false); + assertRay(STOPPED(c, 12, 16, EDGE(DOWN )), setStart(bc), setEnd(tc), false); + + // then check at various slopes + assertRay(STOPPED(c, 14, 8, EDGE(UP )), setStart(tc), setEnd(br), false); + assertRay(STOPPED(c, 10, 8, EDGE(UP )), setStart(tc), setEnd(bl), false); + assertRay(STOPPED(c, 14, 16, EDGE(DOWN)), setStart(bc), setEnd(tr), false); + assertRay(STOPPED(c, 10, 16, EDGE(DOWN)), setStart(bc), setEnd(tl), false); + } + + function resultToString(result:FlxRayResult) + { + return switch result + { + case STOPPED(index, x, y, EDGE(dir)): 'STOPPED($index, $x, $y, EDGE($dir))'; + default: Std.string(result); + } + } + @Test function testNegativeIndicesTreatedAsZero() { @@ -444,6 +589,11 @@ class FlxTilemapTest extends FlxTest tilemap.y += 20; tilemap.scale.set(2, 2); + Assert.areEqual(16, tilemap.scaledTileWidth); + Assert.areEqual(16, tilemap.scaledTileHeight); + Assert.areEqual(16, tilemap.getTileWidth()); + Assert.areEqual(16, tilemap.getTileHeight()); + final size = 16; final half = 8; @@ -486,6 +636,31 @@ class FlxTilemapTest extends FlxTest Assert.areEqual(4, tilemap.getRowAt(10 + 24, false)); } + @Test + function testGetColumnRowPosAt() + { + final mapData = [ + 0, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 0, 0, + ]; + tilemap.x += 10; + tilemap.y += 20; + tilemap.loadMapFromArray(mapData, 4, 3, getBitmapData(), 8, 8); + + Assert.areEqual(10 + 24 , tilemap.getColumnPosAt(10 + 24)); + Assert.areEqual(10 + 32 , tilemap.getColumnPosAt(10 + 32 , false)); + Assert.areEqual(10 + 32 + 4, tilemap.getColumnPosAt(10 + 32 , true)); + Assert.areEqual(10 + 32 , tilemap.getColumnPosAt(10 + 32 + 4, false)); + Assert.areEqual(10 + 32 + 4, tilemap.getColumnPosAt(10 + 32 + 4, true)); + + Assert.areEqual(20 + 16 , tilemap.getRowPosAt(20 + 16)); + Assert.areEqual(20 + 24 , tilemap.getRowPosAt(20 + 24 , false)); + Assert.areEqual(20 + 24 + 4, tilemap.getRowPosAt(20 + 24 , true)); + Assert.areEqual(20 + 24 , tilemap.getRowPosAt(20 + 24 + 4, false)); + Assert.areEqual(20 + 24 + 4, tilemap.getRowPosAt(20 + 24 + 4, true)); + } + @Test function testColumnRowPos() { @@ -501,6 +676,14 @@ class FlxTilemapTest extends FlxTest Assert.areEqual(1, tilemap.getColumnAt(tilemap.getColumnPos(1))); Assert.areEqual(2, tilemap.getColumnAt(tilemap.getColumnPos(2))); Assert.areEqual(3, tilemap.getColumnAt(tilemap.getColumnPos(3))); + Assert.areEqual(0, tilemap.getColumnAt(10 + 0 * size)); + Assert.areEqual(1, tilemap.getColumnAt(10 + 1 * size)); + Assert.areEqual(2, tilemap.getColumnAt(10 + 2 * size)); + Assert.areEqual(3, tilemap.getColumnAt(10 + 3 * size)); + Assert.areEqual(0, tilemap.getColumnAt(10 + 0 * size + half)); + Assert.areEqual(1, tilemap.getColumnAt(10 + 1 * size + half)); + Assert.areEqual(2, tilemap.getColumnAt(10 + 2 * size + half)); + Assert.areEqual(3, tilemap.getColumnAt(10 + 3 * size + half)); Assert.areEqual(1000, tilemap.getColumnAt(tilemap.getColumnPos(1000))); Assert.areEqual(10 + 3 * size, tilemap.getColumnPos(3)); Assert.areEqual(10 + 3 * size + half, tilemap.getColumnPos(3, true)); @@ -509,13 +692,22 @@ class FlxTilemapTest extends FlxTest Assert.areEqual(0, tilemap.getRowAt(tilemap.getRowPos(0))); Assert.areEqual(1, tilemap.getRowAt(tilemap.getRowPos(1))); Assert.areEqual(2, tilemap.getRowAt(tilemap.getRowPos(2))); + Assert.areEqual(0, tilemap.getRowAt(20 + 0 * size)); + Assert.areEqual(1, tilemap.getRowAt(20 + 1 * size)); + Assert.areEqual(2, tilemap.getRowAt(20 + 2 * size)); + Assert.areEqual(0, tilemap.getRowAt(20 + 0 * size + half)); + Assert.areEqual(1, tilemap.getRowAt(20 + 1 * size + half)); + Assert.areEqual(2, tilemap.getRowAt(20 + 2 * size + half)); Assert.areEqual(1000, tilemap.getRowAt(tilemap.getRowPos(1000))); Assert.areEqual(20 + 2 * size, tilemap.getRowPos(2)); Assert.areEqual(20 + 2 * size + half, tilemap.getRowPos(2, true)); Assert.areEqual(20 + 1000 * size, tilemap.getRowPos(1000)); - Assert.areEqual(null, tilemap.getTilePos(1000)); + final testPoint = FlxPoint.get(100, 50); + Assert.areEqual(null, tilemap.getTilePos(1000)); // null is returned + FlxAssert.pointsEqualXY(100, 50, testPoint); // result is unchanged Assert.areEqual(null, tilemap.getTilePos(-1)); + FlxAssert.pointsEqualXY(100, 50, testPoint); inline function assertPosEqual(expectedX:Float, expectedY:Float, actual:FlxPoint, ?infos:PosInfos) @@ -653,28 +845,46 @@ class FlxTilemapTest extends FlxTest @Test function testOrientDelta() { - final mapData = [0]; - tilemap.loadMapFromArray(mapData, 1, 1, getBitmapData(), 8, 8); + final mapData = [ + 0, 0, 0, + 0, 1, 0, + 0, 0, 0 + ]; + tilemap.loadMapFromArray(mapData, 3, 3, getBitmapData(), 8, 8); step(); tilemap.x = 0; tilemap.last.x = 0; final tile = tilemap.getTileData(0); tile.orient(0, 0); - - Assert.areEqual(tile.x, tile.last.x); + Assert.areEqual(0, tile.last.x); Assert.areEqual(0, tile.x); - tilemap.last.x = 10; + final tile = tilemap.getTileData(4, true);// get oriented + Assert.areEqual(8, tile.last.x); + Assert.areEqual(8, tile.x); + + tilemap.last.set(0, 5); tilemap.x = 10; - tile.orient(0, 0); + tilemap.y = 10; + final tile = tilemap.getTileData(1, 1, true);// get oriented - Assert.areEqual(tilemap.x - tilemap.last.x, tile.x - tile.last.x); - Assert.areEqual(tile.x, tile.last.x); + Assert.areEqual(8, tile.last.x); + Assert.areEqual(13, tile.last.y); + Assert.areEqual(18, tile.x); + Assert.areEqual(18, tile.y); + + final tileA = tilemap.getTileData(1, 1); + final tileB = tilemap.getTileData(4); + final tileC = tilemap.getTileDataAt(22, 22); + final tileD = tilemap.getTileData(FlxPoint.weak(22, 22)); + Assert.areEqual(tileA, tileB); + Assert.areEqual(tileA, tileC); + Assert.areEqual(tileA, tileD); + Assert.areNotEqual(null, tileA); } @Test - @:haxe.warning("-WDeprecated") function testNegativeIndex() { final mapData = [ @@ -694,7 +904,7 @@ class FlxTilemapTest extends FlxTest var overlapResult = true; var rayResult = false; var rayStepResult = false; - var getIndexResult:FlxTile = null; + var getIndexResult:Null = null; try { overlapResult = tilemap.overlaps(object); @@ -713,6 +923,489 @@ class FlxTilemapTest extends FlxTest Assert.isNull(getIndexResult); } + @Test + function testForEachInRay() + { + final mapData = [ + // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 - Map indices + 0, 1, 2, 3, 0, 0, 0, 4, 5, 6, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]; + tilemap.loadMapFromArray(mapData, 10, 2, getBitmapData(), 8, 8); + tilemap.setTileIndex(4, -1); + tilemap.setTileIndex(5, -1); + tilemap.setTileIndex(6, -1); + + // Test all overloads + var callCount = 0; + var nullCount = 0; + tilemap.forEachInRay(FlxPoint.weak(4, 4), FlxPoint.weak(84, 4), function (index, tile, entry) + { + final isNullIndex = index >= 4 && index <= 6; + Assert.areEqual(isNullIndex, tile == null, 'Expected index $index to be, ${isNullIndex ? "null" : "non-null"}'); + if (isNullIndex) + nullCount++; + + final expectedEntry = index == 0 ? START : EDGE(LEFT); + Assert.isTrue(entry.equals(expectedEntry), 'Expected entry $expectedEntry got $entry'); + Assert.areEqual(callCount, index, 'Reached index $index without reaching index $callCount'); + callCount++; + }); + Assert.areEqual(10, callCount); + Assert.areEqual(3, nullCount); + + // Test left skip ends + var expectedIndex = 8; + tilemap.forEachInRay(FlxPoint.weak(68, 4), FlxPoint.weak(14, 4), function (index, tile, entry) + { + Assert.isTrue(index == 8 ? entry.equals(START) : entry.equals(EDGE(RIGHT))); + Assert.areEqual(expectedIndex, index, tile == null ? null : 'Reached tile $index at ${tile.x} | ${tile.y}'); + expectedIndex--; + }); + Assert.areEqual(0, expectedIndex); + } + + @Test + function testFindInRay() + { + final mapData = [ + // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 - columns + 0, 1, 2, 3, 0, 0, 0, 4, 5, 6, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]; + tilemap.loadMapFromArray(mapData, 10, 2, getBitmapData(), 8, 8); + tilemap.setTileIndex(4, -1); + tilemap.setTileIndex(5, -1); + tilemap.setTileIndex(6, -1); + + // Test all overloads + var callCount = 0; + var nullCount = 0; + final result = tilemap.findInRay(FlxPoint.weak(4, 4), FlxPoint.weak(76, 4), function (index, tile, entry) + { + final isNullIndex = index >= 4 && index <= 6; + Assert.areEqual(isNullIndex, tile == null, 'Expected index $index to be, ${isNullIndex ? "null" : "non-null"}'); + if (isNullIndex) + nullCount++; + + final expectedEntry = index == 0 ? START : EDGE(LEFT); + Assert.isTrue(entry.equals(expectedEntry), 'Expected entry $expectedEntry got $entry'); + Assert.areEqual(callCount, index, 'Reached index $index without reaching index $callCount'); + ++callCount; + return index == 9; + }); + final expectedResult = STOPPED(9, 72, 4, EDGE(LEFT)); + Assert.isTrue(result.equals(expectedResult), 'Expected ${resultToString(expectedResult)}, got ${resultToString(result)}'); + Assert.areEqual(3, nullCount); + + callCount = 0; + final result = tilemap.findInRay(FlxPoint.weak(4, 4), FlxPoint.weak(76, 4), function (_, _, _) + { + callCount++; + return false; + }); + Assert.isTrue(result.equals(END), 'Expected END, got ${resultToString(result)}'); + Assert.areEqual(10, callCount); + + // Test left skip ends + var expectedIndex = 8; + final result = tilemap.findInRay(FlxPoint.weak(68, 4), FlxPoint.weak(14, 4), function (index:Int, tile:Null, entry:FlxRayEntry) + { + final expectedEntry = index == 8 ? START : EDGE(RIGHT); + Assert.isTrue(entry.equals(expectedEntry), 'Expected entry $expectedEntry got $entry'); + Assert.areEqual(expectedIndex, index, tile == null ? null : 'Reached tile $index at ${tile.x} | ${tile.y}'); + expectedIndex--; + return index == 1; + }); + final expectedResult = STOPPED(1, 16, 4, EDGE(RIGHT)); + Assert.isTrue(result.equals(expectedResult), 'Expected ${resultToString(expectedResult)}, got ${resultToString(result)}'); + Assert.areEqual(0, expectedIndex); + } + + @Test + function testFindIndexInRay() + { + final mapData = [ + // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 - columns + 0, 1, 2, 3, 0, 0, 0, 4, 5, 6, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]; + tilemap.loadMapFromArray(mapData, 10, 2, getBitmapData(), 8, 8); + tilemap.setTileIndex(4, -1); + tilemap.setTileIndex(5, -1); + tilemap.setTileIndex(6, -1); + + // Test all overloads + var callCount = 0; + var nullCount = 0; + final result = tilemap.findIndexInRay(FlxPoint.weak(4, 4), FlxPoint.weak(76, 4), function (index:Int, tile:Null, entry:FlxRayEntry) + { + final isNullIndex = index >= 4 && index <= 6; + Assert.areEqual(isNullIndex, tile == null, 'Expected index $index to be, ${isNullIndex ? "null" : "non-null"}'); + if (isNullIndex) + nullCount++; + + final expectedEntry = index == 0 ? START : EDGE(LEFT); + Assert.isTrue(entry.equals(expectedEntry), 'Expected entry $expectedEntry got $entry'); + Assert.areEqual(callCount, index, 'Reached index $index without reaching index $callCount'); + ++callCount; + return index == 9; + }); + Assert.areEqual(3, nullCount); + Assert.areEqual(9, result); + + callCount = 0; + final result = tilemap.findIndexInRay(FlxPoint.weak(4, 4), FlxPoint.weak(76, 4), function (_, _, _) + { + callCount++; + return false; + }); + Assert.areEqual(-1, result); + Assert.areEqual(10, callCount); + + var expectedIndex = 8; + final result = tilemap.findIndexInRay(FlxPoint.weak(68, 4), FlxPoint.weak(14, 4), function (index, _, entry) + { + final expectedEntry = index == 8 ? START : EDGE(RIGHT); + Assert.isTrue(entry.equals(expectedEntry), 'Expected entry $expectedEntry got $entry'); + + Assert.areEqual(expectedIndex, index, 'Expected index $expectedIndex, got $index'); + expectedIndex--; + return index == 1; + }); + Assert.areEqual(1, result); + Assert.areEqual(0, expectedIndex); + } + + @Test + function testForEachInRow() + { + final mapData = [ + // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 - Map indices + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 2, 3, 0, 0, 0, 4, 5, 6 + ]; + tilemap.loadMapFromArray(mapData, 10, 2, getBitmapData(), 8, 8); + tilemap.setTileIndex(14, -1); + tilemap.setTileIndex(15, -1); + tilemap.setTileIndex(16, -1); + + try + { + tilemap.forEachInRow(2, 0, 8, (_, _)->{}); + Assert.fail("Expected error t be thrown for invalid row"); + } + catch(e) + { + Assert.assertionCount++; + } + + // Test all overloads + var callCount = 0; + var nullCount = 0; + tilemap.forEachInRow(1, 0, 100, function (index:Int, tile:Null) + { + final isNullIndex = index >= 14 && index <= 16; + Assert.areEqual(isNullIndex, tile == null, 'Expected index $index to be, ${isNullIndex ? "null" : "non-null"}'); + if (isNullIndex) + nullCount++; + + Assert.areEqual(callCount, index - 10, 'Reached index $index without reaching index $callCount'); + callCount++; + }); + Assert.areEqual(10, callCount); + Assert.areEqual(3, nullCount); + + var expectedIndex = 18; + tilemap.forEachInRow(1, 8, 1, function (index, _) + { + Assert.areEqual(expectedIndex, index, 'Expected index $expectedIndex, got $index'); + expectedIndex--; + }); + Assert.areEqual(10, expectedIndex); + } + + @Test + function testFindInRow() + { + final mapData = [ + // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 - Map indices + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 2, 3, 0, 0, 0, 4, 5, 6 + ]; + tilemap.loadMapFromArray(mapData, 10, 2, getBitmapData(), 8, 8); + tilemap.setTileIndex(14, -1); + tilemap.setTileIndex(15, -1); + tilemap.setTileIndex(16, -1); + + try + { + tilemap.findInRow(2, 0, 8, (_, _)->false); + Assert.fail("Expected error t be thrown for invalid row"); + } + catch(e) + { + Assert.assertionCount++; + } + + // Test all overloads + var callCount = 0; + var nullCount = 0; + final result = tilemap.findInRow(1, 0, 100, function (index:Int, tile:Null) + { + final isNullIndex = index >= 14 && index <= 16; + Assert.areEqual(isNullIndex, tile == null, 'Expected index $index to be, ${isNullIndex ? "null" : "non-null"}'); + if (isNullIndex) + nullCount++; + + Assert.areEqual(callCount, index - 10, 'Reached index $index without reaching index $callCount'); + callCount++; + return index == 19; + }); + Assert.isNotNull(result); + if (result == null) throw "check needed for null safety"; + Assert.areEqual(72, result.x); + Assert.areEqual(8, result.y); + Assert.areEqual(10, callCount); + Assert.areEqual(3, nullCount); + + var expectedIndex = 18; + tilemap.findInRow(1, 8, 1, function (index, _) + { + Assert.areEqual(expectedIndex, index, 'Expected index $expectedIndex, got $index'); + expectedIndex--; + return false; + }); + Assert.areEqual(10, expectedIndex); + } + + @Test + function testFindIndexInRow() + { + final mapData = [ + // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 - Map indices + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 2, 3, 0, 0, 0, 4, 5, 6 + ]; + tilemap.loadMapFromArray(mapData, 10, 2, getBitmapData(), 8, 8); + tilemap.setTileIndex(14, -1); + tilemap.setTileIndex(15, -1); + tilemap.setTileIndex(16, -1); + + try + { + tilemap.findIndexInRow(2, 0, 8, (i,t)->false); + Assert.fail("Expected error t be thrown for invalid row"); + } + catch(e) + { + Assert.assertionCount++; + } + + // Test all overloads + var callCount = 0; + var nullCount = 0; + final result = tilemap.findIndexInRow(1, 0, 100, function (index:Int, tile:Null) + { + final isNullIndex = index >= 14 && index <= 16; + Assert.areEqual(isNullIndex, tile == null, 'Expected index $index to be, ${isNullIndex ? "null" : "non-null"}'); + if (isNullIndex) + nullCount++; + + Assert.areEqual(callCount, index - 10, 'Reached index $index without reaching index $callCount'); + callCount++; + return index == 19; + }); + Assert.areEqual(10, callCount); + Assert.areEqual(3, nullCount); + Assert.areEqual(19, result); + + var expectedIndex = 18; + final result = tilemap.findIndexInRow(1, 8, 1, function (index, _) + { + Assert.areEqual(expectedIndex, index, 'Expected index $expectedIndex, got $index'); + expectedIndex--; + return false; + }); + Assert.areEqual(10, expectedIndex); + } + + @Test + function testFindInColumn() + { + final mapData = [ + 0, 0, // 0 + 0, 1, // 1 + 0, 2, // 2 + 0, 3, // 3 + 0, 0, // 4 + 0, 0, // 5 + 0, 0, // 6 + 0, 4, // 7 + 0, 5, // 8 + 0, 6 // 9 + ]; + tilemap.loadMapFromArray(mapData, 2, 10, getBitmapData(), 8, 8); + tilemap.setTileIndex(9, -1); + tilemap.setTileIndex(11, -1); + tilemap.setTileIndex(13, -1); + + try + { + tilemap.findInColumn(2, 0, 8, (i,t)->false); + Assert.fail("Expected error t be thrown for invalid row"); + } + catch(e) + { + Assert.assertionCount++; + } + + // Test all overloads + var callCount = 0; + var nullCount = 0; + final result = tilemap.findInColumn(1, 0, 100, function (index, tile) + { + final isNullIndex = index == 9 || index == 11 || index == 13; + Assert.areEqual(isNullIndex, tile == null, 'Expected index $index to be, ${isNullIndex ? "null" : "non-null"}'); + if (isNullIndex) + nullCount++; + + Assert.areEqual(index, callCount * 2 + 1, 'Reached index $index without reaching index $callCount'); + callCount++; + return index == 19; + }); + Assert.isNotNull(result); + if (result == null) throw "check needed for null safety"; + Assert.areEqual(8, result.x); + Assert.areEqual(72, result.y); + Assert.areEqual(10, callCount); + Assert.areEqual(3, nullCount); + + var expectedIndex = 17; + final result = tilemap.findInColumn(1, 8, 1, function (index, _) + { + Assert.areEqual(expectedIndex, index, 'Expected index $expectedIndex, got $index'); + expectedIndex -= 2; + return false; + }); + Assert.areEqual(1, expectedIndex); + } + + + @Test + function testFindIndexInColumn() + { + final mapData = [ + 0, 0, // 0 + 0, 1, // 1 + 0, 2, // 2 + 0, 3, // 3 + 0, 0, // 4 + 0, 0, // 5 + 0, 0, // 6 + 0, 4, // 7 + 0, 5, // 8 + 0, 6 // 9 + ]; + tilemap.loadMapFromArray(mapData, 2, 10, getBitmapData(), 8, 8); + tilemap.setTileIndex(9, -1); + tilemap.setTileIndex(11, -1); + tilemap.setTileIndex(13, -1); + + try + { + tilemap.findIndexInColumn(2, 0, 8, (i,t)->false); + Assert.fail("Expected error t be thrown for invalid row"); + } + catch(e) + { + Assert.assertionCount++; + } + + // Test all overloads + var callCount = 0; + var nullCount = 0; + final result = tilemap.findIndexInColumn(1, 0, 100, function (index, tile) + { + final isNullIndex = index == 9 || index == 11 || index == 13; + Assert.areEqual(isNullIndex, tile == null, 'Expected index $index to be, ${isNullIndex ? "null" : "non-null"}'); + if (isNullIndex) + nullCount++; + + Assert.areEqual(index, callCount * 2 + 1, 'Reached index $index without reaching index $callCount'); + callCount++; + return index == 19; + }); + Assert.isNotNull(19); + Assert.areEqual(10, callCount); + Assert.areEqual(3, nullCount); + + var expectedIndex = 17; + final result = tilemap.findIndexInColumn(1, 8, 1, function (index, _) + { + Assert.areEqual(expectedIndex, index, 'Expected index $expectedIndex, got $index'); + expectedIndex -= 2; + return false; + }); + Assert.areEqual(-1, result); + Assert.areEqual(1, expectedIndex); + } + + @Test + function testForEachInColumn() + { + final mapData = [ + 0, 0, // 0 + 0, 1, // 1 + 0, 2, // 2 + 0, 3, // 3 + 0, 0, // 4 + 0, 0, // 5 + 0, 0, // 6 + 0, 4, // 7 + 0, 5, // 8 + 0, 6 // 9 + ]; + tilemap.loadMapFromArray(mapData, 2, 10, getBitmapData(), 8, 8); + tilemap.setTileIndex(9, -1); + tilemap.setTileIndex(11, -1); + tilemap.setTileIndex(13, -1); + + try + { + tilemap.forEachInColumn(2, 0, 8, (i,t)->false); + Assert.fail("Expected error t be thrown for invalid row"); + } + catch(e) + { + Assert.assertionCount++; + } + + // Test all overloads + var callCount = 0; + var nullCount = 0; + tilemap.forEachInColumn(1, 0, 100, function (index, tile) + { + final isNullIndex = index == 9 || index == 11 || index == 13; + Assert.areEqual(isNullIndex, tile == null, 'Expected index $index to be, ${isNullIndex ? "null" : "non-null"}'); + if (isNullIndex) + nullCount++; + + Assert.areEqual(index, callCount * 2 + 1, 'Reached index $index without reaching index $callCount'); + callCount++; + }); + Assert.areEqual(10, callCount); + Assert.areEqual(3, nullCount); + + var expectedIndex = 17; + tilemap.forEachInColumn(1, 8, 1, function (index, _) + { + Assert.areEqual(expectedIndex, index, 'Expected index $expectedIndex, got $index'); + expectedIndex -= 2; + }); + Assert.areEqual(1, expectedIndex); + } + function getBitmapData() { return new BitmapData(8*16, 8);