fuzzyworld

herd / files / src / entities / Dog.ts

Dog.ts

3.1 KB · history · blame

1import { Vec2 } from '../math/Vec2.ts'
2
3const ARRIVAL_THRESHOLD_SQ = 10 * 10
4
5export class Dog {
6 pos: Vec2
7 vel: Vec2 = Vec2.ZERO
8 target: Vec2 | null = null
9 speed: number
10 private facing: number = -Math.PI / 2
11
12 constructor(x: number, y: number, speed = 160) {
13 this.pos = new Vec2(x, y)
14 this.speed = speed
15 }
16
17 setTarget(worldX: number, worldY: number) {
18 this.target = new Vec2(worldX, worldY)
19 }
20
21 update(dt: number) {
22 if (!this.target) return
23 const diff = this.target.sub(this.pos)
24 if (diff.lengthSq() < ARRIVAL_THRESHOLD_SQ) {
25 this.target = null
26 this.vel = Vec2.ZERO
27 return
28 }
29 this.vel = diff.normalize().scale(this.speed)
30 this.facing = Math.atan2(this.vel.y, this.vel.x)
31 this.pos = this.pos.add(this.vel.scale(dt))
32 }
33
34 draw(ctx: CanvasRenderingContext2D, scale = 1) {
35 const { x, y } = this.pos
36 const facing = this.facing
37
38 ctx.save()
39 ctx.translate(x, y)
40 ctx.rotate(facing)
41 ctx.scale(scale, scale)
42
43 // tail — curves up from rear, drawn behind body
44 ctx.strokeStyle = '#111'
45 ctx.lineWidth = 3
46 ctx.lineCap = 'round'
47 ctx.beginPath()
48 ctx.moveTo(-10, 0)
49 ctx.quadraticCurveTo(-16, -5, -12, -13)
50 ctx.stroke()
51
52 // body — elongated black ellipse, border-collie proportions
53 ctx.fillStyle = '#111'
54 ctx.beginPath()
55 ctx.ellipse(0, 0, 11, 7, 0, 0, Math.PI * 2)
56 ctx.fill()
57
58 // white chest/belly saddle
59 ctx.fillStyle = '#eee8d8'
60 ctx.beginPath()
61 ctx.ellipse(-1, 0, 5.5, 4.5, 0, 0, Math.PI * 2)
62 ctx.fill()
63
64 // head at front (+x in local space)
65 ctx.fillStyle = '#111'
66 ctx.beginPath()
67 ctx.arc(13, 0, 5.5, 0, Math.PI * 2)
68 ctx.fill()
69
70 // white blaze down centre of face
71 ctx.fillStyle = '#eee8d8'
72 ctx.beginPath()
73 ctx.ellipse(14.5, 0, 2, 3.5, 0, 0, Math.PI * 2)
74 ctx.fill()
75
76 // floppy ears — drop slightly behind/below head centre
77 ctx.fillStyle = '#111'
78 for (const side of [-1, 1]) {
79 ctx.beginPath()
80 ctx.ellipse(10, side * 6, 3, 4.5, Math.PI * 0.25 * side, 0, Math.PI * 2)
81 ctx.fill()
82 }
83
84 // amber eyes
85 ctx.fillStyle = '#c8860a'
86 for (const side of [-1, 1]) {
87 ctx.beginPath()
88 ctx.arc(15, side * 2.2, 1.6, 0, Math.PI * 2)
89 ctx.fill()
90 }
91
92 // pupils
93 ctx.fillStyle = '#111'
94 for (const side of [-1, 1]) {
95 ctx.beginPath()
96 ctx.arc(15.4, side * 2.2, 0.7, 0, Math.PI * 2)
97 ctx.fill()
98 }
99
100 // nose
101 ctx.fillStyle = '#111'
102 ctx.beginPath()
103 ctx.ellipse(18.5, 0, 1.8, 1.3, 0, 0, Math.PI * 2)
104 ctx.fill()
105
106 ctx.restore()
107
108 // target indicator — world space (not scaled, always thin)
109 if (this.target) {
110 ctx.strokeStyle = 'rgba(255,255,255,0.25)'
111 ctx.lineWidth = 1
112 ctx.setLineDash([4, 4])
113 ctx.beginPath()
114 ctx.moveTo(x, y)
115 ctx.lineTo(this.target.x, this.target.y)
116 ctx.stroke()
117 ctx.setLineDash([])
118
119 ctx.strokeStyle = 'rgba(255,255,255,0.5)'
120 ctx.lineWidth = 1.5
121 ctx.beginPath()
122 ctx.arc(this.target.x, this.target.y, 5 * scale, 0, Math.PI * 2)
123 ctx.stroke()
124 }
125 }
126}