sg* API Reference
Regenerated: 2026-09-17 from the engine as it exists · Engine release: v66 · Commit:
57ef2d0
Every entry below was read out of the source. Where a name appears here, it exists; where
it does not appear, it does not exist. The previous version of this file documented
sixteen functions that were never in the engine and omitted five entire modules.
Audience — read this before anything else
The
sg* names span three different runtimes. Calling one from the wrong place fails
silently, and mixing them up is the single most common mistake this reference exists to
prevent.
| Marker | Where it runs | Who can call it |
|---|---|---|
| GAME | The game page (the engine) | Your game scripts, model script hooks, the browser console on a running game |
| STUDIO | The Studio editor page | Studio JS only. Not reachable from a game script |
| SERVER | packages/game-server, inside the game's own server | Server-side scripts only |
Two message buses, and they are not interchangeable
sg:* names travel on both window CustomEvents and postMessage, and a
listener on one never hears the other.
- Events (§14) are CustomEvents on
window. Usewindow.addEventListener('sg:…')
and
window.dispatchEvent(new CustomEvent('sg:…', { detail })).
- Commands (§15) are
postMessagefrom the Studio into the game iframe. The handler
reads
e.data.type, so a CustomEvent with the same name is dropped on the floor.
The engine itself gets this wrong once:
sg:clearTerrain is dispatched as a CustomEvent
and only handled as a message, so it never fires. It is masked because
sg:sceneLoaded
clears terrain anyway.
1. Scene, models and animation — sg-scene-manager.js
sgScene, sgCamera, sgRenderer, sgControls, sgOrbitControls // live THREE objects
sgGameConfig // the running game's config (legacy alias: sgSceneConfig)
sgModels // { label: THREE.Object3D }, populated as models load
sgOnModel(label, cb) // cb(mesh) now if loaded, else when it loads
sgGetModel(id) // by model ID (not label) → Object3D | null
sgCurrentScene() // → sceneId
sgLoadScene(id, transition = 'fade', duration = 0.5, reason)
sgTransitionScene(...) // alias of sgLoadScene
sgSetPosition(label, x, y, z)
sgSetRotation(label, x, y, z) // DEGREES
sgSetScale(label, s) // uniform only
sgPlayAnim(label, clip, opts = {}) // a clip it lacks: its idle (not its first clip)
sgStopAnim(label)
sgPlayPath(label) / sgStopPath(label)
sgRoam(label, opts = {}) // { radius = 5, speed = 1 }
sgStopRoam(label)
sgSetFog({ density, color, lerpSec })
sgLoadGLB(url) // → Promise<gltf>, cached per URL
sgCloneGLB(url) // → Promise<Object3D>, skeleton-safe
sgCloneCharacter(url) // → Promise<{ object, body, clips }>
sgSetMaterialOverride(url, override) / sgGetMaterialOverrides() / sgApplyMaterialOverrides(map)
sgPlatformGroundAt(x, z, currentY) // cloud-platform top under a point, or null
sgGroundUnder(x, z, nearY) // the ground a foot stands on: terrain, a platform, or a blocker top no more than 0.45 m above nearY; null off every ground
sgGroundTypeAt(x, z, y) // 'grass' | 'stone' | 'wood' | 'sand' under a point (footsteps)
sgBlockerShape(obj) // { inv, lbox }: a blocker's own box in its turned frame; a blocker entry that carries it is stood on only over that box
sgGroundTypes() // ['grass', 'stone', 'wood', 'sand']
sgSettleMaterials(root) // decide blending from the texture (sg-materials.js)
sgSettleNpc(id)
sgOnZoneTransfer(fn) / sgRequestZoneTransfer(legId) / sgInitZoneLegs(legs)
sgAddonUrl(sub) // resolve a three.js addon URL for this release
Always pass a
reason to sgLoadScene. It feeds the 40-entry flight recorder
(
window._sgFlowLog) that ?sgdebug=1 reads; transitions without one are
unattributable.
2. Player — sg-player-controller.js
sgGetPlayerStat(path) // e.g. 'vitals.health.current'
sgSetPlayerStat(path, val)
sgGetPlayerState() // deep copy of all attributes
sgPlayerMotion() // { speed, moving, grounded, swimming, crouching, … }
sgPlayerIsDead()
sgPlayerAnimRole() // current animation role
sgPlayerEmote(name)
sgGetPlayerBirthDay() / sgGetPlayerBirthWeekDay()
sgCreateAnimator(mixer, clips, overrides)
sgCameraMode() // the camera mode now: 'first-person' | 'third-person' | 'top-down' | 'none'
sgCameraModes() // the modes Switch Camera steps through (empty with no player)
sgSwitchCamera(to) // next allowed mode, or to if allowed; returns the mode now
// Camera shots — sg-cinematic.js (game.cinematics: shots, sequences, options; Studio > Camera shots)
sgPlayShot(id) / sgPlaySequence(id) // play; the body is held still, game keys and phone controls wait; → true if it started
sgReturnToPlayerCamera() // stop now: the camera is back where it was, in the same view mode; → the mode
sgCinematicPlaying() // null, or { kind, id, time, length, step }
sgCinematicPose(id, t) // where shot id puts the camera t seconds in: { x, y, z, look, fov }
sgCinematicLength(kind, id) // seconds a 'shot' or a 'sequence' lasts (the server holds a player that long)
sgCinematicMarkers() // the editor's camera outlines: [{ id, visible, x, y, z }] (none in play)
sgIsPlayMode() // sg-scene-manager.js: true in play, false in the editor
// Events: sg:cinematicStart { kind, id, length } · sg:cinematicEnd { kind, id, reason: 'done'|'skip'|'stop', server, freed } (server: the game server started it; freed: the server already freed the player)
// Skip: the skip keys (KeyboardEvent.code list) or a tap, once skip.after seconds have passed.
// Story actions: playShot, playSequence, playerCamera. A scene with screenType 'cinematic'
// plays scene.cinematic.sequence, then goes to scene.cinematic.next ('a|b' fallbacks; '' = next slot).
3. Characters and appearance — sg-character.js
sgCharacterDefaults() // neutral slider set
sgResolveAppearance(seed, opts) // deterministic sliders from a seed
sgAppearanceScale(entry, appearance) // root scale for a real-world height
sgApplyAppearance(root, entry, appearance) // mutate a loaded character
sgApplyTint(root, tint)
sgApplyRestPose(root, entry, opts) / sgStartIdle(root, entry, opts)
sgBuildWarp(entry, appearance)
sgJointsOutside(root, entry) // canonical joints outside the skin (six axis lines, bind space; for a warning)
sgCharacterBases() // the shipped base list
sgSetAppearance(appearance) / sgGetAppearance()
sgTickSecondary(root, dt, state) // tails and wings sway as the body moves
sgFootIK(root, entry, dt, state, opts) // feet placed on the ground after the mixer; -> { weight, hipDrop, feet[] }
sgLookIK(root, entry, dt, state, target, opts) // head, neck and chest turn toward a world point (null: turn back, let go)
sgAnimEventsTick(mixer, events, state, fire) // clip events crossed since the last call, once per loop
sgCharacterMotion // { defaults(), settings(), choice(id, key), lookAt(who, at, layer), stopLooking(who, layer), looks(), eventLog(), eventsFor(url, clip), tick(r, id, dt, ctx) }
sgLookAt(who, at) / sgStopLooking(who) // who: '__player__' or a model id; at: a model id, '__player__' or { x, y, z }
Feet on ground, Look at speakers, Footstep sounds (undeployed). The scene manager calls
sgCharacterMotion.tick for every character it animated this frame, after the mixer, in play
mode only, and only within the near character tier (
characterLOD.mid, 15 m by default);
beyond it nothing runs. The stand-in body is skipped.
sgFootIK measures each sole once in
the foot bone's own frame (the lowest skin point bound to foot and toe), takes the lower sole
at the first frame as the clip's ground, and each frame moves a foot that is down (lifted
under 4 % of hip height, free from 15 %) by the ground under it (
sgGroundUnder); the hips
come down by the larger downward move, the other leg bends at the knee in the plane it
already bends in (never past 99.9 % of full stretch, so a knee cannot bend backwards), and
the foot is tilted to the slope (at most 35 degrees). A step more than half the hip height
up or down is left to the clip. It blends off over about an eighth of a second when the
player jumps or swims, an NPC stands in deep water or has both soles a quarter metre or
more above the ground under them (a jump, a knock-back, a fall), or the choice is off. The
hip height every threshold scales with is the tallest stance seen so far, so a character
first seen sitting is not measured short. A blocker is stood on only over its own box,
turned with it (
sgBlockerShape), not the square around a turned one. sgLookIK shares
the turn between chest (a fifth), neck (three tenths) and head (half), within 70 degrees of
turn and 35 of nod, at 4 radians a second; a target more than 120 degrees behind is not
followed. Targets, highest first: a story Look at, a conversation (the character talked to
looks at the player and the player at it, and both at a speaker whose name is a model's),
then, for the player in Third person, where the camera looks. The turn is measured from the
body's facing (the root's, offset once by where the feet first pointed), not the feet each
frame, so a walking head does not swing with the feet. Every bone written is put back
the next frame unless the mixer wrote it, so a bone no clip animates never creeps; the
skeleton and the inverse binds are never touched.
sgAnimEventsTick fires an event when its action's time crosses it: forward, backward
(negative speed) or round a loop (the action's loop count), once each. A time moved back
without a loop (a restart mid-clip) fires nothing it skipped. In a crossfade only the clip
that shows fires (weight over a half, or the heaviest running). Footsteps found from the
feet (a walk, run, jog or sprint clip with no footstep events) need Footstep sounds only:
they also play with Feet on ground off and past the near tier.
game.characterMotion = { feet, look, footsteps, lookCamera } (all true by default,
Studio Characters > Motion);
model.motion = { feet, look, footsteps } (true/false,
absent follows the game; the model's Motion card).
model.ground (Motion card, Ground
sound) names what a blocker model sounds like underfoot; without it a model whose name or
file says wood, plank, crate, deck, dock, bridge, barrel, cabin, house, fence, table or box
is wood, any other stone. Terrain sounds by its biome (desert, beach, sand, dune, savanna:
sand; rock, stone, mountain, cave, tundra, snow, volcanic, canyon, mesa, cliff: stone; any
other: grass), and off every region
characterMotion.footstepDefault or grass.
Clip events.
game.animEvents = { [model url]: { [clip]: [{ t, kind, name }] } },
kind one of footL, footR, sound (name: an audioPlan.sfx id, played where the
character is on the Effects bus),
story (name: fires the story event anim.event with
{ name, model }). Each fires once per loop at its time, for clips the mixer is playing on
screen (the scene manager does not run the mixer of a character outside the view). A
walking clip (walk, run, jog, sprint) with no events of its own gets footsteps from the feet:
a foot coming down is a step.
sg:animEvent is dispatched for each, and
sgCharacterMotion.eventLog() keeps the last 200. A Story event is heard by the browser's
story only: a game server runs no clips, so in a server game nothing on the server hears it.
Body values are not limited to 0..1: 0.5 is untouched, and past 0 or 1 the same shape carries
on further (2 pushes three times as far as 1). Only the seeded generator stays inside 0..1. The
Forge compares
sgJointsOutside against the base at rest and shows "Joints are outside the body"
when the body values push a joint out; nothing is blocked.
Number fields (Studio and Forge),
/studio-ui/sg-num-field.js. Every number control is a
drag-or-type field:
<input type="text" data-num data-step>. Drag sideways to change it (Shift
finer, Ctrl coarser), click to type, arrows to step. There is no maximum.
data-min / data-max
are only for true limits (a 0..1 fraction, a volume below 0),
data-wrap for an angle or time
that wraps,
data-usual-min / data-usual-max (and data-warn-high / data-warn-low) only show
a short warning next to the field. Without
data-call the field fires its own input and
change events, so oninput readers keep working.
sgNumField.arm({ call }) // once per page; call(name, arg, value) for data-call fields
sgNumField.set(elOrId, value) // set a field as if typed, and deliver it
sgNumField.setWarning(el, text) // show or clear ('') the warning next to a field
sgTickSecondary is called by the scene manager every frame, after the mixer has posed the
character. It sways any bone named
Tail or Wing, phase-lagged down the chain, with an
amplitude taken from the character's own smoothed speed, and composes that onto the authored
rest pose rather than replacing it. Bones with no such name are never written. A model with no
tail and no wings costs one scan, once, and nothing thereafter.
This module never touches the skeleton or inverse binds — height is a uniform root scale,
shape is a vertex displacement. Several shipped bases have geometry and skeleton in
different unit scales, and the inverse binds are the only thing reconciling them.
4. Stats and rules — sg-stat-rules.js (window.sgStatRules)
A UMD library, byte-identical to
packages/game-server/stat-rules.js so a browser
game and a game's own server give the same numbers.
tools/stats-parity-test.js keeps
the copies equal.
sgStatRules.environment(skyState, extra) // the world a rule reads
sgStatRules.matches(source, env, player) // does a rule's source hold?
sgStatRules.amount(rule, env) // the value it contributes
sgStatRules.apply(attrs, base, rules, env, player, memo)
sgStatRules.applyOnly(attrs, base, rules, env, player, paths)
sgStatRules.celestial(base, attrs, rules, env, player, dt, opts)
sgStatRules.targetPath(target) / getPath / setPath / flatten
sgStatRules.activeCharacteristics(list, chosen) / traitSet / rulesFor / characteristicBase
sgStatRules.gearDeltas(equip)
sgStatRules.signForYear(p) / harmony(a, b) / moonBand(phase) / moonFactor(phase) / weatherIntensity(state)
sgStatRules.SIGNS · MOON_BANDS · WEATHER_INTENSITY
A rule is
{ source, target, type, value } with type one of add | percent | multiply |
set. Numbers are recomputed from a baseline every pass, so rules never accumulate.
5. Items, gear and resources — sg-items.js, sg-resources.js
> Load-order trap. Both modules define
sgAddItem, sgRemoveItem, sgSwapSlots,
>
sgGetItems and sgClearInventory. sg-items.js loads second and wins, with a
> different
sgAddItem signature. The bag you get is the items module's; the resources
> module's slot array is dead, which means
sgInventorySave() does not save your bag —
>
sgSaveItems() does. This ordering is baked into every published game and cannot be
> changed by editing
public/lib/.
Items (the live bag):
sgRegisterItem(id, def) / sgGetItemDef(id)
sgRegisterAugmentation(id, def) / sgGetAugDef(id)
sgDefineGearSlots(slots) / sgGetGearSlots()
sgAddItem(item = {}) // → slot index, or -1
sgRemoveItem(slotIndex, qty = 1)
sgSwapSlots(a, b) / sgGetItems() / sgClearInventory()
sgLootItem(itemId, opts = {}) // { rolled = true, qty, overrides } → instance | null
sgGetGear() / sgEquipItem(bagSlotIndex) / sgUnequipItem(gearSlotName)
sgGetEquippedBonuses()
sgAugmentItem(bagSlotIndex, augId) // → { success, message? }
await sgSaveItems() / await sgLoadItems(slots, gear)
Resources (counted, not slotted):
sgGetInventory() // → { resourceId: qty }
sgAddResource(id, qty) // → new qty
sgRemoveResource(id, qty)
sgGetResourceDef(id) // the DEFINITION, not the quantity
await sgInventorySave()
There is no
sgGetResource. For a quantity use sgGetInventory()[id].
6. Widgets, HUD and flow — sg-widget-manager.js
sgWidget(id) // → { set(props), show(), hide(), on('click', cb), el } | null
sgDialog({ speaker, text, portrait, choices }) // → Promise<choice>
sgNotify(msg, { duration = 2500, icon = '', type = 'info' })
sgRegisterWidgetType(name, def) // def.build(w) required
sgRegisterUIPack(name, types) / sgInstallUIPack(name)
sgFlowNext() // advance one flow slot, forward only
sgGoFlowScene(slotIdOrLabel) // → bool; jump to a named slot
Widgets are declared in
sgGameConfig.widgets and built by the engine. There is no
sgCreateWidget / sgUpdateWidget / sgShowWidget / sgHideWidget / sgDestroyWidget /
sgOnWidgetEvent — use the handle sgWidget(id) returns. on() supports 'click' only.
There is no
sgNextScene / sgPrevScene / sgGetFlowScene.
7. Input — sg-input-manager.js
sgInputDown(action) // is this named action held?
sgActionPressed(action)
sgOnInput(action, cb) / sgOffInput(action, cb) / sgOnInputUp(action, cb)
sgKeyHeld(key) / sgKeyPressed(key)
sgMousePosition() / sgMouseDown(btn = 0)
sgGamepad(index = 0)
sgSetTouchControlsVisible(visible)
sgTouchPlacePrompt(el) // phone: place a prompt element just above the move stick
The name is
sgInputDown. There is no sgIsActionActive. Actions come from
sgGameConfig.inputBindings, so a rebinding in the Studio reaches your script for free.
8. Audio — sg-audio-manager.js
sgPlayBGM(src, opts) // { volume = 0.5, loop = true, fadeIn = 0 }
sgStopBGM(opts) // { fadeOut }
sgPlaySFX(id)
sgPlaySFX3D(id, pos, opts) // panned at pos; returns { stop(), setPosition(x,y,z) } or null
sgPlayFootstep(type, pos, opts) // a step on grass, stone, wood or sand at pos, Effects bus; -> 'footstep-<type>' | 'footstep' | 'made:<type>' | null
sgSetMasterVolume(v)
sgUpdateAudioListener(camPos, forward, up)
sgPlayModelSound(modelId, soundId) // the model's Sounds entry for soundId plays; false if it has none
sgStopModelSounds(modelId?) // every model's sounds when no id
sgAudioBuses() // ['music', 'effects', 'voices', 'ambient']
sgSetBusVolume(bus, v) / sgGetBusVolume(bus) // the developer's volume (audioPlan.buses)
sgSetPlayerBusVolume(bus, v) / sgGetPlayerBusVolume(bus) // the player's own, kept in this browser
sgAudioState() // { state, listener, buses, sources, pending, echo: { room, wet }, voice: { sound, bus, ended } | null }
sgPlayVoice(id) / sgStopVoice() // a dialogue line's voice file (an Audio library id), always on the Voices bus; a new voice stops the one before
sgAudioContextFactory // optional, set before the manager loads: () => the context to use
sgPlayBGM() with no src is a silent no-op — it does not play the scene's
configured track. There is no
sgPlayMusic / sgStopMusic. No streaming.
The listener follows the camera. The scene manager calls
window._sgAudioTick(camera, dt)
every frame with the camera it draws with: the player's camera, a camera shot's while one plays,
the editor's in the Studio preview. It moves the listener and the panners of sounds on models
that move.
sgPlaySFX3D(id, pos, opts). opts: fadesFrom and heardWithin (metres) give a
straight-line fall: full volume up to Fades from, silent from Heard within on;
volume
(no maximum, not below 0);
bus; loop. Without fadesFrom/heardWithin the old fall-off is
kept exactly (HRTF, inverse, 1, 50, 1.2). On a touch device the straight-line panner uses
equal-power panning (lighter than HRTF).
Buses. Every sound goes through one of four buses, then the master: background music
through Music, an effect through its
audioPlan.sfx[id].bus (Effects when missing), a sound on
a model through its entry's bus (Ambient when missing). A bus's volume is
audioPlan.buses[bus] (Studio > Audio > Volumes, 1 when missing) times the player's own
(the Graphics settings widget's Volume rows, saved in the browser under
sgAudioBuses).
All at 1, a game sounds exactly as before.
Sounds on models.
scenes[n].models[m].sounds (Studio: a model's Sounds card), any
number:
{ sound, loop, volume, heardWithin, fadesFrom, when: 'always' | 'event', bus }.
Defaults: loop off, volume 1, heard within 20, fades from 2, when always, bus ambient.
always starts with the scene and stops when the scene changes; event waits for a story
beat's Play sound with At set to this model (which plays the model's entry for that sound
with its distances; a model with no entry for it keeps the old plain positioned play).
Echo areas. An area (
__area__) with area.echo = { on: true, room }, room small,
hall, cave or outdoors (Studio: the Area card's Echo and Room). Effects, voices and
ambient (not music) go through a room made from generated noise, no sound files. While the
listener is inside, that room's level rises (a quarter-second curve) and it falls when they
leave.
sgAudioState().echo.room names the room the listener is in.
Phones. The first tap, click, touch or key unlocks the sound, as before.
The Studio sends
{ type: 'sg:updateModelSounds', models: [{ id, sounds?, area? }] } to
the preview when a model's Sounds or an area's Echo change. Proven by
tools/audio-positional-test.js and tools/studio-audio-test.js.
9. Sky, time and astronomy — sg-sky-manager.js
Time and calendar
sgSetTime(t01) / sgGetTime() / sgGetTimeDisplay()
sgSetDayLength(minutes) / sgPauseCycle() / sgResumeCycle() / sgLerpTime(t, durationSec)
sgSetCalendarDay(day, year) / sgAdvanceDay(n = 1) / sgGetCalendar()
sgIsDay() / sgIsNight() / sgGetSeason() / sgGetSkyState()
Fog colour from the sky (stylized sky only). The saved scene setting
scenes[i].fog.colorFrom (on the live three.js fog, sgScene.fog._sgColorFrom): 'fixed' (or missing:
every fog saved before this) keeps
fog.color; 'sky' makes the stylized sky copy its horizon
colour into
sgScene.fog.color each frame. Only the colour changes: near, far and the horizon
land distance stay. Writer order: Fixed, the author's colour; weather fog on (
sgSetFog density
above 0,
window._sgWeatherFogOn), the weather's colour (#8fa3b8 unless given); otherwise, in
From the sky mode, the sky's. The realistic sky never writes it. Studio > Scene > Fog > Fog colour
(Fixed / From the sky; From the sky only with the stylized sky; a new fog starts on it). Proven by
tools/fog-sky-test.js and tools/studio-fog-sky-test.js.
Zodiac and the player's birthday
sgGetActiveZodiac() / sgGetZodiacForDay(day) / sgGetHoroscope(sign)
sgSetPlayerBirthDay(day) / sgGetPlayerBirthSign() / sgGetSignHarmony(a, b)
Celestial bodies
sgAddCelestialBody(cfg) / sgRemoveCelestialBody(id)
sgSetBodyColor(id, color, durationSec) / sgResetBodyColor(id, durationSec)
sgSetBodyIntensity(id, v, durationSec) / sgResetBodyIntensity(id, durationSec)
sgSetBodySize(id, scale, durationSec) / sgResetBodySize(id, durationSec)
sgSetBodyVisible(id, visible) / sgResetBody(id)
sgSetBodyPath(id, waypoints, opts) / sgSetBodyOrbit(id, opts) / sgGetBodyOrbit / sgResetBodyOrbit
sgGetBodyElevation(id) // → { elevation, elevationDeg, azimuth, visible }
sgGetMoonPhase(id?) // → OBJECT { phase, phaseFactor, phaseName, phaseEmoji, daysToFull, daysToNew }
Sky appearance, wind and cloud
sgSetSkyColors(overrides) / sgResetSkyColors()
sgSetAmbient(intensity, color, durationSec) / sgResetAmbient()
sgSkyPresets() / sgApplySkyPreset(nameOrCfg, opts)
sgSetWind(cfg) / sgWind() // → { headingDeg, speed, baseSpeed, gustiness, dirX, dirZ }
sgSetClouds(cfg) / sgGetClouds()
sgSetSkyScale(scale) / sgGetSkyScale()
sgSetSkyExtents(sx, sy, sz) / sgGetSkyExtents() / sgResetSkyExtents()
sgAutoFitSky(sizeXorRadius, sizeZ, padding) / sgFitToTerrainBounds(minX, maxX, minZ, maxZ, padding)
sgGetSkyCenter() / sgSetSkyCenter(cx, cy, cz)
sgSetSkyFollowsViewer(v) / sgFitShadows() / sgSkyDebug()
Stars and constellations
sgGetStarCatalog() / sgIdentifyStar(sx, sy) / sgHighlightStar(id, color)
sgAddConstellation(cfg) / sgRemoveConstellation(id)
sgSetConstellationVisible(id, visible, opts) / sgSetAllConstellationsVisible(v, opts)
sgShowConstellation(id, opts) / sgHideConstellation(id, opts)
sgSetConstellationLinesVisible(v)
sgSetConstellationMode(mode) / sgSetConstellationColor(id, color, opts) / sgResetConstellationColor(id)
sgHighlightConstellation(id, color) // see the note below
sgPulseConstellation(id, color, cycles = 0)
sgGetConstellationAltitude(name) / sgGetVisibleConstellations(thresholdDeg?)
sgDiscoverStar(id) / sgDiscoverConstellation(id) / sgUndiscoverConstellation(id)
sgGetAstronomyProgress() / sgSetStarMap(url)
sgSetStarVisibility(v01) / sgResetStarVisibility()
sgAddStarEvent(evt) / sgRemoveStarEvent(id)
sgStartPathEdit(id) / sgStopPathEdit() // STUDIO - drives the orbit path editor
>
sgHighlightConstellation is defined twice in the module. The later definition wins
> and takes
(id, color) only — a third duration argument is accepted and silently
> ignored, so the highlight never auto-restores.
Sky events
sgTriggerEclipse(opts) / sgTriggerLunarEclipse(opts)
sgTriggerBloodMoon(opts) / sgTriggerMeteorShower(opts)
sgCancelSkyEvent(eventId)
10. Terrain, foliage and farmables — sg-terrain-manager.js
sgTerrainHeightAt(x, z) / sgTerrainHeightAtOrNull(x, z)
sgWaterSurfaceAt(x, z)
sgWorldBoundary() / sgClampToBoundary(x, z)
sgRegionAt(worldX, worldZ) // → { id, weather } | null
sgHasRegionWeather()
sgTerrainBlocks / sgGetTerrain(label)
sgSetTerrainColor(label, hex) / sgSetTerrainTexture(label, url, repeat)
sgHitFarmable(farmable, instanceId, damage)
sgApplyFarmableHarvested(regionId, globalIdx) / sgApplyFarmableRespawn(regionId, globalIdx)
sgFloraCredits() // attribution for the flora actually used in this scene
sgExportZone() / sgWorldPackage
Custom biomes
The built-in presets are a closed list. A custom biome is the same shape as a
preset — ground, terrain shape and a prop list — defined in the Studio from the
textures and props the library already carries, and stored in the game config as
customBiomes, so it saves with the game and travels with it.
Resolution order is custom, then built-in, then grassland. A custom biome may
deliberately reuse a built-in id, which is how a game re-tunes
desert for itself
without a code change. Anything a custom biome does not state is inherited from the
preset named by its
basedOn, so a biome can be three lines.
sgSetCustomBiomes(list) // register; returns how many were taken
sgGetCustomBiomes() // what is registered, as plain data
sgResolveBiome(name) // the preset a name resolves to, after inheritance
// a biome built from what the library already has
{
id: 'meadow', label: 'Meadow', basedOn: 'grassland',
groundColor: '#6f9c4a', texture: '/sg-assets/tex/grass_01.jpg', textureRepeat: 10,
amplitude: 3,
props: [
{ type: 'flora:grass_clump', density: 0.10, scale: [0.7, 1.3] },
{ type: 'flora:dandelion', density: 0.06, scale: [0.7, 1.2] }
]
}
Registered automatically from the scene config before any region builds. Proven by
tools/custom-biome-test.js.
Foliage performance
sgSetFoliageDrawDistance(m) / sgGetFoliageDrawDistance()
sgSetFoliageShadowDistance(m) / sgGetFoliageShadowDistance()
sgSetFoliageChunking(on, cell) / sgGetFoliageChunking() / sgRefreshFoliageChunks()
sgSetFoliageAlphaMode('fast' | 'soft') / sgGetFoliageAlphaMode()
sgSetTreeDetailDistance(m) / sgGetTreeDetailDistance()
sgSetFarTreeShadows(on) / sgGetFarTreeShadows()
sgSetLeafLighting(mode) / sgGetLeafLighting() // 'realistic' (default) or 'flat'
sgForceTreeTier(0 | 1 | null)
Tree detail distance. Past
m metres (default 60; Studio > Biomes >
Foliage Performance > Tree detail distance, saved as
scene.perf.treeDetail)
a tree with a distance picture draws the picture: one quad, 2 triangles, that
turns to face the camera and shows the tree from the nearest of 16 sides, lit
by the scene's sun through a baked surface-direction sheet.
0 is Off: no
pictures are built and every tree draws as before.
sgSetTreeDetailDistance
returns
true when the change crosses Off, which needs the land rebuilt (the
Studio does this). Trees a game can harvest (
farmableProps, or a game-wide
propInteractable) always draw at full detail. sgForceTreeTier is for
tests:
0 forces every picture tree to its model, 1 to its picture, null
back to distance. Between 90 % and 110 % of the distance a tree cross-fades
(both drawn, each with half of one screen-door pattern). Distant tree shadows
(
scene.perf.farTreeShadows, default on; sgSetFarTreeShadows(false) turns
it off live): a picture casts a picture-shaped shadow while the scene's
Shadows option is on. Proven by
tools/tree-detail-test.js and
tools/flora-far-tier-test.js.
Leaf lighting (
scene.perf.leafLight, 'realistic' by default or 'flat';
Studio: Scene > Terrain > Leaf lighting).
sgSetLeafLighting('flat') switches
live (the foliage shaders compile once again),
sgGetLeafLighting() reads it.
Realistic: wrap lighting, sun light through the leaf, a leaf at least 0.8
rough, and a card's back (and, by how far it faces away from up, its front)
takes the sky light an upward face gets plus 0.15 of the sun, in the leaf's
own colour times 0.6, never less than its own, and its shine in the leaf's
colour at the same brightness. The procedural crowns (tree_pine, tree_broad,
bush) and the procedural grass and fern tufts get it too. Distance pictures get the wrap, the light through and the
roughness. Flat compiles the shader as it was before. Proven by
tools/leaf-light-test.js.
Ground (one shader patch on every biome ground, chained under the cascaded shadows hook)
sgSetGroundWetness(v) / sgGroundWetness() // 0 dry .. 1 soaked; the weather writes it
sgGroundPatchState() // per region: uniforms and what compiled, for tests
Wet ground: the weather is the one writer, from the rain amount only (rain
0.6, storm 1; snow, fog and dry weather 0), following Scene > Weather > Wet
ground (
sky.weather.wetGround: 'off', 'rain' default, 'always').
At 1 the colour goes to the power 1.3 and times 0.8, roughness down to 0.7,
and the shine is moved half way to the ground's own colour at the same
brightness (never the sky's blue); snow on the ground cancels it. The sky's
environment light includes the cloud layers and dims with the weather (the
same multiplier as the lights), and is retaken when the cloud cover moves by
a tenth. Large patches (biome keys
largePatches, patchStrength; Biome maker > Ground > Large patches, Patch
strength): the same map read again at a tenth of the repeat, turned 37
degrees and shifted, from a blurred level, as lighter or darker than the
map's average, scaled by how much the map varies at 32 x 32, plus a smooth
noise mask about one repeat across, turned -61 degrees (+-0.3 at strength 1),
so neighbouring tiles differ. On at 0.35 for a
textured ground unless the biome says otherwise; strength 0 draws exactly the
unpatched ground. Snow on (biome key
snowSlope; Biome maker > Ground > Snow
on): missing is All high ground (the old rule, every built-in preset); a
number (0.75 from the Studio) is Flat tops only, where per pixel a face
steeper than that keeps rock (it fades over 0.1 either side). A new biome in
the maker starts with Flat tops only. Every effect is a uniform, so a change
never recompiles. Proven by
tools/ground-patch-test.js and
tools/studio-ground-test.js.
Plants: gap and season colour
sgBiomePlaceStats(regionId) // { [propType]: { placed, asked } } from the region's last scatter
sgSetPlantSeasons(on) / sgGetPlantSeasons() // 'on' / 'off' (default off)
sgPlantSeasonState() // { on, types: { [propType]: { kind, colour:[r,g,b], amount } } }
sgSeasonKindDefault(propType) // 'change' | 'evergreen' | 'fixed' when a prop has no seasons key
Gap (prop key
minGap, metres; Biome maker > prop row > Gap): a candidate
closer than that to an already placed prop of the same kind in the region is
skipped, checked on a grid with cells the size of the gap. With a gap the
scatter may try up to 4 times as many spots; unset or 0 is the old scatter,
spot for spot. The maker starts a newly added tree at 2 and anything else at
0, and with clumping on holds the gap to half the group radius. The row's
Placed label is placed versus asked from the preview's last scatter
(
sgBiomePlaceStats, also sent as placeStats in sg:biomePropsGenerated).
A re-scatter moves every prop, so a world build (zone
props.json) made
before it is out of date: build again. Scattered props are never collision
blockers, before or after.
Season colour (
scene.perf.plantSeasons, 'off' by default; Studio: Scene >
Sky > Calendar > Plant colours follow season): swaying leaves and distance
pictures are mixed towards a season colour at their own brightness, from
sgGetCalendar().yearProgress, so a far tree keeps its colour at the switch.
Per prop (key
seasons; Biome maker > prop row > Seasons): 'change'
(dull winter, fresh spring, orange autumn),
'evergreen' (a slight winter
dulling only) or
'fixed' (never). A prop with no key: conifers (pine,
spruce, palm, procedural pine) evergreen; the maple (already autumn coloured)
and dead trees fixed; the rest change. Uniforms only, never a recompile.
Proven by
tools/plant-gap-season-test.js and tools/studio-plant-test.js.
Land to the horizon —
sg-horizon-land.js
sgHorizonLandState() // { built, reason, sides, rings, triangles, meshes, trees, ms, distance, fold, rect }
sgRebuildHorizonLand() // → Promise of the state, after the current terrain build
sgSetHorizonLand({ enabled, distance }) // writes scene.boundary.horizonLand, rebuilds only the ring
sgHorizonLandMeshes() // the ring meshes (tests)
sgTerrainRegionsForHorizon() // terrain manager: read-only copies of the built regions for the ring
Past the world rectangle the ground carries on as scenery, in three rings out to
the distance. Ring 1 folds the edge regions' stored final heights across the
edge, so the seam is exact (same points, heights and normals); over the next
100 m it blends into fresh noise (same preset, seed + 7777, amplitude x 0.6, no
edge fade), so there is no low band and no mirrored copy of a peak further out.
It fades out at the far edge to whatever is behind it. Scenery only: rays never
hit it, height queries never answer for it, it is never saved and never in the
world package. Needs the world edge (
scene.boundary.enabled); not built on a
side a horizon ocean covers, nor on a side a polygon region's outline covers
less than 90 % of. Setting
scene.boundary.horizonLand = { enabled, distance }
(Studio > Terrain > World Edge: Land to horizon, On by default; Distance Auto,
500, 1000 or 2000 m). Auto is the fog distance, else 90 % of the camera's far
plane; every distance is capped at 90 % of the far plane. Distant trees in ring 1
use the tree distance pictures. Proven by
tools/horizon-land-test.js.
11. Water, weather and space
Water —
sg-water-manager.js
sgIsUnderwater() / sgUpdateUnderwater() / sgWaterShownSurfaceAt(x, z)
sgSetWater(cfg) / sgRebuildWater() / sgGetWaterBodies()
sgSetWaterQuality('low' | 'medium' | 'high' | null) / sgGetWaterQuality()
Weather —
sg-weather-manager.js
sgSetWeather(state, opts = {}) // { transitionSec }
sgGetWeather()
sgSetWetGround('off' | 'rain' | 'always') / sgGetWetGround() // default 'rain'
States:
clear · partly · overcast · rain · storm · snow · fog.
Planets and suns —
sg-planet.js
sgSetSpace(cfg) / sgRebuildSpace() / sgGetSpaceBodies()
sgBuildPlanetMesh(cfg) / sgBakedPlanetMesh(cfg) / sgBakePlanetTexture(style, seed, res)
sgReleasePlanetMesh(group)
12. Physics, collision, particles, dialogue, spawning, achievements
// Physics — sg-physics-manager.js (Cannon-ES; box and sphere colliders only)
sgPhysicsEnabled // boolean
sgPhysicsBody(id) / sgAddPhysicsBody(model) / sgRemovePhysicsBody(id)
sgApplyImpulse(id, v) / sgApplyForce(id, v) / sgSetVelocity(id, v)
// Collision — sg-collision-manager.js (AABB, 16 layers + masks)
sgSetCollisionLayer(id, layer) / sgSetCollisionMask(id, mask) / sgSetCollisionGroup(id, layer, mask)
sgGetCollisionLayer(id) / sgCollides(a, b)
sgOnCollide(a, b, cb) / sgOnCollideAny(modelId, cb) / sgOffCollide(a, b, cb)
SG_LAYER_DEFAULT · SG_LAYER_PLAYER · SG_LAYER_NPC · SG_LAYER_PROJECTILE · SG_LAYER_TERRAIN · SG_LAYER_TRIGGER
// Particles — sg-particle-manager.js
sgSpawnParticles(cfg) / sgSpawnPreset(name, position, opts) // → emitterId
sgStopParticles(id) / sgClearParticles(id?)
await sgGetSpriteLibrary()
// Dialogue — sg-dialogue-manager.js
sgStartDialogue(treeId, object?, { at? }) / sgStopDialogue() / sgDialogueActive() / sgOnDialogueEnd(cb) // starts only the dialogue of the character object within Talk range, or from a story action (else → false)
sgDialogueFromServer(line) // engine use: shows a dialogue line the game server chose (server games only)
sgLanguage.current() / sgLanguage.list() // the language in play (a code) / [{ code, name }] the game's languages, the source first
sgLanguage.text(lineId, source) // a line's text in the language in play; no translation: source
sgLanguage.set(code) // the player's own choice (the settings widget's Language row), kept in this browser; false for a code the game lacks
// The language in play: the player's choice, else Studio > Game > Languages > Start in, else the browser's language (fr-CA finds fr), else the source.
// A dialogue line shows in it by its lineId (the server sends ids and the source text, never a translation), and plays its voice in it.
// Events: sg:languageChanged { code }. Studio hot update: postMessage({ type: 'sg:updateLanguages', languages })
// Spawning — sg-spawn-manager.js
sgSpawnGroup(groupId) / sgStopGroup(groupId)
// Achievements and profile — sg-achievements.js
sgProfile.get() / sgProfile.save() / sgProfile.reset()
sgSlotUnlocked(slotId) / sgAchievements()
// Story — sg-story.js (decides with sg-story-rules.js, the file the game's server runs)
sgStory.get(name, object?) / sgStory.set(name, value, object?) // a declared variable (player, world or object scope; object: a model id)
sgStory.check(condition, self?) // → bool, against the live game (self: "this object")
sgAreaDrawing() // → true while the Studio is drawing an Area in the preview (Studio use)
sgDeviceClass() // -> "phone", "slow computer" or "computer" (one owner; Auto corner shadows use it)
sgStory.run(actions, self?) // apply story actions (browser games)
sgStory.changes() // a count that grows when something a condition reads may have changed
sgStory.event(name, payload) // feed an event to the achievements
sgStory.modelState(id) // → { hidden, off } of a placed model now (Show / Hide model, Turn on / off; Starts hidden / Starts turned off)
sgStory.mirrorModel(id, { hidden, off }) // a model state the game's server sent (server games)
sgStory.playSound(sound, at?) // play a library sound (audioPlan.sfx id), at a model's place when at names one
sgModelInert(id) // → true while a placed model is hidden or turned off: it does not block and cannot be talked to or used
// Beat actions: showModel, hideModel, turnOn, turnOff (name: model id), wait (value: seconds; the rest of the list runs later), playSound (name: sound, object: model)
// Beat events: timer { every: seconds }, scene.started { scene }, area.enter / area.leave { area, who: '' any player | a character's model id }; a beat's cooldown: seconds
StoryRules.modelState(game, states, id) / StoryRules.inertModels(game, states) // the same rules on the server (states: { [id]: { hidden?, off? } })
sgStory.page(id) // → the page in use of a model with pages: { index, says, plays, visible, use } (index -1: no page passes), or null for a model with none
// model.pages = [{ if: condition (Show if), says: dialogue tree id, plays: an animation clip of the model, visible: false to hide it, use: [actions] (On use) }]
// the LAST page whose Show if passes is in use, chosen again whenever the story changes; with no pages a model is as before (model.talk.tree)
// a server game: the server chooses each player's page and refuses a talk or use that page does not allow; story:page { id, page, plays } in the browser
StoryRules.pagesOf(model) / StoryRules.activePage(model, ctx) / StoryRules.pageView(game, model, index) // the same rules on the server; ctx is "this object" = the model
StoryRules.interactTargets(game, scene, inert?, pageOf?) // pageOf(model) → its page index: a model with pages offers what that page allows
sgStory.flowExit(slot?) // → the slot id the slot's exits go to now (the current slot when omitted), or null (none passes; a server game: its server decides)
sgStory.flowServerDecides(slot?) // → true in a server game whose slot has exits: the browser loads no slot of its own when the level is won, it waits for the server's
// flowSlots[i].exits = [{ if: condition, to: slot id }]: checked in order when the slot's scene ends (a level won, a screen's Continue);
// the first whose If passes, and whose slot has a scene, is taken; none passing (or no exits): the next slot as before.
// story.play.exits: 'player' (default: each player by their own values) | 'world' (everyone in the level together, by the first player's values) - server games
StoryRules.exitsOf(slot) / StoryRules.exitTo(slot, ctx, usable?) / StoryRules.EXIT_DECIDERS // the same rules on the server (usable(id): may that slot be entered)
StoryRules.condWords(cond, game?) // → a condition in a few plain words ('trust at least 3'), for a short label
// Dialogue node.pos = { x, y } (optional): where the line's box sits in the Studio's dialogue graph; a line without one is laid out from the start line
sgStory.quest(id) / sgStory.journal() // a quest's stage (0 before it starts) / [{ quest, title, stage, journal }] the started quests
sgStory.goals(id) // → [{ kind, label, have, need, done }] the goals of the stage a quest is at, this player's counts (3 / 5)
sgStory.questLog() // → { active, finished, failed }: [{ quest, title, stage, journal, status, goals, stages: [{ n, journal }] }]
sgStory.mirrorQuest(id, stage, { counts, status, trail }) / sgStory.mirrorGoals(id, counts) // what the game's server sent (server games)
// Stage goals: story.quests[id] = { title, scope: 'player'|'world', stages: [{ n, journal, goals: [goal], next?, completes?, fails? }],
// onComplete: [actions], onFail: [actions], failIf: condition }. goal = { kind, count, label?, model?, sameKind?, group?, item?, area?, if? }
// kinds: talk (model), defeat (model, sameKind: any of its library model; or group: a spawn group), collect (item),
// reach (area), herd (area; model or group: who counts; measured twice a second), value (if: a condition).
// When every goal of the stage is done the quest moves on by itself: to next, else the next number, else it completes.
// A stage with completes / fails ends the quest there; Fail when goes to the first fail stage (or fails it where it is).
// Counts are per player, or kept in the world for scope 'world'; the server decides (outcomes quest { counts, status, trail }, questGoals { quest, counts }).
// Events: story:quest { quest, stage, old, status } · story:goals { quest, counts }
// Widgets: quest_log { label, quest: '' every quest, showFinished, accentColor, size.w } and goal_tracker { label, quest, accentColor, size.w }, placed like any widget
StoryRules.GOAL_KINDS / StoryRules.QUEST_SCOPES // the goal kinds and what each asks for; Each player, The whole world
StoryRules.questStep(game, eventName, payload, ctx, st) // the one driver, browser and server: counts goals, moves stages, completes, fails
StoryRules.stepGoals(game, quests, id, n, counts, eventName, payload, ctx, measure?) / StoryRules.goalsDone(quests, id, n, counts)
StoryRules.goalCounts(game, goal, eventName, payload) / StoryRules.herdCounts(game, goal, who) / StoryRules.goalNeed(goal)
StoryRules.afterStage(quests, id, n) / StoryRules.stageEnds(quests, id, n) / StoryRules.failStage(quests, id) / StoryRules.questScope(quests, id)
StoryRules.questOf(quests, id) / StoryRules.stageAt(quests, id, n) / StoryRules.goalsOf(quests, id, n) / StoryRules.measuredGoals(game)
StoryRules.goalLabel(game, goal) / StoryRules.goalView(game, id, n, counts) / StoryRules.modelKind(game, id) / StoryRules.groupOfId(id)
// The checker (sg-story-rules.js, the same in the browser, the server and Node; StoryRules = require("./story-rules.js") on the server)
// It knows stage goals: a goal naming a missing character, item, area or spawn group; Then go to a missing stage;
// a stage whose goals can never complete (an item nothing gives, a value nothing can make pass, more of a character than the game has)
StoryRules.lint(game) // → [{ where, message }] errors: unknown names, unreachable lines, a quest stage nothing moves on, an item nothing gives
StoryRules.warnings(game) // → [{ kind, where, message }] the game still runs: neverRead, neverSet, readBeforeSet, deadEnd, serverSplit, hiddenPage
StoryRules.lintSummary(game) // → { errors: n, warnings: n, problems: [{ level: 'error'|'warning', where, message }] }
StoryRules.lineProblems(game) // → [{ kind: 'noLineId'|'sameLineId'|'oldTranslation', where, message }] warnings; tools/story-check.js and the Studio's Warnings show them
// Line ids: node.lineId, version.lineId, choice.lineId, quest.lineId (title), stage.lineId (journal), goal.lineId (goal words);
// the Talk and Use words and the quest notices have fixed ids (StoryRules.WORD_IDS: play.talkWord, play.useWord, notice.questComplete, notice.questFailed).
// Given by the Studio when a line is made; an older game gets them once on open, from where each line is (the same ids for everyone); never changed by edits or moves.
// game.languages = { source: { code, name } (default en, English), list: [{ code, name }], start: '' (the browser's language) | code, strings: { [code]: { [lineId]: text } } }
// A dialogue line or version may have voices: { [code]: Audio library id }.
StoryRules.lineEntries(game) / StoryRules.giveLineIds(game, newId?) // every line [{ id, kind, place, where, speaker, text, note, holder }] / give missing ids (→ how many)
StoryRules.languagesOf(game) / StoryRules.languageCode(text) / StoryRules.languageFor(game, browserLanguages, saved?) / StoryRules.SOURCE_LANGUAGE
StoryRules.textFor(game, code, lineId, source) / StoryRules.translated(game, code, lineId) / StoryRules.voiceFor(game, holder, code)
StoryRules.exportRows(game, code) // → [{ id, where, speaker, source, translation, note }], one per line with text
StoryRules.toCSV(rows) / StoryRules.parseCSV(text) / StoryRules.toPO(rows, { code, name }) / StoryRules.parsePO(text) // the files a translator gets and returns
StoryRules.importRows(game, code, rows) // → { changed, unknown: [ids], missing: [ids], refused? }; sets translations, never source text; refused (noRows | noIdColumn | noTranslationColumn) changes nothing
// node tools/story-check.js <gameDir | game.config.json | index.html> | --published [root] exit 1 on errors
// A dialogue line may set end: true (Studio: End here): the talk ends there on purpose, so it is not a dead end.
// In a server game the server decides; the page only mirrors its 'story' outcomes.
// Events: story:varChanged { name, value, scope, object? } · story:actionRun { do, name, value }
// Entity stats — sg-entity-manager.js
sgRegisterEntity(label, stats) / sgUnregisterEntity(label) / sgEntityExists(label)
sgGetEntity(label) / sgGetEntitiesByGroup(key, val)
sgGetStat(label, key) / sgSetStat(label, key, val) / sgModStat(label, key, val, { min, max })
sgApplyEntityHit(modelId, hp, maxHp, died) / sgApplyEntityRespawn(modelId)
// Utilities
sgLog(...args) / sgTime() / sgRandom(min, max) / sgLerp(a, b, t)
// Crowds — sg-vat-manager.js (vertex-animation-texture instancing)
await sgVATBake(gltf, fps = 30)
sgVATCreateGroup(url, vatData) / sgVATGroupExists(url) / sgVATGetDuration(url)
sgVATAddInstance(url, position, ry, timeOffset) / sgVATRemoveInstance(url, instanceId)
sgVATTick(dt)
13. Server-authoritative games — sg-server-client.js
Engages only when
sgGameConfig.dataSource === 'server', in play mode, and a welcome
actually arrives. There is never a moment where both sides calculate.
sgServerIntent(kind, payload, cb) // → bool; ask the server to do something
sgServer // the socket.io socket
window.__sgServerActive // set on the first welcome, stays set
While active the engine stops computing stats locally and takes server set-points
instead. Send intents, never results.
13b. Rendering options, detail and crowds (engine v82 to v99)
Each rendering feature is a developer option with a Studio control; these are the functions
behind those controls, for a game's own scripts. Each
*State() returns a plain object for
reading, not for writing.
sgSetShadows({ mode: 'cascaded', cascades: 3 }); sgShadowState();
sgSetEnvLight({ mode: 'sky', strength: 1 }); sgRefreshEnvLight(); sgEnvLightState(); sgLightingState();
sgSetLook({ bloom: { enabled: false } }); sgLookState();
sgSetTextures({ mode: 'original' }); sgTextureUrl('/sg-assets/flora/fern_lod0.glb'); sgTextureState();
sgRegisterKtx2('/sg-dev-flora/<dev>/<tree>/<tree>_lod0.glb'); // a Forge tree in a custom biome has a side-car
sgCharacterLOD.attach(root, url); sgModelLOD.attach(object, simplifier);
sgVATProxy(url, instanceId); sgVATSetRate(url, instanceId, 1.5); sgVATInstanceCount(url);
sgCrowdCount(); sgGetMoves(); sgChooseMove('shoot');
| Function | Module | What it does | ||
|---|---|---|---|---|
sgSetShadows(cfg) / sgShadowState() | sg-sky-manager.js | Shadows: { mode: 'off' \ | 'single' \ | 'cascaded', cascades, distance, mapSize, soft }. Studio > Scene > Lighting |
sgSetEnvLight(cfg) / sgRefreshEnvLight() / sgEnvLightState() | sg-sky-manager.js | Light from the sky: { mode: 'sky' \ | 'off', strength, updateSec }; refresh redraws it now | |
sgLightingState() | sg-sky-manager.js | Whether the sky is up (skyActive) or Shadows and Environment light run without it (standalone, the day/night cycle off). Saved as scene.lighting { brightness, shadows, envLight }; Brightness multiplies the exposure in every scene | ||
sgSetLook(cfg) / sgLookState() | sg-scene-manager.js | Post-processing per scene: tone mapping, glow (its threshold divided by the exposure), dark edges, smoothing. Game mode and published games only | ||
sgSetTextures(cfg) / sgTextureUrl(url) / sgTextureState() | sg-scene-manager.js | { mode: 'compressed' \ | 'original' }; sgTextureUrl gives the file a model URL will load | |
sgRegisterKtx2(url) | sg-scene-manager.js | Marks a model outside the asset library as having a .ktx2.glb side-car (a Flora Forge tree in a custom biome). With Textures Compressed it loads the side-car, and the plain file if the side-car is missing | ||
sgCharacterLOD | sg-character.js | Character detail by distance: attach(root, url), tick(camera); reads sgGameConfig.characterLOD { enabled, mid, far } | ||
sgModelLOD | sg-character.js | Placed-model detail by screen size: attach(object, simplifier), tick(camera); reads sgGameConfig.modelLOD { enabled } | ||
sgVATProxy(url, instanceId) / sgVATSetRate(url, instanceId, rate) / sgVATInstanceCount(url) | sg-vat-manager.js | The crowd renderer: one drawn instance stands in for a body; set its animation rate; count instances | ||
sgCrowdCount() | sg-server-client.js | How many other players are drawn by the crowd renderer rather than as full characters | ||
sgGetMoves() / sgChooseMove(id) | sg-server-client.js | Server-run combat: the player's moves, and which one the next attack uses | ||
sgServerNpcs(list) | sg-scene-manager.js | Engine-internal: the server client hands over the NPCs the server moves | ||
sgSetEntityHp(modelId, hp, maxHp) | sg-entity-manager.js | Engine-internal: the server's word on a model's health |
window CustomEvents)
Lifecycle:
sg:sceneManagerReady · sg:sceneLoaded · sg:sceneWillChange ·
sg:modelReady · sg:playerModelReady · sg:terrainReady · sg:terrainPropsBuilt ·
sg:biomesBuilt · sg:planetFactoryReady
Player:
sg:playerStateChanged · sg:playerDeath · sg:playerRespawned ·
sg:playerRespawnRequest · sg:playerAttack · sg:playerInteractKey · sg:cycleTarget
Items and resources:
sg:itemAdded · sg:itemRemoved · sg:itemEquipped ·
sg:itemUnequipped · sg:gearChanged · sg:inventoryChanged · sg:playerGainResource ·
sg:lootSpawned · sg:lootPickedUp · sg:openLootWindow
Entities:
sg:entityClick · sg:entityHit · sg:entityRespawned ·
sg:entityTargeted · sg:entityPathEnd · sg:entityAnimEnd · sg:spawnModel ·
sg:npcCaptured
World:
sg:skyTick · sg:skyPhaseChanged · sg:calendarDayChanged ·
sg:zodiacChanged · sg:moonPhaseChanged · sg:windChanged · sg:weatherChanged ·
sg:lightning · sg:thunder · sg:underwaterChanged · sg:starEvent ·
sg:skyEventStarted · sg:skyEventEnded · sg:starDiscovered ·
sg:constellationDiscovered · sg:terrainBoundsChanged
Objectives and progress:
sg:objectiveProgress · sg:objectiveComplete ·
sg:objectiveFailed · sg:objectivesAllComplete · sg:achievementUnlocked ·
sg:profileChanged · sg:screenAdvance · sg:characterSelected
Collision and physics:
sg:collisionStart · sg:collisionEnd · sg:physicsCollision ·
sg:physicsBodyRemoved
Dialogue:
sg:dialogueStart · sg:dialogueNode · sg:dialogueEnd
Characters:
sg:animEvent (a clip event fired: { model, clip, kind, name, t, ground? }) ·
sg:footstep ({ type, sound, bus, x, y, z, model, side })
Input:
sg:inputDown · sg:inputUp
Server:
sg:serverConnected · sg:serverClosed · sg:serverSnap ·
sg:serverOutcome · sg:zoneWeather · sg:lobby
Known dead wiring
sg:zoneEnteredis listened for (it is the sole wiring for theonZoneEntermodel
script hook) and nothing dispatches it. That hook can never fire today.
sg:gameReadyhas a listener and no dispatcher.
sg:clearTerrainis dispatched as a CustomEvent but only handled as apostMessage
command, so the dispatch is never observed.
15. Studio commands (postMessage) — STUDIO
Sent from the Studio page into the game iframe; the handler reads
e.data.type. These
are not callable from a game script and are not CustomEvents.
sg:selectModel · sg:setMode · sg:setTransform · sg:addModel · sg:removeModel ·
sg:updateModel · sg:clearScene · sg:generateBiome · sg:startBiomeDraw ·
sg:startRoadDraw · sg:startPathEdit · sg:updateDialogueTrees · sg:updatePhysics ·
sg:updateParticles · sg:injectPlayerModel · sg:updatePlayer · sg:previewBiomeBounds
and about twenty more. An editor-only subset is suppressed in published games.
Studio-page helpers, also STUDIO only:
sgScenePost(msg), sgRemoveBiomeRegion(id),
sgClearAllBiomes(), sgSetBirthDay(day), sgStatRuleProblems(),
sgAddCharacteristicValue(key), sgSetCharacteristicVital(i, pool, max).
16. window.sgGameConfig — the keys the engine reads
Top level:
currentScene · scenes[] · player · characteristics[] · widgets[] ·
flowSlots[] · inputBindings · audioPlan.sfx · dialogueTrees · resources[] ·
items[] · gearSlots[] · achievements[] · zones[] · legs[] · world ·
dataSource · gameId · gameSlug · sceneId · characterEntries · characterMotion ·
animEvents
Per scene:
id · label · bg · grid · orbit · camera · screenType ·
models[] · terrain[] · heightmapTerrain · biomeRegions[] · boundary · water ·
space · sky · fog · ambientLight · dirLight · environment ·
materialOverrides · physics · perf · emitters[] · spawnGroups[] · objectives ·
audio.bgm · widgetRefs[]
Per model:
id · url · transform · path · physics · interactable ·
npcBlocker · snapY · spawnPoint · goalZone · scripts[] · clickAction ·
hitModes · range · cloudPlatform · zoneTrigger · motion · ground
scene.physics is an object ({ enabled, gravity, groundY, snapToTerrain }), not a
boolean. A model's physics key is
physics, not physicsBody. dialogueTrees.<id>.nodes
is an object keyed by node id, not an array.
Read by the engine but not written by any Studio control:
scene.environment,
region.props, propInteractable, player.anims, clickAction, maxConcurrent,
physics.collisionLayer, sky.starEvents, water tide/reflectTint/flowSpeed.
Two of these —
scene.environment and model.cloudPlatform — are additionally deleted
on every Studio save, because the save allowlist does not carry them.
17. Traps
1. Load order decides your inventory API.
sg-items.js overwrites five
sg-resources.js functions. Baked into every published game.
2.
sgPlayBGM() with no argument does nothing. Pass a source.
3.
sgGetMoonPhase() returns an object, not a number.
4.
sgSetRotation takes degrees, while THREE takes radians.
5.
sgHighlightConstellation ignores a duration argument.
6. CustomEvent and postMessage are different buses. A
sg:removeModel CustomEvent
does nothing; it is a Studio command.
7.
onZoneEnter never fires. Nothing dispatches sg:zoneEntered.
8. Setting
window.sgForgeDebug = true crashes the Character Forge — its _log
helper calls itself. Do not enable it until that line is fixed.