fuzzyworld

herd / commit

Add Sheep entity with fluffy wool and smooth rotation

bce6efa44d9b3a567ac478d14a1f65ab90796ac4 · Pavel Safronov · 2026-05-16 22:04:16 -0700 · parents 369d8083f7a5

Add Sheep entity with fluffy wool and smooth rotation

Body is overlapping same-colour puffs (no dark blobs) so the silhouette
reads as woolly. Head, wool, and eyes are all drawn in local space so the
whole animal rotates together — no orbiting head. Fear level shifts colour
from white toward orange-red.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
 src/entities/Sheep.ts | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 99 insertions(+)

diff --git a/src/entities/Sheep.ts b/src/entities/Sheep.ts
new file mode 100644
index 0000000..b80e8cf
--- /dev/null
+++ b/src/entities/Sheep.ts
@@ -0,0 +1,99 @@
+import { Vec2 } from '../math/Vec2.ts'
+
+// Overlapping white puffs that form the wool body.
+// [dx, dy, radius] — all drawn in the same wool colour so the lumpy
+// silhouette reads as fluffiness rather than dark spots.
+const WOOL_BLOBS: [number, number, number][] = [
+  [ 0,   0,  6.5],  // centre
+  [-5,  -4,  5.5],  // upper-left
+  [ 5,  -4,  5.5],  // upper-right
+  [-6,   2,  5.0],  // left
+  [ 6,   2,  5.0],  // right
+  [ 0,  -7,  4.5],  // top
+  [ 0,   5,  4.5],  // bottom
+]
+
+export interface SheepProps {
+  stubborn?: boolean
+  leader?: boolean
+  subFlock?: number
+  startFear?: number
+}
+
+export class Sheep {
+  pos: Vec2
+  vel: Vec2
+  facing: number = -Math.PI / 2 // radians, starts facing up
+  fearLevel: number
+
+  readonly stubborn: boolean
+  readonly leader: boolean
+  readonly subFlock: number
+
+  constructor(x: number, y: number, props: SheepProps = {}) {
+    this.pos = new Vec2(x, y)
+    this.vel = Vec2.ZERO
+    this.fearLevel = props.startFear ?? 0
+    this.stubborn = props.stubborn ?? false
+    this.leader = props.leader ?? false
+    this.subFlock = props.subFlock ?? 0
+  }
+
+  draw(ctx: CanvasRenderingContext2D, dark = false) {
+    if (dark) return // caller handles visibility in dark mode
+
+    const { x, y } = this.pos
+
+    // fear tint: white → orange-red
+    const g = Math.round(255 - this.fearLevel * 160)
+    const b = Math.round(255 - this.fearLevel * 255)
+    const bodyColor = `rgb(255,${g},${b})`
+
+    // Draw wool + head in local space so the whole body rotates together.
+    ctx.save()
+    ctx.translate(x, y)
+    ctx.rotate(this.facing)
+
+    // Fluffy wool body — puffs in local space, facing = +x direction
+    ctx.fillStyle = bodyColor
+    for (const [dx, dy, r] of WOOL_BLOBS) {
+      ctx.beginPath()
+      ctx.arc(dx, dy, r, 0, Math.PI * 2)
+      ctx.fill()
+    }
+
+    // Head always at the front (+x side in local space)
+    ctx.fillStyle = '#d4c5a0'
+    ctx.beginPath()
+    ctx.arc(9, 0, 4.5, 0, Math.PI * 2)
+    ctx.fill()
+
+    // Eyes on either side of the head centre
+    ctx.fillStyle = '#1a1a1a'
+    for (const side of [-1, 1]) {
+      ctx.beginPath()
+      ctx.arc(9, side * 2.5, 1.2, 0, Math.PI * 2)
+      ctx.fill()
+    }
+
+    ctx.restore()
+
+    // Indicators stay upright in world space regardless of facing
+    if (this.leader) {
+      ctx.fillStyle = '#f5c518'
+      ctx.font = '10px sans-serif'
+      ctx.textAlign = 'center'
+      ctx.fillText('★', x, y - 16)
+    }
+
+    if (this.stubborn) {
+      ctx.strokeStyle = '#e05'
+      ctx.lineWidth = 1.5
+      const s = 4
+      ctx.beginPath()
+      ctx.moveTo(x - s, y - 16 - s); ctx.lineTo(x + s, y - 16 + s)
+      ctx.moveTo(x + s, y - 16 - s); ctx.lineTo(x - s, y - 16 + s)
+      ctx.stroke()
+    }
+  }
+}