This guide provides a comprehensive overview of the Lua scripting capabilities within Remere's Map Editor (RME). It covers the file structure, API reference, and examples to help you create tools, generators, and extensions.
Scripts allow you to automate tasks, create custom tools, and extend the editor's functionality.
All Lua scripts must be placed in the scripts directory of your RME installation.
The editor scans this directory on startup. Files must have the .lua extension.
Create a file scripts/hello.lua:
-- Simple script that shows an alert
app.alert("Hello from RME Lua!")Scripts are executed sequentially when the editor loads. You can define global functions or use the API immediately.
There are two ways to create scripts in RME:
This is best for simple scripts, tools, or quick experiments. You just create a single .lua file in the scripts/ directory.
You can define metadata in the file header using comments. The editor parses these to display information in the menus.
-- My Script Name v1.0
-- A description of what this script does.
-- Additional description lines...
-- Code starts hereThe first line is treated as the script name (and optional version).
Subsequent lines starting with -- are treated as the description.
Metadata parsing stops at the first empty line or code line.
You can also use special tags for specific fields:
@Tile: Script Name- Sets the display name.@Description: Text- Sets the description.@Author: Name- Sets the script author.@Version: 1.0- Sets the script version.@Shortcut: KeyCombination- Assigns a default keyboard shortcut (e.g.,Ctrl+Shift+K).
-- @Title: My Script v1.0
-- @Description: A description of what this script does.
-- @Author: John Doe
-- @Version: 1.0
-- @Shortcut: Ctrl+Shift+GThis method is recommended for complex tools or extensions that require multiple files, custom modules, or external resources (like images or data files).
- Create a subdirectory in
scripts/(e.g.,scripts/my_cool_tool/). - Create a
manifest.luafile inside that directory. - Place your main script and resources in that directory.
manifest.lua Example:
return {
name = "My Cool Tool",
description = "A tool that does amazing things.",
author = "Jane Doe",
version = "1.0",
main = "main.lua", -- Entry point script (defaults to main.lua)
autorun = true, -- Run automatically on startup (optional)
shortcut = "Ctrl+Alt+T" -- Keyboard shortcut (optional)
}When using packages, the SCRIPT_DIR global variable in your main script will point to your package directory, allowing you to easily load resources:
local img = Image.fromFile(SCRIPT_DIR .. "/assets/icon.png")- Wrap your functionality in local functions.
- Use
app.transactionfor any operations that modify the map to ensure Undo/Redo support works correctly. - Use
app.alertfor simple user feedback.
The app table provides access to global editor state and utility functions.
| Property/Function | Type | Description |
|---|---|---|
app.version |
string | The current RME version. |
app.apiVersion |
number | The API version number (currently 2). |
app.map |
Map | Returns the currently active Map object (or nil). |
app.selection |
Selection | Returns the current Selection object. |
app.keyboard |
Keyboard | State of modifier keys. |
app.borders |
table | Table of all available borders. |
app.brush |
Brush | The currently selected brush. |
app.brushSize |
number | Current brush size (radius). |
app.brushShape |
string | Current brush shape ("circle" or "square"). |
app.spawnTime |
number | Default spawn time for creatures. |
app.getDataDirectory() |
function | Returns the absolute path to the data directory. |
app.hasMap() |
function | Returns true if a map is currently open. |
app.refresh() |
function | Refreshes the map view. |
app.copy() |
function | Copies current selection to internal clipboard. |
app.cut() |
function | Cuts current selection to internal clipboard. |
app.paste() |
function | Pastes from internal clipboard to current map position. |
app.setClipboard(text) |
function | Sets the system clipboard text. |
app.setCameraPosition(x, y, z) |
function | Moves the camera to the specified map coordinates. |
app.storage(name) |
function | Returns a per-script storage helper (load/save/clear) backed by JSON. |
app.mapView |
table | Access to map overlay APIs (addOverlay/removeOverlay/setEnabled/registerShow). |
app.events |
Events | Access to the event system. |
app.yield() |
function | Yields to process pending UI events. Use in long-running loops to prevent UI freeze. |
app.sleep(ms) |
function | Sleeps for the given milliseconds (max 10000). Blocks the UI thread. |
Access via app.keyboard.
| Method | Description |
|---|---|
isCtrlDown() |
Returns true if Control key is pressed. |
isShiftDown() |
Returns true if Shift key is pressed. |
isAltDown() |
Returns true if Alt key is pressed. |
The app.events object allows scripts to listen for global editor events.
-- Register an event listener
local listenerId = app.events:on("spawnChange", function(action, tile)
if action == "add" then
print("Spawn added at " .. tile.x .. "," .. tile.y)
end
end)
-- Remove an event listener
app.events:off(listenerId)Available Events:
| Event Name | Arguments | Description |
|---|---|---|
spawnChange |
action ("add"|"remove"), tile |
Triggered when a spawn is added to or removed from a tile. |
mapLoad |
- | Triggered when a new map is loaded. |
mapSave |
filename |
Triggered when the current map is saved. |
selectionChange |
- | Triggered when the tile selection changes. |
brushChange |
brushName |
Triggered when the active brush changes. |
floorChange |
newFloor, oldFloor |
Triggered when the visible map floor changes. |
-- Simple
app.alert("Message")
-- Advanced
app.alert({
title = "Confirmation",
text = "Are you sure?",
buttons = {"Yes", "No"}
})All map modifications must be wrapped in a transaction.
app.transaction("My Script Action", function()
-- Modify map here
local tile = app.map:getTile(100, 100, 7)
if tile then
tile:addItem(2160) -- Add Crystal Coin
end
end)These variables are automatically set by the engine before your script runs.
| Variable | Type | Description |
|---|---|---|
SCRIPT_DIR |
string | The directory containing the currently executing script. Use this to load resources (images, data files) relative to your script location. Note: This is only available when running scripts from a file. |
Usage:
-- Load an image from the script's directory
local myImage = Image.fromFile(SCRIPT_DIR .. "/my_image.png")
-- Load a data file
local file = io.open(SCRIPT_DIR .. "/config.txt", "r")The Map object represents the open map file.
| Property/Method | Description |
|---|---|
name |
Name of the map. |
width, height |
Dimensions of the map. |
tileCount |
Total number of tiles. |
getTile(x, y, z) |
Returns a Tile or nil. |
getTile(position) |
Same as above, using a position table/object. |
getOrCreateTile(x, y, z) |
Returns a Tile, creating it if it doesn't exist. |
tiles |
Iterator for looping through all tiles. |
Usage:
for tile in app.map.tiles do
-- Process tile
endA Tile represents a specific coordinate (x, y, z) on the map.
| Property/Method | Description |
|---|---|
position |
Returns {x, y, z} table. |
x, y, z |
Coordinate properties. |
ground |
The ground Item (read/write). |
items |
Lua table of all items on the tile (excluding ground). |
itemCount |
Number of items on the tile. |
creature |
The Creature on the tile (or nil). |
spawn |
The Spawn on the tile (or nil). |
houseId |
ID of the house this tile belongs to. |
isPZ, isBlocking |
Flags (Protection Zone, etc.). |
addItem(id, [count]) |
Adds an item by ID. Returns the new Item. |
removeItem(item) |
Removes a specific item object. |
setCreature(name) |
Sets the creature. |
removeCreature() |
Removes the creature. |
setSpawn(radius) |
Sets a spawn zone. |
removeSpawn() |
Removes the spawn zone. |
getTopItem() |
Returns the item visually on top. |
Represents an item on a tile or in a container.
| Property/Method | Description |
|---|---|
id |
The item ID (read-only). |
name |
The item name. |
count |
Stack count (or subtype). |
actionId, uniqueId |
Properties for scripting. |
text, description |
Text properties (e.g., for signs). |
isStackable, isMoveable |
Type checks. |
clone() |
Returns a copy of the item. |
rotate() |
Rotates the item if possible. |
Creature
name: Creature name.spawnTime: Time in seconds.direction: Enum (Direction.NORTH,Direction.EAST, etc.).isNpc: Boolean.
Spawn
radius(orsize): The spawn radius/size (1-50).
Access via app.selection.
| Property/Method | Description |
|---|---|
tiles |
Table of selected Tile objects. |
size |
Number of selected tiles. |
bounds |
Table {min={x,y,z}, max={x,y,z}}. |
clear() |
Deselects everything. |
add(tile, [item]) |
Adds a tile (and optional item) to selection. |
remove(tile, [item]) |
Removes from selection. |
Scripts can interact with the current brush.
app.setBrush(name): Switches the active brush.Brushes.get(name): Returns a Brush object.Brushes.getNames(): Returns a list of all brush names.
Brush Object:
name,id,type(e.g., "terrain", "doodad").
The Image class allows you to load and manipulate images from files or game sprites.
-- From file
local img = Image.fromFile("path/to/image.png")
local img = Image("path/to/image.png")
local img = Image{path = "path/to/image.png"}
-- From item sprite (by item ID)
local img = Image.fromItemSprite(2160) -- Crystal Coin
local img = Image{itemid = 2160}
-- From raw sprite ID
local img = Image.fromSprite(100)
local img = Image{spriteid = 100}
-- Using SCRIPT_DIR for relative paths
local img = Image.fromFile(SCRIPT_DIR .. "/my_image.png")| Property | Type | Description |
|---|---|---|
width |
number | Width of the image in pixels. |
height |
number | Height of the image in pixels. |
valid |
boolean | true if the image was loaded successfully. |
path |
string | File path (if loaded from file). |
spriteId |
number | Sprite ID (if loaded from sprite). |
isFromSprite |
boolean | true if the image was loaded from a game sprite. |
| Method | Description |
|---|---|
resize(width, height, [smooth]) |
Returns a new resized image. smooth defaults to true. Set to false for pixel-perfect scaling. |
scale(factor, [smooth]) |
Returns a new scaled image by the given factor. |
Scaling Modes:
smooth = true(default): Uses bilinear interpolation. Good for photos, may look blurry on pixel art.smooth = false: Uses nearest-neighbor interpolation. Keeps pixels sharp, ideal for sprites.
-- Smooth scaling (may look blurry)
local large = img:resize(64, 64, true)
-- Pixel-perfect scaling (sharp pixels)
local large = img:resize(64, 64, false)
local doubled = img:scale(2.0, false)You can create custom dialogs using the Dialog class.
Constructor:
Dialog(title) or Dialog({title="...", resizable=true, dockable=true, onclose=function...})
onclose: Optional callback function executed when the dialog is closed.
Widgets (Chainable):
label({text="My Label"})input({id="my_input", text="Default", label="Prompt"})number({id="num", value=10, min=0, max=100})check({id="cb", text="Enable Feature", selected=true})button({text="Click Me", onclick=function() ... end})combobox({id="combo", options={"A", "B"}, option="A"})color({id="col", label="Pick Color"})item({id="itm", itemid=100})image({id="img", itemid=100})orimage({path="...", smooth=false})file({id="f", filename="test.txt", save=false})mapCanvas({id="preview"})
Layout:
box({orient="horizontal"})/endbox()wrap()/endwrap()
Showing:
show(): Displays the dialog modally. The script waits here until the dialog is closed.close(): Closes the dialog programmatically.show({wait=true, center="parent"|"screen", center=false, bounds=...}):centerdefaults to parent when no explicitx/yorboundsare set.
Dialog Properties:
dlg.activeTab: The current tab label (string) ornilif no tab is active.
List/Grid Options (Common)
onchange,ondoubleclick,onleftclick,onrightclick,oncontextmenu.listsupportsicon_size,item_height,show_text,smooth, and per-itemtooltip/image/icon.gridsupportsitem_size,cell_size,label_wrap,show_text, and per-itemtooltip/image.
All widget methods return the Dialog object itself, allowing for method chaining. Most widgets accept an options table.
| Widget | Options | Description |
|---|---|---|
box(options) |
{orient="vertical"|"horizontal", label="Title"} |
Starts a container box. If label is provided, draws a border with a title. |
endbox() |
- | Ends the current box. |
wrap() |
- | Starts a horizontal wrapper. Elements will be placed side-by-side. |
endwrap() |
- | Ends the wrapper. |
newrow() |
- | Forces the next widget to start on a new line (in default layout). |
separator() |
- | Draws a horizontal line separator. |
tab(options) |
{id="tab_id", text="Tab Name", button=false, index=1, onclick=func, oncontextmenu=func} |
Starts a new tab page or a tab-like button (button=true). |
endtabs() |
- | Ends the tab definition. |
panel(options) |
{bgcolor=Color.white, padding=5, margin=5, expand=true, height=30} |
Starts a styled container panel. Supports background color, padding, margin, and standard layout options. |
endpanel() |
- | Ends the current panel. |
Most widgets support these additional layout and styling properties:
| Property | Type | Description |
|---|---|---|
expand |
boolean | If true, the widget expands to fill available space in the layout direction. |
align |
string | Horizontal alignment: "left", "center", "right". |
valign |
string | Vertical alignment: "top", "center", "bottom". |
width, height |
number | Explicit width/height in pixels. |
min_width, min_height |
number | Minimum dimensions. |
max_width, max_height |
number | Maximum dimensions. |
margin |
number | Outer margin (all sides). |
padding |
number | Inner padding (for containers like panel or box). |
bgcolor |
string/Color | Background color (e.g., Color.red or "#FF0000"). |
fgcolor |
string/Color | Foreground (text) color. |
font_size |
number | Font size in points. |
font_weight |
string | Font weight: "normal", "bold". |
| Widget | Options | Description |
|---|---|---|
label(options) |
{text="Text"} |
Displays static text. |
input(options) |
{id="key", label="Label", text="Default", onchange=func} |
Single-line text input. |
number(options) |
{id="key", label="Label", value=0, min=0, max=100, decimals=0} |
Numeric input spinner. |
slider(options) |
{id="key", label="Label", value=0, min=0, max=100} |
Horizontal slider bar. |
check(options) |
{id="key", text="Label", selected=false, onclick=func} |
Checkbox (boolean). |
radio(options) |
{id="key", text="Label", selected=false} |
Radio button (boolean). |
combobox(options) |
{id="key", label="Label", options={"A", "B"}, option="A"} |
Dropdown selection list. |
color(options) |
{id="key", label="Label", color={red=255, green=0, blue=0}} |
Color picker button. Returns {red,green,blue} table. |
file(options) |
{id="key", label="Label", filename="file.txt", save=false} |
File picker. save=true for save dialog. |
item(options) |
{id="key", label="Label", itemid=100} |
Button showing an item sprite. Click to change item. |
image(options) |
{id="key", label="Label", image=Image, path="...", itemid=100, spriteid=100, width=32, height=32, smooth=true} |
Displays an image. Can load from file, item sprite, or raw sprite. Use smooth=false for pixel-perfect scaling. |
mapCanvas(options) |
{id="key", label="Label"} |
Renders a live preview of the current map view. |
| Widget | Options | Description |
|---|---|---|
button(options) |
{id="btn_id", text="Click Me", onclick=func, focus=false} |
Standard button. onclick is a callback function receiving (dlg). |
list(options) |
{id="lst", items={{text="A", image=Image, tooltip="..."}, {text="B", icon=123}}, oncontextmenu=func} |
List box with tooltips and images (image or icon). Supports onchange, ondoubleclick, onleftclick, onrightclick, oncontextmenu. |
grid(options) |
{id="grid", items={{text="", image=Image, tooltip="..."}}, item_size=32, cell_size=32} |
Icon grid with tooltips, per-item images, and size controls. Supports onchange, ondoubleclick, onleftclick, onrightclick, oncontextmenu. |
dlg.data: Table of current values{id = value}. Can be read or written to.dlg.values: Alias fordata.
dlg:modify({ widget_id = { property = new_value } })- Example:
dlg:modify({ my_label = { text = "New Text" } }) - Grid size updates: you can pass
icon_size/item_size/item_width/item_heightandcell_size/cell_width/cell_height. To apply size changes, resenditemsso the image list can be rebuilt.
- Example:
dlg:repaint(): Forces a UI refresh.
Context Menus (Tabs/List/Grid) Callbacks can return a menu table:
oncontextmenu = function(dlg, info)
return {
{ text = "Action", onclick = function() end },
{ separator = true },
{ text = "Disabled", enabled = false }
}
endUse app.mapView to draw overlays on the map and react to hover changes. Overlays run without requiring a dialog and can still expose UI if desired.
app.mapView:addOverlay("Light Strength", {
ondraw = function(ctx)
-- ctx.view: {x1,y1,x2,y2,z,zoom}
-- ctx:rect{ x,y,z,w,h,color={r,g,b,a}, filled=true }
end,
onhover = function(info)
-- info.pos, info.screen, info.tile, info.topItem
return {
tooltip = { text = "Light: 6", color = {255, 200, 50} },
highlight = { color = {255, 200, 50, 120}, filled = false, width = 1 }
}
end
})To add a toggle in the Show menu, register a show input (custom entries are appended after a separator):
app.mapView:registerShow("Light Strength", "Light Strength", {
enabled = true,
ontoggle = function(enabled)
-- Persist or react to state change
end
})local store = app.storage("settings")
local data = store:load() or {}
data.enabled = true
store:save(data)The http table provides HTTP request functionality for scripts.
| Function | Description |
|---|---|
http.get(url, [headers]) |
Performs a GET request. Returns response table. |
http.post(url, body, [headers]) |
Performs a POST request with string body. |
http.postJson(url, table, [headers]) |
Performs a POST request with JSON body. |
Response Table:
{
ok = true, -- true if status is 2xx
status = 200, -- HTTP status code
body = "...", -- Response body as string
error = "", -- Error message if failed
headers = {} -- Response headers table
}Example:
local result = http.get("https://api.example.com/data")
if result.ok then
local data = json.decode(result.body)
app.alert("Got: " .. data.message)
else
app.alert("Error: " .. result.error)
endFor large responses or real-time data (like AI APIs), use streaming:
| Function | Description |
|---|---|
http.postStream(url, body, headers) |
Starts streaming POST request. Returns {sessionId, ok}. |
http.postJsonStream(url, table, headers) |
Starts streaming POST with JSON body. |
http.streamRead(sessionId) |
Reads available data from stream. |
http.streamStatus(sessionId) |
Checks stream status without reading. |
http.streamClose(sessionId) |
Closes and cleans up stream session. |
streamRead Response:
{
data = "...", -- New data received since last read
finished = false, -- true when stream is complete
hasError = false, -- true if an error occurred
error = "", -- Error message if hasError
ok = true, -- false if error
status = 200, -- HTTP status (only when finished)
headers = {} -- Response headers (only when finished)
}Streaming Example:
local stream = http.postJsonStream("https://api.openai.com/v1/chat/completions", {
model = "gpt-4",
messages = {{ role = "user", content = "Hello" }},
stream = true
}, {
["Authorization"] = "Bearer " .. apiKey
})
if stream.ok then
local content = ""
while true do
local result = http.streamRead(stream.sessionId)
if result.data ~= "" then
content = content .. result.data
end
if result.finished or result.hasError then
break
end
app.yield() -- Prevent UI freeze
end
http.streamClose(stream.sessionId)
endReplaces all items of ID 100 with ID 200 in the current selection.
local function replaceItems()
local sel = app.selection
if sel.isEmpty then
app.alert("Select an area first!")
return
end
app.transaction("Replace Items", function()
for _, tile in pairs(sel.tiles) do
for _, item in pairs(tile.items) do
if item.id == 100 then
-- Create new item
tile:addItem(200, item.count)
-- Remove old
tile:removeItem(item)
end
end
end
end)
app.alert("Done!")
end
-- Currently, to run this, you can assign it to a menu or run via console if available.
-- Or simply executing the file runs main scope.
replaceItems()A simple UI to generate grass.
local dlg = Dialog("Grass Generator")
dlg:label({text="Settings"})
dlg:box({orient="horizontal"})
dlg:number({id="density", label="Density %", value=50, min=0, max=100})
dlg:check({id="flowers", text="Add Flowers", selected=true})
dlg:endbox()
dlg:button({text="Generate", onclick=function()
local density = dlg.values.density -- Access values
local flowers = dlg.values.flowers
app.transaction("Generate Grass", function()
-- Logic to place grass...
app.alert("Generated with density: " .. density)
end)
end})
dlg:show()(Note: dlg.values is the table where widget values are stored by ID)
Display images from files and game sprites with different scaling modes.
-- Load an image from the script's directory
local myImage = Image.fromFile(SCRIPT_DIR .. "/logo.png")
-- Load item sprites
local goldCoin = Image.fromItemSprite(2148)
local crystalCoin = Image{itemid = 2160}
local dlg = Dialog{title = "Image Demo", width = 400}
-- Display image from file
if myImage.valid then
dlg:label{text = "Logo (" .. myImage.width .. "x" .. myImage.height .. "):"}
dlg:newrow()
dlg:image{image = myImage, width = 128, height = 128, smooth = false}
dlg:newrow()
end
-- Display item sprites with pixel-perfect scaling
dlg:label{text = "Item sprites (64x64, pixel-perfect):"}
dlg:newrow()
dlg:image{itemid = 2148, width = 64, height = 64, smooth = false}
dlg:image{itemid = 2152, width = 64, height = 64, smooth = false}
dlg:image{itemid = 2160, width = 64, height = 64, smooth = false}
dlg:newrow()
-- Dynamic image update
local currentItem = 2148
dlg:image{id = "preview", itemid = currentItem, width = 64, height = 64, smooth = false}
dlg:button{text = "Next Item", onclick = function()
local items = {2148, 2152, 2160}
for i, v in ipairs(items) do
if v == currentItem then
currentItem = items[(i % #items) + 1]
break
end
end
dlg:modify{preview = {itemid = currentItem, width = 64, height = 64, smooth = false}}
end}
dlg:button{id = "close", text = "Close"}
dlg:show()RME includes powerful procedural generation APIs for creating terrain, caves, dungeons, and more.
The noise table provides various noise functions for procedural terrain generation.
| Function | Description |
|---|---|
noise.perlin(x, y, seed?, frequency?) |
2D Perlin noise, returns [-1, 1] |
noise.perlin3d(x, y, z, seed?, frequency?) |
3D Perlin noise |
noise.simplex(x, y, seed?, frequency?) |
OpenSimplex2 noise (recommended) |
noise.simplex3d(x, y, z, seed?, frequency?) |
3D OpenSimplex2 noise |
noise.simplexSmooth(x, y, seed?, frequency?) |
Smoother OpenSimplex2S variant |
noise.cellular(x, y, seed?, frequency?, distanceFunc?, returnType?) |
Cellular/Voronoi noise |
noise.value(x, y, seed?, frequency?) |
Value noise (blocky) |
noise.valueCubic(x, y, seed?, frequency?) |
Cubic interpolated value noise |
| Function | Description |
|---|---|
noise.fbm(x, y, seed?, options?) |
Fractional Brownian Motion - layered noise |
noise.fbm3d(x, y, z, seed?, options?) |
3D FBM |
noise.ridged(x, y, seed?, options?) |
Ridged fractal noise (mountains, veins) |
FBM Options:
{
frequency = 0.01, -- Base frequency
octaves = 4, -- Number of noise layers
lacunarity = 2.0, -- Frequency multiplier per octave
gain = 0.5, -- Amplitude multiplier per octave
noiseType = "simplex" -- "perlin", "simplex", "value", "cellular"
}-- Distort coordinates for organic effects
local warped = noise.warp(x, y, seed, {
amplitude = 30,
frequency = 0.01,
type = "simplex" -- "simplex", "simplexReduced", "basic"
})
-- warped.x, warped.y contain the distorted coordinates| Function | Description |
|---|---|
noise.normalize(value, min?, max?) |
Normalize [-1,1] to [min, max] (default [0, 1]) |
noise.threshold(value, threshold) |
Returns true if value >= threshold |
noise.map(value, inMin, inMax, outMin, outMax) |
Map value between ranges |
noise.clamp(value, min, max) |
Clamp value to range |
noise.lerp(a, b, t) |
Linear interpolation |
noise.smoothstep(edge0, edge1, x) |
Smooth interpolation |
-- Generate noise grid (faster than individual calls)
local grid = noise.generateGrid(0, 0, 100, 100, {
seed = 1337,
frequency = 0.02,
noiseType = "simplex",
fractal = "fbm",
octaves = 4
})
-- grid[y][x] contains noise valuesExample - Island Generator:
local seed = os.time()
local sel = app.selection.bounds
local minX, minY, z = sel.min.x, sel.min.y, sel.min.z
local maxX, maxY = sel.max.x, sel.max.y
app.transaction("Generate Island", function()
for y = minY, maxY do
for x = minX, maxX do
local n = noise.fbm(x, y, seed, {frequency = 0.03, octaves = 4})
-- Radial falloff for island shape
local cx, cy = (maxX - minX) / 2, (maxY - minY) / 2
local dx, dy = (x - minX) - cx, (y - minY) - cy
local dist = math.sqrt(dx*dx + dy*dy) / math.min(cx, cy)
n = n * (1 - dist * dist)
local tile = app.map:getOrCreateTile(x, y, z)
if n < -0.2 then
tile:addItem(4608) -- Water
elseif n < 0.3 then
tile:addItem(4526) -- Grass
else
tile:addItem(919) -- Mountain
end
end
end
end)The algo table provides procedural generation algorithms.
-- Run cellular automata on existing grid
local result = algo.cellularAutomata(grid, {
iterations = 4,
birthLimit = 4, -- Become wall if neighbors >= this
deathLimit = 3, -- Stay wall if neighbors >= this
width = 100,
height = 100
})
-- Generate a cave map directly
local caveMap = algo.generateCave(100, 100, {
fillProbability = 0.45,
iterations = 5,
birthLimit = 4,
deathLimit = 3,
seed = os.time()
})
-- caveMap[y][x] == 1 means wall, 0 means floor-- Hydraulic erosion for realistic terrain
local eroded = algo.erode(heightmap, {
iterations = 50000,
erosionRadius = 3,
inertia = 0.05,
sedimentCapacity = 4.0,
erosionSpeed = 0.3,
depositSpeed = 0.3,
evaporateSpeed = 0.01,
gravity = 4.0,
seed = os.time()
})
-- Thermal erosion (talus/slope erosion)
local thermal = algo.thermalErode(heightmap, {
iterations = 50,
talusAngle = 0.5,
erosionAmount = 0.5
})-- Generate random seed points
local points = algo.generateRandomPoints(100, 100, 20, seed)
-- Generate Voronoi regions
local voronoi = algo.voronoi(100, 100, points)
-- voronoi[y][x] contains region index (1-based)local result = algo.generateDungeon(100, 100, {
minRoomSize = 5,
maxRoomSize = 15,
maxDepth = 4,
seed = os.time()
})
-- result.grid[y][x] == 1 means wall, 0 means floor
-- result.rooms is array of {x, y, width, height}local maze = algo.generateMaze(51, 51, {seed = os.time()})
-- maze[y][x] == 1 means wall, 0 means passagelocal smoothed = algo.smooth(grid, {
iterations = 2,
kernelSize = 3
})The geo table provides geometric algorithms for drawing shapes and paths.
-- 2D line
local points = geo.bresenhamLine(x1, y1, x2, y2)
-- points = {{x=.., y=..}, {x=.., y=..}, ...}
-- 3D line
local points3d = geo.bresenhamLine3d(x1, y1, z1, x2, y2, z2)-- Bezier curve through control points
local controlPoints = {
{x = 0, y = 0},
{x = 50, y = 100}, -- Control point
{x = 150, y = 100}, -- Control point
{x = 200, y = 0}
}
local curve = geo.bezierCurve(controlPoints, 50) -- 50 steps
-- 3D Bezier
local curve3d = geo.bezierCurve3d(controlPoints3d, 50)-- Fill region with new value
local filled = geo.floodFill(grid, startX, startY, newValue, {
eightConnected = false -- true for 8-directional
})
-- Get positions without modifying grid
local positions = geo.getFloodFillPositions(grid, startX, startY, {
eightConnected = false
})-- Circle (outline or filled)
local circle = geo.circle(centerX, centerY, radius, {filled = true})
-- Ellipse
local ellipse = geo.ellipse(centerX, centerY, radiusX, radiusY, {filled = false})
-- Rectangle
local rect = geo.rectangle(x1, y1, x2, y2, {filled = true})
-- Polygon (outline)
local polygon = geo.polygon({{x=0,y=0}, {x=100,y=0}, {x=50,y=100}})geo.distance(x1, y1, x2, y2) -- Euclidean
geo.distanceSq(x1, y1, x2, y2) -- Squared (faster)
geo.distanceManhattan(x1, y1, x2, y2) -- Manhattan
geo.distanceChebyshev(x1, y1, x2, y2) -- Chebyshev (king's move)geo.pointInCircle(px, py, cx, cy, radius)
geo.pointInRectangle(px, py, x1, y1, x2, y2)
geo.pointInPolygon(px, py, vertices)-- Simple random scatter
local points = geo.randomScatter(x1, y1, x2, y2, count, {
seed = os.time(),
minDistance = 5 -- Minimum spacing
})
-- Poisson disk sampling (blue noise - evenly distributed)
local blueNoise = geo.poissonDiskSampling(x1, y1, x2, y2, minDistance, {
seed = os.time(),
maxAttempts = 30
})Example - Draw River with Bezier:
local sel = app.selection.bounds
local minX, minY, z = sel.min.x, sel.min.y, sel.min.z
local maxX, maxY = sel.max.x, sel.max.y
-- Create curved river path
local controlPoints = {
{x = minX, y = (minY + maxY) / 2},
{x = minX + 30, y = minY + 20},
{x = maxX - 30, y = maxY - 20},
{x = maxX, y = (minY + maxY) / 2}
}
local riverPath = geo.bezierCurve(controlPoints, 100)
app.transaction("Draw River", function()
for _, point in ipairs(riverPath) do
-- Draw circle at each point for river width
local circle = geo.circle(point.x, point.y, 2, {filled = true})
for _, cp in ipairs(circle) do
local tile = app.map:getOrCreateTile(cp.x, cp.y, z)
if tile then
tile:addItem(4608) -- Water
end
end
end
end)