Retrorings / index.html
Melfi's picture
Add 1 files
669f2bb verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Waterful Ring Toss</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.4/gsap.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap');
:root {
--water-color: rgba(64, 164, 223, 0.7);
--container-color: rgba(255, 255, 255, 0.15);
--container-border: 3px solid rgba(255, 255, 255, 0.3);
}
body {
font-family: 'Poppins', sans-serif;
overflow: hidden;
touch-action: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
}
#gameView {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
.water-layer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 45%;
background: var(--water-color);
z-index: 5;
border-top-left-radius: 50% 30%;
border-top-right-radius: 50% 30%;
box-shadow: inset 0 -15px 30px rgba(0, 119, 182, 0.5);
pointer-events: none;
overflow: hidden;
}
.water-surface {
position: absolute;
top: 0;
left: 0;
width: 200%;
height: 100px;
background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.3) 50%, rgba(255,255,255,0) 100%);
animation: waterFlow 8s linear infinite;
pointer-events: none;
}
@keyframes waterFlow {
0% { transform: translateX(-50%) rotate(0.5deg); }
100% { transform: translateX(0%) rotate(-0.5deg); }
}
.water-ripple {
position: absolute;
width: 100px;
height: 20px;
background: radial-gradient(ellipse at center, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0) 70%);
border-radius: 50%;
pointer-events: none;
}
.container {
position: absolute;
bottom: 10%;
left: 50%;
width: 80%;
height: 60%;
transform: translateX(-50%);
background: var(--container-color);
border: var(--container-border);
border-bottom: none;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
backdrop-filter: blur(5px);
z-index: 10;
overflow: hidden;
}
.container-glass {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(ellipse at top, rgba(255,255,255,0.3) 0%, transparent 70%);
pointer-events: none;
z-index: 11;
}
.ring {
width: 40px;
height: 40px;
border-radius: 50%;
position: absolute;
z-index: 20;
border: 3px solid;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
transition: transform 0.2s, box-shadow 0.2s;
pointer-events: all;
}
.ring:hover {
transform: scale(1.05);
box-shadow: 0 6px 15px rgba(0,0,0,0.4);
}
.active-ring {
border: 3px dashed;
opacity: 0.8;
z-index: 25;
}
.peg {
position: absolute;
width: 14px;
height: 14px;
background: #FF5722;
border-radius: 50%;
z-index: 15;
box-shadow: 0 2px 5px rgba(0,0,0,0.3), inset 0 -2px 3px rgba(0,0,0,0.2);
border: 2px solid #E64A19;
}
.bubble {
position: absolute;
width: 20px;
height: 20px;
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0.4) 60%);
border-radius: 50%;
z-index: 6;
pointer-events: none;
box-shadow: 0 0 10px rgba(255,255,255,0.4);
}
@keyframes bubbleFloat {
0% { transform: translateY(0) scale(1); opacity: 0.8; }
100% { transform: translateY(-150px) scale(1.5); opacity: 0; }
}
.bubble-float {
animation: bubbleFloat 2s ease-out forwards;
}
.bubble-btn {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 60px;
background: radial-gradient(circle, #4CAF50, #2E7D32);
border-radius: 50%;
border: 3px solid #1B5E20;
box-shadow: 0 4px 10px rgba(0,0,0,0.3), inset 0 -4px 10px rgba(0,0,0,0.2);
color: white;
font-size: 12px;
font-weight: bold;
text-align: center;
line-height: 60px;
cursor: pointer;
z-index: 50;
transition: all 0.2s;
overflow: hidden;
}
.bubble-btn::after {
content: '';
position: absolute;
top: -10px;
left: -10px;
right: -10px;
bottom: -10px;
background: radial-gradient(circle, rgba(76, 175, 80, 0.5), transparent);
border-radius: 50%;
opacity: 0;
pointer-events: none;
}
.bubble-btn:active {
transform: translateX(-50%) scale(0.95);
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
.bubble-btn:active::after {
animation: ripple 0.6s ease-out;
}
@keyframes ripple {
0% { transform: scale(0.3); opacity: 1; }
100% { transform: scale(2); opacity: 0; }
}
.screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
transition: opacity 0.3s;
opacity: 1;
pointer-events: all;
}
.screen.hidden {
opacity: 0;
pointer-events: none;
}
.screen-title {
font-size: 3rem;
font-weight: bold;
margin-bottom: 2rem;
text-shadow: 0 4px 8px rgba(0,0,0,0.5);
background: linear-gradient(45deg, #4FC3F7, #2196F3);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.screen-content {
background: rgba(30, 30, 40, 0.9);
padding: 2rem;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
}
.btn {
background: linear-gradient(145deg, #4FC3F7, #29B6F6);
border: none;
color: white;
padding: 12px 24px;
border-radius: 30px;
font-weight: bold;
cursor: pointer;
margin: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
transition: all 0.3s;
text-align: center;
min-width: 200px;
}
.btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0,0,0,0.3);
}
.btn:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.btn-secondary {
background: linear-gradient(145deg, #666, #444);
}
.btn-challenge {
background: linear-gradient(145deg, #FFA726, #FB8C00);
}
.game-display {
position: fixed;
background: rgba(0,0,0,0.7);
color: white;
padding: 10px 15px;
border-radius: 10px;
z-index: 100;
font-size: 1rem;
}
#scoreDisplay {
top: 20px;
right: 20px;
}
#timerDisplay {
top: 20px;
left: 20px;
}
#ringCounter {
bottom: 100px;
right: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.ring-indicator {
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid;
margin: 5px;
}
.settings-group {
margin-bottom: 1.5rem;
}
.settings-label {
display: block;
margin-bottom: 0.5rem;
color: #BBBBBB;
}
.slider-container {
display: flex;
align-items: center;
gap: 10px;
}
.slider {
-webkit-appearance: none;
width: 100%;
height: 8px;
border-radius: 4px;
background: #444;
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #4FC3F7;
cursor: pointer;
}
.settings-option {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #444;
}
.lang-option {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
}
.lang-flag {
width: 24px;
height: 16px;
margin-right: 10px;
border-radius: 2px;
}
.difficulty-option {
display: flex;
justify-content: space-between;
margin-top: 0.5rem;
}
.difficulty-btn {
padding: 8px 16px;
margin: 0;
min-width: auto;
background: #444;
}
.difficulty-btn.active {
background: linear-gradient(145deg, #4FC3F7, #29B6F6);
}
.credits-content {
text-align: center;
line-height: 1.8;
}
.credits-name {
font-size: 1.5rem;
margin-bottom: 1rem;
color: #4FC3F7;
}
.credits-title {
color: #FFA726;
margin: 1rem 0;
}
.credits-text {
color: #BBBBBB;
}
.game-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(0,0,0,0.3), rgba(0,0,0,0.1));
z-index: 500;
display: none;
}
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(30, 30, 40, 0.95);
padding: 2rem;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.8);
max-width: 80%;
max-height: 80vh;
overflow-y: auto;
z-index: 501;
display: none;
}
.modal-title {
font-size: 2rem;
font-weight: bold;
margin-bottom: 1.5rem;
text-align: center;
color: #4FC3F7;
}
.modal-text {
margin-bottom: 2rem;
line-height: 1.6;
}
.modal-buttons {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 1.5rem;
}
.modal-btn {
min-width: 120px;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.screen-title {
font-size: 2rem;
margin-bottom: 1.5rem;
}
.screen-content {
padding: 1.5rem;
}
.btn {
min-width: 160px;
padding: 10px 20px;
margin: 8px;
}
.container {
width: 90%;
height: 55%;
}
}
@media (max-width: 480px) {
.screen-title {
font-size: 1.8rem;
margin-bottom: 1rem;
}
.screen-content {
padding: 1rem;
}
.btn {
min-width: 140px;
padding: 8px 16px;
margin: 5px;
font-size: 0.9rem;
}
#scoreDisplay, #timerDisplay {
font-size: 0.9rem;
padding: 8px 12px;
}
.container {
width: 95%;
height: 50%;
}
}
</style>
</head>
<body>
<!-- Game View -->
<div id="gameView">
<!-- Water Background -->
<div class="water-layer">
<div class="water-surface"></div>
</div>
<!-- Game Container -->
<div class="container">
<div class="container-glass"></div>
</div>
<!-- Game UI Elements -->
<div id="scoreDisplay" class="game-display">
Score: <span id="scoreValue">0</span>
</div>
<div id="timerDisplay" class="game-display">
Time: <span id="timeValue">60</span>s
</div>
<div id="ringCounter" class="game-display">
<div>Rings Left:</div>
<div id="ringIndicators" class="flex"></div>
</div>
<div id="bubbleBtn" class="bubble-btn">Bubble</div>
</div>
<!-- Screens -->
<div id="startScreen" class="screen">
<div class="screen-content text-center">
<h2 class="screen-title">Waterful Ring Toss</h2>
<button id="startBtn" class="btn">Play Game</button>
<button id="optionsBtn" class="btn btn-secondary">Options</button>
<button id="creditsBtn" class="btn btn-secondary">Credits</button>
</div>
</div>
<div id="modeScreen" class="screen hidden">
<div class="screen-content text-center">
<h2 class="screen-title">Select Mode</h2>
<button id="classicModeBtn" class="btn">Classic Mode</button>
<button id="challengeModeBtn" class="btn btn-challenge">Challenge Mode</button>
<button id="backToStartBtn" class="btn btn-secondary">Back</button>
</div>
</div>
<div id="optionsScreen" class="screen hidden">
<div class="screen-content">
<h2 class="screen-title mb-6">Options</h2>
<div class="settings-group">
<div class="settings-option">
<label class="settings-label">Music Volume</label>
<div class="slider-container">
<input type="range" min="0" max="100" value="50" class="slider" id="musicSlider">
<span id="musicValue">50%</span>
</div>
</div>
<div class="settings-option">
<label class="settings-label">Sound Effects</label>
<div class="slider-container">
<input type="range" min="0" max="100" value="70" class="slider" id="sfxSlider">
<span id="sfxValue">70%</span>
</div>
</div>
<div class="settings-option">
<label class="settings-label">Language</label>
<div class="lang-option">
<img src="https://flagcdn.com/w20/us.png" class="lang-flag" alt="English">
<span>English (US)</span>
</div>
<div class="lang-option">
<img src="https://flagcdn.com/w20/es.png" class="lang-flag" alt="Spanish">
<span>Español</span>
</div>
<div class="lang-option">
<img src="https://flagcdn.com/w20/fr.png" class="lang-flag" alt="French">
<span>Français</span>
</div>
</div>
<div class="settings-option">
<label class="settings-label">Game Difficulty</label>
<div class="difficulty-option">
<button class="difficulty-btn btn-secondary active">Easy</button>
<button class="difficulty-btn btn-secondary">Medium</button>
<button class="difficulty-btn btn-secondary">Hard</button>
</div>
</div>
</div>
<div class="text-center mt-6">
<button id="saveOptionsBtn" class="btn">Save Settings</button>
<button id="cancelOptionsBtn" class="btn btn-secondary">Cancel</button>
</div>
</div>
</div>
<div id="creditsScreen" class="screen hidden">
<div class="screen-content">
<h2 class="screen-title">Credits</h2>
<div class="credits-content">
<div class="credits-name">WATERFUL RING TOSS</div>
<div class="credits-text">A digital recreation of the classic arcade game</div>
<div class="credits-title">Developed By</div>
<div class="credits-name">Your Name Here</div>
<div class="credits-text">© 2023 All Rights Reserved</div>
<div class="credits-title">Inspired By</div>
<div class="credits-text">Tomy's Original Waterful Ring Toss Game</div>
<div class="credits-title">Special Thanks</div>
<div class="credits-text">To everyone who enjoys classic arcade games!</div>
</div>
<div class="text-center mt-6">
<button id="backFromCreditsBtn" class="btn">Back</button>
</div>
</div>
</div>
<!-- Game Overlay and Modals -->
<div id="gameOverlay" class="game-overlay"></div>
<div id="resultModal" class="modal">
<h3 class="modal-title">Success!</h3>
<p class="modal-text" id="resultText">You landed the ring on a peg!</p>
<div class="modal-buttons">
<button id="continueBtn" class="btn modal-btn">Continue</button>
</div>
</div>
<div id="pauseModal" class="modal">
<h3 class="modal-title">Game Paused</h3>
<div class="modal-buttons">
<button id="resumeBtn" class="btn modal-btn">Resume</button>
<button id="quitBtn" class="btn btn-secondary modal-btn">Quit</button>
</div>
</div>
<div id="gameOverModal" class="modal">
<h3 class="modal-title">Game Over</h3>
<p class="modal-text" id="finalScoreText">Your final score: <span id="finalScoreValue">0</span></p>
<div class="modal-buttons">
<button id="playAgainBtn" class="btn modal-btn">Play Again</button>
<button id="mainMenuBtn" class="btn btn-secondary modal-btn">Main Menu</button>
</div>
</div>
<div id="countdownDisplay" class="fixed inset-0 flex items-center justify-center text-white text-9xl font-bold text-shadow-lg z-1000 hidden">
3
</div>
<!-- Game Elements (dynamically added) -->
<div id="gameElements"></div>
<script>
// Game Model - Manages data and game logic
class GameModel {
constructor() {
this.mode = null; // 'classic' or 'challenge'
this.score = 0;
this.timeLeft = 60;
this.ringsLeft = 0;
this.difficulty = 'easy';
this.musicVolume = 50;
this.sfxVolume = 70;
this.language = 'en';
this.isPaused = false;
this.gameActive = false;
this.pegs = [];
this.rings = [];
this.activeRing = null;
this.physicsEngine = null;
}
setMode(mode) {
this.mode = mode;
if (mode === 'classic') {
this.ringsLeft = 10;
} else {
this.ringsLeft = 15;
this.timeLeft = 60;
}
this.score = 0;
}
updateScore(points) {
this.score += points;
return this.score;
}
decrementTime() {
if (this.timeLeft > 0 && !this.isPaused) {
this.timeLeft--;
}
return this.timeLeft;
}
decrementRings() {
if (this.ringsLeft > 0) {
this.ringsLeft--;
}
return this.ringsLeft;
}
addTime(seconds) {
this.timeLeft += seconds;
return this.timeLeft;
}
togglePause() {
this.isPaused = !this.isPaused;
return this.isPaused;
}
resetGame() {
this.score = 0;
if (this.mode === 'classic') {
this.ringsLeft = 10;
} else {
this.ringsLeft = 15;
this.timeLeft = 60;
}
this.isPaused = false;
}
}
// Game View - Handles rendering and UI updates
class GameView {
constructor() {
// DOM elements
this.startScreen = document.getElementById('startScreen');
this.modeScreen = document.getElementById('modeScreen');
this.optionsScreen = document.getElementById('optionsScreen');
this.creditsScreen = document.getElementById('creditsScreen');
this.scoreDisplay = document.getElementById('scoreDisplay');
this.scoreValue = document.getElementById('scoreValue');
this.timeDisplay = document.getElementById('timerDisplay');
this.timeValue = document.getElementById('timeValue');
this.ringIndicators = document.getElementById('ringIndicators');
this.bubbleBtn = document.getElementById('bubbleBtn');
this.gameView = document.getElementById('gameView');
this.container = document.querySelector('.container');
this.gameElements = document.getElementById('gameElements');
this.countdownDisplay = document.getElementById('countdownDisplay');
// Buttons
this.startBtn = document.getElementById('startBtn');
this.optionsBtn = document.getElementById('optionsBtn');
this.creditsBtn = document.getElementById('creditsBtn');
this.classicModeBtn = document.getElementById('classicModeBtn');
this.challengeModeBtn = document.getElementById('challengeModeBtn');
this.backToStartBtn = document.getElementById('backToStartBtn');
this.saveOptionsBtn = document.getElementById('saveOptionsBtn');
this.cancelOptionsBtn = document.getElementById('cancelOptionsBtn');
this.backFromCreditsBtn = document.getElementById('backFromCreditsBtn');
// Modals
this.gameOverlay = document.getElementById('gameOverlay');
this.resultModal = document.getElementById('resultModal');
this.resultText = document.getElementById('resultText');
this.continueBtn = document.getElementById('continueBtn');
this.pauseModal = document.getElementById('pauseModal');
this.resumeBtn = document.getElementById('resumeBtn');
this.quitBtn = document.getElementById('quitBtn');
this.gameOverModal = document.getElementById('gameOverModal');
this.finalScoreText = document.getElementById('finalScoreText');
this.finalScoreValue = document.getElementById('finalScoreValue');
this.playAgainBtn = document.getElementById('playAgainBtn');
this.mainMenuBtn = document.getElementById('mainMenuBtn');
}
updateScore(score) {
this.scoreValue.textContent = score;
}
updateTime(time) {
this.timeValue.textContent = time;
}
updateRings(ringsLeft, totalRings) {
this.ringIndicators.innerHTML = '';
for (let i = 0; i < totalRings; i++) {
const indicator = document.createElement('div');
indicator.className = 'ring-indicator';
if (i < ringsLeft) {
const randomColor = this.getRandomRingColor();
indicator.style.borderColor = randomColor;
indicator.style.backgroundColor = `${randomColor}40`;
} else {
indicator.style.borderColor = '#666';
indicator.style.backgroundColor = 'transparent';
}
this.ringIndicators.appendChild(indicator);
}
}
getRandomRingColor() {
const colors = ['#FF5722', '#4CAF50', '#2196F3', '#FFC107', '#9C27B0'];
return colors[Math.floor(Math.random() * colors.length)];
}
showScreen(screenName) {
document.getElementById('startScreen').classList.add('hidden');
document.getElementById('modeScreen').classList.add('hidden');
document.getElementById('optionsScreen').classList.add('hidden');
document.getElementById('creditsScreen').classList.add('hidden');
if (screenName) {
document.getElementById(screenName).classList.remove('hidden');
}
}
showGameUI() {
this.scoreDisplay.classList.remove('hidden');
this.timeDisplay.classList.remove('hidden');
this.ringCounter.classList.remove('hidden');
this.bubbleBtn.classList.remove('hidden');
}
hideGameUI() {
this.scoreDisplay.classList.add('hidden');
this.timeDisplay.classList.add('hidden');
this.ringCounter.classList.add('hidden');
this.bubbleBtn.classList.add('hidden');
}
createRing(x, y, size, color) {
const ring = document.createElement('div');
ring.className = 'ring active-ring';
ring.style.width = `${size}px`;
ring.style.height = `${size}px`;
ring.style.left = `${x - size/2}px`;
ring.style.top = `${y - size/2}px`;
ring.style.borderColor = color;
ring.style.backgroundColor = `${color}40`;
this.gameElements.appendChild(ring);
return ring;
}
createPeg(x, y, size, color, points) {
const peg = document.createElement('div');
peg.className = 'peg';
peg.style.left = `${x - size/2}px`;
peg.style.top = `${y - size/2}px`;
peg.style.width = `${size}px`;
peg.style.height = `${size}px`;
peg.style.backgroundColor = color;
this.container.appendChild(peg);
return peg;
}
createBubble(x, y) {
const size = 15 + Math.random() * 20;
const bubble = document.createElement('div');
bubble.className = 'bubble bubble-float';
bubble.style.width = `${size}px`;
bubble.style.height = `${size}px`;
bubble.style.left = `${x - size/2}px`;
bubble.style.top = `${y - size/2}px`;
this.gameView.appendChild(bubble);
setTimeout(() => {
bubble.remove();
}, 2000);
}
showResult(message) {
this.resultText.textContent = message;
this.gameOverlay.style.display = 'block';
this.resultModal.style.display = 'block';
}
hideResult() {
this.gameOverlay.style.display = 'none';
this.resultModal.style.display = 'none';
}
showPauseMenu() {
this.gameOverlay.style.display = 'block';
this.pauseModal.style.display = 'block';
}
hidePauseMenu() {
this.gameOverlay.style.display = 'none';
this.pauseModal.style.display = 'none';
}
showGameOver(score) {
this.finalScoreValue.textContent = score;
this.gameOverlay.style.display = 'block';
this.gameOverModal.style.display = 'block';
}
hideGameOver() {
this.gameOverlay.style.display = 'none';
this.gameOverModal.style.display = 'none';
}
showCountdown(count, callback) {
this.countdownDisplay.textContent = count;
this.countdownDisplay.classList.remove('hidden');
let current = count;
const interval = setInterval(() => {
current--;
if (current > 0) {
this.countdownDisplay.textContent = current;
} else {
this.countdownDisplay.textContent = 'GO!';
setTimeout(() => {
this.countdownDisplay.classList.add('hidden');
clearInterval(interval);
if (callback) callback();
}, 500);
}
}, 1000);
}
clearGameElements() {
this.gameElements.innerHTML = '';
const pegs = document.querySelectorAll('.peg');
pegs.forEach(peg => peg.remove());
}
}
// Game Controller - Manages game flow and user interactions
class GameController {
constructor(model, view) {
this.model = model;
this.view = view;
this.isDragging = false;
this.startDragPos = { x: 0, y: 0 };
this.currentDragPos = { x: 0, y: 0 };
this.gameInterval = null;
this.physicsWorld = null;
this.physicsRings = [];
this.activeRingElement = null;
// Initialize event listeners
this.initEventListeners();
// Initialize physics engine (Matter.js)
this.initPhysics();
}
initEventListeners() {
// Screen navigation
this.view.startBtn.addEventListener('click', () => {
this.view.showScreen('modeScreen');
});
this.view.optionsBtn.addEventListener('click', () => {
this.view.showScreen('optionsScreen');
});
this.view.creditsBtn.addEventListener('click', () => {
this.view.showScreen('creditsScreen');
});
this.view.backToStartBtn.addEventListener('click', () => {
this.view.showScreen('startScreen');
});
this.view.backFromCreditsBtn.addEventListener('click', () => {
this.view.showScreen('startScreen');
});
this.view.cancelOptionsBtn.addEventListener('click', () => {
this.view.showScreen('startScreen');
});
// Game mode selection
this.view.classicModeBtn.addEventListener('click', () => {
this.startGame('classic');
});
this.view.challengeModeBtn.addEventListener('click', () => {
this.startGame('challenge');
});
// Bubble button
this.view.bubbleBtn.addEventListener('click', (e) => {
this.createBubble();
});
// Pause/resume game
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.model.gameActive) {
this.togglePause();
}
});
// For mobile devices, add a pause button if needed
// Modal controls
this.view.continueBtn.addEventListener('click', () => {
this.view.hideResult();
if (this.model.ringsLeft > 0) {
this.createNewRing();
} else if (this.model.mode === 'challenge' && this.model.timeLeft <= 0) {
this.endGame();
}
});
this.view.resumeBtn.addEventListener('click', () => {
this.togglePause();
});
this.view.quitBtn.addEventListener('click', () => {
this.endGame();
});
this.view.playAgainBtn.addEventListener('click', () => {
this.view.hideGameOver();
this.startGame(this.model.mode);
});
this.view.mainMenuBtn.addEventListener('click', () => {
this.view.hideGameOver();
this.view.showScreen('startScreen');
});
// Ring dragging controls
this.view.gameView.addEventListener('touchstart', (e) => this.handleStart(e));
this.view.gameView.addEventListener('mousedown', (e) => this.handleStart(e));
this.view.gameView.addEventListener('touchmove', (e) => this.handleMove(e));
this.view.gameView.addEventListener('mousemove', (e) => this.handleMove(e));
this.view.gameView.addEventListener('touchend', (e) => this.handleEnd(e));
this.view.gameView.addEventListener('mouseup', (e) => this.handleEnd(e));
// Window resize
window.addEventListener('resize', () => this.handleResize());
}
initPhysics() {
const engine = Matter.Engine.create({
gravity: { x: 0, y: 1 }
});
this.physicsWorld = engine.world;
// Create invisible boundaries
const ground = Matter.Bodies.rectangle(window.innerWidth / 2, window.innerHeight + 50, window.innerWidth, 100, {
isStatic: true,
render: { fillStyle: 'transparent' },
label: 'ground'
});
const leftWall = Matter.Bodies.rectangle(-50, window.innerHeight / 2, 100, window.innerHeight, {
isStatic: true,
render: { fillStyle: 'transparent' },
label: 'leftWall'
});
const rightWall = Matter.Bodies.rectangle(window.innerWidth + 50, window.innerHeight / 2, 100, window.innerHeight, {
isStatic: true,
render: { fillStyle: 'transparent' },
label: 'rightWall'
});
const ceiling = Matter.Bodies.rectangle(window.innerWidth / 2, -50, window.innerWidth, 100, {
isStatic: true,
render: { fillStyle: 'transparent' },
label: 'ceiling'
});
Matter.World.add(this.physicsWorld, [ground, leftWall, rightWall, ceiling]);
// Start the engine
Matter.Engine.run(engine);
}
startGame(mode) {
this.model.setMode(mode);
this.view.showGameUI();
this.view.updateScore(0);
this.view.updateRings(this.model.ringsLeft, this.model.ringsLeft);
if (mode === 'challenge') {
this.view.timeDisplay.classList.remove('hidden');
this.view.updateTime(this.model.timeLeft);
} else {
this.view.timeDisplay.classList.add('hidden');
}
// Clear previous game elements
this.view.clearGameElements();
this.physicsRings = [];
// Generate pegs
this.generatePegs();
// Show countdown
this.view.showCountdown(3, () => {
this.model.gameActive = true;
// Start game timer if in challenge mode
if (mode === 'challenge') {
this.gameInterval = setInterval(() => {
const timeLeft = this.model.decrementTime();
this.view.updateTime(timeLeft);
if (timeLeft <= 0 && this.model.ringsLeft <= 0) {
clearInterval(this.gameInterval);
this.endGame();
}
}, 1000);
}
// Create the first ring
this.createNewRing();
});
}
generatePegs() {
// Clear previous pegs
this.model.pegs = [];
// Generate pegs based on game mode
const pegCount = this.model.mode === 'classic' ? 8 : 12;
// Get container dimensions
const containerRect = this.view.container.getBoundingClientRect();
const containerWidth = containerRect.width;
const containerHeight = containerRect.height;
const containerLeft = containerRect.left;
const containerTop = containerRect.top;
// Create pegs with physical bodies
for (let i = 0; i < pegCount; i++) {
const pegSize = 12 + Math.random() * 8; // Random size between 12 and 20
const pegRadius = pegSize / 2;
// Position pegs within the container
let pegX, pegY;
let validPosition = false;
let attempts = 0;
// Keep trying until we find a position that doesn't overlap
while (!validPosition && attempts < 100) {
attempts++;
pegX = containerLeft + 30 + Math.random() * (containerWidth - 60);
pegY = containerTop + 30 + Math.random() * (containerHeight - 60);
validPosition = true;
// Check for overlap with existing pegs
for (const existingPeg of this.model.pegs) {
const dx = pegX - existingPeg.x;
const dy = pegY - existingPeg.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < pegRadius + existingPeg.radius + 15) { // 15px minimum spacing
validPosition = false;
break;
}
}
}
if (validPosition) {
// Choose a color for the peg
const colors = ['#FF5722', '#4CAF50', '#2196F3', '#FFC107', '#9C27B0'];
const points = [10, 20, 30, 40, 50];
const colorIndex = Math.floor(Math.random() * colors.length);
const pegColor = colors[colorIndex];
const pegPoints = points[colorIndex];
// Create physics body for the peg
const pegBody = Matter.Bodies.circle(
pegX,
pegY,
pegRadius,
{
isStatic: true,
render: {
fillStyle: pegColor,
strokeStyle: '#000',
lineWidth: 1
}
}
);
Matter.World.add(this.physicsWorld, pegBody);
// Store peg data
this.model.pegs.push({
body: pegBody,
x: pegX,
y: pegY,
radius: pegRadius,
size: pegSize,
color: pegColor,
points: pegPoints
});
// Create visual peg
this.view.createPeg(pegX, pegY, pegSize, pegColor, pegPoints);
}
}
}
createNewRing() {
if (!this.model.gameActive || this.model.isPaused || this.model.ringsLeft <= 0) return;
// Create a new active ring at the bottom right position
const holderRect = this.view.ringCounter.getBoundingClientRect();
const ringX = holderRect.left + holderRect.width / 2;
const ringY = holderRect.top + holderRect.height / 2;
// Set active ring position (hold position)
this.startDragPos = { x: ringX, y: ringY };
this.currentDragPos = { x: ringX, y: ringY };
// Style the active ring
const ringSize = 30 + Math.floor(Math.random() * 20); // 30-50px diameter
const ringColor = this.view.getRandomRingColor();
// Create visual ring
this.activeRingElement = this.view.createRing(ringX, ringY, ringSize, ringColor);
// Store active ring data
this.activeRing = {
x: ringX,
y: ringY,
size: ringSize,
radius: ringSize / 2,
color: ringColor,
element: this.activeRingElement
};
}
handleStart(e) {
if (!this.model.gameActive || this.model.isPaused || !this.activeRing || this.isDragging) return;
e.preventDefault();
this.isDragging = true;
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
this.startDragPos = { x: clientX, y: clientY };
this.currentDragPos = { x: clientX, y: clientY };
// Style the active ring when dragging
this.activeRingElement.style.transform = 'scale(1.1)';
this.activeRingElement.style.opacity = '1';
this.activeRingElement.style.border = '3px solid';
// Update ring position
this.updateActiveRingPosition(clientX, clientY);
}
handleMove(e) {
if (!this.isDragging) return;
e.preventDefault();
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
this.currentDragPos = { x: clientX, y: clientY };
// Update ring position
this.updateActiveRingPosition(clientX, clientY);
}
handleEnd(e) {
if (!this.isDragging) return;
e.preventDefault();
const clientX = e.clientX || (e.changedTouches ? e.changedTouches[0].clientX : 0);
const clientY = e.clientY || (e.changedTouches ? e.changedTouches[0].clientY : 0);
if (clientX && clientY) {
this.currentDragPos = { x: clientX, y: clientY };
// Calculate throw velocity
const velocityX = (this.startDragPos.x - this.currentDragPos.x) * 0.25;
const velocityY = (this.startDragPos.y - this.currentDragPos.y) * 0.15 - 2; // Slight upward boost
// Calculate throw position (adjust for aiming)
const throwX = this.currentDragPos.x;
const throwY = this.currentDragPos.y - 10; // Slightly higher for better aiming
// Throw the ring
this.throwRing(throwX, throwY, velocityX, velocityY);
}
// Reset dragging state
this.isDragging = false;
// Reset active ring style
if (this.activeRingElement) {
this.activeRingElement.style.transform = 'scale(1)';
this.activeRingElement.style.opacity = '0.8';
this.activeRingElement.style.border = '3px dashed';
}
}
updateActiveRingPosition(x, y) {
if (this.activeRingElement) {
this.activeRingElement.style.left = `${x - this.activeRing.size/2}px`;
this.activeRingElement.style.top = `${y - this.activeRing.size/2}px`;
}
}
throwRing(x, y, velocityX, velocityY) {
if (!this.activeRing) return;
// Remove the visual active ring
this.activeRingElement.remove();
// Create physics ring
const ring = Matter.Bodies.circle(
x,
y,
this.activeRing.radius,
{
restitution: 0.5,
friction: 0.05,
density: 0.1,
render: {
fillStyle: this.activeRing.color,
strokeStyle: this.activeRing.color,
lineWidth: 3
}
}
);
// Apply initial velocity
Matter.Body.setVelocity(ring, { x: velocityX, y: velocityY });
Matter.Body.setAngularVelocity(ring, Math.random() * 0.1 - 0.05);
// Add ring to world
Matter.World.add(this.physicsWorld, ring);
// Store ring reference with additional data
this.physicsRings.push({
body: ring,
color: this.activeRing.color,
points: 0, // Will be set when landing on a peg
landed: false
});
// Create splash effect
this.createSplash(x, y + this.activeRing.radius);
// Decrement rings left and update UI
const ringsLeft = this.model.decrementRings();
this.view.updateRings(ringsLeft, this.model.mode === 'classic' ? 10 : 15);
// Clear the active ring
this.activeRing = null;
this.activeRingElement = null;
// Setup collision detection for this ring
this.setupRingCollisions(ring);
// When the ring comes to rest
Events.on(this.physicsWorld.engine, 'afterUpdate', () => {
if (ring && ring.isSleeping && !this.model.isPaused) {
// Show the active ring again (if there are rings left)
if (this.model.ringsLeft > 0) {
setTimeout(() => {
this.createNewRing();
}, 500);
} else if (this.model.mode === 'challenge' && this.model.timeLeft <= 0) {
this.endGame();
}
}
});
}
setupRingCollisions(ring) {
// Current ring index
const ringIndex = this.physicsRings.length - 1;
Events.on(this.physicsWorld.engine, 'collisionStart', (event) => {
const pairs = event.pairs;
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i];
const ringBody = this.physicsRings[ringIndex]?.body;
// Check if our ring collided with a peg
if ((pair.bodyA === ringBody && this.model.pegs.some(peg => peg.body === pair.bodyB)) ||
(pair.bodyB === ringBody && this.model.pegs.some(peg => peg.body === pair.bodyA))) {
const peg = this.model.pegs.find(peg =>
peg.body === pair.bodyA || peg.body === pair.bodyB
);
// Check if the ring is moving slowly enough to count as a successful toss
const speed = Math.sqrt(ringBody.velocity.x * ringBody.velocity.x +
ringBody.velocity.y * ringBody.velocity.y);
const angularSpeed = Math.abs(ringBody.angularVelocity);
if (speed < 0.5 && angularSpeed < 0.1 && !this.physicsRings[ringIndex].landed) {
// Successful toss
this.handleSuccessfulToss(ringIndex, peg);
}
}
// Check if the ring hit the water surface
if ((pair.bodyA === ringBody && pair.bodyB.position.y > window.innerHeight * 0.55) ||
(pair.bodyB === ringBody && pair.bodyA.position.y > window.innerHeight * 0.55)) {
// Create bubble effect
this.createBubble(
ringBody.position.x,
ringBody.position.y + this.physicsRings[ringIndex].body.circleRadius
);
}
}
});
}
handleSuccessfulToss(ringIndex, peg) {
// Mark ring as landed
this.physicsRings[ringIndex].landed = true;
this.physicsRings[ringIndex].points = peg.points;
// Add to score
const newScore = this.model.updateScore(peg.points);
this.view.updateScore(newScore);
// Show success message
if (this.model.mode === 'challenge') {
this.model.addTime(3); // 3 second bonus
this.view.updateTime(this.model.timeLeft);
this.view.showResult(`You scored ${peg.points} points! +3 sec bonus`);
} else {
this.view.showResult(`You scored ${peg.points} points!`);
}
// Create ripple effect
this.createRipple(
this.physicsRings[ringIndex].body.position.x,
this.physicsRings[ringIndex].body.position.y,
peg.color
);
// Change ring color to match peg
this.physicsRings[ringIndex].body.render.fillStyle = peg.color;
this.physicsRings[ringIndex].body.render.strokeStyle = peg.color;
// Make the ring static so it stays on the peg
Matter.Body.setStatic(this.physicsRings[ringIndex].body, true);
}
createBubble() {
// Create bubble in the center of the container
const containerRect = this.view.container.getBoundingClientRect();
const bubbleX = containerRect.left + containerRect.width / 2 + (Math.random() * 60 - 30);
const bubbleY = containerRect.top + containerRect.height;
// Create visual bubble
this.view.createBubble(bubbleX, bubbleY);
// Apply upward force to rings in the water
const waterY = containerRect.top + containerRect.height * 0.6;
for (const ring of this.physicsRings) {
if (ring.body.position.y > waterY && !ring.landed) {
const forceMagnitude = 0.002 * ring.body.mass;
const forceDirection = {
x: (Math.random() - 0.5) * 0.1,
y: -1
};
Matter.Body.applyForce(ring.body, ring.body.position, {
x: forceDirection.x * forceMagnitude,
y: forceDirection.y * forceMagnitude
});
}
}
}
createSplash(x, y) {
// Create multiple bubbles for splash effect
for (let i = 0; i < 5; i++) {
const offsetX = (Math.random() - 0.5) * 30;
const offsetY = (Math.random() - 0.5) * 10;
this.view.createBubble(x + offsetX, y + offsetY);
}
}
createRipple(x, y, color) {
const ripple = document.createElement('div');
ripple.className = 'water-ripple';
ripple.style.left = `${x - 50}px`;
ripple.style.top = `${y - 10}px`;
ripple.style.background = `radial-gradient(ellipse at center, ${color}40 0%, rgba(255,255,255,0) 70%)`;
this.view.gameView.appendChild(ripple);
gsap.to(ripple, {
width: 200,
height: 40,
opacity: 0,
duration: 1,
onComplete: () => ripple.remove()
});
}
togglePause() {
const isPaused = this.model.togglePause();
if (isPaused) {
this.view.showPauseMenu();
Matter.Engine.clear(this.physicsWorld.engine);
} else {
this.view.hidePauseMenu();
Matter.Engine.run(this.physicsWorld.engine);
}
}
endGame() {
this.model.gameActive = false;
if (this.gameInterval) {
clearInterval(this.gameInterval);
}
this.view.showGameOver(this.model.score);
}
handleResize() {
// Handle window resize if needed
// This would need to reposition elements and update physics bounds
}
}
// Initialize the game
document.addEventListener('DOMContentLoaded', () => {
const model = new GameModel();
const view = new GameView();
const controller = new GameController(model, view);
// Show start screen initially
view.showScreen('startScreen');
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Melfi/Retrorings" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>