hex position indicators: chair icons at hex edge midpoints replace gate-slot circles

- Split .gate-overlay into .gate-backdrop (z-100, blur) + .gate-overlay modal (z-120) so .table-position elements (z-110) render above backdrop but below modal
- New _table_positions.html partial: 6 .table-position divs with .fa-chair, role label, and .fa-ban/.fa-circle-check status icons; included unconditionally in room.html
- New epic:room view at /gameboard/room/<uuid>/; gatekeeper redirects there when table_status set; pick_roles redirects there
- role-select.js: adds .active glow to position on selectRole(); swaps .fa-ban→.fa-circle-check in placeCard onComplete; handleTurnChanged clears stale .active from all positions
- FTs: PositionIndicatorsTest (5 tests) + RoleSelectTest 8a/8b (glow + check state)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-03-30 18:31:05 -04:00
parent 8b006be138
commit a8592aeaec
11 changed files with 370 additions and 35 deletions

View File

@@ -71,6 +71,17 @@ def _notify_sig_selected(room_id, card_id, role, deck_type='levity'):
)
SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
def _gate_positions(room):
"""Return list of dicts [{slot, role_label}] for _table_positions.html."""
return [
{"slot": slot, "role_label": SLOT_ROLE_LABELS.get(slot.slot_number, "")}
for slot in room.gate_slots.order_by("slot_number")
]
def _expire_reserved_slots(room):
cutoff = timezone.now() - RESERVE_TIMEOUT
room.gate_slots.filter(
@@ -135,6 +146,7 @@ def _gate_context(room, user):
"carte_slots_claimed": carte_slots_claimed,
"carte_nvm_slot_number": carte_nvm_slot_number,
"carte_next_slot_number": carte_next_slot_number,
"gate_positions": _gate_positions(room),
}
@@ -186,6 +198,7 @@ def _role_select_context(room, user):
.values_list("slot_number", flat=True)
) if user.is_authenticated else [],
"active_slot": active_slot,
"gate_positions": _gate_positions(room),
}
if room.table_status == Room.SIG_SELECT:
user_seat = room.table_seats.filter(gamer=user).first() if user.is_authenticated else None
@@ -215,9 +228,16 @@ def create_room(request):
def gatekeeper(request, room_id):
room = Room.objects.get(id=room_id)
if room.table_status:
ctx = _role_select_context(room, request.user)
else:
ctx = _gate_context(room, request.user)
return redirect("epic:room", room_id=room_id)
ctx = _gate_context(room, request.user)
ctx["room"] = room
ctx["page_class"] = "page-gameboard"
return render(request, "apps/gameboard/room.html", ctx)
def room_view(request, room_id):
room = Room.objects.get(id=room_id)
ctx = _role_select_context(room, request.user)
ctx["room"] = room
ctx["page_class"] = "page-gameboard"
return render(request, "apps/gameboard/room.html", ctx)
@@ -390,17 +410,20 @@ def select_role(request, room_id):
if request.method == "POST":
room = Room.objects.get(id=room_id)
if room.table_status != Room.ROLE_SELECT:
return redirect("epic:gatekeeper", room_id=room_id)
return redirect(
"epic:room" if room.table_status else "epic:gatekeeper",
room_id=room_id,
)
role = request.POST.get("role")
valid_roles = [code for code, _ in TableSeat.ROLE_CHOICES]
if not role or role not in valid_roles:
return redirect("epic:gatekeeper", room_id=room_id)
return redirect("epic:room", room_id=room_id)
with transaction.atomic():
active_seat = room.table_seats.select_for_update().filter(
role__isnull=True
).order_by("slot_number").first()
if not active_seat or active_seat.gamer != request.user:
return redirect("epic:gatekeeper", room_id=room_id)
return redirect("epic:room", room_id=room_id)
if room.table_seats.filter(role=role).exists():
return HttpResponse(status=409)
active_seat.role = role
@@ -416,7 +439,7 @@ def select_role(request, room_id):
record(room, GameEvent.ROLES_REVEALED)
_notify_roles_revealed(room_id)
return HttpResponse(status=200)
return redirect("epic:gatekeeper", room_id=room_id)
return redirect("epic:room", room_id=room_id)
@login_required
@@ -433,7 +456,7 @@ def pick_roles(request, room_id):
slot_number=slot.slot_number,
)
_notify_role_select_start(room_id)
return redirect("epic:gatekeeper", room_id=room_id)
return redirect("epic:room", room_id=room_id)
@login_required
@@ -487,7 +510,10 @@ def select_sig(request, room_id):
return redirect("epic:gatekeeper", room_id=room_id)
room = Room.objects.get(id=room_id)
if room.table_status != Room.SIG_SELECT:
return redirect("epic:gatekeeper", room_id=room_id)
return redirect(
"epic:room" if room.table_status else "epic:gatekeeper",
room_id=room_id,
)
active_seat = active_sig_seat(room)
if active_seat is None or active_seat.gamer != request.user:
return HttpResponse(status=403)