added character loaded

This commit is contained in:
Haeri Kim
2026-05-08 21:44:29 +09:00
parent e4ca52d256
commit 906a35093b
61 changed files with 607 additions and 205 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
assets/.DS_Store vendored

Binary file not shown.

BIN
assets/beach.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 MiB

BIN
assets/boat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

BIN
assets/coconut/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 158 KiB

BIN
assets/effect/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
assets/kakamora-characters/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 450 KiB

After

Width:  |  Height:  |  Size: 450 KiB

View File

Before

Width:  |  Height:  |  Size: 396 KiB

After

Width:  |  Height:  |  Size: 396 KiB

View File

Before

Width:  |  Height:  |  Size: 397 KiB

After

Width:  |  Height:  |  Size: 397 KiB

View File

Before

Width:  |  Height:  |  Size: 386 KiB

After

Width:  |  Height:  |  Size: 386 KiB

View File

Before

Width:  |  Height:  |  Size: 395 KiB

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

BIN
assets/knife.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

BIN
assets/sound/slice.wav Normal file

Binary file not shown.

View File

@@ -4,14 +4,18 @@
<meta charset="UTF-8" />
<title>Kakamora Game</title>
<link rel="stylesheet" href="style.css" />
<link href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;700&display=swap" rel="stylesheet">
<style>
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@700&display=swap');
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/howler@2.2.4/dist/howler.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.4/lib/p5.js"></script>
<script src="https://cdn.jsdelivr.net/npm/planck/dist/planck.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5play@3/p5play.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="src/Player.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="sketch.js"></script>
</body>
</html>

800
sketch.js
View File

@@ -5,57 +5,77 @@ 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 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,
shakeX: 0,
slices: 0,
shakeX: 0, shakeY: 0,
shakeTimer: 0,
baseAngle: 0,
flying: false,
flyVX: 0, flyVY: 0,
flyAlpha: 255,
flyRotation: 0, flyRotSpeed: 0
flyRotation: 0, flyRotSpeed: 0,
entering: false,
enterT: 0,
enterFromX: 0, enterFromY: 0
};
let coconutImgs = [];
let kakomoraImgs = [];
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`);
}
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'),
];
// 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`));
idleAni = loadAni('assets/kakamora-characters/co_ani_1.png', 5);
}
// ====== SETUP ======
@@ -64,18 +84,81 @@ function setup() {
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;
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;
@@ -84,100 +167,41 @@ function resetCoconutHome() {
// ====== DRAW ======
function draw() {
background(lerpColor(color(30, 50, 80), color(25, 90, 200), bgBlue / 255));
if (bgBlue > 0) bgBlue = max(0, bgBlue - 4);
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);
}
drawBackground();
drawTrees();
updateShake();
updateFlyAway();
drawCoconut();
drawTrail();
if (gameState === 'name') { drawNameEntry(); return; }
if (gameState === 'playing') {
checkCoconutTimer();
updateShake();
updateEnter();
updateFlyAway();
drawCoconut();
drawCoconutTimer();
drawTrail();
updateImgParticles();
drawImgParticles();
}
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);
if (gameState !== 'intro' && gameState !== 'name') {
drawKakamoraSlots();
updateFlyingKakamoraList();
drawFlyingKakamoraList();
}
// 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);
if (gameState === 'intro') drawIntroQuest();
if (gameState === 'departure') drawDepartureQuest();
}
// ====== 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() {
@@ -190,22 +214,50 @@ function drawCoconut() {
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);
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.2) * coconut.shakeTimer * 0.55;
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;
}
}
@@ -214,27 +266,34 @@ function updateFlyAway() {
coconut.x += coconut.flyVX;
coconut.y += coconut.flyVY;
coconut.flyVY += 0.9;
coconut.flyVY += 0.55;
coconut.flyRotation += coconut.flyRotSpeed;
coconut.flyAlpha -= 14;
coconut.flyAlpha -= 7;
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.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);
@@ -248,101 +307,206 @@ function mouseDragged() {
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);
spawnParticles(mouseX, mouseY);
spawnImgParticles(mouseX, mouseY, dx, dy);
coconut.shakeTimer = max(coconut.shakeTimer, 18);
}
if (trail.length > 15) trail.shift();
if (velocities.length > 15) velocities.shift();
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) {
coconut.state++;
sliceSound.play();
coconut.slices++;
coconut.state = floor(coconut.slices / 2);
flashAlpha = 25;
if (coconut.state >= 4) {
// 4번째 드래그 → 날아가기 시작
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(10, 16);
coconut.flyVY = random(-22, -16);
coconut.flyRotSpeed = random(-0.28, 0.28);
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 drawTrail() {
function drawSlashSegments(pts, vels, masterAlpha) {
if (pts.length < 2) return;
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);
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 = []; }
}
}
// ====== SCORE ======
function drawScore() {
noStroke();
textSize(22);
textAlign(LEFT, TOP);
fill(255, 255, 255, 220);
text(`coconut x${score}`, 20, 20);
// ====== 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);
}
// ====== SLOT POSITION HELPER ======
function getSlotPos(i) {
const gap = 8;
const size = floor((width - 40 - gap * 9) / 10);
const startX = 20 + size / 2;
let sz = getSlotSize();
return {
x: startX + i * (size + gap),
y: height - size / 2 - 16,
size
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() {
for (let i = 0; i < 10; i++) {
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) {
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(CORNER);
image(kakomoraImgs[i % 5], x - size / 2, y - size / 2, size, size);
imageMode(CENTER);
image(img, x, y, img.width * sc, img.height * sc);
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);
// 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();
fill(170, 170, 170);
textSize(size * 0.28);
// 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);
text('?', x, y);
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);
}
}
}
@@ -350,13 +514,18 @@ function drawKakamoraSlots() {
// ====== 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,
imgIdx: slotIdx % 5,
charIdx,
questShown: false,
size
});
}
@@ -373,41 +542,38 @@ function drawFlyingKakamoraList() {
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(CORNER);
image(kakomoraImgs[k.imgIdx], x - sz / 2, y - sz / 2, sz, sz);
imageMode(CENTER);
image(img, x, y, img.width * sc, img.height * sc);
drawingContext.restore();
noFill();
stroke(255, 200);
strokeWeight(2);
ellipse(x, y, sz, sz);
}
}
// ====== PARTICLES ======
class Particle {
constructor(x, y, dx, dy) {
constructor(x, y) {
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);
// 전방향 랜덤 퍼짐 (드래그 방향 편향 없이 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(3, 7);
this.isHusk = random() > 0.4; // 60% 껍질(갈색), 40% 과육(크림)
this.r = random(1.5, 3.5);
this.isHusk = random() > 0.4;
}
update() {
this.pos.add(this.vel);
this.vel.y += 0.18; // 중력
this.vel.mult(0.93); // 마찰
this.life -= 7;
this.vel.y += 0.22;
this.vel.mult(0.91);
this.life -= 9;
}
draw() {
@@ -418,8 +584,8 @@ class Particle {
}
}
function spawnParticles(x, y, dx, dy) {
for (let i = 0; i < 5; i++) particles.push(new Particle(x, y, dx, dy));
function spawnParticles(x, y) {
for (let i = 0; i < 12; i++) particles.push(new Particle(x, y));
}
function drawParticles() {
@@ -438,3 +604,235 @@ function drawFlash() {
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;
}
}