-
Notifications
You must be signed in to change notification settings - Fork 21
Custom Tools
Tools are the items in the first section of the menu on the right-hand side of the interface; they are the primary way to interact with stuff in a map. It is possible to add custom tools, and they can do a wide variety of things — in effect, they act as input devices that are only active when selected in the menu.
The fundamental structure of a tool is as follows:
-- this require is optional but useful
local toolUtils = require("tool_utils")
-- the following is mandatory
local tool = {}
tool._type = "tool"
tool.name = "example_tool"
tool.group = "example"
return tool
The name of the tool is used to get its language keys (in this case tools.name.example_tool
and tools.description.example_tool
), and is used as-is for sorting.
The group is used internally as the primary sort key; hence all tools with brush
as their group are next to each other.
Lönn's built in tools use the brush
and placement
group names.
It is recommended that custom tools sort after built-in ones for usability reasons, with the exception of custom brush-like tools which should sort after the built-in brushes and before the placement tools.
You do not actually have to implement any of these, but it is a good idea to at least implement materials, so that Lönn has something to fill the list with, and you aren't left with the previously selected tool's list.
Layers are used to distinguish between foreground vs. background tiles, entities vs. triggers vs. decals, and so on. If your tool uses layers, you can add:
tool.layer = "example_layer_a"
tool.validLayers = {
"example_layer_a", "example_layer_b"
}
These are localised using language keys like layers.name.example_layer_a
.
Lönn will then automatically change the value of tool.layer
when different layers are selected in the UI.
If you need some more advanced behaviour, you can optionally define functions to get and set the allowed layers:
function tool.getLayers()
return {"example_layer_a", "example_layer_b"}
end
function tool.setLayer(layer, oldLayer)
handler.layer = layer
-- by default, lönn automatically switches material when switching layer, but the advanced function overrides this
local materialValue = toolUtils.getPersistenceMaterial(handler, layer)
if materialValue then
-- for simplicity, the oldMaterial argument is left out here
tool.setMaterial(materialValue)
end
return true
end
-- this one doesn't seem as useful as the first two
function tool.getLayer()
return tool.layer
end
Note that these are individually optional, so you can define none, some, or all of them.
If you use all of the advanced functions, Lönn will ignore tool.layer
and tool.validLayers
.
These work very similarly to layers, with the main distinctions being that they are further down in the interface, and that the names involved are different:
tool.mode = "example_mode_a"
tool.modes = {
"example_mode_a", "example_mode_b"
}
The language keys for modes are dependent on the tool name also, like tools.example_tool.modes.name.example_mode_a
and tools.example_tool.modes.description.example_mode_a
.
The advanced functions for modes work in the same way as for layers, but are called tool.getModes()
, tool.setMode(mode, oldMode)
, and tool.getMode()
.
Materials are different to layers and modes in that you have to use the advanced functions for getting and setting materials — this is because it is expected that the materials available for a tool will depend on the map or editor state in some way. Also, there are no language keys involved; the material name is displayed directly in the UI. For example:
tool.material = "Material Display Name"
-- mandatory
function tool.getMaterials(layer)
return {"Material Display Name", "Another Material"}
end
-- mandatory
function tool.setMaterial(material, oldMaterial)
tool.material = material
end
-- optional
function tool.getMaterial()
return tool.material
end
Technical Aside: The material list is actually a magic list, which doesn't display all its contents at all times but instead only those that are visible. This means it uses less memory and is generally more performant. This fact probably won't affect your tool's functionality.
This section is a work in progress!
Because tools are actually input devices under the hood, they can listen for any supported event, so they can do a lot of things. However, most people expect tools to do things when they click or drag on the map canvas, so those are the most important events for most tools to listen to.
You can listen for an event, for example mouseclicked
, by creating a function with that name:
function tool.mouseclicked(x, y, button, istouch, pressed)
-- it's a good idea to use these, rather than hardcoding which buttons do what
local actionButton = configs.editor.toolActionButton
local cloneButton = configs.editor.objectCloneButton
-- you can do anything here
end
For a more concrete example, here's an example of a click-and-drag action:
Full Example
local configs = require("configs")
local state = require("loaded_state")
local viewportHandler = require("viewport_handler")
local toolUtils = require("tool_utils")
local colors = require("consts.colors")
local drawing = require("utils.drawing")
local tool = {}
tool._type = "tool"
tool.name = "click_and_drag_example"
tool.group = "example"
local startX, startY
local dragX, dragY
---
local function clicked(px, py)
local room = state.getSelectedRoom()
if room then
-- do something as a result of a click
print("clicked", px .. ", " .. py)
end
end
local function handleDragFinished(start_px, start_py, end_px, end_py)
local room = state.getSelectedRoom()
if room and startX and startY and dragX and dragY then
local dx = end_px - start_px
local dy = end_py - start_py
-- do something as a result of a drag finishing
--
-- notice that if you don't check to make sure dx and dy aren't zero, sometimes a single click action can be
-- counted as both a click and a drag.
-- for this reason, if your tool does different things when clicking vs. dragging, you need to check that
-- either dx or dy is nonzero!
print("dragged", start_px .. ", " .. start_py, end_px .. ", " .. end_py)
end
end
---
function tool.mouseclicked(x, y, button, istouch, pressed)
local actionButton = configs.editor.toolActionButton
local room = state.getSelectedRoom()
if button == actionButton and room then
local px, py = viewportHandler.getRoomCoordinates(room, x, y)
clicked(px, py)
end
end
function tool.mousepressed(x, y, button, istouch, pressed)
local actionButton = configs.editor.toolActionButton
local room = state.getSelectedRoom()
if button == actionButton and room then
local px, py = viewportHandler.getRoomCoordinates(room, x, y)
startX, startY = px, py
dragX, dragY = px, py
end
end
function tool.mousemoved(x, y, dx, dy, istouch)
local actionButton = configs.editor.toolActionButton
local room = state.getSelectedRoom()
if love.mouse.isDown(actionButton) and room then
local px, py = viewportHandler.getRoomCoordinates(room, x, y)
dragX, dragY = px, py
end
end
function tool.mousereleased(x, y, button)
local actionButton = configs.editor.toolActionButton
if button == actionButton then
handleDragFinished(startX, startY, dragX, dragY)
startX, startY = nil, nil
dragX, dragY = nil, nil
end
end
---
function tool.draw()
local room = state.getSelectedRoom()
if room then
if startX and startY and dragX and dragY then
-- draw a line from the start to the end position
viewportHandler.drawRelativeTo(room.x, room.y, function()
drawing.callKeepOriginalColor(function()
love.graphics.setColor(colors.brushColor)
love.graphics.line(startX, startY, dragX, dragY)
end)
end)
else
-- in an actual tool you'd want to draw something when no drag is happening
end
end
end
---
return tool
This makes use of local variables to store the position at which the user started dragging, and also the position they are currently at.
These are all nil
when a drag is not happening.
In an actual tool you wouldn't use print
statements; instead replace them with the tool-specific action logic.
You can draw graphics by listening to the draw
event, which you can do by making a tool.draw
function.
For example:
-- adapted from https://github.com/JaThePlayer/LoennScripts/blob/main/Loenn/tools/scripts.lua under the MIT license
-- copyright (c) 2021 JaThePlayer
local state = require("loaded_state")
local viewportHandler = require("viewport_handler")
local drawing = require("utils.drawing")
function tool.draw()
local room = state.getSelectedRoom()
if room then
local px, py = viewportHandler.getRoomCoordinates(room)
viewportHandler.drawRelativeTo(room.x, room.y, function()
-- draw a box with a dot in it at the cursor position
love.graphics.rectangle("line", px - 2.5, py - 2.5, 5, 5)
love.graphics.rectangle("line", px, py, .1, .1)
end)
end
end
This section is a work in progress!