Add Sport Enjoyer and Europe presets plus UI improvements
- New Sport Enjoyer preset with rugby, football, and basketball categories - New Europe preset with auto-built country lists - Define SPORT_CAT_IDS constant for sport preset - Fix preset generation with auto-build from database - Improve mobile UI: tooltip overflow, button interactions, reverse mode consistency - Refactor tooltip logic: consolidated show/hide, fixed event handling - Harmonize reverse mode styling with rounded buttons and question mark - Fix translation key presetIslands in preset grid - Ensure helper functions defined before use in applyPreset - Remove duplicates from auto-generated category lists
This commit is contained in:
parent
2be9826b1c
commit
1ffeeab8c8
6 changed files with 677 additions and 79 deletions
|
|
@ -1,5 +1,13 @@
|
|||
# LeGrandGeoQuiz
|
||||
|
||||
> Presets for custom mode are now automatically computed at runtime from the
|
||||
> country descriptions. Previously a set of hard-coded arrays (e.g.
|
||||
> `EUROPEAN_CODES`, `ISLANDS_CODES`) were required; they have been regenerated
|
||||
> dynamically using keywords in `js/hints.js`. This ensures all buttons work even
|
||||
> after data updates.
|
||||
|
||||
LeGrandGeoQuiz
|
||||
|
||||
A strategic geography quiz game — 8 countries, 8 categories, the lowest score wins.
|
||||
|
||||
**Languages:** FR | EN | UA | DE
|
||||
|
|
|
|||
275
css/styles.css
275
css/styles.css
|
|
@ -158,8 +158,90 @@ h1 { font-size:2.8em; font-weight:800; line-height:1; margin-bottom:6px; }
|
|||
.cat-info-btn { display:inline-flex; align-items:center; justify-content:center; width:17px; height:17px; border-radius:50%; background:rgba(79,195,255,0.12); border:1px solid rgba(79,195,255,0.3); color:var(--accent3); font-family:'DM Mono',monospace; font-size:0.65em; font-weight:700; cursor:pointer; flex-shrink:0; line-height:1; transition:all 0.15s; user-select:none; }
|
||||
.cat-info-btn:hover { background:rgba(79,195,255,0.28); border-color:var(--accent3); transform:scale(1.15); }
|
||||
|
||||
/* Groupe des boutons action dans une cat-btn */
|
||||
.cat-btn-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Bouton de tri ⇅ */
|
||||
.cat-sort-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
border-radius: 50%;
|
||||
background: rgba(79,255,176,0.1);
|
||||
border: 1px solid rgba(79,255,176,0.25);
|
||||
color: var(--accent);
|
||||
font-family: 'DM Mono', monospace;
|
||||
font-size: 0.72em;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
transition: all 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
.cat-sort-btn:hover {
|
||||
background: rgba(79,255,176,0.28);
|
||||
border-color: var(--accent);
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
/* Bouton sort dans reverse-cat-info-row */
|
||||
.reverse-sort-btn {
|
||||
/* Match the visual language of reverse-info-toggle: small rounded pill */
|
||||
font-family: 'DM Mono', monospace;
|
||||
font-size: 0.65em;
|
||||
color: var(--accent3);
|
||||
border: 1px solid rgba(79,195,255,0.35);
|
||||
background: rgba(79,195,255,0.08);
|
||||
border-radius: 99px;
|
||||
padding: 3px 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.reverse-sort-btn:hover { background: rgba(79,195,255,0.2); }
|
||||
|
||||
/* Hardcore */
|
||||
body.hardcore .cat-sort-btn {
|
||||
background: rgba(255,61,90,0.1);
|
||||
border-color: rgba(255,61,90,0.25);
|
||||
color: #ff3d5a;
|
||||
}
|
||||
body.hardcore .cat-sort-btn:hover {
|
||||
background: rgba(255,61,90,0.25);
|
||||
border-color: #ff3d5a;
|
||||
}
|
||||
|
||||
/* Custom mode */
|
||||
body.custom-mode .cat-sort-btn {
|
||||
background: rgba(167,139,250,0.1);
|
||||
border-color: rgba(167,139,250,0.25);
|
||||
color: var(--accent3);
|
||||
}
|
||||
body.custom-mode .cat-sort-btn:hover {
|
||||
background: rgba(167,139,250,0.28);
|
||||
border-color: var(--accent3);
|
||||
}
|
||||
|
||||
/* Tooltip sort — variante couleur */
|
||||
#cat-tooltip[data-mode="sort"] {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
#cat-tooltip[data-mode="sort"] .tt-title {
|
||||
color: var(--accent);}
|
||||
|
||||
/* tooltip */
|
||||
#cat-tooltip { position:fixed; z-index:300; max-width:280px; background:var(--surface); border:1px solid var(--accent3); border-radius:10px; padding:12px 14px; box-shadow:0 8px 32px rgba(0,0,0,0.5); pointer-events:none; opacity:0; transform:translateY(4px) scale(0.97); transition:opacity 0.15s ease,transform 0.15s ease; }
|
||||
#cat-tooltip { position:fixed; z-index:300; max-width:280px; background:var(--surface); border:1px solid var(--accent3); border-radius:10px; padding:12px 14px; box-shadow:0 8px 32px rgba(0,0,0,0.5); pointer-events:none; opacity:0; transform:translateY(4px) scale(0.97); transition:opacity 0.08s ease,transform 0.08s ease; }
|
||||
#cat-tooltip.visible { opacity:1; transform:translateY(0) scale(1); pointer-events:auto; }
|
||||
.tt-title { font-size:0.8em; font-weight:700; color:var(--accent3); margin-bottom:6px; }
|
||||
.tt-body { font-size:0.78em; color:var(--muted); line-height:1.5; font-family:'DM Mono',monospace; }
|
||||
|
|
@ -582,13 +664,21 @@ body.reverse-mode .cats-grid { display:none !important; }
|
|||
border-radius:99px; padding:3px 10px; cursor:pointer; transition:all 0.15s;
|
||||
}
|
||||
.reverse-info-toggle:hover { background:rgba(79,195,255,0.2); }
|
||||
.reverse-sort-btn .rsq { font-weight:700; margin-left:4px; opacity:0.95; }
|
||||
.reverse-cat-tooltip {
|
||||
display:none; margin-top:8px; text-align:left;
|
||||
font-family:'DM Mono',monospace; font-size:0.68em; color:var(--muted); line-height:1.5;
|
||||
background:rgba(79,195,255,0.06); border:1px solid rgba(79,195,255,0.2);
|
||||
border-radius:8px; padding:8px 10px;
|
||||
opacity: 0;
|
||||
transform: translateY(4px) scale(0.97);
|
||||
transition: opacity 0.08s ease, transform 0.08s ease;
|
||||
}
|
||||
.reverse-cat-tooltip.open {
|
||||
display:block;
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
.reverse-cat-tooltip.open { display:block; }
|
||||
|
||||
/* ── Country buttons grid (reverse mode) ── */
|
||||
#countries-grid {
|
||||
|
|
@ -1064,22 +1154,177 @@ body.reverse-mode #countries-grid { display:grid; }
|
|||
|
||||
/* ── Appareils tactiles : targets min 44px, no hover ── */
|
||||
@media (pointer: coarse) {
|
||||
.cat-btn { min-height: 46px; }
|
||||
.btn-primary, .btn-hardcore, .btn-reverse,
|
||||
.btn-daily, .btn-custom { min-height: 48px; }
|
||||
.btn-secondary { min-height: 44px; }
|
||||
.draft-pill, .submode-pill { min-height: 58px; }
|
||||
.lang-btn { min-height: 30px; min-width: 34px; }
|
||||
.mob-ribbon-action { min-height: 40px; }
|
||||
/* Supprimer les hover-transforms qui "collent" sur touch */
|
||||
.btn-primary:hover, .btn-hardcore:hover,
|
||||
.btn-daily:hover, .btn-reverse:hover { transform: none; }
|
||||
.cat-btn:hover:not(:disabled) { transform: none; }
|
||||
.ribbon-item:hover { background: transparent; color: var(--muted); }
|
||||
/* ─── CAT BTN ACTIONS (⇅ + i) ─────────────────────────────────────────────── */
|
||||
.cat-btn-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Base style shared by both mini-buttons */
|
||||
.cat-sort-btn,
|
||||
.cat-info-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
font-family: 'DM Mono', monospace;
|
||||
font-size: 0.72em;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s, background 0.15s, opacity 0.15s;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
/* Larger tap target via padding so finger doesn't miss */
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.cat-info-btn {
|
||||
background: rgba(79, 255, 176, 0.1);
|
||||
border: 1px solid rgba(79, 255, 176, 0.35);
|
||||
color: var(--accent);
|
||||
}
|
||||
.cat-info-btn:hover { transform: scale(1.15); background: rgba(79, 255, 176, 0.2); }
|
||||
|
||||
.cat-sort-btn {
|
||||
background: rgba(79, 255, 176, 0.08);
|
||||
border: 1px solid rgba(79, 255, 176, 0.28);
|
||||
color: var(--accent);
|
||||
}
|
||||
.cat-sort-btn:hover { transform: scale(1.15); background: rgba(79, 255, 176, 0.18); }
|
||||
|
||||
/* Hardcore */
|
||||
body.hardcore .cat-info-btn,
|
||||
body.hardcore .cat-sort-btn {
|
||||
border-color: rgba(255, 61, 90, 0.4);
|
||||
color: #ff3d5a;
|
||||
background: rgba(255, 61, 90, 0.08);
|
||||
}
|
||||
body.hardcore .cat-info-btn:hover,
|
||||
body.hardcore .cat-sort-btn:hover {
|
||||
background: rgba(255, 61, 90, 0.18);
|
||||
}
|
||||
|
||||
/* Custom/reverse accent */
|
||||
body.custom-mode .cat-info-btn,
|
||||
body.custom-mode .cat-sort-btn,
|
||||
body.reverse-mode .cat-info-btn,
|
||||
body.reverse-mode .cat-sort-btn {
|
||||
border-color: rgba(var(--accent3-rgb, 180, 100, 255), 0.4);
|
||||
color: var(--accent3, #b464ff);
|
||||
background: rgba(var(--accent3-rgb, 180, 100, 255), 0.08);
|
||||
}
|
||||
|
||||
/* ─── REVERSE SORT BTN ─────────────────────────────────────────────────────── */
|
||||
.reverse-sort-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
font-family: 'DM Mono', monospace;
|
||||
font-size: 0.8em;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
background: rgba(79, 255, 176, 0.08);
|
||||
border: 1px solid rgba(79, 255, 176, 0.3);
|
||||
color: var(--accent);
|
||||
transition: transform 0.15s, background 0.15s;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.reverse-sort-btn:hover { transform: scale(1.1); background: rgba(79, 255, 176, 0.18); }
|
||||
|
||||
/* ─── TOOLTIP ──────────────────────────────────────────────────────────────── */
|
||||
#cat-tooltip {
|
||||
pointer-events: none; /* disable pointer by default to avoid blocking taps on mobile */
|
||||
z-index: 9999;
|
||||
}
|
||||
#cat-tooltip[data-mode="sort"] {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 18px rgba(79, 255, 176, 0.2);
|
||||
}
|
||||
|
||||
/* ─── MOBILE OVERRIDES ─────────────────────────────────────────────────────── */
|
||||
@media (max-width: 600px), (pointer: coarse) {
|
||||
/* Bigger tap zones — invisible padding trick */
|
||||
.cat-sort-btn,
|
||||
.cat-info-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 0.85em;
|
||||
/* extend tap area beyond visible circle */
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
/* keep layout stable and increase tappable area without negative margins */
|
||||
}
|
||||
|
||||
.cat-btn-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Reverse sort pill also bigger */
|
||||
.reverse-sort-btn {
|
||||
padding: 3px 10px;
|
||||
font-size: 0.65em;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
/* Tooltip: fixed to bottom of viewport on mobile so it's always readable */
|
||||
#cat-tooltip.visible {
|
||||
position: fixed !important;
|
||||
bottom: 16px !important;
|
||||
top: auto !important;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%);
|
||||
width: calc(100vw - 32px) !important;
|
||||
max-width: 340px;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||
pointer-events: auto; /* allow interacting with tooltip content when visible */
|
||||
}
|
||||
}
|
||||
|
||||
/* Enable tooltip interactions on non-touch devices (hover-capable) */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
#cat-tooltip { pointer-events: auto; }
|
||||
}
|
||||
|
||||
/* On mobile in reverse mode, position reverse-cat-tooltip the same as cat-tooltip (bottom fixed) for consistency */
|
||||
@media (max-width: 600px), (pointer: coarse) {
|
||||
.reverse-cat-tooltip.open {
|
||||
display: block !important;
|
||||
position: fixed !important;
|
||||
bottom: 16px !important;
|
||||
top: auto !important;
|
||||
left: 50% !important;
|
||||
/* start slightly lower and slide up for a smoother effect */
|
||||
transform: translateX(-50%) translateY(10px);
|
||||
width: calc(100vw - 32px) !important;
|
||||
max-width: 340px;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||
margin-top: 0 !important;
|
||||
padding: 12px 14px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* transition for mobile bottom slide */
|
||||
.reverse-cat-tooltip {
|
||||
transition: transform 0.18s cubic-bezier(.2,.9,.2,1), opacity 0.14s ease, max-height 0.18s ease;
|
||||
}
|
||||
.reverse-cat-tooltip.open {
|
||||
transform: translateX(-50%) translateY(0) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Scroll fluide iOS ─────────────────────────────────── */
|
||||
#panel-custom, .ribbon-list,
|
||||
.daily-modal-box, #daily-lb-overlay .daily-modal-box {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}}
|
||||
|
|
@ -321,7 +321,7 @@
|
|||
</button>
|
||||
<button class="preset-btn" onclick="applyPreset('islands',this)">
|
||||
<span class="p-icon">🏝️</span>
|
||||
<span class="p-name"> data-i18n="presetIslands"></span>
|
||||
<span class="p-name" data-i18n="presetIslands"></span>
|
||||
</button>
|
||||
<button class="preset-btn" onclick="applyPreset('flag-guesser',this)">
|
||||
<span class="p-icon">🚩</span>
|
||||
|
|
@ -355,7 +355,8 @@
|
|||
<div class="reverse-cat-icon" id="reverse-cat-icon">?</div>
|
||||
<div class="reverse-cat-name" id="reverse-cat-name">—</div>
|
||||
<div class="reverse-cat-info-row">
|
||||
<span class="reverse-info-toggle" onclick="toggleReverseCatInfo()" data-i18n="reverseInfoBtn"></span>
|
||||
<span class="reverse-sort-btn" id="reverse-sort-btn" data-catid="">⇅ <span class="rsq">?</span></span>
|
||||
<span class="reverse-info-toggle" onclick="toggleReverseCatInfo()" data-i18n="reverseInfoBtn"></span>
|
||||
</div>
|
||||
<div class="reverse-cat-tooltip" id="reverse-cat-tooltip"></div>
|
||||
</div>
|
||||
|
|
|
|||
356
js/game.js
356
js/game.js
|
|
@ -68,7 +68,6 @@ function showCountdown(onDone) {
|
|||
numEl.className = 'countdown-num';
|
||||
void numEl.offsetWidth;
|
||||
numEl.textContent = counts[i];
|
||||
// On GO: show draft emoji as subtitle
|
||||
if (i === counts.length - 1) {
|
||||
subEl.textContent = lastDraftUsed === 'countries' ? '🌍' : '📊';
|
||||
} else {
|
||||
|
|
@ -102,7 +101,6 @@ function startGame(modeOrSeed, seedOverride) {
|
|||
seedOverride = modeOrSeed;
|
||||
}
|
||||
|
||||
// Apply N_TURNS and time per mode
|
||||
document.body.classList.remove('hardcore', 'custom-mode', 'reverse-mode');
|
||||
if (gameMode === 'custom') {
|
||||
if (!seedOverride) {
|
||||
|
|
@ -112,7 +110,6 @@ function startGame(modeOrSeed, seedOverride) {
|
|||
document.body.classList.add('custom-mode');
|
||||
document.getElementById('panel-custom').classList.add('hidden');
|
||||
document.getElementById('panel-setup').classList.add('hidden');
|
||||
// Apply sub-mode
|
||||
if (customSubMode === 'hardcore') {
|
||||
document.body.classList.add('hardcore');
|
||||
} else if (customSubMode === 'reverse') {
|
||||
|
|
@ -134,7 +131,6 @@ function startGame(modeOrSeed, seedOverride) {
|
|||
|
||||
var seed = seedOverride || generateSeed();
|
||||
if (!applyGameFromSeed(seed)) return;
|
||||
// Sync N_TURNS to actual game size (important when loading a seed)
|
||||
N_TURNS = gameCountries.length;
|
||||
currentSeed = seed;
|
||||
currentStep = 0;
|
||||
|
|
@ -144,11 +140,9 @@ function startGame(modeOrSeed, seedOverride) {
|
|||
isRevealing = false;
|
||||
if (typeof resetHints === 'function') resetHints();
|
||||
|
||||
// Show countdown, then launch
|
||||
showCountdown(function() {
|
||||
document.getElementById('panel-game').classList.remove('hidden');
|
||||
document.getElementById('game-seed-display').textContent = currentSeed;
|
||||
// Mettre à jour le hash URL pour permettre le partage
|
||||
history.replaceState(null, '', '#seed=' + encodeURIComponent(currentSeed));
|
||||
renderCategoryButtons();
|
||||
showTurn();
|
||||
|
|
@ -158,10 +152,7 @@ function startGame(modeOrSeed, seedOverride) {
|
|||
function startWithSeedInput() {
|
||||
var raw = document.getElementById('seed-input').value.trim();
|
||||
if (!raw) { alert(t('enterSeed')); return; }
|
||||
// Support ancien format legacy (12-45_0-5) et nouveau format Base62
|
||||
var seed = raw;
|
||||
// Si l'utilisateur a collé du texte avec une seed entourée d'espaces, on nettoie
|
||||
seed = seed.replace(/\s+/g, '');
|
||||
var seed = raw.replace(/\s+/g, '');
|
||||
startGame('custom', seed);
|
||||
}
|
||||
|
||||
|
|
@ -169,7 +160,10 @@ function startWithSeedInput() {
|
|||
function makeCatBtnHTML(cat) {
|
||||
return '<div class="cat-btn-top">'
|
||||
+ '<span class="cat-icon">' + cat.icon + '</span>'
|
||||
+ '<span class="cat-btn-actions">'
|
||||
+ '<span class="cat-sort-btn" data-catid="' + cat.id + '">⇅</span>'
|
||||
+ '<span class="cat-info-btn" data-catid="' + cat.id + '">i</span>'
|
||||
+ '</span>'
|
||||
+ '</div>'
|
||||
+ '<span class="cat-label-text">' + catName(cat) + '</span>';
|
||||
}
|
||||
|
|
@ -184,6 +178,7 @@ function renderCategoryButtons() {
|
|||
btn.innerHTML = makeCatBtnHTML(cat);
|
||||
btn.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.cat-info-btn')) return;
|
||||
if (e.target.closest('.cat-sort-btn')) return;
|
||||
handleChoice(cat.id);
|
||||
});
|
||||
grid.appendChild(btn);
|
||||
|
|
@ -191,10 +186,7 @@ function renderCategoryButtons() {
|
|||
attachInfoListeners();
|
||||
}
|
||||
|
||||
|
||||
// ─── RERENDER CURRENT TURN ON LANG CHANGE ─────────────────────────────────────
|
||||
// Appelé par setLang() pour mettre à jour immédiatement le tour en cours
|
||||
// sans attendre le prochain tour.
|
||||
function rerenderCurrentTurn() {
|
||||
var inGame = (document.getElementById('panel-game') &&
|
||||
!document.getElementById('panel-game').classList.contains('hidden'));
|
||||
|
|
@ -205,7 +197,6 @@ function rerenderCurrentTurn() {
|
|||
(gameMode === 'custom' && customSubMode === 'reverse'));
|
||||
|
||||
if (isReverse) {
|
||||
// ── Reverse : mettre à jour la catégorie affichée ─────────────────────
|
||||
if (currentStep < gameCategories.length) {
|
||||
var cat = gameCategories[currentStep];
|
||||
document.getElementById('reverse-cat-name').textContent = catName(cat);
|
||||
|
|
@ -214,14 +205,12 @@ function rerenderCurrentTurn() {
|
|||
t('reverseTurn') + ' ' + (currentStep + 1) + ' ' + t('of') + ' ' + gameCategories.length;
|
||||
}
|
||||
} else {
|
||||
// ── Normal / Hardcore / Custom ─────────────────────────────────────────
|
||||
if (currentStep < gameCountries.length) {
|
||||
var c = gameCountries[currentStep];
|
||||
document.getElementById('country-name').textContent = getCountryName(c);
|
||||
document.getElementById('turn-label').textContent =
|
||||
t('turn') + ' ' + (currentStep + 1) + ' ' + t('of') + ' ' + gameCountries.length;
|
||||
|
||||
// Hint buttons : état selon s'ils ont été révélés ou non
|
||||
var btn1 = document.getElementById('hint-btn-1');
|
||||
var btn2 = document.getElementById('hint-btn-2');
|
||||
if (btn1 && !btn1.classList.contains('revealed')) {
|
||||
|
|
@ -231,7 +220,6 @@ function rerenderCurrentTurn() {
|
|||
btn2.innerHTML = '\uD83D\uDD0D ' + t('hint2btn');
|
||||
}
|
||||
|
||||
// Labels des hints déjà révélés (titre de section)
|
||||
if (hintState.hint1Revealed) {
|
||||
var lbl1 = document.getElementById('hint-label-1');
|
||||
if (lbl1) lbl1.innerHTML = t('hintLabel1') + ' <span class="cost-badge cost-badge-1">-25 pts</span>';
|
||||
|
|
@ -258,19 +246,279 @@ function rerenderCategoryButtonNames() {
|
|||
attachInfoListeners();
|
||||
}
|
||||
|
||||
function attachInfoListeners() {
|
||||
document.querySelectorAll('.cat-info-btn').forEach(function(el) {
|
||||
var clone = el.cloneNode(true);
|
||||
el.parentNode.replaceChild(clone, el);
|
||||
clone.addEventListener('mouseenter', function() { showTooltip(clone.dataset.catid, clone); });
|
||||
clone.addEventListener('mouseleave', hideTooltip);
|
||||
// ─── TOOLTIP SYSTEM ──────────────────────────────────────────────────────────
|
||||
// Desktop : mouseenter shows, mouseleave schedules hide (counter-based to
|
||||
// survive rapid i→⇅ transitions without flicker).
|
||||
// Mobile : purely tap-to-toggle. mouseenter/mouseleave are IGNORED on touch
|
||||
// devices because they fire unreliably after touchend and cause the
|
||||
// flash-then-close bug. A global touchstart listener on the document
|
||||
// closes any open tooltip when the user taps outside it.
|
||||
|
||||
var _tooltipHideTimer = null;
|
||||
var _mouseOverTooltipUI = 0; // counter for desktop hover zone
|
||||
|
||||
// after hiding, suppress any re-opening for a short window to avoid ghost flashes
|
||||
var _tooltipSuppressUntil = 0;
|
||||
|
||||
// Detect touch device once (re-evaluated each call is fine too)
|
||||
function _isTouchDevice() {
|
||||
return ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
|
||||
}
|
||||
|
||||
function _tooltipUIEnter() {
|
||||
_mouseOverTooltipUI++;
|
||||
_cancelTooltipHide();
|
||||
}
|
||||
|
||||
function _tooltipUILeave() {
|
||||
_mouseOverTooltipUI--;
|
||||
_cancelTooltipHide();
|
||||
_tooltipHideTimer = setTimeout(function() {
|
||||
if (_mouseOverTooltipUI <= 0) {
|
||||
_mouseOverTooltipUI = 0;
|
||||
hideTooltip();
|
||||
}
|
||||
}, 60);
|
||||
}
|
||||
|
||||
function _cancelTooltipHide() {
|
||||
if (_tooltipHideTimer) { clearTimeout(_tooltipHideTimer); _tooltipHideTimer = null; }
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
_cancelTooltipHide();
|
||||
_mouseOverTooltipUI = 0;
|
||||
var tip = document.getElementById('cat-tooltip');
|
||||
if (!tip) return;
|
||||
tip.classList.remove('visible');
|
||||
tip.dataset.openedBy = '';
|
||||
tip.dataset.mode = '';
|
||||
// Also close reverse info tooltip for consistency
|
||||
var reverseTt = document.getElementById('reverse-cat-tooltip');
|
||||
if (reverseTt) reverseTt.classList.remove('open');
|
||||
// blur any focused element to clear :active styling on buttons
|
||||
if (document.activeElement && document.activeElement.classList.contains('reverse-sort-btn')) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
// suppress immediate reopening (e.g. synthetic click) for 400ms
|
||||
_tooltipSuppressUntil = Date.now() + 400;
|
||||
}
|
||||
|
||||
|
||||
function _positionTooltip(tip, anchor) {
|
||||
// On mobile: center horizontally on screen, appear above the button
|
||||
// so it's not hidden under the finger.
|
||||
var rect = anchor.getBoundingClientRect();
|
||||
var tw = Math.min(280, window.innerWidth - 16);
|
||||
var left, top;
|
||||
if (_isTouchDevice()) {
|
||||
left = (window.innerWidth - tw) / 2;
|
||||
// prefer above; fall back to below if not enough room
|
||||
top = rect.top - 8 + window.scrollY;
|
||||
if (top - 120 < window.scrollY) {
|
||||
top = rect.bottom + 8 + window.scrollY;
|
||||
} else {
|
||||
top = rect.top - 8 + window.scrollY; // will be shifted up by CSS transform
|
||||
}
|
||||
} else {
|
||||
left = rect.left + rect.width / 2 - tw / 2;
|
||||
top = rect.bottom + 8 + window.scrollY;
|
||||
left = Math.max(8, Math.min(left, window.innerWidth - tw - 8));
|
||||
}
|
||||
tip.style.left = left + 'px';
|
||||
tip.style.top = top + 'px';
|
||||
tip.style.width = tw + 'px';
|
||||
}
|
||||
|
||||
// Helper to show the shared cat-tooltip; we keep the old positioning logic but
|
||||
// ensure the bubble is hidden while we measure its height/width so that the CSS
|
||||
// transition never fires prematurely (this was causing the "ghost" flash on mobile).
|
||||
function _showCatTooltip(catId, anchor, mode) {
|
||||
// suppress reopening if it was just hidden a moment ago
|
||||
if (Date.now() < _tooltipSuppressUntil) return;
|
||||
|
||||
var cat = gameCategories.find(function(c) { return c.id === catId; });
|
||||
if (!cat) return;
|
||||
var tip = document.getElementById('cat-tooltip');
|
||||
_cancelTooltipHide();
|
||||
tip.dataset.mode = mode;
|
||||
tip.dataset.openedBy = mode + '-' + catId;
|
||||
|
||||
if (mode === 'info') {
|
||||
document.getElementById('tt-title').textContent = cat.icon + ' ' + catName(cat);
|
||||
document.getElementById('tt-body').textContent = catDesc(cat);
|
||||
} else {
|
||||
document.getElementById('tt-title').innerHTML = '⇅ ' + catName(cat);
|
||||
document.getElementById('tt-body').textContent = catSort(cat);
|
||||
}
|
||||
|
||||
// temporarily hide the tip off-screen while we compute its size so the
|
||||
// CSS transition can't flash; this mirrors the logic added to hints.js.
|
||||
tip.style.visibility = 'hidden';
|
||||
tip.style.position = 'fixed';
|
||||
tip.style.top = '-9999px';
|
||||
tip.classList.add('visible');
|
||||
var tipH = tip.offsetHeight;
|
||||
tip.classList.remove('visible');
|
||||
tip.style.visibility = '';
|
||||
// Only position on desktop; on mobile, CSS .visible handles all positioning
|
||||
if (!_isTouchDevice()) {
|
||||
_positionTooltip(tip, anchor);
|
||||
}
|
||||
tip.classList.add('visible');
|
||||
}
|
||||
|
||||
|
||||
function showTooltip(catId, anchor) {
|
||||
_showCatTooltip(catId, anchor, 'info');
|
||||
}
|
||||
|
||||
function showSortTooltip(catId, anchor) {
|
||||
// If we're in reverse mode, show the info-style dropdown (reverse-cat-tooltip)
|
||||
var isReverse = (gameMode === 'reverse' || (gameMode === 'custom' && customSubMode === 'reverse'));
|
||||
if (isReverse) {
|
||||
// close floating cat-tooltip if present
|
||||
var floating = document.getElementById('cat-tooltip');
|
||||
if (floating) { floating.classList.remove('visible'); floating.dataset.openedBy = ''; floating.dataset.mode = ''; }
|
||||
var el = document.getElementById('reverse-cat-tooltip');
|
||||
var cat = gameCategories.find(function(c) { return c.id === catId; });
|
||||
if (!el || !cat) return;
|
||||
// toggle behavior: if already open for this sort, close it
|
||||
if (el.classList.contains('open') && el.dataset.openedBy === 'sort-' + catId) {
|
||||
el.classList.remove('open'); el.dataset.openedBy = '';
|
||||
} else {
|
||||
el.dataset.openedBy = 'sort-' + catId;
|
||||
el.textContent = catSort(cat);
|
||||
el.classList.add('open');
|
||||
}
|
||||
return;
|
||||
}
|
||||
_showCatTooltip(catId, anchor, 'sort');
|
||||
}
|
||||
|
||||
// Close tooltip when tapping outside on mobile
|
||||
function _initGlobalTouchClose() {
|
||||
if (document._tooltipTouchCloseAttached) return;
|
||||
document._tooltipTouchCloseAttached = true;
|
||||
|
||||
// Close tooltips when tapping outside on mobile
|
||||
document.addEventListener('touchstart', function(e) {
|
||||
var tip = document.getElementById('cat-tooltip');
|
||||
if (tip && tip.classList.contains('visible')) {
|
||||
// If touch is on the tooltip itself → do nothing (let it stay open)
|
||||
if (tip.contains(e.target)) return;
|
||||
// If touch is on a tooltip-trigger button → the button handler will deal with it
|
||||
if (e.target.closest('.cat-info-btn') || e.target.closest('.cat-sort-btn') ||
|
||||
e.target.closest('.reverse-sort-btn')) return;
|
||||
hideTooltip();
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
// Also close reverse info tooltip when clicking outside in reverse mode
|
||||
document.addEventListener('click', function(e) {
|
||||
var reverseTt = document.getElementById('reverse-cat-tooltip');
|
||||
if (!reverseTt || !reverseTt.classList.contains('open')) return;
|
||||
// If click is on the tooltip itself → do nothing (let it stay open)
|
||||
if (reverseTt.contains(e.target)) return;
|
||||
// If click is on the info button → the button handler will deal with it
|
||||
if (e.target.closest('.reverse-info-toggle')) return;
|
||||
reverseTt.classList.remove('open');
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
// Helper: attach touch-and-click to a tooltip trigger button
|
||||
// showFn(catId, anchor) — the function that populates + shows the tooltip
|
||||
// openedByKey — the string stored in tip.dataset.openedBy when this btn is active
|
||||
function _attachTooltipTrigger(clone, openedByKey, showFn) {
|
||||
var touch = _isTouchDevice();
|
||||
|
||||
if (touch) {
|
||||
// ── MOBILE: tap-to-toggle only ────────────────────────────────────────
|
||||
clone.addEventListener('touchend', function(e) {
|
||||
e.preventDefault(); // prevent ghost click + page scroll
|
||||
e.stopPropagation();
|
||||
var tip = document.getElementById('cat-tooltip');
|
||||
if (tip.classList.contains('visible') && tip.dataset.openedBy === openedByKey) {
|
||||
hideTooltip();
|
||||
} else {
|
||||
showFn(clone.dataset.catid, clone);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// ── DESKTOP: hover + click-to-toggle ─────────────────────────────────
|
||||
clone.addEventListener('mouseenter', function() {
|
||||
_tooltipUIEnter();
|
||||
showFn(clone.dataset.catid, clone);
|
||||
});
|
||||
clone.addEventListener('mouseleave', _tooltipUILeave);
|
||||
clone.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
var tip = document.getElementById('cat-tooltip');
|
||||
if (tip.classList.contains('visible')) hideTooltip();
|
||||
else showTooltip(clone.dataset.catid, clone);
|
||||
if (tip.classList.contains('visible') && tip.dataset.openedBy === openedByKey) {
|
||||
hideTooltip();
|
||||
} else {
|
||||
showFn(clone.dataset.catid, clone);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Variant that only responds to explicit clicks/taps – no hover logic.
|
||||
// Used for reverse mode: click on desktop, tap on mobile (same as normal mode on mobile, but no hover on desktop).
|
||||
function _attachTooltipClickOnly(el, openedByKey, showFn) {
|
||||
if (_isTouchDevice()) {
|
||||
// ── MOBILE: exactly like normal mode (tap-to-toggle) ────────────────
|
||||
el.addEventListener('touchend', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var tip = document.getElementById('cat-tooltip');
|
||||
if (tip.classList.contains('visible') && tip.dataset.openedBy === openedByKey) {
|
||||
hideTooltip();
|
||||
} else {
|
||||
showFn(el.dataset.catid, el);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// ── DESKTOP: click-only, no hover ──────────────────────────────────
|
||||
el.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
var tip = document.getElementById('cat-tooltip');
|
||||
if (tip.classList.contains('visible') && tip.dataset.openedBy === openedByKey) {
|
||||
hideTooltip();
|
||||
} else {
|
||||
showFn(el.dataset.catid, el);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function attachInfoListeners() {
|
||||
_initGlobalTouchClose();
|
||||
|
||||
// ── Info buttons (i) ──────────────────────────────────────────────────────
|
||||
document.querySelectorAll('.cat-info-btn').forEach(function(el) {
|
||||
var clone = el.cloneNode(true);
|
||||
el.parentNode.replaceChild(clone, el);
|
||||
_attachTooltipTrigger(clone, 'info-' + clone.dataset.catid, showTooltip);
|
||||
});
|
||||
|
||||
// ── Sort buttons (⇅) ─────────────────────────────────────────────────────
|
||||
document.querySelectorAll('.cat-sort-btn').forEach(function(el) {
|
||||
var clone = el.cloneNode(true);
|
||||
el.parentNode.replaceChild(clone, el);
|
||||
_attachTooltipTrigger(clone, 'sort-' + clone.dataset.catid, showSortTooltip);
|
||||
});
|
||||
|
||||
// ── Tooltip bubble: keep open while hovered (desktop only) ───────────────
|
||||
var tip = document.getElementById('cat-tooltip');
|
||||
if (!tip.dataset.listenerAttached) {
|
||||
tip.dataset.listenerAttached = '1';
|
||||
if (!_isTouchDevice()) {
|
||||
tip.addEventListener('mouseenter', _tooltipUIEnter);
|
||||
tip.addEventListener('mouseleave', _tooltipUILeave);
|
||||
}
|
||||
// On touch: tapping inside the tooltip does nothing (global handler ignores it)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── TURN DISPLAY ─────────────────────────────────────────────────────────────
|
||||
|
|
@ -304,12 +552,12 @@ function showTurn() {
|
|||
btn.innerHTML = makeCatBtnHTML(cat);
|
||||
btn.style.borderColor = '';
|
||||
btn.style.color = '';
|
||||
// re-bind click
|
||||
var newBtn = btn.cloneNode(true);
|
||||
btn.parentNode.replaceChild(newBtn, btn);
|
||||
(function(catId) {
|
||||
newBtn.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.cat-info-btn')) return;
|
||||
if (e.target.closest('.cat-sort-btn')) return;
|
||||
handleChoice(catId);
|
||||
});
|
||||
})(cat.id);
|
||||
|
|
@ -326,30 +574,57 @@ function showTurn() {
|
|||
|
||||
// ─── REVERSE MODE TURN ────────────────────────────────────────────────────────
|
||||
function showTurnReverse() {
|
||||
// In reverse: currentStep = which category is active
|
||||
// All countries shown as buttons; user picks which country gets this category
|
||||
// make sure any open cat-tooltip from normal mode is shut, removing ghost flashes
|
||||
hideTooltip();
|
||||
var cat = gameCategories[currentStep];
|
||||
document.getElementById('turn-label').textContent = t('reverseTurn') + ' ' + (currentStep+1) + ' ' + t('of') + ' ' + gameCategories.length;
|
||||
document.getElementById('total-score').textContent = totalScore;
|
||||
|
||||
// Show the active category
|
||||
document.getElementById('reverse-cat-icon').textContent = cat.icon;
|
||||
applyEmoji(document.getElementById('reverse-cat-icon'));
|
||||
document.getElementById('reverse-cat-name').textContent = catName(cat);
|
||||
document.getElementById('reverse-cat-tooltip').textContent = catDesc(cat);
|
||||
document.getElementById('reverse-cat-tooltip').classList.remove('open');
|
||||
|
||||
// Update reverse sort button
|
||||
var sortBtn = document.getElementById('reverse-sort-btn');
|
||||
if (sortBtn) {
|
||||
sortBtn.dataset.catid = cat.id;
|
||||
var newSortBtn = sortBtn.cloneNode(true);
|
||||
sortBtn.parentNode.replaceChild(newSortBtn, sortBtn);
|
||||
// reverse mode should use click-only tooltips to avoid hover activation
|
||||
if (gameMode === 'reverse') {
|
||||
_attachTooltipClickOnly(newSortBtn, 'sort-' + cat.id, showSortTooltip);
|
||||
} else {
|
||||
_attachTooltipTrigger(newSortBtn, 'sort-' + cat.id, showSortTooltip);
|
||||
}
|
||||
}
|
||||
|
||||
renderCountryButtons();
|
||||
clearFeedback();
|
||||
startTimer();
|
||||
}
|
||||
|
||||
function toggleReverseCatInfo() {
|
||||
// Close the sort tooltip before toggling info
|
||||
var sortTt = document.getElementById('cat-tooltip');
|
||||
if (sortTt && sortTt.classList.contains('visible')) {
|
||||
sortTt.classList.remove('visible');
|
||||
sortTt.dataset.openedBy = '';
|
||||
sortTt.dataset.mode = '';
|
||||
}
|
||||
// Now toggle the info tooltip
|
||||
var el = document.getElementById('reverse-cat-tooltip');
|
||||
el.classList.toggle('open');
|
||||
// refresh text in case lang changed
|
||||
var cat = gameCategories[currentStep];
|
||||
if (cat) el.textContent = catDesc(cat);
|
||||
if (!el) return;
|
||||
if (el.classList.contains('open') && el.dataset.openedBy === 'info-' + (cat ? cat.id : '')) {
|
||||
el.classList.remove('open');
|
||||
el.dataset.openedBy = '';
|
||||
} else {
|
||||
el.dataset.openedBy = 'info-' + (cat ? cat.id : '');
|
||||
el.textContent = cat ? catDesc(cat) : '';
|
||||
el.classList.add('open');
|
||||
}
|
||||
}
|
||||
|
||||
function renderCountryButtons() {
|
||||
|
|
@ -402,7 +677,6 @@ function handleCountryChoice(countryIdx) {
|
|||
reverseAssignments[countryIdx] = cat.id;
|
||||
isRevealing = true;
|
||||
|
||||
// Mark chosen button
|
||||
document.querySelectorAll('.country-btn').forEach(function(b) { b.disabled = true; });
|
||||
btn.style.borderColor = '#f5c842';
|
||||
btn.style.boxShadow = '0 0 14px rgba(245,200,66,0.35)';
|
||||
|
|
@ -423,7 +697,6 @@ function handleCountryChoice(countryIdx) {
|
|||
}
|
||||
|
||||
function autoPick_reverse() {
|
||||
// Timeout in reverse: penalty, assign to first available country
|
||||
var firstFree = -1;
|
||||
for (var i = 0; i < gameCountries.length; i++) {
|
||||
if (!reverseAssignments[i]) { firstFree = i; break; }
|
||||
|
|
@ -471,7 +744,6 @@ function autoPick_reverse() {
|
|||
function startTimer() {
|
||||
if (timerInterval && timerInterval._raf) timerInterval._raf(); timerInterval = null;
|
||||
|
||||
// Infinite time: show ∞, no countdown, no autopick
|
||||
if (TIME_PER_TURN === 0) {
|
||||
var numEl2 = document.getElementById('timer-num');
|
||||
var fillEl2 = document.getElementById('progress-fill');
|
||||
|
|
@ -513,7 +785,6 @@ function startTimer() {
|
|||
function updateTimerUI(remaining) {
|
||||
var numEl = document.getElementById('timer-num');
|
||||
var fillEl = document.getElementById('progress-fill');
|
||||
// remaining can be undefined on first call
|
||||
var pct = (remaining !== undefined)
|
||||
? (remaining / TIME_PER_TURN * 100)
|
||||
: 100;
|
||||
|
|
@ -523,7 +794,6 @@ function updateTimerUI(remaining) {
|
|||
var urgent = displayNum <= (isHardcoreActive() ? 4 : 8);
|
||||
numEl.classList.toggle('urgent', urgent);
|
||||
fillEl.classList.toggle('urgent', urgent);
|
||||
// Hardcore danger pulse
|
||||
if (isHardcoreActive()) {
|
||||
numEl.classList.toggle('danger', displayNum <= 4);
|
||||
} else {
|
||||
|
|
@ -546,13 +816,11 @@ function autoPick() {
|
|||
var flag = country.flag || country.emoji || '🌍';
|
||||
gameLog.push({flag:flag, name:cName, catObj:null, points:PENALTY, isPenalty:true, country:country});
|
||||
|
||||
// HC: no overlay, advance silently
|
||||
if (isHardcoreActive()) {
|
||||
setTimeout(function() { isRevealing = false; currentStep++; showTurn(); }, 350);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal mode: show penalty overlay
|
||||
document.getElementById('total-score').textContent = totalScore;
|
||||
var overlay = document.createElement('div');
|
||||
overlay.id = 'reveal-overlay';
|
||||
|
|
@ -637,11 +905,9 @@ function disableAllCatButtons() {
|
|||
|
||||
// ─── STAR RATING HELPERS ─────────────────────────────────────────────────────
|
||||
function computeStarData(country, chosenCatId) {
|
||||
// Le jeu = MINIMISER le score : etoile = valeur la plus BASSE choisie
|
||||
var chosenRaw = country[chosenCatId];
|
||||
var chosenVal = (chosenRaw === false || chosenRaw === null || chosenRaw === undefined || isNaN(Number(chosenRaw))) ? null : Number(chosenRaw);
|
||||
if (chosenVal === null) return { chosenVal:0, gameBest:0, globalBest:0, isGameBest:false, isGlobalBest:false };
|
||||
// gameBest = valeur minimale parmi les categories disponibles pour ce pays
|
||||
var gb = Infinity;
|
||||
gameCategories.forEach(function(cat) {
|
||||
var v = country[cat.id];
|
||||
|
|
@ -649,7 +915,6 @@ function computeStarData(country, chosenCatId) {
|
|||
var n = Number(v); if (n < gb) gb = n;
|
||||
}
|
||||
});
|
||||
// globalBest = valeur minimale sur TOUTES les categories connues
|
||||
var ab = Infinity;
|
||||
ALL_CATEGORIES.forEach(function(cat) {
|
||||
var v = country[cat.id];
|
||||
|
|
@ -663,8 +928,8 @@ function computeStarData(country, chosenCatId) {
|
|||
var isGlobalBest = (chosenVal > 0 && chosenVal <= ab);
|
||||
return { chosenVal:chosenVal, gameBest:gb, globalBest:ab, isGameBest:isGameBest, isGlobalBest:isGlobalBest };
|
||||
}
|
||||
|
||||
function computeStarDataReverse(cat, chosenIdx) {
|
||||
// Mode reverse : etoile = pays avec la valeur la plus BASSE pour cette categorie
|
||||
var country = gameCountries[chosenIdx];
|
||||
var chosenRaw = country[cat.id];
|
||||
var chosenVal = (chosenRaw===false||chosenRaw===null||chosenRaw===undefined||isNaN(Number(chosenRaw))) ? null : Number(chosenRaw);
|
||||
|
|
@ -689,7 +954,7 @@ function computeStarDataReverse(cat, chosenIdx) {
|
|||
var isGlobalBest = (chosenVal > 0 && chosenVal <= ab);
|
||||
return { chosenVal:chosenVal, gameBest:gb, globalBest:ab, isGameBest:isGameBest, isGlobalBest:isGlobalBest };
|
||||
}
|
||||
// Panneau 'meilleurs choix' : trier par valeur ASCENDANTE (bas = meilleur score)
|
||||
|
||||
function buildBestCatsPanel(country, chosenCatId) {
|
||||
var rows=[];
|
||||
gameCategories.forEach(function(cat){var v=country[cat.id];if(v!==false&&v!==null&&v!==undefined&&!isNaN(Number(v)))rows.push({cat:cat,val:Number(v)});});
|
||||
|
|
@ -699,6 +964,7 @@ function buildBestCatsPanel(country, chosenCatId) {
|
|||
return '<div class="reveal-best-row"><span class="rbr-cat">'+r.cat.icon+' '+catName(r.cat)+mark+'</span><span class="rbr-val">'+r.val+'</span></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function buildBestCountriesPanel(cat, chosenIdx) {
|
||||
var rows=[];
|
||||
gameCountries.forEach(function(c,i){var v=c[cat.id];if(v!==false&&v!==null&&v!==undefined&&!isNaN(Number(v)))rows.push({c:c,i:i,val:Number(v)});});
|
||||
|
|
@ -709,6 +975,7 @@ function buildBestCountriesPanel(cat, chosenIdx) {
|
|||
return '<div class="reveal-best-row"><span class="rbr-cat">'+flag+' '+getCountryName(r.c)+mark+'</span><span class="rbr-val">'+r.val+'</span></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function showReveal(flag, cName, catObj, finalValue, onDone, starData, bestPanelHtml) {
|
||||
var overlay = document.createElement('div');
|
||||
overlay.id = 'reveal-overlay';
|
||||
|
|
@ -835,4 +1102,3 @@ function clearFeedback() {
|
|||
var el = document.getElementById('feedback');
|
||||
el.textContent = ''; el.className = 'feedback';
|
||||
}
|
||||
|
||||
|
|
|
|||
13
js/hints.js
13
js/hints.js
|
|
@ -12,11 +12,15 @@ function showTooltip(catId, targetEl) {
|
|||
var left = Math.max(8, Math.min(rect.left + rect.width/2 - tipWidth/2, window.innerWidth - tipWidth - 8));
|
||||
tip.style.width = tipWidth + 'px';
|
||||
tip.style.left = left + 'px';
|
||||
/* Measure height without triggering transition: use offsetHeight with visibility hidden + position off-screen */
|
||||
tip.style.visibility = 'hidden';
|
||||
tip.classList.add('visible');
|
||||
tip.style.position = 'fixed';
|
||||
tip.style.top = '-9999px';
|
||||
tip.classList.add('visible'); /* Add class to get full computed size */
|
||||
var tipH = tip.offsetHeight;
|
||||
tip.classList.remove('visible');
|
||||
tip.classList.remove('visible'); /* Remove before showing to avoid flash */
|
||||
tip.style.visibility = '';
|
||||
/* Now set correct position and show with transition */
|
||||
tip.style.top = (rect.top - 8 - tipH < 8) ? (rect.bottom + 8) + 'px' : (rect.top - 8 - tipH) + 'px';
|
||||
clearTimeout(tooltipTimeout);
|
||||
tip.classList.add('visible');
|
||||
|
|
@ -24,9 +28,8 @@ function showTooltip(catId, targetEl) {
|
|||
|
||||
function hideTooltip() {
|
||||
clearTimeout(tooltipTimeout);
|
||||
tooltipTimeout = setTimeout(function() {
|
||||
document.getElementById('cat-tooltip').classList.remove('visible');
|
||||
}, 120);
|
||||
/* Remove visible class immediately to avoid flash on rapid clicks */
|
||||
document.getElementById('cat-tooltip').classList.remove('visible');
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
|
|
|
|||
99
js/setup.js
99
js/setup.js
|
|
@ -2,6 +2,15 @@
|
|||
var customTimeSelected = 20;
|
||||
var customSubMode = 'normal'; // 'normal' | 'hardcore' | 'reverse'
|
||||
|
||||
// list of category IDs used by the “Sport Enjoyer” preset; keeps only
|
||||
// rugby (men), football men/women and men’s basketball.
|
||||
var SPORT_CAT_IDS = [
|
||||
'classement_rugby_H',
|
||||
'classement_fifa_H',
|
||||
'classement_fifa_F',
|
||||
'classement_bball_M'
|
||||
];
|
||||
|
||||
function selectSubModePill(mode) {
|
||||
customSubMode = mode;
|
||||
['normal','hardcore','reverse'].forEach(function(m) {
|
||||
|
|
@ -77,6 +86,9 @@ function applyPreset(name, btn) {
|
|||
if (Object.keys(ribbonState).length === 0) ribbonInit();
|
||||
if (Object.keys(catRibbonState).length === 0) catRibbonInit();
|
||||
|
||||
// make sure preset code lists exist (build once from country data)
|
||||
ensurePresets();
|
||||
|
||||
// Helper : set country ribbon by code whitelist
|
||||
function setCountryByList(allowedCodes) {
|
||||
for (var i = 0; i < countriesDB.length; i++) {
|
||||
|
|
@ -96,6 +108,70 @@ function applyPreset(name, btn) {
|
|||
catRibbonSelected = {};
|
||||
}
|
||||
|
||||
// Helper to safely set country list from a global codes array, with a fallback
|
||||
function safeSetCodes(varName) {
|
||||
try {
|
||||
var codes = window[varName];
|
||||
if (codes && Array.isArray(codes)) {
|
||||
setCountryByList(codes);
|
||||
} else {
|
||||
console.warn('Preset codes not found or invalid:', varName);
|
||||
resetCountries();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error accessing preset codes:', varName, e);
|
||||
resetCountries();
|
||||
}
|
||||
resetCats();
|
||||
setPresetDefaults();
|
||||
}
|
||||
|
||||
// ensurePresets() builds the various *_CODES arrays from the country database
|
||||
var _presetsDone = false;
|
||||
function ensurePresets() {
|
||||
if (_presetsDone) return;
|
||||
if (!countriesDB || countriesDB.length === 0) return; // can't build yet
|
||||
_presetsDone = true;
|
||||
|
||||
// utility for description text (fr preferred)
|
||||
function descText(code) {
|
||||
var d = COUNTRY_DESCRIPTIONS[code];
|
||||
if (!d) return '';
|
||||
return (d.fr || d.en || '').toString().toLowerCase();
|
||||
}
|
||||
|
||||
// initialize empty arrays on window
|
||||
window.EUROPEAN_CODES = [];
|
||||
window.AFRICAN_CODES = [];
|
||||
window.AMERICAS_CODES = [];
|
||||
window.ASIAN_CODES = [];
|
||||
window.OCEANIA_CODES = [];
|
||||
window.TOP100_PIB_CODES = [];
|
||||
window.SMALL_COUNTRIES_CODES = [];
|
||||
window.FRANCE_NEIGHBORS_CODES = ['BE','LU','DE','CH','IT','ES','AD','MC'];
|
||||
window.LANDLOCKED_CODES = [];
|
||||
window.ISLANDS_CODES = [];
|
||||
|
||||
countriesDB.forEach(function(c) {
|
||||
var code = (c.country_code||'').toUpperCase();
|
||||
var d = descText(code);
|
||||
if (/afrique/.test(d)) window.AFRICAN_CODES.push(code);
|
||||
if (/am[eé]rique/.test(d)) window.AMERICAS_CODES.push(code);
|
||||
if (/asie/.test(d)) window.ASIAN_CODES.push(code);
|
||||
if (/europ/.test(d)) window.EUROPEAN_CODES.push(code);
|
||||
if (/oc[eé]an|pacifique/.test(d) || code==='AU' || code==='NZ') window.OCEANIA_CODES.push(code);
|
||||
if (/île|archipel|island/.test(d)) window.ISLANDS_CODES.push(code);
|
||||
if (/enclav|landlocked/.test(d)) window.LANDLOCKED_CODES.push(code);
|
||||
if (typeof c.pib === 'number' && c.pib <= 100) window.TOP100_PIB_CODES.push(code);
|
||||
if (typeof c.nb_habitant === 'number' && c.nb_habitant >= 200) window.SMALL_COUNTRIES_CODES.push(code);
|
||||
});
|
||||
|
||||
// remove duplicates just in case
|
||||
['EUROPEAN_CODES','AFRICAN_CODES','AMERICAS_CODES','ASIAN_CODES','OCEANIA_CODES','LANDLOCKED_CODES','ISLANDS_CODES'].forEach(function(varName) {
|
||||
window[varName] = Array.from(new Set(window[varName]));
|
||||
});
|
||||
}
|
||||
|
||||
if (name === 'no-russia') {
|
||||
resetCountries();
|
||||
for (var i = 0; i < countriesDB.length; i++) {
|
||||
|
|
@ -103,17 +179,16 @@ function applyPreset(name, btn) {
|
|||
}
|
||||
resetCats();
|
||||
setPresetDefaults();
|
||||
|
||||
} else if (name === 'europe') { setCountryByList(EUROPEAN_CODES); resetCats(); setPresetDefaults();
|
||||
} else if (name === 'africa') { setCountryByList(AFRICAN_CODES); resetCats(); setPresetDefaults();
|
||||
} else if (name === 'americas') { setCountryByList(AMERICAS_CODES); resetCats(); setPresetDefaults();
|
||||
} else if (name === 'asia') { setCountryByList(ASIAN_CODES); resetCats(); setPresetDefaults();
|
||||
} else if (name === 'oceania') { setCountryByList(OCEANIA_CODES); resetCats(); setPresetDefaults();
|
||||
} else if (name === 'top100pib'){ setCountryByList(TOP100_PIB_CODES);resetCats(); setPresetDefaults();
|
||||
} else if (name === 'small') { setCountryByList(SMALL_COUNTRIES_CODES); resetCats(); setPresetDefaults();
|
||||
} else if (name === 'france-neighbors') { setCountryByList(FRANCE_NEIGHBORS_CODES); resetCats(); setPresetDefaults();
|
||||
} else if (name === 'landlocked') { setCountryByList(LANDLOCKED_CODES); resetCats(); setPresetDefaults();
|
||||
} else if (name === 'islands') { setCountryByList(ISLANDS_CODES); resetCats(); setPresetDefaults();
|
||||
} else if (name === 'africa') { safeSetCodes('AFRICAN_CODES');
|
||||
} else if (name === 'americas') { safeSetCodes('AMERICAS_CODES');
|
||||
} else if (name === 'asia') { safeSetCodes('ASIAN_CODES');
|
||||
} else if (name === 'europe') { safeSetCodes('EUROPEAN_CODES');
|
||||
} else if (name === 'oceania') { safeSetCodes('OCEANIA_CODES');
|
||||
} else if (name === 'top100pib'){ safeSetCodes('TOP100_PIB_CODES');
|
||||
} else if (name === 'small') { safeSetCodes('SMALL_COUNTRIES_CODES');
|
||||
} else if (name === 'france-neighbors') { safeSetCodes('FRANCE_NEIGHBORS_CODES');
|
||||
} else if (name === 'landlocked') { safeSetCodes('LANDLOCKED_CODES');
|
||||
} else if (name === 'islands') { safeSetCodes('ISLANDS_CODES');
|
||||
|
||||
} else if (name === 'flag-guesser') {
|
||||
resetCountries(); resetCats();
|
||||
|
|
@ -130,7 +205,7 @@ function applyPreset(name, btn) {
|
|||
} else if (name === 'sport') {
|
||||
resetCountries();
|
||||
for (var i = 0; i < ALL_CATEGORIES.length; i++) {
|
||||
catRibbonState[i] = (SPORT_CAT_IDS.indexOf(ALL_CATEGORIES[i].id) !== -1) ? 'incl' : 'poss';
|
||||
catRibbonState[i] = (SPORT_CAT_IDS.indexOf(ALL_CATEGORIES[i].id) !== -1) ? 'incl' : 'excl';
|
||||
}
|
||||
catRibbonSelected = {};
|
||||
setPresetDefaults();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue