add Carte Blanche trinket: equip system, gatekeeper multi-slot, mini tooltip portal; new token type Token.CARTE ('carte') with fa-money-check icon; migrations 0010-0012:
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
CARTE type, User.equipped_trinket FK, Token.slots_claimed field; post_save signal sets equipped_trinket=COIN for new users, PASS for staff; kit bag now shows only the equipped trinket in Trinkets section; Game Kit applet mini tooltip portal shows Equipped or Equip Trinket per token; AJAX POST equip-trinket id updates equippedId in-place; equip btn now works for COIN, PASS, and CARTE (data-token-id added to all three); Gatekeeper CARTE flow: drop_token sets current_room (no slot reserved); each empty slot up to slots_claimed+1 gets a drop-token-btn; slots_claimed high-water mark advances on fill, never decrements; highest CARTE-filled slot gets NVM (release_slot); token_return_btn resets current_room + slots_claimed + un-fills all CARTE slots; gate_status always returns full template so launch-game-btn persists via HTMX when gate_status == OPEN; room.html includes gatekeeper when GATHERING or OPEN; new FT test_trinket_carte_blanche.py (2 tests, both passing); 299 tests green
This commit is contained in:
@@ -26,14 +26,30 @@ def _gate_context(room, user):
|
||||
pending_slot = slots.filter(status=GateSlot.RESERVED).first()
|
||||
user_reserved_slot = None
|
||||
user_filled_slot = None
|
||||
carte_token = None
|
||||
carte_slots_claimed = 0
|
||||
carte_nvm_slot_number = None
|
||||
if user.is_authenticated:
|
||||
user_reserved_slot = slots.filter(gamer=user, status=GateSlot.RESERVED).first()
|
||||
user_filled_slot = slots.filter(gamer=user, status=GateSlot.FILLED).first()
|
||||
carte_token = user.tokens.filter(
|
||||
token_type=Token.CARTE, current_room=room
|
||||
).first()
|
||||
if carte_token:
|
||||
carte_slots_claimed = carte_token.slots_claimed
|
||||
# NVM shown on the highest-numbered slot this user filled via CARTE
|
||||
nvm_slot = slots.filter(
|
||||
debited_token_type=Token.CARTE, gamer=user, status=GateSlot.FILLED
|
||||
).order_by("-slot_number").first()
|
||||
if nvm_slot:
|
||||
carte_nvm_slot_number = nvm_slot.slot_number
|
||||
carte_active = carte_token is not None
|
||||
eligible = (
|
||||
user.is_authenticated
|
||||
and pending_slot is None
|
||||
and user_reserved_slot is None
|
||||
and user_filled_slot is None
|
||||
and not carte_active
|
||||
)
|
||||
token_depleted = eligible and select_token(user) is None
|
||||
can_drop = eligible and not token_depleted
|
||||
@@ -41,7 +57,7 @@ def _gate_context(room, user):
|
||||
user_reserved_slot is not None
|
||||
and slots.filter(status=GateSlot.EMPTY).count() == 0
|
||||
)
|
||||
user_can_reject = user_reserved_slot is not None or user_filled_slot is not None
|
||||
user_can_reject = user_reserved_slot is not None or user_filled_slot is not None or carte_active
|
||||
return {
|
||||
"slots": slots,
|
||||
"pending_slot": pending_slot,
|
||||
@@ -51,6 +67,9 @@ def _gate_context(room, user):
|
||||
"token_depleted": token_depleted,
|
||||
"is_last_slot": is_last_slot,
|
||||
"user_can_reject": user_can_reject,
|
||||
"carte_active": carte_active,
|
||||
"carte_slots_claimed": carte_slots_claimed,
|
||||
"carte_nvm_slot_number": carte_nvm_slot_number,
|
||||
}
|
||||
|
||||
|
||||
@@ -75,10 +94,6 @@ def gatekeeper(request, room_id):
|
||||
def drop_token(request, room_id):
|
||||
if request.method == "POST":
|
||||
room = Room.objects.get(id=room_id)
|
||||
if room.gate_slots.filter(status=GateSlot.RESERVED).exists():
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
if room.gate_slots.filter(gamer=request.user, status=GateSlot.FILLED).exists():
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
token_id = request.POST.get("token_id")
|
||||
if token_id:
|
||||
token = request.user.tokens.filter(id=token_id).first()
|
||||
@@ -86,6 +101,17 @@ def drop_token(request, room_id):
|
||||
token = select_token(request.user)
|
||||
if token is None:
|
||||
return HttpResponse(status=402)
|
||||
if token.token_type == Token.CARTE:
|
||||
# CARTE enters the machine without reserving a slot — all slots
|
||||
# become individually claimable via .drop-token-btn
|
||||
token.current_room = room
|
||||
token.save()
|
||||
request.session["kit_token_id"] = str(token.id)
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
if room.gate_slots.filter(status=GateSlot.RESERVED).exists():
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
if room.gate_slots.filter(gamer=request.user, status=GateSlot.FILLED).exists():
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
slot = room.gate_slots.filter(
|
||||
status=GateSlot.EMPTY
|
||||
).order_by("slot_number").first()
|
||||
@@ -102,18 +128,35 @@ def drop_token(request, room_id):
|
||||
def confirm_token(request, room_id):
|
||||
if request.method == "POST":
|
||||
room = Room.objects.get(id=room_id)
|
||||
slot = room.gate_slots.filter(
|
||||
gamer=request.user, status=GateSlot.RESERVED
|
||||
).first()
|
||||
if slot:
|
||||
token_id = request.session.pop("kit_token_id", None)
|
||||
token = None
|
||||
if token_id:
|
||||
token = request.user.tokens.filter(id=token_id).first()
|
||||
if not token:
|
||||
token = select_token(request.user)
|
||||
if token:
|
||||
debit_token(request.user, slot, token)
|
||||
slot_number = request.POST.get("slot_number")
|
||||
if slot_number:
|
||||
# CARTE per-slot fill: directly fill the requested slot
|
||||
carte = request.user.tokens.filter(
|
||||
token_type=Token.CARTE, current_room=room
|
||||
).first()
|
||||
if carte:
|
||||
slot = room.gate_slots.filter(
|
||||
slot_number=slot_number, status=GateSlot.EMPTY
|
||||
).first()
|
||||
if slot:
|
||||
debit_token(request.user, slot, carte)
|
||||
# slots_claimed is the high-water mark — advance if beyond current
|
||||
if int(slot_number) > carte.slots_claimed:
|
||||
carte.slots_claimed = int(slot_number)
|
||||
carte.save()
|
||||
else:
|
||||
slot = room.gate_slots.filter(
|
||||
gamer=request.user, status=GateSlot.RESERVED
|
||||
).first()
|
||||
if slot:
|
||||
token_id = request.session.pop("kit_token_id", None)
|
||||
token = None
|
||||
if token_id:
|
||||
token = request.user.tokens.filter(id=token_id).first()
|
||||
if not token:
|
||||
token = select_token(request.user)
|
||||
if token:
|
||||
debit_token(request.user, slot, token)
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
|
||||
@@ -121,6 +164,22 @@ def confirm_token(request, room_id):
|
||||
def return_token(request, room_id):
|
||||
if request.method == "POST":
|
||||
room = Room.objects.get(id=room_id)
|
||||
# CARTE full return: reset token + all CARTE-debited slots
|
||||
carte = request.user.tokens.filter(
|
||||
token_type=Token.CARTE, current_room=room
|
||||
).first()
|
||||
if carte:
|
||||
room.gate_slots.filter(
|
||||
debited_token_type=Token.CARTE, gamer=request.user
|
||||
).update(
|
||||
gamer=None, status=GateSlot.EMPTY, filled_at=None,
|
||||
debited_token_type=None, debited_token_expires_at=None,
|
||||
)
|
||||
carte.current_room = None
|
||||
carte.slots_claimed = 0
|
||||
carte.save()
|
||||
request.session.pop("kit_token_id", None)
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
slot = room.gate_slots.filter(
|
||||
gamer=request.user,
|
||||
status__in=[GateSlot.RESERVED, GateSlot.FILLED],
|
||||
@@ -152,6 +211,29 @@ def return_token(request, room_id):
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
|
||||
@login_required
|
||||
def release_slot(request, room_id):
|
||||
"""Un-fill a single CARTE-claimed slot without returning the CARTE itself."""
|
||||
if request.method == "POST":
|
||||
room = Room.objects.get(id=room_id)
|
||||
slot_number = request.POST.get("slot_number")
|
||||
if slot_number:
|
||||
slot = room.gate_slots.filter(
|
||||
slot_number=slot_number,
|
||||
debited_token_type=Token.CARTE,
|
||||
gamer=request.user,
|
||||
status=GateSlot.FILLED,
|
||||
).first()
|
||||
if slot:
|
||||
slot.gamer = None
|
||||
slot.status = GateSlot.EMPTY
|
||||
slot.filled_at = None
|
||||
slot.debited_token_type = None
|
||||
slot.debited_token_expires_at = None
|
||||
slot.save()
|
||||
return redirect("epic:gatekeeper", room_id=room_id)
|
||||
|
||||
|
||||
@login_required
|
||||
def invite_gamer(request, room_id):
|
||||
if request.method == "POST":
|
||||
@@ -192,8 +274,6 @@ def abandon_room(request, room_id):
|
||||
|
||||
def gate_status(request, room_id):
|
||||
room = Room.objects.get(id=room_id)
|
||||
if room.gate_status == Room.OPEN:
|
||||
return HttpResponse("")
|
||||
ctx = _gate_context(room, request.user)
|
||||
ctx["room"] = room
|
||||
return render(request, "apps/gameboard/_partials/_gatekeeper.html", ctx)
|
||||
|
||||
Reference in New Issue
Block a user