Balatro-Feel¶
Card hover / select / drag / play, pure tween expressiveness.
▶ Run in browserUpstream: https://github.com/mixandjam/balatro-feel
Tags: port tier-1
Balatro-Feel: SimVX port¶
Recreates the mixandjam Balatro-feel Unity demo in pure SimVX. A hand of seven cards with hover scale, follow-rotation, drag-to-reorder, selection lift, and a Space-key “play hand” punch sequence.
Run¶
From the SimVX repo root:
# Interactive
uv run python ported_games/balatro_feel/simvx_port/main.py
# Headless capture (frames 30, 60, 119 → screenshots/frame_*.png)
uv run python ported_games/balatro_feel/simvx_port/main.py --test
# Scripted-input harness (six staged screenshots demonstrating hover/select/drag/play)
uv run python ported_games/balatro_feel/simvx_port/harness.py
# Web export
uv run simvx export web ported_games/balatro_feel/simvx_port/main.py \
-o ported_games/balatro_feel/simvx_port/web/index.html
Controls¶
Input |
Action |
|---|---|
Hover |
Lift, scale-up, parallax tilt toward cursor |
Click |
Toggle selection (selected cards rest higher) |
Drag |
Card follows cursor; crosses swap slots in the hand |
Right-click |
Deselect all |
Space |
Punch-pulse the selected cards in left-to-right order, then deselect |
Delete |
Remove the hovered card |
Layout¶
simvx_port/
├── main.py # entry + InputMap + root scene
├── harness.py # scripted input + screenshot capture
├── pyproject.toml # [tool.simvx] root = "BalatroFeelRoot"
├── nodes/
│ ├── card_textures.py # procedural card faces, suit pips, drop shadow
│ ├── card.py # Card (input root) + CardVisual (spring-follow visual)
│ └── hand_holder.py # hand layout, slot swap, input dispatch
├── screenshots/ # idle + harness captures
└── web/ # exported HTML bundle
See ../NOTES.md for design decisions, deviations from upstream, and engine friction.
License¶
MIT (matches upstream).
Source¶
1"""Balatro-Feel: Card hover / select / drag / play, pure tween expressiveness.
2
3# /// simvx
4# tags = ["port", "tier-1"]
5# upstream = "https://github.com/mixandjam/balatro-feel"
6# web = { width = 1280, height = 720, responsive = true }
7# ///
8
9Run:
10 uv run python ported_games/balatro_feel/simvx_port/main.py
11 uv run python ported_games/balatro_feel/simvx_port/main.py --test # headless capture
12"""
13
14from __future__ import annotations
15
16import sys
17from pathlib import Path
18
19# Allow running from any cwd
20_PORT_DIR = Path(__file__).parent
21if str(_PORT_DIR) not in sys.path:
22 sys.path.insert(0, str(_PORT_DIR))
23
24from nodes.card_textures import make_default_hand # noqa: E402
25from nodes.hand_holder import HorizontalHandHolder # noqa: E402
26
27from simvx.core import Node2D, Text2D # noqa: E402
28from simvx.core.input.enums import Key, MouseButton # noqa: E402
29from simvx.core.input.map import InputMap # noqa: E402
30from simvx.core.math.types import Vec2 # noqa: E402
31from simvx.graphics import App # noqa: E402
32
33WIDTH = 1280
34HEIGHT = 720
35
36
37class BalatroFeelRoot(Node2D):
38 """Root scene: hand of 7 cards centred low on the screen."""
39
40 def on_ready(self) -> None:
41 # Input map (must live in root.on_ready for web export)
42 InputMap.add_action("delete_card", [Key.DELETE])
43 InputMap.add_action("play_hand", [Key.SPACE])
44 InputMap.add_action("primary", [MouseButton.LEFT])
45 InputMap.add_action("secondary", [MouseButton.RIGHT])
46
47 cards = make_default_hand()
48 self.hand = HorizontalHandHolder(
49 cards=cards,
50 centre=Vec2(WIDTH / 2, HEIGHT * 0.55),
51 )
52 self.add_child(self.hand)
53
54 # Title + instructions
55 self.add_child(Text2D(
56 text="Balatro-Feel \u00b7 SimVX port",
57 x=WIDTH / 2 - 200, y=64,
58 font_scale=1.6,
59 ))
60 instructions = (
61 "Hover cards \u2022 Click to select \u2022 Drag to reorder "
62 "\u2022 Right-click clears \u2022 Space plays \u2022 Delete removes"
63 )
64 self.add_child(Text2D(
65 text=instructions,
66 x=WIDTH / 2 - 460, y=110,
67 font_scale=0.85,
68 colour=(0.75, 0.75, 0.78, 1.0),
69 ))
70
71
72def main() -> None:
73 headless = "--test" in sys.argv
74 if headless:
75 from simvx.graphics import save_png
76
77 capture_at = [30, 60, 119]
78 app = App(width=WIDTH, height=HEIGHT, title="Balatro-Feel (SimVX)", visible=False)
79 frames = app.run_headless(BalatroFeelRoot(), frames=120, capture_frames=capture_at)
80 out_dir = _PORT_DIR / "screenshots"
81 out_dir.mkdir(exist_ok=True)
82 for idx, img in zip(capture_at, frames, strict=False):
83 out_path = out_dir / f"frame_{idx}.png"
84 save_png(out_path, img)
85 print(f"saved {out_path}")
86 else:
87 app = App(width=WIDTH, height=HEIGHT, title="Balatro-Feel (SimVX)")
88 app.run(BalatroFeelRoot())
89
90
91if __name__ == "__main__":
92 main()