added kakamora, count feature
BIN
assets/coconut/coconut_0.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
assets/coconut/coconut_1.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
assets/coconut/coconut_2.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
assets/coconut/coconut_3.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
assets/coconut/coconut_4.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/fonts/Moanas.ttf
Normal file
BIN
assets/kakamora characters/Kakamora-TikiRob-Claw-Edition.png
Normal file
|
After Width: | Height: | Size: 413 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 54 KiB |
BIN
assets/kakamora characters/Kokomoro-TikiRob-Seashell-Edition.png
Normal file
|
After Width: | Height: | Size: 442 KiB |
|
After Width: | Height: | Size: 66 KiB |
@@ -10,6 +10,7 @@
|
|||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||||||
|
<script src="src/Player.js"></script>
|
||||||
<script src="sketch.js"></script>
|
<script src="sketch.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
417
sketch.js
@@ -1,161 +1,324 @@
|
|||||||
// ====== [GLOBAL VARIABLES] ======
|
// ====== GLOBALS ======
|
||||||
let trail = [];
|
let trail = [];
|
||||||
let velocities = [];
|
let velocities = [];
|
||||||
|
|
||||||
let particles = [];
|
let particles = [];
|
||||||
let flashAlpha = 0;
|
let flashAlpha = 0;
|
||||||
let lastSliceTime = 0;
|
let score = 0;
|
||||||
|
let bgBlue = 0; // 0=dark, 255=blue, fades out
|
||||||
|
|
||||||
let coconut = {
|
let coconut = {
|
||||||
x: 400,
|
x: 0, y: 0, // setup()에서 초기화
|
||||||
y: 300,
|
r: 0,
|
||||||
r: 50
|
state: 0,
|
||||||
|
shakeX: 0,
|
||||||
|
shakeTimer: 0,
|
||||||
|
// fly-away
|
||||||
|
flying: false,
|
||||||
|
flyVX: 0, flyVY: 0,
|
||||||
|
flyAlpha: 255,
|
||||||
|
flyRotation: 0, flyRotSpeed: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
// ====== [SETUP] ======
|
let coconutImgs = [];
|
||||||
|
let kakomoraImgs = [];
|
||||||
|
let flyingKakamoraList = [];
|
||||||
|
|
||||||
|
// ====== PRELOAD ======
|
||||||
|
function preload() {
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
coconutImgs[i] = loadImage(`assets/coconut/coconut_${i}.png`);
|
||||||
|
}
|
||||||
|
kakomoraImgs[0] = loadImage('assets/kakamora characters/Kakamora-TikiRob-Claw-Edition.png');
|
||||||
|
kakomoraImgs[1] = loadImage('assets/kakamora characters/Kokomoro-TikiRob-Fishhead-Edition.webp');
|
||||||
|
kakomoraImgs[2] = loadImage('assets/kakamora characters/Kokomoro-TikiRob-Mickey-Mouse-Edition-1-250x250.png');
|
||||||
|
kakomoraImgs[3] = loadImage('assets/kakamora characters/Kokomoro-TikiRob-Seashell-Edition.png');
|
||||||
|
kakomoraImgs[4] = loadImage('assets/kakamora characters/Kokomoro-TikiRob-Starfish-Edition-250x250.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== SETUP ======
|
||||||
function setup() {
|
function setup() {
|
||||||
createCanvas(800, 600);
|
createCanvas(windowWidth, windowHeight);
|
||||||
|
resetCoconutHome();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== [DRAW LOOP] ======
|
function windowResized() {
|
||||||
|
resizeCanvas(windowWidth, windowHeight);
|
||||||
|
resetCoconutHome();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetCoconutHome() {
|
||||||
|
coconut.r = min(windowWidth, windowHeight) * 0.13;
|
||||||
|
coconut.x = width / 2;
|
||||||
|
coconut.y = height * 0.42;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== DRAW ======
|
||||||
function draw() {
|
function draw() {
|
||||||
background(30);
|
background(lerpColor(color(30), color(25, 90, 200), bgBlue / 255));
|
||||||
|
if (bgBlue > 0) bgBlue = max(0, bgBlue - 4);
|
||||||
|
|
||||||
drawTrail();
|
updateShake();
|
||||||
drawCoconut();
|
updateFlyAway();
|
||||||
checkSlice();
|
drawCoconut();
|
||||||
|
drawTrail();
|
||||||
|
|
||||||
// ====== PARTICLES ======
|
drawParticles();
|
||||||
for (let i = particles.length - 1; i >= 0; i--) {
|
drawFlash();
|
||||||
particles[i].update();
|
drawScore();
|
||||||
particles[i].draw();
|
drawKakamoraSlots();
|
||||||
|
updateFlyingKakamoraList();
|
||||||
if (particles[i].life <= 0) {
|
drawFlyingKakamoraList();
|
||||||
particles.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====== FLASH EFFECT ======
|
|
||||||
if (flashAlpha > 0) {
|
|
||||||
fill(255, flashAlpha);
|
|
||||||
rect(0, 0, width, height);
|
|
||||||
flashAlpha -= 20;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== [INPUT: KNIFE TRACKING] ======
|
// ====== COCONUT ======
|
||||||
function mouseDragged() {
|
|
||||||
let pos = createVector(mouseX, mouseY);
|
|
||||||
trail.push(pos);
|
|
||||||
|
|
||||||
if (trail.length > 1) {
|
|
||||||
let v = p5.Vector.sub(
|
|
||||||
trail[trail.length - 1],
|
|
||||||
trail[trail.length - 2]
|
|
||||||
);
|
|
||||||
velocities.push(v.mag());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trail.length > 20) trail.shift();
|
|
||||||
if (velocities.length > 20) velocities.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====== [DRAW TRAIL] ======
|
|
||||||
function drawTrail() {
|
|
||||||
noFill();
|
|
||||||
|
|
||||||
for (let i = 1; i < trail.length; i++) {
|
|
||||||
let speed = velocities[i] || 0;
|
|
||||||
let thickness = map(speed, 0, 50, 1, 10);
|
|
||||||
|
|
||||||
strokeWeight(thickness);
|
|
||||||
stroke(255, 200);
|
|
||||||
|
|
||||||
line(
|
|
||||||
trail[i - 1].x,
|
|
||||||
trail[i - 1].y,
|
|
||||||
trail[i].x,
|
|
||||||
trail[i].y
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====== [DRAW COCONUT] ======
|
|
||||||
function drawCoconut() {
|
function drawCoconut() {
|
||||||
fill(100, 255, 100);
|
imageMode(CENTER);
|
||||||
noStroke();
|
if (coconut.flying) {
|
||||||
ellipse(coconut.x, coconut.y, coconut.r * 2);
|
push();
|
||||||
|
translate(coconut.x, coconut.y);
|
||||||
|
rotate(coconut.flyRotation);
|
||||||
|
tint(255, coconut.flyAlpha);
|
||||||
|
image(coconutImgs[coconut.state], 0, 0, coconut.r * 2, coconut.r * 2);
|
||||||
|
noTint();
|
||||||
|
pop();
|
||||||
|
} else {
|
||||||
|
image(coconutImgs[coconut.state], coconut.x + coconut.shakeX, coconut.y, coconut.r * 2, coconut.r * 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== [SLICE CHECK] ======
|
function updateShake() {
|
||||||
function checkSlice() {
|
if (coconut.shakeTimer > 0) {
|
||||||
for (let i = 1; i < trail.length; i++) {
|
coconut.shakeX = sin(coconut.shakeTimer * 2.2) * coconut.shakeTimer * 0.55;
|
||||||
let hit = lineCircle(
|
coconut.shakeTimer--;
|
||||||
trail[i - 1],
|
} else {
|
||||||
trail[i],
|
coconut.shakeX = 0;
|
||||||
coconut.x,
|
}
|
||||||
coconut.y,
|
}
|
||||||
coconut.r
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hit && millis() - lastSliceTime > 100) {
|
function updateFlyAway() {
|
||||||
lastSliceTime = millis();
|
if (!coconut.flying) return;
|
||||||
|
|
||||||
let speed = velocities[i] || 10;
|
coconut.x += coconut.flyVX;
|
||||||
|
coconut.y += coconut.flyVY;
|
||||||
|
coconut.flyVY += 0.9;
|
||||||
|
coconut.flyRotation += coconut.flyRotSpeed;
|
||||||
|
coconut.flyAlpha -= 14;
|
||||||
|
|
||||||
console.log("SLICE!");
|
if (coconut.flyAlpha <= 0 || coconut.y > height + 100) {
|
||||||
|
if (score < 10) launchKakamoraToSlot(score);
|
||||||
|
score++;
|
||||||
|
coconut.x = width / 2;
|
||||||
|
coconut.y = height * 0.42;
|
||||||
|
coconut.state = 0;
|
||||||
|
coconut.flying = false;
|
||||||
|
coconut.flyAlpha = 255;
|
||||||
|
coconut.flyRotation = 0;
|
||||||
|
coconut.shakeX = 0;
|
||||||
|
coconut.shakeTimer = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
spawnParticles(coconut.x, coconut.y, speed);
|
// ====== TRAIL INPUT ======
|
||||||
flashAlpha = map(speed, 0, 50, 50, 150);
|
function mouseDragged() {
|
||||||
}
|
let pos = createVector(mouseX, mouseY);
|
||||||
|
trail.push(pos);
|
||||||
|
|
||||||
|
let dx = 0, dy = 0;
|
||||||
|
if (trail.length > 1) {
|
||||||
|
let prev = trail[trail.length - 2];
|
||||||
|
dx = pos.x - prev.x;
|
||||||
|
dy = pos.y - prev.y;
|
||||||
|
velocities.push(sqrt(dx * dx + dy * dy));
|
||||||
|
} else {
|
||||||
|
velocities.push(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 날아가는 중엔 드래그 무시
|
||||||
|
if (dist(mouseX, mouseY, coconut.x, coconut.y) < coconut.r && !coconut.flying) {
|
||||||
|
spawnParticles(mouseX, mouseY, dx, dy);
|
||||||
|
coconut.shakeTimer = max(coconut.shakeTimer, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trail.length > 15) trail.shift();
|
||||||
|
if (velocities.length > 15) velocities.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
function mouseReleased() {
|
||||||
|
// 드래그가 코코넛을 지나쳤는지 확인
|
||||||
|
let hits = trail.filter(p => dist(p.x, p.y, coconut.x, coconut.y) < coconut.r).length;
|
||||||
|
|
||||||
|
if (hits >= 2 && !coconut.flying) {
|
||||||
|
coconut.state++;
|
||||||
|
flashAlpha = 25;
|
||||||
|
if (coconut.state >= 4) {
|
||||||
|
// 4번째 드래그 → 날아가기 시작
|
||||||
|
coconut.flying = true;
|
||||||
|
coconut.flyVX = random([-1, 1]) * random(10, 16);
|
||||||
|
coconut.flyVY = random(-22, -16);
|
||||||
|
coconut.flyRotSpeed = random(-0.28, 0.28);
|
||||||
|
bgBlue = 255;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trail = [];
|
||||||
|
velocities = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== [COLLISION: LINE vs CIRCLE] ======
|
// ====== TRAIL DRAW ======
|
||||||
function lineCircle(p1, p2, cx, cy, r) {
|
function drawTrail() {
|
||||||
let ac = createVector(cx - p1.x, cy - p1.y);
|
noFill();
|
||||||
let ab = p5.Vector.sub(p2, p1);
|
for (let i = 1; i < trail.length; i++) {
|
||||||
|
let speed = velocities[i] || 0;
|
||||||
let ab2 = ab.magSq();
|
strokeWeight(map(speed, 0, 50, 1, 8));
|
||||||
let acab = ac.dot(ab);
|
stroke(255, 200);
|
||||||
|
line(trail[i-1].x, trail[i-1].y, trail[i].x, trail[i].y);
|
||||||
let t = constrain(acab / ab2, 0, 1);
|
}
|
||||||
|
|
||||||
let h = createVector(
|
|
||||||
p1.x + ab.x * t,
|
|
||||||
p1.y + ab.y * t
|
|
||||||
);
|
|
||||||
|
|
||||||
let distSq = (h.x - cx) ** 2 + (h.y - cy) ** 2;
|
|
||||||
|
|
||||||
return distSq <= r * r;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== [PARTICLE CLASS] ======
|
|
||||||
|
// ====== SCORE ======
|
||||||
|
function drawScore() {
|
||||||
|
noStroke();
|
||||||
|
textSize(22);
|
||||||
|
textAlign(LEFT, TOP);
|
||||||
|
|
||||||
|
fill(255, 255, 255, 220);
|
||||||
|
text(`coconut x${score}`, 20, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== SLOT POSITION HELPER ======
|
||||||
|
function getSlotPos(i) {
|
||||||
|
const size = constrain(floor(width * 0.055), 40, 62);
|
||||||
|
const gap = 6;
|
||||||
|
return {
|
||||||
|
x: width - 20 - size / 2 - (9 - i) * (size + gap),
|
||||||
|
y: 20 + size / 2,
|
||||||
|
size
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== KAKAMORA SLOTS ======
|
||||||
|
function drawKakamoraSlots() {
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
let { x, y, size } = getSlotPos(i);
|
||||||
|
let filled = i < score;
|
||||||
|
|
||||||
|
if (filled) {
|
||||||
|
drawingContext.save();
|
||||||
|
drawingContext.beginPath();
|
||||||
|
drawingContext.arc(x, y, size / 2, 0, Math.PI * 2);
|
||||||
|
drawingContext.clip();
|
||||||
|
imageMode(CORNER);
|
||||||
|
image(kakomoraImgs[i % 5], x - size / 2, y - size / 2, size, size);
|
||||||
|
drawingContext.restore();
|
||||||
|
|
||||||
|
noFill();
|
||||||
|
stroke(255);
|
||||||
|
strokeWeight(2.5);
|
||||||
|
ellipse(x, y, size, size);
|
||||||
|
} else {
|
||||||
|
fill(50, 50, 50, 190);
|
||||||
|
stroke(110, 110, 110);
|
||||||
|
strokeWeight(1.5);
|
||||||
|
ellipse(x, y, size, size);
|
||||||
|
|
||||||
|
noStroke();
|
||||||
|
fill(170, 170, 170);
|
||||||
|
textSize(size * 0.28);
|
||||||
|
textAlign(CENTER, CENTER);
|
||||||
|
text('?', x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== FLYING KAKAMORA ======
|
||||||
|
function launchKakamoraToSlot(slotIdx) {
|
||||||
|
let { x: tx, y: ty, size } = getSlotPos(slotIdx);
|
||||||
|
flyingKakamoraList.push({
|
||||||
|
startX: width / 2,
|
||||||
|
startY: height * 0.42,
|
||||||
|
targetX: tx,
|
||||||
|
targetY: ty,
|
||||||
|
t: 0,
|
||||||
|
imgIdx: slotIdx % 5,
|
||||||
|
size
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFlyingKakamoraList() {
|
||||||
|
for (let k of flyingKakamoraList) k.t += 0.032;
|
||||||
|
flyingKakamoraList = flyingKakamoraList.filter(k => k.t <= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawFlyingKakamoraList() {
|
||||||
|
for (let k of flyingKakamoraList) {
|
||||||
|
let ease = 1 - pow(1 - min(k.t, 1), 2);
|
||||||
|
let x = lerp(k.startX, k.targetX, ease);
|
||||||
|
let y = lerp(k.startY, k.targetY, ease) - sin(k.t * PI) * height * 0.3;
|
||||||
|
let sz = lerp(k.size * 1.8, k.size, ease);
|
||||||
|
|
||||||
|
drawingContext.save();
|
||||||
|
drawingContext.beginPath();
|
||||||
|
drawingContext.arc(x, y, sz / 2, 0, Math.PI * 2);
|
||||||
|
drawingContext.clip();
|
||||||
|
imageMode(CORNER);
|
||||||
|
image(kakomoraImgs[k.imgIdx], x - sz / 2, y - sz / 2, sz, sz);
|
||||||
|
drawingContext.restore();
|
||||||
|
|
||||||
|
noFill();
|
||||||
|
stroke(255, 200);
|
||||||
|
strokeWeight(2);
|
||||||
|
ellipse(x, y, sz, sz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== PARTICLES ======
|
||||||
class Particle {
|
class Particle {
|
||||||
constructor(x, y, speed) {
|
constructor(x, y, dx, dy) {
|
||||||
this.pos = createVector(x, y);
|
this.pos = createVector(x, y);
|
||||||
this.vel = p5.Vector.random2D().mult(random(1, speed * 0.3));
|
// 드래그 반대 방향 + 랜덤 퍼짐
|
||||||
this.life = 255;
|
let baseAngle = (dx !== 0 || dy !== 0)
|
||||||
}
|
? atan2(dy, dx) + PI + random(-0.7, 0.7)
|
||||||
|
: random(TWO_PI);
|
||||||
|
let spd = random(1.5, 5);
|
||||||
|
this.vel = createVector(cos(baseAngle) * spd, sin(baseAngle) * spd);
|
||||||
|
this.life = 255;
|
||||||
|
this.r = random(3, 7);
|
||||||
|
this.isHusk = random() > 0.4; // 60% 껍질(갈색), 40% 과육(크림)
|
||||||
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
this.pos.add(this.vel);
|
this.pos.add(this.vel);
|
||||||
this.life -= 5;
|
this.vel.y += 0.18; // 중력
|
||||||
}
|
this.vel.mult(0.93); // 마찰
|
||||||
|
this.life -= 7;
|
||||||
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
noStroke();
|
noStroke();
|
||||||
fill(200, 255, 200, this.life);
|
if (this.isHusk) fill(158, 108, 52, this.life);
|
||||||
ellipse(this.pos.x, this.pos.y, 5);
|
else fill(242, 224, 188, this.life);
|
||||||
}
|
ellipse(this.pos.x, this.pos.y, this.r, this.r * 0.55);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== [SPAWN PARTICLES] ======
|
function spawnParticles(x, y, dx, dy) {
|
||||||
function spawnParticles(x, y, speed) {
|
for (let i = 0; i < 5; i++) particles.push(new Particle(x, y, dx, dy));
|
||||||
let count = map(speed, 0, 50, 5, 25);
|
}
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
function drawParticles() {
|
||||||
particles.push(new Particle(x, y, speed));
|
for (let i = particles.length - 1; i >= 0; i--) {
|
||||||
}
|
particles[i].update();
|
||||||
}
|
particles[i].draw();
|
||||||
|
if (particles[i].life <= 0) particles.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== FLASH ======
|
||||||
|
function drawFlash() {
|
||||||
|
if (flashAlpha > 0) {
|
||||||
|
fill(255, flashAlpha);
|
||||||
|
rect(0, 0, width, height);
|
||||||
|
flashAlpha -= 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
111
src/Player.js
@@ -0,0 +1,111 @@
|
|||||||
|
class Player {
|
||||||
|
constructor() {
|
||||||
|
this.x = 130;
|
||||||
|
this.groundY = GROUND_Y;
|
||||||
|
this.speed = 3.5;
|
||||||
|
this.dir = 1; // 1=right, -1=left
|
||||||
|
this.moving = false;
|
||||||
|
this.stepT = 0;
|
||||||
|
|
||||||
|
// --- sprite animation state ---
|
||||||
|
this.anim = "idle";
|
||||||
|
this.prevAnim = "idle";
|
||||||
|
this.frameIdx = 0;
|
||||||
|
this.frameTimer = 0;
|
||||||
|
|
||||||
|
// frames per p5 tick each animation holds a frame
|
||||||
|
this.frameLengths = { idle: 40, walk: 8 };
|
||||||
|
this.totalFrames = { idle: 2, walk: 4 };
|
||||||
|
|
||||||
|
this.sprites = null; // set via setSprites() when images are ready
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call this once real PNGs are ready:
|
||||||
|
// player.setSprites({ idle: [img0, img1], walk: [img0, img1, img2, img3] })
|
||||||
|
setSprites(map) {
|
||||||
|
this.sprites = map;
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.moving = false;
|
||||||
|
if (keys[LEFT_ARROW]) { this.x -= this.speed; this.dir = -1; this.moving = true; }
|
||||||
|
if (keys[RIGHT_ARROW]) { this.x += this.speed; this.dir = 1; this.moving = true; }
|
||||||
|
this.x = constrain(this.x, 50, width - 50);
|
||||||
|
|
||||||
|
this.anim = this.moving ? "walk" : "idle";
|
||||||
|
|
||||||
|
// reset frame when animation changes
|
||||||
|
if (this.anim !== this.prevAnim) {
|
||||||
|
this.frameIdx = 0;
|
||||||
|
this.frameTimer = 0;
|
||||||
|
this.prevAnim = this.anim;
|
||||||
|
}
|
||||||
|
|
||||||
|
// advance frame
|
||||||
|
this.frameTimer++;
|
||||||
|
if (this.frameTimer >= this.frameLengths[this.anim]) {
|
||||||
|
this.frameTimer = 0;
|
||||||
|
this.frameIdx = (this.frameIdx + 1) % this.totalFrames[this.anim];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.moving) this.stepT += 0.22;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
let bobY = this.moving
|
||||||
|
? sin(this.stepT) * 3
|
||||||
|
: sin(frameCount * 0.05) * 1.5;
|
||||||
|
|
||||||
|
push();
|
||||||
|
translate(this.x, this.groundY + bobY);
|
||||||
|
scale(this.dir, 1);
|
||||||
|
|
||||||
|
if (this.sprites) {
|
||||||
|
this._drawSprite();
|
||||||
|
} else {
|
||||||
|
this._drawDummy();
|
||||||
|
}
|
||||||
|
|
||||||
|
pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawSprite() {
|
||||||
|
let img = this.sprites[this.anim][this.frameIdx];
|
||||||
|
imageMode(CENTER);
|
||||||
|
// adjust (0, -60, 80, 120) to match actual sprite dimensions
|
||||||
|
image(img, 0, -60, 80, 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
_drawDummy() {
|
||||||
|
rectMode(CENTER);
|
||||||
|
noStroke();
|
||||||
|
|
||||||
|
// shadow
|
||||||
|
fill(0, 0, 0, 25);
|
||||||
|
ellipse(0, 6, 42, 11);
|
||||||
|
|
||||||
|
// body placeholder — green=walk, blue=idle
|
||||||
|
fill(this.anim === "walk" ? color(80, 200, 120) : color(90, 150, 255));
|
||||||
|
rect(0, -38, 40, 64, 12);
|
||||||
|
|
||||||
|
// head
|
||||||
|
fill(255, 220, 170);
|
||||||
|
ellipse(0, -80, 48, 48);
|
||||||
|
|
||||||
|
// eyes
|
||||||
|
fill(40, 24, 14);
|
||||||
|
ellipse(-11, -82, 7, 8);
|
||||||
|
ellipse(11, -82, 7, 8);
|
||||||
|
|
||||||
|
// frame number — confirms animation is cycling
|
||||||
|
fill(255);
|
||||||
|
textAlign(CENTER, CENTER);
|
||||||
|
textSize(20);
|
||||||
|
text(this.frameIdx, 0, -38);
|
||||||
|
|
||||||
|
// anim label
|
||||||
|
textSize(9);
|
||||||
|
fill(255, 255, 255, 180);
|
||||||
|
text(this.anim, 0, -20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||