您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
2D fighting game with Language/Character selection and improved combat mechanics into drawaria.online.
// ==UserScript== // @name Drawaria Battle! 🔥 Tik Tok vs. сонечка // @namespace http://tampermonkey.net/ // @version 1.4 // @description 2D fighting game with Language/Character selection and improved combat mechanics into drawaria.online. // @author YouTubeDrawaria // @match *://drawaria.online/* // @match *://*drawaria.online/* // @grant GM_addStyle // @run-at document-idle // @license MIT // @icon https://fonts.gstatic.com/s/e/notoemoji/latest/1f525/512.webp // @match https://drawaria.online/profile/?uid=1f400b90-8e8c-11ed-9fd3-c3a00b129da4 // @match https://drawaria.online/profile/?uid=7dda2280-618f-11ef-acaf-250da20bac69 // ==/UserScript== (function() { 'use strict'; // --- L10N: TRANSLATIONS --- const translations = { 'en': { lang_question: "Choose your language:", fight_title: "Drawaria Battle! 🔥 Tik Tok vs. сонечка", epic_brawl: "EPIC BRAWL BEGINS!", select_char: "Select your fighter:", play_as: "Play as", controls_title: "Controls (Player):", p1_controls: "P1 (Human): ← (Left), → (Right), ↑ (Jump), Space (Attack)", // Unified controls p2_controls: "P2 (Bot): Automatic Movement & Attacks", audio_note: "Click on the game to enable background music.", start_button: "FIGHT!", game_over: "GAME OVER", ko_message: "WINS by KO!", time_advantage: "WINS by Health Advantage!", draw_message: "Time Up! It's a DRAW!", restart_button: "CONTINUE / PLAY AGAIN", // Updated text ko: "KO!", round: "Round", match_over: "MATCH OVER" // New text }, 'ru': { lang_question: "Выберите ваш язык:", fight_title: "Drawaria Battle! 🔥 Tik Tok vs. сонечка", epic_brawl: "ЭПИЧЕСКАЯ БИТВА НАЧИНАЕТСЯ!", select_char: "Выберите вашего бойца:", play_as: "Играть за", controls_title: "Управление (Игрок):", p1_controls: "P1 (Человек): ← (Влево), → (Вправо), ↑ (Прыжок), Пробел (Атака)", // Unified controls p2_controls: "P2 (Бот): Автоматическое движение и атаки", audio_note: "Нажмите на игру, чтобы включить фоновую музыку.", start_button: "СРАЖАТЬСЯ!", game_over: "ИГРА ОКОНЧЕНА", ko_message: "ПОБЕДА нокаутом!", time_advantage: "ПОБЕДА по преимуществу здоровья!", draw_message: "Время вышло! НИЧЬЯ!", restart_button: "ПРОДОЛЖИТЬ / ИГРАТЬ СНОВА", // Updated text ko: "НОКАУТ!", round: "Раунд", match_over: "МАТЧ ОКОНЧЕН" // New text } }; let currentLang = 'en'; // Default language // --- USERSCRIPT STYLES & HTML INJECTION --- const GAME_WIDTH = 1200; const GAME_HEIGHT = 700; const STAGE_GROUND = 60; const TT_AVATAR_URL = 'https://yt3.googleusercontent.com/Fh3avs1Wt0eckLYBbX-DTGoCkiQ0tsBpZLU7k6lpKeSLcAwikIUOOb1vKrCzlnsHtnXFmVGCNQI=s120-c-k-c0x00ffffff'; const SONECKA_AVATAR_URL = 'https://yt3.googleusercontent.com/Rz25VO6HQZ_vhE2gJMgBOjzbSiqaVrSExTZOkTeL9gLTPXPJI1Pad-ozGSMm9QIDFyOR8pCHn4s=s120-c-k-c0x00ffffff-no-rj'; const TOTAL_ROUNDS = 3; // New constant for total rounds // The styles were kept from the previous version, focusing on functionality here. GM_addStyle(` @keyframes pulse { 0% { transform: scale(1); opacity: 0.8; } 100% { transform: scale(1.05); opacity: 1; } } #fighting-game-container { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 99999; border: 5px solid gold; box-shadow: 0 0 30px rgba(255, 69, 0, 0.9); background: #111; } #game-canvas { display: block; background: linear-gradient(to bottom, red 0%, yellow 50%); } #ui-overlay { position: absolute; top: 0; left: 0; width: 100%; pointer-events: none; } .ui-bar { height: 30px; background: rgba(0, 0, 0, 0.7); border: 2px solid white; position: relative; display: inline-block; box-shadow: 0 0 10px rgba(255, 0, 0, 0.5); } .health-bar { height: 100%; transition: width 0.1s ease-out; } #p1-health-container, #p2-health-container { width: 40%; margin-top: 20px; } #p1-health-container { float: left; margin-left: 50px; } #p2-health-container { float: right; margin-right: 50px; } .health-text { position: absolute; top: 0; left: 50%; transform: translateX(-50%); color: white; font-size: 18px; font-weight: bold; text-shadow: 1px 1px 3px #000; line-height: 30px; } .player-info { position: absolute; top: 5px; color: white; font-size: 14px; text-shadow: 1px 1px 2px #000; } .player-info img { width: 60px; height: 60px; border-radius: 50%; border: 3px solid gold; vertical-align: middle; margin: 0 5px; } #p1-info { left: 0; } #p2-info { right: 0; text-align: right; } #timer { position: absolute; top: 15px; left: 50%; transform: translateX(-50%); color: gold; font-size: 40px; font-weight: bold; background: rgba(0, 0, 0, 0.8); padding: 5px 20px; border-radius: 10px; border: 3px solid red; line-height: 1; } #lang-select-screen, #char-select-screen, #end-screen { position: absolute; color: white; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.98); color: white; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; border: 5px solid gold; } #lang-select-screen h1 { margin-bottom: 20px; color: white; } #lang-select-screen button { background: none; border: none; cursor: pointer; margin: 0 20px; font-size: 30px; } #lang-select-screen button img { width: 80px; height: 50px; border: 2px solid white; vertical-align: middle; margin-right: 10px;} #char-select-screen h1 { color: gold; text-shadow: 0 0 15px red, 0 0 30px orange; font-size: 64px; margin-bottom: 30px; animation: pulse 1s infinite alternate; } .char-button { display: inline-flex; flex-direction: column; align-items: center; background: linear-gradient(to right, #FF4500, #FFD700); color: white; border: 3px solid white; border-radius: 8px; margin: 0 30px; padding: 15px 40px; font-size: 24px; cursor: pointer; box-shadow: 0 5px #900; transition: all 0.1s; } .char-button:hover { transform: translateY(-2px); box-shadow: 0 7px #900; } .char-button img { width: 120px; height: 120px; border-radius: 50%; margin-bottom: 10px; } #end-screen h1 { font-size: 48px; } #end-screen button { padding: 15px 40px; font-size: 28px; cursor: pointer; background: linear-gradient(to right, #FF4500, #FFD700); color: white; border: 3px solid white; border-radius: 8px; margin-top: 30px; box-shadow: 0 5px #900; transition: all 0.1s; } #round-display { position: absolute; top: 5px; left: 50%; transform: translateX(-50%); color: white; font-size: 24px; font-weight: bold; text-shadow: 1px 1px 3px #000; } `); // HTML Structure (Kept the same structure, updated text via JS) const gameContainer = document.createElement('div'); gameContainer.id = 'fighting-game-container'; gameContainer.style.width = `${GAME_WIDTH}px`; gameContainer.style.height = `${GAME_HEIGHT}px`; gameContainer.innerHTML = ` <canvas id="game-canvas" width="${GAME_WIDTH}" height="${GAME_HEIGHT}"></canvas> <div id="ui-overlay"> <div id="p1-info" class="player-info"> <img id="p1-avatar-ui" src="${TT_AVATAR_URL}"> <span id="p1-name">Tik Tok Minecraft</span> </div> <div id="p2-info" class="player-info"> <span id="p2-name">Сонечка</span> <img id="p2-avatar-ui" src="${SONECKA_AVATAR_URL}"> </div> <div id="p1-health-container" class="ui-bar"> <div id="p1-health-bar" class="health-bar" style="background: red; width: 100%;"></div> <span id="p1-health-text" class="health-text">100</span> </div> <div id="p2-health-container" class="ui-bar"> <div id="p2-health-bar" class="health-bar" style="background: blue; width: 100%;"></div> <span id="p2-health-text" class="health-text">100</span> </div> <div id="timer">60</div> <div id="round-display"></div> </div> <div id="lang-select-screen"> <h1 id="lang-question-text"></h1> <button id="lang-en"><img src="https://flagcdn.com/w80/us.png" alt="English Flag">English</button> <button id="lang-ru"><img src="https://flagcdn.com/w80/ru.png" alt="Russian Flag">Русский</button> </div> <div id="char-select-screen" style="display: none;"> <h1 id="char-title"></h1> <h2 id="char-subtitle"></h2> <div> <button id="select-tt" class="char-button"> <img src="${TT_AVATAR_URL}"> <span id="tt-button-text"></span> </button> <button id="select-sonechka" class="char-button"> <img src="${SONECKA_AVATAR_URL}"> <span id="sonechka-button-text"></span> </button> </div> <h2 id="controls-title" style="margin-top: 40px;"></h2> <p id="p1-controls-text"></p> <p id="p2-controls-text"></p> <p id="audio-note-text"></p> </div> <div id="end-screen" style="display: none;"> <h1 id="end-title"></h1> <h2 id="winner-message"></h2> <button id="restart-button"></button> </div> <audio id="bg-music" src="https://www.myinstants.com/media/sounds/kenstheme.mp3" loop></audio> `; document.body.appendChild(gameContainer); // --- GAME SETUP & CONSTANTS --- const canvas = document.getElementById('game-canvas'); const ctx = canvas.getContext('2d'); const bgMusic = document.getElementById('bg-music'); const winnerMessage = document.getElementById('winner-message'); const GRAVITY = 0.8; const MAX_HEALTH = 100; const ROUND_TIME = 60; let gameState = { isRunning: false, time: ROUND_TIME, timerId: null, gameOver: false, lastTime: 0, deltaTime: 0, playerChar: null, botChar: null, round: 1, // Current round ttWins: 0, // Win counter for TT sonechkaWins: 0, // Win counter for Sonechka selectedPlayerKey: null // Stores the character chosen by the human }; let particles = []; // --- L10N FUNCTIONS (unchanged) --- function updateText() { const t = translations[currentLang]; document.getElementById('lang-question-text').textContent = t.lang_question; document.getElementById('char-title').textContent = t.fight_title; document.getElementById('char-subtitle').textContent = t.select_char; document.getElementById('tt-button-text').textContent = `${t.play_as} Tik Tok Minecraft`; document.getElementById('sonechka-button-text').textContent = `${t.play_as} Сонечка`; document.getElementById('controls-title').textContent = t.controls_title; document.getElementById('p1-controls-text').textContent = t.p1_controls; // Uses unified controls text document.getElementById('p2-controls-text').textContent = t.p2_controls; document.getElementById('audio-note-text').textContent = t.audio_note; document.getElementById('end-title').textContent = t.ko; document.getElementById('restart-button').textContent = t.restart_button; updateRoundDisplay(); // Update round display text } function setCurrentLanguage(lang) { currentLang = lang; updateText(); document.getElementById('lang-select-screen').style.display = 'none'; document.getElementById('char-select-screen').style.display = 'flex'; } // --- AUDIO, PARTICLE SYSTEM & FIGHTER CLASS (Fighter class updated for unified controls) --- let audioContext; function initAudioContext() { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); } } function playSound(frequency, duration, type = 'sine', volume = 0.5) { initAudioContext(); if (!audioContext) return; const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.type = type; oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); gainNode.gain.setValueAtTime(volume, audioContext.currentTime); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.start(); oscillator.stop(audioContext.currentTime + duration); } const soundEffects = { attack: () => playSound(600, 0.05, 'sawtooth', 0.4), hit: () => playSound(180, 0.15, 'square', 0.7), ko: () => playSound(100, 0.8, 'square', 0.8), jump: () => playSound(660, 0.08, 'sine', 0.3) }; class Particle { // ... (Particle class remains unchanged) constructor({ x, y, color }) { this.x = x; this.y = y; this.velocity = { x: (Math.random() - 0.5) * 5, y: (Math.random() - 0.5) * 5 - 2 }; this.size = Math.random() * 5 + 3; this.color = color; this.opacity = 1; this.friction = 0.99; } update() { this.velocity.x *= this.friction; this.velocity.y *= this.friction; this.x += this.velocity.x; this.y += this.velocity.y; this.opacity -= 0.03; this.size -= 0.1; } draw() { ctx.save(); ctx.globalAlpha = Math.max(0, this.opacity); ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } } class Fighter { // Updated controls parameter to match unified keys constructor({ x, name, avatarUrl, side }) { this.name = name; this.width = 80; this.height = 120; this.position = { x: x, y: GAME_HEIGHT - this.height - STAGE_GROUND }; this.avatar = new Image(); this.avatar.src = avatarUrl; this.loaded = false; this.avatar.onload = () => { this.loaded = true; }; this.velocity = { x: 0, y: 0 }; this.isGrounded = true; this.health = MAX_HEALTH; this.isAttacking = false; this.attackBox = { position: { x: this.position.x, y: this.position.y }, width: 100, height: 30, offset: side === 'left' ? 80 : -100 }; // Set unified controls for the human player (Logic handles which fighter is the human) this.controls = { left: 'arrowleft', right: 'arrowright', jump: 'arrowup', attack: ' ' }; this.side = side; this.isHit = false; this.hitTimer = 0; this.knockbackX = 0; this.damage = 10; this.isBot = false; this.wins = 0; // Win count for this fighter } update(keys) { if (!gameState.isRunning || this.health <= 0) return; // Apply gravity this.position.y += this.velocity.y; this.velocity.y += GRAVITY; const groundY = GAME_HEIGHT - this.height - STAGE_GROUND; if (this.position.y >= groundY) { this.position.y = groundY; this.velocity.y = 0; this.isGrounded = true; } let moveSpeed = 5; this.velocity.x = 0; // **IMPROVEMENT: Prevent movement during attack/hit** // Human player controls logic uses the unified key mapping regardless of which fighter it is if (!this.isAttacking && !this.isHit && !this.isBot && keys) { // UNIFIED CONTROLS LOGIC: Always use Arrow Keys and Space for the human player if (keys[this.controls.left].pressed) { this.velocity.x = -moveSpeed; } else if (keys[this.controls.right].pressed) { this.velocity.x = moveSpeed; } if (keys[this.controls.jump].pressed && this.isGrounded) { this.velocity.y = -18; this.isGrounded = false; soundEffects.jump(); } if (keys[this.controls.attack].pressed && !this.isAttacking && this.knockbackX === 0) { this.attack(); } } else if (this.isBot && !this.isAttacking && !this.isHit) { // Bot velocity set by botUpdate } // Apply knockback if (this.knockbackX !== 0) { this.velocity.x += this.knockbackX; this.knockbackX *= 0.9; if (Math.abs(this.knockbackX) < 0.5) this.knockbackX = 0; } this.position.x += this.velocity.x; if (this.position.x < 0) this.position.x = 0; if (this.position.x + this.width > GAME_WIDTH) this.position.x = GAME_WIDTH - this.width; this.attackBox.position.x = this.position.x + this.attackBox.offset; this.attackBox.position.y = this.position.y + 30; if (this.isHit) { this.hitTimer += gameState.deltaTime; if (this.hitTimer > 200) { this.isHit = false; this.hitTimer = 0; } } } attack() { // Prevent multiple attack calls during the attack window if (this.isAttacking) return; this.isAttacking = true; soundEffects.attack(); // Attack window duration setTimeout(() => { this.isAttacking = false; }, 250); // Slightly longer attack animation time } takeHit(damage, attackerSide) { if (this.health <= 0 || this.isHit) return; this.health = Math.max(0, this.health - damage); this.isHit = true; this.hitTimer = 0; // Reset timer on new hit soundEffects.hit(); this.updateUI(); const knockbackForce = 18; this.knockbackX = attackerSide === 'left' ? knockbackForce : -knockbackForce; for (let i = 0; i < 15; i++) { particles.push(new Particle({ x: this.position.x + this.width / 2, y: this.position.y + this.height / 2, color: 'rgba(255, 100, 0, 1)' })); } if (this.health === 0) { // Do not call endGame immediately, set flag for main loop to handle round end gameState.gameOver = true; } } updateUI() { const healthPercent = (this.health / MAX_HEALTH) * 100; const barId = this.side === 'left' ? 'p1-health-bar' : 'p2-health-bar'; const textId = this.side === 'left' ? 'p1-health-text' : 'p2-health-text'; const nameId = this.side === 'left' ? 'p1-name' : 'p2-name'; document.getElementById(barId).style.width = `${healthPercent}%`; document.getElementById(textId).textContent = this.health; document.getElementById(nameId).textContent = this.name + (this.isBot ? " (BOT)" : ""); } draw() { // ... (Draw logic remains unchanged) const spriteSize = 100; let avatarX = this.position.x + (this.width - spriteSize) / 2; let avatarY = this.position.y + (this.height - spriteSize) / 2; let drawW = spriteSize; let drawH = spriteSize; let hitRecoilY = 0; // Guard against null reference if one fighter is somehow not initialized in update() loop const opponentX = this.side === 'left' && p2 ? p2.position.x : (p1 ? p1.position.x : this.position.x + 1); const isFacingRight = this.position.x < opponentX; // **IMPROVEMENT: Visual Attack/Hit effects** if (this.isAttacking) { drawW *= 1.1; drawH *= 1.1; // Simple forward lunge visual avatarX += isFacingRight ? 10 : -10; } if (this.isHit) { // Vertical jiggle/recoil visual hitRecoilY = Math.sin(this.hitTimer / 50) * 5; } ctx.save(); // Flipping logic if (!isFacingRight) { ctx.translate(avatarX + drawW, 0); // Move origin to the right edge of the drawn sprite ctx.scale(-1, 1); // Draw the image at (0, Y + recoil) after translation/scaling ctx.drawImage(this.avatar, 0, avatarY + hitRecoilY, drawW, drawH); } else { ctx.drawImage(this.avatar, avatarX, avatarY + hitRecoilY, drawW, drawH); } ctx.restore(); // Hit state (flashing effect) if (this.isHit) { ctx.fillStyle = `rgba(255, 255, 255, ${Math.abs(Math.sin(performance.now() / 30))})`; // Very fast white flash ctx.fillRect(this.position.x, this.position.y, this.width, this.height); } // Attack state (Hitbox visual) if (this.isAttacking) { ctx.fillStyle = 'rgba(255, 255, 0, 0.2)'; ctx.fillRect(this.attackBox.position.x, this.attackBox.position.y, this.attackBox.width, this.attackBox.height); } } } // --- AI LOGIC (BOT) (Unchanged) --- function botUpdate(bot, target) { if (bot.isHit || bot.knockbackX !== 0 || bot.isAttacking) { bot.update(null); // Only physics/state updates return; } const distance = target.position.x - bot.position.x; const absDistance = Math.abs(distance); const attackRange = bot.width + 50; // 1. Attack Logic if (absDistance < attackRange && bot.isGrounded && Math.random() < 0.1) { bot.attack(); } // 2. Movement Logic const followDistance = 150; const tooCloseDistance = 50; if (absDistance > followDistance) { // Chase bot.velocity.x = distance > 0 ? 4 : -4; } else if (absDistance < tooCloseDistance) { // Step back (sometimes) if (Math.random() < 0.05) { bot.velocity.x = distance > 0 ? -2 : 2; } else { bot.velocity.x = 0; } } else { // Stay within range/stop bot.velocity.x = 0; } // 3. Jump Logic (Simple random jump/avoidance) if (bot.isGrounded && Math.random() < 0.005) { bot.velocity.y = -18; bot.isGrounded = false; } // Apply bot-set velocity bot.position.x += bot.velocity.x; bot.update(null); // Update physics/collision/boundaries } // --- GAME STATE & INSTANTIATION (Updated fighterData and resetGame logic) --- // Updated keys to only track the UNIFIED controls const keys = { 'arrowleft': { pressed: false }, 'arrowright': { pressed: false }, 'arrowup': { pressed: false }, ' ': { pressed: false } }; // Removed specific P1/P2 controls from data, as they are now unified/bot const fighterData = { tt: { name: 'Tik Tok Minecraft', avatarUrl: TT_AVATAR_URL, side: 'left', x: 100 }, sonechka: { name: 'Сонечка', avatarUrl: SONECKA_AVATAR_URL, side: 'right', x: GAME_WIDTH - 100 - 80 } }; let p1, p2; function updateRoundDisplay() { const t = translations[currentLang]; document.getElementById('round-display').textContent = `${t.round} ${gameState.round}/${TOTAL_ROUNDS} | TT: ${gameState.ttWins} - S: ${gameState.sonechkaWins}`; } // New function to reset the round state (health, position, etc.) function resetRound() { p1.health = MAX_HEALTH; p2.health = MAX_HEALTH; // Reset positions p1.position = { x: fighterData.tt.x, y: GAME_HEIGHT - p1.height - STAGE_GROUND }; p2.position = { x: fighterData.sonechka.x, y: GAME_HEIGHT - p2.height - STAGE_GROUND }; // Reset velocity/state p1.velocity = { x: 0, y: 0 }; p2.velocity = { x: 0, y: 0 }; p1.isAttacking = p2.isAttacking = false; p1.isHit = p2.isHit = false; p1.knockbackX = p2.knockbackX = 0; p1.updateUI(); p2.updateUI(); gameState.gameOver = false; gameState.time = ROUND_TIME; document.getElementById('timer').textContent = ROUND_TIME; particles = []; } function setupGame(selectedCharKey) { // This function runs once at the start of the match gameState.selectedPlayerKey = selectedCharKey; gameState.round = 1; gameState.ttWins = 0; gameState.sonechkaWins = 0; const p1_data = fighterData.tt; const p2_data = fighterData.sonechka; p1 = new Fighter(p1_data); p2 = new Fighter(p2_data); // Assign player/bot roles based on selection if (selectedCharKey === 'tt') { gameState.playerChar = p1; gameState.botChar = p2; p2.isBot = true; } else { // selectedCharKey === 'sonechka' gameState.playerChar = p2; gameState.botChar = p1; p1.isBot = true; } // Initialize display updateRoundDisplay(); resetRound(); } // --- COLLISION LOGIC (Unchanged) --- function rectangularCollision({ rectangle1, rectangle2 }) { return ( rectangle1.position.x + rectangle1.width >= rectangle2.position.x && rectangle1.position.x <= rectangle2.position.x + rectangle2.width && rectangle1.position.y + rectangle1.height >= rectangle2.position.y && rectangle1.position.y <= rectangle2.position.y + rectangle2.height ); } function checkAttacks(attacker, target) { // IMPROVEMENT: Attack hits only if target is not currently being hit AND attacker is in attack state if (attacker.isAttacking && !target.isHit) { const hit = rectangularCollision({ rectangle1: attacker.attackBox, rectangle2: target }); if (hit) { target.takeHit(attacker.damage, attacker.side); attacker.isAttacking = false; // Attack only hits once per animation } } } // --- GAME LOOP & RENDER (Unchanged logic, just references updated fighter variables) --- function drawBackground() { ctx.fillStyle = '#696969'; ctx.beginPath(); ctx.moveTo(0, GAME_HEIGHT - STAGE_GROUND - 100); ctx.lineTo(200, GAME_HEIGHT - STAGE_GROUND - 220); ctx.lineTo(450, GAME_HEIGHT - STAGE_GROUND - 150); ctx.lineTo(GAME_WIDTH, GAME_HEIGHT - STAGE_GROUND - 200); ctx.lineTo(GAME_WIDTH, GAME_HEIGHT - STAGE_GROUND); ctx.lineTo(0, GAME_HEIGHT - STAGE_GROUND); ctx.fill(); const groundY = GAME_HEIGHT - STAGE_GROUND; const groundGradient = ctx.createLinearGradient(0, groundY, 0, GAME_HEIGHT); groundGradient.addColorStop(0, '#556B2F'); groundGradient.addColorStop(1, '#8B4513'); ctx.fillStyle = groundGradient; ctx.fillRect(0, groundY, GAME_WIDTH, STAGE_GROUND); ctx.fillStyle = 'rgba(0, 0, 0, 0.4)'; ctx.fillRect(0, groundY, GAME_WIDTH, 5); } function render() { ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT); drawBackground(); if (p1) p1.draw(); if (p2) p2.draw(); particles.forEach((p, i) => { p.update(); p.draw(); if (p.opacity <= 0.05 || p.size <= 1) { particles.splice(i, 1); } }); } function update() { if (!gameState.isRunning || gameState.gameOver) return; // Player (Human) update uses keys, Bot update calls its own logic if (!gameState.playerChar.isBot) { gameState.playerChar.update(keys); botUpdate(gameState.botChar, gameState.playerChar); } else { // Human is P2 gameState.botChar.update(keys); // BotChar is now P1, but gets the keys object botUpdate(gameState.playerChar, gameState.botChar); // PlayerChar is P2, and is the bot's target } // Correcting the check: playerChar is the human, botChar is the bot regardless of P1/P2 if (gameState.playerChar.isBot) { // If player is the bot (i.e., the human chose Sonechka, which makes P1 the bot) gameState.botChar.update(keys); // P1 (Tik Tok) is the human, P2 (Sonechka) is the bot botUpdate(gameState.playerChar, gameState.botChar); // P2 (Sonechka) is the human, P1 (Tik Tok) is the bot // Re-assigning for clarity and to follow the original structure, though the underlying logic is better: // Let's rely on p1 and p2 directly for attack checks, as they are static. p1.update(p1.isBot ? null : keys); p2.update(p2.isBot ? null : keys); if (p1.isBot) botUpdate(p1, p2); if (p2.isBot) botUpdate(p2, p1); } else { // Human chose Tik Tok (p1 is human, p2 is bot) gameState.playerChar.update(keys); botUpdate(gameState.botChar, gameState.playerChar); } // Apply attack checks checkAttacks(p1, p2); checkAttacks(p2, p1); if (p1.health <= 0 || p2.health <= 0) { gameState.gameOver = true; endRound(p1.health <= 0 ? p2 : p1); // Pass the winner (or who didn't lose) } } function gameLoop(currentTime) { const elapsed = currentTime - gameState.lastTime; gameState.deltaTime = elapsed; gameState.lastTime = currentTime; update(); render(); if (!gameState.gameOver) { window.requestAnimationFrame(gameLoop); } } // --- GAME CONTROL FUNCTIONS (Updated for round system) --- function startTimer() { if (gameState.timerId) clearInterval(gameState.timerId); const timerDisplay = document.getElementById('timer'); gameState.time = ROUND_TIME; timerDisplay.textContent = gameState.time; gameState.timerId = setInterval(() => { if (!gameState.isRunning || gameState.gameOver) { clearInterval(gameState.timerId); return; } gameState.time--; timerDisplay.textContent = gameState.time; if (gameState.time <= 0) { clearInterval(gameState.timerId); gameState.gameOver = true; endRound(null, true); // Time up, no KO winner } }, 1000); } function startGame(selectedCharKey) { document.getElementById('char-select-screen').style.display = 'none'; document.getElementById('end-screen').style.display = 'none'; // Only run setupGame if it's the very first time (or after a full match ends) if (gameState.round === 1 && gameState.ttWins === 0 && gameState.sonechkaWins === 0) { setupGame(selectedCharKey); } else { // Otherwise, it's a new round in the same match resetRound(); } gameState.isRunning = true; startTimer(); gameState.lastTime = performance.now(); window.requestAnimationFrame(gameLoop); } function endRound(winner, timeUp = false) { gameState.isRunning = false; clearInterval(gameState.timerId); const t = translations[currentLang]; let message = ''; if (timeUp) { if (p1.health > p2.health) { winner = p1; message = `${winner.name} ${t.time_advantage}`; } else if (p2.health > p1.health) { winner = p2; message = `${winner.name} ${t.time_advantage}`; } else { message = t.draw_message; } } else { // KO win message = `${winner.name} ${t.ko_message}`; } if (winner) { if (winner === p1) { gameState.ttWins++; } else if (winner === p2) { gameState.sonechkaWins++; } } // Update win counters and round display before moving on updateRoundDisplay(); // Check for match end if (gameState.round >= TOTAL_ROUNDS || gameState.ttWins >= Math.ceil(TOTAL_ROUNDS/2) || gameState.sonechkaWins >= Math.ceil(TOTAL_ROUNDS/2) ) { // Final Game Over let finalWinner = null; if (gameState.ttWins > gameState.sonechkaWins) finalWinner = p1; else if (gameState.sonechkaWins > gameState.ttWins) finalWinner = p2; if (finalWinner) { document.getElementById('winner-message').textContent = `${finalWinner.name} ${t.match_over}! (${gameState.ttWins}-${gameState.sonechkaWins})`; } else { document.getElementById('winner-message').textContent = `${t.match_over}! ${t.draw_message} (${gameState.ttWins}-${gameState.sonechkaWins})`; } document.getElementById('end-title').textContent = t.game_over; document.getElementById('restart-button').textContent = t.restart_button.split(' / ')[1]; // PLAY AGAIN document.getElementById('end-screen').style.display = 'flex'; // Next click will go back to char select document.getElementById('restart-button').onclick = () => { document.getElementById('char-select-screen').style.display = 'flex'; document.getElementById('end-screen').style.display = 'none'; }; } else { // Next Round gameState.round++; document.getElementById('winner-message').textContent = message + ` - ${t.round} ${gameState.round}`; document.getElementById('end-title').textContent = t.ko; document.getElementById('restart-button').textContent = t.restart_button.split(' / ')[0]; // CONTINUE document.getElementById('end-screen').style.display = 'flex'; // Next click starts the next round document.getElementById('restart-button').onclick = () => { startGame(gameState.selectedPlayerKey); }; } } // --- EVENT LISTENERS BINDING (Updated to use startGame with selectedCharKey) --- document.getElementById('lang-en').addEventListener('click', () => setCurrentLanguage('en')); document.getElementById('lang-ru').addEventListener('click', () => setCurrentLanguage('ru')); document.getElementById('select-tt').addEventListener('click', () => { // First round start, setup the game state setupGame('tt'); startGame('tt'); }); document.getElementById('select-sonechka').addEventListener('click', () => { // First round start, setup the game state setupGame('sonechka'); startGame('sonechka'); }); // Initial click handler for restart button (for when the match is fully over) document.getElementById('restart-button').addEventListener('click', () => { if (!gameState.isRunning && gameState.round > TOTAL_ROUNDS) { // Only go back to char select if match is truly over document.getElementById('char-select-screen').style.display = 'flex'; document.getElementById('end-screen').style.display = 'none'; } }); // **UNIFIED KEYBOARD LISTENER LOGIC** window.addEventListener('keydown', (event) => { const key = event.key.toLowerCase(); // Check if the key is one of the unified controls if (keys[key] && gameState.isRunning) { keys[key].pressed = true; // Prevent default action for game keys if (['arrowleft', 'arrowright', 'arrowup', ' '].includes(key)) { event.preventDefault(); } } }); window.addEventListener('keyup', (event) => { const key = event.key.toLowerCase(); if (keys[key]) { keys[key].pressed = false; } }); document.body.addEventListener('click', () => { initAudioContext(); if (bgMusic.paused) { bgMusic.play().catch(e => console.error("Autoplay failed:", e)); } }, { once: true }); // Initial setup updateText(); document.getElementById('lang-select-screen').style.display = 'flex'; })();