from django.contrib.auth.decorators import login_required from django.http import HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone from apps.applets.utils import applet_context, apply_applet_toggle def _annotate_deck_in_use(decks, user): """Attach .in_use_room_name to each deck — the name of the active room using it, or None.""" active = { ts.deck_variant_id: ts.room.name for ts in TableSeat.objects.filter( gamer=user, deck_variant__isnull=False, ).select_related("room") } for deck in decks: deck.in_use_room_name = active.get(deck.pk) return decks from apps.epic.models import DeckVariant, Room, TableSeat from apps.epic.utils import annotate_latest_event, rooms_for_user from apps.lyric.models import Token GAMEBOARD_APPLET_ORDER = [ "new-game", "my-games", "game-kit", ] @login_required(login_url="/") def gameboard(request): pass_token = request.user.tokens.filter(token_type=Token.PASS).first() if request.user.is_staff else None coin = request.user.tokens.filter(token_type=Token.COIN).first() carte = request.user.tokens.filter(token_type=Token.CARTE).first() free_tokens = list(request.user.tokens.filter( token_type=Token.FREE, expires_at__gt=timezone.now() ).order_by("expires_at")) return render( request, "apps/gameboard/gameboard.html", { "pass_token": pass_token, "coin": coin, "carte": carte, "equipped_trinket_id": request.user.equipped_trinket_id, "equipped_deck_id": request.user.equipped_deck_id, "deck_variants": _annotate_deck_in_use(list(request.user.unlocked_decks.all()), request.user), "free_tokens": free_tokens, "free_count": len(free_tokens), "applets": applet_context(request.user, "gameboard"), "page_class": "page-gameboard", "my_games": annotate_latest_event(rooms_for_user(request.user)), } ) @login_required(login_url="/") def toggle_game_applets(request): checked = request.POST.getlist("applets") apply_applet_toggle(request.user, "gameboard", checked) if request.headers.get("HX-Request"): free_tokens = list(request.user.tokens.filter( token_type=Token.FREE, expires_at__gt=timezone.now() ).order_by("expires_at")) return render(request, "apps/gameboard/_partials/_applets.html", { "applets": applet_context(request.user, "gameboard"), "pass_token": request.user.tokens.filter(token_type=Token.PASS).first() if request.user.is_staff else None, "coin": request.user.tokens.filter(token_type=Token.COIN).first(), "carte": request.user.tokens.filter(token_type=Token.CARTE).first(), "equipped_trinket_id": request.user.equipped_trinket_id, "equipped_deck_id": request.user.equipped_deck_id, "deck_variants": _annotate_deck_in_use(list(request.user.unlocked_decks.all()), request.user), "free_tokens": free_tokens, "free_count": len(free_tokens), "my_games": annotate_latest_event(rooms_for_user(request.user)), }) return redirect("gameboard") @login_required(login_url="/") def equip_trinket(request, token_id): token = get_object_or_404(Token, pk=token_id, user=request.user) if request.method == "POST": request.user.equipped_trinket = token request.user.save(update_fields=["equipped_trinket"]) return HttpResponse(status=204) return render( request, "apps/gameboard/_partials/_equip_trinket_btn.html", {"token": token}, ) @login_required(login_url="/") def equip_deck(request, deck_id): deck = get_object_or_404(DeckVariant, pk=deck_id) if request.method == "POST": request.user.equipped_deck = deck request.user.save(update_fields=["equipped_deck"]) return HttpResponse(status=204) return HttpResponse(status=405) @login_required(login_url="/") def unequip_trinket(request, token_id): token = get_object_or_404(Token, pk=token_id, user=request.user) if request.method == "POST": if request.user.equipped_trinket_id == token.pk: request.user.equipped_trinket = None request.user.save(update_fields=["equipped_trinket"]) return HttpResponse(status=204) return HttpResponse(status=405) @login_required(login_url="/") def unequip_deck(request, deck_id): get_object_or_404(DeckVariant, pk=deck_id) if request.method == "POST": if request.user.equipped_deck_id == deck_id: request.user.equipped_deck = None request.user.save(update_fields=["equipped_deck"]) return HttpResponse(status=204) return HttpResponse(status=405) def _game_kit_context(user): from apps.lyric.models import PRONOUN_CHOICES coin = user.tokens.filter(token_type=Token.COIN).first() pass_token = user.tokens.filter(token_type=Token.PASS).first() if user.is_staff else None carte = user.tokens.filter(token_type=Token.CARTE).first() free_tokens = list(user.tokens.filter( token_type=Token.FREE, expires_at__gt=timezone.now() ).order_by("expires_at")) tithe_tokens = list(user.tokens.filter(token_type=Token.TITHE)) pronoun_options = [ {"key": k, "label": label, "active": (k == user.pronouns)} for (k, label) in PRONOUN_CHOICES ] return { "coin": coin, "pass_token": pass_token, "carte": carte, "free_tokens": free_tokens, "tithe_tokens": tithe_tokens, "unlocked_decks": list(user.unlocked_decks.all()), "applets": applet_context(user, "game-kit"), "pronoun_options": pronoun_options, "current_pronouns": user.pronouns, } @login_required(login_url="/") def game_kit(request): return render(request, "apps/gameboard/game_kit.html", { **_game_kit_context(request.user), "page_class": "page-gameboard", }) @login_required(login_url="/") def toggle_game_kit_sections(request): checked = request.POST.getlist("applets") apply_applet_toggle(request.user, "game-kit", checked) if request.headers.get("HX-Request"): return render(request, "apps/gameboard/_partials/_game_kit_sections.html", _game_kit_context(request.user)) return redirect("game_kit") @login_required(login_url="/") def my_sea(request): """Shell view for the My Sea standalone page. Branches three ways: 1. No sig → Look!-formatted gate w. FYI/NVM (Sprint 4b). 2. Sig + equipped deck → DRAW SEA landing (Sprint 5 iter 1) — hex w. 6 chair seats labeled 1C-6C + central DRAW SEA btn. Click swaps data-phase to picker (the picker UX itself lands in iter 2). 3. Sig + no equipped deck → same landing PLUS a 'Default deck warning' Brief banner identical to the one on /billboard/my-sign/ (the user is headed for a draw against the Earthman [Shabby Cardstock] backup deck unless they equip one first). """ user_has_sig = request.user.significator_id is not None no_equipped_deck = request.user.equipped_deck_id is None return render(request, "apps/gameboard/my_sea.html", { "user_has_sig": user_has_sig, "no_equipped_deck": no_equipped_deck, "show_backup_intro_banner": user_has_sig and no_equipped_deck, # Sprint 5 iter 2 — significator pinned in `.sea-pos-core` on the # picker phase. Template guards on `user_has_sig` so a None pass- # through is safe; we pass the FK directly so `.corner_rank` + # `.suit_icon` resolve at render time. "significator": request.user.significator, "significator_reversed": request.user.significator_reversed, # Sprint 5 iter 3 — SPREAD dropdown defaults to Situation/Action/ # Outcome (a 3-card spread) per user-locked spec; `reversals_pct` # is a placeholder UI value pending the per-user setting. "default_spread": "situation-action-outcome", "reversals_pct": 25, "page_class": "page-gameboard page-my-sea", }) @login_required(login_url="/") def tarot_fan(request, deck_id): from apps.epic.models import TarotCard deck = get_object_or_404(DeckVariant, pk=deck_id) if not request.user.unlocked_decks.filter(pk=deck_id).exists(): return HttpResponse(status=403) _suit_order = {"BRANDS": 0, "GRAILS": 1, "BLADES": 2, "CROWNS": 3, "WANDS": 0, "CUPS": 1, "SWORDS": 2, "PENTACLES": 3} cards = sorted( TarotCard.objects.filter(deck_variant=deck), key=lambda c: (0 if c.arcana == "MAJOR" else 1, _suit_order.get(c.suit or "", 9), c.number), ) return render(request, "apps/gameboard/_partials/_tarot_fan.html", { "deck": deck, "cards": cards, })