class Monster {
constructor(archetype) {
this.archetype = archetype;
// Position & size relative to canvas
this.x = width * 0.25;
this.baseY = height * 0.6;
this.size = min(width, height) * 0.25;
this.bobPhase = random(TWO_PI); // for subtle bobbing animation
this.baseColor = color(archetype.color);
}
display(state, mood) {
push();
translate(
this.x,
this.baseY + sin(frameCount * 0.03 + this.bobPhase) * 10
);
const s = this.size;
// Ground shadow
noStroke();
fill(0, 0, 0, 160);
ellipse(0, s * 0.45, s * 0.9, s * 0.28);
// Adjust color slightly based on mood
let bodyColor = this.baseColor;
if (state === "reacting") {
if (mood === "happy") {
bodyColor = lerpColor(this.baseColor, color(255), 0.25);
} else if (mood === "angry" || mood === "explode") {
bodyColor = lerpColor(this.baseColor, color(0), 0.45);
} else if (mood === "sad" || mood === "cry") {
bodyColor = lerpColor(this.baseColor, color("#3366ff"), 0.3);
}
}
// Slight outline to make them pop
stroke(0, 180);
strokeWeight(3);
fill(bodyColor);
// Scarier cartoon bodies per archetype
if (this.archetype.kind === "Vampire") {
// Tall, sharp cloak with ragged bottom
beginShape();
vertex(-s * 0.32, -s * 0.1);
vertex(s * 0.32, -s * 0.1);
vertex(s * 0.26, s * 0.45);
vertex(0, s * 0.58);
vertex(-s * 0.26, s * 0.45);
endShape(CLOSE);
// Dark inner cloak
fill(15, 0, 20);
beginShape();
vertex(-s * 0.2, -s * 0.08);
vertex(s * 0.2, -s * 0.08);
vertex(s * 0.16, s * 0.4);
vertex(0, s * 0.5);
vertex(-s * 0.16, s * 0.4);
endShape(CLOSE);
// High collar (bat-like)
fill(bodyColor);
triangle(-s * 0.32, -s * 0.1, -s * 0.05, -s * 0.55, 0, -s * 0.1);
triangle(s * 0.32, -s * 0.1, s * 0.05, -s * 0.55, 0, -s * 0.1);
// Head (pale)
fill(235);
ellipse(0, -s * 0.6, s * 0.5, s * 0.5);
// Pointy ears
fill(210);
triangle(-s * 0.22, -s * 0.7, -s * 0.06, -s * 0.62, -s * 0.18, -s * 0.5);
triangle(s * 0.22, -s * 0.7, s * 0.06, -s * 0.62, s * 0.18, -s * 0.5);
// Tiny fangs
fill(250);
noStroke();
triangle(-s * 0.07, -s * 0.47, -s * 0.03, -s * 0.47, -s * 0.05, -s * 0.38);
triangle(s * 0.03, -s * 0.47, s * 0.07, -s * 0.47, s * 0.05, -s * 0.38);
stroke(0, 180);
strokeWeight(3);
} else if (this.archetype.kind === "Dragon") {
// Bulkier, spiky body
beginShape();
vertex(-s * 0.35, -s * 0.1);
vertex(s * 0.35, -s * 0.1);
vertex(s * 0.3, s * 0.45);
vertex(-s * 0.3, s * 0.45);
endShape(CLOSE);
// Spiky back ridge
fill(255, 240, 160);
for (let i = -2; i <= 2; i++) {
const px = (i / 2) * s * 0.3;
triangle(px - s * 0.06, -s * 0.05, px, -s * 0.35, px + s * 0.06, -s * 0.05);
}
// Horns
fill(240, 230, 210);
triangle(-s * 0.18, -s * 0.45, -s * 0.06, -s * 0.75, -s * 0.01, -s * 0.45);
triangle(s * 0.18, -s * 0.45, s * 0.06, -s * 0.75, s * 0.01, -s * 0.45);
// Dragon head
fill(bodyColor);
rectMode(CENTER);
rect(0, -s * 0.45, s * 0.42, s * 0.32, s * 0.08);
// Jaw
rect(0, -s * 0.32, s * 0.3, s * 0.14, s * 0.04);
// Little teeth
noStroke();
fill(250);
for (let i = -2; i <= 2; i++) {
const px = (i / 2) * s * 0.24;
triangle(px - s * 0.02, -s * 0.26, px + s * 0.02, -s * 0.26, px, -s * 0.21);
}
stroke(0, 180);
strokeWeight(3);
// Simple wings
fill(25, 5, 0, 210);
beginShape();
vertex(-s * 0.4, -s * 0.05);
vertex(-s * 0.9, -s * 0.15);
vertex(-s * 0.6, s * 0.2);
vertex(-s * 0.35, s * 0.1);
endShape(CLOSE);
beginShape();
vertex(s * 0.4, -s * 0.05);
vertex(s * 0.9, -s * 0.15);
vertex(s * 0.6, s * 0.2);
vertex(s * 0.35, s * 0.1);
endShape(CLOSE);
} else {
// Slime blob - eerie outline
beginShape();
vertex(-s * 0.32, s * 0.2);
bezierVertex(-s * 0.5, 0, -s * 0.38, -s * 0.5, 0, -s * 0.55);
bezierVertex(s * 0.38, -s * 0.5, s * 0.5, 0, s * 0.32, s * 0.2);
bezierVertex(s * 0.25, s * 0.5, s * 0.05, s * 0.55, 0, s * 0.58);
bezierVertex(-s * 0.05, s * 0.55, -s * 0.25, s * 0.5, -s * 0.32, s * 0.2);
endShape(CLOSE);
// Inner darker slime core
noStroke();
fill(5, 50, 30, 160);
ellipse(0, 0, s * 0.45, s * 0.55);
stroke(0, 180);
strokeWeight(3);
}
// Eyes & mouth (expression)
this.drawFace(state, mood, s);
pop();
// Extra FX for big feelings
if (state === "reacting") {
this.drawReactionFX(mood);
}
}
drawFace(state, mood, s) {
const arch = this.archetype;
const isHappy = mood === "happy";
const isAngry = mood === "angry" || mood === "explode";
const isSad = mood === "sad" || mood === "cry";
// Glowing eye colors per monster
let eyeColor = color(255);
if (arch.kind === "Vampire") {
eyeColor = color(255, 80, 80);
} else if (arch.kind === "Dragon") {
eyeColor = color(255, 240, 150);
} else {
eyeColor = color(160, 255, 230);
}
if (isAngry) {
eyeColor = lerpColor(eyeColor, color(255, 0, 0), 0.4);
} else if (isSad) {
eyeColor = lerpColor(eyeColor, color(120, 170, 255), 0.4);
}
// Eyes
let eyeY = -s * 0.55;
let eyeOffsetX = s * 0.12;
if (arch.kind === "Dragon") eyeY = -s * 0.42;
if (arch.kind === "Slime") eyeY = -s * 0.18;
stroke(0);
strokeWeight(3);
if (isHappy) {
// happy glowing arcs
noFill();
stroke(eyeColor);
strokeWeight(4);
arc(-eyeOffsetX, eyeY, s * 0.15, s * 0.1, PI, TWO_PI);
arc(eyeOffsetX, eyeY, s * 0.15, s * 0.1, PI, TWO_PI);
noStroke();
} else if (isAngry) {
// Sharp, angled eyes
stroke(eyeColor);
strokeWeight(5);
line(
-eyeOffsetX - s * 0.07,
eyeY - s * 0.03,
-eyeOffsetX + s * 0.04,
eyeY + s * 0.03
);
line(
eyeOffsetX - s * 0.04,
eyeY + s * 0.03,
eyeOffsetX + s * 0.07,
eyeY - s * 0.03
);
// Small glowing pupils
noStroke();
fill(eyeColor);
ellipse(-eyeOffsetX, eyeY + s * 0.03, s * 0.05, s * 0.05);
ellipse(eyeOffsetX, eyeY + s * 0.03, s * 0.05, s * 0.05);
} else if (isSad) {
// Droopy glowing eyes
noStroke();
fill(eyeColor);
ellipse(-eyeOffsetX, eyeY, s * 0.06, s * 0.08);
ellipse(eyeOffsetX, eyeY, s * 0.06, s * 0.08);
} else {
// Neutral glow
noStroke();
fill(eyeColor);
ellipse(-eyeOffsetX, eyeY, s * 0.06, s * 0.06);
ellipse(eyeOffsetX, eyeY, s * 0.06, s * 0.06);
}
// Extra third eye for slime
if (arch.kind === "Slime") {
fill(eyeColor);
ellipse(0, eyeY - s * 0.03, s * 0.04, s * 0.04);
}
// Mouth
let mouthY = eyeY + s * 0.16;
if (arch.kind === "Dragon") mouthY = eyeY + s * 0.16;
if (arch.kind === "Slime") mouthY = eyeY + s * 0.12;
stroke(0);
strokeWeight(3);
noFill();
if (isHappy) {
// Big, sharp grin
arc(0, mouthY, s * 0.22, s * 0.16, 0, PI);
// Tiny teeth inside grin
noStroke();
fill(250);
for (let i = -2; i <= 2; i++) {
const px = (i / 2) * s * 0.16;
triangle(px - s * 0.01, mouthY, px + s * 0.01, mouthY, px, mouthY - s * 0.04);
}
} else if (isAngry) {
// Jagged mouth
beginShape();
const w = s * 0.22;
const segs = 5;
for (let i = 0; i <= segs; i++) {
const t = i / segs;
const x = lerp(-w / 2, w / 2, t);
const y = mouthY + (i % 2 === 0 ? -s * 0.02 : s * 0.05);
vertex(x, y);
}
endShape();
} else if (isSad) {
// Upside-down curve
arc(0, mouthY + s * 0.07, s * 0.18, s * 0.13, PI, TWO_PI);
} else {
// Neutral
line(-s * 0.1, mouthY, s * 0.1, mouthY);
}
}
drawReactionFX(mood) {
if (mood === "explode") {
// Radiating fiery rings + spikes
push();
translate(this.x, this.baseY);
noFill();
const baseR = this.size * 0.65;
const t = frameCount * 0.15;
// Glowing rings
for (let i = 0; i < 3; i++) {
const r = baseR + sin(t + i) * this.size * 0.18;
stroke(255, 150 + i * 30, 80, 200 - i * 40);
strokeWeight(4 - i);
ellipse(0, 0, r, r);
}
// Fiery spikes
noStroke();
fill(255, 200, 120, 190);
const spikes = 10;
for (let i = 0; i < spikes; i++) {
const ang = (TWO_PI * i) / spikes + t * 0.4;
const r1 = baseR * 0.6;
const r2 = baseR * 0.95;
const x1 = cos(ang) * r1;
const y1 = sin(ang) * r1;
const x2 = cos(ang + 0.1) * r2;
const y2 = sin(ang + 0.1) * r2;
const x3 = cos(ang - 0.1) * r2;
const y3 = sin(ang - 0.1) * r2;
triangle(x1, y1, x2, y2, x3, y3);
}
pop();
} else if (mood === "cry") {
// Falling tears
push();
translate(this.x, this.baseY);
noStroke();
fill(100, 180, 255, 200);
for (let i = 0; i < 4; i++) {
const offsetX = (i < 2 ? -1 : 1) * this.size * 0.12;
const phase = (i % 2) * PI;
const y =
sin(frameCount * 0.12 + phase) * this.size * 0.1 +
this.size * 0.25;
ellipse(offsetX, y, this.size * 0.06, this.size * 0.12);
}
pop();
}
}
}