PICK SEA reversal axis: server-side roll + preview + deposited slot — TDD
- new apps/epic/utils.STACK_REVERSAL_PROBABILITY (=0.25) + stack_reversal_probability(user, room) helper; single source of truth across game phases & one-line swap point for forthcoming per-user-profile config - sea_deck view rolls each card's `reversed` axis at fetch time using the helper, attaches to card JSON; matches the eager shuffle pattern (whole deal determined at phase start) - room_view + sea_partial pass `stack_reversal_pct` into context for the new <p class="sea-reversal-hint">25% reversals</p> hint above the SPREAD combobox (italic, 0.7rem, 0.55 opacity) - SeaDeal.openStage applies .stage-card--reversed + .is-reversed to stat block when card.reversed → preview lands face-reversed w. REVERSAL keywords - _fillSlot adds .sea-card-slot--reversed → slot itself rotates 180° (bg + border + content stack flips, not just inner chars upside-down in place); .sea-pos-cross overrides to 270° to compose w. its existing 90° - _fillSlot adds .sea-card-slot--rank-long when corner_rank.length ≥ 5 (XVIII / XXIII / XXVIII / XXXIII / XXXVIII / XLIII / XLVIII) → SCSS scaleX(0.7) + letter-spacing -0.05em squeezes horizontally w.o changing font-size Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,10 @@ var SeaDeal = (function () {
|
|||||||
slot.classList.remove('sea-card-slot--empty');
|
slot.classList.remove('sea-card-slot--empty');
|
||||||
slot.classList.add('sea-card-slot--filled');
|
slot.classList.add('sea-card-slot--filled');
|
||||||
slot.classList.add(isLevity ? 'sea-card-slot--levity' : 'sea-card-slot--gravity');
|
slot.classList.add(isLevity ? 'sea-card-slot--levity' : 'sea-card-slot--gravity');
|
||||||
|
if (card.reversed) slot.classList.add('sea-card-slot--reversed');
|
||||||
|
// Long Roman numerals (XVIII / XXIII / XXVIII / XXXIII / XXXVIII /
|
||||||
|
// XLIII / XLVIII) need horizontal squeezing to fit the slot — see SCSS.
|
||||||
|
if ((card.corner_rank || '').length >= 5) slot.classList.add('sea-card-slot--rank-long');
|
||||||
slot.dataset.cardId = String(card.id);
|
slot.dataset.cardId = String(card.id);
|
||||||
slot.dataset.posKey = posSelector;
|
slot.dataset.posKey = posSelector;
|
||||||
slot.innerHTML =
|
slot.innerHTML =
|
||||||
@@ -101,6 +105,13 @@ var SeaDeal = (function () {
|
|||||||
_viewingPos = posSelector;
|
_viewingPos = posSelector;
|
||||||
_seaHand[posSelector] = { card: card, isLevity: isLevity };
|
_seaHand[posSelector] = { card: card, isLevity: isLevity };
|
||||||
_populate(card, isLevity);
|
_populate(card, isLevity);
|
||||||
|
// Server pre-rolled the reversal axis at deck-fetch time
|
||||||
|
// (apps.epic.utils.stack_reversal_probability). Honor it here so the
|
||||||
|
// card lands face-reversed if rolled.
|
||||||
|
if (card.reversed) {
|
||||||
|
statBlock.classList.add('is-reversed');
|
||||||
|
stageCard.classList.add('stage-card--reversed');
|
||||||
|
}
|
||||||
_fillSlot(posSelector, card, isLevity);
|
_fillSlot(posSelector, card, isLevity);
|
||||||
_showStage(isLevity);
|
_showStage(isLevity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,28 @@ from django.db.models import Q
|
|||||||
from apps.epic.models import Room, RoomInvite
|
from apps.epic.models import Room, RoomInvite
|
||||||
|
|
||||||
|
|
||||||
|
# ── Game-wide constants ────────────────────────────────────────────────────
|
||||||
|
# Reversal probability applied to any card pulled from a stack, anywhere in
|
||||||
|
# the game (PICK SEA initially; future phases — gameplay draws etc. — will
|
||||||
|
# share this single source of truth). Stub for a future per-user profile
|
||||||
|
# override: callers MUST go through stack_reversal_probability(user, room)
|
||||||
|
# rather than referencing the constant directly so the user-config hookup is
|
||||||
|
# a one-line change inside the helper.
|
||||||
|
STACK_REVERSAL_PROBABILITY = 0.25
|
||||||
|
|
||||||
|
|
||||||
|
def stack_reversal_probability(user=None, room=None):
|
||||||
|
"""Reversal probability for a draw stack in this user's context.
|
||||||
|
|
||||||
|
Current behavior: returns the module default for everyone. Plumbing point
|
||||||
|
for a forthcoming per-user setting — when that lands, swap the body to
|
||||||
|
something like `return getattr(user.profile, 'reversal_rate', STACK_REVERSAL_PROBABILITY)`
|
||||||
|
and every call site picks up the per-user value automatically.
|
||||||
|
"""
|
||||||
|
return STACK_REVERSAL_PROBABILITY
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _planet_house(degree, cusps):
|
def _planet_house(degree, cusps):
|
||||||
"""Return 1-based house number for a planet at ecliptic degree.
|
"""Return 1-based house number for a planet at ecliptic degree.
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from apps.epic.models import (
|
|||||||
active_sig_seat, debit_token, levity_sig_cards, gravity_sig_cards,
|
active_sig_seat, debit_token, levity_sig_cards, gravity_sig_cards,
|
||||||
select_token, sig_deck_cards,
|
select_token, sig_deck_cards,
|
||||||
)
|
)
|
||||||
from apps.epic.utils import _compute_distinctions, _planet_house
|
from apps.epic.utils import _compute_distinctions, _planet_house, stack_reversal_probability
|
||||||
from apps.lyric.models import Token
|
from apps.lyric.models import Token
|
||||||
|
|
||||||
|
|
||||||
@@ -402,6 +402,9 @@ def room_view(request, room_id):
|
|||||||
ctx = _role_select_context(room, request.user)
|
ctx = _role_select_context(room, request.user)
|
||||||
ctx["room"] = room
|
ctx["room"] = room
|
||||||
ctx["page_class"] = "page-gameboard"
|
ctx["page_class"] = "page-gameboard"
|
||||||
|
# Reversal-rate hint label under PICK SEA's SPREAD select — same helper as
|
||||||
|
# sea_partial so the value tracks any future per-user override automatically.
|
||||||
|
ctx["stack_reversal_pct"] = int(round(stack_reversal_probability(request.user, room) * 100))
|
||||||
return render(request, "apps/gameboard/room.html", ctx)
|
return render(request, "apps/gameboard/room.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
@@ -1132,6 +1135,11 @@ def sea_deck(request, room_id):
|
|||||||
.values_list('significator_id', flat=True)
|
.values_list('significator_id', flat=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Roll reversal eagerly during the shuffle — the deck order is fully
|
||||||
|
# determined at phase start, so the reversal axis should be too. Future
|
||||||
|
# per-user-profile config rides this same helper.
|
||||||
|
reversal_prob = stack_reversal_probability(request.user, room)
|
||||||
|
|
||||||
def _card_dict(c):
|
def _card_dict(c):
|
||||||
return {
|
return {
|
||||||
'id': c.id,
|
'id': c.id,
|
||||||
@@ -1157,6 +1165,8 @@ def sea_deck(request, room_id):
|
|||||||
'keywords_reversed': c.keywords_reversed,
|
'keywords_reversed': c.keywords_reversed,
|
||||||
'energies': c.energies,
|
'energies': c.energies,
|
||||||
'operations': c.operations,
|
'operations': c.operations,
|
||||||
|
# Pre-rolled reversal axis — server-deterministic, client just reads
|
||||||
|
'reversed': _random.random() < reversal_prob,
|
||||||
}
|
}
|
||||||
|
|
||||||
available = list(
|
available = list(
|
||||||
@@ -1178,5 +1188,10 @@ def sea_partial(request, room_id):
|
|||||||
if not ctx.get('sky_confirmed'):
|
if not ctx.get('sky_confirmed'):
|
||||||
return HttpResponse(status=403)
|
return HttpResponse(status=403)
|
||||||
ctx['room'] = room
|
ctx['room'] = room
|
||||||
|
# Reversal-rate hint label under SPREAD — both the percentage AND the raw
|
||||||
|
# probability flow from the same helper, so when per-user config lands we
|
||||||
|
# only swap the helper body and every render picks it up.
|
||||||
|
_prob = stack_reversal_probability(request.user, room)
|
||||||
|
ctx['stack_reversal_pct'] = int(round(_prob * 100))
|
||||||
return render(request, 'apps/gameboard/_partials/_sea_overlay.html', ctx)
|
return render(request, 'apps/gameboard/_partials/_sea_overlay.html', ctx)
|
||||||
|
|
||||||
|
|||||||
@@ -1053,6 +1053,28 @@ $sea-card-h: 6.5rem;
|
|||||||
border-color: rgba(var(--secUser), 0.6);
|
border-color: rgba(var(--secUser), 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reversed — pre-rolled by sea_deck server-side. Rotate the whole slot
|
||||||
|
// (background + border + content) so the rank/icon stacking order also
|
||||||
|
// flips (rank-top + icon-bottom upright → icon-top + rank-bottom reversed),
|
||||||
|
// not just each character upside-down in place.
|
||||||
|
.sea-card-slot--reversed { transform: rotate(180deg); }
|
||||||
|
|
||||||
|
// Cross-position adds 90° already; reversed cross combines to 270°. Higher
|
||||||
|
// specificity than the .sea-pos-cross .sea-card-slot rule so it wins.
|
||||||
|
.sea-pos-cross .sea-card-slot--reversed { transform: rotate(270deg); }
|
||||||
|
|
||||||
|
// Long Roman numerals (≥ 5 chars: XVIII, XXIII, XXVIII, XXXIII, XXXVIII,
|
||||||
|
// XLIII, XLVIII) — squeeze horizontally via scaleX so they fit the slot
|
||||||
|
// without dropping font-size (height stays the same). Class added in
|
||||||
|
// _fillSlot when card.corner_rank.length >= 5. Slot-level reversed rotation
|
||||||
|
// already carries the rank along, so scaleX is the only inner transform
|
||||||
|
// regardless of reversal state.
|
||||||
|
.sea-card-slot--rank-long .fan-corner-rank {
|
||||||
|
display: inline-block;
|
||||||
|
transform: scaleX(0.7);
|
||||||
|
letter-spacing: -0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
// Deposited — fully opaque by default; Cover/Cross are semi-transparent
|
// Deposited — fully opaque by default; Cover/Cross are semi-transparent
|
||||||
.sea-card-slot--visible { opacity: 1; transition: opacity 1s ease, box-shadow 0.15s ease; }
|
.sea-card-slot--visible { opacity: 1; transition: opacity 1s ease, box-shadow 0.15s ease; }
|
||||||
|
|
||||||
@@ -1188,6 +1210,16 @@ $sea-card-h: 6.5rem;
|
|||||||
label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; }
|
label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Forthcoming-feature hint between SPREAD label and the combobox; rendered
|
||||||
|
// value comes from apps.epic.utils.stack_reversal_probability via the view
|
||||||
|
// context.
|
||||||
|
.sea-reversal-hint {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
opacity: 0.55;
|
||||||
|
margin: -0.1rem 0 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
// Custom combobox replacement for native <select>. See combobox.js for the
|
// Custom combobox replacement for native <select>. See combobox.js for the
|
||||||
// expected markup; SCSS owns all visuals because the OS-native dropdown ignored
|
// expected markup; SCSS owns all visuals because the OS-native dropdown ignored
|
||||||
// option background/color anyway.
|
// option background/color anyway.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<header class="sea-modal-header">
|
<header class="sea-modal-header">
|
||||||
<h2>PICK <span>SEA</span></h2>
|
<h2>PICK <span>SEA</span></h2>
|
||||||
<p>Draw cards to circumscribe your character's influences and seed the Voronoi map.</p>
|
<p>Draw +6 cards to describe your character's influences and seed the game-map.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="sea-modal-body">
|
<div class="sea-modal-body">
|
||||||
@@ -63,6 +63,13 @@
|
|||||||
<div class="sea-form-main">
|
<div class="sea-form-main">
|
||||||
<div class="sea-field">
|
<div class="sea-field">
|
||||||
<label for="id_sea_spread" id="id_sea_spread_label">Spread</label>
|
<label for="id_sea_spread" id="id_sea_spread_label">Spread</label>
|
||||||
|
{% comment %}
|
||||||
|
Reversal-rate hint — `stack_reversal_pct` flows from
|
||||||
|
apps.epic.utils.stack_reversal_probability via the
|
||||||
|
view. Currently a module default; placeholder UI for
|
||||||
|
a forthcoming per-user setting.
|
||||||
|
{% endcomment %}
|
||||||
|
<p class="sea-reversal-hint">{{ stack_reversal_pct|default:25 }}% reversals</p>
|
||||||
{% comment %}
|
{% comment %}
|
||||||
Custom combobox — native <select> dropdowns ignore most CSS on
|
Custom combobox — native <select> dropdowns ignore most CSS on
|
||||||
Firefox/Chrome (OS-rendered list); this gives full styling control.
|
Firefox/Chrome (OS-rendered list); this gives full styling control.
|
||||||
|
|||||||
Reference in New Issue
Block a user