let trail = []; let velocities = []; let particles = []; let flashAlpha = 0; let score = 0; let bgBlue = 0; let treeImgs = {}; let treeHits = 0; const TREE_COUNT = 8; let waveTime = 0; let idleSprite; let idleAni; let coconut = { x: 0, y: 0, r: 0, state: 0, shakeX: 0, shakeTimer: 0, flying: false, flyVX: 0, flyVY: 0, flyAlpha: 255, flyRotation: 0, flyRotSpeed: 0 }; let coconutImgs = []; let kakomoraImgs = []; let flyingKakamoraList = []; // ====== PRELOAD ====== function preload() { for (let i = 0; i < 5; i++) { coconutImgs[i] = loadImage(`assets/coconut/coconut_${i}.png`); } kakomoraImgs[0] = loadImage('assets/kakamora-characters/Kakamora-TikiRob-Claw-Edition.png'); kakomoraImgs[1] = loadImage('assets/kakamora-characters/Kokomoro-TikiRob-Fishhead-Edition.webp'); kakomoraImgs[2] = loadImage('assets/kakamora-characters/Kokomoro-TikiRob-Mickey-Mouse-Edition-1-250x250.png'); kakomoraImgs[3] = loadImage('assets/kakamora-characters/Kokomoro-TikiRob-Seashell-Edition.png'); kakomoraImgs[4] = loadImage('assets/kakamora-characters/Kokomoro-TikiRob-Starfish-Edition-250x250.png'); treeImgs.full = [ loadImage('assets/tree/tree_full_1.png'), loadImage('assets/tree/tree_full_2.png'), ]; treeImgs.half = [ loadImage('assets/tree/tree_half_1.png'), loadImage('assets/tree/tree_half_2.png'), ]; treeImgs.one = [loadImage('assets/tree/tree_one_1.png')]; treeImgs.empty = [ loadImage('assets/tree/tree_empty_1.png'), loadImage('assets/tree/tree_empty_2.png'), loadImage('assets/tree/tree_empty_3.png'), ]; idleAni = loadAni('assets/kakamora-characters/co_ani_1.png', 5); } // ====== SETUP ====== function setup() { createCanvas(windowWidth, windowHeight); resetCoconutHome(); world.gravity.y = 0; idleSprite = new Sprite(); idleSprite.addAni('idle', idleAni); idleSprite.collider = 'none'; idleSprite.visible = false; // p5play auto-draw 비활성 — 수동으로 그림 idleSprite.ani.frameDelay = 8; } function windowResized() { resizeCanvas(windowWidth, windowHeight); resetCoconutHome(); } function resetCoconutHome() { coconut.r = min(windowWidth, windowHeight) * 0.13; coconut.x = width / 2; coconut.y = height * 0.42; } // ====== DRAW ====== function draw() { background(lerpColor(color(30, 50, 80), color(25, 90, 200), bgBlue / 255)); if (bgBlue > 0) bgBlue = max(0, bgBlue - 4); drawBackground(); drawTrees(); updateShake(); updateFlyAway(); drawCoconut(); drawTrail(); drawParticles(); drawFlash(); drawScore(); drawKakamoraSlots(); updateFlyingKakamoraList(); drawFlyingKakamoraList(); } // ====== BACKGROUND ====== function drawBackground() { const horizonY = height * 0.60; const sandY = height * 0.70; waveTime += 0.016; noStroke(); // Ocean base fill(18, 90, 165); rect(0, horizonY, width, sandY - horizonY); // Ocean depth gradient (slightly lighter toward shore) fill(40, 120, 190, 80); rect(0, (horizonY + sandY) / 2, width, (sandY - horizonY) / 2); // 3 animated wave layers (far → near) const waveLayers = [ { t: 0.18, amp: 3, freq: 0.007, speed: 0.7, alpha: 60, wh: 3 }, { t: 0.52, amp: 6, freq: 0.009, speed: 1.1, alpha: 100, wh: 5 }, { t: 0.82, amp: 9, freq: 0.011, speed: 1.6, alpha: 145, wh: 8 }, ]; for (let lyr of waveLayers) { let wy = horizonY + (sandY - horizonY) * lyr.t; fill(255, 255, 255, lyr.alpha); beginShape(); vertex(0, wy + lyr.wh); for (let x = 0; x <= width; x += 8) { vertex(x, wy + sin(x * lyr.freq + waveTime * lyr.speed) * lyr.amp); } vertex(width, wy + lyr.wh); endShape(CLOSE); } // Shore foam fill(255, 255, 255, 90); beginShape(); vertex(0, sandY + 6); for (let x = 0; x <= width; x += 6) { vertex(x, sandY + sin(x * 0.013 + waveTime * 2.2) * 4); } vertex(width, sandY + 6); endShape(CLOSE); // Sand fill(210, 185, 135); rect(0, sandY, width, height - sandY); // Wet sand strip at shore fill(185, 158, 105, 200); rect(0, sandY, width, 14); // Sand highlight fill(235, 215, 165, 50); rect(0, sandY + 10, width, 35); } // ====== TREES ====== function drawTrees() { imageMode(CENTER); for (let i = 0; i < TREE_COUNT; i++) { let stage = constrain(treeHits - i * 3, 0, 3); let img; if (stage === 0) img = treeImgs.full[i % treeImgs.full.length]; else if (stage === 1) img = treeImgs.half[i % treeImgs.half.length]; else if (stage === 2) img = treeImgs.one[0]; else img = treeImgs.empty[i % treeImgs.empty.length]; let x = ((i + 0.5) / TREE_COUNT) * width; let h = height * (0.30 + (i % 3) * 0.02); let w = h * (img.width / img.height); let groundY = height * 0.82; let cy = groundY - h / 2; image(img, x, cy, w, h); } } // ====== 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 if (coconut.state === 0) { // idle: p5play 애니메이션 현재 프레임을 수동 렌더링 let frame = idleSprite && idleSprite.ani ? idleSprite.ani.src : null; let img = frame || coconutImgs[0]; image(img, coconut.x + coconut.shakeX, coconut.y, coconut.r * 2, coconut.r * 2); } else { image(coconutImgs[coconut.state], coconut.x + coconut.shakeX, coconut.y, coconut.r * 2, coconut.r * 2); } } function updateShake() { if (coconut.shakeTimer > 0) { coconut.shakeX = sin(coconut.shakeTimer * 2.2) * coconut.shakeTimer * 0.55; coconut.shakeTimer--; } else { coconut.shakeX = 0; } } function updateFlyAway() { if (!coconut.flying) return; coconut.x += coconut.flyVX; coconut.y += coconut.flyVY; coconut.flyVY += 0.9; coconut.flyRotation += coconut.flyRotSpeed; coconut.flyAlpha -= 14; if (coconut.flyAlpha <= 0 || coconut.y > height + 100) { if (score < 10) launchKakamoraToSlot(score); score++; treeHits++; coconut.x = width / 2; coconut.y = height * 0.42; coconut.state = 0; coconut.flying = false; coconut.flyAlpha = 255; coconut.flyRotation = 0; coconut.shakeX = 0; coconut.shakeTimer = 0; } } // ====== TRAIL INPUT ====== function mouseDragged() { 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, dx, dy); coconut.shakeTimer = max(coconut.shakeTimer, 10); } if (trail.length > 15) trail.shift(); if (velocities.length > 15) velocities.shift(); } function mouseReleased() { // 드래그가 코코넛을 지나쳤는지 확인 let hits = trail.filter(p => dist(p.x, p.y, coconut.x, coconut.y) < coconut.r).length; if (hits >= 2 && !coconut.flying) { coconut.state++; flashAlpha = 25; if (coconut.state >= 4) { // 4번째 드래그 → 날아가기 시작 coconut.flying = true; coconut.flyVX = random([-1, 1]) * random(10, 16); coconut.flyVY = random(-22, -16); coconut.flyRotSpeed = random(-0.28, 0.28); bgBlue = 255; } } trail = []; velocities = []; } // ====== TRAIL DRAW ====== function drawTrail() { noFill(); for (let i = 1; i < trail.length; i++) { let speed = velocities[i] || 0; strokeWeight(map(speed, 0, 50, 1, 8)); stroke(255, 200); line(trail[i - 1].x, trail[i - 1].y, trail[i].x, trail[i].y); } } // ====== SCORE ====== function drawScore() { noStroke(); textSize(22); textAlign(LEFT, TOP); fill(255, 255, 255, 220); text(`coconut x${score}`, 20, 20); } // ====== SLOT POSITION HELPER ====== function getSlotPos(i) { const gap = 8; const size = floor((width - 40 - gap * 9) / 10); const startX = 20 + size / 2; return { x: startX + i * (size + gap), y: height - size / 2 - 16, size }; } // ====== KAKAMORA SLOTS ====== function drawKakamoraSlots() { for (let i = 0; i < 10; i++) { let { x, y, size } = getSlotPos(i); let filled = i < score; if (filled) { drawingContext.save(); drawingContext.beginPath(); drawingContext.arc(x, y, size / 2, 0, Math.PI * 2); drawingContext.clip(); imageMode(CORNER); image(kakomoraImgs[i % 5], x - size / 2, y - size / 2, size, size); drawingContext.restore(); noFill(); stroke(255); strokeWeight(2.5); ellipse(x, y, size, size); } else { fill(50, 50, 50, 190); stroke(110, 110, 110); strokeWeight(1.5); ellipse(x, y, size, size); noStroke(); fill(170, 170, 170); textSize(size * 0.28); textAlign(CENTER, CENTER); text('?', x, y); } } } // ====== FLYING KAKAMORA ====== function launchKakamoraToSlot(slotIdx) { let { x: tx, y: ty, size } = getSlotPos(slotIdx); flyingKakamoraList.push({ startX: width / 2, startY: height * 0.42, targetX: tx, targetY: ty, t: 0, imgIdx: slotIdx % 5, 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); drawingContext.save(); drawingContext.beginPath(); drawingContext.arc(x, y, sz / 2, 0, Math.PI * 2); drawingContext.clip(); imageMode(CORNER); image(kakomoraImgs[k.imgIdx], x - sz / 2, y - sz / 2, sz, sz); drawingContext.restore(); noFill(); stroke(255, 200); strokeWeight(2); ellipse(x, y, sz, sz); } } // ====== PARTICLES ====== class Particle { constructor(x, y, dx, dy) { this.pos = createVector(x, y); // 드래그 반대 방향 + 랜덤 퍼짐 let baseAngle = (dx !== 0 || dy !== 0) ? atan2(dy, dx) + PI + random(-0.7, 0.7) : random(TWO_PI); let spd = random(1.5, 5); this.vel = createVector(cos(baseAngle) * spd, sin(baseAngle) * spd); this.life = 255; this.r = random(3, 7); this.isHusk = random() > 0.4; // 60% 껍질(갈색), 40% 과육(크림) } update() { this.pos.add(this.vel); this.vel.y += 0.18; // 중력 this.vel.mult(0.93); // 마찰 this.life -= 7; } 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, dx, dy) { for (let i = 0; i < 5; i++) particles.push(new Particle(x, y, dx, dy)); } 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; } }