Drag back. Let go. Stick the landing.

Pull away from the gecko to load the slingshot, aim the arc, and release. Every ledge you stick adds to your score. The creeping rot never stops, so don't dawdle.

best 0player none
Turn your device sideways for the full screen.
Mouse or finger: drag anywhere on the screen, away from the gecko, then release. F fullscreens, Esc opens the menu, R restarts, F3 shows the node monitor.
untitled segment
1024 Γ— 576 β€” fixed tablet frame

add a player

stored on this device only

leaderboard

#playeremailphonebestledgesrunslast run
No players yet. Add one above and your runs start counting.

Stickyfoot is built from three separable parts: an editor that writes segment files, a runtime that links those segments into an endless chain, and a local player store. Here is how each one works, and what the files look like.

The node chain

The world is a singly linked list of LevelNode objects. Every node is exactly 1024 Γ— 576 world pixels β€” the same frame the editor draws in, sized for a small tablet β€” so nodes butt together with no seams and no scaling maths.

head β†’ [node 12] β†’ [node 13] β†’ [node 14] β†’ tail
        x=12288   x=13312   x=14336

Each frame the chain does two things:

  • Extend. While tail.x + 1024 < camX + viewWidth + 1024, a new node is appended: the next segment is chosen from the pool, its art is baked once into an offscreen canvas, and its collision boxes are translated into world space.
  • Cull. While head.x + 1024 < camX, the head node has fully left the screen. It is unlinked, its baked canvas is sized to 0 Γ— 0 to release the bitmap, its box array is emptied, and its next/prev pointers are nulled so the garbage collector can take the whole object.

Only three or four nodes exist at any moment, no matter how long the run lasts. Open the node monitor on the play screen (F3) to watch spawned, live, and destroyed counts plus the live bitmap budget.

Two files per segment

The editor exports a pair of files that share an id and a frame size. Collision never mentions art, art never mentions collision; the runtime joins them by id.

seg_id.collision.json

{
  "format": "stickyfoot.collision",
  "version": 1,
  "id": "seg_meadow_01",
  "width": 1024,
  "height": 576,
  "entry": { "x": 96, "y": 380 },
  "boxes": [
    { "x": 0,   "y": 448, "w": 256, "h": 128, "type": "platform" },
    { "x": 448, "y": 320, "w": 192, "h": 64,  "type": "platform" },
    { "x": 704, "y": 256, "w": 64,  "h": 192, "type": "cling"    },
    { "x": 448, "y": 288, "w": 192, "h": 32,  "type": "hazard"   }
  ]
}

seg_id.art.json

{
  "format": "stickyfoot.art",
  "version": 1,
  "id": "seg_meadow_01",
  "width": 1024,
  "height": 576,
  "layers": [
    { "name": "background", "z": 0, "pieces": [ { "sprite": "fern", "x": 320, "y": 384, "w": 64, "h": 64 } ] },
    { "name": "midground",  "z": 1, "pieces": [ { "sprite": "dirt", "x": 0, "y": 448, "w": 256, "h": 128 } ] },
    { "name": "foreground", "z": 2, "pieces": [] }
  ]
}

entry is where the gecko stands when a segment is the first node of a run, and the camera centres on it, so put it over a ledge. The editor draws a ghost gecko there. If a run starts on a segment whose entry is not above anything solid, the runtime falls back to the leftmost ledge.

Both files are validated on load: mismatched width/height, a missing id, or an unknown box type is rejected rather than silently drawn wrong. A segment with art but no collision is playable but empty; a segment with collision but no art renders as untextured hitboxes, which is useful while blocking out a layout.

Box types

  • platform β€” solid. Landed on from above, blocks from the sides and below.
  • cling β€” a gecko surface. Stick to any face of it, including walls and ceilings, then launch again.
  • hazard β€” touching it ends the run.
  • bounce β€” springy. Reverses vertical speed at 80% and keeps you airborne.

Bringing in your own art

The palette takes imported PNG, GIF, WebP or JPEG files. One thing decides whether a piece looks right: its size in pixels. Every art pixel is drawn as a 4 Γ— 4 block, and a piece repeats across whatever rectangle you drag out, so:

  • 16 Γ— 16 px is one block β€” 64 world pixels, the same as the grid. This is the size for ground, walls and anything you stretch.
  • Multiples of 16 keep the repeat on the grid: 32 Γ— 32 for a chunkier block, 64 Γ— 16 for a wide band, 32 Γ— 48 for a tall prop.
  • Between 4 and 128 px a side. Past that a piece stops reading as pixel art on a tablet, and the import is refused.
  • Transparent background, no anti-aliasing, no soft shadows. Hard pixels only β€” the game scales with nearest-neighbour, so fuzzy edges turn into fuzzy blocks.
  • Flat pixel art at these sizes lands well under 5 KB. The limit is about 30 KB per piece; if you are anywhere near it, the file is probably a photo.

The starter file button hands you a correctly sized PNG to paint over: a 16 Γ— 16 shaded block that repeats cleanly, a 32 Γ— 32 version of the same, or a transparent 32 Γ— 32 guide with corner ticks and a centre cross for lining up a prop. Paint over it, keep the canvas size, save, and import.

Imported pieces live in this browser under stickyfoot.art.v1 and appear in the palette with a dashed border. Exported art files carry a copy of every custom piece they use, in a sprites block keyed by sprite name:

{
  "format": "stickyfoot.art",
  "id": "seg_meadow_01",
  "sprites": {
    "art_mossy_brick": { "label": "mossy", "w": 16, "h": 16, "src": "data:image/png;base64,…" }
  },
  "layers": [
    { "name": "midground", "z": 1,
      "pieces": [ { "sprite": "art_mossy_brick", "x": 0, "y": 448, "w": 256, "h": 128 } ] }
  ]
}

So a segment stays portable: importing it on another machine also installs the art it needs. Saved maps skip the copy, since the library already has the pixels. If a piece ever goes missing, the game draws a magenta box where it should be rather than quietly leaving a hole.

Maps and slides

The editor edits a map: an ordered list of slides, where one slide is one node. The strip under the canvas is that list, left to right, in the order the runtime will link them. Click a slide to edit it; the + between two slides inserts there, either empty or as a copy of the slide you are on.

Saving a map keeps it in this browser under stickyfoot.maps.v1 and makes it the active map, so the world switch on the play screen can run it. β€œPlay from this slide” pins the slides from the current one onward to the head of the chain and lets the endless generator take over after the last one. Loading the example gives you a copy of the opening six nodes of the built-in world, editable without touching the original.

A whole map exports as one file, which is just the pairs collected in order:

{
  "format": "stickyfoot.map",
  "version": 1,
  "id": "map_fern_hollow",
  "name": "Fern Hollow",
  "width": 1024, "height": 576,
  "nodes": [
    { "collision": { … }, "art": { … } },
    { "collision": { … }, "art": { … } }
  ]
}

Import takes either a .map.json (replaces the whole map) or one .collision.json + .art.json pair (replaces the current slide), so the per-segment files stay the unit you can hand to someone else.

Slingshot physics

At rest, a drag vector d is measured from where the pointer went down to where it is now. Launch velocity is the reverse of that drag, clamped and scaled:

power = min(|d|, 190) / 190          // 0 … 1
v     = normalize(-d) Γ— power Γ— 23   // world px per tick
each tick:  vy += 0.62 ;  x += vx ;  y += vy ;  vx *= 0.9994

The simulation runs on a fixed 60 Hz accumulator so the arc is identical on any refresh rate. The dotted preview is the same integrator run forward, and it gets shorter as your score climbs β€” early ledges show you the whole arc, later ones make you read it.

Cabinet mode

Run game in the header launches the cabinet: fullscreen, no page chrome, no navigation. It is meant to be left running at an event, so it does not let go on its own β€” the browser back button is caught and pushed back, closing the tab asks for confirmation, and the only way out is the ⏻ button, which asks before it shuts down. Fullscreen is the one thing a browser will always take back: if someone presses Escape the cabinet keeps running filled to the window and the β›Ά button restores it.

The attract screen shows the top eight names and scores and offers one action: play. A cabinet run is anonymous while it is being played β€” there is no player to pick. When the run ends, the score is held aside and the player is asked for a name, an email and a phone number. All three are trimmed of surrounding and repeated whitespace, then checked: at least two characters of name, an address of the form name@host.tld, and 7 to 15 digits of phone. If any of them is missing or malformed the score is discarded β€” nothing is written. The same happens if the player presses discard, or walks away and the ninety-second timer runs out.

A score that passes is matched against the existing players by email or by phone digits, ignoring case and formatting, so a regular keeps one row on the board rather than collecting duplicates. Their best score is the highest they have ever posted, not the most recent.

Where your data lives

Players, high scores and saved segments are kept in this browser's local storage under the stickyfoot.* keys. Nothing is sent anywhere, and clearing the browser's site data erases it. Exported segment files are plain JSON you can keep, edit by hand, or hand to someone else to import.

starter file

Paint over one of these in any pixel editor, then bring it back with β€œimport art”. The size is the whole trick: the game draws every art pixel as a 4 Γ— 4 block, so the file stays tiny.

add a slide

are you sure?

who is playing?

Runs are only scored when a player is active. You can add players on the players screen.

edit player