1184 lines
No EOL
38 KiB
HTML
1184 lines
No EOL
38 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: -100px;
|
|
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: relative;
|
|
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-groups {
|
|
display: block;
|
|
position: absolute;
|
|
pointer-events: none;
|
|
height: 50px;
|
|
}
|
|
.hand-group {
|
|
display: flex;
|
|
flex-direction: row;
|
|
gap: 2px;
|
|
align-items: center;
|
|
position: absolute;
|
|
top: 0;
|
|
margin-right: 20px;
|
|
z-index: 10;
|
|
}
|
|
.hand-group:nth-child(1) { z-index: 11; }
|
|
.hand-group:nth-child(2) { z-index: 10; }
|
|
.hand-group:nth-child(3) { z-index: 9; }
|
|
.hand-group:nth-child(4) { z-index: 8; }
|
|
.hand-group:nth-child(5) { z-index: 7; }
|
|
.hand-group svg {
|
|
height: 40px;
|
|
width: 30px;
|
|
}
|
|
.stolen-tile {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 30px;
|
|
margin: 0;
|
|
flex-shrink: 0;
|
|
}
|
|
.stolen-tile svg {
|
|
height: 40px;
|
|
width: 30px;
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
/* Augmenter l'écart quand la tuile penchée est à une extrémité */
|
|
.hand-group.stolen-left .stolen-tile {
|
|
margin-right: 5px;
|
|
}
|
|
|
|
.hand-group.stolen-right .stolen-tile {
|
|
margin-left: 5px;
|
|
}
|
|
|
|
/* Gap différent si la tuile volée est au centre */
|
|
.hand-group.stolen-middle {
|
|
gap: 8px;
|
|
}
|
|
|
|
#groups-1 .stolen-tile svg {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
#groups-3 .stolen-tile svg {
|
|
transform: rotate(-90deg);
|
|
}
|
|
|
|
#groups-0 {
|
|
bottom: -7%;
|
|
right: 10%;
|
|
transform: translateX(50%);
|
|
}
|
|
|
|
#groups-1 {
|
|
top: 28%;
|
|
right: 12%;
|
|
transform: translateX(50%) translateY(-50%) rotate(90deg) scaleX(-1) rotateZ(180deg);
|
|
transform-origin: center;
|
|
}
|
|
|
|
#groups-2 {
|
|
top: 27%;
|
|
left: 11%;
|
|
transform: translateX(-50%) rotate(180deg);
|
|
}
|
|
|
|
#groups-3 {
|
|
top: 106%;
|
|
right: 88%;
|
|
transform: translateX(50%) translateY(-50%) rotate(-90deg) scaleX(-1) rotateZ(180deg);
|
|
transform-origin: center;
|
|
}
|
|
#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;
|
|
}
|
|
|
|
#call-panel {
|
|
position: absolute;
|
|
transform: translate(-50%, 50%);
|
|
bottom: -3%;
|
|
left: 78%;
|
|
display: none;
|
|
gap: 10px;
|
|
z-index: 100;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.call-button {
|
|
padding: 16px 32px;
|
|
font-size: 1.3em;
|
|
font-weight: bold;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
color: white;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.call-button:hover {
|
|
transform: scale(1.05);
|
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.call-button:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
/* Code couleur pour les 6 appels */
|
|
#pon-button {
|
|
background-color: #FF6B6B; /* Rouge */
|
|
}
|
|
|
|
#chii-button {
|
|
background-color: #4ECDC4; /* Turquoise */
|
|
}
|
|
|
|
#kan-button {
|
|
background-color: #95E1D3; /* Menthe */
|
|
}
|
|
|
|
#tsumo-button {
|
|
background-color: #FFD700; /* Or */
|
|
color: #333;
|
|
}
|
|
|
|
#ron-button {
|
|
background-color: #9B59B6; /* Violet */
|
|
}
|
|
|
|
#skip-button {
|
|
background-color: #808080; /* Gris */
|
|
}
|
|
|
|
@keyframes ponAnimation {
|
|
0% {
|
|
opacity: 0;
|
|
transform: translateX(30px) scale(0.3) rotate(-15deg);
|
|
}
|
|
10% {
|
|
opacity: 1;
|
|
transform: translateX(0) scale(1.2) rotate(0deg);
|
|
}
|
|
20% {
|
|
opacity: 1;
|
|
transform: translateX(0) scale(1) rotate(0deg);
|
|
}
|
|
70% {
|
|
opacity: 1;
|
|
transform: translateX(0) scale(1) rotate(0deg);
|
|
}
|
|
85% {
|
|
opacity: 1;
|
|
transform: translateX(0) scale(1) rotate(0deg);
|
|
}
|
|
100% {
|
|
opacity: 0;
|
|
transform: translateX(-60px) scale(0.5) rotate(20deg);
|
|
}
|
|
}
|
|
|
|
.pon-text {
|
|
position: fixed;
|
|
font-size: 3.5em;
|
|
font-weight: 900;
|
|
color: #CC3333;
|
|
z-index: 999;
|
|
animation: ponAnimation 1.2s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards;
|
|
pointer-events: none;
|
|
text-shadow: 0 0 15px rgba(204, 51, 51, 0.7), 0 0 30px rgba(204, 51, 51, 0.3);
|
|
transform: translate(-50%, -50%);
|
|
white-space: nowrap;
|
|
}
|
|
</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 id="call-panel">
|
|
<button id="pon-button" class="call-button">Pon</button>
|
|
<button id="chii-button" class="call-button" style="display: none;">Chii</button>
|
|
<button id="kan-button" class="call-button" style="display: none;">Kan</button>
|
|
<button id="tsumo-button" class="call-button" style="display: none;">Tsumo</button>
|
|
<button id="ron-button" class="call-button" style="display: none;">Ron</button>
|
|
<button id="skip-button" class="call-button">Skip</button>
|
|
</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;
|
|
let canPlayerAct = false;
|
|
let lastDiscarder = -1;
|
|
// 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;
|
|
canPlayerAct = true;
|
|
}
|
|
|
|
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 ou si ce n'est pas son tour
|
|
if (isDistributing || !canPlayerAct) return;
|
|
|
|
// Trouver l'index actuel de la tuile
|
|
const currentIdx = game.hands[0].tiles.findIndex(t => t === tileToDiscard);
|
|
if (currentIdx !== -1) {
|
|
canPlayerAct = false;
|
|
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 () => {
|
|
// Ne rien faire si ce n'est pas son tour ou pendant la distribution
|
|
if (isDistributing || !canPlayerAct) return;
|
|
|
|
// Défausser la tuile piochée (index === tiles.length)
|
|
canPlayerAct = false;
|
|
lastDiscarder = 0;
|
|
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);
|
|
}
|
|
renderGroups();
|
|
}
|
|
|
|
function renderGroups() {
|
|
const gameDiv = document.getElementById('game');
|
|
|
|
// Créer les conteneurs de groupes pour chaque joueur s'ils n'existent pas
|
|
for (let i = 0; i < 4; i++) {
|
|
if (!document.getElementById(`groups-${i}`)) {
|
|
const groupsContainer = document.createElement('div');
|
|
groupsContainer.id = `groups-${i}`;
|
|
groupsContainer.className = 'hand-groups';
|
|
gameDiv.appendChild(groupsContainer);
|
|
}
|
|
}
|
|
|
|
// Mettre à jour les groupes pour chaque joueur
|
|
for (let i = 0; i < 4; i++) {
|
|
const groupsContainer = document.getElementById(`groups-${i}`);
|
|
const currentGroupDivs = Array.from(groupsContainer.children);
|
|
|
|
// Ajouter les nouveaux groupes
|
|
for (let groupIndex = game.hands[i].groups.length - 1; groupIndex >= 0; groupIndex--) {
|
|
if (groupIndex >= currentGroupDivs.length) {
|
|
// Nouveau groupe à ajouter
|
|
const group = game.hands[i].groups[groupIndex];
|
|
const groupDiv = createGroupDiv(group);
|
|
// Ajouter au début du conteneur (à gauche)
|
|
groupsContainer.insertBefore(groupDiv, groupsContainer.firstChild);
|
|
}
|
|
}
|
|
|
|
// Recalculer toutes les positions
|
|
const allGroupDivs = Array.from(groupsContainer.children);
|
|
|
|
if (i === 0 || i === 2) {
|
|
// Joueurs 0 et 2 : ancrage sur right, positions inversées
|
|
let rightOffset = 0;
|
|
const groupsToPosition = allGroupDivs.slice().reverse();
|
|
|
|
for (let j = 0; j < groupsToPosition.length; j++) {
|
|
const groupDiv = groupsToPosition[j];
|
|
const groupWidth = (groupDiv.children.length) * 30;
|
|
|
|
groupDiv.style.right = rightOffset + 'px';
|
|
groupDiv.style.left = 'auto';
|
|
|
|
rightOffset += groupWidth + 12; // +12 pour le gap entre les groupes
|
|
}
|
|
} else {
|
|
// Bots 1 et 3 : ancrage sur left, ordre inversé
|
|
let offset = 0;
|
|
const groupsToPosition = allGroupDivs.slice().reverse();
|
|
|
|
for (let j = 0; j < groupsToPosition.length; j++) {
|
|
const groupDiv = groupsToPosition[j];
|
|
const groupWidth = (groupDiv.children.length) * 30;
|
|
|
|
groupDiv.style.left = offset + 'px';
|
|
groupDiv.style.right = 'auto';
|
|
|
|
offset += groupWidth + 12; // +12 pour le gap entre les groupes
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function createGroupDiv(group) {
|
|
const groupDiv = document.createElement('div');
|
|
groupDiv.className = 'hand-group';
|
|
|
|
// Pour un Pon, placer la tuile volée selon la direction (dir: 1=gauche, 2=milieu, 3=droite)
|
|
if (group.stollenTile && group.dir) {
|
|
if (group.dir === 1) {
|
|
// Tuile volée à gauche
|
|
groupDiv.classList.add('stolen-left');
|
|
const stolenDiv = document.createElement('div');
|
|
stolenDiv.innerHTML = group.stollenTile.getSVG();
|
|
stolenDiv.className = 'stolen-tile';
|
|
groupDiv.appendChild(stolenDiv);
|
|
|
|
for (let j = 0; j < group.tiles.length; j++) {
|
|
const tileDiv = document.createElement('div');
|
|
tileDiv.innerHTML = group.tiles[j].getSVG();
|
|
// Ajouter la classe middle-tile à la tuile du milieu
|
|
if (j === Math.floor(group.tiles.length / 2)) {
|
|
tileDiv.classList.add('middle-tile');
|
|
}
|
|
groupDiv.appendChild(tileDiv);
|
|
}
|
|
} else if (group.dir === 2) {
|
|
// Tuile volée au milieu
|
|
groupDiv.classList.add('stolen-middle');
|
|
for (let j = 0; j < group.tiles.length; j++) {
|
|
if (j === Math.floor(group.tiles.length / 2)) {
|
|
const stolenDiv = document.createElement('div');
|
|
stolenDiv.innerHTML = group.stollenTile.getSVG();
|
|
stolenDiv.className = 'stolen-tile stolen-middle';
|
|
groupDiv.appendChild(stolenDiv);
|
|
}
|
|
const tileDiv = document.createElement('div');
|
|
tileDiv.innerHTML = group.tiles[j].getSVG();
|
|
groupDiv.appendChild(tileDiv);
|
|
}
|
|
} else if (group.dir === 3) {
|
|
// Tuile volée à droite
|
|
groupDiv.classList.add('stolen-right');
|
|
for (let j = 0; j < group.tiles.length; j++) {
|
|
const tileDiv = document.createElement('div');
|
|
tileDiv.innerHTML = group.tiles[j].getSVG();
|
|
// Ajouter la classe middle-tile à la tuile du milieu
|
|
if (j === Math.floor(group.tiles.length / 2)) {
|
|
tileDiv.classList.add('middle-tile');
|
|
}
|
|
groupDiv.appendChild(tileDiv);
|
|
}
|
|
|
|
const stolenDiv = document.createElement('div');
|
|
stolenDiv.innerHTML = group.stollenTile.getSVG();
|
|
stolenDiv.className = 'stolen-tile';
|
|
groupDiv.appendChild(stolenDiv);
|
|
}
|
|
} else {
|
|
// Groupe fermé ou non volé : afficher les tuiles normalement
|
|
for (let tile of group.tiles) {
|
|
const tileDiv = document.createElement('div');
|
|
tileDiv.innerHTML = tile.getSVG();
|
|
groupDiv.appendChild(tileDiv);
|
|
}
|
|
}
|
|
|
|
return groupDiv;
|
|
}
|
|
|
|
function renderAll() {
|
|
renderHands();
|
|
renderGroups();
|
|
renderDiscards();
|
|
}
|
|
|
|
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 showPonAnimation(player) {
|
|
// Obtenir la position du joueur
|
|
const handDiv = document.getElementById(`hand-${player}`);
|
|
const rect = handDiv.getBoundingClientRect();
|
|
|
|
// Positions spécifiques pour chaque joueur
|
|
const positionOffsets = {
|
|
0: { left: rect.left + rect.width / 2 - 50, top: rect.top - 100 },
|
|
1: { left: rect.left + rect.width - 250, top: rect.top + rect.height / 2 - 50},
|
|
2: { left: rect.left + rect.width / 2 - 75, top: rect.top + rect.height + 30 },
|
|
3: { left: rect.left +150, top: rect.top + rect.height / 2 - 50 }
|
|
};
|
|
|
|
const pos = positionOffsets[player] || { left: rect.left + rect.width / 2, top: rect.top - 50 };
|
|
|
|
// Créer l'élément de texte "Pon"
|
|
const ponText = document.createElement('div');
|
|
ponText.className = 'pon-text';
|
|
ponText.textContent = 'Pon';
|
|
ponText.style.left = pos.left + 'px';
|
|
ponText.style.top = pos.top + 'px';
|
|
|
|
document.body.appendChild(ponText);
|
|
|
|
// Attendre la fin de l'animation
|
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
|
|
// Supprimer l'élément
|
|
ponText.remove();
|
|
}
|
|
|
|
async function continueBotPlay() {
|
|
// Continue la boucle des bots après une réponse du joueur
|
|
canPlayerAct = false;
|
|
|
|
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));
|
|
|
|
// Défausser le bot
|
|
game.botPlay();
|
|
renderDiscards();
|
|
|
|
// Vérifier si d'autres bots peuvent faire un pon (mais pas sur leur propre défausse)
|
|
let ponMade = false;
|
|
for (let player = 1; player < 4; player++) {
|
|
if (player !== game.turn && game.canPon(player)) {
|
|
// Ce bot fait un pon
|
|
const lastTile = game.discards[game.turn].tiles[game.discards[game.turn].tiles.length - 1];
|
|
const tiles = game.hands[player].tiles.filter(t => t.equals(lastTile)).slice(0, 2);
|
|
game.pon(player, tiles, (player - game.turn) % 4);
|
|
game.discards[game.turn].beeingStollen();
|
|
await showPonAnimation(player);
|
|
// Après un Pon, le tour passe au joueur qui appelle le Pon
|
|
game.turn = player;
|
|
updateTurnIndicator();
|
|
renderHands();
|
|
renderDiscards();
|
|
ponMade = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ponMade) continue;
|
|
|
|
// Vérifier si le joueur peut faire un appel
|
|
if (game.canPon(0)) {
|
|
// Le joueur peut faire un pon, lui proposer
|
|
canPlayerAct = true;
|
|
showCallPanelForPon();
|
|
return; // Attendre la réponse du joueur
|
|
}
|
|
|
|
// Sinon, passer au joueur suivant
|
|
game.nextTurn();
|
|
updateTurnIndicator();
|
|
renderHands();
|
|
}
|
|
|
|
// C'est au tour du joueur - il pioche d'abord
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
game.draw(0);
|
|
renderHands();
|
|
canPlayerAct = true;
|
|
}
|
|
|
|
async function playBotsSequentially() {
|
|
// Le joueur ne peut plus agir pendant que les bots jouent
|
|
canPlayerAct = false;
|
|
|
|
// 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));
|
|
|
|
// Défausser le bot
|
|
game.botPlay();
|
|
renderDiscards();
|
|
|
|
// Vérifier si d'autres bots peuvent faire un pon (mais pas sur leur propre défausse)
|
|
let ponMade = false;
|
|
for (let player = 1; player < 4; player++) {
|
|
if (player !== game.turn && game.canPon(player)) {
|
|
// Ce bot fait un pon
|
|
const lastTile = game.discards[game.turn].tiles[game.discards[game.turn].tiles.length - 1];
|
|
const tiles = game.hands[player].tiles.filter(t => t.equals(lastTile)).slice(0, 2);
|
|
game.pon(player, tiles, (player - game.turn) % 4);
|
|
game.discards[game.turn].beeingStollen();
|
|
await showPonAnimation(player);
|
|
// Après un Pon, le tour passe au joueur qui appelle le Pon
|
|
game.turn = player;
|
|
updateTurnIndicator();
|
|
renderHands();
|
|
renderDiscards();
|
|
ponMade = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ponMade) continue;
|
|
|
|
// Vérifier si le joueur peut faire un appel
|
|
if (game.canPon(0)) {
|
|
// Le joueur peut faire un pon, lui proposer
|
|
canPlayerAct = true;
|
|
showCallPanelForPon();
|
|
return; // Attendre la réponse du joueur
|
|
}
|
|
|
|
// Sinon, passer au joueur suivant
|
|
game.nextTurn();
|
|
updateTurnIndicator();
|
|
renderHands();
|
|
}
|
|
|
|
// C'est au tour du joueur - il pioche d'abord
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
game.draw(0);
|
|
renderHands();
|
|
canPlayerAct = true;
|
|
}
|
|
|
|
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';
|
|
}
|
|
}
|
|
}
|
|
|
|
function hideCallPanel() {
|
|
document.getElementById('call-panel').style.display = 'none';
|
|
}
|
|
|
|
function showCallPanelForPon() {
|
|
const callPanel = document.getElementById('call-panel');
|
|
|
|
// Afficher seulement Pon et Skip
|
|
document.getElementById('pon-button').style.display = 'block';
|
|
document.getElementById('chii-button').style.display = 'none';
|
|
document.getElementById('kan-button').style.display = 'none';
|
|
document.getElementById('tsumo-button').style.display = 'none';
|
|
document.getElementById('ron-button').style.display = 'none';
|
|
|
|
callPanel.style.display = 'flex';
|
|
}
|
|
|
|
function showCallPanel() {
|
|
const callPanel = document.getElementById('call-panel');
|
|
|
|
// Ne pas afficher les appels si c'est le joueur qui vient de défausser
|
|
if (lastDiscarder === 0) {
|
|
callPanel.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
// Le joueur peut agir
|
|
canPlayerAct = true;
|
|
|
|
// Vérifier les appels possibles pour le joueur
|
|
const canPon = game.canPon(0);
|
|
|
|
// Pour l'instant, on implémente seulement Pon
|
|
document.getElementById('pon-button').style.display = canPon ? 'block' : 'none';
|
|
|
|
// Cacher les autres appels pour l'instant
|
|
document.getElementById('chii-button').style.display = 'none';
|
|
document.getElementById('kan-button').style.display = 'none';
|
|
document.getElementById('tsumo-button').style.display = 'none';
|
|
document.getElementById('ron-button').style.display = 'none';
|
|
|
|
// Afficher le panel si au moins un appel est possible
|
|
if (canPon) {
|
|
callPanel.style.display = 'flex';
|
|
} else {
|
|
callPanel.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Gestionnaires des boutons d'appels
|
|
document.getElementById('pon-button').addEventListener('click', async () => {
|
|
if (!canPlayerAct) return;
|
|
canPlayerAct = false;
|
|
const lastTile = game.discards[game.turn].tiles[game.discards[game.turn].tiles.length - 1];
|
|
const tiles = game.hands[0].tiles.filter(t => t.equals(lastTile)).slice(0, 2);
|
|
game.pon(0, tiles, (0 - game.turn) % 4);
|
|
game.discards[game.turn].beeingStollen();
|
|
await showPonAnimation(0);
|
|
// Après un Pon, le tour passe au joueur qui appelle le Pon
|
|
game.turn = 0;
|
|
hideCallPanel();
|
|
updateTurnIndicator();
|
|
renderHands();
|
|
renderDiscards();
|
|
continueBotPlay();
|
|
});
|
|
|
|
document.getElementById('skip-button').addEventListener('click', () => {
|
|
if (!canPlayerAct) return;
|
|
canPlayerAct = false;
|
|
hideCallPanel();
|
|
continueBotPlay();
|
|
});
|
|
|
|
// Ne pas rendre les mains si le jeu n'a pas commencé
|
|
if (game) {
|
|
renderHands();
|
|
updateTurnIndicator();
|
|
}
|
|
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |