It sounds like your game is turn-based, so there will be a movement phase where you get to use up a certain number of movement points or move a certain number of tiles. And it sounds like for the movement phase you don't care how many "movement points" or such can be used, just whether you can get there or not.
A different case would be if you had something like 'action points' where you might want to spend some on movement and retain some to perform an action like attacking. That would make it more complicated.
So what you want is a list of all available tiles you can go to and the least damage you can take while getting there. I think each tile in your path then needs an array of objects with distance and damage, and you can't simply mark a tile as 'visited', but check if the new way you get there gives you less distance, or less distance for a given damage value. Both paths should be evaluated.
So when you are checking tilesTo
and the tile doesn't exist, add it. If it already exists, add path to an array of from paths along with the distance. An example, say you are on tile B trying to get to E, with the numbers representing the heights, and you take 10 points of damage for each level dropped above the first, and tiles are joined horizontally and vertically:
ABCDE
FGHIJ
KLMNO
PQRST
Height map:
24100
23200
12100
01000
You begin with a starting node:
{ tile: B, from: START, distance: 0, damage: 0 }
Then you look at the tiles you can get to and add paths to them, queueing for processing:
{ tile: A, from: B, distance: 1, damage: 10 }
{ tile: C, from: B, distance: 1, damage: 20 }
{ tile: G, from: B, distance: 1, damage: 0 }
The tile A node above generates only one new path to F if you can't climb back to B, carrying along the 10 damage from the previous record, which is added to your list and queued:
{ tile: F, from: A, distance: 2, damage: 10 }
The tile C node when you process it generates two new records (assuming you can't climb back to B) based on the distance 1 and damage 20, adding 1 more distance but no more damage. These are added to your results and queued for processing.
{ tile: D, from: C, distance: 2, damage: 20 }
{ tile: H, from: C, distance: 2, damage: 20 }
Then you add the 4 paths you can get to from G. But here you can check the existing nodes and either replace them or ignore your current path if it is worse/equal in both distance and damage:
{ tile: B, from: G, distance: 2, damage: 0 } // ignored
{ tile: F, from: G, distance: 2, damage: 0 } // replaces and queues
{ tile: H, from: G, distance: 2, damage: 0 } // replaces and queues
{ tile: L, from: G, distance: 2, damage: 0 } // adds and queues
So for new nodes you generate, check existing nodes for that tile:
- If an node already exists for the same tile where the damage and distance are both <= the new node's damage and distance, ignore the new node - STOP
- Remove existing nodes for the same tile that have >= both damage and >= distance of the new node. Because we ignored the new node and stopped at step 1 if they were both equal, either distance or damage will be greater than our new node and the other value be greater or equal, so the new node is better in some way and at least as good in the other.
- Add the new node and queue it for further processing
When adding the node to tile B, we see that there is an existing node with damage 0 and distance 0. Since there is an existing node with the same or lower damage and the same or lower distance, we ignore our new node, we don't add it and don't queue it for processing.
When adding the node for tile F, we see an existing node with the same or higher distance and more damage, so we remove that existing node, then add our new node and queue the new node for more processing.
When adding the node for tile H, there is an existing node from C with distance 2 and damage 20, so we replace it with our new one because it has lower damage and queue the new node.
When adding the node for tile L, there is no existing node for tile L so we add it and queue the new node. Our list now looks like this (comments on queued nodes)
{ tile: B, from: start, distance: 0, damage: 0 }
{ tile: A, from: B, distance: 1, damage: 10 }
{ tile: C, from: B, distance: 1, damage: 20 }
{ tile: G, from: B, distance: 1, damage: 0 }
{ tile: D, from: C, distance: 2, damage: 20 }// in queue
{ tile: F, from: G, distance: 2, damage: 0 } // in queue
{ tile: H, from: G, distance: 2, damage: 0 } // in queue
{ tile: L, from: G, distance: 2, damage: 0 } // in queue
That was the first difference, we can replace existing nodes only distance or damage is less than any existing nodes for that tile and the other value is the same or less than the existing tile. But you can have multiple nodes for tiles as well if one value is higher but the other is lower. In the queue we will generate a new node for C from H that will have a higher distance than the existing node, but a lower damage. So we will queue it and keep both.
{ tile: C, from: H, distance: 3, damage: 0 }
Both of those nodes will generate nodes for tile D by carrying over the damage and adding 1 to the distance:
{ tile: D, from: C, distance: 2, damage: 20 }
{ tile: D, from: C, distance: 4, damage: 0 }
Say you have a maximum movement of 4, so in this case you would not queue up the last node because the distance is already 4, but the first node would be in the queue and allow you to move on to tile E:
{ tile: E, from: D, distance: 3, damage: 20 }
If you were using action points, you might want to present both options for moving to tile D to your player. Either they can move there using 2 points and taking 20 damage but have 2 action points remaining, or use all 4 action points to move there and take no damage.
In your game I think you would want to condense the list to have only one entry per tile with the minimum damage, so you would keep the tile D node with distance 4 and damage 0. It seems like for your display you would just show it as green or something since you won't be taking damage, but the E tile will show as red because the only way to get that far is to take damage.