Repository navigation
CustomMaps Scripts
A map script animates a world: water that scrolls, lava that breathes, vents that throw smoke, weather over the whole map. It runs on the client, once per frame, for as long as the player is on that map.
Data\Custom\Scripts\Game\Maps\<mapnumber>.gsc production
Data\Custom\Scripts\Game\Maps\<mapnumber>.lua dev mode only
The example throughout is Maps\160.lua, the Tarkan Copy script.
There is no OnLoad. The chunk itself runs once, when the map is entered, and
that is where you declare what you want and hand over your closures:
local Helpers = require("Helpers")
local Enums = require("Enums")
local R = require("Registry")
Map.RegisterObject(60) -- smoke box
Map.RegisterObject(63) -- blue lamp
Map.OnObject(function(o) ... end)
Map.OnUpdate(function() ... end)When the map changes the whole state is rebuilt, so anything the script kept is gone. Keep state in file-scope locals; globals are shared with every other script in the state.
Per frame, in this order:
| Step | Handler | Where it lands |
|---|---|---|
| 1 | Map.OnUpdate |
before the world is drawn - movement and spawning |
| 2 | Map.OnObject |
once per registered object, as the world is drawn |
| 3 | Map.OnRenderWorld |
inside the 3D pass - world coordinates, depth tested |
| 4 | Map.OnRender |
over the finished world, under the interface - 2D |
The distinction that matters is 3 versus 4. Anything with a position in the world
belongs in OnRenderWorld; a full-screen wash or overlay belongs in OnRender. Get
it backwards and the effect is either painted over by the world or floating in front
of it.
Only object types you register ever reach the script - everything else is rejected before Lua is entered, which is what keeps a per-object handler cheap.
Map.RegisterObject(2) -- water
Map.RegisterObject(4) -- lavaThe type number is the object's type in the world file, and it is the model's file
number minus one: Data\Object200\Object95.bmd is type 94 wherever World200 places it.
Helpers.RegisterAll() lets every type through, for a rule that has to see all objects -
Fading roofs uses it to open whatever stands on a roof. It costs one
handler call per visible object per frame.
Called once per registered object per frame, with the object as its only argument. Everything about the object is read and written through properties - see Objects and the sandbox for the full list.
Map.OnObject(function(o)
if o.type == 4 then
-- Lava: the texture crawls, the brightness breathes, and the same value is
-- spilled on the terrain so the rock around the pit lights up with it.
local sine = math.sin(Map.WorldTime() * 0.002) * 0.35 + 0.65
o.blendMesh = 0
o.blendMeshLight = sine
o.blendMeshTexCoordV = -(Map.WorldTime() % 10000) * 0.0001
o:AddTerrainLight(sine, sine, sine, 3)
end
end)o.id is stable for the life of the object, so it works as a key for per-object
state:
local state = {}
Map.OnObject(function(o)
if not state[o.id] then
state[o.id] = { phase = math.random() * 1000 }
end
end)For variation without any state at all, derive the phase from the object's own rotation - stable across frames and identical on every client:
local lum = math.sin((Helpers.PhaseOf(o) + Map.WorldTime()) * 0.002) * 0.3 + 0.7| Function | When |
|---|---|
Map.RegisterObject(type) |
at load - lets one object type through to the handler |
Map.OnObject(fn) |
fn(o) per registered object per frame |
Map.OnUpdate(fn) |
fn() once a frame, before the world is drawn |
Map.OnRenderWorld(fn) |
fn() once a frame, inside the 3D pass |
Map.OnRender(fn) |
fn() once a frame, over the world and under the interface |
| Function | Returns |
|---|---|
Map.WorldTime() |
the client's own clock in milliseconds |
Map.HeroPosition() |
x, y, z of the player, or nothing until the world is up |
Map.TerrainHeight(x, y) |
ground height (Z) at a world position, in world units |
Map.TerrainWall(x, y) |
the flags of the tile at a world position, from the world's .att, added together: 1 safe zone, 2 a character stands there, 4 blocked, 8 no ground, 16 water - named in Enums.TerrainWall; a safe tile with a fence through it reads 5
|
Use Map.WorldTime() for anything that animates. os.clock is wall clock and will
drift against the frame rate and keep running while the game is paused.
Map.HeroPosition() returns nothing before the world is ready - always check:
local hx, hy = Map.HeroPosition()
if not hx then return endNever use a picture id read out of the client - it moves with every new client build. Load your own by file name into a slot:
| Function | Returns |
|---|---|
Map.LoadImage(slot, path, repeat_, smooth) |
texture id, or nothing when the file cannot be read; path is under Data\, written as the .jpg / .tga name - "Effect\sand01.jpg"
|
Map.ImageTexture(slot) |
the id a slot already holds, without touching the file - slots outlive the map, so it can be a picture another map loaded there |
Map.LoadModel(slot, folder, name, index) |
model id, or nothing on failure; the file is folder + name + index in two digits - ("Data\Object161\\", "Bug", 2) reads Bug02.bmd
|
Picture and model slots are 0..2999, sound slots 0..999, each pool shared by every map and
monster script. In practice you never call these directly - name the file in
Registry.lua and ask Helpers:
local sand = Helpers.Texture(R, "sand01")
local bug = Helpers.Model(R, "bug")repeat_ is required for anything whose texture coordinates scroll past 1, or the
sampler drags the edge pixels across the screen as streaks. smooth asks for a
mipmapped filter and is only valid for a picture that has mipmaps - without them
the texture is incomplete and draws as flat white.
| Function | Pass | What it draws | Defaults |
|---|---|---|---|
Map.RenderPlane(tex, x, y, z, w, h, ax, ay, az, r, g, b, additive) |
OnRenderWorld |
a textured quad at a world position, turned by ax, ay, az degrees; w and h are half-sizes in world units - the corners reach w out on X and on Y and h up and down, before the turn |
r, g, b 1, additive false |
Map.RenderBillboard(tex, x, y, z, w, h, r, g, b, toCamera) |
OnRenderWorld |
a quad always facing the camera, added; w / h are half-sizes in world units, toCamera pulls it that many units towards the viewer so the model it glows on does not cut it |
r, g, b 1, toCamera 0 |
Map.BeginBillboards(tex), Map.Billboard(x, y, z, w, h, r, g, b, toCamera), Map.EndBillboards()
|
OnRenderWorld, monster handlers |
many quads, each exactly as Map.RenderBillboard draws it, as one run in one texture - far cheaper for dozens of them. Any other draw call, or the end of the handler, ends the run; Map.Billboard outside a run is an error |
as Map.RenderBillboard
|
Map.Beam(x1, y1, z1, x2, y2, z2, width, r, g, b) |
inside a Map.BeginBillboards run |
a strip from one point to the other, width wide, turned to the camera like the run's quads; the texture runs along it, u 0 at the start and 1 at the end - a bolt, a trail or a laser out of your own picture |
r, g, b 1 |
Map.RenderOverlay(tex, u, v, uScale, vScale, r, g, b, a, w, h) |
OnRender |
one scrolling layer from the top left corner of the screen, in interface coordinates (640 x 480); u / v slide it, uScale / vScale repeat it |
r, g, b, a 1, w 640, h 435 - the bottom 45 are left to the interface bar |
Map.RenderRect(x, y, w, h, rgba) |
OnRender |
a flat coloured rectangle in interface coordinates (640 x 480), rgba packed 0xRRGGBBAA
|
- |
Map.RenderPlane and Map.RenderBillboard are the ways to draw in world coordinates -
the plane at a fixed angle, the billboard always turned to the camera, which suits a halo
or a glow. Map.RenderPlane's additive
argument decides the blend: false (default) is alpha blended with the alpha test
on, for a cutout with an alpha channel; true adds, which is what a .jpg needs -
drawn alpha blended a .jpg comes out as a black rectangle.
Map.RenderOverlay's uScale / vScale decide how many times the texture repeats:
small numbers give one big blurred sheet, large ones a fine grain.
Map.RenderRect takes a packed 0xRRGGBBAA and needs no texture at all, which
makes a colour grade or a vignette a few lines rather than an art asset:
Map.RenderRect(0, 0, 640, 480, 0x1A1008FF)Map.SpawnParticle(texture, subType, scale, x, y, z, r, g, b) puts a particle
anywhere, with no object behind it - which is what falling weather needs, since the
object-bound version can only spawn where an object already stands. subType defaults to
0, scale and r, g, b to 1.
Map.SpawnJoint(type, subType, scale, x, y, z, tx, ty, tz) draws one of the client's
trails between two world points; every argument is needed, and scale is the trail's
width. A joint id is a number of the client's that moves with every client version, and
none ship with the scripts - a trail is drawn with Map.Beam out of your own picture
instead: the storm's bolt out of Effect\JointThunder01.jpg, the insects' streaks out of
Effect\JointLaser01.jpg (Weather.MoteTrails).
| Function | What it does |
|---|---|
Map.AddTerrainLight(x, y, r, g, b, range) |
lights the ground around a world position; range in tiles, default 3 - ask every frame for as long as it should glow |
Map.LoadSound(slot, path) |
loads a .wav into sound slot slot (0..999), path from the client folder - "Data\Sound\aRain.wav"; returns its id, nothing for a bad slot or path |
Map.PlaySound(id, loop) |
plays a loaded sound, heard the same anywhere; loop true keeps it going for as long as it is asked for every frame, default false |
Ask for terrain light every frame for as long as the ground should glow. Any handler works:
the light is kept and laid down right after the client rebuilds its own, once per 40 ms game
step. The same spot asked for twice before a step counts once, and a step with no new request
keeps the last light, so the glow holds steady whatever the frame rate. o:AddTerrainLight
works the same way.
Name sound files in Registry.lua and ask Helpers.Sound(R, name) for the id, the same
way as pictures. Any .wav works - the client's own under Data\Sound or a file of yours.
A looped sound plays seamlessly for as long as the script asks for it every frame, and stops by itself a quarter of a second after it stops asking - when the player changes map or goes back to the character screen. A one-shot that is still playing is not started again.
The client keeps a pool of ambient slots for its own wildlife. On a custom map they are free, and they are still drawn and still culled for you.
| Function | Meaning |
|---|---|
Map.Ambient(i) |
slot i, 1..Map.AmbientCount(), as an ordinary object; nothing outside that |
Map.AmbientCount() |
how many slots there are |
Map.AmbientClientSlots() |
how many of them the client drives by itself |
Map.MoveAmbient(i) |
steers one slot away from crowding and back toward the group |
On a custom map the client drives every slot that holds a model your script
loaded, exactly the way it drives Tarkan's insects: it animates it, walks it, keeps
it with the group, sticks it to the ground and turns it around at a wall. It drops a
slot once it is 15 tiles from the player or has hit a wall twice.
Map.AmbientClientSlots() says so by returning all of them.
So a script only fills a free slot - position, model, scale, velocity, gravity -
and sets it live last. velocity is the walking speed; gravity is how many
degrees it may turn per frame toward the group, so a low value flocks loosely and a
high one tightly. Refill a slot the frame after it dropped, not the same frame, or
the trail of the old occupant stays attached to the new one.
Map.MoveAmbient and moving a slot yourself are only for slots past
Map.AmbientClientSlots() - on a custom map there are none.
for i = 1, Map.AmbientCount() do
local o = Map.Ambient(i)
...
endMap.Boid(i), i 1..40, and Map.BoidCount() reach the client's Boids pool - the 40 slots where
Lorencia keeps its birds, Dungeon its bats and Noria its butterflies. On a custom map the
client flies every live slot: it animates it, flocks it and moves it forward, and drops it
15 tiles from the player or at random every 20 seconds or so. Unlike an ambient slot, it
is not pinned to the ground.
The client creates nothing there, so a script fills a free slot and sets it live last,
as with ambient slots. ai is the boid state - 0 fly, 1 dive, 3 climb; 2 means sitting on
the ground, and the client does not move it - and directionZ is the climb per frame.
Weather.Birds uses both to port Lorencia's swoop over the player.
Map.Fog(color, near, far) overrides the fog this map was given, until the player
leaves it: color is 0xRRGGBB, near and far the distances from the camera where it
starts and where it is full, in world units - Map.Fog(0x2A3340, 900, 2600).
Map.Fog(false) turns it off. Normally fog is configured in
CustomMap.xdc instead - see Distance fog.
Helpers.FadeUnder, Helpers.FadeArea and Helpers.FadeAboveRoofs open a roof while
the player is under it - by object type or by rectangle, whole buildings at once, and
whatever stands on top of them. See Fading roofs.
Weather.lua ships with the sample scripts and is a library, not a handler. Each
preset belongs to one pass, so call it from the matching handler:
| Preset | Call from |
|---|---|
Weather.Motes(o) |
Map.OnUpdate |
Weather.MoteTrails(o) |
Map.OnRenderWorld |
Weather.Birds(o) |
Map.OnUpdate |
Weather.Sandstorm(o) |
Map.OnRender |
Weather.Grade(o, extra) |
Map.OnRender |
Weather.Storm(o) |
Map.OnRender |
Weather.StormBolt() |
Map.OnRenderWorld |
Weather.Rain(o) |
Map.OnRenderWorld |
Weather.Snow(o) |
Map.OnRenderWorld |
Weather.Leaves(o) |
Map.OnRenderWorld |
Weather.Steam(o) |
Map.OnRenderWorld |
Tarkan Copy uses three of them - the motes, the streaks behind them, and the sandstorm:
Map.OnUpdate(function()
Weather.Motes()
end)
Map.OnRenderWorld(function()
Weather.MoteTrails()
end)
Map.OnRender(function()
Weather.Sandstorm()
end)Weather.MoteTrails() draws the streak behind each mote out of Effect\JointLaser01.jpg
with Map.Beam, the way the client's own trail looked on Tarkan; give it the same table
as Weather.Motes() when you pass one.
Weather.Grade(name) adds a colour wash over the finished frame; the names live in
Weather.Look.
Weather.Storm() is a thunderstorm. The screen darkens, and every few seconds a lightning
bolt comes down near the player and flashes the screen and the ground. Thunder follows.
Pair it with Weather.Rain() for the drops and the rain sound:
Map.OnRenderWorld(function()
Weather.Rain()
end)
Map.OnRender(function()
Weather.Storm()
end)
-- The bolt is drawn in the world, so it has a call of its own there.
Map.OnRenderWorld(function()
Weather.StormBolt()
end)Weather.Storm({ everyMin = 2000, everyMax = 6000 }) strikes more often, and
bolt = false leaves only the flash.
A preset with a sound plays it looped while it runs: rain for the rain, wind for snow and
leaves, the desert for the sandstorm. sound = false mutes one, and sound = "name"
swaps in any sound named in Registry.lua.
Log.Add(text) and Log.AddC(color, text) print to the Lua console, which only
exists in dev mode - so they are safe to leave in. Use string.format for anything
with values in it.
Log.AddC(Enums.Color.Crimson, "object " .. o.id .. " reached the vent")- Objects and the sandbox - object properties and methods
- Adding a map - the files and the config
- Distance fog - fog per map
- Fading roofs - opening roofs over the player
MuLua Scripting Plugin | Home
🖼️ Lua UI
- File map
- Creating windows
- Client and server
- Designing a window
- Protocol
- Scrolling and scrollbars
- Controls
- Custom text
- Text input
- Debug console
- Bitmap Slicer
- Encrypting scripts
Maps
Monsters
Shared
Version 1.14 | 2026-09-09