diff --git a/.DS_Store b/.DS_Store
index a85f9e8..054b754 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/assets/.DS_Store b/assets/.DS_Store
index 3dd8d70..7db9757 100644
Binary files a/assets/.DS_Store and b/assets/.DS_Store differ
diff --git a/assets/beach2.png b/assets/beach2.png
new file mode 100644
index 0000000..29318b1
Binary files /dev/null and b/assets/beach2.png differ
diff --git a/assets/boat.png b/assets/boat.png
index 6cf570f..c52da0d 100644
Binary files a/assets/boat.png and b/assets/boat.png differ
diff --git a/assets/image.png b/assets/image.png
new file mode 100644
index 0000000..1c5e24e
Binary files /dev/null and b/assets/image.png differ
diff --git a/assets/kakamora-characters/.DS_Store b/assets/kakamora-characters/.DS_Store
index 5ca1bea..550672c 100644
Binary files a/assets/kakamora-characters/.DS_Store and b/assets/kakamora-characters/.DS_Store differ
diff --git a/assets/kakamora-characters/char1-2/Frame 45.png b/assets/kakamora-characters/char1-2/Frame 45.png
new file mode 100644
index 0000000..c391d59
Binary files /dev/null and b/assets/kakamora-characters/char1-2/Frame 45.png differ
diff --git a/assets/kakamora-characters/char1-2/Frame 61.png b/assets/kakamora-characters/char1-2/Frame 61.png
new file mode 100644
index 0000000..c112d66
Binary files /dev/null and b/assets/kakamora-characters/char1-2/Frame 61.png differ
diff --git a/assets/kakamora-characters/char1-2/Frame 62.png b/assets/kakamora-characters/char1-2/Frame 62.png
new file mode 100644
index 0000000..84afe84
Binary files /dev/null and b/assets/kakamora-characters/char1-2/Frame 62.png differ
diff --git a/assets/kakamora-characters/char1-2/Frame 63.png b/assets/kakamora-characters/char1-2/Frame 63.png
new file mode 100644
index 0000000..c3d57d1
Binary files /dev/null and b/assets/kakamora-characters/char1-2/Frame 63.png differ
diff --git a/assets/kakamora-characters/char1-2/Frame 64.png b/assets/kakamora-characters/char1-2/Frame 64.png
new file mode 100644
index 0000000..87a8ab1
Binary files /dev/null and b/assets/kakamora-characters/char1-2/Frame 64.png differ
diff --git a/assets/kakamora-characters/char2-2/Frame 46.png b/assets/kakamora-characters/char2-2/Frame 46.png
new file mode 100644
index 0000000..acaa930
Binary files /dev/null and b/assets/kakamora-characters/char2-2/Frame 46.png differ
diff --git a/assets/kakamora-characters/char2-2/Frame 47.png b/assets/kakamora-characters/char2-2/Frame 47.png
new file mode 100644
index 0000000..03dae9e
Binary files /dev/null and b/assets/kakamora-characters/char2-2/Frame 47.png differ
diff --git a/assets/kakamora-characters/char2-2/Frame 48.png b/assets/kakamora-characters/char2-2/Frame 48.png
new file mode 100644
index 0000000..429b953
Binary files /dev/null and b/assets/kakamora-characters/char2-2/Frame 48.png differ
diff --git a/assets/kakamora-characters/char2-2/Frame 49.png b/assets/kakamora-characters/char2-2/Frame 49.png
new file mode 100644
index 0000000..e2708bb
Binary files /dev/null and b/assets/kakamora-characters/char2-2/Frame 49.png differ
diff --git a/assets/kakamora-characters/char2-2/Frame 50.png b/assets/kakamora-characters/char2-2/Frame 50.png
new file mode 100644
index 0000000..29351fc
Binary files /dev/null and b/assets/kakamora-characters/char2-2/Frame 50.png differ
diff --git a/assets/kakamora-characters/char3-2/Frame 51.png b/assets/kakamora-characters/char3-2/Frame 51.png
new file mode 100644
index 0000000..304fad8
Binary files /dev/null and b/assets/kakamora-characters/char3-2/Frame 51.png differ
diff --git a/assets/kakamora-characters/char3-2/Frame 52.png b/assets/kakamora-characters/char3-2/Frame 52.png
new file mode 100644
index 0000000..a3f3260
Binary files /dev/null and b/assets/kakamora-characters/char3-2/Frame 52.png differ
diff --git a/assets/kakamora-characters/char3-2/Frame 53.png b/assets/kakamora-characters/char3-2/Frame 53.png
new file mode 100644
index 0000000..19615e8
Binary files /dev/null and b/assets/kakamora-characters/char3-2/Frame 53.png differ
diff --git a/assets/kakamora-characters/char3-2/Frame 54.png b/assets/kakamora-characters/char3-2/Frame 54.png
new file mode 100644
index 0000000..d303995
Binary files /dev/null and b/assets/kakamora-characters/char3-2/Frame 54.png differ
diff --git a/assets/kakamora-characters/char3-2/Frame 55.png b/assets/kakamora-characters/char3-2/Frame 55.png
new file mode 100644
index 0000000..c28c55c
Binary files /dev/null and b/assets/kakamora-characters/char3-2/Frame 55.png differ
diff --git a/assets/kakamora-characters/char4-2/Frame 56.png b/assets/kakamora-characters/char4-2/Frame 56.png
new file mode 100644
index 0000000..aa8e549
Binary files /dev/null and b/assets/kakamora-characters/char4-2/Frame 56.png differ
diff --git a/assets/kakamora-characters/char4-2/Frame 57.png b/assets/kakamora-characters/char4-2/Frame 57.png
new file mode 100644
index 0000000..b22f083
Binary files /dev/null and b/assets/kakamora-characters/char4-2/Frame 57.png differ
diff --git a/assets/kakamora-characters/char4-2/Frame 58.png b/assets/kakamora-characters/char4-2/Frame 58.png
new file mode 100644
index 0000000..3b37d0e
Binary files /dev/null and b/assets/kakamora-characters/char4-2/Frame 58.png differ
diff --git a/assets/kakamora-characters/char4-2/Frame 59.png b/assets/kakamora-characters/char4-2/Frame 59.png
new file mode 100644
index 0000000..b005f02
Binary files /dev/null and b/assets/kakamora-characters/char4-2/Frame 59.png differ
diff --git a/assets/kakamora-characters/char4-2/Frame 60.png b/assets/kakamora-characters/char4-2/Frame 60.png
new file mode 100644
index 0000000..0a70fcc
Binary files /dev/null and b/assets/kakamora-characters/char4-2/Frame 60.png differ
diff --git a/assets/kakamora-characters/char5-2/Frame 65.png b/assets/kakamora-characters/char5-2/Frame 65.png
new file mode 100644
index 0000000..34d1b3b
Binary files /dev/null and b/assets/kakamora-characters/char5-2/Frame 65.png differ
diff --git a/assets/kakamora-characters/char5-2/Frame 66.png b/assets/kakamora-characters/char5-2/Frame 66.png
new file mode 100644
index 0000000..98d1aea
Binary files /dev/null and b/assets/kakamora-characters/char5-2/Frame 66.png differ
diff --git a/assets/kakamora-characters/char5-2/Frame 67.png b/assets/kakamora-characters/char5-2/Frame 67.png
new file mode 100644
index 0000000..c169851
Binary files /dev/null and b/assets/kakamora-characters/char5-2/Frame 67.png differ
diff --git a/assets/kakamora-characters/char6-2/Frame 68.png b/assets/kakamora-characters/char6-2/Frame 68.png
new file mode 100644
index 0000000..23b554c
Binary files /dev/null and b/assets/kakamora-characters/char6-2/Frame 68.png differ
diff --git a/assets/kakamora-characters/char6-2/Frame 69.png b/assets/kakamora-characters/char6-2/Frame 69.png
new file mode 100644
index 0000000..61d239e
Binary files /dev/null and b/assets/kakamora-characters/char6-2/Frame 69.png differ
diff --git a/assets/kakamora-characters/char6-2/Frame 70.png b/assets/kakamora-characters/char6-2/Frame 70.png
new file mode 100644
index 0000000..c50d715
Binary files /dev/null and b/assets/kakamora-characters/char6-2/Frame 70.png differ
diff --git a/assets/kakamora-characters/char6-2/Frame 71.png b/assets/kakamora-characters/char6-2/Frame 71.png
new file mode 100644
index 0000000..4455baf
Binary files /dev/null and b/assets/kakamora-characters/char6-2/Frame 71.png differ
diff --git a/assets/kakamora-characters/char6-2/Frame 72.png b/assets/kakamora-characters/char6-2/Frame 72.png
new file mode 100644
index 0000000..e4f0787
Binary files /dev/null and b/assets/kakamora-characters/char6-2/Frame 72.png differ
diff --git a/assets/kakamora-characters/char7-2/Frame 73.png b/assets/kakamora-characters/char7-2/Frame 73.png
new file mode 100644
index 0000000..22fdeaa
Binary files /dev/null and b/assets/kakamora-characters/char7-2/Frame 73.png differ
diff --git a/assets/kakamora-characters/char7-2/Frame 74.png b/assets/kakamora-characters/char7-2/Frame 74.png
new file mode 100644
index 0000000..c46fe4b
Binary files /dev/null and b/assets/kakamora-characters/char7-2/Frame 74.png differ
diff --git a/assets/kakamora-characters/char7-2/Frame 75.png b/assets/kakamora-characters/char7-2/Frame 75.png
new file mode 100644
index 0000000..060268a
Binary files /dev/null and b/assets/kakamora-characters/char7-2/Frame 75.png differ
diff --git a/assets/sound/mixkit-bomb-explosion-in-battle-2800.wav b/assets/sound/mixkit-bomb-explosion-in-battle-2800.wav
new file mode 100644
index 0000000..c1bb441
Binary files /dev/null and b/assets/sound/mixkit-bomb-explosion-in-battle-2800.wav differ
diff --git a/assets/sound/mixkit-drums-of-war-call-2780.wav b/assets/sound/mixkit-drums-of-war-call-2780.wav
new file mode 100644
index 0000000..8862ba7
Binary files /dev/null and b/assets/sound/mixkit-drums-of-war-call-2780.wav differ
diff --git a/assets/sound/mixkit-knife-fast-hit-2184.wav b/assets/sound/mixkit-knife-fast-hit-2184.wav
new file mode 100644
index 0000000..7bde57d
Binary files /dev/null and b/assets/sound/mixkit-knife-fast-hit-2184.wav differ
diff --git a/assets/sound/mixkit-ow-exclamation-of-pain-2204.wav b/assets/sound/mixkit-ow-exclamation-of-pain-2204.wav
new file mode 100644
index 0000000..dccfefe
Binary files /dev/null and b/assets/sound/mixkit-ow-exclamation-of-pain-2204.wav differ
diff --git a/assets/sound/mixkit-wooden-ship-on-the-sea-1187.wav b/assets/sound/mixkit-wooden-ship-on-the-sea-1187.wav
new file mode 100644
index 0000000..7bba7f0
Binary files /dev/null and b/assets/sound/mixkit-wooden-ship-on-the-sea-1187.wav differ
diff --git a/assets/sound/strikes.wav b/assets/sound/strikes.wav
new file mode 100644
index 0000000..f9c7e80
Binary files /dev/null and b/assets/sound/strikes.wav differ
diff --git a/index.html b/index.html
index 9399b49..c44d2c8 100644
--- a/index.html
+++ b/index.html
@@ -4,10 +4,7 @@
Kakamora Game
-
-
+
@@ -15,6 +12,7 @@
+
diff --git a/sketch.js b/sketch.js
index 1a39c9c..724c65a 100644
--- a/sketch.js
+++ b/sketch.js
@@ -1,3 +1,5 @@
+const DEV_SAILING = false; // false = normal game flow
+
let trail = [];
let velocities = [];
let particles = [];
@@ -6,14 +8,16 @@ let score = 0;
let bgBlue = 0;
-let beachImg;
+let beachImg, sailBgImg;
let moanaFont;
-let beachSound, sliceSound;
+let beachSound, sliceSound, strikesSound, drumsSound, shipSound, owSound, bombSound;
let charFrames = [];
+let battleFrames = [];
let slotChars = new Array(10).fill(-1);
let slotOffsets = new Array(10).fill(0);
const ANI_FPS = 18;
+const BATTLE_ANI_FPS = 5; // faster for battle sprites
const CHAR_SCALES = [0.9, 1.5, 1.2, 1.5, 1.5, 1.5, 1.5];
@@ -40,6 +44,7 @@ let coconut = {
flyVX: 0, flyVY: 0,
flyAlpha: 255,
flyRotation: 0, flyRotSpeed: 0,
+ bounces: 0, squish: 1,
entering: false,
enterT: 0,
enterFromX: 0, enterFromY: 0
@@ -51,8 +56,26 @@ let knifeImg;
let effectImgs = [];
let imgParticles = [];
let slashLingerAlpha = 0;
+let sailBgOffset = 0;
+let wakeParticles = [];
+let warriors = [];
+let rocks = [];
+let battleHits = [];
+let battleStarted = false;
+let battleOver = false;
+let battleResult = '';
+let rockSpawnCd = 0;
let lingerTrail = [];
let lingerVelocities = [];
+let hitBursts = [];
+let deflectedRocks = [];
+let scorePopups = [];
+let activeWarriorIdx = 0;
+let screenFlash = 0;
+let battleScore = 0;
+let attackKeyPressed = false;
+let pCanvas;
+let proton, fireEmitter;
// ====== PRELOAD ======
function preload() {
@@ -71,7 +94,25 @@ function preload() {
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');
+ // battle animations: char1-2 ~ char7-2
+ const battleFrameNums = [
+ [45, 61, 62, 63, 64],
+ [46, 47, 48, 49, 50],
+ [51, 52, 53, 54, 55],
+ [56, 57, 58, 59, 60],
+ [65, 66, 67],
+ [68, 69, 70, 71, 72],
+ [73, 74, 75],
+ ];
+ for (let c = 0; c < 7; c++) {
+ battleFrames[c] = [];
+ for (let n of battleFrameNums[c]) {
+ battleFrames[c].push(loadImage(`assets/kakamora-characters/char${c + 1}-2/Frame ${n}.png`));
+ }
+ }
+
+ beachImg = loadImage('assets/beach.png');
+ sailBgImg = loadImage('assets/image.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`));
@@ -80,29 +121,50 @@ function preload() {
// ====== SETUP ======
function setup() {
- createCanvas(windowWidth, windowHeight);
+ pCanvas = createCanvas(windowWidth, windowHeight);
resetCoconutHome();
+ window.addEventListener('keydown', function(e) {
+ if (e.code === 'Space' || e.code === 'KeyZ') { attackKeyPressed = true; e.preventDefault(); }
+ });
+
world.gravity.y = 0;
shuffleCharOrder();
+ Howler.autoSuspend = false;
beachSound = new Howl({
- src: ['assets/sound/beach%20sound.wav'],
+ src: ['assets/sound/beach sound.wav'],
loop: true,
- volume: 0.45,
- fade: true
+ volume: 0.45
});
sliceSound = new Howl({
- src: ['assets/sound/slice.wav'],
+ src: ['assets/sound/mixkit-knife-fast-hit-2184.wav'],
volume: 0.75
});
+ strikesSound = new Howl({
+ src: ['assets/sound/strikes.wav'],
+ volume: 0.6
+ });
+ drumsSound = new Howl({
+ src: ['assets/sound/mixkit-drums-of-war-call-2780.wav'],
+ loop: true,
+ volume: 0.55
+ });
+ shipSound = new Howl({
+ src: ['assets/sound/mixkit-wooden-ship-on-the-sea-1187.wav'],
+ loop: true,
+ volume: 0.5
+ });
+ owSound = new Howl({ src: ['assets/sound/mixkit-ow-exclamation-of-pain-2204.wav'], volume: 2.0 });
+ bombSound = new Howl({ src: ['assets/sound/mixkit-bomb-explosion-in-battle-2800.wav'], volume: 0.6 });
+
// HTML name input
nameInput = createInput('');
- nameInput.attribute('placeholder', 'Enter your name...');
+ nameInput.attribute('placeholder', 'Your name, brave one...');
nameInput.attribute('maxlength', '16');
nameInput.style('font-size', '18px');
- nameInput.style('font-family', 'Google Sans, Nunito, sans-serif');
+ nameInput.style('font-family', '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)');
@@ -114,33 +176,31 @@ function setup() {
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();
+
+ if (DEV_SAILING) {
+ nameInput.hide();
+ score = 7;
+ for (let i = 0; i < 7; i++) {
+ slotChars[i] = i;
+ slotOffsets[i] = floor(random(1000));
+ slotPowers[i] = floor(random(40, 100));
+ }
+ battleStarted = false;
+ gameState = 'sailing';
+ }
}
function positionNameUI() {
- let pw = min(windowWidth * 0.5, 440);
- let ph = 240;
+ let pw = min(windowWidth * 0.52, 500);
+ let ph = 300;
let px = (windowWidth - pw) / 2;
let py = (windowHeight - ph) / 2;
- let inputW = pw * 0.65;
+ let inputW = pw * 0.72;
let inputX = px + (pw - inputW) / 2;
- nameInput.position(inputX, py + 104);
+ nameInput.position(inputX, py + 168);
nameInput.style('width', inputW + 'px');
- nameConfirmBtn.position(px + pw / 2 - 80, py + 164);
}
function shuffleCharOrder() {
@@ -167,6 +227,21 @@ function resetCoconutHome() {
// ====== DRAW ======
function draw() {
+ // Dev shortcut — uses p5play kb so it works regardless of other listeners
+ if (kb.presses('s') && gameState !== 'playing') {
+ score = 7;
+ for (let i = 0; i < 7; i++) {
+ slotChars[i] = i;
+ slotOffsets[i] = floor(random(1000));
+ slotPowers[i] = floor(random(40, 100));
+ }
+ battleStarted = false;
+ gameState = 'sailing';
+ if (!shipSound.playing()) shipSound.play();
+ }
+
+ if (gameState === 'sailing') { drawSailingScene(); return; }
+
imageMode(CORNER);
image(beachImg, 0, 0, width, height);
if (bgBlue > 0) {
@@ -182,7 +257,6 @@ function draw() {
checkCoconutTimer();
updateShake();
updateEnter();
- updateFlyAway();
drawCoconut();
drawCoconutTimer();
drawTrail();
@@ -205,48 +279,69 @@ function draw() {
// ====== COCONUT ======
function drawCoconut() {
+ let alpha = coconut.entering
+ ? (coconut.enterT < 1 ? lerp(50, 220, coconut.enterT) : 255)
+ : 255;
+ tint(255, alpha);
+ push();
+ translate(coconut.x + coconut.shakeX, coconut.y + coconut.shakeY);
+ rotate(coconut.baseAngle);
+ scale(coconut.squish, 1 / coconut.squish);
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();
- }
+ 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.enterT += 0.10; // phase 0→1 = fall, 1→2 = bounce up
+
+ let groundY = height * 0.80;
+ let startX = width + coconut.r;
+ let startY = height * 0.22;
+ let midX = width * 0.58; // where it hits ground
+ let endX = width / 2;
+ let endY = height * 0.42;
+
+ coconut.baseAngle += 0.08; // spin while flying in
+
+ if (coconut.enterT < 1) {
+ // Phase 1: arc down from right to ground
+ let t = coconut.enterT;
+ coconut.x = lerp(startX, midX, t);
+ coconut.y = lerp(startY, groundY, t * t); // ease-in (accelerate downward)
+
+ } else if (coconut.enterT < 1.05) {
+ // Squash on impact
+ coconut.squish = 1.7;
+ coconut.x = midX;
+ coconut.y = groundY;
+
+ } else if (coconut.enterT < 2) {
+ // Phase 2: bounce from ground up to center
+ let t = coconut.enterT - 1; // 0 → 1
+ coconut.squish += (1 - coconut.squish) * 0.2; // spring back
+ coconut.x = lerp(midX, endX, t);
+ // Parabola: starts at groundY, arcs up, lands at endY
+ let yLinear = lerp(groundY, endY, t);
+ let yArc = sin(t * PI) * (groundY - endY) * 0.55;
+ coconut.y = yLinear - yArc;
+
+ } else {
coconut.entering = false;
- coconut.x = width / 2;
- coconut.y = height * 0.42;
+ coconut.squish = 1;
+ coconut.x = endX;
+ coconut.y = endY;
}
}
function startCoconutEntrance() {
coconut.entering = true;
- coconut.enterT = 0;
- coconut.enterFromX = width - 100;
- coconut.enterFromY = 100;
- coconut.x = width - 100;
- coconut.y = 100;
+ coconut.enterT = 0;
+ coconut.squish = 1;
+ coconut.x = width + coconut.r;
+ coconut.y = height * 0.22;
coconut.baseAngle = random(TWO_PI);
}
@@ -266,11 +361,27 @@ function updateFlyAway() {
coconut.x += coconut.flyVX;
coconut.y += coconut.flyVY;
- coconut.flyVY += 0.55;
+ coconut.flyVY += 0.6;
coconut.flyRotation += coconut.flyRotSpeed;
- coconut.flyAlpha -= 7;
- if (coconut.flyAlpha <= 0 || coconut.y > height + 100) {
+ // Squish springs back toward 1
+ coconut.squish += (1 - coconut.squish) * 0.22;
+
+ // Ground bounce
+ let groundY = height * 0.82;
+ if (coconut.y >= groundY && coconut.flyVY > 0 && coconut.bounces < 3) {
+ coconut.y = groundY;
+ coconut.flyVY *= -0.48; // bounce up with damping
+ coconut.flyVX *= 0.80; // friction
+ coconut.flyRotSpeed *= 0.55;
+ coconut.squish = 1.65; // squash on impact
+ coconut.bounces++;
+ }
+
+ // Fade only after 2nd bounce
+ if (coconut.bounces >= 2) coconut.flyAlpha -= 11;
+
+ if (coconut.flyAlpha <= 0 || coconut.y > height + 150) {
if (score < 10) launchKakamoraToSlot(score);
score++;
coconut.state = 0;
@@ -281,6 +392,8 @@ function updateFlyAway() {
coconut.shakeX = 0;
coconut.shakeY = 0;
coconut.shakeTimer = 0;
+ coconut.bounces = 0;
+ coconut.squish = 1;
coconutTimerEnd = millis() + COCONUT_TIME_MS;
if (score >= 7 && gameState === 'playing') {
@@ -307,7 +420,7 @@ function mouseDragged() {
velocities.push(0);
}
- if (dist(mouseX, mouseY, coconut.x, coconut.y) < coconut.r && !coconut.flying) {
+ if (dist(mouseX, mouseY, coconut.x, coconut.y) < coconut.r && !coconut.entering) {
spawnParticles(mouseX, mouseY);
spawnImgParticles(mouseX, mouseY, dx, dy);
coconut.shakeTimer = max(coconut.shakeTimer, 18);
@@ -322,7 +435,7 @@ function mouseReleased() {
let hits = trail.filter(p => dist(p.x, p.y, coconut.x, coconut.y) < coconut.r).length;
- if (hits >= 2 && !coconut.flying) {
+ if (hits >= 2 && !coconut.entering) {
sliceSound.play();
coconut.slices++;
coconut.state = floor(coconut.slices / 2);
@@ -330,11 +443,22 @@ function mouseReleased() {
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);
+ flashAlpha = 80;
bgBlue = 255;
+ if (score < 7) launchKakamoraToSlot(score);
+ score++;
+ coconut.state = 0;
+ coconut.slices = 0;
+ coconut.shakeX = 0;
+ coconut.shakeY = 0;
+ coconut.shakeTimer = 0;
+ coconut.squish = 1;
+ coconutTimerEnd = millis() + COCONUT_TIME_MS;
+ if (score >= 7 && gameState === 'playing') {
+ gameState = 'departure';
+ } else {
+ startCoconutEntrance();
+ }
}
}
@@ -435,7 +559,7 @@ function drawKakamoraSlots() {
fill(200, 210, 230);
textSize(12);
text(`${score} / 7`, px + panelW - S_PAD, py + S_HDRH / 2);
- textFont('sans-serif');
+ textFont('Nunito, sans-serif');
for (let i = 0; i < 7; i++) {
let { x, y, size } = getSlotPos(i);
@@ -488,12 +612,12 @@ function drawKakamoraSlots() {
// label: "PWR XX"
fill(255, 255, 255, 245);
textAlign(CENTER, CENTER);
- textFont('Google Sans, Nunito, sans-serif');
+ textFont('Nunito, sans-serif');
textStyle(BOLD);
textSize(12);
text(`PWR ${pwr}`, x, pillY + pillH / 2);
textStyle(NORMAL);
- textFont('sans-serif');
+ textFont('Nunito, sans-serif');
} else {
fill(22, 28, 42);
noStroke();
@@ -625,7 +749,7 @@ function drawCoconutTimer() {
let tx = width - 18;
let ty = 18;
let label = `TIME ${secs}s`;
- textFont('Google Sans, Nunito, sans-serif');
+ textFont('Nunito, sans-serif');
textSize(26);
let tw = textWidth(label) + 36;
let th = 54;
@@ -643,7 +767,7 @@ function drawCoconutTimer() {
fill(c);
textAlign(RIGHT, TOP);
text(label, tx - 10, ty + 10);
- textFont('sans-serif');
+ textFont('Nunito, sans-serif');
}
@@ -671,34 +795,34 @@ function drawDialogBox(dx, dy, dw, dh, speakerName, lines, btnLabel) {
textAlign(LEFT, CENTER);
text(speakerName, dx + 34, dy + 2);
- // Quest lines — system font for readability
- textFont('sans-serif');
+ // Quest lines — centered
+ textFont('Nunito, sans-serif');
textSize(14);
fill(210, 210, 215);
- textAlign(LEFT, TOP);
+ textAlign(CENTER, CENTER);
+ let lineSpacing = 30;
+ let totalH = lines.length * lineSpacing;
+ let startY = dy + 46 + (dh - 46 - 56 - totalH) / 2 + lineSpacing / 2;
for (let i = 0; i < lines.length; i++) {
- text(`• ${lines[i]}`, dx + 28, dy + 46 + i * 34);
+ text(lines[i], dx + dw / 2, startY + i * lineSpacing);
}
- // Button row — Moana font
+ // Button row
let btnW = 180;
- let btnH = 36;
+ let btnH = 40;
let btnX = dx + dw / 2 - btnW / 2;
let btnY = dy + dh - btnH - 16;
- fill(18, 80, 150);
+ let over = mouseX >= btnX && mouseX <= btnX + btnW && mouseY >= btnY && mouseY <= btnY + btnH;
+ fill(over ? color(255, 200, 60) : color(200, 140, 30));
+ stroke(255, 230, 120); strokeWeight(2);
+ rect(btnX, btnY, btnW, btnH, 10);
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);
+ fill(30, 15, 5);
+ textFont('Nunito, sans-serif'); textStyle(BOLD);
textAlign(CENTER, CENTER);
textSize(18);
- text(btnLabel, dx + dw / 2, btnY + btnH / 2);
- textFont('sans-serif');
+ text(btnLabel, dx + dw / 2, btnY + btnH / 2 + 1);
+ textStyle(NORMAL);
}
// ====== INTRO QUEST ======
@@ -708,9 +832,9 @@ function drawIntroQuest() {
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.'
+ 'Crack each coconut to free a warrior. You have 5 seconds.',
+ 'Slash fast — the quicker the strike, the stronger they fight.',
+ 'Claim all 7. The ocean waits for no one.'
], 'BEGIN');
}
@@ -719,11 +843,11 @@ function drawDepartureQuest() {
let dw = min(width * 0.54, 580);
let dh = 190;
let dx = (width - dw) / 2;
- let dy = height - dh - 28;
+ let dy = (height - dh) / 2;
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.'
+ 'Your crew is assembled. The Kakamora won\'t wait.',
+ 'Deflect their rocks. Protect your warriors.',
+ 'Hold the line until the last one standing.'
], 'SET SAIL');
}
@@ -733,31 +857,55 @@ function drawNameEntry() {
noStroke();
rect(0, 0, width, height);
- let pw = min(width * 0.5, 440);
- let ph = 240;
+ let pw = min(width * 0.52, 500);
+ let ph = 300;
let px = (width - pw) / 2;
let py = (height - ph) / 2;
- fill(14, 20, 36, 225);
+ // Panel background
+ fill(14, 20, 36, 230);
noStroke();
- rect(px, py, pw, ph, 2);
+ rect(px, py, pw, ph, 6);
// Header bar
- fill(24, 35, 58);
- rect(px, py, pw, 42, 2, 2, 0, 0);
+ fill(22, 32, 54);
+ rect(px, py, pw, 54, 6, 6, 0, 0);
+
+ // Title — large, gold
textFont(moanaFont);
textAlign(CENTER, CENTER);
- textSize(18);
+ textSize(26);
fill(255, 215, 80);
- text('KAKAMORA WARRIOR', width / 2, py + 21);
- textFont('sans-serif');
+ text('THE OCEAN IS CALLING', width / 2, py + 27);
+ textFont('Nunito, sans-serif');
- // Prompt
- fill(160, 185, 220);
- textAlign(CENTER, CENTER);
+ // Sub-headline — medium, bright white
+ textSize(16);
+ textStyle(BOLD);
+ fill(220, 240, 255);
+ text('A warrior without a name cannot sail.', width / 2, py + 88);
+ textStyle(NORMAL);
+
+ // Flavour lines — smaller, muted
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);
+ fill(120, 155, 200);
+ text('The Kakamora fear those who are named.', width / 2, py + 116);
+ text('Chief Tamatoa demands to know yours.', width / 2, py + 136);
+
+ // Canvas-drawn JOIN THE CREW button
+ let bw = 200, bh = 44;
+ let bx = width / 2 - bw / 2, by = py + ph - bh - 20;
+ let over = mouseX >= bx && mouseX <= bx + bw && mouseY >= by && mouseY <= by + bh;
+ fill(over ? color(255, 200, 60) : color(200, 140, 30));
+ stroke(255, 230, 120); strokeWeight(2);
+ rect(bx, by, bw, bh, 10);
+ noStroke();
+ fill(30, 15, 5);
+ textFont('Nunito, sans-serif'); textStyle(BOLD);
+ textSize(17);
+ textAlign(CENTER, CENTER);
+ text('JOIN THE CREW', width / 2, by + bh / 2 + 1);
+ textStyle(NORMAL);
}
function confirmName() {
@@ -765,7 +913,6 @@ function confirmName() {
if (val.length === 0) return;
playerName = val;
nameInput.hide();
- nameConfirmBtn.hide();
gameState = 'intro';
}
@@ -821,18 +968,848 @@ function drawImgParticles() {
for (let p of imgParticles) p.draw();
}
+// ====== KEYBOARD HANDLER ======
+function keyPressed() {}
+
// ====== MOUSE HANDLER ======
function mousePressed() {
- if (gameState === 'name') return;
+ if (gameState === 'name') {
+ let ph = 300;
+ let py = (height - ph) / 2;
+ let bw = 200, bh = 44;
+ let bx = width / 2 - bw / 2, by = py + ph - bh - 20;
+ if (mouseX >= bx && mouseX <= bx + bw && mouseY >= by && mouseY <= by + bh) confirmName();
+ return;
+ }
if (gameState === 'intro') {
gameState = 'playing';
coconutTimerEnd = millis() + COCONUT_TIME_MS;
startCoconutEntrance();
beachSound.play();
+ drumsSound.play();
return;
}
if (gameState === 'departure') {
- // 출항 처리 (추후 배틀 씬으로 전환)
+ let b = getSailBtnBounds();
+ if (mouseX >= b.x && mouseX <= b.x + b.w && mouseY >= b.y && mouseY <= b.y + b.h) {
+ drumsSound.stop();
+ beachSound.stop();
+ battleStarted = false;
+ gameState = 'sailing';
+ shipSound.play();
+ }
+ return;
+ }
+ if (gameState === 'sailing' && battleOver && battleResult === 'defeat') {
+ let bw = 180, bh = 46;
+ let bx = width/2 - bw/2, by = height/2 + 120;
+ if (mouseX >= bx && mouseX <= bx + bw && mouseY >= by && mouseY <= by + bh) {
+ // Reset battle
+ deflectedRocks = []; scorePopups = []; hitBursts = [];
+ screenFlash = 0;
+ initBattle();
+ }
return;
}
}
+
+// Returns SET SAIL button hit rect (mirrors drawDepartureQuest layout)
+function getSailBtnBounds() {
+ let dw = min(width * 0.54, 580);
+ let dh = 190;
+ let dx = (width - dw) / 2;
+ let dy = (height - dh) / 2;
+ return { x: dx + dw / 2 - 90, y: dy + dh - 36 - 16, w: 180, h: 36 };
+}
+
+// Dev shortcut: press 'S' from any non-playing screen to jump to sailing
+// ====== BATTLE SYSTEM ======
+class Rock {
+ constructor(tx, ty) {
+ // Always launch from screen center (background enemy position)
+ this.vpx = width / 2 + random(-width * 0.08, width * 0.08);
+ this.vpy = height * 0.32 + random(-30, 30);
+ this.tx = tx + random(-30, 30);
+ this.ty = ty;
+ // Arc height: how high above the straight line the peak rises
+ this.arcH = random(height * 0.22, height * 0.38);
+ this.t = 0;
+ this.spd = random(0.020, 0.032);
+ this.r0 = random(5, 9);
+ this.r1 = random(60, 90);
+ this.x = this.vpx;
+ this.y = this.vpy;
+ this.size = this.r0;
+ this.rot = random(TWO_PI);
+ this.rotSpd = random(-0.18, 0.18);
+ this.hp = floor(random(2, 4));
+ this.maxHp = this.hp;
+ this.hit = 0;
+ this.targetWarrior = null; // homing reference
+ this.trail = [];
+ this.verts = [];
+ for (let i = 0; i < 9; i++) {
+ let a = (TWO_PI / 9) * i + random(-0.3, 0.3);
+ this.verts.push({ a, r: random(0.65, 1.25) });
+ }
+ }
+ update() {
+ // Soft homing toward active warrior x
+ if (this.targetWarrior && !this.targetWarrior.dead && this.t < 0.70) {
+ this.tx = lerp(this.tx, this.targetWarrior.x, 0.025 * this.t);
+ }
+ this.trail.push({ x: this.x, y: this.y, size: this.size });
+ if (this.trail.length > 10) this.trail.shift();
+ this.t = min(this.t + this.spd, 1);
+ this.x = lerp(this.vpx, this.tx, this.t);
+ // Parabolic arc: linear interpolation minus upward arc peak at t=0.5
+ this.y = lerp(this.vpy, this.ty, this.t) - this.arcH * sin(this.t * PI);
+ this.size = lerp(this.r0, this.r1, this.t * this.t);
+ this.rot += this.rotSpd * (1 + this.t * 2);
+ if (this.hit > 0) this.hit--;
+ }
+ draw() {
+ // Fire trail
+ for (let i = 0; i < this.trail.length; i++) {
+ let tp = this.trail[i];
+ let frac = i / this.trail.length;
+ let ta = frac * 0.6;
+ let ts = tp.size * frac * 0.85;
+ noStroke();
+ // Trail fades from orange-red at head to dark red at tail
+ fill(lerpColor(color(255, 40, 0, ta * 220), color(180, 0, 0, ta * 80), 1 - frac));
+ ellipse(tp.x, tp.y, ts * 2.0, ts * 2.0);
+ }
+ let flashHit = this.hit > 0;
+ let pulse = 0.5 + 0.5 * sin(frameCount * 0.5); // pulsing glow
+ push();
+ translate(this.x, this.y);
+ rotate(this.rot);
+
+ // Wide outer fire glow (additive-like, very transparent)
+ let glowR = this.size * (1.7 + pulse * 0.25);
+ noStroke();
+ fill(255, 60, 0, 35 + pulse * 30);
+ beginShape();
+ for (let v of this.verts) vertex(cos(v.a) * v.r * glowR, sin(v.a) * v.r * glowR);
+ endShape(CLOSE);
+
+ // Mid glow — orange
+ fill(255, 120, 0, 80 + pulse * 40);
+ let midR = this.size * (1.25 + pulse * 0.1);
+ beginShape();
+ for (let v of this.verts) vertex(cos(v.a) * v.r * midR, sin(v.a) * v.r * midR);
+ endShape(CLOSE);
+
+ // Main body — deep red to orange-red
+ let c = flashHit
+ ? color(255, 240, 80)
+ : lerpColor(color(200, 20, 0), color(255, 80, 10), pulse);
+ fill(c);
+ beginShape();
+ for (let v of this.verts) vertex(cos(v.a) * v.r * this.size, sin(v.a) * v.r * this.size);
+ endShape(CLOSE);
+
+ // Hot white-yellow core highlight
+ stroke(255, flashHit ? 255 : 180, flashHit ? 100 : 40, flashHit ? 255 : 140 + pulse * 80);
+ strokeWeight(3);
+ noFill();
+ beginShape();
+ for (let v of this.verts) vertex(cos(v.a) * v.r * this.size * 0.72, sin(v.a) * v.r * this.size * 0.72);
+ endShape(CLOSE);
+ pop();
+ noStroke();
+ }
+}
+
+function initBattle() {
+ warriors = [];
+ activeWarriorIdx = 0;
+ battleScore = 0;
+ let frontY = height * 0.80;
+ let backY = height * 0.88;
+ let backXs = [0.05, 0.19, 0.33, 0.55, 0.72, 0.88];
+ for (let i = 0; i < 7; i++) {
+ let pwr = slotPowers[i] !== undefined ? slotPowers[i] : 50;
+ let maxHp = max(2, floor(pwr / 35) + 1);
+ let px = i === 0 ? width / 2 : width * backXs[i - 1];
+ let py = i === 0 ? frontY : backY + random(-15, 15);
+ warriors.push({
+ charIdx: slotChars[i] >= 0 ? slotChars[i] : i % 7,
+ offset: slotOffsets[i] || 0,
+ power: pwr, maxHp, hp: maxHp,
+ dead: false, deathT: 0, hitFlash: 0,
+ x: px, y: py,
+ jumpT: 0, jumpH: 0, dashCd: 0,
+ bobRate: random(0.04, 0.09),
+ bobPhase: random(TWO_PI)
+ });
+ }
+ rocks = []; battleHits = []; hitBursts = [];
+ battleOver = false; battleResult = '';
+ rockSpawnCd = 40;
+ initFireEffect();
+}
+
+function initFireEffect() {
+ if (!window.Proton) return;
+ if (proton) { proton.destroy(); proton = null; }
+
+ proton = new Proton();
+ fireEmitter = new Proton.Emitter();
+
+ // Start with rate=0; updated each frame based on dead count
+ fireEmitter.rate = new Proton.Rate(new Proton.Span(0, 0), 0.05);
+
+ fireEmitter.addInitialize(new Proton.Radius(6, 20));
+ fireEmitter.addInitialize(new Proton.Life(0.7, 1.8));
+ fireEmitter.addInitialize(
+ new Proton.Velocity(new Proton.Span(1.5, 3.5), new Proton.Span(260, 280, true), 'polar')
+ );
+
+ fireEmitter.addBehaviour(new Proton.Color('#ff2200', '#ffcc00'));
+ fireEmitter.addBehaviour(new Proton.Alpha(0.85, 0));
+ fireEmitter.addBehaviour(new Proton.Scale(1.4, 0));
+
+ fireEmitter.p.x = width / 2;
+ fireEmitter.p.y = height + 5;
+ fireEmitter.emit();
+
+ proton.addEmitter(fireEmitter);
+ proton.addRenderer(new Proton.CanvasRenderer(pCanvas.elt));
+}
+
+function spawnBurst(x, y, big, redRing = false) {
+ let count = big ? 32 : 18;
+ for (let b = 0; b < count; b++) {
+ let a = random(TWO_PI);
+ let spd = big ? random(4, 14) : random(3, 9);
+ let col = b < count * 0.25 ? [255, 255, 200]
+ : b < count * 0.55 ? [255, floor(random(120, 200)), 20]
+ : [220, 55, 20];
+ hitBursts.push({
+ x, y,
+ vx: cos(a) * spd, vy: sin(a) * spd - (big ? 3.5 : 2),
+ life: floor(random(big ? 28 : 20, big ? 55 : 38)),
+ maxLife: big ? 55 : 38,
+ r: random(big ? 8 : 4, big ? 22 : 13),
+ ring: false, col
+ });
+ }
+ let rings = big ? 3 : 2;
+ for (let ri = 0; ri < rings; ri++) {
+ let life = 22 - ri * 5;
+ hitBursts.push({ x, y, ring: true, redRing, life, maxLife: life, r: big ? 14 - ri * 3 : 8 - ri * 2 });
+ }
+ if (big) screenFlash = 12;
+}
+
+function updateBattle() {
+ if (battleOver) return;
+ if (screenFlash > 0) screenFlash--;
+
+ let aw = warriors[activeWarriorIdx];
+
+ // ── Player controls active warrior ──
+ if (aw && !aw.dead) {
+ if (kb.pressing('left')) aw.x = max(width * 0.06, aw.x - 13);
+ if (kb.pressing('right')) aw.x = min(width * 0.94, aw.x + 13);
+ if (aw.jumpT > 0) aw.jumpT = max(0, aw.jumpT - 0.07);
+ if (aw.hitFlash > 0) aw.hitFlash--;
+
+ // Space or Z = attack swing
+ if (attackKeyPressed) {
+ attackKeyPressed = false;
+ aw.jumpT = 1.0; aw.jumpH = 60;
+ strikesSound.play();
+ let hit = false;
+ // Find closest rock in generous range
+ let closest = null, closestD = Infinity;
+ for (let r of rocks) {
+ let d = dist(aw.x, aw.y, r.x, r.y);
+ if (d < r.size + 100 && d < closestD) { closestD = d; closest = r; }
+ }
+ if (closest) {
+ closest.hp -= 2; closest.hit = 12;
+ battleHits.push({ x1: aw.x, y1: aw.y - 30, x2: closest.x, y2: closest.y, life: 14, type: 'slash' });
+ if (closest.hp <= 0) {
+ // Deflect: rock flies away
+ let dvx = (closest.x - aw.x) * 0.22 + random(-8, 8);
+ let dvy = random(-22, -14);
+ deflectedRocks.push({
+ x: closest.x, y: closest.y,
+ vx: dvx, vy: dvy,
+ size: closest.size * 1.15, rot: closest.rot,
+ rotSpd: (dvx > 0 ? 1 : -1) * random(0.32, 0.65),
+ verts: closest.verts, alpha: 255,
+ trail: []
+ });
+ spawnBurst(closest.x, closest.y, true, true);
+ let pts = max(1, floor(aw.power / 20));
+ battleScore += pts;
+ scorePopups.push({ x: aw.x + random(-20, 20), y: aw.y - 80, val: '+' + pts, life: 55, maxLife: 55 });
+ rocks.splice(rocks.indexOf(closest), 1);
+ } else {
+ // Partial hit — still give 1 point
+ battleScore += 1;
+ scorePopups.push({ x: aw.x + random(-20, 20), y: aw.y - 80, val: '+1', life: 40, maxLife: 40 });
+ }
+ hit = true;
+ }
+ if (!hit) battleHits.push({ x1: aw.x - 70, y1: aw.y - 20, x2: aw.x + 70, y2: aw.y - 40, life: 10, type: 'slash' });
+ }
+ }
+
+ // ── Spawn rocks — irregular timing, burst or trickle ──
+ rockSpawnCd--;
+ if (rockSpawnCd <= 0 && aw && !aw.dead) {
+ let baseRate = max(10, 30 - floor(battleScore * 0.4));
+ let maxVolley = min(4, 1 + floor(battleScore / 8));
+
+ // Random pattern each volley: single, quick double, or full burst
+ let pattern = random();
+ let count, nextCd;
+ if (pattern < 0.35) {
+ // Single rock, short wait
+ count = 1;
+ nextCd = floor(random(baseRate * 0.6, baseRate * 1.1));
+ } else if (pattern < 0.70) {
+ // Quick 2-rock burst
+ count = min(2, maxVolley);
+ nextCd = floor(random(baseRate * 0.9, baseRate * 1.5));
+ } else {
+ // Full volley, medium breather
+ count = maxVolley;
+ nextCd = floor(random(baseRate * 1.2, baseRate * 2.0));
+ }
+
+ for (let v = 0; v < count; v++) {
+ let targetX = random() < 0.6
+ ? aw.x + random(-width * 0.22, width * 0.22)
+ : random(width * 0.05, width * 0.95);
+ let r = new Rock(targetX, aw.y);
+ r.targetWarrior = aw;
+ // Each rock in a volley gets a slightly different speed so they don't land together
+ r.spd = random(0.018, 0.042) + battleScore * 0.0008;
+ rocks.push(r);
+ }
+ rockSpawnCd = nextCd;
+ }
+
+ // ── Update rocks, check collision ──
+ for (let i = rocks.length - 1; i >= 0; i--) {
+ let r = rocks[i];
+ r.update();
+ if (r.t >= 1) {
+ // Only damage if warrior is near landing spot and NOT jumping
+ let hitRadius = r.r1 * 0.55 + 35;
+ let inAir = aw && aw.jumpH >= 130 && aw.jumpT > 0.15;
+ let actuallyHit = aw && !aw.dead && !inAir && dist(r.tx, r.ty, aw.x, aw.y) < hitRadius;
+ if (actuallyHit) {
+ aw.hp--; aw.hitFlash = 28;
+ aw.power = max(5, aw.power - 8);
+ owSound.play();
+ spawnBurst(aw.x, aw.y, true);
+ battleHits.push({ x1: r.x, y1: r.y, x2: aw.x, y2: aw.y, life: 18, type: 'impact' });
+ if (aw.hp <= 0) {
+ aw.dead = true;
+ let next = activeWarriorIdx + 1;
+ while (next < warriors.length && warriors[next].dead) next++;
+ activeWarriorIdx = next;
+ // Reposition next warrior to front
+ if (activeWarriorIdx < warriors.length) {
+ warriors[activeWarriorIdx].x = width / 2;
+ warriors[activeWarriorIdx].y = height * 0.80;
+ }
+ }
+ } else {
+ bombSound.play();
+ }
+ rocks.splice(i, 1);
+ }
+ }
+
+ // Dead warriors tick deathT
+ for (let w of warriors) { if (w.dead) w.deathT++; }
+
+ for (let i = battleHits.length - 1; i >= 0; i--) {
+ if (--battleHits[i].life <= 0) battleHits.splice(i, 1);
+ }
+
+ if (activeWarriorIdx >= warriors.length) {
+ battleOver = true; battleResult = 'defeat';
+ shipSound.stop();
+ }
+}
+
+function drawWarriorsOnDeck() {
+ for (let i = 0; i < warriors.length; i++) {
+ let w = warriors[i];
+ let isActive = (i === activeWarriorIdx);
+ let sz = isActive ? 110 : 85;
+ let frames = (isActive ? battleFrames[w.charIdx] : charFrames[w.charIdx]) || charFrames[w.charIdx];
+ if (!frames) continue;
+
+ if (!w.dead) {
+ let bobY = sin(frameCount * w.bobRate + w.bobPhase) * (isActive ? 5 : 3);
+ let jumpY = sin(w.jumpT * PI) * w.jumpH;
+ let drawY = w.y + bobY - jumpY;
+ let fps = isActive ? BATTLE_ANI_FPS : 32; // waiting warriors animate slowly
+ let fi = floor((frameCount + w.offset) / fps) % frames.length;
+ let img = frames[fi];
+ let sc = min(sz / img.width, sz / img.height) * CHAR_SCALES[w.charIdx];
+
+ // Waiting warriors: dimmer
+ if (!isActive) tint(180, 180, 200, 190);
+ if (w.hitFlash > 0 && w.hitFlash % 4 < 2) tint(255, 70, 70);
+ imageMode(CENTER);
+ image(img, w.x, drawY, img.width * sc, img.height * sc);
+ noTint();
+
+ // Active warrior glow ring
+ if (isActive) {
+ noFill();
+ stroke(100, 220, 255, 80 + sin(frameCount * 0.1) * 40);
+ strokeWeight(5);
+ ellipse(w.x, drawY, sz * 1.2, sz * 1.2);
+ noStroke();
+ }
+
+ // Shadow — scales with jump height
+ let shadowScale = map(jumpY, 0, 130, 1, 0.15);
+ noStroke(); fill(0, 0, 0, 55 * shadowScale);
+ ellipse(w.x, w.y + sz * 0.52, sz * 0.9 * shadowScale, sz * 0.18 * shadowScale);
+
+ // HP bar (active only shows full; waiting shows compact)
+ if (isActive) {
+ let hpF = w.hp / w.maxHp;
+ let barW = sz + 20; let barH = 8;
+ let barX = w.x - barW / 2;
+ let barY = w.y + sz * 0.65;
+ noStroke(); fill(20, 20, 20, 200);
+ rect(barX, barY, barW, barH, 4);
+ fill(lerpColor(color(220, 50, 50), color(55, 210, 85), min(hpF / 0.55, 1)));
+ rect(barX, barY, barW * hpF, barH, 4);
+ // Power label
+ let pwrT = min(w.power / 55, 1);
+ let pwrCol = lerpColor(color(220, 80, 80), color(80, 220, 100), pwrT);
+ noStroke(); fill(0, 0, 0, 140);
+ rect(w.x - 26, drawY - sz * 0.68 - 14, 52, 17, 4);
+ fill(pwrCol);
+ textAlign(CENTER, CENTER);
+ textFont('Nunito, sans-serif');
+ textStyle(BOLD); textSize(11);
+ text(`PWR ${w.power}`, w.x, drawY - sz * 0.68 - 6);
+ textStyle(NORMAL); textFont('Nunito, sans-serif');
+ }
+ } else if (w.deathT < 50) {
+ let img = frames[0];
+ let sc = min(sz / img.width, sz / img.height) * CHAR_SCALES[w.charIdx];
+ tint(200, 70, 70, max(0, 220 - w.deathT * 5));
+ imageMode(CENTER);
+ image(img, w.x, w.y + w.deathT * 1.6, img.width * sc, img.height * sc);
+ noTint();
+ }
+ }
+}
+
+function drawBattleHits() {
+ for (let h of battleHits) {
+ let t = h.life / 16; // 1→0
+ let a = t * 255;
+ if (h.type === 'slash') {
+ // Wide glowing slash arc — 3 layered strokes
+ let cx = (h.x1 + h.x2) / 2;
+ let cy = (h.y1 + h.y2) / 2;
+ let ang = atan2(h.y2 - h.y1, h.x2 - h.x1);
+ let len = dist(h.x1, h.y1, h.x2, h.y2);
+
+ // Outer glow (very wide, faint orange-white)
+ stroke(255, 210, 100, a * 0.4);
+ strokeWeight(55 * t);
+ push(); noFill(); translate(cx, cy); rotate(ang);
+ arc(0, 0, len * 1.5, len * 0.9, -PI * 0.6, PI * 0.6);
+ pop();
+
+ // Mid glow (yellow-white)
+ stroke(255, 245, 160, a * 0.7);
+ strokeWeight(26 * t);
+ push(); noFill(); translate(cx, cy); rotate(ang);
+ arc(0, 0, len * 1.25, len * 0.7, -PI * 0.55, PI * 0.55);
+ pop();
+
+ // Sharp bright core (white)
+ stroke(255, 255, 255, a);
+ strokeWeight(6 * t);
+ push(); noFill(); translate(cx, cy); rotate(ang);
+ arc(0, 0, len * 1.0, len * 0.55, -PI * 0.5, PI * 0.5);
+ pop();
+
+ // Radial spark lines at impact point
+ let sp = min(h.life, 8) / 8;
+ let nSpk = 10;
+ stroke(255, 240, 120, sp * 240);
+ strokeWeight(3 * sp);
+ for (let k = 0; k < nSpk; k++) {
+ let sa = ang + map(k, 0, nSpk - 1, -PI * 0.6, PI * 0.6);
+ let r0 = 14;
+ let r1 = 14 + sp * random(35, 70);
+ line(h.x2 + cos(sa) * r0, h.y2 + sin(sa) * r0,
+ h.x2 + cos(sa) * r1, h.y2 + sin(sa) * r1);
+ }
+ } else {
+ // Impact hit — orange blast ring
+ let a2 = map(h.life, 0, 16, 0, 255);
+ noFill();
+ stroke(255, 140, 40, a2 * 0.7);
+ strokeWeight(8 * t);
+ ellipse((h.x1 + h.x2) / 2, (h.y1 + h.y2) / 2, 50 * (1 - t + 0.3), 50 * (1 - t + 0.3));
+ stroke(255, 200, 100, a2);
+ strokeWeight(2.5 * t);
+ ellipse((h.x1 + h.x2) / 2, (h.y1 + h.y2) / 2, 28 * (1 - t + 0.3), 28 * (1 - t + 0.3));
+ }
+ }
+ noStroke();
+}
+
+// ====== WAKE PARTICLES ======
+class WakeParticle {
+ constructor(x, y) {
+ this.x = x + random(-22, 22);
+ this.y = y + random(-5, 8);
+ this.vx = random(1.8, 4.5); // drifts right as boat moves left
+ this.vy = random(-0.3, 0.7);
+ this.life = random(140, 240);
+ this.decay = random(3.5, 6);
+ this.r = random(3, 10);
+ }
+ update() {
+ this.x += this.vx;
+ this.y += this.vy;
+ this.vx *= 0.97;
+ this.life -= this.decay;
+ }
+ draw() {
+ noStroke();
+ fill(255, 255, 255, this.life * 0.6);
+ ellipse(this.x, this.y, this.r, this.r * 0.42);
+ }
+}
+
+// ====== SAILING SCENE ======
+function drawSailingScene() {
+ if (!battleStarted) {
+ initBattle();
+ battleStarted = true;
+ if (!shipSound.playing()) shipSound.play();
+ }
+
+ // Fill canvas first so rotation gaps never show white
+ background(10, 18, 35);
+
+ let t = millis() / 1000;
+ let tiltDeg = sin(t * 0.22) * 10 + sin(t * 0.57) * 2.5;
+ let tilt = radians(tiltDeg);
+
+ // Scale image to cover the full canvas (contain = no crop, cover = no gap)
+ let imgW = sailBgImg.width;
+ let imgH = sailBgImg.height;
+ let scaleX = width / imgW;
+ let scaleY = height / imgH;
+ // Use the larger scale so the image covers the canvas (no gaps), then +extra for rotation
+ let sc = max(scaleX, scaleY) * 1.42;
+ let drawW = imgW * sc;
+ let drawH = imgH * sc;
+
+ push();
+ translate(width / 2, height / 2);
+ rotate(tilt);
+ imageMode(CENTER);
+ image(sailBgImg, 0, 0, drawW, drawH);
+ pop();
+
+ // Battle logic + rendering
+ updateBattle();
+ for (let r of rocks) r.draw();
+ drawWarriorsOnDeck();
+ drawBattleHits();
+
+ // Hit burst particles
+ noStroke();
+ for (let i = hitBursts.length - 1; i >= 0; i--) {
+ let b = hitBursts[i];
+ b.life--;
+ if (b.life <= 0) { hitBursts.splice(i, 1); continue; }
+ let t = b.life / b.maxLife;
+ if (b.ring) {
+ let radius = map(t, 1, 0, 10, 90);
+ push(); noFill();
+ if (b.redRing) stroke(255, 40, 40, t * 255);
+ else stroke(255, 200, 80, t * 220);
+ strokeWeight(4 * t);
+ ellipse(b.x, b.y, radius * 2, radius * 2);
+ pop();
+ } else if (b.ghost) {
+ // Dash afterimage — wide horizontal streak
+ noStroke();
+ fill(180, 230, 255, t * 140);
+ rect(b.x - 30, b.y - 40, 60, 80, 8);
+ } else {
+ b.x += b.vx; b.y += b.vy;
+ b.vy += 0.28; b.vx *= 0.92;
+ fill(b.col[0], b.col[1], b.col[2], t * 230);
+ ellipse(b.x, b.y, b.r * t + 1, b.r * t + 1);
+ }
+ }
+ noStroke();
+
+ // Deflected rocks flying away
+ for (let i = deflectedRocks.length - 1; i >= 0; i--) {
+ let d = deflectedRocks[i];
+ d.trail.push({ x: d.x, y: d.y, rot: d.rot, alpha: d.alpha });
+ if (d.trail.length > 8) d.trail.shift();
+ d.x += d.vx; d.y += d.vy;
+ d.vy += 0.6; d.vx *= 0.98;
+ d.rot += d.rotSpd;
+ d.alpha -= 4;
+ if (d.alpha <= 0 || d.y > height + 120) { deflectedRocks.splice(i, 1); continue; }
+
+ // Motion trail (ghost copies fading out)
+ for (let ti = 0; ti < d.trail.length; ti++) {
+ let tr = d.trail[ti];
+ let ta = (ti / d.trail.length) * d.alpha * 0.4;
+ let ts = d.size * (0.55 + 0.45 * (ti / d.trail.length));
+ push();
+ translate(tr.x, tr.y);
+ rotate(tr.rot);
+ fill(200, 170, 120, ta);
+ noStroke();
+ beginShape();
+ for (let v of d.verts) vertex(cos(v.a) * v.r * ts, sin(v.a) * v.r * ts);
+ endShape(CLOSE);
+ pop();
+ }
+
+ // Main rock
+ push();
+ translate(d.x, d.y);
+ rotate(d.rot);
+ // Glowing orange outline at first
+ if (d.alpha > 160) {
+ stroke(255, 140, 40, (d.alpha - 160) * 2);
+ strokeWeight(3);
+ } else {
+ noStroke();
+ }
+ fill(160, 140, 110, d.alpha);
+ beginShape();
+ for (let v of d.verts) vertex(cos(v.a) * v.r * d.size, sin(v.a) * v.r * d.size);
+ endShape(CLOSE);
+ noStroke();
+ pop();
+ }
+
+ // Score popups
+ for (let i = scorePopups.length - 1; i >= 0; i--) {
+ let p = scorePopups[i];
+ p.life--;
+ if (p.life <= 0) { scorePopups.splice(i, 1); continue; }
+ let t = p.life / p.maxLife;
+ let py = p.y - (1 - t) * 70; // floats upward
+ let sz = t > 0.75 ? map(t, 0.75, 1, 38, 52) : map(t, 0, 0.75, 14, 38); // pop then shrink
+ textAlign(CENTER, CENTER);
+ textFont('Nunito, sans-serif');
+ textStyle(BOLD);
+ textSize(sz);
+ // Shadow
+ fill(80, 40, 0, t * 200);
+ text(p.val, p.x + 2, py + 2);
+ // Main text
+ fill(255, 240, 60, t * 255);
+ stroke(200, 80, 0, t * 220);
+ strokeWeight(3);
+ text(p.val, p.x, py);
+ textStyle(NORMAL); noStroke();
+ }
+
+ // Wake particles
+ if (frameCount % 2 === 0) wakeParticles.push(new WakeParticle(width * 0.78, height * 0.73));
+ if (frameCount % 5 === 0) {
+ let p = new WakeParticle(width * 0.22, height * 0.69);
+ p.vx = random(-1.5, 0.5); p.vy = random(-2.5, -0.8); p.decay = 9;
+ wakeParticles.push(p);
+ }
+ for (let i = wakeParticles.length - 1; i >= 0; i--) {
+ wakeParticles[i].update(); wakeParticles[i].draw();
+ if (wakeParticles[i].life <= 0) wakeParticles.splice(i, 1);
+ }
+
+ // Screen flash on hit
+ if (screenFlash > 0) {
+ noStroke();
+ fill(255, 120, 30, screenFlash * 14);
+ rect(0, 0, width, height);
+ }
+
+ // Fire effect at bottom — scales with dead warriors
+ if (proton && fireEmitter && warriors.length > 0) {
+ let deadCount = warriors.filter(w => w.dead).length;
+ let n = deadCount * 5;
+ fireEmitter.rate = new Proton.Rate(new Proton.Span(n, n + 3), 0.05);
+ // Spread emitter x across bottom based on dead count
+ fireEmitter.p.x = width / 2 + sin(millis() * 0.001) * (deadCount * 30);
+ fireEmitter.p.y = height + 5;
+ drawingContext.save();
+ drawingContext.globalCompositeOperation = 'lighter';
+ proton.update();
+ drawingContext.restore();
+ }
+
+ // Top status bar
+ drawBattleStatusBar();
+
+ // Score + controls HUD
+ if (!battleOver) {
+ // Score pill top-right
+ let aw = warriors[activeWarriorIdx];
+ noStroke(); fill(8, 12, 22, 200);
+ rect(width - 160, 10, 150, 52, 6);
+ textFont('Nunito, sans-serif'); textSize(11); fill(160, 190, 220);
+ textAlign(LEFT, TOP);
+ text('SCORE', width - 148, 18);
+ textStyle(BOLD); textSize(26); fill(255, 220, 80);
+ text(battleScore, width - 148, 32);
+ textStyle(NORMAL);
+ // Power loss warning flash
+ if (aw && aw.hitFlash > 0) {
+ textSize(13); fill(255, 80, 80);
+ textAlign(CENTER, TOP);
+ text('-PWR', width - 83, 20);
+ }
+ // Controls
+ textFont('Nunito, sans-serif'); textSize(12); fill(255, 255, 255, 100);
+ textAlign(CENTER, BOTTOM);
+ text('← → MOVE SPACE / Z ATTACK', width / 2, height - 12);
+ textFont('Nunito, sans-serif');
+ }
+
+ // Game over
+ if (battleOver && battleResult === 'defeat') {
+ fill(0, 0, 0, 160);
+ noStroke();
+ rect(0, 0, width, height);
+ textFont(moanaFont);
+ textAlign(CENTER, CENTER);
+ textSize(80);
+ fill(220, 55, 55);
+ text('DEFEAT', width/2, height/2 - 30);
+ textFont('Nunito, sans-serif');
+ textSize(20);
+ fill(200, 175, 155);
+ text('The crew has fallen.', width/2, height/2 + 40);
+ textSize(26); fill(255, 215, 80);
+ text(`SCORE: ${battleScore}`, width/2, height/2 + 80);
+
+ // Try Again button
+ let bw = 180, bh = 46;
+ let bx = width/2 - bw/2, by = height/2 + 120;
+ let over = mouseX >= bx && mouseX <= bx + bw && mouseY >= by && mouseY <= by + bh;
+ fill(over ? color(255, 200, 60) : color(200, 140, 30));
+ stroke(255, 230, 120); strokeWeight(2);
+ rect(bx, by, bw, bh, 10);
+ noStroke();
+ fill(30, 15, 5);
+ textFont('Nunito, sans-serif'); textStyle(BOLD);
+ textSize(20);
+ text('TRY AGAIN', width/2, by + bh/2 + 1);
+ textStyle(NORMAL);
+ }
+}
+
+
+function drawBattleStatusBar() {
+ let n = warriors.length;
+ if (n === 0) return;
+ let iconSz = 36;
+ let gap = 8;
+ let barW = n * (iconSz + gap) - gap + 24;
+ let barH = iconSz + 22;
+ let bx = (width - barW) / 2;
+ let by = 10;
+
+ // Panel
+ noStroke();
+ fill(8, 12, 22, 200);
+ rect(bx, by, barW, barH, 6);
+
+ for (let i = 0; i < n; i++) {
+ let w = warriors[i];
+ let cx = bx + 12 + i * (iconSz + gap) + iconSz / 2;
+ let cy = by + barH / 2;
+ let frames = battleFrames[w.charIdx] || charFrames[w.charIdx];
+ if (!frames) continue;
+
+ if (w.dead) {
+ // Dark slot + X mark
+ fill(40, 10, 10, 220);
+ ellipse(cx, cy, iconSz, iconSz);
+ stroke(200, 50, 50, 200);
+ strokeWeight(2.5);
+ let r = iconSz * 0.28;
+ line(cx - r, cy - r, cx + r, cy + r);
+ line(cx + r, cy - r, cx - r, cy + r);
+ noStroke();
+ } else {
+ // Alive — small portrait clipped to circle
+ let fi = floor((frameCount + w.offset) / ANI_FPS) % frames.length;
+ let img = frames[fi];
+ let sc = min(iconSz / img.width, iconSz / img.height) * CHAR_SCALES[w.charIdx];
+ drawingContext.save();
+ drawingContext.beginPath();
+ drawingContext.arc(cx, cy, iconSz / 2, 0, Math.PI * 2);
+ drawingContext.clip();
+ imageMode(CENTER);
+ image(img, cx, cy, img.width * sc, img.height * sc);
+ drawingContext.restore();
+ // HP ring
+ let hpF = w.hp / w.maxHp;
+ let hpCol = lerpColor(color(220, 50, 50), color(55, 210, 85), min(hpF / 0.55, 1));
+ noFill();
+ stroke(hpCol);
+ strokeWeight(2.5);
+ arc(cx, cy, iconSz + 3, iconSz + 3, -HALF_PI, -HALF_PI + TWO_PI * hpF);
+ noStroke();
+ }
+ }
+ // Dead count label
+ let deadCount = warriors.filter(w => w.dead).length;
+ if (deadCount > 0) {
+ fill(220, 60, 60);
+ textAlign(RIGHT, CENTER);
+ textFont(moanaFont);
+ textSize(13);
+ text(`${deadCount} FALLEN`, bx + barW - 6, by + barH / 2);
+ textFont('Nunito, sans-serif');
+ }
+}
+
+function drawWarriorCircle(charIdx, offset, x, y, sz) {
+ let frames = charFrames[charIdx];
+ let frameIdx = floor((frameCount + offset) / ANI_FPS) % frames.length;
+ let img = frames[frameIdx];
+ let sc = min(sz / img.width, sz / img.height) * CHAR_SCALES[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();
+
+ noFill();
+ strokeWeight(2.5);
+ stroke(255, 255, 255, 190);
+ ellipse(x, y, sz, sz);
+ noStroke();
+}