let trail = []; let velocities = []; let particles = []; let flashAlpha = 0; let score = 0; let bgBlue = 0; let beachImg; let moanaFont; let beachSound, sliceSound; let charFrames = []; let slotChars = new Array(10).fill(-1); let slotOffsets = new Array(10).fill(0); const ANI_FPS = 18; const CHAR_SCALES = [0.9, 1.5, 1.2, 1.5, 1.5, 1.5, 1.5]; let charOrder = []; let charOrderIdx = 0; // ====== GAME STATE ====== let gameState = 'name'; // 'name' | 'intro' | 'playing' | 'departure' let playerName = ''; let nameInput, nameConfirmBtn; const COCONUT_TIME_MS = 5000; let coconutTimerEnd = 0; // millis() when current coconut expires let slotPowers = []; // 0–100, speed bonus per slot let coconut = { x: 0, y: 0, r: 0, state: 0, slices: 0, shakeX: 0, shakeY: 0, shakeTimer: 0, baseAngle: 0, flying: false, flyVX: 0, flyVY: 0, flyAlpha: 255, flyRotation: 0, flyRotSpeed: 0, entering: false, enterT: 0, enterFromX: 0, enterFromY: 0 }; let coconutImgs = []; let flyingKakamoraList = []; let knifeImg; let effectImgs = []; let imgParticles = []; let slashLingerAlpha = 0; let lingerTrail = []; let lingerVelocities = []; // ====== PRELOAD ====== function preload() { for (let i = 0; i < 5; i++) { coconutImgs[i] = loadImage(`assets/coconut/coconut_${i}.png`); } // char1 charFrames[0] = []; for (let i = 1; i <= 5; i++) charFrames[0].push(loadImage(`assets/kakamora-characters/char1/co_ani_${i}.png`)); // char2–7: Frame N.png const charRanges = [[6,10],[11,15],[16,21],[22,26],[27,31],[32,36]]; for (let c = 0; c < charRanges.length; c++) { let [start, end] = charRanges[c]; charFrames[c + 1] = []; for (let n = start; n <= end; n++) charFrames[c + 1].push(loadImage(`assets/kakamora-characters/char${c + 2}/Frame ${n}.png`)); } beachImg = loadImage('assets/beach.png'); moanaFont = loadFont('assets/fonts/Moanas.ttf'); knifeImg = loadImage('assets/knife.png'); for (let i = 0; i < 3; i++) effectImgs.push(loadImage(`assets/effect/particles${i}.png`)); } // ====== SETUP ====== function setup() { createCanvas(windowWidth, windowHeight); resetCoconutHome(); world.gravity.y = 0; shuffleCharOrder(); beachSound = new Howl({ src: ['assets/sound/beach%20sound.wav'], loop: true, volume: 0.45, fade: true }); sliceSound = new Howl({ src: ['assets/sound/slice.wav'], volume: 0.75 }); // HTML name input nameInput = createInput(''); nameInput.attribute('placeholder', 'Enter your name...'); nameInput.attribute('maxlength', '16'); nameInput.style('font-size', '18px'); nameInput.style('font-family', 'Google Sans, Nunito, sans-serif'); nameInput.style('background', 'rgba(14, 20, 36, 0.92)'); nameInput.style('color', '#e8f4ff'); nameInput.style('border', '2px solid rgba(90, 185, 255, 0.55)'); nameInput.style('border-radius', '2px'); nameInput.style('padding', '10px 16px'); nameInput.style('width', '260px'); nameInput.style('outline', 'none'); nameInput.style('letter-spacing', '1px'); nameInput.style('position', 'absolute'); nameInput.elt.addEventListener('keydown', e => { if (e.key === 'Enter') confirmName(); }); nameConfirmBtn = createButton('CONFIRM'); nameConfirmBtn.style('font-size', '16px'); nameConfirmBtn.style('font-family', 'Google Sans, Nunito, sans-serif'); nameConfirmBtn.style('font-weight', '700'); nameConfirmBtn.style('letter-spacing', '2px'); nameConfirmBtn.style('background', 'rgba(18, 80, 150, 0.95)'); nameConfirmBtn.style('color', '#cce8ff'); nameConfirmBtn.style('border', '2px solid rgba(90, 185, 255, 0.7)'); nameConfirmBtn.style('border-radius', '2px'); nameConfirmBtn.style('padding', '10px 36px'); nameConfirmBtn.style('cursor', 'pointer'); nameConfirmBtn.style('position', 'absolute'); nameConfirmBtn.mousePressed(confirmName); positionNameUI(); } function positionNameUI() { let pw = min(windowWidth * 0.5, 440); let ph = 240; let px = (windowWidth - pw) / 2; let py = (windowHeight - ph) / 2; let inputW = pw * 0.65; let inputX = px + (pw - inputW) / 2; nameInput.position(inputX, py + 104); nameInput.style('width', inputW + 'px'); nameConfirmBtn.position(px + pw / 2 - 80, py + 164); } function shuffleCharOrder() { charOrder = [0, 1, 2, 3, 4, 5, 6]; for (let i = charOrder.length - 1; i > 0; i--) { let j = floor(random(i + 1)); [charOrder[i], charOrder[j]] = [charOrder[j], charOrder[i]]; } charOrderIdx = 0; } function windowResized() { resizeCanvas(windowWidth, windowHeight); resetCoconutHome(); if (gameState === 'name') positionNameUI(); } function resetCoconutHome() { coconut.r = min(windowWidth, windowHeight) * 0.13; coconut.x = width / 2; coconut.y = height * 0.42; } // ====== DRAW ====== function draw() { imageMode(CORNER); image(beachImg, 0, 0, width, height); if (bgBlue > 0) { fill(25, 90, 200, bgBlue * 0.4); noStroke(); rect(0, 0, width, height); bgBlue = max(0, bgBlue - 4); } if (gameState === 'name') { drawNameEntry(); return; } if (gameState === 'playing') { checkCoconutTimer(); updateShake(); updateEnter(); updateFlyAway(); drawCoconut(); drawCoconutTimer(); drawTrail(); updateImgParticles(); drawImgParticles(); } drawParticles(); drawFlash(); if (gameState !== 'intro' && gameState !== 'name') { drawKakamoraSlots(); updateFlyingKakamoraList(); drawFlyingKakamoraList(); } if (gameState === 'intro') drawIntroQuest(); if (gameState === 'departure') drawDepartureQuest(); } // ====== COCONUT ====== function drawCoconut() { imageMode(CENTER); if (coconut.flying) { push(); translate(coconut.x, coconut.y); rotate(coconut.flyRotation); tint(255, coconut.flyAlpha); image(coconutImgs[coconut.state], 0, 0, coconut.r * 2, coconut.r * 2); noTint(); pop(); } else { let alpha = coconut.entering ? lerp(40, 255, min(coconut.enterT * 1.8, 1)) : 255; tint(255, alpha); push(); translate(coconut.x + coconut.shakeX, coconut.y + coconut.shakeY); rotate(coconut.baseAngle); imageMode(CENTER); image(coconutImgs[coconut.state], 0, 0, coconut.r * 2, coconut.r * 2); pop(); noTint(); } } function updateEnter() { if (!coconut.entering) return; coconut.enterT += 0.038; let ease = 1 - pow(1 - min(coconut.enterT, 1), 3); coconut.x = lerp(coconut.enterFromX, width / 2, ease); coconut.y = lerp(coconut.enterFromY, height * 0.42, ease); if (coconut.enterT >= 1) { coconut.entering = false; coconut.x = width / 2; coconut.y = height * 0.42; } } function startCoconutEntrance() { coconut.entering = true; coconut.enterT = 0; coconut.enterFromX = width - 100; coconut.enterFromY = 100; coconut.x = width - 100; coconut.y = 100; coconut.baseAngle = random(TWO_PI); } function updateShake() { if (coconut.shakeTimer > 0) { coconut.shakeX = sin(coconut.shakeTimer * 2.4) * coconut.shakeTimer * 0.7; coconut.shakeY = cos(coconut.shakeTimer * 3.1) * coconut.shakeTimer * 0.4; coconut.shakeTimer--; } else { coconut.shakeX = 0; coconut.shakeY = 0; } } function updateFlyAway() { if (!coconut.flying) return; coconut.x += coconut.flyVX; coconut.y += coconut.flyVY; coconut.flyVY += 0.55; coconut.flyRotation += coconut.flyRotSpeed; coconut.flyAlpha -= 7; if (coconut.flyAlpha <= 0 || coconut.y > height + 100) { if (score < 10) launchKakamoraToSlot(score); score++; coconut.state = 0; coconut.slices = 0; coconut.flying = false; coconut.flyAlpha = 255; coconut.flyRotation = 0; coconut.shakeX = 0; coconut.shakeY = 0; coconut.shakeTimer = 0; coconutTimerEnd = millis() + COCONUT_TIME_MS; if (score >= 7 && gameState === 'playing') { gameState = 'departure'; } else { startCoconutEntrance(); } } } // ====== TRAIL INPUT ====== function mouseDragged() { if (gameState !== 'playing') return; let pos = createVector(mouseX, mouseY); trail.push(pos); let dx = 0, dy = 0; if (trail.length > 1) { let prev = trail[trail.length - 2]; dx = pos.x - prev.x; dy = pos.y - prev.y; velocities.push(sqrt(dx * dx + dy * dy)); } else { velocities.push(0); } if (dist(mouseX, mouseY, coconut.x, coconut.y) < coconut.r && !coconut.flying) { spawnParticles(mouseX, mouseY); spawnImgParticles(mouseX, mouseY, dx, dy); coconut.shakeTimer = max(coconut.shakeTimer, 18); } if (trail.length > 22) trail.shift(); if (velocities.length > 22) velocities.shift(); } function mouseReleased() { if (gameState !== 'playing') { trail = []; velocities = []; return; } let hits = trail.filter(p => dist(p.x, p.y, coconut.x, coconut.y) < coconut.r).length; if (hits >= 2 && !coconut.flying) { sliceSound.play(); coconut.slices++; coconut.state = floor(coconut.slices / 2); flashAlpha = 25; if (coconut.slices >= 8) { let power = floor(max(0, (coconutTimerEnd - millis()) / COCONUT_TIME_MS) * 100); slotPowers[score] = power; coconut.flying = true; coconut.flyVX = random([-1, 1]) * random(3, 6); coconut.flyVY = random(-6, -2); coconut.flyRotSpeed = random() > 0.5 ? random(0.07, 0.13) : random(-0.13, -0.07); bgBlue = 255; } } // linger the slash line briefly lingerTrail = trail.slice(); lingerVelocities = velocities.slice(); slashLingerAlpha = 255; trail = []; velocities = []; } // ====== TRAIL DRAW ====== function drawSlashSegments(pts, vels, masterAlpha) { if (pts.length < 2) return; noFill(); for (let i = 1; i < pts.length; i++) { let t = i / pts.length; // 0=tail, 1=head let speed = vels[i] || 0; let a = t * masterAlpha; // Outer glow strokeWeight(map(speed, 0, 40, 6, 22)); stroke(255, 240, 180, a * 0.18); line(pts[i-1].x, pts[i-1].y, pts[i].x, pts[i].y); // Mid glow strokeWeight(map(speed, 0, 40, 3, 12)); stroke(255, 255, 220, a * 0.45); line(pts[i-1].x, pts[i-1].y, pts[i].x, pts[i].y); // Sharp core strokeWeight(map(speed, 0, 40, 1, 4)); stroke(255, 255, 255, a); line(pts[i-1].x, pts[i-1].y, pts[i].x, pts[i].y); } noStroke(); } function drawTrail() { drawSlashSegments(trail, velocities, 255); // Linger fade after release if (slashLingerAlpha > 0) { drawSlashSegments(lingerTrail, lingerVelocities, slashLingerAlpha); slashLingerAlpha -= 28; if (slashLingerAlpha <= 0) { lingerTrail = []; lingerVelocities = []; } } } // ====== SLOT LAYOUT (horizontal top-left) ====== const S_MARGIN = 14; const S_HDRH = 30; const S_PAD = 12; const S_GAP = 10; function getSlotSize() { // fit 7 slots between left margin and timer area (right ~220px) let available = width - S_MARGIN * 2 - S_PAD * 2 - 6 * S_GAP - 220; return constrain(floor(available / 7), 60, 92); } function getSlotPos(i) { let sz = getSlotSize(); return { x: S_MARGIN + S_PAD + sz / 2 + i * (sz + S_GAP), y: S_MARGIN + S_HDRH + S_PAD + sz / 2, size: sz }; } // ====== KAKAMORA SLOTS ====== function drawKakamoraSlots() { let sz = getSlotSize(); let panelW = S_PAD * 2 + 7 * sz + 6 * S_GAP; let pwrH = 20; let panelH = S_HDRH + S_PAD + sz + pwrH + S_PAD; let px = S_MARGIN; let py = S_MARGIN; // Panel background fill(12, 16, 26, 210); noStroke(); rect(px, py, panelW, panelH, 2); // Header bar fill(35, 42, 60, 255); noStroke(); rect(px, py, panelW, S_HDRH, 2, 2, 0, 0); fill(255, 195, 55); textAlign(LEFT, CENTER); textFont(moanaFont); textSize(13); text('WARRIOR CREW', px + S_PAD, py + S_HDRH / 2); // score counter right side textAlign(RIGHT, CENTER); fill(200, 210, 230); textSize(12); text(`${score} / 7`, px + panelW - S_PAD, py + S_HDRH / 2); textFont('sans-serif'); for (let i = 0; i < 7; i++) { let { x, y, size } = getSlotPos(i); let filled = i < score; if (filled && slotChars[i] >= 0) { let frames = charFrames[slotChars[i]]; let frameIdx = floor((frameCount + slotOffsets[i]) / ANI_FPS) % frames.length; let img = frames[frameIdx]; let sc = min(size / img.width, size / img.height) * CHAR_SCALES[slotChars[i]]; drawingContext.save(); drawingContext.beginPath(); drawingContext.arc(x, y, size / 2, 0, Math.PI * 2); drawingContext.clip(); imageMode(CENTER); image(img, x, y, img.width * sc, img.height * sc); drawingContext.restore(); // Power display — threshold at 55: pwr >= 55 = full green let pwr = slotPowers[i] !== undefined ? slotPowers[i] : 0; let pwrT = min(pwr / 55, 1); let pwrCol = lerpColor(color(220, 50, 50), color(50, 215, 85), pwrT); // Glow ring (outer) — pulses slightly based on power noFill(); strokeWeight(7); stroke(red(pwrCol), green(pwrCol), blue(pwrCol), 55 + pwrT * 50); ellipse(x, y, size + 11, size + 11); // Sharp ring (inner) strokeWeight(2.5); stroke(pwrCol); ellipse(x, y, size + 2, size + 2); noStroke(); // Power badge below circle let pillW = floor(size * 0.86); let pillH = 23; let pillX = x - pillW / 2; let pillY = y + size / 2 + 6; // shadow fill(0, 0, 0, 150); rect(pillX + 1, pillY + 1, pillW, pillH, 12); // dark background fill(8, 10, 20, 240); rect(pillX, pillY, pillW, pillH, 12); // progress fill — bar shows true pwr/100, color uses lowered threshold fill(red(pwrCol), green(pwrCol), blue(pwrCol), 210); let barW = max(0, (pillW - 4) * (pwr / 100)); if (barW > 0) rect(pillX + 2, pillY + 2, barW, pillH - 4, 10); // label: "PWR XX" fill(255, 255, 255, 245); textAlign(CENTER, CENTER); textFont('Google Sans, Nunito, sans-serif'); textStyle(BOLD); textSize(12); text(`PWR ${pwr}`, x, pillY + pillH / 2); textStyle(NORMAL); textFont('sans-serif'); } else { fill(22, 28, 42); noStroke(); ellipse(x, y, size, size); stroke(50, 58, 78); strokeWeight(1.5); noFill(); ellipse(x, y, size, size); fill(55, 65, 88); noStroke(); textAlign(CENTER, CENTER); textSize(13); text(i + 1, x, y); } } } // ====== FLYING KAKAMORA ====== function launchKakamoraToSlot(slotIdx) { let { x: tx, y: ty, size } = getSlotPos(slotIdx); let charIdx = charOrder[charOrderIdx % 7]; charOrderIdx++; slotChars[slotIdx] = charIdx; slotOffsets[slotIdx] = floor(random(1000)); flyingKakamoraList.push({ startX: width / 2, startY: height * 0.42, targetX: tx, targetY: ty, t: 0, charIdx, questShown: false, size }); } function updateFlyingKakamoraList() { for (let k of flyingKakamoraList) k.t += 0.032; flyingKakamoraList = flyingKakamoraList.filter(k => k.t <= 1); } function drawFlyingKakamoraList() { for (let k of flyingKakamoraList) { let ease = 1 - pow(1 - min(k.t, 1), 2); let x = lerp(k.startX, k.targetX, ease); let y = lerp(k.startY, k.targetY, ease) - sin(k.t * PI) * height * 0.3; let sz = lerp(k.size * 1.8, k.size, ease); let frames = charFrames[k.charIdx]; let frameIdx = floor(frameCount / ANI_FPS) % frames.length; let img = frames[frameIdx]; let sc = min(sz / img.width, sz / img.height) * CHAR_SCALES[k.charIdx]; drawingContext.save(); drawingContext.beginPath(); drawingContext.arc(x, y, sz / 2, 0, Math.PI * 2); drawingContext.clip(); imageMode(CENTER); image(img, x, y, img.width * sc, img.height * sc); drawingContext.restore(); } } // ====== PARTICLES ====== class Particle { constructor(x, y) { this.pos = createVector(x, y); // 전방향 랜덤 퍼짐 (드래그 방향 편향 없이 360도) let baseAngle = random(TWO_PI); let spd = random(2, 6); this.vel = createVector(cos(baseAngle) * spd, sin(baseAngle) * spd); this.life = 255; this.r = random(1.5, 3.5); this.isHusk = random() > 0.4; } update() { this.pos.add(this.vel); this.vel.y += 0.22; this.vel.mult(0.91); this.life -= 9; } draw() { noStroke(); if (this.isHusk) fill(158, 108, 52, this.life); else fill(242, 224, 188, this.life); ellipse(this.pos.x, this.pos.y, this.r, this.r * 0.55); } } function spawnParticles(x, y) { for (let i = 0; i < 12; i++) particles.push(new Particle(x, y)); } function drawParticles() { for (let i = particles.length - 1; i >= 0; i--) { particles[i].update(); particles[i].draw(); if (particles[i].life <= 0) particles.splice(i, 1); } } // ====== FLASH ====== function drawFlash() { if (flashAlpha > 0) { fill(255, flashAlpha); rect(0, 0, width, height); flashAlpha -= 20; } } // ====== COCONUT TIMER ====== function checkCoconutTimer() { if (coconut.flying || coconutTimerEnd === 0) return; if (millis() > coconutTimerEnd) { coconut.state = 0; coconut.slices = 0; coconutTimerEnd = millis() + COCONUT_TIME_MS; } } function drawCoconutTimer() { if (coconut.flying || coconutTimerEnd === 0) return; let fraction = max(0, (coconutTimerEnd - millis()) / COCONUT_TIME_MS); let secs = (fraction * (COCONUT_TIME_MS / 1000)).toFixed(1); let c = lerpColor(color(255, 60, 60), color(80, 220, 100), fraction); // Top-right pill let tx = width - 18; let ty = 18; let label = `TIME ${secs}s`; textFont('Google Sans, Nunito, sans-serif'); textSize(26); let tw = textWidth(label) + 36; let th = 54; fill(12, 16, 26, 220); noStroke(); rect(tx - tw, ty, tw, th, 2); // Progress bar fill(30, 37, 54); rect(tx - tw + 8, ty + th - 12, tw - 16, 6, 2); fill(c); rect(tx - tw + 8, ty + th - 12, (tw - 16) * fraction, 6, 2); fill(c); textAlign(RIGHT, TOP); text(label, tx - 10, ty + 10); textFont('sans-serif'); } // ====== SHARED DIALOG HELPER ====== function drawDialogBox(dx, dy, dw, dh, speakerName, lines, btnLabel) { fill(0, 0, 0, 90); noStroke(); rect(0, 0, width, height); fill(28, 32, 45, 185); noStroke(); rect(dx, dy, dw, dh, 2); // Name banner — sharp corners textFont(moanaFont); textSize(20); let bw = textWidth(speakerName) + 44; let bh = 36; fill(210, 105, 20); stroke(255, 175, 70); strokeWeight(2); rect(dx + 22, dy - bh / 2, bw, bh, 2); noStroke(); fill(255); textAlign(LEFT, CENTER); text(speakerName, dx + 34, dy + 2); // Quest lines — system font for readability textFont('sans-serif'); textSize(14); fill(210, 210, 215); textAlign(LEFT, TOP); for (let i = 0; i < lines.length; i++) { text(`• ${lines[i]}`, dx + 28, dy + 46 + i * 34); } // Button row — Moana font let btnW = 180; let btnH = 36; let btnX = dx + dw / 2 - btnW / 2; let btnY = dy + dh - btnH - 16; fill(18, 80, 150); noStroke(); rect(btnX, btnY, btnW, btnH, 2); stroke(90, 185, 255); strokeWeight(1.5); noFill(); rect(btnX, btnY, btnW, btnH, 2); noStroke(); fill(210, 240, 255); textFont(moanaFont); textAlign(CENTER, CENTER); textSize(18); text(btnLabel, dx + dw / 2, btnY + btnH / 2); textFont('sans-serif'); } // ====== INTRO QUEST ====== function drawIntroQuest() { let dw = min(width * 0.54, 580); let dh = 230; let dx = (width - dw) / 2; let dy = (height - dh) / 2; drawDialogBox(dx, dy, dw, dh, 'Chief Tamatoa', [ 'You have 5 seconds to crack each coconut and claim a warrior.', 'Strike fast. The quicker the slash, the stronger they become.', 'Recruit all 7 warriors before we head into battle.' ], 'BEGIN'); } // ====== DEPARTURE QUEST ====== function drawDepartureQuest() { let dw = min(width * 0.54, 580); let dh = 190; let dx = (width - dw) / 2; let dy = height - dh - 28; drawDialogBox(dx, dy, dw, dh, 'Chief Tamatoa', [ 'Seven warriors stand ready. The crew is assembled.', 'The enemy will not hold back. Neither will we.', 'The tide is turning. Now we sail and fight.' ], 'SET SAIL'); } // ====== NAME ENTRY ====== function drawNameEntry() { fill(0, 0, 0, 130); noStroke(); rect(0, 0, width, height); let pw = min(width * 0.5, 440); let ph = 240; let px = (width - pw) / 2; let py = (height - ph) / 2; fill(14, 20, 36, 225); noStroke(); rect(px, py, pw, ph, 2); // Header bar fill(24, 35, 58); rect(px, py, pw, 42, 2, 2, 0, 0); textFont(moanaFont); textAlign(CENTER, CENTER); textSize(18); fill(255, 215, 80); text('KAKAMORA WARRIOR', width / 2, py + 21); textFont('sans-serif'); // Prompt fill(160, 185, 220); textAlign(CENTER, CENTER); textSize(13); text('Enter your name to join the crew.', width / 2, py + 72); text('Your score will be saved to the leaderboard.', width / 2, py + 90); } function confirmName() { let val = nameInput.value().trim(); if (val.length === 0) return; playerName = val; nameInput.hide(); nameConfirmBtn.hide(); gameState = 'intro'; } // ====== IMAGE PARTICLES ====== class ImgParticle { constructor(x, y, dx, dy) { this.pos = createVector(x, y); let baseAngle = (dx !== 0 || dy !== 0) ? atan2(dy, dx) + PI + random(-0.6, 0.6) : random(TWO_PI); let spd = random(1.5, 4.5); this.vel = createVector(cos(baseAngle) * spd, sin(baseAngle) * spd); this.life = 255; this.size = random(20, 40); this.img = random(effectImgs); this.rot = random(TWO_PI); this.rotSpeed = random(-0.18, 0.18); } update() { this.pos.add(this.vel); this.vel.y += 0.14; this.vel.mult(0.91); this.life -= 14; this.rot += this.rotSpeed; } draw() { push(); translate(this.pos.x, this.pos.y); rotate(this.rot); tint(255, this.life); imageMode(CENTER); image(this.img, 0, 0, this.size, this.size); noTint(); pop(); } } function spawnImgParticles(x, y, dx, dy) { for (let i = 0; i < 4; i++) imgParticles.push(new ImgParticle(x, y, dx, dy)); } function updateImgParticles() { for (let i = imgParticles.length - 1; i >= 0; i--) { imgParticles[i].update(); if (imgParticles[i].life <= 0) imgParticles.splice(i, 1); } } function drawImgParticles() { for (let p of imgParticles) p.draw(); } // ====== MOUSE HANDLER ====== function mousePressed() { if (gameState === 'name') return; if (gameState === 'intro') { gameState = 'playing'; coconutTimerEnd = millis() + COCONUT_TIME_MS; startCoconutEntrance(); beachSound.play(); return; } if (gameState === 'departure') { // 출항 처리 (추후 배틀 씬으로 전환) return; } }