Riichi/frontend/pages/riichi/chap5.html
2026-03-14 11:18:29 +01:00

636 lines
No EOL
19 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Tuto Riichi</title>
<link rel="stylesheet" href="/frontend/css/style.css">
<link rel="stylesheet" href="/frontend/css/text.css">
<link rel="stylesheet" href="/frontend/css/speech-bubble.css">
<link rel="stylesheet" href="/frontend/css/hand.css">
<style>
#menu {
position: relative;
z-index: 10;
}
#mascotte {
position: absolute;
top: 670px;
right: 100px;
z-index: 999;
transform: scaleX(-1);
cursor: pointer;
}
#mascotte img {
width: 250px;
height: auto;
display: block;
}
.speech-bubble {
position: absolute;
top: 250px;
right: 170px;
z-index: 101;
display: none;
}
.speech-bubble.visible {
display: flex;
}
#tiles-container {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 10px;
padding: 20px;
}
:root {
--game_radius: 1100px;
}
#game-center {
position: absolute;
top: 0px;
left: 350px;
display: none;
align-items: center;
justify-content: center;
min-height: 100vh;
}
#start-screen {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
background: transparent;
z-index: 100;
}
#start-screen h2 {
color: white;
font-size: 3em;
margin-bottom: 20px;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
#start-screen p {
color: white;
font-size: 1.2em;
margin-bottom: 40px;
text-align: center;
max-width: 500px;
}
#start-button {
padding: 15px 50px;
font-size: 1.3em;
background-color: #FFD700;
color: #333;
border: none;
border-radius: 50px;
cursor: pointer;
font-weight: bold;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
}
#start-button:hover {
background-color: #FFA500;
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}
#start-button:active {
transform: scale(0.98);
}
#game {
position: relatives;
width: var(--game_radius);
height: var(--game_radius);
}
#hand-0 {
position: absolute;
transform: translate(-50%, 50%);
bottom: -10%;
left: 50%;
display: flex;
flex-direction: row;
align-items: flex-end;
gap: 5px;
padding: 20px 0 0 20px;
}
#hand-1 {
position: absolute;
transform: translate(-50%, -400%) rotate(-90deg) translate(0%, 450%);
bottom: -10%;
left: 50%;
display: flex;
flex-direction: row;
align-items: flex-end;
gap: 5px;
padding: 20px 0 0 20px;
}
.hand-tiles {
display: flex;
flex-direction: row;
gap: 5px;
}
.hand-with-draw {
display: flex;
flex-direction: row;
align-items: flex-end;
}
.draw-tile {
margin-left: 12px;
position: relative;
top: 0;
display: inline-block;
}
#hand-2 {
position: absolute;
transform: translate(-50%, -400%) rotate(180deg) translate(0%, 450%);
bottom: -10%;
left: 50%;
display: flex;
flex-direction: row;
align-items: flex-end;
gap: 5px;
padding: 20px 0 0 20px;
}
#hand-3 {
position: absolute;
transform: translate(-50%, -400%) rotate(90deg) translate(0%, 450%);
bottom: -10%;
left: 50%;
display: flex;
flex-direction: row;
align-items: flex-end;
gap: 5px;
padding: 20px 0 0 20px;
}
.player-tile:hover {
transform: translateY(-15px);
transition: transform 0.2s;
z-index: 2;
}
#game-info {
position: absolute;
top: 65.5%;
left: 49%;
transform: translate(-50%, -50%);
width: 259px;
height: 259px;
background-color: #f5f5f0;
border-radius: 20px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1;
}
#discard-0 {
position: absolute;
transform: translate(-50%, 50%);
bottom: 16%;
left: 48%;
display: block;
width: 250px;
height: auto;
padding: 10px;
}
#discard-1 {
position: absolute;
transform: translate(-50%, -400%) rotate(-90deg) translate(0%, 450%);
bottom: 25%;
left: 58%;
display: block;
width: 250px;
height: auto;
padding: 10px;
}
#discard-2 {
position: absolute;
transform: translate(-50%, -400%) rotate(180deg) translate(0%, 450%);
bottom: 35%;
left: 50.5%;
display: block;
width: 250px;
height: auto;
padding: 10px;
}
#discard-3 {
position: absolute;
transform: translate(-50%, -400%) rotate(90deg) translate(0%, 450%);
bottom: 27.5%;
left: 40%;
display: block;
width: 250px;
height: auto;
padding: 10px;
}
#discard-0 div,
#discard-1 div,
#discard-2 div,
#discard-3 div {
flex-shrink: 0;
}
.discard-tile svg {
height: 65px;
width: 48px;
}
.turn-indicator {
position: absolute;
display: none;
height: 6px;
background: linear-gradient(90deg, #FFD700, #FFA500);
border-radius: 3px;
box-shadow: 0 0 10px rgba(255, 215, 0, 0.6), inset 0 1px 2px rgba(255, 255, 255, 0.3);
}
#turn-indicator-0 {
transform: translate(-50%, 50%);
bottom: 20%;
left: 49%;
width: 250px;
}
#turn-indicator-1 {
transform: translate(-50%, -400%) rotate(-90deg) translate(0%, 450%);
bottom: 32.5%;
left: 61%;
width: 250px;
}
#turn-indicator-2 {
transform: translate(-50%, -400%) rotate(180deg) translate(0%, 450%);
bottom: 44%;
left: 49%;
width: 250px;
}
#turn-indicator-3 {
transform: translate(-50%, -400%) rotate(90deg) translate(0%, 450%);
bottom: 32%;
left: 37%;
width: 250px;
}
</style>
</head>
<body>
<div id="menu"></div>
<h1>Chapitre 5</h1>
<div class="speech-bubble">
<div class="speech-text" id="speech-text"></div>
</div>
<div id="mascotte"></div>
<div id="start-screen">
<h2>Riichi Mahjong</h2>
<p>Entraînez-vous à maîtriser les appels en jouant une partie contre trois bots</p>
<button id="start-button">Lancer la partie</button>
</div>
<div id="game-center">
<div id="game">
<div id="game-info"></div>
<div id="hand-0"></div>
<div id="discard-0"></div>
<div id="turn-indicator-0" class="turn-indicator"></div>
<div id="hand-1"></div>
<div id="discard-1"></div>
<div id="turn-indicator-1" class="turn-indicator"></div>
<div id="hand-2"></div>
<div id="discard-2"></div>
<div id="turn-indicator-2" class="turn-indicator"></div>
<div id="hand-3"></div>
<div id="discard-3"></div>
<div id="turn-indicator-3" class="turn-indicator"></div>
</div>
</div>
<script src="/frontend/js/fr/texts.js"></script>
<script src="/frontend/js/tile.js"></script>
<script type="module">
import { Game } from '/frontend/js/game.js';
window.addEventListener('DOMContentLoaded', () => {
// Gestion du bouton de démarrage
const startButton = document.getElementById('start-button');
const startScreen = document.getElementById('start-screen');
const gameCenter = document.getElementById('game-center');
startButton.addEventListener('click', () => {
startScreen.style.display = 'none';
gameCenter.style.display = 'flex';
// Instancier une nouvelle partie avec mains vides pour distribution animée
game = new Game(true);
// Lancer l'animation de distribution
distributeHands();
});
// Déclarer les variables et fonctions ici pour qu'elles soient accessibles partout
let game;
let isDistributing = false;
// Charger le menu
fetch('/components/menu.html')
.then(r => r.text())
.then(html => document.getElementById('menu').innerHTML = html);
// Afficher la mascotte et le texte
const mascotteEl = document.getElementById('mascotte');
mascotteEl.innerHTML = '<img src="/img/tilineau/idle.png" alt="Tilineau">';
document.getElementById('speech-text').innerHTML = texts.riichi.chap5;
// Toggle bulle de dialogue au clic sur la mascotte
const speechBubble = document.querySelector('.speech-bubble');
mascotteEl.addEventListener('click', () => {
speechBubble.classList.toggle('visible');
mascotteEl.innerHTML = speechBubble.classList.contains('visible')
? '<img class="explaining" src="/img/tilineau/explaining.png" alt="Tilineau">'
: '<img src="/img/tilineau/idle.png" alt="Tilineau">';
});
// Instancier une nouvelle partie et afficher les mains
async function distributeHands() {
isDistributing = true;
// Distribution 4 par 4, puis 1 par 1 comme dans la vraie vie
// 3 rounds de 4 tuiles par joueur = 12 tuiles
for (let round = 0; round < 3; round++) {
for (let player = 0; player < 4; player++) {
for (let i = 0; i < 4; i++) {
game.hands[player].tiles.push(game.wall.drawTile());
}
renderHands();
await new Promise(resolve => setTimeout(resolve, 300));
}
}
// Distribution finale : 1 par 1 pour la 13e tuile
for (let player = 0; player < 4; player++) {
game.hands[player].tiles.push(game.wall.drawTile());
renderHands();
await new Promise(resolve => setTimeout(resolve, 300));
}
// Trier les mains avec animation pour le joueur
const playerHandContainer = document.getElementById('hand-0').querySelector('.hand-tiles');
const playerTiles = game.hands[0].tiles;
const playerTileDivs = Array.from(playerHandContainer.children);
// Créer un map pour associer chaque tuile à son élément DOM
const tileToElement = new Map();
playerTileDivs.forEach((div, index) => {
tileToElement.set(playerTiles[index], div);
});
// Enregistrer les positions avant tri (en pixels relatifs au conteneur)
const originalPositions = new Map();
playerTileDivs.forEach((div, index) => {
originalPositions.set(div, index * (60 + 5)); // calcul basé sur width + gap
});
// Trier les mains
for (let hand of game.hands) {
hand.sort();
}
// Récupérer les nouvelles positions
const newPositions = new Map();
game.hands[0].tiles.forEach((tile, newIndex) => {
newPositions.set(tileToElement.get(tile), newIndex * (60 + 5));
});
// Appliquer une translation pour que les éléments restent à leur place visuelle
playerTileDivs.forEach(div => {
const originalX = originalPositions.get(div);
const newX = newPositions.get(div);
const deltaX = originalX - newX;
div.style.transform = `translateX(${deltaX}px)`;
div.style.transition = 'none';
});
// Forcer un reflow
void playerHandContainer.offsetWidth;
// Ré-ordonner les éléments dans le DOM selon le tri
const sortedDivs = game.hands[0].tiles.map(tile => tileToElement.get(tile));
sortedDivs.forEach(div => playerHandContainer.appendChild(div));
// Forcer un autre reflow
void playerHandContainer.offsetWidth;
// Animer vers les positions finales
playerTileDivs.forEach(div => {
div.style.transition = 'transform 0.6s ease-in-out';
div.style.transform = 'translateX(0)';
});
// Attendre la fin de l'animation
await new Promise(resolve => setTimeout(resolve, 600));
// Nettoyer les styles
playerTileDivs.forEach(div => {
div.style.transition = '';
div.style.transform = '';
});
// Re-rendre les bots sans animation
for (let i = 1; i < 4; i++) {
const botHandDiv = document.getElementById(`hand-${i}`);
const botHandTiles = document.createElement('div');
botHandTiles.className = 'hand-tiles';
for (let k = 0; k < game.hands[i].tiles.length; k++) {
const tileDiv = document.createElement('div');
tileDiv.innerHTML = `<svg class="tile tile-back" viewBox="0 0 310 410" height="82" width="60">
<image href="/img/Regular/Gray.svg" x="10" y="10" height="410" width="310"/>
<image href="/img/Regular/Back.svg" x="0" y="0" height="410" width="310"/>
</svg>`;
botHandTiles.appendChild(tileDiv);
}
const botHandWithDraw = botHandDiv.querySelector('.hand-with-draw') || botHandDiv;
const oldHandTiles = botHandWithDraw.querySelector('.hand-tiles');
if (oldHandTiles) oldHandTiles.remove();
botHandWithDraw.insertBefore(botHandTiles, botHandWithDraw.firstChild);
}
// Le joueur 0 pioche sa tuile supplémentaire
await new Promise(resolve => setTimeout(resolve, 500));
game.draw(game.turn);
renderHands();
updateTurnIndicator();
isDistributing = false;
}
function renderHands() {
for (let i = 0; i < 4; i++) {
const handDiv = document.getElementById(`hand-${i}`);
handDiv.innerHTML = '';
// Conteneur main + draw
const handWithDraw = document.createElement('div');
handWithDraw.className = 'hand-with-draw';
// Sous-div pour les tuiles de la main
const handTiles = document.createElement('div');
handTiles.className = 'hand-tiles';
for (let k = 0; k < game.hands[i].tiles.length; k++) {
const tile = game.hands[i].tiles[k];
const tileDiv = document.createElement('div');
// Ajouter la classe 'player-tile' uniquement pour le joueur
if (i === 0) {
tileDiv.className = 'player-tile';
tileDiv.innerHTML = tile.getSVG();
tileDiv.style.cursor = 'pointer';
const handleClick = (() => {
const tileToDiscard = tile;
return () => {
// Ne rien faire pendant la distribution
if (isDistributing) return;
// Trouver l'index actuel de la tuile
const currentIdx = game.hands[0].tiles.findIndex(t => t === tileToDiscard);
if (currentIdx !== -1) {
game.discard(0, currentIdx);
game.nextTurn();
updateTurnIndicator();
renderHands();
renderDiscards();
playBotsSequentially();
}
};
})();
tileDiv.addEventListener('click', handleClick);
} else {
// Bots : afficher le dos
tileDiv.innerHTML = `<svg class="tile tile-back" viewBox="0 0 310 410" height="82" width="60">
<image href="/img/Regular/Gray.svg" x="10" y="10" height="410" width="310"/>
<image href="/img/Regular/Back.svg" x="0" y="0" height="410" width="310"/>
</svg>`;
}
handTiles.appendChild(tileDiv);
}
handWithDraw.appendChild(handTiles);
// Afficher la tuile draw si elle existe
if (game.hands[i].drawn) {
const drawDiv = document.createElement('div');
drawDiv.className = 'draw-tile';
if (i === 0) {
drawDiv.classList.add('player-tile');
drawDiv.innerHTML = game.hands[i].drawn.getSVG();
drawDiv.style.cursor = 'pointer';
const drawnTile = game.hands[i].drawn;
const handleDrawnClick = (() => {
return () => {
// Défausser la tuile piochée (index === tiles.length)
game.discard(0, game.hands[0].tiles.length);
game.nextTurn();
updateTurnIndicator();
renderHands();
renderDiscards();
playBotsSequentially();
};
})();
drawDiv.addEventListener('click', handleDrawnClick);
} else {
drawDiv.innerHTML = `<svg class="tile tile-back" viewBox="0 0 310 410" height="82" width="60">
<image href="/img/Regular/Gray.svg" x="10" y="10" height="410" width="310"/>
<image href="/img/Regular/Back.svg" x="0" y="0" height="410" width="310"/>
</svg>`;
}
handWithDraw.appendChild(drawDiv);
}
handDiv.appendChild(handWithDraw);
}
}
function renderDiscards() {
for (let i = 0; i < 4; i++) {
const discardDiv = document.getElementById(`discard-${i}`);
discardDiv.innerHTML = '';
for (let tileIndex = 0; tileIndex < game.discards[i].tiles.length; tileIndex++) {
const tile = game.discards[i].tiles[tileIndex];
const tileDiv = document.createElement('div');
tileDiv.className = 'discard-tile';
tileDiv.innerHTML = tile.getSVG();
// Calculer la position en grille (6 tuiles par ligne)
const row = Math.floor(tileIndex / 6);
const col = tileIndex % 6;
const x = col * 50;
const y = row * 67;
tileDiv.style.position = 'absolute';
tileDiv.style.left = x + 'px';
tileDiv.style.top = y + 'px';
discardDiv.appendChild(tileDiv);
}
}
}
async function playBotsSequentially() {
// Les bots jouent après le joueur humain
while (game.turn !== 0) {
await new Promise(resolve => setTimeout(resolve, 500));
game.draw(game.turn);
renderHands();
// Pause aléatoire pour simuler la réflexion
const randomDelay = 1000 + Math.random() * 1000;
await new Promise(resolve => setTimeout(resolve, randomDelay));
game.botPlay();
game.nextTurn();
updateTurnIndicator();
renderHands();
renderDiscards();
}
game.draw(0);
renderHands();
renderDiscards();
}
function updateTurnIndicator() {
for (let i = 0; i < 4; i++) {
const indicator = document.getElementById(`turn-indicator-${i}`);
if (i === game.turn) {
indicator.style.display = 'block';
} else {
indicator.style.display = 'none';
}
}
}
renderHands();
updateTurnIndicator();
});
</script>
</body>
</html>