Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}