Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Doge Whale Game</title> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| font-family: Arial, sans-serif; | |
| background: linear-gradient(to bottom, #4169E1, #000080); | |
| color: white; | |
| height: 100vh; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| header { | |
| background-color: rgba(0, 0, 128, 0.7); | |
| padding: 10px 20px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| h1 { | |
| margin: 0; | |
| font-size: 24px; | |
| } | |
| .user-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .coin-display { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| background-color: rgba(0, 0, 0, 0.3); | |
| padding: 5px 10px; | |
| border-radius: 20px; | |
| } | |
| .coin-icon { | |
| width: 20px; | |
| height: 20px; | |
| background-color: gold; | |
| border-radius: 50%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| font-weight: bold; | |
| color: #333; | |
| } | |
| .nav-buttons { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .nav-button { | |
| background-color: rgba(65, 105, 225, 0.7); | |
| border: 2px solid #87CEEB; | |
| color: white; | |
| padding: 5px 15px; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| .nav-button:hover { | |
| background-color: rgba(135, 206, 235, 0.7); | |
| transform: scale(1.05); | |
| } | |
| main { | |
| flex: 1; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| position: relative; | |
| } | |
| #game-container { | |
| position: relative; | |
| width: 800px; | |
| height: 500px; | |
| background-color: rgba(30, 144, 255, 0.3); | |
| border-radius: 10px; | |
| box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); | |
| overflow: hidden; | |
| } | |
| #game-canvas { | |
| width: 100%; | |
| height: 100%; | |
| display: block; | |
| } | |
| .overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 128, 0.7); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 10; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.5s; | |
| } | |
| .overlay.active { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .overlay h2 { | |
| font-size: 36px; | |
| margin-bottom: 20px; | |
| text-shadow: 0 0 10px rgba(255, 255, 255, 0.5); | |
| } | |
| .overlay p { | |
| font-size: 18px; | |
| margin-bottom: 30px; | |
| max-width: 80%; | |
| text-align: center; | |
| } | |
| .button { | |
| background-color: #4169E1; | |
| color: white; | |
| border: none; | |
| padding: 10px 30px; | |
| font-size: 18px; | |
| border-radius: 30px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); | |
| } | |
| .button:hover { | |
| background-color: #87CEEB; | |
| transform: scale(1.05); | |
| } | |
| #hud { | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .bar-container { | |
| width: 200px; | |
| height: 20px; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| border-radius: 10px; | |
| overflow: hidden; | |
| } | |
| #health-bar { | |
| height: 100%; | |
| width: 100%; | |
| background: linear-gradient(to right, #FF4500, #FF8C00); | |
| transition: width 0.3s; | |
| } | |
| #power-bar { | |
| height: 100%; | |
| width: 0%; | |
| background: linear-gradient(to right, #4169E1, #00BFFF); | |
| transition: width 0.3s; | |
| } | |
| #score-display { | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| font-size: 36px; | |
| font-weight: bold; | |
| color: white; | |
| text-shadow: 0 0 5px rgba(0, 0, 0, 0.5); | |
| } | |
| #combo-display { | |
| position: absolute; | |
| top: 50px; | |
| right: 10px; | |
| font-size: 24px; | |
| color: gold; | |
| text-shadow: 0 0 5px rgba(0, 0, 0, 0.5); | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| #power-up-notification { | |
| position: absolute; | |
| top: 100px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background-color: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| padding: 10px 20px; | |
| border-radius: 20px; | |
| font-size: 18px; | |
| opacity: 0; | |
| transition: opacity 0.3s, transform 0.3s; | |
| } | |
| #mobile-controls { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 20px; | |
| display: flex; | |
| gap: 20px; | |
| } | |
| .mobile-button { | |
| width: 50px; | |
| height: 50px; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| border-radius: 50%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| color: white; | |
| font-size: 24px; | |
| cursor: pointer; | |
| user-select: none; | |
| } | |
| #combat-controls { | |
| position: absolute; | |
| bottom: 20px; | |
| right: 20px; | |
| display: flex; | |
| gap: 20px; | |
| } | |
| .combat-button { | |
| width: 60px; | |
| height: 60px; | |
| border-radius: 50%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| color: white; | |
| font-size: 24px; | |
| cursor: pointer; | |
| user-select: none; | |
| } | |
| #combat-fire { | |
| background-color: rgba(255, 69, 0, 0.7); | |
| } | |
| #combat-special { | |
| background-color: rgba(65, 105, 225, 0.7); | |
| } | |
| #auto-fire-toggle { | |
| background-color: rgba(0, 255, 0, 0.7); | |
| } | |
| #auto-target-toggle { | |
| background-color: rgba(0, 255, 0, 0.7); | |
| } | |
| #orientation-toggle { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 20px; | |
| width: 40px; | |
| height: 40px; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| border-radius: 5px; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| cursor: pointer; | |
| } | |
| .vertical #game-container { | |
| width: 400px; | |
| height: 600px; | |
| } | |
| .background-particle { | |
| position: absolute; | |
| background-color: rgba(255, 255, 255, 0.3); | |
| border-radius: 50%; | |
| pointer-events: none; | |
| animation: float var(--duration) linear infinite; | |
| } | |
| @keyframes float { | |
| 0% { | |
| transform: translateY(100vh); | |
| } | |
| 100% { | |
| transform: translateY(-100px); | |
| } | |
| } | |
| .cloud { | |
| position: absolute; | |
| background-color: rgba(255, 255, 255, 0.7); | |
| border-radius: 50%; | |
| pointer-events: none; | |
| animation: drift 30s linear infinite; | |
| } | |
| @keyframes drift { | |
| 0% { | |
| transform: translateX(-100px); | |
| } | |
| 100% { | |
| transform: translateX(100vw); | |
| } | |
| } | |
| /* Multi-layered water animation */ | |
| .water-container { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 80px; | |
| overflow: hidden; | |
| z-index: 5; | |
| } | |
| .water-wave-1, .water-wave-2, .water-wave-3 { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| width: 200%; | |
| height: 100%; | |
| background-repeat: repeat-x; | |
| background-position: 0 bottom; | |
| transform-origin: center bottom; | |
| } | |
| .water-wave-1 { | |
| background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"><path fill="%23FF0000" fill-opacity="0.6" d="M0,192L48,197.3C96,203,192,213,288,229.3C384,245,480,267,576,250.7C672,235,768,181,864,181.3C960,181,1056,235,1152,234.7C1248,235,1344,181,1392,154.7L1440,128L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path></svg>'); | |
| animation: wave1 13s linear infinite; | |
| opacity: 0.7; | |
| z-index: 3; | |
| } | |
| .water-wave-2 { | |
| background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"><path fill="%23808080" fill-opacity="0.5" d="M0,160L48,181.3C96,203,192,245,288,240C384,235,480,181,576,186.7C672,192,768,256,864,261.3C960,267,1056,213,1152,192C1248,171,1344,181,1392,186.7L1440,192L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path></svg>'); | |
| animation: wave2 7s linear infinite; | |
| opacity: 0.5; | |
| z-index: 2; | |
| } | |
| .water-wave-3 { | |
| background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"><path fill="%23A00000" fill-opacity="0.4" d="M0,224L48,213.3C96,203,192,181,288,154.7C384,128,480,96,576,106.7C672,117,768,171,864,197.3C960,224,1056,224,1152,208C1248,192,1344,160,1392,144L1440,128L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path></svg>'); | |
| animation: wave3 10s linear infinite; | |
| opacity: 0.3; | |
| z-index: 1; | |
| } | |
| @keyframes wave1 { | |
| 0% { transform: translateX(0) translateZ(0) scaleY(1); } | |
| 50% { transform: translateX(-25%) translateZ(0) scaleY(1.1); } | |
| 100% { transform: translateX(-50%) translateZ(0) scaleY(1); } | |
| } | |
| @keyframes wave2 { | |
| 0% { transform: translateX(0) translateZ(0) scaleY(1); } | |
| 50% { transform: translateX(-30%) translateZ(0) scaleY(0.9); } | |
| 100% { transform: translateX(-50%) translateZ(0) scaleY(1); } | |
| } | |
| @keyframes wave3 { | |
| 0% { transform: translateX(-50%) translateZ(0) scaleY(1); } | |
| 50% { transform: translateX(-25%) translateZ(0) scaleY(1.05); } | |
| 100% { transform: translateX(0) translateZ(0) scaleY(1); } | |
| } | |
| /* Add water particles for extra effect */ | |
| .water-particle { | |
| position: absolute; | |
| background-color: rgba(255, 255, 255, 0.6); | |
| border-radius: 50%; | |
| pointer-events: none; | |
| animation: float-up var(--duration) ease-out forwards; | |
| z-index: 4; | |
| } | |
| @keyframes float-up { | |
| 0% { | |
| transform: translateY(0) scale(1); | |
| opacity: 0.7; | |
| } | |
| 100% { | |
| transform: translateY(-100px) scale(0); | |
| opacity: 0; | |
| } | |
| } | |
| #cloud-container { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100px; | |
| overflow: hidden; | |
| pointer-events: none; | |
| } | |
| /* Spinner Wheel Styles */ | |
| #spinner-overlay { | |
| background-color: rgba(0, 0, 128, 0.9); | |
| } | |
| #spinner-container { | |
| position: relative; | |
| width: 300px; | |
| height: 300px; | |
| margin: 20px auto; | |
| } | |
| #spinner-wheel { | |
| width: 100%; | |
| height: 100%; | |
| border-radius: 50%; | |
| background-color: #4169E1; | |
| position: relative; | |
| overflow: hidden; | |
| box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); | |
| transition: transform 3s cubic-bezier(0.17, 0.67, 0.83, 0.67); | |
| } | |
| #spinner-arrow { | |
| position: absolute; | |
| top: -10px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 0; | |
| height: 0; | |
| border-left: 15px solid transparent; | |
| border-right: 15px solid transparent; | |
| border-top: 30px solid #FF4500; | |
| z-index: 1; | |
| } | |
| .wheel-section { | |
| position: absolute; | |
| width: 50%; | |
| height: 50%; | |
| transform-origin: bottom right; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| color: white; | |
| font-weight: bold; | |
| font-size: 18px; | |
| text-shadow: 0 0 5px rgba(0, 0, 0, 0.5); | |
| } | |
| #spin-result { | |
| margin-top: 20px; | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: gold; | |
| text-shadow: 0 0 10px rgba(0, 0, 0, 0.5); | |
| height: 30px; | |
| } | |
| #spin-timer { | |
| margin-top: 10px; | |
| font-size: 16px; | |
| color: white; | |
| height: 20px; | |
| } | |
| /* Lottery Styles */ | |
| #lottery-overlay { | |
| background-color: rgba(0, 0, 128, 0.9); | |
| } | |
| #lottery-container { | |
| width: 80%; | |
| max-width: 600px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| #lottery-info { | |
| width: 100%; | |
| text-align: center; | |
| margin-bottom: 20px; | |
| } | |
| #lottery-pool { | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: gold; | |
| margin-bottom: 10px; | |
| } | |
| #lottery-status { | |
| font-size: 18px; | |
| margin-bottom: 10px; | |
| } | |
| #lottery-timer { | |
| font-size: 16px; | |
| color: #87CEEB; | |
| } | |
| #lottery-cards { | |
| display: flex; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| gap: 20px; | |
| margin: 20px 0; | |
| } | |
| .lottery-card { | |
| width: 100px; | |
| height: 150px; | |
| background-color: #4169E1; | |
| border-radius: 10px; | |
| position: relative; | |
| cursor: pointer; | |
| box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); | |
| transition: transform 0.3s; | |
| } | |
| .lottery-card:hover { | |
| transform: scale(1.05); | |
| } | |
| .card-cover { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: #87CEEB; | |
| border-radius: 10px; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| font-size: 36px; | |
| color: white; | |
| transition: opacity 0.5s; | |
| } | |
| .card-content { | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: gold; | |
| } | |
| .lottery-card.scratched .card-cover { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| /* Admin Panel Styles */ | |
| #admin-overlay { | |
| background-color: rgba(0, 0, 128, 0.9); | |
| overflow-y: auto; | |
| } | |
| #admin-container { | |
| width: 80%; | |
| max-width: 800px; | |
| padding: 20px; | |
| background-color: rgba(0, 0, 0, 0.3); | |
| border-radius: 10px; | |
| margin: 20px 0; | |
| } | |
| .admin-section { | |
| margin-bottom: 30px; | |
| } | |
| .admin-section h3 { | |
| font-size: 24px; | |
| margin-bottom: 15px; | |
| color: #87CEEB; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.3); | |
| padding-bottom: 5px; | |
| } | |
| .admin-controls { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
| gap: 15px; | |
| } | |
| .admin-control { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 5px; | |
| } | |
| .admin-control label { | |
| font-size: 16px; | |
| } | |
| .admin-control input[type="number"] { | |
| padding: 8px; | |
| border-radius: 5px; | |
| border: 1px solid #4169E1; | |
| background-color: rgba(0, 0, 0, 0.2); | |
| color: white; | |
| } | |
| .admin-control input[type="checkbox"] { | |
| width: 20px; | |
| height: 20px; | |
| } | |
| .admin-buttons { | |
| display: flex; | |
| justify-content: center; | |
| gap: 20px; | |
| margin-top: 20px; | |
| } | |
| /* Store Styles */ | |
| #store-overlay { | |
| background-color: rgba(0, 0, 128, 0.9); | |
| overflow-y: auto; | |
| } | |
| #store-container { | |
| width: 80%; | |
| max-width: 800px; | |
| padding: 20px; | |
| background-color: rgba(0, 0, 0, 0.3); | |
| border-radius: 10px; | |
| margin: 20px 0; | |
| } | |
| .store-items { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| } | |
| .store-item { | |
| background-color: rgba(65, 105, 225, 0.3); | |
| border-radius: 10px; | |
| padding: 15px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 10px; | |
| transition: transform 0.3s; | |
| } | |
| .store-item:hover { | |
| transform: scale(1.03); | |
| background-color: rgba(65, 105, 225, 0.5); | |
| } | |
| .store-item-image { | |
| width: 60px; | |
| height: 60px; | |
| background-color: rgba(0, 0, 0, 0.3); | |
| border-radius: 50%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| font-size: 30px; | |
| color: #87CEEB; | |
| } | |
| .store-item-name { | |
| font-size: 18px; | |
| font-weight: bold; | |
| color: white; | |
| } | |
| .store-item-description { | |
| font-size: 14px; | |
| text-align: center; | |
| color: #CCC; | |
| } | |
| .store-item-price { | |
| font-size: 16px; | |
| font-weight: bold; | |
| color: gold; | |
| } | |
| .store-item-button { | |
| background-color: #4169E1; | |
| color: white; | |
| border: none; | |
| padding: 8px 15px; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| .store-item-button:hover:not(:disabled) { | |
| background-color: #87CEEB; | |
| transform: scale(1.05); | |
| } | |
| .store-item-button:disabled { | |
| background-color: #666; | |
| cursor: not-allowed; | |
| } | |
| #withdrawal-section { | |
| text-align: center; | |
| padding: 20px; | |
| background-color: rgba(0, 0, 0, 0.2); | |
| border-radius: 10px; | |
| } | |
| /* Leaderboard Styles */ | |
| #leaderboard-overlay { | |
| background-color: rgba(0, 0, 128, 0.9); | |
| } | |
| #leaderboard-container { | |
| width: 80%; | |
| max-width: 600px; | |
| padding: 20px; | |
| background-color: rgba(0, 0, 0, 0.3); | |
| border-radius: 10px; | |
| margin: 20px 0; | |
| } | |
| .leaderboard-tabs { | |
| display: flex; | |
| justify-content: center; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| } | |
| .leaderboard-tab { | |
| padding: 8px 15px; | |
| background-color: rgba(65, 105, 225, 0.3); | |
| border-radius: 20px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| .leaderboard-tab:hover { | |
| background-color: rgba(65, 105, 225, 0.5); | |
| } | |
| .leaderboard-tab.active { | |
| background-color: #4169E1; | |
| font-weight: bold; | |
| } | |
| .leaderboard-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin-bottom: 20px; | |
| } | |
| .leaderboard-table th, .leaderboard-table td { | |
| padding: 10px; | |
| text-align: left; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .leaderboard-table th { | |
| background-color: rgba(0, 0, 0, 0.3); | |
| color: #87CEEB; | |
| } | |
| .leaderboard-table .rank, .leaderboard-table .score { | |
| text-align: center; | |
| } | |
| /* Close and Back Buttons */ | |
| .close-button { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| width: 40px; | |
| height: 40px; | |
| background-color: rgba(255, 0, 0, 0.7); | |
| border: none; | |
| border-radius: 50%; | |
| color: white; | |
| font-size: 20px; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| z-index: 20; | |
| } | |
| .close-button:hover { | |
| background-color: rgba(255, 0, 0, 0.9); | |
| transform: scale(1.1); | |
| } | |
| .back-button { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| width: 40px; | |
| height: 40px; | |
| background-color: rgba(65, 105, 225, 0.7); | |
| border: none; | |
| border-radius: 50%; | |
| color: white; | |
| font-size: 20px; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| z-index: 20; | |
| } | |
| .back-button:hover { | |
| background-color: rgba(65, 105, 225, 0.9); | |
| transform: scale(1.1); | |
| } | |
| @media (max-width: 768px) { | |
| #game-container { | |
| width: 100%; | |
| height: 100%; | |
| border-radius: 0; | |
| } | |
| header { | |
| padding: 5px 10px; | |
| } | |
| h1 { | |
| font-size: 18px; | |
| } | |
| .nav-button { | |
| padding: 3px 10px; | |
| font-size: 14px; | |
| } | |
| #hud { | |
| top: 5px; | |
| left: 5px; | |
| } | |
| .bar-container { | |
| width: 150px; | |
| height: 15px; | |
| } | |
| #score-display { | |
| font-size: 24px; | |
| } | |
| #combo-display { | |
| font-size: 18px; | |
| } | |
| .mobile-button, .combat-button { | |
| width: 40px; | |
| height: 40px; | |
| font-size: 18px; | |
| } | |
| } | |
| </style> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>Doge Whale Game</h1> | |
| <div class="nav-buttons"> | |
| <button class="nav-button" id="play-button">Play</button> | |
| <button class="nav-button" id="spinner-button">Spinner</button> | |
| <button class="nav-button" id="lottery-button">Lottery</button> | |
| <button class="nav-button" id="store-button">Store</button> | |
| <button class="nav-button" id="leaderboard-button">Leaderboard</button> | |
| <button class="nav-button" id="admin-button">Admin</button> | |
| </div> | |
| <div class="user-info"> | |
| <span>Guest</span> | |
| <div class="coin-display"> | |
| <div class="coin-icon">$</div> | |
| <span id="coin-count">0</span> | |
| </div> | |
| </div> | |
| </header> | |
| <main> | |
| <div id="game-container"> | |
| <canvas id="game-canvas"></canvas> | |
| <div id="hud"> | |
| <div class="bar-container"> | |
| <div id="health-bar"></div> | |
| </div> | |
| <div class="bar-container"> | |
| <div id="power-bar"></div> | |
| </div> | |
| </div> | |
| <div id="score-display">0</div> | |
| <div id="combo-display">Combo x1</div> | |
| <div id="power-up-notification"></div> | |
| <div id="mobile-controls"> | |
| <div class="mobile-button" id="mobile-up"><i class="fas fa-arrow-up"></i></div> | |
| <div class="mobile-button" id="mobile-down"><i class="fas fa-arrow-down"></i></div> | |
| </div> | |
| <div id="combat-controls"> | |
| <div class="combat-button" id="combat-fire"><i class="fas fa-bolt"></i></div> | |
| <div class="combat-button" id="combat-special"><i class="fas fa-star"></i></div> | |
| <div class="combat-button" id="auto-fire-toggle"><i class="fas fa-robot"></i></div> | |
| <div class="combat-button" id="auto-target-toggle"><i class="fas fa-crosshairs"></i></div> | |
| </div> | |
| <div class="water-container"> | |
| <div class="water-wave-1"></div> | |
| <div class="water-wave-2"></div> | |
| <div class="water-wave-3"></div> | |
| </div> | |
| <div class="overlay active" id="start-overlay"> | |
| <h2>Doge Whale Game</h2> | |
| <p>Navigate through pipes and defeat enemy whales!</p> | |
| <button class="button" id="start-button">Start Game</button> | |
| </div> | |
| <div class="overlay" id="game-over-overlay"> | |
| <h2>Game Over</h2> | |
| <p>Your score: <span id="final-score">0</span></p> | |
| <button class="button" id="restart-button">Play Again</button> | |
| <button class="close-button" id="game-over-close-button"><i class="fas fa-times"></i></button> | |
| </div> | |
| <!-- Spinner Wheel Overlay --> | |
| <div class="overlay" id="spinner-overlay"> | |
| <button class="close-button" id="spinner-x-button"><i class="fas fa-times"></i></button> | |
| <button class="back-button" id="spinner-back-button"><i class="fas fa-arrow-left"></i></button> | |
| <h2>Spin & Win</h2> | |
| <p>Spin the wheel to win DWHL coins!</p> | |
| <div id="spinner-container"> | |
| <div id="spinner-arrow"></div> | |
| <div id="spinner-wheel"> | |
| <!-- Wheel sections will be generated by JavaScript --> | |
| </div> | |
| </div> | |
| <div id="spin-result"></div> | |
| <div id="spin-timer"></div> | |
| <button class="button" id="spin-button">SPIN</button> | |
| <button class="button" id="spinner-close-button">Close</button> | |
| </div> | |
| <!-- Lottery Overlay --> | |
| <div class="overlay" id="lottery-overlay"> | |
| <button class="close-button" id="lottery-x-button"><i class="fas fa-times"></i></button> | |
| <button class="back-button" id="lottery-back-button"><i class="fas fa-arrow-left"></i></button> | |
| <h2>DWHL Lottery</h2> | |
| <p>Pay 5 DWHL to enter the lottery pool and scratch cards to win!</p> | |
| <div id="lottery-container"> | |
| <div id="lottery-info"> | |
| <div id="lottery-pool">Current Pool: 0 DWHL</div> | |
| <div id="lottery-status">Enter the lottery for a chance to win!</div> | |
| <div id="lottery-timer"></div> | |
| </div> | |
| <div id="lottery-cards"> | |
| <!-- Lottery cards will be generated by JavaScript --> | |
| </div> | |
| <button class="button" id="lottery-entry-button">Enter Lottery (5 DWHL)</button> | |
| <button class="button" id="lottery-close-button">Close</button> | |
| </div> | |
| </div> | |
| <!-- Admin Panel Overlay --> | |
| <div class="overlay" id="admin-overlay"> | |
| <button class="close-button" id="admin-x-button"><i class="fas fa-times"></i></button> | |
| <button class="back-button" id="admin-back-button"><i class="fas fa-arrow-left"></i></button> | |
| <h2>Admin Panel</h2> | |
| <p>Configure game settings and features</p> | |
| <div id="admin-container"> | |
| <div class="admin-section"> | |
| <h3>Spinner Settings</h3> | |
| <div class="admin-controls"> | |
| <div class="admin-control"> | |
| <label for="spinner-min-prize">Minimum Prize</label> | |
| <input type="number" id="spinner-min-prize" min="1" max="100" value="1"> | |
| </div> | |
| <div class="admin-control"> | |
| <label for="spinner-max-prize">Maximum Prize</label> | |
| <input type="number" id="spinner-max-prize" min="1" max="1000" value="100"> | |
| </div> | |
| <div class="admin-control"> | |
| <label for="spinner-cooldown">Cooldown (hours)</label> | |
| <input type="number" id="spinner-cooldown" min="0" max="72" value="24"> | |
| </div> | |
| <div class="admin-control"> | |
| <label for="spinner-enabled">Enable Spinner</label> | |
| <input type="checkbox" id="spinner-enabled" checked> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="admin-section"> | |
| <h3>Lottery Settings</h3> | |
| <div class="admin-controls"> | |
| <div class="admin-control"> | |
| <label for="lottery-entry-fee">Entry Fee</label> | |
| <input type="number" id="lottery-entry-fee" min="1" max="100" value="5"> | |
| </div> | |
| <div class="admin-control"> | |
| <label for="lottery-max-prize">Maximum Prize</label> | |
| <input type="number" id="lottery-max-prize" min="10" max="1000" value="200"> | |
| </div> | |
| <div class="admin-control"> | |
| <label for="lottery-card-count">Card Count</label> | |
| <input type="number" id="lottery-card-count" min="3" max="12" value="6"> | |
| </div> | |
| <div class="admin-control"> | |
| <label for="lottery-win-chance">Win Chance (%)</label> | |
| <input type="number" id="lottery-win-chance" min="1" max="100" value="30"> | |
| </div> | |
| <div class="admin-control"> | |
| <label for="lottery-enabled">Enable Lottery</label> | |
| <input type="checkbox" id="lottery-enabled" checked> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="admin-section"> | |
| <h3>Game Settings</h3> | |
| <div class="admin-controls"> | |
| <div class="admin-control"> | |
| <label for="game-difficulty">Difficulty</label> | |
| <input type="number" id="game-difficulty" min="1" max="10" value="5"> | |
| </div> | |
| <div class="admin-control"> | |
| <label for="game-coin-multiplier">Coin Multiplier</label> | |
| <input type="number" id="game-coin-multiplier" min="0.1" max="10" step="0.1" value="1"> | |
| </div> | |
| <div class="admin-control"> | |
| <label for="game-score-multiplier">Score Multiplier</label> | |
| <input type="number" id="game-score-multiplier" min="0.1" max="10" step="0.1" value="1"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="admin-buttons"> | |
| <button class="button" id="admin-save-button">Save Settings</button> | |
| <button class="button" id="admin-reset-button">Reset to Default</button> | |
| <button class="button" id="admin-close-button">Close</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Store Overlay --> | |
| <div class="overlay" id="store-overlay"> | |
| <button class="close-button" id="store-x-button"><i class="fas fa-times"></i></button> | |
| <button class="back-button" id="store-back-button"><i class="fas fa-arrow-left"></i></button> | |
| <h2>DWHL Store</h2> | |
| <p>Spend your DWHL coins on upgrades and items!</p> | |
| <div id="store-container"> | |
| <div class="store-items"> | |
| <!-- Store items will be generated by JavaScript --> | |
| </div> | |
| <div id="withdrawal-section"> | |
| <h3>Withdraw DWHL</h3> | |
| <p>Convert your DWHL coins to real value!</p> | |
| <p>Current Balance: <span id="withdrawal-balance">0</span> DWHL</p> | |
| <button class="button" id="withdrawal-button">Withdraw</button> | |
| </div> | |
| <button class="button" id="store-close-button">Close</button> | |
| </div> | |
| </div> | |
| <!-- Leaderboard Overlay --> | |
| <div class="overlay" id="leaderboard-overlay"> | |
| <button class="close-button" id="leaderboard-x-button"><i class="fas fa-times"></i></button> | |
| <button class="back-button" id="leaderboard-back-button"><i class="fas fa-arrow-left"></i></button> | |
| <h2>Leaderboard</h2> | |
| <p>See the top players and their scores!</p> | |
| <div id="leaderboard-container"> | |
| <div class="leaderboard-tabs"> | |
| <div class="leaderboard-tab active" data-tab="daily">Daily</div> | |
| <div class="leaderboard-tab" data-tab="weekly">Weekly</div> | |
| <div class="leaderboard-tab" data-tab="all-time">All Time</div> | |
| </div> | |
| <table class="leaderboard-table"> | |
| <thead> | |
| <tr> | |
| <th class="rank">Rank</th> | |
| <th>Player</th> | |
| <th class="score">Score</th> | |
| </tr> | |
| </thead> | |
| <tbody id="leaderboard-body"> | |
| <!-- Leaderboard entries will be generated by JavaScript --> | |
| </tbody> | |
| </table> | |
| <button class="button" id="leaderboard-close-button">Close</button> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Audio elements for sound effects --> | |
| <audio id="water-ambient" loop preload="auto"> | |
| <source src="https://assets.mixkit.co/sfx/preview/mixkit-water-flowing-loop-1189.mp3" type="audio/mpeg"> | |
| </audio> | |
| <audio id="water-splash" preload="auto"> | |
| <source src="https://assets.mixkit.co/sfx/preview/mixkit-water-splash-1295.mp3" type="audio/mpeg"> | |
| </audio> | |
| <audio id="weapon-sound" preload="auto"> | |
| <source src="https://assets.mixkit.co/sfx/preview/mixkit-laser-weapon-shot-1681.mp3" type="audio/mpeg"> | |
| </audio> | |
| <script> | |
| // Game initialization | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Game variables | |
| const canvas = document.getElementById('game-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const healthBar = document.getElementById('health-bar'); | |
| const powerBar = document.getElementById('power-bar'); | |
| const scoreDisplay = document.getElementById('score-display'); | |
| const comboDisplay = document.getElementById('combo-display'); | |
| const powerUpNotification = document.getElementById('power-up-notification'); | |
| const startOverlay = document.getElementById('start-overlay'); | |
| const gameOverOverlay = document.getElementById('game-over-overlay'); | |
| const finalScoreDisplay = document.getElementById('final-score'); | |
| const startButton = document.getElementById('start-button'); | |
| const restartButton = document.getElementById('restart-button'); | |
| const gameOverCloseButton = document.getElementById('game-over-close-button'); | |
| const mobileUpButton = document.getElementById('mobile-up'); | |
| const mobileDownButton = document.getElementById('mobile-down'); | |
| const combatFireButton = document.getElementById('combat-fire'); | |
| const combatSpecialButton = document.getElementById('combat-special'); | |
| const autoFireToggle = document.getElementById('auto-fire-toggle'); | |
| const autoTargetToggle = document.getElementById('auto-target-toggle'); | |
| // Audio elements | |
| const waterAmbientSound = document.getElementById('water-ambient'); | |
| const waterSplashSound = document.getElementById('water-splash'); | |
| const weaponSound = document.getElementById('weapon-sound'); | |
| // Set volume levels | |
| waterAmbientSound.volume = 0.3; | |
| waterSplashSound.volume = 0.4; | |
| weaponSound.volume = 0.2; | |
| // Game state | |
| let gameActive = false; | |
| let health = 100; | |
| let power = 0; | |
| let score = 0; | |
| let combo = 1; | |
| let coins = 0; | |
| let lastFrameTime = 0; | |
| let keys = {}; | |
| // Game objects | |
| let player = { | |
| x: 50, | |
| y: 200, | |
| width: 60, | |
| height: 40, | |
| velocity: 0, | |
| color: '#FF4500' | |
| }; | |
| let pipes = []; | |
| let enemies = []; | |
| let projectiles = []; | |
| let enemyProjectiles = []; | |
| let powerUps = []; | |
| let particles = []; | |
| let clouds = []; | |
| // Game settings | |
| const gameSettings = { | |
| combat: { | |
| autoFireEnabled: true, | |
| autoFireRate: 500, // milliseconds | |
| autoTargetingEnabled: true, | |
| targetingRange: 300, | |
| projectileSpeed: 10, | |
| projectileSize: 5, | |
| projectileDamage: 1 | |
| } | |
| }; | |
| let lastAutoFireTime = 0; | |
| // Whale animator | |
| const whaleAnimator = new WhaleAnimator(ctx); | |
| let whaleImagesLoaded = false; | |
| // Initialize the game | |
| function initGame() { | |
| // Set canvas size | |
| canvas.width = canvas.clientWidth; | |
| canvas.height = canvas.clientHeight; | |
| // Reset game state | |
| health = 100; | |
| power = 0; | |
| score = 0; | |
| combo = 1; | |
| // Update UI | |
| healthBar.style.width = `${health}%`; | |
| powerBar.style.width = `${power}%`; | |
| scoreDisplay.textContent = score; | |
| comboDisplay.textContent = `Combo x${combo}`; | |
| comboDisplay.style.opacity = '0'; | |
| // Reset game objects | |
| player = { | |
| x: 50, | |
| y: canvas.height / 2 - 20, | |
| width: 60, | |
| height: 40, | |
| velocity: 0, | |
| color: '#FF4500' | |
| }; | |
| pipes = []; | |
| enemies = []; | |
| projectiles = []; | |
| enemyProjectiles = []; | |
| powerUps = []; | |
| particles = []; | |
| // Generate initial clouds | |
| clouds = []; | |
| generateClouds(); | |
| // Create water particles | |
| createWaterParticles(); | |
| // Start ambient water sound | |
| waterAmbientSound.play(); | |
| // Load whale images if not already loaded | |
| if (!whaleImagesLoaded) { | |
| whaleAnimator.loadImages().then(success => { | |
| whaleImagesLoaded = success; | |
| console.log("Whale images loaded:", success); | |
| }); | |
| } | |
| // Start game loop | |
| gameActive = true; | |
| requestAnimationFrame(gameLoop); | |
| } | |
| // Game loop | |
| function gameLoop(timestamp) { | |
| if (!gameActive) return; | |
| // Calculate delta time | |
| const deltaTime = (timestamp - lastFrameTime) / 1000; | |
| lastFrameTime = timestamp; | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Update game state | |
| updateGameState(deltaTime); | |
| // Check collisions | |
| checkCollisions(); | |
| // Draw game objects | |
| drawGameObjects(); | |
| // Request next frame | |
| requestAnimationFrame(gameLoop); | |
| } | |
| // Update game state | |
| function updateGameState(deltaTime) { | |
| // Update player position | |
| if (keys['ArrowUp'] || keys['w']) { | |
| player.velocity -= 0.5; | |
| } | |
| if (keys['ArrowDown'] || keys['s']) { | |
| player.velocity += 0.5; | |
| } | |
| // Apply gravity | |
| player.velocity += 0.2; | |
| // Limit velocity | |
| player.velocity = Math.max(-10, Math.min(10, player.velocity)); | |
| // Update player position | |
| player.y += player.velocity; | |
| // Keep player within bounds | |
| if (player.y < 0) { | |
| player.y = 0; | |
| player.velocity = 0; | |
| } | |
| if (player.y + player.height > canvas.height) { | |
| player.y = canvas.height - player.height; | |
| player.velocity = 0; | |
| } | |
| // Generate pipes | |
| if (Math.random() < 0.02) { | |
| const gapHeight = 150; | |
| const gapPosition = Math.random() * (canvas.height - gapHeight); | |
| pipes.push({ | |
| x: canvas.width, | |
| y: 0, | |
| width: 50, | |
| height: gapPosition, | |
| passed: false | |
| }); | |
| pipes.push({ | |
| x: canvas.width, | |
| y: gapPosition + gapHeight, | |
| width: 50, | |
| height: canvas.height - gapPosition - gapHeight, | |
| passed: false | |
| }); | |
| } | |
| // Update pipes | |
| for (let i = 0; i < pipes.length; i++) { | |
| pipes[i].x -= 2; | |
| // Check if pipe is passed | |
| if (!pipes[i].passed && pipes[i].x + pipes[i].width < player.x) { | |
| pipes[i].passed = true; | |
| score += 10; | |
| scoreDisplay.textContent = score; | |
| } | |
| // Remove pipes that are off screen | |
| if (pipes[i].x + pipes[i].width < 0) { | |
| pipes.splice(i, 1); | |
| i--; | |
| } | |
| } | |
| // Generate enemies | |
| if (Math.random() < 0.01) { | |
| const enemy = { | |
| x: canvas.width, | |
| y: Math.random() * (canvas.height - 40), | |
| width: 50, | |
| height: 40, | |
| velocity: -2 - Math.random() * 2, | |
| color: '#4169E1', | |
| health: 3, | |
| lastShot: 0 | |
| }; | |
| enemies.push(enemy); | |
| } | |
| // Update enemies | |
| for (let i = 0; i < enemies.length; i++) { | |
| enemies[i].x += enemies[i].velocity; | |
| // Enemy shooting | |
| if (Date.now() - enemies[i].lastShot > 2000) { | |
| enemies[i].lastShot = Date.now(); | |
| // Calculate angle to player | |
| const dx = player.x + player.width/2 - (enemies[i].x + enemies[i].width/2); | |
| const dy = player.y + player.height/2 - (enemies[i].y + enemies[i].height/2); | |
| const angle = Math.atan2(dy, dx); | |
| // Create enemy projectile | |
| enemyProjectiles.push({ | |
| x: enemies[i].x, | |
| y: enemies[i].y + enemies[i].height / 2, | |
| size: 5, | |
| color: '#FF0000', | |
| vx: Math.cos(angle) * 5, | |
| vy: Math.sin(angle) * 5 | |
| }); | |
| } | |
| // Remove enemies that are off screen | |
| if (enemies[i].x + enemies[i].width < 0) { | |
| enemies.splice(i, 1); | |
| i--; | |
| } | |
| } | |
| // Auto-fire system | |
| if (gameSettings.combat.autoFireEnabled && gameActive) { | |
| const currentTime = Date.now(); | |
| if (currentTime - lastAutoFireTime >= gameSettings.combat.autoFireRate) { | |
| lastAutoFireTime = currentTime; | |
| autoFireProjectile(); | |
| } | |
| } | |
| // Update projectiles | |
| updateProjectiles(); | |
| // Update enemy projectiles | |
| for (let i = 0; i < enemyProjectiles.length; i++) { | |
| enemyProjectiles[i].x += enemyProjectiles[i].vx; | |
| enemyProjectiles[i].y += enemyProjectiles[i].vy; | |
| // Remove projectiles that are off screen | |
| if ( | |
| enemyProjectiles[i].x < 0 || | |
| enemyProjectiles[i].x > canvas.width || | |
| enemyProjectiles[i].y < 0 || | |
| enemyProjectiles[i].y > canvas.height | |
| ) { | |
| enemyProjectiles.splice(i, 1); | |
| i--; | |
| } | |
| } | |
| // Update power-ups | |
| for (let i = 0; i < powerUps.length; i++) { | |
| powerUps[i].x -= 2; | |
| // Remove power-ups that are off screen | |
| if (powerUps[i].x + powerUps[i].size < 0) { | |
| powerUps.splice(i, 1); | |
| i--; | |
| } | |
| } | |
| // Update particles | |
| for (let i = 0; i < particles.length; i++) { | |
| particles[i].x += particles[i].vx; | |
| particles[i].y += particles[i].vy; | |
| particles[i].alpha -= 0.01; | |
| // Remove particles that are faded out | |
| if (particles[i].alpha <= 0) { | |
| particles.splice(i, 1); | |
| i--; | |
| } | |
| } | |
| // Update clouds | |
| for (let i = 0; i < clouds.length; i++) { | |
| clouds[i].x += clouds[i].speed; | |
| // Remove clouds that are off screen | |
| if (clouds[i].x - clouds[i].size > canvas.width) { | |
| clouds.splice(i, 1); | |
| i--; | |
| } | |
| } | |
| // Generate new clouds | |
| if (clouds.length < 5 && Math.random() < 0.01) { | |
| generateCloud(); | |
| } | |
| // Update whale animations | |
| if (whaleImagesLoaded) { | |
| whaleAnimator.update(deltaTime); | |
| } | |
| } | |
| // Update projectiles with homing behavior | |
| function updateProjectiles() { | |
| for (let i = 0; i < projectiles.length; i++) { | |
| const projectile = projectiles[i]; | |
| // Handle homing projectiles | |
| if (projectile.homing && projectile.targetId >= 0 && projectile.targetId < enemies.length) { | |
| const target = enemies[projectile.targetId]; | |
| // Calculate new angle to target | |
| const dx = target.x + target.width/2 - projectile.x; | |
| const dy = target.y + target.height/2 - projectile.y; | |
| const targetAngle = Math.atan2(dy, dx); | |
| // Gradually adjust angle towards target (homing effect) | |
| const angleDiff = targetAngle - projectile.angle; | |
| // Normalize angle difference to -PI to PI range | |
| let normalizedAngleDiff = angleDiff; | |
| while (normalizedAngleDiff > Math.PI) normalizedAngleDiff -= 2 * Math.PI; | |
| while (normalizedAngleDiff < -Math.PI) normalizedAngleDiff += 2 * Math.PI; | |
| // Adjust angle with a turning rate factor (0.1 for gentle homing) | |
| projectile.angle += normalizedAngleDiff * 0.1; | |
| // Update velocity based on new angle | |
| projectile.vx = Math.cos(projectile.angle) * projectile.speed; | |
| projectile.vy = Math.sin(projectile.angle) * projectile.speed; | |
| } else if (!projectile.homing) { | |
| // Non-homing projectiles move straight | |
| projectile.vx = projectile.speed; | |
| projectile.vy = 0; | |
| } | |
| // Move projectile | |
| projectile.x += projectile.vx; | |
| projectile.y += projectile.vy; | |
| // Create trail effect for homing projectiles | |
| if (projectile.homing) { | |
| createParticles(projectile.x, projectile.y, 1, projectile.color); | |
| } | |
| // Remove projectiles that are off screen | |
| if ( | |
| projectile.x < 0 || | |
| projectile.x > canvas.width || | |
| projectile.y < 0 || | |
| projectile.y > canvas.height | |
| ) { | |
| projectiles.splice(i, 1); | |
| i--; | |
| } | |
| } | |
| } | |
| // Draw game objects | |
| function drawGameObjects() { | |
| // Draw background | |
| ctx.fillStyle = 'rgba(135, 206, 235, 0.3)'; // Light blue background | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Draw clouds | |
| for (let i = 0; i < clouds.length; i++) { | |
| const cloud = clouds[i]; | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; | |
| ctx.beginPath(); | |
| ctx.arc(cloud.x, cloud.y, cloud.size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Draw additional cloud parts | |
| ctx.beginPath(); | |
| ctx.arc(cloud.x + cloud.size * 0.5, cloud.y - cloud.size * 0.2, cloud.size * 0.7, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.beginPath(); | |
| ctx.arc(cloud.x - cloud.size * 0.5, cloud.y - cloud.size * 0.1, cloud.size * 0.6, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| // Draw pipes with gradient | |
| for (let i = 0; i < pipes.length; i++) { | |
| const gradient = ctx.createLinearGradient( | |
| pipes[i].x, | |
| pipes[i].y, | |
| pipes[i].x, | |
| pipes[i].y + pipes[i].height | |
| ); | |
| gradient.addColorStop(0, '#FF0000'); // Red at top | |
| gradient.addColorStop(1, '#808080'); // Gray at bottom | |
| ctx.fillStyle = gradient; | |
| ctx.fillRect(pipes[i].x, pipes[i].y, pipes[i].width, pipes[i].height); | |
| // Add highlight on the left edge for depth | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; | |
| ctx.fillRect(pipes[i].x, pipes[i].y, 3, pipes[i].height); | |
| // Add shadow on the right edge for depth | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; | |
| ctx.fillRect(pipes[i].x + pipes[i].width - 3, pipes[i].y, 3, pipes[i].height); | |
| // Add a border for better definition | |
| ctx.strokeStyle = '#606060'; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(pipes[i].x, pipes[i].y, pipes[i].width, pipes[i].height); | |
| } | |
| // Draw power-ups | |
| for (let i = 0; i < powerUps.length; i++) { | |
| ctx.fillStyle = powerUps[i].color; | |
| ctx.beginPath(); | |
| ctx.arc(powerUps[i].x, powerUps[i].y, powerUps[i].size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Draw icon | |
| ctx.fillStyle = 'white'; | |
| ctx.font = `${powerUps[i].size}px Arial`; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText(powerUps[i].icon, powerUps[i].x, powerUps[i].y); | |
| } | |
| // Draw player whale | |
| if (whaleImagesLoaded) { | |
| whaleAnimator.animateSwimming(player.x, player.y, player.width, player.height, 2); | |
| } else { | |
| // Fallback to original player drawing | |
| ctx.fillStyle = player.color; | |
| ctx.beginPath(); | |
| ctx.ellipse( | |
| player.x + player.width / 2, | |
| player.y + player.height / 2, | |
| player.width / 2, | |
| player.height / 2, | |
| 0, | |
| 0, | |
| Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| // Draw player eye | |
| ctx.fillStyle = 'black'; | |
| ctx.beginPath(); | |
| ctx.arc( | |
| player.x + player.width * 0.7, | |
| player.y + player.height * 0.4, | |
| player.width * 0.1, | |
| 0, | |
| Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| } | |
| // Draw enemies with targeting indicators | |
| for (let i = 0; i < enemies.length; i++) { | |
| if (whaleImagesLoaded) { | |
| // Use whale animation for enemies | |
| whaleAnimator.animateCombat(enemies[i].x, enemies[i].y, enemies[i].width, enemies[i].height); | |
| } else { | |
| // Fallback to original enemy drawing | |
| ctx.fillStyle = enemies[i].color; | |
| ctx.beginPath(); | |
| ctx.ellipse( | |
| enemies[i].x + enemies[i].width / 2, | |
| enemies[i].y + enemies[i].height / 2, | |
| enemies[i].width / 2, | |
| enemies[i].height / 2, | |
| 0, | |
| 0, | |
| Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| // Draw enemy eye | |
| ctx.fillStyle = 'black'; | |
| ctx.beginPath(); | |
| ctx.arc( | |
| enemies[i].x + enemies[i].width * 0.3, | |
| enemies[i].y + enemies[i].height * 0.4, | |
| enemies[i].width * 0.1, | |
| 0, | |
| Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| } | |
| // Draw targeting indicator for nearest enemy | |
| if (gameSettings.combat.autoTargetingEnabled) { | |
| const nearestEnemy = findNearestEnemy(); | |
| if (nearestEnemy === enemies[i]) { | |
| drawTargetingIndicator(enemies[i]); | |
| } | |
| } | |
| } | |
| // Draw projectiles | |
| for (let i = 0; i < projectiles.length; i++) { | |
| ctx.fillStyle = projectiles[i].color; | |
| ctx.beginPath(); | |
| ctx.arc(projectiles[i].x, projectiles[i].y, projectiles[i].size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Draw trail for homing projectiles | |
| if (projectiles[i].homing) { | |
| ctx.strokeStyle = projectiles[i].color; | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| ctx.moveTo(projectiles[i].x, projectiles[i].y); | |
| ctx.lineTo(projectiles[i].x - 20, projectiles[i].y); | |
| ctx.stroke(); | |
| } | |
| } | |
| // Draw enemy projectiles | |
| for (let i = 0; i < enemyProjectiles.length; i++) { | |
| ctx.fillStyle = enemyProjectiles[i].color; | |
| ctx.beginPath(); | |
| ctx.arc(enemyProjectiles[i].x, enemyProjectiles[i].y, enemyProjectiles[i].size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| // Draw particles | |
| for (let i = 0; i < particles.length; i++) { | |
| ctx.fillStyle = `rgba(${particles[i].r}, ${particles[i].g}, ${particles[i].b}, ${particles[i].alpha})`; | |
| ctx.beginPath(); | |
| ctx.arc(particles[i].x, particles[i].y, particles[i].size, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| } | |
| // Find the nearest enemy within range | |
| function findNearestEnemy() { | |
| if (enemies.length === 0) return null; | |
| let nearestEnemy = null; | |
| let shortestDistance = Infinity; | |
| for (let i = 0; i < enemies.length; i++) { | |
| const enemy = enemies[i]; | |
| const dx = enemy.x - player.x; | |
| const dy = enemy.y - player.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| // Check if enemy is within targeting range | |
| if (distance < gameSettings.combat.targetingRange && distance < shortestDistance) { | |
| shortestDistance = distance; | |
| nearestEnemy = enemy; | |
| } | |
| } | |
| return nearestEnemy; | |
| } | |
| // Draw targeting indicator for enemy | |
| function drawTargetingIndicator(enemy) { | |
| ctx.strokeStyle = 'rgba(255, 0, 0, 0.7)'; | |
| ctx.lineWidth = 2; | |
| // Draw targeting circle | |
| ctx.beginPath(); | |
| ctx.arc( | |
| enemy.x + enemy.width/2, | |
| enemy.y + enemy.height/2, | |
| enemy.width/2 + 5, | |
| 0, | |
| Math.PI * 2 | |
| ); | |
| ctx.stroke(); | |
| // Draw crosshair lines | |
| const centerX = enemy.x + enemy.width/2; | |
| const centerY = enemy.y + enemy.height/2; | |
| const size = enemy.width/2 + 10; | |
| ctx.beginPath(); | |
| // Horizontal line | |
| ctx.moveTo(centerX - size, centerY); | |
| ctx.lineTo(centerX + size, centerY); | |
| // Vertical line | |
| ctx.moveTo(centerX, centerY - size); | |
| ctx.lineTo(centerX, centerY + size); | |
| ctx.stroke(); | |
| } | |
| // Fire projectile | |
| function fireProjectile() { | |
| let projectileType = 'normal'; | |
| let projectileColor = '#4169E1'; // Royal blue | |
| let projectileSize = 5; | |
| let projectileSpeed = 10; | |
| // Create projectile | |
| const projectile = { | |
| x: player.x + player.width, | |
| y: player.y + player.height / 2, | |
| size: projectileSize, | |
| speed: projectileSpeed, | |
| color: projectileColor, | |
| type: projectileType, | |
| vx: projectileSpeed, | |
| vy: 0 | |
| }; | |
| projectiles.push(projectile); | |
| // Play weapon sound | |
| weaponSound.currentTime = 0; | |
| weaponSound.play(); | |
| } | |
| // Auto-fire projectile with targeting | |
| function autoFireProjectile() { | |
| // Find nearest enemy | |
| const targetEnemy = findNearestEnemy(); | |
| // If no enemy in range, don't fire | |
| if (!targetEnemy) return; | |
| let projectileType = 'homing'; | |
| let projectileColor = '#4169E1'; // Royal blue | |
| let projectileSize = gameSettings.combat.projectileSize; | |
| let projectileSpeed = gameSettings.combat.projectileSpeed; | |
| // Calculate angle to target | |
| const dx = targetEnemy.x + targetEnemy.width/2 - (player.x + player.width); | |
| const dy = targetEnemy.y + targetEnemy.height/2 - (player.y + player.height/2); | |
| const angle = Math.atan2(dy, dx); | |
| // Create projectile with targeting information | |
| const projectile = { | |
| x: player.x + player.width, | |
| y: player.y + player.height / 2, | |
| size: projectileSize, | |
| speed: projectileSpeed, | |
| color: projectileColor, | |
| type: projectileType, | |
| damage: gameSettings.combat.projectileDamage, | |
| homing: true, | |
| targetId: enemies.indexOf(targetEnemy), | |
| vx: Math.cos(angle) * projectileSpeed, | |
| vy: Math.sin(angle) * projectileSpeed, | |
| angle: angle | |
| }; | |
| // Add to projectiles array | |
| projectiles.push(projectile); | |
| // Create muzzle flash effect | |
| createParticles(player.x + player.width, player.y + player.height / 2, 5, projectileColor); | |
| // Play weapon sound | |
| weaponSound.currentTime = 0; | |
| weaponSound.play(); | |
| } | |
| // Create particles | |
| function createParticles(x, y, count, color) { | |
| // Parse color to RGB | |
| let r, g, b; | |
| if (color.startsWith('#')) { | |
| // Hex color | |
| const hex = color.substring(1); | |
| r = parseInt(hex.substring(0, 2), 16); | |
| g = parseInt(hex.substring(2, 4), 16); | |
| b = parseInt(hex.substring(4, 6), 16); | |
| } else if (color.startsWith('rgb')) { | |
| // RGB color | |
| const rgb = color.match(/\d+/g); | |
| r = parseInt(rgb[0]); | |
| g = parseInt(rgb[1]); | |
| b = parseInt(rgb[2]); | |
| } else { | |
| // Default color (white) | |
| r = g = b = 255; | |
| } | |
| // Create particles | |
| for (let i = 0; i < count; i++) { | |
| const angle = Math.random() * Math.PI * 2; | |
| const speed = 1 + Math.random() * 3; | |
| particles.push({ | |
| x: x, | |
| y: y, | |
| size: 1 + Math.random() * 3, | |
| vx: Math.cos(angle) * speed, | |
| vy: Math.sin(angle) * speed, | |
| alpha: 1, | |
| r: r, | |
| g: g, | |
| b: b | |
| }); | |
| } | |
| } | |
| // Generate clouds | |
| function generateClouds() { | |
| for (let i = 0; i < 5; i++) { | |
| generateCloud(true); | |
| } | |
| } | |
| // Generate a single cloud | |
| function generateCloud(initial = false) { | |
| const size = 20 + Math.random() * 30; | |
| const x = initial ? Math.random() * canvas.width : -size * 2; | |
| const y = 20 + Math.random() * 100; | |
| const speed = 0.2 + Math.random() * 0.3; | |
| clouds.push({ | |
| x: x, | |
| y: y, | |
| size: size, | |
| speed: speed | |
| }); | |
| } | |
| // Create water particles for animation | |
| function createWaterParticles() { | |
| const waterContainer = document.querySelector('.water-container'); | |
| // Create a new particle | |
| function createParticle() { | |
| const particle = document.createElement('div'); | |
| particle.classList.add('water-particle'); | |
| // Random size between 3px and 8px | |
| const size = 3 + Math.random() * 5; | |
| particle.style.width = `${size}px`; | |
| particle.style.height = `${size}px`; | |
| // Random position along the water | |
| const posX = Math.random() * waterContainer.offsetWidth; | |
| particle.style.left = `${posX}px`; | |
| particle.style.bottom = `${Math.random() * 20}px`; | |
| // Random duration between 1s and 3s | |
| const duration = 1 + Math.random() * 2; | |
| particle.style.setProperty('--duration', `${duration}s`); | |
| // Add to container | |
| waterContainer.appendChild(particle); | |
| // Remove after animation completes | |
| setTimeout(() => { | |
| particle.remove(); | |
| }, duration * 1000); | |
| } | |
| // Create particles at random intervals | |
| setInterval(createParticle, 300); | |
| } | |
| // Check water collision for sound effects | |
| function checkWaterCollision() { | |
| const waterLevel = canvas.height - 50; // Adjust based on water height | |
| // If player is near water level and moving | |
| if (player.y + player.height > waterLevel && player.velocity > 1) { | |
| waterSplashSound.currentTime = 0; | |
| waterSplashSound.play(); | |
| } | |
| } | |
| // Check collisions | |
| function checkCollisions() { | |
| // Check player-pipe collisions | |
| for (let i = 0; i < pipes.length; i++) { | |
| if ( | |
| player.x < pipes[i].x + pipes[i].width && | |
| player.x + player.width > pipes[i].x && | |
| player.y < pipes[i].y + pipes[i].height && | |
| player.y + player.height > pipes[i].y | |
| ) { | |
| // Player hit pipe | |
| takeDamage(10); | |
| createParticles(player.x, player.y, 20, player.color); | |
| } | |
| } | |
| // Check player-enemy collisions | |
| for (let i = 0; i < enemies.length; i++) { | |
| if ( | |
| player.x < enemies[i].x + enemies[i].width && | |
| player.x + player.width > enemies[i].x && | |
| player.y < enemies[i].y + enemies[i].height && | |
| player.y + player.height > enemies[i].y | |
| ) { | |
| // Player hit enemy | |
| takeDamage(20); | |
| createParticles(enemies[i].x, enemies[i].y, 30, enemies[i].color); | |
| // Remove enemy | |
| enemies.splice(i, 1); | |
| i--; | |
| } | |
| } | |
| // Check projectile-enemy collisions | |
| for (let i = 0; i < projectiles.length; i++) { | |
| for (let j = 0; j < enemies.length; j++) { | |
| if ( | |
| projectiles[i].x > enemies[j].x && | |
| projectiles[i].x < enemies[j].x + enemies[j].width && | |
| projectiles[i].y > enemies[j].y && | |
| projectiles[i].y < enemies[j].y + enemies[j].height | |
| ) { | |
| // Projectile hit enemy | |
| createParticles(projectiles[i].x, projectiles[i].y, 10, projectiles[i].color); | |
| // Damage enemy | |
| enemies[j].health--; | |
| // Check if enemy is defeated | |
| if (enemies[j].health <= 0) { | |
| // Create explosion | |
| createParticles( | |
| enemies[j].x + enemies[j].width / 2, | |
| enemies[j].y + enemies[j].height / 2, | |
| 30, | |
| enemies[j].color | |
| ); | |
| // Increase score | |
| score += 50 * combo; | |
| scoreDisplay.textContent = score; | |
| // Increase combo | |
| combo++; | |
| comboDisplay.textContent = `Combo x${combo}`; | |
| comboDisplay.style.opacity = '1'; | |
| // Increase power | |
| power += 10 * combo; | |
| if (power > 100) power = 100; | |
| powerBar.style.width = `${power}%`; | |
| // Chance to spawn power-up | |
| if (Math.random() < 0.3) { | |
| const powerUpType = Math.floor(Math.random() * 3); | |
| let powerUpColor, powerUpIcon, powerUpEffect; | |
| switch (powerUpType) { | |
| case 0: // Health | |
| powerUpColor = '#FF4500'; | |
| powerUpIcon = '+'; | |
| powerUpEffect = () => { | |
| health += 20; | |
| if (health > 100) health = 100; | |
| healthBar.style.width = `${health}%`; | |
| showNotification('Health +20'); | |
| }; | |
| break; | |
| case 1: // Power | |
| powerUpColor = '#4169E1'; | |
| powerUpIcon = '*'; | |
| powerUpEffect = () => { | |
| power += 30; | |
| if (power > 100) power = 100; | |
| powerBar.style.width = `${power}%`; | |
| showNotification('Power +30'); | |
| }; | |
| break; | |
| case 2: // Coins | |
| powerUpColor = '#FFD700'; | |
| powerUpIcon = '$'; | |
| powerUpEffect = () => { | |
| updateCoins(10); | |
| showNotification('Coins +10'); | |
| }; | |
| break; | |
| } | |
| powerUps.push({ | |
| x: enemies[j].x + enemies[j].width / 2, | |
| y: enemies[j].y + enemies[j].height / 2, | |
| size: 15, | |
| color: powerUpColor, | |
| icon: powerUpIcon, | |
| effect: powerUpEffect | |
| }); | |
| } | |
| // Remove enemy | |
| enemies.splice(j, 1); | |
| j--; | |
| } | |
| // Remove projectile | |
| projectiles.splice(i, 1); | |
| i--; | |
| break; | |
| } | |
| } | |
| } | |
| // Check player-enemy projectile collisions | |
| for (let i = 0; i < enemyProjectiles.length; i++) { | |
| if ( | |
| enemyProjectiles[i].x > player.x && | |
| enemyProjectiles[i].x < player.x + player.width && | |
| enemyProjectiles[i].y > player.y && | |
| enemyProjectiles[i].y < player.y + player.height | |
| ) { | |
| // Player hit by enemy projectile | |
| takeDamage(5); | |
| createParticles(enemyProjectiles[i].x, enemyProjectiles[i].y, 10, enemyProjectiles[i].color); | |
| // Remove projectile | |
| enemyProjectiles.splice(i, 1); | |
| i--; | |
| } | |
| } | |
| // Check player-power up collisions | |
| for (let i = 0; i < powerUps.length; i++) { | |
| const dx = powerUps[i].x - (player.x + player.width / 2); | |
| const dy = powerUps[i].y - (player.y + player.height / 2); | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < powerUps[i].size + player.width / 2) { | |
| // Player collected power-up | |
| powerUps[i].effect(); | |
| // Remove power-up | |
| powerUps.splice(i, 1); | |
| i--; | |
| } | |
| } | |
| // Check water collision for sound effects | |
| checkWaterCollision(); | |
| } | |
| // Take damage | |
| function takeDamage(amount) { | |
| health -= amount; | |
| healthBar.style.width = `${health}%`; | |
| // Reset combo | |
| combo = 1; | |
| comboDisplay.style.opacity = '0'; | |
| // Check if player is defeated | |
| if (health <= 0) { | |
| gameOver(); | |
| } | |
| } | |
| // Game over | |
| function gameOver() { | |
| gameActive = false; | |
| finalScoreDisplay.textContent = score; | |
| gameOverOverlay.classList.add('active'); | |
| // Stop ambient water sound | |
| waterAmbientSound.pause(); | |
| waterAmbientSound.currentTime = 0; | |
| } | |
| // Show notification | |
| function showNotification(text) { | |
| powerUpNotification.textContent = text; | |
| powerUpNotification.style.opacity = '1'; | |
| powerUpNotification.style.transform = 'translateX(-50%) translateY(-20px)'; | |
| setTimeout(() => { | |
| powerUpNotification.style.opacity = '0'; | |
| powerUpNotification.style.transform = 'translateX(-50%) translateY(0)'; | |
| }, 2000); | |
| } | |
| // Update coins | |
| function updateCoins(amount) { | |
| coins += amount; | |
| document.getElementById('coin-count').textContent = coins; | |
| } | |
| // Event listeners | |
| startButton.addEventListener('click', () => { | |
| startOverlay.classList.remove('active'); | |
| initGame(); | |
| }); | |
| restartButton.addEventListener('click', () => { | |
| gameOverOverlay.classList.remove('active'); | |
| initGame(); | |
| }); | |
| gameOverCloseButton.addEventListener('click', () => { | |
| gameOverOverlay.classList.remove('active'); | |
| }); | |
| // Keyboard controls | |
| document.addEventListener('keydown', (e) => { | |
| keys[e.key] = true; | |
| // Fire on space | |
| if (e.key === ' ' && gameActive) { | |
| fireProjectile(); | |
| } | |
| }); | |
| document.addEventListener('keyup', (e) => { | |
| keys[e.key] = false; | |
| }); | |
| // Mobile controls | |
| mobileUpButton.addEventListener('touchstart', () => { | |
| keys['ArrowUp'] = true; | |
| }); | |
| mobileUpButton.addEventListener('touchend', () => { | |
| keys['ArrowUp'] = false; | |
| }); | |
| mobileDownButton.addEventListener('touchstart', () => { | |
| keys['ArrowDown'] = true; | |
| }); | |
| mobileDownButton.addEventListener('touchend', () => { | |
| keys['ArrowDown'] = false; | |
| }); | |
| // Combat controls | |
| combatFireButton.addEventListener('click', () => { | |
| if (gameActive) { | |
| fireProjectile(); | |
| } | |
| }); | |
| combatSpecialButton.addEventListener('click', () => { | |
| if (gameActive && power >= 50) { | |
| // Special attack: multiple projectiles | |
| for (let i = -2; i <= 2; i++) { | |
| const angle = i * Math.PI / 10; | |
| const projectile = { | |
| x: player.x + player.width, | |
| y: player.y + player.height / 2, | |
| size: 5, | |
| speed: 10, | |
| color: '#FF8C00', | |
| type: 'special', | |
| vx: Math.cos(angle) * 10, | |
| vy: Math.sin(angle) * 10 | |
| }; | |
| projectiles.push(projectile); | |
| } | |
| // Use power | |
| power -= 50; | |
| powerBar.style.width = `${power}%`; | |
| // Play sound | |
| weaponSound.currentTime = 0; | |
| weaponSound.play(); | |
| } | |
| }); | |
| // Auto-fire toggle | |
| autoFireToggle.addEventListener('click', () => { | |
| gameSettings.combat.autoFireEnabled = !gameSettings.combat.autoFireEnabled; | |
| autoFireToggle.style.backgroundColor = gameSettings.combat.autoFireEnabled ? | |
| 'rgba(0, 255, 0, 0.7)' : 'rgba(255, 0, 0, 0.7)'; | |
| }); | |
| // Auto-targeting toggle | |
| autoTargetToggle.addEventListener('click', () => { | |
| gameSettings.combat.autoTargetingEnabled = !gameSettings.combat.autoTargetingEnabled; | |
| autoTargetToggle.style.backgroundColor = gameSettings.combat.autoTargetingEnabled ? | |
| 'rgba(0, 255, 0, 0.7)' : 'rgba(255, 0, 0, 0.7)'; | |
| }); | |
| // Resize canvas when window is resized | |
| window.addEventListener('resize', () => { | |
| canvas.width = canvas.clientWidth; | |
| canvas.height = canvas.clientHeight; | |
| }); | |
| }); | |
| // Whale Animator Class | |
| class WhaleAnimator { | |
| constructor(gameContext) { | |
| this.ctx = gameContext; | |
| this.whaleImages = []; | |
| this.animationSequences = { | |
| swim: { | |
| frames: [1, 2, 3, 4, 5, 6, 7, 8], | |
| frameRate: 8, // frames per second | |
| loop: true | |
| }, | |
| attack: { | |
| frames: [10, 11, 12, 13, 14, 15], | |
| frameRate: 10, | |
| loop: false | |
| }, | |
| combat: { | |
| frames: [20, 21, 22, 23, 24, 25, 26, 27], | |
| frameRate: 12, | |
| loop: true | |
| }, | |
| idle: { | |
| frames: [30, 31, 32, 33], | |
| frameRate: 4, | |
| loop: true | |
| }, | |
| damaged: { | |
| frames: [40, 41, 42, 43], | |
| frameRate: 8, | |
| loop: false | |
| } | |
| }; | |
| this.currentAnimation = 'swim'; | |
| this.currentFrame = 0; | |
| this.frameCounter = 0; | |
| this.lastFrameTime = 0; | |
| } | |
| // Load all whale images | |
| async loadImages() { | |
| const imagePaths = []; | |
| for (let i = 1; i <= 200; i++) { | |
| imagePaths.push(`/home/ubuntu/processed_images_hd/1 (${i}).png`); | |
| } | |
| // Load all images | |
| for (let path of imagePaths) { | |
| const img = new Image(); | |
| img.src = path; | |
| await new Promise(resolve => { | |
| img.onload = resolve; | |
| img.onerror = resolve; // Continue even if some images fail to load | |
| }); | |
| this.whaleImages.push(img); | |
| } | |
| console.log(`Loaded ${this.whaleImages.length} whale images`); | |
| return this.whaleImages.length > 0; | |
| } | |
| // Set current animation | |
| setAnimation(animationName) { | |
| if (this.animationSequences[animationName] && this.currentAnimation !== animationName) { | |
| this.currentAnimation = animationName; | |
| this.currentFrame = 0; | |
| this.frameCounter = 0; | |
| } | |
| } | |
| // Update animation frame | |
| update(deltaTime) { | |
| const sequence = this.animationSequences[this.currentAnimation]; | |
| if (!sequence) return; | |
| this.frameCounter += deltaTime * sequence.frameRate; | |
| if (this.frameCounter >= 1) { | |
| this.currentFrame = (this.currentFrame + Math.floor(this.frameCounter)) % sequence.frames.length; | |
| this.frameCounter = this.frameCounter % 1; | |
| // If animation is not looping and we reached the end | |
| if (!sequence.loop && this.currentFrame === sequence.frames.length - 1) { | |
| // Switch back to swimming animation | |
| this.setAnimation('swim'); | |
| } | |
| } | |
| } | |
| // Draw the current animation frame | |
| draw(x, y, width, height, flipX = false) { | |
| const sequence = this.animationSequences[this.currentAnimation]; | |
| if (!sequence || this.whaleImages.length === 0) return; | |
| const frameIndex = sequence.frames[this.currentFrame]; | |
| if (frameIndex >= this.whaleImages.length) return; | |
| const image = this.whaleImages[frameIndex]; | |
| this.ctx.save(); | |
| if (flipX) { | |
| this.ctx.translate(x + width, y); | |
| this.ctx.scale(-1, 1); | |
| this.ctx.drawImage(image, 0, 0, width, height); | |
| } else { | |
| this.ctx.drawImage(image, x, y, width, height); | |
| } | |
| this.ctx.restore(); | |
| } | |
| // Create a swimming animation | |
| animateSwimming(x, y, width, height, speed) { | |
| this.setAnimation('swim'); | |
| // Add wave-like motion | |
| const waveAmplitude = 5; | |
| const waveFrequency = 0.1; | |
| const offsetY = Math.sin(Date.now() * waveFrequency) * waveAmplitude; | |
| this.draw(x, y + offsetY, width, height); | |
| } | |
| // Create an attack animation | |
| animateAttack(x, y, width, height) { | |
| this.setAnimation('attack'); | |
| // Add forward thrust motion during attack | |
| const thrustDistance = 10; | |
| const attackProgress = this.currentFrame / this.animationSequences.attack.frames.length; | |
| const thrustOffset = Math.sin(attackProgress * Math.PI) * thrustDistance; | |
| this.draw(x + thrustOffset, y, width, height); | |
| } | |
| // Create a combat animation | |
| animateCombat(x, y, width, height) { | |
| this.setAnimation('combat'); | |
| // Add combat effects (rotation, scaling) | |
| const rotationAmount = 0.05; | |
| const scaleAmount = 0.1; | |
| const rotationOffset = Math.sin(Date.now() * 0.01) * rotationAmount; | |
| const scaleOffset = 1 + Math.abs(Math.sin(Date.now() * 0.02)) * scaleAmount; | |
| this.ctx.save(); | |
| this.ctx.translate(x + width/2, y + height/2); | |
| this.ctx.rotate(rotationOffset); | |
| this.ctx.scale(scaleOffset, scaleOffset); | |
| this.ctx.translate(-(x + width/2), -(y + height/2)); | |
| this.draw(x, y, width, height); | |
| this.ctx.restore(); | |
| } | |
| // Create a damaged animation | |
| animateDamaged(x, y, width, height) { | |
| this.setAnimation('damaged'); | |
| // Add flash effect | |
| const flashRate = 100; // ms | |
| const shouldFlash = Math.floor(Date.now() / flashRate) % 2 === 0; | |
| if (shouldFlash) { | |
| this.ctx.globalAlpha = 0.7; | |
| } | |
| this.draw(x, y, width, height); | |
| this.ctx.globalAlpha = 1.0; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |