herd / commit
Improve sheep visual differentiation and pen behaviour
src/ai/Boids.ts | 4 +++- src/entities/Sheep.ts | 65 +++++++++++++++++++++++++++------------------------ 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/ai/Boids.ts b/src/ai/Boids.ts index b2518d0..9b88a99 100644 --- a/src/ai/Boids.ts +++ b/src/ai/Boids.ts @@ -156,8 +156,10 @@ export function updateBoids( } // ── dog flee (linear gradient across flee radius) ───────────────────── + // Sheep inside the pen ignore the dog — they've been herded and should + // stay calm. Fear decays normally so their eyes return to normal. const dogDSq = s.pos.distanceSqTo(dog.pos) - if (dogDSq < fleeRadiusSq) { + if (!pen.contains(s.pos) && dogDSq < fleeRadiusSq) { const dogD = Math.sqrt(dogDSq) const gradient = 1 - dogD / fleeRadius // 1 at dog, 0 at flee radius edge steering = steering.add( diff --git a/src/entities/Sheep.ts b/src/entities/Sheep.ts index 6417350..0180afd 100644 --- a/src/entities/Sheep.ts +++ b/src/entities/Sheep.ts @@ -1,16 +1,24 @@ 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 + [0, 0, 6.5], + [-5, -4, 5.5], + [5, -4, 5.5], + [-6, 2, 5.0], + [6, 2, 5.0], + [0, -7, 4.5], + [0, 5, 4.5], +] + +// Distinct but natural-looking tints — warm creams, cool whites, light grays. +const NORMAL_WOOL_COLORS = [ + '#ffffff', // pure white + '#f0e8d0', // warm cream + '#dce8f2', // cool blue-white + '#f0dde0', // blush + '#d8e8d8', // pale sage + '#e8e0d0', // linen + '#d8d8d8', // light gray ] export interface SheepProps { @@ -23,13 +31,16 @@ export interface SheepProps { export class Sheep { pos: Vec2 vel: Vec2 - facing: number = -Math.PI / 2 // radians, starts facing up + facing: number = -Math.PI / 2 fearLevel: number readonly stubborn: boolean readonly leader: boolean readonly subFlock: number + private readonly woolColor: string + private readonly headColor: string + constructor(x: number, y: number, props: SheepProps = {}) { this.pos = new Vec2(x, y) this.vel = Vec2.ZERO @@ -37,33 +48,35 @@ export class Sheep { this.stubborn = props.stubborn ?? false this.leader = props.leader ?? false this.subFlock = props.subFlock ?? 0 + + if (this.stubborn) { + this.woolColor = '#aaa' + this.headColor = '#888' + } else { + this.woolColor = NORMAL_WOOL_COLORS[Math.floor(Math.random() * NORMAL_WOOL_COLORS.length)]! + this.headColor = '#d4c5a0' + } } draw(ctx: CanvasRenderingContext2D, scale = 1) { const { x, y } = this.pos - // 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 = '#fff' + ctx.fillStyle = this.woolColor for (const [dx, dy, r] of WOOL_BLOBS) { ctx.beginPath() ctx.arc(dx * scale, dy * scale, r * scale, 0, Math.PI * 2) ctx.fill() } - // Head always at the front (+x side in local space) - ctx.fillStyle = '#d4c5a0' + ctx.fillStyle = this.headColor ctx.beginPath() ctx.arc(9 * scale, 0, 4.5 * scale, 0, Math.PI * 2) ctx.fill() - // Eyes: white sclera grows with fear, dark pupil stays fixed. - // At fearLevel=0 the sclera equals the pupil and is fully covered — plain dot. - // As fear rises a white ring appears, giving a wide-eyed look. const eyeX = 9 * scale const eyeY = 2.5 * scale const pupilR = 1.2 * scale @@ -84,23 +97,13 @@ export class Sheep { ctx.restore() - // Indicators stay upright in world space regardless of facing - const indicatorY = y - 16 * scale + // Leader star stays — it's the only remaining indicator drawn in world space if (this.leader) { + const indicatorY = y - 16 * scale ctx.fillStyle = '#f5c518' ctx.font = `${Math.round(10 * scale)}px sans-serif` ctx.textAlign = 'center' ctx.fillText('★', x, indicatorY) } - - if (this.stubborn) { - ctx.strokeStyle = '#e05' - ctx.lineWidth = 1.5 * scale - const s = 4 * scale - ctx.beginPath() - ctx.moveTo(x - s, indicatorY - s); ctx.lineTo(x + s, indicatorY + s) - ctx.moveTo(x + s, indicatorY - s); ctx.lineTo(x - s, indicatorY + s) - ctx.stroke() - } } }