sketch out more menus, implement editing and deleting cities, quantize 8-bit sailables back to 4

This commit is contained in:
wan-may 2024-07-20 14:43:09 -03:00
parent 1f2899077c
commit db5ce8b4f7
20 changed files with 293 additions and 100 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

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

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

View File

@ -64,14 +64,16 @@ 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
if y < mainmenu.menuHeight then
button.selectIn( x, y ) button.selectIn( x, y )
--mouse on map --mouse on map
else
if map.selectionLocked then return end if map.selectionLocked then return end
if map.editLayer and map.editLayer.selectNearest then if map.editLayer and map.editLayer.selectNearest then
map.selected = map.editLayer:selectNearest( Camera.GetWorldCoordinate( x, y ) ) 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)

View File

@ -17,6 +17,14 @@ function aiNode:formatDisplayInfo()
]]):format( self.idx, self.x, self.y, tostring(self.attacking) ) ]]):format( self.idx, self.x, self.y, tostring(self.attacking) )
end end
function aiNode:add()
end
function aiNode:moveTo( x, y )
end
function t.load( filename ) function t.load( filename )
local img, imgd = bmp.load( filename ) local img, imgd = bmp.load( filename )
local nodes = { local nodes = {
@ -82,4 +90,8 @@ function t.save( nodes )
return bmp.ai( nodes.all ) return bmp.ai( nodes.all )
end end
function t.newNode( isAttacking )
end
return t return t

View File

@ -244,6 +244,29 @@ function formats.sailable:test()
print "sailable OK" print "sailable OK"
end 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 ) function formats.sailable:encode( data )
local w, h = self.w, self.h local w, h = self.w, self.h
local bytes = { self.header:sub( 1, -2 ) } local bytes = { self.header:sub( 1, -2 ) }
@ -252,7 +275,7 @@ function formats.sailable:encode( data )
--y coordinates are written top to bottom --y coordinates are written top to bottom
for y = h - 1, 0, -1 do for y = h - 1, 0, -1 do
for x = 0, w - 1 do for x = 0, w - 1 do
bytes[i] = assert( self.palette[ math.floor( data:getPixel(x, y) * 255 )] ) bytes[i] = sailableQuantize( data:getPixel(x, y) )
i = i + 1 i = i + 1
end end
end end
@ -276,7 +299,7 @@ function formats.africa:test()
test.compareData( love.filesystem.read( filename ), encoded ) test.compareData( love.filesystem.read( filename ), encoded )
print "africa OK" print "africa OK"
end end
end
function formats.africa:encode( data ) function formats.africa:encode( data )
local w, h = self.w, self.h local w, h = self.w, self.h

View File

@ -13,6 +13,7 @@ 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
@ -54,7 +55,14 @@ function city:formatDisplayInfo()
end end
function city:delete() 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 end
function city:add() function city:add()
@ -81,7 +89,17 @@ function city:moveTo(x, y)
end end
function city:toggleCapital() function city:toggleCapital()
self.capital = not( self.capital ) 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 end
function t.newCity( tbl ) function t.newCity( tbl )
@ -142,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
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 ) 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

@ -191,18 +191,24 @@ local function write( filename, string )
end end
function map.save() function map.save()
--should be cross platform-ish --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 for _, folder in ipairs{ "/data/", "/data/earth/", "/data/graphics/" } do
--getInfo checks if a directory exists
assert( mkdir.exists( map.path ), map.path ) assert( mkdir.exists( map.path ), map.path )
local path = map.path..folder local path = map.path..folder
if not mkdir.exists( path ) then mkdir.mkdir( path ) end if not mkdir.exists( path ) then mkdir.mkdir( path ) end
end end
--OK back to normal 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 for k, layer in pairs( layers ) do
write( map.path..tostring( layer.filename ), assert( layer:save() ) ) files[ map.path..tostring( layer.filename ) ] = assert( layer:save() )
end end
for filename, str in pairs( files ) do
write( filename, str )
end
end end
function map.hover(x, y) function map.hover(x, y)

View File

@ -11,6 +11,7 @@ local t = {
h = 24, h = 24,
group = false, group = false,
visible = false, 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
@ -45,7 +46,7 @@ function t.draw( b )
b.x + (b.icon and b.h or 0), b.x + (b.icon and b.h or 0),
b.y + 0.5 * ( b.h - lg.getFont():getHeight() ), b.y + 0.5 * ( b.h - lg.getFont():getHeight() ),
b.w - (b.icon and b.h or 0), b.w - (b.icon and b.h or 0),
"center" ) b.align )
if b.icon then if b.icon then
local h = b.icon:getHeight() local h = b.icon:getHeight()
lg.draw( b.icon, lg.draw( b.icon,

View File

@ -40,8 +40,8 @@ function t.start()
modal.start( t ) modal.start( t )
loadLocation = loadLocation or map.path loadLocation = loadLocation or map.path
button.selected = loadButton button.selected = loadButton
loadButton.name = "save to "..loadLocation loadButton.name = "load from "..loadLocation
button.displayGroup( t, true ) button.displayGroup( t, false, true )
end end
function t.draw() function t.draw()

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

View File

@ -22,27 +22,29 @@ end
--and clicking escape will clear the previously selected mode --and clicking escape will clear the previously selected mode
local selectModal = modal.new{} local selectModal = modal.new{}
local textModal = modal.new{} local textModal = modal.new{}
local numberModal = modal.new{ cursor = 1 } local numberModal = modal.new{}
local editModal = modal.new{ mousepressed = button.mousepressed, keypressed = keypressed, } local editModal = modal.new{ mousepressed = button.mousepressed, keypressed = keypressed, }
local moveModal = modal.new{ mousepressed = button.mousepressed, keypressed = keypressed, mousemoved = button.selectIn } local moveModal = modal.new{ mousepressed = button.mousepressed, keypressed = keypressed, mousemoved = button.selectIn }
local deleteModal = modal.new{ mousepressed = button.mousepressed, keypressed = keypressed } local deleteModal = modal.new{ keypressed = keypressed }
local currentMode local currentMode
button.new{ name = "NEW CITY", button.new{ name = "NEW CITY",
group = t, group = t,
icon = lg.newImage("icons/layer-cities.png"), icon = lg.newImage("icons/city-new.png"),
x = 615, x = 615,
y = 0, y = 0,
callback = function() callback = function()
city = map.cities.newCity() city = map.cities.newCity()
city:add()
return editModal:start() return editModal:start()
end end
} }
moveModal.button = button.new{ name = "MOVE CITY", moveModal.button = button.new{ name = "MOVE CITY",
group = t, group = t,
icon = lg.newImage("icons/city-move.png"),
x = 615, x = 615,
y = 28, y = button.h + 4,
callback = function( self ) callback = function( self )
self.lit = true self.lit = true
selectModal.mode = moveModal selectModal.mode = moveModal
@ -53,7 +55,7 @@ moveModal.button = button.new{ name = "MOVE CITY",
editModal.button = button.new{ name = "EDIT CITY", editModal.button = button.new{ name = "EDIT CITY",
group = t, group = t,
x = 615, x = 615,
y = 28 * 2, y = (button.h + 4) * 2,
callback = function( self ) callback = function( self )
self.lit = true self.lit = true
selectModal.mode = editModal selectModal.mode = editModal
@ -63,48 +65,57 @@ editModal.button = button.new{ name = "EDIT CITY",
deleteModal.button = button.new{ name = "DELETE CITY", deleteModal.button = button.new{ name = "DELETE CITY",
group = t, group = t,
icon = lg.newImage("icons/city-delete.png"),
x = 615, x = 615,
y = 28 * 3, y = (button.h + 4) * 3,
icon = lg.newImage("icons/x.png"),
callback = function( self ) callback = function( self )
self.lit = true self.lit = true
selectModal.mode = deleteModal return deleteModal:start()
return selectModal:start()
end, end,
} }
--editButtons --editButtons
local function editText( self ) local function editText( self )
print( "editing: ", self.field, city.name ) print( "editing: ", self.field, city.name )
self.lit = true
return textModal:start( self.field ) return textModal:start( self.field )
end end
local function editNumber( self ) local function editNumber( self )
local field = self.field print( "editing: ", self.field, city.name )
self.lit = true
return numberModal:start( self.field )
end end
local editButtons = { local editButtons = {
save = button.new{ save = button.new{
icon = lg.newImage( "icons/save.png" ), icon = lg.newImage( "icons/check.png" ),
callback = function() print( "stop editing city" ) return editModal:stop() end,}, callback = function() return editModal:stop() end,},
name = button.new{ callback = editText }, name = button.new{ callback = editText },
country = button.new{ callback = editText }, country = button.new{ callback = editText },
x = button.new{ callback = editNumber }, x = button.new{ callback = editNumber },
y = button.new{ callback = editNumber }, y = button.new{ callback = editNumber },
capital = button.new{ callback = function() if city then return city:toggleCapital() end end }, capital = button.new{ callback = function( self )
if city then
self.name = tostring( not( city.capital ) )
return city:toggleCapital()
end
end },
} }
do do
local i = 0 local i = 0
for key, b in pairs( editButtons ) do for _, key in ipairs{ "save", "name", "country", "x", "y", "capital" } do
local b = assert( editButtons[ key ] )
b.align = "right"
b.field = key b.field = key
b.name = key b.name = tostring( key ) --bools must be cast to string before getting passed to printf
b.group = editModal b.group = editModal
b.x = 0 b.x = lg.getWidth() / 2 - button.w / 2
b.y = 28 * i b.y = (button.h + 4) * i
i = i + 1 i = i + 1
end end
end end
@ -113,17 +124,34 @@ function editModal:start()
modal.start( self ) modal.start( self )
button.displayGroup( self, false, true ) button.displayGroup( self, false, true )
for k, b in pairs( editButtons ) do for k, b in pairs( editButtons ) do
b.name = city[k] or b.name b.name = tostring( city[k] or b.name )
end end
if city.capital == false then editButtons.capital.name = "false" end
end
function editModal:stop()
return modal.stop( self )
end end
function editModal.draw() function editModal.draw()
lg.setColor( 1, 1, 1, 0.5 )
return button:draw() return button:draw()
end end
function editModal.resize( w, h )
local i = 0
local high = h / 6
for key, b in pairs( editButtons ) do
b.x = w / 2 - button.w / 2
b.y = ( high + 4 ) * i
b.h = high
i = i + 1
end
end
function moveModal.update( dt ) function moveModal.update( dt )
local x, y = love.mouse.getPosition() local x, y = love.mouse.getPosition()
if y > 200 and love.mouse.isDown( 1 ) then if y > t.menuHeight and love.mouse.isDown( 1 ) then
local wx, wy = camera.GetWorldCoordinate( x, y ) local wx, wy = camera.GetWorldCoordinate( x, y )
city:moveTo( wx, wy ) city:moveTo( wx, wy )
end end
@ -134,7 +162,7 @@ function moveModal.mousemoved( x, y, dx, dy, istouch )
end end
function moveModal.mousepressed( x, y, mouseButton, istouch, presses ) function moveModal.mousepressed( x, y, mouseButton, istouch, presses )
if y < 200 then if y < t.menuHeight then
moveModal:stop() moveModal:stop()
return button.mousepressed( x, y, mouseButton, istouch, presses ) return button.mousepressed( x, y, mouseButton, istouch, presses )
end end
@ -143,19 +171,48 @@ function moveModal.mousepressed( x, y, mouseButton, istouch, presses )
end end
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 ) function numberModal:start( field )
self.field = field self.field = field
return modal.start( self ) return modal.start( self )
end end
function numberModal.keypressed( key, code, isrepeat ) function numberModal.keypressed( key, code, isrepeat )
if code == 'backspace' then end if code == "backspace" then
if code == 'escape' then return numberModal:stop() end 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 end
function numberModal.textinput( char ) function numberModal.textinput( char )
local str = tostring( city[ numberModal.field ] ) local str = tostring( city[ numberModal.field ] )
local plus = str..char local plus = str..char
print( "text input: ", char )
if tonumber( plus ) then if tonumber( plus ) then
city[ numberModal.field ] = plus city[ numberModal.field ] = plus
editButtons[ numberModal.field ].name = plus editButtons[ numberModal.field ].name = plus
@ -196,7 +253,7 @@ function textModal.keypressed( key, code, isRepeat )
editButtons[textModal.field].name = city[textModal.field] editButtons[textModal.field].name = city[textModal.field]
end end
end end
if code == "escape" then if code == "escape" or code == "return" then
return textModal:stop() return textModal:stop()
end end
end end
@ -215,7 +272,7 @@ function selectModal:stop()
end end
function selectModal.mousepressed( x, y, mouseButton, istouch, presses ) function selectModal.mousepressed( x, y, mouseButton, istouch, presses )
if y < 200 then if y < t.menuHeight then
selectModal:stop() selectModal:stop()
return button.mousepressed( x, y, mouseButton, istouch, presses ) return button.mousepressed( x, y, mouseButton, istouch, presses )
end end
@ -226,6 +283,8 @@ function selectModal.mousepressed( x, y, mouseButton, istouch, presses )
end end
end end
function t.setMenuHeight( h )
t.menuHeight = h
end
return t return t

View File

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

View File

@ -1,11 +1,10 @@
local love = assert( love ) local love = assert( love )
local button = require 'ui.button' local button = require 'ui.button'
local savemodal = require 'ui.savemodal' local modal = require 'ui.modal'
local loadmodal = require 'ui.loadmodal' local camera = require 'ui.camera'
local Camera = require 'ui.camera'
local map = require 'map.map' local map = require 'map.map'
local t = {} local t = { menuHeight = 200 }
local loadImg = love.graphics.newImage local loadImg = love.graphics.newImage
local layers = { local layers = {
@ -27,12 +26,12 @@ local layers = {
button.new{ button.new{
name = "LOAD", x = 250, y = 0, name = "LOAD", x = 250, y = 0,
group = t, group = t,
callback = loadmodal.start, callback = require( 'ui.loadmodal' ).start,
icon = love.graphics.newImage( "icons/load.png" )} icon = love.graphics.newImage( "icons/load.png" )}
button.new{ button.new{
name = "SAVE", x = 250, y = 28, name = "SAVE", x = 250, y = 28,
group = t, group = t,
callback = savemodal.start, callback = require( 'ui.savemodal' ).start,
icon = love.graphics.newImage( "icons/save.png" )} icon = love.graphics.newImage( "icons/save.png" )}
button.new{ button.new{
name = "UNDO", x = 250, y = 2 * 28, name = "UNDO", x = 250, y = 2 * 28,
@ -61,9 +60,13 @@ end
local activeLayerButton local activeLayerButton
local function back( self ) local function back( self )
print( "back button clicked" )
if activeLayerButton then
activeLayerButton.lit = false activeLayerButton.lit = false
end
activeLayerButton = nil activeLayerButton = nil
map.setEditLayer() map.setEditLayer()
modal.exitAll()
button.displayGroup( t, false, true ) button.displayGroup( t, false, true )
for i, b in ipairs( editButtons ) do for i, b in ipairs( editButtons ) do
b.visible = true b.visible = true
@ -86,6 +89,7 @@ local backButton = button.new{
} }
local function editLayer( self ) local function editLayer( self )
back( backButton )
self.lit = true self.lit = true
map.setEditLayer( self.layer ) map.setEditLayer( self.layer )
activeLayerButton = self activeLayerButton = self
@ -106,6 +110,8 @@ end
local x = 250 local x = 250
for i = 1, #layers do for i = 1, #layers do
layers[i].menu.menuHeight = t.menuHeight
editButtons[i] = button.new( copy( i, { editButtons[i] = button.new( copy( i, {
y = 3 * 28, y = 3 * 28,
x = x + (button.h + 4) * ( i - 1 ), x = x + (button.h + 4) * ( i - 1 ),
@ -126,17 +132,20 @@ for i = 1, #layers do
group = showButtons, group = showButtons,
tooltip = "show "..layers[i].layer tooltip = "show "..layers[i].layer
})) }))
layerButtons[ 2 * i - 1 ] = showButtons[i] layerButtons[ 2 * i - 1 ] = showButtons[i]
end end
function t.draw() function t.draw()
--Status bar. --Status bar.
love.graphics.setScissor( 0, 0, 250, 200 ) love.graphics.setScissor( 0, 0, 250, t.menuHeight )
local x, y = love.mouse.getPosition() local x, y = love.mouse.getPosition()
local wx, wy = Camera.GetWorldCoordinate( x, y ) local wx, wy = camera.GetWorldCoordinate( x, y )
local bx, by = Camera.GetBitmapCoordinate( x, y ) local bx, by = camera.GetBitmapCoordinate( x, y )
local h = love.graphics.getHeight() - 60 local h = love.graphics.getHeight() - 60
love.graphics.setColor( 0, 0, 0, 1 ) love.graphics.setColor( 0, 0, 0, 1 )
love.graphics.rectangle( "fill", 0, 0, 250, love.graphics.getHeight() ) love.graphics.rectangle( "fill", 0, 0, 250, love.graphics.getHeight() )
@ -156,10 +165,10 @@ function t.draw()
if map.selected then love.graphics.print( map.selected:formatDisplayInfo(), 0, 80 ) end if map.selected then love.graphics.print( map.selected:formatDisplayInfo(), 0, 80 ) end
if map.selectionLocked then end if map.selectionLocked then end
love.graphics.setScissor( 250, 0, love.graphics.getWidth() - 250, 200 ) love.graphics.setScissor( 250, 0, love.graphics.getWidth() - 250, t.menuHeight)
love.graphics.rectangle( "line", 0, 0 , 250, 200 ) love.graphics.rectangle( "line", 0, 0 , 250, t.menuHeight )
love.graphics.rectangle( "line", 250, 0, love.graphics.getWidth() - 250, 200 ) love.graphics.rectangle( "line", 250, 0, love.graphics.getWidth() - 250, t.menuHeight )
love.graphics.rectangle( "line", 250, 0, button.w, 200 ) love.graphics.rectangle( "line", 250, 0, button.w, t.menuHeight )
love.graphics.setColor( 1, 1, 1, 0.8 ) love.graphics.setColor( 1, 1, 1, 0.8 )

View File

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

View File

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

View File

@ -34,7 +34,6 @@ function t.start( self )
end end
function t.stop( self ) function t.stop( self )
print( "stopping modal:", i )
--restore callbacks --restore callbacks
for name in pairs( self ) do for name in pairs( self ) do
if love[name] then if love[name] then
@ -59,6 +58,12 @@ function t.stop( self )
love.graphics.setScissor(0, 0, love.graphics.getDimensions()) love.graphics.setScissor(0, 0, love.graphics.getDimensions())
end end
function t.exitAll()
if i < 1 then return end
i = 1
return t.stop( love )
end
function t.new( modal ) function t.new( modal )
return setmetatable( modal or {}, t ) return setmetatable( modal or {}, t )
end end