Scripting

Vortex Engine Lua API

What Vortex Engine adds to Psych Engine's Lua, with an example for every function. In HScript, game is the PlayState: every setProperty('name', value) here is game.name = value; there.

How to read the entries: ?name is an optional argument, = value is what it is when left out, and what comes after the last : is what the function returns (Void: nothing).

Supports all of Psych Engine 1.0.4's Lua

Everything documented on ShadowMario's Psych Engine Lua Script API page works in Vortex Engine the same way: its functions, callbacks, variables and code snippets. This page only lists what Vortex adds on top of it.

Open the Psych Engine Lua API https://shadowmario.github.io/psychengine.lua/

No function matches that search.

Detecting Vortex Engine

The VortexEngine variable is true on Vortex and nil on every other engine, so one script can work on both: use what only Vortex has when it's there, and something else (or nothing) when it isn't.

function onCreatePost()
    if VortexEngine then
        setLaneUnderlay(true, 0.7)    -- only Vortex has this function
    else
        debugPrint('Playing on another engine: no lanes')
    end
end

On Vortex the dark lanes are turned on; on Psych Engine the script prints a message instead of stopping with an error.

// HScript: reading a variable that doesn't exist is an error on other engines
var isVortex = false;
try { if (VortexEngine == true) isVortex = true; } catch(e) { isVortex = false; }

Variables

Read like any of Psych's variables, in Lua and HScript.

playAsOpponent:Bool#

The player turned on "Play as Opponent": they're hitting the opponent's notes. The sides of the chart don't change: mustPress and goodNoteHit are still the right side's notes, now played by the computer. Use the notes' playable property for "the notes the player hits". The health scripts get is still Boyfriend's, as songs are made, except before the song starts and in noteMiss / noteMissPress.

function onCreatePost()
    if playAsOpponent then
        setProperty('healthDrain', 0)  -- the song's drain would hurt the player now
    end
end

Turns off the Health Drain when the player is playing the opponent's side.

onlinePlay:Bool#

The song is an online match of Vortex's multiplayer.

function onCreatePost()
    if onlinePlay then
        close() -- this mechanic would be unfair online: the script turns itself off
    end
end

The script stops running in online matches and keeps working in normal ones.

onlineHost:Bool#

An online match, and this player is the one hosting it.

If a script stops the countdown (a dialogue, a cutscene) and starts it later with startCountdown(), the two players wait for each other and the song starts at the same time for both. Nothing has to be done in the script for it.

function onSongStart()
    if onlineHost then
        debugPrint('You are hosting this match')
    end
end

Shows a message only on the host's screen.

Gameplay

setLaneUnderlay(show:Bool, ?alpha:Float = 0.5):Void#

Turns the dark lanes under the player's notes on or off and sets how dark they are, whatever the player's option says. Works at any time of the song.

  • show Bool true shows the lanes, false hides them
  • alpha Float how dark they are, from 0 (invisible) to 1 (black) (0.5)
setLaneUnderlay(true, 0.8)

Shows very dark lanes, for a part of the song with a busy background.

setScoreCamera(where:String):Void#

Where the ratings ("Sick!"), the combo numbers and "Combo" pop up. The player's option is only the default.

  • where String "game" (in the stage, moving and zooming with it) or "hud" (on the HUD, always in the same place)
setScoreCamera('hud')

The ratings stay on the screen while the stage's camera moves a lot.

setHiddenNotes(hide:Bool):Void#

Hides every note, the ones already on the screen too (the "Hidden Notes" gameplay changer), or shows them again.

  • hide Bool true hides the notes, false shows them
function onBeatHit()
    if curBeat == 128 then setHiddenNotes(true) end
    if curBeat == 144 then setHiddenNotes(false) end
end

For 16 beats the notes are invisible: the player hits them by memory.

getTallies():Table#

What the V-Slice style results screen counts, so far in the song.

Returns a table with sick, good, bad, shit, missed, combo, maxCombo, totalNotesHit and totalNotes

function onEndSong()
    local t = getTallies()
    debugPrint('Sicks: ' .. t.sick .. ' / ' .. t.totalNotes .. ', best combo: ' .. t.maxCombo)
end

Prints how many Sicks the player got and the biggest combo when the song ends.

startConversation(name:String):Bool#

Shows a V-Slice dialogue in a Psych song. Like startDialogue: call it in onStartCountdown (returning Function_Stop) or onEndSong, and the song starts (or ends) when the dialogue is over.

  • name String the conversation's file: data/dialogue/conversations/NAME.json of the mod

Returns true if the conversation was found

local talked = false
function onStartCountdown()
    if not talked and startConversation('week1-intro') then
        talked = true
        return Function_Stop -- the countdown waits for the dialogue
    end
    return Function_Continue
end

The V-Slice dialogue "week1-intro" plays before the song; the countdown starts when it ends.

setBlendMode(tag:String, mode:String):BoolMore modes#

Psych's function, with more modes on Vortex: "overlay", "hardlight", "difference" and "invert" really work on Lua sprites, drawn the way V-Slice's cameras draw them.

  • tag String the Lua sprite
  • mode String the blend mode (Psych's ones, plus the 4 above)
makeLuaSprite('light', 'stage/light', 0, 0)
setBlendMode('light', 'overlay')
addLuaSprite('light', true)

A light image that brightens and colors what's under it, like a real light, instead of covering it.

Cameras

makeLuaCamera(tag:String, ?above:String = 'hud'):Bool#

A camera of the script's own. Sprites go on it with setObjectCamera(sprite, tag), and it's moved, zoomed and tweened by its tag like the song's cameras (setProperty('tag.zoom', ...), doTweenZoom, cameraShake...).

  • tag String the camera's name
  • above String the camera it goes right over: "game", "hud", "other" or another Lua camera's tag ('hud')

Returns false if the tag is already used

function onCreatePost()
    makeLuaCamera('camBG', 'game')   -- between the stage and the HUD
    makeLuaSprite('fog', 'stage/fog', 0, 0)
    setObjectCamera('fog', 'camBG')
    addLuaSprite('fog', true)
end
function onBeatHit()
    setProperty('camBG.zoom', 1.1)
    doTweenZoom('fogZoom', 'camBG', 1, 0.4, 'quadOut')
end

A fog layer on its own camera, over the stage and under the HUD, that bops on every beat without moving the stage.

removeLuaCamera(tag:String):Bool#

Takes a Lua camera out: what's on it stops being drawn. The song's own cameras can't be removed.

  • tag String the Lua camera

Returns false if there's no Lua camera with that tag

removeLuaCamera('camBG')

The fog camera of the example above is gone, with the fog.

luaCameraExists(tag:String):Bool#

If there's a camera with that tag.

  • tag String the camera's name
if not luaCameraExists('camBG') then
    makeLuaCamera('camBG', 'game')
end

Makes the camera only once, even if this code runs again.

Turning a camera (setProperty('camGame.angle', 10)) no longer leaves empty corners on Vortex: scripts like zCameraFix aren't needed.

Shaders on whole cameras

The .frag files of the mod's shaders/ folder, the same ones setSpriteShader uses, on everything a camera shows. They need the player's Shaders option on: with it off these do nothing and return false.

addCameraShader(camera:String, shader:String, ?tag:String):Bool#

Puts a shader on a camera. A camera can have more than one: they're applied in the order they were added. Its values are set with the usual functions (setShaderFloat, setShaderBool...), using the tag.

  • camera String "game", "hud", "other" or a Lua camera's tag
  • shader String the shader's file name, without .frag
  • tag String the name its values are set by; using it again replaces the old shader (the shader's name)

Returns false if the shader wasn't found or Shaders are off

function onCreatePost()
    addCameraShader('game', 'chromatic', 'chroma')
    setShaderFloat('chroma', 'amount', 0.005)
end

A chromatic aberration on the whole stage (the HUD isn't affected).

removeCameraShader(camera:String, ?tag:String):Bool#

Takes a shader off.

  • camera String the camera
  • tag String the shader to take off, from whatever camera has it; without it, every shader of the camera
removeCameraShader('game', 'chroma')   -- just that one
removeCameraShader('hud')              -- every shader of the HUD

The stage's chromatic aberration is gone, and the HUD is back to normal.

tweenShaderFloat(tag:String, shaderTag:String, property:String, value:Float, duration:Float, ?ease:String = 'linear'):String#

Changes a float of a shader smoothly. onTweenCompleted(tag, shaderTag) is called at the end, and cancelTween(tag) stops it.

  • tag String the tween's name
  • shaderTag String a camera shader's tag, or a sprite with a shader (by the sprite's tag)
  • property String the shader's uniform float
  • value Float where it goes
  • duration Float in seconds
  • ease String the ease, like Psych's tweens ('linear')
function onBeatHit()
    setShaderFloat('chroma', 'amount', 0.02)
    tweenShaderFloat('chromaOut', 'chroma', 'amount', 0, 0.4, 'quadOut')
end

The chromatic aberration hits hard on every beat and fades away before the next one.

3D

Everything is seen in perspective from the center of the screen. Angles are in degrees.

angleX, angleY, z:Float (properties)#

New properties of Lua sprites, receptors and notes, set with setProperty / setPropertyFromGroup and tweened with startTween. angle still turns it on the screen as always.

  • angleX Float turned forward/back, around the horizontal line through its origin
  • angleY Float turned left/right, around the vertical line through its origin
  • z Float pushed away from the screen (it gets smaller, toward the center); negative brings it closer

Notes copy them from their receptor, long notes too. To turn a note on its own, set its copy3D to false and then its own values.

function onSongStart()
    for i = 0, 3 do
        setPropertyFromGroup('opponentStrums', i, 'z', 400)     -- far away
        startTween('spin' .. i, 'playerStrums.members[' .. i .. ']', {angleY = 360}, 1.5, {ease = 'quadInOut'})
    end
end

The opponent's receptors go into the distance, and the player's spin around once, with their notes.

setCamera3D(camera:String, ?angleX:Float = 0, ?angleY:Float = 0, ?z:Float = 0):Bool#

Turns everything a camera shows in perspective, like a card. Turning the HUD tilts the receptors and the notes together (the long notes too), like a 3D modchart. Needs the Shaders option on.

  • camera String "game", "hud", "other" or a Lua camera's tag
  • angleX Float leaning forward/back (0)
  • angleY Float turned left/right (0)
  • z Float pushed away (0)

All zeros put it back to normal. Returns false if it couldn't be turned (Shaders off)

setCamera3D('hud', 30)     -- the HUD leans back
setCamera3D('hud')         -- back to normal

The receptors and notes lean back like a road going into the distance, then go back to flat.

getCamera3D(camera:String):Table#

How a camera is turned now.

  • camera String the camera

Returns a table with angleX, angleY and z (zeros if it isn't turned)

local hud = getCamera3D('hud')
setCamera3D('hud', hud.angleX + 5, hud.angleY, hud.z)

Leans the HUD 5 degrees more than it already was.

tweenCamera3D(tag:String, camera:String, angleX:Float, angleY:Float, z:Float, duration:Float, ?ease:String = 'linear'):String#

setCamera3D little by little, from how the camera is now. onTweenCompleted(tag, camera) is called at the end, and cancelTween(tag) stops it.

  • tag String the tween's name
  • camera String the camera
  • angleX, angleY, z Float where it goes
  • duration Float in seconds
  • ease String the ease ('linear')
function onBeatHit()
    if curBeat % 8 == 0 then
        tweenCamera3D('sway', 'hud', 0, 20, 0, 1, 'sineInOut')
    elseif curBeat % 8 == 4 then
        tweenCamera3D('sway', 'hud', 0, -20, 0, 1, 'sineInOut')
    end
end

The HUD sways from one side to the other every 4 beats.

setPerspective(distance:Float):Bool#

How strong the perspective is, for everything 3D (sprites, notes, cameras and models).

  • distance Float how far the eye is from the screen, in pixels (1000 if never changed, at least 50). Smaller is a stronger perspective
setPerspective(500)

Things turned in 3D look much deeper, like seen through a wide lens.

3D models (.obj)

makeLuaModel(tag:String, model:String, ?texture:String, ?x:Float = 0, ?y:Float = 0):Bool#

Makes a 3D model, 200 pixels on its biggest side. It's put on the screen and handled like a Lua sprite: addLuaSprite(tag, front) (behind or in front of the characters), setObjectCamera, setProperty, startTween, removeLuaSprite.

  • tag String the model's name
  • model String models/NAME.obj of the mod (just "NAME" works)
  • texture String an image of images/, painted with the model's texture coordinates (none: plain white, tinted by color)
  • x, y Float where its center goes (0)

Returns false if the model wasn't found or has no triangles that could be read

Its properties, besides x, y, z, angleX, angleY, angle, alpha, color, visible and scrollFactor:

  • modelScale Float pixels per unit of the model
  • lighting Bool each triangle is shaded by how much it faces the light (true)
  • ambient Float how dark the side away from the light gets, 0 to 1 (0.35)
  • cullBack Bool triangles facing away aren't drawn; turn it off for open or flat models (true)
  • triangleCount Int how many triangles it has (read only)
function onCreatePost()
    makeLuaModel('ring', 'ring', 'ringTexture', 600, 300)
    setProperty('ring.angleX', 20)
    addLuaSprite('ring', false)   -- behind the characters
end

function onUpdate(elapsed)
    setProperty('ring.angleY', getProperty('ring.angleY') + elapsed * 90)
end

A textured ring (mods/MyMod/models/ring.obj with images/ringTexture.png) leaning forward and spinning a quarter turn per second, behind the characters.

The triangles are drawn from the farthest to the closest (there's no depth buffer), so models of up to a few thousand triangles work best. Faces with more than 3 corners are split into triangles. Each model has one texture.

setModelMaterialColor(tag:String, material:String, color:String):Bool#

The color of one part of a model: the parts its .obj names with usemtl (a tree's trunk and leaves). A model with no texture gets them from the .mtl file its .obj names (mtllib, next to it); without one every part is white, and this colors them.

  • tag String the model
  • material String the material's name, as in the .obj
  • color String the color, like '6B4226'

Returns false if the model has no material with that name (the error lists the ones it has)

function onCreatePost()
    makeLuaModel('tree', 'tree', '', getMidpointX('dad') - 300, getMidpointY('dad') - 80)
    setProperty('tree.modelScale', getProperty('tree.modelScale') * 2.5)
    setModelMaterialColor('tree', 'Bark', '6B4226')   -- the trunk brown
    setModelMaterialColor('tree', 'Tree', '3E9E4F')   -- the leaves green
    addLuaSprite('tree', false)
end

function onUpdate(elapsed)
    setProperty('tree.angleY', getProperty('tree.angleY') + elapsed * 20)
end

A low-poly tree with a brown trunk and green leaves next to the opponent, behind the characters, turning slowly.

getModelMaterials(tag:String):Table#

The names of a model's materials, to know what to color.

  • tag String the model
for _, name in ipairs(getModelMaterials('tree')) do
    debugPrint(name)
end

Prints "Bark" and "Tree" for the tree above.

Notes during the song

spawnNote(time:Float, column:Int, mustPress:Bool, ?holdLength:Float = 0, ?noteType:String = '', ?singers:String):Bool#

Adds a note to the song, like one of the chart's. A note added for a time that already went by comes as a miss. Note types with a script must already be in the song's chart to have their script loaded.

  • time Float when it has to be hit, in milliseconds of the song
  • column Int from 0 to getKeyCount() - 1 (0 = left on a 4-key song)
  • mustPress Bool true for the right side's notes (the player's), false for the opponent's
  • holdLength Float in milliseconds; 0 is not a long note (0)
  • noteType String a note type; '' is a normal note ('')
  • singers String who sings it: "main", "all" or extra characters' tags (the default singers)

Returns false if the column doesn't exist on this song

function onBeatHit()
    if curBeat == 32 then
        for i = 0, 3 do
            spawnNote(getSongPosition() + 2000 + i * 250, i, true)
        end
        spawnNote(getSongPosition() + 3000, 0, true, 800, 'Hurt Note')
    end
end

On beat 32, a left-down-up-right staircase comes 2 seconds later, followed by a long Hurt Note on the left.

getKeyCount():Int#

How many arrows each side has: 4 on most songs, 1 to 9 on songs with extra keys.

for i = 0, getKeyCount() - 1 do
    setPropertyFromGroup('playerStrums', i, 'alpha', 0.5)
end

Makes every receptor of the player half see-through, whether the song has 4, 6 or 9 of them.

Extra characters

Characters singing besides the opponent, the player and the girlfriend. They're set up in the chart editor (see the modding guide) or added during the song. Scripts get each one by its tag, like a Lua sprite: setProperty('pico2.alpha', 0.5), playAnim('pico2', 'singUP', true). A "Change Character" event with the tag as its first value changes that extra character.

addExtraCharacter(tag:String, character:String, ?side:String = 'opponent', ?x:Float = 0, ?y:Float = 0, ?singsAll:Bool = false, ?behind:Bool = false):Bool#

Adds an extra character during the song, the same as one of the chart's: it dances, sings the notes of its side that are marked for it (or all of them with singsAll), holds long notes and misses.

  • tag String its name for notes and scripts
  • character String the character file
  • side String "opponent", "player" or "gf" ('opponent')
  • x, y Float where it stands, from the spot of the main character of that side (0)
  • singsAll Bool sings every note of its side (a duet) (false)
  • behind Bool stands behind the main character (false)

Returns false if the tag is already used or the character doesn't exist

function onBeatHit()
    if curBeat == 64 then
        addExtraCharacter('backup', 'pico', 'opponent', -300, 0, true)
        playAnim('backup', 'singUP', true)
    end
end

On beat 64 Pico shows up next to the opponent, strikes a pose, and then sings every note with them.

removeExtraCharacter(tag:String):Bool#

Takes an extra character out of the song (one of the chart's too).

  • tag String the extra character

Returns false if no extra character has that tag

if curBeat == 128 then removeExtraCharacter('backup') end

Pico from the example above leaves on beat 128.

getExtraCharacters():Table#

The tags of every extra character of the song.

for _, tag in ipairs(getExtraCharacters()) do
    setProperty(tag .. '.alpha', 0.5)
end

Every extra character becomes half see-through, without knowing their tags.

changeStage(name:String, ?flags:String):Bool#

Changes the stage in the middle of the song, like the Change Stage event. The old stage's sprites and scripts go away, the new one is built where the characters are, and they go to its places.

  • name String the stage (its file in stages, a V-Slice stage, or one the game makes)
  • flags String (optional) separated by commas: keep (the characters stay where they are), snap (the camera is there at once), keepzoom (the zoom stays)

Returns false if the stage didn't change (it doesn't exist, it's the one already there, or a script called it off)

function onBeatHit()
    if curBeat == 64 then changeStage('school', 'snap') end
end

On beat 64 the song moves to Week 6's school, with the camera there at once.

The game's window

Whatever a song changes (title, position, size) is put back when the song is left.

setWindowTitle(title:String):Void#

Changes the text on the window's title bar.

  • title String the new title
setWindowTitle('YOU CAN\'T RUN')

The window's title bar says "YOU CAN'T RUN" until the song is left.

getWindowTitle():String#

The window's title now.

setWindowTitle(getWindowTitle() .. ' - Hard Mode')

Adds " - Hard Mode" to the end of the title.

setWindowPosition(x:Int, y:Int):Bool#

Moves the window on the computer's screen.

  • x, y Int the window's top-left corner, in pixels of the monitor

Returns false while the game is fullscreen (it isn't moved)

local baseX, baseY
function onSongStart()
    baseX = getWindowX()
    baseY = getWindowY()
end
function onBeatHit()
    if curBeat >= 64 and curBeat < 96 then
        setWindowPosition(baseX + getRandomInt(-15, 15), baseY + getRandomInt(-15, 15))
    elseif curBeat == 96 then
        setWindowPosition(baseX, baseY)
    end
end

The window shakes on every beat from 64 to 96, then goes back where it was.

getWindowX():Int · getWindowY():Int#

Where the window's top-left corner is on the monitor.

debugPrint('The window is at ' .. getWindowX() .. ', ' .. getWindowY())

Prints the window's position.

setWindowSize(width:Int, height:Int):Bool#

Resizes the window. The game is stretched to fit it, as when the player resizes it.

  • width Int at least 160
  • height Int at least 90

Returns false while the game is fullscreen, or for a size too small

setWindowSize(960, 540)

The window shrinks to 960x540, for a "the game is getting smaller" effect.

getWindowWidth():Int · getWindowHeight():Int#

The window's size now.

setWindowSize(math.floor(getWindowWidth() * 0.9), math.floor(getWindowHeight() * 0.9))

Makes the window 10% smaller than it is.

getScreenWidth():Int · getScreenHeight():Int#

The size of the monitor the window is on.

setWindowPosition(math.floor((getScreenWidth() - getWindowWidth()) / 2), math.floor((getScreenHeight() - getWindowHeight()) / 2))

Puts the window in the middle of the monitor.

isWindowFullscreen():Bool#

If the game is fullscreen (then the window can't be moved or resized).

if not isWindowFullscreen() then
    setWindowSize(960, 540)
end

Only shrinks the window when the player is playing in a window.

windowAlert(message:String, ?title:String):Void#

Shows a message box of the system. The game waits until it's closed.

  • message String the text
  • title String the box's title (the window's title)
function onEndSong()
    windowAlert('You beat me... for now.', 'Sonic.exe')
    return Function_Continue
end

A Windows message box shows up when the song ends, before the results.

Clipboard

setClipboard(text:String):Void#

Puts text on the computer's clipboard (what CTRL+V pastes).

  • text String the text
function onEndSong()
    setClipboard('I got ' .. getProperty('songScore') .. ' points on ' .. songName .. '!')
    return Function_Continue
end

The player's score is copied, ready to paste on Discord.

getClipboard():String#

The text on the computer's clipboard ('' if there's none).

if getClipboard() == 'secret code' then
    debugPrint('You found the secret!')
end

A secret that unlocks when the player copied a certain text before the song.

Callbacks

Functions the engine calls on Lua and HScript scripts, like Psych's own (onBeatHit, goodNoteHit...).

onBreakStart(seconds:Float)#

A part of the song with 3 seconds or more without notes to hit started. Called even if the player turned the Break Timer off.

  • seconds Float how long the break is, in real seconds (playback rate included)
function onBreakStart(seconds)
    doTweenZoom('breakZoom', 'camGame', 0.7, 1, 'quadOut')
end

The camera zooms out during the breaks of the song.

onBreakEnd()#

The notes are coming again (or the song ended).

function onBreakEnd()
    doTweenZoom('breakZoom', 'camGame', getProperty('defaultCamZoom'), 0.5, 'quadIn')
end

With the example above: the camera zooms back in when the notes come back.

onConversationEnd(name:String)#

A V-Slice dialogue started with startConversation ended.

  • name String the conversation that ended
function onConversationEnd(name)
    if name == 'week1-intro' then
        playSound('confirmMenu')
    end
end

A sound plays when the intro dialogue is over, right before the countdown.

onChangeStage(newStage:String, oldStage:String)#

The stage is about to change (the Change Stage event, or changeStage). Return Function_Stop to call it off.

function onChangeStage(newStage, oldStage)
    if newStage == 'limo' and not getModSetting('limo') then
        return Function_Stop
    end
end

The limo only comes if the mod's setting says so.

onChangeStagePost(newStage:String, oldStage:String)#

The stage changed. curStage, defaultBoyfriendX/Y, defaultOpponentX/Y and defaultGirlfriendX/Y already have the new stage's values.

function onChangeStagePost(newStage, oldStage)
    cameraFlash('camGame', 'FFFFFF', 0.5)
end

A white flash hides the moment the stage changes.

Properties

With setProperty / getProperty. Each one starts with the player's option and can be changed at any time of the song.

PropertyTypeWhat it is
iconBopBoolThe health icons bop on the beat.
sustainWiggleBoolThe long notes wiggle (needs Shaders on).
showBreakTimerBoolThe countdown shown when there are 3 seconds or more without notes.
ghostNotesBoolTwo notes of the same character together leave an afterimage of the first pose.
healthDrainFloat, 0 to 2Every note the opponent hits takes health. It never kills: health stops at 0.01.
speedMissBoolEvery miss makes the song 5% faster, up to 2x.
hiddennotesBoolNotes that show up from now on are invisible (setHiddenNotes also hides the ones already there).
showLaneUnderlayBoolShows or hides the lanes (use setLaneUnderlay to make them when the player's option is off).
laneUnderlayAlphaFloat, 0 to 1How dark the lanes are.
totalColumnsInt (read)Arrows per side (same as getKeyCount()).
isRestingBool (read)A break is going on.
maxRestTimeFloat (read)How long the current or last break is, in seconds.
maxComboInt (read)The biggest combo of the song.
scoreCameraString (read)"Game" or "HUD" (change it with setScoreCamera).
isVSliceStageBool (read)The stage is a V-Slice stage.
playableCharacterCharacterThe character the player is playing (Boyfriend, or the opponent with Play as Opponent).
otherCharacterCharacterThe one singing by itself.
function onSongStart()
    setProperty('healthDrain', 1.5)
    setProperty('ghostNotes', true)
    debugPrint('You are playing ' .. getProperty('playableCharacter.curCharacter'))
end

A harder song: strong health drain and ghost notes, whatever the player's options are.

On notes (getPropertyFromGroup('notes', i, ...)): playable (the player has to hit it, also with Play as Opponent), singers (who sings it), copy3D, angleX, angleY, z.

On the health icons (iconP1 / iconP2): autoUpdate (false: the game stops placing and bopping it), isAnimated and hasWinningIcon (read only).

Objects of Vortex

Vortex's own things on the screen, to move, recolor or hide like any other sprite.

NameWhat it is
restCountdownTxt, restTimerBar, restportraitThe Break Timer's text, bar and picture.
keystrokesThe Keystrokes display (null when the option is off).
laneUnderlaysThe lanes (a group).
camSus, camLanesThe cameras of the long notes and of the lanes (under the HUD's).
camOverlayThe camera over every other one, with the keystrokes and the Break Timer.
susWiggleThe wiggle of the long notes: waveAmplitude, waveFrequency, waveSpeed.
grpHoldCoversThe sparks on the receptors while a long note is held.
onlineStatsTxtOnline: the other player's name, score, misses and accuracy (null when not online).
function onCreatePost()
    setProperty('restCountdownTxt.color', getColorFromHex('FF66CC'))
    setPropertyFromGroup('laneUnderlays', 0, 'color', getColorFromHex('220033'))
    setProperty('susWiggle.waveAmplitude', 0.15)
end

A pink Break Timer, a purple left lane, and long notes that wiggle more.

HScript

HScript reaches everything above directly (game.spawnNote(...), game.addExtraCharacter(...), FlxG.cameras, FlxG.stage.window), and has a few things of its own:

FlxColor.fromString('0x80FF0000');         // "0xAARRGGBB" and "#RRGGBB", with alpha too
FlxColor.interpolate(color1, color2, 0.5); // a color between two others
FlxColor.gradient(color1, color2, 10);     // a list of 10 colors from one to the other

game.playableCharacter; game.otherCharacter; // the one being played and the one singing by itself
game.playableStrums; game.otherStrums;       // the receptors of each
game.makeStrumsEarly();  // in onCreatePost: the receptors now, to move them before the song
game.extraCharacters;    // a list: .tag, .side, .char
game.getExtraCharacter('pico2').char;

Menus and the loading screen have scripts of their own: see state scripts.

Making a mod for other engines too

Everything on this page only exists in Vortex Engine. The mods menu has a CHECK MOD button: it reads a mod's scripts, charts and icons and lists everything it uses that only Vortex has, with the file and line of each one.

LevelMeaning
ERROROther engines stop the script there (a function only Vortex has).
DIFFERENTIt works differently or not at all there (callbacks, properties, blend modes, extra characters).
NOTEUsually fine (a variable that is just nil there, an option that reads as nothing).

A mod with no ERROR or DIFFERENT should also work on Psych Engine 1.0.4. Checking the VortexEngine variable before using something is how a script can work on both.