SOURCE / PINNED RELEASE
Made of little things.
Powder Tool V600Billion
- Release
- 142767edcab8…
- Author-recorded commit
- 6d92971effd0…
- License
- LICENSE
- Author’s source reference
- nostr://npub1fllw8kw0thjj55wds0uugcnp5kej2nfxd36eruq39d56wwz8r44q5q78wj/wss%3A%2F%2Fgit.napplet.soy%2F/powder-toy
Archive hash verified: ed7d6a8ea7083197…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
/**
* Scenes as data: a scene file's header, and the Lua that loads a scene. No
* Vite here (src/ui/scenes.js collects the files), so tests can use it.
*/
/** Stop the running scene's scripts: before a new scene, and when the player empties the simulation. */
export const STOP = String.raw`if napplet_scene_step then evt.unregister(evt.AFTERSIM, napplet_scene_step) napplet_scene_step = nil end
if napplet_scene_watch then evt.unregister(evt.TICK, napplet_scene_watch) napplet_scene_watch = nil end
napplet_scene_ended = nil`;
/**
* Before every scene: stop the previous scene's scripts, keep what was on
* screen for Undo, empty the simulation and put its settings back to the
* game's defaults.
*/
export const RESET = String.raw`${STOP}
sim.takeSnapshot()
sim.clearSim()
sim.gravityMode(0) sim.airMode(0) sim.edgeMode(0) sim.waterEqualization(0)
sim.newtonianGravity(false) sim.ambientHeatSim(false) sim.ambientAirTemp(295.15)
ren.decorations(true)
ren.useDisplayPreset(4)
sim.paused(false)`;
/** Helpers for the scene scripts (locals of the scene's chunk). */
export const KIT = String.raw`local E = elem
local WALL = { erase = 0, conductive = 1, detector = 3, fan = 5, liquid = 6, absorb = 7, wall = 8, air = 9, powder = 10, conductor = 11, gas = 13, gravity = 14, energy = 15, noair = 16, stasis = 18 }
-- An unknown element would be nil, and the game would draw with the selected tool instead.
local function el(t) if t == nil then error("unknown element", 3) end return t end
local function box(x1, y1, x2, y2, t) sim.createBox(x1, y1, x2, y2, el(t)) end
local function disc(x, y, r, t) sim.createParts(x, y, r, r, el(t), 0) end
local function line(x1, y1, x2, y2, t, r) sim.createLine(x1, y1, x2, y2, r or 0, r or 0, el(t), 0) end
local function walls(x1, y1, x2, y2, w) sim.createWallBox(x1, y1, x2, y2, w or WALL.wall) end
local function each(x1, y1, x2, y2, f)
for x = x1, x2 do for y = y1, y2 do local i = sim.partID(x, y) if i then f(i, x, y) end end end
end
local function set(x1, y1, x2, y2, field, value) each(x1, y1, x2, y2, function(i) sim.partProperty(i, field, value) end) end
local function celsius(c) return c + 273.15 end
-- The scene's scripts run while the scene is on screen, that is while the wall cells it
-- was built on (its anchors) are still there: emptying the simulation, opening a save or
-- undoing the scene ends them.
local anchors = {}
local function standing()
for _, a in ipairs(anchors) do if sim.wallMap(a[1], a[2]) ~= a[3] then return false end end
return true
end
-- f runs after every simulation step (not while paused); it may change particles, not settings.
local function every(f)
napplet_scene_step = function()
if napplet_scene_ended or not standing() then napplet_scene_ended = true return end
f()
end
evt.register(evt.AFTERSIM, napplet_scene_step)
end
-- f runs every frame, paused or not; it may change settings (air, gravity) too. This one
-- also takes the scene's scripts off once the scene is gone.
local function watch(f)
napplet_scene_watch = function()
if napplet_scene_ended or not standing() then
napplet_scene_ended = true
if napplet_scene_step then evt.unregister(evt.AFTERSIM, napplet_scene_step) napplet_scene_step = nil end
evt.unregister(evt.TICK, napplet_scene_watch)
napplet_scene_watch = nil
return
end
f()
end
evt.register(evt.TICK, napplet_scene_watch)
end`;
/**
* After the scene: the ground's wall cells become its anchors, and a scene
* with anchors gets a watch (if it has none) that notices when it is gone.
*/
export const ANCHOR = String.raw`for _, cx in ipairs({ 1, 76, 151 }) do
local w = sim.wallMap(cx, 94)
if w ~= 0 then anchors[#anchors + 1] = { cx, 94, w } end
end
if #anchors > 0 and not napplet_scene_watch then watch(function() end) end`;
/** A scene from its file: the `-- key: value` header, the Lua, and its picture if there is one. */
export function parseScene(path, source, thumbs = {}) {
const id = path.replace(/^.*\/(?:\d+-)?([\w-]+)\.lua$/, '$1');
const meta = {};
for (const line of source.split('\n')) {
const field = /^--\s*([a-z]+):\s*(.*)$/.exec(line);
if (!field) break;
meta[field[1]] = field[2].trim();
}
const thumbPath = Object.keys(thumbs).find((p) => p.endsWith(`/${id}.webp`));
return {
id,
title: meta.title ?? id,
icon: meta.icon ?? '',
blurb: meta.blurb ?? '',
hint: meta.hint ?? '',
view: meta.view === undefined ? 4 : Number(meta.view),
source,
thumb: thumbPath ? thumbs[thumbPath] : '',
};
}
/** The Lua that loads `scene`: reset, helpers, the scene, its anchors, then its display mode. */
export function sceneScript(scene) {
return `${RESET}\n${KIT}\n${scene.source}\n${ANCHOR}\nren.useDisplayPreset(${scene.view})`;
}
