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(); +}