A game where you slice fruit (and not bombs!) with your ninja-knife-hands.
  • GDScript 89.1%
  • Nix 8.1%
  • Shell 2.8%
Find a file
Daniel Flanagan 7759238d28
All checks were successful
/ check (push) Successful in 1m30s
fix: a grab points from the hand, and is still judged on the wrists
Daniel: "when 'clap-grabbing' a setting slider, we seem to be resorting
to wrist position again?" It was — `grab.gd` took its midpoint from the
wrists, so grabbing a slider snapped the marker back onto the joint
after every other pointer in the game had just agreed the hand is
further along the forearm, and the drag was then measured from
somewhere the player was not looking.

These are two questions and only one was being asked. SEPARATION
decides whether this is a clap, and every threshold in grab.gd was
tuned against wrist distance on real footage — moving both endpoints
outward along the forearms would rescale all of them for no benefit.
POSITION is where the player is pointing, which the rest of the game
answers with the middle of the hand.

So the pair carries both: wrists for the gesture, centres for the
marker and the drag origin. Absent centres fall back to the wrists, so
nothing changes for a caller that does not have them.

This is the same split I made when the pointer moved into the hand and
then only did half of — the commit then said `_hand_pairs` was left
alone deliberately because the clap is judged by the size of the gap
rather than where the gap is. True, and it silently answered the OTHER
question with the wrist too.

# Testing

`_the_grab_marker_sits_in_the_hands_but_the_clap_is_judged_on_wrists`
puts the hand centres straight DOWN from the wrists — a large offset
that leaves wrist separation untouched — so the two questions can be
told apart. It asserts the same wrists engage either way, the marker
moves to the hands, the fallback still works, and that the two
positions differ at all, without which the test would prove nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtnYWFX9eMBH7oK3gmcMMs
2026-08-14 11:52:54 -05:00
.forgejo/workflows chore: stand up deck-slice as its own repository 2026-08-04 15:54:21 -05:00
doc refactor: one button, drawn one way 2026-08-12 11:54:44 -05:00
issues feat: say "I can see you but not your hands", and stop colours drifting 2026-08-07 11:36:26 -05:00
nix feat: live-editable tuning, reloaded while the game runs 2026-08-12 15:22:48 -05:00
project fix: a grab points from the hand, and is still judged on the wrists 2026-08-14 11:52:54 -05:00
steam feat(steam): prepare publishing — scripts, depot, checklist. Upload nothing. 2026-08-07 10:32:48 -05:00
.gitignore build: consume the branding addon from godot-branding, not a vendored copy 2026-08-05 20:28:36 -05:00
flake.lock fix: pin the deck-pose that publishes the detected region 2026-08-12 13:19:49 -05:00
flake.nix docs: there is no submodule route, and the docs said there was 2026-08-05 23:54:50 -05:00
LICENSE chore: license under GPL-3.0-or-later, and plan the release 2026-08-05 14:20:44 -05:00
README.md feat: the words match the game — punching, gloves, no slicing 2026-08-06 16:48:45 -05:00
THIRD-PARTY.md docs: jpeg-encoder's IJG term is an obligation, not an alternative 2026-08-05 20:53:38 -05:00

deck-slice — Fruit Punch Party

The repository and package are deck-slice; the game is called Fruit Punch Party. The product name lives in one constant (TITLE in project/title_screen.gd) and the repo name is a separate, coordinated change because it is a flake input URL elsewhere.

A game where your hands are boxing gloves. Fruit is flung up from the bottom of the screen; you punch it, and it bursts in the direction your fist was going. Up to six players at once, each with their own colour and score. Godot 4, GDScript, CC0 art.

2.5D: lit, tumbling 3D fruit that burst into splatter when hit, over a game whose logic is strictly 2D. An orthographic camera maps one world unit to one screen pixel, so the letterbox mapping that gameplay depends on is untouched. --flat reverts to the original drawn shapes and is kept working.

Anti-aliasing, glow and ambient occlusion are settings on a Graphics page, each with a measured frame-time cost and pixel-diff in doc/deck-slice.md. Shadows are deliberately absent rather than off-by-default: nothing in the scene could receive one.

It opens on a title screen that says whether the camera is working, and on a first run explains the gestures nobody can guess before letting you press Play.

It opens on a title screen that says whether the camera is working, and on a first run explains the gestures nobody can guess before letting you press Play.

It is the first real consumer of deck-pose, the markerless body-tracking daemon, and exists as much to prove that daemon's socket protocol is usable as to be a game.

Licence

GPL-3.0-or-later — see LICENSE.

Bundled art and audio are Kenney's, under CC0; the Godot engine is MIT; the model weights and several dependencies are Apache-2.0. Those keep their own terms, and a binary distribution must carry THIRD-PARTY.md alongside LICENSE.

GPLv3 rather than GPLv2 is a requirement rather than a preference: Apache-2.0 is compatible with the former and not the latter, and both the model weights and much of the Rust dependency tree are Apache-2.0.

Read this first

  • doc/deck-slice.md — how to run it, every tunable flag, the framing model, and what the tests do and deliberately do not cover.
  • deck-pose's doc/protocol.md — the wire format this game consumes. Read it before touching project/pose_client.gd or project/video_client.gd.

Running it

Two processes. The daemon owns the camera; the game reads its socket. Order does not matter — the game polls for the socket and reconnects on its own.

nix run .#deck-pose -- --headless   # terminal 1
nix run .#deck-slice                # terminal 2

Both default to $XDG_RUNTIME_DIR/deck-pose.sock, so the common case needs no arguments. On the steamdeck host the daemon is socket-activated by the NixOS module and deck-slice-launch starts the socket for you.

Settings, without a terminal

Every tunable is reachable with your hands: hold a hand in the top-left corner box for ~1.5s (or press Esc) to open the settings screen, then hold over a button until the ring around your hand fills. Bring both hands together to grab the highlighted row and slide it — steppers for nudging, clap-to-grab for sweeping.

Controller, keyboard and mouse are first-class too, not a camera-absent fallback: d-pad/arrows to move, right stick to slide, A/Enter to press, B/Esc to close, mouse wheel to adjust.

Precedence is default < environment < flags < saved settings — the in-game screen wins, because a value you changed with your hands should stick. --reset-settings is the way back if a saved value makes the game unplayable. See doc/deck-slice.md.

Outputs

output what
packages.<system>.deck-slice the game + deck-slice-launch + a .desktop entry (also .default)
packages.<system>.deck-pose re-exported from the input, so nix run .#deck-pose works
overlays.default adds pkgs.deck-slice
checks.<system> package (build + seven test suites), protocol-drift

The protocol-drift check

checks.protocol-drift is why deck-pose is a flake input rather than a link in a document. It reads the message types and event kinds the pinned daemon can emit, straight out of its source, and fails if the game names none of them.

Before the split, a daemon change that added or renamed an event showed up in the same diff as the game consuming it. It no longer does, and the failure mode is silent: the game's match stops matching and the feature quietly does nothing. Nothing errors, nothing logs.

Naming a kind is a low bar on purpose — it proves someone looked at it, not that the handling is right. project/main.gd lists the kinds this game ignores and why.

Building and testing

nix build
nix flake check

The framing, view-mapping, blade-geometry, dwell-timing, clap-gesture, settings-rules and settings-screen tests run inside the build (pure arithmetic — no camera, no socket, no clock). project/tests/render_test.gd reads back real pixels and is deliberately not in the build: Godot's headless renderer produces none, so it needs Xvfb and software Mesa. nix develop provides them; see the header of that file.