keeper.gno
24.86 Kb · 991 lines
1package physics
2
3// Keeper phases.
4const (
5 phaseReady = iota
6 phasePush
7 phaseAir
8 phaseGround
9 phaseRecover
10)
11
12// Body parts, in the order the browser builds them. Both torso capsules are
13// one part ("body"), as in the browser, where parts are keyed by name.
14const (
15 partLeftLeg = iota
16 partBody
17 partLeftArm
18 partLeftGlove
19 partRightLeg
20 partRightArm
21 partRightGlove
22 partHead
23 nParts
24)
25
26// PartNames are the browser's names for the parts.
27var PartNames = [nParts]string{"left leg", "body", "left arm", "left glove", "right leg", "right arm", "right glove", "head"}
28
29const (
30 kindLeg = iota
31 kindTorso
32 kindArm
33 kindGlove
34 kindHead
35)
36
37const nCaps = 9
38
39type capsule struct {
40 a, b vec
41 r float64
42 hit float64
43 kind int
44 part int
45}
46
47type caps [nCaps]capsule
48
49type plant struct {
50 on bool
51 side float64
52 x, y, z float64
53}
54
55type lean2 struct{ s, c float64 }
56
57// Commit is one move the keeper made: when, how fast sideways, how high.
58type Commit struct {
59 Step int
60 T0 float64
61 VX float64
62 Jump float64
63 Star bool
64 Read bool
65}
66
67type misread struct {
68 on bool
69 x, y, vx, jump float64
70}
71
72type keeper struct {
73 mis misread
74 phase int
75 pushT float64
76 groundT float64
77 pos, vel vec
78 lean lean2
79 arms armPair
80 launchVel vec
81 leanTarget lean2
82 armsTarget armPair
83 plant plant
84 plantUntil float64
85 commits []Commit
86 holding bool
87 read pred // its latest read of where the ball will cross
88 nextRead int // the step of its next read
89 seen ball // the ball at a read not worked out yet (see keeperStep)
90 seenZ float64 // and the keeper's z, and the time, at that read
91 seenT float64
92 pending bool
93 clr float64
94 clrS, clrC float64 // the lean clr was computed for
95 clrOK bool
96}
97
98type pred struct {
99 ok bool
100 x, y float64
101 tau float64
102}
103
104func (e *engine) keeperInit(m misread) keeper {
105 return keeper{
106 mis: m,
107 phase: phaseReady,
108 pos: vec{0, e.standHipY, goalZ + kLineOffset},
109 lean: lean2{0, 1},
110 arms: armsReady,
111 leanTarget: lean2{0, 1},
112 armsTarget: armsReady,
113 nextRead: 1,
114 }
115}
116
117func margin(kind int) float64 {
118 switch kind {
119 case kindGlove:
120 return kMarginGlove
121 case kindArm:
122 return kMarginArm
123 case kindTorso:
124 return kMarginTorso
125 case kindLeg:
126 return kMarginLeg
127 }
128 return kMarginHead
129}
130
131// Fixed offsets of the body points, already scaled: what the browser's w()
132// computes as x *= S, y *= S for each constant point, per side.
133type sideK struct {
134 side float64
135 sx float64 // shoulder x, unscaled
136 legX, legY, footX, footY float64
137 torX, torAY, torBY float64
138 armAX, armAY float64
139 legR, torR, armR, gloveR float64
140 legH, torH, armH, gloveH float64
141 legPart, armPart, glovePart int
142}
143
144func (e *engine) initBody() {
145 S := e.scale
146 zero := 0.0
147 for i, side := range [2]float64{-1, 1} {
148 k := sideK{side: side, sx: kShoulderX * side}
149 k.legX, k.legY = 0.11*side*S, zero*S
150 k.footX, k.footY = 0.15*side*S, -0.85*S
151 k.torX, k.torAY, k.torBY = 0.09*side*S, 0.05*S, 0.46*S
152 k.armAX, k.armAY = k.sx*S, kShoulderY*S
153 k.legR, k.torR, k.armR, k.gloveR = 0.08*S, 0.14*S, 0.055*S, kGloveR
154 k.legH, k.torH, k.armH, k.gloveH = k.legR+kMarginLeg, k.torR+kMarginTorso, k.armR+kMarginArm, k.gloveR+kMarginGlove
155 if side < 0 {
156 k.legPart, k.armPart, k.glovePart = partLeftLeg, partLeftArm, partLeftGlove
157 } else {
158 k.legPart, k.armPart, k.glovePart = partRightLeg, partRightArm, partRightGlove
159 }
160 e.body[i] = k
161 }
162 e.headX, e.headY = zero*S, 0.72*S
163 e.headR = 0.12 * S
164 e.headH = e.headR + kMarginHead
165 e.zeroZ = zero * S
166}
167
168// wpt is the browser's w() for a point already scaled: X = x*S, Y = y*S,
169// Z = z*S.
170func wpt(pos vec, ln lean2, X, Y, Z float64) vec {
171 return vec{pos.x + X*ln.c + Y*ln.s, pos.y - X*ln.s + Y*ln.c, pos.z + Z}
172}
173
174// initCaps sets what never changes in the keeper's collision shapes: radii,
175// kinds and parts, in the browser's order (left leg, body, left arm, left
176// glove, the same on the right, head).
177func (e *engine) initCaps(cs *caps) {
178 for i := 0; i < 2; i++ {
179 k := &e.body[i]
180 j := 4 * i
181 cs[j] = capsule{r: k.legR, hit: k.legH, kind: kindLeg, part: k.legPart}
182 cs[j+1] = capsule{r: k.torR, hit: k.torH, kind: kindTorso, part: partBody}
183 cs[j+2] = capsule{r: k.armR, hit: k.armH, kind: kindArm, part: k.armPart}
184 cs[j+3] = capsule{r: k.gloveR, hit: k.gloveH, kind: kindGlove, part: k.glovePart}
185 }
186 cs[8] = capsule{r: e.headR, hit: e.headH, kind: kindHead, part: partHead}
187}
188
189// capsulesInto sets the end points of the shapes in cs (see initCaps) for a
190// pose: the browser's keeperCapsules, each point wpt written out.
191func (e *engine) capsulesInto(cs *caps, ps *pose) {
192 S := e.scale
193 px, py, pz := ps.pos.x, ps.pos.y, ps.pos.z
194 s, c := ps.lean.s, ps.lean.c
195 z0 := pz + e.zeroZ
196 pl := &ps.plant
197 for i := 0; i < 2; i++ {
198 k := &e.body[i]
199 arm := &ps.arms.r
200 if k.side < 0 {
201 arm = &ps.arms.l
202 }
203 ax, ay, az := arm.x, arm.y, arm.z
204 j := 4 * i
205 cp := &cs[j]
206 X, Y := k.legX, k.legY
207 cp.a = vec{px + X*c + Y*s, py - X*s + Y*c, z0}
208 if pl.on && pl.side == k.side {
209 cp.b = vec{pl.x, pl.y, pl.z}
210 } else {
211 X, Y = k.footX, k.footY
212 cp.b = vec{px + X*c + Y*s, py - X*s + Y*c, z0}
213 }
214 cp = &cs[j+1]
215 X, Y = k.torX, k.torAY
216 cp.a = vec{px + X*c + Y*s, py - X*s + Y*c, z0}
217 Y = k.torBY
218 cp.b = vec{px + X*c + Y*s, py - X*s + Y*c, z0}
219 cp = &cs[j+2]
220 X, Y = k.armAX, k.armAY
221 cp.a = vec{px + X*c + Y*s, py - X*s + Y*c, z0}
222 X, Y = (k.sx+ax*kArmLen)*S, (kShoulderY+ay*kArmLen)*S
223 cp.b = vec{px + X*c + Y*s, py - X*s + Y*c, pz + az*kArmLen*S}
224 cp = &cs[j+3]
225 X, Y = (k.sx+ax*kGloveReach)*S, (kShoulderY+ay*kGloveReach)*S
226 g := vec{px + X*c + Y*s, py - X*s + Y*c, pz + az*kGloveReach*S}
227 cp.a, cp.b = g, g
228 }
229 X, Y := e.headX, e.headY
230 h := vec{px + X*c + Y*s, py - X*s + Y*c, z0}
231 cs[8].a, cs[8].b = h, h
232}
233
234func (e *engine) keeperCaps(k *keeper) caps {
235 var cs caps
236 e.initCaps(&cs)
237 ps := poseOf(k)
238 e.capsulesInto(&cs, &ps)
239 return cs
240}
241
242// clearance is keeperClearance: the hip height at which the lowest of the
243// legs, torso and head touches the ground (the browser builds every shape at
244// the origin; only these heights matter).
245func (e *engine) clearance(k *keeper) float64 {
246 s, c := k.lean.s, k.lean.c
247 // The browser's Math.min over these heights, in its order, signed zeros
248 // included: jsMin only when two are equal.
249 l := &e.body[0]
250 r := &e.body[1]
251 m := 0 - l.legX*s + l.legY*c - l.legR
252 if v := 0 - l.footX*s + l.footY*c - l.legR; v < m {
253 m = v
254 } else if v == m {
255 m = jsMin(m, v)
256 }
257 if v := 0 - l.torX*s + l.torAY*c - l.torR; v < m {
258 m = v
259 } else if v == m {
260 m = jsMin(m, v)
261 }
262 if v := 0 - l.torX*s + l.torBY*c - l.torR; v < m {
263 m = v
264 } else if v == m {
265 m = jsMin(m, v)
266 }
267 if v := 0 - r.legX*s + r.legY*c - r.legR; v < m {
268 m = v
269 } else if v == m {
270 m = jsMin(m, v)
271 }
272 if v := 0 - r.footX*s + r.footY*c - r.legR; v < m {
273 m = v
274 } else if v == m {
275 m = jsMin(m, v)
276 }
277 if v := 0 - r.torX*s + r.torAY*c - r.torR; v < m {
278 m = v
279 } else if v == m {
280 m = jsMin(m, v)
281 }
282 if v := 0 - r.torX*s + r.torBY*c - r.torR; v < m {
283 m = v
284 } else if v == m {
285 m = jsMin(m, v)
286 }
287 if v := 0 - e.headX*s + e.headY*c - e.headR; v < m {
288 m = v
289 } else if v == m {
290 m = jsMin(m, v)
291 }
292 return -m
293}
294
295func (e *engine) armsAboveGround(k *keeper) {
296 s, c := k.lean.s, k.lean.c
297 sy := e.shY
298 reach := e.gloveReachS
299 py := k.pos.y
300 syc := sy * c
301 // Left arm, then right, as the browser's loop over the sides.
302 shoulderY := py - e.shXL*s + syc
303 minDy := (kGloveR - shoulderY) / reach
304 if minDy < 1 {
305 arm := &k.arms.l
306 if -arm.x*s+arm.y*c < minDy {
307 liftArm(arm, minDy, s, c)
308 }
309 }
310 shoulderY = py - e.shXR*s + syc
311 minDy = (kGloveR - shoulderY) / reach
312 if minDy < 1 {
313 arm := &k.arms.r
314 if -arm.x*s+arm.y*c < minDy {
315 liftArm(arm, minDy, s, c)
316 }
317 }
318}
319
320// liftArm raises an arm (body frame) whose hand would be below the ground
321// to world height minDy, keeping its direction across.
322func liftArm(arm *vec, minDy, s, c float64) {
323 wx := arm.x*c + arm.y*s
324 h0 := fsqrt(wx*wx + arm.z*arm.z)
325 h := fsqrt(1 - minDy*minDy)
326 nx, nz := h, 0.0
327 if h0 > 0 {
328 nx = (wx * h) / h0
329 nz = (arm.z * h) / h0
330 }
331 *arm = vec{nx*c - minDy*s, nx*s + minDy*c, nz}
332}
333
334// pose is what the keeper's shapes are built from.
335type pose struct {
336 pos vec
337 lean lean2
338 arms armPair
339 plant plant
340}
341
342func poseOf(k *keeper) pose { return pose{k.pos, k.lean, k.arms, k.plant} }
343
344// blocks is the keeper's movement at 50 Hz: blocks of keeperBlock steps,
345// each one keeper step of that length taken at the block's start, from pose
346// A (the block's start) to pose B (its end). A block in which its plan
347// commits is a single millisecond instead, so the reaction time keeps 1 ms
348// precision. Within a block the shapes the ball meets are interpolated
349// between A's and B's, one per millisecond, at the block's constant velocity.
350type blocks struct {
351 s0, n int
352 ia int // pose[ia] is pose A (the block's start), pose[1-ia] pose B
353 pose [2]pose
354 cbuf [2]caps // the shapes of each pose
355 ok [2]bool // whether cbuf holds them
356 vel [nCaps][2]vec
357 velOK [nCaps]bool
358 box [nCaps][6]float64 // lo/hi x, y, z of each part's end points over the block
359 boxOK [nCaps]bool
360 lastBox [nCaps][6]float64 // the same for pose B alone (the block's last step)
361 lastBoxOK [nCaps]bool
362 body [6]float64 // every part over the block, widened by maxReach
363 bodyOK bool
364 cand [nCaps]bool // parts that can reach a ball within candR of candP this block
365 candP vec
366 candOK bool
367}
368
369func blockLen(k *keeper, pl *Plan, step int) int {
370 if k.phase != phaseReady {
371 return keeperBlock
372 }
373 if k.holding {
374 return keeperBlock
375 }
376 if pl.commitTime() <= float64(step+keeperBlock-1)*simDT {
377 return 1
378 }
379 return keeperBlock
380}
381
382func (e *engine) keeperBlock(k *keeper, pl *Plan, step int, b *ball, kb *blocks) {
383 kb.s0 = step
384 kb.n = blockLen(k, pl, step)
385 kb.ia = 1 - kb.ia // B becomes A, with its shapes
386 kb.velOK = [nCaps]bool{}
387 kb.boxOK = [nCaps]bool{}
388 kb.lastBoxOK = [nCaps]bool{}
389 kb.bodyOK = false
390 kb.candOK = false
391 end := step + kb.n - 1
392 e.keeperStep(k, pl, end, float64(end)*simDT, float64(kb.n)*simDT, b)
393 kb.pose[1-kb.ia] = poseOf(k)
394 kb.ok[1-kb.ia] = false
395}
396
397func (e *engine) blockCapsA(kb *blocks) *caps {
398 i := kb.ia
399 if !kb.ok[i] {
400 e.capsulesInto(&kb.cbuf[i], &kb.pose[i])
401 kb.ok[i] = true
402 }
403 return &kb.cbuf[i]
404}
405
406func (e *engine) blockCapsB(kb *blocks) *caps {
407 i := 1 - kb.ia
408 if !kb.ok[i] {
409 e.capsulesInto(&kb.cbuf[i], &kb.pose[i])
410 kb.ok[i] = true
411 }
412 return &kb.cbuf[i]
413}
414
415// blockCap is shape i num steps into the block (num = n is pose B).
416func (e *engine) blockCap(kb *blocks, i, num int) capsule {
417 b := e.blockCapsB(kb)[i]
418 if num == kb.n {
419 return b
420 }
421 a := &e.blockCapsA(kb)[i]
422 f := float64(num) / float64(kb.n)
423 b.a = lerpV(a.a, b.a, f)
424 b.b = lerpV(a.b, b.b, f)
425 return b
426}
427
428func (e *engine) blockCaps(kb *blocks, num int) caps {
429 if num == kb.n {
430 return *e.blockCapsB(kb)
431 }
432 var cs caps
433 for i := range cs {
434 cs[i] = e.blockCap(kb, i, num)
435 }
436 return cs
437}
438
439// blockVel is the velocity of shape i's end points during the block.
440func (e *engine) blockVel(kb *blocks, i int) (vec, vec) {
441 if !kb.velOK[i] {
442 a := &e.blockCapsA(kb)[i]
443 b := &e.blockCapsB(kb)[i]
444 inv := 1 / (float64(kb.n) * simDT)
445 kb.vel[i] = [2]vec{scale(sub(b.a, a.a), inv), scale(sub(b.b, a.b), inv)}
446 kb.velOK[i] = true
447 }
448 return kb.vel[i][0], kb.vel[i][1]
449}
450
451// blockBox bounds part i over the rest of the block: its end points at the
452// block's start and end (every interpolated position lies between them), or
453// at the end alone on the block's last step, as the browser's outsideBox4.
454func (e *engine) blockBox(kb *blocks, i, num int) *[6]float64 {
455 if num == kb.n {
456 if !kb.lastBoxOK[i] {
457 c := &e.blockCapsB(kb)[i]
458 boxInto(&kb.lastBox[i], c, c)
459 kb.lastBoxOK[i] = true
460 }
461 return &kb.lastBox[i]
462 }
463 if !kb.boxOK[i] {
464 boxInto(&kb.box[i], &e.blockCapsA(kb)[i], &e.blockCapsB(kb)[i])
465 kb.boxOK[i] = true
466 }
467 return &kb.box[i]
468}
469
470// candSlack is how far the ball may move before the candidate parts are
471// worked out again.
472var candSlack = 0.1
473
474// blockCands lists the parts whose swept box over the block, widened by the
475// most that part can reach (collision radius, slow-ball reaction, late
476// hands, the ball, a centimetre), comes within candSlack of the ball: no
477// other part can pass the per-part test while the ball stays within
478// candSlack of where the list was made, so the list is reused until then.
479func (e *engine) blockCands(kb *blocks, p vec) *[nCaps]bool {
480 if kb.candOK {
481 q := kb.candP
482 if p.x-q.x >= candSlack {
483 } else if q.x-p.x >= candSlack {
484 } else if p.y-q.y >= candSlack {
485 } else if q.y-p.y >= candSlack {
486 } else if p.z-q.z >= candSlack {
487 } else if q.z-p.z >= candSlack {
488 } else {
489 return &kb.cand
490 }
491 }
492 cb := e.blockCapsB(kb)
493 px, py, pz := p.x, p.y, p.z
494 for i := 0; i < nCaps; i++ {
495 bx := e.blockBox(kb, i, 0) // start and end poses: covers the last step's box too
496 c := &cb[i]
497 m := ballR + c.hit + kReactReach + 0.01 + candSlack
498 if c.kind == kindGlove {
499 m += kHandLateMax
500 }
501 in := false
502 if px < bx[0]-m {
503 } else if px > bx[1]+m {
504 } else if py < bx[2]-m {
505 } else if py > bx[3]+m {
506 } else if pz < bx[4]-m {
507 } else if pz > bx[5]+m {
508 } else {
509 in = true
510 }
511 kb.cand[i] = in
512 }
513 kb.candP = p
514 kb.candOK = true
515 return &kb.cand
516}
517
518// blockBody bounds every part over the block (start and end poses), widened
519// by the most any part reaches (e.maxReach).
520func (e *engine) blockBody(kb *blocks) *[6]float64 {
521 if !kb.bodyOK {
522 bx := *e.blockBox(kb, 0, 0)
523 for i := 1; i < nCaps; i++ {
524 o := e.blockBox(kb, i, 0)
525 if o[0] < bx[0] {
526 bx[0] = o[0]
527 }
528 if o[1] > bx[1] {
529 bx[1] = o[1]
530 }
531 if o[2] < bx[2] {
532 bx[2] = o[2]
533 }
534 if o[3] > bx[3] {
535 bx[3] = o[3]
536 }
537 if o[4] < bx[4] {
538 bx[4] = o[4]
539 }
540 if o[5] > bx[5] {
541 bx[5] = o[5]
542 }
543 }
544 m := e.maxReach
545 kb.body = [6]float64{bx[0] - m, bx[1] + m, bx[2] - m, bx[3] + m, bx[4] - m, bx[5] + m}
546 kb.bodyOK = true
547 }
548 return &kb.body
549}
550
551// boxInto sets bx to the box of c0's and c1's end points: per axis the
552// least and greatest of the four, found in pairs. (The browser keeps a
553// running min and max; the two differ at most in the sign of a zero, which
554// no comparison against the box can see.)
555func boxInto(bx *[6]float64, c0, c1 *capsule) {
556 a, b, c, d := c0.a, c0.b, c1.a, c1.b
557 lo, hi := a.x, b.x
558 if hi < lo {
559 lo, hi = hi, lo
560 }
561 l2, h2 := c.x, d.x
562 if h2 < l2 {
563 l2, h2 = h2, l2
564 }
565 if l2 < lo {
566 lo = l2
567 }
568 if h2 > hi {
569 hi = h2
570 }
571 bx[0], bx[1] = lo, hi
572 lo, hi = a.y, b.y
573 if hi < lo {
574 lo, hi = hi, lo
575 }
576 l2, h2 = c.y, d.y
577 if h2 < l2 {
578 l2, h2 = h2, l2
579 }
580 if l2 < lo {
581 lo = l2
582 }
583 if h2 > hi {
584 hi = h2
585 }
586 bx[2], bx[3] = lo, hi
587 lo, hi = a.z, b.z
588 if hi < lo {
589 lo, hi = hi, lo
590 }
591 l2, h2 = c.z, d.z
592 if h2 < l2 {
593 l2, h2 = h2, l2
594 }
595 if l2 < lo {
596 lo = l2
597 }
598 if h2 > hi {
599 hi = h2
600 }
601 bx[4], bx[5] = lo, hi
602}
603
604func lerpV(p, q vec, f float64) vec {
605 return vec{p.x + (q.x-p.x)*f, p.y + (q.y-p.y)*f, p.z + (q.z-p.z)*f}
606}
607
608// predictCrossing: where the keeper expects the ball to cross z = zk.
609// (ballSpeed written out.)
610func predictCrossing(b *ball, zk float64) pred {
611 ux, uz := b.v.x, b.v.z
612 if uz >= -0.5 {
613 return pred{}
614 }
615 dz := zk - b.p.z
616 if dz >= 0 {
617 return pred{}
618 }
619 uy := b.v.y
620 sh := fsqrt(ux*ux + uz*uz)
621 s2 := ux*ux + uy*uy + uz*uz
622 sp := b.sp
623 if q := sp; !(q > 0) {
624 sp = fsqrt(s2)
625 } else if d, lim := s2-q*q, 0.01*s2; d > lim {
626 sp = fsqrt(s2)
627 } else if d < -lim {
628 sp = fsqrt(s2)
629 } else {
630 sp = 0.5 * (q + s2/q)
631 }
632 dist := (dz / uz) * sh
633 grounded := b.grounded
634 roll := 0.0
635 if grounded {
636 roll = ballRollDecel
637 }
638 decel := dragK*sp*sh + roll
639 tau := dist / sh
640 if decel > 1e-9 {
641 disc := sh*sh - 2*decel*dist
642 if disc <= 0 {
643 return pred{}
644 }
645 tau = (sh - fsqrt(disc)) / decel
646 }
647 y := ballR
648 if !grounded {
649 if y = b.p.y + uy*tau - 0.5*simG*tau*tau; y < ballR {
650 y = ballR
651 }
652 }
653 return pred{ok: true, x: b.p.x + (ux/sh)*dist, y: y, tau: tau}
654}
655
656func (e *engine) readLaunch(k *keeper, pr pred) vec {
657 S := e.scale
658 dx := pr.x - k.pos.x
659 sgn := 1.0
660 ax := dx
661 if dx < 0 {
662 sgn = -1
663 ax = -dx
664 }
665 T := jsMax(0.15, pr.tau-kPushTime*0.5)
666 need := jsMax(0, ax-kReadReach*S)
667 vx := jsMin(e.p.DiveMax, need/T)
668 if pr.y < 0.6*S && ax > 0.2 {
669 vx = jsMax(vx, kLowDropSpeed)
670 }
671 jump := clamp((pr.y-0.9*S)*kReadHeightGn, e.p.JumpMin, e.p.JumpMax)
672 return vec{sgn * vx, jump, 0.12 * vx}
673}
674
675func diveArms(side float64, jump float64, ln lean2) armPair {
676 toBody := func(wx, wy float64) vec {
677 return norm(vec{wx*ln.c - wy*ln.s, wx*ln.s + wy*ln.c, 0.1})
678 }
679 reach := (jump - kReachPivot) / kReachSpan
680 lead := toBody(side, reach)
681 trail := toBody(side, reach+0.45)
682 if side > 0 {
683 return armPair{trail, lead}
684 }
685 return armPair{lead, trail}
686}
687
688func (e *engine) commitLaunch(k *keeper, step int, t float64, lv vec, read bool) {
689 k.phase = phasePush
690 k.pushT = 0
691 k.groundT = 0
692 k.launchVel = lv
693 lx := lv.x
694 ly := jsMax(kLeanMin, lv.y+kLeanBias)
695 l := fsqrt(lx*lx + ly*ly)
696 k.leanTarget = lean2{lx / l, ly / l}
697 ax := lx
698 if lx < 0 {
699 ax = -lx
700 }
701 star := ax < kStarSpeed && lv.y > 0.5
702 if star {
703 k.armsTarget = armsStar
704 } else {
705 side := -1.0
706 if lx > 0 {
707 side = 1
708 }
709 k.armsTarget = diveArms(side, lv.y, k.leanTarget)
710 }
711 k.commits = append(k.commits, Commit{Step: int(jsRound(t / simDT)), T0: t, VX: lx, Jump: lv.y, Star: star, Read: read})
712 if ax >= kStarSpeed {
713 side := 1.0
714 bk := &e.body[1]
715 if lx > 0 {
716 side = -1
717 bk = &e.body[0]
718 }
719 // That side's foot, as the browser takes it from keeperCapsules.
720 foot := wpt(k.pos, k.lean, bk.footX, bk.footY, e.zeroZ)
721 k.plant = plant{on: true, side: side, x: foot.x, y: foot.y, z: foot.z}
722 k.plantUntil = t + kPushTime + kPlantExtra
723 }
724}
725
726// trackArms steers both hands at the predicted crossing (body frame). The
727// arithmetic is the browser's aim() and norm(), written out.
728func (e *engine) trackArms(k *keeper, pr pred) {
729 s, c := k.lean.s, k.lean.c
730 sy := e.shY
731 dz, dz2 := e.aimDZ, e.aimDZ2
732 px, py := k.pos.x, k.pos.y
733 sys, syc := sy*s, sy*c
734 // Left hand, then right.
735 sx := e.shXL
736 wx := pr.x - (px + sx*c + sys)
737 wy := pr.y - (py - sx*s + syc)
738 var dx, dy, dd float64
739 if l := fsqrt(wx*wx + wy*wy + dz2); l > 0 {
740 inv := 1 / l
741 dx, dy, dd = wx*inv, wy*inv, dz*inv
742 }
743 k.armsTarget.l = vec{dx*c - dy*s, dx*s + dy*c, dd}
744 sx = e.shXR
745 wx = pr.x - (px + sx*c + sys)
746 wy = pr.y - (py - sx*s + syc)
747 dx, dy, dd = 0, 0, 0
748 if l := fsqrt(wx*wx + wy*wy + dz2); l > 0 {
749 inv := 1 / l
750 dx, dy, dd = wx*inv, wy*inv, dz*inv
751 }
752 k.armsTarget.r = vec{dx*c - dy*s, dx*s + dy*c, dd}
753}
754
755func lerpRange(r [2]float64, f float64) float64 { return r[0] + (r[1]-r[0])*f }
756
757func (e *engine) diveEarliness(reaction float64) float64 {
758 return clamp((e.p.ReactionMax-reaction)/(e.p.ReactionMax-e.p.ReactionMin), 0, 1)
759}
760
761func (e *engine) misjudge(k *keeper, lv vec, reaction float64) vec {
762 if !k.mis.on {
763 return lv
764 }
765 f := e.diveEarliness(reaction)
766 vx := clamp(lv.x+k.mis.vx*lerpRange(kMisjudgeVX, f), -e.p.DiveMax, e.p.DiveMax)
767 jump := clamp(lv.y+k.mis.jump*lerpRange(kMisjudgeJump, f), e.p.JumpMin, e.p.JumpMax)
768 return vec{vx, jump, lv.z}
769}
770
771// readIdle reports whether nothing in a keeper step at t can act on the
772// keeper's read: before kPerceive the hands do not track it, a keeper
773// waiting to dive or diving does not use it, and one waiting to read uses it
774// only from kReadBase + its reaction.
775func readIdle(k *keeper, pl *Plan, t float64) bool {
776 if t >= kPerceive {
777 return false
778 }
779 switch k.phase {
780 case phaseReady:
781 return pl.Mode != ModeRead || t < kReadBase+pl.Reaction
782 case phasePush, phaseAir:
783 return true
784 }
785 return false
786}
787
788// perceive is the keeper's read of a predicted crossing at t: off by its
789// misread, more the earlier it reads.
790func (e *engine) perceive(k *keeper, pr pred, t float64) pred {
791 if !pr.ok || !k.mis.on {
792 return pr
793 }
794 f := clamp((e.misT1-t)/e.misTd, 0, 1)
795 y := pr.y + k.mis.y*(e.misY0+e.misYd*f)
796 if y < ballR {
797 y = ballR
798 }
799 return pred{ok: true, x: pr.x + k.mis.x*(e.misX0+e.misXd*f), y: y, tau: pr.tau}
800}
801
802func (e *engine) keeperStep(k *keeper, pl *Plan, step int, t, dt float64, b *ball) {
803 // It reads the ball every kReadEvery steps and acts on its latest read
804 // in between; once it holds the ball it stops reading.
805 fresh := false
806 if !k.holding && step >= k.nextRead {
807 k.nextRead = step + kReadEvery
808 fresh = true
809 if readIdle(k, pl, t) {
810 // Nothing acts on this read before the next one (see readIdle):
811 // keep what the keeper saw, worked out only if something does.
812 k.seen, k.seenZ, k.seenT, k.pending = *b, k.pos.z, t, true
813 } else {
814 k.read = e.perceive(k, predictCrossing(b, k.pos.z), t)
815 k.pending = false
816 }
817 }
818 var pr pred
819 if !k.holding {
820 if k.pending && !readIdle(k, pl, t) {
821 k.read = e.perceive(k, predictCrossing(&k.seen, k.seenZ), k.seenT)
822 k.pending = false
823 }
824 pr = k.read
825 } else {
826 k.armsTarget = armsHold
827 }
828
829 if k.phase == phaseReady && !k.holding {
830 if pl.Mode == ModeRead {
831 if t >= kReadBase+pl.Reaction {
832 if pr.ok && pr.tau <= kReadShuffleT {
833 e.commitLaunch(k, step, t, e.readLaunch(k, pr), true)
834 } else {
835 k.phase = phaseRecover
836 }
837 }
838 } else if t >= pl.Reaction {
839 e.commitLaunch(k, step, t, e.misjudge(k, launchVelocity(pl), pl.Reaction), false)
840 }
841 }
842 if fresh && t >= kPerceive && pr.ok {
843 e.trackArms(k, pr)
844 }
845
846 leanRate := kLeanRate
847 switch k.phase {
848 case phasePush:
849 k.pushT += dt
850 f := jsMin(1, k.pushT/kPushTime)
851 lv := k.launchVel
852 k.vel = vec{lv.x * f, lv.y*f - simG*k.pushT, lv.z * f}
853 if f >= 1 {
854 k.phase = phaseAir
855 }
856 case phaseAir:
857 k.vel.y -= simG * dt
858 case phaseGround:
859 sp := fsqrt(k.vel.x*k.vel.x + k.vel.z*k.vel.z)
860 sc := 0.0
861 if sp > 0 {
862 sc = jsMax(0, sp-kSlideDecel*dt) / sp
863 }
864 k.vel = vec{k.vel.x * sc, 0, k.vel.z * sc}
865 k.groundT += dt
866 if k.groundT >= kRecoverDelay && !(pr.ok && pr.tau < kHoldDownT) {
867 k.phase = phaseRecover
868 k.leanTarget = lean2{0, 1}
869 }
870 case phaseRecover:
871 leanRate = kRecoverLeanRt
872 vx := k.vel.x
873 vz := 0.0
874 if k.lean.c > 0.35 {
875 tx := k.pos.x
876 if pr.ok {
877 tx = clamp(pr.x, -e.p.GoalHalfWidth, e.p.GoalHalfWidth)
878 }
879 top := kRecoverSpeed * k.lean.c
880 want := clamp((tx-k.pos.x)*5, -top, top)
881 vx += clamp(want-vx, -kRecoverAccel*dt, kRecoverAccel*dt)
882 vz = clamp((goalZ+kLineOffset-k.pos.z)*3, -1.5, 1.5)
883 } else {
884 vx = 0
885 }
886 k.vel = vec{vx, 0, vz}
887 if len(k.commits) < kMaxCommits && k.lean.c > 0.55 && pr.ok && pr.tau < kRediveWindow {
888 dx := pr.x - k.pos.x
889 if dx > kRediveMinDX || dx < -kRediveMinDX || pr.y < 0.45 || pr.y > 1.9 {
890 e.commitLaunch(k, step, t, e.readLaunch(k, pr), true)
891 }
892 }
893 }
894 kp, kv := &k.pos, &k.vel
895 kp.x += kv.x * dt
896 kp.y += kv.y * dt
897 kp.z += kv.z * dt
898 fl := leanRate * dt
899 if fl >= 1 {
900 fl = 1 // jsMin(1, fl) for a positive rate
901 }
902 ln, lt := &k.lean, &k.leanTarget
903 ls, lc := ln.s+(lt.s-ln.s)*fl, ln.c+(lt.c-ln.c)*fl
904 // nlerp2: three Newton steps towards unit length, as the browser.
905 g := 1.5 - 0.5*(ls*ls+lc*lc)
906 ls, lc = ls*g, lc*g
907 g = 1.5 - 0.5*(ls*ls+lc*lc)
908 ls, lc = ls*g, lc*g
909 g = 1.5 - 0.5*(ls*ls+lc*lc)
910 ln.s, ln.c = ls*g, lc*g
911 rate := kArmRate
912 if t >= kPerceive {
913 rate = kArmTrackRate
914 }
915 fa := rate * dt
916 if fa >= 1 {
917 fa = 1
918 }
919 // blendUnit for each arm, written out. Each arm is replaced whole, not
920 // updated field by field: the arms start as copies of armsReady, and a
921 // gno struct copy shares its nested structs, so writing a.x in place
922 // would write to this package's armsReady (which a realm may not do).
923 a, b2 := &k.arms.l, &k.armsTarget.l
924 x, y, z := a.x+(b2.x-a.x)*fa, a.y+(b2.y-a.y)*fa, a.z+(b2.z-a.z)*fa
925 g = 1.5 - 0.5*(x*x+y*y+z*z)
926 x, y, z = x*g, y*g, z*g
927 g = 1.5 - 0.5*(x*x+y*y+z*z)
928 x, y, z = x*g, y*g, z*g
929 g = 1.5 - 0.5*(x*x+y*y+z*z)
930 *a = vec{x * g, y * g, z * g}
931 a, b2 = &k.arms.r, &k.armsTarget.r
932 x, y, z = a.x+(b2.x-a.x)*fa, a.y+(b2.y-a.y)*fa, a.z+(b2.z-a.z)*fa
933 g = 1.5 - 0.5*(x*x+y*y+z*z)
934 x, y, z = x*g, y*g, z*g
935 g = 1.5 - 0.5*(x*x+y*y+z*z)
936 x, y, z = x*g, y*g, z*g
937 g = 1.5 - 0.5*(x*x+y*y+z*z)
938 *a = vec{x * g, y * g, z * g}
939
940 // The clearance depends on the lean alone (cached), and no lean puts it
941 // above the keeper's scale: with the hips higher than that it cannot
942 // matter.
943 grounded := false
944 if k.phase == phaseGround {
945 grounded = true
946 } else if k.phase == phaseRecover {
947 grounded = true
948 }
949 clr := negInf
950 if grounded || k.pos.y <= e.scale {
951 stale := true
952 if !k.clrOK {
953 } else if k.clrS != k.lean.s {
954 } else if k.clrC != k.lean.c {
955 } else {
956 stale = false
957 }
958 if stale {
959 k.clr = e.clearance(k)
960 k.clrS, k.clrC, k.clrOK = k.lean.s, k.lean.c, true
961 }
962 clr = k.clr
963 }
964 if grounded || k.pos.y < clr {
965 k.pos = vec{k.pos.x, clr, k.pos.z}
966 if k.phase == phaseAir {
967 if k.vel.y <= 0 {
968 k.phase = phaseGround
969 k.groundT = 0
970 }
971 }
972 if grounded {
973 k.vel = vec{k.vel.x, 0, k.vel.z}
974 } else if k.vel.y < 0 {
975 k.vel = vec{k.vel.x, 0, k.vel.z}
976 }
977 }
978 e.armsAboveGround(k)
979
980 if k.plant.on {
981 S := e.scale
982 hx := k.pos.x + 0.11*k.plant.side*S*k.lean.c
983 hy := k.pos.y - 0.11*k.plant.side*S*k.lean.s
984 dx := k.plant.x - hx
985 dy := k.plant.y - hy
986 maxL := 0.85 * S * kPlantStretch
987 if t > k.plantUntil || dx*dx+dy*dy > maxL*maxL {
988 k.plant = plant{}
989 }
990 }
991}