2015-03-25 01:38:53 -04:00
require " level_editor/object_attributes "
require " level_editor/gui "
editorView = " room " --can be "off", "room", "world", "asset"
selectedColor = " red "
local bacSprite = love.graphics . newImage ( " art/_ui/bac.png " )
local forwardSprite = love.graphics . newImage ( " art/_ui/forward.png " )
local folderSprite = love.graphics . newImage ( " art/_ui/folder.png " )
local cancelSprite = love.graphics . newImage ( " art/_ui/cancel.png " )
local confirmSprite = love.graphics . newImage ( " art/_ui/confirm.png " )
2026-08-07 01:39:47 -04:00
local clearSavesSprite = love.graphics . newImage ( " art/_ui/clear_saves.png " )
2015-03-25 01:38:53 -04:00
local selectedFolder = nil
local selectedAsset = nil
local selectedRoom = nil
local selectedRow = 1
2015-03-25 04:22:10 -04:00
local gameAssets = { }
2015-03-25 01:38:53 -04:00
local inputText = " "
local assetProperties = { }
2015-03-25 03:58:37 -04:00
local hierarchy = { }
2015-03-25 01:38:53 -04:00
local activeField = nil --this is for whenwe are doing text input things
local uiButtons = { }
local folderButtons = { }
local assetButtons = { { } } --table for rows so we can do stuff
local editingAssetButtons = { }
local labels = { }
2026-08-07 01:39:47 -04:00
local editorRoomName = nil
local roomNameField = nil
local editorStatus = nil
local clearSaveConfirmationExpires = 0
2026-08-07 03:22:28 -04:00
local clearSaveConfirmationScope = nil
2026-08-07 01:39:47 -04:00
local worldSeedConfirmationExpires = 0
local worldMode = " place "
local worldRoomButtons = { }
local worldControls = { }
local worldPage = 1
local worldPageSize = 5
local function buttonContains ( button , x , y )
return x > button.x and x < button.x + button.w and y > button.y and y < button.y + button.h
end
local function rebuildWorldRoomButtons ( )
worldRoomButtons = { }
local names = gameWorld : listRoomNames ( )
local pages = math.max ( 1 , math.ceil ( # names / worldPageSize ) )
worldPage = constrain ( worldPage , 1 , pages )
local first = ( worldPage - 1 ) * worldPageSize + 1
for index = first , math.min ( first + worldPageSize - 1 , # names ) do
local roomName = names [ index ]
local row = index - first
table.insert ( worldRoomButtons , { name = roomName , x = 10 , y = 36 + row * 68 , w = sideBarBox.w - 20 , h = 60 } )
end
worldControls = {
{ text = worldMode == " place " and " MODE: PLACE ROOMS " or " MODE: OPEN LEVELS " , x = 10 , y = sideBarBox.h - 194 , w = sideBarBox.w - 20 , h = 34 ,
onClic = function ( ) worldMode = worldMode == " place " and " jump " or " place " ; rebuildWorldRoomButtons ( ) end } ,
{ text = " RESEED OLD EXITS " , x = 10 , y = sideBarBox.h - 152 , w = sideBarBox.w - 20 , h = 34 ,
onClic = function ( )
local now = love.timer . getTime ( )
if now > worldSeedConfirmationExpires then
worldSeedConfirmationExpires = now + 3
editorStatus = " Click RESEED OLD EXITS again within 3 seconds to replace the current map. "
return
end
worldSeedConfirmationExpires = 0
local result = seedWorldFromLegacyExits ( gameWorld )
editorStatus = " Seeded " .. result.placed .. " rooms; " .. result.links .. " legacy links ( " .. result.conflicts .. " conflicts kept separate). "
rebuildWorldRoomButtons ( )
end } ,
{ text = " < PAGE " .. worldPage .. " / " .. pages .. " > " , x = 10 , y = sideBarBox.h - 110 , w = sideBarBox.w - 20 , h = 34 ,
onClic = function ( buttonX )
if buttonX < sideBarBox.w / 2 then worldPage = math.max ( 1 , worldPage - 1 ) else worldPage = math.min ( pages , worldPage + 1 ) end
rebuildWorldRoomButtons ( )
end } ,
}
end
local function drawWorldSidebar ( )
love.graphics . setColor ( gColor.white : set ( ) )
love.graphics . printf ( " ROOMS — " .. ( worldMode == " place " and " select to place; re-click to clear " or " click a map room to open " ) , 8 , 10 , sideBarBox.w - 16 , " center " )
for _ , button in ipairs ( worldRoomButtons ) do
love.graphics . setColor ( button.name == selectedRoom and 255 or 100 , button.name == selectedRoom and 255 or 100 , button.name == selectedRoom and 255 or 100 , 255 )
love.graphics . rectangle ( " line " , button.x , button.y , button.w , button.h )
gameWorld : drawRoomPreview ( button.name , button.x + 4 , button.y + 4 , button.h - 8 )
love.graphics . setColor ( gColor.white : set ( ) )
love.graphics . printf ( button.name , button.x + button.h + 4 , button.y + 21 , button.w - button.h - 8 , " left " )
end
for _ , control in ipairs ( worldControls ) do
love.graphics . setColor ( 255 , 255 , 255 , 255 )
love.graphics . rectangle ( " line " , control.x , control.y , control.w , control.h )
love.graphics . printf ( control.text , control.x , control.y + 9 , control.w , " center " )
end
uiButtons.world : draw ( )
end
local function validRoomName ( name )
name = ( name or " " ) : match ( " ^%s*(.-)%s*$ " )
if name == " " then return nil , " Enter a level name. " end
if not name : match ( " ^[%w][%w_-]*$ " ) then
return nil , " Use letters, numbers, _ or - for level names. "
end
return name
end
local function beginRoomNameEdit ( )
activeField = " roomName "
inputText = " "
response = " "
roomNameField : editText ( " " )
editorStatus = " Type a name, press Enter, then Save. "
end
local function saveRoomFromEditor ( )
local requestedName = editorRoomName or currentRoom.name
if activeField == " roomName " then requestedName = inputText end
local name , err = validRoomName ( requestedName )
if not name then
editorStatus = err
return
end
local oldName = currentRoom.name
if name ~= oldName and readFromSource ( " rooms/ " .. name .. " .sav " ) then
editorStatus = " A source level named ' " .. name .. " ' already exists. "
return
end
2026-08-07 03:22:28 -04:00
local savePlayerPositions = love.keyboard . isDown ( " lshift " , " rshift " )
if not currentRoom : saveToSource ( name , savePlayerPositions , oldName ) then
2026-08-07 01:39:47 -04:00
editorStatus = " Could not save ' " .. name .. " '. "
return
end
-- A changed name is a rename, not an accidental duplicate. Do this only
-- after the new source file is safely written.
if oldName and oldName ~= name then
removeFromSource ( " rooms/ " .. oldName .. " .sav " )
gameWorld : renameRoom ( oldName , name )
end
gameWorld : invalidatePreview ( name )
if editorView == " world " then rebuildWorldRoomButtons ( ) end
currentRoom.name = name
editorRoomName = name
roomNameField : editText ( name )
activeField = nil
2026-08-07 03:22:28 -04:00
editorStatus = " Saved source level ' " .. name .. " ' " .. ( savePlayerPositions and " with player positions. " or " . " )
2026-08-07 01:39:47 -04:00
end
local function clearRuntimeSaves ( )
local now = love.timer . getTime ( )
2026-08-07 03:22:28 -04:00
local clearAll = love.keyboard . isDown ( " lshift " , " rshift " )
local roomName = currentRoom.name
local scope = clearAll and " all " or roomName
if now > clearSaveConfirmationExpires or clearSaveConfirmationScope ~= scope then
2026-08-07 01:39:47 -04:00
clearSaveConfirmationExpires = now + 3
2026-08-07 03:22:28 -04:00
clearSaveConfirmationScope = scope
if clearAll then
editorStatus = " Shift-click the clear-save icon again within 3 seconds to erase runtime saves for every room. "
else
editorStatus = " Click the clear-save icon again within 3 seconds to erase the runtime save for ' " .. ( roomName or " this room " ) .. " '. "
end
2026-08-07 01:39:47 -04:00
return
end
2026-08-07 03:22:28 -04:00
local removed = clearRuntimeSaveData ( clearAll and nil or roomName )
2026-08-07 01:39:47 -04:00
clearSaveConfirmationExpires = 0
2026-08-07 03:22:28 -04:00
clearSaveConfirmationScope = nil
2026-08-07 01:39:47 -04:00
if roomName then
currentRoom = Room : new { name = roomName }
setEditorRoomName ( currentRoom.name )
editorStatus = " Cleared " .. removed .. " runtime save item(s) and reloaded ' " .. roomName .. " .' "
else
editorStatus = " Cleared " .. removed .. " runtime save item(s). No named level to reload. "
end
end
2015-03-25 01:38:53 -04:00
function _initLevelEditor ( )
2015-03-25 04:22:10 -04:00
gameAssets = getAllAssets ( )
2026-08-07 01:39:47 -04:00
editorRoomName = " "
2015-03-25 01:38:53 -04:00
--create folder buttons
local counter = 1
2015-03-25 03:58:37 -04:00
for asset , attributes in pairs ( gameAssets ) do
if not hierarchy [ gameAssets [ asset ] . directory ] then
hierarchy [ gameAssets [ asset ] . directory ] = { }
end
table.insert ( hierarchy [ gameAssets [ asset ] . directory ] , asset )
end
for folder , object in pairs ( hierarchy ) do
2015-03-25 01:38:53 -04:00
local button = createUIElement {
name = " folder " ,
text = folder ,
x = 30 ,
y = ( sideBarBox.h - 50 ) * ( counter / 15 ) ,
sprite = folderSprite ,
onClic = {
left = function ( self )
print ( " my asse " )
selectFolder ( folder )
end ,
right = function ( )
end
} ,
}
counter = counter + 1
table.insert ( folderButtons , button )
end
_addUIButton ( createUIElement {
name = " save " ,
sprite = love.graphics . newImage ( " art/_ui/save.png " ) ,
onClic = {
left = function ( )
if editorView == " room " then
2026-08-07 01:39:47 -04:00
saveRoomFromEditor ( )
2015-03-25 01:38:53 -04:00
end
end ,
right = function ( )
end
} ,
x = sideBarBox.w / 4 ,
y = sideBarBox.h * ( 9 / 10 )
} )
_addUIButton ( createUIElement {
name = " world " ,
sprite = love.graphics . newImage ( " art/_ui/world.png " ) ,
onClic = {
left = function ( )
2026-08-07 01:39:47 -04:00
if editorView == " world " then assetsView ( ) else worldView ( ) end
2015-03-25 01:38:53 -04:00
end ,
right = function ( )
end
} ,
x = sideBarBox.w * ( 3 / 4 ) ,
y = sideBarBox.h * ( 9 / 10 )
2026-08-07 01:39:47 -04:00
} , " world " )
_addUIButton ( createUIElement {
name = " clearSaves " ,
sprite = clearSavesSprite ,
onClic = {
left = clearRuntimeSaves ,
} ,
x = sideBarBox.w / 2 ,
y = sideBarBox.h * ( 9 / 10 ) ,
scale = drawScale / 3
2015-03-25 01:38:53 -04:00
} )
for _ , color in ipairs ( { " white " , " red " , " green " , " blue " } ) do
_addUIButton ( createUIElement {
name = color ,
sprite = love.graphics . newImage ( " art/_ui/ " .. color .. " .png " ) ,
onClic = {
left = function ( self )
if editorView == " room " then
setSelectedColor ( color )
end
end ,
right = function ( )
end
} ,
x = sideBarBox.w * ( _ / 5 ) ,
y = sideBarBox.h * ( 8 / 10 )
} )
end
2026-08-07 01:39:47 -04:00
roomNameField = createUIElement {
name = " roomName " ,
text = editorRoomName ,
x = 25 ,
y = sideBarBox.h * ( 7 / 10 ) ,
onClic = {
left = beginRoomNameEdit ,
}
}
_addUIButton ( roomNameField , " roomName " )
end
function setEditorRoomName ( name )
editorRoomName = name or " "
if roomNameField then roomNameField : editText ( editorRoomName ) end
2015-03-25 01:38:53 -04:00
end
function setSelectedColor ( color )
selectedColor = color
end
function selectFolder ( folder )
selectedFolder = folder
local bacButton = _addUIButton ( createUIElement {
name = " bacToFolderView " ,
sprite = bacSprite ,
x = sideBarBox.w / 2 ,
y = 37.5 ,
onClic = {
left = function ( self )
print ( self , self.x )
self = nil
folderView ( self )
end ,
right = function ( self ) return end
}
} , " bacToFolderView " )
local row = 1
local counter = 1
local limit = 10
2015-03-25 03:58:37 -04:00
for _ , asset in pairs ( hierarchy [ selectedFolder ] ) do
2015-03-25 01:38:53 -04:00
if counter % limit == 0 then
counter = 1
local forwardButton = createUIElement {
name = " forward " ,
sprite = forwardSprite ,
x = sideBarBox.w / 2 ,
y = sideBarBox.h - 250 ,
onClic = {
left = function ( )
selectedRow = selectedRow + 1
end ,
right = function ( )
end
}
}
table.insert ( assetButtons [ row ] , forwardbutton )
row = row + 1
table.insert ( assetButtons , { } )
end
local sprites = { }
2015-03-25 03:58:37 -04:00
for color , location in pairs ( gameAssets [ asset ] . sprites ) do
2015-03-25 01:38:53 -04:00
sprites [ color ] = love.graphics . newImage ( location )
end
local assetButton = createUIElement {
sprites = sprites ,
name = " asset " ,
text = asset ,
x = 30 ,
y = ( sideBarBox.h - 225 ) * ( counter / limit ) + 20 ,
onClic = {
left = function ( )
print ( asset )
selectAsset ( asset )
end ,
right = function ( )
editAsset ( asset )
end
}
}
table.insert ( assetButtons [ row ] , assetButton )
counter = counter + 1
end
end
function selectAsset ( asset )
print ( " selecting " , asset )
selectedAsset = asset
print ( selectedAsset )
end
function editAsset ( asset )
inputText = " "
editorView = " asset "
selectedAsset = asset
2015-03-25 03:58:37 -04:00
assetProperties = gameAssets [ selectedAsset ]
2015-03-25 01:38:53 -04:00
local cancelButton = createUIElement {
name = " cancel " ,
sprite = cancelSprite ,
x = sideBarBox.w / 3 ,
y = sideBarBox.h - 75 ,
onClic = {
left = function ( self )
assetsView ( )
end ,
right = function ( self ) return end
}
}
local confirmButton = createUIElement {
name = " confirm " ,
sprite = confirmSprite ,
x = sideBarBox.w * 2 / 3 ,
y = sideBarBox.h - 75 ,
onClic = {
left = function ( self )
saveAsset ( )
end ,
right = function ( self ) return end
}
}
editingAssetButtons [ cancelButton ] = cancelButton
editingAssetButtons [ confirmButton ] = confirmButton
local assetSprites = { }
2015-03-25 03:58:37 -04:00
for color , path in pairs ( assetProperties.sprites ) do
2015-03-25 01:38:53 -04:00
assetSprites [ color ] = love.graphics . newImage ( path )
end
local assetButton = createUIElement {
name = asset ,
sprites = assetSprites ,
x = sideBarBox.w / 2 - 18 ,
y = 80 ,
}
local classFieldLabel = {
draw = function ( ) love.graphics . printf ( " CLASS " , 0 , 125 , sideBarBox.w , " center " ) end
}
labels [ classFieldLabel ] = classFieldLabel
local classButton = createUIElement {
name = " class " ,
text = assetProperties.class or " " ,
x = 25 ,
y = 150 ,
onClic = {
left = function ( )
activeField = " class "
end
}
}
2015-03-25 03:58:37 -04:00
local preloadLabel = {
draw = function ( ) love.graphics . printf ( " preload " , 0 , 225 , sideBarBox.w , " center " ) end
}
labels [ preloadLabel ] = preloadLabel
local preloadButton = createUIElement {
name = " preload " ,
text = tostring ( assetProperties.preload ) ,
x = 25 ,
y = 250 ,
onClic = {
left = function ( )
assetProperties.preload = not assetProperties.preload
end
}
}
2015-03-25 01:38:53 -04:00
editingAssetButtons [ assetButton ] = assetButton
editingAssetButtons.class = classButton
2015-03-25 03:58:37 -04:00
editingAssetButtons.preload = preloadButton
2015-03-25 01:38:53 -04:00
end
function saveAsset ( )
2015-03-25 03:58:37 -04:00
gameAssets [ selectedAsset ] = assetProperties
2026-08-07 00:56:33 -04:00
-- the asset's class lives in its .meta next to the art (source of truth,
-- read back by getAllAssets); write it there
local metaPath = " art/ " .. assetProperties.directory .. " / " .. selectedAsset .. " .meta "
writeToSource ( metaPath , TSerial.pack ( { class = assetProperties.class } , nil , true ) )
2015-03-25 01:38:53 -04:00
assetsView ( )
end
function _addUIButton ( button , name )
local index = name or button
uiButtons [ index ] = button
end
function folderView ( )
selectedRow = 1
selectedFolder = nil
assetButtons = { { } }
uiButtons.bacToFolderView = nil
labels = { }
end
function assetsView ( )
editingAssetButtons = { }
editorView = " room "
labels = { }
end
function worldView ( )
2026-08-07 01:39:47 -04:00
editorView = " world "
activeField = nil
selectedFolder = nil
worldMode = " place "
worldPage = 1
rebuildWorldRoomButtons ( )
end
local function newWorldRoomName ( cellX , cellY )
local base = string.format ( " room_%02d_%02d " , cellX , cellY )
local name , suffix = base , 2
while gameWorld : roomLocation ( name ) or readFromSource ( " rooms/ " .. name .. " .sav " ) do
name = base .. " _ " .. suffix
suffix = suffix + 1
end
return name
end
2015-03-25 01:38:53 -04:00
2026-08-07 01:39:47 -04:00
local function openWorldRoom ( cellX , cellY )
local name = gameWorld : roomAt ( cellX , cellY )
if not name then
name = newWorldRoomName ( cellX , cellY )
currentRoom = Room : new { name = name }
-- Creating from an empty cell is a real level creation, not merely a
-- transient map reference, so it immediately joins the room picker.
currentRoom : saveToSource ( name )
gameWorld : placeRoom ( name , cellX , cellY )
gameWorld : invalidatePreview ( name )
editorStatus = " Created and opened ' " .. name .. " .' "
else
currentRoom = Room : new { name = name }
editorStatus = " Editing ' " .. name .. " .' "
end
2026-08-07 03:22:28 -04:00
gameWorld : setLastRoom ( currentRoom.name )
2026-08-07 01:39:47 -04:00
setEditorRoomName ( currentRoom.name )
assetsView ( )
2015-03-25 01:38:53 -04:00
end
function editorMouseHandler ( x , y , button )
if x > width then
--we need to adjust for sidebar coords
x = x - width
if editorView == " room " then
if not selectedFolder then
for _ , uiElement in pairs ( folderButtons ) do
if uiElement : clicIsInBox ( x , y ) then
uiElement : onClic ( button )
return
end
end
else
for _ , uiElement in pairs ( assetButtons [ selectedRow ] ) do
if uiElement : clicIsInBox ( x , y ) then
uiElement : onClic ( button )
return
end
end
end
for _ , uiElement in pairs ( uiButtons ) do
if uiElement : clicIsInBox ( x , y ) then
uiElement : onClic ( button )
end
end
elseif editorView == " asset " then
for _ , uiElement in pairs ( editingAssetButtons ) do
if uiElement : clicIsInBox ( x , y ) then
uiElement : onClic ( button )
end
end
2026-08-07 01:39:47 -04:00
elseif editorView == " world " then
for _ , roomButton in ipairs ( worldRoomButtons ) do
if buttonContains ( roomButton , x , y ) then
if selectedRoom == roomButton.name then
selectedRoom = nil
editorStatus = " Room brush cleared. Click the map to open or create a level. "
else
selectedRoom = roomButton.name
editorStatus = " Selected ' " .. selectedRoom .. " ' for placement. Click it again to clear. "
end
return
end
end
for _ , control in ipairs ( worldControls ) do
if buttonContains ( control , x , y ) then
control : onClic ( x )
return
end
end
if uiButtons.world : clicIsInBox ( x , y ) then uiButtons.world : onClic ( button ) end
2015-03-25 01:38:53 -04:00
end
2026-08-06 23:54:49 -04:00
else
-- click landed on the grid (game area): place the selected asset
if editorView == " room " and button == 1 and selectedAsset then
currentRoom : attemptPlace ( selectedAsset , gameAssets [ selectedAsset ] , x , y , selectedColor )
2026-08-07 01:39:47 -04:00
elseif editorView == " world " then
local cellX , cellY = gameWorld : getCellFromMousePos ( x , y )
if not gameWorld : isInBounds ( cellX , cellY ) then return end
if button == 2 then
local removed , name = gameWorld : removeRoom ( cellX , cellY )
if removed then editorStatus = " Removed ' " .. name .. " ' from the world. " end
elseif worldMode == " jump " or not selectedRoom then
openWorldRoom ( cellX , cellY )
elseif selectedRoom then
gameWorld : placeRoom ( selectedRoom , cellX , cellY )
editorStatus = " Placed ' " .. selectedRoom .. " ' at " .. cellX .. " , " .. cellY .. " . "
end
2026-08-06 23:54:49 -04:00
end
2015-03-25 01:38:53 -04:00
end
end
function editorTextHandler ( text )
if editorView == " asset " and activeField then
inputText = text
editingAssetButtons [ activeField ] : editText ( text )
2026-08-07 01:39:47 -04:00
elseif editorView == " room " and activeField == " roomName " then
inputText = text
roomNameField : editText ( text )
2015-03-25 01:38:53 -04:00
end
end
function editorCompleteResponse ( )
2026-08-07 01:39:47 -04:00
if editorView == " asset " and activeField then
2015-03-25 01:38:53 -04:00
assetProperties [ activeField ] = inputText
2026-08-07 01:39:47 -04:00
elseif editorView == " room " and activeField == " roomName " then
editorRoomName = inputText
2015-03-25 01:38:53 -04:00
end
activeField = nil
end
function sortAllAssets ( )
setAll ( )
end
function createUIElement ( t )
return GUI : new ( t )
end
function editorDraw ( )
if editorView == " room " then
2026-08-07 01:39:47 -04:00
love.graphics . setColor ( gColor.white : set ( ) )
love.graphics . printf ( " LEVEL NAME " , 0 , sideBarBox.h * ( 7 / 10 ) - 24 , sideBarBox.w , " center " )
2015-03-25 01:38:53 -04:00
for _ , button in pairs ( uiButtons ) do
button : draw ( )
end
if not selectedFolder then
for _ , folder in pairs ( folderButtons ) do
folder : draw ( )
end
else
for _ , entity in ipairs ( assetButtons [ selectedRow ] ) do
entity : draw ( )
end
end
elseif editorView == " asset " then
for _ , button in pairs ( editingAssetButtons ) do
button : draw ( )
end
2026-08-07 01:39:47 -04:00
elseif editorView == " world " then
drawWorldSidebar ( )
2015-03-25 01:38:53 -04:00
end
for _ , label in pairs ( labels ) do
label : draw ( )
end
2026-08-07 01:39:47 -04:00
if editorStatus then
love.graphics . setColor ( gColor.white : set ( ) )
love.graphics . printf ( editorStatus , 10 , sideBarBox.h * ( 6 / 10 ) , sideBarBox.w - 20 , " center " )
end
2015-03-25 01:38:53 -04:00
end