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.
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
@ -43,7 +62,13 @@ function t.load( filename )
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
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.ai( nodes.all )
end
return t

309
bmp.lua
View File

@ -1,11 +1,300 @@
--Load and save the bmp formats used by DEFCON.
local t = {}
local assert = assert
local print = print
local error = error
local table = table
local math = math
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 )
local imgd = love.image.newImageData( filename )
@ -15,8 +304,16 @@ function t.load( filename )
return img, imgd
end
function t.save( data, filename )
local w, h = data:getDimensions()
function t.save( data, format )
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
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
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
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 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.icon and b.h or 0),
b.y + 0.5 * ( b.h - lg.getFont():getHeight() ),
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
function t.draw( button )
function t.select( b )
t.selected = b
end
t.__index = t
t.__call = t.newButton
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,18 @@ 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 )
if Camera.zoom < 25.0 and delta > 0 or --zooming in
Camera.zoom > 0.5 and delta < 0 then --zooming out
delta = delta * Camera.zoom
local cx, cy = Camera.x, Camera.y
return Camera.Set(
cx,
cy,
Camera.w + delta,
Camera.h * (Camera.w + delta) / Camera.w )
end
end
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)
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 )
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 +53,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,49 +23,69 @@ 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 )
cities = { visible = true, active = false }
print( "=== LOADING CITIES. ===" )
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
@ -73,17 +93,17 @@ function t.load( filename )
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,32 +3,96 @@ 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 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 )
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 = { "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
function t.newPolygon( x, y )

View File

@ -1,17 +1,25 @@
--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
@ -29,6 +37,7 @@ function t.getClosestPoint( points, x, y )
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
@ -50,13 +59,21 @@ 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

200
main.lua
View File

@ -1,115 +1,72 @@
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 + dt * 150 / Camera.zoom 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 - dt * 150 / Camera.zoom end
if love.keyboard.isScancodeDown( "d" ) then moveCamera = true; tx = tx + dt * 150 / Camera.zoom end
if love.keyboard.isScancodeDown( "q" ) then Camera.Zoom( dt * 400 ) end
if love.keyboard.isScancodeDown( "e" ) then Camera.Zoom( -dt* 400 ) 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, 0, 0, 0.9 )
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.rectangle( "line", 0, 0 , 250, 218 )
love.graphics.rectangle( "line", 0, 218, 250, love.graphics.getHeight() )
love.graphics.setColor( 1, 1, 1, 0.6 )
button:draw()
end
function love.resize(w, h)
@ -117,54 +74,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 3 or -3 )
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

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

149
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,33 +39,50 @@ 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()
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" )
map.international = Lines.load( "data/earth/international.dat" )
map.sailable = Territory.load( "data/earth/sailable.bmp", "sailable" )
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" )
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" )
map.international = Lines.load( "/data/earth/international.dat" )
map.sailable = Territory.load( "/data/earth/sailable.bmp", "sailable" )
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
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
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,57 @@ 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, 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.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
@ -115,23 +168,29 @@ function map.draw()
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( "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()
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
write( map.path..tostring( layer.filename ), assert( layer:save() ) )
end
end
@ -139,4 +198,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

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 = {
visible = true,
name = name,
filename = filename,
colour = colours[name],
border = {},
img = img,
@ -56,10 +57,6 @@ function t.getPixel( territory, x, y )
return territory.imgd:getPixel( imgx, imgy )
end
function t.toggleVisibility( territory )
end
--[[
0
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 n = 1
for x = 0, 511 do
for y = 0, 284 do
local w, h = territory.imgd:getWidth() - 1, territory.imgd:getHeight() - 1
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:
local blx, bly = x, y + 1
local brx, bry = x + 1, y + 1
@ -143,12 +142,10 @@ function t.computeBorder( territory, threshold, key )
end
end
function t.drawConnections( nodes )
end
function t.save( nodes, filename )
function t.save( territory )
local fmt = (territory.name == "sailable") and "sailable" or "territory"
print( "saving bitmap: ", territory.name, fmt )
return bmp[fmt]( territory.imgd )
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

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