/
DOLOR3SS
/
mdmdmd
Обзор
Документация
Войти
/
DOLOR3SS
/
mdmdmd
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
index.html
3 138 строк
140 KB
DOLOR3SS
Create: index.html
04 июл 2026, 23:22
Верифицирован
04 июл 2026, 23:22
729efad
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>Любимому другу :3</title> <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet"> <style> * { margin: 0; padding: 0; box-sizing: border-box; image-rendering: pixelated; user-select: none; -webkit-tap-highlight-color: transparent; } :root { --pixel-font: 'Press Start 2P', monospace; --color-bg: #3b3936; --color-primary: #576a71; --color-secondary: #96b8cb; --color-accent: #b6cad1; --color-light: #e2e5e2; --color-border: #96b8cb; --color-text: #e2e5e2; --color-text-muted: rgba(226, 229, 226, 0.6); } html, body { width: 100%; height: 100%; overflow: hidden; background: #1a1a1a; touch-action: none; } body { font-family: var(--pixel-font); background: var(--color-bg); display: flex; justify-content: center; align-items: center; color: var(--color-text); touch-action: none; } #gameContainer { position: relative; width: 1000px; height: 650px; max-width: 100vw; max-height: 100vh; background: var(--color-bg); border: 4px solid var(--color-border); border-radius: 8px; overflow: hidden; box-shadow: 0 0 60px rgba(150, 184, 203, 0.1); touch-action: none; } @media (orientation: portrait) { #gameContainer { width: 100vw; height: 56.25vw; max-height: 100vh; border-radius: 0; border-width: 2px; } } @media (max-width: 1024px) and (orientation: landscape) { #gameContainer { width: 100vw; height: 56.25vw; max-height: 100vh; border-radius: 0; border-width: 2px; } } @media (max-width: 768px) and (orientation: landscape) { #gameContainer { width: 100vw; height: 56.25vw; max-height: 100vh; border-radius: 0; border-width: 2px; } } .screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; opacity: 1; transition: opacity 0.6s ease; background: var(--color-bg); } .screen.hidden { opacity: 0; pointer-events: none; display: none; } .gothic-btn { background: transparent; border: 3px solid var(--color-border); color: var(--color-text); padding: 12px 32px; font-family: var(--pixel-font); font-size: 11px; letter-spacing: 2px; cursor: pointer; transition: all 0.3s ease; text-transform: uppercase; border-radius: 4px; background: rgba(87, 106, 113, 0.15); touch-action: manipulation; } .gothic-btn:hover:not(:disabled) { background: var(--color-secondary); color: var(--color-bg); box-shadow: 0 0 30px rgba(150, 184, 203, 0.2); transform: scale(1.05); } .gothic-btn:disabled { opacity: 0.3; cursor: not-allowed; transform: none; } .gothic-btn.small { padding: 8px 18px; font-size: 9px; letter-spacing: 1px; } .gothic-btn.danger { border-color: #8b5a5a; color: #8b5a5a; } .gothic-btn.danger:hover:not(:disabled) { background: #8b5a5a; color: var(--color-text); } .gothic-btn::before { content: '◆'; margin-right: 10px; font-size: 10px; color: var(--color-secondary); } .gothic-btn::after { content: '◆'; margin-left: 10px; font-size: 10px; color: var(--color-secondary); } .gothic-btn.small::before, .gothic-btn.small::after { content: '◇'; font-size: 8px; } .gothic-input { background: rgba(87, 106, 113, 0.15); border: 3px solid var(--color-border); color: var(--color-text); padding: 10px 20px; font-family: var(--pixel-font); font-size: 10px; border-radius: 4px; width: 220px; text-align: center; transition: all 0.3s ease; } .gothic-input:focus { outline: none; border-color: var(--color-secondary); box-shadow: 0 0 30px rgba(150, 184, 203, 0.1); background: rgba(87, 106, 113, 0.25); } .gothic-input::placeholder { color: rgba(226, 229, 226, 0.3); font-size: 8px; } .auth-container { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; justify-content: center; } .auth-error { color: #8b5a5a; font-size: 9px; width: 100%; text-align: center; margin-top: 4px; transition: all 0.3s ease; } .auth-error.hidden { opacity: 0; transform: translateY(-10px); } .menu-buttons { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; } .menu-subtitle { color: var(--color-text-muted); font-size: 9px; letter-spacing: 4px; opacity: 0.6; font-family: var(--pixel-font); } #mainMenu { background: radial-gradient(ellipse at center, #2a2a28 0%, var(--color-bg) 80%); } .menu-content { display: flex; flex-direction: column; align-items: center; gap: 16px; padding: 20px; } .game-title { font-size: 44px; color: var(--color-text); text-shadow: 0 0 60px rgba(150, 184, 203, 0.1); letter-spacing: 4px; line-height: 1.2; text-align: center; font-family: var(--pixel-font); } .game-title::after { content: '✦'; display: block; font-size: 18px; color: var(--color-secondary); margin-top: -4px; } .character-preview { width: 120px; height: 140px; display: flex; justify-content: center; align-items: center; position: relative; cursor: pointer; } .character { position: relative; width: 60px; height: 100px; transition: transform 0.3s ease; } .character-body { position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 40px; height: 50px; background: #4a4a48; border-radius: 4px 4px 2px 2px; border: 3px solid var(--color-border); } .character-head { position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 35px; height: 40px; background: #4a4a48; border-radius: 50% 50% 40% 40%; border: 3px solid var(--color-border); } .eye { position: absolute; width: 6px; height: 8px; background: var(--color-light); top: 12px; } .left-eye { left: 7px; } .right-eye { right: 7px; } .thought-bubble { position: absolute; top: -50px; left: 50%; transform: translateX(-50%) scale(0.8); background: rgba(87, 106, 113, 0.15); border: 3px solid var(--color-border); padding: 6px 14px; border-radius: 4px; font-size: 9px; color: var(--color-text); white-space: nowrap; opacity: 0; transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); font-family: var(--pixel-font); } .thought-bubble::after { content: ''; position: absolute; bottom: -8px; left: 50%; transform: translateX(-50%); border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 8px solid rgba(87, 106, 113, 0.15); } .character-preview:hover .thought-bubble { opacity: 1; transform: translateX(-50%) scale(1); } .character-preview:hover .character { animation: jumpPreview 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); } @keyframes jumpPreview { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-30px); } } #rulesScreen { background: radial-gradient(ellipse at center, #2a2a28 0%, var(--color-bg) 80%); } .rules-container { display: flex; width: 85%; height: 60%; gap: 20px; align-items: center; justify-content: center; border: 3px solid rgba(150, 184, 203, 0.15); padding: 20px; border-radius: 4px; } .rules-left, .rules-right { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; } .rules-center { flex: 2; max-width: 40%; } .rules-text-wrapper { background: rgba(87, 106, 113, 0.08); border: 3px solid rgba(150, 184, 203, 0.1); border-radius: 4px; padding: 8px; height: 100%; max-height: 250px; } .rules-text-scroll { max-height: 230px; overflow-y: auto; padding: 12px 16px; color: var(--color-text); } .rules-text-scroll::-webkit-scrollbar { width: 4px; } .rules-text-scroll::-webkit-scrollbar-thumb { background: var(--color-secondary); border-radius: 2px; } .rules-text-scroll h2 { color: var(--color-light); font-size: 14px; letter-spacing: 3px; margin-bottom: 12px; font-family: var(--pixel-font); } .rules-text-scroll ul { list-style: none; padding: 0; } .rules-text-scroll ul li { padding: 4px 0; padding-left: 20px; position: relative; font-size: 9px; line-height: 1.8; color: var(--color-text-muted); border-bottom: 1px solid rgba(150, 184, 203, 0.05); font-family: var(--pixel-font); } .rules-text-scroll ul li::before { content: '✦'; position: absolute; left: 0; color: var(--color-secondary); font-size: 8px; } .pipe-entrance { width: 40px; height: 80px; background: linear-gradient(180deg, var(--color-secondary), var(--color-primary)); border-radius: 20px 20px 4px 4px; border: 3px solid var(--color-border); position: relative; box-shadow: 0 4px 30px rgba(150, 184, 203, 0.1); } .pipe-entrance::after { content: ''; position: absolute; top: -4px; left: -6px; right: -6px; height: 10px; background: var(--color-light); border-radius: 6px 6px 0 0; opacity: 0.2; } .pipe-label { color: var(--color-text-muted); font-size: 7px; margin-top: 6px; letter-spacing: 2px; opacity: 0.5; font-family: var(--pixel-font); } #rulesCharacter { transform: scale(1.2); } #rulesStartBtn { margin-top: 16px; } #gameScreen { background: var(--color-bg); padding: 0; } #gameCanvas { width: 100%; height: 100%; display: block; image-rendering: pixelated; touch-action: none; } .hud { position: absolute; top: 12px; left: 20px; right: 20px; display: flex; justify-content: space-between; align-items: flex-start; font-size: 9px; color: var(--color-text); letter-spacing: 1px; pointer-events: none; font-family: var(--pixel-font); background: rgba(59, 57, 54, 0.8); padding: 8px 16px; border-radius: 4px; border: 2px solid rgba(150, 184, 203, 0.1); z-index: 20; } .hud-left { display: flex; flex-direction: column; gap: 4px; } .hud-right { display: flex; gap: 16px; align-items: center; } .hud span { text-shadow: 0 0 20px rgba(0, 0, 0, 0.5); font-size: 9px; } .health-bar-container { display: flex; align-items: center; gap: 8px; background: rgba(0, 0, 0, 0.2); padding: 2px 10px 2px 6px; border-radius: 4px; border: 2px solid rgba(150, 184, 203, 0.1); } .health-icon { font-size: 10px; color: #ff6b6b; } .health-bar-outer { width: 80px; height: 6px; background: rgba(255, 107, 107, 0.1); border-radius: 3px; overflow: hidden; border: 2px solid rgba(255, 107, 107, 0.05); } .health-bar-inner { height: 100%; width: 100%; background: linear-gradient(90deg, #ff6b6b, #ff8787); border-radius: 3px; transition: width 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); box-shadow: 0 0 20px rgba(255, 107, 107, 0.1); } .health-bar-inner.low { background: linear-gradient(90deg, #ff4444, #ff6b6b); animation: healthPulse 0.8s ease-in-out infinite; } @keyframes healthPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .health-text { font-size: 7px; color: var(--color-text-muted); min-width: 20px; text-align: center; font-family: var(--pixel-font); } .level-notification { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(1); pointer-events: none; z-index: 10; opacity: 0; transition: all 0.8s cubic-bezier(0.34, 1.56, 0.64, 1); } .level-notification.visible { opacity: 1; transform: translate(-50%, -50%) scale(1); } .level-notification.fading { opacity: 0; transform: translate(-50%, -60%) scale(1.05); } .level-number { font-size: 36px; font-family: var(--pixel-font); color: var(--color-light); padding: 16px 36px; border: 4px solid var(--color-border); background: rgba(59, 57, 54, 0.85); border-radius: 8px; box-shadow: 0 0 40px rgba(150, 184, 203, 0.1), inset 0 0 40px rgba(150, 184, 203, 0.05); letter-spacing: 3px; white-space: nowrap; } .star-notification { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.8); font-size: 16px; color: var(--color-light); pointer-events: none; z-index: 10; opacity: 0; transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); background: rgba(59, 57, 54, 0.85); padding: 16px 32px; border-radius: 8px; border: 3px solid var(--color-border); text-align: center; font-family: var(--pixel-font); box-shadow: 0 8px 40px rgba(150, 184, 203, 0.05); } .star-notification.visible { opacity: 1; transform: translate(-50%, -60%) scale(1); } .star-notification .star-icon { font-size: 28px; display: block; margin-bottom: 4px; filter: drop-shadow(0 0 20px rgba(150, 184, 203, 0.2)); } .star-notification .compliment { font-size: 12px; color: var(--color-light); font-family: var(--pixel-font); } .achievement-notification { position: absolute; bottom: 80px; left: 50%; transform: translateX(-50%) translateY(20px); font-size: 10px; color: var(--color-light); background: rgba(59, 57, 54, 0.9); border: 3px solid var(--color-border); padding: 10px 24px; border-radius: 4px; pointer-events: none; z-index: 10; opacity: 0; transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); font-family: var(--pixel-font); text-align: center; max-width: 80%; } .achievement-notification.visible { opacity: 1; transform: translateX(-50%) translateY(0); } #deathScreen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 100; opacity: 0; pointer-events: none; transition: opacity 0.6s ease; } #deathScreen.visible { opacity: 1; pointer-events: all; } .death-content { text-align: center; transform: scale(0.9); transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); } #deathScreen.visible .death-content { transform: scale(1); } .death-icon { font-size: 60px; animation: deathPulse 1.5s ease-in-out infinite; } @keyframes deathPulse { 0%, 100% { transform: scale(1); opacity: 0.8; } 50% { transform: scale(1.1); opacity: 1; } } .death-content h2 { color: #ff6b6b; font-size: 28px; font-family: var(--pixel-font); letter-spacing: 4px; margin: 12px 0; text-shadow: 0 0 40px rgba(255, 107, 107, 0.2); } .death-content p { color: var(--color-text-muted); font-size: 10px; margin-bottom: 20px; opacity: 0.7; font-family: var(--pixel-font); } .death-stats { display: flex; gap: 30px; margin: 12px 0 20px; color: var(--color-text-muted); font-size: 9px; font-family: var(--pixel-font); } .death-stats span { display: flex; align-items: center; gap: 6px; } .death-stats strong { color: var(--color-light); } #achievementsScreen { background: radial-gradient(ellipse at center, #2a2a28 0%, var(--color-bg) 80%); } .achievements-container { max-width: 80%; width: 100%; max-height: 80%; display: flex; flex-direction: column; align-items: center; gap: 16px; } .achievements-container h2 { color: var(--color-light); font-size: 18px; font-family: var(--pixel-font); letter-spacing: 3px; } .achievements-list { width: 100%; max-height: 400px; overflow-y: auto; padding: 12px 16px; border: 3px solid rgba(150, 184, 203, 0.1); border-radius: 4px; background: rgba(0, 0, 0, 0.2); display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } .achievements-list::-webkit-scrollbar { width: 4px; } .achievements-list::-webkit-scrollbar-thumb { background: var(--color-secondary); border-radius: 2px; } .achievement-item { display: flex; align-items: center; gap: 10px; padding: 8px 12px; background: rgba(87, 106, 113, 0.08); border-radius: 4px; border: 2px solid rgba(150, 184, 203, 0.05); transition: all 0.3s ease; } .achievement-item.unlocked { border-color: rgba(150, 184, 203, 0.2); background: rgba(150, 184, 203, 0.05); } .achievement-item.locked { opacity: 0.4; } .achievement-item .ach-icon { font-size: 16px; } .achievement-item .ach-info { flex: 1; } .achievement-item .ach-name { color: var(--color-text); font-size: 9px; font-family: var(--pixel-font); } .achievement-item .ach-desc { color: var(--color-text-muted); font-size: 7px; opacity: 0.7; font-family: var(--pixel-font); } .achievement-item .ach-status { font-size: 9px; color: var(--color-text-muted); } .achievement-item.unlocked .ach-status { color: var(--color-secondary); } #completionScreen { background: radial-gradient(ellipse at center, #2a2a28 0%, var(--color-bg) 80%); } .completion-content { display: flex; flex-direction: column; align-items: center; gap: 16px; max-width: 70%; text-align: center; } .final-star { font-size: 60px; animation: starPulse 1.5s ease-in-out infinite; } @keyframes starPulse { 0%, 100% { transform: scale(1); opacity: 1; text-shadow: 0 0 60px rgba(150, 184, 203, 0.3); } 50% { transform: scale(1.15); opacity: 0.8; text-shadow: 0 0 100px rgba(150, 184, 203, 0.5); } } .completion-content h2 { color: var(--color-light); font-size: 22px; font-family: var(--pixel-font); letter-spacing: 4px; } .completion-text-scroll { max-height: 200px; overflow-y: auto; padding: 12px 20px; color: var(--color-text); font-size: 10px; line-height: 2; border: 3px solid rgba(150, 184, 203, 0.1); border-radius: 4px; background: rgba(0, 0, 0, 0.2); font-family: var(--pixel-font); } .completion-text-scroll::-webkit-scrollbar { width: 4px; } .completion-text-scroll::-webkit-scrollbar-thumb { background: var(--color-secondary); border-radius: 2px; } .sound-controls { position: absolute; bottom: 15px; right: 15px; display: flex; gap: 8px; z-index: 50; } .sound-btn { background: rgba(59, 57, 54, 0.8); border: 2px solid rgba(150, 184, 203, 0.15); color: var(--color-text); width: 34px; height: 34px; border-radius: 4px; cursor: pointer; font-size: 16px; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; font-family: var(--pixel-font); } .sound-btn:hover { background: rgba(150, 184, 203, 0.1); border-color: var(--color-border); transform: scale(1.05); } .sound-btn.muted { opacity: 0.4; } #editorScreen { background: radial-gradient(ellipse at center, #2a2a28 0%, var(--color-bg) 80%); } .editor-container { display: flex; flex-direction: column; align-items: center; gap: 12px; max-width: 90%; width: 100%; max-height: 90%; } .editor-preview { display: flex; gap: 40px; align-items: center; justify-content: center; flex-wrap: wrap; background: rgba(0, 0, 0, 0.3); border-radius: 8px; padding: 16px 24px; border: 2px solid rgba(150, 184, 203, 0.1); } .editor-preview canvas { image-rendering: pixelated; width: 120px; height: 180px; background: rgba(0, 0, 0, 0.2); border-radius: 4px; border: 2px solid rgba(150, 184, 203, 0.2); cursor: pointer; transition: transform 0.3s ease; } .editor-preview canvas:hover { transform: scale(1.05); animation: jumpPreview 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); } .editor-controls { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; } .editor-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; } .editor-row label { font-size: 9px; color: var(--color-text-muted); font-family: var(--pixel-font); min-width: 60px; } .editor-row select, .editor-row input[type="color"] { background: rgba(87, 106, 113, 0.15); border: 2px solid var(--color-border); color: var(--color-text); padding: 4px 8px; font-family: var(--pixel-font); font-size: 8px; border-radius: 4px; } .editor-row select option { background: #2a2a28; } .editor-row input[type="color"] { width: 40px; height: 30px; padding: 2px; cursor: pointer; } .editor-row input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; } .editor-row input[type="color"]::-webkit-color-swatch { border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 4px; } .editor-actions { display: flex; gap: 12px; margin-top: 4px; flex-wrap: wrap; justify-content: center; } @keyframes damageFlash { 0%, 100% { filter: brightness(1); } 25% { filter: brightness(1.5); } 50% { filter: brightness(0.7); } 75% { filter: brightness(1.3); } } .damage-flash { animation: damageFlash 0.3s ease; } @media (max-width: 1024px) { #gameContainer { width: 95vw; height: 80vh; border-radius: 4px; } .game-title { font-size: 28px; } } @media (max-width: 768px) { .game-title { font-size: 20px; } .rules-container { flex-direction: column; height: auto; padding: 12px; } .rules-center { max-width: 100%; width: 100%; } .rules-left, .rules-right { display: none; } .level-number { font-size: 20px; padding: 10px 16px; } .hud { font-size: 7px; padding: 4px 8px; flex-wrap: wrap; top: 6px; left: 8px; right: 8px; } .health-bar-outer { width: 50px; } .achievements-list { grid-template-columns: 1fr; max-height: 300px; } .menu-buttons { flex-direction: column; align-items: center; } .auth-container { flex-direction: column; } .gothic-input { width: 100%; max-width: 200px; } .death-content h2 { font-size: 20px; } .game-title { font-size: 24px; } .editor-preview { flex-direction: column; gap: 16px; } } @media (max-width: 480px) { .game-title { font-size: 16px; } .gothic-btn { padding: 8px 16px; font-size: 8px; } .gothic-btn.small { padding: 6px 12px; font-size: 7px; } .hud { font-size: 6px; padding: 3px 6px; } .health-bar-outer { width: 35px; } .level-number { font-size: 14px; padding: 8px 12px; } } #mobileControls { position: absolute; bottom: 10px; left: 0; right: 0; display: none; justify-content: space-between; padding: 0 20px; pointer-events: none; z-index: 30; } .mobile-btn-group { display: flex; gap: 12px; pointer-events: all; } .mobile-btn { width: 60px; height: 60px; border-radius: 50%; background: rgba(87, 106, 113, 0.5); border: 3px solid rgba(150, 184, 203, 0.4); color: var(--color-text); font-size: 24px; display: flex; align-items: center; justify-content: center; touch-action: none; user-select: none; -webkit-user-select: none; backdrop-filter: blur(4px); transition: all 0.15s ease; } .mobile-btn:active { transform: scale(0.85); background: rgba(150, 184, 203, 0.4); } .mobile-btn.jump-btn { width: 70px; height: 70px; font-size: 28px; background: rgba(255, 107, 107, 0.3); border-color: rgba(255, 107, 107, 0.5); } .mobile-btn.jump-btn:active { background: rgba(255, 107, 107, 0.6); } @media (pointer: coarse) { #mobileControls { display: flex; } } @media (max-width: 768px) { .mobile-btn { width: 50px; height: 50px; font-size: 20px; } .mobile-btn.jump-btn { width: 60px; height: 60px; font-size: 24px; } } @media (max-width: 480px) { .mobile-btn { width: 40px; height: 40px; font-size: 16px; } .mobile-btn.jump-btn { width: 50px; height: 50px; font-size: 20px; } #mobileControls { padding: 0 10px; bottom: 5px; } } </style> </head> <body> <div id="gameContainer"> <div id="mainMenu" class="screen"> <div class="menu-content"> <h1 class="game-title">Любимомууу<br>:3</h1> <div class="auth-container"> <input type="text" id="playerNameInput" placeholder="Введи своё имя..." maxlength="20" class="gothic-input"> <button id="authButton" class="gothic-btn small">Войти</button> <div id="authError" class="auth-error hidden">Это имя уже занято!</div> </div> <div class="character-preview" id="characterPreview"> <div class="character" id="menuCharacter"> <div class="character-body"></div> <div class="character-head"> <div class="eye left-eye"></div> <div class="eye right-eye"></div> </div> <div class="thought-bubble">Привет!!</div> </div> </div> <div class="menu-buttons"> <button id="startButton" class="gothic-btn" disabled>Начать</button> <button id="editorMenuBtn" class="gothic-btn small">🎨 Скин</button> <button id="achievementsMenuBtn" class="gothic-btn small">🏆 Достижения</button> <button id="resetProgressBtn" class="gothic-btn small danger">🔄 Сбросить</button> </div> <p class="menu-subtitle">~ путешествие начинается ~</p> </div> </div> <div id="rulesScreen" class="screen hidden"> <div class="rules-container"> <div class="rules-left"> <div class="character" id="rulesCharacter"> <div class="character-body"></div> <div class="character-head"> <div class="eye left-eye"></div> <div class="eye right-eye"></div> </div> </div> </div> <div class="rules-center"> <div class="rules-text-wrapper"> <div class="rules-text-scroll"> <h2>Правила игры</h2> <ul> <li>Управляй стрелками или WASD</li> <li>Прыгай по блокам (Пробел или W)</li> <li>Собирай звёздочки (1-3 на уровне)</li> <li>У тебя 5 здоровья и 3 жизни</li> <li>Избегай шипов на платформах</li> <li>Запрыгни на трубу сверху, чтобы пройти уровень</li> <li>Всего 30 уровней и 50 звёзд</li> </ul> </div> </div> </div> <div class="rules-right"> <div class="pipe-entrance"></div><span class="pipe-label">→ Вход</span> </div> </div> <button id="rulesStartBtn" class="gothic-btn">Начать игру</button> </div> <div id="gameScreen" class="screen hidden"> <canvas id="gameCanvas"></canvas> <div id="hud" class="hud"> <div class="hud-left"> <span id="playerNameDisplay">Игрок</span> <div class="health-bar-container"> <span class="health-icon">❤️</span> <div class="health-bar-outer"><div class="health-bar-inner" id="healthBar" style="width:100%"></div></div> <span class="health-text" id="healthText">5/5</span> </div> </div> <div class="hud-right"> <span id="levelDisplay">Уровень: 1</span> <span id="starsDisplay">⭐ 0</span> <span id="achievementsDisplay">🏆 0</span> <span id="livesDisplay">❤️ x3</span> </div> </div> <div class="sound-controls"> <button id="musicToggle" class="sound-btn">🎵</button> <button id="soundToggle" class="sound-btn">🔊</button> </div> <div id="levelNotification" class="level-notification hidden"><span class="level-number">Уровень №1</span></div> <div id="starNotification" class="star-notification hidden"></div> <div id="achievementNotification" class="achievement-notification hidden"></div> <div id="mobileControls"> <div class="mobile-btn-group"> <button class="mobile-btn" id="btnLeft">◀</button> <button class="mobile-btn" id="btnRight">▶</button> </div> <div class="mobile-btn-group"> <button class="mobile-btn jump-btn" id="btnJump">⬆</button> </div> </div> </div> <div id="deathScreen"> <div class="death-content"> <div class="death-icon">💀</div> <h2>Ты погиб!</h2> <p>Попробуй снова! ✨</p> <div class="death-stats"> <span>⭐ Собрано: <strong id="deathStars">0</strong></span> <span>🏆 Уровень: <strong id="deathLevel">0</strong></span> <span>❤️ Жизней: <strong id="deathLives">0</strong></span> </div> <div> <button id="retryButton" class="gothic-btn small">🔄 Попробовать</button> <button id="menuFromDeathBtn" class="gothic-btn small danger">🏠 В меню</button> </div> </div> </div> <div id="achievementsScreen" class="screen hidden"> <div class="achievements-container"> <h2>🏆 Мои достижения</h2> <div class="achievements-list" id="achievementsList"></div> <button id="achievementsBackBtn" class="gothic-btn small">Назад</button> </div> </div> <div id="editorScreen" class="screen hidden"> <div class="editor-container"> <h2 style="font-size:16px; font-family:var(--pixel-font); letter-spacing:2px;">🎨 Редактор персонажа</h2> <div class="editor-preview"> <canvas id="editorCanvas" width="120" height="180"></canvas> <div class="editor-controls"> <div class="editor-row"><label>Форма</label><select id="headShapeSelect"><option value="round">Круглая</option><option value="square">Квадрат</option><option value="triangle">Треуг</option></select></div> <div class="editor-row"><label>Обводка</label><input type="color" id="outlineColor" value="#96b8cb"></div> <div class="editor-row"><label>Одежда</label><select id="clothingSelect"><option value="tshirt">Футболка</option><option value="hoodie">Кофта</option><option value="dress">Платье</option><option value="pants">Штаны</option><option value="shorts">Шорты</option><option value="overalls">Комбинезон</option><option value="suit">Костюм</option><option value="cape">Плащ</option><option value="vest">Жилет</option><option value="sweater">Свитер</option><option value="jacket">Куртка</option><option value="coat">Пальто</option><option value="tunic">Туника</option><option value="kimono">Кимоно</option></select></div> <div class="editor-row"><label>Цвет одежды</label><input type="color" id="clothingColor" value="#4a4a48"></div> <div class="editor-row"><label>Головной убор</label><select id="hatSelect"><option value="none">Нет</option><option value="beanie">Шапка</option><option value="tophat">Цилиндр</option><option value="cap">Кепка</option><option value="crown">Корона</option><option value="headband">Повязка</option><option value="beret">Берет</option><option value="cowboy">Ковбой</option><option value="hood">Капюшон</option></select></div> <div class="editor-row"><label>Цвет убора</label><input type="color" id="hatColor" value="#576a71"></div> </div> </div> <div class="editor-actions"><button id="editorBackBtn" class="gothic-btn small">Назад</button></div> </div> </div> <div id="completionScreen" class="screen hidden"> <div class="completion-content"> <div class="final-star">⭐</div> <h2>Поздравляю, <span id="completionPlayerName">друг</span>!</h2> <div class="completion-text-scroll"> <p>Ты стал для меня по-настоящему близким человеком, и это настоящее чудо. С самого начала нашего общения ты показался мне весёлым, родным и понимающим — и с каждым днём это только укрепляется.</p> <p>С тобой легко и спокойно: я могу быть собой, делиться сокровенным, смеяться до слёз или говорить о серьёзном — и знать, что меня услышат и не осудят. Ты показал мне, какой может быть настоящая д и забота — и я это очень ценю.</p> <p>Спасибо за твоё терпение, доброту и поддержку. За то, что всегда готов выслушать, подбодрить и заставить улыбнуться в хмурый день. Благодаря тебе я знаю, каково это — встречаться с человеком, которому можно доверять безоговорочно.</p> <p style="color: #96b8cb; margin-top: 8px;">спасибаааа тебе люблю тебя оченьь :3</p> </div> <button id="restartButton" class="gothic-btn">Начать заново</button> </div> </div> </div> <script> class SoundManager { constructor() { this.enabled = true; this.musicEnabled = true; this.audioContext = null; this.isInitialized = false; this.initAudio(); } initAudio() { try { this.audioContext = new(window.AudioContext || window.webkitAudioContext)(); this.isInitialized = true; } catch (e) { this.enabled = false; this.musicEnabled = false; } } playSound(name, volume = 0.3) { if (!this.enabled || !this.isInitialized) return; try { const osc = this.audioContext.createOscillator(); const gain = this.audioContext.createGain(); const settings = { star: { freq: 880, dur: 0.1, type: 'sine' }, compliment: { freq: 660, dur: 0.12, type: 'sine' }, achievement: { freq: 523, dur: 0.15, type: 'square' }, jump: { freq: 300, dur: 0.08, type: 'sine' }, hit: { freq: 150, dur: 0.3, type: 'sawtooth' }, levelComplete: { freq: 523, dur: 0.1, type: 'sine' }, finalStar: { freq: 523, dur: 0.2, type: 'sine' }, button: { freq: 300, dur: 0.1, type: 'sine' }, menuHover: { freq: 440, dur: 0.06, type: 'sine' }, transition: { freq: 200, dur: 0.2, type: 'sine' }, death: { freq: 200, dur: 0.8, type: 'sawtooth' } }; const cfg = settings[name] || settings.button; osc.type = cfg.type; osc.frequency.value = cfg.freq; gain.gain.setValueAtTime(volume, this.audioContext.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, this.audioContext.currentTime + cfg.dur); osc.connect(gain); gain.connect(this.audioContext.destination); osc.start(); osc.stop(this.audioContext.currentTime + cfg.dur); } catch (e) {} } toggleSound() { this.enabled = !this.enabled; return this.enabled; } toggleMusic() { this.musicEnabled = !this.musicEnabled; return this.musicEnabled; } playStarSound() { this.playSound('star', 0.25); setTimeout(() => this.playSound('compliment', 0.2), 200); } playAchievementSound() { this.playSound('achievement', 0.3); } playJumpSound() { this.playSound('jump', 0.15); } playLevelCompleteSound() { this.playSound('levelComplete', 0.25); } playFinalStarSound() { this.playSound('finalStar', 0.3); } playButtonSound() { this.playSound('button', 0.15); } playMenuHoverSound() { this.playSound('menuHover', 0.1); } playScreenTransitionSound() { this.playSound('transition', 0.15); } playDeathSound() { this.playSound('death', 0.35); } } const soundManager = new SoundManager(); document.addEventListener('click', () => { if (soundManager.audioContext && soundManager.audioContext.state === 'suspended') { soundManager.audioContext.resume(); } }, { once: false }); const compliments = [ 'Ты очень милии!!', 'Ты прекрасный!!', 'Ты самое лучшее что было со мной!!', 'Ты очень красивыый!!', 'Ты — чудо!', 'Ты очень добрии!!', 'Ты самый лучший!!', 'Ты шикарныый!!', 'Ты очень хорошиий!!', 'Ты заботливыый!', 'Ты делаешь мою жизнь лучше!', 'Ты очень пиздатый!', 'Ты моя мечтаа! :3', 'Ты очень крутойй!!', 'Ты единственный кому я доверяю', 'Ты идеален!', 'Спасибо тебе за твое существование', 'Ты безумно дорог мне', 'Ты ослепителен!', 'Ты — совершенство!', 'Ты очень круто шутишь!', 'я тебя очень ценю!', 'Ты безумно интересная личность!', 'Ты мой подарочеек!!', 'Ты котеноооок!!', 'Ты похож на ангелочкаа!!', 'Ты очень щедрый!', 'Ты лучик света!', 'Ты очень ласковыый!!', 'я рада каждому твоему сообщению!', 'Ты уникален :3', 'я за тебя жизнь отдам.', 'Ты самый лучший в мире', 'Ты безумно привлекателен!', 'С тобой легко и спокойно!', 'Ты очень весёлии', 'Ты понимающий', 'Ты бесценен!', 'Ты надежный!', 'Твоя душа прекрасна!', 'Ты очень вайбовый челиик', 'я тебя очень люблю!', 'Ты очень забавный!', 'Ты пресеть пряям!!', 'Ты очень умныый! ты всё сможеешь!', 'Ты — лучший!' ]; const achievements = [ { id: 'first_star', name: 'Первая звезда', desc: 'Собери свою первую звезду', icon: '⭐' }, { id: 'star_collector', name: 'Коллекционер', desc: 'Собери 10 звёзд', icon: '🌟' }, { id: 'star_hunter', name: 'Охотник', desc: 'Собери 25 звёзд', icon: '🌠' }, { id: 'star_legend', name: 'Легенда', desc: 'Собери все 50 звёзд', icon: '🌌' }, { id: 'level_5', name: 'Путешественник', desc: 'Пройди 5 уровней', icon: '🚶' }, { id: 'level_15', name: 'Исследователь', desc: 'Пройди 15 уровней', icon: '🔍' }, { id: 'level_30', name: 'Покоритель', desc: 'Пройди все 30 уровней', icon: '🏔️' }, { id: 'jump_master', name: 'Мастер прыжка', desc: 'Соверши 100 прыжков', icon: '🦘' }, { id: 'no_hit', name: 'Неуязвимый', desc: 'Пройди 5 уровней без шипов', icon: '🛡️' }, { id: 'survivor', name: 'Выживший', desc: 'Погибни 10 раз', icon: '💪' }, { id: 'dedicated', name: 'Преданный друг', desc: 'Наиграй 30 минут', icon: '⏰' }, { id: 'final_star', name: 'Последняя звезда', desc: 'Собери финальную звезду', icon: '💫' }, { id: 'perfect_game', name: 'Идеальная игра', desc: 'Пройди игру с честью', icon: '🏅' }, { id: 'friend_forever', name: 'Друг навсегда', desc: 'Заверши игру с любовью', icon: '❤️' }, { id: 'never_give_up', name: 'Никогда не сдавайся', desc: 'Погибни 5 раз на уровне и пройди его', icon: '🔥' } ]; const levelPalettes = [ { bg: '#0d1310', primary: '#482b15', secondary: '#bd9480', accent: '#a5978a', light: '#bfbeba' }, { bg: '#7c8992', primary: '#aab1b1', secondary: '#806d5c', accent: '#9b8c7d', light: '#d7d2c4' }, { bg: '#565741', primary: '#a07565', secondary: '#4a5a6c', accent: '#bbbbbb', light: '#e3e3e3' }, { bg: '#9b5e61', primary: '#e3a09a', secondary: '#567f75', accent: '#96aba2', light: '#e9e7e5' }, { bg: '#2a081f', primary: '#7d0d51', secondary: '#994c76', accent: '#797876', light: '#f7d1e2' }, { bg: '#151a1a', primary: '#38443f', secondary: '#545b33', accent: '#7f8954', light: '#9eaba3' }, { bg: '#9b415e', primary: '#c14c7a', secondary: '#d581aa', accent: '#d2aac4', light: '#f2dee9' }, { bg: '#3b3936', primary: '#576a71', secondary: '#96b8cb', accent: '#b6cad1', light: '#e2e5e2' }, { bg: '#040000', primary: '#78353b', secondary: '#a65363', accent: '#a8825c', light: '#d7d0be' }, { bg: '#5a5840', primary: '#888a7f', secondary: '#b9b6a7', accent: '#c4caca', light: '#e2e2e2' }, { bg: '#71521e', primary: '#bc8f4d', secondary: '#d5c3a1', accent: '#ecdeb5', light: '#e6e6e6' }, { bg: '#551f38', primary: '#8f653b', secondary: '#967c7f', accent: '#bdb2c3', light: '#cbcbcc' }, { bg: '#a6b196', primary: '#bec6b1', secondary: '#d3dbcd', accent: '#e0e8e0', light: '#e8efe7' }, { bg: '#2b2b32', primary: '#654748', secondary: '#7e3a36', accent: '#abb7c2', light: '#c2c6cc' }, { bg: '#475651', primary: '#48616b', secondary: '#768064', accent: '#8ba8b1', light: '#c0d4d8' }, { bg: '#56349b', primary: '#a97aeb', secondary: '#b3a6f6', accent: '#c789b9', light: '#f2cdd9' }, { bg: '#6d2520', primary: '#a45e57', secondary: '#aa928d', accent: '#e1dfd0', light: '#b3b9b9' }, { bg: '#e30122', primary: '#5f2222', secondary: '#88413e', accent: '#967169', light: '#d6aa87' }, { bg: '#2f2820', primary: '#846e60', secondary: '#a98460', accent: '#d1b99d', light: '#cfc8c0' }, { bg: '#161616', primary: '#36332f', secondary: '#695f5d', accent: '#ac9e9b', light: '#e7e3e0' }, { bg: '#23281b', primary: '#343a53', secondary: '#648ba4', accent: '#a8a090', light: '#caccbb' }, { bg: '#754368', primary: '#b66da2', secondary: '#df85a9', accent: '#f6c5ca', light: '#f7eae9' }, { bg: '#563734', primary: '#986769', secondary: '#cbb6c1', accent: '#ccae95', light: '#e7d2ac' }, { bg: '#1c1c1f', primary: '#571a19', secondary: '#8b2323', accent: '#5e5f5c', light: '#b0b2af' }, { bg: '#4a0d0e', primary: '#930e12', secondary: '#115e3e', accent: '#8b8f22', light: '#dec3a5' }, { bg: '#413836', primary: '#5f574c', secondary: '#7b776f', accent: '#5b3b2a', light: '#c4b8a8' }, { bg: '#ae5d5c', primary: '#eb908f', secondary: '#abaaba', accent: '#b6d1ef', light: '#e5dfe9' }, { bg: '#16202b', primary: '#114665', secondary: '#720f32', accent: '#7b445a', light: '#d1d1d6' }, { bg: '#5d5a53', primary: '#959595', secondary: '#95c8cc', accent: '#b9cece', light: '#e5e3de' }, { bg: '#b1979e', primary: '#ecb8a6', secondary: '#fcd6c1', accent: '#dce0e9', light: '#ede0dc' } ]; function safeColor(color, fallback = '#888888') { if (typeof color === 'string' && /^#[0-9a-fA-F]{6}$/.test(color)) return color; return fallback; } class SaveManager { constructor() { this.storageKey = 'game_save_data'; } savePlayerData(pn, data) { try { const all = this.getAllData(); all[pn] = { ...all[pn], ...data, lastSaved: Date.now() }; localStorage.setItem(this.storageKey, JSON.stringify(all)); return true; } catch (e) { return false; } } loadPlayerData(pn) { try { const all = this.getAllData(); return all[pn] || null; } catch (e) { return null; } } getAllData() { try { const data = localStorage.getItem(this.storageKey); return data ? JSON.parse(data) : {}; } catch (e) { return {}; } } playerExists(pn) { const all = this.getAllData(); return !!all[pn]; } resetPlayerData(pn) { try { const all = this.getAllData(); delete all[pn]; localStorage.setItem(this.storageKey, JSON.stringify(all)); return true; } catch (e) { return false; } } } const saveManager = new SaveManager(); const gameState = { currentLevel: 0, stars: 0, totalStars: 0, unlockedAchievements: [], levelsCompleted: 0, jumps: 0, noHitLevels: 0, finalStarCollected: false, playerName: null, player: null, levels: [], isGameActive: false, playTime: 0, usedCompliments: [], levelComplete: false, health: 5, maxHealth: 5, lives: 3, deaths: 0, isDead: false, isInvincible: false, invincibleTimer: 0, deathCountOnLevel: 0, skin: { headShape: 'round', outlineColor: '#96b8cb', clothing: 'tshirt', clothingColor: '#4a4a48', hat: 'none', hatColor: '#576a71' } }; class Player { constructor() { this.x = 50; this.y = 350; this.width = 28; this.height = 42; this.vx = 0; this.vy = 0; this.speed = 3.5; this.baseSpeed = 3.5; this.jumpPower = -16; this.baseJumpPower = -16; this.gravity = 0.5; this.onGround = false; this.facingRight = true; this.isJumping = false; this.walkFrame = 0; this.walkTimer = 0; this.isMoving = false; this.smoothX = this.x; this.smoothY = this.y; this.eyeBlinkTimer = 0; this.eyeOpen = true; this.clothingOffset = 0; this.eyeDirection = 1; // 1 - right, -1 - left } update(level) { if (gameState.isDead) return; this.vy += this.gravity; this.x += this.vx; this.y += this.vy; if (this.y > level.height + 80) { this.x = level.spawnX || 50; this.y = level.spawnY || 350; this.smoothX = this.x; this.smoothY = this.y; this.vx = 0; this.vy = 0; this.onGround = false; return; } if (this.x < 0) this.x = 0; if (this.x + this.width > level.width) this.x = level.width - this.width; if (this.y + this.height > level.height) { this.y = level.height - this.height; this.vy = 0; this.onGround = true; this.isJumping = false; } this.onGround = false; for (const platform of level.platforms) { if (this.collidesWith(platform)) { if (this.vy > 0) { this.y = platform.y - this.height; this.vy = 0; this.onGround = true; this.isJumping = false; } else if (this.vy < 0) { this.y = platform.y + platform.height; this.vy = 0; } } } for (const spike of level.spikes) { if (this.collidesWith(spike)) { this.die(level); return; } } for (const pipe of level.pipes) { if (pipe.isEntrance && !gameState.levelComplete) { const playerBottom = this.y + this.height; if (playerBottom >= pipe.y - 5 && playerBottom <= pipe.y + 15 && this.x + this.width > pipe .x + 5 && this.x < pipe.x + pipe.width - 5 && this.vy >= -1) { gameState.levelComplete = true; if (gameState.currentLevel < 29) setTimeout(() => showLevelComplete(), 300); else setTimeout(() => showFinalStar(), 300); return; } } } for (const star of level.stars) { if (!star.collected && this.collidesWith(star)) { star.collected = true; gameState.stars++; gameState.totalStars++; let c = compliments[Math.floor(Math.random() * compliments.length)]; gameState.usedCompliments.push(c); star.compliment = c; showStarNotification(star); checkAchievements(); } } if (gameState.invincibleTimer > 0) { gameState.invincibleTimer -= 1 / 60; if (gameState.invincibleTimer <= 0) gameState.isInvincible = false; } if (Math.abs(this.vx) > 0.5 && this.onGround) { this.isMoving = true; this.walkTimer++; if (this.walkTimer > 8) { this.walkTimer = 0; this.walkFrame = (this.walkFrame + 1) % 4; } } else { this.isMoving = false; this.walkFrame = 0; this.walkTimer = 0; } this.smoothX += (this.x - this.smoothX) * 0.15; this.smoothY += (this.y - this.smoothY) * 0.15; this.eyeBlinkTimer += 1/60; if (this.eyeBlinkTimer > 3.0) { this.eyeOpen = !this.eyeOpen; this.eyeBlinkTimer = 0; } // Направление глаз if (this.vx > 0.5) this.eyeDirection = 1; else if (this.vx < -0.5) this.eyeDirection = -1; // Анимация одежды при прыжке и движении if (this.isJumping || !this.onGround) { this.clothingOffset = Math.sin(Date.now() / 200) * 3; } else if (this.isMoving) { this.clothingOffset = Math.sin(this.walkTimer * 0.4) * 2; } else { this.clothingOffset *= 0.9; } } collidesWith(obj) { return this.x < obj.x + obj.width && this.x + this.width > obj.x && this.y < obj.y + obj.height && this .y + this.height > obj.y; } die(level) { if (gameState.isDead) return; gameState.isDead = true; gameState.deaths++; gameState.lives--; gameState.deathCountOnLevel++; this.speed = this.baseSpeed; this.jumpPower = this.baseJumpPower; this.vx = 0; this.vy = 0; soundManager.playDeathSound(); if (gameState.deathCountOnLevel >= 5 && !gameState.unlockedAchievements.includes('never_give_up')) { gameState.unlockedAchievements.push('never_give_up'); showAchievementNotification(achievements.find(a => a.id === 'never_give_up')); } if (gameState.deaths >= 10 && !gameState.unlockedAchievements.includes('survivor')) { gameState.unlockedAchievements.push('survivor'); showAchievementNotification(achievements.find(a => a.id === 'survivor')); } showDeathScreen(); saveProgress(); } respawn(level) { if (gameState.lives <= 0) { gameState.lives = 3; gameState.health = gameState.maxHealth; gameState.currentLevel = 0; gameState.levelsCompleted = 0; gameState.stars = 0; gameState.totalStars = 0; gameState.deaths = 0; gameState.deathCountOnLevel = 0; this.speed = this.baseSpeed; this.jumpPower = this.baseJumpPower; hideDeathScreen(); document.getElementById('gameScreen').classList.add('hidden'); document.getElementById('mainMenu').classList.remove('hidden'); gameState.isDead = false; gameState.isGameActive = false; saveProgress(); updateHealthDisplay(); return; } gameState.isDead = false; gameState.isInvincible = true; gameState.invincibleTimer = 1.5; gameState.health = gameState.maxHealth; this.speed = this.baseSpeed; this.jumpPower = this.baseJumpPower; this.vx = 0; this.vy = 0; this.x = level.spawnX || 50; this.y = level.spawnY || 350; this.smoothX = this.x; this.smoothY = this.y; for (const star of level.stars) { if (star.collected) { star.collected = false; gameState.stars--; gameState.totalStars--; } } updateHealthDisplay(); hideDeathScreen(); gameState.isGameActive = true; loadLevel(gameState.currentLevel); saveProgress(); } jump() { if (this.onGround && !gameState.isDead) { this.vy = this.jumpPower; this.onGround = false; this.isJumping = true; gameState.jumps++; soundManager.playJumpSound(); } } moveLeft() { if (!gameState.isDead) { this.vx = -this.speed; this.facingRight = false; } } moveRight() { if (!gameState.isDead) { this.vx = this.speed; this.facingRight = true; } } stop() { this.vx = 0; } draw(ctx) { const skin = gameState.skin; if (gameState.isDead) ctx.globalAlpha = 0.3; if (gameState.isInvincible && !gameState.isDead) { if (Math.sin(Date.now() / 100) > 0) ctx.globalAlpha = 0.5; } const dx = this.smoothX, dy = this.smoothY; ctx.save(); ctx.translate(dx + this.width / 2, dy + this.height / 2); if (!this.facingRight) ctx.scale(-1, 1); ctx.translate(-this.width / 2, -this.height / 2); // Тело (основа) ctx.fillStyle = '#4a4a48'; ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 2; ctx.beginPath(); ctx.roundRect(2, 18, this.width - 4, this.height - 20, 3); ctx.fill(); ctx.stroke(); // Одежда с анимацией прыжка и движения const clothOffset = this.clothingOffset; const jumpSquash = this.isJumping || !this.onGround ? Math.sin(Date.now() / 200) * 2 : 0; ctx.fillStyle = skin.clothingColor || '#4a4a48'; ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; const cw = this.width - 4, ch = this.height - 20, cx = 2, cy = 18; const bottomCloth = skin.clothing === 'pants' || skin.clothing === 'shorts' || skin.clothing === 'overalls' || skin.clothing === 'suit' || skin.clothing === 'tshirt' || skin.clothing === 'hoodie' || skin.clothing === 'sweater' || skin.clothing === 'jacket' || skin.clothing === 'coat' || skin.clothing === 'tunic' || skin.clothing === 'kimono'; // Верхняя часть одежды if (skin.clothing === 'tshirt') { ctx.beginPath(); ctx.roundRect(cx + 2, cy + 2 + jumpSquash, cw - 4, ch - 8 - jumpSquash, 2); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(this.width/2 - 4, cy + 2 + jumpSquash); ctx.lineTo(this.width/2, cy - 1 + jumpSquash); ctx.lineTo(this.width/2 + 4, cy + 2 + jumpSquash); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(cx + 4, cy + ch - 6 + clothOffset + jumpSquash); ctx.lineTo(this.width - cx - 4, cy + ch - 6 + clothOffset + jumpSquash); ctx.stroke(); } else if (skin.clothing === 'hoodie') { ctx.beginPath(); ctx.roundRect(cx, cy + jumpSquash, cw, ch - 4 - jumpSquash, 4); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.roundRect(this.width/2 - 6, cy + ch/2 - 2 + clothOffset + jumpSquash, 12, 6, 2); ctx.stroke(); ctx.beginPath(); ctx.arc(this.width/2, cy - 2 + jumpSquash, 8, 0, Math.PI); ctx.stroke(); } else if (skin.clothing === 'dress') { ctx.beginPath(); ctx.moveTo(cx + 4, cy + 4 + jumpSquash); ctx.quadraticCurveTo(cx, cy + 12 + jumpSquash, cx + 4, cy + ch - 2 + clothOffset + jumpSquash); ctx.lineTo(this.width - cx - 4, cy + ch - 2 + clothOffset + jumpSquash); ctx.quadraticCurveTo(this.width - cx, cy + 12 + jumpSquash, this.width - cx - 4, cy + 4 + jumpSquash); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(cx + 6, cy + 12 + clothOffset + jumpSquash); ctx.lineTo(this.width - cx - 6, cy + 12 + clothOffset + jumpSquash); ctx.stroke(); } else if (skin.clothing === 'pants') { ctx.beginPath(); ctx.roundRect(cx + 2, cy + ch - 10 + clothOffset + jumpSquash, 8, 10, 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.roundRect(this.width - cx - 10, cy + ch - 10 + clothOffset + jumpSquash, 8, 10, 2); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(cx + 2, cy + ch - 10 + clothOffset + jumpSquash); ctx.lineTo(this.width - cx - 2, cy + ch - 10 + clothOffset + jumpSquash); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 0.5; ctx.beginPath(); ctx.moveTo(cx + 6, cy + ch - 8 + clothOffset + jumpSquash); ctx.lineTo(cx + 6, cy + ch - 2 + clothOffset + jumpSquash); ctx.stroke(); ctx.beginPath(); ctx.moveTo(this.width - cx - 6, cy + ch - 8 + clothOffset + jumpSquash); ctx.lineTo(this.width - cx - 6, cy + ch - 2 + clothOffset + jumpSquash); ctx.stroke(); } else if (skin.clothing === 'shorts') { ctx.beginPath(); ctx.roundRect(cx + 2, cy + ch - 6 + clothOffset + jumpSquash, 8, 6, 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.roundRect(this.width - cx - 10, cy + ch - 6 + clothOffset + jumpSquash, 8, 6, 2); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(cx + 3, cy + ch - 6 + clothOffset + jumpSquash); ctx.lineTo(this.width - cx - 3, cy + ch - 6 + clothOffset + jumpSquash); ctx.stroke(); } else if (skin.clothing === 'overalls') { ctx.beginPath(); ctx.roundRect(cx + 2, cy + 2 + jumpSquash, cw - 4, ch - 6 - jumpSquash, 2); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(cx + 8, cy + 2 + jumpSquash); ctx.lineTo(cx + 8, cy - 4 + jumpSquash); ctx.stroke(); ctx.beginPath(); ctx.moveTo(this.width - cx - 8, cy + 2 + jumpSquash); ctx.lineTo(this.width - cx - 8, cy - 4 + jumpSquash); ctx.stroke(); ctx.beginPath(); ctx.roundRect(this.width/2 - 5, cy + ch/2 - 1 + clothOffset + jumpSquash, 10, 5, 2); ctx.stroke(); } else if (skin.clothing === 'suit') { ctx.beginPath(); ctx.roundRect(cx + 2, cy + 2 + jumpSquash, cw - 4, ch - 4 - jumpSquash, 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#ff6b6b'; ctx.fillRect(this.width/2 - 3, cy + 6 + jumpSquash, 6, 14); ctx.fillRect(this.width/2 - 5, cy + 18 + jumpSquash, 10, 4); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(cx + 2, cy + ch - 6 + clothOffset + jumpSquash); ctx.lineTo(cx + 2, cy + ch + 2 + jumpSquash); ctx.stroke(); ctx.beginPath(); ctx.moveTo(this.width - cx - 2, cy + ch - 6 + clothOffset + jumpSquash); ctx.lineTo(this.width - cx - 2, cy + ch + 2 + jumpSquash); ctx.stroke(); } else if (skin.clothing === 'cape') { ctx.beginPath(); ctx.moveTo(cx - 2, cy + 2 + jumpSquash); ctx.quadraticCurveTo(cx - 6 + clothOffset, cy + ch / 2 + jumpSquash, cx - 2 + clothOffset, cy + ch + jumpSquash); ctx.lineTo(this.width - cx + 2 + clothOffset, cy + ch + jumpSquash); ctx.quadraticCurveTo(this.width - cx + 6 - clothOffset, cy + ch / 2 + jumpSquash, this.width - cx + 2, cy + 2 + jumpSquash); ctx.fill(); ctx.stroke(); } else if (skin.clothing === 'vest') { ctx.beginPath(); ctx.roundRect(cx + 4, cy + 2 + jumpSquash, cw - 8, ch - 4 - jumpSquash, 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#e2e5e2'; for (let i = 0; i < 3; i++) { ctx.beginPath(); ctx.arc(this.width/2, cy + 6 + i*6 + clothOffset + jumpSquash, 1.5, 0, Math.PI*2); ctx.fill(); } } else if (skin.clothing === 'sweater') { ctx.beginPath(); ctx.roundRect(cx, cy + jumpSquash, cw, ch - 2 - jumpSquash, 6); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1; for (let i = 0; i < 4; i++) { ctx.beginPath(); ctx.moveTo(cx + 4 + i*8, cy + 6 + clothOffset + jumpSquash); ctx.lineTo(cx + 4 + i*8, cy + ch - 4 + clothOffset + jumpSquash); ctx.stroke(); } ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(cx + 2, cy + ch - 2 + clothOffset + jumpSquash); ctx.lineTo(this.width - cx - 2, cy + ch - 2 + clothOffset + jumpSquash); ctx.stroke(); } else if (skin.clothing === 'jacket') { ctx.beginPath(); ctx.roundRect(cx, cy + jumpSquash, cw, ch - 4 - jumpSquash, 4); ctx.fill(); ctx.stroke(); ctx.strokeStyle = '#e2e5e2'; ctx.lineWidth = 1.5; ctx.setLineDash([2, 2]); ctx.beginPath(); ctx.moveTo(this.width/2, cy + 4 + jumpSquash); ctx.lineTo(this.width/2, cy + ch - 4 + clothOffset + jumpSquash); ctx.stroke(); ctx.setLineDash([]); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(cx + 2, cy + ch - 2 + clothOffset + jumpSquash); ctx.lineTo(this.width - cx - 2, cy + ch - 2 + clothOffset + jumpSquash); ctx.stroke(); } else if (skin.clothing === 'coat') { ctx.beginPath(); ctx.roundRect(cx - 1, cy - 2 + jumpSquash, cw + 2, ch + 2 - jumpSquash, 6); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(cx, cy + ch/2 + clothOffset + jumpSquash); ctx.lineTo(this.width - cx, cy + ch/2 + clothOffset + jumpSquash); ctx.stroke(); ctx.fillStyle = '#e2e5e2'; for (let i = 0; i < 4; i++) { ctx.beginPath(); ctx.arc(this.width/2, cy + 6 + i*8 + clothOffset + jumpSquash, 1.5, 0, Math.PI*2); ctx.fill(); } } else if (skin.clothing === 'tunic') { ctx.beginPath(); ctx.roundRect(cx, cy + jumpSquash, cw, ch - jumpSquash, 2); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(cx + 4, cy + 4 + clothOffset + jumpSquash); ctx.lineTo(this.width - cx - 4, cy + 4 + clothOffset + jumpSquash); ctx.stroke(); ctx.beginPath(); ctx.moveTo(cx + 4, cy + ch - 4 + clothOffset + jumpSquash); ctx.lineTo(this.width - cx - 4, cy + ch - 4 + clothOffset + jumpSquash); ctx.stroke(); } else if (skin.clothing === 'kimono') { ctx.beginPath(); ctx.roundRect(cx, cy + jumpSquash, cw, ch - jumpSquash, 4); ctx.fill(); ctx.stroke(); ctx.fillStyle = skin.outlineColor || '#96b8cb'; ctx.fillRect(this.width/2 - 6, cy + 8 + clothOffset + jumpSquash, 12, 6); ctx.fillStyle = skin.clothingColor || '#4a4a48'; ctx.beginPath(); ctx.roundRect(cx - 4, cy + 4 + clothOffset + jumpSquash, 4, 12, 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.roundRect(this.width - cx, cy + 4 + clothOffset + jumpSquash, 4, 12, 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#e2e5e2'; ctx.fillRect(this.width/2 - 8, cy + 10 + clothOffset + jumpSquash, 16, 2); } // Ноги для одежды без штанов if (!bottomCloth || skin.clothing === 'dress' || skin.clothing === 'cape' || skin.clothing === 'tshirt' || skin.clothing === 'hoodie' || skin.clothing === 'sweater' || skin.clothing === 'jacket' || skin.clothing === 'coat' || skin.clothing === 'tunic' || skin.clothing === 'kimono' || skin.clothing === 'vest' || skin.clothing === 'overalls') { if (skin.clothing !== 'dress' && skin.clothing !== 'cape' && skin.clothing !== 'coat') { ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 2; const legOff = this.walkFrame % 2 === 0 ? 0 : 3; ctx.beginPath(); ctx.moveTo(6, this.height - 4); ctx.lineTo(4 + legOff, this.height + 5); ctx.stroke(); ctx.beginPath(); ctx.moveTo(this.width - 6, this.height - 4); ctx.lineTo(this.width - 4 - legOff, this.height + 5); ctx.stroke(); } } // Голова ctx.fillStyle = '#4a4a48'; ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 2; const headSize = 14, headY = 2; if (skin.headShape === 'round') { ctx.beginPath(); ctx.arc(this.width / 2, headY + headSize, headSize, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } else if (skin.headShape === 'square') { ctx.beginPath(); ctx.roundRect(this.width / 2 - headSize, headY, headSize * 2, headSize * 2, 3); ctx.fill(); ctx.stroke(); } else if (skin.headShape === 'triangle') { ctx.beginPath(); ctx.moveTo(this.width / 2, headY); ctx.lineTo(this.width / 2 - headSize, headY + headSize * 2); ctx.lineTo(this.width / 2 + headSize, headY + headSize * 2); ctx.closePath(); ctx.fill(); ctx.stroke(); } // Глаза с направлением взгляда const eyeY = headY + headSize - 2; const eyeOffset = this.eyeDirection * 1.5; if (this.eyeOpen) { ctx.fillStyle = '#e2e5e2'; ctx.beginPath(); ctx.arc(this.width / 2 - 5 + eyeOffset, eyeY, 3.5, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(this.width / 2 + 5 + eyeOffset, eyeY, 3.5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#3b3936'; ctx.beginPath(); ctx.arc(this.width / 2 - 3.5 + eyeOffset, eyeY + 0.5, 2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(this.width / 2 + 3.5 + eyeOffset, eyeY + 0.5, 2, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'white'; ctx.beginPath(); ctx.arc(this.width / 2 - 4.5 + eyeOffset, eyeY - 1.5, 1, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(this.width / 2 + 4.5 + eyeOffset, eyeY - 1.5, 1, 0, Math.PI * 2); ctx.fill(); } else { ctx.strokeStyle = '#e2e5e2'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(this.width / 2 - 7 + eyeOffset, eyeY); ctx.lineTo(this.width / 2 - 3 + eyeOffset, eyeY); ctx.stroke(); ctx.beginPath(); ctx.moveTo(this.width / 2 + 3 + eyeOffset, eyeY); ctx.lineTo(this.width / 2 + 7 + eyeOffset, eyeY); ctx.stroke(); } // Головной убор if (skin.hat !== 'none') { ctx.fillStyle = skin.hatColor || '#576a71'; ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; if (skin.hat === 'beanie') { ctx.beginPath(); ctx.ellipse(this.width / 2, headY + 4, 12, 8, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#ff6b6b'; ctx.beginPath(); ctx.arc(this.width/2, headY - 2, 3, 0, Math.PI*2); ctx.fill(); } else if (skin.hat === 'tophat') { ctx.beginPath(); ctx.roundRect(this.width / 2 - 8, headY - 12, 16, 10, 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.roundRect(this.width / 2 - 12, headY - 4, 24, 4, 2); ctx.fill(); ctx.stroke(); } else if (skin.hat === 'cap') { ctx.beginPath(); ctx.ellipse(this.width / 2, headY + 3, 14, 6, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.moveTo(this.width/2 - 2, headY + 6); ctx.lineTo(this.width/2 + 12, headY + 6); ctx.lineTo(this.width/2 + 8, headY + 10); ctx.closePath(); ctx.fill(); ctx.stroke(); } else if (skin.hat === 'crown') { ctx.beginPath(); ctx.moveTo(this.width / 2 - 12, headY + 6); ctx.lineTo(this.width / 2 - 12, headY - 4); ctx.lineTo(this.width / 2 - 8, headY + 2); ctx.lineTo(this.width / 2 - 4, headY - 6); ctx.lineTo(this.width / 2, headY + 2); ctx.lineTo(this.width / 2 + 4, headY - 6); ctx.lineTo(this.width / 2 + 8, headY + 2); ctx.lineTo(this.width / 2 + 12, headY - 4); ctx.lineTo(this.width / 2 + 12, headY + 6); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#ff6b6b'; ctx.beginPath(); ctx.arc(this.width/2 - 6, headY - 2, 2, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#4ecdc4'; ctx.beginPath(); ctx.arc(this.width/2, headY - 4, 2, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#ffe66d'; ctx.beginPath(); ctx.arc(this.width/2 + 6, headY - 2, 2, 0, Math.PI*2); ctx.fill(); } else if (skin.hat === 'headband') { ctx.beginPath(); ctx.ellipse(this.width / 2, headY + 8, 12, 4, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } else if (skin.hat === 'beret') { ctx.beginPath(); ctx.ellipse(this.width / 2, headY + 2, 14, 8, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } else if (skin.hat === 'cowboy') { ctx.beginPath(); ctx.ellipse(this.width / 2, headY + 8, 14, 6, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.moveTo(this.width/2 - 16, headY + 6); ctx.quadraticCurveTo(this.width/2, headY - 2, this.width/2 + 16, headY + 6); ctx.stroke(); } else if (skin.hat === 'hood') { ctx.beginPath(); ctx.arc(this.width / 2, headY - 2, 16, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } } // Руки с анимацией ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 2; if (this.isJumping || !this.onGround) { const armSwing = Math.sin(Date.now() / 200) * 4; ctx.beginPath(); ctx.moveTo(2, 20 + jumpSquash); ctx.lineTo(-5 + armSwing, 10 + jumpSquash); ctx.stroke(); ctx.beginPath(); ctx.moveTo(this.width - 2, 20 + jumpSquash); ctx.lineTo(this.width + 5 - armSwing, 10 + jumpSquash); ctx.stroke(); } else if (this.isMoving) { const swing = Math.sin(this.walkTimer * 0.5) * 3; ctx.beginPath(); ctx.moveTo(2, 22); ctx.lineTo(-2 + swing, 32 + Math.abs(swing)); ctx.stroke(); ctx.beginPath(); ctx.moveTo(this.width - 2, 22); ctx.lineTo(this.width + 2 - swing, 32 + Math.abs(swing)); ctx.stroke(); } else { ctx.beginPath(); ctx.moveTo(2, 22); ctx.lineTo(-2, 32); ctx.stroke(); ctx.beginPath(); ctx.moveTo(this.width - 2, 22); ctx.lineTo(this.width + 2, 32); ctx.stroke(); } ctx.restore(); ctx.globalAlpha = 1; } } function generateLevel(levelIndex) { const palette = levelPalettes[levelIndex % levelPalettes.length]; const level = { width: 1000, height: 600, palette, platforms: [], pipes: [], stars: [], spikes: [], decor: [], spawnX: 50, spawnY: 350 }; level.platforms.push({ x: 0, y: 560, width: level.width, height: 40 }); const seed = levelIndex * 7 + 13; const rng = (() => { let s = seed; return () => { s = (s * 9301 + 49297) % 233280; return s / 233280; }; })(); const pipeWidth = 45 + rng() * 25, pipeHeight = 55 + rng() * 25; const pipeX = 100 + rng() * (level.width - 200), pipeY = 560 - pipeHeight; level.pipes.push({ x: pipeX, y: pipeY, width: pipeWidth, height: pipeHeight, color: safeColor(palette .accent, '#888888'), isEntrance: true }); const numPlatforms = 5 + Math.floor(rng() * 7); for (let i = 0; i < numPlatforms; i++) { let x, y, w, h, valid; let attempts = 0; do { x = 30 + rng() * (level.width - 150); y = 100 + rng() * (level.height - 250); w = 60 + rng() * 100; h = 16 + rng() * 10; valid = true; if (x < pipeX + pipeWidth + 30 && x + w > pipeX - 30 && y < pipeY + 60) valid = false; for (const p of level.platforms) { if (Math.abs(x - p.x) < 100 && Math.abs(y - p.y) < 80) { valid = false; break; } } attempts++; } while (!valid && attempts < 25); if (valid) level.platforms.push({ x, y, width: w, height: h }); } const numSpikes = Math.min(Math.floor(rng() * 3), level.platforms.length - 1); for (let i = 0; i < numSpikes; i++) { const idx = 1 + Math.floor(rng() * (level.platforms.length - 1)); const p = level.platforms[idx]; if (p && Math.abs(p.x + p.width / 2 - pipeX) > 80) level.spikes.push({ x: p.x + p.width / 2 - 12, y: p .y - 18, width: 24, height: 18 }); } const numStars = Math.floor(rng() * 3) + 1; for (let i = 0; i < numStars; i++) { let sx, sy, valid; let attempts = 0; do { sx = 60 + rng() * (level.width - 120); sy = 60 + rng() * (level.height - 150); valid = true; for (const p of level.platforms) { if (sx > p.x - 30 && sx < p.x + p.width + 30 && sy > p.y - 30 && sy < p.y + p.height + 30) valid = false; } for (const s of level.spikes) { if (sx > s.x - 40 && sx < s.x + s.width + 40 && sy > s.y - 40 && sy < s.y + s.height + 40) valid = false; } attempts++; } while (!valid && attempts < 25); if (valid) level.stars.push({ x: sx, y: sy, width: 20, height: 20, collected: false }); } return level; } function drawLevel(ctx, level, player) { const p = level.palette; const bg = safeColor(p.bg, '#111111'), primary = safeColor(p.primary, '#444444'); const secondary = safeColor(p.secondary, '#888888'), accent = safeColor(p.accent, '#aaaaaa'); const light = safeColor(p.light, '#ffffff'); // Фон с радиальным свечением const grad = ctx.createRadialGradient(500, 300, 50, 500, 300, 500); grad.addColorStop(0, light + '40'); grad.addColorStop(0.5, bg); grad.addColorStop(1, bg); ctx.fillStyle = bg; ctx.fillRect(0, 0, level.width, level.height); ctx.fillStyle = grad; ctx.fillRect(0, 0, level.width, level.height); for (const pl of level.platforms) { const isFloor = pl.width === level.width && pl.y === 560; ctx.fillStyle = isFloor ? bg : primary; ctx.strokeStyle = accent; ctx.lineWidth = 2; ctx.beginPath(); ctx.roundRect(pl.x, pl.y, pl.width, pl.height, 3); ctx.fill(); ctx.stroke(); } for (const sp of level.spikes) { ctx.fillStyle = '#ff4444'; ctx.beginPath(); ctx.moveTo(sp.x + sp.width / 2, sp.y); ctx.lineTo(sp.x, sp.y + sp.height); ctx.lineTo(sp.x + sp.width, sp.y + sp.height); ctx.closePath(); ctx.fill(); } for (const pipe of level.pipes) { ctx.fillStyle = accent; ctx.strokeStyle = light; ctx.lineWidth = 2; ctx.beginPath(); ctx.roundRect(pipe.x, pipe.y, pipe.width, pipe.height, [4, 4, 0, 0]); ctx.fill(); ctx.stroke(); if (pipe.isEntrance) { ctx.fillStyle = '#FFD700'; ctx.font = '10px "Press Start 2P", monospace'; ctx.textAlign = 'center'; ctx.fillText('ВХОД', pipe.x + pipe.width / 2, pipe.y - 10); } } for (const star of level.stars) { if (star.collected) continue; const cx = star.x + star.width / 2, cy = star.y + star.height / 2; const time = Date.now() / 1000, bounce = Math.sin(time * 2.5 + star.x) * 2; ctx.fillStyle = '#FFD700'; ctx.shadowColor = '#FFD700'; ctx.shadowBlur = 15; ctx.beginPath(); for (let i = 0; i < 10; i++) { const r = i % 2 === 0 ? 9 : 4, a = (i * Math.PI) / 5 - Math.PI / 2 + time * 0.4; const sx = cx + Math.cos(a) * r, sy = cy + Math.sin(a) * r + bounce; if (i === 0) ctx.moveTo(sx, sy); else ctx.lineTo(sx, sy); } ctx.closePath(); ctx.fill(); ctx.shadowBlur = 0; } player.draw(ctx); } function showLevelNotification(levelIndex) { const el = document.getElementById('levelNotification'); const palette = levelPalettes[levelIndex % levelPalettes.length]; const numEl = el.querySelector('.level-number'); if (numEl) { numEl.textContent = 'Уровень №' + (levelIndex + 1); numEl.style.borderColor = safeColor(palette.secondary, '#96b8cb'); numEl.style.color = safeColor(palette.light, '#e2e5e2'); numEl.style.background = safeColor(palette.bg, '#3b3936'); } el.className = 'level-notification visible'; soundManager.playScreenTransitionSound(); setTimeout(() => { el.className = 'level-notification fading'; setTimeout(() => el.className = 'level-notification hidden', 800); }, 1500); } function showStarNotification(star) { const el = document.getElementById('starNotification'); el.innerHTML = '<span class="star-icon">⭐</span><span class="compliment">' + (star.compliment || 'Ты супер! ✨') + '</span>'; el.className = 'star-notification visible'; soundManager.playStarSound(); setTimeout(() => el.className = 'star-notification hidden', 4000); } function showAchievementNotification(ach) { if (!ach) return; const el = document.getElementById('achievementNotification'); el.textContent = '🏆 Достижение: ' + ach.name; el.className = 'achievement-notification visible'; soundManager.playAchievementSound(); setTimeout(() => el.className = 'achievement-notification hidden', 5000); } function showLevelComplete() { soundManager.playLevelCompleteSound(); gameState.levelsCompleted++; gameState.currentLevel++; gameState.levelComplete = false; gameState.deathCountOnLevel = 0; if (gameState.player) { gameState.player.speed = gameState.player.baseSpeed; gameState.player.jumpPower = gameState.player.baseJumpPower; } saveProgress(); setTimeout(() => loadLevel(gameState.currentLevel), 400); } function showFinalStar() { soundManager.playFinalStarSound(); gameState.finalStarCollected = true; saveProgress(); setTimeout(showCompletionScreen, 2000); } function showDeathScreen() { document.getElementById('deathStars').textContent = gameState.totalStars; document.getElementById('deathLevel').textContent = gameState.currentLevel + 1; document.getElementById('deathLives').textContent = gameState.lives; document.getElementById('deathScreen').classList.add('visible'); soundManager.playDeathSound(); } function hideDeathScreen() { document.getElementById('deathScreen').classList.remove('visible'); } function showCompletionScreen() { document.getElementById('completionPlayerName').textContent = gameState.playerName || 'друг'; document.getElementById('gameScreen').classList.add('hidden'); document.getElementById('completionScreen').classList.remove('hidden'); } function checkAchievements() { const ul = []; if (gameState.totalStars >= 1 && !gameState.unlockedAchievements.includes('first_star')) ul.push( 'first_star'); if (gameState.totalStars >= 10 && !gameState.unlockedAchievements.includes('star_collector')) ul.push( 'star_collector'); if (gameState.totalStars >= 25 && !gameState.unlockedAchievements.includes('star_hunter')) ul.push( 'star_hunter'); if (gameState.totalStars >= 50 && !gameState.unlockedAchievements.includes('star_legend')) ul.push( 'star_legend'); if (gameState.levelsCompleted >= 5 && !gameState.unlockedAchievements.includes('level_5')) ul.push('level_5'); if (gameState.levelsCompleted >= 15 && !gameState.unlockedAchievements.includes('level_15')) ul.push('level_15'); if (gameState.levelsCompleted >= 30 && !gameState.unlockedAchievements.includes('level_30')) ul.push('level_30'); if (gameState.jumps >= 100 && !gameState.unlockedAchievements.includes('jump_master')) ul.push('jump_master'); if (gameState.noHitLevels >= 5 && !gameState.unlockedAchievements.includes('no_hit')) ul.push('no_hit'); if (gameState.finalStarCollected && !gameState.unlockedAchievements.includes('final_star')) ul.push('final_star'); if (gameState.levelsCompleted >= 30 && gameState.totalStars >= 50 && !gameState.unlockedAchievements.includes( 'perfect_game')) ul.push('perfect_game'); if (gameState.levelsCompleted >= 30 && !gameState.unlockedAchievements.includes('friend_forever')) ul.push( 'friend_forever'); for (const id of ul) { gameState.unlockedAchievements.push(id); showAchievementNotification(achievements.find(a => a.id === id)); } document.getElementById('achievementsDisplay').textContent = '🏆 ' + gameState.unlockedAchievements.length; saveProgress(); } function saveProgress() { if (!gameState.playerName) return; saveManager.savePlayerData(gameState.playerName, { stars: gameState.stars, totalStars: gameState.totalStars, unlockedAchievements: gameState.unlockedAchievements, levelsCompleted: gameState.levelsCompleted, jumps: gameState.jumps, finalStarCollected: gameState.finalStarCollected, playTime: gameState.playTime, lives: gameState.lives, deaths: gameState.deaths, noHitLevels: gameState.noHitLevels, skin: gameState.skin }); } function loadProgress(pn) { const data = saveManager.loadPlayerData(pn); if (data) { gameState.stars = data.stars || 0; gameState.totalStars = data.totalStars || 0; gameState.unlockedAchievements = data.unlockedAchievements || []; gameState.levelsCompleted = data.levelsCompleted || 0; gameState.jumps = data.jumps || 0; gameState.finalStarCollected = data.finalStarCollected || false; gameState.playTime = data.playTime || 0; gameState.lives = data.lives || 3; gameState.deaths = data.deaths || 0; gameState.noHitLevels = data.noHitLevels || 0; if (data.skin) gameState.skin = data.skin; return true; } return false; } function updateHealthDisplay() { const bar = document.getElementById('healthBar'), text = document.getElementById('healthText'), lives = document.getElementById('livesDisplay'); if (bar) { const pct = (gameState.health / gameState.maxHealth) * 100; bar.style.width = Math.max(0, pct) + '%'; if (pct < 30) bar.classList.add('low'); else bar.classList.remove('low'); } if (text) text.textContent = Math.max(0, gameState.health) + '/' + gameState.maxHealth; if (lives) lives.textContent = '❤️ x' + Math.max(0, gameState.lives); } function loadLevel(index) { if (index >= 30) { showCompletionScreen(); return; } if (gameState.lives <= 0) { gameState.lives = 3; gameState.health = gameState.maxHealth; gameState.currentLevel = 0; gameState.levelsCompleted = 0; gameState.stars = 0; saveProgress(); loadLevel(0); return; } gameState.levelComplete = false; const level = generateLevel(index); gameState.levels[index] = level; gameState.currentLevel = index; const canvas = document.getElementById('gameCanvas'), ctx = canvas.getContext('2d'); document.getElementById('levelDisplay').textContent = 'Уровень: ' + (index + 1); document.getElementById('starsDisplay').textContent = '⭐ ' + gameState.stars; document.getElementById('playerNameDisplay').textContent = gameState.playerName || 'Игрок'; updateHealthDisplay(); showLevelNotification(index); const player = gameState.player; player.x = level.spawnX; player.y = level.spawnY; player.smoothX = player.x; player.smoothY = player.y; player.vx = 0; player.vy = 0; player.speed = player.baseSpeed; player.jumpPower = player.baseJumpPower; drawLevel(ctx, level, player); } function gameLoop() { const canvas = document.getElementById('gameCanvas'), ctx = canvas.getContext('2d'); const level = gameState.levels[gameState.currentLevel], player = gameState.player; if (level && !gameState.levelComplete && gameState.isGameActive) { player.update(level); document.getElementById('starsDisplay').textContent = '⭐ ' + gameState.stars; drawLevel(ctx, level, player); gameState.playTime += 1 / 60; if (gameState.playTime >= 1800 && !gameState.unlockedAchievements.includes('dedicated')) { gameState .unlockedAchievements.push('dedicated'); showAchievementNotification(achievements.find(a => a.id === 'dedicated')); saveProgress(); } } requestAnimationFrame(gameLoop); } function renderAchievements() { const container = document.getElementById('achievementsList'); container.innerHTML = ''; for (const ach of achievements) { const isUnlocked = gameState.unlockedAchievements.includes(ach.id); const div = document.createElement('div'); div.className = 'achievement-item ' + (isUnlocked ? 'unlocked' : 'locked'); div.innerHTML = '<span class="ach-icon">' + (isUnlocked ? ach.icon : '❓') + '</span><div class="ach-info"><div class="ach-name">' + (isUnlocked ? ach.name : '???') + '</div><div class="ach-desc">' + (isUnlocked ? ach.desc : 'Не разблокировано') + '</div></div><span class="ach-status">' + (isUnlocked ? '✅' : '🔒') + '</span>'; container.appendChild(div); } } function updateEditorUI() { const skin = gameState.skin; document.getElementById('headShapeSelect').value = skin.headShape || 'round'; document.getElementById('outlineColor').value = skin.outlineColor || '#96b8cb'; document.getElementById('clothingSelect').value = skin.clothing || 'tshirt'; document.getElementById('clothingColor').value = skin.clothingColor || '#4a4a48'; document.getElementById('hatSelect').value = skin.hat || 'none'; document.getElementById('hatColor').value = skin.hatColor || '#576a71'; } function drawEditorPreview() { const c = document.getElementById('editorCanvas'), ctx = c.getContext('2d'), skin = gameState.skin; ctx.clearRect(0, 0, 120, 180); ctx.save(); ctx.translate(60, 90); ctx.fillStyle = '#4a4a48'; ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 2; ctx.beginPath(); ctx.roundRect(-14, 20, 28, 36, 3); ctx.fill(); ctx.stroke(); ctx.fillStyle = skin.clothingColor || '#4a4a48'; ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; if (skin.clothing === 'tshirt') { ctx.beginPath(); ctx.roundRect(-10, 22, 20, 16, 2); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(-2, 22); ctx.lineTo(0, 19); ctx.lineTo(2, 22); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-8, 36); ctx.lineTo(8, 36); ctx.stroke(); } else if (skin.clothing === 'hoodie') { ctx.beginPath(); ctx.roundRect(-14, 20, 28, 20, 4); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.roundRect(-4, 34, 8, 6, 2); ctx.stroke(); ctx.beginPath(); ctx.arc(0, 18, 8, 0, Math.PI); ctx.stroke(); } else if (skin.clothing === 'dress') { ctx.beginPath(); ctx.moveTo(-10, 22); ctx.quadraticCurveTo(-14, 38, -6, 52); ctx.lineTo(6, 52); ctx.quadraticCurveTo(14, 38, 10, 22); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(-8, 28); ctx.lineTo(8, 28); ctx.stroke(); } else if (skin.clothing === 'pants') { ctx.beginPath(); ctx.roundRect(-8, 34, 6, 12, 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.roundRect(2, 34, 6, 12, 2); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(-8, 34); ctx.lineTo(8, 34); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-6, 36); ctx.lineTo(-6, 46); ctx.stroke(); ctx.beginPath(); ctx.moveTo(6, 36); ctx.lineTo(6, 46); ctx.stroke(); } else if (skin.clothing === 'shorts') { ctx.beginPath(); ctx.roundRect(-8, 38, 6, 8, 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.roundRect(2, 38, 6, 8, 2); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(-8, 38); ctx.lineTo(8, 38); ctx.stroke(); } else if (skin.clothing === 'overalls') { ctx.beginPath(); ctx.roundRect(-12, 22, 24, 20, 2); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(-6, 22); ctx.lineTo(-6, 16); ctx.stroke(); ctx.beginPath(); ctx.moveTo(6, 22); ctx.lineTo(6, 16); ctx.stroke(); ctx.beginPath(); ctx.roundRect(-4, 34, 8, 5, 2); ctx.stroke(); } else if (skin.clothing === 'suit') { ctx.beginPath(); ctx.roundRect(-12, 22, 24, 20, 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#ff6b6b'; ctx.fillRect(-2, 26, 4, 12); ctx.fillRect(-4, 36, 8, 3); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(-12, 40); ctx.lineTo(-12, 44); ctx.stroke(); ctx.beginPath(); ctx.moveTo(12, 40); ctx.lineTo(12, 44); ctx.stroke(); } else if (skin.clothing === 'cape') { ctx.beginPath(); ctx.moveTo(-14, 20); ctx.quadraticCurveTo(-18, 40, -14, 56); ctx.lineTo(14, 56); ctx.quadraticCurveTo(18, 40, 14, 20); ctx.fill(); ctx.stroke(); } else if (skin.clothing === 'vest') { ctx.beginPath(); ctx.roundRect(-8, 22, 16, 20, 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#e2e5e2'; for (let i = 0; i < 3; i++) { ctx.beginPath(); ctx.arc(0, 24 + i*6, 1.5, 0, Math.PI*2); ctx.fill(); } } else if (skin.clothing === 'sweater') { ctx.beginPath(); ctx.roundRect(-14, 20, 28, 22, 6); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1; for (let i = 0; i < 4; i++) { ctx.beginPath(); ctx.moveTo(-10 + i*6, 24); ctx.lineTo(-10 + i*6, 42); ctx.stroke(); } ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(-12, 40); ctx.lineTo(12, 40); ctx.stroke(); } else if (skin.clothing === 'jacket') { ctx.beginPath(); ctx.roundRect(-14, 20, 28, 22, 4); ctx.fill(); ctx.stroke(); ctx.strokeStyle = '#e2e5e2'; ctx.lineWidth = 1.5; ctx.setLineDash([2, 2]); ctx.beginPath(); ctx.moveTo(0, 22); ctx.lineTo(0, 42); ctx.stroke(); ctx.setLineDash([]); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(-12, 40); ctx.lineTo(12, 40); ctx.stroke(); } else if (skin.clothing === 'coat') { ctx.beginPath(); ctx.roundRect(-15, 18, 30, 26, 6); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(-14, 36); ctx.lineTo(14, 36); ctx.stroke(); ctx.fillStyle = '#e2e5e2'; for (let i = 0; i < 4; i++) { ctx.beginPath(); ctx.arc(0, 22 + i*6, 1.5, 0, Math.PI*2); ctx.fill(); } } else if (skin.clothing === 'tunic') { ctx.beginPath(); ctx.roundRect(-14, 20, 28, 24, 2); ctx.fill(); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(-10, 24); ctx.lineTo(10, 24); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-10, 42); ctx.lineTo(10, 42); ctx.stroke(); } else if (skin.clothing === 'kimono') { ctx.beginPath(); ctx.roundRect(-14, 20, 28, 24, 4); ctx.fill(); ctx.stroke(); ctx.fillStyle = skin.outlineColor || '#96b8cb'; ctx.fillRect(-4, 24, 8, 6); ctx.fillStyle = skin.clothingColor || '#4a4a48'; ctx.beginPath(); ctx.roundRect(-18, 22, 4, 12, 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.roundRect(14, 22, 4, 12, 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#e2e5e2'; ctx.fillRect(-6, 26, 12, 2); } ctx.fillStyle = '#4a4a48'; ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 2; const hs = 14; if (skin.headShape === 'round') { ctx.beginPath(); ctx.arc(0, hs, hs, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } else if (skin.headShape === 'square') { ctx.beginPath(); ctx.roundRect(-hs, 0, hs * 2, hs * 2, 3); ctx.fill(); ctx.stroke(); } else if (skin.headShape === 'triangle') { ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(-hs, hs * 2); ctx.lineTo(hs, hs * 2); ctx.closePath(); ctx.fill(); ctx.stroke(); } const eyeY = hs - 2; ctx.fillStyle = '#e2e5e2'; ctx.beginPath(); ctx.arc(-4, eyeY, 3.5, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(4, eyeY, 3.5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#3b3936'; ctx.beginPath(); ctx.arc(-2.5, eyeY + 0.5, 2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(2.5, eyeY + 0.5, 2, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'white'; ctx.beginPath(); ctx.arc(-3.5, eyeY - 1.5, 1, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(3.5, eyeY - 1.5, 1, 0, Math.PI * 2); ctx.fill(); if (skin.hat !== 'none') { ctx.fillStyle = skin.hatColor || '#576a71'; ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 1.5; if (skin.hat === 'beanie') { ctx.beginPath(); ctx.ellipse(0, 4, 12, 8, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#ff6b6b'; ctx.beginPath(); ctx.arc(0, -2, 3, 0, Math.PI*2); ctx.fill(); } else if (skin.hat === 'tophat') { ctx.beginPath(); ctx.roundRect(-8, -12, 16, 10, 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.roundRect(-12, -4, 24, 4, 2); ctx.fill(); ctx.stroke(); } else if (skin.hat === 'cap') { ctx.beginPath(); ctx.ellipse(0, 3, 14, 6, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-2, 6); ctx.lineTo(10, 6); ctx.lineTo(6, 10); ctx.closePath(); ctx.fill(); ctx.stroke(); } else if (skin.hat === 'crown') { ctx.beginPath(); ctx.moveTo(-12, 6); ctx.lineTo(-12, -4); ctx.lineTo(-8, 2); ctx.lineTo(-4, -6); ctx.lineTo(0, 2); ctx.lineTo(4, -6); ctx.lineTo(8, 2); ctx.lineTo(12, -4); ctx.lineTo(12, 6); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.fillStyle = '#ff6b6b'; ctx.beginPath(); ctx.arc(-4, -2, 2, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#4ecdc4'; ctx.beginPath(); ctx.arc(0, -4, 2, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#ffe66d'; ctx.beginPath(); ctx.arc(4, -2, 2, 0, Math.PI*2); ctx.fill(); } else if (skin.hat === 'headband') { ctx.beginPath(); ctx.ellipse(0, 8, 12, 4, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } else if (skin.hat === 'beret') { ctx.beginPath(); ctx.ellipse(0, 2, 14, 8, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } else if (skin.hat === 'cowboy') { ctx.beginPath(); ctx.ellipse(0, 8, 14, 6, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-16, 6); ctx.quadraticCurveTo(0, -2, 16, 6); ctx.stroke(); } else if (skin.hat === 'hood') { ctx.beginPath(); ctx.arc(0, -2, 16, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } } ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(-6, 56); ctx.lineTo(-8, 64); ctx.stroke(); ctx.beginPath(); ctx.moveTo(6, 56); ctx.lineTo(8, 64); ctx.stroke(); ctx.strokeStyle = skin.outlineColor || '#96b8cb'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(-14, 24); ctx.lineTo(-18, 34); ctx.stroke(); ctx.beginPath(); ctx.moveTo(14, 24); ctx.lineTo(18, 34); ctx.stroke(); ctx.restore(); } function init() { const canvas = document.getElementById('gameCanvas'); canvas.width = 1000; canvas.height = 600; gameState.player = new Player(); gameState.levels = []; const keys = {}; document.addEventListener('keydown', (e) => { keys[e.key] = true; const p = gameState.player; if (e.key === ' ' || e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') { e.preventDefault(); p.jump(); } if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') p.moveLeft(); if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') p.moveRight(); }); document.addEventListener('keyup', (e) => { keys[e.key] = false; const p = gameState.player; if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') { if (!keys[ 'ArrowRight'] && !keys['d'] && !keys['D']) p.stop(); } if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') { if (!keys['ArrowLeft'] && !keys['a'] && !keys['A']) p.stop(); } }); // Мобильные контролы const btnLeft = document.getElementById('btnLeft'); const btnRight = document.getElementById('btnRight'); const btnJump = document.getElementById('btnJump'); function setupMobileButton(btn, onStart, onEnd) { if (!btn) return; btn.addEventListener('touchstart', (e) => { e.preventDefault(); onStart(); }, { passive: false }); btn.addEventListener('touchend', (e) => { e.preventDefault(); onEnd(); }, { passive: false }); btn.addEventListener('touchcancel', (e) => { e.preventDefault(); onEnd(); }, { passive: false }); btn.addEventListener('mousedown', (e) => { e.preventDefault(); onStart(); }); btn.addEventListener('mouseup', (e) => { e.preventDefault(); onEnd(); }); btn.addEventListener('mouseleave', (e) => { onEnd(); }); } setupMobileButton(btnLeft, () => { if (gameState.player) gameState.player.moveLeft(); }, () => { if (gameState.player && gameState.player.vx < 0) { if (!keys['ArrowRight'] && !keys['d'] && !keys['D']) gameState.player.stop(); } } ); setupMobileButton(btnRight, () => { if (gameState.player) gameState.player.moveRight(); }, () => { if (gameState.player && gameState.player.vx > 0) { if (!keys['ArrowLeft'] && !keys['a'] && !keys['A']) gameState.player.stop(); } } ); setupMobileButton(btnJump, () => { if (gameState.player) gameState.player.jump(); }, () => {} ); const ni = document.getElementById('playerNameInput'), ab = document.getElementById('authButton'), ae = document.getElementById('authError'), sb = document.getElementById('startButton'); function auth() { const n = ni.value.trim(); if (!n || n.length < 2) { ae.textContent = 'Минимум 2 символа!'; ae.classList.remove('hidden'); return; } if (saveManager.playerExists(n)) { loadProgress(n); gameState.playerName = n; sb.disabled = false; ae.classList.add('hidden'); ni.style.borderColor = '#96b8cb'; document.getElementById('playerNameDisplay').textContent = n; document.getElementById('completionPlayerName').textContent = n; soundManager.playButtonSound(); renderAchievements(); updateHealthDisplay(); updateEditorUI(); drawEditorPreview(); } else { gameState.playerName = n; gameState.stars = 0; gameState.totalStars = 0; gameState.unlockedAchievements = []; gameState.levelsCompleted = 0; gameState.jumps = 0; gameState.finalStarCollected = false; gameState.playTime = 0; gameState.lives = 3; gameState.deaths = 0; gameState.noHitLevels = 0; gameState.skin = { headShape: 'round', outlineColor: '#96b8cb', clothing: 'tshirt', clothingColor: '#4a4a48', hat: 'none', hatColor: '#576a71' }; sb.disabled = false; ae.classList.add('hidden'); ni.style.borderColor = '#96b8cb'; document.getElementById('playerNameDisplay').textContent = n; document.getElementById('completionPlayerName').textContent = n; soundManager.playButtonSound(); saveProgress(); updateHealthDisplay(); updateEditorUI(); drawEditorPreview(); } } ab.addEventListener('click', auth); ni.addEventListener('keydown', (e) => { if (e.key === 'Enter') auth(); }); sb.addEventListener('click', () => { soundManager.playButtonSound(); document.getElementById('mainMenu').classList.add('hidden'); document.getElementById('rulesScreen').classList.remove('hidden'); }); document.getElementById('rulesStartBtn').addEventListener('click', () => { soundManager.playButtonSound(); gameState.isGameActive = true; document.getElementById('rulesScreen').classList.add('hidden'); document.getElementById('gameScreen').classList.remove('hidden'); loadLevel(gameState.levelsCompleted); gameLoop(); }); document.getElementById('editorMenuBtn').addEventListener('click', () => { if (!gameState.playerName) { ae .textContent = 'Сначала войдите!'; ae.classList.remove('hidden'); return; } soundManager.playButtonSound(); document.getElementById('mainMenu').classList.add('hidden'); document.getElementById('editorScreen').classList.remove('hidden'); updateEditorUI(); drawEditorPreview(); }); document.getElementById('editorBackBtn').addEventListener('click', () => { soundManager.playButtonSound(); document.getElementById('editorScreen').classList.add('hidden'); document.getElementById('mainMenu').classList.remove('hidden'); saveProgress(); }); ['headShapeSelect', 'outlineColor', 'clothingSelect', 'clothingColor', 'hatSelect', 'hatColor'].forEach( id => { document.getElementById(id).addEventListener('input', () => { gameState.skin.headShape = document.getElementById('headShapeSelect').value; gameState.skin.outlineColor = document.getElementById('outlineColor').value; gameState.skin.clothing = document.getElementById('clothingSelect').value; gameState.skin.clothingColor = document.getElementById('clothingColor').value; gameState.skin.hat = document.getElementById('hatSelect').value; gameState.skin.hatColor = document.getElementById('hatColor').value; drawEditorPreview(); }); }); document.getElementById('achievementsMenuBtn').addEventListener('click', () => { if (!gameState .playerName) { ae.textContent = 'Сначала войдите!'; ae.classList.remove('hidden'); return; } soundManager.playButtonSound(); document.getElementById('mainMenu').classList.add('hidden'); document.getElementById('achievementsScreen').classList.remove('hidden'); renderAchievements(); }); document.getElementById('achievementsBackBtn').addEventListener('click', () => { soundManager .playButtonSound(); document.getElementById('achievementsScreen').classList.add('hidden'); document.getElementById('mainMenu').classList.remove('hidden'); }); document.getElementById('resetProgressBtn').addEventListener('click', () => { if (!gameState.playerName) { ae .textContent = 'Сначала войдите!'; ae.classList.remove('hidden'); return; } if (confirm('Сбросить прогресс для "' + gameState .playerName + '"?')) { soundManager.playButtonSound(); saveManager.resetPlayerData(gameState.playerName); gameState.stars = 0; gameState.totalStars = 0; gameState.unlockedAchievements = []; gameState.levelsCompleted = 0; gameState.jumps = 0; gameState.finalStarCollected = false; gameState.playTime = 0; gameState.lives = 3; gameState.deaths = 0; gameState.noHitLevels = 0; gameState.skin = { headShape: 'round', outlineColor: '#96b8cb', clothing: 'tshirt', clothingColor: '#4a4a48', hat: 'none', hatColor: '#576a71' }; sb.disabled = true; ni.value = ''; ni.style.borderColor = ''; document.getElementById('playerNameDisplay').textContent = 'Игрок'; updateHealthDisplay(); updateEditorUI(); drawEditorPreview(); ae.textContent = 'Сброшено!'; ae.classList.remove('hidden'); setTimeout(() => ae.classList.add('hidden'), 3000); } }); document.getElementById('restartButton').addEventListener('click', () => { soundManager.playButtonSound(); gameState.currentLevel = 0; gameState.stars = 0; gameState.totalStars = 0; gameState.unlockedAchievements = []; gameState.levelsCompleted = 0; gameState.jumps = 0; gameState.finalStarCollected = false; gameState.playTime = 0; gameState.lives = 3; gameState.deaths = 0; document.getElementById('completionScreen').classList.add('hidden'); document.getElementById('gameScreen').classList.remove('hidden'); gameState.isGameActive = true; loadLevel(0); saveProgress(); updateHealthDisplay(); }); document.getElementById('retryButton').addEventListener('click', () => { soundManager.playButtonSound(); const lv = gameState.levels[gameState.currentLevel]; if (lv) gameState.player.respawn(lv); }); document.getElementById('menuFromDeathBtn').addEventListener('click', () => { soundManager.playButtonSound(); hideDeathScreen(); document.getElementById('gameScreen').classList.add('hidden'); document.getElementById('mainMenu').classList.remove('hidden'); gameState.isDead = false; gameState.isGameActive = false; }); document.getElementById('soundToggle').addEventListener('click', function() { const en = soundManager .toggleSound(); this.textContent = en ? '🔊' : '🔇'; this.classList.toggle('muted', !en); }); document.getElementById('musicToggle').addEventListener('click', function() { this.classList.toggle( 'muted'); }); if (!CanvasRenderingContext2D.prototype.roundRect) { CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { if (typeof r === 'number') r = { tl: r, tr: r, br: r, bl: r }; else r = r || { tl: 0, tr: 0, br: 0, bl: 0 }; this.moveTo(x + r.tl, y); this.arcTo(x + w, y, x + w, y + h, r.tr); this.arcTo(x + w, y + h, x, y + h, r.br); this.arcTo(x, y + h, x, y, r.bl); this.arcTo(x, y, x + w, y, r.tl); return this; }; } updateHealthDisplay(); updateEditorUI(); drawEditorPreview(); document.addEventListener('touchmove', (e) => { e.preventDefault(); }, { passive: false }); } window.onload = init; </script> </body> </html>