-
Notifications
You must be signed in to change notification settings - Fork 118
Jump Game #110
Copy link
Copy link
Open
Description
zzangddo-stack
opened on Jul 19, 2026
Issue body actions
<title>우당탕탕 점프 모험나라</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
font-family: 'Jua', sans-serif;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
/* 모바일 터치 오작동 방지 */
.no-double-tap {
touch-action: manipulation;
}
/* 12간지 선택 그리드 스크롤 디자인 */
.char-grid-scroll {
max-height: 280px;
overflow-y: auto;
}
/* 스크롤바 커스텀 */
.char-grid-scroll::-webkit-scrollbar {
width: 8px;
}
.char-grid-scroll::-webkit-scrollbar-track {
background: rgba(255,255,255,0.1);
border-radius: 10px;
}
.char-grid-scroll::-webkit-scrollbar-thumb {
background: #facc15;
border-radius: 10px;
}
</style>
<script>
// 전역 변수 및 게임 상태 초기화
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let gameState = 'tutorial';
let selectedCharacter = 'boy';
let currentLevel = 1;
const maxLevel = 10;
let unlockedLevel = 1;
let lives = 3;
let maxLives = 3;
let score = 0;
let coinsCount = 0;
let levelTimer = 120;
let timerInterval = null;
let isVerticalStage = false; // 홀짝 스테이지 방향
const player = {
x: 100, y: 300, width: 38, height: 48,
vx: 0, vy: 0, speed: 4.9, jumpForce: 11.2, friction: 0.82, magnetRange: 140,
isOnGround: false, doubleJumpCount: 0, maxDoubleJumps: 0, invincibilityFrames: 0,
powerupType: null, powerupTimer: 0, hasPassiveShield: false, shieldCooldown: 0
};
const GRAVITY = 0.45;
let cameraX = 0, cameraY = 0;
const keys = { left: false, right: false, jump: false, shoot: false };
let platforms = [], coins = [], enemies = [], items = [], projectiles = [], playerBullets = [], jumpSparks = [], clouds = [];
let goalFlag = { x: 0, y: 0, width: 40, height: 160 };
const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = null;
let bgmInterval = null, bgmStep = 0, currentBgmType = null;
function initAudio() {
if (!audioCtx) audioCtx = new AudioContext();
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
}
function midiToFreq(note) { return 440 * Math.pow(2, (note - 69) / 12); }
function getWeeklyKpopBGM() {
const now = new Date();
const start = new Date(now.getFullYear(), 0, 1);
const week = Math.floor((now - start) / (7 * 24 * 60 * 60 * 1000));
const trackType = week % 3;
let melody = [], bass = [], bpm = 120;
if (trackType === 0) { // 걸그룹 팝
melody = [72, 0, 72, 72, 69, 72, 0, 74, 76, 76, 74, 72, 69, 0, 69, 72];
bass = [48, 48, 48, 48, 41, 41, 41, 41, 45, 45, 45, 45, 43, 43, 43, 43];
bpm = 140;
} else if (trackType === 1) { // 트랩 베이스 보이그룹
melody = [60, 63, 65, 0, 67, 65, 63, 60, 60, 0, 63, 60, 58, 60, 0, 0];
bass = [36, 0, 0, 36, 0, 36, 36, 0, 43, 0, 0, 43, 0, 41, 39, 0];
bpm = 125;
} else { // 레트로 신스팝
melody = [67, 69, 71, 74, 71, 69, 67, 64, 67, 0, 67, 69, 71, 69, 67, 0];
bass = [43, 43, 43, 43, 47, 47, 47, 47, 48, 48, 48, 48, 45, 45, 45, 45];
bpm = 135;
}
return { melody, bass, bpm };
}
function startBGM(type) {
initAudio();
if (!audioCtx) return;
if (currentBgmType === type && bgmInterval) return;
stopBGM();
currentBgmType = type;
bgmStep = 0;
let melody = [], bass = [], bpm = 120, waveType = 'triangle', bassWaveType = 'sine', melodyVolume = 0.04, bassVolume = 0.05;
if (type === 'menu') {
melody = [60, 64, 67, 72, 67, 64, 60, 64, 62, 65, 69, 74, 69, 65, 62, 65];
bass = [36, 36, 43, 43, 38, 38, 45, 45]; bpm = 100;
} else if (type === 'stage') {
const kpop = getWeeklyKpopBGM();
melody = kpop.melody; bass = kpop.bass; bpm = kpop.bpm;
waveType = 'triangle'; bassWaveType = 'sawtooth'; melodyVolume = 0.035; bassVolume = 0.035;
}
const stepDuration = 60 / bpm / 2;
bgmInterval = setInterval(() => {
if (gameState !== 'playing' && type !== 'menu') { stopBGM(); return; }
try {
const now = audioCtx.currentTime;
if (melody.length > 0) {
const midiNote = melody[bgmStep % melody.length];
if (midiNote > 0) {
const osc = audioCtx.createOscillator(), gainNode = audioCtx.createGain();
osc.connect(gainNode); gainNode.connect(audioCtx.destination);
osc.type = waveType; osc.frequency.setValueAtTime(midiToFreq(midiNote), now);
gainNode.gain.setValueAtTime(melodyVolume, now);
gainNode.gain.exponentialRampToValueAtTime(0.001, now + stepDuration - 0.015);
osc.start(now); osc.stop(now + stepDuration);
}
}
if (bass.length > 0 && bgmStep % 2 === 0) {
const bassNote = bass[Math.floor(bgmStep / 2) % bass.length];
if (bassNote > 0) {
const oscBass = audioCtx.createOscillator(), gainBass = audioCtx.createGain();
oscBass.connect(gainBass); gainBass.connect(audioCtx.destination);
oscBass.type = bassWaveType; oscBass.frequency.setValueAtTime(midiToFreq(bassNote), now);
gainBass.gain.setValueAtTime(bassVolume, now);
gainBass.gain.exponentialRampToValueAtTime(0.001, now + (stepDuration * 2) - 0.02);
oscBass.start(now); oscBass.stop(now + (stepDuration * 2));
}
}
bgmStep++;
} catch (e) { console.warn("BGM 에러", e); }
}, stepDuration * 1000);
}
function stopBGM() {
if (bgmInterval) { clearInterval(bgmInterval); bgmInterval = null; }
currentBgmType = null;
}
function playSound(type) {
try {
initAudio(); if (!audioCtx) return;
const osc = audioCtx.createOscillator(), gainNode = audioCtx.createGain();
osc.connect(gainNode); gainNode.connect(audioCtx.destination);
const now = audioCtx.currentTime;
if (type === 'jump') {
osc.type = 'sine'; osc.frequency.setValueAtTime(160, now); osc.frequency.exponentialRampToValueAtTime(650, now + 0.16);
gainNode.gain.setValueAtTime(0.25, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.16);
osc.start(now); osc.stop(now + 0.16);
} else if (type === 'doubleJump') {
osc.type = 'triangle'; osc.frequency.setValueAtTime(320, now); osc.frequency.exponentialRampToValueAtTime(850, now + 0.22);
gainNode.gain.setValueAtTime(0.25, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.22);
osc.start(now); osc.stop(now + 0.22);
} else if (type === 'coin') {
const osc2 = audioCtx.createOscillator(), gainNode2 = audioCtx.createGain();
osc2.connect(gainNode2); gainNode2.connect(audioCtx.destination);
osc.type = 'sine'; osc.frequency.setValueAtTime(987.77, now); osc.frequency.setValueAtTime(1318.51, now + 0.08);
gainNode.gain.setValueAtTime(0.12, now); gainNode.gain.exponentialRampToValueAtTime(0.005, now + 0.25);
osc2.type = 'sine'; osc2.frequency.setValueAtTime(1318.51, now); osc2.frequency.setValueAtTime(1567.98, now + 0.08);
gainNode2.gain.setValueAtTime(0.08, now); gainNode2.gain.exponentialRampToValueAtTime(0.005, now + 0.25);
osc.start(now); osc.stop(now + 0.25); osc2.start(now); osc2.stop(now + 0.25);
} else if (type === 'shoot') {
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(550, now); osc.frequency.exponentialRampToValueAtTime(120, now + 0.12);
gainNode.gain.setValueAtTime(0.12, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.12);
osc.start(now); osc.stop(now + 0.12);
} else if (type === 'hurt') {
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(320, now); osc.frequency.linearRampToValueAtTime(80, now + 0.28);
gainNode.gain.setValueAtTime(0.4, now); gainNode.gain.exponentialRampToValueAtTime(0.005, now + 0.28);
osc.start(now); osc.stop(now + 0.28);
} else if (type === 'stomp') {
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(150, now); osc.frequency.linearRampToValueAtTime(30, now + 0.14);
gainNode.gain.setValueAtTime(0.35, now); gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.14);
osc.start(now); osc.stop(now + 0.14);
} else if (type === 'powerup' || type === 'heal' || type === 'clear' || type === 'fail') {
let notes = type === 'powerup' ? [261.63, 329.63, 392.00, 523.25, 659.25] :
type === 'heal' ? [392.00, 493.88, 587.33, 783.99] :
type === 'clear' ? [523.25, 659.25, 783.99, 1046.50] : [440.00, 415.30, 392.00, 349.23];
let dur = type === 'powerup' ? 0.06 : type === 'heal' ? 0.07 : type === 'clear' ? 0.08 : 0.15;
notes.forEach((freq, idx) => {
const o = audioCtx.createOscillator(), g = audioCtx.createGain();
o.connect(g); g.connect(audioCtx.destination);
o.type = type === 'fail' ? 'sawtooth' : 'sine';
o.frequency.setValueAtTime(freq, now + idx * dur);
g.gain.setValueAtTime(0.15, now + idx * dur);
g.gain.exponentialRampToValueAtTime(0.001, now + idx * dur + 0.25);
o.start(now + idx * dur); o.stop(now + idx * dur + 0.25);
});
}
} catch (e) {}
}
function showScreen(screenId) {
['tutorial', 'start', 'char-select', 'level-select', 'stage-preview', 'clear', 'fail', 'all-clear'].forEach(id => {
let el = document.getElementById(id.startsWith('pop-') ? id : 'screen-' + id) || document.getElementById('pop-' + id);
if (el) el.classList.add('hidden');
});
if (screenId === 'tutorial') { document.getElementById('screen-tutorial').classList.remove('hidden'); stopBGM(); }
else if (screenId === 'start') { document.getElementById('screen-start').classList.remove('hidden'); startBGM('menu'); }
else if (screenId === 'char_select') { document.getElementById('screen-char-select').classList.remove('hidden'); startBGM('menu'); }
else if (screenId === 'level_select') { buildLevelGrid(); document.getElementById('screen-level-select').classList.remove('hidden'); startBGM('menu'); }
else if (screenId === 'preview') { document.getElementById('screen-stage-preview').classList.remove('hidden'); stopBGM(); }
else if (screenId === 'clear') { document.getElementById('pop-clear').classList.remove('hidden'); stopBGM(); }
else if (screenId === 'fail') { document.getElementById('pop-fail').classList.remove('hidden'); stopBGM(); }
else if (screenId === 'ending') { document.getElementById('pop-all-clear').classList.remove('hidden'); stopBGM(); }
}
function selectHero(heroName) {
selectedCharacter = heroName; playSound('coin');
document.querySelectorAll('.char-card').forEach(btn => btn.classList.remove('border-yellow-400', 'bg-white/30'));
const activeBtn = document.getElementById(`btn-char-${heroName}`);
if (activeBtn) activeBtn.classList.add('border-yellow-400', 'bg-white/30');
setTimeout(() => showScreen('level_select'), 150);
}
function buildLevelGrid() {
const grid = document.getElementById('level-grid'); grid.innerHTML = '';
for (let i = 1; i <= maxLevel; i++) {
const btn = document.createElement('button');
btn.className = `w-14 h-14 rounded-2xl flex items-center justify-center font-bold text-xl transition-all shadow-md `;
if (i <= unlockedLevel) {
btn.className += `bg-yellow-400 hover:bg-yellow-300 border-4 border-white text-blue-900 cursor-pointer active:scale-90`;
btn.innerHTML = `${i}`; btn.onclick = () => { initAudio(); playSound('coin'); startGame(i); };
} else {
btn.className += `bg-slate-700/60 border-4 border-slate-600 text-slate-400 cursor-not-allowed`; btn.innerHTML = `🔒`;
}
grid.appendChild(btn);
}
}
function startTimer() {
clearInterval(timerInterval);
timerInterval = setInterval(() => {
if (gameState === 'playing') {
levelTimer--; document.getElementById('hud-timer').textContent = levelTimer;
if (levelTimer <= 0) handlePlayerHurt(true);
}
}, 1000);
}
function drawCharacterPreview(canvasId, type) {
const cvs = document.getElementById(canvasId); if (!cvs) return;
const cx = cvs.getContext('2d'); cx.clearRect(0, 0, cvs.width, cvs.height);
cx.save(); cx.translate(cvs.width / 2, cvs.height / 2 + 5);
cx.fillStyle = 'rgba(0,0,0,0.15)'; cx.beginPath(); cx.ellipse(0, 22, 16, 5, 0, 0, Math.PI*2); cx.fill();
if (type === 'boy' || type === 'girl') {
cx.fillStyle = (type === 'boy') ? '#3b82f6' : '#ec4899'; cx.beginPath(); cx.arc(0, 15, 12, Math.PI, 0, false); cx.fill();
cx.fillStyle = '#fed7aa'; cx.fillRect(-15, 8, 4, 10); cx.fillRect(11, 8, 4, 10);
cx.beginPath(); cx.arc(0, -3, 13, 0, Math.PI * 2); cx.fill();
if (type === 'boy') {
cx.fillStyle = '#78350f'; cx.beginPath(); cx.arc(0, -6, 14, Math.PI, 0); cx.fill();
cx.fillStyle = '#1d4ed8'; cx.beginPath(); cx.arc(0, -11, 10, Math.PI, 0); cx.fill();
} else {
cx.fillStyle = '#fbbf24'; cx.beginPath(); cx.arc(0, -6, 14, Math.PI, 0); cx.fill();
cx.fillStyle = '#f43f5e'; cx.beginPath(); cx.arc(-10, -9, 4, 0, Math.PI*2); cx.arc(10, -9, 4, 0, Math.PI*2); cx.fill();
}
cx.fillStyle = '#1e293b'; cx.beginPath(); cx.arc(-4, -3, 2, 0, Math.PI * 2); cx.arc(4, -3, 2, 0, Math.PI * 2); cx.fill();
cx.strokeStyle = '#1e293b'; cx.lineWidth = 1.5; cx.beginPath(); cx.arc(0, 1, 3, 0, Math.PI); cx.stroke();
} else {
drawZodiacFace(cx, type);
}
cx.restore();
}
function drawZodiacFace(ctx, type) {
ctx.save();
const shapes = {
'rat': { c: '#94a3b8', extra: () => { ctx.fillStyle='#475569'; ctx.beginPath(); ctx.arc(-13,-6,7,0,7); ctx.arc(13,-6,7,0,7); ctx.fill(); ctx.fillStyle='#fca5a5'; ctx.beginPath(); ctx.arc(-13,-6,4,0,7); ctx.arc(13,-6,4,0,7); ctx.fill(); } },
'ox': { c: '#b45309', extra: () => { ctx.fillStyle='#e2e8f0'; ctx.beginPath(); ctx.moveTo(-10,-3); ctx.quadraticCurveTo(-14,-13,-8,-13); ctx.lineTo(-6,-3); ctx.moveTo(10,-3); ctx.quadraticCurveTo(14,-13,8,-13); ctx.lineTo(6,-3); ctx.fill(); } },
'tiger': { c: '#f97316', extra: () => { ctx.beginPath(); ctx.arc(-10,-6,5,0,7); ctx.arc(10,-6,5,0,7); ctx.fill(); ctx.fillStyle='#000'; ctx.fillRect(-12,4,4,2); ctx.fillRect(8,4,4,2); ctx.fillRect(-3,-7,6,2); } },
'rabbit': { c: '#f8fafc', extra: () => { ctx.fillStyle='#f1f5f9'; ctx.beginPath(); ctx.roundRect(-9,-15,6,16,3); ctx.roundRect(3,-15,6,16,3); ctx.fill(); ctx.fillStyle='#fca5a5'; ctx.beginPath(); ctx.roundRect(-7,-12,3,11,2); ctx.roundRect(5,-12,3,11,2); ctx.fill(); } },
'dragon': { c: '#10b981', extra: () => { ctx.fillStyle='#fbbf24'; ctx.fillRect(-6,-11,3,8); ctx.fillRect(3,-11,3,8); } },
'snake': { c: '#22c55e', r: 11, y: 8, extra: () => { ctx.fillStyle='#ef4444'; ctx.fillRect(-1.5,13,3,6); } },
'horse': { c: '#854d0e', extra: () => { ctx.fillStyle='#451a03'; ctx.fillRect(-3,-11,6,10); } },
'sheep': { c: '#f1f5f9', r: 13, extra: () => { ctx.beginPath(); ctx.arc(-10,-5,5,0,7); ctx.arc(10,-5,5,0,7); ctx.arc(0,-10,6,0,7); ctx.fill(); } },
'monkey': { c: '#b45309', extra: () => { ctx.fillStyle='#ffedd5'; ctx.beginPath(); ctx.arc(-5,5,7,0,7); ctx.arc(5,5,7,0,7); ctx.fill(); } },
'rooster': { c: '#ffffff', r: 13, y: 7, extra: () => { ctx.fillStyle='#ef4444'; ctx.beginPath(); ctx.arc(0,-6,5,0,7); ctx.fill(); ctx.fillStyle='#f59e0b'; ctx.beginPath(); ctx.moveTo(-4,5); ctx.lineTo(4,5); ctx.lineTo(0,11); ctx.fill(); } },
'dog': { c: '#eab308', r: 13, extra: () => { ctx.fillStyle='#ca8a04'; ctx.beginPath(); ctx.roundRect(-15,-1,5,12,2); ctx.roundRect(10,-1,5,12,2); ctx.fill(); } },
'pig': { c: '#f472b6', extra: () => { ctx.fillStyle='#ec4899'; ctx.beginPath(); ctx.roundRect(-5,5,10,6,3); ctx.fill(); } },
};
let s = shapes[type];
ctx.fillStyle = s.c; ctx.beginPath(); ctx.arc(0, s.y || 5, s.r || 14, 0, Math.PI*2); ctx.fill();
if(s.extra) s.extra();
ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(-4, 2, 2.2, 0, 7); ctx.arc(4, 2, 2.2, 0, 7); ctx.fill();
ctx.restore();
}
window.addEventListener('DOMContentLoaded', () => {
['boy', 'girl', 'rat', 'ox', 'tiger', 'rabbit', 'dragon', 'snake', 'horse', 'sheep', 'monkey', 'rooster', 'dog', 'pig'].forEach(id => drawCharacterPreview('canvas-' + id, id));
});
function createPlatform(x, y, w, h, type = 'grass', isDisappearing = false) { platforms.push({ x, y, width: w, height: h, type, isDisappearing, opacity: 1, timer: 0 }); }
function createCoin(x, y) { coins.push({ x, y, radius: 10, isCollected: false }); }
function createItem(x, y, type) { items.push({ x, y, width: 30, height: 30, type, isCollected: false }); }
function createEnemy(x, y, type = 'slime', patrolDist = 150) {
enemies.push({ startX: x, x, y, width: type === 'boss' ? 70 : 34, height: type === 'boss' ? 60 : 30, type, patrolDist, speed: type === 'boss' ? 3 : type === 'bat' ? 2.6 : 1.7 * (1.35 + currentLevel * 0.12), dir: 1, bossHp: type === 'boss' ? 6 : 1, squashed: false, squashTimer: 0, shootCooldown: Math.random() * 60 });
}
function generateMap(level) {
platforms = []; coins = []; enemies = []; items = []; clouds = []; projectiles = []; playerBullets = []; jumpSparks = [];
isVerticalStage = (level % 2 === 0);
for (let i = 0; i < 15; i++) {
clouds.push({ x: Math.random() * 4000, y: (isVerticalStage ? -2000 : 40) + Math.random() * (isVerticalStage ? 2500 : 150), size: 40 + Math.random() * 50, speed: 0.1 + Math.random() * 0.3 });
}
if (isVerticalStage) {
const mapW = Math.max(800, canvas.width);
createPlatform(0, 420, mapW, 80, 'grass');
let curY = 300, side = 0;
const targetMinY = -600 - (level * 350);
while(curY > targetMinY) {
let pW = 120 + Math.random() * 80, pX;
if (side === 0) pX = 40 + Math.random() * 80;
else if (side === 1) pX = mapW - pW - 40 - Math.random() * 80;
else pX = (mapW - pW)/2 + (Math.random() * 100 - 50);
createPlatform(pX, curY, pW, 30, Math.random() > 0.6 ? 'brick' : 'cloud_platform');
if(Math.random() < 0.6) createCoin(pX + pW/2, curY - 30);
// 짝수 스테이지에서도 적 출현하도록 로직 수정
if(Math.random() < 0.45) createEnemy(pX + 10, curY - 30, Math.random() > 0.5 ? 'bat' : 'slime', pW - 20);
if(Math.random() < 0.1) createItem(pX + pW/2, curY - 60, ['star', 'potion', 'heart'][Math.floor(Math.random()*3)]);
// 계단 높이 고정 (70~110 픽셀)
curY -= (70 + Math.random() * 40);
side = (side + 1 + Math.floor(Math.random()*2)) % 3;
}
const topPlatformX = mapW/2 - 100;
createPlatform(topPlatformX, targetMinY, 200, 20, 'brick');
goalFlag.x = topPlatformX + 80; goalFlag.y = targetMinY - 160;
if(level === 10) {
createEnemy(topPlatformX + 50, targetMinY - 60, 'boss', 150);
}
} else {
const mapLength = 1900 + (level * 350);
let curX = 0, gaps = [];
while (curX < mapLength) {
let gap = (level > 1 && curX > 400 && curX < mapLength - 800 && Math.random() < 0.28 + (level * 0.03)) ? Math.min(260, 120 + (Math.random() * 100) + (level * 6)) : 0;
if (gap > 0) { gaps.push({start: curX, end: curX + gap}); curX += gap; }
let chunkW = Math.min(mapLength - curX, 400 + Math.random() * 400);
createPlatform(curX, 420, chunkW, 80, 'grass'); curX += chunkW;
}
let segmentX = 300;
while (segmentX < mapLength - 800) {
// 발판 높이를 60~110 픽셀 사이로 고정
const h = 420 - (60 + Math.random() * 50);
const w = level === 1 ? 140 : 90 + Math.random() * 70;
let isSafe = true;
for(let g of gaps) if (segmentX + w > g.start - 30 && segmentX < g.end + 30) { isSafe = false; break; }
if (isSafe) {
createPlatform(segmentX, h, w, 30, Math.random() > 0.5 ? 'brick' : 'cloud_platform');
if (level === 1) { createCoin(segmentX+w/2-20, h-25); createCoin(segmentX+w/2, h-25); createCoin(segmentX+w/2+20, h-25); }
else if (Math.random() < 0.7) { createCoin(segmentX+w/2, h-30); createCoin(segmentX+w/2-25, h-30); }
if (Math.random() < 0.45) createItem(segmentX + w/2, h - 65, ['star', 'gem', 'potion', 'heart'][Math.floor(Math.random() * 4)]);
if (Math.random() < 0.5 + (level * 0.04)) createEnemy(segmentX + 15, level >= 3 && Math.random() < 0.5 ? h - 75 : 390, level >= 3 && Math.random() < 0.5 ? 'bat' : 'slime', w - 30);
if (level >= 3 && Math.random() < 0.35) createPlatform(segmentX + w - 30, 390, 35, 30, 'spike');
if (level >= 4 && Math.random() < 0.4) createPlatform(segmentX + w + 35, h + 20, 70, 20, 'disappearing', true);
}
segmentX += w + 110 + (Math.random() * 100);
}
const twrX = mapLength - 350;
createPlatform(twrX, 340, 80, 20, 'brick'); createPlatform(twrX + 80, 260, 80, 20, 'brick'); createPlatform(twrX + 160, 180, 80, 20, 'brick'); createPlatform(twrX + 240, 110, 100, 20, 'brick');
goalFlag.x = twrX + 270; goalFlag.y = 110 - 160;
}
}
function drawSkyGradient() {
let grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
if (currentLevel <= 3) { grad.addColorStop(0, '#7dd3fc'); grad.addColorStop(1, '#e0f2fe'); }
else if (currentLevel <= 7) { grad.addColorStop(0, '#f97316'); grad.addColorStop(0.6, '#ec4899'); grad.addColorStop(1, '#818cf8'); }
else { grad.addColorStop(0, '#0f172a'); grad.addColorStop(0.6, '#311042'); grad.addColorStop(1, '#1e1b4b'); }
ctx.fillStyle = grad; ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function drawClouds() {
ctx.fillStyle = 'rgba(255, 255, 255, 0.75)';
clouds.forEach(c => {
const rX = (c.x - cameraX * 0.3) % (canvas.width + 200), aX = rX < -100 ? rX + canvas.width + 200 : rX - 100, aY = c.y - cameraY * 0.1;
ctx.beginPath(); ctx.arc(aX, aY, c.size * 0.5, 0, 7); ctx.arc(aX + c.size * 0.4, aY - c.size * 0.1, c.size * 0.4, 0, 7);
ctx.arc(aX - c.size * 0.4, aY - c.size * 0.1, c.size * 0.35, 0, 7); ctx.arc(aX + c.size * 0.7, aY + c.size * 0.1, c.size * 0.3, 0, 7); ctx.fill();
});
}
function drawPlatforms() {
platforms.forEach(p => {
if (p.x + p.width - cameraX < -50 || p.x - cameraX > canvas.width + 50 || p.y + p.height - cameraY < -50 || p.y - cameraY > canvas.height + 50) return;
ctx.save(); ctx.globalAlpha = p.opacity;
const rx = p.x - cameraX, ry = p.y - cameraY;
if (p.type === 'grass') {
ctx.fillStyle = '#15803d'; ctx.fillRect(rx, ry, p.width, 18); ctx.fillStyle = '#78350f'; ctx.fillRect(rx, ry + 18, p.width, p.height - 18);
ctx.fillStyle = '#451a03'; for (let ox = 15; ox < p.width; ox += 60) { ctx.fillRect(rx + ox, ry + 25, 10, 10); ctx.fillRect(rx + ox + 20, ry + 45, 10, 10); }
} else if (p.type === 'brick' || p.type === 'disappearing') {
ctx.fillStyle = p.type === 'brick' ? '#b45309' : '#f59e0b'; ctx.fillRect(rx, ry, p.width, p.height);
ctx.strokeStyle = '#fef08a'; ctx.lineWidth = 2.5; ctx.strokeRect(rx, ry, p.width, p.height);
if(p.type === 'brick') { ctx.beginPath(); for (let ey = ry + 15; ey < ry + p.height; ey += 15) { ctx.moveTo(rx, ey); ctx.lineTo(rx + p.width, ey); } ctx.stroke(); }
} else if (p.type === 'cloud_platform') {
ctx.fillStyle = '#f1f5f9'; ctx.strokeStyle = '#cbd5e1'; ctx.lineWidth = 3; ctx.beginPath(); ctx.roundRect(rx, ry, p.width, p.height, 15); ctx.fill(); ctx.stroke();
} else if (p.type === 'spike') {
ctx.fillStyle = '#cbd5e1'; ctx.strokeStyle = '#475569'; ctx.lineWidth = 2;
for (let i = 0; i < Math.floor(p.width / 20); i++) { ctx.beginPath(); ctx.moveTo(rx + i*20, ry + p.height); ctx.lineTo(rx + i*20 + 10, ry); ctx.lineTo(rx + i*20 + 20, ry + p.height); ctx.fill(); ctx.stroke(); }
}
ctx.restore();
});
}
function drawCoins() {
coins.forEach(c => {
if (!c.isCollected) {
ctx.save(); ctx.fillStyle = '#fbbf24'; ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.beginPath();
ctx.ellipse(c.x - cameraX, c.y - cameraY, c.radius + Math.sin(Date.now() * 0.01) * 1.5, c.radius, 0, 0, 7); ctx.fill(); ctx.stroke(); ctx.restore();
}
});
}
function drawItems() {
items.forEach(it => {
if (!it.isCollected) {
ctx.save(); const rx = it.x - cameraX, ry = it.y - cameraY + Math.sin(Date.now() * 0.008) * 5;
ctx.fillStyle = 'rgba(255,255,255,0.2)'; ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(rx + 15, ry + 15, 22, 0, 7); ctx.fill(); ctx.stroke();
if (it.type === 'star') { ctx.fillStyle = '#facc15'; drawStarShape(rx + 15, ry + 15, 5, 14, 6); }
else if (it.type === 'gem') { ctx.fillStyle = '#ec4899'; ctx.beginPath(); ctx.moveTo(rx+15, ry+3); ctx.lineTo(rx+28, ry+15); ctx.lineTo(rx+15, ry+27); ctx.lineTo(rx+2, ry+15); ctx.fill(); }
else if (it.type === 'potion') { ctx.fillStyle = '#3b82f6'; ctx.fillRect(rx+10, ry+12, 10, 14); ctx.fillStyle = '#60a5fa'; ctx.fillRect(rx+12, ry+4, 6, 8); }
else if (it.type === 'heart') { ctx.fillStyle = '#ef4444'; ctx.beginPath(); ctx.arc(rx+10, ry+10, 7, 0, Math.PI, true); ctx.arc(rx+20, ry+10, 7, 0, Math.PI, true); ctx.lineTo(rx+15, ry+25); ctx.fill(); }
ctx.restore();
}
});
}
function drawStarShape(cx, cy, spikes, outerRadius, innerRadius) {
let rot = Math.PI / 2 * 3, step = Math.PI / spikes; ctx.beginPath(); ctx.moveTo(cx, cy - outerRadius);
for (let i = 0; i < spikes; i++) {
ctx.lineTo(cx + Math.cos(rot) * outerRadius, cy + Math.sin(rot) * outerRadius); rot += step;
ctx.lineTo(cx + Math.cos(rot) * innerRadius, cy + Math.sin(rot) * innerRadius); rot += step;
}
ctx.lineTo(cx, cy - outerRadius); ctx.fill();
}
function drawEnemies() {
enemies.forEach(e => {
if (e.x - cameraX < -100 || e.x - cameraX > canvas.width + 100) return;
ctx.save(); const rx = e.x - cameraX, ry = e.y - cameraY;
if (e.squashed) { ctx.fillStyle = '#cbd5e1'; ctx.fillRect(rx, ry + e.height - 10, e.width, 10); }
else if (e.type === 'slime') {
const sq = Math.sin(Date.now() * 0.015) * 2; ctx.fillStyle = '#a855f7'; ctx.beginPath(); ctx.ellipse(rx + 17, ry + 15 + sq/2, 17, 15 - sq/2, 0, 0, 7); ctx.fill();
ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(rx + 10, ry + 12, 4, 0, 7); ctx.arc(rx + 24, ry + 12, 4, 0, 7); ctx.fill();
} else if (e.type === 'bat') {
const wY = Math.sin(Date.now() * 0.02) * 10; ctx.fillStyle = '#ef4444'; ctx.beginPath(); ctx.arc(rx + 17, ry + 15, 12, 0, 7); ctx.fill();
ctx.fillStyle = '#991b1b'; ctx.beginPath(); ctx.moveTo(rx + 9, ry + 15); ctx.lineTo(rx - 8, ry + 15 - wY); ctx.lineTo(rx + 2, ry + 21); ctx.fill();
} else if (e.type === 'boss') {
ctx.fillStyle = '#374151'; ctx.beginPath(); ctx.roundRect(rx, ry, e.width, e.height, 15); ctx.fill();
ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.moveTo(rx + 15, ry); ctx.lineTo(rx + 10, ry - 20); ctx.lineTo(rx + 35, ry - 25); ctx.lineTo(rx + 55, ry); ctx.fill();
ctx.fillStyle = '#1e293b'; ctx.fillRect(rx - 5, ry - 40, 80, 8); ctx.fillStyle = '#ef4444'; ctx.fillRect(rx - 5, ry - 40, 80 * (e.bossHp / 6), 8);
}
ctx.restore();
});
}
function drawProjectiles() {
projectiles.forEach(p => {
ctx.save(); const rx = p.x - cameraX, ry = p.y - cameraY;
ctx.fillStyle = p.type === 'acid' ? '#22c55e' : p.type === 'darkness' ? '#a855f7' : '#f97316';
ctx.beginPath(); ctx.arc(rx, ry, p.type === 'boss_fire' ? 12 : p.type === 'darkness' ? 8 : 7, 0, 7); ctx.fill(); ctx.restore();
});
}
function drawPlayerBullets() {
playerBullets.forEach(b => {
ctx.save(); ctx.fillStyle = selectedCharacter === 'tiger' ? '#ef4444' : '#facc15'; ctx.shadowColor = '#f59e0b'; ctx.shadowBlur = 8;
ctx.beginPath(); ctx.arc(b.x - cameraX, b.y - cameraY, selectedCharacter === 'tiger' ? 10 : 7, 0, 7); ctx.fill(); ctx.restore();
});
}
function drawGoalFlag() {
const rx = goalFlag.x - cameraX, ry = goalFlag.y - cameraY; ctx.save();
ctx.fillStyle = '#94a3b8'; ctx.fillRect(rx + 16, ry, 8, goalFlag.height);
ctx.fillStyle = '#eab308'; ctx.beginPath(); ctx.arc(rx + 20, ry, 8, 0, 7); ctx.fill();
ctx.fillStyle = '#10b981'; ctx.beginPath(); ctx.moveTo(rx + 24, ry + 15); ctx.lineTo(rx + 80 + Math.sin(Date.now() * 0.007) * 4, ry + 35); ctx.lineTo(rx + 24, ry + 55); ctx.fill();
ctx.fillStyle = '#475569'; ctx.fillRect(rx, ry + goalFlag.height - 15, 40, 15); ctx.restore();
}
function drawPlayer() {
if (player.invincibilityFrames > 0 && Math.floor(player.invincibilityFrames / 4) % 2 === 0) return;
ctx.save(); ctx.translate(player.x - cameraX + player.width / 2, player.y - cameraY + player.height / 2); ctx.rotate(player.vx * 0.02);
if (player.powerupType === 'gem' || player.hasPassiveShield) {
ctx.strokeStyle = player.hasPassiveShield ? 'rgba(147, 197, 253, 0.8)' : 'rgba(236, 72, 153, 0.7)'; ctx.lineWidth = 4;
ctx.beginPath(); ctx.arc(0, 0, 32 + Math.sin(Date.now() * 0.02) * 3, 0, 7); ctx.stroke();
}
if (selectedCharacter === 'boy' || selectedCharacter === 'girl') {
ctx.fillStyle = selectedCharacter === 'boy' ? '#3b82f6' : '#ec4899'; ctx.beginPath(); ctx.roundRect(-19, -14, 38, 38, 12); ctx.fill();
ctx.fillStyle = '#fed7aa'; const ac = Math.sin(Date.now() * 0.015) * 8;
if (!player.isOnGround) { ctx.fillRect(-23, -12, 5, 12); ctx.fillRect(18, -12, 5, 12); }
else if (Math.abs(player.vx) > 0.2) { ctx.fillRect(-22, 4 + ac, 5, 10); ctx.fillRect(17, 4 - ac, 5, 10); }
else { ctx.fillRect(-22, 6, 5, 10); ctx.fillRect(17, 6, 5, 10); }
ctx.fillStyle = '#fed7aa'; ctx.beginPath(); ctx.arc(0, -12, 14, 0, 7); ctx.fill();
if (selectedCharacter === 'boy') { ctx.fillStyle = '#78350f'; ctx.beginPath(); ctx.arc(0, -15, 15, Math.PI, 0); ctx.fill(); ctx.fillStyle = '#1d4ed8'; ctx.beginPath(); ctx.arc(0, -18, 11, Math.PI, 0); ctx.fill(); }
else { ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.arc(0, -14, 15, Math.PI, 0); ctx.fill(); }
ctx.fillStyle = '#1e293b'; ctx.beginPath(); ctx.arc(-5, -12, 2.5, 0, 7); ctx.arc(5, -12, 2.5, 0, 7); ctx.fill();
ctx.strokeStyle = '#1e293b'; ctx.lineWidth = 1.8; ctx.beginPath(); ctx.arc(0, -9, 3, 0, Math.PI); ctx.stroke();
} else {
ctx.save(); ctx.scale(1.2, 1.2); if(selectedCharacter === 'snake') ctx.scale(1, 0.7); drawZodiacFace(ctx, selectedCharacter); ctx.restore();
}
ctx.restore();
}
function createJumpSpark(x, y) {
jumpSparks = []; for (let i = 0; i < 8; i++) jumpSparks.push({ x, y, vx: (Math.random() - 0.5) * 6, vy: (Math.random() - 0.5) * 6, life: 15 });
}
function updateAndDrawSparks() {
jumpSparks.forEach(s => {
s.x += s.vx; s.y += s.vy; s.life--;
if (s.life > 0) { ctx.fillStyle = '#fef08a'; ctx.beginPath(); ctx.arc(s.x - cameraX, s.y - cameraY, 4, 0, 7); ctx.fill(); }
});
}
function updatePhysics() {
if (gameState !== 'playing') return;
let activeSpeed = player.powerupType === 'potion' ? player.speed * 1.35 : player.speed;
if (keys.left) player.vx = -activeSpeed; else if (keys.right) player.vx = activeSpeed; else player.vx *= player.friction;
player.vy += GRAVITY; if (player.vy > 12) player.vy = 12;
player.x += player.vx; if (player.x < 0) { player.x = 0; player.vx = 0; }
const maxMapLimitX = isVerticalStage ? Math.max(800, canvas.width) : (1900 + (currentLevel * 350) + 100);
if (player.x > maxMapLimitX - player.width) player.x = maxMapLimitX - player.width;
coins.forEach(c => {
if (!c.isCollected && Math.hypot(player.x + player.width/2 - c.x, player.y + player.height/2 - c.y) < player.magnetRange) {
const a = Math.atan2(player.y + player.height/2 - c.y, player.x + player.width/2 - c.x); c.x += Math.cos(a)*5; c.y += Math.sin(a)*5;
}
});
if (selectedCharacter === 'sheep' && !player.hasPassiveShield) { if (--player.shieldCooldown <= 0) { player.hasPassiveShield = true; playSound('powerup'); } }
platforms.forEach(p => {
if (checkCollision(player, p)) {
if (p.type === 'spike') return handlePlayerHurt();
if (player.vx > 0) { player.x = p.x - player.width; player.vx = 0; } else if (player.vx < 0) { player.x = p.x + p.width; player.vx = 0; }
}
});
player.y += player.vy; player.isOnGround = false;
platforms.forEach(p => {
if (checkCollision(player, p)) {
if (p.type === 'spike') return handlePlayerHurt();
if (player.vy > 0) {
player.y = p.y - player.height; player.vy = 0; player.isOnGround = true; player.doubleJumpCount = 0;
if (p.isDisappearing) { p.timer++; p.opacity = Math.max(0, 1 - (p.timer / 30)); if (p.timer > 30) p.y += 1000; }
} else if (player.vy < 0) { player.y = p.y + p.height; player.vy = 0; }
}
});
if (selectedCharacter === 'rooster' && keys.jump && player.vy > 0.5) player.vy = 0.65;
if (player.powerupType && --player.powerupTimer <= 0) { player.powerupType = null; player.maxDoubleJumps = selectedCharacter === 'monkey' ? 2 : 0; document.getElementById('hud-powerup').classList.add('hidden'); }
if (player.invincibilityFrames > 0) player.invincibilityFrames--;
cameraX = Math.max(0, Math.min(cameraX * 0.9 + (player.x - canvas.width * (isVerticalStage ? 0.5 : 0.35)) * 0.1, isVerticalStage ? maxMapLimitX - canvas.width : 1900 + currentLevel * 350 - canvas.width));
cameraY = Math.min(50, cameraY * 0.9 + (isVerticalStage ? player.y - canvas.height * 0.55 : Math.min(0, player.y - canvas.height * 0.55)) * 0.1);
if (player.y > (isVerticalStage ? cameraY + canvas.height + 400 : canvas.height + 40)) handlePlayerHurt(true);
}
function checkCollision(a, b) { return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y; }
function performJump() {
if (player.isOnGround) { player.vy = -player.jumpForce; player.isOnGround = false; playSound('jump'); }
else if (player.doubleJumpCount < player.maxDoubleJumps) { player.vy = -player.jumpForce * 0.95; player.doubleJumpCount++; playSound('doubleJump'); createJumpSpark(player.x + 19, player.y + 48); }
}
function fireBullet() {
if (coinsCount >= 5 || selectedCharacter === 'dragon') { playSound('shoot'); playerBullets.push({ x: player.x + player.width, y: player.y + 24, vx: selectedCharacter === 'tiger' ? 11.5 : 7.5, vy: 0, life: 80 }); }
}
function updateCollisionsAndEntities() {
if (gameState !== 'playing') return;
coins.forEach(c => {
if (!c.isCollected && Math.hypot((player.x + 19) - c.x, (player.y + 24) - c.y) < c.radius + 20) {
c.isCollected = true; score += 100; coinsCount++;
document.getElementById('hud-score').textContent = score; document.getElementById('hud-coins').textContent = `${coinsCount}/5`; playSound('coin');
if (coinsCount === 5) {
const ws = document.getElementById('hud-weapon-status'); ws.textContent = "🔥 총알 공격 준비완료! (PC: F / 모바일: 공격)"; ws.classList.replace('bg-red-600/90', 'bg-emerald-500/90'); playSound('powerup');
}
}
});
for(let i=playerBullets.length-1; i>=0; i--) {
let b = playerBullets[i]; b.x += b.vx; b.life--;
if (b.life <= 0) { playerBullets.splice(i, 1); continue; }
for(let e of enemies) {
if (!e.squashed && Math.hypot(b.x - (e.x + e.width/2), b.y - (e.y + e.height/2)) < 26) {
playerBullets.splice(i, 1); e.bossHp -= selectedCharacter === 'tiger' ? 2 : 1; playSound('stomp');
if (e.bossHp <= 0) { e.squashed = true; score += e.type === 'boss' ? 3000 : 200; document.getElementById('hud-score').textContent = score; }
break;
}
}
}
for(let i=projectiles.length-1; i>=0; i--) {
let p = projectiles[i]; p.x += p.vx; p.y += p.vy; p.life--;
if (p.life <= 0) { projectiles.splice(i, 1); continue; }
if (Math.hypot((player.x + 19) - p.x, (player.y + 24) - p.y) < 20) {
if (player.powerupType === 'gem' || player.hasPassiveShield) { projectiles.splice(i, 1); playSound('coin'); }
else { handlePlayerHurt(); projectiles.splice(i, 1); }
}
}
items.forEach(it => { if (!it.isCollected && checkCollision(player, it)) { it.isCollected = true; playSound(it.type === 'heart' ? 'heal' : 'powerup'); applyPowerup(it.type); } });
enemies.forEach(e => {
if (e.squashed) return;
e.x += e.speed * e.dir; if (Math.abs(e.x - e.startX) > e.patrolDist) e.dir *= -1;
if (e.type === 'bat') e.y += Math.sin(Date.now() * 0.015) * 1.5;
if (--e.shootCooldown <= 0) {
if (Math.abs(player.x - e.x) < 420) {
if (e.type === 'slime') { projectiles.push({ x: e.x + 17, y: e.y + 15, vx: (player.x < e.x ? -1 : 1) * (4.2 + currentLevel * 0.3), vy: 0, type: 'acid', life: 100 }); e.shootCooldown = 110 - currentLevel * 5; }
else if (e.type === 'bat') { projectiles.push({ x: e.x + 17, y: e.y + 30, vx: 0, vy: 5.2 + currentLevel * 0.4, type: 'darkness', life: 110 }); e.shootCooldown = 80 - currentLevel * 4; }
else if (e.type === 'boss') {
const ang = Math.atan2((player.y + 24) - (e.y + 30), (player.x + 19) - (e.x + 35));
[-0.35, 0, 0.35].forEach(off => projectiles.push({ x: e.x + 35, y: e.y + 30, vx: Math.cos(ang + off)*5.2, vy: Math.sin(ang + off)*5.2, type: 'boss_fire', life: 140 })); e.shootCooldown = 75;
}
} else e.shootCooldown = 30;
}
if (checkCollision(player, e)) {
if (player.vy > 0 && player.y + player.height - player.vy <= e.y + 16) {
player.vy = -player.jumpForce * 0.85; playSound('stomp'); e.bossHp--;
if (e.bossHp <= 0) { e.squashed = true; score += e.type === 'boss' ? 3000 : 200; } else if (e.type === 'boss') e.x += e.dir * -50;
document.getElementById('hud-score').textContent = score;
} else if (player.powerupType === 'gem' || player.hasPassiveShield) { e.squashed = true; playSound('stomp'); score += 200; document.getElementById('hud-score').textContent = score; }
else handlePlayerHurt();
}
});
if (checkCollision(player, goalFlag)) handleStageClear();
}
function applyPowerup(type) {
player.powerupType = type; const banner = document.getElementById('hud-powerup'); banner.classList.remove('hidden');
if (type === 'star') { player.maxDoubleJumps = selectedCharacter === 'monkey' ? 3 : 1; player.powerupTimer = 600; document.getElementById('powerup-icon').textContent = '⭐'; document.getElementById('powerup-text').textContent = '공중 다단 점프 개방!'; }
else if (type === 'gem') { player.powerupTimer = 480; document.getElementById('powerup-icon').textContent = '🛡️'; document.getElementById('powerup-text').textContent = '천하무적 반짝 보호막!'; }
else if (type === 'potion') { player.powerupTimer = 600; document.getElementById('powerup-icon').textContent = '⚡'; document.getElementById('powerup-text').textContent = '바람보다 빠른 신속 물약!'; }
else if (type === 'heart') { if (lives < maxLives) { lives++; updateLivesHUD(); } player.powerupType = null; banner.classList.add('hidden'); }
}
function handlePlayerHurt(forceInstantKill = false) {
if (player.invincibilityFrames > 0 && !forceInstantKill) return;
if (player.powerupType === 'gem' && !forceInstantKill) return;
if (player.hasPassiveShield && !forceInstantKill) { player.hasPassiveShield = false; player.shieldCooldown = 1200; player.invincibilityFrames = 90; playSound('heal'); return; }
lives--; playSound('hurt'); updateLivesHUD();
if (lives <= 0 || forceInstantKill) { gameState = 'fail'; clearInterval(timerInterval); playSound('fail'); showScreen('fail'); }
else { player.invincibilityFrames = 120; player.vy = -6; player.vx = -6; }
}
function updateLivesHUD() { document.getElementById('hud-lives').textContent = '❤️'.repeat(lives) + '🖤'.repeat(maxLives - lives); }
function handleStageClear() {
gameState = 'clear'; clearInterval(timerInterval); playSound('clear');
score += levelTimer * 50; document.getElementById('hud-score').textContent = score;
if (currentLevel === unlockedLevel && unlockedLevel < maxLevel) unlockedLevel++;
if (currentLevel === maxLevel) { document.getElementById('final-score').textContent = score.toLocaleString() + " 점"; showScreen('ending'); }
else { document.getElementById('clear-title').textContent = `스테이지 ${currentLevel} 완료!`; showScreen('clear'); }
}
function startGame(level) {
showScreen('preview'); gameState = 'preview';
document.getElementById('preview-title').textContent = `스테이지 ${level}`;
document.getElementById('preview-mission').innerHTML = (level % 2 === 0) ? "임무: 위로 높이 올라가 타워 정상에 도달하세요! 👆" : "임무: 앞으로 치고 달려가 우측 돌파구로 나가세요! 👉";
setTimeout(() => executeStartGame(level), 2500);
}
function executeStartGame(level) {
gameState = 'playing'; currentLevel = level; coinsCount = 0;
Object.assign(player, {
speed: 4.9, jumpForce: 11.2, width: 38, height: 48, friction: 0.82,
magnetRange: 140, maxDoubleJumps: 2, // 모든 캐릭터 3단 점프 기본 적용
hasPassiveShield: false, shieldCooldown: 0, x: 100, y: 200, vx: 0, vy: 0,
isOnGround: false, powerupType: null, powerupTimer: 0, invincibilityFrames: 0
});
maxLives = 3;
if (selectedCharacter === 'girl') player.speed = 5.7;
else if (selectedCharacter === 'rat') { player.width = 28; player.height = 36; player.speed = 5.4; }
else if (selectedCharacter === 'ox') maxLives = 5;
else if (selectedCharacter === 'tiger') { player.speed = 6.1; player.jumpForce = 11.5; }
else if (selectedCharacter === 'rabbit') { player.jumpForce = 14.0; player.speed = 4.8; }
else if (selectedCharacter === 'snake') player.height = 24;
else if (selectedCharacter === 'horse') { player.speed = 7.2; player.friction = 0.95; }
else if (selectedCharacter === 'sheep') player.hasPassiveShield = true;
else if (selectedCharacter === 'monkey') player.maxDoubleJumps = 2; // 원숭이는 그대로 유지
else if (selectedCharacter === 'dog') player.magnetRange = 300;
else if (selectedCharacter === 'pig') { maxLives = 6; player.speed = 4.3; }
lives = maxLives; updateLivesHUD(); levelTimer = 120;
document.getElementById('hud-level').textContent = `스테이지 ${currentLevel}`; document.getElementById('hud-score').textContent = score; document.getElementById('hud-coins').textContent = `0/5`; document.getElementById('hud-timer').textContent = levelTimer; document.getElementById('hud-powerup').classList.add('hidden');
const ws = document.getElementById('hud-weapon-status');
if (selectedCharacter === 'dragon') { ws.textContent = "🔥 용의 특수능력: 총알 발사 무제한!"; ws.classList.replace('bg-red-600/90', 'bg-emerald-500/90'); }
else { ws.textContent = "🔒 코인 5개를 모으면 총알 가능!"; ws.classList.replace('bg-emerald-500/90', 'bg-red-600/90'); }
generateMap(currentLevel); cameraX = 0; cameraY = isVerticalStage ? player.y - 240 : 0;
showScreen('playing'); startTimer(); startBGM('stage');
}
window.addEventListener('keydown', e => {
if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyF'].includes(e.code)) e.preventDefault();
if (e.code === 'ArrowLeft') keys.left = true; if (e.code === 'ArrowRight') keys.right = true;
if ((e.code === 'Space' || e.code === 'ArrowUp') && !keys.jump) { performJump(); keys.jump = true; }
if (e.code === 'KeyF' && !keys.shoot) { fireBullet(); keys.shoot = true; }
});
window.addEventListener('keyup', e => {
if (e.code === 'ArrowLeft') keys.left = false; if (e.code === 'ArrowRight') keys.right = false;
if (e.code === 'Space' || e.code === 'ArrowUp') keys.jump = false; if (e.code === 'KeyF') keys.shoot = false;
});
function setupMobileControls() {
const attach = (id, k, act) => {
const b = document.getElementById(id);
b.addEventListener('touchstart', e => { e.preventDefault(); initAudio(); if(act) act(); else keys[k] = true; });
b.addEventListener('touchend', e => { e.preventDefault(); if(!act) keys[k] = false; else if(k) keys[k] = false; });
};
attach('btn-left', 'left'); attach('btn-right', 'right');
attach('btn-jump', 'jump', () => { performJump(); keys.jump = true; }); attach('btn-shoot', '', fireBullet);
}
document.getElementById('btn-tutorial-ok').onclick = () => { initAudio(); playSound('clear'); showScreen('start'); };
document.getElementById('btn-go-char').onclick = () => { initAudio(); playSound('coin'); showScreen('char_select'); };
['btn-back-start', 'btn-back-char', 'btn-go-levels', 'btn-go-levels-fail'].forEach(id => {
document.getElementById(id).onclick = () => { playSound('jump'); showScreen(id.includes('start') ? 'start' : id.includes('char') ? 'char_select' : 'level_select'); };
});
document.getElementById('btn-next-stage').onclick = () => { playSound('coin'); startGame(++currentLevel); };
document.getElementById('btn-retry').onclick = () => { playSound('coin'); startGame(currentLevel); };
document.getElementById('btn-ending-restart').onclick = () => { playSound('coin'); unlockedLevel = 1; score = 0; showScreen('start'); };
setupMobileControls();
function resizeCanvas() { canvas.width = canvas.parentElement.clientWidth; canvas.height = 480; }
window.addEventListener('resize', resizeCanvas);
function mainLoop() {
updatePhysics(); updateCollisionsAndEntities();
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawSkyGradient(); drawClouds(); drawPlatforms(); drawCoins(); drawItems(); drawEnemies(); drawProjectiles(); drawPlayerBullets(); drawGoalFlag(); updateAndDrawSparks(); drawPlayer();
requestAnimationFrame(mainLoop);
}
window.onload = function () { resizeCanvas(); requestAnimationFrame(mainLoop); };
</script>
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels