From ac6cc5ebabac3f1410da312634fee8b6f72be1a5 Mon Sep 17 00:00:00 2001 From: George Kurelic Date: Thu, 9 May 2024 14:47:36 -0500 Subject: [PATCH 01/22] Replay duration (#3135) * add FlxReplay getDuration * style * add 1 to frame duration * add docs * add unit test --- flixel/input/keyboard/FlxKeyboard.hx | 14 +++++++------- flixel/system/replay/FlxReplay.hx | 18 ++++++++++++++++++ .../src/flixel/system/replay/FlxReplayTest.hx | 9 +++++++++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/flixel/input/keyboard/FlxKeyboard.hx b/flixel/input/keyboard/FlxKeyboard.hx index c70ddc1f07..a21a40fe73 100644 --- a/flixel/input/keyboard/FlxKeyboard.hx +++ b/flixel/input/keyboard/FlxKeyboard.hx @@ -168,16 +168,16 @@ class FlxKeyboard extends FlxKeyManager * @param Record Array of data about key states. */ @:allow(flixel.system.replay.FlxReplay) - function playback(Record:Array):Void + function playback(record:Array):Void { - var i:Int = 0; - var l:Int = Record.length; + var i = 0; + final len = record.length; - while (i < l) + while (i < len) { - var o = Record[i++]; - var o2 = getKey(o.code); - o2.current = o.value; + final keyRecord = record[i++]; + final key = getKey(keyRecord.code); + key.current = keyRecord.value; } } } diff --git a/flixel/system/replay/FlxReplay.hx b/flixel/system/replay/FlxReplay.hx index 6771ba789e..013092f684 100644 --- a/flixel/system/replay/FlxReplay.hx +++ b/flixel/system/replay/FlxReplay.hx @@ -24,6 +24,7 @@ class FlxReplay /** * The number of frames in this recording. + * **Note:** This doesn't include empty records, unlike `getDuration()` */ public var frameCount:Int; @@ -241,4 +242,21 @@ class FlxReplay FlxArrayUtil.setLength(_frames, _capacity); frameCount = 0; } + + /** + * The duration of this replay, in frames. **Note:** this is different from `frameCount`, which + * is the number of unique records, which doesn't count frames with no input + * + * @since 5.9.0 + */ + public function getDuration() + { + if (_frames != null) + { + // Add 1 to the last frame index, because they are zero-based + return _frames[_frames.length - 1].frame + 1; + } + + return 0; + } } diff --git a/tests/unit/src/flixel/system/replay/FlxReplayTest.hx b/tests/unit/src/flixel/system/replay/FlxReplayTest.hx index 85a6d564a5..094465c074 100644 --- a/tests/unit/src/flixel/system/replay/FlxReplayTest.hx +++ b/tests/unit/src/flixel/system/replay/FlxReplayTest.hx @@ -102,6 +102,15 @@ class FlxReplayTest extends FlxTest { return new FrameRecord().create(i, null, new MouseRecord(0, 0, mouseState, 0)); } + + @Test // #3135 + function testGetDuration() + { + var replay = new FlxReplay(); + replay.load("987654321\n299km0,0,2,0\n"); + // add 1 because frame indices are zero-based + Assert.areEqual(300, replay.getDuration()); + } } class ReplayState extends FlxState From fd3eff9bd10a52c4a33937446a230a3c5b680390 Mon Sep 17 00:00:00 2001 From: George Kurelic Date: Thu, 9 May 2024 17:46:45 -0500 Subject: [PATCH 02/22] allow multiple inputs of the same type (#3134) * allow multiple inputs of the same type * fix error * use addInput * add InputFrontEnd tests --- flixel/FlxG.hx | 27 +++--- flixel/system/frontEnds/InputFrontEnd.hx | 95 +++++++++---------- tests/unit/src/FlxAssert.hx | 20 ++++ .../system/frontEnds/InputFrontEndTest.hx | 70 ++++++++++++++ 4 files changed, 150 insertions(+), 62 deletions(-) create mode 100644 tests/unit/src/flixel/system/frontEnds/InputFrontEndTest.hx diff --git a/flixel/FlxG.hx b/flixel/FlxG.hx index d98913926a..705819cc52 100644 --- a/flixel/FlxG.hx +++ b/flixel/FlxG.hx @@ -630,23 +630,23 @@ class FlxG // Instantiate inputs #if FLX_KEYBOARD - keys = inputs.add(new FlxKeyboard()); + keys = inputs.addInput(new FlxKeyboard()); #end #if FLX_MOUSE - mouse = inputs.add(new FlxMouse(game._inputContainer)); + mouse = inputs.addInput(new FlxMouse(game._inputContainer)); #end #if FLX_TOUCH - touches = inputs.add(new FlxTouchManager()); + touches = inputs.addInput(new FlxTouchManager()); #end #if FLX_GAMEPAD - gamepads = inputs.add(new FlxGamepadManager()); + gamepads = inputs.addInput(new FlxGamepadManager()); #end #if android - android = inputs.add(new FlxAndroidKeys()); + android = inputs.addInput(new FlxAndroidKeys()); #end #if FLX_ACCELEROMETER @@ -756,21 +756,24 @@ class FlxG } #if FLX_MOUSE - static function set_mouse(NewMouse:FlxMouse):FlxMouse + static function set_mouse(newMouse:FlxMouse):FlxMouse { - if (mouse == null) // if no mouse, just add it + // if there's no mouse, add it + if (mouse == null) { - mouse = inputs.add(NewMouse); // safe to do b/c it won't add repeats! + mouse = inputs.addUniqueType(newMouse); return mouse; } - var oldMouse:FlxMouse = mouse; - var result:FlxMouse = inputs.replace(oldMouse, NewMouse); // replace existing mouse + + // replace existing mouse + final oldMouse:FlxMouse = mouse; + final result:FlxMouse = inputs.replace(oldMouse, newMouse, true); if (result != null) { mouse = result; - oldMouse.destroy(); - return NewMouse; + return newMouse; } + return oldMouse; } #end diff --git a/flixel/system/frontEnds/InputFrontEnd.hx b/flixel/system/frontEnds/InputFrontEnd.hx index ebbdf387b7..c1249e5ccf 100644 --- a/flixel/system/frontEnds/InputFrontEnd.hx +++ b/flixel/system/frontEnds/InputFrontEnd.hx @@ -22,76 +22,71 @@ class InputFrontEnd /** * Add an input to the system - * - * @param Input The input to add - * @return The input */ @:generic - public function add(Input:T):T + @:deprecated("add is deprecated, use addUniqueType") + public inline function add(input:T):T { - // Don't add repeats - for (input in list) - { - if (FlxStringUtil.sameClassName(Input, input)) - { - return Input; - } - } + return addUniqueType(input); + } + + /** + * Add an input to the system, unless the same instance was already added + */ + @:generic + public function addInput(input:T):T + { + if (!list.contains(input)) + list.push(input); + + return input; + } - list.push(Input); - return Input; + /** + * Add an input to the system, unless the same type was already added + */ + @:generic + public function addUniqueType(input:T):T + { + // Don't add repeat types + if (!Lambda.exists(list, FlxStringUtil.sameClassName.bind(_, input))) + list.push(input); + + return input; } /** * Removes an input from the system * - * @param Input The input to remove - * @return Bool indicating whether it was removed or not + * @param Input The input to remove + * @return Bool indicating whether it was removed or not */ @:generic - public function remove(Input:T):Bool + public inline function remove(input:T):Bool { - var i:Int = 0; - for (input in list) - { - if (input == Input) - { - list.splice(i, 1); - return true; - } - i++; - } - return false; + return list.remove(input); } /** * Replace an existing input in the system with a new one * - * @param Old The old input to replace - * @param New The new input to put in its place - * @return If successful returns New. Otherwise returns null. + * @param oldInput The old input to replace + * @param newInput The new input to put in its place + * @param destroyOld Whether to destroy the old input + * @return If successful returns `newInput`. Otherwise returns `null`. */ @:generic - public function replace(Old:T, New:T):T + public function replace(oldInput:T, newInput:T, destroyOld = false):Null { - var i:Int = 0; - var success:Bool = false; - for (input in list) - { - if (input == Old) - { - list[i] = New; // Replace Old with New - success = true; - break; - } - i++; - } - - if (success) - { - return New; - } - return null; + final index = list.indexOf(oldInput); + if (index == -1) + return null; + + if (destroyOld) + oldInput.destroy(); + + list[index] = newInput; + return newInput; } public function reset():Void diff --git a/tests/unit/src/FlxAssert.hx b/tests/unit/src/FlxAssert.hx index 0d5b174a88..1c501493ac 100644 --- a/tests/unit/src/FlxAssert.hx +++ b/tests/unit/src/FlxAssert.hx @@ -59,6 +59,26 @@ class FlxAssert Assert.fail('\nValue\n ${actual}\nwas equal to\n ${expected}\n', info); } + public static function arrayContains(array:Array, item:T, ?msg:String, ?info:PosInfos):Void + { + if (array.contains(item)) + Assert.assertionCount++; + else if (msg != null) + Assert.fail(msg, info); + else + Assert.fail('\nValue\n ${item}\nwas not found in array\n ${array}\n', info); + } + + public static function arrayNotContains(array:Array, item:T, ?msg:String, ?info:PosInfos):Void + { + if (!array.contains(item)) + Assert.assertionCount++; + else if (msg != null) + Assert.fail(msg, info); + else + Assert.fail('\nValue\n ${item}\nwas found in array\n ${array}\n', info); + } + public static function pointsEqual(expected:FlxPoint, actual:FlxPoint, ?msg:String, ?info:PosInfos) { if (expected.equals(actual)) diff --git a/tests/unit/src/flixel/system/frontEnds/InputFrontEndTest.hx b/tests/unit/src/flixel/system/frontEnds/InputFrontEndTest.hx new file mode 100644 index 0000000000..43e6272e59 --- /dev/null +++ b/tests/unit/src/flixel/system/frontEnds/InputFrontEndTest.hx @@ -0,0 +1,70 @@ +package flixel.system.frontEnds; + +import flixel.system.frontEnds.InputFrontEnd; +import flixel.FlxG; +import flixel.input.IFlxInputManager; +import massive.munit.Assert; + +class InputFrontEndTest +{ + var inputs:InputFrontEnd; + + @Before + function before():Void + { + @:privateAccess + inputs = new InputFrontEnd(); + } + + @Test + @:haxe.warning("-WDeprecated") + function testAdd() + { + final input1 = new CustomInputManager(); + inputs.add(input1); + FlxAssert.arrayContains(inputs.list, input1); + + final input2 = new CustomInputManager(); + inputs.add(input2); + FlxAssert.arrayNotContains(inputs.list, input2); + } + + @Test + function testAddUniqueType() + { + final input1 = new CustomInputManager(); + inputs.addUniqueType(input1); + FlxAssert.arrayContains(inputs.list, input1); + + final input2 = new CustomInputManager(); + inputs.addUniqueType(input2); + FlxAssert.arrayNotContains(inputs.list, input2); + } + + @Test + function testAddInput() + { + final input1 = new CustomInputManager(); + inputs.addInput(input1); + FlxAssert.arrayContains(inputs.list, input1); + + final oldLength = inputs.list.length; + // add again + inputs.addInput(input1); + Assert.areEqual(inputs.list.length, oldLength); + + final input2 = new CustomInputManager(); + inputs.addInput(input2); + FlxAssert.arrayContains(inputs.list, input2); + } +} + +class CustomInputManager implements IFlxInputManager +{ + public function new () {} + public function destroy() {} + public function reset():Void {} + function update():Void {} + function onFocus():Void {} + function onFocusLost():Void {} +} From d2f3ae252ab6dc570c963b05cc6da7578ff101ff Mon Sep 17 00:00:00 2001 From: George Kurelic Date: Wed, 15 May 2024 15:46:47 -0500 Subject: [PATCH 03/22] fix docs (#3139) * fix docs * fix typo --- flixel/system/frontEnds/InputFrontEnd.hx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/flixel/system/frontEnds/InputFrontEnd.hx b/flixel/system/frontEnds/InputFrontEnd.hx index c1249e5ccf..5a3e0e60b4 100644 --- a/flixel/system/frontEnds/InputFrontEnd.hx +++ b/flixel/system/frontEnds/InputFrontEnd.hx @@ -49,9 +49,15 @@ class InputFrontEnd public function addUniqueType(input:T):T { // Don't add repeat types - if (!Lambda.exists(list, FlxStringUtil.sameClassName.bind(_, input))) - list.push(input); + for (i in list) + { + if (FlxStringUtil.sameClassName(input, i, false)) + { + return input; + } + } + list.push(input); return input; } From 05f510cf5cddfa0751a851309df966cf405bfc83 Mon Sep 17 00:00:00 2001 From: ACrazyTown <47027981+ACrazyTown@users.noreply.github.com> Date: Tue, 21 May 2024 22:08:05 +0200 Subject: [PATCH 04/22] Add onVolumeChange to SoundFrontEnd (#3148) * Add onVolumeChange to SoundFrontEnd * Move import inside conditional * deprecate old func * simplify import * remove deprecation warnings * fix typo in @:deprecated --------- Co-authored-by: George Kurelic --- flixel/system/frontEnds/SoundFrontEnd.hx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/flixel/system/frontEnds/SoundFrontEnd.hx b/flixel/system/frontEnds/SoundFrontEnd.hx index 4abab4d04a..0a5e6bb07f 100644 --- a/flixel/system/frontEnds/SoundFrontEnd.hx +++ b/flixel/system/frontEnds/SoundFrontEnd.hx @@ -9,6 +9,7 @@ import flixel.system.FlxAssets; import flixel.sound.FlxSound; import flixel.sound.FlxSoundGroup; import flixel.system.ui.FlxSoundTray; +import flixel.util.FlxSignal; import openfl.Assets; import openfl.media.Sound; #if (openfl >= "8.0.0") @@ -35,8 +36,14 @@ class SoundFrontEnd * Set this hook to get a callback whenever the volume changes. * Function should take the form myVolumeHandler(volume:Float). */ + @:deprecated("volumeHandler is deprecated, use onVolumeChange, instead") public var volumeHandler:Float->Void; + /** + * A signal that gets dispatched whenever the volume changes. + */ + public var onVolumeChange(default, null):FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + #if FLX_KEYBOARD /** * The key codes used to increase volume (see FlxG.keys for the keys available). @@ -328,6 +335,7 @@ class SoundFrontEnd /** * Toggles muted, also activating the sound tray. */ + @:haxe.warning("-WDeprecated") public function toggleMuted():Void { muted = !muted; @@ -337,6 +345,8 @@ class SoundFrontEnd volumeHandler(muted ? 0 : volume); } + onVolumeChange.dispatch(muted ? 0 : volume); + showSoundTray(true); } @@ -448,15 +458,18 @@ class SoundFrontEnd } #end + @:haxe.warning("-WDeprecated") function set_volume(Volume:Float):Float { Volume = FlxMath.bound(Volume, 0, 1); if (volumeHandler != null) { - var param:Float = muted ? 0 : Volume; - volumeHandler(param); + volumeHandler(muted ? 0 : Volume); } + + onVolumeChange.dispatch(muted ? 0 : Volume); + return volume = Volume; } } From 2af1238ae9dc623f83d9443868504f0078a2a7ce Mon Sep 17 00:00:00 2001 From: George Kurelic Date: Tue, 28 May 2024 23:50:35 -0500 Subject: [PATCH 05/22] easier extension of FlxTypedTilemap and FlxTile (#3154) --- flixel/tile/FlxTile.hx | 5 ++-- flixel/tile/FlxTilemap.hx | 52 +++++++++++++++++++++++---------------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/flixel/tile/FlxTile.hx b/flixel/tile/FlxTile.hx index a0270ec393..fe7b9c8fe7 100644 --- a/flixel/tile/FlxTile.hx +++ b/flixel/tile/FlxTile.hx @@ -2,6 +2,7 @@ package flixel.tile; import flixel.FlxObject; import flixel.graphics.frames.FlxFrame; +import flixel.tile.FlxTilemap; import flixel.util.FlxDirectionFlags; /** @@ -29,7 +30,7 @@ class FlxTile extends FlxObject /** * A reference to the tilemap this tile object belongs to. */ - public var tilemap:FlxTilemap; + public var tilemap:FlxTypedTilemap; /** * The index of this tile type in the core map data. @@ -60,7 +61,7 @@ class FlxTile extends FlxObject * @param visible Whether the tile is visible or not. * @param allowCollisions The collision flags for the object. By default this value is ANY or NONE depending on the parameters sent to loadMap(). */ - public function new(tilemap:FlxTilemap, index:Int, width:Float, height:Float, visible:Bool, allowCollisions:FlxDirectionFlags) + public function new(tilemap:FlxTypedTilemap, index:Int, width:Float, height:Float, visible:Bool, allowCollisions:FlxDirectionFlags) { super(0, 0, width, height); diff --git a/flixel/tile/FlxTilemap.hx b/flixel/tile/FlxTilemap.hx index ff0aaf16dd..473646d6b8 100644 --- a/flixel/tile/FlxTilemap.hx +++ b/flixel/tile/FlxTilemap.hx @@ -129,28 +129,10 @@ class FlxTilemap extends FlxTypedTilemap { super(); } - - override function initTileObjects():Void + + override function createTile(index, width, height, visible, allowCollisions):FlxTile { - if (frames == null) - return; - - _tileObjects = FlxDestroyUtil.destroyArray(_tileObjects); - // Create some tile objects that we'll use for overlap checks (one for each tile) - _tileObjects = new Array(); - - var length:Int = frames.numFrames; - length += _startingIndex; - - for (i in 0...length) - _tileObjects[i] = new FlxTile(this, i, tileWidth, tileHeight, (i >= _drawIndex), (i >= _collideIndex) ? allowCollisions : NONE); - - // Create debug tiles for rendering bounding boxes on demand - #if FLX_DEBUG - updateDebugTileBoundingBoxSolid(); - updateDebugTileBoundingBoxNotSolid(); - updateDebugTileBoundingBoxPartial(); - #end + return new FlxTile(this, index, width, height, visible, allowCollisions); } } @@ -384,7 +366,35 @@ class FlxTypedTilemap extends FlxBaseTilemap super.destroy(); } + + override function initTileObjects():Void + { + if (frames == null) + return; + + _tileObjects = FlxDestroyUtil.destroyArray(_tileObjects); + // Create some tile objects that we'll use for overlap checks (one for each tile) + _tileObjects = []; + + var length:Int = frames.numFrames; + length += _startingIndex; + + for (i in 0...length) + _tileObjects[i] = createTile(i, tileWidth, tileHeight, (i >= _drawIndex), (i >= _collideIndex) ? allowCollisions : NONE); + // Create debug tiles for rendering bounding boxes on demand + #if FLX_DEBUG + updateDebugTileBoundingBoxSolid(); + updateDebugTileBoundingBoxNotSolid(); + updateDebugTileBoundingBoxPartial(); + #end + } + + function createTile(index, width, height, visible, allowCollisions):Tile + { + throw "createTile not implemented"; + } + function set_frames(value:FlxFramesCollection):FlxFramesCollection { frames = value; From f3034134319cad76bd6fbfa7a9d1709944e166c3 Mon Sep 17 00:00:00 2001 From: George Kurelic Date: Thu, 30 May 2024 08:51:32 -0500 Subject: [PATCH 06/22] add FlxBasePath, extends base in FlxPath (#3153) * add FlxBasePath, extends base in FlxPath * codeclimate fix * prevent breaking changes + doc * more cc * fix debug drawing * rename loop to loopType * rename CenterMode to FlxPathAnchorMode * docs * more doc * doc and deprecations * use non-null ints for indices * restartPath(dir) -> restart() * move writeable fields above readonly * remove direction from startAt * add direction to constructor --- flixel/FlxObject.hx | 10 +- flixel/path/FlxBasePath.hx | 461 +++++++++++++++++++++++++ flixel/path/FlxPath.hx | 688 ++++++++++++++----------------------- 3 files changed, 728 insertions(+), 431 deletions(-) create mode 100644 flixel/path/FlxBasePath.hx diff --git a/flixel/FlxObject.hx b/flixel/FlxObject.hx index fa268062cb..4d2c077117 100644 --- a/flixel/FlxObject.hx +++ b/flixel/FlxObject.hx @@ -1266,12 +1266,16 @@ class FlxObject extends FlxBasic if (ignoreDrawDebug) return; + final drawPath = path != null && !path.ignoreDrawDebug; + for (camera in getCamerasLegacy()) { drawDebugOnCamera(camera); - - if (path != null && !path.ignoreDrawDebug) - path.drawDebug(); + + if (drawPath) + { + path.drawDebugOnCamera(camera); + } } } diff --git a/flixel/path/FlxBasePath.hx b/flixel/path/FlxBasePath.hx new file mode 100644 index 0000000000..758d59c8c6 --- /dev/null +++ b/flixel/path/FlxBasePath.hx @@ -0,0 +1,461 @@ +package flixel.path; + +import flixel.FlxBasic; +import flixel.FlxG; +import flixel.FlxObject; +import flixel.math.FlxPoint; +import flixel.util.FlxAxes; +import flixel.util.FlxColor; +import flixel.util.FlxDestroyUtil; +import flixel.util.FlxSignal; +import flixel.util.FlxSpriteUtil; +import openfl.display.Graphics; + +/** + * A simple ordered list of nodes that iterates based on conditions. For this class, + * that condition is not defined, and must be implemented in your extending class. + * + * ## Example + * The following is an example of a class that moves the target to the next node and + * and advances the iterator when it is near. +```haxe +class SimplePath extends flixel.path.FlxBasePath +{ + public var speed:Float; + + public function new (?nodes, ?target, speed = 100.0) + { + this.speed = speed; + super(nodes, target); + } + + override function isTargetAtNext(elapsed:Float):Bool + { + final frameSpeed = elapsed * speed; + final deltaX = next.x - target.x; + final deltaY = next.y - target.y; + // Whether the distance remaining is less than the distance we will travel this frame + return Math.sqrt(deltaX * deltaX + deltaY * deltaY) <= frameSpeed; + } + + override function updateTarget(elapsed:Float) + { + // Aim velocity towards the next node then set magnitude to the desired speed + target.velocity.set(next.x - target.x, next.y - target.y); + target.velocity.length = speed; + } +} +``` + * + * @since 5.9.0 + */ +typedef FlxBasePath = FlxTypedBasePath; + +/** + * Typed version of `FlxBasePath` for flexibility in derived classes + * + * @see flixel.path.FlxBasePath + * @since 5.9.0 + */ +class FlxTypedBasePath extends FlxBasic implements IFlxDestroyable +{ + /** The list of points that make up the path data */ + public var nodes:Array; + + /** The target traversing our path */ + public var target:TTarget; + + /** Behavior when the end(s) are reached */ + public var loopType:FlxPathLoopType = LOOP; + + /** The direction the list of nodes is being traversed. `FORWARD` leads to the last node */ + public var direction(default, null) = FlxPathDirection.FORWARD; + + /** The length of the `nodes` array */ + public var totalNodes(get, never):Int; + + /** Whether this path is done, only `true` when `loopType` is `ONCE` */ + public var finished(get, never):Bool; + + /** Called whenenever the end is reached, for `YOYO` this means both ends */ + public var onEndReached(default, null) = new FlxTypedSignal<(FlxTypedBasePath)->Void>(); + + /** Called whenenever any node reached */ + public var onNodeReached(default, null) = new FlxTypedSignal<(FlxTypedBasePath)->Void>(); + + /** Called when the end is reached and `loopType1 is `ONCE` */ + public var onFinish(default, null) = new FlxTypedSignal<(FlxTypedBasePath)->Void>(); + + /** The index of the last node the target has reached, `-1` means "no current node" */ + public var currentIndex(default, null):Int = -1; + /** The index of the node the target is currently moving toward, `-1` means the path is finished */ + public var nextIndex(default, null):Int = -1; + + /** The last node the target has reached */ + public var current(get, never):Null; + /** The node the target is currently moving toward */ + public var next(get, never):Null; + + /** + * Creates a new path. If valid nodes and a target are given it will start immediately. + * + * @param nodes An Optional array of nodes. Unlike `FlxPath`, no copy is made + * @param target The target traversing our path + */ + public function new (?nodes:Array, ?target:TTarget, direction = FORWARD) + { + this.nodes = nodes; + this.target = target; + super(); + + if (nodes != null && nodes.length > 0 && target != null) + restart(); + } + + override function destroy():Void + { + FlxDestroyUtil.putArray(nodes); + nodes = null; + onEndReached.removeAll(); + } + + /** + * Sets the current node to the beginning, or the end if `direction` is `BACKWARD` + */ + public function restart():FlxTypedBasePath + { + currentIndex = getStartingNode(); + setNextIndex(); + + return this; + } + + function getStartingNode() + { + return direction == BACKWARD ? nodes.length - 1 : 0; + } + + function nodeReached() + { + advance(); + + onNodeReached.dispatch(this); + + if (finished) + { + onFinish.dispatch(this); + } + } + + /** Iterates to the next node according to the desired `direction` */ + function advance() + { + if (finished) + { + FlxG.log.warn('Cannot advance after path is finished'); + return; + } + + currentIndex = nextIndex; + setNextIndex(); + } + + /** + * Determines the next index based on the current index and direction. + * Fires onEndReached if the end is reached + */ + function setNextIndex() + { + // reached last + if (currentIndex == nodes.length - 1 && direction == FORWARD) + { + nextIndex = switch (loopType) + { + case ONCE: -1; + case LOOP: 0; + case YOYO: + direction = BACKWARD; + currentIndex - 1; + } + onEndReached.dispatch(this); + return; + } + + // reached first + if (currentIndex == 0 && direction == BACKWARD) + { + nextIndex = switch (loopType) + { + case ONCE: -1; + case LOOP: nodes.length - 1; + case YOYO: + direction = FORWARD; + currentIndex + 1; + } + onEndReached.dispatch(this); + return; + } + + nextIndex = currentIndex + direction.toInt(); + } + + /** + * Change the path node this object is currently at. + * + * @param index The index of the new node out of path.nodes. + * @param direction Whether to head towards the head or the tail, if `null` the previous + * value is maintained + */ + public function startAt(index:Int):FlxTypedBasePath + { + currentIndex = index; + setNextIndex(); + + return this; + } + + // Following logic + + override function update(elapsed:Float) + { + super.update(elapsed); + + if (finished || target == null) + return; + + if (isTargetAtNext(elapsed)) + { + nodeReached(); + if (finished) + return; + } + + updateTarget(elapsed); + } + + /** Override this with your logic that whether the target has reached the next node */ + function isTargetAtNext(elapsed:Float):Bool + { + throw 'isTargetAtNext is not implemented'; + } + + /** Override this with your logic that brings the target towards the next node */ + function updateTarget(elapsed:Float) {} + + inline function get_totalNodes() + { + return nodes != null ? nodes.length : 0; + } + + inline function get_finished() + { + return nextIndex < 0; + } + + inline function get_current() + { + return nodes != null && currentIndex >= 0 ? nodes[currentIndex] : null; + } + + inline function get_next() + { + return nodes != null && nextIndex >= 0? nodes[nextIndex] : null; + } + + /** + * Determines to which camera this will draw (or debug draw). The priority is from high to low: + * - Whatever value you've manually given the `cameras` or `camera` field + * - Any cameras drawing path's `container`, if one exists + * - Any cameras drawing path's `target`, if one exists + * - The default cameras + */ + override function getCameras():Array + { + return if (_cameras != null) + _cameras; + else if (container != null) + container.getCameras(); + else if (target != null) + target.getCameras(); + else + @:privateAccess FlxCamera._defaultCameras; + } + + #if FLX_DEBUG + /** + * Specify a debug display color for the path. Default is WHITE. + */ + public var debugDrawData:FlxPathDrawData = {}; + + /** + * Setting this to true will prevent the object from appearing + * when FlxG.debugger.drawDebug is true. + */ + public var ignoreDrawDebug:Bool = false; + + override function draw() + { + // super.draw(); + + if (FlxG.debugger.drawDebug && !ignoreDrawDebug) + { + FlxBasic.visibleCount++; + + for (camera in getCameras()) + { + drawDebugOnCamera(camera); + } + } + } + + /** + * Based on this path data, it draws a simple lines-and-boxes representation of the path + * if the `drawDebug` mode was toggled in the debugger overlay. + * You can use `debugColor` to control the path's appearance. + * + * @param camera The camera object the path will draw to. + */ + public function drawDebugOnCamera(camera:FlxCamera):Void + { + // Set up our global flash graphics object to draw out the path + var gfx:Graphics = null; + if (FlxG.renderBlit) + { + gfx = FlxSpriteUtil.flashGfx; + gfx.clear(); + } + else + { + gfx = camera.debugLayer.graphics; + } + + final length = nodes.length; + // Then fill up the object with node and path graphics + for (i=>node in nodes) + { + // find the screen position of the node on this camera + final prevNodeScreen = copyWorldToScreenPos(node, camera); + + // decide what color this node should be + var nodeSize:Int = debugDrawData.nodeSize; + var nodeColor:FlxColor = debugDrawData.nodeColor; + if (length > 1) + { + if (i == 0) + { + nodeColor = debugDrawData.startColor; + nodeSize = debugDrawData.startSize; + } + else if (i == length - 1) + { + nodeColor = debugDrawData.endColor; + nodeSize = debugDrawData.endSize; + } + } + + // draw a box for the node + drawNode(gfx, prevNodeScreen, nodeSize, nodeColor); + + if (i + 1 < length || loopType == LOOP) + { + // draw a line to the next node, if LOOP, get connect the tail and head + final nextNode = nodes[(i + 1) % length]; + final nextNodeScreen = copyWorldToScreenPos(nextNode, camera); + drawLine(gfx, prevNodeScreen, nextNodeScreen); + nextNodeScreen.put(); + } + prevNodeScreen.put(); + } + + if (FlxG.renderBlit) + { + // then stamp the path down onto the game buffer + camera.buffer.draw(FlxSpriteUtil.flashGfxSprite); + } + } + + @:access(flixel.FlxCamera) + function copyWorldToScreenPos(point:FlxPoint, camera:FlxCamera, ?result:FlxPoint) + { + result = point.clone(result); + if (target is FlxObject) + { + final object:FlxObject = cast target; + result.x -= camera.scroll.x * object.scrollFactor.x; + result.y -= camera.scroll.y * object.scrollFactor.y; + } + + if (FlxG.renderBlit) + { + result.x -= camera.viewMarginX; + result.y -= camera.viewMarginY; + } + + camera.transformPoint(result); + return result; + } + + inline function drawNode(gfx:Graphics, node:FlxPoint, size:Int, color:FlxColor) + { + gfx.beginFill(color.rgb, color.alphaFloat); + gfx.lineStyle(); + final offset = Math.floor(size * 0.5); + gfx.drawRect(node.x - offset, node.y - offset, size, size); + gfx.endFill(); + } + + function drawLine(gfx:Graphics, node1:FlxPoint, node2:FlxPoint) + { + // then draw a line to the next node + final color = debugDrawData.lineColor; + final size = debugDrawData.lineSize; + gfx.lineStyle(size, color.rgb, color.alphaFloat); + + final lineOffset = debugDrawData.lineSize / 2; + gfx.moveTo(node1.x + lineOffset, node1.y + lineOffset); + gfx.lineTo(node2.x + lineOffset, node2.y + lineOffset); + } + #end +} + +/** Path behavior for when an end is reached */ +enum abstract FlxPathLoopType(Int) from Int to Int +{ + /** Stops when reaching the end */ + var ONCE = 0x000000; + + /** When the end is reached, go back to the other end and start again */ + var LOOP = 0x000010; + + /** When the end is reached, change direction and continue */ + var YOYO = 0x001000; +} + +/** The direction to traverse the nodes */ +enum abstract FlxPathDirection(Bool) +{ + /** Head towards the last node in the array */ + var FORWARD = true; + + /** Head towards the first node in the array */ + var BACKWARD = false; + + public inline function toInt() + { + return this ? 1 : -1; + } +} + +/** The drawing scheme of a path's debug draw */ +@:structInit +class FlxPathDrawData +{ + public var lineColor = FlxColor.WHITE; + public var nodeColor = FlxColor.WHITE; + public var startColor = FlxColor.GREEN; + public var endColor = FlxColor.RED; + public var lineSize = 1; + public var nodeSize = 3; + public var startSize = 5; + public var endSize = 5; +} diff --git a/flixel/path/FlxPath.hx b/flixel/path/FlxPath.hx index c72b026673..ccfe6b0c80 100644 --- a/flixel/path/FlxPath.hx +++ b/flixel/path/FlxPath.hx @@ -3,16 +3,19 @@ package flixel.path; import flixel.FlxG; import flixel.FlxObject; import flixel.math.FlxPoint; +import flixel.path.FlxBasePath; import flixel.util.FlxAxes; import flixel.util.FlxColor; import flixel.util.FlxDestroyUtil; import flixel.util.FlxSpriteUtil; import openfl.display.Graphics; +typedef CenterMode = FlxPathAnchorMode; /** - * CenterMode defines how an object should be placed when following the path. + * Determines an object position in relation to the path */ -enum CenterMode + @:using(flixel.path.FlxPath.AnchorTools) +enum FlxPathAnchorMode { /** @@ -37,6 +40,42 @@ enum CenterMode CUSTOM(offset:FlxPoint); } +private class AnchorTools +{ + public static function computeAnchor(mode:FlxPathAnchorMode, object:FlxObject, ?result:FlxPoint):FlxPoint + { + result = computeAnchorOffset(mode, object, result); + return result.add(object.x, object.y); + } + + public static function computeAnchorOffset(mode:FlxPathAnchorMode, object:FlxObject, ?result:FlxPoint):FlxPoint + { + if (result == null) + result = FlxPoint.get(); + else + result.set(); + + return switch (mode) + { + case ORIGIN: + if (object is FlxSprite) + { + result.add(cast(object, FlxSprite).origin.x, cast(object, FlxSprite).origin.y); + } + else + { + result; + } + case CENTER: + result.add(object.width * 0.5, object.height * 0.5); + case TOP_LEFT: + result; + case CUSTOM(offset): + result.addPoint(offset); + } + } +} + /** * This is a simple path data container. Basically a list of points that * a `FlxObject` can follow. Also has code for drawing debug visuals. @@ -72,7 +111,7 @@ enum CenterMode * object.path = new FlxPath([new FlxPoint(0, 0), new FlxPoint(100, 0)]).start(); * ``` */ -class FlxPath implements IFlxDestroyable +class FlxPath extends FlxBasePath { /** * Move from the start of the path to the end then stop. @@ -80,28 +119,28 @@ class FlxPath implements IFlxDestroyable @:deprecated("Use FORWARD or FlxPathType.FORWARD instead") @:noCompletion public static inline var FORWARD = FlxPathType.FORWARD; - + /** * Move from the end of the path to the start then stop. */ @:deprecated("Use BACKWARD or FlxPathType.BACKWARD instead") @:noCompletion public static inline var BACKWARD = FlxPathType.BACKWARD; - + /** * Move from the start of the path to the end then directly back to the start, and start over. */ @:deprecated("Use LOOP_FORWARD or FlxPathType.LOOP_FORWARD instead") @:noCompletion public static inline var LOOP_FORWARD = FlxPathType.LOOP_FORWARD; - + /** * Move from the end of the path to the start then directly back to the end, and start over. */ @:deprecated("Use LOOP_BACKWARD or FlxPathType.LOOP_BACKWARD instead") @:noCompletion public static inline var LOOP_BACKWARD = FlxPathType.LOOP_BACKWARD; - + /** * Move from the start of the path to the end then turn around and go back to the start, over and over. */ @@ -114,16 +153,6 @@ class FlxPath implements IFlxDestroyable */ static var _point:FlxPoint = FlxPoint.get(); - /** - * The list of FlxPoints that make up the path data. - */ - public var nodes(get, set):Array; - - /** - * An actual array, which holds all the path points. - */ - var _nodes:Array; - /** * The speed at which the object is moving on the path. * When an object completes a non-looping path circuit, @@ -132,33 +161,34 @@ class FlxPath implements IFlxDestroyable * to check if this object is currently following a path or not. */ public var speed:Float = 0; - + /** * Whether to make the object immovable while active. */ public var immovable(default, set):Bool = false; - + /** * The angle in degrees between this object and the next node, where -90 is directly upward, and 0 is to the right. */ public var angle(default, null):Float = 0; - + /** * Legacy method of alignment for the object following the path. If true, align the midpoint of the object on the path, else use the x, y position. */ @:deprecated("path.autoCenter is deprecated, use centerMode") // 5.7.0 public var autoCenter(get, set):Bool; - + /** * How to center the object on the path. + * @since 5.7.0 */ - public var centerMode:CenterMode = CENTER; - + public var centerMode:FlxPathAnchorMode = CENTER; + /** * Whether the object's angle should be adjusted to the path angle during path follow behavior. */ public var autoRotate:Bool = false; - + /** * The amount of degrees to offset from the path's angle, when `autoRotate` is `true`. To use * flixel 4.11's autoRotate behavior, set this to `90`, so there is no rotation at 0 degrees. @@ -167,48 +197,32 @@ class FlxPath implements IFlxDestroyable * @since 5.0.0 */ public var angleOffset:Float = 0; - - /** - * Pauses or checks the pause state of the path. - */ - public var active:Bool = false; - + + @:deprecated("onComplete is deprecated, use the onEndReached signal, instead") public var onComplete:FlxPath->Void; - - #if FLX_DEBUG - /** - * Specify a debug display color for the path. Default is WHITE. - */ - public var debugDrawData:FlxPathDrawData = {}; - - /** - * Setting this to true will prevent the object from appearing - * when FlxG.debugger.drawDebug is true. - */ - public var ignoreDrawDebug:Bool = false; - #end - + /** * Tracks which node of the path this object is currently moving toward. */ - public var nodeIndex(default, null):Int = 0; - - public var finished(default, null):Bool = false; - + @:deprecated("nodeIndex is deprecated, use nextIndex, instead") + public var nodeIndex(get, never):Int; + /** * Whether to limit movement to certain axes. */ public var axes:FlxAxes = XY; - + /** * Internal tracker for path behavior flags (like looping, yoyo, etc). */ - var _mode:FlxPathType; - + @:noCompletion + var _mode(get, set):FlxPathType; + /** * Internal helper for node navigation, specifically yo-yo and backwards movement. */ - var _inc:Int = 1; + @:noCompletion + var _inc(get, set):Int; var _wasObjectImmovable:Null = null; @@ -218,14 +232,19 @@ class FlxPath implements IFlxDestroyable * Object which will follow this path */ @:allow(flixel.FlxObject) - var object:FlxObject; - + var object(get, set):FlxObject; + + @:haxe.warning("-WDeprecated") public function new(?nodes:Array) { - if (nodes != null) - _nodes = nodes.copy(); - else - _nodes = []; + super(nodes != null ? nodes.copy() : []); + + active = false; + onEndReached.add(function (_) + { + if (onComplete != null) + onComplete(this); + }); } /** @@ -279,15 +298,17 @@ class FlxPath implements IFlxDestroyable { if (nodesAsReference) { - _nodes = nodes; + this.nodes = nodes; } else { - _nodes = nodes.copy(); + this.nodes = nodes.copy(); } } + setProperties(speed, mode, autoRotate); - if (_nodes.length > 0) + + if (this.nodes.length > 0) { restart(); } @@ -300,70 +321,50 @@ class FlxPath implements IFlxDestroyable * * @return This path object. */ - public function restart():FlxPath + override function restart():FlxPath { - finished = false; - _firstUpdate = true; - active = _nodes.length > 0; - if (!active) - { - return this; - } - - // get starting node - if ((_mode == FlxPathType.BACKWARD) || (_mode == FlxPathType.LOOP_BACKWARD)) - { - nodeIndex = _nodes.length - 1; - _inc = -1; - } - else - { - nodeIndex = 0; - _inc = 1; - } - + super.restart(); + active = nodes.length > 0; return this; } /** * Change the path node this object is currently at. * - * @param NodeIndex The index of the new node out of path.nodes. + * @param nodeIndex The index of the new node out of path.nodes. */ public function setNode(nodeIndex:Int):FlxPath { - if (nodeIndex < 0) - nodeIndex = 0; - else if (nodeIndex > _nodes.length - 1) - nodeIndex = _nodes.length - 1; - - this.nodeIndex = nodeIndex; - advancePath(); + startAt(nodeIndex); return this; } function computeCenter(point:FlxPoint):FlxPoint { - point.x = object.x; - point.y = object.y; - return switch (centerMode) + return centerMode.computeAnchor(object, point); + } + + override function isTargetAtNext(elapsed:Float):Bool + { + // first check if we need to be pointing at the next node yet + final center = computeCenter(FlxPoint.get()); + final deltaX = next.x - center.x; + final deltaY = next.y - center.y; + center.put(); + + inline function abs(n:Float) return n > 0 ? n : -n; + + if (axes == X) { - case ORIGIN: - if (object is FlxSprite) - { - point.add(cast(object, FlxSprite).origin.x, cast(object, FlxSprite).origin.y); - } - else - { - point; - } - case CENTER: - point.add(object.width * 0.5, object.height * 0.5); - case TOP_LEFT: - point; - case CUSTOM(offset): - point.addPoint(offset); + return abs(deltaX) < speed * elapsed; } + + if (axes == Y) + { + return abs(deltaY) < speed * elapsed; + } + + return Math.sqrt(deltaX * deltaX + deltaY * deltaY) < speed * elapsed; } /** @@ -371,11 +372,8 @@ class FlxPath implements IFlxDestroyable * The first half of the function decides if the object can advance to the next node in the path, * while the second half handles actually picking a velocity toward the next node. */ - public function update(elapsed:Float):Void + override function updateTarget(elapsed:Float):Void { - if (object == null) - return; - if (_firstUpdate) { if (immovable) @@ -386,65 +384,29 @@ class FlxPath implements IFlxDestroyable _firstUpdate = false; } - // first check if we need to be pointing at the next node yet + // then just move toward the current node at the requested speed + if (speed == 0) + return; + + // set velocity based on path mode _point = computeCenter(_point); + final node = next; - var node:FlxPoint = _nodes[nodeIndex]; - var deltaX:Float = node.x - _point.x; - var deltaY:Float = node.y - _point.y; - - var horizontalOnly:Bool = axes == X; - var verticalOnly:Bool = axes == Y; - - if (horizontalOnly) - { - if (((deltaX > 0) ? deltaX : -deltaX) < speed * elapsed) - { - node = advancePath(); - } - } - else if (verticalOnly) + if (!_point.equals(node)) { - if (((deltaY > 0) ? deltaY : -deltaY) < speed * elapsed) - { - node = advancePath(); - } + calculateVelocity(node, axes == X, axes == Y); } else { - if (Math.sqrt(deltaX * deltaX + deltaY * deltaY) < speed * elapsed) - { - node = advancePath(); - } + object.velocity.set(); } - // then just move toward the current node at the requested speed - if (object != null && speed != 0) + // then set object rotation if necessary + if (autoRotate) { - // set velocity based on path mode - _point = computeCenter(_point); - - if (!_point.equals(node)) - { - calculateVelocity(node, horizontalOnly, verticalOnly); - } - else - { - object.velocity.set(); - } - - // then set object rotation if necessary - if (autoRotate) - { - object.angularVelocity = 0; - object.angularAcceleration = 0; - object.angle = angle + angleOffset; - } - - if (finished) - { - cancel(); - } + object.angularVelocity = 0; + object.angularAcceleration = 0; + object.angle = angle + angleOffset; } } @@ -485,123 +447,70 @@ class FlxPath implements IFlxDestroyable */ function advancePath(snap:Bool = true):FlxPoint { - if (snap) - { - var oldNode:FlxPoint = _nodes[nodeIndex]; - if (oldNode != null) - { - if (axes.x) - { - object.x = oldNode.x; - switch (centerMode) - { - case ORIGIN: - if (object is FlxSprite) - object.x -= (cast object:FlxSprite).origin.x; - case CUSTOM(offset): - object.x -= offset.x; - case CENTER: - object.x -= object.width * 0.5; - case TOP_LEFT: - } - } - if (axes.y) - { - object.y = oldNode.y; - switch (centerMode) - { - case ORIGIN: - if (object is FlxSprite) - object.y -= (cast object:FlxSprite).origin.y; - case CUSTOM(offset): - object.y -= offset.y; - case CENTER: - object.y -= object.height * 0.5; - case TOP_LEFT: - } - } - } - } - - var callComplete:Bool = false; - nodeIndex += _inc; - - if (_mode == FlxPathType.BACKWARD) - { - if (nodeIndex < 0) - { - nodeIndex = 0; - callComplete = true; - onEnd(); - } - } - else if (_mode == FlxPathType.LOOP_FORWARD) - { - if (nodeIndex >= _nodes.length) - { - callComplete = true; - nodeIndex = 0; - } - } - else if (_mode == FlxPathType.LOOP_BACKWARD) - { - if (nodeIndex < 0) - { - nodeIndex = _nodes.length - 1; - callComplete = true; - if (nodeIndex < 0) - { - nodeIndex = 0; - } - } - } - else if (_mode == FlxPathType.YOYO) + advance(); + + return current; + } + + override function advance() + { + if (axes.x) { - if (_inc > 0) + object.x = next.x; + switch (centerMode) { - if (nodeIndex >= _nodes.length) - { - nodeIndex = _nodes.length - 2; - callComplete = true; - if (nodeIndex < 0) - { - nodeIndex = 0; - } - _inc = -_inc; - } - } - else if (nodeIndex < 0) - { - nodeIndex = 1; - callComplete = true; - if (nodeIndex >= _nodes.length) - { - nodeIndex = _nodes.length - 1; - } - if (nodeIndex < 0) - { - nodeIndex = 0; - } - _inc = -_inc; + case ORIGIN: + if (object is FlxSprite) + object.x -= (cast object:FlxSprite).origin.x; + case CUSTOM(offset): + object.x -= offset.x; + case CENTER: + object.x -= object.width * 0.5; + case TOP_LEFT: } } - else + + if (axes.y) { - if (nodeIndex >= _nodes.length) + object.y = next.y; + switch (centerMode) { - nodeIndex = _nodes.length - 1; - callComplete = true; - onEnd(); + case ORIGIN: + if (object is FlxSprite) + object.y -= (cast object:FlxSprite).origin.y; + case CUSTOM(offset): + object.y -= offset.y; + case CENTER: + object.y -= object.height * 0.5; + case TOP_LEFT: } } - - if (callComplete && onComplete != null) - { - onComplete(this); - } - - return _nodes[nodeIndex]; + + super.advance(); } + + #if FLX_DEBUG + + /** + * While this doesn't override `FlxBasic.drawDebug()`, the behavior is very similar. + * Based on this path data, it draws a simple lines-and-boxes representation of the path + * if the `drawDebug` mode was toggled in the debugger overlay. + * You can use `debugColor` to control the path's appearance. + * + * @param camera The camera object the path will draw to. + */ + @:deprecated("FlxPath.debugDraw() is deprecated, use draw() OR drawDebugOnCamera(camera), instead") + public function drawDebug(?camera:FlxCamera):Void + { + if (nodes == null || nodes.length <= 0 || ignoreDrawDebug) + return; + + if (camera == null) + camera = FlxG.camera; + + drawDebugOnCamera(camera); + } + #end /** * Stops the path's movement. @@ -624,24 +533,13 @@ class FlxPath implements IFlxDestroyable */ function onEnd():Void { - finished = true; active = false; if (_wasObjectImmovable != null) object.immovable = _wasObjectImmovable; + _wasObjectImmovable = null; } - /** - * Clean up memory. - */ - public function destroy():Void - { - FlxDestroyUtil.putArray(_nodes); - _nodes = null; - object = null; - onComplete = null; - } - /** * Add a new node to the end of the path at the specified location. * @@ -652,7 +550,7 @@ class FlxPath implements IFlxDestroyable */ public function add(x:Float, y:Float):FlxPath { - _nodes.push(FlxPoint.get(x, y)); + nodes.push(FlxPoint.get(x, y)); return this; } @@ -669,7 +567,7 @@ class FlxPath implements IFlxDestroyable { if (index < 0) return this; - _nodes.insert(index, FlxPoint.get(x, y)); + nodes.insert(index, FlxPoint.get(x, y)); return this; } @@ -687,11 +585,11 @@ class FlxPath implements IFlxDestroyable { if (asReference) { - _nodes.push(node); + nodes.push(node); } else { - _nodes.push(FlxPoint.get(node.x, node.y)); + nodes.push(FlxPoint.get(node.x, node.y)); } return this; } @@ -713,11 +611,11 @@ class FlxPath implements IFlxDestroyable return this; if (asReference) { - _nodes.insert(index, node); + nodes.insert(index, node); } else { - _nodes.insert(index, FlxPoint.get(node.x, node.y)); + nodes.insert(index, FlxPoint.get(node.x, node.y)); } return this; } @@ -731,10 +629,10 @@ class FlxPath implements IFlxDestroyable */ public function remove(node:FlxPoint):FlxPoint { - var index:Int = _nodes.indexOf(node); + var index:Int = nodes.indexOf(node); if (index >= 0) { - return _nodes.splice(index, 1)[0]; + return nodes.splice(index, 1)[0]; } return null; } @@ -747,15 +645,15 @@ class FlxPath implements IFlxDestroyable */ public function removeAt(index:Int):FlxPoint { - if (_nodes.length <= 0) + if (nodes.length <= 0) { return null; } - if (index >= _nodes.length - 1) + if (index >= nodes.length - 1) { - _nodes.pop(); + nodes.pop(); } - return _nodes.splice(index, 1)[0]; + return nodes.splice(index, 1)[0]; } /** @@ -765,9 +663,9 @@ class FlxPath implements IFlxDestroyable */ public function head():FlxPoint { - if (_nodes.length > 0) + if (nodes.length > 0) { - return _nodes[0]; + return nodes[0]; } return null; } @@ -779,129 +677,16 @@ class FlxPath implements IFlxDestroyable */ public function tail():FlxPoint { - if (_nodes.length > 0) + if (nodes.length > 0) { - return _nodes[_nodes.length - 1]; + return nodes[nodes.length - 1]; } return null; } - - #if FLX_DEBUG - /** - * While this doesn't override `FlxBasic.drawDebug()`, the behavior is very similar. - * Based on this path data, it draws a simple lines-and-boxes representation of the path - * if the `drawDebug` mode was toggled in the debugger overlay. - * You can use `debugColor` to control the path's appearance. - * - * @param camera The camera object the path will draw to. - */ - @:access(flixel.FlxCamera) - public function drawDebug(?camera:FlxCamera):Void - { - if (_nodes == null || _nodes.length <= 0) - { - return; - } - - if (camera == null) - { - camera = FlxG.camera; - } - - var gfx:Graphics = null; - - // Set up our global flash graphics object to draw out the path - if (FlxG.renderBlit) - { - gfx = FlxSpriteUtil.flashGfx; - gfx.clear(); - } - else - { - gfx = camera.debugLayer.graphics; - } - - // Then fill up the object with node and path graphics - var length = _nodes.length; - for (i in 0...length) - { - // get a reference to the current node - var node = _nodes[i]; - - // find the screen position of the node on this camera - _point.x = node.x - (camera.scroll.x * object.scrollFactor.x); // copied from getScreenPosition() - _point.y = node.y - (camera.scroll.y * object.scrollFactor.y); - - _point = camera.transformPoint(_point); - - // decide what color this node should be - var nodeSize:Int = debugDrawData.nodeSize; - var nodeColor:FlxColor = debugDrawData.nodeColor; - if (length > 1) - { - if (i == 0) - { - nodeColor = debugDrawData.startColor; - nodeSize = debugDrawData.startSize; - } - else if (i == length - 1) - { - nodeColor = debugDrawData.endColor; - nodeSize = debugDrawData.endSize; - } - } - - // draw a box for the node - gfx.beginFill(nodeColor.rgb, nodeColor.alphaFloat); - gfx.lineStyle(); - var nodeOffset = Math.floor(nodeSize * 0.5); - gfx.drawRect(_point.x - nodeOffset, _point.y - nodeOffset, nodeSize, nodeSize); - gfx.endFill(); - - // then find the next node in the path - var nextNode:FlxPoint; - if (i < length - 1) - { - nextNode = _nodes[i + 1]; - } - else - { - nextNode = _nodes[i]; - } - - // then draw a line to the next node - var lineOffset = debugDrawData.lineSize / 2; - gfx.moveTo(_point.x + lineOffset, _point.y + lineOffset); - gfx.lineStyle(debugDrawData.lineSize, debugDrawData.lineColor & 0xFFFFFF, debugDrawData.lineColor.alphaFloat); - _point.x = nextNode.x - (camera.scroll.x * object.scrollFactor.x); // copied from getScreenPosition() - _point.y = nextNode.y - (camera.scroll.y * object.scrollFactor.y); - - if (FlxG.renderBlit) - _point.subtract(camera.viewMarginX, camera.viewMarginY); - - gfx.lineTo(_point.x + lineOffset, _point.y + lineOffset); - } - - if (FlxG.renderBlit) - { - // then stamp the path down onto the game buffer - camera.buffer.draw(FlxSpriteUtil.flashGfxSprite); - } - } - #end - - function get_nodes():Array - { - return _nodes; - } - - function set_nodes(nodes:Array):Array + + inline function get_nodeIndex() { - if (nodes != null) - { - _nodes = nodes; - } - return _nodes; + return nextIndex; } function set_immovable(value:Bool):Bool @@ -937,6 +722,66 @@ class FlxPath implements IFlxDestroyable { return centerMode.match(CENTER); } + + function get__inc() + { + return direction.toInt(); + } + + function set__inc(value:Int):Int + { + direction = value < 0 ? FlxPathDirection.BACKWARD : FlxPathDirection.FORWARD; + return value; + } + + function get__mode() + { + final isForward = direction == FlxPathDirection.FORWARD; + return switch(loopType) + { + case FlxPathLoopType.ONCE: + isForward ? FlxPathType.FORWARD : FlxPathType.BACKWARD; + case FlxPathLoopType.LOOP: + isForward ? FlxPathType.LOOP_FORWARD : FlxPathType.LOOP_BACKWARD; + case FlxPathLoopType.YOYO: + FlxPathType.YOYO; + } + } + + function set__mode(value:FlxPathType):FlxPathType + { + loopType = switch (value) + { + case FlxPathType.YOYO: + FlxPathLoopType.YOYO; + case FlxPathType.FORWARD | FlxPathType.BACKWARD: + FlxPathLoopType.ONCE; + case FlxPathType.LOOP_FORWARD | FlxPathType.LOOP_BACKWARD: + FlxPathLoopType.LOOP; + } + + direction = switch (value) + { + case FlxPathType.YOYO: + direction; + case FlxPathType.FORWARD | FlxPathType.LOOP_FORWARD: + FlxPathDirection.FORWARD; + case FlxPathType.BACKWARD | FlxPathType.LOOP_BACKWARD: + FlxPathDirection.BACKWARD; + } + + return value; + } + + function get_object() + { + return target; + } + + function set_object(value:FlxObject) + { + return target = value; + } } /** @@ -968,17 +813,4 @@ enum abstract FlxPathType(Int) from Int to Int * Move from the start of the path to the end then turn around and go back to the start, over and over. */ var YOYO = 0x001000; -} - -@:structInit -class FlxPathDrawData -{ - public var lineColor = FlxColor.WHITE; - public var nodeColor = FlxColor.WHITE; - public var startColor = FlxColor.GREEN; - public var endColor = FlxColor.RED; - public var lineSize = 1; - public var nodeSize = 3; - public var startSize = 5; - public var endSize = 5; -} +} \ No newline at end of file From 4d8b2d05fd16ca4141318185875ce141b06ffbf0 Mon Sep 17 00:00:00 2001 From: Blue2359 <112711167+Blue2359@users.noreply.github.com> Date: Thu, 30 May 2024 23:08:42 +0200 Subject: [PATCH 07/22] Better description for `persistentUpdate & persistentDraw` in `FlxState` (#3155) * Better documentation for `persistentUpdate & persistentDraw` Updated documentation for `persistentUpdate & persistentDraw` in `FlxState`, so that they make more sense, and get to be understandable better, also fixes the "update" parts in `persistentDraw`'s documentation, when they should be using the word "draw(n)" instead. * change wording --------- Co-authored-by: George Kurelic --- flixel/FlxState.hx | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/flixel/FlxState.hx b/flixel/FlxState.hx index f3835db19f..c13149f646 100644 --- a/flixel/FlxState.hx +++ b/flixel/FlxState.hx @@ -18,21 +18,23 @@ import flixel.util.typeLimit.NextState; class FlxState extends FlxContainer { /** - * Determines whether or not this state is updated even when it is not the active state. - * For example, if you have your game state first, and then you push a menu state on top of it, - * if this is set to `true`, the game state would continue to update in the background. - * By default this is `false`, so background states will be "paused" when they are not active. + * Determines whether the current state is updated, even when it is not the active state. + * For example, if you have your game state open first, and then you push a pause state on top of it, + * if this is set to `true`, the game state would still continue to be updated in the background. + * + * By default, this is set to `false`, so the background states will continue to be "paused" when they are not active. */ public var persistentUpdate:Bool = false; /** - * Determines whether or not this state is updated even when it is not the active state. - * For example, if you have your game state first, and then you push a menu state on top of it, - * if this is set to `true`, the game state would continue to be drawn behind the pause state. - * By default this is `true`, so background states will continue to be drawn behind the current state. + * Determines whether the current state is drawn, even when it is not the active state. + * For example, if you have your game state open first, and then you push a pause state on top of it, + * if this is set to `true`, the game state would still continue to be drawn behind that pause state. + * + * By default, this is set to `true`, so the background states will continue to be "drawn" behind the current state. * - * If background states are not `visible` when you have a different state on top, - * you should set this to `false` for improved performance. + * If you do not want background states to be `visible` when you have a different state on top, + * then you should set this to `false` for improved performance. */ public var persistentDraw:Bool = true; From 82fe51418c69a7f42d5de9fe5a33aadb991d6ca2 Mon Sep 17 00:00:00 2001 From: ACrazyTown <47027981+ACrazyTown@users.noreply.github.com> Date: Fri, 31 May 2024 03:39:04 +0200 Subject: [PATCH 08/22] Fix path in include.xml (#3156) --- include.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include.xml b/include.xml index f742fbd22c..d0c2a32b00 100644 --- a/include.xml +++ b/include.xml @@ -1,6 +1,6 @@ - + From cdd86278be529373b0214532ababb8283ceb65bb Mon Sep 17 00:00:00 2001 From: ACrazyTown <47027981+ACrazyTown@users.noreply.github.com> Date: Fri, 31 May 2024 03:40:06 +0200 Subject: [PATCH 09/22] Fix logo link in README.md (#3157) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4574744c15..9e5703a478 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![](https://raw.github.com/HaxeFlixel/haxeflixel.com/master/src/files/images/flixel-logos/HaxeFlixel.png)](http://haxeflixel.com/) +[![](https://raw.githubusercontent.com/HaxeFlixel/haxeflixel.com/master/content/_static/images/flixel-logos/HaxeFlixel.png)](http://haxeflixel.com/) [flixel](https://github.com/HaxeFlixel/flixel) | [addons](https://github.com/HaxeFlixel/flixel-addons) | [ui](https://github.com/HaxeFlixel/flixel-ui) | [demos](https://github.com/HaxeFlixel/flixel-demos) | [tools](https://github.com/HaxeFlixel/flixel-tools) | [templates](https://github.com/HaxeFlixel/flixel-templates) | [docs](https://github.com/HaxeFlixel/flixel-docs) | [haxeflixel.com](https://github.com/HaxeFlixel/haxeflixel.com) | [türkçe](https://github.com/HaxeFlixel/flixel/blob/dev/.github/README_TR.md) From 1017eab2649108d7a33888e5bbaf2747145304a3 Mon Sep 17 00:00:00 2001 From: George Kurelic Date: Fri, 31 May 2024 13:42:45 -0500 Subject: [PATCH 10/22] add missing direction arg --- flixel/path/FlxBasePath.hx | 1 + 1 file changed, 1 insertion(+) diff --git a/flixel/path/FlxBasePath.hx b/flixel/path/FlxBasePath.hx index 758d59c8c6..3fa14e379a 100644 --- a/flixel/path/FlxBasePath.hx +++ b/flixel/path/FlxBasePath.hx @@ -106,6 +106,7 @@ class FlxTypedBasePath extends FlxBasic implements IFlxDestroy { this.nodes = nodes; this.target = target; + this.direction = direction; super(); if (nodes != null && nodes.length > 0 && target != null) From 073b4c4058017889fa4ebb695cf88579b3f0e782 Mon Sep 17 00:00:00 2001 From: Blue2359 <112711167+Blue2359@users.noreply.github.com> Date: Sun, 2 Jun 2024 15:14:24 +0200 Subject: [PATCH 11/22] Fix documentation in `FlxCamera` (#3161) * Fix documentation in `FlxCamera` Fixed documentation for `screen & useBgAlphaBlending`, also got rid of the invalid links only available on flash. * small change --------- Co-authored-by: George Kurelic --- flixel/FlxCamera.hx | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/flixel/FlxCamera.hx b/flixel/FlxCamera.hx index f902a0188f..9e7156cff8 100644 --- a/flixel/FlxCamera.hx +++ b/flixel/FlxCamera.hx @@ -182,25 +182,22 @@ class FlxCamera extends FlxBasic public var bgColor:FlxColor; /** - * Sometimes it's easier to just work with a `FlxSprite` than it is to work directly with the `BitmapData` buffer. + * Sometimes it's easier to just work with a `FlxSprite`, than it is to work directly with the `BitmapData` buffer. * This sprite reference will allow you to do exactly that. - * Basically this sprite's `pixels` property is camera's `BitmapData` buffer. - * NOTE: This variable is used only in blit render mode. + * Basically, this sprite's `pixels` property is the camera's `BitmapData` buffer. * - * The FlxBloom demo shows how you can use this variable in blit render mode. - * @see http://haxeflixel.com/demos/FlxBloom/ + * **NOTE:** This field is only used in blit render mode. */ public var screen:FlxSprite; /** - * Whether to use alpha blending for camera's background fill or not. - * If `true` then previously drawn graphics won't be erased, - * and if camera's `bgColor` is transparent/semitransparent then you - * will be able to see graphics of the previous frame. - * Useful for blit render mode (and works only in this mode). Default value is `false`. + * Whether to use alpha blending for the camera's background fill or not. + * If `true`, then the previously drawn graphics won't be erased, + * and if the camera's `bgColor` is transparent/semitransparent, then you + * will be able to see the graphics of the previous frame. * - * Usage example can be seen in FlxBloom demo. - * @see http://haxeflixel.com/demos/FlxBloom/ + * This is Useful for blit render mode (and only works in this mode). + * Default value is `false`. */ public var useBgAlphaBlending:Bool = false; From 34c6942bf47fbb482ed4e346bcc224bf6969ceaa Mon Sep 17 00:00:00 2001 From: Blue2359 <112711167+Blue2359@users.noreply.github.com> Date: Sun, 2 Jun 2024 15:27:02 +0200 Subject: [PATCH 12/22] Update documentation for `NextState` (#3160) * Update documentation for `NextState` I noticed that the NextState.hx file had some tiny errors in it's documentation so i went ahead and fixed them. * doc touch-up --------- Co-authored-by: George Kurelic --- flixel/util/typeLimit/NextState.hx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flixel/util/typeLimit/NextState.hx b/flixel/util/typeLimit/NextState.hx index 5802975fdb..bf2d92f9b0 100644 --- a/flixel/util/typeLimit/NextState.hx +++ b/flixel/util/typeLimit/NextState.hx @@ -3,11 +3,11 @@ package flixel.util.typeLimit; import flixel.FlxState; /** - * A utility type that allows methods to accept multiple types, when dealing with "future" F`lxStates`. - * Prior to haxeFlixel 6, `FlxG.switchState` and other similar methods took a `FlxState` instance - * which meant FlxStates were instantiated before the previous state was destroyed, potentially - * causing errors. It also meant states with args couldn't be reset via FlxG.resetState. In version - * 5.6.0 and higher, these methods now take a function that returns a newly created instance. This + * A utility type that allows methods to accept multiple types, when dealing with "future" `FlxStates`. + * Prior to HaxeFlixel 6, `FlxG.switchState` and other similar methods took a `FlxState` instance, + * which meant `FlxStates` were instantiated before the previous state was destroyed, potentially + * causing errors. It also meant that states with args couldn't be reset via `FlxG.resetState`. In version + * 5.6.0 and higher, these methods now take a function that returns a newly created state instance. This * allows the state's instantiation to happen after the previous state is destroyed. * * ## examples: From d8d71490c0bf56dccc550fcd43281c347f62763e Mon Sep 17 00:00:00 2001 From: DigiEggz Date: Thu, 6 Jun 2024 13:18:21 -0400 Subject: [PATCH 13/22] Prevent "Unsupported recursive type" error when targeting HashLink (#3170) * Prevent "Unsupported recursive type" error when targeting HashLink Referencing TweenOptions creates an issue when trying to compile to HashLink, causing TweenCallback to become ambiguous. Resolves #3149. * style stuff --------- Co-authored-by: George Kurelic --- flixel/tweens/misc/FlickerTween.hx | 49 +++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/flixel/tweens/misc/FlickerTween.hx b/flixel/tweens/misc/FlickerTween.hx index 4be92afe97..b139845631 100644 --- a/flixel/tweens/misc/FlickerTween.hx +++ b/flixel/tweens/misc/FlickerTween.hx @@ -2,29 +2,68 @@ package flixel.tweens.misc; import flixel.FlxBasic; import flixel.tweens.FlxTween; +import flixel.tweens.FlxEase; /** * Special tween options for flicker tweens * @since 5.7.0 */ -typedef FlickerTweenOptions = TweenOptions & +/*Note: FlickerTweenOptions previously referenced TweenOptions, but was causing a "Unsupported recursive type" circular dependency issue when targeting HashLink + * See https://github.com/HaxeFlixel/flixel/issues/3149 + */ +typedef FlickerTweenOptions = { + /** + * Tween type - bit field of `FlxTween`'s static type constants. + */ + @:optional var type:FlxTweenType; + + /** + * Optional easer function (see `FlxEase`). + */ + @:optional var ease:EaseFunction; + + /** + * Optional start callback function. + */ + @:optional var onStart:TweenCallback; + + /** + * Optional update callback function. + */ + @:optional var onUpdate:TweenCallback; + + /** + * Optional complete callback function. + */ + @:optional var onComplete:TweenCallback; + + /** + * Seconds to wait until starting this tween, `0` by default. + */ + @:optional var startDelay:Float; + + /** + * Seconds to wait between loops of this tween, `0` by default. + */ + @:optional var loopDelay:Float; + /** * Whether the object will show after the tween, defaults to `true` */ - ?endVisibility:Bool, + @:optional var endVisibility:Bool; /** * The amount of time the object will show, compared to the total duration, The default is `0.5`, * meaning equal times visible and invisible. */ - ?ratio:Float, + @:optional var ratio:Float; /** * An optional custom flicker function, defaults to * `function (tween) { return (tween.time / tween.period) % 1 > tween.ratio; }` */ - ?tweenFunction:(FlickerTween)->Bool + @:optional var tweenFunction:(FlickerTween) -> Bool; }; /** @@ -37,7 +76,7 @@ class FlickerTween extends FlxTween public var basic(default, null):FlxBasic; /** Controls how the object flickers over time */ - public var tweenFunction(default, null):(FlickerTween)->Bool; + public var tweenFunction(default, null):(FlickerTween) -> Bool; /** Whether the object will show after the tween, defaults to `true` */ public var endVisibility(default, null):Bool = true; From 4ed1cfb86cc72f2e77521401225e0975bcadfa99 Mon Sep 17 00:00:00 2001 From: rich <87835336+richTrash21@users.noreply.github.com> Date: Mon, 10 Jun 2024 22:35:18 +0400 Subject: [PATCH 14/22] Fix FlxBGSprite render on camera zoom != 1 (#3142) * Update FlxBGSprite.hx * remove alpha check --------- Co-authored-by: George Kurelic --- flixel/system/FlxBGSprite.hx | 1 + 1 file changed, 1 insertion(+) diff --git a/flixel/system/FlxBGSprite.hx b/flixel/system/FlxBGSprite.hx index 85407277ae..8f430f410c 100644 --- a/flixel/system/FlxBGSprite.hx +++ b/flixel/system/FlxBGSprite.hx @@ -29,6 +29,7 @@ class FlxBGSprite extends FlxSprite _matrix.identity(); _matrix.scale(camera.viewWidth, camera.viewHeight); + _matrix.translate(camera.viewMarginLeft, camera.viewMarginTop); camera.drawPixels(frame, _matrix, colorTransform); #if FLX_DEBUG From 1553b5af0871462fcefedc091b7885437d6c36d2 Mon Sep 17 00:00:00 2001 From: George Kurelic Date: Mon, 10 Jun 2024 13:42:48 -0500 Subject: [PATCH 15/22] account for scale, origin, offset, angle and pixelPerfectPosition in getGraphicMidpoint (#3125) * account for scale, origin, offset and pixelPerfectPosition * add angle * add since * doc * spacing and style --- flixel/FlxSprite.hx | 43 +++++++++++++++++++++----- flixel/math/FlxRect.hx | 16 +++++++++- tests/unit/src/flixel/FlxSpriteTest.hx | 41 ++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 9 deletions(-) diff --git a/flixel/FlxSprite.hx b/flixel/FlxSprite.hx index 7071f35a7f..8792a468d6 100644 --- a/flixel/FlxSprite.hx +++ b/flixel/FlxSprite.hx @@ -1206,21 +1206,48 @@ class FlxSprite extends FlxObject dirty = false; return framePixels; } - + /** * Retrieve the midpoint of this sprite's graphic in world coordinates. * - * @param point Allows you to pass in an existing `FlxPoint` if you're so inclined. - * Otherwise a new one is created. - * @return A `FlxPoint` containing the midpoint of this sprite's graphic in world coordinates. + * @param point The resulting point, if `null` a new one is created */ public function getGraphicMidpoint(?point:FlxPoint):FlxPoint { - if (point == null) - point = FlxPoint.get(); - return point.set(x + frameWidth * 0.5 * scale.x, y + frameHeight * 0.5 * scale.y); + final rect = getGraphicBounds(); + point = rect.getMidpoint(point); + rect.put(); + return point; } - + + /** + * Retrieves the world bounds of this sprite's graphic + * **Note:** Ignores `scrollFactor`, to get the screen position of the graphic use + * `getScreenBounds` + * + * @param rect The resulting rect, if `null` a new one is created + * @since 5.9.0 + */ + public function getGraphicBounds(?rect:FlxRect):FlxRect + { + if (rect == null) + rect = FlxRect.get(); + + rect.set(x, y); + if (pixelPerfectPosition) + rect.floor(); + + _scaledOrigin.set(origin.x * scale.x, origin.y * scale.y); + rect.x += origin.x - offset.x - _scaledOrigin.x; + rect.y += origin.y - offset.y - _scaledOrigin.y; + rect.setSize(frameWidth * scale.x, frameHeight * scale.y); + + if (angle % 360 != 0) + rect.getRotatedBounds(angle, _scaledOrigin, rect); + + return rect; + } + /** * Check and see if this object is currently on screen. Differs from `FlxObject`'s implementation * in that it takes the actual graphic into account, not just the hitbox or bounding box or whatever. diff --git a/flixel/math/FlxRect.hx b/flixel/math/FlxRect.hx index 45ca561a48..63e380a906 100644 --- a/flixel/math/FlxRect.hx +++ b/flixel/math/FlxRect.hx @@ -462,7 +462,21 @@ class FlxRect implements IFlxPooled rect.putWeak(); return result.set(x0, y0, x1 - x0, y1 - y0); } - + + /** + * The middle point of this rect + * + * @param point The point to hold the result, if `null` a new one is created + * @since 5.9.0 + */ + public function getMidpoint(?point:FlxPoint) + { + if (point == null) + point = FlxPoint.get(); + + return point.set(x + 0.5 * width, y + 0.5 * height); + } + /** * Convert object to readable string name. Useful for debugging, save games, etc. */ diff --git a/tests/unit/src/flixel/FlxSpriteTest.hx b/tests/unit/src/flixel/FlxSpriteTest.hx index b462763c43..d58c103b87 100644 --- a/tests/unit/src/flixel/FlxSpriteTest.hx +++ b/tests/unit/src/flixel/FlxSpriteTest.hx @@ -4,6 +4,7 @@ import openfl.display.BitmapData; import flixel.animation.FlxAnimation; import flixel.graphics.atlas.FlxAtlas; import flixel.math.FlxRect; +import flixel.math.FlxPoint; import flixel.text.FlxText; import flixel.util.FlxColor; import massive.munit.Assert; @@ -328,4 +329,44 @@ class FlxSpriteTest extends FlxTest expected.put(); } + + @Test + function testGetGraphicMidpoint() + { + final full:SimplePoint = [sprite1.frameWidth, sprite1.frameHeight]; + final mid:SimplePoint = [full.x / 2, full.y / 2]; + final zero:SimplePoint = [0, 0]; + assertGraphicMidpoint({ pos:[0, 5], size:full, origin:mid, offset:zero}); + assertGraphicMidpoint({ pos:[0, 5], size:full, origin:full, offset:zero}); + assertGraphicMidpoint({ pos:[0, 5], size:[10, 10], origin:mid, offset:zero}); + assertGraphicMidpoint({ pos:[0, 5], size:[50, 50], origin:mid, offset:[1, 3]}); + assertGraphicMidpoint({ pos:[0, 5], size:[50, 50], origin:zero, offset:zero}); + assertGraphicMidpoint({ pos:[0, 5], size:[50, 50], origin:full, offset:[50, 60]}); + assertGraphicMidpoint({ pos:[0, 5], size:[50, 100], origin:[10, 20], offset:[-50, 60]}); + } + + function assertGraphicMidpoint(orientation:Orientation, ?pos:PosInfos) + { + sprite1.x = orientation.pos.x; + sprite1.y = orientation.pos.y; + sprite1.setGraphicSize(orientation.size.x, orientation.size.y); + sprite1.offset.set(orientation.offset.x, orientation.offset.y); + sprite1.origin.set(orientation.origin.x, orientation.origin.y); + final actual = sprite1.getGraphicMidpoint(FlxPoint.weak()); + + // check against getScreenBounds + final rect = sprite1.getScreenBounds(FlxRect.weak()); + FlxAssert.areNear(rect.x + 0.5 * rect.width, actual.x, 0.001, pos); + FlxAssert.areNear(rect.y + 0.5 * rect.height, actual.y, 0.001, pos); + } } + +abstract SimplePoint(Array) from Array +{ + public var x(get, never):Float; + inline function get_x() return this[0]; + + public var y(get, never):Float; + inline function get_y() return this[1]; +} +typedef Orientation = { pos:SimplePoint, size:SimplePoint, offset:SimplePoint, origin:SimplePoint } \ No newline at end of file From 27d0097ba81c8bad8918436b7f1f2c4dc84d3fe7 Mon Sep 17 00:00:00 2001 From: George Kurelic Date: Mon, 10 Jun 2024 15:31:54 -0500 Subject: [PATCH 16/22] test 4.3.4 (#3172) --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a52fc25113..ba7695c6ac 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: build: strategy: matrix: - haxe-version: ["4.2.5", "4.3.3"] + haxe-version: ["4.2.5", "4.3.4"] target: [html5, hl, neko, flash, cpp] fail-fast: false runs-on: ubuntu-latest From 8489570f4e5c1d2df4e17646a2bef18ee1408733 Mon Sep 17 00:00:00 2001 From: George Kurelic Date: Tue, 11 Jun 2024 09:59:58 -0500 Subject: [PATCH 17/22] Major change to FlxTilemap/FlxTiles Improve collision, debug drawing, add various features (#3158) * drawDebug boundingBox helpers * allow tile instances to determine overlap * simplify createTile * make overlapsObject dynamic * cc * add orient + forEachOverlappingTile * add separateObjects and similar helpers, revert overlapsWithCallback changes * doc * deprecate overrides * fix deprecation warning * add isOverlappingTile and docs to base * add since * cc * doc * remove new public helpers * extract legacy code to new func * cc * add legacyCollision to coverage * separate tests * cleanup computeOverlap funcs * add separate, computeOverlap tests * check diagonal against walls * check immovable, recursively in collision * add other orient helpers * add overloaded setTile/getTile methods + tests * soft deprecation * small cleanup * better backwards compatibility * add since * more since * more since * dd getRowAt, getColumnAt, rowExists and columnExists * D'oh! * D'OH! * processOverlaps -> objectOverlapsTiles + tests * small changes * test literal edge case * more small stuff * D'OH!! --- flixel/FlxObject.hx | 699 ++++++++++--------- flixel/math/FlxRect.hx | 15 +- flixel/path/FlxPathfinder.hx | 12 +- flixel/tile/FlxBaseTilemap.hx | 467 ++++++++++--- flixel/tile/FlxTile.hx | 104 ++- flixel/tile/FlxTilemap.hx | 228 +++--- haxelib.json | 2 +- tests/coverage/Project.xml | 1 + tests/unit/src/flixel/FlxObjectTest.hx | 99 ++- tests/unit/src/flixel/path/FlxPathTest.hx | 1 + tests/unit/src/flixel/tile/FlxTilemapTest.hx | 367 +++++++++- 11 files changed, 1420 insertions(+), 575 deletions(-) diff --git a/flixel/FlxObject.hx b/flixel/FlxObject.hx index 4d2c077117..21f5795b88 100644 --- a/flixel/FlxObject.hx +++ b/flixel/FlxObject.hx @@ -164,41 +164,7 @@ class FlxObject extends FlxBasic @:deprecated("Use ANY or FlxDirectionFlags.ANY instead") @:noCompletion public static inline var ANY = FlxDirectionFlags.ANY; - - @:noCompletion - static var _firstSeparateFlxRect:FlxRect = FlxRect.get(); - @:noCompletion - static var _secondSeparateFlxRect:FlxRect = FlxRect.get(); - - /** - * The main collision resolution function in Flixel. - * - * @param Object1 Any `FlxObject`. - * @param Object2 Any other `FlxObject`. - * @return Whether the objects in fact touched and were separated. - */ - public static function separate(object1:FlxObject, object2:FlxObject):Bool - { - var separatedX:Bool = separateX(object1, object2); - var separatedY:Bool = separateY(object1, object2); - return separatedX || separatedY; - } - - /** - * Similar to `separate()`, but only checks whether any overlap is found and updates - * the `touching` flags of the input objects, but no separation is performed. - * - * @param Object1 Any `FlxObject`. - * @param Object2 Any other `FlxObject`. - * @return Whether the objects in fact touched. - */ - public static function updateTouchingFlags(Object1:FlxObject, Object2:FlxObject):Bool - { - var touchingX:Bool = updateTouchingFlagsX(Object1, Object2); - var touchingY:Bool = updateTouchingFlagsY(Object1, Object2); - return touchingX || touchingY; - } - + static function allowCollisionDrag(type:CollisionDragType, object1:FlxObject, object2:FlxObject):Bool { return object2.active && object2.moves && switch (type) @@ -209,227 +175,411 @@ class FlxObject extends FlxBasic case HEAVIER: object2.immovable || object2.mass > object1.mass; } } - + /** - * Internal function that computes overlap among two objects on the X axis. It also updates the `touching` variable. - * `checkMaxOverlap` is used to determine whether we want to exclude (therefore check) overlaps which are - * greater than a certain maximum (linked to `SEPARATE_BIAS`). Default is `true`, handy for `separateX` code. - */ - @:noCompletion - static function computeOverlapX(object1:FlxObject, object2:FlxObject, checkMaxOverlap:Bool = true):Float + * Internal elper that determines whether either object is a tilemap, determines + * which tiles are overlapping and calls the appropriate separator + * + * + * + * @param func The process you wish to call with both objects, or between tiles, + * + * @param isCollision Does nothing, if both objects are immovable + * @return The result of whichever separator was used + * @since 5.9.0 + */ + @:haxe.warning("-WDeprecated") + static function processCheckTilemap(object1:FlxObject, object2:FlxObject, func:(FlxObject, FlxObject)->Bool, + ?position:FlxPoint, isCollision = true):Bool { - var overlap:Float = 0; - // First, get the two object deltas - var obj1delta:Float = object1.x - object1.last.x; - var obj2delta:Float = object2.x - object2.last.x; - - if (obj1delta != obj2delta) + // two immovable objects cannot collide + if (isCollision && object1.immovable && object2.immovable) + return false; + + // If one of the objects is a tilemap, just pass it off. + if (object1.flixelType == TILEMAP) { - // Check if the X hulls actually overlap - var obj1deltaAbs:Float = (obj1delta > 0) ? obj1delta : -obj1delta; - var obj2deltaAbs:Float = (obj2delta > 0) ? obj2delta : -obj2delta; - - var obj1rect:FlxRect = _firstSeparateFlxRect.set(object1.x - ((obj1delta > 0) ? obj1delta : 0), object1.last.y, object1.width + obj1deltaAbs, - object1.height); - var obj2rect:FlxRect = _secondSeparateFlxRect.set(object2.x - ((obj2delta > 0) ? obj2delta : 0), object2.last.y, object2.width + obj2deltaAbs, - object2.height); - - if ((obj1rect.x + obj1rect.width > obj2rect.x) - && (obj1rect.x < obj2rect.x + obj2rect.width) - && (obj1rect.y + obj1rect.height > obj2rect.y) - && (obj1rect.y < obj2rect.y + obj2rect.height)) + final tilemap:FlxBaseTilemap = cast object1; + // If object1 is a tilemap, check it's tiles against object2, which may also be a tilemap + function recurseProcess(tile, _) { - var maxOverlap:Float = checkMaxOverlap ? (obj1deltaAbs + obj2deltaAbs + SEPARATE_BIAS) : 0; - - // If they did overlap (and can), figure out by how much and flip the corresponding flags - if (obj1delta > obj2delta) - { - overlap = object1.x + object1.width - object2.x; - if ((checkMaxOverlap && (overlap > maxOverlap)) - || ((object1.allowCollisions & FlxDirectionFlags.RIGHT) == 0) - || ((object2.allowCollisions & FlxDirectionFlags.LEFT) == 0)) - { - overlap = 0; - } - else - { - object1.touching |= FlxDirectionFlags.RIGHT; - object2.touching |= FlxDirectionFlags.LEFT; - } - } - else if (obj1delta < obj2delta) - { - overlap = object1.x - object2.width - object2.x; - if ((checkMaxOverlap && (-overlap > maxOverlap)) - || ((object1.allowCollisions & FlxDirectionFlags.LEFT) == 0) - || ((object2.allowCollisions & FlxDirectionFlags.RIGHT) == 0)) - { - overlap = 0; - } - else - { - object1.touching |= FlxDirectionFlags.LEFT; - object2.touching |= FlxDirectionFlags.RIGHT; - } - } + // Keep tile as first arg + return processCheckTilemap(tile, object2, func, position, isCollision); } + return tilemap.overlapsWithCallback(object2, recurseProcess, false, position); } - return overlap; + else if (object2.flixelType == TILEMAP) + { + final tilemap:FlxBaseTilemap = cast object2; + // If object1 is a tilemap, check it's tiles against object2, which may also be a tilemap + function recurseProcess(tile, _) + { + // Keep tile as second arg + return processCheckTilemap(object1, tile, func, position, isCollision); + } + return tilemap.overlapsWithCallback(object1, recurseProcess, false, position); + } + + return func(object1, object2); } - + /** - * The X-axis component of the object separation process. - * - * @param object1 Any `FlxObject`. - * @param object2 Any other `FlxObject`. - * @return Whether the objects in fact touched and were separated along the X axis. + * Separates 2 overlapping objects. If an object is a tilemap, + * it will separate it from any tiles that overlap it. + * + * @return Whether the objects were overlapping and were separated + */ + public static function separate(object1:FlxObject, object2:FlxObject):Bool + { + final separatedX = separateX(object1, object2); + final separatedY = separateY(object1, object2); + return separatedX || separatedY; + + /* + * Note: can't do the following, FlxTilemapExt works better when you separate all + * tiles in the x and then all tiles the y, rather than iterating all overlapping + * tiles and separating the x and y on each of them. If we find a way around this + * if would be more efficient to do the following + */ + // function helper(object1, object2) + // { + // final separatedX = separateXHelper(object1, object2); + // final separatedY = separateYHelper(object1, object2); + // return separatedX || separatedY; + // } + // return processCheckTilemap(object1, object2, helper); + } + + /** + * Separates 2 overlapping objects along the X-axis. if an object is a tilemap, + * it will separate it from any tiles that overlap it. + * + * @return Whether the objects were overlapping and were separated along the X-axis */ public static function separateX(object1:FlxObject, object2:FlxObject):Bool { - // can't separate two immovable objects - var immovable1 = object1.immovable; - var immovable2 = object2.immovable; - if (immovable1 && immovable2) - { - return false; - } - - // If one of the objects is a tilemap, just pass it off. - if (object1.flixelType == TILEMAP) - { - var tilemap:FlxBaseTilemap = cast object1; - return tilemap.overlapsWithCallback(object2, separateX); - } - if (object2.flixelType == TILEMAP) - { - var tilemap:FlxBaseTilemap = cast object2; - return tilemap.overlapsWithCallback(object1, separateX, true); - } - - var overlap:Float = computeOverlapX(object1, object2); + return processCheckTilemap(object1, object2, separateXHelper); + } + + /** + * Separates 2 overlapping objects along the Y-axis. if an object is a tilemap, + * it will separate it from any tiles that overlap it. + * + * @return Whether the objects were overlapping and were separated along the Y-axis + */ + public static function separateY(object1:FlxObject, object2:FlxObject):Bool + { + return processCheckTilemap(object1, object2, separateYHelper); + } + + /** + * Same as `separateX` but assumes both are not immovable and not tilemaps + */ + static function separateXHelper(object1:FlxObject, object2:FlxObject):Bool + { + final overlap:Float = computeOverlapX(object1, object2); // Then adjust their positions and velocities accordingly (if there was any overlap) if (overlap != 0) { - var delta1 = object1.x - object1.last.x; - var delta2 = object2.x - object2.last.x; - var vel1 = object1.velocity.x; - var vel2 = object2.velocity.x; - var mass1 = object1.mass; - var mass2 = object2.mass; - var massSum = mass1 + mass2; - var elasticity1 = object1.elasticity; - var elasticity2 = object2.elasticity; - - if (!immovable1 && !immovable2) + final delta1 = object1.x - object1.last.x; + final delta2 = object2.x - object2.last.x; + final vel1 = object1.velocity.x; + final vel2 = object2.velocity.x; + + if (!object1.immovable && !object2.immovable) { #if FLX_4_LEGACY_COLLISION - overlap *= 0.5; - object1.x = object1.x - overlap; - object2.x += overlap; - - var newVel1 = Math.sqrt((vel2 * vel2 * mass2) / mass1) * ((vel2 > 0) ? 1 : -1); - var newVel2 = Math.sqrt((vel1 * vel1 * mass1) / mass2) * ((vel1 > 0) ? 1 : -1); - var average = (newVel1 + newVel2) * 0.5; - newVel1 -= average; - newVel2 -= average; - object1.velocity.x = average + newVel1 * elasticity1; - object2.velocity.x = average + newVel2 * elasticity2; + legacySeparateX(object1, object2, overlap); #else - object1.x -= overlap / 2; - object2.x += overlap / 2; + object1.x -= overlap * 0.5; + object2.x += overlap * 0.5; - var momentum = mass1 * vel1 + mass2 * vel2; - var newVel1 = (momentum + elasticity1 * mass2 * (vel2 - vel1)) / massSum; - var newVel2 = (momentum + elasticity2 * mass1 * (vel1 - vel2)) / massSum; - object1.velocity.x = newVel1; - object2.velocity.x = newVel2; + final mass1 = object1.mass; + final mass2 = object2.mass; + final momentum = mass1 * vel1 + mass2 * vel2; + object1.velocity.x = (momentum + object1.elasticity * mass2 * (vel2 - vel1)) / (mass1 + mass2); + object2.velocity.x = (momentum + object2.elasticity * mass1 * (vel1 - vel2)) / (mass1 + mass2); #end } - else if (!immovable1) + else if (!object1.immovable) { object1.x -= overlap; - object1.velocity.x = vel2 - vel1 * elasticity1; + object1.velocity.x = vel2 - vel1 * object1.elasticity; } - else if (!immovable2) + else if (!object2.immovable) { object2.x += overlap; - object2.velocity.x = vel1 - vel2 * elasticity2; + object2.velocity.x = vel1 - vel2 * object2.elasticity; } - + // use collisionDrag properties to determine whether one object if (allowCollisionDrag(object1.collisionYDrag, object1, object2) && delta1 > delta2) object1.y += object2.y - object2.last.y; else if (allowCollisionDrag(object2.collisionYDrag, object2, object1) && delta2 > delta1) object2.y += object1.y - object1.last.y; - + return true; } - + return false; } - + /** - * Checking overlap and updating `touching` variables, X-axis part used by `updateTouchingFlags`. - * - * @param object1 Any `FlxObject`. - * @param object2 Any other `FlxObject`. - * @return Whether the objects in fact touched along the X axis. + * Same as `separateY` but assumes both are not immovable and not tilemaps */ - public static function updateTouchingFlagsX(object1:FlxObject, object2:FlxObject):Bool + static function separateYHelper(object1:FlxObject, object2:FlxObject):Bool { - // If one of the objects is a tilemap, just pass it off. - if (object1.flixelType == TILEMAP) + final overlap:Float = computeOverlapY(object1, object2); + // Then adjust their positions and velocities accordingly (if there was any overlap) + if (overlap != 0) { - var tilemap:FlxBaseTilemap = cast object1; - return tilemap.overlapsWithCallback(object2, updateTouchingFlagsX); + final delta1 = object1.y - object1.last.y; + final delta2 = object2.y - object2.last.y; + final vel1 = object1.velocity.y; + final vel2 = object2.velocity.y; + + if (!object1.immovable && !object2.immovable) + { + #if FLX_4_LEGACY_COLLISION + legacySeparateY(object1, object2, overlap); + #else + object1.y -= overlap / 2; + object2.y += overlap / 2; + + final mass1 = object1.mass; + final mass2 = object2.mass; + final momentum = mass1 * vel1 + mass2 * vel2; + final newVel1 = (momentum + object1.elasticity * mass2 * (vel2 - vel1)) / (mass1 + mass2); + final newVel2 = (momentum + object2.elasticity * mass1 * (vel1 - vel2)) / (mass1 + mass2); + object1.velocity.y = newVel1; + object2.velocity.y = newVel2; + #end + } + else if (!object1.immovable) + { + object1.y -= overlap; + object1.velocity.y = vel2 - vel1 * object1.elasticity; + } + else if (!object2.immovable) + { + object2.y += overlap; + object2.velocity.y = vel1 - vel2 * object2.elasticity; + } + + // use collisionDrag properties to determine whether one object + if (allowCollisionDrag(object1.collisionXDrag, object1, object2) && delta1 > delta2) + object1.x += object2.x - object2.last.x; + else if (allowCollisionDrag(object2.collisionXDrag, object2, object1) && delta2 > delta1) + object2.x += object1.x - object1.last.x; + + return true; } - if (object2.flixelType == TILEMAP) + + return false; + } + + /** + * The separateX that existed before HaxeFlixel 5.0, preserved for anyone who + * needs to use it in an old project. Does not preserve momentum, avoid if possible + */ + static inline function legacySeparateX(object1:FlxObject, object2:FlxObject, overlap:Float) + { + final vel1 = object1.velocity.x; + final vel2 = object2.velocity.x; + final mass1 = object1.mass; + final mass2 = object2.mass; + object1.x = object1.x - (overlap * 0.5); + object2.x += overlap * 0.5; + + var newVel1 = Math.sqrt((vel2 * vel2 * mass2) / mass1) * ((vel2 > 0) ? 1 : -1); + var newVel2 = Math.sqrt((vel1 * vel1 * mass1) / mass2) * ((vel1 > 0) ? 1 : -1); + final average = (newVel1 + newVel2) * 0.5; + newVel1 -= average; + newVel2 -= average; + object1.velocity.x = average + (newVel1 * object1.elasticity); + object2.velocity.x = average + (newVel2 * object2.elasticity); + } + + /** + * The separateY that existed before HaxeFlixel 5.0, preserved for anyone who + * needs to use it in an old project. Does not preserve momentum, avoid if possible + */ + static inline function legacySeparateY(object1:FlxObject, object2:FlxObject, overlap:Float) + { + final vel1 = object1.velocity.y; + final vel2 = object2.velocity.y; + final mass1 = object1.mass; + final mass2 = object2.mass; + object1.y = object1.y - (overlap * 0.5); + object2.y += overlap * 0.5; + + var newVel1 = Math.sqrt((vel2 * vel2 * mass2) / mass1) * ((vel2 > 0) ? 1 : -1); + var newVel2 = Math.sqrt((vel1 * vel1 * mass1) / mass2) * ((vel1 > 0) ? 1 : -1); + final average = (newVel1 + newVel2) * 0.5; + newVel1 -= average; + newVel2 -= average; + object1.velocity.y = average + (newVel1 * object1.elasticity); + object2.velocity.y = average + (newVel2 * object2.elasticity); + } + + /** + * Checks two objects for overlaps and sets their touching flags, accordingly. + * If either object may be a tilemap, this will check the object against individual tiles + * + * @return Whether the objects in fact touched + */ + public static function updateTouchingFlags(object1:FlxObject, object2:FlxObject):Bool + { + function helper(object1:FlxObject, object2:FlxObject):Bool { - var tilemap:FlxBaseTilemap = cast object2; - return tilemap.overlapsWithCallback(object1, updateTouchingFlagsX, true); + final touchingX:Bool = updateTouchingFlagsXHelper(object1, object2); + final touchingY:Bool = updateTouchingFlagsYHelper(object1, object2); + return touchingX || touchingY; } + return processCheckTilemap(object1, object2, helper, false); + } + + /** + * Checks two objects for overlaps in the X-axis and sets their touching flags, accordingly. + * If either object may be a tilemap, this will check the object against individual tiles + * + * @return Whether the objects are overlapping in the X-axis + */ + public static function updateTouchingFlagsX(object1:FlxObject, object2:FlxObject):Bool + { + return processCheckTilemap(object1, object2, updateTouchingFlagsXHelper, false); + } + + static function updateTouchingFlagsXHelper(object1:FlxObject, object2:FlxObject):Bool + { // Since we are not separating, always return any amount of overlap => false as last parameter return computeOverlapX(object1, object2, false) != 0; } + + /** + * Checks two objects for overlaps in the Y-axis and sets their touching flags, accordingly. + * If either object may be a tilemap, this will check the object against individual tiles + * + * @return Whether the objects are overlapping in the Y-axis + */ + public static function updateTouchingFlagsY(object1:FlxObject, object2:FlxObject):Bool + { + return processCheckTilemap(object1, object2, updateTouchingFlagsYHelper, false); + } + + static function updateTouchingFlagsYHelper(object1:FlxObject, object2:FlxObject):Bool + { + // Since we are not separating, always return any amount of overlap => false as last parameter + return computeOverlapY(object1, object2, false) != 0; + } + + /** + * Internal function that computes overlap among two objects on the X axis. It also updates the `touching` variable. + * `checkMaxOverlap` is used to determine whether we want to exclude (therefore check) overlaps which are + * greater than a certain maximum (linked to `SEPARATE_BIAS`). Default is `true`, handy for `separateX` code. + */ + public static function computeOverlapX(object1:FlxObject, object2:FlxObject, checkMaxOverlap:Bool = true):Float + { + var overlap:Float = 0; + // First, get the two object deltas + final delta1:Float = object1.x - object1.last.x; + final delta2:Float = object2.x - object2.last.x; + if (delta1 != delta2) + { + // Check if the X hulls actually overlap + final delta1Abs:Float = (delta1 > 0) ? delta1 : -delta1; + final delta2Abs:Float = (delta2 > 0) ? delta2 : -delta2; + + final rect1 = FlxRect.get(object1.x - (delta1 > 0 ? delta1 : 0), object1.last.y, object1.width + delta1Abs, object1.height); + final rect2 = FlxRect.get(object2.x - (delta2 > 0 ? delta2 : 0), object2.last.y, object2.width + delta2Abs, object2.height); + + if (rect1.overlaps(rect2)) + { + final maxOverlap:Float = checkMaxOverlap ? (delta1Abs + delta2Abs + SEPARATE_BIAS) : 0; + + inline function canCollide(obj:FlxObject, dir:FlxDirectionFlags) + { + return obj.allowCollisions.has(dir); + } + + // If they do overlap (and can), figure out by how much and flip the corresponding flags + if (delta1 > delta2) + { + overlap = object1.x + object1.width - object2.x; + if ((checkMaxOverlap && overlap > maxOverlap) + || !canCollide(object1, FlxDirectionFlags.RIGHT) + || !canCollide(object2, FlxDirectionFlags.LEFT)) + { + overlap = 0; + } + else + { + object1.touching |= FlxDirectionFlags.RIGHT; + object2.touching |= FlxDirectionFlags.LEFT; + } + } + else if (delta1 < delta2) + { + overlap = object1.x - object2.width - object2.x; + if ((checkMaxOverlap && -overlap > maxOverlap) + || !canCollide(object1, FlxDirectionFlags.LEFT) + || !canCollide(object2, FlxDirectionFlags.RIGHT)) + { + overlap = 0; + } + else + { + object1.touching |= FlxDirectionFlags.LEFT; + object2.touching |= FlxDirectionFlags.RIGHT; + } + } + } + + rect1.put(); + rect2.put(); + } + + return overlap; + } + /** * Internal function that computes overlap among two objects on the Y axis. It also updates the `touching` variable. * `checkMaxOverlap` is used to determine whether we want to exclude (therefore check) overlaps which are * greater than a certain maximum (linked to `SEPARATE_BIAS`). Default is `true`, handy for `separateY` code. */ - @:noCompletion - static function computeOverlapY(object1:FlxObject, object2:FlxObject, checkMaxOverlap:Bool = true):Float + public static function computeOverlapY(object1:FlxObject, object2:FlxObject, checkMaxOverlap:Bool = true):Float { var overlap:Float = 0; // First, get the two object deltas - var obj1delta:Float = object1.y - object1.last.y; - var obj2delta:Float = object2.y - object2.last.y; + final delta1:Float = object1.y - object1.last.y; + final delta2:Float = object2.y - object2.last.y; - if (obj1delta != obj2delta) + if (delta1 != delta2) { // Check if the Y hulls actually overlap - var obj1deltaAbs:Float = (obj1delta > 0) ? obj1delta : -obj1delta; - var obj2deltaAbs:Float = (obj2delta > 0) ? obj2delta : -obj2delta; - - var obj1rect:FlxRect = _firstSeparateFlxRect.set(object1.x, object1.y - ((obj1delta > 0) ? obj1delta : 0), object1.width, - object1.height + obj1deltaAbs); - var obj2rect:FlxRect = _secondSeparateFlxRect.set(object2.x, object2.y - ((obj2delta > 0) ? obj2delta : 0), object2.width, - object2.height + obj2deltaAbs); - - if ((obj1rect.x + obj1rect.width > obj2rect.x) - && (obj1rect.x < obj2rect.x + obj2rect.width) - && (obj1rect.y + obj1rect.height > obj2rect.y) - && (obj1rect.y < obj2rect.y + obj2rect.height)) - { - var maxOverlap:Float = checkMaxOverlap ? (obj1deltaAbs + obj2deltaAbs + SEPARATE_BIAS) : 0; + final delta1Abs:Float = (delta1 > 0) ? delta1 : -delta1; + final delta2Abs:Float = (delta2 > 0) ? delta2 : -delta2; + + final rect1 = FlxRect.get(object1.last.x, object1.y - (delta1 > 0 ? delta1 : 0), object1.width, object1.height + delta1Abs); + final rect2 = FlxRect.get(object2.last.x, object2.y - (delta2 > 0 ? delta2 : 0), object2.width, object2.height + delta2Abs); + if (rect1.overlaps(rect2)) + { + final maxOverlap:Float = checkMaxOverlap ? (delta1Abs + delta2Abs + SEPARATE_BIAS) : 0; + + inline function canCollide(obj:FlxObject, dir:FlxDirectionFlags) + { + return obj.allowCollisions.has(dir); + } + // If they did overlap (and can), figure out by how much and flip the corresponding flags - if (obj1delta > obj2delta) + if (delta1 > delta2) { overlap = object1.y + object1.height - object2.y; if ((checkMaxOverlap && (overlap > maxOverlap)) - || ((object1.allowCollisions & FlxDirectionFlags.DOWN) == 0) - || ((object2.allowCollisions & FlxDirectionFlags.UP) == 0)) + || !canCollide(object1, FlxDirectionFlags.DOWN) + || !canCollide(object2, FlxDirectionFlags.UP)) { overlap = 0; } @@ -439,12 +589,12 @@ class FlxObject extends FlxBasic object2.touching |= FlxDirectionFlags.UP; } } - else if (obj1delta < obj2delta) + else if (delta1 < delta2) { overlap = object1.y - object2.height - object2.y; if ((checkMaxOverlap && (-overlap > maxOverlap)) - || ((object1.allowCollisions & FlxDirectionFlags.UP) == 0) - || ((object2.allowCollisions & FlxDirectionFlags.DOWN) == 0)) + || !canCollide(object1, FlxDirectionFlags.UP) + || !canCollide(object2, FlxDirectionFlags.DOWN)) { overlap = 0; } @@ -455,125 +605,14 @@ class FlxObject extends FlxBasic } } } + + rect1.put(); + rect2.put(); } + return overlap; } - - /** - * The Y-axis component of the object separation process. - * - * @param object1 Any `FlxObject`. - * @param object2 Any other `FlxObject`. - * @return Whether the objects in fact touched and were separated along the Y axis. - */ - public static function separateY(object1:FlxObject, object2:FlxObject):Bool - { - // can't separate two immovable objects - var immovable1:Bool = object1.immovable; - var immovable2:Bool = object2.immovable; - if (immovable1 && immovable2) - { - return false; - } - - // If one of the objects is a tilemap, just pass it off. - if (object1.flixelType == TILEMAP) - { - var tilemap:FlxBaseTilemap = cast object1; - return tilemap.overlapsWithCallback(object2, separateY); - } - if (object2.flixelType == TILEMAP) - { - var tilemap:FlxBaseTilemap = cast object2; - return tilemap.overlapsWithCallback(object1, separateY, true); - } - - var overlap:Float = computeOverlapY(object1, object2); - // Then adjust their positions and velocities accordingly (if there was any overlap) - if (overlap != 0) - { - var delta1 = object1.y - object1.last.y; - var delta2 = object2.y - object2.last.y; - var vel1 = object1.velocity.y; - var vel2 = object2.velocity.y; - var mass1 = object1.mass; - var mass2 = object2.mass; - var massSum = mass1 + mass2; - var elasticity1 = object1.elasticity; - var elasticity2 = object2.elasticity; - - if (!immovable1 && !immovable2) - { - #if FLX_4_LEGACY_COLLISION - overlap *= 0.5; - object1.y = object1.y - overlap; - object2.y += overlap; - - var newVel1 = Math.sqrt((vel2 * vel2 * mass2) / mass1) * ((vel2 > 0) ? 1 : -1); - var newVel2 = Math.sqrt((vel1 * vel1 * mass1) / mass2) * ((vel1 > 0) ? 1 : -1); - var average = (newVel1 + newVel2) * 0.5; - newVel1 -= average; - newVel2 -= average; - object1.velocity.y = average + newVel1 * elasticity1; - object2.velocity.y = average + newVel2 * elasticity2; - #else - object1.y -= overlap / 2; - object2.y += overlap / 2; - - var momentum = mass1 * vel1 + mass2 * vel2; - var newVel1 = (momentum + elasticity1 * mass2 * (vel2 - vel1)) / massSum; - var newVel2 = (momentum + elasticity2 * mass1 * (vel1 - vel2)) / massSum; - object1.velocity.y = newVel1; - object2.velocity.y = newVel2; - #end - } - else if (!immovable1) - { - object1.y -= overlap; - object1.velocity.y = vel2 - vel1 * elasticity1; - } - else if (!immovable2) - { - object2.y += overlap; - object2.velocity.y = vel1 - vel2 * elasticity2; - } - - // use collisionDrag properties to determine whether one object - if (allowCollisionDrag(object1.collisionXDrag, object1, object2) && delta1 > delta2) - object1.x += object2.x - object2.last.x; - else if (allowCollisionDrag(object2.collisionXDrag, object2, object1) && delta2 > delta1) - object2.x += object1.x - object1.last.x; - - return true; - } - - return false; - } - - /** - * Checking overlap and updating touching variables, Y-axis part used by `updateTouchingFlags`. - * - * @param object1 Any `FlxObject`. - * @param object2 Any other `FlxObject`. - * @return Whether the objects in fact touched along the Y axis. - */ - public static function updateTouchingFlagsY(object1:FlxObject, object2:FlxObject):Bool - { - // If one of the objects is a tilemap, just pass it off. - if (object1.flixelType == TILEMAP) - { - var tilemap:FlxBaseTilemap = cast object1; - return tilemap.overlapsWithCallback(object2, updateTouchingFlagsY); - } - if (object2.flixelType == TILEMAP) - { - var tilemap:FlxBaseTilemap = cast object2; - return tilemap.overlapsWithCallback(object1, updateTouchingFlagsY, true); - } - // Since we are not separating, always return any amount of overlap => false as last parameter - return computeOverlapY(object1, object2, false) != 0; - } - + /** * X position of the upper left corner of this object in world space. */ @@ -1299,19 +1338,27 @@ class FlxObject extends FlxBasic function drawDebugBoundingBox(gfx:Graphics, rect:FlxRect, allowCollisions:Int, partial:Bool) { // Find the color to use - var color:Null = debugBoundingBoxColor; - if (color == null) - { - if (allowCollisions != FlxDirectionFlags.NONE) - { - color = partial ? debugBoundingBoxColorPartial : debugBoundingBoxColorSolid; - } - else - { - color = debugBoundingBoxColorNotSolid; - } - } - + final color = getDebugBoundingBoxColor(allowCollisions); + drawDebugBoundingBoxColor(gfx, rect, color); + } + + function getDebugBoundingBoxColor(allowCollisions:Int) + { + if (debugBoundingBoxColor != null) + return debugBoundingBoxColor; + + if (allowCollisions == FlxDirectionFlags.NONE) + return debugBoundingBoxColorNotSolid; + + if (allowCollisions == FlxDirectionFlags.ANY) + return debugBoundingBoxColorSolid; + + return debugBoundingBoxColorPartial; + + } + + function drawDebugBoundingBoxColor(gfx:Graphics, rect:FlxRect, color:FlxColor) + { // fill static graphics object with square shape gfx.lineStyle(1, color, 0.75); gfx.drawRect(rect.x + 0.5, rect.y + 0.5, rect.width - 1.0, rect.height - 1.0); diff --git a/flixel/math/FlxRect.hx b/flixel/math/FlxRect.hx index 63e380a906..31387c58ed 100644 --- a/flixel/math/FlxRect.hx +++ b/flixel/math/FlxRect.hx @@ -212,15 +212,18 @@ class FlxRect implements IFlxPooled } /** - * Checks to see if some FlxRect object overlaps this FlxRect object. + * Checks to see if this rectangle overlaps another * - * @param Rect The rectangle being tested. - * @return Whether or not the two rectangles overlap. + * @param rect The other rectangle + * @return Whether the two rectangles overlap */ - public inline function overlaps(Rect:FlxRect):Bool + public inline function overlaps(rect:FlxRect):Bool { - var result = (Rect.x + Rect.width > x) && (Rect.x < x + width) && (Rect.y + Rect.height > y) && (Rect.y < y + height); - Rect.putWeak(); + final result = rect.right > left + && rect.left < right + && rect.bottom > top + && rect.top < bottom; + rect.putWeak(); return result; } diff --git a/flixel/path/FlxPathfinder.hx b/flixel/path/FlxPathfinder.hx index 73ac6a04b1..56a77fe2bf 100644 --- a/flixel/path/FlxPathfinder.hx +++ b/flixel/path/FlxPathfinder.hx @@ -617,10 +617,8 @@ class FlxTypedPathfinderData> public function hasValidStartEnd() { - return startIndex >= 0 - && endIndex >= 0 - && startIndex < map.totalTiles - && endIndex < map.totalTiles; + return map.tileExists(startIndex) + && map.tileExists(endIndex); } public function destroy() @@ -647,7 +645,7 @@ class FlxTypedPathfinderData> */ inline function getX(tile:Int) { - return tile % map.widthInTiles; + return map.getColumn(tile); } /** @@ -655,7 +653,7 @@ class FlxTypedPathfinderData> */ inline function getY(tile:Int) { - return Std.int(tile / map.widthInTiles); + return map.getRow(tile); } /** @@ -664,7 +662,7 @@ class FlxTypedPathfinderData> inline function getTileCollisionsByIndex(tile:Int) { #if debug numChecks++; #end - return map.getTileCollisions(map.getTileByIndex(tile)); + return map.getTileData(tile).allowCollisions; } } diff --git a/flixel/tile/FlxBaseTilemap.hx b/flixel/tile/FlxBaseTilemap.hx index a3a1a8a642..6ad1ac97d8 100644 --- a/flixel/tile/FlxBaseTilemap.hx +++ b/flixel/tile/FlxBaseTilemap.hx @@ -16,6 +16,7 @@ import openfl.display.BitmapData; using StringTools; +@:autoBuild(flixel.system.macros.FlxMacroUtil.deprecateOverride("overlapsWithCallback", "overlapsWithCallback is deprecated, use objectOverlapsTiles")) class FlxBaseTilemap extends FlxObject { /** @@ -134,17 +135,39 @@ class FlxBaseTilemap extends FlxObject { throw "computeDimensions must be implemented"; } - + + /** + * Finds the row number that overlaps the given Y in world space + * @param worldY A Y coordinate in the world + * @param bind If true, it will prevent out of range values + * @return A row index, where 0 is the top-most row + * @since 5.9.0 + */ + public function getRowAt(worldY:Float, bind = false):Int + { + throw "getRowAt must be implemented"; + } + + /** + * Finds the row number that overlaps the given X in world space + * @param worldX A X coordinate in the world + * @param bind If true, it will prevent out of range values + * @return A column index, where 0 is the left-most column + * @since 5.9.0 + */ + public function getColumnAt(worldX:Float, bind = false):Int + { + throw "getColumnAt must be implemented"; + } + public function getTileIndexByCoords(coord:FlxPoint):Int { throw "getTileIndexByCoords must be implemented"; - return 0; } public function getTileCoordsByIndex(index:Int, midpoint = true):FlxPoint { throw "getTileCoordsByIndex must be implemented"; - return null; } /** @@ -229,13 +252,68 @@ class FlxBaseTilemap extends FlxObject { return calcRayEntry(end, start, result); } - - public function overlapsWithCallback(object:FlxObject, ?callback:(FlxObject,FlxObject)->Bool, flipCallbackParams = false, ?position:FlxPoint):Bool + + /** + * 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 + * + * @param object The object + * @param filter Function that takes a tile and returns whether is satisfies the + * disired condition, if `null`, any overlapping tile will satisfy + * @param position Optional, specify a custom position for the tilemap + * @return Whether any overlapping tile satisfied the condition, if there was one + * @since 5.9.0 + */ + public function isOverlappingTile(object:FlxObject, ?filter:(tile:Tile)->Bool, ?position:FlxPoint):Bool { throw "overlapsWithCallback must be implemented"; - return false; } - + + /** + * Calls the given function on ever tile that is overlapping the target object + * + * @param object The object + * @param filter Function that takes a tile and returns whether is satisfies the + * disired condition + * @param position Optional, specify a custom position for the tilemap + * @return Whether any overlapping tile was found + * @since 5.9.0 + */ + public function forEachOverlappingTile(object:FlxObject, func:(tile:Tile)->Void, ?position:FlxPoint):Bool + { + throw "overlapsWithCallback must be implemented"; + } + + @:deprecated("overlapsWithCallback is deprecated, use objectOverlapsTiles(object, callback, pos), instead") + public function overlapsWithCallback(object:FlxObject, ?callback:(FlxObject, FlxObject)->Bool, flipCallbackParams = false, ?position:FlxPoint):Bool + { + return objectOverlapsTiles(object, (t, o)->{ return flipCallbackParams ? callback(o, t) : callback(t, o); }, position); + } + + /** + * Checks if the Object overlaps any tiles with any collision flags set, + * and calls the specified callback function (if there is one). + * Also calls the tile's registered callback if the filter matches. + * + * **Note:** To flip the callback params you can simply swap them in a arrow func, like so: + * ```haxe + * final result = objectOverlapsTiles(obj, (tile, obj)->myCallback(obj, tile)); + * ``` + * + * @param object The FlxObject you are checking for overlaps against + * @param callback An optional function that takes the overlapping tile and object + * where `a` is a `FlxTile`, and `b` is the given `object` paaram + * @param position Optional, specify a custom position for the tilemap (see `overlapsAt`) + * @param isCollision If true, tiles where `allowCollisions` is `NONE` are excluded, + * and the tiles' `onCollide` is dispatched + * @return Whether there were overlaps that resulted in a positive callback, if one was specified + * @since 5.9.0 + */ + public function objectOverlapsTiles(object:TObj, ?callback:(Tile, TObj)->Bool, ?position:FlxPoint, isCollision = true):Bool + { + throw "objectOverlapsTiles must be implemented"; + } + public function setDirty(dirty:Bool = true):Void { throw "setDirty must be implemented"; @@ -662,135 +740,329 @@ class FlxBaseTilemap extends FlxObject throw "You must provide valid 'randomChoices' if you wish to randomize tilemap indices, please read documentation of 'setCustomTileMappings' function."; } } - + /** - * Check the value of a particular tile. + * Calculates a mapIndex via `row * widthInTiles + column` + * + * @param column the grid X location, in tiles + * @param row the grid Y location, in tiles + * @since 5.9.0 + */ + public inline function getMapIndex(column:Int, row:Int):Int + { + return row * widthInTiles + column; + } + + /** + * Calculates the column from a map location + * + * @param mapIndex The location in the map where `mapIndex = row * widthInTiles + column` + * @since 5.9.0 + */ + public inline function getColumn(mapIndex:Int):Int + { + return mapIndex % widthInTiles; + } + + /** + * Calculates the column from a map location + * + * @param mapIndex The location in the map where `mapIndex = row * widthInTiles + column` + * @since 5.9.0 + */ + public inline function getRow(mapIndex:Int):Int + { + return Std.int(mapIndex / widthInTiles); + } + + /** + * Whether a tile exists at the given map location * - * @param x The X coordinate of the tile (in tiles, not pixels). - * @param y The Y coordinate of the tile (in tiles, not pixels). - * @return An integer containing the value of the tile at this spot in the array. + * @param row The grid X coordinate of the tile (in tiles, not pixels) + * @param column The grid Y coordinate of the tile (in tiles, not pixels) + * @since 5.9.0 */ - public function getTile(x:Int, y:Int):Int + public overload extern inline function tileExists(column:Int, row:Int):Bool { - return _data[y * widthInTiles + x]; + return columnExists(column) && rowExists(row); } - + + /** + * Whether a tile exists at the given map location + * + * **Note:** A tile's mapIndex can be calculated via `row * widthInTiles + column` + * + * @param mapIndex The desired location in the map + * @since 5.9.0 + */ + public overload extern inline function tileExists(mapIndex:Int):Bool + { + return mapIndex >= 0 && mapIndex < _data.length; + } + + /** + * Whether a row exists at the given map location + * + * @param column The desired location in the map + * @since 5.9.0 + */ + public overload extern inline function columnExists(column:Int):Bool + { + return column >= 0 && column < widthInTiles; + } + /** - * Get the value of a tile in the tilemap by index. + * Whether a row exists at the given map location * - * @param index The slot in the data array (Y * widthInTiles + X) where this tile is stored. + * @param row The desired location in the map + * @since 5.9.0 + */ + public overload extern inline function rowExists(row:Int):Bool + { + return row >= 0 && row < heightInTiles; + } + + /** + * Finds the tile instance at a particular column and row + * + * @param row The grid X coordinate of the tile (in tiles, not pixels) + * @param column The grid Y coordinate of the tile (in tiles, not pixels) + * @return The tile index of the tile at this location + * @since 5.9.0 + */ + public overload extern inline function getTileData(column:Int, row:Int):Null + { + return getTileData(getMapIndex(column, row)); + } + + /** + * Finds the tile instance with the given mapIndex + * + * **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 * @return An integer containing the value of the tile at this spot in the array. + * @since 5.9.0 */ - public function getTileByIndex(index:Int):Int + public overload extern inline function getTileData(mapIndex:Int):Null { - return _data[index]; + return _tileObjects[getTileIndex(mapIndex)]; } - + + /** + * Check the value of a particular tile. + * + * @param row The grid X coordinate of the tile (in tiles, not pixels) + * @param column The grid Y coordinate of the tile (in tiles, not pixels) + * @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 + { + return getTileIndex(getMapIndex(column, row)); + } + + /** + * Get the `tileIndex` at the given map location + * + * **Note:** A tile's `mapIndex` can be calculated via `row * widthInTiles + column` + * + * @param mapIndex The desired location in the map + * @return The tileIndex of the tile with this `mapIndex` + * @since 5.9.0 + */ + public overload extern inline function getTileIndex(mapIndex:Int):Int + { + return _data[mapIndex]; + } + /** - * Gets the collision flags of tile by index. + * Check the value of a particular tile. * - * @param index Tile index returned by getTile or getTileByIndex + * @param row The grid X coordinate of the tile (in tiles, not pixels) + * @param column The grid Y coordinate of the tile (in tiles, not pixels) + * @return The tile index of the tile at this location + */ + @:deprecated("getTile is deprecated use getTileIndex(column, row), instead") + public function getTile(column:Int, row:Int):Int + { + return getTileIndex(column, row); + } + + /** + * Get the `tileIndex` at the given map location + * + * **Note:** A tile's `mapIndex` can be calculated via `row * widthInTiles + column` + * + * @param mapIndex The desired location in the map + * @return An integer containing the value of the tile at this spot in the array. + */ + @:deprecated("getTileByIndex is deprecated use getTileIndex(mapIndex), instead") + public function getTileByIndex(mapIndex:Int):Int + { + return getTileIndex(mapIndex); + } + + /** + * Gets the collision flags of the tile at the given location + * + * **Note:** A tile's `mapIndex` can be calculated via `row * widthInTiles + column` + * + * ##Soft Deprecation + * You should use `getTileData(mapIndex).allowCollisions`, instead + * + * @param mapIndex The desired location in the map * @return The internal collision flag for the requested tile. */ - public function getTileCollisions(index:Int):FlxDirectionFlags + public function getTileCollisions(mapIndex:Int):FlxDirectionFlags { - return _tileObjects[index].allowCollisions; + return getTileData(mapIndex).allowCollisions; } - + /** - * Returns a new array full of every map index of the requested tile type. + * Returns a new array full of every map index of the requested tile type + * + * **Note:** Unlike `getAllMapIndices` this will return `null` if no tiles are found * * @param index The requested tile type. * @return An Array with a list of all map indices of that tile type. */ - public function getTileInstances(index:Int):Array + @:deprecated("getTileInstances is deprecated, use getTileIndices, instead")// 5.9.0 + public inline function getTileInstances(tileIndex:Int):Array { - var array:Array = null; - var i:Int = 0; - var l:Int = widthInTiles * heightInTiles; - - while (i < l) + // for backwards compat, return `null` if none are found + final result = getAllMapIndices(tileIndex); + return result.length == 0 ? null : result; + } + + /** + * Returns a new array full of every map index of the requested tile type. + * + * **Note:** Unlike `getTileInstances` this will return `[]` if no tiles are found + * + * @param index The requested tile type. + * @return An Array with a list of all map indices of that tile type. + * @since 5.9.0 + */ + public function getAllMapIndices(tileIndex:Int):Array + { + final result:Array = []; + var i:Int = widthInTiles * heightInTiles; + + while (i-- > 0) { - if (_data[i] == index) + if (_data[i] == tileIndex) { - if (array == null) - { - array = []; - } - array.push(i); + result.unshift(i); } - i++; } - - return array; + + return result; } - + /** * Change the data and graphic of a tile in the tilemap. * - * @param x The X coordinate of the tile (in tiles, not pixels). - * @param y The Y coordinate of the tile (in tiles, not pixels). - * @param tile The new integer data you wish to inject. + * @param mapIndex The slot in the data array (Y * widthInTiles + X) where this tile is stored. + * @param tileIndex The new tileIndex to place at the mapIndex * @param updateGraphics Whether the graphical representation of this tile should change. * @return Whether or not the tile was actually changed. + * @since 5.9.0 */ - public function setTile(x:Int, y:Int, tile:Int, updateGraphics = true):Bool + public overload extern inline function setTileIndex(mapIndex:Int, tileIndex:Int, updateGraphics = true):Bool { - if ((x >= widthInTiles) || (y >= heightInTiles)) - { - return false; - } - - return setTileByIndex(y * widthInTiles + x, tile, updateGraphics); + return setTileHelper(mapIndex, tileIndex, updateGraphics); } - + /** * Change the data and graphic of a tile in the tilemap. * - * @param index The slot in the data array (Y * widthInTiles + X) where this tile is stored. - * @param tile The new integer data you wish to inject. + * @param row The grid X coordinate of the tile (in tiles, not pixels) + * @param column The grid Y coordinate of the tile (in tiles, not pixels) + * @param tileIndex The new integer data you wish to inject. * @param updateGraphics Whether the graphical representation of this tile should change. * @return Whether or not the tile was actually changed. + * @since 5.9.0 */ - public function setTileByIndex(index:Int, tile:Int, updateGraphics = true):Bool + public overload extern inline function setTileIndex(column:Int, row:Int, tileIndex:Int, updateGraphics = true):Bool { - if (index >= _data.length) - { + return setTileHelper(getMapIndex(column, row), tileIndex, updateGraphics); + } + + /** + * Change the data and graphic of a tile in the tilemap. + * + * @param row The grid X coordinate of the tile (in tiles, not pixels) + * @param column The grid Y coordinate of the tile (in tiles, not pixels) + * @param tileIndex The new integer data you wish to inject. + * @param updateGraphics Whether the graphical representation of this tile should change. + * @return Whether or not the tile was actually changed. + */ + @:deprecated("setTile is deprecated, use setTileIndex(column, row, tileIndex,...), instead") + public function setTile(column:Int, row:Int, tileIndex:Int, updateGraphics = true):Bool + { + return setTileIndex(getMapIndex(column, row), tileIndex, updateGraphics); + } + + /** + * Change the data and graphic of a tile in the tilemap. + * + * @param mapIndex The slot in the data array (Y * widthInTiles + X) where this tile is stored. + * @param tileIndex The new tileIndex to place at the mapIndex + * @param updateGraphics Whether the graphical representation of this tile should change. + * @return Whether or not the tile was actually changed. + */ + @:deprecated("setTileByIndex is deprecated, use setTileIndex(mapIndex, tileIndex,...), instead") + public function setTileByIndex(mapIndex:Int, tileIndex:Int, updateGraphics = true):Bool + { + return setTileIndex(mapIndex, tileIndex, updateGraphics); + } + + function setTileHelper(mapIndex:Int, tileIndex:Int, updateGraphics = true):Bool + { + if (!tileExists(mapIndex)) return false; - } - - var ok:Bool = true; - _data[index] = tile; - + + _data[mapIndex] = tileIndex; + if (!updateGraphics) { - return ok; + return true; } - + setDirty(); - - if (auto == OFF) + + switch (auto) { - updateTile(_data[index]); - return ok; + case OFF: + updateTile(_data[mapIndex]); + default: + updateTileWithAutoTile(mapIndex); } - + + return true; + } + + function updateTileWithAutoTile(mapIndex:Int) + { // If this map is auto-tiled and it changes, locally update the arrangement - var i:Int; - var row:Int = Std.int(index / widthInTiles) - 1; - var rowLength:Int = row + 3; - var column:Int = index % widthInTiles - 1; - var columnHeight:Int = column + 3; - + var row:Int = getRow(mapIndex) - 1; + var column:Int = getColumn(mapIndex) - 1; + final rowLength:Int = row + 3; + final columnHeight:Int = column + 3; + while (row < rowLength) { column = columnHeight - 3; - + while (column < columnHeight) { - if ((row >= 0) && (row < heightInTiles) && (column >= 0) && (column < widthInTiles)) + if (tileExists(column, row)) { - i = row * widthInTiles + column; + final i = getMapIndex(column, row); autoTile(i); updateTile(_data[i]); } @@ -798,13 +1070,11 @@ class FlxBaseTilemap extends FlxObject } row++; } - - return ok; } /** * Adjust collision settings and/or bind a callback function to a range of tiles. - * This callback function, if present, is triggered by calls to overlap() or overlapsWithCallback(). + * This callback function, if present, is triggered by calls to `overlap` or `objectOverlapsTiles`. * * @param tile The tile or tiles you want to adjust. * @param allowCollisions Modify the tile or tiles to only allow collisions from certain directions, use FlxObject constants NONE, ANY, LEFT, RIGHT, etc. Default is "ANY". @@ -855,22 +1125,13 @@ class FlxBaseTilemap extends FlxObject public function getData(simple:Bool = false):Array { if (!simple) - { return _data; - } - - var i:Int = 0; - var l:Int = _data.length; - var data:Array = new Array(); - FlxArrayUtil.setLength(data, l); - - while (i < l) - { - data[i] = (_tileObjects[_data[i]].allowCollisions > 0) ? 1 : 0; - i++; - } - - return data; + + return + [ + for (i in 0..._data.length) + (getTileData(i).solid ? 1 : 0) + ]; } /** @@ -982,7 +1243,7 @@ class FlxBaseTilemap extends FlxObject { if (objectOrGroup.flixelType == OBJECT || objectOrGroup.flixelType == TILEMAP) { - return overlapsWithCallback(cast objectOrGroup); + return objectOverlapsTiles(cast objectOrGroup); } else { @@ -1003,7 +1264,7 @@ class FlxBaseTilemap extends FlxObject * @return Whether or not the two objects overlap. */ @:access(flixel.group.FlxTypedGroup) - override public function overlapsAt(x:Float, y:Float, objectOrGroup:FlxBasic, inScreenSpace:Bool = false, ?camera:FlxCamera):Bool + override function overlapsAt(x:Float, y:Float, objectOrGroup:FlxBasic, inScreenSpace:Bool = false, ?camera:FlxCamera):Bool { final group = FlxTypedGroup.resolveGroup(objectOrGroup); if (group != null) // if it is a group @@ -1016,7 +1277,7 @@ class FlxBaseTilemap extends FlxObject { if (objectOrGroup.flixelType == OBJECT || objectOrGroup.flixelType == TILEMAP) { - return overlapsWithCallback(cast objectOrGroup, null, false, _point.set(x, y)); + return objectOverlapsTiles(cast objectOrGroup, null, _point.set(x, y)); } else { @@ -1048,10 +1309,8 @@ class FlxBaseTilemap extends FlxObject function tileAtPointAllowsCollisions(point:FlxPoint):Bool { - var tileIndex = getTileIndexByCoords(point); - if (tileIndex < 0 || tileIndex >= _data.length) - return false; - return _tileObjects[_data[tileIndex]].allowCollisions > 0; + final mapIndex = getTileIndexByCoords(point); + return tileExists(mapIndex) && getTileData(mapIndex).solid; } /** diff --git a/flixel/tile/FlxTile.hx b/flixel/tile/FlxTile.hx index fe7b9c8fe7..41a4eb91bc 100644 --- a/flixel/tile/FlxTile.hx +++ b/flixel/tile/FlxTile.hx @@ -4,6 +4,7 @@ import flixel.FlxObject; import flixel.graphics.frames.FlxFrame; import flixel.tile.FlxTilemap; import flixel.util.FlxDirectionFlags; +import flixel.util.FlxSignal; /** * A simple helper object for FlxTilemap that helps expand collision opportunities and control. @@ -17,8 +18,14 @@ class FlxTile extends FlxObject * This function should take the form myFunction(Tile:FlxTile,Object:FlxObject):void. * Defaults to null, set through FlxTilemap.setTileProperties(). */ - public var callbackFunction:FlxObject->FlxObject->Void = null; - + public var callbackFunction:(FlxObject, FlxObject)->Void = null; + + /** + * Dispatched whenever FlxG.collide resolves a collision with a tile of this type + * @since 5.9.0 + */ + public var onCollide = new FlxTypedSignal<(FlxTile, FlxObject)->Void>(); + /** * Each tile can store its own filter class for their callback functions. * That is, the callback will only be triggered if an object with a class @@ -73,16 +80,103 @@ class FlxTile extends FlxObject this.visible = visible; this.allowCollisions = allowCollisions; } - + /** * Clean up memory. */ override public function destroy():Void { + super.destroy(); + callbackFunction = null; tilemap = null; frame = null; - - super.destroy(); + onCollide.removeAll(); + } + + /** + * Whether this tile overlaps the object. this should be called directly after calling + * `orient` to ensure this tile is in the correct world space. + * + * This method is dynamic, meaning you can set custom behavior per tile, without extension. + * @since 5.9.0 + */ + public dynamic function overlapsObject(object:FlxObject):Bool + { + return object.x + object.width > x + && object.x < x + width + && object.y + object.height > y + && object.y < y + height + && (filter == null || Std.isOfType(object, filter)); + } + + /** + * Places this tile in the world according to the desired map location. + * often used before calling `overlapsObject` + * + * This method is dynamic, meaning you can set custom behavior per tile, without extension. + * + * @param xPos May be the true or a theoretical x of the map, based on what called this + * @param yPos May be the true or a theoretical y of the map, based on what called this + * @param col The tilemap column where this is being placed + * @param row The tilemap row where this is being placed + * @since 5.9.0 + */ + public dynamic function orientAt(xPos:Float, yPos:Float, col:Int, row:Int) + { + mapIndex = (row * tilemap.widthInTiles) + col; + width = tilemap.scaledTileWidth; + height = tilemap.scaledTileHeight; + x = xPos + col * width; + y = yPos + row * height; + last.x = x - xPos - tilemap.last.x; + last.y = y - yPos - tilemap.last.y; + } + + /** + * Places this tile in the world according to the desired map location. + * often used before calling `overlapsObject` + * + * Calls `orientAt` with the tilemap's current position + * + * @param col The tilemap column where this is being placed + * @param row The tilemap row where this is being placed + * @since 5.9.0 + */ + public inline function orient(col:Int, row:Int) + { + orientAt(tilemap.x, tilemap.y, col, row); + } + + /** + * Places this tile in the world according to the desired map location. + * often used before calling `overlapsObject` + * + * Calls `orientAt` with the tilemap's current position + * + * **Note:** A tile's mapIndex can be calculated via `row * widthInTiles + column` + * + * @param mapIndex The desired location in the map + * @since 5.9.0 + */ + public inline function orientByIndex(mapIndex:Int) + { + orientAtByIndex(tilemap.x, tilemap.y, mapIndex); + } + + /** + * Places this tile in the world according to the desired map location. + * often used before calling `overlapsObject` + * + * Calls `orientAt` with the tilemap's current position + * + * **Note:** A tile's mapIndex can be calculated via `row * widthInTiles + column` + * + * @param mapIndex The desired location in the map + * @since 5.9.0 + */ + public inline function orientAtByIndex(xPos:Float, yPos:Float, mapIndex:Int) + { + orientAt(xPos, yPos, tilemap.getColumn(mapIndex), tilemap.getRow(mapIndex)); } } diff --git a/flixel/tile/FlxTilemap.hx b/flixel/tile/FlxTilemap.hx index 473646d6b8..ea702a3cc6 100644 --- a/flixel/tile/FlxTilemap.hx +++ b/flixel/tile/FlxTilemap.hx @@ -130,8 +130,10 @@ class FlxTilemap extends FlxTypedTilemap super(); } - override function createTile(index, width, height, visible, allowCollisions):FlxTile + override function createTile(index:Int, width, height):FlxTile { + final visible = index >= _drawIndex; + final allowCollisions = index >= _collideIndex ? this.allowCollisions : NONE; return new FlxTile(this, index, width, height, visible, allowCollisions); } } @@ -380,7 +382,7 @@ class FlxTypedTilemap extends FlxBaseTilemap length += _startingIndex; for (i in 0...length) - _tileObjects[i] = createTile(i, tileWidth, tileHeight, (i >= _drawIndex), (i >= _collideIndex) ? allowCollisions : NONE); + _tileObjects[i] = createTile(i, tileWidth, tileHeight); // Create debug tiles for rendering bounding boxes on demand #if FLX_DEBUG @@ -390,7 +392,7 @@ class FlxTypedTilemap extends FlxBaseTilemap #end } - function createTile(index, width, height, visible, allowCollisions):Tile + function createTile(index, width, height):Tile { throw "createTile not implemented"; } @@ -573,11 +575,9 @@ class FlxTypedTilemap extends FlxBaseTilemap // Copied from getScreenPosition() _helperPoint.x = x - camera.scroll.x * scrollFactor.x; _helperPoint.y = y - camera.scroll.y * scrollFactor.y; - - var rectWidth:Float = scaledTileWidth; - var rectHeight:Float = scaledTileHeight; - var rect = FlxRect.get(0, 0, rectWidth, rectHeight); - + + final rect = FlxRect.get(0, 0, scaledTileWidth, scaledTileHeight); + // Copy tile images into the tile buffer // Modified from getScreenPosition() _point.x = (camera.scroll.x * scrollFactor.x) - x; @@ -599,21 +599,30 @@ class FlxTypedTilemap extends FlxBaseTilemap for (column in 0...screenColumns) { - final tile = _tileObjects[_data[columnIndex]]; + final tile = getTileData(columnIndex); - if (tile != null && tile.visible) + if (tile != null && tile.visible && !tile.ignoreDrawDebug) { - rect.x = _helperPoint.x + (columnIndex % widthInTiles) * rectWidth; - rect.y = _helperPoint.y + Math.floor(columnIndex / widthInTiles) * rectHeight; - drawDebugBoundingBox(camera.debugLayer.graphics, rect, tile.allowCollisions, tile.allowCollisions != ANY); + rect.x = _helperPoint.x + (columnIndex % widthInTiles) * rect.width; + rect.y = _helperPoint.y + Math.floor(columnIndex / widthInTiles) * rect.height; + + final color = tile.debugBoundingBoxColor != null + ? tile.debugBoundingBoxColor + : getDebugBoundingBoxColor(tile.allowCollisions); + + if (color != null) + { + final colStr = color.toHexString(); + drawDebugBoundingBoxColor(camera.debugLayer.graphics, rect, color); + } } columnIndex++; } - + rowIndex += widthInTiles; } - + rect.put(); } #end @@ -723,104 +732,122 @@ class FlxTypedTilemap extends FlxBaseTilemap if (buffer != null) buffer.dirty = dirty; } - - /** - * Checks if the Object overlaps any tiles with any collision flags set, - * and calls the specified callback function (if there is one). - * Also calls the tile's registered callback if the filter matches. - * - * @param object The FlxObject you are checking for overlaps against. - * @param callback An optional function that takes the form `myCallback(a:FlxObject, b:FlxObject)`, - * where `a` is a `FlxTile`, and `b` is the given `object` paaram. - * @param flipCallbackParams Used to preserve A-B list ordering from `FlxObject.separate()`, - * returns the `FlxTile` object as the second parameter instead. - * @param position Optional, specify a custom position for the tilemap (used for `overlapsAt`). - * @return Whether there were overlaps, and the result of the callback, if one was specified. - */ - override public function overlapsWithCallback(object:FlxObject, ?callback:FlxObject->FlxObject->Bool, flipCallbackParams:Bool = false, - ?position:FlxPoint):Bool + + override function isOverlappingTile(object:FlxObject, ?filter:(tile:Tile)->Bool, ?position:FlxPoint) + { + return forEachOverlappingTileHelper(object, filter, position, true); + } + + override function forEachOverlappingTile(object:FlxObject, func:(tile:Tile)->Void, ?position:FlxPoint):Bool + { + function filter(tile) + { + // call func on every overlapping tile + func(tile); + + // return true, since an overlapping tile was found + return true; + } + + return forEachOverlappingTileHelper(object, filter, position, false); + } + + function forEachOverlappingTileHelper(object:FlxObject, ?filter:(tile:Tile)->Bool, ?position:FlxPoint, stopAtFirst:Bool):Bool { - var results = false; - var xPos = x; var yPos = y; - + if (position != null) { xPos = position.x; yPos = position.y; position.putWeak(); } - + inline function bindInt(value:Int, min:Int, max:Int) { return Std.int(FlxMath.bound(value, min, max)); } - + // Figure out what tiles we need to check against, and bind them by the map edges final minTileX:Int = bindInt(Math.floor((object.x - xPos) / scaledTileWidth), 0, widthInTiles); final minTileY:Int = bindInt(Math.floor((object.y - yPos) / scaledTileHeight), 0, heightInTiles); final maxTileX:Int = bindInt(Math.ceil((object.x + object.width - xPos) / scaledTileWidth), 0, widthInTiles); final maxTileY:Int = bindInt(Math.ceil((object.y + object.height - yPos) / scaledTileHeight), 0, heightInTiles); - - // Loop through the range of tiles and call the callback on them, accordingly - final deltaX:Float = xPos - last.x; - final deltaY:Float = yPos - last.y; - + + var result = false; for (row in minTileY...maxTileY) { for (column in minTileX...maxTileX) { - final mapIndex:Int = (row * widthInTiles) + column; - final dataIndex:Int = _data[mapIndex]; - if (dataIndex < 0) - continue; - - final tile = _tileObjects[dataIndex]; - tile.width = scaledTileWidth; - tile.height = scaledTileHeight; - tile.x = xPos + column * tile.width; - tile.y = yPos + row * tile.height; - tile.last.x = tile.x - deltaX; - tile.last.y = tile.y - deltaY; - - var overlapFound = ((object.x + object.width) > tile.x) - && (object.x < (tile.x + tile.width)) - && ((object.y + object.height) > tile.y) - && (object.y < (tile.y + tile.height)); - - if (tile.allowCollisions != NONE) + final tile = getTileData(column, row); + tile.orientAt(xPos, yPos, column, row); + if (tile.overlapsObject(object) && (filter == null || filter(tile))) { - if (callback != null) - { - if (flipCallbackParams) - { - overlapFound = callback(object, tile); - } - else - { - overlapFound = callback(tile, object); - } - } + if (stopAtFirst) + return true; + + result = true; } - - if (overlapFound) + } + } + + return result; + } + + override function objectOverlapsTiles(object:TObj, ?callback:(Tile, TObj)->Bool, ?position:FlxPoint, isCollision = true):Bool + { + var results = false; + + function each(tile:Tile) + { + var overlapFound = tile.solid || !isCollision; + if (overlapFound && callback != null) + { + overlapFound = callback(tile, object); + } + + if (overlapFound) + { + if (tile.callbackFunction != null) { - if (tile.callbackFunction != null && (tile.filter == null || Std.isOfType(object, tile.filter))) - { - tile.mapIndex = mapIndex; - tile.callbackFunction(tile, object); - } - - if (tile.allowCollisions != NONE) - results = true; + tile.callbackFunction(tile, object); + } + + // check again in case callback changed it (for backwards compatibility) + if (tile.solid || !isCollision) + { + tile.onCollide.dispatch(tile, object); + results = true; } } } - + + forEachOverlappingTile(object, each, position); + return results; } - + + override function getColumnAt(worldX:Float, bind = false):Int + { + final result = Math.floor(worldX / scaledTileWidth); + + if (bind) + return result < 0 ? 0 : (result >= widthInTiles ? widthInTiles - 1 : result); + + return result; + } + + override function getRowAt(worldY:Float, bind = false):Int + { + final result = Math.floor(worldY / scaledTileHeight); + + if (bind) + return result < 0 ? 0 : (result >= heightInTiles ? heightInTiles -1 : result); + + return result; + } + override public function getTileIndexByCoords(coord:FlxPoint):Int { var localX = coord.x - x; @@ -943,7 +970,7 @@ class FlxTypedTilemap extends FlxBaseTilemap final endIndex = getTileIndexByCoords(end); // If the starting tile is solid, return the starting position - if (getTileCollisions(getTileByIndex(startIndex)) != NONE) + if (getTileData(startIndex).allowCollisions != NONE) { if (result != null) result.copyFrom(start); @@ -1050,8 +1077,8 @@ class FlxTypedTilemap extends FlxBaseTilemap final step = startY <= endY ? 1 : -1; while (true) { - var index = y * widthInTiles + x; - if (getTileCollisions(getTileByIndex(index)) != NONE) + var index = getMapIndex(x, y); + if (getTileData(index).solid) return index; if (y == endY) @@ -1093,8 +1120,6 @@ class FlxTypedTilemap extends FlxBaseTilemap var stepY:Float = deltaY / steps; var curX:Float = start.x - stepX - x; var curY:Float = start.y - stepY - y; - var tileX:Int; - var tileY:Int; var i:Int = 0; start.putWeak(); @@ -1111,10 +1136,10 @@ class FlxTypedTilemap extends FlxBaseTilemap continue; } - tileX = Math.floor(curX / scaledTileWidth); - tileY = Math.floor(curY / scaledTileHeight); - - if (_tileObjects[_data[tileY * widthInTiles + tileX]].allowCollisions != NONE) + var tileX = Math.floor(curX / scaledTileWidth); + var tileY = Math.floor(curY / scaledTileHeight); + + if (getTileData(tileX, tileY).solid) { // Some basic helper stuff tileX *= Std.int(scaledTileWidth); @@ -1190,9 +1215,8 @@ class FlxTypedTilemap extends FlxBaseTilemap { if (spriteFactory == null) spriteFactory = defaultTileToSprite; - - final rowIndex:Int = tileX + (tileY * widthInTiles); - final tile:FlxTile = _tileObjects[_data[rowIndex]]; + + final tile:FlxTile = getTileData(tileX, tileY); var image:FlxImageFrame = null; if (tile != null && tile.visible) @@ -1202,7 +1226,7 @@ class FlxTypedTilemap extends FlxBaseTilemap final worldX:Float = tileX * tileWidth * scale.x + x; final worldY:Float = tileY * tileHeight * scale.y + y; - var tileSprite:FlxSprite = spriteFactory({ + final tileSprite:FlxSprite = spriteFactory({ graphic: image, x: worldX, y: worldY, @@ -1212,7 +1236,7 @@ class FlxTypedTilemap extends FlxBaseTilemap }); if (newTile >= 0) - setTile(tileX, tileY, newTile); + setTileIndex(tileX, tileY, newTile); return tileSprite; } @@ -1292,7 +1316,7 @@ class FlxTypedTilemap extends FlxBaseTilemap for (column in 0...screenColumns) { - tile = _tileObjects[_data[columnIndex]]; + tile = getTileData(columnIndex); if (tile != null && tile.visible && tile.frame.type != FlxFrameType.EMPTY) { @@ -1407,7 +1431,13 @@ class FlxTypedTilemap extends FlxBaseTilemap setDirty(); } #end - + + /** Guards against -1 */ + override function setTileHelper(mapIndex:Int, tileIndex:Int, updateGraphics:Bool = true):Bool + { + return super.setTileHelper(mapIndex, tileIndex < 0 ? 0 : tileIndex, updateGraphics); + } + /** * Internal function used in setTileByIndex() and the constructor to update the map. * diff --git a/haxelib.json b/haxelib.json index 8e9b9ae825..a5ddb35510 100644 --- a/haxelib.json +++ b/haxelib.json @@ -4,7 +4,7 @@ "license": "MIT", "tags": ["game", "openfl", "flash", "html5", "neko", "cpp", "android", "ios", "cross"], "description": "HaxeFlixel is a 2D game engine based on OpenFL that delivers cross-platform games.", - "version": "5.8.1", + "version": "5.9.0", "releasenote": "TBD", "contributors": ["haxeflixel", "Gama11", "GeoKureli"], "dependencies": {} diff --git a/tests/coverage/Project.xml b/tests/coverage/Project.xml index 97a5198510..8a7d118958 100644 --- a/tests/coverage/Project.xml +++ b/tests/coverage/Project.xml @@ -53,6 +53,7 @@ +
diff --git a/tests/unit/src/flixel/FlxObjectTest.hx b/tests/unit/src/flixel/FlxObjectTest.hx index 292048bba0..6fe89c7865 100644 --- a/tests/unit/src/flixel/FlxObjectTest.hx +++ b/tests/unit/src/flixel/FlxObjectTest.hx @@ -1,14 +1,15 @@ package flixel; -import openfl.display.BitmapData; +import flixel.FlxObject; import flixel.graphics.FlxGraphic; import flixel.math.FlxPoint; import flixel.math.FlxMath; import flixel.math.FlxRect; import flixel.tile.FlxTilemap; import flixel.util.FlxDirectionFlags; -import massive.munit.Assert; import haxe.PosInfos; +import massive.munit.Assert; +import openfl.display.BitmapData; class FlxObjectTest extends FlxTest { @@ -77,7 +78,92 @@ class FlxObjectTest extends FlxTest step(60); Assert.isFalse(FlxG.overlap(object1, object2)); } - + + @Test + function testSeprateX():Void + { + final object1 = new FlxObject(5, 0, 10, 10); + object1.last.x = 10; + final object2 = new FlxObject(0, 0, 10, 10); + object2.last.x = -5; + + Assert.areEqual(-5, FlxObject.computeOverlapX(object1, object2)); + Assert.areEqual(0, FlxObject.computeOverlapY(object1, object2)); + Assert.isTrue(FlxG.overlap(object1, object2)); + + Assert.isTrue(FlxObject.separateX(object1, object2)); + + Assert.areEqual(0, FlxObject.computeOverlapX(object1, object2)); + Assert.areEqual(0, FlxObject.computeOverlapY(object1, object2)); + Assert.isFalse(FlxG.overlap(object1, object2)); + Assert.isTrue(object1.x > object2.x); + } + + @Test + function testSeprateY():Void + { + final object1 = new FlxObject(0, 5, 10, 10); + object1.last.y = 10; + final object2 = new FlxObject(0, 0, 10, 10); + object2.last.y = -5; + + Assert.areEqual(-5, FlxObject.computeOverlapY(object1, object2)); + Assert.areEqual(0, FlxObject.computeOverlapX(object1, object2)); + + Assert.isTrue(FlxObject.separateY(object1, object2)); + + Assert.areEqual(0, FlxObject.computeOverlapY(object1, object2)); + Assert.areEqual(0, FlxObject.computeOverlapX(object1, object2)); + Assert.isFalse(FlxG.overlap(object1, object2)); + Assert.isTrue(object1.y > object2.y); + } + + @Test + function testSeprateXFromOpposite():Void + { + /* + * NOTE: An odd y value on either may result in a rounding error where the second + * computeOverlapY is 0 but FlxG.overlap returns true + */ + final object1 = new FlxObject(20, 0, 10, 10); + object1.last.x = object1.x - 30; + final object2 = new FlxObject(0, 0, 10, 10); + object2.last.x = object2.x + 30; + + Assert.areEqual(30, FlxObject.computeOverlapX(object1, object2)); + Assert.areEqual(0, FlxObject.computeOverlapY(object1, object2)); + + Assert.isTrue(FlxObject.separateX(object1, object2)); + + Assert.areEqual(0, FlxObject.computeOverlapX(object1, object2)); + Assert.areEqual(0, FlxObject.computeOverlapY(object1, object2)); + Assert.isFalse(FlxG.overlap(object1, object2)); + Assert.isTrue(object1.x < object2.x); + } + + @Test + function testSeprateYFromOpposite():Void + { + /* + * NOTE: An odd y value on either may result in a rounding error where the second + * computeOverlapY is 0 but FlxG.overlap returns true + */ + final object1 = new FlxObject(0, 20, 10, 10); + object1.last.y = object1.y - 30; + final object2 = new FlxObject(0, 0, 10, 10); + object2.last.y = object2.y + 30; + + Assert.areEqual(30, FlxObject.computeOverlapY(object1, object2)); + Assert.areEqual(0, FlxObject.computeOverlapX(object1, object2)); + + Assert.isTrue(FlxObject.separateY(object1, object2)); + + Assert.areEqual(0, FlxObject.computeOverlapY(object1, object2)); + Assert.areEqual(0, FlxObject.computeOverlapX(object1, object2)); + Assert.isFalse(FlxG.overlap(object1, object2)); + Assert.isTrue(object1.y < object2.y); + } + @Test // closes #1564, tests #1561 function testSeparateYAfterX():Void { @@ -189,9 +275,14 @@ class FlxObjectTest extends FlxTest } @Test - function testOverlapsPoint() + function testOverlapsPointInScreenSpace() { overlapsPointInScreenSpace(true); + } + + @Test + function testOverlapsPointNotInScreenSpace() + { overlapsPointInScreenSpace(false); } diff --git a/tests/unit/src/flixel/path/FlxPathTest.hx b/tests/unit/src/flixel/path/FlxPathTest.hx index 8fb321f66c..336ea0348d 100644 --- a/tests/unit/src/flixel/path/FlxPathTest.hx +++ b/tests/unit/src/flixel/path/FlxPathTest.hx @@ -42,6 +42,7 @@ class FlxPathTest extends FlxTest } @Test + @:haxe.warning("-WDeprecated") function testCancelNoCallback() { startPath(); diff --git a/tests/unit/src/flixel/tile/FlxTilemapTest.hx b/tests/unit/src/flixel/tile/FlxTilemapTest.hx index 94a2f3f0e0..1e953f55c7 100644 --- a/tests/unit/src/flixel/tile/FlxTilemapTest.hx +++ b/tests/unit/src/flixel/tile/FlxTilemapTest.hx @@ -1,11 +1,13 @@ package flixel.tile; -import openfl.display.BitmapData; -import openfl.errors.ArgumentError; +import flixel.FlxObject; import flixel.math.FlxPoint; import flixel.util.FlxColor; +import flixel.util.FlxDirectionFlags; import haxe.PosInfos; import massive.munit.Assert; +import openfl.display.BitmapData; +import openfl.errors.ArgumentError; using StringTools; @@ -32,7 +34,7 @@ class FlxTilemapTest extends FlxTest @Test function test1x1Map() { - tilemap.loadMapFromCSV("1", getBitmapData()); + tilemap.loadMapFromCSV("1", getBitmapData(), 8, 8); try { @@ -44,30 +46,93 @@ class FlxTilemapTest extends FlxTest } Assert.areEqual(1, tilemap.getData()[0]); + Assert.areEqual(1, tilemap.getData(true)[0]); + Assert.areEqual(1, tilemap.getTileIndex(0)); } - + @Test function testLoadMapArray() { - var mapData = [0, 1, 0, 1, 1, 1]; - tilemap.loadMapFromArray(mapData, 3, 2, getBitmapData()); - + final mapData = + [ + 0, 1, 0, + 1, 1, 1 + ]; + tilemap.loadMapFromArray(mapData, 3, 2, getBitmapData(), 8, 8); + Assert.areEqual(3, tilemap.widthInTiles); Assert.areEqual(2, tilemap.heightInTiles); FlxAssert.arraysEqual([0, 1, 0, 1, 1, 1], tilemap.getData()); } - + @Test function testLoadMap2DArray() { - var mapData = [[0, 1, 0], [1, 1, 1]]; - tilemap.loadMapFrom2DArray(mapData, getBitmapData()); - + final mapData = + [ + [0, 1, 0], + [1, 1, 1] + ]; + tilemap.loadMapFrom2DArray(mapData, getBitmapData(), 8, 8); + Assert.areEqual(3, tilemap.widthInTiles); Assert.areEqual(2, tilemap.heightInTiles); FlxAssert.arraysEqual([0, 1, 0, 1, 1, 1], tilemap.getData()); } + + @Test + function testAutoTiling() + { + final mapData = + [ + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 1, 1, 1, 1, + 0, 0, 0, 0, 0 + ]; + tilemap.loadMapFromArray(mapData, 5, 3, getBitmapData(), 8, 8, AUTO); + Assert.areEqual(5, tilemap.widthInTiles); + Assert.areEqual(3, tilemap.heightInTiles); + + assertSolidData(mapData); + assertData + ([ + 0, 0, 6, 0, 0, + 0, 0, 6, 0, 0, + 0, 3, 12, 11, 11, + 0, 0, 0, 0, 0 + ]); + + tilemap.setTileIndex(2, 0); + + assertSolidData + ([ + 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 1, 1, 1, 1, + 0, 0, 0, 0, 0 + ]); + + assertData + ([ + 0, 0, 0, 0, 0, + 0, 0, 5, 0, 0, + 0, 3, 12, 11, 11, + 0, 0, 0, 0, 0 + ]); + } + + function assertSolidData(expected:Array, ?msg:String, ?info:PosInfos) + { + FlxAssert.arraysEqual(expected, tilemap.getData(true), msg, info); + } + + function assertData(expected:Array, ?msg:String, ?info:PosInfos) + { + FlxAssert.arraysEqual(expected, tilemap.getData(false), msg, info); + } + @Test function testLoadMapFromGraphic() { @@ -106,12 +171,26 @@ class FlxTilemapTest extends FlxTest } @Test // #1546 + @:haxe.warning("-WDeprecated") function testOffMapOverlap() { - tilemap.loadMapFrom2DArray([[1], [0]], getBitmapData()); + tilemap.loadMapFrom2DArray([[1], [0]], getBitmapData(), 8, 8); var sprite = new FlxSprite(-2, 10); Assert.isFalse(tilemap.overlapsWithCallback(sprite)); } + + @Test // #1546 + // same as testOffMapOverlap but with objectOverlapsTiles + function testOffMapOverlap2() + { + tilemap.loadMapFrom2DArray([[1], [0]], getBitmapData(), 8, 8); + final obj = new FlxObject(-10, 10, 8, 8); + Assert.isFalse(tilemap.objectOverlapsTiles(obj)); + + obj.x = 8; + obj.y = 8; + Assert.isFalse(tilemap.objectOverlapsTiles(obj)); + } @Test // #1550 function testLoadMapFromCSVTrailingNewline() @@ -121,7 +200,7 @@ class FlxTilemapTest extends FlxTest function testLoadMapFromCSVWithNewline(csv:String, newlines:String) { - tilemap.loadMapFromCSV(csv.replace("[nl]", newlines), getBitmapData()); + tilemap.loadMapFromCSV(csv.replace("[nl]", newlines), getBitmapData(), 8, 8); Assert.areEqual(4, tilemap.widthInTiles); Assert.areEqual(3, tilemap.heightInTiles); @@ -132,7 +211,7 @@ class FlxTilemapTest extends FlxTest function testRayEmpty() { var mapData = [0, 0, 0]; // 3x1 - tilemap.loadMapFromArray(mapData, 3, 1, getBitmapData()); + tilemap.loadMapFromArray(mapData, 3, 1, getBitmapData(), 8, 8); Assert.isTrue(tilemap.ray(new FlxPoint(0, tilemap.height / 2), new FlxPoint(tilemap.width, tilemap.height / 2))); } @@ -141,7 +220,7 @@ class FlxTilemapTest extends FlxTest function testRayStraight() { var mapData = [0, 1, 0]; // 3x1 with a solid block in the middle - tilemap.loadMapFromArray(mapData, 3, 1, getBitmapData()); + tilemap.loadMapFromArray(mapData, 3, 1, getBitmapData(), 8, 8); Assert.isFalse(tilemap.ray(new FlxPoint(0, tilemap.height / 2), new FlxPoint(tilemap.width, tilemap.height / 2))); } @@ -150,7 +229,7 @@ class FlxTilemapTest extends FlxTest function testRayImperfectDiagonal() { var mapData = [0, 0, 0, 0, 1, 0, 0, 0, 0]; // 3x3 with a solid block in the middle - tilemap.loadMapFromArray(mapData, 3, 3, getBitmapData()); + tilemap.loadMapFromArray(mapData, 3, 3, getBitmapData(), 8, 8); Assert.isFalse(tilemap.ray(new FlxPoint(0, 0), new FlxPoint(tilemap.width - tilemap.width / 8, tilemap.height))); } @@ -159,7 +238,7 @@ class FlxTilemapTest extends FlxTest function testRayPerfectDiagonal() { var mapData = [0, 0, 0, 0, 1, 0, 0, 0, 0]; // 3x3 with a solid block in the middle - tilemap.loadMapFromArray(mapData, 3, 3, getBitmapData()); + tilemap.loadMapFromArray(mapData, 3, 3, getBitmapData(), 8, 8); Assert.isFalse(tilemap.ray(new FlxPoint(0, 0), new FlxPoint(tilemap.width, tilemap.height))); } @@ -167,14 +246,16 @@ class FlxTilemapTest extends FlxTest @Test function testNegativeIndicesTreatedAsZero() { - tilemap.loadMapFromCSV("-1,1", getBitmapData()); + tilemap.loadMapFromCSV("-1,1", getBitmapData(), 8, 8); FlxAssert.arraysEqual([0, 1], tilemap.getData()); + + Assert.areEqual(0, tilemap.getTileIndex(0)); } @Test // #1520 function testLoadMapFromCSVTrailingComma() { - tilemap.loadMapFromCSV("1,", getBitmapData()); + tilemap.loadMapFromCSV("1,", getBitmapData(), 8, 8); FlxAssert.arraysEqual([1], tilemap.getData()); } @@ -184,7 +265,7 @@ class FlxTilemapTest extends FlxTest var exceptionThrown = false; try { - tilemap.loadMapFromCSV("1,f,1", getBitmapData()); + tilemap.loadMapFromCSV("1,f,1", getBitmapData(), 8, 8); } catch (e:Dynamic) { @@ -197,7 +278,7 @@ class FlxTilemapTest extends FlxTest @Test // #1835 function testOverlapsPointCrash() { - tilemap.loadMapFromCSV("1,", getBitmapData()); + tilemap.loadMapFromCSV("1,", getBitmapData(), 8, 8); var point = FlxPoint.get(1000, 1000); Assert.isFalse(tilemap.overlapsPoint(point, false)); Assert.isFalse(tilemap.overlapsPoint(point, true)); @@ -214,7 +295,247 @@ class FlxTilemapTest extends FlxTest Assert.isTrue(overlaps(0, 0)); Assert.isFalse(overlaps(1, 1)); } - + + @Test // #3158 + function testIsOverlappingTile() + { + final mapData = + [ + 0, 0, 0, + 0, 1, 0, + 0, 0, 0 + ]; // 3x3 with a solid block in the middle + tilemap.loadMapFromArray(mapData, 3, 3, getBitmapData(), 8, 8); + + final obj = new FlxObject(4, 12, 8, 8); + var count = 0; + final result = tilemap.isOverlappingTile(obj, (tile)->{ count++; return tile.solid; } ); + // should be touching bottom-left 4 tiles only + Assert.isTrue(result); + // wont need to check all 4 before finding + Assert.areEqual(2, count); + + // test position + count = 0; + final result = tilemap.isOverlappingTile(obj, (tile)->{ count++; return tile.solid; }, FlxPoint.get(0, 8)); + // should be touching top-left 4 tiles only + Assert.isTrue(result); + Assert.areEqual(4, count); + } + + @Test + function testWallLeft() + { + final mapData = [ + 1, 0, 0, + 1, 0, 0, + 1, 0, 0 + ]; + tilemap.loadMapFromArray(mapData, 3, 3, getBitmapData(), 8, 8); + + final obj = new FlxObject(0, 0, 8, 8); + + // move up-left towards a left wal, make sure we only separate on the X + for (i in 0...40) + { + // check a bunch of locations + obj.x = 4; + obj.y = 4 + (i / 10); + obj.touching = NONE; + obj.last.set(12, obj.y + 8); + FlxG.collide(tilemap, obj); + + final result = obj.touching.toString(); + Assert.areEqual(LEFT.toString(), result, 'Value [$result] was not equal to expected value [L] on i=$i'); + } + } + + @Test + function testWallRight() + { + final mapData = [ + 0, 0, 1, + 0, 0, 1, + 0, 0, 1 + ]; + tilemap.loadMapFromArray(mapData, 3, 3, getBitmapData(), 8, 8); + + final obj = new FlxObject(0, 0, 8, 8); + + // move up-right towards a right wall, make sure we only separate on the X + for (i in 0...40) + { + // check a bunch of locations + obj.x = 12; + obj.y = 4 + (i / 10); + obj.touching = NONE; + obj.last.set(4, obj.y + 8); + FlxG.collide(tilemap, obj); + + final result = obj.touching.toString(); + Assert.areEqual(RIGHT.toString(), result, 'Value [$result] was not equal to expected value [R] on i=$i'); + } + } + + @Test + function testWallTop() + { + final mapData = [ + 1, 1, 1, + 0, 0, 0, + 0, 0, 0 + ]; + tilemap.loadMapFromArray(mapData, 3, 3, getBitmapData(), 8, 8); + + final obj = new FlxObject(0, 0, 8, 8); + + // move up-left towards a ceiling, make sure we only separate on the Y + for (i in 0...40) + { + // check a bunch of locations + obj.x = 4 + (i / 10); + obj.y = 4; + obj.touching = NONE; + obj.last.set(obj.x + 8, 12); + FlxG.collide(tilemap, obj); + + final result = obj.touching.toString(); + Assert.areEqual(UP.toString(), result, 'Value [$result] was not equal to expected value [U] on i=$i'); + } + } + + @Test + function testWallBottom() + { + final mapData = [ + 0, 0, 0, + 0, 0, 0, + 1, 1, 1 + ]; + tilemap.loadMapFromArray(mapData, 3, 3, getBitmapData(), 8, 8); + + final obj = new FlxObject(0, 0, 8, 8); + + // move down-right towards a floor, make sure we only separate on the Y + for (i in 0...40) + { + // check a bunch of locations + obj.x = 12 - (i / 10); + obj.y = 12; + obj.touching = NONE; + obj.last.set(obj.x - 8, 4); + FlxG.collide(tilemap, obj); + + final result = obj.touching.toString(); + Assert.areEqual(DOWN.toString(), result, 'Value [$result] was not equal to expected value [D] on i=$i'); + } + } + + @Test + function testMapIndex() + { + final mapData = [ + 0, 0, 0, + 0, 1, 0, + 0, 0, 0 + ]; + tilemap.loadMapFromArray(mapData, 3, 3, getBitmapData(), 8, 8); + + Assert.areEqual(tilemap.getTileIndex(4), tilemap.getTileIndex(1, 1)); + Assert.areEqual(1, tilemap.getTileIndex(4)); + Assert.areEqual(2, tilemap.getColumn(8)); + Assert.areEqual(2, tilemap.getRow(8)); + + Assert.areEqual(tilemap.getTileData(4), tilemap.getTileData(1, 1)); + } + + function testGetColumnRowAt() + { + + final mapData = [ + 0, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 0, 0, + ]; + tilemap.loadMapFromArray(mapData, 4, 3, getBitmapData(), 8, 8); + + Assert.areEqual(tilemap.getColumnAt(24, true), tilemap.getColumnAt(24, false)); + Assert.areEqual(3, tilemap.getColumnAt(24)); + Assert.areNotEqual(tilemap.getColumnAt(32, true), tilemap.getColumnAt(32, false)); + Assert.areEqual(4, tilemap.getColumnAt(32, true)); + Assert.areEqual(5, tilemap.getColumnAt(32, false)); + + Assert.areEqual(tilemap.getRowAt(16, true), tilemap.getRowAt(16, false)); + Assert.areEqual(2, tilemap.getRowAt(16)); + Assert.areNotEqual(tilemap.getRowAt(24, true), tilemap.getRowAt(24, false)); + Assert.areEqual(3, tilemap.getRowAt(24, true)); + Assert.areEqual(4, tilemap.getRowAt(24, false)); + } + + @Test + function testTileExists() + { + final mapData = [ + 0, 0, 0, + 0, 1, 0, + 0, 0, 0 + ]; + tilemap.loadMapFromArray(mapData, 3, 3, getBitmapData(), 8, 8); + + Assert.isTrue(tilemap.tileExists(4)); + Assert.isTrue(tilemap.tileExists(1, 1)); + Assert.isFalse(tilemap.tileExists(9)); + Assert.isFalse(tilemap.tileExists(3, 1)); + Assert.isFalse(tilemap.tileExists(1, 3)); + Assert.isFalse(tilemap.tileExists(5, 5)); + } + + @Test + function testColumnRowExists() + { + final mapData = [ + 0, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 0, 0 + ]; + tilemap.loadMapFromArray(mapData, 4, 3, getBitmapData(), 8, 8); + + Assert.isFalse(tilemap.columnExists(5)); + Assert.isFalse(tilemap.rowExists(5)); + + Assert.isFalse(tilemap.columnExists(4)); + Assert.isFalse(tilemap.rowExists(4)); + + Assert.isTrue(tilemap.columnExists(3)); + Assert.isFalse(tilemap.rowExists(3)); + + Assert.isTrue(tilemap.columnExists(2)); + Assert.isTrue(tilemap.rowExists(2)); + + Assert.isTrue(tilemap.columnExists(1)); + Assert.isTrue(tilemap.rowExists(1)); + + Assert.isTrue(tilemap.columnExists(0)); + Assert.isTrue(tilemap.rowExists(0)); + + Assert.isFalse(tilemap.columnExists(-1)); + Assert.isFalse(tilemap.rowExists(-1)); + } + + @Test + function testGetAllMapIndices() + { + final mapData = [ + 0, 0, 0, + 0, 1, 0, + 0, 0, 0 + ]; + tilemap.loadMapFromArray(mapData, 3, 3, getBitmapData(), 8, 8); + + FlxAssert.arraysEqual([4], tilemap.getAllMapIndices(1)); + FlxAssert.arraysEqual([0,1,2,3,5,6,7,8], tilemap.getAllMapIndices(0)); + } + function assertPixelHasColor(x:Int, color:UInt, ?info:PosInfos) { Assert.areEqual(FlxG.camera.buffer.getPixel(x, 0), color, info); @@ -222,6 +543,6 @@ class FlxTilemapTest extends FlxTest function getBitmapData() { - return new BitmapData(16, 8); + return new BitmapData(8*16, 8); } } From 09195367cca7e15ec481a860a3a60a0837bcceeb Mon Sep 17 00:00:00 2001 From: George FunBook Date: Tue, 11 Jun 2024 10:55:49 -0500 Subject: [PATCH 18/22] add various 5.9.0 changes --- CHANGELOG.md | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c861a6b2fd..a2dbd9b9d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,47 @@ -5.8.1 (TBD) +5.9.0 (TBD) + +#### New features: +- `FlxReplay`: Add `getDuration` ([#3135](https://github.com/HaxeFlixel/flixel/pull/3135)) +- `InputFrontEnd`: ([#3134](https://github.com/HaxeFlixel/flixel/pull/3134)) + - Add `addInput` and `addUniqueType` to replace `add` + - add `destroyOld` arg to the `replace` method + - Allow more than one instance of a certain `IFlxInputManager` type +- `SoundFrontEnd`: Add `onVolumeChange` signal ([#3148](https://github.com/HaxeFlixel/flixel/pull/3148)) +- `FlxBasePath`: ([#3153](https://github.com/HaxeFlixel/flixel/pull/3153)) + - Create base class for `FlxPath` for simpler custom path following logic + - Add signals `onEndReached`, `onFinished` and `onNodeReached` + - Replacement fields: `startAt`, `direction`, `loopType`, `target`, `currentIndex`, `nextIndex`, `current` and `next` +- #### Changes and improvements: - `FlxKey`: Add `NONE` to `fromStringMap` and `toStringMap` ([#3119](https://github.com/HaxeFlixel/flixel/pull/3119)) -- `FlxPreloader`: Improve documentation ([#3119](https://github.com/HaxeFlixel/flixel/pull/3119)) +- `FlxPreloader`: Improve documentation ([#3123](https://github.com/HaxeFlixel/flixel/pull/3123)) +- `FlxDrawTrianglesItem`: minor optimization ([#3121](https://github.com/HaxeFlixel/flixel/pull/3121)) +- `FlxTypedTilemap`: Add private `createTile` method for easier extension, with custom tile types ([#3154](https://github.com/HaxeFlixel/flixel/pull/3154)) +- `FlxState`: Improve doc for `persistentUpdate` & `persistentDraw` ([#3155](https://github.com/HaxeFlixel/flixel/pull/3155)) +- `FlxCamera`: Improve doc ([#3161](https://github.com/HaxeFlixel/flixel/pull/3161)) +- `NextState`: Improve doc ([#3160](https://github.com/HaxeFlixel/flixel/pull/3160)) +- `FlxSprite`: Account for `scale`, `origin`, `offset`, `angle` and `pixelPerfectPosition` in `getGraphicMidpoint` ([#3125](https://github.com/HaxeFlixel/flixel/pull/3125)) +- Major change to `FlxTilemap` and `FlxTiles` collision ([#3158](https://github.com/HaxeFlixel/flixel/pull/3158)) + - `FlxTile`: Various features for allowing custom overlap/collision logic + - Add dynamic methods `overlapsObject` and `orientAt` with helpers `orient`, `orientByIndex` and `orientAtByIndex` + - Add `onCollide` signal, when overlaps are detected for collision purposes + - Tilemaps: Add various new tools and helpers to `FlxTilemap` and `FlxBaseTilemap` + - Added new `forEachOverlappingTile` calls a method with every tile that overlaps an object + - Added new `isOverlappingTile` method, allows you to check all tiles overlapping an object + - Added new `objectOverlapsTiles` to replace the now deprecated `overlapsWithCallbacks` + - Eschews `flipCallbackParams` arg, allowing better typing of both callback params + - Adds `isCollision` flag to control whether the Tiles' collision callbacks are fired and allows for processing non-solid tiles + - Added new helpers: `getMapIndex`, `getRow`, `getColumn`, `getTileIndex`, `getTileData`, `tileExists`, `setTileIndex`, `getColumnAt`, `getRowAt`, `columnExists` and `rowExists` + - `FlxObject`: Add internal helpers for processing tilemaps in `separate`, `updateTouchingFlags` and similar functions + - Debug Drawing: Various improvements to debug drawing tilemaps + - Now honors tiles' `ignoreDrawDebug` and `debugBoundingBoxColor` fields + - New `getDebugBoundingBoxColor` in `FlxObject`. Meant to eventually replace `drawDebugBoundingBox` + #### Bugfixes: -- +- `FlxFlickerTween`: Fix "Unsupported recursive type" error on hl ([#3170](https://github.com/HaxeFlixel/flixel/pull/3170)) +- `FlxBGSprite`: Fix draw size when scale is not `1.0` ([#3142](https://github.com/HaxeFlixel/flixel/pull/3142)) 5.8.0 (April 19, 2024) From 59e8ef076c8d6ed165f98f517b521f2d275fc20a Mon Sep 17 00:00:00 2001 From: George Kurelic Date: Tue, 11 Jun 2024 11:20:51 -0500 Subject: [PATCH 19/22] prevent draw calls to bgsprite when transparent (#3173) * prevent draw calls to bgsprite when transparent * D'oh --- flixel/FlxSubState.hx | 23 +++++++++++++++-------- flixel/system/FlxBGSprite.hx | 1 + 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/flixel/FlxSubState.hx b/flixel/FlxSubState.hx index c657ad7cd5..7e614d48fe 100644 --- a/flixel/FlxSubState.hx +++ b/flixel/FlxSubState.hx @@ -45,17 +45,19 @@ class FlxSubState extends FlxState var _created:Bool = false; /** - * @param BGColor background color for this substate + * @param bgColor background color for this substate */ - public function new(BGColor:FlxColor = FlxColor.TRANSPARENT) + public function new(bgColor = FlxColor.TRANSPARENT) { super(); closeCallback = null; openCallback = null; if (FlxG.renderTile) + { _bgSprite = new FlxBGSprite(); - bgColor = BGColor; + } + this.bgColor = bgColor; } override public function draw():Void @@ -68,9 +70,10 @@ class FlxSubState extends FlxState camera.fill(bgColor); } } - else + else // FlxG.renderTile { - _bgSprite.draw(); + if (_bgSprite != null && _bgSprite.visible) + _bgSprite.draw(); } // Now draw all children @@ -102,11 +105,15 @@ class FlxSubState extends FlxState } @:noCompletion - override function set_bgColor(Value:FlxColor):FlxColor + override function set_bgColor(value:FlxColor):FlxColor { if (FlxG.renderTile && _bgSprite != null) - _bgSprite.pixels.setPixel32(0, 0, Value); + { + _bgSprite.alpha = value.alphaFloat; + _bgSprite.visible = _bgSprite.alpha > 0; + _bgSprite.color = value.rgb; + } - return _bgColor = Value; + return _bgColor = value; } } diff --git a/flixel/system/FlxBGSprite.hx b/flixel/system/FlxBGSprite.hx index 8f430f410c..125c321381 100644 --- a/flixel/system/FlxBGSprite.hx +++ b/flixel/system/FlxBGSprite.hx @@ -10,6 +10,7 @@ class FlxBGSprite extends FlxSprite public function new() { super(); + // TODO: Use unique:false, now that we're not editing the pixels makeGraphic(1, 1, FlxColor.WHITE, true, FlxG.bitmap.getUniqueKey("bg_graphic_")); scrollFactor.set(); } From 25a781d1ab8c18f1e6e4b309cc1d7e25ccb4133f Mon Sep 17 00:00:00 2001 From: George Kurelic Date: Tue, 11 Jun 2024 12:04:43 -0500 Subject: [PATCH 20/22] use array.resize for setLength (#3094) * use array.resize for setLength * fix ci and backwards comp * revert ability to increase length, deprecate --- flixel/util/FlxArrayUtil.hx | 23 ++++--------------- .../unit/src/flixel/util/FlxArrayUtilTest.hx | 18 +++++++++++++++ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/flixel/util/FlxArrayUtil.hx b/flixel/util/FlxArrayUtil.hx index c95692beb7..f874e3a016 100644 --- a/flixel/util/FlxArrayUtil.hx +++ b/flixel/util/FlxArrayUtil.hx @@ -11,25 +11,12 @@ class FlxArrayUtil * @param array The array. * @param newLength The length you want the array to have. */ - @:generic - public static function setLength(array:Array, newLength:Int):Array + @:deprecated("setLength is deprecated, use array.resize instead") + public static inline function setLength(array:Array, newLength:Int):Array { - if (newLength < 0) - return array; - - var oldLength:Int = array.length; - var diff:Int = newLength - oldLength; - if (diff >= 0) - return array; - - #if flash - untyped array.length = newLength; - #else - diff = -diff; - for (i in 0...diff) - array.pop(); - #end - + if (newLength > 0 && newLength < array.length) + array.resize(newLength); + return array; } diff --git a/tests/unit/src/flixel/util/FlxArrayUtilTest.hx b/tests/unit/src/flixel/util/FlxArrayUtilTest.hx index 3338ae694d..550a7355ce 100644 --- a/tests/unit/src/flixel/util/FlxArrayUtilTest.hx +++ b/tests/unit/src/flixel/util/FlxArrayUtilTest.hx @@ -102,4 +102,22 @@ class FlxArrayUtilTest Assert.isTrue([0, 1, 2, 3, 4, 5].safeSwap(0, 2).equals([2, 1, 0, 3, 4, 5])); Assert.isTrue([0, 1, 2, 3, 4, 5].safeSwap(1, 6).equals([0, 1, 2, 3, 4, 5])); } + + @Test + function testSetLength() + { + final arr = [0, 1, 2, 3, 4, 5]; + + // ignores negative numbers + arr.setLength(-1); + FlxAssert.arraysEqual([0, 1, 2, 3, 4, 5], arr); + + // expected usage + arr.setLength(3); + FlxAssert.arraysEqual([0, 1, 2], arr); + + // can't make arrays bigger + arr.setLength(5); + FlxAssert.arraysEqual([0, 1, 2], arr); + } } From c99df1f21ee6fbbcc5b528ca23dddd12c50a5153 Mon Sep 17 00:00:00 2001 From: George Kurelic Date: Tue, 11 Jun 2024 14:37:23 -0500 Subject: [PATCH 21/22] Fixes for recent PRs (#3176) * fix math error * remove deprecation warnings * add test --- flixel/system/replay/FlxReplay.hx | 7 +++--- flixel/tile/FlxTile.hx | 4 ++-- tests/unit/src/flixel/tile/FlxTilemapTest.hx | 24 +++++++++++++++++--- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/flixel/system/replay/FlxReplay.hx b/flixel/system/replay/FlxReplay.hx index 013092f684..74d0f5ba5d 100644 --- a/flixel/system/replay/FlxReplay.hx +++ b/flixel/system/replay/FlxReplay.hx @@ -118,7 +118,7 @@ class FlxReplay if (frameCount >= _capacity) { _capacity *= 2; - FlxArrayUtil.setLength(_frames, _capacity); + _frames.resize(_capacity); } } } @@ -184,7 +184,7 @@ class FlxReplay if (frameCount >= _capacity) { _capacity *= 2; - FlxArrayUtil.setLength(_frames, _capacity); + _frames.resize(_capacity); } } @@ -238,8 +238,7 @@ class FlxReplay function init():Void { _capacity = 100; - _frames = new Array( /*_capacity*/); - FlxArrayUtil.setLength(_frames, _capacity); + _frames = new Array(); frameCount = 0; } diff --git a/flixel/tile/FlxTile.hx b/flixel/tile/FlxTile.hx index 41a4eb91bc..b69a929c81 100644 --- a/flixel/tile/FlxTile.hx +++ b/flixel/tile/FlxTile.hx @@ -129,8 +129,8 @@ class FlxTile extends FlxObject height = tilemap.scaledTileHeight; x = xPos + col * width; y = yPos + row * height; - last.x = x - xPos - tilemap.last.x; - last.y = y - yPos - tilemap.last.y; + last.x = x - (xPos - tilemap.last.x); + last.y = y - (yPos - tilemap.last.y); } /** diff --git a/tests/unit/src/flixel/tile/FlxTilemapTest.hx b/tests/unit/src/flixel/tile/FlxTilemapTest.hx index 1e953f55c7..8536036920 100644 --- a/tests/unit/src/flixel/tile/FlxTilemapTest.hx +++ b/tests/unit/src/flixel/tile/FlxTilemapTest.hx @@ -536,11 +536,29 @@ class FlxTilemapTest extends FlxTest FlxAssert.arraysEqual([0,1,2,3,5,6,7,8], tilemap.getAllMapIndices(0)); } - function assertPixelHasColor(x:Int, color:UInt, ?info:PosInfos) + @Test + function testOrientDelta() { - Assert.areEqual(FlxG.camera.buffer.getPixel(x, 0), color, info); + final mapData = [0]; + tilemap.loadMapFromArray(mapData, 1, 1, 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.x); + + tilemap.last.x = 10; + tilemap.x = 10; + tile.orient(0, 0); + + Assert.areEqual(tilemap.x - tilemap.last.x, tile.x - tile.last.x); + Assert.areEqual(tile.x, tile.last.x); } - + function getBitmapData() { return new BitmapData(8*16, 8); From 00a45c8b2607279e24da1b7ac3e2f8b2517978eb Mon Sep 17 00:00:00 2001 From: George Kurelic Date: Tue, 11 Jun 2024 15:49:10 -0500 Subject: [PATCH 22/22] ignore failing unit tests on hl (#3175) * ignore failing unit tests on hl * use new setup-flixel * try running hl on macos-13 * remove apt-get, yolo * test * more tests * revert yml * d'oh! --- .github/workflows/main.yml | 2 +- .../flixel/input/actions/FlxActionInputAnalogTest.hx | 3 ++- .../input/actions/FlxActionInputDigitalTest.hx | 12 ++++++++---- .../src/flixel/input/actions/FlxActionManagerTest.hx | 3 ++- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ba7695c6ac..c57248b91b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,4 +37,4 @@ jobs: flixel-versions: dev test-location: local target: ${{matrix.target}} - run-tests: true + run-tests: true \ No newline at end of file diff --git a/tests/unit/src/flixel/input/actions/FlxActionInputAnalogTest.hx b/tests/unit/src/flixel/input/actions/FlxActionInputAnalogTest.hx index eb0467e41a..175a2b8adf 100644 --- a/tests/unit/src/flixel/input/actions/FlxActionInputAnalogTest.hx +++ b/tests/unit/src/flixel/input/actions/FlxActionInputAnalogTest.hx @@ -248,7 +248,8 @@ class FlxActionInputAnalogTest extends FlxTest testInputStates(test, clear, move, [pos1, pos2, pos3, pos4], axis, a, b, c, d, callbacks); } - + + #if hl @Ignore("makeFakeGamepad is failing on Hashlink #3140") #end @Test function testGamepad() { diff --git a/tests/unit/src/flixel/input/actions/FlxActionInputDigitalTest.hx b/tests/unit/src/flixel/input/actions/FlxActionInputDigitalTest.hx index 0113af0ace..c560cfb05c 100644 --- a/tests/unit/src/flixel/input/actions/FlxActionInputDigitalTest.hx +++ b/tests/unit/src/flixel/input/actions/FlxActionInputDigitalTest.hx @@ -427,7 +427,8 @@ class FlxActionInputDigitalTest extends FlxTest gamepad.update(); } #end - + + #if hl @Ignore("makeFakeGamepad is failing on Hashlink #3140") #end @Test function testFlxGamepad() { @@ -454,7 +455,8 @@ class FlxActionInputDigitalTest extends FlxTest t.assertTrue(btn + ".release2.value"); } } - + + #if hl @Ignore("makeFakeGamepad is failing on Hashlink #3140") #end @Test function testFlxGamepadAny() { @@ -478,7 +480,8 @@ class FlxActionInputDigitalTest extends FlxTest t.assertTrue(btn + ".any.release2.value"); } } - + + #if hl @Ignore("makeFakeGamepad is failing on Hashlink #3140") #end @Test function testFlxGamepadCallbacks() { @@ -510,7 +513,8 @@ class FlxActionInputDigitalTest extends FlxTest } } } - + + #if hl @Ignore("makeFakeGamepad is failing on Hashlink #3140") #end @Test function testFlxGamepadAnyCallbacks() { diff --git a/tests/unit/src/flixel/input/actions/FlxActionManagerTest.hx b/tests/unit/src/flixel/input/actions/FlxActionManagerTest.hx index 94c01e156c..e802b461a1 100644 --- a/tests/unit/src/flixel/input/actions/FlxActionManagerTest.hx +++ b/tests/unit/src/flixel/input/actions/FlxActionManagerTest.hx @@ -418,7 +418,8 @@ class FlxActionManagerTest extends FlxTest t.destroy(); } - + + #if hl @Ignore("makeFakeGamepad is failing on Hashlink #3140") #end @Test function testDeviceConnectedDisconnected() {