draw() {
if (this.hp <= 0) return;
push();
let bodyOffset = -15; // Lifts the visual body up so the long legs fit in the bounding box
let cx = this.x + this.w/2;
let cy = this.y + this.h/2; // Center of physics bounding box
let gunDist = 55; // Super long arms
let shieldDist = 35;
let gunHX = this.aimVec.x * gunDist;
let gunHY = bodyOffset + this.aimVec.y * gunDist;
let shieldX = -this.aimVec.x * shieldDist;
let shieldY = bodyOffset - this.aimVec.y * shieldDist + 6;
// Laser sight (spawns exactly from the gun barrel tip)
if (this.hp > 0 && !this.blockingFrames) {
let gunX = cx + gunHX;
let gunY = cy + gunHY;
let endX = gunX + this.aimVec.x * 1200;
let endY = gunY + this.aimVec.y * 1200;
stroke(this.color.levels[0], this.color.levels[1], this.color.levels[2], 30);
strokeWeight(2);
line(gunX, gunY, endX, endY);
}
if (this.hasOrbitals) {
for(let i=0; i<2; i++) {
let ox = cx + cos(this.orbitalAngle + i*PI) * 45;
let oy = cy + bodyOffset + sin(this.orbitalAngle + i*PI) * 45;
fill(this.color); noStroke();
circle(ox, oy, 12);
fill(255); circle(ox, oy, 4);
}
}
let vMag = dist(0, 0, this.vx, this.vy);
let stretchAngle = atan2(this.vy, this.vx);
let stretchFactor = constrain(vMag * 0.04, 0, 0.4);
translate(cx, cy);
// ==========================================
// NOODLE LEGS (Animated scurrying)
// ==========================================
// The player's physics box bottom is at y=15.
// We lift the legs up in a cycle so they don't clip down through the ground!
let walkPhase = frameCount * 0.4;
let lift1 = (this.onGround && abs(this.vx) > 0.5) ? max(0, sin(walkPhase)) * 8 : 0;
let lift2 = (this.onGround && abs(this.vx) > 0.5) ? max(0, -sin(walkPhase)) * 8 : 0;
let foot1X = -8;
let foot1Y = 10 - lift1; // 10 instead of 15 to account for the 5px circle radius
let foot2X = 8;
let foot2Y = 10 - lift2;
let cR = red(this.color) * 0.7;
let cG = green(this.color) * 0.7;
let cB = blue(this.color) * 0.7;
// Draw lines connecting body to feet
stroke(cR, cG, cB, this.alpha);
strokeWeight(6);
strokeCap(ROUND);
line(0, bodyOffset + 5, foot1X, foot1Y);
line(0, bodyOffset + 5, foot2X, foot2Y);
// Draw cute round feet
noStroke();
fill(cR, cG, cB, this.alpha);
circle(foot1X, foot1Y, 10);
circle(foot2X, foot2Y, 10);
// ==========================================
// NOODLE ARMS
// ==========================================
stroke(this.color.levels[0]*0.8, this.color.levels[1]*0.8, this.color.levels[2]*0.8, this.alpha);
strokeWeight(5);
line(0, bodyOffset + 2, shieldX, shieldY);
line(0, bodyOffset + 2, gunHX, gunHY);
// Block shield renders behind the main body
if (this.blockingFrames > 0) {
noFill();
stroke(255);
strokeWeight(4);
let progress = 1 - (this.blockingFrames / this.blockDuration);
let shieldSize = this.w * 2.5 + (progress * 40); // Larger shield to cover long arms
circle(0, bodyOffset, shieldSize);
}
// ==========================================
// SQUISHY BODY
// ==========================================
push();
translate(0, bodyOffset); // Render body higher up
rotate(stretchAngle);
this.color.setAlpha(this.alpha);
fill(this.color);
noStroke();
ellipse(0, 0, this.w * (1.1 + stretchFactor), this.h * (1.1 - stretchFactor * 0.5));
this.color.setAlpha(255);
rotate(-stretchAngle);
// TWO EYES
let eyeOffsetX = this.aimVec.x * (this.w * 0.2);
let eyeOffsetY = this.aimVec.y * (this.h * 0.2);
let eyeW = this.w * 0.35;
fill(255, this.alpha);
circle(eyeOffsetX - 6, eyeOffsetY, eyeW);
circle(eyeOffsetX + 6, eyeOffsetY, eyeW);
fill(20, 20, 24, this.alpha);
circle(eyeOffsetX - 6 + this.aimVec.x * 2, eyeOffsetY + this.aimVec.y * 2, eyeW * 0.4);
circle(eyeOffsetX + 6 + this.aimVec.x * 2, eyeOffsetY + this.aimVec.y * 2, eyeW * 0.4);
pop();
// ==========================================
// SHIELD HAND (With Pie Cooldown)
// ==========================================
push();
translate(shieldX, shieldY);
fill(40, 40, 45, this.alpha);
noStroke();
circle(0, 0, 18);
if (this.blockTimer > 0) {
fill(255, 100);
let p = 1 - (this.blockTimer / this.maxBlockCooldown);
arc(0, 0, 18, 18, -HALF_PI, -HALF_PI + TWO_PI * p, PIE);
} else {
fill(255, this.alpha);
circle(0, 0, 18);
}
if (this.blockingFrames > 0) {
fill(255, 150);
circle(0, 0, 28);
}
pop();
// ==========================================
// GUN & RIGHT HAND
// ==========================================
push();
let aimAngle = atan2(this.aimVec.y, this.aimVec.x);
translate(gunHX, gunHY);
rotate(aimAngle);
// Right Hand physically gripping the gun base
fill(this.color.levels[0], this.color.levels[1], this.color.levels[2], this.alpha);
noStroke();
circle(0, 0, 14);
if (abs(aimAngle) > HALF_PI) {
scale(1, -1);
}
this.color.setAlpha(this.alpha);
fill(40, 40, 45, this.alpha);
stroke(255, 255, 255, this.alpha);
strokeWeight(2);
let gunLen = 22 + min(this.maxAmmo * 2, 35);
let t = 8;
let h = 16;
// L-Shaped Poly Gun
beginShape();
vertex(-t/2, 0);
vertex(t/2, 0);
vertex(t/2, -h + t);
vertex(gunLen, -h + t);
vertex(gunLen, -h);
vertex(-t/2, -h);
endShape(CLOSE);
noStroke();
let ammoYOffset = -h - 6;
let barrelCenter = (gunLen - t/2) / 2;
let space = min((gunLen + t/2) / this.maxAmmo, 7);
let startX = barrelCenter - ((this.maxAmmo - 1) * space) / 2;
if (this.reloadTimer > 0) {
let progress = 1 - (this.reloadTimer / this.reloadTime);
fill(255, 255, 255, 220);
rectMode(CORNER);
let barW = (this.maxAmmo - 1) * space + 6;
rect(startX - 3, ammoYOffset - 3, barW * progress, 6, 2);
} else {
for(let i=0; i < this.maxAmmo; i++) {
if (i < this.ammo) fill(255, 210, 50, this.alpha);
else fill(80, this.alpha);
circle(startX + i * space, ammoYOffset, 4);
}
}
this.color.setAlpha(255);
pop();
pop(); // Revert center translate
// Health Bar
noStroke();
fill(255, 0, 0);
// Draw health bar higher up to clear the long body
rect(this.x - 5, this.y - 45, this.w + 10, 6, 2);
fill(this.poisonTimer > 0 ? color(100, 255, 100) : color(0, 255, 0));
let hpW = map(max(0, this.hp), 0, this.maxHp, 0, this.w + 10);
rect(this.x - 5, this.y - 45, hpW, 6, 2);
}