Compare commits

...

9 Commits
v0.1.0 ... main

56 changed files with 1978 additions and 683 deletions

1
.gitignore vendored
View File

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

142
bmp.lua
View File

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

View File

@ -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

103
lib/mkdir.lua Normal file
View File

@ -0,0 +1,103 @@
-- mkdir only
-- A portable filesystem API using LuaJIT's FFI
-- Retrieved 2024-07-13 from https://gist.githubusercontent.com/Techcable/503f35ceea9554fb81cf3a5c1aa550da/raw/33a29f59207335b743824fbb657e4721a12ce280/fs.lua
local ffi = require("ffi")
local table = require("table")
require("string")
-- Cache needed functions and locals
local C, errno, string = ffi.C, ffi.errno, ffi.string
local concat, insert = table.concat, table.insert
-- "Standard" C99 functions
ffi.cdef[[
char *strerror(int errnum);
]]
local exists, mkdir, PATH_SEPARATOR
if ffi.os == "Windows" then
ffi.cdef[[
bool CreateDirectoryA(const char *path, void *lpSecurityAttributes);
]]
function mkdir(path, _)
assert(type(path) == "string", "path isn't a string")
if not C.CreateDirectoryA(path, nil) then
local message = string(C.strerror(errno()))
error("Unable to create directory " .. path .. ": " .. message)
end
end
PATH_SEPARATOR = "\\"
elseif ffi.os == "Linux" or ffi.os == "OSX" then
ffi.cdef[[
int mkdir(const char *path, int mode);
]]
function mkdir(path, mode)
assert(type(path) == "string", "path isn't a string")
local mode = tonumber(mode or "755", 8)
if C.mkdir(path, mode) ~= 0 then
local message = string(C.strerror(errno()))
error("Unable to create directory " .. path .. ": " .. message)
end
end
PATH_SEPARATOR = "/"
else
error("Unsupported operating system: " .. ffi.os)
end
local function join(...)
local parts = {}
for i = 1, select("#", ...) do
local part = select(i, ...)
insert(parts, part)
end
return concat(parts, PATH_SEPARATOR)
end
local function splitPath(path)
assert(type(path) == "string", "path isn't a string!")
local parts = {}
local lastIndex = 0
for i = 1, path:len() do
if path:sub(i, i) == PATH_SEPARATOR then
insert(parts, path:sub(lastIndex, i - 1))
lastIndex = i + 1
end
end
insert(parts, path:sub(lastIndex))
return parts
end
local function mkdirs(path)
local parts = splitPath(path)
local currentPath = parts[1]
for i=2, #parts do
if not exists(currentPath) then
mkdir(currentPath)
end
-- Note: This isn't suboptimal, since we really do need the intermediate results
currentPath = currentPath .. PATH_SEPARATOR .. parts[i]
end
if not exists(path) then
mkdir(path)
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 {
exists = exists,
join = join,
mkdir = mkdir,
mkdirs = mkdirs,
splitPath = splitPath,
PATH_SEPERATOR = PATH_SEPARATOR
}

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,15 +20,19 @@ 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
if love.keyboard.isScancodeDown( "w" ) then moveCamera = true; ty = ty + 30 * dt 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 - 30 * dt 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 - 30 * dt 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 + 30 * dt 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 ) end if love.keyboard.isScancodeDown( "q" ) then Camera.Zoom( dt * 400 ) end
if love.keyboard.isScancodeDown( "e" ) then Camera.Zoom( -dt ) end if love.keyboard.isScancodeDown( "e" ) then Camera.Zoom( -dt* 400 ) end
if moveCamera then Camera.Translate( tx, ty ) end if moveCamera then Camera.Translate( tx, ty ) end
@ -44,25 +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.1, 0.1, 0.5, 0.8 )
love.graphics.rectangle( "fill", 0, 0, 250, love.graphics.getHeight() )
love.graphics.setColor( 1, 1, 1, 1 )
love.graphics.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.setColor( 1, 1, 1, 0.8 )
button:draw()
end end
function love.resize(w, h) function love.resize(w, h)
@ -70,37 +56,35 @@ function love.resize(w, h)
end end
function love.wheelmoved(x, y) function love.wheelmoved(x, y)
Camera.Zoom( (y > 0) and 0.1 or -0.1 ) Camera.Zoom( (y > 0) and 3 or -3 )
end 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
@ -108,9 +92,6 @@ function love.keypressed(key, code, isRepeat)
end end
end end
function love.textinput()
do
end end

View File

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

203
map.lua
View File

@ -1,203 +0,0 @@
local lg = love.graphics
local AI = require 'ai'
local Cities = require 'cities'
local Lines = require 'lines'
local Nodes = require 'travelNodes'
local Camera = require 'camera'
local Territory = require 'territory'
--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,
northamerica = false,
russia = false,
southamerica = false,
southasia = false
},
background = false,
coastlines = false,
coastlinesLow = false,
international = false,
travelnodes = false,
sailable = false,
ainodes = false,
cities = false
}
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 )
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.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()
lg.setLineWidth( 1 / Camera.zoom )
v:drawBorder( "land" )
lg.setLineWidth( 3 / Camera.zoom )
v:drawBorder( "sea" )
end
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 --all this stuff is drawn in world coordinates, ( -180, 180 ) x ( -100, 100 )
lg.replaceTransform( Camera.tf )
if map.selected then
if map.selected[1] then --lines
local p = map.selected
lg.setColor( 0.4, 0.5, 0.8, 0.5 )
lg.setLineWidth( 0.2 / Camera.zoom )
lg.rectangle( "fill", p.x, p.y, p.X - p.x, p.Y - p.y )
lg.setColor( 1.0, 0, 0, 1 )
lg.line( p )
else --points
lg.setColor( 1.0, 0.5, 0.5, 0.9 )
lg.setLineJoin( "miter" )
lg.setLineWidth( 1.0 / Camera.zoom )
lg.circle( "line", map.selected.x, map.selected.y, 0.2 + 1.0 / Camera.zoom )
end
end
if map.cities.visible then --points
lg.setColor( 1, 0, 0, 0.5 )
lg.setPointSize( 5.0 )
map.cities.draw()
lg.setColor( 1, 1, 0.0, 0.5 )
map.cities.drawCapitals()
end
if map.ainodes.visible then
lg.setPointSize( 5.0 )
map.ainodes:draw()
end
do --line stuff
lg.setColor(1, 1, 1, 0.2 )
lg.setLineJoin( "miter" )
lg.setLineWidth( 0.2 / Camera.zoom )
map.international:draw()
lg.setColor(1, 1, 1, 0.5 )
map.coastlines:draw()
map.coastlinesLow:draw()
--International Date Line
lg.line( -180, -100, -180, 100 )
lg.line( 180, -100, 180, 100 )
lg.line( -180, 90, 180, 90 )
lg.line( -180, -90, 180, -90 )
lg.line( -180, 100, 180, 100 )
lg.line( -180, -100, 180, -100 )
end
do --travel nodes
lg.replaceTransform( Camera.tfNodes )
if map.travelnodes.visible then
map.travelnodes:draw()
end
end
end
end
local function write( filename, string )
print( "Pretending to write", string:len(), "bytes to", filename )
--[[ os.rename( filename, filename..".bak" ) --just in case :^)
local file = assert( io.open( filename, "w+" ) )
assert( file:write( string ) )
file:close()]]
end
function map.save()
for k, layer in pairs( layers ) do
print( "SAVING:", k, tostring( layer.filename ) )
write( map.path..tostring( layer.filename ), assert( layer:save() ) )
end
end
function map.hover(x, y)
end
function map.undo()
print( "=== UNDO ===" )
end
return map

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 = {
@ -79,7 +87,11 @@ function t.draw( nodes )
end end
function t.save( nodes ) function t.save( nodes )
return bmp.savePoints( nodes.all, "512rgb24" ) return bmp.ai( nodes.all )
end
function t.newNode( isAttacking )
end 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

363
map/bmp.lua Normal file
View File

@ -0,0 +1,363 @@
--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 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" )
img:setWrap( "repeat", "clampzero" )
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 ) )
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 we only use one palette ever
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,
},
["blur"] = {
header = getHeader( "data/graphics/blur.bmp" ),
w = 512,
h = 285,
},
["screenshot"] = {
header = getHeader( "screenshot.bmp" ),
w = 256,
h = 128,
},
}
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)
--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
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
do --sailable
local reversePalette = {}
for eight, four in pairs( formats.sailable.palette ) do
reversePalette[ four ] = eight
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
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] = 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
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
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 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 )
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" )
img:setWrap( "repeat", "clampzero" )
return img, imgd
end
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

@ -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,6 +54,65 @@ 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. ===" )
@ -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,21 @@ 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
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 end
function t.load( filename ) function t.load( filename )
@ -59,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
@ -73,11 +88,14 @@ function t.selectNearest( lines, wx, wy )
end end
function t.save( lines ) function t.save( lines )
local str = {} local str = { "b" } --initial B
for i, poly in ipairs( lines ) do for i, poly in ipairs( lines ) do
str[i] = 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

267
map/map.lua Normal file
View File

@ -0,0 +1,267 @@
local love = assert( love )
local io = io
local coroutine = coroutine
local mkdir = assert( require 'lib.mkdir' )
local lg = love.graphics
local AI = require 'map.ai'
local Cities = require 'map.cities'
local Lines = require 'map.lines'
local Nodes = require 'map.travelNodes'
local Camera = require 'ui.camera'
local Territory = require 'map.territory'
local Blur = require 'map.blur'
--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,
blur = false,
}
local map = {
layers = layers,
path = false,
loaded = false,
selected = false,
selectionLocked = false,
editLayer = false,
territory = {
africa = false,
europe = false,
northamerica = false,
russia = false,
southamerica = false,
southasia = false
},
background = false,
coastlines = false,
coastlinesLow = false,
international = false,
travelnodes = false,
sailable = false,
ainodes = 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 )
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" )
map.blur = Blur.load( "/data/graphics/blur.bmp", map )
for k, v in pairs(map.territory) do
map.territory[k] = Territory.load( "/data/earth/"..k..".bmp", k )
end
map.loaded = true
map.path = path
--update references
for k, v in pairs( layers ) do
layers[k] = map[k] or map.territory[k]
end
end
function map.draw()
love.graphics.setScissor( 0, 200, love.graphics.getWidth(), love.graphics.getHeight() - 200 )
lg.clear( 0, 0, 0, 1 )
if not map.loaded then return end
do --territory
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 )
local sh = require 'shaders.sailable'
lg.setShader( sh )
sh:send( "lowBorder", 60 )
sh:send( "highBorder", 130 )
for k, v in pairs(map.territory) do
if v.visible then
v:draw()
--[[lg.setLineWidth( 1 / Camera.zoom )
v:drawBorder( "land" )
lg.setLineWidth( 3 / Camera.zoom )
v:drawBorder( "sea" )]]
end
end
if map.sailable.visible then
sh:send( "lowBorder", 20 )
sh:send( "highBorder", 60 )
lg.setShader( require 'shaders.sailable' )
map.sailable:draw()
end
lg.setShader()
lg.setBlendMode( "alpha" )
lg.setColor( 1, 1, 1, 1 )
end
do --all this stuff is drawn in world coordinates, ( -180, 180 ) x ( -100, 100 )
lg.replaceTransform( Camera.tf )
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( 5.0 )
map.cities.draw()
lg.setColor( 1, 1, 0.0, 0.5 )
map.cities.drawCapitals()
end
if map.ainodes.visible then
lg.setPointSize( 5.0 )
map.ainodes:draw()
end
do --line stuff
lg.setColor(1, 1, 1, 0.2 )
lg.setLineJoin( "miter" )
lg.setLineWidth( 0.2 / Camera.zoom )
map.international:draw()
lg.setColor(1, 1, 1, 0.5 )
map.coastlines:draw()
map.coastlinesLow:draw()
--International Date Line
lg.line( -180, -100, -180, 100 )
lg.line( 180, -100, 180, 100 )
lg.line( -180, 90, 180, 90 )
lg.line( -180, -90, 180, -90 )
lg.line( -180, 100, 180, 100 )
lg.line( -180, -100, 180, -100 )
end
do --travel nodes
lg.replaceTransform( Camera.tfNodes )
if map.travelnodes.visible then
map.travelnodes:draw()
end
end
end
end
do
local function write( filename, string )
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
local function save()
--should be cross platform-ish.
--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
map.save = coroutine.wrap( save )
end
function map.hover(x, y)
end
function map.undo()
print( "=== UNDO ===" )
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

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 = {
@ -57,10 +57,6 @@ function t.getPixel( territory, x, y )
return territory.imgd:getPixel( imgx, imgy ) return territory.imgd:getPixel( imgx, imgy )
end end
function t.toggleVisibility( territory )
end
--[[ --[[
0 0
20 -- once sailable.bmp is brighter than this, the area is traversable by ships 20 -- once sailable.bmp is brighter than this, the area is traversable by ships
@ -111,6 +107,7 @@ function t.computeBorder( territory, threshold, key )
local n = 1 local n = 1
local w, h = territory.imgd:getWidth() - 1, territory.imgd:getHeight() - 1 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 x = 0, w do
for y = 0, h do for y = 0, h do
--Bottom left, bottom right, and top right of pixel in image coordinates: --Bottom left, bottom right, and top right of pixel in image coordinates:
@ -146,7 +143,8 @@ function t.computeBorder( territory, threshold, key )
end end
function t.save( territory ) function t.save( territory )
return bmp.save( territory.imgd, "512rgb24" ) local fmt = (territory.name == "sailable") and "sailable" or "territory"
return bmp[fmt]( territory.imgd )
end end
return t return t

View File

@ -2,25 +2,26 @@
--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 )
local t = setmetatable({}, {__index = locationQuery}) local t = setmetatable({}, {__index = locationQuery})
local isSailable local isSailable
local function isConnected( startNode, endNode ) local function hasEdge( startNode, endNode )
local ix, iy, fx, fy = startNode.x, startNode.y, endNode.x, endNode.y 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
if fx > 180 then fx = fx - 180 end if fx > 180 then fx = fx - 180 end
local dx, dy = fx - ix, fy - iy local dx, dy = fx - ix, fy - iy
local mag = math.sqrt( dx * dx + dy * dy ) local mag = math.sqrt( dx * dx + dy * dy )
local n = 2 * math.floor( mag ) local n = math.floor( mag )--/ 0.5 )
dx, dy = 0.5 * dx / mag, 0.5 * dy / mag dx, dy = dx / mag, dy / mag
--dx, dy = 0.5 * dx, 0.5 * dy
for i = 1, n do for i = 0, n - 1 do
ix, iy = ix + dx, iy + dy ix, iy = ix + dx, iy + dy
if not( isSailable( ix, -iy ) ) then return nil end if not( isSailable( ix, -iy ) ) then return nil end
end end
@ -46,15 +47,20 @@ local function worldToBitmap( x, y )
end end
local function bitmapToWorld( x, y ) local function bitmapToWorld( x, y )
return 360 * ( x - 800 ) / 800 - 360 / 2 + 360, local w, a = 360.0, 600.0 / 800.0
360 * ( 600 / 800 ) * ( y - 600 ) / 600 + 180 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 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
local n = 1 local n = 1
for x = 0, 799 do for x = 0, 799 do
for y = 0, 399 do for y = 0, 399 do
@ -72,31 +78,70 @@ function t.load( filename, sailable )
local adjacent = {} local adjacent = {}
for j, destNode in ipairs( nodes.nodes ) do for j, destNode in ipairs( nodes.nodes ) do
adjacent[j] = isConnected( srcNode, destNode ) adjacent[j] = hasEdge( srcNode, destNode )
end end
nodes.connections[i] = adjacent nodes.connections[i] = adjacent
end 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 ) nodes.nodes = locationQuery.New( nodes.nodes )
setmetatable( nodes, {__index = t} ) setmetatable( nodes, {__index = t} )
return nodes return nodes
end 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
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. --Determine if graph has more than one connected component.
function t.isConnected( nodes ) 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 end
function t.draw( nodes ) function t.draw( nodes )
lg.setPointSize( 8 ) lg.setPointSize( 8 )
lg.setColor( 1, 1, 1, 0.5 ) lg.setColor( 1, 1, 1, 0.7 )
lg.points( nodes.points ) lg.points( nodes.points )
return t.drawConnections( nodes ) return t.drawConnections( nodes )
end end
function t.drawConnections( nodes ) 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 i, connection in pairs( nodes.connections ) do
for j in pairs( connection ) 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 local ix, iy, fx, fy = nodes.nodes[i].x, nodes.nodes[i].y, nodes.nodes[j].x, nodes.nodes[j].y
@ -107,7 +152,7 @@ function t.drawConnections( nodes )
end end
function t.save( nodes ) function t.save( nodes )
return bmp.savePoints( nodes.nodes, "800r4") return bmp.travel( nodes.nodes )
end end
return t return t

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.

View File

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

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
@ -19,50 +21,32 @@ function t.contains( button, x, y )
and y < button.y + button.h and y > button.y and y < button.y + button.h and y > button.y
end end
local function debugLoop()
local i = 0
local j = 0
local a = t
repeat
i = i + 1
--print( "BUTTON", i, tostring( a ) )
a = a.next
until a == t or i > 100
a = t
repeat
j = j + 1
--print( "BUTTON", i, tostring( a ) )
a = a.prev
until a == t or j > 100
print( i, j, "BUTTONS" )
end
local k = 1
function t.new( b ) function t.new( b )
b = setmetatable( b or {}, t ) b = setmetatable( b or {}, t )
b.next = t b.next = t
t.prev.next = b t.prev.next = b
b.prev = t.prev b.prev = t.prev
t.prev = b t.prev = b
--nonsense
k = k + 1
print( "ADD BUTTON", k, tostring( b ) )
debugLoop()
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.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.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,
@ -70,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 )
@ -96,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
@ -108,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 }
@ -21,20 +21,28 @@ function Camera.GetNodeCoordinate( x, y )
end end
function Camera.Zoom( delta ) function Camera.Zoom( delta )
local scale = 1.0 + delta
if Camera.zoom < 25.0 and delta > 0 or --zooming in if Camera.zoom < 25.0 and delta > 0 or --zooming in
Camera.zoom > 0.5 and delta < 0 then --zooming out Camera.zoom > 0.5 and delta < 0 then --zooming out
return Camera.Set( Camera.x , Camera.y, Camera.w * scale, Camera.h * scale )
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
end 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 w, h. --In world coordinates: top left corner at x, y, extent of 1/w, 1/h.
function Camera.Set( x, y, w, h ) function Camera.Set( x, y, w, h )
Camera.x, Camera.y, Camera.w, Camera.h = x, y, w, h Camera.x, Camera.y, Camera.w, Camera.h = x, y, w, h
Camera.zoom = w / 800 Camera.zoom = w / 800
@ -42,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
@ -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 )

61
ui/savemodal.lua Normal file
View File

@ -0,0 +1,61 @@
--Modal for setting save options.
local love = assert( love )
local modal = require( "ui.modal" )
local button = require( "ui.button" )
local map = require( "map.map" )
local save = require( "ui.saveprogress" )
local t = { w = love.graphics.getWidth(), h = 200 }
local saveLocation = false
local floppy = love.graphics.newImage( "icons/save.png" )
floppy:setFilter( "nearest", "nearest" )
local saveButton = button.new{
group = t,
name = "save",
align = "left",
callback = function() save:start() end,
visible = false,
icon = floppy,
w = 400 - button.x,
h = 64,
y = 0,
}
local xIcon = love.graphics.newImage( "icons/x.png" )
xIcon:setFilter( "nearest", "nearest" )
local cancelButton = button.new{
group = t,
name = " cancel",
align = "left",
visible = false,
icon = xIcon,
callback = function() return t:stop() end,
y = 68,
w = 400 - button.x,
h = 64,
}
function t.start()
modal.start( t )
saveLocation = saveLocation or map.path
button.selected = saveButton
saveButton.name = " save to "..saveLocation
button.displayGroup( t, false, true )
end
function t.draw()
love.graphics.clear( 0,0,0,1 )
love.graphics.setColor( 1, 1, 1, 0.9 )
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 )

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