- Displays map number in map list
- Ability to use up/down arrows to reorder maps
- Ability to filter objects by their traits
- Key shortcuts to restart the editor (Ctrl+Y) and go back to menu (Ctrl+T)
- Makes map list a little bigger by default
How to use:
1 - First you have to be on the new beta branch. On steam, right click the game > Properties > Betas. Add the code "ggllooeegggg" to unlock the secret "nutcracker" beta
2 - Go to "\Documents\Almost Human\legend of grimrock 2". Once the beta is downloaded, you'll see a file named "mods.cfg" and a "Mods" folder
3 - In the Mods folder, create a text file and paste the code from the end of this post, name it "editorPlus.lua" (or any name you want). Confirm when windows ask if you want to change the extension
4 - Add the mod to mods.cfg so it looks like this:
Code: Select all
mods = {
"editorPlus.lua",
}
Code:
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.
--[=[
=== UModManager Info Section ===
id = "EditorPlus"
name = "Editor Plus"
description = [[Adds some minor improvements to the dungeon editor.
Made by 7Soul (henriquelazarini@gmail.com)]]
version = "0.2.0"
priority = 100
modifiedFields = { "DungeonEditor.init" }
overwrittenFields = { "DungeonEditor.update", "DungeonEditor.projectExplorer", "DungeonEditor.assetBrowser", "DungeonEditor.drawConnectors" }
=== End of Mod Info ===
]=]
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
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