Creating a respawn point on map changed?

Started by Max, February 27, 2018, 04:23:51 PM

Previous topic - Next topic
Hi guys!

One thing I dislike about A Link to the Past is that you have to restart at your house or the Pyramid every time you die in the overworld. In the game I'm working on, you spend quite a lot of time in the overworld doing side-quests, and I don't want to set the player back so far if they're killed. I'd like to make it so if you die, you can be taken back whatever map you're currently on.

I know if the player is transported to a new map via a destination, you can save that location for restarting. However, if you use the side of the map scrolling feature, there is no destination to go back to, so one workaround is to design my overworld so there's never any side of the map teletransporters, but I'd like to avoid that if possible.

I also tried dynamically creating a destination using game:on_map_changed() and map:create_destination(), however it seems like the game erases this destination when a gameover happens, because the hero isn't taken back to it. I had this function in my game_manager script. This was my code that doesn't seem to work:

Code (lua) Select

--Set Respawn point whenver map changes
function game:on_map_changed()
  local map = game:get_map()
  local hero = game:get_hero()
  local x, y, layer = hero:get_position()
  map:create_destination({
    name = "respawn",
    x = x, y = y, layer = layer,
    direction = hero:get_direction(),
    save_location = "yes",
    default = true,
  })


But yeah, it seems like that destination is erased whenever a gameover happens, which makes it kind of pointless. Any ideas?

February 27, 2018, 07:23:31 PM #1 Last Edit: February 27, 2018, 07:27:01 PM by Diarandor
This is a bit tricky, but it is doable. What I did in my switching-between-heroes feature was to save the important parameters (position, direction, etc) in some local variables, but it can be saved in savegame variables too if you want the game to automatically save the respawn position even after reseting the game.

The first (and easy) part is to save the coordinates of the position (and direction of the hero) where you entered a map. Put some good code to save the info (as you are already doing) in this event: "game:on_map_changed". You can add some restrictions to this respawning feature if you want it to work only in certain parts of the game (like the overworld) and not in others (like different floors of a dungeon). For that, you can use the world name to define some restrictions (custom map properties in Solarus v1.6 will help too).

Next, you need to respawn the hero position, but only after death. You can easily select the map where you need to respawn and call
Code (Lua) Select
hero:teleport(map_id, [destination_name, [transition_style]])
where you can see that the "destination" is optional, so don't use a destination if there is no one. Instead, change immediately the position of the hero by registering some code in the event "map:on_started" (with the multievents script). This can be used for other purposes like creating falling effects between floors, and more stuff (you can register all the functions you need if you make a clean and flexible code). To register events, use the multievents script to do it for all maps at once (i.e., register the event for the map metatable), and remember that you need to use the syntax defined in the multievents script to avoid overriding functions.
To summarise: I recommend to define a function in some script apart and register it in the event "map_METATABLE:on_started" with the multievents script.

In my second video (very old now) I show how Solarus allows switching between different heroes in different maps, wherever you left them:
https://youtu.be/mFBOuENNbIY
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

Thanks Diarandor! I've got it basically working. In my game_manager script, I've used the multi events script to register a function for map:on_opening_transition_finished() to set a respawn point. Map:on_started wouldn't work because the hero would be stuck halfway into the map and couldn't move.  My code looks like this for any others interested:

Code (lua) Select

local map_meta = sol.main.get_metatable("map")
map_meta:register_event("on_opening_transition_finished", function()
    local map = game:get_map()
    game:set_value("respawn_map", map:get_id() )
    print(map:get_id().." respawn saved") --this is for testing
    local hero = game:get_hero()
    local x, y, layer = hero:get_position()
    game:set_value("respawn_x", x) game:set_value("respawn_y", y) game:set_value("respawn_layer", layer)
    game:set_value("respawn_direction", hero:get_direction())
end)


Then, when the game over event is called, I first teleport the hero to the saved map, then use map:create_destination() to make a respawn destination using the respawn data I've saved. Then I teleport the hero to this destination, refill her health, and do game:stop_game_over()



So, this basically works. There's a couple problems. The most easily solved is that this won't work for any maps that have map:on_opening_transition_finished() defined already until I go back and fix those to fit with the syntax for the multi events script, but that's easily solved.

More pressingly, this method of sending the hero back to the saved respawn point doesn't reset the map- for example, all enemies you've killed are still dead as if you've never left the map, any blocks you've pushed don't reset, they're still where you've pushed them. I can see this causing serious problems with puzzles, if you partially finish one, then die, you could easily trap yourself since it's basically as if you've never left the room. I haven't tested this much yet, but I'm pretty sure it'll break my game at some point- for example if you die during a boss battle and the room has been closed to trap you in, you'll then be locked outside and won't be able to get back in to fight.

I'm looking around to try to figure out if there's a way to reset the map. I've tried calling game:start(), which I thought would work because I call this after I teleport the hero to a destination that has {save_location = "yes"} as one of it's properties. Since being sent to a destination that saves your location should save your location, I'd assume that after that, calling game:start() would take you back to the saved location, but in my testing, it won't. It'll go back to the last destination that had that property.



For anyone following along and trying to replicate this, I'd definitely recommend just designing your maps so that if you want the respawn point to be saved, you have to transition to the new map using a destination. Doing it this way is complicated and could have been solved by just designing better upfront, haha.

Quote from: Max on February 28, 2018, 10:02:11 PM
I'm looking around to try to figure out if there's a way to reset the map. I've tried calling game:start(), which I thought would work because I call this after I teleport the hero to a destination that has {save_location = "yes"} as one of it's properties. Since being sent to a destination that saves your location should save your location, I'd assume that after that, calling game:start() would take you back to the saved location, but in my testing, it won't. It'll go back to the last destination that had that property.
I don't know what would be the best way to do this. But as a hacky workaround you can always use an empty map with a black surface above (so that nothing is shown) and make two immediate teleportations, first to the empty map, and then from there to the original map. You can make a script that makes use of this empty map only when necessary for teleportations that reset the map. Maybe @Christopho has a better and cleaner solution for this.
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

I actually tried this- still didn't reset the map. Perhaps it's because I used the same script to send the hero there and send her back. The game over function went something like:

Hero:teleport(blank_map)
Hero:teleport(respawn_map, respawn_destination)

Maybe it happens so quick the engine didn't register the change?

Quote from: Max on February 28, 2018, 10:48:58 PM
I actually tried this- still didn't reset the map. Perhaps it's because I used the same script to send the hero there and send her back. The game over function went something like:

Hero:teleport(blank_map)
Hero:teleport(respawn_map, respawn_destination)

Maybe it happens so quick the engine didn't register the change?
I am not sure what the engine does with those 2 lines of code, but I think it only calls the second one. You can call "Hero:teleport(blank_map)" in your current map and "Hero:teleport(respawn_map, respawn_destination)" in your blank map. That should work.
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

I think a simple solution to your problem would be to assign a unique world to every map. Then the default behavior would be that the player respawns at the point they entered the map, and you don't have to code anything special. Could be a problem though if you are already using the world property for something else.

It's a nice idea, but that behavior isn't actually default. The default, on restarting the game, teleports you to the last destination entity that saved your location. It might seem like that's the last map that has a different world, but that's actually only the case if you arrive on that world via a destination entity that has "on world changed" as its save location property.

But yeah, if you only change maps via teleports to a destination (ie, not side-of-map transitions), and set a new world for every map, that will work great, and I'd definitely recommend it, haha. I might end up going back and reworking all my maps so there's point-to-destination transition instead of scrolling ones : /

Quote from: Max on March 01, 2018, 03:33:01 AM
The default, on restarting the game, teleports you to the last destination entity that saved your location. It might seem like that's the last map that has a different world, but that's actually only the case if you arrive on that world via a destination entity that has "on world changed" as its save location property.

Interesting, I did not know it worked that way. In that case, Diarandor's suggestion should work.

Quote from: Diarandor on February 28, 2018, 11:35:31 PM
I am not sure what the engine does with those 2 lines of code, but I think it only calls the second one. You can call "Hero:teleport(blank_map)" in your current map and "Hero:teleport(respawn_map, respawn_destination)" in your blank map. That should work.

I thought it would work too, but I can't seem to get it to. So, here's what I can do. I can send the hero to the blank map on gameover, then in the blank map's script, send them back to the map that has the respawn location saved. However, I can't seem to get the respawn destination entity to show up and send the hero to it.

Here's the script for the blank map:
Code (lua) Select

function map:on_opening_transition_finished()
  hero:teleport(game:get_value("respawn_map")) --this line doesn't seem to have any issues, it sends the hero to the default destination though. So let's make a destination for respawning with the info we saved earlier.
          map:create_destination({
            name = "respawn_destination",
            layer = game:get_value("respawn_layer"), x = game:get_value("respawn_x"), y = game:get_value("respawn_y"),
            direction = game:get_value("respawn_direction"), save_location = "yes",
          })
  hero:teleport(game:get_value("respawn_map"), "respawn_destination")
end


My best guess why this won't work is that the map:create_distination() method is creating this destination on the blank map. So I tried rewriting that line as:

Code (lua) Select

  hero:teleport(game:get_value("respawn_map"))
  local new_map = game:get_map()
          new_map:create_destination({
            name = "respawn_destination",
            layer = game:get_value("respawn_layer"), x = game:get_value("respawn_x"), y = game:get_value("respawn_y"),
            direction = game:get_value("respawn_direction"), save_location = "yes",
          })
  hero:teleport(game:get_value("respawn_map"), "respawn_destination")


But it still won't work. For all these, I'm getting the No such destination: "respawn_destination" error. So it seems like the map:create_destination() method isn't working as I'm expecting it to. I don't believe there's a function to get the map userdata for the map you aren't currently on, which would make sense as I'm not sure it technically exists when you aren't on it. So I'm not sure exactly how the engine is interpreting this code.

Perhaps for now I'll just leave this, maybe make some map design decisions that make things a little easier for the player, and come back to it when I've learned more.

Maps are destroyed when you leave them and regenerated when you enter them again. If you put a print statement as the first line in a map script you can see that it gets called every time you enter the map. This means you can only create a destination on the current map.

What you should do is send the hero to the blank map on game over, then in the blank map script send them to the map with respawn location saved at the default destination, then as the hero enters that map (in map_meta:on_started()) create the respawn destination and redirect the hero there. The blank map would have to override the map_meta behavior.

I'm not sure if first sending the hero to a default location on the respawn map before sending them to the real destination could cause problems (like triggering things in your map script you don't want to trigger yet). From in the quest editor on every map you might have to create a dummy destination that is somewhere you can safely send the hero to initially.

As I said in my first post, the destination is OPTIONAL! You don't really need to create a new destination for this, and therefore you shouldn't.
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

Quote from: Diarandor on March 02, 2018, 08:05:14 AM
As I said in my first post, the destination is OPTIONAL! You don't really need to create a new destination for this, and therefore you shouldn't.
It is true that is is not necessary to specify a destination in hero:teleport(), in which case the hero gets sent to the default destination. My point is that you'd have to go though each and every map to verify that sending the hero first to the default destination then moving the hero in map:on_started() to the respawn destination is not going to cause a problem. For example, there may be unintended consequences if the default destination overlaps a sensor that causes things to change on the map.

March 02, 2018, 03:47:33 PM #13 Last Edit: March 02, 2018, 03:59:27 PM by Diarandor
Remark: There is no need of having 1 destination in the map (the hero would teleport to the upper-left corner of the map if there is not a default one, if I remember correctly). I think that sensors and collision tests will not be activated in the process because that can only happen when the map opening transition has finished, and you would do the teleportation in the event "map:on_started" (and not in "map:on_opening_transition_finished").
"If you make people think they're thinking, they'll love you. But if you really make them think, they'll hate you."

Can confirm, sensors seem to only start on_map_transition_finished().

Interestingly, I still can't get the map to create a destination based off the saved respawn data and send the hero to it. My last attempt, to summarize, altered the map:on_opening_transition_finished() meta, so that it did two things.

If game:get_value("currently_gameover") was false, it'd save your location in respawn savegame data.
If game:get_value("currently_gameover") was true, it should create a destination from the respawn savegame data and teleport you there.

Then if you die, the engine would set "currently_gameover" true, and send you to a blank map. The blank map's script would send you to the map saved for respawning, and since "currently_gameover" was true, it SHOULD then create the destination from the respawn data and send you there.... but it didn't.



Anyway, I'm starting to feel like this technique is ballooning out. The much simpler, if less elegant, method would just be to place sensors that use game:set_starting_location() to set a manually placed destination as the starting location. It would require something like:

sensor_east:on_activated()
  game:set_starting_location(map:get_id(), "respawn_destination_1")
end

sensor_wast:on_activated()
  game:set_starting_location(map:get_id(), "respawn_destination_2")
end


to be placed on every map that is reached by scrolling, but honestly for my game, that's probably not more than a dozen maps tops, and in the time I've spent trying to figure out an alternate solution, I could have used this less elegant one and gotten it done a while ago, haha.