Compare commits
2 Commits
441def9a34
...
596175cd1c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
596175cd1c | ||
|
|
1aaf353066 |
18
src/apps/epic/migrations/0018_alter_tarotcard_suit.py
Normal file
18
src/apps/epic/migrations/0018_alter_tarotcard_suit.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-04-01 17:37
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epic', '0017_tableseat_significator_fk'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tarotcard',
|
||||||
|
name='suit',
|
||||||
|
field=models.CharField(blank=True, choices=[('WANDS', 'Wands'), ('CUPS', 'Cups'), ('SWORDS', 'Swords'), ('PENTACLES', 'Pentacles')], max_length=10, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
Data migration: rename The Schiz (card 0) and the five Pope cards (cards 1–5)
|
||||||
|
in the Earthman deck.
|
||||||
|
|
||||||
|
0: "The Schiz" → "The Nomad"
|
||||||
|
1: "Pope 1: Chancellor" → "Pope 1: The Schizo"
|
||||||
|
2: "Pope 2: President" → "Pope 2: The Despot"
|
||||||
|
3: "Pope 3: Tsar" → "Pope 3: The Capitalist"
|
||||||
|
4: "Pope 4: Chairman" → "Pope 4: The Fascist"
|
||||||
|
5: "Pope 5: Emperor" → "Pope 5: The War Machine"
|
||||||
|
"""
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
NEW_NAMES = {
|
||||||
|
0: ("The Nomad", "the-nomad"),
|
||||||
|
1: ("Pope 1: The Schizo", "pope-1-the-schizo"),
|
||||||
|
2: ("Pope 2: The Despot", "pope-2-the-despot"),
|
||||||
|
3: ("Pope 3: The Capitalist", "pope-3-the-capitalist"),
|
||||||
|
4: ("Pope 4: The Fascist", "pope-4-the-fascist"),
|
||||||
|
5: ("Pope 5: The War Machine","pope-5-the-war-machine"),
|
||||||
|
}
|
||||||
|
|
||||||
|
OLD_NAMES = {
|
||||||
|
0: ("The Schiz", "the-schiz"),
|
||||||
|
1: ("Pope 1: Chancellor", "pope-1-chancellor"),
|
||||||
|
2: ("Pope 2: President", "pope-2-president"),
|
||||||
|
3: ("Pope 3: Tsar", "pope-3-tsar"),
|
||||||
|
4: ("Pope 4: Chairman", "pope-4-chairman"),
|
||||||
|
5: ("Pope 5: Emperor", "pope-5-emperor"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def rename_forward(apps, schema_editor):
|
||||||
|
TarotCard = apps.get_model("epic", "TarotCard")
|
||||||
|
DeckVariant = apps.get_model("epic", "DeckVariant")
|
||||||
|
|
||||||
|
earthman = DeckVariant.objects.filter(slug="earthman").first()
|
||||||
|
if not earthman:
|
||||||
|
return
|
||||||
|
|
||||||
|
for number, (new_name, new_slug) in NEW_NAMES.items():
|
||||||
|
TarotCard.objects.filter(
|
||||||
|
deck_variant=earthman, arcana="MAJOR", number=number
|
||||||
|
).update(name=new_name, slug=new_slug)
|
||||||
|
|
||||||
|
|
||||||
|
def rename_reverse(apps, schema_editor):
|
||||||
|
TarotCard = apps.get_model("epic", "TarotCard")
|
||||||
|
DeckVariant = apps.get_model("epic", "DeckVariant")
|
||||||
|
|
||||||
|
earthman = DeckVariant.objects.filter(slug="earthman").first()
|
||||||
|
if not earthman:
|
||||||
|
return
|
||||||
|
|
||||||
|
for number, (old_name, old_slug) in OLD_NAMES.items():
|
||||||
|
TarotCard.objects.filter(
|
||||||
|
deck_variant=earthman, arcana="MAJOR", number=number
|
||||||
|
).update(name=old_name, slug=old_slug)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("epic", "0018_alter_tarotcard_suit"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(rename_forward, reverse_code=rename_reverse),
|
||||||
|
]
|
||||||
@@ -213,13 +213,11 @@ class TarotCard(models.Model):
|
|||||||
CUPS = "CUPS"
|
CUPS = "CUPS"
|
||||||
SWORDS = "SWORDS"
|
SWORDS = "SWORDS"
|
||||||
PENTACLES = "PENTACLES" # Fiorentine 4th suit
|
PENTACLES = "PENTACLES" # Fiorentine 4th suit
|
||||||
COINS = "COINS" # Earthman 4th suit (Ossum / Stone)
|
|
||||||
SUIT_CHOICES = [
|
SUIT_CHOICES = [
|
||||||
(WANDS, "Wands"),
|
(WANDS, "Wands"),
|
||||||
(CUPS, "Cups"),
|
(CUPS, "Cups"),
|
||||||
(SWORDS, "Swords"),
|
(SWORDS, "Swords"),
|
||||||
(PENTACLES, "Pentacles"),
|
(PENTACLES, "Pentacles"),
|
||||||
(COINS, "Coins"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
deck_variant = models.ForeignKey(
|
deck_variant = models.ForeignKey(
|
||||||
@@ -282,7 +280,6 @@ class TarotCard(models.Model):
|
|||||||
self.WANDS: 'fa-wand-sparkles',
|
self.WANDS: 'fa-wand-sparkles',
|
||||||
self.CUPS: 'fa-trophy',
|
self.CUPS: 'fa-trophy',
|
||||||
self.SWORDS: 'fa-gun',
|
self.SWORDS: 'fa-gun',
|
||||||
self.COINS: 'fa-star',
|
|
||||||
self.PENTACLES: 'fa-star',
|
self.PENTACLES: 'fa-star',
|
||||||
}.get(self.suit, '')
|
}.get(self.suit, '')
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ function initGameKitPage() {
|
|||||||
fanContent.innerHTML = html;
|
fanContent.innerHTML = html;
|
||||||
cards = Array.from(fanContent.querySelectorAll('.fan-card'));
|
cards = Array.from(fanContent.querySelectorAll('.fan-card'));
|
||||||
if (currentIndex >= cards.length) currentIndex = 0;
|
if (currentIndex >= cards.length) currentIndex = 0;
|
||||||
|
cards.forEach(function(c) {
|
||||||
|
c.style.transition = 'transform 0.18s ease-out, opacity 0.18s ease-out';
|
||||||
|
});
|
||||||
updateFan();
|
updateFan();
|
||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
});
|
});
|
||||||
@@ -84,6 +87,21 @@ function initGameKitPage() {
|
|||||||
updateFan();
|
updateFan();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step through multiple cards one at a time so intermediate cards are visible
|
||||||
|
var _navTimer = null;
|
||||||
|
function navigateAnimated(steps) {
|
||||||
|
if (!cards.length || steps === 0) return;
|
||||||
|
clearTimeout(_navTimer);
|
||||||
|
var sign = steps > 0 ? 1 : -1;
|
||||||
|
var remaining = Math.abs(steps);
|
||||||
|
function tick() {
|
||||||
|
navigate(sign);
|
||||||
|
remaining--;
|
||||||
|
if (remaining > 0) _navTimer = setTimeout(tick, 60);
|
||||||
|
}
|
||||||
|
tick();
|
||||||
|
}
|
||||||
|
|
||||||
// Click on the dark backdrop (the dialog or fan-wrap itself, not on any card child) closes
|
// Click on the dark backdrop (the dialog or fan-wrap itself, not on any card child) closes
|
||||||
var fanWrap = dialog.querySelector('.tarot-fan-wrap');
|
var fanWrap = dialog.querySelector('.tarot-fan-wrap');
|
||||||
dialog.addEventListener('click', function(e) {
|
dialog.addEventListener('click', function(e) {
|
||||||
@@ -96,16 +114,46 @@ function initGameKitPage() {
|
|||||||
if (e.key === 'ArrowLeft') navigate(-1);
|
if (e.key === 'ArrowLeft') navigate(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mousewheel navigation — throttled so each detent advances one card
|
// Mousewheel navigation — accumulate delta, cap at 3 cards per event so fast
|
||||||
var lastWheel = 0;
|
// spins don't overshoot; CSS transitions handle the visual smoothness.
|
||||||
|
var wheelAccum = 0;
|
||||||
|
var wheelDecayTimer = null;
|
||||||
|
var WHEEL_STEP = 150;
|
||||||
dialog.addEventListener('wheel', function(e) {
|
dialog.addEventListener('wheel', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var now = Date.now();
|
clearTimeout(wheelDecayTimer);
|
||||||
if (now - lastWheel < 150) return;
|
wheelAccum += e.deltaY;
|
||||||
lastWheel = now;
|
var steps = Math.trunc(wheelAccum / WHEEL_STEP);
|
||||||
navigate(e.deltaY > 0 ? 1 : -1);
|
if (steps !== 0) {
|
||||||
|
steps = Math.sign(steps) * Math.min(Math.abs(steps), 3);
|
||||||
|
wheelAccum -= steps * WHEEL_STEP;
|
||||||
|
navigate(steps);
|
||||||
|
}
|
||||||
|
wheelDecayTimer = setTimeout(function() { wheelAccum = 0; }, 200);
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
|
// Touch/swipe navigation — uses navigateAnimated so intermediate cards are visible
|
||||||
|
var touchStartX = 0;
|
||||||
|
var touchStartY = 0;
|
||||||
|
var touchStartTime = 0;
|
||||||
|
dialog.addEventListener('touchstart', function(e) {
|
||||||
|
touchStartX = e.touches[0].clientX;
|
||||||
|
touchStartY = e.touches[0].clientY;
|
||||||
|
touchStartTime = Date.now();
|
||||||
|
}, { passive: true });
|
||||||
|
dialog.addEventListener('touchend', function(e) {
|
||||||
|
var dx = e.changedTouches[0].clientX - touchStartX;
|
||||||
|
var dy = e.changedTouches[0].clientY - touchStartY;
|
||||||
|
if (Math.abs(dy) > Math.abs(dx)) return; // vertical swipe — ignore
|
||||||
|
if (Math.abs(dx) < 60) return; // dead zone — raise to 40–60 for more deliberate swipe required
|
||||||
|
var elapsed = Math.max(1, Date.now() - touchStartTime);
|
||||||
|
var velocity = Math.abs(dx) / elapsed; // px/ms
|
||||||
|
var steps = velocity > 0.8 // flick threshold — raise (e.g. 0.6) so more swipes use drag formula
|
||||||
|
? Math.max(1, Math.round(velocity * 4)) // flick multiplier — lower (e.g. 4–5) to reduce cards per fast flick
|
||||||
|
: Math.round(Math.abs(dx) / 150); // slow-drag divisor — raise (e.g. 120–150) for fewer cards per short drag
|
||||||
|
navigateAnimated(dx < 0 ? steps : -steps);
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
prevBtn.addEventListener('click', function() { navigate(-1); });
|
prevBtn.addEventListener('click', function() { navigate(-1); });
|
||||||
nextBtn.addEventListener('click', function() { navigate(1); });
|
nextBtn.addEventListener('click', function() { navigate(1); });
|
||||||
|
|
||||||
|
|||||||
@@ -227,6 +227,14 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
perspective: 900px;
|
perspective: 900px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tarot-fan {
|
.tarot-fan {
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ html:has(.gate-backdrop) {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.launch-game-btn {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.gate-modal {
|
.gate-modal {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -246,6 +250,11 @@ html:has(.gate-backdrop) {
|
|||||||
|
|
||||||
// Narrow viewport — scale down, 2×3 slot grid (portrait mobile + narrow desktop)
|
// Narrow viewport — scale down, 2×3 slot grid (portrait mobile + narrow desktop)
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
|
// Floor the gatekeeper modal below the position-strip circles (~1.5rem top + 3rem height)
|
||||||
|
.gate-overlay {
|
||||||
|
padding-top: 5.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.gate-modal {
|
.gate-modal {
|
||||||
padding: 1.25rem 1.5rem;
|
padding: 1.25rem 1.5rem;
|
||||||
|
|
||||||
@@ -298,7 +307,7 @@ $pos-d-y: round($pos-d * 0.866); // 95px
|
|||||||
// absolute children share the root stacking context with the fixed overlays.
|
// absolute children share the root stacking context with the fixed overlays.
|
||||||
.position-strip {
|
.position-strip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.5rem;
|
top: 1.5rem;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 130;
|
z-index: 130;
|
||||||
@@ -675,6 +684,32 @@ $card-h: 120px;
|
|||||||
|
|
||||||
// Landscape mobile — aggressively scale down to fit short viewport
|
// Landscape mobile — aggressively scale down to fit short viewport
|
||||||
@media (orientation: landscape) and (max-width: 1440px) {
|
@media (orientation: landscape) and (max-width: 1440px) {
|
||||||
|
// Reflow position strip into a vertical column along the left edge,
|
||||||
|
// reversed so 6 is at top, 1 at bottom, below the GAMEROOM title.
|
||||||
|
.position-strip {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
top: 3rem;
|
||||||
|
left: 0.5rem;
|
||||||
|
right: auto;
|
||||||
|
gap: round($gate-gap * 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shallow landscape (phones): wrap into two columns — left: 6,5,4 / right: 3,2,1
|
||||||
|
// Columns grow rightward (wrap, not wrap-reverse) so overflow: hidden doesn't clip.
|
||||||
|
// order: -1 on slots 4–6 pulls them to the front of the flex sequence; combined
|
||||||
|
// with column-reverse they land in the left column reading 6,5,4 top-to-bottom.
|
||||||
|
@media (max-height: 550px) {
|
||||||
|
.position-strip {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
// cap height to exactly 3 circles so the 4th wraps to a new column
|
||||||
|
max-height: #{3 * round($gate-node * 0.75) + 2 * round($gate-gap * 0.4)};
|
||||||
|
|
||||||
|
.gate-slot[data-slot="4"],
|
||||||
|
.gate-slot[data-slot="5"],
|
||||||
|
.gate-slot[data-slot="6"] { order: -1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.gate-modal {
|
.gate-modal {
|
||||||
padding: 0.6rem 1.25rem;
|
padding: 0.6rem 1.25rem;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user