Projet_Web_IceBreaker/assets/js/index.js
2025-12-21 13:10:23 +01:00

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'); }
})();