fuzzyworld

herd / files / src / entities / Sheep.ts

Sheep.ts

Blame: the commit that last changed each line · file · history

bce6efa4 2026-05-17 Pavel Safronov1import { Vec2 } from '../math/Vec2.ts'
2
3const WOOL_BLOBS: [number, number, number][] = [
3ccad7d4 2026-05-18 Pavel Safronov4 [0, 0, 6.5],
5 [-5, -4, 5.5],
6 [5, -4, 5.5],
7 [-6, 2, 5.0],
8 [6, 2, 5.0],
9 [0, -7, 4.5],
10 [0, 5, 4.5],
11]
12
13// Distinct but natural-looking tints — warm creams, cool whites, light grays.
14const NORMAL_WOOL_COLORS = [
15 '#ffffff', // pure white
16 '#f0e8d0', // warm cream
17 '#dce8f2', // cool blue-white
18 '#f0dde0', // blush
19 '#d8e8d8', // pale sage
20 '#e8e0d0', // linen
21 '#d8d8d8', // light gray
bce6efa4 2026-05-17 Pavel Safronov22]
23
24export interface SheepProps {
25 stubborn?: boolean
26 leader?: boolean
27 subFlock?: number
28 startFear?: number
29}
30
31export class Sheep {
32 pos: Vec2
33 vel: Vec2
3ccad7d4 2026-05-18 Pavel Safronov34 facing: number = -Math.PI / 2
bce6efa4 2026-05-17 Pavel Safronov35 fearLevel: number
36
37 readonly stubborn: boolean
38 readonly leader: boolean
39 readonly subFlock: number
40
3ccad7d4 2026-05-18 Pavel Safronov41 private readonly woolColor: string
42 private readonly headColor: string
43
bce6efa4 2026-05-17 Pavel Safronov44 constructor(x: number, y: number, props: SheepProps = {}) {
45 this.pos = new Vec2(x, y)
46 this.vel = Vec2.ZERO
47 this.fearLevel = props.startFear ?? 0
48 this.stubborn = props.stubborn ?? false
49 this.leader = props.leader ?? false
50 this.subFlock = props.subFlock ?? 0
3ccad7d4 2026-05-18 Pavel Safronov51
52 if (this.stubborn) {
53 this.woolColor = '#aaa'
54 this.headColor = '#888'
55 } else {
56 this.woolColor = NORMAL_WOOL_COLORS[Math.floor(Math.random() * NORMAL_WOOL_COLORS.length)]!
57 this.headColor = '#d4c5a0'
58 }
bce6efa4 2026-05-17 Pavel Safronov59 }
60
21437a01 2026-05-18 Pavel Safronov61 draw(ctx: CanvasRenderingContext2D, scale = 1) {
bce6efa4 2026-05-17 Pavel Safronov62 const { x, y } = this.pos
63
64 ctx.save()
65 ctx.translate(x, y)
66 ctx.rotate(this.facing)
67
3ccad7d4 2026-05-18 Pavel Safronov68 ctx.fillStyle = this.woolColor
bce6efa4 2026-05-17 Pavel Safronov69 for (const [dx, dy, r] of WOOL_BLOBS) {
70 ctx.beginPath()
21437a01 2026-05-18 Pavel Safronov71 ctx.arc(dx * scale, dy * scale, r * scale, 0, Math.PI * 2)
bce6efa4 2026-05-17 Pavel Safronov72 ctx.fill()
73 }
74
3ccad7d4 2026-05-18 Pavel Safronov75 ctx.fillStyle = this.headColor
bce6efa4 2026-05-17 Pavel Safronov76 ctx.beginPath()
21437a01 2026-05-18 Pavel Safronov77 ctx.arc(9 * scale, 0, 4.5 * scale, 0, Math.PI * 2)
bce6efa4 2026-05-17 Pavel Safronov78 ctx.fill()
79
2b484d78 2026-05-18 Pavel Safronov80 const eyeX = 9 * scale
81 const eyeY = 2.5 * scale
82 const pupilR = 1.2 * scale
83 const scleraR = pupilR + this.fearLevel * 1.5 * scale
84
85 ctx.fillStyle = '#fff'
86 for (const side of [-1, 1]) {
87 ctx.beginPath()
88 ctx.arc(eyeX, side * eyeY, scleraR, 0, Math.PI * 2)
89 ctx.fill()
90 }
bce6efa4 2026-05-17 Pavel Safronov91 ctx.fillStyle = '#1a1a1a'
92 for (const side of [-1, 1]) {
93 ctx.beginPath()
2b484d78 2026-05-18 Pavel Safronov94 ctx.arc(eyeX, side * eyeY, pupilR, 0, Math.PI * 2)
bce6efa4 2026-05-17 Pavel Safronov95 ctx.fill()
96 }
97
98 ctx.restore()
99
3ccad7d4 2026-05-18 Pavel Safronov100 // Leader star stays — it's the only remaining indicator drawn in world space
bce6efa4 2026-05-17 Pavel Safronov101 if (this.leader) {
3ccad7d4 2026-05-18 Pavel Safronov102 const indicatorY = y - 16 * scale
bce6efa4 2026-05-17 Pavel Safronov103 ctx.fillStyle = '#f5c518'
21437a01 2026-05-18 Pavel Safronov104 ctx.font = `${Math.round(10 * scale)}px sans-serif`
bce6efa4 2026-05-17 Pavel Safronov105 ctx.textAlign = 'center'
21437a01 2026-05-18 Pavel Safronov106 ctx.fillText('★', x, indicatorY)
bce6efa4 2026-05-17 Pavel Safronov107 }
108 }
109}