Compare commits

..

12 Commits

Author SHA1 Message Date
wan-may b8e00cf340 fixed bitmap export (files were being written in text mode) 2024-07-12 23:37:50 -03:00
wan-may 03f1a96112 fix a couple of travel node saving bugs 2024-05-24 23:43:39 -03:00
wan-may 9635e0ea2f Fixed the travel node loading bugs! 2024-05-01 21:32:07 -03: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
34 changed files with 1273 additions and 385 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build/
test/
backup/

39
ai.lua
View File

@ -1,13 +1,28 @@
--Manage the AI nodes used by DEFCON. --Manage the AI nodes used by DEFCON.
local t = {}
local bmp = require 'bmp' local bmp = require 'bmp'
local lg = assert( love.graphics ) local lg = assert( love.graphics )
local locationQuery = require 'locationQuery'
local t = setmetatable( {}, {__index = locationQuery } )
local print = print 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 ) function t.load( filename )
local img, imgd = bmp.load( filename ) local img, imgd = bmp.load( filename )
local nodes = { local nodes = {
filename = filename,
visible = true, visible = true,
all = {},
att = {}, att = {},
ptsAtt = {}, ptsAtt = {},
def = {}, def = {},
@ -16,15 +31,19 @@ function t.load( filename )
imgd = imgd } imgd = imgd }
print( "=== Loading AI Markers: ===" ) print( "=== Loading AI Markers: ===" )
local idx = 1
for x = 0, 511 do for x = 0, 511 do
for y = 0, 284 do for y = 0, 284 do
local r, g = imgd:getPixel( x, 284 - y ) local r, g = imgd:getPixel( x, 284 - y )
if r > 0.5 or g > 0.5 then if r > 0.5 or g > 0.5 then
local long = x * (360 / imgd:getWidth()) - 180 local long = x * (360 / imgd:getWidth()) - 180
local lat = y * (200 / img:getHeight()) - 100 local lat = y * (200 / img:getHeight()) - 100
local set = (r > 0.5) and nodes.att or nodes.def local attacking = (r > 0.5)
set[#set + 1] = {x = long, y = lat} local set = attacking and nodes.att or nodes.def
print( #set, long, lat ) 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 end
end end
@ -43,7 +62,13 @@ function t.load( filename )
end end
end end
return setmetatable( nodes, {__index = t } ) nodes.all = locationQuery.New( nodes.all )
setmetatable( nodes, {__index = t } )
return nodes
end
function t.selectNearest( nodes, x, y )
return (nodes.all):getClosestPoint( x, y )
end end
function t.draw( nodes ) function t.draw( nodes )
@ -53,8 +78,8 @@ function t.draw( nodes )
lg.points( nodes.ptsDef ) lg.points( nodes.ptsDef )
end end
function t.save( nodes, filename ) function t.save( nodes )
return bmp.ai( nodes.all )
end end
return t return t

309
bmp.lua
View File

@ -1,11 +1,300 @@
--Load and save the bmp formats used by DEFCON. --Load and save the bmp formats used by DEFCON.
local t = {} local t = {}
local assert = assert
local print = print
local error = error
local table = table
local math = math
local love = assert( love ) local love = assert( love )
local lfs = love.filesystem
--FFI bit-twiddling stuff.
local ffi = require 'ffi'
local bit = require 'bit' local test = {}
function test.load( filename )
local imgd = love.image.newImageData( filename )
print( "LOADING BITMAP: ", filename, imgd:getSize(), imgd:getFormat(), imgd:getDimensions() )
local img = love.graphics.newImage( imgd )
img:setFilter( "nearest", "nearest" )
return img, imgd
end
function test.bitmapToWorld( x, y )
local w, a = 360.0, 600.0 / 800.0
local h = 360.0 * a
x = w * ( x - 800 ) / 800 - w / 2 + 360
y = h * ( y - 600 ) / 600 + 180
return x, y
end
function test.compareData( a, b )
if a == b then return true end
print( "lengths:", a:len(), b:len() )
local errors = 0
for i = 1, math.min( a:len(), b:len() ) do
local a, b = a:sub( i, i ), b:sub( i, i )
if a ~= b then
errors = errors + 1
print( "mismatch:", errors, i, string.byte( a ), string.byte( b ) )
end
if errors > 1000 then break end
end
error( "test failed. output bitmap does not match!" )
end
function test.worldToBitmap( x, y )
x = 800 * ( x + 180 ) / 360
y = 600 + 800 * ( y - 180 ) / 360
return x, y
end
local function getHeader( filename )
local offset = 2 + 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", string.byte( header, 1, size ) )
return header
end
--BGR24 format. Takes the input of Love2D's data:getPixel( x, y )
--on paper we would only care about one channel because all of these images are grayscale,
--but a few of the pixels in the game's default territories are not exactly gray,
--so we do it like this to make sure our output is byte-faithful when saving an unmodified vanilla map
local function bgrChar( r, g, b )
return string.char( math.floor( b * 255 ), math.floor( g * 255 ), math.floor( r * 255 ) )
end
local formats = {
["africa"] = {
header = getHeader( "data/earth/africa.bmp" ),
w = 512,
h = 285,
},
["sailable"] = {
header = getHeader( "data/earth/sailable.bmp" ),
w = 512,
h = 285,
--technically this information is in the header already but I don't want to write code to parse it since we only use this one header
palette = {
[0] = 0,
[8] = 1,
[20] = 2,
[33] = 3,
[49] = 4,
[90] = 5,
[139] = 6,
[169] = 7,
[189] = 8,
[206] = 9,
[214] = 10,
[222] = 11,
[231] = 12,
[239] = 13,
[247] = 14,
[255] = 15
},
},
["travel"] = {
header = getHeader( "data/earth/travel_nodes.bmp" ),
w = 800,
h = 400,
},
["ai"] = {
header = getHeader( "data/earth/ai_markers.bmp" ),
w = 512,
h = 285,
},
}
function formats.ai:test( )
print "testing ai nodes"
local filename = "data/earth/ai_markers.bmp"
local img, imgd = test.load( filename )
local idx = 1
local nodes = {}
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 attacking = (r > 0.5)
local node = {x = long, y = lat, attacking = attacking, idx = idx}
print( "ai marker", idx, x, y, long, lat )
nodes[ idx ] = node
idx = idx + 1
end
end
end
local encodedString = self:encode( nodes )
love.filesystem.write( "ai_markers.bmp", encodedString )
local eimg, eimgd = test.load( "ai_markers.bmp" ) --the one we just saved
for x = 0, 511 do
for y = 0, 284 do
local r, g = imgd:getPixel( x, 284 - y )
local er, eg = eimgd:getPixel( x, 284 - y )
if math.max( r, g, er, eg ) > 0.5 then
print( "node pixel: ", x, 284 - y, r, g, er, eg )
assert( ( r > 0.5 and er > 0.5 ) or ( g > 0.5 and eg > 0.5 ), "ai marker mismatch!" )
end
end
end
print( "ai markers OK" )
end
function formats.ai:encode( data )
--set up bitmap as an array of pixels
local w, h = self.w, self.h
local size = 2 + self.w * self.h
local bitmap = { self.header:sub( 1, -3 ) }
for j = 2, size do bitmap[j] = 0 end
for i, point in ipairs( data ) do
local wx, wy = point.x, point.y
--get bitmap coordinates
local x = math.floor(self.w * (wx + 180) / 360)
--idk why exactly I need to round the value here instead of truncating it
local y = math.floor(0.5 + self.h * (wy + 100) / 200)
print( "export ai marker", i, x, y, wx, wy )
--get index into byte array
local idx = 2 + y * self.w + x
--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( 0, bitmap[j].place and 0xff or 0, bitmap[j].attack and 0xff or 0 ) --bgr24
end
end
return table.concat( bitmap )
end
function formats.travel:test()
print "testing travel nodes"
local filename = "data/earth/travel_nodes.bmp"
local img, imgd = test.load( filename )
local nodes = {}
local n = 1
for x = 0, 799 do
for y = 0, 399 do
if imgd:getPixel( x, 399 - y ) > 0 then
local long, lat = test.bitmapToWorld( x, y )
nodes[n] = {x = long, y = lat, idx = n}
print( "read:", n, long, lat, x, y )
n = n + 1
end
end
end
print( "loaded ", n, "nodes" )
local encodedString = self:encode( nodes )
test.compareData( love.filesystem.read( filename ), encodedString )
love.filesystem.write( "travel_nodes.bmp", encodedString )
print( "travel nodes OK" )
end
--Sparse bitmap with a few white pixels.
function formats.travel:encode( points )
--set up bitmap as an array of 8-bit pixels
local w, h = self.w, self.h
local size = 2 + w * h
local bitmap = { self.header }
for j = 2, size do bitmap[j] = 0 end
--write white pixels
for i, point in ipairs( points ) do
local wx, wy = point.x, point.y
-- get bitmap coordinates:
local x = 800 * ( wx + 180 ) / 360
local y = 600 + 800 * ( wy - 180 ) / 360
local offset = math.floor( y * self.w + x - 2 )
--1 := white
bitmap[offset] = 1
print( "encoded:", point.idx, wx, wy, x, y )
end
--fold into 4-bit pixel array
local nybbles = { bitmap[1] }
for j = 2, size / 2 do
local a, b = bitmap[ 2 * j - 2 ], bitmap[ 2 * j - 1 ]
nybbles[j] = string.char( 16 * a + b )
end
return table.concat( nybbles )
end
function formats.sailable:test()
print "testing sailable"
local filename = "data/earth/sailable.bmp"
local img, imgd = test.load( filename )
local encoded = self:encode( imgd )
love.filesystem.write( "sailable_out.bmp", encoded )
test.compareData( love.filesystem.read( filename ), encoded )
print "sailable OK"
end
function formats.sailable:encode( data )
local w, h = self.w, self.h
local bytes = { self.header:sub( 1, -2 ) }
local i = 2
--y coordinates are written top to bottom
for y = h - 1, 0, -1 do
for x = 0, w - 1 do
bytes[i] = assert( self.palette[ math.floor( data:getPixel(x, y) * 255 )] )
i = i + 1
end
end
--fold into 4-bit pixel array
local nybbles = { bytes[1] }
for j = 2, #bytes / 2 do
local a, b = bytes[ 2 * j ], bytes[ 2 * j + 1 ]
nybbles[j] = string.char( 16 * a + b )
end
return table.concat( nybbles )
end
function formats.africa:test()
print "testing africa"
local filename = "data/earth/africa.bmp"
local img, imgd = test.load( filename )
local encoded = self:encode( imgd )
love.filesystem.write( "africa_out.bmp", encoded )
test.compareData( love.filesystem.read( filename ), encoded )
print "africa OK"
end
function formats.africa:encode( data )
local w, h = self.w, self.h
local bytes = { self.header:sub( 1, -3 ) }
local i = 2
--y coordinates are written top to bottom
for y = h - 1, 0, -1 do
for x = 0, w - 1 do
bytes[i] = bgrChar( data:getPixel(x, y) )
i = i + 1
end
end
return table.concat( bytes )
end
function t.load( filename ) function t.load( filename )
local imgd = love.image.newImageData( filename ) local imgd = love.image.newImageData( filename )
@ -15,8 +304,16 @@ function t.load( filename )
return img, imgd return img, imgd
end end
function t.save( data, filename ) function t.save( data, format )
local w, h = data:getDimensions() return assert(formats[format]):encode( data )
end
--convenience
formats.territory = formats.africa
for fmt, tbl in pairs( formats ) do
t[fmt] = function( data ) return tbl:encode( data ) end
--TESTING
tbl:test()
end end
return t return t

View File

@ -1,32 +1,106 @@
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 end
function t.onClick( button )
function t.new( b )
b = setmetatable( b or {}, t )
b.next = t
t.prev.next = b
b.prev = t.prev
t.prev = b
return b
end end
function t.newButton( name, tooltip, icon, x, y, w, h, callback ) local drawPassOngoing = false
return setmetatable( { function t.draw( b )
name = name, if b == t then
tooltip = tooltip, drawPassOngoing = not( drawPassOngoing )
icon = icon, if not drawPassOngoing then return end
x = x, elseif b.visible then
y = y, lg.rectangle( "line", b.x, b.y, b.w, b.h, 6 )
w = w, lg.printf( b.name,
h = h, b.x + (b.icon and b.h or 0),
callback = callback }, b.y + 0.5 * ( b.h - lg.getFont():getHeight() ),
t ) b.w - (b.icon and b.h or 0),
"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 end
function t.draw( button ) function t.select( b )
t.selected = b
end end
t.__index = t function t.selectNext()
t.__call = t.newButton 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 ) setmetatable( t, t )
t.__index = t
t.__call = t.new
return t return t

View File

@ -3,7 +3,7 @@ local tfTerritory = love.math.newTransform()
local tfNodes = love.math.newTransform() local tfNodes = love.math.newTransform()
local lg = assert( love.graphics ) local lg = assert( love.graphics )
local Camera = { local Camera = {
x = 0, y = 0, x = -90, y = 45,
w = 360, h = 200, w = 360, h = 200,
zoom = 1, tf = tf, zoom = 1, tf = tf,
tfTerritory = tfTerritory, tfNodes = tfNodes } tfTerritory = tfTerritory, tfNodes = tfNodes }
@ -20,12 +20,18 @@ function Camera.GetNodeCoordinate( x, y )
return tfNodes:inverseTransformPoint( x, y ) return tfNodes:inverseTransformPoint( x, y )
end end
function Camera.Zoom( out ) function Camera.Zoom( delta )
local scale = out and 1.1 or 0.9 if Camera.zoom < 25.0 and delta > 0 or --zooming in
tf:scale( scale, scale ) Camera.zoom > 0.5 and delta < 0 then --zooming out
local x = Camera.x
local y = Camera.y delta = delta * Camera.zoom
return Camera.Set( x, y, Camera.w * scale, Camera.h * scale ) local cx, cy = Camera.x, Camera.y
return Camera.Set(
cx,
cy,
Camera.w + delta,
Camera.h * (Camera.w + delta) / Camera.w )
end
end end
function Camera.Translate( x, y ) function Camera.Translate( x, y )
@ -34,11 +40,10 @@ function Camera.Translate( x, y )
return Camera.Set( math.max(-180, math.min(360, Camera.x + x)), math.min(100, Camera.y + y), Camera.w, Camera.h) return Camera.Set( math.max(-180, math.min(360, Camera.x + x)), math.min(100, Camera.y + y), Camera.w, Camera.h)
end end
--In world coordinates: top left corner at x, y, extent of w, h. --In world coordinates: top left corner at x, y, extent of 1/w, 1/h.
function Camera.Set( x, y, 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.x, Camera.y, Camera.w, Camera.h = x, y, w, h
Camera.zoom = w / 360 Camera.zoom = w / 800
tf:reset() tf:reset()
tf:scale( w / 360, -h / 200 ) tf:scale( w / 360, -h / 200 )
tf:translate( 180 - x, -y - 100 ) tf:translate( 180 - x, -y - 100 )
@ -48,8 +53,8 @@ function Camera.Set( x, y, w, h )
tfTerritory:translate( -x * 512 / 360, y * 512 / 360 ) tfTerritory:translate( -x * 512 / 360, y * 512 / 360 )
tfNodes:reset() tfNodes:reset()
tfNodes:scale( w / 360, h / 200 ) tfNodes:scale( w / 360, -h / 200 )
tfNodes:translate( 180 - x , y + 100 ) tfNodes:translate( 180 - x , -y - 100 )
--tfNodes:translate( -x * 800 / 360, y * 400 / 200 ) --tfNodes:translate( -x * 800 / 360, y * 400 / 200 )
end end

View File

@ -23,40 +23,57 @@ function t.unlockSelection()
end end
function t.draw() function t.draw()
if cities.visible then lg.points( points ) end return lg.points( points )
end end
function t.drawSelected( r ) function t.drawSelected( )
if not cities.visible then return end
local c = t.selected local c = t.selected
if not c then return end if not c then return end
lg.circle( "fill", c.x, c.y, r ) lg.circle( "line", c.x, c.y, 1.0 )
end end
function t.drawCapitals() function t.drawCapitals()
if cities.visible then lg.points( caps ) end if cities.visible then lg.points( caps ) end
end end
function t.selectNearestCity(x, y) function t.selectNearest( cities, x, y )
if not t.selectionLocked then t.selected = cities:getClosestPoint(x, y) end 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 end
function t.load( filename ) function t.load( filename )
cities = { visible = true, active = false } print( "=== LOADING CITIES. ===" )
cities = { visible = true, active = false, filename = filename }
local n = 1 local n = 1
local idxPts = 1 local idxPts = 1
local idxCaps = 1 local idxCaps = 1
points = {}
caps = {}
for line in assert( lfs.lines( filename ), "Error: could not open cities.dat" ) do 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+)" ) local _, _, x, y, pop, capital = line:sub( 83 ):find( "(%g+)%s+(%g+)%s+(%g+)%s+(%g+)" )
if capital then --check against empty or malformed line
x, y, pop, capital = tonumber( x ), tonumber( y ), tonumber( pop ), ( tonumber( capital ) > 0) x, y, pop, capital = tonumber( x ), tonumber( y ), tonumber( pop ), ( tonumber( capital ) > 0)
local city = { local city = setmetatable({
name = line:sub( 1, 39 ):gsub("%s+$",""), name = line:sub( 1, 39 ):gsub("%s+$",""),
country = line:sub( 42, 82 ):gsub("%s+$",""), country = line:sub( 42, 82 ):gsub("%s+$",""),
x = x, y = y, pop = pop, capital = capital x = x, y = y, pop = pop, capital = capital
} }, citymt )
cities[n] = city cities[n] = city
n = n + 1 n = n + 1
@ -67,23 +84,26 @@ function t.load( filename )
caps[idxCaps], caps[idxCaps + 1] = x, y caps[idxCaps], caps[idxCaps + 1] = x, y
idxCaps = idxCaps + 2 idxCaps = idxCaps + 2
end end
else
print( "CITIES: malformed line:", line )
end
end end
--Multiple inheritance. --Multiple inheritance.
cities = locationQuery.New( cities ) cities = locationQuery.New( cities )
setmetatable( getmetatable( cities ).__index, {__index = t } ) setmetatable( getmetatable( cities ).__index, {__index = t } )
print( "LOADED", filename, n ) print( "=== CITIES LOADED:", filename, n, "===" )
return cities return cities
end end
function t.save( cities, filename ) function t.save( cities )
local str = {} local str = {}
for n, city in ipairs( cities ) do 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 end
assert( lfs.write( filename, table.concat( str, "\n" ) ) ) return assert(table.concat( str, "\n" ))
print( "Saved", filename )
end end
return t 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.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.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.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.borderless = false -- Remove all border visuals from the window (boolean)
t.window.resizable = true -- Let the window be user-resizable (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) 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.sound = false -- Enable the sound module (boolean)
t.modules.system = true -- Enable the system module (boolean) t.modules.system = true -- Enable the system module (boolean)
t.modules.thread = false -- Enable the thread 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.touch = false -- Enable the touch module (boolean)
t.modules.video = false -- Enable the video module (boolean) t.modules.video = false -- Enable the video module (boolean)
t.modules.window = true -- Enable the window module (boolean) t.modules.window = true -- Enable the window module (boolean)

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,32 +3,96 @@ local t = {}
local lfs = love.filesystem local lfs = love.filesystem
local lg = love.graphics 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 polygon:drawDirection()
local a,b,c,d = self[1], self[2], self[3], self[4]
local bx, by = (c + a) / 2, (b + d) / 2
local dx, dy = c - a, d - b
local r = math.max( math.sqrt( dy * dy + dx * dx ), 0.0001 )
dx, dy = dx / r, dy / r
lg.polygon( "fill",
a + dx, b + dy,
a - 0.4 * dy, b + 0.4 * dx,
a + 0.4 * dy, b - 0.4 * dx )
end
function t.load( filename ) function t.load( filename )
local polys = { visible = true } local polys = { visible = true, filename = filename }
local poly = {} local poly = {}
local n = 0 local n = 1
local k = 1 local k = 1
for line in assert( lfs.lines( filename ) ) do for line in assert( lfs.lines( filename ) ) do
if line == "b" then if line:find "b" then
k = 1 k = 1
n = n + 1 if #poly > 2 then n = n + 1 end
poly = {} poly = setmetatable({}, polymt) --axis-aligned bounding box
polys[n] = poly polys[n] = poly
else else
local _, _, x, y = line:find( "(%g+)%s+(%g+)" ) local _, _, x, y = line:find( "(%g+)%s+(%g+)" )
x, y = assert( tonumber( x ) ), assert( tonumber( y ) ) x, y = tonumber( x ), tonumber( y )
if x and y then
poly[k], poly[ k + 1 ] = x, y poly[k], poly[ k + 1 ] = x, y
k = k + 2 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 end
end
if not polys[n] or (#(polys[n]) < 3) then
polys[n] = nil
n = n - 1
end
print( "LOADED", filename, n ) print( "LOADED", filename, n )
return setmetatable( polys, {__index = t } ) return setmetatable( polys, {__index = t } )
end 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 = { "b" }
for i, poly in ipairs( lines ) do
str[i + 1] = table.concat( poly, " " )
end
str = table.concat( str, "\nb\n" ):gsub("(%S+) (%S+) ", "%1 %2\n")
return str
end end
function t.newPolygon( x, y ) function t.newPolygon( x, y )

View File

@ -1,17 +1,25 @@
--Shitty acceleration structure: get closest point in set. --Shitty acceleration structure: get closest point in set.
--Assumed to be in world coordinates: ( -180, 180 ) x ( -100, 100 ) --Assumed to be in world coordinates: ( -180, 180 ) x ( -100, 100 )
local t = {} local t = {}
local hashes = {}
local math = math local math = math
local hash = function( x, y, debug ) 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 return s
end end
function t.getHashes( points )
return assert( points.hashes )
end
function t.getClosestPoint( points, x, y ) function t.getClosestPoint( points, x, y )
local hashes = t.getHashes( points )
local closePoints = hashes[hash( x, y )] local closePoints = hashes[hash( x, y )]
if not closePoints then return end
if not closePoints then
return
end
local distance = math.huge local distance = math.huge
local px, py, point local px, py, point
for k, v in pairs(closePoints) do for k, v in pairs(closePoints) do
@ -29,6 +37,7 @@ function t.getClosestPoint( points, x, y )
end end
function t.Edit( points, point, x, y ) function t.Edit( points, point, x, y )
local hashes = t.getHashes( points )
local h = hashes[hash( point.x, point.y )] local h = hashes[hash( point.x, point.y )]
if h then if h then
for i, p in pairs(h) do for i, p in pairs(h) do
@ -50,13 +59,21 @@ function t.Add( points, x, y )
end end
function t.New( points ) function t.New( points )
local hashes = {}
for i = 1, #points do for i = 1, #points do
local x, y = points[i].x, points[i].y local x, y = points[i].x, points[i].y
local h = hash( x, y ) local h = hash( x, y )
hashes[h] = hashes[h] or {} hashes[h] = hashes[h] or {}
hashes[h][#hashes[h] + 1] = points[i] hashes[h][#hashes[h] + 1] = points[i]
end 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 end
return t return t

200
main.lua
View File

@ -1,115 +1,72 @@
local love = assert( love, "This tool requires LOVE: love2d.org" ) local love = assert( love, "This tool requires LOVE: love2d.org" )
--assert( require('mobdebug') ).start() --remote debugger
local map = require 'map' local map = require 'map'
local button = require 'button' local button = require 'button'
local SAVEDIRECTORY = "out/" require 'mainmenu'
local Camera = require 'camera' local Camera = require 'camera'
local wasKeyPressed
function love.load() function love.load()
love.filesystem.setIdentity( "dcearth", false )
love.keyboard.setKeyRepeat( true )
love.graphics.setNewFont( 14 )
end
local lfs = assert( love.filesystem ) function love.directorydropped( path )
lfs.setIdentity( "dcearth", false ) if map.path then
assert( lfs.createDirectory( SAVEDIRECTORY.."data/earth" )) assert( love.filesystem.unmount( map.path ) )
assert( lfs.createDirectory( SAVEDIRECTORY.."data/graphics" )) map.loaded = false
map.load() end
love.filesystem.mount( path, "" )
return map.load( path )
love.graphics.setNewFont( 12, "mono" )
end end
function love.update( dt ) function love.update( dt )
local tx, ty = 0, 0 local tx, ty = 0, 0
local moveCamera = false local moveCamera = false
if love.keyboard.isScancodeDown( "w" ) then moveCamera = true; ty = ty + 1 end if love.keyboard.isScancodeDown( "w" ) then moveCamera = true; ty = ty + dt * 150 / Camera.zoom end
if love.keyboard.isScancodeDown( "a" ) then moveCamera = true; tx = tx - 1 end if love.keyboard.isScancodeDown( "a" ) then moveCamera = true; tx = tx - dt * 150 / Camera.zoom end
if love.keyboard.isScancodeDown( "s" ) then moveCamera = true; ty = ty - 1 end if love.keyboard.isScancodeDown( "s" ) then moveCamera = true; ty = ty - dt * 150 / Camera.zoom end
if love.keyboard.isScancodeDown( "d" ) then moveCamera = true; tx = tx + 1 end if love.keyboard.isScancodeDown( "d" ) then moveCamera = true; tx = tx + dt * 150 / Camera.zoom end
if love.keyboard.isScancodeDown( "q" ) then Camera.Zoom( true ) end if love.keyboard.isScancodeDown( "q" ) then Camera.Zoom( dt * 400 ) end
if love.keyboard.isScancodeDown( "e" ) then Camera.Zoom( false ) end if love.keyboard.isScancodeDown( "e" ) then Camera.Zoom( -dt* 400 ) end
if moveCamera then Camera.Translate( tx, ty ) end if moveCamera then Camera.Translate( tx, ty ) end
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() 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" ) love.graphics.push( "all" )
map.draw() map.draw()
love.graphics.pop() 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. --Status bar.
local x, y = love.mouse.getPosition() local x, y = love.mouse.getPosition()
local wx, wy = Camera.GetWorldCoordinate( x, y ) local wx, wy = Camera.GetWorldCoordinate( x, y )
local bx, by = Camera.GetBitmapCoordinate( x, y ) local bx, by = Camera.GetBitmapCoordinate( x, y )
local h = love.graphics.getHeight() - 30 local h = love.graphics.getHeight() - 60
love.graphics.setColor( 0.2, 0.1, 0.1, 0.5 ) love.graphics.setColor( 0, 0, 0, 0.9 )
love.graphics.rectangle( "fill", 0, h, love.graphics.getWidth() / 2, 30 ) love.graphics.rectangle( "fill", 0, 0, 250, love.graphics.getHeight() )
love.graphics.setColor( 1, 1, 1, 1 ) love.graphics.setColor( 1, 1, 1, 1 )
love.graphics.rectangle( "line", 0, h, love.graphics.getWidth() / 2, 30 ) love.graphics.print(([[
love.graphics.print(("SCREEN\t%d\t%d\nWORLD \t%5.2f\t%5.2f"):format(x, y, wx, wy), 0, h) SCREEN %-12d %-12d
love.graphics.print(("BITMAP\t%5.2f\t%5.2f"):format(bx, by), 200, h ) 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. if map.selected then love.graphics.print( map.selected:formatDisplayInfo(), 0, 80 ) end
love.graphics.rectangle( "line", love.graphics.getWidth() / 2, h, love.graphics.getWidth() / 2, 30 ) if map.selectionLocked then end
if map.cities.selected then
local c = map.cities.selected
love.graphics.setColor( 0.2, 0.1, 0.1, 0.5 ) love.graphics.rectangle( "line", 0, 0 , 250, 218 )
love.graphics.rectangle( "fill", 0, 0, 150 ,100 ) love.graphics.rectangle( "line", 0, 218, 250, love.graphics.getHeight() )
love.graphics.setColor( 1, 1, 1, 1 )
love.graphics.rectangle( "line", 0, 0, 150 ,100 ) love.graphics.setColor( 1, 1, 1, 0.6 )
love.graphics.setColor( 1.2, 1.1, 1.1, 1.5 ) button:draw()
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
end end
function love.resize(w, h) function love.resize(w, h)
@ -117,54 +74,47 @@ function love.resize(w, h)
end end
function love.wheelmoved(x, y) function love.wheelmoved(x, y)
Camera.Zoom( (y > 0) and true or false ) Camera.Zoom( (y > 0) and 3 or -3 )
end end
function love.mousepressed( x, y, button, istouch, presses ) function love.mousepressed( x, y, mouseButton, istouch, presses )
local wx, wy = Camera.GetWorldCoordinate( x, y ) local wx, wy = Camera.GetWorldCoordinate( x, y )
if button == 1 then
map.cities.lockSelection() if button.selected and button.selected:contains( x, y ) then
else
map.cities.unlockSelection()
end
print( ("MOUSE\tx %f\ty %f\twx %f\twy %f"):format(x, y, wx, wy) ) print( ("MOUSE\tx %f\ty %f\twx %f\twy %f"):format(x, y, wx, wy) )
button.callback( button.selected )
return button.selected:callback()
end
end end
function love.mousemoved( x, y, dx, dy, istouch ) function love.mousemoved( x, y, dx, dy, istouch )
map.cities.selectNearestCity( Camera.GetWorldCoordinate( x, y ) ) if not map.loaded then return end
end --mouse over menu
button.selectIn( x, y )
local function ToggleVisibility( layer ) --mouse on map
if not layer then return end if map.selectionLocked then return end
local ml if map.editLayer and map.editLayer.selectNearest then
if map[layer] then ml = map[layer] end map.selected = map.editLayer:selectNearest( Camera.GetWorldCoordinate( x, y ) )
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())
end end
wasKeyPressed = true
end 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

142
mainmenu.lua Normal file
View File

@ -0,0 +1,142 @@
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
local 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 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

133
map.lua
View File

@ -2,15 +2,35 @@ local lg = love.graphics
local AI = require 'ai' local AI = require 'ai'
local Cities = require 'cities' local Cities = require 'cities'
local Lines = require 'lines' local Lines = require 'lines'
local Nodes = require 'nodes' local Nodes = require 'travelNodes'
local Bitmap = require 'bmp'
local Camera = require 'camera' local Camera = require 'camera'
local Territory = require 'territory' local Territory = require 'territory'
local map = { --flat list of editable layers for convenience
local layers = {
coastlines = false, coastlines = false,
coastlinesLow = false, coastlinesLow = false,
international = 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 = { territory = {
africa = false, africa = false,
europe = false, europe = false,
@ -19,33 +39,50 @@ local map = {
southamerica = false, southamerica = false,
southasia = false southasia = false
}, },
background = false,
coastlines = false,
coastlinesLow = false,
international = false,
travelnodes = false, travelnodes = false,
sailable = false, sailable = false,
aimarkers = false, ainodes = false,
cities = false cities = false
} }
function map.load() function map.load( path )
map.cities = Cities.load( "data/earth/cities.dat" ) map.background = lg.newImage( "/data/graphics/blur.bmp" )
map.coastlines = Lines.load( "data/earth/coastlines.dat" ) map.cities = Cities.load( "/data/earth/cities.dat" )
map.coastlinesLow = Lines.load( "data/earth/coastlines-low.dat" ) map.coastlines = Lines.load( "/data/earth/coastlines.dat" )
map.international = Lines.load( "data/earth/international.dat" ) map.coastlinesLow = Lines.load( "/data/earth/coastlines-low.dat" )
map.sailable = Territory.load( "data/earth/sailable.bmp", "sailable" ) map.international = Lines.load( "/data/earth/international.dat" )
map.travelnodes = Nodes.load( "data/earth/travel_nodes.bmp", map.sailable.isSailable ) --travel node adjacency matrix depends on sailable bitmap map.sailable = Territory.load( "/data/earth/sailable.bmp", "sailable" )
map.ainodes = AI.load( "data/earth/ai_markers.bmp" ) map.travelnodes = Nodes.load( "/data/earth/travel_nodes.bmp", map.sailable.isSailable ) --travel node adjacency matrix depends on sailable bitmap
map.ainodes = AI.load( "/data/earth/ai_markers.bmp" )
for k, v in pairs(map.territory) do for k, v in pairs(map.territory) do
map.territory[k] = Territory.load( "data/earth/"..k..".bmp", k ) 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
end end
function map.draw() function map.draw()
lg.clear( 0, 0, 0, 1 ) lg.clear( 0, 0, 0, 1 )
if not map.loaded then return end
do --territory do --territory
lg.setColor( 1,1,1,1)
lg.setLineJoin( "none" ) lg.setLineJoin( "none" )
lg.replaceTransform( Camera.tfTerritory ) lg.replaceTransform( Camera.tfTerritory )
lg.setBlendMode( "add" ) 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 for k, v in pairs(map.territory) do
if v.visible then if v.visible then
v:draw() v:draw()
@ -55,13 +92,9 @@ function map.draw()
v:drawBorder( "sea" ) v:drawBorder( "sea" )
end end
end end
if map.sailable.visible then map.sailable:draw() end if map.sailable.visible then
lg.setBlendMode( "alpha" ) map.sailable:draw()
lg.setColor( 1, 1, 1, 1 )
end
do --borders
lg.setLineJoin( "none" ) lg.setLineJoin( "none" )
lg.setLineWidth( 1 / Camera.zoom ) lg.setLineWidth( 1 / Camera.zoom )
@ -71,25 +104,45 @@ function map.draw()
lg.setLineWidth( 3 / Camera.zoom ) lg.setLineWidth( 3 / Camera.zoom )
map.sailable:drawBorder( "placeable" ) map.sailable:drawBorder( "placeable" )
end end
lg.setBlendMode( "alpha" )
lg.setColor( 1, 1, 1, 1 )
end
do --all this stuff is drawn in world coordinates, ( -180, 180 ) x ( -100, 100 ) do --all this stuff is drawn in world coordinates, ( -180, 180 ) x ( -100, 100 )
lg.replaceTransform( Camera.tf ) 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, 0.5 )
lg.line( p )
p:drawDirection()
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.setColor( 1, 0, 0, 0.5 )
lg.setPointSize( 0.5 * Camera.zoom ) lg.setPointSize( 5.0 )
map.cities.draw() map.cities.draw()
lg.setColor( 1, 1, 1.0, 0.5 ) lg.setColor( 1, 1, 0.0, 0.5 )
lg.setPointSize( 1.0 * Camera.zoom )
map.cities.drawCapitals() map.cities.drawCapitals()
lg.setColor( 1, 1, 1, 0.5 ) end
map.cities.drawSelected( 15.0 / Camera.zoom )
if map.ainodes.visible then
lg.setPointSize( 5.0 )
map.ainodes:draw() map.ainodes:draw()
end end
@ -115,23 +168,29 @@ function map.draw()
do --travel nodes do --travel nodes
lg.replaceTransform( Camera.tfNodes ) lg.replaceTransform( Camera.tfNodes )
if map.travelnodes.visible then
map.travelnodes:draw() map.travelnodes:draw()
end end
end
end end
end end
local function write( filename, string )
--
print( "Writing", string:len(), "bytes to", filename )
os.rename( filename, filename..".bak" ) --just in case :^)
local file = assert( io.open( filename, "wb+" ) )
assert( file:write( string ) )
assert( file:flush() ) --your toilet is set to stun, not kill
file:close()
end
function map.save() function map.save()
map.cities.save() for k, layer in pairs( layers ) do
map.coastlines.save() write( map.path..tostring( layer.filename ), assert( layer: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()
end end
end end
@ -139,4 +198,8 @@ function map.hover(x, y)
end end
function map.undo()
print( "=== UNDO ===" )
end
return map 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

101
nodes.lua
View File

@ -1,101 +0,0 @@
--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 lg = assert( love.graphics )
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
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
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 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}
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 } )
end
--Determine if graph has more than one connected component.
function t.isConnected( nodes )
end
function t.draw( nodes )
lg.setPointSize( 10 )
lg.setColor( 1, 1, 1, 0.5 )
lg.points( nodes.points )
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 )
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 - 300,
y = love.graphics.getHeight() / 2 - 150,
w = 600,
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 - 300,
y = love.graphics.getHeight() / 2,
w = 600,
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 = { local territory = {
visible = true, visible = true,
name = name, name = name,
filename = filename,
colour = colours[name], colour = colours[name],
border = {}, border = {},
img = img, img = img,
@ -56,10 +57,6 @@ function t.getPixel( territory, x, y )
return territory.imgd:getPixel( imgx, imgy ) return territory.imgd:getPixel( imgx, imgy )
end end
function t.toggleVisibility( territory )
end
--[[ --[[
0 0
20 -- once sailable.bmp is brighter than this, the area is traversable by ships 20 -- once sailable.bmp is brighter than this, the area is traversable by ships
@ -109,8 +106,10 @@ function t.computeBorder( territory, threshold, key )
local border = territory[key] local border = territory[key]
local n = 1 local n = 1
for x = 0, 511 do local w, h = territory.imgd:getWidth() - 1, territory.imgd:getHeight() - 1
for y = 0, 284 do w, h = math.min( 512, w ), math.min( 285, h )
for x = 0, w do
for y = 0, h do
--Bottom left, bottom right, and top right of pixel in image coordinates: --Bottom left, bottom right, and top right of pixel in image coordinates:
local blx, bly = x, y + 1 local blx, bly = x, y + 1
local brx, bry = x + 1, y + 1 local brx, bry = x + 1, y + 1
@ -143,12 +142,10 @@ function t.computeBorder( territory, threshold, key )
end end
end end
function t.drawConnections( nodes ) function t.save( territory )
local fmt = (territory.name == "sailable") and "sailable" or "territory"
end print( "saving bitmap: ", territory.name, fmt )
return bmp[fmt]( territory.imgd )
function t.save( nodes, filename )
end end
return t 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

159
travelNodes.lua Normal file
View File

@ -0,0 +1,159 @@
--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 bmp = require 'bmp'
local locationQuery = require 'locationQuery'
local lg = assert( love.graphics )
local t = setmetatable({}, {__index = locationQuery})
local isSailable
local function hasEdge( 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 = math.floor( mag )--/ 0.5 )
dx, dy = dx / mag, dy / mag
--dx, dy = 0.5 * dx, 0.5 * dy
for i = 0, n - 1 do
ix, iy = ix + dx, iy + dy
if not( isSailable( ix, -iy ) ) then return nil end
end
return true
end
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 )
local w, a = 360.0, 600.0 / 800.0
local h = 360.0 * a
x = w * ( x - 800 ) / 800 - w / 2 + 360
y = h * ( y - 600 ) / 600 + 180
return x, y
--(360 * ( 600 / 800 )) * ( y - 600 ) / 600 + 180
end
function t.load( filename, sailable )
isSailable = sailable
local img, imgd = bmp.load( filename )
local nodes = { filename = filename, visible = true, nodes = {}, points = {}, connections = {}, img = img }
t.nodes = nodes
local n = 1
for x = 0, 799 do
for y = 0, 399 do
if imgd:getPixel( x, 399 - y ) > 0 then
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
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] = hasEdge( srcNode, destNode )
end
nodes.connections[i] = adjacent
end
for i, node in ipairs( nodes.nodes ) do
for j, destNode in ipairs( nodes.nodes ) do
nodes.connections[i][j] = nodes.connections[i][j] or nodes.connections[j][i]
end
end
nodes.connected = t.isConnected( nodes )
nodes.nodes = locationQuery.New( nodes.nodes )
setmetatable( nodes, {__index = t} )
return nodes
end
local function updateAdjacency( connections, i )
for j in pairs( connections[i] ) do
local edge = hasEdge( i, j )
connections[j][i] = edge
connections[i][j] = edge
end
end
local function dfs( idx, adj, unvisited )
if not unvisited[idx] then return end
print( "visiting node", idx )
unvisited[ idx ] = nil
for i in pairs( adj[idx] ) do dfs( i, adj, unvisited ) end
end
--Determine if graph has more than one connected component.
function t.isConnected( nodes )
local adj = nodes.connections
local unvisited = {}
for i in ipairs( nodes.nodes ) do
unvisited[i] = true
end
print( "DEPTH FIRST SEARCH:", "total nodes", #unvisited)
dfs( 1, adj, unvisited )
for k in pairs( unvisited ) do
print( "DEPTH FIRST SEARCH:", "unvisited", k)
end
return not(next(unvisited)) --empty if a graph is connected
end
function t.draw( nodes )
lg.setPointSize( 8 )
lg.setColor( 1, 1, 1, 0.7 )
lg.points( nodes.points )
return t.drawConnections( nodes )
end
function t.drawConnections( nodes )
if nodes.connected then
lg.setColor( 1, 1, 1, 0.4 )
else
lg.setColor( 1, 0, 0, 0.7 )
end
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.save( nodes )
return bmp.travel( nodes.nodes )
end
return t