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.
/**
* The game's tools (elements, walls, tools, decoration, Game of Life) as the
* page's palette shows them. The list comes from the game itself (Lua's
* `tools.index`), so it is whatever this build of the game has.
*/
/** The game's menu sections (SimulationData.cpp LoadMenus), in its order; 14 is its own favourites list. */
export const SECTIONS = [
{ id: 8, key: 'powders', name: 'Powders' },
{ id: 7, key: 'liquids', name: 'Liquids' },
{ id: 6, key: 'gases', name: 'Gases' },
{ id: 9, key: 'solids', name: 'Solids' },
{ id: 5, key: 'explosives', name: 'Explosives' },
{ id: 10, key: 'radioactive', name: 'Radioactive' },
{ id: 1, key: 'electronics', name: 'Electronics' },
{ id: 2, key: 'powered', name: 'Powered' },
{ id: 3, key: 'sensors', name: 'Sensors' },
{ id: 4, key: 'force', name: 'Force' },
{ id: 11, key: 'special', name: 'Special' },
{ id: 12, key: 'life', name: 'Life' },
{ id: 0, key: 'walls', name: 'Walls' },
{ id: 13, key: 'tools', name: 'Tools' },
{ id: 15, key: 'deco', name: 'Paint' },
];
export const ERASER = 'DEFAULT_PT_NONE';
const LIST_TOOLS = String.raw`local out = {}
for id, i in pairs(tools.index) do
if tools.property(i, "MenuVisible") then
local text = tools.property(i, "Description"):gsub("%s+", " ")
out[#out + 1] = table.concat({ i, id, tools.property(i, "MenuSection"), tools.property(i, "MenuSort"),
tools.property(i, "Colour"), tools.property(i, "Name"), text }, "\t")
end
end
return table.concat(out, "\n")`;
/** Every tool the game shows in its menus, in the game's order within each section. */
export function readTools(game) {
return game.lua(LIST_TOOLS).split('\n').filter(Boolean).map((line) => {
const [index, id, section, sort, colour, name, description] = line.split('\t');
return {
index: Number(index),
id,
section: Number(section),
sort: Number(sort) || 0,
colour: `#${(Number(colour) & 0xffffff).toString(16).padStart(6, '0')}`,
code: name,
label: label(id, name, description, Number(section)),
description,
};
}).sort((a, b) => a.section - b.section || a.sort - b.sort || a.index - b.index);
}
/** Names for tools whose description does not start with one. */
const NAMES = {
DEFAULT_PT_NONE: 'Eraser', DEFAULT_PT_ACID: 'Acid', DEFAULT_PT_GAS: 'Gas', DEFAULT_PT_OIL: 'Oil',
DEFAULT_PT_FIRE: 'Fire', DEFAULT_PT_GOO: 'Goo', DEFAULT_PT_DMG: 'Damage', DEFAULT_PT_ICEI: 'Ice',
DEFAULT_PT_ROCK: 'Rock', DEFAULT_PT_RIME: 'Rime', DEFAULT_PT_WAX: 'Wax', DEFAULT_PT_FUSE: 'Fuse',
DEFAULT_PT_HEAC: 'Heat conductor', DEFAULT_PT_SPRK: 'Spark', DEFAULT_PT_LIGH: 'Lightning',
DEFAULT_PT_THDR: 'Thunder', DEFAULT_PT_IRON: 'Iron', DEFAULT_PT_GOLD: 'Gold', DEFAULT_PT_VOID: 'Void',
DEFAULT_PT_WARP: 'Warp', DEFAULT_PT_SNOW: 'Snow', DEFAULT_PT_TRON: 'Tron', DEFAULT_PT_DUST: 'Dust',
DEFAULT_PT_GRAV: 'Gravity dust', DEFAULT_PT_DLAY: 'Delay', DEFAULT_PT_BASE: 'Base', DEFAULT_PT_C5: 'C-5',
DEFAULT_PT_ANAR: 'Anti-air', DEFAULT_PT_INVIS: 'Invisible', DEFAULT_PT_FIGH: 'Fighter',
DEFAULT_TOOL_HEAT: 'Heat', DEFAULT_TOOL_COOL: 'Cool', DEFAULT_TOOL_AIR: 'Air', DEFAULT_TOOL_VAC: 'Vacuum',
DEFAULT_TOOL_PGRV: 'Gravity +', DEFAULT_TOOL_NGRV: 'Gravity −', DEFAULT_TOOL_MIX: 'Mix',
DEFAULT_TOOL_CYCL: 'Cyclone', DEFAULT_TOOL_WIND: 'Wind', DEFAULT_TOOL_AMBP: 'Ambient +',
DEFAULT_TOOL_AMBM: 'Ambient −', DEFAULT_UI_SAMPLE: 'Sample', DEFAULT_UI_SIGN: 'Sign',
DEFAULT_UI_PROPERTY: 'Property', DEFAULT_UI_ADDLIFE: 'Custom life', DEFAULT_DECOR_SET: 'Paint',
DEFAULT_DECOR_CLR: 'Clear paint', DEFAULT_DECOR_ADD: 'Lighten', DEFAULT_DECOR_SUB: 'Darken',
DEFAULT_DECOR_MUL: 'Multiply', DEFAULT_DECOR_DIV: 'Divide', DEFAULT_DECOR_SMDG: 'Smudge',
DEFAULT_WL_ERASE: 'Erase walls', DEFAULT_WL_ERASEA: 'Erase all', DEFAULT_WL_WALL: 'Wall',
DEFAULT_WL_AIR: 'Air-only wall', DEFAULT_WL_NOAIR: 'Air-block wall',
};
const LIFE = 12;
const NOT_A_NAME = /^(dissolves|ignites|diffuses|flammable|erases|burns|generates|conducts|deforms|crushes|rusts|solid|displaces|accelerates|blocks|allows|absorbs|heats|cools|creates|removes|changes|makes|repels|smart|tries|light|corrosive|cold|instantly|basic|the)\b/i;
/** A readable name: the list above, else the description's opening words ("Water. Conducts …"). */
function label(id, name, description, section) {
if (NAMES[id]) return NAMES[id];
// The Game of Life rules all start "Game Of Life: …"; their codes are their names.
if (section === LIFE) return name;
if (/\s/.test(name) || name.length > 5) return name.charAt(0) + name.slice(1).toLowerCase();
const first = /^([^.,!:(]+)/.exec(description)?.[1]?.trim() ?? '';
if (first && first.length <= 22 && first.split(' ').length <= 3 && /^[A-Z]/.test(first) && !NOT_A_NAME.test(first)) return first;
return name.charAt(0) + name.slice(1).toLowerCase();
}
/** Black or white, whichever reads better on the tool's colour. */
export function inkFor(colour) {
const value = Number.parseInt(colour.slice(1), 16);
const [r, g, b] = [value >> 16, (value >> 8) & 255, value & 255].map((c) => {
const s = c / 255;
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.22 ? '#111' : '#fff';
}
