(() => { const canvas = document.getElementById('gsGameCanvas'); if (!canvas) return; const ctx = canvas.getContext('2d'); const rulesWrap = document.getElementById('rules-wrap'); const startGameBtn = document.getElementById('start-game'); const finalWrap = document.getElementById('final-wrap'); const restartGameBtn = document.getElementById('restart-game-btn'); const finalHeadingWrap = finalWrap ? finalWrap.querySelector('.bp__heading-wrapper') : null; const finalHeading = finalHeadingWrap ? finalHeadingWrap.querySelector('h1, h2, h3, .bp__heading, [data-final-title]') : null; const finalText = finalHeadingWrap ? finalHeadingWrap.querySelector('p, .bp__text, [data-final-text]') : null; const fastRules = document.getElementById('fast-rules'); const fastRulesCloser = document.getElementById('fast-rules-closer'); const isMobile = window.matchMedia('(max-width: 991px)').matches; const ASSET_URLS = { bg: 'https://cdn.prod.website-files.com/69c28df2b8eba7d5be9d1615/69ce33a67b26333d164def51_gsh_bg.png', table: 'https://cdn.prod.website-files.com/6969fa6f60d9e42a73b56b7b/69bbe89855820c89d9130a0f_940f3d50921ba65ad751442b67d3f9dd_gs_table.png', ball: 'https://cdn.prod.website-files.com/6969fa6f60d9e42a73b56b7b/69bbe898e59877b5e16c06ef_gs_ball.png', reload: 'https://cdn.prod.website-files.com/69c28df2b8eba7d5be9d1615/69c3a83496bde4fefb54c92d_icon-reload.png', question: 'https://cdn.prod.website-files.com/69c28df2b8eba7d5be9d1615/69c3d18d60f591df1818c333_icon_question.png', cup: 'https://cdn.prod.website-files.com/6969fa6f60d9e42a73b56b7b/69bbe898c43952a8541e5573_gs_cup.png', enemy: 'https://cdn.prod.website-files.com/69c28df2b8eba7d5be9d1615/69ce33a68a5e6fd23c22ad46_gs_enemy-calm2.png', enemyConfused: 'https://cdn.prod.website-files.com/69c28df2b8eba7d5be9d1615/69ce33a6a0fcc4574daef82a_gs_enemy-confused.png', enemyCalm2: 'https://cdn.prod.website-files.com/69c28df2b8eba7d5be9d1615/69ce33a664f8deba98e2f88c_gs_enemy-calm.png', plantBody: 'https://cdn.prod.website-files.com/6969fa6f60d9e42a73b56b7b/69bbe8981a254ba9b7600fc5_f3c08b9df37ac21827dae0ac876137af_gs_plant-body.png', plantLeafRight: 'https://cdn.prod.website-files.com/6969fa6f60d9e42a73b56b7b/69bbe898cf9c3c8183a5b4b9_gs_plant-leaf-right.png', plantLeafLeft: 'https://cdn.prod.website-files.com/6969fa6f60d9e42a73b56b7b/69bbe8986001fef818b44442_gs_plant-leaf-left.png', plantLeafBottom: 'https://cdn.prod.website-files.com/6969fa6f60d9e42a73b56b7b/69bbe89838baeafd246ee4df_gs_plant-leaf-bottom.png', rightDuck: 'https://cdn.prod.website-files.com/6969fa6f60d9e42a73b56b7b/69bbe898140833807652acff_gs_right-picture-duck.png', rightW: 'https://cdn.prod.website-files.com/6969fa6f60d9e42a73b56b7b/69bbe89861483cdeb5eea85d_gs_right-picture-key-w.png', rightR: 'https://cdn.prod.website-files.com/6969fa6f60d9e42a73b56b7b/69bbe89803d24a13a678edcd_gs_right-picture-key-r.png', pillows: 'https://cdn.prod.website-files.com/6969fa6f60d9e42a73b56b7b/69bbe89861483cdeb5eea875_gs_pillows.png', leftSun: 'https://cdn.prod.website-files.com/6969fa6f60d9e42a73b56b7b/69bbe8987a539faf39ef58ef_gs_left-picture-sun.png', leftQ: 'https://cdn.prod.website-files.com/6969fa6f60d9e42a73b56b7b/69bbe8983cbfb57da6c913e2_gs_left-picture-key-q.png', leftCat: 'https://cdn.prod.website-files.com/6969fa6f60d9e42a73b56b7b/69bbe89834091349ea77694f_gs_left-picture-cat.png', }; const images = {}; function createFxState() { return { hitCooldown: 0, shakeTime: 0, tiltTime: 0, tiltAngle: 0, fallActive: false, fallSpeed: 0, fallRotation: 0, fallDirection: 1, hidden: false, hitCount: 0 }; } function makeDesktopConfig() { return { width: 1664, height: 998, physics: { gravity: 0.5, air: 0.98, bounce: 0.43, floorFriction: 0.93, maxDrag: 120, power: 0.32 }, ball: { startX: 740, startY: 690, baseSize: 72, minSize: 24, radius: 36 }, scene: { bg: { x: 0, y: 0, w: 1664, h: 998 }, leftSun: { x: 76, y: 126, w: 74, h: 184, fx: createFxState() }, leftQ: { x: 200, y: 144, w: 66, h: 80, fx: createFxState() }, leftCat: { x: 334, y: 68, w: 112, h: 178, fx: createFxState() }, rightDuck: { x: 1128, y: 48, w: 112, h: 344, fx: createFxState() }, rightW: { x: 1282, y: 102, w: 82, h: 84, fx: createFxState() }, rightR: { x: 1290, y: 236, w: 94, h: 86, fx: createFxState() }, plantBody: { x: 150, y: 280, w: 288, h: 356 }, plantLeafLeft: { x: 190, y: 370, w: 90, h: 110, fx: createFxState() }, plantLeafRight: { x: 275, y: 300, w: 130, h: 135, fx: createFxState() }, plantLeafBottom: { x: 200, y: 450, w: 135, h: 160, fx: createFxState() }, pillows: { x: 1110, y: 500, w: 244, h: 300 }, enemy: { x: 640, y: 40, w: 261, h: 766, fx: createFxState() }, table: { x: 432, y: 415, w: 690, h: 586 }, cups: [ { x: 640, y: 400, w: 52, h: 72, visible: true, hit: false, shakeTime: 0 }, { x: 710, y: 410, w: 52, h: 72, visible: true, hit: false, shakeTime: 0 }, { x: 780, y: 410, w: 52, h: 72, visible: true, hit: false, shakeTime: 0 }, { x: 850, y: 400, w: 52, h: 72, visible: true, hit: false, shakeTime: 0 } ] } }; } function makeMobileConfig() { return { width: 900, height: 1400, physics: { gravity: 0.5, air: 0.98, bounce: 0.43, floorFriction: 0.93, maxDrag: 160, power: 0.28 }, ball: { startX: 384, startY: 1090, baseSize: 72, minSize: 36, radius: 36 }, scene: { bg: { x: 0, y: 0, w: 900, h: 1400 }, leftSun: { x: 40, y: 170, w: 74, h: 184, fx: createFxState() }, leftQ: { x: 132, y: 205, w: 66, h: 80, fx: createFxState() }, leftCat: { x: 250, y: 120, w: 112, h: 178, fx: createFxState() }, rightDuck: { x: 560, y: 140, w: 112, h: 344, fx: createFxState() }, rightW: { x: 700, y: 195, w: 82, h: 84, fx: createFxState() }, rightR: { x: 708, y: 320, w: 94, h: 86, fx: createFxState() }, plantBody: { x: 0, y: 550, w: 240, h: 296 }, plantLeafLeft: { x: 35, y: 640, w: 76, h: 92, fx: createFxState() }, plantLeafRight: { x: 108, y: 575, w: 104, h: 108, fx: createFxState() }, plantLeafBottom: { x: 52, y: 715, w: 108, h: 128, fx: createFxState() }, pillows: { x: 690, y: 700, w: 200, h: 245 }, enemy: { x: 295, y: 400, w: 280, h: 750, fx: createFxState() }, table: { x: 17, y: 820, w: 877, h: 583 }, cups: [ { x: 290, y: 785, w: 65, h: 88, visible: true, hit: false, shakeTime: 0 }, { x: 365, y: 815, w: 65, h: 88, visible: true, hit: false, shakeTime: 0 }, { x: 445, y: 815, w: 65, h: 88, visible: true, hit: false, shakeTime: 0 }, { x: 520, y: 785, w: 65, h: 88, visible: true, hit: false, shakeTime: 0 } ] } }; } function restartGame() { resetGame(); if (finalWrap) { finalWrap.classList.add('u-display-none'); } gameState.isStarted = true; } const CONFIG = isMobile ? makeMobileConfig() : makeDesktopConfig(); const initialSceneSnapshot = JSON.parse(JSON.stringify(CONFIG.scene)); const BASE_WIDTH = CONFIG.width; const BASE_HEIGHT = CONFIG.height; const physics = CONFIG.physics; const scene = CONFIG.scene; canvas.width = BASE_WIDTH; canvas.height = BASE_HEIGHT; const ball = { startX: CONFIG.ball.startX, startY: CONFIG.ball.startY, baseSize: CONFIG.ball.baseSize, minSize: CONFIG.ball.minSize, size: CONFIG.ball.baseSize, x: CONFIG.ball.startX, y: CONFIG.ball.startY, vx: 0, vy: 0, radius: CONFIG.ball.radius, rotation: 0, rotationSpeed: 0, isDragging: false, isFlying: false, isReturning: false }; const input = { pointerX: 0, pointerY: 0, dragStartX: 0, dragStartY: 0, powerX: 0, powerY: 0 }; const throwState = { enemyHitThisThrow: false, cupHitThisThrow: false, lastThrowHitEnemy: false, lastThrowHitCup: false, hitObjectsThisThrow: new Set() }; const enemyState = { spriteKey: 'enemy' }; const gameState = { maxAttempts: 10, attemptsLeft: 10, isStarted: !rulesWrap, isFinished: false, enemyHitsInRow: 0 }; const uiState = { restartBtnRect: null, questionBtnRect: null }; let ignoreNextDocumentClick = false; const END_TEXT = { win: { title: 'У тебя получилось!', text: 'Муся гордо отворачивает мордочку и делает вид, что так и задумано. Сейчас ты победил, но перед уходом не забудь проверить обувь. Серьёзно.' }, lose: { title: 'Не переживай, еще повезет...', text: 'Муся умывается и даже не смотрит в твою сторону. Она выигрывала и не таких. Ты можешь перейти к следующей игре или зайти в наш Телеграм-чат и там высказать всё, что думаешь о её методах игры.' } }; const decorTargets = [ { key: 'plantLeafLeft', type: 'leaf', hitbox: () => ({ x: scene.plantLeafLeft.x + 8, y: scene.plantLeafLeft.y + 8, w: scene.plantLeafLeft.w - 16, h: scene.plantLeafLeft.h - 16 }) }, { key: 'plantLeafRight', type: 'leaf', hitbox: () => ({ x: scene.plantLeafRight.x + 8, y: scene.plantLeafRight.y + 8, w: scene.plantLeafRight.w - 16, h: scene.plantLeafRight.h - 16 }) }, { key: 'plantLeafBottom', type: 'leaf', hitbox: () => ({ x: scene.plantLeafBottom.x + 10, y: scene.plantLeafBottom.y + 10, w: scene.plantLeafBottom.w - 20, h: scene.plantLeafBottom.h - 20 }) }, { key: 'leftQ', type: 'smallPicture', hitbox: () => ({ x: scene.leftQ.x + 6, y: scene.leftQ.y + 6, w: scene.leftQ.w - 12, h: scene.leftQ.h - 12 }) }, { key: 'rightW', type: 'smallPicture', hitbox: () => ({ x: scene.rightW.x + 6, y: scene.rightW.y + 6, w: scene.rightW.w - 12, h: scene.rightW.h - 12 }) }, { key: 'rightR', type: 'smallPicture', hitbox: () => ({ x: scene.rightR.x + 6, y: scene.rightR.y + 6, w: scene.rightR.w - 12, h: scene.rightR.h - 12 }) }, { key: 'rightDuck', type: 'bigPicture', hitbox: () => ({ x: scene.rightDuck.x + 8, y: scene.rightDuck.y + 8, w: scene.rightDuck.w - 16, h: scene.rightDuck.h - 16 }) }, { key: 'leftSun', type: 'bigPicture', hitbox: () => ({ x: scene.leftSun.x + 8, y: scene.leftSun.y + 8, w: scene.leftSun.w - 16, h: scene.leftSun.h - 16 }) }, { key: 'leftCat', type: 'bigPicture', hitbox: () => ({ x: scene.leftCat.x + 8, y: scene.leftCat.y + 8, w: scene.leftCat.w - 16, h: scene.leftCat.h - 16 }) }, { key: 'enemy', type: 'enemy', hitbox: () => ({ x: scene.enemy.x + 34, y: scene.enemy.y + 18, w: scene.enemy.w - 68, h: scene.enemy.h * 0.2 }) } ]; function setFinalContent(result) { if (!finalWrap || !finalHeadingWrap) return; const payload = result === 'win' ? END_TEXT.win : END_TEXT.lose; if (finalHeading) { finalHeading.textContent = payload.title; } else { const first = finalHeadingWrap.children[0]; if (first) first.textContent = payload.title; } if (finalText) { finalText.textContent = payload.text; finalText.style.whiteSpace = 'pre-line'; } else { const second = finalHeadingWrap.children[1]; if (second) { second.textContent = payload.text; second.style.whiteSpace = 'pre-line'; } } } function restoreSceneFromConfig() { const freshScene = isMobile ? makeMobileConfig().scene : makeDesktopConfig().scene; Object.keys(scene).forEach((key) => { if (key === 'cups') { scene.cups.length = 0; freshScene.cups.forEach(cup => scene.cups.push(cup)); } else { scene[key] = freshScene[key]; } }); } function finishGame(result) { if (gameState.isFinished) return; gameState.isFinished = true; ball.isDragging = false; ball.isFlying = false; canvas.classList.remove('is-dragging'); setFinalContent(result); if (finalWrap) { finalWrap.classList.remove('u-display-none'); } } function getVisibleCupsCount() { return scene.cups.filter(cup => cup.visible).length; } function evaluateGameEnd() { if (gameState.isFinished) return; if (getVisibleCupsCount() === 0) { finishGame('win'); return; } if (gameState.enemyHitsInRow >= 3) { finishGame('lose'); return; } if (gameState.attemptsLeft <= 0) { finishGame('lose'); } } function loadImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => resolve(img); img.onerror = reject; img.src = src; }); } async function preloadAssets() { const entries = Object.entries(ASSET_URLS); await Promise.all( entries.map(async ([key, url]) => { images[key] = await loadImage(url); }) ); } function updateBallScale() { const nearY = ball.startY; const farY = isMobile ? 720 : 410; const clampedY = Math.max(farY, Math.min(nearY, ball.y)); const t = (clampedY - farY) / (nearY - farY); const nextSize = ball.minSize + (ball.baseSize - ball.minSize) * t; const centerX = ball.x + ball.size / 2; const centerY = ball.y + ball.size / 2; ball.size = nextSize; ball.radius = ball.size / 2; ball.x = centerX - ball.size / 2; ball.y = centerY - ball.size / 2; } function resetDecorStates() { [ scene.leftSun, scene.leftQ, scene.leftCat, scene.rightDuck, scene.rightW, scene.rightR, scene.plantLeafLeft, scene.plantLeafRight, scene.plantLeafBottom, scene.enemy ].forEach(item => { if (!item.fx) return; item.fx.hitCooldown = 0; item.fx.shakeTime = 0; item.fx.tiltTime = 0; item.fx.tiltAngle = 0; item.fx.fallActive = false; item.fx.fallSpeed = 0; item.fx.fallRotation = 0; item.fx.fallDirection = 1; item.fx.hidden = false; item.fx.hitCount = 0; }); } function consumeAttemptIfNeeded() { if (gameState.attemptsLeft > 0) { gameState.attemptsLeft -= 1; } } function resetBall(consumeAttempt = true) { if (consumeAttempt) { consumeAttemptIfNeeded(); } throwState.lastThrowHitEnemy = throwState.enemyHitThisThrow; throwState.lastThrowHitCup = throwState.cupHitThisThrow; if (throwState.lastThrowHitCup) { enemyState.spriteKey = 'enemyCalm2'; } else if (throwState.lastThrowHitEnemy) { enemyState.spriteKey = 'enemyConfused'; } else { enemyState.spriteKey = 'enemy'; } if (throwState.lastThrowHitEnemy) { gameState.enemyHitsInRow += 1; } else { gameState.enemyHitsInRow = 0; } ball.size = ball.baseSize; ball.radius = ball.size / 2; ball.x = ball.startX; ball.y = ball.startY; ball.vx = 0; ball.vy = 0; ball.rotation = 0; ball.rotationSpeed = 0; ball.isDragging = false; ball.isFlying = false; ball.isReturning = false; throwState.enemyHitThisThrow = false; throwState.cupHitThisThrow = false; evaluateGameEnd(); } function resetGame() { restoreSceneFromConfig(); scene.cups.forEach(cup => { cup.visible = true; cup.hit = false; cup.shakeTime = 0; }); resetDecorStates(); enemyState.spriteKey = 'enemy'; throwState.enemyHitThisThrow = false; throwState.cupHitThisThrow = false; throwState.lastThrowHitEnemy = false; throwState.lastThrowHitCup = false; throwState.hitObjectsThisThrow.clear(); gameState.attemptsLeft = gameState.maxAttempts; gameState.isFinished = false; gameState.enemyHitsInRow = 0; if (finalWrap) { finalWrap.classList.add('u-display-none'); } resetBall(false); updateBallScale(); } function startGame() { gameState.isStarted = true; if (rulesWrap) { rulesWrap.classList.add('u-display-none'); } } function getPointerPosition(event) { const rect = canvas.getBoundingClientRect(); const clientX = event.touches ? event.touches[0].clientX : event.clientX; const clientY = event.touches ? event.touches[0].clientY : event.clientY; const scaleX = BASE_WIDTH / rect.width; const scaleY = BASE_HEIGHT / rect.height; return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY }; } function pointInBall(px, py) { const cx = ball.x + ball.size / 2; const cy = ball.y + ball.size / 2; const dx = px - cx; const dy = py - cy; const bonus = isMobile ? 18 : 0; return Math.sqrt(dx * dx + dy * dy) <= ball.radius + bonus; } function onPointerDown(event) { const pos = getPointerPosition(event); if (pointInRect(pos.x, pos.y, uiState.restartBtnRect)) { restartGame(); event.preventDefault(); return; } if (pointInRect(pos.x, pos.y, uiState.questionBtnRect)) { ignoreNextDocumentClick = true; openFastRules(); event.preventDefault(); return; } if (!gameState.isStarted) return; if (gameState.isFinished) return; if (ball.isFlying) return; if (gameState.attemptsLeft <= 0) return; if (pointInBall(pos.x, pos.y)) { ball.isDragging = true; input.dragStartX = ball.x + ball.size / 2; input.dragStartY = ball.y + ball.size / 2; input.pointerX = pos.x; input.pointerY = pos.y; canvas.classList.add('is-dragging'); event.preventDefault(); } } function onPointerMove(event) { const pos = getPointerPosition(event); input.pointerX = pos.x; input.pointerY = pos.y; if (!ball.isDragging) return; let dx = pos.x - input.dragStartX; let dy = pos.y - input.dragStartY; const distance = Math.sqrt(dx * dx + dy * dy); if (distance > physics.maxDrag) { const ratio = physics.maxDrag / distance; dx *= ratio; dy *= ratio; } input.powerX = dx; input.powerY = dy; event.preventDefault(); } function onPointerUp() { if (!ball.isDragging) return; if (gameState.isFinished) return; ball.isDragging = false; ball.isFlying = true; canvas.classList.remove('is-dragging'); ball.vx = -input.powerX * physics.power; ball.vy = -input.powerY * physics.power; ball.rotationSpeed = ball.vx * 0.035; input.powerX = 0; input.powerY = 0; throwState.enemyHitThisThrow = false; throwState.cupHitThisThrow = false; throwState.hitObjectsThisThrow.clear(); } function updateBall() { if (!ball.isFlying) return; if (gameState.isFinished) return; ball.vy += physics.gravity; ball.vx *= physics.air; ball.vy *= physics.air; ball.x += ball.vx; ball.y += ball.vy; ball.rotation += ball.rotationSpeed; ball.rotationSpeed *= 0.992; updateBallScale(); const tableTopY = scene.table.y + (isMobile ? 120 : 90); const tableLeftX = scene.table.x + (isMobile ? 65 : 53); const tableRightX = scene.table.x + scene.table.w - (isMobile ? 65 : 60); const ballBottom = ball.y + ball.size; const ballCenterX = ball.x + ball.size / 2; const onTableX = ballCenterX > tableLeftX && ballCenterX < tableRightX; if (ballBottom >= tableTopY && onTableX && ball.vy > 0) { ball.y = tableTopY - ball.size; ball.vy *= -physics.bounce; ball.vx *= 0.96; ball.rotationSpeed += ball.vx * 0.01; if (Math.abs(ball.vy) < 1.4) { ball.vy = 0; } } if (ball.x < -200 || ball.x > BASE_WIDTH + 200 || ball.y > BASE_HEIGHT + 200) { resetBall(); return; } if (ball.y + ball.size >= BASE_HEIGHT - 20) { ball.y = BASE_HEIGHT - 20 - ball.size; ball.vy *= -physics.bounce; ball.vx *= physics.floorFriction; ball.rotationSpeed *= 0.9; if (Math.abs(ball.vx) < 0.4 && Math.abs(ball.vy) < 0.8) { resetBall(); return; } } if (Math.abs(ball.vx) < 0.03 && Math.abs(ball.vy) < 0.03 && ball.y > tableTopY - ball.size - 2) { resetBall(); } } function getCupHitbox(cup) { if (isMobile) { return { x: cup.x + 4, y: cup.y - 4, w: cup.w - 8, h: 20 }; } return { x: cup.x + 6, y: cup.y - 2, w: cup.w - 10, h: 16 }; } function circleIntersectsRect(cx, cy, r, rect) { const nearestX = Math.max(rect.x, Math.min(cx, rect.x + rect.w)); const nearestY = Math.max(rect.y, Math.min(cy, rect.y + rect.h)); const dx = cx - nearestX; const dy = cy - nearestY; return dx * dx + dy * dy <= r * r; } function triggerDecorHit(target) { const obj = scene[target.key]; if (!obj || !obj.fx || obj.fx.hidden) return; if (obj.fx.hitCooldown > 0) return; obj.fx.hitCooldown = 12; if (target.type === 'leaf') { if (!obj.fx.fallActive) { obj.fx.fallActive = true; obj.fx.fallSpeed = 2.8; obj.fx.fallRotation = 0; obj.fx.fallDirection = obj.x < BASE_WIDTH / 2 ? -1 : 1; } } if (target.type === 'smallPicture') { if (!obj.fx.fallActive) { obj.fx.fallActive = true; obj.fx.fallSpeed = 3.8; obj.fx.fallRotation = 0; obj.fx.fallDirection = obj.x < BASE_WIDTH / 2 ? -1 : 1; } } if (target.type === 'bigPicture') { const hitKey = `bigPicture:${target.key}`; if (throwState.hitObjectsThisThrow.has(hitKey)) { return; } throwState.hitObjectsThisThrow.add(hitKey); obj.fx.hitCount += 1; if (obj.fx.hitCount === 1) { obj.fx.tiltAngle = obj.x < BASE_WIDTH / 2 ? -0.14 : 0.14; } else if (obj.fx.hitCount === 2) { obj.fx.tiltAngle = obj.x < BASE_WIDTH / 2 ? 0.12 : -0.12; } else if (obj.fx.hitCount >= 3 && !obj.fx.fallActive) { obj.fx.fallActive = true; obj.fx.fallSpeed = 3.4; obj.fx.fallRotation = 0; obj.fx.fallDirection = obj.x < BASE_WIDTH / 2 ? -1 : 1; } } if (target.type === 'enemy') { if (!throwState.enemyHitThisThrow) { throwState.enemyHitThisThrow = true; enemyState.spriteKey = 'enemyConfused'; obj.fx.shakeTime = 14; } } } function checkDecorHits() { if (!ball.isFlying) return; const cx = ball.x + ball.size / 2; const cy = ball.y + ball.size / 2; const r = ball.radius * 0.9; for (const target of decorTargets) { const obj = scene[target.key]; if (!obj || !obj.fx || obj.fx.hidden) continue; const hb = target.hitbox(); const hit = circleIntersectsRect(cx, cy, r, hb); if (!hit) continue; triggerDecorHit(target); } } function checkCupHits() { if (!ball.isFlying) return; const cx = ball.x + ball.size / 2; const cy = ball.y + ball.size / 2; for (const cup of scene.cups) { if (!cup.visible || cup.hit) continue; const hb = getCupHitbox(cup); const insideX = cx > hb.x && cx < hb.x + hb.w; const insideY = cy > hb.y && cy < hb.y + hb.h; const fallingDown = ball.vy > 0.09; const cleanShot = !throwState.enemyHitThisThrow; if (insideX && insideY && fallingDown && cleanShot) { cup.hit = true; cup.shakeTime = 22; throwState.cupHitThisThrow = true; enemyState.spriteKey = 'enemyCalm2'; ball.isFlying = false; ball.vx = 0; ball.vy = 0; setTimeout(() => { cup.visible = false; cup.hit = false; cup.shakeTime = 0; resetBall(); }, 260); break; } } } function updateFxItem(item, type) { if (!item.fx) return; const fx = item.fx; if (fx.hitCooldown > 0) fx.hitCooldown -= 1; if (fx.shakeTime > 0) fx.shakeTime -= 1; if ((type === 'smallPicture' || type === 'leaf' || type === 'bigPicture') && fx.fallActive && !fx.hidden) { item.y += fx.fallSpeed; if (type === 'leaf') { fx.fallSpeed += 0.18; fx.fallRotation += 0.035 * fx.fallDirection; } else if (type === 'smallPicture') { fx.fallSpeed += 0.28; fx.fallRotation += 0.055 * fx.fallDirection; } else if (type === 'bigPicture') { fx.fallSpeed += 0.22; fx.fallRotation += 0.045 * fx.fallDirection; } if (item.y > BASE_HEIGHT + 140) { fx.hidden = true; fx.fallActive = false; } } } function updateDecorEffects() { updateFxItem(scene.plantLeafLeft, 'leaf'); updateFxItem(scene.plantLeafRight, 'leaf'); updateFxItem(scene.plantLeafBottom, 'leaf'); updateFxItem(scene.leftQ, 'smallPicture'); updateFxItem(scene.rightW, 'smallPicture'); updateFxItem(scene.rightR, 'smallPicture'); updateFxItem(scene.leftSun, 'bigPicture'); updateFxItem(scene.leftCat, 'bigPicture'); updateFxItem(scene.rightDuck, 'bigPicture'); updateFxItem(scene.enemy, 'enemy'); } function updateCups() { scene.cups.forEach(cup => { if (cup.shakeTime > 0) { cup.shakeTime -= 1; } }); } function drawImageSafe(img, obj, offsetX = 0, offsetY = 0) { ctx.drawImage(img, obj.x + offsetX, obj.y + offsetY, obj.w, obj.h); } function drawObjectWithFx(img, obj, mode = 'normal') { if (!obj) return; if (obj.fx && obj.fx.hidden) return; const fx = obj.fx || {}; let drawX = obj.x; let drawY = obj.y; let rotation = 0; if (mode === 'shake' && fx.shakeTime > 0) { const isEnemy = obj === scene.enemy; const ampX = isEnemy ? 2.2 : 1.0; const ampY = isEnemy ? 0.7 : 0.35; drawX += Math.sin(fx.shakeTime * 1.5) * ampX; drawY += Math.cos(fx.shakeTime * 1.1) * ampY; } if (mode === 'tilt') { rotation += fx.tiltAngle || 0; } if (mode === 'fall') { rotation += fx.fallRotation || 0; } if (rotation !== 0) { const cx = drawX + obj.w / 2; const cy = drawY + obj.h / 2; ctx.save(); ctx.translate(cx, cy); ctx.rotate(rotation); ctx.drawImage(img, -obj.w / 2, -obj.h / 2, obj.w, obj.h); ctx.restore(); return; } ctx.drawImage(img, drawX, drawY, obj.w, obj.h); } function drawBall() { const cx = ball.x + ball.size / 2; const cy = ball.y + ball.size / 2; ctx.save(); ctx.translate(cx, cy); ctx.rotate(ball.rotation); ctx.drawImage(images.ball, -ball.size / 2, -ball.size / 2, ball.size, ball.size); ctx.restore(); } function drawAttempts() { const iconSize = isMobile ? 40 : 28; const gapX = 10; const gapY = 10; const cols = 5; const rows = 2; const totalWidth = cols * iconSize + (cols - 1) * gapX; const startX = BASE_WIDTH - totalWidth - (isMobile ? 40 : 40); const startY = isMobile ? 40 : 40; for (let i = 0; i < rows * cols; i += 1) { const col = i % cols; const row = Math.floor(i / cols); const x = startX + col * (iconSize + gapX); const y = startY + row * (iconSize + gapY); ctx.save(); ctx.globalAlpha = i < gameState.attemptsLeft ? 1 : 0.22; ctx.drawImage(images.ball, x, y, iconSize, iconSize); ctx.restore(); } } function drawQuickRestartButton() { const iconSize = isMobile ? 40 : 28; const gapX = 10; const gapY = 10; const cols = 5; const totalWidth = cols * iconSize + (cols - 1) * gapX; const startX = BASE_WIDTH - totalWidth - (isMobile ? 40 : 40); const startY = isMobile ? 40 : 40; const x = startX + totalWidth - iconSize; const y = startY + (iconSize + gapY) * 2; uiState.restartBtnRect = { x, y, w: iconSize, h: iconSize }; ctx.drawImage(images.reload, x, y, iconSize, iconSize); } function drawQuestionButton() { const iconSize = isMobile ? 40 : 28; const margin = isMobile ? 40 : 40; const y = BASE_HEIGHT - iconSize - margin; const x = margin; uiState.questionBtnRect = { x, y, w: iconSize, h: iconSize }; ctx.drawImage(images.question, x, y, iconSize, iconSize); } function drawAimLine() { if (!ball.isDragging) return; const cx = ball.x + ball.size / 2; const cy = ball.y + ball.size / 2; const aimX = cx + input.powerX; const aimY = cy + input.powerY; const strength = Math.min(Math.sqrt(input.powerX ** 2 + input.powerY ** 2) / physics.maxDrag, 1); ctx.save(); ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(aimX, aimY); ctx.strokeStyle = 'rgba(255,255,255,0.9)'; ctx.lineWidth = 6; ctx.stroke(); ctx.beginPath(); ctx.arc(aimX, aimY, 10 + strength * 8, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255, 120, 120, 0.9)'; ctx.fill(); ctx.beginPath(); ctx.arc(cx, cy, 56 + strength * 18, Math.PI * 0.8, Math.PI * 1.25); ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 10; ctx.stroke(); ctx.restore(); } function drawScene() { ctx.clearRect(0, 0, BASE_WIDTH, BASE_HEIGHT); drawImageSafe(images.bg, scene.bg); drawObjectWithFx(images[enemyState.spriteKey], scene.enemy, 'shake'); drawObjectWithFx(images.leftSun, scene.leftSun, scene.leftSun.fx.fallActive ? 'fall' : 'tilt'); drawObjectWithFx(images.leftQ, scene.leftQ, 'fall'); drawObjectWithFx(images.leftCat, scene.leftCat, scene.leftCat.fx.fallActive ? 'fall' : 'tilt'); drawObjectWithFx(images.rightDuck, scene.rightDuck, scene.rightDuck.fx.fallActive ? 'fall' : 'tilt'); drawObjectWithFx(images.rightW, scene.rightW, 'fall'); drawObjectWithFx(images.rightR, scene.rightR, 'fall'); drawImageSafe(images.plantBody, scene.plantBody); drawObjectWithFx(images.plantLeafLeft, scene.plantLeafLeft, 'fall'); drawObjectWithFx(images.plantLeafRight, scene.plantLeafRight, 'fall'); drawObjectWithFx(images.plantLeafBottom, scene.plantLeafBottom, 'fall'); drawImageSafe(images.pillows, scene.pillows); drawImageSafe(images.table, scene.table); scene.cups.forEach(cup => { if (!cup.visible) return; let shakeOffsetX = 0; if (cup.shakeTime > 0) { shakeOffsetX = Math.sin(cup.shakeTime * 1.9) * 4; } drawImageSafe(images.cup, cup, shakeOffsetX, 0); }); drawAttempts(); drawQuickRestartButton(); drawQuestionButton(); drawAimLine(); drawBall(); } function pointInRect(px, py, rect) { if (!rect) return false; return px >= rect.x && px <= rect.x + rect.w && py >= rect.y && py <= rect.y + rect.h; } function loop() { updateBall(); checkDecorHits(); checkCupHits(); updateDecorEffects(); updateCups(); drawScene(); requestAnimationFrame(loop); } function openFastRules() { if (fastRules) { fastRules.classList.remove('u-display-none'); } } function closeFastRules() { if (fastRules) { fastRules.classList.add('u-display-none'); } } canvas.addEventListener('mousedown', onPointerDown); canvas.addEventListener('mousemove', onPointerMove); window.addEventListener('mouseup', onPointerUp); canvas.addEventListener('touchstart', onPointerDown, { passive: false }); canvas.addEventListener('touchmove', onPointerMove, { passive: false }); window.addEventListener('touchend', onPointerUp, { passive: false }); if (startGameBtn) { startGameBtn.addEventListener('click', startGame); } if (restartGameBtn) { restartGameBtn.addEventListener('click', restartGame); } if (fastRulesCloser) { fastRulesCloser.addEventListener('click', closeFastRules); } document.addEventListener('click', (event) => { if (ignoreNextDocumentClick) { ignoreNextDocumentClick = false; return; } if (!fastRules || fastRules.classList.contains('u-display-none')) return; if (fastRules.contains(event.target)) return; closeFastRules(); }); preloadAssets() .then(() => { resetGame(); loop(); }) .catch((error) => { console.error('Game assets failed to load:', error); }); })();