JavaScript · 4934 bytes Raw Blame History
1 // Duck module - Doug's behavior and rendering (p5.js version)
2
3 export class Duck {
4 constructor(p, x, y, pond) {
5 this.p = p
6 this.x = x
7 this.y = y
8 this.pond = pond
9
10 this.targetX = x
11 this.targetY = y
12 this.speed = 0.8
13 this.idleSpeed = 0.3
14
15 this.state = 'idle'
16 this.waitTimer = 0
17 this.waitDuration = 0
18
19 this.wobble = 0
20 this.headBob = 0
21 this.direction = 1
22
23 this.idleTimer = 0
24 this.nextIdleMove = this.randomIdleTime()
25 }
26
27 randomIdleTime() {
28 return this.p.random(120, 300)
29 }
30
31 setTarget(x, y, isFood = false) {
32 if (isFood) {
33 this.state = 'waiting'
34 this.waitTimer = 0
35 this.waitDuration = this.p.random(30, 60)
36 this.pendingTargetX = x
37 this.pendingTargetY = y
38 } else {
39 this.targetX = x
40 this.targetY = y
41 }
42 }
43
44 pickIdleTarget() {
45 const angle = this.p.random(this.p.TWO_PI)
46 const radiusX = this.p.random(0.2, 0.8) * (this.pond.width / 2)
47 const radiusY = this.p.random(0.2, 0.8) * (this.pond.height / 2)
48 this.targetX = this.pond.x + Math.cos(angle) * radiusX
49 this.targetY = this.pond.y + Math.sin(angle) * radiusY
50 }
51
52 update(breadBits) {
53 const p = this.p
54
55 let closestBread = null
56 let closestDist = Infinity
57 for (const bread of breadBits) {
58 if (!bread.eaten) {
59 const d = p.dist(this.x, this.y, bread.x, bread.y)
60 if (d < closestDist) {
61 closestDist = d
62 closestBread = bread
63 }
64 }
65 }
66
67 if (closestBread && this.state === 'idle') {
68 this.setTarget(closestBread.x, closestBread.y, true)
69 }
70
71 if (this.state === 'waiting') {
72 this.waitTimer++
73 this.headBob = Math.sin(this.waitTimer * 0.3) * 3
74 if (this.waitTimer >= this.waitDuration) {
75 this.state = 'swimming'
76 this.targetX = this.pendingTargetX
77 this.targetY = this.pendingTargetY
78 }
79 }
80
81 const dx = this.targetX - this.x
82 const dy = this.targetY - this.y
83 const dist = Math.sqrt(dx * dx + dy * dy)
84
85 if (dist > 3) {
86 const currentSpeed = this.state === 'swimming' ? this.speed : this.idleSpeed
87 const moveX = (dx / dist) * currentSpeed
88 const moveY = (dy / dist) * currentSpeed
89 this.x += moveX
90 this.y += moveY
91
92 if (Math.abs(dx) > 0.1) {
93 this.direction = dx > 0 ? 1 : -1
94 }
95
96 this.wobble += 0.15
97 } else {
98 if (this.state === 'swimming') {
99 this.state = 'idle'
100 for (const bread of breadBits) {
101 if (!bread.eaten && p.dist(this.x, this.y, bread.x, bread.y) < 20) {
102 bread.eaten = true
103 }
104 }
105 }
106 }
107
108 if (this.state === 'idle' && !closestBread) {
109 this.idleTimer++
110 if (this.idleTimer >= this.nextIdleMove) {
111 this.pickIdleTarget()
112 this.idleTimer = 0
113 this.nextIdleMove = this.randomIdleTime()
114 }
115 }
116
117 this.headBob = Math.sin(p.frameCount * 0.05) * 2
118 }
119
120 draw() {
121 const p = this.p
122
123 p.push()
124 p.translate(this.x, this.y)
125 p.scale(this.direction, 1)
126
127 const wobbleAmount = Math.sin(this.wobble) * 3
128 const outlineWeight = 2.5
129 const outlineColor = p.color(25, 20, 15)
130
131 // Water ripple
132 p.noFill()
133 p.stroke(255, 255, 255, 60)
134 p.strokeWeight(2)
135 p.ellipse(0, 12, 50, 16)
136
137 // Shadow
138 p.noStroke()
139 p.fill(40, 80, 110, 80)
140 p.ellipse(0, 10, 40, 12)
141
142 // Tail
143 p.stroke(outlineColor)
144 p.strokeWeight(outlineWeight)
145 p.fill(255, 200, 60)
146 p.push()
147 p.translate(-18, wobbleAmount - 2)
148 p.rotate(p.radians(-20))
149 p.ellipse(0, 0, 14, 8)
150 p.pop()
151
152 // Body
153 p.stroke(outlineColor)
154 p.strokeWeight(outlineWeight)
155 p.fill(255, 220, 80)
156 p.ellipse(0, wobbleAmount, 38, 30)
157
158 // Body highlight
159 p.noStroke()
160 p.fill(255, 240, 140)
161 p.ellipse(-5, wobbleAmount - 5, 20, 14)
162
163 // Body shadow
164 p.fill(230, 180, 50)
165 p.ellipse(5, wobbleAmount + 8, 25, 10)
166
167 // Wing
168 p.stroke(outlineColor)
169 p.strokeWeight(outlineWeight)
170 p.fill(245, 200, 65)
171 p.ellipse(3, wobbleAmount + 2, 22, 18)
172
173 // Head
174 const headY = -14 + this.headBob + wobbleAmount * 0.5
175 p.stroke(outlineColor)
176 p.strokeWeight(outlineWeight)
177 p.fill(255, 220, 80)
178 p.ellipse(14, headY, 26, 24)
179
180 // Head highlight
181 p.noStroke()
182 p.fill(255, 240, 140)
183 p.ellipse(10, headY - 5, 14, 10)
184
185 // Beak
186 p.stroke(outlineColor)
187 p.strokeWeight(outlineWeight)
188 p.fill(255, 160, 40)
189 p.push()
190 p.translate(26, headY + 2)
191 p.beginShape()
192 p.vertex(0, -5)
193 p.vertex(14, 0)
194 p.vertex(0, 5)
195 p.endShape(p.CLOSE)
196 p.pop()
197
198 // Eye
199 p.stroke(outlineColor)
200 p.strokeWeight(1.5)
201 p.fill(255)
202 p.ellipse(20, headY - 2, 10, 10)
203
204 // Pupil
205 p.noStroke()
206 p.fill(25, 20, 15)
207 p.ellipse(21, headY - 1, 5, 6)
208
209 // Eye shine
210 p.fill(255)
211 p.ellipse(22, headY - 3, 3, 3)
212
213 p.pop()
214 }
215 }