Compare commits

..

2 Commits

Author SHA1 Message Date
wan-may 7f457daa94 fixed the camera so its motion is no longer tied to framerate
limited minimum and maximum zoom on camera
changed point scaling so that cities get smaller when zooming in
changed selection cursor to hollow circle
changed capital cities to orange squares
included data/earth/ directory so that files are loaded from there when they're not available in the mod folder
when cities are hidden, hovering over travel nodes will display the travel node location (broken for now)
when cities and travel nodes are hidden, hovering over ai markers will display their details
made the information boxes more opaque
changed border-calculating code so that it works with higher resolutions
2024-04-24 22:09:38 -03:00
wan-may ce15bfdcf7 add drag and drop, parse lines more robustly 2024-04-21 13:07:56 -03:00
17 changed files with 225 additions and 135 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
build/

34
ai.lua
View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 KiB

BIN
icons/eye.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -9,18 +9,27 @@ function t.load( filename )
local n = 0
local k = 1
for line in assert( lfs.lines( filename ) ) do
if line == "b" then
if line:find "b" then
k = 1
n = n + 1
if #poly > 2 then n = n + 1 end
poly = {}
polys[n] = poly
else
local _, _, x, y = line:find( "(%g+)%s+(%g+)" )
x, y = assert( tonumber( x ) ), assert( tonumber( y ) )
x, y = tonumber( x ), tonumber( y )
if x and y then
poly[k], poly[ k + 1 ] = x, y
k = k + 2
else
print( "LINES: malformed line:", filename, line )
end
end
end
if not polys[n] or (#(polys[n]) < 3) then
polys[n] = nil
n = n - 1
end
print( "LOADED", filename, n )
return setmetatable( polys, {__index = t } )

View File

@ -1,7 +1,6 @@
--Shitty acceleration structure: get closest point in set.
--Assumed to be in world coordinates: ( -180, 180 ) x ( -100, 100 )
local t = {}
local hashes = {}
local math = math
local hash = function( x, y, debug )
@ -9,9 +8,18 @@ local hash = function( x, y, debug )
return s
end
function t.getHashes( points )
return assert( points.hashes )
end
function t.getClosestPoint( points, x, y )
local hashes = t.getHashes( points )
local closePoints = hashes[hash( x, y )]
if not closePoints then return end
if not closePoints then
return
end
local distance = math.huge
local px, py, point
for k, v in pairs(closePoints) do
@ -29,6 +37,7 @@ function t.getClosestPoint( points, x, y )
end
function t.Edit( points, point, x, y )
local hashes = t.getHashes( points )
local h = hashes[hash( point.x, point.y )]
if h then
for i, p in pairs(h) do
@ -50,13 +59,21 @@ function t.Add( points, x, y )
end
function t.New( points )
local hashes = {}
for i = 1, #points do
local x, y = points[i].x, points[i].y
local h = hash( x, y )
hashes[h] = hashes[h] or {}
hashes[h][#hashes[h] + 1] = points[i]
end
return setmetatable( points, {__index = t } )
points.hashes = hashes
do
local count = 0
for k, v in pairs(hashes) do count = count + 1 end
print( "LOCATION QUERY. Points:", count )
end
return setmetatable( points, {__index = t} )
end
return t

View File

@ -1,4 +1,5 @@
local love = assert( love, "This tool requires LOVE: love2d.org" )
--assert( require('mobdebug') ).start() --remote debugger
local map = require 'map'
local button = require 'button'
local SAVEDIRECTORY = "out/"
@ -11,21 +12,25 @@ function love.load()
lfs.setIdentity( "dcearth", false )
assert( lfs.createDirectory( SAVEDIRECTORY.."data/earth" ))
assert( lfs.createDirectory( SAVEDIRECTORY.."data/graphics" ))
map.load()
love.graphics.setNewFont( 12, "mono" )
end
function love.directorydropped( path )
love.filesystem.mount( path, "" )
return map.load( path )
end
function love.update( dt )
local tx, ty = 0, 0
local moveCamera = false
if love.keyboard.isScancodeDown( "w" ) then moveCamera = true; ty = ty + 1 end
if love.keyboard.isScancodeDown( "a" ) then moveCamera = true; tx = tx - 1 end
if love.keyboard.isScancodeDown( "s" ) then moveCamera = true; ty = ty - 1 end
if love.keyboard.isScancodeDown( "d" ) then moveCamera = true; tx = tx + 1 end
if love.keyboard.isScancodeDown( "q" ) then Camera.Zoom( true ) end
if love.keyboard.isScancodeDown( "e" ) then Camera.Zoom( false ) end
if love.keyboard.isScancodeDown( "w" ) then moveCamera = true; ty = ty + 30 * dt end
if love.keyboard.isScancodeDown( "a" ) then moveCamera = true; tx = tx - 30 * dt end
if love.keyboard.isScancodeDown( "s" ) then moveCamera = true; ty = ty - 30 * dt end
if love.keyboard.isScancodeDown( "d" ) then moveCamera = true; tx = tx + 30 * dt end
if love.keyboard.isScancodeDown( "q" ) then Camera.Zoom( dt ) end
if love.keyboard.isScancodeDown( "e" ) then Camera.Zoom( -dt ) end
if moveCamera then Camera.Translate( tx, ty ) end
@ -57,6 +62,10 @@ local layerButtons = {
function love.draw()
if not map.loaded then
return love.graphics.print( "Drag and drop folder to begin.")
end
love.graphics.push( "all" )
map.draw()
love.graphics.pop()
@ -76,7 +85,7 @@ function love.draw()
local wx, wy = Camera.GetWorldCoordinate( x, y )
local bx, by = Camera.GetBitmapCoordinate( x, y )
local h = love.graphics.getHeight() - 30
love.graphics.setColor( 0.2, 0.1, 0.1, 0.5 )
love.graphics.setColor( 0.2, 0.1, 0.1, 1.0 )
love.graphics.rectangle( "fill", 0, h, love.graphics.getWidth() / 2, 30 )
love.graphics.setColor( 1, 1, 1, 1 )
love.graphics.rectangle( "line", 0, h, love.graphics.getWidth() / 2, 30 )
@ -85,30 +94,13 @@ function love.draw()
--Edit box.
love.graphics.rectangle( "line", love.graphics.getWidth() / 2, h, love.graphics.getWidth() / 2, 30 )
if map.cities.selected then
local c = map.cities.selected
love.graphics.setColor( 0.2, 0.1, 0.1, 0.5 )
if map.selected then
love.graphics.setColor( 0.2, 0.1, 0.1, 1.0 )
love.graphics.rectangle( "fill", 0, 0, 150 ,100 )
love.graphics.setColor( 1, 1, 1, 1 )
love.graphics.rectangle( "line", 0, 0, 150 ,100 )
love.graphics.setColor( 1.2, 1.1, 1.1, 1.5 )
love.graphics.print( ("NAME: %s\nX: %3.2f\nY: %3.2f\nPOP: %d\nCAPITAL: %s\nCOUNTRY: %s"):format(c.name, c.x, c.y, c.pop, tostring(c.capital), c.country), 0, 0 )
elseif map.travelnodes.selected then
local c = map.travelnodes.selected
love.graphics.setColor( 0.2, 0.1, 0.1, 0.5 )
love.graphics.rectangle( "fill", 0, 0, 150 ,100 )
love.graphics.setColor( 1, 1, 1, 1 )
love.graphics.rectangle( "line", 0, 0, 150 ,100 )
love.graphics.setColor( 1.2, 1.1, 1.1, 1.5 )
love.graphics.print( ("Node: %d\nX: %3.2f\nY: %3.2f\n"):format(c.number, c.x, c.y) )
elseif map.ainodes.selectedNode then
local c = map.ainodes.selected
love.graphics.setColor( 0.2, 0.1, 0.1, 0.5 )
love.graphics.rectangle( "fill", 0, 0, 150 ,100 )
love.graphics.setColor( 1, 1, 1, 1 )
love.graphics.rectangle( "line", 0, 0, 150 ,100 )
love.graphics.setColor( 1.2, 1.1, 1.1, 1.5 )
love.graphics.print( ("Node: %d\nX: %3.2f\nY: %3.2f\noffensive: %s"):format(c.number, c.x, c.y, c.attack) )
love.graphics.print( map.selected:formatDisplayInfo(), 0, 0 )
end
end
@ -117,21 +109,29 @@ function love.resize(w, h)
end
function love.wheelmoved(x, y)
Camera.Zoom( (y > 0) and true or false )
Camera.Zoom( (y > 0) and 0.5 or -0.5 )
end
function love.mousepressed( x, y, button, istouch, presses )
local wx, wy = Camera.GetWorldCoordinate( x, y )
if map.loaded then
if button == 1 then
map.cities.lockSelection()
else
map.cities.unlockSelection()
end
end
print( ("MOUSE\tx %f\ty %f\twx %f\twy %f"):format(x, y, wx, wy) )
end
function love.mousemoved( x, y, dx, dy, istouch )
map.cities.selectNearestCity( Camera.GetWorldCoordinate( x, y ) )
local wx, wy = Camera.GetWorldCoordinate( x, y )
if map.loaded then
if map.cities.visible then map.selected = map.cities:selectNearest( wx, wy )
elseif map.travelnodes.visible then map.selected = map.travelnodes:selectNearest( wx, wy )
elseif map.ainodes.visible then map.selected = map.ainodes:selectNearest( wx, wy )
end
end
end
local function ToggleVisibility( layer )
@ -155,7 +155,9 @@ local layerVisibilityKeybinds = {
["8"] = "coastlines",
["9"] = "coastlinesLow",
["0"] = "international",
["-"] = "cities"
["-"] = "cities",
["="] = "travelnodes",
["backspace"] = "ainodes",
}
function love.keypressed(key)

43
map.lua
View File

@ -2,12 +2,12 @@ local lg = love.graphics
local AI = require 'ai'
local Cities = require 'cities'
local Lines = require 'lines'
local Nodes = require 'nodes'
local Bitmap = require 'bmp'
local Nodes = require 'travelNodes'
local Camera = require 'camera'
local Territory = require 'territory'
local map = {
loaded = false,
coastlines = false,
coastlinesLow = false,
international = false,
@ -21,11 +21,11 @@ local map = {
},
travelnodes = false,
sailable = false,
aimarkers = false,
ainodes = false,
cities = false
}
function map.load()
function map.load( )
map.cities = Cities.load( "data/earth/cities.dat" )
map.coastlines = Lines.load( "data/earth/coastlines.dat" )
map.coastlinesLow = Lines.load( "data/earth/coastlines-low.dat" )
@ -36,10 +36,12 @@ function map.load()
for k, v in pairs(map.territory) do
map.territory[k] = Territory.load( "data/earth/"..k..".bmp", k )
end
map.loaded = true
end
function map.draw()
lg.clear( 0, 0, 0, 1 )
if not map.loaded then return end
do --territory
lg.setColor( 1,1,1,1)
@ -55,13 +57,9 @@ function map.draw()
v:drawBorder( "sea" )
end
end
if map.sailable.visible then map.sailable:draw() end
lg.setBlendMode( "alpha" )
if map.sailable.visible then
map.sailable:draw()
lg.setColor( 1, 1, 1, 1 )
end
do --borders
lg.setLineJoin( "none" )
lg.setLineWidth( 1 / Camera.zoom )
@ -71,25 +69,35 @@ function map.draw()
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 )
do --points
if map.selected then
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
if map.cities.visible then --points
lg.setColor( 1, 0, 0, 0.5 )
lg.setPointSize( 0.5 * Camera.zoom )
lg.setPointSize( 5.0 )
map.cities.draw()
lg.setColor( 1, 1, 1.0, 0.5 )
lg.setPointSize( 1.0 * Camera.zoom )
lg.setColor( 1, 1, 0.0, 0.5 )
map.cities.drawCapitals()
lg.setColor( 1, 1, 1, 0.5 )
map.cities.drawSelected( 15.0 / Camera.zoom )
end
if map.ainodes.visible then
lg.setPointSize( 5.0 )
map.ainodes:draw()
end
@ -115,8 +123,11 @@ function map.draw()
do --travel nodes
lg.replaceTransform( Camera.tfNodes )
if map.travelnodes.visible then
map.travelnodes:draw()
end
end
end

View File

@ -109,8 +109,9 @@ function t.computeBorder( territory, threshold, key )
local border = territory[key]
local n = 1
for x = 0, 511 do
for y = 0, 284 do
local w, h = territory.imgd:getWidth() - 1, territory.imgd:getHeight() - 1
for x = 0, w do
for y = 0, h do
--Bottom left, bottom right, and top right of pixel in image coordinates:
local blx, bly = x, y + 1
local brx, bry = x + 1, y + 1

View File

@ -1,10 +1,13 @@
--Manage the pathfinding nodes used by DEFCON.
--This is important for a mapping tool because the DEFCON client will not load a map unless
--the pathfinding nodes form a connected graph.
local t = {}
local bmp = require 'bmp'
local locationQuery = require 'locationQuery'
local lg = assert( love.graphics )
local t = setmetatable({}, {__index = locationQuery})
local isSailable
local function isConnected( startNode, endNode )
@ -25,15 +28,17 @@ local function isConnected( startNode, endNode )
return true
end
function t.getClosest( nodes, x, y )
local d = math.huge
local closestNode
for _, node in pairs( nodes.nodes ) do
local nx, ny = node.x, node.y
local nd = (nx - x) * (nx - x) + (ny - y) * (ny - y)
if nd < d then d = nd; closestNode = node end
end
return closestNode
function t.selectNearest( nodes, x, y )
return (nodes.nodes):getClosestPoint( x, y )
end
local travelNode = {}
local mtTravelNode = { __index = travelNode }
function travelNode:formatDisplayInfo()
return ([[ TRAVEL NODE: %d
LONGITUDE: %3.2f
LATITUDE: %3.2f]]):format( self.idx, self.x, self.y )
end
function t.load( filename, sailable )
@ -49,10 +54,10 @@ function t.load( filename, sailable )
if imgd:getPixel( x, 399 - y ) > 0 then
local long = 360 * ( x - 800 ) / 800 - 360 / 2 + 360
local lat = 360 * ( 600 / 800 ) * ( 600 - y ) / 600 - 180
nodes.nodes[n] = {x = long, y = lat}
nodes.nodes[n] = setmetatable({x = long, y = lat, idx = n}, mtTravelNode )
print( nodes.nodes[n]:formatDisplayInfo() )
nodes.points[ 2 * n - 1 ] = long
nodes.points[ 2 * n ] = lat
print( n, long, lat )
n = n + 1
end
end
@ -69,7 +74,9 @@ function t.load( filename, sailable )
end
print( "=== Nodes Loaded ===" )
return setmetatable( nodes, {__index = t } )
nodes.nodes = locationQuery.New( nodes.nodes )
setmetatable( nodes, {__index = t} )
return nodes
end
--Determine if graph has more than one connected component.
@ -78,9 +85,13 @@ function t.isConnected( nodes )
end
function t.draw( nodes )
lg.setPointSize( 10 )
lg.setPointSize( 8 )
lg.setColor( 1, 1, 1, 0.5 )
lg.points( nodes.points )
return t.drawConnections( nodes )
end
function t.drawConnections( nodes )
for i, connection in pairs( nodes.connections ) do
for j in pairs( connection ) do
@ -88,9 +99,6 @@ function t.draw( nodes )
lg.line( ix, iy, fx, fy )
end
end
end
function t.drawConnections( nodes )
end