Releases / v0.102.0

v0.102.0 Leaves A Mark

2026-09-29 Engine

Sun shadows can come from a shadow map, which in a dense city cuts the lighting cost by more than half and lets every drawn mesh cast. Blood, scorch marks and bullet holes wrap round edges and climb walls without being nodes. Characters can reach for things, look at things and plant their feet. A server can load levels as layers that every player follows. Voice chat can sit in the room with reverb sends and muffling, and quiet microphones are levelled. Terrain flattens are dozens of times cheaper, and script lines can hide behind the scene.

Sun shadows from a shadow map

Turn on shadow map in the Lighting panel (shadow_map: true in a scene's lighting, or env.shadowMap = true), and the sun's shadows come from a shadow map instead of a march through the distance field. In a dense city at 1080p, lighting fell from 9.5 ms to 4.1 ms, plus 0.65 ms for the map, and the whole GPU frame from 12.9 ms to 8.0 ms. The cost stays flat as a level adds buildings and props.

It also changes what casts. The march only saw colliders, so a mesh that doesn't collide cast nothing, and neither did geometry built with the map tools. With the shadow map, everything that is drawn casts. The shadow softness widens the edge, and the shadow distance sets how far around the camera it reaches. It's off by default, so no game's look changes on update. perf.gpu() shows it as its own "sun shadow map" pass.

Levels in one session

A server's scene.load("levels/cave", { additive = true }) now reaches every player, including players who join later. The level's networked nodes replicate, and offset = vec3(...) places it anywhere, so a hub with portals into separate levels no longer has to be one giant scene. Each level brings the geometry built for it with the map tools. scene.unload only lets go of that level's sounds, effects, animators and colliders, so it no longer hitches the rest of the world.

Switching a node off with node.enabled = false now takes its colliders out of physics and rays too, and they come back when it's switched on. A spawned prefab's walls and floors collide the moment they appear.

Voice that sounds like it's in the room

A voice, or any sound, can now go partly into a reverb track and partly into another: sends = { ["Verb Large"] = 0.4 } on audio.play and voice.attach, or :setSend(track, level) on either handle. It's one stream heard dry and wet together, so moving between rooms glides. lowpass = 850, or :setLowpass, muffles a voice through a wall.

Each machine now levels its own microphone to a steady speaking level before sending it: quiet microphones are raised and loud ones lowered, and silence is never pumped up. voice.setAutoGain(false) turns it off, and voice.setInputGain(db) is there for a settings slider. A microphone plugged into one side of a stereo interface is no longer halved. A mistyped mode or falloff on a voice is now reported, as it is on audio.play.

Marks that wrap round corners

decals.add lays a picture on the world's surfaces: level meshes, terrain, and Collidable boxes and planes. Near an edge it folds over and carries on down the side. On a floor by a wall it climbs the wall and never pokes through. On a rock it follows the rock. It's lit, shadowed and fogged like the surface it lies on.

lua
decals.add{ texture = "textures/decals/blood.png",
            pos = vec3(hit.x, hit.y, hit.z), normal = vec3(hit.nx, hit.ny, hit.nz),
            size = 0.8, rotation = math.random() * math.pi * 2 }

A decal isn't a node. Every decal that shares a picture is one draw, so a fight can leave hundreds. The engine keeps the budget: 1024 until decals.setMax says otherwise, dropping the oldest. decals.set fades or tints one, decals.remove and decals.clear take them off, and perf.counts().decals says how many there are.

Characters reach, look and plant their feet

A script can now change a rigged model's pose after its animations have posed it, one frame at a time:

  • anim:setBoneRot and anim:addBoneRot turn one bone, and anim:setBonePos and anim:addBonePos move one. For example, lean the chest over a run, or drop the hips on a slope.
  • anim:reach("Hand", target, { pole = elbow }) bends an arm or leg so the hand or foot lands on a point: a grapple line, a wall, a step.
  • anim:lookAt({ "Spine2", "Neck", "Head" }, target, { limit = 80 }) turns a head, or a whole neck, toward something, within a limit.
  • anim:boneWorld("Hand") says where a bone ended up. It now also works under floptle run and on a dedicated server, so a hitbox can follow a hand where nothing is drawn.

The animation guide has a foot-planting recipe for uneven ground.

Controllers can do more too:

  • A blend state mixes clips by a number a script sets with anim:setParam, such as aim left, centre and right by aimX.
  • An additive layer adds its motion on top of the layers below.
  • A mask limits a layer to the bones you name.
  • mirror: true plays a clip as its left-right mirror image, so one left wallrun serves as the right one too.
  • anim:setSpeed(x, "Movement") speeds up one layer and leaves the others alone.

A map view leaves the terrain alone

terrain.lodAnchor(node) makes terrain detail, far planets and far craft follow that node instead of the camera. Pulling a map camera out into space no longer throws away the ground under the player, and closing the map no longer rebuilds it. Scattered props keep nothing loaded while the camera is far above them.

A lower render scale stays sharp

A render scale below 1 now sharpens as it scales up. app.setRenderSharpness sets how much, from 0 to 1 (the default is 0.4). draw.nativeLines(true) draws script lines over the finished picture at full resolution: one pixel wide and exactly the colour asked for, where before they came out soft and shimmering.

A host that blips keeps its lobby

When a listen host's connection dropped for a moment, its lobby was held for it, but only a host on a managed relay could claim it back. Every relay now gives the host a private token along with its lobby code, and a host that reconnects gets its lobby, code and players back with it. The relay needs this version too.

Cloud data by name

cloud.rank("laps"), cloud.docs("stages"), cloud.blobs("replays") and cloud.counter("installs") read and write the Cloud's data collections without building paths by hand. Every call reports errors the same way: { code, message, status }. floptle check warns about a collection a script uses that the project doesn't declare.

Cheaper spawning, rays and terrain edits

  • Spawning or destroying a node no longer costs time for the whole scene. A game that still makes the scripts re-read the whole scene every frame gets one Console warning naming the cause.
  • raycast checks only what the ray passes near, and hits level meshes exactly. On a level of a hundred models, a ray costs a fraction of what it did. raycastMany casts a batch in one call, and perf.counts() reports the rays cast and their time.
  • terrain.sculpt(..., "flatten") levels to a planet's up where it stands, or to a plane you name with { normal = ... }. Flattens and smooths now cost the ground they move rather than everything around it: fifteen building pads in one frame on a planet went from nearly 8 seconds to a fifth of a second. Edits beyond 64 a frame wait their turn instead of being dropped.
  • perf.hitches() lists every slow frame since you last asked, with what took the time, so a probe that checks every few seconds still sees each spike.

Shadows you can budget

The 32 shadow casters nearest the camera are the ones that cast, and node.castShadow = false takes one out. env.shadowSteps trades shadow-edge precision for speed on a weaker GPU. perf.gpu() shows how long each pass took on the graphics card, so you can see where frame time goes.

Also

  • draw.depthTest(true) hides the lines drawn after it behind whatever is in front of them, drawn native or not: a ground reticle behind the character, an orbit behind its planet.

  • draw.polyline draws a line through a list of points, and draw.conic draws an orbit's ellipse or hyperbola.

  • A script's variables survive its node being switched off and on.

  • An export skips anything listed in .floptleignore, lists the heaviest files nothing in the project names, and no longer warns wrongly about files outside the project.

  • floptle shot --frames looks through the scene's own camera unless --turn is given.

Floptle Hub

Linux x86_64 9.0 MB Download → sha256 13a0da814e174d9132955f621112911cc0243774ccd32794a90710abd2f1527f
macOS Apple Silicon 5.9 MB Download → sha256 609d940cc3ce625b72d1bf503ad6bd6856c7d00be67f5e1fe5cb19eb2e5a1a4f
macOS Intel 6.4 MB Download → sha256 d68b21bd47a6fd1af9dd8a51d27486250910e6c8269e79a1f84d908217dd25fe
Windows x86_64 7.3 MB Download → sha256 adbc57dff2e5522c8c9f4f711914d9898a0c98cc6d5765e07465a9d25888ea55

Engine

Linux x86_64 37.3 MB Download → sha256 555e08ee85d41a3e9ccd5a438c3be69483c6e54945fed01b610330b14962a2bb
macOS Apple Silicon 28.7 MB Download → sha256 8be6703dfc9d407db9337e1cf1c461fa3ed700b11d4f01decffbe65213813700
macOS Intel 31.0 MB Download → sha256 5257e56b5ae75d9ab056827a57cb317956b44f57de8c285a80112e65bbdc8e89
web 4.6 MB Download → sha256 38de4c150134bd257c4934c69bc21d3def9e1483bcb6cd3272ba6c2cca383de3
Windows x86_64 35.0 MB Download → sha256 6c49b7f42a8b3b2ccfe4ece25c79136fb236ba54c7334e12c05bbc33e3011be0

The binaries are not signed yet. On macOS, right-click the app and pick Open the first time. On Windows, if SmartScreen appears, pick More info then Run anyway.