Author Topic: Generating a map image from map .dat file  (Read 368 times)

llamazing

  • Full Member
  • ***
  • Posts: 113
    • View Profile
Generating a map image from map .dat file
« on: March 11, 2018, 08:41:09 am »
Given the discussion on the Ocean's Heart project topic about making a map menu, I wanted to test the feasibility of generating a mini-map image from the info in the map .dat file. The endeavor has been a success.

What I did is read the map .dat file from a lua script, which gives the size, position and stacking order of all tiles. Also reading the tileset .dat file used by that map gives the ground type (e.g. traversable, wall, etc.) for each tile. I assigned colors to the tiles based on the ground type. Then on a surface I drew rectangles whose size and position corresponds to each tile at 1/8th scale, drawn in the same order as on the map. I drew layers 0 to 2 separately and ended up with this result for the overworld maps:



Note that sometimes the layer 2 image got clipped because the window I was using was only 480 pixels in height. It didn't appear to be losing anything in the cases where it was clipped.

I then manually overlaid the layers, using 50% opacity for layers 1 & 2, and lined up the maps for how they fit together. In hindsight I realize I could have made the script to do the same thing.



Note that the Hourglass Fort Outside and Naerreturn Bay maps overlap with each other, so I moved the Hourglass map off to the side.

This mapping technique could be useful either in generating a reference image from which to create hand-drawn images of the maps, or a modified version of the script could be used to have the quest itself generate the map images to use for the map menu. For pixel-precise accuracy, the purple border between maps (so that edge teletranpsorters are visible) should be removed.

In order to reproduce my results and run the script, do the following:
1) Download version 0.1 of the Ocean's Heart project from the topic linked above (back up any existing save files if you already have the quest)
2) Change the quest size to 640 by 480 in quest.dat
3) In main.lua of that quest comment out line 5, require("scripts/features"), so that the HUD is not displayed
4) Replace the scripts/game_manager.lua file with this one:
Code: Lua
  1. local game_manager = {}
  2.  
  3. local initial_game = require("scripts/initial_game")
  4. require("scripts/multi_events")
  5.  
  6. -- Starts the game from the given savegame file,
  7. -- initializing it if necessary.
  8. function game_manager:start_game(file_name)
  9.  
  10.         local exists = sol.game.exists(file_name)
  11.         local game = sol.game.load(file_name)
  12.         if not exists then
  13.                 -- Initialize a new savegame.
  14.                 initial_game:initialize_new_savegame(game)
  15.         end
  16.         sol.video.set_window_size(640, 480) --to prevent display from scaling at 2x
  17.         game:start()
  18.        
  19.         local map_layers = require("scripts/menus/display_map")
  20.        
  21.         function game:on_draw(dst_surface)
  22.                 dst_surface:fill_color{0,0,0} --fill black, don't want to see what's underneath
  23.                
  24.                 for layer = 0,2 do
  25.                         local surface = map_layers[layer]
  26.                         if surface then
  27.                                 local width, height = surface:get_size()
  28.                                 surface:draw(dst_surface, 1, layer*(height+1)+1) --1 px from edge of screen with images separated by 1 px
  29.                         end
  30.                 end
  31.         end
  32. end
  33.  
  34. return game_manager
  35.  
5) Add this script to the scripts/menus directory, named display_map.lua:
Code: Lua
  1. local map_id = "new_limestone/limestone_present"
  2. --local map_id = "goatshead_island/goatshead_harbor"
  3. --local map_id = "goatshead_island/west_goat"
  4. --local map_id = "goatshead_island/goat_hill"
  5. --local map_id = "goatshead_island/lighthouse_cape"
  6. --local map_id = "goatshead_island/poplar_forest"
  7. --local map_id = "goatshead_island/poplar_coast"
  8. --local map_id = "goatshead_island/crabhook_village"
  9. --local map_id = "Yarrowmouth/yarrowmouth_village"
  10. --local map_id = "Yarrowmouth/juniper_grove"
  11. --local map_id = "Yarrowmouth/foothills"
  12. --local map_id = "Yarrowmouth/silent_glade"
  13. --local map_id = "Yarrowmouth/naerreturn_bay"
  14. --local map_id = "Yarrowmouth/kingsdown"
  15. --local map_id = "Yarrowmouth/hourglass_fort/outside"
  16. --local map_id = "ballast_harbor/ballast_harbor"
  17.  
  18. local tilesets = {}
  19. local maps = {}
  20.  
  21. local SCALE_FACTOR = 8 --draw maps at 1/8th actual size
  22.  
  23. --color to draw tile based on ground property; ignore if not listed
  24. local tile_colors = {
  25.         traversable = {0, 255, 0}, --green
  26.         wall = {127, 127, 127}, --grey
  27.         low_wall = {204, 204, 204}, --light grey
  28.         teletransporter = {255, 255, 0}, --yellow; this is a type of entity, not a tile ground property
  29.         shallow_water = {0, 255, 255}, --light blue
  30.         deep_water = {0, 0, 255}, --blue
  31.         hole = {0, 0, 0}, --black
  32.         ladder = {255, 0, 255}, --pink
  33.         lava = {255, 0, 0}, --red
  34.         prickles = {255, 127, 0}, --orange
  35.         ice = {127, 255, 255}, --pale blue
  36. }
  37.  
  38. --load tileset .dat file to determine ground property of each tile
  39. local function load_tileset(tileset_id)
  40.         local tileset = {}
  41.        
  42.         local env = {}
  43.         function env.tile_pattern(properties)
  44.                 local id = properties.id
  45.                 assert(id, "tile pattern without id")
  46.                 id = tonumber(id)
  47.                 assert(id, "tile pattern id must be a number")
  48.                
  49.                 local ground = properties.ground
  50.                 assert(ground, "tile pattern without ground")
  51.                
  52.                 if tile_colors[ground] then --ignore ground properties that don't have a color assigned
  53.                         tileset[id] = tile_colors[ground] --link the color to use with the tile id
  54.                 end
  55.         end
  56.        
  57.         setmetatable(env, {__index = function() return function() end end})
  58.        
  59.         local chunk = sol.main.load_file("tilesets/"..tileset_id..".dat")
  60.         setfenv(chunk, env)
  61.         chunk()
  62.        
  63.         return tileset
  64. end
  65.  
  66. --load map .dat file to get list of tiles used
  67. local function load_map(map_id)
  68.         local map = { tiles = {} }
  69.        
  70.         local env = {}
  71.        
  72.         --properties stores the size and coordinates for the map and the tileset used
  73.         function env.properties(properties)
  74.                 local x = tonumber(properties.x)
  75.                 assert(x, "property x must be a number")
  76.                 local y = tonumber(properties.y)
  77.                 assert(y, "property y must be a number")
  78.                
  79.                 local width = tonumber(properties.width)
  80.                 assert(width, "property width must be a number")
  81.                 local height = tonumber(properties.height)
  82.                 assert(height, "property height must be a number")
  83.                
  84.                 local tileset = properties.tileset
  85.                 assert(tileset, "properties without tileset")
  86.                
  87.                 map.x = x
  88.                 map.y = y
  89.                 map.width = width
  90.                 map.height = height
  91.                 map.tileset = tileset
  92.         end
  93.        
  94.         --each tile defines a size, coordinates and layer as well as the tile id to use
  95.         function env.tile(properties)
  96.                 local pattern = properties.pattern --pattern is the tile id
  97.                 assert(pattern, "tile without pattern")
  98.                 pattern = tonumber(pattern)
  99.                 assert(pattern, "tile pattern must be a number")
  100.                
  101.                 local layer = properties.layer
  102.                 assert(layer, "tile without layer")
  103.                 layer = tonumber(layer)
  104.                 assert(layer, "tile layer must be a number")
  105.                
  106.                 local x = tonumber(properties.x)
  107.                 assert(x, "tile x must be a number")
  108.                 local y = tonumber(properties.y)
  109.                 assert(y, "tile y must be a number")
  110.                
  111.                 local width = tonumber(properties.width)
  112.                 assert(width, "tile width must be a number")
  113.                 local height = tonumber(properties.height)
  114.                 assert(height, "tile height must be a number")
  115.                
  116.                 table.insert(map.tiles, {
  117.                         pattern = pattern,
  118.                         layer = layer,
  119.                         x = x,
  120.                         y = y,
  121.                         width = width,
  122.                         height = height,
  123.                 })
  124.         end
  125.        
  126.         --also extract teletransporter usage to be able to draw teletransporter locations
  127.         function env.teletransporter(properties)
  128.                 local layer = properties.layer
  129.                 assert(layer, "tile without layer")
  130.                 layer = tonumber(layer)
  131.                 assert(layer, "tile layer must be a number")
  132.                
  133.                 local x = tonumber(properties.x)
  134.                 assert(x, "tile x must be a number")
  135.                 local y = tonumber(properties.y)
  136.                 assert(y, "tile y must be a number")
  137.                
  138.                 local width = tonumber(properties.width)
  139.                 assert(width, "tile width must be a number")
  140.                 local height = tonumber(properties.height)
  141.                 assert(height, "tile height must be a number")
  142.                
  143.                 table.insert(map.tiles, {
  144.                         pattern = "teletransporter", --instead of using tile pattern to determine color, tile_colors has a "teletransporter" entry
  145.                         layer = layer,
  146.                         x = x,
  147.                         y = y,
  148.                         width = width,
  149.                         height = height,
  150.                 })
  151.         end
  152.        
  153.         setmetatable(env, {__index = function() return function() end end})
  154.        
  155.         local chunk, err = sol.main.load_file("maps/"..map_id..".dat")
  156.         setfenv(chunk, env)
  157.         chunk()
  158.  
  159.         return map
  160. end
  161.  
  162. local function generate_map(map_id)
  163.         assert(type(map_id)=="string", "Bad argument #1 to 'generate_map' (string expected)")
  164.        
  165.         --load map info from .dat file if not already loaded
  166.         if not maps[map_id] then maps[map_id] = load_map(map_id) end
  167.         local map = maps[map_id]
  168.        
  169.         --load tileset info from .dat file if not already loaded
  170.         if not tilesets[map.tileset] then tilesets[map.tileset] = load_tileset(map.tileset) end
  171.         local tileset = tilesets[map.tileset]
  172.        
  173.         --create a surface for each layer
  174.         local layers = {}
  175.         for layer = 0,2 do
  176.                 layers[layer] = sol.surface.create(map.width/SCALE_FACTOR+2, map.height/SCALE_FACTOR+2) --include room for 1px border
  177.                 layers[layer]:fill_color({127, 0, 255}, 0, 0, map.width/SCALE_FACTOR+2, 1) --top 1px purple border
  178.                 layers[layer]:fill_color({127, 0, 255}, 0, 0, 1, map.height/SCALE_FACTOR+2) --left 1px purple border
  179.                 layers[layer]:fill_color({127, 0, 255}, 0, map.height/SCALE_FACTOR+1, map.width/SCALE_FACTOR+2, 1) --bottom 1px purple border
  180.                 layers[layer]:fill_color({127, 0, 255}, map.width/SCALE_FACTOR+1, 0, 1, map.height/SCALE_FACTOR+2) --right 1px purple border
  181.         end
  182.        
  183.         --draw each tile
  184.         for _,tile in ipairs(map.tiles) do
  185.                 local pattern = tile.pattern
  186.                 local tile_color = tile_colors[pattern] or tileset[pattern]
  187.                
  188.                 --draw the tile as solid color on surface corresponding to the correct layer
  189.                 if tile_color and layers[tile.layer] then --ignore corner and empty tiles
  190.                         layers[tile.layer]:fill_color(
  191.                                 tile_color,
  192.                                 tile.x/SCALE_FACTOR+1,
  193.                                 tile.y/SCALE_FACTOR+1,
  194.                                 tile.width/SCALE_FACTOR,
  195.                                 tile.height/SCALE_FACTOR
  196.                         )
  197.                 end
  198.         end
  199.        
  200.         return layers
  201. end
  202.  
  203. return generate_map(map_id)
  204.  

santiago_itzcoatl

  • Newbie
  • *
  • Posts: 13
    • View Profile
Re: Generating a map image from map .dat file
« Reply #1 on: March 12, 2018, 02:20:21 am »

great !

Max

  • Full Member
  • ***
  • Posts: 101
    • View Profile
Re: Generating a map image from map .dat file
« Reply #2 on: March 14, 2018, 03:00:06 am »
I wanted to say this is really cool! I might look into using something like this for my next game. I'm most impressed with how readable it is, considering everything that is impassible is brown, be it cliff, house, tree, rock, fence, whatever, it's still easy to look at just the shapes and be like, oh, there's a town there.

It's interesting how it looks like stairs and teleporters and prickles (etc?) are their own colors too. That's cool that you've designed it to go into such detail, yet wisely omitted things like NPCs. I wonder if it would be configured to show particular NPCs- a person who's the object of your quest, for example, or if particular teleporters could be color coded differently to indicate they're the dungeon you're looking for.

I wouldn't personally want that because I like games about exploring, but it'd be an interesting take if you were interested in further developing this idea.

Small critique- I wouldn't go with purple for stairs, I'd go with white or something that looked more natural with the blue, green, and brown that dominate the colors.

llamazing

  • Full Member
  • ***
  • Posts: 113
    • View Profile
Re: Generating a map image from map .dat file
« Reply #3 on: March 15, 2018, 06:08:27 am »
Small critique- I wouldn't go with purple for stairs, I'd go with white or something that looked more natural with the blue, green, and brown that dominate the colors.
Fair enough. This was really more of a proof of concept and I was more concerned about different things being discernible, however obnoxious their appearance may be. There are a number of things I don't like about the colors that I would tweak on my next pass. I'm also thinking that showing prickles is probably too much detail, and it is probably better to just ignore them.

That's cool that you've designed it to go into such detail, yet wisely omitted things like NPCs. I wonder if it would be configured to show particular NPCs- a person who's the object of your quest, for example, or if particular teleporters could be color coded differently to indicate they're the dungeon you're looking for.
NPCs are an entirely different hurdle to tackle, but still entirely possible to implement. Due to how they wander around, you'd want to find their present locations on the current map rather than getting their starting point from the map.dat file. Dynamic tiles are a similar obstacle that would need to properly be accounted for too. Coding particular teletransporters to appear differently at certain times is also doable.

I wouldn't personally want that because I like games about exploring, but it'd be an interesting take if you were interested in further developing this idea.
Another possibility is to hide tiles that the player hasn't seen yet by painting over them in black. I'm not sure if that would end up being too processor intensive, though, since I have not tried it.

An additional improvement I'd make would be to do certain tileset-specific coloring, but this would require deliberate planning when naming the tiles of the tileset. For example, all exterior house tiles could be given a name that ends in "_house" and then instead of coloring those tiles grey like a wall, they could have a unique color. I would do something similar with tree tops and give those partial transparency.

Also, as I was manually overlaying the individual layer images, I ended up manually darkening the traversible tiles on layer 1 so that bridges could be properly seen. In retrospect in would have made more sense to have the upper layers brighter and the lower layers darker. It would be pretty easy to make the script slightly lighten or darken the color depending on what layer it is on (but it would probably only make sense to do that for traversable type tiles and possibly wall tiles as well).

MetalZelda

  • Hero Member
  • *****
  • Posts: 546
    • View Profile
Re: Generating a map image from map .dat file
« Reply #4 on: March 18, 2018, 06:22:34 pm »
This is what I try to pull with my minimap / Pause Subscreen Minimap
Scaling to a 50x50 square should be the hardest thing to pull

This is awesome
« Last Edit: March 18, 2018, 06:24:13 pm by MetalZelda »