Files
kakamora-game/sketch.js
2026-05-08 21:44:29 +09:00

839 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = []; // 0100, 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`));
// char27: 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;
}
}