Repository navigation
CustomGame Objects
Map scripts and monster scripts share one Lua state, one object API and three shared files. This page is the reference for all of it.
Every handler that receives an o receives the same thing, whether it came from
Map.OnObject, Monster.OnRender or Map.Ambient(i). Properties are read and
written directly.
A tile is 100 world units. Colours and brightness are floats from 0 to 1.
| Property | Meaning | Example |
|---|---|---|
scale |
size multiplier; 1.0 is the model's own size |
o.scale = 1.4 |
alpha |
opacity: 0 invisible, 1 solid |
o.alpha = 0.5 |
timer |
a counter of the object's own; some client objects step their animation on it, and a script driving an object can do the same | o.timer = o.timer + 0.1 |
hiddenMesh |
which part of the model is not drawn: -1 nothing, every mesh is drawn; 0, 1, 2... that one mesh (a monster's m.meshCount says how many it has); -2 the whole model, while its script and effects keep running |
o.hiddenMesh = 2 hides mesh 2, o.hiddenMesh = -2 everything |
blendMesh |
a mesh drawn glowing (added) instead of solid; -1 none |
o.blendMesh = 0 |
blendMeshLight |
brightness of the blendMesh: 0 invisible, 1 full |
o.blendMeshLight = 0.65 |
blendMeshTexCoordU blendMeshTexCoordV
|
texture offset, 1 being one whole texture; set it from world time every frame to scroll |
o.blendMeshTexCoordV = -(Map.WorldTime() % 10000) * 0.0001 - one turn every 10 s |
positionX positionY positionZ
|
world position: tile 184, 23 is x 18400, y 2300; Z is the height |
o.positionZ = Map.TerrainHeight(o.positionX, o.positionY) |
angleX angleY angleZ
|
rotation in degrees; angleZ turns it round the vertical axis - which way it faces |
o.angleZ = 90 |
lightR lightG lightB
|
the object's light colour; 1, 1, 1 is full brightness |
o:SetLight(1, 0.6, 0.2) - warm orange |
boundingBoxMaxX/Y/Z |
far corner of the box the client clicks and culls the object by, in world units from its origin | Hydra: 100, 100, 150
|
renderShadow |
this object's own shadow switch | o.renderShadow = false |
enableShadow |
the wider shadow gate over renderShadow
|
o.enableShadow = false |
shadowScale |
size of the shadow | birds: 10
|
visible |
whether the camera sees it - the client works it out again every frame, so it is for reading | if not o.visible then return end |
live |
the object exists; on a Map.Ambient or Map.Boid slot false frees it, and true, set last, starts it |
m.live = true |
velocity |
speed per frame of an ambient or boid slot the client moves | birds: 1
|
gravity |
on an ambient or boid slot, how many degrees a frame it may turn while flocking | motes 9, birds 13
|
lifeTime |
frames an ambient object has left | motes: 100
|
ai |
a boid's state: 0 flying, 1 diving, 2 on the ground, 3 climbing - see Birds in Map scripts
|
b.ai = 0 |
directionX/Y/Z |
a boid's movement; directionZ is its climb per frame |
b.directionZ = -20 - a dive |
alphaTarget |
the alpha the client's own fade moves towards |
o.alphaTarget = 1 |
alphaFadeRate |
the part of the remaining gap that fade covers each frame |
0.12 - about a second |
alphaEnable |
the blended render path - leave it off on map objects, it breaks their shadow and depth; fade them through alpha as Fading roofs does |
ambient motes: true
|
lightEnable |
lit by the terrain light where it stands | m.lightEnable = true |
billBoard |
always turned to face the camera, like a sprite | motes: true
|
eyeLeftX/Y/Z |
where a trail created on the object hangs; keep it on the object for as long as the trail should follow | m.eyeLeftX = m.positionX |
type |
the object's type: on a map the model it was placed as, rewritten by the client every frame; on an ambient or boid slot the model to fly | m.type = Helpers.Model(R, "bug") |
subType |
the type's variant; most objects use 0
|
m.subType = 0 |
| Property | Meaning | Example |
|---|---|---|
id |
a stable key for this object, for script-side state kept in a table | state[o.id] = { phase = 0 } |
animation |
the object's animation number; with type it picks the object's row in the object tables
|
0 |
currentAction |
the action playing now - see Actions | if o.currentAction == Enums.Action.Walk then |
hiddenMesh = -2 hides the entire model, which is the usual way to keep an
invisible marker object and draw your own effect at its position.
| Method | What it does | Defaults |
|---|---|---|
o:SpawnParticle(type, subType, scale, x, y, z) |
a particle, tinted with the object's light; type is a picture id - Helpers.Texture(R, "smoke")
|
subType 0, scale the object's, x, y, z the object's position |
o:SpawnSprite(type, scale, rotation, subType, cols, rows, ticks, x, y, z, r, g, b) |
a flat picture facing the camera, rotation in degrees, optionally animated - see Sprite sheets
|
scale the object's, rotation 0, subType 0, cols, rows, ticks 1, position the object's, r, g, b its light |
o:SpawnEffect(type, subType, scale, x, y, z, r, g, b, angleX, angleY, angleZ) |
one of the client's model effects, type from Helpers.ClientModel(R, name) - see Client effects
|
subType 0, scale 0 - the effect's own size, position the object's, r, g, b its light, angles its angles |
o:SpawnJoint(type, subType, scale, targetX, targetY, targetZ) |
one of the client's trails from the object to a point, type a client joint id - none ship, a trail of your own picture is Map.Beam; scale is its width |
subType 0, scale 10, target the object's position - a spark in place |
o:TransformPosition(bone, dx, dy, dz) |
a model-local offset through bone bone → world x, y, z; nothing for a bone the model does not have |
dx, dy, dz 0 |
o:SetAction(action) |
plays an action from Enums.Action - and, with <SoundAction>, its sound; one the model does not have is ignored |
- |
o:SetLight(r, g, b) |
sets lightR/G/B at once, 0..1
|
a missing value is 0 |
o:AddTerrainLight(r, g, b, range) |
lights the ground around the object, r, g, b 0..1, range in tiles |
range 3 |
o:SetTexCoord(u, v) |
sets blendMeshTexCoordU/V at once |
a missing value is 0 |
o:GetPosition() |
→ x, y, z
|
- |
TransformPosition returns nothing for a bone the model does not have, so always
check:
local x, y, z = o:TransformPosition(4, 0, -20, 0)
if x then
o:SpawnParticle(smoke, 0, 1.0, x, y, z)
endAddTerrainLight's range defaults to 3 - enough that a lava pit lights the rock
around it. Call it every frame for as long as the ground should glow; the light is laid
down once per 40 ms game step - see Light and sound in Map scripts.
SpawnSprite's last three arguments animate it: the texture is cut into
cols x rows cells, the frame advances every ticks of world time, and the frame
count is cols * rows. All three default to 1, meaning no animation. Values below 1
are raised to 1.
Scripts run in an isolated state with a deliberately small standard library:
Available: base, table, string, math, and from os only time, clock,
date and difftime.
Not available: ffi, jit, package, io, require on anything outside the
shipped files, loadstring, os.execute / remove / rename / getenv / exit,
and every Lua UI binding.
There is no bit library, so flag tests are arithmetic:
local isMonster = math.floor(kind / Enums.Kind.Monster) % 2 == 1os.* is wall clock. For anything that animates, use Map.WorldTime() - it follows
the client's own clock, so effects do not drift with the frame rate or keep running
while the game is paused.
Map.* may only be called from a map script and Monster.* only from a monster
script, even though both are visible everywhere. A mismatch is an error at load
time rather than a handler that silently replaces someone else's.
Five shared files sit next to the script folders. A script loads one with
require("Name"); each loads once per state and is shared from then on.
Data\Custom\Scripts\Game\Enums.gsc names
Data\Custom\Scripts\Game\Helpers.gsc functions
Data\Custom\Scripts\Game\Registry.gsc what may be loaded, and from where
Data\Custom\Scripts\Game\Weather.gsc weather presets
Data\Custom\Scripts\Game\SkillEffects.gsc monster skills round the caster
Names only - no functions.
| Table | Contents |
|---|---|
Enums.Action |
animation actions |
Enums.AttackAction |
the attack actions in <AttackRate A1..A5> order |
Enums.RenderFlags |
body render flags |
Enums.Skill |
skill ids |
Enums.Kind |
what an object represents - bit flags |
Enums.TerrainWall |
what a tile allows, as Map.TerrainWall returns it - bit flags |
Enums.Color |
named colours, packed 0xRRGGBBAA
|
The numbering is not contiguous - the extra attacks were appended after the
original eight, so Attack3 is 8 rather than 5.
| Name | Value | Name | Value | |
|---|---|---|---|---|
Stop1 |
0 | Die |
6 | |
Stop2 |
1 | Appear |
7 | |
Walk |
2 | Attack3 |
8 | |
Attack1 |
3 | Attack4 |
9 | |
Attack2 |
4 | Run |
10 | |
Shock |
5 | Attack5 |
11 |
o:SetAction() is safe with any of them - an action the model does not have is
dropped. The client starts Appear and Run only for a few of its own monsters; on a
custom monster they play when a script sets them.
Enums.Color values are packed 0xRRGGBBAA, which is what Log.AddC takes. It is
not what lights take - SetLight, bodyLight* and the colour arguments of
SpawnSprite / SpawnEffect all want three 0..1 floats, so run the value through
Helpers.LightOf first:
o:SetLight(Helpers.LightOf(Enums.Color.Gold))Some of the client's effects decide what they do by their id - a thrown stone's arc,
Inferno's ring of fire - so a copy of the model in a slot of your own would only stand
still. Such an effect is named in Registry.lua by the file the client loads it from,
and Helpers.ClientModel(R, name) asks the plugin which id the client gave that file.
No number sits in the script or in the plugin, so a new client needs no id hunting:
local inferno = Helpers.ClientModel(R, "inferno")
if inferno then
o:SpawnEffect(inferno, 0, 0, x, y, z)
endAnything that is only a picture - a laser, a trail, a flame - is drawn from your own
copy instead, with o:SpawnSprite or Map.Beam.
A few of the client's effects are built in its code and load no file at all - Raining
Arrow's rain is one. Those cannot be named this way, so SkillEffects.lua works the id out
from the files of the effects sitting next to it and refuses it unless they line up; see
Monster skills. Either way no script writes a client number down.
| Function | Purpose |
|---|---|
Helpers.Oscillate(periodMs) |
smooth 0..1 oscillation |
Helpers.PhaseOf(o) |
per-object phase from its rotation, no state kept |
Helpers.AtBones(o, bones, fn) |
run fn(x, y, z) at each bone, skipping missing ones |
Helpers.RenderLight(o, texture, scale, bone, ox, oy, oz) |
a pulsing sprite pinned to a bone |
Helpers.RenderLightAt(o, texture, scale, x, y, z) |
the same at a position already worked out |
Helpers.LightOf(rgba) |
unpacks 0xRRGGBBAA into three 0..1 floats |
Helpers.Texture(R, name) |
texture id for a Registry name, loading on first use |
Helpers.Model(R, name) |
model id for a Registry name, loading on first use |
Helpers.ClientModel(R, name) |
the id the client gave one of its own model files, for a client effect - see Client effects |
Helpers.Sound(R, name) |
sound id for a Registry name, loading on first use |
Helpers.NewIdSet(startAt, maxCount, label) |
a named id pool |
Helpers.FadeUnder(o, cfg) |
fades a roof while the player is under it - see Fading roofs |
Helpers.FadeArea(o, area) |
the same for whatever stands in a rectangle of tiles |
Helpers.FadeAboveRoofs(o) |
whatever hangs over the player in a whole + upper building opens with its roof |
Helpers.RegisterAll() |
registers every object type, at load |
Use RenderLightAt when several lights share one bone - the transform is then done
once instead of per light.
Data only: what may be loaded and from where. Adding a picture is a one-line change.
R.file =
{
-- a plain string is the common case
raindrop = "World1\\rain01.tga",
-- a table adds the wrap mode and the filter
sand01 = { path = "Effect\\sand01.jpg", repeat_ = true },
}
R.modelFile =
{
bug = { folder = "Data\\Object161\\", name = "Bug", index = 2, file = "Bug02.bmd" },
}
-- a client effect, by the file the client loads it from - for Helpers.ClientModel
R.clientModel =
{
inferno = "Data\\Skill\\Inferno01.bmd",
}
R.sound =
{
-- any .wav, the client's own or a file of yours
rain = "Data\\Sound\\aRain.wav",
}| Key | Meaning |
|---|---|
path |
file, written as the logical .jpg / .tga name - the loader finds the packed form |
repeat_ |
wrap mode. Required for anything whose texture coordinates scroll past 1, or the edge pixels smear across the screen |
smooth |
mipmapped filter. Only for a picture that has mipmaps - without them it draws as flat white |
Then in a script:
local R = require("Registry")
local sand = Helpers.Texture(R, "sand01")Resolve on first use rather than at load - a picture needs a map that is already up. Asking again is free once the slot is filled.
This Registry.lua has nothing to do with the Lua UI one. Different state, different
pools, and the same name in both means two different things.
| Function | Purpose |
|---|---|
Log.Add(text) |
one line to the Lua console |
Log.AddC(color, text) |
the same, coloured |
RGBA(r, g, b, a) |
packs a colour for Log.AddC
|
The console only exists in dev mode, so both are no-ops otherwise and safe to leave
in. Use string.format for anything with values in it.
-
Map scripts - the
Map.*API -
Monster scripts - the
Monster.*API and render flags
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