Compare commits
14 Commits
55bb450d27
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3800c5bdad | ||
|
|
12d575a84b | ||
|
|
c14b6d7062 | ||
|
|
a7c5468cbc | ||
|
|
4da8750c60 | ||
|
|
cf40f626e6 | ||
|
|
99a826f6c9 | ||
|
|
51fe2614fa | ||
|
|
56dc094b45 | ||
|
|
520fdf7862 | ||
|
|
e2cc38686f | ||
|
|
0bcc7567bb | ||
|
|
6654785f25 | ||
|
|
99a69202b9 |
@@ -0,0 +1,154 @@
|
|||||||
|
"""
|
||||||
|
Data migration — Earthman deck:
|
||||||
|
1. Rename three suit codes (and card names) for Earthman cards:
|
||||||
|
WANDS → BRANDS (Wands → Brands)
|
||||||
|
CUPS → GRAILS (Cups → Grails)
|
||||||
|
SWORDS → BLADES (Swords → Blades)
|
||||||
|
CROWNS stays CROWNS.
|
||||||
|
2. Copy keywords_upright / keywords_reversed from the Fiorentine Minchiate
|
||||||
|
deck to corresponding Earthman cards:
|
||||||
|
• Major: explicit number-to-number map based on card correspondences.
|
||||||
|
• Minor/Middle: same number, suit mapped (BRANDS→WANDS, GRAILS→CUPS,
|
||||||
|
BLADES→SWORDS, CROWNS→PENTACLES). Cards with no Fiorentine counterpart
|
||||||
|
stay with empty keyword lists.
|
||||||
|
"""
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
# ── 1. Suit rename map ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
SUIT_RENAMES = {
|
||||||
|
"WANDS": "BRANDS",
|
||||||
|
"CUPS": "GRAILS",
|
||||||
|
"SWORDS": "BLADES",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 2. Major arcana: Earthman number → Fiorentine number ─────────────────────
|
||||||
|
# Cards without a Fiorentine counterpart are omitted (keywords stay empty).
|
||||||
|
|
||||||
|
MAJOR_KEYWORD_MAP = {
|
||||||
|
0: 0, # The Schiz → The Fool
|
||||||
|
1: 1, # Pope I (President) → The Magician
|
||||||
|
2: 2, # Pope II (Tsar) → The High Priestess
|
||||||
|
3: 3, # Pope III (Chairman) → The Empress
|
||||||
|
4: 4, # Pope IV (Emperor) → The Emperor
|
||||||
|
5: 5, # Pope V (Chancellor) → The Hierophant
|
||||||
|
6: 8, # Virtue VI (Controlled Folly) → Strength
|
||||||
|
7: 11, # Virtue VII (Not-Doing) → Justice
|
||||||
|
8: 14, # Virtue VIII (Losing Self-Importance) → Temperance
|
||||||
|
# 9: Prudence — no Fiorentine equivalent
|
||||||
|
10: 10, # Wheel of Fortune → Wheel of Fortune
|
||||||
|
11: 7, # The Junkboat → The Chariot
|
||||||
|
12: 12, # The Junkman → The Hanged Man
|
||||||
|
13: 13, # Death → Death
|
||||||
|
14: 15, # The Traitor → The Devil
|
||||||
|
15: 16, # Disco Inferno → The Tower
|
||||||
|
# 16: Torre Terrestre (Purgatory) — no equivalent
|
||||||
|
# 17: Fantasia Celestia (Paradise) — no equivalent
|
||||||
|
18: 6, # Virtue XVIII (Stalking) → The Lovers
|
||||||
|
# 19: Virtue XIX (Intent / Hope) — no equivalent
|
||||||
|
# 20: Virtue XX (Dreaming / Faith)— no equivalent
|
||||||
|
# 21–38: Classical Elements + Zodiac — no equivalents
|
||||||
|
39: 17, # Wanderer XXXIX (Polestar) → The Star
|
||||||
|
40: 18, # Wanderer XL (Antichthon) → The Moon
|
||||||
|
41: 19, # Wanderer XLI (Corestar) → The Sun
|
||||||
|
# 42–49: Planets + The Binary — no equivalents
|
||||||
|
50: 20, # The Eagle → Judgement
|
||||||
|
51: 21, # Divine Calculus → The World
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 3. Minor suit map: Earthman (post-rename) → Fiorentine ───────────────────
|
||||||
|
|
||||||
|
MINOR_SUIT_MAP = {
|
||||||
|
"BRANDS": "WANDS",
|
||||||
|
"GRAILS": "CUPS",
|
||||||
|
"BLADES": "SWORDS",
|
||||||
|
"CROWNS": "PENTACLES",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def forward(apps, schema_editor):
|
||||||
|
TarotCard = apps.get_model("epic", "TarotCard")
|
||||||
|
DeckVariant = apps.get_model("epic", "DeckVariant")
|
||||||
|
|
||||||
|
try:
|
||||||
|
earthman = DeckVariant.objects.get(slug="earthman")
|
||||||
|
fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate")
|
||||||
|
except DeckVariant.DoesNotExist:
|
||||||
|
return # decks not seeded — nothing to do
|
||||||
|
|
||||||
|
# ── Step 1: rename Earthman suit codes + card names ───────────────────────
|
||||||
|
for old_suit, new_suit in SUIT_RENAMES.items():
|
||||||
|
old_display = old_suit.capitalize() # e.g. "Wands"
|
||||||
|
new_display = new_suit.capitalize() # e.g. "Brands"
|
||||||
|
cards = TarotCard.objects.filter(deck_variant=earthman, suit=old_suit)
|
||||||
|
for card in cards:
|
||||||
|
card.name = card.name.replace(f" of {old_display}", f" of {new_display}")
|
||||||
|
card.suit = new_suit
|
||||||
|
card.save()
|
||||||
|
|
||||||
|
# ── Step 2: copy major arcana keywords ───────────────────────────────────
|
||||||
|
fio_major = {
|
||||||
|
card.number: card
|
||||||
|
for card in TarotCard.objects.filter(deck_variant=fiorentine, arcana="MAJOR")
|
||||||
|
}
|
||||||
|
for em_num, fio_num in MAJOR_KEYWORD_MAP.items():
|
||||||
|
fio_card = fio_major.get(fio_num)
|
||||||
|
if not fio_card:
|
||||||
|
continue
|
||||||
|
TarotCard.objects.filter(
|
||||||
|
deck_variant=earthman, arcana="MAJOR", number=em_num
|
||||||
|
).update(
|
||||||
|
keywords_upright=fio_card.keywords_upright,
|
||||||
|
keywords_reversed=fio_card.keywords_reversed,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Step 3: copy minor/middle arcana keywords ─────────────────────────────
|
||||||
|
for em_suit, fio_suit in MINOR_SUIT_MAP.items():
|
||||||
|
fio_by_number = {
|
||||||
|
card.number: card
|
||||||
|
for card in TarotCard.objects.filter(deck_variant=fiorentine, suit=fio_suit)
|
||||||
|
}
|
||||||
|
for em_card in TarotCard.objects.filter(deck_variant=earthman, suit=em_suit):
|
||||||
|
fio_card = fio_by_number.get(em_card.number)
|
||||||
|
if fio_card:
|
||||||
|
em_card.keywords_upright = fio_card.keywords_upright
|
||||||
|
em_card.keywords_reversed = fio_card.keywords_reversed
|
||||||
|
em_card.save()
|
||||||
|
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
TarotCard = apps.get_model("epic", "TarotCard")
|
||||||
|
DeckVariant = apps.get_model("epic", "DeckVariant")
|
||||||
|
|
||||||
|
try:
|
||||||
|
earthman = DeckVariant.objects.get(slug="earthman")
|
||||||
|
except DeckVariant.DoesNotExist:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Reverse suit renames
|
||||||
|
reverse_renames = {new: old for old, new in SUIT_RENAMES.items()}
|
||||||
|
for new_suit, old_suit in reverse_renames.items():
|
||||||
|
new_display = new_suit.capitalize()
|
||||||
|
old_display = old_suit.capitalize()
|
||||||
|
cards = TarotCard.objects.filter(deck_variant=earthman, suit=new_suit)
|
||||||
|
for card in cards:
|
||||||
|
card.name = card.name.replace(f" of {new_display}", f" of {old_display}")
|
||||||
|
card.suit = old_suit
|
||||||
|
card.save()
|
||||||
|
|
||||||
|
# Clear all Earthman keywords
|
||||||
|
TarotCard.objects.filter(deck_variant=earthman).update(
|
||||||
|
keywords_upright=[],
|
||||||
|
keywords_reversed=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("epic", "0025_earthman_middle_arcana_and_major_icons"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse_code=reverse),
|
||||||
|
]
|
||||||
65
src/apps/epic/migrations/0027_tarotcard_cautions.py
Normal file
65
src/apps/epic/migrations/0027_tarotcard_cautions.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
Schema + data migration:
|
||||||
|
1. Add `cautions` JSONField (list, default=[]) to TarotCard.
|
||||||
|
2. Seed The Schizo (Earthman MAJOR #1) with 4 rival-interaction cautions.
|
||||||
|
All other cards default to [] — the UI shows a placeholder when empty.
|
||||||
|
"""
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
SCHIZO_CAUTIONS = [
|
||||||
|
'This card will reverse into <span class="card-ref">The Pervert</span> when it'
|
||||||
|
' comes under dominion of <span class="card-ref">The Occultist</span>, which in turn'
|
||||||
|
' reverses into <span class="card-ref">Pestilence</span>.',
|
||||||
|
|
||||||
|
'This card will reverse into <span class="card-ref">The Paranoiac</span> when it'
|
||||||
|
' comes under dominion of <span class="card-ref">The Despot</span>, which in turn'
|
||||||
|
' reverses into <span class="card-ref">War</span>.',
|
||||||
|
|
||||||
|
'This card will reverse into <span class="card-ref">The Neurotic</span> when it'
|
||||||
|
' comes under dominion of <span class="card-ref">The Capitalist</span>, which in turn'
|
||||||
|
' reverses into <span class="card-ref">Famine</span>.',
|
||||||
|
|
||||||
|
'This card will reverse into <span class="card-ref">The Suicidal</span> when it'
|
||||||
|
' comes under dominion of <span class="card-ref">The Fascist</span>, which in turn'
|
||||||
|
' reverses into <span class="card-ref">Death</span>.',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def seed_schizo_cautions(apps, schema_editor):
|
||||||
|
TarotCard = apps.get_model("epic", "TarotCard")
|
||||||
|
DeckVariant = apps.get_model("epic", "DeckVariant")
|
||||||
|
try:
|
||||||
|
earthman = DeckVariant.objects.get(slug="earthman")
|
||||||
|
except DeckVariant.DoesNotExist:
|
||||||
|
return
|
||||||
|
TarotCard.objects.filter(
|
||||||
|
deck_variant=earthman, arcana="MAJOR", number=1
|
||||||
|
).update(cautions=SCHIZO_CAUTIONS)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_schizo_cautions(apps, schema_editor):
|
||||||
|
TarotCard = apps.get_model("epic", "TarotCard")
|
||||||
|
DeckVariant = apps.get_model("epic", "DeckVariant")
|
||||||
|
try:
|
||||||
|
earthman = DeckVariant.objects.get(slug="earthman")
|
||||||
|
except DeckVariant.DoesNotExist:
|
||||||
|
return
|
||||||
|
TarotCard.objects.filter(
|
||||||
|
deck_variant=earthman, arcana="MAJOR", number=1
|
||||||
|
).update(cautions=[])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("epic", "0026_earthman_suit_renames_and_keywords"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="tarotcard",
|
||||||
|
name="cautions",
|
||||||
|
field=models.JSONField(default=list),
|
||||||
|
),
|
||||||
|
migrations.RunPython(seed_schizo_cautions, reverse_code=clear_schizo_cautions),
|
||||||
|
]
|
||||||
18
src/apps/epic/migrations/0028_alter_tarotcard_suit.py
Normal file
18
src/apps/epic/migrations/0028_alter_tarotcard_suit.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-04-07 03:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epic', '0027_tarotcard_cautions'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tarotcard',
|
||||||
|
name='suit',
|
||||||
|
field=models.CharField(blank=True, choices=[('WANDS', 'Wands'), ('CUPS', 'Cups'), ('SWORDS', 'Swords'), ('PENTACLES', 'Pentacles'), ('CROWNS', 'Crowns'), ('BRANDS', 'Brands'), ('GRAILS', 'Grails'), ('BLADES', 'Blades')], max_length=10, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
61
src/apps/epic/migrations/0029_fix_schizo_cautions.py
Normal file
61
src/apps/epic/migrations/0029_fix_schizo_cautions.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
Data fix: clear Schizo cautions from The Nomad (number=0) if present,
|
||||||
|
and ensure they land on The Schizo (number=1).
|
||||||
|
"""
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
SCHIZO_CAUTIONS = [
|
||||||
|
'This card will reverse into <span class="card-ref">I. The Pervert</span> when it'
|
||||||
|
' comes under dominion of <span class="card-ref">II. The Occultist</span>, which in turn'
|
||||||
|
' reverses into <span class="card-ref">II. Pestilence</span>.',
|
||||||
|
|
||||||
|
'This card will reverse into <span class="card-ref">I. The Paranoiac</span> when it'
|
||||||
|
' comes under dominion of <span class="card-ref">III. The Despot</span>, which in turn'
|
||||||
|
' reverses into <span class="card-ref">III. War</span>.',
|
||||||
|
|
||||||
|
'This card will reverse into <span class="card-ref">I. The Neurotic</span> when it'
|
||||||
|
' comes under dominion of <span class="card-ref">IV. The Capitalist</span>, which in turn'
|
||||||
|
' reverses into <span class="card-ref">IV. Famine</span>.',
|
||||||
|
|
||||||
|
'This card will reverse into <span class="card-ref">I. The Suicidal</span> when it'
|
||||||
|
' comes under dominion of <span class="card-ref">V. The Fascist</span>, which in turn'
|
||||||
|
' reverses into <span class="card-ref">V. Death</span>.',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def forward(apps, schema_editor):
|
||||||
|
TarotCard = apps.get_model("epic", "TarotCard")
|
||||||
|
DeckVariant = apps.get_model("epic", "DeckVariant")
|
||||||
|
try:
|
||||||
|
earthman = DeckVariant.objects.get(slug="earthman")
|
||||||
|
except DeckVariant.DoesNotExist:
|
||||||
|
return
|
||||||
|
TarotCard.objects.filter(
|
||||||
|
deck_variant=earthman, arcana="MAJOR", number=0
|
||||||
|
).update(cautions=[])
|
||||||
|
TarotCard.objects.filter(
|
||||||
|
deck_variant=earthman, arcana="MAJOR", number=1
|
||||||
|
).update(cautions=SCHIZO_CAUTIONS)
|
||||||
|
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
TarotCard = apps.get_model("epic", "TarotCard")
|
||||||
|
DeckVariant = apps.get_model("epic", "DeckVariant")
|
||||||
|
try:
|
||||||
|
earthman = DeckVariant.objects.get(slug="earthman")
|
||||||
|
except DeckVariant.DoesNotExist:
|
||||||
|
return
|
||||||
|
TarotCard.objects.filter(
|
||||||
|
deck_variant=earthman, arcana="MAJOR", number=1
|
||||||
|
).update(cautions=[])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("epic", "0028_alter_tarotcard_suit"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse_code=reverse),
|
||||||
|
]
|
||||||
23
src/apps/epic/migrations/0030_sigreservation_seat_fk.py
Normal file
23
src/apps/epic/migrations/0030_sigreservation_seat_fk.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epic', '0029_fix_schizo_cautions'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sigreservation',
|
||||||
|
name='seat',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name='sig_reservation',
|
||||||
|
to='epic.tableseat',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -216,13 +216,19 @@ class TarotCard(models.Model):
|
|||||||
CUPS = "CUPS"
|
CUPS = "CUPS"
|
||||||
SWORDS = "SWORDS"
|
SWORDS = "SWORDS"
|
||||||
PENTACLES = "PENTACLES" # Fiorentine 4th suit
|
PENTACLES = "PENTACLES" # Fiorentine 4th suit
|
||||||
CROWNS = "CROWNS" # Earthman 4th suit (renamed from Pentacles)
|
CROWNS = "CROWNS" # Earthman 4th suit
|
||||||
|
BRANDS = "BRANDS" # Earthman Wands
|
||||||
|
GRAILS = "GRAILS" # Earthman Cups
|
||||||
|
BLADES = "BLADES" # Earthman Swords
|
||||||
SUIT_CHOICES = [
|
SUIT_CHOICES = [
|
||||||
(WANDS, "Wands"),
|
(WANDS, "Wands"),
|
||||||
(CUPS, "Cups"),
|
(CUPS, "Cups"),
|
||||||
(SWORDS, "Swords"),
|
(SWORDS, "Swords"),
|
||||||
(PENTACLES, "Pentacles"),
|
(PENTACLES, "Pentacles"),
|
||||||
(CROWNS, "Crowns"),
|
(CROWNS, "Crowns"),
|
||||||
|
(BRANDS, "Brands"),
|
||||||
|
(GRAILS, "Grails"),
|
||||||
|
(BLADES, "Blades"),
|
||||||
]
|
]
|
||||||
|
|
||||||
deck_variant = models.ForeignKey(
|
deck_variant = models.ForeignKey(
|
||||||
@@ -239,6 +245,7 @@ class TarotCard(models.Model):
|
|||||||
group = models.CharField(max_length=100, blank=True) # Earthman major grouping
|
group = models.CharField(max_length=100, blank=True) # Earthman major grouping
|
||||||
keywords_upright = models.JSONField(default=list)
|
keywords_upright = models.JSONField(default=list)
|
||||||
keywords_reversed = models.JSONField(default=list)
|
keywords_reversed = models.JSONField(default=list)
|
||||||
|
cautions = models.JSONField(default=list)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["deck_variant", "arcana", "suit", "number"]
|
ordering = ["deck_variant", "arcana", "suit", "number"]
|
||||||
@@ -290,8 +297,16 @@ class TarotCard(models.Model):
|
|||||||
self.SWORDS: 'fa-gun',
|
self.SWORDS: 'fa-gun',
|
||||||
self.PENTACLES: 'fa-star',
|
self.PENTACLES: 'fa-star',
|
||||||
self.CROWNS: 'fa-crown',
|
self.CROWNS: 'fa-crown',
|
||||||
|
self.BRANDS: 'fa-wand-sparkles',
|
||||||
|
self.GRAILS: 'fa-trophy',
|
||||||
|
self.BLADES: 'fa-gun',
|
||||||
}.get(self.suit, '')
|
}.get(self.suit, '')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cautions_json(self):
|
||||||
|
import json
|
||||||
|
return json.dumps(self.cautions)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@@ -344,6 +359,10 @@ class SigReservation(models.Model):
|
|||||||
gamer = models.ForeignKey(
|
gamer = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='sig_reservations'
|
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='sig_reservations'
|
||||||
)
|
)
|
||||||
|
seat = models.ForeignKey(
|
||||||
|
'TableSeat', null=True, blank=True,
|
||||||
|
on_delete=models.SET_NULL, related_name='sig_reservation',
|
||||||
|
)
|
||||||
card = models.ForeignKey(
|
card = models.ForeignKey(
|
||||||
'TarotCard', on_delete=models.CASCADE, related_name='sig_reservations'
|
'TarotCard', on_delete=models.CASCADE, related_name='sig_reservations'
|
||||||
)
|
)
|
||||||
@@ -369,9 +388,9 @@ class SigReservation(models.Model):
|
|||||||
def sig_deck_cards(room):
|
def sig_deck_cards(room):
|
||||||
"""Return 36 TarotCard objects forming the Significator deck (18 unique × 2).
|
"""Return 36 TarotCard objects forming the Significator deck (18 unique × 2).
|
||||||
|
|
||||||
PC/BC pair → WANDS + CROWNS Middle Arcana court cards (11–14): 8 unique
|
PC/BC pair → BRANDS/WANDS + CROWNS Middle Arcana court cards (11–14): 8 unique
|
||||||
SC/AC pair → SWORDS + CUPS Middle Arcana court cards (11–14): 8 unique
|
SC/AC pair → BLADES/SWORDS + GRAILS/CUPS Middle Arcana court cards (11–14): 8 unique
|
||||||
NC/EC pair → MAJOR arcana numbers 0 and 1: 2 unique
|
NC/EC pair → MAJOR arcana numbers 0 and 1: 2 unique
|
||||||
Total: 18 unique × 2 (levity + gravity piles) = 36 cards.
|
Total: 18 unique × 2 (levity + gravity piles) = 36 cards.
|
||||||
"""
|
"""
|
||||||
deck_variant = room.owner.equipped_deck
|
deck_variant = room.owner.equipped_deck
|
||||||
@@ -380,13 +399,13 @@ def sig_deck_cards(room):
|
|||||||
wands_crowns = list(TarotCard.objects.filter(
|
wands_crowns = list(TarotCard.objects.filter(
|
||||||
deck_variant=deck_variant,
|
deck_variant=deck_variant,
|
||||||
arcana=TarotCard.MIDDLE,
|
arcana=TarotCard.MIDDLE,
|
||||||
suit__in=[TarotCard.WANDS, TarotCard.CROWNS],
|
suit__in=[TarotCard.WANDS, TarotCard.BRANDS, TarotCard.CROWNS],
|
||||||
number__in=[11, 12, 13, 14],
|
number__in=[11, 12, 13, 14],
|
||||||
))
|
))
|
||||||
swords_cups = list(TarotCard.objects.filter(
|
swords_cups = list(TarotCard.objects.filter(
|
||||||
deck_variant=deck_variant,
|
deck_variant=deck_variant,
|
||||||
arcana=TarotCard.MIDDLE,
|
arcana=TarotCard.MIDDLE,
|
||||||
suit__in=[TarotCard.SWORDS, TarotCard.CUPS],
|
suit__in=[TarotCard.SWORDS, TarotCard.BLADES, TarotCard.CUPS, TarotCard.GRAILS],
|
||||||
number__in=[11, 12, 13, 14],
|
number__in=[11, 12, 13, 14],
|
||||||
))
|
))
|
||||||
major = list(TarotCard.objects.filter(
|
major = list(TarotCard.objects.filter(
|
||||||
@@ -406,13 +425,13 @@ def _sig_unique_cards(room):
|
|||||||
wands_crowns = list(TarotCard.objects.filter(
|
wands_crowns = list(TarotCard.objects.filter(
|
||||||
deck_variant=deck_variant,
|
deck_variant=deck_variant,
|
||||||
arcana=TarotCard.MIDDLE,
|
arcana=TarotCard.MIDDLE,
|
||||||
suit__in=[TarotCard.WANDS, TarotCard.CROWNS],
|
suit__in=[TarotCard.WANDS, TarotCard.BRANDS, TarotCard.CROWNS],
|
||||||
number__in=[11, 12, 13, 14],
|
number__in=[11, 12, 13, 14],
|
||||||
))
|
))
|
||||||
swords_cups = list(TarotCard.objects.filter(
|
swords_cups = list(TarotCard.objects.filter(
|
||||||
deck_variant=deck_variant,
|
deck_variant=deck_variant,
|
||||||
arcana=TarotCard.MIDDLE,
|
arcana=TarotCard.MIDDLE,
|
||||||
suit__in=[TarotCard.SWORDS, TarotCard.CUPS],
|
suit__in=[TarotCard.SWORDS, TarotCard.BLADES, TarotCard.CUPS, TarotCard.GRAILS],
|
||||||
number__in=[11, 12, 13, 14],
|
number__in=[11, 12, 13, 14],
|
||||||
))
|
))
|
||||||
major = list(TarotCard.objects.filter(
|
major = list(TarotCard.objects.filter(
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #354a9c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #381507;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
stroke-width: 2.75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3, .cls-4 {
|
||||||
|
fill: none;
|
||||||
|
stroke: #381507;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #4f66d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
fill: #4258b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-7 {
|
||||||
|
fill: #3d180d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-8 {
|
||||||
|
fill: #3a1709;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
stroke-width: 2.2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path class="cls-5" d="M185.31,203.37l.12.54,1.16,5.4-2.22.49c-.08,8.17-.62,15.76-1.54,24.08-.26,2.33.18,7.31,3.04,7.58,4.78.44,9.33-1.88,14.1-2.1,1.59-.07,3.44-.05,4.69-.32l.98,9.54c.24,2.25,2.12,2.5,3.74,3.16,1.99.81,2.31,3.55,3.46,5.16l3.75,5.23c.62.86,1.45,1.66,2.51,1.36-.12,2.28.15,4.61.11,7.14l-.44,1.49c-1.47-.84-2.37,1.63-3.55,1.77l-16.08,1.97-21.66,2.39c4.99,1.47,9.61,1.45,14.55,2.03l23.5,2.78c.22,1.66.49.84,1.47.31,1.01,6.14,2.22,12.45,2.53,19.3,1.2,4.17.5,8.59-3.88,10.35l-3.91,2.35c.65.86,1.46,1.29,1.81,2.27,2.77,1.35,3.4,4.57,3.09,7.75,2.44,5.7,2.42,11.51,1.87,17.57l-.91,9.96c-.14,1.55-1.69,1.51-2.77,1.41l-17.57-1.6c-4.26-.39-8.71.46-12.16-2.84l-5.53-7.04c-.25.2-.35.47-.36.58,0-.11.11-.38.36-.58-.25.2-.35.47-.36.58-.28,3.07-.81,5.92-2.85,8.97l-21.21,1.95c-.12-1.76-.26-3.48-1.01-5.15l-4.42-9.91-3.75-25.3c-.14-.97-.63-1.8-1.07-2.16.43-1.73-.03-4.16-.58-6.18-1.9-4.98-3.59-9.83-4.83-15.35.18-.51-1.33-1.69-.39-2.35l1.62-1.16c1-.71-.63-1.33-.17-2.07.4-.66,1.75-.46,1.97-1.2.33-1.08-.35-2.18-1.64-2.16l-4.2.07-9.67-35.31c-.2-.75-.95-.91-1.34-1.05-.54-.19-1.5,0-1.53.93-.02.66-.64,1.72-1.09,2.78-1.2-1.34-4.74-2.35-5.46-.22l-12.57,37.25-7.39,1.96-1.8.57c-.58.18-.63,2.12,0,2.14,1.81.06,3.73-1.13,5.06.44-1.62.37-4.59.86-3.54,2.77.86,1.55,3.62.48,4.95.31l-4.78,15.62c-.24.78.03,1.76.57,2.34.4.43,1.33.72,2.04-.13,1.08-1.29,1.25-3.2,1.82-4.45.97-.38,2.22.37.73.97-.13,1.21-.23,2.19.18,3.14.23.53.84.92,2.01.61,2.71-6.4,4.7-12.72,6.8-19.73l18.11-2.9,5.96,21.86c.44,1.61,1.47,1.6,2.91,1.32,1.12-.22.51-1.69.51-2.99v-1.31c0-.37.34-.39,1.2-.31-1.04,1.12-.03,2.08.5,3.48.36.96,1.5,1.25,2.74,1.23l-.5,20.05c-.09,3.58-.56,6.95-.22,10.34l-21.98.14-2.92-9.49c2.68-4.69,5.77-9.41,3.04-13.61l-6.91,1.61c-1.59.37.86,2.08.68,2.8l-1.98,7.57c-.27,1.01.11,2.26-.46,3.03-1.37,1.88-6.3,1.61-9.49.15-1.3-2.98,1.24-7.91.16-13.45-.44-2.25-3.52-1.29-5.21-1.15l-8.08.67-6.35-5.57c.37-4.82-6.34-6.88-5.95-12.46.07-.96,1.36-1.31,1.96-2.42l5.08-9.34c.18-1.24-1.67-1.79-2.25-2.13l-3.45-1.97c-1-.57-1.54-1.46-1.61-2.3-.11-1.23,3.32-3.74,6.12-4.08l23.77-2.92c-4.84-1.4-9.6-.46-14.47-.77l-20.58-1.25-1.75-17.45c-.27-2.73-1.52-5.99.31-8.64,1.48-2.15,4.9-2.45,7.04-3.85,3.01-1.97-4.02-5.01-3.67-6.6,1.58-7.04-3.55-9.14-2.92-17.11l1.35-16.91c19.69,3.11,39.13,3.08,58.78.65,2.37-.29,3.87.88,5.27,2.25l.56.54-.56-.54-2.23,1.33c3.23,4.36,4.37,8.99,5.14,13.85l2.71,17.1,3.13,17.22c.82-.18.95-1.1.85-1.6-.94-4.55-.24-9.14-.6-13.84-.32-4.1-.56-7.97.06-12.11.92-6.26,1.04-12.4-.43-18.5-.18-.76-1.4-.95-1.7-.74.48-1.08.49-3.87,2.16-4.43l15.4-1.26,24.35-2.21ZM187.63,257.75c.4.93.52,1.9,1.14,2.91,2.36,3.88,4.35-8.06-7.31-13.58-7.5-3.55-15.29-1.54-21.1,4.83-13.04,14.31-17.28,39.69-4.52,53.8,5.7,6.31,14.78,8.03,22.8,5.87,13.32-3.6,20.27-20.17,13.32-16.11-.83,1.08-1.92.91-1.28-.45.23-1.19-.5-2.25-1.19-2.44-2.65-.75-3.54,8.1-13.46,10.47-5.92,1.42-11.86-1.51-14.55-6.83-4.2-8.32-4.34-17.68-1.05-26.64,3.45-9.39,10.35-17.68,18.54-13.94,4.41,2.01,4.83,8.81,5.68,8.88s1.2-.15,1.64-.32c.27-.1,1.75-2.67.35-6.47.34.07.6.1.97.03Z"/>
|
||||||
|
<path class="cls-6" d="M142.89,343.17l.29,2.88c.86.07,1.58.05,2.04.23.67.25,1.91,1.3,1.32,2.05-1.46,1.19-1.67,3.08-2.56,5.44l-38.54,2.82c-2.19.16-4,.22-5.07-1.87-.06.09-.12.09-.17.04l-31.82-30.7.34-.51,6.2,1.52c6.89,2.24,14.28,1.74,21.71,1.33,1.56-.09,1.37,2.99,1.43,3.85-.3,7.59-.1,14.62,2.15,21.47.05.41.41.65.83.61.39-.4.95-.17.83-.59l-.37-1.33.4-13.65c.01-.34.23-.76.19-.97-.05-.23.41-.49.79-.42,3.19,1.46,8.11,1.72,9.49-.15.56-.77.19-2.01.46-3.03l1.98-7.57c.19-.72-2.26-2.43-.68-2.8l6.91-1.61c2.74,4.2-.36,8.92-3.04,13.61l2.92,9.49,21.98-.14Z"/>
|
||||||
|
<path class="cls-6" d="M89.74,321.43c-1.43.12-3.07.88-4.9.94l-9.56.29c-.16,1.21.7,1.9-.37,2.41l-6.2-1.52-.57-.13-.57-.14c.06-.16.14-.32.15-.48l1.45-15.57,1.33-17.08,1.52-13.95,20.58,1.25c4.86.31,9.63-.63,14.47.77l-23.77,2.92c-2.8.34-6.23,2.85-6.12,4.08.07.84.61,1.73,1.61,2.3l3.45,1.97c.58.33,2.43.89,2.25,2.13l-5.08,9.34c-.6,1.11-1.89,1.46-1.96,2.42-.4,5.59,6.32,7.64,5.95,12.46l6.35,5.57Z"/>
|
||||||
|
<path class="cls-5" d="M185.31,203.37l.41.36,32.83,32.44c.15.15.23.32.17.52l-3.36-.08c-.18,0-.37-.17-.52.11-9.2-3.04-24.62.27-26.12-.81-2.61-1.89,1.62-13.42-2.11-26.59l-1.16-5.4-.12-.54Z"/>
|
||||||
|
<path class="cls-6" d="M219.09,263.48c-1.07.3-1.89-.5-2.51-1.36l-3.75-5.23c-1.15-1.61-1.47-4.34-3.46-5.16-1.62-.66-3.5-.91-3.74-3.16l-.98-9.54c2.56-.56,5.24-.75,8.24-.37l1.71-.29c.49-.08,1.13-1.43.74-1.77-.18,0-.37-.17-.52.11.15-.28.34-.11.52-.11l3.36.08c.06-.2-.01-.36-.17-.52.15.15.23.32.17.52,2.85,1.17,1.38,7.41,1.07,13.48l-.68,13.31Z"/>
|
||||||
|
<path class="cls-6" d="M137.02,209.09l5.09,4.92c1.03-.68.94-1.93,1.29-2.74.3-.21,1.51-.02,1.7.74,1.47,6.1,1.35,12.23.43,18.5-.61,4.14-.37,8.01-.06,12.11.37,4.7-.33,9.3.6,13.84.1.5-.03,1.42-.85,1.6l-3.13-17.22-2.71-17.1c-.77-4.87-1.91-9.49-5.14-13.85l2.23-1.33.56.54Z"/>
|
||||||
|
<path class="cls-1" d="M155.13,354.34l-6.55-5.25c-.5-.4-.58-1.25-1.2-1.09-.28.07-.6.17-.85.32.59-.75-.65-1.8-1.32-2.05-.47-.17-1.18-.15-2.04-.23l-.29-2.88c-.34-3.39.13-6.76.22-10.34l.5-20.05c.35-.26.61-.87,1.26-.95.44.36.93,1.18,1.07,2.16l3.75,25.3,4.42,9.91c.75,1.67.89,3.39,1.01,5.15Z"/>
|
||||||
|
<path class="cls-1" d="M218.76,272.12c.55,2.96-1.28,5.06-3.13,7.88-.92,1.4,1.16,2.13,1.36,3.36-.98.53-1.25,1.36-1.47-.31l-23.5-2.78c-4.93-.58-9.56-.56-14.55-2.03l21.66-2.39,16.08-1.97c1.17-.14,2.07-2.61,3.55-1.77Z"/>
|
||||||
|
<path class="cls-8" d="M144.88,311.82c-.66.08-.92.69-1.26.95-1.24.01-2.37-.28-2.74-1.23-.53-1.4-1.54-2.36-.5-3.48-.86-.08-1.19-.06-1.2.31v1.31c0,1.3.6,2.76-.52,2.99-1.43.29-2.47.29-2.91-1.32l-5.96-21.86-18.11,2.9c-2.1,7.01-4.09,13.33-6.8,19.73-1.17.31-1.78-.08-2.01-.61-.41-.95-.31-1.94-.18-3.14,1.5-.6.24-1.35-.73-.97-.56,1.24-.74,3.16-1.82,4.45-.71.85-1.64.57-2.04.13-.54-.58-.8-1.56-.57-2.34l4.78-15.62c-1.33.17-4.1,1.25-4.95-.31-1.05-1.92,1.92-2.4,3.54-2.77-1.34-1.57-3.26-.38-5.06-.44-.62-.02-.58-1.96,0-2.14l1.8-.57,7.39-1.96,12.57-37.25c.72-2.13,4.25-1.12,5.46.22.45-1.06,1.07-2.12,1.09-2.78.03-.94.98-1.12,1.53-.93.39.13,1.13.3,1.34,1.05l9.67,35.31,4.2-.07c1.29-.02,1.98,1.07,1.64,2.16-.23.73-1.57.54-1.97,1.2-.45.74,1.17,1.36.17,2.07l-1.62,1.16c-.93.67.57,1.85.39,2.35,1.24,5.52,2.93,10.37,4.83,15.35.55,2.02,1.01,4.45.58,6.18ZM122.28,263.01l-7.68,21.2,13.37-1.5-5.69-19.69Z"/>
|
||||||
|
<path class="cls-7" d="M187.63,257.75l-1.21-2.81c-.33-.78-.83-2.11-2.39-2.23l-.53-1.37c-.16-.41-.82-.21-1.64-.58-1.01-.56-1.29.55.06.58,2.16,2.07,3.86,4.01,4.73,6.38,1.4,3.8-.08,6.37-.35,6.47-.45.17-.81.39-1.64.32s-1.27-6.87-5.68-8.88c-8.19-3.73-15.09,4.55-18.54,13.94-3.29,8.97-3.16,18.32,1.05,26.64,2.69,5.33,8.63,8.25,14.55,6.83,9.92-2.37,10.81-11.22,13.46-10.47.69.19,1.42,1.25,1.19,2.44-.64,1.37.45,1.54,1.28.45,6.95-4.06,0,12.51-13.32,16.11-8.02,2.17-17.1.44-22.8-5.87-12.75-14.1-8.52-39.49,4.52-53.8,5.8-6.37,13.59-8.37,21.1-4.83,11.66,5.51,9.67,17.45,7.31,13.58-.61-1.01-.74-1.98-1.14-2.91ZM157.46,302.61l3.6,3.77c.64.67.95.15,1.82.3.52.09-.23-.61-.29-.93-.07-.41-1.13-.1-1.37-.35-1.17-1.26-1.91-3.37-3.77-2.78Z"/>
|
||||||
|
<path class="cls-1" d="M186.59,209.31c3.74,13.17-.49,24.7,2.11,26.59,1.5,1.08,16.92-2.22,26.12.81.15-.28.34-.11.52-.11.4.33-.25,1.68-.74,1.77l-1.71.29c-3-.38-5.68-.19-8.24.37-1.25.27-3.1.25-4.69.32-4.76.22-9.32,2.54-14.1,2.1-2.86-.26-3.3-5.24-3.04-7.58.92-8.32,1.46-15.9,1.54-24.08l2.22-.49Z"/>
|
||||||
|
<path class="cls-1" d="M102.87,335.37c-.38-.07-.84.19-.79.42.05.21-.18.63-.19.97l-.4,13.65.37,1.33c.12.42-.44.19-.83.59-.42.03-.78-.2-.83-.61-2.25-6.85-2.45-13.87-2.15-21.47-.06-.86.13-3.93-1.43-3.85-7.43.41-14.82.91-21.71-1.33,1.07-.51.21-1.2.37-2.41l9.56-.29c1.83-.06,3.48-.82,4.9-.94l8.08-.67c1.68-.14,4.77-1.1,5.21,1.15,1.08,5.55-1.46,10.47-.16,13.45Z"/>
|
||||||
|
<path class="cls-1" d="M187.63,257.75c-.37.07-.64.04-.97-.03-.88-2.38-2.57-4.31-4.73-6.38-1.35-.04-1.07-1.15-.06-.58.82.37,1.48.17,1.64.58l.53,1.37c1.56.12,2.06,1.45,2.39,2.23l1.21,2.81Z"/>
|
||||||
|
<polygon class="cls-5" points="122.28 263.01 127.97 282.71 114.6 284.21 122.28 263.01"/>
|
||||||
|
<path class="cls-1" d="M157.46,302.61c1.86-.59,2.59,1.52,3.77,2.78.23.25,1.3-.06,1.37.35.05.32.81,1.02.29.93-.87-.15-1.18.38-1.82-.3l-3.6-3.77Z"/>
|
||||||
|
<path class="cls-2" d="M210.46,277.3l6.02,1.81c1.38.43.78,2.5-.62,2.11,0,0-6.03-1.81-6.03-1.81-1.97-.86-4.17-1.86-6.07-2.91,1.33.16,5.03.6,6.7.81h0Z"/>
|
||||||
|
<path class="cls-4" d="M185.31,203.37l-24.35,2.21-15.4,1.26c-1.66.57-1.68,3.35-2.16,4.43-.36.81-.27,2.06-1.29,2.74l-5.09-4.92-.56-.54c-1.41-1.37-2.9-2.54-5.27-2.25-19.66,2.42-39.1,2.45-58.78-.65l-1.35,16.91c-.64,7.97,4.5,10.07,2.92,17.11-.36,1.59,6.68,4.62,3.67,6.6-2.14,1.4-5.56,1.7-7.04,3.85-1.83,2.66-.58,5.92-.31,8.64l1.75,17.45-1.52,13.95-1.33,17.08-1.45,15.57-.15.48"/>
|
||||||
|
<path class="cls-4" d="M185.32,203.34l.4.39,32.83,32.44c.15.15.23.32.17.52l-3.36-.08c-.18,0-.37-.17-.52.11-9.2-3.04-24.62.27-26.12-.81-2.61-1.89,1.62-13.42-2.11-26.59l-1.16-5.4-.11-.58Z"/>
|
||||||
|
<path class="cls-4" d="M213.54,317.63c2.77,1.35,3.4,4.57,3.09,7.75,2.44,5.7,2.42,11.51,1.87,17.57l-.91,9.96c-.14,1.55-1.69,1.51-2.77,1.41l-17.57-1.6c-4.26-.39-8.71.46-12.16-2.84l-5.53-7.04c-.25.2-.35.47-.36.58-.28,3.07-.81,5.92-2.85,8.97l-21.21,1.95-6.55-5.25c-.5-.4-.58-1.25-1.2-1.09-.28.07-.6.17-.85.32-1.46,1.19-1.67,3.08-2.56,5.44l-38.54,2.82c-2.19.16-4,.22-5.07-1.87-.62-.8-.65-1.96-.17-3.01-2.25-6.85-2.45-13.87-2.15-21.47-.06-.86.13-3.93-1.43-3.85-7.43.41-14.82.91-21.71-1.33l-6.2-1.52-.57-.13-.57-.14"/>
|
||||||
|
<polyline class="cls-3" points="100.19 354.76 68.38 324.06 67.57 323.28"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 9.8 KiB |
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #39170a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #6b1f65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #852f7e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #3d1a0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: none;
|
||||||
|
stroke: #381507;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-width: 2.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
fill: #9e3d96;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path class="cls-6" d="M134.56,198.73c-.18.4-.63,1.06-.86,1.01l-1.53-.32,2.73,6.11c3.16,4.21,3.45,9.15,4.36,14.26l5.43,30.42-.31-26.42.69-6.77-.39-15.14c.78.13,1.2.48,1.86.21.09,1.59-.38,3.25-.01,4.91,3.95,1.89,3.31,4.73,5.82,4.74,1.17,0,3.35.51,3.8-.95.88-2.85,3.04-3.98,3.96-8.34,2.01-.62,4.59.12,5.2,2.41l2.94,4.65.47,2.59c.11.63,3.07.5,5.91,4.7.74-2.1,2.63-2.9,4.18-3.47,2.1-.76,3.81.92,5.23,2.46l1.28.36-1.2,10.28c-.16,1.36.06,4.75,1.34,5.26l12.32-1.33c1.3-.14,2.73-.73,4.27-.46-.74,2.06,2.02,3.53,2.3,4.95.63,3.15.89,5.85,3.93,7.95,1.18.81,2.14,2.65,4.4,1.82l2.14,4.34c-1.97,3.85-2.69,7.52-1.18,11.55l-7.95,3.92-9.59,1.55-24.02,4.56,31.72.18c3.86-1.21,7.59-.06,10.11,3.3-3.28,1.87-2.97,4.67-2,7.19,3.17,8.23-.69,15.5-7.22,20.39-2.27,2.33-3.26,5.96-4.66,8.69l-14.97-1.08-1.98.67c-1.17.4-1.4,6.07-1.2,10.28l-5.97.64c.01,1.31.89,2.27,1.35,3.52l-3.55,5.37c-5.53-.62-8.28,7-13.89,10.9-1.98,1.37-3.73-.25-5.2-.67-1.79-.51-3.25-.83-4.12-2.65-1.17.63-2.28.31-3.09,0-.93-.36-1.45-1.42-1.58-2.63l-1.24-12.07-.54-21.41-3.37,26.93-2.32,11.58c-1.35-.6-2.56-.79-3.93-.64-2.87.32-5.45-.38-8.31-.41-1.55-.02-1.88-1.53-3.11-3.07l2.02-2.29c-1.1-2.21-3.48-2.22-5.28-3.2-.75-.41-1.27-1.25-2.14-1.2-2.2.13.67,6.44-2.89,6.37l-6.18-.11c-.97-.02-1.52-.97-1.46-1.38l.36-2.39c-2.16-3.31-6.64-4.97-10.34-6.05l-4.61-1.35,9.71-11.1c1.03-1.17,2.56-2.27,2.2-3.91l-10.97,8.35c-4.57-1.25-8.28-3.55-8.21-8.02-.63-.81-1.9-1.86-1.64-2.9.76-2.95,3.93-2.39,4-6.01.03-1.5-1.05-2.01-2.21-3.07-1.5-1.37-2.59-3.79-4.86-4.28-1.89-.41-6.44-4.24-7.02-6.09-1.01-3.25,2.85-5.63.92-8.2l8.46-2.25,18.21-3.46.55,22.34-4.07-1.09c-.62-.17-1.15.71-1.26,1.11-.71,2.66,6.57,4.49,6.46,4.96-.06.26-.4.99-.55.77-.26-.37-.68-.81-1.29-.89l-2.57-.33c-.63-.08-1.82,2.04-.78,2.4,13.51,4.72,30.52,4.52,40.19-6.04,4.26-4.65,7-10.96,5.44-17.11-1.88-7.43-8.07-12.69-15.99-13.65,4.61-3.97,8.07-9.52,6.06-15.48s-7.82-9.61-13.78-10.33c-13.8-1.66-26.43,7.86-23.9,11.48l1.82.27c.83.77.9,1.54,1.58,1.28l2.21-.86-.14,17.59-10.69.46-11.02-.53c-3.1-.15-5.69-2.07-8.52-1.76l-2.44-19.92c-1.31-2.93-.76-6.46,2.46-7.96,3.42-1.59,4.59-3.37,5.99-2.92.03-2.33-2.37-3.38-4.36-4.69l-.32-6.53c-5.46-9.7-.92-17.59-1.51-28.43l16.23,2.03,23.56-.02,16.48-1.23c2.97-.22,5.24-1.39,7.61.8ZM185.54,293.84c2.42-3.82,5.39-7.88,3.02-9.23s-3.38,6.71-11.7,9.93c-4.18,1.62-8.99.92-12.68-1.81-3.38-2.5-5.13-6.55-6.15-11.09-3.58-15.97,6.89-36.59,18.87-32.85,6.82,2.13,4.36,12.37,8.6,8.12.92-1.52.02-3.6.33-5.64l2.51,3.38c1.54-.91,1.02-2.87.63-4.33-1.61-6.12-7.34-9.87-13.12-11.04-6.3-1.28-11.96,1.46-16.42,6.15-6.23,6.56-9.49,15.21-11.29,23.96-2.95,14.33,3.3,32.61,19.18,34.63,11.19,1.43,22.11-4.68,26.13-15.27.19-.49.04-1.09.04-1.41,0-.73-2.81-.21-2.68-.3-1.54,1.13-1.96,6.72-5.28,6.8Z"/>
|
||||||
|
<path class="cls-3" d="M74.46,278.72c1.93,2.57-1.93,4.94-.92,8.2.57,1.85,5.13,5.69,7.02,6.09,2.27.49,3.37,2.91,4.86,4.28,1.15,1.06,2.24,1.57,2.21,3.07-.07,3.62-3.24,3.06-4,6.01-.26,1.03,1.01,2.08,1.64,2.9-.07,4.47,3.65,6.77,8.21,8.02l10.97-8.35c.36,1.64-1.18,2.73-2.2,3.91l-9.71,11.1,4.61,1.35c3.7,1.08,8.18,2.75,10.34,6.05l-.36,2.39c-.06.4.49,1.36,1.46,1.38l6.18.11c3.56.07.69-6.24,2.89-6.37.86-.05,1.39.79,2.14,1.2,1.8.99,4.18.99,5.28,3.2l-2.02,2.29c1.23,1.55,1.56,3.06,3.11,3.07,2.87.03,5.44.73,8.31.41,1.37-.15,2.57.04,3.93.64l2.32-11.58,3.37-26.93.54,21.41,1.24,12.07c.12,1.21.65,2.27,1.58,2.63.81.32,1.92.64,3.09,0,.86,1.82,2.33,2.14,4.12,2.65,1.47.42,3.22,2.04,5.2.67,5.6-3.9,8.36-11.52,13.89-10.9l3.55-5.37c-.47-1.25-1.34-2.21-1.35-3.52l5.97-.64.85,18.06-.17,3.08c.67-.03,1.46-.09,2.22.11l-1.57,4.51-.4,1.16-.74-.15-9.48-.44-13.17-.04c-5.51-.02-10.81.2-15.91-2.37-4.17,3.3-8.82,3.85-13.75,4.04l-11.57.44c-4.22.16-8-1.47-12.1-2.44l-9.23-2.18-2.37-2.53-1-1.14c-.19-.22-.42.16-1.49,0l-2.88,3.29c-1.64,3.38-4.27,3.48-7.57,2.93l-8.76-1.47c-1.06-.18-1.68-1.56-2.63-2.24l-.25-10.04c-.09-3.55-2.33-6.92-2.12-10.04l.33-4.87,1.13-9.81c.44-3.77.39-7.85,0-11.53l-.45-4.37c-.88-2.66-.01-5.19,1.15-7.52l3.1-6.18c.48,0,.8.42,1.38.27Z"/>
|
||||||
|
<path class="cls-3" d="M219.05,227.87l.79.78-.21.25.6.14-.13.88-1.61,30.29-6.75,7.18,6.67,3.15,1.66,35.78c.09,1.99,0,4.05-1.49,5-.19.12-.4.28-.59.29l.1.11-.1-.11-3.17.11c-.29-.45-.37-1.65-1.08-1.62l-5.37.21c-2.72.1-5.69.2-8.32.01,1.39-2.73,2.38-6.36,4.66-8.69,6.53-4.89,10.39-12.17,7.22-20.39-.97-2.52-1.28-5.32,2-7.19-2.53-3.36-6.25-4.51-10.11-3.3l-31.72-.18,24.02-4.56,9.59-1.55,7.95-3.92c-1.51-4.02-.8-7.7,1.18-11.55l-2.14-4.34c-2.26.84-3.21-1-4.4-1.82-3.04-2.1-3.3-4.8-3.93-7.95-.28-1.42-3.04-2.89-2.3-4.95l3.85-.57,8.82.23c.74-.32,1.1.42,1.09-.02-.02-.58.56-1.21.18-1.42l2.43.46.62-.74Z"/>
|
||||||
|
<path class="cls-3" d="M188.5,197.92c-.14-.14-.26-.35-.42-.36.16.02.28.22.42.36l-.8,2.07.09,1.09-1.96.21c.42,5.17.06,10.03-.5,14.87l-1.28-.36c-1.42-1.53-3.13-3.22-5.23-2.46-1.55.57-3.44,1.37-4.18,3.47-2.84-4.2-5.79-4.07-5.91-4.7l-.47-2.59-2.94-4.65c-.61-2.29-3.19-3.03-5.2-2.41-.92,4.36-3.08,5.48-3.96,8.34-.45,1.46-2.64.96-3.8.95-2.51,0-1.87-2.85-5.82-4.74-.37-1.66.1-3.32.01-4.91-.66.27-1.08-.08-1.86-.21l.39,15.14-.69,6.77.31,26.42-5.43-30.42c-.91-5.11-1.2-10.05-4.36-14.26l-2.73-6.11,1.53.32c.22.05.68-.6.86-1.01,2.07,1.91,4.3,4.1,6.23,6.93l3.69-7.11,37.01-2.68c2.54-1.25,5.05-.41,6.58,1.69.16.02.28.22.42.36Z"/>
|
||||||
|
<path class="cls-6" d="M217.99,311.6l-.39.42-33.59,33.88-.77.03,1.57-4.51c2.75-7.94,3.11-16.57,1.63-25.15-.21-1.2,1.02-2.43,2.16-2.43l11.01.06c5.33.03,10.37-.9,15.23-2.19l3.17-.11Z"/>
|
||||||
|
<path class="cls-3" d="M219.05,227.87l-.62.74-2.43-.46c-8.06-2.53-16.09-3.31-24.73-2.08-1.96.28-2.28-4.19-1.56-5.38.71-6.92-.38-13.29-1.92-19.6l-.09-1.09.8-2.07c-.14-.14-.26-.35-.42-.36.16.02.28.22.42.36l30.55,29.94Z"/>
|
||||||
|
<path class="cls-2" d="M101.08,269.43l.04,3.58-18.21,3.46-8.46,2.25c-.58.15-.9-.27-1.38-.27l2.44-3.67c-.82-2.73-4.33-4.52-4.66-7.18,2.83-.32,5.42,1.61,8.52,1.76l11.02.53,10.69-.46Z"/>
|
||||||
|
<path class="cls-1" d="M101.08,269.43l.14-17.59-2.21.86c-.67.26-.75-.52-1.58-1.28l-1.82-.27c-2.52-3.62,10.1-13.15,23.9-11.48,5.96.72,11.76,4.34,13.78,10.33s-1.45,11.51-6.06,15.48c7.92.96,14.11,6.22,15.99,13.65,1.56,6.15-1.18,12.46-5.44,17.11-9.67,10.56-26.68,10.76-40.19,6.04-1.04-.36.15-2.48.78-2.4l2.57.33c.61.08,1.03.52,1.29.89.15.22.49-.51.55-.77.11-.47-7.17-2.29-6.46-4.96.11-.41.64-1.28,1.26-1.11l4.07,1.09-.55-22.34-.04-3.58ZM124.8,255.68c1.58-3.1.42-5.63-2.19-7.08-3.93-2.19-8.3-2.63-13.25-1.34v16.49c0,.62.35,1.06.86,1.37.26.15.45-.22,1.12-.47,5.49-1.09,10.87-3.89,13.45-8.97ZM106.69,248.96l-1.41-.03.17,8.81c.23-.03.44-.47.56-.3l.68-8.48ZM129.95,257.42c.65-1.58.8-4.23,1.16-5.86-1.76-.16-.51-1.16-.52-1.51,0-.22-.65-.01-1.42-.09,1.09,3.53.8,7.12-1.99,10.11,1.03.81,1.77-.21,1.99-.75l.77-1.9ZM133.96,284.81c.89-6.65-3.22-11.63-9.46-12.87-5.07-1.01-9.78.28-14.97,1.72l.7,23.53c5.81,1,11.07-.12,16.57-2.3,3.82-1.51,6.61-6.02,7.16-10.08ZM105.85,278.54c-.96,2.12-1.03,4.41.28,6.27.22-1.99.88-3.84-.28-6.27ZM138.89,279.18h-1.19s.2,6.6.2,6.6c.2-.03.41-.45.51-.3.97-1.95.1-4,.47-6.3Z"/>
|
||||||
|
<path class="cls-4" d="M185.54,293.84c3.32-.08,3.73-5.67,5.28-6.8-.13.1,2.68-.43,2.68.3,0,.32.14.92-.04,1.41-4.02,10.58-14.94,16.69-26.13,15.27-15.88-2.03-22.13-20.3-19.18-34.63,1.8-8.74,5.06-17.4,11.29-23.96,4.45-4.69,10.12-7.43,16.42-6.15,5.78,1.17,11.5,4.93,13.12,11.04.38,1.46.9,3.42-.63,4.33l-2.51-3.38c-.3,2.04.6,4.12-.33,5.64-4.24,4.25-1.78-5.98-8.6-8.12-11.98-3.75-22.45,16.87-18.87,32.85,1.02,4.54,2.77,8.58,6.15,11.09,3.68,2.73,8.49,3.43,12.68,1.81,8.32-3.22,9.32-11.28,11.7-9.93s-.59,5.41-3.02,9.23Z"/>
|
||||||
|
<path class="cls-2" d="M187.78,201.08c1.54,6.31,2.64,12.68,1.92,19.6-.72,1.2-.4,5.66,1.56,5.38,8.64-1.23,16.67-.45,24.73,2.08.38.2-.2.83-.18,1.42.02.44-.35-.31-1.09.02l-8.82-.23-3.85.57c-1.54-.27-2.98.32-4.27.46l-12.32,1.33c-1.29-.51-1.5-3.9-1.34-5.26l1.2-10.28c.57-4.84.93-9.7.5-14.87l1.96-.21Z"/>
|
||||||
|
<path class="cls-2" d="M200.05,310.31c2.64.19,5.6.09,8.32-.01l5.37-.21c.71-.03.79,1.17,1.08,1.62-4.86,1.29-9.9,2.22-15.23,2.19l-11.01-.06c-1.13,0-2.36,1.22-2.16,2.43,1.48,8.57,1.13,17.21-1.63,25.15-.76-.2-1.55-.15-2.22-.11l.17-3.08-.85-18.06c-.2-4.21.03-9.88,1.2-10.28l1.98-.67,14.97,1.08Z"/>
|
||||||
|
<path class="cls-6" d="M133.96,284.81c-.55,4.07-3.34,8.57-7.16,10.08-5.5,2.17-10.76,3.29-16.57,2.3l-.7-23.53c5.2-1.44,9.9-2.72,14.97-1.72,6.24,1.24,10.35,6.22,9.46,12.87Z"/>
|
||||||
|
<path class="cls-6" d="M124.8,255.68c-2.58,5.08-7.97,7.88-13.45,8.97-.67.25-.86.62-1.12.47-.51-.3-.87-.74-.87-1.37v-16.49c4.96-1.3,9.32-.85,13.25,1.34,2.61,1.45,3.76,3.98,2.19,7.08Z"/>
|
||||||
|
<path class="cls-2" d="M129.95,257.42l-.77,1.9c-.22.54-.96,1.56-1.99.75,2.79-3,3.08-6.58,1.99-10.11.77.08,1.42-.13,1.42.09,0,.36-1.24,1.36.52,1.51-.36,1.63-.51,4.27-1.16,5.86Z"/>
|
||||||
|
<path class="cls-6" d="M106.69,248.96l-.68,8.48c-.13-.17-.33.27-.56.3l-.17-8.81,1.41.03Z"/>
|
||||||
|
<path class="cls-2" d="M138.89,279.18c-.37,2.3.5,4.35-.47,6.3-.1-.15-.32.27-.51.3l-.2-6.6h1.19Z"/>
|
||||||
|
<path class="cls-2" d="M105.85,278.54c1.16,2.43.51,4.28.28,6.27-1.31-1.86-1.24-4.15-.28-6.27Z"/>
|
||||||
|
<path class="cls-5" d="M220.24,229.03l-.6-.14-1.21-.28-2.43-.46c-8.06-2.53-16.09-3.31-24.73-2.08-1.96.28-2.28-4.19-1.56-5.38.71-6.92-.38-13.29-1.92-19.6l-.09-1.09"/>
|
||||||
|
<path class="cls-5" d="M76.87,236.81c.03-2.33-2.37-3.38-4.36-4.69l-.32-6.53c-5.46-9.7-.92-17.59-1.51-28.43l16.23,2.03,23.56-.02,16.48-1.23c2.97-.22,5.24-1.39,7.61.8,2.07,1.91,4.3,4.1,6.23,6.93l3.69-7.11,37.01-2.68c2.54-1.25,5.05-.41,6.58,1.69.16.02.28.22.42.36l30.55,29.94.79.78.4.39"/>
|
||||||
|
<path class="cls-5" d="M220.24,229.03l-.13.88-1.61,30.29-6.75,7.18,6.67,3.15,1.66,35.78c.09,1.99,0,4.05-1.49,5l-.49.39-.5.31-33.59,33.88-1.17,1.19"/>
|
||||||
|
<path class="cls-5" d="M182.84,347.08l-.74-.15-9.48-.44-13.17-.04c-5.51-.02-10.81.2-15.91-2.37-4.17,3.3-8.82,3.85-13.75,4.04l-11.57.44c-4.22.16-8-1.47-12.1-2.44l-9.23-2.18-2.37-2.53"/>
|
||||||
|
<path class="cls-5" d="M182.84,347.08l.4-1.16,1.57-4.51c2.75-7.94,3.11-16.57,1.63-25.15-.21-1.2,1.02-2.43,2.16-2.43l11.01.06c5.33.03,10.37-.9,15.23-2.19l3.17-.11"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #006d30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #00873e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: none;
|
||||||
|
stroke: #381507;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-width: 2.75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #3a160a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #3d180d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
fill: #00a04b;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path class="cls-6" d="M153.66,195.96l-.46-.48-2.25.71,3.07,18.95.16,18.83c.02,1.82-1.11,3.58.04,5.52.86-1.54,1.21-3.1,1.51-4.81l4.63-26.09c.51-2.89.63-6.32-1.68-7.91,3.44-.33,1.92-4.68,4.63-6.02,1.06-.53,2.58-.34,3.85-.57l.57-.11.09,1.97c-1.23.02-2.43-.35-3.03.56,1.72-.21,4.69,1.64,3.78,3.07-1.78,2.8-2.55,5.21-1.44,8.53.2.59-.73,1.95-.05,2.92.42.6,1.37.66,2.45,1.35l.89,1.5c.22.37,1.97.27,1.95-.45-.06-2.39-4.42-4.98-2.14-5.98.89-.39,1.59-.33,2.08.47l3.78,6.21,3.2-1.66,6.99.65c-.45,5.85-2.86,9.72.48,15.56l8.02-1.38c4.46-.77,3.87,5.08,6.83,6.44-.37,2.19.28,4.05,1.64,6.45-6.86,3.73-8.55.88-9.78,1.54-2.84,1.52-2.36,6.78,0,8.07,2.28-5.08,5.7-2.99,8.83-5.88l3.1,3.95c.13.5-.52.62-.77.88-.49.54-1.76-.53-2.23-.28-1.18.63-.37,1.39-.08,1.77.39.5,2.4.95,4.35,1.17.61,1.74-1.58,2.73-2.4,3.52.4,2.18,3.63,1.16,5.22,1.12,1.87-.05,3.1,2.33,5.08,1.26l-.3,8.25c-7.48,2.34-14.03,3.49-21.14,4.16l-23.27,2.2c.09.33.06.66.03.83.7.61,1.85-.06,2.86-.16l30.5,2.09c2.57.18,5.12.05,7.54,1.41-6.47,3.24-11.29,0-12.25.87-.59.54-1.48,2.27-.13,2.72.91.3,1.87.35,2.3.69,1.2.95-1.2,5.83,1.12,6.74.78.3,2.4-.64,2.93.56.31.71.23,2.04-.94,2.69l-1.39,1.83c-1.39.83-1.28,1.3-1.07,2.69.24,1.57,1.51,1.36,3.53.89v3.28s4.12.29,4.12.29l.1,2.95c-4.9.84-9.59,2.46-12.77,6.65l-8.81-1.19c-7.44-1-2.19,11.29-2.46,19.21l-4.89-.16c-2.63-.09-6.35,0-7.58-2.4,1.86-2.75-1.3-5.62-.12-9.66-1.36-.25-2.17-1.48-3.52-1.43-2.58,1.83-5.92,2.68-6.55,5.79-.14.7,1.35,1.6.78,2.39-.88,1.22-5.4.29-5.27,3.28.14,3.32,3.47,1.65,4.91,3.68-1.55,1.93-3.69,4.09-5.51,5.41-.67.04-2.1-.08-2.75-.12l-7.79-.48-1.91-10.68-.87-20.54c-.02-.38.98-.89.38-1.07-.21-.06-.37-.3-.58-.52-.15-.16-.64.62-.67.95l-1.71,16.74c-.72,7.06-2.02,13.71-4.34,20.67-4.06.6-8.21-.87-12.16-2.67-.9-.41-2.41-.75-2.22-2.2.46-.15,1.4-.34,1.49-.82.06-.36.32-1.32-.43-1.33l-9.76-.09c-.43,0-.7-1.15-.36-1.47.61-.58,1.76-.61,2.03-1.46.31-.96.25-1.75.13-3.16l-14.56.05c-3.39.01-6.59.16-10.04-.64l12.59-14.11c-.78-.51-1.89.32-2.63,1.01-4.11,3.83-8.08,7.52-13.71,8.92.38-1.11,1.48-1.61,2.04-2.49l-1.39-4.29c-2.64.06-2.8-2.85-3.9-3.74-2.66-2.14-5.75-3.87-5.86-7.47-.13-4.69,5.58-4.68,4.87-7.46-1.89-.09-2.79-.22-3.53-1.67-2.76-5.4-5.55-10.74-6.13-16.95l.26-3.84c1.78-1.26,3.89-1.22,6.04-1.61l19.34-3.5c-7.89-1.44-15.42-1.63-22.88-4.1l-3.59-.27c-1.87-2.75-1.64-5.92-2.07-9l-1.2-8.62-.43-6.7c-.53-8.33-.99-6.87,3.53-10.65l3.35-2.87.41.38-.41-.38-6.25-5.77,1.99-24.45,29.01,2.92c.3,2.58-.26,4.92.22,7,.52-.09,1.76-.17,2.02.58.15.44.4.77.33,1.31l3.48,3.71c1.73,1.85,4.57,3.61,5.86,5.97,1.68,3.06,3.62,8.54,4.74,8.09.28-.11.94-.29,1.08-1.02.16-.85-.78-2.14-.85-3.37l-.49-9.68c-1.06-2.47-3.15-4.98-2.29-7.94l7.42-2.99.74-.79c.25-.26.12-.53-.1-.59-.25-.07-.86-.46-1.49.09l-.35-1.99,28.3-2.23c1.58-.12,3.1-.91,3.9,1l.46.48ZM186.99,284.59c.05-.8-.21-2.93-.89-3.46-1.03-.8-2.09-.08-2.67.96-2.92,5.19-8.02,9.43-14.12,9.6-10.92.3-16.23-12.43-14.77-24.11,1.5-12,9.42-26.8,19.63-23.04,6.4,2.36,4.16,10.81,8.14,8.64,1.03-.56,1.08-3.75.59-6.65,1.56,1.07,1.13,2.55,1.92,3.73.35.51,1.07.12,1.41-.26,1.54-1.69-1.42-11.67-11.3-14.47-18.44-5.21-31.37,21.26-30.31,39.87.46,8.02,4.55,18.71,12.73,22.74,12.45,6.13,27.46.08,33.05-12.33.29-.63.45-1.54.17-1.99s-.96-1.1-1.65-.81c-.59.25-1.21.83-1.92,1.58ZM101.07,245.27c-.97-.28-1.36,2.51-.27,3.04.84.41,1.82.3,3.43.02l-.02,15.84.52,28.42c.02,1.26-3.05.8-3.99,2.17-.37.54-.43,1.53.14,1.9.48.31,1.1.34,2.1.38l1.74.07c.21,0-.06,1.21-.78.9-1.53-.66-2.37,1.04-2.06,2.1.54,1.85,3.63,1.46,6.98.54.54.72.85,2.18,1.64,2.22.74.04,1.45-.09,1.73-.57.43-.71.32-1.49.26-2.52l13.46-1.99,15.07-1.37c.5-.05,1.21-.93,1.18-1.32-.02-.33-.33-.94-.83-.93l-6.64.11v-.92s7.21-.58,7.21-.58c.67-.05,1.07-1,1.09-1.41s-.74-1.12-1.09-1.32l-2.18-1.26c-7.01-.15-13.57.87-20.29,1.77l-6.33.85-.28-18.72c1.4-.01,2.1-.11,2.8-.21l14.99-2.18c1.84-.27,2.77-2.22,2.67-4.08-1.51-.62-2.4-.02-3.68-.25.35-1.33,1.22-1.8.93-2.28-.46-.75-.97-1.35-1.81-1.22l-16.69,2.55v-17.6c8.76-1.96,17.15-2.99,25.82-3.25.13,0,3.38-2.78,2.38-4.14-.56-.77-2.05-.63-3.18-.4-.1-1.28.99-1.47.68-1.91-.6-.86-1.04-1.38-1.97-1.52-8.39.05-16.34,1.7-24.78,2.69-.47-.45-.8-1.64-1.29-1.61-.68.04-.97.86-1.59,1.72-.59-.64-1.13-1.26-1.75-1.19-.85.11-1.69,0-1.57,1.15.13,1.23-2,1.2-2.96,1.52l-2.84.93c-1.36.44-1.02,1.83-.51,2.86l5.73-.22-1.66.49c-.18.57-.19,1.1-1.51.72Z"/>
|
||||||
|
<path class="cls-2" d="M189.09,192.96c.05.24.06.17.06-.01l.12.59,1.48,13.12.05,15.42c9.75-1.12,18.49.18,27.17,2.92l.66-.85,1.15,1.22.02.65-.12,6.48c-.06,3.28-.1,6.15-.34,9.4l-1.49,20.28c-.14,1.92.53,4.27-.65,6.12l-5.41,3.97,6.02,6.36,1.49,21.56c.37,5.41,2.59,8.94-3.33,12.4.34.27.29,1.3.03,1.53s-.16.34-.97.66c-.37,1.66-2.22,1.95-3.2,2.95l-20.55,20.92c-1.09,1.11-1.62,2.3-2.99,2.91-1.44.64-2.89,1.38-4.34,2.88l-11.83-.23-21.18-.57c-2.81-.08-5.69-.28-7.16-3.49l-2.05-.16c-3.58,7.25-10.93,5.75-18.03,6.21-3.56.23-7.24,1.2-10.75.28l-13.96-3.64c-3.32-.87-4.74-4.5-6.28-4.44-1.22.05-1.49,1.13-2.18,1.92l-3.06,3.53c-1.81,2.08-4.75,1.11-7.01.74l-7.91-1.31c-.88-.14-1.36-1.64-2.36-2.17.88-6.23-.67-11.82-2.56-17.57-.32-.99.84-5.27,1.01-8.28l.29-4.88.53-6.71.52-27.67,1.82-3.79c.68.02,1.42.1,2.03.75l-.26,3.84c.58,6.2,3.37,11.55,6.13,16.95.74,1.45,1.64,1.58,3.53,1.67.71,2.78-5.01,2.77-4.87,7.46.1,3.59,3.19,5.33,5.86,7.47,1.1.88,1.26,3.8,3.9,3.74l1.39,4.29c-.55.88-1.66,1.38-2.04,2.49,5.64-1.4,9.6-5.08,13.71-8.92.74-.69,1.84-1.52,2.63-1.01l-12.59,14.11c3.44.8,6.65.65,10.04.64l14.56-.05c.11,1.41.17,2.2-.13,3.16-.27.85-1.42.88-2.03,1.46-.33.32-.07,1.46.36,1.47l9.76.09c.76,0,.5.97.43,1.33-.08.47-1.03.66-1.49.82-.2,1.44,1.31,1.79,2.22,2.2,3.95,1.79,8.1,3.27,12.16,2.67,2.32-6.96,3.62-13.61,4.34-20.67l1.71-16.74c.03-.33.52-1.11.67-.95.21.23.36.46.58.52.6.17-.4.69-.38,1.07l.87,20.54,1.91,10.68,7.79.48c.64.04,2.08.16,2.75.12,1.82-1.32,3.96-3.47,5.51-5.41-1.45-2.03-4.77-.36-4.91-3.68-.13-2.98,4.38-2.06,5.27-3.28.57-.79-.92-1.69-.78-2.39.63-3.11,3.98-3.96,6.55-5.79,1.36-.05,2.16,1.19,3.52,1.43-1.18,4.04,1.98,6.9.12,9.66,1.23,2.39,4.95,2.31,7.58,2.4l4.89.16c.28-7.91-4.98-20.21,2.46-19.21l8.81,1.19c3.18-4.19,7.87-5.81,12.77-6.65l-.1-2.95-4.12-.29v-3.28c-2.02.46-3.28.68-3.52-.89-.21-1.38-.32-1.86,1.07-2.69l1.39-1.83c1.17-.65,1.25-1.97.94-2.69-.53-1.2-2.15-.26-2.93-.56-2.32-.9.09-5.78-1.12-6.74-.43-.34-1.39-.39-2.3-.69-1.35-.45-.46-2.18.13-2.72.95-.87,5.78,2.37,12.25-.87-2.42-1.36-4.97-1.23-7.54-1.41l-30.5-2.09c-1,.11-2.15.77-2.86.16.03-.17.07-.51-.03-.83l23.27-2.2c7.11-.67,13.66-1.83,21.14-4.16l.3-8.25c-1.98,1.07-3.21-1.31-5.08-1.26-1.6.04-4.83,1.06-5.22-1.12.82-.79,3.01-1.78,2.4-3.52-1.94-.22-3.96-.67-4.35-1.17-.3-.39-1.1-1.14.08-1.77.47-.25,1.75.82,2.23.28.24-.27.9-.39.77-.88l-3.1-3.95c-3.13,2.9-6.55.8-8.83,5.88-2.36-1.3-2.84-6.55,0-8.07,1.24-.66,2.93,2.18,9.78-1.54-1.36-2.4-2.01-4.25-1.64-6.45-2.96-1.36-2.37-7.21-6.83-6.44l-8.02,1.38c-3.34-5.83-.94-9.71-.48-15.56l-6.99-.65-3.2,1.66-3.78-6.21c-.48-.79-1.18-.86-2.08-.47-2.29,1,2.08,3.59,2.14,5.98.02.72-1.73.82-1.95.45l-.89-1.5c-1.07-.69-2.02-.75-2.45-1.35-.68-.97.25-2.32.05-2.92-1.11-3.33-.34-5.73,1.44-8.53.91-1.43-2.05-3.28-3.78-3.07.6-.91,1.8-.54,3.03-.56l-.09-1.97-.57.11.57-.11,13.46-1.04c2.81-.22,5.44-.4,7.96,0,0,.25,0,.18-.06.01Z"/>
|
||||||
|
<path class="cls-6" d="M189.09,192.96c.13-.05.29.17.44.4l29.1,30.8-.66.85c-8.68-2.74-17.43-4.04-27.17-2.92l-.05-15.42-1.48-13.12-.12-.59c0,.18-.01.25-.06.01Z"/>
|
||||||
|
<path class="cls-2" d="M121,196.71l.35,1.99c.63-.55,1.25-.16,1.49-.09.22.06.34.33.1.59l-.74.79-7.42,2.99c-.86,2.97,1.23,5.48,2.29,7.94l.49,9.68c.06,1.23,1,2.52.85,3.37-.14.73-.8.91-1.08,1.02-1.11.45-3.05-5.03-4.74-8.09-1.29-2.35-4.13-4.12-5.86-5.97l-3.48-3.71c.07-.54-.17-.88-.33-1.31-.26-.75-1.5-.67-2.02-.58-.48-2.08.07-4.42-.22-7,3.74.38,6.19,6.49,8.76,5.54,2.12-.78.47-6.55,7.47-6.94l4.09-.23Z"/>
|
||||||
|
<path class="cls-1" d="M153.66,195.96c1.59,1.69,3.16,3.44,5.02,4.72,2.32,1.59,2.2,5.02,1.68,7.91l-4.63,26.09c-.3,1.71-.65,3.27-1.51,4.81-1.16-1.94-.03-3.7-.04-5.52l-.16-18.83-3.07-18.95,2.25-.71.46.48Z"/>
|
||||||
|
<path class="cls-1" d="M73.83,272.96c-.61-.65-1.35-.72-2.03-.75l3.58-4.84-2.64-3.89,3.59.27c7.46,2.46,14.99,2.66,22.88,4.1l-19.34,3.5c-2.15.39-4.26.34-6.04,1.61Z"/>
|
||||||
|
<path class="cls-4" d="M101.07,245.27c1.32.37,1.33-.16,1.51-.72l1.66-.49-5.73.22c-.51-1.03-.85-2.41.51-2.86l2.84-.93c.97-.32,3.09-.29,2.96-1.52-.12-1.15.72-1.05,1.57-1.15.61-.08,1.16.55,1.75,1.19.62-.86.91-1.67,1.59-1.72.49-.03.82,1.16,1.29,1.61,8.43-1,16.39-2.64,24.78-2.69.93.14,1.37.67,1.97,1.52.31.44-.79.63-.68,1.91,1.13-.23,2.61-.37,3.18.4,1,1.35-2.25,4.13-2.38,4.14-8.67.26-17.05,1.29-25.81,3.25v17.6s16.68-2.55,16.68-2.55c.84-.13,1.36.47,1.81,1.22.29.47-.58.95-.93,2.28,1.28.22,2.17-.38,3.68.25.1,1.86-.83,3.81-2.67,4.08l-14.99,2.18c-.7.1-1.4.2-2.8.21l.28,18.72,6.33-.85c6.72-.9,13.28-1.91,20.29-1.77l2.18,1.26c.35.2,1.11.91,1.09,1.32s-.42,1.36-1.09,1.41l-7.21.58v.92s6.65-.11,6.65-.11c.5,0,.81.59.83.93.02.4-.69,1.28-1.18,1.32l-15.07,1.37-13.46,1.99c.07,1.03.17,1.8-.26,2.52-.29.48-.99.61-1.73.57-.79-.04-1.1-1.5-1.64-2.22-3.36.92-6.44,1.31-6.98-.54-.31-1.06.53-2.77,2.06-2.1.72.31,1-.89.78-.9l-1.74-.07c-.99-.04-1.62-.08-2.1-.38-.57-.37-.51-1.36-.14-1.9.94-1.37,4.01-.91,3.99-2.17l-.52-28.42.02-15.84c-1.61.28-2.59.39-3.43-.02-1.08-.53-.7-3.32.27-3.04Z"/>
|
||||||
|
<path class="cls-5" d="M186.99,284.59c.71-.75,1.33-1.33,1.92-1.58.69-.29,1.36.35,1.65.81s.12,1.36-.17,1.99c-5.59,12.41-20.6,18.47-33.05,12.33-8.18-4.03-12.28-14.72-12.73-22.74-1.06-18.61,11.87-45.08,30.31-39.87,9.88,2.79,12.84,12.78,11.3,14.47-.34.38-1.07.77-1.41.26-.8-1.18-.36-2.66-1.92-3.73.49,2.89.44,6.09-.59,6.65-3.98,2.17-1.74-6.28-8.14-8.64-10.22-3.76-18.14,11.04-19.63,23.04-1.45,11.68,3.85,24.41,14.77,24.11,6.1-.17,11.2-4.41,14.12-9.6.59-1.04,1.65-1.76,2.67-.96.68.53.94,2.66.89,3.46-.24,0-.82-.26-1.15.32l-2.56,4.45.51.54c1.45-1.23,3.14-4.22,3.13-4.06l.07-1.24Z"/>
|
||||||
|
<path class="cls-1" d="M186.99,284.59l-.07,1.24c0-.16-1.68,2.83-3.13,4.06l-.51-.54,2.56-4.45c.33-.57.91-.32,1.15-.32Z"/>
|
||||||
|
<path class="cls-3" d="M219.78,225.37l-1.81-.37c-8.68-2.74-17.43-4.04-27.17-2.92l-.05-15.42-1.48-13.12-.19-.58"/>
|
||||||
|
<path class="cls-3" d="M219.78,225.37l.02.65-.12,6.48c-.06,3.28-.1,6.15-.34,9.4l-1.49,20.28c-.14,1.92.53,4.27-.65,6.12l-5.41,3.97,6.02,6.36,1.49,21.56c.37,5.41,2.59,8.94-3.33,12.4-7.99.64-14.33,1.69-23.27.55-5.4-.69-.74,10.29-4.65,22.7-.68,2.15-.83,4.01.26,5.73"/>
|
||||||
|
<path class="cls-3" d="M189.15,192.95c-2.52-.41-5.15-.22-7.96,0l-13.46,1.04-.57.11c-1.27.24-2.79.05-3.85.57-2.71,1.35-1.19,5.7-4.63,6.02-1.86-1.28-3.42-3.03-5.02-4.72l-.46-.48c-.8-1.91-2.32-1.12-3.9-1l-28.3,2.23-4.09.23c-7.01.39-5.35,6.16-7.47,6.94-2.57.94-5.02-5.17-8.76-5.54l-29.01-2.92-1.99,24.45,6.25,5.77.41.38.41.37"/>
|
||||||
|
<polyline class="cls-3" points="189.15 192.95 189.53 193.36 218.63 224.15 219.78 225.37"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #39170a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: none;
|
||||||
|
stroke: #381507;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-width: 2.75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #3d180b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #a88a21;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #d3ac2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
fill: #ffcf34;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path class="cls-6" d="M179.77,205.02l.77,4.04c.04.21.1.45.08.59.02-.14-.04-.38-.08-.59-.85,1.19-1.1-.58-2.67-.25l.79,8.42-.89,10.4c-.41,4.82-1.24,9.97,1.41,14.45,1.05,1.78,9.63-.31,17.16-.75,1.02,1.74,4.84.68,6.63,4.83.88,2.04,2.91,3.45,3.86,5.38,1.4,2.81,2.25,5.73,4.84,7.45,1.02-.82,1.88-.07,2.66-.28l.07,6.27c.03,2.58.2,4.71-.47,6.93-.75.09-2.1-.23-2.94.31-2.87,1.83-6.08,2.33-9.57,2.78l-6.5.84-16.99.61c-3.66.13-7.28-.69-11.12,1.38l16.47,2.61,22.92,3.07,7.37.46c.9,1.94.62,4.11.83,6.29l1.64,17c.53,5.45-8.08,6.66-5.27,8.93,2.01,1.62,5.55,12.81,5.16,16.41l-2.13,19.73c-8.18,1.27-16.4,2.09-24.72,1.88l-6.37-8.74c-.46-.63-1.12-1.06-1.64-1.08-.67-.03-1.56.51-2.14,1.26l-5.23,6.74-16.53,1.49c-2.64.24-5.37.61-7.42-.64l-.56-.34c.37,0,.52-.6.99-.96.23-.18-.52-.27-.45-.87l-1.23-17.67c-.63-9.07-.23-17.95-2.92-26.95l-2.35,17.78-4.21,27.18,3.26.18c-1.16,2.56-3.95,2.42-6.56,2.51l-13.67.49-16.87,1.08c-.05-.45-.49-.46-.65-.67l-.4-.38-.4-.38-.4-.38.58-.54.2.56c2.01-.21.19-3.36.22-9.43,0-2.13-.72-4.28.05-6.44.35-.98,2.07-.69,2.87-.56.43-1.64-1.95-1.67-2.13-2.33l.95-10.42c.09-.94.1-1.71-.31-2.02-1.64-1.25-3.51.15-5.41-.22.06-.7-.29-1.6-.74-2.1-.64-.71-1.77-.71-2.91-.62l-12.92,1.07.18-1.34s-.25-1-.63-1.37l-3.78-3.63c-1.39-1.34-2.03-2.83-1.58-5.16l-3.26-3.67c-1.51-1.7-2.88-4.39-.81-6.29,1.1-.32,3.6.81,4.46-.47,2.86-4.24-1.31-5.58-.49-7.74,1.33-3.48-4.58-6-4.26-8.5l13.01-2.5c3.67-1.59,6.78-3.8,10.59-3.22l.1,32.81c0,1.01,1.11,1.93,1.68,2.01,2.03.27,1.28-3.03,2.08-6.24,2.13.46-1.02,4.51,1.57,8.01.45.61,1.46,0,1.84-.26.62-.41.57-1.41.77-2.39l1.09-.2c.09-.02.41-.44.4-.93l-.43-14.35-.45-23.53c-.01-.52-.02-1.19.34-1.48.58-.46,1.53-.32.75.63l8.86,15.58,13.62,24.51c.37.67,1.86,1.46,2.5,1.41.85-.07,1.58-1.73,1.05-2.73-.13-1.47.35-1.05,1.48-.63,1.09-.52,1.88.67,2.48.34.91-.51,1.18-1.31,1.03-2.48-1.7-13.24-2.12-26.26-1.41-39.55l.91-17.03c.05-.93-.07-1.64-.43-2.01-.49-.49-1.44-.78-1.91-.55-.75.36-1.15,1.14-1.13,2.02l.19,7.68h-.86s-.48-6.72-.48-6.72c-.08-1.09-1.4-1.77-2.14-1.88-1.31-.2-2.03,1.16-2.03,2.59l-.14,40.7c-1.18.02-.63-.49-.97-1.13-7.29-13.26-14.57-26.19-20.99-39.77-.21-.45-.86-.62-1.11-.66-.4-.05-.83.64-1.21.96l-.5-2.31c-.16-.75-1.63-1.47-2.45-.7-1.35,1.27-.5,3.3-.84,4.65-1.25-.1-.52-1.14-.93-1.47-.51-.4-1.14-.55-2.26-.76l-.37,3.69c-2.89-2.03-5.73-4.3-9.04-6.32-.72-.34-2.29.61-2.32,1.39-.03,1.15-.89,2.45.54,3.02l10.66,3.95.41,19.15-14.09-.14c-5.14-.17-9.74-2.39-14.67-4.07-.85.62-1.32,1.36-2.39.69l-1.67-18.92c-.18-2.03.78-3.84,2.65-4.71l7.42-3.44-5.93-5.13.16-5.46c-1.39-3.37-3.58-6.69-3.35-10.39l1.05-16.95c.05-.8-.56-1.46-.02-2.1s1.18.05,2.02.15l15.08,1.79,14.48.61,27.87-1.53c2.78-.15,4.85.21,6.45,2.28-.81,1.03-1.69-.01-3.26,0l4.13,7.93c4.58,11.64,3.97,31.83,8.32,46.37l-.45-23.47,1.02-30.81c-.29-.23-.77.13-1.3.05-.68-.1.4-.6.29-1.64l-.63.29.63-.29,16.21-1.53,9.43-.31c4.27-.14,8.52-1.66,12.53-.39l.66.23c-.03.16-.03.22-.03-.04ZM183.3,260.1c.11.72.67,3.38,2.19,2.35,1.76-7.21-5.95-14.22-14.14-15.14-14.41-1.63-23.52,15.84-26.38,29.41-2.46,11.72.92,27.85,11.7,33.33,3.81,1.94,8.55,3.2,12.6,2.71,17.38-2.11,23.73-18.78,19.2-17.57-1.69.45-1.96,3.32-2.63,3.58-2.37.93.71-2.54.04-4.79-.24-.81-1.17-1.31-1.71-.96-2.42,1.56-3.83,8.02-11.84,10.21-7.31,2.01-14.11-2.14-16.6-9.5-6.23-18.48,5.85-40.89,17.78-36.88,5.29,1.78,5.29,9.24,6.76,9.31,2.39.13,2.79-4.52,2.18-6.12l.85.05Z"/>
|
||||||
|
<path class="cls-5" d="M97.21,275.95c.18.35.62.79.03.98-3.81-.59-6.93,1.63-10.59,3.22l-13.01,2.5c-.32,2.5,5.59,5.02,4.26,8.5-.82,2.16,3.35,3.5.49,7.74-.86,1.28-3.37.15-4.46.47-2.07,1.9-.69,4.59.81,6.29l3.26,3.67c-.45,2.32.19,3.82,1.58,5.16l3.78,3.63c.38.37.64,1.34.63,1.37l-.18,1.34c-5.9.49-11.78-1.34-17.6-2.56,0,.16,0,.27,0,.02l-.69-.73c.06-1.46-.73-4.12-.11-5.67.88-2.2.34-3.61.48-5.56l1.71-23.37c.25-3.49-1.22-7.09-1.53-10.53,1.07.67,1.54-.07,2.39-.69,4.93,1.69,9.53,3.9,14.67,4.07l14.09.14Z"/>
|
||||||
|
<path class="cls-6" d="M83.8,320.81l12.92-1.07c1.13-.09,2.26-.09,2.91.62.45.5.8,1.4.74,2.1-.91,10.45-.01,20.93,3.56,30.87l-.58.54-36.74-35.2-.4-.41c5.82,1.22,11.7,3.05,17.6,2.56Z"/>
|
||||||
|
<path class="cls-6" d="M179.77,205.02c0,.18,0,.24.03.04l34.21,33.44-.49.54-1.92-.35c-.22-.04-.45-.09-.64-.11l-2.66-.27c-13.98-3.98-24.18.77-25.99-.65-.73-.57-.59-1.87-.82-2.93,1.42-8.61.58-16.85-.87-25.09.02-.14-.04-.38-.08-.59l-.77-4.04Z"/>
|
||||||
|
<path class="cls-5" d="M149.77,353.23l-.56-.34-3.01-2.1c-1.49-1.04-3.18-.66-3.91.8l-3.26-.18,4.21-27.18,2.35-17.78c2.69,9,2.29,17.87,2.92,26.95l1.23,17.67c-.07.59.69.68.45.87-.47.37-.62.97-.99.96l.56.34Z"/>
|
||||||
|
<path class="cls-5" d="M140.36,207.35l.63-.29c.11,1.04-.97,1.54-.29,1.64.53.08,1.01-.28,1.3-.05l-1.02,30.81.45,23.47c-4.34-14.54-3.74-34.72-8.32-46.37l-4.13-7.93c1.57-.02,2.45,1.02,3.26,0,1.15,1.48,3.05,3.31,5.03,3.99l3.08-5.27Z"/>
|
||||||
|
<path class="cls-5" d="M213.94,271.89c-1.02,3.42-5.64,5.2-4.81,6.04l3.77,3.83c.8.81.39,1.56.68,2.19l-7.37-.46-22.92-3.07-16.47-2.61c3.84-2.08,7.46-1.25,11.12-1.38l16.99-.61,6.5-.84c3.49-.45,6.7-.95,9.57-2.78.84-.53,2.19-.22,2.94-.31Z"/>
|
||||||
|
<path class="cls-5" d="M214.34,258.7c-.77.21-1.64-.54-2.66.28-2.59-1.73-3.44-4.65-4.84-7.45-.96-1.92-2.99-3.34-3.86-5.38-1.79-4.15-5.61-3.09-6.63-4.83l14.51-.85c-.03-.62-.15-1.25.1-1.89.19.02.43.07.64.11l1.92.35.49-.54.4.39c.14.14.38.23.4.39l.1.72c.48,3.66-.57,7.44-.57,11.31v7.39Z"/>
|
||||||
|
<path class="cls-1" d="M96.95,254.77l.37-3.69c1.12.21,1.75.37,2.26.76.42.33-.32,1.37.93,1.47.34-1.34-.52-3.38.84-4.65.82-.77,2.29-.05,2.45.7l.5,2.31c.37-.32.81-1.01,1.21-.96.25.03.9.2,1.11.66,6.43,13.58,13.7,26.51,20.99,39.77.33.64-.22,1.15.97,1.13l.14-40.7c0-1.44.72-2.79,2.03-2.59.75.11,2.07.79,2.14,1.88l.48,6.73h.86s-.19-7.69-.19-7.69c-.02-.88.38-1.66,1.13-2.02.47-.23,1.41.06,1.91.55.36.36.48,1.08.43,2.01l-.91,17.03c-.71,13.29-.29,26.3,1.41,39.55.15,1.17-.12,1.97-1.03,2.48-.6.34-1.39-.86-2.48-.34-1.13-.42-1.61-.84-1.48.63.53,1-.2,2.66-1.05,2.73-.63.05-2.13-.74-2.5-1.41l-13.62-24.51-8.86-15.58c.78-.95-.17-1.09-.75-.63-.36.29-.35.96-.34,1.48l.45,23.53.43,14.35c.01.49-.31.91-.4.93l-1.09.2c-.2.98-.16,1.98-.77,2.39-.38.26-1.39.87-1.84.26-2.59-3.5.56-7.55-1.57-8.01-.8,3.22-.05,6.51-2.08,6.24-.58-.08-1.68-1-1.68-2.01l-.1-32.81c.59-.19.15-.63-.03-.98l-.41-19.15c-.01-.67.09-1.35.16-2.03ZM131.61,294.87c.56.17.86.1,1.15.05l-.14-5.48h-.88s-.12,5.43-.12,5.43Z"/>
|
||||||
|
<path class="cls-3" d="M183.3,260.1l-.32-2.09c-.11-.72-1.11-1.15-.97-1.75l-3.04-3.05c-.51-.51-.92-.45-1.63-.42,2.53,1.87,4.03,4.39,5.11,7.26.61,1.6.21,6.25-2.18,6.12-1.47-.08-1.46-7.54-6.76-9.31-11.93-4-24.01,18.41-17.78,36.88,2.48,7.36,9.28,11.51,16.6,9.5,8-2.2,9.41-8.66,11.84-10.21.54-.35,1.47.15,1.71.96.66,2.24-2.41,5.72-.04,4.79.67-.26.94-3.13,2.63-3.58,4.53-1.21-1.83,15.46-19.2,17.57-4.05.49-8.79-.77-12.6-2.71-10.78-5.48-14.16-21.61-11.7-33.33,2.86-13.58,11.96-31.05,26.38-29.41,8.2.93,15.9,7.93,14.14,15.14-1.52,1.03-2.08-1.63-2.19-2.35Z"/>
|
||||||
|
<path class="cls-4" d="M180.62,209.65c1.45,8.24,2.29,16.48.87,25.09.23,1.06.09,2.35.82,2.93,1.81,1.42,12.02-3.33,25.99.65l2.66.27c.19.02.43.07.64.11-.22-.04-.45-.09-.64-.11-.26.64-.13,1.27-.1,1.89l-14.51.85c-7.53.44-16.11,2.54-17.16.75-2.65-4.48-1.82-9.62-1.41-14.45l.89-10.4-.79-8.42c1.56-.33,1.82,1.45,2.67.25.04.21.1.45.08.59Z"/>
|
||||||
|
<path class="cls-4" d="M100.37,322.45c1.9.37,3.77-1.03,5.41.22.41.31.4,1.07.31,2.02l-.95,10.42c.18.66,2.56.69,2.13,2.33-.8-.12-2.52-.42-2.87.56-.77,2.16-.04,4.31-.05,6.44-.03,6.07,1.79,9.23-.22,9.43l-.2-.56c-3.58-9.94-4.47-20.41-3.56-30.87Z"/>
|
||||||
|
<path class="cls-5" d="M96.95,254.77c-.07.68-.17,1.36-.16,2.03l-10.66-3.95c-1.43-.57-.57-1.87-.54-3.02.02-.78,1.6-1.73,2.32-1.39,3.31,2.02,6.15,4.3,9.04,6.32Z"/>
|
||||||
|
<path class="cls-4" d="M183.3,260.1l-.85-.05c-1.08-2.87-2.59-5.4-5.11-7.26.71-.03,1.12-.09,1.63.42l3.04,3.05c-.15.6.86,1.03.97,1.75l.32,2.09Z"/>
|
||||||
|
<path class="cls-4" d="M131.61,294.87l.12-5.43h.88s.14,5.47.14,5.47c-.29.05-.59.12-1.15-.05Z"/>
|
||||||
|
<path class="cls-2" d="M179.77,205.02l-.62-.2c-4-1.27-8.26.25-12.53.39l-9.43.31-16.21,1.53-.63.29-3.08,5.27c-1.99-.68-3.89-2.51-5.03-3.99-1.6-2.07-3.66-2.43-6.45-2.28l-27.87,1.53-14.48-.61-15.08-1.79c-.84-.1-1.46-.81-2.02-.15s.07,1.3.02,2.1l-1.05,16.95c-.23,3.7,1.96,7.02,3.35,10.39l-.16,5.46,5.93,5.13-7.42,3.44c-1.86.87-2.83,2.67-2.65,4.71l1.67,18.92c.3,3.43,1.78,7.04,1.53,10.53l-1.71,23.37c-.14,1.96.39,3.36-.48,5.56-.62,1.55.17,4.21.11,5.67l.69.73"/>
|
||||||
|
<path class="cls-2" d="M214.8,239.27l-1.28-.24-1.92-.35c-.22-.04-.45-.09-.64-.11l-2.66-.27c-13.98-3.98-24.18.77-25.99-.65-.73-.57-.59-1.87-.82-2.93,1.42-8.61.58-16.85-.87-25.09.02-.14-.04-.38-.08-.59l-.77-4.04"/>
|
||||||
|
<path class="cls-2" d="M104.54,355.01l-.41-1.13-.2-.56c-3.58-9.94-4.47-20.41-3.56-30.87.06-.7-.29-1.6-.74-2.1-.64-.71-1.77-.71-2.91-.62l-12.92,1.07c-5.9.49-11.78-1.34-17.6-2.56"/>
|
||||||
|
<polyline class="cls-2" points="179.8 205.06 214.01 238.49 214.41 238.88 214.8 239.27"/>
|
||||||
|
<path class="cls-2" d="M214.8,239.27l.1.72c.48,3.66-.57,7.44-.57,11.31v7.39s.07,6.27.07,6.27c.03,2.58.2,4.71-.47,6.93-1.02,3.42-5.64,5.2-4.81,6.04"/>
|
||||||
|
<polyline class="cls-2" points="66.21 318.28 66.6 318.66 103.34 353.86 103.74 354.24 104.14 354.63 104.54 355.01"/>
|
||||||
|
<path class="cls-2" d="M210.79,316.19c2.01,1.62,5.55,12.81,5.16,16.41l-2.13,19.73c-8.18,1.27-16.4,2.09-24.72,1.88l-6.37-8.74c-.46-.63-1.12-1.06-1.64-1.08-.67-.03-1.56.51-2.14,1.26l-5.23,6.74-16.53,1.49c-2.64.24-5.37.61-7.42-.64l-.56-.34-3.01-2.1c-1.49-1.04-3.18-.66-3.91.8-1.16,2.56-3.95,2.42-6.56,2.51l-13.67.49-16.87,1.08-.65-.67"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 9.6 KiB |
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #9b1f0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #3a160a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #e93525;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #3d180d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: #c12b1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
fill: none;
|
||||||
|
stroke: #381507;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-width: 2.2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path class="cls-3" d="M141.35,207.84c-.15-.1-.38-.08-.57-.14-.5.53.56,1.73-.35,2.63,2.2,6.52,2.73,13.17,3.12,20.16l1.18,20.95c.3-.07.67-.04.84-.02.7-.89-.2-2.18-.08-3.4l3.51-37.22c.07-.78-.41-1.44-.5-1.88l19.5-2.14c1.96.84,1.89,4.16,2.43,5.54-.19-.36-1.21.01-1.82.21-.39.13.11.72-.4,1.5.58,10.03-3.74,18.54-4.28,28.36,3.88-6.54,14.81-30.94,15.78-31.51,1.1-.65,1.54.47,2.43.78,1.28.45,3.34-.37,3.73,1.34.25,1.09,1.66.7,2.44,1.56-.46,1.49-1.33,2.83-1.48,4.39,3.42.29,7.69-.88,11.02.3,2.37.84-.91,3.18-1.22,4.37.17,4.04,4.98,4.47,10.21,3.99.82.43.92,2.65,2.23,2.64l-4.61,3.64c4.87-.22,8.3,1.84,11.15,1.44l-.61,4.42c-.69.13-1.97-.34-2.8.24-6.06,4.22-12.42,7.63-18.95,11.04l-6.29,4.39c4.87.92,9.07-1.38,13.76-2.48,3.75-.88,7.71-1.15,11.61-1.17,1.47,0,3.02,1.79,4.47.65l-1.19,12.44c-.25,2.67-.6,5.14-1.34,7.45-.83-.98-1.63-.31-2.63.07-6.58,2.5-13.24,3.89-20.13,5l-17.39,2.78c1.75,1.3,3.52.34,5.24.34h32.08c-1.46,8.73-7.23,6.14-7.99,8.08-1.21,3.1,2.92,5.92,1.74,7.9s-3.15,3.27-4.07,5.7c2.08.02,5.13-3.57,7.39-2.41.77.39,1.39,1.77,1.11,3.19-3.36,4.96-9.42,5.44-8.61,13.35l-.74,3.75c-1.58.06-2.1,1.03-2.21,2.08l-12.24-1.78c-1.47-.21-4.87-.09-4.63,1.94l1.6,13.76c.17,1.43.76,2.77-.25,3.79l-5.27,2.54-2.14.63-1.93-1.16c-6.66,3.78-4.71,7.07-15.02,6.31l-11.95,2.56-1.39-7.83c-3.15-7.67-3.81-15.27-3.95-23.41l-.15-8.71c-.38.04-.44-.34-.87-.53l-1.62,20.27c-.21,2.63-.8,4.53-1.38,7.39l-20.37-.61c-2.55-.08-4.58-3.05-7.89-1.97-.6-2.94-2.88-3.36-5.33-3.82.34-4.53,4.71-6.78,3.51-8.96l-7.49,7.73-1.13-2.09c-1.15-2.13-1.4-5.14-4.15-6.09-.73-1.72-2.1-3.05-4.58-2.9-.17-1.75-2.45-1.79-2.86-2.71-.24-.54.43-.87-.61-.57-.2.06-3.02-3.72-3.62-7.19-3.05-.69-.25-4.22-2.63-4.24,3.16-3.19-3.84-1.11-4.81-3.41-.81-1.91-.04-3.39.74-4.96.26-.51-.56-4.29,2.44-9.1,6.33-2.1,12.4-3.33,18.8-4.23.25-.04.88.64,1.17.5.34-.16.76-.41.89-1.14l-22.09-1.45-1.42-.42c.68.2-2.27-4.82-.93-7.13,1.88-.95,4.57-.3,6.65-.77.49-.11,4.68-5.32,4.6-4.85.15-.81-.35-1.49-.9-1.62-1.08-.25-1.49,2.01-5.22,2.29-1.4.11-3.82-3.52-3.86-3.9-.09-.81.64-.76,1.09-1.56,3.25-5.81,2.03-5.22,1.61-9.67-.13-1.35-1.69-2.4-.19-3.82,2.65-2.49,5.15-5.35,6.94-8.32.25-.1.64-.51,1.11-.47l13.86,1.31c3.55-2.76,1.46-9.93,1.27-15.68l-.49-14.85c.39-.69.33-1.35.37-1.85.06-.7-.98-1.5-1.55-.94l.55-2.93.11-.58.22-1.16-.39.4.39-.34c4.32.66,9.46,1.37,13.92,1.69l14.97,1.06c3.88.27,7.66-.03,11.21,1.08.19.06.42.03.57.14ZM102.09,290.2l.93,21.66c.06,1.44-.52,3.82,1.01,4.64.98.52,2.05,1,2.45.38l1.25-1.93c.34-.52-.14-1.15.09-1.4.18-.19.64-.12.85-.03.32.12.23.16.26.39.08.53-.64.83-.5,1.16.41.96,1.63,1.3,2.11.96.43-.3,1.27-1.06,1.24-1.95l-.87-22.21c8.59-.6,16.52-3.59,22.83-8.98,7.59-6.48,9.09-17.7,2.59-25.38s-21.36-8.59-30.78-5.14c-.19-1.38-.35-2.08-.8-2.15l-2.07-.29-.32,3.56c-9.69,4.25-8.22,8.19-6.55,7.13.37-.23.75-.79,2.02-.94l-1.7,2.61c-.33.5-.28,1.36.15,1.65.29.2,1.12.33,1.58,0l4.17-3.06-.03,24.78c-1.77.34-3.13-1.12-4.55-.4-1.95.98.56,3.2,4.63,4.93ZM183.9,295.43c-2.54-1.64-4.08,8.31-13.8,10.5-5.05,1.14-10.5-1.01-13.47-5.57-4.78-7.32-5.07-16.43-2.58-24.6,3.13-10.28,10.83-20.05,19.27-15.53,5.81,3.12,2.84,9.38,6.76,8.1.76-.25,1.62-3.66.55-6.94,1.78.38,1.35,3.89,2.76,4.16,2.82.54.95-11.97-10.88-15.11-17.71-4.7-30.43,20.87-29.61,38.76.36,7.72,2.8,15.24,8.44,20.4,5.98,5.47,14.37,6.55,22.33,4.17,9.83-2.94,16.54-13.86,14.45-15.95-.52-.53-1.42-.6-1.75-.07l-.88,1.4c-.87.17-.68.78-.87.89-1.52.83,1.03-3.48-.73-4.61Z"/>
|
||||||
|
<path class="cls-5" d="M100.35,205.56l-.55,2.93c-1.08,5.75-2.71,11.68-2.27,17.86.25,3.54.85,6.96.03,10.68-8.89.06-16.45-2.38-25.75.39-.64.15-.28,1.16-.01,1.44.24.26.66-.03,1.36.22l11.41,1.34c.18.5.36.67.67.55-1.8,2.97-4.29,5.82-6.94,8.32-1.5,1.41.06,2.46.19,3.82.42,4.45,1.64,3.85-1.61,9.67-.45.81-1.18.76-1.09,1.56.04.39,2.46,4.01,3.86,3.9,3.73-.29,4.14-2.55,5.22-2.29.56.13,1.05.81.9,1.62.09-.47-4.1,4.74-4.6,4.85-2.08.48-4.77-.18-6.65.77-1.35,2.31,1.61,7.33.93,7.13l1.42.42,22.09,1.45c-.13.74-.55.98-.89,1.14-.29.14-.91-.53-1.17-.5-6.4.9-12.47,2.12-18.8,4.23-3,4.81-2.18,8.59-2.44,9.1-.78,1.57-1.55,3.04-.74,4.96.97,2.29,7.97.21,4.81,3.41,2.38.02-.42,3.55,2.63,4.24.6,3.47,3.42,7.25,3.62,7.19,1.04-.3.38.02.61.57.4.92,2.69.96,2.86,2.71,2.48-.16,3.85,1.18,4.58,2.9,2.75.95,3,3.96,4.15,6.09l1.13,2.09,7.49-7.73c1.2,2.18-3.16,4.43-3.51,8.96,2.45.45,4.73.87,5.33,3.82,3.31-1.07,5.34,1.9,7.89,1.97l20.37.61c.58-2.85,1.17-4.75,1.38-7.39l1.62-20.27c.43.19.49.57.87.53l.15,8.71c.14,8.14.8,15.74,3.95,23.41l1.39,7.83,11.95-2.56c10.32.76,8.36-2.53,15.02-6.31l1.93,1.16,2.14-.63,5.27-2.54c.4-.02.63.4.83.64.16.18-.17.6-.2,1.04l-.67,10.12c.82-.14,1.5-.45,2.27,0,2.81-6.65,3.02-13.87,2.67-21.4.15-.92-.09-2.34.39-2.85.45-.48,1.84-1.22,2.79-1.11,7.02.82,13.69.47,20.29-1.35,1.03.54,2.17.4,3.18.19.08.61.02,1.31-.5,1.83l-14.08,13.99c-3.04,3.02-6.73,5.26-9.07,8.88l-3.1,3.24c-1.03-1.01-2.03-.45-2.82-.97l-2.06,4.85-35.37-1.32c-4.34-1.79-6.54-8.04-7.73-7.92s-2.8,6.38-5.48,6.54l-10.41.64c-5.38.33-10.15.72-15.49,2.19-3.14.87-7.62.86-10.83.08-2.95-1.18-3.85-5.14-6.2-6.63-2.58.57-3.1,5.36-5.17,6.12s-15.98-.69-16.94-1.64c-1.04-1.02-1.66-2.74-1.93-4.36l-1.46-8.74c-.26-1.57-1.4-2.96-.05-5l.25-3.57c1.08-2.07.21-4.64.52-7.05l1.27-9.71c.24-1.86.5-4.12-.13-5.95l-.38-7.85c-.09-1.91-.47-3.76-.13-5.55l1.4-7.4c.42-2.24,1.09-4.38,1.82-6.51-.7-1.43-2.17-2.49-2.25-3.91l-.41-7.84-.51-7.74-.6-8.71-.3-7.65c-.86-4.02-1.08-8.37,2.47-11.45.28-.11.66-.01.92.02l30.19-31.05.84.55Z"/>
|
||||||
|
<path class="cls-5" d="M215.61,235.34c-2.85.4-6.28-1.66-11.15-1.44l4.61-3.64c-1.3,0-1.41-2.21-2.23-2.64-5.23.47-10.04.05-10.21-3.99.31-1.19,3.58-3.53,1.22-4.37-3.33-1.18-7.6,0-11.02-.3.14-1.57,1.01-2.91,1.48-4.39-.78-.86-2.19-.47-2.44-1.56-.39-1.71-2.45-.89-3.73-1.34-.89-.32-1.33-1.43-2.43-.78-.97.57-11.91,24.97-15.78,31.51.54-9.82,4.86-18.33,4.28-28.36.51-.78,0-1.37.4-1.5.61-.2,1.63-.57,1.82-.21.52,1.34,1.39,2.32,2.25,3.36l.35.42-.35-.42c2.99-4.01,4.32-9.78,6.79-11.04,1.99-1.01,6.98.63,10.05.99,2.25.26,4.67-.67,7.13.45l5.57.52c4.86.45,13.52,1.2,14.2,1.84.95.89.45,1.58.67,2.86,1.43,8.35.85,16.35-1.48,24.06Z"/>
|
||||||
|
<path class="cls-5" d="M214.27,272.31c-.4,1.25-2.02,2.41-1.74,4.07l2.61,3.32,1.18,21.98,1.27,18.14c.18,2.6-1.32,4.68-3.5,5.32-1,.22-2.14.35-3.18-.19.51-.44.48-1.12.22-2.05-4.36.28-8.77-.13-13.07-.76.11-1.05.62-2.02,2.21-2.08l.74-3.75c-.81-7.92,5.25-8.4,8.61-13.35.28-1.42-.34-2.79-1.11-3.19-2.26-1.16-5.3,2.44-7.39,2.41.92-2.43,2.85-3.65,4.07-5.7s-2.95-4.8-1.74-7.9c.76-1.94,6.53.65,7.99-8.08h-32.08c-1.72,0-3.48.96-5.24-.34l17.39-2.78c6.89-1.1,13.55-2.49,20.13-5,1-.38,1.8-1.05,2.63-.07Z"/>
|
||||||
|
<path class="cls-1" d="M148.5,208.91c.09.44.57,1.1.5,1.88l-3.51,37.22c-.12,1.22.79,2.51.08,3.4-.17-.02-.55-.05-.84.02l-1.18-20.95c-.39-6.98-.92-13.64-3.12-20.16.91-.89-.15-2.09.35-2.63.19.06.42.03.57.14,2.12,1.41,4.66,1.34,7.15,1.07Z"/>
|
||||||
|
<path class="cls-5" d="M215,239.76c-.37,2.69-1.06,4.75-4.09,5.88,0,1.06-1.32,1.19-1.19,1.7.25,1,1.38,1.43,2.04,1.7,1.6.65,5.23,1.29,5.03,3.38-1.45,1.14-3-.66-4.47-.65-3.89.02-7.85.3-11.61,1.17-4.69,1.1-8.9,3.39-13.76,2.48l6.29-4.39c6.53-3.41,12.89-6.83,18.95-11.04.83-.58,2.12-.11,2.8-.24Z"/>
|
||||||
|
<path class="cls-2" d="M102.09,290.2c-4.07-1.73-6.58-3.95-4.63-4.93,1.42-.72,2.78.75,4.55.4l.03-24.78-4.17,3.06c-.46.33-1.29.2-1.58,0-.43-.29-.48-1.15-.15-1.65l1.7-2.61c-1.27.15-1.65.71-2.02.94-1.68,1.06-3.15-2.89,6.55-7.13l.32-3.56,2.07.29c.45.06.61.77.8,2.15,9.43-3.46,24.09-2.78,30.78,5.14s5,18.9-2.59,25.38c-6.31,5.39-14.24,8.38-22.83,8.98l.87,22.21c.03.89-.81,1.65-1.24,1.95-.48.34-1.7,0-2.11-.96-.15-.34.58-.64.5-1.16-.04-.23.06-.27-.26-.39-.21-.08-.67-.16-.85.03-.23.25.25.88-.09,1.4l-1.25,1.93c-.4.62-1.47.15-2.45-.38-1.53-.82-.95-3.2-1.01-4.64l-.93-21.66ZM131.84,268.69c-.17-9.31-11.39-12.74-20.83-10.93-.29,9.08-1.11,17.3-.24,26.28,9.58-2.04,21.23-6.33,21.06-15.35Z"/>
|
||||||
|
<path class="cls-4" d="M183.9,295.43c1.76,1.14-.79,5.45.73,4.61.19-.11,0-.72.87-.89l.88-1.4c.33-.52,1.23-.45,1.75.07,2.08,2.09-4.63,13.02-14.45,15.95-7.96,2.38-16.35,1.3-22.33-4.17-5.64-5.17-8.09-12.68-8.44-20.4-.82-17.89,11.9-43.46,29.61-38.76,11.84,3.14,13.7,15.66,10.88,15.11-1.41-.27-.98-3.78-2.76-4.16,1.07,3.28.2,6.69-.55,6.94-3.93,1.28-.95-4.98-6.76-8.1-8.45-4.53-16.14,5.24-19.27,15.53-2.49,8.17-2.2,17.28,2.58,24.6,2.98,4.56,8.43,6.7,13.47,5.57,9.71-2.19,11.26-12.13,13.8-10.5ZM177.02,256.36l3.08,4.12c1.58-.8-.62-4.44-3.08-4.12Z"/>
|
||||||
|
<path class="cls-1" d="M198.07,322.14c4.3.63,8.71,1.03,13.07.76.25.92.29,1.6-.22,2.05-6.61,1.82-13.27,2.17-20.29,1.35-.95-.11-2.34.63-2.79,1.11-.48.51-.23,1.94-.39,2.85.34,7.53.14,14.75-2.67,21.4-.77-.46-1.44-.14-2.27,0l.67-10.12c.03-.44.36-.86.2-1.04-.2-.24-.43-.66-.83-.64,1.02-1.02.42-2.37.25-3.79l-1.6-13.76c-.24-2.03,3.16-2.15,4.63-1.94l12.24,1.78Z"/>
|
||||||
|
<path class="cls-1" d="M85.24,240.96c-.31.13-.49-.05-.67-.55l-11.41-1.34c-.71-.24-1.12.04-1.36-.22-.26-.28-.63-1.29.01-1.44,9.3-2.77,16.87-.34,25.75-.39.82-3.72.22-7.14-.03-10.68-.44-6.17,1.19-12.1,2.27-17.86.58-.57,1.61.24,1.55.94-.04.5.02,1.16-.37,1.85l.49,14.85c.19,5.76,2.28,12.92-1.27,15.68l-13.86-1.31c-.47-.04-.87.37-1.11.47Z"/>
|
||||||
|
<path class="cls-3" d="M131.84,268.69c.17,9.02-11.48,13.31-21.06,15.35-.87-8.98-.05-17.2.24-26.28,9.44-1.81,20.65,1.63,20.83,10.93Z"/>
|
||||||
|
<path class="cls-1" d="M177.02,256.36c2.46-.32,4.66,3.32,3.08,4.12l-3.08-4.12Z"/>
|
||||||
|
<path class="cls-6" d="M211.77,249.04c1.6.65,5.23,1.29,5.03,3.38l-1.19,12.44c-.25,2.67-.6,5.14-1.34,7.45-.4,1.25-2.02,2.41-1.74,4.07l2.61,3.32,1.18,21.98,1.27,18.14c.18,2.6-1.32,4.68-3.5,5.32-1,.22-2.14.35-3.18-.19-6.61,1.82-13.27,2.17-20.29,1.35-.95-.11-2.34.63-2.79,1.11-.48.51-.23,1.94-.39,2.85.34,7.53.14,14.75-2.67,21.4-.09.1-.19.28-.26.44l-2.06,4.85-35.37-1.32c-4.34-1.79-6.54-8.04-7.73-7.92s-2.8,6.38-5.48,6.54l-10.41.64c-5.38.33-10.15.72-15.49,2.19-3.14.87-7.62.86-10.83.08-2.95-1.18-3.85-5.14-6.2-6.63"/>
|
||||||
|
<path class="cls-6" d="M100.69,203.88c4.32.66,9.46,1.37,13.92,1.69l14.97,1.06c3.88.27,7.66-.03,11.21,1.08.19.06.42.03.57.14,2.12,1.41,4.66,1.34,7.15,1.07l19.5-2.14c1.96.84,1.89,4.16,2.43,5.54s1.39,2.32,2.25,3.36l.35.42"/>
|
||||||
|
<path class="cls-6" d="M100.68,203.82l-.39.4-.78.79-30.19,31.05c.27,1.12,1.58,1.24,2.49,1.36,9.3-2.77,16.87-.34,25.75-.39.82-3.72.22-7.14-.03-10.68-.44-6.17,1.19-12.1,2.27-17.86l.55-2.93.11-.58.22-1.16Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 288 560">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: #0db3c8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: #007988;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
fill: #0c96a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-4 {
|
||||||
|
fill: #3a170d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-5 {
|
||||||
|
fill: none;
|
||||||
|
stroke: #381507;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-width: 2.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-6 {
|
||||||
|
fill: #3c1b0d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path class="cls-1" d="M214.65,239.1l-2.58-.1c-3.79,2.1-7.73,4.08-11.61,6.32l-6.56,3.79c-2.2,1.27-5.02,2.22-6.12,5.03l12.97-3.01,11.9-1.19c.44-.63.06-1.58.21-2.2,1.11.52,3.9,1.16,3.71,3.1l-1.82,18.71c-1.5,1.84-3.42,2.56-4.11,4.45-.39.04-.84-.28-1.1-.57-.2-.23.18-1.3-.21-1.23l-13.32,2.67-24.66,3.99c2.94.91,5.47.34,8.11.31l30.61-.32c.58,0,1.24.32,1.55.45s.06,1.13-.11,1.49l-5.94,12.1-2.34,5.89c1.31.57,2.15-1.03,3.19-1.23,1.71-.34,2.18,1.87,2.29,2.91.19,1.76-.21,2.53-1.63,3.75l-3.61,3.1c-2.25,1.93-2.51,5.48-3.11,8.25l-2.36,4.83-14.21-1.65c-1.16-.13-2.3.56-2.59,1.21-1.13,2.5,3.49,19.87,1.51,29.66l2.38-.32.4,1.88c-.02,1.11-.76,2.05-1.49,2.86-.9,1-2.37.8-3.88.8l-24.17.07c-3.64.01-6.98.15-10.19-1.01l-.55-.2c.38-.35-.12-1.32,1-2.02l-3.02-10.07c-.55-1.82-.65-4.03-.68-6.05l-.38-23.49c0-.35.21-1.3-.12-1.78-.19-.26,1.36-1.02-.61-1.17l-4.19,32.02c-.53,4.08-2.01,7.72-1.64,11.98,1.56-.87,2.59-.64,3.6-.38-1.06.63-1.9,2.74-3.63,3-8.84,1.34-17.96,1.85-26.77.44l-13.88-3.27c-1.56-1.74-2.19-3.54-3.98-4.71l-6.41,7.6c-5.24-.33-10.27-1.31-15.49-2.21-2.15-4.26-.12-10.16-1.62-14.2-1.64-4.39-1.98-8.67-1.04-13.21,1.9-9.16,1.37-17.8.49-27.02-.43-4.52,3.2-9.56,5.02-12.83l.8-.58.55-.55-.55.55-.8.58c.11.87,1.36,1.26,2.07.83.33-.2.87.26,1.48-.62l30.23-5.39c1.79-.32,6.53.08,5.59-1.14-.12-.15-.41-.79-.78-.81l-16.48-.56-11.54.04c-3.89-.56-7.57.1-11.44-1.87.21,1.09-.13,1.57-.76,1.25-.22-.11-.39.11-.92.03l-2.78-19.95c-.32-2.3-1.51-5.82-.08-7.61l6.94-4.85c.64-.45.02-1.82-.8-1.97-2.71-1.37-4.11-3.91-2.9-7.27-2.45-4.5-2.75-9.25-2.39-14.17l.97-13.21c.38-5.16,5.65-2.67,12.92-.43,3.25.54,6.3-.23,9.33-1.63,8.42.55,16.41-.06,24.8-.67l16.36-1.2,8.59,1.05c.2.02.38.14.56.21-.19-.07-.37-.18-.56-.21-.54,4.1,1.56,7.36,1.72,9.19l1.11,13.13.8,17.66-.2,8.37c.25,0,.57-.14.92.02l3.92-47.24-.57.22.57-.22,19.53-1.91c1.57.6,1.43,3.49,2.12,4.44.09.12.09.4.12.6-.03-.2-.03-.48-.12-.6l-2.15.91c.17,8.51-1.44,13.81-3.15,21.63l-2.13,9.75c2.92-2.92,3.58-6.64,5.27-10.02l10.81-21.63c.85-1.69.49-4.05,3.09-4.69-.5-.7-.42-1.53-.6-2.46,1.54.33,3.13-.5,4.85.26l5.72.4,10.88,1.12,6.13.57c2.16.2,6.59-.53,8.1,2.25.4,6.45,1.14,13.48-.1,19.79l-2.24,11.36ZM168.83,303.16c-6.94.18-12.13-5.11-13.68-11.47-1.72-5.03-2.28-10.32-.88-15.55,1.12-10.54,9.73-25.45,20.19-20.69,5.19,2.36,4.68,11.87,7.33,8.34-.13.18.87-3.46,1.06-3.05-.47-1.02-.68-2.77-.56-2.97.38-.62.92-.03.9.33-.01.16.1.13.26.77.43.68.42,1.72.95,1.95,3.46,1.48,1.8-9.41-6.67-13.59-5.47-2.7-10.86-3.08-15.84-.29-7.33,4.11-11.48,11.04-14.58,18.6-3.51,8.58-5.17,17.76-2.65,26.66,1.55,5.5,3.59,11.18,8.36,15.03,6.06,4.89,14.44,6.2,22.41,3.63,13.87-4.48,18.3-21.76,11.29-15.14-.28.27.49.89.09,1.03-.25.08-.69-.15-1.14.58-.32.51.58,1.26-.3,1.42l-1.48.26,1.93-3.85c.54-1.09.15-2.4-.76-2.62-3.3-.79-4.86,10.32-16.21,10.62ZM99.04,299.48l-2.33-4.68-3.47.03c-.04,6.06,3.08,10.63,7.43,14.01,4.93,3.83,10.3,5.14,16.54,4.87,10.18-.44,19.57-6.79,21.53-16.96,1.57-8.14-2.54-15.62-9.31-19.96-7.16-4.59-16.77-5.53-20.09-10.13-1.83-2.53-1.29-5.65.09-8.51,1.08-2.23,3.67-3.76,6.68-4.4,8.68-1.84,13.61,5.15,15,4.52.46-.2,1.46-.79,1.17-1.39l-1.53-3.17c2.36.38,3.63,3.38,4.81,1.76,1.42-1.94-9.35-13.75-24.73-9.15-8.64,2.58-13.65,10.74-11.83,19.53.96,4.64,3.52,8.84,8.3,10.67l12.22,4.68c6.12,2.34,10.3,8.86,8.33,15.46-1.45,4.86-6.35,7.18-11.25,7.51-5.75.39-12.14-2.11-13.82-7.94-.66-2.3-.6-5.38-4.17-5.51-.75,4.05,1.16,6.89.42,8.75ZM103.94,315l-7.75,6.79c.97.96,1.64.88,2.27.52-.28.16,5.09-5.27,5.48-7.31Z"/>
|
||||||
|
<path class="cls-3" d="M210.63,274c-.34,2.98,4.2,2.59,4.27,3.76l2.51,42.37c.1,1.67-2.35,3.33-3.58,3.16l.11-2.9c-5.22.96-10.62.62-15.97,0l2.36-4.83c.6-2.77.86-6.32,3.11-8.25l3.61-3.1c1.42-1.22,1.82-1.99,1.63-3.75-.11-1.05-.58-3.25-2.29-2.91-1.04.2-1.88,1.81-3.19,1.23l2.34-5.89,5.94-12.1c.18-.36.42-1.36.11-1.49s-.97-.45-1.55-.45l-30.61.32c-2.64.03-5.17.59-8.11-.31l24.66-3.99,13.32-2.67c.39-.08,0,1,.21,1.23.25.29.71.61,1.1.57Z"/>
|
||||||
|
<path class="cls-1" d="M213.84,323.29c.69.99.45,1.15-.19,1.78l-17.39,17.12c-3.16,3.11-6.44,5.57-9.4,9.05-.38.45-.97.07-1.39-.06l-.4-1.88c1.94-6.68,3.23-13.66,2.17-20.91-.22-1.52-.28-2.52.82-3.51.9-.81,1.78-.11,3.06-.13l14.27-.23c3.03-.05,5.64-1.6,8.45-1.22Z"/>
|
||||||
|
<path class="cls-3" d="M69.29,278.12c.52.08.69-.14.92-.03.63.32.97-.16.76-1.25,3.87,1.96,7.55,1.31,11.44,1.87l11.54-.04,16.48.56c.38.01.66.65.78.81.93,1.22-3.8.82-5.59,1.14l-30.23,5.39c-.61.87-1.15.42-1.48.62-.71.43-1.96.04-2.07-.83l.8-.58.55-.55-.55.55.55-.55c.13-.13.21-.35.38-.39.47-.12.74-1.1.12-1.45-.29-.17-.73,0-.96-1.07-.92-1.33-3.2-2.39-3.45-4.18Z"/>
|
||||||
|
<path class="cls-3" d="M145.75,353.91l-.55-.2c-1.16-.42-2.27-1.18-3.54-1.9-1.2-.68-1.89,1.02-2.51.94-1.01-.26-2.05-.49-3.6.38-.38-4.26,1.1-7.9,1.64-11.98l4.19-32.02c1.97.15.43.91.61,1.17.33.47.11,1.42.12,1.78l.38,23.49c.03,2.02.13,4.23.68,6.05l3.02,10.07c-1.11.7-.61,1.67-1,2.02l.55.2Z"/>
|
||||||
|
<path class="cls-2" d="M181.3,203.36c.18.94.09,1.76.6,2.46-2.6.64-2.24,3-3.09,4.69l-10.81,21.63c-1.69,3.38-2.35,7.1-5.27,10.02l2.13-9.75c1.71-7.83,3.31-13.12,3.15-21.63l2.15-.91c.09.12.09.4.12.6.19,1.19.46,2.37,1.32,3.3.37-.75,1.52-.99,1.94-1.77l3.72-6.88c.54-1,.67-1.36,1.64-1.89,1.02-.56,1.31-.09,2.4.14Z"/>
|
||||||
|
<path class="cls-3" d="M147.94,207.55l.57-.22-3.92,47.24c-.34-.16-.67-.01-.92-.02l.2-8.37-.8-17.66-1.11-13.13c-.15-1.82-2.26-5.09-1.72-9.19.2.02.38.14.56.21,2.24.82,4.78.99,7.14,1.13Z"/>
|
||||||
|
<path class="cls-2" d="M214.65,239.1c-.45,2.26-2.11,3.94-3.91,5.65-.84.16-1.05.39-1.13.79-.15.82.08,1.71,1.03,1.66.76-.04,1.5.2,2.23.54-.16.62.22,1.57-.21,2.2l-11.9,1.19-12.97,3.01c1.1-2.81,3.92-3.76,6.12-5.03l6.56-3.79c3.88-2.24,7.82-4.22,11.61-6.32l2.58.1Z"/>
|
||||||
|
<path class="cls-4" d="M99.04,299.48c.73-1.86-1.18-4.7-.42-8.75,3.57.13,3.51,3.2,4.17,5.51,1.68,5.83,8.07,8.33,13.82,7.94,4.9-.33,9.79-2.65,11.25-7.51,1.97-6.6-2.21-13.11-8.33-15.46l-12.22-4.68c-4.78-1.83-7.34-6.03-8.3-10.67-1.82-8.79,3.19-16.95,11.83-19.53,15.38-4.6,26.15,7.2,24.73,9.15-1.18,1.62-2.45-1.38-4.81-1.76l1.53,3.17c.29.6-.71,1.18-1.17,1.39-1.39.62-6.32-6.36-15-4.52-3.02.64-5.61,2.16-6.68,4.4-1.38,2.86-1.93,5.98-.09,8.51,3.33,4.59,12.93,5.53,20.09,10.13,6.77,4.34,10.89,11.82,9.31,19.96-1.96,10.18-11.36,16.52-21.53,16.96-6.25.27-11.61-1.04-16.54-4.87-4.35-3.38-7.47-7.95-7.43-14.01l3.47-.03,2.33,4.68Z"/>
|
||||||
|
<path class="cls-6" d="M168.83,303.16c11.35-.3,12.91-11.42,16.21-10.62.9.22,1.3,1.53.76,2.62l-1.93,3.85,1.48-.26c.88-.16-.02-.91.3-1.42.46-.73.9-.5,1.14-.58.4-.14-.37-.76-.09-1.03,7.01-6.62,2.58,10.66-11.29,15.14-7.97,2.58-16.35,1.27-22.41-3.63-4.76-3.85-6.8-9.53-8.36-15.03-2.51-8.9-.86-18.09,2.65-26.66,3.1-7.56,7.25-14.49,14.58-18.6,4.98-2.79,10.38-2.41,15.84.29,8.47,4.18,10.13,15.07,6.67,13.59-.52-.22-.52-1.27-.95-1.95-.16-.64-.27-.62-.26-.77.02-.36-.52-.95-.9-.33-.12.2.09,1.95.56,2.97-.19-.41-1.19,3.23-1.06,3.05-2.65,3.53-2.14-5.98-7.33-8.34-10.46-4.76-19.07,10.15-20.19,20.69-1.4,5.23-.84,10.52.88,15.55,1.55,6.36,6.74,11.66,13.68,11.47Z"/>
|
||||||
|
<path class="cls-2" d="M197.98,320.39c5.36.62,10.75.96,15.97,0l-.11,2.9c-2.81-.38-5.42,1.17-8.45,1.22l-14.27.23c-1.27.02-2.16-.67-3.06.13-1.1.99-1.04,1.99-.82,3.51,1.06,7.25-.23,14.24-2.17,20.91l-2.38.32c1.98-9.79-2.64-27.16-1.51-29.66.29-.65,1.43-1.35,2.59-1.21l14.21,1.65Z"/>
|
||||||
|
<path class="cls-3" d="M103.94,315c-.39,2.04-5.76,7.47-5.48,7.31-.63.36-1.3.44-2.27-.52l7.75-6.79Z"/>
|
||||||
|
<path class="cls-5" d="M210.64,247.2c.76-.04,1.5.2,2.23.54,1.11.52,3.9,1.16,3.71,3.1l-1.82,18.71c-1.5,1.84-3.42,2.56-4.11,4.45-.34,2.98,4.2,2.59,4.27,3.76l2.51,42.37c.1,1.67-2.35,3.33-3.58,3.16-2.81-.38-5.42,1.17-8.45,1.22l-14.27.23c-1.27.02-2.16-.67-3.06.13-1.1.99-1.04,1.99-.82,3.51,1.06,7.25-.23,14.24-2.17,20.91l.4,1.88c-.02,1.11-.76,2.05-1.49,2.86-.9,1-2.37.8-3.88.8l-24.17.07c-3.64.01-6.98.15-10.19-1.01l-.55-.2c-1.16-.42-2.27-1.18-3.54-1.9-1.2-.68-1.89,1.02-2.51.94-1.06.63-1.9,2.74-3.63,3-8.84,1.34-17.96,1.85-26.77.44l-13.88-3.27c-1.56-1.74-2.19-3.54-3.98-4.71l-6.41,7.6c-5.24-.33-10.27-1.31-15.49-2.21-2.15-4.26-.12-10.16-1.62-14.2-1.64-4.39-1.98-8.67-1.04-13.21,1.9-9.16,1.37-17.8.49-27.02-.43-4.52,3.2-9.56,5.02-12.83l.8-.58.55-.55c.13-.13.21-.35.38-.39"/>
|
||||||
|
<path class="cls-5" d="M172.5,214.73l-.9-.97c-.85-.92-1.12-2.1-1.32-3.3-.03-.2-.03-.48-.12-.6-.7-.95-.55-3.83-2.12-4.44l-19.53,1.91-.57.22c-2.36-.14-4.9-.31-7.14-1.13-.19-.07-.37-.18-.56-.21l-8.59-1.05-16.36,1.2c-8.38.62-16.38,1.23-24.8.67-3.03,1.4-6.08,2.17-9.33,1.63-7.26-2.24-12.54-4.74-12.92.43l-.97,13.21c-.36,4.92-.07,9.67,2.39,14.17-1.22,3.36.19,5.9,2.9,7.27"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.7 KiB |
@@ -61,11 +61,13 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Landscape: right clears gear/kit buttons (~4rem); bottom is fixed 60px for
|
// Landscape: right clears gear/kit buttons; bottom is fixed 60px for the
|
||||||
// the kit-bag handle strip — tray is ignored so the stage has room to breathe.
|
// kit-bag handle strip — tray is ignored so the stage has room to breathe.
|
||||||
|
// At ≥1800px the right sidebar doubles to 8rem so clear 128px.
|
||||||
if (isLandscape) {
|
if (isLandscape) {
|
||||||
rightInset = Math.max(rightInset, 64); // 4rem
|
var xlBreak = vw >= 1800;
|
||||||
bottomInset = 60; // kit-bag handle
|
rightInset = Math.max(rightInset, xlBreak ? 128 : 64);
|
||||||
|
bottomInset = 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
overlay.style.paddingRight = rightInset + 'px';
|
overlay.style.paddingRight = rightInset + 'px';
|
||||||
@@ -78,7 +80,11 @@
|
|||||||
var sw = stageEl.offsetWidth - 24; // subtract padding (0.75rem × 2)
|
var sw = stageEl.offsetWidth - 24; // subtract padding (0.75rem × 2)
|
||||||
var sh = stageEl.offsetHeight - 24;
|
var sh = stageEl.offsetHeight - 24;
|
||||||
if (sw > 0 && sh > 0) {
|
if (sw > 0 && sh > 0) {
|
||||||
var cardW = Math.min(sw * 0.4, sh * 0.8 * 5 / 8);
|
// Clamp between 90px (never tiny in landscape) and 160px (never
|
||||||
|
// dominant on very wide/tall viewports). In portrait, skip the
|
||||||
|
// floor so small modals still scale down naturally.
|
||||||
|
var cardW = Math.min(sw * 0.4, sh * 0.8 * 5 / 8, 160);
|
||||||
|
if (isLandscape) { cardW = Math.max(cardW, 90); }
|
||||||
overlay.style.setProperty('--sig-card-w', cardW + 'px');
|
overlay.style.setProperty('--sig-card-w', cardW + 'px');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,23 @@ var SigSelect = (function () {
|
|||||||
gravity: ['BC', 'EC', 'AC'],
|
gravity: ['BC', 'EC', 'AC'],
|
||||||
};
|
};
|
||||||
|
|
||||||
var overlay, deckGrid, stage, stageCard;
|
var overlay, deckGrid, stage, stageCard, statBlock;
|
||||||
|
var cautionEl, cautionEffect, cautionPrev, cautionNext, cautionIndexEl;
|
||||||
|
var _flipBtn, _cautionBtn, _flipOrigLabel, _cautionOrigLabel;
|
||||||
var reserveUrl, userRole, userPolarity;
|
var reserveUrl, userRole, userPolarity;
|
||||||
|
|
||||||
|
var _cautionData = [];
|
||||||
|
var _cautionIdx = 0;
|
||||||
|
|
||||||
var _focusedCardEl = null; // card currently shown in stage
|
var _focusedCardEl = null; // card currently shown in stage
|
||||||
var _reservedCardId = null; // card with active reservation
|
var _reservedCardId = null; // card with active reservation
|
||||||
var _stageFrozen = false; // true after OK — stage locks on reserved card
|
var _stageFrozen = false; // true after OK — stage locks on reserved card
|
||||||
var _requestInFlight = false;
|
var _requestInFlight = false;
|
||||||
|
|
||||||
|
var _floatingCursors = {}; // key: cardId+posClass → portal <i> element (hover)
|
||||||
|
var _reservedFloats = {}; // key: role → portal <i> element (thumbs-up, frozen)
|
||||||
|
var _cursorPortal = null;
|
||||||
|
|
||||||
function getCsrf() {
|
function getCsrf() {
|
||||||
var m = document.cookie.match(/csrftoken=([^;]+)/);
|
var m = document.cookie.match(/csrftoken=([^;]+)/);
|
||||||
return m ? m[1] : '';
|
return m ? m[1] : '';
|
||||||
@@ -20,8 +29,60 @@ var SigSelect = (function () {
|
|||||||
|
|
||||||
// ── Stage ──────────────────────────────────────────────────────────────
|
// ── Stage ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function _populateKeywordList(listEl, csv) {
|
||||||
|
var keywords = csv ? csv.split(',').filter(Boolean) : [];
|
||||||
|
listEl.innerHTML = keywords.map(function (k) {
|
||||||
|
return '<li>' + k.trim() + '</li>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Caution tooltip ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function _renderCaution() {
|
||||||
|
if (_cautionData.length === 0) {
|
||||||
|
cautionEffect.innerHTML = '<em>Rival interactions pending.</em>';
|
||||||
|
cautionPrev.disabled = true;
|
||||||
|
cautionNext.disabled = true;
|
||||||
|
cautionIndexEl.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cautionEffect.innerHTML = _cautionData[_cautionIdx];
|
||||||
|
cautionPrev.disabled = (_cautionData.length <= 1);
|
||||||
|
cautionNext.disabled = (_cautionData.length <= 1);
|
||||||
|
cautionIndexEl.textContent = _cautionData.length > 1
|
||||||
|
? (_cautionIdx + 1) + ' / ' + _cautionData.length
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _openCaution() {
|
||||||
|
if (!_focusedCardEl) return;
|
||||||
|
try {
|
||||||
|
_cautionData = JSON.parse(_focusedCardEl.dataset.cautions || '[]');
|
||||||
|
} catch (e) {
|
||||||
|
_cautionData = [];
|
||||||
|
}
|
||||||
|
_cautionIdx = 0;
|
||||||
|
_renderCaution();
|
||||||
|
_flipBtn.classList.add('btn-disabled');
|
||||||
|
_cautionBtn.classList.add('btn-disabled');
|
||||||
|
_flipBtn.textContent = '\u00D7';
|
||||||
|
_cautionBtn.textContent = '\u00D7';
|
||||||
|
stage.classList.add('sig-caution-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _closeCaution() {
|
||||||
|
stage.classList.remove('sig-caution-open');
|
||||||
|
if (_flipBtn) {
|
||||||
|
_flipBtn.classList.remove('btn-disabled');
|
||||||
|
_cautionBtn.classList.remove('btn-disabled');
|
||||||
|
_flipBtn.textContent = _flipOrigLabel;
|
||||||
|
_cautionBtn.textContent = _cautionOrigLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateStage(cardEl) {
|
function updateStage(cardEl) {
|
||||||
if (_stageFrozen) return;
|
if (_stageFrozen) return;
|
||||||
|
_closeCaution();
|
||||||
if (!cardEl) {
|
if (!cardEl) {
|
||||||
stageCard.style.display = 'none';
|
stageCard.style.display = 'none';
|
||||||
stage.classList.remove('sig-stage--active');
|
stage.classList.remove('sig-stage--active');
|
||||||
@@ -47,9 +108,26 @@ var SigSelect = (function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
stageCard.querySelector('.fan-card-name-group').textContent = group;
|
stageCard.querySelector('.fan-card-name-group').textContent = group;
|
||||||
stageCard.querySelector('.fan-card-name').textContent = title;
|
|
||||||
stageCard.querySelector('.fan-card-arcana').textContent = arcana;
|
stageCard.querySelector('.fan-card-arcana').textContent = arcana;
|
||||||
stageCard.querySelector('.fan-card-correspondence').textContent = corr;
|
stageCard.querySelector('.fan-card-correspondence').textContent = ''; // shown in game-kit only
|
||||||
|
|
||||||
|
var qualifier = userPolarity === 'levity' ? 'Leavened' : 'Graven';
|
||||||
|
var isMajor = arcana.toLowerCase().indexOf('major') !== -1;
|
||||||
|
// Major arcana: qualifier sits below the title — append comma so it reads as a subtitle.
|
||||||
|
stageCard.querySelector('.fan-card-name').textContent = isMajor ? title + ',' : title;
|
||||||
|
stageCard.querySelector('.sig-qualifier-above').textContent = isMajor ? '' : qualifier;
|
||||||
|
stageCard.querySelector('.sig-qualifier-below').textContent = isMajor ? qualifier : '';
|
||||||
|
|
||||||
|
// Populate stat block keyword faces and reset to upright
|
||||||
|
statBlock.classList.remove('is-reversed');
|
||||||
|
_populateKeywordList(
|
||||||
|
statBlock.querySelector('#id_stat_keywords_upright'),
|
||||||
|
cardEl.dataset.keywordsUpright
|
||||||
|
);
|
||||||
|
_populateKeywordList(
|
||||||
|
statBlock.querySelector('#id_stat_keywords_reversed'),
|
||||||
|
cardEl.dataset.keywordsReversed
|
||||||
|
);
|
||||||
|
|
||||||
stageCard.style.display = '';
|
stageCard.style.display = '';
|
||||||
stage.classList.add('sig-stage--active');
|
stage.classList.add('sig-stage--active');
|
||||||
@@ -110,6 +188,38 @@ var SigSelect = (function () {
|
|||||||
|
|
||||||
// ── Apply reservation state (local + from WS) ─────────────────────────
|
// ── Apply reservation state (local + from WS) ─────────────────────────
|
||||||
|
|
||||||
|
function _placeReservedFloat(cardId, cardEl, role) {
|
||||||
|
// Remove any pre-existing reserved float for this role (e.g. page-load replay)
|
||||||
|
if (_reservedFloats[role]) { _reservedFloats[role].remove(); }
|
||||||
|
|
||||||
|
// Retire ALL hover floats for this role — may be on a different card than reserved
|
||||||
|
var roles = POLARITY_ROLES[userPolarity] || [];
|
||||||
|
var idx = roles.indexOf(role);
|
||||||
|
var posClass = ['--left', '--mid', '--right'][idx] || '--left';
|
||||||
|
Object.keys(_floatingCursors).forEach(function (key) {
|
||||||
|
if (key.slice(-posClass.length) === posClass) {
|
||||||
|
_floatingCursors[key].remove();
|
||||||
|
var hCid = key.slice(0, key.length - posClass.length);
|
||||||
|
var hEl = deckGrid.querySelector('.sig-card[data-card-id="' + hCid + '"]');
|
||||||
|
if (hEl) {
|
||||||
|
var a = hEl.querySelector('.sig-cursor' + posClass);
|
||||||
|
if (a) a.classList.remove('active');
|
||||||
|
}
|
||||||
|
delete _floatingCursors[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var rect = cardEl.getBoundingClientRect();
|
||||||
|
var xFractions = [0.15, 0.5, 0.85];
|
||||||
|
var fc = document.createElement('i');
|
||||||
|
fc.className = 'fa-solid fa-thumbs-up sig-cursor-float sig-cursor-float--reserved';
|
||||||
|
fc.dataset.role = role;
|
||||||
|
fc.style.left = (rect.left + rect.width * xFractions[idx < 0 ? 1 : idx]) + 'px';
|
||||||
|
fc.style.top = rect.bottom + 'px';
|
||||||
|
_ensureCursorPortal().appendChild(fc);
|
||||||
|
_reservedFloats[role] = fc;
|
||||||
|
}
|
||||||
|
|
||||||
function applyReservation(cardId, role, reserved) {
|
function applyReservation(cardId, role, reserved) {
|
||||||
var cardEl = deckGrid.querySelector('.sig-card[data-card-id="' + cardId + '"]');
|
var cardEl = deckGrid.querySelector('.sig-card[data-card-id="' + cardId + '"]');
|
||||||
if (!cardEl) return;
|
if (!cardEl) return;
|
||||||
@@ -127,6 +237,8 @@ var SigSelect = (function () {
|
|||||||
_stageFrozen = true;
|
_stageFrozen = true;
|
||||||
stage.classList.add('sig-stage--frozen');
|
stage.classList.add('sig-stage--frozen');
|
||||||
}
|
}
|
||||||
|
// Thumbs-up float for all reservations — own role sees their own indicator too
|
||||||
|
_placeReservedFloat(cardId, cardEl, role);
|
||||||
} else {
|
} else {
|
||||||
delete cardEl.dataset.reservedBy;
|
delete cardEl.dataset.reservedBy;
|
||||||
cardEl.classList.remove('sig-reserved', 'sig-reserved--own');
|
cardEl.classList.remove('sig-reserved', 'sig-reserved--own');
|
||||||
@@ -135,26 +247,65 @@ var SigSelect = (function () {
|
|||||||
_stageFrozen = false;
|
_stageFrozen = false;
|
||||||
stage.classList.remove('sig-stage--frozen');
|
stage.classList.remove('sig-stage--frozen');
|
||||||
}
|
}
|
||||||
|
// Remove thumbs-up float for all releases — own role included
|
||||||
|
if (_reservedFloats[role]) {
|
||||||
|
_reservedFloats[role].remove();
|
||||||
|
delete _reservedFloats[role];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Apply hover cursor (WS only — own hover is CSS :hover) ────────────
|
// ── Apply hover cursor (WS only — own hover is CSS :hover) ────────────
|
||||||
|
//
|
||||||
|
// Cursor icons are portaled to document root so they escape overflow/clip
|
||||||
|
// contexts in the deck grid. The in-card anchor elements only carry the
|
||||||
|
// .active class (for test assertions and the :has() z-index rule).
|
||||||
|
|
||||||
|
function _ensureCursorPortal() {
|
||||||
|
if (!_cursorPortal || !document.body.contains(_cursorPortal)) {
|
||||||
|
_cursorPortal = document.getElementById('id_sig_cursor_portal');
|
||||||
|
if (!_cursorPortal) {
|
||||||
|
_cursorPortal = document.createElement('div');
|
||||||
|
_cursorPortal.id = 'id_sig_cursor_portal';
|
||||||
|
document.body.appendChild(_cursorPortal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _cursorPortal;
|
||||||
|
}
|
||||||
|
|
||||||
function applyHover(cardId, role, active) {
|
function applyHover(cardId, role, active) {
|
||||||
if (role === userRole) return;
|
if (role === userRole) return;
|
||||||
|
if (_reservedFloats[role]) return; // role already has thumbs-up — ignore hover
|
||||||
var cardEl = deckGrid.querySelector('.sig-card[data-card-id="' + cardId + '"]');
|
var cardEl = deckGrid.querySelector('.sig-card[data-card-id="' + cardId + '"]');
|
||||||
if (!cardEl) return;
|
if (!cardEl) return;
|
||||||
|
|
||||||
var roles = POLARITY_ROLES[userPolarity] || [];
|
var roles = POLARITY_ROLES[userPolarity] || [];
|
||||||
var idx = roles.indexOf(role);
|
var idx = roles.indexOf(role);
|
||||||
var posClass = ['--left', '--mid', '--right'][idx] || '--left';
|
var posClass = ['--left', '--mid', '--right'][idx] || '--left';
|
||||||
var cursor = cardEl.querySelector('.sig-cursor' + posClass);
|
var anchor = cardEl.querySelector('.sig-cursor' + posClass);
|
||||||
if (!cursor) return;
|
if (!anchor) return;
|
||||||
|
|
||||||
|
var key = cardId + posClass;
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
cursor.classList.add('active');
|
anchor.classList.add('active'); // kept for test assertions + :has() z-index
|
||||||
|
|
||||||
|
// Place a fixed-position clone in the portal, positioned from card bounds
|
||||||
|
var rect = cardEl.getBoundingClientRect();
|
||||||
|
var xFractions = [0.15, 0.5, 0.85];
|
||||||
|
var fc = document.createElement('i');
|
||||||
|
fc.className = 'fa-solid fa-hand-pointer sig-cursor-float';
|
||||||
|
fc.dataset.role = role;
|
||||||
|
fc.style.left = (rect.left + rect.width * xFractions[idx]) + 'px';
|
||||||
|
fc.style.top = rect.bottom + 'px';
|
||||||
|
_ensureCursorPortal().appendChild(fc);
|
||||||
|
_floatingCursors[key] = fc;
|
||||||
} else {
|
} else {
|
||||||
cursor.classList.remove('active');
|
anchor.classList.remove('active');
|
||||||
|
if (_floatingCursors[key]) {
|
||||||
|
_floatingCursors[key].remove();
|
||||||
|
delete _floatingCursors[key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,17 +339,64 @@ var SigSelect = (function () {
|
|||||||
deckGrid = overlay.querySelector('.sig-deck-grid');
|
deckGrid = overlay.querySelector('.sig-deck-grid');
|
||||||
stage = overlay.querySelector('.sig-stage');
|
stage = overlay.querySelector('.sig-stage');
|
||||||
stageCard = stage.querySelector('.sig-stage-card');
|
stageCard = stage.querySelector('.sig-stage-card');
|
||||||
|
statBlock = stage.querySelector('.sig-stat-block');
|
||||||
|
|
||||||
|
_flipBtn = statBlock.querySelector('.sig-flip-btn');
|
||||||
|
_cautionBtn = statBlock.querySelector('.sig-caution-btn');
|
||||||
|
_flipOrigLabel = _flipBtn.textContent;
|
||||||
|
_cautionOrigLabel = _cautionBtn.textContent;
|
||||||
|
|
||||||
|
_flipBtn.addEventListener('click', function () {
|
||||||
|
if (_flipBtn.classList.contains('btn-disabled')) return;
|
||||||
|
statBlock.classList.toggle('is-reversed');
|
||||||
|
});
|
||||||
|
|
||||||
|
cautionEl = stage.querySelector('.sig-caution-tooltip');
|
||||||
|
cautionEffect = cautionEl.querySelector('.sig-caution-effect');
|
||||||
|
cautionPrev = statBlock.querySelector('.sig-caution-prev');
|
||||||
|
cautionNext = statBlock.querySelector('.sig-caution-next');
|
||||||
|
cautionIndexEl = cautionEl.querySelector('.sig-caution-index');
|
||||||
|
|
||||||
|
// Clicking the tooltip (not nav buttons) dismisses it
|
||||||
|
cautionEl.addEventListener('click', function () {
|
||||||
|
_closeCaution();
|
||||||
|
});
|
||||||
|
|
||||||
|
_cautionBtn.addEventListener('click', function () {
|
||||||
|
if (_cautionBtn.classList.contains('btn-disabled')) return;
|
||||||
|
stage.classList.contains('sig-caution-open') ? _closeCaution() : _openCaution();
|
||||||
|
});
|
||||||
|
cautionPrev.addEventListener('click', function () {
|
||||||
|
_cautionIdx = (_cautionIdx - 1 + _cautionData.length) % _cautionData.length;
|
||||||
|
_renderCaution();
|
||||||
|
});
|
||||||
|
cautionNext.addEventListener('click', function () {
|
||||||
|
_cautionIdx = (_cautionIdx + 1) % _cautionData.length;
|
||||||
|
_renderCaution();
|
||||||
|
});
|
||||||
|
|
||||||
reserveUrl = overlay.dataset.reserveUrl;
|
reserveUrl = overlay.dataset.reserveUrl;
|
||||||
userRole = overlay.dataset.userRole;
|
userRole = overlay.dataset.userRole;
|
||||||
userPolarity= overlay.dataset.polarity;
|
userPolarity= overlay.dataset.polarity;
|
||||||
|
|
||||||
// Restore reservations from server-rendered JSON (page-load state)
|
// Restore reservations from server-rendered JSON (page-load state).
|
||||||
|
// Deferred to 'load' so sizeSigModal() (also a 'load' listener, registered
|
||||||
|
// in room.js before this script) has already applied paddingBottom and
|
||||||
|
// --sig-card-w before _placeReservedFloat calls getBoundingClientRect().
|
||||||
try {
|
try {
|
||||||
var existing = JSON.parse(overlay.dataset.reservations || '{}');
|
var existing = JSON.parse(overlay.dataset.reservations || '{}');
|
||||||
Object.keys(existing).forEach(function (cardId) {
|
if (Object.keys(existing).length) {
|
||||||
applyReservation(cardId, existing[cardId], true);
|
var _replayReservations = function () {
|
||||||
});
|
Object.keys(existing).forEach(function (cardId) {
|
||||||
|
applyReservation(cardId, existing[cardId], true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
_replayReservations();
|
||||||
|
} else {
|
||||||
|
window.addEventListener('load', _replayReservations, { once: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) { /* malformed JSON — ignore */ }
|
} catch (e) { /* malformed JSON — ignore */ }
|
||||||
|
|
||||||
// Hover: update stage preview + broadcast cursor
|
// Hover: update stage preview + broadcast cursor
|
||||||
@@ -264,6 +462,13 @@ var SigSelect = (function () {
|
|||||||
_reservedCardId = null;
|
_reservedCardId = null;
|
||||||
_stageFrozen = false;
|
_stageFrozen = false;
|
||||||
_requestInFlight = false;
|
_requestInFlight = false;
|
||||||
|
_cautionData = [];
|
||||||
|
_cautionIdx = 0;
|
||||||
|
Object.keys(_floatingCursors).forEach(function (k) { _floatingCursors[k].remove(); });
|
||||||
|
_floatingCursors = {};
|
||||||
|
Object.keys(_reservedFloats).forEach(function (k) { _reservedFloats[k].remove(); });
|
||||||
|
_reservedFloats = {};
|
||||||
|
_cursorPortal = null;
|
||||||
init();
|
init();
|
||||||
},
|
},
|
||||||
_setFrozen: function (v) { _stageFrozen = v; },
|
_setFrozen: function (v) { _stageFrozen = v; },
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ var Tray = (function () {
|
|||||||
var _tray = null;
|
var _tray = null;
|
||||||
var _grid = null;
|
var _grid = null;
|
||||||
|
|
||||||
|
// Role code → scrawl SVG name mapping for tray card display.
|
||||||
|
var _ROLE_SCRAWL = {
|
||||||
|
PC: 'Player', NC: 'Narrator', EC: 'Economist',
|
||||||
|
SC: 'Shepherd', AC: 'Alchemist', BC: 'Builder'
|
||||||
|
};
|
||||||
|
var _roleIconsUrl = null;
|
||||||
|
|
||||||
// Portrait bounds (X axis)
|
// Portrait bounds (X axis)
|
||||||
var _minLeft = 0;
|
var _minLeft = 0;
|
||||||
var _maxLeft = 0;
|
var _maxLeft = 0;
|
||||||
@@ -94,12 +101,7 @@ var Tray = (function () {
|
|||||||
// Closed: tray hidden above viewport, handle visible at y=0.
|
// Closed: tray hidden above viewport, handle visible at y=0.
|
||||||
_maxTop = -(gearBtnTop - handleH);
|
_maxTop = -(gearBtnTop - handleH);
|
||||||
} else {
|
} else {
|
||||||
// Portrait: slide on X axis.
|
// Portrait: wrap width = full viewport; handle parks at right edge.
|
||||||
// Wrap width is pinned to viewportW (JS) so its right edge only
|
|
||||||
// reaches the viewport boundary when left = 0 (fully open).
|
|
||||||
// This mirrors landscape: the open edge appears only at the last moment.
|
|
||||||
// Open: left = 0 → wrap right = viewportW exactly.
|
|
||||||
// Closed: left = viewportW - handleW → tray fully off-screen right.
|
|
||||||
var handleW = _btn.offsetWidth || 48;
|
var handleW = _btn.offsetWidth || 48;
|
||||||
if (_wrap) _wrap.style.width = window.innerWidth + 'px';
|
if (_wrap) _wrap.style.width = window.innerWidth + 'px';
|
||||||
_minLeft = 0;
|
_minLeft = 0;
|
||||||
@@ -251,7 +253,13 @@ var Tray = (function () {
|
|||||||
|
|
||||||
firstCell.classList.add('tray-role-card');
|
firstCell.classList.add('tray-role-card');
|
||||||
firstCell.dataset.role = roleCode;
|
firstCell.dataset.role = roleCode;
|
||||||
firstCell.textContent = roleCode;
|
firstCell.textContent = '';
|
||||||
|
if (_roleIconsUrl) {
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.src = _roleIconsUrl + 'starter-role-' + (_ROLE_SCRAWL[roleCode] || 'Blank') + '.svg';
|
||||||
|
img.alt = roleCode;
|
||||||
|
firstCell.appendChild(img);
|
||||||
|
}
|
||||||
|
|
||||||
open();
|
open();
|
||||||
_arcIn(firstCell, function () {
|
_arcIn(firstCell, function () {
|
||||||
@@ -313,7 +321,7 @@ var Tray = (function () {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (_tray) _tray.style.display = 'none';
|
if (_tray) _tray.style.display = 'none';
|
||||||
if (_wrap) { _wrap.style.top = ''; _wrap.style.height = ''; }
|
if (_wrap) { _wrap.style.top = ''; _wrap.style.height = ''; _wrap.style.width = ''; }
|
||||||
_computeBounds();
|
_computeBounds();
|
||||||
_applyVerticalBounds();
|
_applyVerticalBounds();
|
||||||
_computeCellSize();
|
_computeCellSize();
|
||||||
@@ -331,6 +339,7 @@ var Tray = (function () {
|
|||||||
_btn = document.getElementById('id_tray_btn');
|
_btn = document.getElementById('id_tray_btn');
|
||||||
_tray = document.getElementById('id_tray');
|
_tray = document.getElementById('id_tray');
|
||||||
_grid = document.getElementById('id_tray_grid');
|
_grid = document.getElementById('id_tray_grid');
|
||||||
|
_roleIconsUrl = (_grid && _grid.dataset.roleIconsUrl) || null;
|
||||||
if (!_btn) return;
|
if (!_btn) return;
|
||||||
|
|
||||||
if (_isLandscape()) {
|
if (_isLandscape()) {
|
||||||
@@ -342,8 +351,8 @@ var Tray = (function () {
|
|||||||
if (_wrap) _wrap.style.top = _maxTop + 'px';
|
if (_wrap) _wrap.style.top = _maxTop + 'px';
|
||||||
_computeCellSize();
|
_computeCellSize();
|
||||||
} else {
|
} else {
|
||||||
// Clear landscape's inline top so portrait CSS applies.
|
// Clear landscape's inline top/height/width so portrait CSS applies.
|
||||||
if (_wrap) _wrap.style.top = '';
|
if (_wrap) { _wrap.style.top = ''; _wrap.style.width = ''; }
|
||||||
_applyVerticalBounds();
|
_applyVerticalBounds();
|
||||||
_computeCellSize(); // wrap has correct height after _applyVerticalBounds
|
_computeCellSize(); // wrap has correct height after _applyVerticalBounds
|
||||||
_computeBounds();
|
_computeBounds();
|
||||||
|
|||||||
@@ -269,16 +269,16 @@ class SigDeckCompositionTest(TestCase):
|
|||||||
cards = sig_deck_cards(self.room)
|
cards = sig_deck_cards(self.room)
|
||||||
self.assertEqual(len(cards), 36)
|
self.assertEqual(len(cards), 36)
|
||||||
|
|
||||||
def test_sc_ac_contribute_court_cards_of_swords_and_cups(self):
|
def test_sc_ac_contribute_court_cards_of_blades_and_grails(self):
|
||||||
cards = sig_deck_cards(self.room)
|
cards = sig_deck_cards(self.room)
|
||||||
sc_ac = [c for c in cards if c.suit in ("SWORDS", "CUPS")]
|
sc_ac = [c for c in cards if c.suit in ("BLADES", "GRAILS")]
|
||||||
# M/J/Q/K × 2 suits × 2 roles = 16
|
# M/J/Q/K × 2 suits × 2 roles = 16
|
||||||
self.assertEqual(len(sc_ac), 16)
|
self.assertEqual(len(sc_ac), 16)
|
||||||
self.assertTrue(all(c.number in (11, 12, 13, 14) for c in sc_ac))
|
self.assertTrue(all(c.number in (11, 12, 13, 14) for c in sc_ac))
|
||||||
|
|
||||||
def test_pc_bc_contribute_court_cards_of_wands_and_crowns(self):
|
def test_pc_bc_contribute_court_cards_of_brands_and_crowns(self):
|
||||||
cards = sig_deck_cards(self.room)
|
cards = sig_deck_cards(self.room)
|
||||||
pc_bc = [c for c in cards if c.suit in ("WANDS", "CROWNS")]
|
pc_bc = [c for c in cards if c.suit in ("BRANDS", "CROWNS")]
|
||||||
self.assertEqual(len(pc_bc), 16)
|
self.assertEqual(len(pc_bc), 16)
|
||||||
self.assertTrue(all(c.number in (11, 12, 13, 14) for c in pc_bc))
|
self.assertTrue(all(c.number in (11, 12, 13, 14) for c in pc_bc))
|
||||||
|
|
||||||
@@ -342,7 +342,7 @@ class SigCardFieldTest(TestCase):
|
|||||||
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
||||||
)
|
)
|
||||||
self.card = TarotCard.objects.get(
|
self.card = TarotCard.objects.get(
|
||||||
deck_variant=earthman, arcana="MIDDLE", suit="WANDS", number=11,
|
deck_variant=earthman, arcana="MIDDLE", suit="BRANDS", number=11,
|
||||||
)
|
)
|
||||||
owner = User.objects.create(email="owner@test.io")
|
owner = User.objects.create(email="owner@test.io")
|
||||||
room = Room.objects.create(name="Field Test", owner=owner)
|
room = Room.objects.create(name="Field Test", owner=owner)
|
||||||
@@ -479,3 +479,54 @@ class SigCardHelperTest(TestCase):
|
|||||||
self.owner.save()
|
self.owner.save()
|
||||||
self.assertEqual(levity_sig_cards(self.room), [])
|
self.assertEqual(levity_sig_cards(self.room), [])
|
||||||
self.assertEqual(gravity_sig_cards(self.room), [])
|
self.assertEqual(gravity_sig_cards(self.room), [])
|
||||||
|
|
||||||
|
|
||||||
|
class TarotCardCautionsTest(TestCase):
|
||||||
|
"""TarotCard.cautions JSONField — field existence and Schizo seed data."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.earthman = DeckVariant.objects.get(slug="earthman")
|
||||||
|
|
||||||
|
def test_cautions_field_saves_and_retrieves_list(self):
|
||||||
|
card = TarotCard.objects.create(
|
||||||
|
deck_variant=self.earthman,
|
||||||
|
arcana="MINOR",
|
||||||
|
suit="CROWNS",
|
||||||
|
number=99,
|
||||||
|
name="Test Card",
|
||||||
|
slug="test-card-cautions",
|
||||||
|
cautions=["First caution.", "Second caution."],
|
||||||
|
)
|
||||||
|
card.refresh_from_db()
|
||||||
|
self.assertEqual(card.cautions, ["First caution.", "Second caution."])
|
||||||
|
|
||||||
|
def test_cautions_defaults_to_empty_list(self):
|
||||||
|
card = TarotCard.objects.create(
|
||||||
|
deck_variant=self.earthman,
|
||||||
|
arcana="MINOR",
|
||||||
|
suit="CROWNS",
|
||||||
|
number=98,
|
||||||
|
name="Default Cautions Card",
|
||||||
|
slug="default-cautions-card",
|
||||||
|
)
|
||||||
|
self.assertEqual(card.cautions, [])
|
||||||
|
|
||||||
|
def test_schizo_has_4_cautions(self):
|
||||||
|
schizo = TarotCard.objects.get(
|
||||||
|
deck_variant=self.earthman, arcana="MAJOR", number=1
|
||||||
|
)
|
||||||
|
self.assertEqual(len(schizo.cautions), 4)
|
||||||
|
|
||||||
|
def test_schizo_caution_references_the_pervert(self):
|
||||||
|
schizo = TarotCard.objects.get(
|
||||||
|
deck_variant=self.earthman, arcana="MAJOR", number=1
|
||||||
|
)
|
||||||
|
self.assertIn("The Pervert", schizo.cautions[0])
|
||||||
|
|
||||||
|
def test_schizo_cautions_use_reverse_language(self):
|
||||||
|
schizo = TarotCard.objects.get(
|
||||||
|
deck_variant=self.earthman, arcana="MAJOR", number=1
|
||||||
|
)
|
||||||
|
for caution in schizo.cautions:
|
||||||
|
self.assertIn("reverse", caution)
|
||||||
|
self.assertNotIn("transform", caution)
|
||||||
|
|||||||
@@ -926,7 +926,7 @@ def _full_sig_setUp(test_case, role_order=None):
|
|||||||
room.table_status = Room.SIG_SELECT
|
room.table_status = Room.SIG_SELECT
|
||||||
room.save()
|
room.save()
|
||||||
card_in_deck = TarotCard.objects.get(
|
card_in_deck = TarotCard.objects.get(
|
||||||
deck_variant=earthman, arcana="MIDDLE", suit="WANDS", number=11
|
deck_variant=earthman, arcana="MIDDLE", suit="BRANDS", number=11
|
||||||
)
|
)
|
||||||
test_case.client.force_login(founder)
|
test_case.client.force_login(founder)
|
||||||
return room, gamers, earthman, card_in_deck
|
return room, gamers, earthman, card_in_deck
|
||||||
@@ -963,6 +963,32 @@ class SigSelectRenderingTest(TestCase):
|
|||||||
response = self.client.get(self.url)
|
response = self.client.get(self.url)
|
||||||
self.assertNotContains(response, "id_sig_deck")
|
self.assertNotContains(response, "id_sig_deck")
|
||||||
|
|
||||||
|
def test_sig_cards_render_keyword_data_attributes(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
content = response.content.decode()
|
||||||
|
self.assertIn("data-keywords-upright=", content)
|
||||||
|
self.assertIn("data-keywords-reversed=", content)
|
||||||
|
|
||||||
|
def test_sig_stat_block_structure_rendered(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertContains(response, "sig-stat-block")
|
||||||
|
self.assertContains(response, "sig-flip-btn")
|
||||||
|
self.assertContains(response, "stat-face--upright")
|
||||||
|
self.assertContains(response, "stat-face--reversed")
|
||||||
|
|
||||||
|
def test_sig_cards_render_cautions_data_attribute(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertContains(response, "data-cautions=")
|
||||||
|
|
||||||
|
def test_sig_caution_tooltip_structure_rendered(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
self.assertContains(response, "sig-caution-tooltip")
|
||||||
|
self.assertContains(response, "sig-caution-btn")
|
||||||
|
self.assertContains(response, "sig-caution-effect")
|
||||||
|
self.assertContains(response, "sig-caution-index")
|
||||||
|
self.assertContains(response, "sig-caution-prev")
|
||||||
|
self.assertContains(response, "sig-caution-next")
|
||||||
|
|
||||||
|
|
||||||
class SelectSigCardViewTest(TestCase):
|
class SelectSigCardViewTest(TestCase):
|
||||||
"""select_sig view — records choice, enforces turn order, rejects bad input."""
|
"""select_sig view — records choice, enforces turn order, rejects bad input."""
|
||||||
@@ -1000,8 +1026,8 @@ class SelectSigCardViewTest(TestCase):
|
|||||||
def test_select_sig_card_not_in_deck_returns_400(self):
|
def test_select_sig_card_not_in_deck_returns_400(self):
|
||||||
# Create a pip card (number=5) — not in the sig deck (only court 11–14 + major 0–1)
|
# Create a pip card (number=5) — not in the sig deck (only court 11–14 + major 0–1)
|
||||||
other = TarotCard.objects.create(
|
other = TarotCard.objects.create(
|
||||||
deck_variant=self.earthman, arcana="MINOR", suit="WANDS", number=5,
|
deck_variant=self.earthman, arcana="MINOR", suit="BRANDS", number=5,
|
||||||
name="Five of Wands Test", slug="five-of-wands-test",
|
name="Five of Brands Test", slug="five-of-brands-test",
|
||||||
keywords_upright=[], keywords_reversed=[],
|
keywords_upright=[], keywords_reversed=[],
|
||||||
)
|
)
|
||||||
response = self._post(card_id=other.id)
|
response = self._post(card_id=other.id)
|
||||||
@@ -1188,7 +1214,7 @@ class SigReserveViewTest(TestCase):
|
|||||||
def test_reserve_different_card_while_holding_returns_409(self):
|
def test_reserve_different_card_while_holding_returns_409(self):
|
||||||
"""Cannot OK a different card while holding one — must NVM first."""
|
"""Cannot OK a different card while holding one — must NVM first."""
|
||||||
card_b = TarotCard.objects.filter(
|
card_b = TarotCard.objects.filter(
|
||||||
deck_variant=self.earthman, arcana="MIDDLE", suit="WANDS", number=12
|
deck_variant=self.earthman, arcana="MIDDLE", suit="BRANDS", number=12
|
||||||
).first()
|
).first()
|
||||||
self._reserve() # PC grabs card A → 200
|
self._reserve() # PC grabs card A → 200
|
||||||
response = self._reserve(card_id=card_b.id) # tries card B → 409
|
response = self._reserve(card_id=card_b.id) # tries card B → 409
|
||||||
@@ -1210,7 +1236,7 @@ class SigReserveViewTest(TestCase):
|
|||||||
def test_reserve_blocked_then_unblocked_after_release(self):
|
def test_reserve_blocked_then_unblocked_after_release(self):
|
||||||
"""After NVM, a new card can be OK'd."""
|
"""After NVM, a new card can be OK'd."""
|
||||||
card_b = TarotCard.objects.filter(
|
card_b = TarotCard.objects.filter(
|
||||||
deck_variant=self.earthman, arcana="MIDDLE", suit="WANDS", number=12
|
deck_variant=self.earthman, arcana="MIDDLE", suit="BRANDS", number=12
|
||||||
).first()
|
).first()
|
||||||
self._reserve() # hold card A
|
self._reserve() # hold card A
|
||||||
self._reserve(action="release") # NVM
|
self._reserve(action="release") # NVM
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ from django.shortcuts import redirect, render
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from apps.drama.models import GameEvent, record
|
from apps.drama.models import GameEvent, record
|
||||||
|
from django.db.models import Case, IntegerField, Value, When
|
||||||
|
|
||||||
from apps.epic.models import (
|
from apps.epic.models import (
|
||||||
GateSlot, Room, RoomInvite, SigReservation, TableSeat, TarotCard, TarotDeck,
|
GateSlot, Room, RoomInvite, SIG_SEAT_ORDER, SigReservation, TableSeat,
|
||||||
|
TarotCard, TarotDeck,
|
||||||
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,
|
||||||
)
|
)
|
||||||
@@ -92,6 +95,27 @@ def _notify_sig_reserved(room_id, card_id, role, reserved):
|
|||||||
|
|
||||||
SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
|
SLOT_ROLE_LABELS = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
|
||||||
|
|
||||||
|
_SIG_SEAT_ORDERING = Case(
|
||||||
|
*[When(role=r, then=Value(i)) for i, r in enumerate(SIG_SEAT_ORDER)],
|
||||||
|
default=Value(99),
|
||||||
|
output_field=IntegerField(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_user_seat(room, user):
|
||||||
|
"""Return the user's seat whose role comes first in PC→NC→EC→SC→AC→BC order.
|
||||||
|
|
||||||
|
In normal play (one user = one seat) this is equivalent to .first().
|
||||||
|
For Carte Blanche (one user = all seats) it returns the PC seat, ensuring
|
||||||
|
sig-select cursor placement is seat-based, not position/slot-based.
|
||||||
|
"""
|
||||||
|
return room.table_seats.filter(gamer=user).order_by(_SIG_SEAT_ORDERING).first()
|
||||||
|
|
||||||
|
_ROLE_SCRAWL_NAMES = {
|
||||||
|
"PC": "Player", "NC": "Narrator", "EC": "Economist",
|
||||||
|
"SC": "Shepherd", "AC": "Alchemist", "BC": "Builder",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _gate_positions(room):
|
def _gate_positions(room):
|
||||||
"""Return list of dicts [{slot, role_label, role_assigned}] for _table_positions.html."""
|
"""Return list of dicts [{slot, role_label, role_assigned}] for _table_positions.html."""
|
||||||
@@ -216,10 +240,16 @@ def _role_select_context(room, user):
|
|||||||
if user.is_authenticated else []
|
if user.is_authenticated else []
|
||||||
)
|
)
|
||||||
active_slot = active_seat.slot_number if active_seat else None
|
active_slot = active_seat.slot_number if active_seat else None
|
||||||
|
_my_role = assigned_seats[0].role if assigned_seats else None
|
||||||
ctx = {
|
ctx = {
|
||||||
"card_stack_state": card_stack_state,
|
"card_stack_state": card_stack_state,
|
||||||
"starter_roles": starter_roles,
|
"starter_roles": starter_roles,
|
||||||
"assigned_seats": assigned_seats,
|
"assigned_seats": assigned_seats,
|
||||||
|
"my_tray_role": _my_role,
|
||||||
|
"my_tray_scrawl_static_path": (
|
||||||
|
f"apps/epic/icons/cards-roles/starter-role-{_ROLE_SCRAWL_NAMES[_my_role]}.svg"
|
||||||
|
if _my_role else None
|
||||||
|
),
|
||||||
"user_seat": user_seat,
|
"user_seat": user_seat,
|
||||||
"user_slots": list(
|
"user_slots": list(
|
||||||
room.table_seats.filter(gamer=user, role__isnull=True)
|
room.table_seats.filter(gamer=user, role__isnull=True)
|
||||||
@@ -231,7 +261,7 @@ def _role_select_context(room, user):
|
|||||||
"slots": room.gate_slots.order_by("slot_number"),
|
"slots": room.gate_slots.order_by("slot_number"),
|
||||||
}
|
}
|
||||||
if room.table_status == Room.SIG_SELECT:
|
if room.table_status == Room.SIG_SELECT:
|
||||||
user_seat = room.table_seats.filter(gamer=user).first() if user.is_authenticated else None
|
user_seat = _canonical_user_seat(room, user) if user.is_authenticated else None
|
||||||
user_role = user_seat.role if user_seat else None
|
user_role = user_seat.role if user_seat else None
|
||||||
user_polarity = None
|
user_polarity = None
|
||||||
if user_role in _LEVITY_ROLES:
|
if user_role in _LEVITY_ROLES:
|
||||||
@@ -572,7 +602,7 @@ def sig_reserve(request, room_id):
|
|||||||
if room.table_status != Room.SIG_SELECT:
|
if room.table_status != Room.SIG_SELECT:
|
||||||
return HttpResponse(status=400)
|
return HttpResponse(status=400)
|
||||||
|
|
||||||
user_seat = room.table_seats.filter(gamer=request.user).first()
|
user_seat = _canonical_user_seat(room, request.user)
|
||||||
if not user_seat or not user_seat.role:
|
if not user_seat or not user_seat.role:
|
||||||
return HttpResponse(status=403)
|
return HttpResponse(status=403)
|
||||||
|
|
||||||
@@ -611,7 +641,7 @@ def sig_reserve(request, room_id):
|
|||||||
|
|
||||||
SigReservation.objects.create(
|
SigReservation.objects.create(
|
||||||
room=room, gamer=request.user, card=card,
|
room=room, gamer=request.user, card=card,
|
||||||
role=user_seat.role, polarity=polarity,
|
seat=user_seat, role=user_seat.role, polarity=polarity,
|
||||||
)
|
)
|
||||||
_notify_sig_reserved(room_id, card.pk, user_seat.role, reserved=True)
|
_notify_sig_reserved(room_id, card.pk, user_seat.role, reserved=True)
|
||||||
return HttpResponse(status=200)
|
return HttpResponse(status=200)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from . import views as lyric_views
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('send_login_email', lyric_views.send_login_email, name='send_login_email'),
|
path('send_login_email', lyric_views.send_login_email, name='send_login_email'),
|
||||||
path('login', lyric_views.login, name='login'),
|
path('login', lyric_views.login, name='login'),
|
||||||
path('logout', auth_views.LogoutView.as_view(next_page='/'), name='logout')
|
path('logout', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
|
||||||
|
path('dev-login/<str:session_key>/', lyric_views.dev_login, name='dev_login'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
from django.conf import settings
|
||||||
from django.contrib import auth, messages
|
from django.contrib import auth, messages
|
||||||
|
from django.http import Http404
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
@@ -27,3 +29,13 @@ def login(request):
|
|||||||
else:
|
else:
|
||||||
messages.error(request, "Invalid login link!—please request another")
|
messages.error(request, "Invalid login link!—please request another")
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
|
||||||
|
|
||||||
|
def dev_login(request, session_key):
|
||||||
|
"""DEBUG-only: set session cookie and redirect. Used by setup_sig_session command."""
|
||||||
|
if not settings.DEBUG:
|
||||||
|
raise Http404
|
||||||
|
next_url = request.GET.get("next", "/")
|
||||||
|
response = redirect(next_url)
|
||||||
|
response.set_cookie(settings.SESSION_COOKIE_NAME, session_key, httponly=True)
|
||||||
|
return response
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ INSTALLED_APPS = [
|
|||||||
# Board apps
|
# Board apps
|
||||||
'apps.dashboard',
|
'apps.dashboard',
|
||||||
'apps.gameboard',
|
'apps.gameboard',
|
||||||
|
'apps.billboard',
|
||||||
# Gamer apps
|
# Gamer apps
|
||||||
'apps.lyric',
|
'apps.lyric',
|
||||||
'apps.epic',
|
'apps.epic',
|
||||||
'apps.drama',
|
'apps.drama',
|
||||||
'apps.billboard',
|
|
||||||
'apps.ap',
|
|
||||||
# Custom apps
|
# Custom apps
|
||||||
|
'apps.ap',
|
||||||
'apps.api',
|
'apps.api',
|
||||||
'apps.applets',
|
'apps.applets',
|
||||||
'functional_tests',
|
'functional_tests',
|
||||||
|
|||||||
128
src/functional_tests/management/commands/setup_sig_session.py
Normal file
128
src/functional_tests/management/commands/setup_sig_session.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
"""
|
||||||
|
Management command for manual multi-user sig-select testing.
|
||||||
|
|
||||||
|
Creates (or reuses) a room with all 6 gate slots filled, roles assigned,
|
||||||
|
and table_status=SIG_SELECT. Prints one pre-auth URL per gamer so you can
|
||||||
|
paste them into 6 Firefox Multi-Account Container tabs.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python src/manage.py setup_sig_session
|
||||||
|
python src/manage.py setup_sig_session --base-url http://localhost:8000
|
||||||
|
python src/manage.py setup_sig_session --room <uuid> # reuse existing room
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY
|
||||||
|
from django.contrib.sessions.backends.db import SessionStore
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from apps.epic.models import DeckVariant, GateSlot, Room, TableSeat, TarotCard
|
||||||
|
from apps.lyric.models import User
|
||||||
|
|
||||||
|
|
||||||
|
GAMERS = [
|
||||||
|
("founder@test.io", "discoman"),
|
||||||
|
("amigo@test.io", "amigo"),
|
||||||
|
("bud@test.io", "bud"),
|
||||||
|
("pal@test.io", "pal"),
|
||||||
|
("dude@test.io", "dude"),
|
||||||
|
("bro@test.io", "bro"),
|
||||||
|
]
|
||||||
|
|
||||||
|
ROLES = ["PC", "NC", "EC", "SC", "AC", "BC"]
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_earthman():
|
||||||
|
"""Return (or create) the Earthman DeckVariant with enough sig-deck cards seeded."""
|
||||||
|
earthman, _ = DeckVariant.objects.get_or_create(
|
||||||
|
slug="earthman",
|
||||||
|
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
||||||
|
)
|
||||||
|
_NAME = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
|
||||||
|
for suit in ("WANDS", "PENTACLES", "SWORDS", "CUPS"):
|
||||||
|
for number in (11, 12, 13, 14):
|
||||||
|
TarotCard.objects.get_or_create(
|
||||||
|
deck_variant=earthman,
|
||||||
|
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em",
|
||||||
|
defaults={
|
||||||
|
"arcana": "MINOR",
|
||||||
|
"suit": suit,
|
||||||
|
"number": number,
|
||||||
|
"name": f"{_NAME[number]} of {suit.capitalize()}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return earthman
|
||||||
|
|
||||||
|
|
||||||
|
def _make_session(user):
|
||||||
|
session = SessionStore()
|
||||||
|
session[SESSION_KEY] = str(user.pk)
|
||||||
|
session[BACKEND_SESSION_KEY] = "apps.lyric.authentication.PasswordlessAuthenticationBackend"
|
||||||
|
session[HASH_SESSION_KEY] = user.get_session_auth_hash()
|
||||||
|
session.save()
|
||||||
|
return session.session_key
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Set up a SIG_SELECT room and print pre-auth URLs for all six gamers"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("--base-url", default="http://localhost:8000")
|
||||||
|
parser.add_argument("--room", default=None, help="UUID of an existing room to reuse")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
base_url = options["base_url"].rstrip("/")
|
||||||
|
earthman = _ensure_earthman()
|
||||||
|
|
||||||
|
# ── Users ────────────────────────────────────────────────────────────
|
||||||
|
users = []
|
||||||
|
for email, _ in GAMERS:
|
||||||
|
user, _ = User.objects.get_or_create(email=email)
|
||||||
|
user.is_staff = True
|
||||||
|
user.is_superuser = True
|
||||||
|
if not user.equipped_deck:
|
||||||
|
user.equipped_deck = earthman
|
||||||
|
user.save()
|
||||||
|
users.append(user)
|
||||||
|
|
||||||
|
# ── Room ─────────────────────────────────────────────────────────────
|
||||||
|
if options["room"]:
|
||||||
|
room = Room.objects.get(pk=options["room"])
|
||||||
|
else:
|
||||||
|
room = Room.objects.create(
|
||||||
|
name="Sig Select Test Room",
|
||||||
|
owner=users[0],
|
||||||
|
visibility=Room.PUBLIC,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Gate slots ───────────────────────────────────────────────────────
|
||||||
|
for i, user in enumerate(users, start=1):
|
||||||
|
slot = room.gate_slots.get(slot_number=i)
|
||||||
|
slot.gamer = user
|
||||||
|
slot.status = GateSlot.FILLED
|
||||||
|
slot.save()
|
||||||
|
|
||||||
|
room.gate_status = Room.OPEN
|
||||||
|
room.save()
|
||||||
|
|
||||||
|
# ── Table seats + roles ──────────────────────────────────────────────
|
||||||
|
for i, (user, role) in enumerate(zip(users, ROLES), start=1):
|
||||||
|
TableSeat.objects.update_or_create(
|
||||||
|
room=room, slot_number=i,
|
||||||
|
defaults={"gamer": user, "role": role, "role_revealed": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
room.table_status = Room.SIG_SELECT
|
||||||
|
room.save()
|
||||||
|
|
||||||
|
# ── Print URLs ───────────────────────────────────────────────────────
|
||||||
|
room_path = f"/gameboard/room/{room.pk}/"
|
||||||
|
self.stdout.write(f"\nRoom: {base_url}{room_path}\n")
|
||||||
|
self.stdout.write(f"{'Container':<12} {'Email':<22} {'Role':<6} URL")
|
||||||
|
self.stdout.write("─" * 100)
|
||||||
|
|
||||||
|
for (email, container), user, role in zip(GAMERS, users, ROLES):
|
||||||
|
session_key = _make_session(user)
|
||||||
|
url = f"{base_url}/lyric/dev-login/{session_key}/?next={room_path}"
|
||||||
|
self.stdout.write(f"{container:<12} {email:<22} {role:<6} {url}")
|
||||||
|
|
||||||
|
self.stdout.write("")
|
||||||
@@ -119,6 +119,9 @@ class DashboardMaintenanceTest(FunctionalTest):
|
|||||||
class AppletMenuDismissTest(FunctionalTest):
|
class AppletMenuDismissTest(FunctionalTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
# Portrait viewport: sidebars don't activate, h2 sits safely above
|
||||||
|
# #id_dash_content and can't be obscured by it regardless of font metrics.
|
||||||
|
self.browser.set_window_size(800, 1200)
|
||||||
Applet.objects.get_or_create(slug="username", defaults={"name": "Username"})
|
Applet.objects.get_or_create(slug="username", defaults={"name": "Username"})
|
||||||
Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"})
|
Applet.objects.get_or_create(slug="palette", defaults={"name": "Palette"})
|
||||||
self.create_pre_authenticated_session("discoman@example.com")
|
self.create_pre_authenticated_session("discoman@example.com")
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ class NavbarByeTest(FunctionalTest):
|
|||||||
class NavbarContGameTest(FunctionalTest):
|
class NavbarContGameTest(FunctionalTest):
|
||||||
"""
|
"""
|
||||||
When the authenticated user has at least one room with a game event the
|
When the authenticated user has at least one room with a game event the
|
||||||
CONT GAME btn-primary btn-xl appears in the navbar and navigates to that
|
CONT GAME btn-primary appears in the navbar and navigates to that
|
||||||
room on confirmation. Its tooltip must also appear below the button.
|
room on confirmation. Its tooltip must also appear below the button.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -139,7 +139,6 @@ class NavbarContGameTest(FunctionalTest):
|
|||||||
)
|
)
|
||||||
btn = self.browser.find_element(By.ID, "id_cont_game")
|
btn = self.browser.find_element(By.ID, "id_cont_game")
|
||||||
self.assertIn("btn-primary", btn.get_attribute("class"))
|
self.assertIn("btn-primary", btn.get_attribute("class"))
|
||||||
self.assertIn("btn-xl", btn.get_attribute("class"))
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# T6 — CONT GAME tooltip appears below btn #
|
# T6 — CONT GAME tooltip appears below btn #
|
||||||
|
|||||||
@@ -34,14 +34,15 @@ def _assign_all_roles(room, role_order=None):
|
|||||||
slug="earthman",
|
slug="earthman",
|
||||||
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
||||||
)
|
)
|
||||||
# Seed the 18 sig deck cards (migration data is flushed in TransactionTestCase FTs)
|
# Seed the 18 sig deck cards (migration data is flushed in TransactionTestCase FTs).
|
||||||
|
# _sig_unique_cards() filters arcana=MIDDLE, suits BRANDS/CROWNS/BLADES/GRAILS (Earthman).
|
||||||
_NAME = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
|
_NAME = {11: "Maid", 12: "Jack", 13: "Queen", 14: "King"}
|
||||||
for suit in ("WANDS", "PENTACLES", "SWORDS", "CUPS"):
|
for suit in ("BRANDS", "CROWNS", "BLADES", "GRAILS"):
|
||||||
for number in (11, 12, 13, 14):
|
for number in (11, 12, 13, 14):
|
||||||
TarotCard.objects.get_or_create(
|
TarotCard.objects.get_or_create(
|
||||||
deck_variant=earthman,
|
deck_variant=earthman,
|
||||||
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em",
|
slug=f"{_NAME[number].lower()}-of-{suit.lower()}-em",
|
||||||
defaults={"arcana": "MINOR", "suit": suit, "number": number,
|
defaults={"arcana": "MIDDLE", "suit": suit, "number": number,
|
||||||
"name": f"{_NAME[number]} of {suit.capitalize()}"},
|
"name": f"{_NAME[number]} of {suit.capitalize()}"},
|
||||||
)
|
)
|
||||||
for number, name, slug in [
|
for number, name, slug in [
|
||||||
@@ -138,3 +139,234 @@ class SigSelectChannelsTest(ChannelsFunctionalTest):
|
|||||||
))
|
))
|
||||||
return b
|
return b
|
||||||
|
|
||||||
|
def _setup_sig_select_room(self):
|
||||||
|
"""Create a full SIG_SELECT room; return (room, [user_pc, user_nc, ...])."""
|
||||||
|
emails = [
|
||||||
|
"founder@test.io", "amigo@test.io", "bud@test.io",
|
||||||
|
"pal@test.io", "dude@test.io", "bro@test.io",
|
||||||
|
]
|
||||||
|
founder, _ = User.objects.get_or_create(email=emails[0])
|
||||||
|
room = Room.objects.create(name="Cursor Colour Test", owner=founder)
|
||||||
|
gamers = _fill_room_via_orm(room, emails)
|
||||||
|
_assign_all_roles(room)
|
||||||
|
return room, gamers
|
||||||
|
|
||||||
|
# ── SC1: NC hover → PC sees mid cursor active, coloured --priYl ────────── #
|
||||||
|
|
||||||
|
@tag('channels')
|
||||||
|
def test_nc_hover_activates_mid_cursor_in_pc_browser(self):
|
||||||
|
"""
|
||||||
|
When NC (levity mid) hovers a card, PC (levity left) must see the
|
||||||
|
--mid cursor become active, coloured --priYl (rgb 255 207 52).
|
||||||
|
Verifies: WS broadcast pipeline + JS applyHover + CSS role colouring.
|
||||||
|
"""
|
||||||
|
room, gamers = self._setup_sig_select_room()
|
||||||
|
room_url = self.live_server_url + f"/gameboard/room/{room.pk}/"
|
||||||
|
|
||||||
|
# ── Browser 1: PC (founder) ───────────────────────────────────────────
|
||||||
|
self.create_pre_authenticated_session("founder@test.io")
|
||||||
|
self.browser.get(room_url)
|
||||||
|
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
|
||||||
|
|
||||||
|
# ── Browser 2: NC (amigo) ─────────────────────────────────────────────
|
||||||
|
browser2 = self._make_browser2("amigo@test.io")
|
||||||
|
try:
|
||||||
|
browser2.get(room_url)
|
||||||
|
self.wait_for(lambda: browser2.find_element(By.CSS_SELECTOR, ".sig-overlay"))
|
||||||
|
|
||||||
|
# Grab the first card ID visible in browser2's deck
|
||||||
|
first_card = browser2.find_element(By.CSS_SELECTOR, ".sig-card")
|
||||||
|
card_id = first_card.get_attribute("data-card-id")
|
||||||
|
|
||||||
|
# Hover over it — triggers sendHover() → WS broadcast
|
||||||
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
|
ActionChains(browser2).move_to_element(first_card).perform()
|
||||||
|
|
||||||
|
# ── Browser 1 should see --mid cursor go active (anchor carries class) ─
|
||||||
|
mid_cursor_sel = f'.sig-card[data-card-id="{card_id}"] .sig-cursor--mid'
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, mid_cursor_sel + ".active"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# CSS colour check: portal float has data-role="NC" → --priYl = 255, 207, 52
|
||||||
|
portal_sel = '.sig-cursor-float[data-role="NC"]'
|
||||||
|
portal_cursor = self.browser.find_element(By.CSS_SELECTOR, portal_sel)
|
||||||
|
color = self.browser.execute_script(
|
||||||
|
"return window.getComputedStyle(arguments[0]).color",
|
||||||
|
portal_cursor,
|
||||||
|
)
|
||||||
|
self.assertEqual(color, "rgb(255, 207, 52)", f"Expected --priYl colour for NC cursor, got {color}")
|
||||||
|
|
||||||
|
# ── Mouse-off: anchor class removed, portal float gone ────────────
|
||||||
|
ActionChains(browser2).move_to_element(
|
||||||
|
browser2.find_element(By.CSS_SELECTOR, ".sig-stage")
|
||||||
|
).perform()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: not self.browser.find_elements(
|
||||||
|
By.CSS_SELECTOR, mid_cursor_sel + ".active"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
browser2.quit()
|
||||||
|
|
||||||
|
# ── SC2: NC reserves → PC sees card border coloured --priYl ──────────── #
|
||||||
|
|
||||||
|
@tag('channels')
|
||||||
|
def test_nc_reservation_glows_priYl_in_pc_browser(self):
|
||||||
|
"""
|
||||||
|
When NC (levity mid) clicks OK on a card, PC must see that card's border
|
||||||
|
coloured --priYl (rgb 255 207 52) via the data-reserved-by CSS selector.
|
||||||
|
Verifies: sig_reserve view → WS broadcast → applyReservation → CSS glow.
|
||||||
|
"""
|
||||||
|
room, gamers = self._setup_sig_select_room()
|
||||||
|
room_url = self.live_server_url + f"/gameboard/room/{room.pk}/"
|
||||||
|
|
||||||
|
# ── Browser 1: PC (founder) ───────────────────────────────────────────
|
||||||
|
self.create_pre_authenticated_session("founder@test.io")
|
||||||
|
self.browser.get(room_url)
|
||||||
|
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
|
||||||
|
|
||||||
|
# ── Browser 2: NC (amigo) ─────────────────────────────────────────────
|
||||||
|
browser2 = self._make_browser2("amigo@test.io")
|
||||||
|
try:
|
||||||
|
browser2.get(room_url)
|
||||||
|
self.wait_for(lambda: browser2.find_element(By.CSS_SELECTOR, ".sig-overlay"))
|
||||||
|
|
||||||
|
# Get first card in B2's deck
|
||||||
|
first_card = browser2.find_element(By.CSS_SELECTOR, ".sig-card")
|
||||||
|
card_id = first_card.get_attribute("data-card-id")
|
||||||
|
|
||||||
|
# Click card body → .sig-focused → OK button appears
|
||||||
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
|
ActionChains(browser2).move_to_element(first_card).perform()
|
||||||
|
first_card.click()
|
||||||
|
|
||||||
|
ok_btn = self.wait_for(
|
||||||
|
lambda: browser2.find_element(By.CSS_SELECTOR, ".sig-focused .sig-ok-btn")
|
||||||
|
)
|
||||||
|
ok_btn.click()
|
||||||
|
|
||||||
|
# ── B1 should see the card's border turn --priYl ──────────────────
|
||||||
|
reserved_card_sel = f'.sig-card[data-card-id="{card_id}"]'
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, reserved_card_sel + '[data-reserved-by="NC"]'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
reserved_card = self.browser.find_element(By.CSS_SELECTOR, reserved_card_sel)
|
||||||
|
box_shadow = self.browser.execute_script(
|
||||||
|
"return window.getComputedStyle(arguments[0]).boxShadow",
|
||||||
|
reserved_card,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"255, 207, 52", box_shadow,
|
||||||
|
f"Expected --priYl (255,207,52) in box-shadow for NC reservation, got {box_shadow}",
|
||||||
|
)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
browser2.quit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ── Polarity theming: qualifier text + no correspondence ─────────────────────
|
||||||
|
|
||||||
|
class SigSelectThemeTest(FunctionalTest):
|
||||||
|
"""Polarity-qualifier display (Graven/Leavened) and correspondence suppression.
|
||||||
|
No WebSocket needed — stage updates are local; uses plain FunctionalTest."""
|
||||||
|
|
||||||
|
EMAILS = [
|
||||||
|
"founder@test.io", "amigo@test.io", "bud@test.io",
|
||||||
|
"pal@test.io", "dude@test.io", "bro@test.io",
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.browser.set_window_size(800, 1200)
|
||||||
|
Applet.objects.get_or_create(slug="new-game", defaults={"name": "New Game", "context": "gameboard"})
|
||||||
|
Applet.objects.get_or_create(slug="my-games", defaults={"name": "My Games", "context": "gameboard"})
|
||||||
|
|
||||||
|
def _setup_sig_room(self):
|
||||||
|
founder, _ = User.objects.get_or_create(email=self.EMAILS[0])
|
||||||
|
room = Room.objects.create(name="Theme Test", owner=founder)
|
||||||
|
_fill_room_via_orm(room, self.EMAILS)
|
||||||
|
_assign_all_roles(room)
|
||||||
|
return room
|
||||||
|
|
||||||
|
def _hover_card(self, css):
|
||||||
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
|
card = self.browser.find_element(By.CSS_SELECTOR, css)
|
||||||
|
ActionChains(self.browser).move_to_element(card).perform()
|
||||||
|
return card
|
||||||
|
|
||||||
|
# ── ST1: Levity (Leavened) qualifier ──────────────────────────────────── #
|
||||||
|
|
||||||
|
def test_levity_non_major_card_shows_leavened_above(self):
|
||||||
|
"""Hovering a non-major card in the levity overlay shows 'Leavened' in
|
||||||
|
qualifier-above and nothing in qualifier-below."""
|
||||||
|
room = self._setup_sig_room()
|
||||||
|
self.create_pre_authenticated_session("founder@test.io") # PC = levity
|
||||||
|
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||||
|
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
|
||||||
|
|
||||||
|
self._hover_card('.sig-card[data-arcana="Middle Arcana"]')
|
||||||
|
|
||||||
|
above = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
|
||||||
|
)
|
||||||
|
self.assertEqual(above.text, "Leavened")
|
||||||
|
below = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below")
|
||||||
|
self.assertEqual(below.text, "")
|
||||||
|
|
||||||
|
def test_levity_major_card_shows_leavened_below(self):
|
||||||
|
"""Hovering a major arcana card in the levity overlay shows 'Leavened' in
|
||||||
|
qualifier-below and nothing in qualifier-above."""
|
||||||
|
room = self._setup_sig_room()
|
||||||
|
self.create_pre_authenticated_session("founder@test.io") # PC = levity
|
||||||
|
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||||
|
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
|
||||||
|
|
||||||
|
self._hover_card('.sig-card[data-arcana="Major Arcana"]')
|
||||||
|
|
||||||
|
below = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below")
|
||||||
|
)
|
||||||
|
self.assertEqual(below.text, "Leavened")
|
||||||
|
above = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
|
||||||
|
self.assertEqual(above.text, "")
|
||||||
|
|
||||||
|
# ── ST2: Gravity (Graven) qualifier ───────────────────────────────────── #
|
||||||
|
|
||||||
|
def test_gravity_non_major_card_shows_graven_above(self):
|
||||||
|
"""EC (bud) sees the gravity overlay; hovering a non-major card shows 'Graven'."""
|
||||||
|
room = self._setup_sig_room()
|
||||||
|
self.create_pre_authenticated_session("bud@test.io") # EC = gravity
|
||||||
|
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||||
|
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
|
||||||
|
|
||||||
|
self._hover_card('.sig-card[data-arcana="Middle Arcana"]')
|
||||||
|
|
||||||
|
above = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
|
||||||
|
)
|
||||||
|
self.assertEqual(above.text, "Graven")
|
||||||
|
|
||||||
|
# ── ST3: Correspondence not shown ─────────────────────────────────────── #
|
||||||
|
|
||||||
|
def test_correspondence_not_shown_in_sig_select(self):
|
||||||
|
"""The Minchiate-equivalence field must always be blank on the stage card."""
|
||||||
|
room = self._setup_sig_room()
|
||||||
|
self.create_pre_authenticated_session("founder@test.io")
|
||||||
|
self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
|
||||||
|
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
|
||||||
|
|
||||||
|
# Hover any card — correspondence should remain empty regardless
|
||||||
|
self._hover_card(".sig-card")
|
||||||
|
self.wait_for(lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, ".sig-stage-card"
|
||||||
|
))
|
||||||
|
corr = self.browser.find_element(By.CSS_SELECTOR, ".fan-card-correspondence")
|
||||||
|
self.assertEqual(corr.text, "")
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
describe("SigSelect", () => {
|
describe("SigSelect", () => {
|
||||||
let testDiv, stageCard, card;
|
let testDiv, stageCard, card, statBlock;
|
||||||
|
|
||||||
function makeFixture({ reservations = '{}' } = {}) {
|
function makeFixture({ reservations = '{}', cardCautions = '[]', polarity = 'levity', userRole = 'PC' } = {}) {
|
||||||
testDiv = document.createElement("div");
|
testDiv = document.createElement("div");
|
||||||
testDiv.innerHTML = `
|
testDiv.innerHTML = `
|
||||||
<div class="sig-overlay"
|
<div class="sig-overlay"
|
||||||
data-polarity="levity"
|
data-polarity="${polarity}"
|
||||||
data-user-role="PC"
|
data-user-role="${userRole}"
|
||||||
data-reserve-url="/epic/room/test/sig-reserve"
|
data-reserve-url="/epic/room/test/sig-reserve"
|
||||||
data-reservations="${reservations.replace(/"/g, '"')}">
|
data-reservations="${reservations.replace(/"/g, '"')}">
|
||||||
<div class="sig-modal">
|
<div class="sig-modal">
|
||||||
@@ -15,10 +15,35 @@ describe("SigSelect", () => {
|
|||||||
<span class="fan-corner-rank"></span>
|
<span class="fan-corner-rank"></span>
|
||||||
<i class="stage-suit-icon"></i>
|
<i class="stage-suit-icon"></i>
|
||||||
<p class="fan-card-name-group"></p>
|
<p class="fan-card-name-group"></p>
|
||||||
|
<p class="sig-qualifier-above"></p>
|
||||||
<h3 class="fan-card-name"></h3>
|
<h3 class="fan-card-name"></h3>
|
||||||
|
<p class="sig-qualifier-below"></p>
|
||||||
<p class="fan-card-arcana"></p>
|
<p class="fan-card-arcana"></p>
|
||||||
<p class="fan-card-correspondence"></p>
|
<p class="fan-card-correspondence"></p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sig-stat-block">
|
||||||
|
<button class="btn btn-reverse sig-flip-btn" type="button">FLIP</button>
|
||||||
|
<button class="btn btn-caution sig-caution-btn" type="button">!!</button>
|
||||||
|
<div class="stat-face stat-face--upright">
|
||||||
|
<p class="stat-face-label">Upright</p>
|
||||||
|
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="stat-face stat-face--reversed">
|
||||||
|
<p class="stat-face-label">Reversed</p>
|
||||||
|
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-nav-left sig-caution-prev" type="button">◀</button>
|
||||||
|
<button class="btn btn-nav-right sig-caution-next" type="button">▶</button>
|
||||||
|
<div class="sig-caution-tooltip" id="id_sig_caution">
|
||||||
|
<div class="sig-caution-header">
|
||||||
|
<h4 class="sig-caution-title">Caution!</h4>
|
||||||
|
<span class="sig-caution-type">Rival Interaction</span>
|
||||||
|
</div>
|
||||||
|
<p class="sig-caution-shoptalk">[Shoptalk forthcoming]</p>
|
||||||
|
<p class="sig-caution-effect"></p>
|
||||||
|
<span class="sig-caution-index"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sig-deck-grid">
|
<div class="sig-deck-grid">
|
||||||
<div class="sig-card"
|
<div class="sig-card"
|
||||||
@@ -28,7 +53,10 @@ describe("SigSelect", () => {
|
|||||||
data-name-group="Pentacles"
|
data-name-group="Pentacles"
|
||||||
data-name-title="King of Pentacles"
|
data-name-title="King of Pentacles"
|
||||||
data-arcana="Minor Arcana"
|
data-arcana="Minor Arcana"
|
||||||
data-correspondence="">
|
data-correspondence=""
|
||||||
|
data-keywords-upright="action,impulsiveness,ambition"
|
||||||
|
data-keywords-reversed="no direction,disregard for consequences"
|
||||||
|
data-cautions="${cardCautions.replace(/"/g, '"')}">
|
||||||
<div class="fan-card-corner fan-card-corner--tl">
|
<div class="fan-card-corner fan-card-corner--tl">
|
||||||
<span class="fan-corner-rank">K</span>
|
<span class="fan-corner-rank">K</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,6 +76,7 @@ describe("SigSelect", () => {
|
|||||||
`;
|
`;
|
||||||
document.body.appendChild(testDiv);
|
document.body.appendChild(testDiv);
|
||||||
stageCard = testDiv.querySelector(".sig-stage-card");
|
stageCard = testDiv.querySelector(".sig-stage-card");
|
||||||
|
statBlock = testDiv.querySelector(".sig-stat-block");
|
||||||
card = testDiv.querySelector(".sig-card");
|
card = testDiv.querySelector(".sig-card");
|
||||||
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||||
Promise.resolve({ ok: true })
|
Promise.resolve({ ok: true })
|
||||||
@@ -107,80 +136,6 @@ describe("SigSelect", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Touch: OK btn tap allows synthetic click through ──────────────── //
|
|
||||||
|
|
||||||
describe("touch on OK button", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
|
|
||||||
makeFixture();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("touchstart on OK btn does not call preventDefault (allows synthetic click)", () => {
|
|
||||||
// First tap the card body to show OK
|
|
||||||
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
||||||
expect(card.classList.contains("sig-focused")).toBe(true);
|
|
||||||
|
|
||||||
// Now tap the OK button — touchstart should NOT preventDefault
|
|
||||||
var okBtn = card.querySelector(".sig-ok-btn");
|
|
||||||
var touchEvent = new TouchEvent("touchstart", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
touches: [new Touch({ identifier: 1, target: okBtn })],
|
|
||||||
});
|
|
||||||
okBtn.dispatchEvent(touchEvent);
|
|
||||||
expect(touchEvent.defaultPrevented).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("touchstart on card body (not OK btn) calls preventDefault", () => {
|
|
||||||
var touchEvent = new TouchEvent("touchstart", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
touches: [new Touch({ identifier: 1, target: card })],
|
|
||||||
});
|
|
||||||
card.dispatchEvent(touchEvent);
|
|
||||||
expect(touchEvent.defaultPrevented).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Touch outside grid dismisses stage (mobile) ───────────────────── //
|
|
||||||
|
|
||||||
describe("touch outside grid", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
|
|
||||||
makeFixture();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("dismisses stage preview when touching outside the grid (unfocused state)", () => {
|
|
||||||
// Focus a card first
|
|
||||||
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
||||||
expect(stageCard.style.display).toBe("");
|
|
||||||
|
|
||||||
// Touch on the sig-stage (outside the grid)
|
|
||||||
var stage = testDiv.querySelector(".sig-stage");
|
|
||||||
stage.dispatchEvent(new TouchEvent("touchstart", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
touches: [new Touch({ identifier: 2, target: stage })],
|
|
||||||
}));
|
|
||||||
expect(stageCard.style.display).toBe("none");
|
|
||||||
expect(card.classList.contains("sig-focused")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does NOT dismiss stage preview when frozen (card reserved)", () => {
|
|
||||||
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
||||||
SigSelect._setFrozen(true);
|
|
||||||
// _focusedCardEl is set but frozen — use internal state trick via _setFrozen
|
|
||||||
// We also need a focused card; simulate it by setting frozen after focus
|
|
||||||
var stage = testDiv.querySelector(".sig-stage");
|
|
||||||
stage.dispatchEvent(new TouchEvent("touchstart", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
touches: [new Touch({ identifier: 3, target: stage })],
|
|
||||||
}));
|
|
||||||
expect(stageCard.style.display).toBe("");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Lock after reservation ─────────────────────────────────────────── //
|
// ── Lock after reservation ─────────────────────────────────────────── //
|
||||||
|
|
||||||
describe("lock after reservation", () => {
|
describe("lock after reservation", () => {
|
||||||
@@ -200,18 +155,6 @@ describe("SigSelect", () => {
|
|||||||
expect(window.fetch).not.toHaveBeenCalled();
|
expect(window.fetch).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not call preventDefault on touchstart while a card is reserved", () => {
|
|
||||||
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
|
|
||||||
SigSelect._setReservedCardId("99");
|
|
||||||
var touchEvent = new TouchEvent("touchstart", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
touches: [new Touch({ identifier: 1, target: card })],
|
|
||||||
});
|
|
||||||
card.dispatchEvent(touchEvent);
|
|
||||||
expect(touchEvent.defaultPrevented).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows focus again after reservation is cleared", () => {
|
it("allows focus again after reservation is cleared", () => {
|
||||||
SigSelect._setReservedCardId("99");
|
SigSelect._setReservedCardId("99");
|
||||||
SigSelect._setReservedCardId(null);
|
SigSelect._setReservedCardId(null);
|
||||||
@@ -252,4 +195,414 @@ describe("SigSelect", () => {
|
|||||||
expect(card.classList.contains("sig-focused")).toBe(true);
|
expect(card.classList.contains("sig-focused")).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Caution tooltip (!!) ──────────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("caution tooltip", () => {
|
||||||
|
var cautionTooltip, cautionEffect, cautionPrev, cautionNext, cautionBtn;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
makeFixture();
|
||||||
|
cautionTooltip = testDiv.querySelector(".sig-caution-tooltip");
|
||||||
|
cautionEffect = testDiv.querySelector(".sig-caution-effect");
|
||||||
|
cautionPrev = testDiv.querySelector(".sig-caution-prev");
|
||||||
|
cautionNext = testDiv.querySelector(".sig-caution-next");
|
||||||
|
cautionBtn = testDiv.querySelector(".sig-caution-btn");
|
||||||
|
});
|
||||||
|
|
||||||
|
function hover() {
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCaution() {
|
||||||
|
hover();
|
||||||
|
cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
it("!! click adds .sig-caution-open to the stage", () => {
|
||||||
|
openCaution();
|
||||||
|
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("FYI click when btn-disabled does not close caution", () => {
|
||||||
|
openCaution();
|
||||||
|
expect(cautionBtn.classList.contains("btn-disabled")).toBe(true);
|
||||||
|
cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows placeholder text when cautions list is empty", () => {
|
||||||
|
card.dataset.cautions = "[]";
|
||||||
|
openCaution();
|
||||||
|
expect(cautionEffect.innerHTML).toContain("pending");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders first caution effect HTML including .card-ref spans", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(['First <span class="card-ref">Card</span> effect.']);
|
||||||
|
openCaution();
|
||||||
|
expect(cautionEffect.querySelector(".card-ref")).not.toBeNull();
|
||||||
|
expect(cautionEffect.querySelector(".card-ref").textContent).toBe("Card");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with 1 caution both nav arrows are disabled", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["Single caution."]);
|
||||||
|
openCaution();
|
||||||
|
expect(cautionPrev.disabled).toBe(true);
|
||||||
|
expect(cautionNext.disabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with multiple cautions both nav arrows are always enabled", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["C1", "C2", "C3", "C4"]);
|
||||||
|
openCaution();
|
||||||
|
expect(cautionPrev.disabled).toBe(false);
|
||||||
|
expect(cautionNext.disabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("next click advances to second caution", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["First", "Second"]);
|
||||||
|
openCaution();
|
||||||
|
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(cautionEffect.innerHTML).toContain("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("next wraps from last caution back to first", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["First", "Last"]);
|
||||||
|
openCaution();
|
||||||
|
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(cautionEffect.innerHTML).toContain("First");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prev click goes back to first caution", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["First", "Second"]);
|
||||||
|
openCaution();
|
||||||
|
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(cautionEffect.innerHTML).toContain("First");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prev wraps from first caution to last", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["First", "Middle", "Last"]);
|
||||||
|
openCaution();
|
||||||
|
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(cautionEffect.innerHTML).toContain("Last");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("index label shows n / total when multiple cautions", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["C1", "C2", "C3"]);
|
||||||
|
openCaution();
|
||||||
|
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("1 / 3");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("index label is empty when only 1 caution", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["Only one."]);
|
||||||
|
openCaution();
|
||||||
|
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("card mouseleave closes the caution", () => {
|
||||||
|
openCaution();
|
||||||
|
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opening again resets to first caution", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["First", "Second"]);
|
||||||
|
openCaution();
|
||||||
|
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
// Close and reopen
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||||
|
openCaution();
|
||||||
|
expect(cautionEffect.innerHTML).toContain("First");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opening caution adds .btn-disabled and swaps labels to ×", () => {
|
||||||
|
openCaution();
|
||||||
|
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
||||||
|
expect(flipBtn.classList.contains("btn-disabled")).toBe(true);
|
||||||
|
expect(cautionBtn.classList.contains("btn-disabled")).toBe(true);
|
||||||
|
expect(flipBtn.textContent).toBe("\u00D7");
|
||||||
|
expect(cautionBtn.textContent).toBe("\u00D7");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closing caution removes .btn-disabled and restores original labels", () => {
|
||||||
|
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
||||||
|
var origFlip = flipBtn.textContent;
|
||||||
|
var origCaution = cautionBtn.textContent;
|
||||||
|
openCaution();
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||||
|
expect(flipBtn.classList.contains("btn-disabled")).toBe(false);
|
||||||
|
expect(cautionBtn.classList.contains("btn-disabled")).toBe(false);
|
||||||
|
expect(flipBtn.textContent).toBe(origFlip);
|
||||||
|
expect(cautionBtn.textContent).toBe(origCaution);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking the tooltip closes caution", () => {
|
||||||
|
openCaution();
|
||||||
|
cautionEffect.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("FLIP click when caution open (btn-disabled) does nothing", () => {
|
||||||
|
openCaution();
|
||||||
|
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
||||||
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
|
||||||
|
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Stat block: keyword population and FLIP toggle ────────────────── //
|
||||||
|
|
||||||
|
describe("stat block and FLIP", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
it("populates upright keywords when a card is hovered", () => {
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
var items = statBlock.querySelectorAll("#id_stat_keywords_upright li");
|
||||||
|
expect(items.length).toBe(3);
|
||||||
|
expect(items[0].textContent).toBe("action");
|
||||||
|
expect(items[1].textContent).toBe("impulsiveness");
|
||||||
|
expect(items[2].textContent).toBe("ambition");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("populates reversed keywords when a card is hovered", () => {
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
var items = statBlock.querySelectorAll("#id_stat_keywords_reversed li");
|
||||||
|
expect(items.length).toBe(2);
|
||||||
|
expect(items[0].textContent).toBe("no direction");
|
||||||
|
expect(items[1].textContent).toBe("disregard for consequences");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("FLIP click adds .is-reversed to the stat block", () => {
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
var flipBtn = statBlock.querySelector(".sig-flip-btn");
|
||||||
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("second FLIP click removes .is-reversed", () => {
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
var flipBtn = statBlock.querySelector(".sig-flip-btn");
|
||||||
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hovering a new card resets .is-reversed", () => {
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
statBlock.querySelector(".sig-flip-btn").dispatchEvent(
|
||||||
|
new MouseEvent("click", { bubbles: true })
|
||||||
|
);
|
||||||
|
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
||||||
|
|
||||||
|
// Leave and re-enter (simulates moving to a different card)
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("card with no keywords yields empty lists", () => {
|
||||||
|
card.dataset.keywordsUpright = "";
|
||||||
|
card.dataset.keywordsReversed = "";
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(statBlock.querySelectorAll("#id_stat_keywords_upright li").length).toBe(0);
|
||||||
|
expect(statBlock.querySelectorAll("#id_stat_keywords_reversed li").length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── WS cursor hover (applyHover) ──────────────────────────────────────── //
|
||||||
|
//
|
||||||
|
// Fixture polarity = levity, userRole = PC.
|
||||||
|
// POLARITY_ROLES: levity → [PC, NC, SC] = [left, mid, right]
|
||||||
|
//
|
||||||
|
// Only tests the JS position mapping — colour is CSS-only.
|
||||||
|
|
||||||
|
describe("WS cursor hover", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
it("NC hover activates the --mid cursor", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||||
|
detail: { card_id: 42, role: "NC", active: true },
|
||||||
|
}));
|
||||||
|
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("SC hover activates the --right cursor", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||||
|
detail: { card_id: 42, role: "SC", active: true },
|
||||||
|
}));
|
||||||
|
expect(card.querySelector(".sig-cursor--right").classList.contains("active")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("own role (PC) hover event is ignored — no cursor activates", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||||
|
detail: { card_id: 42, role: "PC", active: true },
|
||||||
|
}));
|
||||||
|
expect(card.querySelectorAll(".sig-cursor.active").length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hover-off removes .active from the cursor", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||||
|
detail: { card_id: 42, role: "NC", active: true },
|
||||||
|
}));
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||||
|
detail: { card_id: 42, role: "NC", active: false },
|
||||||
|
}));
|
||||||
|
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hover on unknown card_id is a no-op", () => {
|
||||||
|
expect(() => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||||
|
detail: { card_id: 9999, role: "NC", active: true },
|
||||||
|
}));
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── WS reservation — data-reserved-by attribute ───────────────────────── //
|
||||||
|
//
|
||||||
|
// applyReservation() sets data-reserved-by so the CSS can glow the card in
|
||||||
|
// the reserving gamer's role colour. These tests assert the attribute, not
|
||||||
|
// the colour (CSS variables aren't resolvable in the SpecRunner context).
|
||||||
|
|
||||||
|
describe("WS reservation sets data-reserved-by", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
it("peer reservation sets data-reserved-by to the reserving role", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "NC", reserved: true },
|
||||||
|
}));
|
||||||
|
expect(card.dataset.reservedBy).toBe("NC");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("peer reservation also adds .sig-reserved class", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "NC", reserved: true },
|
||||||
|
}));
|
||||||
|
expect(card.classList.contains("sig-reserved")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("release removes data-reserved-by", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "NC", reserved: true },
|
||||||
|
}));
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "NC", reserved: false },
|
||||||
|
}));
|
||||||
|
expect(card.dataset.reservedBy).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("own reservation (PC) sets data-reserved-by AND .sig-reserved--own", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "PC", reserved: true },
|
||||||
|
}));
|
||||||
|
expect(card.dataset.reservedBy).toBe("PC");
|
||||||
|
expect(card.classList.contains("sig-reserved--own")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("peer reservation places a thumbs-up float and removes any hand-pointer float", () => {
|
||||||
|
// First, a hover float exists for NC (mid cursor)
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||||
|
detail: { card_id: 42, role: "NC", active: true },
|
||||||
|
}));
|
||||||
|
expect(document.querySelector('.sig-cursor-float[data-role="NC"]')).not.toBeNull();
|
||||||
|
expect(document.querySelector('.fa-hand-pointer[data-role="NC"]')).not.toBeNull();
|
||||||
|
|
||||||
|
// NC then clicks OK — reservation arrives
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "NC", reserved: true },
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Thumbs-up replaces hand-pointer
|
||||||
|
const floatEl = document.querySelector('.sig-cursor-float[data-role="NC"]');
|
||||||
|
expect(floatEl).not.toBeNull();
|
||||||
|
expect(floatEl.classList.contains("fa-thumbs-up")).toBe(true);
|
||||||
|
expect(floatEl.classList.contains("fa-hand-pointer")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("peer release removes the thumbs-up float", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "NC", reserved: true },
|
||||||
|
}));
|
||||||
|
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).not.toBeNull();
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "NC", reserved: false },
|
||||||
|
}));
|
||||||
|
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Polarity theming — stage qualifier text ────────────────────────────── //
|
||||||
|
//
|
||||||
|
// On mouseenter, updateStage() injects "Leavened" or "Graven" into the
|
||||||
|
// sig-qualifier-above (non-major) or sig-qualifier-below (major arcana) slot.
|
||||||
|
// Correspondence field is never populated in sig-select context.
|
||||||
|
|
||||||
|
describe("polarity theming — stage qualifier", () => {
|
||||||
|
it("levity non-major card puts 'Leavened' in qualifier-above, qualifier-below empty", () => {
|
||||||
|
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||||
|
// data-arcana defaults to "Minor Arcana" in fixture → non-major
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("Leavened");
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("levity major arcana card puts 'Leavened' in qualifier-below, qualifier-above empty", () => {
|
||||||
|
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||||
|
card.dataset.arcana = "Major Arcana";
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("");
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Leavened");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("major arcana title gets a trailing comma (qualifier reads as subtitle)", () => {
|
||||||
|
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||||
|
card.dataset.arcana = "Major Arcana";
|
||||||
|
card.dataset.nameTitle = "The Schizo";
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".fan-card-name").textContent).toBe("The Schizo,");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("non-major arcana title has no trailing comma", () => {
|
||||||
|
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||||
|
// fixture default: Minor Arcana, "King of Pentacles"
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".fan-card-name").textContent).toBe("King of Pentacles");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gravity non-major card puts 'Graven' in qualifier-above", () => {
|
||||||
|
makeFixture({ polarity: 'gravity', userRole: 'BC' });
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("Graven");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gravity major arcana card puts 'Graven' in qualifier-below", () => {
|
||||||
|
makeFixture({ polarity: 'gravity', userRole: 'BC' });
|
||||||
|
card.dataset.arcana = "Major Arcana";
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Graven");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hovering clears qualifier slots from the previous card", () => {
|
||||||
|
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
card.dataset.arcana = "Major Arcana";
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
// Now major — above should be empty, below filled
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("");
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Leavened");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correspondence field is never populated", () => {
|
||||||
|
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||||
|
card.dataset.correspondence = "Il Bagatto (Minchiate)";
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".fan-card-correspondence").textContent).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -110,7 +110,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// In landscape: shift gear btn and applet menus left of the footer right sidebar
|
// In landscape: shift gear btn and applet menus left of the footer right sidebar
|
||||||
@media (orientation: landscape) and (max-width: 1440px) {
|
// XL override below doubles sidebar to 8rem — centre items in the wider column.
|
||||||
|
@media (orientation: landscape) {
|
||||||
$sidebar-w: 4rem;
|
$sidebar-w: 4rem;
|
||||||
|
|
||||||
.gameboard-page,
|
.gameboard-page,
|
||||||
@@ -119,7 +120,7 @@
|
|||||||
.room-page,
|
.room-page,
|
||||||
.billboard-page {
|
.billboard-page {
|
||||||
> .gear-btn {
|
> .gear-btn {
|
||||||
right: 0.5rem;
|
right: 1rem;
|
||||||
bottom: 3.95rem; // same gap above kit btn as portrait; no page-specific overrides needed
|
bottom: 3.95rem; // same gap above kit btn as portrait; no page-specific overrides needed
|
||||||
top: auto;
|
top: auto;
|
||||||
}
|
}
|
||||||
@@ -131,12 +132,30 @@
|
|||||||
#id_wallet_applet_menu,
|
#id_wallet_applet_menu,
|
||||||
#id_room_menu,
|
#id_room_menu,
|
||||||
#id_billboard_applet_menu {
|
#id_billboard_applet_menu {
|
||||||
right: 0.5rem;
|
right: 1rem;
|
||||||
bottom: 6.6rem;
|
bottom: 6.6rem;
|
||||||
top: auto;
|
top: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) and (min-width: 1800px) {
|
||||||
|
// Centre gear btn and menus in the doubled 8rem sidebar (was 0.5rem from right edge)
|
||||||
|
.gameboard-page,
|
||||||
|
.dashboard-page,
|
||||||
|
.wallet-page,
|
||||||
|
.room-page,
|
||||||
|
.billboard-page {
|
||||||
|
> .gear-btn { right: 2.5rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#id_dash_applet_menu,
|
||||||
|
#id_game_applet_menu,
|
||||||
|
#id_game_kit_menu,
|
||||||
|
#id_wallet_applet_menu,
|
||||||
|
#id_room_menu,
|
||||||
|
#id_billboard_applet_menu { right: 2.5rem; }
|
||||||
|
}
|
||||||
|
|
||||||
// ── Applet box visual shell (reusable outside the grid) ────
|
// ── Applet box visual shell (reusable outside the grid) ────
|
||||||
%applet-box {
|
%applet-box {
|
||||||
border:
|
border:
|
||||||
@@ -215,6 +234,16 @@
|
|||||||
black 99%,
|
black 99%,
|
||||||
transparent 100%
|
transparent 100%
|
||||||
);
|
);
|
||||||
|
margin-left: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
@media (orientation: landscape) and (min-width: 900px) {
|
||||||
|
margin-left: 2rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
@media (orientation: landscape) and (min-width: 1800px) {
|
||||||
|
margin-left: 4rem;
|
||||||
|
margin-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
@extend %applet-box;
|
@extend %applet-box;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ body {
|
|||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.container-fluid {
|
.container-fluid {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -192,8 +193,18 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (orientation: landscape) and (max-width: 1440px) {
|
@media (orientation: landscape) and (max-width: 1100px) {
|
||||||
$sidebar-w: 4rem;
|
body .container {
|
||||||
|
.navbar {
|
||||||
|
h1 {
|
||||||
|
font-size: 1rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
$sidebar-w: 5rem;
|
||||||
|
|
||||||
// ── Sidebar layout: navbar ← left, footer → right ────────────────────────────
|
// ── Sidebar layout: navbar ← left, footer → right ────────────────────────────
|
||||||
body {
|
body {
|
||||||
@@ -264,13 +275,12 @@ body {
|
|||||||
.navbar-label { opacity: 0.7; }
|
.navbar-label { opacity: 0.7; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
// .btn-primary {
|
||||||
width: 3rem;
|
// width: 4rem;
|
||||||
height: 3rem;
|
// height: 4rem;
|
||||||
font-size: 0.75rem;
|
// font-size: 0.875rem;
|
||||||
border-width: 0.125rem;
|
// border-width: 0.21rem;
|
||||||
// margin-left: 0.75rem;
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
// Login form: offset from fixed sidebars in landscape
|
// Login form: offset from fixed sidebars in landscape
|
||||||
.input-group {
|
.input-group {
|
||||||
@@ -288,26 +298,35 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Container: fill center, compensate for fixed sidebars on both sides
|
// Container: fill center, compensate for fixed sidebars on both sides.
|
||||||
|
// max-width: none overrides the @media (min-width: 1200px) rule above so the
|
||||||
|
// container fills all available space between the two sidebars on wide screens.
|
||||||
body .container {
|
body .container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
margin-left: $sidebar-w;
|
margin-left: $sidebar-w;
|
||||||
margin-right: $sidebar-w;
|
margin-right: $sidebar-w;
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header row: compact in landscape
|
// Header row: h2 rotates into the left gutter (just right of the navbar border).
|
||||||
|
// position:fixed takes h2 out of flow; .row collapses to zero height automatically.
|
||||||
body .container .row {
|
body .container .row {
|
||||||
padding: 0.25rem 0;
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
.col-lg-6 h2 {
|
}
|
||||||
font-size: 1.5rem;
|
body .container .row .col-lg-6 h2 {
|
||||||
margin: 0 0 0.25rem;
|
position: fixed;
|
||||||
letter-spacing: 0.4em;
|
left: 5rem; // $sidebar-w — flush with the navbar right border
|
||||||
text-align: center;
|
top: 50%;
|
||||||
text-align-last: left;
|
transform: translateY(-50%) rotate(180deg);
|
||||||
}
|
writing-mode: vertical-rl;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
letter-spacing: 0.4em;
|
||||||
|
margin: 0;
|
||||||
|
z-index: 85;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer → fixed right sidebar (mirrors navbar approach — explicit right boundary)
|
// Footer → fixed right sidebar (mirrors navbar approach — explicit right boundary)
|
||||||
@@ -324,6 +343,7 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-left: 0.1rem solid rgba(var(--secUser), 0.3);
|
border-left: 0.1rem solid rgba(var(--secUser), 0.3);
|
||||||
|
background-color: rgba(var(--priUser), 1); // opaque: masks tray sliding behind it
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
@@ -332,7 +352,8 @@ body {
|
|||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
gap: 3rem;
|
gap: 1.5rem !important;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
@@ -344,13 +365,104 @@ body {
|
|||||||
|
|
||||||
.footer-container {
|
.footer-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0.75rem;
|
top: 0.25rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.55rem;
|
line-height: 0.75 !important;
|
||||||
line-height: 1.4;
|
color: rgba(var(--secUser), 1);
|
||||||
color: rgba(var(--secUser), 0.5);
|
|
||||||
|
|
||||||
br { display: block; }
|
br { display: block; }
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 0.75rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) and (min-width: 700px) {
|
||||||
|
body .container .row .col-lg-6 h2 {
|
||||||
|
@media (min-height: 400px) {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
@media (min-height: 500px) {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body #id_footer {
|
||||||
|
#id_footer_nav {
|
||||||
|
gap: 3rem !important;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-container {
|
||||||
|
line-height: 1;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── XL landscape (≥1800px): double sidebar widths and scale content ────────────
|
||||||
|
@media (orientation: landscape) and (min-width: 1800px) {
|
||||||
|
$sidebar-xl: 8rem;
|
||||||
|
|
||||||
|
body .container .navbar {
|
||||||
|
width: $sidebar-xl;
|
||||||
|
|
||||||
|
.container-fluid {
|
||||||
|
gap: 2rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand h1 { font-size: 2.4rem; }
|
||||||
|
.navbar-text { font-size: 0.78rem; } // 0.65rem × 1.2
|
||||||
|
// .btn-primary { width: 4rem; height: 4rem; font-size: 0.875rem; }
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
left: $sidebar-xl;
|
||||||
|
right: $sidebar-xl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body .container {
|
||||||
|
margin-left: $sidebar-xl;
|
||||||
|
margin-right: $sidebar-xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// h2 page title: keep vertical rotation; shift left to clear the wider XL navbar.
|
||||||
|
body .container .row .col-lg-6 h2 {
|
||||||
|
left: 8rem; // $sidebar-xl
|
||||||
|
@media (min-height: 800px) {
|
||||||
|
font-size: 4.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body #id_footer {
|
||||||
|
width: $sidebar-xl;
|
||||||
|
|
||||||
|
#id_footer_nav {
|
||||||
|
gap: 8rem !important;
|
||||||
|
a { font-size: 3rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-container {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,13 +475,6 @@ body {
|
|||||||
.navbar-brand h1 {
|
.navbar-brand h1 {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
border-width: 0.125rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.row .col-lg-6 h2 {
|
.row .col-lg-6 h2 {
|
||||||
@@ -436,8 +541,8 @@ body {
|
|||||||
br { display: none; }
|
br { display: none; }
|
||||||
|
|
||||||
small {
|
small {
|
||||||
font-size: 0.7rem;
|
font-size: 0.75rem;
|
||||||
opacity: 0.6;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.btn-primary {
|
&.btn-primary {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border-width: 0.21rem;
|
||||||
color: rgba(var(--quaUser), 1);
|
color: rgba(var(--quaUser), 1);
|
||||||
border-color: rgba(var(--quaUser), 1);
|
border-color: rgba(var(--quaUser), 1);
|
||||||
background-color: rgba(var(--quiUser), 1);
|
background-color: rgba(var(--quiUser), 1);
|
||||||
@@ -34,37 +38,6 @@
|
|||||||
0.25rem 0.25rem 0.25rem rgba(var(--quiUser), 0.12)
|
0.25rem 0.25rem 0.25rem rgba(var(--quiUser), 0.12)
|
||||||
;
|
;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-shadow:
|
|
||||||
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
|
|
||||||
0 0 1rem rgba(var(--quaUser), 1)
|
|
||||||
;
|
|
||||||
box-shadow:
|
|
||||||
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
|
|
||||||
0 0 0.5rem rgba(var(--quaUser), 0.12)
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
border: 0.18rem solid rgba(var(--quaUser), 1);
|
|
||||||
text-shadow:
|
|
||||||
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
|
|
||||||
0 0 0.12rem rgba(var(--quaUser), 1)
|
|
||||||
;
|
|
||||||
box-shadow:
|
|
||||||
-0.1rem -0.1rem 0.12rem rgba(var(--quiUser), 0.25),
|
|
||||||
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
|
|
||||||
0 0 0.5rem rgba(var(--quaUser), 0.12)
|
|
||||||
;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.btn-xl {
|
|
||||||
width: 4rem;
|
|
||||||
height: 4rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
border-width: 0.21rem;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-shadow:
|
text-shadow:
|
||||||
0.2rem 0.2rem 0.2rem rgba(0, 0, 0, 0.25),
|
0.2rem 0.2rem 0.2rem rgba(0, 0, 0, 0.25),
|
||||||
@@ -72,7 +45,7 @@
|
|||||||
;
|
;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0.24rem 0.24rem 0.5rem rgba(0, 0, 0, 0.25),
|
0.24rem 0.24rem 0.5rem rgba(0, 0, 0, 0.25),
|
||||||
0 0 0.5rem rgba(var(--quaUser), 22)
|
0 0 0.5rem rgba(var(--quaUser), 0.22)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +60,14 @@
|
|||||||
-0.2rem -0.2rem 0.24rem rgba(0, 0, 0, 0.25),
|
-0.2rem -0.2rem 0.24rem rgba(0, 0, 0, 0.25),
|
||||||
0 0 0.5rem rgba(var(--quaUser), 0.22)
|
0 0 0.5rem rgba(var(--quaUser), 0.22)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) and (max-width: 1100px) {
|
||||||
|
width: 2.75rem !important;
|
||||||
|
height: 2.75rem !important;
|
||||||
|
font-size: 0.625rem !important;
|
||||||
|
border-width: 0.125rem !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn-abandon {
|
&.btn-abandon {
|
||||||
@@ -300,13 +280,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) and (min-width: 1800px) {
|
||||||
|
width: 2.4rem; // 2rem × 1.2
|
||||||
|
height: 2.4rem;
|
||||||
|
font-size: 0.75rem; // 0.63rem × 1.2
|
||||||
|
}
|
||||||
|
|
||||||
&.btn-disabled {
|
&.btn-disabled {
|
||||||
cursor: default !important;
|
cursor: default !important;
|
||||||
|
pointer-events: none;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
padding-bottom: 0.1rem;
|
padding-bottom: 0.1rem;
|
||||||
color: rgba(var(--secUser), 0.25);
|
color: rgba(var(--secUser), 0.25) !important;
|
||||||
background-color: rgba(var(--priUser), 1);
|
background-color: rgba(var(--priUser), 1) !important;
|
||||||
border-color: rgba(var(--secUser), 0.25);
|
border-color: rgba(var(--secUser), 0.25) !important;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0.1rem 0.1rem 0.12rem rgba(var(--priUser), 0.5),
|
0.1rem 0.1rem 0.12rem rgba(var(--priUser), 0.5),
|
||||||
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
|
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||||
@@ -336,4 +323,144 @@
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.btn-nav-left {
|
||||||
|
color: rgba(var(--priFs), 1);
|
||||||
|
border-color: rgba(var(--priFs), 1);
|
||||||
|
background-color: rgba(var(--terFs), 1);
|
||||||
|
box-shadow:
|
||||||
|
0.1rem 0.1rem 0.12rem rgba(var(--terFs), 0.25),
|
||||||
|
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||||
|
0.25rem 0.25rem 0.25rem rgba(var(--terFs), 0.12)
|
||||||
|
;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-shadow:
|
||||||
|
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 1rem rgba(var(--priFs), 1)
|
||||||
|
;
|
||||||
|
box-shadow:
|
||||||
|
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 0.5rem rgba(var(--priFs), 0.12)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border: 0.18rem solid rgba(var(--priFs), 1);
|
||||||
|
text-shadow:
|
||||||
|
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 0.12rem rgba(var(--priFs), 1)
|
||||||
|
;
|
||||||
|
box-shadow:
|
||||||
|
-0.1rem -0.1rem 0.12rem rgba(var(--terFs), 0.25),
|
||||||
|
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 0.5rem rgba(var(--priFs), 0.12)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-nav-right {
|
||||||
|
color: rgba(var(--priLm), 1);
|
||||||
|
border-color: rgba(var(--priLm), 1);
|
||||||
|
background-color: rgba(var(--terLm), 1);
|
||||||
|
box-shadow:
|
||||||
|
0.1rem 0.1rem 0.12rem rgba(var(--terLm), 0.25),
|
||||||
|
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||||
|
0.25rem 0.25rem 0.25rem rgba(var(--terLm), 0.12)
|
||||||
|
;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-shadow:
|
||||||
|
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 1rem rgba(var(--priLm), 1)
|
||||||
|
;
|
||||||
|
box-shadow:
|
||||||
|
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 0.5rem rgba(var(--priLm), 0.12)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border: 0.18rem solid rgba(var(--priLm), 1);
|
||||||
|
text-shadow:
|
||||||
|
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 0.12rem rgba(var(--priLm), 1)
|
||||||
|
;
|
||||||
|
box-shadow:
|
||||||
|
-0.1rem -0.1rem 0.12rem rgba(var(--terLm), 0.25),
|
||||||
|
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 0.5rem rgba(var(--priLm), 0.12)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-reverse {
|
||||||
|
color: rgba(var(--priCy), 1);
|
||||||
|
border-color: rgba(var(--priCy), 1);
|
||||||
|
background-color: rgba(var(--terCy), 1);
|
||||||
|
box-shadow:
|
||||||
|
0.1rem 0.1rem 0.12rem rgba(var(--terCy), 0.25),
|
||||||
|
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||||
|
0.25rem 0.25rem 0.25rem rgba(var(--terCy), 0.12)
|
||||||
|
;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-shadow:
|
||||||
|
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 1rem rgba(var(--priCy), 1)
|
||||||
|
;
|
||||||
|
box-shadow:
|
||||||
|
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 0.5rem rgba(var(--priCy), 0.12)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border: 0.18rem solid rgba(var(--priCy), 1);
|
||||||
|
text-shadow:
|
||||||
|
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 0.12rem rgba(var(--priCy), 1)
|
||||||
|
;
|
||||||
|
box-shadow:
|
||||||
|
-0.1rem -0.1rem 0.12rem rgba(var(--terCy), 0.25),
|
||||||
|
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 0.5rem rgba(var(--priCy), 0.12)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-tip {
|
||||||
|
color: rgba(var(--priLm), 1);
|
||||||
|
border-color: rgba(var(--priLm), 1);
|
||||||
|
background-color: rgba(var(--terLm), 1);
|
||||||
|
box-shadow:
|
||||||
|
0.1rem 0.1rem 0.12rem rgba(var(--terLm), 0.25),
|
||||||
|
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||||
|
0.25rem 0.25rem 0.25rem rgba(var(--terLm), 0.12)
|
||||||
|
;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-shadow:
|
||||||
|
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 1rem rgba(var(--priLm), 1)
|
||||||
|
;
|
||||||
|
box-shadow:
|
||||||
|
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 0.5rem rgba(var(--priLm), 0.12)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border: 0.18rem solid rgba(var(--priLm), 1);
|
||||||
|
text-shadow:
|
||||||
|
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 0.12rem rgba(var(--priLm), 1)
|
||||||
|
;
|
||||||
|
box-shadow:
|
||||||
|
-0.1rem -0.1rem 0.12rem rgba(var(--terLm), 0.25),
|
||||||
|
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 0.5rem rgba(var(--priLm), 0.12)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
672
src/static_src/scss/_card-deck.scss
Normal file
672
src/static_src/scss/_card-deck.scss
Normal file
@@ -0,0 +1,672 @@
|
|||||||
|
// ─── Card deck primitives — fan cards + sig-select overlay ─────────────────────
|
||||||
|
//
|
||||||
|
// Shared card display classes (.fan-card, .fan-card-corner, .fan-card-face, .fan-nav)
|
||||||
|
// extracted from _game-kit.scss; sig-select overlay extracted from _room.scss.
|
||||||
|
|
||||||
|
// ── Tarot fan modal ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#id_tarot_fan_dialog {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: none;
|
||||||
|
max-height: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: rgba(0, 0, 0, 0.88);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::backdrop { display: none; } // Dialog IS the backdrop
|
||||||
|
}
|
||||||
|
|
||||||
|
.tarot-fan-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
perspective: 900px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tarot-fan {
|
||||||
|
position: relative;
|
||||||
|
width: 220px;
|
||||||
|
height: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fan-card {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 220px;
|
||||||
|
height: 340px;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: rgba(var(--priUser), 1);
|
||||||
|
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: transform 0.25s ease, opacity 0.25s ease;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
border-color: rgba(var(--secUser), 1);
|
||||||
|
box-shadow: 0 0 2rem rgba(var(--secUser), 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fan-card-corner {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.15rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: rgba(var(--secUser), 0.75);
|
||||||
|
|
||||||
|
&--tl { top: 0.4rem; left: 0.4rem; }
|
||||||
|
&--br { bottom: 0.4rem; right: 0.4rem; transform: rotate(180deg); }
|
||||||
|
|
||||||
|
.fan-corner-rank {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.18rem 0;
|
||||||
|
}
|
||||||
|
i { font-size: 1.5rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.fan-card-face {
|
||||||
|
padding: 1.25rem;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
.fan-card-number { font-size: 0.65rem; }
|
||||||
|
.fan-card-name-group { font-size: 0.65rem; margin: 0; text-transform: uppercase; letter-spacing: 0.08em; color: rgba(var(--secUser), 1); }
|
||||||
|
.fan-card-name { font-size: 0.95rem; font-weight: bold; margin: 0; color: rgba(var(--terUser), 1); }
|
||||||
|
.fan-card-arcana { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.1em; color: rgba(var(--secUser), 1); }
|
||||||
|
.fan-card-correspondence { font-size: 0.6rem; font-style: italic; color: rgba(var(--secUser), 0.5); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.fan-nav {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 20;
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(var(--secUser), 0.6);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 1rem;
|
||||||
|
transition: color 0.15s;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
&:hover { color: rgba(var(--secUser), 1); }
|
||||||
|
// Suppress browser focus ring on mouse/touch clicks; retain it for keyboard nav
|
||||||
|
&:focus:not(:focus-visible) { outline: none; box-shadow: none; }
|
||||||
|
&--prev { left: 1rem; }
|
||||||
|
&--next { right: 1rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Sig Select overlay (SIG_SELECT phase) ────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Two overlays (levity / gravity) run in parallel, one per polarity group.
|
||||||
|
// Layout mirrors the gatekeeper: dark Gaussian backdrop + centred modal.
|
||||||
|
// Inside the modal: upper stage (card preview) + lower mini card grid (no scroll).
|
||||||
|
|
||||||
|
html:has(.sig-backdrop) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
z-index: 100;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 120;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig-modal {
|
||||||
|
pointer-events: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%; // respects overlay padding-right set by JS
|
||||||
|
max-width: 420px;
|
||||||
|
max-height: 100%; // respects overlay padding-bottom set by JS
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Stage ────────────────────────────────────────────────────────────────────
|
||||||
|
// flex: 1 — fills all space above the card grid; no background (backdrop blur).
|
||||||
|
// Row layout: preview card bottom-left, stat block fills the right.
|
||||||
|
// Card width is set by sizeSigCard() in room.js (smaller of 40% stage width or
|
||||||
|
// 80% stage height × 5/8) via --sig-card-w CSS variable — libsass can't handle
|
||||||
|
// container query units inside min().
|
||||||
|
|
||||||
|
.sig-stage {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
gap: 0.75rem;
|
||||||
|
|
||||||
|
// Preview card — width driven by JS via --sig-card-w; aspect-ratio derives height.
|
||||||
|
.sig-stage-card {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: var(--sig-card-w, 120px);
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 5 / 8;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: rgba(var(--priUser), 1);
|
||||||
|
border: 0.15rem solid rgba(var(--secUser), 0.6);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
padding: 0.25rem;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
// game-kit sets .fan-card-corner { position: absolute; top/left offsets }
|
||||||
|
// so these just need display/font overrides; the corners land at the card edges.
|
||||||
|
// All font-sizes scale with --sig-card-w (ratio = original-rem × 16 / 120).
|
||||||
|
.fan-card-corner--tl {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1.1;
|
||||||
|
gap: 0.1rem;
|
||||||
|
|
||||||
|
.fan-corner-rank { font-size: calc(var(--sig-card-w, 120px) * 0.133); font-weight: 700; }
|
||||||
|
i { font-size: calc(var(--sig-card-w, 120px) * 0.1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.fan-card-corner--br {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1.1;
|
||||||
|
gap: 0.1rem;
|
||||||
|
|
||||||
|
.fan-corner-rank { font-size: calc(var(--sig-card-w, 120px) * 0.12); font-weight: 700; }
|
||||||
|
i { font-size: calc(var(--sig-card-w, 120px) * 0.1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.fan-card-face {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.25rem 0.15rem;
|
||||||
|
gap: 0.2rem;
|
||||||
|
|
||||||
|
.fan-card-name-group { font-size: calc(var(--sig-card-w, 120px) * 0.073); opacity: 0.6; }
|
||||||
|
.sig-qualifier-above,
|
||||||
|
.sig-qualifier-below { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; }
|
||||||
|
.fan-card-name { font-size: calc(var(--sig-card-w, 120px) * 0.093); font-weight: 600; }
|
||||||
|
.fan-card-arcana { font-size: calc(var(--sig-card-w, 120px) * 0.067); text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.5; }
|
||||||
|
.fan-card-correspondence{ display: none; } // Minchiate equivalence shown in game-kit only
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat block — same dimensions as the preview card (width × 5:8 aspect).
|
||||||
|
// flex: 0 0 auto so it doesn't stretch to fill the stage; the rest of the
|
||||||
|
// stage row is simply empty, giving the card room to breathe.
|
||||||
|
.sig-stat-block {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: var(--sig-card-w, 120px);
|
||||||
|
height: calc(var(--sig-card-w, 120px) * 8 / 5);
|
||||||
|
align-self: flex-end;
|
||||||
|
background: rgba(var(--priUser), 0.5);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
border: 0.1rem solid rgba(var(--terUser), 0.15);
|
||||||
|
display: none;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
|
||||||
|
.sig-flip-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: -1rem;
|
||||||
|
right: -1rem;
|
||||||
|
margin: 0;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig-caution-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.25rem;
|
||||||
|
right: -1rem;
|
||||||
|
margin: 0;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caution tooltip — covers the entire stat block (inset: 0), z-index above buttons.
|
||||||
|
.sig-caution-tooltip {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 60;
|
||||||
|
background-color: rgba(var(--tooltip-bg), 0.6);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
border: 0.1rem solid rgba(var(--priYl), 0.35);
|
||||||
|
padding: 0.75rem;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig-caution-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig-caution-title {
|
||||||
|
font-size: calc(var(--sig-card-w, 120px) * 0.093);
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
color: rgba(var(--priYl), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig-caution-type {
|
||||||
|
font-size: calc(var(--sig-card-w, 120px) * 0.058);
|
||||||
|
opacity: 0.7;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig-caution-shoptalk {
|
||||||
|
font-size: calc(var(--sig-card-w, 120px) * 0.063);
|
||||||
|
opacity: 0.55;
|
||||||
|
margin: 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig-caution-effect {
|
||||||
|
flex: 1;
|
||||||
|
font-size: calc(var(--sig-card-w, 120px) * 0.075);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.55;
|
||||||
|
|
||||||
|
.card-ref {
|
||||||
|
color: rgba(var(--terUser), 1);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig-caution-index {
|
||||||
|
font-size: calc(var(--sig-card-w, 120px) * 0.063);
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nav arrows portaled out of tooltip — sit at bottom corners above tooltip (z-70)
|
||||||
|
.sig-caution-prev,
|
||||||
|
.sig-caution-next {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1rem;
|
||||||
|
margin: 0;
|
||||||
|
z-index: 70;
|
||||||
|
}
|
||||||
|
.sig-caution-prev { left: -1rem; }
|
||||||
|
.sig-caution-next { right: -1rem; }
|
||||||
|
|
||||||
|
.stat-face {
|
||||||
|
display: none;
|
||||||
|
padding: calc(var(--sig-card-w, 120px) * 0.37) calc(var(--sig-card-w, 120px) * 0.1) calc(var(--sig-card-w, 120px) * 0.08);
|
||||||
|
|
||||||
|
&--upright { display: block; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-reversed {
|
||||||
|
.stat-face--upright { display: none; }
|
||||||
|
.stat-face--reversed { display: block; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-face-label {
|
||||||
|
font-size: calc(var(--sig-card-w, 120px) * 0.063);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.09em;
|
||||||
|
opacity: 0.4;
|
||||||
|
margin: 0 0 calc(var(--sig-card-w, 120px) * 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-keywords {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: calc(var(--sig-card-w, 120px) * 0.083);
|
||||||
|
padding: calc(var(--sig-card-w, 120px) * 0.042) 0;
|
||||||
|
opacity: 0.85;
|
||||||
|
border-bottom: 0.05rem solid rgba(var(--terUser), 0.12);
|
||||||
|
|
||||||
|
&:last-child { border-bottom: none; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sig-stage--frozen .sig-stat-block { display: block; }
|
||||||
|
&.sig-caution-open .sig-stat-block {
|
||||||
|
.sig-caution-tooltip { display: flex; }
|
||||||
|
.sig-caution-prev, .sig-caution-next { display: inline-flex; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Mini card grid ───────────────────────────────────────────────────────────
|
||||||
|
// flex: 0 0 auto — shrinks to card content; no background (backdrop blur).
|
||||||
|
// align-content: start prevents CSS grid from distributing extra height between rows.
|
||||||
|
|
||||||
|
.sig-deck-grid {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
align-content: start;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0 1rem 5rem 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig-card {
|
||||||
|
aspect-ratio: 5 / 8;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background: rgba(var(--priUser), 0.97);
|
||||||
|
border: 1px solid rgba(var(--secUser), 0.3);
|
||||||
|
position: relative;
|
||||||
|
cursor: grab;
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
// game-kit sets .fan-card-corner { position:absolute; top:0.4rem; left:0.4rem }
|
||||||
|
// Override: center the element within the card instead.
|
||||||
|
.fan-card-corner--tl {
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
gap: 0; // game-kit has gap:0.15rem — too large at 0.5rem font-size
|
||||||
|
|
||||||
|
.fan-corner-rank { font-size: 1rem; font-weight: 700; }
|
||||||
|
i { font-size: 0.75rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK / NVM overlay — appears on click (focused) or own reservation
|
||||||
|
.sig-card-actions {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 3px;
|
||||||
|
background: rgba(var(--priUser), 0.92);
|
||||||
|
border-radius: inherit;
|
||||||
|
|
||||||
|
.sig-nvm-btn { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sig-focused .sig-card-actions { display: flex; }
|
||||||
|
&.sig-reserved--own .sig-card-actions {
|
||||||
|
display: flex;
|
||||||
|
.sig-ok-btn { display: none; }
|
||||||
|
.sig-nvm-btn { display: flex; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor strip — hangs below the card bottom edge; overflow: visible allows this.
|
||||||
|
.sig-card-cursors {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -0.6rem;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rise above DOM-order siblings when a peer's cursor is active on this card.
|
||||||
|
// Without this, later cards in the grid paint over the overflowing cursor icons.
|
||||||
|
&:has(.sig-cursor.active) { z-index: 5; }
|
||||||
|
|
||||||
|
&:hover:not([data-reserved-by]) {
|
||||||
|
border-color: rgba(var(--secUser), 0.8);
|
||||||
|
box-shadow: 0 0 4px rgba(var(--secUser), 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sig-reserved {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role-coloured reservation glow — border/shadow matches the reserving gamer's role.
|
||||||
|
// data-reserved-by is set by applyReservation() in sig-select.js.
|
||||||
|
// Own reservation also shows role colour (same as peers see), not a separate style.
|
||||||
|
&.sig-reserved {
|
||||||
|
&[data-reserved-by="PC"] { border-color: rgba(var(--priRd), 1); box-shadow: 0 0 0 2px rgba(var(--priRd), 1); }
|
||||||
|
&[data-reserved-by="NC"] { border-color: rgba(var(--priYl), 1); box-shadow: 0 0 0 2px rgba(var(--priYl), 1); }
|
||||||
|
&[data-reserved-by="EC"] { border-color: rgba(var(--priGn), 1); box-shadow: 0 0 0 2px rgba(var(--priGn), 1); }
|
||||||
|
&[data-reserved-by="SC"] { border-color: rgba(var(--priCy), 1); box-shadow: 0 0 0 2px rgba(var(--priCy), 1); }
|
||||||
|
&[data-reserved-by="AC"] { border-color: rgba(var(--priId), 1); box-shadow: 0 0 0 2px rgba(var(--priId), 1); }
|
||||||
|
&[data-reserved-by="BC"] { border-color: rgba(var(--priFs), 1); box-shadow: 0 0 0 2px rgba(var(--priFs), 1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sig-reserved--own {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Cursor anchors ───────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Three tiny dots along the bottom of each mini card, one per role in the group.
|
||||||
|
// Inactive: invisible. Active (another gamer is hovering): role-coloured dot.
|
||||||
|
// Position order is fixed per polarity (POLARITY_ROLES in sig-select.js):
|
||||||
|
// levity (PC / NC / SC) → left / mid / right
|
||||||
|
// gravity (BC / EC / AC) → left / mid / right
|
||||||
|
|
||||||
|
// In-card cursor elements — invisible anchors only.
|
||||||
|
// Visible icons are portaled to document root by applyHover() in sig-select.js.
|
||||||
|
.sig-cursor {
|
||||||
|
display: block;
|
||||||
|
font-size: 0; // zero-size: no layout impact, just carries .active class
|
||||||
|
color: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Floating cursor portal ───────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// sig-select.js creates these <i> elements inside #id_sig_cursor_portal, a
|
||||||
|
// position:fixed root-level container, so they escape all overflow/clip contexts.
|
||||||
|
// Positioned via getBoundingClientRect() on the card element.
|
||||||
|
|
||||||
|
#id_sig_cursor_portal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 200; // above sig-overlay (120), below tray (310)
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig-cursor-float {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
transform: translateX(-50%); // centre on the x coordinate from JS
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role-specific colour + outline shadow + ninUser glow
|
||||||
|
.sig-cursor-float[data-role="PC"] {
|
||||||
|
color: rgba(var(--priRd), 1);
|
||||||
|
text-shadow: 2px 0 0 rgba(var(--priOr),1), -2px 0 0 rgba(var(--priOr),1),
|
||||||
|
0 2px 0 rgba(var(--priOr),1), 0 -2px 0 rgba(var(--priOr),1),
|
||||||
|
0 0 6px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.sig-cursor-float[data-role="NC"] {
|
||||||
|
color: rgba(var(--priYl), 1);
|
||||||
|
text-shadow: 2px 0 0 rgba(var(--priLm),1), -2px 0 0 rgba(var(--priLm),1),
|
||||||
|
0 2px 0 rgba(var(--priLm),1), 0 -2px 0 rgba(var(--priLm),1),
|
||||||
|
0 0 6px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.sig-cursor-float[data-role="EC"] {
|
||||||
|
color: rgba(var(--priGn), 1);
|
||||||
|
text-shadow: 2px 0 0 rgba(var(--priTk),1), -2px 0 0 rgba(var(--priTk),1),
|
||||||
|
0 2px 0 rgba(var(--priTk),1), 0 -2px 0 rgba(var(--priTk),1),
|
||||||
|
0 0 6px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.sig-cursor-float[data-role="SC"] {
|
||||||
|
color: rgba(var(--priCy), 1);
|
||||||
|
text-shadow: 2px 0 0 rgba(var(--priBl),1), -2px 0 0 rgba(var(--priBl),1),
|
||||||
|
0 2px 0 rgba(var(--priBl),1), 0 -2px 0 rgba(var(--priBl),1),
|
||||||
|
0 0 6px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.sig-cursor-float[data-role="AC"] {
|
||||||
|
color: rgba(var(--priId), 1);
|
||||||
|
text-shadow: 2px 0 0 rgba(var(--priVt),1), -2px 0 0 rgba(var(--priVt),1),
|
||||||
|
0 2px 0 rgba(var(--priVt),1), 0 -2px 0 rgba(var(--priVt),1),
|
||||||
|
0 0 6px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
.sig-cursor-float[data-role="BC"] {
|
||||||
|
color: rgba(var(--priFs), 1);
|
||||||
|
text-shadow: 2px 0 0 rgba(var(--priMe),1), -2px 0 0 rgba(var(--priMe),1),
|
||||||
|
0 2px 0 rgba(var(--priMe),1), 0 -2px 0 rgba(var(--priMe),1),
|
||||||
|
0 0 6px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Polarity theming — card colour inversion ────────────────────────────────
|
||||||
|
//
|
||||||
|
// Gravity (Graven): --priUser bg / --secUser text — standard dark palette.
|
||||||
|
// Levity (Leavened): --secUser bg / --priUser text — inverted, lighter feel.
|
||||||
|
// Both mini-cards and the stage preview card follow the same rule.
|
||||||
|
|
||||||
|
.sig-overlay[data-polarity="levity"] {
|
||||||
|
// Mini card: inverted palette. game-kit sets explicit colours on .fan-card-name
|
||||||
|
// and .fan-card-corner that out-specifc the parent color, so re-target them here.
|
||||||
|
.sig-card {
|
||||||
|
background: rgba(var(--secUser), 0.97);
|
||||||
|
border-color: rgba(var(--priUser), 0.3);
|
||||||
|
color: rgba(var(--priUser), 1);
|
||||||
|
.fan-card-corner { color: rgba(var(--priUser), 0.75); }
|
||||||
|
.fan-card-name { color: rgba(var(--quiUser), 1); }
|
||||||
|
// OK / NVM overlay — must match the inverted card background
|
||||||
|
.sig-card-actions { background: rgba(var(--secUser), 0.92); }
|
||||||
|
}
|
||||||
|
// Stage preview card: same inversion + title colour.
|
||||||
|
// .fan-card-name-group and .fan-card-arcana have explicit color in the base
|
||||||
|
// .fan-card-face rule (specificity 0,2,0) — must re-target them here (0,3,0).
|
||||||
|
// Opacity dim is still applied by the nested sig-stage-card rule.
|
||||||
|
.sig-stage-card {
|
||||||
|
background: rgba(var(--secUser), 1);
|
||||||
|
border-color: rgba(var(--priUser), 0.6);
|
||||||
|
color: rgba(var(--priUser), 1);
|
||||||
|
.fan-card-corner { color: rgba(var(--priUser), 0.75); }
|
||||||
|
.fan-card-name-group{ color: rgba(var(--priUser), 1); }
|
||||||
|
.fan-card-name { color: rgba(var(--quiUser), 1); }
|
||||||
|
.fan-card-arcana { color: rgba(var(--priUser), 1); }
|
||||||
|
}
|
||||||
|
// Polarity qualifier: same colour as the card title in this context
|
||||||
|
.sig-qualifier-above,
|
||||||
|
.sig-qualifier-below { color: rgba(var(--quiUser), 1); }
|
||||||
|
// card-ref spans inside the caution tooltip — must match the base rule's
|
||||||
|
// .sig-stat-block .sig-caution-effect .card-ref specificity (0,3,0) to win.
|
||||||
|
.sig-caution-effect .card-ref { color: rgba(var(--quiUser), 1); }
|
||||||
|
// Cursor colours live in .sig-cursor-float[data-role] rules (portal elements)
|
||||||
|
}
|
||||||
|
.sig-overlay[data-polarity="gravity"] {
|
||||||
|
// Stat block: invert priUser/secUser so gravity gets the same stark contrast as leavened cards
|
||||||
|
.sig-stat-block {
|
||||||
|
background: rgba(var(--secUser), 0.75);
|
||||||
|
color: rgba(var(--priUser), 1);
|
||||||
|
border-color: rgba(var(--priUser), 0.15);
|
||||||
|
}
|
||||||
|
// Caution tooltip: --tooltip-bg is black so priUser text (dark) would be invisible —
|
||||||
|
// override to secUser (light) so body text reads against the dark backdrop.
|
||||||
|
.sig-caution-tooltip { color: rgba(var(--secUser), 1); }
|
||||||
|
// Polarity qualifier: terUser for gravity (quiUser is levity's equivalent)
|
||||||
|
.sig-qualifier-above,
|
||||||
|
.sig-qualifier-below { color: rgba(var(--terUser), 1); }
|
||||||
|
// Cursor colours live in .sig-cursor-float[data-role] rules (portal elements)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Sig select: landscape overrides ─────────────────────────────────────────
|
||||||
|
// Landscape base: 9×2 grid of 3rem cards. At ≥992px (wide enough for 18 cards
|
||||||
|
// at 3rem + 4rem left + ~4rem right): collapse to a single 18×1 row so the
|
||||||
|
// stage preview gets maximum vertical real-estate.
|
||||||
|
// padding-left clears the fixed left navbar (JS sets right/bottom but not left).
|
||||||
|
// Grid margins reset to 0 — overlay padding handles all edge clearance.
|
||||||
|
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
.sig-modal {
|
||||||
|
max-width: none;
|
||||||
|
flex-direction: row; // grid to the right, stage + card preview to the left
|
||||||
|
margin-left: 4rem;
|
||||||
|
margin-right: 3rem;
|
||||||
|
}
|
||||||
|
.sig-stage {
|
||||||
|
min-width: 0; // allow shrinking in row layout; align-items:flex-end already set
|
||||||
|
}
|
||||||
|
.sig-deck-grid {
|
||||||
|
grid-template-columns: repeat(6, 2.5rem);
|
||||||
|
margin: 0;
|
||||||
|
align-self: flex-end; // sit at the bottom of the modal row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) and (min-width: 900px) {
|
||||||
|
// Wide landscape: revert to stacked layout (stage top, 18-card row grid bottom).
|
||||||
|
.sig-modal {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.sig-stage {
|
||||||
|
min-width: auto;
|
||||||
|
align-self: stretch; // fill full modal width so JS sizeSigCard() gets correct stageWidth
|
||||||
|
margin-left: 3rem;
|
||||||
|
}
|
||||||
|
.sig-deck-grid {
|
||||||
|
grid-template-columns: repeat(18, 3rem);
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) and (min-width: 1800px) {
|
||||||
|
// Sig overlay: clear doubled sidebars (8rem each instead of 4rem/6rem)
|
||||||
|
.sig-overlay { padding-left: 8rem; padding-right: 8rem; }
|
||||||
|
.sig-stage {
|
||||||
|
align-self: stretch; // fill full modal width so JS sizeSigCard() gets correct stageWidth
|
||||||
|
margin-left: 3rem;
|
||||||
|
}
|
||||||
|
.sig-deck-grid {
|
||||||
|
grid-template-columns: repeat(18, 5rem);
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room menu: base right: 0.5rem (same-specificity ID rule) overrides _applets.scss
|
||||||
|
// XL block because _card-deck.scss is imported after _applets.scss. Re-declare here to win the cascade.
|
||||||
|
#id_room_menu { right: 2.5rem; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ body.page-dashboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (orientation: landscape) and (max-width: 1440px) {
|
@media (orientation: landscape) {
|
||||||
// Reset the 666px min-width so #id_dash_content shrinks to fit within the
|
// Reset the 666px min-width so #id_dash_content shrinks to fit within the
|
||||||
// sidebar-bounded container rather than overflowing into the footer sidebar.
|
// sidebar-bounded container rather than overflowing into the footer sidebar.
|
||||||
#id_dash_content {
|
#id_dash_content {
|
||||||
|
|||||||
@@ -3,12 +3,16 @@
|
|||||||
bottom: 0.5rem;
|
bottom: 0.5rem;
|
||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
|
|
||||||
@media (orientation: landscape) and (max-width: 1440px) {
|
@media (orientation: landscape) {
|
||||||
right: 0.5rem;
|
right: 1rem;
|
||||||
bottom: 0.5rem;
|
bottom: 0.5rem;
|
||||||
top: auto;
|
top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) and (min-width: 1800px) {
|
||||||
|
right: 2.5rem; // centre in doubled 8rem sidebar
|
||||||
|
}
|
||||||
|
|
||||||
z-index: 318;
|
z-index: 318;
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -45,7 +49,7 @@
|
|||||||
z-index: 316;
|
z-index: 316;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@media (orientation: landscape) and (max-width: 1440px) {
|
@media (orientation: landscape) {
|
||||||
$sidebar-w: 4rem;
|
$sidebar-w: 4rem;
|
||||||
// left: $sidebar-w;
|
// left: $sidebar-w;
|
||||||
right: $sidebar-w;
|
right: $sidebar-w;
|
||||||
@@ -208,118 +212,3 @@
|
|||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tarot fan modal ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#id_tarot_fan_dialog {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
max-width: none;
|
|
||||||
max-height: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
background: rgba(0, 0, 0, 0.88);
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&::backdrop { display: none; } // Dialog IS the backdrop
|
|
||||||
}
|
|
||||||
|
|
||||||
.tarot-fan-wrap {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
perspective: 900px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
box-shadow: none;
|
|
||||||
|
|
||||||
&:hover, &.active {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tarot-fan {
|
|
||||||
position: relative;
|
|
||||||
width: 220px;
|
|
||||||
height: 340px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fan-card {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 220px;
|
|
||||||
height: 340px;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
background: rgba(var(--priUser), 1);
|
|
||||||
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: transform 0.25s ease, opacity 0.25s ease;
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
|
|
||||||
&--active {
|
|
||||||
border-color: rgba(var(--secUser), 1);
|
|
||||||
box-shadow: 0 0 2rem rgba(var(--secUser), 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fan-card-corner {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.15rem;
|
|
||||||
line-height: 1;
|
|
||||||
color: rgba(var(--secUser), 0.75);
|
|
||||||
|
|
||||||
&--tl { top: 0.4rem; left: 0.4rem; }
|
|
||||||
&--br { bottom: 0.4rem; right: 0.4rem; transform: rotate(180deg); }
|
|
||||||
|
|
||||||
.fan-corner-rank {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 0.18rem 0;
|
|
||||||
}
|
|
||||||
i { font-size: 1.5rem; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.fan-card-face {
|
|
||||||
padding: 1.25rem;
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
.fan-card-number { font-size: 0.65rem; }
|
|
||||||
.fan-card-name-group { font-size: 0.65rem; margin: 0; text-transform: uppercase; letter-spacing: 0.08em; color: rgba(var(--secUser), 1); }
|
|
||||||
.fan-card-name { font-size: 0.95rem; font-weight: bold; margin: 0; color: rgba(var(--terUser), 1); }
|
|
||||||
.fan-card-arcana { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.1em; color: rgba(var(--secUser), 1); }
|
|
||||||
.fan-card-correspondence { font-size: 0.6rem; font-style: italic; color: rgba(var(--secUser), 0.5); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.fan-nav {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 20;
|
|
||||||
font-size: 3rem;
|
|
||||||
line-height: 1;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(var(--secUser), 0.6);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 1rem;
|
|
||||||
transition: color 0.15s;
|
|
||||||
pointer-events: auto;
|
|
||||||
|
|
||||||
&:hover { color: rgba(var(--secUser), 1); }
|
|
||||||
// Suppress browser focus ring on mouse/touch clicks; retain it for keyboard nav
|
|
||||||
&:focus:not(:focus-visible) { outline: none; box-shadow: none; }
|
|
||||||
&--prev { left: 1rem; }
|
|
||||||
&--next { right: 1rem; }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ body.page-gameboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (orientation: landscape) and (max-width: 1440px) {
|
@media (orientation: landscape) {
|
||||||
// Restore clip in landscape — overrides the >738px overflow:visible above,
|
// Restore clip in landscape — overrides the >738px overflow:visible above,
|
||||||
// preventing the gameboard applets from bleeding into the footer sidebar.
|
// preventing the gameboard applets from bleeding into the footer sidebar.
|
||||||
body.page-gameboard .container {
|
body.page-gameboard .container {
|
||||||
|
|||||||
@@ -800,291 +800,43 @@ $card-h: 60px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
// Sink navbar below gate/role-select overlays when a modal is open.
|
// Sink navbar + footer sidebar below any modal backdrop when open.
|
||||||
// Landscape navbar z-index is 100 (_base.scss); gate-backdrop/overlay are
|
// Landscape navbar and footer sidebar are both z-index:100 (_base.scss).
|
||||||
// 100/120 — same level causes paint-order ties so we drop it to 50.
|
// Gate/role-select/sig backdrops are also z-index:100 — DOM paint-order ties
|
||||||
|
// let the footer (later in DOM) bleed through. Drop both to 50.
|
||||||
html:has(.gate-backdrop) body .container .navbar,
|
html:has(.gate-backdrop) body .container .navbar,
|
||||||
html:has(.role-select-backdrop) body .container .navbar {
|
html:has(.role-select-backdrop) body .container .navbar,
|
||||||
|
html:has(.sig-backdrop) body .container .navbar {
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
html:has(.gate-backdrop) body #id_footer,
|
||||||
|
html:has(.role-select-backdrop) body #id_footer,
|
||||||
|
html:has(.sig-backdrop) body #id_footer {
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reflow position strip into a vertical column along the left edge,
|
// Position strip: horizontal row across the top, slots 1-6 in order.
|
||||||
// reversed so 6 is at top, 1 at bottom, below the GAMEROOM title.
|
// Offset from both sidebars (5rem each) and centred with gap.
|
||||||
.position-strip {
|
.position-strip {
|
||||||
flex-direction: column-reverse;
|
flex-direction: row;
|
||||||
top: 3rem;
|
top: 2.5rem;
|
||||||
left: 0.5rem;
|
left: 5rem;
|
||||||
right: auto;
|
right: 5rem;
|
||||||
|
justify-content: center;
|
||||||
gap: round($gate-gap * 0.4);
|
gap: round($gate-gap * 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shallow landscape (phones): wrap into two columns — left: 6,5,4 / right: 3,2,1
|
// Small landscape (phones ≤550px tall): strip stays horizontal — no two-column
|
||||||
// Columns grow rightward (wrap, not wrap-reverse) so overflow: hidden doesn't clip.
|
// trick needed now that the h2 is in the gutter. Just clear any order overrides.
|
||||||
// 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) {
|
@media (max-height: 550px) {
|
||||||
.position-strip {
|
.position-strip {
|
||||||
flex-wrap: wrap;
|
.gate-slot { order: 0; }
|
||||||
// cap height to exactly 3 circles so the 4th wraps to a new column
|
top: 1rem;
|
||||||
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; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ─── Sig Select overlay (SIG_SELECT phase) ────────────────────────────────────
|
|
||||||
//
|
|
||||||
// Two overlays (levity / gravity) run in parallel, one per polarity group.
|
|
||||||
// Layout mirrors the gatekeeper: dark Gaussian backdrop + centred modal.
|
|
||||||
// Inside the modal: upper stage (card preview) + lower mini card grid (no scroll).
|
|
||||||
|
|
||||||
html:has(.sig-backdrop) {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.75);
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
z-index: 100;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 120;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-modal {
|
|
||||||
pointer-events: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%; // respects overlay padding-right set by JS
|
|
||||||
max-width: 420px;
|
|
||||||
max-height: 100%; // respects overlay padding-bottom set by JS
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Stage ────────────────────────────────────────────────────────────────────
|
|
||||||
// flex: 1 — fills all space above the card grid; no background (backdrop blur).
|
|
||||||
// Row layout: preview card bottom-left, stat block fills the right.
|
|
||||||
// Card width is set by sizeSigCard() in room.js (smaller of 40% stage width or
|
|
||||||
// 80% stage height × 5/8) via --sig-card-w CSS variable — libsass can't handle
|
|
||||||
// container query units inside min().
|
|
||||||
|
|
||||||
.sig-stage {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-end;
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
gap: 0.75rem;
|
|
||||||
|
|
||||||
// Preview card — width driven by JS via --sig-card-w; aspect-ratio derives height.
|
|
||||||
.sig-stage-card {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: var(--sig-card-w, 120px);
|
|
||||||
height: auto;
|
|
||||||
aspect-ratio: 5 / 8;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
background: rgba(var(--priUser), 1);
|
|
||||||
border: 0.15rem solid rgba(var(--secUser), 0.6);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
padding: 0.25rem;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
// game-kit sets .fan-card-corner { position: absolute; top/left offsets }
|
|
||||||
// so these just need display/font overrides; the corners land at the card edges.
|
|
||||||
.fan-card-corner--tl {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
line-height: 1.1;
|
|
||||||
gap: 0.1rem;
|
|
||||||
|
|
||||||
.fan-corner-rank { font-size: 1rem; font-weight: 700; }
|
|
||||||
i { font-size: 0.75rem; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.fan-card-corner--br {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
line-height: 1.1;
|
|
||||||
gap: 0.1rem;
|
|
||||||
|
|
||||||
.fan-corner-rank { font-size: 0.9rem; font-weight: 700; }
|
|
||||||
i { font-size: 0.75rem; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.fan-card-face {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0.25rem 0.15rem;
|
|
||||||
gap: 0.2rem;
|
|
||||||
|
|
||||||
.fan-card-name-group { font-size: 0.55rem; opacity: 0.6; }
|
|
||||||
.fan-card-name { font-size: 0.7rem; font-weight: 600; }
|
|
||||||
.fan-card-arcana { font-size: 0.5rem; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.5; }
|
|
||||||
.fan-card-correspondence{ font-size: 0.5rem; opacity: 0.5; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stat block — same height as the preview card; fills remaining stage width.
|
|
||||||
// Height derived from the JS-computed --sig-card-w via aspect ratio 5:8.
|
|
||||||
.sig-stat-block {
|
|
||||||
flex: 1;
|
|
||||||
height: calc(var(--sig-card-w, 120px) * 8 / 5);
|
|
||||||
align-self: flex-end;
|
|
||||||
background: rgba(var(--priUser), 0.25);
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
border: 0.1rem solid rgba(var(--terUser), 0.15);
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sig-stage--frozen .sig-stat-block { display: block; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Mini card grid ───────────────────────────────────────────────────────────
|
|
||||||
// flex: 0 0 auto — shrinks to card content; no background (backdrop blur).
|
|
||||||
// align-content: start prevents CSS grid from distributing extra height between rows.
|
|
||||||
|
|
||||||
.sig-deck-grid {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(6, 1fr);
|
|
||||||
align-content: start;
|
|
||||||
gap: 2px;
|
|
||||||
padding: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 0 1rem 5rem 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sig-card {
|
|
||||||
aspect-ratio: 5 / 8;
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
background: rgba(var(--priUser), 0.97);
|
|
||||||
border: 1px solid rgba(var(--secUser), 0.3);
|
|
||||||
position: relative;
|
|
||||||
cursor: grab;
|
|
||||||
transition: border-color 0.15s, box-shadow 0.15s;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
// game-kit sets .fan-card-corner { position:absolute; top:0.4rem; left:0.4rem }
|
|
||||||
// Override: center the element within the card instead.
|
|
||||||
.fan-card-corner--tl {
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
gap: 0; // game-kit has gap:0.15rem — too large at 0.5rem font-size
|
|
||||||
|
|
||||||
.fan-corner-rank { font-size: 1rem; font-weight: 700; }
|
|
||||||
i { font-size: 0.75rem; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// OK / NVM overlay — appears on click (focused) or own reservation
|
|
||||||
.sig-card-actions {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: none;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 3px;
|
|
||||||
background: rgba(var(--priUser), 0.92);
|
|
||||||
border-radius: inherit;
|
|
||||||
|
|
||||||
.sig-nvm-btn { display: none; }
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sig-focused .sig-card-actions { display: flex; }
|
|
||||||
&.sig-reserved--own .sig-card-actions {
|
|
||||||
display: flex;
|
|
||||||
.sig-ok-btn { display: none; }
|
|
||||||
.sig-nvm-btn { display: flex; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cursor anchors strip — bottom of card
|
|
||||||
.sig-card-cursors {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 2px;
|
|
||||||
left: 2px;
|
|
||||||
right: 2px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover:not([data-reserved-by]) {
|
|
||||||
border-color: rgba(var(--secUser), 0.8);
|
|
||||||
box-shadow: 0 0 4px rgba(var(--secUser), 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sig-reserved {
|
|
||||||
border-color: rgba(var(--terUser), 1);
|
|
||||||
box-shadow:
|
|
||||||
0 0 0.4rem rgba(var(--terUser), 0.7),
|
|
||||||
0 0 1rem rgba(var(--ninUser), 0.4);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.sig-reserved--own {
|
|
||||||
border-color: rgba(var(--secUser), 1);
|
|
||||||
box-shadow:
|
|
||||||
0 0 0.4rem rgba(var(--secUser), 0.7),
|
|
||||||
0 0 1rem rgba(var(--ninUser), 0.5);
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Cursor anchors ───────────────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// Three tiny dots along the bottom of each mini card, one per role in the group.
|
|
||||||
// Inactive: invisible. Active (another gamer is hovering): coloured dot.
|
|
||||||
|
|
||||||
.sig-cursor {
|
|
||||||
display: block;
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: transparent;
|
|
||||||
transition: background 0.1s;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: rgba(var(--terUser), 1);
|
|
||||||
box-shadow: 0 0 3px rgba(var(--ninUser), 0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Sig select: landscape overrides ─────────────────────────────────────────
|
|
||||||
// Wider viewport → 2 rows of 9 cards; modal fills full available width.
|
|
||||||
// padding-left clears the fixed left navbar (JS sets right/bottom but not left).
|
|
||||||
// Grid margins reset to 0 — overlay padding handles all edge clearance in landscape.
|
|
||||||
|
|
||||||
@media (orientation: landscape) {
|
|
||||||
.sig-overlay { padding-left: 4rem; }
|
|
||||||
.sig-modal { max-width: none; }
|
|
||||||
.sig-deck-grid {
|
|
||||||
grid-template-columns: repeat(9, minmax(0, 90px));
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Seat tray — see _tray.scss ─────────────────────────────────────────────
|
// ─── Seat tray — see _tray.scss ─────────────────────────────────────────────
|
||||||
|
|||||||
@@ -121,24 +121,29 @@ $handle-r: 1rem;
|
|||||||
&::before { border-color: rgba(var(--quaUser), 1); }
|
&::before { border-color: rgba(var(--quaUser), 1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Role card: arc-in animation (portrait) ─────────────────────────────────
|
// ─── Role card: scrawl fade-in ───────────────────────────────────────────────
|
||||||
@keyframes tray-role-arc-in {
|
@keyframes tray-role-arc-in {
|
||||||
from { opacity: 0; transform: scale(0.3) translate(-40%, -40%); }
|
from { opacity: 0; }
|
||||||
to { opacity: 1; transform: scale(1) translate(0, 0); }
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.tray-role-card {
|
.tray-role-card {
|
||||||
background: rgba(var(--quaUser), 0.25);
|
padding: 0;
|
||||||
display: flex;
|
overflow: hidden;
|
||||||
align-items: flex-start;
|
background: transparent;
|
||||||
justify-content: flex-start;
|
|
||||||
padding: 0.2em;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
color: rgba(var(--quaUser), 1);
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
&.arc-in {
|
img {
|
||||||
animation: tray-role-arc-in 1.0s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
transform: scale(1.4); // crop SVG's internal margins
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cell stays static; only the scrawl image fades in.
|
||||||
|
&.arc-in img {
|
||||||
|
animation: tray-role-arc-in 1s ease forwards;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,15 +306,7 @@ $handle-r: 1rem;
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Role card arc-in for landscape
|
// Role card: same fade-in in landscape — no override needed.
|
||||||
@keyframes tray-role-arc-in-landscape {
|
|
||||||
from { opacity: 0; transform: scale(0.3) translate(-40%, 40%); }
|
|
||||||
to { opacity: 1; transform: scale(1) translate(0, 0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.tray-role-card.arc-in {
|
|
||||||
animation: tray-role-arc-in-landscape 1.0s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes tray-wobble-landscape {
|
@keyframes tray-wobble-landscape {
|
||||||
0%, 100% { transform: translateY(0); }
|
0%, 100% { transform: translateY(0); }
|
||||||
@@ -328,3 +325,5 @@ $handle-r: 1rem;
|
|||||||
80% { transform: translateY(-3px); }
|
80% { transform: translateY(-3px); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ≥1800px uses the same landscape tray rules as narrower landscape — no override block needed.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
@import 'gameboard';
|
@import 'gameboard';
|
||||||
@import 'palette-picker';
|
@import 'palette-picker';
|
||||||
@import 'room';
|
@import 'room';
|
||||||
|
@import 'card-deck';
|
||||||
@import 'tray';
|
@import 'tray';
|
||||||
@import 'billboard';
|
@import 'billboard';
|
||||||
@import 'game-kit';
|
@import 'game-kit';
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
describe("SigSelect", () => {
|
describe("SigSelect", () => {
|
||||||
let testDiv, stageCard, card;
|
let testDiv, stageCard, card, statBlock;
|
||||||
|
|
||||||
function makeFixture({ reservations = '{}' } = {}) {
|
function makeFixture({ reservations = '{}', cardCautions = '[]', polarity = 'levity', userRole = 'PC' } = {}) {
|
||||||
testDiv = document.createElement("div");
|
testDiv = document.createElement("div");
|
||||||
testDiv.innerHTML = `
|
testDiv.innerHTML = `
|
||||||
<div class="sig-overlay"
|
<div class="sig-overlay"
|
||||||
data-polarity="levity"
|
data-polarity="${polarity}"
|
||||||
data-user-role="PC"
|
data-user-role="${userRole}"
|
||||||
data-reserve-url="/epic/room/test/sig-reserve"
|
data-reserve-url="/epic/room/test/sig-reserve"
|
||||||
data-reservations="${reservations.replace(/"/g, '"')}">
|
data-reservations="${reservations.replace(/"/g, '"')}">
|
||||||
<div class="sig-modal">
|
<div class="sig-modal">
|
||||||
@@ -15,10 +15,35 @@ describe("SigSelect", () => {
|
|||||||
<span class="fan-corner-rank"></span>
|
<span class="fan-corner-rank"></span>
|
||||||
<i class="stage-suit-icon"></i>
|
<i class="stage-suit-icon"></i>
|
||||||
<p class="fan-card-name-group"></p>
|
<p class="fan-card-name-group"></p>
|
||||||
|
<p class="sig-qualifier-above"></p>
|
||||||
<h3 class="fan-card-name"></h3>
|
<h3 class="fan-card-name"></h3>
|
||||||
|
<p class="sig-qualifier-below"></p>
|
||||||
<p class="fan-card-arcana"></p>
|
<p class="fan-card-arcana"></p>
|
||||||
<p class="fan-card-correspondence"></p>
|
<p class="fan-card-correspondence"></p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="sig-stat-block">
|
||||||
|
<button class="btn btn-reverse sig-flip-btn" type="button">FLIP</button>
|
||||||
|
<button class="btn btn-caution sig-caution-btn" type="button">!!</button>
|
||||||
|
<div class="stat-face stat-face--upright">
|
||||||
|
<p class="stat-face-label">Upright</p>
|
||||||
|
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="stat-face stat-face--reversed">
|
||||||
|
<p class="stat-face-label">Reversed</p>
|
||||||
|
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-nav-left sig-caution-prev" type="button">◀</button>
|
||||||
|
<button class="btn btn-nav-right sig-caution-next" type="button">▶</button>
|
||||||
|
<div class="sig-caution-tooltip" id="id_sig_caution">
|
||||||
|
<div class="sig-caution-header">
|
||||||
|
<h4 class="sig-caution-title">Caution!</h4>
|
||||||
|
<span class="sig-caution-type">Rival Interaction</span>
|
||||||
|
</div>
|
||||||
|
<p class="sig-caution-shoptalk">[Shoptalk forthcoming]</p>
|
||||||
|
<p class="sig-caution-effect"></p>
|
||||||
|
<span class="sig-caution-index"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sig-deck-grid">
|
<div class="sig-deck-grid">
|
||||||
<div class="sig-card"
|
<div class="sig-card"
|
||||||
@@ -28,7 +53,10 @@ describe("SigSelect", () => {
|
|||||||
data-name-group="Pentacles"
|
data-name-group="Pentacles"
|
||||||
data-name-title="King of Pentacles"
|
data-name-title="King of Pentacles"
|
||||||
data-arcana="Minor Arcana"
|
data-arcana="Minor Arcana"
|
||||||
data-correspondence="">
|
data-correspondence=""
|
||||||
|
data-keywords-upright="action,impulsiveness,ambition"
|
||||||
|
data-keywords-reversed="no direction,disregard for consequences"
|
||||||
|
data-cautions="${cardCautions.replace(/"/g, '"')}">
|
||||||
<div class="fan-card-corner fan-card-corner--tl">
|
<div class="fan-card-corner fan-card-corner--tl">
|
||||||
<span class="fan-corner-rank">K</span>
|
<span class="fan-corner-rank">K</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,6 +76,7 @@ describe("SigSelect", () => {
|
|||||||
`;
|
`;
|
||||||
document.body.appendChild(testDiv);
|
document.body.appendChild(testDiv);
|
||||||
stageCard = testDiv.querySelector(".sig-stage-card");
|
stageCard = testDiv.querySelector(".sig-stage-card");
|
||||||
|
statBlock = testDiv.querySelector(".sig-stat-block");
|
||||||
card = testDiv.querySelector(".sig-card");
|
card = testDiv.querySelector(".sig-card");
|
||||||
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||||
Promise.resolve({ ok: true })
|
Promise.resolve({ ok: true })
|
||||||
@@ -107,80 +136,6 @@ describe("SigSelect", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Touch: OK btn tap allows synthetic click through ──────────────── //
|
|
||||||
|
|
||||||
describe("touch on OK button", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
|
|
||||||
makeFixture();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("touchstart on OK btn does not call preventDefault (allows synthetic click)", () => {
|
|
||||||
// First tap the card body to show OK
|
|
||||||
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
||||||
expect(card.classList.contains("sig-focused")).toBe(true);
|
|
||||||
|
|
||||||
// Now tap the OK button — touchstart should NOT preventDefault
|
|
||||||
var okBtn = card.querySelector(".sig-ok-btn");
|
|
||||||
var touchEvent = new TouchEvent("touchstart", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
touches: [new Touch({ identifier: 1, target: okBtn })],
|
|
||||||
});
|
|
||||||
okBtn.dispatchEvent(touchEvent);
|
|
||||||
expect(touchEvent.defaultPrevented).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("touchstart on card body (not OK btn) calls preventDefault", () => {
|
|
||||||
var touchEvent = new TouchEvent("touchstart", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
touches: [new Touch({ identifier: 1, target: card })],
|
|
||||||
});
|
|
||||||
card.dispatchEvent(touchEvent);
|
|
||||||
expect(touchEvent.defaultPrevented).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Touch outside grid dismisses stage (mobile) ───────────────────── //
|
|
||||||
|
|
||||||
describe("touch outside grid", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
|
|
||||||
makeFixture();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("dismisses stage preview when touching outside the grid (unfocused state)", () => {
|
|
||||||
// Focus a card first
|
|
||||||
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
||||||
expect(stageCard.style.display).toBe("");
|
|
||||||
|
|
||||||
// Touch on the sig-stage (outside the grid)
|
|
||||||
var stage = testDiv.querySelector(".sig-stage");
|
|
||||||
stage.dispatchEvent(new TouchEvent("touchstart", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
touches: [new Touch({ identifier: 2, target: stage })],
|
|
||||||
}));
|
|
||||||
expect(stageCard.style.display).toBe("none");
|
|
||||||
expect(card.classList.contains("sig-focused")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does NOT dismiss stage preview when frozen (card reserved)", () => {
|
|
||||||
card.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
||||||
SigSelect._setFrozen(true);
|
|
||||||
// _focusedCardEl is set but frozen — use internal state trick via _setFrozen
|
|
||||||
// We also need a focused card; simulate it by setting frozen after focus
|
|
||||||
var stage = testDiv.querySelector(".sig-stage");
|
|
||||||
stage.dispatchEvent(new TouchEvent("touchstart", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
touches: [new Touch({ identifier: 3, target: stage })],
|
|
||||||
}));
|
|
||||||
expect(stageCard.style.display).toBe("");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Lock after reservation ─────────────────────────────────────────── //
|
// ── Lock after reservation ─────────────────────────────────────────── //
|
||||||
|
|
||||||
describe("lock after reservation", () => {
|
describe("lock after reservation", () => {
|
||||||
@@ -200,18 +155,6 @@ describe("SigSelect", () => {
|
|||||||
expect(window.fetch).not.toHaveBeenCalled();
|
expect(window.fetch).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not call preventDefault on touchstart while a card is reserved", () => {
|
|
||||||
if (typeof TouchEvent === 'undefined') { pending('TouchEvent unavailable in desktop Firefox'); return; }
|
|
||||||
SigSelect._setReservedCardId("99");
|
|
||||||
var touchEvent = new TouchEvent("touchstart", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
touches: [new Touch({ identifier: 1, target: card })],
|
|
||||||
});
|
|
||||||
card.dispatchEvent(touchEvent);
|
|
||||||
expect(touchEvent.defaultPrevented).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows focus again after reservation is cleared", () => {
|
it("allows focus again after reservation is cleared", () => {
|
||||||
SigSelect._setReservedCardId("99");
|
SigSelect._setReservedCardId("99");
|
||||||
SigSelect._setReservedCardId(null);
|
SigSelect._setReservedCardId(null);
|
||||||
@@ -252,4 +195,414 @@ describe("SigSelect", () => {
|
|||||||
expect(card.classList.contains("sig-focused")).toBe(true);
|
expect(card.classList.contains("sig-focused")).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Caution tooltip (!!) ──────────────────────────────────────────── //
|
||||||
|
|
||||||
|
describe("caution tooltip", () => {
|
||||||
|
var cautionTooltip, cautionEffect, cautionPrev, cautionNext, cautionBtn;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
makeFixture();
|
||||||
|
cautionTooltip = testDiv.querySelector(".sig-caution-tooltip");
|
||||||
|
cautionEffect = testDiv.querySelector(".sig-caution-effect");
|
||||||
|
cautionPrev = testDiv.querySelector(".sig-caution-prev");
|
||||||
|
cautionNext = testDiv.querySelector(".sig-caution-next");
|
||||||
|
cautionBtn = testDiv.querySelector(".sig-caution-btn");
|
||||||
|
});
|
||||||
|
|
||||||
|
function hover() {
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCaution() {
|
||||||
|
hover();
|
||||||
|
cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
it("!! click adds .sig-caution-open to the stage", () => {
|
||||||
|
openCaution();
|
||||||
|
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("FYI click when btn-disabled does not close caution", () => {
|
||||||
|
openCaution();
|
||||||
|
expect(cautionBtn.classList.contains("btn-disabled")).toBe(true);
|
||||||
|
cautionBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows placeholder text when cautions list is empty", () => {
|
||||||
|
card.dataset.cautions = "[]";
|
||||||
|
openCaution();
|
||||||
|
expect(cautionEffect.innerHTML).toContain("pending");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders first caution effect HTML including .card-ref spans", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(['First <span class="card-ref">Card</span> effect.']);
|
||||||
|
openCaution();
|
||||||
|
expect(cautionEffect.querySelector(".card-ref")).not.toBeNull();
|
||||||
|
expect(cautionEffect.querySelector(".card-ref").textContent).toBe("Card");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with 1 caution both nav arrows are disabled", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["Single caution."]);
|
||||||
|
openCaution();
|
||||||
|
expect(cautionPrev.disabled).toBe(true);
|
||||||
|
expect(cautionNext.disabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with multiple cautions both nav arrows are always enabled", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["C1", "C2", "C3", "C4"]);
|
||||||
|
openCaution();
|
||||||
|
expect(cautionPrev.disabled).toBe(false);
|
||||||
|
expect(cautionNext.disabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("next click advances to second caution", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["First", "Second"]);
|
||||||
|
openCaution();
|
||||||
|
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(cautionEffect.innerHTML).toContain("Second");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("next wraps from last caution back to first", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["First", "Last"]);
|
||||||
|
openCaution();
|
||||||
|
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(cautionEffect.innerHTML).toContain("First");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prev click goes back to first caution", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["First", "Second"]);
|
||||||
|
openCaution();
|
||||||
|
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(cautionEffect.innerHTML).toContain("First");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prev wraps from first caution to last", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["First", "Middle", "Last"]);
|
||||||
|
openCaution();
|
||||||
|
cautionPrev.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(cautionEffect.innerHTML).toContain("Last");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("index label shows n / total when multiple cautions", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["C1", "C2", "C3"]);
|
||||||
|
openCaution();
|
||||||
|
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("1 / 3");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("index label is empty when only 1 caution", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["Only one."]);
|
||||||
|
openCaution();
|
||||||
|
expect(testDiv.querySelector(".sig-caution-index").textContent).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("card mouseleave closes the caution", () => {
|
||||||
|
openCaution();
|
||||||
|
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opening again resets to first caution", () => {
|
||||||
|
card.dataset.cautions = JSON.stringify(["First", "Second"]);
|
||||||
|
openCaution();
|
||||||
|
cautionNext.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
// Close and reopen
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||||
|
openCaution();
|
||||||
|
expect(cautionEffect.innerHTML).toContain("First");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opening caution adds .btn-disabled and swaps labels to ×", () => {
|
||||||
|
openCaution();
|
||||||
|
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
||||||
|
expect(flipBtn.classList.contains("btn-disabled")).toBe(true);
|
||||||
|
expect(cautionBtn.classList.contains("btn-disabled")).toBe(true);
|
||||||
|
expect(flipBtn.textContent).toBe("\u00D7");
|
||||||
|
expect(cautionBtn.textContent).toBe("\u00D7");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("closing caution removes .btn-disabled and restores original labels", () => {
|
||||||
|
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
||||||
|
var origFlip = flipBtn.textContent;
|
||||||
|
var origCaution = cautionBtn.textContent;
|
||||||
|
openCaution();
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||||
|
expect(flipBtn.classList.contains("btn-disabled")).toBe(false);
|
||||||
|
expect(cautionBtn.classList.contains("btn-disabled")).toBe(false);
|
||||||
|
expect(flipBtn.textContent).toBe(origFlip);
|
||||||
|
expect(cautionBtn.textContent).toBe(origCaution);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking the tooltip closes caution", () => {
|
||||||
|
openCaution();
|
||||||
|
cautionEffect.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("FLIP click when caution open (btn-disabled) does nothing", () => {
|
||||||
|
openCaution();
|
||||||
|
var flipBtn = testDiv.querySelector(".sig-flip-btn");
|
||||||
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-stage").classList.contains("sig-caution-open")).toBe(true);
|
||||||
|
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Stat block: keyword population and FLIP toggle ────────────────── //
|
||||||
|
|
||||||
|
describe("stat block and FLIP", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
it("populates upright keywords when a card is hovered", () => {
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
var items = statBlock.querySelectorAll("#id_stat_keywords_upright li");
|
||||||
|
expect(items.length).toBe(3);
|
||||||
|
expect(items[0].textContent).toBe("action");
|
||||||
|
expect(items[1].textContent).toBe("impulsiveness");
|
||||||
|
expect(items[2].textContent).toBe("ambition");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("populates reversed keywords when a card is hovered", () => {
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
var items = statBlock.querySelectorAll("#id_stat_keywords_reversed li");
|
||||||
|
expect(items.length).toBe(2);
|
||||||
|
expect(items[0].textContent).toBe("no direction");
|
||||||
|
expect(items[1].textContent).toBe("disregard for consequences");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("FLIP click adds .is-reversed to the stat block", () => {
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
var flipBtn = statBlock.querySelector(".sig-flip-btn");
|
||||||
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("second FLIP click removes .is-reversed", () => {
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
var flipBtn = statBlock.querySelector(".sig-flip-btn");
|
||||||
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
flipBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hovering a new card resets .is-reversed", () => {
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
statBlock.querySelector(".sig-flip-btn").dispatchEvent(
|
||||||
|
new MouseEvent("click", { bubbles: true })
|
||||||
|
);
|
||||||
|
expect(statBlock.classList.contains("is-reversed")).toBe(true);
|
||||||
|
|
||||||
|
// Leave and re-enter (simulates moving to a different card)
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(statBlock.classList.contains("is-reversed")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("card with no keywords yields empty lists", () => {
|
||||||
|
card.dataset.keywordsUpright = "";
|
||||||
|
card.dataset.keywordsReversed = "";
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(statBlock.querySelectorAll("#id_stat_keywords_upright li").length).toBe(0);
|
||||||
|
expect(statBlock.querySelectorAll("#id_stat_keywords_reversed li").length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── WS cursor hover (applyHover) ──────────────────────────────────────── //
|
||||||
|
//
|
||||||
|
// Fixture polarity = levity, userRole = PC.
|
||||||
|
// POLARITY_ROLES: levity → [PC, NC, SC] = [left, mid, right]
|
||||||
|
//
|
||||||
|
// Only tests the JS position mapping — colour is CSS-only.
|
||||||
|
|
||||||
|
describe("WS cursor hover", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
it("NC hover activates the --mid cursor", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||||
|
detail: { card_id: 42, role: "NC", active: true },
|
||||||
|
}));
|
||||||
|
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("SC hover activates the --right cursor", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||||
|
detail: { card_id: 42, role: "SC", active: true },
|
||||||
|
}));
|
||||||
|
expect(card.querySelector(".sig-cursor--right").classList.contains("active")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("own role (PC) hover event is ignored — no cursor activates", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||||
|
detail: { card_id: 42, role: "PC", active: true },
|
||||||
|
}));
|
||||||
|
expect(card.querySelectorAll(".sig-cursor.active").length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hover-off removes .active from the cursor", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||||
|
detail: { card_id: 42, role: "NC", active: true },
|
||||||
|
}));
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||||
|
detail: { card_id: 42, role: "NC", active: false },
|
||||||
|
}));
|
||||||
|
expect(card.querySelector(".sig-cursor--mid").classList.contains("active")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hover on unknown card_id is a no-op", () => {
|
||||||
|
expect(() => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||||
|
detail: { card_id: 9999, role: "NC", active: true },
|
||||||
|
}));
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── WS reservation — data-reserved-by attribute ───────────────────────── //
|
||||||
|
//
|
||||||
|
// applyReservation() sets data-reserved-by so the CSS can glow the card in
|
||||||
|
// the reserving gamer's role colour. These tests assert the attribute, not
|
||||||
|
// the colour (CSS variables aren't resolvable in the SpecRunner context).
|
||||||
|
|
||||||
|
describe("WS reservation sets data-reserved-by", () => {
|
||||||
|
beforeEach(() => makeFixture());
|
||||||
|
|
||||||
|
it("peer reservation sets data-reserved-by to the reserving role", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "NC", reserved: true },
|
||||||
|
}));
|
||||||
|
expect(card.dataset.reservedBy).toBe("NC");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("peer reservation also adds .sig-reserved class", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "NC", reserved: true },
|
||||||
|
}));
|
||||||
|
expect(card.classList.contains("sig-reserved")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("release removes data-reserved-by", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "NC", reserved: true },
|
||||||
|
}));
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "NC", reserved: false },
|
||||||
|
}));
|
||||||
|
expect(card.dataset.reservedBy).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("own reservation (PC) sets data-reserved-by AND .sig-reserved--own", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "PC", reserved: true },
|
||||||
|
}));
|
||||||
|
expect(card.dataset.reservedBy).toBe("PC");
|
||||||
|
expect(card.classList.contains("sig-reserved--own")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("peer reservation places a thumbs-up float and removes any hand-pointer float", () => {
|
||||||
|
// First, a hover float exists for NC (mid cursor)
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_hover", {
|
||||||
|
detail: { card_id: 42, role: "NC", active: true },
|
||||||
|
}));
|
||||||
|
expect(document.querySelector('.sig-cursor-float[data-role="NC"]')).not.toBeNull();
|
||||||
|
expect(document.querySelector('.fa-hand-pointer[data-role="NC"]')).not.toBeNull();
|
||||||
|
|
||||||
|
// NC then clicks OK — reservation arrives
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "NC", reserved: true },
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Thumbs-up replaces hand-pointer
|
||||||
|
const floatEl = document.querySelector('.sig-cursor-float[data-role="NC"]');
|
||||||
|
expect(floatEl).not.toBeNull();
|
||||||
|
expect(floatEl.classList.contains("fa-thumbs-up")).toBe(true);
|
||||||
|
expect(floatEl.classList.contains("fa-hand-pointer")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("peer release removes the thumbs-up float", () => {
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "NC", reserved: true },
|
||||||
|
}));
|
||||||
|
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).not.toBeNull();
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent("room:sig_reserved", {
|
||||||
|
detail: { card_id: 42, role: "NC", reserved: false },
|
||||||
|
}));
|
||||||
|
expect(document.querySelector('.sig-cursor-float--reserved[data-role="NC"]')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Polarity theming — stage qualifier text ────────────────────────────── //
|
||||||
|
//
|
||||||
|
// On mouseenter, updateStage() injects "Leavened" or "Graven" into the
|
||||||
|
// sig-qualifier-above (non-major) or sig-qualifier-below (major arcana) slot.
|
||||||
|
// Correspondence field is never populated in sig-select context.
|
||||||
|
|
||||||
|
describe("polarity theming — stage qualifier", () => {
|
||||||
|
it("levity non-major card puts 'Leavened' in qualifier-above, qualifier-below empty", () => {
|
||||||
|
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||||
|
// data-arcana defaults to "Minor Arcana" in fixture → non-major
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("Leavened");
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("levity major arcana card puts 'Leavened' in qualifier-below, qualifier-above empty", () => {
|
||||||
|
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||||
|
card.dataset.arcana = "Major Arcana";
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("");
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Leavened");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("major arcana title gets a trailing comma (qualifier reads as subtitle)", () => {
|
||||||
|
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||||
|
card.dataset.arcana = "Major Arcana";
|
||||||
|
card.dataset.nameTitle = "The Schizo";
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".fan-card-name").textContent).toBe("The Schizo,");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("non-major arcana title has no trailing comma", () => {
|
||||||
|
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||||
|
// fixture default: Minor Arcana, "King of Pentacles"
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".fan-card-name").textContent).toBe("King of Pentacles");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gravity non-major card puts 'Graven' in qualifier-above", () => {
|
||||||
|
makeFixture({ polarity: 'gravity', userRole: 'BC' });
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("Graven");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gravity major arcana card puts 'Graven' in qualifier-below", () => {
|
||||||
|
makeFixture({ polarity: 'gravity', userRole: 'BC' });
|
||||||
|
card.dataset.arcana = "Major Arcana";
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Graven");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hovering clears qualifier slots from the previous card", () => {
|
||||||
|
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
card.dataset.arcana = "Major Arcana";
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }));
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
// Now major — above should be empty, below filled
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-above").textContent).toBe("");
|
||||||
|
expect(testDiv.querySelector(".sig-qualifier-below").textContent).toBe("Leavened");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correspondence field is never populated", () => {
|
||||||
|
makeFixture({ polarity: 'levity', userRole: 'PC' });
|
||||||
|
card.dataset.correspondence = "Il Bagatto (Minchiate)";
|
||||||
|
card.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
|
||||||
|
expect(testDiv.querySelector(".fan-card-correspondence").textContent).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-xl">Share</button>
|
<button type="submit" class="btn btn-primary">Share</button>
|
||||||
</form>
|
</form>
|
||||||
<small>Note shared with:
|
<small>Note shared with:
|
||||||
{% for user in note.shared_with.all %}
|
{% for user in note.shared_with.all %}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
{% if room.gate_status == 'OPEN' %}
|
{% if room.gate_status == 'OPEN' %}
|
||||||
<form method="POST" action="{% url 'epic:pick_roles' room.id %}" style="display:contents">
|
<form method="POST" action="{% url 'epic:pick_roles' room.id %}" style="display:contents">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button type="submit" class="launch-game-btn btn btn-primary btn-xl">PICK ROLES</button>
|
<button type="submit" class="launch-game-btn btn btn-primary">PICK ROLES</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,16 +21,40 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
|
|||||||
</div>
|
</div>
|
||||||
<div class="fan-card-face">
|
<div class="fan-card-face">
|
||||||
<p class="fan-card-name-group"></p>
|
<p class="fan-card-name-group"></p>
|
||||||
|
<p class="sig-qualifier-above"></p>
|
||||||
<h3 class="fan-card-name"></h3>
|
<h3 class="fan-card-name"></h3>
|
||||||
|
<p class="sig-qualifier-below"></p>
|
||||||
<p class="fan-card-arcana"></p>
|
<p class="fan-card-arcana"></p>
|
||||||
<p class="fan-card-correspondence"></p>
|
<p class="fan-card-correspondence"></p>{# not shown in sig-select — game-kit only #}
|
||||||
</div>
|
</div>
|
||||||
<div class="fan-card-corner fan-card-corner--br">
|
<div class="fan-card-corner fan-card-corner--br">
|
||||||
<span class="fan-corner-rank"></span>
|
<span class="fan-corner-rank"></span>
|
||||||
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
<i class="fa-solid stage-suit-icon" style="display:none"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sig-stat-block"></div>
|
<div class="sig-stat-block">
|
||||||
|
<button class="btn btn-reverse sig-flip-btn" type="button">FLIP</button>
|
||||||
|
<button class="btn btn-caution sig-caution-btn" type="button">FYI</button>
|
||||||
|
<div class="stat-face stat-face--upright">
|
||||||
|
<p class="stat-face-label">Upright</p>
|
||||||
|
<ul class="stat-keywords" id="id_stat_keywords_upright"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="stat-face stat-face--reversed">
|
||||||
|
<p class="stat-face-label">Reversed</p>
|
||||||
|
<ul class="stat-keywords" id="id_stat_keywords_reversed"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="sig-caution-tooltip" id="id_sig_caution">
|
||||||
|
<div class="sig-caution-header">
|
||||||
|
<h4 class="sig-caution-title">Caution!</h4>
|
||||||
|
<p class="sig-caution-type">Rival Interaction</p>
|
||||||
|
</div>
|
||||||
|
<p class="sig-caution-shoptalk">[Shoptalk forthcoming]</p>
|
||||||
|
<p class="sig-caution-effect"></p>
|
||||||
|
<span class="sig-caution-index"></span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-nav-left sig-caution-prev" type="button">PRV</button>
|
||||||
|
<button class="btn btn-nav-right sig-caution-next" type="button">NXT</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sig-deck-grid" id="id_sig_deck">
|
<div class="sig-deck-grid" id="id_sig_deck">
|
||||||
@@ -42,7 +66,10 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
|
|||||||
data-name-group="{{ card.name_group }}"
|
data-name-group="{{ card.name_group }}"
|
||||||
data-name-title="{{ card.name_title }}"
|
data-name-title="{{ card.name_title }}"
|
||||||
data-arcana="{{ card.get_arcana_display }}"
|
data-arcana="{{ card.get_arcana_display }}"
|
||||||
data-correspondence="{{ card.correspondence|default:'' }}">
|
data-correspondence="{{ card.correspondence|default:'' }}"
|
||||||
|
data-keywords-upright="{{ card.keywords_upright|join:',' }}"
|
||||||
|
data-keywords-reversed="{{ card.keywords_reversed|join:',' }}"
|
||||||
|
data-cautions="{{ card.cautions_json }}">
|
||||||
<div class="fan-card-corner fan-card-corner--tl">
|
<div class="fan-card-corner fan-card-corner--tl">
|
||||||
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
<span class="fan-corner-rank">{{ card.corner_rank }}</span>
|
||||||
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
{% if card.suit_icon %}<i class="fa-solid {{ card.suit_icon }}"></i>{% endif %}
|
||||||
@@ -52,9 +79,9 @@ Context: sig_cards, user_polarity, user_seat, sig_reserve_url, sig_reservations_
|
|||||||
<button class="sig-nvm-btn btn btn-cancel" type="button">NVM</button>
|
<button class="sig-nvm-btn btn btn-cancel" type="button">NVM</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sig-card-cursors">
|
<div class="sig-card-cursors">
|
||||||
<span class="sig-cursor sig-cursor--left"></span>
|
<i class="fa-solid fa-hand-pointer sig-cursor sig-cursor--left"></i>
|
||||||
<span class="sig-cursor sig-cursor--mid"></span>
|
<i class="fa-solid fa-hand-pointer sig-cursor sig-cursor--mid"></i>
|
||||||
<span class="sig-cursor sig-cursor--right"></span>
|
<i class="fa-solid fa-hand-pointer sig-cursor sig-cursor--right"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<form method="POST" action="{% url 'epic:confirm_token' room.id %}">
|
<form method="POST" action="{% url 'epic:confirm_token' room.id %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if is_last_slot %}
|
{% if is_last_slot %}
|
||||||
<button type="submit" class="btn btn-primary btn-xl">PICK ROLES</button>
|
<button type="submit" class="btn btn-primary">PICK ROLES</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="submit" class="btn btn-confirm">OK</button>
|
<button type="submit" class="btn btn-confirm">OK</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div id="id_pick_sigs_wrap"{% if starter_roles|length < 6 %} style="display:none"{% endif %}>
|
<div id="id_pick_sigs_wrap"{% if starter_roles|length < 6 %} style="display:none"{% endif %}>
|
||||||
<form method="POST" action="{% url 'epic:pick_sigs' room.id %}">
|
<form method="POST" action="{% url 'epic:pick_sigs' room.id %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button id="id_pick_sigs_btn" type="submit" class="btn btn-primary btn-xl">PICK<br>SIGS</button>
|
<button id="id_pick_sigs_btn" type="submit" class="btn btn-primary">PICK<br>SIGS</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
<i class="fa-solid fa-dice-d20"></i>
|
<i class="fa-solid fa-dice-d20"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="id_tray" style="display:none"><div id="id_tray_grid">{% for i in "12345678" %}<div class="tray-cell"></div>{% endfor %}</div></div>
|
<div id="id_tray" style="display:none"><div id="id_tray_grid" data-role-icons-url="{% static 'apps/epic/icons/cards-roles/' %}">{% if my_tray_role %}<div class="tray-cell tray-role-card" data-role="{{ my_tray_role }}"><img src="{% static my_tray_scrawl_static_path %}" alt="{{ my_tray_role }}"></div>{% else %}<div class="tray-cell"></div>{% endif %}{% for i in "2345678" %}<div class="tray-cell"></div>{% endfor %}</div></div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "apps/gameboard/_partials/_room_gear.html" %}
|
{% include "apps/gameboard/_partials/_room_gear.html" %}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
{% if navbar_recent_room_url %}
|
{% if navbar_recent_room_url %}
|
||||||
<button
|
<button
|
||||||
id="id_cont_game"
|
id="id_cont_game"
|
||||||
class="btn btn-primary btn-xl"
|
class="btn btn-primary"
|
||||||
type="button"
|
type="button"
|
||||||
data-confirm="Continue game?"
|
data-confirm="Continue game?"
|
||||||
data-href="{{ navbar_recent_room_url }}"
|
data-href="{{ navbar_recent_room_url }}"
|
||||||
|
|||||||
Reference in New Issue
Block a user