room scroll-of-events: table-hex aperture binary scroll-snaps to the room provenance feed — TDD

From Role Select onwards, scrolling DOWN in the table-hex aperture swaps the entire hex view for the room's GameEvent feed (mirrors my_sky's wheel<->form; scroll-snap-stop:always = no partial scroll). Reuses the Billscroll query + core/_partials/_scroll.html so the feed renders identically.

room.html: #id_room_aperture wraps .room-hex-pane (existing .room-shell + the _table_positions strip moved INSIDE so the circles scroll away with the hex) + .room-scroll-pane (includes new _room_scroll.html); 'is-scrollable' added iff table_status set.

_room_scroll.html (new) = the DRY seam for my_sea; includes the shared scroll partial + a tiny dots-animation script (no scroll-position persistence). room_view adds events/viewer/scroll_position (same query as billboard.views.scroll).

_room.scss: .room-aperture + .room-pane (height:100%, not min-height); .is-scrollable engages scroll-snap-type:y mandatory + per-pane scroll-snap-align:start & scroll-snap-stop:always; .room-scroll-pane styles #id_drama_scroll + .scroll-buffer { margin-top:auto } (pure-CSS bottom-pin).

Trap: the aperture & panes set NO z-index/transform/opacity/filter -> NO stacking context, so the position strip's z-130 still resolves in the root context, above the gate/sig overlays (z-100/120). Verified by gatekeeper FTs (token drop + circle/modal layering).

Deferred INDEFINITELY (user): rising-game-cost + max room membership + max simultaneous CARTE slots + in-slot token combinations — until CARTE is anything but a secret type of Trinket.

Tests: RoomScrollOfEventsTest (5 ITs — aperture wraps hex+scroll panes, is-scrollable from Role Select, feed renders + scoped to room, no scroll pane in gate phase); functional_tests/test_game_room_scroll.py (2 FTs — computed scroll-snap props + scroll-down reveals the feed). 551 epic ITs/UTs green; 2 new FTs green; gatekeeper (token-drop + circle layering) + role-select (card fan) FTs green.

[[project-room-scroll-of-events]] [[project-position-circle-tooltips]]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-01 18:32:57 -04:00
parent 5229b9f96a
commit a4adf9664b
6 changed files with 303 additions and 3 deletions

View File

@@ -3288,3 +3288,69 @@ class ExpireLapsedRoomSeatsCommandTest(TestCase):
call_command("expire_lapsed_room_seats")
slot.refresh_from_db()
self.assertEqual(slot.status, GateSlot.FILLED)
class RoomScrollOfEventsTest(TestCase):
"""The table-hex aperture gains a 2nd scroll-snap section: scrolling down
past the hex reveals the room's provenance feed (the same GameEvent stream
the Billscroll page shows). Available from Role Select onwards — the gate
phase (no `table_status`) shows no scroll. Mirrors my_sky's wheel<->form
binary scroll-snap toggle; built to DRY-template onto my_sea next.
The shared row list comes from `core/_partials/_scroll.html` (the same
partial the Billscroll page uses), so the feed renders identically here."""
def setUp(self):
self.user = User.objects.create(email="founder@test.io", username="disco")
self.client.force_login(self.user)
self.room = Room.objects.create(
name="Whataburgher", owner=self.user,
gate_status=Room.OPEN, table_status=Room.ROLE_SELECT,
)
TableSeat.objects.create(
room=self.room, gamer=self.user, slot_number=1, role="PC",
)
# A welcome line (system-authored) + a deposit line — feed content.
GameEvent.objects.create(room=self.room, verb=GameEvent.ROOM_CREATED)
GameEvent.objects.create(
room=self.room, actor=self.user, verb=GameEvent.SLOT_FILLED,
data={"token_type": "carte", "token_display": "Carte Blanche",
"slot_number": 1, "renewal_days": 7},
)
self.url = reverse("epic:room", kwargs={"room_id": self.room.id})
def test_aperture_wraps_hex_and_scroll_panes(self):
content = self.client.get(self.url).content.decode()
self.assertIn("room-aperture", content)
self.assertIn("room-hex-pane", content)
self.assertIn("room-scroll-pane", content)
def test_aperture_is_scrollable_from_role_select(self):
# `is-scrollable` engages the binary scroll-snap (2 panes present).
content = self.client.get(self.url).content.decode()
self.assertIn("room-aperture is-scrollable", content)
def test_scroll_pane_renders_the_event_feed(self):
content = self.client.get(self.url).content.decode()
self.assertIn("id_drama_scroll", content)
self.assertIn("Welcome to Whataburgher!", content)
self.assertIn("deposits a Carte Blanche for slot 1", content)
def test_scroll_feed_scoped_to_this_room(self):
other = Room.objects.create(
name="Elsewhere", owner=self.user,
gate_status=Room.OPEN, table_status=Room.ROLE_SELECT,
)
GameEvent.objects.create(room=other, verb=GameEvent.ROOM_CREATED)
content = self.client.get(self.url).content.decode()
self.assertNotIn("Welcome to Elsewhere!", content)
def test_no_scroll_pane_in_gate_phase(self):
"""The gate phase (table_status unset) shows no scroll — the feed is
reachable only from Role Select onwards."""
gate_room = Room.objects.create(name="Gatehouse", owner=self.user)
content = self.client.get(
reverse("epic:gatekeeper", args=[gate_room.id])
).content.decode()
self.assertNotIn("room-scroll-pane", content)
self.assertNotIn("is-scrollable", content)