Compare commits

...

10 Commits

Author SHA1 Message Date
wanmay ee394d71e0 Merge pull request 'merge to main' (#1) from bmploading into main
Reviewed-on: #1
lgtm let er rip
2024-04-29 02:54:05 +00:00
wan-may e39e772589 fix menu bug, integer bitmap coordinates 2024-04-28 23:44:18 -03:00
wan-may 03e45194cf add some saving, some buttons 2024-04-28 22:21:32 -03:00
wan-may 8b766723f1 add some UI 2024-04-27 22:38:23 -03:00
wan-may a2eeafcdb3 changed saving. fixed polygon loading (it was dropping the first line) 2024-04-26 13:42:52 -03:00
wan-may 0fbbbe4409 Reload map
Save cities.dat
Tweak travel node loading
2024-04-26 13:11:55 -03:00
wan-may 7c2c3867cf fixed travel node loading (they were upside-down!) 2024-04-25 19:29:04 -03:00
wan-may 18ed5c1ce1 add readme 2024-04-24 22:25:28 -03:00
wan-may 7f457daa94 fixed the camera so its motion is no longer tied to framerate
limited minimum and maximum zoom on camera
changed point scaling so that cities get smaller when zooming in
changed selection cursor to hollow circle
changed capital cities to orange squares
included data/earth/ directory so that files are loaded from there when they're not available in the mod folder
when cities are hidden, hovering over travel nodes will display the travel node location (broken for now)
when cities and travel nodes are hidden, hovering over ai markers will display their details
made the information boxes more opaque
changed border-calculating code so that it works with higher resolutions
2024-04-24 22:09:38 -03:00
wan-may ce15bfdcf7 add drag and drop, parse lines more robustly 2024-04-21 13:07:56 -03:00
33 changed files with 966 additions and 304 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
build/
test/

39
ai.lua
View File

@ -1,13 +1,28 @@
--Manage the AI nodes used by DEFCON.
local t = {}
local bmp = require 'bmp'
local lg = assert( love.graphics )
local locationQuery = require 'locationQuery'
local t = setmetatable( {}, {__index = locationQuery } )
local print = print
local aiNode = {}
local mtAiNode = { __index = aiNode }
function aiNode:formatDisplayInfo()
return ([[AI NODE: %d
LONGITUDE: %3.2f
LATITUDE: %3.2f
ATTACKING: %s
]]):format( self.idx, self.x, self.y, tostring(self.attacking) )
end
function t.load( filename )
local img, imgd = bmp.load( filename )
local nodes = {
filename = filename,
visible = true,
all = {},
att = {},
ptsAtt = {},
def = {},
@ -16,15 +31,19 @@ function t.load( filename )
imgd = imgd }
print( "=== Loading AI Markers: ===" )
local idx = 1
for x = 0, 511 do
for y = 0, 284 do
local r, g = imgd:getPixel( x, 284 - y )
if r > 0.5 or g > 0.5 then
local long = x * (360 / imgd:getWidth()) - 180
local lat = y * (200 / img:getHeight()) - 100
local set = (r > 0.5) and nodes.att or nodes.def
set[#set + 1] = {x = long, y = lat}
print( #set, long, lat )
local attacking = (r > 0.5)
local set = attacking and nodes.att or nodes.def
local node = setmetatable( {x = long, y = lat, attacking = attacking, idx = idx}, mtAiNode )
nodes.all[ idx ] = node
set[#set + 1] = node
idx = idx + 1
end
end
end
@ -42,8 +61,14 @@ function t.load( filename )
k = k + 2
end
end
nodes.all = locationQuery.New( nodes.all )
setmetatable( nodes, {__index = t } )
return nodes
end
return setmetatable( nodes, {__index = t } )
function t.selectNearest( nodes, x, y )
return (nodes.all):getClosestPoint( x, y )
end
function t.draw( nodes )
@ -53,8 +78,8 @@ function t.draw( nodes )
lg.points( nodes.ptsDef )
end
function t.save( nodes, filename )
function t.save( nodes )
return bmp.savePoints( nodes.all, "512rgb24" )
end
return t

124
bmp.lua
View File

@ -7,6 +7,41 @@ local lfs = love.filesystem
local ffi = require 'ffi'
local bit = require 'bit'
local function getHeader( filename )
local offset = love.data.unpack( "<I4", assert(love.filesystem.read( "data", filename, 14 )), 11 )
local header, size = assert( love.filesystem.read( filename, offset ) )
print( "BMP HEADER", filename, size, "\n", header )
return header
end
local formats = {
["512rgb24"] = {
header = getHeader( "data/earth/africa.bmp" ),
encode = function( r ) return math.floor( r * 255 ) end,
bytesPerPixel = 3,
w = 512,
h = 285,
},
["512r4"] = {
header = getHeader( "data/earth/sailable.bmp" ),
encode = function( r ) return math.floor( r * 255 / 16 ) end,
bytesPerPixel = 0.5,
w = 512,
h = 285,
},
["800r4"] = {
header = getHeader( "data/earth/travel_nodes.bmp" ),
encode = function( r ) return math.floor( r * 255 / 16 ) end,
bytesPerPixel = 0.5,
w = 512,
h = 285,
}
}
function t.header( format )
return formats[format] and formats[format].header
end
function t.load( filename )
local imgd = love.image.newImageData( filename )
print( "LOADING BITMAP: ", filename, imgd:getSize(), imgd:getFormat(), imgd:getDimensions() )
@ -15,8 +50,93 @@ function t.load( filename )
return img, imgd
end
function t.save( data, filename )
local w, h = data:getDimensions()
--maps an array of 1-byte pixel values (numbers 0 to 15 inclusive)
--to an array of half-byte pixel values (which can be concatenated into a string)
--BUT don't touch the first element (which is assumed to be a header or something)
local function foldByteArray( bytes )
local nybbles = { bytes[1] }
for j = 2, #bytes / 2 do
local a, b = bytes[ 2 * j - 2 ], bytes[ 2 * j - 1 ]
nybbles[j] = string.char( 16 * a + b )
end
return nybbles
end
local function packTwentyFour( bytes )
local twentyFour = { bytes[1] }
for j = 2, #bytes do
bytes[j] = string.char( bytes[j], bytes[j], bytes[j] )
end
end
function t.save( data, format )
local w, h = data:getDimensions()
format = assert( formats[format] )
local bytes = { format.header }
format.byte = 0
local i = 2
for x = 0, w - 1 do
for y = 0, h - 1 do
bytes[i] = format.encode( data:getPixel(x, y) )
i = i + 1
end
end
if format.bytesPerPixel < 1 then bytes = foldByteArray( bytes )
else packTwentyFour( bytes ) end
return table.concat( bytes )
end
--takes an array of world-space points in [-180, 180] x [-100, 100]
--e.g { { x = 0.5, y = 0.5 }, { x = 0.4, y = 0.6 }, }, etc.
--and a function which maps points to colours
function t.savePoints( points, format )
format = assert( formats[format] )
--set up bitmap as an array of pixels
local w, h = format.w, format.h
local size = 2 + format.w * format.h
local bitmap = { format.header }
for j = 2, size do bitmap[j] = 0 end
--this is black-and-white only. easy case
if format.bytesPerPixel < 1 then
for i, point in ipairs( points ) do
local wx, wy = point.x, point.y
--get bitmap coordinates
local x, y = math.floor(format.w * (wx + 180) / 360), math.floor(format.h * (1.0 - (wy + 100) / 200))
--get index into byte array
local idx = 2 + x * format.h + y
bitmap[idx] = 0x0f --0b00001111
end
--now map pixels to 4-bit strings, slap on the header, and pass it back up
return table.concat( foldByteArray( bitmap ) )
end
--cf. ai.lua: these points can be red or green
for i, point in ipairs( points ) do
local wx, wy = point.x, point.y
--get bitmap coordinates
local x, y = math.floor(format.w * (wx + 180) / 360), math.floor(format.h * (1.0 - (wy + 100) / 200))
--get index into byte array
local idx = 2 + x * format.h + y
--in-editor we could have two points in the same place, possibly of the same or different types
--in case we miss something upstream, don't corrupt these duplicate points,
--just saturate the attack/defense value
if bitmap[idx] == 0 then bitmap[idx] = { attack = point.attacking, place = not(point.attacking)}
else
bitmap[idx].attack = bitmap[idx].attack or point.attacking
bitmap[idx].place = bitmap[idx].place or not(point.attacking)
end
end
--now map pixels to 3-byte strings
for j = 2, size do
if bitmap[j] == 0
then bitmap[j] = "\0\0\0"
else bitmap[j] = string.char( bitmap[j].attack and 0xff or 0, bitmap[j].place and 0xff or 0, 0 )
end
end
return table.concat( bitmap )
end
return t

View File

@ -1,32 +1,128 @@
local t = {}
local lg = love.graphics
function t.onHover( button )
local t = {
name = "",
tooltip = "button",
icon = false,
x = 8,
y = 250,
w = 176,
h = 24,
group = false,
visible = true,
callback = function( self ) return print( "clicked button: ", self.name, self.x, self.y, self.w, self.h, self.visible ) end
}
t.selected, t.next, t.prev = t, t, t
function t.contains( button, x, y )
return x < button.x + button.w and x > button.x
and y < button.y + button.h and y > button.y
end
function t.onClick( button )
local function debugLoop()
local i = 0
local j = 0
local a = t
repeat
i = i + 1
--print( "BUTTON", i, tostring( a ) )
a = a.next
until a == t or i > 100
a = t
repeat
j = j + 1
--print( "BUTTON", i, tostring( a ) )
a = a.prev
until a == t or j > 100
print( i, j, "BUTTONS" )
end
function t.newButton( name, tooltip, icon, x, y, w, h, callback )
return setmetatable( {
name = name,
tooltip = tooltip,
icon = icon,
x = x,
y = y,
w = w,
h = h,
callback = callback },
t )
local k = 1
function t.new( b )
b = setmetatable( b or {}, t )
b.next = t
t.prev.next = b
b.prev = t.prev
t.prev = b
--nonsense
k = k + 1
print( "ADD BUTTON", k, tostring( b ) )
debugLoop()
return b
end
function t.draw( button )
local drawPassOngoing = false
function t.draw( b )
if b == t then
drawPassOngoing = not( drawPassOngoing )
if not drawPassOngoing then return end
elseif b.visible then
lg.rectangle( "line", b.x, b.y, b.w, b.h, 6 )
lg.printf( b.name,
b.x,
b.y + 0.5 * ( b.h- lg.getFont():getHeight() ),
b.w,
"center" )
if b.icon then
local h = b.icon:getHeight()
lg.draw( b.icon,
b.x, b.y,
0,
b.h / h )
end
if t.selected == b then
lg.rectangle( "fill", b.x, b.y, b.w, b.h, 6 )
end
end
return t.draw( b.next )
end
t.__index = t
t.__call = t.newButton
function t.select( b )
t.selected = b
end
function t.selectNext()
repeat t.selected = t.selected.next until (t.selected == t) or t.selected.visible
end
function t.selectPrev()
repeat t.selected = t.selected.prev until (t.selected == t) or t.selected.visible
end
function t.selectIn( x, y )
t.selected = t
repeat t.selected = t.selected.next until (t.selected == t) or (t.selected.visible and t.selected:contains( x, y ))
end
function t.selectNextInGroup()
--make sure our group is visible, otherwise the loop doesn't end
local group = t.selected and t.selected.visible and t.selected.group
if not group then return t.selectNext() end
repeat t.selectNext() until group == t.selected.group
end
function t.selectPrevInGroup()
--make sure our group is visible, otherwise the loop doesn't end
local group = t.selected and t.selected.visible and t.selected.group
if not group then return t.selectPrev() end
repeat t.selectPrev() until group == t.selected.group
end
function t.displayGroup( group, show )
local b = t
repeat
b = b.next
b.visible = ( b.group == group )
until b == t
t.visible = true
end
function t.deselect( b )
t.selected = t
end
setmetatable( t, t )
t.__index = t
t.__call = t.new
return t

View File

@ -3,7 +3,7 @@ local tfTerritory = love.math.newTransform()
local tfNodes = love.math.newTransform()
local lg = assert( love.graphics )
local Camera = {
x = 0, y = 0,
x = -90, y = 45,
w = 360, h = 200,
zoom = 1, tf = tf,
tfTerritory = tfTerritory, tfNodes = tfNodes }
@ -20,12 +20,12 @@ function Camera.GetNodeCoordinate( x, y )
return tfNodes:inverseTransformPoint( x, y )
end
function Camera.Zoom( out )
local scale = out and 1.1 or 0.9
tf:scale( scale, scale )
local x = Camera.x
local y = Camera.y
return Camera.Set( x, y, Camera.w * scale, Camera.h * scale )
function Camera.Zoom( delta )
local scale = 1.0 + delta
if Camera.zoom < 25.0 and delta > 0 or --zooming in
Camera.zoom > 0.5 and delta < 0 then --zooming out
return Camera.Set( Camera.x , Camera.y, Camera.w * scale, Camera.h * scale )
end
end
function Camera.Translate( x, y )
@ -36,9 +36,8 @@ end
--In world coordinates: top left corner at x, y, extent of w, h.
function Camera.Set( x, y, w, h )
print( ("CAMERA: %3.2f %3.2f %3.2f %3.2f"):format(x, y, w, h) )
Camera.x, Camera.y, Camera.w, Camera.h = x, y, w, h
Camera.zoom = w / 360
Camera.zoom = w / 800
tf:reset()
tf:scale( w / 360, -h / 200 )
tf:translate( 180 - x, -y - 100 )
@ -48,8 +47,8 @@ function Camera.Set( x, y, w, h )
tfTerritory:translate( -x * 512 / 360, y * 512 / 360 )
tfNodes:reset()
tfNodes:scale( w / 360, h / 200 )
tfNodes:translate( 180 - x , y + 100 )
tfNodes:scale( w / 360, -h / 200 )
tfNodes:translate( 180 - x , -y - 100 )
--tfNodes:translate( -x * 800 / 360, y * 400 / 200 )
end

View File

@ -23,67 +23,87 @@ function t.unlockSelection()
end
function t.draw()
if cities.visible then lg.points( points ) end
return lg.points( points )
end
function t.drawSelected( r )
if not cities.visible then return end
function t.drawSelected( )
local c = t.selected
if not c then return end
lg.circle( "fill", c.x, c.y, r )
lg.circle( "line", c.x, c.y, 1.0 )
end
function t.drawCapitals()
if cities.visible then lg.points( caps ) end
end
function t.selectNearestCity(x, y)
if not t.selectionLocked then t.selected = cities:getClosestPoint(x, y) end
function t.selectNearest( cities, x, y )
return cities:getClosestPoint(x, y) --defer to locationQuery
end
local city = {}
local citymt = {__index = city}
function city:formatDisplayInfo()
return (
[[
NAME: %s
COUNTRY: %s
LONGITUDE: %3.2f
LATITUDE: %3.2f
POP: %d
CAPITAL: %s]]):format( self.name, self.country, self.x, self.y, self.pop, tostring(self.capital) )
end
function t.load( filename )
print( "=== LOADING CITIES. ===" )
cities = { visible = true, active = false }
cities = { visible = true, active = false, filename = filename }
local n = 1
local idxPts = 1
local idxCaps = 1
points = {}
caps = {}
for line in assert( lfs.lines( filename ), "Error: could not open cities.dat" ) do
local _, _, x, y, pop, capital = line:sub( 83 ):find( "(%g+)%s+(%g+)%s+(%g+)%s+(%g+)" )
x, y, pop, capital = tonumber( x ), tonumber( y ), tonumber( pop ), ( tonumber( capital ) > 0)
local city = {
name = line:sub( 1, 39 ):gsub("%s+$",""),
country = line:sub( 42, 82 ):gsub("%s+$",""),
x = x, y = y, pop = pop, capital = capital
}
cities[n] = city
n = n + 1
if capital then --check against empty or malformed line
x, y, pop, capital = tonumber( x ), tonumber( y ), tonumber( pop ), ( tonumber( capital ) > 0)
local city = setmetatable({
name = line:sub( 1, 39 ):gsub("%s+$",""),
country = line:sub( 42, 82 ):gsub("%s+$",""),
x = x, y = y, pop = pop, capital = capital
}, citymt )
cities[n] = city
n = n + 1
points[idxPts], points[idxPts + 1] = x, y
idxPts = idxPts + 2
points[idxPts], points[idxPts + 1] = x, y
idxPts = idxPts + 2
if capital then
caps[idxCaps], caps[idxCaps + 1] = x, y
idxCaps = idxCaps + 2
if capital then
caps[idxCaps], caps[idxCaps + 1] = x, y
idxCaps = idxCaps + 2
end
else
print( "CITIES: malformed line:", line )
end
end
--Multiple inheritance.
cities = locationQuery.New( cities )
setmetatable( getmetatable( cities ).__index, {__index = t } )
print( "LOADED", filename, n )
print( "=== CITIES LOADED:", filename, n, "===" )
return cities
end
function t.save( cities, filename )
function t.save( cities )
local str = {}
for n, city in ipairs( cities ) do
str[n] = ("%-40s%-40s%f %f %d %d"):format( city.name, city.country, city.x, city.y, city.pop, city.capital and 1 or 0 )
str[n] = ("%-41s%-41s%-14f%-14f%-19d %d"):format(
city.name, city.country, city.x, city.y, city.pop, city.capital and 1 or 0 )
end
assert( lfs.write( filename, table.concat( str, "\n" ) ) )
print( "Saved", filename )
return assert(table.concat( str, "\n" ))
end
return t

View File

@ -11,9 +11,9 @@ function love.conf(t)
t.audio.mixwithsystem = true -- Keep background music playing when opening LOVE (boolean, iOS and Android only)
t.window.title = "dcEarth" -- The window title (string)
t.window.icon = "favicon.png" -- Filepath to an image to use as the window's icon (string)
t.window.icon = "icons/favicon.png" -- Filepath to an image to use as the window's icon (string)
t.window.width = 800 -- The window width (number)
t.window.height = 600 -- The window height (number)
t.window.height = 640 -- The window height (number)
t.window.borderless = false -- Remove all border visuals from the window (boolean)
t.window.resizable = true -- Let the window be user-resizable (boolean)
t.window.minwidth = 512 -- Minimum window width if the window is resizable (number)
@ -44,7 +44,7 @@ function love.conf(t)
t.modules.sound = false -- Enable the sound module (boolean)
t.modules.system = true -- Enable the system module (boolean)
t.modules.thread = false -- Enable the thread module (boolean)
t.modules.timer = false -- Enable the timer module (boolean), Disabling it will result 0 delta time in love.update
t.modules.timer = true -- Enable the timer module (boolean), Disabling it will result 0 delta time in love.update
t.modules.touch = false -- Enable the touch module (boolean)
t.modules.video = false -- Enable the video module (boolean)
t.modules.window = true -- Enable the window module (boolean)

View File

@ -1,4 +1,4 @@
Cambridge United Kingdom 6.166700 35.566700 106673 0
Cambridge United Kingdom 6.166700 35.566700 106673 0
Gloucester United Kingdom 6.666700 36.366699 108150 0
Hastings/Bexhill United Kingdom 5.400000 36.183300 112080 0
Slough United Kingdom 13.250000 -8.833300 106882 0

BIN
data/graphics/blur.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 KiB

BIN
icons/city.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
icons/cursorerase.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
icons/cursorselect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
icons/eye.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
icons/save.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
icons/undo.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
icons/up.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
icons/x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -3,48 +3,98 @@ local t = {}
local lfs = love.filesystem
local lg = love.graphics
local polygon = { x = 180, X = -180, y = 100, Y = -100 } --default empty bounding box
local polymt = { __index = polygon }
function polygon:formatDisplayInfo()
return ([[
x: %f
y: %f
X: %f
Y: %f
N: %d]]):format( self.x, self.y, self.X, self.Y, #self )
end
function t.load( filename )
local polys = { visible = true }
local polys = { visible = true, filename = filename }
local poly = {}
local n = 0
local n = 1
local k = 1
for line in assert( lfs.lines( filename ) ) do
if line == "b" then
if line:find "b" then
k = 1
n = n + 1
poly = {}
if #poly > 2 then n = n + 1 end
poly = setmetatable({}, polymt) --axis-aligned bounding box
polys[n] = poly
else
local _, _, x, y = line:find( "(%g+)%s+(%g+)" )
x, y = assert( tonumber( x ) ), assert( tonumber( y ) )
poly[k], poly[ k + 1 ] = x, y
k = k + 2
x, y = tonumber( x ), tonumber( y )
if x and y then
poly[k], poly[ k + 1 ] = x, y
k = k + 2
if x < poly.x then poly.x = x end
if x > poly.X then poly.X = x end
if y < poly.y then poly.y = y end
if y > poly.Y then poly.Y = y end
else
print( "LINES: malformed line:", filename, line )
end
end
end
if not polys[n] or (#(polys[n]) < 3) then
polys[n] = nil
n = n - 1
end
print( "LOADED", filename, n )
return setmetatable( polys, {__index = t } )
end
function t.save( lines, filename )
function t.selectNearest( lines, wx, wy )
local d = math.huge
local nearest
for i, poly in ipairs( lines ) do
if poly.x - 5 < wx and
poly.X + 5 > wx and
poly.y - 5 < wy and
poly.Y + 5 > wy then
for k = 1, #poly, 2 do
local x, y = poly[k], poly[k + 1]
local r = ( x - wx ) * ( x - wx ) + ( y - wy ) * ( y - wy )
if r < d then
d = r
nearest = poly
end
end
end
end
return nearest
end
function t.save( lines )
local str = {}
for i, poly in ipairs( lines ) do
str[i] = table.concat( poly, " " )
end
str = table.concat( str, "\nb\n" ):gsub("(%S+) (%S+) ", "%1 %2\n")
return str
end
function t.newPolygon( x, y )
end
function t.addToCurrentPolygon( x, y )
end
function t.deletePolygon( poly )
end
function t.drawPolygonEditHandles( poly )
end
function t.draw( lines )

View File

@ -1,34 +1,43 @@
--Shitty acceleration structure: get closest point in set.
--Assumed to be in world coordinates: ( -180, 180 ) x ( -100, 100 )
local t = {}
local hashes = {}
local math = math
local hash = function( x, y, debug )
local s = "h"..("%2d%2d"):format(math.floor( 0.5 + (180 + x) / 10 ), math.floor( 0.5 + (100 + y) / 10 ) )
local s = "h"..("%2d%2d"):format(math.floor( 0.5 + (180 + x) / 20 ), math.floor( 0.5 + (100 + y) / 20 ) )
return s
end
function t.getHashes( points )
return assert( points.hashes )
end
function t.getClosestPoint( points, x, y )
local hashes = t.getHashes( points )
local closePoints = hashes[hash( x, y )]
if not closePoints then return end
if not closePoints then
return
end
local distance = math.huge
local px, py, point
for k, v in pairs(closePoints) do
px, py = v.x, v.y
local d = (x - px) * (x - px) + (y - py) * (y - py)
if d < distance then
distance = d
point = v
end
end
return point
end
function t.Edit( points, point, x, y )
local hashes = t.getHashes( points )
local h = hashes[hash( point.x, point.y )]
if h then
for i, p in pairs(h) do
@ -38,25 +47,33 @@ function t.Edit( points, point, x, y )
end
end
end
table.insert( hashes[ hash( x, y ) ], point )
point.x = x
point.y = y
end
function t.Add( points, x, y )
end
function t.New( points )
local hashes = {}
for i = 1, #points do
local x, y = points[i].x, points[i].y
local h = hash( x, y )
hashes[h] = hashes[h] or {}
hashes[h][#hashes[h] + 1] = points[i]
end
return setmetatable( points, {__index = t } )
points.hashes = hashes
do
local count = 0
for k, v in pairs(hashes) do count = count + 1 end
print( "LOCATION QUERY. Points:", count )
end
return setmetatable( points, {__index = t} )
end
return t

196
main.lua
View File

@ -1,115 +1,68 @@
local love = assert( love, "This tool requires LOVE: love2d.org" )
--assert( require('mobdebug') ).start() --remote debugger
local map = require 'map'
local button = require 'button'
local SAVEDIRECTORY = "out/"
require 'mainmenu'
local Camera = require 'camera'
local wasKeyPressed
function love.load()
love.filesystem.setIdentity( "dcearth", false )
love.keyboard.setKeyRepeat( true )
love.graphics.setNewFont( 14 )
end
local lfs = assert( love.filesystem )
lfs.setIdentity( "dcearth", false )
assert( lfs.createDirectory( SAVEDIRECTORY.."data/earth" ))
assert( lfs.createDirectory( SAVEDIRECTORY.."data/graphics" ))
map.load()
love.graphics.setNewFont( 12, "mono" )
function love.directorydropped( path )
if map.path then
assert( love.filesystem.unmount( map.path ) )
map.loaded = false
end
love.filesystem.mount( path, "" )
return map.load( path )
end
function love.update( dt )
local tx, ty = 0, 0
local moveCamera = false
if love.keyboard.isScancodeDown( "w" ) then moveCamera = true; ty = ty + 1 end
if love.keyboard.isScancodeDown( "a" ) then moveCamera = true; tx = tx - 1 end
if love.keyboard.isScancodeDown( "s" ) then moveCamera = true; ty = ty - 1 end
if love.keyboard.isScancodeDown( "d" ) then moveCamera = true; tx = tx + 1 end
if love.keyboard.isScancodeDown( "q" ) then Camera.Zoom( true ) end
if love.keyboard.isScancodeDown( "e" ) then Camera.Zoom( false ) end
if love.keyboard.isScancodeDown( "w" ) then moveCamera = true; ty = ty + 30 * dt end
if love.keyboard.isScancodeDown( "a" ) then moveCamera = true; tx = tx - 30 * dt end
if love.keyboard.isScancodeDown( "s" ) then moveCamera = true; ty = ty - 30 * dt end
if love.keyboard.isScancodeDown( "d" ) then moveCamera = true; tx = tx + 30 * dt end
if love.keyboard.isScancodeDown( "q" ) then Camera.Zoom( dt ) end
if love.keyboard.isScancodeDown( "e" ) then Camera.Zoom( -dt ) end
if moveCamera then Camera.Translate( tx, ty ) end
end
local toolButtons = {
"Brush",
"Move Nodes",
"Add Nodes",
"Edit Node",
"Draw Polygon",
"Erase Polygon"
}
local layerButtons = {
"Africa",
"Europe",
"North America",
"South America",
"Asia",
"Russia",
"Travel Nodes",
"AI Markers",
"Cities",
"Coastlines",
"Coastlines Low",
"Sailable"
}
function love.draw()
if not map.loaded then
local w, h = love.graphics.getDimensions()
return love.graphics.printf( "Drag and drop folder to begin.", w / 2 - 200, h / 2 - 128, 400, "center")
end
love.graphics.push( "all" )
map.draw()
love.graphics.pop()
--Layer buttons.
do
love.graphics.setColor( 1, 1, 1, 1 )
local h = love.graphics.getHeight() - 60
for x = 0, 300, 30 do
love.graphics.rectangle( "line", x, h, 30, 30 )
end
end
--Status bar.
local x, y = love.mouse.getPosition()
local wx, wy = Camera.GetWorldCoordinate( x, y )
local bx, by = Camera.GetBitmapCoordinate( x, y )
local h = love.graphics.getHeight() - 30
love.graphics.setColor( 0.2, 0.1, 0.1, 0.5 )
love.graphics.rectangle( "fill", 0, h, love.graphics.getWidth() / 2, 30 )
local h = love.graphics.getHeight() - 60
love.graphics.setColor( 0.1, 0.1, 0.5, 0.8 )
love.graphics.rectangle( "fill", 0, 0, 250, love.graphics.getHeight() )
love.graphics.setColor( 1, 1, 1, 1 )
love.graphics.rectangle( "line", 0, h, love.graphics.getWidth() / 2, 30 )
love.graphics.print(("SCREEN\t%d\t%d\nWORLD \t%5.2f\t%5.2f"):format(x, y, wx, wy), 0, h)
love.graphics.print(("BITMAP\t%5.2f\t%5.2f"):format(bx, by), 200, h )
love.graphics.print(([[
SCREEN %-12d %-12d
WORLD %-12.2f%-12.2f
BITMAP %-12d %-12d
%s]]):format(x, y, wx, wy, bx, by, map.editLayer and map.editLayer.filename or ""), 0, 0)
--Edit box.
love.graphics.rectangle( "line", love.graphics.getWidth() / 2, h, love.graphics.getWidth() / 2, 30 )
if map.cities.selected then
local c = map.cities.selected
love.graphics.setColor( 0.2, 0.1, 0.1, 0.5 )
love.graphics.rectangle( "fill", 0, 0, 150 ,100 )
love.graphics.setColor( 1, 1, 1, 1 )
love.graphics.rectangle( "line", 0, 0, 150 ,100 )
love.graphics.setColor( 1.2, 1.1, 1.1, 1.5 )
love.graphics.print( ("NAME: %s\nX: %3.2f\nY: %3.2f\nPOP: %d\nCAPITAL: %s\nCOUNTRY: %s"):format(c.name, c.x, c.y, c.pop, tostring(c.capital), c.country), 0, 0 )
elseif map.travelnodes.selected then
local c = map.travelnodes.selected
love.graphics.setColor( 0.2, 0.1, 0.1, 0.5 )
love.graphics.rectangle( "fill", 0, 0, 150 ,100 )
love.graphics.setColor( 1, 1, 1, 1 )
love.graphics.rectangle( "line", 0, 0, 150 ,100 )
love.graphics.setColor( 1.2, 1.1, 1.1, 1.5 )
love.graphics.print( ("Node: %d\nX: %3.2f\nY: %3.2f\n"):format(c.number, c.x, c.y) )
elseif map.ainodes.selectedNode then
local c = map.ainodes.selected
love.graphics.setColor( 0.2, 0.1, 0.1, 0.5 )
love.graphics.rectangle( "fill", 0, 0, 150 ,100 )
love.graphics.setColor( 1, 1, 1, 1 )
love.graphics.rectangle( "line", 0, 0, 150 ,100 )
love.graphics.setColor( 1.2, 1.1, 1.1, 1.5 )
love.graphics.print( ("Node: %d\nX: %3.2f\nY: %3.2f\noffensive: %s"):format(c.number, c.x, c.y, c.attack) )
end
if map.selected then love.graphics.print( map.selected:formatDisplayInfo(), 0, 80 ) end
if map.selectionLocked then end
love.graphics.setColor( 1, 1, 1, 0.8 )
button:draw()
end
function love.resize(w, h)
@ -117,54 +70,47 @@ function love.resize(w, h)
end
function love.wheelmoved(x, y)
Camera.Zoom( (y > 0) and true or false )
Camera.Zoom( (y > 0) and 0.1 or -0.1 )
end
function love.mousepressed( x, y, button, istouch, presses )
function love.mousepressed( x, y, mouseButton, istouch, presses )
local wx, wy = Camera.GetWorldCoordinate( x, y )
if button == 1 then
map.cities.lockSelection()
else
map.cities.unlockSelection()
if button.selected and button.selected:contains( x, y ) then
print( ("MOUSE\tx %f\ty %f\twx %f\twy %f"):format(x, y, wx, wy) )
button.callback( button.selected )
return button.selected:callback()
end
print( ("MOUSE\tx %f\ty %f\twx %f\twy %f"):format(x, y, wx, wy) )
end
function love.mousemoved( x, y, dx, dy, istouch )
map.cities.selectNearestCity( Camera.GetWorldCoordinate( x, y ) )
end
if not map.loaded then return end
--mouse over menu
button.selectIn( x, y )
local function ToggleVisibility( layer )
if not layer then return end
local ml
if map[layer] then ml = map[layer] end
if map.territory[layer] then ml = map.territory[layer] end
assert( ml )
ml.visible = not( ml.visible )
print( layer, ml.visible )
end
local layerVisibilityKeybinds = {
["1"] = "africa",
["2"] = "europe",
["3"] = "northamerica",
["4"] = "southamerica",
["5"] = "southasia",
["6"] = "russia",
["7"] = "sailable",
["8"] = "coastlines",
["9"] = "coastlinesLow",
["0"] = "international",
["-"] = "cities"
}
function love.keypressed(key)
ToggleVisibility( layerVisibilityKeybinds[key] )
if key == "l" then
-- To open a file or folder, "file://" must be prepended to the path.
love.system.openURL("file://"..love.filesystem.getSaveDirectory())
--mouse on map
if map.selectionLocked then return end
if map.editLayer and map.editLayer.selectNearest then
map.selected = map.editLayer:selectNearest( Camera.GetWorldCoordinate( x, y ) )
end
wasKeyPressed = true
end
function love.keypressed(key, code, isRepeat)
if code == "left" then return button.selectPrev() end
if code == "right" then return button.selectNext() end
if code == "down" then return button.selectNextInGroup() end
if code == "up" then return button.selectPrevInGroup() end
if code == "return" then return button.selected:callback() end
if key == "c" then
map.selectionLocked = not( map.selectionLocked )
end
end
do
end

143
mainmenu.lua Normal file
View File

@ -0,0 +1,143 @@
local love = assert( love )
local button = require 'button'
local savemodal = require 'savemodal'
local map = require 'map'
button.new{ name = "SAVE", y = 222, callback = savemodal.start, icon = love.graphics.newImage( "icons/save.png" )}
button.new{ name = "UNDO", y = 250, callback = map.undo, icon = love.graphics.newImage( "icons/undo.bmp" ) }
local tools
local layerButtons = {}
local function back( self )
for k, button in pairs( tools ) do button.visible = false end
for k, button in pairs( layerButtons ) do button.visible = true end
self.visible = false
map.editLayer = false
end
backButton = button.new{
name = "UP",
visible = false,
y = 250 + button.h + 4,
icon = love.graphics.newImage( "icons/up.bmp" ),
callback = back,
}
local function toolCallback( self )
local f = (map.layers[self.layer])[self.name]
if f then return f(self) end
end
tools = {
button.new{ name = "SELECT"},
button.new{ name = "ERASE",},
button.new{ name = "MOVE", },
button.new{ name = "ADD", },
button.new{ name = "EDIT", },
button.new{ name = "DRAW", },
}
for i, v in ipairs( tools ) do
v.callback = toolCallback
v.y = 250 + (v.h + 4) * ( i + 1 )
v.visible = false
end
local layers = {
{ name = "AF", layer = "africa" },
{ name = "EU", layer = "europe" },
{ name = "NA", layer = "northamerica" },
{ name = "SA", layer = "southamerica" },
{ name = "AS", layer = "southasia" },
{ name = "RU", layer = "russia" },
{ name = "PATH", layer = "travelnodes" },
{ name = "AI", layer = "ainodes" },
{ name = "CITY", layer = "cities" },
{ name = "COAST", layer = "coastlines" },
{ name = "LOW", layer = "coastlinesLow"},
{ name = "INT", layer = "international"},
{ name = "SAIL", layer = "sailable" },
}
local showButtons = {}
local visibilityIcon = love.graphics.newImage( "icons/eye.bmp" )
local function updateVisibilityIcons()
for i = 1, #showButtons do
showButtons[i].icon = map.layers[ showButtons[i].layer ].visible and visibilityIcon
end
end
local function toggleVisibleLayer( self )
if not (self and self.layer) then return end
local ml = map.layers[ self.layer ]
ml.visible = not( ml.visible )
return updateVisibilityIcons()
end
local soloIcon = false--love.graphics.newImage( "icons/eye.bmp" )
local function soloVisibleLayer( self )
for k, layer in pairs( map.layers ) do
print( "invisible layer, map:", k, layer)
layer.visible = false
end
map.layers[ self.layer ].visible = true
return updateVisibilityIcons()
end
local backButton
local function editLayer( self )
map.editLayer = map.layers[ self.layer ]
map.editLayer.visible = true
for k, button in pairs( layerButtons ) do button.visible = false end
for k, button in pairs( tools ) do
button.visible = true
button.layer = self.layer
end
backButton.visible = true
print( "EDITING LAYER", self.layer )
return updateVisibilityIcons()
end
local function copy( i, target )
for k, v in pairs( layers[i] ) do
target[k] = target[k] or v
end
return target
end
local y = 250
local soloButtons = {}
local editButtons = {}
for i = 1, #layers do
editButtons[i] = button.new( copy( i, {
x = 8,
y = y + (button.h + 4) * i,
w = 112,
callback = editLayer,
group = "edit",
}))
layerButtons[ 3 * i - 2 ] = editButtons[i]
showButtons[i] = button.new( copy( i, {
x = 128,
y = y + (button.h + 4) * i,
w = 24,
name = "",
callback = toggleVisibleLayer,
icon = visibilityIcon,
group = "show",
}))
layerButtons[ 3 * i - 1 ] = showButtons[i]
soloButtons[i] = button.new( copy( i, {
x = 160,
y = y + (button.h + 4) * i,
w = 24,
name = "S",
callback = soloVisibleLayer,
icon = soloIcon,
group = "solo",
}))
layerButtons[ 3 * i ] = soloButtons[i]
end

139
map.lua
View File

@ -2,15 +2,35 @@ local lg = love.graphics
local AI = require 'ai'
local Cities = require 'cities'
local Lines = require 'lines'
local Nodes = require 'nodes'
local Bitmap = require 'bmp'
local Nodes = require 'travelNodes'
local Camera = require 'camera'
local Territory = require 'territory'
local map = {
--flat list of editable layers for convenience
local layers = {
coastlines = false,
coastlinesLow = false,
international = false,
africa = false,
europe = false,
northamerica = false,
russia = false,
southamerica = false,
southasia = false,
travelnodes = false,
sailable = false,
ainodes = false,
cities = false,
}
local map = {
layers = layers,
path = false,
loaded = false,
selected = false,
selectionLocked = false,
editLayer = false,
territory = {
africa = false,
europe = false,
@ -19,13 +39,19 @@ local map = {
southamerica = false,
southasia = false
},
background = false,
coastlines = false,
coastlinesLow = false,
international = false,
travelnodes = false,
sailable = false,
aimarkers = false,
ainodes = false,
cities = false
}
function map.load()
function map.load( path )
map.background = lg.newImage( "data/graphics/blur.bmp" )
map.cities = Cities.load( "data/earth/cities.dat" )
map.coastlines = Lines.load( "data/earth/coastlines.dat" )
map.coastlinesLow = Lines.load( "data/earth/coastlines-low.dat" )
@ -36,16 +62,27 @@ function map.load()
for k, v in pairs(map.territory) do
map.territory[k] = Territory.load( "data/earth/"..k..".bmp", k )
end
map.loaded = true
map.path = path
--update references
for k, v in pairs( layers ) do
layers[k] = map[k] or map.territory[k]
end
end
function map.draw()
lg.clear( 0, 0, 0, 1 )
if not map.loaded then return end
do --territory
lg.setColor( 1,1,1,1)
lg.setLineJoin( "none" )
lg.replaceTransform( Camera.tfTerritory )
lg.setBlendMode( "add" )
lg.setColor( 1, 1, 1, 0.2 )
lg.draw( map.background )
lg.setColor( 1, 1, 1, 0.5 )
for k, v in pairs(map.territory) do
if v.visible then
v:draw()
@ -55,41 +92,56 @@ function map.draw()
v:drawBorder( "sea" )
end
end
if map.sailable.visible then map.sailable:draw() end
if map.sailable.visible then
map.sailable:draw()
lg.setLineJoin( "none" )
lg.setLineWidth( 1 / Camera.zoom )
lg.setColor( 1, 1, 1, 0.5)
map.sailable:drawBorder( "sailable" )
lg.setLineWidth( 3 / Camera.zoom )
map.sailable:drawBorder( "placeable" )
end
lg.setBlendMode( "alpha" )
lg.setColor( 1, 1, 1, 1 )
end
do --borders
lg.setLineJoin( "none" )
lg.setLineWidth( 1 / Camera.zoom )
lg.setColor( 1, 1, 1, 0.5)
map.sailable:drawBorder( "sailable" )
lg.setLineWidth( 3 / Camera.zoom )
map.sailable:drawBorder( "placeable" )
end
do --all this stuff is drawn in world coordinates, ( -180, 180 ) x ( -100, 100 )
lg.replaceTransform( Camera.tf )
do --points
if map.selected then
if map.selected[1] then --lines
local p = map.selected
lg.setColor( 0.4, 0.5, 0.8, 0.5 )
lg.setLineWidth( 0.2 / Camera.zoom )
lg.rectangle( "fill", p.x, p.y, p.X - p.x, p.Y - p.y )
lg.setColor( 1.0, 0, 0, 1 )
lg.line( p )
else --points
lg.setColor( 1.0, 0.5, 0.5, 0.9 )
lg.setLineJoin( "miter" )
lg.setLineWidth( 1.0 / Camera.zoom )
lg.circle( "line", map.selected.x, map.selected.y, 0.2 + 1.0 / Camera.zoom )
end
end
if map.cities.visible then --points
lg.setColor( 1, 0, 0, 0.5 )
lg.setPointSize( 0.5 * Camera.zoom )
lg.setPointSize( 5.0 )
map.cities.draw()
lg.setColor( 1, 1, 1.0, 0.5 )
lg.setPointSize( 1.0 * Camera.zoom )
lg.setColor( 1, 1, 0.0, 0.5 )
map.cities.drawCapitals()
lg.setColor( 1, 1, 1, 0.5 )
map.cities.drawSelected( 15.0 / Camera.zoom )
end
if map.ainodes.visible then
lg.setPointSize( 5.0 )
map.ainodes:draw()
end
@ -112,26 +164,31 @@ function map.draw()
lg.line( -180, 100, 180, 100 )
lg.line( -180, -100, 180, -100 )
end
do --travel nodes
lg.replaceTransform( Camera.tfNodes )
map.travelnodes:draw()
if map.travelnodes.visible then
map.travelnodes:draw()
end
end
end
end
local function write( filename, string )
print( "Pretending to write", string:len(), "bytes to", filename )
--[[ os.rename( filename, filename..".bak" ) --just in case :^)
local file = assert( io.open( filename, "w+" ) )
assert( file:write( string ) )
file:close()]]
end
function map.save()
map.cities.save()
map.coastlines.save()
map.coastlinesLow.save()
map.international.save()
map.sailable.save()
map.travelnodes.save()
map.ainodes.save()
for k, v in pairs(map.territory) do
map.territory[k].save()
for k, layer in pairs( layers ) do
print( "SAVING:", k, tostring( layer.filename ) )
write( map.path..tostring( layer.filename ), assert( layer:save() ) )
end
end
@ -139,4 +196,8 @@ function map.hover(x, y)
end
function map.undo()
print( "=== UNDO ===" )
end
return map

52
modal.lua Normal file
View File

@ -0,0 +1,52 @@
local love = assert( love )
local button = require( "button" )
local t = {}
t.__index = t
local i = 0
function t.start( self )
i = i + 1
t[i] = t[i] or {}
--store callbacks
for name in pairs( self ) do
if love[name] then
t[i][name] = love[name]
love[name] = self[name]
end
end
--store menus
local b = button.next
repeat
t[i][b] = b.visible
b = b.next
until b == button
end
function t.stop( self )
--restore callbacks
for name in pairs( self ) do
if love[name] then
love[name] = t[i][name]
end
end
--restore menus
local b = button
button.selected = button
repeat
b = b.next
b.visible = t[i][b]
until b == button
t[i] = nil
i = i - 1
end
function t.new( modal )
return setmetatable( modal or {}, t )
end
return t

1
readme.txt Normal file
View File

@ -0,0 +1 @@
Map editor for DEFCON, the strategy game, written in LOVE2D, the engine.

58
savemodal.lua Normal file
View File

@ -0,0 +1,58 @@
local love = assert( love )
local modal = require( "modal" )
local button = require( "button" )
local map = require( "map" )
local t = {}
local saveLocation = map.path
local floppy = love.graphics.newImage( "icons/save.png" )
floppy:setFilter( "nearest", "nearest" )
local saveButton = button.new{
group = "saveModal",
name = "save",
callback = function() map.save(); return t:stop() end,
visible = false,
icon = floppy,
x = love.graphics.getWidth() / 2 - 200,
y = love.graphics.getHeight() / 2 - 150,
w = 400,
h = 100,
}
local xIcon = love.graphics.newImage( "icons/x.png" )
xIcon:setFilter( "nearest", "nearest" )
local cancelButton = button.new{
group = "saveModal",
name = "cancel",
visible = false,
icon = xIcon,
callback = function() return t:stop() end,
x = love.graphics.getWidth() / 2 - 200,
y = love.graphics.getHeight() / 2,
w = 400,
h = 100
}
function t.start()
modal.start( t )
button.selected = saveButton
saveButton.name = "save to "..map.path
button.displayGroup( "saveModal", true )
end
function t.draw()
love.graphics.clear( 0,0,0,1 )
love.graphics.setColor( 1, 0, 0, 0.4 )
button:draw()
end
function t.directorydropped( path )
saveLocation = path
map.path = path
saveButton.name = "save to "..map.path
return love.filesystem.mount( path, "" )
end
return modal.new( t )

View File

@ -1,2 +0,0 @@
local jit = require 'jit'
for k, v in pairs( jit.opt ) do print(k , v ) end

View File

@ -18,6 +18,7 @@ function t.load( filename, name )
local territory = {
visible = true,
name = name,
filename = filename,
colour = colours[name],
border = {},
img = img,
@ -109,8 +110,9 @@ function t.computeBorder( territory, threshold, key )
local border = territory[key]
local n = 1
for x = 0, 511 do
for y = 0, 284 do
local w, h = territory.imgd:getWidth() - 1, territory.imgd:getHeight() - 1
for x = 0, w do
for y = 0, h do
--Bottom left, bottom right, and top right of pixel in image coordinates:
local blx, bly = x, y + 1
local brx, bry = x + 1, y + 1
@ -143,12 +145,8 @@ function t.computeBorder( territory, threshold, key )
end
end
function t.drawConnections( nodes )
end
function t.save( nodes, filename )
function t.save( territory )
return bmp.save( territory.imgd, "512rgb24" )
end
return t

64
textinput.lua Normal file
View File

@ -0,0 +1,64 @@
local love = assert( love )
local utf8 = require("utf8")
local modal = require( "modal" )
local t = modal.new{ }
function t.setCurrentModal( fields )
t.currentModal = assert( fields )
t.currentIdx = 1
return t:start()
end
function t.draw()
if not t.currentModal then return end
love.graphics.setColor( 0, 0, 0, 0.3 )
love.graphics.rectangle("fill", 0, 0, love.graphics.getDimensions())
-- other fields
for i, field in ipairs( t.currentModal ) do
love.graphics.setColor( 1, 1, 1, 0.2 )
local mode = (i == t.currentIdx) and "fill" or "line"
love.graphics.rectangle( mode, 200, i * 28, 200, 14, 4 )
love.graphics.setColor( 1, 1, 1, 1 )
love.graphics.print( field.name, 8, i * 28 )
love.graphics.print( field.value, 216, i * 28 )
end
end
function t.textinput( char )
if t.currentModal then
local field = t.currentModal.currentFieldIdx
t.currentModal.currentField.value = t.currentModal.currentField.value .. char
end
end
function t.keypressed(key, code, isRepeat)
if code == "down" then
t.currentIdx = t.currentIdx + 1
if t.currentIdx > #(t.currentModal) then
t.currentIdx = 1
end
elseif code == "up" then
t.currentIdx = t.currentIdx - 1
if t.currentIdx < 1 then
t.currentIdx = #(t.currentModal)
end
end
if key == "backspace" then
-- get the byte offset to the last UTF-8 character in the string.
local byteoffset = utf8.offset(text, -1)
if byteoffset then
-- remove the last UTF-8 character.
-- string.sub operates on bytes rather than UTF-8 characters, so we couldn't do string.sub(text, 1, -2).
text = string.sub(text, 1, byteoffset - 1)
end
end
if key == "escape" then
return t:stop()
end
end
return t

View File

@ -1,101 +1,113 @@
--Manage the pathfinding nodes used by DEFCON.
--This is important for a mapping tool because the DEFCON client will not load a map unless
--the pathfinding nodes form a connected graph.
local t = {}
local bmp = require 'bmp'
local locationQuery = require 'locationQuery'
local lg = assert( love.graphics )
local t = setmetatable({}, {__index = locationQuery})
local isSailable
local function isConnected( startNode, endNode )
local ix, iy, fx, fy = startNode.x, startNode.y, endNode.x, endNode.y
if fx < -180 then fx = fx + 180 end
if fx > 180 then fx = fx - 180 end
local dx, dy = fx - ix, fy - iy
local mag = math.sqrt( dx * dx + dy * dy )
local n = 2 * math.floor( mag )
dx, dy = 0.5 * dx / mag, 0.5 * dy / mag
for i = 1, n do
ix, iy = ix + dx, iy + dy
if not( isSailable( ix, iy ) ) then return nil end
if not( isSailable( ix, -iy ) ) then return nil end
end
return true
end
function t.getClosest( nodes, x, y )
local d = math.huge
local closestNode
for _, node in pairs( nodes.nodes ) do
local nx, ny = node.x, node.y
local nd = (nx - x) * (nx - x) + (ny - y) * (ny - y)
if nd < d then d = nd; closestNode = node end
end
return closestNode
function t.selectNearest( nodes, x, y )
return (nodes.nodes):getClosestPoint( x, y )
end
local travelNode = {}
local mtTravelNode = { __index = travelNode }
function travelNode:formatDisplayInfo()
return ([[ TRAVEL NODE: %d
LONGITUDE: %3.2f
LATITUDE: %3.2f]]):format( self.idx, self.x, self.y )
end
local function worldToBitmap( x, y )
end
local function bitmapToWorld( x, y )
return 360 * ( x - 800 ) / 800 - 360 / 2 + 360,
360 * ( 600 / 800 ) * ( y - 600 ) / 600 + 180
end
function t.load( filename, sailable )
isSailable = sailable
local img, imgd = bmp.load( filename )
local nodes = { visible = true, nodes = {}, points = {}, connections = {}, img = img }
print( "=== Loading Nodes: ===" )
local nodes = { filename = filename, visible = true, nodes = {}, points = {}, connections = {}, img = img }
local n = 1
for x = 0, 799 do
for y = 0, 399 do
if imgd:getPixel( x, 399 - y ) > 0 then
local long = 360 * ( x - 800 ) / 800 - 360 / 2 + 360
local lat = 360 * ( 600 / 800 ) * ( 600 - y ) / 600 - 180
nodes.nodes[n] = {x = long, y = lat}
local long, lat = bitmapToWorld( x, y )
nodes.nodes[n] = setmetatable({x = long, y = lat, idx = n}, mtTravelNode )
nodes.points[ 2 * n - 1 ] = long
nodes.points[ 2 * n ] = lat
print( n, long, lat )
n = n + 1
end
end
end
for i, srcNode in ipairs( nodes.nodes ) do
local adjacent = {}
for j, destNode in ipairs( nodes.nodes ) do
adjacent[j] = isConnected( srcNode, destNode )
end
nodes.connections[i] = adjacent
end
print( "=== Nodes Loaded ===" )
return setmetatable( nodes, {__index = t } )
nodes.nodes = locationQuery.New( nodes.nodes )
setmetatable( nodes, {__index = t} )
return nodes
end
--Determine if graph has more than one connected component.
function t.isConnected( nodes )
end
function t.draw( nodes )
lg.setPointSize( 10 )
lg.setPointSize( 8 )
lg.setColor( 1, 1, 1, 0.5 )
lg.points( nodes.points )
return t.drawConnections( nodes )
end
function t.drawConnections( nodes )
for i, connection in pairs( nodes.connections ) do
for j in pairs( connection ) do
local ix, iy, fx, fy = nodes.nodes[i].x, nodes.nodes[i].y, nodes.nodes[j].x, nodes.nodes[j].y
lg.line( ix, iy, fx, fy )
end
end
end
function t.drawConnections( nodes )
end
function t.save( nodes, filename )
function t.save( nodes )
return bmp.savePoints( nodes.nodes, "800r4")
end
return t