Code: Select all
-- This file contains sections of Legend of Grimrock 2 source code; anything you
-- do with this file must comply with the Grimrock modding terms:
-- http://www.grimrock.net/modding_log1/modding-and-asset-usage-terms/
--
-- You are free to alter this mod or reuse its code in other Grimrock mods.
--[=[
Made by 7Soul (henriquelazarini@gmail.com)]]
version = "0.3.0"
]=]
DungeonEditor.backToMenuKey = "T" -- ctrl + this key goes back to main menu
DungeonEditor.fullRestart = "Y" -- ctrl + this key restarts the editor and reloads mods
-------------------------------------------------------------------------------------------------------
-- Editor Functions --
-------------------------------------------------------------------------------------------------------
local oldDungeonInit = DungeonEditor.init
function DungeonEditor:init()
oldDungeonInit(self)
self.splitter1 = 252
self.splitter2 = 476
self.splitter3 = 340
self.splitter4 = 200 + 310 -- larger map list
self.minHeight = -6
self.maxHeight = 6
end
function DungeonEditor:update()
if not renderer:isReadyToRender() or (config.sleepWhenNoFocus and not mainFrame:hasFocus()) then
sys.sleep(100)
return
end
if self.pendingLoadDungeon then
self:loadDungeon(self.pendingLoadDungeon)
self.pendingLoadDungeon = nil
end
updateTime()
updateFileChangeRequests()
--sys.sleep(50)
-- update input state
local windowSizeChanged = false
do
local state = imgui.state
state.doubleClick = detectDoubleClick()
-- clear unprocessed keys
state.keyInput = {}
state.mouseWheel = 0
-- poll events
while true do
local event = mainFrame:pollEvents()
if not event then break end
if event.type == "key" and event.down then
--print(event.key, event.char)
state.keyInput[#state.keyInput+1] = event
-- global keys
local action = config:convertEditorKeyToAction(event.keyCode, event.modifiers)
if event.key == "O" and event.modifiers == 2 then self:onOpenProject() end
if event.key == "S" and event.modifiers == 2 then self:onSaveProject() end
if event.key == "R" and event.modifiers == 2 then self:onReloadProject() end
if event.key == DungeonEditor.backToMenuKey and event.modifiers == 2 then self:onBackToGame() end
if event.key == DungeonEditor.fullRestart and event.modifiers == 2 then sys.restart{ "launchEditor" } end
if action == "start_preview" then self:playPreview() end
if action == "stop_preview" then self:stopPreview() end
elseif event.type == "mouse_wheel" then
state.mouseWheel = state.mouseWheel + event.delta
elseif event.type == "menu" then
self:onMenuEvent(event.id)
elseif event.type == "resize" then
self.windowWidth = event.width -- 0 when window is minimized
self.windowHeight = event.height
windowSizeChanged = true
elseif event.type == "close" then
self:confirmClose(function() sys.exit() end)
end
end
end
-- early out if window is minimized
if self.windowWidth == 0 or self.windowHeight == 0 then return end
-- recreate render window if window was resized
if windowSizeChanged then
renderer:resizeRenderBuffers(self.windowWidth, self.windowHeight)
end
-- update steam
steamContext:update()
renderer:setViewport(0, 0, self.windowWidth, self.windowHeight)
renderer:beginRender()
ImmediateMode.beginDraw()
imgui.prepare(mainFrame)
local screenWidth = self.windowWidth
local screenHeight = self.windowHeight
if self.previewMode and self.fullscreen then
-- fullscreen preview mode
self:preview(0, 0, screenWidth, screenHeight)
else
ImmediateMode.fillRect(0, 0, screenWidth, screenHeight, {41,41,41,255})
self:toolBar(20, 10, 200, 30)
self:brushInfo(self.splitter1 + 2, 10, (screenWidth - self.splitter1) - self.splitter2 - 4, 30)
-- screen height without status bar
local screenHeight2 = screenHeight - 20
-- project/asset browser splitter
do
local x = 0
local y = 44
local width = self.splitter1 - 2
local height = screenHeight2 - y
-- asset browser
do
local y = self.splitter4+2
local height = screenHeight2 - y
self:assetBrowser(x, y, width, height)
end
-- project explorer
do
local height = self.splitter4 - y - 2
self:projectExplorer(x, y, width, height)
end
-- splitter 4
self.splitter4 = imgui.vsplitter("splitter4", x, self.splitter4, width)
self.splitter4 = math.clamp(self.splitter4, y + 50, screenHeight2 - 22)
end
-- map view
do
local x = self.splitter1 + 4
local y = 44
local width = (screenWidth - self.splitter2) - self.splitter1 - 8
local height = screenHeight2 - y
self:mapView(x, y, width, height)
end
-- preview/inspector splitter
do
local x = (screenWidth - self.splitter2) + 2
local y = 44
local width = screenWidth - x
local height = screenHeight2 - y
-- inspectors
do
local y = self.splitter3+2
local height = screenHeight2 - y
local sel = iff(#self.selection == 1, self.selection[1], nil)
self.inspector:inspect(sel, x, y, width, height, 0)
end
-- preview
do
local height = self.splitter3 - y - 2
self:previewButtons(x, 10)
if self.previewMode and self.fullscreen then
self:preview(0, 0, screenWidth, screenHeight)
else
self:preview(x, y, width, height)
end
end
-- splitter 3
self.splitter3 = imgui.vsplitter("splitter3", x, self.splitter3, width)
self.splitter3 = math.clamp(self.splitter3, y + 50, screenHeight2 - 22)
end
-- status bar
do
local h = 17
local x = 0
local y = screenHeight - h
ImmediateMode.fillRect(x, y, screenWidth, h, {56,56,56,255})
if self.status then ImmediateMode.drawText(self.status, x+4, y+2, imgui.state.font, {200,200,200,255}) end
-- draw coordinates
if self.mouseCellX and self.mouseCellY then
local text = string.format("%d,%d", self.mouseCellX, self.mouseCellY)
local textWidth = imgui.state.font:getTextWidth(text)
local x = x + screenWidth - textWidth - 4
ImmediateMode.fillRect(x-4, y, textWidth+8, h, {56,56,56,255})
ImmediateMode.drawText(text, x, y + 2, imgui.state.font, {200,200,200,255})
end
end
-- splitter 1 (measured from left screen edge)
self.splitter1 = imgui.hsplitter("splitter1", self.splitter1, 0, screenHeight)
self.splitter1 = math.clamp(self.splitter1, 10, (screenWidth - self.splitter2)-10)
-- splitter 2 (measured from right screen edge)
self.splitter2 = screenWidth - imgui.hsplitter("splitter2", screenWidth - self.splitter2, 0, screenHeight)
self.splitter2 = math.clamp(self.splitter2, 10, (screenWidth - self.splitter1)-10)
self:updateContextMenu()
end
if self.dialog then
self.dialog:update()
if self.dialog.close then self.dialog = nil end
end
imgui.finish()
ImmediateMode.endDraw()
renderer:endRender()
tvec.free()
tmat.free()
end
-- Project explorer
function DungeonEditor:projectExplorer(x, y, width, height)
x,y,width,height = self:panel("Project", x, y, width, height)
if not dungeon then return end
imgui.beginArea(x, y, width, height)
ImmediateMode.drawRect(x, y, x+width-1, y+height-1, {50,50,50,255})
local lineHeight = 13
local scroll = self.projectScroll
do
local y = y
for i=1,#dungeon.maps do
local map = dungeon.maps[i]
-- level visibility
local oldState = map._levelDisabled
if oldState == nil then oldState = false end
map._levelDisabled = not self:levelTick("level_check_"..i, x, y - scroll, 16, lineHeight, not map._levelDisabled)
-- special case: control click hides all other levels
if oldState ~= map._levelDisabled and sys.keyDown("control") then
for j=1,#dungeon.maps do
local map = dungeon.maps[j]
map._levelDisabled = (i ~= j)
end
self.map = dungeon.maps[i]
end
-- level coord and name
local x = x + 20
local lx,ly,lz = map:getLevelCoord()
local item = string.format("%02d (%d,%d,%d) %s", i, lx, ly, lz, map.name) -- show map id
if map == self.map then
ImmediateMode.fillRect(x, y - scroll, width, lineHeight, {48,72,96,255})
local color = iff(map._levelDisabled, {200,200,200,255}, Color.White)
ImmediateMode.drawText(item, x, y - scroll, imgui.state.font, color)
else
local color = iff(map._levelDisabled, {115,115,115,255}, {206,206,206,255})
ImmediateMode.drawText(item, x, y - scroll, imgui.state.font, color)
end
y = y + lineHeight
end
end
imgui.endArea()
if imgui.buttonLogic("project_explorer", x + 14, y, width - 14, height) then
local y = math.floor((imgui.state.mouseY - y + scroll) / lineHeight) + 1
if y >= 1 and y <= #dungeon.maps then
self.map = dungeon.maps[y]
end
end
-- context menu
if imgui.state.hot == "project_explorer" and sys.mousePressed(2) then
local state = imgui.state
local y = math.floor((imgui.state.mouseY - y + scroll) / lineHeight) + 1
if y >= 1 and y <= #dungeon.maps then
self.map = dungeon.maps[y]
self:contextMenu{
"New Level", function() self:newLevel() end,
"Delete", function() self:deleteLevel() end,
"Move Up", function() self:moveLevelUp() end,
"Move Down", function() self:moveLevelDown() end,
"Sort", function() self:sortLevels() end,
"Properties", function() self.dialog = MapPropertiesDialog.create() end,
}
end
end
if imgui.state.hot == "project_explorer" and not self.dialog then
while #imgui.state.keyInput > 0 do
local ev = imgui.state.keyInput[1]
table.remove(imgui.state.keyInput, 1)
local action = config:convertEditorKeyToAction(ev.keyCode, ev.modifiers)
if ev.key == "up" and ev.modifiers == 0 then self:moveLevelUp() end
if ev.key == "down" and ev.modifiers == 0 then self:moveLevelDown() end
end
end
-- scroll bar
local numItems = #dungeon.maps
self.projectScroll = imgui.vscrollbar("project_scroller", x+width-10, y, 10, height, self.projectScroll, height, numItems*lineHeight)
-- mouse wheel scrolling
if imgui.state.hot == "project_explorer" and imgui.state.mouseWheel ~= 0 then
self.projectScroll = self.projectScroll - imgui.state.mouseWheel * lineHeight * 5
end
self.projectScroll = math.clamp(self.projectScroll, 0, math.max(numItems*lineHeight - height, 0))
end
function DungeonEditor:assetBrowser(x, y, width, height)
--ImmediateMode.fillRect(x, y, width, height, {50,50,50,255})
x,y,width,height = self:panel("Asset Browser", x, y, width, height)
x = x + 2
y = y + 0
width = width - 2
height = height
if not dungeon then return end
ImmediateMode.pushState()
ImmediateMode.clipTo(x, y, x+width, y+height)
-- search field
do
local w = math.min(120, width)
self.findAsset = self:searchBox("asset_find", x, y+1, width, nil, self.findAsset)
y = y + 20
height = height - 20
end
-- collect set of tags from archs
local tags = {}
do
local s = {}
for _,a in pairs(dungeon.archs) do
if a.editorIcon then
for t,_ in pairs(a.tags) do
s[t] = true
end
end
end
-- convert to list of tags
for t,_ in pairs(s) do
tags[#tags+1] = t
end
table.sort(tags)
table.insert(tags, 1, "any")
end
-- collect set of traits from archs
local traits = {}
do
local s = {}
for _,a in pairs(dungeon.archs) do
if a.editorIcon and a.components then
for _,c in ipairs(a.components) do
if c.traits then
for _,t in pairs(c.traits) do
s[t] = true
end
end
end
end
end
-- convert to list of traits
for t,_ in pairs(s) do
traits[#traits+1] = t
end
table.sort(traits)
table.insert(traits, 1, "any")
end
-- filter
if self.mode ~= "brush_tool" then
y = y + 3
imgui.label("Tags", x+2, y+3)
self.assetFilter = imgui.combobox("asset_filter", x+45, y, 135, 18, self.assetFilter or 1, tags)
y = y + 23
height = height - 26
y = y + 3
imgui.label("Traits", x+2, y+3)
self.assetFilter2 = imgui.combobox("asset_filter2", x+50, y, 135, 18, self.assetFilter2 or 1, traits)
y = y + 23
height = height - 26
else
local h = 3
y = y + h
height = height - h
end
-- list of assets
do
local height = height
ImmediateMode.pushState()
ImmediateMode.clipTo(x, y, x+width, y+height)
ImmediateMode.drawRect(x, y, x+width-1, y+height-1, {50,50,50,255})
local filter = tags[self.assetFilter]
local filter2 = traits[self.assetFilter2]
local assets = {}
if self.mode == "brush_tool" then
-- tiles
for _,tile in pairs(self.dungeon.tiles) do
if not self.currentBrush[1] then self.currentBrush[1] = tile end
if not self.currentBrush[2] then self.currentBrush[2] = tile end
-- filter asset by name
local ignore
if #self.findAsset > 0 and not string.match(tile.name, self.findAsset, 1, true) then
ignore = true
end
if not ignore then assets[#assets+1] = tile end
end
else
-- archs
for _,a in pairs(dungeon.archs) do
if a.editorIcon and not string.match(a.name, "^base%_") then
-- filter by tags
local ignore
if filter ~= "any" and not a.tags[filter] then
ignore = true
end
-- filter by traits
if a.components then
local traitComp
for _,c in ipairs(a.components) do
if c.traits then
traitComp = c
end
end
if traitComp then
if filter2 ~= "any" and not table.contains(traitComp.traits, filter2) then
ignore = true
end
else
if filter2 ~= "any" then
ignore = true
end
end
end
if a.name == "party" then ignore = true end
-- filter asset by name
if #self.findAsset > 0 and not string.find(a.name, self.findAsset, 1, true) then
ignore = true
end
if not ignore then
assets[#assets+1] = a
end
end
end
end
-- sort alphabetically
table.sort(assets, function(l,r) return l.name < r.name end)
local lineHeight = 20
do
local y = y
for i=1,#assets do
local a = assets[i]
local selected
if self.mode == "brush_tool" then
selected = (self.currentBrush[1] == a)
else
selected = (self.selectedAsset == a)
end
if self.currentBrush[2] == a then
ImmediateMode.fillRect(x, y - self.assetScroll, width, lineHeight, {48,72,96,128})
end
if selected then
ImmediateMode.fillRect(x, y - self.assetScroll, width, lineHeight, {48,72,96,255})
ImmediateMode.drawText(a.name, x+22, y - self.assetScroll+4, imgui.state.font, Color.White)
else
ImmediateMode.drawText(a.name, x+22, y - self.assetScroll+4, imgui.state.font, {200,200,200,255})
end
if a.editorIcon then
local y = y - self.assetScroll
if a.editorIcon == 24 then y = y + 6 end
self:drawMapTile(x, y, a.editorIcon, false, 0, a.color or Color.White)
end
y = y + lineHeight
end
end
ImmediateMode.popState()
if self.mode == "brush_tool" then
-- imgui.buttonLogic() does not work with rmb...
local hover = imgui.regionHit(x, y, width, height)
if hover then
local y = math.floor((imgui.state.mouseY - y + self.assetScroll) / lineHeight) + 1
if imgui.state.hot == "asset_browser" then
if sys.mousePressed(0) then self.currentBrush[1] = assets[y] end
if sys.mousePressed(2) then self.currentBrush[2] = assets[y] end
end
imgui.state.newHot = "asset_browser"
end
else
if imgui.buttonLogic("asset_browser", x, y, width, height) then
local y = math.floor((imgui.state.mouseY - y + self.assetScroll) / lineHeight) + 1
self.selectedAsset = assets[y]
self.mode = "place_objects"
self.status = "Add Object: Click on the map to add object"
imgui.state.focus = "asset_browser"
end
end
-- scroll bar
self.assetScroll = imgui.vscrollbar("asset_scroller", x+width-10, y, 10, height, self.assetScroll, height, #assets*lineHeight)
-- mouse wheel scrolling
if imgui.state.hot == "asset_browser" and imgui.state.mouseWheel ~= 0 then
self.assetScroll = self.assetScroll - imgui.state.mouseWheel * lineHeight * 4
end
self.assetScroll = math.clamp(self.assetScroll, 0, math.max(#assets*lineHeight - height, 0))
end
ImmediateMode.popState()
end
function DungeonEditor:drawConnectors(x, y, ent, color)
local s = 20
for i=1,ent.components.length do
local comp = ent.components[i]
if comp.connectors then
for _,connector in ipairs(comp.connectors) do
local target = connector.target
if target then
target = self.map:findEntity(target)
if target then
local x1 = x + ent.x * s + s/2
local y1 = y + ent.y * s + s/2
if ent.arch.placement == "wall" then
local dx,dy = getDxDy(ent.facing)
x1 = x1 + dx*10
y1 = y1 + dy*10
elseif ent.arch.placement == "pillar" then
x1 = x1 - 10
y1 = y1 - 10
end
local x2 = x + target.x * s + s/2
local y2 = y + target.y * s + s/2
if target.arch.placement == "wall" then
local dx,dy = getDxDy(target.facing)
x2 = x2 + dx*10
y2 = y2 + dy*10
elseif target.arch.placement == "pillar" then
x2 = x2 - 10
y2 = y2 - 10
end
self:drawConnectorArrow(x1, y1, x2, y2, color)
end
end
end
end
if comp.__class == TeleporterComponent or comp.__class == StairsComponent then
local tlevel,tx,ty = comp:getTeleportTarget(comp)
if tx and ty then
local x1 = x + ent.x * s + s/2
local y1 = y + ent.y * s + s/2
local x2 = x + tx * s + s/2
local y2 = y + ty * s + s/2
self:drawConnectorArrow(x1, y1, x2, y2, color)
end
end
end
end
function DungeonEditor:addObjectTool(x, y, width, height, pressed)
local arch = self.selectedAsset
local s = 20
local map = self.map
if not arch then return end
-- mouse square
local mx,my = self.mouseX,self.mouseY
mx = (mx - x) / s
my = (my - y) / s
local fx,fy = mx % 1,my % 1
mx = math.floor(mx)
my = math.floor(my)
if mx < 0 or my < 0 or mx >= map.width or my >= map.height then
return
end
local facing = 0
local valid = true
local tx,ty
if arch.placement == "pillar" then
-- pillar
facing = math.random(0,3)
mx = math.floor((self.mouseX - x) / s + 0.5)
my = math.floor((self.mouseY - y) / s + 0.5)
tx = x + mx * s - 2
ty = y + my * s - 2
-- snap to wall if holding shift
if sys.keyDown("shift") then
valid = not map:isWall(mx-1, my-1) or not map:isWall(mx-1, my) or not map:isWall(mx, my-1) or not map:isWall(mx, my)
end
elseif arch.placement == "wall" then
facing = nil
-- snap to wall if holding shift
if sys.keyDown("shift") then
local dist = math.huge
if map:isWall(mx, my) then
if not map:isWall(mx, my-1) then
local d = fy
if d < dist then facing = 0; dist = d end
end
if not map:isWall(mx+1, my) then
local d = 1 - fx
if d < dist then facing = 1; dist = d end
end
if not map:isWall(mx, my+1) then
local d = 1 - fy
if d < dist then facing = 2; dist = d end
end
if not map:isWall(mx-1, my) then
local d = fx
if d < dist then facing = 3; dist = d end
end
if facing then
local dx,dy = getDxDy(facing)
mx = mx + dx
my = my + dy
facing = (facing + 2) % 4
end
else
if map:isWall(mx, my-1) then
local d = fy
if d < dist then facing = 0; dist = d end
end
if map:isWall(mx+1, my) then
local d = 1 - fx
if d < dist then facing = 1; dist = d end
end
if map:isWall(mx, my+1) then
local d = 1 - fy
if d < dist then facing = 2; dist = d end
end
if map:isWall(mx-1, my) then
local d = fx
if d < dist then facing = 3; dist = d end
end
end
valid = (facing ~= nil)
end
if not facing then facing = getQuadrant(fx, fy) end
local dx,dy = getDxDy(facing)
tx = x + mx * s + dx*10
ty = y + my * s + dy*10
elseif arch.placement == "floor" or arch.placement == "ceiling" then
-- floor item
facing = getQuadrant(fx, fy)
tx = x + mx * s
ty = y + my * s
-- snap to empty tile if holding shift
if sys.keyDown("shift") then
valid = not map:isWall(mx, my)
end
else
facing = getQuadrant(fx, fy)
tx = x + mx * s
ty = y + my * s
end
self:drawMapTile(tx, ty, arch.editorIcon + facing, false, 0, iff(valid, Color.White, {255,255,255,64}))
if pressed and valid then
local name = self.selectedAsset.name
-- Prevents crash from placing pillars at the edge of the map
if mx < 0 or my < 0 or mx >= map.width or my >= map.height then
systemLog:write("[Warn] Attempted to insert map object " .. name .. " outside map boundaries")
return
end
-- replace existing starting location
if name == "starting_location" then
self:setStartingLocationTo(self.map.level, mx, my, facing)
else
local id_ = self:generateUniqueId(name)
-- Created objects now take on the elevation of the tile they're placed on
local elevation = self.map:getElevation(mx, my)
spawn(self.map, name, mx, my, facing, elevation, id_, false)
end
self:modify()
end
end
-- Prevents a bug when copy-pasting objects (such as walls) that destroys other walls
local oldMapRemoveWall = Map.removeWall
function Map:removeWall(x, y, facing, elevation)
if self.dungeon.editorDungeon then return end
oldMapRemoveWall(self, x, y, facing, elevation)
end