Back to Powder Tool V600Billion
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.

source/src/ui/catalog.js
/**
 * 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';
}