431 lines
No EOL
15 KiB
JavaScript
431 lines
No EOL
15 KiB
JavaScript
/*
|
|
* Page d'accueil - gestion capitaine + leaderboard
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
const MAX_SCORES_DISPLAY = 5;
|
|
|
|
// refs DOM (on les cache au début)
|
|
const DOM = {
|
|
captainSection: null,
|
|
captainInput: null,
|
|
saveCaptainBtn: null,
|
|
difficultySection: null,
|
|
captainGreeting: null,
|
|
subTitle: null,
|
|
resultsTableBody: null,
|
|
audioToggle: null,
|
|
audioSliderContainer: null,
|
|
masterVolumeSlider: null,
|
|
volumeValue: null,
|
|
snowContainer: null
|
|
};
|
|
|
|
// textes selon si le joueur est nouveau ou pas
|
|
const TEXTS = {
|
|
new: {
|
|
subtitle: "Traversez les eaux glacées arctiques et livrez votre précieuse cargaison.",
|
|
greeting: "Prêt pour votre première traversée, Capitaine ?",
|
|
noScores: "Votre journal de bord est vide. Partez en mer pour écrire votre légende !"
|
|
},
|
|
returning: {
|
|
subtitle: "Content de vous revoir ! Prêt à battre vos records ?",
|
|
greetings: [
|
|
"De retour aux commandes, Capitaine {name} ! La mer vous attend.",
|
|
"Les icebergs n'attendent que vous, Capitaine {name}.",
|
|
"Capitaine {name}, votre équipage est prêt à lever l'ancre !",
|
|
"Ahoy Capitaine {name} ! Une nouvelle tempête se profile.",
|
|
"Bienvenue à bord, Capitaine {name}. Le cargo est chargé."
|
|
],
|
|
noScores: "Hmm, votre journal semble vide. Reprenez la mer !"
|
|
},
|
|
veteran: {
|
|
subtitle: "Légende des mers glacées ! Votre réputation vous précède.",
|
|
greetings: [
|
|
"La légende revient ! Capitaine {name}, montrez-leur de quel bois vous êtes fait.",
|
|
"Capitaine {name}, vos exploits sont chantés dans tous les ports arctiques !",
|
|
"Les icebergs tremblent à votre approche, Capitaine {name} !"
|
|
]
|
|
}
|
|
};
|
|
|
|
// --- Init ---
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
function init() {
|
|
cacheDOMElements();
|
|
initSnowEffect();
|
|
initAudioControls();
|
|
|
|
// Vérifier et sauvegarder un nouveau score depuis l'URL
|
|
checkAndSaveNewScore();
|
|
|
|
// Configurer l'interface selon le statut du joueur
|
|
setupPlayerExperience();
|
|
|
|
// Charger le leaderboard
|
|
loadLeaderboard();
|
|
|
|
// Attacher les événements
|
|
bindEvents();
|
|
}
|
|
|
|
function cacheDOMElements() {
|
|
DOM.captainSection = document.getElementById('captain-section');
|
|
DOM.captainInput = document.getElementById('captain-name-input');
|
|
DOM.saveCaptainBtn = document.getElementById('save-captain-btn');
|
|
DOM.difficultySection = document.getElementById('difficulty-selection');
|
|
DOM.captainGreeting = document.getElementById('captain-greeting');
|
|
DOM.subTitle = document.getElementById('sub-title');
|
|
DOM.resultsTableBody = document.getElementById('results-table-body');
|
|
DOM.audioToggle = document.getElementById('audio-toggle');
|
|
DOM.audioSliderContainer = document.getElementById('audio-slider-container');
|
|
DOM.masterVolumeSlider = document.getElementById('master-volume');
|
|
DOM.volumeValue = document.getElementById('volume-value');
|
|
DOM.snowContainer = document.getElementById('snow-container');
|
|
}
|
|
|
|
// flocons de neige
|
|
function initSnowEffect() {
|
|
if (!DOM.snowContainer) return;
|
|
|
|
const snowflakeCount = 50;
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
for (let i = 0; i < snowflakeCount; i++) {
|
|
const flake = document.createElement('div');
|
|
flake.className = 'snowflake';
|
|
flake.style.cssText = `
|
|
left: ${Math.random() * 100}%;
|
|
animation-delay: ${Math.random() * 10}s;
|
|
animation-duration: ${8 + Math.random() * 7}s;
|
|
opacity: ${0.3 + Math.random() * 0.7};
|
|
font-size: ${8 + Math.random() * 12}px;
|
|
`;
|
|
flake.textContent = '❄';
|
|
fragment.appendChild(flake);
|
|
}
|
|
|
|
DOM.snowContainer.appendChild(fragment);
|
|
}
|
|
|
|
// --- Audio ---
|
|
function initAudioControls() {
|
|
if (!DOM.audioToggle) return;
|
|
|
|
// Initialiser le gestionnaire audio
|
|
if (typeof AudioManager !== 'undefined') {
|
|
AudioManager.init();
|
|
}
|
|
|
|
// Charger le volume sauvegardé
|
|
const settings = typeof IceBreakerStorage !== 'undefined'
|
|
? IceBreakerStorage.getSettings()
|
|
: { masterVolume: 0.8 };
|
|
|
|
if (DOM.masterVolumeSlider) {
|
|
DOM.masterVolumeSlider.value = Math.round(settings.masterVolume * 100);
|
|
updateVolumeDisplay(settings.masterVolume * 100);
|
|
}
|
|
|
|
// Initialiser la musique d'ambiance
|
|
initBackgroundMusic(settings.masterVolume);
|
|
|
|
// Toggle du panneau audio
|
|
DOM.audioToggle.addEventListener('click', () => {
|
|
DOM.audioSliderContainer.classList.toggle('hidden');
|
|
});
|
|
|
|
// Changement de volume
|
|
if (DOM.masterVolumeSlider) {
|
|
DOM.masterVolumeSlider.addEventListener('input', (e) => {
|
|
const value = parseInt(e.target.value);
|
|
updateVolumeDisplay(value);
|
|
|
|
if (typeof AudioManager !== 'undefined') {
|
|
AudioManager.setMasterVolume(value / 100);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Fermer le panneau en cliquant ailleurs
|
|
document.addEventListener('click', (e) => {
|
|
if (!e.target.closest('#audio-controls')) {
|
|
DOM.audioSliderContainer?.classList.add('hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
function initBackgroundMusic(masterVolume) {
|
|
const bgm = document.getElementById('background-audio');
|
|
if (!bgm) return;
|
|
|
|
bgm.volume = masterVolume * 0.5;
|
|
|
|
// Démarrer la musique au premier clic utilisateur (politique autoplay)
|
|
const startMusic = () => {
|
|
bgm.play().catch(() => { });
|
|
document.removeEventListener('click', startMusic);
|
|
document.removeEventListener('keydown', startMusic);
|
|
};
|
|
|
|
document.addEventListener('click', startMusic);
|
|
document.addEventListener('keydown', startMusic);
|
|
|
|
// Mettre à jour le volume quand le slider change
|
|
if (DOM.masterVolumeSlider) {
|
|
DOM.masterVolumeSlider.addEventListener('input', (e) => {
|
|
bgm.volume = (parseInt(e.target.value) / 100) * 0.5;
|
|
});
|
|
}
|
|
}
|
|
|
|
function updateVolumeDisplay(value) {
|
|
if (DOM.volumeValue) {
|
|
DOM.volumeValue.textContent = `${Math.round(value)}%`;
|
|
}
|
|
|
|
// Mettre à jour l'icône
|
|
const icon = DOM.audioToggle?.querySelector('.audio-icon');
|
|
if (icon) {
|
|
if (value === 0) {
|
|
icon.textContent = '🔇';
|
|
} else if (value < 50) {
|
|
icon.textContent = '🔉';
|
|
} else {
|
|
icon.textContent = '🔊';
|
|
}
|
|
}
|
|
}
|
|
|
|
// config interface selon le joueur
|
|
function setupPlayerExperience() {
|
|
const captainName = IceBreakerStorage.getCaptainName();
|
|
const playCount = IceBreakerStorage.getPlayCount();
|
|
const isReturning = IceBreakerStorage.isReturningPlayer();
|
|
|
|
if (!captainName) {
|
|
// Nouveau joueur : afficher le formulaire
|
|
showCaptainForm();
|
|
} else {
|
|
// Joueur existant : personnaliser l'interface
|
|
hideCaptainForm();
|
|
personalizeInterface(captainName, playCount);
|
|
}
|
|
}
|
|
|
|
function showCaptainForm() {
|
|
DOM.captainSection?.classList.remove('hidden');
|
|
DOM.subTitle.textContent = TEXTS.new.subtitle;
|
|
|
|
// Focus sur l'input après un délai pour l'animation
|
|
setTimeout(() => {
|
|
DOM.captainInput?.focus();
|
|
}, 500);
|
|
}
|
|
|
|
function hideCaptainForm() {
|
|
DOM.captainSection?.classList.add('hidden');
|
|
}
|
|
|
|
function personalizeInterface(captainName, playCount) {
|
|
let textSet;
|
|
|
|
if (playCount >= 10) {
|
|
textSet = TEXTS.veteran;
|
|
} else if (playCount > 0) {
|
|
textSet = TEXTS.returning;
|
|
} else {
|
|
textSet = TEXTS.new;
|
|
}
|
|
|
|
// Subtitle
|
|
DOM.subTitle.textContent = textSet.subtitle;
|
|
|
|
// Greeting personnalisé
|
|
if (textSet.greetings) {
|
|
const greeting = textSet.greetings[Math.floor(Math.random() * textSet.greetings.length)];
|
|
DOM.captainGreeting.textContent = greeting.replace('{name}', captainName);
|
|
} else {
|
|
DOM.captainGreeting.textContent = textSet.greeting || '';
|
|
}
|
|
}
|
|
|
|
// events
|
|
function bindEvents() {
|
|
// Sauvegarder le nom du capitaine
|
|
DOM.saveCaptainBtn?.addEventListener('click', saveCaptainName);
|
|
DOM.captainInput?.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') saveCaptainName();
|
|
});
|
|
|
|
// Boutons de difficulté
|
|
document.querySelectorAll('.diff-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
const difficulty = e.currentTarget.getAttribute('data-difficulty');
|
|
startGame(difficulty);
|
|
});
|
|
});
|
|
}
|
|
|
|
function saveCaptainName() {
|
|
const name = DOM.captainInput?.value.trim();
|
|
const MIN_LEN = 3;
|
|
const MAX_LEN = 20;
|
|
const validRe = /^[A-Za-z0-9 _-]+$/;
|
|
|
|
if (!name) {
|
|
showInputError('Veuillez entrer un pseudo.');
|
|
return;
|
|
}
|
|
// Validation du pseudo comme demandé
|
|
if (name.length < MIN_LEN || name.length > MAX_LEN || !validRe.test(name)) {
|
|
showInputError(`Pseudo invalide — ${MIN_LEN}-${MAX_LEN} caractères, lettres/chiffres/_/- seulement.`);
|
|
return;
|
|
}
|
|
|
|
IceBreakerStorage.saveCaptainName(name);
|
|
hideCaptainForm();
|
|
personalizeInterface(name, 0);
|
|
DOM.difficultySection?.classList.add('fade-in');
|
|
}
|
|
|
|
// démarrer le jeu
|
|
function startGame(difficulty) {
|
|
// Incrémenter le compteur de parties
|
|
IceBreakerStorage.incrementPlayCount();
|
|
|
|
// Rediriger vers le jeu
|
|
window.location.href = `cruise.html?score=0&difficulty=${difficulty}`;
|
|
}
|
|
|
|
// vérifier si on revient avec un score
|
|
function checkAndSaveNewScore() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
// Format unifié pour tous les scores (checkpoint et game over)
|
|
if (params.get('finalScore')) {
|
|
const totalDelivered = parseInt(params.get('totalDelivered') || 0);
|
|
const totalPossible = parseInt(params.get('totalPossible') || 1);
|
|
const cargoDeliveryRate = Math.round((totalDelivered / totalPossible) * 100);
|
|
|
|
const scoreData = {
|
|
score: parseInt(params.get('finalScore')),
|
|
difficulty: parseFloat(params.get('finalDifficulty') || 1),
|
|
cargoDeliveryRate: cargoDeliveryRate,
|
|
checkpoints: parseInt(params.get('checkpoints') || 0),
|
|
totalDelivered: totalDelivered,
|
|
totalPossible: totalPossible
|
|
};
|
|
|
|
if (!isNaN(scoreData.score)) {
|
|
IceBreakerStorage.saveScore(scoreData);
|
|
// Nettoyer l'URL
|
|
window.history.replaceState({}, document.title, window.location.pathname);
|
|
}
|
|
}
|
|
}
|
|
|
|
function loadLeaderboard() {
|
|
const scores = IceBreakerStorage.getLeaderboard();
|
|
|
|
if (!DOM.resultsTableBody) return;
|
|
|
|
DOM.resultsTableBody.innerHTML = '';
|
|
|
|
if (scores.length === 0) {
|
|
const isReturning = IceBreakerStorage.isReturningPlayer();
|
|
const noDataText = isReturning ? TEXTS.returning.noScores : TEXTS.new.noScores;
|
|
DOM.resultsTableBody.innerHTML = `<tr><td colspan="5" class="no-data">${noDataText}</td></tr>`;
|
|
return;
|
|
}
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
scores.slice(0, MAX_SCORES_DISPLAY).forEach((result, index) => {
|
|
const row = document.createElement('tr');
|
|
row.className = index === 0 ? 'top-score' : '';
|
|
|
|
const rankIcon = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `${index + 1}`;
|
|
|
|
// Afficher le pourcentage de livraison ou fallback pour anciens scores
|
|
let deliveryDisplay;
|
|
if (typeof result.cargoDeliveryRate === 'number') {
|
|
deliveryDisplay = `${result.cargoDeliveryRate}%`;
|
|
} else if (result.containers !== undefined) {
|
|
// Fallback pour anciens scores
|
|
const rate = Math.round((result.containers / (result.initialContainers || 20)) * 100);
|
|
deliveryDisplay = `${rate}%`;
|
|
} else {
|
|
deliveryDisplay = '-';
|
|
}
|
|
|
|
row.innerHTML = `
|
|
<td class="rank">${rankIcon}</td>
|
|
<td class="captain">${escapeHtml(result.captain || 'Anonyme')}</td>
|
|
<td class="score">${result.score.toLocaleString()}</td>
|
|
<td class="cargo">${deliveryDisplay}</td>
|
|
<td class="difficulty">${mapDifficulty(result.difficulty)}</td>
|
|
`;
|
|
|
|
fragment.appendChild(row);
|
|
});
|
|
|
|
DOM.resultsTableBody.appendChild(fragment);
|
|
}
|
|
|
|
// helpers
|
|
function mapDifficulty(diff) {
|
|
if (diff <= 1.1) return '❄️ Calme';
|
|
if (diff <= 1.6) return '🌊 Normal';
|
|
if (diff <= 2.6) return '⚡ Tempête';
|
|
return `🔥 Expert`;
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// messages d'erreur input
|
|
function showInputMessage(msg, type = 'error') {
|
|
if (!DOM.captainInput) return;
|
|
|
|
let msgEl = document.getElementById('captain-input-message');
|
|
|
|
if (!msgEl) {
|
|
msgEl = document.createElement('div');
|
|
msgEl.id = 'captain-input-message';
|
|
msgEl.className = 'input-message';
|
|
DOM.captainInput.parentNode?.insertBefore(msgEl, DOM.captainInput.nextSibling);
|
|
}
|
|
|
|
msgEl.textContent = msg;
|
|
msgEl.classList.remove('error', 'success', 'visible');
|
|
msgEl.classList.add(type);
|
|
|
|
// Force reflow then show for CSS transitions
|
|
void msgEl.offsetWidth;
|
|
msgEl.classList.add('visible');
|
|
|
|
// Animation visuelle sur l'input
|
|
DOM.captainInput.classList.remove('shake');
|
|
void DOM.captainInput.offsetWidth;
|
|
DOM.captainInput.classList.add('shake');
|
|
|
|
DOM.captainInput.focus();
|
|
|
|
// Nettoyage automatique
|
|
clearTimeout(showInputMessage._timeout);
|
|
showInputMessage._timeout = setTimeout(() => {
|
|
msgEl.classList.remove('visible');
|
|
DOM.captainInput.classList.remove('shake');
|
|
}, 3000);
|
|
}
|
|
|
|
function showInputError(msg) { showInputMessage(msg, 'error'); }
|
|
function showInputSuccess(msg) { showInputMessage(msg, 'success'); }
|
|
})(); |