Skip to content
microlith57 edited this page Jun 30, 2023 · 7 revisions

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.

Layers, Modes, and Materials

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

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.

Modes

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

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.

Attaching Data to Materials

This section is a work in progress!

Making your tool do something

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

Click and Drag

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.

Graphics

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

Brush-Like Tools

This section is a work in progress!