diff --git a/assets/coconut/coconut_0.png b/assets/coconut/coconut_0.png new file mode 100644 index 0000000..6498470 Binary files /dev/null and b/assets/coconut/coconut_0.png differ diff --git a/assets/coconut/coconut_1.png b/assets/coconut/coconut_1.png new file mode 100644 index 0000000..8737795 Binary files /dev/null and b/assets/coconut/coconut_1.png differ diff --git a/assets/coconut/coconut_2.png b/assets/coconut/coconut_2.png new file mode 100644 index 0000000..c545bd2 Binary files /dev/null and b/assets/coconut/coconut_2.png differ diff --git a/assets/coconut/coconut_3.png b/assets/coconut/coconut_3.png new file mode 100644 index 0000000..0d671a4 Binary files /dev/null and b/assets/coconut/coconut_3.png differ diff --git a/assets/coconut/coconut_4.png b/assets/coconut/coconut_4.png new file mode 100644 index 0000000..829325f Binary files /dev/null and b/assets/coconut/coconut_4.png differ diff --git a/assets/fonts/Moanas.ttf b/assets/fonts/Moanas.ttf new file mode 100644 index 0000000..77ab143 Binary files /dev/null and b/assets/fonts/Moanas.ttf differ diff --git a/assets/kakamora characters/Kakamora-TikiRob-Claw-Edition.png b/assets/kakamora characters/Kakamora-TikiRob-Claw-Edition.png new file mode 100644 index 0000000..eb0d385 Binary files /dev/null and b/assets/kakamora characters/Kakamora-TikiRob-Claw-Edition.png differ diff --git a/assets/kakamora characters/Kokomoro-TikiRob-Fishhead-Edition.webp b/assets/kakamora characters/Kokomoro-TikiRob-Fishhead-Edition.webp new file mode 100644 index 0000000..5233c75 Binary files /dev/null and b/assets/kakamora characters/Kokomoro-TikiRob-Fishhead-Edition.webp differ diff --git a/assets/kakamora characters/Kokomoro-TikiRob-Mickey-Mouse-Edition-1-250x250.png b/assets/kakamora characters/Kokomoro-TikiRob-Mickey-Mouse-Edition-1-250x250.png new file mode 100644 index 0000000..f5b09f3 Binary files /dev/null and b/assets/kakamora characters/Kokomoro-TikiRob-Mickey-Mouse-Edition-1-250x250.png differ diff --git a/assets/kakamora characters/Kokomoro-TikiRob-Seashell-Edition.png b/assets/kakamora characters/Kokomoro-TikiRob-Seashell-Edition.png new file mode 100644 index 0000000..8f7efe9 Binary files /dev/null and b/assets/kakamora characters/Kokomoro-TikiRob-Seashell-Edition.png differ diff --git a/assets/kakamora characters/Kokomoro-TikiRob-Starfish-Edition-250x250.png b/assets/kakamora characters/Kokomoro-TikiRob-Starfish-Edition-250x250.png new file mode 100644 index 0000000..bff8b56 Binary files /dev/null and b/assets/kakamora characters/Kokomoro-TikiRob-Starfish-Edition-250x250.png differ diff --git a/index.html b/index.html index f8e0d7c..7433eff 100644 --- a/index.html +++ b/index.html @@ -10,6 +10,7 @@ + \ No newline at end of file diff --git a/sketch.js b/sketch.js index 39e9d7a..dd3a33d 100644 --- a/sketch.js +++ b/sketch.js @@ -1,161 +1,324 @@ -// ====== [GLOBAL VARIABLES] ====== +// ====== GLOBALS ====== let trail = []; let velocities = []; - let particles = []; let flashAlpha = 0; -let lastSliceTime = 0; +let score = 0; +let bgBlue = 0; // 0=dark, 255=blue, fades out let coconut = { - x: 400, - y: 300, - r: 50 + x: 0, y: 0, // setup()에서 초기화 + r: 0, + state: 0, + shakeX: 0, + shakeTimer: 0, + // fly-away + flying: false, + flyVX: 0, flyVY: 0, + flyAlpha: 255, + flyRotation: 0, flyRotSpeed: 0 }; -// ====== [SETUP] ====== +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'); +} + +// ====== SETUP ====== function setup() { - createCanvas(800, 600); + createCanvas(windowWidth, windowHeight); + resetCoconutHome(); } -// ====== [DRAW LOOP] ====== +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(30); + background(lerpColor(color(30), color(25, 90, 200), bgBlue / 255)); + if (bgBlue > 0) bgBlue = max(0, bgBlue - 4); - drawTrail(); - drawCoconut(); - checkSlice(); + updateShake(); + updateFlyAway(); + drawCoconut(); + drawTrail(); - // ====== PARTICLES ====== - 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 EFFECT ====== - if (flashAlpha > 0) { - fill(255, flashAlpha); - rect(0, 0, width, height); - flashAlpha -= 20; - } + drawParticles(); + drawFlash(); + drawScore(); + drawKakamoraSlots(); + updateFlyingKakamoraList(); + drawFlyingKakamoraList(); } -// ====== [INPUT: KNIFE TRACKING] ====== -function mouseDragged() { - let pos = createVector(mouseX, mouseY); - trail.push(pos); - - if (trail.length > 1) { - let v = p5.Vector.sub( - trail[trail.length - 1], - trail[trail.length - 2] - ); - velocities.push(v.mag()); - } - - if (trail.length > 20) trail.shift(); - if (velocities.length > 20) velocities.shift(); -} - -// ====== [DRAW TRAIL] ====== -function drawTrail() { - noFill(); - - for (let i = 1; i < trail.length; i++) { - let speed = velocities[i] || 0; - let thickness = map(speed, 0, 50, 1, 10); - - strokeWeight(thickness); - stroke(255, 200); - - line( - trail[i - 1].x, - trail[i - 1].y, - trail[i].x, - trail[i].y - ); - } -} - -// ====== [DRAW COCONUT] ====== +// ====== COCONUT ====== function drawCoconut() { - fill(100, 255, 100); - noStroke(); - ellipse(coconut.x, coconut.y, coconut.r * 2); + 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 { + image(coconutImgs[coconut.state], coconut.x + coconut.shakeX, coconut.y, coconut.r * 2, coconut.r * 2); + } } -// ====== [SLICE CHECK] ====== -function checkSlice() { - for (let i = 1; i < trail.length; i++) { - let hit = lineCircle( - trail[i - 1], - trail[i], - coconut.x, - coconut.y, - coconut.r - ); +function updateShake() { + if (coconut.shakeTimer > 0) { + coconut.shakeX = sin(coconut.shakeTimer * 2.2) * coconut.shakeTimer * 0.55; + coconut.shakeTimer--; + } else { + coconut.shakeX = 0; + } +} - if (hit && millis() - lastSliceTime > 100) { - lastSliceTime = millis(); +function updateFlyAway() { + if (!coconut.flying) return; - let speed = velocities[i] || 10; + coconut.x += coconut.flyVX; + coconut.y += coconut.flyVY; + coconut.flyVY += 0.9; + coconut.flyRotation += coconut.flyRotSpeed; + coconut.flyAlpha -= 14; - console.log("SLICE!"); + if (coconut.flyAlpha <= 0 || coconut.y > height + 100) { + if (score < 10) launchKakamoraToSlot(score); + score++; + 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; + } +} - spawnParticles(coconut.x, coconut.y, speed); - flashAlpha = map(speed, 0, 50, 50, 150); - } +// ====== 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 = []; } -// ====== [COLLISION: LINE vs CIRCLE] ====== -function lineCircle(p1, p2, cx, cy, r) { - let ac = createVector(cx - p1.x, cy - p1.y); - let ab = p5.Vector.sub(p2, p1); - - let ab2 = ab.magSq(); - let acab = ac.dot(ab); - - let t = constrain(acab / ab2, 0, 1); - - let h = createVector( - p1.x + ab.x * t, - p1.y + ab.y * t - ); - - let distSq = (h.x - cx) ** 2 + (h.y - cy) ** 2; - - return distSq <= r * r; +// ====== 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); + } } -// ====== [PARTICLE CLASS] ====== + +// ====== 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 size = constrain(floor(width * 0.055), 40, 62); + const gap = 6; + return { + x: width - 20 - size / 2 - (9 - i) * (size + gap), + y: 20 + size / 2, + 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, speed) { - this.pos = createVector(x, y); - this.vel = p5.Vector.random2D().mult(random(1, speed * 0.3)); - this.life = 255; - } + 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.life -= 5; - } + update() { + this.pos.add(this.vel); + this.vel.y += 0.18; // 중력 + this.vel.mult(0.93); // 마찰 + this.life -= 7; + } - draw() { - noStroke(); - fill(200, 255, 200, this.life); - ellipse(this.pos.x, this.pos.y, 5); - } + 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); + } } -// ====== [SPAWN PARTICLES] ====== -function spawnParticles(x, y, speed) { - let count = map(speed, 0, 50, 5, 25); +function spawnParticles(x, y, dx, dy) { + for (let i = 0; i < 5; i++) particles.push(new Particle(x, y, dx, dy)); +} - for (let i = 0; i < count; i++) { - particles.push(new Particle(x, y, speed)); - } -} \ No newline at end of file +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; + } +} diff --git a/src/Player.js b/src/Player.js index e69de29..b1c4f94 100644 --- a/src/Player.js +++ b/src/Player.js @@ -0,0 +1,111 @@ +class Player { + constructor() { + this.x = 130; + this.groundY = GROUND_Y; + this.speed = 3.5; + this.dir = 1; // 1=right, -1=left + this.moving = false; + this.stepT = 0; + + // --- sprite animation state --- + this.anim = "idle"; + this.prevAnim = "idle"; + this.frameIdx = 0; + this.frameTimer = 0; + + // frames per p5 tick each animation holds a frame + this.frameLengths = { idle: 40, walk: 8 }; + this.totalFrames = { idle: 2, walk: 4 }; + + this.sprites = null; // set via setSprites() when images are ready + } + + // Call this once real PNGs are ready: + // player.setSprites({ idle: [img0, img1], walk: [img0, img1, img2, img3] }) + setSprites(map) { + this.sprites = map; + } + + update() { + this.moving = false; + if (keys[LEFT_ARROW]) { this.x -= this.speed; this.dir = -1; this.moving = true; } + if (keys[RIGHT_ARROW]) { this.x += this.speed; this.dir = 1; this.moving = true; } + this.x = constrain(this.x, 50, width - 50); + + this.anim = this.moving ? "walk" : "idle"; + + // reset frame when animation changes + if (this.anim !== this.prevAnim) { + this.frameIdx = 0; + this.frameTimer = 0; + this.prevAnim = this.anim; + } + + // advance frame + this.frameTimer++; + if (this.frameTimer >= this.frameLengths[this.anim]) { + this.frameTimer = 0; + this.frameIdx = (this.frameIdx + 1) % this.totalFrames[this.anim]; + } + + if (this.moving) this.stepT += 0.22; + } + + draw() { + let bobY = this.moving + ? sin(this.stepT) * 3 + : sin(frameCount * 0.05) * 1.5; + + push(); + translate(this.x, this.groundY + bobY); + scale(this.dir, 1); + + if (this.sprites) { + this._drawSprite(); + } else { + this._drawDummy(); + } + + pop(); + } + + _drawSprite() { + let img = this.sprites[this.anim][this.frameIdx]; + imageMode(CENTER); + // adjust (0, -60, 80, 120) to match actual sprite dimensions + image(img, 0, -60, 80, 120); + } + + _drawDummy() { + rectMode(CENTER); + noStroke(); + + // shadow + fill(0, 0, 0, 25); + ellipse(0, 6, 42, 11); + + // body placeholder — green=walk, blue=idle + fill(this.anim === "walk" ? color(80, 200, 120) : color(90, 150, 255)); + rect(0, -38, 40, 64, 12); + + // head + fill(255, 220, 170); + ellipse(0, -80, 48, 48); + + // eyes + fill(40, 24, 14); + ellipse(-11, -82, 7, 8); + ellipse(11, -82, 7, 8); + + // frame number — confirms animation is cycling + fill(255); + textAlign(CENTER, CENTER); + textSize(20); + text(this.frameIdx, 0, -38); + + // anim label + textSize(9); + fill(255, 255, 255, 180); + text(this.anim, 0, -20); + } +} diff --git a/style.css b/style.css index e69de29..82c7eda 100644 --- a/style.css +++ b/style.css @@ -0,0 +1,3 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } +body { overflow: hidden; background: #000; } +canvas { display: block; }