Luawe the Wayfarer Engine's scripting language All pages

Things, the cube and the camera

This page is about what a script can reach in the level it runs in: its things, the player's cube, the camera, the level's variables, its sounds, and the keys the player presses.

One rule runs through all of it: a script never changes the level itself. What it does lasts for the run. When the player tries again, the level is as its maker built it, and the scripts start afresh.

Finding things

A thing is an object (a block, a saw, a spike, a decoration), a piece of floor, a text or a group. Scripts find things by the name they were given in the editor, at the top of the thing's rows in the inspector.

local door = things.find("door") -- the first thing called door, or nil
local lamps = things.all("lamp") -- a list of every thing called lamp

print(door, #lamps)

Several things can share a name. That is how a script handles many at once:

for _, lamp in things.all("lamp") do
    lamp.alpha = 0.3
end

Reading and changing a thing

A thing's numbers are the ones its rows show in the editor, under the names a level file uses:

WrittenIs
thing.x, thing.yWhere its left and top edges are, in tiles (y grows downwards), as its rows in the editor show them
thing.width, thing.heightHow big it is (also thing.w, thing.h)
thing.rotationHow far it is turned, in degrees
thing.alphaHow solid it looks: 1 fully, 0 not at all
thing.color.r, .g, .bIts colour, each from 0 to 255
thing.visibleWhether it is there at all
thing.textA text's words

Set one and the thing changes at once:

local door = things.find("door")
door.x += 2          -- two tiles to the right
door.rotation = 45
door.color.r = 255
door.visible = false -- it sinks away, and can't be touched

A thing's children have numbers too, reached through the child's name: thing.blur.amount, thing.particles.rate. And every number can be read and set by its id as text, which is handy when the id is in a variable:

local door = things.find("door")
door:set("color.g", 120)
print(door:get("color.g"))

What a thing can tell about itself:

WrittenIs
thing.nameIts name
thing.kind"object", "floor", "text" or "group"
thing.typeFor an object: "block", "saw", "spike" or "deco"
thing.existsfalse once it was destroyed
thing.touchingIs the cube touching it right now?
thing.centerX, thing.centerYWhere its middle really is right now, with its keys, its group and whatever else moves it (read only)

Gliding

Setting a number makes a thing jump. tween makes it glide there instead:

local door = things.find("door")

door:tween({ y = door.y - 3 }, 1.5) -- up three tiles, in a second and a half
door:tween({ alpha = 0, rotation = 90 }, 2, "back", "inout")

The values are: a table of the numbers to move and where to, how many seconds it takes (1 if left out), and how it moves: an ease and a direction.

EaseMoves
"linear"At one speed
"sine", "quad", "cubic", "quart", "quint", "expo", "circ"Gently, each a little sharper than the one before
"back"Past the end and back
"bounce"Like a ball coming to rest
"elastic"Like a spring

The direction is "in" (starts slowly), "out" (ends slowly) or "inout" (both). Left out, a tween is "quad", "out".

tween does not wait: the script goes straight on while the thing glides. It gives back an event that happens when the glide has arrived, so waiting for it is one word more:

local door = things.find("door")
door:tween({ y = door.y - 3 }, 1):wait()
print("the door is open now")

To time something by the music, beats(n) gives the seconds that n beats take: door:tween({ y = 2 }, beats(4)) lasts exactly four beats.

Making and removing things

things.new makes a thing while the level runs:

local saw = things.new("saw", { x = 10, y = 4, w = 2, h = 2 })
local label = things.new("text", { x = 10, y = 2, text = "Mind the saw", size = 0.8 })
local spark = things.new("deco", { x = 10, y = 6, life = 2 }) -- gone after two seconds

The kinds are "block", "saw", "spike", "deco", "floor" and "text". The table says where its middle is (x, y), how big it is (w, h), and optionally its name, a text's text and size, a life in seconds, and any other number that kind of thing has (rotation = 45, alpha = 0.5, ["color.r"] = 255).

thing:clone() makes a copy of a thing, and thing:destroy() removes one for the rest of the run:

local coin = things.find("coin")
for i = 1, 5 do
    local copy = coin:clone()
    copy.x = coin.x + i * 2
end

coin.touched:wait()
coin:destroy()

Children

A child is something a thing does or has: it follows the cube, it is blurred, it gives off particles. thing:addChild puts one on, with the child's numbers in a table:

local ghost = things.find("ghost")
ghost:addChild("follow", { influence = 0.5 })
ghost:addChild("blur", { amount = 0.4 })

if ghost:hasChild("blur") then
    ghost.blur.amount = 0.1
end
ghost:removeChild("follow")

The kinds are "follow", "lookat", "blur", "opacity", "proximity", "nohitbox", "gradient", "particles" and "image".

The cube

player is the player's cube.

print(player.x, player.y) -- where it is, in tiles

player.size = 0.5         -- drawn half as big
player.trail = false      -- no trail of sparkles
player.god = true         -- nothing kills it
player.sensitivity = 0.5  -- it moves half as far for a move of the mouse
WrittenIs
player.x, player.yWhere the cube is (read only: the player decides)
player.sizeHow big it is drawn: 1 as always
player.visibleDoes it show?
player.trailDoes it leave its trail?
player.crashDoes it burst into pieces when it dies?
player.godGod mode: while true, nothing kills it
player.sensitivityHow far it goes for a move of the mouse: 2 is twice as far
player.influenceHow much the camera takes it along, from 0 to 1

These start as the level's The cube rows have them (Level, in the inspector).

The cube goes where the player puts it, so a script cannot set player.x. It can put the cube somewhere else in one jump, and the mouse goes along:

player:teleport(12, 4)

Dying, and not dying

player:kill() fails the run. The other way round is more interesting: player.hurt is an event that happens in the moment the cube touches something that would kill it, and player:revive(), called then, saves it.

local lives = 3

player.hurt:connect(function()
    if lives <= 0 then
        return -- no lives left: this time the run is over
    end
    lives -= 1
    player:revive()

    -- Nothing hurts it for three seconds: without that it would die the moment after.
    player.god = true
    wait(3)
    player.god = false
end)

The camera

camera reads where the camera looks and sets what it does besides.

print(camera.x, camera.y)

camera.zoom = 1.5 -- one and a half times as near
camera.roll = 10  -- turned ten degrees
camera:tween({ zoom = 1, roll = 0 }, 2)

Where the camera looks is its path in the level: a script can read camera.x and camera.y, not set them. Its other numbers are free: zoom, roll, water (how fast the water flows), and the numbers of its children by their ids, such as camera.vignette.density or camera.blur.amount.

camera:effect gives the screen a hit that fades out by itself:

camera:effect("shake", 0.6, 0.5) -- what, how strong, for how many seconds
camera:effect("flash")

The effects are "pulse", "shake", "blur", "rgb" and "flash".

The level's colours are reached the same way, through theme: theme.water.r, theme.floor.g and so on, each from 0 to 255.

The level

level tells about the level and ends the run:

print(level.title, level.author, level.bpm, level.length)

level.checkpoint() -- a try that fails starts again from here
level.win()        -- the level is completed

level.fail() fails the run, like player:kill().

level.checkpoint() sets a checkpoint where the cube is. It keeps everything as it is at that moment, the scripts with their variables included: after a fail, each script carries on from where it was when the checkpoint was set.

The time is in time: time.now is the seconds since the level began, time.beat the song's beat (with decimals), time.delta the seconds since the last step.

The level's variables

The variables made in the Logic tab are in vars, by name. A script and the nodes see the same ones, so this is how the two kinds of logic talk to each other: a node can count coins and a script can read the count, or the other way round.

vars.coins += 1
if vars.coins >= 10 then
    vars.doorOpen = true
end

A variable has to be made in the Logic tab first (under Variables), with its kind: a number, yes / no, words or a list. A script cannot make a new one, and setting one that is not there is an error. For values only the script needs, use the script's own local variables.

A text in the level shows a variable when its words have the variable's name in curly brackets, like Coins: {coins}. That works for what a script sets, too.

Sounds

sound.play("@coin") -- one of the game's own sounds
sound.play("assets/ding.wav", { volume = 0.5, pitch = 1.5 })

local hum = sound.loop("assets/hum.wav") -- over and over, until it is stopped
wait(4)
hum:stop()

A level's own sounds are the files in its Assets tab, by the path shown there. The game's sounds start with @: @coin and @door. The options are volume (0 to 1), pitch (1 as it is, 2 an octave up) and from and to in seconds, to play only a part of the sound.

Keys and the mouse

input tells what the player is pressing. This is what makes a level a small game of its own.

-- Asked at any moment:
if input.isDown("space") then
    print("space is held")
end

-- Or told when it happens:
input.keyDown:connect(function(key)
    if key == "e" then
        print("E was pressed")
    end
end)

input.mouseDown:connect(function(button)
    print(button .. " mouse button")
end)

Keys go by their names in small letters: "a", "1", "space", "return", "left", "up", "left shift". The mouse buttons are "mouse left", "mouse right" and "mouse middle" for input.isDown, and "left", "right" and "middle" in the events. input.wheel happens when the wheel turns, and input.mouseMove whenever the mouse moves the cube (the cube is where the mouse is: player.x, player.y).

Next: waiting and tasks.