Skip to content

CustomMaps Scripts

wezzzyrek1 edited this page Sep 18, 2026 · 1 revision

Map 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.

How a script starts

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.

Order of execution

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.

Registering object types

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)    -- lava

The 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.

Map.OnObject

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

The Map API

Handlers

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

Time and place

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 end

Loading pictures and models

Never 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.

Drawing

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)

Free particles

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).

Light and sound

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.

Ambient slots

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)
    ...
end

Birds

Map.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.

Fog

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.

Roofs and buildings

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

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.

Logging

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")

See Also

Clone this wiki locally