Compare commits

..

5 Commits
v0.1.1 ... main

52 changed files with 1290 additions and 331 deletions

View File

@ -12,7 +12,7 @@ function love.conf(t)
t.window.title = "dcEarth" -- The window title (string) t.window.title = "dcEarth" -- The window title (string)
t.window.icon = "icons/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 = 1024 -- The window width (number)
t.window.height = 640 -- 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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 428 KiB

BIN
icons/check.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
icons/city-delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

BIN
icons/city-move.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
icons/city-new.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
icons/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
icons/layer-ainodes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
icons/layer-cities.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

BIN
icons/layer-coastlines.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

BIN
icons/layer-sailable.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

BIN
icons/layer-travelnodes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

BIN
icons/load.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
icons/node-attack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
icons/node-place.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -17,14 +17,7 @@ local exists, mkdir, PATH_SEPARATOR
if ffi.os == "Windows" then if ffi.os == "Windows" then
ffi.cdef[[ ffi.cdef[[
bool CreateDirectoryA(const char *path, void *lpSecurityAttributes); bool CreateDirectoryA(const char *path, void *lpSecurityAttributes);
int _access(const char *path, int mode);
]] ]]
function exists(path)
assert(type(path) == "string", "path isn't a string")
local result = C._access(path, 0) -- Check existence
return result == 0
end
function mkdir(path, _) function mkdir(path, _)
assert(type(path) == "string", "path isn't a string") assert(type(path) == "string", "path isn't a string")
if not C.CreateDirectoryA(path, nil) then if not C.CreateDirectoryA(path, nil) then
@ -36,13 +29,7 @@ if ffi.os == "Windows" then
elseif ffi.os == "Linux" or ffi.os == "OSX" then elseif ffi.os == "Linux" or ffi.os == "OSX" then
ffi.cdef[[ ffi.cdef[[
int mkdir(const char *path, int mode); int mkdir(const char *path, int mode);
int access(const char *path, int amode);
]] ]]
function exists(path)
assert(type(path) == "string", "path isn't a string")
local result = C.access(path, 0) -- Check existence
return result == 0
end
function mkdir(path, mode) function mkdir(path, mode)
assert(type(path) == "string", "path isn't a string") assert(type(path) == "string", "path isn't a string")
local mode = tonumber(mode or "755", 8) local mode = tonumber(mode or "755", 8)
@ -94,6 +81,18 @@ local function mkdirs(path)
end end
end end
--- Check if a file or directory exists in this path
function exists(file)
local ok, err, code = os.rename(file, file)
if not ok then
if code == 13 then
-- Permission denied, but it exists
return true
end
end
return ok, err
end
return { return {
exists = exists, exists = exists,
join = join, join = join,

View File

@ -1,9 +1,9 @@
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 --assert( require('mobdebug') ).start() --remote debugger
local map = require 'map' local map = require 'map.map'
local button = require 'button' local button = require 'ui.button'
require 'mainmenu' local mainmenu = require 'ui.menu.mainmenu'
local Camera = require 'camera' local Camera = require 'ui.camera'
function love.load() function love.load()
love.filesystem.setIdentity( "dcearth", false ) love.filesystem.setIdentity( "dcearth", false )
@ -20,6 +20,10 @@ function love.directorydropped( path )
return map.load( path ) return map.load( path )
end end
function love.filedropped( path )
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
@ -44,29 +48,7 @@ function love.draw()
map.draw() map.draw()
love.graphics.pop() love.graphics.pop()
--Status bar. mainmenu.draw()
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() - 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.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)
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 end
function love.resize(w, h) function love.resize(w, h)
@ -80,31 +62,29 @@ end
function love.mousepressed( x, y, mouseButton, 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.selected and button.selected:contains( x, y ) then return button.mousepressed( x, y )
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 )
if not map.loaded then return end if not map.loaded then return end
--mouse over menu --mouse over menu
button.selectIn( x, y ) if y < mainmenu.menuHeight then
button.selectIn( x, y )
--mouse on map --mouse on map
if map.selectionLocked then return end else
if map.editLayer and map.editLayer.selectNearest then if map.selectionLocked then return end
map.selected = map.editLayer:selectNearest( Camera.GetWorldCoordinate( x, y ) ) if map.editLayer and map.editLayer.selectNearest then
map.selected = map.editLayer:selectNearest( Camera.GetWorldCoordinate( x, y ) )
end
end end
end end
function love.keypressed(key, code, isRepeat) function love.keypressed(key, code, isRepeat)
if code == "left" then return button.selectPrev() end if code == "up" then return button.selectPrev() end
if code == "right" then return button.selectNext() end if code == "down" then return button.selectNext() end
if code == "down" then return button.selectNextInGroup() end if code == "right" then return button.selectNextInGroup() end
if code == "up" then return button.selectPrevInGroup() end if code == "left" then return button.selectPrevInGroup() end
if code == "return" then return button.selected:callback() end if code == "return" then return button.selected:callback() end
if key == "c" then if key == "c" then
@ -112,9 +92,6 @@ function love.keypressed(key, code, isRepeat)
end end
end end
function love.textinput()
do
end end

View File

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

View File

@ -1,7 +1,7 @@
--Manage the AI nodes used by DEFCON. --Manage the AI nodes used by DEFCON.
local bmp = require 'bmp' local bmp = require 'map.bmp'
local lg = assert( love.graphics ) local lg = assert( love.graphics )
local locationQuery = require 'locationQuery' local locationQuery = require 'map.locationQuery'
local t = setmetatable( {}, {__index = locationQuery } ) local t = setmetatable( {}, {__index = locationQuery } )
local print = print local print = print
@ -17,6 +17,14 @@ function aiNode:formatDisplayInfo()
]]):format( self.idx, self.x, self.y, tostring(self.attacking) ) ]]):format( self.idx, self.x, self.y, tostring(self.attacking) )
end end
function aiNode:add()
end
function aiNode:moveTo( x, y )
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 = {
@ -82,4 +90,8 @@ function t.save( nodes )
return bmp.ai( nodes.all ) return bmp.ai( nodes.all )
end end
function t.newNode( isAttacking )
end
return t return t

113
map/blur.lua Normal file
View File

@ -0,0 +1,113 @@
local love = assert( love )
local lg = love.graphics
local lt = love.timer
local coroutine = assert( coroutine )
local bmp = require 'map.bmp'
local scale = 1
local w, h = scale * 512, scale * 285
local dilateShader = require 'shaders.dilate'
local finalCanvas = lg.newCanvas( w, h )
local canvas = lg.newCanvas( w, h )
local identityTransform = love.math.newTransform()
local tiles = {}
do
local sideLength = 4
local i = 1
for x = 1, sideLength do
for y = 1, sideLength do
local width = math.floor( w / sideLength )
local height = math.floor( h / sideLength )
tiles[i] = function() return
(x - 1) * width, (y - 1) * height,
width, height
end
i = i + 1
end
end
end
--the coroutine library doesn't reset all the global graphics state for us
--so this yield function does
local function y( s, currentTile )
lg.pop( "all" )
coroutine.yield( s )
lg.push( "all" )
lg.setCanvas( canvas )
lg.setShader( dilateShader )
lg.setScissor( currentTile() )
lg.setBlendMode( "add" )
end
local function renderRadar( map, filename )
local statusString = "%s: rendering %s radius\n %s\n %d/%d"
--we need this first push to the stack to keep y() simple
lg.push( "all" )
y( filename, tiles[1] )
--then we clear the whole canvas to opaque black, pause, and select the first tile
lg.setScissor()
lg.clear( 0, 0, 0, 1 )
dilateShader:send( "sailable", map.sailable.img )
for tile = 1, #tiles do
--dilate placeable land area by 30 degrees (radar radius)
dilateShader:send( "radius", math.floor( 512 * 30 / 360 ) )
for name, terr in pairs( map.territory ) do
y( statusString:format( filename, "radar", name, tile, #tiles ), tiles[tile] )
lg.setColor(
0.9 + terr.colour[1],
0.9 + terr.colour[2],
0.9 + terr.colour[3],
1 / 6 )
lg.draw( terr.img, 0, 0, 0, scale, scale )
end
--dilate placeable land area by 45 degrees (sub launch radius)
dilateShader:send( "radius", math.floor( 512 * 45 / 360 ) )
for name, terr in pairs( map.territory ) do
y( statusString:format( filename, "sub", name, tile, #tiles ), tiles[tile] )
lg.setColor(
0.9 + terr.colour[1],
0.9 + terr.colour[2],
0.9 + terr.colour[3],
1 / 12 )
lg.draw( terr.img, 0, 0, 0, scale, scale )
end
end
lg.setCanvas( finalCanvas )
lg.setScissor()
lg.setShader()
lg.clear( 0, 0, 0, 1 )
lg.setColor( 1, 1, 1, 1 )
lg.draw( canvas )
lg.pop( "all" )
coroutine.yield( ("%s: encoding image data"):format( filename ))
return bmp.blur( finalCanvas:newImageData() )
end
local t = {}
function t:save( )
return renderRadar( self.map, self.filename )
end
function t.load( filename, map )
t.filename = filename
t.map = map
return t
end
function t.draw()
lg.setScissor()
return lg.draw( canvas )
end
return t

View File

@ -16,6 +16,7 @@ function test.load( filename )
print( "LOADING BITMAP: ", filename, imgd:getSize(), imgd:getFormat(), imgd:getDimensions() ) print( "LOADING BITMAP: ", filename, imgd:getSize(), imgd:getFormat(), imgd:getDimensions() )
local img = love.graphics.newImage( imgd ) local img = love.graphics.newImage( imgd )
img:setFilter( "nearest", "nearest" ) img:setFilter( "nearest", "nearest" )
img:setWrap( "repeat", "clampzero" )
return img, imgd return img, imgd
end end
@ -103,6 +104,16 @@ local formats = {
w = 512, w = 512,
h = 285, h = 285,
}, },
["blur"] = {
header = getHeader( "data/graphics/blur.bmp" ),
w = 512,
h = 285,
},
["screenshot"] = {
header = getHeader( "screenshot.bmp" ),
w = 256,
h = 128,
},
} }
function formats.ai:test( ) function formats.ai:test( )
@ -244,40 +255,63 @@ function formats.sailable:test()
print "sailable OK" print "sailable OK"
end 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 do --sailable
for y = h - 1, 0, -1 do
for x = 0, w - 1 do local reversePalette = {}
bytes[i] = assert( self.palette[ math.floor( data:getPixel(x, y) * 255 )] ) for eight, four in pairs( formats.sailable.palette ) do
i = i + 1 reversePalette[ four ] = eight
end end
--take the red channel in float [0, 1] format
--expand it to a byte, then quantize it to 4 bits
--according to the sailable palette
local function sailableQuantize( r )
if r == 0 then return 0 end
if r >= 1 then return 15 end
r = math.floor( r * 255 )
for four = 1, #reversePalette do
if reversePalette[ four ] > r then return four end
end
error( "Could not quantize sailable.bmp!" )
end end
--fold into 4-bit pixel array function formats.sailable:encode( data )
local nybbles = { bytes[1] } local w, h = self.w, self.h
for j = 2, #bytes / 2 do local bytes = { self.header:sub( 1, -2 ) }
local a, b = bytes[ 2 * j ], bytes[ 2 * j + 1 ] local i = 2
nybbles[j] = string.char( 16 * a + b )
--y coordinates are written top to bottom
for y = h - 1, 0, -1 do
for x = 0, w - 1 do
bytes[i] = sailableQuantize( data:getPixel(x, y) )
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 end
return table.concat( nybbles ) 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
end 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 ) function formats.africa:encode( data )
local w, h = self.w, self.h local w, h = self.w, self.h
local bytes = { self.header:sub( 1, -3 ) } local bytes = { self.header:sub( 1, -3 ) }
@ -293,11 +327,24 @@ function formats.africa:encode( data )
return table.concat( bytes ) return table.concat( bytes )
end end
function formats.screenshot:encode( data )
return formats.territory.encode( self, data )
end
--this one was 8-bit grayscale in the original game
--but we're going to increase the bit depth to 24 because
--the game can render colours in blur.bmp just fine
--and we want to render tinted radar ranges into that file
function formats.blur:encode( data )
return formats.territory.encode( self, data )
end
function t.load( filename ) function t.load( filename )
local imgd = love.image.newImageData( filename ) local imgd = love.image.newImageData( filename )
print( "LOADING BITMAP: ", filename, imgd:getSize(), imgd:getFormat(), imgd:getDimensions() ) print( "LOADING BITMAP: ", filename, imgd:getSize(), imgd:getFormat(), imgd:getDimensions() )
local img = love.graphics.newImage( imgd ) local img = love.graphics.newImage( imgd )
img:setFilter( "nearest", "nearest" ) img:setFilter( "nearest", "nearest" )
img:setWrap( "repeat", "clampzero" )
return img, imgd return img, imgd
end end

View File

@ -6,13 +6,14 @@ local table = table
local tonumber = tonumber local tonumber = tonumber
local lfs = love.filesystem local lfs = love.filesystem
local lg = love.graphics local lg = love.graphics
local locationQuery = require 'locationQuery' local locationQuery = require 'map.locationQuery'
local cities local cities
local points = {} local points = {}
local caps = {} local caps = {}
t.selected = nil t.selected = nil
t.selectionLocked = false t.selectionLocked = false
local invisible = 10000 --sentinel value outside the draw rectangle
function t.lockSelection() function t.lockSelection()
t.selectionLocked = true t.selectionLocked = true
@ -53,8 +54,67 @@ function city:formatDisplayInfo()
CAPITAL: %s]]):format( self.name, self.country, self.x, self.y, self.pop, tostring(self.capital) ) CAPITAL: %s]]):format( self.name, self.country, self.x, self.y, self.pop, tostring(self.capital) )
end end
function city:delete()
print( "deleting city:", self.name )
self.deleted = true
if self.capital then
caps[ self.caps ] = invisible
caps[ self.caps + 1] = invisible
end
points[ self.points ] = invisible
points[ self.points + 1] = invisible
end
function city:add()
local n = #cities + 1
cities[ n ] = self
self.n = n
local idxPoints = #points + 1
self.points = idxPoints
points[ idxPoints ], points[ idxPoints + 1 ] = self.x, self.y
end
function city:moveTo(x, y)
self.x, self.y = x, y
if self.points then
points[ self.points ] = x
points[ self.points + 1 ] = y
end
if self.capital then
caps[ self.caps ] = x
caps[ self.caps + 1 ] = y
end
end
function city:toggleCapital()
if self.capital then
self.capital = false
caps[ self.caps ] = invisible
caps[ self.caps + 1 ] = invisible
else
self.capital = true
local idx = #caps + 1
caps[ idx ] = self.x
caps[ idx + 1 ] = self.y
self.caps = idx
end
end
function t.newCity( tbl )
return setmetatable({
name = "",
country = "",
x = 0,
y = 0,
pop = 0,
capital = false,
}, citymt )
end
function t.load( filename ) function t.load( filename )
print( "=== LOADING CITIES. ===" ) print( "=== LOADING CITIES. ===" )
cities = { visible = true, active = false, filename = filename } cities = { visible = true, active = false, filename = filename }
@ -70,10 +130,11 @@ function t.load( filename )
if capital then --check against empty or malformed line 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 = setmetatable({ 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 ) n = n, points = idxPts, caps = capital and idxCaps
}, citymt )
cities[n] = city cities[n] = city
n = n + 1 n = n + 1
@ -99,9 +160,13 @@ end
function t.save( cities ) function t.save( cities )
local str = {} local str = {}
for n, city in ipairs( cities ) do local i = 1
str[n] = ("%-41s%-41s%-14f%-14f%-19d %d"):format( for _, city in ipairs( cities ) do
city.name, city.country, city.x, city.y, city.pop, city.capital and 1 or 0 ) if not city.deleted then
str[i] = ("%-41s%-41s%-14f%-14f%-19d %d"):format(
city.name, city.country, city.x, city.y, city.pop, city.capital and 1 or 0 )
i = i + 1
end
end end
return assert(table.concat( str, "\n" )) return assert(table.concat( str, "\n" ))
end end

View File

@ -12,7 +12,7 @@ function polygon:formatDisplayInfo()
y: %f y: %f
X: %f X: %f
Y: %f Y: %f
N: %d]]):format( self.x, self.y, self.X, self.Y, #self ) Length: %d]]):format( self.x, self.y, self.X, self.Y, #self / 4 )
end end
function polygon:drawDirection() function polygon:drawDirection()
@ -73,8 +73,9 @@ function t.selectNearest( lines, wx, wy )
poly.X + 5 > wx and poly.X + 5 > wx and
poly.y - 5 < wy and poly.y - 5 < wy and
poly.Y + 5 > wy then poly.Y + 5 > wy then
for k = 1, #poly, 2 do for k = 1, #poly - 3, 4 do
local x, y = poly[k], poly[k + 1] --find the midpoint of each line segment
local x, y = 0.5 * (poly[k] + poly[k + 2]), 0.5 * (poly[k + 1] + poly[k + 3])
local r = ( x - wx ) * ( x - wx ) + ( y - wy ) * ( y - wy ) local r = ( x - wx ) * ( x - wx ) + ( y - wy ) * ( y - wy )
if r < d then if r < d then
d = r d = r
@ -87,11 +88,14 @@ function t.selectNearest( lines, wx, wy )
end end
function t.save( lines ) function t.save( lines )
local str = { "b" } local str = { "b" } --initial B
for i, poly in ipairs( lines ) do for i, poly in ipairs( lines ) do
str[i + 1] = table.concat( poly, " " ) str[i + 1] = table.concat( poly, " " )
end end
str = table.concat( str, "\nb\n" ):gsub("(%S+) (%S+) ", "%1 %2\n") --concatenate into one big string, one line per polygon
--then put each pair of numbers on their own line (without concatenating)
--we use CRLF line breaks here because that's what's in the original game files
str = table.concat( str, "\13\nb\13\n" ):gsub("(%S+) (%S+) ", "%1 %2\13\n")
return str return str
end end

View File

@ -1,13 +1,15 @@
local love = assert( love ) local love = assert( love )
local io = io local io = io
local coroutine = coroutine
local mkdir = assert( require 'lib.mkdir' ) local mkdir = assert( require 'lib.mkdir' )
local lg = love.graphics local lg = love.graphics
local AI = require 'ai' local AI = require 'map.ai'
local Cities = require 'cities' local Cities = require 'map.cities'
local Lines = require 'lines' local Lines = require 'map.lines'
local Nodes = require 'travelNodes' local Nodes = require 'map.travelNodes'
local Camera = require 'camera' local Camera = require 'ui.camera'
local Territory = require 'territory' local Territory = require 'map.territory'
local Blur = require 'map.blur'
--flat list of editable layers for convenience --flat list of editable layers for convenience
local layers = { local layers = {
@ -24,6 +26,7 @@ local layers = {
sailable = false, sailable = false,
ainodes = false, ainodes = false,
cities = false, cities = false,
blur = false,
} }
local map = { local map = {
@ -33,7 +36,7 @@ local map = {
selected = false, selected = false,
selectionLocked = false, selectionLocked = false,
editLayer = false, editLayer = false,
territory = { territory = {
africa = false, africa = false,
europe = false, europe = false,
@ -42,7 +45,7 @@ local map = {
southamerica = false, southamerica = false,
southasia = false southasia = false
}, },
background = false, background = false,
coastlines = false, coastlines = false,
coastlinesLow = false, coastlinesLow = false,
@ -50,9 +53,28 @@ local map = {
travelnodes = false, travelnodes = false,
sailable = false, sailable = false,
ainodes = false, ainodes = false,
cities = false cities = false,
blur = false,
} }
function map.reloadLayer( path )
--Shouldn't call this before the map loads, but just in case
if not map.loaded then return end
for name, layer in pairs( layers ) do
if layer.filename and
(layer.filename:gsub( ".+[\\/]", "") == path.filename:gsub( ".+[\\/]", "" )) then
local newLayer = layer:load( path )
newLayer.filename = layer.filename --we don't store the full path in there
map[ name ] = newLayer
layers[ name ] = newLayer
return layer.filename
end
end
return
end
function map.load( path ) function map.load( path )
map.background = lg.newImage( "/data/graphics/blur.bmp" ) map.background = lg.newImage( "/data/graphics/blur.bmp" )
map.cities = Cities.load( "/data/earth/cities.dat" ) map.cities = Cities.load( "/data/earth/cities.dat" )
@ -62,12 +84,13 @@ function map.load( path )
map.sailable = Territory.load( "/data/earth/sailable.bmp", "sailable" ) 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.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" ) map.ainodes = AI.load( "/data/earth/ai_markers.bmp" )
map.blur = Blur.load( "/data/graphics/blur.bmp", map )
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 end
map.loaded = true map.loaded = true
map.path = path map.path = path
--update references --update references
for k, v in pairs( layers ) do for k, v in pairs( layers ) do
layers[k] = map[k] or map.territory[k] layers[k] = map[k] or map.territory[k]
@ -75,6 +98,7 @@ function map.load( path )
end end
function map.draw() function map.draw()
love.graphics.setScissor( 0, 200, love.graphics.getWidth(), love.graphics.getHeight() - 200 )
lg.clear( 0, 0, 0, 1 ) lg.clear( 0, 0, 0, 1 )
if not map.loaded then return end if not map.loaded then return end
@ -84,31 +108,31 @@ function map.draw()
lg.setBlendMode( "add" ) lg.setBlendMode( "add" )
lg.setColor( 1, 1, 1, 0.2 ) lg.setColor( 1, 1, 1, 0.2 )
lg.draw( map.background ) --lg.draw( map.background )
lg.setColor( 1, 1, 1, 0.5 ) lg.setColor( 1, 1, 1, 0.5 )
local sh = require 'shaders.sailable'
lg.setShader( sh )
sh:send( "lowBorder", 60 )
sh:send( "highBorder", 130 )
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()
lg.setLineWidth( 1 / Camera.zoom ) --[[lg.setLineWidth( 1 / Camera.zoom )
v:drawBorder( "land" ) v:drawBorder( "land" )
lg.setLineWidth( 3 / Camera.zoom ) lg.setLineWidth( 3 / Camera.zoom )
v:drawBorder( "sea" ) v:drawBorder( "sea" )]]
end end
end end
if map.sailable.visible then if map.sailable.visible then
sh:send( "lowBorder", 20 )
sh:send( "highBorder", 60 )
lg.setShader( require 'shaders.sailable' )
map.sailable:draw() 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 end
lg.setShader()
lg.setBlendMode( "alpha" ) lg.setBlendMode( "alpha" )
lg.setColor( 1, 1, 1, 1 ) lg.setColor( 1, 1, 1, 1 )
end end
@ -181,26 +205,42 @@ function map.draw()
end end
local function write( filename, string ) do
print( "Writing", string:len(), "bytes to", filename ) local function write( filename, string )
local file = assert( io.open( filename, "wb" ) ) local file = assert( io.open( filename, "wb" ) )
assert( file:write( string ) ) assert( file:write( string ) )
assert( file:flush() ) --your toilet is set to stun, not kill assert( file:flush() ) --your toilet is set to stun, not kill
file:close() file:close()
end
function map.save()
--should be cross platform-ish
for _, folder in ipairs{ "/data/", "/data/earth/", "/data/graphics/" } do
assert( mkdir.exists( map.path ) )
local path = map.path..folder
if not mkdir.exists( path ) then mkdir.mkdir( path ) end
end end
--OK back to normal local function save()
for k, layer in pairs( layers ) do --should be cross platform-ish.
write( map.path..tostring( layer.filename ), assert( layer:save() ) ) --race condition, unfortunately.
--maybe we should do this on part on load, then keep a lockfile open in each of these folders
for _, folder in ipairs{ "/data/", "/data/earth/", "/data/graphics/" } do
coroutine.yield( "Creating folder ".. folder )
assert( mkdir.exists( map.path ), map.path )
local path = map.path..folder
if not mkdir.exists( path ) then mkdir.mkdir( path ) end
end
local files = {}
--Write everything to strings first, in case there are errors we don't want to half-write the map
for k, layer in pairs( layers ) do
coroutine.yield( "Exporting layer ".. k )
files[ map.path..tostring( layer.filename ) ] = assert( layer:save() )
end
for filename, str in pairs( files ) do
coroutine.yield( "Writing ".. filename )
write( filename, str )
end
coroutine.yield() --yield nothing to indicate we're done
return save() --save again
end end
map.save = coroutine.wrap( save )
end end
function map.hover(x, y) function map.hover(x, y)
@ -211,4 +251,17 @@ function map.undo()
print( "=== UNDO ===" ) print( "=== UNDO ===" )
end end
function map.setEditLayer( layerName )
if not layerName then
map.editLayer = nil
for name, layer in pairs( layers ) do layer.visible = true end
else
for name, layer in pairs( layers ) do
layer.visible = false
end
map.editLayer = layers[ layerName ]
if map.editLayer then map.editLayer.visible = true end
end
end
return map return map

19
map/saveoptions.lua Normal file
View File

@ -0,0 +1,19 @@
local t = {}
t.options = {
Name = "New DEFCON Mod",
Version = "0.1",
Author = "DeFacto",
Website = "https://wan-may.art/dev/",
Comment = "DEFCON map made with dcEarth",
radarExport = true,
screenGenerator = true,
highDetail = true,
}
--parse and load metadata from mod.txt
function t.load( filename )
end
return t

View File

@ -1,5 +1,5 @@
local t = {} local t = {}
local bmp = require 'bmp' local bmp = require 'map.bmp'
local lg = assert( love.graphics ) local lg = assert( love.graphics )
local colours = { local colours = {
@ -144,7 +144,6 @@ end
function t.save( territory ) function t.save( territory )
local fmt = (territory.name == "sailable") and "sailable" or "territory" local fmt = (territory.name == "sailable") and "sailable" or "territory"
print( "saving bitmap: ", territory.name, fmt )
return bmp[fmt]( territory.imgd ) return bmp[fmt]( territory.imgd )
end end

View File

@ -2,8 +2,8 @@
--This is important for a mapping tool because the DEFCON client will not load a map unless --This is important for a mapping tool because the DEFCON client will not load a map unless
--the pathfinding nodes form a connected graph. --the pathfinding nodes form a connected graph.
local bmp = require 'bmp' local bmp = require 'map.bmp'
local locationQuery = require 'locationQuery' local locationQuery = require 'map.locationQuery'
local lg = assert( love.graphics ) local lg = assert( love.graphics )
@ -57,7 +57,7 @@ end
function t.load( filename, sailable ) function t.load( filename, sailable )
isSailable = sailable isSailable = sailable or isSailable
local img, imgd = bmp.load( filename ) local img, imgd = bmp.load( filename )
local nodes = { filename = filename, visible = true, nodes = {}, points = {}, connections = {}, img = img } local nodes = { filename = filename, visible = true, nodes = {}, points = {}, connections = {}, img = img }
t.nodes = nodes t.nodes = nodes

5
mod.txt Normal file
View File

@ -0,0 +1,5 @@
Name New DEFCON Mod
Version 0.1
Author DeFacto
Website https://wan-may.art/dev/
Comment DEFCON map made with dcEarth

View File

@ -1 +1,5 @@
Map editor for DEFCON, the strategy game, written in LOVE2D, the engine. Map editor for DEFCON, the strategy game, written in LOVE2D, the engine.
Currently does not do anything besides read the files and write them back out.
Need to make a more structured UI, rewriting menu.lua and button.lua for this purpose.

BIN
screenshot.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

46
shaders/dilate.lua Normal file
View File

@ -0,0 +1,46 @@
return love.graphics.newShader[[
#pragma language glsl3
uniform int radius;
uniform Image sailable;
bool own( float x )
{
if( x * 255.0 > 130.0 ){
return true;
}
else {
return false;
}
}
bool land( float x )
{
if( x * 255.0 <= 60.0 ){
return true;
}
else {
return false;
}
}
bool placeable( Image tex, vec2 uv )
{
return own(Texel(tex, uv).r) && land(Texel(sailable, uv).r);
}
vec4 effect( vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords )
{
for(int i = -radius; i <= radius; ++i) {
for(int j = -radius; j <= radius; ++j) {
vec2 offset = vec2( ivec2( i, j ) ) / vec2( textureSize(tex, 0) );
int r = ((i * i) + (j * j));
if ( (r < (radius * radius) )
&& placeable( tex, texture_coords + offset )){
return color;
}
}
}
return vec4( 0.0, 0.0, 0.0, 0.0 );
}
]]

36
shaders/sailable.lua Normal file
View File

@ -0,0 +1,36 @@
return love.graphics.newShader[[
#pragma language glsl3
uniform float highBorder;
uniform float lowBorder;
#define p(x) ((x) * 255.0 > highBorder)
#define t(x) ((x) * 255.0 > lowBorder)
vec4 effect( vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords )
{
vec2 s = vec2( 1.0, 1.0 ) / vec2( textureSize(tex, 0) );
float c = Texel( tex, texture_coords ).r;
vec4 g = vec4(
Texel( tex, texture_coords + s * vec2(1.0, 0.0)).r,
Texel( tex, texture_coords + s * vec2(0.0, 1.0)).r,
Texel( tex, texture_coords + s * vec2(-1.0, 0.0)).r,
Texel( tex, texture_coords + s * vec2(0.0, -1.0)).r
);
bvec4 place = bvec4( p(g.r), p(g.g), p(g.b), p(g.a) );
bvec4 traverse = bvec4( t(g.r), t(g.g), t(g.b), t(g.a) );
float a;
if ( p(c) ){
if( all( place ) ) { a = 1.0; }
else { a = 2.0; }
}
else if( t(c) ){
if( all( traverse ) ) { a = 0.5; }
else { a = 0.75; }
}
else{
a = 0.0;
}
return vec4( color.rgb, a * color.a );
}
]]

54
shaders/territory.lua Normal file
View File

@ -0,0 +1,54 @@
--[[
0
20 -- once sailable.bmp is brighter than this, the area is traversable by ships
60 -- once sailable.bmp and territory.bmp are brighter than this, ships can be placed here
130 -- if territory.bmp is brighter than this and sailable is darker than 60, structures are placeable.
SO:
SAILABLE: 0 (not), 21 (traverse not place), 61 ( traverse and place )
TERRITORY: 131 ( place land if sailable <= 60 ), 61 ( place sea ), 0
]]
return love.graphics.newShader[[
#pragma language glsl3
uniform int radius;
uniform Image sailable;
bool seaPlace( float x, float y )
{
return ( x * 255.0 > 60.0 ) && ( y * 255.0 > 60.0 );
}
bool own( float x )
{
if( x * 255.0 > 130.0 ){
return true;
}
else {
return false;
}
}
bool land( float x )
{
if( x * 255.0 <= 60.0 ){
return true;
}
else {
return false;
}
}
bool placeable( Image tex, vec2 uv )
{
return own(Texel(tex, uv).r) && land(Texel(sailable, uv).r);
}
vec4 effect( vec4 color, Image tex, vec2 texture_coords, vec2 screen_coords )
{
if (placeable( tex, texture_coords + offset )){
return color;
}
return vec4( 0.0, 0.0, 0.0, 0.0 );
}
]]

View File

@ -2,14 +2,16 @@ local lg = love.graphics
local t = { local t = {
name = "", name = "",
tooltip = "button", tooltip = "",
icon = false, icon = false,
lit = false,
x = 8, x = 8,
y = 250, y = 250,
w = 176, w = 13 * 28 - 4,
h = 24, h = 24,
group = false, group = false,
visible = true, visible = false,
align = "center",
callback = function( self ) return print( "clicked button: ", self.name, self.x, self.y, self.w, self.h, self.visible ) end 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 t.selected, t.next, t.prev = t, t, t
@ -29,18 +31,22 @@ function t.new( b )
return b return b
end end
function t.highlight( b )
lg.rectangle( "fill", b.x, b.y, b.w, b.h )
end
local drawPassOngoing = false local drawPassOngoing = false
function t.draw( b ) function t.draw( b )
if b == t then if b == t then
drawPassOngoing = not( drawPassOngoing ) drawPassOngoing = not( drawPassOngoing )
if not drawPassOngoing then return end if not drawPassOngoing then return end
elseif b.visible then elseif b.visible then
lg.rectangle( "line", b.x, b.y, b.w, b.h, 6 ) lg.rectangle( "line", b.x, b.y, b.w, b.h )
lg.printf( b.name, lg.printf( b.name,
b.x + (b.icon and b.h or 0), b.x + (b.icon and b.h or 0),
b.y + 0.5 * ( b.h - lg.getFont():getHeight() ), b.y + 0.5 * ( b.h - lg.getFont():getHeight() ),
b.w - (b.icon and b.h or 0), b.w - (b.icon and b.h or 0),
"center" ) b.align )
if b.icon then if b.icon then
local h = b.icon:getHeight() local h = b.icon:getHeight()
lg.draw( b.icon, lg.draw( b.icon,
@ -48,8 +54,8 @@ function t.draw( b )
0, 0,
b.h / h ) b.h / h )
end end
if t.selected == b then if b.lit or t.selected == b then
lg.rectangle( "fill", b.x, b.y, b.w, b.h, 6 ) b:highlight()
end end
end end
return t.draw( b.next ) return t.draw( b.next )
@ -74,7 +80,9 @@ end
function t.selectNextInGroup() function t.selectNextInGroup()
--make sure our group is visible, otherwise the loop doesn't end --make sure our group is visible, otherwise the loop doesn't end
local group = t.selected and t.selected.visible and t.selected.group local group = t.selected
group = group and t.selected.visible
group = group and t.selected.group
if not group then return t.selectNext() end if not group then return t.selectNext() end
repeat t.selectNext() until group == t.selected.group repeat t.selectNext() until group == t.selected.group
end end
@ -86,15 +94,27 @@ function t.selectPrevInGroup()
repeat t.selectPrev() until group == t.selected.group repeat t.selectPrev() until group == t.selected.group
end end
function t.displayGroup( group, show ) --show/hide all buttons in a group
--passing hide=true will hide the group, hide=false or nil will show the group
--solo=true will hide all buttons outside the group
function t.displayGroup( group, hide, solo )
local b = t local b = t
repeat repeat
b = b.next b = b.next
b.visible = ( b.group == group ) local inGroup = (group == b.group)
if solo or inGroup then
b.visible = not(hide) and inGroup
end
until b == t until b == t
t.visible = true t.visible = true
end end
function t.mousepressed( x, y )
if t.selected and t.selected:contains( x, y ) then
return t.selected:callback()
end
end
function t.deselect( b ) function t.deselect( b )
t.selected = t t.selected = t
end end

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 = -90, y = 45, x = 0, y = 70,
w = 360, h = 200, w = 360, h = 200,
zoom = 1, tf = tf, zoom = 1, tf = tf,
tfTerritory = tfTerritory, tfNodes = tfNodes } tfTerritory = tfTerritory, tfNodes = tfNodes }
@ -37,7 +37,9 @@ end
function Camera.Translate( x, y ) function Camera.Translate( x, y )
x = x or 0 x = x or 0
y = y or 0 y = y or 0
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(-360, math.min(360, Camera.x + x)),
math.max(-140, math.min(140, Camera.y + y)), Camera.w, Camera.h)
end end
--In world coordinates: top left corner at x, y, extent of 1/w, 1/h. --In world coordinates: top left corner at x, y, extent of 1/w, 1/h.
@ -48,14 +50,17 @@ function Camera.Set( x, y, w, h )
tf:scale( w / 360, -h / 200 ) tf:scale( w / 360, -h / 200 )
tf:translate( 180 - x, -y - 100 ) tf:translate( 180 - x, -y - 100 )
tfTerritory:reset() tfTerritory:reset()
tfTerritory:scale( w / 512, h / 285 ) tfTerritory:scale( w / 512, h / 285 )
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
function Camera.Resize( w, h ) function Camera.Resize( w, h )

65
ui/loadmodal.lua Normal file
View File

@ -0,0 +1,65 @@
local love = assert( love )
local modal = require( "ui.modal" )
local button = require( "ui.button" )
local map = require( "map.map" )
local t = {}
local loadLocation = false
local folder = love.graphics.newImage( "icons/load.png" )
folder:setFilter( "nearest", "nearest" )
local loadButton = button.new{
group = t,
name = "load",
callback = function()
if not loadLocation then return end
map.load( loadLocation )
return t:stop() end,
visible = false,
icon = folder,
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 = t,
name = "cancel load",
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 )
loadLocation = loadLocation or map.path
button.selected = loadButton
loadButton.name = "load from "..loadLocation
button.displayGroup( t, false, true )
end
function t.draw()
love.graphics.clear( 0,0,0,1 )
love.graphics.setColor( 0, 0, 1, 0.4 )
button:draw()
end
function t.directorydropped( path )
loadLocation = path
map.path = path
loadButton.name = "load from "..map.path
return love.filesystem.mount( path, "" )
end
function t.filedropped( path )
return map.reloadLayer( path )
end
return modal.new( t )

45
ui/menu/ainodes.lua Normal file
View File

@ -0,0 +1,45 @@
local t = {}
local lg = assert( love ).graphics
local modal = require 'ui.modal'
local button = require 'ui.button'
local camera = require 'ui.camera'
local map = require 'map.map'
local node
local moveModal = modal.new{}
local selectModal = modal.new{}
button.new{ name = "ATTACK NODE",
group = t,
icon = lg.newImage("icons/node-attack.png"),
x = 615,
y = 0,
callback = function()
node = map.ainodes.newNode( true )
return moveModal:start()
end
}
button.new{ name = "PLACEMENT NODE",
group = t,
icon = lg.newImage("icons/node-place.png"),
x = 615,
y = 1 * (4 + button.h),
callback = function()
node = map.ainodes.newNode( true )
return moveModal:start()
end
}
button.new{ name = "MOVE NODE",
group = t,
y = 2 * (4 + button.h),
x = 615,
}
button.new{ name = "DELETE NODE",
group = t,
y = 3 * (4 + button.h),
x = 615,
}
return t

279
ui/menu/cities.lua Normal file
View File

@ -0,0 +1,279 @@
local love = assert( love )
local lg = assert( love ).graphics
local utf8 = require 'utf8'
local button = require 'ui.button'
local textinput = require 'ui.textinput'
local modal = require 'ui.modal'
local map = require 'map.map'
local camera = require 'ui.camera'
local t = {}
local city
local function keypressed( key, code, isRepeat )
if code == 'escape' then return modal.current:stop() end
if modal.previous then return modal.previous.keypressed( key, code, isRepeat ) end
end
--select modal: as normal, but clicking a city will proceed to the previously selected mode,
--and clicking escape will clear the previously selected mode
local selectModal = modal.new{}
local textModal = modal.new{}
local numberModal = modal.new{}
local editModal = modal.new{ mousepressed = button.mousepressed, keypressed = keypressed, }
local moveModal = modal.new{ mousepressed = button.mousepressed, keypressed = keypressed, mousemoved = button.selectIn }
local deleteModal = modal.new{ keypressed = keypressed }
local currentMode
button.new{ name = "NEW CITY",
group = t,
icon = lg.newImage("icons/city-new.png"),
x = 615,
y = 0,
callback = function()
city = map.cities.newCity()
city:add()
return editModal:start()
end
}
moveModal.button = button.new{ name = "MOVE CITY",
group = t,
icon = lg.newImage("icons/city-move.png"),
x = 615,
y = button.h + 4,
callback = function( self )
self.lit = true
selectModal.mode = moveModal
return selectModal:start()
end,
}
editModal.button = button.new{ name = "EDIT CITY",
group = t,
x = 615,
y = (button.h + 4) * 2,
callback = function( self )
self.lit = true
selectModal.mode = editModal
return selectModal:start()
end,
}
deleteModal.button = button.new{ name = "DELETE CITY",
group = t,
icon = lg.newImage("icons/city-delete.png"),
x = 615,
y = (button.h + 4) * 3,
callback = function( self )
self.lit = true
return deleteModal:start()
end,
}
--editButtons
local function editText( self )
print( "editing: ", self.field, city.name )
self.lit = true
return textModal:start( self.field )
end
local function editNumber( self )
print( "editing: ", self.field, city.name )
self.lit = true
return numberModal:start( self.field )
end
local editButtons = {
save = button.new{
icon = lg.newImage( "icons/check.png" ),
callback = function() return editModal:stop() end,},
name = button.new{ callback = editText },
country = button.new{ callback = editText },
x = button.new{ callback = editNumber },
y = button.new{ callback = editNumber },
capital = button.new{ callback = function( self )
if city then
self.name = tostring( not( city.capital ) )
return city:toggleCapital()
end
end },
}
do
local i = 0
for _, key in ipairs{ "save", "name", "country", "x", "y", "capital" } do
local b = assert( editButtons[ key ] )
b.align = "right"
b.field = key
b.name = tostring( key ) --bools must be cast to string before getting passed to printf
b.group = editModal
b.x = lg.getWidth() / 2 - button.w / 2
b.y = (button.h + 4) * i
i = i + 1
end
end
function editModal:start()
modal.start( self )
button.displayGroup( self, false, true )
for k, b in pairs( editButtons ) do
b.name = tostring( city[k] or b.name )
end
if city.capital == false then editButtons.capital.name = "false" end
end
function editModal:stop()
return modal.stop( self )
end
function editModal.draw()
lg.setColor( 1, 1, 1, 0.5 )
return button:draw()
end
function moveModal.update( dt )
local x, y = love.mouse.getPosition()
if y > t.menuHeight and love.mouse.isDown( 1 ) then
local wx, wy = camera.GetWorldCoordinate( x, y )
city:moveTo( wx, wy )
end
end
function moveModal.mousemoved( x, y, dx, dy, istouch )
return button.selectIn( x, y )
end
function moveModal.mousepressed( x, y, mouseButton, istouch, presses )
if y < t.menuHeight then
moveModal:stop()
return button.mousepressed( x, y, mouseButton, istouch, presses )
end
if map.selected and mouseButton == 2 then
city = map.selected
end
end
function deleteModal.mousepressed( x, y, mouseButton, istouch, presses )
if map.selected then
map.selected:delete()
end
if y < t.menuHeight then
deleteModal.button.lit = false
deleteModal:stop()
return button.mousepressed( x, y, mouseButton, istouch, presses )
end
end
function numberModal:start( field )
self.field = field
return modal.start( self )
end
function numberModal.keypressed( key, code, isrepeat )
if code == "backspace" then
local text = tostring( city[numberModal.field] )
-- get the byte offset to the last UTF-8 character in the string.
local byteoffset = utf8.offset(text, -1)
print( "textmodal: backspace", byteoffset )
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).
local newstr = text:sub( 1, byteoffset - 1)
if newstr == "" then newstr = 0 end
city[numberModal.field] = tonumber( newstr ) or city[numberModal.field]
editButtons[numberModal.field].name = city[numberModal.field]
end
end
if code == "escape" or code == "return" then
return numberModal:stop()
end
end
function numberModal.textinput( char )
local str = tostring( city[ numberModal.field ] )
local plus = str..char
print( "text input: ", char )
if tonumber( plus ) then
city[ numberModal.field ] = plus
editButtons[ numberModal.field ].name = plus
end
end
function textModal.textinput( char )
print( "text input: ", char )
city[textModal.field] = city[textModal.field] .. char
editButtons[textModal.field].name = city[textModal.field]
end
function textModal.mousepressed()
end
function textModal:stop()
self.field = nil
return modal.stop( self )
end
function textModal:start( field )
self.field = field
return modal.start( self )
end
function textModal.keypressed( key, code, isRepeat )
if code == "backspace" then
local text = city[textModal.field]
-- get the byte offset to the last UTF-8 character in the string.
local byteoffset = utf8.offset(text, -1)
print( "textmodal: backspace", byteoffset )
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).
city[textModal.field] = text:sub( 1, byteoffset - 1)
editButtons[textModal.field].name = city[textModal.field]
end
end
if code == "escape" or code == "return" then
return textModal:stop()
end
end
function selectModal.keypressed( key, code, isRepeat )
if code == 'escape' then return selectModal:stop() end
if modal.previous then return modal.previous.keypressed( key, code, isRepeat ) end
end
function selectModal:stop()
local mode = selectModal.mode
if mode then
mode.button.lit = false
end
return modal.stop( self )
end
function selectModal.mousepressed( x, y, mouseButton, istouch, presses )
if y < t.menuHeight then
selectModal:stop()
return button.mousepressed( x, y, mouseButton, istouch, presses )
end
if map.selected then
city = map.selected
return selectModal.mode:start()
end
end
function t.setMenuHeight( h )
t.menuHeight = h
end
return t

3
ui/menu/lines.lua Normal file
View File

@ -0,0 +1,3 @@
local t = {}
return t

186
ui/menu/mainmenu.lua Normal file
View File

@ -0,0 +1,186 @@
local love = assert( love )
local button = require 'ui.button'
local modal = require 'ui.modal'
local camera = require 'ui.camera'
local map = require 'map.map'
local t = { menuHeight = 200 }
local loadImg = love.graphics.newImage
local layers = {
{ name = "AF", layer = "africa" , menu = require 'ui.menu.territory' },
{ name = "EU", layer = "europe" , menu = require 'ui.menu.territory' },
{ name = "NA", layer = "northamerica" , menu = require 'ui.menu.territory' },
{ name = "SA", layer = "southamerica" , menu = require 'ui.menu.territory' },
{ name = "AS", layer = "southasia" , menu = require 'ui.menu.territory' },
{ name = "RU", layer = "russia" , menu = require 'ui.menu.territory' },
{ name = "PATH", layer = "travelnodes" , menu = require 'ui.menu.travelnodes', icon = loadImg( "icons/layer-travelnodes.png" )},
{ name = "AI", layer = "ainodes" , menu = require 'ui.menu.ainodes', icon = loadImg( "icons/layer-ainodes.png" )},
{ name = "CITY", layer = "cities" , menu = require 'ui.menu.cities', icon = loadImg( "icons/layer-cities.png" )},
{ name = "COAST", layer = "coastlines" , menu = require 'ui.menu.lines', icon = loadImg( "icons/layer-coastlines.png" )},
{ name = "LOW", layer = "coastlinesLow", menu = require 'ui.menu.lines', icon = loadImg( "icons/layer-coastlines-low.png" )},
{ name = "INT", layer = "international", menu = require 'ui.menu.lines', icon = loadImg( "icons/layer-international.png" )},
{ name = "SAIL", layer = "sailable" , menu = require 'ui.menu.territory', icon = loadImg( "icons/layer-sailable.png" )},
}
button.new{
name = "LOAD", x = 250, y = 0,
group = t,
callback = require( 'ui.loadmodal' ).start,
icon = love.graphics.newImage( "icons/load.png" )}
button.new{
name = "SAVE", x = 250, y = 28,
group = t,
callback = require( 'ui.savemodal' ).start,
icon = love.graphics.newImage( "icons/save.png" )}
button.new{
name = "UNDO", x = 250, y = 2 * 28,
group = t,
callback = map.undo,
icon = love.graphics.newImage( "icons/undo.bmp" ) }
local editButtons = {}
local layerButtons = {}
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 activeLayerButton
local function back( self )
print( "back button clicked" )
if activeLayerButton then
activeLayerButton.lit = false
end
activeLayerButton = nil
map.setEditLayer()
modal.exitAll()
button.displayGroup( t, false, true )
for i, b in ipairs( editButtons ) do
b.visible = true
end
for i, b in ipairs( showButtons ) do
b.visible = true
end
self.visible = false
return updateVisibilityIcons()
end
local backButton = button.new{
name = "UP",
visible = false,
y = 5 * 28,
x = 250,
group = false,
icon = love.graphics.newImage( "icons/up.bmp" ),
callback = back,
}
local function editLayer( self )
back( backButton )
self.lit = true
map.setEditLayer( self.layer )
activeLayerButton = self
if self.menu then
button.displayGroup( self.menu )
end
backButton.visible = true
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 x = 250
for i = 1, #layers do
layers[i].menu.menuHeight = t.menuHeight
editButtons[i] = button.new( copy( i, {
y = 3 * 28,
x = x + (button.h + 4) * ( i - 1 ),
w = 24,
callback = editLayer,
group = editButtons,
tooltip = "edit "..layers[i].layer
}))
layerButtons[ 2 * i - 2 ] = editButtons[i]
showButtons[i] = button.new( copy( i, {
y = 4 * 28,
x = x + (button.h + 4) * ( i - 1 ),
w = 24,
name = "",
callback = toggleVisibleLayer,
icon = visibilityIcon,
group = showButtons,
tooltip = "show "..layers[i].layer
}))
layerButtons[ 2 * i - 1 ] = showButtons[i]
end
function t.draw()
--Status bar.
love.graphics.setScissor( 0, 0, 250, t.menuHeight )
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() - 60
love.graphics.setColor( 0, 0, 0, 1 )
love.graphics.rectangle( "fill", 0, 0, 250, love.graphics.getHeight() )
love.graphics.setColor( 1, 1, 1, 1 )
love.graphics.print(([[
SCREEN %-12d %-12d
WORLD %-12.2f%-12.2f
BITMAP %-12d %-12d
%s
%s]]):format(
x, y,
wx, wy,
bx, by,
button.selected and button.selected.tooltip or "",
map.editLayer and map.editLayer.filename or ""), 0, 0)
if map.selected then love.graphics.print( map.selected:formatDisplayInfo(), 0, 80 ) end
if map.selectionLocked then end
love.graphics.setScissor( 250, 0, love.graphics.getWidth() - 250, t.menuHeight)
love.graphics.rectangle( "line", 0, 0 , 250, t.menuHeight )
love.graphics.rectangle( "line", 250, 0, love.graphics.getWidth() - 250, t.menuHeight )
love.graphics.rectangle( "line", 250, 0, button.w, t.menuHeight )
love.graphics.setColor( 1, 1, 1, 0.8 )
button:draw()
love.graphics.setColor( 1, 0, 0, 0.4 )
end
do --button visibility
button.displayGroup( t, false, true )
button.displayGroup( editButtons, false, false )
button.displayGroup( layerButtons, false, false )
button.displayGroup( showButtons, false, false )
end
return t

3
ui/menu/territory.lua Normal file
View File

@ -0,0 +1,3 @@
local t = {}
return t

3
ui/menu/travelnodes.lua Normal file
View File

@ -0,0 +1,3 @@
local t = {}
return t

View File

@ -1,12 +1,22 @@
local love = assert( love ) local love = assert( love )
local button = require( "button" ) local button = require( "ui.button" )
local t = {} local t = {}
t.__index = t t.__index = t
local i = 0 local i = 0
function t.start( self ) function t.start( self )
print( "starting modal:", i + 1)
love.graphics.push( "all" )
love.graphics.setScissor(
self.x or 0,
self.y or 0,
self.w or love.graphics.getWidth(),
self.h or love.graphics.getHeight())
i = i + 1 i = i + 1
t[i] = t[i] or {} t[i] = { modal = self }
t.previous = t[i]
t.current = self
--store callbacks --store callbacks
for name in pairs( self ) do for name in pairs( self ) do
@ -18,7 +28,7 @@ function t.start( self )
--store menus --store menus
local b = button.next local b = button.next
repeat repeat
t[i][b] = b.visible t[i][b] = b.visible
b = b.next b = b.next
until b == button until b == button
@ -28,21 +38,31 @@ function t.stop( self )
--restore callbacks --restore callbacks
for name in pairs( self ) do for name in pairs( self ) do
if love[name] then if love[name] then
love[name] = t[i][name] love[name] = t[i][name] or love[name]
end end
end end
--restore menus --restore menus
local b = button local b = button
button.selected = button button.deselect()
repeat repeat
b = b.next b = b.next
b.visible = t[i][b] b.visible = t[i][b] or false
until b == button until b == button
t.current = t[i].modal
t[i] = nil t[i] = nil
i = i - 1 i = i - 1
t.previous = t[i - 1]
love.graphics.pop( "all" )
end
function t.exitAll()
if i < 1 then return end
i = 1
return t.stop( love )
end end
function t.new( modal ) function t.new( modal )

View File

@ -1,49 +1,51 @@
--Modal for setting save options.
local love = assert( love ) local love = assert( love )
local modal = require( "modal" ) local modal = require( "ui.modal" )
local button = require( "button" ) local button = require( "ui.button" )
local map = require( "map" ) local map = require( "map.map" )
local t = {} local save = require( "ui.saveprogress" )
local t = { w = love.graphics.getWidth(), h = 200 }
local saveLocation = false local saveLocation = false
local floppy = love.graphics.newImage( "icons/save.png" ) local floppy = love.graphics.newImage( "icons/save.png" )
floppy:setFilter( "nearest", "nearest" ) floppy:setFilter( "nearest", "nearest" )
local saveButton = button.new{ local saveButton = button.new{
group = "saveModal", group = t,
name = "save", name = "save",
callback = function() map.save(); return t:stop() end, align = "left",
callback = function() save:start() end,
visible = false, visible = false,
icon = floppy, icon = floppy,
x = love.graphics.getWidth() / 2 - 300, w = 400 - button.x,
y = love.graphics.getHeight() / 2 - 150, h = 64,
w = 600, y = 0,
h = 100,
} }
local xIcon = love.graphics.newImage( "icons/x.png" ) local xIcon = love.graphics.newImage( "icons/x.png" )
xIcon:setFilter( "nearest", "nearest" ) xIcon:setFilter( "nearest", "nearest" )
local cancelButton = button.new{ local cancelButton = button.new{
group = "saveModal", group = t,
name = "cancel", name = " cancel",
align = "left",
visible = false, visible = false,
icon = xIcon, icon = xIcon,
callback = function() return t:stop() end, callback = function() return t:stop() end,
x = love.graphics.getWidth() / 2 - 300, y = 68,
y = love.graphics.getHeight() / 2, w = 400 - button.x,
w = 600, h = 64,
h = 100
} }
function t.start() function t.start()
modal.start( t ) modal.start( t )
saveLocation = saveLocation or map.path saveLocation = saveLocation or map.path
button.selected = saveButton button.selected = saveButton
saveButton.name = "save to "..saveLocation saveButton.name = " save to "..saveLocation
button.displayGroup( "saveModal", true ) button.displayGroup( t, false, true )
end end
function t.draw() function t.draw()
love.graphics.clear( 0,0,0,1 ) love.graphics.clear( 0,0,0,1 )
love.graphics.setColor( 1, 0, 0, 0.4 ) love.graphics.setColor( 1, 1, 1, 0.9 )
button:draw() button:draw()
end end
@ -51,7 +53,7 @@ end
function t.directorydropped( path ) function t.directorydropped( path )
saveLocation = path saveLocation = path
map.path = path map.path = path
saveButton.name = "save to "..map.path saveButton.name = " save to "..map.path
return love.filesystem.mount( path, "" ) return love.filesystem.mount( path, "" )
end end

36
ui/saveprogress.lua Normal file
View File

@ -0,0 +1,36 @@
--What to display when saving is in progress.
local love = assert( love )
local modal = require( "ui.modal" )
local button = require( "ui.button" )
local map = require( "map.map" )
local timer = love.timer
local time = 0
local t = { w = 400, h = 200 }
local progressMessage = ""
function t.start()
time = timer.getTime()
progressMessage = ""
return modal.start( t )
end
function t.update( dt )
local msg = map.save()
if not msg then return t:stop() end
progressMessage = msg
end
function t.draw()
love.graphics.push( "all" )
love.graphics.setCanvas()
love.graphics.setShader()
love.graphics.setScissor( 0, 0, t.w, t.h )
love.graphics.clear()
love.graphics.setColor( 1, 1, 1, 1 )
love.graphics.print( timer.getTime() - time )
love.graphics.printf( progressMessage, 0, love.graphics.getFont():getHeight(), t.w, "left")
love.graphics.pop( "all" )
end
return modal.new( t )

0
ui/territory.lua Normal file
View File

View File

@ -1,6 +1,6 @@
local love = assert( love ) local love = assert( love )
local utf8 = require("utf8") local utf8 = require("utf8")
local modal = require( "modal" ) local modal = require( "ui.modal" )
local t = modal.new{ } local t = modal.new{ }
function t.setCurrentModal( fields ) function t.setCurrentModal( fields )
@ -47,16 +47,17 @@ function t.keypressed(key, code, isRepeat)
end end
if key == "backspace" then if key == "backspace" then
local text = t.currentModal.currentField
-- get the byte offset to the last UTF-8 character in the string. -- get the byte offset to the last UTF-8 character in the string.
local byteoffset = utf8.offset(text, -1) local byteoffset = utf8.offset(text, -1)
if byteoffset then if byteoffset then
-- remove the last UTF-8 character. -- 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). -- 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) t.currentModal.currentField = text:sub( 1, byteoffset - 1)
end end
end end
if key == "escape" then if code == "escape" then
return t:stop() return t:stop()
end end
end end